diff --git a/contrib/file_fdw/input/file_fdw.source b/contrib/file_fdw/input/file_fdw.source new file mode 100644 index f7fd28d..063e23c *** a/contrib/file_fdw/input/file_fdw.source --- b/contrib/file_fdw/input/file_fdw.source *************** DELETE FROM agg_csv WHERE a = 100; *** 121,126 **** --- 121,148 ---- -- but this should be ignored SELECT * FROM agg_csv FOR UPDATE; + -- simple views on top of the foreign table aren't updatable + CREATE VIEW agg_csv_v AS SELECT * FROM agg_csv; + + SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'agg_csv%' + ORDER BY table_name; + + SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'agg_csv%' + ORDER BY table_name; + + SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'agg_csv%' + ORDER BY table_name, ordinal_position; + + INSERT INTO agg_csv_v VALUES(1,2.0); + UPDATE agg_csv_v SET a = 1; + DELETE FROM agg_csv_v WHERE a = 100; + -- privilege tests SET ROLE file_fdw_superuser; SELECT * FROM agg_text ORDER BY a; diff --git a/contrib/file_fdw/output/file_fdw.source b/contrib/file_fdw/output/file_fdw.source new file mode 100644 index 4f90bae..2e015cc *** a/contrib/file_fdw/output/file_fdw.source --- b/contrib/file_fdw/output/file_fdw.source *************** SELECT * FROM agg_csv FOR UPDATE; *** 199,204 **** --- 199,243 ---- 42 | 324.78 (3 rows) + -- simple views on top of the foreign table aren't updatable + CREATE VIEW agg_csv_v AS SELECT * FROM agg_csv; + SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'agg_csv%' + ORDER BY table_name; + table_name | is_insertable_into + ------------+-------------------- + agg_csv | NO + agg_csv_v | NO + (2 rows) + + SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'agg_csv%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into + ------------+--------------+-------------------- + agg_csv_v | NO | NO + (1 row) + + SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'agg_csv%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable + ------------+-------------+-------------- + agg_csv | a | NO + agg_csv | b | NO + agg_csv_v | a | NO + agg_csv_v | b | NO + (4 rows) + + INSERT INTO agg_csv_v VALUES(1,2.0); + ERROR: cannot insert into foreign table "agg_csv" + UPDATE agg_csv_v SET a = 1; + ERROR: cannot update foreign table "agg_csv" + DELETE FROM agg_csv_v WHERE a = 100; + ERROR: cannot delete from foreign table "agg_csv" -- privilege tests SET ROLE file_fdw_superuser; SELECT * FROM agg_text ORDER BY a; *************** SET ROLE file_fdw_superuser; *** 243,255 **** -- cleanup RESET ROLE; DROP EXTENSION file_fdw CASCADE; ! NOTICE: drop cascades to 8 other objects DETAIL: drop cascades to server file_server drop cascades to user mapping for file_fdw_user drop cascades to user mapping for file_fdw_superuser drop cascades to user mapping for no_priv_user drop cascades to foreign table agg_text drop cascades to foreign table agg_csv drop cascades to foreign table agg_bad drop cascades to foreign table text_csv DROP ROLE file_fdw_superuser, file_fdw_user, no_priv_user; --- 282,295 ---- -- cleanup RESET ROLE; DROP EXTENSION file_fdw CASCADE; ! NOTICE: drop cascades to 9 other objects DETAIL: drop cascades to server file_server drop cascades to user mapping for file_fdw_user drop cascades to user mapping for file_fdw_superuser drop cascades to user mapping for no_priv_user drop cascades to foreign table agg_text drop cascades to foreign table agg_csv + drop cascades to view agg_csv_v drop cascades to foreign table agg_bad drop cascades to foreign table text_csv DROP ROLE file_fdw_superuser, file_fdw_user, no_priv_user; diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out new file mode 100644 index cb007cd..8f4fe90 *** a/contrib/postgres_fdw/expected/postgres_fdw.out --- b/contrib/postgres_fdw/expected/postgres_fdw.out *************** select c2, count(*) from "S 1"."T 1" whe *** 2339,2341 **** --- 2339,2410 ---- 407 | 100 (13 rows) + -- Test updatable view on top of writable foreign table + CREATE VIEW ft2_v AS SELECT * FROM ft2; + SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'ft2%' + ORDER BY table_name; + table_name | is_insertable_into + ------------+-------------------- + ft2 | YES + ft2_v | YES + (2 rows) + + SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'ft2%' + ORDER BY table_name; + table_name | is_updatable | is_insertable_into + ------------+--------------+-------------------- + ft2_v | YES | YES + (1 row) + + SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'ft2%' + ORDER BY table_name, ordinal_position; + table_name | column_name | is_updatable + ------------+-------------+-------------- + ft2 | c1 | YES + ft2 | c2 | YES + ft2 | c3 | YES + ft2 | c4 | YES + ft2 | c5 | YES + ft2 | c6 | YES + ft2 | c7 | YES + ft2 | c8 | YES + ft2_v | c1 | YES + ft2_v | c2 | YES + ft2_v | c3 | YES + ft2_v | c4 | YES + ft2_v | c5 | YES + ft2_v | c6 | YES + ft2_v | c7 | YES + ft2_v | c8 | YES + (16 rows) + + INSERT INTO ft2_v (c1,c2,c3) VALUES (9999, 250, 'xxx') RETURNING *; + c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 + ------+-----+-----------------+----+----+----+------------+---- + 9999 | 250 | xxx_trig_update | | | | ft2 | + (1 row) + + UPDATE ft2_v SET c2 = c2 + 500 WHERE c1 = 9999 RETURNING *; + c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 + ------+-----+-----------------------------+----+----+----+------------+---- + 9999 | 750 | xxx_trig_update_trig_update | | | | ft2 | + (1 row) + + SELECT * FROM ft2_v WHERE c1 = 9999; + c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 + ------+-----+-----------------------------+----+----+----+------------+---- + 9999 | 750 | xxx_trig_update_trig_update | | | | ft2 | + (1 row) + + DELETE FROM ft2_v WHERE c1 = 9999 RETURNING *; + c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 + ------+-----+-----------------------------+----+----+----+------------+---- + 9999 | 750 | xxx_trig_update_trig_update | | | | ft2 | + (1 row) + diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql new file mode 100644 index 670d769..416afc3 *** a/contrib/postgres_fdw/sql/postgres_fdw.sql --- b/contrib/postgres_fdw/sql/postgres_fdw.sql *************** select c2, count(*) from "S 1"."T 1" whe *** 369,371 **** --- 369,394 ---- commit; select c2, count(*) from ft2 where c2 < 500 group by 1 order by 1; select c2, count(*) from "S 1"."T 1" where c2 < 500 group by 1 order by 1; + + -- Test updatable view on top of writable foreign table + CREATE VIEW ft2_v AS SELECT * FROM ft2; + + SELECT table_name, is_insertable_into + FROM information_schema.tables + WHERE table_name LIKE 'ft2%' + ORDER BY table_name; + + SELECT table_name, is_updatable, is_insertable_into + FROM information_schema.views + WHERE table_name LIKE 'ft2%' + ORDER BY table_name; + + SELECT table_name, column_name, is_updatable + FROM information_schema.columns + WHERE table_name LIKE 'ft2%' + ORDER BY table_name, ordinal_position; + + INSERT INTO ft2_v (c1,c2,c3) VALUES (9999, 250, 'xxx') RETURNING *; + UPDATE ft2_v SET c2 = c2 + 500 WHERE c1 = 9999 RETURNING *; + SELECT * FROM ft2_v WHERE c1 = 9999; + DELETE FROM ft2_v WHERE c1 = 9999 RETURNING *; diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql new file mode 100644 index 2307586..1837b1a *** a/src/backend/catalog/information_schema.sql --- b/src/backend/catalog/information_schema.sql *************** CREATE VIEW columns AS *** 731,737 **** CAST(null AS character_data) AS generation_expression, CAST(CASE WHEN c.relkind = 'r' OR ! (c.relkind = 'v' AND pg_view_is_updatable(c.oid)) THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_updatable FROM (pg_attribute a LEFT JOIN pg_attrdef ad ON attrelid = adrelid AND attnum = adnum) --- 731,737 ---- CAST(null AS character_data) AS generation_expression, CAST(CASE WHEN c.relkind = 'r' OR ! (c.relkind IN ('v', 'f') AND pg_column_is_updatable(c.oid, a.attnum)) THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_updatable FROM (pg_attribute a LEFT JOIN pg_attrdef ad ON attrelid = adrelid AND attnum = adnum) *************** CREATE VIEW tables AS *** 1895,1901 **** CAST(t.typname AS sql_identifier) AS user_defined_type_name, CAST(CASE WHEN c.relkind = 'r' OR ! (c.relkind = 'v' AND pg_view_is_insertable(c.oid)) THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, CAST(CASE WHEN t.typname IS NOT NULL THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_typed, --- 1895,1901 ---- CAST(t.typname AS sql_identifier) AS user_defined_type_name, CAST(CASE WHEN c.relkind = 'r' OR ! (c.relkind IN ('v', 'f') AND pg_relation_is_insertable(c.oid)) THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, CAST(CASE WHEN t.typname IS NOT NULL THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_typed, *************** CREATE VIEW views AS *** 2494,2504 **** CAST('NONE' AS character_data) AS check_option, CAST( ! CASE WHEN pg_view_is_updatable(c.oid) THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_updatable, CAST( ! CASE WHEN pg_view_is_insertable(c.oid) THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, CAST( --- 2494,2504 ---- CAST('NONE' AS character_data) AS check_option, CAST( ! CASE WHEN pg_relation_is_updatable(c.oid) THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_updatable, CAST( ! CASE WHEN pg_relation_is_insertable(c.oid) THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into, CAST( diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c new file mode 100644 index 83f26e3..a75be0e *** a/src/backend/rewrite/rewriteHandler.c --- b/src/backend/rewrite/rewriteHandler.c *************** view_is_auto_updatable(Relation view) *** 2014,2019 **** --- 2014,2020 ---- base_rte = rt_fetch(rtr->rtindex, viewquery->rtable); if (base_rte->rtekind != RTE_RELATION || (base_rte->relkind != RELKIND_RELATION && + base_rte->relkind != RELKIND_FOREIGN_TABLE && base_rte->relkind != RELKIND_VIEW)) return gettext_noop("Views that do not select from a single table or view are not automatically updatable."); *************** relation_is_updatable(Oid reloid, int re *** 2096,2101 **** --- 2097,2109 ---- if (rel == NULL) return false; + /* If the relation is a table, it is always updatable */ + if (rel->rd_rel->relkind == RELKIND_RELATION) + { + relation_close(rel, AccessShareLock); + return true; + } + /* Look for unconditional DO INSTEAD rules, and note supported events */ rulelocks = rel->rd_rules; if (rulelocks != NULL) *************** relation_is_updatable(Oid reloid, int re *** 2120,2125 **** --- 2128,2150 ---- } } + /* If this is a foreign table, check if it is updatable */ + if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + { + int events = 0; + FdwRoutine *fdwroutine = GetFdwRoutineForRelation(rel, false); + + if (fdwroutine->ExecForeignInsert != NULL) + events |= (1 << CMD_INSERT); + if (fdwroutine->ExecForeignUpdate != NULL) + events |= (1 << CMD_UPDATE); + if (fdwroutine->ExecForeignDelete != NULL) + events |= (1 << CMD_DELETE); + + relation_close(rel, AccessShareLock); + return (events & req_events) == req_events; + } + /* Check if this is an automatically updatable view */ if (rel->rd_rel->relkind == RELKIND_VIEW && view_is_auto_updatable(rel) == NULL) diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c new file mode 100644 index 4e38d7c..fece924 *** a/src/backend/utils/adt/misc.c --- b/src/backend/utils/adt/misc.c *************** pg_collation_for(PG_FUNCTION_ARGS) *** 527,557 **** } ! /* ! * information_schema support functions ! * ! * Test whether a view (identified by pg_class OID) is insertable-into or ! * updatable. The latter requires delete capability too. This is an ! * artifact of the way the SQL standard defines the information_schema views: ! * if we defined separate functions for update and delete, we'd double the ! * work required to compute the view columns. * * These rely on relation_is_updatable(), which is in rewriteHandler.c. */ Datum ! pg_view_is_insertable(PG_FUNCTION_ARGS) { ! Oid viewoid = PG_GETARG_OID(0); int req_events = (1 << CMD_INSERT); ! PG_RETURN_BOOL(relation_is_updatable(viewoid, req_events)); } Datum ! pg_view_is_updatable(PG_FUNCTION_ARGS) { ! Oid viewoid = PG_GETARG_OID(0); int req_events = (1 << CMD_UPDATE) | (1 << CMD_DELETE); ! PG_RETURN_BOOL(relation_is_updatable(viewoid, req_events)); } --- 527,591 ---- } ! /* ---------------------------------------------------------------------- ! * Information_schema support functions. * * These rely on relation_is_updatable(), which is in rewriteHandler.c. + * ---------------------------------------------------------------------- + */ + + /* + * Test whether a relation (identified by pg_class OID) is insertable-into. + * + * This is used in information_schema.tables and information_schema.views, and + * it supports all kinds of relations although we only actually use it for + * views and foreign tables since the other relkinds are trivial. */ Datum ! pg_relation_is_insertable(PG_FUNCTION_ARGS) { ! Oid oid = PG_GETARG_OID(0); int req_events = (1 << CMD_INSERT); ! PG_RETURN_BOOL(relation_is_updatable(oid, req_events)); } + /* + * Test whether a relation (identified by pg_class OID) is updatable. + * + * This is only used for views in information_schema.views, although it will + * also work for all other relation kinds. + * + * NOTE: It requires delete capability too, which is an artifact of the way + * the SQL standard defines the information_schema views: if we defined + * separate functions for update and delete, we'd double the work required to + * compute the view columns. + */ Datum ! pg_relation_is_updatable(PG_FUNCTION_ARGS) { ! Oid oid = PG_GETARG_OID(0); int req_events = (1 << CMD_UPDATE) | (1 << CMD_DELETE); ! PG_RETURN_BOOL(relation_is_updatable(oid, req_events)); ! } ! ! /* ! * Test whether a column (identified by pg_class OID and attnum) is updatable. ! * ! * This is used in information_schema.columns, and it supports all kinds of ! * relations although we only actually use it for views and foreign tables ! * since the other relkinds are trivial. ! */ ! Datum ! pg_column_is_updatable(PG_FUNCTION_ARGS) ! { ! /* ! * For now, we ignore the column argument, but in the future we may have ! * relations with a mix of updatable and non-updatable columns. ! */ ! Oid oid = PG_GETARG_OID(0); ! int req_events = (1 << CMD_UPDATE); ! ! PG_RETURN_BOOL(relation_is_updatable(oid, req_events)); } diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h new file mode 100644 index 685b9c7..5b07d58 *** a/src/include/catalog/pg_proc.h --- b/src/include/catalog/pg_proc.h *************** DESCR("type of the argument"); *** 1976,1985 **** DATA(insert OID = 3162 ( pg_collation_for PGNSP PGUID 12 1 0 0 0 f f f f f f s 1 0 25 "2276" _null_ _null_ _null_ _null_ pg_collation_for _null_ _null_ _null_ )); DESCR("collation of the argument; implementation of the COLLATION FOR expression"); ! DATA(insert OID = 3842 ( pg_view_is_insertable PGNSP PGUID 12 10 0 0 0 f f f f t f s 1 0 16 "26" _null_ _null_ _null_ _null_ pg_view_is_insertable _null_ _null_ _null_ )); ! DESCR("is a view insertable-into"); ! DATA(insert OID = 3843 ( pg_view_is_updatable PGNSP PGUID 12 10 0 0 0 f f f f t f s 1 0 16 "26" _null_ _null_ _null_ _null_ pg_view_is_updatable _null_ _null_ _null_ )); ! DESCR("is a view updatable"); /* Deferrable unique constraint trigger */ DATA(insert OID = 1250 ( unique_key_recheck PGNSP PGUID 12 1 0 0 0 f f f f t f v 0 0 2279 "" _null_ _null_ _null_ _null_ unique_key_recheck _null_ _null_ _null_ )); --- 1976,1987 ---- DATA(insert OID = 3162 ( pg_collation_for PGNSP PGUID 12 1 0 0 0 f f f f f f s 1 0 25 "2276" _null_ _null_ _null_ _null_ pg_collation_for _null_ _null_ _null_ )); DESCR("collation of the argument; implementation of the COLLATION FOR expression"); ! DATA(insert OID = 3842 ( pg_relation_is_insertable PGNSP PGUID 12 10 0 0 0 f f f f t f s 1 0 16 "26" _null_ _null_ _null_ _null_ pg_relation_is_insertable _null_ _null_ _null_ )); ! DESCR("is a relation insertable-into"); ! DATA(insert OID = 3843 ( pg_relation_is_updatable PGNSP PGUID 12 10 0 0 0 f f f f t f s 1 0 16 "26" _null_ _null_ _null_ _null_ pg_relation_is_updatable _null_ _null_ _null_ )); ! DESCR("is a relation updatable"); ! DATA(insert OID = 3847 ( pg_column_is_updatable PGNSP PGUID 12 10 0 0 0 f f f f t f s 2 0 16 "26 23" _null_ _null_ _null_ _null_ pg_column_is_updatable _null_ _null_ _null_ )); ! DESCR("is a column updatable"); /* Deferrable unique constraint trigger */ DATA(insert OID = 1250 ( unique_key_recheck PGNSP PGUID 12 1 0 0 0 f f f f t f v 0 0 2279 "" _null_ _null_ _null_ _null_ unique_key_recheck _null_ _null_ _null_ )); diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h new file mode 100644 index 15b60ab..d2e639e *** a/src/include/utils/builtins.h --- b/src/include/utils/builtins.h *************** extern Datum pg_sleep(PG_FUNCTION_ARGS); *** 485,492 **** extern Datum pg_get_keywords(PG_FUNCTION_ARGS); extern Datum pg_typeof(PG_FUNCTION_ARGS); extern Datum pg_collation_for(PG_FUNCTION_ARGS); ! extern Datum pg_view_is_insertable(PG_FUNCTION_ARGS); ! extern Datum pg_view_is_updatable(PG_FUNCTION_ARGS); /* oid.c */ extern Datum oidin(PG_FUNCTION_ARGS); --- 485,493 ---- extern Datum pg_get_keywords(PG_FUNCTION_ARGS); extern Datum pg_typeof(PG_FUNCTION_ARGS); extern Datum pg_collation_for(PG_FUNCTION_ARGS); ! extern Datum pg_relation_is_insertable(PG_FUNCTION_ARGS); ! extern Datum pg_relation_is_updatable(PG_FUNCTION_ARGS); ! extern Datum pg_column_is_updatable(PG_FUNCTION_ARGS); /* oid.c */ extern Datum oidin(PG_FUNCTION_ARGS); diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out new file mode 100644 index ecb61e0..3adba33 *** a/src/test/regress/expected/updatable_views.out --- b/src/test/regress/expected/updatable_views.out *************** SELECT table_name, column_name, is_updat *** 468,477 **** ORDER BY table_name, ordinal_position; table_name | column_name | is_updatable ------------+-------------+-------------- ! rw_view1 | a | NO ! rw_view1 | b | NO ! rw_view2 | a | NO ! rw_view2 | b | NO (4 rows) CREATE RULE rw_view1_del_rule AS ON DELETE TO rw_view1 --- 468,477 ---- ORDER BY table_name, ordinal_position; table_name | column_name | is_updatable ------------+-------------+-------------- ! rw_view1 | a | YES ! rw_view1 | b | YES ! rw_view2 | a | YES ! rw_view2 | b | YES (4 rows) CREATE RULE rw_view1_del_rule AS ON DELETE TO rw_view1