diff --git a/contrib/oid2name/oid2name.c b/contrib/oid2name/oid2name.c index d8ed06f420..8341a1ffef 100644 --- a/contrib/oid2name/oid2name.c +++ b/contrib/oid2name/oid2name.c @@ -444,7 +444,7 @@ sql_exec_dumpalltables(PGconn *conn, struct options * opts) " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace " " LEFT JOIN pg_catalog.pg_database d ON d.datname = pg_catalog.current_database()," " pg_catalog.pg_tablespace t " - "WHERE relkind IN ('r'%s%s) AND " + "WHERE relkind IN ('r', 'm'%s%s) AND " " %s" " t.oid = CASE" " WHEN reltablespace <> 0 THEN reltablespace" @@ -515,7 +515,7 @@ sql_exec_searchtables(PGconn *conn, struct options * opts) " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n" " LEFT JOIN pg_catalog.pg_database d ON d.datname = pg_catalog.current_database(),\n" " pg_catalog.pg_tablespace t \n" - "WHERE relkind IN ('r', 'i', 'S', 't') AND \n" + "WHERE relkind IN ('r', 'm', 'i', 'S', 't') AND \n" " t.oid = CASE\n" " WHEN reltablespace <> 0 THEN reltablespace\n" " ELSE dattablespace\n" diff --git a/contrib/pg_upgrade/info.c b/contrib/pg_upgrade/info.c index 1905c4399d..a5aa40f625 100644 --- a/contrib/pg_upgrade/info.c +++ b/contrib/pg_upgrade/info.c @@ -282,7 +282,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo) "CREATE TEMPORARY TABLE info_rels (reloid) AS SELECT c.oid " "FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n " " ON c.relnamespace = n.oid " - "WHERE relkind IN ('r', 'i'%s) AND " + "WHERE relkind IN ('r', 'm', 'i'%s) AND " /* exclude possible orphaned temp tables */ " ((n.nspname !~ '^pg_temp_' AND " " n.nspname !~ '^pg_toast_temp_' AND " diff --git a/contrib/pg_upgrade/pg_upgrade.c b/contrib/pg_upgrade/pg_upgrade.c index cd6497c220..489b68003c 100644 --- a/contrib/pg_upgrade/pg_upgrade.c +++ b/contrib/pg_upgrade/pg_upgrade.c @@ -525,8 +525,8 @@ set_frozenxids(void) PQclear(executeQueryOrDie(conn, "UPDATE pg_catalog.pg_class " "SET relfrozenxid = '%u' " - /* only heap and TOAST are vacuumed */ - "WHERE relkind IN ('r', 't')", + /* only heap, materialized view, and TOAST are vacuumed */ + "WHERE relkind IN ('r', 'm', 't')", old_cluster.controldata.chkpnt_nxtxid)); PQfinish(conn); diff --git a/contrib/pg_upgrade/version_old_8_3.c b/contrib/pg_upgrade/version_old_8_3.c index 4551d68ba4..e244dcf9ea 100644 --- a/contrib/pg_upgrade/version_old_8_3.c +++ b/contrib/pg_upgrade/version_old_8_3.c @@ -145,6 +145,7 @@ old_8_3_check_for_tsquery_usage(ClusterInfo *cluster) "FROM pg_catalog.pg_class c, " " pg_catalog.pg_namespace n, " " pg_catalog.pg_attribute a " + /* materialized views didn't exist in 8.3, so no need to check 'm' */ "WHERE c.relkind = 'r' AND " " c.oid = a.attrelid AND " " NOT a.attisdropped AND " @@ -323,6 +324,7 @@ old_8_3_rebuild_tsvector_tables(ClusterInfo *cluster, bool check_mode) "FROM pg_catalog.pg_class c, " " pg_catalog.pg_namespace n, " " pg_catalog.pg_attribute a " + /* materialized views didn't exist in 8.3, so no need to check 'm' */ "WHERE c.relkind = 'r' AND " " c.oid = a.attrelid AND " " NOT a.attisdropped AND " @@ -343,6 +345,7 @@ old_8_3_rebuild_tsvector_tables(ClusterInfo *cluster, bool check_mode) "FROM pg_catalog.pg_class c, " \ " pg_catalog.pg_namespace n, " \ " pg_catalog.pg_attribute a " \ + /* materialized views didn't exist in 8.3, so no need to check 'm' */ \ "WHERE c.relkind = 'r' AND " \ " c.oid = a.attrelid AND " \ " NOT a.attisdropped AND " \ diff --git a/contrib/pgstattuple/pgstattuple.c b/contrib/pgstattuple/pgstattuple.c index 8d4fd8b0c9..7f41ec3ad9 100644 --- a/contrib/pgstattuple/pgstattuple.c +++ b/contrib/pgstattuple/pgstattuple.c @@ -216,6 +216,7 @@ pgstat_relation(Relation rel, FunctionCallInfo fcinfo) switch (rel->rd_rel->relkind) { case RELKIND_RELATION: + case RELKIND_MATVIEW: case RELKIND_TOASTVALUE: case RELKIND_SEQUENCE: return pgstat_heap(rel, fcinfo); diff --git a/contrib/vacuumlo/vacuumlo.c b/contrib/vacuumlo/vacuumlo.c index 107eaf9fa1..607849c912 100644 --- a/contrib/vacuumlo/vacuumlo.c +++ b/contrib/vacuumlo/vacuumlo.c @@ -209,7 +209,7 @@ vacuumlo(const char *database, const struct _param * param) strcat(buf, " AND a.atttypid = t.oid "); strcat(buf, " AND c.relnamespace = s.oid "); strcat(buf, " AND t.typname in ('oid', 'lo') "); - strcat(buf, " AND c.relkind = 'r'"); + strcat(buf, " AND c.relkind in ('r', 'm')"); strcat(buf, " AND s.nspname !~ '^pg_'"); res = PQexec(conn, buf); if (PQresultStatus(res) != PGRES_TUPLES_OK) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 9144eec674..81c1be3567 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1597,8 +1597,8 @@ The catalog pg_class catalogs tables and most everything else that has columns or is otherwise similar to a table. This includes indexes (but see also - pg_index), sequences, views, composite types, - and TOAST tables; see relkind. + pg_index), sequences, views, materialized + views, composite types, and TOAST tables; see relkind. Below, when we mean all of these kinds of objects we speak of relations. Not all columns are meaningful for all relation types. @@ -1789,8 +1789,9 @@ r = ordinary table, i = index, - S = sequence, v = view, c = - composite type, t = TOAST table, + S = sequence, v = view, + m = materialized view, + c = composite type, t = TOAST table, f = foreign table diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 92a79d350a..9b7e967758 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -13743,6 +13743,10 @@ SELECT pg_type_is_visible('myschema.widget'::regtype); pg_tablespace_location + + pg_relation_is_scannable + + pg_typeof @@ -13867,29 +13871,29 @@ SELECT pg_type_is_visible('myschema.widget'::regtype); pg_get_viewdef(view_name) text - get underlying SELECT command for view (deprecated) + get underlying SELECT command for view or materialized view (deprecated) pg_get_viewdef(view_name, pretty_bool) text - get underlying SELECT command for view (deprecated) + get underlying SELECT command for view or materialized view (deprecated) pg_get_viewdef(view_oid) text - get underlying SELECT command for view + get underlying SELECT command for view or materialized view pg_get_viewdef(view_oid, pretty_bool) text - get underlying SELECT command for view + get underlying SELECT command for view or materialized view pg_get_viewdef(view_oid, wrap_column_int) text - get underlying SELECT command for view; - lines with fields are wrapped to specified number of columns, - pretty-printing is implied + get underlying SELECT command for view or + materialized view; lines with fields are wrapped to specified + number of columns, pretty-printing is implied pg_options_to_table(reloptions) @@ -13906,6 +13910,11 @@ SELECT pg_type_is_visible('myschema.widget'::regtype); text get the path in the file system that this tablespace is located in + + pg_relation_is_scannable(relation_oid) + boolean + is the relation scannable; a materialized view which has not been loaded will not be scannable + pg_typeof(any) regtype diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index c61c62f228..5846974feb 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -21,6 +21,7 @@ Complete list of usable sgml source files in this directory. + @@ -63,6 +64,7 @@ Complete list of usable sgml source files in this directory. + @@ -102,6 +104,7 @@ Complete list of usable sgml source files in this directory. + @@ -136,6 +139,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/alter_extension.sgml b/doc/src/sgml/ref/alter_extension.sgml index 60bc747269..2dbba0c0bb 100644 --- a/doc/src/sgml/ref/alter_extension.sgml +++ b/doc/src/sgml/ref/alter_extension.sgml @@ -39,6 +39,7 @@ ALTER EXTENSION name DROP object_name | FOREIGN TABLE object_name | FUNCTION function_name ( [ [ argmode ] [ argname ] argtype [, ...] ] ) | + MATERIALIZED VIEW object_name | OPERATOR operator_name (left_type, right_type) | OPERATOR CLASS object_name USING index_method | OPERATOR FAMILY object_name USING index_method | diff --git a/doc/src/sgml/ref/alter_materialized_view.sgml b/doc/src/sgml/ref/alter_materialized_view.sgml new file mode 100644 index 0000000000..b60451374b --- /dev/null +++ b/doc/src/sgml/ref/alter_materialized_view.sgml @@ -0,0 +1,167 @@ + + + + + ALTER MATERIALIZED VIEW + 7 + SQL - Language Statements + + + + ALTER MATERIALIZED VIEW + change the definition of a materialized view + + + + ALTER MATERIALIZED VIEW + + + + +ALTER MATERIALIZED VIEW [ IF EXISTS ] name + action [, ... ] +ALTER MATERIALIZED VIEW [ IF EXISTS ] name + RENAME [ COLUMN ] column_name TO new_column_name +ALTER MATERIALIZED VIEW [ IF EXISTS ] name + RENAME TO new_name +ALTER MATERIALIZED VIEW [ IF EXISTS ] name + SET SCHEMA new_schema + +where action is one of: + + ALTER [ COLUMN ] column_name SET STATISTICS integer + ALTER [ COLUMN ] column_name SET ( attribute_option = value [, ... ] ) + ALTER [ COLUMN ] column_name RESET ( attribute_option [, ... ] ) + ALTER [ COLUMN ] column_name SET STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN } + CLUSTER ON index_name + SET WITHOUT CLUSTER + SET ( storage_parameter = value [, ... ] ) + RESET ( storage_parameter [, ... ] ) + OWNER TO new_owner + SET TABLESPACE new_tablespace + + + + + Description + + + ALTER MATERIALIZED VIEW changes various auxiliary + properties of an existing materialized view. + + + + You must own the materialized view to use ALTER MATERIALIZED + VIEW. To change a materialized view's schema, you must also have + CREATE privilege on the new schema. + To alter the owner, you must also be a direct or indirect member of the new + owning role, and that role must have CREATE privilege on + the materialized view's schema. (These restrictions enforce that altering + the owner doesn't do anything you couldn't do by dropping and recreating the + materialized view. However, a superuser can alter ownership of any view + anyway.) + + + + The statement subforms and actions available for + ALTER MATERIALIZED VIEW are a subset of those available + for ALTER TABLE, and have the same meaning when used for + materialized views. See the descriptions for + for details. + + + + + Parameters + + + + + name + + + The name (optionally schema-qualified) of an existing materialized view. + + + + + + column_name + + + Name of a new or existing column. + + + + + + new_column_name + + + New name for an existing column. + + + + + + new_owner + + + The user name of the new owner of the materialized view. + + + + + + new_name + + + The new name for the materialized view. + + + + + + new_schema + + + The new schema for the materialized view. + + + + + + + + Examples + + + To rename the materialized view foo to + bar: + +ALTER MATERIALIZED VIEW foo RENAME TO bar; + + + + + Compatibility + + + ALTER MATERIALIZED VIEW is a + PostgreSQL extension. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index a03f15cd56..e94dd4b8de 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -38,6 +38,7 @@ COMMENT ON FUNCTION function_name ( [ [ argmode ] [ argname ] argtype [, ...] ] ) | INDEX object_name | LARGE OBJECT large_object_oid | + MATERIALIZED VIEW object_name | OPERATOR operator_name (left_type, right_type) | OPERATOR CLASS object_name USING index_method | OPERATOR FAMILY object_name USING index_method | @@ -279,6 +280,7 @@ COMMENT ON FUNCTION my_function (timestamp) IS 'Returns Roman Numeral'; COMMENT ON INDEX my_index IS 'Enforces uniqueness on employee ID'; COMMENT ON LANGUAGE plpython IS 'Python support for stored procedures'; COMMENT ON LARGE OBJECT 346344 IS 'Planning document'; +COMMENT ON MATERIALIZED VIEW my_matview IS 'Summary of order history'; COMMENT ON OPERATOR ^ (text, text) IS 'Performs intersection of two texts'; COMMENT ON OPERATOR - (NONE, integer) IS 'Unary minus'; COMMENT ON OPERATOR CLASS int4ops USING btree IS '4 byte integer operators for btrees'; diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml index d800701ff4..01faa3afcf 100644 --- a/doc/src/sgml/ref/create_index.sgml +++ b/doc/src/sgml/ref/create_index.sgml @@ -33,8 +33,8 @@ CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ nameDescription - CREATE INDEX constructs an index - on the specified column(s) of the specified table. + CREATE INDEX constructs an index on the specified column(s) + of the specified relation, which can be a table or a materialized view. Indexes are primarily used to enhance database performance (though inappropriate use can result in slower performance). diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml new file mode 100644 index 0000000000..ed3bb4d3ae --- /dev/null +++ b/doc/src/sgml/ref/create_materialized_view.sgml @@ -0,0 +1,154 @@ + + + + + CREATE MATERIALIZED VIEW + 7 + SQL - Language Statements + + + + CREATE MATERIALIZED VIEW + define a new materialized view + + + + CREATE MATERIALIZED VIEW + + + + +CREATE [ UNLOGGED ] MATERIALIZED VIEW table_name + [ (column_name [, ...] ) ] + [ WITH ( storage_parameter [= value] [, ... ] ) ] + [ TABLESPACE tablespace_name ] + AS query + [ WITH [ NO ] DATA ] + + + + + Description + + + CREATE MATERIALIZED VIEW defines a materialized view of + a query. The query is executed and used to populate the view at the time + the command is issued (unless WITH NO DATA is used) and may be + refreshed later using REFRESH MATERIALIZED VIEW. + + + + CREATE MATERIALIZED VIEW is similar to + CREATE TABLE AS, except that it also remembers the query used + to initialize the view, so that it can be refreshed later upon demand. + + + + + Parameters + + + + UNLOGGED + + + If specified, the materialized view will be unlogged. + Refer to for details. + + + + + + table_name + + + The name (optionally schema-qualified) of the materialized view to be + created. + + + + + + column_name + + + The name of a column in the new materialized view. If column names are + not provided, they are taken from the output column names of the query. + + + + + + WITH ( storage_parameter [= value] [, ... ] ) + + + This clause specifies optional storage parameters for the new + materialized view; see for more + information. + See for more information. + + + + + + TABLESPACE tablespace_name + + + The tablespace_name is the name + of the tablespace in which the new materialized view is to be created. + If not specified, is consulted. + + + + + + query + + + A , TABLE, + or command. + + + + + + WITH [ NO ] DATA + + + This clause specifies whether or not the materialized view should be + populated at creation time. If not, the materialized view will be + flagged as unscannable and cannot be queried until REFRESH + MATERIALIZED VIEW is used. + + + + + + + + + Compatibility + + + CREATE MATERIALIZED VIEW is a + PostgreSQL extension. + + + + + See Also + + + + + + + + + + + diff --git a/doc/src/sgml/ref/create_table_as.sgml b/doc/src/sgml/ref/create_table_as.sgml index 9739417a70..29c80405bf 100644 --- a/doc/src/sgml/ref/create_table_as.sgml +++ b/doc/src/sgml/ref/create_table_as.sgml @@ -340,6 +340,7 @@ CREATE TEMP TABLE films_recent WITH (OIDS) ON COMMIT DROP AS See Also + diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml index 0745e3cdb5..aa3fc1515a 100644 --- a/doc/src/sgml/ref/create_view.sgml +++ b/doc/src/sgml/ref/create_view.sgml @@ -379,6 +379,7 @@ CREATE VIEW name [ ( See Also + diff --git a/doc/src/sgml/ref/drop_materialized_view.sgml b/doc/src/sgml/ref/drop_materialized_view.sgml new file mode 100644 index 0000000000..80d8acea36 --- /dev/null +++ b/doc/src/sgml/ref/drop_materialized_view.sgml @@ -0,0 +1,114 @@ + + + + + DROP MATERIALIZED VIEW + 7 + SQL - Language Statements + + + + DROP MATERIALIZED VIEW + remove a materialized view + + + + DROP MATERIALIZED VIEW + + + + +DROP MATERIALIZED VIEW [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] + + + + + Description + + + DROP MATERIALIZED VIEW drops an existing materialized + view. To execute this command you must be the owner of the materialized + view. + + + + + Parameters + + + + IF EXISTS + + + Do not throw an error if the materialized view does not exist. A notice + is issued in this case. + + + + + + name + + + The name (optionally schema-qualified) of the materialized view to + remove. + + + + + + CASCADE + + + Automatically drop objects that depend on the materialized view (such as + other materialized views, or regular views). + + + + + + RESTRICT + + + Refuse to drop the materialized view if any objects depend on it. This + is the default. + + + + + + + + Examples + + + This command will remove the materialized view called + order_summary: + +DROP MATERIALIZED VIEW order_summary; + + + + + Compatibility + + + DROP MATERIALIZED VIEW is a + PostgreSQL extension. + + + + + See Also + + + + + + + + + diff --git a/doc/src/sgml/ref/refresh_materialized_view.sgml b/doc/src/sgml/ref/refresh_materialized_view.sgml new file mode 100644 index 0000000000..44cff9c98e --- /dev/null +++ b/doc/src/sgml/ref/refresh_materialized_view.sgml @@ -0,0 +1,113 @@ + + + + + REFRESH MATERIALIZED VIEW + 7 + SQL - Language Statements + + + + REFRESH MATERIALIZED VIEW + replace the contents of a materialized view + + + + REFRESH MATERIALIZED VIEW + + + + +REFRESH MATERIALIZED VIEW name + [ WITH [ NO ] DATA ] + + + + + Description + + + REFRESH MATERIALIZED VIEW completely replaces the + contents of a materialized view. The old contents are discarded. If + WITH DATA is specified (or defaults) the backing query + is executed to provide the new data, and the materialized view is left in a + scannable state. If WITH NO DATA is specified no new + data is generated and the materialized view is left in an unscannable + state. + + + + + Parameters + + + + name + + + The name (optionally schema-qualified) of the materialized view to + refresh. + + + + + + + + Notes + + + While the default index for future + + operations is retained, REFRESH MATERIALIZED VIEW does not + order the generated rows based on this property. If you want the data + to be ordered upon generation, you must use an ORDER BY + clause in the backing query. + + + + + Examples + + + This command will replace the contents of the materialized view called + order_summary using the query from the materialized + view's definition, and leave it in a scannable state: + +REFRESH MATERIALIZED VIEW order_summary; + + + + + This command will free storage associated with the materialized view + annual_statistics_basis and leave it in an unscannable + state: + +REFRESH MATERIALIZED VIEW annual_statistics_basis WITH NO DATA; + + + + + + Compatibility + + + REFRESH MATERIALIZED VIEW is a + PostgreSQL extension. + + + + + See Also + + + + + + + + + diff --git a/doc/src/sgml/ref/security_label.sgml b/doc/src/sgml/ref/security_label.sgml index d946b92e19..52cb1d16f4 100644 --- a/doc/src/sgml/ref/security_label.sgml +++ b/doc/src/sgml/ref/security_label.sgml @@ -32,6 +32,7 @@ SECURITY LABEL [ FOR provider ] ON FOREIGN TABLE object_name FUNCTION function_name ( [ [ argmode ] [ argname ] argtype [, ...] ] ) | LARGE OBJECT large_object_oid | + MATERIALIZED VIEW object_name | [ PROCEDURAL ] LANGUAGE object_name | ROLE object_name | SCHEMA object_name | diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 5b0c7745e3..14e217a907 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -49,6 +49,7 @@ &alterIndex; &alterLanguage; &alterLargeObject; + &alterMaterializedView; &alterOperator; &alterOperatorClass; &alterOperatorFamily; @@ -91,6 +92,7 @@ &createGroup; &createIndex; &createLanguage; + &createMaterializedView; &createOperator; &createOperatorClass; &createOperatorFamily; @@ -130,6 +132,7 @@ &dropGroup; &dropIndex; &dropLanguage; + &dropMaterializedView; &dropOperator; &dropOperatorClass; &dropOperatorFamily; @@ -164,6 +167,7 @@ &prepare; &prepareTransaction; &reassignOwned; + &refreshMaterializedView; &reindex; &releaseSavepoint; &reset; diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml index 5811de7942..68a0f53445 100644 --- a/doc/src/sgml/rules.sgml +++ b/doc/src/sgml/rules.sgml @@ -893,6 +893,206 @@ SELECT t1.a, t2.b, t1.ctid FROM t1, t2 WHERE t1.a = t2.a; + +Materialized Views + + + rule + and materialized views + + + + materialized view + implementation through rules + + + + view + materialized + + + + Materialized views in PostgreSQL use the + rule system like views do, but persist the results in a table-like form. + The main differences between: + + +CREATE MATERIALIZED VIEW mymatview AS SELECT * FROM mytab; + + + and: + + +CREATE TABLE mymatview AS SELECT * FROM mytab; + + + are that the materialized view cannot subsequently be directly updated + and that the query used to create the materialized view is stored in + exactly the same way that a view's query is stored, so that fresh data + can be generated for the materialized view with: + + +REFRESH MATERIALIZED VIEW mymatview; + + + The information about a materialized view in the + PostgreSQL system catalogs is exactly + the same as it is for a table or view. So for the parser, a + materialized view is a relation, just like a table or a view. When + a materialized view is referenced in a query, the data is returned + directly from the materialized view, like from a table; the rule is + only used for populating the materialized view. + + + + While access to the data stored in a materialized view is often much + faster than accessing the underlying tables directly or through a view, + the data is not always current; yet sometimes current data is not needed. + Consider a table which records sales: + + +CREATE TABLE invoice ( + invoice_no integer PRIMARY KEY, + seller_no integer, -- ID of salesperson + invoice_date date, -- date of sale + invoice_amt numeric(13,2) -- amount of sale +); + + + If people want to be able to quickly graph historical sales data, they + might want to summarize, and they may not care about the incomplete data + for the current date: + + +CREATE MATERIALIZED VIEW sales_summary AS + SELECT + seller_no, + invoice_date, + sum(invoice_amt)::numeric(13,2) as sales_amt + FROM invoice + WHERE invoice_date < CURRENT_DATE + GROUP BY + seller_no, + invoice_date + ORDER BY + seller_no, + invoice_date; + +CREATE UNIQUE INDEX sales_summary_seller + ON sales_summary (seller_no, invoice_date); + + + This materialized view might be useful for displaying a graph in the + dashboard created for salespeople. A job could be scheduled to update + the statistics each night using this SQL statement: + + +REFRESH MATERIALIZED VIEW sales_summary; + + + + + Another use for a materialized view is to allow faster access to data + brought across from a remote system, through a foreign data wrapper. + A simple example using file_fdw is below, with timings, + but since this is using cache on the local system the performance + difference on a foreign data wrapper to a remote system could be greater. + + Setup: + + +CREATE EXTENSION file_fdw; +CREATE SERVER local_file FOREIGN DATA WRAPPER file_fdw ; +CREATE FOREIGN TABLE words (word text NOT NULL) + SERVER local_file + OPTIONS (filename '/etc/dictionaries-common/words'); +CREATE MATERIALIZED VIEW wrd AS SELECT * FROM words; +CREATE UNIQUE INDEX wrd_word ON wrd (word); +CREATE EXTENSION pg_trgm ; +CREATE INDEX wrd_trgm ON wrd USING gist (word gist_trgm_ops); +VACUUM ANALYZE wrd; + + + Now let's spell-check a word. Using file_fdw directly: + + +SELECT count(*) FROM words WHERE word = 'caterpiler'; + + count +------- + 0 +(1 row) + + + The plan is: + + + Aggregate (cost=4125.19..4125.20 rows=1 width=0) (actual time=26.013..26.014 rows=1 loops=1) + -> Foreign Scan on words (cost=0.00..4124.70 rows=196 width=0) (actual time=26.011..26.011 rows=0 loops=1) + Filter: (word = 'caterpiler'::text) + Rows Removed by Filter: 99171 + Foreign File: /etc/dictionaries-common/words + Foreign File Size: 938848 + Total runtime: 26.081 ms + + + If the materialized view is used instead, the query is much faster: + + + Aggregate (cost=4.44..4.45 rows=1 width=0) (actual time=0.074..0.074 rows=1 loops=1) + -> Index Only Scan using wrd_word on wrd (cost=0.42..4.44 rows=1 width=0) (actual time=0.071..0.071 rows=0 loops=1) + Index Cond: (word = 'caterpiler'::text) + Heap Fetches: 0 + Total runtime: 0.119 ms + + + Either way, the word is spelled wrong, so let's look for what we might + have wanted. Again using file_fdw: + + +SELECT word FROM words ORDER BY word <-> 'caterpiler' LIMIT 10; + + word +--------------- + cater + caterpillar + Caterpillar + caterpillars + caterpillar's + Caterpillar's + caterer + caterer's + caters + catered +(10 rows) + + + + Limit (cost=2195.70..2195.72 rows=10 width=32) (actual time=218.904..218.906 rows=10 loops=1) + -> Sort (cost=2195.70..2237.61 rows=16765 width=32) (actual time=218.902..218.904 rows=10 loops=1) + Sort Key: ((word <-> 'caterpiler'::text)) + Sort Method: top-N heapsort Memory: 25kB + -> Foreign Scan on words (cost=0.00..1833.41 rows=16765 width=32) (actual time=0.046..200.965 rows=99171 loops=1) + Foreign File: /etc/dictionaries-common/words + Foreign File Size: 938848 + Total runtime: 218.966 ms + + + Using the materialized view: + + + Limit (cost=0.28..1.02 rows=10 width=9) (actual time=24.916..25.079 rows=10 loops=1) + -> Index Scan using wrd_trgm on wrd (cost=0.28..7383.70 rows=99171 width=9) (actual time=24.914..25.076 rows=10 loops=1) + Order By: (word <-> 'caterpiler'::text) + Total runtime: 25.884 ms + + + If you can tolerate periodic update of the remote data to the local + database, the performance benefit can be substantial. + + + + Rules on <command>INSERT</>, <command>UPDATE</>, and <command>DELETE</> diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index 456d7462ad..c439702a01 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -791,6 +791,7 @@ extractRelOptions(HeapTuple tuple, TupleDesc tupdesc, Oid amoptions) case RELKIND_RELATION: case RELKIND_TOASTVALUE: case RELKIND_VIEW: + case RELKIND_MATVIEW: options = heap_reloptions(classForm->relkind, datum, false); break; case RELKIND_INDEX: @@ -1191,6 +1192,7 @@ heap_reloptions(char relkind, Datum reloptions, bool validate) } return (bytea *) rdopts; case RELKIND_RELATION: + case RELKIND_MATVIEW: return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP); case RELKIND_VIEW: return default_reloptions(reloptions, validate, RELOPT_KIND_VIEW); diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index d226726654..5250ec7f41 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -2214,7 +2214,8 @@ heap_prepare_insert(Relation relation, HeapTuple tup, TransactionId xid, * If the new tuple is too big for storage or contains already toasted * out-of-line attributes from some other relation, invoke the toaster. */ - if (relation->rd_rel->relkind != RELKIND_RELATION) + if (relation->rd_rel->relkind != RELKIND_RELATION && + relation->rd_rel->relkind != RELKIND_MATVIEW) { /* toast table entries should never be recursively toasted */ Assert(!HeapTupleHasExternal(tup)); @@ -2802,7 +2803,8 @@ l1: * because we need to look at the contents of the tuple, but it's OK to * release the content lock on the buffer first. */ - if (relation->rd_rel->relkind != RELKIND_RELATION) + if (relation->rd_rel->relkind != RELKIND_RELATION && + relation->rd_rel->relkind != RELKIND_MATVIEW) { /* toast table entries should never be recursively toasted */ Assert(!HeapTupleHasExternal(&tp)); @@ -3346,7 +3348,8 @@ l2: * We need to invoke the toaster if there are already any out-of-line * toasted values present, or if the new tuple is over-threshold. */ - if (relation->rd_rel->relkind != RELKIND_RELATION) + if (relation->rd_rel->relkind != RELKIND_RELATION && + relation->rd_rel->relkind != RELKIND_MATVIEW) { /* toast table entries should never be recursively toasted */ Assert(!HeapTupleHasExternal(&oldtup)); diff --git a/src/backend/access/heap/tuptoaster.c b/src/backend/access/heap/tuptoaster.c index 49f155346c..fc37ceb4a3 100644 --- a/src/backend/access/heap/tuptoaster.c +++ b/src/backend/access/heap/tuptoaster.c @@ -353,10 +353,11 @@ toast_delete(Relation rel, HeapTuple oldtup) bool toast_isnull[MaxHeapAttributeNumber]; /* - * We should only ever be called for tuples of plain relations --- - * recursing on a toast rel is bad news. + * We should only ever be called for tuples of plain relations or + * materialized views --- recursing on a toast rel is bad news. */ - Assert(rel->rd_rel->relkind == RELKIND_RELATION); + Assert(rel->rd_rel->relkind == RELKIND_RELATION || + rel->rd_rel->relkind == RELKIND_MATVIEW); /* * Get the tuple descriptor and break down the tuple into fields. @@ -443,7 +444,8 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup, * We should only ever be called for tuples of plain relations --- * recursing on a toast rel is bad news. */ - Assert(rel->rd_rel->relkind == RELKIND_RELATION); + Assert(rel->rd_rel->relkind == RELKIND_RELATION || + rel->rd_rel->relkind == RELKIND_MATVIEW); /* * Get the tuple descriptor and break down the tuple(s) into fields. diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index b3f5ba0e6e..340350fa39 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -765,6 +765,8 @@ objectsInSchemaToOids(GrantObjectType objtype, List *nspnames) objects = list_concat(objects, objs); objs = getRelationsInNamespace(namespaceId, RELKIND_VIEW); objects = list_concat(objects, objs); + objs = getRelationsInNamespace(namespaceId, RELKIND_MATVIEW); + objects = list_concat(objects, objs); objs = getRelationsInNamespace(namespaceId, RELKIND_FOREIGN_TABLE); objects = list_concat(objects, objs); break; diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index d2037251ae..32f05bbabb 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -3024,6 +3024,10 @@ getRelationDescription(StringInfo buffer, Oid relid) appendStringInfo(buffer, _("view %s"), relname); break; + case RELKIND_MATVIEW: + appendStringInfo(buffer, _("materialized view %s"), + relname); + break; case RELKIND_COMPOSITE_TYPE: appendStringInfo(buffer, _("composite type %s"), relname); diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index db51e0b608..0ecfc78ed0 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -835,6 +835,7 @@ AddNewRelationTuple(Relation pg_class_desc, switch (relkind) { case RELKIND_RELATION: + case RELKIND_MATVIEW: case RELKIND_INDEX: case RELKIND_TOASTVALUE: /* The relation is real, but as yet empty */ @@ -858,6 +859,7 @@ AddNewRelationTuple(Relation pg_class_desc, /* Initialize relfrozenxid and relminmxid */ if (relkind == RELKIND_RELATION || + relkind == RELKIND_MATVIEW || relkind == RELKIND_TOASTVALUE) { /* @@ -1069,8 +1071,8 @@ heap_create_with_catalog(const char *relname, if (IsBinaryUpgrade && OidIsValid(binary_upgrade_next_heap_pg_class_oid) && (relkind == RELKIND_RELATION || relkind == RELKIND_SEQUENCE || - relkind == RELKIND_VIEW || relkind == RELKIND_COMPOSITE_TYPE || - relkind == RELKIND_FOREIGN_TABLE)) + relkind == RELKIND_VIEW || relkind == RELKIND_MATVIEW || + relkind == RELKIND_COMPOSITE_TYPE || relkind == RELKIND_FOREIGN_TABLE)) { relid = binary_upgrade_next_heap_pg_class_oid; binary_upgrade_next_heap_pg_class_oid = InvalidOid; @@ -1096,6 +1098,7 @@ heap_create_with_catalog(const char *relname, { case RELKIND_RELATION: case RELKIND_VIEW: + case RELKIND_MATVIEW: case RELKIND_FOREIGN_TABLE: relacl = get_user_default_acl(ACL_OBJECT_RELATION, ownerid, relnamespace); @@ -1139,6 +1142,7 @@ heap_create_with_catalog(const char *relname, */ if (IsUnderPostmaster && (relkind == RELKIND_RELATION || relkind == RELKIND_VIEW || + relkind == RELKIND_MATVIEW || relkind == RELKIND_FOREIGN_TABLE || relkind == RELKIND_COMPOSITE_TYPE)) new_array_oid = AssignTypeArrayOid(); @@ -1316,7 +1320,8 @@ heap_create_with_catalog(const char *relname, if (relpersistence == RELPERSISTENCE_UNLOGGED) { - Assert(relkind == RELKIND_RELATION || relkind == RELKIND_TOASTVALUE); + Assert(relkind == RELKIND_RELATION || relkind == RELKIND_MATVIEW || + relkind == RELKIND_TOASTVALUE); heap_create_init_fork(new_rel_desc); } @@ -1347,6 +1352,26 @@ heap_create_init_fork(Relation rel) smgrimmedsync(rel->rd_smgr, INIT_FORKNUM); } +/* + * Check whether a materialized view is in an initial, unloaded state. + * + * The check here must match what is set up in heap_create_init_fork(). + * Currently the init fork is an empty file. A missing heap is also + * considered to be unloaded. + */ +bool +heap_is_matview_init_state(Relation rel) +{ + Assert(rel->rd_rel->relkind == RELKIND_MATVIEW); + + RelationOpenSmgr(rel); + + if (!smgrexists(rel->rd_smgr, MAIN_FORKNUM)) + return true; + + return (smgrnblocks(rel->rd_smgr, MAIN_FORKNUM) < 1); +} + /* * RelationRemoveInheritance * diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index d902f9d6ba..6f60d7cad1 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -444,6 +444,7 @@ get_object_address(ObjectType objtype, List *objname, List *objargs, case OBJECT_SEQUENCE: case OBJECT_TABLE: case OBJECT_VIEW: + case OBJECT_MATVIEW: case OBJECT_FOREIGN_TABLE: address = get_relation_by_qualified_name(objtype, objname, @@ -816,6 +817,13 @@ get_relation_by_qualified_name(ObjectType objtype, List *objname, errmsg("\"%s\" is not a view", RelationGetRelationName(relation)))); break; + case OBJECT_MATVIEW: + if (relation->rd_rel->relkind != RELKIND_MATVIEW) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a materialized view", + RelationGetRelationName(relation)))); + break; case OBJECT_FOREIGN_TABLE: if (relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, @@ -1073,6 +1081,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_SEQUENCE: case OBJECT_TABLE: case OBJECT_VIEW: + case OBJECT_MATVIEW: case OBJECT_FOREIGN_TABLE: case OBJECT_COLUMN: case OBJECT_RULE: diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index c479c23683..f727acd68f 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -94,6 +94,19 @@ CREATE VIEW pg_tables AS LEFT JOIN pg_tablespace T ON (T.oid = C.reltablespace) WHERE C.relkind = 'r'; +CREATE VIEW pg_matviews AS + SELECT + N.nspname AS schemaname, + C.relname AS matviewname, + pg_get_userbyid(C.relowner) AS matviewowner, + T.spcname AS tablespace, + C.relhasindex AS hasindexes, + pg_relation_is_scannable(C.oid) AS isscannable, + pg_get_viewdef(C.oid) AS definition + FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) + LEFT JOIN pg_tablespace T ON (T.oid = C.reltablespace) + WHERE C.relkind = 'm'; + CREATE VIEW pg_indexes AS SELECT N.nspname AS schemaname, @@ -105,7 +118,7 @@ CREATE VIEW pg_indexes AS JOIN pg_class I ON (I.oid = X.indexrelid) LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) LEFT JOIN pg_tablespace T ON (T.oid = I.reltablespace) - WHERE C.relkind = 'r' AND I.relkind = 'i'; + WHERE C.relkind IN ('r', 'm') AND I.relkind = 'i'; CREATE VIEW pg_stats AS SELECT @@ -206,6 +219,7 @@ SELECT l.objoid, l.classoid, l.objsubid, CASE WHEN rel.relkind = 'r' THEN 'table'::text WHEN rel.relkind = 'v' THEN 'view'::text + WHEN rel.relkind = 'm' THEN 'materialized view'::text WHEN rel.relkind = 'S' THEN 'sequence'::text WHEN rel.relkind = 'f' THEN 'foreign table'::text END AS objtype, rel.relnamespace AS objnamespace, @@ -402,7 +416,7 @@ CREATE VIEW pg_stat_all_tables AS FROM pg_class C LEFT JOIN pg_index I ON C.oid = I.indrelid LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) - WHERE C.relkind IN ('r', 't') + WHERE C.relkind IN ('r', 't', 'm') GROUP BY C.oid, N.nspname, C.relname; CREATE VIEW pg_stat_xact_all_tables AS @@ -422,7 +436,7 @@ CREATE VIEW pg_stat_xact_all_tables AS FROM pg_class C LEFT JOIN pg_index I ON C.oid = I.indrelid LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) - WHERE C.relkind IN ('r', 't') + WHERE C.relkind IN ('r', 't', 'm') GROUP BY C.oid, N.nspname, C.relname; CREATE VIEW pg_stat_sys_tables AS @@ -467,7 +481,7 @@ CREATE VIEW pg_statio_all_tables AS pg_class T ON C.reltoastrelid = T.oid LEFT JOIN pg_class X ON T.reltoastidxid = X.oid LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) - WHERE C.relkind IN ('r', 't') + WHERE C.relkind IN ('r', 't', 'm') GROUP BY C.oid, N.nspname, C.relname, T.oid, X.oid; CREATE VIEW pg_statio_sys_tables AS @@ -494,7 +508,7 @@ CREATE VIEW pg_stat_all_indexes AS pg_index X ON C.oid = X.indrelid JOIN pg_class I ON I.oid = X.indexrelid LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) - WHERE C.relkind IN ('r', 't'); + WHERE C.relkind IN ('r', 't', 'm'); CREATE VIEW pg_stat_sys_indexes AS SELECT * FROM pg_stat_all_indexes @@ -520,7 +534,7 @@ CREATE VIEW pg_statio_all_indexes AS pg_index X ON C.oid = X.indrelid JOIN pg_class I ON I.oid = X.indexrelid LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) - WHERE C.relkind IN ('r', 't'); + WHERE C.relkind IN ('r', 't', 'm'); CREATE VIEW pg_statio_sys_indexes AS SELECT * FROM pg_statio_all_indexes diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c index 7c4ccbdbc0..385d64d4c0 100644 --- a/src/backend/catalog/toasting.c +++ b/src/backend/catalog/toasting.c @@ -84,10 +84,11 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid) rel = heap_openrv(makeRangeVar(NULL, relName, -1), AccessExclusiveLock); - if (rel->rd_rel->relkind != RELKIND_RELATION) + if (rel->rd_rel->relkind != RELKIND_RELATION && + rel->rd_rel->relkind != RELKIND_MATVIEW) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table", + errmsg("\"%s\" is not a table or materialized view", relName))); /* create_toast_table does all the work */ diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 3c322a3441..22f116b78d 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -16,7 +16,7 @@ OBJS = aggregatecmds.o alter.o analyze.o async.o cluster.o comment.o \ collationcmds.o constraint.o conversioncmds.o copy.o createas.o \ dbcommands.o define.o discard.o dropcmds.o \ event_trigger.o explain.o extension.o foreigncmds.o functioncmds.o \ - indexcmds.o lockcmds.o operatorcmds.o opclasscmds.o \ + indexcmds.o lockcmds.o matview.o operatorcmds.o opclasscmds.o \ portalcmds.o prepare.o proclang.o \ schemacmds.o seclabel.o sequence.o tablecmds.o tablespace.o trigger.o \ tsearchcmds.o typecmds.o user.o vacuum.o vacuumlazy.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index 269d19cea6..416a068fc7 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -317,6 +317,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TABLE: case OBJECT_SEQUENCE: case OBJECT_VIEW: + case OBJECT_MATVIEW: case OBJECT_INDEX: case OBJECT_FOREIGN_TABLE: return RenameRelation(stmt); @@ -393,6 +394,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt) case OBJECT_SEQUENCE: case OBJECT_TABLE: case OBJECT_VIEW: + case OBJECT_MATVIEW: return AlterTableNamespace(stmt); case OBJECT_DOMAIN: diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c index d7b17a5aba..ad9c911542 100644 --- a/src/backend/commands/analyze.c +++ b/src/backend/commands/analyze.c @@ -206,11 +206,12 @@ analyze_rel(Oid relid, VacuumStmt *vacstmt, BufferAccessStrategy bstrategy) } /* - * Check that it's a plain table or foreign table; we used to do this in - * get_rel_oids() but seems safer to check after we've locked the - * relation. + * Check that it's a plain table, materialized view, or foreign table; we + * used to do this in get_rel_oids() but seems safer to check after we've + * locked the relation. */ - if (onerel->rd_rel->relkind == RELKIND_RELATION) + if (onerel->rd_rel->relkind == RELKIND_RELATION || + onerel->rd_rel->relkind == RELKIND_MATVIEW) { /* Regular table, so we'll use the regular row acquisition function */ acquirefunc = acquire_sample_rows; diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index c0cb2f6654..8ab8c17519 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -29,6 +29,7 @@ #include "catalog/namespace.h" #include "catalog/toasting.h" #include "commands/cluster.h" +#include "commands/matview.h" #include "commands/tablecmds.h" #include "commands/vacuum.h" #include "miscadmin.h" @@ -378,6 +379,19 @@ cluster_rel(Oid tableOid, Oid indexOid, bool recheck, bool verbose, if (OidIsValid(indexOid)) check_index_is_clusterable(OldHeap, indexOid, recheck, AccessExclusiveLock); + /* + * Quietly ignore the request if the a materialized view is not scannable. + * No harm is done because there is nothing no data to deal with, and we + * don't want to throw an error if this is part of a multi-relation + * request -- for example, CLUSTER was run on the entire database. + */ + if (OldHeap->rd_rel->relkind == RELKIND_MATVIEW && + !OldHeap->rd_isscannable) + { + relation_close(OldHeap, AccessExclusiveLock); + return; + } + /* * All predicate locks on the tuples or pages are about to be made * invalid, because we move tuples around. Promote them to relation @@ -901,6 +915,10 @@ copy_heap_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex, get_namespace_name(RelationGetNamespace(OldHeap)), RelationGetRelationName(OldHeap)))); + if (OldHeap->rd_rel->relkind == RELKIND_MATVIEW) + /* Make sure the heap looks good even if no rows are written. */ + SetRelationIsScannable(NewHeap); + /* * Scan through the OldHeap, either in OldIndex order or sequentially; * copy each tuple into the NewHeap, or transiently to the tuplesort diff --git a/src/backend/commands/comment.c b/src/backend/commands/comment.c index 3ec12e7cb9..60db27c205 100644 --- a/src/backend/commands/comment.c +++ b/src/backend/commands/comment.c @@ -83,15 +83,17 @@ CommentObject(CommentStmt *stmt) case OBJECT_COLUMN: /* - * Allow comments only on columns of tables, views, composite - * types, and foreign tables (which are the only relkinds for - * which pg_dump will dump per-column comments). In particular we - * wish to disallow comments on index columns, because the naming - * of an index's columns may change across PG versions, so dumping - * per-column comments could create reload failures. + * Allow comments only on columns of tables, views, materialized + * views, composite types, and foreign tables (which are the only + * relkinds for which pg_dump will dump per-column comments). In + * particular we wish to disallow comments on index columns, + * because the naming of an index's columns may change across PG + * versions, so dumping per-column comments could create reload + * failures. */ if (relation->rd_rel->relkind != RELKIND_RELATION && relation->rd_rel->relkind != RELKIND_VIEW && + relation->rd_rel->relkind != RELKIND_MATVIEW && relation->rd_rel->relkind != RELKIND_COMPOSITE_TYPE && relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index c651ea3028..4825bca363 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -1496,6 +1496,12 @@ BeginCopyTo(Relation rel, errmsg("cannot copy from view \"%s\"", RelationGetRelationName(rel)), errhint("Try the COPY (SELECT ...) TO variant."))); + else if (rel->rd_rel->relkind == RELKIND_MATVIEW) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot copy from materialized view \"%s\"", + RelationGetRelationName(rel)), + errhint("Try the COPY (SELECT ...) TO variant."))); else if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -2016,6 +2022,11 @@ CopyFrom(CopyState cstate) (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot copy to view \"%s\"", RelationGetRelationName(cstate->rel)))); + else if (cstate->rel->rd_rel->relkind == RELKIND_MATVIEW) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot copy to materialized view \"%s\"", + RelationGetRelationName(cstate->rel)))); else if (cstate->rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index 66a49db330..a3ff1d56c8 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -2,6 +2,8 @@ * * createas.c * Execution of CREATE TABLE ... AS, a/k/a SELECT INTO + * Since CREATE MATERIALIZED VIEW shares syntax and most behaviors, + * implement that here, too. * * We implement this by diverting the query's normal output to a * specialized DestReceiver type. @@ -27,8 +29,11 @@ #include "access/xact.h" #include "catalog/toasting.h" #include "commands/createas.h" +#include "commands/matview.h" #include "commands/prepare.h" #include "commands/tablecmds.h" +#include "commands/view.h" +#include "parser/analyze.h" #include "parser/parse_clause.h" #include "rewrite/rewriteHandler.h" #include "storage/smgr.h" @@ -43,6 +48,7 @@ typedef struct { DestReceiver pub; /* publicly-known function pointers */ IntoClause *into; /* target relation specification */ + Query *viewParse; /* the query which defines/populates data */ /* These fields are filled by intorel_startup: */ Relation rel; /* relation to write to */ CommandId output_cid; /* cmin to insert in output tuples */ @@ -56,6 +62,62 @@ static void intorel_shutdown(DestReceiver *self); static void intorel_destroy(DestReceiver *self); +/* + * Common setup needed by both normal execution and EXPLAIN ANALYZE. + */ +Query * +SetupForCreateTableAs(Query *query, IntoClause *into, const char *queryString, + ParamListInfo params, DestReceiver *dest) +{ + List *rewritten; + Query *viewParse = NULL; + + Assert(query->commandType == CMD_SELECT); + + if (into->relkind == RELKIND_MATVIEW) + viewParse = (Query *) parse_analyze((Node *) copyObject(query), + queryString, NULL, 0)->utilityStmt; + + /* + * Parse analysis was done already, but we still have to run the rule + * rewriter. We do not do AcquireRewriteLocks: we assume the query either + * came straight from the parser, or suitable locks were acquired by + * plancache.c. + * + * Because the rewriter and planner tend to scribble on the input, we make + * a preliminary copy of the source querytree. This prevents problems in + * the case that CTAS is in a portal or plpgsql function and is executed + * repeatedly. (See also the same hack in EXPLAIN and PREPARE.) + */ + rewritten = QueryRewrite((Query *) copyObject(query)); + + /* SELECT should never rewrite to more or less than one SELECT query */ + if (list_length(rewritten) != 1) + elog(ERROR, "unexpected rewrite result for CREATE TABLE AS SELECT"); + query = (Query *) linitial(rewritten); + + Assert(query->commandType == CMD_SELECT); + + /* Save the query after rewrite but before planning. */ + ((DR_intorel *) dest)->viewParse = viewParse; + ((DR_intorel *) dest)->into = into; + + if (into->relkind == RELKIND_MATVIEW) + { + /* + * A materialized view would either need to save parameters for use in + * maintaining or loading the data or prohibit them entirely. The + * latter seems safer and more sane. + */ + if (params != NULL && params->numParams > 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("materialized views may not be defined using bound parameters"))); + } + + return query; +} + /* * ExecCreateTableAs -- execute a CREATE TABLE AS command */ @@ -66,7 +128,6 @@ ExecCreateTableAs(CreateTableAsStmt *stmt, const char *queryString, Query *query = (Query *) stmt->query; IntoClause *into = stmt->into; DestReceiver *dest; - List *rewritten; PlannedStmt *plan; QueryDesc *queryDesc; ScanDirection dir; @@ -90,26 +151,8 @@ ExecCreateTableAs(CreateTableAsStmt *stmt, const char *queryString, return; } - Assert(query->commandType == CMD_SELECT); - /* - * Parse analysis was done already, but we still have to run the rule - * rewriter. We do not do AcquireRewriteLocks: we assume the query either - * came straight from the parser, or suitable locks were acquired by - * plancache.c. - * - * Because the rewriter and planner tend to scribble on the input, we make - * a preliminary copy of the source querytree. This prevents problems in - * the case that CTAS is in a portal or plpgsql function and is executed - * repeatedly. (See also the same hack in EXPLAIN and PREPARE.) - */ - rewritten = QueryRewrite((Query *) copyObject(stmt->query)); - - /* SELECT should never rewrite to more or less than one SELECT query */ - if (list_length(rewritten) != 1) - elog(ERROR, "unexpected rewrite result for CREATE TABLE AS SELECT"); - query = (Query *) linitial(rewritten); - Assert(query->commandType == CMD_SELECT); + query = SetupForCreateTableAs(query, into, queryString, params, dest); /* plan the query */ plan = pg_plan_query(query, 0, params); @@ -169,15 +212,21 @@ ExecCreateTableAs(CreateTableAsStmt *stmt, const char *queryString, int GetIntoRelEFlags(IntoClause *intoClause) { + int flags; /* * We need to tell the executor whether it has to produce OIDs or not, * because it doesn't have enough information to do so itself (since we * can't build the target relation until after ExecutorStart). */ if (interpretOidsOption(intoClause->options)) - return EXEC_FLAG_WITH_OIDS; + flags = EXEC_FLAG_WITH_OIDS; else - return EXEC_FLAG_WITHOUT_OIDS; + flags = EXEC_FLAG_WITHOUT_OIDS; + + if (intoClause->skipData) + flags |= EXEC_FLAG_WITH_NO_DATA; + + return flags; } /* @@ -299,12 +348,38 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) if (lc != NULL) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("CREATE TABLE AS specifies too many column names"))); + errmsg("too many column names are specified"))); + + /* + * Enforce validations needed for materialized views only. + */ + if (into->relkind == RELKIND_MATVIEW) + { + /* + * Prohibit a data-modifying CTE in the query used to create a + * materialized view. It's not sufficiently clear what the user would + * want to happen if the MV is refreshed or incrementally maintained. + */ + if (myState->viewParse->hasModifyingCTE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("materialized views must not use data-modifying statements in WITH"))); + + /* + * Check whether any temporary database objects are used in the + * creation query. It would be hard to refresh data or incrementally + * maintain it if a source disappeared. + */ + if (isQueryUsingTempRelation(myState->viewParse)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("materialized views must not use temporary tables or views"))); + } /* * Actually create the target table */ - intoRelationId = DefineRelation(create, RELKIND_RELATION, InvalidOid); + intoRelationId = DefineRelation(create, into->relkind, InvalidOid); /* * If necessary, create a TOAST table for the target table. Note that @@ -324,11 +399,22 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) AlterTableCreateToastTable(intoRelationId, toast_options); + /* Create the "view" part of a materialized view. */ + if (into->relkind == RELKIND_MATVIEW) + { + StoreViewQuery(intoRelationId, myState->viewParse, false); + CommandCounterIncrement(); + } + /* * Finally we can open the target table */ intoRelationDesc = heap_open(intoRelationId, AccessExclusiveLock); + if (into->relkind == RELKIND_MATVIEW && !into->skipData) + /* Make sure the heap looks good even if no rows are written. */ + SetRelationIsScannable(intoRelationDesc); + /* * Check INSERT permission on the constructed table. * @@ -338,7 +424,8 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) rte = makeNode(RangeTblEntry); rte->rtekind = RTE_RELATION; rte->relid = intoRelationId; - rte->relkind = RELKIND_RELATION; + rte->relkind = into->relkind; + rte->isResultRel = true; rte->requiredPerms = ACL_INSERT; for (attnum = 1; attnum <= intoRelationDesc->rd_att->natts; attnum++) diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index 18b37537c0..596178fbda 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -67,6 +67,7 @@ static event_trigger_support_data event_trigger_support[] = { { "FUNCTION", true }, { "INDEX", true }, { "LANGUAGE", true }, + { "MATERIALIZED VIEW", true }, { "OPERATOR", true }, { "OPERATOR CLASS", true }, { "OPERATOR FAMILY", true }, @@ -217,6 +218,7 @@ check_ddl_tag(const char *tag) */ if (pg_strcasecmp(tag, "CREATE TABLE AS") == 0 || pg_strcasecmp(tag, "SELECT INTO") == 0 || + pg_strcasecmp(tag, "REFRESH MATERIALIZED VIEW") == 0 || pg_strcasecmp(tag, "ALTER DEFAULT PRIVILEGES") == 0 || pg_strcasecmp(tag, "ALTER LARGE OBJECT") == 0) return EVENT_TRIGGER_COMMAND_TAG_OK; diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index fbad0d027a..989b52da9d 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -47,7 +47,7 @@ explain_get_index_name_hook_type explain_get_index_name_hook = NULL; #define X_NOWHITESPACE 4 static void ExplainOneQuery(Query *query, IntoClause *into, ExplainState *es, - const char *queryString, ParamListInfo params); + const char *queryString, DestReceiver *dest, ParamListInfo params); static void report_triggers(ResultRelInfo *rInfo, bool show_relname, ExplainState *es); static double elapsed_time(instr_time *starttime); @@ -218,7 +218,7 @@ ExplainQuery(ExplainStmt *stmt, const char *queryString, foreach(l, rewritten) { ExplainOneQuery((Query *) lfirst(l), NULL, &es, - queryString, params); + queryString, None_Receiver, params); /* Separate plans with an appropriate separator */ if (lnext(l) != NULL) @@ -299,7 +299,8 @@ ExplainResultDesc(ExplainStmt *stmt) */ static void ExplainOneQuery(Query *query, IntoClause *into, ExplainState *es, - const char *queryString, ParamListInfo params) + const char *queryString, DestReceiver *dest, + ParamListInfo params) { /* planner will not cope with utility statements */ if (query->commandType == CMD_UTILITY) @@ -319,7 +320,7 @@ ExplainOneQuery(Query *query, IntoClause *into, ExplainState *es, plan = pg_plan_query(query, 0, params); /* run it (if needed) and produce output */ - ExplainOnePlan(plan, into, es, queryString, params); + ExplainOnePlan(plan, into, es, queryString, dest, params); } } @@ -343,19 +344,23 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, if (IsA(utilityStmt, CreateTableAsStmt)) { + DestReceiver *dest; + /* * We have to rewrite the contained SELECT and then pass it back to * ExplainOneQuery. It's probably not really necessary to copy the * contained parsetree another time, but let's be safe. */ CreateTableAsStmt *ctas = (CreateTableAsStmt *) utilityStmt; - List *rewritten; + Query *query = (Query *) ctas->query; + + dest = CreateIntoRelDestReceiver(into); Assert(IsA(ctas->query, Query)); - rewritten = QueryRewrite((Query *) copyObject(ctas->query)); - Assert(list_length(rewritten) == 1); - ExplainOneQuery((Query *) linitial(rewritten), ctas->into, es, - queryString, params); + + query = SetupForCreateTableAs(query, ctas->into, queryString, params, dest); + + ExplainOneQuery(query, ctas->into, es, queryString, dest, params); } else if (IsA(utilityStmt, ExecuteStmt)) ExplainExecuteQuery((ExecuteStmt *) utilityStmt, into, es, @@ -396,9 +401,8 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, */ void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es, - const char *queryString, ParamListInfo params) + const char *queryString, DestReceiver *dest, ParamListInfo params) { - DestReceiver *dest; QueryDesc *queryDesc; instr_time starttime; double totaltime = 0; @@ -422,15 +426,6 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es, PushCopiedSnapshot(GetActiveSnapshot()); UpdateActiveSnapshotCommandId(); - /* - * Normally we discard the query's output, but if explaining CREATE TABLE - * AS, we'd better use the appropriate tuple receiver. - */ - if (into) - dest = CreateIntoRelDestReceiver(into); - else - dest = None_Receiver; - /* Create a QueryDesc for the query */ queryDesc = CreateQueryDesc(plannedstmt, queryString, GetActiveSnapshot(), InvalidSnapshot, diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index c3385a113a..f855befd4d 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -355,7 +355,8 @@ DefineIndex(IndexStmt *stmt, relationId = RelationGetRelid(rel); namespaceId = RelationGetNamespace(rel); - if (rel->rd_rel->relkind != RELKIND_RELATION) + if (rel->rd_rel->relkind != RELKIND_RELATION && + rel->rd_rel->relkind != RELKIND_MATVIEW) { if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) @@ -1835,7 +1836,8 @@ ReindexDatabase(const char *databaseName, bool do_system, bool do_user) { Form_pg_class classtuple = (Form_pg_class) GETSTRUCT(tuple); - if (classtuple->relkind != RELKIND_RELATION) + if (classtuple->relkind != RELKIND_RELATION && + classtuple->relkind != RELKIND_MATVIEW) continue; /* Skip temp tables of other backends; we can't reindex them at all */ diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c new file mode 100644 index 0000000000..e040bedb7e --- /dev/null +++ b/src/backend/commands/matview.c @@ -0,0 +1,374 @@ +/*------------------------------------------------------------------------- + * + * matview.c + * materialized view support + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/matview.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/multixact.h" +#include "access/relscan.h" +#include "access/xact.h" +#include "catalog/catalog.h" +#include "catalog/heap.h" +#include "catalog/namespace.h" +#include "commands/cluster.h" +#include "commands/matview.h" +#include "commands/tablecmds.h" +#include "executor/executor.h" +#include "miscadmin.h" +#include "rewrite/rewriteHandler.h" +#include "storage/lmgr.h" +#include "storage/smgr.h" +#include "tcop/tcopprot.h" +#include "utils/snapmgr.h" + + +typedef struct +{ + DestReceiver pub; /* publicly-known function pointers */ + Oid transientoid; /* OID of new heap into which to store */ + /* These fields are filled by transientrel_startup: */ + Relation transientrel; /* relation to write to */ + CommandId output_cid; /* cmin to insert in output tuples */ + int hi_options; /* heap_insert performance options */ + BulkInsertState bistate; /* bulk insert state */ +} DR_transientrel; + +static void transientrel_startup(DestReceiver *self, int operation, TupleDesc typeinfo); +static void transientrel_receive(TupleTableSlot *slot, DestReceiver *self); +static void transientrel_shutdown(DestReceiver *self); +static void transientrel_destroy(DestReceiver *self); +static void refresh_matview_datafill(DestReceiver *dest, Query *query, + const char *queryString); + +/* + * SetRelationIsScannable + * Make the relation appear scannable. + * + * NOTE: This is only implemented for materialized views. The heap starts out + * in a state that doesn't look scannable, and can only transition from there + * to scannable, unless a new heap is created. + * + * NOTE: caller must be holding an appropriate lock on the relation. + */ +void +SetRelationIsScannable(Relation relation) +{ + Page page; + + Assert(relation->rd_rel->relkind == RELKIND_MATVIEW); + Assert(relation->rd_isscannable == false); + + RelationOpenSmgr(relation); + page = (Page) palloc(BLCKSZ); + PageInit(page, BLCKSZ, 0); + smgrextend(relation->rd_smgr, MAIN_FORKNUM, 0, (char *) page, true); + pfree(page); + + smgrimmedsync(relation->rd_smgr, MAIN_FORKNUM); + + RelationCacheInvalidateEntry(relation->rd_id); +} + +/* + * ExecRefreshMatView -- execute a REFRESH MATERIALIZED VIEW command + * + * This refreshes the materialized view by creating a new table and swapping + * the relfilenodes of the new table and the old materialized view, so the OID + * of the original materialized view is preserved. Thus we do not lose GRANT + * nor references to this materialized view. + * + * If WITH NO DATA was specified, this is effectively like a TRUNCATE; + * otherwise it is like a TRUNCATE followed by an INSERT using the SELECT + * statement associated with the materialized view. The statement node's + * skipData field is used to indicate that the clause was used. + * + * Indexes are rebuilt too, via REINDEX. Since we are effectively bulk-loading + * the new heap, it's better to create the indexes afterwards than to fill them + * incrementally while we load. + * + * The scannable state is changed based on whether the contents reflect the + * result set of the materialized view's query. + */ +void +ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, + ParamListInfo params, char *completionTag) +{ + Oid matviewOid; + Relation matviewRel; + RewriteRule *rule; + List *actions; + Query *dataQuery; + Oid tableSpace; + Oid OIDNewHeap; + DestReceiver *dest; + + /* + * Get a lock until end of transaction. + */ + matviewOid = RangeVarGetRelidExtended(stmt->relation, + AccessExclusiveLock, false, false, + RangeVarCallbackOwnsTable, NULL); + matviewRel = heap_open(matviewOid, NoLock); + + /* Make sure it is a materialized view. */ + if (matviewRel->rd_rel->relkind != RELKIND_MATVIEW) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("\"%s\" is not a materialized view", + RelationGetRelationName(matviewRel)))); + + /* + * We're not using materialized views in the system catalogs. + */ + Assert(!IsSystemRelation(matviewRel)); + + Assert(!matviewRel->rd_rel->relhasoids); + + /* + * Check that everything is correct for a refresh. Problems at this point + * are internal errors, so elog is sufficient. + */ + if (matviewRel->rd_rel->relhasrules == false || + matviewRel->rd_rules->numLocks < 1) + elog(ERROR, + "materialized view \"%s\" is missing rewrite information", + RelationGetRelationName(matviewRel)); + + if (matviewRel->rd_rules->numLocks > 1) + elog(ERROR, + "materialized view \"%s\" has too many rules", + RelationGetRelationName(matviewRel)); + + rule = matviewRel->rd_rules->rules[0]; + if (rule->event != CMD_SELECT || !(rule->isInstead)) + elog(ERROR, + "the rule for materialized view \"%s\" is not a SELECT INSTEAD OF rule", + RelationGetRelationName(matviewRel)); + + actions = rule->actions; + if (list_length(actions) != 1) + elog(ERROR, + "the rule for materialized view \"%s\" is not a single action", + RelationGetRelationName(matviewRel)); + + /* + * The stored query was rewritten at the time of the MV definition, but + * has not been scribbled on by the planner. + */ + dataQuery = (Query *) linitial(actions); + Assert(IsA(dataQuery, Query)); + + /* + * Check for active uses of the relation in the current transaction, such + * as open scans. + * + * NB: We count on this to protect us against problems with refreshing the + * data using HEAP_INSERT_FROZEN. + */ + CheckTableNotInUse(matviewRel, "REFRESH MATERIALIZED VIEW"); + + tableSpace = matviewRel->rd_rel->reltablespace; + + heap_close(matviewRel, NoLock); + + /* Create the transient table that will receive the regenerated data. */ + OIDNewHeap = make_new_heap(matviewOid, tableSpace); + dest = CreateTransientRelDestReceiver(OIDNewHeap); + + if (!stmt->skipData) + refresh_matview_datafill(dest, dataQuery, queryString); + + /* + * Swap the physical files of the target and transient tables, then + * rebuild the target's indexes and throw away the transient table. + */ + finish_heap_swap(matviewOid, OIDNewHeap, false, false, true, RecentXmin, + ReadNextMultiXactId()); + + RelationCacheInvalidateEntry(matviewOid); +} + +/* + * refresh_matview_datafill + */ +static void +refresh_matview_datafill(DestReceiver *dest, Query *query, + const char *queryString) +{ + List *rewritten; + PlannedStmt *plan; + QueryDesc *queryDesc; + List *rtable; + RangeTblEntry *initial_rte; + RangeTblEntry *second_rte; + + rewritten = QueryRewrite((Query *) copyObject(query)); + + /* SELECT should never rewrite to more or less than one SELECT query */ + if (list_length(rewritten) != 1) + elog(ERROR, "unexpected rewrite result for REFRESH MATERIALIZED VIEW"); + query = (Query *) linitial(rewritten); + + /* Check for user-requested abort. */ + CHECK_FOR_INTERRUPTS(); + + /* + * Kludge here to allow refresh of a materialized view which is invalid + * (that is, it was created or refreshed WITH NO DATA. We flag the first + * two RangeTblEntry list elements, which were added to the front of the + * rewritten Query to keep the rules system happy, with the isResultRel + * flag to indicate that it is OK if they are flagged as invalid. See + * UpdateRangeTableOfViewParse() for details. + * + * NOTE: The rewrite has switched the frist two RTEs, but they are still + * in the first two positions. If that behavior changes, the asserts here + * will fail. + */ + rtable = query->rtable; + initial_rte = ((RangeTblEntry *) linitial(rtable)); + Assert(strcmp(initial_rte->alias->aliasname, "new")); + initial_rte->isResultRel = true; + second_rte = ((RangeTblEntry *) lsecond(rtable)); + Assert(strcmp(second_rte->alias->aliasname, "old")); + second_rte->isResultRel = true; + + /* Plan the query which will generate data for the refresh. */ + plan = pg_plan_query(query, 0, NULL); + + /* + * Use a snapshot with an updated command ID to ensure this query sees + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be safe.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* Create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, queryString, + GetActiveSnapshot(), InvalidSnapshot, + dest, NULL, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, EXEC_FLAG_WITHOUT_OIDS); + + /* run the plan */ + ExecutorRun(queryDesc, ForwardScanDirection, 0L); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} + +DestReceiver * +CreateTransientRelDestReceiver(Oid transientoid) +{ + DR_transientrel *self = (DR_transientrel *) palloc0(sizeof(DR_transientrel)); + + self->pub.receiveSlot = transientrel_receive; + self->pub.rStartup = transientrel_startup; + self->pub.rShutdown = transientrel_shutdown; + self->pub.rDestroy = transientrel_destroy; + self->pub.mydest = DestTransientRel; + self->transientoid = transientoid; + + return (DestReceiver *) self; +} + +/* + * transientrel_startup --- executor startup + */ +static void +transientrel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + DR_transientrel *myState = (DR_transientrel *) self; + Relation transientrel; + + transientrel = heap_open(myState->transientoid, NoLock); + + /* + * Fill private fields of myState for use by later routines + */ + myState->transientrel = transientrel; + myState->output_cid = GetCurrentCommandId(true); + + /* + * We can skip WAL-logging the insertions, unless PITR or streaming + * replication is in use. We can skip the FSM in any case. + */ + myState->hi_options = HEAP_INSERT_SKIP_FSM | HEAP_INSERT_FROZEN; + if (!XLogIsNeeded()) + myState->hi_options |= HEAP_INSERT_SKIP_WAL; + myState->bistate = GetBulkInsertState(); + + SetRelationIsScannable(transientrel); + + /* Not using WAL requires smgr_targblock be initially invalid */ + Assert(RelationGetTargetBlock(transientrel) == InvalidBlockNumber); +} + +/* + * transientrel_receive --- receive one tuple + */ +static void +transientrel_receive(TupleTableSlot *slot, DestReceiver *self) +{ + DR_transientrel *myState = (DR_transientrel *) self; + HeapTuple tuple; + + /* + * get the heap tuple out of the tuple table slot, making sure we have a + * writable copy + */ + tuple = ExecMaterializeSlot(slot); + + heap_insert(myState->transientrel, + tuple, + myState->output_cid, + myState->hi_options, + myState->bistate); + + /* We know this is a newly created relation, so there are no indexes */ +} + +/* + * transientrel_shutdown --- executor end + */ +static void +transientrel_shutdown(DestReceiver *self) +{ + DR_transientrel *myState = (DR_transientrel *) self; + + FreeBulkInsertState(myState->bistate); + + /* If we skipped using WAL, must heap_sync before commit */ + if (myState->hi_options & HEAP_INSERT_SKIP_WAL) + heap_sync(myState->transientrel); + + /* close transientrel, but keep lock until commit */ + heap_close(myState->transientrel, NoLock); + myState->transientrel = NULL; +} + +/* + * transientrel_destroy --- release DestReceiver object + */ +static void +transientrel_destroy(DestReceiver *self) +{ + pfree(self); +} diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 62208eb995..c79bc020c2 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -665,7 +665,7 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es, PlannedStmt *pstmt = (PlannedStmt *) lfirst(p); if (IsA(pstmt, PlannedStmt)) - ExplainOnePlan(pstmt, into, es, query_string, paramLI); + ExplainOnePlan(pstmt, into, es, query_string, None_Receiver, paramLI); else ExplainOneUtility((Node *) pstmt, into, es, query_string, paramLI); diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index c83cda1b10..3b27ac26c8 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -101,11 +101,12 @@ ExecSecLabelStmt(SecLabelStmt *stmt) /* * Allow security labels only on columns of tables, views, - * composite types, and foreign tables (which are the only - * relkinds for which pg_dump will dump labels). + * materialized views, composite types, and foreign tables (which + * are the only relkinds for which pg_dump will dump labels). */ if (relation->rd_rel->relkind != RELKIND_RELATION && relation->rd_rel->relkind != RELKIND_VIEW && + relation->rd_rel->relkind != RELKIND_MATVIEW && relation->rd_rel->relkind != RELKIND_COMPOSITE_TYPE && relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index eeddd9a80b..2a55e02577 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -217,6 +217,12 @@ static const struct dropmsgstrings dropmsgstringarray[] = { gettext_noop("view \"%s\" does not exist, skipping"), gettext_noop("\"%s\" is not a view"), gettext_noop("Use DROP VIEW to remove a view.")}, + {RELKIND_MATVIEW, + ERRCODE_UNDEFINED_TABLE, + gettext_noop("materialized view \"%s\" does not exist"), + gettext_noop("materialized view \"%s\" does not exist, skipping"), + gettext_noop("\"%s\" is not a materialized view"), + gettext_noop("Use DROP MATERIALIZED VIEW to remove a materialized view.")}, {RELKIND_INDEX, ERRCODE_UNDEFINED_OBJECT, gettext_noop("index \"%s\" does not exist"), @@ -248,9 +254,10 @@ struct DropRelationCallbackState /* Alter table target-type flags for ATSimplePermissions */ #define ATT_TABLE 0x0001 #define ATT_VIEW 0x0002 -#define ATT_INDEX 0x0004 -#define ATT_COMPOSITE_TYPE 0x0008 -#define ATT_FOREIGN_TABLE 0x0010 +#define ATT_MATVIEW 0x0004 +#define ATT_INDEX 0x0008 +#define ATT_COMPOSITE_TYPE 0x0010 +#define ATT_FOREIGN_TABLE 0x0020 static void truncate_check_rel(Relation rel); static List *MergeAttributes(List *schema, List *supers, char relpersistence, @@ -399,6 +406,8 @@ static void RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, static void RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid, void *arg); +static bool isQueryUsingTempRelation_walker(Node *node, void *context); + /* ---------------------------------------------------------------- * DefineRelation @@ -735,7 +744,7 @@ DropErrorMsgWrongType(const char *relname, char wrongkind, char rightkind) /* * RemoveRelations * Implements DROP TABLE, DROP INDEX, DROP SEQUENCE, DROP VIEW, - * DROP FOREIGN TABLE + * DROP MATERIALIZED VIEW, DROP FOREIGN TABLE */ void RemoveRelations(DropStmt *drop) @@ -787,6 +796,10 @@ RemoveRelations(DropStmt *drop) relkind = RELKIND_VIEW; break; + case OBJECT_MATVIEW: + relkind = RELKIND_MATVIEW; + break; + case OBJECT_FOREIGN_TABLE: relkind = RELKIND_FOREIGN_TABLE; break; @@ -2067,12 +2080,13 @@ renameatt_check(Oid myrelid, Form_pg_class classform, bool recursing) */ if (relkind != RELKIND_RELATION && relkind != RELKIND_VIEW && + relkind != RELKIND_MATVIEW && relkind != RELKIND_COMPOSITE_TYPE && relkind != RELKIND_INDEX && relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table, view, composite type, index, or foreign table", + errmsg("\"%s\" is not a table, view, materialized view, composite type, index, or foreign table", NameStr(classform->relname)))); /* @@ -2989,12 +3003,12 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, break; case AT_SetOptions: /* ALTER COLUMN SET ( options ) */ case AT_ResetOptions: /* ALTER COLUMN RESET ( options ) */ - ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_FOREIGN_TABLE); + ATSimplePermissions(rel, ATT_TABLE | ATT_MATVIEW | ATT_INDEX | ATT_FOREIGN_TABLE); /* This command never recurses */ pass = AT_PASS_MISC; break; case AT_SetStorage: /* ALTER COLUMN SET STORAGE */ - ATSimplePermissions(rel, ATT_TABLE); + ATSimplePermissions(rel, ATT_TABLE | ATT_MATVIEW); ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode); /* No command-specific prep needed */ pass = AT_PASS_MISC; @@ -3007,7 +3021,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, pass = AT_PASS_DROP; break; case AT_AddIndex: /* ADD INDEX */ - ATSimplePermissions(rel, ATT_TABLE); + ATSimplePermissions(rel, ATT_TABLE | ATT_MATVIEW); /* This command never recurses */ /* No command-specific prep needed */ pass = AT_PASS_ADD_INDEX; @@ -3054,7 +3068,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, break; case AT_ClusterOn: /* CLUSTER ON */ case AT_DropCluster: /* SET WITHOUT CLUSTER */ - ATSimplePermissions(rel, ATT_TABLE); + ATSimplePermissions(rel, ATT_TABLE | ATT_MATVIEW); /* These commands never recurse */ /* No command-specific prep needed */ pass = AT_PASS_MISC; @@ -3081,7 +3095,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, pass = AT_PASS_DROP; break; case AT_SetTableSpace: /* SET TABLESPACE */ - ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX); + ATSimplePermissions(rel, ATT_TABLE | ATT_MATVIEW | ATT_INDEX); /* This command never recurses */ ATPrepSetTableSpace(tab, rel, cmd->name, lockmode); pass = AT_PASS_MISC; /* doesn't actually matter */ @@ -3089,7 +3103,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_SetRelOptions: /* SET (...) */ case AT_ResetRelOptions: /* RESET (...) */ case AT_ReplaceRelOptions: /* reset them all, then set just these */ - ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_VIEW); + ATSimplePermissions(rel, ATT_TABLE | ATT_VIEW | ATT_MATVIEW | ATT_INDEX); /* This command never recurses */ /* No command-specific prep needed */ pass = AT_PASS_MISC; @@ -3202,7 +3216,8 @@ ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode) { AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab); - if (tab->relkind == RELKIND_RELATION) + if (tab->relkind == RELKIND_RELATION || + tab->relkind == RELKIND_MATVIEW) AlterTableCreateToastTable(tab->relid, (Datum) 0); } } @@ -3937,6 +3952,9 @@ ATSimplePermissions(Relation rel, int allowed_targets) case RELKIND_VIEW: actual_target = ATT_VIEW; break; + case RELKIND_MATVIEW: + actual_target = ATT_MATVIEW; + break; case RELKIND_INDEX: actual_target = ATT_INDEX; break; @@ -3983,18 +4001,27 @@ ATWrongRelkindError(Relation rel, int allowed_targets) case ATT_TABLE: msg = _("\"%s\" is not a table"); break; - case ATT_TABLE | ATT_INDEX: - msg = _("\"%s\" is not a table or index"); - break; case ATT_TABLE | ATT_VIEW: msg = _("\"%s\" is not a table or view"); break; + case ATT_TABLE | ATT_VIEW | ATT_MATVIEW | ATT_INDEX: + msg = _("\"%s\" is not a table, view, materialized view, or index"); + break; + case ATT_TABLE | ATT_MATVIEW: + msg = _("\"%s\" is not a table or materialized view"); + break; + case ATT_TABLE | ATT_MATVIEW | ATT_INDEX: + msg = _("\"%s\" is not a table, materialized view, or index"); + break; case ATT_TABLE | ATT_FOREIGN_TABLE: msg = _("\"%s\" is not a table or foreign table"); break; case ATT_TABLE | ATT_COMPOSITE_TYPE | ATT_FOREIGN_TABLE: msg = _("\"%s\" is not a table, composite type, or foreign table"); break; + case ATT_TABLE | ATT_MATVIEW | ATT_INDEX | ATT_FOREIGN_TABLE: + msg = _("\"%s\" is not a table, materialized view, composite type, or foreign table"); + break; case ATT_VIEW: msg = _("\"%s\" is not a view"); break; @@ -4147,7 +4174,8 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, rel = relation_open(pg_depend->objid, AccessShareLock); att = rel->rd_att->attrs[pg_depend->objsubid - 1]; - if (rel->rd_rel->relkind == RELKIND_RELATION) + if (rel->rd_rel->relkind == RELKIND_RELATION || + rel->rd_rel->relkind == RELKIND_MATVIEW) { if (origTypeName) ereport(ERROR, @@ -4975,11 +5003,12 @@ ATPrepSetStatistics(Relation rel, const char *colName, Node *newValue, LOCKMODE * allowSystemTableMods to be turned on. */ if (rel->rd_rel->relkind != RELKIND_RELATION && + rel->rd_rel->relkind != RELKIND_MATVIEW && rel->rd_rel->relkind != RELKIND_INDEX && rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table, index, or foreign table", + errmsg("\"%s\" is not a table, materialized view, index, or foreign table", RelationGetRelationName(rel)))); /* Permissions checks */ @@ -8087,6 +8116,7 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock { case RELKIND_RELATION: case RELKIND_VIEW: + case RELKIND_MATVIEW: case RELKIND_FOREIGN_TABLE: /* ok to change owner */ break; @@ -8243,11 +8273,12 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock tuple_class->relkind == RELKIND_COMPOSITE_TYPE); /* - * If we are operating on a table, also change the ownership of any - * indexes and sequences that belong to the table, as well as the - * table's toast table (if it has one) + * If we are operating on a table or materialized view, also change + * the ownership of any indexes and sequences that belong to the + * relation, as well as its toast table (if it has one). */ if (tuple_class->relkind == RELKIND_RELATION || + tuple_class->relkind == RELKIND_MATVIEW || tuple_class->relkind == RELKIND_TOASTVALUE) { List *index_oid_list; @@ -8263,7 +8294,8 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock list_free(index_oid_list); } - if (tuple_class->relkind == RELKIND_RELATION) + if (tuple_class->relkind == RELKIND_RELATION || + tuple_class->relkind == RELKIND_MATVIEW) { /* If it has a toast table, recurse to change its ownership */ if (tuple_class->reltoastrelid != InvalidOid) @@ -8533,6 +8565,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, case RELKIND_RELATION: case RELKIND_TOASTVALUE: case RELKIND_VIEW: + case RELKIND_MATVIEW: (void) heap_reloptions(rel->rd_rel->relkind, newOptions, true); break; case RELKIND_INDEX: @@ -8541,7 +8574,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, default: ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table, index, or TOAST table", + errmsg("\"%s\" is not a table, view, materialized view, index, or TOAST table", RelationGetRelationName(rel)))); break; } @@ -9824,8 +9857,9 @@ AlterTableNamespace(AlterObjectSchemaStmt *stmt) } /* - * The guts of relocating a table to another namespace: besides moving - * the table itself, its dependent objects are relocated to the new schema. + * The guts of relocating a table or materialized view to another namespace: + * besides moving the relation itself, its dependent objects are relocated to + * the new schema. */ void AlterTableNamespaceInternal(Relation rel, Oid oldNspOid, Oid nspOid, @@ -9846,7 +9880,8 @@ AlterTableNamespaceInternal(Relation rel, Oid oldNspOid, Oid nspOid, nspOid, false, false, objsMoved); /* Fix other dependent stuff */ - if (rel->rd_rel->relkind == RELKIND_RELATION) + if (rel->rd_rel->relkind == RELKIND_RELATION || + rel->rd_rel->relkind == RELKIND_MATVIEW) { AlterIndexNamespaces(classRel, rel, oldNspOid, nspOid, objsMoved); AlterSeqNamespaces(classRel, rel, oldNspOid, nspOid, @@ -10257,10 +10292,11 @@ AtEOSubXact_on_commit_actions(bool isCommit, SubTransactionId mySubid, /* * This is intended as a callback for RangeVarGetRelidExtended(). It allows - * the table to be locked only if (1) it's a plain table or TOAST table and - * (2) the current user is the owner (or the superuser). This meets the - * permission-checking needs of both CLUSTER and REINDEX TABLE; we expose it - * here so that it can be used by both. + * the relation to be locked only if (1) it's a plain table, materialized + * view, or TOAST table and (2) the current user is the owner (or the + * superuser). This meets the permission-checking needs of CLUSTER, REINDEX + * TABLE, and REFRESH MATERIALIZED VIEW; we expose it here so that it can be + * used by all. */ void RangeVarCallbackOwnsTable(const RangeVar *relation, @@ -10280,10 +10316,11 @@ RangeVarCallbackOwnsTable(const RangeVar *relation, relkind = get_rel_relkind(relId); if (!relkind) return; - if (relkind != RELKIND_RELATION && relkind != RELKIND_TOASTVALUE) + if (relkind != RELKIND_RELATION && relkind != RELKIND_TOASTVALUE && + relkind != RELKIND_MATVIEW) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table", relation->relname))); + errmsg("\"%s\" is not a table or materialized view", relation->relname))); /* Check permissions */ if (!pg_class_ownercheck(relId, GetUserId())) @@ -10365,6 +10402,11 @@ RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("\"%s\" is not a view", rv->relname))); + if (reltype == OBJECT_MATVIEW && relkind != RELKIND_MATVIEW) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a materialized view", rv->relname))); + if (reltype == OBJECT_FOREIGN_TABLE && relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -10401,9 +10443,9 @@ RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid, * Don't allow ALTER TABLE .. SET SCHEMA on relations that can't be moved * to a different schema, such as indexes and TOAST tables. */ - if (IsA(stmt, AlterObjectSchemaStmt) &&relkind != RELKIND_RELATION - && relkind != RELKIND_VIEW && relkind != RELKIND_SEQUENCE - && relkind != RELKIND_FOREIGN_TABLE) + if (IsA(stmt, AlterObjectSchemaStmt) && relkind != RELKIND_RELATION + && relkind != RELKIND_VIEW && relkind != RELKIND_MATVIEW + && relkind != RELKIND_SEQUENCE && relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("\"%s\" is not a table, view, sequence, or foreign table", @@ -10411,3 +10453,51 @@ RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid, ReleaseSysCache(tuple); } + +/* + * Returns true iff any relation underlying this query is a temporary database + * object (table, view, or materialized view). + * + */ +bool +isQueryUsingTempRelation(Query *query) +{ + return isQueryUsingTempRelation_walker((Node *) query, NULL); +} + +static bool +isQueryUsingTempRelation_walker(Node *node, void *context) +{ + if (node == NULL) + return false; + + if (IsA(node, Query)) + { + Query *query = (Query *) node; + ListCell *rtable; + + foreach(rtable, query->rtable) + { + RangeTblEntry *rte = lfirst(rtable); + + if (rte->rtekind == RTE_RELATION) + { + Relation rel = heap_open(rte->relid, AccessShareLock); + char relpersistence = rel->rd_rel->relpersistence; + + heap_close(rel, AccessShareLock); + if (relpersistence == RELPERSISTENCE_TEMP) + return true; + } + } + + return query_tree_walker(query, + isQueryUsingTempRelation_walker, + context, + QTW_IGNORE_JOINALIASES); + } + + return expression_tree_walker(node, + isQueryUsingTempRelation_walker, + context); +} diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 0e55263d4e..1ba6d5e6e9 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -2803,7 +2803,8 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) format_type_be(domainOid)); /* Otherwise we can ignore views, composite types, etc */ - if (rel->rd_rel->relkind != RELKIND_RELATION) + if (rel->rd_rel->relkind != RELKIND_RELATION && + rel->rd_rel->relkind != RELKIND_MATVIEW) { relation_close(rel, lockmode); continue; diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index 4800b43764..c984488e03 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -341,23 +341,26 @@ get_rel_oids(Oid relid, const RangeVar *vacrel) } else { - /* Process all plain relations listed in pg_class */ + /* + * Process all plain relations and materialized views listed in + * pg_class + */ Relation pgclass; HeapScanDesc scan; HeapTuple tuple; - ScanKeyData key; - - ScanKeyInit(&key, - Anum_pg_class_relkind, - BTEqualStrategyNumber, F_CHAREQ, - CharGetDatum(RELKIND_RELATION)); pgclass = heap_open(RelationRelationId, AccessShareLock); - scan = heap_beginscan(pgclass, SnapshotNow, 1, &key); + scan = heap_beginscan(pgclass, SnapshotNow, 0, NULL); while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) { + Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple); + + if (classForm->relkind != RELKIND_RELATION && + classForm->relkind != RELKIND_MATVIEW) + continue; + /* Make a relation list entry for this guy */ oldcontext = MemoryContextSwitchTo(vac_context); oid_list = lappend_oid(oid_list, HeapTupleGetOid(tuple)); @@ -743,6 +746,7 @@ vac_update_datfrozenxid(void) * InvalidTransactionId in relfrozenxid anyway.) */ if (classForm->relkind != RELKIND_RELATION && + classForm->relkind != RELKIND_MATVIEW && classForm->relkind != RELKIND_TOASTVALUE) continue; @@ -1045,6 +1049,7 @@ vacuum_rel(Oid relid, VacuumStmt *vacstmt, bool do_toast, bool for_wraparound) * relation. */ if (onerel->rd_rel->relkind != RELKIND_RELATION && + onerel->rd_rel->relkind != RELKIND_MATVIEW && onerel->rd_rel->relkind != RELKIND_TOASTVALUE) { ereport(WARNING, diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c index 4d10f80ec4..aba6944bdf 100644 --- a/src/backend/commands/view.c +++ b/src/backend/commands/view.c @@ -36,57 +36,6 @@ static void checkViewTupleDesc(TupleDesc newdesc, TupleDesc olddesc); -static bool isViewOnTempTable_walker(Node *node, void *context); - -/*--------------------------------------------------------------------- - * isViewOnTempTable - * - * Returns true iff any of the relations underlying this view are - * temporary tables. - *--------------------------------------------------------------------- - */ -static bool -isViewOnTempTable(Query *viewParse) -{ - return isViewOnTempTable_walker((Node *) viewParse, NULL); -} - -static bool -isViewOnTempTable_walker(Node *node, void *context) -{ - if (node == NULL) - return false; - - if (IsA(node, Query)) - { - Query *query = (Query *) node; - ListCell *rtable; - - foreach(rtable, query->rtable) - { - RangeTblEntry *rte = lfirst(rtable); - - if (rte->rtekind == RTE_RELATION) - { - Relation rel = heap_open(rte->relid, AccessShareLock); - char relpersistence = rel->rd_rel->relpersistence; - - heap_close(rel, AccessShareLock); - if (relpersistence == RELPERSISTENCE_TEMP) - return true; - } - } - - return query_tree_walker(query, - isViewOnTempTable_walker, - context, - QTW_IGNORE_JOINALIASES); - } - - return expression_tree_walker(node, - isViewOnTempTable_walker, - context); -} /*--------------------------------------------------------------------- * DefineVirtualRelation @@ -506,7 +455,7 @@ DefineView(ViewStmt *stmt, const char *queryString) */ view = copyObject(stmt->view); /* don't corrupt original command */ if (view->relpersistence == RELPERSISTENCE_PERMANENT - && isViewOnTempTable(viewParse)) + && isQueryUsingTempRelation(viewParse)) { view->relpersistence = RELPERSISTENCE_TEMP; ereport(NOTICE, @@ -530,6 +479,17 @@ DefineView(ViewStmt *stmt, const char *queryString) */ CommandCounterIncrement(); + StoreViewQuery(viewOid, viewParse, stmt->replace); + + return viewOid; +} + +/* + * Use the rules system to store the query for the view. + */ +void +StoreViewQuery(Oid viewOid, Query *viewParse, bool replace) +{ /* * The range table of 'viewParse' does not contain entries for the "OLD" * and "NEW" relations. So... add them! @@ -539,7 +499,5 @@ DefineView(ViewStmt *stmt, const char *queryString) /* * Now create the rules associated with the view. */ - DefineViewRules(viewOid, viewParse, stmt->replace); - - return viewOid; + DefineViewRules(viewOid, viewParse, replace); } diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 632644f1d8..288b29e44a 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -84,6 +84,7 @@ static char *ExecBuildSlotValueDescription(TupleTableSlot *slot, int maxfieldlen); static void EvalPlanQualStart(EPQState *epqstate, EState *parentestate, Plan *planTree); +static bool RelationIdIsScannable(Oid relid); /* end of local decls */ @@ -492,6 +493,65 @@ ExecutorRewind(QueryDesc *queryDesc) } +/* + * ExecCheckRelationsScannable + * Check that relations which are to be accessed are in a scannable + * state. + * + * If not, throw error. For a materialized view, suggest refresh. + */ +static void +ExecCheckRelationsScannable(List *rangeTable) +{ + ListCell *l; + + foreach(l, rangeTable) + { + RangeTblEntry *rte = (RangeTblEntry *) lfirst(l); + + if (rte->rtekind != RTE_RELATION) + continue; + + if (!RelationIdIsScannable(rte->relid)) + { + if (rte->relkind == RELKIND_MATVIEW) + { + /* It is OK to replace the contents of an invalid matview. */ + if (rte->isResultRel) + continue; + + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("materialized view \"%s\" has not been populated", + get_rel_name(rte->relid)), + errhint("Use the REFRESH MATERIALIZED VIEW command."))); + } + else + /* This should never happen, so elog will do. */ + elog(ERROR, "relation \"%s\" is not flagged as scannable", + get_rel_name(rte->relid)); + } + } +} + +/* + * Tells whether a relation is scannable. + * + * Currently only non-populated materialzed views are not. + */ +static bool +RelationIdIsScannable(Oid relid) +{ + Relation relation; + bool result; + + relation = RelationIdGetRelation(relid); + result = relation->rd_isscannable; + RelationClose(relation); + + return result; +} + /* * ExecCheckRTPerms * Check access permissions for all relations listed in a range table. @@ -882,6 +942,13 @@ InitPlan(QueryDesc *queryDesc, int eflags) */ planstate = ExecInitNode(plan, estate, eflags); + /* + * Unless we are creating a view or are creating a materialized view WITH + * NO DATA, ensure that all referenced relations are scannable. + */ + if ((eflags & EXEC_FLAG_WITH_NO_DATA) == 0) + ExecCheckRelationsScannable(rangeTable); + /* * Get the tuple descriptor describing the type of tuples to return. */ @@ -995,6 +1062,12 @@ CheckValidResultRel(Relation resultRel, CmdType operation) break; } break; + case RELKIND_MATVIEW: + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change materialized view \"%s\"", + RelationGetRelationName(resultRel)))); + break; case RELKIND_FOREIGN_TABLE: ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -1045,6 +1118,13 @@ CheckValidRowMarkRel(Relation rel, RowMarkType markType) errmsg("cannot lock rows in view \"%s\"", RelationGetRelationName(rel)))); break; + case RELKIND_MATVIEW: + /* Should not get here */ + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot lock rows in materialized view \"%s\"", + RelationGetRelationName(rel)))); + break; case RELKIND_FOREIGN_TABLE: /* Perhaps we can support this someday, but not today */ ereport(ERROR, diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index de8d59a8cd..cc7764dba2 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -2122,6 +2122,13 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI, if (((CreateTableAsStmt *) stmt)->is_select_into) res = SPI_OK_SELINTO; } + else if (IsA(stmt, RefreshMatViewStmt)) + { + Assert(strncmp(completionTag, + "REFRESH MATERIALIZED VIEW ", 23) == 0); + _SPI_current->processed = strtoul(completionTag + 23, + NULL, 10); + } else if (IsA(stmt, CopyStmt)) { Assert(strncmp(completionTag, "COPY ", 5) == 0); diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 23ec88d54c..867b0c09d9 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -1032,6 +1032,7 @@ _copyIntoClause(const IntoClause *from) COPY_SCALAR_FIELD(onCommit); COPY_STRING_FIELD(tableSpaceName); COPY_SCALAR_FIELD(skipData); + COPY_SCALAR_FIELD(relkind); return newnode; } @@ -1970,6 +1971,7 @@ _copyRangeTblEntry(const RangeTblEntry *from) COPY_SCALAR_FIELD(rtekind); COPY_SCALAR_FIELD(relid); COPY_SCALAR_FIELD(relkind); + COPY_SCALAR_FIELD(isResultRel); COPY_NODE_FIELD(subquery); COPY_SCALAR_FIELD(security_barrier); COPY_SCALAR_FIELD(jointype); @@ -3228,11 +3230,23 @@ _copyCreateTableAsStmt(const CreateTableAsStmt *from) COPY_NODE_FIELD(query); COPY_NODE_FIELD(into); + COPY_SCALAR_FIELD(relkind); COPY_SCALAR_FIELD(is_select_into); return newnode; } +static RefreshMatViewStmt * +_copyRefreshMatViewStmt(const RefreshMatViewStmt *from) +{ + RefreshMatViewStmt *newnode = makeNode(RefreshMatViewStmt); + + COPY_SCALAR_FIELD(skipData); + COPY_NODE_FIELD(relation); + + return newnode; +} + static CreateSeqStmt * _copyCreateSeqStmt(const CreateSeqStmt *from) { @@ -4303,6 +4317,9 @@ copyObject(const void *from) case T_CreateTableAsStmt: retval = _copyCreateTableAsStmt(from); break; + case T_RefreshMatViewStmt: + retval = _copyRefreshMatViewStmt(from); + break; case T_CreateSeqStmt: retval = _copyCreateSeqStmt(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 99c034ab68..085cd5bee1 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -124,6 +124,7 @@ _equalIntoClause(const IntoClause *a, const IntoClause *b) COMPARE_SCALAR_FIELD(onCommit); COMPARE_STRING_FIELD(tableSpaceName); COMPARE_SCALAR_FIELD(skipData); + COMPARE_SCALAR_FIELD(relkind); return true; } @@ -1525,11 +1526,21 @@ _equalCreateTableAsStmt(const CreateTableAsStmt *a, const CreateTableAsStmt *b) { COMPARE_NODE_FIELD(query); COMPARE_NODE_FIELD(into); + COMPARE_SCALAR_FIELD(relkind); COMPARE_SCALAR_FIELD(is_select_into); return true; } +static bool +_equalRefreshMatViewStmt(const RefreshMatViewStmt *a, const RefreshMatViewStmt *b) +{ + COMPARE_SCALAR_FIELD(skipData); + COMPARE_NODE_FIELD(relation); + + return true; +} + static bool _equalCreateSeqStmt(const CreateSeqStmt *a, const CreateSeqStmt *b) { @@ -2223,6 +2234,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b) COMPARE_SCALAR_FIELD(rtekind); COMPARE_SCALAR_FIELD(relid); COMPARE_SCALAR_FIELD(relkind); + COMPARE_SCALAR_FIELD(isResultRel); COMPARE_NODE_FIELD(subquery); COMPARE_SCALAR_FIELD(security_barrier); COMPARE_SCALAR_FIELD(jointype); @@ -2790,6 +2802,9 @@ equal(const void *a, const void *b) case T_CreateTableAsStmt: retval = _equalCreateTableAsStmt(a, b); break; + case T_RefreshMatViewStmt: + retval = _equalRefreshMatViewStmt(a, b); + break; case T_CreateSeqStmt: retval = _equalCreateSeqStmt(a, b); break; diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index ffd123d506..be4e548281 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -893,6 +893,7 @@ _outIntoClause(StringInfo str, const IntoClause *node) WRITE_ENUM_FIELD(onCommit, OnCommitAction); WRITE_STRING_FIELD(tableSpaceName); WRITE_BOOL_FIELD(skipData); + WRITE_CHAR_FIELD(relkind); } static void @@ -2351,6 +2352,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) case RTE_RELATION: WRITE_OID_FIELD(relid); WRITE_CHAR_FIELD(relkind); + WRITE_BOOL_FIELD(isResultRel); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 472c82361a..cee67f2eb9 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -395,6 +395,7 @@ _readIntoClause(void) READ_ENUM_FIELD(onCommit, OnCommitAction); READ_STRING_FIELD(tableSpaceName); READ_BOOL_FIELD(skipData); + READ_CHAR_FIELD(relkind); READ_DONE(); } @@ -1190,6 +1191,7 @@ _readRangeTblEntry(void) case RTE_RELATION: READ_OID_FIELD(relid); READ_CHAR_FIELD(relkind); + READ_BOOL_FIELD(isResultRel); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 5b97cb5a24..db3d5c5018 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -3386,7 +3386,7 @@ plan_cluster_use_sort(Oid tableOid, Oid indexOid) rte = makeNode(RangeTblEntry); rte->rtekind = RTE_RELATION; rte->relid = tableOid; - rte->relkind = RELKIND_RELATION; + rte->relkind = RELKIND_RELATION; /* Don't be too picky. */ rte->lateral = false; rte->inh = false; rte->inFromCl = true; diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 01f988f7c3..bff7aff593 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -409,6 +409,7 @@ estimate_rel_size(Relation rel, int32 *attr_widths, { case RELKIND_RELATION: case RELKIND_INDEX: + case RELKIND_MATVIEW: case RELKIND_TOASTVALUE: /* it has storage, ok to call the smgr */ curpages = RelationGetNumberOfBlocks(rel); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 240faca72a..d34fca5466 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -190,6 +190,7 @@ transformTopLevelStmt(ParseState *pstate, Node *parseTree) ctas->query = parseTree; ctas->into = stmt->intoClause; + ctas->relkind = OBJECT_TABLE; ctas->is_select_into = true; /* @@ -324,6 +325,11 @@ analyze_requires_snapshot(Node *parseTree) result = true; break; + case T_RefreshMatViewStmt: + /* yes, because the SELECT from pg_rewrite must be analyzed */ + result = true; + break; + default: /* other utility statements don't have any real parse analysis */ result = false; @@ -2117,7 +2123,8 @@ transformExplainStmt(ParseState *pstate, ExplainStmt *stmt) /* * transformCreateTableAsStmt - - * transform a CREATE TABLE AS (or SELECT ... INTO) Statement + * transform a CREATE TABLE AS, SELECT ... INTO, or CREATE MATERIALIZED VIEW + * Statement * * As with EXPLAIN, transform the contained statement now. */ @@ -2126,6 +2133,24 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt) { Query *result; + /* + * Set relkind in IntoClause based on statement relkind. These are + * different types, because the parser users the ObjectType enumeration + * and the executor uses RELKIND_* defines. + */ + switch (stmt->relkind) + { + case (OBJECT_TABLE): + stmt->into->relkind = RELKIND_RELATION; + break; + case (OBJECT_MATVIEW): + stmt->into->relkind = RELKIND_MATVIEW; + break; + default: + elog(ERROR, "unrecognized object relkind: %d", + (int) stmt->relkind); + } + /* transform contained query */ stmt->query = (Node *) transformStmt(pstate, stmt->query); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index d3009b67b4..0787d2f506 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -121,6 +121,13 @@ typedef struct PrivTarget #define CAS_NOT_VALID 0x10 #define CAS_NO_INHERIT 0x20 +/* + * In the IntoClause structure there is a char value which will eventually be + * set to RELKIND_RELATION or RELKIND_MATVIEW based on the relkind field in + * the statement-level structure, which is an ObjectType. Define the default + * here, which should always be overridden later. + */ +#define INTO_CLAUSE_RELKIND_DEFAULT '\0' #define parser_yyerror(msg) scanner_yyerror(msg, yyscanner) #define parser_errposition(pos) scanner_errposition(pos, yyscanner) @@ -248,6 +255,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DeallocateStmt PrepareStmt ExecuteStmt DropOwnedStmt ReassignOwnedStmt AlterTSConfigurationStmt AlterTSDictionaryStmt + CreateMatViewStmt RefreshMatViewStmt %type select_no_parens select_with_parens select_clause simple_select values_clause @@ -351,7 +359,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type fdw_option %type OptTempTableName -%type into_clause create_as_target +%type into_clause create_as_target create_mv_target %type createfunc_opt_item common_func_opt_item dostmt_opt_item %type func_arg func_arg_with_default table_func_column @@ -360,6 +368,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type opt_trusted opt_restart_seqs %type OptTemp +%type OptNoLog %type OnCommitOption %type for_locking_strength @@ -557,7 +566,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P - MAPPING MATCH MAXVALUE MINUTE_P MINVALUE MODE MONTH_P MOVE + MAPPING MATCH MATERIALIZED MAXVALUE MINUTE_P MINVALUE MODE MONTH_P MOVE NAME_P NAMES NATIONAL NATURAL NCHAR NEXT NO NONE NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF @@ -572,7 +581,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); QUOTE - RANGE READ REAL REASSIGN RECHECK RECURSIVE REF REFERENCES REINDEX + RANGE READ REAL REASSIGN RECHECK RECURSIVE REF REFERENCES REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA RESET RESTART RESTRICT RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROW ROWS RULE @@ -745,6 +754,7 @@ stmt : | CreateForeignTableStmt | CreateFunctionStmt | CreateGroupStmt + | CreateMatViewStmt | CreateOpClassStmt | CreateOpFamilyStmt | AlterOpFamilyStmt @@ -790,6 +800,7 @@ stmt : | IndexStmt | InsertStmt | ListenStmt + | RefreshMatViewStmt | LoadStmt | LockStmt | NotifyStmt @@ -1704,9 +1715,9 @@ DiscardStmt: /***************************************************************************** * - * ALTER [ TABLE | INDEX | SEQUENCE | VIEW ] variations + * ALTER [ TABLE | INDEX | SEQUENCE | VIEW | MATERIALIZED VIEW ] variations * - * Note: we accept all subcommands for each of the four variants, and sort + * Note: we accept all subcommands for each of the five variants, and sort * out what's really legal at execution time. *****************************************************************************/ @@ -1783,6 +1794,24 @@ AlterTableStmt: n->missing_ok = true; $$ = (Node *)n; } + | ALTER MATERIALIZED VIEW qualified_name alter_table_cmds + { + AlterTableStmt *n = makeNode(AlterTableStmt); + n->relation = $4; + n->cmds = $5; + n->relkind = OBJECT_MATVIEW; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER MATERIALIZED VIEW IF_P EXISTS qualified_name alter_table_cmds + { + AlterTableStmt *n = makeNode(AlterTableStmt); + n->relation = $6; + n->cmds = $7; + n->relkind = OBJECT_MATVIEW; + n->missing_ok = true; + $$ = (Node *)n; + } ; alter_table_cmds: @@ -3186,6 +3215,7 @@ CreateAsStmt: CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt); ctas->query = $6; ctas->into = $4; + ctas->relkind = OBJECT_TABLE; ctas->is_select_into = false; /* cram additional flags into the IntoClause */ $4->rel->relpersistence = $2; @@ -3204,6 +3234,7 @@ create_as_target: $$->onCommit = $4; $$->tableSpaceName = $5; $$->skipData = false; /* might get changed later */ + $$->relkind = INTO_CLAUSE_RELKIND_DEFAULT; } ; @@ -3214,6 +3245,65 @@ opt_with_data: ; +/***************************************************************************** + * + * QUERY : + * CREATE MATERIALIZED VIEW relname AS SelectStmt + * + *****************************************************************************/ + +CreateMatViewStmt: + CREATE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data + { + CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt); + ctas->query = $7; + ctas->into = $5; + ctas->relkind = OBJECT_MATVIEW; + ctas->is_select_into = false; + /* cram additional flags into the IntoClause */ + $5->rel->relpersistence = $2; + $5->skipData = !($8); + $$ = (Node *) ctas; + } + ; + +create_mv_target: + qualified_name opt_column_list opt_reloptions OptTableSpace + { + $$ = makeNode(IntoClause); + $$->rel = $1; + $$->colNames = $2; + $$->options = $3; + $$->onCommit = ONCOMMIT_NOOP; + $$->tableSpaceName = $4; + $$->skipData = false; /* might get changed later */ + $$->relkind = INTO_CLAUSE_RELKIND_DEFAULT; + } + ; + +OptNoLog: UNLOGGED { $$ = RELPERSISTENCE_UNLOGGED; } + | /*EMPTY*/ { $$ = RELPERSISTENCE_PERMANENT; } + ; + + +/***************************************************************************** + * + * QUERY : + * REFRESH MATERIALIZED VIEW qualified_name + * + *****************************************************************************/ + +RefreshMatViewStmt: + REFRESH MATERIALIZED VIEW qualified_name opt_with_data + { + RefreshMatViewStmt *n = makeNode(RefreshMatViewStmt); + n->relation = $4; + n->skipData = !($5); + $$ = (Node *) n; + } + ; + + /***************************************************************************** * * QUERY : @@ -3731,6 +3821,15 @@ AlterExtensionContentsStmt: n->objname = $6; $$ = (Node *)n; } + | ALTER EXTENSION name add_drop MATERIALIZED VIEW any_name + { + AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt); + n->extname = $3; + n->action = $4; + n->objtype = OBJECT_MATVIEW; + n->objname = $7; + $$ = (Node *)n; + } | ALTER EXTENSION name add_drop FOREIGN TABLE any_name { AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt); @@ -5057,6 +5156,7 @@ DropStmt: DROP drop_type IF_P EXISTS any_name_list opt_drop_behavior drop_type: TABLE { $$ = OBJECT_TABLE; } | SEQUENCE { $$ = OBJECT_SEQUENCE; } | VIEW { $$ = OBJECT_VIEW; } + | MATERIALIZED VIEW { $$ = OBJECT_MATVIEW; } | INDEX { $$ = OBJECT_INDEX; } | FOREIGN TABLE { $$ = OBJECT_FOREIGN_TABLE; } | EVENT TRIGGER { $$ = OBJECT_EVENT_TRIGGER; } @@ -5123,7 +5223,8 @@ opt_restart_seqs: * EXTENSION | ROLE | TEXT SEARCH PARSER | * TEXT SEARCH DICTIONARY | TEXT SEARCH TEMPLATE | * TEXT SEARCH CONFIGURATION | FOREIGN TABLE | - * FOREIGN DATA WRAPPER | SERVER | EVENT TRIGGER ] | + * FOREIGN DATA WRAPPER | SERVER | EVENT TRIGGER | + * MATERIALIZED VIEW] | * AGGREGATE (arg1, ...) | * FUNCTION (arg1, arg2, ...) | * OPERATOR (leftoperand_typ, rightoperand_typ) | @@ -5297,6 +5398,7 @@ comment_type: | DOMAIN_P { $$ = OBJECT_DOMAIN; } | TYPE_P { $$ = OBJECT_TYPE; } | VIEW { $$ = OBJECT_VIEW; } + | MATERIALIZED VIEW { $$ = OBJECT_MATVIEW; } | COLLATION { $$ = OBJECT_COLLATION; } | CONVERSION_P { $$ = OBJECT_CONVERSION; } | TABLESPACE { $$ = OBJECT_TABLESPACE; } @@ -5398,6 +5500,7 @@ security_label_type: | TABLESPACE { $$ = OBJECT_TABLESPACE; } | TYPE_P { $$ = OBJECT_TYPE; } | VIEW { $$ = OBJECT_VIEW; } + | MATERIALIZED VIEW { $$ = OBJECT_MATVIEW; } ; security_label: Sconst { $$ = $1; } @@ -6940,6 +7043,26 @@ RenameStmt: ALTER AGGREGATE func_name aggr_args RENAME TO name n->missing_ok = true; $$ = (Node *)n; } + | ALTER MATERIALIZED VIEW qualified_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_MATVIEW; + n->relation = $4; + n->subname = NULL; + n->newname = $7; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER MATERIALIZED VIEW IF_P EXISTS qualified_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_MATVIEW; + n->relation = $6; + n->subname = NULL; + n->newname = $9; + n->missing_ok = true; + $$ = (Node *)n; + } | ALTER INDEX qualified_name RENAME TO name { RenameStmt *n = makeNode(RenameStmt); @@ -7002,6 +7125,28 @@ RenameStmt: ALTER AGGREGATE func_name aggr_args RENAME TO name n->missing_ok = true; $$ = (Node *)n; } + | ALTER MATERIALIZED VIEW qualified_name RENAME opt_column name TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_COLUMN; + n->relationType = OBJECT_MATVIEW; + n->relation = $4; + n->subname = $7; + n->newname = $9; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER MATERIALIZED VIEW IF_P EXISTS qualified_name RENAME opt_column name TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_COLUMN; + n->relationType = OBJECT_MATVIEW; + n->relation = $6; + n->subname = $9; + n->newname = $11; + n->missing_ok = true; + $$ = (Node *)n; + } | ALTER TABLE relation_expr RENAME CONSTRAINT name TO name { RenameStmt *n = makeNode(RenameStmt); @@ -7357,6 +7502,24 @@ AlterObjectSchemaStmt: n->missing_ok = true; $$ = (Node *)n; } + | ALTER MATERIALIZED VIEW qualified_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_MATVIEW; + n->relation = $4; + n->newschema = $7; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER MATERIALIZED VIEW IF_P EXISTS qualified_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_MATVIEW; + n->relation = $6; + n->newschema = $9; + n->missing_ok = true; + $$ = (Node *)n; + } | ALTER FOREIGN TABLE relation_expr SET SCHEMA name { AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); @@ -8535,6 +8698,8 @@ ExplainableStmt: | DeleteStmt | DeclareCursorStmt | CreateAsStmt + | CreateMatViewStmt + | RefreshMatViewStmt | ExecuteStmt /* by default all are $$=$1 */ ; @@ -8619,6 +8784,7 @@ ExecuteStmt: EXECUTE name execute_param_clause n->params = $8; ctas->query = (Node *) n; ctas->into = $4; + ctas->relkind = OBJECT_TABLE; ctas->is_select_into = false; /* cram additional flags into the IntoClause */ $4->rel->relpersistence = $2; @@ -9166,6 +9332,7 @@ into_clause: $$->onCommit = ONCOMMIT_NOOP; $$->tableSpaceName = NULL; $$->skipData = false; + $$->relkind = INTO_CLAUSE_RELKIND_DEFAULT; } | /*EMPTY*/ { $$ = NULL; } @@ -12652,6 +12819,7 @@ unreserved_keyword: | LOCK_P | MAPPING | MATCH + | MATERIALIZED | MAXVALUE | MINUTE_P | MINVALUE @@ -12697,6 +12865,7 @@ unreserved_keyword: | RECHECK | RECURSIVE | REF + | REFRESH | REINDEX | RELATIVE_P | RELEASE diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 10a3be59c0..8a1876c8a3 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -646,6 +646,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla if (relation->rd_rel->relkind != RELKIND_RELATION && relation->rd_rel->relkind != RELKIND_VIEW && + relation->rd_rel->relkind != RELKIND_MATVIEW && relation->rd_rel->relkind != RELKIND_COMPOSITE_TYPE && relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, @@ -1999,6 +2000,11 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString, */ rel = heap_openrv(stmt->relation, AccessExclusiveLock); + if (rel->rd_rel->relkind == RELKIND_MATVIEW) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("rules on materialized views are not supported"))); + /* Set up pstate */ pstate = make_parsestate(NULL); pstate->p_sourcetext = queryString; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 7ab0801887..00cb3f760d 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -1990,22 +1990,17 @@ do_autovacuum(void) * Scan pg_class to determine which tables to vacuum. * * We do this in two passes: on the first one we collect the list of plain - * relations, and on the second one we collect TOAST tables. The reason - * for doing the second pass is that during it we want to use the main - * relation's pg_class.reloptions entry if the TOAST table does not have - * any, and we cannot obtain it unless we know beforehand what's the main - * table OID. + * relations and materialized views, and on the second one we collect + * TOAST tables. The reason for doing the second pass is that during it we + * want to use the main relation's pg_class.reloptions entry if the TOAST + * table does not have any, and we cannot obtain it unless we know + * beforehand what's the main table OID. * * We need to check TOAST tables separately because in cases with short, * wide tables there might be proportionally much more activity in the * TOAST table than in its parent. */ - ScanKeyInit(&key, - Anum_pg_class_relkind, - BTEqualStrategyNumber, F_CHAREQ, - CharGetDatum(RELKIND_RELATION)); - - relScan = heap_beginscan(classRel, SnapshotNow, 1, &key); + relScan = heap_beginscan(classRel, SnapshotNow, 0, NULL); /* * On the first pass, we collect main tables to vacuum, and also the main @@ -2021,6 +2016,10 @@ do_autovacuum(void) bool doanalyze; bool wraparound; + if (classForm->relkind != RELKIND_RELATION && + classForm->relkind != RELKIND_MATVIEW) + continue; + relid = HeapTupleGetOid(tuple); /* Fetch reloptions and the pgstat entry for this table */ @@ -2406,6 +2405,7 @@ extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc) AutoVacOpts *av; Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION || + ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW || ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE); relopts = extractRelOptions(tup, pg_class_desc, InvalidOid); diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c index bdd67dd97f..12ca295422 100644 --- a/src/backend/postmaster/pgstat.c +++ b/src/backend/postmaster/pgstat.c @@ -1599,6 +1599,7 @@ pgstat_initstats(Relation rel) /* We only count stats for things that have storage */ if (!(relkind == RELKIND_RELATION || + relkind == RELKIND_MATVIEW || relkind == RELKIND_INDEX || relkind == RELKIND_TOASTVALUE || relkind == RELKIND_SEQUENCE)) diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index a1a9808e5d..8963266157 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -260,6 +260,7 @@ DefineQueryRewrite(char *rulename, * Verify relation is of a type that rules can sensibly be applied to. */ if (event_relation->rd_rel->relkind != RELKIND_RELATION && + event_relation->rd_rel->relkind != RELKIND_MATVIEW && event_relation->rd_rel->relkind != RELKIND_VIEW) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -356,7 +357,8 @@ DefineQueryRewrite(char *rulename, */ checkRuleResultList(query->targetList, RelationGetDescr(event_relation), - true); + event_relation->rd_rel->relkind != + RELKIND_MATVIEW); /* * ... there must not be another ON SELECT rule already ... @@ -414,7 +416,8 @@ DefineQueryRewrite(char *rulename, * business of converting relations to views is just a kluge to allow * dump/reload of views that participate in circular dependencies.) */ - if (event_relation->rd_rel->relkind != RELKIND_VIEW) + if (event_relation->rd_rel->relkind != RELKIND_VIEW && + event_relation->rd_rel->relkind != RELKIND_MATVIEW) { HeapScanDesc scanDesc; diff --git a/src/backend/rewrite/rewriteDefine.c.orig b/src/backend/rewrite/rewriteDefine.c.orig new file mode 100644 index 0000000000..a1a9808e5d --- /dev/null +++ b/src/backend/rewrite/rewriteDefine.c.orig @@ -0,0 +1,945 @@ +/*------------------------------------------------------------------------- + * + * rewriteDefine.c + * routines for defining a rewrite rule + * + * Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/rewrite/rewriteDefine.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/heapam.h" +#include "access/htup_details.h" +#include "access/multixact.h" +#include "access/transam.h" +#include "access/xact.h" +#include "catalog/catalog.h" +#include "catalog/dependency.h" +#include "catalog/heap.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_rewrite.h" +#include "catalog/storage.h" +#include "miscadmin.h" +#include "nodes/nodeFuncs.h" +#include "parser/parse_utilcmd.h" +#include "rewrite/rewriteDefine.h" +#include "rewrite/rewriteManip.h" +#include "rewrite/rewriteSupport.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/syscache.h" +#include "utils/tqual.h" + + +static void checkRuleResultList(List *targetList, TupleDesc resultDesc, + bool isSelect); +static bool setRuleCheckAsUser_walker(Node *node, Oid *context); +static void setRuleCheckAsUser_Query(Query *qry, Oid userid); + + +/* + * InsertRule - + * takes the arguments and inserts them as a row into the system + * relation "pg_rewrite" + */ +static Oid +InsertRule(char *rulname, + int evtype, + Oid eventrel_oid, + AttrNumber evslot_index, + bool evinstead, + Node *event_qual, + List *action, + bool replace) +{ + char *evqual = nodeToString(event_qual); + char *actiontree = nodeToString((Node *) action); + Datum values[Natts_pg_rewrite]; + bool nulls[Natts_pg_rewrite]; + bool replaces[Natts_pg_rewrite]; + NameData rname; + Relation pg_rewrite_desc; + HeapTuple tup, + oldtup; + Oid rewriteObjectId; + ObjectAddress myself, + referenced; + bool is_update = false; + + /* + * Set up *nulls and *values arrays + */ + MemSet(nulls, false, sizeof(nulls)); + + namestrcpy(&rname, rulname); + values[Anum_pg_rewrite_rulename - 1] = NameGetDatum(&rname); + values[Anum_pg_rewrite_ev_class - 1] = ObjectIdGetDatum(eventrel_oid); + values[Anum_pg_rewrite_ev_attr - 1] = Int16GetDatum(evslot_index); + values[Anum_pg_rewrite_ev_type - 1] = CharGetDatum(evtype + '0'); + values[Anum_pg_rewrite_ev_enabled - 1] = CharGetDatum(RULE_FIRES_ON_ORIGIN); + values[Anum_pg_rewrite_is_instead - 1] = BoolGetDatum(evinstead); + values[Anum_pg_rewrite_ev_qual - 1] = CStringGetTextDatum(evqual); + values[Anum_pg_rewrite_ev_action - 1] = CStringGetTextDatum(actiontree); + + /* + * Ready to store new pg_rewrite tuple + */ + pg_rewrite_desc = heap_open(RewriteRelationId, RowExclusiveLock); + + /* + * Check to see if we are replacing an existing tuple + */ + oldtup = SearchSysCache2(RULERELNAME, + ObjectIdGetDatum(eventrel_oid), + PointerGetDatum(rulname)); + + if (HeapTupleIsValid(oldtup)) + { + if (!replace) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("rule \"%s\" for relation \"%s\" already exists", + rulname, get_rel_name(eventrel_oid)))); + + /* + * When replacing, we don't need to replace every attribute + */ + MemSet(replaces, false, sizeof(replaces)); + replaces[Anum_pg_rewrite_ev_attr - 1] = true; + replaces[Anum_pg_rewrite_ev_type - 1] = true; + replaces[Anum_pg_rewrite_is_instead - 1] = true; + replaces[Anum_pg_rewrite_ev_qual - 1] = true; + replaces[Anum_pg_rewrite_ev_action - 1] = true; + + tup = heap_modify_tuple(oldtup, RelationGetDescr(pg_rewrite_desc), + values, nulls, replaces); + + simple_heap_update(pg_rewrite_desc, &tup->t_self, tup); + + ReleaseSysCache(oldtup); + + rewriteObjectId = HeapTupleGetOid(tup); + is_update = true; + } + else + { + tup = heap_form_tuple(pg_rewrite_desc->rd_att, values, nulls); + + rewriteObjectId = simple_heap_insert(pg_rewrite_desc, tup); + } + + /* Need to update indexes in either case */ + CatalogUpdateIndexes(pg_rewrite_desc, tup); + + heap_freetuple(tup); + + /* If replacing, get rid of old dependencies and make new ones */ + if (is_update) + deleteDependencyRecordsFor(RewriteRelationId, rewriteObjectId, false); + + /* + * Install dependency on rule's relation to ensure it will go away on + * relation deletion. If the rule is ON SELECT, make the dependency + * implicit --- this prevents deleting a view's SELECT rule. Other kinds + * of rules can be AUTO. + */ + myself.classId = RewriteRelationId; + myself.objectId = rewriteObjectId; + myself.objectSubId = 0; + + referenced.classId = RelationRelationId; + referenced.objectId = eventrel_oid; + referenced.objectSubId = 0; + + recordDependencyOn(&myself, &referenced, + (evtype == CMD_SELECT) ? DEPENDENCY_INTERNAL : DEPENDENCY_AUTO); + + /* + * Also install dependencies on objects referenced in action and qual. + */ + recordDependencyOnExpr(&myself, (Node *) action, NIL, + DEPENDENCY_NORMAL); + + if (event_qual != NULL) + { + /* Find query containing OLD/NEW rtable entries */ + Query *qry = (Query *) linitial(action); + + qry = getInsertSelectQuery(qry, NULL); + recordDependencyOnExpr(&myself, event_qual, qry->rtable, + DEPENDENCY_NORMAL); + } + + /* Post creation hook for new rule */ + InvokeObjectAccessHook(OAT_POST_CREATE, + RewriteRelationId, rewriteObjectId, 0, NULL); + + heap_close(pg_rewrite_desc, RowExclusiveLock); + + return rewriteObjectId; +} + +/* + * DefineRule + * Execute a CREATE RULE command. + */ +Oid +DefineRule(RuleStmt *stmt, const char *queryString) +{ + List *actions; + Node *whereClause; + Oid relId; + + /* Parse analysis. */ + transformRuleStmt(stmt, queryString, &actions, &whereClause); + + /* + * Find and lock the relation. Lock level should match + * DefineQueryRewrite. + */ + relId = RangeVarGetRelid(stmt->relation, AccessExclusiveLock, false); + + /* ... and execute */ + return DefineQueryRewrite(stmt->rulename, + relId, + whereClause, + stmt->event, + stmt->instead, + stmt->replace, + actions); +} + + +/* + * DefineQueryRewrite + * Create a rule + * + * This is essentially the same as DefineRule() except that the rule's + * action and qual have already been passed through parse analysis. + */ +Oid +DefineQueryRewrite(char *rulename, + Oid event_relid, + Node *event_qual, + CmdType event_type, + bool is_instead, + bool replace, + List *action) +{ + Relation event_relation; + int event_attno; + ListCell *l; + Query *query; + bool RelisBecomingView = false; + Oid ruleId = InvalidOid; + + /* + * If we are installing an ON SELECT rule, we had better grab + * AccessExclusiveLock to ensure no SELECTs are currently running on the + * event relation. For other types of rules, it would be sufficient to + * grab ShareRowExclusiveLock to lock out insert/update/delete actions and + * to ensure that we lock out current CREATE RULE statements; but because + * of race conditions in access to catalog entries, we can't do that yet. + * + * Note that this lock level should match the one used in DefineRule. + */ + event_relation = heap_open(event_relid, AccessExclusiveLock); + + /* + * Verify relation is of a type that rules can sensibly be applied to. + */ + if (event_relation->rd_rel->relkind != RELKIND_RELATION && + event_relation->rd_rel->relkind != RELKIND_VIEW) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a table or view", + RelationGetRelationName(event_relation)))); + + if (!allowSystemTableMods && IsSystemRelation(event_relation)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: \"%s\" is a system catalog", + RelationGetRelationName(event_relation)))); + + /* + * Check user has permission to apply rules to this relation. + */ + if (!pg_class_ownercheck(event_relid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CLASS, + RelationGetRelationName(event_relation)); + + /* + * No rule actions that modify OLD or NEW + */ + foreach(l, action) + { + query = (Query *) lfirst(l); + if (query->resultRelation == 0) + continue; + /* Don't be fooled by INSERT/SELECT */ + if (query != getInsertSelectQuery(query, NULL)) + continue; + if (query->resultRelation == PRS2_OLD_VARNO) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("rule actions on OLD are not implemented"), + errhint("Use views or triggers instead."))); + if (query->resultRelation == PRS2_NEW_VARNO) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("rule actions on NEW are not implemented"), + errhint("Use triggers instead."))); + } + + if (event_type == CMD_SELECT) + { + /* + * Rules ON SELECT are restricted to view definitions + * + * So there cannot be INSTEAD NOTHING, ... + */ + if (list_length(action) == 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("INSTEAD NOTHING rules on SELECT are not implemented"), + errhint("Use views instead."))); + + /* + * ... there cannot be multiple actions, ... + */ + if (list_length(action) > 1) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("multiple actions for rules on SELECT are not implemented"))); + + /* + * ... the one action must be a SELECT, ... + */ + query = (Query *) linitial(action); + if (!is_instead || + query->commandType != CMD_SELECT || + query->utilityStmt != NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("rules on SELECT must have action INSTEAD SELECT"))); + + /* + * ... it cannot contain data-modifying WITH ... + */ + if (query->hasModifyingCTE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("rules on SELECT must not contain data-modifying statements in WITH"))); + + /* + * ... there can be no rule qual, ... + */ + if (event_qual != NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("event qualifications are not implemented for rules on SELECT"))); + + /* + * ... the targetlist of the SELECT action must exactly match the + * event relation, ... + */ + checkRuleResultList(query->targetList, + RelationGetDescr(event_relation), + true); + + /* + * ... there must not be another ON SELECT rule already ... + */ + if (!replace && event_relation->rd_rules != NULL) + { + int i; + + for (i = 0; i < event_relation->rd_rules->numLocks; i++) + { + RewriteRule *rule; + + rule = event_relation->rd_rules->rules[i]; + if (rule->event == CMD_SELECT) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("\"%s\" is already a view", + RelationGetRelationName(event_relation)))); + } + } + + /* + * ... and finally the rule must be named _RETURN. + */ + if (strcmp(rulename, ViewSelectRuleName) != 0) + { + /* + * In versions before 7.3, the expected name was _RETviewname. For + * backwards compatibility with old pg_dump output, accept that + * and silently change it to _RETURN. Since this is just a quick + * backwards-compatibility hack, limit the number of characters + * checked to a few less than NAMEDATALEN; this saves having to + * worry about where a multibyte character might have gotten + * truncated. + */ + if (strncmp(rulename, "_RET", 4) != 0 || + strncmp(rulename + 4, RelationGetRelationName(event_relation), + NAMEDATALEN - 4 - 4) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("view rule for \"%s\" must be named \"%s\"", + RelationGetRelationName(event_relation), + ViewSelectRuleName))); + rulename = pstrdup(ViewSelectRuleName); + } + + /* + * Are we converting a relation to a view? + * + * If so, check that the relation is empty because the storage for the + * relation is going to be deleted. Also insist that the rel not have + * any triggers, indexes, or child tables. (Note: these tests are too + * strict, because they will reject relations that once had such but + * don't anymore. But we don't really care, because this whole + * business of converting relations to views is just a kluge to allow + * dump/reload of views that participate in circular dependencies.) + */ + if (event_relation->rd_rel->relkind != RELKIND_VIEW) + { + HeapScanDesc scanDesc; + + scanDesc = heap_beginscan(event_relation, SnapshotNow, 0, NULL); + if (heap_getnext(scanDesc, ForwardScanDirection) != NULL) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not convert table \"%s\" to a view because it is not empty", + RelationGetRelationName(event_relation)))); + heap_endscan(scanDesc); + + if (event_relation->rd_rel->relhastriggers) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not convert table \"%s\" to a view because it has triggers", + RelationGetRelationName(event_relation)), + errhint("In particular, the table cannot be involved in any foreign key relationships."))); + + if (event_relation->rd_rel->relhasindex) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not convert table \"%s\" to a view because it has indexes", + RelationGetRelationName(event_relation)))); + + if (event_relation->rd_rel->relhassubclass) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not convert table \"%s\" to a view because it has child tables", + RelationGetRelationName(event_relation)))); + + RelisBecomingView = true; + } + } + else + { + /* + * For non-SELECT rules, a RETURNING list can appear in at most one of + * the actions ... and there can't be any RETURNING list at all in a + * conditional or non-INSTEAD rule. (Actually, there can be at most + * one RETURNING list across all rules on the same event, but it seems + * best to enforce that at rule expansion time.) If there is a + * RETURNING list, it must match the event relation. + */ + bool haveReturning = false; + + foreach(l, action) + { + query = (Query *) lfirst(l); + + if (!query->returningList) + continue; + if (haveReturning) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot have multiple RETURNING lists in a rule"))); + haveReturning = true; + if (event_qual != NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("RETURNING lists are not supported in conditional rules"))); + if (!is_instead) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("RETURNING lists are not supported in non-INSTEAD rules"))); + checkRuleResultList(query->returningList, + RelationGetDescr(event_relation), + false); + } + } + + /* + * This rule is allowed - prepare to install it. + */ + event_attno = -1; + + /* discard rule if it's null action and not INSTEAD; it's a no-op */ + if (action != NIL || is_instead) + { + ruleId = InsertRule(rulename, + event_type, + event_relid, + event_attno, + is_instead, + event_qual, + action, + replace); + + /* + * Set pg_class 'relhasrules' field TRUE for event relation. + * + * Important side effect: an SI notice is broadcast to force all + * backends (including me!) to update relcache entries with the new + * rule. + */ + SetRelationRuleStatus(event_relid, true); + } + + /* --------------------------------------------------------------------- + * If the relation is becoming a view: + * - delete the associated storage files + * - get rid of any system attributes in pg_attribute; a view shouldn't + * have any of those + * - remove the toast table; there is no need for it anymore, and its + * presence would make vacuum slightly more complicated + * - set relkind to RELKIND_VIEW, and adjust other pg_class fields + * to be appropriate for a view + * + * NB: we had better have AccessExclusiveLock to do this ... + * --------------------------------------------------------------------- + */ + if (RelisBecomingView) + { + Relation relationRelation; + Oid toastrelid; + HeapTuple classTup; + Form_pg_class classForm; + + relationRelation = heap_open(RelationRelationId, RowExclusiveLock); + toastrelid = event_relation->rd_rel->reltoastrelid; + + /* drop storage while table still looks like a table */ + RelationDropStorage(event_relation); + DeleteSystemAttributeTuples(event_relid); + + /* + * Drop the toast table if any. (This won't take care of updating + * the toast fields in the relation's own pg_class entry; we handle + * that below.) + */ + if (OidIsValid(toastrelid)) + { + ObjectAddress toastobject; + + /* + * Delete the dependency of the toast relation on the main + * relation so we can drop the former without dropping the latter. + */ + deleteDependencyRecordsFor(RelationRelationId, toastrelid, + false); + + /* Make deletion of dependency record visible */ + CommandCounterIncrement(); + + /* Now drop toast table, including its index */ + toastobject.classId = RelationRelationId; + toastobject.objectId = toastrelid; + toastobject.objectSubId = 0; + performDeletion(&toastobject, DROP_RESTRICT, + PERFORM_DELETION_INTERNAL); + } + + /* + * SetRelationRuleStatus may have updated the pg_class row, so we must + * advance the command counter before trying to update it again. + */ + CommandCounterIncrement(); + + /* + * Fix pg_class entry to look like a normal view's, including setting + * the correct relkind and removal of reltoastrelid/reltoastidxid of + * the toast table we potentially removed above. + */ + classTup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(event_relid)); + if (!HeapTupleIsValid(classTup)) + elog(ERROR, "cache lookup failed for relation %u", event_relid); + classForm = (Form_pg_class) GETSTRUCT(classTup); + + classForm->reltablespace = InvalidOid; + classForm->relpages = 0; + classForm->reltuples = 0; + classForm->relallvisible = 0; + classForm->reltoastrelid = InvalidOid; + classForm->reltoastidxid = InvalidOid; + classForm->relhasindex = false; + classForm->relkind = RELKIND_VIEW; + classForm->relhasoids = false; + classForm->relhaspkey = false; + classForm->relfrozenxid = InvalidTransactionId; + classForm->relminmxid = InvalidMultiXactId; + + simple_heap_update(relationRelation, &classTup->t_self, classTup); + CatalogUpdateIndexes(relationRelation, classTup); + + heap_freetuple(classTup); + heap_close(relationRelation, RowExclusiveLock); + } + + /* Close rel, but keep lock till commit... */ + heap_close(event_relation, NoLock); + + return ruleId; +} + +/* + * checkRuleResultList + * Verify that targetList produces output compatible with a tupledesc + * + * The targetList might be either a SELECT targetlist, or a RETURNING list; + * isSelect tells which. (This is mostly used for choosing error messages, + * but also we don't enforce column name matching for RETURNING.) + */ +static void +checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect) +{ + ListCell *tllist; + int i; + + i = 0; + foreach(tllist, targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(tllist); + int32 tletypmod; + Form_pg_attribute attr; + char *attname; + + /* resjunk entries may be ignored */ + if (tle->resjunk) + continue; + i++; + if (i > resultDesc->natts) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + isSelect ? + errmsg("SELECT rule's target list has too many entries") : + errmsg("RETURNING list has too many entries"))); + + attr = resultDesc->attrs[i - 1]; + attname = NameStr(attr->attname); + + /* + * Disallow dropped columns in the relation. This won't happen in the + * cases we actually care about (namely creating a view via CREATE + * TABLE then CREATE RULE, or adding a RETURNING rule to a view). + * Trying to cope with it is much more trouble than it's worth, + * because we'd have to modify the rule to insert dummy NULLs at the + * right positions. + */ + if (attr->attisdropped) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot convert relation containing dropped columns to view"))); + + if (isSelect && strcmp(tle->resname, attname) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("SELECT rule's target entry %d has different column name from \"%s\"", i, attname))); + + if (attr->atttypid != exprType((Node *) tle->expr)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + isSelect ? + errmsg("SELECT rule's target entry %d has different type from column \"%s\"", + i, attname) : + errmsg("RETURNING list's entry %d has different type from column \"%s\"", + i, attname))); + + /* + * Allow typmods to be different only if one of them is -1, ie, + * "unspecified". This is necessary for cases like "numeric", where + * the table will have a filled-in default length but the select + * rule's expression will probably have typmod = -1. + */ + tletypmod = exprTypmod((Node *) tle->expr); + if (attr->atttypmod != tletypmod && + attr->atttypmod != -1 && tletypmod != -1) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + isSelect ? + errmsg("SELECT rule's target entry %d has different size from column \"%s\"", + i, attname) : + errmsg("RETURNING list's entry %d has different size from column \"%s\"", + i, attname))); + } + + if (i != resultDesc->natts) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + isSelect ? + errmsg("SELECT rule's target list has too few entries") : + errmsg("RETURNING list has too few entries"))); +} + +/* + * setRuleCheckAsUser + * Recursively scan a query or expression tree and set the checkAsUser + * field to the given userid in all rtable entries. + * + * Note: for a view (ON SELECT rule), the checkAsUser field of the OLD + * RTE entry will be overridden when the view rule is expanded, and the + * checkAsUser field of the NEW entry is irrelevant because that entry's + * requiredPerms bits will always be zero. However, for other types of rules + * it's important to set these fields to match the rule owner. So we just set + * them always. + */ +void +setRuleCheckAsUser(Node *node, Oid userid) +{ + (void) setRuleCheckAsUser_walker(node, &userid); +} + +static bool +setRuleCheckAsUser_walker(Node *node, Oid *context) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + setRuleCheckAsUser_Query((Query *) node, *context); + return false; + } + return expression_tree_walker(node, setRuleCheckAsUser_walker, + (void *) context); +} + +static void +setRuleCheckAsUser_Query(Query *qry, Oid userid) +{ + ListCell *l; + + /* Set all the RTEs in this query node */ + foreach(l, qry->rtable) + { + RangeTblEntry *rte = (RangeTblEntry *) lfirst(l); + + if (rte->rtekind == RTE_SUBQUERY) + { + /* Recurse into subquery in FROM */ + setRuleCheckAsUser_Query(rte->subquery, userid); + } + else + rte->checkAsUser = userid; + } + + /* Recurse into subquery-in-WITH */ + foreach(l, qry->cteList) + { + CommonTableExpr *cte = (CommonTableExpr *) lfirst(l); + + setRuleCheckAsUser_Query((Query *) cte->ctequery, userid); + } + + /* If there are sublinks, search for them and process their RTEs */ + if (qry->hasSubLinks) + query_tree_walker(qry, setRuleCheckAsUser_walker, (void *) &userid, + QTW_IGNORE_RC_SUBQUERIES); +} + + +/* + * Change the firing semantics of an existing rule. + */ +void +EnableDisableRule(Relation rel, const char *rulename, + char fires_when) +{ + Relation pg_rewrite_desc; + Oid owningRel = RelationGetRelid(rel); + Oid eventRelationOid; + HeapTuple ruletup; + bool changed = false; + + /* + * Find the rule tuple to change. + */ + pg_rewrite_desc = heap_open(RewriteRelationId, RowExclusiveLock); + ruletup = SearchSysCacheCopy2(RULERELNAME, + ObjectIdGetDatum(owningRel), + PointerGetDatum(rulename)); + if (!HeapTupleIsValid(ruletup)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("rule \"%s\" for relation \"%s\" does not exist", + rulename, get_rel_name(owningRel)))); + + /* + * Verify that the user has appropriate permissions. + */ + eventRelationOid = ((Form_pg_rewrite) GETSTRUCT(ruletup))->ev_class; + Assert(eventRelationOid == owningRel); + if (!pg_class_ownercheck(eventRelationOid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CLASS, + get_rel_name(eventRelationOid)); + + /* + * Change ev_enabled if it is different from the desired new state. + */ + if (DatumGetChar(((Form_pg_rewrite) GETSTRUCT(ruletup))->ev_enabled) != + fires_when) + { + ((Form_pg_rewrite) GETSTRUCT(ruletup))->ev_enabled = + CharGetDatum(fires_when); + simple_heap_update(pg_rewrite_desc, &ruletup->t_self, ruletup); + + /* keep system catalog indexes current */ + CatalogUpdateIndexes(pg_rewrite_desc, ruletup); + + changed = true; + } + + heap_freetuple(ruletup); + heap_close(pg_rewrite_desc, RowExclusiveLock); + + /* + * If we changed anything, broadcast a SI inval message to force each + * backend (including our own!) to rebuild relation's relcache entry. + * Otherwise they will fail to apply the change promptly. + */ + if (changed) + CacheInvalidateRelcache(rel); +} + + +/* + * Perform permissions and integrity checks before acquiring a relation lock. + */ +static void +RangeVarCallbackForRenameRule(const RangeVar *rv, Oid relid, Oid oldrelid, + void *arg) +{ + HeapTuple tuple; + Form_pg_class form; + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(tuple)) + return; /* concurrently dropped */ + form = (Form_pg_class) GETSTRUCT(tuple); + + /* only tables and views can have rules */ + if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a table or view", rv->relname))); + + if (!allowSystemTableMods && IsSystemClass(form)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: \"%s\" is a system catalog", + rv->relname))); + + /* you must own the table to rename one of its rules */ + if (!pg_class_ownercheck(relid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CLASS, rv->relname); + + ReleaseSysCache(tuple); +} + +/* + * Rename an existing rewrite rule. + */ +Oid +RenameRewriteRule(RangeVar *relation, const char *oldName, + const char *newName) +{ + Oid relid; + Relation targetrel; + Relation pg_rewrite_desc; + HeapTuple ruletup; + Form_pg_rewrite ruleform; + Oid ruleOid; + + /* + * Look up name, check permissions, and acquire lock (which we will NOT + * release until end of transaction). + */ + relid = RangeVarGetRelidExtended(relation, AccessExclusiveLock, + false, false, + RangeVarCallbackForRenameRule, + NULL); + + /* Have lock already, so just need to build relcache entry. */ + targetrel = relation_open(relid, NoLock); + + /* Prepare to modify pg_rewrite */ + pg_rewrite_desc = heap_open(RewriteRelationId, RowExclusiveLock); + + /* Fetch the rule's entry (it had better exist) */ + ruletup = SearchSysCacheCopy2(RULERELNAME, + ObjectIdGetDatum(relid), + PointerGetDatum(oldName)); + if (!HeapTupleIsValid(ruletup)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("rule \"%s\" for relation \"%s\" does not exist", + oldName, RelationGetRelationName(targetrel)))); + ruleform = (Form_pg_rewrite) GETSTRUCT(ruletup); + ruleOid = HeapTupleGetOid(ruletup); + + /* rule with the new name should not already exist */ + if (IsDefinedRewriteRule(relid, newName)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("rule \"%s\" for relation \"%s\" already exists", + newName, RelationGetRelationName(targetrel)))); + + /* + * We disallow renaming ON SELECT rules, because they should always be + * named "_RETURN". + */ + if (ruleform->ev_type == CMD_SELECT + '0') + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("renaming an ON SELECT rule is not allowed"))); + + /* OK, do the update */ + namestrcpy(&(ruleform->rulename), newName); + + simple_heap_update(pg_rewrite_desc, &ruletup->t_self, ruletup); + + /* keep system catalog indexes current */ + CatalogUpdateIndexes(pg_rewrite_desc, ruletup); + + heap_freetuple(ruletup); + heap_close(pg_rewrite_desc, RowExclusiveLock); + + /* + * Invalidate relation's relcache entry so that other backends (and this + * one too!) are sent SI message to make them rebuild relcache entries. + * (Ideally this should happen automatically...) + */ + CacheInvalidateRelcache(targetrel); + + /* + * Close rel, but keep exclusive lock! + */ + relation_close(targetrel, NoLock); + + return ruleOid; +} diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index b458de6971..83c83a6a8a 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -1168,7 +1168,8 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte, const char *attrname; TargetEntry *tle; - if (target_relation->rd_rel->relkind == RELKIND_RELATION) + if (target_relation->rd_rel->relkind == RELKIND_RELATION || + target_relation->rd_rel->relkind == RELKIND_MATVIEW) { /* * Emit CTID so that executor can find the row to update or delete. @@ -1590,6 +1591,23 @@ fireRIRrules(Query *parsetree, List *activeRIRs, bool forUpdatePushedDown) */ rel = heap_open(rte->relid, NoLock); + /* + * Skip materialized view expansion when it is being created. + * + * NOTE: This is assuming that we cannot have gotten to this point + * with a non-scannable materialized view unless it is being + * populated, and that if it is scannable we want to use the existing + * contents. It would be nice to have some way to confirm that we're + * doing the right thing here, but rule expansion doesn't give us a + * lot to work with, so we are trusting earlier validations and + * execution steps to get it right. + */ + if (rel->rd_rel->relkind == RELKIND_MATVIEW && rel->rd_isscannable) + { + heap_close(rel, NoLock); + break; + } + /* * Collect the RIR rules that we must apply */ diff --git a/src/backend/storage/lmgr/predicate.c b/src/backend/storage/lmgr/predicate.c index 51c350797d..6029cfb78e 100644 --- a/src/backend/storage/lmgr/predicate.c +++ b/src/backend/storage/lmgr/predicate.c @@ -460,13 +460,14 @@ static void OnConflict_CheckForSerializationFailure(const SERIALIZABLEXACT *read /* * Does this relation participate in predicate locking? Temporary and system - * relations are exempt. + * relations are exempt, as are materialized views. */ static inline bool PredicateLockingNeededForRelation(Relation relation) { return !(relation->rd_id < FirstBootstrapObjectId || - RelationUsesLocalBuffers(relation)); + RelationUsesLocalBuffers(relation) || + relation->rd_rel->relkind == RELKIND_MATVIEW); } /* diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index 8767d05cc1..fb2ff32f57 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -32,6 +32,7 @@ #include "access/xact.h" #include "commands/copy.h" #include "commands/createas.h" +#include "commands/matview.h" #include "executor/functions.h" #include "executor/tstoreReceiver.h" #include "libpq/libpq.h" @@ -125,6 +126,9 @@ CreateDestReceiver(CommandDest dest) case DestSQLFunction: return CreateSQLFunctionDestReceiver(); + + case DestTransientRel: + return CreateTransientRelDestReceiver(InvalidOid); } /* should never get here */ @@ -157,6 +161,7 @@ EndCommand(const char *commandTag, CommandDest dest) case DestIntoRel: case DestCopyOut: case DestSQLFunction: + case DestTransientRel: break; } } @@ -198,6 +203,7 @@ NullCommand(CommandDest dest) case DestIntoRel: case DestCopyOut: case DestSQLFunction: + case DestTransientRel: break; } } @@ -241,6 +247,7 @@ ReadyForQuery(CommandDest dest) case DestIntoRel: case DestCopyOut: case DestSQLFunction: + case DestTransientRel: break; } } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 8904c6f2da..a1c03f1f76 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -37,6 +37,7 @@ #include "commands/event_trigger.h" #include "commands/explain.h" #include "commands/extension.h" +#include "commands/matview.h" #include "commands/lockcmds.h" #include "commands/portalcmds.h" #include "commands/prepare.h" @@ -202,6 +203,7 @@ check_xact_readonly(Node *parsetree) case T_CreateSeqStmt: case T_CreateStmt: case T_CreateTableAsStmt: + case T_RefreshMatViewStmt: case T_CreateTableSpaceStmt: case T_CreateTrigStmt: case T_CompositeTypeStmt: @@ -713,6 +715,7 @@ standard_ProcessUtility(Node *parsetree, case OBJECT_TABLE: case OBJECT_SEQUENCE: case OBJECT_VIEW: + case OBJECT_MATVIEW: case OBJECT_FOREIGN_TABLE: RemoveRelations((DropStmt *) parsetree); break; @@ -1164,6 +1167,13 @@ standard_ProcessUtility(Node *parsetree, queryString, params, completionTag)); break; + case T_RefreshMatViewStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); + ExecRefreshMatView((RefreshMatViewStmt *) parsetree, + queryString, params, completionTag); + break; + case T_VariableSetStmt: ExecSetVariableStmt((VariableSetStmt *) parsetree); break; @@ -1290,6 +1300,7 @@ standard_ProcessUtility(Node *parsetree, ReindexIndex(stmt->relation); break; case OBJECT_TABLE: + case OBJECT_MATVIEW: ReindexTable(stmt->relation); break; case OBJECT_DATABASE: @@ -1509,9 +1520,10 @@ QueryReturnsTuples(Query *parsetree) * We assume it is invoked only on already-parse-analyzed statements * (else the contained parsetree isn't a Query yet). * - * In some cases (currently, only EXPLAIN of CREATE TABLE AS/SELECT INTO), - * potentially Query-containing utility statements can be nested. This - * function will drill down to a non-utility Query, or return NULL if none. + * In some cases (currently, only EXPLAIN of CREATE TABLE AS/SELECT INTO and + * CREATE MATERIALIZED VIEW), potentially Query-containing utility statements + * can be nested. This function will drill down to a non-utility Query, or + * return NULL if none. */ Query * UtilityContainsQuery(Node *parsetree) @@ -1655,6 +1667,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_VIEW: tag = "ALTER VIEW"; break; + case OBJECT_MATVIEW: + tag = "ALTER MATERIALIZED VIEW"; + break; default: tag = "???"; break; @@ -1852,6 +1867,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_VIEW: tag = "DROP VIEW"; break; + case OBJECT_MATVIEW: + tag = "DROP MATERIALIZED VIEW"; + break; case OBJECT_INDEX: tag = "DROP INDEX"; break; @@ -2113,10 +2131,24 @@ CreateCommandTag(Node *parsetree) break; case T_CreateTableAsStmt: - if (((CreateTableAsStmt *) parsetree)->is_select_into) - tag = "SELECT INTO"; - else - tag = "CREATE TABLE AS"; + switch (((CreateTableAsStmt *) parsetree)->relkind) + { + case OBJECT_TABLE: + if (((CreateTableAsStmt *) parsetree)->is_select_into) + tag = "SELECT INTO"; + else + tag = "CREATE TABLE AS"; + break; + case OBJECT_MATVIEW: + tag = "CREATE MATERIALIZED VIEW"; + break; + default: + tag = "???"; + } + break; + + case T_RefreshMatViewStmt: + tag = "REFRESH MATERIALIZED VIEW"; break; case T_VariableSetStmt: @@ -2681,6 +2713,10 @@ GetCommandLogLevel(Node *parsetree) lev = LOGSTMT_DDL; break; + case T_RefreshMatViewStmt: + lev = LOGSTMT_DDL; + break; + case T_VariableSetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 11b004072f..d589d26070 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -719,6 +719,7 @@ pg_relation_filenode(PG_FUNCTION_ARGS) switch (relform->relkind) { case RELKIND_RELATION: + case RELKIND_MATVIEW: case RELKIND_INDEX: case RELKIND_SEQUENCE: case RELKIND_TOASTVALUE: @@ -767,6 +768,7 @@ pg_relation_filepath(PG_FUNCTION_ARGS) switch (relform->relkind) { case RELKIND_RELATION: + case RELKIND_MATVIEW: case RELKIND_INDEX: case RELKIND_SEQUENCE: case RELKIND_TOASTVALUE: @@ -832,3 +834,25 @@ pg_relation_filepath(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text(path)); } + + +/* + * Indicate whether a relation is scannable. + * + * Currently, this is always true except for a materialized view which has not + * been populated. + */ +Datum +pg_relation_is_scannable(PG_FUNCTION_ARGS) +{ + Oid relid; + Relation relation; + bool result; + + relid = PG_GETARG_OID(0); + relation = RelationIdGetRelation(relid); + result = relation->rd_isscannable; + RelationClose(relation); + + PG_RETURN_BOOL(result); +} diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c index e101ea6349..d5d48d5c06 100644 --- a/src/backend/utils/adt/xml.c +++ b/src/backend/utils/adt/xml.c @@ -2285,7 +2285,7 @@ schema_get_xml_visible_tables(Oid nspid) StringInfoData query; initStringInfo(&query); - appendStringInfo(&query, "SELECT oid FROM pg_catalog.pg_class WHERE relnamespace = %u AND relkind IN ('r', 'v') AND pg_catalog.has_table_privilege (oid, 'SELECT') ORDER BY relname;", nspid); + appendStringInfo(&query, "SELECT oid FROM pg_catalog.pg_class WHERE relnamespace = %u AND relkind IN ('r', 'm', 'v') AND pg_catalog.has_table_privilege (oid, 'SELECT') ORDER BY relname;", nspid); return query_to_oid_list(query.data); } @@ -2311,7 +2311,7 @@ static List * database_get_xml_visible_tables(void) { /* At the moment there is no order required here. */ - return query_to_oid_list("SELECT oid FROM pg_catalog.pg_class WHERE relkind IN ('r', 'v') AND pg_catalog.has_table_privilege (pg_class.oid, 'SELECT') AND relnamespace IN (" XML_VISIBLE_SCHEMAS ");"); + return query_to_oid_list("SELECT oid FROM pg_catalog.pg_class WHERE relkind IN ('r', 'm', 'v') AND pg_catalog.has_table_privilege (pg_class.oid, 'SELECT') AND relnamespace IN (" XML_VISIBLE_SCHEMAS ");"); } diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index e85c7a9872..ba03dfcbb2 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -37,6 +37,7 @@ #include "access/transam.h" #include "access/xact.h" #include "catalog/catalog.h" +#include "catalog/heap.h" #include "catalog/index.h" #include "catalog/indexing.h" #include "catalog/namespace.h" @@ -399,6 +400,7 @@ RelationParseRelOptions(Relation relation, HeapTuple tuple) case RELKIND_TOASTVALUE: case RELKIND_INDEX: case RELKIND_VIEW: + case RELKIND_MATVIEW: break; default: return; @@ -954,6 +956,12 @@ RelationBuildDesc(Oid targetRelId, bool insertIt) /* make sure relation is marked as having no open file yet */ relation->rd_smgr = NULL; + if (relation->rd_rel->relkind == RELKIND_MATVIEW && + heap_is_matview_init_state(relation)) + relation->rd_isscannable = false; + else + relation->rd_isscannable = true; + /* * now we can free the memory allocated for pg_class_tuple */ @@ -1523,6 +1531,7 @@ formrdesc(const char *relationName, Oid relationReltype, * initialize physical addressing information for the relation */ RelationInitPhysicalAddr(relation); + relation->rd_isscannable = true; /* * initialize the rel-has-index flag, using hardwired knowledge @@ -1747,6 +1756,7 @@ RelationReloadIndexInfo(Relation relation) heap_freetuple(pg_class_tuple); /* We must recalculate physical address in case it changed */ RelationInitPhysicalAddr(relation); + relation->rd_isscannable = true; /* * For a non-system index, there are fields of the pg_index row that are @@ -1893,6 +1903,11 @@ RelationClearRelation(Relation relation, bool rebuild) if (relation->rd_isnailed) { RelationInitPhysicalAddr(relation); + if (relation->rd_rel->relkind == RELKIND_MATVIEW && + heap_is_matview_init_state(relation)) + relation->rd_isscannable = false; + else + relation->rd_isscannable = true; if (relation->rd_rel->relkind == RELKIND_INDEX) { @@ -2681,6 +2696,12 @@ RelationBuildLocalRelation(const char *relname, RelationInitPhysicalAddr(rel); + /* materialized view not initially scannable */ + if (relkind == RELKIND_MATVIEW) + rel->rd_isscannable = false; + else + rel->rd_isscannable = true; + /* * Okay to insert into the relcache hash tables. */ @@ -4424,6 +4445,11 @@ load_relcache_init_file(bool shared) */ RelationInitLockInfo(rel); RelationInitPhysicalAddr(rel); + if (rel->rd_rel->relkind == RELKIND_MATVIEW && + heap_is_matview_init_state(rel)) + rel->rd_isscannable = false; + else + rel->rd_isscannable = true; } /* diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index b50113265b..b0b346f72a 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -2047,7 +2047,7 @@ setup_privileges(void) static char *privileges_setup[] = { "UPDATE pg_class " " SET relacl = E'{\"=r/\\\\\"$POSTGRES_SUPERUSERNAME\\\\\"\"}' " - " WHERE relkind IN ('r', 'v', 'S') AND relacl IS NULL;\n", + " WHERE relkind IN ('r', 'v', 'm', 'S') AND relacl IS NULL;\n", "GRANT USAGE ON SCHEMA pg_catalog TO PUBLIC;\n", "GRANT CREATE, USAGE ON SCHEMA public TO PUBLIC;\n", "REVOKE ALL ON pg_largeobject FROM PUBLIC;\n", diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index 01739ab717..b8832af250 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -270,7 +270,8 @@ flagInhTables(TableInfo *tblinfo, int numTables, { /* Sequences and views never have parents */ if (tblinfo[i].relkind == RELKIND_SEQUENCE || - tblinfo[i].relkind == RELKIND_VIEW) + tblinfo[i].relkind == RELKIND_VIEW || + tblinfo[i].relkind == RELKIND_MATVIEW) continue; /* Don't bother computing anything for non-target tables, either */ @@ -315,7 +316,8 @@ flagInhAttrs(TableInfo *tblinfo, int numTables) /* Sequences and views never have parents */ if (tbinfo->relkind == RELKIND_SEQUENCE || - tbinfo->relkind == RELKIND_VIEW) + tbinfo->relkind == RELKIND_VIEW || + tbinfo->relkind == RELKIND_MATVIEW) continue; /* Don't bother computing anything for non-target tables, either */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 1c663cd14f..d500bfd234 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -2908,7 +2908,8 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te, ArchiveHandle *AH) const char *type = te->desc; /* Use ALTER TABLE for views and sequences */ - if (strcmp(type, "VIEW") == 0 || strcmp(type, "SEQUENCE") == 0) + if (strcmp(type, "VIEW") == 0 || strcmp(type, "SEQUENCE") == 0|| + strcmp(type, "MATERIALIZED VIEW") == 0) type = "TABLE"; /* objects named by a schema and name */ @@ -3140,6 +3141,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, RestoreOptions *ropt, bool isDat strcmp(te->desc, "TABLE") == 0 || strcmp(te->desc, "TYPE") == 0 || strcmp(te->desc, "VIEW") == 0 || + strcmp(te->desc, "MATERIALIZED VIEW") == 0 || strcmp(te->desc, "SEQUENCE") == 0 || strcmp(te->desc, "FOREIGN TABLE") == 0 || strcmp(te->desc, "TEXT SEARCH DICTIONARY") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 7903b79a32..e6c85ac0ae 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -151,6 +151,7 @@ static void expand_table_name_patterns(Archive *fout, SimpleOidList *oids); static NamespaceInfo *findNamespace(Archive *fout, Oid nsoid, Oid objoid); static void dumpTableData(Archive *fout, TableDataInfo *tdinfo); +static void refreshMatViewData(Archive *fout, TableDataInfo *tdinfo); static void guessConstraintInheritance(TableInfo *tblinfo, int numTables); static void dumpComment(Archive *fout, const char *target, const char *namespace, const char *owner, @@ -223,6 +224,7 @@ static void addBoundaryDependencies(DumpableObject **dobjs, int numObjs, static void getDomainConstraints(Archive *fout, TypeInfo *tyinfo); static void getTableData(TableInfo *tblinfo, int numTables, bool oids); static void makeTableDataInfo(TableInfo *tbinfo, bool oids); +static void buildMatViewRefreshDependencies(Archive *fout); static void getTableDataFKConstraints(void); static char *format_function_arguments(FuncInfo *finfo, char *funcargs); static char *format_function_arguments_old(Archive *fout, @@ -723,6 +725,7 @@ main(int argc, char **argv) if (!schemaOnly) { getTableData(tblinfo, numTables, oids); + buildMatViewRefreshDependencies(fout); if (dataOnly) getTableDataFKConstraints(); } @@ -1075,9 +1078,9 @@ expand_table_name_patterns(Archive *fout, "SELECT c.oid" "\nFROM pg_catalog.pg_class c" "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace" - "\nWHERE c.relkind in ('%c', '%c', '%c', '%c')\n", + "\nWHERE c.relkind in ('%c', '%c', '%c', '%c', '%c')\n", RELKIND_RELATION, RELKIND_SEQUENCE, RELKIND_VIEW, - RELKIND_FOREIGN_TABLE); + RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE); processSQLNamePattern(GetConnection(fout), query, cell->val, true, false, "n.nspname", "c.relname", NULL, "pg_catalog.pg_table_is_visible(c.oid)"); @@ -1637,6 +1640,49 @@ dumpTableData(Archive *fout, TableDataInfo *tdinfo) destroyPQExpBuffer(copyBuf); } +/* + * refreshMatViewData - + * load or refresh the contents of a single materialized view + * + * Actually, this just makes an ArchiveEntry for the REFRESH MATERIALIZED VIEW + * statement. + */ +static void +refreshMatViewData(Archive *fout, TableDataInfo *tdinfo) +{ + TableInfo *tbinfo = tdinfo->tdtable; + PQExpBuffer q; + + /* If the materialized view is not flagged as scannable, skip this. */ + if (!tbinfo->isscannable) + return; + + q = createPQExpBuffer(); + + appendPQExpBuffer(q, "REFRESH MATERIALIZED VIEW %s;\n", + fmtId(tbinfo->dobj.name)); + + ArchiveEntry(fout, + tdinfo->dobj.catId, /* catalog ID */ + tdinfo->dobj.dumpId, /* dump ID */ + tbinfo->dobj.name, /* Name */ + tbinfo->dobj.namespace->dobj.name, /* Namespace */ + NULL, /* Tablespace */ + tbinfo->rolname, /* Owner */ + false, /* with oids */ + "MATERIALIZED VIEW DATA", /* Desc */ + SECTION_POST_DATA, /* Section */ + q->data, /* Create */ + "", /* Del */ + NULL, /* Copy */ + tdinfo->dobj.dependencies, /* Deps */ + tdinfo->dobj.nDeps, /* # Deps */ + NULL, /* Dumper */ + NULL); /* Dumper Arg */ + + destroyPQExpBuffer(q); +} + /* * getTableData - * set up dumpable objects representing the contents of tables @@ -1691,7 +1737,10 @@ makeTableDataInfo(TableInfo *tbinfo, bool oids) /* OK, let's dump it */ tdinfo = (TableDataInfo *) pg_malloc(sizeof(TableDataInfo)); - tdinfo->dobj.objType = DO_TABLE_DATA; + if (tbinfo->relkind == RELKIND_MATVIEW) + tdinfo->dobj.objType = DO_REFRESH_MATVIEW; + else + tdinfo->dobj.objType = DO_TABLE_DATA; /* * Note: use tableoid 0 so that this object won't be mistaken for @@ -1710,6 +1759,114 @@ makeTableDataInfo(TableInfo *tbinfo, bool oids) tbinfo->dataObj = tdinfo; } +/* + * The refresh for a materialized view must be dependent on the refresh for + * any materialized view that this one is dependent on. + * + * This must be called after all the objects are created, but before they are + * sorted. + */ +static void +buildMatViewRefreshDependencies(Archive *fout) +{ + PQExpBuffer query = createPQExpBuffer(); + PGresult *res; + int ntups, + i; + int i_classid, + i_objid, + i_refobjid; + + /* Make sure we are in proper schema */ + selectSourceSchema(fout, "pg_catalog"); + + if (fout->remoteVersion >= 90300) + { + appendPQExpBuffer(query, "with recursive w as " + "( " + "select d1.objid, d2.refobjid, c2.relkind as refrelkind " + "from pg_depend d1 " + "join pg_class c1 on c1.oid = d1.objid " + "and c1.relkind = 'm' " + "join pg_rewrite r1 on r1.ev_class = d1.objid " + "join pg_depend d2 on d2.classid = 'pg_rewrite'::regclass " + "and d2.objid = r1.oid " + "and d2.refobjid <> d1.objid " + "join pg_class c2 on c2.oid = d2.refobjid " + "and c2.relkind in ('m','v') " + "where d1.classid = 'pg_class'::regclass " + "union " + "select w.objid, d3.refobjid, c3.relkind " + "from w " + "join pg_rewrite r3 on r3.ev_class = w.refobjid " + "join pg_depend d3 on d3.classid = 'pg_rewrite'::regclass " + "and d3.objid = r3.oid " + "and d3.refobjid <> w.refobjid " + "join pg_class c3 on c3.oid = d3.refobjid " + "and c3.relkind in ('m','v') " + ") " + "select 'pg_class'::regclass::oid as classid, objid, refobjid " + "from w " + "where refrelkind = 'm'"); + } + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_classid = PQfnumber(res, "classid"); + i_objid = PQfnumber(res, "objid"); + i_refobjid = PQfnumber(res, "refobjid"); + + for (i = 0; i < ntups; i++) + { + CatalogId objId; + CatalogId refobjId; + DumpableObject *dobj; + DumpableObject *refdobj; + TableInfo *tbinfo; + TableInfo *reftbinfo; + + objId.tableoid = atooid(PQgetvalue(res, i, i_classid)); + objId.oid = atooid(PQgetvalue(res, i, i_objid)); + refobjId.tableoid = objId.tableoid; + refobjId.oid = atooid(PQgetvalue(res, i, i_refobjid)); + + dobj = findObjectByCatalogId(objId); + if (dobj == NULL) + continue; + + Assert(dobj->objType == DO_TABLE); + tbinfo = (TableInfo *) dobj; + Assert(tbinfo->relkind == RELKIND_MATVIEW); + dobj = (DumpableObject *) tbinfo->dataObj; + if (dobj == NULL) + continue; + Assert(dobj->objType == DO_REFRESH_MATVIEW); + + refdobj = findObjectByCatalogId(refobjId); + if (refdobj == NULL) + continue; + + Assert(refdobj->objType == DO_TABLE); + reftbinfo = (TableInfo *) refdobj; + Assert(reftbinfo->relkind == RELKIND_MATVIEW); + refdobj = (DumpableObject *) reftbinfo->dataObj; + if (refdobj == NULL) + continue; + Assert(refdobj->objType == DO_REFRESH_MATVIEW); + + addObjectDependency(dobj, refdobj->dumpId); + + if (!reftbinfo->isscannable) + tbinfo->isscannable = false; + } + + PQclear(res); + + destroyPQExpBuffer(query); +} + /* * getTableDataFKConstraints - * add dump-order dependencies reflecting foreign key constraints @@ -3953,6 +4110,7 @@ getTables(Archive *fout, int *numTables) int i_toastoid; int i_toastfrozenxid; int i_relpersistence; + int i_isscannable; int i_owning_tab; int i_owning_col; int i_reltablespace; @@ -3970,7 +4128,7 @@ getTables(Archive *fout, int *numTables) * defined to inherit from a system catalog (pretty weird, but...) * * We ignore relations that are not ordinary tables, sequences, views, - * composite types, or foreign tables. + * materialized views, composite types, or foreign tables. * * Composite-type table entries won't be dumped as such, but we have to * make a DumpableObject for them so that we can track dependencies of the @@ -3997,7 +4155,7 @@ getTables(Archive *fout, int *numTables) "c.relhasindex, c.relhasrules, c.relhasoids, " "c.relfrozenxid, tc.oid AS toid, " "tc.relfrozenxid AS tfrozenxid, " - "c.relpersistence, " + "c.relpersistence, pg_relation_is_scannable(c.oid) as isscannable, " "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, " "d.refobjid AS owning_tab, " "d.refobjsubid AS owning_col, " @@ -4011,13 +4169,13 @@ getTables(Archive *fout, int *numTables) "d.objsubid = 0 AND " "d.refclassid = c.tableoid AND d.deptype = 'a') " "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) " - "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c') " + "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c') " "ORDER BY c.oid", username_subquery, RELKIND_SEQUENCE, RELKIND_RELATION, RELKIND_SEQUENCE, RELKIND_VIEW, RELKIND_COMPOSITE_TYPE, - RELKIND_FOREIGN_TABLE); + RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE); } else if (fout->remoteVersion >= 90000) { @@ -4033,7 +4191,7 @@ getTables(Archive *fout, int *numTables) "c.relhasindex, c.relhasrules, c.relhasoids, " "c.relfrozenxid, tc.oid AS toid, " "tc.relfrozenxid AS tfrozenxid, " - "'p' AS relpersistence, " + "'p' AS relpersistence, 't'::bool as isscannable, " "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, " "d.refobjid AS owning_tab, " "d.refobjsubid AS owning_col, " @@ -4068,7 +4226,7 @@ getTables(Archive *fout, int *numTables) "c.relhasindex, c.relhasrules, c.relhasoids, " "c.relfrozenxid, tc.oid AS toid, " "tc.relfrozenxid AS tfrozenxid, " - "'p' AS relpersistence, " + "'p' AS relpersistence, 't'::bool as isscannable, " "NULL AS reloftype, " "d.refobjid AS owning_tab, " "d.refobjsubid AS owning_col, " @@ -4103,7 +4261,7 @@ getTables(Archive *fout, int *numTables) "c.relhasindex, c.relhasrules, c.relhasoids, " "c.relfrozenxid, tc.oid AS toid, " "tc.relfrozenxid AS tfrozenxid, " - "'p' AS relpersistence, " + "'p' AS relpersistence, 't'::bool as isscannable, " "NULL AS reloftype, " "d.refobjid AS owning_tab, " "d.refobjsubid AS owning_col, " @@ -4139,7 +4297,7 @@ getTables(Archive *fout, int *numTables) "0 AS relfrozenxid, " "0 AS toid, " "0 AS tfrozenxid, " - "'p' AS relpersistence, " + "'p' AS relpersistence, 't'::bool as isscannable, " "NULL AS reloftype, " "d.refobjid AS owning_tab, " "d.refobjsubid AS owning_col, " @@ -4174,7 +4332,7 @@ getTables(Archive *fout, int *numTables) "0 AS relfrozenxid, " "0 AS toid, " "0 AS tfrozenxid, " - "'p' AS relpersistence, " + "'p' AS relpersistence, 't'::bool as isscannable, " "NULL AS reloftype, " "d.refobjid AS owning_tab, " "d.refobjsubid AS owning_col, " @@ -4205,7 +4363,7 @@ getTables(Archive *fout, int *numTables) "0 AS relfrozenxid, " "0 AS toid, " "0 AS tfrozenxid, " - "'p' AS relpersistence, " + "'p' AS relpersistence, 't'::bool as isscannable, " "NULL AS reloftype, " "NULL::oid AS owning_tab, " "NULL::int4 AS owning_col, " @@ -4231,7 +4389,7 @@ getTables(Archive *fout, int *numTables) "0 AS relfrozenxid, " "0 AS toid, " "0 AS tfrozenxid, " - "'p' AS relpersistence, " + "'p' AS relpersistence, 't'::bool as isscannable, " "NULL AS reloftype, " "NULL::oid AS owning_tab, " "NULL::int4 AS owning_col, " @@ -4267,7 +4425,7 @@ getTables(Archive *fout, int *numTables) "0 as relfrozenxid, " "0 AS toid, " "0 AS tfrozenxid, " - "'p' AS relpersistence, " + "'p' AS relpersistence, 't'::bool as isscannable, " "NULL AS reloftype, " "NULL::oid AS owning_tab, " "NULL::int4 AS owning_col, " @@ -4315,6 +4473,7 @@ getTables(Archive *fout, int *numTables) i_toastoid = PQfnumber(res, "toid"); i_toastfrozenxid = PQfnumber(res, "tfrozenxid"); i_relpersistence = PQfnumber(res, "relpersistence"); + i_isscannable = PQfnumber(res, "isscannable"); i_owning_tab = PQfnumber(res, "owning_tab"); i_owning_col = PQfnumber(res, "owning_col"); i_reltablespace = PQfnumber(res, "reltablespace"); @@ -4356,6 +4515,7 @@ getTables(Archive *fout, int *numTables) tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0); tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0); tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0); + tblinfo[i].isscannable = (strcmp(PQgetvalue(res, i, i_isscannable), "t") == 0); tblinfo[i].frozenxid = atooid(PQgetvalue(res, i, i_relfrozenxid)); tblinfo[i].toast_oid = atooid(PQgetvalue(res, i, i_toastoid)); tblinfo[i].toast_frozenxid = atooid(PQgetvalue(res, i, i_toastfrozenxid)); @@ -4551,8 +4711,11 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) { TableInfo *tbinfo = &tblinfo[i]; - /* Only plain tables have indexes */ - if (tbinfo->relkind != RELKIND_RELATION || !tbinfo->hasindex) + /* Only plain tables and materialized views have indexes. */ + if (tbinfo->relkind != RELKIND_RELATION && + tbinfo->relkind != RELKIND_MATVIEW) + continue; + if (!tbinfo->hasindex) continue; /* Ignore indexes of tables not to be dumped */ @@ -5134,12 +5297,14 @@ getRules(Archive *fout, int *numRules) if (ruleinfo[i].ruletable) { /* - * If the table is a view, force its ON SELECT rule to be sorted - * before the view itself --- this ensures that any dependencies - * for the rule affect the table's positioning. Other rules are - * forced to appear after their table. + * If the table is a view or materialized view, force its ON + * SELECT rule to be sorted before the view itself --- this + * ensures that any dependencies for the rule affect the table's + * positioning. Other rules are forced to appear after their + * table. */ - if (ruleinfo[i].ruletable->relkind == RELKIND_VIEW && + if ((ruleinfo[i].ruletable->relkind == RELKIND_VIEW || + ruleinfo[i].ruletable->relkind == RELKIND_MATVIEW) && ruleinfo[i].ev_type == '1' && ruleinfo[i].is_instead) { addObjectDependency(&ruleinfo[i].ruletable->dobj, @@ -7345,6 +7510,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_INDEX: dumpIndex(fout, (IndxInfo *) dobj); break; + case DO_REFRESH_MATVIEW: + refreshMatViewData(fout, (TableDataInfo *) dobj); + break; case DO_RULE: dumpRule(fout, (RuleInfo *) dobj); break; @@ -12383,6 +12551,64 @@ dumpTable(Archive *fout, TableInfo *tbinfo) } } +/* + * Create the AS clause for a view or materialized view. The semicolon is + * stripped because a materialized view must add a WITH NO DATA clause. + * + * This returns a new buffer which must be freed by the caller. + */ +static PQExpBuffer +createViewAsClause(Archive *fout, TableInfo *tbinfo) +{ + PQExpBuffer query = createPQExpBuffer(); + PQExpBuffer result = createPQExpBuffer(); + PGresult *res; + int len; + + /* Fetch the view definition */ + if (fout->remoteVersion >= 70300) + { + /* Beginning in 7.3, viewname is not unique; rely on OID */ + appendPQExpBuffer(query, + "SELECT pg_catalog.pg_get_viewdef('%u'::pg_catalog.oid) AS viewdef", + tbinfo->dobj.catId.oid); + } + else + { + appendPQExpBuffer(query, "SELECT definition AS viewdef " + "FROM pg_views WHERE viewname = "); + appendStringLiteralAH(query, tbinfo->dobj.name, fout); + appendPQExpBuffer(query, ";"); + } + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + if (PQntuples(res) != 1) + { + if (PQntuples(res) < 1) + exit_horribly(NULL, "query to obtain definition of view \"%s\" returned no data\n", + tbinfo->dobj.name); + else + exit_horribly(NULL, "query to obtain definition of view \"%s\" returned more than one definition\n", + tbinfo->dobj.name); + } + + len = PQgetlength(res, 0, 0); + + if (len == 0) + exit_horribly(NULL, "definition of view \"%s\" appears to be empty (length zero)\n", + tbinfo->dobj.name); + + /* Strip off the trailing semicolon so that other things may follow. */ + Assert(PQgetvalue(res, 0, 0)[len-1] == ';'); + appendBinaryPQExpBuffer(result, PQgetvalue(res, 0, 0), len - 1); + + PQclear(res); + destroyPQExpBuffer(query); + + return result; +} + /* * dumpTableSchema * write the declaration (not data) of one user-defined table or view @@ -12390,11 +12616,9 @@ dumpTable(Archive *fout, TableInfo *tbinfo) static void dumpTableSchema(Archive *fout, TableInfo *tbinfo) { - PQExpBuffer query = createPQExpBuffer(); PQExpBuffer q = createPQExpBuffer(); PQExpBuffer delq = createPQExpBuffer(); PQExpBuffer labelq = createPQExpBuffer(); - PGresult *res; int numParents; TableInfo **parents; int actual_atts; /* number of attrs in this CREATE statement */ @@ -12415,44 +12639,10 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) /* Is it a table or a view? */ if (tbinfo->relkind == RELKIND_VIEW) { - char *viewdef; + PQExpBuffer result; reltypename = "VIEW"; - /* Fetch the view definition */ - if (fout->remoteVersion >= 70300) - { - /* Beginning in 7.3, viewname is not unique; rely on OID */ - appendPQExpBuffer(query, - "SELECT pg_catalog.pg_get_viewdef('%u'::pg_catalog.oid) AS viewdef", - tbinfo->dobj.catId.oid); - } - else - { - appendPQExpBuffer(query, "SELECT definition AS viewdef " - "FROM pg_views WHERE viewname = "); - appendStringLiteralAH(query, tbinfo->dobj.name, fout); - appendPQExpBuffer(query, ";"); - } - - res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); - - if (PQntuples(res) != 1) - { - if (PQntuples(res) < 1) - exit_horribly(NULL, "query to obtain definition of view \"%s\" returned no data\n", - tbinfo->dobj.name); - else - exit_horribly(NULL, "query to obtain definition of view \"%s\" returned more than one definition\n", - tbinfo->dobj.name); - } - - viewdef = PQgetvalue(res, 0, 0); - - if (strlen(viewdef) == 0) - exit_horribly(NULL, "definition of view \"%s\" appears to be empty (length zero)\n", - tbinfo->dobj.name); - /* * DROP must be fully qualified in case same name appears in * pg_catalog @@ -12469,49 +12659,60 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) appendPQExpBuffer(q, "CREATE VIEW %s", fmtId(tbinfo->dobj.name)); if (tbinfo->reloptions && strlen(tbinfo->reloptions) > 0) appendPQExpBuffer(q, " WITH (%s)", tbinfo->reloptions); - appendPQExpBuffer(q, " AS\n %s\n", viewdef); + result = createViewAsClause(fout, tbinfo); + appendPQExpBuffer(q, " AS\n%s;\n", result->data); + destroyPQExpBuffer(result); appendPQExpBuffer(labelq, "VIEW %s", fmtId(tbinfo->dobj.name)); - - PQclear(res); } else { - if (tbinfo->relkind == RELKIND_FOREIGN_TABLE) + switch (tbinfo->relkind) { - int i_srvname; - int i_ftoptions; + case (RELKIND_FOREIGN_TABLE): + { + PQExpBuffer query = createPQExpBuffer(); + PGresult *res; + int i_srvname; + int i_ftoptions; - reltypename = "FOREIGN TABLE"; + reltypename = "FOREIGN TABLE"; - /* retrieve name of foreign server and generic options */ - appendPQExpBuffer(query, - "SELECT fs.srvname, " - "pg_catalog.array_to_string(ARRAY(" - "SELECT pg_catalog.quote_ident(option_name) || " - "' ' || pg_catalog.quote_literal(option_value) " - "FROM pg_catalog.pg_options_to_table(ftoptions) " - "ORDER BY option_name" - "), E',\n ') AS ftoptions " - "FROM pg_catalog.pg_foreign_table ft " - "JOIN pg_catalog.pg_foreign_server fs " - "ON (fs.oid = ft.ftserver) " - "WHERE ft.ftrelid = '%u'", - tbinfo->dobj.catId.oid); - res = ExecuteSqlQueryForSingleRow(fout, query->data); - i_srvname = PQfnumber(res, "srvname"); - i_ftoptions = PQfnumber(res, "ftoptions"); - srvname = pg_strdup(PQgetvalue(res, 0, i_srvname)); - ftoptions = pg_strdup(PQgetvalue(res, 0, i_ftoptions)); - PQclear(res); - } - else - { - reltypename = "TABLE"; - srvname = NULL; - ftoptions = NULL; + /* retrieve name of foreign server and generic options */ + appendPQExpBuffer(query, + "SELECT fs.srvname, " + "pg_catalog.array_to_string(ARRAY(" + "SELECT pg_catalog.quote_ident(option_name) || " + "' ' || pg_catalog.quote_literal(option_value) " + "FROM pg_catalog.pg_options_to_table(ftoptions) " + "ORDER BY option_name" + "), E',\n ') AS ftoptions " + "FROM pg_catalog.pg_foreign_table ft " + "JOIN pg_catalog.pg_foreign_server fs " + "ON (fs.oid = ft.ftserver) " + "WHERE ft.ftrelid = '%u'", + tbinfo->dobj.catId.oid); + res = ExecuteSqlQueryForSingleRow(fout, query->data); + i_srvname = PQfnumber(res, "srvname"); + i_ftoptions = PQfnumber(res, "ftoptions"); + srvname = pg_strdup(PQgetvalue(res, 0, i_srvname)); + ftoptions = pg_strdup(PQgetvalue(res, 0, i_ftoptions)); + PQclear(res); + destroyPQExpBuffer(query); + break; + } + case (RELKIND_MATVIEW): + reltypename = "MATERIALIZED VIEW"; + srvname = NULL; + ftoptions = NULL; + break; + default: + reltypename = "TABLE"; + srvname = NULL; + ftoptions = NULL; } + numParents = tbinfo->numParents; parents = tbinfo->parents; @@ -12544,6 +12745,8 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) if (tbinfo->reloftype && !binary_upgrade) appendPQExpBuffer(q, " OF %s", tbinfo->reloftype); + if (tbinfo->relkind != RELKIND_MATVIEW) + { /* Dump the attributes */ actual_atts = 0; for (j = 0; j < tbinfo->numatts; j++) @@ -12583,7 +12786,7 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) actual_atts++; /* Attribute name */ - appendPQExpBuffer(q, "%s ", + appendPQExpBuffer(q, "%s", fmtId(tbinfo->attnames[j])); if (tbinfo->attisdropped[j]) @@ -12593,7 +12796,7 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) * so we will not have gotten a valid type name; insert * INTEGER as a stopgap. We'll clean things up later. */ - appendPQExpBuffer(q, "INTEGER /* dummy */"); + appendPQExpBuffer(q, " INTEGER /* dummy */"); /* Skip all the rest, too */ continue; } @@ -12601,17 +12804,17 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) /* Attribute type */ if (tbinfo->reloftype && !binary_upgrade) { - appendPQExpBuffer(q, "WITH OPTIONS"); + appendPQExpBuffer(q, " WITH OPTIONS"); } else if (fout->remoteVersion >= 70100) { - appendPQExpBuffer(q, "%s", + appendPQExpBuffer(q, " %s", tbinfo->atttypnames[j]); } else { /* If no format_type, fake it */ - appendPQExpBuffer(q, "%s", + appendPQExpBuffer(q, " %s", myFormatType(tbinfo->atttypnames[j], tbinfo->atttypmod[j])); } @@ -12694,6 +12897,7 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) if (tbinfo->relkind == RELKIND_FOREIGN_TABLE) appendPQExpBuffer(q, "\nSERVER %s", fmtId(srvname)); + } if ((tbinfo->reloptions && strlen(tbinfo->reloptions) > 0) || (tbinfo->toast_reloptions && strlen(tbinfo->toast_reloptions) > 0)) @@ -12718,7 +12922,20 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) if (ftoptions && ftoptions[0]) appendPQExpBuffer(q, "\nOPTIONS (\n %s\n)", ftoptions); - appendPQExpBuffer(q, ";\n"); + /* + * For materialized views, create the AS clause just like a view. + */ + if (tbinfo->relkind == RELKIND_MATVIEW) + { + PQExpBuffer result; + + result = createViewAsClause(fout, tbinfo); + appendPQExpBuffer(q, " AS\n%s\n WITH NO DATA;\n", + result->data); + destroyPQExpBuffer(result); + } + else + appendPQExpBuffer(q, ";\n"); /* * To create binary-compatible heap files, we have to ensure the same @@ -12974,7 +13191,6 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) dumpTableConstraintComment(fout, constr); } - destroyPQExpBuffer(query); destroyPQExpBuffer(q); destroyPQExpBuffer(delq); destroyPQExpBuffer(labelq); @@ -14468,6 +14684,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, addObjectDependency(postDataBound, dobj->dumpId); break; case DO_INDEX: + case DO_REFRESH_MATVIEW: case DO_TRIGGER: case DO_EVENT_TRIGGER: case DO_DEFAULT_ACL: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index b6dc856600..01ec27b632 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -110,7 +110,8 @@ typedef enum DO_BLOB_DATA, DO_PRE_DATA_BOUNDARY, DO_POST_DATA_BOUNDARY, - DO_EVENT_TRIGGER + DO_EVENT_TRIGGER, + DO_REFRESH_MATVIEW } DumpableObjectType; typedef struct _dumpableObject @@ -242,6 +243,7 @@ typedef struct _tableInfo bool hasrules; /* does it have any rules? */ bool hastriggers; /* does it have any triggers? */ bool hasoids; /* does it have OIDs? */ + bool isscannable; /* is valid for use in queries */ uint32 frozenxid; /* for restore frozen xid */ Oid toast_oid; /* for restore toast frozen xid */ uint32 toast_frozenxid; /* for restore toast frozen xid */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 955c231b1a..2c3d850f3d 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -24,9 +24,9 @@ static const char *modulename = gettext_noop("sorter"); * Objects are sorted by priority levels, and within an equal priority level * by OID. (This is a relatively crude hack to provide semi-reasonable * behavior for old databases without full dependency info.) Note: collations, - * extensions, text search, foreign-data, event trigger, and default ACL - * objects can't really happen here, so the rather bogus priorities for them - * don't matter. + * extensions, text search, foreign-data, materialized view, event trigger, + * and default ACL objects can't really happen here, so the rather bogus + * priorities for them don't matter. * * NOTE: object-type priorities must match the section assignments made in * pg_dump.c; that is, PRE_DATA objects must sort before DO_PRE_DATA_BOUNDARY, @@ -68,7 +68,8 @@ static const int oldObjectTypePriority[] = 12, /* DO_BLOB_DATA */ 10, /* DO_PRE_DATA_BOUNDARY */ 13, /* DO_POST_DATA_BOUNDARY */ - 20 /* DO_EVENT_TRIGGER */ + 20, /* DO_EVENT_TRIGGER */ + 15 /* DO_REFRESH_MATVIEW */ }; /* @@ -115,7 +116,8 @@ static const int newObjectTypePriority[] = 24, /* DO_BLOB_DATA */ 22, /* DO_PRE_DATA_BOUNDARY */ 25, /* DO_POST_DATA_BOUNDARY */ - 32 /* DO_EVENT_TRIGGER */ + 32, /* DO_EVENT_TRIGGER */ + 33 /* DO_REFRESH_MATVIEW */ }; static DumpId preDataBoundId; @@ -1152,6 +1154,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "INDEX %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_REFRESH_MATVIEW: + snprintf(buf, bufsize, + "REFRESH MATERIALIZED VIEW %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; case DO_RULE: snprintf(buf, bufsize, "RULE %s (ID %d OID %u)", diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 012cb75e52..3be7c442a4 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -355,7 +355,7 @@ exec_command(const char *cmd, success = describeTableDetails(pattern, show_verbose, show_system); else /* standard listing of interesting things */ - success = listTables("tvsE", NULL, show_verbose, show_system); + success = listTables("tvmsE", NULL, show_verbose, show_system); break; case 'a': success = describeAggregates(pattern, show_verbose, show_system); @@ -422,6 +422,7 @@ exec_command(const char *cmd, break; case 't': case 'v': + case 'm': case 'i': case 's': case 'E': diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 8064a3d702..217c3f5a3a 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -721,11 +721,20 @@ permissionsList(const char *pattern) printfPQExpBuffer(&buf, "SELECT n.nspname as \"%s\",\n" " c.relname as \"%s\",\n" - " CASE c.relkind WHEN 'r' THEN '%s' WHEN 'v' THEN '%s' WHEN 'S' THEN '%s' WHEN 'f' THEN '%s' END as \"%s\",\n" + " CASE c.relkind" + " WHEN 'r' THEN '%s'" + " WHEN 'v' THEN '%s'" + " WHEN 'm' THEN '%s'" + " WHEN 'S' THEN '%s'" + " WHEN 'f' THEN '%s'" + " END as \"%s\",\n" " ", gettext_noop("Schema"), gettext_noop("Name"), - gettext_noop("table"), gettext_noop("view"), gettext_noop("sequence"), + gettext_noop("table"), + gettext_noop("view"), + gettext_noop("materialized view"), + gettext_noop("sequence"), gettext_noop("foreign table"), gettext_noop("Type")); @@ -742,7 +751,7 @@ permissionsList(const char *pattern) appendPQExpBuffer(&buf, "\nFROM pg_catalog.pg_class c\n" " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n" - "WHERE c.relkind IN ('r', 'v', 'S', 'f')\n"); + "WHERE c.relkind IN ('r', 'v', 'm', 'S', 'f')\n"); /* * Unless a schema pattern is specified, we suppress system and temp @@ -1319,6 +1328,7 @@ describeOneTableDetails(const char *schemaname, * types, and foreign tables (c.f. CommentObject() in comment.c). */ if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v' || + tableinfo.relkind == 'm' || tableinfo.relkind == 'f' || tableinfo.relkind == 'c') appendPQExpBuffer(&buf, ", pg_catalog.col_description(a.attrelid, a.attnum)"); } @@ -1347,6 +1357,14 @@ describeOneTableDetails(const char *schemaname, printfPQExpBuffer(&title, _("View \"%s.%s\""), schemaname, relationname); break; + case 'm': + if (tableinfo.relpersistence == 'u') + printfPQExpBuffer(&title, _("Unlogged materialized view \"%s.%s\""), + schemaname, relationname); + else + printfPQExpBuffer(&title, _("Materialized view \"%s.%s\""), + schemaname, relationname); + break; case 'S': printfPQExpBuffer(&title, _("Sequence \"%s.%s\""), schemaname, relationname); @@ -1389,6 +1407,7 @@ describeOneTableDetails(const char *schemaname, cols = 2; if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v' || + tableinfo.relkind == 'm' || tableinfo.relkind == 'f' || tableinfo.relkind == 'c') { show_modifiers = true; @@ -1408,10 +1427,12 @@ describeOneTableDetails(const char *schemaname, if (verbose) { headers[cols++] = gettext_noop("Storage"); - if (tableinfo.relkind == 'r' || tableinfo.relkind == 'f') + if (tableinfo.relkind == 'r' || tableinfo.relkind == 'm' || + tableinfo.relkind == 'f') headers[cols++] = gettext_noop("Stats target"); /* Column comments, if the relkind supports this feature. */ if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v' || + tableinfo.relkind == 'm' || tableinfo.relkind == 'c' || tableinfo.relkind == 'f') headers[cols++] = gettext_noop("Description"); } @@ -1422,8 +1443,8 @@ describeOneTableDetails(const char *schemaname, for (i = 0; i < cols; i++) printTableAddHeader(&cont, headers[i], true, 'l'); - /* Check if table is a view */ - if (tableinfo.relkind == 'v' && verbose) + /* Check if table is a view or materialized view */ + if ((tableinfo.relkind == 'v' || tableinfo.relkind == 'm') && verbose) { PGresult *result; @@ -1511,7 +1532,8 @@ describeOneTableDetails(const char *schemaname, false, false); /* Statistics target, if the relkind supports this feature */ - if (tableinfo.relkind == 'r' || tableinfo.relkind == 'f') + if (tableinfo.relkind == 'r' || tableinfo.relkind == 'm' || + tableinfo.relkind == 'f') { printTableAddCell(&cont, PQgetvalue(res, i, firstvcol + 1), false, false); @@ -1519,6 +1541,7 @@ describeOneTableDetails(const char *schemaname, /* Column comments, if the relkind supports this feature. */ if (tableinfo.relkind == 'r' || tableinfo.relkind == 'v' || + tableinfo.relkind == 'm' || tableinfo.relkind == 'c' || tableinfo.relkind == 'f') printTableAddCell(&cont, PQgetvalue(res, i, firstvcol + 2), false, false); @@ -1615,44 +1638,6 @@ describeOneTableDetails(const char *schemaname, PQclear(result); } - else if (view_def) - { - PGresult *result = NULL; - - /* Footer information about a view */ - printTableAddFooter(&cont, _("View definition:")); - printTableAddFooter(&cont, view_def); - - /* print rules */ - if (tableinfo.hasrules) - { - printfPQExpBuffer(&buf, - "SELECT r.rulename, trim(trailing ';' from pg_catalog.pg_get_ruledef(r.oid, true))\n" - "FROM pg_catalog.pg_rewrite r\n" - "WHERE r.ev_class = '%s' AND r.rulename != '_RETURN' ORDER BY 1;", - oid); - result = PSQLexec(buf.data, false); - if (!result) - goto error_return; - - if (PQntuples(result) > 0) - { - printTableAddFooter(&cont, _("Rules:")); - for (i = 0; i < PQntuples(result); i++) - { - const char *ruledef; - - /* Everything after "CREATE RULE" is echoed verbatim */ - ruledef = PQgetvalue(result, i, 1); - ruledef += 12; - - printfPQExpBuffer(&buf, " %s", ruledef); - printTableAddFooter(&cont, buf.data); - } - } - PQclear(result); - } - } else if (tableinfo.relkind == 'S') { /* Footer information about a sequence */ @@ -1691,7 +1676,8 @@ describeOneTableDetails(const char *schemaname, */ PQclear(result); } - else if (tableinfo.relkind == 'r' || tableinfo.relkind == 'f') + else if (tableinfo.relkind == 'r' || tableinfo.relkind == 'm' || + tableinfo.relkind == 'f') { /* Footer information about a table */ PGresult *result = NULL; @@ -1892,7 +1878,7 @@ describeOneTableDetails(const char *schemaname, } /* print rules */ - if (tableinfo.hasrules) + if (tableinfo.hasrules && tableinfo.relkind != 'm') { if (pset.sversion >= 80300) { @@ -1987,6 +1973,45 @@ describeOneTableDetails(const char *schemaname, } } + if (view_def) + { + PGresult *result = NULL; + + /* Footer information about a view */ + printTableAddFooter(&cont, _("View definition:")); + printTableAddFooter(&cont, view_def); + + /* print rules */ + if (tableinfo.hasrules) + { + printfPQExpBuffer(&buf, + "SELECT r.rulename, trim(trailing ';' from pg_catalog.pg_get_ruledef(r.oid, true))\n" + "FROM pg_catalog.pg_rewrite r\n" + "WHERE r.ev_class = '%s' AND r.rulename != '_RETURN' ORDER BY 1;", + oid); + result = PSQLexec(buf.data, false); + if (!result) + goto error_return; + + if (PQntuples(result) > 0) + { + printTableAddFooter(&cont, _("Rules:")); + for (i = 0; i < PQntuples(result); i++) + { + const char *ruledef; + + /* Everything after "CREATE RULE" is echoed verbatim */ + ruledef = PQgetvalue(result, i, 1); + ruledef += 12; + + printfPQExpBuffer(&buf, " %s", ruledef); + printTableAddFooter(&cont, buf.data); + } + } + PQclear(result); + } + } + /* * Print triggers next, if any (but only user-defined triggers). This * could apply to either a table or a view. @@ -2110,7 +2135,8 @@ describeOneTableDetails(const char *schemaname, /* * Finish printing the footer information about a table. */ - if (tableinfo.relkind == 'r' || tableinfo.relkind == 'f') + if (tableinfo.relkind == 'r' || tableinfo.relkind == 'm' || + tableinfo.relkind == 'f') { PGresult *result; int tuples; @@ -2235,8 +2261,8 @@ describeOneTableDetails(const char *schemaname, printTableAddFooter(&cont, buf.data); } - /* OIDs, if verbose */ - if (verbose) + /* OIDs, if verbose and not a materialized view */ + if (verbose && tableinfo.relkind != 'm') { const char *s = _("Has OIDs"); @@ -2307,7 +2333,7 @@ add_tablespace_footer(printTableContent *const cont, char relkind, Oid tablespace, const bool newline) { /* relkinds for which we support tablespaces */ - if (relkind == 'r' || relkind == 'i') + if (relkind == 'r' || relkind == 'm' || relkind == 'i') { /* * We ignore the database default tablespace so that users not using @@ -2589,6 +2615,7 @@ listDbRoleSettings(const char *pattern, const char *pattern2) * t - tables * i - indexes * v - views + * m - materialized views * s - sequences * E - foreign table (Note: different from 'f', the relkind value) * (any order of the above is fine) @@ -2600,6 +2627,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys bool showTables = strchr(tabtypes, 't') != NULL; bool showIndexes = strchr(tabtypes, 'i') != NULL; bool showViews = strchr(tabtypes, 'v') != NULL; + bool showMatViews = strchr(tabtypes, 'm') != NULL; bool showSeq = strchr(tabtypes, 's') != NULL; bool showForeign = strchr(tabtypes, 'E') != NULL; @@ -2608,8 +2636,8 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys printQueryOpt myopt = pset.popt; static const bool translate_columns[] = {false, false, true, false, false, false, false}; - if (!(showTables || showIndexes || showViews || showSeq || showForeign)) - showTables = showViews = showSeq = showForeign = true; + if (!(showTables || showIndexes || showViews || showMatViews || showSeq || showForeign)) + showTables = showViews = showMatViews = showSeq = showForeign = true; initPQExpBuffer(&buf); @@ -2620,12 +2648,21 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys printfPQExpBuffer(&buf, "SELECT n.nspname as \"%s\",\n" " c.relname as \"%s\",\n" - " CASE c.relkind WHEN 'r' THEN '%s' WHEN 'v' THEN '%s' WHEN 'i' THEN '%s' WHEN 'S' THEN '%s' WHEN 's' THEN '%s' WHEN 'f' THEN '%s' END as \"%s\",\n" + " CASE c.relkind" + " WHEN 'r' THEN '%s'" + " WHEN 'v' THEN '%s'" + " WHEN 'm' THEN '%s'" + " WHEN 'i' THEN '%s'" + " WHEN 'S' THEN '%s'" + " WHEN 's' THEN '%s'" + " WHEN 'f' THEN '%s'" + " END as \"%s\",\n" " pg_catalog.pg_get_userbyid(c.relowner) as \"%s\"", gettext_noop("Schema"), gettext_noop("Name"), gettext_noop("table"), gettext_noop("view"), + gettext_noop("materialized view"), gettext_noop("index"), gettext_noop("sequence"), gettext_noop("special"), @@ -2671,6 +2708,8 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys appendPQExpBuffer(&buf, "'r',"); if (showViews) appendPQExpBuffer(&buf, "'v',"); + if (showMatViews) + appendPQExpBuffer(&buf, "'m',"); if (showIndexes) appendPQExpBuffer(&buf, "'i',"); if (showSeq) diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 43cb550bd2..819a20f18d 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -221,6 +221,7 @@ slashUsage(unsigned short int pager) fprintf(output, _(" \\di[S+] [PATTERN] list indexes\n")); fprintf(output, _(" \\dl list large objects, same as \\lo_list\n")); fprintf(output, _(" \\dL[S+] [PATTERN] list procedural languages\n")); + fprintf(output, _(" \\dm[S+] [PATTERN] list materialized views\n")); fprintf(output, _(" \\dn[S+] [PATTERN] list schemas\n")); fprintf(output, _(" \\do[S] [PATTERN] list operators\n")); fprintf(output, _(" \\dO[S+] [PATTERN] list collations\n")); diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index edfba67766..d2170308f7 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -435,11 +435,11 @@ static const SchemaQuery Query_for_list_of_relations = { NULL }; -static const SchemaQuery Query_for_list_of_tsvf = { +static const SchemaQuery Query_for_list_of_tsvmf = { /* catname */ "pg_catalog.pg_class c", /* selcondition */ - "c.relkind IN ('r', 'S', 'v', 'f')", + "c.relkind IN ('r', 'S', 'v', 'm', 'f')", /* viscondition */ "pg_catalog.pg_table_is_visible(c.oid)", /* namespace */ @@ -450,11 +450,26 @@ static const SchemaQuery Query_for_list_of_tsvf = { NULL }; -static const SchemaQuery Query_for_list_of_tf = { +static const SchemaQuery Query_for_list_of_tmf = { /* catname */ "pg_catalog.pg_class c", /* selcondition */ - "c.relkind IN ('r', 'f')", + "c.relkind IN ('r', 'm', 'f')", + /* viscondition */ + "pg_catalog.pg_table_is_visible(c.oid)", + /* namespace */ + "c.relnamespace", + /* result */ + "pg_catalog.quote_ident(c.relname)", + /* qualresult */ + NULL +}; + +static const SchemaQuery Query_for_list_of_tm = { + /* catname */ + "pg_catalog.pg_class c", + /* selcondition */ + "c.relkind IN ('r', 'm')", /* viscondition */ "pg_catalog.pg_table_is_visible(c.oid)", /* namespace */ @@ -480,6 +495,21 @@ static const SchemaQuery Query_for_list_of_views = { NULL }; +static const SchemaQuery Query_for_list_of_matviews = { + /* catname */ + "pg_catalog.pg_class c", + /* selcondition */ + "c.relkind IN ('m')", + /* viscondition */ + "pg_catalog.pg_table_is_visible(c.oid)", + /* namespace */ + "c.relnamespace", + /* result */ + "pg_catalog.quote_ident(c.relname)", + /* qualresult */ + NULL +}; + /* * Queries to get lists of names of various kinds of things, possibly @@ -752,6 +782,7 @@ static const pgsql_thing_t words_after_create[] = { {"GROUP", Query_for_list_of_roles}, {"LANGUAGE", Query_for_list_of_languages}, {"INDEX", NULL, &Query_for_list_of_indexes}, + {"MATERIALIZED VIEW", NULL, NULL}, {"OPERATOR", NULL, NULL}, /* Querying for this is probably not such a * good idea. */ {"OWNED", NULL, NULL, THING_NO_CREATE}, /* for DROP OWNED BY ... */ @@ -853,7 +884,7 @@ psql_completion(char *text, int start, int end) "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", "FETCH", "GRANT", "INSERT", "LISTEN", "LOAD", "LOCK", "MOVE", "NOTIFY", "PREPARE", - "REASSIGN", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", + "REASSIGN", "REFRESH", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", "TABLE", "TRUNCATE", "UNLISTEN", "UPDATE", "VACUUM", "VALUES", "WITH", NULL @@ -933,7 +964,7 @@ psql_completion(char *text, int start, int end) static const char *const list_ALTER[] = {"AGGREGATE", "COLLATION", "CONVERSION", "DATABASE", "DEFAULT PRIVILEGES", "DOMAIN", "EXTENSION", "FOREIGN DATA WRAPPER", "FOREIGN TABLE", "FUNCTION", - "GROUP", "INDEX", "LANGUAGE", "LARGE OBJECT", "OPERATOR", + "GROUP", "INDEX", "LANGUAGE", "LARGE OBJECT", "MATERIALIZED VIEW", "OPERATOR", "ROLE", "RULE", "SCHEMA", "SERVER", "SEQUENCE", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRIGGER", "TYPE", "USER", "USER MAPPING FOR", "VIEW", NULL}; @@ -1102,6 +1133,14 @@ psql_completion(char *text, int start, int end) COMPLETE_WITH_LIST(list_ALTERLARGEOBJECT); } + /* ALTER MATERIALIZED VIEW */ + else if (pg_strcasecmp(prev3_wd, "ALTER") == 0 && + pg_strcasecmp(prev2_wd, "MATERIALIZED") == 0 && + pg_strcasecmp(prev_wd, "VIEW") == 0) + { + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL); + } + /* ALTER USER,ROLE */ else if (pg_strcasecmp(prev3_wd, "ALTER") == 0 && !(pg_strcasecmp(prev2_wd, "USER") == 0 && pg_strcasecmp(prev_wd, "MAPPING") == 0) && @@ -1268,6 +1307,16 @@ psql_completion(char *text, int start, int end) COMPLETE_WITH_LIST(list_ALTERVIEW); } + /* ALTER MATERIALIZED VIEW */ + else if (pg_strcasecmp(prev4_wd, "ALTER") == 0 && + pg_strcasecmp(prev3_wd, "MATERIALIZED") == 0 && + pg_strcasecmp(prev2_wd, "VIEW") == 0) + { + static const char *const list_ALTERMATVIEW[] = + {"ALTER COLUMN", "OWNER TO", "RENAME TO", "SET SCHEMA", NULL}; + + COMPLETE_WITH_LIST(list_ALTERMATVIEW); + } /* ALTER RULE , add ON */ else if (pg_strcasecmp(prev3_wd, "ALTER") == 0 && @@ -1746,14 +1795,14 @@ psql_completion(char *text, int start, int end) */ else if (pg_strcasecmp(prev_wd, "CLUSTER") == 0 && pg_strcasecmp(prev2_wd, "WITHOUT") != 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, "UNION SELECT 'VERBOSE'"); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, "UNION SELECT 'VERBOSE'"); /* * If the previous words are CLUSTER VERBOSE produce list of tables */ else if (pg_strcasecmp(prev_wd, "VERBOSE") == 0 && pg_strcasecmp(prev2_wd, "CLUSTER") == 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, NULL); /* If we have CLUSTER , then add "USING" */ else if (pg_strcasecmp(prev2_wd, "CLUSTER") == 0 && @@ -1800,7 +1849,7 @@ psql_completion(char *text, int start, int end) {"CAST", "COLLATION", "CONVERSION", "DATABASE", "EXTENSION", "FOREIGN DATA WRAPPER", "FOREIGN TABLE", "SERVER", "INDEX", "LANGUAGE", "RULE", "SCHEMA", "SEQUENCE", - "TABLE", "TYPE", "VIEW", "COLUMN", "AGGREGATE", "FUNCTION", + "TABLE", "TYPE", "VIEW", "MATERIALIZED VIEW", "COLUMN", "AGGREGATE", "FUNCTION", "OPERATOR", "TRIGGER", "CONSTRAINT", "DOMAIN", "LARGE OBJECT", "TABLESPACE", "TEXT SEARCH", "ROLE", NULL}; @@ -1845,6 +1894,13 @@ psql_completion(char *text, int start, int end) completion_info_charp = prev2_wd; COMPLETE_WITH_QUERY(Query_for_list_of_tables_for_constraint); } + else if (pg_strcasecmp(prev4_wd, "COMMENT") == 0 && + pg_strcasecmp(prev3_wd, "ON") == 0 && + pg_strcasecmp(prev2_wd, "MATERIALIZED") == 0 && + pg_strcasecmp(prev_wd, "VIEW") == 0) + { + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL); + } else if ((pg_strcasecmp(prev4_wd, "COMMENT") == 0 && pg_strcasecmp(prev3_wd, "ON") == 0) || (pg_strcasecmp(prev5_wd, "COMMENT") == 0 && @@ -1974,7 +2030,7 @@ psql_completion(char *text, int start, int end) pg_strcasecmp(prev2_wd, "INDEX") == 0 || pg_strcasecmp(prev2_wd, "CONCURRENTLY") == 0) && pg_strcasecmp(prev_wd, "ON") == 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, NULL); /* If we have CREATE|UNIQUE INDEX CONCURRENTLY, then add "ON" */ else if ((pg_strcasecmp(prev3_wd, "INDEX") == 0 || pg_strcasecmp(prev2_wd, "INDEX") == 0) && @@ -2080,7 +2136,10 @@ psql_completion(char *text, int start, int end) else if (pg_strcasecmp(prev2_wd, "CREATE") == 0 && pg_strcasecmp(prev_wd, "UNLOGGED") == 0) { - COMPLETE_WITH_CONST("TABLE"); + static const char *const list_UNLOGGED[] = + {"TABLE", "MATERIALIZED VIEW", NULL}; + + COMPLETE_WITH_LIST(list_UNLOGGED); } /* CREATE TABLESPACE */ @@ -2249,6 +2308,22 @@ psql_completion(char *text, int start, int end) pg_strcasecmp(prev_wd, "AS") == 0) COMPLETE_WITH_CONST("SELECT"); +/* CREATE MATERIALIZED VIEW */ + else if (pg_strcasecmp(prev2_wd, "CREATE") == 0 && + pg_strcasecmp(prev_wd, "MATERIALIZED") == 0) + COMPLETE_WITH_CONST("VIEW"); + /* Complete CREATE MATERIALIZED VIEW with AS */ + else if (pg_strcasecmp(prev4_wd, "CREATE") == 0 && + pg_strcasecmp(prev3_wd, "MATERIALIZED") == 0 && + pg_strcasecmp(prev2_wd, "VIEW") == 0) + COMPLETE_WITH_CONST("AS"); + /* Complete "CREATE MATERIALIZED VIEW AS with "SELECT" */ + else if (pg_strcasecmp(prev5_wd, "CREATE") == 0 && + pg_strcasecmp(prev4_wd, "MATERIALIZED") == 0 && + pg_strcasecmp(prev3_wd, "VIEW") == 0 && + pg_strcasecmp(prev_wd, "AS") == 0) + COMPLETE_WITH_CONST("SELECT"); + /* DECLARE */ else if (pg_strcasecmp(prev2_wd, "DECLARE") == 0) { @@ -2375,6 +2450,20 @@ psql_completion(char *text, int start, int end) COMPLETE_WITH_LIST(drop_CREATE_FOREIGN); } + + /* DROP MATERIALIZED VIEW */ + else if (pg_strcasecmp(prev2_wd, "DROP") == 0 && + pg_strcasecmp(prev_wd, "MATERIALIZED") == 0) + { + COMPLETE_WITH_CONST("VIEW"); + } + else if (pg_strcasecmp(prev3_wd, "DROP") == 0 && + pg_strcasecmp(prev2_wd, "MATERIALIZED") == 0 && + pg_strcasecmp(prev_wd, "VIEW") == 0) + { + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL); + } + else if (pg_strcasecmp(prev4_wd, "DROP") == 0 && (pg_strcasecmp(prev3_wd, "AGGREGATE") == 0 || pg_strcasecmp(prev3_wd, "FUNCTION") == 0) && @@ -2550,7 +2639,7 @@ psql_completion(char *text, int start, int end) else if ((pg_strcasecmp(prev3_wd, "GRANT") == 0 || pg_strcasecmp(prev3_wd, "REVOKE") == 0) && pg_strcasecmp(prev_wd, "ON") == 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvf, + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvmf, " UNION SELECT 'DATABASE'" " UNION SELECT 'DOMAIN'" " UNION SELECT 'FOREIGN DATA WRAPPER'" @@ -2769,6 +2858,37 @@ psql_completion(char *text, int start, int end) pg_strcasecmp(prev5_wd, "REASSIGN") == 0) COMPLETE_WITH_QUERY(Query_for_list_of_roles); +/* REFRESH MATERIALIZED VIEW */ + else if (pg_strcasecmp(prev_wd, "REFRESH") == 0) + COMPLETE_WITH_CONST("MATERIALIZED VIEW"); + else if (pg_strcasecmp(prev2_wd, "REFRESH") == 0 && + pg_strcasecmp(prev_wd, "MATERIALIZED") == 0) + COMPLETE_WITH_CONST("VIEW"); + else if (pg_strcasecmp(prev3_wd, "REFRESH") == 0 && + pg_strcasecmp(prev2_wd, "MATERIALIZED") == 0 && + pg_strcasecmp(prev_wd, "VIEW") == 0) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL); + else if (pg_strcasecmp(prev4_wd, "REFRESH") == 0 && + pg_strcasecmp(prev3_wd, "MATERIALIZED") == 0 && + pg_strcasecmp(prev2_wd, "VIEW") == 0) + COMPLETE_WITH_CONST("WITH"); + else if (pg_strcasecmp(prev5_wd, "REFRESH") == 0 && + pg_strcasecmp(prev4_wd, "MATERIALIZED") == 0 && + pg_strcasecmp(prev3_wd, "VIEW") == 0 && + pg_strcasecmp(prev_wd, "WITH") == 0) + { + static const char *const list_WITH_DATA[] = + {"NO DATA", "DATA", NULL}; + + COMPLETE_WITH_LIST(list_WITH_DATA); + } + else if (pg_strcasecmp(prev6_wd, "REFRESH") == 0 && + pg_strcasecmp(prev5_wd, "MATERIALIZED") == 0 && + pg_strcasecmp(prev4_wd, "VIEW") == 0 && + pg_strcasecmp(prev2_wd, "WITH") == 0 && + pg_strcasecmp(prev_wd, "NO") == 0) + COMPLETE_WITH_CONST("DATA"); + /* REINDEX */ else if (pg_strcasecmp(prev_wd, "REINDEX") == 0) { @@ -2780,7 +2900,7 @@ psql_completion(char *text, int start, int end) else if (pg_strcasecmp(prev2_wd, "REINDEX") == 0) { if (pg_strcasecmp(prev_wd, "TABLE") == 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, NULL); else if (pg_strcasecmp(prev_wd, "INDEX") == 0) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes, NULL); else if (pg_strcasecmp(prev_wd, "SYSTEM") == 0 || @@ -2812,9 +2932,9 @@ psql_completion(char *text, int start, int end) pg_strcasecmp(prev_wd, "ON") == 0)) { static const char *const list_SECURITY_LABEL[] = - {"LANGUAGE", "SCHEMA", "SEQUENCE", "TABLE", "TYPE", "VIEW", "COLUMN", - "AGGREGATE", "FUNCTION", "DOMAIN", "LARGE OBJECT", - NULL}; + {"LANGUAGE", "SCHEMA", "SEQUENCE", "TABLE", "TYPE", "VIEW", + "MATERIALIZED VIEW", "COLUMN", "AGGREGATE", "FUNCTION", "DOMAIN", + "LARGE OBJECT", NULL}; COMPLETE_WITH_LIST(list_SECURITY_LABEL); } @@ -3061,7 +3181,7 @@ psql_completion(char *text, int start, int end) * VACUUM [ FULL | FREEZE ] [ VERBOSE ] ANALYZE [ table [ (column [, ...] ) ] ] */ else if (pg_strcasecmp(prev_wd, "VACUUM") == 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, " UNION SELECT 'FULL'" " UNION SELECT 'FREEZE'" " UNION SELECT 'ANALYZE'" @@ -3069,34 +3189,34 @@ psql_completion(char *text, int start, int end) else if (pg_strcasecmp(prev2_wd, "VACUUM") == 0 && (pg_strcasecmp(prev_wd, "FULL") == 0 || pg_strcasecmp(prev_wd, "FREEZE") == 0)) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, " UNION SELECT 'ANALYZE'" " UNION SELECT 'VERBOSE'"); else if (pg_strcasecmp(prev3_wd, "VACUUM") == 0 && pg_strcasecmp(prev_wd, "ANALYZE") == 0 && (pg_strcasecmp(prev2_wd, "FULL") == 0 || pg_strcasecmp(prev2_wd, "FREEZE") == 0)) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, " UNION SELECT 'VERBOSE'"); else if (pg_strcasecmp(prev3_wd, "VACUUM") == 0 && pg_strcasecmp(prev_wd, "VERBOSE") == 0 && (pg_strcasecmp(prev2_wd, "FULL") == 0 || pg_strcasecmp(prev2_wd, "FREEZE") == 0)) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, " UNION SELECT 'ANALYZE'"); else if (pg_strcasecmp(prev2_wd, "VACUUM") == 0 && pg_strcasecmp(prev_wd, "VERBOSE") == 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, " UNION SELECT 'ANALYZE'"); else if (pg_strcasecmp(prev2_wd, "VACUUM") == 0 && pg_strcasecmp(prev_wd, "ANALYZE") == 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, " UNION SELECT 'VERBOSE'"); else if ((pg_strcasecmp(prev_wd, "ANALYZE") == 0 && pg_strcasecmp(prev2_wd, "VERBOSE") == 0) || (pg_strcasecmp(prev_wd, "VERBOSE") == 0 && pg_strcasecmp(prev2_wd, "ANALYZE") == 0)) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tm, NULL); /* WITH [RECURSIVE] */ @@ -3111,7 +3231,7 @@ psql_completion(char *text, int start, int end) /* ANALYZE */ /* If the previous word is ANALYZE, produce list of tables */ else if (pg_strcasecmp(prev_wd, "ANALYZE") == 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tf, NULL); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tmf, NULL); /* WHERE */ /* Simple case of the word before the where being the table name */ @@ -3123,11 +3243,11 @@ psql_completion(char *text, int start, int end) else if (pg_strcasecmp(prev_wd, "FROM") == 0 && pg_strcasecmp(prev3_wd, "COPY") != 0 && pg_strcasecmp(prev3_wd, "\\copy") != 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvf, NULL); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvmf, NULL); /* ... JOIN ... */ else if (pg_strcasecmp(prev_wd, "JOIN") == 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvf, NULL); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvmf, NULL); /* Backslash commands */ /* TODO: \dc \dd \dl */ @@ -3167,7 +3287,7 @@ psql_completion(char *text, int start, int end) COMPLETE_WITH_QUERY(Query_for_list_of_schemas); else if (strncmp(prev_wd, "\\dp", strlen("\\dp")) == 0 || strncmp(prev_wd, "\\z", strlen("\\z")) == 0) - COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvf, NULL); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvmf, NULL); else if (strncmp(prev_wd, "\\ds", strlen("\\ds")) == 0) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences, NULL); else if (strncmp(prev_wd, "\\dt", strlen("\\dt")) == 0) @@ -3179,6 +3299,8 @@ psql_completion(char *text, int start, int end) COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (strncmp(prev_wd, "\\dv", strlen("\\dv")) == 0) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views, NULL); + else if (strncmp(prev_wd, "\\dm", strlen("\\dm")) == 0) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_matviews, NULL); /* must be at end of \d list */ else if (strncmp(prev_wd, "\\d", strlen("\\d")) == 0) diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h index 3a1788d792..989089a25a 100644 --- a/src/include/catalog/heap.h +++ b/src/include/catalog/heap.h @@ -70,6 +70,7 @@ extern Oid heap_create_with_catalog(const char *relname, bool is_internal); extern void heap_create_init_fork(Relation rel); +extern bool heap_is_matview_init_state(Relation rel); extern void heap_drop_with_catalog(Oid relid); diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h index 820552f013..fd97141e9e 100644 --- a/src/include/catalog/pg_class.h +++ b/src/include/catalog/pg_class.h @@ -153,6 +153,7 @@ DESCR(""); #define RELKIND_VIEW 'v' /* view */ #define RELKIND_COMPOSITE_TYPE 'c' /* composite type */ #define RELKIND_FOREIGN_TABLE 'f' /* foreign table */ +#define RELKIND_MATVIEW 'm' /* materialized view */ #define RELPERSISTENCE_PERMANENT 'p' /* regular table */ #define RELPERSISTENCE_UNLOGGED 'u' /* unlogged permanent table */ diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index d9f50d22d2..0e26ebf219 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -1980,6 +1980,8 @@ DATA(insert OID = 3842 ( pg_view_is_insertable PGNSP PGUID 12 10 0 0 0 f f f f 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"); +DATA(insert OID = 3846 ( pg_relation_is_scannable 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_scannable _null_ _null_ _null_ )); +DESCR("is a relation scannable"); /* 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/commands/createas.h b/src/include/commands/createas.h index 2ac718762b..012334b42e 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -19,6 +19,10 @@ #include "tcop/dest.h" +extern Query *SetupForCreateTableAs(Query *query, IntoClause *into, + const char *queryString, + ParamListInfo params, DestReceiver *dest); + extern void ExecCreateTableAs(CreateTableAsStmt *stmt, const char *queryString, ParamListInfo params, char *completionTag); diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h index ca213d7f70..24ef493115 100644 --- a/src/include/commands/explain.h +++ b/src/include/commands/explain.h @@ -67,8 +67,8 @@ extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into, const char *queryString, ParamListInfo params); extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, - ExplainState *es, - const char *queryString, ParamListInfo params); + ExplainState *es, const char *queryString, + DestReceiver *dest, ParamListInfo params); extern void ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc); diff --git a/src/include/commands/matview.h b/src/include/commands/matview.h new file mode 100644 index 0000000000..6de3c1aa93 --- /dev/null +++ b/src/include/commands/matview.h @@ -0,0 +1,28 @@ +/*------------------------------------------------------------------------- + * + * matview.h + * prototypes for matview.c. + * + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/matview.h + * + *------------------------------------------------------------------------- + */ +#ifndef MATVIEW_H +#define MATVIEW_H + +#include "nodes/params.h" +#include "tcop/dest.h" +#include "utils/relcache.h" + +extern void SetRelationIsScannable(Relation relation); + +extern void ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, + ParamListInfo params, char *completionTag); + +extern DestReceiver *CreateTransientRelDestReceiver(Oid oid); + +#endif /* MATVIEW_H */ diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index 27dc5e8ebb..031c77c9ef 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -78,4 +78,6 @@ extern void AtEOSubXact_on_commit_actions(bool isCommit, extern void RangeVarCallbackOwnsTable(const RangeVar *relation, Oid relId, Oid oldRelId, void *arg); +extern bool isQueryUsingTempRelation(Query *query); + #endif /* TABLECMDS_H */ diff --git a/src/include/commands/view.h b/src/include/commands/view.h index ff80ed6a44..972c7d208c 100644 --- a/src/include/commands/view.h +++ b/src/include/commands/view.h @@ -18,4 +18,6 @@ extern Oid DefineView(ViewStmt *stmt, const char *queryString); +extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace); + #endif /* VIEW_H */ diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index b1213a0635..aec6c7f7df 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -61,6 +61,7 @@ #define EXEC_FLAG_SKIP_TRIGGERS 0x0010 /* skip AfterTrigger calls */ #define EXEC_FLAG_WITH_OIDS 0x0020 /* force OIDs in returned tuples */ #define EXEC_FLAG_WITHOUT_OIDS 0x0040 /* force no OIDs in returned tuples */ +#define EXEC_FLAG_WITH_NO_DATA 0x0080 /* rel scannability doesn't matter */ /* diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 0854579fdf..0d5c007f93 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -361,6 +361,7 @@ typedef enum NodeTag T_AlterExtensionContentsStmt, T_CreateEventTrigStmt, T_AlterEventTrigStmt, + T_RefreshMatViewStmt, /* * TAGS FOR PARSE TREE NODES (parsenodes.h) diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d54990d39c..2229ef0f95 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -713,6 +713,7 @@ typedef struct RangeTblEntry */ Oid relid; /* OID of the relation */ char relkind; /* relation kind (see pg_class.relkind) */ + bool isResultRel; /* used in target of SELECT INTO or similar */ /* * Fields valid for a subquery RTE (else NULL): @@ -1135,6 +1136,7 @@ typedef enum ObjectType OBJECT_INDEX, OBJECT_LANGUAGE, OBJECT_LARGEOBJECT, + OBJECT_MATVIEW, OBJECT_OPCLASS, OBJECT_OPERATOR, OBJECT_OPFAMILY, @@ -2447,6 +2449,8 @@ typedef struct ExplainStmt * A query written as CREATE TABLE AS will produce this node type natively. * A query written as SELECT ... INTO will be transformed to this form during * parse analysis. + * A query written as CREATE MATERIALIZED view will produce this node type, + * during parse analysis, since it needs all the same data. * * The "query" field is handled similarly to EXPLAIN, though note that it * can be a SELECT or an EXECUTE, but not other DML statements. @@ -2457,9 +2461,21 @@ typedef struct CreateTableAsStmt NodeTag type; Node *query; /* the query (see comments above) */ IntoClause *into; /* destination table */ + ObjectType relkind; /* type of object */ bool is_select_into; /* it was written as SELECT INTO */ } CreateTableAsStmt; +/* ---------------------- + * REFRESH MATERIALIZED VIEW Statement + * ---------------------- + */ +typedef struct RefreshMatViewStmt +{ + NodeTag type; + bool skipData; /* true for WITH NO DATA */ + RangeVar *relation; /* relation to insert into */ +} RefreshMatViewStmt; + /* ---------------------- * Checkpoint Statement * ---------------------- @@ -2517,7 +2533,7 @@ typedef struct ConstraintsSetStmt typedef struct ReindexStmt { NodeTag type; - ObjectType kind; /* OBJECT_INDEX, OBJECT_TABLE, OBJECT_DATABASE */ + ObjectType kind; /* OBJECT_INDEX, OBJECT_TABLE, etc. */ RangeVar *relation; /* Table or index to reindex */ const char *name; /* name of database to reindex */ bool do_system; /* include system tables in database case */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 1d657669e1..27d4e4cd67 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -80,7 +80,8 @@ typedef struct RangeVar } RangeVar; /* - * IntoClause - target information for SELECT INTO and CREATE TABLE AS + * IntoClause - target information for SELECT INTO, CREATE TABLE AS, and + * CREATE MATERIALIZED VIEW */ typedef struct IntoClause { @@ -92,6 +93,7 @@ typedef struct IntoClause OnCommitAction onCommit; /* what do we do at COMMIT? */ char *tableSpaceName; /* table space to use, or NULL */ bool skipData; /* true for WITH NO DATA */ + char relkind; /* RELKIND_RELATION or RELKIND_MATVIEW */ } IntoClause; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f67a65f3d..68a13b7a7b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -232,6 +232,7 @@ PG_KEYWORD("location", LOCATION, UNRESERVED_KEYWORD) PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD) PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD) PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD) +PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD) PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD) PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD) PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD) @@ -302,6 +303,7 @@ PG_KEYWORD("recheck", RECHECK, UNRESERVED_KEYWORD) PG_KEYWORD("recursive", RECURSIVE, UNRESERVED_KEYWORD) PG_KEYWORD("ref", REF, UNRESERVED_KEYWORD) PG_KEYWORD("references", REFERENCES, RESERVED_KEYWORD) +PG_KEYWORD("refresh", REFRESH, UNRESERVED_KEYWORD) PG_KEYWORD("reindex", REINDEX, UNRESERVED_KEYWORD) PG_KEYWORD("relative", RELATIVE_P, UNRESERVED_KEYWORD) PG_KEYWORD("release", RELEASE, UNRESERVED_KEYWORD) diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 6e260bdfa2..79002184be 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -93,7 +93,8 @@ typedef enum DestTuplestore, /* results sent to Tuplestore */ DestIntoRel, /* results sent to relation (SELECT INTO) */ DestCopyOut, /* results sent to COPY TO code */ - DestSQLFunction /* results sent to SQL-language func mgr */ + DestSQLFunction, /* results sent to SQL-language func mgr */ + DestTransientRel /* results sent to transient relation */ } CommandDest; /* ---------------- diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h index 533539ca29..c0debe400c 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -461,6 +461,7 @@ extern Datum pg_table_size(PG_FUNCTION_ARGS); extern Datum pg_indexes_size(PG_FUNCTION_ARGS); extern Datum pg_relation_filenode(PG_FUNCTION_ARGS); extern Datum pg_relation_filepath(PG_FUNCTION_ARGS); +extern Datum pg_relation_is_scannable(PG_FUNCTION_ARGS); /* genfile.c */ extern bytea *read_binary_file(const char *filename, diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index c342eaa66f..06e1531e9a 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -83,6 +83,7 @@ typedef struct RelationData BackendId rd_backend; /* owning backend id, if temporary relation */ bool rd_islocaltemp; /* rel is a temp rel of this session */ bool rd_isnailed; /* rel is nailed in cache */ + bool rd_isscannable; /* rel can be scanned */ bool rd_isvalid; /* relcache entry is valid */ char rd_indexvalid; /* state of rd_indexlist: 0 = not valid, 1 = * valid, 2 = temporarily forced */ diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c index f093037741..d2e832fa67 100644 --- a/src/pl/plpgsql/src/pl_comp.c +++ b/src/pl/plpgsql/src/pl_comp.c @@ -1760,11 +1760,13 @@ plpgsql_parse_cwordtype(List *idents) classStruct = (Form_pg_class) GETSTRUCT(classtup); /* - * It must be a relation, sequence, view, composite type, or foreign table + * It must be a relation, sequence, view, materialized view, composite + * type, or foreign table */ if (classStruct->relkind != RELKIND_RELATION && classStruct->relkind != RELKIND_SEQUENCE && classStruct->relkind != RELKIND_VIEW && + classStruct->relkind != RELKIND_MATVIEW && classStruct->relkind != RELKIND_COMPOSITE_TYPE && classStruct->relkind != RELKIND_FOREIGN_TABLE) goto done; @@ -1982,10 +1984,14 @@ build_row_from_class(Oid classOid) classStruct = RelationGetForm(rel); relname = RelationGetRelationName(rel); - /* accept relation, sequence, view, composite type, or foreign table */ + /* + * Accept relation, sequence, view, materialized view, composite type, or + * foreign table. + */ if (classStruct->relkind != RELKIND_RELATION && classStruct->relkind != RELKIND_SEQUENCE && classStruct->relkind != RELKIND_VIEW && + classStruct->relkind != RELKIND_MATVIEW && classStruct->relkind != RELKIND_COMPOSITE_TYPE && classStruct->relkind != RELKIND_FOREIGN_TABLE) ereport(ERROR, diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c index c4d5fa1700..4cd59bc3ea 100644 --- a/src/pl/tcl/pltcl.c +++ b/src/pl/tcl/pltcl.c @@ -506,6 +506,7 @@ pltcl_init_load_unknown(Tcl_Interp *interp) return; /* must be table or view, else ignore */ if (!(pmrel->rd_rel->relkind == RELKIND_RELATION || + pmrel->rd_rel->relkind == RELKIND_MATVIEW || pmrel->rd_rel->relkind == RELKIND_VIEW)) { relation_close(pmrel, AccessShareLock); diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out new file mode 100644 index 0000000000..1077651b8e --- /dev/null +++ b/src/test/regress/expected/matview.out @@ -0,0 +1,406 @@ +-- create a table to use as a basis for views and materialized views in various combinations +CREATE TABLE t (id int NOT NULL PRIMARY KEY, type text NOT NULL, amt numeric NOT NULL); +INSERT INTO t VALUES + (1, 'x', 2), + (2, 'x', 3), + (3, 'y', 5), + (4, 'y', 7), + (5, 'z', 11); +-- we want a view based on the table, too, since views present additional challenges +CREATE VIEW tv AS SELECT type, sum(amt) AS totamt FROM t GROUP BY type; +SELECT * FROM tv; + type | totamt +------+-------- + y | 12 + z | 11 + x | 5 +(3 rows) + +-- create a materialized view with no data, and confirm correct behavior +EXPLAIN (costs off) + CREATE MATERIALIZED VIEW tm AS SELECT type, sum(amt) AS totamt FROM t GROUP BY type WITH NO DATA; + QUERY PLAN +--------------------- + HashAggregate + -> Seq Scan on t +(2 rows) + +CREATE MATERIALIZED VIEW tm AS SELECT type, sum(amt) AS totamt FROM t GROUP BY type WITH NO DATA; +SELECT pg_relation_is_scannable('tm'::regclass); + pg_relation_is_scannable +-------------------------- + f +(1 row) + +SELECT * FROM tm; +ERROR: materialized view "tm" has not been populated +HINT: Use the REFRESH MATERIALIZED VIEW command. +REFRESH MATERIALIZED VIEW tm; +SELECT pg_relation_is_scannable('tm'::regclass); + pg_relation_is_scannable +-------------------------- + t +(1 row) + +CREATE UNIQUE INDEX tm_type ON tm (type); +SELECT * FROM tm; + type | totamt +------+-------- + y | 12 + z | 11 + x | 5 +(3 rows) + +-- create various views +EXPLAIN (costs off) + CREATE MATERIALIZED VIEW tvm AS SELECT * FROM tv; + QUERY PLAN +--------------------- + HashAggregate + -> Seq Scan on t +(2 rows) + +CREATE MATERIALIZED VIEW tvm AS SELECT * FROM tv; +SELECT * FROM tvm; + type | totamt +------+-------- + y | 12 + z | 11 + x | 5 +(3 rows) + +CREATE MATERIALIZED VIEW tmm AS SELECT sum(totamt) AS grandtot FROM tm; +CREATE MATERIALIZED VIEW tvmm AS SELECT sum(totamt) AS grandtot FROM tvm; +CREATE VIEW tvv AS SELECT sum(totamt) AS grandtot FROM tv; +EXPLAIN (costs off) + CREATE MATERIALIZED VIEW tvvm AS SELECT * FROM tvv; + QUERY PLAN +--------------------------- + Aggregate + -> HashAggregate + -> Seq Scan on t +(3 rows) + +CREATE MATERIALIZED VIEW tvvm AS SELECT * FROM tvv; +CREATE VIEW tvvmv AS SELECT * FROM tvvm; +CREATE MATERIALIZED VIEW bb AS SELECT * FROM tvvmv; +CREATE INDEX aa ON bb (grandtot); +-- check that plans seem reasonable +\d+ tvm + Materialized view "public.tvm" + Column | Type | Modifiers | Storage | Stats target | Description +--------+---------+-----------+----------+--------------+------------- + type | text | | extended | | + totamt | numeric | | main | | +View definition: + SELECT tv.type, + tv.totamt + FROM tv; + +\d+ tvm + Materialized view "public.tvm" + Column | Type | Modifiers | Storage | Stats target | Description +--------+---------+-----------+----------+--------------+------------- + type | text | | extended | | + totamt | numeric | | main | | +View definition: + SELECT tv.type, + tv.totamt + FROM tv; + +\d+ tvvm + Materialized view "public.tvvm" + Column | Type | Modifiers | Storage | Stats target | Description +----------+---------+-----------+---------+--------------+------------- + grandtot | numeric | | main | | +View definition: + SELECT tvv.grandtot + FROM tvv; + +\d+ bb + Materialized view "public.bb" + Column | Type | Modifiers | Storage | Stats target | Description +----------+---------+-----------+---------+--------------+------------- + grandtot | numeric | | main | | +Indexes: + "aa" btree (grandtot) +View definition: + SELECT tvvmv.grandtot + FROM tvvmv; + +-- test schema behavior +CREATE SCHEMA mvschema; +ALTER MATERIALIZED VIEW tvm SET SCHEMA mvschema; +\d+ tvm +\d+ tvmm + Materialized view "public.tvmm" + Column | Type | Modifiers | Storage | Stats target | Description +----------+---------+-----------+---------+--------------+------------- + grandtot | numeric | | main | | +View definition: + SELECT sum(tvm.totamt) AS grandtot + FROM mvschema.tvm; + +SET search_path = mvschema, public; +\d+ tvm + Materialized view "mvschema.tvm" + Column | Type | Modifiers | Storage | Stats target | Description +--------+---------+-----------+----------+--------------+------------- + type | text | | extended | | + totamt | numeric | | main | | +View definition: + SELECT tv.type, + tv.totamt + FROM tv; + +-- modify the underlying table data +INSERT INTO t VALUES (6, 'z', 13); +-- confirm pre- and post-refresh contents of fairly simple materialized views +SELECT * FROM tm ORDER BY type; + type | totamt +------+-------- + x | 5 + y | 12 + z | 11 +(3 rows) + +SELECT * FROM tvm ORDER BY type; + type | totamt +------+-------- + x | 5 + y | 12 + z | 11 +(3 rows) + +REFRESH MATERIALIZED VIEW tm; +REFRESH MATERIALIZED VIEW tvm; +SELECT * FROM tm ORDER BY type; + type | totamt +------+-------- + x | 5 + y | 12 + z | 24 +(3 rows) + +SELECT * FROM tvm ORDER BY type; + type | totamt +------+-------- + x | 5 + y | 12 + z | 24 +(3 rows) + +RESET search_path; +-- confirm pre- and post-refresh contents of nested materialized views +EXPLAIN (costs off) + SELECT * FROM tmm; + QUERY PLAN +----------------- + Seq Scan on tmm +(1 row) + +EXPLAIN (costs off) + SELECT * FROM tvmm; + QUERY PLAN +------------------ + Seq Scan on tvmm +(1 row) + +EXPLAIN (costs off) + SELECT * FROM tvvm; + QUERY PLAN +------------------ + Seq Scan on tvvm +(1 row) + +SELECT * FROM tmm; + grandtot +---------- + 28 +(1 row) + +SELECT * FROM tvmm; + grandtot +---------- + 28 +(1 row) + +SELECT * FROM tvvm; + grandtot +---------- + 28 +(1 row) + +REFRESH MATERIALIZED VIEW tmm; +REFRESH MATERIALIZED VIEW tvmm; +REFRESH MATERIALIZED VIEW tvvm; +EXPLAIN (costs off) + SELECT * FROM tmm; + QUERY PLAN +----------------- + Seq Scan on tmm +(1 row) + +EXPLAIN (costs off) + SELECT * FROM tvmm; + QUERY PLAN +------------------ + Seq Scan on tvmm +(1 row) + +EXPLAIN (costs off) + SELECT * FROM tvvm; + QUERY PLAN +------------------ + Seq Scan on tvvm +(1 row) + +SELECT * FROM tmm; + grandtot +---------- + 41 +(1 row) + +SELECT * FROM tvmm; + grandtot +---------- + 41 +(1 row) + +SELECT * FROM tvvm; + grandtot +---------- + 41 +(1 row) + +-- test diemv when the mv does not exist +DROP MATERIALIZED VIEW IF EXISTS tum; +NOTICE: materialized view "tum" does not exist, skipping +-- make sure that an unlogged materialized view works (in the absence of a crash) +CREATE UNLOGGED MATERIALIZED VIEW tum AS SELECT type, sum(amt) AS totamt FROM t GROUP BY type WITH NO DATA; +SELECT pg_relation_is_scannable('tum'::regclass); + pg_relation_is_scannable +-------------------------- + f +(1 row) + +SELECT * FROM tum; +ERROR: materialized view "tum" has not been populated +HINT: Use the REFRESH MATERIALIZED VIEW command. +REFRESH MATERIALIZED VIEW tum; +SELECT pg_relation_is_scannable('tum'::regclass); + pg_relation_is_scannable +-------------------------- + t +(1 row) + +SELECT * FROM tum; + type | totamt +------+-------- + y | 12 + z | 24 + x | 5 +(3 rows) + +REFRESH MATERIALIZED VIEW tum WITH NO DATA; +SELECT pg_relation_is_scannable('tum'::regclass); + pg_relation_is_scannable +-------------------------- + f +(1 row) + +SELECT * FROM tum; +ERROR: materialized view "tum" has not been populated +HINT: Use the REFRESH MATERIALIZED VIEW command. +REFRESH MATERIALIZED VIEW tum WITH DATA; +SELECT pg_relation_is_scannable('tum'::regclass); + pg_relation_is_scannable +-------------------------- + t +(1 row) + +SELECT * FROM tum; + type | totamt +------+-------- + y | 12 + z | 24 + x | 5 +(3 rows) + +-- test diemv when the mv does exist +DROP MATERIALIZED VIEW IF EXISTS tum; +-- make sure that dependencies are reported properly when they block the drop +DROP TABLE t; +ERROR: cannot drop table t because other objects depend on it +DETAIL: view tv depends on table t +view tvv depends on view tv +materialized view tvvm depends on view tvv +view tvvmv depends on materialized view tvvm +materialized view bb depends on view tvvmv +materialized view mvschema.tvm depends on view tv +materialized view tvmm depends on materialized view mvschema.tvm +materialized view tm depends on table t +materialized view tmm depends on materialized view tm +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- make sure dependencies are dropped and reported +-- and make sure that transactional behavior is correct on rollback +-- incidentally leaving some interesting materialized views for pg_dump testing +BEGIN; +DROP TABLE t CASCADE; +NOTICE: drop cascades to 9 other objects +DETAIL: drop cascades to view tv +drop cascades to view tvv +drop cascades to materialized view tvvm +drop cascades to view tvvmv +drop cascades to materialized view bb +drop cascades to materialized view mvschema.tvm +drop cascades to materialized view tvmm +drop cascades to materialized view tm +drop cascades to materialized view tmm +ROLLBACK; +-- some additional tests not using base tables +CREATE VIEW v_test1 AS SELECT 1 moo; +CREATE VIEW v_test2 AS SELECT moo, 2*moo FROM v_test1 UNION ALL SELECT moo, 3*moo FROM v_test1; +\d+ v_test2 + View "public.v_test2" + Column | Type | Modifiers | Storage | Description +----------+---------+-----------+---------+------------- + moo | integer | | plain | + ?column? | integer | | plain | +View definition: + SELECT v_test1.moo, + 2 * v_test1.moo + FROM v_test1 +UNION ALL + SELECT v_test1.moo, + 3 * v_test1.moo + FROM v_test1; + +CREATE MATERIALIZED VIEW mv_test2 AS SELECT moo, 2*moo FROM v_test2 UNION ALL SELECT moo, 3*moo FROM v_test2; +\d+ mv_test2 + Materialized view "public.mv_test2" + Column | Type | Modifiers | Storage | Stats target | Description +----------+---------+-----------+---------+--------------+------------- + moo | integer | | plain | | + ?column? | integer | | plain | | +View definition: + SELECT v_test2.moo, + 2 * v_test2.moo + FROM v_test2 +UNION ALL + SELECT v_test2.moo, + 3 * v_test2.moo + FROM v_test2; + +CREATE MATERIALIZED VIEW mv_test3 AS SELECT * FROM mv_test2 WHERE moo = 12345; +SELECT pg_relation_is_scannable('mv_test3'::regclass); + pg_relation_is_scannable +-------------------------- + t +(1 row) + +DROP VIEW v_test1 CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to view v_test2 +drop cascades to materialized view mv_test2 +drop cascades to materialized view mv_test3 diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 6ba984a928..a4ecfd2aea 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1325,7 +1325,7 @@ SELECT viewname, definition FROM pg_views WHERE schemaname <> 'information_schem | JOIN pg_class i ON ((i.oid = x.indexrelid))) + | LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) + | LEFT JOIN pg_tablespace t ON ((t.oid = i.reltablespace))) + - | WHERE ((c.relkind = 'r'::"char") AND (i.relkind = 'i'::"char")); + | WHERE ((c.relkind = ANY (ARRAY['r'::"char", 'm'::"char"])) AND (i.relkind = 'i'::"char")); pg_locks | SELECT l.locktype, + | l.database, + | l.relation, + @@ -1342,6 +1342,17 @@ SELECT viewname, definition FROM pg_views WHERE schemaname <> 'information_schem | l.granted, + | l.fastpath + | FROM pg_lock_status() l(locktype, database, relation, page, tuple, virtualxid, transactionid, classid, objid, objsubid, virtualtransaction, pid, mode, granted, fastpath); + pg_matviews | SELECT n.nspname AS schemaname, + + | c.relname AS matviewname, + + | pg_get_userbyid(c.relowner) AS matviewowner, + + | t.spcname AS tablespace, + + | c.relhasindex AS hasindexes, + + | pg_relation_is_scannable(c.oid) AS isscannable, + + | pg_get_viewdef(c.oid) AS definition + + | FROM ((pg_class c + + | LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) + + | LEFT JOIN pg_tablespace t ON ((t.oid = c.reltablespace))) + + | WHERE (c.relkind = 'm'::"char"); pg_prepared_statements | SELECT p.name, + | p.statement, + | p.prepare_time, + @@ -1385,6 +1396,7 @@ SELECT viewname, definition FROM pg_views WHERE schemaname <> 'information_schem | CASE + | WHEN (rel.relkind = 'r'::"char") THEN 'table'::text + | WHEN (rel.relkind = 'v'::"char") THEN 'view'::text + + | WHEN (rel.relkind = 'm'::"char") THEN 'materialized view'::text + | WHEN (rel.relkind = 'S'::"char") THEN 'sequence'::text + | WHEN (rel.relkind = 'f'::"char") THEN 'foreign table'::text + | ELSE NULL::text + @@ -1600,7 +1612,7 @@ SELECT viewname, definition FROM pg_views WHERE schemaname <> 'information_schem | JOIN pg_index x ON ((c.oid = x.indrelid))) + | JOIN pg_class i ON ((i.oid = x.indexrelid))) + | LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) + - | WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char"])); + | WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"])); pg_stat_all_tables | SELECT c.oid AS relid, + | n.nspname AS schemaname, + | c.relname, + @@ -1625,7 +1637,7 @@ SELECT viewname, definition FROM pg_views WHERE schemaname <> 'information_schem | FROM ((pg_class c + | LEFT JOIN pg_index i ON ((c.oid = i.indrelid))) + | LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) + - | WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char"])) + + | WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"])) + | GROUP BY c.oid, n.nspname, c.relname; pg_stat_bgwriter | SELECT pg_stat_get_bgwriter_timed_checkpoints() AS checkpoints_timed, + | pg_stat_get_bgwriter_requested_checkpoints() AS checkpoints_req, + @@ -1774,7 +1786,7 @@ SELECT viewname, definition FROM pg_views WHERE schemaname <> 'information_schem | FROM ((pg_class c + | LEFT JOIN pg_index i ON ((c.oid = i.indrelid))) + | LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) + - | WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char"])) + + | WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"])) + | GROUP BY c.oid, n.nspname, c.relname; pg_stat_xact_sys_tables | SELECT pg_stat_xact_all_tables.relid, + | pg_stat_xact_all_tables.schemaname, + @@ -1822,7 +1834,7 @@ SELECT viewname, definition FROM pg_views WHERE schemaname <> 'information_schem | JOIN pg_index x ON ((c.oid = x.indrelid))) + | JOIN pg_class i ON ((i.oid = x.indexrelid))) + | LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) + - | WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char"])); + | WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"])); pg_statio_all_sequences | SELECT c.oid AS relid, + | n.nspname AS schemaname, + | c.relname, + @@ -1847,7 +1859,7 @@ SELECT viewname, definition FROM pg_views WHERE schemaname <> 'information_schem | LEFT JOIN pg_class t ON ((c.reltoastrelid = t.oid))) + | LEFT JOIN pg_class x ON ((t.reltoastidxid = x.oid))) + | LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) + - | WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char"])) + + | WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"])) + | GROUP BY c.oid, n.nspname, c.relname, t.oid, x.oid; pg_statio_sys_indexes | SELECT pg_statio_all_indexes.relid, + | pg_statio_all_indexes.indexrelid, + @@ -2119,7 +2131,15 @@ SELECT viewname, definition FROM pg_views WHERE schemaname <> 'information_schem | emp.location, + | (12 * emp.salary) AS annualsal + | FROM emp; -(60 rows) + tv | SELECT t.type, + + | sum(t.amt) AS totamt + + | FROM t + + | GROUP BY t.type; + tvv | SELECT sum(tv.totamt) AS grandtot + + | FROM tv; + tvvmv | SELECT tvvm.grandtot + + | FROM tvvm; +(64 rows) SELECT tablename, rulename, definition FROM pg_rules ORDER BY tablename, rulename; diff --git a/src/test/regress/output/misc.source b/src/test/regress/output/misc.source index 4353d0b1e3..2dd5b2389e 100644 --- a/src/test/regress/output/misc.source +++ b/src/test/regress/output/misc.source @@ -588,6 +588,7 @@ SELECT user_relns() AS user_relns arrtest b b_star + bb box_tbl bprime bt_f8_heap @@ -671,6 +672,7 @@ SELECT user_relns() AS user_relns student subselect_tbl suffix_text_tbl + t tenk1 tenk2 test_range_excl @@ -683,10 +685,18 @@ SELECT user_relns() AS user_relns timestamptz_tbl timetz_tbl tinterval_tbl + tm + tmm toyemp + tv + tvm + tvmm + tvv + tvvm + tvvmv varchar_tbl xacttest -(108 rows) +(118 rows) SELECT name(equipment(hobby_construct(text 'skywalking', text 'mer'))); name diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index d3def07f92..2af28b1502 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi # ---------- # Another group of parallel tests # ---------- -test: privileges security_label collate +test: privileges security_label collate matview # ---------- # Another group of parallel tests diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule index 7059fca092..d6eaa7aa4d 100644 --- a/src/test/regress/serial_schedule +++ b/src/test/regress/serial_schedule @@ -95,6 +95,7 @@ test: prepared_xacts test: privileges test: security_label test: collate +test: matview test: alter_generic test: misc test: psql diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql new file mode 100644 index 0000000000..e1c0e1583d --- /dev/null +++ b/src/test/regress/sql/matview.sql @@ -0,0 +1,128 @@ +-- create a table to use as a basis for views and materialized views in various combinations +CREATE TABLE t (id int NOT NULL PRIMARY KEY, type text NOT NULL, amt numeric NOT NULL); +INSERT INTO t VALUES + (1, 'x', 2), + (2, 'x', 3), + (3, 'y', 5), + (4, 'y', 7), + (5, 'z', 11); + +-- we want a view based on the table, too, since views present additional challenges +CREATE VIEW tv AS SELECT type, sum(amt) AS totamt FROM t GROUP BY type; +SELECT * FROM tv; + +-- create a materialized view with no data, and confirm correct behavior +EXPLAIN (costs off) + CREATE MATERIALIZED VIEW tm AS SELECT type, sum(amt) AS totamt FROM t GROUP BY type WITH NO DATA; +CREATE MATERIALIZED VIEW tm AS SELECT type, sum(amt) AS totamt FROM t GROUP BY type WITH NO DATA; +SELECT pg_relation_is_scannable('tm'::regclass); +SELECT * FROM tm; +REFRESH MATERIALIZED VIEW tm; +SELECT pg_relation_is_scannable('tm'::regclass); +CREATE UNIQUE INDEX tm_type ON tm (type); +SELECT * FROM tm; + +-- create various views +EXPLAIN (costs off) + CREATE MATERIALIZED VIEW tvm AS SELECT * FROM tv; +CREATE MATERIALIZED VIEW tvm AS SELECT * FROM tv; +SELECT * FROM tvm; +CREATE MATERIALIZED VIEW tmm AS SELECT sum(totamt) AS grandtot FROM tm; +CREATE MATERIALIZED VIEW tvmm AS SELECT sum(totamt) AS grandtot FROM tvm; +CREATE VIEW tvv AS SELECT sum(totamt) AS grandtot FROM tv; +EXPLAIN (costs off) + CREATE MATERIALIZED VIEW tvvm AS SELECT * FROM tvv; +CREATE MATERIALIZED VIEW tvvm AS SELECT * FROM tvv; +CREATE VIEW tvvmv AS SELECT * FROM tvvm; +CREATE MATERIALIZED VIEW bb AS SELECT * FROM tvvmv; +CREATE INDEX aa ON bb (grandtot); + +-- check that plans seem reasonable +\d+ tvm +\d+ tvm +\d+ tvvm +\d+ bb + +-- test schema behavior +CREATE SCHEMA mvschema; +ALTER MATERIALIZED VIEW tvm SET SCHEMA mvschema; +\d+ tvm +\d+ tvmm +SET search_path = mvschema, public; +\d+ tvm + +-- modify the underlying table data +INSERT INTO t VALUES (6, 'z', 13); + +-- confirm pre- and post-refresh contents of fairly simple materialized views +SELECT * FROM tm ORDER BY type; +SELECT * FROM tvm ORDER BY type; +REFRESH MATERIALIZED VIEW tm; +REFRESH MATERIALIZED VIEW tvm; +SELECT * FROM tm ORDER BY type; +SELECT * FROM tvm ORDER BY type; +RESET search_path; + +-- confirm pre- and post-refresh contents of nested materialized views +EXPLAIN (costs off) + SELECT * FROM tmm; +EXPLAIN (costs off) + SELECT * FROM tvmm; +EXPLAIN (costs off) + SELECT * FROM tvvm; +SELECT * FROM tmm; +SELECT * FROM tvmm; +SELECT * FROM tvvm; +REFRESH MATERIALIZED VIEW tmm; +REFRESH MATERIALIZED VIEW tvmm; +REFRESH MATERIALIZED VIEW tvvm; +EXPLAIN (costs off) + SELECT * FROM tmm; +EXPLAIN (costs off) + SELECT * FROM tvmm; +EXPLAIN (costs off) + SELECT * FROM tvvm; +SELECT * FROM tmm; +SELECT * FROM tvmm; +SELECT * FROM tvvm; + +-- test diemv when the mv does not exist +DROP MATERIALIZED VIEW IF EXISTS tum; + +-- make sure that an unlogged materialized view works (in the absence of a crash) +CREATE UNLOGGED MATERIALIZED VIEW tum AS SELECT type, sum(amt) AS totamt FROM t GROUP BY type WITH NO DATA; +SELECT pg_relation_is_scannable('tum'::regclass); +SELECT * FROM tum; +REFRESH MATERIALIZED VIEW tum; +SELECT pg_relation_is_scannable('tum'::regclass); +SELECT * FROM tum; +REFRESH MATERIALIZED VIEW tum WITH NO DATA; +SELECT pg_relation_is_scannable('tum'::regclass); +SELECT * FROM tum; +REFRESH MATERIALIZED VIEW tum WITH DATA; +SELECT pg_relation_is_scannable('tum'::regclass); +SELECT * FROM tum; + +-- test diemv when the mv does exist +DROP MATERIALIZED VIEW IF EXISTS tum; + +-- make sure that dependencies are reported properly when they block the drop +DROP TABLE t; + +-- make sure dependencies are dropped and reported +-- and make sure that transactional behavior is correct on rollback +-- incidentally leaving some interesting materialized views for pg_dump testing +BEGIN; +DROP TABLE t CASCADE; +ROLLBACK; + +-- some additional tests not using base tables +CREATE VIEW v_test1 AS SELECT 1 moo; +CREATE VIEW v_test2 AS SELECT moo, 2*moo FROM v_test1 UNION ALL SELECT moo, 3*moo FROM v_test1; +\d+ v_test2 +CREATE MATERIALIZED VIEW mv_test2 AS SELECT moo, 2*moo FROM v_test2 UNION ALL SELECT moo, 3*moo FROM v_test2; +\d+ mv_test2 +CREATE MATERIALIZED VIEW mv_test3 AS SELECT * FROM mv_test2 WHERE moo = 12345; +SELECT pg_relation_is_scannable('mv_test3'::regclass); + +DROP VIEW v_test1 CASCADE;