diff --git a/doc/src/sgml/ref/create_view.sgmlin b/doc/src/sgml/ref/create_view.sgmlin index 6796e4fc1..168f58b4d 100644 --- a/doc/src/sgml/ref/create_view.sgmlin +++ b/doc/src/sgml/ref/create_view.sgmlin @@ -12,7 +12,8 @@ CREATE [ OR REPLACE ] [DEFINER = user] [ TEMP | TEMPORARY ] VIEW view_name [ ( column_name [, ...] ) ] [ WITH ( {view_option_name [= view_option_value]} [, ... ] ) ] - AS query; + AS query + [ WITH [ CASCADED | LOCAL ] CHECK OPTION ]; \ No newline at end of file diff --git a/src/bin/pg_dump/pg_dump.cpp b/src/bin/pg_dump/pg_dump.cpp index 888796643..5a28d9acd 100644 --- a/src/bin/pg_dump/pg_dump.cpp +++ b/src/bin/pg_dump/pg_dump.cpp @@ -6546,6 +6546,7 @@ TableInfo* getTables(Archive* fout, int* numTables) #endif int i_reltablespace = 0; int i_reloptions = 0; + int i_checkoption = 0; int i_toastreloptions = 0; int i_reloftype = 0; int i_parttype = 0; @@ -6653,7 +6654,9 @@ TableInfo* getTables(Archive* fout, int* numTables) "(SELECT pg_catalog.string_agg(node_name,',') AS pgxc_node_names from pgxc_node n where n.oid " "in (select pg_catalog.unnest(nodeoids) from pgxc_class v where v.pcrelid=c.oid) ) , " #endif - "pg_catalog.array_to_string(c.reloptions, ', ') AS reloptions, " + "pg_catalog.array_to_string(pg_catalog.array_remove(pg_catalog.array_remove(c.reloptions,'check_option=local'),'check_option=cascaded'), ', ') AS reloptions, " + "CASE WHEN 'check_option=local' = ANY (c.reloptions) THEN 'LOCAL'::text " + "WHEN 'check_option=cascaded' = ANY (c.reloptions) THEN 'CASCADED'::text ELSE NULL END AS checkoption, " "pg_catalog.array_to_string(array(SELECT 'toast.' || " "x FROM pg_catalog.unnest(tc.reloptions) x), ', ') AS toast_reloptions " "FROM pg_class c " @@ -6703,7 +6706,9 @@ TableInfo* getTables(Archive* fout, int* numTables) "(SELECT pg_catalog.string_agg(node_name,',') AS pgxc_node_names from pgxc_node n where n.oid " "in (select pg_catalog.unnest(nodeoids) from pgxc_class v where v.pcrelid=c.oid) ) , " #endif - "pg_catalog.array_to_string(c.reloptions, ', ') AS reloptions, " + "pg_catalog.array_to_string(pg_catalog.array_remove(pg_catalog.array_remove(c.reloptions,'check_option=local'),'check_option=cascaded'), ', ') AS reloptions, " + "CASE WHEN 'check_option=local' = ANY (c.reloptions) THEN 'LOCAL'::text " + "WHEN 'check_option=cascaded' = ANY (c.reloptions) THEN 'CASCADED'::text ELSE NULL END AS checkoption, " "pg_catalog.array_to_string(array(SELECT 'toast.' || " "x FROM pg_catalog.unnest(tc.reloptions) x), ', ') AS toast_reloptions " "FROM pg_class c " @@ -7056,6 +7061,7 @@ TableInfo* getTables(Archive* fout, int* numTables) #endif i_reltablespace = PQfnumber(res, "reltablespace"); i_reloptions = PQfnumber(res, "reloptions"); + i_checkoption = PQfnumber(res, "checkoption"); i_toastreloptions = PQfnumber(res, "toast_reloptions"); i_reloftype = PQfnumber(res, "reloftype"); @@ -7227,6 +7233,10 @@ TableInfo* getTables(Archive* fout, int* numTables) free(tmp_str); tmp_str = NULL; } + if (i_checkoption == -1 || PQgetisnull(res, i, i_checkoption)) + tblinfo[i].checkoption = NULL; + else + tblinfo[i].checkoption = gs_strdup(PQgetvalue(res, i, i_checkoption)); tblinfo[i].toast_reloptions = gs_strdup(PQgetvalue(res, i, i_toastreloptions)); /* other fields were zeroed above */ @@ -18176,7 +18186,14 @@ static void dumpViewSchema( appendPQExpBuffer(q, " VIEW %s(%s)", fmtId(tbinfo->dobj.name), schemainfo); if ((tbinfo->reloptions != NULL) && strlen(tbinfo->reloptions) > 0) appendPQExpBuffer(q, " WITH (%s)", tbinfo->reloptions); - appendPQExpBuffer(q, " AS\n %s\n", viewdef); + appendPQExpBuffer(q, " AS\n "); + + Assert(viewdef[strlen(viewdef) - 1] == ';'); + appendBinaryPQExpBuffer(q, viewdef, strlen(viewdef) - 1); + + if (tbinfo->checkoption != NULL) + appendPQExpBuffer(q, "\n WITH %s CHECK OPTION", tbinfo->checkoption); + appendPQExpBuffer(q, ";\n"); appendPQExpBuffer(labelq, "VIEW %s", fmtId(tbinfo->dobj.name)); diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index cc9e4add2..49a1d7fbd 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -216,6 +216,7 @@ typedef struct _tableInfo { char relreplident; /* replica identifier */ char* reltablespace; /* relation tablespace */ char* reloptions; /* options specified by WITH (...) */ + char* checkoption; /* WITH CHECK OPTION */ Oid relbucket; /* relation bucket OID */ char* toast_reloptions; /* ditto, for the TOAST table */ bool hasindex; /* does it have any indexes? */ diff --git a/src/common/backend/catalog/builtin_funcs.ini b/src/common/backend/catalog/builtin_funcs.ini index ae3c772f3..1fef82305 100755 --- a/src/common/backend/catalog/builtin_funcs.ini +++ b/src/common/backend/catalog/builtin_funcs.ini @@ -9226,6 +9226,14 @@ AddFuncGroup( "pg_typeof", 1, AddBuiltinFunc(_0(PGTYPEOFFUNCOID), _1("pg_typeof"), _2(1), _3(false), _4(false), _5(pg_typeof), _6(2206), _7(PG_CATALOG_NAMESPACE), _8(BOOTSTRAP_SUPERUSERID), _9(INTERNALlanguageId), _10(1), _11(0), _12(0), _13(0), _14(false), _15(false), _16(false), _17(false), _18('s'), _19(0), _20(1, 2276), _21(NULL), _22(NULL), _23(NULL), _24(NULL), _25("pg_typeof"), _26(NULL), _27(NULL), _28(NULL), _29(0), _30(false), _31(true), _32(false), _33("type of the argument"), _34('f'), _35(NULL), _36(0), _37(false), _38(NULL), _39(NULL), _40(0)) ), + AddFuncGroup( + "pg_relation_is_updatable", 1, + AddBuiltinFunc(_0(3846), _1("pg_relation_is_updatable"), _2(2), _3(true), _4(false), _5(pg_relation_is_updatable), _6(23), _7(PG_CATALOG_NAMESPACE), _8(BOOTSTRAP_SUPERUSERID), _9(INTERNALlanguageId), _10(1), _11(0), _12(0), _13(0), _14(false), _15(false), _16(false), _17(false), _18('s'), _19(0), _20(2, 26, 16), _21(NULL), _22(NULL), _23(NULL), _24(NULL), _25("pg_relation_is_updatable"), _26(NULL), _27(NULL), _28(NULL), _29(0), _30(false), _31(NULL), _32(false), _33("is a relation insertable/updatable/deletable"), _34('f'), _35(NULL), _36(0), _37(false), _38(NULL), _39(NULL), _40(0)) + ), + AddFuncGroup( + "pg_column_is_updatable", 1, + AddBuiltinFunc(_0(3847), _1("pg_column_is_updatable"), _2(3), _3(true), _4(false), _5(pg_column_is_updatable), _6(16), _7(PG_CATALOG_NAMESPACE), _8(BOOTSTRAP_SUPERUSERID), _9(INTERNALlanguageId), _10(1), _11(0), _12(0), _13(0), _14(false), _15(false), _16(false), _17(false), _18('s'), _19(0), _20(3, 26, 21, 16), _21(NULL), _22(NULL), _23(NULL), _24(NULL), _25("pg_column_is_updatable"), _26(NULL), _27(NULL), _28(NULL), _29(0), _30(false), _31(NULL), _32(false), _33("is a column updatable"), _34('f'), _35(NULL), _36(0), _37(false), _38(NULL), _39(NULL), _40(0)) + ), AddFuncGroup( "pg_user_iostat", 1, AddBuiltinFunc(_0(5013), _1("pg_user_iostat"), _2(1), _3(false), _4(true), _5(pg_stat_get_wlm_user_iostat_info), _6(2249), _7(PG_CATALOG_NAMESPACE), _8(BOOTSTRAP_SUPERUSERID), _9(INTERNALlanguageId), _10(1), _11(100), _12(0), _13(0), _14(false), _15(false), _16(false), _17(false), _18('s'), _19(0), _20(1, 2275), _21(8, 26, 23, 23, 23, 23, 23, 25, 23), _22(8, 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o'), _23(8, "userid", "min_curr_iops", "max_curr_iops", "min_peak_iops", "max_peak_iops", "io_limits", "io_priority", "curr_io_limits"), _24(NULL), _25("pg_stat_get_wlm_user_iostat_info"), _26(NULL), _27(NULL), _28(NULL), _29(0), _30(false), _31(NULL), _32(false), _33(NULL), _34('f'), _35(NULL), _36(0), _37(false), _38(NULL), _39(NULL), _40(0)) diff --git a/src/common/backend/catalog/information_schema.sql b/src/common/backend/catalog/information_schema.sql index 5e1b865c3..6a5b03d6a 100644 --- a/src/common/backend/catalog/information_schema.sql +++ b/src/common/backend/catalog/information_schema.sql @@ -744,9 +744,7 @@ CREATE VIEW columns AS CAST(CASE WHEN ad.adgencol = 's' THEN pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS generation_expression, CAST(CASE WHEN c.relkind = 'r' - OR (c.relkind = 'v' - AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '2' AND is_instead) - AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '4' AND is_instead)) + OR (c.relkind in ('v', 'f') AND pg_column_is_updatable(c.oid, a.attnum, false)) THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_updatable FROM (pg_attribute a LEFT JOIN pg_attrdef ad ON attrelid = adrelid AND attnum = adnum) @@ -1884,9 +1882,10 @@ CREATE VIEW tables AS CAST(nt.nspname AS sql_identifier) AS user_defined_type_schema, CAST(t.typname AS sql_identifier) AS user_defined_type_name, - CAST(CASE WHEN c.relkind = 'r' - OR (c.relkind = 'v' - AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '3' AND is_instead)) + CAST(CASE WHEN c.relkind = 'r' OR + (c.relkind in ('v', 'f') AND + -- 1 << CMD_INSERT + pg_relation_is_updatable(c.oid, false) & 8 = 8) THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, CAST(CASE WHEN t.typname IS NOT NULL THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_typed, @@ -2469,16 +2468,23 @@ CREATE VIEW views AS ELSE null END AS character_data) AS view_definition, - CAST('NONE' AS character_data) AS check_option, + CAST( + CASE WHEN 'check_option=cascaded' = ANY (c.reloptions) + THEN 'CASCADED' + WHEN 'check_option=local' = ANY (c.reloptions) + THEN 'LOCAL' + ELSE 'NONE' END + AS character_data) AS check_option, CAST( - CASE WHEN EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '2' AND is_instead) - AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '4' AND is_instead) + -- (1 << CMD_UPDATE) + (1 << CMD_DELETE) + CASE WHEN pg_relation_is_updatable(c.oid, false) & 20 = 20 THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_updatable, CAST( - CASE WHEN EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '3' AND is_instead) + -- 1 << CMD_INSERT + CASE WHEN pg_relation_is_updatable(c.oid, false) & 8 = 8 THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, diff --git a/src/common/backend/catalog/sql_features.txt b/src/common/backend/catalog/sql_features.txt index cf04390ca..361051aff 100644 --- a/src/common/backend/catalog/sql_features.txt +++ b/src/common/backend/catalog/sql_features.txt @@ -227,7 +227,7 @@ F311 Schema definition statement NO F311 Schema definition statement 01 CREATE SCHEMA YES F311 Schema definition statement 02 CREATE TABLE for persistent base tables YES F311 Schema definition statement 03 CREATE VIEW YES -F311 Schema definition statement 04 CREATE VIEW: WITH CHECK OPTION NO +F311 Schema definition statement 04 CREATE VIEW: WITH CHECK OPTION YES F311 Schema definition statement 05 GRANT statement YES F312 MERGE statement YES F313 Enhanced MERGE statement YES diff --git a/src/common/backend/nodes/copyfuncs.cpp b/src/common/backend/nodes/copyfuncs.cpp index 5a46b68e8..a2606ad64 100644 --- a/src/common/backend/nodes/copyfuncs.cpp +++ b/src/common/backend/nodes/copyfuncs.cpp @@ -321,6 +321,7 @@ static ModifyTable* _copyModifyTable(const ModifyTable* from) COPY_SCALAR_FIELD(exclRelRTIndex); COPY_NODE_FIELD(upsertWhere); COPY_NODE_FIELD(targetlists); + COPY_NODE_FIELD(withCheckOptionLists); return newnode; } @@ -3602,6 +3603,18 @@ static TimeCapsuleClause* CopyTimeCapsuleClause(const TimeCapsuleClause* from) return newnode; } +static WithCheckOption* _copyWithCheckOption(const WithCheckOption* from) +{ + WithCheckOption* newnode = makeNode(WithCheckOption); + + COPY_STRING_FIELD(viewname); + COPY_NODE_FIELD(qual); + COPY_SCALAR_FIELD(cascaded); + COPY_SCALAR_FIELD(rtindex); + + return newnode; +} + static SortGroupClause* _copySortGroupClause(const SortGroupClause* from) { SortGroupClause* newnode = makeNode(SortGroupClause); @@ -4608,6 +4621,7 @@ static Query* _copyQuery(const Query* from) COPY_SCALAR_FIELD(can_push); COPY_SCALAR_FIELD(unique_check); COPY_NODE_FIELD(resultRelations); + COPY_NODE_FIELD(withCheckOptions); return newnode; } @@ -5423,6 +5437,7 @@ static ViewStmt* _copyViewStmt(const ViewStmt* from) COPY_SCALAR_FIELD(relkind); COPY_STRING_FIELD(definer); COPY_SCALAR_FIELD(is_alter); + COPY_SCALAR_FIELD(withCheckOption); return newnode; } @@ -8117,6 +8132,9 @@ void* copyObject(const void* from) case T_TimeCapsuleClause: retval = CopyTimeCapsuleClause((TimeCapsuleClause*)from); break; + case T_WithCheckOption: + retval = _copyWithCheckOption((WithCheckOption*)from); + break; case T_SortGroupClause: retval = _copySortGroupClause((SortGroupClause*)from); break; diff --git a/src/common/backend/nodes/equalfuncs.cpp b/src/common/backend/nodes/equalfuncs.cpp index 380a7e9b9..5760ae199 100644 --- a/src/common/backend/nodes/equalfuncs.cpp +++ b/src/common/backend/nodes/equalfuncs.cpp @@ -896,6 +896,7 @@ static bool _equalQuery(const Query* a, const Query* b) COMPARE_SCALAR_FIELD(can_push); COMPARE_SCALAR_FIELD(unique_check); COMPARE_NODE_FIELD(resultRelations); + COMPARE_NODE_FIELD(withCheckOptions); return true; } @@ -1620,6 +1621,7 @@ static bool _equalViewStmt(const ViewStmt* a, const ViewStmt* b) COMPARE_SCALAR_FIELD(relkind); COMPARE_STRING_FIELD(definer); COMPARE_SCALAR_FIELD(is_alter); + COMPARE_SCALAR_FIELD(withCheckOption); return true; } @@ -2805,6 +2807,15 @@ static bool EqualTimeCapsuleClause(const TimeCapsuleClause* a, const TimeCapsule return true; } +static bool _equalWithCheckOption(const WithCheckOption* a, const WithCheckOption* b) +{ + COMPARE_STRING_FIELD(viewname); + COMPARE_NODE_FIELD(qual); + COMPARE_SCALAR_FIELD(cascaded); + COMPARE_SCALAR_FIELD(rtindex); + + return true; +} static bool _equalSortGroupClause(const SortGroupClause* a, const SortGroupClause* b) { @@ -4115,6 +4126,9 @@ bool equal(const void* a, const void* b) case T_TimeCapsuleClause: retval = EqualTimeCapsuleClause((TimeCapsuleClause*)a, (TimeCapsuleClause*)b); break; + case T_WithCheckOption: + retval = _equalWithCheckOption((WithCheckOption*)a, (WithCheckOption*)b); + break; case T_SortGroupClause: retval = _equalSortGroupClause((SortGroupClause*)a, (SortGroupClause*)b); break; diff --git a/src/common/backend/nodes/nodeFuncs.cpp b/src/common/backend/nodes/nodeFuncs.cpp index df2188520..852011e44 100644 --- a/src/common/backend/nodes/nodeFuncs.cpp +++ b/src/common/backend/nodes/nodeFuncs.cpp @@ -1607,6 +1607,8 @@ bool expression_tree_walker(Node* node, bool (*walker)(), void* context) case T_SetVariableExpr: /* primitive node types with no expression subnodes */ break; + case T_WithCheckOption: + return p2walker(((WithCheckOption*)node)->qual, context); case T_Aggref: { Aggref* expr = (Aggref*)node; @@ -1961,6 +1963,8 @@ bool query_tree_walker(Query* query, bool (*walker)(), void* context, int flags) if (p2walker((Node*)query->targetList, context)) { return true; } + if (p2walker((Node*)query->withCheckOptions, context)) + return true; if (p2walker((Node*)query->mergeSourceTargetList, context)) { return true; } @@ -2209,6 +2213,14 @@ Node* expression_tree_mutator(Node* node, Node* (*mutator)(Node*, void*), void* } else { return node; } + case T_WithCheckOption: { + WithCheckOption* wco = (WithCheckOption*)node; + WithCheckOption* newnode; + + FLATCOPY(newnode, wco, WithCheckOption, isCopy); + MUTATE(newnode->qual, wco->qual, Node*); + return (Node*)newnode; + } case T_Aggref: { Aggref* aggref = (Aggref*)node; Aggref* newnode = NULL; @@ -2721,6 +2733,7 @@ Query* query_tree_mutator(Query* query, Node* (*mutator)(Node*, void*), void* co } MUTATE(query->targetList, query->targetList, List*); + MUTATE(query->withCheckOptions, query->withCheckOptions, List *); MUTATE(query->mergeSourceTargetList, query->mergeSourceTargetList, List*); MUTATE(query->mergeActionList, query->mergeActionList, List*); MUTATE(query->upsertClause, query->upsertClause, UpsertExpr*); diff --git a/src/common/backend/nodes/outfuncs.cpp b/src/common/backend/nodes/outfuncs.cpp index 2bc478080..9e1fac243 100755 --- a/src/common/backend/nodes/outfuncs.cpp +++ b/src/common/backend/nodes/outfuncs.cpp @@ -811,6 +811,9 @@ static void _outModifyTable(StringInfo str, ModifyTable* node) WRITE_NODE_FIELD(targetlists); } #endif + if (t_thrd.proc->workingVersionNum >= SUPPORT_VIEW_AUTO_UPDATABLE) { + WRITE_NODE_FIELD(withCheckOptionLists); + } } static void _outUpsertClause(StringInfo str, const UpsertClause* node) @@ -4474,6 +4477,19 @@ static void _outQuery(StringInfo str, Query* node) if (t_thrd.proc->workingVersionNum >= MULTI_MODIFY_VERSION_NUM) { WRITE_NODE_FIELD(resultRelations); } + if (t_thrd.proc->workingVersionNum >= SUPPORT_VIEW_AUTO_UPDATABLE) { + WRITE_NODE_FIELD(withCheckOptions); + } +} + +static void _outWithCheckOption(StringInfo str, const WithCheckOption* node) +{ + WRITE_NODE_TYPE("WITHCHECKOPTION"); + + WRITE_STRING_FIELD(viewname); + WRITE_NODE_FIELD(qual); + WRITE_BOOL_FIELD(cascaded); + WRITE_UINT_FIELD(rtindex); } static void _outSortGroupClause(StringInfo str, SortGroupClause* node) @@ -6277,6 +6293,9 @@ static void _outNode(StringInfo str, const void* obj) case T_Query: _outQuery(str, (Query*)obj); break; + case T_WithCheckOption: + _outWithCheckOption(str, (WithCheckOption*)obj); + break; case T_SortGroupClause: _outSortGroupClause(str, (SortGroupClause*)obj); break; diff --git a/src/common/backend/nodes/readfuncs.cpp b/src/common/backend/nodes/readfuncs.cpp index 6c06b5e26..0a2cc9b21 100755 --- a/src/common/backend/nodes/readfuncs.cpp +++ b/src/common/backend/nodes/readfuncs.cpp @@ -1536,6 +1536,10 @@ static Query* _readQuery(void) } else if (local_node->resultRelation != 0) { local_node->resultRelations = list_make1_int(local_node->resultRelation); } + + IF_EXIST(withCheckOptions) { + READ_NODE_FIELD(withCheckOptions); + } READ_DONE(); } @@ -1619,6 +1623,21 @@ static PLDebug_frame* _readPLDebug_frame(void) READ_DONE(); } +/* + * _readWithCheckOption + */ +static WithCheckOption* _readWithCheckOption(void) +{ + READ_LOCALS(WithCheckOption); + + READ_STRING_FIELD(viewname); + READ_NODE_FIELD(qual); + READ_BOOL_FIELD(cascaded); + READ_UINT_FIELD(rtindex); + + READ_DONE(); +} + /* * _readSortGroupClause */ @@ -5856,6 +5875,8 @@ Node* parseNodeString(void) if (MATCH("QUERY", 5)) { return_value = _readQuery(); + } else if (MATCH("WITHCHECKOPTION", 15)) { + return_value = _readWithCheckOption(); } else if (MATCH("SORTGROUPCLAUSE", 15)) { return_value = _readSortGroupClause(); } else if (MATCH("GROUPINGSET", 11)) { diff --git a/src/common/backend/parser/analyze.cpp b/src/common/backend/parser/analyze.cpp index 7b8c5b4f6..a853dfb3b 100644 --- a/src/common/backend/parser/analyze.cpp +++ b/src/common/backend/parser/analyze.cpp @@ -61,6 +61,7 @@ #include "parser/parsetree.h" #include "rewrite/rewriteManip.h" #include "rewrite/rewriteRlsPolicy.h" +#include "rewrite/rewriteHandler.h" #ifdef PGXC #include "miscadmin.h" #include "pgxc/pgxc.h" @@ -953,6 +954,73 @@ static void transformLimitSortClause(ParseState* pstate, void* stmt, Query* qry, } } +static void CheckRelationSupportMultiModify(Relation targetrel, bool forDel) +{ + const char* type = NULL; + if (forDel) { + CheckDeleteRelation(targetrel); + type = "DELETE"; + } else { + CheckUpdateRelation(targetrel); + type = "UPDATE"; + } + + if (RelationIsColStore(targetrel)) { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Un-support feature"), + errdetail("column stored relation in mutilple-relations modifying doesn't support %s", type))); + } + if (RelationIsForeignTable(targetrel) || RelationIsStream(targetrel)) { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Un-support feature"), + errdetail("FDW relation in mutilple-relations modifying doesn't support %s", type))); + } + if (targetrel->rd_rel->relhasrules) { + if (RelationIsView(targetrel) && (targetrel->rd_rules->numLocks > 1 || + view_has_instead_trigger(targetrel, forDel ? CMD_DELETE : CMD_UPDATE))) { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Un-support feature"), + errdetail("view has rules or tiggers in mutilple-relations modifying doesn't support %s", + type))); + } else if (!RelationIsView(targetrel)) { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Un-support feature"), + errdetail("relation has rules in mutilple-relations modifying doesn't support %s", type))); + } else { + /* recursive check for view */ + Query* viewquery = get_view_query(targetrel); + RangeTblRef* rtr = NULL; + RangeTblEntry* base_rte = NULL; + Relation base_rel; + + /* Ignore here, and it would throw an error during query rewrite. */ + if (list_length(viewquery->jointree->fromlist) != 1) { + return; + } + + rtr = (RangeTblRef*)linitial(viewquery->jointree->fromlist); + + /* Ignore here, it would throw an error during query rewrite. */ + if (!IsA(rtr, RangeTblRef)) { + return; + } + + base_rte = rt_fetch(rtr->rtindex, viewquery->rtable); + base_rel = try_relation_open(base_rte->relid, AccessShareLock); + + if (base_rel != NULL) { + CheckRelationSupportMultiModify(base_rel, forDel); + + relation_close(base_rel, AccessShareLock); + } + } + } +} + static void CheckUDRelations(ParseState* pstate, List* sortClause, Node* limitClause, List* returningList, bool forDel) { @@ -987,36 +1055,7 @@ static void CheckUDRelations(ParseState* pstate, List* sortClause, Node* limitCl foreach_cell (l, pstate->p_target_relation) { Relation targetrel = (Relation)lfirst(l); - if (forDel) { - CheckDeleteRelation(targetrel); - } else { - CheckUpdateRelation(targetrel); - } - - if (RelationIsColStore(targetrel)) { - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("Un-support feature"), - errdetail("column stored relation in mutilple-relations modifying doesn't support %s", type))); - } - if (RelationIsForeignTable(targetrel) || RelationIsStream(targetrel)) { - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("Un-support feature"), - errdetail("FDW relation in mutilple-relations modifying doesn't support %s", type))); - } - if (RelationIsView(targetrel)) { - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("Un-support feature"), - errdetail("view in mutilple-relations modifying doesn't support %s", type))); - } - if (targetrel->rd_rel->relhasrules) { - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("Un-support feature"), - errdetail("relation has rules in mutilple-relations modifying doesn't support %s", type))); - } + CheckRelationSupportMultiModify(targetrel, forDel); } } @@ -4192,8 +4231,6 @@ static List* transformUpdateTargetList(ParseState* pstate, List* qryTlist, List* RangeTblEntry* target_rte = NULL; ListCell* tl; ListCell* orig_tl; - ListCell* rb; - ListCell* r; Relation targetrel = NULL; int rtindex = 0; int targetRelationNum = list_length(pstate->p_target_relation); @@ -4241,12 +4278,6 @@ static List* transformUpdateTargetList(ParseState* pstate, List* qryTlist, List* */ transformMultiTargetList(pstate->p_target_rangetblentry, new_tle); - forboth (rb, pstate->p_target_rangetblentry, r, pstate->p_target_relation) { - target_rte = (RangeTblEntry*)lfirst(rb); - targetrel = (Relation)lfirst(r); - setExtraUpdatedCols(target_rte, targetrel->rd_att); - } - for (int i = 0; i < targetRelationNum; i++) { if (new_tle[i]) { tlist = list_concat(tlist, new_tle[i]); diff --git a/src/common/backend/parser/gram.y b/src/common/backend/parser/gram.y index 35e473109..430591b64 100644 --- a/src/common/backend/parser/gram.y +++ b/src/common/backend/parser/gram.y @@ -662,7 +662,7 @@ static void setDelimiterName(core_yyscan_t yyscanner, char*input, VariableSetStm %type OptRelative %type OptGPI %type OptTableSpace OptConsTableSpace OptTableSpaceOwner LoggingStr size_clause OptMaxSize OptDatafileSize OptReuse OptAuto OptNextStr OptDatanodeName -%type opt_check_option +%type opt_check_option %type opt_provider security_label @@ -15562,6 +15562,7 @@ RenameStmt: ALTER AGGREGATE func_name aggr_args RENAME TO name n->replace = true; n->sql_statement = NULL; n->is_alter = true; + n->withCheckOption = (ViewCheckOption)$7; $$ = (Node *) n; } | ALTER definer_expression VIEW qualified_name opt_column_list AS SelectStmt opt_check_option @@ -15582,6 +15583,7 @@ RenameStmt: ALTER AGGREGATE func_name aggr_args RENAME TO name n->replace = true; n->sql_statement = NULL; n->is_alter = true; + n->withCheckOption = (ViewCheckOption)$8; $$ = (Node *) n; } | ALTER MATERIALIZED VIEW qualified_name RENAME TO name @@ -17041,7 +17043,8 @@ ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list opt_reloptions n->query = $8; n->replace = false; n->options = $6; - n->sql_statement = NULL; + n->sql_statement = NULL; + n->withCheckOption = (ViewCheckOption)$9; $$ = (Node *) n; } | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list opt_reloptions @@ -17054,7 +17057,8 @@ ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list opt_reloptions n->query = $10; n->replace = true; n->options = $8; - n->sql_statement = NULL; + n->sql_statement = NULL; + n->withCheckOption = (ViewCheckOption)$11; $$ = (Node *) n; } | CREATE opt_or_replace definer_expression OptTemp VIEW qualified_name opt_column_list opt_reloptions @@ -17069,36 +17073,16 @@ ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list opt_reloptions n->replace = $2; n->options = $8; n->sql_statement = NULL; + n->withCheckOption = (ViewCheckOption)$11; $$ = (Node *) n; } ; opt_check_option: - WITH CHECK OPTION - { - const char* message = "WITH CHECK OPTION is not implemented"; - InsertErrorMessage(message, u_sess->plsql_cxt.plpgsql_yylloc); - ereport(errstate, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("WITH CHECK OPTION is not implemented"))); - } - | WITH CASCADED CHECK OPTION - { - const char* message = "WITH CHECK OPTION is not implemented"; - InsertErrorMessage(message, u_sess->plsql_cxt.plpgsql_yylloc); - ereport(errstate, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("WITH CHECK OPTION is not implemented"))); - } - | WITH LOCAL CHECK OPTION - { - const char* message = "WITH CHECK OPTION is not implemented"; - InsertErrorMessage(message, u_sess->plsql_cxt.plpgsql_yylloc); - ereport(errstate, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("WITH CHECK OPTION is not implemented"))); - } - | /* EMPTY */ { $$ = NIL; } + WITH CHECK OPTION { $$ = CASCADED_CHECK_OPTION; } + | WITH CASCADED CHECK OPTION { $$ = CASCADED_CHECK_OPTION; } + | WITH LOCAL CHECK OPTION { $$ = LOCAL_CHECK_OPTION; } + | /* EMPTY */ { $$ = NO_CHECK_OPTION; } ; /***************************************************************************** diff --git a/src/common/backend/parser/parse_merge.cpp b/src/common/backend/parser/parse_merge.cpp index cbca9ac5e..2f1f613b4 100644 --- a/src/common/backend/parser/parse_merge.cpp +++ b/src/common/backend/parser/parse_merge.cpp @@ -743,8 +743,6 @@ static List* transformUpdateTargetList(ParseState* pstate, List* origTlist) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("UPDATE target count mismatch --- internal error"))); } - setExtraUpdatedCols(target_rte, targetrel->rd_att); - return tlist; } diff --git a/src/common/backend/utils/adt/misc.cpp b/src/common/backend/utils/adt/misc.cpp index e6cf87fc8..eb2b72e8a 100644 --- a/src/common/backend/utils/adt/misc.cpp +++ b/src/common/backend/utils/adt/misc.cpp @@ -20,6 +20,7 @@ #include #include +#include "access/sysattr.h" #include "catalog/catalog.h" #include "catalog/pg_authid.h" #include "catalog/pg_tablespace.h" @@ -33,6 +34,7 @@ #include "parser/keywords.h" #include "pgstat.h" #include "postmaster/syslogger.h" +#include "rewrite/rewriteHandler.h" #include "replication/replicainternal.h" #include "storage/smgr/fd.h" #include "storage/pmsignal.h" @@ -1046,3 +1048,46 @@ Datum b_database_row_count(PG_FUNCTION_ARGS) { PG_RETURN_INT64(u_sess->statement_cxt.last_row_count); } + +/* + * pg_relation_is_updatable - determine which update events the specified + * relation supports. + * + * This relies on relation_is_updatable() in rewriteHandler.c, which see + * for additional information. + */ +Datum pg_relation_is_updatable(PG_FUNCTION_ARGS) +{ + Oid reloid = PG_GETARG_OID(0); + bool include_triggers = PG_GETARG_BOOL(1); + + PG_RETURN_INT32(relation_is_updatable(reloid, include_triggers, NULL)); +} + +/* + * pg_column_is_updatable - determine whether a column is updatable + * + * This function encapsulates the decision about just what + * information_schema.columns.is_updatable actually means. It's not clear + * whether deletability of the column's relation should be required, so + * we want that decision in C code where we could change it without initdb. + */ +Datum pg_column_is_updatable(PG_FUNCTION_ARGS) +{ + Oid reloid = PG_GETARG_OID(0); + AttrNumber attnum = PG_GETARG_INT16(1); + AttrNumber col = attnum - FirstLowInvalidHeapAttributeNumber; + bool include_triggers = PG_GETARG_BOOL(2); + int events; + + /* System columns are never updatable */ + if (attnum <= 0) + PG_RETURN_BOOL(false); + + events = relation_is_updatable(reloid, include_triggers, bms_make_singleton(col)); + + /* We require both updatability and deletability of the relation */ +#define REQ_EVENTS ((1 << CMD_UPDATE) | (1 << CMD_DELETE)) + + PG_RETURN_BOOL((events & REQ_EVENTS) == REQ_EVENTS); +} diff --git a/src/common/backend/utils/init/globals.cpp b/src/common/backend/utils/init/globals.cpp index 08fc64ed4..7197fb4e9 100644 --- a/src/common/backend/utils/init/globals.cpp +++ b/src/common/backend/utils/init/globals.cpp @@ -59,7 +59,7 @@ bool open_join_children = true; bool will_shutdown = false; /* hard-wired binary version number */ -const uint32 GRAND_VERSION_NUM = 92837; +const uint32 GRAND_VERSION_NUM = 92838; const uint32 SELECT_INTO_VAR_VERSION_NUM = 92834; const uint32 DOLPHIN_ENABLE_DROP_NUM = 92830; @@ -116,7 +116,7 @@ const uint32 SWITCH_ROLE_VERSION_NUM = 92668; const uint32 PLAN_SELECT_VERSION_NUM = 92826; const uint32 REPLACE_INTO_VERSION_NUM = 92828; const uint32 PG_AUTHID_PASSWORDEXT_VERSION_NUM = 92830; - +const uint32 SUPPORT_VIEW_AUTO_UPDATABLE = 92838; /* Version number of the guc parameter backend_version added in V500R001C20 */ const uint32 V5R1C20_BACKEND_VERSION_NUM = 92305; diff --git a/src/gausskernel/optimizer/commands/tablecmds.cpp b/src/gausskernel/optimizer/commands/tablecmds.cpp index 3f1498877..b6d1ea971 100644 --- a/src/gausskernel/optimizer/commands/tablecmds.cpp +++ b/src/gausskernel/optimizer/commands/tablecmds.cpp @@ -15572,6 +15572,36 @@ static void ATExecSetRelOptions(Relation rel, List* defList, AlterTableType oper } CheckSupportModifyCompression(rel, relOpt, defList); + /* Special-case validation of view options */ + if (rel->rd_rel->relkind == RELKIND_VIEW) { + Query* view_query = get_view_query(rel); + ListCell* cell = NULL; + bool check_option = false; + + foreach(cell, defList) { + DefElem* defel = (DefElem*)lfirst(cell); + + if (pg_strcasecmp(defel->defname, "check_option") == 0) { + check_option = true; + break; + } + } + + /* + * If the check option is specified, look to see if the view is + * actually auto-updatable or not. + */ + if (check_option) { + const char *view_updatable_error = view_query_is_auto_updatable(view_query, true); + + if (view_updatable_error) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("WITH CHECK OPTION is supported only on auto-updatable views"), + errhint("%s", view_updatable_error))); + } + } + /* * All we need do here is update the pg_class row; the new options will be * propagated into relcaches during post-commit cache inval. diff --git a/src/gausskernel/optimizer/commands/view.cpp b/src/gausskernel/optimizer/commands/view.cpp index bc3ca1ed5..68f2d4a02 100644 --- a/src/gausskernel/optimizer/commands/view.cpp +++ b/src/gausskernel/optimizer/commands/view.cpp @@ -35,6 +35,7 @@ #include "parser/parse_relation.h" #include "rewrite/rewriteDefine.h" #include "rewrite/rewriteManip.h" +#include "rewrite/rewriteHandler.h" #include "rewrite/rewriteSupport.h" #include "optimizer/nodegroups.h" #include "utils/acl.h" @@ -51,6 +52,19 @@ static void checkViewTupleDesc(TupleDesc newdesc, TupleDesc olddesc); InSideView query_from_view_hook = NULL; +/*--------------------------------------------------------------------- + * Validator for "check_option" reloption on views. The allowed values + * are "local" and "cascaded". + */ +void validateWithCheckOption(const char *value) +{ + if (value == NULL || (pg_strcasecmp(value, "local") != 0 && pg_strcasecmp(value, "cascaded") != 0)) { + ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid value for \"check_option\" option"), + errdetail("Valid values are \"local\", and \"cascaded\"."))); + } +} + static void setEncryptedColumnRef(ColumnDef *def, TargetEntry *tle) { def->clientLogicColumnRef = (ClientLogicColumnRef*)palloc(sizeof(ClientLogicColumnRef)); @@ -78,14 +92,12 @@ static void setEncryptedColumnRef(ColumnDef *def, TargetEntry *tle) /* --------------------------------------------------------------------- * DefineVirtualRelation * - * Create the "view" relation. `DefineRelation' does all the work, - * we just provide the correct arguments ... at least when we're - * creating a view. If we're updating an existing view, we have to - * work harder. + * Create a view relation and use the rules system to store the query + * for the view. * --------------------------------------------------------------------- */ static Oid DefineVirtualRelation(RangeVar* relation, List* tlist, bool replace, List* options, ObjectType relkind, - ViewStmt* stmt) + ViewStmt* stmt, Query* viewParse) { Oid viewOid; LOCKMODE lockmode; @@ -204,15 +216,6 @@ static Oid DefineVirtualRelation(RangeVar* relation, List* tlist, bool replace, if (!u_sess->attr.attr_common.IsInplaceUpgrade) checkViewTupleDesc(descriptor, rel->rd_att); - /* - * The new options list replaces the existing options list, even if - * it's empty. - */ - atcmd = makeNode(AlterTableCmd); - atcmd->subtype = AT_ReplaceRelOptions; - atcmd->def = (Node*)options; - atcmds = lappend(atcmds, atcmd); - /* * set definer by AlterTameCmd */ @@ -257,6 +260,38 @@ static Oid DefineVirtualRelation(RangeVar* relation, List* tlist, bool replace, } } + if (atcmds) { + AlterTableInternal(viewOid, atcmds, true); + + /* Make the new view columns visible */ + CommandCounterIncrement(); + } + + /* + * Update the query for the view. + * + * Note that we must do this before updating the view options, because + * the new options may not be compatible with the old view query (for + * example if we attempt to add the WITH CHECK OPTION, we require that + * the new view be automatically updatable, but the old view may not + * have been). + */ + StoreViewQuery(viewOid, viewParse, replace); + + /* Make the new view query visible */ + CommandCounterIncrement(); + + /* + * Finally update the view options. + * + * The new options list replaces the existing options list, even if + * it's empty. + */ + atcmd = makeNode(AlterTableCmd); + atcmd->subtype = AT_ReplaceRelOptions; + atcmd->def = (Node*)options; + atcmds = list_make1(atcmd); + /* OK, let's do it. */ AlterTableInternal(viewOid, atcmds, true); @@ -318,6 +353,13 @@ static Oid DefineVirtualRelation(RangeVar* relation, List* tlist, bool replace, relid = DefineRelation(createStmt, RELKIND_VIEW, ownerOid); } Assert(relid != InvalidOid); + + /* Make the new view relation visible */ + CommandCounterIncrement(); + + /* Store the query for the view */ + StoreViewQuery(relid, viewParse, replace); + return relid; } } @@ -498,6 +540,8 @@ Oid DefineView(ViewStmt* stmt, const char* queryString, bool send_remote, bool i Query* viewParse = NULL; Oid viewOid = InvalidOid; RangeVar* view = NULL; + ListCell* cell = NULL; + bool check_option; /* * Run parse analysis to convert the raw parse tree to a Query. Note this @@ -542,6 +586,42 @@ Oid DefineView(ViewStmt* stmt, const char* queryString, bool send_remote, bool i #ifdef ENABLE_MULTIPLE_NODES validate_streaming_engine_status((Node*) stmt); #endif + + /* + * If the user specified the WITH CHECK OPTION, add it to the list of + * reloptions. + */ + if (stmt->withCheckOption == LOCAL_CHECK_OPTION) + stmt->options = lappend(stmt->options, makeDefElem("check_option", (Node*)makeString("local"))); + else if (stmt->withCheckOption == CASCADED_CHECK_OPTION) + stmt->options = lappend(stmt->options, makeDefElem("check_option", (Node*)makeString("cascaded"))); + + /* + * Check that the view is auto-updatable if WITH CHECK OPTION was + * specified. + */ + check_option = false; + + foreach(cell, stmt->options) { + DefElem* defel = (DefElem*)lfirst(cell); + + if (pg_strcasecmp(defel->defname, "check_option") == 0) + check_option = true; + } + + /* + * If the check option is specified, look to see if the view is + * actually auto-updatable or not. + */ + if (check_option) { + const char *view_updatable_error = view_query_is_auto_updatable(viewParse, true); + + if (view_updatable_error) + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("WITH CHECK OPTION is supported only on auto-updatable views"), + errhint("%s", view_updatable_error))); + } + /* * If a list of column names was given, run through and insert these into * the actual query tree. - thomas 2000-03-08 @@ -608,6 +688,7 @@ Oid DefineView(ViewStmt* stmt, const char* queryString, bool send_remote, bool i CreateMvCommand(stmt, queryString); } #endif + StoreViewQuery(viewOid, viewParse, stmt->replace); } else { /* * Create the view relation @@ -615,7 +696,8 @@ Oid DefineView(ViewStmt* stmt, const char* queryString, bool send_remote, bool i * NOTE: if it already exists and replace is false, the xact will be * aborted. */ - viewOid = DefineVirtualRelation(view, viewParse->targetList, stmt->replace, stmt->options, stmt->relkind, stmt); + viewOid = DefineVirtualRelation(view, viewParse->targetList, stmt->replace, stmt->options, + stmt->relkind, stmt, viewParse); /* * The relation we have just created is not visible to any other commands @@ -625,8 +707,6 @@ Oid DefineView(ViewStmt* stmt, const char* queryString, bool send_remote, bool i CommandCounterIncrement(); } - StoreViewQuery(viewOid, viewParse, stmt->replace); - return viewOid; } diff --git a/src/gausskernel/optimizer/path/allpaths.cpp b/src/gausskernel/optimizer/path/allpaths.cpp index c14b5c0f5..3a6539c4e 100755 --- a/src/gausskernel/optimizer/path/allpaths.cpp +++ b/src/gausskernel/optimizer/path/allpaths.cpp @@ -3723,7 +3723,8 @@ static void subquery_push_qual(Query* subquery, RangeTblEntry* rte, Index rti, N */ if (levelsup == 0) { - qual = ResolveNew(qual, rti, 0, rte, subquery->targetList, CMD_SELECT, 0, &subquery->hasSubLinks); + qual = ReplaceVarsFromTargetList(qual, rti, 0, rte, subquery->targetList, REPLACEVARS_REPORT_ERROR, + 0, &subquery->hasSubLinks); } else { diff --git a/src/gausskernel/optimizer/plan/createplan.cpp b/src/gausskernel/optimizer/plan/createplan.cpp index 555b01360..333cf3c02 100755 --- a/src/gausskernel/optimizer/plan/createplan.cpp +++ b/src/gausskernel/optimizer/plan/createplan.cpp @@ -8483,19 +8483,19 @@ static void deparallelize_modifytable(List* subplans) * Build a ModifyTable plan node * * Currently, we don't charge anything extra for the actual table modification - * work, nor for the RETURNING expressions if any. It would only be window - * dressing, since these are always top-level nodes and there is no way for - * the costs to change any higher-level planning choices. But we might want - * to make it look better sometime. + * work, nor for the WITH CHECK OPTIONS OR RETURNING expressions if any. It + * would only be window dressing, since these are always top-level nodes and + * there is no way for the costs to change any higher-level planning choices. + * But we might want to make it look better sometime. */ #ifdef STREAMPLAN Plan* make_modifytable(PlannerInfo* root, CmdType operation, bool canSetTag, List* resultRelations, List* subplans, - List* returningLists, List* rowMarks, int epqParam, bool partKeyUpdated, Index mergeTargetRelation, - List* mergeSourceTargetList, List* mergeActionList, UpsertExpr* upsertClause) + List *withCheckOptionLists, List* returningLists, List* rowMarks, int epqParam, bool partKeyUpdated, + Index mergeTargetRelation, List* mergeSourceTargetList, List* mergeActionList, UpsertExpr* upsertClause) #else ModifyTable* make_modifytable(CmdType operation, bool canSetTag, List* resultRelations, List* subplans, - List* returningLists, List* rowMarks, int epqParam, bool partKeyUpdated, Index mergeTargetRelation, - List* mergeSourceTargetList, List* mergeActionList, UpsertExpr* upsertClause) + List *withCheckOptionLists, List* returningLists, List* rowMarks, int epqParam, bool partKeyUpdated, + Index mergeTargetRelation, List* mergeSourceTargetList, List* mergeActionList, UpsertExpr* upsertClause) #endif { ModifyTable* node = makeNode(ModifyTable); @@ -8511,6 +8511,7 @@ ModifyTable* make_modifytable(CmdType operation, bool canSetTag, List* resultRel #endif Assert(list_length(resultRelations) == list_length(subplans)); + Assert(withCheckOptionLists == NIL || list_length(resultRelations) == list_length(withCheckOptionLists)); Assert(returningLists == NIL || list_length(resultRelations) == list_length(returningLists)); if (operation == CMD_MERGE) { @@ -8570,6 +8571,7 @@ ModifyTable* make_modifytable(CmdType operation, bool canSetTag, List* resultRel node->resultRelations = resultRelations; node->resultRelIndex = -1; /* will be set correctly in setrefs.c */ node->plans = subplans; + node->withCheckOptionLists = withCheckOptionLists; node->returningLists = returningLists; node->rowMarks = rowMarks; node->epqParam = epqParam; @@ -8746,6 +8748,7 @@ ModifyTable* make_modifytables(CmdType operation, bool canSetTag, List* resultRe canSetTag, resultRelations, subplans, + NIL, returningLists, rowMarks, epqParam, @@ -8763,6 +8766,7 @@ ModifyTable* make_modifytables(CmdType operation, bool canSetTag, List* resultRe canSetTag, resultRelations, subplans, + NIL, returningLists, rowMarks, epqParam, @@ -8778,6 +8782,7 @@ ModifyTable* make_modifytables(CmdType operation, bool canSetTag, List* resultRe canSetTag, resultRelations, subplans, + NIL, returningLists, rowMarks, epqParam, diff --git a/src/gausskernel/optimizer/plan/planner.cpp b/src/gausskernel/optimizer/plan/planner.cpp index 4700266f2..8fcee43e3 100755 --- a/src/gausskernel/optimizer/plan/planner.cpp +++ b/src/gausskernel/optimizer/plan/planner.cpp @@ -1224,6 +1224,7 @@ Plan* subquery_planner(PlannerGlobal* glob, Query* parse, PlannerInfo* parent_ro int num_old_subplans = list_length(glob->subplans); PlannerInfo* root = NULL; Plan* plan = NULL; + List* newWithCheckOptions = NIL; List* newHaving = NIL; bool hasOuterJoins = false; bool hasResultRTEs = false; @@ -1552,6 +1553,15 @@ Plan* subquery_planner(PlannerGlobal* glob, Query* parse, PlannerInfo* parent_ro */ parse->targetList = (List*)preprocess_expression(root, (Node*)parse->targetList, EXPRKIND_TARGET); + foreach(l, parse->withCheckOptions) { + WithCheckOption* wco = (WithCheckOption*)lfirst(l); + + wco->qual = preprocess_expression(root, wco->qual, EXPRKIND_QUAL); + if (wco->qual != NULL) + newWithCheckOptions = lappend(newWithCheckOptions, wco); + } + parse->withCheckOptions = newWithCheckOptions; + parse->returningList = (List*)preprocess_expression(root, (Node*)parse->returningList, EXPRKIND_TARGET); preprocess_qual_conditions(root, (Node*)parse->jointree); @@ -1784,6 +1794,7 @@ Plan* subquery_planner(PlannerGlobal* glob, Query* parse, PlannerInfo* parent_ro plan = grouping_planner(root, tuple_fraction); /* If it's not SELECT, we need a ModifyTable node */ if (parse->commandType != CMD_SELECT) { + List* withCheckOptionLists = NIL; List* returningLists = NIL; List* rowMarks = NIL; Relation mainRel = NULL; @@ -1793,8 +1804,14 @@ Plan* subquery_planner(PlannerGlobal* glob, Query* parse, PlannerInfo* parent_ro RelationClose(mainRel); /* - * Set up the RETURNING list-of-lists, if needed. + * Set up the WITH CHECK OPTION and RETURNING lists-of-lists, if + * needed. */ + if (parse->withCheckOptions) + withCheckOptionLists = list_make1(parse->withCheckOptions); + else + withCheckOptionLists = NIL; + if (parse->returningList) returningLists = list_make1(parse->returningList); else @@ -1815,6 +1832,7 @@ Plan* subquery_planner(PlannerGlobal* glob, Query* parse, PlannerInfo* parent_ro parse->canSetTag, list_make1(parse->resultRelations), list_make1(plan), + withCheckOptionLists, returningLists, rowMarks, SS_assign_special_param(root), @@ -1828,6 +1846,7 @@ Plan* subquery_planner(PlannerGlobal* glob, Query* parse, PlannerInfo* parent_ro parse->canSetTag, list_make1(parse->resultRelations), list_make1(plan), + withCheckOptionLists, returningLists, rowMarks, SS_assign_special_param(root), @@ -4977,7 +4996,7 @@ static void preprocess_rowmarks(PlannerInfo* root) PlanRowMark* newrc = NULL; i++; - if (rte->rtekind == RTE_JOIN || rte->rtekind == RTE_REMOTE_DUMMY) + if (rte->rtekind == RTE_JOIN || rte->rtekind == RTE_REMOTE_DUMMY || rte->relkind == RELKIND_VIEW) continue; if (!bms_is_member(i, rels) && list_length(parse->resultRelations) <= 1) continue; diff --git a/src/gausskernel/optimizer/rewrite/rewriteHandler.cpp b/src/gausskernel/optimizer/rewrite/rewriteHandler.cpp index c03de5a6e..2e3de2ac6 100644 --- a/src/gausskernel/optimizer/rewrite/rewriteHandler.cpp +++ b/src/gausskernel/optimizer/rewrite/rewriteHandler.cpp @@ -29,6 +29,7 @@ #include "foreign/fdwapi.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" +#include "optimizer/clauses.h" #include "parser/analyze.h" #include "parser/parse_coerce.h" #include "parser/parsetree.h" @@ -75,7 +76,8 @@ static List *rewriteTargetListIU(List *targetList, CmdType commandType, Relation List **attrno_list, bool *hasGenCol); static TargetEntry* process_matched_tle(TargetEntry* src_tle, TargetEntry* prior_tle, const char* attrName); static Node* get_assignment_input(Node* node); -static void rewriteValuesRTE(RangeTblEntry* rte, Relation target_relation, List* attrnos); +static bool rewriteValuesRTE(Query* parsetree, RangeTblEntry* rte, Relation target_relation, List* attrnos, + bool force_nulls); static void rewriteTargetListUD(Query* parsetree, RangeTblEntry* target_rte, Relation target_relation, int rtindex); static void rewriteTargetListMutilUD(Query* parsetree, List* rtable, List* resultRelations); static void rewriteTargetListMutilUpdate(Query* parsetree, List* rtable, List* resultRelations); @@ -83,6 +85,7 @@ static void markQueryForLocking(Query* qry, Node* jtnode, LockClauseStrength str int waitSec); static List* matchLocks(CmdType event, RuleLock* rulelocks, int varno, Query* parsetree); static Query* fireRIRrules(Query* parsetree, List* activeRIRs, bool forUpdatePushedDown); +static Bitmapset* adjust_view_column_set(Bitmapset* cols, List* targetlist); #ifdef PGXC typedef struct pull_qual_vars_context { @@ -505,20 +508,22 @@ static Query* rewriteRuleAction( AddQual(sub_action, parsetree->jointree->quals); /* - * Rewrite new.attribute w/ right hand side of target-list entry for + * Rewrite new.attribute with right hand side of target-list entry for * appropriate field name in insert/update. * - * KLUGE ALERT: since ResolveNew returns a mutated copy, we can't just - * apply it to sub_action; we have to remember to update the sublink - * inside rule_action, too. + * KLUGE ALERT: since ReplaceVarsFromTargetList returns a mutated copy, we + * can't just apply it to sub_action; we have to remember to update the + * sublink inside rule_action, too. */ if ((event == CMD_INSERT || event == CMD_UPDATE) && sub_action->commandType != CMD_UTILITY) { - sub_action = (Query*)ResolveNew((Node*)sub_action, + sub_action = (Query*)ReplaceVarsFromTargetList((Node*)sub_action, new_varno, 0, rt_fetch(new_varno, sub_action->rtable), parsetree->targetList, - event, + (event == CMD_UPDATE) ? + REPLACEVARS_CHANGE_VARNO : + REPLACEVARS_SUBSTITUTE_NULL, current_varno, NULL); if (sub_action_ptr != NULL) @@ -540,12 +545,12 @@ static Query* rewriteRuleAction( ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot have RETURNING lists in multiple rules"))); *returning_flag = true; - rule_action->returningList = (List*)ResolveNew((Node*)parsetree->returningList, + rule_action->returningList = (List*)ReplaceVarsFromTargetList((Node*)parsetree->returningList, linitial_int(parsetree->resultRelations), 0, rt_fetch(linitial_int(parsetree->resultRelations), parsetree->rtable), rule_action->returningList, - CMD_SELECT, + REPLACEVARS_REPORT_ERROR, 0, &rule_action->hasSubLinks); @@ -602,12 +607,18 @@ static List* adjustJoinTreeList(Query* parsetree, bool removert, int rt_index) * and UPDATE, replace explicit DEFAULT specifications with column default * expressions. * - * 2. For an UPDATE on a view, add tlist entries for any unassigned-to - * attributes, assigning them their old values. These will later get - * expanded to the output values of the view. (This is equivalent to what - * the planner's expand_targetlist() will do for UPDATE on a regular table, - * but it's more convenient to do it here while we still have easy access - * to the view's original RT index.) + * 2. For an UPDATE on a trigger-updatable view, add tlist entries for any + * unassigned-to attributes, assigning them their old values. These will + * later get expanded to the output values of the view. (This is equivalent + * to what the planner's expand_targetlist() will do for UPDATE on a regular + * table, but it's more convenient to do it here while we still have easy + * access to the view's original RT index.) This is only necessary for + * trigger-updatable views, for which the view remains the result relation of + * the query. For auto-updatable views we must not do this, since it might + * add assignments to non-updatable view columns. For rule-updatable views it + * is unnecessary extra work, since the query will be rewritten with a + * different result relation which will be processed when we recurse via + * RewriteQuery. * * 3. Merge multiple entries for the same target attribute, or declare error * if we can't. Multiple entries are only allowed for INSERT/UPDATE of @@ -669,7 +680,7 @@ static List* rewriteTargetListIU(List* targetList, CmdType commandType, Relation att_tup = target_relation->rd_att->attrs[attrno - 1]; /* put attrno into attrno_list even if it's dropped */ - if (attrno_list != NULL) + if (attrno_list != NULL && IsA(old_tle->expr, Var)) *attrno_list = lappend_int(*attrno_list, attrno); /* We can (and must) ignore deleted attributes */ @@ -776,8 +787,8 @@ static List* rewriteTargetListIU(List* targetList, CmdType commandType, Relation * For an UPDATE on a view, provide a dummy entry whenever there is no * explicit assignment. */ - if (new_tle == NULL && commandType == CMD_UPDATE && - (target_relation->rd_rel->relkind == RELKIND_VIEW + if (new_tle == NULL && commandType == CMD_UPDATE && ((target_relation->rd_rel->relkind == RELKIND_VIEW + && view_has_instead_trigger(target_relation, CMD_UPDATE)) || target_relation->rd_rel->relkind == RELKIND_CONTQUERY)) { Node* new_expr = NULL; @@ -919,7 +930,8 @@ static void rewriteTargetListMutilUpdate(Query* parsetree, List* rtable, List* r * For an UPDATE on a view, provide a dummy entry whenever there is no * explicit assignment. */ - if (new_tle == NULL && (target_relation->rd_rel->relkind == RELKIND_VIEW + if (new_tle == NULL && ((target_relation->rd_rel->relkind == RELKIND_VIEW + && view_has_instead_trigger(target_relation, CMD_UPDATE)) || target_relation->rd_rel->relkind == RELKIND_CONTQUERY)) { Node* new_expr = NULL; @@ -954,6 +966,20 @@ static void rewriteTargetListMutilUpdate(Query* parsetree, List* rtable, List* r rewriteTargetListMutilUD(parsetree, rtable, resultRelations); } +static void multiUpdateSetExtraUpdatedCols(Query* parsetree) +{ + ListCell* lc = NULL; + RangeTblEntry* rte = NULL; + Relation rel; + + foreach (lc, parsetree->resultRelations) { + rte = rt_fetch(lfirst_int(lc), parsetree->rtable); + rel = heap_open(rte->relid, NoLock); + setExtraUpdatedCols(rte, rel->rd_att); + heap_close(rel, NoLock); + } +} + /* * Convert a matched TLE from the original tlist into a correct new TLE. * @@ -1277,33 +1303,102 @@ static void checkGenDefault(RangeTblEntry* rte, Relation target_relation, List* * the appropriate default expressions. The other aspects of targetlist * rewriting need be applied only to the query's targetlist proper. * - * Note that we currently can't support subscripted or field assignment - * in the multi-VALUES case. The targetlist will contain simple Vars - * referencing the VALUES RTE, and therefore process_matched_tle() will - * reject any such attempt with "multiple assignments to same column". + * For an auto-updatable view, each DEFAULT item in the VALUES list is + * replaced with the default from the view, if it has one. Otherwise it is + * left untouched so that the underlying base relation's default can be + * applied instead (when we later recurse to here after rewriting the query + * to refer to the base relation instead of the view). + * + * For other types of relation, including rule- and trigger-updatable views, + * all DEFAULT items are replaced, and if the target relation doesn't have a + * default, the value is explicitly set to NULL. + * + * Additionally, if force_nulls is true, the target relation's defaults are + * ignored and all DEFAULT items in the VALUES list are explicitly set to + * NULL, regardless of the target relation's type. This is used for the + * product queries generated by DO ALSO rules attached to an auto-updatable + * view, for which we will have already called this function with force_nulls + * false. For these product queries, we must then force any remaining DEFAULT + * items to NULL to provide concrete values for the rule actions. + * Essentially, this is a mix of the 2 cases above --- the original query is + * an insert into an auto-updatable view, and the product queries are inserts + * into a rule-updatable view. + * + * Note that we may have subscripted or field assignment targetlist entries, + * as well as more complex expressions from already-replaced DEFAULT items if + * we have recursed to here for an auto-updatable view. However, it ought to + * be impossible for such entries to have DEFAULTs assigned to them --- we + * should only have to replace DEFAULT items for targetlist entries that + * contain simple Vars referencing the VALUES RTE. + * + * Returns true if all DEFAULT items were replaced, and false if some were + * left untouched. */ -static void rewriteValuesRTE(RangeTblEntry* rte, Relation target_relation, List* attrnos) +static bool rewriteValuesRTE(Query* parsetree, RangeTblEntry* rte, Relation target_relation, List* attrnos, + bool force_nulls) { List* newValues = NIL; ListCell* lc = NULL; + bool isAutoUpdatableView; + bool allReplaced; + + /* Steps below are not sensible for non-INSERT queries */ + Assert(parsetree->commandType == CMD_INSERT); + Assert(rte->rtekind == RTE_VALUES); /* * Rebuilding all the lists is a pretty expensive proposition in a big * VALUES list, and it's a waste of time if there aren't any DEFAULT * placeholders. So first scan to see if there are any. + * We skip this check if force_nulls is true, because we know that there + * are DEFAULT items present in that case. */ - if (!searchForDefault(rte)) - return; /* nothing to do */ + if (!force_nulls && !searchForDefault(rte)) + return true; /* nothing to do */ - /* Check list lengths (we can assume all the VALUES sublists are alike) */ - AssertEreport(list_length(attrnos) == list_length((const List*)linitial(rte->values_lists)), MOD_OPT, ""); + /* + * Check if the target relation is an auto-updatable view, in which case + * unresolved defaults will be left untouched rather than being set to + * NULL. If force_nulls is true, we always set DEFAULT items to NULL, so + * skip this check in that case --- it isn't an auto-updatable view. + */ + isAutoUpdatableView = false; + if (!force_nulls && target_relation->rd_rel->relkind == RELKIND_VIEW && + !view_has_instead_trigger(target_relation, CMD_INSERT)) { + List* locks = NIL; + bool found; + ListCell* l = NULL; + + /* Look for an unconditional DO INSTEAD rule */ + locks = matchLocks(CMD_INSERT, target_relation->rd_rules, linitial_int(parsetree->resultRelations), parsetree); + + found = false; + foreach (l, locks) { + RewriteRule* rule_lock = (RewriteRule*)lfirst(l); + + if (rule_lock->isInstead && rule_lock->qual == NULL) { + found = true; + break; + } + } + + /* + * If we didn't find an unconditional DO INSTEAD rule, assume that the + * view is auto-updatable. If it isn't, rewriteTargetView() will + * throw an error. + */ + if (!found) + isAutoUpdatableView = true; + } newValues = NIL; + allReplaced = true; foreach (lc, rte->values_lists) { List* sublist = (List*)lfirst(lc); List* newList = NIL; ListCell* lc2 = NULL; ListCell* lc3 = NULL; + int i = 0; forboth(lc2, sublist, lc3, attrnos) { @@ -1316,17 +1411,29 @@ static void rewriteValuesRTE(RangeTblEntry* rte, Relation target_relation, List* if (applyDefault) { Node *new_expr = NULL; + if (attrno == 0) { + ereport(ERROR, (errmsg("cannot set value in column %d to DEFAULT", ++i))); + } + /* stored generated column will be computed in executor */ - if (att_tup->attisdropped || generatedCol) + if (force_nulls || att_tup->attisdropped || generatedCol) new_expr = NULL; else new_expr = build_column_default(target_relation, attrno, true); /* * If there is no default (ie, default is effectively NULL), - * we've got to explicitly set the column to NULL. + * we've got to explicitly set the column to NULL, unless the + * target relation is an auto-updatable view. */ if (new_expr == NULL) { + if (isAutoUpdatableView) { + /* Leave the value untouched */ + newList = lappend(newList, col); + allReplaced = false; + continue; + } + new_expr = (Node*)makeConst(att_tup->atttypid, -1, att_tup->attcollation, @@ -1346,6 +1453,8 @@ static void rewriteValuesRTE(RangeTblEntry* rte, Relation target_relation, List* newValues = lappend(newValues, newList); } rte->values_lists = newValues; + + return allReplaced; } #ifdef PGXC @@ -1823,6 +1932,10 @@ static Query* ApplyRetrieveRule(Query* parsetree, RewriteRule* rule, int rt_inde parsetree->returningList = (List*)copyObject(parsetree->returningList); ChangeVarNodes((Node*)parsetree->returningList, rt_index, linitial2_int(parsetree->resultRelations), 0); + /* rtindex and vars in withCheckOptions also need to change */ + parsetree->withCheckOptions = (List*)copyObject(parsetree->withCheckOptions); + ChangeVarNodes((Node*)parsetree->withCheckOptions, rt_index, linitial2_int(parsetree->resultRelations), 0); + /* Now, continue with expanding the original view RTE */ } else { ereport(ERROR, @@ -2274,12 +2387,14 @@ static Query* CopyAndAddInvertedQual(Query* parsetree, Node* rule_qual, int rt_i ChangeVarNodes(new_qual, PRS2_OLD_VARNO, rt_index, 0); /* Fix references to NEW */ if (event == CMD_INSERT || event == CMD_UPDATE) - new_qual = ResolveNew(new_qual, + new_qual = ReplaceVarsFromTargetList(new_qual, PRS2_NEW_VARNO, 0, rt_fetch(rt_index, parsetree->rtable), parsetree->targetList, - event, + (event == CMD_UPDATE) ? + REPLACEVARS_CHANGE_VARNO : + REPLACEVARS_SUBSTITUTE_NULL, rt_index, &parsetree->hasSubLinks); /* And attach the fixed qual */ @@ -2534,6 +2649,960 @@ static List* fireRules(Query* parsetree, int rt_index, CmdType event, List* lock return results; } +/* + * get_view_query - get the Query from a view's _RETURN rule. + * + * Caller should have verified that the relation is a view, and therefore + * we should find an ON SELECT action. + * + * Note that the pointer returned is into the relcache and therefore must + * be treated as read-only to the caller and not modified or scribbled on. + */ +Query* get_view_query(Relation view) +{ + int i; + + Assert(view->rd_rel->relkind == RELKIND_VIEW); + + for (i = 0; i < view->rd_rules->numLocks; i++) { + RewriteRule *rule = view->rd_rules->rules[i]; + + if (rule->event == CMD_SELECT) { + /* A _RETURN rule should have only one action */ + if (list_length(rule->actions) != 1) + ereport(ERROR, (errmsg("invalid _RETURN rule action specification"))); + + return (Query*)linitial(rule->actions); + } + } + + ereport(ERROR, (errmsg("failed to find _RETURN rule for view"))); + return NULL; /* keep compiler quiet */ +} + +/* + * view_has_instead_trigger - does view have an INSTEAD OF trigger for event? + * + * If it does, we don't want to treat it as auto-updatable. This test can't + * be folded into view_query_is_auto_updatable because it's not an error + * condition. + */ +bool view_has_instead_trigger(Relation view, CmdType event) +{ + TriggerDesc *trigDesc = view->trigdesc; + + switch (event) { + case CMD_INSERT: + if (trigDesc && trigDesc->trig_insert_instead_row) + return true; + break; + case CMD_UPDATE: + if (trigDesc && trigDesc->trig_update_instead_row) + return true; + break; + case CMD_DELETE: + if (trigDesc && trigDesc->trig_delete_instead_row) + return true; + break; + default: + ereport(ERROR, (errmsg("unrecognized CmdType: %d", (int)event))); + break; + } + return false; +} + +/* + * view_col_is_auto_updatable - test whether the specified column of a view + * is auto-updatable. Returns NULL (if the column can be updated) or a message + * string giving the reason that it cannot be. + * + * Note that the checks performed here are local to this view. We do not check + * whether the referenced column of the underlying base relation is updatable. + */ +static const char* view_col_is_auto_updatable(RangeTblRef* rtr, TargetEntry* tle) +{ + Var* var = (Var*)tle->expr; + + /* + * For now, the only updatable columns we support are those that are Vars + * referring to user columns of the underlying base relation. + * + * The view targetlist may contain resjunk columns (e.g., a view defined + * like "SELECT * FROM t ORDER BY a+b" is auto-updatable) but such columns + * are not auto-updatable, and in fact should never appear in the outer + * query's targetlist. + */ + if (tle->resjunk) + return gettext_noop("Junk view columns are not updatable."); + + if (!IsA(var, Var) || var->varno != (unsigned int)rtr->rtindex || var->varlevelsup != 0) + return gettext_noop("View columns that are not columns of their base relation are not updatable."); + + if (var->varattno < 0) + return gettext_noop("View columns that refer to system columns are not updatable."); + + if (var->varattno == 0) + return gettext_noop("View columns that return whole-row references are not updatable."); + + return NULL; /* the view column is updatable */ +} + +/* + * view_query_is_auto_updatable - test whether the specified view definition + * represents an auto-updatable view. Returns NULL (if the view can be updated) + * or a message string giving the reason that it cannot be. + * + * If check_cols is true, the view is required to have at least one updatable + * column (necessary for INSERT/UPDATE). Otherwise the view's columns are not + * checked for updatability. See also view_cols_are_auto_updatable. + * + * Note that the checks performed here are only based on the view definition. + * We do not check whether any base relations referred to by the view are + * updatable. + */ +const char* view_query_is_auto_updatable(Query *viewquery, bool check_cols) +{ + RangeTblRef* rtr = NULL; + RangeTblEntry* base_rte = NULL; + + /*---------- + * Check if the view is simply updatable. According to SQL-92 this means: + * - No DISTINCT clause. + * - Each TLE is a column reference, and each column appears at most once. + * - FROM contains exactly one base relation. + * - No GROUP BY or HAVING clauses. + * - No set operations (UNION, INTERSECT or EXCEPT). + * - No sub-queries in the WHERE clause that reference the target table. + * + * We ignore that last restriction since it would be complex to enforce + * and there isn't any actual benefit to disallowing sub-queries. (The + * semantic issues that the standard is presumably concerned about don't + * arise in Postgres, since any such sub-query will not see any updates + * executed by the outer query anyway, thanks to MVCC snapshotting.) + * + * We also relax the second restriction by supporting part of SQL:1999 + * feature T111, which allows for a mix of updatable and non-updatable + * columns, provided that an INSERT or UPDATE doesn't attempt to assign to + * a non-updatable column. + * + * In addition we impose these constraints, involving features that are + * not part of SQL-92: + * - No CTEs (WITH clauses). + * - No OFFSET or LIMIT clauses (this matches a SQL:2008 restriction). + * - No system columns (including whole-row references) in the tlist. + * - No window functions in the tlist. + * - No set-returning functions in the tlist. + * + * Note that we do these checks without recursively expanding the view. + * If the base relation is a view, we'll recursively deal with it later. + *---------- + */ + if (viewquery->distinctClause != NIL) + return gettext_noop("Views containing DISTINCT are not automatically updatable."); + + if (viewquery->groupClause != NIL) + return gettext_noop("Views containing GROUP BY are not automatically updatable."); + + if (viewquery->havingQual != NULL) + return gettext_noop("Views containing HAVING are not automatically updatable."); + + if (viewquery->setOperations != NULL) + return gettext_noop("Views containing UNION, INTERSECT or EXCEPT are not automatically updatable."); + + if (viewquery->cteList != NIL) + return gettext_noop("Views containing WITH are not automatically updatable."); + + if (viewquery->limitOffset != NULL || viewquery->limitCount != NULL) + return gettext_noop("Views containing LIMIT or OFFSET are not automatically updatable."); + + /* + * We must not allow window functions or set returning functions in the + * targetlist. Otherwise we might end up inserting them into the quals of + * the main query. We must also check for aggregates in the targetlist in + * case they appear without a GROUP BY. + * + * These restrictions ensure that each row of the view corresponds to a + * unique row in the underlying base relation. + */ + if (viewquery->hasAggs) + return gettext_noop("Views that return aggregate functions are not automatically updatable."); + + if (viewquery->hasWindowFuncs) + return gettext_noop("Views that return window functions are not automatically updatable."); + + if (expression_returns_set((Node *) viewquery->targetList)) + return gettext_noop("Views that return set-returning functions are not automatically updatable."); + + /* + * The view query should select from a single base relation, which must be + * a table or another view. + */ + if (list_length(viewquery->jointree->fromlist) != 1) + return gettext_noop("Views that do not select from a single table or view are not automatically updatable."); + + rtr = (RangeTblRef*)linitial(viewquery->jointree->fromlist); + if (!IsA(rtr, RangeTblRef)) + return gettext_noop("Views that do not select from a single table or view are not automatically updatable."); + + base_rte = rt_fetch(rtr->rtindex, viewquery->rtable); + if (base_rte->rtekind != RTE_RELATION || (base_rte->relkind != RELKIND_RELATION && + base_rte->relkind != RELKIND_FOREIGN_TABLE && base_rte->relkind != RELKIND_VIEW)) + return gettext_noop("Views that do not select from a single table or view are not automatically updatable."); + + /* + * Check that the view has at least one updatable column. This is required + * for INSERT/UPDATE but not for DELETE. + */ + if (check_cols) { + ListCell* cell= NULL; + bool found = false; + + foreach (cell, viewquery->targetList) { + TargetEntry* tle = (TargetEntry*)lfirst(cell); + + if (view_col_is_auto_updatable(rtr, tle) == NULL) { + found = true; + break; + } + } + + if (!found) + return gettext_noop("Views that have no updatable columns are not automatically updatable."); + } + + return NULL; /* the view is simply updatable */ +} + +/* + * view_cols_are_auto_updatable - test whether all of the required columns of + * an auto-updatable view are actually updatable. Returns NULL (if all the + * required columns can be updated) or a message string giving the reason that + * they cannot be. + * + * This should be used for INSERT/UPDATE to ensure that we don't attempt to + * assign to any non-updatable columns. + * + * Additionally it may be used to retrieve the set of updatable columns in the + * view, or if one or more of the required columns is not updatable, the name + * of the first offending non-updatable column. + * + * The caller must have already verified that this is an auto-updatable view + * using view_query_is_auto_updatable. + * + * Note that the checks performed here are only based on the view definition. + * We do not check whether the referenced columns of the base relation are + * updatable. + */ +static const char* view_cols_are_auto_updatable(Query *viewquery, Bitmapset *required_cols, + Bitmapset **updatable_cols, char **non_updatable_col) +{ + RangeTblRef* rtr = NULL; + AttrNumber col; + ListCell* cell = NULL; + + /* + * The caller should have verified that this view is auto-updatable and + * so there should be a single base relation. + */ + Assert(list_length(viewquery->jointree->fromlist) == 1); + rtr = (RangeTblRef *) linitial(viewquery->jointree->fromlist); + Assert(IsA(rtr, RangeTblRef)); + + /* Initialize the optional return values */ + if (updatable_cols != NULL) + *updatable_cols = NULL; + if (non_updatable_col != NULL) + *non_updatable_col = NULL; + + /* Test each view column for updatability */ + col = -FirstLowInvalidHeapAttributeNumber; + foreach (cell, viewquery->targetList) { + TargetEntry* tle = (TargetEntry*)lfirst(cell); + const char* col_update_detail; + + col++; + col_update_detail = view_col_is_auto_updatable(rtr, tle); + + if (col_update_detail == NULL) { + /* The column is updatable */ + if (updatable_cols != NULL) + *updatable_cols = bms_add_member(*updatable_cols, col); + } else if (bms_is_member(col, required_cols)) { + /* The required column is not updatable */ + if (non_updatable_col != NULL) + *non_updatable_col = tle->resname; + return col_update_detail; + } + } + + return NULL; /* all the required view columns are updatable */ +} + +/* + * relation_is_updatable - determine which update events the specified + * relation supports. + * + * Note that views may contain a mix of updatable and non-updatable columns. + * For a view to support INSERT/UPDATE it must have at least one updatable + * column, but there is no such restriction for DELETE. If include_cols is + * non-NULL, then only the specified columns are considered when testing for + * updatability. + * + * This is used for the information_schema views, which have separate concepts + * of "updatable" and "trigger updatable". A relation is "updatable" if it + * can be updated without the need for triggers (either because it has a + * suitable RULE, or because it is simple enough to be automatically updated). + * A relation is "trigger updatable" if it has a suitable INSTEAD OF trigger. + * The SQL standard regards this as not necessarily updatable, presumably + * because there is no way of knowing what the trigger will actually do. + * The information_schema views therefore call this function with + * include_triggers = false. However, other callers might only care whether + * data-modifying SQL will work, so they can pass include_triggers = true + * to have trigger updatability included in the result. + * + * The return value is a bitmask of rule event numbers indicating which of + * the INSERT, UPDATE and DELETE operations are supported. (We do it this way + * so that we can test for UPDATE plus DELETE support in a single call.) + */ +int relation_is_updatable(Oid reloid, bool include_triggers, Bitmapset* include_cols) +{ + int events = 0; + Relation rel; + RuleLock* rulelocks = NULL; + +#define ALL_EVENTS ((1 << CMD_INSERT) | (1 << CMD_UPDATE) | (1 << CMD_DELETE)) + + rel = try_relation_open(reloid, AccessShareLock); + + /* + * If the relation doesn't exist, return zero rather than throwing an + * error. This is helpful since scanning an information_schema view + * under MVCC rules can result in referencing rels that were just + * deleted according to a SnapshotNow probe. + */ + if (rel == NULL) + return 0; + + /* If the relation is a table, it is always updatable */ + if (rel->rd_rel->relkind == RELKIND_RELATION) { + relation_close(rel, AccessShareLock); + return ALL_EVENTS; + } + + /* Look for unconditional DO INSTEAD rules, and note supported events */ + rulelocks = rel->rd_rules; + if (rulelocks != NULL) { + int i; + + for (i = 0; i < rulelocks->numLocks; i++) { + if (rulelocks->rules[i]->isInstead && rulelocks->rules[i]->qual == NULL) { + events |= ((1 << rulelocks->rules[i]->event) & ALL_EVENTS); + } + } + + /* If we have rules for all events, we're done */ + if (events == ALL_EVENTS) { + relation_close(rel, AccessShareLock); + return events; + } + } + + /* Similarly look for INSTEAD OF triggers, if they are to be included */ + if (include_triggers) { + TriggerDesc *trigDesc = rel->trigdesc; + + if (trigDesc) { + if (trigDesc->trig_insert_instead_row) + events |= (1 << CMD_INSERT); + if (trigDesc->trig_update_instead_row) + events |= (1 << CMD_UPDATE); + if (trigDesc->trig_delete_instead_row) + events |= (1 << CMD_DELETE); + + /* If we have triggers for all events, we're done */ + if (events == ALL_EVENTS) { + relation_close(rel, AccessShareLock); + return events; + } + } + } + + /* If this is a foreign table, check which update events it supports */ + if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) { + FdwRoutine *fdwroutine = GetFdwRoutineForRelation(rel, false); + + if (fdwroutine->IsForeignRelUpdatable != NULL) + events |= fdwroutine->IsForeignRelUpdatable(rel); + else { + /* Assume presence of executor functions is sufficient */ + if (fdwroutine->ExecForeignInsert != NULL) + events |= (1 << CMD_INSERT); + if (fdwroutine->ExecForeignUpdate != NULL) + events |= (1 << CMD_UPDATE); + if (fdwroutine->ExecForeignDelete != NULL) + events |= (1 << CMD_DELETE); + } + + relation_close(rel, AccessShareLock); + return events; + } + + /* Check if this is an automatically updatable view */ + if (rel->rd_rel->relkind == RELKIND_VIEW) { + Query* viewquery = get_view_query(rel); + + if (view_query_is_auto_updatable(viewquery, false) == NULL) { + Bitmapset* updatable_cols; + int auto_events; + RangeTblRef* rtr; + RangeTblEntry* base_rte; + Oid baseoid; + + /* + * Determine which of the view's columns are updatable. If there + * are none within the set of of columns we are looking at, then + * the view doesn't support INSERT/UPDATE, but it may still + * support DELETE. + */ + view_cols_are_auto_updatable(viewquery, NULL, &updatable_cols, NULL); + + if (include_cols != NULL) + updatable_cols = bms_int_members(updatable_cols, include_cols); + + if (bms_is_empty(updatable_cols)) + auto_events = (1 << CMD_DELETE); /* May support DELETE */ + else + auto_events = ALL_EVENTS; /* May support all events */ + + /* + * The base relation must also support these update commands. + * Tables are always updatable, but for any other kind of base + * relation we must do a recursive check limited to the columns + * referenced by the locally updatable columns in this view. + */ + rtr = (RangeTblRef*)linitial(viewquery->jointree->fromlist); + base_rte = rt_fetch(rtr->rtindex, viewquery->rtable); + Assert(base_rte->rtekind == RTE_RELATION); + + if (base_rte->relkind != RELKIND_RELATION) { + baseoid = base_rte->relid; + include_cols = adjust_view_column_set(updatable_cols, viewquery->targetList); + auto_events &= relation_is_updatable(baseoid, + include_triggers, + include_cols); + } + events |= auto_events; + } + + } + + /* If we reach here, the relation may support some update commands */ + relation_close(rel, AccessShareLock); + return events; +} + +/* + * adjust_view_column_set - map a set of column numbers according to targetlist + * + * This is used with simply-updatable views to map column-permissions sets for + * the view columns onto the matching columns in the underlying base relation. + * The targetlist is expected to be a list of plain Vars of the underlying + * relation (as per the checks above in view_query_is_auto_updatable). + */ +static Bitmapset* adjust_view_column_set(Bitmapset* cols, List* targetlist) +{ + Bitmapset* result = NULL; + Bitmapset* tmpcols = NULL; + AttrNumber col; + + tmpcols = bms_copy(cols); + while ((col = bms_first_member(tmpcols)) >= 0) { + /* bit numbers are offset by FirstLowInvalidHeapAttributeNumber */ + AttrNumber attno = col + FirstLowInvalidHeapAttributeNumber; + + if (attno == InvalidAttrNumber) { + /* + * There's a whole-row reference to the view. For permissions + * purposes, treat it as a reference to each column available from + * the view. (We should *not* convert this to a whole-row + * reference to the base relation, since the view may not touch + * all columns of the base relation.) + */ + ListCell* lc = NULL; + + foreach(lc, targetlist) { + TargetEntry* tle = (TargetEntry *) lfirst(lc); + Var* var = NULL; + + if (tle->resjunk) + continue; + var = (Var*)tle->expr; + Assert(IsA(var, Var)); + result = bms_add_member(result, var->varattno - FirstLowInvalidHeapAttributeNumber); + } + } else { + /* + * Views do not have system columns, so we do not expect to see + * any other system attnos here. If we do find one, the error + * case will apply. + */ + TargetEntry* tle = get_tle_by_resno(targetlist, attno); + + if (tle != NULL && !tle->resjunk && IsA(tle->expr, Var)) { + Var* var = (Var*)tle->expr; + + result = bms_add_member(result, var->varattno - FirstLowInvalidHeapAttributeNumber); + } else + ereport(ERROR, (errmsg("attribute number %d not found in view targetlist", attno))); + } + } + bms_free(tmpcols); + + return result; +} + +/* + * If target relation is already exist in parsetree, make new_rte + * and new_rt_index point to it, and return true. + */ +static bool setNewRteIfExist(Query* parsetree, Oid base_relid, int result_relation, RangeTblEntry** new_rte, + int* new_rt_index) +{ + int rtindex = 1; + bool targetIsExist = false; + ListCell* lc = NULL; + + foreach (lc, parsetree->rtable) { + RangeTblEntry* rte = (RangeTblEntry*)lfirst(lc); + + if (base_relid == rte->relid && rtindex != result_relation) { + parsetree->resultRelations = list_delete_int(parsetree->resultRelations, result_relation); + *new_rt_index = rtindex; + *new_rte = rte; + + targetIsExist = true; + break; + } + rtindex++; + } + + if (targetIsExist) { + ListCell* l = NULL; + foreach (l, parsetree->jointree->fromlist) { + RangeTblRef* rtf = (RangeTblRef*)lfirst(l); + + if (rtf->rtindex == result_relation) { + parsetree->jointree->fromlist = list_delete_ptr(parsetree->jointree->fromlist, rtf); + break; + } + } + } + + return targetIsExist; +} + +/* + * rewriteTargetView - + * Attempt to rewrite a query where the target relation is a view, so that + * the view's base relation becomes the target relation. + * + * Note that the base relation here may itself be a view, which may or may not + * have INSTEAD OF triggers or rules to handle the update. That is handled by + * the recursion in RewriteQuery. + * + * For multiple modifying, result_relation is needed to indicate which modified + * view to rewrite. + */ +static Query* rewriteTargetView(Query *parsetree, Relation view, int result_relation) +{ + Query* viewquery = NULL; + const char* auto_update_detail = NULL; + RangeTblRef* rtr = NULL; + int base_rt_index; + int new_rt_index; + RangeTblEntry* base_rte = NULL; + RangeTblEntry* view_rte = NULL; + RangeTblEntry* new_rte = NULL; + Relation base_rel; + List* view_targetlist = NIL; + ListCell* lc = NULL; + + /* + * Get the Query from the view's ON SELECT rule. We're going to munge the + * Query to change the view's base relation into the target relation, + * along with various other changes along the way, so we need to make a + * copy of it (get_view_query() returns a pointer into the relcache, so we + * have to treat it as read-only). + */ + viewquery = (Query*)copyObject(get_view_query(view)); + + auto_update_detail = view_query_is_auto_updatable(viewquery, parsetree->commandType != CMD_DELETE); + + if (auto_update_detail) { + /* messages here should match execMain.c's CheckValidResultRel */ + switch (parsetree->commandType) { + case CMD_INSERT: + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot insert into view \"%s\"", RelationGetRelationName(view)), + errdetail_internal("%s", _(auto_update_detail)), + errhint("To enable inserting into the view, provide an INSTEAD OF INSERT trigger or " + "an unconditional ON INSERT DO INSTEAD rule."))); + break; + case CMD_UPDATE: + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot update view \"%s\"", RelationGetRelationName(view)), + errdetail_internal("%s", _(auto_update_detail)), + errhint("To enable updating the view, provide an INSTEAD OF UPDATE trigger or " + "an unconditional ON UPDATE DO INSTEAD rule."))); + break; + case CMD_DELETE: + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot delete from view \"%s\"", RelationGetRelationName(view)), + errdetail_internal("%s", _(auto_update_detail)), + errhint("To enable deleting from the view, provide an INSTEAD OF DELETE trigger or " + "an unconditional ON DELETE DO INSTEAD rule."))); + break; + default: + ereport(ERROR, (errmsg("unrecognized CmdType: %d", (int)parsetree->commandType))); + break; + } + } + + /* + * For INSERT/UPDATE the modified columns must all be updatable. Note that + * we get the modified columns from the query's targetlist, not from the + * result RTE's modifiedCols set, since rewriteTargetListIU may have added + * additional targetlist entries for view defaults, and these must also be + * updatable. + */ + if (parsetree->commandType != CMD_DELETE) { + Bitmapset *modified_cols = NULL; + char* non_updatable_col = NULL; + + foreach (lc, parsetree->targetList) { + TargetEntry* tle = (TargetEntry*)lfirst(lc); + + if ((tle->rtindex == 0 || tle->rtindex == (Index)result_relation) && !tle->resjunk) + modified_cols = bms_add_member(modified_cols, tle->resno - FirstLowInvalidHeapAttributeNumber); + } + + auto_update_detail = view_cols_are_auto_updatable(viewquery, modified_cols, NULL, &non_updatable_col); + if (auto_update_detail) { + /* + * This is a different error, caused by an attempt to update a + * non-updatable column in an otherwise updatable view. + */ + switch (parsetree->commandType) { + case CMD_INSERT: + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot insert into column \"%s\" of view \"%s\"", non_updatable_col, + RelationGetRelationName(view)), + errdetail_internal("%s", _(auto_update_detail)))); + break; + case CMD_UPDATE: + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot update column \"%s\" of view \"%s\"", non_updatable_col, + RelationGetRelationName(view)), + errdetail_internal("%s", _(auto_update_detail)))); + break; + default: + elog(ERROR, "unrecognized CmdType: %d", (int)parsetree->commandType); + break; + } + } + } + + /* Locate RTE describing the view in the outer query */ + view_rte = rt_fetch(result_relation, parsetree->rtable); + + /* + * If we get here, view_query_is_auto_updatable() has verified that the + * view contains a single base relation. + */ + + Assert(list_length(viewquery->jointree->fromlist) == 1); + rtr = (RangeTblRef*)linitial(viewquery->jointree->fromlist); + Assert(IsA(rtr, RangeTblRef)); + + base_rt_index = rtr->rtindex; + base_rte = rt_fetch(base_rt_index, viewquery->rtable); + Assert(base_rte->rtekind == RTE_RELATION); + + /* + * Up to now, the base relation hasn't been touched at all in our query. + * We need to acquire lock on it before we try to do anything with it. + * (The subsequent recursive call of RewriteQuery will suppose that we + * already have the right lock!) Since it will become the query target + * relation, RowExclusiveLock is always the right thing. + */ + base_rel = heap_open(base_rte->relid, RowExclusiveLock); + + /* + * While we have the relation open, update the RTE's relkind, just in case + * it changed since this view was made (cf. AcquireRewriteLocks). + */ + base_rte->relkind = base_rel->rd_rel->relkind; + + heap_close(base_rel, NoLock); + + /* + * If the view query contains any sublink subqueries then we need to also + * acquire locks on any relations they refer to. We know that there won't + * be any subqueries in the range table or CTEs, so we can skip those, as + * in AcquireRewriteLocks. + */ + if (viewquery->hasSubLinks) { + (void)query_tree_walker(viewquery, (bool (*)())acquireLocksOnSubLinks,NULL, QTW_IGNORE_RC_SUBQUERIES); + } + + /* + * Create a new target RTE describing the base relation, and add it to the + * outer query's rangetable. (What's happening in the next few steps is + * very much like what the planner would do to "pull up" the view into the + * outer query. Perhaps someday we should refactor things enough so that + * we can share code with the planner.) + * + * We will not do so if basic relation of view is already exist in rtables, + * cause multiple-relation modifying include views is allowed. + */ + if (!setNewRteIfExist(parsetree, base_rte->relid, result_relation, &new_rte, &new_rt_index)) { + new_rte = base_rte; + parsetree->rtable = lappend(parsetree->rtable, new_rte); + new_rt_index = list_length(parsetree->rtable); + } + + /* + * Adjust the view's targetlist Vars to reference the new target RTE, ie + * make their varnos be new_rt_index instead of base_rt_index. There can + * be no Vars for other rels in the tlist, so this is sufficient to pull + * up the tlist expressions for use in the outer query. The tlist will + * provide the replacement expressions used by ReplaceVarsFromTargetList + * below. + */ + view_targetlist = viewquery->targetList; + + ChangeVarNodes((Node*)view_targetlist, base_rt_index, new_rt_index, 0); + + /* + * Mark the new target RTE for the permissions checks that we want to + * enforce against the view owner, as distinct from the query caller. At + * the relation level, require the same INSERT/UPDATE/DELETE permissions + * that the query caller needs against the view. We drop the ACL_SELECT + * bit that is presumably in new_rte->requiredPerms initially. + * + * Note: the original view RTE remains in the query's rangetable list. + * Although it will be unused in the query plan, we need it there so that + * the executor still performs appropriate permissions checks for the + * query caller's use of the view. + */ + new_rte->checkAsUser = view->rd_rel->relowner; + new_rte->requiredPerms = view_rte->requiredPerms; + + /* + * Now for the per-column permissions bits. + * + * Initially, new_rte contains selectedCols permission check bits for all + * base-rel columns referenced by the view, but since the view is a SELECT + * query its modifiedCols is empty. We set modifiedCols to include all + * the columns the outer query is trying to modify, adjusting the column + * numbers as needed. But we leave selectedCols as-is, so the view owner + * must have read permission for all columns used in the view definition, + * even if some of them are not read by the outer query. We could try to + * limit selectedCols to only columns used in the transformed query, but + * that does not correspond to what happens in ordinary SELECT usage of a + * view: all referenced columns must have read permission, even if + * optimization finds that some of them can be discarded during query + * transformation. The flattening we're doing here is an optional + * optimization, too. (If you are unpersuaded and want to change this, + * note that applying adjust_view_column_set to view_rte->selectedCols is + * clearly *not* the right answer, since that neglects base-rel columns + * used in the view's WHERE quals.) + * + * This step needs the modified view targetlist, so we have to do things + * in this order. + */ + new_rte->insertedCols = bms_add_members(new_rte->insertedCols, + adjust_view_column_set(view_rte->insertedCols, view_targetlist)); + new_rte->updatedCols = bms_add_members(new_rte->updatedCols, + adjust_view_column_set(view_rte->updatedCols, view_targetlist)); + new_rte->modifiedCols = bms_union(new_rte->insertedCols, new_rte->updatedCols); + + /* + * Move any security barrier quals from the view RTE onto the new target + * RTE. Any such quals should now apply to the new target RTE and will not + * reference the original view RTE in the rewritten query. + */ + new_rte->securityQuals = list_concat(new_rte->securityQuals, view_rte->securityQuals); + view_rte->securityQuals = NIL; + + /* + * For UPDATE/DELETE, rewriteTargetListUD will have added a wholerow junk + * TLE for the view to the end of the targetlist, which we no longer need. + * Remove it to avoid unnecessary work when we process the targetlist. + * Note that when we recurse through rewriteQuery a new junk TLE will be + * added to allow the executor to find the proper row in the new target + * relation. (So, if we failed to do this, we might have multiple junk + * TLEs with the same name, which would be disastrous.) + */ + if (parsetree->commandType != CMD_INSERT) { + ListCell* res = NULL; + TargetEntry* tle = NULL; + + foreach (res, parsetree->targetList) { + tle = (TargetEntry*)lfirst(res); + if (tle->rtindex == (Index)result_relation && strcmp(tle->resname, "wholerow") == 0) { + parsetree->targetList = list_delete_ptr(parsetree->targetList, tle); + break; + } + } + } + + /* + * Now update all Vars in the outer query that reference the view to + * reference the appropriate column of the base relation instead. + */ + parsetree = (Query*)ReplaceVarsFromTargetList((Node*)parsetree, + result_relation, + 0, + view_rte, + view_targetlist, + REPLACEVARS_REPORT_ERROR, + 0, + &parsetree->hasSubLinks); + + /* + * Update all other RTI references in the query that point to the view + * (for example, parsetree->resultRelation itself) to point to the new + * base relation instead. Vars will not be affected since none of them + * reference parsetree->resultRelation any longer. + */ + ChangeVarNodes((Node*)parsetree, result_relation, new_rt_index, 0); + + /* + * For INSERT/UPDATE we must also update resnos in the targetlist to refer + * to columns of the base relation, since those indicate the target + * columns to be affected. + * + * Note that this destroys the resno ordering of the targetlist, but that + * will be fixed when we recurse through rewriteQuery, which will invoke + * rewriteTargetListIU again on the updated targetlist. + */ + if (parsetree->commandType != CMD_DELETE) { + foreach(lc, parsetree->targetList) { + TargetEntry* tle = (TargetEntry*)lfirst(lc); + TargetEntry* view_tle = NULL; + + if (tle->resjunk) + continue; + + view_tle = get_tle_by_resno(view_targetlist, tle->resno); + if (view_tle != NULL && !view_tle->resjunk && IsA(view_tle->expr, Var)) + tle->resno = ((Var*)view_tle->expr)->varattno; + else + ereport(ERROR, (errmsg("attribute number %d not found in view targetlist", tle->resno))); + } + } + + /* + * For UPDATE/DELETE, pull up any WHERE quals from the view. We know that + * any Vars in the quals must reference the one base relation, so we need + * only adjust their varnos to reference the new target (just the same as + * we did with the view targetlist). + * + * Note that there is special-case handling for the quals of a security + * barrier view, since they need to be kept separate from any user-supplied + * quals, so these quals are kept on the new target RTE. + * For INSERT, the view's quals can be ignored in the main query. + */ + if (parsetree->commandType != CMD_INSERT && viewquery->jointree->quals != NULL) { + Node* viewqual = (Node*)viewquery->jointree->quals; + + ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0); + + if (RelationIsSecurityView(view)) { + /* + * Note: the parsetree has been mutated, so the new_rte pointer is + * stale and needs to be re-computed. + */ + new_rte = rt_fetch(new_rt_index, parsetree->rtable); + new_rte->securityQuals = lcons(viewqual, new_rte->securityQuals); + + /* + * Make sure that the query is marked correctly if the added qual + * has sublinks. + */ + if (!parsetree->hasSubLinks) + parsetree->hasSubLinks = checkExprHasSubLink(viewqual); + } else + AddQual(parsetree, (Node*)viewqual); + } + + /* + * For INSERT/UPDATE, if the view has the WITH CHECK OPTION, or any parent + * view specified WITH CASCADED CHECK OPTION, add the quals from the view + * to the query's withCheckOptions list. + */ + if (parsetree->commandType != CMD_DELETE) { + bool has_wco = RelationHasCheckOption(view); + bool cascaded = RelationHasCascadedCheckOption(view); + + /* + * If the parent view has a cascaded check option, treat this view as + * if it also had a cascaded check option. + * + * New WithCheckOptions are added to the start of the list, so if there + * is a cascaded check option, it will be the first item in the list. + */ + if (parsetree->withCheckOptions != NIL) { + WithCheckOption* parent_wco = (WithCheckOption*)linitial(parsetree->withCheckOptions); + + if (parent_wco->cascaded) { + has_wco = true; + cascaded = true; + } + } + + /* + * Add the new WithCheckOption to the start of the list, so that + * checks on inner views are run before checks on outer views, as + * required by the SQL standard. + * + * If the new check is CASCADED, we need to add it even if this view + * has no quals, since there may be quals on child views. A LOCAL + * check can be omitted if this view has no quals. + */ + if (has_wco && (cascaded || viewquery->jointree->quals != NULL)) { + WithCheckOption* wco = makeNode(WithCheckOption); + wco->viewname = pstrdup(RelationGetRelationName(view)); + wco->qual = NULL; + wco->cascaded = cascaded; + wco->rtindex = new_rt_index; + + parsetree->withCheckOptions = lcons(wco, parsetree->withCheckOptions); + + if (viewquery->jointree->quals != NULL) { + wco->qual = (Node*)viewquery->jointree->quals; + ChangeVarNodes(wco->qual, base_rt_index, new_rt_index, 0); + + /* + * Make sure that the query is marked correctly if the added + * qual has sublinks. We can skip this check if the query is + * already marked, or if the command is an UPDATE, in which + * case the same qual will have already been added, and this + * check will already have been done. + */ + if (!parsetree->hasSubLinks && + parsetree->commandType != CMD_UPDATE) + parsetree->hasSubLinks = checkExprHasSubLink(wco->qual); + } + } + } + + return parsetree; +} + void ereport_for_each_cmdtype(CmdType event, Relation rt_entry_relation) { switch (event) { @@ -2659,6 +3728,13 @@ static List* RewriteQuery(Query* parsetree, List* rewrite_events) Relation rt_entry_relation; List* locks = NIL; bool hasGenCol = false; + List* product_queries = NIL; + List* attrnos = NIL; + int values_rte_index = 0; + bool defaults_remaining = false; + ListCell* resultRel = NULL; + bool rewriteView = false; + List* rewriteRelations = NIL; result_relation = linitial_int(parsetree->resultRelations); @@ -2690,14 +3766,14 @@ static List* RewriteQuery(Query* parsetree, List* rewrite_events) if (IsA(rtr, RangeTblRef)) { RangeTblEntry* rte = rt_fetch(rtr->rtindex, parsetree->rtable); - if (rte->rtekind == RTE_VALUES) + if (rte->rtekind == RTE_VALUES) { values_rte = rte; + values_rte_index = rtr->rtindex; + } } } if (values_rte != NULL) { - List* attrnos = NIL; - /* Process the main targetlist ... */ parsetree->targetList = rewriteTargetListIU(parsetree->targetList, parsetree->commandType, @@ -2705,7 +3781,9 @@ static List* RewriteQuery(Query* parsetree, List* rewrite_events) &hasGenCol); checkGenDefault(values_rte, rt_entry_relation, attrnos, hasGenCol); /* ... and the VALUES expression lists */ - rewriteValuesRTE(values_rte, rt_entry_relation, attrnos); + if (!rewriteValuesRTE(parsetree, values_rte, rt_entry_relation, attrnos, false)) { + defaults_remaining = true; + } } else { /* Process just the main targetlist */ parsetree->targetList = @@ -2724,11 +3802,15 @@ static List* RewriteQuery(Query* parsetree, List* rewrite_events) } else if (event == CMD_UPDATE) { if (list_length(parsetree->resultRelations) > 1) { rewriteTargetListMutilUpdate(parsetree, parsetree->rtable, parsetree->resultRelations); + /* Also populate extraUpdatedCols (for generated columns) */ + multiUpdateSetExtraUpdatedCols(parsetree); } else { parsetree->targetList = rewriteTargetListIU(parsetree->targetList, parsetree->commandType, rt_entry_relation, result_relation, NULL, &hasGenCol); + /* Also populate extraUpdatedCols (for generated columns) */ + setExtraUpdatedCols(rt_entry, rt_entry_relation->rd_att); rewriteTargetListUD(parsetree, rt_entry, rt_entry_relation, result_relation); } } else if (event == CMD_DELETE) { @@ -2775,6 +3857,8 @@ static List* RewriteQuery(Query* parsetree, List* rewrite_events) } parsetree->upsertQuery = (Query*)linitial(querytree_list); } + /* Also populate extraUpdatedCols (for generated columns) */ + setExtraUpdatedCols(rt_entry, rt_entry_relation->rd_att); } else { ereport(ERROR, (errcode(ERRCODE_INVALID_OPERATION), errmsg("unrecognized commandType: %d", (int)event))); } @@ -2787,49 +3871,194 @@ static List* RewriteQuery(Query* parsetree, List* rewrite_events) */ locks = matchLocks(event, rt_entry_relation->rd_rules, result_relation, parsetree); - if (locks != NIL) { - List* product_queries = NIL; - #ifdef ENABLE_MULTIPLE_NODES - if (IS_PGXC_COORDINATOR) { - product_queries = - fireRules(parsetree, result_relation, event, locks, &instead, &returning, &qual_product); - } -#else + if (IS_PGXC_COORDINATOR) { product_queries = fireRules(parsetree, result_relation, event, locks, &instead, &returning, &qual_product); + } +#else + product_queries = + fireRules(parsetree, result_relation, event, locks, &instead, &returning, &qual_product); #endif - /* - * If we got any product queries, recursively rewrite them --- but - * first check for recursion! - */ - if (product_queries != NIL) { - ListCell* n = NULL; - rewrite_event* rev = NULL; + /* + * Relation has rules in multiple-relations modifying doesn't support, + * which is checked in CheckUDRelations. + */ + Assert(list_length(parsetree->resultRelations) <= 1 || product_queries == NULL); - foreach (n, rewrite_events) { - rev = (rewrite_event*)lfirst(n); - if (rev->relation == RelationGetRelid(rt_entry_relation) && rev->event == event) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("infinite recursion detected in rules for relation \"%s\"", - RelationGetRelationName(rt_entry_relation)))); + /* + * If we have a VALUES RTE with any remaining untouched DEFAULT items, + * and we got any product queries, finalize the VALUES RTE for each + * product query (replacing the remaining DEFAULT items with NULLs). + * We don't do this for the original query, because we know that it + * must be an auto-insert on a view, and so should use the base + * relation's defaults for any remaining DEFAULT items. + */ + if (defaults_remaining && product_queries != NIL) { + ListCell* n = NULL; + + /* + * Each product query has its own copy of the VALUES RTE at the + * same index in the rangetable, so we must finalize each one. + */ + foreach(n, product_queries) { + Query* pt = (Query*)lfirst(n); + RangeTblEntry* values_rte = rt_fetch(values_rte_index, pt->rtable); + + rewriteValuesRTE(pt, values_rte, rt_entry_relation, attrnos, true); + } + } + + if (product_queries != NIL) { + rewriteRelations = lappend_oid(rewriteRelations, RelationGetRelid(rt_entry_relation)); + } + + /* Element of resultRelations may be deleted under rewriteTargetView. */ + List* tempResultRelations = (List*)copyObject(parsetree->resultRelations); + foreach (resultRel, tempResultRelations) { + result_relation = lfirst_int(resultRel); + + heap_close(rt_entry_relation, NoLock); + + rt_entry = rt_fetch(result_relation, parsetree->rtable); + rt_entry_relation = heap_open(rt_entry->relid, NoLock); + + /* + * If there was no unqualified INSTEAD rule, and the target relation + * is a view without any INSTEAD OF triggers, see if the view can be + * automatically updated. If so, we perform the necessary query + * transformation here and add the resulting query to the + * product_queries list, so that it gets recursively rewritten if + * necessary. + * + * If the view cannot be automatically updated, we throw an error here + * which is OK since the query would fail at runtime anyway. Throwing + * the error here is preferable to the executor check since we have + * more detailed information available about why the view isn't + * updatable. + */ + if (!instead && rt_entry_relation->rd_rel->relkind == RELKIND_VIEW && + !view_has_instead_trigger(rt_entry_relation, event)) { + /* + * If there were any qualified INSTEAD rules, don't allow the view + * to be automatically updated (an unqualified INSTEAD rule or + * INSTEAD OF trigger is required). + * + * The messages here should match execMain.c's CheckValidResultRel + * and in principle make those checks in executor unnecessary, but + * we keep them just in case. + */ + if (qual_product != NULL) { + switch (parsetree->commandType) { + case CMD_INSERT: + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot insert into view \"%s\"", + RelationGetRelationName(rt_entry_relation)), + errdetail("Views with conditional DO INSTEAD rules are not " + "automatically updatable."), + errhint("To enable inserting into the view, provide an INSTEAD OF INSERT trigger " + "or an unconditional ON INSERT DO INSTEAD rule."))); + break; + case CMD_UPDATE: + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot update view \"%s\"", + RelationGetRelationName(rt_entry_relation)), + errdetail("Views with conditional DO INSTEAD rules are not " + "automatically updatable."), + errhint("To enable updating the view, provide an INSTEAD OF UPDATE trigger " + "or an unconditional ON UPDATE DO INSTEAD rule."))); + break; + case CMD_DELETE: + ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot delete from view \"%s\"", + RelationGetRelationName(rt_entry_relation)), + errdetail("Views with conditional DO INSTEAD rules are not " + "automatically updatable."), + errhint("To enable deleting from the view, provide an INSTEAD OF DELETE trigger " + "or an unconditional ON DELETE DO INSTEAD rule."))); + break; + default: + ereport(ERROR, (errmsg("unrecognized CmdType: %d", (int)parsetree->commandType))); + break; + } } + /* + * Attempt to rewrite the query to automatically update the view. + * This throws an error if the view can't be automatically + * updated. + */ + parsetree = rewriteTargetView(parsetree, rt_entry_relation, result_relation); + + /* + * Set the "instead" flag, as if there had been an unqualified + * INSTEAD, to prevent the original query from being included a + * second time below. The transformation will have rewritten any + * RETURNING list, so we can also set "returning" to forestall + * throwing an error below. + */ + instead = true; + returning = true; + rewriteView = true; + + /* + * If there were any unqualified INSTEAD rules, rewriteRelations + * may contain duplicated oid. But it is ok cause we just use the + * list to check if recursive. + */ + rewriteRelations = lappend_oid(rewriteRelations, RelationGetRelid(rt_entry_relation)); + } + } + pfree(tempResultRelations); + + /* + * At this point product_queries contains any DO ALSO rule actions. + * Add the rewritten query before or after those. This must match + * the handling the original query would have gotten below, if + * we allowed it to be included again. + */ + if (rewriteView) { + if (parsetree->commandType == CMD_INSERT) + product_queries = lcons(parsetree, product_queries); + else + product_queries = lappend(product_queries, parsetree); + } + + /* + * If we got any product queries, recursively rewrite them --- but + * first check for recursion! + */ + if (product_queries != NIL) { + ListCell* n = NULL; + rewrite_event* rev = NULL; + int i; + + foreach (n, rewrite_events) { + rev = (rewrite_event*)lfirst(n); + if (list_member_oid(rewriteRelations, rev->relation) && rev->event == event) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("infinite recursion detected in rules for relation \"%s\"", + RelationGetRelationName(RelationIdGetRelation(rev->relation))))); + } + + foreach (n, rewriteRelations) { rev = (rewrite_event*)palloc(sizeof(rewrite_event)); - rev->relation = RelationGetRelid(rt_entry_relation); + rev->relation = lfirst_oid(n); rev->event = event; rewrite_events = lcons(rev, rewrite_events); + } - foreach (n, product_queries) { - Query* pt = (Query*)lfirst(n); - List* newstuff = NULL; + foreach (n, product_queries) { + Query* pt = (Query*)lfirst(n); + List* newstuff = NULL; - newstuff = RewriteQuery(pt, rewrite_events); - rewritten = list_concat(rewritten, newstuff); - } + newstuff = RewriteQuery(pt, rewrite_events); + rewritten = list_concat(rewritten, newstuff); + } + for (i = 0; i < list_length(rewriteRelations); i++) { rewrite_events = list_delete_first(rewrite_events); } } @@ -2840,6 +4069,8 @@ static List* RewriteQuery(Query* parsetree, List* rewrite_events) * DefineQueryRewrite only allows RETURNING in unconditional INSTEAD * rules, there's no need to worry whether the substituted RETURNING * will actually be executed --- it must be.) + * + * Ignore for multiple modifying cause returning clause is not supported. */ if ((instead || qual_product != NULL) && parsetree->returningList && !returning) { ereport_for_each_cmdtype(event, rt_entry_relation); diff --git a/src/gausskernel/optimizer/rewrite/rewriteManip.cpp b/src/gausskernel/optimizer/rewrite/rewriteManip.cpp index f86fa3489..cd2776494 100644 --- a/src/gausskernel/optimizer/rewrite/rewriteManip.cpp +++ b/src/gausskernel/optimizer/rewrite/rewriteManip.cpp @@ -486,6 +486,18 @@ static bool ChangeVarNodes_walker(Node* node, ChangeVarNodes_context* context) /* the subquery itself is visited separately */ return false; } + if (IsA(node, TargetEntry)) { + TargetEntry* te = (TargetEntry*)node; + + if (context->sublevels_up == 0 && te->rtindex == (unsigned int)context->rt_index) + te->rtindex = context->new_index; + } + if (IsA(node, WithCheckOption)) { + WithCheckOption* wco = (WithCheckOption*)node; + + if (context->sublevels_up == 0 && wco->rtindex == (unsigned int)context->rt_index) + wco->rtindex = context->new_index; + } if (IsA(node, JoinExpr)) { JoinExpr* j = (JoinExpr*)node; @@ -567,8 +579,11 @@ void ChangeVarNodes(Node* node, int rt_index, int new_index, int sublevels_up) if (sublevels_up == 0) { ListCell* l = NULL; - if (linitial2_int(qry->resultRelations) == rt_index) - linitial_int(qry->resultRelations) = new_index; + foreach (l, qry->resultRelations) { + if (lfirst_int(l) == rt_index) { + lfirst_int(l) = new_index; + } + } foreach (l, qry->rowMarks) { RowMarkClause* rc = (RowMarkClause*)lfirst(l); @@ -1208,12 +1223,16 @@ Node* map_variable_attnos( } /* - * ResolveNew - replace Vars with corresponding items from a targetlist + * ReplaceVarsFromTargetList - replace Vars with items from a targetlist * * Vars matching target_varno and sublevels_up are replaced by the * entry with matching resno from targetlist, if there is one. - * If not, we either change the unmatched Var's varno to update_varno - * (when event == CMD_UPDATE) or replace it with a constant NULL. + * + * If there is no matching resno for such a Var, the action depends on the + * nomatch_option: + * REPLACEVARS_REPORT_ERROR: throw an error + * REPLACEVARS_CHANGE_VARNO: change Var's varno to nomatch_varno + * REPLACEVARS_SUBSTITUTE_NULL: replace Var with a NULL Const of same type * * The caller must also provide target_rte, the RTE describing the target * relation. This is needed to handle whole-row Vars referencing the target. @@ -1224,13 +1243,13 @@ Node* map_variable_attnos( typedef struct { RangeTblEntry* target_rte; List* targetlist; - int event; - int update_varno; -} ResolveNew_context; + ReplaceVarsNoMatchOption nomatch_option; + int nomatch_varno; +} ReplaceVarsFromTargetList_context; -static Node* ResolveNew_callback(Var* var, replace_rte_variables_context* context) +static Node* ReplaceVarsFromTargetList_callback(Var* var, replace_rte_variables_context* context) { - ResolveNew_context* rcon = (ResolveNew_context*)context->callback_arg; + ReplaceVarsFromTargetList_context* rcon = (ReplaceVarsFromTargetList_context*)context->callback_arg; TargetEntry* tle = NULL; if (var->varattno == InvalidAttrNumber) { @@ -1269,24 +1288,33 @@ static Node* ResolveNew_callback(Var* var, replace_rte_variables_context* contex tle = get_tle_by_resno(rcon->targetlist, var->varattno); if (tle == NULL || tle->resjunk) { /* Failed to find column in insert/update tlist */ - if (rcon->event == CMD_UPDATE) { - /* For update, just change unmatched var's varno */ - var = (Var*)copyObject(var); - var->varno = rcon->update_varno; - var->varnoold = rcon->update_varno; - return (Node*)var; - } else { - /* Otherwise replace unmatched var with a null */ - /* need coerce_to_domain in case of NOT NULL domain constraint */ - return coerce_to_domain((Node*)makeNullConst(var->vartype, var->vartypmod, var->varcollid), - InvalidOid, - -1, - var->vartype, - COERCE_IMPLICIT_CAST, - -1, - false, - false); + switch (rcon->nomatch_option) { + case REPLACEVARS_REPORT_ERROR: + /* fall through, throw error below */ + break; + + case REPLACEVARS_CHANGE_VARNO: + var = (Var*)copyObject(var); + var->varno = rcon->nomatch_varno; + var->varnoold = rcon->nomatch_varno; + return (Node*)var; + + case REPLACEVARS_SUBSTITUTE_NULL: + /* + * If Var is of domain type, we should add a CoerceToDomain + * node, in case there is a NOT NULL domain constraint. + */ + return coerce_to_domain((Node*)makeNullConst(var->vartype, var->vartypmod, var->varcollid), + InvalidOid, + -1, + var->vartype, + COERCE_IMPLICIT_CAST, + -1, + false, + false); } + ereport(ERROR, (errmsg("could not find replacement targetlist entry for attno %d", var->varattno))); + return NULL; /* keep compiler quiet */ } else { /* Make a copy of the tlist item to return */ Node* newnode = (Node*)copyObject(tle->expr); @@ -1299,16 +1327,16 @@ static Node* ResolveNew_callback(Var* var, replace_rte_variables_context* contex } } -Node* ResolveNew(Node* node, int target_varno, int sublevels_up, RangeTblEntry* target_rte, List* targetlist, int event, - int update_varno, bool* outer_hasSubLinks) +Node* ReplaceVarsFromTargetList(Node* node, int target_varno, int sublevels_up, RangeTblEntry* target_rte, + List* targetlist, ReplaceVarsNoMatchOption nomatch_option, int nomatch_varno, bool* outer_hasSubLinks) { - ResolveNew_context context; + ReplaceVarsFromTargetList_context context; context.target_rte = target_rte; context.targetlist = targetlist; - context.event = event; - context.update_varno = update_varno; + context.nomatch_option = nomatch_option; + context.nomatch_varno = nomatch_varno; return replace_rte_variables( - node, target_varno, sublevels_up, ResolveNew_callback, (void*)&context, outer_hasSubLinks); + node, target_varno, sublevels_up, ReplaceVarsFromTargetList_callback, (void*)&context, outer_hasSubLinks); } diff --git a/src/gausskernel/runtime/executor/execMain.cpp b/src/gausskernel/runtime/executor/execMain.cpp index 0d0efc19e..3fbb5cdfb 100755 --- a/src/gausskernel/runtime/executor/execMain.cpp +++ b/src/gausskernel/runtime/executor/execMain.cpp @@ -1578,9 +1578,8 @@ void InitPlan(QueryDesc *queryDesc, int eflags) /* * Check that a proposed result relation is a legal target for the operation * - * In most cases parser and/or planner should have noticed this already, but - * let's make sure. In the view case we do need a test here, because if the - * view wasn't rewritten by a rule, it had better have an INSTEAD trigger. + * Generally the parser and/or planner should have noticed any such mistake + * already, but let's make sure. * * Note: when changing this function, you probably also need to look at * CheckValidRowMarkRel. @@ -1610,13 +1609,20 @@ void CheckValidResultRel(Relation resultRel, CmdType operation) break; case RELKIND_VIEW: case RELKIND_CONTQUERY: + /* + * Okay only if there's a suitable INSTEAD OF trigger. Messages + * here should match rewriteHandler.c's rewriteTargetView and + * RewriteQuery, except that we omit errdetail because we haven't + * got the information handy (and given that we really shouldn't + * get here anyway, it's not worth great exertion to get). + */ switch (operation) { case CMD_INSERT: if (trigDesc == NULL || !trigDesc->trig_insert_instead_row) { ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("cannot insert into view \"%s\"", RelationGetRelationName(resultRel)), - errhint("You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT " - "trigger."))); + errhint("To enable inserting into the view, provide an INSTEAD OF INSERT trigger or " + "an unconditional ON INSERT DO INSTEAD rule."))); } break; @@ -1624,16 +1630,16 @@ void CheckValidResultRel(Relation resultRel, CmdType operation) if (trigDesc == NULL || !trigDesc->trig_update_instead_row) { ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("cannot update view \"%s\"", RelationGetRelationName(resultRel)), - errhint("You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE " - "trigger."))); + errhint("To enable updating the view, provide an INSTEAD OF UPDATE trigger or " + "an unconditional ON UPDATE DO INSTEAD rule."))); } break; case CMD_DELETE: if (trigDesc == NULL || !trigDesc->trig_delete_instead_row) { ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("cannot delete from view \"%s\"", RelationGetRelationName(resultRel)), - errhint("You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE " - "trigger."))); + errhint("To enable deleting from the view, provide an INSTEAD OF DELETE trigger or " + "an unconditional ON DELETE DO INSTEAD rule."))); } break; default: @@ -1719,7 +1725,7 @@ static void CheckValidRowMarkRel(Relation rel, RowMarkType markType) errmsg("cannot lock rows in TOAST relation \"%s\"", RelationGetRelationName(rel)))); break; case RELKIND_VIEW: - /* Should not get here */ + /* Should not get here; planner should have expanded the view */ ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot lock rows in view \"%s\"", RelationGetRelationName(rel)))); break; @@ -2590,6 +2596,57 @@ bool ExecConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, EState return true; } +/* + * ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs + */ +void ExecWithCheckOptions(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, EState *estate) +{ + Relation rel = resultRelInfo->ri_RelationDesc; + TupleDesc tupdesc = RelationGetDescr(rel); + ExprContext* econtext = NULL; + ListCell *l1 = NULL, *l2 = NULL; + + /* + * We will use the EState's per-tuple context for evaluating constraint + * expressions (creating it if it's not already there). + */ + econtext = GetPerTupleExprContext(estate); + + /* Arrange for econtext's scan tuple to be the tuple under test */ + econtext->ecxt_scantuple = slot; + + /* Check each of the constraints */ + forboth (l1, resultRelInfo->ri_WithCheckOptions, l2, resultRelInfo->ri_WithCheckOptionExprs) { + WithCheckOption* wco = (WithCheckOption*)lfirst(l1); + ExprState* wcoExpr = (ExprState*)lfirst(l2); + Bitmapset* modifiedCols = NULL; + Bitmapset *insertedCols = NULL; + Bitmapset *updatedCols = NULL; + char* val_desc = NULL; + + insertedCols = GetInsertedColumns(resultRelInfo, estate); + updatedCols = GetUpdatedColumns(resultRelInfo, estate); + modifiedCols = bms_union(insertedCols, updatedCols); + val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel), + slot, + tupdesc, + modifiedCols, + 64); + + /* + * WITH CHECK OPTION checks are intended to ensure that the new tuple + * is visible in the view. If the view's qual evaluates to NULL, then + * the new tuple won't be included in the view. Therefore we need to + * tell ExecQual to return FALSE for NULL (the opposite of what we do + * above for CHECK constraints). + */ + if (!ExecQual((List*)wcoExpr, econtext, false)) + ereport(ERROR, (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION), + errmsg("new row violates WITH CHECK OPTION for view \"%s\"", wco->viewname), + val_desc ? errdetail("Failing row contains %s.", val_desc) : 0)); + } +} + /* * ExecBuildSlotValueDescription -- construct a string representing a tuple * diff --git a/src/gausskernel/runtime/executor/nodeModifyTable.cpp b/src/gausskernel/runtime/executor/nodeModifyTable.cpp index 82ad8da0c..ee61defd3 100644 --- a/src/gausskernel/runtime/executor/nodeModifyTable.cpp +++ b/src/gausskernel/runtime/executor/nodeModifyTable.cpp @@ -1564,6 +1564,10 @@ TupleTableSlot* ExecInsertT(ModifyTableState* state, TupleTableSlot* slot, Tuple list_free_ext(recheck_indexes); + /* Check any WITH CHECK OPTION constraints */ + if (result_rel_info->ri_WithCheckOptions != NIL) + ExecWithCheckOptions(result_rel_info, slot, estate); + /* Process RETURNING if present */ if (result_rel_info->ri_projectReturning) #ifdef PGXC @@ -3050,6 +3054,10 @@ ldelete: list_free_ext(recheck_indexes); + /* Check any WITH CHECK OPTION constraints */ + if (result_rel_info->ri_WithCheckOptions != NIL) + ExecWithCheckOptions(result_rel_info, slot, estate); + /* Process RETURNING if present */ if (result_rel_info->ri_projectReturning) #ifdef PGXC @@ -3986,6 +3994,37 @@ ModifyTableState* ExecInitModifyTable(ModifyTable* node, EState* estate, int efl estate->es_result_remoterel = saved_remote_rel_info; #endif + /* + * Initialize any WITH CHECK OPTION constraints if needed. + */ + result_rel_info = mt_state->resultRelInfo; + i = 0; + + /* + * length of node->withCheckOptionLists must be 1 because inherited + * table is not supported yet. + */ + Assert(node->withCheckOptionLists == NULL || list_length(node->withCheckOptionLists) == 1); + + for (int ri = 0; node->withCheckOptionLists != NULL && ri < resultRelationNum; ri++) { + List* wcoList = NIL; + List* wcoExprs = NIL; + ListCell* ll = NULL; + + foreach(ll, (List*)linitial(node->withCheckOptionLists)) { + WithCheckOption* wco = (WithCheckOption*)lfirst(ll); + if (wco->rtindex == result_rel_info->ri_RangeTableIndex) { + ExprState* wcoExpr = ExecInitExpr((Expr*)wco->qual, mt_state->mt_plans[i]); + wcoExprs = lappend(wcoExprs, wcoExpr); + wcoList = lappend(wcoList, wco); + } + } + + result_rel_info->ri_WithCheckOptions = wcoList; + result_rel_info->ri_WithCheckOptionExprs = wcoExprs; + result_rel_info++; + } + /* * Initialize RETURNING projections if needed. */ diff --git a/src/gausskernel/runtime/opfusion/opfusion_insert.cpp b/src/gausskernel/runtime/opfusion/opfusion_insert.cpp index c12f19b0b..739ea7195 100644 --- a/src/gausskernel/runtime/opfusion/opfusion_insert.cpp +++ b/src/gausskernel/runtime/opfusion/opfusion_insert.cpp @@ -391,6 +391,9 @@ unsigned long InsertFusion::ExecInsert(Relation rel, ResultRelInfo* result_rel_i list_free_ext(recheck_indexes); + if (result_rel_info->ri_WithCheckOptions != NIL) + ExecWithCheckOptions(result_rel_info, m_local.m_reslot, m_c_local.m_estate); + tableam_tops_free_tuple(tuple); (void)ExecClearTuple(m_local.m_reslot); @@ -436,6 +439,24 @@ bool InsertFusion::execute(long max_rows, char* completionTag) m_c_local.m_estate->es_plannedstmt = m_global->m_planstmt; refreshParameterIfNecessary(); + ModifyTable* node = (ModifyTable*)(m_global->m_planstmt->planTree); + if (node->withCheckOptionLists != NIL) { + Plan* plan = (Plan*)linitial(node->plans); + PlanState* ps = ExecInitNode(plan, m_c_local.m_estate, 0); + List* wcoList = (List*)linitial(node->withCheckOptionLists); + List* wcoExprs = NIL; + ListCell* ll = NULL; + + foreach(ll, wcoList) { + WithCheckOption* wco = (WithCheckOption*)lfirst(ll); + ExprState* wcoExpr = ExecInitExpr((Expr*)wco->qual, ps); + wcoExprs = lappend(wcoExprs, wcoExpr); + } + + result_rel_info->ri_WithCheckOptions = wcoList; + result_rel_info->ri_WithCheckOptionExprs = wcoExprs; + } + /************************ * step 2: begin insert * ************************/ diff --git a/src/gausskernel/runtime/opfusion/opfusion_update.cpp b/src/gausskernel/runtime/opfusion/opfusion_update.cpp index 73b80298c..b13f32a89 100644 --- a/src/gausskernel/runtime/opfusion/opfusion_update.cpp +++ b/src/gausskernel/runtime/opfusion/opfusion_update.cpp @@ -523,6 +523,9 @@ lreplace: list_free_ext(recheck_indexes); } + if (result_rel_info->ri_WithCheckOptions != NIL) + ExecWithCheckOptions(result_rel_info, m_local.m_reslot, m_c_local.m_estate); + tableam_tops_free_tuple(tup); (void)ExecClearTuple(m_local.m_reslot); @@ -567,6 +570,24 @@ bool UpdateFusion::execute(long max_rows, char *completionTag) ExecOpenIndices(result_rel_info, true); } + ModifyTable* node = (ModifyTable*)(m_global->m_planstmt->planTree); + if (node->withCheckOptionLists != NIL) { + Plan* plan = (Plan*)linitial(node->plans); + PlanState* ps = ExecInitNode(plan, m_c_local.m_estate, 0); + List* wcoList = (List*)linitial(node->withCheckOptionLists); + List* wcoExprs = NIL; + ListCell* ll = NULL; + + foreach(ll, wcoList) { + WithCheckOption* wco = (WithCheckOption*)lfirst(ll); + ExprState* wcoExpr = ExecInitExpr((Expr*)wco->qual, ps); + wcoExprs = lappend(wcoExprs, wcoExpr); + } + + result_rel_info->ri_WithCheckOptions = wcoList; + result_rel_info->ri_WithCheckOptionExprs = wcoExprs; + } + /* ******************************** * step 2: begin scan and update * * ******************************* */ diff --git a/src/gausskernel/storage/access/common/reloptions.cpp b/src/gausskernel/storage/access/common/reloptions.cpp index 4ee0c78c6..4c9abd30e 100644 --- a/src/gausskernel/storage/access/common/reloptions.cpp +++ b/src/gausskernel/storage/access/common/reloptions.cpp @@ -26,6 +26,7 @@ #include "catalog/pg_type.h" #include "commands/defrem.h" #include "commands/tablespace.h" +#include "commands/view.h" #include "nodes/makefuncs.h" #include "pgxc/redistrib.h" #ifdef ENABLE_MULTIPLE_NODES @@ -511,6 +512,13 @@ static relopt_string stringRelOpts[] = { ValidateStrOptStringOptimize, COLUMN_UNDEFINED, }, + { + {"check_option", "View has WITH CHECK OPTION defined (local or cascaded).", RELOPT_KIND_VIEW}, + 0, + true, + validateWithCheckOption, + NULL + }, /* list terminator */ {{NULL}} }; @@ -1975,7 +1983,8 @@ bytea *default_reloptions(Datum reloptions, bool validate, relopt_kind kind) { "compress_byte_convert", RELOPT_TYPE_BOOL, offsetof(StdRdOptions, compress) + offsetof(PageCompressOpts, compressByteConvert)}, { "compress_diff_convert", RELOPT_TYPE_BOOL, - offsetof(StdRdOptions, compress) + offsetof(PageCompressOpts, compressDiffConvert)} + offsetof(StdRdOptions, compress) + offsetof(PageCompressOpts, compressDiffConvert)}, + { "check_option", RELOPT_TYPE_STRING, offsetof(StdRdOptions, check_option_offset)} }; options = parseRelOptions(reloptions, validate, kind, &numoptions); diff --git a/src/include/catalog/upgrade_sql/rollback_catalog_maindb/rollback-post_catalog_maindb_92_838.sql b/src/include/catalog/upgrade_sql/rollback_catalog_maindb/rollback-post_catalog_maindb_92_838.sql new file mode 100644 index 000000000..d5d4cc9dd --- /dev/null +++ b/src/include/catalog/upgrade_sql/rollback_catalog_maindb/rollback-post_catalog_maindb_92_838.sql @@ -0,0 +1,218 @@ +DROP FUNCTION IF EXISTS pg_catalog.pg_relation_is_updatable(oid, bool); + +DROP FUNCTION IF EXISTS pg_catalog.pg_column_is_updatable(oid, int2, bool); + +SET search_path TO information_schema; +CREATE OR REPLACE VIEW columns AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + CAST(a.attname AS sql_identifier) AS column_name, + CAST(a.attnum AS cardinal_number) AS ordinal_position, + CAST(CASE WHEN ad.adgencol <> 's' THEN pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS column_default, + CAST(CASE WHEN a.attnotnull OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END + AS yes_or_no) + AS is_nullable, + + CAST( + CASE WHEN t.typtype = 'd' THEN + CASE WHEN bt.typelem <> 0 AND bt.typlen = -1 THEN 'ARRAY' + WHEN nbt.nspname = 'pg_catalog' THEN pg_catalog.format_type(t.typbasetype, null) + ELSE 'USER-DEFINED' END + ELSE + CASE WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY' + WHEN nt.nspname = 'pg_catalog' THEN pg_catalog.format_type(a.atttypid, null) + ELSE 'USER-DEFINED' END + END + AS character_data) + AS data_type, + + CAST( + _pg_char_max_length(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS character_maximum_length, + + CAST( + _pg_char_octet_length(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS character_octet_length, + + CAST( + _pg_numeric_precision(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_precision, + + CAST( + _pg_numeric_precision_radix(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_precision_radix, + + CAST( + _pg_numeric_scale(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_scale, + + CAST( + _pg_datetime_precision(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS datetime_precision, + + CAST( + _pg_interval_type(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS character_data) + AS interval_type, + CAST(null AS cardinal_number) AS interval_precision, + + CAST(null AS sql_identifier) AS character_set_catalog, + CAST(null AS sql_identifier) AS character_set_schema, + CAST(null AS sql_identifier) AS character_set_name, + + CAST(CASE WHEN nco.nspname IS NOT NULL THEN pg_catalog.current_database() END AS sql_identifier) AS collation_catalog, + CAST(nco.nspname AS sql_identifier) AS collation_schema, + CAST(co.collname AS sql_identifier) AS collation_name, + + CAST(CASE WHEN t.typtype = 'd' THEN pg_catalog.current_database() ELSE null END + AS sql_identifier) AS domain_catalog, + CAST(CASE WHEN t.typtype = 'd' THEN nt.nspname ELSE null END + AS sql_identifier) AS domain_schema, + CAST(CASE WHEN t.typtype = 'd' THEN t.typname ELSE null END + AS sql_identifier) AS domain_name, + + CAST(pg_catalog.current_database() AS sql_identifier) AS udt_catalog, + CAST(coalesce(nbt.nspname, nt.nspname) AS sql_identifier) AS udt_schema, + CAST(coalesce(bt.typname, t.typname) AS sql_identifier) AS udt_name, + + CAST(null AS sql_identifier) AS scope_catalog, + CAST(null AS sql_identifier) AS scope_schema, + CAST(null AS sql_identifier) AS scope_name, + + CAST(null AS cardinal_number) AS maximum_cardinality, + CAST(a.attnum AS sql_identifier) AS dtd_identifier, + CAST('NO' AS yes_or_no) AS is_self_referencing, + + CAST('NO' AS yes_or_no) AS is_identity, + CAST(null AS character_data) AS identity_generation, + CAST(null AS character_data) AS identity_start, + CAST(null AS character_data) AS identity_increment, + CAST(null AS character_data) AS identity_maximum, + CAST(null AS character_data) AS identity_minimum, + CAST(null AS yes_or_no) AS identity_cycle, + + CAST(CASE WHEN ad.adgencol = 's' THEN 'ALWAYS' ELSE 'NEVER' END AS character_data) AS is_generated, + CAST(CASE WHEN ad.adgencol = 's' THEN pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS generation_expression, + + CAST(CASE WHEN c.relkind = 'r' + OR (c.relkind = 'v' + AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '2' AND is_instead) + AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '4' AND is_instead)) + THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_updatable + + FROM (pg_attribute a LEFT JOIN pg_attrdef ad ON attrelid = adrelid AND attnum = adnum) + JOIN (pg_class c JOIN pg_namespace nc ON (c.relnamespace = nc.oid)) ON a.attrelid = c.oid + JOIN (pg_type t JOIN pg_namespace nt ON (t.typnamespace = nt.oid)) ON a.atttypid = t.oid + LEFT JOIN (pg_type bt JOIN pg_namespace nbt ON (bt.typnamespace = nbt.oid)) + ON (t.typtype = 'd' AND t.typbasetype = bt.oid) + LEFT JOIN (pg_collation co JOIN pg_namespace nco ON (co.collnamespace = nco.oid)) + ON a.attcollation = co.oid AND (nco.nspname, co.collname) <> ('pg_catalog', 'default') + + WHERE (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + + AND a.attnum > 0 AND NOT a.attisdropped AND c.relkind in ('r', 'm', 'v', 'f') + + AND (c.relname not like 'mlog\_%' AND c.relname not like 'matviewmap\_%') + + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_column_privilege(c.oid, a.attnum, + 'SELECT, INSERT, UPDATE, REFERENCES')); + +CREATE OR REPLACE VIEW tables AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + + CAST( + CASE WHEN nc.oid = pg_catalog.pg_my_temp_schema() THEN 'LOCAL TEMPORARY' + WHEN c.relkind = 'r' THEN 'BASE TABLE' + WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW' + WHEN c.relkind = 'v' THEN 'VIEW' + WHEN c.relkind = 'f' THEN 'FOREIGN TABLE' + ELSE null END + AS character_data) AS table_type, + + CAST(null AS sql_identifier) AS self_referencing_column_name, + CAST(null AS character_data) AS reference_generation, + + CAST(CASE WHEN t.typname IS NOT NULL THEN pg_catalog.current_database() ELSE null END AS sql_identifier) AS user_defined_type_catalog, + CAST(nt.nspname AS sql_identifier) AS user_defined_type_schema, + CAST(t.typname AS sql_identifier) AS user_defined_type_name, + + CAST(CASE WHEN c.relkind = 'r' + OR (c.relkind = 'v' + AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '3' AND is_instead)) + THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, + + CAST(CASE WHEN t.typname IS NOT NULL THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_typed, + CAST(null AS character_data) AS commit_action + + FROM pg_namespace nc JOIN pg_class c ON (nc.oid = c.relnamespace) + LEFT JOIN (pg_type t JOIN pg_namespace nt ON (t.typnamespace = nt.oid)) ON (c.reloftype = t.oid) + + WHERE c.relkind IN ('r', 'm', 'v', 'f') + AND (c.relname not like 'mlog\_%' AND c.relname not like 'matviewmap\_%') + AND (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') + OR pg_catalog.has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') ); + +CREATE OR REPLACE VIEW views AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + + CAST( + CASE WHEN pg_catalog.pg_has_role(c.relowner, 'USAGE') + THEN pg_catalog.pg_get_viewdef(c.oid) + ELSE null END + AS character_data) AS view_definition, + + CAST('NONE' AS character_data) AS check_option, + + CAST( + CASE WHEN EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '2' AND is_instead) + AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '4' AND is_instead) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_updatable, + + CAST( + CASE WHEN EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '3' AND is_instead) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_insertable_into, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_UPDATE + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 81 = 81) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_updatable, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_DELETE + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 73 = 73) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_deletable, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_INSERT + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 69 = 69) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_insertable_into + + FROM pg_namespace nc, pg_class c + + WHERE c.relnamespace = nc.oid + AND c.relkind = 'v' + AND (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') + OR pg_catalog.has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') ); + +reset search_path; \ No newline at end of file diff --git a/src/include/catalog/upgrade_sql/rollback_catalog_otherdb/rollback-post_catalog_otherdb_92_838.sql b/src/include/catalog/upgrade_sql/rollback_catalog_otherdb/rollback-post_catalog_otherdb_92_838.sql new file mode 100644 index 000000000..d5d4cc9dd --- /dev/null +++ b/src/include/catalog/upgrade_sql/rollback_catalog_otherdb/rollback-post_catalog_otherdb_92_838.sql @@ -0,0 +1,218 @@ +DROP FUNCTION IF EXISTS pg_catalog.pg_relation_is_updatable(oid, bool); + +DROP FUNCTION IF EXISTS pg_catalog.pg_column_is_updatable(oid, int2, bool); + +SET search_path TO information_schema; +CREATE OR REPLACE VIEW columns AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + CAST(a.attname AS sql_identifier) AS column_name, + CAST(a.attnum AS cardinal_number) AS ordinal_position, + CAST(CASE WHEN ad.adgencol <> 's' THEN pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS column_default, + CAST(CASE WHEN a.attnotnull OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END + AS yes_or_no) + AS is_nullable, + + CAST( + CASE WHEN t.typtype = 'd' THEN + CASE WHEN bt.typelem <> 0 AND bt.typlen = -1 THEN 'ARRAY' + WHEN nbt.nspname = 'pg_catalog' THEN pg_catalog.format_type(t.typbasetype, null) + ELSE 'USER-DEFINED' END + ELSE + CASE WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY' + WHEN nt.nspname = 'pg_catalog' THEN pg_catalog.format_type(a.atttypid, null) + ELSE 'USER-DEFINED' END + END + AS character_data) + AS data_type, + + CAST( + _pg_char_max_length(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS character_maximum_length, + + CAST( + _pg_char_octet_length(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS character_octet_length, + + CAST( + _pg_numeric_precision(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_precision, + + CAST( + _pg_numeric_precision_radix(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_precision_radix, + + CAST( + _pg_numeric_scale(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_scale, + + CAST( + _pg_datetime_precision(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS datetime_precision, + + CAST( + _pg_interval_type(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS character_data) + AS interval_type, + CAST(null AS cardinal_number) AS interval_precision, + + CAST(null AS sql_identifier) AS character_set_catalog, + CAST(null AS sql_identifier) AS character_set_schema, + CAST(null AS sql_identifier) AS character_set_name, + + CAST(CASE WHEN nco.nspname IS NOT NULL THEN pg_catalog.current_database() END AS sql_identifier) AS collation_catalog, + CAST(nco.nspname AS sql_identifier) AS collation_schema, + CAST(co.collname AS sql_identifier) AS collation_name, + + CAST(CASE WHEN t.typtype = 'd' THEN pg_catalog.current_database() ELSE null END + AS sql_identifier) AS domain_catalog, + CAST(CASE WHEN t.typtype = 'd' THEN nt.nspname ELSE null END + AS sql_identifier) AS domain_schema, + CAST(CASE WHEN t.typtype = 'd' THEN t.typname ELSE null END + AS sql_identifier) AS domain_name, + + CAST(pg_catalog.current_database() AS sql_identifier) AS udt_catalog, + CAST(coalesce(nbt.nspname, nt.nspname) AS sql_identifier) AS udt_schema, + CAST(coalesce(bt.typname, t.typname) AS sql_identifier) AS udt_name, + + CAST(null AS sql_identifier) AS scope_catalog, + CAST(null AS sql_identifier) AS scope_schema, + CAST(null AS sql_identifier) AS scope_name, + + CAST(null AS cardinal_number) AS maximum_cardinality, + CAST(a.attnum AS sql_identifier) AS dtd_identifier, + CAST('NO' AS yes_or_no) AS is_self_referencing, + + CAST('NO' AS yes_or_no) AS is_identity, + CAST(null AS character_data) AS identity_generation, + CAST(null AS character_data) AS identity_start, + CAST(null AS character_data) AS identity_increment, + CAST(null AS character_data) AS identity_maximum, + CAST(null AS character_data) AS identity_minimum, + CAST(null AS yes_or_no) AS identity_cycle, + + CAST(CASE WHEN ad.adgencol = 's' THEN 'ALWAYS' ELSE 'NEVER' END AS character_data) AS is_generated, + CAST(CASE WHEN ad.adgencol = 's' THEN pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS generation_expression, + + CAST(CASE WHEN c.relkind = 'r' + OR (c.relkind = 'v' + AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '2' AND is_instead) + AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '4' AND is_instead)) + THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_updatable + + FROM (pg_attribute a LEFT JOIN pg_attrdef ad ON attrelid = adrelid AND attnum = adnum) + JOIN (pg_class c JOIN pg_namespace nc ON (c.relnamespace = nc.oid)) ON a.attrelid = c.oid + JOIN (pg_type t JOIN pg_namespace nt ON (t.typnamespace = nt.oid)) ON a.atttypid = t.oid + LEFT JOIN (pg_type bt JOIN pg_namespace nbt ON (bt.typnamespace = nbt.oid)) + ON (t.typtype = 'd' AND t.typbasetype = bt.oid) + LEFT JOIN (pg_collation co JOIN pg_namespace nco ON (co.collnamespace = nco.oid)) + ON a.attcollation = co.oid AND (nco.nspname, co.collname) <> ('pg_catalog', 'default') + + WHERE (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + + AND a.attnum > 0 AND NOT a.attisdropped AND c.relkind in ('r', 'm', 'v', 'f') + + AND (c.relname not like 'mlog\_%' AND c.relname not like 'matviewmap\_%') + + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_column_privilege(c.oid, a.attnum, + 'SELECT, INSERT, UPDATE, REFERENCES')); + +CREATE OR REPLACE VIEW tables AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + + CAST( + CASE WHEN nc.oid = pg_catalog.pg_my_temp_schema() THEN 'LOCAL TEMPORARY' + WHEN c.relkind = 'r' THEN 'BASE TABLE' + WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW' + WHEN c.relkind = 'v' THEN 'VIEW' + WHEN c.relkind = 'f' THEN 'FOREIGN TABLE' + ELSE null END + AS character_data) AS table_type, + + CAST(null AS sql_identifier) AS self_referencing_column_name, + CAST(null AS character_data) AS reference_generation, + + CAST(CASE WHEN t.typname IS NOT NULL THEN pg_catalog.current_database() ELSE null END AS sql_identifier) AS user_defined_type_catalog, + CAST(nt.nspname AS sql_identifier) AS user_defined_type_schema, + CAST(t.typname AS sql_identifier) AS user_defined_type_name, + + CAST(CASE WHEN c.relkind = 'r' + OR (c.relkind = 'v' + AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '3' AND is_instead)) + THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, + + CAST(CASE WHEN t.typname IS NOT NULL THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_typed, + CAST(null AS character_data) AS commit_action + + FROM pg_namespace nc JOIN pg_class c ON (nc.oid = c.relnamespace) + LEFT JOIN (pg_type t JOIN pg_namespace nt ON (t.typnamespace = nt.oid)) ON (c.reloftype = t.oid) + + WHERE c.relkind IN ('r', 'm', 'v', 'f') + AND (c.relname not like 'mlog\_%' AND c.relname not like 'matviewmap\_%') + AND (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') + OR pg_catalog.has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') ); + +CREATE OR REPLACE VIEW views AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + + CAST( + CASE WHEN pg_catalog.pg_has_role(c.relowner, 'USAGE') + THEN pg_catalog.pg_get_viewdef(c.oid) + ELSE null END + AS character_data) AS view_definition, + + CAST('NONE' AS character_data) AS check_option, + + CAST( + CASE WHEN EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '2' AND is_instead) + AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '4' AND is_instead) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_updatable, + + CAST( + CASE WHEN EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '3' AND is_instead) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_insertable_into, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_UPDATE + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 81 = 81) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_updatable, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_DELETE + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 73 = 73) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_deletable, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_INSERT + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 69 = 69) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_insertable_into + + FROM pg_namespace nc, pg_class c + + WHERE c.relnamespace = nc.oid + AND c.relkind = 'v' + AND (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') + OR pg_catalog.has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') ); + +reset search_path; \ No newline at end of file diff --git a/src/include/catalog/upgrade_sql/upgrade_catalog_maindb/upgrade-post_catalog_maindb_92_838.sql b/src/include/catalog/upgrade_sql/upgrade_catalog_maindb/upgrade-post_catalog_maindb_92_838.sql new file mode 100644 index 000000000..b257649c7 --- /dev/null +++ b/src/include/catalog/upgrade_sql/upgrade_catalog_maindb/upgrade-post_catalog_maindb_92_838.sql @@ -0,0 +1,234 @@ +DROP FUNCTION IF EXISTS pg_catalog.pg_relation_is_updatable(oid, bool); +SET LOCAL inplace_upgrade_next_system_object_oids=IUO_PROC, 3846; +CREATE FUNCTION pg_catalog.pg_relation_is_updatable(oid, bool) +RETURNS int4 LANGUAGE INTERNAL AS 'pg_relation_is_updatable'; + +comment on function PG_CATALOG.pg_relation_is_updatable(oid, bool) is 'is a relation insertable/updatable/deletable'; + +DROP FUNCTION IF EXISTS pg_catalog.pg_column_is_updatable(oid, int2, bool); +SET LOCAL inplace_upgrade_next_system_object_oids=IUO_PROC, 3847; +CREATE FUNCTION pg_catalog.pg_column_is_updatable(oid, int2, bool) +RETURNS bool LANGUAGE INTERNAL AS 'pg_column_is_updatable'; + +comment on function PG_CATALOG.pg_column_is_updatable(oid, int2, bool) is 'is a column updatable'; + +SET search_path TO information_schema; +CREATE OR REPLACE VIEW columns AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + CAST(a.attname AS sql_identifier) AS column_name, + CAST(a.attnum AS cardinal_number) AS ordinal_position, + CAST(CASE WHEN ad.adgencol <> 's' THEN pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS column_default, + CAST(CASE WHEN a.attnotnull OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END + AS yes_or_no) + AS is_nullable, + + CAST( + CASE WHEN t.typtype = 'd' THEN + CASE WHEN bt.typelem <> 0 AND bt.typlen = -1 THEN 'ARRAY' + WHEN nbt.nspname = 'pg_catalog' THEN pg_catalog.format_type(t.typbasetype, null) + ELSE 'USER-DEFINED' END + ELSE + CASE WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY' + WHEN nt.nspname = 'pg_catalog' THEN pg_catalog.format_type(a.atttypid, null) + ELSE 'USER-DEFINED' END + END + AS character_data) + AS data_type, + + CAST( + _pg_char_max_length(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS character_maximum_length, + + CAST( + _pg_char_octet_length(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS character_octet_length, + + CAST( + _pg_numeric_precision(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_precision, + + CAST( + _pg_numeric_precision_radix(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_precision_radix, + + CAST( + _pg_numeric_scale(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_scale, + + CAST( + _pg_datetime_precision(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS datetime_precision, + + CAST( + _pg_interval_type(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS character_data) + AS interval_type, + CAST(null AS cardinal_number) AS interval_precision, + + CAST(null AS sql_identifier) AS character_set_catalog, + CAST(null AS sql_identifier) AS character_set_schema, + CAST(null AS sql_identifier) AS character_set_name, + + CAST(CASE WHEN nco.nspname IS NOT NULL THEN pg_catalog.current_database() END AS sql_identifier) AS collation_catalog, + CAST(nco.nspname AS sql_identifier) AS collation_schema, + CAST(co.collname AS sql_identifier) AS collation_name, + + CAST(CASE WHEN t.typtype = 'd' THEN pg_catalog.current_database() ELSE null END + AS sql_identifier) AS domain_catalog, + CAST(CASE WHEN t.typtype = 'd' THEN nt.nspname ELSE null END + AS sql_identifier) AS domain_schema, + CAST(CASE WHEN t.typtype = 'd' THEN t.typname ELSE null END + AS sql_identifier) AS domain_name, + + CAST(pg_catalog.current_database() AS sql_identifier) AS udt_catalog, + CAST(coalesce(nbt.nspname, nt.nspname) AS sql_identifier) AS udt_schema, + CAST(coalesce(bt.typname, t.typname) AS sql_identifier) AS udt_name, + + CAST(null AS sql_identifier) AS scope_catalog, + CAST(null AS sql_identifier) AS scope_schema, + CAST(null AS sql_identifier) AS scope_name, + + CAST(null AS cardinal_number) AS maximum_cardinality, + CAST(a.attnum AS sql_identifier) AS dtd_identifier, + CAST('NO' AS yes_or_no) AS is_self_referencing, + + CAST('NO' AS yes_or_no) AS is_identity, + CAST(null AS character_data) AS identity_generation, + CAST(null AS character_data) AS identity_start, + CAST(null AS character_data) AS identity_increment, + CAST(null AS character_data) AS identity_maximum, + CAST(null AS character_data) AS identity_minimum, + CAST(null AS yes_or_no) AS identity_cycle, + + CAST(CASE WHEN ad.adgencol = 's' THEN 'ALWAYS' ELSE 'NEVER' END AS character_data) AS is_generated, + CAST(CASE WHEN ad.adgencol = 's' THEN pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS generation_expression, + + CAST(CASE WHEN c.relkind = 'r' + OR (c.relkind in ('v', 'f') AND pg_column_is_updatable(c.oid, a.attnum, false)) + THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_updatable + + FROM (pg_attribute a LEFT JOIN pg_attrdef ad ON attrelid = adrelid AND attnum = adnum) + JOIN (pg_class c JOIN pg_namespace nc ON (c.relnamespace = nc.oid)) ON a.attrelid = c.oid + JOIN (pg_type t JOIN pg_namespace nt ON (t.typnamespace = nt.oid)) ON a.atttypid = t.oid + LEFT JOIN (pg_type bt JOIN pg_namespace nbt ON (bt.typnamespace = nbt.oid)) + ON (t.typtype = 'd' AND t.typbasetype = bt.oid) + LEFT JOIN (pg_collation co JOIN pg_namespace nco ON (co.collnamespace = nco.oid)) + ON a.attcollation = co.oid AND (nco.nspname, co.collname) <> ('pg_catalog', 'default') + + WHERE (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + + AND a.attnum > 0 AND NOT a.attisdropped AND c.relkind in ('r', 'm', 'v', 'f') + + AND (c.relname not like 'mlog\_%' AND c.relname not like 'matviewmap\_%') + + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_column_privilege(c.oid, a.attnum, + 'SELECT, INSERT, UPDATE, REFERENCES')); + +CREATE OR REPLACE VIEW tables AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + + CAST( + CASE WHEN nc.oid = pg_catalog.pg_my_temp_schema() THEN 'LOCAL TEMPORARY' + WHEN c.relkind = 'r' THEN 'BASE TABLE' + WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW' + WHEN c.relkind = 'v' THEN 'VIEW' + WHEN c.relkind = 'f' THEN 'FOREIGN TABLE' + ELSE null END + AS character_data) AS table_type, + + CAST(null AS sql_identifier) AS self_referencing_column_name, + CAST(null AS character_data) AS reference_generation, + + CAST(CASE WHEN t.typname IS NOT NULL THEN pg_catalog.current_database() ELSE null END AS sql_identifier) AS user_defined_type_catalog, + CAST(nt.nspname AS sql_identifier) AS user_defined_type_schema, + CAST(t.typname AS sql_identifier) AS user_defined_type_name, + + CAST(CASE WHEN c.relkind = 'r' OR + (c.relkind in ('v', 'f') AND + -- 1 << CMD_INSERT + pg_relation_is_updatable(c.oid, false) & 8 = 8) + THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, + + CAST(CASE WHEN t.typname IS NOT NULL THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_typed, + CAST(null AS character_data) AS commit_action + + FROM pg_namespace nc JOIN pg_class c ON (nc.oid = c.relnamespace) + LEFT JOIN (pg_type t JOIN pg_namespace nt ON (t.typnamespace = nt.oid)) ON (c.reloftype = t.oid) + + WHERE c.relkind IN ('r', 'm', 'v', 'f') + AND (c.relname not like 'mlog\_%' AND c.relname not like 'matviewmap\_%') + AND (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') + OR pg_catalog.has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') ); + +CREATE OR REPLACE VIEW views AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + + CAST( + CASE WHEN pg_catalog.pg_has_role(c.relowner, 'USAGE') + THEN pg_catalog.pg_get_viewdef(c.oid) + ELSE null END + AS character_data) AS view_definition, + + CAST( + CASE WHEN 'check_option=cascaded' = ANY (c.reloptions) + THEN 'CASCADED' + WHEN 'check_option=local' = ANY (c.reloptions) + THEN 'LOCAL' + ELSE 'NONE' END + AS character_data) AS check_option, + + CAST( + -- (1 << CMD_UPDATE) + (1 << CMD_DELETE) + CASE WHEN pg_relation_is_updatable(c.oid, false) & 20 = 20 + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_updatable, + + CAST( + -- 1 << CMD_INSERT + CASE WHEN pg_relation_is_updatable(c.oid, false) & 8 = 8 + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_insertable_into, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_UPDATE + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 81 = 81) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_updatable, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_DELETE + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 73 = 73) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_deletable, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_INSERT + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 69 = 69) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_insertable_into + + FROM pg_namespace nc, pg_class c + + WHERE c.relnamespace = nc.oid + AND c.relkind = 'v' + AND (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') + OR pg_catalog.has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') ); + +reset search_path; \ No newline at end of file diff --git a/src/include/catalog/upgrade_sql/upgrade_catalog_otherdb/upgrade-post_catalog_otherdb_92_838.sql b/src/include/catalog/upgrade_sql/upgrade_catalog_otherdb/upgrade-post_catalog_otherdb_92_838.sql new file mode 100644 index 000000000..b257649c7 --- /dev/null +++ b/src/include/catalog/upgrade_sql/upgrade_catalog_otherdb/upgrade-post_catalog_otherdb_92_838.sql @@ -0,0 +1,234 @@ +DROP FUNCTION IF EXISTS pg_catalog.pg_relation_is_updatable(oid, bool); +SET LOCAL inplace_upgrade_next_system_object_oids=IUO_PROC, 3846; +CREATE FUNCTION pg_catalog.pg_relation_is_updatable(oid, bool) +RETURNS int4 LANGUAGE INTERNAL AS 'pg_relation_is_updatable'; + +comment on function PG_CATALOG.pg_relation_is_updatable(oid, bool) is 'is a relation insertable/updatable/deletable'; + +DROP FUNCTION IF EXISTS pg_catalog.pg_column_is_updatable(oid, int2, bool); +SET LOCAL inplace_upgrade_next_system_object_oids=IUO_PROC, 3847; +CREATE FUNCTION pg_catalog.pg_column_is_updatable(oid, int2, bool) +RETURNS bool LANGUAGE INTERNAL AS 'pg_column_is_updatable'; + +comment on function PG_CATALOG.pg_column_is_updatable(oid, int2, bool) is 'is a column updatable'; + +SET search_path TO information_schema; +CREATE OR REPLACE VIEW columns AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + CAST(a.attname AS sql_identifier) AS column_name, + CAST(a.attnum AS cardinal_number) AS ordinal_position, + CAST(CASE WHEN ad.adgencol <> 's' THEN pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS column_default, + CAST(CASE WHEN a.attnotnull OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END + AS yes_or_no) + AS is_nullable, + + CAST( + CASE WHEN t.typtype = 'd' THEN + CASE WHEN bt.typelem <> 0 AND bt.typlen = -1 THEN 'ARRAY' + WHEN nbt.nspname = 'pg_catalog' THEN pg_catalog.format_type(t.typbasetype, null) + ELSE 'USER-DEFINED' END + ELSE + CASE WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY' + WHEN nt.nspname = 'pg_catalog' THEN pg_catalog.format_type(a.atttypid, null) + ELSE 'USER-DEFINED' END + END + AS character_data) + AS data_type, + + CAST( + _pg_char_max_length(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS character_maximum_length, + + CAST( + _pg_char_octet_length(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS character_octet_length, + + CAST( + _pg_numeric_precision(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_precision, + + CAST( + _pg_numeric_precision_radix(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_precision_radix, + + CAST( + _pg_numeric_scale(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS numeric_scale, + + CAST( + _pg_datetime_precision(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS cardinal_number) + AS datetime_precision, + + CAST( + _pg_interval_type(_pg_truetypid(a, t), _pg_truetypmod(a, t)) + AS character_data) + AS interval_type, + CAST(null AS cardinal_number) AS interval_precision, + + CAST(null AS sql_identifier) AS character_set_catalog, + CAST(null AS sql_identifier) AS character_set_schema, + CAST(null AS sql_identifier) AS character_set_name, + + CAST(CASE WHEN nco.nspname IS NOT NULL THEN pg_catalog.current_database() END AS sql_identifier) AS collation_catalog, + CAST(nco.nspname AS sql_identifier) AS collation_schema, + CAST(co.collname AS sql_identifier) AS collation_name, + + CAST(CASE WHEN t.typtype = 'd' THEN pg_catalog.current_database() ELSE null END + AS sql_identifier) AS domain_catalog, + CAST(CASE WHEN t.typtype = 'd' THEN nt.nspname ELSE null END + AS sql_identifier) AS domain_schema, + CAST(CASE WHEN t.typtype = 'd' THEN t.typname ELSE null END + AS sql_identifier) AS domain_name, + + CAST(pg_catalog.current_database() AS sql_identifier) AS udt_catalog, + CAST(coalesce(nbt.nspname, nt.nspname) AS sql_identifier) AS udt_schema, + CAST(coalesce(bt.typname, t.typname) AS sql_identifier) AS udt_name, + + CAST(null AS sql_identifier) AS scope_catalog, + CAST(null AS sql_identifier) AS scope_schema, + CAST(null AS sql_identifier) AS scope_name, + + CAST(null AS cardinal_number) AS maximum_cardinality, + CAST(a.attnum AS sql_identifier) AS dtd_identifier, + CAST('NO' AS yes_or_no) AS is_self_referencing, + + CAST('NO' AS yes_or_no) AS is_identity, + CAST(null AS character_data) AS identity_generation, + CAST(null AS character_data) AS identity_start, + CAST(null AS character_data) AS identity_increment, + CAST(null AS character_data) AS identity_maximum, + CAST(null AS character_data) AS identity_minimum, + CAST(null AS yes_or_no) AS identity_cycle, + + CAST(CASE WHEN ad.adgencol = 's' THEN 'ALWAYS' ELSE 'NEVER' END AS character_data) AS is_generated, + CAST(CASE WHEN ad.adgencol = 's' THEN pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS generation_expression, + + CAST(CASE WHEN c.relkind = 'r' + OR (c.relkind in ('v', 'f') AND pg_column_is_updatable(c.oid, a.attnum, false)) + THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_updatable + + FROM (pg_attribute a LEFT JOIN pg_attrdef ad ON attrelid = adrelid AND attnum = adnum) + JOIN (pg_class c JOIN pg_namespace nc ON (c.relnamespace = nc.oid)) ON a.attrelid = c.oid + JOIN (pg_type t JOIN pg_namespace nt ON (t.typnamespace = nt.oid)) ON a.atttypid = t.oid + LEFT JOIN (pg_type bt JOIN pg_namespace nbt ON (bt.typnamespace = nbt.oid)) + ON (t.typtype = 'd' AND t.typbasetype = bt.oid) + LEFT JOIN (pg_collation co JOIN pg_namespace nco ON (co.collnamespace = nco.oid)) + ON a.attcollation = co.oid AND (nco.nspname, co.collname) <> ('pg_catalog', 'default') + + WHERE (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + + AND a.attnum > 0 AND NOT a.attisdropped AND c.relkind in ('r', 'm', 'v', 'f') + + AND (c.relname not like 'mlog\_%' AND c.relname not like 'matviewmap\_%') + + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_column_privilege(c.oid, a.attnum, + 'SELECT, INSERT, UPDATE, REFERENCES')); + +CREATE OR REPLACE VIEW tables AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + + CAST( + CASE WHEN nc.oid = pg_catalog.pg_my_temp_schema() THEN 'LOCAL TEMPORARY' + WHEN c.relkind = 'r' THEN 'BASE TABLE' + WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW' + WHEN c.relkind = 'v' THEN 'VIEW' + WHEN c.relkind = 'f' THEN 'FOREIGN TABLE' + ELSE null END + AS character_data) AS table_type, + + CAST(null AS sql_identifier) AS self_referencing_column_name, + CAST(null AS character_data) AS reference_generation, + + CAST(CASE WHEN t.typname IS NOT NULL THEN pg_catalog.current_database() ELSE null END AS sql_identifier) AS user_defined_type_catalog, + CAST(nt.nspname AS sql_identifier) AS user_defined_type_schema, + CAST(t.typname AS sql_identifier) AS user_defined_type_name, + + CAST(CASE WHEN c.relkind = 'r' OR + (c.relkind in ('v', 'f') AND + -- 1 << CMD_INSERT + pg_relation_is_updatable(c.oid, false) & 8 = 8) + THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, + + CAST(CASE WHEN t.typname IS NOT NULL THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_typed, + CAST(null AS character_data) AS commit_action + + FROM pg_namespace nc JOIN pg_class c ON (nc.oid = c.relnamespace) + LEFT JOIN (pg_type t JOIN pg_namespace nt ON (t.typnamespace = nt.oid)) ON (c.reloftype = t.oid) + + WHERE c.relkind IN ('r', 'm', 'v', 'f') + AND (c.relname not like 'mlog\_%' AND c.relname not like 'matviewmap\_%') + AND (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') + OR pg_catalog.has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') ); + +CREATE OR REPLACE VIEW views AS + SELECT CAST(pg_catalog.current_database() AS sql_identifier) AS table_catalog, + CAST(nc.nspname AS sql_identifier) AS table_schema, + CAST(c.relname AS sql_identifier) AS table_name, + + CAST( + CASE WHEN pg_catalog.pg_has_role(c.relowner, 'USAGE') + THEN pg_catalog.pg_get_viewdef(c.oid) + ELSE null END + AS character_data) AS view_definition, + + CAST( + CASE WHEN 'check_option=cascaded' = ANY (c.reloptions) + THEN 'CASCADED' + WHEN 'check_option=local' = ANY (c.reloptions) + THEN 'LOCAL' + ELSE 'NONE' END + AS character_data) AS check_option, + + CAST( + -- (1 << CMD_UPDATE) + (1 << CMD_DELETE) + CASE WHEN pg_relation_is_updatable(c.oid, false) & 20 = 20 + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_updatable, + + CAST( + -- 1 << CMD_INSERT + CASE WHEN pg_relation_is_updatable(c.oid, false) & 8 = 8 + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_insertable_into, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_UPDATE + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 81 = 81) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_updatable, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_DELETE + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 73 = 73) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_deletable, + + CAST( + -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_INSERT + CASE WHEN EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = c.oid AND tgtype & 69 = 69) + THEN 'YES' ELSE 'NO' END + AS yes_or_no) AS is_trigger_insertable_into + + FROM pg_namespace nc, pg_class c + + WHERE c.relnamespace = nc.oid + AND c.relkind = 'v' + AND (NOT pg_catalog.pg_is_other_temp_schema(nc.oid)) + AND (pg_catalog.pg_has_role(c.relowner, 'USAGE') + OR pg_catalog.has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') + OR pg_catalog.has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') ); + +reset search_path; \ No newline at end of file diff --git a/src/include/commands/view.h b/src/include/commands/view.h index 506a8508d..55fd70730 100644 --- a/src/include/commands/view.h +++ b/src/include/commands/view.h @@ -16,6 +16,7 @@ #include "nodes/parsenodes.h" +extern void validateWithCheckOption(const char* value); extern Oid DefineView(ViewStmt* stmt, const char* queryString, bool send_remote = true, bool isFirstNode = true); extern bool IsViewTemp(ViewStmt* stmt, const char* queryString); extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace); diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index c4590210b..1d39bfc87 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -227,6 +227,7 @@ extern void InitResultRelInfo( extern ResultRelInfo* ExecGetTriggerResultRel(EState* estate, Oid relid); extern bool ExecContextForcesOids(PlanState* planstate, bool* hasoids); extern bool ExecConstraints(ResultRelInfo* resultRelInfo, TupleTableSlot* slot, EState* estate, bool skipAutoInc = false); +extern void ExecWithCheckOptions(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, EState *estate); extern ExecRowMark* ExecFindRowMark(EState* estate, Index rti); extern ExecAuxRowMark* ExecBuildAuxRowMark(ExecRowMark* erm, List* targetlist); extern TupleTableSlot* EvalPlanQual(EState* estate, EPQState* epqstate, Relation relation, Index rti, diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index c4adf5d14..e11b65357 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -116,6 +116,7 @@ extern const uint32 ON_UPDATE_TIMESTAMP_VERSION_NUM; extern const uint32 STANDBY_STMTHIST_VERSION_NUM; extern const uint32 PG_AUTHID_PASSWORDEXT_VERSION_NUM; extern const uint32 MAT_VIEW_RECURSIVE_VERSION_NUM; +extern const uint32 SUPPORT_VIEW_AUTO_UPDATABLE; extern void register_backend_version(uint32 backend_version); extern bool contain_backend_version(uint32 version_number); diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 5e56865e6..764642f4f 100755 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -471,6 +471,8 @@ struct ExprState { * junkFilter for removing junk attributes from tuples * projectReturning for computing a RETURNING list * updateProj for computing a UPSERT update list + * WithCheckOptions list of WithCheckOption's for views + * WithCheckOptionExprs list of WithCheckOption expr states * ---------------- */ typedef struct ResultRelInfo { @@ -522,6 +524,8 @@ typedef struct ResultRelInfo { int ri_NumGeneratedNeeded; /* number of stored UpdateExpr columns we need to compute */ int ri_NumUpdatedNeeded; + List* ri_WithCheckOptions; + List* ri_WithCheckOptionExprs; } ResultRelInfo; /* bloom filter controller */ diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index fb5f078cd..3f12aa7d9 100755 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -546,6 +546,7 @@ typedef enum NodeTag { T_Constraint, T_DefElem, T_RangeTblEntry, + T_WithCheckOption, T_TableSampleClause, T_TimeCapsuleClause, T_SortGroupClause, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 2b7f972be..e95c24b20 100755 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -376,6 +376,20 @@ typedef struct RangeTblEntry { */ } RangeTblEntry; +/* + * WithCheckOption - + * representation of WITH CHECK OPTION checks to be applied to new tuples + * when inserting/updating an auto-updatable view. + */ +typedef struct WithCheckOption { + NodeTag type; + char* viewname; /* name of view that specified the WCO */ + Node* qual; /* constraint qual to check */ + bool cascaded; /* true = WITH CASCADED CHECK OPTION */ + Index rtindex; /* used when multiple modifying. It indicates the resultRelation + * to which this wco belongs */ +} WithCheckOption; + /* * SortGroupClause - * representation of ORDER BY, GROUP BY, PARTITION BY, diff --git a/src/include/nodes/parsenodes_common.h b/src/include/nodes/parsenodes_common.h index fd1fa8f18..9d969a8d5 100644 --- a/src/include/nodes/parsenodes_common.h +++ b/src/include/nodes/parsenodes_common.h @@ -2034,6 +2034,7 @@ typedef struct Query { List* resultRelations; /* rtable index list of target relation for INSERT/UPDATE/DELETE/MERGE. */ RightRefState* rightRefState; + List* withCheckOptions; /* a list of WithCheckOption's */ } Query; /* ---------------------- @@ -2065,6 +2066,12 @@ typedef struct TransactionStmt { * Create View Statement * ---------------------- */ +typedef enum ViewCheckOption { + NO_CHECK_OPTION, + LOCAL_CHECK_OPTION, + CASCADED_CHECK_OPTION +} ViewCheckOption; + typedef struct ViewStmt { NodeTag type; RangeVar *view; /* the view to be created */ @@ -2082,6 +2089,7 @@ typedef struct ViewStmt { #ifdef ENABLE_MULTIPLE_NODES struct PGXCSubCluster* subcluster; /* subcluster of table */ #endif + ViewCheckOption withCheckOption; /* WITH CHECK OPTION */ } ViewStmt; /* ---------------------- diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 5ad1e08fb..8db2b9313 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -430,6 +430,7 @@ typedef struct ModifyTable { OpMemInfo mem_info; /* Memory info for modify node */ List* targetlists; /* For multi-relation modifying */ + List* withCheckOptionLists; /* per-target-table WCO lists */ } ModifyTable; /* ---------------- diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 34134c993..d5b775347 100755 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -118,8 +118,9 @@ extern bool check_dsitribute_key_in_targetlist(PlannerInfo* root, List* distribu extern int get_plan_actual_total_width(Plan* plan, bool vectorized, OpType type, int newcol = 0); #ifdef STREAMPLAN extern Plan* make_modifytable(PlannerInfo* root, CmdType operation, bool canSetTag, List* resultRelations, - List* subplans, List* returningLists, List* rowMarks, int epqParam, bool partKeyUpdated, Index mergeTargetRelation, - List* mergeSourceTargetList, List* mergeActionList, UpsertExpr* upsertClause); + List* subplans, List *withCheckOptionLists, List* returningLists, List* rowMarks, int epqParam, + bool partKeyUpdated, Index mergeTargetRelation, List* mergeSourceTargetList, List* mergeActionList, + UpsertExpr* upsertClause); extern Plan* make_modifytables(PlannerInfo* root, CmdType operation, bool canSetTag, List* resultRelations, List* subplans, List* returningLists, List* rowMarks, int epqParam, bool partKeyUpdated, Index mergeTargetRelation, List* mergeSourceTargetList, List *mergeActionList, UpsertExpr *upsertClause); @@ -129,8 +130,9 @@ extern Plan* make_stream_plan(PlannerInfo* root, Plan* lefttree, List* redistrib Distribution* target_distribution = NULL); #else extern ModifyTable* make_modifytable(CmdType operation, bool canSetTag, List* resultRelations, - List* subplans, List* returningLists, List* rowMarks, int epqParam, bool partKeyUpdated, Index mergeTargetRelation, - List* mergeSourceTargetList, List* mergeActionList, UpsertExpr* upsertClause); + List* subplans, List *withCheckOptionLists, List* returningLists, List* rowMarks, int epqParam, + bool partKeyUpdated, Index mergeTargetRelation, List* mergeSourceTargetList, List* mergeActionList, + UpsertExpr* upsertClause); extern ModifyTable* make_modifytables(CmdType operation, bool canSetTag, List* resultRelations, List* subplans, List* returningLists, List* rowMarks, int epqParam, bool partKeyUpdated, Index mergeTargetRelation, List* mergeSourceTargetList, List* mergeActionList, UpsertExpr* upsertClause); diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h index 90f1781a3..d584543e4 100644 --- a/src/include/rewrite/rewriteHandler.h +++ b/src/include/rewrite/rewriteHandler.h @@ -24,6 +24,10 @@ extern List* pull_qual_vars(Node* node, int varno = 0, int flags = 0, bool nonRe extern void rewriteTargetListMerge(Query* parsetree, Index result_relation, List* range_table); extern List *query_rewrite_multiset_stmt(Query* parse_tree); extern List *query_rewrite_set_stmt(Query* parse_tree); +extern Query* get_view_query(Relation view); +extern const char* view_query_is_auto_updatable(Query* viewquery, bool check_cols); +extern int relation_is_updatable(Oid reloid, bool include_triggers, Bitmapset* include_cols); +extern bool view_has_instead_trigger(Relation view, CmdType event); #ifdef PGXC extern List* QueryRewriteCTAS(Query* parsetree); diff --git a/src/include/rewrite/rewriteManip.h b/src/include/rewrite/rewriteManip.h index af01f0e01..c687135b6 100644 --- a/src/include/rewrite/rewriteManip.h +++ b/src/include/rewrite/rewriteManip.h @@ -28,6 +28,12 @@ struct replace_rte_variables_context { bool inserted_sublink; /* have we inserted a SubLink? */ }; +typedef enum ReplaceVarsNoMatchOption { + REPLACEVARS_REPORT_ERROR, /* throw error if no match */ + REPLACEVARS_CHANGE_VARNO, /* change the Var's varno, nothing else */ + REPLACEVARS_SUBSTITUTE_NULL /* replace with a NULL Const */ +} ReplaceVarsNoMatchOption; + extern void OffsetVarNodes(Node* node, int offset, int sublevels_up); extern void ChangeVarNodes(Node* node, int old_varno, int new_varno, int sublevels_up); extern void IncrementVarSublevelsUp(Node* node, int delta_sublevels_up, int min_sublevels_up); @@ -56,7 +62,7 @@ extern Node* replace_rte_variables_mutator(Node* node, replace_rte_variables_con extern Node* map_variable_attnos( Node* node, int target_varno, int sublevels_up, const AttrNumber* attno_map, int map_length, bool* found_whole_row); -extern Node* ResolveNew(Node* node, int target_varno, int sublevels_up, RangeTblEntry* target_rte, List* targetlist, - int event, int update_varno, bool* outer_hasSubLinks); +extern Node* ReplaceVarsFromTargetList(Node* node, int target_varno, int sublevels_up, RangeTblEntry* target_rte, + List* targetlist, ReplaceVarsNoMatchOption nomatch_option, int nomatch_varno, bool* outer_hasSubLinks); #endif /* REWRITEMANIP_H */ diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h index abed78d50..851582df9 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -720,6 +720,8 @@ extern Datum pg_collation_for(PG_FUNCTION_ARGS); extern Datum pg_sync_cstore_delta(PG_FUNCTION_ARGS); extern Datum pg_sync_all_cstore_delta(PG_FUNCTION_ARGS); extern Datum pg_test_err_contain_err(PG_FUNCTION_ARGS); +extern Datum pg_relation_is_updatable(PG_FUNCTION_ARGS); +extern Datum pg_column_is_updatable(PG_FUNCTION_ARGS); /* oid.c */ extern Datum oidin(PG_FUNCTION_ARGS); diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 1b3bd79e5..4ff327c72 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -405,6 +405,7 @@ typedef struct StdRdOptions { bool enable_tde; /* switch flag for table-level TDE encryption */ bool on_commit_delete_rows; /* global temp table */ PageCompressOpts compress; /* page compress related reloptions. */ + int check_option_offset; /* for views */ } StdRdOptions; #define HEAP_MIN_FILLFACTOR 10 @@ -463,6 +464,39 @@ typedef struct StdRdOptions { #define RelationIsSecurityView(relation) \ ((relation)->rd_options ? ((StdRdOptions*)(relation)->rd_options)->security_barrier : false) +/* + * RelationHasCheckOption + * Returns true if the relation is a view defined with either the local + * or the cascaded check option. + */ +#define RelationHasCheckOption(relation) \ + ((relation)->rd_options && \ + ((StdRdOptions *) (relation)->rd_options)->check_option_offset != 0) + +/* + * RelationHasLocalCheckOption + * Returns true if the relation is a view defined with the local check + * option. + */ +#define RelationHasLocalCheckOption(relation) \ + ((relation)->rd_options && \ + ((StdRdOptions *) (relation)->rd_options)->check_option_offset != 0 ? \ + strcmp((char *) (relation)->rd_options + \ + ((StdRdOptions *) (relation)->rd_options)->check_option_offset, \ + "local") == 0 : false) + +/* + * RelationHasCascadedCheckOption + * Returns true if the relation is a view defined with the cascaded check + * option. + */ +#define RelationHasCascadedCheckOption(relation) \ + ((relation)->rd_options && \ + ((StdRdOptions *) (relation)->rd_options)->check_option_offset != 0 ? \ + strcmp((char *) (relation)->rd_options + \ + ((StdRdOptions *) (relation)->rd_options)->check_option_offset, \ + "cascaded") == 0 : false) + /* * RelationGetParallelWorkers * Returns the relation's parallel_workers reloption setting. diff --git a/src/test/regress/expected/generated_col.out b/src/test/regress/expected/generated_col.out index 2250d4800..1cbf8485d 100644 --- a/src/test/regress/expected/generated_col.out +++ b/src/test/regress/expected/generated_col.out @@ -154,8 +154,8 @@ SELECT * FROM gtest1v; (1 row) INSERT INTO gtest1v VALUES (4, 8); -- fails -ERROR: cannot insert into view "gtest1v" -HINT: You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger. +ERROR: cannot insert into column "b" +DETAIL: Column "b" is a generated column. DROP VIEW gtest1v; -- CTEs WITH foo AS (SELECT * FROM gtest1) SELECT * FROM foo; diff --git a/src/test/regress/expected/multi_delete.out b/src/test/regress/expected/multi_delete.out index a9926ee7d..05558d967 100644 --- a/src/test/regress/expected/multi_delete.out +++ b/src/test/regress/expected/multi_delete.out @@ -237,14 +237,6 @@ ERROR: Unsupported feature DETAIL: Materialized view doesn't allow DELETE drop MATERIALIZED VIEW mate_multiview1; drop MATERIALIZED VIEW mate_multiview2; --- view -create view multiview1 as select * from t_t_mutil_t1; -create view multiview2 as select * from t_t_mutil_t2; -delete t_t_mutil_t1 a,multiview1 b,multiview2 c where a.col1 = 4 and b.col2 = 5 and c.col2 =6; --error -ERROR: Un-support feature -DETAIL: view in mutilple-relations modifying doesn't support DELETE -drop view multiview1; -drop view multiview2; -- different explain plan explain(verbose) delete/*+nestloop(a b)*/ from t_t_mutil_t1 a,t_t_mutil_t2 b where a.col2=b.col2; QUERY PLAN @@ -1001,5 +993,53 @@ ERROR: table name "t_t_mutil_t1" specified more than once delete from t_t_mutil_t1 a,t_t_mutil_t1 a where a.col1=1; ERROR: table name "a" specified more than once delete from t_t_mutil_t1 a,t_t_mutil_t1 b where a.col1=1; +-- view +drop table if exists t_t_mutil_t1; +drop table if exists t_t_mutil_t2; +drop table if exists t_t_mutil_t3; +create table t_t_mutil_t1(col1 int,col2 int); +create table t_t_mutil_t2(col1 int,col2 int,col3 int); +create table t_t_mutil_t3(col1 int,col2 int); +insert into t_t_mutil_t1 values(1,1),(1,1); +insert into t_t_mutil_t2 values(1,1),(1,2); +insert into t_t_mutil_t3 values(1,1),(1,3); +create view multiview1 as select * from t_t_mutil_t1; +create view multiview2 as select * from t_t_mutil_t2; +create view multiview3 as select * from t_t_mutil_t3; +delete multiview1 a,multiview2 b,multiview3 c where a.col1 = b.col1 and b.col2 = c.col2; +select * from t_t_mutil_t1; + col1 | col2 +------+------ +(0 rows) + +select * from t_t_mutil_t2; + col1 | col2 | col3 +------+------+------ + 1 | 2 | +(1 row) + +select * from t_t_mutil_t3; + col1 | col2 +------+------ + 1 | 3 +(1 row) + +-- left join +insert into t_t_mutil_t1 values(1,1),(1,1); +insert into t_t_mutil_t2 values(1,1),(1,2); +delete from multiview1,t_t_mutil_t2 using t_t_mutil_t2 left join multiview1 on multiview1.col2=t_t_mutil_t2.col1; +select * from t_t_mutil_t1; + col1 | col2 +------+------ +(0 rows) + +select * from t_t_mutil_t2; + col1 | col2 | col3 +------+------+------ +(0 rows) + +drop view multiview1; +drop view multiview2; +drop view multiview3; \c regression drop database multidelete; diff --git a/src/test/regress/expected/multi_update.out b/src/test/regress/expected/multi_update.out index 6d99bfac5..8adfa7f10 100644 --- a/src/test/regress/expected/multi_update.out +++ b/src/test/regress/expected/multi_update.out @@ -262,14 +262,6 @@ ERROR: Unsupported feature DETAIL: Materialized view doesn't allow UPDATE drop MATERIALIZED VIEW mate_multiview1; drop MATERIALIZED VIEW mate_multiview2; --- view -create view multiview1 as select * from t_t_mutil_t1; -create view multiview2 as select * from t_t_mutil_t2; -update t_t_mutil_t1 a,multiview1 b,multiview2 c set a.col1 = 4, b.col2 = 5,c.col2 =6; --error -ERROR: Un-support feature -DETAIL: view in mutilple-relations modifying doesn't support UPDATE -drop view multiview1; -drop view multiview2; -- same relname begin; update t_t_mutil_t1 a,t_t_mutil_t1 b set a.col2=5,b.col2=4 where a.col1=b.col1; @@ -1276,5 +1268,156 @@ ERROR: multiple assignments to same column "col2" select * from t_t_mutil_t1; ERROR: current transaction is aborted, commands ignored until end of transaction block, firstChar[Q] rollback; +-- view +create view multiview1 as select * from t_t_mutil_t1; +create view multiview2 as select * from t_t_mutil_t2; +create view multiview3 as select * from t_t_mutil_t3; +update multiview1 a,multiview2 b,multiview3 c set a.col2 = 6, b.col2 = 7,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; +select * from t_t_mutil_t1; + col1 | col2 +------+------ + 1 | 6 + 1 | 6 +(2 rows) + +select * from t_t_mutil_t2; + col1 | col2 | col3 +------+------+------ + 1 | 7 | + 1 | 7 | +(2 rows) + +select * from t_t_mutil_t3; + col1 | col2 +------+------ + 1 | 8 + 1 | 8 +(2 rows) + +update t_t_mutil_t1 a,multiview2 b,t_t_mutil_t3 c set a.col2 = 4, b.col2 = 5,c.col2 = 6 where a.col1 = b.col1 and a.col1 = c.col1; +select * from t_t_mutil_t1; + col1 | col2 +------+------ + 1 | 4 + 1 | 4 +(2 rows) + +select * from t_t_mutil_t2; + col1 | col2 | col3 +------+------+------ + 1 | 5 | + 1 | 5 | +(2 rows) + +select * from t_t_mutil_t3; + col1 | col2 +------+------ + 1 | 6 + 1 | 6 +(2 rows) + +-- left join +update multiview1 a left join multiview2 b on a.col1=b.col1 set a.col2=7,b.col2=8; +select * from t_t_mutil_t1; + col1 | col2 +------+------ + 1 | 7 + 1 | 7 +(2 rows) + +select * from t_t_mutil_t2; + col1 | col2 | col3 +------+------+------ + 1 | 8 | + 1 | 8 | +(2 rows) + +select * from t_t_mutil_t3; + col1 | col2 +------+------ + 1 | 6 + 1 | 6 +(2 rows) + +-- with check option +create or replace view multiview1 as select * from t_t_mutil_t1 where col2 > 3 with local check option; +create or replace view multiview2 as select * from t_t_mutil_t2 where col2 < 10 with local check option; +update multiview1 a,multiview2 b,multiview3 c set a.col2 = 2, b.col2 = 7,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; --error +ERROR: new row violates WITH CHECK OPTION for view "multiview1" +DETAIL: Failing row contains (1, 2). +update multiview1 a,multiview2 b,multiview3 c set a.col2 = 5, b.col2 = 15,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; --error +ERROR: new row violates WITH CHECK OPTION for view "multiview2" +DETAIL: Failing row contains (1, 15, null). +update multiview1 a,multiview2 b,multiview3 c set a.col2 = 5, b.col2 = 7,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; +select * from t_t_mutil_t1; + col1 | col2 +------+------ + 1 | 5 + 1 | 5 +(2 rows) + +select * from t_t_mutil_t2; + col1 | col2 | col3 +------+------+------ + 1 | 7 | + 1 | 7 | +(2 rows) + +select * from t_t_mutil_t3; + col1 | col2 +------+------ + 1 | 8 + 1 | 8 +(2 rows) + +-- update same relation +update t_t_mutil_t1 a,multiview2 b,t_t_mutil_t2 c,t_t_mutil_t3 d set a.col2 = 6, b.col2 = 9, c.col3 = 10, d.col2 = 6 where a.col1 = b.col1 and a.col1 = d.col1; +select * from t_t_mutil_t1; + col1 | col2 +------+------ + 1 | 6 + 1 | 6 +(2 rows) + +select * from t_t_mutil_t2; + col1 | col2 | col3 +------+------+------ + 1 | 9 | 10 + 1 | 9 | 10 +(2 rows) + +select * from t_t_mutil_t3; + col1 | col2 +------+------ + 1 | 6 + 1 | 6 +(2 rows) + +-- view with rules or triggers should fail +create rule multiview1_rule as on insert to multiview1 do instead insert into t_t_mutil_t1 values (new.col1, new.col2); +update multiview1 a,multiview2 b,t_t_mutil_t3 c set a.col2 = 6, b.col2 = 7,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; --error +ERROR: Un-support feature +DETAIL: view has rules or tiggers in mutilple-relations modifying doesn't support UPDATE +drop rule multiview1_rule on multiview1; +CREATE OR REPLACE FUNCTION trigger_func_update_multiview1() RETURNS TRIGGER AS +$$ +DECLARE +BEGIN +UPDATE t_t_mutil_t1 SET col2 = NEW.col2 WHERE col1=OLD.col1; +RETURN OLD; +END +$$ LANGUAGE PLPGSQL; +CREATE TRIGGER update_multiview1_trigger +INSTEAD OF UPDATE ON multiview1 +FOR EACH ROW +EXECUTE PROCEDURE trigger_func_update_multiview1(); +update multiview1 a,multiview2 b,t_t_mutil_t3 c set a.col2 = 6, b.col2 = 7,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; --error +ERROR: Un-support feature +DETAIL: view has rules or tiggers in mutilple-relations modifying doesn't support UPDATE +drop trigger update_multiview1_trigger on multiview1; +drop function trigger_func_update_multiview1(); +drop view multiview1; +drop view multiview2; +drop view multiview3; \c regression drop database multiupdate; diff --git a/src/test/regress/expected/rule_test.out b/src/test/regress/expected/rule_test.out index 3b28ceb53..60fdb1369 100644 --- a/src/test/regress/expected/rule_test.out +++ b/src/test/regress/expected/rule_test.out @@ -315,15 +315,14 @@ create rule "_RETURN" as on select to ttt1 do instead ( select * from ttt2; ); -- test -insert into ttt1 values (1, 'hello'); --error -ERROR: cannot insert into view "ttt1" -HINT: You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger. +insert into ttt1 values (1, 'hello'); insert into ttt2 values (10, 'world'); select * from ttt1; a | b ----+------- + 1 | hello 10 | world -(1 row) +(2 rows) drop table if exists ttt1; --error ERROR: "ttt1" is not a table diff --git a/src/test/regress/expected/single_node_triggers.out b/src/test/regress/expected/single_node_triggers.out index 875b7c416..07f9cdaa4 100644 --- a/src/test/regress/expected/single_node_triggers.out +++ b/src/test/regress/expected/single_node_triggers.out @@ -866,20 +866,16 @@ ERROR: table "min_updates_test_oids" does not exist -- Test triggers on views -- CREATE VIEW main_view AS SELECT a, b FROM main_table; --- Updates should fail without rules or triggers +-- Updates should succeed without rules or triggers, cause this simple view are auto-updatable. INSERT INTO main_view VALUES (1,2); -ERROR: cannot insert into view "main_view" -HINT: You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger. +NOTICE: trigger_func(before_ins_stmt) called: action = INSERT, when = BEFORE, level = STATEMENT +NOTICE: trigger_func(after_ins_stmt) called: action = INSERT, when = AFTER, level = STATEMENT UPDATE main_view SET b = 20 WHERE a = 50; -ERROR: cannot update view "main_view" -HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger. +ERROR: duplicate key value violates unique constraint "main_table_pkey" +DETAIL: Key (a, b)=(50, 20) already exists. DELETE FROM main_view WHERE a = 50; -ERROR: cannot delete from view "main_view" -HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger. --- Should fail even when there are no matching rows +-- Should succeed even when there are no matching rows, cause this simple view are auto-updatable. DELETE FROM main_view WHERE a = 51; -ERROR: cannot delete from view "main_view" -HINT: You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger. -- VIEW trigger function CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger LANGUAGE plpgsql AS $$ diff --git a/src/test/regress/expected/sqlLLT.out b/src/test/regress/expected/sqlLLT.out index 49c9f3c0c..33a6168ed 100644 --- a/src/test/regress/expected/sqlLLT.out +++ b/src/test/regress/expected/sqlLLT.out @@ -3258,8 +3258,6 @@ VACUUM LLT_PART_TEST1 PARTITION (P1); CLUSTER LLT_PART_TEST1 USING IDX_ON_T1; CREATE VIEW VIEW_ON_LLT_PART_TEST1 AS SELECT * FROM LLT_PART_TEST1; INSERT INTO VIEW_ON_LLT_PART_TEST1 SELECT * FROM LLT_PART_TEST1; -ERROR: cannot insert into view "view_on_llt_part_test1" -HINT: You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger. CREATE OR REPLACE FUNCTION RESULT_COUNT() RETURNS INTEGER AS $$ diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out index cd41624df..ae07c665a 100644 --- a/src/test/regress/expected/updatable_views.out +++ b/src/test/regress/expected/updatable_views.out @@ -39,8 +39,8 @@ SELECT * FROM rw_view1 FOR UPDATE; -- not allowed ERROR: permission denied for relation base_tbl DETAIL: N/A UPDATE rw_view1 SET b = 'foo' WHERE a = 1; -- unlike pgsql, we do not support updating views -ERROR: cannot update view "rw_view1" -HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger. +ERROR: permission denied for relation base_tbl +DETAIL: N/A SET SESSION AUTHORIZATION regress_view_user2 PASSWORD 'Gauss@123'; SELECT * FROM rw_view2; -- not allowed ERROR: permission denied for relation rw_view1 @@ -64,8 +64,8 @@ SELECT * FROM rw_view2 FOR UPDATE; -- not allowed ERROR: permission denied for relation rw_view1 DETAIL: N/A UPDATE rw_view2 SET b = 'bar' WHERE a = 1; -- unlike pgsql, we do not support updating views -ERROR: cannot update view "rw_view2" -HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger. +ERROR: permission denied for relation rw_view1 +DETAIL: N/A RESET SESSION AUTHORIZATION; GRANT UPDATE ON base_tbl TO regress_view_user1; SET SESSION AUTHORIZATION regress_view_user1 PASSWORD 'Gauss@123'; @@ -82,67 +82,63 @@ SELECT * FROM rw_view1 FOR UPDATE; (1 row) UPDATE rw_view1 SET b = 'foo' WHERE a = 1; -- unlike pgsql, we do not support updating views -ERROR: cannot update view "rw_view1" -HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger. SET SESSION AUTHORIZATION regress_view_user2 PASSWORD 'Gauss@123'; SELECT * FROM rw_view2; - a | b | c ----+-------+--- - 1 | Row 1 | 1 + a | b | c +---+-----+--- + 1 | foo | 1 (1 row) SELECT * FROM rw_view2 FOR UPDATE; -- not allowed ERROR: permission denied for relation rw_view1 DETAIL: N/A UPDATE rw_view2 SET b = 'bar' WHERE a = 1; -- unlike pgsql, we do not support updating views -ERROR: cannot update view "rw_view2" -HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger. +ERROR: permission denied for relation rw_view1 +DETAIL: N/A SET SESSION AUTHORIZATION regress_view_user1 PASSWORD 'Gauss@123'; GRANT UPDATE ON rw_view1 TO regress_view_user2; SET SESSION AUTHORIZATION regress_view_user2 PASSWORD 'Gauss@123'; SELECT * FROM rw_view2; - a | b | c ----+-------+--- - 1 | Row 1 | 1 + a | b | c +---+-----+--- + 1 | foo | 1 (1 row) SELECT * FROM rw_view2 FOR UPDATE; - a | b | c ----+-------+--- - 1 | Row 1 | 1 + a | b | c +---+-----+--- + 1 | foo | 1 (1 row) UPDATE rw_view2 SET b = 'bar' WHERE a = 1; -- unlike pgsql, we do not support updating views -ERROR: cannot update view "rw_view2" -HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger. RESET SESSION AUTHORIZATION; REVOKE UPDATE ON base_tbl FROM regress_view_user1; SET SESSION AUTHORIZATION regress_view_user1 PASSWORD 'Gauss@123'; SELECT * FROM rw_view1; - a | b | c ----+-------+--- - 1 | Row 1 | 1 + a | b | c +---+-----+--- + 1 | bar | 1 (1 row) SELECT * FROM rw_view1 FOR UPDATE; -- not allowed ERROR: permission denied for relation base_tbl DETAIL: N/A UPDATE rw_view1 SET b = 'foo' WHERE a = 1; -- unlike pgsql, we do not support updating views -ERROR: cannot update view "rw_view1" -HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger. +ERROR: permission denied for relation base_tbl +DETAIL: N/A SET SESSION AUTHORIZATION regress_view_user2 PASSWORD 'Gauss@123'; SELECT * FROM rw_view2; - a | b | c ----+-------+--- - 1 | Row 1 | 1 + a | b | c +---+-----+--- + 1 | bar | 1 (1 row) SELECT * FROM rw_view2 FOR UPDATE; -- not allowed ERROR: permission denied for relation base_tbl DETAIL: N/A UPDATE rw_view2 SET b = 'bar' WHERE a = 1; -- unlike pgsql, we do not support updating views -ERROR: cannot update view "rw_view2" -HINT: You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger. +ERROR: permission denied for relation base_tbl +DETAIL: N/A RESET SESSION AUTHORIZATION; DROP TABLE base_tbl CASCADE; NOTICE: drop cascades to 2 other objects @@ -150,3 +146,2296 @@ DETAIL: drop cascades to view regress_view_user1.rw_view1 drop cascades to view regress_view_user2.rw_view2 DROP USER regress_view_user1; DROP USER regress_view_user2; +-- +-- UPDATABLE VIEWS +-- +-- check that non-updatable views and columns are rejected with useful error messages +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "base_tbl_pkey" for table "base_tbl" +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); +CREATE VIEW ro_view1 AS SELECT DISTINCT a, b FROM base_tbl; -- DISTINCT not supported +CREATE VIEW ro_view2 AS SELECT a, b FROM base_tbl GROUP BY a, b; -- GROUP BY not supported +CREATE VIEW ro_view3 AS SELECT 1 FROM base_tbl HAVING max(a) > 0; -- HAVING not supported +CREATE VIEW ro_view4 AS SELECT count(*) FROM base_tbl; -- Aggregate functions not supported +CREATE VIEW ro_view5 AS SELECT a, rank() OVER() FROM base_tbl; -- Window functions not supported +CREATE VIEW ro_view6 AS SELECT a, b FROM base_tbl UNION SELECT -a, b FROM base_tbl; -- Set ops not supported +CREATE VIEW ro_view7 AS WITH t AS (SELECT a, b FROM base_tbl) SELECT * FROM t; -- WITH not supported +CREATE VIEW ro_view8 AS SELECT a, b FROM base_tbl ORDER BY a OFFSET 1; -- OFFSET not supported +CREATE VIEW ro_view9 AS SELECT a, b FROM base_tbl ORDER BY a LIMIT 1; -- LIMIT not supported +CREATE VIEW ro_view10 AS SELECT 1 AS a; -- No base relations +CREATE VIEW ro_view11 AS SELECT b1.a, b2.b FROM base_tbl b1, base_tbl b2; -- Multiple base relations +CREATE VIEW ro_view12 AS SELECT * FROM generate_series(1, 10) AS g(a); -- SRF in rangetable +CREATE VIEW ro_view13 AS SELECT a, b FROM (SELECT * FROM base_tbl) AS t; -- Subselect in rangetable +CREATE VIEW rw_view14 AS SELECT ctid, a, b FROM base_tbl; -- System columns may be part of an updatable view +CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view +CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view +CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable +CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable +CREATE SEQUENCE seq; +CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence +CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name; + table_name | is_insertable_into +------------+-------------------- + ro_view1 | NO + ro_view10 | NO + ro_view11 | NO + ro_view12 | NO + ro_view13 | NO + ro_view17 | NO + ro_view18 | NO + ro_view19 | NO + ro_view2 | NO + ro_view20 | NO + ro_view3 | NO + ro_view4 | NO + ro_view5 | NO + ro_view6 | NO + ro_view7 | NO + ro_view8 | NO + ro_view9 | NO + rw_view14 | YES + rw_view15 | YES + rw_view16 | YES +(20 rows) + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into +------------+--------------+-------------------- + ro_view1 | NO | NO + ro_view10 | NO | NO + ro_view11 | NO | NO + ro_view12 | NO | NO + ro_view13 | NO | NO + ro_view17 | NO | NO + ro_view18 | NO | NO + ro_view19 | NO | NO + ro_view2 | NO | NO + ro_view20 | NO | NO + ro_view3 | NO | NO + ro_view4 | NO | NO + ro_view5 | NO | NO + ro_view6 | NO | NO + ro_view7 | NO | NO + ro_view8 | NO | NO + ro_view9 | NO | NO + rw_view14 | YES | YES + rw_view15 | YES | YES + rw_view16 | YES | YES +(20 rows) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable +------------+---------------+-------------- + ro_view1 | a | NO + ro_view1 | b | NO + ro_view10 | a | NO + ro_view11 | a | NO + ro_view11 | b | NO + ro_view12 | a | NO + ro_view13 | a | NO + ro_view13 | b | NO + ro_view17 | a | NO + ro_view17 | b | NO + ro_view18 | a | NO + ro_view19 | sequence_name | NO + ro_view19 | last_value | NO + ro_view19 | start_value | NO + ro_view19 | increment_by | NO + ro_view19 | max_value | NO + ro_view19 | min_value | NO + ro_view19 | cache_value | NO + ro_view19 | log_cnt | NO + ro_view19 | is_cycled | NO + ro_view19 | is_called | NO + ro_view19 | uuid | NO + ro_view2 | a | NO + ro_view2 | b | NO + ro_view20 | a | NO + ro_view20 | b | NO + ro_view20 | g | NO + ro_view3 | ?column? | NO + ro_view4 | count | NO + ro_view5 | a | NO + ro_view5 | rank | NO + ro_view6 | a | NO + ro_view6 | b | NO + ro_view7 | a | NO + ro_view7 | b | NO + ro_view8 | a | NO + ro_view8 | b | NO + ro_view9 | a | NO + ro_view9 | b | NO + rw_view14 | ctid | NO + rw_view14 | a | YES + rw_view14 | b | YES + rw_view15 | a | YES + rw_view15 | upper | NO + rw_view16 | a | YES + rw_view16 | b | YES + rw_view16 | aa | YES +(47 rows) + +-- Read-only views +DELETE FROM ro_view1; +ERROR: cannot delete from view "ro_view1" +DETAIL: Views containing DISTINCT are not automatically updatable. +HINT: To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule. +DELETE FROM ro_view2; +ERROR: cannot delete from view "ro_view2" +DETAIL: Views containing GROUP BY are not automatically updatable. +HINT: To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule. +DELETE FROM ro_view3; +ERROR: cannot delete from view "ro_view3" +DETAIL: Views containing HAVING are not automatically updatable. +HINT: To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule. +DELETE FROM ro_view4; +ERROR: cannot delete from view "ro_view4" +DETAIL: Views that return aggregate functions are not automatically updatable. +HINT: To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule. +DELETE FROM ro_view5; +ERROR: cannot delete from view "ro_view5" +DETAIL: Views that return window functions are not automatically updatable. +HINT: To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule. +DELETE FROM ro_view6; +ERROR: cannot delete from view "ro_view6" +DETAIL: Views containing UNION, INTERSECT or EXCEPT are not automatically updatable. +HINT: To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule. +UPDATE ro_view7 SET a=a+1; +ERROR: cannot update view "ro_view7" +DETAIL: Views containing WITH are not automatically updatable. +HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule. +UPDATE ro_view8 SET a=a+1; +ERROR: cannot update view "ro_view8" +DETAIL: Views containing LIMIT or OFFSET are not automatically updatable. +HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule. +UPDATE ro_view9 SET a=a+1; +ERROR: cannot update view "ro_view9" +DETAIL: Views containing LIMIT or OFFSET are not automatically updatable. +HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule. +UPDATE ro_view10 SET a=a+1; +ERROR: cannot update view "ro_view10" +DETAIL: Views that do not select from a single table or view are not automatically updatable. +HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule. +UPDATE ro_view11 SET a=a+1; +ERROR: cannot update view "ro_view11" +DETAIL: Views that do not select from a single table or view are not automatically updatable. +HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule. +UPDATE ro_view12 SET a=a+1; +ERROR: cannot update view "ro_view12" +DETAIL: Views that do not select from a single table or view are not automatically updatable. +HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule. +INSERT INTO ro_view13 VALUES (3, 'Row 3'); +ERROR: cannot insert into view "ro_view13" +DETAIL: Views that do not select from a single table or view are not automatically updatable. +HINT: To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule. +-- Partially updatable view +INSERT INTO rw_view14 VALUES (null, 3, 'Row 3'); -- should fail +ERROR: cannot insert into column "ctid" of view "rw_view14" +DETAIL: View columns that refer to system columns are not updatable. +INSERT INTO rw_view14 (a, b) VALUES (3, 'Row 3'); -- should be OK +UPDATE rw_view14 SET ctid=null WHERE a=3; -- should fail +ERROR: cannot update column "ctid" of view "rw_view14" +DETAIL: View columns that refer to system columns are not updatable. +UPDATE rw_view14 SET b='ROW 3' WHERE a=3; -- should be OK +SELECT * FROM base_tbl; + a | b +----+-------- + -2 | Row -2 + -1 | Row -1 + 0 | Row 0 + 1 | Row 1 + 2 | Row 2 + 3 | ROW 3 +(6 rows) + +DELETE FROM rw_view14 WHERE a=3; -- should be OK +-- Partially updatable view +INSERT INTO rw_view15 VALUES (3, 'ROW 3'); -- should fail +ERROR: cannot insert into column "upper" of view "rw_view15" +DETAIL: View columns that are not columns of their base relation are not updatable. +INSERT INTO rw_view15 (a) VALUES (3); -- should be OK +ALTER VIEW rw_view15 ALTER COLUMN upper SET DEFAULT 'NOT SET'; +INSERT INTO rw_view15 (a) VALUES (4); -- should fail +ERROR: cannot insert into column "upper" of view "rw_view15" +DETAIL: View columns that are not columns of their base relation are not updatable. +UPDATE rw_view15 SET upper='ROW 3' WHERE a=3; -- should fail +ERROR: cannot update column "upper" of view "rw_view15" +DETAIL: View columns that are not columns of their base relation are not updatable. +UPDATE rw_view15 SET upper=DEFAULT WHERE a=3; -- should fail +ERROR: cannot update column "upper" of view "rw_view15" +DETAIL: View columns that are not columns of their base relation are not updatable. +UPDATE rw_view15 SET a=4 WHERE a=3; -- should be OK +SELECT * FROM base_tbl; + a | b +----+------------- + -2 | Row -2 + -1 | Row -1 + 0 | Row 0 + 1 | Row 1 + 2 | Row 2 + 4 | Unspecified +(6 rows) + +DELETE FROM rw_view15 WHERE a=4; -- should be OK +-- Partially updatable view +INSERT INTO rw_view16 VALUES (3, 'Row 3', 3); -- should fail +ERROR: multiple assignments to same column "a" +INSERT INTO rw_view16 (a, b) VALUES (3, 'Row 3'); -- should be OK +UPDATE rw_view16 SET a=3, aa=-3 WHERE a=3; -- should fail +ERROR: multiple assignments to same column "a" +UPDATE rw_view16 SET aa=-3 WHERE a=3; -- should be OK +SELECT * FROM base_tbl; + a | b +----+-------- + -2 | Row -2 + -1 | Row -1 + 0 | Row 0 + 1 | Row 1 + 2 | Row 2 + -3 | Row 3 +(6 rows) + +DELETE FROM rw_view16 WHERE a=-3; -- should be OK +-- Read-only views +INSERT INTO ro_view17 VALUES (3, 'ROW 3'); +ERROR: cannot insert into view "ro_view1" +DETAIL: Views containing DISTINCT are not automatically updatable. +HINT: To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule. +DELETE FROM ro_view18; +ERROR: cannot delete from view "ro_view18" +DETAIL: Views that do not select from a single table or view are not automatically updatable. +HINT: To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule. +UPDATE ro_view19 SET max_value=1000; +ERROR: cannot update view "ro_view19" +DETAIL: Views that do not select from a single table or view are not automatically updatable. +HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule. +UPDATE ro_view20 SET b=upper(b); +ERROR: cannot update view "ro_view20" +DETAIL: Views that return set-returning functions are not automatically updatable. +HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule. +-- A view with a conditional INSTEAD rule but no unconditional INSTEAD rules +-- or INSTEAD OF triggers should be non-updatable and generate useful error +-- messages with appropriate detail +CREATE RULE rw_view16_ins_rule AS ON INSERT TO rw_view16 + WHERE NEW.a > 0 DO INSTEAD INSERT INTO base_tbl VALUES (NEW.a, NEW.b); +CREATE RULE rw_view16_upd_rule AS ON UPDATE TO rw_view16 + WHERE OLD.a > 0 DO INSTEAD UPDATE base_tbl SET b=NEW.b WHERE a=OLD.a; +CREATE RULE rw_view16_del_rule AS ON DELETE TO rw_view16 + WHERE OLD.a > 0 DO INSTEAD DELETE FROM base_tbl WHERE a=OLD.a; +INSERT INTO rw_view16 (a, b) VALUES (3, 'Row 3'); -- should fail +ERROR: cannot insert into view "rw_view16" +DETAIL: Views with conditional DO INSTEAD rules are not automatically updatable. +HINT: To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule. +UPDATE rw_view16 SET b='ROW 2' WHERE a=2; -- should fail +ERROR: cannot update view "rw_view16" +DETAIL: Views with conditional DO INSTEAD rules are not automatically updatable. +HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule. +DELETE FROM rw_view16 WHERE a=2; -- should fail +ERROR: cannot delete from view "rw_view16" +DETAIL: Views with conditional DO INSTEAD rules are not automatically updatable. +HINT: To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule. +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to 16 other objects +DETAIL: drop cascades to view ro_view1 +drop cascades to view ro_view17 +drop cascades to view ro_view2 +drop cascades to view ro_view3 +drop cascades to view ro_view5 +drop cascades to view ro_view6 +drop cascades to view ro_view7 +drop cascades to view ro_view8 +drop cascades to view ro_view9 +drop cascades to view ro_view11 +drop cascades to view ro_view13 +drop cascades to view rw_view15 +drop cascades to view rw_view16 +drop cascades to view ro_view20 +drop cascades to view ro_view4 +drop cascades to view rw_view14 +DROP VIEW ro_view10, ro_view12, ro_view18; +DROP SEQUENCE seq CASCADE; +NOTICE: drop cascades to view ro_view19 +-- simple updatable view +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "base_tbl_pkey" for table "base_tbl" +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0; +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name = 'rw_view1'; + table_name | is_insertable_into +------------+-------------------- + rw_view1 | YES +(1 row) + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name = 'rw_view1'; + table_name | is_updatable | is_insertable_into +------------+--------------+-------------------- + rw_view1 | YES | YES +(1 row) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name = 'rw_view1' + ORDER BY ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view1 | a | YES + rw_view1 | b | YES +(2 rows) + +INSERT INTO rw_view1 VALUES (3, 'Row 3'); +INSERT INTO rw_view1 (a) VALUES (4); +UPDATE rw_view1 SET a=5 WHERE a=4; +DELETE FROM rw_view1 WHERE b='Row 2'; +SELECT * FROM base_tbl; + a | b +----+------------- + -2 | Row -2 + -1 | Row -1 + 0 | Row 0 + 1 | Row 1 + 3 | Row 3 + 5 | Unspecified +(6 rows) + +EXPLAIN (costs off) UPDATE rw_view1 SET a=6 WHERE a=5; + QUERY PLAN +-------------------------------------------------- + [Bypass] + Update on base_tbl + -> Index Scan using base_tbl_pkey on base_tbl + Index Cond: ((a > 0) AND (a = 5)) +(4 rows) + +EXPLAIN (costs off) DELETE FROM rw_view1 WHERE a=5; + QUERY PLAN +-------------------------------------------------- + [Bypass] + Delete on base_tbl + -> Index Scan using base_tbl_pkey on base_tbl + Index Cond: ((a > 0) AND (a = 5)) +(4 rows) + +-- it's still updatable if we add a DO ALSO rule +CREATE TABLE base_tbl_hist(ts timestamptz default now(), a int, b text); +CREATE RULE base_tbl_log AS ON INSERT TO rw_view1 DO ALSO + INSERT INTO base_tbl_hist(a,b) VALUES(new.a, new.b); +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name = 'rw_view1'; + table_name | is_updatable | is_insertable_into +------------+--------------+-------------------- + rw_view1 | YES | YES +(1 row) + +-- Check behavior with DEFAULTs +INSERT INTO rw_view1 VALUES (9, DEFAULT), (10, DEFAULT); +SELECT a, b FROM base_tbl_hist; + a | b +----+--- + 9 | + 10 | +(2 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to view rw_view1 +DROP TABLE base_tbl_hist; +-- view on top of view +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "base_tbl_pkey" for table "base_tbl" +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); +CREATE VIEW rw_view1 AS SELECT b AS bb, a AS aa FROM base_tbl WHERE a>0; +CREATE VIEW rw_view2 AS SELECT aa AS aaa, bb AS bbb FROM rw_view1 WHERE aa<10; +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name = 'rw_view2'; + table_name | is_insertable_into +------------+-------------------- + rw_view2 | YES +(1 row) + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name = 'rw_view2'; + table_name | is_updatable | is_insertable_into +------------+--------------+-------------------- + rw_view2 | YES | YES +(1 row) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name = 'rw_view2' + ORDER BY ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view2 | aaa | YES + rw_view2 | bbb | YES +(2 rows) + +INSERT INTO rw_view2 VALUES (3, 'Row 3'); +INSERT INTO rw_view2 (aaa) VALUES (4); +SELECT * FROM rw_view2; + aaa | bbb +-----+------------- + 1 | Row 1 + 2 | Row 2 + 3 | Row 3 + 4 | Unspecified +(4 rows) + +UPDATE rw_view2 SET bbb='Row 4' WHERE aaa=4; +DELETE FROM rw_view2 WHERE aaa=2; +SELECT * FROM rw_view2; + aaa | bbb +-----+------- + 1 | Row 1 + 3 | Row 3 + 4 | Row 4 +(3 rows) + +EXPLAIN (costs off) UPDATE rw_view2 SET aaa=5 WHERE aaa=4; + QUERY PLAN +-------------------------------------------------------- + [Bypass] + Update on base_tbl + -> Index Scan using base_tbl_pkey on base_tbl + Index Cond: ((a < 10) AND (a > 0) AND (a = 4)) +(4 rows) + +EXPLAIN (costs off) DELETE FROM rw_view2 WHERE aaa=4; + QUERY PLAN +-------------------------------------------------------- + [Bypass] + Delete on base_tbl + -> Index Scan using base_tbl_pkey on base_tbl + Index Cond: ((a < 10) AND (a > 0) AND (a = 4)) +(4 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to view rw_view1 +drop cascades to view rw_view2 +-- view on top of view with rules +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "base_tbl_pkey" for table "base_tbl" +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0 OFFSET 0; -- not updatable without rules/triggers +CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a<10; +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_insertable_into +------------+-------------------- + rw_view1 | NO + rw_view2 | NO +(2 rows) + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into +------------+--------------+-------------------- + rw_view1 | NO | NO + rw_view2 | NO | NO +(2 rows) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view1 | a | NO + rw_view1 | b | NO + rw_view2 | a | NO + rw_view2 | b | NO +(4 rows) + +CREATE RULE rw_view1_ins_rule AS ON INSERT TO rw_view1 + DO INSTEAD INSERT INTO base_tbl VALUES (NEW.a, NEW.b) RETURNING *; +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_insertable_into +------------+-------------------- + rw_view1 | YES + rw_view2 | YES +(2 rows) + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into +------------+--------------+-------------------- + rw_view1 | NO | YES + rw_view2 | NO | YES +(2 rows) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view1 | a | NO + rw_view1 | b | NO + rw_view2 | a | NO + rw_view2 | b | NO +(4 rows) + +CREATE RULE rw_view1_upd_rule AS ON UPDATE TO rw_view1 + DO INSTEAD UPDATE base_tbl SET b=NEW.b WHERE a=OLD.a RETURNING NEW.*; +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_insertable_into +------------+-------------------- + rw_view1 | YES + rw_view2 | YES +(2 rows) + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into +------------+--------------+-------------------- + rw_view1 | NO | YES + rw_view2 | NO | YES +(2 rows) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view1 | a | NO + rw_view1 | b | NO + rw_view2 | a | NO + rw_view2 | b | NO +(4 rows) + +CREATE RULE rw_view1_del_rule AS ON DELETE TO rw_view1 + DO INSTEAD DELETE FROM base_tbl WHERE a=OLD.a RETURNING OLD.*; +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_insertable_into +------------+-------------------- + rw_view1 | YES + rw_view2 | YES +(2 rows) + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into +------------+--------------+-------------------- + rw_view1 | YES | YES + rw_view2 | YES | YES +(2 rows) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view1 | a | YES + rw_view1 | b | YES + rw_view2 | a | YES + rw_view2 | b | YES +(4 rows) + +INSERT INTO rw_view2 VALUES (3, 'Row 3') RETURNING *; + a | b +---+------- + 3 | Row 3 +(1 row) + +UPDATE rw_view2 SET b='Row three' WHERE a=3 RETURNING *; + a | b +---+----------- + 3 | Row three +(1 row) + +SELECT * FROM rw_view2; + a | b +---+----------- + 1 | Row 1 + 2 | Row 2 + 3 | Row three +(3 rows) + +DELETE FROM rw_view2 WHERE a=3 RETURNING *; + a | b +---+----------- + 3 | Row three +(1 row) + +SELECT * FROM rw_view2; + a | b +---+------- + 1 | Row 1 + 2 | Row 2 +(2 rows) + +EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2; + QUERY PLAN +------------------------------------------------------------------ + Update on base_tbl + -> Nested Loop + -> Index Scan using base_tbl_pkey on base_tbl + Index Cond: ((a = 2) AND (a < 10)) + -> Subquery Scan on rw_view1 + Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2)) + -> Limit + -> Bitmap Heap Scan on base_tbl + Recheck Cond: (a > 0) + -> Bitmap Index Scan on base_tbl_pkey + Index Cond: (a > 0) +(11 rows) + +EXPLAIN (costs off) DELETE FROM rw_view2 WHERE a=2; + QUERY PLAN +------------------------------------------------------------------ + Delete on base_tbl + -> Nested Loop + -> Index Scan using base_tbl_pkey on base_tbl + Index Cond: ((a = 2) AND (a < 10)) + -> Subquery Scan on rw_view1 + Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2)) + -> Limit + -> Bitmap Heap Scan on base_tbl + Recheck Cond: (a > 0) + -> Bitmap Index Scan on base_tbl_pkey + Index Cond: (a > 0) +(11 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to view rw_view1 +drop cascades to view rw_view2 +-- view on top of view with triggers +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "base_tbl_pkey" for table "base_tbl" +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0 OFFSET 0; -- not updatable without rules/triggers +CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a<10; +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_insertable_into +------------+-------------------- + rw_view1 | NO + rw_view2 | NO +(2 rows) + +SELECT table_name, is_updatable, is_insertable_into, + is_trigger_updatable, is_trigger_deletable, + is_trigger_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into +------------+--------------+--------------------+----------------------+----------------------+---------------------------- + rw_view1 | NO | NO | NO | NO | NO + rw_view2 | NO | NO | NO | NO | NO +(2 rows) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view1 | a | NO + rw_view1 | b | NO + rw_view2 | a | NO + rw_view2 | b | NO +(4 rows) + +CREATE FUNCTION rw_view1_trig_fn() +RETURNS trigger AS +$$ +BEGIN + IF TG_OP = 'INSERT' THEN + INSERT INTO base_tbl VALUES (NEW.a, NEW.b); + RETURN NEW; + ELSIF TG_OP = 'UPDATE' THEN + UPDATE base_tbl SET b=NEW.b WHERE a=OLD.a; + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + DELETE FROM base_tbl WHERE a=OLD.a; + RETURN OLD; + END IF; +END; +$$ +LANGUAGE plpgsql; +CREATE TRIGGER rw_view1_ins_trig INSTEAD OF INSERT ON rw_view1 + FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn(); +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_insertable_into +------------+-------------------- + rw_view1 | NO + rw_view2 | NO +(2 rows) + +SELECT table_name, is_updatable, is_insertable_into, + is_trigger_updatable, is_trigger_deletable, + is_trigger_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into +------------+--------------+--------------------+----------------------+----------------------+---------------------------- + rw_view1 | NO | NO | NO | NO | YES + rw_view2 | NO | NO | NO | NO | NO +(2 rows) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view1 | a | NO + rw_view1 | b | NO + rw_view2 | a | NO + rw_view2 | b | NO +(4 rows) + +CREATE TRIGGER rw_view1_upd_trig INSTEAD OF UPDATE ON rw_view1 + FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn(); +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_insertable_into +------------+-------------------- + rw_view1 | NO + rw_view2 | NO +(2 rows) + +SELECT table_name, is_updatable, is_insertable_into, + is_trigger_updatable, is_trigger_deletable, + is_trigger_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into +------------+--------------+--------------------+----------------------+----------------------+---------------------------- + rw_view1 | NO | NO | YES | NO | YES + rw_view2 | NO | NO | NO | NO | NO +(2 rows) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view1 | a | NO + rw_view1 | b | NO + rw_view2 | a | NO + rw_view2 | b | NO +(4 rows) + +CREATE TRIGGER rw_view1_del_trig INSTEAD OF DELETE ON rw_view1 + FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn(); +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_insertable_into +------------+-------------------- + rw_view1 | NO + rw_view2 | NO +(2 rows) + +SELECT table_name, is_updatable, is_insertable_into, + is_trigger_updatable, is_trigger_deletable, + is_trigger_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into +------------+--------------+--------------------+----------------------+----------------------+---------------------------- + rw_view1 | NO | NO | YES | YES | YES + rw_view2 | NO | NO | NO | NO | NO +(2 rows) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view1 | a | NO + rw_view1 | b | NO + rw_view2 | a | NO + rw_view2 | b | NO +(4 rows) + +INSERT INTO rw_view2 VALUES (3, 'Row 3') RETURNING *; + a | b +---+------- + 3 | Row 3 +(1 row) + +UPDATE rw_view2 SET b='Row three' WHERE a=3 RETURNING *; + a | b +---+----------- + 3 | Row three +(1 row) + +SELECT * FROM rw_view2; + a | b +---+----------- + 1 | Row 1 + 2 | Row 2 + 3 | Row three +(3 rows) + +DELETE FROM rw_view2 WHERE a=3 RETURNING *; + a | b +---+----------- + 3 | Row three +(1 row) + +SELECT * FROM rw_view2; + a | b +---+------- + 1 | Row 1 + 2 | Row 2 +(2 rows) + +EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2; + QUERY PLAN +------------------------------------------------------------ + Update on rw_view1 + -> Subquery Scan on rw_view1 + Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2)) + -> Limit + -> Bitmap Heap Scan on base_tbl + Recheck Cond: (a > 0) + -> Bitmap Index Scan on base_tbl_pkey + Index Cond: (a > 0) +(8 rows) + +EXPLAIN (costs off) DELETE FROM rw_view2 WHERE a=2; + QUERY PLAN +------------------------------------------------------------ + Delete on rw_view1 + -> Subquery Scan on rw_view1 + Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2)) + -> Limit + -> Bitmap Heap Scan on base_tbl + Recheck Cond: (a > 0) + -> Bitmap Index Scan on base_tbl_pkey + Index Cond: (a > 0) +(8 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to view rw_view1 +drop cascades to view rw_view2 +DROP FUNCTION rw_view1_trig_fn(); +-- update using whole row from view +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "base_tbl_pkey" for table "base_tbl" +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); +CREATE VIEW rw_view1 AS SELECT b AS bb, a AS aa FROM base_tbl; +CREATE FUNCTION rw_view1_aa(x rw_view1) + RETURNS int AS $$ SELECT x.aa $$ LANGUAGE sql; +UPDATE rw_view1 v SET bb='Updated row 2' WHERE rw_view1_aa(v)=2 + RETURNING rw_view1_aa(v), v.bb; + rw_view1_aa | bb +-------------+--------------- + 2 | Updated row 2 +(1 row) + +SELECT * FROM base_tbl; + a | b +----+--------------- + -2 | Row -2 + -1 | Row -1 + 0 | Row 0 + 1 | Row 1 + 2 | Updated row 2 +(5 rows) + +EXPLAIN (costs off) +UPDATE rw_view1 v SET bb='Updated row 2' WHERE rw_view1_aa(v)=2 + RETURNING rw_view1_aa(v), v.bb; + QUERY PLAN +-------------------------------------------------- + Update on base_tbl + -> Index Scan using base_tbl_pkey on base_tbl + Index Cond: (a = 2) +(3 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to view rw_view1 +drop cascades to function rw_view1_aa(rw_view1) +-- permissions checks +CREATE USER view_user1 PASSWORD 'Gauss@123'; +CREATE USER view_user2 PASSWORD 'Gauss@123'; +SET SESSION AUTHORIZATION view_user1 PASSWORD 'Gauss@123'; +CREATE TABLE base_tbl(a int, b text, c float); +INSERT INTO base_tbl VALUES (1, 'Row 1', 1.0); +CREATE VIEW rw_view1 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl; +INSERT INTO rw_view1 VALUES ('Row 2', 2.0, 2); +GRANT USAGE ON SCHEMA view_user1 to view_user2; +GRANT SELECT ON base_tbl TO view_user2; +GRANT SELECT ON rw_view1 TO view_user2; +GRANT UPDATE (a,c) ON base_tbl TO view_user2; +GRANT UPDATE (bb,cc) ON rw_view1 TO view_user2; +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION view_user2 PASSWORD 'Gauss@123'; +CREATE VIEW rw_view2 AS SELECT b AS bb, c AS cc, a AS aa FROM view_user1.base_tbl; +SELECT * FROM view_user1.base_tbl; -- ok + a | b | c +---+-------+--- + 1 | Row 1 | 1 + 2 | Row 2 | 2 +(2 rows) + +SELECT * FROM view_user1.rw_view1; -- ok + bb | cc | aa +-------+----+---- + Row 1 | 1 | 1 + Row 2 | 2 | 2 +(2 rows) + +SELECT * FROM rw_view2; -- ok + bb | cc | aa +-------+----+---- + Row 1 | 1 | 1 + Row 2 | 2 | 2 +(2 rows) + +INSERT INTO view_user1.base_tbl VALUES (3, 'Row 3', 3.0); -- not allowed +ERROR: permission denied for relation base_tbl +DETAIL: N/A +INSERT INTO view_user1.rw_view1 VALUES ('Row 3', 3.0, 3); -- not allowed +ERROR: permission denied for relation rw_view1 +DETAIL: N/A +INSERT INTO rw_view2 VALUES ('Row 3', 3.0, 3); -- not allowed +ERROR: permission denied for relation base_tbl +DETAIL: N/A +UPDATE view_user1.base_tbl SET a=a, c=c; -- ok +UPDATE view_user1.base_tbl SET b=b; -- not allowed +ERROR: permission denied for relation base_tbl +DETAIL: N/A +UPDATE view_user1.rw_view1 SET bb=bb, cc=cc; -- ok +UPDATE view_user1.rw_view1 SET aa=aa; -- not allowed +ERROR: permission denied for relation rw_view1 +DETAIL: N/A +UPDATE rw_view2 SET aa=aa, cc=cc; -- ok +UPDATE rw_view2 SET bb=bb; -- not allowed +ERROR: permission denied for relation base_tbl +DETAIL: N/A +DELETE FROM view_user1.base_tbl; -- not allowed +ERROR: permission denied for relation base_tbl +DETAIL: N/A +DELETE FROM view_user1.rw_view1; -- not allowed +ERROR: permission denied for relation rw_view1 +DETAIL: N/A +DELETE FROM rw_view2; -- not allowed +ERROR: permission denied for relation base_tbl +DETAIL: N/A +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION view_user1 PASSWORD 'Gauss@123'; +GRANT INSERT, DELETE ON base_tbl TO view_user2; +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION view_user2 PASSWORD 'Gauss@123'; +INSERT INTO view_user1.base_tbl VALUES (3, 'Row 3', 3.0); -- ok +INSERT INTO view_user1.rw_view1 VALUES ('Row 4', 4.0, 4); -- not allowed +ERROR: permission denied for relation rw_view1 +DETAIL: N/A +INSERT INTO rw_view2 VALUES ('Row 4', 4.0, 4); -- ok +DELETE FROM view_user1.base_tbl WHERE a=1; -- ok +DELETE FROM view_user1.rw_view1 WHERE aa=2; -- not allowed +ERROR: permission denied for relation rw_view1 +DETAIL: N/A +DELETE FROM rw_view2 WHERE aa=2; -- ok +SELECT * FROM view_user1.base_tbl; + a | b | c +---+-------+--- + 3 | Row 3 | 3 + 4 | Row 4 | 4 +(2 rows) + +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION view_user1 PASSWORD 'Gauss@123'; +REVOKE INSERT, DELETE ON base_tbl FROM view_user2; +GRANT INSERT, DELETE ON rw_view1 TO view_user2; +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION view_user2 PASSWORD 'Gauss@123'; +INSERT INTO view_user1.base_tbl VALUES (5, 'Row 5', 5.0); -- not allowed +ERROR: permission denied for relation base_tbl +DETAIL: N/A +INSERT INTO view_user1.rw_view1 VALUES ('Row 5', 5.0, 5); -- ok +INSERT INTO rw_view2 VALUES ('Row 6', 6.0, 6); -- not allowed +ERROR: permission denied for relation base_tbl +DETAIL: N/A +DELETE FROM view_user1.base_tbl WHERE a=3; -- not allowed +ERROR: permission denied for relation base_tbl +DETAIL: N/A +DELETE FROM view_user1.rw_view1 WHERE aa=3; -- ok +DELETE FROM rw_view2 WHERE aa=4; -- not allowed +ERROR: permission denied for relation base_tbl +DETAIL: N/A +SELECT * FROM view_user1.base_tbl; + a | b | c +---+-------+--- + 4 | Row 4 | 4 + 5 | Row 5 | 5 +(2 rows) + +RESET SESSION AUTHORIZATION; +DROP TABLE view_user1.base_tbl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to view view_user1.rw_view1 +drop cascades to view view_user2.rw_view2 +DROP USER view_user1; +DROP USER view_user2; +-- column defaults +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified', c serial); +NOTICE: CREATE TABLE will create implicit sequence "base_tbl_c_seq" for serial column "base_tbl.c" +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "base_tbl_pkey" for table "base_tbl" +INSERT INTO base_tbl VALUES (1, 'Row 1'); +INSERT INTO base_tbl VALUES (2, 'Row 2'); +INSERT INTO base_tbl VALUES (3); +CREATE VIEW rw_view1 AS SELECT a AS aa, b AS bb FROM base_tbl; +ALTER VIEW rw_view1 ALTER COLUMN bb SET DEFAULT 'View default'; +INSERT INTO rw_view1 VALUES (4, 'Row 4'); +INSERT INTO rw_view1 (aa) VALUES (5); +SELECT * FROM base_tbl; + a | b | c +---+--------------+--- + 1 | Row 1 | 1 + 2 | Row 2 | 2 + 3 | Unspecified | 3 + 4 | Row 4 | 4 + 5 | View default | 5 +(5 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to view rw_view1 +-- Table having triggers +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "base_tbl_pkey" for table "base_tbl" +INSERT INTO base_tbl VALUES (1, 'Row 1'); +INSERT INTO base_tbl VALUES (2, 'Row 2'); +CREATE FUNCTION rw_view1_trig_fn() +RETURNS trigger AS +$$ +BEGIN + IF TG_OP = 'INSERT' THEN + UPDATE base_tbl SET b=NEW.b WHERE a=1; + RETURN NULL; + END IF; + RETURN NULL; +END; +$$ +LANGUAGE plpgsql; +CREATE TRIGGER rw_view1_ins_trig AFTER INSERT ON base_tbl + FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn(); +CREATE VIEW rw_view1 AS SELECT a AS aa, b AS bb FROM base_tbl; +INSERT INTO rw_view1 VALUES (3, 'Row 3'); +select * from base_tbl; + a | b +---+------- + 2 | Row 2 + 3 | Row 3 + 1 | Row 3 +(3 rows) + +DROP VIEW rw_view1; +DROP TRIGGER rw_view1_ins_trig on base_tbl; +DROP FUNCTION rw_view1_trig_fn(); +DROP TABLE base_tbl; +-- view with ORDER BY +CREATE TABLE base_tbl (a int, b int); +INSERT INTO base_tbl VALUES (1,2), (4,5), (3,-3); +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl ORDER BY a+b; +SELECT * FROM rw_view1; + a | b +---+---- + 3 | -3 + 1 | 2 + 4 | 5 +(3 rows) + +INSERT INTO rw_view1 VALUES (7,-8); +SELECT * FROM rw_view1; + a | b +---+---- + 7 | -8 + 3 | -3 + 1 | 2 + 4 | 5 +(4 rows) + +EXPLAIN (verbose, costs off) UPDATE rw_view1 SET b = b + 1 RETURNING *; + QUERY PLAN +------------------------------------------------------------- + Update on public.base_tbl + Output: base_tbl.a, base_tbl.b + -> Seq Scan on public.base_tbl + Output: base_tbl.a, (base_tbl.b + 1), base_tbl.ctid +(4 rows) + +UPDATE rw_view1 SET b = b + 1 RETURNING *; + a | b +---+---- + 1 | 3 + 4 | 6 + 3 | -2 + 7 | -7 +(4 rows) + +SELECT * FROM rw_view1; + a | b +---+---- + 7 | -7 + 3 | -2 + 1 | 3 + 4 | 6 +(4 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to view rw_view1 +-- multiple array-column updates +CREATE TABLE base_tbl (a int, arr int[]); +INSERT INTO base_tbl VALUES (1,ARRAY[2]), (3,ARRAY[4]); +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl; +UPDATE rw_view1 SET arr[1] = 42, arr[2] = 77 WHERE a = 3; +SELECT * FROM rw_view1; + a | arr +---+--------- + 1 | {2} + 3 | {42,77} +(2 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to view rw_view1 +-- views with updatable and non-updatable columns +CREATE TABLE base_tbl(a float); +INSERT INTO base_tbl SELECT i/10.0 FROM generate_series(1,10) g(i); +CREATE VIEW rw_view1 AS + SELECT ctid, sin(a) s, a, cos(a) c + FROM base_tbl + WHERE a != 0 + ORDER BY abs(a); +INSERT INTO rw_view1 VALUES (null, null, 1.1, null); -- should fail +ERROR: cannot insert into column "ctid" of view "rw_view1" +DETAIL: View columns that refer to system columns are not updatable. +INSERT INTO rw_view1 (s, c, a) VALUES (null, null, 1.1); -- should fail +ERROR: cannot insert into column "s" of view "rw_view1" +DETAIL: View columns that are not columns of their base relation are not updatable. +INSERT INTO rw_view1 (a) VALUES (1.1) RETURNING a, s, c; -- OK + a | s | c +-----+------------------+------------------ + 1.1 | .891207360061435 | .453596121425577 +(1 row) + +UPDATE rw_view1 SET s = s WHERE a = 1.1; -- should fail +ERROR: cannot update column "s" of view "rw_view1" +DETAIL: View columns that are not columns of their base relation are not updatable. +UPDATE rw_view1 SET a = 1.05 WHERE a = 1.1 RETURNING s; -- OK + s +------------------ + .867423225594017 +(1 row) + +DELETE FROM rw_view1 WHERE a = 1.05; -- OK +CREATE VIEW rw_view2 AS + SELECT s, c, s/c t, a base_a, ctid + FROM rw_view1; +INSERT INTO rw_view2 VALUES (null, null, null, 1.1, null); -- should fail +ERROR: cannot insert into column "t" of view "rw_view2" +DETAIL: View columns that are not columns of their base relation are not updatable. +INSERT INTO rw_view2(s, c, base_a) VALUES (null, null, 1.1); -- should fail +ERROR: cannot insert into column "s" of view "rw_view1" +DETAIL: View columns that are not columns of their base relation are not updatable. +INSERT INTO rw_view2(base_a) VALUES (1.1) RETURNING t; -- OK + t +------------------ + 1.96475965724865 +(1 row) + +UPDATE rw_view2 SET s = s WHERE base_a = 1.1; -- should fail +ERROR: cannot update column "s" of view "rw_view1" +DETAIL: View columns that are not columns of their base relation are not updatable. +UPDATE rw_view2 SET t = t WHERE base_a = 1.1; -- should fail +ERROR: cannot update column "t" of view "rw_view2" +DETAIL: View columns that are not columns of their base relation are not updatable. +UPDATE rw_view2 SET base_a = 1.05 WHERE base_a = 1.1; -- OK +DELETE FROM rw_view2 WHERE base_a = 1.05 RETURNING base_a, s, c, t; -- OK + base_a | s | c | t +--------+------------------+------------------+------------------ + 1.05 | .867423225594017 | .497571047891727 | 1.74331530998317 +(1 row) + +CREATE VIEW rw_view3 AS + SELECT s, c, s/c t, ctid + FROM rw_view1; +INSERT INTO rw_view3 VALUES (null, null, null, null); -- should fail +ERROR: cannot insert into column "t" of view "rw_view3" +DETAIL: View columns that are not columns of their base relation are not updatable. +INSERT INTO rw_view3(s) VALUES (null); -- should fail +ERROR: cannot insert into column "s" of view "rw_view1" +DETAIL: View columns that are not columns of their base relation are not updatable. +UPDATE rw_view3 SET s = s; -- should fail +ERROR: cannot update column "s" of view "rw_view1" +DETAIL: View columns that are not columns of their base relation are not updatable. +DELETE FROM rw_view3 WHERE s = sin(0.1); -- should be OK +SELECT * FROM base_tbl ORDER BY a; + a +---- + .2 + .3 + .4 + .5 + .6 + .7 + .8 + .9 + 1 +(9 rows) + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name; + table_name | is_insertable_into +------------+-------------------- + rw_view1 | YES + rw_view2 | YES + rw_view3 | NO +(3 rows) + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into +------------+--------------+-------------------- + rw_view1 | YES | YES + rw_view2 | YES | YES + rw_view3 | NO | NO +(3 rows) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view1 | ctid | NO + rw_view1 | s | NO + rw_view1 | a | YES + rw_view1 | c | NO + rw_view2 | s | NO + rw_view2 | c | NO + rw_view2 | t | NO + rw_view2 | base_a | YES + rw_view2 | ctid | NO + rw_view3 | s | NO + rw_view3 | c | NO + rw_view3 | t | NO + rw_view3 | ctid | NO +(13 rows) + +SELECT events & 4 != 0 AS upd, + events & 8 != 0 AS ins, + events & 16 != 0 AS del + FROM pg_catalog.pg_relation_is_updatable('rw_view3'::regclass, false) t(events); + upd | ins | del +-----+-----+----- + f | f | t +(1 row) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to view rw_view1 +drop cascades to view rw_view2 +drop cascades to view rw_view3 +-- view on table with GENERATED columns +CREATE TABLE base_tbl (id int, idplus1 int GENERATED ALWAYS AS (id + 1) STORED); +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl; +INSERT INTO base_tbl (id) VALUES (1); +INSERT INTO rw_view1 (id) VALUES (2); +INSERT INTO base_tbl (id, idplus1) VALUES (3, DEFAULT); +INSERT INTO rw_view1 (id, idplus1) VALUES (4, DEFAULT); +INSERT INTO base_tbl (id, idplus1) VALUES (5, 6); -- error +ERROR: cannot insert into column "idplus1" +DETAIL: Column "idplus1" is a generated column. +INSERT INTO rw_view1 (id, idplus1) VALUES (6, 7); -- error +ERROR: cannot insert into column "idplus1" +DETAIL: Column "idplus1" is a generated column. +SELECT * FROM base_tbl; + id | idplus1 +----+--------- + 1 | 2 + 2 | 3 + 3 | 4 + 4 | 5 +(4 rows) + +UPDATE base_tbl SET id = 2000 WHERE id = 2; +UPDATE rw_view1 SET id = 3000 WHERE id = 3; +SELECT * FROM base_tbl; + id | idplus1 +------+--------- + 1 | 2 + 4 | 5 + 2000 | 2001 + 3000 | 3001 +(4 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to view rw_view1 +-- simple WITH CHECK OPTION +CREATE TABLE base_tbl (a int, b int DEFAULT 10); +INSERT INTO base_tbl VALUES (1,2), (2,3), (1,-1); +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a < b + WITH LOCAL CHECK OPTION; +\d+ rw_view1 + View "public.rw_view1" + Column | Type | Modifiers | Storage | Description +--------+---------+-----------+---------+------------- + a | integer | | plain | + b | integer | | plain | +View definition: + SELECT * + FROM base_tbl + WHERE base_tbl.a < base_tbl.b; +Options: check_option=local + +SELECT * FROM information_schema.views WHERE table_name = 'rw_view1'; + table_catalog | table_schema | table_name | view_definition | check_option | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into +---------------+--------------+------------+----------------------------------------------------------+--------------+--------------+--------------------+----------------------+----------------------+---------------------------- + regression | public | rw_view1 | SELECT * FROM base_tbl WHERE (base_tbl.a < base_tbl.b); | LOCAL | YES | YES | NO | NO | NO +(1 row) + +INSERT INTO rw_view1 VALUES(3,4); -- ok +INSERT INTO rw_view1 VALUES(4,3); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (4, 3). +INSERT INTO rw_view1 VALUES(5,null); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (5, null). +UPDATE rw_view1 SET b = 5 WHERE a = 3; -- ok +UPDATE rw_view1 SET b = -5 WHERE a = 3; -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (3, -5). +INSERT INTO rw_view1(a) VALUES (9); -- ok +INSERT INTO rw_view1(a) VALUES (10); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (10, 10). +SELECT * FROM base_tbl; + a | b +---+---- + 1 | 2 + 2 | 3 + 1 | -1 + 3 | 5 + 9 | 10 +(5 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to view rw_view1 +-- WITH LOCAL/CASCADED CHECK OPTION +CREATE TABLE base_tbl (a int); +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a > 0; +CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a < 10 + WITH CHECK OPTION; -- implicitly cascaded +\d+ rw_view2 + View "public.rw_view2" + Column | Type | Modifiers | Storage | Description +--------+---------+-----------+---------+------------- + a | integer | | plain | +View definition: + SELECT * + FROM rw_view1 + WHERE rw_view1.a < 10; +Options: check_option=cascaded + +SELECT * FROM information_schema.views WHERE table_name = 'rw_view2'; + table_catalog | table_schema | table_name | view_definition | check_option | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into +---------------+--------------+------------+--------------------------------------------------+--------------+--------------+--------------------+----------------------+----------------------+---------------------------- + regression | public | rw_view2 | SELECT * FROM rw_view1 WHERE (rw_view1.a < 10); | CASCADED | YES | YES | NO | NO | NO +(1 row) + +INSERT INTO rw_view2 VALUES (-5); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (-5). +INSERT INTO rw_view2 VALUES (5); -- ok +INSERT INTO rw_view2 VALUES (15); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +DETAIL: Failing row contains (15). +SELECT * FROM base_tbl; + a +--- + 5 +(1 row) + +UPDATE rw_view2 SET a = a - 10; -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (-5). +UPDATE rw_view2 SET a = a + 10; -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +DETAIL: Failing row contains (15). +CREATE OR REPLACE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a < 10 + WITH LOCAL CHECK OPTION; +\d+ rw_view2 + View "public.rw_view2" + Column | Type | Modifiers | Storage | Description +--------+---------+-----------+---------+------------- + a | integer | | plain | +View definition: + SELECT * + FROM rw_view1 + WHERE rw_view1.a < 10; +Options: check_option=local + +SELECT * FROM information_schema.views WHERE table_name = 'rw_view2'; + table_catalog | table_schema | table_name | view_definition | check_option | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into +---------------+--------------+------------+--------------------------------------------------+--------------+--------------+--------------------+----------------------+----------------------+---------------------------- + regression | public | rw_view2 | SELECT * FROM rw_view1 WHERE (rw_view1.a < 10); | LOCAL | YES | YES | NO | NO | NO +(1 row) + +INSERT INTO rw_view2 VALUES (-10); -- ok, but not in view +INSERT INTO rw_view2 VALUES (20); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +DETAIL: Failing row contains (20). +SELECT * FROM base_tbl; + a +----- + 5 + -10 +(2 rows) + +ALTER VIEW rw_view1 SET (check_option=here); -- invalid +ERROR: invalid value for "check_option" option +DETAIL: Valid values are "local", and "cascaded". +ALTER VIEW rw_view1 SET (check_option=local); +INSERT INTO rw_view2 VALUES (-20); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (-20). +INSERT INTO rw_view2 VALUES (30); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +DETAIL: Failing row contains (30). +ALTER VIEW rw_view2 RESET (check_option); +\d+ rw_view2 + View "public.rw_view2" + Column | Type | Modifiers | Storage | Description +--------+---------+-----------+---------+------------- + a | integer | | plain | +View definition: + SELECT * + FROM rw_view1 + WHERE rw_view1.a < 10; + +SELECT * FROM information_schema.views WHERE table_name = 'rw_view2'; + table_catalog | table_schema | table_name | view_definition | check_option | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into +---------------+--------------+------------+--------------------------------------------------+--------------+--------------+--------------------+----------------------+----------------------+---------------------------- + regression | public | rw_view2 | SELECT * FROM rw_view1 WHERE (rw_view1.a < 10); | NONE | YES | YES | NO | NO | NO +(1 row) + +INSERT INTO rw_view2 VALUES (30); -- ok, but not in view +SELECT * FROM base_tbl; + a +----- + 5 + -10 + 30 +(3 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to view rw_view1 +drop cascades to view rw_view2 +-- WITH CHECK OPTION with no local view qual +CREATE TABLE base_tbl (a int); +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WITH CHECK OPTION; +CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a > 0; +CREATE VIEW rw_view3 AS SELECT * FROM rw_view2 WITH CHECK OPTION; +SELECT * FROM information_schema.views WHERE table_name LIKE E'rw\_view_' ORDER BY table_name; + table_catalog | table_schema | table_name | view_definition | check_option | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into +---------------+--------------+------------+-------------------------------------------------+--------------+--------------+--------------------+----------------------+----------------------+---------------------------- + regression | public | rw_view1 | SELECT * FROM base_tbl; | CASCADED | YES | YES | NO | NO | NO + regression | public | rw_view2 | SELECT * FROM rw_view1 WHERE (rw_view1.a > 0); | NONE | YES | YES | NO | NO | NO + regression | public | rw_view3 | SELECT * FROM rw_view2; | CASCADED | YES | YES | NO | NO | NO +(3 rows) + +INSERT INTO rw_view1 VALUES (-1); -- ok +INSERT INTO rw_view1 VALUES (1); -- ok +INSERT INTO rw_view2 VALUES (-2); -- ok, but not in view +INSERT INTO rw_view2 VALUES (2); -- ok +INSERT INTO rw_view3 VALUES (-3); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +DETAIL: Failing row contains (-3). +INSERT INTO rw_view3 VALUES (3); -- ok +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to view rw_view1 +drop cascades to view rw_view2 +drop cascades to view rw_view3 +-- WITH CHECK OPTION with subquery +CREATE TABLE base_tbl (a int); +CREATE TABLE ref_tbl (a int PRIMARY KEY); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "ref_tbl_pkey" for table "ref_tbl" +INSERT INTO ref_tbl SELECT * FROM generate_series(1,10); +CREATE VIEW rw_view1 AS + SELECT * FROM base_tbl b + WHERE EXISTS(SELECT 1 FROM ref_tbl r WHERE r.a = b.a) + WITH CHECK OPTION; +INSERT INTO rw_view1 VALUES (5); -- ok +INSERT INTO rw_view1 VALUES (15); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (15). +UPDATE rw_view1 SET a = a + 5; -- ok +UPDATE rw_view1 SET a = a + 5; -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (15). +EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (5); + QUERY PLAN +--------------------------------------------------------------- + Insert on base_tbl b + -> Result + SubPlan 1 + -> Index Only Scan using ref_tbl_pkey on ref_tbl r + Index Cond: (a = b.a) + SubPlan 2 + -> Seq Scan on ref_tbl r +(7 rows) + +EXPLAIN (costs off) UPDATE rw_view1 SET a = a + 5; + QUERY PLAN +--------------------------------------------------------------- + Update on base_tbl b + -> Hash Semi Join + Hash Cond: (b.a = r.a) + -> Seq Scan on base_tbl b + -> Hash + -> Seq Scan on ref_tbl r + SubPlan 1 + -> Index Only Scan using ref_tbl_pkey on ref_tbl r + Index Cond: (a = b.a) + SubPlan 2 + -> Seq Scan on ref_tbl r +(11 rows) + +DROP TABLE base_tbl, ref_tbl CASCADE; +NOTICE: drop cascades to view rw_view1 +-- WITH CHECK OPTION with BEFORE trigger on base table +CREATE TABLE base_tbl (a int, b int); +CREATE FUNCTION base_tbl_trig_fn() +RETURNS trigger AS +$$ +BEGIN + NEW.b := 10; + RETURN NEW; +END; +$$ +LANGUAGE plpgsql; +CREATE TRIGGER base_tbl_trig BEFORE INSERT OR UPDATE ON base_tbl + FOR EACH ROW EXECUTE PROCEDURE base_tbl_trig_fn(); +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a < b WITH CHECK OPTION; +INSERT INTO rw_view1 VALUES (5,0); -- ok +INSERT INTO rw_view1 VALUES (15, 20); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (15, 10). +UPDATE rw_view1 SET a = 20, b = 30; -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (20, 10). +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to view rw_view1 +DROP FUNCTION base_tbl_trig_fn(); +-- WITH LOCAL CHECK OPTION with INSTEAD OF trigger on base view +CREATE TABLE base_tbl (a int, b int); +CREATE VIEW rw_view1 AS SELECT a FROM base_tbl WHERE a < b; +CREATE FUNCTION rw_view1_trig_fn() +RETURNS trigger AS +$$ +BEGIN + IF TG_OP = 'INSERT' THEN + INSERT INTO base_tbl VALUES (NEW.a, 10); + RETURN NEW; + ELSIF TG_OP = 'UPDATE' THEN + UPDATE base_tbl SET a=NEW.a WHERE a=OLD.a; + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + DELETE FROM base_tbl WHERE a=OLD.a; + RETURN OLD; + END IF; +END; +$$ +LANGUAGE plpgsql; +CREATE TRIGGER rw_view1_trig + INSTEAD OF INSERT OR UPDATE OR DELETE ON rw_view1 + FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn(); +CREATE VIEW rw_view2 AS + SELECT * FROM rw_view1 WHERE a > 0 WITH LOCAL CHECK OPTION; +INSERT INTO rw_view2 VALUES (-5); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +DETAIL: Failing row contains (-5). +INSERT INTO rw_view2 VALUES (5); -- ok +INSERT INTO rw_view2 VALUES (50); -- ok, but not in view +UPDATE rw_view2 SET a = a - 10; -- should fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +DETAIL: Failing row contains (-5). +SELECT * FROM base_tbl; + a | b +----+---- + 5 | 10 + 50 | 10 +(2 rows) + +-- Check option won't cascade down to base view with INSTEAD OF triggers +ALTER VIEW rw_view2 SET (check_option=cascaded); +INSERT INTO rw_view2 VALUES (100); -- ok, but not in view (doesn't fail rw_view1's check) +UPDATE rw_view2 SET a = 200 WHERE a = 5; -- ok, but not in view (doesn't fail rw_view1's check) +SELECT * FROM base_tbl; + a | b +-----+---- + 50 | 10 + 100 | 10 + 200 | 10 +(3 rows) + +-- Neither local nor cascaded check options work with INSTEAD rules +DROP TRIGGER rw_view1_trig ON rw_view1; +CREATE RULE rw_view1_ins_rule AS ON INSERT TO rw_view1 + DO INSTEAD INSERT INTO base_tbl VALUES (NEW.a, 10); +CREATE RULE rw_view1_upd_rule AS ON UPDATE TO rw_view1 + DO INSTEAD UPDATE base_tbl SET a=NEW.a WHERE a=OLD.a; +INSERT INTO rw_view2 VALUES (-10); -- ok, but not in view (doesn't fail rw_view2's check) +INSERT INTO rw_view2 VALUES (5); -- ok +INSERT INTO rw_view2 VALUES (20); -- ok, but not in view (doesn't fail rw_view1's check) +UPDATE rw_view2 SET a = 30 WHERE a = 5; -- ok, but not in view (doesn't fail rw_view1's check) +INSERT INTO rw_view2 VALUES (5); -- ok +UPDATE rw_view2 SET a = -5 WHERE a = 5; -- ok, but not in view (doesn't fail rw_view2's check) +SELECT * FROM base_tbl; + a | b +-----+---- + 50 | 10 + 100 | 10 + 200 | 10 + -10 | 10 + 20 | 10 + 30 | 10 + -5 | 10 +(7 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to view rw_view1 +drop cascades to view rw_view2 +DROP FUNCTION rw_view1_trig_fn(); +CREATE TABLE base_tbl (a int); +CREATE VIEW rw_view1 AS SELECT a,10 AS b FROM base_tbl; +CREATE RULE rw_view1_ins_rule AS ON INSERT TO rw_view1 + DO INSTEAD INSERT INTO base_tbl VALUES (NEW.a); +CREATE VIEW rw_view2 AS + SELECT * FROM rw_view1 WHERE a > b WITH LOCAL CHECK OPTION; +INSERT INTO rw_view2 VALUES (2,3); -- ok, but not in view (doesn't fail rw_view2's check) +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to view rw_view1 +drop cascades to view rw_view2 +-- security barrier view +CREATE TABLE base_tbl (person text, visibility text); +INSERT INTO base_tbl VALUES ('Tom', 'public'), + ('Dick', 'private'), + ('Harry', 'public'); +CREATE VIEW rw_view1 AS + SELECT person FROM base_tbl WHERE visibility = 'public'; +CREATE FUNCTION snoop(anyelement) +RETURNS boolean AS +$$ +BEGIN + RAISE NOTICE 'snooped value: %', $1; + RETURN true; +END; +$$ +LANGUAGE plpgsql COST 0.000001; +SELECT * FROM rw_view1 WHERE snoop(person); +NOTICE: snooped value: Tom +NOTICE: snooped value: Dick +NOTICE: snooped value: Harry + person +-------- + Tom + Harry +(2 rows) + +UPDATE rw_view1 SET person=person WHERE snoop(person); +NOTICE: snooped value: Tom +NOTICE: snooped value: Dick +NOTICE: snooped value: Harry +DELETE FROM rw_view1 WHERE NOT snoop(person); +NOTICE: snooped value: Dick +NOTICE: snooped value: Tom +NOTICE: snooped value: Harry +ALTER VIEW rw_view1 SET (security_barrier = true); +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name = 'rw_view1'; + table_name | is_insertable_into +------------+-------------------- + rw_view1 | YES +(1 row) + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name = 'rw_view1'; + table_name | is_updatable | is_insertable_into +------------+--------------+-------------------- + rw_view1 | YES | YES +(1 row) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name = 'rw_view1' + ORDER BY ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view1 | person | YES +(1 row) + +SELECT * FROM rw_view1 WHERE snoop(person); +NOTICE: snooped value: Tom +NOTICE: snooped value: Harry + person +-------- + Tom + Harry +(2 rows) + +UPDATE rw_view1 SET person=person WHERE snoop(person); +NOTICE: snooped value: Tom +NOTICE: snooped value: Harry +DELETE FROM rw_view1 WHERE NOT snoop(person); +NOTICE: snooped value: Tom +NOTICE: snooped value: Harry +EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person); + QUERY PLAN +----------------------------------------------- + Subquery Scan on rw_view1 + Filter: snoop(rw_view1.person) + -> Seq Scan on base_tbl + Filter: (visibility = 'public'::text) +(4 rows) + +EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person); + QUERY PLAN +------------------------------------------------------------------- + Update on base_tbl + -> Seq Scan on base_tbl + Filter: ((visibility = 'public'::text) AND snoop(person)) + Notice: This query is influenced by row level security feature +(4 rows) + +EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person); + QUERY PLAN +------------------------------------------------------------------------- + Delete on base_tbl + -> Seq Scan on base_tbl + Filter: ((visibility = 'public'::text) AND (NOT snoop(person))) + Notice: This query is influenced by row level security feature +(4 rows) + +-- security barrier view on top of security barrier view +CREATE VIEW rw_view2 WITH (security_barrier = true) AS + SELECT * FROM rw_view1 WHERE snoop(person); +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name = 'rw_view2'; + table_name | is_insertable_into +------------+-------------------- + rw_view2 | YES +(1 row) + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name = 'rw_view2'; + table_name | is_updatable | is_insertable_into +------------+--------------+-------------------- + rw_view2 | YES | YES +(1 row) + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name = 'rw_view2' + ORDER BY ordinal_position; + table_name | column_name | is_updatable +------------+-------------+-------------- + rw_view2 | person | YES +(1 row) + +SELECT * FROM rw_view2 WHERE snoop(person); +NOTICE: snooped value: Tom +NOTICE: snooped value: Tom +NOTICE: snooped value: Harry +NOTICE: snooped value: Harry + person +-------- + Tom + Harry +(2 rows) + +UPDATE rw_view2 SET person=person WHERE snoop(person); +NOTICE: snooped value: Tom +NOTICE: snooped value: Tom +NOTICE: snooped value: Harry +NOTICE: snooped value: Harry +DELETE FROM rw_view2 WHERE NOT snoop(person); +NOTICE: snooped value: Tom +NOTICE: snooped value: Tom +NOTICE: snooped value: Harry +NOTICE: snooped value: Harry +EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person); + QUERY PLAN +----------------------------------------------------- + Subquery Scan on rw_view2 + Filter: snoop(rw_view2.person) + -> Subquery Scan on rw_view1 + Filter: snoop(rw_view1.person) + -> Seq Scan on base_tbl + Filter: (visibility = 'public'::text) +(6 rows) + +EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person); + QUERY PLAN +------------------------------------------------------------------------------------- + Update on base_tbl + -> Seq Scan on base_tbl + Filter: ((visibility = 'public'::text) AND snoop(person) AND snoop(person)) + Notice: This query is influenced by row level security feature +(4 rows) + +EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person); + QUERY PLAN +------------------------------------------------------------------------------------------- + Delete on base_tbl + -> Seq Scan on base_tbl + Filter: ((visibility = 'public'::text) AND snoop(person) AND (NOT snoop(person))) + Notice: This query is influenced by row level security feature +(4 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to view rw_view1 +drop cascades to view rw_view2 +-- security barrier view on top of table with rules +CREATE TABLE base_tbl(id int PRIMARY KEY, data text, deleted boolean); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "base_tbl_pkey" for table "base_tbl" +INSERT INTO base_tbl VALUES (1, 'Row 1', false), (2, 'Row 2', true); +CREATE RULE base_tbl_ins_rule AS ON INSERT TO base_tbl + WHERE EXISTS (SELECT 1 FROM base_tbl t WHERE t.id = new.id) + DO INSTEAD + UPDATE base_tbl SET data = new.data, deleted = false WHERE id = new.id; +CREATE RULE base_tbl_del_rule AS ON DELETE TO base_tbl + DO INSTEAD + UPDATE base_tbl SET deleted = true WHERE id = old.id; +CREATE VIEW rw_view1 WITH (security_barrier=true) AS + SELECT id, data FROM base_tbl WHERE NOT deleted; +SELECT * FROM rw_view1; + id | data +----+------- + 1 | Row 1 +(1 row) + +EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data); + QUERY PLAN +---------------------------------------------------------------- + Update on base_tbl + -> Nested Loop + -> Index Scan using base_tbl_pkey on base_tbl + Index Cond: (id = 1) + -> Index Scan using base_tbl_pkey on base_tbl + Index Cond: (id = 1) + Filter: ((NOT deleted) AND snoop(data)) + Notice: This query is influenced by row level security feature +(8 rows) + +DELETE FROM rw_view1 WHERE id = 1 AND snoop(data); +NOTICE: snooped value: Row 1 +EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2'); + QUERY PLAN +----------------------------------------------------------- + Insert on base_tbl + InitPlan 1 (returns $0) + -> Index Only Scan using base_tbl_pkey on base_tbl t + Index Cond: (id = 2) + -> Result + One-Time Filter: ($0 IS NOT TRUE) + + Update on base_tbl + InitPlan 1 (returns $0) + -> Index Only Scan using base_tbl_pkey on base_tbl t + Index Cond: (id = 2) + -> Result + One-Time Filter: $0 + -> Index Scan using base_tbl_pkey on base_tbl + Index Cond: (id = 2) +(15 rows) + +INSERT INTO rw_view1 VALUES (2, 'New row 2'); +SELECT * FROM base_tbl; + id | data | deleted +----+-----------+--------- + 1 | Row 1 | t + 2 | New row 2 | f +(2 rows) + +DROP TABLE base_tbl CASCADE; +NOTICE: drop cascades to view rw_view1 +DROP FUNCTION snoop(anyelement); +-- +-- Test CREATE OR REPLACE VIEW turning a non-updatable view into an +-- auto-updatable view and adding check options in a single step +-- +CREATE TABLE t1 (a int, b text); +CREATE VIEW v1 AS SELECT null::int AS a; +CREATE OR REPLACE VIEW v1 AS SELECT * FROM t1 WHERE a > 0 WITH CHECK OPTION; +INSERT INTO v1 VALUES (1, 'ok'); -- ok +INSERT INTO v1 VALUES (-1, 'invalid'); -- should fail +ERROR: new row violates WITH CHECK OPTION for view "v1" +DETAIL: Failing row contains (-1, invalid). +DROP VIEW v1; +DROP TABLE t1; +-- Test single- and multi-row inserts with table and view defaults. +-- Table defaults should be used, unless overridden by view defaults. +create table base_tab_def (a int, b text default 'Table default', + c text default 'Table default', d text, e text); +create view base_tab_def_view as select * from base_tab_def; +alter view base_tab_def_view alter b set default 'View default'; +alter view base_tab_def_view alter d set default 'View default'; +insert into base_tab_def values (1); +insert into base_tab_def values (2), (3); +insert into base_tab_def values (4, default, default, default, default); +insert into base_tab_def values (5, default, default, default, default), + (6, default, default, default, default); +insert into base_tab_def_view values (11); +insert into base_tab_def_view values (12), (13); +insert into base_tab_def_view values (14, default, default, default, default); +insert into base_tab_def_view values (15, default, default, default, default), + (16, default, default, default, default); +insert into base_tab_def_view values (17), (default); +select * from base_tab_def order by a; + a | b | c | d | e +----+---------------+---------------+--------------+--- + 1 | Table default | Table default | | + 2 | Table default | Table default | | + 3 | Table default | Table default | | + 4 | Table default | Table default | | + 5 | Table default | Table default | | + 6 | Table default | Table default | | + 11 | View default | Table default | View default | + 12 | View default | Table default | View default | + 13 | View default | Table default | View default | + 14 | View default | Table default | View default | + 15 | View default | Table default | View default | + 16 | View default | Table default | View default | + 17 | View default | Table default | View default | + | View default | Table default | View default | +(14 rows) + +-- Adding an INSTEAD OF trigger should cause NULLs to be inserted instead of +-- table defaults, where there are no view defaults. +create function base_tab_def_view_instrig_func() returns trigger +as +$$ +begin + insert into base_tab_def values (new.a, new.b, new.c, new.d, new.e); + return new; +end; +$$ +language plpgsql; +create trigger base_tab_def_view_instrig instead of insert on base_tab_def_view + for each row execute procedure base_tab_def_view_instrig_func(); +truncate base_tab_def; +insert into base_tab_def values (1); +insert into base_tab_def values (2), (3); +insert into base_tab_def values (4, default, default, default, default); +insert into base_tab_def values (5, default, default, default, default), + (6, default, default, default, default); +insert into base_tab_def_view values (11); +insert into base_tab_def_view values (12), (13); +insert into base_tab_def_view values (14, default, default, default, default); +insert into base_tab_def_view values (15, default, default, default, default), + (16, default, default, default, default); +insert into base_tab_def_view values (17), (default); +select * from base_tab_def order by a; + a | b | c | d | e +----+---------------+---------------+--------------+--- + 1 | Table default | Table default | | + 2 | Table default | Table default | | + 3 | Table default | Table default | | + 4 | Table default | Table default | | + 5 | Table default | Table default | | + 6 | Table default | Table default | | + 11 | View default | | View default | + 12 | View default | | View default | + 13 | View default | | View default | + 14 | View default | | View default | + 15 | View default | | View default | + 16 | View default | | View default | + 17 | View default | | View default | + | View default | | View default | +(14 rows) + +-- Using an unconditional DO INSTEAD rule should also cause NULLs to be +-- inserted where there are no view defaults. +drop trigger base_tab_def_view_instrig on base_tab_def_view; +drop function base_tab_def_view_instrig_func; +create rule base_tab_def_view_ins_rule as on insert to base_tab_def_view + do instead insert into base_tab_def values (new.a, new.b, new.c, new.d, new.e); +truncate base_tab_def; +insert into base_tab_def values (1); +insert into base_tab_def values (2), (3); +insert into base_tab_def values (4, default, default, default, default); +insert into base_tab_def values (5, default, default, default, default), + (6, default, default, default, default); +insert into base_tab_def_view values (11); +insert into base_tab_def_view values (12), (13); +insert into base_tab_def_view values (14, default, default, default, default); +insert into base_tab_def_view values (15, default, default, default, default), + (16, default, default, default, default); +insert into base_tab_def_view values (17), (default); +select * from base_tab_def order by a; + a | b | c | d | e +----+---------------+---------------+--------------+--- + 1 | Table default | Table default | | + 2 | Table default | Table default | | + 3 | Table default | Table default | | + 4 | Table default | Table default | | + 5 | Table default | Table default | | + 6 | Table default | Table default | | + 11 | View default | | View default | + 12 | View default | | View default | + 13 | View default | | View default | + 14 | View default | | View default | + 15 | View default | | View default | + 16 | View default | | View default | + 17 | View default | | View default | + | View default | | View default | +(14 rows) + +-- A DO ALSO rule should cause each row to be inserted twice. The first +-- insert should behave the same as an auto-updatable view (using table +-- defaults, unless overridden by view defaults). The second insert should +-- behave the same as a rule-updatable view (inserting NULLs where there are +-- no view defaults). +drop rule base_tab_def_view_ins_rule on base_tab_def_view; +create rule base_tab_def_view_ins_rule as on insert to base_tab_def_view + do also insert into base_tab_def values (new.a, new.b, new.c, new.d, new.e); +truncate base_tab_def; +insert into base_tab_def values (1); +insert into base_tab_def values (2), (3); +insert into base_tab_def values (4, default, default, default, default); +insert into base_tab_def values (5, default, default, default, default), + (6, default, default, default, default); +insert into base_tab_def_view values (11); +insert into base_tab_def_view values (12), (13); +insert into base_tab_def_view values (14, default, default, default, default); +insert into base_tab_def_view values (15, default, default, default, default), + (16, default, default, default, default); +insert into base_tab_def_view values (17), (default); +select * from base_tab_def order by a, c NULLS LAST; + a | b | c | d | e +----+---------------+---------------+--------------+--- + 1 | Table default | Table default | | + 2 | Table default | Table default | | + 3 | Table default | Table default | | + 4 | Table default | Table default | | + 5 | Table default | Table default | | + 6 | Table default | Table default | | + 11 | View default | Table default | View default | + 11 | View default | | View default | + 12 | View default | Table default | View default | + 12 | View default | | View default | + 13 | View default | Table default | View default | + 13 | View default | | View default | + 14 | View default | Table default | View default | + 14 | View default | | View default | + 15 | View default | Table default | View default | + 15 | View default | | View default | + 16 | View default | Table default | View default | + 16 | View default | | View default | + 17 | View default | Table default | View default | + 17 | View default | | View default | + | View default | Table default | View default | + | View default | | View default | +(22 rows) + +drop view base_tab_def_view; +drop table base_tab_def; +-- check that an auto-updatable view on a partitioned table works correctly +create table base_partition_tbl ( + num int, + data1 text +) partition by range(num) ( + partition num1 values less than (10), + partition num2 values less than (20), + partition num3 values less than (30) +); +create view num1_view as select * from base_partition_tbl partition (num1); +insert into num1_view values (15, 'should fail'); +ERROR: inserted partition key does not map to the table partition +DETAIL: N/A. +insert into num1_view values (5, 'success'); +drop table base_partition_tbl cascade; +NOTICE: drop cascades to view num1_view +-- check sublink and subquery +CREATE TABLE base_tbl1(id int PRIMARY KEY, num int); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "base_tbl1_pkey" for table "base_tbl1" +CREATE TABLE base_tbl2(id int PRIMARY KEY, num int); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "base_tbl2_pkey" for table "base_tbl2" +INSERT INTO base_tbl1 VALUES (1, 1), (2, 2); +INSERT INTO base_tbl2 VALUES (1, 3), (2, 4); +create view rw_view1 as select *, (select num from base_tbl2 where base_tbl2.id = base_tbl1.id limit 1) c +from base_tbl1 where exists (select * from base_tbl1 a join base_tbl2 b on a.id = b.id); +insert into rw_view1(id, num) values (3, 3); +create view rw_view2 as select *, (select num from base_tbl2 where base_tbl2.id = d.id limit 1) c +from base_tbl1 d where exists (select * from base_tbl1 a right join base_tbl2 b on a.id = b.id where b.id = d.id) +with local check option; +insert into rw_view2(id, num) values (4, 4); -- fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +DETAIL: Failing row contains (4, 4). +insert into base_tbl2 VALUES (4, 5); +insert into rw_view2(id, num) values (4, 4); +-- merge into unsupport +merge into rw_view1 a using base_tbl2 b on (a.id = b.id) +when matched then + update set a.num = b.num +when not matched then + insert values (b.id, b,num); +ERROR: Target relation type is not supported for MERGE INTO +-- upsert unsupport +INSERT INTO rw_view1 VALUES(5, 5) ON DUPLICATE KEY UPDATE num = 5; +ERROR: INSERT ON DUPLICATE KEY UPDATE is not supported on VIEW. +drop table base_tbl1 cascade; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to view rw_view1 +drop cascades to view rw_view2 +drop table base_tbl2 cascade; +CREATE TABLE tx1 (a integer); +CREATE TABLE tx2 (b integer); +CREATE TABLE tx3 (c integer); +CREATE VIEW vx1 AS SELECT a FROM tx1 WHERE EXISTS(SELECT 1 FROM tx2 JOIN tx3 ON b=c); +INSERT INTO vx1 values (1); +SELECT * FROM tx1; + a +--- + 1 +(1 row) + +SELECT * FROM vx1; + a +--- +(0 rows) + +DROP VIEW vx1; +DROP TABLE tx1; +DROP TABLE tx2; +DROP TABLE tx3; +CREATE TABLE tx1 (a integer); +CREATE TABLE tx2 (b integer); +CREATE TABLE tx3 (c integer); +CREATE VIEW vx1 AS SELECT a FROM tx1 WHERE EXISTS(SELECT 1 FROM tx2 JOIN tx3 ON b=c); +INSERT INTO vx1 VALUES (1); +INSERT INTO vx1 VALUES (1); +SELECT * FROM tx1; + a +--- + 1 + 1 +(2 rows) + +SELECT * FROM vx1; + a +--- +(0 rows) + +DROP VIEW vx1; +DROP TABLE tx1; +DROP TABLE tx2; +DROP TABLE tx3; +CREATE TABLE tx1 (a integer, b integer); +CREATE TABLE tx2 (b integer, c integer); +CREATE TABLE tx3 (c integer, d integer); +ALTER TABLE tx1 DROP COLUMN b; +ALTER TABLE tx2 DROP COLUMN c; +ALTER TABLE tx3 DROP COLUMN d; +CREATE VIEW vx1 AS SELECT a FROM tx1 WHERE EXISTS(SELECT 1 FROM tx2 JOIN tx3 ON b=c); +INSERT INTO vx1 VALUES (1); +INSERT INTO vx1 VALUES (1); +SELECT * FROM tx1; + a +--- + 1 + 1 +(2 rows) + +SELECT * FROM vx1; + a +--- +(0 rows) + +DROP VIEW vx1; +DROP TABLE tx1; +DROP TABLE tx2; +DROP TABLE tx3; +create database updatable_views_db DBCOMPATIBILITY = 'B'; +\c updatable_views_db +create table base_tbl1 (id int, num int); +create view rw_view1 as select * from base_tbl1 where id > 0; +create view rw_view2 as select * from rw_view1 where id < 10; +alter view rw_view2 as select * from rw_view1 where id < 10 with local check option; +insert into rw_view2 values (0); -- ok +insert into rw_view2 values (10); -- fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +DETAIL: Failing row contains (10, null). +alter view rw_view2 as select * from rw_view1 where id < 10 with cascaded check option; +insert into rw_view2 values (0); -- fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +DETAIL: Failing row contains (0, null). +insert into rw_view2 values (10); -- fail +ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +DETAIL: Failing row contains (10, null). +\c regression +drop database updatable_views_db; diff --git a/src/test/regress/sql/multi_delete.sql b/src/test/regress/sql/multi_delete.sql index 82bf844d2..3deef8001 100644 --- a/src/test/regress/sql/multi_delete.sql +++ b/src/test/regress/sql/multi_delete.sql @@ -102,12 +102,6 @@ CREATE MATERIALIZED VIEW mate_multiview2 as select * from t_t_mutil_t2; delete t_t_mutil_t1 a,mate_multiview1 b,mate_multiview2 c ; drop MATERIALIZED VIEW mate_multiview1; drop MATERIALIZED VIEW mate_multiview2; --- view -create view multiview1 as select * from t_t_mutil_t1; -create view multiview2 as select * from t_t_mutil_t2; -delete t_t_mutil_t1 a,multiview1 b,multiview2 c where a.col1 = 4 and b.col2 = 5 and c.col2 =6; --error -drop view multiview1; -drop view multiview2; -- different explain plan explain(verbose) delete/*+nestloop(a b)*/ from t_t_mutil_t1 a,t_t_mutil_t2 b where a.col2=b.col2; explain(verbose) delete/*+hashjoin(a b)*/ from t_t_mutil_t1 a,t_t_mutil_t2 b where a.col2=b.col2; @@ -481,5 +475,31 @@ delete from t_t_mutil_t1,t_t_mutil_t1 using t_t_mutil_t1,t_t_mutil_t1 where t_t_ delete from t_t_mutil_t1 using t_t_mutil_t1,t_t_mutil_t1 where t_t_mutil_t1.col1=1; delete from t_t_mutil_t1 a,t_t_mutil_t1 a where a.col1=1; delete from t_t_mutil_t1 a,t_t_mutil_t1 b where a.col1=1; +-- view +drop table if exists t_t_mutil_t1; +drop table if exists t_t_mutil_t2; +drop table if exists t_t_mutil_t3; +create table t_t_mutil_t1(col1 int,col2 int); +create table t_t_mutil_t2(col1 int,col2 int,col3 int); +create table t_t_mutil_t3(col1 int,col2 int); +insert into t_t_mutil_t1 values(1,1),(1,1); +insert into t_t_mutil_t2 values(1,1),(1,2); +insert into t_t_mutil_t3 values(1,1),(1,3); +create view multiview1 as select * from t_t_mutil_t1; +create view multiview2 as select * from t_t_mutil_t2; +create view multiview3 as select * from t_t_mutil_t3; +delete multiview1 a,multiview2 b,multiview3 c where a.col1 = b.col1 and b.col2 = c.col2; +select * from t_t_mutil_t1; +select * from t_t_mutil_t2; +select * from t_t_mutil_t3; +-- left join +insert into t_t_mutil_t1 values(1,1),(1,1); +insert into t_t_mutil_t2 values(1,1),(1,2); +delete from multiview1,t_t_mutil_t2 using t_t_mutil_t2 left join multiview1 on multiview1.col2=t_t_mutil_t2.col1; +select * from t_t_mutil_t1; +select * from t_t_mutil_t2; +drop view multiview1; +drop view multiview2; +drop view multiview3; \c regression drop database multidelete; diff --git a/src/test/regress/sql/multi_update.sql b/src/test/regress/sql/multi_update.sql index f580ba4f8..f5e492cc6 100644 --- a/src/test/regress/sql/multi_update.sql +++ b/src/test/regress/sql/multi_update.sql @@ -90,12 +90,6 @@ CREATE MATERIALIZED VIEW mate_multiview2 as select * from t_t_mutil_t2; update t_t_mutil_t1 a,mate_multiview1 b,mate_multiview2 c set a.col1 = 4, b.col2 = 5,c.col2 =6; --error drop MATERIALIZED VIEW mate_multiview1; drop MATERIALIZED VIEW mate_multiview2; --- view -create view multiview1 as select * from t_t_mutil_t1; -create view multiview2 as select * from t_t_mutil_t2; -update t_t_mutil_t1 a,multiview1 b,multiview2 c set a.col1 = 4, b.col2 = 5,c.col2 =6; --error -drop view multiview1; -drop view multiview2; -- same relname begin; update t_t_mutil_t1 a,t_t_mutil_t1 b set a.col2=5,b.col2=4 where a.col1=b.col1; @@ -580,5 +574,58 @@ begin; update s_t_mutil_t2,s_t_mutil_t1 set s_t_mutil_t1.col2 = 3, s_t_mutil_t2.col2 = 4; select * from t_t_mutil_t1; rollback; +-- view +create view multiview1 as select * from t_t_mutil_t1; +create view multiview2 as select * from t_t_mutil_t2; +create view multiview3 as select * from t_t_mutil_t3; +update multiview1 a,multiview2 b,multiview3 c set a.col2 = 6, b.col2 = 7,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; +select * from t_t_mutil_t1; +select * from t_t_mutil_t2; +select * from t_t_mutil_t3; +update t_t_mutil_t1 a,multiview2 b,t_t_mutil_t3 c set a.col2 = 4, b.col2 = 5,c.col2 = 6 where a.col1 = b.col1 and a.col1 = c.col1; +select * from t_t_mutil_t1; +select * from t_t_mutil_t2; +select * from t_t_mutil_t3; +-- left join +update multiview1 a left join multiview2 b on a.col1=b.col1 set a.col2=7,b.col2=8; +select * from t_t_mutil_t1; +select * from t_t_mutil_t2; +select * from t_t_mutil_t3; +-- with check option +create or replace view multiview1 as select * from t_t_mutil_t1 where col2 > 3 with local check option; +create or replace view multiview2 as select * from t_t_mutil_t2 where col2 < 10 with local check option; +update multiview1 a,multiview2 b,multiview3 c set a.col2 = 2, b.col2 = 7,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; --error +update multiview1 a,multiview2 b,multiview3 c set a.col2 = 5, b.col2 = 15,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; --error +update multiview1 a,multiview2 b,multiview3 c set a.col2 = 5, b.col2 = 7,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; +select * from t_t_mutil_t1; +select * from t_t_mutil_t2; +select * from t_t_mutil_t3; +-- update same relation +update t_t_mutil_t1 a,multiview2 b,t_t_mutil_t2 c,t_t_mutil_t3 d set a.col2 = 6, b.col2 = 9, c.col3 = 10, d.col2 = 6 where a.col1 = b.col1 and a.col1 = d.col1; +select * from t_t_mutil_t1; +select * from t_t_mutil_t2; +select * from t_t_mutil_t3; +-- view with rules or triggers should fail +create rule multiview1_rule as on insert to multiview1 do instead insert into t_t_mutil_t1 values (new.col1, new.col2); +update multiview1 a,multiview2 b,t_t_mutil_t3 c set a.col2 = 6, b.col2 = 7,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; --error +drop rule multiview1_rule on multiview1; +CREATE OR REPLACE FUNCTION trigger_func_update_multiview1() RETURNS TRIGGER AS +$$ +DECLARE +BEGIN +UPDATE t_t_mutil_t1 SET col2 = NEW.col2 WHERE col1=OLD.col1; +RETURN OLD; +END +$$ LANGUAGE PLPGSQL; +CREATE TRIGGER update_multiview1_trigger +INSTEAD OF UPDATE ON multiview1 +FOR EACH ROW +EXECUTE PROCEDURE trigger_func_update_multiview1(); +update multiview1 a,multiview2 b,t_t_mutil_t3 c set a.col2 = 6, b.col2 = 7,c.col2 =8 where a.col1 = b.col1 and a.col1 = c.col1; --error +drop trigger update_multiview1_trigger on multiview1; +drop function trigger_func_update_multiview1(); +drop view multiview1; +drop view multiview2; +drop view multiview3; \c regression drop database multiupdate; \ No newline at end of file diff --git a/src/test/regress/sql/rule_test.sql b/src/test/regress/sql/rule_test.sql index b45239c7a..ec4509cbd 100644 --- a/src/test/regress/sql/rule_test.sql +++ b/src/test/regress/sql/rule_test.sql @@ -199,7 +199,7 @@ create rule "_RETURN" as on select to ttt1 do instead ( ); -- test -insert into ttt1 values (1, 'hello'); --error +insert into ttt1 values (1, 'hello'); insert into ttt2 values (10, 'world'); select * from ttt1; diff --git a/src/test/regress/sql/single_node_triggers.sql b/src/test/regress/sql/single_node_triggers.sql index a51081986..f7468c5dc 100644 --- a/src/test/regress/sql/single_node_triggers.sql +++ b/src/test/regress/sql/single_node_triggers.sql @@ -630,11 +630,11 @@ DROP TABLE min_updates_test_oids; CREATE VIEW main_view AS SELECT a, b FROM main_table; --- Updates should fail without rules or triggers +-- Updates should succeed without rules or triggers, cause this simple view are auto-updatable. INSERT INTO main_view VALUES (1,2); UPDATE main_view SET b = 20 WHERE a = 50; DELETE FROM main_view WHERE a = 50; --- Should fail even when there are no matching rows +-- Should succeed even when there are no matching rows, cause this simple view are auto-updatable. DELETE FROM main_view WHERE a = 51; -- VIEW trigger function diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql index 84cfba786..d38b0a0a1 100644 --- a/src/test/regress/sql/updatable_views.sql +++ b/src/test/regress/sql/updatable_views.sql @@ -78,4 +78,1173 @@ RESET SESSION AUTHORIZATION; DROP TABLE base_tbl CASCADE; DROP USER regress_view_user1; -DROP USER regress_view_user2; \ No newline at end of file +DROP USER regress_view_user2; + +-- +-- UPDATABLE VIEWS +-- + +-- check that non-updatable views and columns are rejected with useful error messages + +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); + +CREATE VIEW ro_view1 AS SELECT DISTINCT a, b FROM base_tbl; -- DISTINCT not supported +CREATE VIEW ro_view2 AS SELECT a, b FROM base_tbl GROUP BY a, b; -- GROUP BY not supported +CREATE VIEW ro_view3 AS SELECT 1 FROM base_tbl HAVING max(a) > 0; -- HAVING not supported +CREATE VIEW ro_view4 AS SELECT count(*) FROM base_tbl; -- Aggregate functions not supported +CREATE VIEW ro_view5 AS SELECT a, rank() OVER() FROM base_tbl; -- Window functions not supported +CREATE VIEW ro_view6 AS SELECT a, b FROM base_tbl UNION SELECT -a, b FROM base_tbl; -- Set ops not supported +CREATE VIEW ro_view7 AS WITH t AS (SELECT a, b FROM base_tbl) SELECT * FROM t; -- WITH not supported +CREATE VIEW ro_view8 AS SELECT a, b FROM base_tbl ORDER BY a OFFSET 1; -- OFFSET not supported +CREATE VIEW ro_view9 AS SELECT a, b FROM base_tbl ORDER BY a LIMIT 1; -- LIMIT not supported +CREATE VIEW ro_view10 AS SELECT 1 AS a; -- No base relations +CREATE VIEW ro_view11 AS SELECT b1.a, b2.b FROM base_tbl b1, base_tbl b2; -- Multiple base relations +CREATE VIEW ro_view12 AS SELECT * FROM generate_series(1, 10) AS g(a); -- SRF in rangetable +CREATE VIEW ro_view13 AS SELECT a, b FROM (SELECT * FROM base_tbl) AS t; -- Subselect in rangetable +CREATE VIEW rw_view14 AS SELECT ctid, a, b FROM base_tbl; -- System columns may be part of an updatable view +CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view +CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view +CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable +CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable +CREATE SEQUENCE seq; +CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence +CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name; + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name, ordinal_position; + +-- Read-only views +DELETE FROM ro_view1; +DELETE FROM ro_view2; +DELETE FROM ro_view3; +DELETE FROM ro_view4; +DELETE FROM ro_view5; +DELETE FROM ro_view6; +UPDATE ro_view7 SET a=a+1; +UPDATE ro_view8 SET a=a+1; +UPDATE ro_view9 SET a=a+1; +UPDATE ro_view10 SET a=a+1; +UPDATE ro_view11 SET a=a+1; +UPDATE ro_view12 SET a=a+1; +INSERT INTO ro_view13 VALUES (3, 'Row 3'); +-- Partially updatable view +INSERT INTO rw_view14 VALUES (null, 3, 'Row 3'); -- should fail +INSERT INTO rw_view14 (a, b) VALUES (3, 'Row 3'); -- should be OK +UPDATE rw_view14 SET ctid=null WHERE a=3; -- should fail +UPDATE rw_view14 SET b='ROW 3' WHERE a=3; -- should be OK +SELECT * FROM base_tbl; +DELETE FROM rw_view14 WHERE a=3; -- should be OK +-- Partially updatable view +INSERT INTO rw_view15 VALUES (3, 'ROW 3'); -- should fail +INSERT INTO rw_view15 (a) VALUES (3); -- should be OK +ALTER VIEW rw_view15 ALTER COLUMN upper SET DEFAULT 'NOT SET'; +INSERT INTO rw_view15 (a) VALUES (4); -- should fail +UPDATE rw_view15 SET upper='ROW 3' WHERE a=3; -- should fail +UPDATE rw_view15 SET upper=DEFAULT WHERE a=3; -- should fail +UPDATE rw_view15 SET a=4 WHERE a=3; -- should be OK +SELECT * FROM base_tbl; +DELETE FROM rw_view15 WHERE a=4; -- should be OK +-- Partially updatable view +INSERT INTO rw_view16 VALUES (3, 'Row 3', 3); -- should fail +INSERT INTO rw_view16 (a, b) VALUES (3, 'Row 3'); -- should be OK +UPDATE rw_view16 SET a=3, aa=-3 WHERE a=3; -- should fail +UPDATE rw_view16 SET aa=-3 WHERE a=3; -- should be OK +SELECT * FROM base_tbl; +DELETE FROM rw_view16 WHERE a=-3; -- should be OK +-- Read-only views +INSERT INTO ro_view17 VALUES (3, 'ROW 3'); +DELETE FROM ro_view18; +UPDATE ro_view19 SET max_value=1000; +UPDATE ro_view20 SET b=upper(b); + +-- A view with a conditional INSTEAD rule but no unconditional INSTEAD rules +-- or INSTEAD OF triggers should be non-updatable and generate useful error +-- messages with appropriate detail +CREATE RULE rw_view16_ins_rule AS ON INSERT TO rw_view16 + WHERE NEW.a > 0 DO INSTEAD INSERT INTO base_tbl VALUES (NEW.a, NEW.b); +CREATE RULE rw_view16_upd_rule AS ON UPDATE TO rw_view16 + WHERE OLD.a > 0 DO INSTEAD UPDATE base_tbl SET b=NEW.b WHERE a=OLD.a; +CREATE RULE rw_view16_del_rule AS ON DELETE TO rw_view16 + WHERE OLD.a > 0 DO INSTEAD DELETE FROM base_tbl WHERE a=OLD.a; + +INSERT INTO rw_view16 (a, b) VALUES (3, 'Row 3'); -- should fail +UPDATE rw_view16 SET b='ROW 2' WHERE a=2; -- should fail +DELETE FROM rw_view16 WHERE a=2; -- should fail + +DROP TABLE base_tbl CASCADE; +DROP VIEW ro_view10, ro_view12, ro_view18; +DROP SEQUENCE seq CASCADE; + +-- simple updatable view + +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); + +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0; + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name = 'rw_view1'; + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name = 'rw_view1'; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name = 'rw_view1' + ORDER BY ordinal_position; + +INSERT INTO rw_view1 VALUES (3, 'Row 3'); +INSERT INTO rw_view1 (a) VALUES (4); +UPDATE rw_view1 SET a=5 WHERE a=4; +DELETE FROM rw_view1 WHERE b='Row 2'; +SELECT * FROM base_tbl; + +EXPLAIN (costs off) UPDATE rw_view1 SET a=6 WHERE a=5; +EXPLAIN (costs off) DELETE FROM rw_view1 WHERE a=5; + +-- it's still updatable if we add a DO ALSO rule + +CREATE TABLE base_tbl_hist(ts timestamptz default now(), a int, b text); + +CREATE RULE base_tbl_log AS ON INSERT TO rw_view1 DO ALSO + INSERT INTO base_tbl_hist(a,b) VALUES(new.a, new.b); + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name = 'rw_view1'; + +-- Check behavior with DEFAULTs + +INSERT INTO rw_view1 VALUES (9, DEFAULT), (10, DEFAULT); +SELECT a, b FROM base_tbl_hist; + +DROP TABLE base_tbl CASCADE; +DROP TABLE base_tbl_hist; + +-- view on top of view + +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); + +CREATE VIEW rw_view1 AS SELECT b AS bb, a AS aa FROM base_tbl WHERE a>0; +CREATE VIEW rw_view2 AS SELECT aa AS aaa, bb AS bbb FROM rw_view1 WHERE aa<10; + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name = 'rw_view2'; + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name = 'rw_view2'; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name = 'rw_view2' + ORDER BY ordinal_position; + +INSERT INTO rw_view2 VALUES (3, 'Row 3'); +INSERT INTO rw_view2 (aaa) VALUES (4); +SELECT * FROM rw_view2; +UPDATE rw_view2 SET bbb='Row 4' WHERE aaa=4; +DELETE FROM rw_view2 WHERE aaa=2; +SELECT * FROM rw_view2; + +EXPLAIN (costs off) UPDATE rw_view2 SET aaa=5 WHERE aaa=4; +EXPLAIN (costs off) DELETE FROM rw_view2 WHERE aaa=4; + +DROP TABLE base_tbl CASCADE; + +-- view on top of view with rules + +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); + +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0 OFFSET 0; -- not updatable without rules/triggers +CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a<10; + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + +CREATE RULE rw_view1_ins_rule AS ON INSERT TO rw_view1 + DO INSTEAD INSERT INTO base_tbl VALUES (NEW.a, NEW.b) RETURNING *; + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + +CREATE RULE rw_view1_upd_rule AS ON UPDATE TO rw_view1 + DO INSTEAD UPDATE base_tbl SET b=NEW.b WHERE a=OLD.a RETURNING NEW.*; + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + +CREATE RULE rw_view1_del_rule AS ON DELETE TO rw_view1 + DO INSTEAD DELETE FROM base_tbl WHERE a=OLD.a RETURNING OLD.*; + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + +INSERT INTO rw_view2 VALUES (3, 'Row 3') RETURNING *; +UPDATE rw_view2 SET b='Row three' WHERE a=3 RETURNING *; +SELECT * FROM rw_view2; +DELETE FROM rw_view2 WHERE a=3 RETURNING *; +SELECT * FROM rw_view2; + +EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2; +EXPLAIN (costs off) DELETE FROM rw_view2 WHERE a=2; + +DROP TABLE base_tbl CASCADE; + +-- view on top of view with triggers + +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); + +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0 OFFSET 0; -- not updatable without rules/triggers +CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a<10; + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, is_updatable, is_insertable_into, + is_trigger_updatable, is_trigger_deletable, + is_trigger_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + +CREATE FUNCTION rw_view1_trig_fn() +RETURNS trigger AS +$$ +BEGIN + IF TG_OP = 'INSERT' THEN + INSERT INTO base_tbl VALUES (NEW.a, NEW.b); + RETURN NEW; + ELSIF TG_OP = 'UPDATE' THEN + UPDATE base_tbl SET b=NEW.b WHERE a=OLD.a; + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + DELETE FROM base_tbl WHERE a=OLD.a; + RETURN OLD; + END IF; +END; +$$ +LANGUAGE plpgsql; + +CREATE TRIGGER rw_view1_ins_trig INSTEAD OF INSERT ON rw_view1 + FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn(); + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, is_updatable, is_insertable_into, + is_trigger_updatable, is_trigger_deletable, + is_trigger_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + +CREATE TRIGGER rw_view1_upd_trig INSTEAD OF UPDATE ON rw_view1 + FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn(); + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, is_updatable, is_insertable_into, + is_trigger_updatable, is_trigger_deletable, + is_trigger_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + +CREATE TRIGGER rw_view1_del_trig INSTEAD OF DELETE ON rw_view1 + FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn(); + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, is_updatable, is_insertable_into, + is_trigger_updatable, is_trigger_deletable, + is_trigger_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'rw_view%' + ORDER BY table_name, ordinal_position; + +INSERT INTO rw_view2 VALUES (3, 'Row 3') RETURNING *; +UPDATE rw_view2 SET b='Row three' WHERE a=3 RETURNING *; +SELECT * FROM rw_view2; +DELETE FROM rw_view2 WHERE a=3 RETURNING *; +SELECT * FROM rw_view2; + +EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2; +EXPLAIN (costs off) DELETE FROM rw_view2 WHERE a=2; + +DROP TABLE base_tbl CASCADE; +DROP FUNCTION rw_view1_trig_fn(); + +-- update using whole row from view + +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i); + +CREATE VIEW rw_view1 AS SELECT b AS bb, a AS aa FROM base_tbl; + +CREATE FUNCTION rw_view1_aa(x rw_view1) + RETURNS int AS $$ SELECT x.aa $$ LANGUAGE sql; + +UPDATE rw_view1 v SET bb='Updated row 2' WHERE rw_view1_aa(v)=2 + RETURNING rw_view1_aa(v), v.bb; +SELECT * FROM base_tbl; + +EXPLAIN (costs off) +UPDATE rw_view1 v SET bb='Updated row 2' WHERE rw_view1_aa(v)=2 + RETURNING rw_view1_aa(v), v.bb; + +DROP TABLE base_tbl CASCADE; + +-- permissions checks + +CREATE USER view_user1 PASSWORD 'Gauss@123'; +CREATE USER view_user2 PASSWORD 'Gauss@123'; + +SET SESSION AUTHORIZATION view_user1 PASSWORD 'Gauss@123'; +CREATE TABLE base_tbl(a int, b text, c float); +INSERT INTO base_tbl VALUES (1, 'Row 1', 1.0); +CREATE VIEW rw_view1 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl; +INSERT INTO rw_view1 VALUES ('Row 2', 2.0, 2); + +GRANT USAGE ON SCHEMA view_user1 to view_user2; +GRANT SELECT ON base_tbl TO view_user2; +GRANT SELECT ON rw_view1 TO view_user2; +GRANT UPDATE (a,c) ON base_tbl TO view_user2; +GRANT UPDATE (bb,cc) ON rw_view1 TO view_user2; +RESET SESSION AUTHORIZATION; + +SET SESSION AUTHORIZATION view_user2 PASSWORD 'Gauss@123'; +CREATE VIEW rw_view2 AS SELECT b AS bb, c AS cc, a AS aa FROM view_user1.base_tbl; +SELECT * FROM view_user1.base_tbl; -- ok +SELECT * FROM view_user1.rw_view1; -- ok +SELECT * FROM rw_view2; -- ok + +INSERT INTO view_user1.base_tbl VALUES (3, 'Row 3', 3.0); -- not allowed +INSERT INTO view_user1.rw_view1 VALUES ('Row 3', 3.0, 3); -- not allowed +INSERT INTO rw_view2 VALUES ('Row 3', 3.0, 3); -- not allowed + +UPDATE view_user1.base_tbl SET a=a, c=c; -- ok +UPDATE view_user1.base_tbl SET b=b; -- not allowed +UPDATE view_user1.rw_view1 SET bb=bb, cc=cc; -- ok +UPDATE view_user1.rw_view1 SET aa=aa; -- not allowed +UPDATE rw_view2 SET aa=aa, cc=cc; -- ok +UPDATE rw_view2 SET bb=bb; -- not allowed + +DELETE FROM view_user1.base_tbl; -- not allowed +DELETE FROM view_user1.rw_view1; -- not allowed +DELETE FROM rw_view2; -- not allowed +RESET SESSION AUTHORIZATION; + +SET SESSION AUTHORIZATION view_user1 PASSWORD 'Gauss@123'; +GRANT INSERT, DELETE ON base_tbl TO view_user2; +RESET SESSION AUTHORIZATION; + +SET SESSION AUTHORIZATION view_user2 PASSWORD 'Gauss@123'; +INSERT INTO view_user1.base_tbl VALUES (3, 'Row 3', 3.0); -- ok +INSERT INTO view_user1.rw_view1 VALUES ('Row 4', 4.0, 4); -- not allowed +INSERT INTO rw_view2 VALUES ('Row 4', 4.0, 4); -- ok +DELETE FROM view_user1.base_tbl WHERE a=1; -- ok +DELETE FROM view_user1.rw_view1 WHERE aa=2; -- not allowed +DELETE FROM rw_view2 WHERE aa=2; -- ok +SELECT * FROM view_user1.base_tbl; +RESET SESSION AUTHORIZATION; + +SET SESSION AUTHORIZATION view_user1 PASSWORD 'Gauss@123'; +REVOKE INSERT, DELETE ON base_tbl FROM view_user2; +GRANT INSERT, DELETE ON rw_view1 TO view_user2; +RESET SESSION AUTHORIZATION; + +SET SESSION AUTHORIZATION view_user2 PASSWORD 'Gauss@123'; +INSERT INTO view_user1.base_tbl VALUES (5, 'Row 5', 5.0); -- not allowed +INSERT INTO view_user1.rw_view1 VALUES ('Row 5', 5.0, 5); -- ok +INSERT INTO rw_view2 VALUES ('Row 6', 6.0, 6); -- not allowed +DELETE FROM view_user1.base_tbl WHERE a=3; -- not allowed +DELETE FROM view_user1.rw_view1 WHERE aa=3; -- ok +DELETE FROM rw_view2 WHERE aa=4; -- not allowed +SELECT * FROM view_user1.base_tbl; +RESET SESSION AUTHORIZATION; + +DROP TABLE view_user1.base_tbl CASCADE; + +DROP USER view_user1; +DROP USER view_user2; + +-- column defaults + +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified', c serial); +INSERT INTO base_tbl VALUES (1, 'Row 1'); +INSERT INTO base_tbl VALUES (2, 'Row 2'); +INSERT INTO base_tbl VALUES (3); + +CREATE VIEW rw_view1 AS SELECT a AS aa, b AS bb FROM base_tbl; +ALTER VIEW rw_view1 ALTER COLUMN bb SET DEFAULT 'View default'; + +INSERT INTO rw_view1 VALUES (4, 'Row 4'); +INSERT INTO rw_view1 (aa) VALUES (5); + +SELECT * FROM base_tbl; + +DROP TABLE base_tbl CASCADE; + +-- Table having triggers + +CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified'); +INSERT INTO base_tbl VALUES (1, 'Row 1'); +INSERT INTO base_tbl VALUES (2, 'Row 2'); + +CREATE FUNCTION rw_view1_trig_fn() +RETURNS trigger AS +$$ +BEGIN + IF TG_OP = 'INSERT' THEN + UPDATE base_tbl SET b=NEW.b WHERE a=1; + RETURN NULL; + END IF; + RETURN NULL; +END; +$$ +LANGUAGE plpgsql; + +CREATE TRIGGER rw_view1_ins_trig AFTER INSERT ON base_tbl + FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn(); + +CREATE VIEW rw_view1 AS SELECT a AS aa, b AS bb FROM base_tbl; + +INSERT INTO rw_view1 VALUES (3, 'Row 3'); +select * from base_tbl; + +DROP VIEW rw_view1; +DROP TRIGGER rw_view1_ins_trig on base_tbl; +DROP FUNCTION rw_view1_trig_fn(); +DROP TABLE base_tbl; + +-- view with ORDER BY + +CREATE TABLE base_tbl (a int, b int); +INSERT INTO base_tbl VALUES (1,2), (4,5), (3,-3); + +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl ORDER BY a+b; + +SELECT * FROM rw_view1; + +INSERT INTO rw_view1 VALUES (7,-8); +SELECT * FROM rw_view1; + +EXPLAIN (verbose, costs off) UPDATE rw_view1 SET b = b + 1 RETURNING *; +UPDATE rw_view1 SET b = b + 1 RETURNING *; +SELECT * FROM rw_view1; + +DROP TABLE base_tbl CASCADE; + +-- multiple array-column updates + +CREATE TABLE base_tbl (a int, arr int[]); +INSERT INTO base_tbl VALUES (1,ARRAY[2]), (3,ARRAY[4]); + +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl; + +UPDATE rw_view1 SET arr[1] = 42, arr[2] = 77 WHERE a = 3; + +SELECT * FROM rw_view1; + +DROP TABLE base_tbl CASCADE; + +-- views with updatable and non-updatable columns + +CREATE TABLE base_tbl(a float); +INSERT INTO base_tbl SELECT i/10.0 FROM generate_series(1,10) g(i); + +CREATE VIEW rw_view1 AS + SELECT ctid, sin(a) s, a, cos(a) c + FROM base_tbl + WHERE a != 0 + ORDER BY abs(a); + +INSERT INTO rw_view1 VALUES (null, null, 1.1, null); -- should fail +INSERT INTO rw_view1 (s, c, a) VALUES (null, null, 1.1); -- should fail +INSERT INTO rw_view1 (a) VALUES (1.1) RETURNING a, s, c; -- OK +UPDATE rw_view1 SET s = s WHERE a = 1.1; -- should fail +UPDATE rw_view1 SET a = 1.05 WHERE a = 1.1 RETURNING s; -- OK +DELETE FROM rw_view1 WHERE a = 1.05; -- OK + +CREATE VIEW rw_view2 AS + SELECT s, c, s/c t, a base_a, ctid + FROM rw_view1; + +INSERT INTO rw_view2 VALUES (null, null, null, 1.1, null); -- should fail +INSERT INTO rw_view2(s, c, base_a) VALUES (null, null, 1.1); -- should fail +INSERT INTO rw_view2(base_a) VALUES (1.1) RETURNING t; -- OK +UPDATE rw_view2 SET s = s WHERE base_a = 1.1; -- should fail +UPDATE rw_view2 SET t = t WHERE base_a = 1.1; -- should fail +UPDATE rw_view2 SET base_a = 1.05 WHERE base_a = 1.1; -- OK +DELETE FROM rw_view2 WHERE base_a = 1.05 RETURNING base_a, s, c, t; -- OK + +CREATE VIEW rw_view3 AS + SELECT s, c, s/c t, ctid + FROM rw_view1; + +INSERT INTO rw_view3 VALUES (null, null, null, null); -- should fail +INSERT INTO rw_view3(s) VALUES (null); -- should fail +UPDATE rw_view3 SET s = s; -- should fail +DELETE FROM rw_view3 WHERE s = sin(0.1); -- should be OK +SELECT * FROM base_tbl ORDER BY a; + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name; + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE E'r_\\_view%' + ORDER BY table_name, ordinal_position; + +SELECT events & 4 != 0 AS upd, + events & 8 != 0 AS ins, + events & 16 != 0 AS del + FROM pg_catalog.pg_relation_is_updatable('rw_view3'::regclass, false) t(events); + +DROP TABLE base_tbl CASCADE; + +-- view on table with GENERATED columns + +CREATE TABLE base_tbl (id int, idplus1 int GENERATED ALWAYS AS (id + 1) STORED); +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl; + +INSERT INTO base_tbl (id) VALUES (1); +INSERT INTO rw_view1 (id) VALUES (2); +INSERT INTO base_tbl (id, idplus1) VALUES (3, DEFAULT); +INSERT INTO rw_view1 (id, idplus1) VALUES (4, DEFAULT); +INSERT INTO base_tbl (id, idplus1) VALUES (5, 6); -- error +INSERT INTO rw_view1 (id, idplus1) VALUES (6, 7); -- error + +SELECT * FROM base_tbl; + +UPDATE base_tbl SET id = 2000 WHERE id = 2; +UPDATE rw_view1 SET id = 3000 WHERE id = 3; + +SELECT * FROM base_tbl; + +DROP TABLE base_tbl CASCADE; + +-- simple WITH CHECK OPTION + +CREATE TABLE base_tbl (a int, b int DEFAULT 10); +INSERT INTO base_tbl VALUES (1,2), (2,3), (1,-1); + +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a < b + WITH LOCAL CHECK OPTION; +\d+ rw_view1 +SELECT * FROM information_schema.views WHERE table_name = 'rw_view1'; + +INSERT INTO rw_view1 VALUES(3,4); -- ok +INSERT INTO rw_view1 VALUES(4,3); -- should fail +INSERT INTO rw_view1 VALUES(5,null); -- should fail +UPDATE rw_view1 SET b = 5 WHERE a = 3; -- ok +UPDATE rw_view1 SET b = -5 WHERE a = 3; -- should fail +INSERT INTO rw_view1(a) VALUES (9); -- ok +INSERT INTO rw_view1(a) VALUES (10); -- should fail +SELECT * FROM base_tbl; + +DROP TABLE base_tbl CASCADE; + +-- WITH LOCAL/CASCADED CHECK OPTION + +CREATE TABLE base_tbl (a int); + +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a > 0; +CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a < 10 + WITH CHECK OPTION; -- implicitly cascaded +\d+ rw_view2 +SELECT * FROM information_schema.views WHERE table_name = 'rw_view2'; + +INSERT INTO rw_view2 VALUES (-5); -- should fail +INSERT INTO rw_view2 VALUES (5); -- ok +INSERT INTO rw_view2 VALUES (15); -- should fail +SELECT * FROM base_tbl; + +UPDATE rw_view2 SET a = a - 10; -- should fail +UPDATE rw_view2 SET a = a + 10; -- should fail + +CREATE OR REPLACE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a < 10 + WITH LOCAL CHECK OPTION; +\d+ rw_view2 +SELECT * FROM information_schema.views WHERE table_name = 'rw_view2'; + +INSERT INTO rw_view2 VALUES (-10); -- ok, but not in view +INSERT INTO rw_view2 VALUES (20); -- should fail +SELECT * FROM base_tbl; + +ALTER VIEW rw_view1 SET (check_option=here); -- invalid +ALTER VIEW rw_view1 SET (check_option=local); + +INSERT INTO rw_view2 VALUES (-20); -- should fail +INSERT INTO rw_view2 VALUES (30); -- should fail + +ALTER VIEW rw_view2 RESET (check_option); +\d+ rw_view2 +SELECT * FROM information_schema.views WHERE table_name = 'rw_view2'; +INSERT INTO rw_view2 VALUES (30); -- ok, but not in view +SELECT * FROM base_tbl; + +DROP TABLE base_tbl CASCADE; + +-- WITH CHECK OPTION with no local view qual + +CREATE TABLE base_tbl (a int); + +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WITH CHECK OPTION; +CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a > 0; +CREATE VIEW rw_view3 AS SELECT * FROM rw_view2 WITH CHECK OPTION; +SELECT * FROM information_schema.views WHERE table_name LIKE E'rw\_view_' ORDER BY table_name; + +INSERT INTO rw_view1 VALUES (-1); -- ok +INSERT INTO rw_view1 VALUES (1); -- ok +INSERT INTO rw_view2 VALUES (-2); -- ok, but not in view +INSERT INTO rw_view2 VALUES (2); -- ok +INSERT INTO rw_view3 VALUES (-3); -- should fail +INSERT INTO rw_view3 VALUES (3); -- ok + +DROP TABLE base_tbl CASCADE; + +-- WITH CHECK OPTION with subquery + +CREATE TABLE base_tbl (a int); +CREATE TABLE ref_tbl (a int PRIMARY KEY); +INSERT INTO ref_tbl SELECT * FROM generate_series(1,10); + +CREATE VIEW rw_view1 AS + SELECT * FROM base_tbl b + WHERE EXISTS(SELECT 1 FROM ref_tbl r WHERE r.a = b.a) + WITH CHECK OPTION; + +INSERT INTO rw_view1 VALUES (5); -- ok +INSERT INTO rw_view1 VALUES (15); -- should fail + +UPDATE rw_view1 SET a = a + 5; -- ok +UPDATE rw_view1 SET a = a + 5; -- should fail + +EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (5); +EXPLAIN (costs off) UPDATE rw_view1 SET a = a + 5; + +DROP TABLE base_tbl, ref_tbl CASCADE; + +-- WITH CHECK OPTION with BEFORE trigger on base table + +CREATE TABLE base_tbl (a int, b int); + +CREATE FUNCTION base_tbl_trig_fn() +RETURNS trigger AS +$$ +BEGIN + NEW.b := 10; + RETURN NEW; +END; +$$ +LANGUAGE plpgsql; + +CREATE TRIGGER base_tbl_trig BEFORE INSERT OR UPDATE ON base_tbl + FOR EACH ROW EXECUTE PROCEDURE base_tbl_trig_fn(); + +CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a < b WITH CHECK OPTION; + +INSERT INTO rw_view1 VALUES (5,0); -- ok +INSERT INTO rw_view1 VALUES (15, 20); -- should fail +UPDATE rw_view1 SET a = 20, b = 30; -- should fail + +DROP TABLE base_tbl CASCADE; +DROP FUNCTION base_tbl_trig_fn(); + +-- WITH LOCAL CHECK OPTION with INSTEAD OF trigger on base view + +CREATE TABLE base_tbl (a int, b int); + +CREATE VIEW rw_view1 AS SELECT a FROM base_tbl WHERE a < b; + +CREATE FUNCTION rw_view1_trig_fn() +RETURNS trigger AS +$$ +BEGIN + IF TG_OP = 'INSERT' THEN + INSERT INTO base_tbl VALUES (NEW.a, 10); + RETURN NEW; + ELSIF TG_OP = 'UPDATE' THEN + UPDATE base_tbl SET a=NEW.a WHERE a=OLD.a; + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + DELETE FROM base_tbl WHERE a=OLD.a; + RETURN OLD; + END IF; +END; +$$ +LANGUAGE plpgsql; + +CREATE TRIGGER rw_view1_trig + INSTEAD OF INSERT OR UPDATE OR DELETE ON rw_view1 + FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn(); + +CREATE VIEW rw_view2 AS + SELECT * FROM rw_view1 WHERE a > 0 WITH LOCAL CHECK OPTION; + +INSERT INTO rw_view2 VALUES (-5); -- should fail +INSERT INTO rw_view2 VALUES (5); -- ok +INSERT INTO rw_view2 VALUES (50); -- ok, but not in view +UPDATE rw_view2 SET a = a - 10; -- should fail +SELECT * FROM base_tbl; + +-- Check option won't cascade down to base view with INSTEAD OF triggers + +ALTER VIEW rw_view2 SET (check_option=cascaded); +INSERT INTO rw_view2 VALUES (100); -- ok, but not in view (doesn't fail rw_view1's check) +UPDATE rw_view2 SET a = 200 WHERE a = 5; -- ok, but not in view (doesn't fail rw_view1's check) +SELECT * FROM base_tbl; + +-- Neither local nor cascaded check options work with INSTEAD rules + +DROP TRIGGER rw_view1_trig ON rw_view1; +CREATE RULE rw_view1_ins_rule AS ON INSERT TO rw_view1 + DO INSTEAD INSERT INTO base_tbl VALUES (NEW.a, 10); +CREATE RULE rw_view1_upd_rule AS ON UPDATE TO rw_view1 + DO INSTEAD UPDATE base_tbl SET a=NEW.a WHERE a=OLD.a; +INSERT INTO rw_view2 VALUES (-10); -- ok, but not in view (doesn't fail rw_view2's check) +INSERT INTO rw_view2 VALUES (5); -- ok +INSERT INTO rw_view2 VALUES (20); -- ok, but not in view (doesn't fail rw_view1's check) +UPDATE rw_view2 SET a = 30 WHERE a = 5; -- ok, but not in view (doesn't fail rw_view1's check) +INSERT INTO rw_view2 VALUES (5); -- ok +UPDATE rw_view2 SET a = -5 WHERE a = 5; -- ok, but not in view (doesn't fail rw_view2's check) +SELECT * FROM base_tbl; + +DROP TABLE base_tbl CASCADE; +DROP FUNCTION rw_view1_trig_fn(); + +CREATE TABLE base_tbl (a int); +CREATE VIEW rw_view1 AS SELECT a,10 AS b FROM base_tbl; +CREATE RULE rw_view1_ins_rule AS ON INSERT TO rw_view1 + DO INSTEAD INSERT INTO base_tbl VALUES (NEW.a); +CREATE VIEW rw_view2 AS + SELECT * FROM rw_view1 WHERE a > b WITH LOCAL CHECK OPTION; +INSERT INTO rw_view2 VALUES (2,3); -- ok, but not in view (doesn't fail rw_view2's check) +DROP TABLE base_tbl CASCADE; + +-- security barrier view + +CREATE TABLE base_tbl (person text, visibility text); +INSERT INTO base_tbl VALUES ('Tom', 'public'), + ('Dick', 'private'), + ('Harry', 'public'); + +CREATE VIEW rw_view1 AS + SELECT person FROM base_tbl WHERE visibility = 'public'; + +CREATE FUNCTION snoop(anyelement) +RETURNS boolean AS +$$ +BEGIN + RAISE NOTICE 'snooped value: %', $1; + RETURN true; +END; +$$ +LANGUAGE plpgsql COST 0.000001; + +SELECT * FROM rw_view1 WHERE snoop(person); +UPDATE rw_view1 SET person=person WHERE snoop(person); +DELETE FROM rw_view1 WHERE NOT snoop(person); + +ALTER VIEW rw_view1 SET (security_barrier = true); + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name = 'rw_view1'; + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name = 'rw_view1'; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name = 'rw_view1' + ORDER BY ordinal_position; + +SELECT * FROM rw_view1 WHERE snoop(person); +UPDATE rw_view1 SET person=person WHERE snoop(person); +DELETE FROM rw_view1 WHERE NOT snoop(person); + +EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person); +EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person); +EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person); + +-- security barrier view on top of security barrier view + +CREATE VIEW rw_view2 WITH (security_barrier = true) AS + SELECT * FROM rw_view1 WHERE snoop(person); + +SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name = 'rw_view2'; + +SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name = 'rw_view2'; + +SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name = 'rw_view2' + ORDER BY ordinal_position; + +SELECT * FROM rw_view2 WHERE snoop(person); +UPDATE rw_view2 SET person=person WHERE snoop(person); +DELETE FROM rw_view2 WHERE NOT snoop(person); + +EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person); +EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person); +EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person); + +DROP TABLE base_tbl CASCADE; + +-- security barrier view on top of table with rules + +CREATE TABLE base_tbl(id int PRIMARY KEY, data text, deleted boolean); +INSERT INTO base_tbl VALUES (1, 'Row 1', false), (2, 'Row 2', true); + +CREATE RULE base_tbl_ins_rule AS ON INSERT TO base_tbl + WHERE EXISTS (SELECT 1 FROM base_tbl t WHERE t.id = new.id) + DO INSTEAD + UPDATE base_tbl SET data = new.data, deleted = false WHERE id = new.id; + +CREATE RULE base_tbl_del_rule AS ON DELETE TO base_tbl + DO INSTEAD + UPDATE base_tbl SET deleted = true WHERE id = old.id; + +CREATE VIEW rw_view1 WITH (security_barrier=true) AS + SELECT id, data FROM base_tbl WHERE NOT deleted; + +SELECT * FROM rw_view1; + +EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data); +DELETE FROM rw_view1 WHERE id = 1 AND snoop(data); + +EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2'); +INSERT INTO rw_view1 VALUES (2, 'New row 2'); + +SELECT * FROM base_tbl; + +DROP TABLE base_tbl CASCADE; + +DROP FUNCTION snoop(anyelement); + +-- +-- Test CREATE OR REPLACE VIEW turning a non-updatable view into an +-- auto-updatable view and adding check options in a single step +-- +CREATE TABLE t1 (a int, b text); +CREATE VIEW v1 AS SELECT null::int AS a; +CREATE OR REPLACE VIEW v1 AS SELECT * FROM t1 WHERE a > 0 WITH CHECK OPTION; + +INSERT INTO v1 VALUES (1, 'ok'); -- ok +INSERT INTO v1 VALUES (-1, 'invalid'); -- should fail + +DROP VIEW v1; +DROP TABLE t1; + +-- Test single- and multi-row inserts with table and view defaults. +-- Table defaults should be used, unless overridden by view defaults. +create table base_tab_def (a int, b text default 'Table default', + c text default 'Table default', d text, e text); +create view base_tab_def_view as select * from base_tab_def; +alter view base_tab_def_view alter b set default 'View default'; +alter view base_tab_def_view alter d set default 'View default'; +insert into base_tab_def values (1); +insert into base_tab_def values (2), (3); +insert into base_tab_def values (4, default, default, default, default); +insert into base_tab_def values (5, default, default, default, default), + (6, default, default, default, default); +insert into base_tab_def_view values (11); +insert into base_tab_def_view values (12), (13); +insert into base_tab_def_view values (14, default, default, default, default); +insert into base_tab_def_view values (15, default, default, default, default), + (16, default, default, default, default); +insert into base_tab_def_view values (17), (default); +select * from base_tab_def order by a; + +-- Adding an INSTEAD OF trigger should cause NULLs to be inserted instead of +-- table defaults, where there are no view defaults. +create function base_tab_def_view_instrig_func() returns trigger +as +$$ +begin + insert into base_tab_def values (new.a, new.b, new.c, new.d, new.e); + return new; +end; +$$ +language plpgsql; +create trigger base_tab_def_view_instrig instead of insert on base_tab_def_view + for each row execute procedure base_tab_def_view_instrig_func(); +truncate base_tab_def; +insert into base_tab_def values (1); +insert into base_tab_def values (2), (3); +insert into base_tab_def values (4, default, default, default, default); +insert into base_tab_def values (5, default, default, default, default), + (6, default, default, default, default); +insert into base_tab_def_view values (11); +insert into base_tab_def_view values (12), (13); +insert into base_tab_def_view values (14, default, default, default, default); +insert into base_tab_def_view values (15, default, default, default, default), + (16, default, default, default, default); +insert into base_tab_def_view values (17), (default); +select * from base_tab_def order by a; + +-- Using an unconditional DO INSTEAD rule should also cause NULLs to be +-- inserted where there are no view defaults. +drop trigger base_tab_def_view_instrig on base_tab_def_view; +drop function base_tab_def_view_instrig_func; +create rule base_tab_def_view_ins_rule as on insert to base_tab_def_view + do instead insert into base_tab_def values (new.a, new.b, new.c, new.d, new.e); +truncate base_tab_def; +insert into base_tab_def values (1); +insert into base_tab_def values (2), (3); +insert into base_tab_def values (4, default, default, default, default); +insert into base_tab_def values (5, default, default, default, default), + (6, default, default, default, default); +insert into base_tab_def_view values (11); +insert into base_tab_def_view values (12), (13); +insert into base_tab_def_view values (14, default, default, default, default); +insert into base_tab_def_view values (15, default, default, default, default), + (16, default, default, default, default); +insert into base_tab_def_view values (17), (default); +select * from base_tab_def order by a; + +-- A DO ALSO rule should cause each row to be inserted twice. The first +-- insert should behave the same as an auto-updatable view (using table +-- defaults, unless overridden by view defaults). The second insert should +-- behave the same as a rule-updatable view (inserting NULLs where there are +-- no view defaults). +drop rule base_tab_def_view_ins_rule on base_tab_def_view; +create rule base_tab_def_view_ins_rule as on insert to base_tab_def_view + do also insert into base_tab_def values (new.a, new.b, new.c, new.d, new.e); +truncate base_tab_def; +insert into base_tab_def values (1); +insert into base_tab_def values (2), (3); +insert into base_tab_def values (4, default, default, default, default); +insert into base_tab_def values (5, default, default, default, default), + (6, default, default, default, default); +insert into base_tab_def_view values (11); +insert into base_tab_def_view values (12), (13); +insert into base_tab_def_view values (14, default, default, default, default); +insert into base_tab_def_view values (15, default, default, default, default), + (16, default, default, default, default); +insert into base_tab_def_view values (17), (default); +select * from base_tab_def order by a, c NULLS LAST; + +drop view base_tab_def_view; +drop table base_tab_def; + +-- check that an auto-updatable view on a partitioned table works correctly +create table base_partition_tbl ( + num int, + data1 text +) partition by range(num) ( + partition num1 values less than (10), + partition num2 values less than (20), + partition num3 values less than (30) +); +create view num1_view as select * from base_partition_tbl partition (num1); +insert into num1_view values (15, 'should fail'); +insert into num1_view values (5, 'success'); + +drop table base_partition_tbl cascade; + +-- check sublink and subquery +CREATE TABLE base_tbl1(id int PRIMARY KEY, num int); +CREATE TABLE base_tbl2(id int PRIMARY KEY, num int); +INSERT INTO base_tbl1 VALUES (1, 1), (2, 2); +INSERT INTO base_tbl2 VALUES (1, 3), (2, 4); +create view rw_view1 as select *, (select num from base_tbl2 where base_tbl2.id = base_tbl1.id limit 1) c +from base_tbl1 where exists (select * from base_tbl1 a join base_tbl2 b on a.id = b.id); +insert into rw_view1(id, num) values (3, 3); + +create view rw_view2 as select *, (select num from base_tbl2 where base_tbl2.id = d.id limit 1) c +from base_tbl1 d where exists (select * from base_tbl1 a right join base_tbl2 b on a.id = b.id where b.id = d.id) +with local check option; + +insert into rw_view2(id, num) values (4, 4); -- fail +insert into base_tbl2 VALUES (4, 5); +insert into rw_view2(id, num) values (4, 4); + +-- merge into unsupport +merge into rw_view1 a using base_tbl2 b on (a.id = b.id) +when matched then + update set a.num = b.num +when not matched then + insert values (b.id, b,num); + +-- upsert unsupport +INSERT INTO rw_view1 VALUES(5, 5) ON DUPLICATE KEY UPDATE num = 5; + +drop table base_tbl1 cascade; +drop table base_tbl2 cascade; + +CREATE TABLE tx1 (a integer); +CREATE TABLE tx2 (b integer); +CREATE TABLE tx3 (c integer); +CREATE VIEW vx1 AS SELECT a FROM tx1 WHERE EXISTS(SELECT 1 FROM tx2 JOIN tx3 ON b=c); +INSERT INTO vx1 values (1); +SELECT * FROM tx1; +SELECT * FROM vx1; + +DROP VIEW vx1; +DROP TABLE tx1; +DROP TABLE tx2; +DROP TABLE tx3; + +CREATE TABLE tx1 (a integer); +CREATE TABLE tx2 (b integer); +CREATE TABLE tx3 (c integer); +CREATE VIEW vx1 AS SELECT a FROM tx1 WHERE EXISTS(SELECT 1 FROM tx2 JOIN tx3 ON b=c); +INSERT INTO vx1 VALUES (1); +INSERT INTO vx1 VALUES (1); +SELECT * FROM tx1; +SELECT * FROM vx1; + +DROP VIEW vx1; +DROP TABLE tx1; +DROP TABLE tx2; +DROP TABLE tx3; + +CREATE TABLE tx1 (a integer, b integer); +CREATE TABLE tx2 (b integer, c integer); +CREATE TABLE tx3 (c integer, d integer); +ALTER TABLE tx1 DROP COLUMN b; +ALTER TABLE tx2 DROP COLUMN c; +ALTER TABLE tx3 DROP COLUMN d; +CREATE VIEW vx1 AS SELECT a FROM tx1 WHERE EXISTS(SELECT 1 FROM tx2 JOIN tx3 ON b=c); +INSERT INTO vx1 VALUES (1); +INSERT INTO vx1 VALUES (1); +SELECT * FROM tx1; +SELECT * FROM vx1; + +DROP VIEW vx1; +DROP TABLE tx1; +DROP TABLE tx2; +DROP TABLE tx3; + +create database updatable_views_db DBCOMPATIBILITY = 'B'; +\c updatable_views_db + +create table base_tbl1 (id int, num int); +create view rw_view1 as select * from base_tbl1 where id > 0; +create view rw_view2 as select * from rw_view1 where id < 10; + +alter view rw_view2 as select * from rw_view1 where id < 10 with local check option; + +insert into rw_view2 values (0); -- ok +insert into rw_view2 values (10); -- fail + +alter view rw_view2 as select * from rw_view1 where id < 10 with cascaded check option; + +insert into rw_view2 values (0); -- fail +insert into rw_view2 values (10); -- fail + +\c regression +drop database updatable_views_db; \ No newline at end of file diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a08f9357e..e7729e1de 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1439,7 +1439,7 @@ ReorderBufferTupleCidKey ReorderBufferTXN ReorderBufferTXNByIdEnt ResTarget -ResolveNew_context +ReplaceVarsFromTargetList_context ResourceOwner ResourceReleaseCallback ResourceReleaseCallbackItem