diff --git a/doc/src/sgml/ref/alter_view.sgml b/doc/src/sgml/ref/alter_view.sgml new file mode 100644 index df527ae..62cce0f *** a/doc/src/sgml/ref/alter_view.sgml --- b/doc/src/sgml/ref/alter_view.sgml *************** ALTER VIEW [ IF EXISTS ] name SET SCHEMA new_schema ALTER VIEW [ IF EXISTS ] name SET ( view_option_name [= view_option_value] [, ... ] ) ALTER VIEW [ IF EXISTS ] name RESET ( view_option_name [, ... ] ) + + where view_option_name can be one of: + + security_barrier [ boolean ] + check_option [ text (local or cascaded) ] diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml new file mode 100644 index 2af6f6e..ff3111f *** a/doc/src/sgml/ref/create_view.sgml --- b/doc/src/sgml/ref/create_view.sgml *************** PostgreSQL documentation *** 24,29 **** --- 24,35 ---- CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] [ RECURSIVE ] VIEW name [ ( column_name [, ...] ) ] [ WITH ( view_option_name [= view_option_value] [, ... ] ) ] AS query + [ WITH [ CASCADED | LOCAL ] CHECK OPTION ] + + where view_option_name can be one of: + + security_barrier [ boolean ] + check_option [ text (local or cascaded) ] *************** 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. --- 126,158 ---- 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 parameter may be either local or ! cascaded, and is equivalent to specifying ! WITH [ CASCADED | LOCAL ] CHECK OPTION (see below). ! This option can be changed on existing views using . ! ! ! ! *************** CREATE VIEW name AS WITH *** 138,143 **** --- 167,243 ---- + + + 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. --- 356,364 ---- 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. --- 416,425 ---- 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/information_schema.sql b/src/backend/catalog/information_schema.sql new file mode 100644 index e1f8e7f..95f267f *** a/src/backend/catalog/information_schema.sql --- b/src/backend/catalog/information_schema.sql *************** CREATE VIEW views AS *** 2494,2500 **** ELSE null END AS character_data) AS view_definition, ! CAST('NONE' AS character_data) AS check_option, CAST( -- (1 << CMD_UPDATE) + (1 << CMD_DELETE) --- 2494,2506 ---- 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) 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/tablecmds.c b/src/backend/commands/tablecmds.c new file mode 100644 index 6708725..54d238c *** a/src/backend/commands/tablecmds.c --- b/src/backend/commands/tablecmds.c *************** ATExecSetRelOptions(Relation rel, List * *** 8773,8778 **** --- 8773,8804 ---- break; } + /* Special-case validation of view options */ + if (rel->rd_rel->relkind == RELKIND_VIEW) + { + Query *view_query = get_view_query(rel); + List *view_options = untransformRelOptions(newOptions); + ListCell *cell; + bool check_option; + bool security_barrier; + + foreach(cell, view_options) + { + DefElem *defel = (DefElem *) lfirst(cell); + + if (pg_strcasecmp(defel->defname, "check_option") == 0) + check_option = true; + if (pg_strcasecmp(defel->defname, "security_barrier") == 0) + security_barrier = defGetBoolean(defel); + } + + if (check_option && + view_query_is_auto_updatable(view_query, security_barrier) != NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("WITH CHECK OPTION is supported only on auto-updatable views"))); + } + /* * 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/backend/commands/view.c b/src/backend/commands/view.c new file mode 100644 index 6186a84..bb1ef86 *** a/src/backend/commands/view.c --- b/src/backend/commands/view.c *************** *** 27,32 **** --- 27,33 ---- #include "parser/parse_relation.h" #include "rewrite/rewriteDefine.h" #include "rewrite/rewriteManip.h" + #include "rewrite/rewriteHandler.h" #include "rewrite/rewriteSupport.h" #include "utils/acl.h" #include "utils/builtins.h" *************** *** 38,43 **** --- 39,62 ---- 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 *** 374,379 **** --- 393,401 ---- Query *viewParse; Oid viewOid; RangeVar *view; + ListCell *cell; + bool check_option; + bool security_barrier; /* * Run parse analysis to convert the raw parse tree to a Query. Note this *************** DefineView(ViewStmt *stmt, const char *q *** 411,416 **** --- 433,471 ---- errmsg("views must not contain data-modifying statements in WITH"))); /* + * 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. + */ + foreach(cell, stmt->options) + { + DefElem *defel = (DefElem *) lfirst(cell); + + if (pg_strcasecmp(defel->defname, "check_option") == 0) + check_option = true; + if (pg_strcasecmp(defel->defname, "security_barrier") == 0) + security_barrier = defGetBoolean(defel); + } + + if (check_option && + view_query_is_auto_updatable(viewParse, security_barrier) != NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("WITH CHECK OPTION is supported only on auto-updatable views"))); + + /* * If a list of column names was given, run through and insert these into * the actual query tree. - thomas 2000-03-08 */ diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c new file mode 100644 index 3b664d0..0ee7820 *** a/src/backend/executor/execMain.c --- b/src/backend/executor/execMain.c *************** ExecConstraints(ResultRelInfo *resultRel *** 1622,1627 **** --- 1622,1670 ---- } /* + * 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 f67ef0c..e06e8b8 *** 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_ *** 7993,7998 **** --- 7993,7999 ---- 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_ *** 8005,8010 **** --- 8006,8012 ---- 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_ *** 8017,8022 **** --- 8019,8025 ---- 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_ *** 8029,8058 **** 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; } ; /***************************************************************************** --- 8032,8047 ---- 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 d909de3..cad6482 *** 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" *************** fireRules(Query *parsetree, *** 1866,1872 **** * Caller should have verified that the relation is a view, and therefore * we should find an ON SELECT action. */ ! static Query * get_view_query(Relation view) { int i; --- 1867,1873 ---- * Caller should have verified that the relation is a view, and therefore * we should find an ON SELECT action. */ ! Query * get_view_query(Relation view) { int i; *************** view_has_instead_trigger(Relation view, *** 1940,1949 **** * Also note that we don't check for INSTEAD triggers or rules here; those * also prevent auto-update, but they must be checked for by the caller. */ ! static const char * ! view_is_auto_updatable(Relation view) { - Query *viewquery = get_view_query(view); RangeTblRef *rtr; RangeTblEntry *base_rte; Bitmapset *bms; --- 1941,1949 ---- * Also note that we don't check for INSTEAD triggers or rules here; those * also prevent auto-update, but they must be checked for by the caller. */ ! const char * ! view_query_is_auto_updatable(Query *viewquery, bool security_barrier) { RangeTblRef *rtr; RangeTblEntry *base_rte; Bitmapset *bms; *************** view_is_auto_updatable(Relation view) *** 1997,2003 **** * difficulty of keeping upper-level qual expressions away from * lower-level data. This might get relaxed in future. */ ! if (RelationIsSecurityView(view)) return gettext_noop("Security-barrier views are not automatically updatable."); /* --- 1997,2003 ---- * difficulty of keeping upper-level qual expressions away from * lower-level data. This might get relaxed in future. */ ! if (security_barrier) return gettext_noop("Security-barrier views are not automatically updatable."); /* *************** view_is_auto_updatable(Relation view) *** 2057,2062 **** --- 2057,2071 ---- return NULL; /* the view is simply updatable */ } + static const char * + view_is_auto_updatable(Relation view) + { + Query *viewquery = get_view_query(view); + bool security_barrier = RelationIsSecurityView(view); + + return view_query_is_auto_updatable(viewquery, security_barrier); + } + /* * relation_is_updatable - determine which update events the specified *************** rewriteTargetView(Query *parsetree, Rela *** 2532,2539 **** * 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) --- 2541,2547 ---- * 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 *** 2544,2549 **** --- 2552,2636 ---- 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/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c new file mode 100644 index f40961f..8987077 *** a/src/bin/pg_dump/pg_dump.c --- b/src/bin/pg_dump/pg_dump.c *************** dumpTableSchema(Archive *fout, TableInfo *** 12813,12822 **** --- 12813,12873 ---- if (tbinfo->relkind == RELKIND_VIEW) { PQExpBuffer result; + char *co; + size_t co_len; + char *new_co = NULL; reltypename = "VIEW"; /* + * If the view's reloptions include "check_option", remove it and use + * the SQL standard syntax instead + */ + co = strstr(tbinfo->reloptions, "check_option=local"); + if (co != NULL) + { + co_len = 18; + if (co == tbinfo->reloptions) + { + while (co[co_len] == ',' || co[co_len] == ' ') + co_len++; + } + else + { + while (co > tbinfo->reloptions && + (co[-1] == ',' || co[-1] == ' ')) + { + co--; + co_len++; + } + } + memmove(co, co+co_len, 1+strlen(co+co_len)); + new_co = "LOCAL CHECK OPTION"; + } + + co = strstr(tbinfo->reloptions, "check_option=cascaded"); + if (co != NULL) + { + co_len = 21; + if (co == tbinfo->reloptions) + { + while (co[co_len] == ',' || co[co_len] == ' ') + co_len++; + } + else + { + while (co > tbinfo->reloptions && + (co[-1] == ',' || co[-1] == ' ')) + { + co--; + co_len++; + } + } + memmove(co, co+co_len, 1+strlen(co+co_len)); + new_co = "CASCADED CHECK OPTION"; + } + + /* * DROP must be fully qualified in case same name appears in * pg_catalog */ *************** dumpTableSchema(Archive *fout, TableInfo *** 12833,12841 **** if (tbinfo->reloptions && strlen(tbinfo->reloptions) > 0) appendPQExpBuffer(q, " WITH (%s)", tbinfo->reloptions); result = createViewAsClause(fout, tbinfo); ! appendPQExpBuffer(q, " AS\n%s;\n", result->data); destroyPQExpBuffer(result); appendPQExpBuffer(labelq, "VIEW %s", fmtId(tbinfo->dobj.name)); } --- 12884,12896 ---- if (tbinfo->reloptions && strlen(tbinfo->reloptions) > 0) appendPQExpBuffer(q, " WITH (%s)", tbinfo->reloptions); result = createViewAsClause(fout, tbinfo); ! appendPQExpBuffer(q, " AS\n%s", result->data); destroyPQExpBuffer(result); + if (new_co != NULL) + appendPQExpBuffer(q, "\n WITH %s", new_co); + appendPQExpBuffer(q, ";\n"); + appendPQExpBuffer(labelq, "VIEW %s", fmtId(tbinfo->dobj.name)); } 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 de22dff..07e86da *** 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 *** 783,788 **** --- 785,803 ---- } 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 *** 2332,2337 **** --- 2347,2359 ---- * Create View Statement * ---------------------- */ + typedef enum ViewCheckOption + { + NO_CHECK_OPTION, + LOCAL_CHECK_OPTION, + CASCADED_CHECK_OPTION + } ViewCheckOption; + typedef struct ViewStmt { NodeTag type; *************** typedef struct ViewStmt *** 2340,2345 **** --- 2362,2368 ---- 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/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h new file mode 100644 index 1831de4..e043ac5 *** a/src/include/rewrite/rewriteHandler.h --- b/src/include/rewrite/rewriteHandler.h *************** extern List *QueryRewrite(Query *parsetr *** 21,26 **** --- 21,29 ---- extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown); extern Node *build_column_default(Relation rel, int attrno); + extern Query *get_view_query(Relation view); + extern const char *view_query_is_auto_updatable(Query *viewquery, + bool security_barrier); extern int relation_is_updatable(Oid reloid, bool include_triggers); #endif /* REWRITEHANDLER_H */ 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/create_view.out b/src/test/regress/expected/create_view.out new file mode 100644 index 11ac795..4fa7749 *** a/src/test/regress/expected/create_view.out --- b/src/test/regress/expected/create_view.out *************** CREATE VIEW mysecview4 WITH (security_ba *** 252,258 **** AS SELECT * FROM tbl1 WHERE a <> 0; CREATE VIEW mysecview5 WITH (security_barrier=100) -- Error AS SELECT * FROM tbl1 WHERE a > 100; ! ERROR: invalid value for boolean option "security_barrier": 100 CREATE VIEW mysecview6 WITH (invalid_option) -- Error AS SELECT * FROM tbl1 WHERE a < 100; ERROR: unrecognized parameter "invalid_option" --- 252,258 ---- AS SELECT * FROM tbl1 WHERE a <> 0; CREATE VIEW mysecview5 WITH (security_barrier=100) -- Error AS SELECT * FROM tbl1 WHERE a > 100; ! ERROR: security_barrier requires a Boolean value CREATE VIEW mysecview6 WITH (invalid_option) -- Error AS SELECT * FROM tbl1 WHERE a < 100; ERROR: unrecognized parameter "invalid_option" diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out new file mode 100644 index 1363103..bdab973 *** a/src/test/regress/expected/updatable_views.out --- b/src/test/regress/expected/updatable_views.out *************** DROP TABLE base_tbl_parent, base_tbl_chi *** 1163,1165 **** --- 1163,1528 ---- NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to view rw_view1 drop cascades to view rw_view2 + -- 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 + + 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 base_tbl.a, +| LOCAL | YES | YES | NO | NO | NO + | | | base_tbl.b +| | | | | | + | | | FROM base_tbl +| | | | | | + | | | WHERE (base_tbl.a < base_tbl.b); | | | | | | + (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 rw_view1.a + 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 rw_view1.a +| CASCADED | YES | YES | NO | NO | NO + | | | FROM rw_view1 +| | | | | | + | | | WHERE (rw_view1.a < 10); | | | | | | + (1 row) + + 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 + + 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 rw_view1.a +| LOCAL | YES | YES | NO | NO | NO + | | | FROM rw_view1 +| | | | | | + | | | WHERE (rw_view1.a < 10); | | | | | | + (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 rw_view1.a + 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 rw_view1.a +| NONE | YES | YES | NO | NO | NO + | | | FROM rw_view1 +| | | | | | + | | | WHERE (rw_view1.a < 10); | | | | | | + (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 base_tbl.a +| CASCADED | YES | YES | NO | NO | NO + | | | FROM base_tbl; | | | | | | + regression | public | rw_view2 | SELECT rw_view1.a +| NONE | YES | YES | NO | NO | NO + | | | FROM rw_view1 +| | | | | | + | | | WHERE (rw_view1.a > 0); | | | | | | + regression | public | rw_view3 | SELECT rw_view2.a +| CASCADED | YES | YES | NO | NO | NO + | | | FROM rw_view2; | | | | | | + (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_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 c8a1c62..0481800 *** a/src/test/regress/sql/updatable_views.sql --- b/src/test/regress/sql/updatable_views.sql *************** SELECT * FROM ONLY base_tbl_parent ORDER *** 541,543 **** --- 541,742 ---- SELECT * FROM base_tbl_child ORDER BY a; DROP TABLE base_tbl_parent, base_tbl_child 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;