diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml new file mode 100644 index ced3115..c991057 *** a/doc/src/sgml/ref/create_view.sgml --- b/doc/src/sgml/ref/create_view.sgml *************** PostgreSQL documentation *** 24,29 **** --- 24,30 ---- CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] [ RECURSIVE ] VIEW name [ ( column_name [, ...] ) ] [ WITH ( view_option_name [= view_option_value] [, ... ] ) ] AS query + [ WITH [ CASCADED | LOCAL ] CHECK OPTION ] *************** CREATE VIEW name AS WITH *** 120,129 **** WITH ( view_option_name [= view_option_value] [, ... ] ) ! This clause specifies optional parameters for a view; currently, the ! only supported parameter name is security_barrier, ! which should be enabled when a view is intended to provide row-level ! security. See for full details. --- 121,154 ---- WITH ( view_option_name [= view_option_value] [, ... ] ) ! This clause specifies optional parameters for a view; the following ! parameters are supported: ! ! ! ! security_barrier(boolean) ! ! ! This should be used if the view is intended to provide row-level ! security. See for full details. ! ! ! ! ! ! check_option(text) ! ! ! This is parameter may be either local or ! cascaded, and is equivalent to specifying ! WITH [ CASCADED | LOCAL ] CHECK OPTION. This allows an ! existing view's check option to be modified using rather than using CREATE OR REPLACE ! VIEW. ! ! ! ! *************** CREATE VIEW name AS WITH *** 138,143 **** --- 163,239 ---- + + + WITH [ CASCADED | LOCAL ] CHECK OPTION + + + + CHECK OPTION + + + WITH CHECK OPTION + + This option controls the behavior of automatically updatable views. When + this option is specified, INSERT and UPDATE + commands on the view will be checked to ensure that new rows satisfy the + view-defining condition (that is, the new rows are checked to ensure that + they are visible through the view). If they are not, the update will be + rejected. If the CHECK OPTION is not specified, + INSERT and UPDATE commands on the view are + allowed to create rows that are not visible through the view. The + following check options are supported: + + + + LOCAL + + + New rows are only checked against the conditions defined directly in + the view itself. Any conditions defined on underlying base views are + not checked (unless they also specify the CHECK OPTION). + + + + + + CASCADED + + + New rows are checked against the conditions of the view and all + underlying base views. If the CHECK OPTION is specified, + and neither LOCAL nor CASCADED is specified, + then CASCADED is assumed. + + + + + + + + The CHECK OPTION may not be used with RECURSIVE + views. + + + + Note that the CHECK OPTION is only supported on views that + are automatically updatable, and do not have INSTEAD OF + triggers or INSTEAD rules. If an automatically updatable + view is defined on top of a base view that has INSTEAD OF + triggers, then the LOCAL CHECK OPTION may be used to check + the conditions on the automatically updatable view, but the conditions + on the base view with INSTEAD OF triggers will not be + checked (a cascaded check option will not cascade down to a + trigger-updatable view, and any check options defined directly on a + trigger-updatable view will be ignored). If the view or any of its base + relations has an INSTEAD rule that causes the + INSERT or UPDATE command to be rewritten, then + all check options will be ignored in the rewritten query, including any + checks from automatically updatable views defined on top of the relation + with the INSTEAD rule. + + + *************** CREATE VIEW vista AS SELECT text 'Hello *** 256,262 **** condition, and thus is no longer visible through the view. Similarly, an INSERT command can potentially insert base-relation rows that do not satisfy the WHERE condition and thus are not ! visible through the view. --- 352,360 ---- condition, and thus is no longer visible through the view. Similarly, an INSERT command can potentially insert base-relation rows that do not satisfy the WHERE condition and thus are not ! visible through the view. The CHECK OPTION may be used to ! prevent INSERT and UPDATE commands from creating ! such rows that are not visible through the view. *************** UNION ALL *** 314,376 **** Compatibility - The SQL standard specifies some additional capabilities for the - CREATE VIEW statement: - - CREATE VIEW name [ ( column_name [, ...] ) ] - AS query - [ WITH [ CASCADED | LOCAL ] CHECK OPTION ] - - - - - The optional clauses for the full SQL command are: - - - - CHECK OPTION - - - This option controls the behavior of automatically updatable views. - When given, INSERT and UPDATE commands on - the view will be checked to ensure new rows satisfy the - view-defining condition (that is, the new rows would be visible - through the view). If they do not, the update will be rejected. - Without CHECK OPTION, INSERT and - UPDATE commands on the view are allowed to create rows - that are not visible through the view. (The latter behavior is the - only one currently provided by PostgreSQL.) - - - - - - LOCAL - - - Check for integrity on this view. - - - - - - CASCADED - - - Check for integrity on this view and on any dependent - view. CASCADED is assumed if neither - CASCADED nor LOCAL is specified. - - - - - - - CREATE OR REPLACE VIEW is a PostgreSQL language extension. So is the concept of a temporary view. ! The WITH clause is an extension as well. --- 412,421 ---- Compatibility CREATE OR REPLACE VIEW is a PostgreSQL language extension. So is the concept of a temporary view. ! The WITH ( ... ) clause is an extension as well. diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c new file mode 100644 index c439702..b5fd30a *** a/src/backend/access/common/reloptions.c --- b/src/backend/access/common/reloptions.c *************** *** 24,29 **** --- 24,30 ---- #include "catalog/pg_type.h" #include "commands/defrem.h" #include "commands/tablespace.h" + #include "commands/view.h" #include "nodes/makefuncs.h" #include "utils/array.h" #include "utils/attoptcache.h" *************** static relopt_string stringRelOpts[] = *** 248,253 **** --- 249,265 ---- gistValidateBufferingOption, "auto" }, + { + { + "check_option", + "View has WITH CHECK OPTION defined (local or cascaded).", + RELOPT_KIND_VIEW + }, + 0, + true, + validateWithCheckOption, + NULL + }, /* list terminator */ {{NULL}} }; *************** default_reloptions(Datum reloptions, boo *** 1152,1157 **** --- 1164,1171 ---- offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)}, {"security_barrier", RELOPT_TYPE_BOOL, offsetof(StdRdOptions, security_barrier)}, + {"check_option", RELOPT_TYPE_STRING, + offsetof(StdRdOptions, check_option_offset)}, }; options = parseRelOptions(reloptions, validate, kind, &numoptions); diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt new file mode 100644 index 3a5e24e..71d2c17 *** a/src/backend/catalog/sql_features.txt --- b/src/backend/catalog/sql_features.txt *************** F311 Schema definition statement NO *** 227,233 **** 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 05 GRANT statement YES F312 MERGE statement NO F313 Enhanced MERGE statement NO --- 227,233 ---- 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 YES F311 Schema definition statement 05 GRANT statement YES F312 MERGE statement NO F313 Enhanced MERGE statement NO *************** F711 ALTER domain YES *** 301,307 **** F721 Deferrable constraints NO foreign and unique keys only F731 INSERT column privileges YES F741 Referential MATCH types NO no partial match yet ! F751 View CHECK enhancements NO F761 Session management YES F762 CURRENT_CATALOG YES F763 CURRENT_SCHEMA YES --- 301,307 ---- F721 Deferrable constraints NO foreign and unique keys only F731 INSERT column privileges YES F741 Referential MATCH types NO no partial match yet ! F751 View CHECK enhancements YES F761 Session management YES F762 CURRENT_CATALOG YES F763 CURRENT_SCHEMA YES diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c new file mode 100644 index 6186a84..0c4d49b *** a/src/backend/commands/view.c --- b/src/backend/commands/view.c *************** *** 38,43 **** --- 38,61 ---- static void checkViewTupleDesc(TupleDesc newdesc, TupleDesc olddesc); /*--------------------------------------------------------------------- + * Validator for "check_option" reloption on views. The allowed values + * are "local" and "cascaded". + */ + void + validateWithCheckOption(char *value) + { + if (value == NULL || + (strcmp(value, "local") != 0 && + strcmp(value, "cascaded") != 0)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid value for \"check_option\" option"), + errdetail("Valid values are \"local\", and \"cascaded\"."))); + } + } + + /*--------------------------------------------------------------------- * DefineVirtualRelation * * Create the "view" relation. `DefineRelation' does all the work, *************** DefineView(ViewStmt *stmt, const char *q *** 463,468 **** --- 481,499 ---- } /* + * 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"))); + + /* * Create the view relation * * NOTE: if it already exists and replace is false, the xact will be diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c new file mode 100644 index 9b0cd8c..6d59c7f *** a/src/backend/executor/execMain.c --- b/src/backend/executor/execMain.c *************** ExecConstraints(ResultRelInfo *resultRel *** 1604,1609 **** --- 1604,1652 ---- } /* + * ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs + */ + void + ExecWithCheckOptions(ResultRelInfo *resultRelInfo, + TupleTableSlot *slot, EState *estate) + { + ExprContext *econtext; + ListCell *l1, *l2; + + /* + * 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); + + /* + * 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), + errdetail("Failing row contains %s.", + ExecBuildSlotValueDescription(slot, 64)))); + } + } + + /* * ExecBuildSlotValueDescription -- construct a string representing a tuple * * This is intentionally very similar to BuildIndexValueDescription, but diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c new file mode 100644 index e934c7b..fe180ba *** a/src/backend/executor/nodeModifyTable.c --- b/src/backend/executor/nodeModifyTable.c *************** ExecInsert(TupleTableSlot *slot, *** 281,286 **** --- 281,290 ---- list_free(recheckIndexes); + /* Check any WITH CHECK OPTION constraints */ + if (resultRelInfo->ri_WithCheckOptions != NIL) + ExecWithCheckOptions(resultRelInfo, slot, estate); + /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) return ExecProcessReturning(resultRelInfo->ri_projectReturning, *************** lreplace:; *** 777,782 **** --- 781,790 ---- list_free(recheckIndexes); + /* Check any WITH CHECK OPTION constraints */ + if (resultRelInfo->ri_WithCheckOptions != NIL) + ExecWithCheckOptions(resultRelInfo, slot, estate); + /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) return ExecProcessReturning(resultRelInfo->ri_projectReturning, *************** ExecInitModifyTable(ModifyTable *node, E *** 1130,1135 **** --- 1138,1168 ---- estate->es_result_relation_info = saved_resultRelInfo; /* + * Initialize any WITH CHECK OPTION constraints if needed. + */ + resultRelInfo = mtstate->resultRelInfo; + i = 0; + foreach(l, node->withCheckOptionLists) + { + List *wcoList = (List *) lfirst(l); + List *wcoExprs = NIL; + ListCell *ll; + + foreach(ll, wcoList) + { + WithCheckOption *wco = (WithCheckOption *) lfirst(ll); + ExprState *wcoExpr = ExecInitExpr((Expr *) wco->qual, + mtstate->mt_plans[i]); + wcoExprs = lappend(wcoExprs, wcoExpr); + } + + resultRelInfo->ri_WithCheckOptions = wcoList; + resultRelInfo->ri_WithCheckOptionExprs = wcoExprs; + resultRelInfo++; + i++; + } + + /* * Initialize RETURNING projections if needed. */ if (node->returningLists) diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c new file mode 100644 index b5b8d63..3c93881 *** a/src/backend/nodes/copyfuncs.c --- b/src/backend/nodes/copyfuncs.c *************** _copyModifyTable(const ModifyTable *from *** 178,183 **** --- 178,184 ---- COPY_NODE_FIELD(resultRelations); COPY_SCALAR_FIELD(resultRelIndex); COPY_NODE_FIELD(plans); + COPY_NODE_FIELD(withCheckOptionLists); COPY_NODE_FIELD(returningLists); COPY_NODE_FIELD(fdwPrivLists); COPY_NODE_FIELD(rowMarks); *************** _copyRangeTblEntry(const RangeTblEntry * *** 2001,2006 **** --- 2002,2019 ---- return newnode; } + static WithCheckOption * + _copyWithCheckOption(const WithCheckOption *from) + { + WithCheckOption *newnode = makeNode(WithCheckOption); + + COPY_STRING_FIELD(viewname); + COPY_NODE_FIELD(qual); + COPY_SCALAR_FIELD(cascaded); + + return newnode; + } + static SortGroupClause * _copySortGroupClause(const SortGroupClause *from) { *************** _copyQuery(const Query *from) *** 2443,2448 **** --- 2456,2462 ---- COPY_NODE_FIELD(rtable); COPY_NODE_FIELD(jointree); COPY_NODE_FIELD(targetList); + COPY_NODE_FIELD(withCheckOptions); COPY_NODE_FIELD(returningList); COPY_NODE_FIELD(groupClause); COPY_NODE_FIELD(havingQual); *************** _copyViewStmt(const ViewStmt *from) *** 3072,3077 **** --- 3086,3092 ---- COPY_NODE_FIELD(query); COPY_SCALAR_FIELD(replace); COPY_NODE_FIELD(options); + COPY_SCALAR_FIELD(withCheckOption); return newnode; } *************** copyObject(const void *from) *** 4513,4518 **** --- 4528,4536 ---- case T_RangeTblEntry: retval = _copyRangeTblEntry(from); break; + case T_WithCheckOption: + retval = _copyWithCheckOption(from); + break; case T_SortGroupClause: retval = _copySortGroupClause(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c new file mode 100644 index 3f96595..345951a *** a/src/backend/nodes/equalfuncs.c --- b/src/backend/nodes/equalfuncs.c *************** _equalQuery(const Query *a, const Query *** 851,856 **** --- 851,857 ---- COMPARE_NODE_FIELD(rtable); COMPARE_NODE_FIELD(jointree); COMPARE_NODE_FIELD(targetList); + COMPARE_NODE_FIELD(withCheckOptions); COMPARE_NODE_FIELD(returningList); COMPARE_NODE_FIELD(groupClause); COMPARE_NODE_FIELD(havingQual); *************** _equalViewStmt(const ViewStmt *a, const *** 1380,1385 **** --- 1381,1387 ---- COMPARE_NODE_FIELD(query); COMPARE_SCALAR_FIELD(replace); COMPARE_NODE_FIELD(options); + COMPARE_SCALAR_FIELD(withCheckOption); return true; } *************** _equalRangeTblEntry(const RangeTblEntry *** 2250,2255 **** --- 2252,2267 ---- } static bool + _equalWithCheckOption(const WithCheckOption *a, const WithCheckOption *b) + { + COMPARE_STRING_FIELD(viewname); + COMPARE_NODE_FIELD(qual); + COMPARE_SCALAR_FIELD(cascaded); + + return true; + } + + static bool _equalSortGroupClause(const SortGroupClause *a, const SortGroupClause *b) { COMPARE_SCALAR_FIELD(tleSortGroupRef); *************** equal(const void *a, const void *b) *** 2983,2988 **** --- 2995,3003 ---- case T_RangeTblEntry: retval = _equalRangeTblEntry(a, b); break; + case T_WithCheckOption: + retval = _equalWithCheckOption(a, b); + break; case T_SortGroupClause: retval = _equalSortGroupClause(a, b); break; diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c new file mode 100644 index 42d6621..45e0c27 *** a/src/backend/nodes/nodeFuncs.c --- b/src/backend/nodes/nodeFuncs.c *************** expression_tree_walker(Node *node, *** 1556,1561 **** --- 1556,1563 ---- case T_SortGroupClause: /* primitive node types with no expression subnodes */ break; + case T_WithCheckOption: + return walker(((WithCheckOption *) node)->qual, context); case T_Aggref: { Aggref *expr = (Aggref *) node; *************** query_tree_walker(Query *query, *** 1869,1874 **** --- 1871,1878 ---- if (walker((Node *) query->targetList, context)) return true; + if (walker((Node *) query->withCheckOptions, context)) + return true; if (walker((Node *) query->returningList, context)) return true; if (walker((Node *) query->jointree, context)) *************** expression_tree_mutator(Node *node, *** 2070,2075 **** --- 2074,2088 ---- case T_RangeTblRef: case T_SortGroupClause: return (Node *) copyObject(node); + case T_WithCheckOption: + { + WithCheckOption *wco = (WithCheckOption *) node; + WithCheckOption *newnode; + + FLATCOPY(newnode, wco, WithCheckOption); + MUTATE(newnode->qual, wco->qual, Node *); + return (Node *) newnode; + } case T_Aggref: { Aggref *aggref = (Aggref *) node; *************** query_tree_mutator(Query *query, *** 2583,2588 **** --- 2596,2602 ---- } MUTATE(query->targetList, query->targetList, List *); + MUTATE(query->withCheckOptions, query->withCheckOptions, List *); MUTATE(query->returningList, query->returningList, List *); MUTATE(query->jointree, query->jointree, FromExpr *); MUTATE(query->setOperations, query->setOperations, Node *); diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c new file mode 100644 index b2183f4..b6ead5e *** a/src/backend/nodes/outfuncs.c --- b/src/backend/nodes/outfuncs.c *************** _outModifyTable(StringInfo str, const Mo *** 332,337 **** --- 332,338 ---- WRITE_NODE_FIELD(resultRelations); WRITE_INT_FIELD(resultRelIndex); WRITE_NODE_FIELD(plans); + WRITE_NODE_FIELD(withCheckOptionLists); WRITE_NODE_FIELD(returningLists); WRITE_NODE_FIELD(fdwPrivLists); WRITE_NODE_FIELD(rowMarks); *************** _outQuery(StringInfo str, const Query *n *** 2244,2249 **** --- 2245,2251 ---- WRITE_NODE_FIELD(rtable); WRITE_NODE_FIELD(jointree); WRITE_NODE_FIELD(targetList); + WRITE_NODE_FIELD(withCheckOptions); WRITE_NODE_FIELD(returningList); WRITE_NODE_FIELD(groupClause); WRITE_NODE_FIELD(havingQual); *************** _outQuery(StringInfo str, const Query *n *** 2258,2263 **** --- 2260,2275 ---- } static void + _outWithCheckOption(StringInfo str, const WithCheckOption *node) + { + WRITE_NODE_TYPE("WITHCHECKOPTION"); + + WRITE_STRING_FIELD(viewname); + WRITE_NODE_FIELD(qual); + WRITE_BOOL_FIELD(cascaded); + } + + static void _outSortGroupClause(StringInfo str, const SortGroupClause *node) { WRITE_NODE_TYPE("SORTGROUPCLAUSE"); *************** _outNode(StringInfo str, const void *obj *** 3111,3116 **** --- 3123,3131 ---- case T_Query: _outQuery(str, obj); break; + case T_WithCheckOption: + _outWithCheckOption(str, obj); + break; case T_SortGroupClause: _outSortGroupClause(str, obj); break; diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c new file mode 100644 index 3a16e9d..44f0c15 *** a/src/backend/nodes/readfuncs.c --- b/src/backend/nodes/readfuncs.c *************** _readQuery(void) *** 210,215 **** --- 210,216 ---- READ_NODE_FIELD(rtable); READ_NODE_FIELD(jointree); READ_NODE_FIELD(targetList); + READ_NODE_FIELD(withCheckOptions); READ_NODE_FIELD(returningList); READ_NODE_FIELD(groupClause); READ_NODE_FIELD(havingQual); *************** _readDeclareCursorStmt(void) *** 255,260 **** --- 256,276 ---- } /* + * _readWithCheckOption + */ + static WithCheckOption * + _readWithCheckOption(void) + { + READ_LOCALS(WithCheckOption); + + READ_STRING_FIELD(viewname); + READ_NODE_FIELD(qual); + READ_BOOL_FIELD(cascaded); + + READ_DONE(); + } + + /* * _readSortGroupClause */ static SortGroupClause * *************** parseNodeString(void) *** 1258,1263 **** --- 1274,1281 ---- 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("WINDOWCLAUSE", 12)) diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c new file mode 100644 index 52bab79..c17b460 *** a/src/backend/optimizer/plan/createplan.c --- b/src/backend/optimizer/plan/createplan.c *************** make_result(PlannerInfo *root, *** 4699,4714 **** * 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. */ ModifyTable * make_modifytable(PlannerInfo *root, CmdType operation, bool canSetTag, ! List *resultRelations, ! List *subplans, List *returningLists, List *rowMarks, int epqParam) { ModifyTable *node = makeNode(ModifyTable); --- 4699,4714 ---- * Build a ModifyTable plan node * * Currently, we don't charge anything extra for the actual table modification ! * 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. */ ModifyTable * make_modifytable(PlannerInfo *root, CmdType operation, bool canSetTag, ! List *resultRelations, List *subplans, ! List *withCheckOptionLists, List *returningLists, List *rowMarks, int epqParam) { ModifyTable *node = makeNode(ModifyTable); *************** make_modifytable(PlannerInfo *root, *** 4720,4725 **** --- 4720,4727 ---- int i; 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)); *************** make_modifytable(PlannerInfo *root, *** 4756,4761 **** --- 4758,4764 ---- 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; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c new file mode 100644 index d80c264..01e2fa3 *** a/src/backend/optimizer/plan/planner.c --- b/src/backend/optimizer/plan/planner.c *************** subquery_planner(PlannerGlobal *glob, Qu *** 294,299 **** --- 294,300 ---- int num_old_subplans = list_length(glob->subplans); PlannerInfo *root; Plan *plan; + List *newWithCheckOptions; List *newHaving; bool hasOuterJoins; ListCell *l; *************** subquery_planner(PlannerGlobal *glob, Qu *** 421,426 **** --- 422,439 ---- preprocess_expression(root, (Node *) parse->targetList, EXPRKIND_TARGET); + newWithCheckOptions = NIL; + 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); *************** subquery_planner(PlannerGlobal *glob, Qu *** 559,570 **** /* If it's not SELECT, we need a ModifyTable node */ if (parse->commandType != CMD_SELECT) { List *returningLists; List *rowMarks; /* ! * Set up the RETURNING list-of-lists, if needed. */ if (parse->returningList) returningLists = list_make1(parse->returningList); else --- 572,590 ---- /* If it's not SELECT, we need a ModifyTable node */ if (parse->commandType != CMD_SELECT) { + List *withCheckOptionLists; List *returningLists; List *rowMarks; /* ! * 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 *************** subquery_planner(PlannerGlobal *glob, Qu *** 585,590 **** --- 605,611 ---- parse->canSetTag, list_make1_int(parse->resultRelation), list_make1(plan), + withCheckOptionLists, returningLists, rowMarks, SS_assign_special_param(root)); *************** inheritance_planner(PlannerInfo *root) *** 770,775 **** --- 791,797 ---- RelOptInfo **save_rel_array = NULL; List *subplans = NIL; List *resultRelations = NIL; + List *withCheckOptionLists = NIL; List *returningLists = NIL; List *rowMarks; ListCell *lc; *************** inheritance_planner(PlannerInfo *root) *** 930,936 **** /* Build list of target-relation RT indexes */ resultRelations = lappend_int(resultRelations, appinfo->child_relid); ! /* Build list of per-relation RETURNING targetlists */ if (parse->returningList) returningLists = lappend(returningLists, subroot.parse->returningList); --- 952,961 ---- /* Build list of target-relation RT indexes */ resultRelations = lappend_int(resultRelations, appinfo->child_relid); ! /* Build lists of per-relation WCO and RETURNING targetlists */ ! if (parse->withCheckOptions) ! withCheckOptionLists = lappend(withCheckOptionLists, ! subroot.parse->withCheckOptions); if (parse->returningList) returningLists = lappend(returningLists, subroot.parse->returningList); *************** inheritance_planner(PlannerInfo *root) *** 979,984 **** --- 1004,1010 ---- parse->canSetTag, resultRelations, subplans, + withCheckOptionLists, returningLists, rowMarks, SS_assign_special_param(root)); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y new file mode 100644 index 5094226..5377bca *** a/src/backend/parser/gram.y --- b/src/backend/parser/gram.y *************** static Node *makeRecursiveViewSelect(cha *** 470,476 **** %type constraints_set_list %type constraints_set_mode %type OptTableSpace OptConsTableSpace OptTableSpaceOwner ! %type opt_check_option %type opt_provider security_label --- 470,476 ---- %type constraints_set_list %type constraints_set_mode %type OptTableSpace OptConsTableSpace OptTableSpaceOwner ! %type opt_check_option %type opt_provider security_label *************** ViewStmt: CREATE OptTemp VIEW qualified_ *** 7976,7981 **** --- 7976,7982 ---- n->query = $8; n->replace = false; n->options = $6; + n->withCheckOption = $9; $$ = (Node *) n; } | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list opt_reloptions *************** ViewStmt: CREATE OptTemp VIEW qualified_ *** 7988,7993 **** --- 7989,7995 ---- n->query = $10; n->replace = true; n->options = $8; + n->withCheckOption = $11; $$ = (Node *) n; } | CREATE OptTemp RECURSIVE VIEW qualified_name '(' columnList ')' opt_reloptions *************** ViewStmt: CREATE OptTemp VIEW qualified_ *** 8000,8005 **** --- 8002,8008 ---- n->query = makeRecursiveViewSelect(n->view->relname, n->aliases, $11); n->replace = false; n->options = $9; + n->withCheckOption = NO_CHECK_OPTION; $$ = (Node *) n; } | CREATE OR REPLACE OptTemp RECURSIVE VIEW qualified_name '(' columnList ')' opt_reloptions *************** ViewStmt: CREATE OptTemp VIEW qualified_ *** 8012,8041 **** n->query = makeRecursiveViewSelect(n->view->relname, n->aliases, $13); n->replace = true; n->options = $11; $$ = (Node *) n; } ; opt_check_option: ! WITH CHECK OPTION ! { ! ereport(ERROR, ! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ! errmsg("WITH CHECK OPTION is not implemented"))); ! } ! | WITH CASCADED CHECK OPTION ! { ! ereport(ERROR, ! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ! errmsg("WITH CHECK OPTION is not implemented"))); ! } ! | WITH LOCAL CHECK OPTION ! { ! ereport(ERROR, ! (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ! errmsg("WITH CHECK OPTION is not implemented"))); ! } ! | /* EMPTY */ { $$ = NIL; } ; /***************************************************************************** --- 8015,8030 ---- n->query = makeRecursiveViewSelect(n->view->relname, n->aliases, $13); n->replace = true; n->options = $11; + n->withCheckOption = NO_CHECK_OPTION; $$ = (Node *) n; } ; opt_check_option: ! 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/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c new file mode 100644 index 01875fc..3e5da5f *** a/src/backend/rewrite/rewriteHandler.c --- b/src/backend/rewrite/rewriteHandler.c *************** *** 19,24 **** --- 19,25 ---- #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" *************** rewriteTargetView(Query *parsetree, Rela *** 2471,2478 **** * only adjust their varnos to reference the new target (just the same as * we did with the view targetlist). * ! * For INSERT, the view's quals can be ignored for now. When we implement ! * WITH CHECK OPTION, this might be a good place to collect them. */ if (parsetree->commandType != CMD_INSERT && viewquery->jointree->quals != NULL) --- 2472,2478 ---- * only adjust their varnos to reference the new target (just the same as * we did with the view targetlist). * ! * For INSERT, the view's quals can be ignored in the main query. */ if (parsetree->commandType != CMD_INSERT && viewquery->jointree->quals != NULL) *************** rewriteTargetView(Query *parsetree, Rela *** 2483,2488 **** --- 2483,2567 ---- 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) + { + WithCheckOption *wco; + + /** + * If the parent view has a cascaded check option, add any quals from + * this view to the parent's WithCheckOption, so that any constraint + * violation is reported against the parent view that defined the + * check. + * + * 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. + */ + wco = NULL; + if (parsetree->withCheckOptions != NIL) + { + WithCheckOption *parent_wco = + (WithCheckOption *) linitial(parsetree->withCheckOptions); + + if (parent_wco->cascaded) + wco = parent_wco; + } + + /* + * Otherwise, if the WITH CHECK OPTION is defined on this view, make a + * new WithCheckOption structure that the quals can be added to. + * + * New WithCheckOptions are added 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 and we don't already have a + * WithCheckOption structure, we need to build one now even if this + * view has no quals, since there may be quals on child views. + */ + if (wco == NULL && + RelationHasCheckOption(view) && + (viewquery->jointree->quals != NULL || + RelationHasCascadedCheckOption(view))) + { + wco = makeNode(WithCheckOption); + + wco->viewname = pstrdup(RelationGetRelationName(view)); + wco->qual = NULL; + wco->cascaded = RelationHasCascadedCheckOption(view); + + parsetree->withCheckOptions = lcons(wco, + parsetree->withCheckOptions); + } + + /* + * Add any quals defined locally on this view to the WithCheckOption + * to be checked, if there is one. + */ + if (wco != NULL && viewquery->jointree->quals != NULL) + { + Node *viewqual = (Node *) copyObject(viewquery->jointree->quals); + + ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0); + wco->qual = make_and_qual(viewqual, wco->qual); + + /* + * 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 to the query's WHERE clause, + * and AddQual will have already done this check. + */ + if (!parsetree->hasSubLinks && + parsetree->commandType != CMD_UPDATE) + parsetree->hasSubLinks = checkExprHasSubLink(viewqual); + } + } + return parsetree; } diff --git a/src/include/commands/view.h b/src/include/commands/view.h new file mode 100644 index 431be94..e9b4b5d *** a/src/include/commands/view.h --- b/src/include/commands/view.h *************** *** 16,21 **** --- 16,23 ---- #include "nodes/parsenodes.h" + extern void validateWithCheckOption(char *value); + extern Oid DefineView(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 new file mode 100644 index bc215d6..75841c8 *** a/src/include/executor/executor.h --- b/src/include/executor/executor.h *************** extern ResultRelInfo *ExecGetTriggerResu *** 191,196 **** --- 191,198 ---- extern bool ExecContextForcesOids(PlanState *planstate, bool *hasoids); extern void ExecConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, EState *estate); + 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, diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h new file mode 100644 index 4f77016..a129b68 *** a/src/include/nodes/execnodes.h --- b/src/include/nodes/execnodes.h *************** typedef struct JunkFilter *** 303,308 **** --- 303,310 ---- * TrigInstrument optional runtime measurements for triggers * FdwRoutine FDW callback functions, if foreign table * FdwState available to save private state of FDW + * WithCheckOptions list of WithCheckOption's for views + * WithCheckOptionExprs list of WithCheckOption expr states * ConstraintExprs array of constraint-checking expr states * junkFilter for removing junk attributes from tuples * projectReturning for computing a RETURNING list *************** typedef struct ResultRelInfo *** 322,327 **** --- 324,331 ---- Instrumentation *ri_TrigInstrument; struct FdwRoutine *ri_FdwRoutine; void *ri_FdwState; + List *ri_WithCheckOptions; + List *ri_WithCheckOptionExprs; List **ri_ConstraintExprs; JunkFilter *ri_junkFilter; ProjectionInfo *ri_projectReturning; diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h new file mode 100644 index 0d5c007..78368c6 *** a/src/include/nodes/nodes.h --- b/src/include/nodes/nodes.h *************** typedef enum NodeTag *** 388,393 **** --- 388,394 ---- T_Constraint, T_DefElem, T_RangeTblEntry, + T_WithCheckOption, T_SortGroupClause, T_WindowClause, T_PrivGrantee, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h new file mode 100644 index 6723647..f4a18fc *** a/src/include/nodes/parsenodes.h --- b/src/include/nodes/parsenodes.h *************** typedef struct Query *** 128,133 **** --- 128,135 ---- List *targetList; /* target list (of TargetEntry) */ + List *withCheckOptions; /* a list of WithCheckOption's */ + List *returningList; /* return-values list (of TargetEntry) */ List *groupClause; /* a list of SortGroupClause's */ *************** typedef struct RangeTblEntry *** 778,783 **** --- 780,798 ---- } 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 */ + } WithCheckOption; + + /* * SortGroupClause - * representation of ORDER BY, GROUP BY, PARTITION BY, * DISTINCT, DISTINCT ON items *************** typedef struct AlterEnumStmt *** 2326,2331 **** --- 2341,2353 ---- * Create View Statement * ---------------------- */ + typedef enum ViewCheckOption + { + NO_CHECK_OPTION, + LOCAL_CHECK_OPTION, + CASCADED_CHECK_OPTION + } ViewCheckOption; + typedef struct ViewStmt { NodeTag type; *************** typedef struct ViewStmt *** 2334,2339 **** --- 2356,2362 ---- Node *query; /* the SELECT query */ bool replace; /* replace an existing view? */ List *options; /* options from WITH clause */ + ViewCheckOption withCheckOption; /* WITH CHECK OPTION */ } ViewStmt; /* ---------------------- diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h new file mode 100644 index 841701e..aa4f12c *** a/src/include/nodes/plannodes.h --- b/src/include/nodes/plannodes.h *************** typedef struct ModifyTable *** 172,177 **** --- 172,178 ---- List *resultRelations; /* integer list of RT indexes */ int resultRelIndex; /* index of first resultRel in plan's list */ List *plans; /* plan(s) producing source data */ + List *withCheckOptionLists; /* per-target-table WCO lists */ List *returningLists; /* per-target-table RETURNING tlists */ List *fdwPrivLists; /* per-target-table FDW private data lists */ List *rowMarks; /* PlanRowMarks (non-locking only) */ diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h new file mode 100644 index 33eaf32..bd6841f *** a/src/include/optimizer/planmain.h --- b/src/include/optimizer/planmain.h *************** extern Result *make_result(PlannerInfo * *** 85,91 **** Node *resconstantqual, Plan *subplan); extern ModifyTable *make_modifytable(PlannerInfo *root, CmdType operation, bool canSetTag, ! List *resultRelations, List *subplans, List *returningLists, List *rowMarks, int epqParam); extern bool is_projection_capable_plan(Plan *plan); --- 85,92 ---- Node *resconstantqual, Plan *subplan); extern ModifyTable *make_modifytable(PlannerInfo *root, CmdType operation, bool canSetTag, ! List *resultRelations, List *subplans, ! List *withCheckOptionLists, List *returningLists, List *rowMarks, int epqParam); extern bool is_projection_capable_plan(Plan *plan); diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h new file mode 100644 index 58cc3f7..589c9a8 *** a/src/include/utils/rel.h --- b/src/include/utils/rel.h *************** typedef struct StdRdOptions *** 208,213 **** --- 208,214 ---- int fillfactor; /* page fill factor in percent (0..100) */ AutoVacOpts autovacuum; /* autovacuum-related options */ bool security_barrier; /* for views */ + int check_option_offset; /* for views */ } StdRdOptions; #define HEAP_MIN_FILLFACTOR 10 *************** typedef struct StdRdOptions *** 244,249 **** --- 245,283 ---- ((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) + + /* * RelationIsValid * True iff relation descriptor is valid. */ diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out new file mode 100644 index ecb61e0..c7fa390 *** a/src/test/regress/expected/updatable_views.out --- b/src/test/regress/expected/updatable_views.out *************** SELECT * FROM rw_view1; *** 1063,1065 **** --- 1063,1363 ---- 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 base_tbl.a, + base_tbl.b + FROM base_tbl + WHERE base_tbl.a < base_tbl.b; + Options: check_option=local + + 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 rw_view1.a + FROM rw_view1 + WHERE rw_view1.a < 10; + Options: check_option=cascaded + + 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 (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_view2" + 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 rw_view1.a + FROM rw_view1 + WHERE rw_view1.a < 10; + Options: check_option=local + + 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). + 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; + 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_view3" + 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); + 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_1 + (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_1 + Index Cond: (a = b.a) + SubPlan 2 + -> Seq Scan on ref_tbl r_2 + (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 diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql new file mode 100644 index 49dfedd..690dcea *** a/src/test/regress/sql/updatable_views.sql --- b/src/test/regress/sql/updatable_views.sql *************** UPDATE rw_view1 SET arr[1] = 42, arr[2] *** 509,511 **** --- 509,700 ---- SELECT * FROM rw_view1; 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 + + 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 + + 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 + + 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 + + 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; + + 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;