pax_global_header00006660000000000000000000000064150751234650014522gustar00rootroot0000000000000052 comment=98a262ce0906b8afbb8e6bce8dd05779f8c50cac pg_ivm-1.13/000077500000000000000000000000001507512346500127275ustar00rootroot00000000000000pg_ivm-1.13/.gitignore000066400000000000000000000004031507512346500147140ustar00rootroot00000000000000# Object files *.o # Coverage files *.gcno *.gcda # Libraries *.lib *.a # LLVM bitcode *.bc # Shared objects (inc. Windows DLLs) *.dll *.so *.so.* *.dylib # Executables *.exe *.app # Dependencies .deps # Generated subdirectories /results/ /output_iso/ pg_ivm-1.13/LICENSE000066400000000000000000000017671507512346500137470ustar00rootroot00000000000000Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group Portions Copyright (c) 2022, IVM Development Group Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL THE AUTHORS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE AUTHORS SPECIFICALLY DISCLAIM ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE AUTHORS HAVE NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. pg_ivm-1.13/Makefile000066400000000000000000000022171507512346500143710ustar00rootroot00000000000000# contrib/pg_ivm/Makefile PG_CONFIG ?= pg_config MODULE_big = pg_ivm OBJS = \ $(WIN32RES) \ createas.o \ matview.o \ pg_ivm.o \ ruleutils.o \ subselect.o PGFILEDESC = "pg_ivm - incremental view maintenance on PostgreSQL" EXTENSION = pg_ivm DATA = pg_ivm--1.0.sql \ pg_ivm--1.0--1.1.sql pg_ivm--1.1--1.2.sql pg_ivm--1.2--1.3.sql \ pg_ivm--1.3--1.4.sql pg_ivm--1.4--1.5.sql pg_ivm--1.5--1.6.sql \ pg_ivm--1.6--1.7.sql pg_ivm--1.7--1.8.sql pg_ivm--1.8--1.9.sql \ pg_ivm--1.9--1.10.sql \ pg_ivm--1.10.sql \ pg_ivm--1.10--1.11.sql pg_ivm--1.11--1.12.sql pg_ivm--1.12--1.13.sql REGRESS = pg_ivm create_immv refresh_immv outer_join PGVER = $(shell $(PG_CONFIG) --version | sed "s/^[^ ]* \([0-9]*\).*$$/\1/" 2>/dev/null) # We assume PG13 is the only version that is supported by pg_ivm but # missing pg_isolation_regress. ifneq ($(PGVER),13) ISOLATION = create_insert refresh_insert insert_insert \ create_insert2 refresh_insert2 insert_insert2 \ create_insert3 refresh_insert3 insert_insert3 ISOLATION_OPTS = --load-extension=pg_ivm endif PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) pg_ivm-1.13/README.md000066400000000000000000000600711507512346500142120ustar00rootroot00000000000000# pg_ivm The `pg_ivm` module provides Incremental View Maintenance (IVM) feature for PostgreSQL. The extension is compatible with PostgreSQL 13, 14, 15, 16, 17, and 18. ## Description **Incremental View Maintenance (IVM)** is a way to make materialized views up-to-date in which only incremental changes are computed and applied on views rather than recomputing the contents from scratch as `REFRESH MATERIALIZED VIEW` does. IVM can update materialized views more efficiently than recomputation when only small parts of the view are changed. There are two approaches with regard to timing of view maintenance: immediate and deferred. In immediate maintenance, views are updated in the same transaction that its base table is modified. In deferred maintenance, views are updated after the transaction is committed, for example, when the view is accessed, as a response to user command like `REFRESH MATERIALIZED VIEW`, or periodically in background, and so on. `pg_ivm` provides a kind of immediate maintenance, in which materialized views are updated immediately in AFTER triggers when a base table is modified. We call a materialized view supporting IVM an **Incrementally Maintainable Materialized View (IMMV)**. To create IMMV, you have to call `create_immv` function with a relation name and a view definition query. For example: ```sql SELECT pgivm.create_immv('myview', 'SELECT * FROM mytab'); ``` creates an IMMV with name 'myview' defined as `SELECT * FROM mytab`. This is corresponding to the following command to create a normal materialized view; ```sql CREATE MATERIALIZED VIEW myview AS SELECT * FROM mytab; ``` When an IMMV is created, some triggers are automatically created so that the view's contents are immediately updated when its base tables are modified. ```sql postgres=# SELECT pgivm.create_immv('m', 'SELECT * FROM t0'); NOTICE: could not create an index on immv "m" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 3 (1 row) postgres=# SELECT * FROM m; i --- 1 2 3 (3 rows) postgres=# INSERT INTO t0 VALUES (4); INSERT 0 1 postgres=# SELECT * FROM m; -- automatically updated i --- 1 2 3 4 (4 rows) ``` Note that if you use PostgreSQL 17 or later, during automatic maintenance of an IMMV, the `search_path` is temporarily changed to `pg_catalog, pg_temp`. ## Installation To install `pg_ivm`, execute this in the module's directory: ```shell make install ``` If you installed PostgreSQL from rpm or deb, you will need the devel package (for example, postgresql14-devel or postgresql-server-dev-14). > **Important:** Don't forget to set the `PG_CONFIG` variable (`make PG_CONFIG=...`) or the `PATH` to the `pg_config` command in case you want to use `pg_ivm` on a non-default or custom build of PostgreSQL. Read more [here](https://wiki.postgresql.org/wiki/Building_and_Installing_PostgreSQL_Extension_Modules). Once installed, execute `CREATE EXTENSION` comand. ```sql CREATE EXTENSION pg_ivm; ``` ### Configuration To ensure `pg_ivm` maintains IMMVs correctly, add it to either shared_preload_libraries or session_preload_libraries in postgresql.conf: ``` # Add pg_ivm to preload libraries shared_preload_libraries = 'pg_ivm' # OR session_preload_libraries = 'pg_ivm' ``` After making this change, restart PostgreSQL for the configuration to take effect. ### RPM packages and yum repository RPM packages of pg_ivm are available from the [PostgreSQL yum repository](https://yum.postgresql.org/). See the [instruction](https://yum.postgresql.org/howto/) for details. Note that we are not the maintainer of this yum repository and RPMs for pg_ivm in it may be not always latest. ## Objects When `pg_ivm` is installed, the 'pgivm' schema is created, along with the following objects within this schema. ### Functions #### create_immv Use `create_immv` function to create IMMV. ``` pgivm.create_immv(immv_name text, view_definition text) RETURNS bigint ``` `create_immv` defines a new IMMV of a query. A table of the name `immv_name` is created and a query specified by `view_definition` is executed and used to populate the IMMV. The query is stored in `pg_ivm_immv`, so that it can be refreshed later upon incremental view maintenance. `create_immv` returns the number of rows in the created IMMV. When an IMMV is created, some triggers are automatically created so that the view's contents are immediately updated when its base tables are modified. In addition, a unique index is created on the IMMV automatically if possible. If the view definition query has a GROUP BY clause, a unique index is created on the columns of GROUP BY expressions. Also, if the view has DISTINCT clause, a unique index is created on all columns in the target list. Otherwise, if the IMMV contains all primary key attributes of its base tables in the target list, a unique index is created on these attributes. In other cases, no index is created. `create_immv` acquires an `AccessExclusiveLock` on the view. However, even if we are able to acquire the lock, a concurrent transaction may have already incrementally updated and committed the view before we can acquire it. In the `REPEATABLE READ` or `SERIALIZABLE` isolation levels, this could lead to an inconsistent state of the view. Unfortunately, this situation cannot be detected during the creation of the view. As a result, `create_immv` raises a warning in these isolation levels, suggesting that the command be used in `READ COMMITTED` or that `refresh_immv` be executed afterward to make the view's contents remain consistent. Note that if you use PostgreSQL 17 or later, while `create_immv` is running, the `search_path` is temporarily changed to `pg_catalog, pg_temp`. #### refresh_immv Use `refresh_immv` function to refresh IMMV. ``` pgivm.refresh_immv(immv_name text, with_data bool) RETURNS bigint ``` `refresh_immv` completely replaces the contents of an IMMV as `REFRESH MATERIALIZED VIEW` command does for a materialized view. To execute this function you must be the owner of the IMMV (with PostgreSQL 16 or earlier) or have the `MAINTAIN` privilege on the IMMV (with PostgreSQL 17 or later). The old contents are discarded. The with_data flag is corresponding to `WITH [NO] DATA` option of `REFRESH MATERIALIZED VIEW` command. If with_data is true, the backing query is executed to provide the new data, and if the IMMV is unpopulated, triggers for maintaining the view are created. Also, a unique index is created for IMMV if it is possible and the view doesn't have that yet. If with_data is false, no new data is generated and the IMMV become unpopulated, and the triggers are dropped from the IMMV. Note that unpopulated IMMV is still scannable although the result is empty. This behaviour may be changed in future to raise an error when an unpopulated IMMV is scanned. `refresh_immv` acquires `AccessExclusiveLock` on the view. However, even if we are able to acquire the lock, a concurrent transaction may have already incrementally updated and committed the view before we can acquire the lock. In `REPEATABLE READ` or `SERIALIZABLE` isolation level, this could lead to an inconsistent state of the view. Therefore, an error is raised to prevent anomalies when this situation is detected. Note that if you use PostgreSQL 17 or later, while `refresh_immv` is running, the `search_path` is temporarily changed to `pg_catalog, pg_temp`. #### get_immv_def `get_immv_def` reconstructs the underlying SELECT command for an IMMV. (This is a decompiled reconstruction, not the original text of the command.) ``` pgivm.get_immv_def(immv regclass) RETURNS text ``` ### IMMV metadata catalog The catalog `pgivm.pg_ivm_immv` stores IMMV information. |Name|Type|Description| |:---|:---|:---| |immvrelid|regclass|The OID of the IMMV| |viewdef|text|Query tree (in the form of a nodeToString() representation) for the view definition| |ispopulated|bool|True if IMMV is currently populated| |lastivmupdate|xid8|The transaction ID of the most recent incremental update to the view| ## Example ### `CREATE MATERIALIZED VIEW` and `REFRESH MATERIALIZED VIEW` In general, IMMVs allow faster updates than `REFRESH MATERIALIZED VIEW` at the price of slower updates to their base tables. Update of base tables is slower because triggers will be invoked and the IMMV is updated in triggers per modification statement. For example, suppose a normal materialized view defined as below: ```sql test=# CREATE MATERIALIZED VIEW mv_normal AS SELECT a.aid, b.bid, a.abalance, b.bbalance FROM pgbench_accounts a JOIN pgbench_branches b USING(bid); SELECT 10000000 ``` Updating a tuple in a base table of this materialized view is rapid but the `REFRESH MATERIALIZED VIEW` command on this view takes a long time: ```sql test=# UPDATE pgbench_accounts SET abalance = 1000 WHERE aid = 1; UPDATE 1 Time: 9.052 ms test=# REFRESH MATERIALIZED VIEW mv_normal ; REFRESH MATERIALIZED VIEW Time: 20575.721 ms (00:20.576) ``` ### Creating an IMMV On the other hand, after creating IMMV with the same view definition as below: ``` test=# SELECT pgivm.create_immv('immv', 'SELECT a.aid, b.bid, a.abalance, b.bbalance FROM pgbench_accounts a JOIN pgbench_branches b USING(bid)'); NOTICE: created index "immv_index" on immv "immv" create_immv ------------- 10000000 (1 row) ``` Updating a tuple in a base table takes more than the normal view, but its content is updated automatically and this is faster than the `REFRESH MATERIALIZED VIEW` command. ```sql test=# UPDATE pgbench_accounts SET abalance = 1234 WHERE aid = 1; UPDATE 1 Time: 15.448 ms test=# SELECT * FROM immv WHERE aid = 1; aid | bid | abalance | bbalance -----+-----+----------+---------- 1 | 1 | 1234 | 0 (1 row) ``` An appropriate index on IMMV is necessary for efficient IVM because we need to look for tuples to be updated in IMMV. If there are no indexes, it will take a long time. Therefore, when an IMMV is created by the `create_immv` function, a unique index is created on it automatically if possible. If the view definition query has a GROUP BY clause, a unique index is created on the columns of GROUP BY expressions. Also, if the view has DISTINCT clause, a unique index is created on all columns in the target list. Otherwise, if the IMMV contains all primary key attributes of its base tables in the target list, a unique index is created on these attributes. In other cases, no index is created. In the previous example, a unique index "immv_index" is created on aid and bid columns of "immv", and this enables the rapid update of the view. Dropping this index make updating the view take a loger time. ```sql test=# DROP INDEX immv_index; DROP INDEX test=# UPDATE pgbench_accounts SET abalance = 9876 WHERE aid = 1; UPDATE 1 Time: 3224.741 ms (00:03.225) ``` ### IMMV with Aggregate Functions You can create an IMMV that includes aggregate functions. ```sql test=# SELECT pgivm.create_immv('immv_agg', 'SELECT bid, count(*), sum(abalance), avg(abalance) FROM pgbench_accounts JOIN pgbench_branches USING(bid) GROUP BY bid'); NOTICE: created index "immv_agg_index" on immv "immv_agg" create_immv ------------- 100 (1 row) Time: 5772.625 ms (00:05.773) ``` Creating this view takes about five seconds, and the normal refresh operation requires a similar amount of time. The following example demonstrates refreshing the IMMV using `refresh_immv`. The execution time would be approximately the same if you used `REFRESH MATERIALIZED VIEW` on a regular materialized view with the same definition. ```sql test=# SELECT pgivm.refresh_immv('immv_agg',true); refresh_immv -------------- 100 (1 row) Time: 5766.292 ms (00:05.766) ``` When a base table is updated, the IMMV is also automatically updated incrementally, as expected. ```sql test=# SELECT bid, count, sum, avg FROM immv_agg WHERE bid = 42; bid | count | sum | avg -----+--------+-------+------------------------ 42 | 100000 | 38774 | 0.38774000000000000000 (1 row) Time: 3.123 ms test=# UPDATE pgbench_accounts SET abalance = abalance + 1000 WHERE aid = 4112345 AND bid = 42; UPDATE 1 Time: 16.616 ms test=# SELECT bid, count, sum, avg FROM immv_agg WHERE bid = 42; bid | count | sum | avg -----+--------+-------+------------------------ 42 | 100000 | 39774 | 0.39774000000000000000 (1 row) Time: 1.987 ms ``` After updating a row in the `pgbench_accounts` table, you can see that the aggregated values in `immv_agg` are updated automatically. ### Listing IMMVs and Viewing Their Definitions You can find all IMMVs in the `pg_ivm_immv` catalog and view their definition queries by executing the `get_immv_def` function." ```sql test=# SELECT immvrelid AS immv, pgivm.get_immv_def(immvrelid) AS immv_def FROM pgivm.pg_ivm_immv; immv | immv_def ----------+-------------------------------------------- immv_agg | SELECT pgbench_accounts.bid, + | count(*) AS count, + | sum(pgbench_accounts.abalance) AS sum,+ | avg(pgbench_accounts.abalance) AS avg + | FROM (pgbench_accounts + | JOIN pgbench_branches USING (bid)) + | GROUP BY pgbench_accounts.bid immv | SELECT a.aid, + | b.bid, + | a.abalance, + | b.bbalance + | FROM (pgbench_accounts a + | JOIN pgbench_branches b USING (bid)) (2 rows) ``` ### Dropping an IMMV An IMMV can be dropped using the `DROP TABLE` command. Once an IMMV is dropped, its entry is automatically removed from the `pg_ivm_immv` catalog. ```sql test=# DROP TABLE immv DROP TABLE test=# SELECT immvrelid AS immv, pgivm.get_immv_def(immvrelid) AS immv_def FROM pgivm.pg_ivm_immv; immv | immv_def ----------+-------------------------------------------- immv_agg | SELECT pgbench_accounts.bid, + | count(*) AS count, + | sum(pgbench_accounts.abalance) AS sum,+ | avg(pgbench_accounts.abalance) AS avg + | FROM (pgbench_accounts + | JOIN pgbench_branches USING (bid)) + | GROUP BY pgbench_accounts.bid (1 row) ``` ### Renaming an IMMV An IMMV can be renamed using the `ALTER TABLE` command. ```sql test=# ALTER TABLE immv_agg RENAME TO immv_agg2; ALTER TABLE test=# SELECT immvrelid AS immv, pgivm.get_immv_def(immvrelid) AS immv_def FROM pgivm.pg_ivm_immv; immv | immv_def -----------+-------------------------------------------- immv_agg2 | SELECT pgbench_accounts.bid, + | count(*) AS count, + | sum(pgbench_accounts.abalance) AS sum,+ | avg(pgbench_accounts.abalance) AS avg + | FROM (pgbench_accounts + | JOIN pgbench_branches USING (bid)) + | GROUP BY pgbench_accounts.bid (1 row) ``` ## `pg_dump` and `pg_upgrade` After restoring data from a `pg_dump` backup or upgrading `PostgreSQL` using `pg_upgrade`, all IMMVs must be manually dropped and recreated. ## Supported View Definitions and Restriction Currently, IMMV's view definition can contain inner and outer joins, DISTINCT clause, some built-in aggregate functions, simple sub-queries in `FROM` clause, EXISTS sub-queries, and simple CTE (`WITH` query). Inner joins including self-join are supported. Supported aggregate functions are count, sum, avg, min and max. Other aggregates, sub-queries which contain an aggregate or `DISTINCT` clause, sub-queries in other than `FROM` clause, window functions, `HAVING`, `ORDER BY`, `LIMIT`/`OFFSET`, `UNION`/`INTERSECT`/`EXCEPT`, `DISTINCT ON`, `TABLESAMPLE`, `VALUES`, and `FOR UPDATE`/`SHARE` can not be used in view definition. The base tables must be simple tables. Views, materialized views, inheritance parent tables, partitioned tables, partitions, and foreign tables can not be used. Any system column cannot be included in the view definition query. The target list cannot columns whose name starts with `__ivm_`. Data type used in the target list in the view must have default operator class for access method btree. For example, `json`, `xml`, or `point` type cannot be in the target list. Logical replication is not supported, that is, even when a base table at a publisher node is modified, IMMVs at subscriber nodes defined on these base tables are not updated. ## Notes ### Aggregates Supported aggregate functions are `count`, `sum`, `avg`, `min`, and `max`. Currently, only built-in aggregate functions are supported and user defined aggregates cannot be used. When an IMMV including aggregate is created, some extra columns whose name start with `__ivm` are automatically added to the target list. `__ivm_count__` contains the number of tuples aggregated in each group. In addition, more than one extra columns for each column of aggregated value are added in order to maintain the value. For example, columns named like `__ivm_count_avg__` and `__ivm_sum_avg__` are added for maintaining an average value. When a base table is modified, the new aggregated values are incrementally calculated using the old aggregated values and values of related extra columns stored in the IMMV. Note that for `min` or `max`, the new values could be re-calculated from base tables with regard to the affected groups when a tuple containing the current minimal or maximal values are deleted from a base table. Therefore, it can takes a long time to update an IMMV containing these functions. Also, note that using `sum` or `avg` on `real` (`float4`) type or `double precision` (`float8`) type in IMMV is unsafe, because aggregated values in IMMV can become different from results calculated from base tables due to the limited precision of these types. To avoid this problem, use the `numeric` type instead. #### Restrictions on Aggregate If we have a `GROUP BY` clause, expressions specified in `GROUP BY` must appear in the target list. This is how tuples to be updated in the IMMV are identified. These attributes are used as scan keys for searching tuples in the IMMV, so indexes on them are required for efficient IVM. Targetlist cannot contain expressions which contain an aggregate in it. ### Outer Joins Outer joins are supported with the following restrictions. #### Restrictions on Outer Joins Only simple equijoin is supported. The join condition must be a single equality comparison between columns of two base tables. All attributes used in join conditions must be included in the target list. These attributes are used as scan keys for searching tuples in the `IMMV`, so indexes on them are required for efficient IVM. The target list cannot contain non-strict functions that may return a non-NULL results for NULL inputs. For example, `CASE` expressions cannot be used. `WHERE` clauses cannot contain non-null-rejecting predicates that can return true for NULL inputs. For example, `IS NULL` cannot be used. Aggregate functions cannot be used with outer joins. Subqueries cannot be used with outer joins. ### Subqueries Simple subqueries in `FROM` clause and `EXISTS` subqueries in 'WHERE' clause are supported. #### Restrictions on Subqueries Subqueries using EXISTS and simple subqueries in FROM clause are supported. EXISTS subqueries with condition other than 'AND' and Subqueries in targetlist are not supported. EXISTS subquery is supported only in WHERE but not in the targetlist. If EXISTS contains columns that refer to columns in tables in the outer query, such columns must be included in the targetlist. Subqueries containing an aggregate function or `DISTINCT` are not supported. ### CTE Simple CTEs (`WITH` queries) are supported. #### Restrictions on CTEs `WITH` queries containing an aggregate function or `DISTINCT` are not supported. Recursive queries (`WITH RECURSIVE`) are not allowed. Unreferenced CTEs are not allowed either, that is, a CTE must be referenced at least once in the view definition query. ### DISTINCT `DISTINCT` is allowed in IMMV's definition queries. Suppose an IMMV defined with DISTINCT on a base table containing duplicate tuples. When tuples are deleted from the base table, a tuple in the view is deleted if and only if the multiplicity of the tuple becomes zero. Moreover, when tuples are inserted into the base table, a tuple is inserted into the view only if the same tuple doesn't already exist in it. Physically, an IMMV defined with `DISTINCT` contains tuples after eliminating duplicates, and the multiplicity of each tuple is stored in an extra column named `__ivm_count__` that is added when such IMMV is created. ### TRUNCATE When a base table is truncated, the IMMV is also truncated and the contents become empty if the view definition query does not contain an aggregate without a `GROUP BY` clause. Aggregate views without a `GROUP BY` clause always have one row. Therefore, in such cases, if a base table is truncated, the IMMV is simply refreshed instead of being truncated. ### Concurrent Transactions Incremental updates of a view are basically performed in sequentially even with concurrent transactions running. Suppose an IMMV is defined on two base tables and each table is modified in different concurrent transactions simultaneously. In the transaction which was committed first, the IMMV can be updated considering only the change made in that transaction. However, to update the IMMV correctly in the transaction that commits later, we need to account for the changes made in both transactions. For this reason, `ExclusiveLock` is held on an IMMV immediately after a base table is modified in `READ COMMITTED` isolation level. This ensures that the IMMV is updated in the latter transaction only after the former transaction has committed. In `REPEATABLE READ` or `SERIALIZABLE` isolation level, an error is raised immediately if lock acquisition fails, as changes made by other transactions are not visible in these levels, and the IMMV cannot be updated correctly in such situations. However, as an exception if the IMMV has only one base table, does not use DISTINCT or GROUP BY, and is modified by `INSERT`, then the lock held on the IMMV is `RowExclusiveLock`. Even if we are able to acquire a lock, a concurrent transaction may have already incrementally updated and committed the view before we can acquire the lock. In `REPEATABLE READ` or `SERIALIZABLE` isolation level, this could lead to an inconsistent state of the view. Therefore, an error is raised to prevent anomalies when this situation is detected. ### Row Level Security If some base tables have row level security policy, rows that are not visible to the materialized view's owner are excluded from the result. In addition, such rows are excluded as well when views are incrementally maintained. However, if a new policy is defined or policies are changed after the materialized view was created, the new policy will not be applied to the view contents. To apply the new policy, you need to refresh or recreate the IMMV. ### How to Disable or Enable Immediate Maintenance IVM is effective when we want to keep an IMMV up-to-date and small fraction of a base table is modified infrequently. Due to the overhead of immediate maintenance, IVM is not effective when a base table is modified frequently. Also, when a large part of a base table is modified or large data is inserted into a base table, IVM is not effective and the cost of maintenance can be larger than refresh from scratch. In such situation, we can use `refresh_immv` function with `with_data = false` to disable immediate maintenance before modifying a base table. After a base table modification, call `refresh_immv`with `with_data = true` to refresh the view data and enable immediate maintenance. ## Developer IVM Development Group - Original Authors - https://github.com/yugo-n - https://github.com/thoshiai ## License [PostgreSQL License](https://github.com/sraoss/pg_ivm/blob/main/LICENSE) ## Copyright - Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group - Portions Copyright (c) 2022, IVM Development Group pg_ivm-1.13/createas.c000066400000000000000000001713101507512346500146650ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * createas.c * incremental view maintenance extension * Routines for creating IMMVs * * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group * Portions Copyright (c) 2022, IVM Development Group * *------------------------------------------------------------------------- */ #include "postgres.h" #include "access/heapam.h" #include "access/reloptions.h" #include "access/xact.h" #include "catalog/dependency.h" #include "catalog/index.h" #include "catalog/indexing.h" #include "catalog/pg_am.h" #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/pg_trigger_d.h" #include "catalog/toasting.h" #include "commands/createas.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" #include "commands/trigger.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "optimizer/clauses.h" #include "optimizer/optimizer.h" #include "optimizer/prep.h" #include "parser/parser.h" #include "parser/parsetree.h" #include "parser/parse_clause.h" #include "parser/parse_func.h" #include "parser/parse_type.h" #include "rewrite/rewriteHandler.h" #include "rewrite/rewriteManip.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/regproc.h" #include "utils/rel.h" #include "utils/syscache.h" #include "pg_ivm.h" typedef struct { DestReceiver pub; /* publicly-known function pointers */ IntoClause *into; /* target relation specification */ /* These fields are filled by intorel_startup: */ Relation rel; /* relation to write to */ ObjectAddress reladdr; /* address of rel, for ExecCreateTableAs */ CommandId output_cid; /* cmin to insert in output tuples */ int ti_options; /* table_tuple_insert performance options */ BulkInsertState bistate; /* bulk insert state */ } DR_intorel; /* utility functions for IMMV definition creation */ static ObjectAddress create_immv_internal(List *attrList, IntoClause *into); static ObjectAddress create_immv_nodata(List *tlist, IntoClause *into); typedef struct { bool has_agg; /* the query has an aggregate */ bool has_outerjoin; bool has_subquery; bool allow_exists; /* EXISTS subquery is allowed in the current node */ bool in_exists_subquery; /* true, if under an EXISTS subquery */ List *join_quals; Relids outer_join_rels; List *exists_qual_vars; /* Vars used in EXISTS subqueries */ int sublevels_up; /* (current) nesting depth */ } check_ivm_restriction_context; static void CreateIvmTriggersOnBaseTablesRecurse(Query *qry, Node *node, Oid matviewOid, Relids *relids, bool ex_lock); static void CreateIvmTrigger(Oid relOid, Oid viewOid, int16 type, int16 timing, bool ex_lock); static void check_ivm_restriction(Node *node); static bool check_ivm_restriction_walker(Node *node, check_ivm_restriction_context *context); static Bitmapset *get_primary_key_attnos_from_query(Query *query, List **constraintList); static bool is_equijoin_condition(OpExpr *op, check_ivm_restriction_context *context); static bool check_aggregate_supports_ivm(Oid aggfnoid); static void StoreImmvQuery(Oid viewOid, Query *viewQuery); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM < 140000) static bool CreateTableAsRelExists(CreateTableAsStmt *ctas); #endif /* * create_immv_internal * * Internal utility used for the creation of the definition of an IMMV. * Caller needs to provide a list of attributes (ColumnDef nodes). * * This imitates PostgreSQL's create_ctas_internal(). */ static ObjectAddress create_immv_internal(List *attrList, IntoClause *into) { CreateStmt *create = makeNode(CreateStmt); char relkind; Datum toast_options; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 180000) const char *const validnsps[] = HEAP_RELOPT_NAMESPACES; #else static char *validnsps[] = HEAP_RELOPT_NAMESPACES; #endif ObjectAddress intoRelationAddr; /* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */ /* relkind of IMMV must be RELKIND_RELATION */ relkind = RELKIND_RELATION; /* * Create the target relation by faking up a CREATE TABLE parsetree and * passing it to DefineRelation. */ create->relation = into->rel; create->tableElts = attrList; create->inhRelations = NIL; create->ofTypename = NULL; create->constraints = NIL; create->options = into->options; create->oncommit = into->onCommit; create->tablespacename = into->tableSpaceName; create->if_not_exists = false; create->accessMethod = into->accessMethod; /* * Create the relation. (This will error out if there's an existing view, * so we don't need more code to complain if "replace" is false.) */ intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL); /* * If necessary, create a TOAST table for the target table. Note that * NewRelationCreateToastTable ends with CommandCounterIncrement(), so * that the TOAST table will be visible for insertion. */ CommandCounterIncrement(); /* parse and validate reloptions for the toast table */ toast_options = transformRelOptions((Datum) 0, create->options, "toast", validnsps, true, false); (void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true); NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options); /* Create the "view" part of an IMMV. */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 180000) StoreImmvQuery(intoRelationAddr.objectId, into->viewQuery); #else StoreImmvQuery(intoRelationAddr.objectId, (Query *) into->viewQuery); #endif CommandCounterIncrement(); return intoRelationAddr; } /* * create_immv_nodata * * Create an IMMV when WITH NO DATA is used, starting from * the targetlist of the view definition. * * This imitates PostgreSQL's create_ctas_nodata(). */ static ObjectAddress create_immv_nodata(List *tlist, IntoClause *into) { List *attrList; ListCell *t, *lc; /* * Build list of ColumnDefs from non-junk elements of the tlist. If a * column name list was specified in CREATE TABLE AS, override the column * names in the query. (Too few column names are OK, too many are not.) */ attrList = NIL; lc = list_head(into->colNames); foreach(t, tlist) { TargetEntry *tle = (TargetEntry *) lfirst(t); if (!tle->resjunk) { ColumnDef *col; char *colname; if (lc) { colname = strVal(lfirst(lc)); lc = lnext(into->colNames, lc); } else colname = tle->resname; col = makeColumnDef(colname, exprType((Node *) tle->expr), exprTypmod((Node *) tle->expr), exprCollation((Node *) tle->expr)); /* * It's possible that the column is of a collatable type but the * collation could not be resolved, so double-check. (We must * check this here because DefineRelation would adopt the type's * default collation rather than complaining.) */ if (!OidIsValid(col->collOid) && type_is_collatable(col->typeName->typeOid)) ereport(ERROR, (errcode(ERRCODE_INDETERMINATE_COLLATION), errmsg("no collation was derived for column \"%s\" with collatable type %s", col->colname, format_type_be(col->typeName->typeOid)), errhint("Use the COLLATE clause to set the collation explicitly."))); attrList = lappend(attrList, col); } } if (lc != NULL) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("too many column names were specified"))); /* Create the relation definition using the ColumnDef list */ return create_immv_internal(attrList, into); } /* * ExecCreateImmv -- execute a create_immv() function * * This imitates PostgreSQL's ExecCreateTableAs(). */ ObjectAddress ExecCreateImmv(ParseState *pstate, CreateTableAsStmt *stmt, QueryCompletion *qc) { Query *query = castNode(Query, stmt->query); IntoClause *into = stmt->into; bool do_refresh = false; ObjectAddress address; /* Check if the relation exists or not */ if (CreateTableAsRelExists(stmt)) return InvalidObjectAddress; /* * The contained Query must be a SELECT. */ Assert(query->commandType == CMD_SELECT); /* * For materialized views, always skip data during table creation, and use * REFRESH instead (see below). */ do_refresh = !into->skipData; /* check if the query is supported in IMMV definition */ if (contain_mutable_functions((Node *) query)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("mutable function is not supported on incrementally maintainable materialized view"), errhint("functions must be marked IMMUTABLE"))); check_ivm_restriction((Node *) query); /* For IMMV, we need to rewrite matview query */ query = rewriteQueryForIMMV(query, into->colNames); /* * If WITH NO DATA was specified, do not go through the rewriter, * planner and executor. Just define the relation using a code path * similar to CREATE VIEW. This avoids dump/restore problems stemming * from running the planner before all dependencies are set up. */ address = create_immv_nodata(query->targetList, into); /* * For materialized views, reuse the REFRESH logic, which locks down * security-restricted operations and restricts the search_path. This * reduces the chance that a subsequent refresh will fail. */ if (do_refresh) { Relation matviewRel; RefreshImmvByOid(address.objectId, true, false, pstate->p_sourcetext, qc); if (qc) qc->commandTag = CMDTAG_SELECT; matviewRel = table_open(address.objectId, NoLock); /* Create an index on incremental maintainable materialized view, if possible */ CreateIndexOnIMMV(query, matviewRel); /* Create triggers to prevent IMMV from being changed */ CreateChangePreventTrigger(address.objectId); table_close(matviewRel, NoLock); if (IsolationUsesXactSnapshot()) ereport(WARNING, (errmsg("inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ"), errdetail("The view may not include effects of a concurrent transaction."), errhint("create_immv should be used in isolation level READ COMMITTED, " "or execute refresh_immv to make sure the view is consistent."))); } return address; } /* * rewriteQueryForIMMV -- rewrite view definition query for IMMV * * count(*) is added for counting distinct tuples in views. * Also, additional hidden columns are added for aggregate values. * * EXISTS sublink is rewritten to LATERAL subquery with HAVING * clause to check count(*) > 0. In addition, a counting column * referring to count(*) in this subquery is added to the original * target list. */ Query * rewriteQueryForIMMV(Query *query, List *colNames) { Query *rewritten; Node *node; ParseState *pstate = make_parsestate(NULL); FuncCall *fn; /* * Check the length of column name list not to override names of * additional columns */ if (list_length(colNames) > list_length(query->targetList)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("too many column names were specified"))); rewritten = copyObject(query); pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; /* * If this query has EXISTS clause, rewrite query and * add __ivm_exists_count_X__ column. */ if (rewritten->hasSubLinks) { ListCell *lc; RangeTblEntry *rte; int varno = 0; /* rewrite EXISTS sublink to LATERAL subquery */ rewrite_query_for_exists_subquery(rewritten); /* Add counting column referring to count(*) in EXISTS clause */ foreach(lc, rewritten->rtable) { char *columnName; int attnum; Node *countCol = NULL; varno++; rte = (RangeTblEntry *) lfirst(lc); if (!rte->subquery || !rte->lateral) continue; pstate->p_rtable = rewritten->rtable; columnName = getColumnNameStartWith(rte, "__ivm_exists", &attnum); if (columnName == NULL) continue; countCol = (Node *) makeVar(varno, attnum, INT8OID, -1, InvalidOid, 0); if (countCol != NULL) { TargetEntry *tle = makeTargetEntry((Expr *) countCol, list_length(rewritten->targetList) + 1, pstrdup(columnName), false); rewritten->targetList = list_concat(rewritten->targetList, list_make1(tle)); } } } /* group keys must be in targetlist */ if (rewritten->groupClause) { ListCell *lc; foreach(lc, rewritten->groupClause) { SortGroupClause *scl = (SortGroupClause *) lfirst(lc); TargetEntry *tle = get_sortgroupclause_tle(scl, rewritten->targetList); if (tle->resjunk) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("GROUP BY expression not appearing in select list is not supported on incrementally maintainable materialized view"))); } } /* Convert DISTINCT to GROUP BY. count(*) will be added afterward. */ else if (!rewritten->hasAggs && rewritten->distinctClause) rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false); /* Add additional columns for aggregate values */ if (rewritten->hasAggs) { ListCell *lc; List *aggs = NIL; AttrNumber next_resno = list_length(rewritten->targetList) + 1; foreach(lc, rewritten->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); char *resname = (colNames == NIL || foreach_current_index(lc) >= list_length(colNames) ? tle->resname : strVal(list_nth(colNames, tle->resno - 1))); if (IsA(tle->expr, Aggref)) makeIvmAggColumn(pstate, (Aggref *) tle->expr, resname, &next_resno, &aggs); } rewritten->targetList = list_concat(rewritten->targetList, aggs); } /* Add count(*) for counting distinct tuples in views */ if (rewritten->distinctClause || rewritten->hasAggs) { TargetEntry *tle; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); #else fn = makeFuncCall(SystemFuncName("count"), NIL, -1); #endif fn->agg_star = true; node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); tle = makeTargetEntry((Expr *) node, list_length(rewritten->targetList) + 1, pstrdup("__ivm_count__"), false); rewritten->targetList = lappend(rewritten->targetList, tle); rewritten->hasAggs = true; } return rewritten; } /* * makeIvmAggColumn -- make additional aggregate columns for IVM * * For an aggregate column specified by aggref, additional aggregate columns * are added, which are used to calculate the new aggregate value in IMMV. * An additional aggregate columns has a name based on resname * (ex. ivm_count_resname), and resno specified by next_resno. The created * columns are returned to aggs, and the resno for the next column is also * returned to next_resno. * * Currently, an additional count() is created for aggref other than count. * In addition, sum() is created for avg aggregate column. */ void makeIvmAggColumn(ParseState *pstate, Aggref *aggref, char *resname, AttrNumber *next_resno, List **aggs) { TargetEntry *tle_count; Node *node; FuncCall *fn; Const *dmy_arg = makeConst(INT4OID, -1, InvalidOid, sizeof(int32), Int32GetDatum(1), false, true); /* pass by value */ const char *aggname = get_func_name(aggref->aggfnoid); /* * For aggregate functions except count, add count() func with the same arg parameters. * This count result is used for determining if the aggregate value should be NULL or not. * Also, add sum() func for avg because we need to calculate an average value as sum/count. * * XXX: If there are same expressions explicitly in the target list, we can use this instead * of adding new duplicated one. */ if (strcmp(aggname, "count") != 0) { #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); #else fn = makeFuncCall(SystemFuncName("count"), NIL, -1); #endif /* Make a Func with a dummy arg, and then override this by the original agg's args. */ node = ParseFuncOrColumn(pstate, fn->funcname, list_make1(dmy_arg), NULL, fn, false, -1); ((Aggref *) node)->args = aggref->args; tle_count = makeTargetEntry((Expr *) node, *next_resno, pstrdup(makeObjectName("__ivm_count",resname, "_")), false); *aggs = lappend(*aggs, tle_count); (*next_resno)++; } if (strcmp(aggname, "avg") == 0) { List *dmy_args = NIL; ListCell *lc; foreach(lc, aggref->aggargtypes) { Oid typeid = lfirst_oid(lc); Type type = typeidType(typeid); Const *con = makeConst(typeid, -1, typeTypeCollation(type), typeLen(type), (Datum) 0, true, typeByVal(type)); dmy_args = lappend(dmy_args, con); ReleaseSysCache(type); } #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) fn = makeFuncCall(SystemFuncName("sum"), NIL, COERCE_EXPLICIT_CALL, -1); #else fn = makeFuncCall(SystemFuncName("sum"), NIL, -1); #endif /* Make a Func with dummy args, and then override this by the original agg's args. */ node = ParseFuncOrColumn(pstate, fn->funcname, dmy_args, NULL, fn, false, -1); ((Aggref *) node)->args = aggref->args; tle_count = makeTargetEntry((Expr *) node, *next_resno, pstrdup(makeObjectName("__ivm_sum",resname, "_")), false); *aggs = lappend(*aggs, tle_count); (*next_resno)++; } } /* * CreateIvmTriggersOnBaseTables -- create IVM triggers on all base tables */ void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid) { Relids relids = NULL; bool ex_lock = false; RangeTblEntry *rte; /* Immediately return if we don't have any base tables. */ if (list_length(qry->rtable) < 1) return; /* * If the view has more than one base tables, we need an exclusive lock * on the view so that the view would be maintained serially to avoid * the inconsistency that occurs when two base tables are modified in * concurrent transactions. However, if the view has only one table, * we can use a weaker lock. * * The type of lock should be determined here, because if we check the * view definition at maintenance time, we need to acquire a weaker lock, * and upgrading the lock level after this increases probability of * deadlock. * * XXX: For the current extension version, DISTINCT and aggregates with GROUP * need exclusive lock to prevent inconsistency that can be avoided by using * nulls_not_distinct which is available only in PG15 or later. * XXX: This lock is not necessary if all columns in group keys or distinct * target list are not nullable. */ rte = list_nth(qry->rtable, 0); if (list_length(qry->rtable) > 1 || rte->rtekind != RTE_RELATION || qry->distinctClause || (qry->hasAggs && qry->groupClause)) ex_lock = true; CreateIvmTriggersOnBaseTablesRecurse(qry, (Node *)qry, matviewOid, &relids, ex_lock); bms_free(relids); } static void CreateIvmTriggersOnBaseTablesRecurse(Query *qry, Node *node, Oid matviewOid, Relids *relids, bool ex_lock) { if (node == NULL) return; /* This can recurse, so check for excessive recursion */ check_stack_depth(); switch (nodeTag(node)) { case T_Query: { Query *query = (Query *) node; ListCell *lc; CreateIvmTriggersOnBaseTablesRecurse(qry, (Node *) query->jointree, matviewOid, relids, ex_lock); foreach(lc, query->cteList) { CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc); Assert(IsA(cte->ctequery, Query)); CreateIvmTriggersOnBaseTablesRecurse((Query *) cte->ctequery, cte->ctequery, matviewOid, relids, ex_lock); } } break; case T_RangeTblRef: { int rti = ((RangeTblRef *) node)->rtindex; RangeTblEntry *rte = rt_fetch(rti, qry->rtable); if (rte->rtekind == RTE_RELATION && !bms_is_member(rte->relid, *relids)) { CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_INSERT, TRIGGER_TYPE_BEFORE, ex_lock); CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_DELETE, TRIGGER_TYPE_BEFORE, ex_lock); CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_UPDATE, TRIGGER_TYPE_BEFORE, ex_lock); CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_TRUNCATE, TRIGGER_TYPE_BEFORE, true); CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_INSERT, TRIGGER_TYPE_AFTER, ex_lock); CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_DELETE, TRIGGER_TYPE_AFTER, ex_lock); CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_UPDATE, TRIGGER_TYPE_AFTER, ex_lock); CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_TRUNCATE, TRIGGER_TYPE_AFTER, true); *relids = bms_add_member(*relids, rte->relid); } else if (rte->rtekind == RTE_SUBQUERY) { Query *subquery = rte->subquery; Assert(rte->subquery != NULL); CreateIvmTriggersOnBaseTablesRecurse(subquery, (Node *) subquery, matviewOid, relids, ex_lock); } } break; case T_FromExpr: { FromExpr *f = (FromExpr *) node; ListCell *l; foreach(l, f->fromlist) CreateIvmTriggersOnBaseTablesRecurse(qry, lfirst(l), matviewOid, relids, ex_lock); } break; case T_JoinExpr: { JoinExpr *j = (JoinExpr *) node; CreateIvmTriggersOnBaseTablesRecurse(qry, j->larg, matviewOid, relids, ex_lock); CreateIvmTriggersOnBaseTablesRecurse(qry, j->rarg, matviewOid, relids, ex_lock); } break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); } } /* * CreateIvmTrigger -- create IVM trigger on a base table */ static void CreateIvmTrigger(Oid relOid, Oid viewOid, int16 type, int16 timing, bool ex_lock) { ObjectAddress refaddr; ObjectAddress address; CreateTrigStmt *ivm_trigger; List *transitionRels = NIL; Assert(timing == TRIGGER_TYPE_BEFORE || timing == TRIGGER_TYPE_AFTER); refaddr.classId = RelationRelationId; refaddr.objectId = viewOid; refaddr.objectSubId = 0; ivm_trigger = makeNode(CreateTrigStmt); ivm_trigger->relation = NULL; ivm_trigger->row = false; ivm_trigger->timing = timing; ivm_trigger->events = type; switch (type) { case TRIGGER_TYPE_INSERT: ivm_trigger->trigname = (timing == TRIGGER_TYPE_BEFORE ? "IVM_trigger_ins_before" : "IVM_trigger_ins_after"); break; case TRIGGER_TYPE_DELETE: ivm_trigger->trigname = (timing == TRIGGER_TYPE_BEFORE ? "IVM_trigger_del_before" : "IVM_trigger_del_after"); break; case TRIGGER_TYPE_UPDATE: ivm_trigger->trigname = (timing == TRIGGER_TYPE_BEFORE ? "IVM_trigger_upd_before" : "IVM_trigger_upd_after"); break; case TRIGGER_TYPE_TRUNCATE: ivm_trigger->trigname = (timing == TRIGGER_TYPE_BEFORE ? "IVM_trigger_truncate_before" : "IVM_trigger_truncate_after"); break; default: elog(ERROR, "unsupported trigger type"); } if (timing == TRIGGER_TYPE_AFTER) { if (type == TRIGGER_TYPE_INSERT || type == TRIGGER_TYPE_UPDATE) { TriggerTransition *n = makeNode(TriggerTransition); n->name = "__ivm_newtable"; n->isNew = true; n->isTable = true; transitionRels = lappend(transitionRels, n); } if (type == TRIGGER_TYPE_DELETE || type == TRIGGER_TYPE_UPDATE) { TriggerTransition *n = makeNode(TriggerTransition); n->name = "__ivm_oldtable"; n->isNew = false; n->isTable = true; transitionRels = lappend(transitionRels, n); } } /* * XXX: When using DELETE or UPDATE, we must use exclusive lock for now * because apply_old_delta(_with_count) doesn't work in concurrent situations. * * If the view doesn't have aggregate, distinct, or tuple duplicate, then it * would work. However, we don't have any way to guarantee the view has a unique * key before opening the IMMV at the maintenance time because users may drop * the unique index. We need something to resolve the issue!! */ if (type == TRIGGER_TYPE_DELETE || type == TRIGGER_TYPE_UPDATE) ex_lock = true; ivm_trigger->funcname = (timing == TRIGGER_TYPE_BEFORE ? PgIvmFuncName("IVM_immediate_before") : PgIvmFuncName("IVM_immediate_maintenance")); ivm_trigger->columns = NIL; ivm_trigger->transitionRels = transitionRels; ivm_trigger->whenClause = NULL; ivm_trigger->isconstraint = false; ivm_trigger->deferrable = false; ivm_trigger->initdeferred = false; ivm_trigger->constrrel = NULL; ivm_trigger->args = list_make2( makeString(DatumGetPointer(DirectFunctionCall1(oidout, ObjectIdGetDatum(viewOid)))), makeString(DatumGetPointer(DirectFunctionCall1(boolout, BoolGetDatum(ex_lock)))) ); address = CreateTrigger(ivm_trigger, NULL, relOid, InvalidOid, InvalidOid, InvalidOid, InvalidOid, InvalidOid, NULL, true, false); recordDependencyOn(&address, &refaddr, DEPENDENCY_AUTO); /* Make changes-so-far visible */ CommandCounterIncrement(); } /* * check_ivm_restriction --- look for specify nodes in the query tree */ static void check_ivm_restriction(Node *node) { check_ivm_restriction_context context; context.has_agg = false; context.has_outerjoin = false; context.has_subquery = false; context.allow_exists = false; context.in_exists_subquery = false; context.join_quals = NIL; context.outer_join_rels = NULL; context.exists_qual_vars = NIL; context.sublevels_up = 0; check_ivm_restriction_walker(node, &context); } static bool check_ivm_restriction_walker(Node *node, check_ivm_restriction_context *context) { /* EXISTS is allowed only in this node */ bool allow_exists = context->allow_exists; context->allow_exists = false; if (node == NULL) return false; /* This can recurse, so check for excessive recursion */ check_stack_depth(); switch (nodeTag(node)) { case T_Query: { Query *qry = (Query *) node; ListCell *lc; List *vars; if (qry->groupClause != NIL && !qry->hasAggs) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("GROUP BY clause without aggregate is not supported on incrementally maintainable materialized view"))); if (qry->havingQual != NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("HAVING clause is not supported on incrementally maintainable materialized view"))); if (qry->sortClause != NIL) /* There is a possibility that we don't need to return an error */ ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("ORDER BY clause is not supported on incrementally maintainable materialized view"))); if (qry->limitOffset != NULL || qry->limitCount != NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view"))); if (qry->hasDistinctOn) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("DISTINCT ON is not supported on incrementally maintainable materialized view"))); if (qry->hasWindowFuncs) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("window functions are not supported on incrementally maintainable materialized view"))); if (qry->groupingSets != NIL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("GROUPING SETS, ROLLUP, or CUBE clauses is not supported on incrementally maintainable materialized view"))); if (qry->setOperations != NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("UNION/INTERSECT/EXCEPT statements are not supported on incrementally maintainable materialized view"))); if (list_length(qry->targetList) == 0) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("empty target list is not supported on incrementally maintainable materialized view"))); if (qry->rowMarks != NIL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("FOR UPDATE/SHARE clause is not supported on incrementally maintainable materialized view"))); if (qry->hasRecursive) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("recursive query is not supported on incrementally maintainable materialized view"))); /* system column restrictions */ vars = pull_vars_of_level((Node *) qry, 0); foreach(lc, vars) { if (IsA(lfirst(lc), Var)) { Var *var = (Var *) lfirst(lc); /* if the view has a system column, raise an error */ if (var->varattno < 0) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("system column is not supported on incrementally maintainable materialized view"))); } } /* check that each type in the target list has an equality operator */ if (context->sublevels_up == 0) { foreach(lc, qry->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Oid atttype = exprType((Node *) tle->expr); Oid opclass; opclass = GetDefaultOpClass(atttype, BTREE_AM_OID); if (!OidIsValid(opclass)) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("data type %s has no default operator class for access method \"%s\"", format_type_be(atttype), "btree"))); } } /* subquery restrictions */ if (context->sublevels_up > 0 && qry->distinctClause != NIL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("DISTINCT clause in nested query are not supported on incrementally maintainable materialized view"))); if (context->sublevels_up > 0 && qry->hasAggs) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("aggregate functions in nested query are not supported on incrementally maintainable materialized view"))); context->has_agg |= qry->hasAggs; /* restrictions for rtable */ foreach(lc, qry->rtable) { RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); if (rte->tablesample != NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("TABLESAMPLE clause is not supported on incrementally maintainable materialized view"))); if (rte->relkind == RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("partitioned table is not supported on incrementally maintainable materialized view"))); if (rte->relkind == RELKIND_RELATION && has_superclass(rte->relid)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("partitions is not supported on incrementally maintainable materialized view"))); if (rte->relkind == RELKIND_RELATION && find_inheritance_children(rte->relid, NoLock) != NIL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("inheritance parent is not supported on incrementally maintainable materialized view"))); if (rte->relkind == RELKIND_FOREIGN_TABLE) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("foreign table is not supported on incrementally maintainable materialized view"))); if (rte->relkind == RELKIND_VIEW || rte->relkind == RELKIND_MATVIEW) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("VIEW or MATERIALIZED VIEW is not supported on incrementally maintainable materialized view"))); if (rte->rtekind == RTE_VALUES) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("VALUES is not supported on incrementally maintainable materialized view"))); if (rte->relkind == RELKIND_RELATION && isImmv(rte->relid)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("including IMMV in definition is not supported on incrementally maintainable materialized view"))); if (rte->rtekind == RTE_SUBQUERY) { context->has_subquery = true; if (context->has_outerjoin) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("subquery is not supported with outer join"))); context->sublevels_up++; check_ivm_restriction_walker((Node *) rte->subquery, context); context->sublevels_up--; } } query_tree_walker(qry, check_ivm_restriction_walker, (void *) context, QTW_IGNORE_RT_SUBQUERIES); /* * additional restriction checks for exists subquery * * If the query has an EXISTS subquery and columns of a table in * the outer query are used in the EXISTS subquery, those columns * must be included in the target list. These columns are required * to identify tuples in the view to be affected by modification * of tables in the EXISTS subquery. */ if (context->exists_qual_vars != NIL && context->sublevels_up == 0) { foreach (lc, context->exists_qual_vars) { Var *var = (Var *) lfirst(lc); ListCell *lc2; bool found = false; foreach(lc2, qry->targetList) { TargetEntry *tle = lfirst(lc2); Var *var2; if (!IsA(tle->expr, Var)) continue; var2 = (Var *) tle->expr; if (var->varno == var2->varno && var->varattno == var2->varattno) { found = true; break; } } if (!found) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("targetlist must contain vars that are referred to in EXISTS subquery"))); } } /* additional restriction checks for outer join query */ if (context->has_outerjoin && context->sublevels_up == 0) { Relids where_relids; Relids nonnullable_rels = find_nonnullable_rels((Node *) qry->jointree->quals); List *qual_vars = NIL; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) PlannerInfo root; #endif foreach (lc, context->join_quals) { OpExpr *op = (OpExpr *) lfirst(lc); /* * Assumed in transforming the jointree tree to normalized form * and for checking whether some tuples are joined by an outer-join qual. * XXX: Perhaps, this restriction could be relaxed a bit. */ if (!is_equijoin_condition(op, context)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("Only simple equijoin is supported with outer join"))); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) op = (OpExpr *) flatten_join_alias_vars(NULL, qry, (Node *) op); #else op = (OpExpr *) flatten_join_alias_vars(qry, (Node *) op); #endif qual_vars = list_concat(qual_vars, pull_vars_of_level((Node *) op, 0)); } foreach (lc, qual_vars) { Var *var = lfirst(lc); ListCell *lc2; bool found = false; foreach(lc2, qry->targetList) { TargetEntry *tle = lfirst(lc2); if (IsA(tle->expr, Var)) { #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) Var *var2 = (Var *) flatten_join_alias_vars(NULL, qry, (Node *) tle->expr); #else Var *var2 = (Var *) flatten_join_alias_vars(qry, (Node *) tle->expr); #endif if (var->varno == var2->varno && var->varattno == var2->varattno) { found = true; break; } } } /* * For checking whether the view or the primary delta * has any tuples generated by the outer-join. */ if (!found) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("targetlist must contain vars in the join condition with outer join"))); } /* * For checking whether the view or the primary delta * has any tuples generated by the outer-join. * */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) where_relids = pull_varnos(&root, flatten_join_alias_vars(NULL, qry, (Node *) qry->jointree->quals)); where_relids = bms_del_members(where_relids, context->outer_join_rels); #elif defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) where_relids = pull_varnos(&root, flatten_join_alias_vars(qry, (Node *) qry->jointree->quals)); #else where_relids = pull_varnos(flatten_join_alias_vars(qry, (Node *) qry->jointree->quals)); #endif if (!bms_equal(where_relids, nonnullable_rels)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("WHERE cannot contain non null-rejecting predicates with outer join"))); /* * For generateing dangling tuples to be inserted from the primary delta. * See the comments in insert_dangling_tuples(). */ if (contain_nonstrict_functions((Node *) qry->targetList)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("targetlist cannot contain non strict functions with outer join"))); bms_free(nonnullable_rels); bms_free(where_relids); } break; } case T_CommonTableExpr: { CommonTableExpr *cte = (CommonTableExpr *) node; if (isIvmName(cte->ctename)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("WITH query name %s is not supported on incrementally maintainable materialized view", cte->ctename))); /* * When a table in an unreferenced CTE is TRUNCATEd, the contents of the * IMMV is not affected so it must not be truncated. For confirming it * at the maintenance time, we have to check if the modified table used * in a CTE is actually referenced. Although it would be possible, we * just disallow to create such IMMVs for now since such unreferenced * CTE is useless unless it doesn't contain modifying commands, that is * already prohibited. */ if (cte->cterefcount == 0) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("Ureferenced WITH query is not supported on incrementally maintainable materialized view"))); context->sublevels_up++; check_ivm_restriction_walker(cte->ctequery, (void *) context); context->sublevels_up--; break; } case T_TargetEntry: { TargetEntry *tle = (TargetEntry *) node; if (isIvmName(tle->resname)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("column name %s is not supported on incrementally maintainable materialized view", tle->resname))); if (context->has_agg && !IsA(tle->expr, Aggref) && contain_aggs_of_level((Node *) tle->expr, 0)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("expression containing an aggregate in it is not supported on incrementally maintainable materialized view"))); expression_tree_walker(node, check_ivm_restriction_walker, (void *) context); break; } case T_FromExpr: { FromExpr *from = (FromExpr *) node; check_ivm_restriction_walker((Node *) from->fromlist, context); /* * EXIST is allowed directly under FROM clause */ context->allow_exists = true; check_ivm_restriction_walker(from->quals, context); break; } case T_JoinExpr: { JoinExpr *joinexpr = (JoinExpr *) node; if (IS_OUTER_JOIN(joinexpr->jointype)) { if (context->has_subquery) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("subquery is not supported with outer join"))); if (context->has_agg) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("aggregate is not supported with outer join"))); context->has_outerjoin = true; context->join_quals = lappend(context->join_quals, joinexpr->quals); context->outer_join_rels = bms_add_member(context->outer_join_rels, joinexpr->rtindex); } expression_tree_walker(node, check_ivm_restriction_walker, (void *) context); break; } case T_Aggref: { /* Check if this supports IVM */ Aggref *aggref = (Aggref *) node; const char *aggname = format_procedure(aggref->aggfnoid); if (aggref->aggfilter != NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("aggregate function with FILTER clause is not supported on incrementally maintainable materialized view"))); if (aggref->aggdistinct != NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("aggregate function with DISTINCT arguments is not supported on incrementally maintainable materialized view"))); if (aggref->aggorder != NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("aggregate function with ORDER clause is not supported on incrementally maintainable materialized view"))); if (!check_aggregate_supports_ivm(aggref->aggfnoid)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("aggregate function %s is not supported on incrementally maintainable materialized view", aggname))); expression_tree_walker(node, check_ivm_restriction_walker, (void *) context); break; } case T_Var: { Var *variable = (Var *) node; /* * Currently, only EXISTS clause is allowed here. * If EXISTS subquery refers to vars of the upper query, collect these vars. */ if (variable->varlevelsup > 0 && context->in_exists_subquery) context->exists_qual_vars = lappend(context->exists_qual_vars, node); break; } case T_BoolExpr: { BoolExpr *expr = (BoolExpr *) node; BoolExprType type = ((BoolExpr *) node)->boolop; ListCell *lc; switch (type) { case AND_EXPR: foreach(lc, expr->args) { Node *opnode = (Node *) lfirst(lc); /* * EXIST is allowed under AND expression only if it is * directly under WHERE. */ if (allow_exists) context->allow_exists = true; check_ivm_restriction_walker(opnode, context); } break; case OR_EXPR: case NOT_EXPR: if (checkExprHasSubLink(node)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("OR or NOT conditions and EXISTS condition can not be used together"))); expression_tree_walker((Node *) expr->args, check_ivm_restriction_walker, (void *) context); break; } break; } case T_SubLink: { Query *subselect; SubLink *sublink = (SubLink *) node; /* Only EXISTS clause is supported if it is directly under WHERE */ if (!allow_exists || sublink->subLinkType != EXISTS_SUBLINK) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("sublink only supports subquery with EXISTS clause in WHERE clause"))); if (context->sublevels_up > 0) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("nested sublink is not supported on incrementally maintainable materialized view"))); subselect = (Query *) sublink->subselect; /* raise ERROR if the sublink has CTE */ if (subselect->cteList) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("CTE in EXIST clause is not supported on incrementally maintainable materialized view"))); if (context->has_outerjoin) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("subquery with outer join is not supported"))); context->in_exists_subquery = true; context->sublevels_up++; check_ivm_restriction_walker(sublink->subselect, context); context->sublevels_up--; context->in_exists_subquery = false; break; } default: expression_tree_walker(node, check_ivm_restriction_walker, (void *) context); break; } return false; } /* * is_equijoin_condition - check if all operators must be btree equality or hash equality */ static bool is_equijoin_condition(OpExpr *op, check_ivm_restriction_context *context) { Oid opno; Node *left_expr; Node *right_expr; Relids left_varnos; Relids right_varnos; Oid opinputtype; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) PlannerInfo root; #endif /* Is it a binary opclause? */ if (!IsA(op, OpExpr) || list_length(op->args) != 2) return false; opno = op->opno; left_expr = linitial(op->args); right_expr = lsecond(op->args); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) left_varnos = pull_varnos(&root, left_expr); right_varnos = pull_varnos(&root, right_expr); #else left_varnos = pull_varnos(left_expr); right_varnos = pull_varnos(right_expr); #endif left_varnos = bms_del_members(left_varnos, context->outer_join_rels); right_varnos = bms_del_members(right_varnos, context->outer_join_rels); opinputtype = exprType(left_expr); /* left and right operands must have exactly one different table respectively */ if (bms_num_members(left_varnos) != 1 || bms_num_members(right_varnos) != 1 || bms_equal(left_varnos, right_varnos)) return false; if (op_mergejoinable(opno, opinputtype) && get_mergejoin_opfamilies(opno) != NIL) return true; if (op_hashjoinable(opno, opinputtype)) return true; return false; } /* * check_aggregate_supports_ivm * * Check if the given aggregate function is supporting IVM */ static bool check_aggregate_supports_ivm(Oid aggfnoid) { #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) switch (aggfnoid) { /* count */ case F_COUNT_ANY: case F_COUNT_: /* sum */ case F_SUM_INT8: case F_SUM_INT4: case F_SUM_INT2: case F_SUM_FLOAT4: case F_SUM_FLOAT8: case F_SUM_MONEY: case F_SUM_INTERVAL: case F_SUM_NUMERIC: /* avg */ case F_AVG_INT8: case F_AVG_INT4: case F_AVG_INT2: case F_AVG_NUMERIC: case F_AVG_FLOAT4: case F_AVG_FLOAT8: case F_AVG_INTERVAL: /* min */ case F_MIN_ANYARRAY: case F_MIN_INT8: case F_MIN_INT4: case F_MIN_INT2: case F_MIN_OID: case F_MIN_FLOAT4: case F_MIN_FLOAT8: case F_MIN_DATE: case F_MIN_TIME: case F_MIN_TIMETZ: case F_MIN_MONEY: case F_MIN_TIMESTAMP: case F_MIN_TIMESTAMPTZ: case F_MIN_INTERVAL: case F_MIN_TEXT: case F_MIN_NUMERIC: case F_MIN_BPCHAR: case F_MIN_TID: case F_MIN_ANYENUM: case F_MIN_INET: case F_MIN_PG_LSN: /* max */ case F_MAX_ANYARRAY: case F_MAX_INT8: case F_MAX_INT4: case F_MAX_INT2: case F_MAX_OID: case F_MAX_FLOAT4: case F_MAX_FLOAT8: case F_MAX_DATE: case F_MAX_TIME: case F_MAX_TIMETZ: case F_MAX_MONEY: case F_MAX_TIMESTAMP: case F_MAX_TIMESTAMPTZ: case F_MAX_INTERVAL: case F_MAX_TEXT: case F_MAX_NUMERIC: case F_MAX_BPCHAR: case F_MAX_TID: case F_MAX_ANYENUM: case F_MAX_INET: case F_MAX_PG_LSN: return true; default: return false; } #else char *funcs[] = { /* count */ "count(\"any\")", "count()", /* sum */ "sum(int8)", "sum(int4)", "sum(int2)", "sum(float4)", "sum(float8)", "sum(money)", "sum(interval)", "sum(numeric)", /* avg */ "avg(int8)", "avg(int4)", "avg(int2)", "avg(numeric)", "avg(float4)", "avg(float8)", "avg(interval)", /* min */ "min(anyarray)", "min(int8)", "min(int4)", "min(int2)", "min(oid)", "min(float4)", "min(float8)", "min(date)", "min(time without time zone)", "min(time with time zone)", "min(money)", "min(timestamp without time zone)", "min(timestamp with time zone)", "min(interval)", "min(text)", "min(numeric)", "min(character)", "min(tid)", "min(anyenum)", "min(inet)", "min(pg_lsn)", /* max */ "max(anyarray)", "max(int8)", "max(int4)", "max(int2)", "max(oid)", "max(float4)", "max(float8)", "max(date)", "max(time without time zone)", "max(time with time zone)", "max(money)", "max(timestamp without time zone)", "max(timestamp with time zone)", "max(interval)", "max(text)", "max(numeric)", "max(character)", "max(tid)", "max(anyenum)", "max(inet)", "max(pg_lsn)", NULL }; char **fname = funcs; while (*fname != NULL) { if (DatumGetObjectId(DirectFunctionCall1(to_regprocedure, CStringGetTextDatum(*fname))) == aggfnoid) return true; fname++; } return false; #endif } /* * CreateIndexOnIMMV * * Create a unique index on incremental maintainable materialized view. * If the view definition query has a GROUP BY clause, the index is created * on the columns of GROUP BY expressions. Otherwise, if the view contains * all primary key attributes of its base tables in the target list, the index * is created on these attributes. In other cases, no index is created. */ void CreateIndexOnIMMV(Query *query, Relation matviewRel) { ListCell *lc; IndexStmt *index; ObjectAddress address; List *constraintList = NIL; char idxname[NAMEDATALEN]; List *indexoidlist = RelationGetIndexList(matviewRel); ListCell *indexoidscan; /* * For aggregate without GROUP BY, we do not need to create an index * because the view has only one row. */ if (query->hasAggs && query->groupClause == NIL) return; snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel)); index = makeNode(IndexStmt); /* * We consider null values not distinct to make sure that views with DISTINCT * or GROUP BY don't contain multiple NULL rows when NULL is inserted to * a base table concurrently. */ /* XXX: nulls_not_distinct is available in PG15 or later */ //index->nulls_not_distinct = true; index->unique = true; index->primary = false; index->isconstraint = false; index->deferrable = false; index->initdeferred = false; index->idxname = idxname; index->relation = makeRangeVar(get_namespace_name(RelationGetNamespace(matviewRel)), pstrdup(RelationGetRelationName(matviewRel)), -1); index->accessMethod = DEFAULT_INDEX_TYPE; index->options = NIL; index->tableSpace = get_tablespace_name(matviewRel->rd_rel->reltablespace); index->whereClause = NULL; index->indexParams = NIL; index->indexIncludingParams = NIL; index->excludeOpNames = NIL; index->idxcomment = NULL; index->indexOid = InvalidOid; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) index->oldNumber = InvalidRelFileNumber; index->oldFirstRelfilelocatorSubid = InvalidSubTransactionId; #else index->oldNode = InvalidOid; index->oldFirstRelfilenodeSubid = InvalidSubTransactionId; #endif index->oldCreateSubid = InvalidSubTransactionId; index->transformed = true; index->concurrent = false; index->if_not_exists = false; if (query->groupClause) { /* create unique constraint on GROUP BY expression columns */ foreach(lc, query->groupClause) { SortGroupClause *scl = (SortGroupClause *) lfirst(lc); TargetEntry *tle = get_sortgroupclause_tle(scl, query->targetList); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); IndexElem *iparam; iparam = makeNode(IndexElem); iparam->name = pstrdup(NameStr(attr->attname)); iparam->expr = NULL; iparam->indexcolname = NULL; iparam->collation = NIL; iparam->opclass = NIL; iparam->opclassopts = NIL; iparam->ordering = SORTBY_DEFAULT; iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; index->indexParams = lappend(index->indexParams, iparam); } } else if (query->distinctClause) { /* create unique constraint on all columns */ foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); IndexElem *iparam; iparam = makeNode(IndexElem); iparam->name = pstrdup(NameStr(attr->attname)); iparam->expr = NULL; iparam->indexcolname = NULL; iparam->collation = NIL; iparam->opclass = NIL; iparam->opclassopts = NIL; iparam->ordering = SORTBY_DEFAULT; iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; index->indexParams = lappend(index->indexParams, iparam); } } else { Bitmapset *key_attnos; /* create index on the base tables' primary key columns */ key_attnos = get_primary_key_attnos_from_query(query, &constraintList); if (key_attnos) { foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos)) { IndexElem *iparam; iparam = makeNode(IndexElem); iparam->name = pstrdup(NameStr(attr->attname)); iparam->expr = NULL; iparam->indexcolname = NULL; iparam->collation = NIL; iparam->opclass = NIL; iparam->opclassopts = NIL; iparam->ordering = SORTBY_DEFAULT; iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; index->indexParams = lappend(index->indexParams, iparam); } } } else { /* create no index, just notice that an appropriate index is necessary for efficient IVM */ ereport(NOTICE, (errmsg("could not create an index on immv \"%s\" automatically", RelationGetRelationName(matviewRel)), errdetail("This target list does not have all the primary key columns, " "or this view does not contain GROUP BY or DISTINCT clause."), errhint("Create an index on the immv for efficient incremental maintenance."))); return; } } /* If we have a compatible index, we don't need to create another. */ foreach(indexoidscan, indexoidlist) { Oid indexoid = lfirst_oid(indexoidscan); Relation indexRel; bool hasCompatibleIndex = false; indexRel = index_open(indexoid, AccessShareLock); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 180000) if (CheckIndexCompatible(indexRel->rd_id, index->accessMethod, index->indexParams, index->excludeOpNames, false)) #else if (CheckIndexCompatible(indexRel->rd_id, index->accessMethod, index->indexParams, index->excludeOpNames)) #endif hasCompatibleIndex = true; index_close(indexRel, AccessShareLock); if (hasCompatibleIndex) return; } address = DefineIndex(RelationGetRelid(matviewRel), index, InvalidOid, InvalidOid, InvalidOid, #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) -1, #endif false, true, false, false, true); ereport(NOTICE, (errmsg("created index \"%s\" on immv \"%s\"", idxname, RelationGetRelationName(matviewRel)))); /* * Make dependencies so that the index is dropped if any base tables' * primary key is dropped. */ foreach(lc, constraintList) { Oid constraintOid = lfirst_oid(lc); ObjectAddress refaddr; refaddr.classId = ConstraintRelationId; refaddr.objectId = constraintOid; refaddr.objectSubId = 0; recordDependencyOn(&address, &refaddr, DEPENDENCY_NORMAL); } } /* * get_primary_key_attnos_from_query * * Identify the columns in base tables' primary keys in the target list. * * Returns a Bitmapset of the column attnos of the primary key's columns of * tables that used in the query. The attnos are offset by * FirstLowInvalidHeapAttributeNumber as same as get_primary_key_attnos. * * If any table has no primary key or any primary key's columns is not in * the target list, return NULL. We also return NULL if any pkey constraint * is deferrable. * * constraintList is set to a list of the OIDs of the pkey constraints. */ static Bitmapset * get_primary_key_attnos_from_query(Query *query, List **constraintList) { List *key_attnos_list = NIL; ListCell *lc; int i; Bitmapset *keys = NULL; Relids rels_in_from; /* convert CTEs to subqueries */ query = copyObject(query); foreach (lc, query->cteList) { PlannerInfo root; CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc); if (cte->cterefcount == 0) continue; root.parse = query; inline_cte(&root, cte); } query->cteList = NIL; /* * Collect primary key attributes from all tables used in query. The key attributes * sets for each table are stored in key_attnos_list in order by RTE index. */ foreach(lc, query->rtable) { RangeTblEntry *r = (RangeTblEntry*) lfirst(lc); Bitmapset *key_attnos; bool has_no_pkey = false; /* for subqueries, scan recursively */ if (r->rtekind == RTE_SUBQUERY) { key_attnos = get_primary_key_attnos_from_query(r->subquery, constraintList); has_no_pkey = (key_attnos == NULL); } /* for tables, call get_primary_key_attnos */ else if (r->rtekind == RTE_RELATION) { Oid constraintOid; key_attnos = get_primary_key_attnos(r->relid, false, &constraintOid); *constraintList = lappend_oid(*constraintList, constraintOid); has_no_pkey = (key_attnos == NULL); } /* * Ignore join rels, because they are flatten later by * flatten_join_alias_vars(). Store NULL into key_attnos_list * as a dummy. */ else if (r->rtekind == RTE_JOIN) { key_attnos = NULL; } /* for other RTEs, we assume they have no candidate key */ else has_no_pkey = true; /* * If any table or subquery has no primary key or its pkey constraint * is deferrable (i.e., get_primary_key_attnos returned NULL), * we cannot get key attributes for this query, so return NULL. */ if (has_no_pkey) return NULL; key_attnos_list = lappend(key_attnos_list, key_attnos); } /* Collect key attributes appearing in the target list */ i = 1; foreach(lc, query->targetList) { #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) TargetEntry *tle = (TargetEntry *) flatten_join_alias_vars(NULL, query, lfirst(lc)); #else TargetEntry *tle = (TargetEntry *) flatten_join_alias_vars(query, lfirst(lc)); #endif if (IsA(tle->expr, Var)) { Var *var = (Var*) tle->expr; Bitmapset *key_attnos = list_nth(key_attnos_list, var->varno - 1); /* check if this attribute is from a base table's primary key */ if (bms_is_member(var->varattno - FirstLowInvalidHeapAttributeNumber, key_attnos)) { /* * Remove found key attributes from key_attnos_list, and add this * to the result list. */ key_attnos = bms_del_member(key_attnos, var->varattno - FirstLowInvalidHeapAttributeNumber); if (bms_is_empty(key_attnos)) { key_attnos_list = list_delete_nth_cell(key_attnos_list, var->varno - 1); key_attnos_list = list_insert_nth(key_attnos_list, var->varno - 1, NULL); } keys = bms_add_member(keys, i - FirstLowInvalidHeapAttributeNumber); } } i++; } /* Collect RTE indexes of relations appearing in the FROM clause */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) rels_in_from = get_relids_in_jointree((Node *) query->jointree, false, false); #else rels_in_from = get_relids_in_jointree((Node *) query->jointree, false); #endif /* * Check if all key attributes of relations in FROM are appearing in the target * list. If an attribute remains in key_attnos_list in spite of the table is used * in FROM clause, the target is missing this key attribute, so we return NULL. */ i = 1; foreach(lc, key_attnos_list) { Bitmapset *bms = (Bitmapset *) lfirst(lc); if (!bms_is_empty(bms) && bms_is_member(i, rels_in_from)) return NULL; i++; } return keys; } /* * Store the query for the IMMV to pg_ivm_immv */ static void StoreImmvQuery(Oid viewOid, Query *viewQuery) { char *querytree = nodeToString((Node *) viewQuery); Datum values[Natts_pg_ivm_immv]; bool isNulls[Natts_pg_ivm_immv]; Relation pgIvmImmv; TupleDesc tupleDescriptor; HeapTuple heapTuple; ObjectAddress address; memset(values, 0, sizeof(values)); memset(isNulls, false, sizeof(isNulls)); values[Anum_pg_ivm_immv_immvrelid -1 ] = ObjectIdGetDatum(viewOid); values[Anum_pg_ivm_immv_ispopulated -1 ] = BoolGetDatum(false); values[Anum_pg_ivm_immv_viewdef -1 ] = CStringGetTextDatum(querytree); isNulls[Anum_pg_ivm_immv_lastivmupdate -1 ] = true; pgIvmImmv = table_open(PgIvmImmvRelationId(), RowExclusiveLock); tupleDescriptor = RelationGetDescr(pgIvmImmv); heapTuple = heap_form_tuple(tupleDescriptor, values, isNulls); CatalogTupleInsert(pgIvmImmv, heapTuple); address.classId = RelationRelationId; address.objectId = viewOid; address.objectSubId = 0; recordDependencyOnExpr(&address, (Node *) viewQuery, NIL, DEPENDENCY_NORMAL); table_close(pgIvmImmv, NoLock); CommandCounterIncrement(); } #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM < 140000) /* * CreateTableAsRelExists --- check existence of relation for CreateTableAsStmt * * Utility wrapper checking if the relation pending for creation in this * CreateTableAsStmt query already exists or not. Returns true if the * relation exists, otherwise false. */ static bool CreateTableAsRelExists(CreateTableAsStmt *ctas) { Oid nspid; IntoClause *into = ctas->into; nspid = RangeVarGetCreationNamespace(into->rel); if (get_relname_relid(into->rel->relname, nspid)) { if (!ctas->if_not_exists) ereport(ERROR, (errcode(ERRCODE_DUPLICATE_TABLE), errmsg("relation \"%s\" already exists", into->rel->relname))); /* The relation exists and IF NOT EXISTS has been specified */ ereport(NOTICE, (errcode(ERRCODE_DUPLICATE_TABLE), errmsg("relation \"%s\" already exists, skipping", into->rel->relname))); return true; } /* Relation does not exist, it can be created */ return false; } #endif pg_ivm-1.13/expected/000077500000000000000000000000001507512346500145305ustar00rootroot00000000000000pg_ivm-1.13/expected/create_immv.out000066400000000000000000000050061507512346500175550ustar00rootroot00000000000000CREATE TABLE t (i int PRIMARY KEY); INSERT INTO t SELECT generate_series(1, 100); SELECT pgivm.create_immv('mv', 'SELECT * FROM t'); NOTICE: created index "mv_index" on immv "mv" create_immv ------------- 100 (1 row) SELECT pgivm.create_immv(' mv2 ( x ) ', 'SELECT * FROM t WHERE i%2 = 0'); NOTICE: created index "mv2_index" on immv "mv2" create_immv ------------- 50 (1 row) SELECT pgivm.create_immv('mv3', 'WITH d AS (DELETE FROM t RETURNING NULL) SELECT * FROM t'); ERROR: materialized views must not use data-modifying statements in WITH SELECT immvrelid, pgivm.get_immv_def(immvrelid) FROM pgivm.pg_ivm_immv ORDER BY 1; immvrelid | get_immv_def -----------+----------------------- mv | SELECT i + | FROM t mv2 | SELECT i AS x + | FROM t + | WHERE ((i % 2) = 0) (2 rows) -- contain immv SELECT pgivm.create_immv('mv_in_immv01', 'SELECT i FROM mv'); ERROR: including IMMV in definition is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_in_immv02', 'SELECT t.i FROM t INNER JOIN mv2 ON t.i = mv2.x'); ERROR: including IMMV in definition is not supported on incrementally maintainable materialized view -- SQL other than SELECT SELECT pgivm.create_immv('mv_in_create', 'CREATE TABLE in_create(i int)'); ERROR: view definition must specify SELECT statement SELECT pgivm.create_immv('mv_in_insert', 'INSERT INTO t VALUES(10)'); ERROR: view definition must specify SELECT statement SELECT pgivm.create_immv('mv_in_update', 'UPDATE t SET i = 10'); ERROR: view definition must specify SELECT statement SELECT pgivm.create_immv('mv_in_delete', 'DELETE FROM t'); ERROR: view definition must specify SELECT statement SELECT pgivm.create_immv('mv_in_drop', 'DROP TABLE t'); ERROR: view definition must specify SELECT statement DROP TABLE t; ERROR: cannot drop table t because other objects depend on it DETAIL: table mv depends on table t table mv2 depends on table t HINT: Use DROP ... CASCADE to drop the dependent objects too. DROP TABLE mv; SELECT immvrelid, pgivm.get_immv_def(immvrelid) FROM pgivm.pg_ivm_immv ORDER BY 1; immvrelid | get_immv_def -----------+----------------------- mv2 | SELECT i AS x + | FROM t + | WHERE ((i % 2) = 0) (1 row) DROP TABLE mv2; SELECT immvrelid, pgivm.get_immv_def(immvrelid) FROM pgivm.pg_ivm_immv ORDER BY 1; immvrelid | get_immv_def -----------+-------------- (0 rows) DROP TABLE t; pg_ivm-1.13/expected/create_insert.out000066400000000000000000000201151507512346500201070ustar00rootroot00000000000000Parsed test spec with 2 sessions starting permutation: s1 create s2 insert c1 check2 c2 mv step s1: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step s2: SELECT; step insert: INSERT INTO a VALUES (2); step c1: COMMIT; step insert: <... completed> step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 create s2 c1 insert check2 c2 mv step s1: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step s2: SELECT; step c1: COMMIT; step insert: INSERT INTO a VALUES (2); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 create insert c1 check2 c2 mv step s1: SELECT; step s2: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step insert: INSERT INTO a VALUES (2); step c1: COMMIT; step insert: <... completed> step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert create c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step insert: INSERT INTO a VALUES (2); step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. step create: <... completed> create_immv ----------- 2 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 create c1 insert check2 c2 mv step s1: SELECT; step s2: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step c1: COMMIT; step insert: INSERT INTO a VALUES (2); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 insert s1 create c2 check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. step create: <... completed> create_immv ----------- 2 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 insert s1 c2 create check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 2 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 s1 insert c2 create check1 c1 mv step s2: SELECT; step s1: SELECT; step insert: INSERT INTO a VALUES (2); step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 2 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) pg_ivm-1.13/expected/create_insert2.out000066400000000000000000000246311507512346500202000ustar00rootroot00000000000000Parsed test spec with 2 sessions starting permutation: s1 create s2 insert c1 check2 c2 mv step s1: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step s2: SELECT; step insert: INSERT INTO a VALUES (2); step c1: COMMIT; step insert: <... completed> step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 create s2 c1 insert check2 c2 mv step s1: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step s2: SELECT; step c1: COMMIT; step insert: INSERT INTO a VALUES (2); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 create insert c1 check2 c2 mv step s1: SELECT; step s2: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step insert: INSERT INTO a VALUES (2); step c1: COMMIT; step insert: <... completed> step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert create c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step insert: INSERT INTO a VALUES (2); step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: <... completed> create_immv ----------- 1 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ng (1 row) starting permutation: s1 s2 create c1 insert check2 c2 mv step s1: SELECT; step s2: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step c1: COMMIT; step insert: INSERT INTO a VALUES (2); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 insert s1 create c2 check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: <... completed> create_immv ----------- 1 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ng (1 row) starting permutation: s2 insert s1 c2 create check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ng (1 row) starting permutation: s2 s1 insert c2 create check1 c1 mv step s2: SELECT; step s1: SELECT; step insert: INSERT INTO a VALUES (2); step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ng (1 row) pg_ivm-1.13/expected/create_insert3.out000066400000000000000000000256511507512346500202040ustar00rootroot00000000000000Parsed test spec with 2 sessions starting permutation: s1 create s2 insert c1 check2 c2 mv step s1: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step s2: SELECT; step insert: INSERT INTO a VALUES (2); step c1: COMMIT; step insert: <... completed> ERROR: could not serialize access due to read/write dependencies among transactions step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ok (1 row) starting permutation: s1 create s2 c1 insert check2 c2 mv step s1: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step s2: SELECT; step c1: COMMIT; step insert: INSERT INTO a VALUES (2); ERROR: could not serialize access due to read/write dependencies among transactions step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 create insert c1 check2 c2 mv step s1: SELECT; step s2: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step insert: INSERT INTO a VALUES (2); step c1: COMMIT; step insert: <... completed> ERROR: could not serialize access due to read/write dependencies among transactions step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 insert create c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step insert: INSERT INTO a VALUES (2); step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: <... completed> create_immv ----------- 1 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ng (1 row) starting permutation: s1 s2 create c1 insert check2 c2 mv step s1: SELECT; step s2: SELECT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step c1: COMMIT; step insert: INSERT INTO a VALUES (2); ERROR: could not serialize access due to read/write dependencies among transactions step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ok (1 row) starting permutation: s2 insert s1 create c2 check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: <... completed> create_immv ----------- 1 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ng (1 row) starting permutation: s2 insert s1 c2 create check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ng (1 row) starting permutation: s2 s1 insert c2 create check1 c1 mv step s2: SELECT; step s1: SELECT; step insert: INSERT INTO a VALUES (2); step c2: COMMIT; tx1: NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. tx1: WARNING: inconsistent view can be created in isolation level SERIALIZABLE or REPEATABLE READ DETAIL: The view may not include effects of a concurrent transaction. HINT: create_immv should be used in isolation level READ COMMITTED, or execute refresh_immv to make sure the view is consistent. step create: SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; create_immv ----------- 1 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ng (1 row) pg_ivm-1.13/expected/insert_insert.out000066400000000000000000000206311507512346500201530ustar00rootroot00000000000000Parsed test spec with 2 sessions starting permutation: s1 update1 s2 update2 c1 check2 c2 mv step s1: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step c1: COMMIT; step update2: <... completed> step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|111 (1 row) check_mv -------- ok (1 row) starting permutation: s1 update1 s2 c1 update2 check2 c2 mv step s1: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step s2: SELECT; step c1: COMMIT; step update2: UPDATE b SET j = 111 WHERE i = 1; step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|111 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 update1 update2 c1 check2 c2 mv step s1: SELECT; step s2: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step update2: UPDATE b SET j = 111 WHERE i = 1; step c1: COMMIT; step update2: <... completed> step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|111 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 update2 update1 c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step update1: UPDATE a SET j = 11 WHERE i = 1; step c2: COMMIT; step update1: <... completed> step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|111 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 update1 c1 update2 check2 c2 mv step s1: SELECT; step s2: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step c1: COMMIT; step update2: UPDATE b SET j = 111 WHERE i = 1; step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|111 (1 row) check_mv -------- ok (1 row) starting permutation: s2 update2 s1 update1 c2 check1 c1 mv step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step s1: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step c2: COMMIT; step update1: <... completed> step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|111 (1 row) check_mv -------- ok (1 row) starting permutation: s2 update2 s1 c2 update1 check1 c1 mv step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step s1: SELECT; step c2: COMMIT; step update1: UPDATE a SET j = 11 WHERE i = 1; step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|111 (1 row) check_mv -------- ok (1 row) starting permutation: s2 s1 update2 c2 update1 check1 c1 mv step s2: SELECT; step s1: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step c2: COMMIT; step update1: UPDATE a SET j = 11 WHERE i = 1; step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|111 (1 row) check_mv -------- ok (1 row) starting permutation: s1 insert1 s2 insert2 c1 check2 c2 mv step s1: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step c1: COMMIT; step insert2: <... completed> step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 20|20|200 20|20|201 20|21|200 20|21|201 21|20|200 21|20|201 21|21|200 21|21|201 (12 rows) check_mv -------- ok (1 row) starting permutation: s1 insert1 s2 c1 insert2 check2 c2 mv step s1: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step s2: SELECT; step c1: COMMIT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 20|20|200 20|20|201 20|21|200 20|21|201 21|20|200 21|20|201 21|21|200 21|21|201 (12 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert1 insert2 c1 check2 c2 mv step s1: SELECT; step s2: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step c1: COMMIT; step insert2: <... completed> step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 20|20|200 20|20|201 20|21|200 20|21|201 21|20|200 21|20|201 21|21|200 21|21|201 (12 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert2 insert1 c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step c2: COMMIT; step insert1: <... completed> step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 20|20|200 20|20|201 20|21|200 20|21|201 21|20|200 21|20|201 21|21|200 21|21|201 (12 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert1 c1 insert2 check2 c2 mv step s1: SELECT; step s2: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step c1: COMMIT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 20|20|200 20|20|201 20|21|200 20|21|201 21|20|200 21|20|201 21|21|200 21|21|201 (12 rows) check_mv -------- ok (1 row) starting permutation: s2 insert2 s1 insert1 c2 check1 c1 mv step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step s1: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step c2: COMMIT; step insert1: <... completed> step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 20|20|200 20|20|201 20|21|200 20|21|201 21|20|200 21|20|201 21|21|200 21|21|201 (12 rows) check_mv -------- ok (1 row) starting permutation: s2 insert2 s1 c2 insert1 check1 c1 mv step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step s1: SELECT; step c2: COMMIT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 20|20|200 20|20|201 20|21|200 20|21|201 21|20|200 21|20|201 21|21|200 21|21|201 (12 rows) check_mv -------- ok (1 row) starting permutation: s2 s1 insert2 c2 insert1 check1 c1 mv step s2: SELECT; step s1: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step c2: COMMIT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 20|20|200 20|20|201 20|21|200 20|21|201 21|20|200 21|20|201 21|21|200 21|21|201 (12 rows) check_mv -------- ok (1 row) pg_ivm-1.13/expected/insert_insert2.out000066400000000000000000000230111507512346500202300ustar00rootroot00000000000000Parsed test spec with 2 sessions starting permutation: s1 update1 s2 update2 c1 check2 c2 mv step s1: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|100 (1 row) check_mv -------- ok (1 row) starting permutation: s1 update1 s2 c1 update2 check2 c2 mv step s1: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step s2: SELECT; step c1: COMMIT; step update2: UPDATE b SET j = 111 WHERE i = 1; ERROR: the materialized view is incrementally updated in concurrent transaction step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|100 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 update1 update2 c1 check2 c2 mv step s1: SELECT; step s2: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step update2: UPDATE b SET j = 111 WHERE i = 1; ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|100 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 update2 update1 c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step update1: UPDATE a SET j = 11 WHERE i = 1; ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c2: COMMIT; step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|111 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 update1 c1 update2 check2 c2 mv step s1: SELECT; step s2: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step c1: COMMIT; step update2: UPDATE b SET j = 111 WHERE i = 1; ERROR: the materialized view is incrementally updated in concurrent transaction step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|100 (1 row) check_mv -------- ok (1 row) starting permutation: s2 update2 s1 update1 c2 check1 c1 mv step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step s1: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c2: COMMIT; step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|111 (1 row) check_mv -------- ok (1 row) starting permutation: s2 update2 s1 c2 update1 check1 c1 mv step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step s1: SELECT; step c2: COMMIT; step update1: UPDATE a SET j = 11 WHERE i = 1; ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|111 (1 row) check_mv -------- ok (1 row) starting permutation: s2 s1 update2 c2 update1 check1 c1 mv step s2: SELECT; step s1: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step c2: COMMIT; step update1: UPDATE a SET j = 11 WHERE i = 1; ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|111 (1 row) check_mv -------- ok (1 row) starting permutation: s1 insert1 s2 insert2 c1 check2 c2 mv step s1: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 20|20|200 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 insert1 s2 c1 insert2 check2 c2 mv step s1: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step s2: SELECT; step c1: COMMIT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); ERROR: the materialized view is incrementally updated in concurrent transaction step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 20|20|200 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert1 insert2 c1 check2 c2 mv step s1: SELECT; step s2: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 20|20|200 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert2 insert1 c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c2: COMMIT; step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 21|21|201 (5 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert1 c1 insert2 check2 c2 mv step s1: SELECT; step s2: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step c1: COMMIT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); ERROR: the materialized view is incrementally updated in concurrent transaction step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 20|20|200 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 insert2 s1 insert1 c2 check1 c1 mv step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step s1: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c2: COMMIT; step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 21|21|201 (5 rows) check_mv -------- ok (1 row) starting permutation: s2 insert2 s1 c2 insert1 check1 c1 mv step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step s1: SELECT; step c2: COMMIT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 21|21|201 (5 rows) check_mv -------- ok (1 row) starting permutation: s2 s1 insert2 c2 insert1 check1 c1 mv step s2: SELECT; step s1: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step c2: COMMIT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 21|21|201 (5 rows) check_mv -------- ok (1 row) pg_ivm-1.13/expected/insert_insert3.out000066400000000000000000000230111507512346500202310ustar00rootroot00000000000000Parsed test spec with 2 sessions starting permutation: s1 update1 s2 update2 c1 check2 c2 mv step s1: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|100 (1 row) check_mv -------- ok (1 row) starting permutation: s1 update1 s2 c1 update2 check2 c2 mv step s1: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step s2: SELECT; step c1: COMMIT; step update2: UPDATE b SET j = 111 WHERE i = 1; ERROR: the materialized view is incrementally updated in concurrent transaction step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|100 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 update1 update2 c1 check2 c2 mv step s1: SELECT; step s2: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step update2: UPDATE b SET j = 111 WHERE i = 1; ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|100 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 update2 update1 c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step update1: UPDATE a SET j = 11 WHERE i = 1; ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c2: COMMIT; step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|111 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 update1 c1 update2 check2 c2 mv step s1: SELECT; step s2: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; step c1: COMMIT; step update2: UPDATE b SET j = 111 WHERE i = 1; ERROR: the materialized view is incrementally updated in concurrent transaction step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 11|11|100 (1 row) check_mv -------- ok (1 row) starting permutation: s2 update2 s1 update1 c2 check1 c1 mv step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step s1: SELECT; step update1: UPDATE a SET j = 11 WHERE i = 1; ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c2: COMMIT; step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|111 (1 row) check_mv -------- ok (1 row) starting permutation: s2 update2 s1 c2 update1 check1 c1 mv step s2: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step s1: SELECT; step c2: COMMIT; step update1: UPDATE a SET j = 11 WHERE i = 1; ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|111 (1 row) check_mv -------- ok (1 row) starting permutation: s2 s1 update2 c2 update1 check1 c1 mv step s2: SELECT; step s1: SELECT; step update2: UPDATE b SET j = 111 WHERE i = 1; step c2: COMMIT; step update1: UPDATE a SET j = 11 WHERE i = 1; ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|111 (1 row) check_mv -------- ok (1 row) starting permutation: s1 insert1 s2 insert2 c1 check2 c2 mv step s1: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 20|20|200 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 insert1 s2 c1 insert2 check2 c2 mv step s1: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step s2: SELECT; step c1: COMMIT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); ERROR: the materialized view is incrementally updated in concurrent transaction step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 20|20|200 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert1 insert2 c1 check2 c2 mv step s1: SELECT; step s2: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 20|20|200 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert2 insert1 c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c2: COMMIT; step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 21|21|201 (5 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert1 c1 insert2 check2 c2 mv step s1: SELECT; step s2: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); step c1: COMMIT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); ERROR: the materialized view is incrementally updated in concurrent transaction step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 20|20|200 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 insert2 s1 insert1 c2 check1 c1 mv step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step s1: SELECT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c2: COMMIT; step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 21|21|201 (5 rows) check_mv -------- ok (1 row) starting permutation: s2 insert2 s1 c2 insert1 check1 c1 mv step s2: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step s1: SELECT; step c2: COMMIT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 21|21|201 (5 rows) check_mv -------- ok (1 row) starting permutation: s2 s1 insert2 c2 insert1 check1 c1 mv step s2: SELECT; step s1: SELECT; step insert2: INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); step c2: COMMIT; step insert1: INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); x| y| z --+--+--- 10|10|100 10|11|100 11|10|100 11|11|100 21|21|201 (5 rows) check_mv -------- ok (1 row) pg_ivm-1.13/expected/pg_ivm.out000066400000000000000000001765101507512346500165540ustar00rootroot00000000000000CREATE EXTENSION pg_ivm; GRANT ALL ON SCHEMA public TO public; -- create a table to use as a basis for views and materialized views in various combinations CREATE TABLE mv_base_a (x int, i int, y int, j int); CREATE TABLE mv_base_b (x int, i int, y int, k int); -- test for base tables with dropped columns ALTER TABLE mv_base_a DROP COLUMN x; ALTER TABLE mv_base_a DROP COLUMN y; ALTER TABLE mv_base_b DROP COLUMN x; ALTER TABLE mv_base_b DROP COLUMN y; INSERT INTO mv_base_a VALUES (1,10), (2,20), (3,30), (4,40), (5,50); INSERT INTO mv_base_b VALUES (1,101), (2,102), (3,103), (4,104); SELECT pgivm.create_immv('mv_ivm_1', 'SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i)'); NOTICE: could not create an index on immv "mv_ivm_1" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 10 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 (4 rows) -- immediate maintenance BEGIN; INSERT INTO mv_base_b VALUES(5,105); SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 10 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 5 | 50 | 105 (5 rows) UPDATE mv_base_a SET j = 0 WHERE i = 1; SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 0 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 5 | 50 | 105 (5 rows) DELETE FROM mv_base_b WHERE (i,k) = (5,105); SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 0 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 (4 rows) ROLLBACK; SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 10 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 (4 rows) -- test for renaming column name to camel style BEGIN; ALTER TABLE mv_base_a RENAME i TO "I"; ALTER TABLE mv_base_a RENAME j TO "J"; UPDATE mv_base_a SET "J" = 0 WHERE "I" = 1; SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 0 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 (4 rows) ROLLBACK; -- TRUNCATE a base table in join views BEGIN; TRUNCATE mv_base_a; SELECT * FROM mv_ivm_1; i | j | k ---+---+--- (0 rows) ROLLBACK; BEGIN; TRUNCATE mv_base_b; SELECT * FROM mv_ivm_1; i | j | k ---+---+--- (0 rows) ROLLBACK; -- some query syntax BEGIN; CREATE FUNCTION ivm_func() RETURNS int LANGUAGE 'sql' AS 'SELECT 1' IMMUTABLE; SELECT pgivm.create_immv('mv_ivm_func', 'SELECT * FROM ivm_func()'); NOTICE: could not create an index on immv "mv_ivm_func" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 1 (1 row) SELECT pgivm.create_immv('mv_ivm_no_tbl', 'SELECT 1'); NOTICE: could not create an index on immv "mv_ivm_no_tbl" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 1 (1 row) ROLLBACK; -- result of materialized view have DISTINCT clause or the duplicate result. BEGIN; SELECT pgivm.create_immv('mv_ivm_duplicate', 'SELECT j FROM mv_base_a'); NOTICE: could not create an index on immv "mv_ivm_duplicate" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 5 (1 row) SELECT pgivm.create_immv('mv_ivm_distinct', 'SELECT DISTINCT j FROM mv_base_a'); NOTICE: created index "mv_ivm_distinct_index" on immv "mv_ivm_distinct" create_immv ------------- 5 (1 row) INSERT INTO mv_base_a VALUES(6,20); SELECT * FROM mv_ivm_duplicate ORDER BY 1; j ---- 10 20 20 30 40 50 (6 rows) SELECT * FROM mv_ivm_distinct ORDER BY 1; j | __ivm_count__ ----+--------------- 10 | 1 20 | 2 30 | 1 40 | 1 50 | 1 (5 rows) DELETE FROM mv_base_a WHERE (i,j) = (2,20); SELECT * FROM mv_ivm_duplicate ORDER BY 1; j ---- 10 20 30 40 50 (5 rows) SELECT * FROM mv_ivm_distinct ORDER BY 1; j | __ivm_count__ ----+--------------- 10 | 1 20 | 1 30 | 1 40 | 1 50 | 1 (5 rows) ROLLBACK; -- support SUM(), COUNT() and AVG() aggregate functions BEGIN; SELECT pgivm.create_immv('mv_ivm_agg', 'SELECT i, SUM(j), COUNT(i), AVG(j) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_agg_index" on immv "mv_ivm_agg" create_immv ------------- 5 (1 row) SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 20 | 1 | 20.0000000000000000 | 1 | 1 | 20 | 1 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) INSERT INTO mv_base_a VALUES(2,100); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 120 | 2 | 60.0000000000000000 | 2 | 2 | 120 | 2 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) UPDATE mv_base_a SET j = 200 WHERE (i,j) = (2,100); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+----------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 220 | 2 | 110.0000000000000000 | 2 | 2 | 220 | 2 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) DELETE FROM mv_base_a WHERE (i,j) = (2,200); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 20 | 1 | 20.0000000000000000 | 1 | 1 | 20 | 1 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) ROLLBACK; -- support COUNT(*) aggregate function BEGIN; SELECT pgivm.create_immv('mv_ivm_agg', 'SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_agg_index" on immv "mv_ivm_agg" create_immv ------------- 5 (1 row) SELECT * FROM mv_ivm_agg ORDER BY 1,2,3; i | sum | count | __ivm_count_sum__ | __ivm_count__ ---+-----+-------+-------------------+--------------- 1 | 10 | 1 | 1 | 1 2 | 20 | 1 | 1 | 1 3 | 30 | 1 | 1 | 1 4 | 40 | 1 | 1 | 1 5 | 50 | 1 | 1 | 1 (5 rows) INSERT INTO mv_base_a VALUES(2,100); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3; i | sum | count | __ivm_count_sum__ | __ivm_count__ ---+-----+-------+-------------------+--------------- 1 | 10 | 1 | 1 | 1 2 | 120 | 2 | 2 | 2 3 | 30 | 1 | 1 | 1 4 | 40 | 1 | 1 | 1 5 | 50 | 1 | 1 | 1 (5 rows) ROLLBACK; -- TRUNCATE a base table in aggregate views BEGIN; SELECT pgivm.create_immv('mv_ivm_agg', 'SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_agg_index" on immv "mv_ivm_agg" create_immv ------------- 5 (1 row) TRUNCATE mv_base_a; SELECT sum, count FROM mv_ivm_agg; sum | count -----+------- (0 rows) SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i; i | sum | count ---+-----+------- (0 rows) ROLLBACK; -- support aggregate functions without GROUP clause BEGIN; SELECT pgivm.create_immv('mv_ivm_group', 'SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a'); create_immv ------------- 1 (1 row) SELECT * FROM mv_ivm_group ORDER BY 1; sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ -----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 150 | 5 | 30.0000000000000000 | 5 | 5 | 150 | 5 (1 row) INSERT INTO mv_base_a VALUES(6,60); SELECT * FROM mv_ivm_group ORDER BY 1; sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ -----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 210 | 6 | 35.0000000000000000 | 6 | 6 | 210 | 6 (1 row) DELETE FROM mv_base_a; SELECT * FROM mv_ivm_group ORDER BY 1; sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ -----+-------+-----+-------------------+-------------------+-----------------+--------------- | 0 | | 0 | 0 | | 0 (1 row) ROLLBACK; -- TRUNCATE a base table in aggregate views without GROUP clause BEGIN; SELECT pgivm.create_immv('mv_ivm_group', 'SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a'); create_immv ------------- 1 (1 row) TRUNCATE mv_base_a; SELECT sum, count, avg FROM mv_ivm_group; sum | count | avg -----+-------+----- | 0 | (1 row) SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a; sum | count | avg -----+-------+----- | 0 | (1 row) ROLLBACK; -- resolved issue: When use AVG() function and values is indivisible, result of AVG() is incorrect. BEGIN; SELECT pgivm.create_immv('mv_ivm_avg_bug', 'SELECT i, SUM(j), COUNT(j), AVG(j) FROM mv_base_A GROUP BY i'); NOTICE: created index "mv_ivm_avg_bug_index" on immv "mv_ivm_avg_bug" create_immv ------------- 5 (1 row) SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 20 | 1 | 20.0000000000000000 | 1 | 1 | 20 | 1 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) INSERT INTO mv_base_a VALUES (1,0), (1,0), (2,30), (2,30); SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 3 | 3.3333333333333333 | 3 | 3 | 10 | 3 2 | 80 | 3 | 26.6666666666666667 | 3 | 3 | 80 | 3 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) DELETE FROM mv_base_a WHERE (i,j) = (1,0); DELETE FROM mv_base_a WHERE (i,j) = (2,30); SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 20 | 1 | 20.0000000000000000 | 1 | 1 | 20 | 1 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) ROLLBACK; -- support MIN(), MAX() aggregate functions BEGIN; SELECT pgivm.create_immv('mv_ivm_min_max(i, min_j, max_j)', 'SELECT i, MIN(j), MAX(j) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_min_max_index" on immv "mv_ivm_min_max" create_immv ------------- 5 (1 row) SELECT * FROM mv_ivm_min_max ORDER BY 1,2,3; i | min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ ---+-------+-------+---------------------+---------------------+--------------- 1 | 10 | 10 | 1 | 1 | 1 2 | 20 | 20 | 1 | 1 | 1 3 | 30 | 30 | 1 | 1 | 1 4 | 40 | 40 | 1 | 1 | 1 5 | 50 | 50 | 1 | 1 | 1 (5 rows) INSERT INTO mv_base_a VALUES (1,11), (1,12), (2,21), (2,22), (3,31), (3,32), (4,41), (4,42), (5,51), (5,52); SELECT * FROM mv_ivm_min_max ORDER BY 1,2,3; i | min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ ---+-------+-------+---------------------+---------------------+--------------- 1 | 10 | 12 | 3 | 3 | 3 2 | 20 | 22 | 3 | 3 | 3 3 | 30 | 32 | 3 | 3 | 3 4 | 40 | 42 | 3 | 3 | 3 5 | 50 | 52 | 3 | 3 | 3 (5 rows) DELETE FROM mv_base_a WHERE (i,j) IN ((1,10), (2,21), (3,32)); SELECT * FROM mv_ivm_min_max ORDER BY 1,2,3; i | min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ ---+-------+-------+---------------------+---------------------+--------------- 1 | 11 | 12 | 2 | 2 | 2 2 | 20 | 22 | 2 | 2 | 2 3 | 30 | 31 | 2 | 2 | 2 4 | 40 | 42 | 3 | 3 | 3 5 | 50 | 52 | 3 | 3 | 3 (5 rows) ROLLBACK; -- support MIN(), MAX() aggregate functions without GROUP clause BEGIN; SELECT pgivm.create_immv('mv_ivm_min_max(min_j, max_j)', 'SELECT MIN(j), MAX(j) FROM mv_base_a'); create_immv ------------- 1 (1 row) SELECT * FROM mv_ivm_min_max; min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ -------+-------+---------------------+---------------------+--------------- 10 | 50 | 5 | 5 | 5 (1 row) INSERT INTO mv_base_a VALUES (0,0), (6,60), (7,70); SELECT * FROM mv_ivm_min_max; min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ -------+-------+---------------------+---------------------+--------------- 0 | 70 | 8 | 8 | 8 (1 row) DELETE FROM mv_base_a WHERE (i,j) IN ((0,0), (7,70)); SELECT * FROM mv_ivm_min_max; min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ -------+-------+---------------------+---------------------+--------------- 10 | 60 | 6 | 6 | 6 (1 row) DELETE FROM mv_base_a; SELECT * FROM mv_ivm_min_max; min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ -------+-------+---------------------+---------------------+--------------- | | 0 | 0 | 0 (1 row) ROLLBACK; -- Test MIN/MAX after search_path change BEGIN; SELECT pgivm.create_immv('mv_ivm_min', 'SELECT MIN(j) FROM mv_base_a'); create_immv ------------- 1 (1 row) SELECT * FROM mv_ivm_min ORDER BY 1,2,3; min | __ivm_count_min__ | __ivm_count__ -----+-------------------+--------------- 10 | 5 | 5 (1 row) CREATE SCHEMA myschema; GRANT ALL ON SCHEMA myschema TO public; CREATE TABLE myschema.mv_base_a (j int); INSERT INTO myschema.mv_base_a VALUES (1); DELETE FROM mv_base_a WHERE (i,j) = (1,10); SELECT * FROM mv_ivm_min ORDER BY 1,2,3; min | __ivm_count_min__ | __ivm_count__ -----+-------------------+--------------- 20 | 4 | 4 (1 row) SET search_path TO myschema,public,pg_catalog; DELETE FROM public.mv_base_a WHERE (i,j) = (2,20); SELECT * FROM mv_ivm_min ORDER BY 1,2,3; min | __ivm_count_min__ | __ivm_count__ -----+-------------------+--------------- 30 | 3 | 3 (1 row) ROLLBACK; -- aggregate views with column names specified BEGIN; SELECT pgivm.create_immv('mv_ivm_agg(a)', 'SELECT i, SUM(j) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_agg_index" on immv "mv_ivm_agg" create_immv ------------- 5 (1 row) INSERT INTO mv_base_a VALUES (1,100), (2,200), (3,300); UPDATE mv_base_a SET j = 2000 WHERE (i,j) = (2,20); DELETE FROM mv_base_a WHERE (i,j) = (3,30); SELECT * FROM mv_ivm_agg ORDER BY 1,2; a | sum | __ivm_count_sum__ | __ivm_count__ ---+------+-------------------+--------------- 1 | 110 | 2 | 2 2 | 2200 | 2 | 2 3 | 300 | 1 | 1 4 | 40 | 1 | 1 5 | 50 | 1 | 1 (5 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_ivm_agg(a,b)', 'SELECT i, SUM(j) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_agg_index" on immv "mv_ivm_agg" create_immv ------------- 5 (1 row) INSERT INTO mv_base_a VALUES (1,100), (2,200), (3,300); UPDATE mv_base_a SET j = 2000 WHERE (i,j) = (2,20); DELETE FROM mv_base_a WHERE (i,j) = (3,30); SELECT * FROM mv_ivm_agg ORDER BY 1,2; a | b | __ivm_count_b__ | __ivm_count__ ---+------+-----------------+--------------- 1 | 110 | 2 | 2 2 | 2200 | 2 | 2 3 | 300 | 1 | 1 4 | 40 | 1 | 1 5 | 50 | 1 | 1 (5 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_ivm_agg(a,b,c)', 'SELECT i, SUM(j) FROM mv_base_a GROUP BY i'); ERROR: too many column names were specified ROLLBACK; -- support self join view and multiple change on the same table BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (1, 10), (2, 20), (3, 30); SELECT pgivm.create_immv('mv_self(v1, v2)', 'SELECT t1.v, t2.v FROM base_t AS t1 JOIN base_t AS t2 ON t1.i = t2.i'); NOTICE: could not create an index on immv "mv_self" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 3 (1 row) SELECT * FROM mv_self ORDER BY v1; v1 | v2 ----+---- 10 | 10 20 | 20 30 | 30 (3 rows) INSERT INTO base_t VALUES (4,40); DELETE FROM base_t WHERE i = 1; UPDATE base_t SET v = v*10 WHERE i=2; SELECT * FROM mv_self ORDER BY v1; v1 | v2 -----+----- 30 | 30 40 | 40 200 | 200 (3 rows) WITH ins_t1 AS (INSERT INTO base_t VALUES (5,50) RETURNING 1), ins_t2 AS (INSERT INTO base_t VALUES (6,60) RETURNING 1), upd_t AS (UPDATE base_t SET v = v + 100 RETURNING 1), dlt_t AS (DELETE FROM base_t WHERE i IN (4,5) RETURNING 1) SELECT NULL; ?column? ---------- (1 row) SELECT * FROM mv_self ORDER BY v1; v1 | v2 -----+----- 50 | 50 60 | 60 130 | 130 300 | 300 (4 rows) --- with sub-transactions SAVEPOINT p1; INSERT INTO base_t VALUES (7,70); RELEASE SAVEPOINT p1; INSERT INTO base_t VALUES (7,77); SELECT * FROM mv_self ORDER BY v1, v2; v1 | v2 -----+----- 50 | 50 60 | 60 70 | 70 70 | 77 77 | 70 77 | 77 130 | 130 300 | 300 (8 rows) ROLLBACK; -- support simultaneous table changes BEGIN; CREATE TABLE base_r (i int, v int); CREATE TABLE base_s (i int, v int); INSERT INTO base_r VALUES (1, 10), (2, 20), (3, 30); INSERT INTO base_s VALUES (1, 100), (2, 200), (3, 300); SELECT pgivm.create_immv('mv(v1, v2)', 'SELECT r.v, s.v FROM base_r AS r JOIN base_s AS s USING(i)');; NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 3 (1 row) SELECT * FROM mv ORDER BY v1; v1 | v2 ----+----- 10 | 100 20 | 200 30 | 300 (3 rows) WITH ins_r AS (INSERT INTO base_r VALUES (1,11) RETURNING 1), ins_r2 AS (INSERT INTO base_r VALUES (3,33) RETURNING 1), ins_s AS (INSERT INTO base_s VALUES (2,222) RETURNING 1), upd_r AS (UPDATE base_r SET v = v + 1000 WHERE i = 2 RETURNING 1), dlt_s AS (DELETE FROM base_s WHERE i = 3 RETURNING 1) SELECT NULL; ?column? ---------- (1 row) SELECT * FROM mv ORDER BY v1; v1 | v2 ------+----- 10 | 100 11 | 100 1020 | 200 1020 | 222 (4 rows) -- support foreign reference constraints BEGIN; WARNING: there is already a transaction in progress CREATE TABLE ri1 (i int PRIMARY KEY); CREATE TABLE ri2 (i int PRIMARY KEY REFERENCES ri1(i) ON UPDATE CASCADE ON DELETE CASCADE, v int); INSERT INTO ri1 VALUES (1),(2),(3); INSERT INTO ri2 VALUES (1),(2),(3); SELECT pgivm.create_immv('mv_ri(i1, i2)', 'SELECT ri1.i, ri2.i FROM ri1 JOIN ri2 USING(i)'); NOTICE: created index "mv_ri_index" on immv "mv_ri" create_immv ------------- 3 (1 row) SELECT * FROM mv_ri ORDER BY i1; i1 | i2 ----+---- 1 | 1 2 | 2 3 | 3 (3 rows) UPDATE ri1 SET i=10 where i=1; DELETE FROM ri1 WHERE i=2; SELECT * FROM mv_ri ORDER BY i2; i1 | i2 ----+---- 3 | 3 10 | 10 (2 rows) ROLLBACK; -- support subquery for using EXISTS() BEGIN; SELECT pgivm.create_immv('mv_ivm_exists_subquery', 'SELECT a.i, a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); NOTICE: could not create an index on immv "mv_ivm_exists_subquery" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) SELECT pgivm.create_immv('mv_ivm_exists_subquery2', 'SELECT a.i,a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) AND a.i > 2'); NOTICE: could not create an index on immv "mv_ivm_exists_subquery2" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 2 (1 row) SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+----+------------------------ 1 | 10 | 1 2 | 20 | 1 3 | 30 | 1 4 | 40 | 1 (4 rows) SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+----+------------------------ 3 | 30 | 1 4 | 40 | 1 (2 rows) INSERT INTO mv_base_a VALUES(1,10),(6,60),(3,30),(3,300); SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 1 | 10 | 1 1 | 10 | 1 2 | 20 | 1 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (7 rows) SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (4 rows) INSERT INTO mv_base_b VALUES(1,101); INSERT INTO mv_base_b VALUES(1,111); INSERT INTO mv_base_b VALUES(2,102); INSERT INTO mv_base_b VALUES(6,106); SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 1 | 10 | 3 1 | 10 | 3 2 | 20 | 2 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 6 | 60 | 1 (8 rows) SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 6 | 60 | 1 (5 rows) UPDATE mv_base_a SET i = 1 WHERE j =60; UPDATE mv_base_b SET i = 10 WHERE k = 101; UPDATE mv_base_b SET k = 1002 WHERE k = 102; SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 1 | 10 | 1 1 | 10 | 1 1 | 60 | 1 2 | 20 | 2 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (8 rows) SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (4 rows) DELETE FROM mv_base_a WHERE (i,j) = (1,60); DELETE FROM mv_base_b WHERE i = 2; SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 1 | 10 | 1 1 | 10 | 1 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (6 rows) SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (4 rows) --- EXISTS subquery with tuple duplication and DISTINCT SELECT pgivm.create_immv('mv_ivm_exists_subquery_distinct', 'SELECT DISTINCT a.i, a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); NOTICE: created index "mv_ivm_exists_subquery_distinct_index" on immv "mv_ivm_exists_subquery_distinct" create_immv ------------- 4 (1 row) DELETE FROM mv_base_b WHERE i = 1 or i = 3; INSERT INTO mv_base_b VALUES (1,100), (3,300); SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 1 | 10 | 1 1 | 10 | 1 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (6 rows) SELECT * FROM mv_ivm_exists_subquery_distinct ORDER BY i, j; i | j | __ivm_exists_count_0__ | __ivm_count__ ---+-----+------------------------+--------------- 1 | 10 | 1 | 2 3 | 30 | 1 | 2 3 | 300 | 1 | 1 4 | 40 | 1 | 1 (4 rows) ROLLBACK; -- support simple subquery in FROM clause BEGIN; SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a,( SELECT * FROM mv_base_b) b WHERE a.i = b.i'); NOTICE: could not create an index on immv "mv_ivm_subquery" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_ivm_subquery ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; -- disallow non-simple subqueries SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a, (SELECT i, COUNT(*) FROM mv_base_b GROUP BY i) b WHERE a.i = b.i'); ERROR: aggregate functions in nested query are not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a, (SELECT DISTINCT i FROM mv_base_b) b WHERE a.i = b.i'); ERROR: DISTINCT clause in nested query are not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT i,j FROM mv_base_a WHERE i IN (SELECT i FROM mv_base_b WHERE k < 103 )'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT i,j, (SELECT k FROM mv_base_b LIMIT 1) FROM mv_base_a a'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT i,j, (SELECT k FROM mv_base_b LIMIT 1) + 1 FROM mv_base_a a'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM generate_series(1, (SELECT k FROM mv_base_b LIMIT 1)) AS v'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: targetlist must contain vars that are referred to in EXISTS subquery SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) OR a.i > 2'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: OR or NOT conditions and EXISTS condition can not be used together SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_a a2 WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a2.i = b.i))'); ERROR: nested sublink is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT EXISTS(SELECT 1 from mv_base_b) FROM mv_base_a a'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT false OR EXISTS(SELECT 1 FROM mv_base_a) FROM mv_base_b'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: OR or NOT conditions and EXISTS condition can not be used together SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM generate_series(1, CASE EXISTS(SELECT 1 FROM mv_base_a) WHEN true THEN 100 ELSE 10 END), mv_base_b'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM mv_base_a a WHERE CASE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) WHEN true THEN false ELSE true END'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM mv_base_a a WHERE true and CASE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) WHEN true THEN false ELSE true END'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause -- support join subquery in FROM clause BEGIN; SELECT pgivm.create_immv('mv_ivm_join_subquery', 'SELECT i, j, k FROM ( SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN mv_base_a a USING(i)) tmp'); NOTICE: could not create an index on immv "mv_ivm_join_subquery" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; -- (1 row) SELECT * FROM mv_ivm_join_subquery ORDER BY i,j,k; i | j | k ---+----+----- 1 | 10 | 101 1 | 10 | 111 1 | 11 | 101 1 | 11 | 111 2 | 20 | 102 2 | 22 | 102 3 | 30 | 103 3 | 30 | 133 (8 rows) ROLLBACK; BEGIN; -- nested subquery SELECT pgivm.create_immv('mv_ivm_join_subquery', 'SELECT i, j, k FROM ( SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN (SELECT * FROM mv_base_a) a USING(i)) tmp'); NOTICE: could not create an index on immv "mv_ivm_join_subquery" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; -- (1 row) SELECT * FROM mv_ivm_join_subquery ORDER BY i,j,k; i | j | k ---+----+----- 1 | 10 | 101 1 | 10 | 111 1 | 11 | 101 1 | 11 | 111 2 | 20 | 102 2 | 22 | 102 3 | 30 | 103 3 | 30 | 133 (8 rows) ROLLBACK; -- support simple CTE BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH b AS ( SELECT * FROM mv_base_b) SELECT a.i,a.j FROM mv_base_a a, b WHERE a.i = b.i'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH a AS (SELECT * FROM mv_base_a), b AS ( SELECT * FROM mv_base_b) SELECT a.i,a.j FROM a, b WHERE a.i = b.i'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH b AS ( SELECT * FROM mv_base_b) SELECT v.i,v.j FROM (WITH a AS (SELECT * FROM mv_base_a) SELECT a.i,a.j FROM a, b WHERE a.i = b.i) v'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'SELECT * FROM (WITH a AS (SELECT * FROM mv_base_a), b AS ( SELECT * FROM mv_base_b) SELECT a.i,a.j FROM a, b WHERE a.i = b.i) v'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH b AS ( SELECT * FROM (SELECT * FROM mv_base_b) b2) SELECT v.i,v.j FROM (WITH a AS (SELECT * FROM mv_base_a) SELECT a.i,a.j FROM a, b WHERE a.i = b.i) v'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH x AS ( SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN mv_base_a a USING(i)) SELECT * FROM x'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; -- (1 row) SELECT * FROM mv_cte ORDER BY i,j,k; i | j | k ---+----+----- 1 | 10 | 101 1 | 10 | 111 1 | 11 | 101 1 | 11 | 111 2 | 20 | 102 2 | 22 | 102 3 | 30 | 103 3 | 30 | 133 (8 rows) ROLLBACK; -- nested CTE BEGIN; SELECT pgivm.create_immv('mv_ivm_nested_cte', 'WITH v AS ( WITH a AS (SELECT * FROM mv_base_a) SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN a USING(i)) SELECT * FROM v'); NOTICE: could not create an index on immv "mv_ivm_nested_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; -- (1 row) SELECT * FROM mv_ivm_nested_cte ORDER BY i,j,k; i | j | k ---+----+----- 1 | 10 | 101 1 | 10 | 111 1 | 11 | 101 1 | 11 | 111 2 | 20 | 102 2 | 22 | 102 3 | 30 | 103 3 | 30 | 133 (8 rows) ROLLBACK; -- Multiply-referenced CTE BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (1, 10), (2, 20), (3, 30); SELECT pgivm.create_immv('mv_cte_multi(v1, v2)', 'WITH t AS (SELECT * FROM base_t) SELECT t1.v, t2.v FROM t AS t1 JOIN t AS t2 ON t1.i = t2.i'); NOTICE: could not create an index on immv "mv_cte_multi" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 3 (1 row) SELECT * FROM mv_cte_multi ORDER BY v1; v1 | v2 ----+---- 10 | 10 20 | 20 30 | 30 (3 rows) INSERT INTO base_t VALUES (4,40); DELETE FROM base_t WHERE i = 1; UPDATE base_t SET v = v*10 WHERE i=2; SELECT * FROM mv_cte_multi ORDER BY v1; v1 | v2 -----+----- 30 | 30 40 | 40 200 | 200 (3 rows) WITH ins_t1 AS (INSERT INTO base_t VALUES (5,50) RETURNING 1), ins_t2 AS (INSERT INTO base_t VALUES (6,60) RETURNING 1), upd_t AS (UPDATE base_t SET v = v + 100 RETURNING 1), dlt_t AS (DELETE FROM base_t WHERE i IN (4,5) RETURNING 1) SELECT NULL; ?column? ---------- (1 row) SELECT * FROM mv_cte_multi ORDER BY v1; v1 | v2 -----+----- 50 | 50 60 | 60 130 | 130 300 | 300 (4 rows) ROLLBACK; --- disallow not-simple CTE SELECT pgivm.create_immv('mv_cte_fail', 'WITH b AS (SELECT i, COUNT(*) FROM mv_base_b GROUP BY i) SELECT a.i,a.j FROM mv_base_a a, b WHERE a.i = b.i'); ERROR: aggregate functions in nested query are not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_cte_fail', 'WITH b AS (SELECT DISTINCT i FROM mv_base_b) SELECT a.i,a.j FROM mv_base_a a, b WHERE a.i = b.i'); ERROR: DISTINCT clause in nested query are not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_cte_fail', 'WITH a AS (SELECT i, j FROM mv_base_a) SELECT a.i,a.j FROM a WHERE EXISTS(WITH b AS (SELECT i FROM mv_base_b) SELECT 1 FROM b WHERE a.i = b.i)'); ERROR: CTE in EXIST clause is not supported on incrementally maintainable materialized view -- unreferenced CTE SELECT pgivm.create_immv('mv_cte_fail', 'WITH b AS (SELECT * FROM mv_base_b) SELECT * FROM mv_base_a a'); ERROR: Ureferenced WITH query is not supported on incrementally maintainable materialized view -- views including NULL BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (1,10),(2, NULL); SELECT pgivm.create_immv('mv', 'SELECT * FROM base_t'); NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 2 (1 row) SELECT * FROM mv ORDER BY i; i | v ---+---- 1 | 10 2 | (2 rows) UPDATE base_t SET v = 20 WHERE i = 2; SELECT * FROM mv ORDER BY i; i | v ---+---- 1 | 10 2 | 20 (2 rows) ROLLBACK; BEGIN; CREATE TABLE base_t (i int); SELECT pgivm.create_immv('mv', 'SELECT * FROM base_t'); NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 0 (1 row) SELECT * FROM mv ORDER BY i; i --- (0 rows) INSERT INTO base_t VALUES (1),(NULL); SELECT * FROM mv ORDER BY i; i --- 1 (2 rows) ROLLBACK; BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (NULL, 1), (NULL, 2), (1, 10), (1, 20); SELECT pgivm.create_immv('mv', 'SELECT i, sum(v) FROM base_t GROUP BY i'); NOTICE: created index "mv_index" on immv "mv" create_immv ------------- 2 (1 row) SELECT * FROM mv ORDER BY i; i | sum | __ivm_count_sum__ | __ivm_count__ ---+-----+-------------------+--------------- 1 | 30 | 2 | 2 | 3 | 2 | 2 (2 rows) UPDATE base_t SET v = v * 10; SELECT * FROM mv ORDER BY i; i | sum | __ivm_count_sum__ | __ivm_count__ ---+-----+-------------------+--------------- 1 | 300 | 2 | 2 | 30 | 2 | 2 (2 rows) ROLLBACK; BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (NULL, 1), (NULL, 2), (NULL, 3), (NULL, 4), (NULL, 5); SELECT pgivm.create_immv('mv', 'SELECT i, min(v), max(v) FROM base_t GROUP BY i'); NOTICE: created index "mv_index" on immv "mv" create_immv ------------- 1 (1 row) SELECT * FROM mv ORDER BY i; i | min | max | __ivm_count_min__ | __ivm_count_max__ | __ivm_count__ ---+-----+-----+-------------------+-------------------+--------------- | 1 | 5 | 5 | 5 | 5 (1 row) DELETE FROM base_t WHERE v = 1; SELECT * FROM mv ORDER BY i; i | min | max | __ivm_count_min__ | __ivm_count_max__ | __ivm_count__ ---+-----+-----+-------------------+-------------------+--------------- | 2 | 5 | 4 | 4 | 4 (1 row) DELETE FROM base_t WHERE v = 3; SELECT * FROM mv ORDER BY i; i | min | max | __ivm_count_min__ | __ivm_count_max__ | __ivm_count__ ---+-----+-----+-------------------+-------------------+--------------- | 2 | 5 | 3 | 3 | 3 (1 row) DELETE FROM base_t WHERE v = 5; SELECT * FROM mv ORDER BY i; i | min | max | __ivm_count_min__ | __ivm_count_max__ | __ivm_count__ ---+-----+-----+-------------------+-------------------+--------------- | 2 | 4 | 2 | 2 | 2 (1 row) ROLLBACK; -- IMMV containing user defined type BEGIN; CREATE TYPE mytype; CREATE FUNCTION mytype_in(cstring) RETURNS mytype AS 'int4in' LANGUAGE INTERNAL STRICT IMMUTABLE; NOTICE: return type mytype is only a shell CREATE FUNCTION mytype_out(mytype) RETURNS cstring AS 'int4out' LANGUAGE INTERNAL STRICT IMMUTABLE; NOTICE: argument type mytype is only a shell LINE 1: CREATE FUNCTION mytype_out(mytype) ^ CREATE TYPE mytype ( LIKE = int4, INPUT = mytype_in, OUTPUT = mytype_out ); CREATE FUNCTION mytype_eq(mytype, mytype) RETURNS bool AS 'int4eq' LANGUAGE INTERNAL STRICT IMMUTABLE; CREATE FUNCTION mytype_lt(mytype, mytype) RETURNS bool AS 'int4lt' LANGUAGE INTERNAL STRICT IMMUTABLE; CREATE FUNCTION mytype_cmp(mytype, mytype) RETURNS integer AS 'btint4cmp' LANGUAGE INTERNAL STRICT IMMUTABLE; CREATE OPERATOR = ( leftarg = mytype, rightarg = mytype, procedure = mytype_eq); CREATE OPERATOR < ( leftarg = mytype, rightarg = mytype, procedure = mytype_lt); CREATE OPERATOR CLASS mytype_ops DEFAULT FOR TYPE mytype USING btree AS OPERATOR 1 <, OPERATOR 3 = , FUNCTION 1 mytype_cmp(mytype,mytype); CREATE TABLE t_mytype (x mytype); SELECT pgivm.create_immv('mv_mytype', 'SELECT * FROM t_mytype'); NOTICE: could not create an index on immv "mv_mytype" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 0 (1 row) INSERT INTO t_mytype VALUES ('1'::mytype); SELECT * FROM mv_mytype; x --- 1 (1 row) ROLLBACK; -- contain system column SELECT pgivm.create_immv('mv_ivm01', 'SELECT i,j,xmin FROM mv_base_a'); ERROR: system column is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm02', 'SELECT i,j FROM mv_base_a WHERE xmin = ''610'''); ERROR: system column is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm03', 'SELECT i,j,xmin::text AS x_min FROM mv_base_a'); ERROR: system column is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm04', 'SELECT i,j,xidsend(xmin) AS x_min FROM mv_base_a'); ERROR: system column is not supported on incrementally maintainable materialized view -- targetlist or WHERE clause without EXISTS contain subquery SELECT pgivm.create_immv('mv_ivm05', 'SELECT i,j FROM mv_base_a WHERE i IN (SELECT i FROM mv_base_b WHERE k < 103 )'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm05', 'SELECT i,j, (SELECT k FROM mv_base_b b WHERE a.i = b.i) FROM mv_base_a a'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause -- contain ORDER BY SELECT pgivm.create_immv('mv_ivm07', 'SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) ORDER BY i,j,k'); ERROR: ORDER BY clause is not supported on incrementally maintainable materialized view -- contain HAVING SELECT pgivm.create_immv('mv_ivm08', 'SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) GROUP BY i,j,k HAVING SUM(i) > 5'); ERROR: HAVING clause is not supported on incrementally maintainable materialized view -- contain GROUP BY without aggregate SELECT pgivm.create_immv('mv_ivm08', 'SELECT i,j FROM mv_base_a GROUP BY i,j'); ERROR: GROUP BY clause without aggregate is not supported on incrementally maintainable materialized view -- contain view or materialized view CREATE VIEW b_view AS SELECT i,k FROM mv_base_b; CREATE MATERIALIZED VIEW b_mview AS SELECT i,k FROM mv_base_b; SELECT pgivm.create_immv('mv_ivm07', 'SELECT a.i,a.j FROM mv_base_a a,b_view b WHERE a.i = b.i'); ERROR: VIEW or MATERIALIZED VIEW is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm08', 'SELECT a.i,a.j FROM mv_base_a a,b_mview b WHERE a.i = b.i'); ERROR: VIEW or MATERIALIZED VIEW is not supported on incrementally maintainable materialized view -- contain mutable functions SELECT pgivm.create_immv('mv_ivm12', 'SELECT i,j FROM mv_base_a WHERE i = random()::int'); ERROR: mutable function is not supported on incrementally maintainable materialized view HINT: functions must be marked IMMUTABLE -- LIMIT/OFFSET is not supported SELECT pgivm.create_immv('mv_ivm13', 'SELECT i,j FROM mv_base_a LIMIT 10 OFFSET 5'); ERROR: LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view -- DISTINCT ON is not supported SELECT pgivm.create_immv('mv_ivm14', 'SELECT DISTINCT ON(i) i, j FROM mv_base_a'); ERROR: DISTINCT ON is not supported on incrementally maintainable materialized view -- TABLESAMPLE clause is not supported SELECT pgivm.create_immv('mv_ivm15', 'SELECT i, j FROM mv_base_a TABLESAMPLE SYSTEM(50)'); ERROR: TABLESAMPLE clause is not supported on incrementally maintainable materialized view -- window functions are not supported SELECT pgivm.create_immv('mv_ivm16', 'SELECT i, j FROM (SELECT *, cume_dist() OVER (ORDER BY i) AS rank FROM mv_base_a) AS t'); ERROR: window functions are not supported on incrementally maintainable materialized view -- aggregate function with some options is not supported SELECT pgivm.create_immv('mv_ivm17', 'SELECT COUNT(*) FILTER(WHERE i < 3) FROM mv_base_a'); ERROR: aggregate function with FILTER clause is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm18', 'SELECT COUNT(DISTINCT i) FROM mv_base_a'); ERROR: aggregate function with DISTINCT arguments is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm19', 'SELECT array_agg(j ORDER BY i DESC) FROM mv_base_a'); ERROR: aggregate function with ORDER clause is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm20', 'SELECT i,SUM(j) FROM mv_base_a GROUP BY GROUPING SETS((i),())'); ERROR: GROUPING SETS, ROLLUP, or CUBE clauses is not supported on incrementally maintainable materialized view -- inheritance parent is not supported BEGIN; CREATE TABLE parent (i int, v int); CREATE TABLE child_a(options text) INHERITS(parent); SELECT pgivm.create_immv('mv_ivm21', 'SELECT * FROM parent'); ERROR: inheritance parent is not supported on incrementally maintainable materialized view ROLLBACK; -- UNION statement is not supported SELECT pgivm.create_immv('mv_ivm22', 'SELECT i,j FROM mv_base_a UNION ALL SELECT i,k FROM mv_base_b'); ERROR: UNION/INTERSECT/EXCEPT statements are not supported on incrementally maintainable materialized view -- DISTINCT clause in nested query are not supported SELECT pgivm.create_immv('mv_ivm23', 'SELECT * FROM (SELECT DISTINCT i,j FROM mv_base_a) AS tmp');; ERROR: DISTINCT clause in nested query are not supported on incrementally maintainable materialized view -- empty target list is not allowed with IVM SELECT pgivm.create_immv('mv_ivm25', 'SELECT FROM mv_base_a'); ERROR: empty target list is not supported on incrementally maintainable materialized view -- FOR UPDATE/SHARE is not supported SELECT pgivm.create_immv('mv_ivm26', 'SELECT i,j FROM mv_base_a FOR UPDATE'); ERROR: FOR UPDATE/SHARE clause is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm27', 'SELECT * FROM (SELECT i,j FROM mv_base_a FOR UPDATE) AS tmp;'); ERROR: FOR UPDATE/SHARE clause is not supported on incrementally maintainable materialized view -- tartget list cannot contain ivm column that start with '__ivm' SELECT pgivm.create_immv('mv_ivm28', 'SELECT i AS "__ivm_count__" FROM mv_base_a'); ERROR: column name __ivm_count__ is not supported on incrementally maintainable materialized view -- expressions specified in GROUP BY must appear in the target list. SELECT pgivm.create_immv('mv_ivm29', 'SELECT COUNT(i) FROM mv_base_a GROUP BY i;'); ERROR: GROUP BY expression not appearing in select list is not supported on incrementally maintainable materialized view -- experssions containing an aggregate is not supported SELECT pgivm.create_immv('mv_ivm30', 'SELECT sum(i)*0.5 FROM mv_base_a'); ERROR: expression containing an aggregate in it is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm31', 'SELECT sum(i)/sum(j) FROM mv_base_a'); ERROR: expression containing an aggregate in it is not supported on incrementally maintainable materialized view -- VALUES is not supported SELECT pgivm.create_immv('mv_ivm_only_values1', 'values(1)'); ERROR: VALUES is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm_only_values2', 'SELECT * FROM (values(1)) AS tmp'); ERROR: VALUES is not supported on incrementally maintainable materialized view -- column of parent query specified in EXISTS clause must appear in the target list. SELECT pgivm.create_immv('mv_ivm32', 'SELECT a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: targetlist must contain vars that are referred to in EXISTS subquery -- views containing base tables with Row Level Security DROP USER IF EXISTS regress_ivm_admin; NOTICE: role "regress_ivm_admin" does not exist, skipping DROP USER IF EXISTS regress_ivm_user; NOTICE: role "regress_ivm_user" does not exist, skipping CREATE USER regress_ivm_admin; CREATE USER regress_ivm_user; --- create a table with RLS SET SESSION AUTHORIZATION regress_ivm_admin; CREATE TABLE rls_tbl(id int, data text, owner name); INSERT INTO rls_tbl VALUES (1,'foo','regress_ivm_user'), (2,'bar','postgres'); CREATE TABLE num_tbl(id int, num text); INSERT INTO num_tbl VALUES (1,'one'), (2,'two'), (3,'three'), (4,'four'), (5,'five'), (6,'six'); --- Users can access only their own rows CREATE POLICY rls_tbl_policy ON rls_tbl FOR SELECT TO PUBLIC USING(owner = current_user); ALTER TABLE rls_tbl ENABLE ROW LEVEL SECURITY; GRANT ALL on rls_tbl TO PUBLIC; GRANT ALL on num_tbl TO PUBLIC; --- create a view owned by regress_ivm_user SET SESSION AUTHORIZATION regress_ivm_user; SELECT pgivm.create_immv('ivm_rls', 'SELECT * FROM rls_tbl'); NOTICE: could not create an index on immv "ivm_rls" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 1 (1 row) SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3; id | data | owner ----+------+------------------ 1 | foo | regress_ivm_user (1 row) RESET SESSION AUTHORIZATION; --- inserts rows owned by different users INSERT INTO rls_tbl VALUES (3,'baz','regress_ivm_user'), (4,'qux','postgres'); SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3; id | data | owner ----+------+------------------ 1 | foo | regress_ivm_user 3 | baz | regress_ivm_user (2 rows) --- combination of diffent kinds of commands WITH i AS (INSERT INTO rls_tbl VALUES(5,'quux','postgres'), (6,'corge','regress_ivm_user')), u AS (UPDATE rls_tbl SET owner = 'postgres' WHERE id = 1), u2 AS (UPDATE rls_tbl SET owner = 'regress_ivm_user' WHERE id = 2) SELECT; -- (1 row) SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3; id | data | owner ----+-------+------------------ 2 | bar | regress_ivm_user 3 | baz | regress_ivm_user 6 | corge | regress_ivm_user (3 rows) --- SET SESSION AUTHORIZATION regress_ivm_user; SELECT pgivm.create_immv('ivm_rls2', 'SELECT * FROM rls_tbl JOIN num_tbl USING(id)'); NOTICE: could not create an index on immv "ivm_rls2" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 3 (1 row) RESET SESSION AUTHORIZATION; WITH x AS (UPDATE rls_tbl SET data = data || '_2' where id in (3,4)), y AS (UPDATE num_tbl SET num = num || '_2' where id in (3,4)) SELECT; -- (1 row) SELECT * FROM ivm_rls2 ORDER BY 1,2,3; id | data | owner | num ----+-------+------------------+--------- 2 | bar | regress_ivm_user | two 3 | baz_2 | regress_ivm_user | three_2 6 | corge | regress_ivm_user | six (3 rows) DROP TABLE rls_tbl CASCADE; NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to table ivm_rls drop cascades to table ivm_rls2 DROP TABLE num_tbl CASCADE; DROP USER regress_ivm_user; DROP USER regress_ivm_admin; -- automatic index creation BEGIN; CREATE TABLE base_a (i int primary key, j int); CREATE TABLE base_b (i int primary key, j int); --- group by: create an index SELECT pgivm.create_immv('mv_idx1', 'SELECT i, sum(j) FROM base_a GROUP BY i'); NOTICE: created index "mv_idx1_index" on immv "mv_idx1" create_immv ------------- 0 (1 row) --- distinct: create an index SELECT pgivm.create_immv('mv_idx2', 'SELECT DISTINCT j FROM base_a'); NOTICE: created index "mv_idx2_index" on immv "mv_idx2" create_immv ------------- 0 (1 row) --- with all pkey columns: create an index SELECT pgivm.create_immv('mv_idx3(i_a, i_b)', 'SELECT a.i, b.i FROM base_a a, base_b b'); NOTICE: created index "mv_idx3_index" on immv "mv_idx3" create_immv ------------- 0 (1 row) --- missing some pkey columns: no index SELECT pgivm.create_immv('mv_idx4', 'SELECT j FROM base_a'); NOTICE: could not create an index on immv "mv_idx4" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 0 (1 row) SELECT pgivm.create_immv('mv_idx5', 'SELECT a.i, b.j FROM base_a a, base_b b'); NOTICE: could not create an index on immv "mv_idx5" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 0 (1 row) --- subqueries: create an index SELECT pgivm.create_immv('mv_idx6(i_a, i_b)', 'SELECT a.i, b.i FROM (SELECT * FROM base_a) a, (SELECT * FROM base_b) b'); NOTICE: created index "mv_idx6_index" on immv "mv_idx6" create_immv ------------- 0 (1 row) --- with set-returning function: no index SELECT pgivm.create_immv('mv_idx7', 'SELECT i FROM base_a, generate_series(1,10)'); NOTICE: could not create an index on immv "mv_idx7" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 0 (1 row) ROLLBACK; -- type that doesn't have default operator class for access method btree BEGIN; CREATE TABLE table_json (j json); SELECT pgivm.create_immv('mv_json', 'SELECT * from table_json'); ERROR: data type json has no default operator class for access method "btree" ROLLBACK; -- prevent IMMV chanages INSERT INTO mv_ivm_1 VALUES(1,1,1); ERROR: cannot change materialized view "mv_ivm_1" UPDATE mv_ivm_1 SET k = 1 WHERE i = 1; ERROR: cannot change materialized view "mv_ivm_1" DELETE FROM mv_ivm_1; ERROR: cannot change materialized view "mv_ivm_1" TRUNCATE mv_ivm_1; ERROR: cannot change materialized view "mv_ivm_1" -- get_immv_def function SELECT immvrelid, pgivm.get_immv_def(immvrelid) FROM pgivm.pg_ivm_immv ORDER BY 1; immvrelid | get_immv_def -----------+---------------------------------- mv_ivm_1 | SELECT a.i, + | a.j, + | b.k + | FROM (mv_base_a a + | JOIN mv_base_b b USING (i)) (1 row) -- mv_base_b is not immv SELECT 'mv_base_b'::regclass, pgivm.get_immv_def('mv_base_b'); regclass | get_immv_def -----------+-------------- mv_base_b | (1 row) DROP TABLE mv_base_b CASCADE; NOTICE: drop cascades to 3 other objects DETAIL: drop cascades to table mv_ivm_1 drop cascades to view b_view drop cascades to materialized view b_mview DROP TABLE mv_base_a CASCADE; pg_ivm-1.13/expected/pg_ivm_0.out000066400000000000000000001763701507512346500167770ustar00rootroot00000000000000CREATE EXTENSION pg_ivm; GRANT ALL ON SCHEMA public TO public; -- create a table to use as a basis for views and materialized views in various combinations CREATE TABLE mv_base_a (x int, i int, y int, j int); CREATE TABLE mv_base_b (x int, i int, y int, k int); -- test for base tables with dropped columns ALTER TABLE mv_base_a DROP COLUMN x; ALTER TABLE mv_base_a DROP COLUMN y; ALTER TABLE mv_base_b DROP COLUMN x; ALTER TABLE mv_base_b DROP COLUMN y; INSERT INTO mv_base_a VALUES (1,10), (2,20), (3,30), (4,40), (5,50); INSERT INTO mv_base_b VALUES (1,101), (2,102), (3,103), (4,104); SELECT pgivm.create_immv('mv_ivm_1', 'SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i)'); NOTICE: could not create an index on immv "mv_ivm_1" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 10 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 (4 rows) -- immediate maintenance BEGIN; INSERT INTO mv_base_b VALUES(5,105); SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 10 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 5 | 50 | 105 (5 rows) UPDATE mv_base_a SET j = 0 WHERE i = 1; SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 0 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 5 | 50 | 105 (5 rows) DELETE FROM mv_base_b WHERE (i,k) = (5,105); SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 0 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 (4 rows) ROLLBACK; SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 10 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 (4 rows) -- test for renaming column name to camel style BEGIN; ALTER TABLE mv_base_a RENAME i TO "I"; ALTER TABLE mv_base_a RENAME j TO "J"; UPDATE mv_base_a SET "J" = 0 WHERE "I" = 1; SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; i | j | k ---+----+----- 1 | 0 | 101 2 | 20 | 102 3 | 30 | 103 4 | 40 | 104 (4 rows) ROLLBACK; -- TRUNCATE a base table in join views BEGIN; TRUNCATE mv_base_a; SELECT * FROM mv_ivm_1; i | j | k ---+---+--- (0 rows) ROLLBACK; BEGIN; TRUNCATE mv_base_b; SELECT * FROM mv_ivm_1; i | j | k ---+---+--- (0 rows) ROLLBACK; -- some query syntax BEGIN; CREATE FUNCTION ivm_func() RETURNS int LANGUAGE 'sql' AS 'SELECT 1' IMMUTABLE; SELECT pgivm.create_immv('mv_ivm_func', 'SELECT * FROM ivm_func()'); NOTICE: could not create an index on immv "mv_ivm_func" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 1 (1 row) SELECT pgivm.create_immv('mv_ivm_no_tbl', 'SELECT 1'); NOTICE: could not create an index on immv "mv_ivm_no_tbl" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 1 (1 row) ROLLBACK; -- result of materialized view have DISTINCT clause or the duplicate result. BEGIN; SELECT pgivm.create_immv('mv_ivm_duplicate', 'SELECT j FROM mv_base_a'); NOTICE: could not create an index on immv "mv_ivm_duplicate" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 5 (1 row) SELECT pgivm.create_immv('mv_ivm_distinct', 'SELECT DISTINCT j FROM mv_base_a'); NOTICE: created index "mv_ivm_distinct_index" on immv "mv_ivm_distinct" create_immv ------------- 5 (1 row) INSERT INTO mv_base_a VALUES(6,20); SELECT * FROM mv_ivm_duplicate ORDER BY 1; j ---- 10 20 20 30 40 50 (6 rows) SELECT * FROM mv_ivm_distinct ORDER BY 1; j | __ivm_count__ ----+--------------- 10 | 1 20 | 2 30 | 1 40 | 1 50 | 1 (5 rows) DELETE FROM mv_base_a WHERE (i,j) = (2,20); SELECT * FROM mv_ivm_duplicate ORDER BY 1; j ---- 10 20 30 40 50 (5 rows) SELECT * FROM mv_ivm_distinct ORDER BY 1; j | __ivm_count__ ----+--------------- 10 | 1 20 | 1 30 | 1 40 | 1 50 | 1 (5 rows) ROLLBACK; -- support SUM(), COUNT() and AVG() aggregate functions BEGIN; SELECT pgivm.create_immv('mv_ivm_agg', 'SELECT i, SUM(j), COUNT(i), AVG(j) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_agg_index" on immv "mv_ivm_agg" create_immv ------------- 5 (1 row) SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 20 | 1 | 20.0000000000000000 | 1 | 1 | 20 | 1 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) INSERT INTO mv_base_a VALUES(2,100); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 120 | 2 | 60.0000000000000000 | 2 | 2 | 120 | 2 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) UPDATE mv_base_a SET j = 200 WHERE (i,j) = (2,100); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+----------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 220 | 2 | 110.0000000000000000 | 2 | 2 | 220 | 2 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) DELETE FROM mv_base_a WHERE (i,j) = (2,200); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 20 | 1 | 20.0000000000000000 | 1 | 1 | 20 | 1 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) ROLLBACK; -- support COUNT(*) aggregate function BEGIN; SELECT pgivm.create_immv('mv_ivm_agg', 'SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_agg_index" on immv "mv_ivm_agg" create_immv ------------- 5 (1 row) SELECT * FROM mv_ivm_agg ORDER BY 1,2,3; i | sum | count | __ivm_count_sum__ | __ivm_count__ ---+-----+-------+-------------------+--------------- 1 | 10 | 1 | 1 | 1 2 | 20 | 1 | 1 | 1 3 | 30 | 1 | 1 | 1 4 | 40 | 1 | 1 | 1 5 | 50 | 1 | 1 | 1 (5 rows) INSERT INTO mv_base_a VALUES(2,100); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3; i | sum | count | __ivm_count_sum__ | __ivm_count__ ---+-----+-------+-------------------+--------------- 1 | 10 | 1 | 1 | 1 2 | 120 | 2 | 2 | 2 3 | 30 | 1 | 1 | 1 4 | 40 | 1 | 1 | 1 5 | 50 | 1 | 1 | 1 (5 rows) ROLLBACK; -- TRUNCATE a base table in aggregate views BEGIN; SELECT pgivm.create_immv('mv_ivm_agg', 'SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_agg_index" on immv "mv_ivm_agg" create_immv ------------- 5 (1 row) TRUNCATE mv_base_a; SELECT sum, count FROM mv_ivm_agg; sum | count -----+------- (0 rows) SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i; i | sum | count ---+-----+------- (0 rows) ROLLBACK; -- support aggregate functions without GROUP clause BEGIN; SELECT pgivm.create_immv('mv_ivm_group', 'SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a'); create_immv ------------- 1 (1 row) SELECT * FROM mv_ivm_group ORDER BY 1; sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ -----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 150 | 5 | 30.0000000000000000 | 5 | 5 | 150 | 5 (1 row) INSERT INTO mv_base_a VALUES(6,60); SELECT * FROM mv_ivm_group ORDER BY 1; sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ -----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 210 | 6 | 35.0000000000000000 | 6 | 6 | 210 | 6 (1 row) DELETE FROM mv_base_a; SELECT * FROM mv_ivm_group ORDER BY 1; sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ -----+-------+-----+-------------------+-------------------+-----------------+--------------- | 0 | | 0 | 0 | | 0 (1 row) ROLLBACK; -- TRUNCATE a base table in aggregate views without GROUP clause BEGIN; SELECT pgivm.create_immv('mv_ivm_group', 'SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a'); create_immv ------------- 1 (1 row) TRUNCATE mv_base_a; SELECT sum, count, avg FROM mv_ivm_group; sum | count | avg -----+-------+----- | 0 | (1 row) SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a; sum | count | avg -----+-------+----- | 0 | (1 row) ROLLBACK; -- resolved issue: When use AVG() function and values is indivisible, result of AVG() is incorrect. BEGIN; SELECT pgivm.create_immv('mv_ivm_avg_bug', 'SELECT i, SUM(j), COUNT(j), AVG(j) FROM mv_base_A GROUP BY i'); NOTICE: created index "mv_ivm_avg_bug_index" on immv "mv_ivm_avg_bug" create_immv ------------- 5 (1 row) SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 20 | 1 | 20.0000000000000000 | 1 | 1 | 20 | 1 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) INSERT INTO mv_base_a VALUES (1,0), (1,0), (2,30), (2,30); SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 3 | 3.3333333333333333 | 3 | 3 | 10 | 3 2 | 80 | 3 | 26.6666666666666667 | 3 | 3 | 80 | 3 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) DELETE FROM mv_base_a WHERE (i,j) = (1,0); DELETE FROM mv_base_a WHERE (i,j) = (2,30); SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3; i | sum | count | avg | __ivm_count_sum__ | __ivm_count_avg__ | __ivm_sum_avg__ | __ivm_count__ ---+-----+-------+---------------------+-------------------+-------------------+-----------------+--------------- 1 | 10 | 1 | 10.0000000000000000 | 1 | 1 | 10 | 1 2 | 20 | 1 | 20.0000000000000000 | 1 | 1 | 20 | 1 3 | 30 | 1 | 30.0000000000000000 | 1 | 1 | 30 | 1 4 | 40 | 1 | 40.0000000000000000 | 1 | 1 | 40 | 1 5 | 50 | 1 | 50.0000000000000000 | 1 | 1 | 50 | 1 (5 rows) ROLLBACK; -- support MIN(), MAX() aggregate functions BEGIN; SELECT pgivm.create_immv('mv_ivm_min_max(i, min_j, max_j)', 'SELECT i, MIN(j), MAX(j) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_min_max_index" on immv "mv_ivm_min_max" create_immv ------------- 5 (1 row) SELECT * FROM mv_ivm_min_max ORDER BY 1,2,3; i | min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ ---+-------+-------+---------------------+---------------------+--------------- 1 | 10 | 10 | 1 | 1 | 1 2 | 20 | 20 | 1 | 1 | 1 3 | 30 | 30 | 1 | 1 | 1 4 | 40 | 40 | 1 | 1 | 1 5 | 50 | 50 | 1 | 1 | 1 (5 rows) INSERT INTO mv_base_a VALUES (1,11), (1,12), (2,21), (2,22), (3,31), (3,32), (4,41), (4,42), (5,51), (5,52); SELECT * FROM mv_ivm_min_max ORDER BY 1,2,3; i | min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ ---+-------+-------+---------------------+---------------------+--------------- 1 | 10 | 12 | 3 | 3 | 3 2 | 20 | 22 | 3 | 3 | 3 3 | 30 | 32 | 3 | 3 | 3 4 | 40 | 42 | 3 | 3 | 3 5 | 50 | 52 | 3 | 3 | 3 (5 rows) DELETE FROM mv_base_a WHERE (i,j) IN ((1,10), (2,21), (3,32)); SELECT * FROM mv_ivm_min_max ORDER BY 1,2,3; i | min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ ---+-------+-------+---------------------+---------------------+--------------- 1 | 11 | 12 | 2 | 2 | 2 2 | 20 | 22 | 2 | 2 | 2 3 | 30 | 31 | 2 | 2 | 2 4 | 40 | 42 | 3 | 3 | 3 5 | 50 | 52 | 3 | 3 | 3 (5 rows) ROLLBACK; -- support MIN(), MAX() aggregate functions without GROUP clause BEGIN; SELECT pgivm.create_immv('mv_ivm_min_max(min_j, max_j)', 'SELECT MIN(j), MAX(j) FROM mv_base_a'); create_immv ------------- 1 (1 row) SELECT * FROM mv_ivm_min_max; min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ -------+-------+---------------------+---------------------+--------------- 10 | 50 | 5 | 5 | 5 (1 row) INSERT INTO mv_base_a VALUES (0,0), (6,60), (7,70); SELECT * FROM mv_ivm_min_max; min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ -------+-------+---------------------+---------------------+--------------- 0 | 70 | 8 | 8 | 8 (1 row) DELETE FROM mv_base_a WHERE (i,j) IN ((0,0), (7,70)); SELECT * FROM mv_ivm_min_max; min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ -------+-------+---------------------+---------------------+--------------- 10 | 60 | 6 | 6 | 6 (1 row) DELETE FROM mv_base_a; SELECT * FROM mv_ivm_min_max; min_j | max_j | __ivm_count_min_j__ | __ivm_count_max_j__ | __ivm_count__ -------+-------+---------------------+---------------------+--------------- | | 0 | 0 | 0 (1 row) ROLLBACK; -- Test MIN/MAX after search_path change BEGIN; SELECT pgivm.create_immv('mv_ivm_min', 'SELECT MIN(j) FROM mv_base_a'); create_immv ------------- 1 (1 row) SELECT * FROM mv_ivm_min ORDER BY 1,2,3; min | __ivm_count_min__ | __ivm_count__ -----+-------------------+--------------- 10 | 5 | 5 (1 row) CREATE SCHEMA myschema; GRANT ALL ON SCHEMA myschema TO public; CREATE TABLE myschema.mv_base_a (j int); INSERT INTO myschema.mv_base_a VALUES (1); DELETE FROM mv_base_a WHERE (i,j) = (1,10); SELECT * FROM mv_ivm_min ORDER BY 1,2,3; min | __ivm_count_min__ | __ivm_count__ -----+-------------------+--------------- 20 | 4 | 4 (1 row) SET search_path TO myschema,public,pg_catalog; DELETE FROM public.mv_base_a WHERE (i,j) = (2,20); SELECT * FROM mv_ivm_min ORDER BY 1,2,3; min | __ivm_count_min__ | __ivm_count__ -----+-------------------+--------------- 30 | 3 | 3 (1 row) ROLLBACK; -- aggregate views with column names specified BEGIN; SELECT pgivm.create_immv('mv_ivm_agg(a)', 'SELECT i, SUM(j) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_agg_index" on immv "mv_ivm_agg" create_immv ------------- 5 (1 row) INSERT INTO mv_base_a VALUES (1,100), (2,200), (3,300); UPDATE mv_base_a SET j = 2000 WHERE (i,j) = (2,20); DELETE FROM mv_base_a WHERE (i,j) = (3,30); SELECT * FROM mv_ivm_agg ORDER BY 1,2; a | sum | __ivm_count_sum__ | __ivm_count__ ---+------+-------------------+--------------- 1 | 110 | 2 | 2 2 | 2200 | 2 | 2 3 | 300 | 1 | 1 4 | 40 | 1 | 1 5 | 50 | 1 | 1 (5 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_ivm_agg(a,b)', 'SELECT i, SUM(j) FROM mv_base_a GROUP BY i'); NOTICE: created index "mv_ivm_agg_index" on immv "mv_ivm_agg" create_immv ------------- 5 (1 row) INSERT INTO mv_base_a VALUES (1,100), (2,200), (3,300); UPDATE mv_base_a SET j = 2000 WHERE (i,j) = (2,20); DELETE FROM mv_base_a WHERE (i,j) = (3,30); SELECT * FROM mv_ivm_agg ORDER BY 1,2; a | b | __ivm_count_b__ | __ivm_count__ ---+------+-----------------+--------------- 1 | 110 | 2 | 2 2 | 2200 | 2 | 2 3 | 300 | 1 | 1 4 | 40 | 1 | 1 5 | 50 | 1 | 1 (5 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_ivm_agg(a,b,c)', 'SELECT i, SUM(j) FROM mv_base_a GROUP BY i'); ERROR: too many column names were specified ROLLBACK; -- support self join view and multiple change on the same table BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (1, 10), (2, 20), (3, 30); SELECT pgivm.create_immv('mv_self(v1, v2)', 'SELECT t1.v, t2.v FROM base_t AS t1 JOIN base_t AS t2 ON t1.i = t2.i'); NOTICE: could not create an index on immv "mv_self" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 3 (1 row) SELECT * FROM mv_self ORDER BY v1; v1 | v2 ----+---- 10 | 10 20 | 20 30 | 30 (3 rows) INSERT INTO base_t VALUES (4,40); DELETE FROM base_t WHERE i = 1; UPDATE base_t SET v = v*10 WHERE i=2; SELECT * FROM mv_self ORDER BY v1; v1 | v2 -----+----- 30 | 30 40 | 40 200 | 200 (3 rows) WITH ins_t1 AS (INSERT INTO base_t VALUES (5,50) RETURNING 1), ins_t2 AS (INSERT INTO base_t VALUES (6,60) RETURNING 1), upd_t AS (UPDATE base_t SET v = v + 100 RETURNING 1), dlt_t AS (DELETE FROM base_t WHERE i IN (4,5) RETURNING 1) SELECT NULL; ?column? ---------- (1 row) SELECT * FROM mv_self ORDER BY v1; v1 | v2 -----+----- 50 | 50 60 | 60 130 | 130 300 | 300 (4 rows) --- with sub-transactions SAVEPOINT p1; INSERT INTO base_t VALUES (7,70); RELEASE SAVEPOINT p1; INSERT INTO base_t VALUES (7,77); SELECT * FROM mv_self ORDER BY v1, v2; v1 | v2 -----+----- 50 | 50 60 | 60 70 | 70 70 | 77 77 | 70 77 | 77 130 | 130 300 | 300 (8 rows) ROLLBACK; -- support simultaneous table changes BEGIN; CREATE TABLE base_r (i int, v int); CREATE TABLE base_s (i int, v int); INSERT INTO base_r VALUES (1, 10), (2, 20), (3, 30); INSERT INTO base_s VALUES (1, 100), (2, 200), (3, 300); SELECT pgivm.create_immv('mv(v1, v2)', 'SELECT r.v, s.v FROM base_r AS r JOIN base_s AS s USING(i)');; NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 3 (1 row) SELECT * FROM mv ORDER BY v1; v1 | v2 ----+----- 10 | 100 20 | 200 30 | 300 (3 rows) WITH ins_r AS (INSERT INTO base_r VALUES (1,11) RETURNING 1), ins_r2 AS (INSERT INTO base_r VALUES (3,33) RETURNING 1), ins_s AS (INSERT INTO base_s VALUES (2,222) RETURNING 1), upd_r AS (UPDATE base_r SET v = v + 1000 WHERE i = 2 RETURNING 1), dlt_s AS (DELETE FROM base_s WHERE i = 3 RETURNING 1) SELECT NULL; ?column? ---------- (1 row) SELECT * FROM mv ORDER BY v1; v1 | v2 ------+----- 10 | 100 11 | 100 1020 | 200 1020 | 222 (4 rows) -- support foreign reference constraints BEGIN; WARNING: there is already a transaction in progress CREATE TABLE ri1 (i int PRIMARY KEY); CREATE TABLE ri2 (i int PRIMARY KEY REFERENCES ri1(i) ON UPDATE CASCADE ON DELETE CASCADE, v int); INSERT INTO ri1 VALUES (1),(2),(3); INSERT INTO ri2 VALUES (1),(2),(3); SELECT pgivm.create_immv('mv_ri(i1, i2)', 'SELECT ri1.i, ri2.i FROM ri1 JOIN ri2 USING(i)'); NOTICE: created index "mv_ri_index" on immv "mv_ri" create_immv ------------- 3 (1 row) SELECT * FROM mv_ri ORDER BY i1; i1 | i2 ----+---- 1 | 1 2 | 2 3 | 3 (3 rows) UPDATE ri1 SET i=10 where i=1; DELETE FROM ri1 WHERE i=2; SELECT * FROM mv_ri ORDER BY i2; i1 | i2 ----+---- 3 | 3 10 | 10 (2 rows) ROLLBACK; -- support subquery for using EXISTS() BEGIN; SELECT pgivm.create_immv('mv_ivm_exists_subquery', 'SELECT a.i, a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); NOTICE: could not create an index on immv "mv_ivm_exists_subquery" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) SELECT pgivm.create_immv('mv_ivm_exists_subquery2', 'SELECT a.i,a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) AND a.i > 2'); NOTICE: could not create an index on immv "mv_ivm_exists_subquery2" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 2 (1 row) SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+----+------------------------ 1 | 10 | 1 2 | 20 | 1 3 | 30 | 1 4 | 40 | 1 (4 rows) SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+----+------------------------ 3 | 30 | 1 4 | 40 | 1 (2 rows) INSERT INTO mv_base_a VALUES(1,10),(6,60),(3,30),(3,300); SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 1 | 10 | 1 1 | 10 | 1 2 | 20 | 1 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (7 rows) SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (4 rows) INSERT INTO mv_base_b VALUES(1,101); INSERT INTO mv_base_b VALUES(1,111); INSERT INTO mv_base_b VALUES(2,102); INSERT INTO mv_base_b VALUES(6,106); SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 1 | 10 | 3 1 | 10 | 3 2 | 20 | 2 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 6 | 60 | 1 (8 rows) SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 6 | 60 | 1 (5 rows) UPDATE mv_base_a SET i = 1 WHERE j =60; UPDATE mv_base_b SET i = 10 WHERE k = 101; UPDATE mv_base_b SET k = 1002 WHERE k = 102; SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 1 | 10 | 1 1 | 10 | 1 1 | 60 | 1 2 | 20 | 2 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (8 rows) SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (4 rows) DELETE FROM mv_base_a WHERE (i,j) = (1,60); DELETE FROM mv_base_b WHERE i = 2; SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 1 | 10 | 1 1 | 10 | 1 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (6 rows) SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (4 rows) --- EXISTS subquery with tuple duplication and DISTINCT SELECT pgivm.create_immv('mv_ivm_exists_subquery_distinct', 'SELECT DISTINCT a.i, a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); NOTICE: created index "mv_ivm_exists_subquery_distinct_index" on immv "mv_ivm_exists_subquery_distinct" create_immv ------------- 4 (1 row) DELETE FROM mv_base_b WHERE i = 1 or i = 3; INSERT INTO mv_base_b VALUES (1,100), (3,300); SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; i | j | __ivm_exists_count_0__ ---+-----+------------------------ 1 | 10 | 1 1 | 10 | 1 3 | 30 | 1 3 | 30 | 1 3 | 300 | 1 4 | 40 | 1 (6 rows) SELECT * FROM mv_ivm_exists_subquery_distinct ORDER BY i, j; i | j | __ivm_exists_count_0__ | __ivm_count__ ---+-----+------------------------+--------------- 1 | 10 | 1 | 2 3 | 30 | 1 | 2 3 | 300 | 1 | 1 4 | 40 | 1 | 1 (4 rows) ROLLBACK; -- support simple subquery in FROM clause BEGIN; SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a,( SELECT * FROM mv_base_b) b WHERE a.i = b.i'); NOTICE: could not create an index on immv "mv_ivm_subquery" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_ivm_subquery ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; -- disallow non-simple subqueries SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a, (SELECT i, COUNT(*) FROM mv_base_b GROUP BY i) b WHERE a.i = b.i'); ERROR: aggregate functions in nested query are not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a, (SELECT DISTINCT i FROM mv_base_b) b WHERE a.i = b.i'); ERROR: DISTINCT clause in nested query are not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT i,j FROM mv_base_a WHERE i IN (SELECT i FROM mv_base_b WHERE k < 103 )'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT i,j, (SELECT k FROM mv_base_b LIMIT 1) FROM mv_base_a a'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT i,j, (SELECT k FROM mv_base_b LIMIT 1) + 1 FROM mv_base_a a'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM generate_series(1, (SELECT k FROM mv_base_b LIMIT 1)) AS v'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: targetlist must contain vars that are referred to in EXISTS subquery SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) OR a.i > 2'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: OR or NOT conditions and EXISTS condition can not be used together SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_a a2 WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a2.i = b.i))'); ERROR: nested sublink is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT EXISTS(SELECT 1 from mv_base_b) FROM mv_base_a a'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT false OR EXISTS(SELECT 1 FROM mv_base_a) FROM mv_base_b'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: OR or NOT conditions and EXISTS condition can not be used together SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM generate_series(1, CASE EXISTS(SELECT 1 FROM mv_base_a) WHEN true THEN 100 ELSE 10 END), mv_base_b'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM mv_base_a a WHERE CASE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) WHEN true THEN false ELSE true END'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM mv_base_a a WHERE true and CASE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) WHEN true THEN false ELSE true END'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause -- support join subquery in FROM clause BEGIN; SELECT pgivm.create_immv('mv_ivm_join_subquery', 'SELECT i, j, k FROM ( SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN mv_base_a a USING(i)) tmp'); NOTICE: could not create an index on immv "mv_ivm_join_subquery" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; -- (1 row) SELECT * FROM mv_ivm_join_subquery ORDER BY i,j,k; i | j | k ---+----+----- 1 | 10 | 101 1 | 10 | 111 1 | 11 | 101 1 | 11 | 111 2 | 20 | 102 2 | 22 | 102 3 | 30 | 103 3 | 30 | 133 (8 rows) ROLLBACK; BEGIN; -- nested subquery SELECT pgivm.create_immv('mv_ivm_join_subquery', 'SELECT i, j, k FROM ( SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN (SELECT * FROM mv_base_a) a USING(i)) tmp'); NOTICE: could not create an index on immv "mv_ivm_join_subquery" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; -- (1 row) SELECT * FROM mv_ivm_join_subquery ORDER BY i,j,k; i | j | k ---+----+----- 1 | 10 | 101 1 | 10 | 111 1 | 11 | 101 1 | 11 | 111 2 | 20 | 102 2 | 22 | 102 3 | 30 | 103 3 | 30 | 133 (8 rows) ROLLBACK; -- support simple CTE BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH b AS ( SELECT * FROM mv_base_b) SELECT a.i,a.j FROM mv_base_a a, b WHERE a.i = b.i'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH a AS (SELECT * FROM mv_base_a), b AS ( SELECT * FROM mv_base_b) SELECT a.i,a.j FROM a, b WHERE a.i = b.i'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH b AS ( SELECT * FROM mv_base_b) SELECT v.i,v.j FROM (WITH a AS (SELECT * FROM mv_base_a) SELECT a.i,a.j FROM a, b WHERE a.i = b.i) v'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'SELECT * FROM (WITH a AS (SELECT * FROM mv_base_a), b AS ( SELECT * FROM mv_base_b) SELECT a.i,a.j FROM a, b WHERE a.i = b.i) v'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH b AS ( SELECT * FROM (SELECT * FROM mv_base_b) b2) SELECT v.i,v.j FROM (WITH a AS (SELECT * FROM mv_base_a) SELECT a.i,a.j FROM a, b WHERE a.i = b.i) v'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; i | j ---+---- 1 | 10 2 | 20 2 | 20 3 | 30 3 | 30 4 | 40 (6 rows) ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH x AS ( SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN mv_base_a a USING(i)) SELECT * FROM x'); NOTICE: could not create an index on immv "mv_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; -- (1 row) SELECT * FROM mv_cte ORDER BY i,j,k; i | j | k ---+----+----- 1 | 10 | 101 1 | 10 | 111 1 | 11 | 101 1 | 11 | 111 2 | 20 | 102 2 | 22 | 102 3 | 30 | 103 3 | 30 | 133 (8 rows) ROLLBACK; -- nested CTE BEGIN; SELECT pgivm.create_immv('mv_ivm_nested_cte', 'WITH v AS ( WITH a AS (SELECT * FROM mv_base_a) SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN a USING(i)) SELECT * FROM v'); NOTICE: could not create an index on immv "mv_ivm_nested_cte" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 4 (1 row) WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; -- (1 row) SELECT * FROM mv_ivm_nested_cte ORDER BY i,j,k; i | j | k ---+----+----- 1 | 10 | 101 1 | 10 | 111 1 | 11 | 101 1 | 11 | 111 2 | 20 | 102 2 | 22 | 102 3 | 30 | 103 3 | 30 | 133 (8 rows) ROLLBACK; -- Multiply-referenced CTE BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (1, 10), (2, 20), (3, 30); SELECT pgivm.create_immv('mv_cte_multi(v1, v2)', 'WITH t AS (SELECT * FROM base_t) SELECT t1.v, t2.v FROM t AS t1 JOIN t AS t2 ON t1.i = t2.i'); NOTICE: could not create an index on immv "mv_cte_multi" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 3 (1 row) SELECT * FROM mv_cte_multi ORDER BY v1; v1 | v2 ----+---- 10 | 10 20 | 20 30 | 30 (3 rows) INSERT INTO base_t VALUES (4,40); DELETE FROM base_t WHERE i = 1; UPDATE base_t SET v = v*10 WHERE i=2; SELECT * FROM mv_cte_multi ORDER BY v1; v1 | v2 -----+----- 30 | 30 40 | 40 200 | 200 (3 rows) WITH ins_t1 AS (INSERT INTO base_t VALUES (5,50) RETURNING 1), ins_t2 AS (INSERT INTO base_t VALUES (6,60) RETURNING 1), upd_t AS (UPDATE base_t SET v = v + 100 RETURNING 1), dlt_t AS (DELETE FROM base_t WHERE i IN (4,5) RETURNING 1) SELECT NULL; ?column? ---------- (1 row) SELECT * FROM mv_cte_multi ORDER BY v1; v1 | v2 -----+----- 50 | 50 60 | 60 130 | 130 300 | 300 (4 rows) ROLLBACK; --- disallow not-simple CTE SELECT pgivm.create_immv('mv_cte_fail', 'WITH b AS (SELECT i, COUNT(*) FROM mv_base_b GROUP BY i) SELECT a.i,a.j FROM mv_base_a a, b WHERE a.i = b.i'); ERROR: aggregate functions in nested query are not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_cte_fail', 'WITH b AS (SELECT DISTINCT i FROM mv_base_b) SELECT a.i,a.j FROM mv_base_a a, b WHERE a.i = b.i'); ERROR: DISTINCT clause in nested query are not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_cte_fail', 'WITH a AS (SELECT i, j FROM mv_base_a) SELECT a.i,a.j FROM a WHERE EXISTS(WITH b AS (SELECT i FROM mv_base_b) SELECT 1 FROM b WHERE a.i = b.i)'); ERROR: CTE in EXIST clause is not supported on incrementally maintainable materialized view -- unreferenced CTE SELECT pgivm.create_immv('mv_cte_fail', 'WITH b AS (SELECT * FROM mv_base_b) SELECT * FROM mv_base_a a'); ERROR: Ureferenced WITH query is not supported on incrementally maintainable materialized view -- views including NULL BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (1,10),(2, NULL); SELECT pgivm.create_immv('mv', 'SELECT * FROM base_t'); NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 2 (1 row) SELECT * FROM mv ORDER BY i; i | v ---+---- 1 | 10 2 | (2 rows) UPDATE base_t SET v = 20 WHERE i = 2; SELECT * FROM mv ORDER BY i; i | v ---+---- 1 | 10 2 | 20 (2 rows) ROLLBACK; BEGIN; CREATE TABLE base_t (i int); SELECT pgivm.create_immv('mv', 'SELECT * FROM base_t'); NOTICE: could not create an index on immv "mv" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 0 (1 row) SELECT * FROM mv ORDER BY i; i --- (0 rows) INSERT INTO base_t VALUES (1),(NULL); SELECT * FROM mv ORDER BY i; i --- 1 (2 rows) ROLLBACK; BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (NULL, 1), (NULL, 2), (1, 10), (1, 20); SELECT pgivm.create_immv('mv', 'SELECT i, sum(v) FROM base_t GROUP BY i'); NOTICE: created index "mv_index" on immv "mv" create_immv ------------- 2 (1 row) SELECT * FROM mv ORDER BY i; i | sum | __ivm_count_sum__ | __ivm_count__ ---+-----+-------------------+--------------- 1 | 30 | 2 | 2 | 3 | 2 | 2 (2 rows) UPDATE base_t SET v = v * 10; SELECT * FROM mv ORDER BY i; i | sum | __ivm_count_sum__ | __ivm_count__ ---+-----+-------------------+--------------- 1 | 300 | 2 | 2 | 30 | 2 | 2 (2 rows) ROLLBACK; BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (NULL, 1), (NULL, 2), (NULL, 3), (NULL, 4), (NULL, 5); SELECT pgivm.create_immv('mv', 'SELECT i, min(v), max(v) FROM base_t GROUP BY i'); NOTICE: created index "mv_index" on immv "mv" create_immv ------------- 1 (1 row) SELECT * FROM mv ORDER BY i; i | min | max | __ivm_count_min__ | __ivm_count_max__ | __ivm_count__ ---+-----+-----+-------------------+-------------------+--------------- | 1 | 5 | 5 | 5 | 5 (1 row) DELETE FROM base_t WHERE v = 1; SELECT * FROM mv ORDER BY i; i | min | max | __ivm_count_min__ | __ivm_count_max__ | __ivm_count__ ---+-----+-----+-------------------+-------------------+--------------- | 2 | 5 | 4 | 4 | 4 (1 row) DELETE FROM base_t WHERE v = 3; SELECT * FROM mv ORDER BY i; i | min | max | __ivm_count_min__ | __ivm_count_max__ | __ivm_count__ ---+-----+-----+-------------------+-------------------+--------------- | 2 | 5 | 3 | 3 | 3 (1 row) DELETE FROM base_t WHERE v = 5; SELECT * FROM mv ORDER BY i; i | min | max | __ivm_count_min__ | __ivm_count_max__ | __ivm_count__ ---+-----+-----+-------------------+-------------------+--------------- | 2 | 4 | 2 | 2 | 2 (1 row) ROLLBACK; -- IMMV containing user defined type BEGIN; CREATE TYPE mytype; CREATE FUNCTION mytype_in(cstring) RETURNS mytype AS 'int4in' LANGUAGE INTERNAL STRICT IMMUTABLE; NOTICE: return type mytype is only a shell CREATE FUNCTION mytype_out(mytype) RETURNS cstring AS 'int4out' LANGUAGE INTERNAL STRICT IMMUTABLE; NOTICE: argument type mytype is only a shell CREATE TYPE mytype ( LIKE = int4, INPUT = mytype_in, OUTPUT = mytype_out ); CREATE FUNCTION mytype_eq(mytype, mytype) RETURNS bool AS 'int4eq' LANGUAGE INTERNAL STRICT IMMUTABLE; CREATE FUNCTION mytype_lt(mytype, mytype) RETURNS bool AS 'int4lt' LANGUAGE INTERNAL STRICT IMMUTABLE; CREATE FUNCTION mytype_cmp(mytype, mytype) RETURNS integer AS 'btint4cmp' LANGUAGE INTERNAL STRICT IMMUTABLE; CREATE OPERATOR = ( leftarg = mytype, rightarg = mytype, procedure = mytype_eq); CREATE OPERATOR < ( leftarg = mytype, rightarg = mytype, procedure = mytype_lt); CREATE OPERATOR CLASS mytype_ops DEFAULT FOR TYPE mytype USING btree AS OPERATOR 1 <, OPERATOR 3 = , FUNCTION 1 mytype_cmp(mytype,mytype); CREATE TABLE t_mytype (x mytype); SELECT pgivm.create_immv('mv_mytype', 'SELECT * FROM t_mytype'); NOTICE: could not create an index on immv "mv_mytype" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 0 (1 row) INSERT INTO t_mytype VALUES ('1'::mytype); SELECT * FROM mv_mytype; x --- 1 (1 row) ROLLBACK; -- contain system column SELECT pgivm.create_immv('mv_ivm01', 'SELECT i,j,xmin FROM mv_base_a'); ERROR: system column is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm02', 'SELECT i,j FROM mv_base_a WHERE xmin = ''610'''); ERROR: system column is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm03', 'SELECT i,j,xmin::text AS x_min FROM mv_base_a'); ERROR: system column is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm04', 'SELECT i,j,xidsend(xmin) AS x_min FROM mv_base_a'); ERROR: system column is not supported on incrementally maintainable materialized view -- targetlist or WHERE clause without EXISTS contain subquery SELECT pgivm.create_immv('mv_ivm05', 'SELECT i,j FROM mv_base_a WHERE i IN (SELECT i FROM mv_base_b WHERE k < 103 )'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause SELECT pgivm.create_immv('mv_ivm05', 'SELECT i,j, (SELECT k FROM mv_base_b b WHERE a.i = b.i) FROM mv_base_a a'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: sublink only supports subquery with EXISTS clause in WHERE clause -- contain ORDER BY SELECT pgivm.create_immv('mv_ivm07', 'SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) ORDER BY i,j,k'); ERROR: ORDER BY clause is not supported on incrementally maintainable materialized view -- contain HAVING SELECT pgivm.create_immv('mv_ivm08', 'SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) GROUP BY i,j,k HAVING SUM(i) > 5'); ERROR: HAVING clause is not supported on incrementally maintainable materialized view -- contain GROUP BY without aggregate SELECT pgivm.create_immv('mv_ivm08', 'SELECT i,j FROM mv_base_a GROUP BY i,j'); ERROR: GROUP BY clause without aggregate is not supported on incrementally maintainable materialized view -- contain view or materialized view CREATE VIEW b_view AS SELECT i,k FROM mv_base_b; CREATE MATERIALIZED VIEW b_mview AS SELECT i,k FROM mv_base_b; SELECT pgivm.create_immv('mv_ivm07', 'SELECT a.i,a.j FROM mv_base_a a,b_view b WHERE a.i = b.i'); ERROR: VIEW or MATERIALIZED VIEW is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm08', 'SELECT a.i,a.j FROM mv_base_a a,b_mview b WHERE a.i = b.i'); ERROR: VIEW or MATERIALIZED VIEW is not supported on incrementally maintainable materialized view -- contain mutable functions SELECT pgivm.create_immv('mv_ivm12', 'SELECT i,j FROM mv_base_a WHERE i = random()::int'); ERROR: mutable function is not supported on incrementally maintainable materialized view HINT: functions must be marked IMMUTABLE -- LIMIT/OFFSET is not supported SELECT pgivm.create_immv('mv_ivm13', 'SELECT i,j FROM mv_base_a LIMIT 10 OFFSET 5'); ERROR: LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view -- DISTINCT ON is not supported SELECT pgivm.create_immv('mv_ivm14', 'SELECT DISTINCT ON(i) i, j FROM mv_base_a'); ERROR: DISTINCT ON is not supported on incrementally maintainable materialized view -- TABLESAMPLE clause is not supported SELECT pgivm.create_immv('mv_ivm15', 'SELECT i, j FROM mv_base_a TABLESAMPLE SYSTEM(50)'); ERROR: TABLESAMPLE clause is not supported on incrementally maintainable materialized view -- window functions are not supported SELECT pgivm.create_immv('mv_ivm16', 'SELECT i, j FROM (SELECT *, cume_dist() OVER (ORDER BY i) AS rank FROM mv_base_a) AS t'); ERROR: window functions are not supported on incrementally maintainable materialized view -- aggregate function with some options is not supported SELECT pgivm.create_immv('mv_ivm17', 'SELECT COUNT(*) FILTER(WHERE i < 3) FROM mv_base_a'); ERROR: aggregate function with FILTER clause is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm18', 'SELECT COUNT(DISTINCT i) FROM mv_base_a'); ERROR: aggregate function with DISTINCT arguments is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm19', 'SELECT array_agg(j ORDER BY i DESC) FROM mv_base_a'); ERROR: aggregate function with ORDER clause is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm20', 'SELECT i,SUM(j) FROM mv_base_a GROUP BY GROUPING SETS((i),())'); ERROR: GROUPING SETS, ROLLUP, or CUBE clauses is not supported on incrementally maintainable materialized view -- inheritance parent is not supported BEGIN; CREATE TABLE parent (i int, v int); CREATE TABLE child_a(options text) INHERITS(parent); SELECT pgivm.create_immv('mv_ivm21', 'SELECT * FROM parent'); ERROR: inheritance parent is not supported on incrementally maintainable materialized view ROLLBACK; -- UNION statement is not supported SELECT pgivm.create_immv('mv_ivm22', 'SELECT i,j FROM mv_base_a UNION ALL SELECT i,k FROM mv_base_b'); ERROR: UNION/INTERSECT/EXCEPT statements are not supported on incrementally maintainable materialized view -- DISTINCT clause in nested query are not supported SELECT pgivm.create_immv('mv_ivm23', 'SELECT * FROM (SELECT DISTINCT i,j FROM mv_base_a) AS tmp');; ERROR: DISTINCT clause in nested query are not supported on incrementally maintainable materialized view -- empty target list is not allowed with IVM SELECT pgivm.create_immv('mv_ivm25', 'SELECT FROM mv_base_a'); ERROR: empty target list is not supported on incrementally maintainable materialized view -- FOR UPDATE/SHARE is not supported SELECT pgivm.create_immv('mv_ivm26', 'SELECT i,j FROM mv_base_a FOR UPDATE'); ERROR: FOR UPDATE/SHARE clause is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm27', 'SELECT * FROM (SELECT i,j FROM mv_base_a FOR UPDATE) AS tmp;'); ERROR: FOR UPDATE/SHARE clause is not supported on incrementally maintainable materialized view -- tartget list cannot contain ivm column that start with '__ivm' SELECT pgivm.create_immv('mv_ivm28', 'SELECT i AS "__ivm_count__" FROM mv_base_a'); ERROR: column name __ivm_count__ is not supported on incrementally maintainable materialized view -- expressions specified in GROUP BY must appear in the target list. SELECT pgivm.create_immv('mv_ivm29', 'SELECT COUNT(i) FROM mv_base_a GROUP BY i;'); ERROR: GROUP BY expression not appearing in select list is not supported on incrementally maintainable materialized view -- experssions containing an aggregate is not supported SELECT pgivm.create_immv('mv_ivm30', 'SELECT sum(i)*0.5 FROM mv_base_a'); ERROR: expression containing an aggregate in it is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm31', 'SELECT sum(i)/sum(j) FROM mv_base_a'); ERROR: expression containing an aggregate in it is not supported on incrementally maintainable materialized view -- VALUES is not supported SELECT pgivm.create_immv('mv_ivm_only_values1', 'values(1)'); ERROR: VALUES is not supported on incrementally maintainable materialized view SELECT pgivm.create_immv('mv_ivm_only_values2', 'SELECT * FROM (values(1)) AS tmp'); ERROR: VALUES is not supported on incrementally maintainable materialized view -- column of parent query specified in EXISTS clause must appear in the target list. SELECT pgivm.create_immv('mv_ivm32', 'SELECT a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); ERROR: this query is not allowed on incrementally maintainable materialized view HINT: targetlist must contain vars that are referred to in EXISTS subquery -- views containing base tables with Row Level Security DROP USER IF EXISTS regress_ivm_admin; NOTICE: role "regress_ivm_admin" does not exist, skipping DROP USER IF EXISTS regress_ivm_user; NOTICE: role "regress_ivm_user" does not exist, skipping CREATE USER regress_ivm_admin; CREATE USER regress_ivm_user; --- create a table with RLS SET SESSION AUTHORIZATION regress_ivm_admin; CREATE TABLE rls_tbl(id int, data text, owner name); INSERT INTO rls_tbl VALUES (1,'foo','regress_ivm_user'), (2,'bar','postgres'); CREATE TABLE num_tbl(id int, num text); INSERT INTO num_tbl VALUES (1,'one'), (2,'two'), (3,'three'), (4,'four'), (5,'five'), (6,'six'); --- Users can access only their own rows CREATE POLICY rls_tbl_policy ON rls_tbl FOR SELECT TO PUBLIC USING(owner = current_user); ALTER TABLE rls_tbl ENABLE ROW LEVEL SECURITY; GRANT ALL on rls_tbl TO PUBLIC; GRANT ALL on num_tbl TO PUBLIC; --- create a view owned by regress_ivm_user SET SESSION AUTHORIZATION regress_ivm_user; SELECT pgivm.create_immv('ivm_rls', 'SELECT * FROM rls_tbl'); NOTICE: could not create an index on immv "ivm_rls" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 1 (1 row) SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3; id | data | owner ----+------+------------------ 1 | foo | regress_ivm_user (1 row) RESET SESSION AUTHORIZATION; --- inserts rows owned by different users INSERT INTO rls_tbl VALUES (3,'baz','regress_ivm_user'), (4,'qux','postgres'); SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3; id | data | owner ----+------+------------------ 1 | foo | regress_ivm_user 3 | baz | regress_ivm_user (2 rows) --- combination of diffent kinds of commands WITH i AS (INSERT INTO rls_tbl VALUES(5,'quux','postgres'), (6,'corge','regress_ivm_user')), u AS (UPDATE rls_tbl SET owner = 'postgres' WHERE id = 1), u2 AS (UPDATE rls_tbl SET owner = 'regress_ivm_user' WHERE id = 2) SELECT; -- (1 row) SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3; id | data | owner ----+-------+------------------ 2 | bar | regress_ivm_user 3 | baz | regress_ivm_user 6 | corge | regress_ivm_user (3 rows) --- SET SESSION AUTHORIZATION regress_ivm_user; SELECT pgivm.create_immv('ivm_rls2', 'SELECT * FROM rls_tbl JOIN num_tbl USING(id)'); NOTICE: could not create an index on immv "ivm_rls2" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 3 (1 row) RESET SESSION AUTHORIZATION; WITH x AS (UPDATE rls_tbl SET data = data || '_2' where id in (3,4)), y AS (UPDATE num_tbl SET num = num || '_2' where id in (3,4)) SELECT; -- (1 row) SELECT * FROM ivm_rls2 ORDER BY 1,2,3; id | data | owner | num ----+-------+------------------+--------- 2 | bar | regress_ivm_user | two 3 | baz_2 | regress_ivm_user | three_2 6 | corge | regress_ivm_user | six (3 rows) DROP TABLE rls_tbl CASCADE; NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to table ivm_rls drop cascades to table ivm_rls2 DROP TABLE num_tbl CASCADE; DROP USER regress_ivm_user; DROP USER regress_ivm_admin; -- automatic index creation BEGIN; CREATE TABLE base_a (i int primary key, j int); CREATE TABLE base_b (i int primary key, j int); --- group by: create an index SELECT pgivm.create_immv('mv_idx1', 'SELECT i, sum(j) FROM base_a GROUP BY i'); NOTICE: created index "mv_idx1_index" on immv "mv_idx1" create_immv ------------- 0 (1 row) --- distinct: create an index SELECT pgivm.create_immv('mv_idx2', 'SELECT DISTINCT j FROM base_a'); NOTICE: created index "mv_idx2_index" on immv "mv_idx2" create_immv ------------- 0 (1 row) --- with all pkey columns: create an index SELECT pgivm.create_immv('mv_idx3(i_a, i_b)', 'SELECT a.i, b.i FROM base_a a, base_b b'); NOTICE: created index "mv_idx3_index" on immv "mv_idx3" create_immv ------------- 0 (1 row) --- missing some pkey columns: no index SELECT pgivm.create_immv('mv_idx4', 'SELECT j FROM base_a'); NOTICE: could not create an index on immv "mv_idx4" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 0 (1 row) SELECT pgivm.create_immv('mv_idx5', 'SELECT a.i, b.j FROM base_a a, base_b b'); NOTICE: could not create an index on immv "mv_idx5" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 0 (1 row) --- subqueries: create an index SELECT pgivm.create_immv('mv_idx6(i_a, i_b)', 'SELECT a.i, b.i FROM (SELECT * FROM base_a) a, (SELECT * FROM base_b) b'); NOTICE: created index "mv_idx6_index" on immv "mv_idx6" create_immv ------------- 0 (1 row) --- with set-returning function: no index SELECT pgivm.create_immv('mv_idx7', 'SELECT i FROM base_a, generate_series(1,10)'); NOTICE: could not create an index on immv "mv_idx7" automatically DETAIL: This target list does not have all the primary key columns, or this view does not contain GROUP BY or DISTINCT clause. HINT: Create an index on the immv for efficient incremental maintenance. create_immv ------------- 0 (1 row) ROLLBACK; -- type that doesn't have default operator class for access method btree BEGIN; CREATE TABLE table_json (j json); SELECT pgivm.create_immv('mv_json', 'SELECT * from table_json'); ERROR: data type json has no default operator class for access method "btree" ROLLBACK; -- prevent IMMV chanages INSERT INTO mv_ivm_1 VALUES(1,1,1); ERROR: cannot change materialized view "mv_ivm_1" UPDATE mv_ivm_1 SET k = 1 WHERE i = 1; ERROR: cannot change materialized view "mv_ivm_1" DELETE FROM mv_ivm_1; ERROR: cannot change materialized view "mv_ivm_1" TRUNCATE mv_ivm_1; ERROR: cannot change materialized view "mv_ivm_1" -- get_immv_def function SELECT immvrelid, pgivm.get_immv_def(immvrelid) FROM pgivm.pg_ivm_immv ORDER BY 1; immvrelid | get_immv_def -----------+---------------------------------- mv_ivm_1 | SELECT a.i, + | a.j, + | b.k + | FROM (mv_base_a a + | JOIN mv_base_b b USING (i)) (1 row) -- mv_base_b is not immv SELECT 'mv_base_b'::regclass, pgivm.get_immv_def('mv_base_b'); regclass | get_immv_def -----------+-------------- mv_base_b | (1 row) DROP TABLE mv_base_b CASCADE; NOTICE: drop cascades to 3 other objects DETAIL: drop cascades to table mv_ivm_1 drop cascades to view b_view drop cascades to materialized view b_mview DROP TABLE mv_base_a CASCADE; pg_ivm-1.13/expected/refresh_immv.out000066400000000000000000000037411507512346500177540ustar00rootroot00000000000000CREATE TABLE t (i int PRIMARY KEY); INSERT INTO t SELECT generate_series(1, 5); SELECT pgivm.create_immv('mv', 'SELECT * FROM t'); NOTICE: created index "mv_index" on immv "mv" create_immv ------------- 5 (1 row) SELECT immvrelid, ispopulated FROM pgivm.pg_ivm_immv ORDER BY 1; immvrelid | ispopulated -----------+------------- mv | t (1 row) -- Refresh IMMV with data SELECT pgivm.refresh_immv('mv', true); refresh_immv -------------- 5 (1 row) SELECT immvrelid, ispopulated FROM pgivm.pg_ivm_immv ORDER BY 1; immvrelid | ispopulated -----------+------------- mv | t (1 row) INSERT INTO t VALUES(6); SELECT i FROM mv ORDER BY 1; i --- 1 2 3 4 5 6 (6 rows) -- Make IMMV unpopulated SELECT pgivm.refresh_immv('mv', false); refresh_immv -------------- 0 (1 row) SELECT immvrelid, ispopulated FROM pgivm.pg_ivm_immv ORDER BY 1; immvrelid | ispopulated -----------+------------- mv | f (1 row) SELECT i FROM mv ORDER BY 1; i --- (0 rows) -- Immediate maintenance is disabled. IMMV can be scannable and is empty. INSERT INTO t VALUES(7); SELECT i FROM mv ORDER BY 1; i --- (0 rows) -- Refresh the IMMV and make it populated. SELECT pgivm.refresh_immv('mv', true); refresh_immv -------------- 7 (1 row) SELECT immvrelid, ispopulated FROM pgivm.pg_ivm_immv ORDER BY 1; immvrelid | ispopulated -----------+------------- mv | t (1 row) SELECT i FROM mv ORDER BY 1; i --- 1 2 3 4 5 6 7 (7 rows) -- Immediate maintenance is enabled. INSERT INTO t VALUES(8); SELECT i FROM mv ORDER BY 1; i --- 1 2 3 4 5 6 7 8 (8 rows) -- Use qualified name SELECT pgivm.refresh_immv('public.mv', true); refresh_immv -------------- 8 (1 row) -- Use not existing IMMV SELECT pgivm.refresh_immv('mv_not_existing', true); ERROR: relation "mv_not_existing" does not exist -- Try to refresh a normal table -- error SELECT pgivm.refresh_immv('t', true); ERROR: "t" is not an IMMV DROP TABLE mv; pg_ivm-1.13/expected/refresh_insert.out000066400000000000000000000074271507512346500203150ustar00rootroot00000000000000Parsed test spec with 2 sessions starting permutation: s1 refresh s2 insert c1 check2 c2 mv step s1: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step s2: SELECT; step insert: INSERT INTO a VALUES (2); step c1: COMMIT; step insert: <... completed> step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 refresh s2 c1 insert check2 c2 mv step s1: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step s2: SELECT; step c1: COMMIT; step insert: INSERT INTO a VALUES (2); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 refresh insert c1 check2 c2 mv step s1: SELECT; step s2: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step insert: INSERT INTO a VALUES (2); step c1: COMMIT; step insert: <... completed> step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 insert refresh c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step insert: INSERT INTO a VALUES (2); step refresh: SELECT pgivm.refresh_immv('mv', true); step c2: COMMIT; step refresh: <... completed> refresh_immv ------------ 2 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 refresh c1 insert check2 c2 mv step s1: SELECT; step s2: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step c1: COMMIT; step insert: INSERT INTO a VALUES (2); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 insert s1 refresh c2 check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); step c2: COMMIT; step refresh: <... completed> refresh_immv ------------ 2 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 insert s1 c2 refresh check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step c2: COMMIT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 2 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 s1 insert c2 refresh check1 c1 mv step s2: SELECT; step s1: SELECT; step insert: INSERT INTO a VALUES (2); step c2: COMMIT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 2 (1 row) step check1: SELECT check_mv(); check_mv -------- ok (1 row) step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) pg_ivm-1.13/expected/refresh_insert2.out000066400000000000000000000104411507512346500203650ustar00rootroot00000000000000Parsed test spec with 2 sessions starting permutation: s1 refresh s2 insert c1 check2 c2 mv step s1: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step s2: SELECT; step insert: INSERT INTO a VALUES (2); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ok (1 row) starting permutation: s1 refresh s2 c1 insert check2 c2 mv step s1: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step s2: SELECT; step c1: COMMIT; step insert: INSERT INTO a VALUES (2); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 refresh insert c1 check2 c2 mv step s1: SELECT; step s2: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step insert: INSERT INTO a VALUES (2); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 insert refresh c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step insert: INSERT INTO a VALUES (2); step refresh: SELECT pgivm.refresh_immv('mv', true); step c2: COMMIT; step refresh: <... completed> ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 refresh c1 insert check2 c2 mv step s1: SELECT; step s2: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step c1: COMMIT; step insert: INSERT INTO a VALUES (2); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 insert s1 refresh c2 check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); step c2: COMMIT; step refresh: <... completed> ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 insert s1 c2 refresh check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step c2: COMMIT; step refresh: SELECT pgivm.refresh_immv('mv', true); ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 s1 insert c2 refresh check1 c1 mv step s2: SELECT; step s1: SELECT; step insert: INSERT INTO a VALUES (2); step c2: COMMIT; step refresh: SELECT pgivm.refresh_immv('mv', true); ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) pg_ivm-1.13/expected/refresh_insert3.out000066400000000000000000000104411507512346500203660ustar00rootroot00000000000000Parsed test spec with 2 sessions starting permutation: s1 refresh s2 insert c1 check2 c2 mv step s1: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step s2: SELECT; step insert: INSERT INTO a VALUES (2); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ok (1 row) starting permutation: s1 refresh s2 c1 insert check2 c2 mv step s1: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step s2: SELECT; step c1: COMMIT; step insert: INSERT INTO a VALUES (2); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 refresh insert c1 check2 c2 mv step s1: SELECT; step s2: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step insert: INSERT INTO a VALUES (2); ERROR: could not obtain lock on materialized view "mv" during incremental maintenance step c1: COMMIT; step check2: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 (1 row) check_mv -------- ok (1 row) starting permutation: s1 s2 insert refresh c2 check1 c1 mv step s1: SELECT; step s2: SELECT; step insert: INSERT INTO a VALUES (2); step refresh: SELECT pgivm.refresh_immv('mv', true); step c2: COMMIT; step refresh: <... completed> ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s1 s2 refresh c1 insert check2 c2 mv step s1: SELECT; step s2: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); refresh_immv ------------ 1 (1 row) step c1: COMMIT; step insert: INSERT INTO a VALUES (2); step check2: SELECT check_mv(); check_mv -------- ok (1 row) step c2: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 insert s1 refresh c2 check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step refresh: SELECT pgivm.refresh_immv('mv', true); step c2: COMMIT; step refresh: <... completed> ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 insert s1 c2 refresh check1 c1 mv step s2: SELECT; step insert: INSERT INTO a VALUES (2); step s1: SELECT; step c2: COMMIT; step refresh: SELECT pgivm.refresh_immv('mv', true); ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) starting permutation: s2 s1 insert c2 refresh check1 c1 mv step s2: SELECT; step s1: SELECT; step insert: INSERT INTO a VALUES (2); step c2: COMMIT; step refresh: SELECT pgivm.refresh_immv('mv', true); ERROR: the materialized view is incrementally updated in concurrent transaction step check1: SELECT check_mv(); ERROR: current transaction is aborted, commands ignored until end of transaction block step c1: COMMIT; step mv: SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); x|y -+- 1|1 2|2 (2 rows) check_mv -------- ok (1 row) pg_ivm-1.13/matview.c000066400000000000000000004352701507512346500145620ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * matview.c * Incremental view maintenance extension * Routines for incremental maintenance of IMMVs * * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group * Portions Copyright (c) 2022, IVM Development Group * *------------------------------------------------------------------------- */ #include "postgres.h" #include "access/genam.h" #include "access/multixact.h" #include "access/table.h" #include "access/tableam.h" #include "access/xact.h" #include "catalog/pg_depend.h" #include "catalog/heap.h" #include "catalog/pg_collation_d.h" #include "catalog/pg_trigger.h" #include "commands/cluster.h" #include "commands/defrem.h" #include "commands/matview.h" #include "commands/tablecmds.h" #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/spi.h" #include "executor/tstoreReceiver.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "optimizer/optimizer.h" #include "optimizer/prep.h" #include "parser/analyze.h" #include "parser/parse_clause.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" #include "parser/parse_func.h" #include "parser/parse_relation.h" #include "parser/parser.h" #include "parser/parsetree.h" #include "pgstat.h" #include "rewrite/rewriteHandler.h" #include "rewrite/rewriteManip.h" #include "rewrite/rowsecurity.h" #include "storage/lmgr.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/fmgrprotos.h" #include "utils/hsearch.h" #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/snapmgr.h" #include "utils/typcache.h" #include "utils/xid8.h" #include "pg_ivm.h" #define MV_INIT_QUERYHASHSIZE 16 /* MV query type codes */ #define MV_PLAN_RECALC 1 #define MV_PLAN_SET_VALUE 2 /* * MI_QueryKey * * The key identifying a prepared SPI plan in our query hashtable */ typedef struct MV_QueryKey { Oid matview_id; /* OID of materialized view */ int32 query_type; /* query type ID, see MV_PLAN_XXX above */ } MV_QueryKey; /* * MV_QueryHashEntry * * Hash entry for cached plans used to maintain materialized views. */ typedef struct MV_QueryHashEntry { MV_QueryKey key; SPIPlanPtr plan; #if PG_VERSION_NUM < 170000 OverrideSearchPath *search_path; /* search_path used for parsing * and planning */ #else SearchPathMatcher *search_path; /* search_path used for parsing * and planning */ #endif } MV_QueryHashEntry; /* * MV_TriggerHashEntry * * Hash entry for base tables on which IVM trigger is invoked */ typedef struct MV_TriggerHashEntry { Oid matview_id; /* OID of the materialized view */ int before_trig_count; /* count of before triggers invoked */ int after_trig_count; /* count of after triggers invoked */ Snapshot snapshot; /* Snapshot just before table change */ List *tables; /* List of MV_TriggerTable */ bool has_old; /* tuples are deleted from any table? */ bool has_new; /* tuples are inserted into any table? */ /* * List of sub-transaction IDs that incrementally updated the view. * This list is maintained through a transaction, and an ID is removed * when a sub-transaction is aborted. If any ID is left when the * transaction is committed, this means the view is incrementally * updated in this transaction. */ List *subxids; } MV_TriggerHashEntry; /* * MV_TriggerTable * * IVM related data for tables on which the trigger is invoked. */ typedef struct MV_TriggerTable { Oid table_id; /* OID of the modified table */ List *old_tuplestores; /* tuplestores for deleted tuples */ List *new_tuplestores; /* tuplestores for inserted tuples */ List *old_rtes; /* RTEs of ENRs for old_tuplestores*/ List *new_rtes; /* RTEs of ENRs for new_tuplestores */ List *rte_paths; /* List of paths to RTE index of the modified table */ RangeTblEntry *original_rte; /* the original RTE saved before rewriting query */ Relation rel; /* relation of the modified table */ TupleTableSlot *slot; /* for checking visibility in the pre-state table */ } MV_TriggerTable; static HTAB *mv_query_cache = NULL; static HTAB *mv_trigger_info = NULL; static bool in_delta_calculation = false; /* kind of IVM operation for the view */ typedef enum { IVM_ADD, IVM_SUB } IvmOp; /* ENR name for materialized view delta */ #define NEW_DELTA_ENRNAME "new_delta" #define OLD_DELTA_ENRNAME "old_delta" /* * Term * * An outer join query can be transformed into a normalized form that * consists of one or more terms. The normalized form is a bag union of * terms, and each term is an inner join of some base tables, which is * usually null-extended due to one or more anti-joins with other base * tables. * * relids: base tables inner-joined in this term * anti_relids: list of Relids, each representing base tables that are * anti-joined in each anti-join in this term */ typedef struct { Relids relids; List *anti_relids; } Term; /* * OuterJoinRelInfo: contains a list of resnames in the view target list * for key attributes of the base tables participating in the outer join. */ typedef struct OuterJoinRelInfo { int rtindex; List *key_attrs; } OuterJoinRelInfo; static int immv_maintenance_depth = 0; static uint64 refresh_immv_datafill(DestReceiver *dest, Query *query, QueryEnvironment *queryEnv, TupleDesc *resultTupleDesc, const char *queryString); static void refresh_by_heap_swap(Oid matviewOid, Oid OIDNewHeap, char relpersistence); static void OpenImmvIncrementalMaintenance(void); static void CloseImmvIncrementalMaintenance(void); static Query *rewrite_query_for_preupdate_state(Query *query, List *tables, ParseState *pstate, List *rte_path, Oid matviewid); static void register_delta_ENRs(ParseState *pstate, Query *query, List *tables); static char*make_subquery_targetlist_from_table(MV_TriggerTable *table); static char *make_delta_enr_name(const char *prefix, Oid relid, int count); static RangeTblEntry *get_prestate_rte(RangeTblEntry *rte, MV_TriggerTable *table, QueryEnvironment *queryEnv, Oid matviewid); static RangeTblEntry *union_ENRs(RangeTblEntry *rte, MV_TriggerTable *table, List *enr_rtes, const char *prefix, QueryEnvironment *queryEnv); static Query *rewrite_query_for_distinct_and_aggregates(Query *query, ParseState *pstate); static List *get_normalized_form(Query *query, Node *jtnode, Relids outer_join_rels, List **outerjoin_quals); static List *multiply_terms(Query *query, List *terms1, List *terms2, Node* qual, JoinType jointype, Relids outer_join_rels); static Query *rewrite_query_for_outerjoin(Query *query, int index, List *terms); static bool rewrite_jointype(Query *query, Node *node, int index, Relids *relids, List **reduced_outerjoins, List **unreduced_relids); static void calc_delta(MV_TriggerTable *table, List *rte_path, Query *query, DestReceiver *dest_old, DestReceiver *dest_new, TupleDesc *tupdesc_old, TupleDesc *tupdesc_new, QueryEnvironment *queryEnv); static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, List *rte_path); static ListCell *getRteListCell(Query *query, List *rte_path); static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores,Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, Query *query, bool use_count, char *count_colname, List *terms, List *rte_path, List *outerjoin_relinfo); static void append_set_clause_for_count(const char *resname, StringInfo buf_old, StringInfo buf_new,StringInfo aggs_list); static void append_set_clause_for_sum(const char *resname, StringInfo buf_old, StringInfo buf_new, StringInfo aggs_list); static void append_set_clause_for_avg(const char *resname, StringInfo buf_old, StringInfo buf_new, StringInfo aggs_list, const char *aggtype); static void append_set_clause_for_minmax(const char *resname, StringInfo buf_old, StringInfo buf_new, StringInfo aggs_list, bool is_min); static char *get_operation_string(IvmOp op, const char *col, const char *arg1, const char *arg2, const char* count_col, const char *castType); static char *get_null_condition_string(IvmOp op, const char *arg1, const char *arg2, const char* count_col); static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys); static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, List *keys, StringInfo aggs_list, StringInfo aggs_set, List *minmax_list, List *is_min_list, const char *count_colname, SPITupleTable **tuptable_recalc, uint64 *num_recalc); static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list); static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, List *keys, StringInfo target_list, StringInfo aggs_set, const char* count_colname, bool distinct); static char *get_matching_condition_string(List *keys); static char *get_returning_string(List *minmax_list, List *is_min_list, List *keys); static char *get_minmax_recalc_condition_string(List *minmax_list, List *is_min_list); static char *get_select_for_recalc_string(List *keys); static void recalc_and_set_values(SPITupleTable *tuptable_recalc, int64 num_tuples, List *namelist, List *keys, Relation matviewRel); static SPIPlanPtr get_plan_for_recalc(Relation matviewRel, List *namelist, List *keys, Oid *keyTypes); static SPIPlanPtr get_plan_for_set_values(Relation matviewRel, List *namelist, Oid *valTypes); static void insert_dangling_tuples(List *terms, Query *query, Relation matviewRel, const char *deltaname_old, bool use_count, int index, List *outerjoin_relinfo); static void delete_dangling_tuples(List *terms, Query *query, Relation matviewRel, const char *deltaname_new, int index, List *outerjoin_relinfo); static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop); static void mv_InitHashTables(void); static SPIPlanPtr mv_FetchPreparedPlan(MV_QueryKey *key); static void mv_HashPreparedPlan(MV_QueryKey *key, SPIPlanPtr plan); static void mv_BuildQueryKey(MV_QueryKey *key, Oid matview_id, int32 query_type); static void clean_up_IVM_hash_entry(MV_TriggerHashEntry *entry, bool is_abort, SubTransactionId subxid); static void setLastUpdateXid(Oid immv_oid, FullTransactionId xid); static FullTransactionId getLastUpdateXid(Oid immv_oid); /* SQL callable functions */ PG_FUNCTION_INFO_V1(IVM_immediate_before); PG_FUNCTION_INFO_V1(IVM_immediate_maintenance); PG_FUNCTION_INFO_V1(ivm_visible_in_prestate); /* * ExecRefreshImmv -- execute a refresh_immv() function * * This imitates PostgreSQL's ExecRefreshMatView(). */ ObjectAddress ExecRefreshImmv(const RangeVar *relation, bool skipData, const char *queryString, QueryCompletion *qc) { Oid matviewOid; LOCKMODE lockmode; /* Determine strength of lock needed. */ //concurrent = stmt->concurrent; //lockmode = concurrent ? ExclusiveLock : AccessExclusiveLock; lockmode = AccessExclusiveLock; /* * Get a lock until end of transaction. */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM < 170000) matviewOid = RangeVarGetRelidExtended(relation, lockmode, 0, RangeVarCallbackOwnsTable, NULL); #else matviewOid = RangeVarGetRelidExtended(relation, lockmode, 0, RangeVarCallbackMaintainsTable, NULL); #endif return RefreshImmvByOid(matviewOid, false, skipData, queryString, qc); } /* * RefreshMatViewByOid -- refresh IMMV view by OID * * This is also used to populate the IMMV created by create_immv command. * * This imitates PostgreSQL's RefreshMatViewByOid(). */ ObjectAddress RefreshImmvByOid(Oid matviewOid, bool is_create, bool skipData, const char *queryString, QueryCompletion *qc) { Relation matviewRel; Query *dataQuery = NULL; /* initialized to keep compiler happy */ Query *viewQuery; Oid tableSpace; Oid relowner; Oid OIDNewHeap; DestReceiver *dest; uint64 processed = 0; char relpersistence; Oid save_userid; int save_sec_context; int save_nestlevel; ObjectAddress address; bool oldPopulated; Relation pgIvmImmv; TupleDesc tupdesc; ScanKeyData key; SysScanDesc scan; HeapTuple tup; bool isnull; Datum datum; matviewRel = table_open(matviewOid, NoLock); relowner = matviewRel->rd_rel->relowner; /* * Switch to the owner's userid, so that any functions are run as that * user. Also lock down security-restricted operations and arrange to * make GUC variable changes local to this command. */ GetUserIdAndSecContext(&save_userid, &save_sec_context); SetUserIdAndSecContext(relowner, save_sec_context | SECURITY_RESTRICTED_OPERATION); save_nestlevel = NewGUCNestLevel(); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 170000) RestrictSearchPath(); #endif /* * Make sure it is an IMMV: * Get the entry in pg_ivm_immv. If it doesn't exist, the relation * is not IMMV. */ pgIvmImmv = table_open(PgIvmImmvRelationId(), RowExclusiveLock); tupdesc = RelationGetDescr(pgIvmImmv); ScanKeyInit(&key, Anum_pg_ivm_immv_immvrelid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(matviewRel))); scan = systable_beginscan(pgIvmImmv, PgIvmImmvPrimaryKeyIndexId(), true, NULL, 1, &key); tup = systable_getnext(scan); if (!HeapTupleIsValid(tup)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("\"%s\" is not an IMMV", RelationGetRelationName(matviewRel)))); datum = heap_getattr(tup, Anum_pg_ivm_immv_ispopulated, tupdesc, &isnull); Assert(!isnull); oldPopulated = DatumGetBool(datum); /* * 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 TABLE_INSERT_FROZEN. */ CheckTableNotInUse(matviewRel, "refresh an IMMV"); /* Tentatively mark the IMMV as populated or not (this will roll back * if we fail later). */ if (skipData != (!oldPopulated)) { Datum values[Natts_pg_ivm_immv]; bool nulls[Natts_pg_ivm_immv]; bool replaces[Natts_pg_ivm_immv]; HeapTuple newtup = NULL; memset(values, 0, sizeof(values)); values[Anum_pg_ivm_immv_ispopulated -1 ] = BoolGetDatum(!skipData); MemSet(nulls, false, sizeof(nulls)); MemSet(replaces, false, sizeof(replaces)); replaces[Anum_pg_ivm_immv_ispopulated -1 ] = true; newtup = heap_modify_tuple(tup, tupdesc, values, nulls, replaces); CatalogTupleUpdate(pgIvmImmv, &newtup->t_self, newtup); heap_freetuple(newtup); /* * Advance command counter to make the updated pg_ivm_immv row locally * visible. */ CommandCounterIncrement(); } systable_endscan(scan); table_close(pgIvmImmv, NoLock); viewQuery = get_immv_query(matviewRel); /* For IMMV, we need to rewrite matview query */ if (!skipData) dataQuery = rewriteQueryForIMMV(viewQuery,NIL); tableSpace = matviewRel->rd_rel->reltablespace; relpersistence = matviewRel->rd_rel->relpersistence; /* delete IMMV triggers. */ if (skipData) { Relation tgRel; Relation depRel; ObjectAddresses *immv_triggers; immv_triggers = new_object_addresses(); tgRel = table_open(TriggerRelationId, RowExclusiveLock); depRel = table_open(DependRelationId, RowExclusiveLock); /* search triggers that depends on IMMV. */ ScanKeyInit(&key, Anum_pg_depend_refobjid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(matviewOid)); scan = systable_beginscan(depRel, DependReferenceIndexId, true, NULL, 1, &key); while ((tup = systable_getnext(scan)) != NULL) { ObjectAddress obj; Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(tup); if (foundDep->classid == TriggerRelationId) { HeapTuple tgtup; ScanKeyData tgkey[1]; SysScanDesc tgscan; Form_pg_trigger tgform; /* Find the trigger name. */ ScanKeyInit(&tgkey[0], Anum_pg_trigger_oid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(foundDep->objid)); tgscan = systable_beginscan(tgRel, TriggerOidIndexId, true, NULL, 1, tgkey); tgtup = systable_getnext(tgscan); if (!HeapTupleIsValid(tgtup)) elog(ERROR, "could not find tuple for immv trigger %u", foundDep->objid); tgform = (Form_pg_trigger) GETSTRUCT(tgtup); /* If trigger is created by IMMV, delete it. */ if (strncmp(NameStr(tgform->tgname), "IVM_trigger_", 12) == 0) { obj.classId = foundDep->classid; obj.objectId = foundDep->objid; obj.objectSubId = foundDep->refobjsubid; add_exact_object_address(&obj, immv_triggers); } systable_endscan(tgscan); } } systable_endscan(scan); performMultipleDeletions(immv_triggers, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); table_close(depRel, RowExclusiveLock); table_close(tgRel, RowExclusiveLock); free_object_addresses(immv_triggers); } /* * Create triggers on incremental maintainable materialized view * This argument should use 'dataQuery'. This needs to use a rewritten query, * because a sublink in jointree is not supported by this function. * * This is performed before generating data because we have to wait * concurrent transactions modifying a base table and then take a snapshot * to see changes by these transactions to make sure a consistent view * is created. */ if (!skipData && !oldPopulated) CreateIvmTriggersOnBaseTables(dataQuery, matviewOid); /* * Create the transient table that will receive the regenerated data. Lock * it against access by any other process until commit (by which time it * will be gone). */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 150000) OIDNewHeap = make_new_heap(matviewOid, tableSpace, matviewRel->rd_rel->relam, relpersistence, ExclusiveLock); #else OIDNewHeap = make_new_heap(matviewOid, tableSpace, relpersistence, ExclusiveLock); #endif LockRelationOid(OIDNewHeap, AccessExclusiveLock); dest = CreateTransientRelDestReceiver(OIDNewHeap); /* * In READ COMMITTED, get and push the latest snapshot again to see the * results of concurrent transactions committed after the current * transaction started. */ if (!IsolationUsesXactSnapshot()) PushActiveSnapshot(GetTransactionSnapshot()); /* * If a concurrent transaction updated the view incrementally and was * committed before we acquired the lock, the results of refresh_immv could * be inconsistent. Therefore, we have to check the transaction ID of the * most recent update of the view, and if this was in progress at the * transaction start, raise an error to prevent anomalies. */ if (!is_create) { FullTransactionId xid; xid = getLastUpdateXid(matviewOid); if (XidInMVCCSnapshot(XidFromFullTransactionId(xid), GetActiveSnapshot())) ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("the materialized view is incrementally updated in concurrent transaction"), errhint("The transaction might succeed if retried."))); } /* Generate the data, if wanted. */ if (!skipData) processed = refresh_immv_datafill(dest, dataQuery, NULL, NULL, queryString); /* Pop the original snapshot. */ if (!IsolationUsesXactSnapshot()) PopActiveSnapshot(); /* Make the matview match the newly generated data. */ refresh_by_heap_swap(matviewOid, OIDNewHeap, relpersistence); /* * Inform cumulative stats system about our activity: basically, we * truncated the matview and inserted some new data. (The concurrent * code path above doesn't need to worry about this because the * inserts and deletes it issues get counted by lower-level code.) */ pgstat_count_truncate(matviewRel); if (!skipData) pgstat_count_heap_insert(matviewRel, processed); table_close(matviewRel, NoLock); /* Roll back any GUC changes */ AtEOXact_GUC(false, save_nestlevel); /* Restore userid and security context */ SetUserIdAndSecContext(save_userid, save_sec_context); ObjectAddressSet(address, RelationRelationId, matviewOid); /* * Save the rowcount so that pg_stat_statements can track the total number * of rows processed by REFRESH MATERIALIZED VIEW command. Note that we * still don't display the rowcount in the command completion tag output, * i.e., the display_rowcount flag of CMDTAG_REFRESH_MATERIALIZED_VIEW * command tag is left false in cmdtaglist.h. Otherwise, the change of * completion tag output might break applications using it. */ if (qc) SetQueryCompletion(qc, CMDTAG_REFRESH_MATERIALIZED_VIEW, processed); return address; } /* * refresh_immv_datafill * * Execute the given query, sending result rows to "dest" (which will * insert them into the target matview). * * Returns number of rows inserted. */ static uint64 refresh_immv_datafill(DestReceiver *dest, Query *query, QueryEnvironment *queryEnv, TupleDesc *resultTupleDesc, const char *queryString) { List *rewritten; PlannedStmt *plan; QueryDesc *queryDesc; Query *copied_query; uint64 processed; /* Lock and rewrite, using a copy to preserve the original query. */ copied_query = copyObject(query); AcquireRewriteLocks(copied_query, true, false); rewritten = QueryRewrite(copied_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(); /* Plan the query which will generate data for the refresh. */ plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, 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, queryEnv ? queryEnv: NULL, 0); /* call ExecutorStart to prepare the plan for execution */ ExecutorStart(queryDesc, 0); /* run the plan */ #if PG_VERSION_NUM < 180000 ExecutorRun(queryDesc, ForwardScanDirection, 0, true); #else ExecutorRun(queryDesc, ForwardScanDirection, 0); #endif processed = queryDesc->estate->es_processed; if (resultTupleDesc) *resultTupleDesc = CreateTupleDescCopy(queryDesc->tupDesc); /* and clean up */ ExecutorFinish(queryDesc); ExecutorEnd(queryDesc); FreeQueryDesc(queryDesc); PopActiveSnapshot(); return processed; } /* * Swap the physical files of the target and transient tables, then rebuild * the target's indexes and throw away the transient table. Security context * swapping is handled by the called function, so it is not needed here. */ static void refresh_by_heap_swap(Oid matviewOid, Oid OIDNewHeap, char relpersistence) { finish_heap_swap(matviewOid, OIDNewHeap, false, false, true, true, RecentXmin, ReadNextMultiXactId(), relpersistence); } /* * This should be used to test whether the backend is in a context where it is * OK to allow DML statements to modify IMMVs. We only want to * allow that for internal code driven by the IMMV definition, * not for arbitrary user-supplied code. */ bool ImmvIncrementalMaintenanceIsEnabled(void) { return immv_maintenance_depth > 0; } static void OpenImmvIncrementalMaintenance(void) { immv_maintenance_depth++; } static void CloseImmvIncrementalMaintenance(void) { immv_maintenance_depth--; Assert(immv_maintenance_depth >= 0); } /* * get_immv_query - get the Query of IMMV. */ Query * get_immv_query(Relation matviewRel) { Relation pgIvmImmv = table_open(PgIvmImmvRelationId(), AccessShareLock); TupleDesc tupdesc = RelationGetDescr(pgIvmImmv); SysScanDesc scan; ScanKeyData key; HeapTuple tup; bool isnull; Datum datum; Query *query; ScanKeyInit(&key, Anum_pg_ivm_immv_immvrelid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(matviewRel))); scan = systable_beginscan(pgIvmImmv, PgIvmImmvPrimaryKeyIndexId(), true, NULL, 1, &key); tup = systable_getnext(scan); if (!HeapTupleIsValid(tup)) { systable_endscan(scan); table_close(pgIvmImmv, NoLock); return NULL; } datum = heap_getattr(tup, Anum_pg_ivm_immv_viewdef, tupdesc, &isnull); Assert(!isnull); query = (Query *) stringToNode(TextDatumGetCString(datum)); systable_endscan(scan); table_close(pgIvmImmv, NoLock); return query; } static Tuplestorestate * tuplestore_copy(Tuplestorestate *tuplestore, Relation rel) { Tuplestorestate *res = NULL; TupleDesc tupdesc = RelationGetDescr(rel); TupleTableSlot *slot = MakeSingleTupleTableSlot(tupdesc, &TTSOpsMinimalTuple); tuplestore_rescan(tuplestore); res = tuplestore_begin_heap(false, false, work_mem); while (tuplestore_gettupleslot(tuplestore, true, false, slot)) tuplestore_puttupleslot(res, slot); ExecDropSingleTupleTableSlot(slot); return res; } /* ---------------------------------------------------- * Incremental View Maintenance routines * --------------------------------------------------- */ /* * IVM_immediate_before * * IVM trigger function invoked before base table is modified. If this is * invoked firstly in the same statement, we save the transaction id and the * command id at that time. */ Datum IVM_immediate_before(PG_FUNCTION_ARGS) { TriggerData *trigdata = (TriggerData *) fcinfo->context; char *matviewOid_text = trigdata->tg_trigger->tgargs[0]; char *ex_lock_text = trigdata->tg_trigger->tgargs[1]; Oid matviewOid; MV_TriggerHashEntry *entry; bool found; bool ex_lock; matviewOid = DatumGetObjectId(DirectFunctionCall1(oidin, CStringGetDatum(matviewOid_text))); ex_lock = DatumGetBool(DirectFunctionCall1(boolin, CStringGetDatum(ex_lock_text))); /* If the view has more than one tables, we have to use an exclusive lock. */ if (ex_lock) { FullTransactionId xid; /* * Wait for concurrent transactions which update this materialized view at * READ COMMITED. This is needed to see changes committed in other * transactions. No wait and raise an error at REPEATABLE READ or * SERIALIZABLE to prevent update anomalies of matviews. * XXX: dead-lock is possible here. */ if (!IsolationUsesXactSnapshot()) LockRelationOid(matviewOid, ExclusiveLock); else if (!ConditionalLockRelationOid(matviewOid, ExclusiveLock)) { /* try to throw error by name; relation could be deleted... */ char *relname = get_rel_name(matviewOid); if (!relname) ereport(ERROR, (errcode(ERRCODE_LOCK_NOT_AVAILABLE), errmsg("could not obtain lock on materialized view during incremental maintenance"))); ereport(ERROR, (errcode(ERRCODE_LOCK_NOT_AVAILABLE), errmsg("could not obtain lock on materialized view \"%s\" during incremental maintenance", relname))); } /* * Even if we can acquire an lock, a concurrent transaction could have * updated the view incrementally and been committed before we acquired * the lock. Therefore, we have to check the transaction ID of the most * recent update of the view, and if this was in progress at the * transaction start, raise an error to prevent anomalies. */ xid = getLastUpdateXid(matviewOid); if (XidInMVCCSnapshot(XidFromFullTransactionId(xid), GetTransactionSnapshot())) ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("the materialized view is incrementally updated in concurrent transaction"), errhint("The transaction might succeed if retried."))); } else LockRelationOid(matviewOid, RowExclusiveLock); /* * On the first call initialize the hashtable */ if (!mv_trigger_info) mv_InitHashTables(); entry = (MV_TriggerHashEntry *) hash_search(mv_trigger_info, (void *) &matviewOid, HASH_ENTER, &found); /* On the first BEFORE to update the view, initialize trigger data */ if (!found || entry->snapshot == InvalidSnapshot) { Snapshot snapshot; /* * Get a snapshot just before the table was modified for checking * tuple visibility in the pre-update state of the table. * * In READ COMMITTED, use the latest snapshot again to see the * results of concurrent transactions committed after the current * transaction started. */ if (IsolationUsesXactSnapshot()) snapshot = GetActiveSnapshot(); else snapshot = GetTransactionSnapshot(); entry->matview_id = matviewOid; entry->before_trig_count = 0; entry->after_trig_count = 0; entry->snapshot = RegisterSnapshot(snapshot); entry->tables = NIL; entry->has_old = false; entry->has_new = false; /* * If this is the first table modifying query in the transaction, * initialize the list of subxids. */ if (!found) entry->subxids = NIL; } entry->before_trig_count++; return PointerGetDatum(NULL); } /* * IVM_immediate_maintenance * * IVM trigger function invoked after base table is modified. * For each table, tuplestores of transition tables are collected. * and after the last modification */ Datum IVM_immediate_maintenance(PG_FUNCTION_ARGS) { TriggerData *trigdata = (TriggerData *) fcinfo->context; Relation rel; Oid relid; Oid matviewOid; Query *query; Query *rewritten = NULL; char *matviewOid_text = trigdata->tg_trigger->tgargs[0]; Relation matviewRel; int old_depth = immv_maintenance_depth; SubTransactionId subxid; Oid relowner; Tuplestorestate *old_tuplestore = NULL; Tuplestorestate *new_tuplestore = NULL; DestReceiver *dest_new = NULL, *dest_old = NULL; Oid save_userid; int save_sec_context; int save_nestlevel; MV_TriggerHashEntry *entry; MV_TriggerTable *table; bool found; bool hasOuterJoins = false; Relids outer_join_rels = NULL; /* relids of outer-join relations */ List *terms = NIL; /* terms in a normalized form of outer-join query */ List *outerjoin_quals = NIL; /* quals in outer-join conditions */ List *outerjoin_relinfo = NIL; /* key attribute information of base tables in the outer join */ ParseState *pstate; QueryEnvironment *queryEnv = create_queryEnv(); MemoryContext oldcxt; ListCell *lc; int i; /* Create a ParseState for rewriting the view definition query */ pstate = make_parsestate(NULL); pstate->p_queryEnv = queryEnv; pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; rel = trigdata->tg_relation; relid = rel->rd_id; matviewOid = DatumGetObjectId(DirectFunctionCall1(oidin, CStringGetDatum(matviewOid_text))); /* * On the first call initialize the hashtable */ if (!mv_trigger_info) mv_InitHashTables(); /* get the entry for this materialized view */ entry = (MV_TriggerHashEntry *) hash_search(mv_trigger_info, (void *) &matviewOid, HASH_FIND, &found); Assert (found && entry != NULL); entry->after_trig_count++; /* search the entry for the modified table and create new entry if not found */ found = false; foreach(lc, entry->tables) { table = (MV_TriggerTable *) lfirst(lc); if (table->table_id == relid) { found = true; break; } } if (!found) { oldcxt = MemoryContextSwitchTo(TopTransactionContext); table = (MV_TriggerTable *) palloc0(sizeof(MV_TriggerTable)); table->table_id = relid; table->old_tuplestores = NIL; table->new_tuplestores = NIL; table->old_rtes = NIL; table->new_rtes = NIL; table->rte_paths = NIL; table->slot = MakeSingleTupleTableSlot(RelationGetDescr(rel), table_slot_callbacks(rel)); /* We assume we have at least RowExclusiveLock on modified tables. */ table->rel = table_open(RelationGetRelid(rel), NoLock); entry->tables = lappend(entry->tables, table); MemoryContextSwitchTo(oldcxt); } /* Save the transition tables and make a request to not free immediately */ if (trigdata->tg_oldtable) { oldcxt = MemoryContextSwitchTo(TopTransactionContext); table->old_tuplestores = lappend(table->old_tuplestores, tuplestore_copy(trigdata->tg_oldtable, rel)); entry->has_old = true; MemoryContextSwitchTo(oldcxt); } if (trigdata->tg_newtable) { oldcxt = MemoryContextSwitchTo(TopTransactionContext); table->new_tuplestores = lappend(table->new_tuplestores, tuplestore_copy(trigdata->tg_newtable, rel)); entry->has_new = true; MemoryContextSwitchTo(oldcxt); } /* If this is not the last AFTER trigger call, immediately exit. */ Assert (entry->before_trig_count >= entry->after_trig_count); if (entry->before_trig_count != entry->after_trig_count) return PointerGetDatum(NULL); /* * If this is the last AFTER trigger call, continue and update the view. */ /* * record the subxid that updated the view incrementally * * Note: * PG16 or later has list_member_xid and lappend_xid. It would be better * to use them, but we use integer for supporting older PGs since there * is no problem or now. */ subxid = GetCurrentSubTransactionId(); if (!list_member_int(entry->subxids, subxid)) { oldcxt = MemoryContextSwitchTo(TopTransactionContext); entry->subxids = lappend_int(entry->subxids, subxid); MemoryContextSwitchTo(oldcxt); } /* * Advance command counter to make the updated base table rows locally * visible. */ CommandCounterIncrement(); matviewRel = table_open(matviewOid, NoLock); /* Make sure IMMV is a table. */ Assert(matviewRel->rd_rel->relkind == RELKIND_RELATION); /* * In READ COMMITTED, get and push the latest snapshot again to see the * results of concurrent transactions committed after the current * transaction started. */ if (!IsolationUsesXactSnapshot()) PushActiveSnapshot(GetTransactionSnapshot()); /* * 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 TABLE_INSERT_FROZEN. */ CheckTableNotInUse(matviewRel, "refresh an IMMV incrementally"); /* * Switch to the owner's userid, so that any functions are run as that * user. Also arrange to make GUC variable changes local to this command. * We will switch modes when we are about to execute user code. */ relowner = matviewRel->rd_rel->relowner; GetUserIdAndSecContext(&save_userid, &save_sec_context); SetUserIdAndSecContext(relowner, save_sec_context | SECURITY_RESTRICTED_OPERATION); save_nestlevel = NewGUCNestLevel(); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 170000) RestrictSearchPath(); #endif /* get view query*/ query = get_immv_query(matviewRel); /* join tree analysis for outer join */ i = 1; foreach(lc, query->rtable) { RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc); if (rte->rtekind == RTE_JOIN && IS_OUTER_JOIN(rte->jointype)) { hasOuterJoins = true; outer_join_rels = bms_add_member(outer_join_rels, i); } i++; } if (hasOuterJoins) { Relids all_qual_vars; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) PlannerInfo root; #endif terms = get_normalized_form(query, (Node *) query->jointree, outer_join_rels, &outerjoin_quals); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) outerjoin_quals = (List *) flatten_join_alias_vars(NULL, query, (Node *) outerjoin_quals); #else outerjoin_quals = (List *) flatten_join_alias_vars(query, (Node *) outerjoin_quals); #endif #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) all_qual_vars = pull_varnos(&root, (Node *) outerjoin_quals); #else all_qual_vars = pull_varnos((Node *) outerjoin_quals); #endif all_qual_vars = bms_del_members(all_qual_vars, outer_join_rels); /* Collect key resnames for each outer-joined table */ i = -1; while ((i = bms_next_member(all_qual_vars, i)) >= 0) { OuterJoinRelInfo *info; found = false; foreach(lc, outerjoin_relinfo) { info = (OuterJoinRelInfo *) lfirst(lc); if (info->rtindex == i) { found = true; break; } } if (!found) { info = palloc(sizeof(OuterJoinRelInfo)); info->rtindex = i; info->key_attrs = NIL; outerjoin_relinfo = lappend(outerjoin_relinfo, info); } foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); Var *var; if (tle->resjunk) continue; if (!IsA(tle->expr, Var)) continue; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) var = (Var *) flatten_join_alias_vars(NULL, query, (Node *) tle->expr); #else var = (Var *) flatten_join_alias_vars(query, (Node *) tle->expr); #endif if (var->varno == i) info->key_attrs = lappend(info->key_attrs, attr); } } bms_free(all_qual_vars); } /* * When a base table is truncated, the view content will be empty if the * view definition query does not contain an outer join or an aggregate * without a GROUP clause. Therefore, such views can be truncated. * * Aggregate views without a GROUP clause always have one row. Therefore, * if a base table is truncated, the view will not be empty and will contain * a row with NULL value (or 0 for count()). So, in this case, we refresh the * view instead of truncating it. * * If you have an outer join, truncating a base table will not empty the * join result. Therefore, refresh the view in this case as well. */ if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event)) { if (!hasOuterJoins && !(query->hasAggs && query->groupClause == NIL)) { OpenImmvIncrementalMaintenance(); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) ExecuteTruncateGuts(list_make1(matviewRel), list_make1_oid(matviewOid), NIL, DROP_RESTRICT, false, false); #else ExecuteTruncateGuts(list_make1(matviewRel), list_make1_oid(matviewOid), NIL, DROP_RESTRICT, false); #endif CloseImmvIncrementalMaintenance(); } else { Oid OIDNewHeap; DestReceiver *dest; uint64 processed = 0; Query *dataQuery = rewriteQueryForIMMV(query, NIL); char relpersistence = matviewRel->rd_rel->relpersistence; /* * Create the transient table that will receive the regenerated data. Lock * it against access by any other process until commit (by which time it * will be gone). */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 150000) OIDNewHeap = make_new_heap(matviewOid, matviewRel->rd_rel->reltablespace, matviewRel->rd_rel->relam, relpersistence, ExclusiveLock); #else OIDNewHeap = make_new_heap(matviewOid, matviewRel->rd_rel->reltablespace, relpersistence, ExclusiveLock); #endif LockRelationOid(OIDNewHeap, AccessExclusiveLock); dest = CreateTransientRelDestReceiver(OIDNewHeap); /* Generate the data */ processed = refresh_immv_datafill(dest, dataQuery, NULL, NULL, ""); refresh_by_heap_swap(matviewOid, OIDNewHeap, relpersistence); /* Inform cumulative stats system about our activity */ pgstat_count_truncate(matviewRel); pgstat_count_heap_insert(matviewRel, processed); } /* Clean up hash entry and delete tuplestores */ clean_up_IVM_hash_entry(entry, false, InvalidSubTransactionId); /* Pop the original snapshot. */ if (!IsolationUsesXactSnapshot()) PopActiveSnapshot(); table_close(matviewRel, NoLock); /* Roll back any GUC changes */ AtEOXact_GUC(false, save_nestlevel); /* Restore userid and security context */ SetUserIdAndSecContext(save_userid, save_sec_context); return PointerGetDatum(NULL); } /* * rewrite query for calculating deltas */ rewritten = copyObject(query); /* Replace resnames in a target list with materialized view's attnames */ i = 0; foreach (lc, rewritten->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, i); char *resname = NameStr(attr->attname); tle->resname = pstrdup(resname); i++; } /* Rewrite for the EXISTS clause */ if (rewritten->hasSubLinks) rewrite_query_for_exists_subquery(rewritten); /* Set all tables in the query to pre-update state */ rewritten = rewrite_query_for_preupdate_state(rewritten, entry->tables, pstate, NIL, matviewOid); /* Rewrite for DISTINCT clause and aggregates functions */ rewritten = rewrite_query_for_distinct_and_aggregates(rewritten, pstate); /* Create tuplestores to store view deltas */ if (entry->has_old) { oldcxt = MemoryContextSwitchTo(TopTransactionContext); old_tuplestore = tuplestore_begin_heap(false, false, work_mem); dest_old = CreateDestReceiver(DestTuplestore); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) SetTuplestoreDestReceiverParams(dest_old, old_tuplestore, TopTransactionContext, false, NULL, NULL); #else SetTuplestoreDestReceiverParams(dest_old, old_tuplestore, TopTransactionContext, false); #endif MemoryContextSwitchTo(oldcxt); } if (entry->has_new) { oldcxt = MemoryContextSwitchTo(TopTransactionContext); new_tuplestore = tuplestore_begin_heap(false, false, work_mem); dest_new = CreateDestReceiver(DestTuplestore); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) SetTuplestoreDestReceiverParams(dest_new, new_tuplestore, TopTransactionContext, false, NULL, NULL); #else SetTuplestoreDestReceiverParams(dest_new, new_tuplestore, TopTransactionContext, false); #endif MemoryContextSwitchTo(oldcxt); } /* for all modified tables */ foreach(lc, entry->tables) { ListCell *lc2; table = (MV_TriggerTable *) lfirst(lc); /* loop for self-join */ foreach(lc2, table->rte_paths) { List *rte_path = lfirst(lc2); Query *querytree = rewritten; RangeTblEntry *rte; TupleDesc tupdesc_old; TupleDesc tupdesc_new; Query *query_for_delta; bool in_exists = false; bool use_count = false; char *count_colname = NULL; /* check if the modified table is in EXISTS clause. */ for (i = 0; i < list_length(rte_path); i++) { int index = lfirst_int(list_nth_cell(rte_path, i)); rte = (RangeTblEntry *) lfirst(list_nth_cell(querytree->rtable, index - 1)); if (rte != NULL && rte->rtekind == RTE_SUBQUERY) { querytree = rte->subquery; if (rte->lateral) { int attnum; count_colname = getColumnNameStartWith(rte, "__ivm_exists", &attnum); if (count_colname) { use_count = true; in_exists = true; } } } } if (count_colname == NULL && (query->hasAggs || query->distinctClause)) { count_colname = pstrdup("__ivm_count__"); use_count = true; } /* For outer join query, we need additional rewrites. */ if (!in_exists && hasOuterJoins) { int index; /* We do not assume nested queries */ Assert(list_length(rte_path) == 1); index = linitial_int(rte_path); query_for_delta = rewrite_query_for_outerjoin(rewritten, index, terms); } else query_for_delta = rewritten; /* calculate delta tables */ calc_delta(table, rte_path, query_for_delta, dest_old, dest_new, &tupdesc_old, &tupdesc_new, queryEnv); /* Set the table in the query to post-update state */ rewritten = rewrite_query_for_postupdate_state(rewritten, table, rte_path); PG_TRY(); { /* apply the delta tables to the materialized view */ apply_delta(matviewOid, old_tuplestore, new_tuplestore, tupdesc_old, tupdesc_new, query, use_count, count_colname, terms, rte_path, outerjoin_relinfo); } PG_CATCH(); { immv_maintenance_depth = old_depth; PG_RE_THROW(); } PG_END_TRY(); /* clear view delta tuplestores */ if (old_tuplestore) tuplestore_clear(old_tuplestore); if (new_tuplestore) tuplestore_clear(new_tuplestore); } } /* Clean up hash entry and delete tuplestores */ clean_up_IVM_hash_entry(entry, false, InvalidSubTransactionId); if (old_tuplestore) { dest_old->rDestroy(dest_old); tuplestore_end(old_tuplestore); } if (new_tuplestore) { dest_new->rDestroy(dest_new); tuplestore_end(new_tuplestore); } /* Clean up resources for outer join maintenance */ if (hasOuterJoins) { bms_free(outer_join_rels); foreach(lc, terms) { Term *term = (Term *) lfirst(lc); bms_free(term->relids); list_free(term->anti_relids); pfree(term); } foreach(lc, outerjoin_relinfo) { OuterJoinRelInfo *info = (OuterJoinRelInfo *) lfirst(lc); pfree(info); } list_free(terms); list_free(outerjoin_quals); list_free(outerjoin_relinfo); } /* Pop the original snapshot. */ if (!IsolationUsesXactSnapshot()) PopActiveSnapshot(); table_close(matviewRel, NoLock); /* Roll back any GUC changes */ AtEOXact_GUC(false, save_nestlevel); /* Restore userid and security context */ SetUserIdAndSecContext(save_userid, save_sec_context); return PointerGetDatum(NULL); } /* * rewrite_query_for_preupdate_state * * Rewrite the query so that base tables' RTEs will represent "pre-update" * state of tables. This is necessary to calculate view delta after multiple * tables are modified. */ static Query* rewrite_query_for_preupdate_state(Query *query, List *tables, ParseState *pstate, List *rte_path, Oid matviewid) { ListCell *lc; int num_rte = list_length(query->rtable); int i; /* This can recurse, so check for excessive recursion */ check_stack_depth(); /* register delta ENRs only one at first call */ if (rte_path == NIL) register_delta_ENRs(pstate, query, tables); /* XXX: Is necessary? Is this right timing? */ AcquireRewriteLocks(query, true, false); /* convert CTEs to subqueries */ foreach (lc, query->cteList) { PlannerInfo root; CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc); if (cte->cterefcount == 0) continue; root.parse = query; inline_cte(&root, cte); } query->cteList = NIL; i = 1; foreach(lc, query->rtable) { RangeTblEntry *r = (RangeTblEntry*) lfirst(lc); /* if rte contains subquery, search recursively */ if (r->rtekind == RTE_SUBQUERY) rewrite_query_for_preupdate_state(r->subquery, tables, pstate, lappend_int(list_copy(rte_path), i), matviewid); else { ListCell *lc2; foreach(lc2, tables) { MV_TriggerTable *table = (MV_TriggerTable *) lfirst(lc2); /* * if the modified table is found then replace the original RTE with * "pre-state" RTE and append its index to the list. */ if (r->relid == table->table_id) { List *securityQuals; List *withCheckOptions; bool hasRowSecurity; bool hasSubLinks; RangeTblEntry *rte_pre = get_prestate_rte(r, table, pstate->p_queryEnv, matviewid); /* * Set a row security poslicies of the modified table to the subquery RTE which * represents the pre-update state of the table. */ get_row_security_policies(query, table->original_rte, i, &securityQuals, &withCheckOptions, &hasRowSecurity, &hasSubLinks); if (hasRowSecurity) { query->hasRowSecurity = true; rte_pre->security_barrier = true; } if (hasSubLinks) query->hasSubLinks = true; rte_pre->securityQuals = securityQuals; lfirst(lc) = rte_pre; table->rte_paths = lappend(table->rte_paths, lappend_int(list_copy(rte_path), i)); break; } } } /* finish the loop if we processed all RTE included in the original query */ if (i++ >= num_rte) break; } return query; } /* * register_delta_ENRs * * For all modified tables, make ENRs for their transition tables * and register them to the queryEnv. ENR's RTEs are also appended * into the list in query tree. */ static void register_delta_ENRs(ParseState *pstate, Query *query, List *tables) { QueryEnvironment *queryEnv = pstate->p_queryEnv; ListCell *lc; RangeTblEntry *rte; foreach(lc, tables) { MV_TriggerTable *table = (MV_TriggerTable *) lfirst(lc); ListCell *lc2; int count; count = 0; foreach(lc2, table->old_tuplestores) { Tuplestorestate *oldtable = (Tuplestorestate *) lfirst(lc2); EphemeralNamedRelation enr = palloc(sizeof(EphemeralNamedRelationData)); ParseNamespaceItem *nsitem; enr->md.name = make_delta_enr_name("old", table->table_id, count); enr->md.reliddesc = table->table_id; enr->md.tupdesc = NULL; enr->md.enrtype = ENR_NAMED_TUPLESTORE; enr->md.enrtuples = tuplestore_tuple_count(oldtable); enr->reldata = oldtable; register_ENR(queryEnv, enr); nsitem = addRangeTableEntryForENR(pstate, makeRangeVar(NULL, enr->md.name, -1), true); rte = nsitem->p_rte; query->rtable = lappend(query->rtable, rte); table->old_rtes = lappend(table->old_rtes, rte); count++; } count = 0; foreach(lc2, table->new_tuplestores) { Tuplestorestate *newtable = (Tuplestorestate *) lfirst(lc2); EphemeralNamedRelation enr = palloc(sizeof(EphemeralNamedRelationData)); ParseNamespaceItem *nsitem; enr->md.name = make_delta_enr_name("new", table->table_id, count); enr->md.reliddesc = table->table_id; enr->md.tupdesc = NULL; enr->md.enrtype = ENR_NAMED_TUPLESTORE; enr->md.enrtuples = tuplestore_tuple_count(newtable); enr->reldata = newtable; register_ENR(queryEnv, enr); nsitem = addRangeTableEntryForENR(pstate, makeRangeVar(NULL, enr->md.name, -1), true); rte = nsitem->p_rte; query->rtable = lappend(query->rtable, rte); table->new_rtes = lappend(table->new_rtes, rte); count++; } } } #define DatumGetItemPointer(X) ((ItemPointer) DatumGetPointer(X)) #define PG_GETARG_ITEMPOINTER(n) DatumGetItemPointer(PG_GETARG_DATUM(n)) /* * ivm_visible_in_prestate * * Check visibility of a tuple specified by the tableoid and item pointer * using the snapshot taken just before the table was modified. */ Datum ivm_visible_in_prestate(PG_FUNCTION_ARGS) { Oid tableoid = PG_GETARG_OID(0); ItemPointer itemPtr = PG_GETARG_ITEMPOINTER(1); Oid matviewOid = PG_GETARG_OID(2); MV_TriggerHashEntry *entry; MV_TriggerTable *table = NULL; ListCell *lc; bool found; bool result; if (!in_delta_calculation) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("ivm_visible_in_prestate can be called only in delta calculation"))); entry = (MV_TriggerHashEntry *) hash_search(mv_trigger_info, (void *) &matviewOid, HASH_FIND, &found); Assert (found && entry != NULL); foreach(lc, entry->tables) { table = (MV_TriggerTable *) lfirst(lc); if (table->table_id == tableoid) break; } Assert (table != NULL); result = table_tuple_fetch_row_version(table->rel, itemPtr, entry->snapshot, table->slot); PG_RETURN_BOOL(result); } /* * get_prestate_rte * * Rewrite RTE of the modified table to a subquery which represents * "pre-state" table. The original RTE is saved in table->rte_original. */ static RangeTblEntry* get_prestate_rte(RangeTblEntry *rte, MV_TriggerTable *table, QueryEnvironment *queryEnv, Oid matviewid) { StringInfoData str; RawStmt *raw; Query *subquery; ParseState *pstate; char *relname; static char *subquery_tl; int i; pstate = make_parsestate(NULL); pstate->p_queryEnv = queryEnv; pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; relname = quote_qualified_identifier( get_namespace_name(RelationGetNamespace(table->rel)), RelationGetRelationName(table->rel)); subquery_tl = make_subquery_targetlist_from_table(table); /* * Filtering inserted row using the snapshot taken before the table * is modified. ctid is required for maintaining outer join views. */ initStringInfo(&str); appendStringInfo(&str, "SELECT %s, ctid::text FROM %s t" " WHERE pgivm.ivm_visible_in_prestate(t.tableoid, t.ctid, %d::pg_catalog.oid)", subquery_tl, relname, matviewid); /* * Append deleted rows contained in old transition tables. * * Add a pseudo ctid to ENR using row_number(), which is required for * calculating the number of dangling tuples to be inserted into * the outer join view. */ for (i = 0; i < list_length(table->old_tuplestores); i++) { appendStringInfo(&str, " UNION ALL "); appendStringInfo(&str," SELECT %s, " " ((row_number() over())::text || '_' || '%d' || '_' || '%d') AS ctid" " FROM %s", subquery_tl, table->table_id, i, make_delta_enr_name("old", table->table_id, i)); } /* Get a subquery representing pre-state of the table */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) raw = (RawStmt*)linitial(raw_parser(str.data, RAW_PARSE_DEFAULT)); #else raw = (RawStmt*)linitial(raw_parser(str.data)); #endif subquery = transformStmt(pstate, raw->stmt); /* save the original RTE */ table->original_rte = copyObject(rte); rte->rtekind = RTE_SUBQUERY; rte->subquery = subquery; rte->eref->colnames = lappend(rte->eref->colnames, makeString(pstrdup("ctid"))); rte->security_barrier = false; /* Clear fields that should not be set in a subquery RTE */ rte->relid = InvalidOid; rte->relkind = 0; rte->rellockmode = 0; rte->tablesample = NULL; rte->inh = false; /* must not be set for a subquery */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) rte->perminfoindex = 0; /* no permission checking for this RTE */ #else rte->requiredPerms = 0; /* no permission check on subquery itself */ rte->checkAsUser = InvalidOid; rte->selectedCols = NULL; rte->insertedCols = NULL; rte->updatedCols = NULL; rte->extraUpdatedCols = NULL; #endif return rte; } /* * make_subquery_targetlist_from_table * * Make a targetlist string of a subquery representing a delta table or a * pre-update state table. This subquery substitutes a modified table RTE * in the view definition query during view maintenance. In the targetlist, * column names appear in order of the table definition. However, for * attribute numbers of vars in the query tree to reference columns of the * subquery correctly even though the table has a dropped column, put "null" * as a dummy value at the position of a dropped column. * * We would also able to walk the query tree to rewrite varattnos, but * crafting targetlist is more simple and reasonable. */ static char* make_subquery_targetlist_from_table(MV_TriggerTable *table) { StringInfoData str; TupleDesc tupdesc; int i; tupdesc = RelationGetDescr(table->rel); initStringInfo(&str); for (i = 0; i < tupdesc->natts; i++) { Form_pg_attribute attr = TupleDescAttr(tupdesc, i); if (i > 0) appendStringInfo(&str, ", "); if (attr->attisdropped) appendStringInfo(&str, "null"); else appendStringInfo(&str, "%s", quote_identifier(NameStr(attr->attname))); } return str.data; } /* * make_delta_enr_name * * Make a name for ENR of a transition table from the base table's oid. * prefix will be "new" or "old" depending on its transition table kind.. */ static char* make_delta_enr_name(const char *prefix, Oid relid, int count) { char buf[NAMEDATALEN]; char *name; snprintf(buf, NAMEDATALEN, "__ivm_%s_%u_%u", prefix, relid, count); name = pstrdup(buf); return name; } /* * union_ENRs * * Replace RTE of the modified table with a single table delta that combine its * all transition tables. */ static RangeTblEntry* union_ENRs(RangeTblEntry *rte, MV_TriggerTable *table, List *enr_rtes, const char *prefix, QueryEnvironment *queryEnv) { StringInfoData str; ParseState *pstate; RawStmt *raw; Query *sub; int i; /* the previous RTE must be a subquery which represents "pre-state" table */ Assert(rte->rtekind == RTE_SUBQUERY); /* Create a ParseState for rewriting the view definition query */ pstate = make_parsestate(NULL); pstate->p_queryEnv = queryEnv; pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; initStringInfo(&str); for (i = 0; i < list_length(enr_rtes); i++) { if (i > 0) appendStringInfo(&str, " UNION ALL "); /* * Add a pseudo ctid to ENR using row_number(), which is required for * calculating the number of dangling tuples to be inserted into * the outer join view. */ appendStringInfo(&str, " SELECT %s, " " ((row_number() over())::text || '_' || '%d' || '_' || '%d') AS ctid" " FROM %s", make_subquery_targetlist_from_table(table), table->table_id, i, make_delta_enr_name(prefix, table->table_id, i)); } #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) raw = (RawStmt*)linitial(raw_parser(str.data, RAW_PARSE_DEFAULT)); #else raw = (RawStmt*)linitial(raw_parser(str.data)); #endif sub = transformStmt(pstate, raw->stmt); /* * Update the subquery so that it represent the combined transition * table. Note that we leave the security_barrier and securityQuals * fields so that the subquery relation can be protected by the RLS * policy as same as the modified table. */ rte->subquery = sub; return rte; } /* * rewrite_query_for_distinct_and_aggregates * * Rewrite query for counting DISTINCT clause and aggregate functions. */ static Query * rewrite_query_for_distinct_and_aggregates(Query *query, ParseState *pstate) { TargetEntry *tle_count; FuncCall *fn; Node *node; int varno = 0; ListCell *tbl_lc; /* For aggregate views */ if (query->hasAggs) { ListCell *lc; List *aggs = NIL; AttrNumber next_resno = list_length(query->targetList) + 1; foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); if (IsA(tle->expr, Aggref)) makeIvmAggColumn(pstate, (Aggref *)tle->expr, tle->resname, &next_resno, &aggs); } query->targetList = list_concat(query->targetList, aggs); } /* Add count(*) used for EXISTS clause */ foreach(tbl_lc, query->rtable) { RangeTblEntry *rte = (RangeTblEntry *) lfirst(tbl_lc); varno++; if (rte->subquery) { char *columnName; int attnum; /* search ivm_exists_count_X__ column in RangeTblEntry */ columnName = getColumnNameStartWith(rte, "__ivm_exists", &attnum); if (columnName == NULL) continue; node = (Node *)makeVar(varno ,attnum, INT8OID, -1, InvalidOid, 0); if (node == NULL) continue; tle_count = makeTargetEntry((Expr *) node, list_length(query->targetList) + 1, pstrdup(columnName), false); query->targetList = lappend(query->targetList, tle_count); } } /* Add count(*) for counting distinct tuples in views */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); #else fn = makeFuncCall(SystemFuncName("count"), NIL, -1); #endif fn->agg_star = true; if (!query->groupClause && !query->hasAggs) query->groupClause = transformDistinctClause(NULL, &query->targetList, query->sortClause, false); node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); tle_count = makeTargetEntry((Expr *) node, list_length(query->targetList) + 1, pstrdup("__ivm_count__"), false); query->targetList = lappend(query->targetList, tle_count); query->hasAggs = true; return query; } static Query * rewrite_exists_subquery_walker(Query *query, Node *node, int *count) { /* This can recurse, so check for excessive recursion */ check_stack_depth(); switch (nodeTag(node)) { case T_Query: { FromExpr *fromexpr; /* get subquery in WHERE clause */ fromexpr = (FromExpr *) query->jointree; if (fromexpr->quals != NULL) { query = rewrite_exists_subquery_walker(query, fromexpr->quals, count); /* drop WHERE clause when it has only one EXISTS */ if (IsA(fromexpr->quals, SubLink)) fromexpr->quals = NULL; } break; } case T_BoolExpr: { BoolExprType type = ((BoolExpr *) node)->boolop; if (type == AND_EXPR) { ListCell *lc; foreach(lc, ((BoolExpr *) node)->args) { /* If simple EXISTS subquery is used, rewrite LATERAL subquery */ Node *opnode = (Node *) lfirst(lc); query = rewrite_exists_subquery_walker(query, opnode, count); /* * overwrite SubLink node to true condition if it is contained in AND_EXPR. * EXISTS clause have already overwritten to LATERAL, so original EXISTS clause * is not necessory. */ if (IsA(opnode, SubLink)) lfirst(lc) = makeConst(BOOLOID, -1, InvalidOid, sizeof(bool), BoolGetDatum(true), false, true); } } break; } case T_SubLink: { char aliasName[NAMEDATALEN]; char columnName[NAMEDATALEN]; Query *subselect; ParseState *pstate; RangeTblEntry *rte; RangeTblRef *rtr; Alias *alias; Oid opId; ParseNamespaceItem *nsitem; TargetEntry *tle_count; FuncCall *fn; Node *fn_node; Expr *opexpr; SubLink *sublink = (SubLink *)node; subselect = (Query *)sublink->subselect; pstate = make_parsestate(NULL); pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; /* * convert EXISTS subquery into LATERAL subquery in FROM clause. */ snprintf(aliasName, sizeof(aliasName), "__ivm_exists_subquery_%d__", *count); snprintf(columnName, sizeof(columnName), "__ivm_exists_count_%d__", *count); /* add COUNT(*) for counting rows that meet exists condition */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1); #else fn = makeFuncCall(SystemFuncName("count"), NIL, -1); #endif fn->agg_star = true; fn_node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); tle_count = makeTargetEntry((Expr *) fn_node, list_length(subselect->targetList) + 1, columnName, false); /* add __ivm_exists_count__ column */ subselect->targetList = list_concat(subselect->targetList, list_make1(tle_count)); subselect->hasAggs = true; /* add a sub-query whth LATERAL into from clause */ alias = makeAlias(aliasName, NIL); nsitem = addRangeTableEntryForSubquery(pstate, subselect, alias, true, true); rte = nsitem->p_rte; query->rtable = lappend(query->rtable, rte); /* assume the new RTE is at the end */ rtr = makeNode(RangeTblRef); rtr->rtindex = list_length(query->rtable); ((FromExpr *) query->jointree)->fromlist = lappend(((FromExpr *) query->jointree)->fromlist, rtr); /* * EXISTS condition is converted to HAVING count(*) > 0. * We use make_opcllause() to get int84gt( '>' operator). We might be able to use make_op(). */ opId = OpernameGetOprid(list_make2(makeString("pg_catalog"), makeString(">")), INT8OID, INT4OID); opexpr = make_opclause(opId, BOOLOID, false, (Expr *) fn_node, (Expr *) makeConst(INT4OID, -1, InvalidOid, sizeof(int32), Int32GetDatum(0), false, true), InvalidOid, InvalidOid); fix_opfuncids((Node *) opexpr); query->hasSubLinks = false; subselect->havingQual = (Node *) opexpr; (*count)++; break; } default: break; } return query; } /* * rewrite_query_for_exists_subquery * * Rewrite EXISTS sublink in WHERE to LATERAL subquery * For example, rewrite * SELECT t1.* FROM t1 * WHERE EXISTS(SELECT 1 FROM t2 WHERE t1.key = t2.key) * to * SELECT t1.*, ex.__ivm_exists_count_0__ * FROM t1, LATERAL( * SELECT 1, COUNT(*) AS __ivm_exists_count_0__ * FROM t2 * WHERE t1.key = t2.key * HAVING COUNT(*) > 0) AS ex */ Query * rewrite_query_for_exists_subquery(Query *query) { int count = 0; if (query->hasAggs) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("this query is not allowed on incrementally maintainable materialized view"), errhint("aggregate function and EXISTS condition are not supported at the same time"))); return rewrite_exists_subquery_walker(query, (Node *) query, &count); } /* * get_normalized_form * * Transform query's jointree to the normalized form which consists of one or * more terms. The normalized form is a bag union of terms, and each term is * an inner join of some base tables, which is usually null-extended due to an * antijoin with other base tables. * * outer_join_rels is Relids of outer-join relations * The quals in the outer-join conditions will be stored into outerjoin_quals */ static List* get_normalized_form(Query *query, Node *jtnode, Relids outer_join_rels, List **outerjoin_quals) { if (jtnode == NULL) return NULL; if (IsA(jtnode, RangeTblRef)) { int varno = ((RangeTblRef *) jtnode)->rtindex; Term *term; /* * Create and initialize a term for a single table. Currently, * we assume this is a normal table. */ Assert(rt_fetch(varno, query->rtable)->relkind == RELKIND_RELATION); term = (Term*) palloc(sizeof(Term)); term->relids = bms_make_singleton(varno); term->anti_relids = NIL; return list_make1(term); } else if (IsA(jtnode, FromExpr)) { List *terms = NIL; FromExpr *f = (FromExpr *) jtnode; ListCell *l; bool is_first = true; /* * Create a term list using the terms in FROM list. The qual of * WHERE clause is specified only the first step. */ foreach(l, f->fromlist) { List *t = get_normalized_form(query, lfirst(l), outer_join_rels, outerjoin_quals); terms = multiply_terms(query, terms, t, (is_first ? f->quals : NULL), JOIN_INNER, outer_join_rels); is_first = false; } return terms; } else if (IsA(jtnode, JoinExpr)) { JoinExpr *j = (JoinExpr *) jtnode; List *lterms = get_normalized_form(query, j->larg, outer_join_rels, outerjoin_quals), *rterms = get_normalized_form(query, j->rarg, outer_join_rels, outerjoin_quals); List *terms = NIL; /* Create a term list from the two term lists and the join qual */ terms = multiply_terms(query, lterms, rterms, j->quals, j->jointype, outer_join_rels); if (j->jointype == JOIN_LEFT || j->jointype == JOIN_RIGHT || j->jointype == JOIN_FULL) *outerjoin_quals = lappend(*outerjoin_quals, j->quals); return terms; } else elog(ERROR, "unrecognized node type: %d", (int) nodeTag(jtnode)); } /* * multiply_terms * * Create new a term list by multiplying two term lists. If qual is * given, remove terms which are filtered by this qual and add this * qual to new terms. If one of the two term list is NIL, we return * the other after filtering by the qual. * * jointype is expected be JOIN_LEFT, JOIN_RIGHT, JOIN_FULL, or * JOIN_INNER. When mutiplying relations direcly under FROM clause, * JONI_INNER should be specified. * * outer_join_rels is Relids of outer-join relations */ static List* multiply_terms(Query *query, List *terms1, List *terms2, Node* qual, JoinType jointype, Relids outer_join_rels) { List *result = NIL; ListCell *l1, *l2; Relids qual_relids; Relids qual_relids_l = NULL, qual_relids_r = NULL; Relids anti_relids_l = NULL, anti_relids_r = NULL; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) PlannerInfo root; #endif Assert(jointype == JOIN_LEFT || jointype == JOIN_RIGHT || jointype == JOIN_FULL || jointype == JOIN_INNER); if (qual) #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) qual = flatten_join_alias_vars(NULL, query, qual); #else qual = flatten_join_alias_vars(query, qual); #endif /* all relids included in quals */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) qual_relids = pull_varnos(&root, qual); #else qual_relids = pull_varnos(qual); #endif qual_relids = bms_del_members(qual_relids, outer_join_rels); /* If either is NIL, return the other after filtering by qual. */ if (terms1 == NIL || terms2 == NIL) { /* This must be the first term in FROM clause */ Assert(jointype == JOIN_INNER); result = (terms1 == NIL ? terms2 : terms1); if (!qual) return result; foreach (l1, result) { Term *term = (Term*) lfirst(l1); /* * If the relids in the qual are not included, this term can not exist * because this qual references NULL values. * XXX: we assume that the qual is null-rejecting. */ if (!bms_is_subset(qual_relids, term->relids)) { result = foreach_delete_current(result, l1); continue; } } bms_free(qual_relids); return result; } /* * join of two terms * * We assume terms1 is the left hand side, and terms2 is the right hand * side. */ /* * Get the RHS relids that participate in the outer join by subtracting * LHS relids. */ qual_relids_r = bms_copy(qual_relids); foreach (l1, terms1) { Term *lterm = (Term*) lfirst(l1); qual_relids_r = bms_del_members(qual_relids_r, lterm->relids); } /* * Get the LHS relids that participate in the outer join by subtracting * RHS relids. */ qual_relids_l = bms_copy(qual_relids); foreach (l1, terms2) { Term *rterm = (Term*) lfirst(l1); qual_relids_l = bms_del_members(qual_relids_l, rterm->relids); } /* Find relids of tables anti-joined with term on the LHS */ if (jointype == JOIN_LEFT || jointype == JOIN_FULL) { foreach (l1, terms2) { /* * If this RHS term is participating in the outer join and the size * of its inner-joined relids is minimal among such terms, add it to * the list of anti-joined relids for the LHS terms. Other terms with * larger inner-joined relids are eliminated under the condition that * the join qual is null-rejecting. For example, in the following term: * * T anti-join [(R1 join R2) + (R1 anti-join R2)] * * if the first anti-join involves only R1, the term can be * transformed into: * * T anti-join R1 * * since all tuples in R1 are preserved regardless of R2. * Otherwise, if the first anti-join involves either only R2 * or both R1 and R2, the term is transformed into: * * T anti-join (R1 join R2) * * The resultant anti_relids_r represents the relids of * tables that could be anti-joined with every term * on the LHS. */ Term *rterm = (Term*) lfirst(l1); if (bms_is_subset(qual_relids_r, rterm->relids)) { if (!anti_relids_r) anti_relids_r = bms_copy(rterm->relids); else anti_relids_r = bms_int_members(anti_relids_r, rterm->relids); } } } /* Find relids of tables anti-joined with term on the RHS */ if (jointype == JOIN_RIGHT|| jointype == JOIN_FULL) { foreach (l1, terms1) { /* * Same, except for the right and left sides. See the comment above. * * The resultant anti_relids_l represents the relids of * tables that could be anti-joined with every term * on the RHS. */ Term *lterm = (Term*) lfirst(l1); if (bms_is_subset(qual_relids_l, lterm->relids)) { if (!anti_relids_l) anti_relids_l = bms_copy(lterm->relids); else anti_relids_l = bms_int_members(anti_relids_l, lterm->relids); } } } foreach (l1, terms1) { Term *lterm = (Term*) lfirst(l1); foreach (l2, terms2) { Term *rterm = (Term*) lfirst(l2); /* * If both the LHS term and the RHS term are participating in the outer join, * the new term fomred by joining them will survive under the condition that * the qual is null-rejecting. Otherwise, the new term cannot exist because * the qual references NULL values. */ if (bms_is_subset(qual_relids_l, lterm->relids) && bms_is_subset(qual_relids_r, rterm->relids)) { Term *newterm = (Term*) palloc(sizeof(Term)); newterm->relids = bms_union(lterm->relids, rterm->relids); newterm->anti_relids = list_concat_copy(lterm->anti_relids, rterm->anti_relids); result = lappend(result, newterm); } /* * If this RHS term is participating in the outer join, add the relids of * anti-joined tables from the LHS to this term's anti-joined table list. * Note that these relids include only relations participating in the join. * * This is done only in the final iteration over the LHS terms, since it * needs to be performed just once for each RHS term. * * The modified terms are later added to the result as terms representing * dangling tuples formed by this outer join. */ if (!lnext(terms1, l1) && (jointype == JOIN_RIGHT || jointype == JOIN_FULL)) { if (bms_is_subset(qual_relids_r, rterm->relids)) rterm->anti_relids = lappend(rterm->anti_relids, bms_copy(anti_relids_l)); } } /* * If this LHS term is participating in the outer join, add the relids of * anti-joined tables from the RHS to this term, similar to the process * above but with LHS and RHS swapped. * * This is performed for each LHS term. */ if (jointype == JOIN_LEFT || jointype == JOIN_FULL) { if (bms_is_subset(qual_relids_l, lterm->relids)) lterm->anti_relids = lappend(lterm->anti_relids, bms_copy(anti_relids_r)); } } /* In outer-join cases, add terms for dangling tuples */ switch (jointype) { case JOIN_LEFT: result = list_concat(result, terms1); break; case JOIN_RIGHT: result = list_concat(result, terms2); break; case JOIN_FULL: result = list_concat(result, terms1); result = list_concat(result, terms2); break; case JOIN_INNER: break; default: elog(ERROR, "unexpected join type: %d", jointype); } bms_free(qual_relids); bms_free(qual_relids_l); bms_free(qual_relids_r); if (anti_relids_l) bms_free(anti_relids_l); if (anti_relids_r) bms_free(anti_relids_r); return result; } /* * rewrite_query_for_outerjoin * * Rewrite the query to calculate the primary delta for an outer join view. * * index: the rteindex of the modified table * terms: the list of terms representing the normalized form of the outer join */ static Query* rewrite_query_for_outerjoin(Query *query, int index, List *terms) { Query *result = copyObject(query); int varno; Node *node; TargetEntry *tle; List *args = NIL; FuncCall *fn; ParseState *pstate = make_parsestate(NULL); Relids allrelids = NULL; ListCell *lc; List *reduced_outerjoins = NIL; List *unreduced_relids = NIL; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) ListCell *lc1, *lc2; #endif pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) fn = makeFuncCall(list_make1(makeString("count")), NIL, COERCE_EXPLICIT_CALL, -1); #else fn = makeFuncCall(list_make1(makeString("count")), NIL, -1); #endif fn->agg_distinct = true; /* * Reduce the outer join type for calculating the primary delta. */ if (!rewrite_jointype(result, (Node *) result->jointree, index, &allrelids, &reduced_outerjoins, &unreduced_relids)) elog(ERROR, "modified range table %d not found", index); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) /* * Remove nullingrel markers that reference removed outer joins. * For a partially reduced FULL join, relids on the unreduced side * are stored in unreduced_relids, and the nullingrel markers for * those must be preserved. */ forboth(lc1, reduced_outerjoins, lc2, unreduced_relids) { Relids reduced = bms_make_singleton(lfirst_int(lc1)); Relids unreduced = (Relids) lfirst(lc2); result = (Query *) remove_nulling_relids((Node *) result, reduced, unreduced); bms_free(reduced); } #endif /* * Add meta information for the primary delta. * * ctid is required for calculating the number of dangling tuples to be * inserted into the outer join view. */ varno = -1; while ((varno = bms_next_member(allrelids, varno)) >= 0) { Var *var = NULL; RangeTblEntry *rte = rt_fetch(varno, result->rtable); if (rte->rtekind == RTE_RELATION) { var = makeVar(varno, SelfItemPointerAttributeNumber, TIDOID, -1, InvalidOid, 0); } else if (rte->rtekind == RTE_SUBQUERY) { /* * Any subquery must be a pre-state table made by get_prestate_rte, * since currently sub-queries cannot be used with a outer join * in the view definition. */ foreach (lc, ((Query *) rte->subquery)->targetList) { tle = (TargetEntry *) lfirst(lc); if (!strcmp(tle->resname, "ctid")) { var = makeVar(varno, tle->resno, TEXTOID, -1, DEFAULT_COLLATION_OID, 0); break; } } } else elog(ERROR, "unexpected rte kind"); /* * Add count(distinct ctid) to count the number of tuples from each * base table that participate in generating a tuple in the primary * delta. */ args = lappend(args, makeConst(INT4OID, -1, InvalidOid, sizeof(int32), Int32GetDatum(varno), false, true)); node = ParseFuncOrColumn(pstate, fn->funcname, list_make1(var), NULL, fn, false, -1); args = lappend(args, node); } /* * Store counts for all base tables in a JSONB object. */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) fn = makeFuncCall(list_make1(makeString("json_build_object")), NIL, COERCE_EXPLICIT_CALL, -1); #else fn = makeFuncCall(list_make1(makeString("json_build_object")), NIL, -1); #endif node = ParseFuncOrColumn(pstate, fn->funcname, args, NULL, fn, false, -1); node = coerce_type(pstate, node, JSONOID, JSONBOID, -1, COERCION_EXPLICIT, COERCE_EXPLICIT_CAST, -1); assign_expr_collations(pstate, node); tle = makeTargetEntry((Expr *) node, list_length(result->targetList) + 1, pstrdup("__ivm_meta__"), false); result->targetList = lappend(result->targetList, tle); return result; } /* * rewrite_jointype * * Traverse the jointree in the query and reduce the strength of any outer * join that includes the modified table, in order to compute the primary * delta (the change in view tuples where the modified table is on the * non-nullable side). Such tuples should be inserted into the view when * any tuples are inserted into the table, or deleted when tuples are deleted * from it. * * When the modified table is on the nullable side of an outer join, * changes in null-extended tuples are excluded from the primary delta, * since those tuples are removed from the view on insertions and added * on deletions if required. Therefore, for computing the primary delta, * the outer join can be reduced in strength; a LEFT or RIGHT join can * be rewritten as an INNER join, and a FULL join as a LEFT or RIGHT join * depending on which side the modified table is on. * * index: rtindex of the modified table. * * All relids in the specified node are stored into *relids. * The rtindexes of reduced outer joins are stored into *reduced_outerjoins. * For reduced FULL outer joins, the relids of tables on the unreduced * side are stored into *unreduced_relids; for other outer joins, NULL * is stored instead. * * Returns true if the modified table appears under the specified node. */ static bool rewrite_jointype(Query *query, Node *node, int index, Relids *relids, List **reduced_outerjoins, List **unreduced_relids) { if (node == NULL) return false; if (IsA(node, RangeTblRef)) { int rtindex = ((RangeTblRef *) node)->rtindex; *relids = bms_add_member(*relids, rtindex); if (rtindex == index) return true; else return false; } else if (IsA(node, FromExpr)) { FromExpr *f = (FromExpr *) node; ListCell *l; bool found = false; foreach(l, f->fromlist) { if (rewrite_jointype(query, lfirst(l), index, relids, reduced_outerjoins, unreduced_relids)) found = true; } return found; } else if (IsA(node, JoinExpr)) { JoinExpr *j = (JoinExpr *) node; RangeTblEntry *rte = rt_fetch(j->rtindex, query->rtable); Relids left_relids = NULL; Relids right_relids = NULL; bool found_left = rewrite_jointype(query, j->larg, index, &left_relids, reduced_outerjoins, unreduced_relids); bool found_right = rewrite_jointype(query, j->rarg, index, &right_relids, reduced_outerjoins, unreduced_relids); /* * When the modified table is on the LHS, a FULL or RIGHT join can be * reduced to a LEFT or INNER join, respectively. However, if it is * a FULL join, the tables on the RHS remain nullable due to the join, * so save their reilds to remove only the correct nullingrel * markers later. */ if (found_left) { if (j->jointype == JOIN_FULL || j->jointype == JOIN_RIGHT) { Relids except = NULL; if (j->jointype == JOIN_FULL) { j->jointype = rte->jointype = JOIN_LEFT; except = right_relids; } else j->jointype = rte->jointype = JOIN_INNER; *reduced_outerjoins = lappend_int(*reduced_outerjoins, j->rtindex); *unreduced_relids = lappend(*unreduced_relids, except); } } /* * When the modified table is on the RHS, a FULL or LEFT join can be * reduced to a LEFT or RIGHT join, respectively. If it is a FULL join, * save the relids of tables on the LHS for a similar reason as above. */ if (found_right) { if (j->jointype == JOIN_FULL || j->jointype == JOIN_LEFT) { Relids except = NULL; if (j->jointype == JOIN_FULL) { j->jointype = rte->jointype = JOIN_RIGHT; except = left_relids; } else if (j->jointype == JOIN_LEFT) j->jointype = rte->jointype = JOIN_INNER; *reduced_outerjoins = lappend_int(*reduced_outerjoins, j->rtindex); *unreduced_relids = lappend(*unreduced_relids, except); } } *relids = bms_union(left_relids, right_relids); return found_left || found_right; } else elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); } /* * calc_delta * * Calculate view deltas generated under the modification of a table specified * by the RTE index. */ static void calc_delta(MV_TriggerTable *table, List *rte_path, Query *query, DestReceiver *dest_old, DestReceiver *dest_new, TupleDesc *tupdesc_old, TupleDesc *tupdesc_new, QueryEnvironment *queryEnv) { ListCell *lc = getRteListCell(query, rte_path); RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); in_delta_calculation = true; /* Generate old delta */ if (dest_old && list_length(table->old_rtes) > 0) { /* Replace the modified table with the old delta table and calculate the old view delta. */ lfirst(lc) = union_ENRs(rte, table, table->old_rtes, "old", queryEnv); refresh_immv_datafill(dest_old, query, queryEnv, tupdesc_old, ""); } /* Generate new delta */ if (dest_new && list_length(table->new_rtes) > 0) { /* Replace the modified table with the new delta table and calculate the new view delta*/ lfirst(lc) = union_ENRs(rte, table, table->new_rtes, "new", queryEnv); refresh_immv_datafill(dest_new, query, queryEnv, tupdesc_new, ""); } in_delta_calculation = false; } /* * rewrite_query_for_postupdate_state * * Rewrite the query so that the specified base table's RTEs will represent * "post-update" state of tables. This is called after the view delta * calculation due to changes on this table finishes. */ static Query* rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, List *rte_path) { ListCell *lc = getRteListCell(query, rte_path); /* Retore the original RTE */ lfirst(lc) = table->original_rte; return query; } /* * getRteListCell * * Get ListCell which contains RTE specified by the given path. */ static ListCell* getRteListCell(Query *query, List *rte_path) { ListCell *lc; ListCell *rte_lc = NULL; Assert(list_length(rte_path) > 0); foreach (lc, rte_path) { int index = lfirst_int(lc); RangeTblEntry *rte; rte_lc = list_nth_cell(query->rtable, index - 1); rte = (RangeTblEntry *) lfirst(rte_lc); if (rte != NULL && rte->rtekind == RTE_SUBQUERY) query = rte->subquery; } return rte_lc; } #define IVM_colname(type, col) makeObjectName("__ivm_" type, col, "_") /* * apply_delta * * Apply deltas to the materialized view. * * The old and new deltas and their TupleDesc should be specified by * old_suplestores, new_tuplestores, tupdesc_old, and tupdesc_new, * respectively. * * When counting is required (e.g., using aggregates, DISTINCT, or EXISTS), * use_count should be true, and count_colname should be the column name * storing the count. * * In outer-join cases: * - The list of terms in the normalized form should be specified in *terms. * - The modified table should be specified in *rte_path. * - The list of OuterJoinRel containing key resname information should be * spedified in *outerjoin_relinfo. * * The delta passed in old_suplestores or new_tuplestores must be the primary * delta (the change in view tuples where the modified table is on the * non-nullable side). Additional delta (the secondary delta) for dangling * tuples in outer joins is applied in insert_dangling_tuples() or * delete_dangling_tuples(). */ static void apply_delta(Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores, TupleDesc tupdesc_old, TupleDesc tupdesc_new, Query *query, bool use_count, char *count_colname, List *terms, List *rte_path, List *outerjoin_relinfo) { StringInfoData querybuf; StringInfoData target_list_buf; StringInfo aggs_list_buf = NULL; StringInfo aggs_set_old = NULL; StringInfo aggs_set_new = NULL; Relation matviewRel; char *matviewname; ListCell *lc; int i; List *keys = NIL; List *minmax_list = NIL; List *is_min_list = NIL; /* * get names of the materialized view and delta tables */ matviewRel = table_open(matviewOid, NoLock); matviewname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(matviewRel)), RelationGetRelationName(matviewRel)); /* * Build parts of the maintenance queries */ initStringInfo(&querybuf); initStringInfo(&target_list_buf); if (query->hasAggs) { if (old_tuplestores && tuplestore_tuple_count(old_tuplestores) > 0) aggs_set_old = makeStringInfo(); if (new_tuplestores && tuplestore_tuple_count(new_tuplestores) > 0) aggs_set_new = makeStringInfo(); aggs_list_buf = makeStringInfo(); } /* build string of target list */ for (i = 0; i < matviewRel->rd_att->natts; i++) { Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, i); char *resname = NameStr(attr->attname); if (i != 0) appendStringInfo(&target_list_buf, ", "); appendStringInfo(&target_list_buf, "%s", quote_qualified_identifier(NULL, resname)); } i = 0; foreach (lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, i); char *resname = NameStr(attr->attname); i++; if (tle->resjunk) continue; /* * For views without aggregates, all attributes are used as keys to identify a * tuple in a view. */ if (!query->hasAggs) keys = lappend(keys, attr); /* For views with aggregates, we need to build SET clause for updating aggregate * values. */ if (query->hasAggs && IsA(tle->expr, Aggref)) { Aggref *aggref = (Aggref *) tle->expr; const char *aggname = get_func_name(aggref->aggfnoid); /* * We can use function names here because it is already checked if these * can be used in IMMV by its OID at the definition time. */ /* count */ if (!strcmp(aggname, "count")) append_set_clause_for_count(resname, aggs_set_old, aggs_set_new, aggs_list_buf); /* sum */ else if (!strcmp(aggname, "sum")) append_set_clause_for_sum(resname, aggs_set_old, aggs_set_new, aggs_list_buf); /* avg */ else if (!strcmp(aggname, "avg")) append_set_clause_for_avg(resname, aggs_set_old, aggs_set_new, aggs_list_buf, format_type_be_qualified(aggref->aggtype)); /* min/max */ else if (!strcmp(aggname, "min") || !strcmp(aggname, "max")) { bool is_min = (!strcmp(aggname, "min")); append_set_clause_for_minmax(resname, aggs_set_old, aggs_set_new, aggs_list_buf, is_min); /* make a resname list of min and max aggregates */ minmax_list = lappend(minmax_list, resname); is_min_list = lappend_int(is_min_list, is_min); } else elog(ERROR, "unsupported aggregate function: %s", aggname); } } /* If we have GROUP BY clause, we use its entries as keys. */ if (query->hasAggs && query->groupClause) { foreach (lc, query->groupClause) { SortGroupClause *sgcl = (SortGroupClause *) lfirst(lc); TargetEntry *tle = get_sortgroupclause_tle(sgcl, query->targetList); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); keys = lappend(keys, attr); } } /* Start maintaining the materialized view. */ OpenImmvIncrementalMaintenance(); /* Open SPI context. */ if (SPI_connect() != SPI_OK_CONNECT) elog(ERROR, "SPI_connect failed"); /* For tuple deletion */ if (old_tuplestores && tuplestore_tuple_count(old_tuplestores) > 0) { EphemeralNamedRelation enr = palloc(sizeof(EphemeralNamedRelationData)); SPITupleTable *tuptable_recalc = NULL; uint64 num_recalc; int rc; /* convert tuplestores to ENR, and register for SPI */ enr->md.name = pstrdup(OLD_DELTA_ENRNAME); enr->md.reliddesc = InvalidOid; enr->md.tupdesc = tupdesc_old; enr->md.enrtype = ENR_NAMED_TUPLESTORE; enr->md.enrtuples = tuplestore_tuple_count(old_tuplestores); enr->reldata = old_tuplestores; rc = SPI_register_relation(enr); if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); if (use_count) /* apply old delta and get rows to be recalculated */ apply_old_delta_with_count(matviewname, OLD_DELTA_ENRNAME, keys, aggs_list_buf, aggs_set_old, minmax_list, is_min_list, count_colname, &tuptable_recalc, &num_recalc); else apply_old_delta(matviewname, OLD_DELTA_ENRNAME, keys); /* * If we have min or max, we might have to recalculate aggregate values from base tables * on some tuples. TIDs and keys such tuples are returned as a result of the above query. */ if (minmax_list && tuptable_recalc) recalc_and_set_values(tuptable_recalc, num_recalc, minmax_list, keys, matviewRel); /* * Insert dangling tuples generated by the deletion. */ if (terms && !query->hasAggs) { int index; Assert(list_length(rte_path) == 1); index = linitial_int(rte_path); insert_dangling_tuples(terms, query, matviewRel, OLD_DELTA_ENRNAME, use_count, index, outerjoin_relinfo); } } /* For tuple insertion */ if (new_tuplestores && tuplestore_tuple_count(new_tuplestores) > 0) { EphemeralNamedRelation enr = palloc(sizeof(EphemeralNamedRelationData)); int rc; /* convert tuplestores to ENR, and register for SPI */ enr->md.name = pstrdup(NEW_DELTA_ENRNAME); enr->md.reliddesc = InvalidOid; enr->md.tupdesc = tupdesc_new;; enr->md.enrtype = ENR_NAMED_TUPLESTORE; enr->md.enrtuples = tuplestore_tuple_count(new_tuplestores); enr->reldata = new_tuplestores; rc = SPI_register_relation(enr); if (rc != SPI_OK_REL_REGISTER) elog(ERROR, "SPI_register failed"); /* apply new delta */ if (use_count) apply_new_delta_with_count(matviewname, NEW_DELTA_ENRNAME, keys, &target_list_buf, aggs_set_new, count_colname, query->distinctClause != NULL); else apply_new_delta(matviewname, NEW_DELTA_ENRNAME, &target_list_buf); /* * Delete dangling tuples removed by the insersion. */ if (terms && !query->hasAggs) { int index; Assert(list_length(rte_path) == 1); index = linitial_int(rte_path); delete_dangling_tuples(terms, query, matviewRel, NEW_DELTA_ENRNAME, index, outerjoin_relinfo); } } /* We're done maintaining the materialized view. */ CloseImmvIncrementalMaintenance(); table_close(matviewRel, NoLock); /* Close SPI context. */ if (SPI_finish() != SPI_OK_FINISH) elog(ERROR, "SPI_finish failed"); } /* * append_set_clause_for_count * * Append SET clause string for count aggregation to given buffers. * Also, append resnames required for calculating the aggregate value. */ static void append_set_clause_for_count(const char *resname, StringInfo buf_old, StringInfo buf_new,StringInfo aggs_list) { /* For tuple deletion */ if (buf_old) { /* resname = mv.resname - t.resname */ appendStringInfo(buf_old, ", %s = %s", quote_qualified_identifier(NULL, resname), get_operation_string(IVM_SUB, resname, "mv", "t", NULL, NULL)); } /* For tuple insertion */ if (buf_new) { /* resname = mv.resname + diff.resname */ appendStringInfo(buf_new, ", %s = %s", quote_qualified_identifier(NULL, resname), get_operation_string(IVM_ADD, resname, "mv", "diff", NULL, NULL)); } appendStringInfo(aggs_list, ", %s", quote_qualified_identifier("diff", resname) ); } /* * append_set_clause_for_sum * * Append SET clause string for sum aggregation to given buffers. * Also, append resnames required for calculating the aggregate value. */ static void append_set_clause_for_sum(const char *resname, StringInfo buf_old, StringInfo buf_new, StringInfo aggs_list) { char *count_col = IVM_colname("count", resname); /* For tuple deletion */ if (buf_old) { /* sum = mv.sum - t.sum */ appendStringInfo(buf_old, ", %s = %s", quote_qualified_identifier(NULL, resname), get_operation_string(IVM_SUB, resname, "mv", "t", count_col, NULL) ); /* count = mv.count - t.count */ appendStringInfo(buf_old, ", %s = %s", quote_qualified_identifier(NULL, count_col), get_operation_string(IVM_SUB, count_col, "mv", "t", NULL, NULL) ); } /* For tuple insertion */ if (buf_new) { /* sum = mv.sum + diff.sum */ appendStringInfo(buf_new, ", %s = %s", quote_qualified_identifier(NULL, resname), get_operation_string(IVM_ADD, resname, "mv", "diff", count_col, NULL) ); /* count = mv.count + diff.count */ appendStringInfo(buf_new, ", %s = %s", quote_qualified_identifier(NULL, count_col), get_operation_string(IVM_ADD, count_col, "mv", "diff", NULL, NULL) ); } appendStringInfo(aggs_list, ", %s, %s", quote_qualified_identifier("diff", resname), quote_qualified_identifier("diff", IVM_colname("count", resname)) ); } /* * append_set_clause_for_avg * * Append SET clause string for avg aggregation to given buffers. * Also, append resnames required for calculating the aggregate value. */ static void append_set_clause_for_avg(const char *resname, StringInfo buf_old, StringInfo buf_new, StringInfo aggs_list, const char *aggtype) { char *sum_col = IVM_colname("sum", resname); char *count_col = IVM_colname("count", resname); /* For tuple deletion */ if (buf_old) { /* avg = (mv.sum - t.sum)::aggtype / (mv.count - t.count) */ appendStringInfo(buf_old, ", %s = %s OPERATOR(pg_catalog./) %s", quote_qualified_identifier(NULL, resname), get_operation_string(IVM_SUB, sum_col, "mv", "t", count_col, aggtype), get_operation_string(IVM_SUB, count_col, "mv", "t", NULL, NULL) ); /* sum = mv.sum - t.sum */ appendStringInfo(buf_old, ", %s = %s", quote_qualified_identifier(NULL, sum_col), get_operation_string(IVM_SUB, sum_col, "mv", "t", count_col, NULL) ); /* count = mv.count - t.count */ appendStringInfo(buf_old, ", %s = %s", quote_qualified_identifier(NULL, count_col), get_operation_string(IVM_SUB, count_col, "mv", "t", NULL, NULL) ); } /* For tuple insertion */ if (buf_new) { /* avg = (mv.sum + diff.sum)::aggtype / (mv.count + diff.count) */ appendStringInfo(buf_new, ", %s = %s OPERATOR(pg_catalog./) %s", quote_qualified_identifier(NULL, resname), get_operation_string(IVM_ADD, sum_col, "mv", "diff", count_col, aggtype), get_operation_string(IVM_ADD, count_col, "mv", "diff", NULL, NULL) ); /* sum = mv.sum + diff.sum */ appendStringInfo(buf_new, ", %s = %s", quote_qualified_identifier(NULL, sum_col), get_operation_string(IVM_ADD, sum_col, "mv", "diff", count_col, NULL) ); /* count = mv.count + diff.count */ appendStringInfo(buf_new, ", %s = %s", quote_qualified_identifier(NULL, count_col), get_operation_string(IVM_ADD, count_col, "mv", "diff", NULL, NULL) ); } appendStringInfo(aggs_list, ", %s, %s, %s", quote_qualified_identifier("diff", resname), quote_qualified_identifier("diff", IVM_colname("sum", resname)), quote_qualified_identifier("diff", IVM_colname("count", resname)) ); } /* * append_set_clause_for_minmax * * Append SET clause string for min or max aggregation to given buffers. * Also, append resnames required for calculating the aggregate value. * is_min is true if this is min, false if not. */ static void append_set_clause_for_minmax(const char *resname, StringInfo buf_old, StringInfo buf_new, StringInfo aggs_list, bool is_min) { char *count_col = IVM_colname("count", resname); /* For tuple deletion */ if (buf_old) { /* * If the new value doesn't became NULL then use the value remaining * in the view although this will be recomputated afterwords. */ appendStringInfo(buf_old, ", %s = CASE WHEN %s THEN NULL ELSE %s END", quote_qualified_identifier(NULL, resname), get_null_condition_string(IVM_SUB, "mv", "t", count_col), quote_qualified_identifier("mv", resname) ); /* count = mv.count - t.count */ appendStringInfo(buf_old, ", %s = %s", quote_qualified_identifier(NULL, count_col), get_operation_string(IVM_SUB, count_col, "mv", "t", NULL, NULL) ); } /* For tuple insertion */ if (buf_new) { /* * min = LEAST(mv.min, diff.min) * max = GREATEST(mv.max, diff.max) */ appendStringInfo(buf_new, ", %s = CASE WHEN %s THEN NULL ELSE %s(%s,%s) END", quote_qualified_identifier(NULL, resname), get_null_condition_string(IVM_ADD, "mv", "diff", count_col), is_min ? "LEAST" : "GREATEST", quote_qualified_identifier("mv", resname), quote_qualified_identifier("diff", resname) ); /* count = mv.count + diff.count */ appendStringInfo(buf_new, ", %s = %s", quote_qualified_identifier(NULL, count_col), get_operation_string(IVM_ADD, count_col, "mv", "diff", NULL, NULL) ); } appendStringInfo(aggs_list, ", %s, %s", quote_qualified_identifier("diff", resname), quote_qualified_identifier("diff", IVM_colname("count", resname)) ); } /* * get_operation_string * * Build a string to calculate the new aggregate values. */ static char * get_operation_string(IvmOp op, const char *col, const char *arg1, const char *arg2, const char* count_col, const char *castType) { StringInfoData buf; StringInfoData castString; char *col1 = quote_qualified_identifier(arg1, col); char *col2 = quote_qualified_identifier(arg2, col); char op_char = (op == IVM_SUB ? '-' : '+'); initStringInfo(&buf); initStringInfo(&castString); if (castType) appendStringInfo(&castString, "::%s", castType); if (!count_col) { /* * If the attributes don't have count columns then calc the result * by using the operator simply. */ appendStringInfo(&buf, "(%s OPERATOR(pg_catalog.%c) %s)%s", col1, op_char, col2, castString.data); } else { /* * If the attributes have count columns then consider the condition * where the result becomes NULL. */ char *null_cond = get_null_condition_string(op, arg1, arg2, count_col); appendStringInfo(&buf, "(CASE WHEN %s THEN NULL " "WHEN %s IS NULL THEN %s " "WHEN %s IS NULL THEN %s " "ELSE (%s OPERATOR(pg_catalog.%c) %s)%s END)", null_cond, col1, col2, col2, col1, col1, op_char, col2, castString.data ); } return buf.data; } /* * get_null_condition_string * * Build a predicate string for CASE clause to check if an aggregate value * will became NULL after the given operation is applied. */ static char * get_null_condition_string(IvmOp op, const char *arg1, const char *arg2, const char* count_col) { StringInfoData null_cond; initStringInfo(&null_cond); switch (op) { case IVM_ADD: appendStringInfo(&null_cond, "%s OPERATOR(pg_catalog.=) 0 AND %s OPERATOR(pg_catalog.=) 0", quote_qualified_identifier(arg1, count_col), quote_qualified_identifier(arg2, count_col) ); break; case IVM_SUB: appendStringInfo(&null_cond, "%s OPERATOR(pg_catalog.=) %s", quote_qualified_identifier(arg1, count_col), quote_qualified_identifier(arg2, count_col) ); break; default: elog(ERROR,"unknown operation"); } return null_cond.data; } /* * apply_old_delta_with_count * * Execute a query for applying a delta table given by deltaname_old * which contains tuples to be deleted from a materialized view given by * matviewname. This is used when counting is required, that is, the view * has aggregate or distinct. Also, when a table in EXISTS sub queries * is modified. * * If the view desn't have aggregates or has GROUP BY, this requires a keys * list to identify a tuple in the view. If the view has aggregates, this * requires strings representing resnames of aggregates and SET clause for * updating aggregate values. * * If the view has min or max aggregate, this requires a list of resnames of * min/max aggregates and a list of boolean which represents which entries in * minmax_list is min. These are necessary to check if we need to recalculate * min or max aggregate values. In this case, this query returns TID and keys * of tuples which need to be recalculated. This result and the number of rows * are stored in tuptables and num_recalc repectedly. */ static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old, List *keys, StringInfo aggs_list, StringInfo aggs_set, List *minmax_list, List *is_min_list, const char *count_colname, SPITupleTable **tuptable_recalc, uint64 *num_recalc) { StringInfoData querybuf; char *match_cond; char *updt_returning = ""; char *select_for_recalc = "SELECT"; bool agg_without_groupby = (list_length(keys) == 0); Assert(tuptable_recalc != NULL); Assert(num_recalc != NULL); /* build WHERE condition for searching tuples to be deleted */ match_cond = get_matching_condition_string(keys); /* * We need a special RETURNING clause and SELECT statement for min/max to * check which tuple needs re-calculation from base tables. */ if (minmax_list) { updt_returning = get_returning_string(minmax_list, is_min_list, keys); select_for_recalc = get_select_for_recalc_string(keys); } /* Search for matching tuples from the view and update or delete if found. */ initStringInfo(&querybuf); appendStringInfo(&querybuf, "WITH t AS (" /* collecting tid of target tuples in the view */ "SELECT diff.%s, " /* count column */ "(diff.%s OPERATOR(pg_catalog.=) mv.%s AND %s) AS for_dlt, " "mv.ctid " "%s " /* aggregate columns */ "FROM %s AS mv, %s AS diff " "WHERE %s" /* tuple matching condition */ "), updt AS (" /* update a tuple if this is not to be deleted */ "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s " "%s" /* SET clauses for aggregates */ "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt " "%s" /* RETURNING clause for recalc infomation */ "), dlt AS (" /* delete a tuple if this is to be deleted */ "DELETE FROM %s AS mv USING t " "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt" ") %s", /* SELECT returning which tuples need to be recalculated */ count_colname, count_colname, count_colname, (agg_without_groupby ? "false" : "true"), (aggs_list != NULL ? aggs_list->data : ""), matviewname, deltaname_old, match_cond, matviewname, count_colname, count_colname, count_colname, (aggs_set != NULL ? aggs_set->data : ""), updt_returning, matviewname, select_for_recalc); if (SPI_exec(querybuf.data, 0) != SPI_OK_SELECT) elog(ERROR, "SPI_exec failed: %s", querybuf.data); /* Return tuples to be recalculated. */ if (minmax_list) { *tuptable_recalc = SPI_tuptable; *num_recalc = SPI_processed; } else { *tuptable_recalc = NULL; *num_recalc = 0; } } /* * apply_old_delta * * Execute a query for applying a delta table given by deltaname_old * which contains tuples to be deleted from a materialized view given by * matviewname. This is used when counting is not required. */ static void apply_old_delta(const char *matviewname, const char *deltaname_old, List *keys) { StringInfoData querybuf; StringInfoData keysbuf; char *match_cond; ListCell *lc; /* build WHERE condition for searching tuples to be deleted */ match_cond = get_matching_condition_string(keys); /* build string of keys list */ initStringInfo(&keysbuf); foreach (lc, keys) { Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); char *resname = NameStr(attr->attname); appendStringInfo(&keysbuf, "%s", quote_qualified_identifier("mv", resname)); if (lnext(keys, lc)) appendStringInfo(&keysbuf, ", "); } /* Search for matching tuples from the view and update or delete if found. */ initStringInfo(&querybuf); appendStringInfo(&querybuf, "DELETE FROM %s WHERE ctid IN (" "SELECT tid FROM (SELECT pg_catalog.row_number() over (partition by %s) AS \"__ivm_row_number__\"," "mv.ctid AS tid," "diff.\"__ivm_count__\"" "FROM %s AS mv, %s AS diff " "WHERE %s) v " "WHERE v.\"__ivm_row_number__\" OPERATOR(pg_catalog.<=) v.\"__ivm_count__\")", matviewname, keysbuf.data, matviewname, deltaname_old, match_cond); if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) elog(ERROR, "SPI_exec failed: %s", querybuf.data); } /* * apply_new_delta_with_count * * Execute a query for applying a delta table given by deltaname_new * which contains tuples to be inserted into a materialized view given by * matviewname. This is used when counting is required, that is, the view * has aggregate or distinct. Also, when a table in EXISTS sub queries * is modified. * * If the view desn't have aggregates or has GROUP BY, this requires a keys * list to identify a tuple in the view. If the view has aggregates, this * requires strings representing SET clause for updating aggregate values. */ static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new, List *keys, StringInfo target_list, StringInfo aggs_set, const char* count_colname, bool distinct) { StringInfoData querybuf; StringInfoData returning_keys; ListCell *lc; char *match_cond = ""; StringInfoData deltaname_new_for_insert; /* build WHERE condition for searching tuples to be updated */ match_cond = get_matching_condition_string(keys); /* build string of keys list */ initStringInfo(&returning_keys); if (keys) { foreach (lc, keys) { Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); char *resname = NameStr(attr->attname); appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname)); if (lnext(keys, lc)) appendStringInfo(&returning_keys, ", "); } } else appendStringInfo(&returning_keys, "NULL"); /* * If count_colname is not "__ivm_count__", the view contains EXISTS * subquery and the count column to be updated here is "__ivm_exists_count_*" * that stores the number of columns generated by corresponding EXISTS * subquery for each row in the view. In this case, __ivm_count__ in * deltaname_new stores duplicity of rows, and each row need to be * duplicated as much as __ivm_count__ by using generate_series at * inserting if DISTINCT is not used. */ initStringInfo(&deltaname_new_for_insert); if (!strcmp(count_colname, "__ivm_count__") || distinct) appendStringInfo(&deltaname_new_for_insert, "%s", deltaname_new); else appendStringInfo(&deltaname_new_for_insert, "(SELECT diff.* FROM %s diff," "pg_catalog.generate_series(1, diff.\"__ivm_count__\"))", deltaname_new); /* Search for matching tuples from the view and update if found or insert if not. */ initStringInfo(&querybuf); appendStringInfo(&querybuf, "WITH updt AS (" /* update a tuple if this exists in the view */ "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s " "%s " /* SET clauses for aggregates */ "FROM %s AS diff " "WHERE %s " /* tuple matching condition */ "RETURNING %s" /* returning keys of updated tuples */ ") INSERT INTO %s (%s)" /* insert a new tuple if this doesn't exist */ "SELECT %s FROM %s AS diff " "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);", matviewname, count_colname, count_colname, count_colname, (aggs_set != NULL ? aggs_set->data : ""), deltaname_new, match_cond, returning_keys.data, matviewname, target_list->data, target_list->data, deltaname_new_for_insert.data, match_cond); if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) elog(ERROR, "SPI_exec failed: %s", querybuf.data); } /* * apply_new_delta * * Execute a query for applying a delta table given by deltaname_new * which contains tuples to be inserted into a materialized view given by * matviewname. This is used when counting is not required. */ static void apply_new_delta(const char *matviewname, const char *deltaname_new, StringInfo target_list) { StringInfoData querybuf; /* Search for matching tuples from the view and update or delete if found. */ initStringInfo(&querybuf); appendStringInfo(&querybuf, "INSERT INTO %s (%s) SELECT %s FROM (" "SELECT diff.*, pg_catalog.generate_series(1, diff.\"__ivm_count__\") AS __ivm_generate_series__ " "FROM %s AS diff) AS v", matviewname, target_list->data, target_list->data, deltaname_new); if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) elog(ERROR, "SPI_exec failed: %s", querybuf.data); } /* * get_matching_condition_string * * Build a predicate string for looking for a tuple with given keys. */ static char * get_matching_condition_string(List *keys) { StringInfoData match_cond; ListCell *lc; /* If there is no key columns, the condition is always true. */ if (keys == NIL) return "true"; initStringInfo(&match_cond); foreach (lc, keys) { Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); char *resname = NameStr(attr->attname); char *mv_resname = quote_qualified_identifier("mv", resname); char *diff_resname = quote_qualified_identifier("diff", resname); Oid typid = attr->atttypid; /* Considering NULL values, we can not use simple = operator. */ appendStringInfo(&match_cond, "("); generate_equal(&match_cond, typid, mv_resname, diff_resname); appendStringInfo(&match_cond, " OR (%s IS NULL AND %s IS NULL))", mv_resname, diff_resname); if (lnext(keys, lc)) appendStringInfo(&match_cond, " AND "); } return match_cond.data; } /* * get_returning_string * * Build a string for RETURNING clause of UPDATE used in apply_old_delta_with_count. * This clause returns ctid and a boolean value that indicates if we need to * recalculate min or max value, for each updated row. */ static char * get_returning_string(List *minmax_list, List *is_min_list, List *keys) { StringInfoData returning; char *recalc_cond; ListCell *lc; Assert(minmax_list != NIL && is_min_list != NIL); recalc_cond = get_minmax_recalc_condition_string(minmax_list, is_min_list); initStringInfo(&returning); appendStringInfo(&returning, "RETURNING mv.ctid AS tid, (%s) AS recalc", recalc_cond); foreach (lc, keys) { Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); char *resname = NameStr(attr->attname); appendStringInfo(&returning, ", %s", quote_qualified_identifier("mv", resname)); } return returning.data; } /* * get_minmax_recalc_condition_string * * Build a predicate string for checking if any min/max aggregate * value needs to be recalculated. */ static char * get_minmax_recalc_condition_string(List *minmax_list, List *is_min_list) { StringInfoData recalc_cond; ListCell *lc1, *lc2; initStringInfo(&recalc_cond); Assert (list_length(minmax_list) == list_length(is_min_list)); forboth (lc1, minmax_list, lc2, is_min_list) { char *resname = (char *) lfirst(lc1); bool is_min = (bool) lfirst_int(lc2); char *op_str = (is_min ? ">=" : "<="); appendStringInfo(&recalc_cond, "%s OPERATOR(pg_catalog.%s) %s", quote_qualified_identifier("mv", resname), op_str, quote_qualified_identifier("t", resname) ); if (lnext(minmax_list, lc1)) appendStringInfo(&recalc_cond, " OR "); } return recalc_cond.data; } /* * get_select_for_recalc_string * * Build a query to return tid and keys of tuples which need * recalculation. This is used as the result of the query * built by apply_old_delta. */ static char * get_select_for_recalc_string(List *keys) { StringInfoData qry; ListCell *lc; initStringInfo(&qry); appendStringInfo(&qry, "SELECT tid"); foreach (lc, keys) { Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); appendStringInfo(&qry, ", %s", NameStr(attr->attname)); } appendStringInfo(&qry, " FROM updt WHERE recalc"); return qry.data; } /* * recalc_and_set_values * * Recalculate tuples in a materialized from base tables and update these. * The tuples which needs recalculation are specified by keys, and resnames * of columns to be updated are specified by namelist. TIDs and key values * are given by tuples in tuptable_recalc. Its first attribute must be TID * and key values must be following this. */ static void recalc_and_set_values(SPITupleTable *tuptable_recalc, int64 num_tuples, List *namelist, List *keys, Relation matviewRel) { TupleDesc tupdesc_recalc = tuptable_recalc->tupdesc; Oid *keyTypes = NULL, *types = NULL; char *keyNulls = NULL, *nulls = NULL; Datum *keyVals = NULL, *vals = NULL; int num_vals = list_length(namelist); int num_keys = list_length(keys); uint64 i; /* If we have keys, initialize arrays for them. */ if (keys) { keyTypes = palloc(sizeof(Oid) * num_keys); keyNulls = palloc(sizeof(char) * num_keys); keyVals = palloc(sizeof(Datum) * num_keys); /* a tuple contains keys to be recalculated and ctid to be updated*/ Assert(tupdesc_recalc->natts == num_keys + 1); /* Types of key attributes */ for (i = 0; i < num_keys; i++) keyTypes[i] = TupleDescAttr(tupdesc_recalc, i + 1)->atttypid; } /* allocate memory for all attribute names and tid */ types = palloc(sizeof(Oid) * (num_vals + 1)); nulls = palloc(sizeof(char) * (num_vals + 1)); vals = palloc(sizeof(Datum) * (num_vals + 1)); /* For each tuple which needs recalculation */ for (i = 0; i < num_tuples; i++) { int j; bool isnull; SPIPlanPtr plan; SPITupleTable *tuptable_newvals; TupleDesc tupdesc_newvals; /* Set group key values as parameters if needed. */ if (keys) { for (j = 0; j < num_keys; j++) { keyVals[j] = SPI_getbinval(tuptable_recalc->vals[i], tupdesc_recalc, j + 2, &isnull); if (isnull) keyNulls[j] = 'n'; else keyNulls[j] = ' '; } } /* * Get recalculated values from base tables. The result must be * only one tuple thich contains the new values for specified keys. */ plan = get_plan_for_recalc(matviewRel, namelist, keys, keyTypes); if (SPI_execute_plan(plan, keyVals, keyNulls, false, 0) != SPI_OK_SELECT) elog(ERROR, "SPI_execute_plan"); if (SPI_processed != 1) elog(ERROR, "SPI_execute_plan returned zero or more than one rows"); tuptable_newvals = SPI_tuptable; tupdesc_newvals = tuptable_newvals->tupdesc; Assert(tupdesc_newvals->natts == num_vals); /* Set the new values as parameters */ for (j = 0; j < tupdesc_newvals->natts; j++) { if (i == 0) types[j] = TupleDescAttr(tupdesc_newvals, j)->atttypid; vals[j] = SPI_getbinval(tuptable_newvals->vals[0], tupdesc_newvals, j + 1, &isnull); if (isnull) nulls[j] = 'n'; else nulls[j] = ' '; } /* Set TID of the view tuple to be updated as a parameter */ types[j] = TIDOID; vals[j] = SPI_getbinval(tuptable_recalc->vals[i], tupdesc_recalc, 1, &isnull); nulls[j] = ' '; /* Update the view tuple to the new values */ plan = get_plan_for_set_values(matviewRel, namelist, types); if (SPI_execute_plan(plan, vals, nulls, false, 0) != SPI_OK_UPDATE) elog(ERROR, "SPI_execute_plan"); } } /* * get_plan_for_recalc * * Create or fetch a plan for recalculating value in the view's target list * from base tables using the definition query of materialized view specified * by matviewRel. namelist is a list of resnames of values to be recalculated. * * keys is a list of keys to identify tuples to be recalculated if this is not * empty. KeyTypes is an array of types of keys. */ static SPIPlanPtr get_plan_for_recalc(Relation matviewRel, List *namelist, List *keys, Oid *keyTypes) { MV_QueryKey hash_key; SPIPlanPtr plan; /* Fetch or prepare a saved plan for the recalculation */ mv_BuildQueryKey(&hash_key, RelationGetRelid(matviewRel), MV_PLAN_RECALC); if ((plan = mv_FetchPreparedPlan(&hash_key)) == NULL) { ListCell *lc; StringInfoData str; char *viewdef; /* get view definition of matview */ viewdef = pg_ivm_get_viewdef(matviewRel, false); /* * Build a query string for recalculating values. This is like * * SELECT x1, x2, x3, ... FROM ( ... view definition query ...) mv * WHERE (key1, key2, ...) = ($1, $2, ...); */ initStringInfo(&str); appendStringInfo(&str, "SELECT "); foreach (lc, namelist) { appendStringInfo(&str, "%s", (char *) lfirst(lc)); if (lnext(namelist, lc)) appendStringInfoString(&str, ", "); } appendStringInfo(&str, " FROM (%s) mv", viewdef); if (keys) { int i = 1; char paramname[16]; appendStringInfo(&str, " WHERE ("); foreach (lc, keys) { Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc); char *resname = NameStr(attr->attname); Oid typid = attr->atttypid; sprintf(paramname, "$%d", i); appendStringInfo(&str, "("); generate_equal(&str, typid, resname, paramname); appendStringInfo(&str, " OR (%s IS NULL AND %s IS NULL))", resname, paramname); if (lnext(keys, lc)) appendStringInfoString(&str, " AND "); i++; } appendStringInfo(&str, ")"); } else keyTypes = NULL; plan = SPI_prepare(str.data, list_length(keys), keyTypes); if (plan == NULL) elog(ERROR, "SPI_prepare returned %s for %s", SPI_result_code_string(SPI_result), str.data); SPI_keepplan(plan); mv_HashPreparedPlan(&hash_key, plan); } return plan; } /* * get_plan_for_set_values * * Create or fetch a plan for applying new values calculated by * get_plan_for_recalc to a materialized view specified by matviewRel. * namelist is a list of resnames of attributes to be updated, and * valTypes is an array of types of the * values. */ static SPIPlanPtr get_plan_for_set_values(Relation matviewRel, List *namelist, Oid *valTypes) { MV_QueryKey key; SPIPlanPtr plan; char *matviewname; matviewname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(matviewRel)), RelationGetRelationName(matviewRel)); /* Fetch or prepare a saved plan for the real check */ mv_BuildQueryKey(&key, RelationGetRelid(matviewRel), MV_PLAN_SET_VALUE); if ((plan = mv_FetchPreparedPlan(&key)) == NULL) { ListCell *lc; StringInfoData str; int i; /* * Build a query string for applying min/max values. This is like * * UPDATE matviewname AS mv * SET (x1, x2, x3, x4) = ($1, $2, $3, $4) * WHERE ctid = $5; */ initStringInfo(&str); appendStringInfo(&str, "UPDATE %s AS mv SET (", matviewname); foreach (lc, namelist) { appendStringInfo(&str, "%s", (char *) lfirst(lc)); if (lnext(namelist, lc)) appendStringInfoString(&str, ", "); } appendStringInfo(&str, ") = ROW("); for (i = 1; i <= list_length(namelist); i++) appendStringInfo(&str, "%s$%d", (i==1 ? "" : ", "), i); appendStringInfo(&str, ") WHERE ctid OPERATOR(pg_catalog.=) $%d", i); plan = SPI_prepare(str.data, list_length(namelist) + 1, valTypes); if (plan == NULL) elog(ERROR, "SPI_prepare returned %s for %s", SPI_result_code_string(SPI_result), str.data); SPI_keepplan(plan); mv_HashPreparedPlan(&key, plan); } return plan; } /* * insert_dangling_tuples * * Insert dangling tuples generated as a result of tuple deletions * on a base table of the materialized view that has an outer join. * * The insertion is performed per term in the normalized form of the * outer join query. Each term is an inner join of some base tables, * which is usually null-extended due to one or more anti-joins with * other base tables. Dangling tuples in a term can be inserted into * the view when the modified table is among the anti-joined tables. * * When a tuple is deleted from a table anti-joined in a term, * some tuples that are the product of joining this tuple and tuples * in other tables joined in this term are deleted from the view. * As a result, if there is no more tuples that satisfy the join * qual between the inner-join and anti-join parts in this term, * dangling tuples generated by null-extending the inner-join part * must be inserted into the view. * * The delta contained in dataname_old must be the primary delta, which * includes only the changes in view tuples where the modified table is * on the non-nullable side. It contains tuples deleted from the view * as a result of the deletions on the table, and the contents are used * to generate dangling tuples to be inserted for each term. * * terms: the normalized form of the outer join query * index: the rtindex of the modified table * outerjoin_relinfo: he list of OuterJoinRel containing key resname information */ static void insert_dangling_tuples(List *terms, Query *query, Relation matviewRel, const char *deltaname_old, bool use_count, int index, List *outerjoin_relinfo) { StringInfoData querybuf; char *matviewname; ListCell *lc; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) Relids relids_with_outer = get_relids_in_jointree((Node *) query->jointree, true, false); Relids relids_without_outer = get_relids_in_jointree((Node *) query->jointree, false, false); Relids outerjoin_relids = bms_del_members(relids_with_outer, relids_without_outer); bms_free(relids_without_outer); #endif matviewname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(matviewRel)), RelationGetRelationName(matviewRel)); /* * For each term in normalized form where any angling tuples can be inserted * into the view, build a query for the insertion and perform it. */ foreach (lc, terms) { ListCell *lc1, *lc2; StringInfoData exists_cond; StringInfoData targetlist; StringInfoData count; StringInfoData joined_cond; StringInfoData joined_in_delta_cond; Term *term = lfirst(lc); int i; List *anti_relids_modified = NULL; /* * Check whether the modified table is anti-joined in this term. If so, * collect the relids of the anti-joined tables. */ foreach(lc1, term->anti_relids) { Relids relids = lfirst(lc1); if (bms_is_member(index, relids)) anti_relids_modified = lappend(anti_relids_modified, relids); } if (list_length(anti_relids_modified) == 0) continue; /* Build the targetlist of dangling tuples to be inserted. */ initStringInfo(&targetlist); foreach (lc1, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc1); Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1); char *resname = NameStr(attr->attname); Relids tle_relids; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) PlannerInfo root; #endif if (tle->resjunk) continue; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) tle = (TargetEntry *) flatten_join_alias_vars(NULL, query, (Node *) tle); #else tle = (TargetEntry *) flatten_join_alias_vars(query, (Node *) tle); #endif /* get relids referenced in this target entry */ #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) tle_relids = pull_varnos(&root, (Node *) tle); #else tle_relids = pull_varnos((Node *) tle); #endif #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) tle_relids = bms_del_members(tle_relids, outerjoin_relids); #endif /* * Check whether all column under this target entry come from joined tables * in this term. This means that the columns belong to the nonnullable table, * and this part of the vars in the primary delta can be used as the * non-nullable part of the dangling tuples inserted into the view. * For other target entries of the dangling tuples, NULL is inserted, * since we assume that the entry expression does not contain * any non-strict function. * * XXX: We could relax this assumption if we would eval the targetlist on the * dangling part of outer join results, but we don't support such evaluation. */ if (bms_is_subset(tle_relids, term->relids)) appendStringInfo(&targetlist, "%s%s", targetlist.len ? "," : "", resname); bms_free(tle_relids); } initStringInfo(&exists_cond); initStringInfo(&joined_cond); initStringInfo(&count); i = -1; /* for each of tables inter-joined in this term */ while ((i = bms_next_member(term->relids, i)) >= 0) { OuterJoinRelInfo *info; bool found = false; /* seach outer-join relation info */ foreach (lc1, outerjoin_relinfo) { info = lfirst(lc1); if (info->rtindex == i) { found = true; break; } } /* * The rel is not involved in the outer join; it is only inner-joined * to another outer-joined table, so it does not have outer-join info, * and is skipped. */ if (!found) continue; /* * Build a condition to check whether the view still has any tuple that * satisfies the join qual which produces the dangling tuple we are trying * to insert. * * The qual should be satisfied when the values of vars referenced by the * join quals are equal between the dangling tuple and the view * (which also implies they are not null). * * Note that we assume that vars referenced in join quals be included * in the view. * * XXX: We would be able to use primary keys instead if they are included, * but we cannot expect it for now. * * Also, build a condition to check whether a tuple belongs to any terms * to which the dangling tuples to be inserted belong. For such tuples, * the keys of the tables joined in this term must be NOT NULL. In addition, * the keys anti-joined by any anti-join in this term must be NULL, since * this part is null-extended; the latter conditions are added later. */ foreach (lc1, info->key_attrs) { Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc1); char *resname = NameStr(attr->attname); char *mv_resname = quote_qualified_identifier("mv", resname); char *diff_resname = quote_qualified_identifier("diff", resname); appendStringInfo(&exists_cond, "%s", exists_cond.len ? " AND " : ""); generate_equal(&exists_cond, attr->atttypid, mv_resname, diff_resname); appendStringInfo(&joined_cond, "%s%s IS NOT NULL", joined_cond.len ? " AND " : "", resname); } /* counting the number of tuples to be inserted */ appendStringInfo(&count, "%s(__ivm_meta__->'%d')::pg_catalog.int8", count.len ? " OPERATOR(pg_catalog.*) " : "", i); } /* * Append to the condition built above a check that the keys anti-joined by * any anti-join in this term must be NULL. */ initStringInfo(&joined_in_delta_cond); foreach(lc1, anti_relids_modified) { Relids anti_relids = (Relids) lfirst(lc1); appendStringInfo(&joined_in_delta_cond, "%s%s", joined_in_delta_cond.len ? "OR" : "", joined_cond.data); i = -1; while ((i = bms_next_member(anti_relids, i)) >= 0) { OuterJoinRelInfo *info; bool found = false; /* seach outer-join relation info */ foreach (lc2, outerjoin_relinfo) { info = lfirst(lc2); if (info->rtindex == i) { found = true; break; } } /* * The rel is not involved in the outer join; it is only inner-joined * to another outer-joined table, so it does not have outer-join info, * and is skipped. */ if (!found) continue; foreach (lc2, info->key_attrs) { Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc2); char *resname = NameStr(attr->attname); appendStringInfo(&joined_in_delta_cond, " AND %s IS NOT NULL", resname); } } } /* Insert dangling tuples if needed */ initStringInfo(&querybuf); if (use_count) appendStringInfo(&querybuf, "INSERT INTO %s (%s, __ivm_count__) " "SELECT diff.* FROM " "(SELECT DISTINCT %s, %s AS __ivm_count__ FROM %s " "WHERE %s ) AS diff " "WHERE NOT EXISTS (SELECT 1 FROM %s mv WHERE %s)", matviewname, targetlist.data, targetlist.data, count.data, deltaname_old, joined_in_delta_cond.data, matviewname, exists_cond.data ); else appendStringInfo(&querybuf, "INSERT INTO %s (%s) " "SELECT %s FROM " "(SELECT diff.*, pg_catalog.generate_series(1, diff.__ivm_count__) " "FROM (SELECT DISTINCT %s, %s AS __ivm_count__ FROM %s " "WHERE %s ) AS diff " "WHERE NOT EXISTS (SELECT 1 FROM %s mv WHERE %s)) v", matviewname, targetlist.data, targetlist.data, targetlist.data, count.data, deltaname_old, joined_in_delta_cond.data, matviewname, exists_cond.data ); if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) elog(ERROR, "SPI_exec failed: %s", querybuf.data); } } /* * delete_dangling_tuples * * Delete dangling tuples to be removed as a result of tuple insertions * on a base table of the materialized view that has an outer join. * * The deletion is performed per term in the normalized form of the * outer join query. Each term is an inner join of some base tables, * which is usually null-extended due to one or more anti-joins with * other base tables. Dangling tuples in a term can be deleted from * the view when the modified table is among the anti-joined tables. * * When a tuple is inserted into a table anti-joined in a term, * some tuples that are the product of joining this tuple and tuples * in other tables joined in this term are inserted into the view. * As a result, dangling tuples generated by null-extending the * inner-join part in this term must be deleted from the view unless * there are no more such dangling tuples. * * The delta contained in dataname_new must be the primary delta, which * includes only the changes in view tuples where the modified table is * on the non-nullable side. It contains tuples inserted into the view * as a result of insertions on the table, and the contents are used * to generate the condition for deleting dangling tuples for each term. * * terms: the normalized form of the outer join query * index: the rtindex of the modified table * outerjoin_relinfo: the list of OuterJoinRel containing key resname information */ static void delete_dangling_tuples(List *terms, Query *query, Relation matviewRel, const char *deltaname_new, int index, List *outerjoin_relinfo) { char *matviewname; ListCell *lc; matviewname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(matviewRel)), RelationGetRelationName(matviewRel)); /* * For each term in normalized form where any angling tuples can be deleted * from the view, build a query for the deletion and perform it. */ foreach (lc, terms) { ListCell *lc1, *lc2; StringInfoData querybuf; StringInfoData dangling_cond; StringInfoData key_cols; StringInfoData joined_cond; StringInfoData joined_in_delta_cond; Term *term = lfirst(lc); List *anti_relids_modified = NULL; /* * Check whether the modified table is anti-joined in this term. If so, * collect the relids of the anti-joined tables. */ foreach(lc1, term->anti_relids) { Relids relids = lfirst(lc1); if (bms_is_member(index, relids)) anti_relids_modified = lappend(anti_relids_modified, relids); } if (list_length(anti_relids_modified) == 0) continue; initStringInfo(&dangling_cond); initStringInfo(&key_cols); initStringInfo(&joined_cond); /* for each table invloving in the outer join */ foreach (lc1, outerjoin_relinfo) { OuterJoinRelInfo *info = lfirst(lc1); /* * Build the condition to check whether a tuple belongs to this term, * and the list of key resnames of the tables joined in this term. * * Also, build a condition to check whether a tuple belongs to any terms * to which the dangling tuples to be deleted belong. For such tuples, * the keys of the tables joined in this term must be NOT NULL. In addition, * the keys anti-joined by any anti-join in this term must be NULL, since * this part is null-extended; the latter conditions are added later. */ foreach (lc2, info->key_attrs) { Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc2); char *resname = NameStr(attr->attname); if (bms_is_member(info->rtindex, term->relids)) { appendStringInfo(&dangling_cond, "%s%s IS NOT NULL ", dangling_cond.len ? " AND " : "" , resname); appendStringInfo(&joined_cond, "%s%s IS NOT NULL ", joined_cond.len ? " AND " : "", resname); appendStringInfo(&key_cols, "%s%s", key_cols.len ? "," : "", resname); } else appendStringInfo(&dangling_cond, "%s%s IS NULL ", dangling_cond.len ? " AND " : "", resname); } } /* * Append to the condition built above a check that the keys anti-joined by * any anti-join in this term must be NULL. */ initStringInfo(&joined_in_delta_cond); foreach(lc1, anti_relids_modified) { Relids anti_relids = (Relids) lfirst(lc1); int i; appendStringInfo(&joined_in_delta_cond, "%s%s", joined_in_delta_cond.len ? "OR" : "", joined_cond.data); i = -1; while ((i = bms_next_member(anti_relids, i)) >= 0) { OuterJoinRelInfo *info; bool found = false; /* seach outer-join relation info */ foreach (lc2, outerjoin_relinfo) { info = lfirst(lc2); if (info->rtindex == i) { found = true; break; } } /* * The rel is not involved in the outer join; it is only inner-joined * to another outer-joined table, so it does not have outer-join info, * and is skipped. */ if (!found) continue; foreach (lc2, info->key_attrs) { Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc2); char *resname = NameStr(attr->attname); appendStringInfo(&joined_in_delta_cond, " AND %s IS NOT NULL", resname); } } } /* Delete dangling tuples if needed */ initStringInfo(&querybuf); appendStringInfo(&querybuf, "DELETE FROM %s " "WHERE %s AND " "(%s) IN (SELECT %s FROM %s diff WHERE %s)", matviewname, dangling_cond.data, key_cols.data, key_cols.data, deltaname_new, joined_in_delta_cond.data ); if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) elog(ERROR, "SPI_exec failed: %s", querybuf.data); } } /* * generate_equal * * Generate an equality clause using given operands' default equality * operator. */ static void generate_equal(StringInfo querybuf, Oid opttype, const char *leftop, const char *rightop) { TypeCacheEntry *typentry; typentry = lookup_type_cache(opttype, TYPECACHE_EQ_OPR); if (!OidIsValid(typentry->eq_opr)) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_FUNCTION), errmsg("could not identify an equality operator for type %s", format_type_be_qualified(opttype)))); generate_operator_clause(querybuf, leftop, opttype, typentry->eq_opr, rightop, opttype); } /* * mv_InitHashTables */ static void mv_InitHashTables(void) { HASHCTL ctl; memset(&ctl, 0, sizeof(ctl)); ctl.keysize = sizeof(MV_QueryKey); ctl.entrysize = sizeof(MV_QueryHashEntry); mv_query_cache = hash_create("MV query cache", MV_INIT_QUERYHASHSIZE, &ctl, HASH_ELEM | HASH_BLOBS); memset(&ctl, 0, sizeof(ctl)); ctl.keysize = sizeof(Oid); ctl.entrysize = sizeof(MV_TriggerHashEntry); mv_trigger_info = hash_create("MV trigger info", MV_INIT_QUERYHASHSIZE, &ctl, HASH_ELEM | HASH_BLOBS); } /* * mv_FetchPreparedPlan */ static SPIPlanPtr mv_FetchPreparedPlan(MV_QueryKey *key) { MV_QueryHashEntry *entry; SPIPlanPtr plan; /* * On the first call initialize the hashtable */ if (!mv_query_cache) mv_InitHashTables(); /* * Lookup for the key */ entry = (MV_QueryHashEntry *) hash_search(mv_query_cache, (void *) key, HASH_FIND, NULL); if (entry == NULL) return NULL; /* * Check whether the plan is still valid. If it isn't, we don't want to * simply rely on plancache.c to regenerate it; rather we should start * from scratch and rebuild the query text too. This is to cover cases * such as table/column renames. We depend on the plancache machinery to * detect possible invalidations, though. * * CAUTION: this check is only trustworthy if the caller has already * locked both materialized views and base tables. * * Also, check whether the search_path is still the same as when we made it. * If it isn't, we need to rebuild the query text because the result of * pg_ivm_get_viewdef() will change. */ plan = entry->plan; if (plan && SPI_plan_is_valid(plan) && #if PG_VERSION_NUM < 170000 OverrideSearchPathMatchesCurrent(entry->search_path)) #else SearchPathMatchesCurrentEnvironment(entry->search_path)) #endif return plan; /* * Otherwise we might as well flush the cached plan now, to free a little * memory space before we make a new one. */ if (plan) SPI_freeplan(plan); if (entry->search_path) pfree(entry->search_path); entry->plan = NULL; entry->search_path = NULL; return NULL; } /* * mv_HashPreparedPlan * * Add another plan to our private SPI query plan hashtable. */ static void mv_HashPreparedPlan(MV_QueryKey *key, SPIPlanPtr plan) { MV_QueryHashEntry *entry; bool found; /* * On the first call initialize the hashtable */ if (!mv_query_cache) mv_InitHashTables(); /* * Add the new plan. We might be overwriting an entry previously found * invalid by mv_FetchPreparedPlan. */ entry = (MV_QueryHashEntry *) hash_search(mv_query_cache, (void *) key, HASH_ENTER, &found); Assert(!found || entry->plan == NULL); entry->plan = plan; #if PG_VERSION_NUM < 170000 entry->search_path = GetOverrideSearchPath(TopMemoryContext); #else entry->search_path = GetSearchPathMatcher(TopMemoryContext); #endif } /* * mv_BuildQueryKey * * Construct a hashtable key for a prepared SPI plan for IVM. */ static void mv_BuildQueryKey(MV_QueryKey *key, Oid matview_id, int32 query_type) { /* * We assume struct MV_QueryKey contains no padding bytes, else we'd need * to use memset to clear them. */ key->matview_id = matview_id; key->query_type = query_type; } /* * AtAbort_IVM * * Clean up hash entries for all materialized views. This is called at * (sub-)transaction abort. When the top-level transaction is aborted, * InvalidSubTransactionId is set to subxid. */ void AtAbort_IVM(SubTransactionId subxid) { HASH_SEQ_STATUS seq; MV_TriggerHashEntry *entry; if (mv_trigger_info) { hash_seq_init(&seq, mv_trigger_info); while ((entry = hash_seq_search(&seq)) != NULL) clean_up_IVM_hash_entry(entry, true, subxid); } in_delta_calculation = false; } /* * AtPreCommit_IVM * * Store the transaction ID that updated the view incrementally * into the pg_ivm_immv catalog at transaction commit. */ void AtPreCommit_IVM() { HASH_SEQ_STATUS seq; MV_TriggerHashEntry *entry; if (mv_trigger_info) { /* * For each view that was incrementally updated in the transaction, * record the transaction ID into the pg_ivm_immv catalog, and perform * the final clean up of the entry. */ hash_seq_init(&seq, mv_trigger_info); while ((entry = hash_seq_search(&seq)) != NULL) { bool found; setLastUpdateXid(entry->matview_id, GetTopFullTransactionId()); hash_search(mv_trigger_info, (void *) &entry->matview_id, HASH_REMOVE, &found); } } in_delta_calculation = false; } /* * clean_up_IVM_hash_entry * * Clean up tuple stores and hash entries for a materialized view after its * maintenance finished. This is called at the end of table modifying query * or (sub-)transaction abort. When the top-level transaction is aborted, * InvalidSubTransactionId is set to subxid. */ static void clean_up_IVM_hash_entry(MV_TriggerHashEntry *entry, bool is_abort, SubTransactionId subxid) { bool found; ListCell *lc; /* clean up tuple stores */ foreach(lc, entry->tables) { MV_TriggerTable *table = (MV_TriggerTable *) lfirst(lc); ListCell *lc2; foreach(lc2, table->old_tuplestores) { Tuplestorestate *tup = (Tuplestorestate *) lfirst(lc2); tuplestore_end(tup); } foreach(lc2, table->new_tuplestores) { Tuplestorestate *tup = (Tuplestorestate *) lfirst(lc2); tuplestore_end(tup); } list_free(table->old_tuplestores); list_free(table->new_tuplestores); if (!is_abort) { ExecDropSingleTupleTableSlot(table->slot); table_close(table->rel, NoLock); } } list_free(entry->tables); entry->tables = NIL; if (is_abort) { bool remove_entry = false; /* * When the top-level transaction is aborted, remove all subxids. * When a sub-transaction is aborted, remove only its subxid. */ if (subxid == InvalidSubTransactionId) remove_entry = true; else { foreach(lc, entry->subxids) { /* Note: * PG16 or later has lfirst_xid, but we use lfirst_int for * supporting older PGs since there is no problem or now. */ if (lfirst_int(lc) == subxid) { entry->subxids = list_delete_cell(entry->subxids, lc); break; } } /* * If all the subxid are removed, it means that the view was not * updated at all in this transaction. */ if (list_length(entry->subxids) == 0) remove_entry = true; } /* * Remove entries of not updated views from the hash table. */ if (remove_entry) hash_search(mv_trigger_info, (void *) &entry->matview_id, HASH_REMOVE, &found); } else { /* When the query sucsessully finished, unregister the snapshot */ UnregisterSnapshot(entry->snapshot); } entry->snapshot = InvalidSnapshot; } /* * setLastUpdateXid * * Store the transaction ID that updated the view incremenally into the * pg_ivm_immv catalog. */ static void setLastUpdateXid(Oid immv_oid, FullTransactionId xid) { Relation pgIvmImmv = table_open(PgIvmImmvRelationId(), ShareRowExclusiveLock); TupleDesc tupdesc = RelationGetDescr(pgIvmImmv); SysScanDesc scan; ScanKeyData key; HeapTuple tup; Datum values[Natts_pg_ivm_immv]; bool nulls[Natts_pg_ivm_immv]; bool replaces[Natts_pg_ivm_immv]; HeapTuple newtup = NULL; ScanKeyInit(&key, Anum_pg_ivm_immv_immvrelid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(immv_oid)); scan = systable_beginscan(pgIvmImmv, PgIvmImmvPrimaryKeyIndexId(), true, NULL, 1, &key); tup = systable_getnext(scan); memset(values, 0, sizeof(values)); values[Anum_pg_ivm_immv_lastivmupdate -1 ] = FullTransactionIdGetDatum(xid); MemSet(nulls, false, sizeof(nulls)); MemSet(replaces, false, sizeof(replaces)); replaces[Anum_pg_ivm_immv_lastivmupdate -1 ] = true; newtup = heap_modify_tuple(tup, tupdesc, values, nulls, replaces); CatalogTupleUpdate(pgIvmImmv, &newtup->t_self, newtup); heap_freetuple(newtup); /* * Advance command counter to make the updated pg_ivm_immv row locally * visible. */ CommandCounterIncrement(); systable_endscan(scan); table_close(pgIvmImmv, ShareRowExclusiveLock); } /* * getLastUpdateXid * * Get the most recent transaction ID that updated the view incrementally * from the pg_ivm_immv catalog. */ static FullTransactionId getLastUpdateXid(Oid immv_oid) { Relation pgIvmImmv = table_open(PgIvmImmvRelationId(), AccessShareLock); TupleDesc tupdesc = RelationGetDescr(pgIvmImmv); SysScanDesc scan; ScanKeyData key; HeapTuple tup; bool isnull; Datum datum; FullTransactionId xid = InvalidFullTransactionId; ScanKeyInit(&key, Anum_pg_ivm_immv_immvrelid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(immv_oid)); scan = systable_beginscan(pgIvmImmv, PgIvmImmvPrimaryKeyIndexId(), true, NULL, 1, &key); tup = systable_getnext(scan); datum = heap_getattr(tup, Anum_pg_ivm_immv_lastivmupdate, tupdesc, &isnull); if (!isnull) xid = DatumGetFullTransactionId(datum); systable_endscan(scan); table_close(pgIvmImmv, NoLock); return xid; } /* * getColumnNameStartWith * * Search a column name which starts with the given string from the given RTE, * and return the first found one or NULL if not found. */ char * getColumnNameStartWith(RangeTblEntry *rte, char *str, int *attnum) { char *colname; ListCell *lc; Alias *alias = rte->eref; (*attnum) = 0; foreach(lc, alias->colnames) { (*attnum)++; if (strncmp(strVal(lfirst(lc)), str, strlen(str)) == 0) { colname = pstrdup(strVal(lfirst(lc))); return colname; } } return NULL; } /* * isIvmName * * Check if this is an IVM hidden column from the name. */ bool isIvmName(const char *s) { if (s) return (strncmp(s, "__ivm_", 6) == 0); return false; } pg_ivm-1.13/meson.build000066400000000000000000000055641507512346500151030ustar00rootroot00000000000000project('pg_ivm', ['c']) pg_config = find_program('pg_config') bindir = run_command(pg_config, '--bindir', check: true).stdout().strip() includedir_server = run_command(pg_config, '--includedir-server', check: true).stdout().strip() includedir = run_command(pg_config, '--includedir', check: true).stdout().strip() pkglibdir = run_command(pg_config, '--pkglibdir', check: true).stdout().strip() sharedir = run_command(pg_config, '--sharedir', check: true).stdout().strip() libdir = run_command(pg_config, '--libdir', check: true).stdout().strip() module_name = meson.project_name() # ruleutils.c includes ruleutils_13.c or ruleutils_14.c based on PostgreSQL version # Note: We don't need to explicitly add these files since they're included by ruleutils.c pg_ivm_sources = files( 'createas.c', 'matview.c', 'pg_ivm.c', 'ruleutils.c', 'subselect.c', ) if meson.get_compiler('c').get_id() == 'msvc' incdir = [includedir_server / 'port/win32_msvc', includedir_server / 'port/win32', includedir_server, includedir] postgres_lib = meson.get_compiler('c').find_library( 'postgres', dirs: libdir, static: true, required: true ) else incdir = [ includedir_server ] postgres_lib = [] endif shared_module(module_name, pg_ivm_sources, include_directories: incdir, install: true, install_dir: pkglibdir, name_prefix: '', dependencies: postgres_lib, ) install_data( 'pg_ivm--1.0.sql', 'pg_ivm--1.0--1.1.sql', 'pg_ivm--1.1--1.2.sql', 'pg_ivm--1.2--1.3.sql', 'pg_ivm--1.3--1.4.sql', 'pg_ivm--1.4--1.5.sql', 'pg_ivm--1.5--1.6.sql', 'pg_ivm--1.6--1.7.sql', 'pg_ivm--1.7--1.8.sql', 'pg_ivm--1.8--1.9.sql', 'pg_ivm--1.9--1.10.sql', 'pg_ivm--1.10.sql', 'pg_ivm--1.10--1.11.sql', 'pg_ivm--1.11--1.12.sql', 'pg_ivm--1.12--1.13.sql', 'pg_ivm.control', install_dir: sharedir / 'extension', ) pg_regress = find_program('pg_regress', dirs: [pkglibdir / 'pgxs/src/test/regress'] ) regress_tests = ['pg_ivm', 'create_immv', 'refresh_immv'] test('regress', pg_regress, args: ['--bindir', bindir, '--inputdir', meson.current_source_dir(), ] + regress_tests, ) pg_isolation_regress = find_program('pg_isolation_regress', dirs: [pkglibdir / 'pgxs/src/test/isolation'] ) isolation_tests = [ 'create_insert', 'refresh_insert', 'insert_insert', 'create_insert2', 'refresh_insert2', 'insert_insert2', 'create_insert3', 'refresh_insert3', 'insert_insert3' ] isolation_opts = [ '--load-extension','pg_ivm', ] test('isolation', pg_isolation_regress, args: ['--bindir', bindir, '--inputdir', meson.current_source_dir(), '--outputdir', 'output_iso', ] + isolation_opts + isolation_tests, ) pg_ivm-1.13/pg_ivm--1.0--1.1.sql000066400000000000000000000003241507512346500156530ustar00rootroot00000000000000-- catalog ALTER TABLE pg_catalog.pg_ivm_immv ADD COLUMN ispopulated bool NOT NULL; -- functions CREATE FUNCTION refresh_immv(text, bool) RETURNS bigint STRICT AS 'MODULE_PATHNAME', 'refresh_immv' LANGUAGE C; pg_ivm-1.13/pg_ivm--1.0.sql000066400000000000000000000026621507512346500153100ustar00rootroot00000000000000-- catalog CREATE SCHEMA __pg_ivm__; CREATE TABLE __pg_ivm__.pg_ivm_immv( immvrelid regclass NOT NULL, viewdef text NOT NULL, CONSTRAINT pg_ivm_immv_pkey PRIMARY KEY (immvrelid) ); ALTER TABLE __pg_ivm__.pg_ivm_immv SET SCHEMA pg_catalog; SELECT pg_catalog.pg_extension_config_dump('pg_catalog.pg_ivm_immv', ''); -- functions CREATE FUNCTION create_immv(text, text) RETURNS bigint STRICT AS 'MODULE_PATHNAME', 'create_immv' LANGUAGE C; -- trigger functions CREATE FUNCTION "IVM_immediate_before"() RETURNS trigger AS 'MODULE_PATHNAME', 'IVM_immediate_before' LANGUAGE C; CREATE FUNCTION "IVM_immediate_maintenance"() RETURNS trigger AS 'MODULE_PATHNAME', 'IVM_immediate_maintenance' LANGUAGE C; CREATE FUNCTION "IVM_prevent_immv_change"() RETURNS trigger AS 'MODULE_PATHNAME', 'IVM_prevent_immv_change' LANGUAGE C; /* * DDL trigger that removes entry from pg_ivm_immv */ CREATE FUNCTION pg_catalog.pg_ivm_sql_drop_trigger_func() RETURNS event_trigger AS $$ DECLARE pg_class_oid OID; relids REGCLASS[]; BEGIN pg_class_oid = 'pg_catalog.pg_class'::regclass; /* Find relids to remove */ DELETE FROM pg_catalog.pg_ivm_immv USING pg_catalog.pg_event_trigger_dropped_objects() AS events WHERE immvrelid = events.objid AND events.classid = pg_class_oid AND events.objsubid = 0; END $$ LANGUAGE plpgsql; CREATE EVENT TRIGGER pg_ivm_sql_drop_trigger ON sql_drop EXECUTE PROCEDURE pg_catalog.pg_ivm_sql_drop_trigger_func(); pg_ivm-1.13/pg_ivm--1.1--1.2.sql000066400000000000000000000000001507512346500156440ustar00rootroot00000000000000pg_ivm-1.13/pg_ivm--1.10--1.11.sql000066400000000000000000000000001507512346500160040ustar00rootroot00000000000000pg_ivm-1.13/pg_ivm--1.10.sql000066400000000000000000000024451507512346500153700ustar00rootroot00000000000000CREATE SCHEMA pgivm; -- catalog CREATE TABLE pgivm.pg_ivm_immv( immvrelid regclass NOT NULL, viewdef text NOT NULL, ispopulated bool NOT NULL, lastivmupdate xid8, CONSTRAINT pg_ivm_immv_pkey PRIMARY KEY (immvrelid) ); SELECT pg_catalog.pg_extension_config_dump('pgivm.pg_ivm_immv', ''); -- functions CREATE FUNCTION pgivm.create_immv(text, text) RETURNS bigint STRICT AS 'MODULE_PATHNAME', 'create_immv' LANGUAGE C; CREATE FUNCTION pgivm.refresh_immv(text, bool) RETURNS bigint STRICT AS 'MODULE_PATHNAME', 'refresh_immv' LANGUAGE C; CREATE FUNCTION pgivm.get_immv_def(IN immvrelid regclass) RETURNS text STRICT AS 'MODULE_PATHNAME', 'get_immv_def' LANGUAGE C; CREATE FUNCTION pgivm.ivm_visible_in_prestate(oid, tid, oid) RETURNS bool STABLE AS 'MODULE_PATHNAME', 'ivm_visible_in_prestate' LANGUAGE C; -- trigger functions CREATE FUNCTION pgivm."IVM_immediate_before"() RETURNS trigger AS 'MODULE_PATHNAME', 'IVM_immediate_before' LANGUAGE C; CREATE FUNCTION pgivm."IVM_immediate_maintenance"() RETURNS trigger AS 'MODULE_PATHNAME', 'IVM_immediate_maintenance' LANGUAGE C; CREATE FUNCTION pgivm."IVM_prevent_immv_change"() RETURNS trigger AS 'MODULE_PATHNAME', 'IVM_prevent_immv_change' LANGUAGE C; GRANT SELECT ON TABLE pgivm.pg_ivm_immv TO PUBLIC; GRANT USAGE ON SCHEMA pgivm TO PUBLIC; pg_ivm-1.13/pg_ivm--1.11--1.12.sql000066400000000000000000000000001507512346500160060ustar00rootroot00000000000000pg_ivm-1.13/pg_ivm--1.12--1.13.sql000066400000000000000000000000001507512346500160100ustar00rootroot00000000000000pg_ivm-1.13/pg_ivm--1.2--1.3.sql000066400000000000000000000005721507512346500156640ustar00rootroot00000000000000-- functions CREATE FUNCTION ivm_visible_in_prestate(oid, tid, oid) RETURNS bool STABLE AS 'MODULE_PATHNAME', 'ivm_visible_in_prestate' LANGUAGE C; CREATE FUNCTION get_immv_def(IN immvrelid regclass) RETURNS text STRICT AS 'MODULE_PATHNAME', 'get_immv_def' LANGUAGE C; -- event trigger DROP EVENT TRIGGER pg_ivm_sql_drop_trigger; DROP FUNCTION pg_ivm_sql_drop_trigger_func; pg_ivm-1.13/pg_ivm--1.3--1.4.sql000066400000000000000000000000001507512346500156500ustar00rootroot00000000000000pg_ivm-1.13/pg_ivm--1.4--1.5.sql000066400000000000000000000000001507512346500156520ustar00rootroot00000000000000pg_ivm-1.13/pg_ivm--1.5--1.6.sql000066400000000000000000000000001507512346500156540ustar00rootroot00000000000000pg_ivm-1.13/pg_ivm--1.6--1.7.sql000066400000000000000000000000001507512346500156560ustar00rootroot00000000000000pg_ivm-1.13/pg_ivm--1.7--1.8.sql000066400000000000000000000000001507512346500156600ustar00rootroot00000000000000pg_ivm-1.13/pg_ivm--1.8--1.9.sql000066400000000000000000000000001507512346500156620ustar00rootroot00000000000000pg_ivm-1.13/pg_ivm--1.9--1.10.sql000066400000000000000000000012741507512346500157510ustar00rootroot00000000000000-- create a new schema pgivm and change the objects' schema to it CREATE SCHEMA pgivm; ALTER TABLE pg_ivm_immv SET SCHEMA pgivm; ALTER FUNCTION create_immv(text, text) SET SCHEMA pgivm; ALTER FUNCTION refresh_immv(text, bool) SET SCHEMA pgivm; ALTER FUNCTION get_immv_def(regclass) SET SCHEMA pgivm; ALTER FUNCTION ivm_visible_in_prestate(oid, tid, oid) SET SCHEMA pgivm; ALTER FUNCTION "IVM_immediate_before"() SET SCHEMA pgivm; ALTER FUNCTION "IVM_immediate_maintenance"() SET SCHEMA pgivm; ALTER FUNCTION "IVM_prevent_immv_change"() SET SCHEMA pgivm; GRANT USAGE ON SCHEMA pgivm TO PUBLIC; ALTER TABLE pgivm.pg_ivm_immv ADD COLUMN lastivmupdate xid8; -- drop a garbage DROP SCHEMA __pg_ivm__; pg_ivm-1.13/pg_ivm.c000066400000000000000000000265311507512346500143630ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * pg_ivm.c * incremental view maintenance extension * Routines for user interfaces and callback functions * * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group * Portions Copyright (c) 2022, IVM Development Group * *------------------------------------------------------------------------- */ #include "postgres.h" #include "access/genam.h" #include "access/table.h" #include "access/xact.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_namespace_d.h" #include "catalog/pg_trigger_d.h" #include "commands/trigger.h" #include "parser/analyze.h" #include "parser/parser.h" #include "parser/scansup.h" #include "tcop/tcopprot.h" #include "nodes/makefuncs.h" #include "utils/syscache.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/regproc.h" #include "utils/rel.h" #include "utils/varlena.h" #include "pg_ivm.h" PG_MODULE_MAGIC; static object_access_hook_type PrevObjectAccessHook = NULL; void _PG_init(void); static void IvmXactCallback(XactEvent event, void *arg); static void IvmSubXactCallback(SubXactEvent event, SubTransactionId mySubid, SubTransactionId parentSubid, void *arg); static void parseNameAndColumns(const char *string, List **names, List **colNames); static void PgIvmObjectAccessHook(ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg); /* SQL callable functions */ PG_FUNCTION_INFO_V1(create_immv); PG_FUNCTION_INFO_V1(refresh_immv); PG_FUNCTION_INFO_V1(IVM_prevent_immv_change); PG_FUNCTION_INFO_V1(get_immv_def); /* * Call back functions for cleaning up */ static void IvmXactCallback(XactEvent event, void *arg) { if (event == XACT_EVENT_PRE_COMMIT) AtPreCommit_IVM(); else if (event == XACT_EVENT_ABORT) AtAbort_IVM(InvalidSubTransactionId); } static void IvmSubXactCallback(SubXactEvent event, SubTransactionId mySubid, SubTransactionId parentSubid, void *arg) { if (event == SUBXACT_EVENT_ABORT_SUB) AtAbort_IVM(mySubid); } /* * Module load callback */ void _PG_init(void) { RegisterXactCallback(IvmXactCallback, NULL); RegisterSubXactCallback(IvmSubXactCallback, NULL); PrevObjectAccessHook = object_access_hook; object_access_hook = PgIvmObjectAccessHook; } /* * Given a C string, parse it into a qualified relation name * followed by an optional parenthesized list of column names. */ static void parseNameAndColumns(const char *string, List **names, List **colNames) { char *rawname; char *ptr; char *ptr2; bool in_quote; bool has_colnames = false; List *cols; ListCell *lc; /* We need a modifiable copy of the input string. */ rawname = pstrdup(string); /* Scan to find the expected left paren; mustn't be quoted */ in_quote = false; for (ptr = rawname; *ptr; ptr++) { if (*ptr == '"') in_quote = !in_quote; else if (*ptr == '(' && !in_quote) { has_colnames = true; break; } } /* Separate the name and parse it into a list */ *ptr++ = '\0'; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) *names = stringToQualifiedNameList(rawname, NULL); #else *names = stringToQualifiedNameList(rawname); #endif if (!has_colnames) goto end; /* Check for the trailing right parenthesis and remove it */ ptr2 = ptr + strlen(ptr); while (--ptr2 > ptr) { if (!scanner_isspace(*ptr2)) break; } if (*ptr2 != ')') ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("expected a right parenthesis"))); *ptr2 = '\0'; if (!SplitIdentifierString(ptr, ',', &cols)) ereport(ERROR, (errcode(ERRCODE_INVALID_NAME), errmsg("invalid name syntax"))); if (list_length(cols) == 0) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("must specify at least one column name"))); foreach(lc, cols) { char *colname = lfirst(lc); *colNames = lappend(*colNames, makeString(pstrdup(colname))); } end: pfree(rawname); } /* * User interface for creating an IMMV */ Datum create_immv(PG_FUNCTION_ARGS) { text *t_relname = PG_GETARG_TEXT_PP(0); text *t_sql = PG_GETARG_TEXT_PP(1); char *relname = text_to_cstring(t_relname); char *sql = text_to_cstring(t_sql); List *parsetree_list; RawStmt *parsetree; Query *query; QueryCompletion qc; List *names = NIL; List *colNames = NIL; ParseState *pstate = make_parsestate(NULL); CreateTableAsStmt *ctas; StringInfoData command_buf; parseNameAndColumns(relname, &names, &colNames); initStringInfo(&command_buf); appendStringInfo(&command_buf, "SELECT create_immv('%s' AS '%s');", relname, sql); appendStringInfo(&command_buf, "%s;", sql); pstate->p_sourcetext = command_buf.data; parsetree_list = pg_parse_query(sql); /* XXX: should we check t_sql before command_buf? */ if (list_length(parsetree_list) != 1) elog(ERROR, "invalid view definition"); parsetree = linitial_node(RawStmt, parsetree_list); /* view definition should specify SELECT query */ if (!IsA(parsetree->stmt, SelectStmt)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("view definition must specify SELECT statement"))); ctas = makeNode(CreateTableAsStmt); ctas->query = parsetree->stmt; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) ctas->objtype = OBJECT_MATVIEW; #else ctas->relkind = OBJECT_MATVIEW; #endif ctas->is_select_into = false; ctas->into = makeNode(IntoClause); ctas->into->rel = makeRangeVarFromNameList(names); ctas->into->colNames = colNames; ctas->into->accessMethod = NULL; ctas->into->options = NIL; ctas->into->onCommit = ONCOMMIT_NOOP; ctas->into->tableSpaceName = NULL; #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 180000) ctas->into->viewQuery = (Query *) parsetree->stmt; #else ctas->into->viewQuery = parsetree->stmt; #endif ctas->into->skipData = false; query = transformStmt(pstate, (Node *) ctas); Assert(query->commandType == CMD_UTILITY && IsA(query->utilityStmt, CreateTableAsStmt)); ExecCreateImmv(pstate, (CreateTableAsStmt *) query->utilityStmt, &qc); PG_RETURN_INT64(qc.nprocessed); } /* * User interface for refreshing an IMMV */ Datum refresh_immv(PG_FUNCTION_ARGS) { text *t_relname = PG_GETARG_TEXT_PP(0); bool ispopulated = PG_GETARG_BOOL(1); char *relname = text_to_cstring(t_relname); QueryCompletion qc; StringInfoData command_buf; initStringInfo(&command_buf); appendStringInfo(&command_buf, "SELECT refresh_immv('%s, %s);", relname, ispopulated ? "true" : "false"); ExecRefreshImmv(makeRangeVarFromNameList(textToQualifiedNameList(t_relname)), !ispopulated, command_buf.data, &qc); PG_RETURN_INT64(qc.nprocessed); } /* * Trigger function to prevent IMMV from being changed */ Datum IVM_prevent_immv_change(PG_FUNCTION_ARGS) { TriggerData *trigdata = (TriggerData *) fcinfo->context; Relation rel = trigdata->tg_relation; if (!ImmvIncrementalMaintenanceIsEnabled()) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot change materialized view \"%s\"", RelationGetRelationName(rel)))); return PointerGetDatum(NULL); } /* * Create triggers to prevent IMMV from being changed */ void CreateChangePreventTrigger(Oid matviewOid) { ObjectAddress refaddr; ObjectAddress address; CreateTrigStmt *ivm_trigger; int16 types[4] = {TRIGGER_TYPE_INSERT, TRIGGER_TYPE_DELETE, TRIGGER_TYPE_UPDATE, TRIGGER_TYPE_TRUNCATE}; int i; refaddr.classId = RelationRelationId; refaddr.objectId = matviewOid; refaddr.objectSubId = 0; ivm_trigger = makeNode(CreateTrigStmt); ivm_trigger->relation = NULL; ivm_trigger->row = false; ivm_trigger->timing = TRIGGER_TYPE_BEFORE; ivm_trigger->trigname = "IVM_prevent_immv_change"; ivm_trigger->funcname = PgIvmFuncName("IVM_prevent_immv_change"); ivm_trigger->columns = NIL; ivm_trigger->transitionRels = NIL; ivm_trigger->whenClause = NULL; ivm_trigger->isconstraint = false; ivm_trigger->deferrable = false; ivm_trigger->initdeferred = false; ivm_trigger->constrrel = NULL; ivm_trigger->args = NIL; for (i = 0; i < 4; i++) { ivm_trigger->events = types[i]; address = CreateTrigger(ivm_trigger, NULL, matviewOid, InvalidOid, InvalidOid, InvalidOid, InvalidOid, InvalidOid, NULL, true, false); recordDependencyOn(&address, &refaddr, DEPENDENCY_AUTO); } /* Make changes-so-far visible */ CommandCounterIncrement(); } /* * Get relid of pg_ivm_immv */ Oid PgIvmImmvRelationId(void) { return RangeVarGetRelid( makeRangeVar("pgivm", "pg_ivm_immv", -1), AccessShareLock, true); } /* * Get relid of pg_ivm_immv's primary key */ Oid PgIvmImmvPrimaryKeyIndexId(void) { return RangeVarGetRelid( makeRangeVar("pgivm", "pg_ivm_immv_pkey", -1), AccessShareLock, true); } /* * Return the SELECT part of an IMMV */ Datum get_immv_def(PG_FUNCTION_ARGS) { Oid matviewOid = PG_GETARG_OID(0); Relation matviewRel = NULL; Query *query = NULL; char *querystring = NULL; /* Make sure IMMV is a table. */ if (get_rel_relkind(matviewOid) != RELKIND_RELATION) PG_RETURN_NULL(); matviewRel = table_open(matviewOid, AccessShareLock); query = get_immv_query(matviewRel); if (query == NULL) { table_close(matviewRel, NoLock); PG_RETURN_NULL(); } querystring = pg_ivm_get_viewdef(matviewRel, false); table_close(matviewRel, NoLock); PG_RETURN_TEXT_P(cstring_to_text(querystring)); } /* * object_access_hook function for dropping an IMMV */ static void PgIvmObjectAccessHook(ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg) { if (PrevObjectAccessHook) PrevObjectAccessHook(access, classId, objectId, subId, arg); if (access == OAT_DROP && classId == RelationRelationId && !OidIsValid(subId)) { Relation pgIvmImmv; SysScanDesc scan; ScanKeyData key; HeapTuple tup; Oid pgIvmImmvOid = PgIvmImmvRelationId(); Oid pgIvmImmvPkOid = PgIvmImmvPrimaryKeyIndexId(); /* * Index or table not yet created (so no IMMVs yet), already dropped * (expect IMMVs also gone soon), or renamed. It's not great that a * rename of either object will silently break IMMVs, but that's * better than ERROR below. */ if (pgIvmImmvPkOid == InvalidOid || pgIvmImmvOid == InvalidOid) return; pgIvmImmv = table_open(pgIvmImmvOid, AccessShareLock); ScanKeyInit(&key, Anum_pg_ivm_immv_immvrelid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(objectId)); scan = systable_beginscan(pgIvmImmv, pgIvmImmvPkOid, true, NULL, 1, &key); tup = systable_getnext(scan); if (HeapTupleIsValid(tup)) CatalogTupleDelete(pgIvmImmv, &tup->t_self); systable_endscan(scan); table_close(pgIvmImmv, NoLock); } } /* * isImmv * * Check if this is an IMMV from oid. */ bool isImmv(Oid immv_oid) { Relation pgIvmImmv = table_open(PgIvmImmvRelationId(), AccessShareLock); SysScanDesc scan; ScanKeyData key; HeapTuple tup; ScanKeyInit(&key, Anum_pg_ivm_immv_immvrelid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(immv_oid)); scan = systable_beginscan(pgIvmImmv, PgIvmImmvPrimaryKeyIndexId(), true, NULL, 1, &key); tup = systable_getnext(scan); systable_endscan(scan); table_close(pgIvmImmv, NoLock); if (!HeapTupleIsValid(tup)) return false; else return true; } /* PgIvmFuncName() * Build a properly-qualified reference to a pg_ivm internal function. */ List * PgIvmFuncName(char *name) { return list_make2(makeString("pgivm"), makeString(name)); } pg_ivm-1.13/pg_ivm.control000066400000000000000000000003051507512346500156100ustar00rootroot00000000000000# incremental view maintenance extension comment = 'incremental view maintenance on PostgreSQL' default_version = '1.13' module_pathname = '$libdir/pg_ivm' relocatable = false schema = pg_catalog pg_ivm-1.13/pg_ivm.h000066400000000000000000000047021507512346500143640ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * pg_ivm.h * incremental view maintenance extension * * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group * Portions Copyright (c) 2022, IVM Development Group * *------------------------------------------------------------------------- */ #ifndef _PG_IVM_H_ #define _PG_IVM_H_ #include "catalog/objectaddress.h" #include "fmgr.h" #include "nodes/params.h" #include "nodes/pathnodes.h" #include "parser/parse_node.h" #include "tcop/dest.h" #include "utils/queryenvironment.h" #define Natts_pg_ivm_immv 4 #define Anum_pg_ivm_immv_immvrelid 1 #define Anum_pg_ivm_immv_viewdef 2 #define Anum_pg_ivm_immv_ispopulated 3 #define Anum_pg_ivm_immv_lastivmupdate 4 /* pg_ivm.c */ extern void CreateChangePreventTrigger(Oid matviewOid); extern Oid PgIvmImmvRelationId(void); extern Oid PgIvmImmvPrimaryKeyIndexId(void); extern bool isImmv(Oid immv_oid); extern List *PgIvmFuncName(char *name); /* createas.c */ extern ObjectAddress ExecCreateImmv(ParseState *pstate, CreateTableAsStmt *stmt, QueryCompletion *qc); extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid); extern void CreateIndexOnIMMV(Query *query, Relation matviewRel); extern Query *rewriteQueryForIMMV(Query *query, List *colNames); extern void makeIvmAggColumn(ParseState *pstate, Aggref *aggref, char *resname, AttrNumber *next_resno, List **aggs); /* matview.c */ extern Query *get_immv_query(Relation matviewRel); extern ObjectAddress ExecRefreshImmv(const RangeVar *relation, bool skipData, const char *queryString, QueryCompletion *qc); extern ObjectAddress RefreshImmvByOid(Oid matviewOid, bool is_create, bool skipData, const char *queryString, QueryCompletion *qc); extern bool ImmvIncrementalMaintenanceIsEnabled(void); extern PGDLLEXPORT Datum IVM_immediate_before(PG_FUNCTION_ARGS); extern PGDLLEXPORT Datum IVM_immediate_maintenance(PG_FUNCTION_ARGS); extern Query* rewrite_query_for_exists_subquery(Query *query); extern PGDLLEXPORT Datum ivm_visible_in_prestate(PG_FUNCTION_ARGS); extern void AtAbort_IVM(SubTransactionId subtxid); extern void AtPreCommit_IVM(void); extern char *getColumnNameStartWith(RangeTblEntry *rte, char *str, int *attnum); extern bool isIvmName(const char *s); /* ruleutils.c */ extern char *pg_ivm_get_viewdef(Relation immvrel, bool pretty); /* subselect.c */ extern void inline_cte(PlannerInfo *root, CommonTableExpr *cte); #endif pg_ivm-1.13/rpm/000077500000000000000000000000001507512346500135255ustar00rootroot00000000000000pg_ivm-1.13/rpm/pg_ivm.spec000066400000000000000000000054061507512346500156670ustar00rootroot00000000000000# How to build RPM: # # rpmbuild -bb pg_ivm.spec --define "pgmajorversion 18" --define "pginstdir /usr/pgsql-18" %global sname pg_ivm %if 0%{?rhel} && 0%{?rhel} >= 7 %global llvm 1 %endif Summary: Incremental View Maintenance (IVM) feature for PostgreSQL. Name: %{sname}_%{pgmajorversion} Version: 1.13 Release: 1%{dist} License: PostgreSQL Vendor: IVM Development Group URL: https://github.com/sraoss/%{sname} Source0: https://github.com/sraoss/%{sname}/archive/v%{version}.tar.gz BuildRequires: postgresql%{pgmajorversion}-devel Requires: postgresql%{pgmajorversion}-server %description pg_ivm provides Incremnetal View Maintenance feature for PostgreSQL. Incremental View Maintenance (IVM) is a way to make materialized views up-to-date in which only incremental changes are computed and applied on views rather than recomputing. %prep %setup -q -n %{sname}-%{version} %build PATH=%{pginstdir}/bin:$PATH %{__make} %{?_smp_mflags} %install %{__rm} -rf %{buildroot} PATH=%{pginstdir}/bin:$PATH %{__make} %{?_smp_mflags} INSTALL_PREFIX=%{buildroot} DESTDIR=%{buildroot} install # Install documentation with a better name: %{__mkdir} -p %{buildroot}%{pginstdir}/doc/extension %{__cp} README.md %{buildroot}%{pginstdir}/doc/extension/README-%{sname}.md %clean %{__rm} -rf %{buildroot} %files %defattr(-,root,root,-) %license LICENSE %doc %{pginstdir}/doc/extension/README-%{sname}.md %{pginstdir}/lib/%{sname}.so %{pginstdir}/share/extension/%{sname}-*.sql %{pginstdir}/share/extension/%{sname}.control %if %llvm %{pginstdir}/lib/bitcode/%{sname}*.bc %{pginstdir}/lib/bitcode/%{sname}/*.bc %endif %changelog * Mon Oct 20 2025 - Yugo Nagata 1.13-1 - Update to 1.13 * Mon Sep 4 2025 - Yugo Nagata 1.12-1 - Update to 1.12 * Mon May 25 2025 - Yugo Nagata 1.11-1 - Update to 1.11 * Tue Mar 11 2025 - Yugo Nagata 1.10-1 - Update to 1.10 * Fri Jul 31 2024 - Yugo Nagata 1.9-1 - Update to 1.9 * Fri Mar 1 2024 - Yugo Nagata 1.8-1 - Update to 1.8 * Thu Sep 13 2023 - Yugo Nagata 1.7-1 - Update to 1.7 * Thu Aug 31 2023 - Yugo Nagata 1.6-1 - Update to 1.6 * Thu Mar 2 2023 - Yugo Nagata 1.5.1-1 - Update to 1.5.1 * Mon Jun 30 2023 - Yugo Nagata 1.5-1 - Update to 1.5 * Fri Dec 16 2022 - Yugo Nagata 1.4-1 - Update to 1.4 * Fri Sep 30 2022 - Yugo Nagata 1.3-1 - Update to 1.3 * Mon Jul 25 2022 - Yugo Nagata 1.2-1 - Update to 1.2 * Thu Jun 23 2022 - Yugo Nagata 1.1-1 - Update to 1.1 * Thu Jun 2 2022 - Yugo Nagata 1.0-1 - Initial pg_ivm 1.0 RPM from IVM Development Group pg_ivm-1.13/ruleutils.c000066400000000000000000000050311507512346500151220ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * ruleutils.c * incremental view maintenance extension * Routines for convert stored expressions/querytrees back to * source text * * Portions Copyright (c) 2022, IVM Development Group * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * *------------------------------------------------------------------------- */ #include "postgres.h" #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 150000) #include "utils/rel.h" #include "utils/ruleutils.h" #elif defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 140000) #include "ruleutils_14.c" #else #include "ruleutils_13.c" #endif #include "pg_ivm.h" /* Standard conversion of a "bool pretty" option to detailed flags */ #define GET_PRETTY_FLAGS(pretty) \ ((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \ : PRETTYFLAG_INDENT) /* ---------- * pg_ivm_get_viewdef * * Public entry point to deparse a view definition query parsetree. * The pretty flags are determined by GET_PRETTY_FLAGS(pretty). * * The result is a palloc'd C string. * ---------- */ char * pg_ivm_get_viewdef(Relation immvrel, bool pretty) { Query *query = get_immv_query(immvrel); TupleDesc resultDesc = RelationGetDescr(immvrel); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 150000) ListCell *lc; int colno = 0; /* * Rewrite the result column name using the view's tuple * descriptor. * * The column name is usually figured out in get_query_def * using a tupleDesc specified as an argument, but this * function is static, so we cannot directly call it. * Therefore, we rewrite them prior to calling the public * function pg_get_querydef (for PG15 or higher). */ query = copyObject(query); foreach (lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); if (tle->resjunk) continue; /* ignore junk entries */ colno++; if (resultDesc && colno <= resultDesc->natts) tle->resname = NameStr(TupleDescAttr(resultDesc, colno - 1)->attname); } return pg_get_querydef(query, pretty); #else StringInfoData buf; int prettyFlags; prettyFlags = GET_PRETTY_FLAGS(pretty); initStringInfo(&buf); /* * For PG14 or earlier, we use get_query_def which is copied * from the core because any public function for this purpose * is not available. */ get_query_def(query, &buf, NIL, resultDesc, true, prettyFlags, WRAP_COLUMN_DEFAULT, 0); return buf.data; #endif } pg_ivm-1.13/ruleutils_13.c000066400000000000000000007017251507512346500154420ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * ruleutils_13.c * Functions to convert stored expressions/querytrees back to * source text * * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * * Part of src/backend/utils/adt/ruleutils.c in PostgreSQL 13 core * *------------------------------------------------------------------------- */ #include "postgres.h" #include "access/relation.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_opclass.h" #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "commands/defrem.h" #include "common/keywords.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "nodes/pathnodes.h" #include "optimizer/optimizer.h" #include "parser/parse_agg.h" #include "parser/parse_func.h" #include "parser/parse_oper.h" #include "parser/parse_relation.h" #include "parser/parser.h" #include "parser/parsetree.h" #include "rewrite/rewriteHandler.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/hsearch.h" #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/ruleutils.h" #include "utils/syscache.h" #include "utils/typcache.h" #include "utils/xml.h" /* ---------- * Pretty formatting constants * ---------- */ /* Indent counts */ #define PRETTYINDENT_STD 8 #define PRETTYINDENT_JOIN 4 #define PRETTYINDENT_VAR 4 #define PRETTYINDENT_LIMIT 40 /* wrap limit */ /* Pretty flags */ #define PRETTYFLAG_PAREN 0x0001 #define PRETTYFLAG_INDENT 0x0002 #define PRETTYFLAG_SCHEMA 0x0004 /* Default line length for pretty-print wrapping: 0 means wrap always */ #define WRAP_COLUMN_DEFAULT 0 /* macros to test if pretty action needed */ #define PRETTY_PAREN(context) ((context)->prettyFlags & PRETTYFLAG_PAREN) #define PRETTY_INDENT(context) ((context)->prettyFlags & PRETTYFLAG_INDENT) #define PRETTY_SCHEMA(context) ((context)->prettyFlags & PRETTYFLAG_SCHEMA) /* ---------- * Local data types * ---------- */ /* Context info needed for invoking a recursive querytree display routine */ typedef struct { StringInfo buf; /* output buffer to append to */ List *namespaces; /* List of deparse_namespace nodes */ List *windowClause; /* Current query level's WINDOW clause */ List *windowTList; /* targetlist for resolving WINDOW clause */ int prettyFlags; /* enabling of pretty-print functions */ int wrapColumn; /* max line length, or -1 for no limit */ int indentLevel; /* current indent level for pretty-print */ bool varprefix; /* true to print prefixes on Vars */ ParseExprKind special_exprkind; /* set only for exprkinds needing special * handling */ Bitmapset *appendparents; /* if not null, map child Vars of these relids * back to the parent rel */ } deparse_context; /* * Each level of query context around a subtree needs a level of Var namespace. * A Var having varlevelsup=N refers to the N'th item (counting from 0) in * the current context's namespaces list. * * rtable is the list of actual RTEs from the Query or PlannedStmt. * rtable_names holds the alias name to be used for each RTE (either a C * string, or NULL for nameless RTEs such as unnamed joins). * rtable_columns holds the column alias names to be used for each RTE. * * subplans is a list of Plan trees for SubPlans and CTEs (it's only used * in the PlannedStmt case). * ctes is a list of CommonTableExpr nodes (only used in the Query case). * appendrels, if not null (it's only used in the PlannedStmt case), is an * array of AppendRelInfo nodes, indexed by child relid. We use that to map * child-table Vars to their inheritance parents. * * In some cases we need to make names of merged JOIN USING columns unique * across the whole query, not only per-RTE. If so, unique_using is true * and using_names is a list of C strings representing names already assigned * to USING columns. * * When deparsing plan trees, there is always just a single item in the * deparse_namespace list (since a plan tree never contains Vars with * varlevelsup > 0). We store the Plan node that is the immediate * parent of the expression to be deparsed, as well as a list of that * Plan's ancestors. In addition, we store its outer and inner subplan nodes, * as well as their targetlists, and the index tlist if the current plan node * might contain INDEX_VAR Vars. (These fields could be derived on-the-fly * from the current Plan node, but it seems notationally clearer to set them * up as separate fields.) */ typedef struct { List *rtable; /* List of RangeTblEntry nodes */ List *rtable_names; /* Parallel list of names for RTEs */ List *rtable_columns; /* Parallel list of deparse_columns structs */ List *subplans; /* List of Plan trees for SubPlans */ List *ctes; /* List of CommonTableExpr nodes */ AppendRelInfo **appendrels; /* Array of AppendRelInfo nodes, or NULL */ /* Workspace for column alias assignment: */ bool unique_using; /* Are we making USING names globally unique */ List *using_names; /* List of assigned names for USING columns */ /* Remaining fields are used only when deparsing a Plan tree: */ Plan *plan; /* immediate parent of current expression */ List *ancestors; /* ancestors of plan */ Plan *outer_plan; /* outer subnode, or NULL if none */ Plan *inner_plan; /* inner subnode, or NULL if none */ List *outer_tlist; /* referent for OUTER_VAR Vars */ List *inner_tlist; /* referent for INNER_VAR Vars */ List *index_tlist; /* referent for INDEX_VAR Vars */ } deparse_namespace; /* * Per-relation data about column alias names. * * Selecting aliases is unreasonably complicated because of the need to dump * rules/views whose underlying tables may have had columns added, deleted, or * renamed since the query was parsed. We must nonetheless print the rule/view * in a form that can be reloaded and will produce the same results as before. * * For each RTE used in the query, we must assign column aliases that are * unique within that RTE. SQL does not require this of the original query, * but due to factors such as *-expansion we need to be able to uniquely * reference every column in a decompiled query. As long as we qualify all * column references, per-RTE uniqueness is sufficient for that. * * However, we can't ensure per-column name uniqueness for unnamed join RTEs, * since they just inherit column names from their input RTEs, and we can't * rename the columns at the join level. Most of the time this isn't an issue * because we don't need to reference the join's output columns as such; we * can reference the input columns instead. That approach can fail for merged * JOIN USING columns, however, so when we have one of those in an unnamed * join, we have to make that column's alias globally unique across the whole * query to ensure it can be referenced unambiguously. * * Another problem is that a JOIN USING clause requires the columns to be * merged to have the same aliases in both input RTEs, and that no other * columns in those RTEs or their children conflict with the USING names. * To handle that, we do USING-column alias assignment in a recursive * traversal of the query's jointree. When descending through a JOIN with * USING, we preassign the USING column names to the child columns, overriding * other rules for column alias assignment. We also mark each RTE with a list * of all USING column names selected for joins containing that RTE, so that * when we assign other columns' aliases later, we can avoid conflicts. * * Another problem is that if a JOIN's input tables have had columns added or * deleted since the query was parsed, we must generate a column alias list * for the join that matches the current set of input columns --- otherwise, a * change in the number of columns in the left input would throw off matching * of aliases to columns of the right input. Thus, positions in the printable * column alias list are not necessarily one-for-one with varattnos of the * JOIN, so we need a separate new_colnames[] array for printing purposes. */ typedef struct { /* * colnames is an array containing column aliases to use for columns that * existed when the query was parsed. Dropped columns have NULL entries. * This array can be directly indexed by varattno to get a Var's name. * * Non-NULL entries are guaranteed unique within the RTE, *except* when * this is for an unnamed JOIN RTE. In that case we merely copy up names * from the two input RTEs. * * During the recursive descent in set_using_names(), forcible assignment * of a child RTE's column name is represented by pre-setting that element * of the child's colnames array. So at that stage, NULL entries in this * array just mean that no name has been preassigned, not necessarily that * the column is dropped. */ int num_cols; /* length of colnames[] array */ char **colnames; /* array of C strings and NULLs */ /* * new_colnames is an array containing column aliases to use for columns * that would exist if the query was re-parsed against the current * definitions of its base tables. This is what to print as the column * alias list for the RTE. This array does not include dropped columns, * but it will include columns added since original parsing. Indexes in * it therefore have little to do with current varattno values. As above, * entries are unique unless this is for an unnamed JOIN RTE. (In such an * RTE, we never actually print this array, but we must compute it anyway * for possible use in computing column names of upper joins.) The * parallel array is_new_col marks which of these columns are new since * original parsing. Entries with is_new_col false must match the * non-NULL colnames entries one-for-one. */ int num_new_cols; /* length of new_colnames[] array */ char **new_colnames; /* array of C strings */ bool *is_new_col; /* array of bool flags */ /* This flag tells whether we should actually print a column alias list */ bool printaliases; /* This list has all names used as USING names in joins above this RTE */ List *parentUsing; /* names assigned to parent merged columns */ /* * If this struct is for a JOIN RTE, we fill these fields during the * set_using_names() pass to describe its relationship to its child RTEs. * * leftattnos and rightattnos are arrays with one entry per existing * output column of the join (hence, indexable by join varattno). For a * simple reference to a column of the left child, leftattnos[i] is the * child RTE's attno and rightattnos[i] is zero; and conversely for a * column of the right child. But for merged columns produced by JOIN * USING/NATURAL JOIN, both leftattnos[i] and rightattnos[i] are nonzero. * Note that a simple reference might be to a child RTE column that's been * dropped; but that's OK since the column could not be used in the query. * * If it's a JOIN USING, usingNames holds the alias names selected for the * merged columns (these might be different from the original USING list, * if we had to modify names to achieve uniqueness). */ int leftrti; /* rangetable index of left child */ int rightrti; /* rangetable index of right child */ int *leftattnos; /* left-child varattnos of join cols, or 0 */ int *rightattnos; /* right-child varattnos of join cols, or 0 */ List *usingNames; /* names assigned to merged columns */ } deparse_columns; /* This macro is analogous to rt_fetch(), but for deparse_columns structs */ #define deparse_columns_fetch(rangetable_index, dpns) \ ((deparse_columns *) list_nth((dpns)->rtable_columns, (rangetable_index)-1)) /* * Entry in set_rtable_names' hash table */ typedef struct { char name[NAMEDATALEN]; /* Hash key --- must be first */ int counter; /* Largest addition used so far for name */ } NameHashEntry; /* Callback signature for resolve_special_varno() */ typedef void (*rsv_callback) (Node *node, deparse_context *context, void *callback_arg); /* ---------- * Local functions * * Most of these functions used to use fixed-size buffers to build their * results. Now, they take an (already initialized) StringInfo object * as a parameter, and append their text output to its contents. * ---------- */ static void set_rtable_names(deparse_namespace *dpns, List *parent_namespaces, Bitmapset *rels_used); static void set_deparse_for_query(deparse_namespace *dpns, Query *query, List *parent_namespaces); static bool has_dangerous_join_using(deparse_namespace *dpns, Node *jtnode); static void set_using_names(deparse_namespace *dpns, Node *jtnode, List *parentUsing); static void set_relation_column_names(deparse_namespace *dpns, RangeTblEntry *rte, deparse_columns *colinfo); static void set_join_column_names(deparse_namespace *dpns, RangeTblEntry *rte, deparse_columns *colinfo); static bool colname_is_unique(const char *colname, deparse_namespace *dpns, deparse_columns *colinfo); static char *make_colname_unique(char *colname, deparse_namespace *dpns, deparse_columns *colinfo); static void expand_colnames_array_to(deparse_columns *colinfo, int n); static void identify_join_columns(JoinExpr *j, RangeTblEntry *jrte, deparse_columns *colinfo); static char *get_rtable_name(int rtindex, deparse_context *context); static void set_deparse_plan(deparse_namespace *dpns, Plan *plan); static void push_child_plan(deparse_namespace *dpns, Plan *plan, deparse_namespace *save_dpns); static void pop_child_plan(deparse_namespace *dpns, deparse_namespace *save_dpns); static void push_ancestor_plan(deparse_namespace *dpns, ListCell *ancestor_cell, deparse_namespace *save_dpns); static void pop_ancestor_plan(deparse_namespace *dpns, deparse_namespace *save_dpns); static void get_query_def(Query *query, StringInfo buf, List *parentnamespace, TupleDesc resultDesc, bool colNamesVisible, int prettyFlags, int wrapColumn, int startIndent); static void get_values_def(List *values_lists, deparse_context *context); static void get_with_clause(Query *query, deparse_context *context); static void get_select_query_def(Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible); static void get_insert_query_def(Query *query, deparse_context *context, bool colNamesVisible); static void get_update_query_def(Query *query, deparse_context *context, bool colNamesVisible); static void get_update_query_targetlist_def(Query *query, List *targetList, deparse_context *context, RangeTblEntry *rte); static void get_delete_query_def(Query *query, deparse_context *context, bool colNamesVisible); static void get_utility_query_def(Query *query, deparse_context *context); static void get_basic_select_query(Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible); static void get_target_list(List *targetList, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible); static void get_setop_query(Node *setOp, Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible); static Node *get_rule_sortgroupclause(Index ref, List *tlist, bool force_colno, deparse_context *context); static void get_rule_groupingset(GroupingSet *gset, List *targetlist, bool omit_parens, deparse_context *context); static void get_rule_orderby(List *orderList, List *targetList, bool force_colno, deparse_context *context); static void get_rule_windowclause(Query *query, deparse_context *context); static void get_rule_windowspec(WindowClause *wc, List *targetList, deparse_context *context); static char *get_variable(Var *var, int levelsup, bool istoplevel, deparse_context *context); static void get_special_variable(Node *node, deparse_context *context, void *callback_arg); static void resolve_special_varno(Node *node, deparse_context *context, rsv_callback callback, void *callback_arg); static Node *find_param_referent(Param *param, deparse_context *context, deparse_namespace **dpns_p, ListCell **ancestor_cell_p); static void get_parameter(Param *param, deparse_context *context); static const char *get_simple_binary_op_name(OpExpr *expr); static bool isSimpleNode(Node *node, Node *parentNode, int prettyFlags); static void appendContextKeyword(deparse_context *context, const char *str, int indentBefore, int indentAfter, int indentPlus); static void removeStringInfoSpaces(StringInfo str); static void get_rule_expr(Node *node, deparse_context *context, bool showimplicit); static void get_rule_expr_toplevel(Node *node, deparse_context *context, bool showimplicit); static void get_rule_list_toplevel(List *lst, deparse_context *context, bool showimplicit); static void get_rule_expr_funccall(Node *node, deparse_context *context, bool showimplicit); static bool looks_like_function(Node *node); static void get_oper_expr(OpExpr *expr, deparse_context *context); static void get_func_expr(FuncExpr *expr, deparse_context *context, bool showimplicit); static void get_agg_expr(Aggref *aggref, deparse_context *context, Aggref *original_aggref); static void get_agg_combine_expr(Node *node, deparse_context *context, void *callback_arg); static void get_windowfunc_expr(WindowFunc *wfunc, deparse_context *context); static void get_coercion_expr(Node *arg, deparse_context *context, Oid resulttype, int32 resulttypmod, Node *parentNode); static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void simple_quote_literal(StringInfo buf, const char *val); static void get_sublink_expr(SubLink *sublink, deparse_context *context); static void get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit); static void get_from_clause(Query *query, const char *prefix, deparse_context *context); static void get_from_clause_item(Node *jtnode, Query *query, deparse_context *context); static void get_column_alias_list(deparse_columns *colinfo, deparse_context *context); static void get_from_clause_coldeflist(RangeTblFunction *rtfunc, deparse_columns *colinfo, deparse_context *context); static void get_tablesample_def(TableSampleClause *tablesample, deparse_context *context); static void get_opclass_name(Oid opclass, Oid actual_datatype, StringInfo buf); static Node *processIndirection(Node *node, deparse_context *context); static void printSubscripts(SubscriptingRef *sbsref, deparse_context *context); static char *get_relation_name(Oid relid); static char *generate_relation_name(Oid relid, List *namespaces); static char *generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes, bool has_variadic, bool *use_variadic_p, ParseExprKind special_exprkind); static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") /* * set_rtable_names: select RTE aliases to be used in printing a query * * We fill in dpns->rtable_names with a list of names that is one-for-one with * the already-filled dpns->rtable list. Each RTE name is unique among those * in the new namespace plus any ancestor namespaces listed in * parent_namespaces. * * If rels_used isn't NULL, only RTE indexes listed in it are given aliases. * * Note that this function is only concerned with relation names, not column * names. */ static void set_rtable_names(deparse_namespace *dpns, List *parent_namespaces, Bitmapset *rels_used) { HASHCTL hash_ctl; HTAB *names_hash; NameHashEntry *hentry; bool found; int rtindex; ListCell *lc; dpns->rtable_names = NIL; /* nothing more to do if empty rtable */ if (dpns->rtable == NIL) return; /* * We use a hash table to hold known names, so that this process is O(N) * not O(N^2) for N names. */ MemSet(&hash_ctl, 0, sizeof(hash_ctl)); hash_ctl.keysize = NAMEDATALEN; hash_ctl.entrysize = sizeof(NameHashEntry); hash_ctl.hcxt = CurrentMemoryContext; names_hash = hash_create("set_rtable_names names", list_length(dpns->rtable), &hash_ctl, HASH_ELEM | HASH_CONTEXT); /* Preload the hash table with names appearing in parent_namespaces */ foreach(lc, parent_namespaces) { deparse_namespace *olddpns = (deparse_namespace *) lfirst(lc); ListCell *lc2; foreach(lc2, olddpns->rtable_names) { char *oldname = (char *) lfirst(lc2); if (oldname == NULL) continue; hentry = (NameHashEntry *) hash_search(names_hash, oldname, HASH_ENTER, &found); /* we do not complain about duplicate names in parent namespaces */ hentry->counter = 0; } } /* Now we can scan the rtable */ rtindex = 1; foreach(lc, dpns->rtable) { RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); char *refname; /* Just in case this takes an unreasonable amount of time ... */ CHECK_FOR_INTERRUPTS(); if (rels_used && !bms_is_member(rtindex, rels_used)) { /* Ignore unreferenced RTE */ refname = NULL; } else if (rte->alias) { /* If RTE has a user-defined alias, prefer that */ refname = rte->alias->aliasname; } else if (rte->rtekind == RTE_RELATION) { /* Use the current actual name of the relation */ refname = get_rel_name(rte->relid); } else if (rte->rtekind == RTE_JOIN) { /* Unnamed join has no refname */ refname = NULL; } else { /* Otherwise use whatever the parser assigned */ refname = rte->eref->aliasname; } /* * If the selected name isn't unique, append digits to make it so, and * make a new hash entry for it once we've got a unique name. For a * very long input name, we might have to truncate to stay within * NAMEDATALEN. */ if (refname) { hentry = (NameHashEntry *) hash_search(names_hash, refname, HASH_ENTER, &found); if (found) { /* Name already in use, must choose a new one */ int refnamelen = strlen(refname); char *modname = (char *) palloc(refnamelen + 16); NameHashEntry *hentry2; do { hentry->counter++; for (;;) { /* * We avoid using %.*s here because it can misbehave * if the data is not valid in what libc thinks is the * prevailing encoding. */ memcpy(modname, refname, refnamelen); sprintf(modname + refnamelen, "_%d", hentry->counter); if (strlen(modname) < NAMEDATALEN) break; /* drop chars from refname to keep all the digits */ refnamelen = pg_mbcliplen(refname, refnamelen, refnamelen - 1); } hentry2 = (NameHashEntry *) hash_search(names_hash, modname, HASH_ENTER, &found); } while (found); hentry2->counter = 0; /* init new hash entry */ refname = modname; } else { /* Name not previously used, need only initialize hentry */ hentry->counter = 0; } } dpns->rtable_names = lappend(dpns->rtable_names, refname); rtindex++; } hash_destroy(names_hash); } /* * set_deparse_for_query: set up deparse_namespace for deparsing a Query tree * * For convenience, this is defined to initialize the deparse_namespace struct * from scratch. */ static void set_deparse_for_query(deparse_namespace *dpns, Query *query, List *parent_namespaces) { ListCell *lc; ListCell *lc2; /* Initialize *dpns and fill rtable/ctes links */ memset(dpns, 0, sizeof(deparse_namespace)); dpns->rtable = query->rtable; dpns->subplans = NIL; dpns->ctes = query->cteList; dpns->appendrels = NULL; /* Assign a unique relation alias to each RTE */ set_rtable_names(dpns, parent_namespaces, NULL); /* Initialize dpns->rtable_columns to contain zeroed structs */ dpns->rtable_columns = NIL; while (list_length(dpns->rtable_columns) < list_length(dpns->rtable)) dpns->rtable_columns = lappend(dpns->rtable_columns, palloc0(sizeof(deparse_columns))); /* If it's a utility query, it won't have a jointree */ if (query->jointree) { /* Detect whether global uniqueness of USING names is needed */ dpns->unique_using = has_dangerous_join_using(dpns, (Node *) query->jointree); /* * Select names for columns merged by USING, via a recursive pass over * the query jointree. */ set_using_names(dpns, (Node *) query->jointree, NIL); } /* * Now assign remaining column aliases for each RTE. We do this in a * linear scan of the rtable, so as to process RTEs whether or not they * are in the jointree (we mustn't miss NEW.*, INSERT target relations, * etc). JOIN RTEs must be processed after their children, but this is * okay because they appear later in the rtable list than their children * (cf Asserts in identify_join_columns()). */ forboth(lc, dpns->rtable, lc2, dpns->rtable_columns) { RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); deparse_columns *colinfo = (deparse_columns *) lfirst(lc2); if (rte->rtekind == RTE_JOIN) set_join_column_names(dpns, rte, colinfo); else set_relation_column_names(dpns, rte, colinfo); } } /* * has_dangerous_join_using: search jointree for unnamed JOIN USING * * Merged columns of a JOIN USING may act differently from either of the input * columns, either because they are merged with COALESCE (in a FULL JOIN) or * because an implicit coercion of the underlying input column is required. * In such a case the column must be referenced as a column of the JOIN not as * a column of either input. And this is problematic if the join is unnamed * (alias-less): we cannot qualify the column's name with an RTE name, since * there is none. (Forcibly assigning an alias to the join is not a solution, * since that will prevent legal references to tables below the join.) * To ensure that every column in the query is unambiguously referenceable, * we must assign such merged columns names that are globally unique across * the whole query, aliasing other columns out of the way as necessary. * * Because the ensuing re-aliasing is fairly damaging to the readability of * the query, we don't do this unless we have to. So, we must pre-scan * the join tree to see if we have to, before starting set_using_names(). */ static bool has_dangerous_join_using(deparse_namespace *dpns, Node *jtnode) { if (IsA(jtnode, RangeTblRef)) { /* nothing to do here */ } else if (IsA(jtnode, FromExpr)) { FromExpr *f = (FromExpr *) jtnode; ListCell *lc; foreach(lc, f->fromlist) { if (has_dangerous_join_using(dpns, (Node *) lfirst(lc))) return true; } } else if (IsA(jtnode, JoinExpr)) { JoinExpr *j = (JoinExpr *) jtnode; /* Is it an unnamed JOIN with USING? */ if (j->alias == NULL && j->usingClause) { /* * Yes, so check each join alias var to see if any of them are not * simple references to underlying columns. If so, we have a * dangerous situation and must pick unique aliases. */ RangeTblEntry *jrte = rt_fetch(j->rtindex, dpns->rtable); /* We need only examine the merged columns */ for (int i = 0; i < jrte->joinmergedcols; i++) { Node *aliasvar = list_nth(jrte->joinaliasvars, i); if (!IsA(aliasvar, Var)) return true; } } /* Nope, but inspect children */ if (has_dangerous_join_using(dpns, j->larg)) return true; if (has_dangerous_join_using(dpns, j->rarg)) return true; } else elog(ERROR, "unrecognized node type: %d", (int) nodeTag(jtnode)); return false; } /* * set_using_names: select column aliases to be used for merged USING columns * * We do this during a recursive descent of the query jointree. * dpns->unique_using must already be set to determine the global strategy. * * Column alias info is saved in the dpns->rtable_columns list, which is * assumed to be filled with pre-zeroed deparse_columns structs. * * parentUsing is a list of all USING aliases assigned in parent joins of * the current jointree node. (The passed-in list must not be modified.) */ static void set_using_names(deparse_namespace *dpns, Node *jtnode, List *parentUsing) { if (IsA(jtnode, RangeTblRef)) { /* nothing to do now */ } else if (IsA(jtnode, FromExpr)) { FromExpr *f = (FromExpr *) jtnode; ListCell *lc; foreach(lc, f->fromlist) set_using_names(dpns, (Node *) lfirst(lc), parentUsing); } else if (IsA(jtnode, JoinExpr)) { JoinExpr *j = (JoinExpr *) jtnode; RangeTblEntry *rte = rt_fetch(j->rtindex, dpns->rtable); deparse_columns *colinfo = deparse_columns_fetch(j->rtindex, dpns); int *leftattnos; int *rightattnos; deparse_columns *leftcolinfo; deparse_columns *rightcolinfo; int i; ListCell *lc; /* Get info about the shape of the join */ identify_join_columns(j, rte, colinfo); leftattnos = colinfo->leftattnos; rightattnos = colinfo->rightattnos; /* Look up the not-yet-filled-in child deparse_columns structs */ leftcolinfo = deparse_columns_fetch(colinfo->leftrti, dpns); rightcolinfo = deparse_columns_fetch(colinfo->rightrti, dpns); /* * If this join is unnamed, then we cannot substitute new aliases at * this level, so any name requirements pushed down to here must be * pushed down again to the children. */ if (rte->alias == NULL) { for (i = 0; i < colinfo->num_cols; i++) { char *colname = colinfo->colnames[i]; if (colname == NULL) continue; /* Push down to left column, unless it's a system column */ if (leftattnos[i] > 0) { expand_colnames_array_to(leftcolinfo, leftattnos[i]); leftcolinfo->colnames[leftattnos[i] - 1] = colname; } /* Same on the righthand side */ if (rightattnos[i] > 0) { expand_colnames_array_to(rightcolinfo, rightattnos[i]); rightcolinfo->colnames[rightattnos[i] - 1] = colname; } } } /* * If there's a USING clause, select the USING column names and push * those names down to the children. We have two strategies: * * If dpns->unique_using is true, we force all USING names to be * unique across the whole query level. In principle we'd only need * the names of dangerous USING columns to be globally unique, but to * safely assign all USING names in a single pass, we have to enforce * the same uniqueness rule for all of them. However, if a USING * column's name has been pushed down from the parent, we should use * it as-is rather than making a uniqueness adjustment. This is * necessary when we're at an unnamed join, and it creates no risk of * ambiguity. Also, if there's a user-written output alias for a * merged column, we prefer to use that rather than the input name; * this simplifies the logic and seems likely to lead to less aliasing * overall. * * If dpns->unique_using is false, we only need USING names to be * unique within their own join RTE. We still need to honor * pushed-down names, though. * * Though significantly different in results, these two strategies are * implemented by the same code, with only the difference of whether * to put assigned names into dpns->using_names. */ if (j->usingClause) { /* Copy the input parentUsing list so we don't modify it */ parentUsing = list_copy(parentUsing); /* USING names must correspond to the first join output columns */ expand_colnames_array_to(colinfo, list_length(j->usingClause)); i = 0; foreach(lc, j->usingClause) { char *colname = strVal(lfirst(lc)); /* Assert it's a merged column */ Assert(leftattnos[i] != 0 && rightattnos[i] != 0); /* Adopt passed-down name if any, else select unique name */ if (colinfo->colnames[i] != NULL) colname = colinfo->colnames[i]; else { /* Prefer user-written output alias if any */ if (rte->alias && i < list_length(rte->alias->colnames)) colname = strVal(list_nth(rte->alias->colnames, i)); /* Make it appropriately unique */ colname = make_colname_unique(colname, dpns, colinfo); if (dpns->unique_using) dpns->using_names = lappend(dpns->using_names, colname); /* Save it as output column name, too */ colinfo->colnames[i] = colname; } /* Remember selected names for use later */ colinfo->usingNames = lappend(colinfo->usingNames, colname); parentUsing = lappend(parentUsing, colname); /* Push down to left column, unless it's a system column */ if (leftattnos[i] > 0) { expand_colnames_array_to(leftcolinfo, leftattnos[i]); leftcolinfo->colnames[leftattnos[i] - 1] = colname; } /* Same on the righthand side */ if (rightattnos[i] > 0) { expand_colnames_array_to(rightcolinfo, rightattnos[i]); rightcolinfo->colnames[rightattnos[i] - 1] = colname; } i++; } } /* Mark child deparse_columns structs with correct parentUsing info */ leftcolinfo->parentUsing = parentUsing; rightcolinfo->parentUsing = parentUsing; /* Now recursively assign USING column names in children */ set_using_names(dpns, j->larg, parentUsing); set_using_names(dpns, j->rarg, parentUsing); } else elog(ERROR, "unrecognized node type: %d", (int) nodeTag(jtnode)); } /* * set_relation_column_names: select column aliases for a non-join RTE * * Column alias info is saved in *colinfo, which is assumed to be pre-zeroed. * If any colnames entries are already filled in, those override local * choices. */ static void set_relation_column_names(deparse_namespace *dpns, RangeTblEntry *rte, deparse_columns *colinfo) { int ncolumns; char **real_colnames; bool changed_any; int noldcolumns; int i; int j; /* * Construct an array of the current "real" column names of the RTE. * real_colnames[] will be indexed by physical column number, with NULL * entries for dropped columns. */ if (rte->rtekind == RTE_RELATION) { /* Relation --- look to the system catalogs for up-to-date info */ Relation rel; TupleDesc tupdesc; rel = relation_open(rte->relid, AccessShareLock); tupdesc = RelationGetDescr(rel); ncolumns = tupdesc->natts; real_colnames = (char **) palloc(ncolumns * sizeof(char *)); for (i = 0; i < ncolumns; i++) { Form_pg_attribute attr = TupleDescAttr(tupdesc, i); if (attr->attisdropped) real_colnames[i] = NULL; else real_colnames[i] = pstrdup(NameStr(attr->attname)); } relation_close(rel, AccessShareLock); } else { /* Otherwise get the column names from eref or expandRTE() */ List *colnames; ListCell *lc; /* * Functions returning composites have the annoying property that some * of the composite type's columns might have been dropped since the * query was parsed. If possible, use expandRTE() to handle that * case, since it has the tedious logic needed to find out about * dropped columns. However, if we're explaining a plan, then we * don't have rte->functions because the planner thinks that won't be * needed later, and that breaks expandRTE(). So in that case we have * to rely on rte->eref, which may lead us to report a dropped * column's old name; that seems close enough for EXPLAIN's purposes. * * For non-RELATION, non-FUNCTION RTEs, we can just look at rte->eref, * which should be sufficiently up-to-date: no other RTE types can * have columns get dropped from under them after parsing. */ if (rte->rtekind == RTE_FUNCTION && rte->functions != NIL) { /* Since we're not creating Vars, rtindex etc. don't matter */ expandRTE(rte, 1, 0, -1, true /* include dropped */ , &colnames, NULL); } else colnames = rte->eref->colnames; ncolumns = list_length(colnames); real_colnames = (char **) palloc(ncolumns * sizeof(char *)); i = 0; foreach(lc, colnames) { /* * If the column name we find here is an empty string, then it's a * dropped column, so change to NULL. */ char *cname = strVal(lfirst(lc)); if (cname[0] == '\0') cname = NULL; real_colnames[i] = cname; i++; } } /* * Ensure colinfo->colnames has a slot for each column. (It could be long * enough already, if we pushed down a name for the last column.) Note: * it's possible that there are now more columns than there were when the * query was parsed, ie colnames could be longer than rte->eref->colnames. * We must assign unique aliases to the new columns too, else there could * be unresolved conflicts when the view/rule is reloaded. */ expand_colnames_array_to(colinfo, ncolumns); Assert(colinfo->num_cols == ncolumns); /* * Make sufficiently large new_colnames and is_new_col arrays, too. * * Note: because we leave colinfo->num_new_cols zero until after the loop, * colname_is_unique will not consult that array, which is fine because it * would only be duplicate effort. */ colinfo->new_colnames = (char **) palloc(ncolumns * sizeof(char *)); colinfo->is_new_col = (bool *) palloc(ncolumns * sizeof(bool)); /* * Scan the columns, select a unique alias for each one, and store it in * colinfo->colnames and colinfo->new_colnames. The former array has NULL * entries for dropped columns, the latter omits them. Also mark * new_colnames entries as to whether they are new since parse time; this * is the case for entries beyond the length of rte->eref->colnames. */ noldcolumns = list_length(rte->eref->colnames); changed_any = false; j = 0; for (i = 0; i < ncolumns; i++) { char *real_colname = real_colnames[i]; char *colname = colinfo->colnames[i]; /* Skip dropped columns */ if (real_colname == NULL) { Assert(colname == NULL); /* colnames[i] is already NULL */ continue; } /* If alias already assigned, that's what to use */ if (colname == NULL) { /* If user wrote an alias, prefer that over real column name */ if (rte->alias && i < list_length(rte->alias->colnames)) colname = strVal(list_nth(rte->alias->colnames, i)); else colname = real_colname; /* Unique-ify and insert into colinfo */ colname = make_colname_unique(colname, dpns, colinfo); colinfo->colnames[i] = colname; } /* Put names of non-dropped columns in new_colnames[] too */ colinfo->new_colnames[j] = colname; /* And mark them as new or not */ colinfo->is_new_col[j] = (i >= noldcolumns); j++; /* Remember if any assigned aliases differ from "real" name */ if (!changed_any && strcmp(colname, real_colname) != 0) changed_any = true; } /* * Set correct length for new_colnames[] array. (Note: if columns have * been added, colinfo->num_cols includes them, which is not really quite * right but is harmless, since any new columns must be at the end where * they won't affect varattnos of pre-existing columns.) */ colinfo->num_new_cols = j; /* * For a relation RTE, we need only print the alias column names if any * are different from the underlying "real" names. For a function RTE, * always emit a complete column alias list; this is to protect against * possible instability of the default column names (eg, from altering * parameter names). For tablefunc RTEs, we never print aliases, because * the column names are part of the clause itself. For other RTE types, * print if we changed anything OR if there were user-written column * aliases (since the latter would be part of the underlying "reality"). */ if (rte->rtekind == RTE_RELATION) colinfo->printaliases = changed_any; else if (rte->rtekind == RTE_FUNCTION) colinfo->printaliases = true; else if (rte->rtekind == RTE_TABLEFUNC) colinfo->printaliases = false; else if (rte->alias && rte->alias->colnames != NIL) colinfo->printaliases = true; else colinfo->printaliases = changed_any; } /* * set_join_column_names: select column aliases for a join RTE * * Column alias info is saved in *colinfo, which is assumed to be pre-zeroed. * If any colnames entries are already filled in, those override local * choices. Also, names for USING columns were already chosen by * set_using_names(). We further expect that column alias selection has been * completed for both input RTEs. */ static void set_join_column_names(deparse_namespace *dpns, RangeTblEntry *rte, deparse_columns *colinfo) { deparse_columns *leftcolinfo; deparse_columns *rightcolinfo; bool changed_any; int noldcolumns; int nnewcolumns; Bitmapset *leftmerged = NULL; Bitmapset *rightmerged = NULL; int i; int j; int ic; int jc; /* Look up the previously-filled-in child deparse_columns structs */ leftcolinfo = deparse_columns_fetch(colinfo->leftrti, dpns); rightcolinfo = deparse_columns_fetch(colinfo->rightrti, dpns); /* * Ensure colinfo->colnames has a slot for each column. (It could be long * enough already, if we pushed down a name for the last column.) Note: * it's possible that one or both inputs now have more columns than there * were when the query was parsed, but we'll deal with that below. We * only need entries in colnames for pre-existing columns. */ noldcolumns = list_length(rte->eref->colnames); expand_colnames_array_to(colinfo, noldcolumns); Assert(colinfo->num_cols == noldcolumns); /* * Scan the join output columns, select an alias for each one, and store * it in colinfo->colnames. If there are USING columns, set_using_names() * already selected their names, so we can start the loop at the first * non-merged column. */ changed_any = false; for (i = list_length(colinfo->usingNames); i < noldcolumns; i++) { char *colname = colinfo->colnames[i]; char *real_colname; /* Join column must refer to at least one input column */ Assert(colinfo->leftattnos[i] != 0 || colinfo->rightattnos[i] != 0); /* Get the child column name */ if (colinfo->leftattnos[i] > 0) real_colname = leftcolinfo->colnames[colinfo->leftattnos[i] - 1]; else if (colinfo->rightattnos[i] > 0) real_colname = rightcolinfo->colnames[colinfo->rightattnos[i] - 1]; else { /* We're joining system columns --- use eref name */ real_colname = strVal(list_nth(rte->eref->colnames, i)); } /* If child col has been dropped, no need to assign a join colname */ if (real_colname == NULL) { colinfo->colnames[i] = NULL; continue; } /* In an unnamed join, just report child column names as-is */ if (rte->alias == NULL) { colinfo->colnames[i] = real_colname; continue; } /* If alias already assigned, that's what to use */ if (colname == NULL) { /* If user wrote an alias, prefer that over real column name */ if (rte->alias && i < list_length(rte->alias->colnames)) colname = strVal(list_nth(rte->alias->colnames, i)); else colname = real_colname; /* Unique-ify and insert into colinfo */ colname = make_colname_unique(colname, dpns, colinfo); colinfo->colnames[i] = colname; } /* Remember if any assigned aliases differ from "real" name */ if (!changed_any && strcmp(colname, real_colname) != 0) changed_any = true; } /* * Calculate number of columns the join would have if it were re-parsed * now, and create storage for the new_colnames and is_new_col arrays. * * Note: colname_is_unique will be consulting new_colnames[] during the * loops below, so its not-yet-filled entries must be zeroes. */ nnewcolumns = leftcolinfo->num_new_cols + rightcolinfo->num_new_cols - list_length(colinfo->usingNames); colinfo->num_new_cols = nnewcolumns; colinfo->new_colnames = (char **) palloc0(nnewcolumns * sizeof(char *)); colinfo->is_new_col = (bool *) palloc0(nnewcolumns * sizeof(bool)); /* * Generating the new_colnames array is a bit tricky since any new columns * added since parse time must be inserted in the right places. This code * must match the parser, which will order a join's columns as merged * columns first (in USING-clause order), then non-merged columns from the * left input (in attnum order), then non-merged columns from the right * input (ditto). If one of the inputs is itself a join, its columns will * be ordered according to the same rule, which means newly-added columns * might not be at the end. We can figure out what's what by consulting * the leftattnos and rightattnos arrays plus the input is_new_col arrays. * * In these loops, i indexes leftattnos/rightattnos (so it's join varattno * less one), j indexes new_colnames/is_new_col, and ic/jc have similar * meanings for the current child RTE. */ /* Handle merged columns; they are first and can't be new */ i = j = 0; while (i < noldcolumns && colinfo->leftattnos[i] != 0 && colinfo->rightattnos[i] != 0) { /* column name is already determined and known unique */ colinfo->new_colnames[j] = colinfo->colnames[i]; colinfo->is_new_col[j] = false; /* build bitmapsets of child attnums of merged columns */ if (colinfo->leftattnos[i] > 0) leftmerged = bms_add_member(leftmerged, colinfo->leftattnos[i]); if (colinfo->rightattnos[i] > 0) rightmerged = bms_add_member(rightmerged, colinfo->rightattnos[i]); i++, j++; } /* Handle non-merged left-child columns */ ic = 0; for (jc = 0; jc < leftcolinfo->num_new_cols; jc++) { char *child_colname = leftcolinfo->new_colnames[jc]; if (!leftcolinfo->is_new_col[jc]) { /* Advance ic to next non-dropped old column of left child */ while (ic < leftcolinfo->num_cols && leftcolinfo->colnames[ic] == NULL) ic++; Assert(ic < leftcolinfo->num_cols); ic++; /* If it is a merged column, we already processed it */ if (bms_is_member(ic, leftmerged)) continue; /* Else, advance i to the corresponding existing join column */ while (i < colinfo->num_cols && colinfo->colnames[i] == NULL) i++; Assert(i < colinfo->num_cols); Assert(ic == colinfo->leftattnos[i]); /* Use the already-assigned name of this column */ colinfo->new_colnames[j] = colinfo->colnames[i]; i++; } else { /* * Unique-ify the new child column name and assign, unless we're * in an unnamed join, in which case just copy */ if (rte->alias != NULL) { colinfo->new_colnames[j] = make_colname_unique(child_colname, dpns, colinfo); if (!changed_any && strcmp(colinfo->new_colnames[j], child_colname) != 0) changed_any = true; } else colinfo->new_colnames[j] = child_colname; } colinfo->is_new_col[j] = leftcolinfo->is_new_col[jc]; j++; } /* Handle non-merged right-child columns in exactly the same way */ ic = 0; for (jc = 0; jc < rightcolinfo->num_new_cols; jc++) { char *child_colname = rightcolinfo->new_colnames[jc]; if (!rightcolinfo->is_new_col[jc]) { /* Advance ic to next non-dropped old column of right child */ while (ic < rightcolinfo->num_cols && rightcolinfo->colnames[ic] == NULL) ic++; Assert(ic < rightcolinfo->num_cols); ic++; /* If it is a merged column, we already processed it */ if (bms_is_member(ic, rightmerged)) continue; /* Else, advance i to the corresponding existing join column */ while (i < colinfo->num_cols && colinfo->colnames[i] == NULL) i++; Assert(i < colinfo->num_cols); Assert(ic == colinfo->rightattnos[i]); /* Use the already-assigned name of this column */ colinfo->new_colnames[j] = colinfo->colnames[i]; i++; } else { /* * Unique-ify the new child column name and assign, unless we're * in an unnamed join, in which case just copy */ if (rte->alias != NULL) { colinfo->new_colnames[j] = make_colname_unique(child_colname, dpns, colinfo); if (!changed_any && strcmp(colinfo->new_colnames[j], child_colname) != 0) changed_any = true; } else colinfo->new_colnames[j] = child_colname; } colinfo->is_new_col[j] = rightcolinfo->is_new_col[jc]; j++; } /* Assert we processed the right number of columns */ #ifdef USE_ASSERT_CHECKING while (i < colinfo->num_cols && colinfo->colnames[i] == NULL) i++; Assert(i == colinfo->num_cols); Assert(j == nnewcolumns); #endif /* * For a named join, print column aliases if we changed any from the child * names. Unnamed joins cannot print aliases. */ if (rte->alias != NULL) colinfo->printaliases = changed_any; else colinfo->printaliases = false; } /* * colname_is_unique: is colname distinct from already-chosen column names? * * dpns is query-wide info, colinfo is for the column's RTE */ static bool colname_is_unique(const char *colname, deparse_namespace *dpns, deparse_columns *colinfo) { int i; ListCell *lc; /* Check against already-assigned column aliases within RTE */ for (i = 0; i < colinfo->num_cols; i++) { char *oldname = colinfo->colnames[i]; if (oldname && strcmp(oldname, colname) == 0) return false; } /* * If we're building a new_colnames array, check that too (this will be * partially but not completely redundant with the previous checks) */ for (i = 0; i < colinfo->num_new_cols; i++) { char *oldname = colinfo->new_colnames[i]; if (oldname && strcmp(oldname, colname) == 0) return false; } /* Also check against USING-column names that must be globally unique */ foreach(lc, dpns->using_names) { char *oldname = (char *) lfirst(lc); if (strcmp(oldname, colname) == 0) return false; } /* Also check against names already assigned for parent-join USING cols */ foreach(lc, colinfo->parentUsing) { char *oldname = (char *) lfirst(lc); if (strcmp(oldname, colname) == 0) return false; } return true; } /* * make_colname_unique: modify colname if necessary to make it unique * * dpns is query-wide info, colinfo is for the column's RTE */ static char * make_colname_unique(char *colname, deparse_namespace *dpns, deparse_columns *colinfo) { /* * If the selected name isn't unique, append digits to make it so. For a * very long input name, we might have to truncate to stay within * NAMEDATALEN. */ if (!colname_is_unique(colname, dpns, colinfo)) { int colnamelen = strlen(colname); char *modname = (char *) palloc(colnamelen + 16); int i = 0; do { i++; for (;;) { /* * We avoid using %.*s here because it can misbehave if the * data is not valid in what libc thinks is the prevailing * encoding. */ memcpy(modname, colname, colnamelen); sprintf(modname + colnamelen, "_%d", i); if (strlen(modname) < NAMEDATALEN) break; /* drop chars from colname to keep all the digits */ colnamelen = pg_mbcliplen(colname, colnamelen, colnamelen - 1); } } while (!colname_is_unique(modname, dpns, colinfo)); colname = modname; } return colname; } /* * expand_colnames_array_to: make colinfo->colnames at least n items long * * Any added array entries are initialized to zero. */ static void expand_colnames_array_to(deparse_columns *colinfo, int n) { if (n > colinfo->num_cols) { if (colinfo->colnames == NULL) colinfo->colnames = (char **) palloc0(n * sizeof(char *)); else { colinfo->colnames = (char **) repalloc(colinfo->colnames, n * sizeof(char *)); memset(colinfo->colnames + colinfo->num_cols, 0, (n - colinfo->num_cols) * sizeof(char *)); } colinfo->num_cols = n; } } /* * identify_join_columns: figure out where columns of a join come from * * Fills the join-specific fields of the colinfo struct, except for * usingNames which is filled later. */ static void identify_join_columns(JoinExpr *j, RangeTblEntry *jrte, deparse_columns *colinfo) { int numjoincols; int jcolno; int rcolno; ListCell *lc; /* Extract left/right child RT indexes */ if (IsA(j->larg, RangeTblRef)) colinfo->leftrti = ((RangeTblRef *) j->larg)->rtindex; else if (IsA(j->larg, JoinExpr)) colinfo->leftrti = ((JoinExpr *) j->larg)->rtindex; else elog(ERROR, "unrecognized node type in jointree: %d", (int) nodeTag(j->larg)); if (IsA(j->rarg, RangeTblRef)) colinfo->rightrti = ((RangeTblRef *) j->rarg)->rtindex; else if (IsA(j->rarg, JoinExpr)) colinfo->rightrti = ((JoinExpr *) j->rarg)->rtindex; else elog(ERROR, "unrecognized node type in jointree: %d", (int) nodeTag(j->rarg)); /* Assert children will be processed earlier than join in second pass */ Assert(colinfo->leftrti < j->rtindex); Assert(colinfo->rightrti < j->rtindex); /* Initialize result arrays with zeroes */ numjoincols = list_length(jrte->joinaliasvars); Assert(numjoincols == list_length(jrte->eref->colnames)); colinfo->leftattnos = (int *) palloc0(numjoincols * sizeof(int)); colinfo->rightattnos = (int *) palloc0(numjoincols * sizeof(int)); /* * Deconstruct RTE's joinleftcols/joinrightcols into desired format. * Recall that the column(s) merged due to USING are the first column(s) * of the join output. We need not do anything special while scanning * joinleftcols, but while scanning joinrightcols we must distinguish * merged from unmerged columns. */ jcolno = 0; foreach(lc, jrte->joinleftcols) { int leftattno = lfirst_int(lc); colinfo->leftattnos[jcolno++] = leftattno; } rcolno = 0; foreach(lc, jrte->joinrightcols) { int rightattno = lfirst_int(lc); if (rcolno < jrte->joinmergedcols) /* merged column? */ colinfo->rightattnos[rcolno] = rightattno; else colinfo->rightattnos[jcolno++] = rightattno; rcolno++; } Assert(jcolno == numjoincols); } /* * get_rtable_name: convenience function to get a previously assigned RTE alias * * The RTE must belong to the topmost namespace level in "context". */ static char * get_rtable_name(int rtindex, deparse_context *context) { deparse_namespace *dpns = (deparse_namespace *) linitial(context->namespaces); Assert(rtindex > 0 && rtindex <= list_length(dpns->rtable_names)); return (char *) list_nth(dpns->rtable_names, rtindex - 1); } /* * set_deparse_plan: set up deparse_namespace to parse subexpressions * of a given Plan node * * This sets the plan, outer_plan, inner_plan, outer_tlist, inner_tlist, * and index_tlist fields. Caller is responsible for adjusting the ancestors * list if necessary. Note that the rtable, subplans, and ctes fields do * not need to change when shifting attention to different plan nodes in a * single plan tree. */ static void set_deparse_plan(deparse_namespace *dpns, Plan *plan) { dpns->plan = plan; /* * We special-case Append and MergeAppend to pretend that the first child * plan is the OUTER referent; we have to interpret OUTER Vars in their * tlists according to one of the children, and the first one is the most * natural choice. Likewise special-case ModifyTable to pretend that the * first child plan is the OUTER referent; this is to support RETURNING * lists containing references to non-target relations. */ if (IsA(plan, Append)) dpns->outer_plan = linitial(((Append *) plan)->appendplans); else if (IsA(plan, MergeAppend)) dpns->outer_plan = linitial(((MergeAppend *) plan)->mergeplans); else if (IsA(plan, ModifyTable)) dpns->outer_plan = linitial(((ModifyTable *) plan)->plans); else dpns->outer_plan = outerPlan(plan); if (dpns->outer_plan) dpns->outer_tlist = dpns->outer_plan->targetlist; else dpns->outer_tlist = NIL; /* * For a SubqueryScan, pretend the subplan is INNER referent. (We don't * use OUTER because that could someday conflict with the normal meaning.) * Likewise, for a CteScan, pretend the subquery's plan is INNER referent. * For ON CONFLICT .. UPDATE we just need the inner tlist to point to the * excluded expression's tlist. (Similar to the SubqueryScan we don't want * to reuse OUTER, it's used for RETURNING in some modify table cases, * although not INSERT .. CONFLICT). */ if (IsA(plan, SubqueryScan)) dpns->inner_plan = ((SubqueryScan *) plan)->subplan; else if (IsA(plan, CteScan)) dpns->inner_plan = list_nth(dpns->subplans, ((CteScan *) plan)->ctePlanId - 1); else if (IsA(plan, ModifyTable)) dpns->inner_plan = plan; else dpns->inner_plan = innerPlan(plan); if (IsA(plan, ModifyTable)) dpns->inner_tlist = ((ModifyTable *) plan)->exclRelTlist; else if (dpns->inner_plan) dpns->inner_tlist = dpns->inner_plan->targetlist; else dpns->inner_tlist = NIL; /* Set up referent for INDEX_VAR Vars, if needed */ if (IsA(plan, IndexOnlyScan)) dpns->index_tlist = ((IndexOnlyScan *) plan)->indextlist; else if (IsA(plan, ForeignScan)) dpns->index_tlist = ((ForeignScan *) plan)->fdw_scan_tlist; else if (IsA(plan, CustomScan)) dpns->index_tlist = ((CustomScan *) plan)->custom_scan_tlist; else dpns->index_tlist = NIL; } /* * push_child_plan: temporarily transfer deparsing attention to a child plan * * When expanding an OUTER_VAR or INNER_VAR reference, we must adjust the * deparse context in case the referenced expression itself uses * OUTER_VAR/INNER_VAR. We modify the top stack entry in-place to avoid * affecting levelsup issues (although in a Plan tree there really shouldn't * be any). * * Caller must provide a local deparse_namespace variable to save the * previous state for pop_child_plan. */ static void push_child_plan(deparse_namespace *dpns, Plan *plan, deparse_namespace *save_dpns) { /* Save state for restoration later */ *save_dpns = *dpns; /* Link current plan node into ancestors list */ dpns->ancestors = lcons(dpns->plan, dpns->ancestors); /* Set attention on selected child */ set_deparse_plan(dpns, plan); } /* * pop_child_plan: undo the effects of push_child_plan */ static void pop_child_plan(deparse_namespace *dpns, deparse_namespace *save_dpns) { List *ancestors; /* Get rid of ancestors list cell added by push_child_plan */ ancestors = list_delete_first(dpns->ancestors); /* Restore fields changed by push_child_plan */ *dpns = *save_dpns; /* Make sure dpns->ancestors is right (may be unnecessary) */ dpns->ancestors = ancestors; } /* * push_ancestor_plan: temporarily transfer deparsing attention to an * ancestor plan * * When expanding a Param reference, we must adjust the deparse context * to match the plan node that contains the expression being printed; * otherwise we'd fail if that expression itself contains a Param or * OUTER_VAR/INNER_VAR/INDEX_VAR variable. * * The target ancestor is conveniently identified by the ListCell holding it * in dpns->ancestors. * * Caller must provide a local deparse_namespace variable to save the * previous state for pop_ancestor_plan. */ static void push_ancestor_plan(deparse_namespace *dpns, ListCell *ancestor_cell, deparse_namespace *save_dpns) { Plan *plan = (Plan *) lfirst(ancestor_cell); /* Save state for restoration later */ *save_dpns = *dpns; /* Build a new ancestor list with just this node's ancestors */ dpns->ancestors = list_copy_tail(dpns->ancestors, list_cell_number(dpns->ancestors, ancestor_cell) + 1); /* Set attention on selected ancestor */ set_deparse_plan(dpns, plan); } /* * pop_ancestor_plan: undo the effects of push_ancestor_plan */ static void pop_ancestor_plan(deparse_namespace *dpns, deparse_namespace *save_dpns) { /* Free the ancestor list made in push_ancestor_plan */ list_free(dpns->ancestors); /* Restore fields changed by push_ancestor_plan */ *dpns = *save_dpns; } /* ---------- * get_query_def - Parse back one query parsetree * * query: parsetree to be displayed * buf: output text is appended to buf * parentnamespace: list (initially empty) of outer-level deparse_namespace's * resultDesc: if not NULL, the output tuple descriptor for the view * represented by a SELECT query. We use the column names from it * to label SELECT output columns, in preference to names in the query * colNamesVisible: true if the surrounding context cares about the output * column names at all (as, for example, an EXISTS() context does not); * when false, we can suppress dummy column labels such as "?column?" * prettyFlags: bitmask of PRETTYFLAG_XXX options * wrapColumn: maximum line length, or -1 to disable wrapping * startIndent: initial indentation amount * ---------- */ static void get_query_def(Query *query, StringInfo buf, List *parentnamespace, TupleDesc resultDesc, bool colNamesVisible, int prettyFlags, int wrapColumn, int startIndent) { deparse_context context; deparse_namespace dpns; /* Guard against excessively long or deeply-nested queries */ CHECK_FOR_INTERRUPTS(); check_stack_depth(); /* * Before we begin to examine the query, acquire locks on referenced * relations, and fix up deleted columns in JOIN RTEs. This ensures * consistent results. Note we assume it's OK to scribble on the passed * querytree! * * We are only deparsing the query (we are not about to execute it), so we * only need AccessShareLock on the relations it mentions. */ AcquireRewriteLocks(query, false, false); context.buf = buf; context.namespaces = lcons(&dpns, list_copy(parentnamespace)); context.windowClause = NIL; context.windowTList = NIL; context.varprefix = (parentnamespace != NIL || list_length(query->rtable) != 1); context.prettyFlags = prettyFlags; context.wrapColumn = wrapColumn; context.indentLevel = startIndent; context.special_exprkind = EXPR_KIND_NONE; context.appendparents = NULL; set_deparse_for_query(&dpns, query, parentnamespace); switch (query->commandType) { case CMD_SELECT: get_select_query_def(query, &context, resultDesc, colNamesVisible); break; case CMD_UPDATE: get_update_query_def(query, &context, colNamesVisible); break; case CMD_INSERT: get_insert_query_def(query, &context, colNamesVisible); break; case CMD_DELETE: get_delete_query_def(query, &context, colNamesVisible); break; case CMD_NOTHING: appendStringInfoString(buf, "NOTHING"); break; case CMD_UTILITY: get_utility_query_def(query, &context); break; default: elog(ERROR, "unrecognized query command type: %d", query->commandType); break; } } /* ---------- * get_values_def - Parse back a VALUES list * ---------- */ static void get_values_def(List *values_lists, deparse_context *context) { StringInfo buf = context->buf; bool first_list = true; ListCell *vtl; appendStringInfoString(buf, "VALUES "); foreach(vtl, values_lists) { List *sublist = (List *) lfirst(vtl); bool first_col = true; ListCell *lc; if (first_list) first_list = false; else appendStringInfoString(buf, ", "); appendStringInfoChar(buf, '('); foreach(lc, sublist) { Node *col = (Node *) lfirst(lc); if (first_col) first_col = false; else appendStringInfoChar(buf, ','); /* * Print the value. Whole-row Vars need special treatment. */ get_rule_expr_toplevel(col, context, false); } appendStringInfoChar(buf, ')'); } } /* ---------- * get_with_clause - Parse back a WITH clause * ---------- */ static void get_with_clause(Query *query, deparse_context *context) { StringInfo buf = context->buf; const char *sep; ListCell *l; if (query->cteList == NIL) return; if (PRETTY_INDENT(context)) { context->indentLevel += PRETTYINDENT_STD; appendStringInfoChar(buf, ' '); } if (query->hasRecursive) sep = "WITH RECURSIVE "; else sep = "WITH "; foreach(l, query->cteList) { CommonTableExpr *cte = (CommonTableExpr *) lfirst(l); appendStringInfoString(buf, sep); appendStringInfoString(buf, quote_identifier(cte->ctename)); if (cte->aliascolnames) { bool first = true; ListCell *col; appendStringInfoChar(buf, '('); foreach(col, cte->aliascolnames) { if (first) first = false; else appendStringInfoString(buf, ", "); appendStringInfoString(buf, quote_identifier(strVal(lfirst(col)))); } appendStringInfoChar(buf, ')'); } appendStringInfoString(buf, " AS "); switch (cte->ctematerialized) { case CTEMaterializeDefault: break; case CTEMaterializeAlways: appendStringInfoString(buf, "MATERIALIZED "); break; case CTEMaterializeNever: appendStringInfoString(buf, "NOT MATERIALIZED "); break; } appendStringInfoChar(buf, '('); if (PRETTY_INDENT(context)) appendContextKeyword(context, "", 0, 0, 0); get_query_def((Query *) cte->ctequery, buf, context->namespaces, NULL, true, context->prettyFlags, context->wrapColumn, context->indentLevel); if (PRETTY_INDENT(context)) appendContextKeyword(context, "", 0, 0, 0); appendStringInfoChar(buf, ')'); sep = ", "; } if (PRETTY_INDENT(context)) { context->indentLevel -= PRETTYINDENT_STD; appendContextKeyword(context, "", 0, 0, 0); } else appendStringInfoChar(buf, ' '); } /* ---------- * get_select_query_def - Parse back a SELECT parsetree * ---------- */ static void get_select_query_def(Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible) { StringInfo buf = context->buf; List *save_windowclause; List *save_windowtlist; bool force_colno; ListCell *l; /* Insert the WITH clause if given */ get_with_clause(query, context); /* Set up context for possible window functions */ save_windowclause = context->windowClause; context->windowClause = query->windowClause; save_windowtlist = context->windowTList; context->windowTList = query->targetList; /* * If the Query node has a setOperations tree, then it's the top level of * a UNION/INTERSECT/EXCEPT query; only the WITH, ORDER BY and LIMIT * fields are interesting in the top query itself. */ if (query->setOperations) { get_setop_query(query->setOperations, query, context, resultDesc, colNamesVisible); /* ORDER BY clauses must be simple in this case */ force_colno = true; } else { get_basic_select_query(query, context, resultDesc, colNamesVisible); force_colno = false; } /* Add the ORDER BY clause if given */ if (query->sortClause != NIL) { appendContextKeyword(context, " ORDER BY ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_orderby(query->sortClause, query->targetList, force_colno, context); } /* * Add the LIMIT/OFFSET clauses if given. If non-default options, use the * standard spelling of LIMIT. */ if (query->limitOffset != NULL) { appendContextKeyword(context, " OFFSET ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); get_rule_expr(query->limitOffset, context, false); } if (query->limitCount != NULL) { if (query->limitOption == LIMIT_OPTION_WITH_TIES) { appendContextKeyword(context, " FETCH FIRST ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); get_rule_expr(query->limitCount, context, false); appendStringInfo(buf, " ROWS WITH TIES"); } else { appendContextKeyword(context, " LIMIT ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); if (IsA(query->limitCount, Const) && ((Const *) query->limitCount)->constisnull) appendStringInfoString(buf, "ALL"); else get_rule_expr(query->limitCount, context, false); } } /* Add FOR [KEY] UPDATE/SHARE clauses if present */ if (query->hasForUpdate) { foreach(l, query->rowMarks) { RowMarkClause *rc = (RowMarkClause *) lfirst(l); /* don't print implicit clauses */ if (rc->pushedDown) continue; switch (rc->strength) { case LCS_NONE: /* we intentionally throw an error for LCS_NONE */ elog(ERROR, "unrecognized LockClauseStrength %d", (int) rc->strength); break; case LCS_FORKEYSHARE: appendContextKeyword(context, " FOR KEY SHARE", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); break; case LCS_FORSHARE: appendContextKeyword(context, " FOR SHARE", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); break; case LCS_FORNOKEYUPDATE: appendContextKeyword(context, " FOR NO KEY UPDATE", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); break; case LCS_FORUPDATE: appendContextKeyword(context, " FOR UPDATE", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); break; } appendStringInfo(buf, " OF %s", quote_identifier(get_rtable_name(rc->rti, context))); if (rc->waitPolicy == LockWaitError) appendStringInfoString(buf, " NOWAIT"); else if (rc->waitPolicy == LockWaitSkip) appendStringInfoString(buf, " SKIP LOCKED"); } } context->windowClause = save_windowclause; context->windowTList = save_windowtlist; } /* * Detect whether query looks like SELECT ... FROM VALUES(), * with no need to rename the output columns of the VALUES RTE. * If so, return the VALUES RTE. Otherwise return NULL. */ static RangeTblEntry * get_simple_values_rte(Query *query, TupleDesc resultDesc) { RangeTblEntry *result = NULL; ListCell *lc; /* * We want to detect a match even if the Query also contains OLD or NEW * rule RTEs. So the idea is to scan the rtable and see if there is only * one inFromCl RTE that is a VALUES RTE. */ foreach(lc, query->rtable) { RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); if (rte->rtekind == RTE_VALUES && rte->inFromCl) { if (result) return NULL; /* multiple VALUES (probably not possible) */ result = rte; } else if (rte->rtekind == RTE_RELATION && !rte->inFromCl) continue; /* ignore rule entries */ else return NULL; /* something else -> not simple VALUES */ } /* * We don't need to check the targetlist in any great detail, because * parser/analyze.c will never generate a "bare" VALUES RTE --- they only * appear inside auto-generated sub-queries with very restricted * structure. However, DefineView might have modified the tlist by * injecting new column aliases, or we might have some other column * aliases forced by a resultDesc. We can only simplify if the RTE's * column names match the names that get_target_list() would select. */ if (result) { ListCell *lcn; int colno; if (list_length(query->targetList) != list_length(result->eref->colnames)) return NULL; /* this probably cannot happen */ colno = 0; forboth(lc, query->targetList, lcn, result->eref->colnames) { TargetEntry *tle = (TargetEntry *) lfirst(lc); char *cname = strVal(lfirst(lcn)); char *colname; if (tle->resjunk) return NULL; /* this probably cannot happen */ /* compute name that get_target_list would use for column */ colno++; if (resultDesc && colno <= resultDesc->natts) colname = NameStr(TupleDescAttr(resultDesc, colno - 1)->attname); else colname = tle->resname; /* does it match the VALUES RTE? */ if (colname == NULL || strcmp(colname, cname) != 0) return NULL; /* column name has been changed */ } } return result; } static void get_basic_select_query(Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible) { StringInfo buf = context->buf; RangeTblEntry *values_rte; char *sep; ListCell *l; if (PRETTY_INDENT(context)) { context->indentLevel += PRETTYINDENT_STD; appendStringInfoChar(buf, ' '); } /* * If the query looks like SELECT * FROM (VALUES ...), then print just the * VALUES part. This reverses what transformValuesClause() did at parse * time. */ values_rte = get_simple_values_rte(query, resultDesc); if (values_rte) { get_values_def(values_rte->values_lists, context); return; } /* * Build up the query string - first we say SELECT */ appendStringInfoString(buf, "SELECT"); /* Add the DISTINCT clause if given */ if (query->distinctClause != NIL) { if (query->hasDistinctOn) { appendStringInfoString(buf, " DISTINCT ON ("); sep = ""; foreach(l, query->distinctClause) { SortGroupClause *srt = (SortGroupClause *) lfirst(l); appendStringInfoString(buf, sep); get_rule_sortgroupclause(srt->tleSortGroupRef, query->targetList, false, context); sep = ", "; } appendStringInfoChar(buf, ')'); } else appendStringInfoString(buf, " DISTINCT"); } /* Then we tell what to select (the targetlist) */ get_target_list(query->targetList, context, resultDesc, colNamesVisible); /* Add the FROM clause if needed */ get_from_clause(query, " FROM ", context); /* Add the WHERE clause if given */ if (query->jointree->quals != NULL) { appendContextKeyword(context, " WHERE ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_expr(query->jointree->quals, context, false); } /* Add the GROUP BY clause if given */ if (query->groupClause != NULL || query->groupingSets != NULL) { ParseExprKind save_exprkind; appendContextKeyword(context, " GROUP BY ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); save_exprkind = context->special_exprkind; context->special_exprkind = EXPR_KIND_GROUP_BY; if (query->groupingSets == NIL) { sep = ""; foreach(l, query->groupClause) { SortGroupClause *grp = (SortGroupClause *) lfirst(l); appendStringInfoString(buf, sep); get_rule_sortgroupclause(grp->tleSortGroupRef, query->targetList, false, context); sep = ", "; } } else { sep = ""; foreach(l, query->groupingSets) { GroupingSet *grp = lfirst(l); appendStringInfoString(buf, sep); get_rule_groupingset(grp, query->targetList, true, context); sep = ", "; } } context->special_exprkind = save_exprkind; } /* Add the HAVING clause if given */ if (query->havingQual != NULL) { appendContextKeyword(context, " HAVING ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); get_rule_expr(query->havingQual, context, false); } /* Add the WINDOW clause if needed */ if (query->windowClause != NIL) get_rule_windowclause(query, context); } /* ---------- * get_target_list - Parse back a SELECT target list * * This is also used for RETURNING lists in INSERT/UPDATE/DELETE. * * resultDesc and colNamesVisible are as for get_query_def() * ---------- */ static void get_target_list(List *targetList, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible) { StringInfo buf = context->buf; StringInfoData targetbuf; bool last_was_multiline = false; char *sep; int colno; ListCell *l; /* we use targetbuf to hold each TLE's text temporarily */ initStringInfo(&targetbuf); sep = " "; colno = 0; foreach(l, targetList) { TargetEntry *tle = (TargetEntry *) lfirst(l); char *colname; char *attname; if (tle->resjunk) continue; /* ignore junk entries */ appendStringInfoString(buf, sep); sep = ", "; colno++; /* * Put the new field text into targetbuf so we can decide after we've * got it whether or not it needs to go on a new line. */ resetStringInfo(&targetbuf); context->buf = &targetbuf; /* * We special-case Var nodes rather than using get_rule_expr. This is * needed because get_rule_expr will display a whole-row Var as * "foo.*", which is the preferred notation in most contexts, but at * the top level of a SELECT list it's not right (the parser will * expand that notation into multiple columns, yielding behavior * different from a whole-row Var). We need to call get_variable * directly so that we can tell it to do the right thing, and so that * we can get the attribute name which is the default AS label. */ if (tle->expr && (IsA(tle->expr, Var))) { attname = get_variable((Var *) tle->expr, 0, true, context); } else { get_rule_expr((Node *) tle->expr, context, true); /* * When colNamesVisible is true, we should always show the * assigned column name explicitly. Otherwise, show it only if * it's not FigureColname's fallback. */ attname = colNamesVisible ? NULL : "?column?"; } /* * Figure out what the result column should be called. In the context * of a view, use the view's tuple descriptor (so as to pick up the * effects of any column RENAME that's been done on the view). * Otherwise, just use what we can find in the TLE. */ if (resultDesc && colno <= resultDesc->natts) colname = NameStr(TupleDescAttr(resultDesc, colno - 1)->attname); else colname = tle->resname; /* Show AS unless the column's name is correct as-is */ if (colname) /* resname could be NULL */ { if (attname == NULL || strcmp(attname, colname) != 0) appendStringInfo(&targetbuf, " AS %s", quote_identifier(colname)); } /* Restore context's output buffer */ context->buf = buf; /* Consider line-wrapping if enabled */ if (PRETTY_INDENT(context) && context->wrapColumn >= 0) { int leading_nl_pos; /* Does the new field start with a new line? */ if (targetbuf.len > 0 && targetbuf.data[0] == '\n') leading_nl_pos = 0; else leading_nl_pos = -1; /* If so, we shouldn't add anything */ if (leading_nl_pos >= 0) { /* instead, remove any trailing spaces currently in buf */ removeStringInfoSpaces(buf); } else { char *trailing_nl; /* Locate the start of the current line in the output buffer */ trailing_nl = strrchr(buf->data, '\n'); if (trailing_nl == NULL) trailing_nl = buf->data; else trailing_nl++; /* * Add a newline, plus some indentation, if the new field is * not the first and either the new field would cause an * overflow or the last field used more than one line. */ if (colno > 1 && ((strlen(trailing_nl) + targetbuf.len > context->wrapColumn) || last_was_multiline)) appendContextKeyword(context, "", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_VAR); } /* Remember this field's multiline status for next iteration */ last_was_multiline = (strchr(targetbuf.data + leading_nl_pos + 1, '\n') != NULL); } /* Add the new field */ appendBinaryStringInfo(buf, targetbuf.data, targetbuf.len); } /* clean up */ pfree(targetbuf.data); } static void get_setop_query(Node *setOp, Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible) { StringInfo buf = context->buf; bool need_paren; /* Guard against excessively long or deeply-nested queries */ CHECK_FOR_INTERRUPTS(); check_stack_depth(); if (IsA(setOp, RangeTblRef)) { RangeTblRef *rtr = (RangeTblRef *) setOp; RangeTblEntry *rte = rt_fetch(rtr->rtindex, query->rtable); Query *subquery = rte->subquery; Assert(subquery != NULL); Assert(subquery->setOperations == NULL); /* Need parens if WITH, ORDER BY, FOR UPDATE, or LIMIT; see gram.y */ need_paren = (subquery->cteList || subquery->sortClause || subquery->rowMarks || subquery->limitOffset || subquery->limitCount); if (need_paren) appendStringInfoChar(buf, '('); get_query_def(subquery, buf, context->namespaces, resultDesc, colNamesVisible, context->prettyFlags, context->wrapColumn, context->indentLevel); if (need_paren) appendStringInfoChar(buf, ')'); } else if (IsA(setOp, SetOperationStmt)) { SetOperationStmt *op = (SetOperationStmt *) setOp; int subindent; /* * We force parens when nesting two SetOperationStmts, except when the * lefthand input is another setop of the same kind. Syntactically, * we could omit parens in rather more cases, but it seems best to use * parens to flag cases where the setop operator changes. If we use * parens, we also increase the indentation level for the child query. * * There are some cases in which parens are needed around a leaf query * too, but those are more easily handled at the next level down (see * code above). */ if (IsA(op->larg, SetOperationStmt)) { SetOperationStmt *lop = (SetOperationStmt *) op->larg; if (op->op == lop->op && op->all == lop->all) need_paren = false; else need_paren = true; } else need_paren = false; if (need_paren) { appendStringInfoChar(buf, '('); subindent = PRETTYINDENT_STD; appendContextKeyword(context, "", subindent, 0, 0); } else subindent = 0; get_setop_query(op->larg, query, context, resultDesc, colNamesVisible); if (need_paren) appendContextKeyword(context, ") ", -subindent, 0, 0); else if (PRETTY_INDENT(context)) appendContextKeyword(context, "", -subindent, 0, 0); else appendStringInfoChar(buf, ' '); switch (op->op) { case SETOP_UNION: appendStringInfoString(buf, "UNION "); break; case SETOP_INTERSECT: appendStringInfoString(buf, "INTERSECT "); break; case SETOP_EXCEPT: appendStringInfoString(buf, "EXCEPT "); break; default: elog(ERROR, "unrecognized set op: %d", (int) op->op); } if (op->all) appendStringInfoString(buf, "ALL "); /* Always parenthesize if RHS is another setop */ need_paren = IsA(op->rarg, SetOperationStmt); /* * The indentation code here is deliberately a bit different from that * for the lefthand input, because we want the line breaks in * different places. */ if (need_paren) { appendStringInfoChar(buf, '('); subindent = PRETTYINDENT_STD; } else subindent = 0; appendContextKeyword(context, "", subindent, 0, 0); get_setop_query(op->rarg, query, context, resultDesc, false); if (PRETTY_INDENT(context)) context->indentLevel -= subindent; if (need_paren) appendContextKeyword(context, ")", 0, 0, 0); } else { elog(ERROR, "unrecognized node type: %d", (int) nodeTag(setOp)); } } /* * Display a sort/group clause. * * Also returns the expression tree, so caller need not find it again. */ static Node * get_rule_sortgroupclause(Index ref, List *tlist, bool force_colno, deparse_context *context) { StringInfo buf = context->buf; TargetEntry *tle; Node *expr; tle = get_sortgroupref_tle(ref, tlist); expr = (Node *) tle->expr; /* * Use column-number form if requested by caller. Otherwise, if * expression is a constant, force it to be dumped with an explicit cast * as decoration --- this is because a simple integer constant is * ambiguous (and will be misinterpreted by findTargetlistEntry()) if we * dump it without any decoration. If it's anything more complex than a * simple Var, then force extra parens around it, to ensure it can't be * misinterpreted as a cube() or rollup() construct. */ if (force_colno) { Assert(!tle->resjunk); appendStringInfo(buf, "%d", tle->resno); } else if (expr && IsA(expr, Const)) get_const_expr((Const *) expr, context, 1); else if (!expr || IsA(expr, Var)) get_rule_expr(expr, context, true); else { /* * We must force parens for function-like expressions even if * PRETTY_PAREN is off, since those are the ones in danger of * misparsing. For other expressions we need to force them only if * PRETTY_PAREN is on, since otherwise the expression will output them * itself. (We can't skip the parens.) */ bool need_paren = (PRETTY_PAREN(context) || IsA(expr, FuncExpr) || IsA(expr, Aggref) || IsA(expr, WindowFunc)); if (need_paren) appendStringInfoChar(context->buf, '('); get_rule_expr(expr, context, true); if (need_paren) appendStringInfoChar(context->buf, ')'); } return expr; } /* * Display a GroupingSet */ static void get_rule_groupingset(GroupingSet *gset, List *targetlist, bool omit_parens, deparse_context *context) { ListCell *l; StringInfo buf = context->buf; bool omit_child_parens = true; char *sep = ""; switch (gset->kind) { case GROUPING_SET_EMPTY: appendStringInfoString(buf, "()"); return; case GROUPING_SET_SIMPLE: { if (!omit_parens || list_length(gset->content) != 1) appendStringInfoChar(buf, '('); foreach(l, gset->content) { Index ref = lfirst_int(l); appendStringInfoString(buf, sep); get_rule_sortgroupclause(ref, targetlist, false, context); sep = ", "; } if (!omit_parens || list_length(gset->content) != 1) appendStringInfoChar(buf, ')'); } return; case GROUPING_SET_ROLLUP: appendStringInfoString(buf, "ROLLUP("); break; case GROUPING_SET_CUBE: appendStringInfoString(buf, "CUBE("); break; case GROUPING_SET_SETS: appendStringInfoString(buf, "GROUPING SETS ("); omit_child_parens = false; break; } foreach(l, gset->content) { appendStringInfoString(buf, sep); get_rule_groupingset(lfirst(l), targetlist, omit_child_parens, context); sep = ", "; } appendStringInfoChar(buf, ')'); } /* * Display an ORDER BY list. */ static void get_rule_orderby(List *orderList, List *targetList, bool force_colno, deparse_context *context) { StringInfo buf = context->buf; const char *sep; ListCell *l; sep = ""; foreach(l, orderList) { SortGroupClause *srt = (SortGroupClause *) lfirst(l); Node *sortexpr; Oid sortcoltype; TypeCacheEntry *typentry; appendStringInfoString(buf, sep); sortexpr = get_rule_sortgroupclause(srt->tleSortGroupRef, targetList, force_colno, context); sortcoltype = exprType(sortexpr); /* See whether operator is default < or > for datatype */ typentry = lookup_type_cache(sortcoltype, TYPECACHE_LT_OPR | TYPECACHE_GT_OPR); if (srt->sortop == typentry->lt_opr) { /* ASC is default, so emit nothing for it */ if (srt->nulls_first) appendStringInfoString(buf, " NULLS FIRST"); } else if (srt->sortop == typentry->gt_opr) { appendStringInfoString(buf, " DESC"); /* DESC defaults to NULLS FIRST */ if (!srt->nulls_first) appendStringInfoString(buf, " NULLS LAST"); } else { appendStringInfo(buf, " USING %s", generate_operator_name(srt->sortop, sortcoltype, sortcoltype)); /* be specific to eliminate ambiguity */ if (srt->nulls_first) appendStringInfoString(buf, " NULLS FIRST"); else appendStringInfoString(buf, " NULLS LAST"); } sep = ", "; } } /* * Display a WINDOW clause. * * Note that the windowClause list might contain only anonymous window * specifications, in which case we should print nothing here. */ static void get_rule_windowclause(Query *query, deparse_context *context) { StringInfo buf = context->buf; const char *sep; ListCell *l; sep = NULL; foreach(l, query->windowClause) { WindowClause *wc = (WindowClause *) lfirst(l); if (wc->name == NULL) continue; /* ignore anonymous windows */ if (sep == NULL) appendContextKeyword(context, " WINDOW ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); else appendStringInfoString(buf, sep); appendStringInfo(buf, "%s AS ", quote_identifier(wc->name)); get_rule_windowspec(wc, query->targetList, context); sep = ", "; } } /* * Display a window definition */ static void get_rule_windowspec(WindowClause *wc, List *targetList, deparse_context *context) { StringInfo buf = context->buf; bool needspace = false; const char *sep; ListCell *l; appendStringInfoChar(buf, '('); if (wc->refname) { appendStringInfoString(buf, quote_identifier(wc->refname)); needspace = true; } /* partition clauses are always inherited, so only print if no refname */ if (wc->partitionClause && !wc->refname) { if (needspace) appendStringInfoChar(buf, ' '); appendStringInfoString(buf, "PARTITION BY "); sep = ""; foreach(l, wc->partitionClause) { SortGroupClause *grp = (SortGroupClause *) lfirst(l); appendStringInfoString(buf, sep); get_rule_sortgroupclause(grp->tleSortGroupRef, targetList, false, context); sep = ", "; } needspace = true; } /* print ordering clause only if not inherited */ if (wc->orderClause && !wc->copiedOrder) { if (needspace) appendStringInfoChar(buf, ' '); appendStringInfoString(buf, "ORDER BY "); get_rule_orderby(wc->orderClause, targetList, false, context); needspace = true; } /* framing clause is never inherited, so print unless it's default */ if (wc->frameOptions & FRAMEOPTION_NONDEFAULT) { if (needspace) appendStringInfoChar(buf, ' '); if (wc->frameOptions & FRAMEOPTION_RANGE) appendStringInfoString(buf, "RANGE "); else if (wc->frameOptions & FRAMEOPTION_ROWS) appendStringInfoString(buf, "ROWS "); else if (wc->frameOptions & FRAMEOPTION_GROUPS) appendStringInfoString(buf, "GROUPS "); else Assert(false); if (wc->frameOptions & FRAMEOPTION_BETWEEN) appendStringInfoString(buf, "BETWEEN "); if (wc->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING) appendStringInfoString(buf, "UNBOUNDED PRECEDING "); else if (wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) appendStringInfoString(buf, "CURRENT ROW "); else if (wc->frameOptions & FRAMEOPTION_START_OFFSET) { get_rule_expr(wc->startOffset, context, false); if (wc->frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING) appendStringInfoString(buf, " PRECEDING "); else if (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING) appendStringInfoString(buf, " FOLLOWING "); else Assert(false); } else Assert(false); if (wc->frameOptions & FRAMEOPTION_BETWEEN) { appendStringInfoString(buf, "AND "); if (wc->frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING) appendStringInfoString(buf, "UNBOUNDED FOLLOWING "); else if (wc->frameOptions & FRAMEOPTION_END_CURRENT_ROW) appendStringInfoString(buf, "CURRENT ROW "); else if (wc->frameOptions & FRAMEOPTION_END_OFFSET) { get_rule_expr(wc->endOffset, context, false); if (wc->frameOptions & FRAMEOPTION_END_OFFSET_PRECEDING) appendStringInfoString(buf, " PRECEDING "); else if (wc->frameOptions & FRAMEOPTION_END_OFFSET_FOLLOWING) appendStringInfoString(buf, " FOLLOWING "); else Assert(false); } else Assert(false); } if (wc->frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW) appendStringInfoString(buf, "EXCLUDE CURRENT ROW "); else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_GROUP) appendStringInfoString(buf, "EXCLUDE GROUP "); else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES) appendStringInfoString(buf, "EXCLUDE TIES "); /* we will now have a trailing space; remove it */ buf->len--; } appendStringInfoChar(buf, ')'); } /* ---------- * get_insert_query_def - Parse back an INSERT parsetree * ---------- */ static void get_insert_query_def(Query *query, deparse_context *context, bool colNamesVisible) { StringInfo buf = context->buf; RangeTblEntry *select_rte = NULL; RangeTblEntry *values_rte = NULL; RangeTblEntry *rte; char *sep; ListCell *l; List *strippedexprs; /* Insert the WITH clause if given */ get_with_clause(query, context); /* * If it's an INSERT ... SELECT or multi-row VALUES, there will be a * single RTE for the SELECT or VALUES. Plain VALUES has neither. */ foreach(l, query->rtable) { rte = (RangeTblEntry *) lfirst(l); if (rte->rtekind == RTE_SUBQUERY) { if (select_rte) elog(ERROR, "too many subquery RTEs in INSERT"); select_rte = rte; } if (rte->rtekind == RTE_VALUES) { if (values_rte) elog(ERROR, "too many values RTEs in INSERT"); values_rte = rte; } } if (select_rte && values_rte) elog(ERROR, "both subquery and values RTEs in INSERT"); /* * Start the query with INSERT INTO relname */ rte = rt_fetch(query->resultRelation, query->rtable); Assert(rte->rtekind == RTE_RELATION); if (PRETTY_INDENT(context)) { context->indentLevel += PRETTYINDENT_STD; appendStringInfoChar(buf, ' '); } appendStringInfo(buf, "INSERT INTO %s ", generate_relation_name(rte->relid, NIL)); /* INSERT requires AS keyword for target alias */ if (rte->alias != NULL) appendStringInfo(buf, "AS %s ", quote_identifier(rte->alias->aliasname)); /* * Add the insert-column-names list. Any indirection decoration needed on * the column names can be inferred from the top targetlist. */ strippedexprs = NIL; sep = ""; if (query->targetList) appendStringInfoChar(buf, '('); foreach(l, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(l); if (tle->resjunk) continue; /* ignore junk entries */ appendStringInfoString(buf, sep); sep = ", "; /* * Put out name of target column; look in the catalogs, not at * tle->resname, since resname will fail to track RENAME. */ appendStringInfoString(buf, quote_identifier(get_attname(rte->relid, tle->resno, false))); /* * Print any indirection needed (subfields or subscripts), and strip * off the top-level nodes representing the indirection assignments. * Add the stripped expressions to strippedexprs. (If it's a * single-VALUES statement, the stripped expressions are the VALUES to * print below. Otherwise they're just Vars and not really * interesting.) */ strippedexprs = lappend(strippedexprs, processIndirection((Node *) tle->expr, context)); } if (query->targetList) appendStringInfoString(buf, ") "); if (query->override) { if (query->override == OVERRIDING_SYSTEM_VALUE) appendStringInfoString(buf, "OVERRIDING SYSTEM VALUE "); else if (query->override == OVERRIDING_USER_VALUE) appendStringInfoString(buf, "OVERRIDING USER VALUE "); } if (select_rte) { /* Add the SELECT */ get_query_def(select_rte->subquery, buf, NIL, NULL, false, context->prettyFlags, context->wrapColumn, context->indentLevel); } else if (values_rte) { /* Add the multi-VALUES expression lists */ get_values_def(values_rte->values_lists, context); } else if (strippedexprs) { /* Add the single-VALUES expression list */ appendContextKeyword(context, "VALUES (", -PRETTYINDENT_STD, PRETTYINDENT_STD, 2); get_rule_list_toplevel(strippedexprs, context, false); appendStringInfoChar(buf, ')'); } else { /* No expressions, so it must be DEFAULT VALUES */ appendStringInfoString(buf, "DEFAULT VALUES"); } /* Add ON CONFLICT if present */ if (query->onConflict) { OnConflictExpr *confl = query->onConflict; appendStringInfoString(buf, " ON CONFLICT"); if (confl->arbiterElems) { /* Add the single-VALUES expression list */ appendStringInfoChar(buf, '('); get_rule_expr((Node *) confl->arbiterElems, context, false); appendStringInfoChar(buf, ')'); /* Add a WHERE clause (for partial indexes) if given */ if (confl->arbiterWhere != NULL) { bool save_varprefix; /* * Force non-prefixing of Vars, since parser assumes that they * belong to target relation. WHERE clause does not use * InferenceElem, so this is separately required. */ save_varprefix = context->varprefix; context->varprefix = false; appendContextKeyword(context, " WHERE ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_expr(confl->arbiterWhere, context, false); context->varprefix = save_varprefix; } } else if (OidIsValid(confl->constraint)) { char *constraint = get_constraint_name(confl->constraint); if (!constraint) elog(ERROR, "cache lookup failed for constraint %u", confl->constraint); appendStringInfo(buf, " ON CONSTRAINT %s", quote_identifier(constraint)); } if (confl->action == ONCONFLICT_NOTHING) { appendStringInfoString(buf, " DO NOTHING"); } else { appendStringInfoString(buf, " DO UPDATE SET "); /* Deparse targetlist */ get_update_query_targetlist_def(query, confl->onConflictSet, context, rte); /* Add a WHERE clause if given */ if (confl->onConflictWhere != NULL) { appendContextKeyword(context, " WHERE ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_expr(confl->onConflictWhere, context, false); } } } /* Add RETURNING if present */ if (query->returningList) { appendContextKeyword(context, " RETURNING", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_target_list(query->returningList, context, NULL, colNamesVisible); } } /* ---------- * get_update_query_def - Parse back an UPDATE parsetree * ---------- */ static void get_update_query_def(Query *query, deparse_context *context, bool colNamesVisible) { StringInfo buf = context->buf; RangeTblEntry *rte; /* Insert the WITH clause if given */ get_with_clause(query, context); /* * Start the query with UPDATE relname SET */ rte = rt_fetch(query->resultRelation, query->rtable); Assert(rte->rtekind == RTE_RELATION); if (PRETTY_INDENT(context)) { appendStringInfoChar(buf, ' '); context->indentLevel += PRETTYINDENT_STD; } appendStringInfo(buf, "UPDATE %s%s", only_marker(rte), generate_relation_name(rte->relid, NIL)); if (rte->alias != NULL) appendStringInfo(buf, " %s", quote_identifier(rte->alias->aliasname)); appendStringInfoString(buf, " SET "); /* Deparse targetlist */ get_update_query_targetlist_def(query, query->targetList, context, rte); /* Add the FROM clause if needed */ get_from_clause(query, " FROM ", context); /* Add a WHERE clause if given */ if (query->jointree->quals != NULL) { appendContextKeyword(context, " WHERE ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_expr(query->jointree->quals, context, false); } /* Add RETURNING if present */ if (query->returningList) { appendContextKeyword(context, " RETURNING", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_target_list(query->returningList, context, NULL, colNamesVisible); } } /* ---------- * get_update_query_targetlist_def - Parse back an UPDATE targetlist * ---------- */ static void get_update_query_targetlist_def(Query *query, List *targetList, deparse_context *context, RangeTblEntry *rte) { StringInfo buf = context->buf; ListCell *l; ListCell *next_ma_cell; int remaining_ma_columns; const char *sep; SubLink *cur_ma_sublink; List *ma_sublinks; /* * Prepare to deal with MULTIEXPR assignments: collect the source SubLinks * into a list. We expect them to appear, in ID order, in resjunk tlist * entries. */ ma_sublinks = NIL; if (query->hasSubLinks) /* else there can't be any */ { foreach(l, targetList) { TargetEntry *tle = (TargetEntry *) lfirst(l); if (tle->resjunk && IsA(tle->expr, SubLink)) { SubLink *sl = (SubLink *) tle->expr; if (sl->subLinkType == MULTIEXPR_SUBLINK) { ma_sublinks = lappend(ma_sublinks, sl); Assert(sl->subLinkId == list_length(ma_sublinks)); } } } } next_ma_cell = list_head(ma_sublinks); cur_ma_sublink = NULL; remaining_ma_columns = 0; /* Add the comma separated list of 'attname = value' */ sep = ""; foreach(l, targetList) { TargetEntry *tle = (TargetEntry *) lfirst(l); Node *expr; if (tle->resjunk) continue; /* ignore junk entries */ /* Emit separator (OK whether we're in multiassignment or not) */ appendStringInfoString(buf, sep); sep = ", "; /* * Check to see if we're starting a multiassignment group: if so, * output a left paren. */ if (next_ma_cell != NULL && cur_ma_sublink == NULL) { /* * We must dig down into the expr to see if it's a PARAM_MULTIEXPR * Param. That could be buried under FieldStores and * SubscriptingRefs and CoerceToDomains (cf processIndirection()), * and underneath those there could be an implicit type coercion. * Because we would ignore implicit type coercions anyway, we * don't need to be as careful as processIndirection() is about * descending past implicit CoerceToDomains. */ expr = (Node *) tle->expr; while (expr) { if (IsA(expr, FieldStore)) { FieldStore *fstore = (FieldStore *) expr; expr = (Node *) linitial(fstore->newvals); } else if (IsA(expr, SubscriptingRef)) { SubscriptingRef *sbsref = (SubscriptingRef *) expr; if (sbsref->refassgnexpr == NULL) break; expr = (Node *) sbsref->refassgnexpr; } else if (IsA(expr, CoerceToDomain)) { CoerceToDomain *cdomain = (CoerceToDomain *) expr; if (cdomain->coercionformat != COERCE_IMPLICIT_CAST) break; expr = (Node *) cdomain->arg; } else break; } expr = strip_implicit_coercions(expr); if (expr && IsA(expr, Param) && ((Param *) expr)->paramkind == PARAM_MULTIEXPR) { cur_ma_sublink = (SubLink *) lfirst(next_ma_cell); next_ma_cell = lnext(ma_sublinks, next_ma_cell); remaining_ma_columns = count_nonjunk_tlist_entries(((Query *) cur_ma_sublink->subselect)->targetList); Assert(((Param *) expr)->paramid == ((cur_ma_sublink->subLinkId << 16) | 1)); appendStringInfoChar(buf, '('); } } /* * Put out name of target column; look in the catalogs, not at * tle->resname, since resname will fail to track RENAME. */ appendStringInfoString(buf, quote_identifier(get_attname(rte->relid, tle->resno, false))); /* * Print any indirection needed (subfields or subscripts), and strip * off the top-level nodes representing the indirection assignments. */ expr = processIndirection((Node *) tle->expr, context); /* * If we're in a multiassignment, skip printing anything more, unless * this is the last column; in which case, what we print should be the * sublink, not the Param. */ if (cur_ma_sublink != NULL) { if (--remaining_ma_columns > 0) continue; /* not the last column of multiassignment */ appendStringInfoChar(buf, ')'); expr = (Node *) cur_ma_sublink; cur_ma_sublink = NULL; } appendStringInfoString(buf, " = "); get_rule_expr(expr, context, false); } } /* ---------- * get_delete_query_def - Parse back a DELETE parsetree * ---------- */ static void get_delete_query_def(Query *query, deparse_context *context, bool colNamesVisible) { StringInfo buf = context->buf; RangeTblEntry *rte; /* Insert the WITH clause if given */ get_with_clause(query, context); /* * Start the query with DELETE FROM relname */ rte = rt_fetch(query->resultRelation, query->rtable); Assert(rte->rtekind == RTE_RELATION); if (PRETTY_INDENT(context)) { appendStringInfoChar(buf, ' '); context->indentLevel += PRETTYINDENT_STD; } appendStringInfo(buf, "DELETE FROM %s%s", only_marker(rte), generate_relation_name(rte->relid, NIL)); if (rte->alias != NULL) appendStringInfo(buf, " %s", quote_identifier(rte->alias->aliasname)); /* Add the USING clause if given */ get_from_clause(query, " USING ", context); /* Add a WHERE clause if given */ if (query->jointree->quals != NULL) { appendContextKeyword(context, " WHERE ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_expr(query->jointree->quals, context, false); } /* Add RETURNING if present */ if (query->returningList) { appendContextKeyword(context, " RETURNING", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_target_list(query->returningList, context, NULL, colNamesVisible); } } /* ---------- * get_utility_query_def - Parse back a UTILITY parsetree * ---------- */ static void get_utility_query_def(Query *query, deparse_context *context) { StringInfo buf = context->buf; if (query->utilityStmt && IsA(query->utilityStmt, NotifyStmt)) { NotifyStmt *stmt = (NotifyStmt *) query->utilityStmt; appendContextKeyword(context, "", 0, PRETTYINDENT_STD, 1); appendStringInfo(buf, "NOTIFY %s", quote_identifier(stmt->conditionname)); if (stmt->payload) { appendStringInfoString(buf, ", "); simple_quote_literal(buf, stmt->payload); } } else { /* Currently only NOTIFY utility commands can appear in rules */ elog(ERROR, "unexpected utility statement type"); } } /* * Display a Var appropriately. * * In some cases (currently only when recursing into an unnamed join) * the Var's varlevelsup has to be interpreted with respect to a context * above the current one; levelsup indicates the offset. * * If istoplevel is true, the Var is at the top level of a SELECT's * targetlist, which means we need special treatment of whole-row Vars. * Instead of the normal "tab.*", we'll print "tab.*::typename", which is a * dirty hack to prevent "tab.*" from being expanded into multiple columns. * (The parser will strip the useless coercion, so no inefficiency is added in * dump and reload.) We used to print just "tab" in such cases, but that is * ambiguous and will yield the wrong result if "tab" is also a plain column * name in the query. * * Returns the attname of the Var, or NULL if the Var has no attname (because * it is a whole-row Var or a subplan output reference). */ static char * get_variable(Var *var, int levelsup, bool istoplevel, deparse_context *context) { StringInfo buf = context->buf; RangeTblEntry *rte; AttrNumber attnum; int netlevelsup; deparse_namespace *dpns; Index varno; AttrNumber varattno; deparse_columns *colinfo; char *refname; char *attname; /* Find appropriate nesting depth */ netlevelsup = var->varlevelsup + levelsup; if (netlevelsup >= list_length(context->namespaces)) elog(ERROR, "bogus varlevelsup: %d offset %d", var->varlevelsup, levelsup); dpns = (deparse_namespace *) list_nth(context->namespaces, netlevelsup); /* * If we have a syntactic referent for the Var, and we're working from a * parse tree, prefer to use the syntactic referent. Otherwise, fall back * on the semantic referent. (Forcing use of the semantic referent when * printing plan trees is a design choice that's perhaps more motivated by * backwards compatibility than anything else. But it does have the * advantage of making plans more explicit.) */ if (var->varnosyn > 0 && dpns->plan == NULL) { varno = var->varnosyn; varattno = var->varattnosyn; } else { varno = var->varno; varattno = var->varattno; } /* * Try to find the relevant RTE in this rtable. In a plan tree, it's * likely that varno is OUTER_VAR or INNER_VAR, in which case we must dig * down into the subplans, or INDEX_VAR, which is resolved similarly. Also * find the aliases previously assigned for this RTE. */ if (varno >= 1 && varno <= list_length(dpns->rtable)) { /* * We might have been asked to map child Vars to some parent relation. */ if (context->appendparents && dpns->appendrels) { Index pvarno = varno; AttrNumber pvarattno = varattno; AppendRelInfo *appinfo = dpns->appendrels[pvarno]; bool found = false; /* Only map up to inheritance parents, not UNION ALL appendrels */ while (appinfo && rt_fetch(appinfo->parent_relid, dpns->rtable)->rtekind == RTE_RELATION) { found = false; if (pvarattno > 0) /* system columns stay as-is */ { if (pvarattno > appinfo->num_child_cols) break; /* safety check */ pvarattno = appinfo->parent_colnos[pvarattno - 1]; if (pvarattno == 0) break; /* Var is local to child */ } pvarno = appinfo->parent_relid; found = true; /* If the parent is itself a child, continue up. */ Assert(pvarno > 0 && pvarno <= list_length(dpns->rtable)); appinfo = dpns->appendrels[pvarno]; } /* * If we found an ancestral rel, and that rel is included in * appendparents, print that column not the original one. */ if (found && bms_is_member(pvarno, context->appendparents)) { varno = pvarno; varattno = pvarattno; } } rte = rt_fetch(varno, dpns->rtable); refname = (char *) list_nth(dpns->rtable_names, varno - 1); colinfo = deparse_columns_fetch(varno, dpns); attnum = varattno; } else { resolve_special_varno((Node *) var, context, get_special_variable, NULL); return NULL; } /* * The planner will sometimes emit Vars referencing resjunk elements of a * subquery's target list (this is currently only possible if it chooses * to generate a "physical tlist" for a SubqueryScan or CteScan node). * Although we prefer to print subquery-referencing Vars using the * subquery's alias, that's not possible for resjunk items since they have * no alias. So in that case, drill down to the subplan and print the * contents of the referenced tlist item. This works because in a plan * tree, such Vars can only occur in a SubqueryScan or CteScan node, and * we'll have set dpns->inner_plan to reference the child plan node. */ if ((rte->rtekind == RTE_SUBQUERY || rte->rtekind == RTE_CTE) && attnum > list_length(rte->eref->colnames) && dpns->inner_plan) { TargetEntry *tle; deparse_namespace save_dpns; tle = get_tle_by_resno(dpns->inner_tlist, attnum); if (!tle) elog(ERROR, "invalid attnum %d for relation \"%s\"", attnum, rte->eref->aliasname); Assert(netlevelsup == 0); push_child_plan(dpns, dpns->inner_plan, &save_dpns); /* * Force parentheses because our caller probably assumed a Var is a * simple expression. */ if (!IsA(tle->expr, Var)) appendStringInfoChar(buf, '('); get_rule_expr((Node *) tle->expr, context, true); if (!IsA(tle->expr, Var)) appendStringInfoChar(buf, ')'); pop_child_plan(dpns, &save_dpns); return NULL; } /* * If it's an unnamed join, look at the expansion of the alias variable. * If it's a simple reference to one of the input vars, then recursively * print the name of that var instead. When it's not a simple reference, * we have to just print the unqualified join column name. (This can only * happen with "dangerous" merged columns in a JOIN USING; we took pains * previously to make the unqualified column name unique in such cases.) * * This wouldn't work in decompiling plan trees, because we don't store * joinaliasvars lists after planning; but a plan tree should never * contain a join alias variable. */ if (rte->rtekind == RTE_JOIN && rte->alias == NULL) { if (rte->joinaliasvars == NIL) elog(ERROR, "cannot decompile join alias var in plan tree"); if (attnum > 0) { Var *aliasvar; aliasvar = (Var *) list_nth(rte->joinaliasvars, attnum - 1); /* we intentionally don't strip implicit coercions here */ if (aliasvar && IsA(aliasvar, Var)) { return get_variable(aliasvar, var->varlevelsup + levelsup, istoplevel, context); } } /* * Unnamed join has no refname. (Note: since it's unnamed, there is * no way the user could have referenced it to create a whole-row Var * for it. So we don't have to cover that case below.) */ Assert(refname == NULL); } if (attnum == InvalidAttrNumber) attname = NULL; else if (attnum > 0) { /* Get column name to use from the colinfo struct */ if (attnum > colinfo->num_cols) elog(ERROR, "invalid attnum %d for relation \"%s\"", attnum, rte->eref->aliasname); attname = colinfo->colnames[attnum - 1]; /* * If we find a Var referencing a dropped column, it seems better to * print something (anything) than to fail. In general this should * not happen, but there are specific cases involving functions * returning named composite types where we don't sufficiently enforce * that you can't drop a column that's referenced in some view. */ if (attname == NULL) attname = "?dropped?column?"; } else { /* System column - name is fixed, get it from the catalog */ attname = get_rte_attribute_name(rte, attnum); } if (refname && (context->varprefix || attname == NULL)) { appendStringInfoString(buf, quote_identifier(refname)); appendStringInfoChar(buf, '.'); } if (attname) appendStringInfoString(buf, quote_identifier(attname)); else { appendStringInfoChar(buf, '*'); if (istoplevel) appendStringInfo(buf, "::%s", format_type_with_typemod(var->vartype, var->vartypmod)); } return attname; } /* * Deparse a Var which references OUTER_VAR, INNER_VAR, or INDEX_VAR. This * routine is actually a callback for resolve_special_varno, which handles * finding the correct TargetEntry. We get the expression contained in that * TargetEntry and just need to deparse it, a job we can throw back on * get_rule_expr. */ static void get_special_variable(Node *node, deparse_context *context, void *callback_arg) { StringInfo buf = context->buf; /* * For a non-Var referent, force parentheses because our caller probably * assumed a Var is a simple expression. */ if (!IsA(node, Var)) appendStringInfoChar(buf, '('); get_rule_expr(node, context, true); if (!IsA(node, Var)) appendStringInfoChar(buf, ')'); } /* * Chase through plan references to special varnos (OUTER_VAR, INNER_VAR, * INDEX_VAR) until we find a real Var or some kind of non-Var node; then, * invoke the callback provided. */ static void resolve_special_varno(Node *node, deparse_context *context, rsv_callback callback, void *callback_arg) { Var *var; deparse_namespace *dpns; /* This function is recursive, so let's be paranoid. */ check_stack_depth(); /* If it's not a Var, invoke the callback. */ if (!IsA(node, Var)) { (*callback) (node, context, callback_arg); return; } /* Find appropriate nesting depth */ var = (Var *) node; dpns = (deparse_namespace *) list_nth(context->namespaces, var->varlevelsup); /* * If varno is special, recurse. (Don't worry about varnosyn; if we're * here, we already decided not to use that.) */ if (var->varno == OUTER_VAR && dpns->outer_tlist) { TargetEntry *tle; deparse_namespace save_dpns; Bitmapset *save_appendparents; tle = get_tle_by_resno(dpns->outer_tlist, var->varattno); if (!tle) elog(ERROR, "bogus varattno for OUTER_VAR var: %d", var->varattno); /* * If we're descending to the first child of an Append or MergeAppend, * update appendparents. This will affect deparsing of all Vars * appearing within the eventually-resolved subexpression. */ save_appendparents = context->appendparents; if (IsA(dpns->plan, Append)) context->appendparents = bms_union(context->appendparents, ((Append *) dpns->plan)->apprelids); else if (IsA(dpns->plan, MergeAppend)) context->appendparents = bms_union(context->appendparents, ((MergeAppend *) dpns->plan)->apprelids); push_child_plan(dpns, dpns->outer_plan, &save_dpns); resolve_special_varno((Node *) tle->expr, context, callback, callback_arg); pop_child_plan(dpns, &save_dpns); context->appendparents = save_appendparents; return; } else if (var->varno == INNER_VAR && dpns->inner_tlist) { TargetEntry *tle; deparse_namespace save_dpns; tle = get_tle_by_resno(dpns->inner_tlist, var->varattno); if (!tle) elog(ERROR, "bogus varattno for INNER_VAR var: %d", var->varattno); push_child_plan(dpns, dpns->inner_plan, &save_dpns); resolve_special_varno((Node *) tle->expr, context, callback, callback_arg); pop_child_plan(dpns, &save_dpns); return; } else if (var->varno == INDEX_VAR && dpns->index_tlist) { TargetEntry *tle; tle = get_tle_by_resno(dpns->index_tlist, var->varattno); if (!tle) elog(ERROR, "bogus varattno for INDEX_VAR var: %d", var->varattno); resolve_special_varno((Node *) tle->expr, context, callback, callback_arg); return; } else if (var->varno < 1 || var->varno > list_length(dpns->rtable)) elog(ERROR, "bogus varno: %d", var->varno); /* Not special. Just invoke the callback. */ (*callback) (node, context, callback_arg); } /* * Get the name of a field of an expression of composite type. The * expression is usually a Var, but we handle other cases too. * * levelsup is an extra offset to interpret the Var's varlevelsup correctly. * * This is fairly straightforward when the expression has a named composite * type; we need only look up the type in the catalogs. However, the type * could also be RECORD. Since no actual table or view column is allowed to * have type RECORD, a Var of type RECORD must refer to a JOIN or FUNCTION RTE * or to a subquery output. We drill down to find the ultimate defining * expression and attempt to infer the field name from it. We ereport if we * can't determine the name. * * Similarly, a PARAM of type RECORD has to refer to some expression of * a determinable composite type. */ static const char * get_name_for_var_field(Var *var, int fieldno, int levelsup, deparse_context *context) { RangeTblEntry *rte; AttrNumber attnum; int netlevelsup; deparse_namespace *dpns; Index varno; AttrNumber varattno; TupleDesc tupleDesc; Node *expr; /* * If it's a RowExpr that was expanded from a whole-row Var, use the * column names attached to it. */ if (IsA(var, RowExpr)) { RowExpr *r = (RowExpr *) var; if (fieldno > 0 && fieldno <= list_length(r->colnames)) return strVal(list_nth(r->colnames, fieldno - 1)); } /* * If it's a Param of type RECORD, try to find what the Param refers to. */ if (IsA(var, Param)) { Param *param = (Param *) var; ListCell *ancestor_cell; expr = find_param_referent(param, context, &dpns, &ancestor_cell); if (expr) { /* Found a match, so recurse to decipher the field name */ deparse_namespace save_dpns; const char *result; push_ancestor_plan(dpns, ancestor_cell, &save_dpns); result = get_name_for_var_field((Var *) expr, fieldno, 0, context); pop_ancestor_plan(dpns, &save_dpns); return result; } } /* * If it's a Var of type RECORD, we have to find what the Var refers to; * if not, we can use get_expr_result_tupdesc(). */ if (!IsA(var, Var) || var->vartype != RECORDOID) { tupleDesc = get_expr_result_tupdesc((Node *) var, false); /* Got the tupdesc, so we can extract the field name */ Assert(fieldno >= 1 && fieldno <= tupleDesc->natts); return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname); } /* Find appropriate nesting depth */ netlevelsup = var->varlevelsup + levelsup; if (netlevelsup >= list_length(context->namespaces)) elog(ERROR, "bogus varlevelsup: %d offset %d", var->varlevelsup, levelsup); dpns = (deparse_namespace *) list_nth(context->namespaces, netlevelsup); /* * If we have a syntactic referent for the Var, and we're working from a * parse tree, prefer to use the syntactic referent. Otherwise, fall back * on the semantic referent. (See comments in get_variable().) */ if (var->varnosyn > 0 && dpns->plan == NULL) { varno = var->varnosyn; varattno = var->varattnosyn; } else { varno = var->varno; varattno = var->varattno; } /* * Try to find the relevant RTE in this rtable. In a plan tree, it's * likely that varno is OUTER_VAR or INNER_VAR, in which case we must dig * down into the subplans, or INDEX_VAR, which is resolved similarly. * * Note: unlike get_variable and resolve_special_varno, we need not worry * about inheritance mapping: a child Var should have the same datatype as * its parent, and here we're really only interested in the Var's type. */ if (varno >= 1 && varno <= list_length(dpns->rtable)) { rte = rt_fetch(varno, dpns->rtable); attnum = varattno; } else if (varno == OUTER_VAR && dpns->outer_tlist) { TargetEntry *tle; deparse_namespace save_dpns; const char *result; tle = get_tle_by_resno(dpns->outer_tlist, varattno); if (!tle) elog(ERROR, "bogus varattno for OUTER_VAR var: %d", varattno); Assert(netlevelsup == 0); push_child_plan(dpns, dpns->outer_plan, &save_dpns); result = get_name_for_var_field((Var *) tle->expr, fieldno, levelsup, context); pop_child_plan(dpns, &save_dpns); return result; } else if (varno == INNER_VAR && dpns->inner_tlist) { TargetEntry *tle; deparse_namespace save_dpns; const char *result; tle = get_tle_by_resno(dpns->inner_tlist, varattno); if (!tle) elog(ERROR, "bogus varattno for INNER_VAR var: %d", varattno); Assert(netlevelsup == 0); push_child_plan(dpns, dpns->inner_plan, &save_dpns); result = get_name_for_var_field((Var *) tle->expr, fieldno, levelsup, context); pop_child_plan(dpns, &save_dpns); return result; } else if (varno == INDEX_VAR && dpns->index_tlist) { TargetEntry *tle; const char *result; tle = get_tle_by_resno(dpns->index_tlist, varattno); if (!tle) elog(ERROR, "bogus varattno for INDEX_VAR var: %d", varattno); Assert(netlevelsup == 0); result = get_name_for_var_field((Var *) tle->expr, fieldno, levelsup, context); return result; } else { elog(ERROR, "bogus varno: %d", varno); return NULL; /* keep compiler quiet */ } if (attnum == InvalidAttrNumber) { /* Var is whole-row reference to RTE, so select the right field */ return get_rte_attribute_name(rte, fieldno); } /* * This part has essentially the same logic as the parser's * expandRecordVariable() function, but we are dealing with a different * representation of the input context, and we only need one field name * not a TupleDesc. Also, we need special cases for finding subquery and * CTE subplans when deparsing Plan trees. */ expr = (Node *) var; /* default if we can't drill down */ switch (rte->rtekind) { case RTE_RELATION: case RTE_VALUES: case RTE_NAMEDTUPLESTORE: case RTE_RESULT: /* * This case should not occur: a column of a table, values list, * or ENR shouldn't have type RECORD. Fall through and fail (most * likely) at the bottom. */ break; case RTE_SUBQUERY: /* Subselect-in-FROM: examine sub-select's output expr */ { if (rte->subquery) { TargetEntry *ste = get_tle_by_resno(rte->subquery->targetList, attnum); if (ste == NULL || ste->resjunk) elog(ERROR, "subquery %s does not have attribute %d", rte->eref->aliasname, attnum); expr = (Node *) ste->expr; if (IsA(expr, Var)) { /* * Recurse into the sub-select to see what its Var * refers to. We have to build an additional level of * namespace to keep in step with varlevelsup in the * subselect. */ deparse_namespace mydpns; const char *result; set_deparse_for_query(&mydpns, rte->subquery, context->namespaces); context->namespaces = lcons(&mydpns, context->namespaces); result = get_name_for_var_field((Var *) expr, fieldno, 0, context); context->namespaces = list_delete_first(context->namespaces); return result; } /* else fall through to inspect the expression */ } else { /* * We're deparsing a Plan tree so we don't have complete * RTE entries (in particular, rte->subquery is NULL). But * the only place we'd see a Var directly referencing a * SUBQUERY RTE is in a SubqueryScan plan node, and we can * look into the child plan's tlist instead. */ TargetEntry *tle; deparse_namespace save_dpns; const char *result; if (!dpns->inner_plan) elog(ERROR, "failed to find plan for subquery %s", rte->eref->aliasname); tle = get_tle_by_resno(dpns->inner_tlist, attnum); if (!tle) elog(ERROR, "bogus varattno for subquery var: %d", attnum); Assert(netlevelsup == 0); push_child_plan(dpns, dpns->inner_plan, &save_dpns); result = get_name_for_var_field((Var *) tle->expr, fieldno, levelsup, context); pop_child_plan(dpns, &save_dpns); return result; } } break; case RTE_JOIN: /* Join RTE --- recursively inspect the alias variable */ if (rte->joinaliasvars == NIL) elog(ERROR, "cannot decompile join alias var in plan tree"); Assert(attnum > 0 && attnum <= list_length(rte->joinaliasvars)); expr = (Node *) list_nth(rte->joinaliasvars, attnum - 1); Assert(expr != NULL); /* we intentionally don't strip implicit coercions here */ if (IsA(expr, Var)) return get_name_for_var_field((Var *) expr, fieldno, var->varlevelsup + levelsup, context); /* else fall through to inspect the expression */ break; case RTE_FUNCTION: case RTE_TABLEFUNC: /* * We couldn't get here unless a function is declared with one of * its result columns as RECORD, which is not allowed. */ break; case RTE_CTE: /* CTE reference: examine subquery's output expr */ { CommonTableExpr *cte = NULL; Index ctelevelsup; ListCell *lc; /* * Try to find the referenced CTE using the namespace stack. */ ctelevelsup = rte->ctelevelsup + netlevelsup; if (ctelevelsup >= list_length(context->namespaces)) lc = NULL; else { deparse_namespace *ctedpns; ctedpns = (deparse_namespace *) list_nth(context->namespaces, ctelevelsup); foreach(lc, ctedpns->ctes) { cte = (CommonTableExpr *) lfirst(lc); if (strcmp(cte->ctename, rte->ctename) == 0) break; } } if (lc != NULL) { Query *ctequery = (Query *) cte->ctequery; TargetEntry *ste = get_tle_by_resno(GetCTETargetList(cte), attnum); if (ste == NULL || ste->resjunk) elog(ERROR, "subquery %s does not have attribute %d", rte->eref->aliasname, attnum); expr = (Node *) ste->expr; if (IsA(expr, Var)) { /* * Recurse into the CTE to see what its Var refers to. * We have to build an additional level of namespace * to keep in step with varlevelsup in the CTE. * Furthermore it could be an outer CTE, so we may * have to delete some levels of namespace. */ List *save_nslist = context->namespaces; List *new_nslist; deparse_namespace mydpns; const char *result; set_deparse_for_query(&mydpns, ctequery, context->namespaces); new_nslist = list_copy_tail(context->namespaces, ctelevelsup); context->namespaces = lcons(&mydpns, new_nslist); result = get_name_for_var_field((Var *) expr, fieldno, 0, context); context->namespaces = save_nslist; return result; } /* else fall through to inspect the expression */ } else { /* * We're deparsing a Plan tree so we don't have a CTE * list. But the only place we'd see a Var directly * referencing a CTE RTE is in a CteScan plan node, and we * can look into the subplan's tlist instead. */ TargetEntry *tle; deparse_namespace save_dpns; const char *result; if (!dpns->inner_plan) elog(ERROR, "failed to find plan for CTE %s", rte->eref->aliasname); tle = get_tle_by_resno(dpns->inner_tlist, attnum); if (!tle) elog(ERROR, "bogus varattno for subquery var: %d", attnum); Assert(netlevelsup == 0); push_child_plan(dpns, dpns->inner_plan, &save_dpns); result = get_name_for_var_field((Var *) tle->expr, fieldno, levelsup, context); pop_child_plan(dpns, &save_dpns); return result; } } break; } /* * We now have an expression we can't expand any more, so see if * get_expr_result_tupdesc() can do anything with it. */ tupleDesc = get_expr_result_tupdesc(expr, false); /* Got the tupdesc, so we can extract the field name */ Assert(fieldno >= 1 && fieldno <= tupleDesc->natts); return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname); } /* * Try to find the referenced expression for a PARAM_EXEC Param that might * reference a parameter supplied by an upper NestLoop or SubPlan plan node. * * If successful, return the expression and set *dpns_p and *ancestor_cell_p * appropriately for calling push_ancestor_plan(). If no referent can be * found, return NULL. */ static Node * find_param_referent(Param *param, deparse_context *context, deparse_namespace **dpns_p, ListCell **ancestor_cell_p) { /* Initialize output parameters to prevent compiler warnings */ *dpns_p = NULL; *ancestor_cell_p = NULL; /* * If it's a PARAM_EXEC parameter, look for a matching NestLoopParam or * SubPlan argument. This will necessarily be in some ancestor of the * current expression's Plan node. */ if (param->paramkind == PARAM_EXEC) { deparse_namespace *dpns; Plan *child_plan; bool in_same_plan_level; ListCell *lc; dpns = (deparse_namespace *) linitial(context->namespaces); child_plan = dpns->plan; in_same_plan_level = true; foreach(lc, dpns->ancestors) { Node *ancestor = (Node *) lfirst(lc); ListCell *lc2; /* * NestLoops transmit params to their inner child only; also, once * we've crawled up out of a subplan, this couldn't possibly be * the right match. */ if (IsA(ancestor, NestLoop) && child_plan == innerPlan(ancestor) && in_same_plan_level) { NestLoop *nl = (NestLoop *) ancestor; foreach(lc2, nl->nestParams) { NestLoopParam *nlp = (NestLoopParam *) lfirst(lc2); if (nlp->paramno == param->paramid) { /* Found a match, so return it */ *dpns_p = dpns; *ancestor_cell_p = lc; return (Node *) nlp->paramval; } } } /* * If ancestor is a SubPlan, check the arguments it provides. */ if (IsA(ancestor, SubPlan)) { SubPlan *subplan = (SubPlan *) ancestor; ListCell *lc3; ListCell *lc4; forboth(lc3, subplan->parParam, lc4, subplan->args) { int paramid = lfirst_int(lc3); Node *arg = (Node *) lfirst(lc4); if (paramid == param->paramid) { /* * Found a match, so return it. But, since Vars in * the arg are to be evaluated in the surrounding * context, we have to point to the next ancestor item * that is *not* a SubPlan. */ ListCell *rest; for_each_cell(rest, dpns->ancestors, lnext(dpns->ancestors, lc)) { Node *ancestor2 = (Node *) lfirst(rest); if (!IsA(ancestor2, SubPlan)) { *dpns_p = dpns; *ancestor_cell_p = rest; return arg; } } elog(ERROR, "SubPlan cannot be outermost ancestor"); } } /* We have emerged from a subplan. */ in_same_plan_level = false; /* SubPlan isn't a kind of Plan, so skip the rest */ continue; } /* * Check to see if we're emerging from an initplan of the current * ancestor plan. Initplans never have any parParams, so no need * to search that list, but we need to know if we should reset * in_same_plan_level. */ foreach(lc2, ((Plan *) ancestor)->initPlan) { SubPlan *subplan = castNode(SubPlan, lfirst(lc2)); if (child_plan != (Plan *) list_nth(dpns->subplans, subplan->plan_id - 1)) continue; /* No parameters to be had here. */ Assert(subplan->parParam == NIL); /* We have emerged from an initplan. */ in_same_plan_level = false; break; } /* No luck, crawl up to next ancestor */ child_plan = (Plan *) ancestor; } } /* No referent found */ return NULL; } /* * Display a Param appropriately. */ static void get_parameter(Param *param, deparse_context *context) { Node *expr; deparse_namespace *dpns; ListCell *ancestor_cell; /* * If it's a PARAM_EXEC parameter, try to locate the expression from which * the parameter was computed. Note that failing to find a referent isn't * an error, since the Param might well be a subplan output rather than an * input. */ expr = find_param_referent(param, context, &dpns, &ancestor_cell); if (expr) { /* Found a match, so print it */ deparse_namespace save_dpns; bool save_varprefix; bool need_paren; /* Switch attention to the ancestor plan node */ push_ancestor_plan(dpns, ancestor_cell, &save_dpns); /* * Force prefixing of Vars, since they won't belong to the relation * being scanned in the original plan node. */ save_varprefix = context->varprefix; context->varprefix = true; /* * A Param's expansion is typically a Var, Aggref, GroupingFunc, or * upper-level Param, which wouldn't need extra parentheses. * Otherwise, insert parens to ensure the expression looks atomic. */ need_paren = !(IsA(expr, Var) || IsA(expr, Aggref) || IsA(expr, GroupingFunc) || IsA(expr, Param)); if (need_paren) appendStringInfoChar(context->buf, '('); get_rule_expr(expr, context, false); if (need_paren) appendStringInfoChar(context->buf, ')'); context->varprefix = save_varprefix; pop_ancestor_plan(dpns, &save_dpns); return; } /* * Not PARAM_EXEC, or couldn't find referent: just print $N. */ appendStringInfo(context->buf, "$%d", param->paramid); } /* * get_simple_binary_op_name * * helper function for isSimpleNode * will return single char binary operator name, or NULL if it's not */ static const char * get_simple_binary_op_name(OpExpr *expr) { List *args = expr->args; if (list_length(args) == 2) { /* binary operator */ Node *arg1 = (Node *) linitial(args); Node *arg2 = (Node *) lsecond(args); const char *op; op = generate_operator_name(expr->opno, exprType(arg1), exprType(arg2)); if (strlen(op) == 1) return op; } return NULL; } /* * isSimpleNode - check if given node is simple (doesn't need parenthesizing) * * true : simple in the context of parent node's type * false : not simple */ static bool isSimpleNode(Node *node, Node *parentNode, int prettyFlags) { if (!node) return false; switch (nodeTag(node)) { case T_Var: case T_Const: case T_Param: case T_CoerceToDomainValue: case T_SetToDefault: case T_CurrentOfExpr: /* single words: always simple */ return true; case T_SubscriptingRef: case T_ArrayExpr: case T_RowExpr: case T_CoalesceExpr: case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: case T_NextValueExpr: case T_NullIfExpr: case T_Aggref: case T_GroupingFunc: case T_WindowFunc: case T_FuncExpr: /* function-like: name(..) or name[..] */ return true; /* CASE keywords act as parentheses */ case T_CaseExpr: return true; case T_FieldSelect: /* * appears simple since . has top precedence, unless parent is * T_FieldSelect itself! */ return (IsA(parentNode, FieldSelect) ? false : true); case T_FieldStore: /* * treat like FieldSelect (probably doesn't matter) */ return (IsA(parentNode, FieldStore) ? false : true); case T_CoerceToDomain: /* maybe simple, check args */ return isSimpleNode((Node *) ((CoerceToDomain *) node)->arg, node, prettyFlags); case T_RelabelType: return isSimpleNode((Node *) ((RelabelType *) node)->arg, node, prettyFlags); case T_CoerceViaIO: return isSimpleNode((Node *) ((CoerceViaIO *) node)->arg, node, prettyFlags); case T_ArrayCoerceExpr: return isSimpleNode((Node *) ((ArrayCoerceExpr *) node)->arg, node, prettyFlags); case T_ConvertRowtypeExpr: return isSimpleNode((Node *) ((ConvertRowtypeExpr *) node)->arg, node, prettyFlags); case T_OpExpr: { /* depends on parent node type; needs further checking */ if (prettyFlags & PRETTYFLAG_PAREN && IsA(parentNode, OpExpr)) { const char *op; const char *parentOp; bool is_lopriop; bool is_hipriop; bool is_lopriparent; bool is_hipriparent; op = get_simple_binary_op_name((OpExpr *) node); if (!op) return false; /* We know only the basic operators + - and * / % */ is_lopriop = (strchr("+-", *op) != NULL); is_hipriop = (strchr("*/%", *op) != NULL); if (!(is_lopriop || is_hipriop)) return false; parentOp = get_simple_binary_op_name((OpExpr *) parentNode); if (!parentOp) return false; is_lopriparent = (strchr("+-", *parentOp) != NULL); is_hipriparent = (strchr("*/%", *parentOp) != NULL); if (!(is_lopriparent || is_hipriparent)) return false; if (is_hipriop && is_lopriparent) return true; /* op binds tighter than parent */ if (is_lopriop && is_hipriparent) return false; /* * Operators are same priority --- can skip parens only if * we have (a - b) - c, not a - (b - c). */ if (node == (Node *) linitial(((OpExpr *) parentNode)->args)) return true; return false; } /* else do the same stuff as for T_SubLink et al. */ } /* FALLTHROUGH */ case T_SubLink: case T_NullTest: case T_BooleanTest: case T_DistinctExpr: switch (nodeTag(parentNode)) { case T_FuncExpr: { /* special handling for casts */ CoercionForm type = ((FuncExpr *) parentNode)->funcformat; if (type == COERCE_EXPLICIT_CAST || type == COERCE_IMPLICIT_CAST) return false; return true; /* own parentheses */ } case T_BoolExpr: /* lower precedence */ case T_SubscriptingRef: /* other separators */ case T_ArrayExpr: /* other separators */ case T_RowExpr: /* other separators */ case T_CoalesceExpr: /* own parentheses */ case T_MinMaxExpr: /* own parentheses */ case T_XmlExpr: /* own parentheses */ case T_NullIfExpr: /* other separators */ case T_Aggref: /* own parentheses */ case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ return true; default: return false; } case T_BoolExpr: switch (nodeTag(parentNode)) { case T_BoolExpr: if (prettyFlags & PRETTYFLAG_PAREN) { BoolExprType type; BoolExprType parentType; type = ((BoolExpr *) node)->boolop; parentType = ((BoolExpr *) parentNode)->boolop; switch (type) { case NOT_EXPR: case AND_EXPR: if (parentType == AND_EXPR || parentType == OR_EXPR) return true; break; case OR_EXPR: if (parentType == OR_EXPR) return true; break; } } return false; case T_FuncExpr: { /* special handling for casts */ CoercionForm type = ((FuncExpr *) parentNode)->funcformat; if (type == COERCE_EXPLICIT_CAST || type == COERCE_IMPLICIT_CAST) return false; return true; /* own parentheses */ } case T_SubscriptingRef: /* other separators */ case T_ArrayExpr: /* other separators */ case T_RowExpr: /* other separators */ case T_CoalesceExpr: /* own parentheses */ case T_MinMaxExpr: /* own parentheses */ case T_XmlExpr: /* own parentheses */ case T_NullIfExpr: /* other separators */ case T_Aggref: /* own parentheses */ case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ return true; default: return false; } default: break; } /* those we don't know: in dubio complexo */ return false; } /* * appendContextKeyword - append a keyword to buffer * * If prettyPrint is enabled, perform a line break, and adjust indentation. * Otherwise, just append the keyword. */ static void appendContextKeyword(deparse_context *context, const char *str, int indentBefore, int indentAfter, int indentPlus) { StringInfo buf = context->buf; if (PRETTY_INDENT(context)) { int indentAmount; context->indentLevel += indentBefore; /* remove any trailing spaces currently in the buffer ... */ removeStringInfoSpaces(buf); /* ... then add a newline and some spaces */ appendStringInfoChar(buf, '\n'); if (context->indentLevel < PRETTYINDENT_LIMIT) indentAmount = Max(context->indentLevel, 0) + indentPlus; else { /* * If we're indented more than PRETTYINDENT_LIMIT characters, try * to conserve horizontal space by reducing the per-level * indentation. For best results the scale factor here should * divide all the indent amounts that get added to indentLevel * (PRETTYINDENT_STD, etc). It's important that the indentation * not grow unboundedly, else deeply-nested trees use O(N^2) * whitespace; so we also wrap modulo PRETTYINDENT_LIMIT. */ indentAmount = PRETTYINDENT_LIMIT + (context->indentLevel - PRETTYINDENT_LIMIT) / (PRETTYINDENT_STD / 2); indentAmount %= PRETTYINDENT_LIMIT; /* scale/wrap logic affects indentLevel, but not indentPlus */ indentAmount += indentPlus; } appendStringInfoSpaces(buf, indentAmount); appendStringInfoString(buf, str); context->indentLevel += indentAfter; if (context->indentLevel < 0) context->indentLevel = 0; } else appendStringInfoString(buf, str); } /* * removeStringInfoSpaces - delete trailing spaces from a buffer. * * Possibly this should move to stringinfo.c at some point. */ static void removeStringInfoSpaces(StringInfo str) { while (str->len > 0 && str->data[str->len - 1] == ' ') str->data[--(str->len)] = '\0'; } /* * get_rule_expr_paren - deparse expr using get_rule_expr, * embracing the string with parentheses if necessary for prettyPrint. * * Never embrace if prettyFlags=0, because it's done in the calling node. * * Any node that does *not* embrace its argument node by sql syntax (with * parentheses, non-operator keywords like CASE/WHEN/ON, or comma etc) should * use get_rule_expr_paren instead of get_rule_expr so parentheses can be * added. */ static void get_rule_expr_paren(Node *node, deparse_context *context, bool showimplicit, Node *parentNode) { bool need_paren; need_paren = PRETTY_PAREN(context) && !isSimpleNode(node, parentNode, context->prettyFlags); if (need_paren) appendStringInfoChar(context->buf, '('); get_rule_expr(node, context, showimplicit); if (need_paren) appendStringInfoChar(context->buf, ')'); } /* ---------- * get_rule_expr - Parse back an expression * * Note: showimplicit determines whether we display any implicit cast that * is present at the top of the expression tree. It is a passed argument, * not a field of the context struct, because we change the value as we * recurse down into the expression. In general we suppress implicit casts * when the result type is known with certainty (eg, the arguments of an * OR must be boolean). We display implicit casts for arguments of functions * and operators, since this is needed to be certain that the same function * or operator will be chosen when the expression is re-parsed. * ---------- */ static void get_rule_expr(Node *node, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; if (node == NULL) return; /* Guard against excessively long or deeply-nested queries */ CHECK_FOR_INTERRUPTS(); check_stack_depth(); /* * Each level of get_rule_expr must emit an indivisible term * (parenthesized if necessary) to ensure result is reparsed into the same * expression tree. The only exception is that when the input is a List, * we emit the component items comma-separated with no surrounding * decoration; this is convenient for most callers. */ switch (nodeTag(node)) { case T_Var: (void) get_variable((Var *) node, 0, false, context); break; case T_Const: get_const_expr((Const *) node, context, 0); break; case T_Param: get_parameter((Param *) node, context); break; case T_Aggref: get_agg_expr((Aggref *) node, context, (Aggref *) node); break; case T_GroupingFunc: { GroupingFunc *gexpr = (GroupingFunc *) node; appendStringInfoString(buf, "GROUPING("); get_rule_expr((Node *) gexpr->args, context, true); appendStringInfoChar(buf, ')'); } break; case T_WindowFunc: get_windowfunc_expr((WindowFunc *) node, context); break; case T_SubscriptingRef: { SubscriptingRef *sbsref = (SubscriptingRef *) node; bool need_parens; /* * If the argument is a CaseTestExpr, we must be inside a * FieldStore, ie, we are assigning to an element of an array * within a composite column. Since we already punted on * displaying the FieldStore's target information, just punt * here too, and display only the assignment source * expression. */ if (IsA(sbsref->refexpr, CaseTestExpr)) { Assert(sbsref->refassgnexpr); get_rule_expr((Node *) sbsref->refassgnexpr, context, showimplicit); break; } /* * Parenthesize the argument unless it's a simple Var or a * FieldSelect. (In particular, if it's another * SubscriptingRef, we *must* parenthesize to avoid * confusion.) */ need_parens = !IsA(sbsref->refexpr, Var) && !IsA(sbsref->refexpr, FieldSelect); if (need_parens) appendStringInfoChar(buf, '('); get_rule_expr((Node *) sbsref->refexpr, context, showimplicit); if (need_parens) appendStringInfoChar(buf, ')'); /* * If there's a refassgnexpr, we want to print the node in the * format "container[subscripts] := refassgnexpr". This is * not legal SQL, so decompilation of INSERT or UPDATE * statements should always use processIndirection as part of * the statement-level syntax. We should only see this when * EXPLAIN tries to print the targetlist of a plan resulting * from such a statement. */ if (sbsref->refassgnexpr) { Node *refassgnexpr; /* * Use processIndirection to print this node's subscripts * as well as any additional field selections or * subscripting in immediate descendants. It returns the * RHS expr that is actually being "assigned". */ refassgnexpr = processIndirection(node, context); appendStringInfoString(buf, " := "); get_rule_expr(refassgnexpr, context, showimplicit); } else { /* Just an ordinary container fetch, so print subscripts */ printSubscripts(sbsref, context); } } break; case T_FuncExpr: get_func_expr((FuncExpr *) node, context, showimplicit); break; case T_NamedArgExpr: { NamedArgExpr *na = (NamedArgExpr *) node; appendStringInfo(buf, "%s => ", quote_identifier(na->name)); get_rule_expr((Node *) na->arg, context, showimplicit); } break; case T_OpExpr: get_oper_expr((OpExpr *) node, context); break; case T_DistinctExpr: { DistinctExpr *expr = (DistinctExpr *) node; List *args = expr->args; Node *arg1 = (Node *) linitial(args); Node *arg2 = (Node *) lsecond(args); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(arg1, context, true, node); appendStringInfoString(buf, " IS DISTINCT FROM "); get_rule_expr_paren(arg2, context, true, node); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } break; case T_NullIfExpr: { NullIfExpr *nullifexpr = (NullIfExpr *) node; appendStringInfoString(buf, "NULLIF("); get_rule_expr((Node *) nullifexpr->args, context, true); appendStringInfoChar(buf, ')'); } break; case T_ScalarArrayOpExpr: { ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node; List *args = expr->args; Node *arg1 = (Node *) linitial(args); Node *arg2 = (Node *) lsecond(args); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(arg1, context, true, node); appendStringInfo(buf, " %s %s (", generate_operator_name(expr->opno, exprType(arg1), get_base_element_type(exprType(arg2))), expr->useOr ? "ANY" : "ALL"); get_rule_expr_paren(arg2, context, true, node); /* * There's inherent ambiguity in "x op ANY/ALL (y)" when y is * a bare sub-SELECT. Since we're here, the sub-SELECT must * be meant as a scalar sub-SELECT yielding an array value to * be used in ScalarArrayOpExpr; but the grammar will * preferentially interpret such a construct as an ANY/ALL * SubLink. To prevent misparsing the output that way, insert * a dummy coercion (which will be stripped by parse analysis, * so no inefficiency is added in dump and reload). This is * indeed most likely what the user wrote to get the construct * accepted in the first place. */ if (IsA(arg2, SubLink) && ((SubLink *) arg2)->subLinkType == EXPR_SUBLINK) appendStringInfo(buf, "::%s", format_type_with_typemod(exprType(arg2), exprTypmod(arg2))); appendStringInfoChar(buf, ')'); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } break; case T_BoolExpr: { BoolExpr *expr = (BoolExpr *) node; Node *first_arg = linitial(expr->args); ListCell *arg; switch (expr->boolop) { case AND_EXPR: if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(first_arg, context, false, node); for_each_from(arg, expr->args, 1) { appendStringInfoString(buf, " AND "); get_rule_expr_paren((Node *) lfirst(arg), context, false, node); } if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); break; case OR_EXPR: if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(first_arg, context, false, node); for_each_from(arg, expr->args, 1) { appendStringInfoString(buf, " OR "); get_rule_expr_paren((Node *) lfirst(arg), context, false, node); } if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); break; case NOT_EXPR: if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); appendStringInfoString(buf, "NOT "); get_rule_expr_paren(first_arg, context, false, node); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); break; default: elog(ERROR, "unrecognized boolop: %d", (int) expr->boolop); } } break; case T_SubLink: get_sublink_expr((SubLink *) node, context); break; case T_SubPlan: { SubPlan *subplan = (SubPlan *) node; /* * We cannot see an already-planned subplan in rule deparsing, * only while EXPLAINing a query plan. We don't try to * reconstruct the original SQL, just reference the subplan * that appears elsewhere in EXPLAIN's result. */ if (subplan->useHashTable) appendStringInfo(buf, "(hashed %s)", subplan->plan_name); else appendStringInfo(buf, "(%s)", subplan->plan_name); } break; case T_AlternativeSubPlan: { AlternativeSubPlan *asplan = (AlternativeSubPlan *) node; ListCell *lc; /* As above, this can only happen during EXPLAIN */ appendStringInfoString(buf, "(alternatives: "); foreach(lc, asplan->subplans) { SubPlan *splan = lfirst_node(SubPlan, lc); if (splan->useHashTable) appendStringInfo(buf, "hashed %s", splan->plan_name); else appendStringInfoString(buf, splan->plan_name); if (lnext(asplan->subplans, lc)) appendStringInfoString(buf, " or "); } appendStringInfoChar(buf, ')'); } break; case T_FieldSelect: { FieldSelect *fselect = (FieldSelect *) node; Node *arg = (Node *) fselect->arg; int fno = fselect->fieldnum; const char *fieldname; bool need_parens; /* * Parenthesize the argument unless it's an SubscriptingRef or * another FieldSelect. Note in particular that it would be * WRONG to not parenthesize a Var argument; simplicity is not * the issue here, having the right number of names is. */ need_parens = !IsA(arg, SubscriptingRef) && !IsA(arg, FieldSelect); if (need_parens) appendStringInfoChar(buf, '('); get_rule_expr(arg, context, true); if (need_parens) appendStringInfoChar(buf, ')'); /* * Get and print the field name. */ fieldname = get_name_for_var_field((Var *) arg, fno, 0, context); appendStringInfo(buf, ".%s", quote_identifier(fieldname)); } break; case T_FieldStore: { FieldStore *fstore = (FieldStore *) node; bool need_parens; /* * There is no good way to represent a FieldStore as real SQL, * so decompilation of INSERT or UPDATE statements should * always use processIndirection as part of the * statement-level syntax. We should only get here when * EXPLAIN tries to print the targetlist of a plan resulting * from such a statement. The plan case is even harder than * ordinary rules would be, because the planner tries to * collapse multiple assignments to the same field or subfield * into one FieldStore; so we can see a list of target fields * not just one, and the arguments could be FieldStores * themselves. We don't bother to try to print the target * field names; we just print the source arguments, with a * ROW() around them if there's more than one. This isn't * terribly complete, but it's probably good enough for * EXPLAIN's purposes; especially since anything more would be * either hopelessly confusing or an even poorer * representation of what the plan is actually doing. */ need_parens = (list_length(fstore->newvals) != 1); if (need_parens) appendStringInfoString(buf, "ROW("); get_rule_expr((Node *) fstore->newvals, context, showimplicit); if (need_parens) appendStringInfoChar(buf, ')'); } break; case T_RelabelType: { RelabelType *relabel = (RelabelType *) node; Node *arg = (Node *) relabel->arg; if (relabel->relabelformat == COERCE_IMPLICIT_CAST && !showimplicit) { /* don't show the implicit cast */ get_rule_expr_paren(arg, context, false, node); } else { get_coercion_expr(arg, context, relabel->resulttype, relabel->resulttypmod, node); } } break; case T_CoerceViaIO: { CoerceViaIO *iocoerce = (CoerceViaIO *) node; Node *arg = (Node *) iocoerce->arg; if (iocoerce->coerceformat == COERCE_IMPLICIT_CAST && !showimplicit) { /* don't show the implicit cast */ get_rule_expr_paren(arg, context, false, node); } else { get_coercion_expr(arg, context, iocoerce->resulttype, -1, node); } } break; case T_ArrayCoerceExpr: { ArrayCoerceExpr *acoerce = (ArrayCoerceExpr *) node; Node *arg = (Node *) acoerce->arg; if (acoerce->coerceformat == COERCE_IMPLICIT_CAST && !showimplicit) { /* don't show the implicit cast */ get_rule_expr_paren(arg, context, false, node); } else { get_coercion_expr(arg, context, acoerce->resulttype, acoerce->resulttypmod, node); } } break; case T_ConvertRowtypeExpr: { ConvertRowtypeExpr *convert = (ConvertRowtypeExpr *) node; Node *arg = (Node *) convert->arg; if (convert->convertformat == COERCE_IMPLICIT_CAST && !showimplicit) { /* don't show the implicit cast */ get_rule_expr_paren(arg, context, false, node); } else { get_coercion_expr(arg, context, convert->resulttype, -1, node); } } break; case T_CollateExpr: { CollateExpr *collate = (CollateExpr *) node; Node *arg = (Node *) collate->arg; if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(arg, context, showimplicit, node); appendStringInfo(buf, " COLLATE %s", generate_collation_name(collate->collOid)); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } break; case T_CaseExpr: { CaseExpr *caseexpr = (CaseExpr *) node; ListCell *temp; appendContextKeyword(context, "CASE", 0, PRETTYINDENT_VAR, 0); if (caseexpr->arg) { appendStringInfoChar(buf, ' '); get_rule_expr((Node *) caseexpr->arg, context, true); } foreach(temp, caseexpr->args) { CaseWhen *when = (CaseWhen *) lfirst(temp); Node *w = (Node *) when->expr; if (caseexpr->arg) { /* * The parser should have produced WHEN clauses of the * form "CaseTestExpr = RHS", possibly with an * implicit coercion inserted above the CaseTestExpr. * For accurate decompilation of rules it's essential * that we show just the RHS. However in an * expression that's been through the optimizer, the * WHEN clause could be almost anything (since the * equality operator could have been expanded into an * inline function). If we don't recognize the form * of the WHEN clause, just punt and display it as-is. */ if (IsA(w, OpExpr)) { List *args = ((OpExpr *) w)->args; if (list_length(args) == 2 && IsA(strip_implicit_coercions(linitial(args)), CaseTestExpr)) w = (Node *) lsecond(args); } } if (!PRETTY_INDENT(context)) appendStringInfoChar(buf, ' '); appendContextKeyword(context, "WHEN ", 0, 0, 0); get_rule_expr(w, context, false); appendStringInfoString(buf, " THEN "); get_rule_expr((Node *) when->result, context, true); } if (!PRETTY_INDENT(context)) appendStringInfoChar(buf, ' '); appendContextKeyword(context, "ELSE ", 0, 0, 0); get_rule_expr((Node *) caseexpr->defresult, context, true); if (!PRETTY_INDENT(context)) appendStringInfoChar(buf, ' '); appendContextKeyword(context, "END", -PRETTYINDENT_VAR, 0, 0); } break; case T_CaseTestExpr: { /* * Normally we should never get here, since for expressions * that can contain this node type we attempt to avoid * recursing to it. But in an optimized expression we might * be unable to avoid that (see comments for CaseExpr). If we * do see one, print it as CASE_TEST_EXPR. */ appendStringInfoString(buf, "CASE_TEST_EXPR"); } break; case T_ArrayExpr: { ArrayExpr *arrayexpr = (ArrayExpr *) node; appendStringInfoString(buf, "ARRAY["); get_rule_expr((Node *) arrayexpr->elements, context, true); appendStringInfoChar(buf, ']'); /* * If the array isn't empty, we assume its elements are * coerced to the desired type. If it's empty, though, we * need an explicit coercion to the array type. */ if (arrayexpr->elements == NIL) appendStringInfo(buf, "::%s", format_type_with_typemod(arrayexpr->array_typeid, -1)); } break; case T_RowExpr: { RowExpr *rowexpr = (RowExpr *) node; TupleDesc tupdesc = NULL; ListCell *arg; int i; char *sep; /* * If it's a named type and not RECORD, we may have to skip * dropped columns and/or claim there are NULLs for added * columns. */ if (rowexpr->row_typeid != RECORDOID) { tupdesc = lookup_rowtype_tupdesc(rowexpr->row_typeid, -1); Assert(list_length(rowexpr->args) <= tupdesc->natts); } /* * SQL99 allows "ROW" to be omitted when there is more than * one column, but for simplicity we always print it. */ appendStringInfoString(buf, "ROW("); sep = ""; i = 0; foreach(arg, rowexpr->args) { Node *e = (Node *) lfirst(arg); if (tupdesc == NULL || !TupleDescAttr(tupdesc, i)->attisdropped) { appendStringInfoString(buf, sep); /* Whole-row Vars need special treatment here */ get_rule_expr_toplevel(e, context, true); sep = ", "; } i++; } if (tupdesc != NULL) { while (i < tupdesc->natts) { if (!TupleDescAttr(tupdesc, i)->attisdropped) { appendStringInfoString(buf, sep); appendStringInfoString(buf, "NULL"); sep = ", "; } i++; } ReleaseTupleDesc(tupdesc); } appendStringInfoChar(buf, ')'); if (rowexpr->row_format == COERCE_EXPLICIT_CAST) appendStringInfo(buf, "::%s", format_type_with_typemod(rowexpr->row_typeid, -1)); } break; case T_RowCompareExpr: { RowCompareExpr *rcexpr = (RowCompareExpr *) node; /* * SQL99 allows "ROW" to be omitted when there is more than * one column, but for simplicity we always print it. Within * a ROW expression, whole-row Vars need special treatment, so * use get_rule_list_toplevel. */ appendStringInfoString(buf, "(ROW("); get_rule_list_toplevel(rcexpr->largs, context, true); /* * We assume that the name of the first-column operator will * do for all the rest too. This is definitely open to * failure, eg if some but not all operators were renamed * since the construct was parsed, but there seems no way to * be perfect. */ appendStringInfo(buf, ") %s ROW(", generate_operator_name(linitial_oid(rcexpr->opnos), exprType(linitial(rcexpr->largs)), exprType(linitial(rcexpr->rargs)))); get_rule_list_toplevel(rcexpr->rargs, context, true); appendStringInfoString(buf, "))"); } break; case T_CoalesceExpr: { CoalesceExpr *coalesceexpr = (CoalesceExpr *) node; appendStringInfoString(buf, "COALESCE("); get_rule_expr((Node *) coalesceexpr->args, context, true); appendStringInfoChar(buf, ')'); } break; case T_MinMaxExpr: { MinMaxExpr *minmaxexpr = (MinMaxExpr *) node; switch (minmaxexpr->op) { case IS_GREATEST: appendStringInfoString(buf, "GREATEST("); break; case IS_LEAST: appendStringInfoString(buf, "LEAST("); break; } get_rule_expr((Node *) minmaxexpr->args, context, true); appendStringInfoChar(buf, ')'); } break; case T_SQLValueFunction: { SQLValueFunction *svf = (SQLValueFunction *) node; /* * Note: this code knows that typmod for time, timestamp, and * timestamptz just prints as integer. */ switch (svf->op) { case SVFOP_CURRENT_DATE: appendStringInfoString(buf, "CURRENT_DATE"); break; case SVFOP_CURRENT_TIME: appendStringInfoString(buf, "CURRENT_TIME"); break; case SVFOP_CURRENT_TIME_N: appendStringInfo(buf, "CURRENT_TIME(%d)", svf->typmod); break; case SVFOP_CURRENT_TIMESTAMP: appendStringInfoString(buf, "CURRENT_TIMESTAMP"); break; case SVFOP_CURRENT_TIMESTAMP_N: appendStringInfo(buf, "CURRENT_TIMESTAMP(%d)", svf->typmod); break; case SVFOP_LOCALTIME: appendStringInfoString(buf, "LOCALTIME"); break; case SVFOP_LOCALTIME_N: appendStringInfo(buf, "LOCALTIME(%d)", svf->typmod); break; case SVFOP_LOCALTIMESTAMP: appendStringInfoString(buf, "LOCALTIMESTAMP"); break; case SVFOP_LOCALTIMESTAMP_N: appendStringInfo(buf, "LOCALTIMESTAMP(%d)", svf->typmod); break; case SVFOP_CURRENT_ROLE: appendStringInfoString(buf, "CURRENT_ROLE"); break; case SVFOP_CURRENT_USER: appendStringInfoString(buf, "CURRENT_USER"); break; case SVFOP_USER: appendStringInfoString(buf, "USER"); break; case SVFOP_SESSION_USER: appendStringInfoString(buf, "SESSION_USER"); break; case SVFOP_CURRENT_CATALOG: appendStringInfoString(buf, "CURRENT_CATALOG"); break; case SVFOP_CURRENT_SCHEMA: appendStringInfoString(buf, "CURRENT_SCHEMA"); break; } } break; case T_XmlExpr: { XmlExpr *xexpr = (XmlExpr *) node; bool needcomma = false; ListCell *arg; ListCell *narg; Const *con; switch (xexpr->op) { case IS_XMLCONCAT: appendStringInfoString(buf, "XMLCONCAT("); break; case IS_XMLELEMENT: appendStringInfoString(buf, "XMLELEMENT("); break; case IS_XMLFOREST: appendStringInfoString(buf, "XMLFOREST("); break; case IS_XMLPARSE: appendStringInfoString(buf, "XMLPARSE("); break; case IS_XMLPI: appendStringInfoString(buf, "XMLPI("); break; case IS_XMLROOT: appendStringInfoString(buf, "XMLROOT("); break; case IS_XMLSERIALIZE: appendStringInfoString(buf, "XMLSERIALIZE("); break; case IS_DOCUMENT: break; } if (xexpr->op == IS_XMLPARSE || xexpr->op == IS_XMLSERIALIZE) { if (xexpr->xmloption == XMLOPTION_DOCUMENT) appendStringInfoString(buf, "DOCUMENT "); else appendStringInfoString(buf, "CONTENT "); } if (xexpr->name) { appendStringInfo(buf, "NAME %s", quote_identifier(map_xml_name_to_sql_identifier(xexpr->name))); needcomma = true; } if (xexpr->named_args) { if (xexpr->op != IS_XMLFOREST) { if (needcomma) appendStringInfoString(buf, ", "); appendStringInfoString(buf, "XMLATTRIBUTES("); needcomma = false; } forboth(arg, xexpr->named_args, narg, xexpr->arg_names) { Node *e = (Node *) lfirst(arg); char *argname = strVal(lfirst(narg)); if (needcomma) appendStringInfoString(buf, ", "); get_rule_expr((Node *) e, context, true); appendStringInfo(buf, " AS %s", quote_identifier(map_xml_name_to_sql_identifier(argname))); needcomma = true; } if (xexpr->op != IS_XMLFOREST) appendStringInfoChar(buf, ')'); } if (xexpr->args) { if (needcomma) appendStringInfoString(buf, ", "); switch (xexpr->op) { case IS_XMLCONCAT: case IS_XMLELEMENT: case IS_XMLFOREST: case IS_XMLPI: case IS_XMLSERIALIZE: /* no extra decoration needed */ get_rule_expr((Node *) xexpr->args, context, true); break; case IS_XMLPARSE: Assert(list_length(xexpr->args) == 2); get_rule_expr((Node *) linitial(xexpr->args), context, true); con = lsecond_node(Const, xexpr->args); Assert(!con->constisnull); if (DatumGetBool(con->constvalue)) appendStringInfoString(buf, " PRESERVE WHITESPACE"); else appendStringInfoString(buf, " STRIP WHITESPACE"); break; case IS_XMLROOT: Assert(list_length(xexpr->args) == 3); get_rule_expr((Node *) linitial(xexpr->args), context, true); appendStringInfoString(buf, ", VERSION "); con = (Const *) lsecond(xexpr->args); if (IsA(con, Const) && con->constisnull) appendStringInfoString(buf, "NO VALUE"); else get_rule_expr((Node *) con, context, false); con = lthird_node(Const, xexpr->args); if (con->constisnull) /* suppress STANDALONE NO VALUE */ ; else { switch (DatumGetInt32(con->constvalue)) { case XML_STANDALONE_YES: appendStringInfoString(buf, ", STANDALONE YES"); break; case XML_STANDALONE_NO: appendStringInfoString(buf, ", STANDALONE NO"); break; case XML_STANDALONE_NO_VALUE: appendStringInfoString(buf, ", STANDALONE NO VALUE"); break; default: break; } } break; case IS_DOCUMENT: get_rule_expr_paren((Node *) xexpr->args, context, false, node); break; } } if (xexpr->op == IS_XMLSERIALIZE) appendStringInfo(buf, " AS %s", format_type_with_typemod(xexpr->type, xexpr->typmod)); if (xexpr->op == IS_DOCUMENT) appendStringInfoString(buf, " IS DOCUMENT"); else appendStringInfoChar(buf, ')'); } break; case T_NullTest: { NullTest *ntest = (NullTest *) node; if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren((Node *) ntest->arg, context, true, node); /* * For scalar inputs, we prefer to print as IS [NOT] NULL, * which is shorter and traditional. If it's a rowtype input * but we're applying a scalar test, must print IS [NOT] * DISTINCT FROM NULL to be semantically correct. */ if (ntest->argisrow || !type_is_rowtype(exprType((Node *) ntest->arg))) { switch (ntest->nulltesttype) { case IS_NULL: appendStringInfoString(buf, " IS NULL"); break; case IS_NOT_NULL: appendStringInfoString(buf, " IS NOT NULL"); break; default: elog(ERROR, "unrecognized nulltesttype: %d", (int) ntest->nulltesttype); } } else { switch (ntest->nulltesttype) { case IS_NULL: appendStringInfoString(buf, " IS NOT DISTINCT FROM NULL"); break; case IS_NOT_NULL: appendStringInfoString(buf, " IS DISTINCT FROM NULL"); break; default: elog(ERROR, "unrecognized nulltesttype: %d", (int) ntest->nulltesttype); } } if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } break; case T_BooleanTest: { BooleanTest *btest = (BooleanTest *) node; if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren((Node *) btest->arg, context, false, node); switch (btest->booltesttype) { case IS_TRUE: appendStringInfoString(buf, " IS TRUE"); break; case IS_NOT_TRUE: appendStringInfoString(buf, " IS NOT TRUE"); break; case IS_FALSE: appendStringInfoString(buf, " IS FALSE"); break; case IS_NOT_FALSE: appendStringInfoString(buf, " IS NOT FALSE"); break; case IS_UNKNOWN: appendStringInfoString(buf, " IS UNKNOWN"); break; case IS_NOT_UNKNOWN: appendStringInfoString(buf, " IS NOT UNKNOWN"); break; default: elog(ERROR, "unrecognized booltesttype: %d", (int) btest->booltesttype); } if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } break; case T_CoerceToDomain: { CoerceToDomain *ctest = (CoerceToDomain *) node; Node *arg = (Node *) ctest->arg; if (ctest->coercionformat == COERCE_IMPLICIT_CAST && !showimplicit) { /* don't show the implicit cast */ get_rule_expr(arg, context, false); } else { get_coercion_expr(arg, context, ctest->resulttype, ctest->resulttypmod, node); } } break; case T_CoerceToDomainValue: appendStringInfoString(buf, "VALUE"); break; case T_SetToDefault: appendStringInfoString(buf, "DEFAULT"); break; case T_CurrentOfExpr: { CurrentOfExpr *cexpr = (CurrentOfExpr *) node; if (cexpr->cursor_name) appendStringInfo(buf, "CURRENT OF %s", quote_identifier(cexpr->cursor_name)); else appendStringInfo(buf, "CURRENT OF $%d", cexpr->cursor_param); } break; case T_NextValueExpr: { NextValueExpr *nvexpr = (NextValueExpr *) node; /* * This isn't exactly nextval(), but that seems close enough * for EXPLAIN's purposes. */ appendStringInfoString(buf, "nextval("); simple_quote_literal(buf, generate_relation_name(nvexpr->seqid, NIL)); appendStringInfoChar(buf, ')'); } break; case T_InferenceElem: { InferenceElem *iexpr = (InferenceElem *) node; bool save_varprefix; bool need_parens; /* * InferenceElem can only refer to target relation, so a * prefix is not useful, and indeed would cause parse errors. */ save_varprefix = context->varprefix; context->varprefix = false; /* * Parenthesize the element unless it's a simple Var or a bare * function call. Follows pg_get_indexdef_worker(). */ need_parens = !IsA(iexpr->expr, Var); if (IsA(iexpr->expr, FuncExpr) && ((FuncExpr *) iexpr->expr)->funcformat == COERCE_EXPLICIT_CALL) need_parens = false; if (need_parens) appendStringInfoChar(buf, '('); get_rule_expr((Node *) iexpr->expr, context, false); if (need_parens) appendStringInfoChar(buf, ')'); context->varprefix = save_varprefix; if (iexpr->infercollid) appendStringInfo(buf, " COLLATE %s", generate_collation_name(iexpr->infercollid)); /* Add the operator class name, if not default */ if (iexpr->inferopclass) { Oid inferopclass = iexpr->inferopclass; Oid inferopcinputtype = get_opclass_input_type(iexpr->inferopclass); get_opclass_name(inferopclass, inferopcinputtype, buf); } } break; case T_PartitionBoundSpec: { PartitionBoundSpec *spec = (PartitionBoundSpec *) node; ListCell *cell; char *sep; if (spec->is_default) { appendStringInfoString(buf, "DEFAULT"); break; } switch (spec->strategy) { case PARTITION_STRATEGY_HASH: Assert(spec->modulus > 0 && spec->remainder >= 0); Assert(spec->modulus > spec->remainder); appendStringInfoString(buf, "FOR VALUES"); appendStringInfo(buf, " WITH (modulus %d, remainder %d)", spec->modulus, spec->remainder); break; case PARTITION_STRATEGY_LIST: Assert(spec->listdatums != NIL); appendStringInfoString(buf, "FOR VALUES IN ("); sep = ""; foreach(cell, spec->listdatums) { Const *val = castNode(Const, lfirst(cell)); appendStringInfoString(buf, sep); get_const_expr(val, context, -1); sep = ", "; } appendStringInfoChar(buf, ')'); break; case PARTITION_STRATEGY_RANGE: Assert(spec->lowerdatums != NIL && spec->upperdatums != NIL && list_length(spec->lowerdatums) == list_length(spec->upperdatums)); appendStringInfo(buf, "FOR VALUES FROM %s TO %s", get_range_partbound_string(spec->lowerdatums), get_range_partbound_string(spec->upperdatums)); break; default: elog(ERROR, "unrecognized partition strategy: %d", (int) spec->strategy); break; } } break; case T_List: { char *sep; ListCell *l; sep = ""; foreach(l, (List *) node) { appendStringInfoString(buf, sep); get_rule_expr((Node *) lfirst(l), context, showimplicit); sep = ", "; } } break; case T_TableFunc: get_tablefunc((TableFunc *) node, context, showimplicit); break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); break; } } /* * get_rule_expr_toplevel - Parse back a toplevel expression * * Same as get_rule_expr(), except that if the expr is just a Var, we pass * istoplevel = true not false to get_variable(). This causes whole-row Vars * to get printed with decoration that will prevent expansion of "*". * We need to use this in contexts such as ROW() and VALUES(), where the * parser would expand "foo.*" appearing at top level. (In principle we'd * use this in get_target_list() too, but that has additional worries about * whether to print AS, so it needs to invoke get_variable() directly anyway.) */ static void get_rule_expr_toplevel(Node *node, deparse_context *context, bool showimplicit) { if (node && IsA(node, Var)) (void) get_variable((Var *) node, 0, true, context); else get_rule_expr(node, context, showimplicit); } /* * get_rule_list_toplevel - Parse back a list of toplevel expressions * * Apply get_rule_expr_toplevel() to each element of a List. * * This adds commas between the expressions, but caller is responsible * for printing surrounding decoration. */ static void get_rule_list_toplevel(List *lst, deparse_context *context, bool showimplicit) { const char *sep; ListCell *lc; sep = ""; foreach(lc, lst) { Node *e = (Node *) lfirst(lc); appendStringInfoString(context->buf, sep); get_rule_expr_toplevel(e, context, showimplicit); sep = ", "; } } /* * get_rule_expr_funccall - Parse back a function-call expression * * Same as get_rule_expr(), except that we guarantee that the output will * look like a function call, or like one of the things the grammar treats as * equivalent to a function call (see the func_expr_windowless production). * This is needed in places where the grammar uses func_expr_windowless and * you can't substitute a parenthesized a_expr. If what we have isn't going * to look like a function call, wrap it in a dummy CAST() expression, which * will satisfy the grammar --- and, indeed, is likely what the user wrote to * produce such a thing. */ static void get_rule_expr_funccall(Node *node, deparse_context *context, bool showimplicit) { if (looks_like_function(node)) get_rule_expr(node, context, showimplicit); else { StringInfo buf = context->buf; appendStringInfoString(buf, "CAST("); /* no point in showing any top-level implicit cast */ get_rule_expr(node, context, false); appendStringInfo(buf, " AS %s)", format_type_with_typemod(exprType(node), exprTypmod(node))); } } /* * Helper function to identify node types that satisfy func_expr_windowless. * If in doubt, "false" is always a safe answer. */ static bool looks_like_function(Node *node) { if (node == NULL) return false; /* probably shouldn't happen */ switch (nodeTag(node)) { case T_FuncExpr: /* OK, unless it's going to deparse as a cast */ return (((FuncExpr *) node)->funcformat == COERCE_EXPLICIT_CALL); case T_NullIfExpr: case T_CoalesceExpr: case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: break; } return false; } /* * get_oper_expr - Parse back an OpExpr node */ static void get_oper_expr(OpExpr *expr, deparse_context *context) { StringInfo buf = context->buf; Oid opno = expr->opno; List *args = expr->args; if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); if (list_length(args) == 2) { /* binary operator */ Node *arg1 = (Node *) linitial(args); Node *arg2 = (Node *) lsecond(args); get_rule_expr_paren(arg1, context, true, (Node *) expr); appendStringInfo(buf, " %s ", generate_operator_name(opno, exprType(arg1), exprType(arg2))); get_rule_expr_paren(arg2, context, true, (Node *) expr); } else { /* unary operator --- but which side? */ Node *arg = (Node *) linitial(args); HeapTuple tp; Form_pg_operator optup; tp = SearchSysCache1(OPEROID, ObjectIdGetDatum(opno)); if (!HeapTupleIsValid(tp)) elog(ERROR, "cache lookup failed for operator %u", opno); optup = (Form_pg_operator) GETSTRUCT(tp); switch (optup->oprkind) { case 'l': appendStringInfo(buf, "%s ", generate_operator_name(opno, InvalidOid, exprType(arg))); get_rule_expr_paren(arg, context, true, (Node *) expr); break; case 'r': get_rule_expr_paren(arg, context, true, (Node *) expr); appendStringInfo(buf, " %s", generate_operator_name(opno, exprType(arg), InvalidOid)); break; default: elog(ERROR, "bogus oprkind: %d", optup->oprkind); } ReleaseSysCache(tp); } if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } /* * get_func_expr - Parse back a FuncExpr node */ static void get_func_expr(FuncExpr *expr, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; Oid funcoid = expr->funcid; Oid argtypes[FUNC_MAX_ARGS]; int nargs; List *argnames; bool use_variadic; ListCell *l; /* * If the function call came from an implicit coercion, then just show the * first argument --- unless caller wants to see implicit coercions. */ if (expr->funcformat == COERCE_IMPLICIT_CAST && !showimplicit) { get_rule_expr_paren((Node *) linitial(expr->args), context, false, (Node *) expr); return; } /* * If the function call came from a cast, then show the first argument * plus an explicit cast operation. */ if (expr->funcformat == COERCE_EXPLICIT_CAST || expr->funcformat == COERCE_IMPLICIT_CAST) { Node *arg = linitial(expr->args); Oid rettype = expr->funcresulttype; int32 coercedTypmod; /* Get the typmod if this is a length-coercion function */ (void) exprIsLengthCoercion((Node *) expr, &coercedTypmod); get_coercion_expr(arg, context, rettype, coercedTypmod, (Node *) expr); return; } /* * Normal function: display as proname(args). First we need to extract * the argument datatypes. */ if (list_length(expr->args) > FUNC_MAX_ARGS) ereport(ERROR, (errcode(ERRCODE_TOO_MANY_ARGUMENTS), errmsg("too many arguments"))); nargs = 0; argnames = NIL; foreach(l, expr->args) { Node *arg = (Node *) lfirst(l); if (IsA(arg, NamedArgExpr)) argnames = lappend(argnames, ((NamedArgExpr *) arg)->name); argtypes[nargs] = exprType(arg); nargs++; } appendStringInfo(buf, "%s(", generate_function_name(funcoid, nargs, argnames, argtypes, expr->funcvariadic, &use_variadic, context->special_exprkind)); nargs = 0; foreach(l, expr->args) { if (nargs++ > 0) appendStringInfoString(buf, ", "); if (use_variadic && lnext(expr->args, l) == NULL) appendStringInfoString(buf, "VARIADIC "); get_rule_expr((Node *) lfirst(l), context, true); } appendStringInfoChar(buf, ')'); } /* * get_agg_expr - Parse back an Aggref node */ static void get_agg_expr(Aggref *aggref, deparse_context *context, Aggref *original_aggref) { StringInfo buf = context->buf; Oid argtypes[FUNC_MAX_ARGS]; int nargs; bool use_variadic; /* * For a combining aggregate, we look up and deparse the corresponding * partial aggregate instead. This is necessary because our input * argument list has been replaced; the new argument list always has just * one element, which will point to a partial Aggref that supplies us with * transition states to combine. */ if (DO_AGGSPLIT_COMBINE(aggref->aggsplit)) { TargetEntry *tle; Assert(list_length(aggref->args) == 1); tle = linitial_node(TargetEntry, aggref->args); resolve_special_varno((Node *) tle->expr, context, get_agg_combine_expr, original_aggref); return; } /* * Mark as PARTIAL, if appropriate. We look to the original aggref so as * to avoid printing this when recursing from the code just above. */ if (DO_AGGSPLIT_SKIPFINAL(original_aggref->aggsplit)) appendStringInfoString(buf, "PARTIAL "); /* Extract the argument types as seen by the parser */ nargs = get_aggregate_argtypes(aggref, argtypes); /* Print the aggregate name, schema-qualified if needed */ appendStringInfo(buf, "%s(%s", generate_function_name(aggref->aggfnoid, nargs, NIL, argtypes, aggref->aggvariadic, &use_variadic, context->special_exprkind), (aggref->aggdistinct != NIL) ? "DISTINCT " : ""); if (AGGKIND_IS_ORDERED_SET(aggref->aggkind)) { /* * Ordered-set aggregates do not use "*" syntax. Also, we needn't * worry about inserting VARIADIC. So we can just dump the direct * args as-is. */ Assert(!aggref->aggvariadic); get_rule_expr((Node *) aggref->aggdirectargs, context, true); Assert(aggref->aggorder != NIL); appendStringInfoString(buf, ") WITHIN GROUP (ORDER BY "); get_rule_orderby(aggref->aggorder, aggref->args, false, context); } else { /* aggstar can be set only in zero-argument aggregates */ if (aggref->aggstar) appendStringInfoChar(buf, '*'); else { ListCell *l; int i; i = 0; foreach(l, aggref->args) { TargetEntry *tle = (TargetEntry *) lfirst(l); Node *arg = (Node *) tle->expr; Assert(!IsA(arg, NamedArgExpr)); if (tle->resjunk) continue; if (i++ > 0) appendStringInfoString(buf, ", "); if (use_variadic && i == nargs) appendStringInfoString(buf, "VARIADIC "); get_rule_expr(arg, context, true); } } if (aggref->aggorder != NIL) { appendStringInfoString(buf, " ORDER BY "); get_rule_orderby(aggref->aggorder, aggref->args, false, context); } } if (aggref->aggfilter != NULL) { appendStringInfoString(buf, ") FILTER (WHERE "); get_rule_expr((Node *) aggref->aggfilter, context, false); } appendStringInfoChar(buf, ')'); } /* * This is a helper function for get_agg_expr(). It's used when we deparse * a combining Aggref; resolve_special_varno locates the corresponding partial * Aggref and then calls this. */ static void get_agg_combine_expr(Node *node, deparse_context *context, void *callback_arg) { Aggref *aggref; Aggref *original_aggref = callback_arg; if (!IsA(node, Aggref)) elog(ERROR, "combining Aggref does not point to an Aggref"); aggref = (Aggref *) node; get_agg_expr(aggref, context, original_aggref); } /* * get_windowfunc_expr - Parse back a WindowFunc node */ static void get_windowfunc_expr(WindowFunc *wfunc, deparse_context *context) { StringInfo buf = context->buf; Oid argtypes[FUNC_MAX_ARGS]; int nargs; List *argnames; ListCell *l; if (list_length(wfunc->args) > FUNC_MAX_ARGS) ereport(ERROR, (errcode(ERRCODE_TOO_MANY_ARGUMENTS), errmsg("too many arguments"))); nargs = 0; argnames = NIL; foreach(l, wfunc->args) { Node *arg = (Node *) lfirst(l); if (IsA(arg, NamedArgExpr)) argnames = lappend(argnames, ((NamedArgExpr *) arg)->name); argtypes[nargs] = exprType(arg); nargs++; } appendStringInfo(buf, "%s(", generate_function_name(wfunc->winfnoid, nargs, argnames, argtypes, false, NULL, context->special_exprkind)); /* winstar can be set only in zero-argument aggregates */ if (wfunc->winstar) appendStringInfoChar(buf, '*'); else get_rule_expr((Node *) wfunc->args, context, true); if (wfunc->aggfilter != NULL) { appendStringInfoString(buf, ") FILTER (WHERE "); get_rule_expr((Node *) wfunc->aggfilter, context, false); } appendStringInfoString(buf, ") OVER "); foreach(l, context->windowClause) { WindowClause *wc = (WindowClause *) lfirst(l); if (wc->winref == wfunc->winref) { if (wc->name) appendStringInfoString(buf, quote_identifier(wc->name)); else get_rule_windowspec(wc, context->windowTList, context); break; } } if (l == NULL) { if (context->windowClause) elog(ERROR, "could not find window clause for winref %u", wfunc->winref); /* * In EXPLAIN, we don't have window context information available, so * we have to settle for this: */ appendStringInfoString(buf, "(?)"); } } /* ---------- * get_coercion_expr * * Make a string representation of a value coerced to a specific type * ---------- */ static void get_coercion_expr(Node *arg, deparse_context *context, Oid resulttype, int32 resulttypmod, Node *parentNode) { StringInfo buf = context->buf; /* * Since parse_coerce.c doesn't immediately collapse application of * length-coercion functions to constants, what we'll typically see in * such cases is a Const with typmod -1 and a length-coercion function * right above it. Avoid generating redundant output. However, beware of * suppressing casts when the user actually wrote something like * 'foo'::text::char(3). * * Note: it might seem that we are missing the possibility of needing to * print a COLLATE clause for such a Const. However, a Const could only * have nondefault collation in a post-constant-folding tree, in which the * length coercion would have been folded too. See also the special * handling of CollateExpr in coerce_to_target_type(): any collation * marking will be above the coercion node, not below it. */ if (arg && IsA(arg, Const) && ((Const *) arg)->consttype == resulttype && ((Const *) arg)->consttypmod == -1) { /* Show the constant without normal ::typename decoration */ get_const_expr((Const *) arg, context, -1); } else { if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(arg, context, false, parentNode); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } /* * Never emit resulttype(arg) functional notation. A pg_proc entry could * take precedence, and a resulttype in pg_temp would require schema * qualification that format_type_with_typemod() would usually omit. We've * standardized on arg::resulttype, but CAST(arg AS resulttype) notation * would work fine. */ appendStringInfo(buf, "::%s", format_type_with_typemod(resulttype, resulttypmod)); } /* ---------- * get_const_expr * * Make a string representation of a Const * * showtype can be -1 to never show "::typename" decoration, or +1 to always * show it, or 0 to show it only if the constant wouldn't be assumed to be * the right type by default. * * If the Const's collation isn't default for its type, show that too. * We mustn't do this when showtype is -1 (since that means the caller will * print "::typename", and we can't put a COLLATE clause in between). It's * caller's responsibility that collation isn't missed in such cases. * ---------- */ static void get_const_expr(Const *constval, deparse_context *context, int showtype) { StringInfo buf = context->buf; Oid typoutput; bool typIsVarlena; char *extval; bool needlabel = false; if (constval->constisnull) { /* * Always label the type of a NULL constant to prevent misdecisions * about type when reparsing. */ appendStringInfoString(buf, "NULL"); if (showtype >= 0) { appendStringInfo(buf, "::%s", format_type_with_typemod(constval->consttype, constval->consttypmod)); get_const_collation(constval, context); } return; } getTypeOutputInfo(constval->consttype, &typoutput, &typIsVarlena); extval = OidOutputFunctionCall(typoutput, constval->constvalue); switch (constval->consttype) { case INT4OID: /* * INT4 can be printed without any decoration, unless it is * negative; in that case print it as '-nnn'::integer to ensure * that the output will re-parse as a constant, not as a constant * plus operator. In most cases we could get away with printing * (-nnn) instead, because of the way that gram.y handles negative * literals; but that doesn't work for INT_MIN, and it doesn't * seem that much prettier anyway. */ if (extval[0] != '-') appendStringInfoString(buf, extval); else { appendStringInfo(buf, "'%s'", extval); needlabel = true; /* we must attach a cast */ } break; case NUMERICOID: /* * NUMERIC can be printed without quotes if it looks like a float * constant (not an integer, and not Infinity or NaN) and doesn't * have a leading sign (for the same reason as for INT4). */ if (isdigit((unsigned char) extval[0]) && strcspn(extval, "eE.") != strlen(extval)) { appendStringInfoString(buf, extval); } else { appendStringInfo(buf, "'%s'", extval); needlabel = true; /* we must attach a cast */ } break; case BOOLOID: if (strcmp(extval, "t") == 0) appendStringInfoString(buf, "true"); else appendStringInfoString(buf, "false"); break; default: simple_quote_literal(buf, extval); break; } pfree(extval); if (showtype < 0) return; /* * For showtype == 0, append ::typename unless the constant will be * implicitly typed as the right type when it is read in. * * XXX this code has to be kept in sync with the behavior of the parser, * especially make_const. */ switch (constval->consttype) { case BOOLOID: case UNKNOWNOID: /* These types can be left unlabeled */ needlabel = false; break; case INT4OID: /* We determined above whether a label is needed */ break; case NUMERICOID: /* * Float-looking constants will be typed as numeric, which we * checked above; but if there's a nondefault typmod we need to * show it. */ needlabel |= (constval->consttypmod >= 0); break; default: needlabel = true; break; } if (needlabel || showtype > 0) appendStringInfo(buf, "::%s", format_type_with_typemod(constval->consttype, constval->consttypmod)); get_const_collation(constval, context); } /* * helper for get_const_expr: append COLLATE if needed */ static void get_const_collation(Const *constval, deparse_context *context) { StringInfo buf = context->buf; if (OidIsValid(constval->constcollid)) { Oid typcollation = get_typcollation(constval->consttype); if (constval->constcollid != typcollation) { appendStringInfo(buf, " COLLATE %s", generate_collation_name(constval->constcollid)); } } } /* * simple_quote_literal - Format a string as a SQL literal, append to buf */ static void simple_quote_literal(StringInfo buf, const char *val) { const char *valptr; /* * We form the string literal according to the prevailing setting of * standard_conforming_strings; we never use E''. User is responsible for * making sure result is used correctly. */ appendStringInfoChar(buf, '\''); for (valptr = val; *valptr; valptr++) { char ch = *valptr; if (SQL_STR_DOUBLE(ch, !standard_conforming_strings)) appendStringInfoChar(buf, ch); appendStringInfoChar(buf, ch); } appendStringInfoChar(buf, '\''); } /* ---------- * get_sublink_expr - Parse back a sublink * ---------- */ static void get_sublink_expr(SubLink *sublink, deparse_context *context) { StringInfo buf = context->buf; Query *query = (Query *) (sublink->subselect); char *opname = NULL; bool need_paren; if (sublink->subLinkType == ARRAY_SUBLINK) appendStringInfoString(buf, "ARRAY("); else appendStringInfoChar(buf, '('); /* * Note that we print the name of only the first operator, when there are * multiple combining operators. This is an approximation that could go * wrong in various scenarios (operators in different schemas, renamed * operators, etc) but there is not a whole lot we can do about it, since * the syntax allows only one operator to be shown. */ if (sublink->testexpr) { if (IsA(sublink->testexpr, OpExpr)) { /* single combining operator */ OpExpr *opexpr = (OpExpr *) sublink->testexpr; get_rule_expr(linitial(opexpr->args), context, true); opname = generate_operator_name(opexpr->opno, exprType(linitial(opexpr->args)), exprType(lsecond(opexpr->args))); } else if (IsA(sublink->testexpr, BoolExpr)) { /* multiple combining operators, = or <> cases */ char *sep; ListCell *l; appendStringInfoChar(buf, '('); sep = ""; foreach(l, ((BoolExpr *) sublink->testexpr)->args) { OpExpr *opexpr = lfirst_node(OpExpr, l); appendStringInfoString(buf, sep); get_rule_expr(linitial(opexpr->args), context, true); if (!opname) opname = generate_operator_name(opexpr->opno, exprType(linitial(opexpr->args)), exprType(lsecond(opexpr->args))); sep = ", "; } appendStringInfoChar(buf, ')'); } else if (IsA(sublink->testexpr, RowCompareExpr)) { /* multiple combining operators, < <= > >= cases */ RowCompareExpr *rcexpr = (RowCompareExpr *) sublink->testexpr; appendStringInfoChar(buf, '('); get_rule_expr((Node *) rcexpr->largs, context, true); opname = generate_operator_name(linitial_oid(rcexpr->opnos), exprType(linitial(rcexpr->largs)), exprType(linitial(rcexpr->rargs))); appendStringInfoChar(buf, ')'); } else elog(ERROR, "unrecognized testexpr type: %d", (int) nodeTag(sublink->testexpr)); } need_paren = true; switch (sublink->subLinkType) { case EXISTS_SUBLINK: appendStringInfoString(buf, "EXISTS "); break; case ANY_SUBLINK: if (strcmp(opname, "=") == 0) /* Represent = ANY as IN */ appendStringInfoString(buf, " IN "); else appendStringInfo(buf, " %s ANY ", opname); break; case ALL_SUBLINK: appendStringInfo(buf, " %s ALL ", opname); break; case ROWCOMPARE_SUBLINK: appendStringInfo(buf, " %s ", opname); break; case EXPR_SUBLINK: case MULTIEXPR_SUBLINK: case ARRAY_SUBLINK: need_paren = false; break; case CTE_SUBLINK: /* shouldn't occur in a SubLink */ default: elog(ERROR, "unrecognized sublink type: %d", (int) sublink->subLinkType); break; } if (need_paren) appendStringInfoChar(buf, '('); get_query_def(query, buf, context->namespaces, NULL, false, context->prettyFlags, context->wrapColumn, context->indentLevel); if (need_paren) appendStringInfoString(buf, "))"); else appendStringInfoChar(buf, ')'); } /* ---------- * get_tablefunc - Parse back a table function * ---------- */ static void get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; /* XMLTABLE is the only existing implementation. */ appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) { ListCell *lc1, *lc2; bool first = true; appendStringInfoString(buf, "XMLNAMESPACES ("); forboth(lc1, tf->ns_uris, lc2, tf->ns_names) { Node *expr = (Node *) lfirst(lc1); Value *ns_node = (Value *) lfirst(lc2); if (!first) appendStringInfoString(buf, ", "); else first = false; if (ns_node != NULL) { get_rule_expr(expr, context, showimplicit); appendStringInfo(buf, " AS %s", strVal(ns_node)); } else { appendStringInfoString(buf, "DEFAULT "); get_rule_expr(expr, context, showimplicit); } } appendStringInfoString(buf, "), "); } appendStringInfoChar(buf, '('); get_rule_expr((Node *) tf->rowexpr, context, showimplicit); appendStringInfoString(buf, ") PASSING ("); get_rule_expr((Node *) tf->docexpr, context, showimplicit); appendStringInfoChar(buf, ')'); if (tf->colexprs != NIL) { ListCell *l1; ListCell *l2; ListCell *l3; ListCell *l4; ListCell *l5; int colnum = 0; appendStringInfoString(buf, " COLUMNS "); forfive(l1, tf->colnames, l2, tf->coltypes, l3, tf->coltypmods, l4, tf->colexprs, l5, tf->coldefexprs) { char *colname = strVal(lfirst(l1)); Oid typid = lfirst_oid(l2); int32 typmod = lfirst_int(l3); Node *colexpr = (Node *) lfirst(l4); Node *coldefexpr = (Node *) lfirst(l5); bool ordinality = (tf->ordinalitycol == colnum); bool notnull = bms_is_member(colnum, tf->notnulls); if (colnum > 0) appendStringInfoString(buf, ", "); colnum++; appendStringInfo(buf, "%s %s", quote_identifier(colname), ordinality ? "FOR ORDINALITY" : format_type_with_typemod(typid, typmod)); if (ordinality) continue; if (coldefexpr != NULL) { appendStringInfoString(buf, " DEFAULT ("); get_rule_expr((Node *) coldefexpr, context, showimplicit); appendStringInfoChar(buf, ')'); } if (colexpr != NULL) { appendStringInfoString(buf, " PATH ("); get_rule_expr((Node *) colexpr, context, showimplicit); appendStringInfoChar(buf, ')'); } if (notnull) appendStringInfoString(buf, " NOT NULL"); } } appendStringInfoChar(buf, ')'); } /* ---------- * get_from_clause - Parse back a FROM clause * * "prefix" is the keyword that denotes the start of the list of FROM * elements. It is FROM when used to parse back SELECT and UPDATE, but * is USING when parsing back DELETE. * ---------- */ static void get_from_clause(Query *query, const char *prefix, deparse_context *context) { StringInfo buf = context->buf; bool first = true; ListCell *l; /* * We use the query's jointree as a guide to what to print. However, we * must ignore auto-added RTEs that are marked not inFromCl. (These can * only appear at the top level of the jointree, so it's sufficient to * check here.) This check also ensures we ignore the rule pseudo-RTEs * for NEW and OLD. */ foreach(l, query->jointree->fromlist) { Node *jtnode = (Node *) lfirst(l); if (IsA(jtnode, RangeTblRef)) { int varno = ((RangeTblRef *) jtnode)->rtindex; RangeTblEntry *rte = rt_fetch(varno, query->rtable); if (!rte->inFromCl) continue; } if (first) { appendContextKeyword(context, prefix, -PRETTYINDENT_STD, PRETTYINDENT_STD, 2); first = false; get_from_clause_item(jtnode, query, context); } else { StringInfoData itembuf; appendStringInfoString(buf, ", "); /* * Put the new FROM item's text into itembuf so we can decide * after we've got it whether or not it needs to go on a new line. */ initStringInfo(&itembuf); context->buf = &itembuf; get_from_clause_item(jtnode, query, context); /* Restore context's output buffer */ context->buf = buf; /* Consider line-wrapping if enabled */ if (PRETTY_INDENT(context) && context->wrapColumn >= 0) { /* Does the new item start with a new line? */ if (itembuf.len > 0 && itembuf.data[0] == '\n') { /* If so, we shouldn't add anything */ /* instead, remove any trailing spaces currently in buf */ removeStringInfoSpaces(buf); } else { char *trailing_nl; /* Locate the start of the current line in the buffer */ trailing_nl = strrchr(buf->data, '\n'); if (trailing_nl == NULL) trailing_nl = buf->data; else trailing_nl++; /* * Add a newline, plus some indentation, if the new item * would cause an overflow. */ if (strlen(trailing_nl) + itembuf.len > context->wrapColumn) appendContextKeyword(context, "", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_VAR); } } /* Add the new item */ appendBinaryStringInfo(buf, itembuf.data, itembuf.len); /* clean up */ pfree(itembuf.data); } } } static void get_from_clause_item(Node *jtnode, Query *query, deparse_context *context) { StringInfo buf = context->buf; deparse_namespace *dpns = (deparse_namespace *) linitial(context->namespaces); if (IsA(jtnode, RangeTblRef)) { int varno = ((RangeTblRef *) jtnode)->rtindex; RangeTblEntry *rte = rt_fetch(varno, query->rtable); char *refname = get_rtable_name(varno, context); deparse_columns *colinfo = deparse_columns_fetch(varno, dpns); RangeTblFunction *rtfunc1 = NULL; bool printalias; if (rte->lateral) appendStringInfoString(buf, "LATERAL "); /* Print the FROM item proper */ switch (rte->rtekind) { case RTE_RELATION: /* Normal relation RTE */ appendStringInfo(buf, "%s%s", only_marker(rte), generate_relation_name(rte->relid, context->namespaces)); break; case RTE_SUBQUERY: /* Subquery RTE */ appendStringInfoChar(buf, '('); get_query_def(rte->subquery, buf, context->namespaces, NULL, true, context->prettyFlags, context->wrapColumn, context->indentLevel); appendStringInfoChar(buf, ')'); break; case RTE_FUNCTION: /* Function RTE */ rtfunc1 = (RangeTblFunction *) linitial(rte->functions); /* * Omit ROWS FROM() syntax for just one function, unless it * has both a coldeflist and WITH ORDINALITY. If it has both, * we must use ROWS FROM() syntax to avoid ambiguity about * whether the coldeflist includes the ordinality column. */ if (list_length(rte->functions) == 1 && (rtfunc1->funccolnames == NIL || !rte->funcordinality)) { get_rule_expr_funccall(rtfunc1->funcexpr, context, true); /* we'll print the coldeflist below, if it has one */ } else { bool all_unnest; ListCell *lc; /* * If all the function calls in the list are to unnest, * and none need a coldeflist, then collapse the list back * down to UNNEST(args). (If we had more than one * built-in unnest function, this would get more * difficult.) * * XXX This is pretty ugly, since it makes not-terribly- * future-proof assumptions about what the parser would do * with the output; but the alternative is to emit our * nonstandard ROWS FROM() notation for what might have * been a perfectly spec-compliant multi-argument * UNNEST(). */ all_unnest = true; foreach(lc, rte->functions) { RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc); if (!IsA(rtfunc->funcexpr, FuncExpr) || ((FuncExpr *) rtfunc->funcexpr)->funcid != F_ARRAY_UNNEST || rtfunc->funccolnames != NIL) { all_unnest = false; break; } } if (all_unnest) { List *allargs = NIL; foreach(lc, rte->functions) { RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc); List *args = ((FuncExpr *) rtfunc->funcexpr)->args; allargs = list_concat(allargs, args); } appendStringInfoString(buf, "UNNEST("); get_rule_expr((Node *) allargs, context, true); appendStringInfoChar(buf, ')'); } else { int funcno = 0; appendStringInfoString(buf, "ROWS FROM("); foreach(lc, rte->functions) { RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc); if (funcno > 0) appendStringInfoString(buf, ", "); get_rule_expr_funccall(rtfunc->funcexpr, context, true); if (rtfunc->funccolnames != NIL) { /* Reconstruct the column definition list */ appendStringInfoString(buf, " AS "); get_from_clause_coldeflist(rtfunc, NULL, context); } funcno++; } appendStringInfoChar(buf, ')'); } /* prevent printing duplicate coldeflist below */ rtfunc1 = NULL; } if (rte->funcordinality) appendStringInfoString(buf, " WITH ORDINALITY"); break; case RTE_TABLEFUNC: get_tablefunc(rte->tablefunc, context, true); break; case RTE_VALUES: /* Values list RTE */ appendStringInfoChar(buf, '('); get_values_def(rte->values_lists, context); appendStringInfoChar(buf, ')'); break; case RTE_CTE: appendStringInfoString(buf, quote_identifier(rte->ctename)); break; default: elog(ERROR, "unrecognized RTE kind: %d", (int) rte->rtekind); break; } /* Print the relation alias, if needed */ printalias = false; if (rte->alias != NULL) { /* Always print alias if user provided one */ printalias = true; } else if (colinfo->printaliases) { /* Always print alias if we need to print column aliases */ printalias = true; } else if (rte->rtekind == RTE_RELATION) { /* * No need to print alias if it's same as relation name (this * would normally be the case, but not if set_rtable_names had to * resolve a conflict). */ if (strcmp(refname, get_relation_name(rte->relid)) != 0) printalias = true; } else if (rte->rtekind == RTE_FUNCTION) { /* * For a function RTE, always print alias. This covers possible * renaming of the function and/or instability of the * FigureColname rules for things that aren't simple functions. * Note we'd need to force it anyway for the columndef list case. */ printalias = true; } else if (rte->rtekind == RTE_VALUES) { /* Alias is syntactically required for VALUES */ printalias = true; } else if (rte->rtekind == RTE_CTE) { /* * No need to print alias if it's same as CTE name (this would * normally be the case, but not if set_rtable_names had to * resolve a conflict). */ if (strcmp(refname, rte->ctename) != 0) printalias = true; } if (printalias) appendStringInfo(buf, " %s", quote_identifier(refname)); /* Print the column definitions or aliases, if needed */ if (rtfunc1 && rtfunc1->funccolnames != NIL) { /* Reconstruct the columndef list, which is also the aliases */ get_from_clause_coldeflist(rtfunc1, colinfo, context); } else { /* Else print column aliases as needed */ get_column_alias_list(colinfo, context); } /* Tablesample clause must go after any alias */ if (rte->rtekind == RTE_RELATION && rte->tablesample) get_tablesample_def(rte->tablesample, context); } else if (IsA(jtnode, JoinExpr)) { JoinExpr *j = (JoinExpr *) jtnode; deparse_columns *colinfo = deparse_columns_fetch(j->rtindex, dpns); bool need_paren_on_right; need_paren_on_right = PRETTY_PAREN(context) && !IsA(j->rarg, RangeTblRef) && !(IsA(j->rarg, JoinExpr) && ((JoinExpr *) j->rarg)->alias != NULL); if (!PRETTY_PAREN(context) || j->alias != NULL) appendStringInfoChar(buf, '('); get_from_clause_item(j->larg, query, context); switch (j->jointype) { case JOIN_INNER: if (j->quals) appendContextKeyword(context, " JOIN ", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_JOIN); else appendContextKeyword(context, " CROSS JOIN ", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_JOIN); break; case JOIN_LEFT: appendContextKeyword(context, " LEFT JOIN ", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_JOIN); break; case JOIN_FULL: appendContextKeyword(context, " FULL JOIN ", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_JOIN); break; case JOIN_RIGHT: appendContextKeyword(context, " RIGHT JOIN ", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_JOIN); break; default: elog(ERROR, "unrecognized join type: %d", (int) j->jointype); } if (need_paren_on_right) appendStringInfoChar(buf, '('); get_from_clause_item(j->rarg, query, context); if (need_paren_on_right) appendStringInfoChar(buf, ')'); if (j->usingClause) { ListCell *lc; bool first = true; appendStringInfoString(buf, " USING ("); /* Use the assigned names, not what's in usingClause */ foreach(lc, colinfo->usingNames) { char *colname = (char *) lfirst(lc); if (first) first = false; else appendStringInfoString(buf, ", "); appendStringInfoString(buf, quote_identifier(colname)); } appendStringInfoChar(buf, ')'); } else if (j->quals) { appendStringInfoString(buf, " ON "); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr(j->quals, context, false); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } else if (j->jointype != JOIN_INNER) { /* If we didn't say CROSS JOIN above, we must provide an ON */ appendStringInfoString(buf, " ON TRUE"); } if (!PRETTY_PAREN(context) || j->alias != NULL) appendStringInfoChar(buf, ')'); /* Yes, it's correct to put alias after the right paren ... */ if (j->alias != NULL) { /* * Note that it's correct to emit an alias clause if and only if * there was one originally. Otherwise we'd be converting a named * join to unnamed or vice versa, which creates semantic * subtleties we don't want. However, we might print a different * alias name than was there originally. */ appendStringInfo(buf, " %s", quote_identifier(get_rtable_name(j->rtindex, context))); get_column_alias_list(colinfo, context); } } else elog(ERROR, "unrecognized node type: %d", (int) nodeTag(jtnode)); } /* * get_column_alias_list - print column alias list for an RTE * * Caller must already have printed the relation's alias name. */ static void get_column_alias_list(deparse_columns *colinfo, deparse_context *context) { StringInfo buf = context->buf; int i; bool first = true; /* Don't print aliases if not needed */ if (!colinfo->printaliases) return; for (i = 0; i < colinfo->num_new_cols; i++) { char *colname = colinfo->new_colnames[i]; if (first) { appendStringInfoChar(buf, '('); first = false; } else appendStringInfoString(buf, ", "); appendStringInfoString(buf, quote_identifier(colname)); } if (!first) appendStringInfoChar(buf, ')'); } /* * get_from_clause_coldeflist - reproduce FROM clause coldeflist * * When printing a top-level coldeflist (which is syntactically also the * relation's column alias list), use column names from colinfo. But when * printing a coldeflist embedded inside ROWS FROM(), we prefer to use the * original coldeflist's names, which are available in rtfunc->funccolnames. * Pass NULL for colinfo to select the latter behavior. * * The coldeflist is appended immediately (no space) to buf. Caller is * responsible for ensuring that an alias or AS is present before it. */ static void get_from_clause_coldeflist(RangeTblFunction *rtfunc, deparse_columns *colinfo, deparse_context *context) { StringInfo buf = context->buf; ListCell *l1; ListCell *l2; ListCell *l3; ListCell *l4; int i; appendStringInfoChar(buf, '('); i = 0; forfour(l1, rtfunc->funccoltypes, l2, rtfunc->funccoltypmods, l3, rtfunc->funccolcollations, l4, rtfunc->funccolnames) { Oid atttypid = lfirst_oid(l1); int32 atttypmod = lfirst_int(l2); Oid attcollation = lfirst_oid(l3); char *attname; if (colinfo) attname = colinfo->colnames[i]; else attname = strVal(lfirst(l4)); Assert(attname); /* shouldn't be any dropped columns here */ if (i > 0) appendStringInfoString(buf, ", "); appendStringInfo(buf, "%s %s", quote_identifier(attname), format_type_with_typemod(atttypid, atttypmod)); if (OidIsValid(attcollation) && attcollation != get_typcollation(atttypid)) appendStringInfo(buf, " COLLATE %s", generate_collation_name(attcollation)); i++; } appendStringInfoChar(buf, ')'); } /* * get_tablesample_def - print a TableSampleClause */ static void get_tablesample_def(TableSampleClause *tablesample, deparse_context *context) { StringInfo buf = context->buf; Oid argtypes[1]; int nargs; ListCell *l; /* * We should qualify the handler's function name if it wouldn't be * resolved by lookup in the current search path. */ argtypes[0] = INTERNALOID; appendStringInfo(buf, " TABLESAMPLE %s (", generate_function_name(tablesample->tsmhandler, 1, NIL, argtypes, false, NULL, EXPR_KIND_NONE)); nargs = 0; foreach(l, tablesample->args) { if (nargs++ > 0) appendStringInfoString(buf, ", "); get_rule_expr((Node *) lfirst(l), context, false); } appendStringInfoChar(buf, ')'); if (tablesample->repeatable != NULL) { appendStringInfoString(buf, " REPEATABLE ("); get_rule_expr((Node *) tablesample->repeatable, context, false); appendStringInfoChar(buf, ')'); } } /* * get_opclass_name - fetch name of an index operator class * * The opclass name is appended (after a space) to buf. * * Output is suppressed if the opclass is the default for the given * actual_datatype. (If you don't want this behavior, just pass * InvalidOid for actual_datatype.) */ static void get_opclass_name(Oid opclass, Oid actual_datatype, StringInfo buf) { HeapTuple ht_opc; Form_pg_opclass opcrec; char *opcname; char *nspname; ht_opc = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclass)); if (!HeapTupleIsValid(ht_opc)) elog(ERROR, "cache lookup failed for opclass %u", opclass); opcrec = (Form_pg_opclass) GETSTRUCT(ht_opc); if (!OidIsValid(actual_datatype) || GetDefaultOpClass(actual_datatype, opcrec->opcmethod) != opclass) { /* Okay, we need the opclass name. Do we need to qualify it? */ opcname = NameStr(opcrec->opcname); if (OpclassIsVisible(opclass)) appendStringInfo(buf, " %s", quote_identifier(opcname)); else { nspname = get_namespace_name(opcrec->opcnamespace); appendStringInfo(buf, " %s.%s", quote_identifier(nspname), quote_identifier(opcname)); } } ReleaseSysCache(ht_opc); } /* * generate_opclass_name * Compute the name to display for a opclass specified by OID * * The result includes all necessary quoting and schema-prefixing. */ char * generate_opclass_name(Oid opclass) { StringInfoData buf; initStringInfo(&buf); get_opclass_name(opclass, InvalidOid, &buf); return &buf.data[1]; /* get_opclass_name() prepends space */ } /* * processIndirection - take care of array and subfield assignment * * We strip any top-level FieldStore or assignment SubscriptingRef nodes that * appear in the input, printing them as decoration for the base column * name (which we assume the caller just printed). We might also need to * strip CoerceToDomain nodes, but only ones that appear above assignment * nodes. * * Returns the subexpression that's to be assigned. */ static Node * processIndirection(Node *node, deparse_context *context) { StringInfo buf = context->buf; CoerceToDomain *cdomain = NULL; for (;;) { if (node == NULL) break; if (IsA(node, FieldStore)) { FieldStore *fstore = (FieldStore *) node; Oid typrelid; char *fieldname; /* lookup tuple type */ typrelid = get_typ_typrelid(fstore->resulttype); if (!OidIsValid(typrelid)) elog(ERROR, "argument type %s of FieldStore is not a tuple type", format_type_be(fstore->resulttype)); /* * Print the field name. There should only be one target field in * stored rules. There could be more than that in executable * target lists, but this function cannot be used for that case. */ Assert(list_length(fstore->fieldnums) == 1); fieldname = get_attname(typrelid, linitial_int(fstore->fieldnums), false); appendStringInfo(buf, ".%s", quote_identifier(fieldname)); /* * We ignore arg since it should be an uninteresting reference to * the target column or subcolumn. */ node = (Node *) linitial(fstore->newvals); } else if (IsA(node, SubscriptingRef)) { SubscriptingRef *sbsref = (SubscriptingRef *) node; if (sbsref->refassgnexpr == NULL) break; printSubscripts(sbsref, context); /* * We ignore refexpr since it should be an uninteresting reference * to the target column or subcolumn. */ node = (Node *) sbsref->refassgnexpr; } else if (IsA(node, CoerceToDomain)) { cdomain = (CoerceToDomain *) node; /* If it's an explicit domain coercion, we're done */ if (cdomain->coercionformat != COERCE_IMPLICIT_CAST) break; /* Tentatively descend past the CoerceToDomain */ node = (Node *) cdomain->arg; } else break; } /* * If we descended past a CoerceToDomain whose argument turned out not to * be a FieldStore or array assignment, back up to the CoerceToDomain. * (This is not enough to be fully correct if there are nested implicit * CoerceToDomains, but such cases shouldn't ever occur.) */ if (cdomain && node == (Node *) cdomain->arg) node = (Node *) cdomain; return node; } static void printSubscripts(SubscriptingRef *sbsref, deparse_context *context) { StringInfo buf = context->buf; ListCell *lowlist_item; ListCell *uplist_item; lowlist_item = list_head(sbsref->reflowerindexpr); /* could be NULL */ foreach(uplist_item, sbsref->refupperindexpr) { appendStringInfoChar(buf, '['); if (lowlist_item) { /* If subexpression is NULL, get_rule_expr prints nothing */ get_rule_expr((Node *) lfirst(lowlist_item), context, false); appendStringInfoChar(buf, ':'); lowlist_item = lnext(sbsref->reflowerindexpr, lowlist_item); } /* If subexpression is NULL, get_rule_expr prints nothing */ get_rule_expr((Node *) lfirst(uplist_item), context, false); appendStringInfoChar(buf, ']'); } } /* * quote_identifier - Quote an identifier only if needed * * When quotes are needed, we palloc the required space; slightly * space-wasteful but well worth it for notational simplicity. */ const char * quote_identifier(const char *ident) { /* * Can avoid quoting if ident starts with a lowercase letter or underscore * and contains only lowercase letters, digits, and underscores, *and* is * not any SQL keyword. Otherwise, supply quotes. */ int nquotes = 0; bool safe; const char *ptr; char *result; char *optr; /* * would like to use macros here, but they might yield unwanted * locale-specific results... */ safe = ((ident[0] >= 'a' && ident[0] <= 'z') || ident[0] == '_'); for (ptr = ident; *ptr; ptr++) { char ch = *ptr; if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || (ch == '_')) { /* okay */ } else { safe = false; if (ch == '"') nquotes++; } } if (quote_all_identifiers) safe = false; if (safe) { /* * Check for keyword. We quote keywords except for unreserved ones. * (In some cases we could avoid quoting a col_name or type_func_name * keyword, but it seems much harder than it's worth to tell that.) * * Note: ScanKeywordLookup() does case-insensitive comparison, but * that's fine, since we already know we have all-lower-case. */ int kwnum = ScanKeywordLookup(ident, &ScanKeywords); if (kwnum >= 0 && ScanKeywordCategories[kwnum] != UNRESERVED_KEYWORD) safe = false; } if (safe) return ident; /* no change needed */ result = (char *) palloc(strlen(ident) + nquotes + 2 + 1); optr = result; *optr++ = '"'; for (ptr = ident; *ptr; ptr++) { char ch = *ptr; if (ch == '"') *optr++ = '"'; *optr++ = ch; } *optr++ = '"'; *optr = '\0'; return result; } /* * quote_qualified_identifier - Quote a possibly-qualified identifier * * Return a name of the form qualifier.ident, or just ident if qualifier * is NULL, quoting each component if necessary. The result is palloc'd. */ char * quote_qualified_identifier(const char *qualifier, const char *ident) { StringInfoData buf; initStringInfo(&buf); if (qualifier) appendStringInfo(&buf, "%s.", quote_identifier(qualifier)); appendStringInfoString(&buf, quote_identifier(ident)); return buf.data; } /* * get_relation_name * Get the unqualified name of a relation specified by OID * * This differs from the underlying get_rel_name() function in that it will * throw error instead of silently returning NULL if the OID is bad. */ static char * get_relation_name(Oid relid) { char *relname = get_rel_name(relid); if (!relname) elog(ERROR, "cache lookup failed for relation %u", relid); return relname; } /* * generate_relation_name * Compute the name to display for a relation specified by OID * * The result includes all necessary quoting and schema-prefixing. * * If namespaces isn't NIL, it must be a list of deparse_namespace nodes. * We will forcibly qualify the relation name if it equals any CTE name * visible in the namespace list. */ static char * generate_relation_name(Oid relid, List *namespaces) { HeapTuple tp; Form_pg_class reltup; bool need_qual; ListCell *nslist; char *relname; char *nspname; char *result; tp = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); if (!HeapTupleIsValid(tp)) elog(ERROR, "cache lookup failed for relation %u", relid); reltup = (Form_pg_class) GETSTRUCT(tp); relname = NameStr(reltup->relname); /* Check for conflicting CTE name */ need_qual = false; foreach(nslist, namespaces) { deparse_namespace *dpns = (deparse_namespace *) lfirst(nslist); ListCell *ctlist; foreach(ctlist, dpns->ctes) { CommonTableExpr *cte = (CommonTableExpr *) lfirst(ctlist); if (strcmp(cte->ctename, relname) == 0) { need_qual = true; break; } } if (need_qual) break; } /* Otherwise, qualify the name if not visible in search path */ if (!need_qual) need_qual = !RelationIsVisible(relid); if (need_qual) nspname = get_namespace_name(reltup->relnamespace); else nspname = NULL; result = quote_qualified_identifier(nspname, relname); ReleaseSysCache(tp); return result; } /* * generate_function_name * Compute the name to display for a function specified by OID, * given that it is being called with the specified actual arg names and * types. (Those matter because of ambiguous-function resolution rules.) * * If we're dealing with a potentially variadic function (in practice, this * means a FuncExpr or Aggref, not some other way of calling a function), then * has_variadic must specify whether variadic arguments have been merged, * and *use_variadic_p will be set to indicate whether to print VARIADIC in * the output. For non-FuncExpr cases, has_variadic should be false and * use_variadic_p can be NULL. * * The result includes all necessary quoting and schema-prefixing. */ static char * generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes, bool has_variadic, bool *use_variadic_p, ParseExprKind special_exprkind) { char *result; HeapTuple proctup; Form_pg_proc procform; char *proname; bool use_variadic; char *nspname; FuncDetailCode p_result; Oid p_funcid; Oid p_rettype; bool p_retset; int p_nvargs; Oid p_vatype; Oid *p_true_typeids; bool force_qualify = false; proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid)); if (!HeapTupleIsValid(proctup)) elog(ERROR, "cache lookup failed for function %u", funcid); procform = (Form_pg_proc) GETSTRUCT(proctup); proname = NameStr(procform->proname); /* * Due to parser hacks to avoid needing to reserve CUBE, we need to force * qualification in some special cases. */ if (special_exprkind == EXPR_KIND_GROUP_BY) { if (strcmp(proname, "cube") == 0 || strcmp(proname, "rollup") == 0) force_qualify = true; } /* * Determine whether VARIADIC should be printed. We must do this first * since it affects the lookup rules in func_get_detail(). * * We always print VARIADIC if the function has a merged variadic-array * argument. Note that this is always the case for functions taking a * VARIADIC argument type other than VARIADIC ANY. If we omitted VARIADIC * and printed the array elements as separate arguments, the call could * match a newer non-VARIADIC function. */ if (use_variadic_p) { /* Parser should not have set funcvariadic unless fn is variadic */ Assert(!has_variadic || OidIsValid(procform->provariadic)); use_variadic = has_variadic; *use_variadic_p = use_variadic; } else { Assert(!has_variadic); use_variadic = false; } /* * The idea here is to schema-qualify only if the parser would fail to * resolve the correct function given the unqualified func name with the * specified argtypes and VARIADIC flag. But if we already decided to * force qualification, then we can skip the lookup and pretend we didn't * find it. */ if (!force_qualify) p_result = func_get_detail(list_make1(makeString(proname)), NIL, argnames, nargs, argtypes, !use_variadic, true, &p_funcid, &p_rettype, &p_retset, &p_nvargs, &p_vatype, &p_true_typeids, NULL); else { p_result = FUNCDETAIL_NOTFOUND; p_funcid = InvalidOid; } if ((p_result == FUNCDETAIL_NORMAL || p_result == FUNCDETAIL_AGGREGATE || p_result == FUNCDETAIL_WINDOWFUNC) && p_funcid == funcid) nspname = NULL; else nspname = get_namespace_name(procform->pronamespace); result = quote_qualified_identifier(nspname, proname); ReleaseSysCache(proctup); return result; } /* * generate_operator_name * Compute the name to display for an operator specified by OID, * given that it is being called with the specified actual arg types. * (Arg types matter because of ambiguous-operator resolution rules. * Pass InvalidOid for unused arg of a unary operator.) * * The result includes all necessary quoting and schema-prefixing, * plus the OPERATOR() decoration needed to use a qualified operator name * in an expression. */ static char * generate_operator_name(Oid operid, Oid arg1, Oid arg2) { StringInfoData buf; HeapTuple opertup; Form_pg_operator operform; char *oprname; char *nspname; Operator p_result; initStringInfo(&buf); opertup = SearchSysCache1(OPEROID, ObjectIdGetDatum(operid)); if (!HeapTupleIsValid(opertup)) elog(ERROR, "cache lookup failed for operator %u", operid); operform = (Form_pg_operator) GETSTRUCT(opertup); oprname = NameStr(operform->oprname); /* * The idea here is to schema-qualify only if the parser would fail to * resolve the correct operator given the unqualified op name with the * specified argtypes. */ switch (operform->oprkind) { case 'b': p_result = oper(NULL, list_make1(makeString(oprname)), arg1, arg2, true, -1); break; case 'l': p_result = left_oper(NULL, list_make1(makeString(oprname)), arg2, true, -1); break; case 'r': p_result = right_oper(NULL, list_make1(makeString(oprname)), arg1, true, -1); break; default: elog(ERROR, "unrecognized oprkind: %d", operform->oprkind); p_result = NULL; /* keep compiler quiet */ break; } if (p_result != NULL && oprid(p_result) == operid) nspname = NULL; else { nspname = get_namespace_name(operform->oprnamespace); appendStringInfo(&buf, "OPERATOR(%s.", quote_identifier(nspname)); } appendStringInfoString(&buf, oprname); if (nspname) appendStringInfoChar(&buf, ')'); if (p_result != NULL) ReleaseSysCache(p_result); ReleaseSysCache(opertup); return buf.data; } pg_ivm-1.13/ruleutils_14.c000066400000000000000000007306031507512346500154400ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * ruleutils_14.c * Functions to convert stored expressions/querytrees back to * source text * * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * * Part of src/backend/utils/adt/ruleutils.c in PostgreSQL 14 core * *------------------------------------------------------------------------- */ #include "postgres.h" #include "access/relation.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_opclass.h" #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "commands/defrem.h" #include "common/keywords.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "nodes/pathnodes.h" #include "optimizer/optimizer.h" #include "parser/parse_agg.h" #include "parser/parse_func.h" #include "parser/parse_oper.h" #include "parser/parse_relation.h" #include "parser/parser.h" #include "parser/parsetree.h" #include "rewrite/rewriteHandler.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/hsearch.h" #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/ruleutils.h" #include "utils/syscache.h" #include "utils/typcache.h" #include "utils/xml.h" /* ---------- * Pretty formatting constants * ---------- */ /* Indent counts */ #define PRETTYINDENT_STD 8 #define PRETTYINDENT_JOIN 4 #define PRETTYINDENT_VAR 4 #define PRETTYINDENT_LIMIT 40 /* wrap limit */ /* Pretty flags */ #define PRETTYFLAG_PAREN 0x0001 #define PRETTYFLAG_INDENT 0x0002 #define PRETTYFLAG_SCHEMA 0x0004 /* Default line length for pretty-print wrapping: 0 means wrap always */ #define WRAP_COLUMN_DEFAULT 0 /* macros to test if pretty action needed */ #define PRETTY_PAREN(context) ((context)->prettyFlags & PRETTYFLAG_PAREN) #define PRETTY_INDENT(context) ((context)->prettyFlags & PRETTYFLAG_INDENT) #define PRETTY_SCHEMA(context) ((context)->prettyFlags & PRETTYFLAG_SCHEMA) /* ---------- * Local data types * ---------- */ /* Context info needed for invoking a recursive querytree display routine */ typedef struct { StringInfo buf; /* output buffer to append to */ List *namespaces; /* List of deparse_namespace nodes */ List *windowClause; /* Current query level's WINDOW clause */ List *windowTList; /* targetlist for resolving WINDOW clause */ int prettyFlags; /* enabling of pretty-print functions */ int wrapColumn; /* max line length, or -1 for no limit */ int indentLevel; /* current indent level for pretty-print */ bool varprefix; /* true to print prefixes on Vars */ ParseExprKind special_exprkind; /* set only for exprkinds needing special * handling */ Bitmapset *appendparents; /* if not null, map child Vars of these relids * back to the parent rel */ } deparse_context; /* * Each level of query context around a subtree needs a level of Var namespace. * A Var having varlevelsup=N refers to the N'th item (counting from 0) in * the current context's namespaces list. * * rtable is the list of actual RTEs from the Query or PlannedStmt. * rtable_names holds the alias name to be used for each RTE (either a C * string, or NULL for nameless RTEs such as unnamed joins). * rtable_columns holds the column alias names to be used for each RTE. * * subplans is a list of Plan trees for SubPlans and CTEs (it's only used * in the PlannedStmt case). * ctes is a list of CommonTableExpr nodes (only used in the Query case). * appendrels, if not null (it's only used in the PlannedStmt case), is an * array of AppendRelInfo nodes, indexed by child relid. We use that to map * child-table Vars to their inheritance parents. * * In some cases we need to make names of merged JOIN USING columns unique * across the whole query, not only per-RTE. If so, unique_using is true * and using_names is a list of C strings representing names already assigned * to USING columns. * * When deparsing plan trees, there is always just a single item in the * deparse_namespace list (since a plan tree never contains Vars with * varlevelsup > 0). We store the Plan node that is the immediate * parent of the expression to be deparsed, as well as a list of that * Plan's ancestors. In addition, we store its outer and inner subplan nodes, * as well as their targetlists, and the index tlist if the current plan node * might contain INDEX_VAR Vars. (These fields could be derived on-the-fly * from the current Plan node, but it seems notationally clearer to set them * up as separate fields.) */ typedef struct { List *rtable; /* List of RangeTblEntry nodes */ List *rtable_names; /* Parallel list of names for RTEs */ List *rtable_columns; /* Parallel list of deparse_columns structs */ List *subplans; /* List of Plan trees for SubPlans */ List *ctes; /* List of CommonTableExpr nodes */ AppendRelInfo **appendrels; /* Array of AppendRelInfo nodes, or NULL */ /* Workspace for column alias assignment: */ bool unique_using; /* Are we making USING names globally unique */ List *using_names; /* List of assigned names for USING columns */ /* Remaining fields are used only when deparsing a Plan tree: */ Plan *plan; /* immediate parent of current expression */ List *ancestors; /* ancestors of plan */ Plan *outer_plan; /* outer subnode, or NULL if none */ Plan *inner_plan; /* inner subnode, or NULL if none */ List *outer_tlist; /* referent for OUTER_VAR Vars */ List *inner_tlist; /* referent for INNER_VAR Vars */ List *index_tlist; /* referent for INDEX_VAR Vars */ /* Special namespace representing a function signature: */ char *funcname; int numargs; char **argnames; } deparse_namespace; /* * Per-relation data about column alias names. * * Selecting aliases is unreasonably complicated because of the need to dump * rules/views whose underlying tables may have had columns added, deleted, or * renamed since the query was parsed. We must nonetheless print the rule/view * in a form that can be reloaded and will produce the same results as before. * * For each RTE used in the query, we must assign column aliases that are * unique within that RTE. SQL does not require this of the original query, * but due to factors such as *-expansion we need to be able to uniquely * reference every column in a decompiled query. As long as we qualify all * column references, per-RTE uniqueness is sufficient for that. * * However, we can't ensure per-column name uniqueness for unnamed join RTEs, * since they just inherit column names from their input RTEs, and we can't * rename the columns at the join level. Most of the time this isn't an issue * because we don't need to reference the join's output columns as such; we * can reference the input columns instead. That approach can fail for merged * JOIN USING columns, however, so when we have one of those in an unnamed * join, we have to make that column's alias globally unique across the whole * query to ensure it can be referenced unambiguously. * * Another problem is that a JOIN USING clause requires the columns to be * merged to have the same aliases in both input RTEs, and that no other * columns in those RTEs or their children conflict with the USING names. * To handle that, we do USING-column alias assignment in a recursive * traversal of the query's jointree. When descending through a JOIN with * USING, we preassign the USING column names to the child columns, overriding * other rules for column alias assignment. We also mark each RTE with a list * of all USING column names selected for joins containing that RTE, so that * when we assign other columns' aliases later, we can avoid conflicts. * * Another problem is that if a JOIN's input tables have had columns added or * deleted since the query was parsed, we must generate a column alias list * for the join that matches the current set of input columns --- otherwise, a * change in the number of columns in the left input would throw off matching * of aliases to columns of the right input. Thus, positions in the printable * column alias list are not necessarily one-for-one with varattnos of the * JOIN, so we need a separate new_colnames[] array for printing purposes. */ typedef struct { /* * colnames is an array containing column aliases to use for columns that * existed when the query was parsed. Dropped columns have NULL entries. * This array can be directly indexed by varattno to get a Var's name. * * Non-NULL entries are guaranteed unique within the RTE, *except* when * this is for an unnamed JOIN RTE. In that case we merely copy up names * from the two input RTEs. * * During the recursive descent in set_using_names(), forcible assignment * of a child RTE's column name is represented by pre-setting that element * of the child's colnames array. So at that stage, NULL entries in this * array just mean that no name has been preassigned, not necessarily that * the column is dropped. */ int num_cols; /* length of colnames[] array */ char **colnames; /* array of C strings and NULLs */ /* * new_colnames is an array containing column aliases to use for columns * that would exist if the query was re-parsed against the current * definitions of its base tables. This is what to print as the column * alias list for the RTE. This array does not include dropped columns, * but it will include columns added since original parsing. Indexes in * it therefore have little to do with current varattno values. As above, * entries are unique unless this is for an unnamed JOIN RTE. (In such an * RTE, we never actually print this array, but we must compute it anyway * for possible use in computing column names of upper joins.) The * parallel array is_new_col marks which of these columns are new since * original parsing. Entries with is_new_col false must match the * non-NULL colnames entries one-for-one. */ int num_new_cols; /* length of new_colnames[] array */ char **new_colnames; /* array of C strings */ bool *is_new_col; /* array of bool flags */ /* This flag tells whether we should actually print a column alias list */ bool printaliases; /* This list has all names used as USING names in joins above this RTE */ List *parentUsing; /* names assigned to parent merged columns */ /* * If this struct is for a JOIN RTE, we fill these fields during the * set_using_names() pass to describe its relationship to its child RTEs. * * leftattnos and rightattnos are arrays with one entry per existing * output column of the join (hence, indexable by join varattno). For a * simple reference to a column of the left child, leftattnos[i] is the * child RTE's attno and rightattnos[i] is zero; and conversely for a * column of the right child. But for merged columns produced by JOIN * USING/NATURAL JOIN, both leftattnos[i] and rightattnos[i] are nonzero. * Note that a simple reference might be to a child RTE column that's been * dropped; but that's OK since the column could not be used in the query. * * If it's a JOIN USING, usingNames holds the alias names selected for the * merged columns (these might be different from the original USING list, * if we had to modify names to achieve uniqueness). */ int leftrti; /* rangetable index of left child */ int rightrti; /* rangetable index of right child */ int *leftattnos; /* left-child varattnos of join cols, or 0 */ int *rightattnos; /* right-child varattnos of join cols, or 0 */ List *usingNames; /* names assigned to merged columns */ } deparse_columns; /* This macro is analogous to rt_fetch(), but for deparse_columns structs */ #define deparse_columns_fetch(rangetable_index, dpns) \ ((deparse_columns *) list_nth((dpns)->rtable_columns, (rangetable_index)-1)) /* * Entry in set_rtable_names' hash table */ typedef struct { char name[NAMEDATALEN]; /* Hash key --- must be first */ int counter; /* Largest addition used so far for name */ } NameHashEntry; /* Callback signature for resolve_special_varno() */ typedef void (*rsv_callback) (Node *node, deparse_context *context, void *callback_arg); /* ---------- * Local functions * * Most of these functions used to use fixed-size buffers to build their * results. Now, they take an (already initialized) StringInfo object * as a parameter, and append their text output to its contents. * ---------- */ static void set_rtable_names(deparse_namespace *dpns, List *parent_namespaces, Bitmapset *rels_used); static void set_deparse_for_query(deparse_namespace *dpns, Query *query, List *parent_namespaces); static bool has_dangerous_join_using(deparse_namespace *dpns, Node *jtnode); static void set_using_names(deparse_namespace *dpns, Node *jtnode, List *parentUsing); static void set_relation_column_names(deparse_namespace *dpns, RangeTblEntry *rte, deparse_columns *colinfo); static void set_join_column_names(deparse_namespace *dpns, RangeTblEntry *rte, deparse_columns *colinfo); static bool colname_is_unique(const char *colname, deparse_namespace *dpns, deparse_columns *colinfo); static char *make_colname_unique(char *colname, deparse_namespace *dpns, deparse_columns *colinfo); static void expand_colnames_array_to(deparse_columns *colinfo, int n); static void identify_join_columns(JoinExpr *j, RangeTblEntry *jrte, deparse_columns *colinfo); static char *get_rtable_name(int rtindex, deparse_context *context); static void set_deparse_plan(deparse_namespace *dpns, Plan *plan); static Plan *find_recursive_union(deparse_namespace *dpns, WorkTableScan *wtscan); static void push_child_plan(deparse_namespace *dpns, Plan *plan, deparse_namespace *save_dpns); static void pop_child_plan(deparse_namespace *dpns, deparse_namespace *save_dpns); static void push_ancestor_plan(deparse_namespace *dpns, ListCell *ancestor_cell, deparse_namespace *save_dpns); static void pop_ancestor_plan(deparse_namespace *dpns, deparse_namespace *save_dpns); static void get_query_def(Query *query, StringInfo buf, List *parentnamespace, TupleDesc resultDesc, bool colNamesVisible, int prettyFlags, int wrapColumn, int startIndent); static void get_values_def(List *values_lists, deparse_context *context); static void get_with_clause(Query *query, deparse_context *context); static void get_select_query_def(Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible); static void get_insert_query_def(Query *query, deparse_context *context, bool colNamesVisible); static void get_update_query_def(Query *query, deparse_context *context, bool colNamesVisible); static void get_update_query_targetlist_def(Query *query, List *targetList, deparse_context *context, RangeTblEntry *rte); static void get_delete_query_def(Query *query, deparse_context *context, bool colNamesVisible); static void get_utility_query_def(Query *query, deparse_context *context); static void get_basic_select_query(Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible); static void get_target_list(List *targetList, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible); static void get_setop_query(Node *setOp, Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible); static Node *get_rule_sortgroupclause(Index ref, List *tlist, bool force_colno, deparse_context *context); static void get_rule_groupingset(GroupingSet *gset, List *targetlist, bool omit_parens, deparse_context *context); static void get_rule_orderby(List *orderList, List *targetList, bool force_colno, deparse_context *context); static void get_rule_windowclause(Query *query, deparse_context *context); static void get_rule_windowspec(WindowClause *wc, List *targetList, deparse_context *context); static char *get_variable(Var *var, int levelsup, bool istoplevel, deparse_context *context); static void get_special_variable(Node *node, deparse_context *context, void *callback_arg); static void resolve_special_varno(Node *node, deparse_context *context, rsv_callback callback, void *callback_arg); static Node *find_param_referent(Param *param, deparse_context *context, deparse_namespace **dpns_p, ListCell **ancestor_cell_p); static void get_parameter(Param *param, deparse_context *context); static const char *get_simple_binary_op_name(OpExpr *expr); static bool isSimpleNode(Node *node, Node *parentNode, int prettyFlags); static void appendContextKeyword(deparse_context *context, const char *str, int indentBefore, int indentAfter, int indentPlus); static void removeStringInfoSpaces(StringInfo str); static void get_rule_expr(Node *node, deparse_context *context, bool showimplicit); static void get_rule_expr_toplevel(Node *node, deparse_context *context, bool showimplicit); static void get_rule_list_toplevel(List *lst, deparse_context *context, bool showimplicit); static void get_rule_expr_funccall(Node *node, deparse_context *context, bool showimplicit); static bool looks_like_function(Node *node); static void get_oper_expr(OpExpr *expr, deparse_context *context); static void get_func_expr(FuncExpr *expr, deparse_context *context, bool showimplicit); static void get_agg_expr(Aggref *aggref, deparse_context *context, Aggref *original_aggref); static void get_agg_combine_expr(Node *node, deparse_context *context, void *callback_arg); static void get_windowfunc_expr(WindowFunc *wfunc, deparse_context *context); static bool get_func_sql_syntax(FuncExpr *expr, deparse_context *context); static void get_coercion_expr(Node *arg, deparse_context *context, Oid resulttype, int32 resulttypmod, Node *parentNode); static void get_const_expr(Const *constval, deparse_context *context, int showtype); static void get_const_collation(Const *constval, deparse_context *context); static void simple_quote_literal(StringInfo buf, const char *val); static void get_sublink_expr(SubLink *sublink, deparse_context *context); static void get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit); static void get_from_clause(Query *query, const char *prefix, deparse_context *context); static void get_from_clause_item(Node *jtnode, Query *query, deparse_context *context); static void get_column_alias_list(deparse_columns *colinfo, deparse_context *context); static void get_from_clause_coldeflist(RangeTblFunction *rtfunc, deparse_columns *colinfo, deparse_context *context); static void get_tablesample_def(TableSampleClause *tablesample, deparse_context *context); static void get_opclass_name(Oid opclass, Oid actual_datatype, StringInfo buf); static Node *processIndirection(Node *node, deparse_context *context); static void printSubscripts(SubscriptingRef *sbsref, deparse_context *context); static char *get_relation_name(Oid relid); static char *generate_relation_name(Oid relid, List *namespaces); static char *generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes, bool has_variadic, bool *use_variadic_p, ParseExprKind special_exprkind); static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") /* * set_rtable_names: select RTE aliases to be used in printing a query * * We fill in dpns->rtable_names with a list of names that is one-for-one with * the already-filled dpns->rtable list. Each RTE name is unique among those * in the new namespace plus any ancestor namespaces listed in * parent_namespaces. * * If rels_used isn't NULL, only RTE indexes listed in it are given aliases. * * Note that this function is only concerned with relation names, not column * names. */ static void set_rtable_names(deparse_namespace *dpns, List *parent_namespaces, Bitmapset *rels_used) { HASHCTL hash_ctl; HTAB *names_hash; NameHashEntry *hentry; bool found; int rtindex; ListCell *lc; dpns->rtable_names = NIL; /* nothing more to do if empty rtable */ if (dpns->rtable == NIL) return; /* * We use a hash table to hold known names, so that this process is O(N) * not O(N^2) for N names. */ hash_ctl.keysize = NAMEDATALEN; hash_ctl.entrysize = sizeof(NameHashEntry); hash_ctl.hcxt = CurrentMemoryContext; names_hash = hash_create("set_rtable_names names", list_length(dpns->rtable), &hash_ctl, HASH_ELEM | HASH_STRINGS | HASH_CONTEXT); /* Preload the hash table with names appearing in parent_namespaces */ foreach(lc, parent_namespaces) { deparse_namespace *olddpns = (deparse_namespace *) lfirst(lc); ListCell *lc2; foreach(lc2, olddpns->rtable_names) { char *oldname = (char *) lfirst(lc2); if (oldname == NULL) continue; hentry = (NameHashEntry *) hash_search(names_hash, oldname, HASH_ENTER, &found); /* we do not complain about duplicate names in parent namespaces */ hentry->counter = 0; } } /* Now we can scan the rtable */ rtindex = 1; foreach(lc, dpns->rtable) { RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); char *refname; /* Just in case this takes an unreasonable amount of time ... */ CHECK_FOR_INTERRUPTS(); if (rels_used && !bms_is_member(rtindex, rels_used)) { /* Ignore unreferenced RTE */ refname = NULL; } else if (rte->alias) { /* If RTE has a user-defined alias, prefer that */ refname = rte->alias->aliasname; } else if (rte->rtekind == RTE_RELATION) { /* Use the current actual name of the relation */ refname = get_rel_name(rte->relid); } else if (rte->rtekind == RTE_JOIN) { /* Unnamed join has no refname */ refname = NULL; } else { /* Otherwise use whatever the parser assigned */ refname = rte->eref->aliasname; } /* * If the selected name isn't unique, append digits to make it so, and * make a new hash entry for it once we've got a unique name. For a * very long input name, we might have to truncate to stay within * NAMEDATALEN. */ if (refname) { hentry = (NameHashEntry *) hash_search(names_hash, refname, HASH_ENTER, &found); if (found) { /* Name already in use, must choose a new one */ int refnamelen = strlen(refname); char *modname = (char *) palloc(refnamelen + 16); NameHashEntry *hentry2; do { hentry->counter++; for (;;) { memcpy(modname, refname, refnamelen); sprintf(modname + refnamelen, "_%d", hentry->counter); if (strlen(modname) < NAMEDATALEN) break; /* drop chars from refname to keep all the digits */ refnamelen = pg_mbcliplen(refname, refnamelen, refnamelen - 1); } hentry2 = (NameHashEntry *) hash_search(names_hash, modname, HASH_ENTER, &found); } while (found); hentry2->counter = 0; /* init new hash entry */ refname = modname; } else { /* Name not previously used, need only initialize hentry */ hentry->counter = 0; } } dpns->rtable_names = lappend(dpns->rtable_names, refname); rtindex++; } hash_destroy(names_hash); } /* * set_deparse_for_query: set up deparse_namespace for deparsing a Query tree * * For convenience, this is defined to initialize the deparse_namespace struct * from scratch. */ static void set_deparse_for_query(deparse_namespace *dpns, Query *query, List *parent_namespaces) { ListCell *lc; ListCell *lc2; /* Initialize *dpns and fill rtable/ctes links */ memset(dpns, 0, sizeof(deparse_namespace)); dpns->rtable = query->rtable; dpns->subplans = NIL; dpns->ctes = query->cteList; dpns->appendrels = NULL; /* Assign a unique relation alias to each RTE */ set_rtable_names(dpns, parent_namespaces, NULL); /* Initialize dpns->rtable_columns to contain zeroed structs */ dpns->rtable_columns = NIL; while (list_length(dpns->rtable_columns) < list_length(dpns->rtable)) dpns->rtable_columns = lappend(dpns->rtable_columns, palloc0(sizeof(deparse_columns))); /* If it's a utility query, it won't have a jointree */ if (query->jointree) { /* Detect whether global uniqueness of USING names is needed */ dpns->unique_using = has_dangerous_join_using(dpns, (Node *) query->jointree); /* * Select names for columns merged by USING, via a recursive pass over * the query jointree. */ set_using_names(dpns, (Node *) query->jointree, NIL); } /* * Now assign remaining column aliases for each RTE. We do this in a * linear scan of the rtable, so as to process RTEs whether or not they * are in the jointree (we mustn't miss NEW.*, INSERT target relations, * etc). JOIN RTEs must be processed after their children, but this is * okay because they appear later in the rtable list than their children * (cf Asserts in identify_join_columns()). */ forboth(lc, dpns->rtable, lc2, dpns->rtable_columns) { RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); deparse_columns *colinfo = (deparse_columns *) lfirst(lc2); if (rte->rtekind == RTE_JOIN) set_join_column_names(dpns, rte, colinfo); else set_relation_column_names(dpns, rte, colinfo); } } /* * has_dangerous_join_using: search jointree for unnamed JOIN USING * * Merged columns of a JOIN USING may act differently from either of the input * columns, either because they are merged with COALESCE (in a FULL JOIN) or * because an implicit coercion of the underlying input column is required. * In such a case the column must be referenced as a column of the JOIN not as * a column of either input. And this is problematic if the join is unnamed * (alias-less): we cannot qualify the column's name with an RTE name, since * there is none. (Forcibly assigning an alias to the join is not a solution, * since that will prevent legal references to tables below the join.) * To ensure that every column in the query is unambiguously referenceable, * we must assign such merged columns names that are globally unique across * the whole query, aliasing other columns out of the way as necessary. * * Because the ensuing re-aliasing is fairly damaging to the readability of * the query, we don't do this unless we have to. So, we must pre-scan * the join tree to see if we have to, before starting set_using_names(). */ static bool has_dangerous_join_using(deparse_namespace *dpns, Node *jtnode) { if (IsA(jtnode, RangeTblRef)) { /* nothing to do here */ } else if (IsA(jtnode, FromExpr)) { FromExpr *f = (FromExpr *) jtnode; ListCell *lc; foreach(lc, f->fromlist) { if (has_dangerous_join_using(dpns, (Node *) lfirst(lc))) return true; } } else if (IsA(jtnode, JoinExpr)) { JoinExpr *j = (JoinExpr *) jtnode; /* Is it an unnamed JOIN with USING? */ if (j->alias == NULL && j->usingClause) { /* * Yes, so check each join alias var to see if any of them are not * simple references to underlying columns. If so, we have a * dangerous situation and must pick unique aliases. */ RangeTblEntry *jrte = rt_fetch(j->rtindex, dpns->rtable); /* We need only examine the merged columns */ for (int i = 0; i < jrte->joinmergedcols; i++) { Node *aliasvar = list_nth(jrte->joinaliasvars, i); if (!IsA(aliasvar, Var)) return true; } } /* Nope, but inspect children */ if (has_dangerous_join_using(dpns, j->larg)) return true; if (has_dangerous_join_using(dpns, j->rarg)) return true; } else elog(ERROR, "unrecognized node type: %d", (int) nodeTag(jtnode)); return false; } /* * set_using_names: select column aliases to be used for merged USING columns * * We do this during a recursive descent of the query jointree. * dpns->unique_using must already be set to determine the global strategy. * * Column alias info is saved in the dpns->rtable_columns list, which is * assumed to be filled with pre-zeroed deparse_columns structs. * * parentUsing is a list of all USING aliases assigned in parent joins of * the current jointree node. (The passed-in list must not be modified.) */ static void set_using_names(deparse_namespace *dpns, Node *jtnode, List *parentUsing) { if (IsA(jtnode, RangeTblRef)) { /* nothing to do now */ } else if (IsA(jtnode, FromExpr)) { FromExpr *f = (FromExpr *) jtnode; ListCell *lc; foreach(lc, f->fromlist) set_using_names(dpns, (Node *) lfirst(lc), parentUsing); } else if (IsA(jtnode, JoinExpr)) { JoinExpr *j = (JoinExpr *) jtnode; RangeTblEntry *rte = rt_fetch(j->rtindex, dpns->rtable); deparse_columns *colinfo = deparse_columns_fetch(j->rtindex, dpns); int *leftattnos; int *rightattnos; deparse_columns *leftcolinfo; deparse_columns *rightcolinfo; int i; ListCell *lc; /* Get info about the shape of the join */ identify_join_columns(j, rte, colinfo); leftattnos = colinfo->leftattnos; rightattnos = colinfo->rightattnos; /* Look up the not-yet-filled-in child deparse_columns structs */ leftcolinfo = deparse_columns_fetch(colinfo->leftrti, dpns); rightcolinfo = deparse_columns_fetch(colinfo->rightrti, dpns); /* * If this join is unnamed, then we cannot substitute new aliases at * this level, so any name requirements pushed down to here must be * pushed down again to the children. */ if (rte->alias == NULL) { for (i = 0; i < colinfo->num_cols; i++) { char *colname = colinfo->colnames[i]; if (colname == NULL) continue; /* Push down to left column, unless it's a system column */ if (leftattnos[i] > 0) { expand_colnames_array_to(leftcolinfo, leftattnos[i]); leftcolinfo->colnames[leftattnos[i] - 1] = colname; } /* Same on the righthand side */ if (rightattnos[i] > 0) { expand_colnames_array_to(rightcolinfo, rightattnos[i]); rightcolinfo->colnames[rightattnos[i] - 1] = colname; } } } /* * If there's a USING clause, select the USING column names and push * those names down to the children. We have two strategies: * * If dpns->unique_using is true, we force all USING names to be * unique across the whole query level. In principle we'd only need * the names of dangerous USING columns to be globally unique, but to * safely assign all USING names in a single pass, we have to enforce * the same uniqueness rule for all of them. However, if a USING * column's name has been pushed down from the parent, we should use * it as-is rather than making a uniqueness adjustment. This is * necessary when we're at an unnamed join, and it creates no risk of * ambiguity. Also, if there's a user-written output alias for a * merged column, we prefer to use that rather than the input name; * this simplifies the logic and seems likely to lead to less aliasing * overall. * * If dpns->unique_using is false, we only need USING names to be * unique within their own join RTE. We still need to honor * pushed-down names, though. * * Though significantly different in results, these two strategies are * implemented by the same code, with only the difference of whether * to put assigned names into dpns->using_names. */ if (j->usingClause) { /* Copy the input parentUsing list so we don't modify it */ parentUsing = list_copy(parentUsing); /* USING names must correspond to the first join output columns */ expand_colnames_array_to(colinfo, list_length(j->usingClause)); i = 0; foreach(lc, j->usingClause) { char *colname = strVal(lfirst(lc)); /* Assert it's a merged column */ Assert(leftattnos[i] != 0 && rightattnos[i] != 0); /* Adopt passed-down name if any, else select unique name */ if (colinfo->colnames[i] != NULL) colname = colinfo->colnames[i]; else { /* Prefer user-written output alias if any */ if (rte->alias && i < list_length(rte->alias->colnames)) colname = strVal(list_nth(rte->alias->colnames, i)); /* Make it appropriately unique */ colname = make_colname_unique(colname, dpns, colinfo); if (dpns->unique_using) dpns->using_names = lappend(dpns->using_names, colname); /* Save it as output column name, too */ colinfo->colnames[i] = colname; } /* Remember selected names for use later */ colinfo->usingNames = lappend(colinfo->usingNames, colname); parentUsing = lappend(parentUsing, colname); /* Push down to left column, unless it's a system column */ if (leftattnos[i] > 0) { expand_colnames_array_to(leftcolinfo, leftattnos[i]); leftcolinfo->colnames[leftattnos[i] - 1] = colname; } /* Same on the righthand side */ if (rightattnos[i] > 0) { expand_colnames_array_to(rightcolinfo, rightattnos[i]); rightcolinfo->colnames[rightattnos[i] - 1] = colname; } i++; } } /* Mark child deparse_columns structs with correct parentUsing info */ leftcolinfo->parentUsing = parentUsing; rightcolinfo->parentUsing = parentUsing; /* Now recursively assign USING column names in children */ set_using_names(dpns, j->larg, parentUsing); set_using_names(dpns, j->rarg, parentUsing); } else elog(ERROR, "unrecognized node type: %d", (int) nodeTag(jtnode)); } /* * set_relation_column_names: select column aliases for a non-join RTE * * Column alias info is saved in *colinfo, which is assumed to be pre-zeroed. * If any colnames entries are already filled in, those override local * choices. */ static void set_relation_column_names(deparse_namespace *dpns, RangeTblEntry *rte, deparse_columns *colinfo) { int ncolumns; char **real_colnames; bool changed_any; int noldcolumns; int i; int j; /* * Construct an array of the current "real" column names of the RTE. * real_colnames[] will be indexed by physical column number, with NULL * entries for dropped columns. */ if (rte->rtekind == RTE_RELATION) { /* Relation --- look to the system catalogs for up-to-date info */ Relation rel; TupleDesc tupdesc; rel = relation_open(rte->relid, AccessShareLock); tupdesc = RelationGetDescr(rel); ncolumns = tupdesc->natts; real_colnames = (char **) palloc(ncolumns * sizeof(char *)); for (i = 0; i < ncolumns; i++) { Form_pg_attribute attr = TupleDescAttr(tupdesc, i); if (attr->attisdropped) real_colnames[i] = NULL; else real_colnames[i] = pstrdup(NameStr(attr->attname)); } relation_close(rel, AccessShareLock); } else { /* Otherwise get the column names from eref or expandRTE() */ List *colnames; ListCell *lc; /* * Functions returning composites have the annoying property that some * of the composite type's columns might have been dropped since the * query was parsed. If possible, use expandRTE() to handle that * case, since it has the tedious logic needed to find out about * dropped columns. However, if we're explaining a plan, then we * don't have rte->functions because the planner thinks that won't be * needed later, and that breaks expandRTE(). So in that case we have * to rely on rte->eref, which may lead us to report a dropped * column's old name; that seems close enough for EXPLAIN's purposes. * * For non-RELATION, non-FUNCTION RTEs, we can just look at rte->eref, * which should be sufficiently up-to-date: no other RTE types can * have columns get dropped from under them after parsing. */ if (rte->rtekind == RTE_FUNCTION && rte->functions != NIL) { /* Since we're not creating Vars, rtindex etc. don't matter */ expandRTE(rte, 1, 0, -1, true /* include dropped */ , &colnames, NULL); } else colnames = rte->eref->colnames; ncolumns = list_length(colnames); real_colnames = (char **) palloc(ncolumns * sizeof(char *)); i = 0; foreach(lc, colnames) { /* * If the column name we find here is an empty string, then it's a * dropped column, so change to NULL. */ char *cname = strVal(lfirst(lc)); if (cname[0] == '\0') cname = NULL; real_colnames[i] = cname; i++; } } /* * Ensure colinfo->colnames has a slot for each column. (It could be long * enough already, if we pushed down a name for the last column.) Note: * it's possible that there are now more columns than there were when the * query was parsed, ie colnames could be longer than rte->eref->colnames. * We must assign unique aliases to the new columns too, else there could * be unresolved conflicts when the view/rule is reloaded. */ expand_colnames_array_to(colinfo, ncolumns); Assert(colinfo->num_cols == ncolumns); /* * Make sufficiently large new_colnames and is_new_col arrays, too. * * Note: because we leave colinfo->num_new_cols zero until after the loop, * colname_is_unique will not consult that array, which is fine because it * would only be duplicate effort. */ colinfo->new_colnames = (char **) palloc(ncolumns * sizeof(char *)); colinfo->is_new_col = (bool *) palloc(ncolumns * sizeof(bool)); /* * Scan the columns, select a unique alias for each one, and store it in * colinfo->colnames and colinfo->new_colnames. The former array has NULL * entries for dropped columns, the latter omits them. Also mark * new_colnames entries as to whether they are new since parse time; this * is the case for entries beyond the length of rte->eref->colnames. */ noldcolumns = list_length(rte->eref->colnames); changed_any = false; j = 0; for (i = 0; i < ncolumns; i++) { char *real_colname = real_colnames[i]; char *colname = colinfo->colnames[i]; /* Skip dropped columns */ if (real_colname == NULL) { Assert(colname == NULL); /* colnames[i] is already NULL */ continue; } /* If alias already assigned, that's what to use */ if (colname == NULL) { /* If user wrote an alias, prefer that over real column name */ if (rte->alias && i < list_length(rte->alias->colnames)) colname = strVal(list_nth(rte->alias->colnames, i)); else colname = real_colname; /* Unique-ify and insert into colinfo */ colname = make_colname_unique(colname, dpns, colinfo); colinfo->colnames[i] = colname; } /* Put names of non-dropped columns in new_colnames[] too */ colinfo->new_colnames[j] = colname; /* And mark them as new or not */ colinfo->is_new_col[j] = (i >= noldcolumns); j++; /* Remember if any assigned aliases differ from "real" name */ if (!changed_any && strcmp(colname, real_colname) != 0) changed_any = true; } /* * Set correct length for new_colnames[] array. (Note: if columns have * been added, colinfo->num_cols includes them, which is not really quite * right but is harmless, since any new columns must be at the end where * they won't affect varattnos of pre-existing columns.) */ colinfo->num_new_cols = j; /* * For a relation RTE, we need only print the alias column names if any * are different from the underlying "real" names. For a function RTE, * always emit a complete column alias list; this is to protect against * possible instability of the default column names (eg, from altering * parameter names). For tablefunc RTEs, we never print aliases, because * the column names are part of the clause itself. For other RTE types, * print if we changed anything OR if there were user-written column * aliases (since the latter would be part of the underlying "reality"). */ if (rte->rtekind == RTE_RELATION) colinfo->printaliases = changed_any; else if (rte->rtekind == RTE_FUNCTION) colinfo->printaliases = true; else if (rte->rtekind == RTE_TABLEFUNC) colinfo->printaliases = false; else if (rte->alias && rte->alias->colnames != NIL) colinfo->printaliases = true; else colinfo->printaliases = changed_any; } /* * set_join_column_names: select column aliases for a join RTE * * Column alias info is saved in *colinfo, which is assumed to be pre-zeroed. * If any colnames entries are already filled in, those override local * choices. Also, names for USING columns were already chosen by * set_using_names(). We further expect that column alias selection has been * completed for both input RTEs. */ static void set_join_column_names(deparse_namespace *dpns, RangeTblEntry *rte, deparse_columns *colinfo) { deparse_columns *leftcolinfo; deparse_columns *rightcolinfo; bool changed_any; int noldcolumns; int nnewcolumns; Bitmapset *leftmerged = NULL; Bitmapset *rightmerged = NULL; int i; int j; int ic; int jc; /* Look up the previously-filled-in child deparse_columns structs */ leftcolinfo = deparse_columns_fetch(colinfo->leftrti, dpns); rightcolinfo = deparse_columns_fetch(colinfo->rightrti, dpns); /* * Ensure colinfo->colnames has a slot for each column. (It could be long * enough already, if we pushed down a name for the last column.) Note: * it's possible that one or both inputs now have more columns than there * were when the query was parsed, but we'll deal with that below. We * only need entries in colnames for pre-existing columns. */ noldcolumns = list_length(rte->eref->colnames); expand_colnames_array_to(colinfo, noldcolumns); Assert(colinfo->num_cols == noldcolumns); /* * Scan the join output columns, select an alias for each one, and store * it in colinfo->colnames. If there are USING columns, set_using_names() * already selected their names, so we can start the loop at the first * non-merged column. */ changed_any = false; for (i = list_length(colinfo->usingNames); i < noldcolumns; i++) { char *colname = colinfo->colnames[i]; char *real_colname; /* Join column must refer to at least one input column */ Assert(colinfo->leftattnos[i] != 0 || colinfo->rightattnos[i] != 0); /* Get the child column name */ if (colinfo->leftattnos[i] > 0) real_colname = leftcolinfo->colnames[colinfo->leftattnos[i] - 1]; else if (colinfo->rightattnos[i] > 0) real_colname = rightcolinfo->colnames[colinfo->rightattnos[i] - 1]; else { /* We're joining system columns --- use eref name */ real_colname = strVal(list_nth(rte->eref->colnames, i)); } /* If child col has been dropped, no need to assign a join colname */ if (real_colname == NULL) { colinfo->colnames[i] = NULL; continue; } /* In an unnamed join, just report child column names as-is */ if (rte->alias == NULL) { colinfo->colnames[i] = real_colname; continue; } /* If alias already assigned, that's what to use */ if (colname == NULL) { /* If user wrote an alias, prefer that over real column name */ if (rte->alias && i < list_length(rte->alias->colnames)) colname = strVal(list_nth(rte->alias->colnames, i)); else colname = real_colname; /* Unique-ify and insert into colinfo */ colname = make_colname_unique(colname, dpns, colinfo); colinfo->colnames[i] = colname; } /* Remember if any assigned aliases differ from "real" name */ if (!changed_any && strcmp(colname, real_colname) != 0) changed_any = true; } /* * Calculate number of columns the join would have if it were re-parsed * now, and create storage for the new_colnames and is_new_col arrays. * * Note: colname_is_unique will be consulting new_colnames[] during the * loops below, so its not-yet-filled entries must be zeroes. */ nnewcolumns = leftcolinfo->num_new_cols + rightcolinfo->num_new_cols - list_length(colinfo->usingNames); colinfo->num_new_cols = nnewcolumns; colinfo->new_colnames = (char **) palloc0(nnewcolumns * sizeof(char *)); colinfo->is_new_col = (bool *) palloc0(nnewcolumns * sizeof(bool)); /* * Generating the new_colnames array is a bit tricky since any new columns * added since parse time must be inserted in the right places. This code * must match the parser, which will order a join's columns as merged * columns first (in USING-clause order), then non-merged columns from the * left input (in attnum order), then non-merged columns from the right * input (ditto). If one of the inputs is itself a join, its columns will * be ordered according to the same rule, which means newly-added columns * might not be at the end. We can figure out what's what by consulting * the leftattnos and rightattnos arrays plus the input is_new_col arrays. * * In these loops, i indexes leftattnos/rightattnos (so it's join varattno * less one), j indexes new_colnames/is_new_col, and ic/jc have similar * meanings for the current child RTE. */ /* Handle merged columns; they are first and can't be new */ i = j = 0; while (i < noldcolumns && colinfo->leftattnos[i] != 0 && colinfo->rightattnos[i] != 0) { /* column name is already determined and known unique */ colinfo->new_colnames[j] = colinfo->colnames[i]; colinfo->is_new_col[j] = false; /* build bitmapsets of child attnums of merged columns */ if (colinfo->leftattnos[i] > 0) leftmerged = bms_add_member(leftmerged, colinfo->leftattnos[i]); if (colinfo->rightattnos[i] > 0) rightmerged = bms_add_member(rightmerged, colinfo->rightattnos[i]); i++, j++; } /* Handle non-merged left-child columns */ ic = 0; for (jc = 0; jc < leftcolinfo->num_new_cols; jc++) { char *child_colname = leftcolinfo->new_colnames[jc]; if (!leftcolinfo->is_new_col[jc]) { /* Advance ic to next non-dropped old column of left child */ while (ic < leftcolinfo->num_cols && leftcolinfo->colnames[ic] == NULL) ic++; Assert(ic < leftcolinfo->num_cols); ic++; /* If it is a merged column, we already processed it */ if (bms_is_member(ic, leftmerged)) continue; /* Else, advance i to the corresponding existing join column */ while (i < colinfo->num_cols && colinfo->colnames[i] == NULL) i++; Assert(i < colinfo->num_cols); Assert(ic == colinfo->leftattnos[i]); /* Use the already-assigned name of this column */ colinfo->new_colnames[j] = colinfo->colnames[i]; i++; } else { /* * Unique-ify the new child column name and assign, unless we're * in an unnamed join, in which case just copy */ if (rte->alias != NULL) { colinfo->new_colnames[j] = make_colname_unique(child_colname, dpns, colinfo); if (!changed_any && strcmp(colinfo->new_colnames[j], child_colname) != 0) changed_any = true; } else colinfo->new_colnames[j] = child_colname; } colinfo->is_new_col[j] = leftcolinfo->is_new_col[jc]; j++; } /* Handle non-merged right-child columns in exactly the same way */ ic = 0; for (jc = 0; jc < rightcolinfo->num_new_cols; jc++) { char *child_colname = rightcolinfo->new_colnames[jc]; if (!rightcolinfo->is_new_col[jc]) { /* Advance ic to next non-dropped old column of right child */ while (ic < rightcolinfo->num_cols && rightcolinfo->colnames[ic] == NULL) ic++; Assert(ic < rightcolinfo->num_cols); ic++; /* If it is a merged column, we already processed it */ if (bms_is_member(ic, rightmerged)) continue; /* Else, advance i to the corresponding existing join column */ while (i < colinfo->num_cols && colinfo->colnames[i] == NULL) i++; Assert(i < colinfo->num_cols); Assert(ic == colinfo->rightattnos[i]); /* Use the already-assigned name of this column */ colinfo->new_colnames[j] = colinfo->colnames[i]; i++; } else { /* * Unique-ify the new child column name and assign, unless we're * in an unnamed join, in which case just copy */ if (rte->alias != NULL) { colinfo->new_colnames[j] = make_colname_unique(child_colname, dpns, colinfo); if (!changed_any && strcmp(colinfo->new_colnames[j], child_colname) != 0) changed_any = true; } else colinfo->new_colnames[j] = child_colname; } colinfo->is_new_col[j] = rightcolinfo->is_new_col[jc]; j++; } /* Assert we processed the right number of columns */ #ifdef USE_ASSERT_CHECKING while (i < colinfo->num_cols && colinfo->colnames[i] == NULL) i++; Assert(i == colinfo->num_cols); Assert(j == nnewcolumns); #endif /* * For a named join, print column aliases if we changed any from the child * names. Unnamed joins cannot print aliases. */ if (rte->alias != NULL) colinfo->printaliases = changed_any; else colinfo->printaliases = false; } /* * colname_is_unique: is colname distinct from already-chosen column names? * * dpns is query-wide info, colinfo is for the column's RTE */ static bool colname_is_unique(const char *colname, deparse_namespace *dpns, deparse_columns *colinfo) { int i; ListCell *lc; /* Check against already-assigned column aliases within RTE */ for (i = 0; i < colinfo->num_cols; i++) { char *oldname = colinfo->colnames[i]; if (oldname && strcmp(oldname, colname) == 0) return false; } /* * If we're building a new_colnames array, check that too (this will be * partially but not completely redundant with the previous checks) */ for (i = 0; i < colinfo->num_new_cols; i++) { char *oldname = colinfo->new_colnames[i]; if (oldname && strcmp(oldname, colname) == 0) return false; } /* Also check against USING-column names that must be globally unique */ foreach(lc, dpns->using_names) { char *oldname = (char *) lfirst(lc); if (strcmp(oldname, colname) == 0) return false; } /* Also check against names already assigned for parent-join USING cols */ foreach(lc, colinfo->parentUsing) { char *oldname = (char *) lfirst(lc); if (strcmp(oldname, colname) == 0) return false; } return true; } /* * make_colname_unique: modify colname if necessary to make it unique * * dpns is query-wide info, colinfo is for the column's RTE */ static char * make_colname_unique(char *colname, deparse_namespace *dpns, deparse_columns *colinfo) { /* * If the selected name isn't unique, append digits to make it so. For a * very long input name, we might have to truncate to stay within * NAMEDATALEN. */ if (!colname_is_unique(colname, dpns, colinfo)) { int colnamelen = strlen(colname); char *modname = (char *) palloc(colnamelen + 16); int i = 0; do { i++; for (;;) { memcpy(modname, colname, colnamelen); sprintf(modname + colnamelen, "_%d", i); if (strlen(modname) < NAMEDATALEN) break; /* drop chars from colname to keep all the digits */ colnamelen = pg_mbcliplen(colname, colnamelen, colnamelen - 1); } } while (!colname_is_unique(modname, dpns, colinfo)); colname = modname; } return colname; } /* * expand_colnames_array_to: make colinfo->colnames at least n items long * * Any added array entries are initialized to zero. */ static void expand_colnames_array_to(deparse_columns *colinfo, int n) { if (n > colinfo->num_cols) { if (colinfo->colnames == NULL) colinfo->colnames = (char **) palloc0(n * sizeof(char *)); else { colinfo->colnames = (char **) repalloc(colinfo->colnames, n * sizeof(char *)); memset(colinfo->colnames + colinfo->num_cols, 0, (n - colinfo->num_cols) * sizeof(char *)); } colinfo->num_cols = n; } } /* * identify_join_columns: figure out where columns of a join come from * * Fills the join-specific fields of the colinfo struct, except for * usingNames which is filled later. */ static void identify_join_columns(JoinExpr *j, RangeTblEntry *jrte, deparse_columns *colinfo) { int numjoincols; int jcolno; int rcolno; ListCell *lc; /* Extract left/right child RT indexes */ if (IsA(j->larg, RangeTblRef)) colinfo->leftrti = ((RangeTblRef *) j->larg)->rtindex; else if (IsA(j->larg, JoinExpr)) colinfo->leftrti = ((JoinExpr *) j->larg)->rtindex; else elog(ERROR, "unrecognized node type in jointree: %d", (int) nodeTag(j->larg)); if (IsA(j->rarg, RangeTblRef)) colinfo->rightrti = ((RangeTblRef *) j->rarg)->rtindex; else if (IsA(j->rarg, JoinExpr)) colinfo->rightrti = ((JoinExpr *) j->rarg)->rtindex; else elog(ERROR, "unrecognized node type in jointree: %d", (int) nodeTag(j->rarg)); /* Assert children will be processed earlier than join in second pass */ Assert(colinfo->leftrti < j->rtindex); Assert(colinfo->rightrti < j->rtindex); /* Initialize result arrays with zeroes */ numjoincols = list_length(jrte->joinaliasvars); Assert(numjoincols == list_length(jrte->eref->colnames)); colinfo->leftattnos = (int *) palloc0(numjoincols * sizeof(int)); colinfo->rightattnos = (int *) palloc0(numjoincols * sizeof(int)); /* * Deconstruct RTE's joinleftcols/joinrightcols into desired format. * Recall that the column(s) merged due to USING are the first column(s) * of the join output. We need not do anything special while scanning * joinleftcols, but while scanning joinrightcols we must distinguish * merged from unmerged columns. */ jcolno = 0; foreach(lc, jrte->joinleftcols) { int leftattno = lfirst_int(lc); colinfo->leftattnos[jcolno++] = leftattno; } rcolno = 0; foreach(lc, jrte->joinrightcols) { int rightattno = lfirst_int(lc); if (rcolno < jrte->joinmergedcols) /* merged column? */ colinfo->rightattnos[rcolno] = rightattno; else colinfo->rightattnos[jcolno++] = rightattno; rcolno++; } Assert(jcolno == numjoincols); } /* * get_rtable_name: convenience function to get a previously assigned RTE alias * * The RTE must belong to the topmost namespace level in "context". */ static char * get_rtable_name(int rtindex, deparse_context *context) { deparse_namespace *dpns = (deparse_namespace *) linitial(context->namespaces); Assert(rtindex > 0 && rtindex <= list_length(dpns->rtable_names)); return (char *) list_nth(dpns->rtable_names, rtindex - 1); } /* * set_deparse_plan: set up deparse_namespace to parse subexpressions * of a given Plan node * * This sets the plan, outer_plan, inner_plan, outer_tlist, inner_tlist, * and index_tlist fields. Caller must already have adjusted the ancestors * list if necessary. Note that the rtable, subplans, and ctes fields do * not need to change when shifting attention to different plan nodes in a * single plan tree. */ static void set_deparse_plan(deparse_namespace *dpns, Plan *plan) { dpns->plan = plan; /* * We special-case Append and MergeAppend to pretend that the first child * plan is the OUTER referent; we have to interpret OUTER Vars in their * tlists according to one of the children, and the first one is the most * natural choice. */ if (IsA(plan, Append)) dpns->outer_plan = linitial(((Append *) plan)->appendplans); else if (IsA(plan, MergeAppend)) dpns->outer_plan = linitial(((MergeAppend *) plan)->mergeplans); else dpns->outer_plan = outerPlan(plan); if (dpns->outer_plan) dpns->outer_tlist = dpns->outer_plan->targetlist; else dpns->outer_tlist = NIL; /* * For a SubqueryScan, pretend the subplan is INNER referent. (We don't * use OUTER because that could someday conflict with the normal meaning.) * Likewise, for a CteScan, pretend the subquery's plan is INNER referent. * For a WorkTableScan, locate the parent RecursiveUnion plan node and use * that as INNER referent. * * For ON CONFLICT .. UPDATE we just need the inner tlist to point to the * excluded expression's tlist. (Similar to the SubqueryScan we don't want * to reuse OUTER, it's used for RETURNING in some modify table cases, * although not INSERT .. CONFLICT). */ if (IsA(plan, SubqueryScan)) dpns->inner_plan = ((SubqueryScan *) plan)->subplan; else if (IsA(plan, CteScan)) dpns->inner_plan = list_nth(dpns->subplans, ((CteScan *) plan)->ctePlanId - 1); else if (IsA(plan, WorkTableScan)) dpns->inner_plan = find_recursive_union(dpns, (WorkTableScan *) plan); else if (IsA(plan, ModifyTable)) dpns->inner_plan = plan; else dpns->inner_plan = innerPlan(plan); if (IsA(plan, ModifyTable)) dpns->inner_tlist = ((ModifyTable *) plan)->exclRelTlist; else if (dpns->inner_plan) dpns->inner_tlist = dpns->inner_plan->targetlist; else dpns->inner_tlist = NIL; /* Set up referent for INDEX_VAR Vars, if needed */ if (IsA(plan, IndexOnlyScan)) dpns->index_tlist = ((IndexOnlyScan *) plan)->indextlist; else if (IsA(plan, ForeignScan)) dpns->index_tlist = ((ForeignScan *) plan)->fdw_scan_tlist; else if (IsA(plan, CustomScan)) dpns->index_tlist = ((CustomScan *) plan)->custom_scan_tlist; else dpns->index_tlist = NIL; } /* * Locate the ancestor plan node that is the RecursiveUnion generating * the WorkTableScan's work table. We can match on wtParam, since that * should be unique within the plan tree. */ static Plan * find_recursive_union(deparse_namespace *dpns, WorkTableScan *wtscan) { ListCell *lc; foreach(lc, dpns->ancestors) { Plan *ancestor = (Plan *) lfirst(lc); if (IsA(ancestor, RecursiveUnion) && ((RecursiveUnion *) ancestor)->wtParam == wtscan->wtParam) return ancestor; } elog(ERROR, "could not find RecursiveUnion for WorkTableScan with wtParam %d", wtscan->wtParam); return NULL; } /* * push_child_plan: temporarily transfer deparsing attention to a child plan * * When expanding an OUTER_VAR or INNER_VAR reference, we must adjust the * deparse context in case the referenced expression itself uses * OUTER_VAR/INNER_VAR. We modify the top stack entry in-place to avoid * affecting levelsup issues (although in a Plan tree there really shouldn't * be any). * * Caller must provide a local deparse_namespace variable to save the * previous state for pop_child_plan. */ static void push_child_plan(deparse_namespace *dpns, Plan *plan, deparse_namespace *save_dpns) { /* Save state for restoration later */ *save_dpns = *dpns; /* Link current plan node into ancestors list */ dpns->ancestors = lcons(dpns->plan, dpns->ancestors); /* Set attention on selected child */ set_deparse_plan(dpns, plan); } /* * pop_child_plan: undo the effects of push_child_plan */ static void pop_child_plan(deparse_namespace *dpns, deparse_namespace *save_dpns) { List *ancestors; /* Get rid of ancestors list cell added by push_child_plan */ ancestors = list_delete_first(dpns->ancestors); /* Restore fields changed by push_child_plan */ *dpns = *save_dpns; /* Make sure dpns->ancestors is right (may be unnecessary) */ dpns->ancestors = ancestors; } /* * push_ancestor_plan: temporarily transfer deparsing attention to an * ancestor plan * * When expanding a Param reference, we must adjust the deparse context * to match the plan node that contains the expression being printed; * otherwise we'd fail if that expression itself contains a Param or * OUTER_VAR/INNER_VAR/INDEX_VAR variable. * * The target ancestor is conveniently identified by the ListCell holding it * in dpns->ancestors. * * Caller must provide a local deparse_namespace variable to save the * previous state for pop_ancestor_plan. */ static void push_ancestor_plan(deparse_namespace *dpns, ListCell *ancestor_cell, deparse_namespace *save_dpns) { Plan *plan = (Plan *) lfirst(ancestor_cell); /* Save state for restoration later */ *save_dpns = *dpns; /* Build a new ancestor list with just this node's ancestors */ dpns->ancestors = list_copy_tail(dpns->ancestors, list_cell_number(dpns->ancestors, ancestor_cell) + 1); /* Set attention on selected ancestor */ set_deparse_plan(dpns, plan); } /* * pop_ancestor_plan: undo the effects of push_ancestor_plan */ static void pop_ancestor_plan(deparse_namespace *dpns, deparse_namespace *save_dpns) { /* Free the ancestor list made in push_ancestor_plan */ list_free(dpns->ancestors); /* Restore fields changed by push_ancestor_plan */ *dpns = *save_dpns; } /* ---------- * get_query_def - Parse back one query parsetree * * query: parsetree to be displayed * buf: output text is appended to buf * parentnamespace: list (initially empty) of outer-level deparse_namespace's * resultDesc: if not NULL, the output tuple descriptor for the view * represented by a SELECT query. We use the column names from it * to label SELECT output columns, in preference to names in the query * colNamesVisible: true if the surrounding context cares about the output * column names at all (as, for example, an EXISTS() context does not); * when false, we can suppress dummy column labels such as "?column?" * prettyFlags: bitmask of PRETTYFLAG_XXX options * wrapColumn: maximum line length, or -1 to disable wrapping * startIndent: initial indentation amount * ---------- */ static void get_query_def(Query *query, StringInfo buf, List *parentnamespace, TupleDesc resultDesc, bool colNamesVisible, int prettyFlags, int wrapColumn, int startIndent) { deparse_context context; deparse_namespace dpns; /* Guard against excessively long or deeply-nested queries */ CHECK_FOR_INTERRUPTS(); check_stack_depth(); /* * Before we begin to examine the query, acquire locks on referenced * relations, and fix up deleted columns in JOIN RTEs. This ensures * consistent results. Note we assume it's OK to scribble on the passed * querytree! * * We are only deparsing the query (we are not about to execute it), so we * only need AccessShareLock on the relations it mentions. */ AcquireRewriteLocks(query, false, false); context.buf = buf; context.namespaces = lcons(&dpns, list_copy(parentnamespace)); context.windowClause = NIL; context.windowTList = NIL; context.varprefix = (parentnamespace != NIL || list_length(query->rtable) != 1); context.prettyFlags = prettyFlags; context.wrapColumn = wrapColumn; context.indentLevel = startIndent; context.special_exprkind = EXPR_KIND_NONE; context.appendparents = NULL; set_deparse_for_query(&dpns, query, parentnamespace); switch (query->commandType) { case CMD_SELECT: get_select_query_def(query, &context, resultDesc, colNamesVisible); break; case CMD_UPDATE: get_update_query_def(query, &context, colNamesVisible); break; case CMD_INSERT: get_insert_query_def(query, &context, colNamesVisible); break; case CMD_DELETE: get_delete_query_def(query, &context, colNamesVisible); break; case CMD_NOTHING: appendStringInfoString(buf, "NOTHING"); break; case CMD_UTILITY: get_utility_query_def(query, &context); break; default: elog(ERROR, "unrecognized query command type: %d", query->commandType); break; } } /* ---------- * get_values_def - Parse back a VALUES list * ---------- */ static void get_values_def(List *values_lists, deparse_context *context) { StringInfo buf = context->buf; bool first_list = true; ListCell *vtl; appendStringInfoString(buf, "VALUES "); foreach(vtl, values_lists) { List *sublist = (List *) lfirst(vtl); bool first_col = true; ListCell *lc; if (first_list) first_list = false; else appendStringInfoString(buf, ", "); appendStringInfoChar(buf, '('); foreach(lc, sublist) { Node *col = (Node *) lfirst(lc); if (first_col) first_col = false; else appendStringInfoChar(buf, ','); /* * Print the value. Whole-row Vars need special treatment. */ get_rule_expr_toplevel(col, context, false); } appendStringInfoChar(buf, ')'); } } /* ---------- * get_with_clause - Parse back a WITH clause * ---------- */ static void get_with_clause(Query *query, deparse_context *context) { StringInfo buf = context->buf; const char *sep; ListCell *l; if (query->cteList == NIL) return; if (PRETTY_INDENT(context)) { context->indentLevel += PRETTYINDENT_STD; appendStringInfoChar(buf, ' '); } if (query->hasRecursive) sep = "WITH RECURSIVE "; else sep = "WITH "; foreach(l, query->cteList) { CommonTableExpr *cte = (CommonTableExpr *) lfirst(l); appendStringInfoString(buf, sep); appendStringInfoString(buf, quote_identifier(cte->ctename)); if (cte->aliascolnames) { bool first = true; ListCell *col; appendStringInfoChar(buf, '('); foreach(col, cte->aliascolnames) { if (first) first = false; else appendStringInfoString(buf, ", "); appendStringInfoString(buf, quote_identifier(strVal(lfirst(col)))); } appendStringInfoChar(buf, ')'); } appendStringInfoString(buf, " AS "); switch (cte->ctematerialized) { case CTEMaterializeDefault: break; case CTEMaterializeAlways: appendStringInfoString(buf, "MATERIALIZED "); break; case CTEMaterializeNever: appendStringInfoString(buf, "NOT MATERIALIZED "); break; } appendStringInfoChar(buf, '('); if (PRETTY_INDENT(context)) appendContextKeyword(context, "", 0, 0, 0); get_query_def((Query *) cte->ctequery, buf, context->namespaces, NULL, true, context->prettyFlags, context->wrapColumn, context->indentLevel); if (PRETTY_INDENT(context)) appendContextKeyword(context, "", 0, 0, 0); appendStringInfoChar(buf, ')'); if (cte->search_clause) { bool first = true; ListCell *lc; appendStringInfo(buf, " SEARCH %s FIRST BY ", cte->search_clause->search_breadth_first ? "BREADTH" : "DEPTH"); foreach(lc, cte->search_clause->search_col_list) { if (first) first = false; else appendStringInfoString(buf, ", "); appendStringInfoString(buf, quote_identifier(strVal(lfirst(lc)))); } appendStringInfo(buf, " SET %s", quote_identifier(cte->search_clause->search_seq_column)); } if (cte->cycle_clause) { bool first = true; ListCell *lc; appendStringInfoString(buf, " CYCLE "); foreach(lc, cte->cycle_clause->cycle_col_list) { if (first) first = false; else appendStringInfoString(buf, ", "); appendStringInfoString(buf, quote_identifier(strVal(lfirst(lc)))); } appendStringInfo(buf, " SET %s", quote_identifier(cte->cycle_clause->cycle_mark_column)); { Const *cmv = castNode(Const, cte->cycle_clause->cycle_mark_value); Const *cmd = castNode(Const, cte->cycle_clause->cycle_mark_default); if (!(cmv->consttype == BOOLOID && !cmv->constisnull && DatumGetBool(cmv->constvalue) == true && cmd->consttype == BOOLOID && !cmd->constisnull && DatumGetBool(cmd->constvalue) == false)) { appendStringInfoString(buf, " TO "); get_rule_expr(cte->cycle_clause->cycle_mark_value, context, false); appendStringInfoString(buf, " DEFAULT "); get_rule_expr(cte->cycle_clause->cycle_mark_default, context, false); } } appendStringInfo(buf, " USING %s", quote_identifier(cte->cycle_clause->cycle_path_column)); } sep = ", "; } if (PRETTY_INDENT(context)) { context->indentLevel -= PRETTYINDENT_STD; appendContextKeyword(context, "", 0, 0, 0); } else appendStringInfoChar(buf, ' '); } /* ---------- * get_select_query_def - Parse back a SELECT parsetree * ---------- */ static void get_select_query_def(Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible) { StringInfo buf = context->buf; List *save_windowclause; List *save_windowtlist; bool force_colno; ListCell *l; /* Insert the WITH clause if given */ get_with_clause(query, context); /* Set up context for possible window functions */ save_windowclause = context->windowClause; context->windowClause = query->windowClause; save_windowtlist = context->windowTList; context->windowTList = query->targetList; /* * If the Query node has a setOperations tree, then it's the top level of * a UNION/INTERSECT/EXCEPT query; only the WITH, ORDER BY and LIMIT * fields are interesting in the top query itself. */ if (query->setOperations) { get_setop_query(query->setOperations, query, context, resultDesc, colNamesVisible); /* ORDER BY clauses must be simple in this case */ force_colno = true; } else { get_basic_select_query(query, context, resultDesc, colNamesVisible); force_colno = false; } /* Add the ORDER BY clause if given */ if (query->sortClause != NIL) { appendContextKeyword(context, " ORDER BY ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_orderby(query->sortClause, query->targetList, force_colno, context); } /* * Add the LIMIT/OFFSET clauses if given. If non-default options, use the * standard spelling of LIMIT. */ if (query->limitOffset != NULL) { appendContextKeyword(context, " OFFSET ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); get_rule_expr(query->limitOffset, context, false); } if (query->limitCount != NULL) { if (query->limitOption == LIMIT_OPTION_WITH_TIES) { appendContextKeyword(context, " FETCH FIRST ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); get_rule_expr(query->limitCount, context, false); appendStringInfoString(buf, " ROWS WITH TIES"); } else { appendContextKeyword(context, " LIMIT ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); if (IsA(query->limitCount, Const) && ((Const *) query->limitCount)->constisnull) appendStringInfoString(buf, "ALL"); else get_rule_expr(query->limitCount, context, false); } } /* Add FOR [KEY] UPDATE/SHARE clauses if present */ if (query->hasForUpdate) { foreach(l, query->rowMarks) { RowMarkClause *rc = (RowMarkClause *) lfirst(l); /* don't print implicit clauses */ if (rc->pushedDown) continue; switch (rc->strength) { case LCS_NONE: /* we intentionally throw an error for LCS_NONE */ elog(ERROR, "unrecognized LockClauseStrength %d", (int) rc->strength); break; case LCS_FORKEYSHARE: appendContextKeyword(context, " FOR KEY SHARE", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); break; case LCS_FORSHARE: appendContextKeyword(context, " FOR SHARE", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); break; case LCS_FORNOKEYUPDATE: appendContextKeyword(context, " FOR NO KEY UPDATE", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); break; case LCS_FORUPDATE: appendContextKeyword(context, " FOR UPDATE", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); break; } appendStringInfo(buf, " OF %s", quote_identifier(get_rtable_name(rc->rti, context))); if (rc->waitPolicy == LockWaitError) appendStringInfoString(buf, " NOWAIT"); else if (rc->waitPolicy == LockWaitSkip) appendStringInfoString(buf, " SKIP LOCKED"); } } context->windowClause = save_windowclause; context->windowTList = save_windowtlist; } /* * Detect whether query looks like SELECT ... FROM VALUES(), * with no need to rename the output columns of the VALUES RTE. * If so, return the VALUES RTE. Otherwise return NULL. */ static RangeTblEntry * get_simple_values_rte(Query *query, TupleDesc resultDesc) { RangeTblEntry *result = NULL; ListCell *lc; /* * We want to detect a match even if the Query also contains OLD or NEW * rule RTEs. So the idea is to scan the rtable and see if there is only * one inFromCl RTE that is a VALUES RTE. */ foreach(lc, query->rtable) { RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); if (rte->rtekind == RTE_VALUES && rte->inFromCl) { if (result) return NULL; /* multiple VALUES (probably not possible) */ result = rte; } else if (rte->rtekind == RTE_RELATION && !rte->inFromCl) continue; /* ignore rule entries */ else return NULL; /* something else -> not simple VALUES */ } /* * We don't need to check the targetlist in any great detail, because * parser/analyze.c will never generate a "bare" VALUES RTE --- they only * appear inside auto-generated sub-queries with very restricted * structure. However, DefineView might have modified the tlist by * injecting new column aliases, or we might have some other column * aliases forced by a resultDesc. We can only simplify if the RTE's * column names match the names that get_target_list() would select. */ if (result) { ListCell *lcn; int colno; if (list_length(query->targetList) != list_length(result->eref->colnames)) return NULL; /* this probably cannot happen */ colno = 0; forboth(lc, query->targetList, lcn, result->eref->colnames) { TargetEntry *tle = (TargetEntry *) lfirst(lc); char *cname = strVal(lfirst(lcn)); char *colname; if (tle->resjunk) return NULL; /* this probably cannot happen */ /* compute name that get_target_list would use for column */ colno++; if (resultDesc && colno <= resultDesc->natts) colname = NameStr(TupleDescAttr(resultDesc, colno - 1)->attname); else colname = tle->resname; /* does it match the VALUES RTE? */ if (colname == NULL || strcmp(colname, cname) != 0) return NULL; /* column name has been changed */ } } return result; } static void get_basic_select_query(Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible) { StringInfo buf = context->buf; RangeTblEntry *values_rte; char *sep; ListCell *l; if (PRETTY_INDENT(context)) { context->indentLevel += PRETTYINDENT_STD; appendStringInfoChar(buf, ' '); } /* * If the query looks like SELECT * FROM (VALUES ...), then print just the * VALUES part. This reverses what transformValuesClause() did at parse * time. */ values_rte = get_simple_values_rte(query, resultDesc); if (values_rte) { get_values_def(values_rte->values_lists, context); return; } /* * Build up the query string - first we say SELECT */ if (query->isReturn) appendStringInfoString(buf, "RETURN"); else appendStringInfoString(buf, "SELECT"); /* Add the DISTINCT clause if given */ if (query->distinctClause != NIL) { if (query->hasDistinctOn) { appendStringInfoString(buf, " DISTINCT ON ("); sep = ""; foreach(l, query->distinctClause) { SortGroupClause *srt = (SortGroupClause *) lfirst(l); appendStringInfoString(buf, sep); get_rule_sortgroupclause(srt->tleSortGroupRef, query->targetList, false, context); sep = ", "; } appendStringInfoChar(buf, ')'); } else appendStringInfoString(buf, " DISTINCT"); } /* Then we tell what to select (the targetlist) */ get_target_list(query->targetList, context, resultDesc, colNamesVisible); /* Add the FROM clause if needed */ get_from_clause(query, " FROM ", context); /* Add the WHERE clause if given */ if (query->jointree->quals != NULL) { appendContextKeyword(context, " WHERE ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_expr(query->jointree->quals, context, false); } /* Add the GROUP BY clause if given */ if (query->groupClause != NULL || query->groupingSets != NULL) { ParseExprKind save_exprkind; appendContextKeyword(context, " GROUP BY ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); if (query->groupDistinct) appendStringInfoString(buf, "DISTINCT "); save_exprkind = context->special_exprkind; context->special_exprkind = EXPR_KIND_GROUP_BY; if (query->groupingSets == NIL) { sep = ""; foreach(l, query->groupClause) { SortGroupClause *grp = (SortGroupClause *) lfirst(l); appendStringInfoString(buf, sep); get_rule_sortgroupclause(grp->tleSortGroupRef, query->targetList, false, context); sep = ", "; } } else { sep = ""; foreach(l, query->groupingSets) { GroupingSet *grp = lfirst(l); appendStringInfoString(buf, sep); get_rule_groupingset(grp, query->targetList, true, context); sep = ", "; } } context->special_exprkind = save_exprkind; } /* Add the HAVING clause if given */ if (query->havingQual != NULL) { appendContextKeyword(context, " HAVING ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); get_rule_expr(query->havingQual, context, false); } /* Add the WINDOW clause if needed */ if (query->windowClause != NIL) get_rule_windowclause(query, context); } /* ---------- * get_target_list - Parse back a SELECT target list * * This is also used for RETURNING lists in INSERT/UPDATE/DELETE. * * resultDesc and colNamesVisible are as for get_query_def() * ---------- */ static void get_target_list(List *targetList, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible) { StringInfo buf = context->buf; StringInfoData targetbuf; bool last_was_multiline = false; char *sep; int colno; ListCell *l; /* we use targetbuf to hold each TLE's text temporarily */ initStringInfo(&targetbuf); sep = " "; colno = 0; foreach(l, targetList) { TargetEntry *tle = (TargetEntry *) lfirst(l); char *colname; char *attname; if (tle->resjunk) continue; /* ignore junk entries */ appendStringInfoString(buf, sep); sep = ", "; colno++; /* * Put the new field text into targetbuf so we can decide after we've * got it whether or not it needs to go on a new line. */ resetStringInfo(&targetbuf); context->buf = &targetbuf; /* * We special-case Var nodes rather than using get_rule_expr. This is * needed because get_rule_expr will display a whole-row Var as * "foo.*", which is the preferred notation in most contexts, but at * the top level of a SELECT list it's not right (the parser will * expand that notation into multiple columns, yielding behavior * different from a whole-row Var). We need to call get_variable * directly so that we can tell it to do the right thing, and so that * we can get the attribute name which is the default AS label. */ if (tle->expr && (IsA(tle->expr, Var))) { attname = get_variable((Var *) tle->expr, 0, true, context); } else { get_rule_expr((Node *) tle->expr, context, true); /* * When colNamesVisible is true, we should always show the * assigned column name explicitly. Otherwise, show it only if * it's not FigureColname's fallback. */ attname = colNamesVisible ? NULL : "?column?"; } /* * Figure out what the result column should be called. In the context * of a view, use the view's tuple descriptor (so as to pick up the * effects of any column RENAME that's been done on the view). * Otherwise, just use what we can find in the TLE. */ if (resultDesc && colno <= resultDesc->natts) colname = NameStr(TupleDescAttr(resultDesc, colno - 1)->attname); else colname = tle->resname; /* Show AS unless the column's name is correct as-is */ if (colname) /* resname could be NULL */ { if (attname == NULL || strcmp(attname, colname) != 0) appendStringInfo(&targetbuf, " AS %s", quote_identifier(colname)); } /* Restore context's output buffer */ context->buf = buf; /* Consider line-wrapping if enabled */ if (PRETTY_INDENT(context) && context->wrapColumn >= 0) { int leading_nl_pos; /* Does the new field start with a new line? */ if (targetbuf.len > 0 && targetbuf.data[0] == '\n') leading_nl_pos = 0; else leading_nl_pos = -1; /* If so, we shouldn't add anything */ if (leading_nl_pos >= 0) { /* instead, remove any trailing spaces currently in buf */ removeStringInfoSpaces(buf); } else { char *trailing_nl; /* Locate the start of the current line in the output buffer */ trailing_nl = strrchr(buf->data, '\n'); if (trailing_nl == NULL) trailing_nl = buf->data; else trailing_nl++; /* * Add a newline, plus some indentation, if the new field is * not the first and either the new field would cause an * overflow or the last field used more than one line. */ if (colno > 1 && ((strlen(trailing_nl) + targetbuf.len > context->wrapColumn) || last_was_multiline)) appendContextKeyword(context, "", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_VAR); } /* Remember this field's multiline status for next iteration */ last_was_multiline = (strchr(targetbuf.data + leading_nl_pos + 1, '\n') != NULL); } /* Add the new field */ appendBinaryStringInfo(buf, targetbuf.data, targetbuf.len); } /* clean up */ pfree(targetbuf.data); } static void get_setop_query(Node *setOp, Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible) { StringInfo buf = context->buf; bool need_paren; /* Guard against excessively long or deeply-nested queries */ CHECK_FOR_INTERRUPTS(); check_stack_depth(); if (IsA(setOp, RangeTblRef)) { RangeTblRef *rtr = (RangeTblRef *) setOp; RangeTblEntry *rte = rt_fetch(rtr->rtindex, query->rtable); Query *subquery = rte->subquery; Assert(subquery != NULL); Assert(subquery->setOperations == NULL); /* Need parens if WITH, ORDER BY, FOR UPDATE, or LIMIT; see gram.y */ need_paren = (subquery->cteList || subquery->sortClause || subquery->rowMarks || subquery->limitOffset || subquery->limitCount); if (need_paren) appendStringInfoChar(buf, '('); get_query_def(subquery, buf, context->namespaces, resultDesc, colNamesVisible, context->prettyFlags, context->wrapColumn, context->indentLevel); if (need_paren) appendStringInfoChar(buf, ')'); } else if (IsA(setOp, SetOperationStmt)) { SetOperationStmt *op = (SetOperationStmt *) setOp; int subindent; /* * We force parens when nesting two SetOperationStmts, except when the * lefthand input is another setop of the same kind. Syntactically, * we could omit parens in rather more cases, but it seems best to use * parens to flag cases where the setop operator changes. If we use * parens, we also increase the indentation level for the child query. * * There are some cases in which parens are needed around a leaf query * too, but those are more easily handled at the next level down (see * code above). */ if (IsA(op->larg, SetOperationStmt)) { SetOperationStmt *lop = (SetOperationStmt *) op->larg; if (op->op == lop->op && op->all == lop->all) need_paren = false; else need_paren = true; } else need_paren = false; if (need_paren) { appendStringInfoChar(buf, '('); subindent = PRETTYINDENT_STD; appendContextKeyword(context, "", subindent, 0, 0); } else subindent = 0; get_setop_query(op->larg, query, context, resultDesc, colNamesVisible); if (need_paren) appendContextKeyword(context, ") ", -subindent, 0, 0); else if (PRETTY_INDENT(context)) appendContextKeyword(context, "", -subindent, 0, 0); else appendStringInfoChar(buf, ' '); switch (op->op) { case SETOP_UNION: appendStringInfoString(buf, "UNION "); break; case SETOP_INTERSECT: appendStringInfoString(buf, "INTERSECT "); break; case SETOP_EXCEPT: appendStringInfoString(buf, "EXCEPT "); break; default: elog(ERROR, "unrecognized set op: %d", (int) op->op); } if (op->all) appendStringInfoString(buf, "ALL "); /* Always parenthesize if RHS is another setop */ need_paren = IsA(op->rarg, SetOperationStmt); /* * The indentation code here is deliberately a bit different from that * for the lefthand input, because we want the line breaks in * different places. */ if (need_paren) { appendStringInfoChar(buf, '('); subindent = PRETTYINDENT_STD; } else subindent = 0; appendContextKeyword(context, "", subindent, 0, 0); get_setop_query(op->rarg, query, context, resultDesc, false); if (PRETTY_INDENT(context)) context->indentLevel -= subindent; if (need_paren) appendContextKeyword(context, ")", 0, 0, 0); } else { elog(ERROR, "unrecognized node type: %d", (int) nodeTag(setOp)); } } /* * Display a sort/group clause. * * Also returns the expression tree, so caller need not find it again. */ static Node * get_rule_sortgroupclause(Index ref, List *tlist, bool force_colno, deparse_context *context) { StringInfo buf = context->buf; TargetEntry *tle; Node *expr; tle = get_sortgroupref_tle(ref, tlist); expr = (Node *) tle->expr; /* * Use column-number form if requested by caller. Otherwise, if * expression is a constant, force it to be dumped with an explicit cast * as decoration --- this is because a simple integer constant is * ambiguous (and will be misinterpreted by findTargetlistEntry()) if we * dump it without any decoration. If it's anything more complex than a * simple Var, then force extra parens around it, to ensure it can't be * misinterpreted as a cube() or rollup() construct. */ if (force_colno) { Assert(!tle->resjunk); appendStringInfo(buf, "%d", tle->resno); } else if (expr && IsA(expr, Const)) get_const_expr((Const *) expr, context, 1); else if (!expr || IsA(expr, Var)) get_rule_expr(expr, context, true); else { /* * We must force parens for function-like expressions even if * PRETTY_PAREN is off, since those are the ones in danger of * misparsing. For other expressions we need to force them only if * PRETTY_PAREN is on, since otherwise the expression will output them * itself. (We can't skip the parens.) */ bool need_paren = (PRETTY_PAREN(context) || IsA(expr, FuncExpr) || IsA(expr, Aggref) || IsA(expr, WindowFunc)); if (need_paren) appendStringInfoChar(context->buf, '('); get_rule_expr(expr, context, true); if (need_paren) appendStringInfoChar(context->buf, ')'); } return expr; } /* * Display a GroupingSet */ static void get_rule_groupingset(GroupingSet *gset, List *targetlist, bool omit_parens, deparse_context *context) { ListCell *l; StringInfo buf = context->buf; bool omit_child_parens = true; char *sep = ""; switch (gset->kind) { case GROUPING_SET_EMPTY: appendStringInfoString(buf, "()"); return; case GROUPING_SET_SIMPLE: { if (!omit_parens || list_length(gset->content) != 1) appendStringInfoChar(buf, '('); foreach(l, gset->content) { Index ref = lfirst_int(l); appendStringInfoString(buf, sep); get_rule_sortgroupclause(ref, targetlist, false, context); sep = ", "; } if (!omit_parens || list_length(gset->content) != 1) appendStringInfoChar(buf, ')'); } return; case GROUPING_SET_ROLLUP: appendStringInfoString(buf, "ROLLUP("); break; case GROUPING_SET_CUBE: appendStringInfoString(buf, "CUBE("); break; case GROUPING_SET_SETS: appendStringInfoString(buf, "GROUPING SETS ("); omit_child_parens = false; break; } foreach(l, gset->content) { appendStringInfoString(buf, sep); get_rule_groupingset(lfirst(l), targetlist, omit_child_parens, context); sep = ", "; } appendStringInfoChar(buf, ')'); } /* * Display an ORDER BY list. */ static void get_rule_orderby(List *orderList, List *targetList, bool force_colno, deparse_context *context) { StringInfo buf = context->buf; const char *sep; ListCell *l; sep = ""; foreach(l, orderList) { SortGroupClause *srt = (SortGroupClause *) lfirst(l); Node *sortexpr; Oid sortcoltype; TypeCacheEntry *typentry; appendStringInfoString(buf, sep); sortexpr = get_rule_sortgroupclause(srt->tleSortGroupRef, targetList, force_colno, context); sortcoltype = exprType(sortexpr); /* See whether operator is default < or > for datatype */ typentry = lookup_type_cache(sortcoltype, TYPECACHE_LT_OPR | TYPECACHE_GT_OPR); if (srt->sortop == typentry->lt_opr) { /* ASC is default, so emit nothing for it */ if (srt->nulls_first) appendStringInfoString(buf, " NULLS FIRST"); } else if (srt->sortop == typentry->gt_opr) { appendStringInfoString(buf, " DESC"); /* DESC defaults to NULLS FIRST */ if (!srt->nulls_first) appendStringInfoString(buf, " NULLS LAST"); } else { appendStringInfo(buf, " USING %s", generate_operator_name(srt->sortop, sortcoltype, sortcoltype)); /* be specific to eliminate ambiguity */ if (srt->nulls_first) appendStringInfoString(buf, " NULLS FIRST"); else appendStringInfoString(buf, " NULLS LAST"); } sep = ", "; } } /* * Display a WINDOW clause. * * Note that the windowClause list might contain only anonymous window * specifications, in which case we should print nothing here. */ static void get_rule_windowclause(Query *query, deparse_context *context) { StringInfo buf = context->buf; const char *sep; ListCell *l; sep = NULL; foreach(l, query->windowClause) { WindowClause *wc = (WindowClause *) lfirst(l); if (wc->name == NULL) continue; /* ignore anonymous windows */ if (sep == NULL) appendContextKeyword(context, " WINDOW ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); else appendStringInfoString(buf, sep); appendStringInfo(buf, "%s AS ", quote_identifier(wc->name)); get_rule_windowspec(wc, query->targetList, context); sep = ", "; } } /* * Display a window definition */ static void get_rule_windowspec(WindowClause *wc, List *targetList, deparse_context *context) { StringInfo buf = context->buf; bool needspace = false; const char *sep; ListCell *l; appendStringInfoChar(buf, '('); if (wc->refname) { appendStringInfoString(buf, quote_identifier(wc->refname)); needspace = true; } /* partition clauses are always inherited, so only print if no refname */ if (wc->partitionClause && !wc->refname) { if (needspace) appendStringInfoChar(buf, ' '); appendStringInfoString(buf, "PARTITION BY "); sep = ""; foreach(l, wc->partitionClause) { SortGroupClause *grp = (SortGroupClause *) lfirst(l); appendStringInfoString(buf, sep); get_rule_sortgroupclause(grp->tleSortGroupRef, targetList, false, context); sep = ", "; } needspace = true; } /* print ordering clause only if not inherited */ if (wc->orderClause && !wc->copiedOrder) { if (needspace) appendStringInfoChar(buf, ' '); appendStringInfoString(buf, "ORDER BY "); get_rule_orderby(wc->orderClause, targetList, false, context); needspace = true; } /* framing clause is never inherited, so print unless it's default */ if (wc->frameOptions & FRAMEOPTION_NONDEFAULT) { if (needspace) appendStringInfoChar(buf, ' '); if (wc->frameOptions & FRAMEOPTION_RANGE) appendStringInfoString(buf, "RANGE "); else if (wc->frameOptions & FRAMEOPTION_ROWS) appendStringInfoString(buf, "ROWS "); else if (wc->frameOptions & FRAMEOPTION_GROUPS) appendStringInfoString(buf, "GROUPS "); else Assert(false); if (wc->frameOptions & FRAMEOPTION_BETWEEN) appendStringInfoString(buf, "BETWEEN "); if (wc->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING) appendStringInfoString(buf, "UNBOUNDED PRECEDING "); else if (wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) appendStringInfoString(buf, "CURRENT ROW "); else if (wc->frameOptions & FRAMEOPTION_START_OFFSET) { get_rule_expr(wc->startOffset, context, false); if (wc->frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING) appendStringInfoString(buf, " PRECEDING "); else if (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING) appendStringInfoString(buf, " FOLLOWING "); else Assert(false); } else Assert(false); if (wc->frameOptions & FRAMEOPTION_BETWEEN) { appendStringInfoString(buf, "AND "); if (wc->frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING) appendStringInfoString(buf, "UNBOUNDED FOLLOWING "); else if (wc->frameOptions & FRAMEOPTION_END_CURRENT_ROW) appendStringInfoString(buf, "CURRENT ROW "); else if (wc->frameOptions & FRAMEOPTION_END_OFFSET) { get_rule_expr(wc->endOffset, context, false); if (wc->frameOptions & FRAMEOPTION_END_OFFSET_PRECEDING) appendStringInfoString(buf, " PRECEDING "); else if (wc->frameOptions & FRAMEOPTION_END_OFFSET_FOLLOWING) appendStringInfoString(buf, " FOLLOWING "); else Assert(false); } else Assert(false); } if (wc->frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW) appendStringInfoString(buf, "EXCLUDE CURRENT ROW "); else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_GROUP) appendStringInfoString(buf, "EXCLUDE GROUP "); else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES) appendStringInfoString(buf, "EXCLUDE TIES "); /* we will now have a trailing space; remove it */ buf->len--; } appendStringInfoChar(buf, ')'); } /* ---------- * get_insert_query_def - Parse back an INSERT parsetree * ---------- */ static void get_insert_query_def(Query *query, deparse_context *context, bool colNamesVisible) { StringInfo buf = context->buf; RangeTblEntry *select_rte = NULL; RangeTblEntry *values_rte = NULL; RangeTblEntry *rte; char *sep; ListCell *l; List *strippedexprs; /* Insert the WITH clause if given */ get_with_clause(query, context); /* * If it's an INSERT ... SELECT or multi-row VALUES, there will be a * single RTE for the SELECT or VALUES. Plain VALUES has neither. */ foreach(l, query->rtable) { rte = (RangeTblEntry *) lfirst(l); if (rte->rtekind == RTE_SUBQUERY) { if (select_rte) elog(ERROR, "too many subquery RTEs in INSERT"); select_rte = rte; } if (rte->rtekind == RTE_VALUES) { if (values_rte) elog(ERROR, "too many values RTEs in INSERT"); values_rte = rte; } } if (select_rte && values_rte) elog(ERROR, "both subquery and values RTEs in INSERT"); /* * Start the query with INSERT INTO relname */ rte = rt_fetch(query->resultRelation, query->rtable); Assert(rte->rtekind == RTE_RELATION); if (PRETTY_INDENT(context)) { context->indentLevel += PRETTYINDENT_STD; appendStringInfoChar(buf, ' '); } appendStringInfo(buf, "INSERT INTO %s ", generate_relation_name(rte->relid, NIL)); /* INSERT requires AS keyword for target alias */ if (rte->alias != NULL) appendStringInfo(buf, "AS %s ", quote_identifier(rte->alias->aliasname)); /* * Add the insert-column-names list. Any indirection decoration needed on * the column names can be inferred from the top targetlist. */ strippedexprs = NIL; sep = ""; if (query->targetList) appendStringInfoChar(buf, '('); foreach(l, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(l); if (tle->resjunk) continue; /* ignore junk entries */ appendStringInfoString(buf, sep); sep = ", "; /* * Put out name of target column; look in the catalogs, not at * tle->resname, since resname will fail to track RENAME. */ appendStringInfoString(buf, quote_identifier(get_attname(rte->relid, tle->resno, false))); /* * Print any indirection needed (subfields or subscripts), and strip * off the top-level nodes representing the indirection assignments. * Add the stripped expressions to strippedexprs. (If it's a * single-VALUES statement, the stripped expressions are the VALUES to * print below. Otherwise they're just Vars and not really * interesting.) */ strippedexprs = lappend(strippedexprs, processIndirection((Node *) tle->expr, context)); } if (query->targetList) appendStringInfoString(buf, ") "); if (query->override) { if (query->override == OVERRIDING_SYSTEM_VALUE) appendStringInfoString(buf, "OVERRIDING SYSTEM VALUE "); else if (query->override == OVERRIDING_USER_VALUE) appendStringInfoString(buf, "OVERRIDING USER VALUE "); } if (select_rte) { /* Add the SELECT */ get_query_def(select_rte->subquery, buf, context->namespaces, NULL, false, context->prettyFlags, context->wrapColumn, context->indentLevel); } else if (values_rte) { /* Add the multi-VALUES expression lists */ get_values_def(values_rte->values_lists, context); } else if (strippedexprs) { /* Add the single-VALUES expression list */ appendContextKeyword(context, "VALUES (", -PRETTYINDENT_STD, PRETTYINDENT_STD, 2); get_rule_list_toplevel(strippedexprs, context, false); appendStringInfoChar(buf, ')'); } else { /* No expressions, so it must be DEFAULT VALUES */ appendStringInfoString(buf, "DEFAULT VALUES"); } /* Add ON CONFLICT if present */ if (query->onConflict) { OnConflictExpr *confl = query->onConflict; appendStringInfoString(buf, " ON CONFLICT"); if (confl->arbiterElems) { /* Add the single-VALUES expression list */ appendStringInfoChar(buf, '('); get_rule_expr((Node *) confl->arbiterElems, context, false); appendStringInfoChar(buf, ')'); /* Add a WHERE clause (for partial indexes) if given */ if (confl->arbiterWhere != NULL) { bool save_varprefix; /* * Force non-prefixing of Vars, since parser assumes that they * belong to target relation. WHERE clause does not use * InferenceElem, so this is separately required. */ save_varprefix = context->varprefix; context->varprefix = false; appendContextKeyword(context, " WHERE ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_expr(confl->arbiterWhere, context, false); context->varprefix = save_varprefix; } } else if (OidIsValid(confl->constraint)) { char *constraint = get_constraint_name(confl->constraint); if (!constraint) elog(ERROR, "cache lookup failed for constraint %u", confl->constraint); appendStringInfo(buf, " ON CONSTRAINT %s", quote_identifier(constraint)); } if (confl->action == ONCONFLICT_NOTHING) { appendStringInfoString(buf, " DO NOTHING"); } else { appendStringInfoString(buf, " DO UPDATE SET "); /* Deparse targetlist */ get_update_query_targetlist_def(query, confl->onConflictSet, context, rte); /* Add a WHERE clause if given */ if (confl->onConflictWhere != NULL) { appendContextKeyword(context, " WHERE ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_expr(confl->onConflictWhere, context, false); } } } /* Add RETURNING if present */ if (query->returningList) { appendContextKeyword(context, " RETURNING", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_target_list(query->returningList, context, NULL, colNamesVisible); } } /* ---------- * get_update_query_def - Parse back an UPDATE parsetree * ---------- */ static void get_update_query_def(Query *query, deparse_context *context, bool colNamesVisible) { StringInfo buf = context->buf; RangeTblEntry *rte; /* Insert the WITH clause if given */ get_with_clause(query, context); /* * Start the query with UPDATE relname SET */ rte = rt_fetch(query->resultRelation, query->rtable); Assert(rte->rtekind == RTE_RELATION); if (PRETTY_INDENT(context)) { appendStringInfoChar(buf, ' '); context->indentLevel += PRETTYINDENT_STD; } appendStringInfo(buf, "UPDATE %s%s", only_marker(rte), generate_relation_name(rte->relid, NIL)); if (rte->alias != NULL) appendStringInfo(buf, " %s", quote_identifier(rte->alias->aliasname)); appendStringInfoString(buf, " SET "); /* Deparse targetlist */ get_update_query_targetlist_def(query, query->targetList, context, rte); /* Add the FROM clause if needed */ get_from_clause(query, " FROM ", context); /* Add a WHERE clause if given */ if (query->jointree->quals != NULL) { appendContextKeyword(context, " WHERE ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_expr(query->jointree->quals, context, false); } /* Add RETURNING if present */ if (query->returningList) { appendContextKeyword(context, " RETURNING", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_target_list(query->returningList, context, NULL, colNamesVisible); } } /* ---------- * get_update_query_targetlist_def - Parse back an UPDATE targetlist * ---------- */ static void get_update_query_targetlist_def(Query *query, List *targetList, deparse_context *context, RangeTblEntry *rte) { StringInfo buf = context->buf; ListCell *l; ListCell *next_ma_cell; int remaining_ma_columns; const char *sep; SubLink *cur_ma_sublink; List *ma_sublinks; /* * Prepare to deal with MULTIEXPR assignments: collect the source SubLinks * into a list. We expect them to appear, in ID order, in resjunk tlist * entries. */ ma_sublinks = NIL; if (query->hasSubLinks) /* else there can't be any */ { foreach(l, targetList) { TargetEntry *tle = (TargetEntry *) lfirst(l); if (tle->resjunk && IsA(tle->expr, SubLink)) { SubLink *sl = (SubLink *) tle->expr; if (sl->subLinkType == MULTIEXPR_SUBLINK) { ma_sublinks = lappend(ma_sublinks, sl); Assert(sl->subLinkId == list_length(ma_sublinks)); } } } } next_ma_cell = list_head(ma_sublinks); cur_ma_sublink = NULL; remaining_ma_columns = 0; /* Add the comma separated list of 'attname = value' */ sep = ""; foreach(l, targetList) { TargetEntry *tle = (TargetEntry *) lfirst(l); Node *expr; if (tle->resjunk) continue; /* ignore junk entries */ /* Emit separator (OK whether we're in multiassignment or not) */ appendStringInfoString(buf, sep); sep = ", "; /* * Check to see if we're starting a multiassignment group: if so, * output a left paren. */ if (next_ma_cell != NULL && cur_ma_sublink == NULL) { /* * We must dig down into the expr to see if it's a PARAM_MULTIEXPR * Param. That could be buried under FieldStores and * SubscriptingRefs and CoerceToDomains (cf processIndirection()), * and underneath those there could be an implicit type coercion. * Because we would ignore implicit type coercions anyway, we * don't need to be as careful as processIndirection() is about * descending past implicit CoerceToDomains. */ expr = (Node *) tle->expr; while (expr) { if (IsA(expr, FieldStore)) { FieldStore *fstore = (FieldStore *) expr; expr = (Node *) linitial(fstore->newvals); } else if (IsA(expr, SubscriptingRef)) { SubscriptingRef *sbsref = (SubscriptingRef *) expr; if (sbsref->refassgnexpr == NULL) break; expr = (Node *) sbsref->refassgnexpr; } else if (IsA(expr, CoerceToDomain)) { CoerceToDomain *cdomain = (CoerceToDomain *) expr; if (cdomain->coercionformat != COERCE_IMPLICIT_CAST) break; expr = (Node *) cdomain->arg; } else break; } expr = strip_implicit_coercions(expr); if (expr && IsA(expr, Param) && ((Param *) expr)->paramkind == PARAM_MULTIEXPR) { cur_ma_sublink = (SubLink *) lfirst(next_ma_cell); next_ma_cell = lnext(ma_sublinks, next_ma_cell); remaining_ma_columns = count_nonjunk_tlist_entries(((Query *) cur_ma_sublink->subselect)->targetList); Assert(((Param *) expr)->paramid == ((cur_ma_sublink->subLinkId << 16) | 1)); appendStringInfoChar(buf, '('); } } /* * Put out name of target column; look in the catalogs, not at * tle->resname, since resname will fail to track RENAME. */ appendStringInfoString(buf, quote_identifier(get_attname(rte->relid, tle->resno, false))); /* * Print any indirection needed (subfields or subscripts), and strip * off the top-level nodes representing the indirection assignments. */ expr = processIndirection((Node *) tle->expr, context); /* * If we're in a multiassignment, skip printing anything more, unless * this is the last column; in which case, what we print should be the * sublink, not the Param. */ if (cur_ma_sublink != NULL) { if (--remaining_ma_columns > 0) continue; /* not the last column of multiassignment */ appendStringInfoChar(buf, ')'); expr = (Node *) cur_ma_sublink; cur_ma_sublink = NULL; } appendStringInfoString(buf, " = "); get_rule_expr(expr, context, false); } } /* ---------- * get_delete_query_def - Parse back a DELETE parsetree * ---------- */ static void get_delete_query_def(Query *query, deparse_context *context, bool colNamesVisible) { StringInfo buf = context->buf; RangeTblEntry *rte; /* Insert the WITH clause if given */ get_with_clause(query, context); /* * Start the query with DELETE FROM relname */ rte = rt_fetch(query->resultRelation, query->rtable); Assert(rte->rtekind == RTE_RELATION); if (PRETTY_INDENT(context)) { appendStringInfoChar(buf, ' '); context->indentLevel += PRETTYINDENT_STD; } appendStringInfo(buf, "DELETE FROM %s%s", only_marker(rte), generate_relation_name(rte->relid, NIL)); if (rte->alias != NULL) appendStringInfo(buf, " %s", quote_identifier(rte->alias->aliasname)); /* Add the USING clause if given */ get_from_clause(query, " USING ", context); /* Add a WHERE clause if given */ if (query->jointree->quals != NULL) { appendContextKeyword(context, " WHERE ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_rule_expr(query->jointree->quals, context, false); } /* Add RETURNING if present */ if (query->returningList) { appendContextKeyword(context, " RETURNING", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); get_target_list(query->returningList, context, NULL, colNamesVisible); } } /* ---------- * get_utility_query_def - Parse back a UTILITY parsetree * ---------- */ static void get_utility_query_def(Query *query, deparse_context *context) { StringInfo buf = context->buf; if (query->utilityStmt && IsA(query->utilityStmt, NotifyStmt)) { NotifyStmt *stmt = (NotifyStmt *) query->utilityStmt; appendContextKeyword(context, "", 0, PRETTYINDENT_STD, 1); appendStringInfo(buf, "NOTIFY %s", quote_identifier(stmt->conditionname)); if (stmt->payload) { appendStringInfoString(buf, ", "); simple_quote_literal(buf, stmt->payload); } } else { /* Currently only NOTIFY utility commands can appear in rules */ elog(ERROR, "unexpected utility statement type"); } } /* * Display a Var appropriately. * * In some cases (currently only when recursing into an unnamed join) * the Var's varlevelsup has to be interpreted with respect to a context * above the current one; levelsup indicates the offset. * * If istoplevel is true, the Var is at the top level of a SELECT's * targetlist, which means we need special treatment of whole-row Vars. * Instead of the normal "tab.*", we'll print "tab.*::typename", which is a * dirty hack to prevent "tab.*" from being expanded into multiple columns. * (The parser will strip the useless coercion, so no inefficiency is added in * dump and reload.) We used to print just "tab" in such cases, but that is * ambiguous and will yield the wrong result if "tab" is also a plain column * name in the query. * * Returns the attname of the Var, or NULL if the Var has no attname (because * it is a whole-row Var or a subplan output reference). */ static char * get_variable(Var *var, int levelsup, bool istoplevel, deparse_context *context) { StringInfo buf = context->buf; RangeTblEntry *rte; AttrNumber attnum; int netlevelsup; deparse_namespace *dpns; Index varno; AttrNumber varattno; deparse_columns *colinfo; char *refname; char *attname; /* Find appropriate nesting depth */ netlevelsup = var->varlevelsup + levelsup; if (netlevelsup >= list_length(context->namespaces)) elog(ERROR, "bogus varlevelsup: %d offset %d", var->varlevelsup, levelsup); dpns = (deparse_namespace *) list_nth(context->namespaces, netlevelsup); /* * If we have a syntactic referent for the Var, and we're working from a * parse tree, prefer to use the syntactic referent. Otherwise, fall back * on the semantic referent. (Forcing use of the semantic referent when * printing plan trees is a design choice that's perhaps more motivated by * backwards compatibility than anything else. But it does have the * advantage of making plans more explicit.) */ if (var->varnosyn > 0 && dpns->plan == NULL) { varno = var->varnosyn; varattno = var->varattnosyn; } else { varno = var->varno; varattno = var->varattno; } /* * Try to find the relevant RTE in this rtable. In a plan tree, it's * likely that varno is OUTER_VAR or INNER_VAR, in which case we must dig * down into the subplans, or INDEX_VAR, which is resolved similarly. Also * find the aliases previously assigned for this RTE. */ if (varno >= 1 && varno <= list_length(dpns->rtable)) { /* * We might have been asked to map child Vars to some parent relation. */ if (context->appendparents && dpns->appendrels) { Index pvarno = varno; AttrNumber pvarattno = varattno; AppendRelInfo *appinfo = dpns->appendrels[pvarno]; bool found = false; /* Only map up to inheritance parents, not UNION ALL appendrels */ while (appinfo && rt_fetch(appinfo->parent_relid, dpns->rtable)->rtekind == RTE_RELATION) { found = false; if (pvarattno > 0) /* system columns stay as-is */ { if (pvarattno > appinfo->num_child_cols) break; /* safety check */ pvarattno = appinfo->parent_colnos[pvarattno - 1]; if (pvarattno == 0) break; /* Var is local to child */ } pvarno = appinfo->parent_relid; found = true; /* If the parent is itself a child, continue up. */ Assert(pvarno > 0 && pvarno <= list_length(dpns->rtable)); appinfo = dpns->appendrels[pvarno]; } /* * If we found an ancestral rel, and that rel is included in * appendparents, print that column not the original one. */ if (found && bms_is_member(pvarno, context->appendparents)) { varno = pvarno; varattno = pvarattno; } } rte = rt_fetch(varno, dpns->rtable); refname = (char *) list_nth(dpns->rtable_names, varno - 1); colinfo = deparse_columns_fetch(varno, dpns); attnum = varattno; } else { resolve_special_varno((Node *) var, context, get_special_variable, NULL); return NULL; } /* * The planner will sometimes emit Vars referencing resjunk elements of a * subquery's target list (this is currently only possible if it chooses * to generate a "physical tlist" for a SubqueryScan or CteScan node). * Although we prefer to print subquery-referencing Vars using the * subquery's alias, that's not possible for resjunk items since they have * no alias. So in that case, drill down to the subplan and print the * contents of the referenced tlist item. This works because in a plan * tree, such Vars can only occur in a SubqueryScan or CteScan node, and * we'll have set dpns->inner_plan to reference the child plan node. */ if ((rte->rtekind == RTE_SUBQUERY || rte->rtekind == RTE_CTE) && attnum > list_length(rte->eref->colnames) && dpns->inner_plan) { TargetEntry *tle; deparse_namespace save_dpns; tle = get_tle_by_resno(dpns->inner_tlist, attnum); if (!tle) elog(ERROR, "invalid attnum %d for relation \"%s\"", attnum, rte->eref->aliasname); Assert(netlevelsup == 0); push_child_plan(dpns, dpns->inner_plan, &save_dpns); /* * Force parentheses because our caller probably assumed a Var is a * simple expression. */ if (!IsA(tle->expr, Var)) appendStringInfoChar(buf, '('); get_rule_expr((Node *) tle->expr, context, true); if (!IsA(tle->expr, Var)) appendStringInfoChar(buf, ')'); pop_child_plan(dpns, &save_dpns); return NULL; } /* * If it's an unnamed join, look at the expansion of the alias variable. * If it's a simple reference to one of the input vars, then recursively * print the name of that var instead. When it's not a simple reference, * we have to just print the unqualified join column name. (This can only * happen with "dangerous" merged columns in a JOIN USING; we took pains * previously to make the unqualified column name unique in such cases.) * * This wouldn't work in decompiling plan trees, because we don't store * joinaliasvars lists after planning; but a plan tree should never * contain a join alias variable. */ if (rte->rtekind == RTE_JOIN && rte->alias == NULL) { if (rte->joinaliasvars == NIL) elog(ERROR, "cannot decompile join alias var in plan tree"); if (attnum > 0) { Var *aliasvar; aliasvar = (Var *) list_nth(rte->joinaliasvars, attnum - 1); /* we intentionally don't strip implicit coercions here */ if (aliasvar && IsA(aliasvar, Var)) { return get_variable(aliasvar, var->varlevelsup + levelsup, istoplevel, context); } } /* * Unnamed join has no refname. (Note: since it's unnamed, there is * no way the user could have referenced it to create a whole-row Var * for it. So we don't have to cover that case below.) */ Assert(refname == NULL); } if (attnum == InvalidAttrNumber) attname = NULL; else if (attnum > 0) { /* Get column name to use from the colinfo struct */ if (attnum > colinfo->num_cols) elog(ERROR, "invalid attnum %d for relation \"%s\"", attnum, rte->eref->aliasname); attname = colinfo->colnames[attnum - 1]; /* * If we find a Var referencing a dropped column, it seems better to * print something (anything) than to fail. In general this should * not happen, but there are specific cases involving functions * returning named composite types where we don't sufficiently enforce * that you can't drop a column that's referenced in some view. */ if (attname == NULL) attname = "?dropped?column?"; } else { /* System column - name is fixed, get it from the catalog */ attname = get_rte_attribute_name(rte, attnum); } if (refname && (context->varprefix || attname == NULL)) { appendStringInfoString(buf, quote_identifier(refname)); appendStringInfoChar(buf, '.'); } if (attname) appendStringInfoString(buf, quote_identifier(attname)); else { appendStringInfoChar(buf, '*'); if (istoplevel) appendStringInfo(buf, "::%s", format_type_with_typemod(var->vartype, var->vartypmod)); } return attname; } /* * Deparse a Var which references OUTER_VAR, INNER_VAR, or INDEX_VAR. This * routine is actually a callback for resolve_special_varno, which handles * finding the correct TargetEntry. We get the expression contained in that * TargetEntry and just need to deparse it, a job we can throw back on * get_rule_expr. */ static void get_special_variable(Node *node, deparse_context *context, void *callback_arg) { StringInfo buf = context->buf; /* * For a non-Var referent, force parentheses because our caller probably * assumed a Var is a simple expression. */ if (!IsA(node, Var)) appendStringInfoChar(buf, '('); get_rule_expr(node, context, true); if (!IsA(node, Var)) appendStringInfoChar(buf, ')'); } /* * Chase through plan references to special varnos (OUTER_VAR, INNER_VAR, * INDEX_VAR) until we find a real Var or some kind of non-Var node; then, * invoke the callback provided. */ static void resolve_special_varno(Node *node, deparse_context *context, rsv_callback callback, void *callback_arg) { Var *var; deparse_namespace *dpns; /* This function is recursive, so let's be paranoid. */ check_stack_depth(); /* If it's not a Var, invoke the callback. */ if (!IsA(node, Var)) { (*callback) (node, context, callback_arg); return; } /* Find appropriate nesting depth */ var = (Var *) node; dpns = (deparse_namespace *) list_nth(context->namespaces, var->varlevelsup); /* * If varno is special, recurse. (Don't worry about varnosyn; if we're * here, we already decided not to use that.) */ if (var->varno == OUTER_VAR && dpns->outer_tlist) { TargetEntry *tle; deparse_namespace save_dpns; Bitmapset *save_appendparents; tle = get_tle_by_resno(dpns->outer_tlist, var->varattno); if (!tle) elog(ERROR, "bogus varattno for OUTER_VAR var: %d", var->varattno); /* * If we're descending to the first child of an Append or MergeAppend, * update appendparents. This will affect deparsing of all Vars * appearing within the eventually-resolved subexpression. */ save_appendparents = context->appendparents; if (IsA(dpns->plan, Append)) context->appendparents = bms_union(context->appendparents, ((Append *) dpns->plan)->apprelids); else if (IsA(dpns->plan, MergeAppend)) context->appendparents = bms_union(context->appendparents, ((MergeAppend *) dpns->plan)->apprelids); push_child_plan(dpns, dpns->outer_plan, &save_dpns); resolve_special_varno((Node *) tle->expr, context, callback, callback_arg); pop_child_plan(dpns, &save_dpns); context->appendparents = save_appendparents; return; } else if (var->varno == INNER_VAR && dpns->inner_tlist) { TargetEntry *tle; deparse_namespace save_dpns; tle = get_tle_by_resno(dpns->inner_tlist, var->varattno); if (!tle) elog(ERROR, "bogus varattno for INNER_VAR var: %d", var->varattno); push_child_plan(dpns, dpns->inner_plan, &save_dpns); resolve_special_varno((Node *) tle->expr, context, callback, callback_arg); pop_child_plan(dpns, &save_dpns); return; } else if (var->varno == INDEX_VAR && dpns->index_tlist) { TargetEntry *tle; tle = get_tle_by_resno(dpns->index_tlist, var->varattno); if (!tle) elog(ERROR, "bogus varattno for INDEX_VAR var: %d", var->varattno); resolve_special_varno((Node *) tle->expr, context, callback, callback_arg); return; } else if (var->varno < 1 || var->varno > list_length(dpns->rtable)) elog(ERROR, "bogus varno: %d", var->varno); /* Not special. Just invoke the callback. */ (*callback) (node, context, callback_arg); } /* * Get the name of a field of an expression of composite type. The * expression is usually a Var, but we handle other cases too. * * levelsup is an extra offset to interpret the Var's varlevelsup correctly. * * This is fairly straightforward when the expression has a named composite * type; we need only look up the type in the catalogs. However, the type * could also be RECORD. Since no actual table or view column is allowed to * have type RECORD, a Var of type RECORD must refer to a JOIN or FUNCTION RTE * or to a subquery output. We drill down to find the ultimate defining * expression and attempt to infer the field name from it. We ereport if we * can't determine the name. * * Similarly, a PARAM of type RECORD has to refer to some expression of * a determinable composite type. */ static const char * get_name_for_var_field(Var *var, int fieldno, int levelsup, deparse_context *context) { RangeTblEntry *rte; AttrNumber attnum; int netlevelsup; deparse_namespace *dpns; Index varno; AttrNumber varattno; TupleDesc tupleDesc; Node *expr; /* * If it's a RowExpr that was expanded from a whole-row Var, use the * column names attached to it. */ if (IsA(var, RowExpr)) { RowExpr *r = (RowExpr *) var; if (fieldno > 0 && fieldno <= list_length(r->colnames)) return strVal(list_nth(r->colnames, fieldno - 1)); } /* * If it's a Param of type RECORD, try to find what the Param refers to. */ if (IsA(var, Param)) { Param *param = (Param *) var; ListCell *ancestor_cell; expr = find_param_referent(param, context, &dpns, &ancestor_cell); if (expr) { /* Found a match, so recurse to decipher the field name */ deparse_namespace save_dpns; const char *result; push_ancestor_plan(dpns, ancestor_cell, &save_dpns); result = get_name_for_var_field((Var *) expr, fieldno, 0, context); pop_ancestor_plan(dpns, &save_dpns); return result; } } /* * If it's a Var of type RECORD, we have to find what the Var refers to; * if not, we can use get_expr_result_tupdesc(). */ if (!IsA(var, Var) || var->vartype != RECORDOID) { tupleDesc = get_expr_result_tupdesc((Node *) var, false); /* Got the tupdesc, so we can extract the field name */ Assert(fieldno >= 1 && fieldno <= tupleDesc->natts); return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname); } /* Find appropriate nesting depth */ netlevelsup = var->varlevelsup + levelsup; if (netlevelsup >= list_length(context->namespaces)) elog(ERROR, "bogus varlevelsup: %d offset %d", var->varlevelsup, levelsup); dpns = (deparse_namespace *) list_nth(context->namespaces, netlevelsup); /* * If we have a syntactic referent for the Var, and we're working from a * parse tree, prefer to use the syntactic referent. Otherwise, fall back * on the semantic referent. (See comments in get_variable().) */ if (var->varnosyn > 0 && dpns->plan == NULL) { varno = var->varnosyn; varattno = var->varattnosyn; } else { varno = var->varno; varattno = var->varattno; } /* * Try to find the relevant RTE in this rtable. In a plan tree, it's * likely that varno is OUTER_VAR or INNER_VAR, in which case we must dig * down into the subplans, or INDEX_VAR, which is resolved similarly. * * Note: unlike get_variable and resolve_special_varno, we need not worry * about inheritance mapping: a child Var should have the same datatype as * its parent, and here we're really only interested in the Var's type. */ if (varno >= 1 && varno <= list_length(dpns->rtable)) { rte = rt_fetch(varno, dpns->rtable); attnum = varattno; } else if (varno == OUTER_VAR && dpns->outer_tlist) { TargetEntry *tle; deparse_namespace save_dpns; const char *result; tle = get_tle_by_resno(dpns->outer_tlist, varattno); if (!tle) elog(ERROR, "bogus varattno for OUTER_VAR var: %d", varattno); Assert(netlevelsup == 0); push_child_plan(dpns, dpns->outer_plan, &save_dpns); result = get_name_for_var_field((Var *) tle->expr, fieldno, levelsup, context); pop_child_plan(dpns, &save_dpns); return result; } else if (varno == INNER_VAR && dpns->inner_tlist) { TargetEntry *tle; deparse_namespace save_dpns; const char *result; tle = get_tle_by_resno(dpns->inner_tlist, varattno); if (!tle) elog(ERROR, "bogus varattno for INNER_VAR var: %d", varattno); Assert(netlevelsup == 0); push_child_plan(dpns, dpns->inner_plan, &save_dpns); result = get_name_for_var_field((Var *) tle->expr, fieldno, levelsup, context); pop_child_plan(dpns, &save_dpns); return result; } else if (varno == INDEX_VAR && dpns->index_tlist) { TargetEntry *tle; const char *result; tle = get_tle_by_resno(dpns->index_tlist, varattno); if (!tle) elog(ERROR, "bogus varattno for INDEX_VAR var: %d", varattno); Assert(netlevelsup == 0); result = get_name_for_var_field((Var *) tle->expr, fieldno, levelsup, context); return result; } else { elog(ERROR, "bogus varno: %d", varno); return NULL; /* keep compiler quiet */ } if (attnum == InvalidAttrNumber) { /* Var is whole-row reference to RTE, so select the right field */ return get_rte_attribute_name(rte, fieldno); } /* * This part has essentially the same logic as the parser's * expandRecordVariable() function, but we are dealing with a different * representation of the input context, and we only need one field name * not a TupleDesc. Also, we need special cases for finding subquery and * CTE subplans when deparsing Plan trees. */ expr = (Node *) var; /* default if we can't drill down */ switch (rte->rtekind) { case RTE_RELATION: case RTE_VALUES: case RTE_NAMEDTUPLESTORE: case RTE_RESULT: /* * This case should not occur: a column of a table, values list, * or ENR shouldn't have type RECORD. Fall through and fail (most * likely) at the bottom. */ break; case RTE_SUBQUERY: /* Subselect-in-FROM: examine sub-select's output expr */ { if (rte->subquery) { TargetEntry *ste = get_tle_by_resno(rte->subquery->targetList, attnum); if (ste == NULL || ste->resjunk) elog(ERROR, "subquery %s does not have attribute %d", rte->eref->aliasname, attnum); expr = (Node *) ste->expr; if (IsA(expr, Var)) { /* * Recurse into the sub-select to see what its Var * refers to. We have to build an additional level of * namespace to keep in step with varlevelsup in the * subselect. */ deparse_namespace mydpns; const char *result; set_deparse_for_query(&mydpns, rte->subquery, context->namespaces); context->namespaces = lcons(&mydpns, context->namespaces); result = get_name_for_var_field((Var *) expr, fieldno, 0, context); context->namespaces = list_delete_first(context->namespaces); return result; } /* else fall through to inspect the expression */ } else { /* * We're deparsing a Plan tree so we don't have complete * RTE entries (in particular, rte->subquery is NULL). But * the only place we'd see a Var directly referencing a * SUBQUERY RTE is in a SubqueryScan plan node, and we can * look into the child plan's tlist instead. */ TargetEntry *tle; deparse_namespace save_dpns; const char *result; if (!dpns->inner_plan) elog(ERROR, "failed to find plan for subquery %s", rte->eref->aliasname); tle = get_tle_by_resno(dpns->inner_tlist, attnum); if (!tle) elog(ERROR, "bogus varattno for subquery var: %d", attnum); Assert(netlevelsup == 0); push_child_plan(dpns, dpns->inner_plan, &save_dpns); result = get_name_for_var_field((Var *) tle->expr, fieldno, levelsup, context); pop_child_plan(dpns, &save_dpns); return result; } } break; case RTE_JOIN: /* Join RTE --- recursively inspect the alias variable */ if (rte->joinaliasvars == NIL) elog(ERROR, "cannot decompile join alias var in plan tree"); Assert(attnum > 0 && attnum <= list_length(rte->joinaliasvars)); expr = (Node *) list_nth(rte->joinaliasvars, attnum - 1); Assert(expr != NULL); /* we intentionally don't strip implicit coercions here */ if (IsA(expr, Var)) return get_name_for_var_field((Var *) expr, fieldno, var->varlevelsup + levelsup, context); /* else fall through to inspect the expression */ break; case RTE_FUNCTION: case RTE_TABLEFUNC: /* * We couldn't get here unless a function is declared with one of * its result columns as RECORD, which is not allowed. */ break; case RTE_CTE: /* CTE reference: examine subquery's output expr */ { CommonTableExpr *cte = NULL; Index ctelevelsup; ListCell *lc; /* * Try to find the referenced CTE using the namespace stack. */ ctelevelsup = rte->ctelevelsup + netlevelsup; if (ctelevelsup >= list_length(context->namespaces)) lc = NULL; else { deparse_namespace *ctedpns; ctedpns = (deparse_namespace *) list_nth(context->namespaces, ctelevelsup); foreach(lc, ctedpns->ctes) { cte = (CommonTableExpr *) lfirst(lc); if (strcmp(cte->ctename, rte->ctename) == 0) break; } } if (lc != NULL) { Query *ctequery = (Query *) cte->ctequery; TargetEntry *ste = get_tle_by_resno(GetCTETargetList(cte), attnum); if (ste == NULL || ste->resjunk) elog(ERROR, "subquery %s does not have attribute %d", rte->eref->aliasname, attnum); expr = (Node *) ste->expr; if (IsA(expr, Var)) { /* * Recurse into the CTE to see what its Var refers to. * We have to build an additional level of namespace * to keep in step with varlevelsup in the CTE. * Furthermore it could be an outer CTE, so we may * have to delete some levels of namespace. */ List *save_nslist = context->namespaces; List *new_nslist; deparse_namespace mydpns; const char *result; set_deparse_for_query(&mydpns, ctequery, context->namespaces); new_nslist = list_copy_tail(context->namespaces, ctelevelsup); context->namespaces = lcons(&mydpns, new_nslist); result = get_name_for_var_field((Var *) expr, fieldno, 0, context); context->namespaces = save_nslist; return result; } /* else fall through to inspect the expression */ } else { /* * We're deparsing a Plan tree so we don't have a CTE * list. But the only places we'd see a Var directly * referencing a CTE RTE are in CteScan or WorkTableScan * plan nodes. For those cases, set_deparse_plan arranged * for dpns->inner_plan to be the plan node that emits the * CTE or RecursiveUnion result, and we can look at its * tlist instead. */ TargetEntry *tle; deparse_namespace save_dpns; const char *result; if (!dpns->inner_plan) elog(ERROR, "failed to find plan for CTE %s", rte->eref->aliasname); tle = get_tle_by_resno(dpns->inner_tlist, attnum); if (!tle) elog(ERROR, "bogus varattno for subquery var: %d", attnum); Assert(netlevelsup == 0); push_child_plan(dpns, dpns->inner_plan, &save_dpns); result = get_name_for_var_field((Var *) tle->expr, fieldno, levelsup, context); pop_child_plan(dpns, &save_dpns); return result; } } break; } /* * We now have an expression we can't expand any more, so see if * get_expr_result_tupdesc() can do anything with it. */ tupleDesc = get_expr_result_tupdesc(expr, false); /* Got the tupdesc, so we can extract the field name */ Assert(fieldno >= 1 && fieldno <= tupleDesc->natts); return NameStr(TupleDescAttr(tupleDesc, fieldno - 1)->attname); } /* * Try to find the referenced expression for a PARAM_EXEC Param that might * reference a parameter supplied by an upper NestLoop or SubPlan plan node. * * If successful, return the expression and set *dpns_p and *ancestor_cell_p * appropriately for calling push_ancestor_plan(). If no referent can be * found, return NULL. */ static Node * find_param_referent(Param *param, deparse_context *context, deparse_namespace **dpns_p, ListCell **ancestor_cell_p) { /* Initialize output parameters to prevent compiler warnings */ *dpns_p = NULL; *ancestor_cell_p = NULL; /* * If it's a PARAM_EXEC parameter, look for a matching NestLoopParam or * SubPlan argument. This will necessarily be in some ancestor of the * current expression's Plan node. */ if (param->paramkind == PARAM_EXEC) { deparse_namespace *dpns; Plan *child_plan; bool in_same_plan_level; ListCell *lc; dpns = (deparse_namespace *) linitial(context->namespaces); child_plan = dpns->plan; in_same_plan_level = true; foreach(lc, dpns->ancestors) { Node *ancestor = (Node *) lfirst(lc); ListCell *lc2; /* * NestLoops transmit params to their inner child only; also, once * we've crawled up out of a subplan, this couldn't possibly be * the right match. */ if (IsA(ancestor, NestLoop) && child_plan == innerPlan(ancestor) && in_same_plan_level) { NestLoop *nl = (NestLoop *) ancestor; foreach(lc2, nl->nestParams) { NestLoopParam *nlp = (NestLoopParam *) lfirst(lc2); if (nlp->paramno == param->paramid) { /* Found a match, so return it */ *dpns_p = dpns; *ancestor_cell_p = lc; return (Node *) nlp->paramval; } } } /* * If ancestor is a SubPlan, check the arguments it provides. */ if (IsA(ancestor, SubPlan)) { SubPlan *subplan = (SubPlan *) ancestor; ListCell *lc3; ListCell *lc4; forboth(lc3, subplan->parParam, lc4, subplan->args) { int paramid = lfirst_int(lc3); Node *arg = (Node *) lfirst(lc4); if (paramid == param->paramid) { /* * Found a match, so return it. But, since Vars in * the arg are to be evaluated in the surrounding * context, we have to point to the next ancestor item * that is *not* a SubPlan. */ ListCell *rest; for_each_cell(rest, dpns->ancestors, lnext(dpns->ancestors, lc)) { Node *ancestor2 = (Node *) lfirst(rest); if (!IsA(ancestor2, SubPlan)) { *dpns_p = dpns; *ancestor_cell_p = rest; return arg; } } elog(ERROR, "SubPlan cannot be outermost ancestor"); } } /* We have emerged from a subplan. */ in_same_plan_level = false; /* SubPlan isn't a kind of Plan, so skip the rest */ continue; } /* * Check to see if we're emerging from an initplan of the current * ancestor plan. Initplans never have any parParams, so no need * to search that list, but we need to know if we should reset * in_same_plan_level. */ foreach(lc2, ((Plan *) ancestor)->initPlan) { SubPlan *subplan = castNode(SubPlan, lfirst(lc2)); if (child_plan != (Plan *) list_nth(dpns->subplans, subplan->plan_id - 1)) continue; /* No parameters to be had here. */ Assert(subplan->parParam == NIL); /* We have emerged from an initplan. */ in_same_plan_level = false; break; } /* No luck, crawl up to next ancestor */ child_plan = (Plan *) ancestor; } } /* No referent found */ return NULL; } /* * Display a Param appropriately. */ static void get_parameter(Param *param, deparse_context *context) { Node *expr; deparse_namespace *dpns; ListCell *ancestor_cell; /* * If it's a PARAM_EXEC parameter, try to locate the expression from which * the parameter was computed. Note that failing to find a referent isn't * an error, since the Param might well be a subplan output rather than an * input. */ expr = find_param_referent(param, context, &dpns, &ancestor_cell); if (expr) { /* Found a match, so print it */ deparse_namespace save_dpns; bool save_varprefix; bool need_paren; /* Switch attention to the ancestor plan node */ push_ancestor_plan(dpns, ancestor_cell, &save_dpns); /* * Force prefixing of Vars, since they won't belong to the relation * being scanned in the original plan node. */ save_varprefix = context->varprefix; context->varprefix = true; /* * A Param's expansion is typically a Var, Aggref, GroupingFunc, or * upper-level Param, which wouldn't need extra parentheses. * Otherwise, insert parens to ensure the expression looks atomic. */ need_paren = !(IsA(expr, Var) || IsA(expr, Aggref) || IsA(expr, GroupingFunc) || IsA(expr, Param)); if (need_paren) appendStringInfoChar(context->buf, '('); get_rule_expr(expr, context, false); if (need_paren) appendStringInfoChar(context->buf, ')'); context->varprefix = save_varprefix; pop_ancestor_plan(dpns, &save_dpns); return; } /* * If it's an external parameter, see if the outermost namespace provides * function argument names. */ if (param->paramkind == PARAM_EXTERN && context->namespaces != NIL) { dpns = llast(context->namespaces); if (dpns->argnames && param->paramid > 0 && param->paramid <= dpns->numargs) { char *argname = dpns->argnames[param->paramid - 1]; if (argname) { bool should_qualify = false; ListCell *lc; /* * Qualify the parameter name if there are any other deparse * namespaces with range tables. This avoids qualifying in * trivial cases like "RETURN a + b", but makes it safe in all * other cases. */ foreach(lc, context->namespaces) { deparse_namespace *dpns = lfirst(lc); if (list_length(dpns->rtable_names) > 0) { should_qualify = true; break; } } if (should_qualify) { appendStringInfoString(context->buf, quote_identifier(dpns->funcname)); appendStringInfoChar(context->buf, '.'); } appendStringInfoString(context->buf, quote_identifier(argname)); return; } } } /* * Not PARAM_EXEC, or couldn't find referent: just print $N. */ appendStringInfo(context->buf, "$%d", param->paramid); } /* * get_simple_binary_op_name * * helper function for isSimpleNode * will return single char binary operator name, or NULL if it's not */ static const char * get_simple_binary_op_name(OpExpr *expr) { List *args = expr->args; if (list_length(args) == 2) { /* binary operator */ Node *arg1 = (Node *) linitial(args); Node *arg2 = (Node *) lsecond(args); const char *op; op = generate_operator_name(expr->opno, exprType(arg1), exprType(arg2)); if (strlen(op) == 1) return op; } return NULL; } /* * isSimpleNode - check if given node is simple (doesn't need parenthesizing) * * true : simple in the context of parent node's type * false : not simple */ static bool isSimpleNode(Node *node, Node *parentNode, int prettyFlags) { if (!node) return false; switch (nodeTag(node)) { case T_Var: case T_Const: case T_Param: case T_CoerceToDomainValue: case T_SetToDefault: case T_CurrentOfExpr: /* single words: always simple */ return true; case T_SubscriptingRef: case T_ArrayExpr: case T_RowExpr: case T_CoalesceExpr: case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: case T_NextValueExpr: case T_NullIfExpr: case T_Aggref: case T_GroupingFunc: case T_WindowFunc: case T_FuncExpr: /* function-like: name(..) or name[..] */ return true; /* CASE keywords act as parentheses */ case T_CaseExpr: return true; case T_FieldSelect: /* * appears simple since . has top precedence, unless parent is * T_FieldSelect itself! */ return (IsA(parentNode, FieldSelect) ? false : true); case T_FieldStore: /* * treat like FieldSelect (probably doesn't matter) */ return (IsA(parentNode, FieldStore) ? false : true); case T_CoerceToDomain: /* maybe simple, check args */ return isSimpleNode((Node *) ((CoerceToDomain *) node)->arg, node, prettyFlags); case T_RelabelType: return isSimpleNode((Node *) ((RelabelType *) node)->arg, node, prettyFlags); case T_CoerceViaIO: return isSimpleNode((Node *) ((CoerceViaIO *) node)->arg, node, prettyFlags); case T_ArrayCoerceExpr: return isSimpleNode((Node *) ((ArrayCoerceExpr *) node)->arg, node, prettyFlags); case T_ConvertRowtypeExpr: return isSimpleNode((Node *) ((ConvertRowtypeExpr *) node)->arg, node, prettyFlags); case T_OpExpr: { /* depends on parent node type; needs further checking */ if (prettyFlags & PRETTYFLAG_PAREN && IsA(parentNode, OpExpr)) { const char *op; const char *parentOp; bool is_lopriop; bool is_hipriop; bool is_lopriparent; bool is_hipriparent; op = get_simple_binary_op_name((OpExpr *) node); if (!op) return false; /* We know only the basic operators + - and * / % */ is_lopriop = (strchr("+-", *op) != NULL); is_hipriop = (strchr("*/%", *op) != NULL); if (!(is_lopriop || is_hipriop)) return false; parentOp = get_simple_binary_op_name((OpExpr *) parentNode); if (!parentOp) return false; is_lopriparent = (strchr("+-", *parentOp) != NULL); is_hipriparent = (strchr("*/%", *parentOp) != NULL); if (!(is_lopriparent || is_hipriparent)) return false; if (is_hipriop && is_lopriparent) return true; /* op binds tighter than parent */ if (is_lopriop && is_hipriparent) return false; /* * Operators are same priority --- can skip parens only if * we have (a - b) - c, not a - (b - c). */ if (node == (Node *) linitial(((OpExpr *) parentNode)->args)) return true; return false; } /* else do the same stuff as for T_SubLink et al. */ } /* FALLTHROUGH */ case T_SubLink: case T_NullTest: case T_BooleanTest: case T_DistinctExpr: switch (nodeTag(parentNode)) { case T_FuncExpr: { /* special handling for casts */ CoercionForm type = ((FuncExpr *) parentNode)->funcformat; if (type == COERCE_EXPLICIT_CAST || type == COERCE_IMPLICIT_CAST) return false; return true; /* own parentheses */ } case T_BoolExpr: /* lower precedence */ case T_SubscriptingRef: /* other separators */ case T_ArrayExpr: /* other separators */ case T_RowExpr: /* other separators */ case T_CoalesceExpr: /* own parentheses */ case T_MinMaxExpr: /* own parentheses */ case T_XmlExpr: /* own parentheses */ case T_NullIfExpr: /* other separators */ case T_Aggref: /* own parentheses */ case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ return true; default: return false; } case T_BoolExpr: switch (nodeTag(parentNode)) { case T_BoolExpr: if (prettyFlags & PRETTYFLAG_PAREN) { BoolExprType type; BoolExprType parentType; type = ((BoolExpr *) node)->boolop; parentType = ((BoolExpr *) parentNode)->boolop; switch (type) { case NOT_EXPR: case AND_EXPR: if (parentType == AND_EXPR || parentType == OR_EXPR) return true; break; case OR_EXPR: if (parentType == OR_EXPR) return true; break; } } return false; case T_FuncExpr: { /* special handling for casts */ CoercionForm type = ((FuncExpr *) parentNode)->funcformat; if (type == COERCE_EXPLICIT_CAST || type == COERCE_IMPLICIT_CAST) return false; return true; /* own parentheses */ } case T_SubscriptingRef: /* other separators */ case T_ArrayExpr: /* other separators */ case T_RowExpr: /* other separators */ case T_CoalesceExpr: /* own parentheses */ case T_MinMaxExpr: /* own parentheses */ case T_XmlExpr: /* own parentheses */ case T_NullIfExpr: /* other separators */ case T_Aggref: /* own parentheses */ case T_GroupingFunc: /* own parentheses */ case T_WindowFunc: /* own parentheses */ case T_CaseExpr: /* other separators */ return true; default: return false; } default: break; } /* those we don't know: in dubio complexo */ return false; } /* * appendContextKeyword - append a keyword to buffer * * If prettyPrint is enabled, perform a line break, and adjust indentation. * Otherwise, just append the keyword. */ static void appendContextKeyword(deparse_context *context, const char *str, int indentBefore, int indentAfter, int indentPlus) { StringInfo buf = context->buf; if (PRETTY_INDENT(context)) { int indentAmount; context->indentLevel += indentBefore; /* remove any trailing spaces currently in the buffer ... */ removeStringInfoSpaces(buf); /* ... then add a newline and some spaces */ appendStringInfoChar(buf, '\n'); if (context->indentLevel < PRETTYINDENT_LIMIT) indentAmount = Max(context->indentLevel, 0) + indentPlus; else { /* * If we're indented more than PRETTYINDENT_LIMIT characters, try * to conserve horizontal space by reducing the per-level * indentation. For best results the scale factor here should * divide all the indent amounts that get added to indentLevel * (PRETTYINDENT_STD, etc). It's important that the indentation * not grow unboundedly, else deeply-nested trees use O(N^2) * whitespace; so we also wrap modulo PRETTYINDENT_LIMIT. */ indentAmount = PRETTYINDENT_LIMIT + (context->indentLevel - PRETTYINDENT_LIMIT) / (PRETTYINDENT_STD / 2); indentAmount %= PRETTYINDENT_LIMIT; /* scale/wrap logic affects indentLevel, but not indentPlus */ indentAmount += indentPlus; } appendStringInfoSpaces(buf, indentAmount); appendStringInfoString(buf, str); context->indentLevel += indentAfter; if (context->indentLevel < 0) context->indentLevel = 0; } else appendStringInfoString(buf, str); } /* * removeStringInfoSpaces - delete trailing spaces from a buffer. * * Possibly this should move to stringinfo.c at some point. */ static void removeStringInfoSpaces(StringInfo str) { while (str->len > 0 && str->data[str->len - 1] == ' ') str->data[--(str->len)] = '\0'; } /* * get_rule_expr_paren - deparse expr using get_rule_expr, * embracing the string with parentheses if necessary for prettyPrint. * * Never embrace if prettyFlags=0, because it's done in the calling node. * * Any node that does *not* embrace its argument node by sql syntax (with * parentheses, non-operator keywords like CASE/WHEN/ON, or comma etc) should * use get_rule_expr_paren instead of get_rule_expr so parentheses can be * added. */ static void get_rule_expr_paren(Node *node, deparse_context *context, bool showimplicit, Node *parentNode) { bool need_paren; need_paren = PRETTY_PAREN(context) && !isSimpleNode(node, parentNode, context->prettyFlags); if (need_paren) appendStringInfoChar(context->buf, '('); get_rule_expr(node, context, showimplicit); if (need_paren) appendStringInfoChar(context->buf, ')'); } /* ---------- * get_rule_expr - Parse back an expression * * Note: showimplicit determines whether we display any implicit cast that * is present at the top of the expression tree. It is a passed argument, * not a field of the context struct, because we change the value as we * recurse down into the expression. In general we suppress implicit casts * when the result type is known with certainty (eg, the arguments of an * OR must be boolean). We display implicit casts for arguments of functions * and operators, since this is needed to be certain that the same function * or operator will be chosen when the expression is re-parsed. * ---------- */ static void get_rule_expr(Node *node, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; if (node == NULL) return; /* Guard against excessively long or deeply-nested queries */ CHECK_FOR_INTERRUPTS(); check_stack_depth(); /* * Each level of get_rule_expr must emit an indivisible term * (parenthesized if necessary) to ensure result is reparsed into the same * expression tree. The only exception is that when the input is a List, * we emit the component items comma-separated with no surrounding * decoration; this is convenient for most callers. */ switch (nodeTag(node)) { case T_Var: (void) get_variable((Var *) node, 0, false, context); break; case T_Const: get_const_expr((Const *) node, context, 0); break; case T_Param: get_parameter((Param *) node, context); break; case T_Aggref: get_agg_expr((Aggref *) node, context, (Aggref *) node); break; case T_GroupingFunc: { GroupingFunc *gexpr = (GroupingFunc *) node; appendStringInfoString(buf, "GROUPING("); get_rule_expr((Node *) gexpr->args, context, true); appendStringInfoChar(buf, ')'); } break; case T_WindowFunc: get_windowfunc_expr((WindowFunc *) node, context); break; case T_SubscriptingRef: { SubscriptingRef *sbsref = (SubscriptingRef *) node; bool need_parens; /* * If the argument is a CaseTestExpr, we must be inside a * FieldStore, ie, we are assigning to an element of an array * within a composite column. Since we already punted on * displaying the FieldStore's target information, just punt * here too, and display only the assignment source * expression. */ if (IsA(sbsref->refexpr, CaseTestExpr)) { Assert(sbsref->refassgnexpr); get_rule_expr((Node *) sbsref->refassgnexpr, context, showimplicit); break; } /* * Parenthesize the argument unless it's a simple Var or a * FieldSelect. (In particular, if it's another * SubscriptingRef, we *must* parenthesize to avoid * confusion.) */ need_parens = !IsA(sbsref->refexpr, Var) && !IsA(sbsref->refexpr, FieldSelect); if (need_parens) appendStringInfoChar(buf, '('); get_rule_expr((Node *) sbsref->refexpr, context, showimplicit); if (need_parens) appendStringInfoChar(buf, ')'); /* * If there's a refassgnexpr, we want to print the node in the * format "container[subscripts] := refassgnexpr". This is * not legal SQL, so decompilation of INSERT or UPDATE * statements should always use processIndirection as part of * the statement-level syntax. We should only see this when * EXPLAIN tries to print the targetlist of a plan resulting * from such a statement. */ if (sbsref->refassgnexpr) { Node *refassgnexpr; /* * Use processIndirection to print this node's subscripts * as well as any additional field selections or * subscripting in immediate descendants. It returns the * RHS expr that is actually being "assigned". */ refassgnexpr = processIndirection(node, context); appendStringInfoString(buf, " := "); get_rule_expr(refassgnexpr, context, showimplicit); } else { /* Just an ordinary container fetch, so print subscripts */ printSubscripts(sbsref, context); } } break; case T_FuncExpr: get_func_expr((FuncExpr *) node, context, showimplicit); break; case T_NamedArgExpr: { NamedArgExpr *na = (NamedArgExpr *) node; appendStringInfo(buf, "%s => ", quote_identifier(na->name)); get_rule_expr((Node *) na->arg, context, showimplicit); } break; case T_OpExpr: get_oper_expr((OpExpr *) node, context); break; case T_DistinctExpr: { DistinctExpr *expr = (DistinctExpr *) node; List *args = expr->args; Node *arg1 = (Node *) linitial(args); Node *arg2 = (Node *) lsecond(args); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(arg1, context, true, node); appendStringInfoString(buf, " IS DISTINCT FROM "); get_rule_expr_paren(arg2, context, true, node); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } break; case T_NullIfExpr: { NullIfExpr *nullifexpr = (NullIfExpr *) node; appendStringInfoString(buf, "NULLIF("); get_rule_expr((Node *) nullifexpr->args, context, true); appendStringInfoChar(buf, ')'); } break; case T_ScalarArrayOpExpr: { ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node; List *args = expr->args; Node *arg1 = (Node *) linitial(args); Node *arg2 = (Node *) lsecond(args); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(arg1, context, true, node); appendStringInfo(buf, " %s %s (", generate_operator_name(expr->opno, exprType(arg1), get_base_element_type(exprType(arg2))), expr->useOr ? "ANY" : "ALL"); get_rule_expr_paren(arg2, context, true, node); /* * There's inherent ambiguity in "x op ANY/ALL (y)" when y is * a bare sub-SELECT. Since we're here, the sub-SELECT must * be meant as a scalar sub-SELECT yielding an array value to * be used in ScalarArrayOpExpr; but the grammar will * preferentially interpret such a construct as an ANY/ALL * SubLink. To prevent misparsing the output that way, insert * a dummy coercion (which will be stripped by parse analysis, * so no inefficiency is added in dump and reload). This is * indeed most likely what the user wrote to get the construct * accepted in the first place. */ if (IsA(arg2, SubLink) && ((SubLink *) arg2)->subLinkType == EXPR_SUBLINK) appendStringInfo(buf, "::%s", format_type_with_typemod(exprType(arg2), exprTypmod(arg2))); appendStringInfoChar(buf, ')'); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } break; case T_BoolExpr: { BoolExpr *expr = (BoolExpr *) node; Node *first_arg = linitial(expr->args); ListCell *arg; switch (expr->boolop) { case AND_EXPR: if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(first_arg, context, false, node); for_each_from(arg, expr->args, 1) { appendStringInfoString(buf, " AND "); get_rule_expr_paren((Node *) lfirst(arg), context, false, node); } if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); break; case OR_EXPR: if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(first_arg, context, false, node); for_each_from(arg, expr->args, 1) { appendStringInfoString(buf, " OR "); get_rule_expr_paren((Node *) lfirst(arg), context, false, node); } if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); break; case NOT_EXPR: if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); appendStringInfoString(buf, "NOT "); get_rule_expr_paren(first_arg, context, false, node); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); break; default: elog(ERROR, "unrecognized boolop: %d", (int) expr->boolop); } } break; case T_SubLink: get_sublink_expr((SubLink *) node, context); break; case T_SubPlan: { SubPlan *subplan = (SubPlan *) node; /* * We cannot see an already-planned subplan in rule deparsing, * only while EXPLAINing a query plan. We don't try to * reconstruct the original SQL, just reference the subplan * that appears elsewhere in EXPLAIN's result. */ if (subplan->useHashTable) appendStringInfo(buf, "(hashed %s)", subplan->plan_name); else appendStringInfo(buf, "(%s)", subplan->plan_name); } break; case T_AlternativeSubPlan: { AlternativeSubPlan *asplan = (AlternativeSubPlan *) node; ListCell *lc; /* * This case cannot be reached in normal usage, since no * AlternativeSubPlan can appear either in parsetrees or * finished plan trees. We keep it just in case somebody * wants to use this code to print planner data structures. */ appendStringInfoString(buf, "(alternatives: "); foreach(lc, asplan->subplans) { SubPlan *splan = lfirst_node(SubPlan, lc); if (splan->useHashTable) appendStringInfo(buf, "hashed %s", splan->plan_name); else appendStringInfoString(buf, splan->plan_name); if (lnext(asplan->subplans, lc)) appendStringInfoString(buf, " or "); } appendStringInfoChar(buf, ')'); } break; case T_FieldSelect: { FieldSelect *fselect = (FieldSelect *) node; Node *arg = (Node *) fselect->arg; int fno = fselect->fieldnum; const char *fieldname; bool need_parens; /* * Parenthesize the argument unless it's an SubscriptingRef or * another FieldSelect. Note in particular that it would be * WRONG to not parenthesize a Var argument; simplicity is not * the issue here, having the right number of names is. */ need_parens = !IsA(arg, SubscriptingRef) && !IsA(arg, FieldSelect); if (need_parens) appendStringInfoChar(buf, '('); get_rule_expr(arg, context, true); if (need_parens) appendStringInfoChar(buf, ')'); /* * Get and print the field name. */ fieldname = get_name_for_var_field((Var *) arg, fno, 0, context); appendStringInfo(buf, ".%s", quote_identifier(fieldname)); } break; case T_FieldStore: { FieldStore *fstore = (FieldStore *) node; bool need_parens; /* * There is no good way to represent a FieldStore as real SQL, * so decompilation of INSERT or UPDATE statements should * always use processIndirection as part of the * statement-level syntax. We should only get here when * EXPLAIN tries to print the targetlist of a plan resulting * from such a statement. The plan case is even harder than * ordinary rules would be, because the planner tries to * collapse multiple assignments to the same field or subfield * into one FieldStore; so we can see a list of target fields * not just one, and the arguments could be FieldStores * themselves. We don't bother to try to print the target * field names; we just print the source arguments, with a * ROW() around them if there's more than one. This isn't * terribly complete, but it's probably good enough for * EXPLAIN's purposes; especially since anything more would be * either hopelessly confusing or an even poorer * representation of what the plan is actually doing. */ need_parens = (list_length(fstore->newvals) != 1); if (need_parens) appendStringInfoString(buf, "ROW("); get_rule_expr((Node *) fstore->newvals, context, showimplicit); if (need_parens) appendStringInfoChar(buf, ')'); } break; case T_RelabelType: { RelabelType *relabel = (RelabelType *) node; Node *arg = (Node *) relabel->arg; if (relabel->relabelformat == COERCE_IMPLICIT_CAST && !showimplicit) { /* don't show the implicit cast */ get_rule_expr_paren(arg, context, false, node); } else { get_coercion_expr(arg, context, relabel->resulttype, relabel->resulttypmod, node); } } break; case T_CoerceViaIO: { CoerceViaIO *iocoerce = (CoerceViaIO *) node; Node *arg = (Node *) iocoerce->arg; if (iocoerce->coerceformat == COERCE_IMPLICIT_CAST && !showimplicit) { /* don't show the implicit cast */ get_rule_expr_paren(arg, context, false, node); } else { get_coercion_expr(arg, context, iocoerce->resulttype, -1, node); } } break; case T_ArrayCoerceExpr: { ArrayCoerceExpr *acoerce = (ArrayCoerceExpr *) node; Node *arg = (Node *) acoerce->arg; if (acoerce->coerceformat == COERCE_IMPLICIT_CAST && !showimplicit) { /* don't show the implicit cast */ get_rule_expr_paren(arg, context, false, node); } else { get_coercion_expr(arg, context, acoerce->resulttype, acoerce->resulttypmod, node); } } break; case T_ConvertRowtypeExpr: { ConvertRowtypeExpr *convert = (ConvertRowtypeExpr *) node; Node *arg = (Node *) convert->arg; if (convert->convertformat == COERCE_IMPLICIT_CAST && !showimplicit) { /* don't show the implicit cast */ get_rule_expr_paren(arg, context, false, node); } else { get_coercion_expr(arg, context, convert->resulttype, -1, node); } } break; case T_CollateExpr: { CollateExpr *collate = (CollateExpr *) node; Node *arg = (Node *) collate->arg; if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(arg, context, showimplicit, node); appendStringInfo(buf, " COLLATE %s", generate_collation_name(collate->collOid)); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } break; case T_CaseExpr: { CaseExpr *caseexpr = (CaseExpr *) node; ListCell *temp; appendContextKeyword(context, "CASE", 0, PRETTYINDENT_VAR, 0); if (caseexpr->arg) { appendStringInfoChar(buf, ' '); get_rule_expr((Node *) caseexpr->arg, context, true); } foreach(temp, caseexpr->args) { CaseWhen *when = (CaseWhen *) lfirst(temp); Node *w = (Node *) when->expr; if (caseexpr->arg) { /* * The parser should have produced WHEN clauses of the * form "CaseTestExpr = RHS", possibly with an * implicit coercion inserted above the CaseTestExpr. * For accurate decompilation of rules it's essential * that we show just the RHS. However in an * expression that's been through the optimizer, the * WHEN clause could be almost anything (since the * equality operator could have been expanded into an * inline function). If we don't recognize the form * of the WHEN clause, just punt and display it as-is. */ if (IsA(w, OpExpr)) { List *args = ((OpExpr *) w)->args; if (list_length(args) == 2 && IsA(strip_implicit_coercions(linitial(args)), CaseTestExpr)) w = (Node *) lsecond(args); } } if (!PRETTY_INDENT(context)) appendStringInfoChar(buf, ' '); appendContextKeyword(context, "WHEN ", 0, 0, 0); get_rule_expr(w, context, false); appendStringInfoString(buf, " THEN "); get_rule_expr((Node *) when->result, context, true); } if (!PRETTY_INDENT(context)) appendStringInfoChar(buf, ' '); appendContextKeyword(context, "ELSE ", 0, 0, 0); get_rule_expr((Node *) caseexpr->defresult, context, true); if (!PRETTY_INDENT(context)) appendStringInfoChar(buf, ' '); appendContextKeyword(context, "END", -PRETTYINDENT_VAR, 0, 0); } break; case T_CaseTestExpr: { /* * Normally we should never get here, since for expressions * that can contain this node type we attempt to avoid * recursing to it. But in an optimized expression we might * be unable to avoid that (see comments for CaseExpr). If we * do see one, print it as CASE_TEST_EXPR. */ appendStringInfoString(buf, "CASE_TEST_EXPR"); } break; case T_ArrayExpr: { ArrayExpr *arrayexpr = (ArrayExpr *) node; appendStringInfoString(buf, "ARRAY["); get_rule_expr((Node *) arrayexpr->elements, context, true); appendStringInfoChar(buf, ']'); /* * If the array isn't empty, we assume its elements are * coerced to the desired type. If it's empty, though, we * need an explicit coercion to the array type. */ if (arrayexpr->elements == NIL) appendStringInfo(buf, "::%s", format_type_with_typemod(arrayexpr->array_typeid, -1)); } break; case T_RowExpr: { RowExpr *rowexpr = (RowExpr *) node; TupleDesc tupdesc = NULL; ListCell *arg; int i; char *sep; /* * If it's a named type and not RECORD, we may have to skip * dropped columns and/or claim there are NULLs for added * columns. */ if (rowexpr->row_typeid != RECORDOID) { tupdesc = lookup_rowtype_tupdesc(rowexpr->row_typeid, -1); Assert(list_length(rowexpr->args) <= tupdesc->natts); } /* * SQL99 allows "ROW" to be omitted when there is more than * one column, but for simplicity we always print it. */ appendStringInfoString(buf, "ROW("); sep = ""; i = 0; foreach(arg, rowexpr->args) { Node *e = (Node *) lfirst(arg); if (tupdesc == NULL || !TupleDescAttr(tupdesc, i)->attisdropped) { appendStringInfoString(buf, sep); /* Whole-row Vars need special treatment here */ get_rule_expr_toplevel(e, context, true); sep = ", "; } i++; } if (tupdesc != NULL) { while (i < tupdesc->natts) { if (!TupleDescAttr(tupdesc, i)->attisdropped) { appendStringInfoString(buf, sep); appendStringInfoString(buf, "NULL"); sep = ", "; } i++; } ReleaseTupleDesc(tupdesc); } appendStringInfoChar(buf, ')'); if (rowexpr->row_format == COERCE_EXPLICIT_CAST) appendStringInfo(buf, "::%s", format_type_with_typemod(rowexpr->row_typeid, -1)); } break; case T_RowCompareExpr: { RowCompareExpr *rcexpr = (RowCompareExpr *) node; /* * SQL99 allows "ROW" to be omitted when there is more than * one column, but for simplicity we always print it. Within * a ROW expression, whole-row Vars need special treatment, so * use get_rule_list_toplevel. */ appendStringInfoString(buf, "(ROW("); get_rule_list_toplevel(rcexpr->largs, context, true); /* * We assume that the name of the first-column operator will * do for all the rest too. This is definitely open to * failure, eg if some but not all operators were renamed * since the construct was parsed, but there seems no way to * be perfect. */ appendStringInfo(buf, ") %s ROW(", generate_operator_name(linitial_oid(rcexpr->opnos), exprType(linitial(rcexpr->largs)), exprType(linitial(rcexpr->rargs)))); get_rule_list_toplevel(rcexpr->rargs, context, true); appendStringInfoString(buf, "))"); } break; case T_CoalesceExpr: { CoalesceExpr *coalesceexpr = (CoalesceExpr *) node; appendStringInfoString(buf, "COALESCE("); get_rule_expr((Node *) coalesceexpr->args, context, true); appendStringInfoChar(buf, ')'); } break; case T_MinMaxExpr: { MinMaxExpr *minmaxexpr = (MinMaxExpr *) node; switch (minmaxexpr->op) { case IS_GREATEST: appendStringInfoString(buf, "GREATEST("); break; case IS_LEAST: appendStringInfoString(buf, "LEAST("); break; } get_rule_expr((Node *) minmaxexpr->args, context, true); appendStringInfoChar(buf, ')'); } break; case T_SQLValueFunction: { SQLValueFunction *svf = (SQLValueFunction *) node; /* * Note: this code knows that typmod for time, timestamp, and * timestamptz just prints as integer. */ switch (svf->op) { case SVFOP_CURRENT_DATE: appendStringInfoString(buf, "CURRENT_DATE"); break; case SVFOP_CURRENT_TIME: appendStringInfoString(buf, "CURRENT_TIME"); break; case SVFOP_CURRENT_TIME_N: appendStringInfo(buf, "CURRENT_TIME(%d)", svf->typmod); break; case SVFOP_CURRENT_TIMESTAMP: appendStringInfoString(buf, "CURRENT_TIMESTAMP"); break; case SVFOP_CURRENT_TIMESTAMP_N: appendStringInfo(buf, "CURRENT_TIMESTAMP(%d)", svf->typmod); break; case SVFOP_LOCALTIME: appendStringInfoString(buf, "LOCALTIME"); break; case SVFOP_LOCALTIME_N: appendStringInfo(buf, "LOCALTIME(%d)", svf->typmod); break; case SVFOP_LOCALTIMESTAMP: appendStringInfoString(buf, "LOCALTIMESTAMP"); break; case SVFOP_LOCALTIMESTAMP_N: appendStringInfo(buf, "LOCALTIMESTAMP(%d)", svf->typmod); break; case SVFOP_CURRENT_ROLE: appendStringInfoString(buf, "CURRENT_ROLE"); break; case SVFOP_CURRENT_USER: appendStringInfoString(buf, "CURRENT_USER"); break; case SVFOP_USER: appendStringInfoString(buf, "USER"); break; case SVFOP_SESSION_USER: appendStringInfoString(buf, "SESSION_USER"); break; case SVFOP_CURRENT_CATALOG: appendStringInfoString(buf, "CURRENT_CATALOG"); break; case SVFOP_CURRENT_SCHEMA: appendStringInfoString(buf, "CURRENT_SCHEMA"); break; } } break; case T_XmlExpr: { XmlExpr *xexpr = (XmlExpr *) node; bool needcomma = false; ListCell *arg; ListCell *narg; Const *con; switch (xexpr->op) { case IS_XMLCONCAT: appendStringInfoString(buf, "XMLCONCAT("); break; case IS_XMLELEMENT: appendStringInfoString(buf, "XMLELEMENT("); break; case IS_XMLFOREST: appendStringInfoString(buf, "XMLFOREST("); break; case IS_XMLPARSE: appendStringInfoString(buf, "XMLPARSE("); break; case IS_XMLPI: appendStringInfoString(buf, "XMLPI("); break; case IS_XMLROOT: appendStringInfoString(buf, "XMLROOT("); break; case IS_XMLSERIALIZE: appendStringInfoString(buf, "XMLSERIALIZE("); break; case IS_DOCUMENT: break; } if (xexpr->op == IS_XMLPARSE || xexpr->op == IS_XMLSERIALIZE) { if (xexpr->xmloption == XMLOPTION_DOCUMENT) appendStringInfoString(buf, "DOCUMENT "); else appendStringInfoString(buf, "CONTENT "); } if (xexpr->name) { appendStringInfo(buf, "NAME %s", quote_identifier(map_xml_name_to_sql_identifier(xexpr->name))); needcomma = true; } if (xexpr->named_args) { if (xexpr->op != IS_XMLFOREST) { if (needcomma) appendStringInfoString(buf, ", "); appendStringInfoString(buf, "XMLATTRIBUTES("); needcomma = false; } forboth(arg, xexpr->named_args, narg, xexpr->arg_names) { Node *e = (Node *) lfirst(arg); char *argname = strVal(lfirst(narg)); if (needcomma) appendStringInfoString(buf, ", "); get_rule_expr((Node *) e, context, true); appendStringInfo(buf, " AS %s", quote_identifier(map_xml_name_to_sql_identifier(argname))); needcomma = true; } if (xexpr->op != IS_XMLFOREST) appendStringInfoChar(buf, ')'); } if (xexpr->args) { if (needcomma) appendStringInfoString(buf, ", "); switch (xexpr->op) { case IS_XMLCONCAT: case IS_XMLELEMENT: case IS_XMLFOREST: case IS_XMLPI: case IS_XMLSERIALIZE: /* no extra decoration needed */ get_rule_expr((Node *) xexpr->args, context, true); break; case IS_XMLPARSE: Assert(list_length(xexpr->args) == 2); get_rule_expr((Node *) linitial(xexpr->args), context, true); con = lsecond_node(Const, xexpr->args); Assert(!con->constisnull); if (DatumGetBool(con->constvalue)) appendStringInfoString(buf, " PRESERVE WHITESPACE"); else appendStringInfoString(buf, " STRIP WHITESPACE"); break; case IS_XMLROOT: Assert(list_length(xexpr->args) == 3); get_rule_expr((Node *) linitial(xexpr->args), context, true); appendStringInfoString(buf, ", VERSION "); con = (Const *) lsecond(xexpr->args); if (IsA(con, Const) && con->constisnull) appendStringInfoString(buf, "NO VALUE"); else get_rule_expr((Node *) con, context, false); con = lthird_node(Const, xexpr->args); if (con->constisnull) /* suppress STANDALONE NO VALUE */ ; else { switch (DatumGetInt32(con->constvalue)) { case XML_STANDALONE_YES: appendStringInfoString(buf, ", STANDALONE YES"); break; case XML_STANDALONE_NO: appendStringInfoString(buf, ", STANDALONE NO"); break; case XML_STANDALONE_NO_VALUE: appendStringInfoString(buf, ", STANDALONE NO VALUE"); break; default: break; } } break; case IS_DOCUMENT: get_rule_expr_paren((Node *) xexpr->args, context, false, node); break; } } if (xexpr->op == IS_XMLSERIALIZE) appendStringInfo(buf, " AS %s", format_type_with_typemod(xexpr->type, xexpr->typmod)); if (xexpr->op == IS_DOCUMENT) appendStringInfoString(buf, " IS DOCUMENT"); else appendStringInfoChar(buf, ')'); } break; case T_NullTest: { NullTest *ntest = (NullTest *) node; if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren((Node *) ntest->arg, context, true, node); /* * For scalar inputs, we prefer to print as IS [NOT] NULL, * which is shorter and traditional. If it's a rowtype input * but we're applying a scalar test, must print IS [NOT] * DISTINCT FROM NULL to be semantically correct. */ if (ntest->argisrow || !type_is_rowtype(exprType((Node *) ntest->arg))) { switch (ntest->nulltesttype) { case IS_NULL: appendStringInfoString(buf, " IS NULL"); break; case IS_NOT_NULL: appendStringInfoString(buf, " IS NOT NULL"); break; default: elog(ERROR, "unrecognized nulltesttype: %d", (int) ntest->nulltesttype); } } else { switch (ntest->nulltesttype) { case IS_NULL: appendStringInfoString(buf, " IS NOT DISTINCT FROM NULL"); break; case IS_NOT_NULL: appendStringInfoString(buf, " IS DISTINCT FROM NULL"); break; default: elog(ERROR, "unrecognized nulltesttype: %d", (int) ntest->nulltesttype); } } if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } break; case T_BooleanTest: { BooleanTest *btest = (BooleanTest *) node; if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren((Node *) btest->arg, context, false, node); switch (btest->booltesttype) { case IS_TRUE: appendStringInfoString(buf, " IS TRUE"); break; case IS_NOT_TRUE: appendStringInfoString(buf, " IS NOT TRUE"); break; case IS_FALSE: appendStringInfoString(buf, " IS FALSE"); break; case IS_NOT_FALSE: appendStringInfoString(buf, " IS NOT FALSE"); break; case IS_UNKNOWN: appendStringInfoString(buf, " IS UNKNOWN"); break; case IS_NOT_UNKNOWN: appendStringInfoString(buf, " IS NOT UNKNOWN"); break; default: elog(ERROR, "unrecognized booltesttype: %d", (int) btest->booltesttype); } if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } break; case T_CoerceToDomain: { CoerceToDomain *ctest = (CoerceToDomain *) node; Node *arg = (Node *) ctest->arg; if (ctest->coercionformat == COERCE_IMPLICIT_CAST && !showimplicit) { /* don't show the implicit cast */ get_rule_expr(arg, context, false); } else { get_coercion_expr(arg, context, ctest->resulttype, ctest->resulttypmod, node); } } break; case T_CoerceToDomainValue: appendStringInfoString(buf, "VALUE"); break; case T_SetToDefault: appendStringInfoString(buf, "DEFAULT"); break; case T_CurrentOfExpr: { CurrentOfExpr *cexpr = (CurrentOfExpr *) node; if (cexpr->cursor_name) appendStringInfo(buf, "CURRENT OF %s", quote_identifier(cexpr->cursor_name)); else appendStringInfo(buf, "CURRENT OF $%d", cexpr->cursor_param); } break; case T_NextValueExpr: { NextValueExpr *nvexpr = (NextValueExpr *) node; /* * This isn't exactly nextval(), but that seems close enough * for EXPLAIN's purposes. */ appendStringInfoString(buf, "nextval("); simple_quote_literal(buf, generate_relation_name(nvexpr->seqid, NIL)); appendStringInfoChar(buf, ')'); } break; case T_InferenceElem: { InferenceElem *iexpr = (InferenceElem *) node; bool save_varprefix; bool need_parens; /* * InferenceElem can only refer to target relation, so a * prefix is not useful, and indeed would cause parse errors. */ save_varprefix = context->varprefix; context->varprefix = false; /* * Parenthesize the element unless it's a simple Var or a bare * function call. Follows pg_get_indexdef_worker(). */ need_parens = !IsA(iexpr->expr, Var); if (IsA(iexpr->expr, FuncExpr) && ((FuncExpr *) iexpr->expr)->funcformat == COERCE_EXPLICIT_CALL) need_parens = false; if (need_parens) appendStringInfoChar(buf, '('); get_rule_expr((Node *) iexpr->expr, context, false); if (need_parens) appendStringInfoChar(buf, ')'); context->varprefix = save_varprefix; if (iexpr->infercollid) appendStringInfo(buf, " COLLATE %s", generate_collation_name(iexpr->infercollid)); /* Add the operator class name, if not default */ if (iexpr->inferopclass) { Oid inferopclass = iexpr->inferopclass; Oid inferopcinputtype = get_opclass_input_type(iexpr->inferopclass); get_opclass_name(inferopclass, inferopcinputtype, buf); } } break; case T_PartitionBoundSpec: { PartitionBoundSpec *spec = (PartitionBoundSpec *) node; ListCell *cell; char *sep; if (spec->is_default) { appendStringInfoString(buf, "DEFAULT"); break; } switch (spec->strategy) { case PARTITION_STRATEGY_HASH: Assert(spec->modulus > 0 && spec->remainder >= 0); Assert(spec->modulus > spec->remainder); appendStringInfoString(buf, "FOR VALUES"); appendStringInfo(buf, " WITH (modulus %d, remainder %d)", spec->modulus, spec->remainder); break; case PARTITION_STRATEGY_LIST: Assert(spec->listdatums != NIL); appendStringInfoString(buf, "FOR VALUES IN ("); sep = ""; foreach(cell, spec->listdatums) { Const *val = castNode(Const, lfirst(cell)); appendStringInfoString(buf, sep); get_const_expr(val, context, -1); sep = ", "; } appendStringInfoChar(buf, ')'); break; case PARTITION_STRATEGY_RANGE: Assert(spec->lowerdatums != NIL && spec->upperdatums != NIL && list_length(spec->lowerdatums) == list_length(spec->upperdatums)); appendStringInfo(buf, "FOR VALUES FROM %s TO %s", get_range_partbound_string(spec->lowerdatums), get_range_partbound_string(spec->upperdatums)); break; default: elog(ERROR, "unrecognized partition strategy: %d", (int) spec->strategy); break; } } break; case T_List: { char *sep; ListCell *l; sep = ""; foreach(l, (List *) node) { appendStringInfoString(buf, sep); get_rule_expr((Node *) lfirst(l), context, showimplicit); sep = ", "; } } break; case T_TableFunc: get_tablefunc((TableFunc *) node, context, showimplicit); break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); break; } } /* * get_rule_expr_toplevel - Parse back a toplevel expression * * Same as get_rule_expr(), except that if the expr is just a Var, we pass * istoplevel = true not false to get_variable(). This causes whole-row Vars * to get printed with decoration that will prevent expansion of "*". * We need to use this in contexts such as ROW() and VALUES(), where the * parser would expand "foo.*" appearing at top level. (In principle we'd * use this in get_target_list() too, but that has additional worries about * whether to print AS, so it needs to invoke get_variable() directly anyway.) */ static void get_rule_expr_toplevel(Node *node, deparse_context *context, bool showimplicit) { if (node && IsA(node, Var)) (void) get_variable((Var *) node, 0, true, context); else get_rule_expr(node, context, showimplicit); } /* * get_rule_list_toplevel - Parse back a list of toplevel expressions * * Apply get_rule_expr_toplevel() to each element of a List. * * This adds commas between the expressions, but caller is responsible * for printing surrounding decoration. */ static void get_rule_list_toplevel(List *lst, deparse_context *context, bool showimplicit) { const char *sep; ListCell *lc; sep = ""; foreach(lc, lst) { Node *e = (Node *) lfirst(lc); appendStringInfoString(context->buf, sep); get_rule_expr_toplevel(e, context, showimplicit); sep = ", "; } } /* * get_rule_expr_funccall - Parse back a function-call expression * * Same as get_rule_expr(), except that we guarantee that the output will * look like a function call, or like one of the things the grammar treats as * equivalent to a function call (see the func_expr_windowless production). * This is needed in places where the grammar uses func_expr_windowless and * you can't substitute a parenthesized a_expr. If what we have isn't going * to look like a function call, wrap it in a dummy CAST() expression, which * will satisfy the grammar --- and, indeed, is likely what the user wrote to * produce such a thing. */ static void get_rule_expr_funccall(Node *node, deparse_context *context, bool showimplicit) { if (looks_like_function(node)) get_rule_expr(node, context, showimplicit); else { StringInfo buf = context->buf; appendStringInfoString(buf, "CAST("); /* no point in showing any top-level implicit cast */ get_rule_expr(node, context, false); appendStringInfo(buf, " AS %s)", format_type_with_typemod(exprType(node), exprTypmod(node))); } } /* * Helper function to identify node types that satisfy func_expr_windowless. * If in doubt, "false" is always a safe answer. */ static bool looks_like_function(Node *node) { if (node == NULL) return false; /* probably shouldn't happen */ switch (nodeTag(node)) { case T_FuncExpr: /* OK, unless it's going to deparse as a cast */ return (((FuncExpr *) node)->funcformat == COERCE_EXPLICIT_CALL || ((FuncExpr *) node)->funcformat == COERCE_SQL_SYNTAX); case T_NullIfExpr: case T_CoalesceExpr: case T_MinMaxExpr: case T_SQLValueFunction: case T_XmlExpr: /* these are all accepted by func_expr_common_subexpr */ return true; default: break; } return false; } /* * get_oper_expr - Parse back an OpExpr node */ static void get_oper_expr(OpExpr *expr, deparse_context *context) { StringInfo buf = context->buf; Oid opno = expr->opno; List *args = expr->args; if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); if (list_length(args) == 2) { /* binary operator */ Node *arg1 = (Node *) linitial(args); Node *arg2 = (Node *) lsecond(args); get_rule_expr_paren(arg1, context, true, (Node *) expr); appendStringInfo(buf, " %s ", generate_operator_name(opno, exprType(arg1), exprType(arg2))); get_rule_expr_paren(arg2, context, true, (Node *) expr); } else { /* prefix operator */ Node *arg = (Node *) linitial(args); appendStringInfo(buf, "%s ", generate_operator_name(opno, InvalidOid, exprType(arg))); get_rule_expr_paren(arg, context, true, (Node *) expr); } if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } /* * get_func_expr - Parse back a FuncExpr node */ static void get_func_expr(FuncExpr *expr, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; Oid funcoid = expr->funcid; Oid argtypes[FUNC_MAX_ARGS]; int nargs; List *argnames; bool use_variadic; ListCell *l; /* * If the function call came from an implicit coercion, then just show the * first argument --- unless caller wants to see implicit coercions. */ if (expr->funcformat == COERCE_IMPLICIT_CAST && !showimplicit) { get_rule_expr_paren((Node *) linitial(expr->args), context, false, (Node *) expr); return; } /* * If the function call came from a cast, then show the first argument * plus an explicit cast operation. */ if (expr->funcformat == COERCE_EXPLICIT_CAST || expr->funcformat == COERCE_IMPLICIT_CAST) { Node *arg = linitial(expr->args); Oid rettype = expr->funcresulttype; int32 coercedTypmod; /* Get the typmod if this is a length-coercion function */ (void) exprIsLengthCoercion((Node *) expr, &coercedTypmod); get_coercion_expr(arg, context, rettype, coercedTypmod, (Node *) expr); return; } /* * If the function was called using one of the SQL spec's random special * syntaxes, try to reproduce that. If we don't recognize the function, * fall through. */ if (expr->funcformat == COERCE_SQL_SYNTAX) { if (get_func_sql_syntax(expr, context)) return; } /* * Normal function: display as proname(args). First we need to extract * the argument datatypes. */ if (list_length(expr->args) > FUNC_MAX_ARGS) ereport(ERROR, (errcode(ERRCODE_TOO_MANY_ARGUMENTS), errmsg("too many arguments"))); nargs = 0; argnames = NIL; foreach(l, expr->args) { Node *arg = (Node *) lfirst(l); if (IsA(arg, NamedArgExpr)) argnames = lappend(argnames, ((NamedArgExpr *) arg)->name); argtypes[nargs] = exprType(arg); nargs++; } appendStringInfo(buf, "%s(", generate_function_name(funcoid, nargs, argnames, argtypes, expr->funcvariadic, &use_variadic, context->special_exprkind)); nargs = 0; foreach(l, expr->args) { if (nargs++ > 0) appendStringInfoString(buf, ", "); if (use_variadic && lnext(expr->args, l) == NULL) appendStringInfoString(buf, "VARIADIC "); get_rule_expr((Node *) lfirst(l), context, true); } appendStringInfoChar(buf, ')'); } /* * get_agg_expr - Parse back an Aggref node */ static void get_agg_expr(Aggref *aggref, deparse_context *context, Aggref *original_aggref) { StringInfo buf = context->buf; Oid argtypes[FUNC_MAX_ARGS]; int nargs; bool use_variadic; /* * For a combining aggregate, we look up and deparse the corresponding * partial aggregate instead. This is necessary because our input * argument list has been replaced; the new argument list always has just * one element, which will point to a partial Aggref that supplies us with * transition states to combine. */ if (DO_AGGSPLIT_COMBINE(aggref->aggsplit)) { TargetEntry *tle; Assert(list_length(aggref->args) == 1); tle = linitial_node(TargetEntry, aggref->args); resolve_special_varno((Node *) tle->expr, context, get_agg_combine_expr, original_aggref); return; } /* * Mark as PARTIAL, if appropriate. We look to the original aggref so as * to avoid printing this when recursing from the code just above. */ if (DO_AGGSPLIT_SKIPFINAL(original_aggref->aggsplit)) appendStringInfoString(buf, "PARTIAL "); /* Extract the argument types as seen by the parser */ nargs = get_aggregate_argtypes(aggref, argtypes); /* Print the aggregate name, schema-qualified if needed */ appendStringInfo(buf, "%s(%s", generate_function_name(aggref->aggfnoid, nargs, NIL, argtypes, aggref->aggvariadic, &use_variadic, context->special_exprkind), (aggref->aggdistinct != NIL) ? "DISTINCT " : ""); if (AGGKIND_IS_ORDERED_SET(aggref->aggkind)) { /* * Ordered-set aggregates do not use "*" syntax. Also, we needn't * worry about inserting VARIADIC. So we can just dump the direct * args as-is. */ Assert(!aggref->aggvariadic); get_rule_expr((Node *) aggref->aggdirectargs, context, true); Assert(aggref->aggorder != NIL); appendStringInfoString(buf, ") WITHIN GROUP (ORDER BY "); get_rule_orderby(aggref->aggorder, aggref->args, false, context); } else { /* aggstar can be set only in zero-argument aggregates */ if (aggref->aggstar) appendStringInfoChar(buf, '*'); else { ListCell *l; int i; i = 0; foreach(l, aggref->args) { TargetEntry *tle = (TargetEntry *) lfirst(l); Node *arg = (Node *) tle->expr; Assert(!IsA(arg, NamedArgExpr)); if (tle->resjunk) continue; if (i++ > 0) appendStringInfoString(buf, ", "); if (use_variadic && i == nargs) appendStringInfoString(buf, "VARIADIC "); get_rule_expr(arg, context, true); } } if (aggref->aggorder != NIL) { appendStringInfoString(buf, " ORDER BY "); get_rule_orderby(aggref->aggorder, aggref->args, false, context); } } if (aggref->aggfilter != NULL) { appendStringInfoString(buf, ") FILTER (WHERE "); get_rule_expr((Node *) aggref->aggfilter, context, false); } appendStringInfoChar(buf, ')'); } /* * This is a helper function for get_agg_expr(). It's used when we deparse * a combining Aggref; resolve_special_varno locates the corresponding partial * Aggref and then calls this. */ static void get_agg_combine_expr(Node *node, deparse_context *context, void *callback_arg) { Aggref *aggref; Aggref *original_aggref = callback_arg; if (!IsA(node, Aggref)) elog(ERROR, "combining Aggref does not point to an Aggref"); aggref = (Aggref *) node; get_agg_expr(aggref, context, original_aggref); } /* * get_windowfunc_expr - Parse back a WindowFunc node */ static void get_windowfunc_expr(WindowFunc *wfunc, deparse_context *context) { StringInfo buf = context->buf; Oid argtypes[FUNC_MAX_ARGS]; int nargs; List *argnames; ListCell *l; if (list_length(wfunc->args) > FUNC_MAX_ARGS) ereport(ERROR, (errcode(ERRCODE_TOO_MANY_ARGUMENTS), errmsg("too many arguments"))); nargs = 0; argnames = NIL; foreach(l, wfunc->args) { Node *arg = (Node *) lfirst(l); if (IsA(arg, NamedArgExpr)) argnames = lappend(argnames, ((NamedArgExpr *) arg)->name); argtypes[nargs] = exprType(arg); nargs++; } appendStringInfo(buf, "%s(", generate_function_name(wfunc->winfnoid, nargs, argnames, argtypes, false, NULL, context->special_exprkind)); /* winstar can be set only in zero-argument aggregates */ if (wfunc->winstar) appendStringInfoChar(buf, '*'); else get_rule_expr((Node *) wfunc->args, context, true); if (wfunc->aggfilter != NULL) { appendStringInfoString(buf, ") FILTER (WHERE "); get_rule_expr((Node *) wfunc->aggfilter, context, false); } appendStringInfoString(buf, ") OVER "); foreach(l, context->windowClause) { WindowClause *wc = (WindowClause *) lfirst(l); if (wc->winref == wfunc->winref) { if (wc->name) appendStringInfoString(buf, quote_identifier(wc->name)); else get_rule_windowspec(wc, context->windowTList, context); break; } } if (l == NULL) { if (context->windowClause) elog(ERROR, "could not find window clause for winref %u", wfunc->winref); /* * In EXPLAIN, we don't have window context information available, so * we have to settle for this: */ appendStringInfoString(buf, "(?)"); } } /* * get_func_sql_syntax - Parse back a SQL-syntax function call * * Returns true if we successfully deparsed, false if we did not * recognize the function. */ static bool get_func_sql_syntax(FuncExpr *expr, deparse_context *context) { StringInfo buf = context->buf; Oid funcoid = expr->funcid; switch (funcoid) { case F_TIMEZONE_INTERVAL_TIMESTAMP: case F_TIMEZONE_INTERVAL_TIMESTAMPTZ: case F_TIMEZONE_INTERVAL_TIMETZ: case F_TIMEZONE_TEXT_TIMESTAMP: case F_TIMEZONE_TEXT_TIMESTAMPTZ: case F_TIMEZONE_TEXT_TIMETZ: /* AT TIME ZONE ... note reversed argument order */ appendStringInfoChar(buf, '('); get_rule_expr((Node *) lsecond(expr->args), context, false); appendStringInfoString(buf, " AT TIME ZONE "); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoChar(buf, ')'); return true; case F_OVERLAPS_TIMESTAMPTZ_INTERVAL_TIMESTAMPTZ_INTERVAL: case F_OVERLAPS_TIMESTAMPTZ_INTERVAL_TIMESTAMPTZ_TIMESTAMPTZ: case F_OVERLAPS_TIMESTAMPTZ_TIMESTAMPTZ_TIMESTAMPTZ_INTERVAL: case F_OVERLAPS_TIMESTAMPTZ_TIMESTAMPTZ_TIMESTAMPTZ_TIMESTAMPTZ: case F_OVERLAPS_TIMESTAMP_INTERVAL_TIMESTAMP_INTERVAL: case F_OVERLAPS_TIMESTAMP_INTERVAL_TIMESTAMP_TIMESTAMP: case F_OVERLAPS_TIMESTAMP_TIMESTAMP_TIMESTAMP_INTERVAL: case F_OVERLAPS_TIMESTAMP_TIMESTAMP_TIMESTAMP_TIMESTAMP: case F_OVERLAPS_TIMETZ_TIMETZ_TIMETZ_TIMETZ: case F_OVERLAPS_TIME_INTERVAL_TIME_INTERVAL: case F_OVERLAPS_TIME_INTERVAL_TIME_TIME: case F_OVERLAPS_TIME_TIME_TIME_INTERVAL: case F_OVERLAPS_TIME_TIME_TIME_TIME: /* (x1, x2) OVERLAPS (y1, y2) */ appendStringInfoString(buf, "(("); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoString(buf, ", "); get_rule_expr((Node *) lsecond(expr->args), context, false); appendStringInfoString(buf, ") OVERLAPS ("); get_rule_expr((Node *) lthird(expr->args), context, false); appendStringInfoString(buf, ", "); get_rule_expr((Node *) lfourth(expr->args), context, false); appendStringInfoString(buf, "))"); return true; case F_EXTRACT_TEXT_DATE: case F_EXTRACT_TEXT_TIME: case F_EXTRACT_TEXT_TIMETZ: case F_EXTRACT_TEXT_TIMESTAMP: case F_EXTRACT_TEXT_TIMESTAMPTZ: case F_EXTRACT_TEXT_INTERVAL: /* EXTRACT (x FROM y) */ appendStringInfoString(buf, "EXTRACT("); { Const *con = (Const *) linitial(expr->args); Assert(IsA(con, Const) && con->consttype == TEXTOID && !con->constisnull); appendStringInfoString(buf, TextDatumGetCString(con->constvalue)); } appendStringInfoString(buf, " FROM "); get_rule_expr((Node *) lsecond(expr->args), context, false); appendStringInfoChar(buf, ')'); return true; case F_IS_NORMALIZED: /* IS xxx NORMALIZED */ appendStringInfoString(buf, "(("); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoString(buf, ") IS"); if (list_length(expr->args) == 2) { Const *con = (Const *) lsecond(expr->args); Assert(IsA(con, Const) && con->consttype == TEXTOID && !con->constisnull); appendStringInfo(buf, " %s", TextDatumGetCString(con->constvalue)); } appendStringInfoString(buf, " NORMALIZED)"); return true; case F_PG_COLLATION_FOR: /* COLLATION FOR */ appendStringInfoString(buf, "COLLATION FOR ("); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoChar(buf, ')'); return true; /* * XXX EXTRACT, a/k/a date_part(), is intentionally not covered * yet. Add it after we change the return type to numeric. */ case F_NORMALIZE: /* NORMALIZE() */ appendStringInfoString(buf, "NORMALIZE("); get_rule_expr((Node *) linitial(expr->args), context, false); if (list_length(expr->args) == 2) { Const *con = (Const *) lsecond(expr->args); Assert(IsA(con, Const) && con->consttype == TEXTOID && !con->constisnull); appendStringInfo(buf, ", %s", TextDatumGetCString(con->constvalue)); } appendStringInfoChar(buf, ')'); return true; case F_OVERLAY_BIT_BIT_INT4: case F_OVERLAY_BIT_BIT_INT4_INT4: case F_OVERLAY_BYTEA_BYTEA_INT4: case F_OVERLAY_BYTEA_BYTEA_INT4_INT4: case F_OVERLAY_TEXT_TEXT_INT4: case F_OVERLAY_TEXT_TEXT_INT4_INT4: /* OVERLAY() */ appendStringInfoString(buf, "OVERLAY("); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoString(buf, " PLACING "); get_rule_expr((Node *) lsecond(expr->args), context, false); appendStringInfoString(buf, " FROM "); get_rule_expr((Node *) lthird(expr->args), context, false); if (list_length(expr->args) == 4) { appendStringInfoString(buf, " FOR "); get_rule_expr((Node *) lfourth(expr->args), context, false); } appendStringInfoChar(buf, ')'); return true; case F_POSITION_BIT_BIT: case F_POSITION_BYTEA_BYTEA: case F_POSITION_TEXT_TEXT: /* POSITION() ... extra parens since args are b_expr not a_expr */ appendStringInfoString(buf, "POSITION(("); get_rule_expr((Node *) lsecond(expr->args), context, false); appendStringInfoString(buf, ") IN ("); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoString(buf, "))"); return true; case F_SUBSTRING_BIT_INT4: case F_SUBSTRING_BIT_INT4_INT4: case F_SUBSTRING_BYTEA_INT4: case F_SUBSTRING_BYTEA_INT4_INT4: case F_SUBSTRING_TEXT_INT4: case F_SUBSTRING_TEXT_INT4_INT4: /* SUBSTRING FROM/FOR (i.e., integer-position variants) */ appendStringInfoString(buf, "SUBSTRING("); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoString(buf, " FROM "); get_rule_expr((Node *) lsecond(expr->args), context, false); if (list_length(expr->args) == 3) { appendStringInfoString(buf, " FOR "); get_rule_expr((Node *) lthird(expr->args), context, false); } appendStringInfoChar(buf, ')'); return true; case F_SUBSTRING_TEXT_TEXT_TEXT: /* SUBSTRING SIMILAR/ESCAPE */ appendStringInfoString(buf, "SUBSTRING("); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoString(buf, " SIMILAR "); get_rule_expr((Node *) lsecond(expr->args), context, false); appendStringInfoString(buf, " ESCAPE "); get_rule_expr((Node *) lthird(expr->args), context, false); appendStringInfoChar(buf, ')'); return true; case F_BTRIM_BYTEA_BYTEA: case F_BTRIM_TEXT: case F_BTRIM_TEXT_TEXT: /* TRIM() */ appendStringInfoString(buf, "TRIM(BOTH"); if (list_length(expr->args) == 2) { appendStringInfoChar(buf, ' '); get_rule_expr((Node *) lsecond(expr->args), context, false); } appendStringInfoString(buf, " FROM "); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoChar(buf, ')'); return true; case F_LTRIM_BYTEA_BYTEA: case F_LTRIM_TEXT: case F_LTRIM_TEXT_TEXT: /* TRIM() */ appendStringInfoString(buf, "TRIM(LEADING"); if (list_length(expr->args) == 2) { appendStringInfoChar(buf, ' '); get_rule_expr((Node *) lsecond(expr->args), context, false); } appendStringInfoString(buf, " FROM "); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoChar(buf, ')'); return true; case F_RTRIM_BYTEA_BYTEA: case F_RTRIM_TEXT: case F_RTRIM_TEXT_TEXT: /* TRIM() */ appendStringInfoString(buf, "TRIM(TRAILING"); if (list_length(expr->args) == 2) { appendStringInfoChar(buf, ' '); get_rule_expr((Node *) lsecond(expr->args), context, false); } appendStringInfoString(buf, " FROM "); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoChar(buf, ')'); return true; case F_XMLEXISTS: /* XMLEXISTS ... extra parens because args are c_expr */ appendStringInfoString(buf, "XMLEXISTS(("); get_rule_expr((Node *) linitial(expr->args), context, false); appendStringInfoString(buf, ") PASSING ("); get_rule_expr((Node *) lsecond(expr->args), context, false); appendStringInfoString(buf, "))"); return true; } return false; } /* ---------- * get_coercion_expr * * Make a string representation of a value coerced to a specific type * ---------- */ static void get_coercion_expr(Node *arg, deparse_context *context, Oid resulttype, int32 resulttypmod, Node *parentNode) { StringInfo buf = context->buf; /* * Since parse_coerce.c doesn't immediately collapse application of * length-coercion functions to constants, what we'll typically see in * such cases is a Const with typmod -1 and a length-coercion function * right above it. Avoid generating redundant output. However, beware of * suppressing casts when the user actually wrote something like * 'foo'::text::char(3). * * Note: it might seem that we are missing the possibility of needing to * print a COLLATE clause for such a Const. However, a Const could only * have nondefault collation in a post-constant-folding tree, in which the * length coercion would have been folded too. See also the special * handling of CollateExpr in coerce_to_target_type(): any collation * marking will be above the coercion node, not below it. */ if (arg && IsA(arg, Const) && ((Const *) arg)->consttype == resulttype && ((Const *) arg)->consttypmod == -1) { /* Show the constant without normal ::typename decoration */ get_const_expr((Const *) arg, context, -1); } else { if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren(arg, context, false, parentNode); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } /* * Never emit resulttype(arg) functional notation. A pg_proc entry could * take precedence, and a resulttype in pg_temp would require schema * qualification that format_type_with_typemod() would usually omit. We've * standardized on arg::resulttype, but CAST(arg AS resulttype) notation * would work fine. */ appendStringInfo(buf, "::%s", format_type_with_typemod(resulttype, resulttypmod)); } /* ---------- * get_const_expr * * Make a string representation of a Const * * showtype can be -1 to never show "::typename" decoration, or +1 to always * show it, or 0 to show it only if the constant wouldn't be assumed to be * the right type by default. * * If the Const's collation isn't default for its type, show that too. * We mustn't do this when showtype is -1 (since that means the caller will * print "::typename", and we can't put a COLLATE clause in between). It's * caller's responsibility that collation isn't missed in such cases. * ---------- */ static void get_const_expr(Const *constval, deparse_context *context, int showtype) { StringInfo buf = context->buf; Oid typoutput; bool typIsVarlena; char *extval; bool needlabel = false; if (constval->constisnull) { /* * Always label the type of a NULL constant to prevent misdecisions * about type when reparsing. */ appendStringInfoString(buf, "NULL"); if (showtype >= 0) { appendStringInfo(buf, "::%s", format_type_with_typemod(constval->consttype, constval->consttypmod)); get_const_collation(constval, context); } return; } getTypeOutputInfo(constval->consttype, &typoutput, &typIsVarlena); extval = OidOutputFunctionCall(typoutput, constval->constvalue); switch (constval->consttype) { case INT4OID: /* * INT4 can be printed without any decoration, unless it is * negative; in that case print it as '-nnn'::integer to ensure * that the output will re-parse as a constant, not as a constant * plus operator. In most cases we could get away with printing * (-nnn) instead, because of the way that gram.y handles negative * literals; but that doesn't work for INT_MIN, and it doesn't * seem that much prettier anyway. */ if (extval[0] != '-') appendStringInfoString(buf, extval); else { appendStringInfo(buf, "'%s'", extval); needlabel = true; /* we must attach a cast */ } break; case NUMERICOID: /* * NUMERIC can be printed without quotes if it looks like a float * constant (not an integer, and not Infinity or NaN) and doesn't * have a leading sign (for the same reason as for INT4). */ if (isdigit((unsigned char) extval[0]) && strcspn(extval, "eE.") != strlen(extval)) { appendStringInfoString(buf, extval); } else { appendStringInfo(buf, "'%s'", extval); needlabel = true; /* we must attach a cast */ } break; case BOOLOID: if (strcmp(extval, "t") == 0) appendStringInfoString(buf, "true"); else appendStringInfoString(buf, "false"); break; default: simple_quote_literal(buf, extval); break; } pfree(extval); if (showtype < 0) return; /* * For showtype == 0, append ::typename unless the constant will be * implicitly typed as the right type when it is read in. * * XXX this code has to be kept in sync with the behavior of the parser, * especially make_const. */ switch (constval->consttype) { case BOOLOID: case UNKNOWNOID: /* These types can be left unlabeled */ needlabel = false; break; case INT4OID: /* We determined above whether a label is needed */ break; case NUMERICOID: /* * Float-looking constants will be typed as numeric, which we * checked above; but if there's a nondefault typmod we need to * show it. */ needlabel |= (constval->consttypmod >= 0); break; default: needlabel = true; break; } if (needlabel || showtype > 0) appendStringInfo(buf, "::%s", format_type_with_typemod(constval->consttype, constval->consttypmod)); get_const_collation(constval, context); } /* * helper for get_const_expr: append COLLATE if needed */ static void get_const_collation(Const *constval, deparse_context *context) { StringInfo buf = context->buf; if (OidIsValid(constval->constcollid)) { Oid typcollation = get_typcollation(constval->consttype); if (constval->constcollid != typcollation) { appendStringInfo(buf, " COLLATE %s", generate_collation_name(constval->constcollid)); } } } /* * simple_quote_literal - Format a string as a SQL literal, append to buf */ static void simple_quote_literal(StringInfo buf, const char *val) { const char *valptr; /* * We form the string literal according to the prevailing setting of * standard_conforming_strings; we never use E''. User is responsible for * making sure result is used correctly. */ appendStringInfoChar(buf, '\''); for (valptr = val; *valptr; valptr++) { char ch = *valptr; if (SQL_STR_DOUBLE(ch, !standard_conforming_strings)) appendStringInfoChar(buf, ch); appendStringInfoChar(buf, ch); } appendStringInfoChar(buf, '\''); } /* ---------- * get_sublink_expr - Parse back a sublink * ---------- */ static void get_sublink_expr(SubLink *sublink, deparse_context *context) { StringInfo buf = context->buf; Query *query = (Query *) (sublink->subselect); char *opname = NULL; bool need_paren; if (sublink->subLinkType == ARRAY_SUBLINK) appendStringInfoString(buf, "ARRAY("); else appendStringInfoChar(buf, '('); /* * Note that we print the name of only the first operator, when there are * multiple combining operators. This is an approximation that could go * wrong in various scenarios (operators in different schemas, renamed * operators, etc) but there is not a whole lot we can do about it, since * the syntax allows only one operator to be shown. */ if (sublink->testexpr) { if (IsA(sublink->testexpr, OpExpr)) { /* single combining operator */ OpExpr *opexpr = (OpExpr *) sublink->testexpr; get_rule_expr(linitial(opexpr->args), context, true); opname = generate_operator_name(opexpr->opno, exprType(linitial(opexpr->args)), exprType(lsecond(opexpr->args))); } else if (IsA(sublink->testexpr, BoolExpr)) { /* multiple combining operators, = or <> cases */ char *sep; ListCell *l; appendStringInfoChar(buf, '('); sep = ""; foreach(l, ((BoolExpr *) sublink->testexpr)->args) { OpExpr *opexpr = lfirst_node(OpExpr, l); appendStringInfoString(buf, sep); get_rule_expr(linitial(opexpr->args), context, true); if (!opname) opname = generate_operator_name(opexpr->opno, exprType(linitial(opexpr->args)), exprType(lsecond(opexpr->args))); sep = ", "; } appendStringInfoChar(buf, ')'); } else if (IsA(sublink->testexpr, RowCompareExpr)) { /* multiple combining operators, < <= > >= cases */ RowCompareExpr *rcexpr = (RowCompareExpr *) sublink->testexpr; appendStringInfoChar(buf, '('); get_rule_expr((Node *) rcexpr->largs, context, true); opname = generate_operator_name(linitial_oid(rcexpr->opnos), exprType(linitial(rcexpr->largs)), exprType(linitial(rcexpr->rargs))); appendStringInfoChar(buf, ')'); } else elog(ERROR, "unrecognized testexpr type: %d", (int) nodeTag(sublink->testexpr)); } need_paren = true; switch (sublink->subLinkType) { case EXISTS_SUBLINK: appendStringInfoString(buf, "EXISTS "); break; case ANY_SUBLINK: if (strcmp(opname, "=") == 0) /* Represent = ANY as IN */ appendStringInfoString(buf, " IN "); else appendStringInfo(buf, " %s ANY ", opname); break; case ALL_SUBLINK: appendStringInfo(buf, " %s ALL ", opname); break; case ROWCOMPARE_SUBLINK: appendStringInfo(buf, " %s ", opname); break; case EXPR_SUBLINK: case MULTIEXPR_SUBLINK: case ARRAY_SUBLINK: need_paren = false; break; case CTE_SUBLINK: /* shouldn't occur in a SubLink */ default: elog(ERROR, "unrecognized sublink type: %d", (int) sublink->subLinkType); break; } if (need_paren) appendStringInfoChar(buf, '('); get_query_def(query, buf, context->namespaces, NULL, false, context->prettyFlags, context->wrapColumn, context->indentLevel); if (need_paren) appendStringInfoString(buf, "))"); else appendStringInfoChar(buf, ')'); } /* ---------- * get_tablefunc - Parse back a table function * ---------- */ static void get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; /* XMLTABLE is the only existing implementation. */ appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) { ListCell *lc1, *lc2; bool first = true; appendStringInfoString(buf, "XMLNAMESPACES ("); forboth(lc1, tf->ns_uris, lc2, tf->ns_names) { Node *expr = (Node *) lfirst(lc1); Value *ns_node = (Value *) lfirst(lc2); if (!first) appendStringInfoString(buf, ", "); else first = false; if (ns_node != NULL) { get_rule_expr(expr, context, showimplicit); appendStringInfo(buf, " AS %s", strVal(ns_node)); } else { appendStringInfoString(buf, "DEFAULT "); get_rule_expr(expr, context, showimplicit); } } appendStringInfoString(buf, "), "); } appendStringInfoChar(buf, '('); get_rule_expr((Node *) tf->rowexpr, context, showimplicit); appendStringInfoString(buf, ") PASSING ("); get_rule_expr((Node *) tf->docexpr, context, showimplicit); appendStringInfoChar(buf, ')'); if (tf->colexprs != NIL) { ListCell *l1; ListCell *l2; ListCell *l3; ListCell *l4; ListCell *l5; int colnum = 0; appendStringInfoString(buf, " COLUMNS "); forfive(l1, tf->colnames, l2, tf->coltypes, l3, tf->coltypmods, l4, tf->colexprs, l5, tf->coldefexprs) { char *colname = strVal(lfirst(l1)); Oid typid = lfirst_oid(l2); int32 typmod = lfirst_int(l3); Node *colexpr = (Node *) lfirst(l4); Node *coldefexpr = (Node *) lfirst(l5); bool ordinality = (tf->ordinalitycol == colnum); bool notnull = bms_is_member(colnum, tf->notnulls); if (colnum > 0) appendStringInfoString(buf, ", "); colnum++; appendStringInfo(buf, "%s %s", quote_identifier(colname), ordinality ? "FOR ORDINALITY" : format_type_with_typemod(typid, typmod)); if (ordinality) continue; if (coldefexpr != NULL) { appendStringInfoString(buf, " DEFAULT ("); get_rule_expr((Node *) coldefexpr, context, showimplicit); appendStringInfoChar(buf, ')'); } if (colexpr != NULL) { appendStringInfoString(buf, " PATH ("); get_rule_expr((Node *) colexpr, context, showimplicit); appendStringInfoChar(buf, ')'); } if (notnull) appendStringInfoString(buf, " NOT NULL"); } } appendStringInfoChar(buf, ')'); } /* ---------- * get_from_clause - Parse back a FROM clause * * "prefix" is the keyword that denotes the start of the list of FROM * elements. It is FROM when used to parse back SELECT and UPDATE, but * is USING when parsing back DELETE. * ---------- */ static void get_from_clause(Query *query, const char *prefix, deparse_context *context) { StringInfo buf = context->buf; bool first = true; ListCell *l; /* * We use the query's jointree as a guide to what to print. However, we * must ignore auto-added RTEs that are marked not inFromCl. (These can * only appear at the top level of the jointree, so it's sufficient to * check here.) This check also ensures we ignore the rule pseudo-RTEs * for NEW and OLD. */ foreach(l, query->jointree->fromlist) { Node *jtnode = (Node *) lfirst(l); if (IsA(jtnode, RangeTblRef)) { int varno = ((RangeTblRef *) jtnode)->rtindex; RangeTblEntry *rte = rt_fetch(varno, query->rtable); if (!rte->inFromCl) continue; } if (first) { appendContextKeyword(context, prefix, -PRETTYINDENT_STD, PRETTYINDENT_STD, 2); first = false; get_from_clause_item(jtnode, query, context); } else { StringInfoData itembuf; appendStringInfoString(buf, ", "); /* * Put the new FROM item's text into itembuf so we can decide * after we've got it whether or not it needs to go on a new line. */ initStringInfo(&itembuf); context->buf = &itembuf; get_from_clause_item(jtnode, query, context); /* Restore context's output buffer */ context->buf = buf; /* Consider line-wrapping if enabled */ if (PRETTY_INDENT(context) && context->wrapColumn >= 0) { /* Does the new item start with a new line? */ if (itembuf.len > 0 && itembuf.data[0] == '\n') { /* If so, we shouldn't add anything */ /* instead, remove any trailing spaces currently in buf */ removeStringInfoSpaces(buf); } else { char *trailing_nl; /* Locate the start of the current line in the buffer */ trailing_nl = strrchr(buf->data, '\n'); if (trailing_nl == NULL) trailing_nl = buf->data; else trailing_nl++; /* * Add a newline, plus some indentation, if the new item * would cause an overflow. */ if (strlen(trailing_nl) + itembuf.len > context->wrapColumn) appendContextKeyword(context, "", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_VAR); } } /* Add the new item */ appendBinaryStringInfo(buf, itembuf.data, itembuf.len); /* clean up */ pfree(itembuf.data); } } } static void get_from_clause_item(Node *jtnode, Query *query, deparse_context *context) { StringInfo buf = context->buf; deparse_namespace *dpns = (deparse_namespace *) linitial(context->namespaces); if (IsA(jtnode, RangeTblRef)) { int varno = ((RangeTblRef *) jtnode)->rtindex; RangeTblEntry *rte = rt_fetch(varno, query->rtable); char *refname = get_rtable_name(varno, context); deparse_columns *colinfo = deparse_columns_fetch(varno, dpns); RangeTblFunction *rtfunc1 = NULL; bool printalias; if (rte->lateral) appendStringInfoString(buf, "LATERAL "); /* Print the FROM item proper */ switch (rte->rtekind) { case RTE_RELATION: /* Normal relation RTE */ appendStringInfo(buf, "%s%s", only_marker(rte), generate_relation_name(rte->relid, context->namespaces)); break; case RTE_SUBQUERY: /* Subquery RTE */ appendStringInfoChar(buf, '('); get_query_def(rte->subquery, buf, context->namespaces, NULL, true, context->prettyFlags, context->wrapColumn, context->indentLevel); appendStringInfoChar(buf, ')'); break; case RTE_FUNCTION: /* Function RTE */ rtfunc1 = (RangeTblFunction *) linitial(rte->functions); /* * Omit ROWS FROM() syntax for just one function, unless it * has both a coldeflist and WITH ORDINALITY. If it has both, * we must use ROWS FROM() syntax to avoid ambiguity about * whether the coldeflist includes the ordinality column. */ if (list_length(rte->functions) == 1 && (rtfunc1->funccolnames == NIL || !rte->funcordinality)) { get_rule_expr_funccall(rtfunc1->funcexpr, context, true); /* we'll print the coldeflist below, if it has one */ } else { bool all_unnest; ListCell *lc; /* * If all the function calls in the list are to unnest, * and none need a coldeflist, then collapse the list back * down to UNNEST(args). (If we had more than one * built-in unnest function, this would get more * difficult.) * * XXX This is pretty ugly, since it makes not-terribly- * future-proof assumptions about what the parser would do * with the output; but the alternative is to emit our * nonstandard ROWS FROM() notation for what might have * been a perfectly spec-compliant multi-argument * UNNEST(). */ all_unnest = true; foreach(lc, rte->functions) { RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc); if (!IsA(rtfunc->funcexpr, FuncExpr) || ((FuncExpr *) rtfunc->funcexpr)->funcid != F_UNNEST_ANYARRAY || rtfunc->funccolnames != NIL) { all_unnest = false; break; } } if (all_unnest) { List *allargs = NIL; foreach(lc, rte->functions) { RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc); List *args = ((FuncExpr *) rtfunc->funcexpr)->args; allargs = list_concat(allargs, args); } appendStringInfoString(buf, "UNNEST("); get_rule_expr((Node *) allargs, context, true); appendStringInfoChar(buf, ')'); } else { int funcno = 0; appendStringInfoString(buf, "ROWS FROM("); foreach(lc, rte->functions) { RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc); if (funcno > 0) appendStringInfoString(buf, ", "); get_rule_expr_funccall(rtfunc->funcexpr, context, true); if (rtfunc->funccolnames != NIL) { /* Reconstruct the column definition list */ appendStringInfoString(buf, " AS "); get_from_clause_coldeflist(rtfunc, NULL, context); } funcno++; } appendStringInfoChar(buf, ')'); } /* prevent printing duplicate coldeflist below */ rtfunc1 = NULL; } if (rte->funcordinality) appendStringInfoString(buf, " WITH ORDINALITY"); break; case RTE_TABLEFUNC: get_tablefunc(rte->tablefunc, context, true); break; case RTE_VALUES: /* Values list RTE */ appendStringInfoChar(buf, '('); get_values_def(rte->values_lists, context); appendStringInfoChar(buf, ')'); break; case RTE_CTE: appendStringInfoString(buf, quote_identifier(rte->ctename)); break; default: elog(ERROR, "unrecognized RTE kind: %d", (int) rte->rtekind); break; } /* Print the relation alias, if needed */ printalias = false; if (rte->alias != NULL) { /* Always print alias if user provided one */ printalias = true; } else if (colinfo->printaliases) { /* Always print alias if we need to print column aliases */ printalias = true; } else if (rte->rtekind == RTE_RELATION) { /* * No need to print alias if it's same as relation name (this * would normally be the case, but not if set_rtable_names had to * resolve a conflict). */ if (strcmp(refname, get_relation_name(rte->relid)) != 0) printalias = true; } else if (rte->rtekind == RTE_FUNCTION) { /* * For a function RTE, always print alias. This covers possible * renaming of the function and/or instability of the * FigureColname rules for things that aren't simple functions. * Note we'd need to force it anyway for the columndef list case. */ printalias = true; } else if (rte->rtekind == RTE_VALUES) { /* Alias is syntactically required for VALUES */ printalias = true; } else if (rte->rtekind == RTE_CTE) { /* * No need to print alias if it's same as CTE name (this would * normally be the case, but not if set_rtable_names had to * resolve a conflict). */ if (strcmp(refname, rte->ctename) != 0) printalias = true; } if (printalias) appendStringInfo(buf, " %s", quote_identifier(refname)); /* Print the column definitions or aliases, if needed */ if (rtfunc1 && rtfunc1->funccolnames != NIL) { /* Reconstruct the columndef list, which is also the aliases */ get_from_clause_coldeflist(rtfunc1, colinfo, context); } else { /* Else print column aliases as needed */ get_column_alias_list(colinfo, context); } /* Tablesample clause must go after any alias */ if (rte->rtekind == RTE_RELATION && rte->tablesample) get_tablesample_def(rte->tablesample, context); } else if (IsA(jtnode, JoinExpr)) { JoinExpr *j = (JoinExpr *) jtnode; deparse_columns *colinfo = deparse_columns_fetch(j->rtindex, dpns); bool need_paren_on_right; need_paren_on_right = PRETTY_PAREN(context) && !IsA(j->rarg, RangeTblRef) && !(IsA(j->rarg, JoinExpr) && ((JoinExpr *) j->rarg)->alias != NULL); if (!PRETTY_PAREN(context) || j->alias != NULL) appendStringInfoChar(buf, '('); get_from_clause_item(j->larg, query, context); switch (j->jointype) { case JOIN_INNER: if (j->quals) appendContextKeyword(context, " JOIN ", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_JOIN); else appendContextKeyword(context, " CROSS JOIN ", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_JOIN); break; case JOIN_LEFT: appendContextKeyword(context, " LEFT JOIN ", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_JOIN); break; case JOIN_FULL: appendContextKeyword(context, " FULL JOIN ", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_JOIN); break; case JOIN_RIGHT: appendContextKeyword(context, " RIGHT JOIN ", -PRETTYINDENT_STD, PRETTYINDENT_STD, PRETTYINDENT_JOIN); break; default: elog(ERROR, "unrecognized join type: %d", (int) j->jointype); } if (need_paren_on_right) appendStringInfoChar(buf, '('); get_from_clause_item(j->rarg, query, context); if (need_paren_on_right) appendStringInfoChar(buf, ')'); if (j->usingClause) { ListCell *lc; bool first = true; appendStringInfoString(buf, " USING ("); /* Use the assigned names, not what's in usingClause */ foreach(lc, colinfo->usingNames) { char *colname = (char *) lfirst(lc); if (first) first = false; else appendStringInfoString(buf, ", "); appendStringInfoString(buf, quote_identifier(colname)); } appendStringInfoChar(buf, ')'); if (j->join_using_alias) appendStringInfo(buf, " AS %s", quote_identifier(j->join_using_alias->aliasname)); } else if (j->quals) { appendStringInfoString(buf, " ON "); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr(j->quals, context, false); if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); } else if (j->jointype != JOIN_INNER) { /* If we didn't say CROSS JOIN above, we must provide an ON */ appendStringInfoString(buf, " ON TRUE"); } if (!PRETTY_PAREN(context) || j->alias != NULL) appendStringInfoChar(buf, ')'); /* Yes, it's correct to put alias after the right paren ... */ if (j->alias != NULL) { /* * Note that it's correct to emit an alias clause if and only if * there was one originally. Otherwise we'd be converting a named * join to unnamed or vice versa, which creates semantic * subtleties we don't want. However, we might print a different * alias name than was there originally. */ appendStringInfo(buf, " %s", quote_identifier(get_rtable_name(j->rtindex, context))); get_column_alias_list(colinfo, context); } } else elog(ERROR, "unrecognized node type: %d", (int) nodeTag(jtnode)); } /* * get_column_alias_list - print column alias list for an RTE * * Caller must already have printed the relation's alias name. */ static void get_column_alias_list(deparse_columns *colinfo, deparse_context *context) { StringInfo buf = context->buf; int i; bool first = true; /* Don't print aliases if not needed */ if (!colinfo->printaliases) return; for (i = 0; i < colinfo->num_new_cols; i++) { char *colname = colinfo->new_colnames[i]; if (first) { appendStringInfoChar(buf, '('); first = false; } else appendStringInfoString(buf, ", "); appendStringInfoString(buf, quote_identifier(colname)); } if (!first) appendStringInfoChar(buf, ')'); } /* * get_from_clause_coldeflist - reproduce FROM clause coldeflist * * When printing a top-level coldeflist (which is syntactically also the * relation's column alias list), use column names from colinfo. But when * printing a coldeflist embedded inside ROWS FROM(), we prefer to use the * original coldeflist's names, which are available in rtfunc->funccolnames. * Pass NULL for colinfo to select the latter behavior. * * The coldeflist is appended immediately (no space) to buf. Caller is * responsible for ensuring that an alias or AS is present before it. */ static void get_from_clause_coldeflist(RangeTblFunction *rtfunc, deparse_columns *colinfo, deparse_context *context) { StringInfo buf = context->buf; ListCell *l1; ListCell *l2; ListCell *l3; ListCell *l4; int i; appendStringInfoChar(buf, '('); i = 0; forfour(l1, rtfunc->funccoltypes, l2, rtfunc->funccoltypmods, l3, rtfunc->funccolcollations, l4, rtfunc->funccolnames) { Oid atttypid = lfirst_oid(l1); int32 atttypmod = lfirst_int(l2); Oid attcollation = lfirst_oid(l3); char *attname; if (colinfo) attname = colinfo->colnames[i]; else attname = strVal(lfirst(l4)); Assert(attname); /* shouldn't be any dropped columns here */ if (i > 0) appendStringInfoString(buf, ", "); appendStringInfo(buf, "%s %s", quote_identifier(attname), format_type_with_typemod(atttypid, atttypmod)); if (OidIsValid(attcollation) && attcollation != get_typcollation(atttypid)) appendStringInfo(buf, " COLLATE %s", generate_collation_name(attcollation)); i++; } appendStringInfoChar(buf, ')'); } /* * get_tablesample_def - print a TableSampleClause */ static void get_tablesample_def(TableSampleClause *tablesample, deparse_context *context) { StringInfo buf = context->buf; Oid argtypes[1]; int nargs; ListCell *l; /* * We should qualify the handler's function name if it wouldn't be * resolved by lookup in the current search path. */ argtypes[0] = INTERNALOID; appendStringInfo(buf, " TABLESAMPLE %s (", generate_function_name(tablesample->tsmhandler, 1, NIL, argtypes, false, NULL, EXPR_KIND_NONE)); nargs = 0; foreach(l, tablesample->args) { if (nargs++ > 0) appendStringInfoString(buf, ", "); get_rule_expr((Node *) lfirst(l), context, false); } appendStringInfoChar(buf, ')'); if (tablesample->repeatable != NULL) { appendStringInfoString(buf, " REPEATABLE ("); get_rule_expr((Node *) tablesample->repeatable, context, false); appendStringInfoChar(buf, ')'); } } /* * get_opclass_name - fetch name of an index operator class * * The opclass name is appended (after a space) to buf. * * Output is suppressed if the opclass is the default for the given * actual_datatype. (If you don't want this behavior, just pass * InvalidOid for actual_datatype.) */ static void get_opclass_name(Oid opclass, Oid actual_datatype, StringInfo buf) { HeapTuple ht_opc; Form_pg_opclass opcrec; char *opcname; char *nspname; ht_opc = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclass)); if (!HeapTupleIsValid(ht_opc)) elog(ERROR, "cache lookup failed for opclass %u", opclass); opcrec = (Form_pg_opclass) GETSTRUCT(ht_opc); if (!OidIsValid(actual_datatype) || GetDefaultOpClass(actual_datatype, opcrec->opcmethod) != opclass) { /* Okay, we need the opclass name. Do we need to qualify it? */ opcname = NameStr(opcrec->opcname); if (OpclassIsVisible(opclass)) appendStringInfo(buf, " %s", quote_identifier(opcname)); else { nspname = get_namespace_name(opcrec->opcnamespace); appendStringInfo(buf, " %s.%s", quote_identifier(nspname), quote_identifier(opcname)); } } ReleaseSysCache(ht_opc); } /* * generate_opclass_name * Compute the name to display for a opclass specified by OID * * The result includes all necessary quoting and schema-prefixing. */ char * generate_opclass_name(Oid opclass) { StringInfoData buf; initStringInfo(&buf); get_opclass_name(opclass, InvalidOid, &buf); return &buf.data[1]; /* get_opclass_name() prepends space */ } /* * processIndirection - take care of array and subfield assignment * * We strip any top-level FieldStore or assignment SubscriptingRef nodes that * appear in the input, printing them as decoration for the base column * name (which we assume the caller just printed). We might also need to * strip CoerceToDomain nodes, but only ones that appear above assignment * nodes. * * Returns the subexpression that's to be assigned. */ static Node * processIndirection(Node *node, deparse_context *context) { StringInfo buf = context->buf; CoerceToDomain *cdomain = NULL; for (;;) { if (node == NULL) break; if (IsA(node, FieldStore)) { FieldStore *fstore = (FieldStore *) node; Oid typrelid; char *fieldname; /* lookup tuple type */ typrelid = get_typ_typrelid(fstore->resulttype); if (!OidIsValid(typrelid)) elog(ERROR, "argument type %s of FieldStore is not a tuple type", format_type_be(fstore->resulttype)); /* * Print the field name. There should only be one target field in * stored rules. There could be more than that in executable * target lists, but this function cannot be used for that case. */ Assert(list_length(fstore->fieldnums) == 1); fieldname = get_attname(typrelid, linitial_int(fstore->fieldnums), false); appendStringInfo(buf, ".%s", quote_identifier(fieldname)); /* * We ignore arg since it should be an uninteresting reference to * the target column or subcolumn. */ node = (Node *) linitial(fstore->newvals); } else if (IsA(node, SubscriptingRef)) { SubscriptingRef *sbsref = (SubscriptingRef *) node; if (sbsref->refassgnexpr == NULL) break; printSubscripts(sbsref, context); /* * We ignore refexpr since it should be an uninteresting reference * to the target column or subcolumn. */ node = (Node *) sbsref->refassgnexpr; } else if (IsA(node, CoerceToDomain)) { cdomain = (CoerceToDomain *) node; /* If it's an explicit domain coercion, we're done */ if (cdomain->coercionformat != COERCE_IMPLICIT_CAST) break; /* Tentatively descend past the CoerceToDomain */ node = (Node *) cdomain->arg; } else break; } /* * If we descended past a CoerceToDomain whose argument turned out not to * be a FieldStore or array assignment, back up to the CoerceToDomain. * (This is not enough to be fully correct if there are nested implicit * CoerceToDomains, but such cases shouldn't ever occur.) */ if (cdomain && node == (Node *) cdomain->arg) node = (Node *) cdomain; return node; } static void printSubscripts(SubscriptingRef *sbsref, deparse_context *context) { StringInfo buf = context->buf; ListCell *lowlist_item; ListCell *uplist_item; lowlist_item = list_head(sbsref->reflowerindexpr); /* could be NULL */ foreach(uplist_item, sbsref->refupperindexpr) { appendStringInfoChar(buf, '['); if (lowlist_item) { /* If subexpression is NULL, get_rule_expr prints nothing */ get_rule_expr((Node *) lfirst(lowlist_item), context, false); appendStringInfoChar(buf, ':'); lowlist_item = lnext(sbsref->reflowerindexpr, lowlist_item); } /* If subexpression is NULL, get_rule_expr prints nothing */ get_rule_expr((Node *) lfirst(uplist_item), context, false); appendStringInfoChar(buf, ']'); } } /* * quote_identifier - Quote an identifier only if needed * * When quotes are needed, we palloc the required space; slightly * space-wasteful but well worth it for notational simplicity. */ const char * quote_identifier(const char *ident) { /* * Can avoid quoting if ident starts with a lowercase letter or underscore * and contains only lowercase letters, digits, and underscores, *and* is * not any SQL keyword. Otherwise, supply quotes. */ int nquotes = 0; bool safe; const char *ptr; char *result; char *optr; /* * would like to use macros here, but they might yield unwanted * locale-specific results... */ safe = ((ident[0] >= 'a' && ident[0] <= 'z') || ident[0] == '_'); for (ptr = ident; *ptr; ptr++) { char ch = *ptr; if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || (ch == '_')) { /* okay */ } else { safe = false; if (ch == '"') nquotes++; } } if (quote_all_identifiers) safe = false; if (safe) { /* * Check for keyword. We quote keywords except for unreserved ones. * (In some cases we could avoid quoting a col_name or type_func_name * keyword, but it seems much harder than it's worth to tell that.) * * Note: ScanKeywordLookup() does case-insensitive comparison, but * that's fine, since we already know we have all-lower-case. */ int kwnum = ScanKeywordLookup(ident, &ScanKeywords); if (kwnum >= 0 && ScanKeywordCategories[kwnum] != UNRESERVED_KEYWORD) safe = false; } if (safe) return ident; /* no change needed */ result = (char *) palloc(strlen(ident) + nquotes + 2 + 1); optr = result; *optr++ = '"'; for (ptr = ident; *ptr; ptr++) { char ch = *ptr; if (ch == '"') *optr++ = '"'; *optr++ = ch; } *optr++ = '"'; *optr = '\0'; return result; } /* * quote_qualified_identifier - Quote a possibly-qualified identifier * * Return a name of the form qualifier.ident, or just ident if qualifier * is NULL, quoting each component if necessary. The result is palloc'd. */ char * quote_qualified_identifier(const char *qualifier, const char *ident) { StringInfoData buf; initStringInfo(&buf); if (qualifier) appendStringInfo(&buf, "%s.", quote_identifier(qualifier)); appendStringInfoString(&buf, quote_identifier(ident)); return buf.data; } /* * get_relation_name * Get the unqualified name of a relation specified by OID * * This differs from the underlying get_rel_name() function in that it will * throw error instead of silently returning NULL if the OID is bad. */ static char * get_relation_name(Oid relid) { char *relname = get_rel_name(relid); if (!relname) elog(ERROR, "cache lookup failed for relation %u", relid); return relname; } /* * generate_relation_name * Compute the name to display for a relation specified by OID * * The result includes all necessary quoting and schema-prefixing. * * If namespaces isn't NIL, it must be a list of deparse_namespace nodes. * We will forcibly qualify the relation name if it equals any CTE name * visible in the namespace list. */ static char * generate_relation_name(Oid relid, List *namespaces) { HeapTuple tp; Form_pg_class reltup; bool need_qual; ListCell *nslist; char *relname; char *nspname; char *result; tp = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); if (!HeapTupleIsValid(tp)) elog(ERROR, "cache lookup failed for relation %u", relid); reltup = (Form_pg_class) GETSTRUCT(tp); relname = NameStr(reltup->relname); /* Check for conflicting CTE name */ need_qual = false; foreach(nslist, namespaces) { deparse_namespace *dpns = (deparse_namespace *) lfirst(nslist); ListCell *ctlist; foreach(ctlist, dpns->ctes) { CommonTableExpr *cte = (CommonTableExpr *) lfirst(ctlist); if (strcmp(cte->ctename, relname) == 0) { need_qual = true; break; } } if (need_qual) break; } /* Otherwise, qualify the name if not visible in search path */ if (!need_qual) need_qual = !RelationIsVisible(relid); if (need_qual) nspname = get_namespace_name(reltup->relnamespace); else nspname = NULL; result = quote_qualified_identifier(nspname, relname); ReleaseSysCache(tp); return result; } /* * generate_function_name * Compute the name to display for a function specified by OID, * given that it is being called with the specified actual arg names and * types. (Those matter because of ambiguous-function resolution rules.) * * If we're dealing with a potentially variadic function (in practice, this * means a FuncExpr or Aggref, not some other way of calling a function), then * has_variadic must specify whether variadic arguments have been merged, * and *use_variadic_p will be set to indicate whether to print VARIADIC in * the output. For non-FuncExpr cases, has_variadic should be false and * use_variadic_p can be NULL. * * The result includes all necessary quoting and schema-prefixing. */ static char * generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes, bool has_variadic, bool *use_variadic_p, ParseExprKind special_exprkind) { char *result; HeapTuple proctup; Form_pg_proc procform; char *proname; bool use_variadic; char *nspname; FuncDetailCode p_result; Oid p_funcid; Oid p_rettype; bool p_retset; int p_nvargs; Oid p_vatype; Oid *p_true_typeids; bool force_qualify = false; proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid)); if (!HeapTupleIsValid(proctup)) elog(ERROR, "cache lookup failed for function %u", funcid); procform = (Form_pg_proc) GETSTRUCT(proctup); proname = NameStr(procform->proname); /* * Due to parser hacks to avoid needing to reserve CUBE, we need to force * qualification in some special cases. */ if (special_exprkind == EXPR_KIND_GROUP_BY) { if (strcmp(proname, "cube") == 0 || strcmp(proname, "rollup") == 0) force_qualify = true; } /* * Determine whether VARIADIC should be printed. We must do this first * since it affects the lookup rules in func_get_detail(). * * We always print VARIADIC if the function has a merged variadic-array * argument. Note that this is always the case for functions taking a * VARIADIC argument type other than VARIADIC ANY. If we omitted VARIADIC * and printed the array elements as separate arguments, the call could * match a newer non-VARIADIC function. */ if (use_variadic_p) { /* Parser should not have set funcvariadic unless fn is variadic */ Assert(!has_variadic || OidIsValid(procform->provariadic)); use_variadic = has_variadic; *use_variadic_p = use_variadic; } else { Assert(!has_variadic); use_variadic = false; } /* * The idea here is to schema-qualify only if the parser would fail to * resolve the correct function given the unqualified func name with the * specified argtypes and VARIADIC flag. But if we already decided to * force qualification, then we can skip the lookup and pretend we didn't * find it. */ if (!force_qualify) p_result = func_get_detail(list_make1(makeString(proname)), NIL, argnames, nargs, argtypes, !use_variadic, true, false, &p_funcid, &p_rettype, &p_retset, &p_nvargs, &p_vatype, &p_true_typeids, NULL); else { p_result = FUNCDETAIL_NOTFOUND; p_funcid = InvalidOid; } if ((p_result == FUNCDETAIL_NORMAL || p_result == FUNCDETAIL_AGGREGATE || p_result == FUNCDETAIL_WINDOWFUNC) && p_funcid == funcid) nspname = NULL; else nspname = get_namespace_name(procform->pronamespace); result = quote_qualified_identifier(nspname, proname); ReleaseSysCache(proctup); return result; } /* * generate_operator_name * Compute the name to display for an operator specified by OID, * given that it is being called with the specified actual arg types. * (Arg types matter because of ambiguous-operator resolution rules. * Pass InvalidOid for unused arg of a unary operator.) * * The result includes all necessary quoting and schema-prefixing, * plus the OPERATOR() decoration needed to use a qualified operator name * in an expression. */ static char * generate_operator_name(Oid operid, Oid arg1, Oid arg2) { StringInfoData buf; HeapTuple opertup; Form_pg_operator operform; char *oprname; char *nspname; Operator p_result; initStringInfo(&buf); opertup = SearchSysCache1(OPEROID, ObjectIdGetDatum(operid)); if (!HeapTupleIsValid(opertup)) elog(ERROR, "cache lookup failed for operator %u", operid); operform = (Form_pg_operator) GETSTRUCT(opertup); oprname = NameStr(operform->oprname); /* * The idea here is to schema-qualify only if the parser would fail to * resolve the correct operator given the unqualified op name with the * specified argtypes. */ switch (operform->oprkind) { case 'b': p_result = oper(NULL, list_make1(makeString(oprname)), arg1, arg2, true, -1); break; case 'l': p_result = left_oper(NULL, list_make1(makeString(oprname)), arg2, true, -1); break; default: elog(ERROR, "unrecognized oprkind: %d", operform->oprkind); p_result = NULL; /* keep compiler quiet */ break; } if (p_result != NULL && oprid(p_result) == operid) nspname = NULL; else { nspname = get_namespace_name(operform->oprnamespace); appendStringInfo(&buf, "OPERATOR(%s.", quote_identifier(nspname)); } appendStringInfoString(&buf, oprname); if (nspname) appendStringInfoChar(&buf, ')'); if (p_result != NULL) ReleaseSysCache(p_result); ReleaseSysCache(opertup); return buf.data; } pg_ivm-1.13/specs/000077500000000000000000000000001507512346500140445ustar00rootroot00000000000000pg_ivm-1.13/specs/create_insert.spec000066400000000000000000000026721507512346500175560ustar00rootroot00000000000000# Test interaction between concurrent transactions performing # create_immv and insert in READ COMMITTED isolation level setup { CREATE TABLE a (i int); INSERT INTO a VALUES (1); CREATE VIEW v(x,y) AS SELECT * FROM a AS a1, a AS a2 WHERE a1.i = a2.i; } teardown { DROP FUNCTION check_mv(); DROP TABLE mv; DROP VIEW v; DROP TABLE a; } session tx1 setup { BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; } step s1 { SELECT; } step create { SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; } step check1 {SELECT check_mv();} step c1 { COMMIT; } step mv { SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); } session tx2 setup { BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; } step s2 { SELECT; } step insert { INSERT INTO a VALUES (2); } step check2 { SELECT check_mv(); } step c2 { COMMIT; } permutation s1 create s2 insert c1 check2 c2 mv permutation s1 create s2 c1 insert check2 c2 mv permutation s1 s2 create insert c1 check2 c2 mv permutation s1 s2 insert create c2 check1 c1 mv permutation s1 s2 create c1 insert check2 c2 mv permutation s2 insert s1 create c2 check1 c1 mv permutation s2 insert s1 c2 create check1 c1 mv permutation s2 s1 insert c2 create check1 c1 mv pg_ivm-1.13/specs/create_insert2.spec000066400000000000000000000033641507512346500176370ustar00rootroot00000000000000# Test interaction between concurrent transactions performing # create_immv and insert in REPEATABLE READ isolation level # # Note: # In this isolation level, it is possible that create_immv could # create an inconsistent view not including effects of a concurrent # transaction. So, an warning message is raised to suggest using it # in READ COMMITTED or executing refresh_immv to make sure to # make the view contents consistent. setup { CREATE TABLE a (i int); INSERT INTO a VALUES (1); CREATE VIEW v(x,y) AS SELECT * FROM a AS a1, a AS a2 WHERE a1.i = a2.i; } teardown { DROP FUNCTION check_mv(); DROP TABLE mv; DROP VIEW v; DROP TABLE a; } session tx1 setup { BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; } step s1 { SELECT; } step create { SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; } step check1 {SELECT check_mv();} step c1 { COMMIT; } step mv { SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); } session tx2 setup { BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; } step s2 { SELECT; } step insert { INSERT INTO a VALUES (2); } step check2 {SELECT check_mv(); } step c2 { COMMIT; } permutation s1 create s2 insert c1 check2 c2 mv permutation s1 create s2 c1 insert check2 c2 mv permutation s1 s2 create insert c1 check2 c2 mv permutation s1 s2 insert create c2 check1 c1 mv permutation s1 s2 create c1 insert check2 c2 mv permutation s2 insert s1 create c2 check1 c1 mv permutation s2 insert s1 c2 create check1 c1 mv permutation s2 s1 insert c2 create check1 c1 mv pg_ivm-1.13/specs/create_insert3.spec000066400000000000000000000033531507512346500176360ustar00rootroot00000000000000# Test interaction between concurrent transactions performing # create_immv and insert in SERIALIZABLE isolation level # # Note: # In this isolation level, it is possible that create_immv could # create an inconsistent view not including effects of a concurrent # transaction. So, an warning message is raised to suggest using it # in READ COMMITTED or executing refresh_immv to make sure to # make the view contents consistent. setup { CREATE TABLE a (i int); INSERT INTO a VALUES (1); CREATE VIEW v(x,y) AS SELECT * FROM a AS a1, a AS a2 WHERE a1.i = a2.i; } teardown { DROP FUNCTION check_mv(); DROP TABLE mv; DROP VIEW v; DROP TABLE a; } session tx1 setup { BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; } step s1 { SELECT; } step create { SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; } step check1 {SELECT check_mv();} step c1 { COMMIT; } step mv { SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); } session tx2 setup { BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; } step s2 { SELECT; } step insert { INSERT INTO a VALUES (2); } step check2 {SELECT check_mv(); } step c2 { COMMIT; } permutation s1 create s2 insert c1 check2 c2 mv permutation s1 create s2 c1 insert check2 c2 mv permutation s1 s2 create insert c1 check2 c2 mv permutation s1 s2 insert create c2 check1 c1 mv permutation s1 s2 create c1 insert check2 c2 mv permutation s2 insert s1 create c2 check1 c1 mv permutation s2 insert s1 c2 create check1 c1 mv permutation s2 s1 insert c2 create check1 c1 mv pg_ivm-1.13/specs/insert_insert.spec000066400000000000000000000043101507512346500176060ustar00rootroot00000000000000# Test interaction between concurrent transactions performing # table modifications in READ COMMITTED isolation level setup { CREATE TABLE a (i int, j int); CREATE TABLE b (i int, j int); INSERT INTO a VALUES (1,10); INSERT INTO b VALUES (1,100); SELECT pgivm.create_immv('mv(x,y,z)', 'SELECT a1.j, a2.j,b.j FROM a AS a1, a AS a2,b WHERE a1.i = a2.i AND a1.i = b.i'); CREATE VIEW v(x,y,z) AS SELECT a1.j, a2.j,b.j FROM a AS a1, a AS a2,b WHERE a1.i = a2.i AND a1.i = b.i; CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; } teardown { DROP FUNCTION check_mv(); DROP TABLE mv; DROP VIEW v; DROP TABLE a; DROP TABLE b; } session tx1 setup { BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; } step s1 { SELECT; } step insert1 { INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); } step update1 { UPDATE a SET j = 11 WHERE i = 1; } step check1 { SELECT check_mv();} step c1 { COMMIT; } step mv { SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); } session tx2 setup { BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; } step s2 { SELECT; } step insert2 { INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); } step update2 { UPDATE b SET j = 111 WHERE i = 1; } step check2 { SELECT check_mv(); } step c2 { COMMIT; } permutation s1 update1 s2 update2 c1 check2 c2 mv permutation s1 update1 s2 c1 update2 check2 c2 mv permutation s1 s2 update1 update2 c1 check2 c2 mv permutation s1 s2 update2 update1 c2 check1 c1 mv permutation s1 s2 update1 c1 update2 check2 c2 mv permutation s2 update2 s1 update1 c2 check1 c1 mv permutation s2 update2 s1 c2 update1 check1 c1 mv permutation s2 s1 update2 c2 update1 check1 c1 mv permutation s1 insert1 s2 insert2 c1 check2 c2 mv permutation s1 insert1 s2 c1 insert2 check2 c2 mv permutation s1 s2 insert1 insert2 c1 check2 c2 mv permutation s1 s2 insert2 insert1 c2 check1 c1 mv permutation s1 s2 insert1 c1 insert2 check2 c2 mv permutation s2 insert2 s1 insert1 c2 check1 c1 mv permutation s2 insert2 s1 c2 insert1 check1 c1 mv permutation s2 s1 insert2 c2 insert1 check1 c1 mv pg_ivm-1.13/specs/insert_insert2.spec000066400000000000000000000043131507512346500176730ustar00rootroot00000000000000# Test interaction between concurrent transactions performing # table modifications in REPEATABLE READ isolation level setup { CREATE TABLE a (i int, j int); CREATE TABLE b (i int, j int); INSERT INTO a VALUES (1,10); INSERT INTO b VALUES (1,100); SELECT pgivm.create_immv('mv(x,y,z)', 'SELECT a1.j, a2.j,b.j FROM a AS a1, a AS a2,b WHERE a1.i = a2.i AND a1.i = b.i'); CREATE VIEW v(x,y,z) AS SELECT a1.j, a2.j,b.j FROM a AS a1, a AS a2,b WHERE a1.i = a2.i AND a1.i = b.i; CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; } teardown { DROP FUNCTION check_mv(); DROP TABLE mv; DROP VIEW v; DROP TABLE a; DROP TABLE b; } session tx1 setup { BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; } step s1 { SELECT; } step insert1 { INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); } step update1 { UPDATE a SET j = 11 WHERE i = 1; } step check1 { SELECT check_mv();} step c1 { COMMIT; } step mv { SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); } session tx2 setup { BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; } step s2 { SELECT; } step insert2 { INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); } step update2 { UPDATE b SET j = 111 WHERE i = 1; } step check2 { SELECT check_mv(); } step c2 { COMMIT; } permutation s1 update1 s2 update2 c1 check2 c2 mv permutation s1 update1 s2 c1 update2 check2 c2 mv permutation s1 s2 update1 update2 c1 check2 c2 mv permutation s1 s2 update2 update1 c2 check1 c1 mv permutation s1 s2 update1 c1 update2 check2 c2 mv permutation s2 update2 s1 update1 c2 check1 c1 mv permutation s2 update2 s1 c2 update1 check1 c1 mv permutation s2 s1 update2 c2 update1 check1 c1 mv permutation s1 insert1 s2 insert2 c1 check2 c2 mv permutation s1 insert1 s2 c1 insert2 check2 c2 mv permutation s1 s2 insert1 insert2 c1 check2 c2 mv permutation s1 s2 insert2 insert1 c2 check1 c1 mv permutation s1 s2 insert1 c1 insert2 check2 c2 mv permutation s2 insert2 s1 insert1 c2 check1 c1 mv permutation s2 insert2 s1 c2 insert1 check1 c1 mv permutation s2 s1 insert2 c2 insert1 check1 c1 mv pg_ivm-1.13/specs/insert_insert3.spec000066400000000000000000000043021507512346500176720ustar00rootroot00000000000000# Test interaction between concurrent transactions performing # table modifications in SERIALIZABLE isolation level setup { CREATE TABLE a (i int, j int); CREATE TABLE b (i int, j int); INSERT INTO a VALUES (1,10); INSERT INTO b VALUES (1,100); SELECT pgivm.create_immv('mv(x,y,z)', 'SELECT a1.j, a2.j,b.j FROM a AS a1, a AS a2,b WHERE a1.i = a2.i AND a1.i = b.i'); CREATE VIEW v(x,y,z) AS SELECT a1.j, a2.j,b.j FROM a AS a1, a AS a2,b WHERE a1.i = a2.i AND a1.i = b.i; CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; } teardown { DROP FUNCTION check_mv(); DROP TABLE mv; DROP VIEW v; DROP TABLE a; DROP TABLE b; } session tx1 setup { BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; } step s1 { SELECT; } step insert1 { INSERT INTO a VALUES (2,20); INSERT INTO b VALUES (2,200); } step update1 { UPDATE a SET j = 11 WHERE i = 1; } step check1 { SELECT check_mv();} step c1 { COMMIT; } step mv { SELECT * FROM mv ORDER BY 1,2,3; SELECT check_mv(); } session tx2 setup { BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; } step s2 { SELECT; } step insert2 { INSERT INTO a VALUES (1,11), (2,21); INSERT INTO b VALUES (2,201); } step update2 { UPDATE b SET j = 111 WHERE i = 1; } step check2 { SELECT check_mv(); } step c2 { COMMIT; } permutation s1 update1 s2 update2 c1 check2 c2 mv permutation s1 update1 s2 c1 update2 check2 c2 mv permutation s1 s2 update1 update2 c1 check2 c2 mv permutation s1 s2 update2 update1 c2 check1 c1 mv permutation s1 s2 update1 c1 update2 check2 c2 mv permutation s2 update2 s1 update1 c2 check1 c1 mv permutation s2 update2 s1 c2 update1 check1 c1 mv permutation s2 s1 update2 c2 update1 check1 c1 mv permutation s1 insert1 s2 insert2 c1 check2 c2 mv permutation s1 insert1 s2 c1 insert2 check2 c2 mv permutation s1 s2 insert1 insert2 c1 check2 c2 mv permutation s1 s2 insert2 insert1 c2 check1 c1 mv permutation s1 s2 insert1 c1 insert2 check2 c2 mv permutation s2 insert2 s1 insert1 c2 check1 c1 mv permutation s2 insert2 s1 c2 insert1 check1 c1 mv permutation s2 s1 insert2 c2 insert1 check1 c1 mv pg_ivm-1.13/specs/refresh_insert.spec000066400000000000000000000027541507512346500177520ustar00rootroot00000000000000# Test interaction between concurrent transactions performing # refresh_immv and insert in READ COMMITTED isolation level setup { CREATE TABLE a (i int); INSERT INTO a VALUES (1); SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE VIEW v(x,y) AS SELECT * FROM a AS a1, a AS a2 WHERE a1.i = a2.i; CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; } teardown { DROP FUNCTION check_mv(); DROP TABLE mv; DROP VIEW v; DROP TABLE a; } session tx1 setup { BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; } step s1 { SELECT; } step refresh { SELECT pgivm.refresh_immv('mv', true); } step check1 {SELECT check_mv();} step c1 { COMMIT; } step mv { SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); } session tx2 setup { BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; } step s2 { SELECT; } step insert { INSERT INTO a VALUES (2); } step check2 {SELECT check_mv(); } step c2 { COMMIT; } permutation s1 refresh s2 insert c1 check2 c2 mv permutation s1 refresh s2 c1 insert check2 c2 mv permutation s1 s2 refresh insert c1 check2 c2 mv permutation s1 s2 insert refresh c2 check1 c1 mv permutation s1 s2 refresh c1 insert check2 c2 mv permutation s2 insert s1 refresh c2 check1 c1 mv permutation s2 insert s1 c2 refresh check1 c1 mv permutation s2 s1 insert c2 refresh check1 c1 mv pg_ivm-1.13/specs/refresh_insert2.spec000066400000000000000000000027571507512346500200370ustar00rootroot00000000000000# Test interaction between concurrent transactions performing # refresh_immv and insert in REPEATABLE READ isolation level setup { CREATE TABLE a (i int); INSERT INTO a VALUES (1); SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE VIEW v(x,y) AS SELECT * FROM a AS a1, a AS a2 WHERE a1.i = a2.i; CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; } teardown { DROP FUNCTION check_mv(); DROP TABLE mv; DROP VIEW v; DROP TABLE a; } session tx1 setup { BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; } step s1 { SELECT; } step refresh { SELECT pgivm.refresh_immv('mv', true); } step check1 {SELECT check_mv();} step c1 { COMMIT; } step mv { SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); } session tx2 setup { BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ; } step s2 { SELECT; } step insert { INSERT INTO a VALUES (2); } step check2 {SELECT check_mv(); } step c2 { COMMIT; } permutation s1 refresh s2 insert c1 check2 c2 mv permutation s1 refresh s2 c1 insert check2 c2 mv permutation s1 s2 refresh insert c1 check2 c2 mv permutation s1 s2 insert refresh c2 check1 c1 mv permutation s1 s2 refresh c1 insert check2 c2 mv permutation s2 insert s1 refresh c2 check1 c1 mv permutation s2 insert s1 c2 refresh check1 c1 mv permutation s2 s1 insert c2 refresh check1 c1 mv pg_ivm-1.13/specs/refresh_insert3.spec000066400000000000000000000027461507512346500200360ustar00rootroot00000000000000# Test interaction between concurrent transactions performing # refresh_immv and insert in SERIALIZABLE isolation level setup { CREATE TABLE a (i int); INSERT INTO a VALUES (1); SELECT pgivm.create_immv('mv(x,y)', 'SELECT * FROM a a1, a a2 WHERE a1.i = a2.i'); CREATE VIEW v(x,y) AS SELECT * FROM a AS a1, a AS a2 WHERE a1.i = a2.i; CREATE FUNCTION check_mv() RETURNS text AS $$ SELECT CASE WHEN count(*) = 0 THEN 'ok' ELSE 'ng' END FROM ((SELECT * FROM mv EXCEPT ALL SELECT * FROM v) UNION ALL (SELECT * FROM v EXCEPT ALL SELECT * FROM mv)) v $$ LANGUAGE sql; } teardown { DROP FUNCTION check_mv(); DROP TABLE mv; DROP VIEW v; DROP TABLE a; } session tx1 setup { BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; } step s1 { SELECT; } step refresh { SELECT pgivm.refresh_immv('mv', true); } step check1 {SELECT check_mv();} step c1 { COMMIT; } step mv { SELECT * FROM mv ORDER BY 1,2; SELECT check_mv(); } session tx2 setup { BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; } step s2 { SELECT; } step insert { INSERT INTO a VALUES (2); } step check2 {SELECT check_mv(); } step c2 { COMMIT; } permutation s1 refresh s2 insert c1 check2 c2 mv permutation s1 refresh s2 c1 insert check2 c2 mv permutation s1 s2 refresh insert c1 check2 c2 mv permutation s1 s2 insert refresh c2 check1 c1 mv permutation s1 s2 refresh c1 insert check2 c2 mv permutation s2 insert s1 refresh c2 check1 c1 mv permutation s2 insert s1 c2 refresh check1 c1 mv permutation s2 s1 insert c2 refresh check1 c1 mv pg_ivm-1.13/sql/000077500000000000000000000000001507512346500135265ustar00rootroot00000000000000pg_ivm-1.13/sql/create_immv.sql000066400000000000000000000021631507512346500165440ustar00rootroot00000000000000CREATE TABLE t (i int PRIMARY KEY); INSERT INTO t SELECT generate_series(1, 100); SELECT pgivm.create_immv('mv', 'SELECT * FROM t'); SELECT pgivm.create_immv(' mv2 ( x ) ', 'SELECT * FROM t WHERE i%2 = 0'); SELECT pgivm.create_immv('mv3', 'WITH d AS (DELETE FROM t RETURNING NULL) SELECT * FROM t'); SELECT immvrelid, pgivm.get_immv_def(immvrelid) FROM pgivm.pg_ivm_immv ORDER BY 1; -- contain immv SELECT pgivm.create_immv('mv_in_immv01', 'SELECT i FROM mv'); SELECT pgivm.create_immv('mv_in_immv02', 'SELECT t.i FROM t INNER JOIN mv2 ON t.i = mv2.x'); -- SQL other than SELECT SELECT pgivm.create_immv('mv_in_create', 'CREATE TABLE in_create(i int)'); SELECT pgivm.create_immv('mv_in_insert', 'INSERT INTO t VALUES(10)'); SELECT pgivm.create_immv('mv_in_update', 'UPDATE t SET i = 10'); SELECT pgivm.create_immv('mv_in_delete', 'DELETE FROM t'); SELECT pgivm.create_immv('mv_in_drop', 'DROP TABLE t'); DROP TABLE t; DROP TABLE mv; SELECT immvrelid, pgivm.get_immv_def(immvrelid) FROM pgivm.pg_ivm_immv ORDER BY 1; DROP TABLE mv2; SELECT immvrelid, pgivm.get_immv_def(immvrelid) FROM pgivm.pg_ivm_immv ORDER BY 1; DROP TABLE t; pg_ivm-1.13/sql/pg_ivm.sql000066400000000000000000000655321507512346500155430ustar00rootroot00000000000000CREATE EXTENSION pg_ivm; GRANT ALL ON SCHEMA public TO public; -- create a table to use as a basis for views and materialized views in various combinations CREATE TABLE mv_base_a (x int, i int, y int, j int); CREATE TABLE mv_base_b (x int, i int, y int, k int); -- test for base tables with dropped columns ALTER TABLE mv_base_a DROP COLUMN x; ALTER TABLE mv_base_a DROP COLUMN y; ALTER TABLE mv_base_b DROP COLUMN x; ALTER TABLE mv_base_b DROP COLUMN y; INSERT INTO mv_base_a VALUES (1,10), (2,20), (3,30), (4,40), (5,50); INSERT INTO mv_base_b VALUES (1,101), (2,102), (3,103), (4,104); SELECT pgivm.create_immv('mv_ivm_1', 'SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i)'); SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; -- immediate maintenance BEGIN; INSERT INTO mv_base_b VALUES(5,105); SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; UPDATE mv_base_a SET j = 0 WHERE i = 1; SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; DELETE FROM mv_base_b WHERE (i,k) = (5,105); SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; ROLLBACK; SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; -- test for renaming column name to camel style BEGIN; ALTER TABLE mv_base_a RENAME i TO "I"; ALTER TABLE mv_base_a RENAME j TO "J"; UPDATE mv_base_a SET "J" = 0 WHERE "I" = 1; SELECT * FROM mv_ivm_1 ORDER BY 1,2,3; ROLLBACK; -- TRUNCATE a base table in join views BEGIN; TRUNCATE mv_base_a; SELECT * FROM mv_ivm_1; ROLLBACK; BEGIN; TRUNCATE mv_base_b; SELECT * FROM mv_ivm_1; ROLLBACK; -- some query syntax BEGIN; CREATE FUNCTION ivm_func() RETURNS int LANGUAGE 'sql' AS 'SELECT 1' IMMUTABLE; SELECT pgivm.create_immv('mv_ivm_func', 'SELECT * FROM ivm_func()'); SELECT pgivm.create_immv('mv_ivm_no_tbl', 'SELECT 1'); ROLLBACK; -- result of materialized view have DISTINCT clause or the duplicate result. BEGIN; SELECT pgivm.create_immv('mv_ivm_duplicate', 'SELECT j FROM mv_base_a'); SELECT pgivm.create_immv('mv_ivm_distinct', 'SELECT DISTINCT j FROM mv_base_a'); INSERT INTO mv_base_a VALUES(6,20); SELECT * FROM mv_ivm_duplicate ORDER BY 1; SELECT * FROM mv_ivm_distinct ORDER BY 1; DELETE FROM mv_base_a WHERE (i,j) = (2,20); SELECT * FROM mv_ivm_duplicate ORDER BY 1; SELECT * FROM mv_ivm_distinct ORDER BY 1; ROLLBACK; -- support SUM(), COUNT() and AVG() aggregate functions BEGIN; SELECT pgivm.create_immv('mv_ivm_agg', 'SELECT i, SUM(j), COUNT(i), AVG(j) FROM mv_base_a GROUP BY i'); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; INSERT INTO mv_base_a VALUES(2,100); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; UPDATE mv_base_a SET j = 200 WHERE (i,j) = (2,100); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; DELETE FROM mv_base_a WHERE (i,j) = (2,200); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4; ROLLBACK; -- support COUNT(*) aggregate function BEGIN; SELECT pgivm.create_immv('mv_ivm_agg', 'SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i'); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3; INSERT INTO mv_base_a VALUES(2,100); SELECT * FROM mv_ivm_agg ORDER BY 1,2,3; ROLLBACK; -- TRUNCATE a base table in aggregate views BEGIN; SELECT pgivm.create_immv('mv_ivm_agg', 'SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i'); TRUNCATE mv_base_a; SELECT sum, count FROM mv_ivm_agg; SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i; ROLLBACK; -- support aggregate functions without GROUP clause BEGIN; SELECT pgivm.create_immv('mv_ivm_group', 'SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a'); SELECT * FROM mv_ivm_group ORDER BY 1; INSERT INTO mv_base_a VALUES(6,60); SELECT * FROM mv_ivm_group ORDER BY 1; DELETE FROM mv_base_a; SELECT * FROM mv_ivm_group ORDER BY 1; ROLLBACK; -- TRUNCATE a base table in aggregate views without GROUP clause BEGIN; SELECT pgivm.create_immv('mv_ivm_group', 'SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a'); TRUNCATE mv_base_a; SELECT sum, count, avg FROM mv_ivm_group; SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a; ROLLBACK; -- resolved issue: When use AVG() function and values is indivisible, result of AVG() is incorrect. BEGIN; SELECT pgivm.create_immv('mv_ivm_avg_bug', 'SELECT i, SUM(j), COUNT(j), AVG(j) FROM mv_base_A GROUP BY i'); SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3; INSERT INTO mv_base_a VALUES (1,0), (1,0), (2,30), (2,30); SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3; DELETE FROM mv_base_a WHERE (i,j) = (1,0); DELETE FROM mv_base_a WHERE (i,j) = (2,30); SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3; ROLLBACK; -- support MIN(), MAX() aggregate functions BEGIN; SELECT pgivm.create_immv('mv_ivm_min_max(i, min_j, max_j)', 'SELECT i, MIN(j), MAX(j) FROM mv_base_a GROUP BY i'); SELECT * FROM mv_ivm_min_max ORDER BY 1,2,3; INSERT INTO mv_base_a VALUES (1,11), (1,12), (2,21), (2,22), (3,31), (3,32), (4,41), (4,42), (5,51), (5,52); SELECT * FROM mv_ivm_min_max ORDER BY 1,2,3; DELETE FROM mv_base_a WHERE (i,j) IN ((1,10), (2,21), (3,32)); SELECT * FROM mv_ivm_min_max ORDER BY 1,2,3; ROLLBACK; -- support MIN(), MAX() aggregate functions without GROUP clause BEGIN; SELECT pgivm.create_immv('mv_ivm_min_max(min_j, max_j)', 'SELECT MIN(j), MAX(j) FROM mv_base_a'); SELECT * FROM mv_ivm_min_max; INSERT INTO mv_base_a VALUES (0,0), (6,60), (7,70); SELECT * FROM mv_ivm_min_max; DELETE FROM mv_base_a WHERE (i,j) IN ((0,0), (7,70)); SELECT * FROM mv_ivm_min_max; DELETE FROM mv_base_a; SELECT * FROM mv_ivm_min_max; ROLLBACK; -- Test MIN/MAX after search_path change BEGIN; SELECT pgivm.create_immv('mv_ivm_min', 'SELECT MIN(j) FROM mv_base_a'); SELECT * FROM mv_ivm_min ORDER BY 1,2,3; CREATE SCHEMA myschema; GRANT ALL ON SCHEMA myschema TO public; CREATE TABLE myschema.mv_base_a (j int); INSERT INTO myschema.mv_base_a VALUES (1); DELETE FROM mv_base_a WHERE (i,j) = (1,10); SELECT * FROM mv_ivm_min ORDER BY 1,2,3; SET search_path TO myschema,public,pg_catalog; DELETE FROM public.mv_base_a WHERE (i,j) = (2,20); SELECT * FROM mv_ivm_min ORDER BY 1,2,3; ROLLBACK; -- aggregate views with column names specified BEGIN; SELECT pgivm.create_immv('mv_ivm_agg(a)', 'SELECT i, SUM(j) FROM mv_base_a GROUP BY i'); INSERT INTO mv_base_a VALUES (1,100), (2,200), (3,300); UPDATE mv_base_a SET j = 2000 WHERE (i,j) = (2,20); DELETE FROM mv_base_a WHERE (i,j) = (3,30); SELECT * FROM mv_ivm_agg ORDER BY 1,2; ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_ivm_agg(a,b)', 'SELECT i, SUM(j) FROM mv_base_a GROUP BY i'); INSERT INTO mv_base_a VALUES (1,100), (2,200), (3,300); UPDATE mv_base_a SET j = 2000 WHERE (i,j) = (2,20); DELETE FROM mv_base_a WHERE (i,j) = (3,30); SELECT * FROM mv_ivm_agg ORDER BY 1,2; ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_ivm_agg(a,b,c)', 'SELECT i, SUM(j) FROM mv_base_a GROUP BY i'); ROLLBACK; -- support self join view and multiple change on the same table BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (1, 10), (2, 20), (3, 30); SELECT pgivm.create_immv('mv_self(v1, v2)', 'SELECT t1.v, t2.v FROM base_t AS t1 JOIN base_t AS t2 ON t1.i = t2.i'); SELECT * FROM mv_self ORDER BY v1; INSERT INTO base_t VALUES (4,40); DELETE FROM base_t WHERE i = 1; UPDATE base_t SET v = v*10 WHERE i=2; SELECT * FROM mv_self ORDER BY v1; WITH ins_t1 AS (INSERT INTO base_t VALUES (5,50) RETURNING 1), ins_t2 AS (INSERT INTO base_t VALUES (6,60) RETURNING 1), upd_t AS (UPDATE base_t SET v = v + 100 RETURNING 1), dlt_t AS (DELETE FROM base_t WHERE i IN (4,5) RETURNING 1) SELECT NULL; SELECT * FROM mv_self ORDER BY v1; --- with sub-transactions SAVEPOINT p1; INSERT INTO base_t VALUES (7,70); RELEASE SAVEPOINT p1; INSERT INTO base_t VALUES (7,77); SELECT * FROM mv_self ORDER BY v1, v2; ROLLBACK; -- support simultaneous table changes BEGIN; CREATE TABLE base_r (i int, v int); CREATE TABLE base_s (i int, v int); INSERT INTO base_r VALUES (1, 10), (2, 20), (3, 30); INSERT INTO base_s VALUES (1, 100), (2, 200), (3, 300); SELECT pgivm.create_immv('mv(v1, v2)', 'SELECT r.v, s.v FROM base_r AS r JOIN base_s AS s USING(i)');; SELECT * FROM mv ORDER BY v1; WITH ins_r AS (INSERT INTO base_r VALUES (1,11) RETURNING 1), ins_r2 AS (INSERT INTO base_r VALUES (3,33) RETURNING 1), ins_s AS (INSERT INTO base_s VALUES (2,222) RETURNING 1), upd_r AS (UPDATE base_r SET v = v + 1000 WHERE i = 2 RETURNING 1), dlt_s AS (DELETE FROM base_s WHERE i = 3 RETURNING 1) SELECT NULL; SELECT * FROM mv ORDER BY v1; -- support foreign reference constraints BEGIN; CREATE TABLE ri1 (i int PRIMARY KEY); CREATE TABLE ri2 (i int PRIMARY KEY REFERENCES ri1(i) ON UPDATE CASCADE ON DELETE CASCADE, v int); INSERT INTO ri1 VALUES (1),(2),(3); INSERT INTO ri2 VALUES (1),(2),(3); SELECT pgivm.create_immv('mv_ri(i1, i2)', 'SELECT ri1.i, ri2.i FROM ri1 JOIN ri2 USING(i)'); SELECT * FROM mv_ri ORDER BY i1; UPDATE ri1 SET i=10 where i=1; DELETE FROM ri1 WHERE i=2; SELECT * FROM mv_ri ORDER BY i2; ROLLBACK; -- support subquery for using EXISTS() BEGIN; SELECT pgivm.create_immv('mv_ivm_exists_subquery', 'SELECT a.i, a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); SELECT pgivm.create_immv('mv_ivm_exists_subquery2', 'SELECT a.i,a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) AND a.i > 2'); SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; INSERT INTO mv_base_a VALUES(1,10),(6,60),(3,30),(3,300); SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; INSERT INTO mv_base_b VALUES(1,101); INSERT INTO mv_base_b VALUES(1,111); INSERT INTO mv_base_b VALUES(2,102); INSERT INTO mv_base_b VALUES(6,106); SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; UPDATE mv_base_a SET i = 1 WHERE j =60; UPDATE mv_base_b SET i = 10 WHERE k = 101; UPDATE mv_base_b SET k = 1002 WHERE k = 102; SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; DELETE FROM mv_base_a WHERE (i,j) = (1,60); DELETE FROM mv_base_b WHERE i = 2; SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; SELECT * FROM mv_ivm_exists_subquery2 ORDER BY i, j; --- EXISTS subquery with tuple duplication and DISTINCT SELECT pgivm.create_immv('mv_ivm_exists_subquery_distinct', 'SELECT DISTINCT a.i, a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); DELETE FROM mv_base_b WHERE i = 1 or i = 3; INSERT INTO mv_base_b VALUES (1,100), (3,300); SELECT * FROM mv_ivm_exists_subquery ORDER BY i, j; SELECT * FROM mv_ivm_exists_subquery_distinct ORDER BY i, j; ROLLBACK; -- support simple subquery in FROM clause BEGIN; SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a,( SELECT * FROM mv_base_b) b WHERE a.i = b.i'); INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_ivm_subquery ORDER BY i,j; ROLLBACK; -- disallow non-simple subqueries SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a, (SELECT i, COUNT(*) FROM mv_base_b GROUP BY i) b WHERE a.i = b.i'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a, (SELECT DISTINCT i FROM mv_base_b) b WHERE a.i = b.i'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT i,j FROM mv_base_a WHERE i IN (SELECT i FROM mv_base_b WHERE k < 103 )'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT i,j, (SELECT k FROM mv_base_b LIMIT 1) FROM mv_base_a a'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT i,j, (SELECT k FROM mv_base_b LIMIT 1) + 1 FROM mv_base_a a'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM generate_series(1, (SELECT k FROM mv_base_b LIMIT 1)) AS v'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.i,a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) OR a.i > 2'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_a a2 WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a2.i = b.i))'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT EXISTS(SELECT 1 from mv_base_b) FROM mv_base_a a'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT false OR EXISTS(SELECT 1 FROM mv_base_a) FROM mv_base_b'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM generate_series(1, CASE EXISTS(SELECT 1 FROM mv_base_a) WHEN true THEN 100 ELSE 10 END), mv_base_b'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM mv_base_a a WHERE CASE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) WHEN true THEN false ELSE true END'); SELECT pgivm.create_immv('mv_ivm_subquery', 'SELECT * FROM mv_base_a a WHERE true and CASE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) WHEN true THEN false ELSE true END'); -- support join subquery in FROM clause BEGIN; SELECT pgivm.create_immv('mv_ivm_join_subquery', 'SELECT i, j, k FROM ( SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN mv_base_a a USING(i)) tmp'); WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; SELECT * FROM mv_ivm_join_subquery ORDER BY i,j,k; ROLLBACK; BEGIN; -- nested subquery SELECT pgivm.create_immv('mv_ivm_join_subquery', 'SELECT i, j, k FROM ( SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN (SELECT * FROM mv_base_a) a USING(i)) tmp'); WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; SELECT * FROM mv_ivm_join_subquery ORDER BY i,j,k; ROLLBACK; -- support simple CTE BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH b AS ( SELECT * FROM mv_base_b) SELECT a.i,a.j FROM mv_base_a a, b WHERE a.i = b.i'); INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH a AS (SELECT * FROM mv_base_a), b AS ( SELECT * FROM mv_base_b) SELECT a.i,a.j FROM a, b WHERE a.i = b.i'); INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH b AS ( SELECT * FROM mv_base_b) SELECT v.i,v.j FROM (WITH a AS (SELECT * FROM mv_base_a) SELECT a.i,a.j FROM a, b WHERE a.i = b.i) v'); INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'SELECT * FROM (WITH a AS (SELECT * FROM mv_base_a), b AS ( SELECT * FROM mv_base_b) SELECT a.i,a.j FROM a, b WHERE a.i = b.i) v'); INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH b AS ( SELECT * FROM (SELECT * FROM mv_base_b) b2) SELECT v.i,v.j FROM (WITH a AS (SELECT * FROM mv_base_a) SELECT a.i,a.j FROM a, b WHERE a.i = b.i) v'); INSERT INTO mv_base_a VALUES(2,20); INSERT INTO mv_base_b VALUES(3,300); SELECT * FROM mv_cte ORDER BY i,j; ROLLBACK; BEGIN; SELECT pgivm.create_immv('mv_cte', 'WITH x AS ( SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN mv_base_a a USING(i)) SELECT * FROM x'); WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; SELECT * FROM mv_cte ORDER BY i,j,k; ROLLBACK; -- nested CTE BEGIN; SELECT pgivm.create_immv('mv_ivm_nested_cte', 'WITH v AS ( WITH a AS (SELECT * FROM mv_base_a) SELECT i, a.j, b.k FROM mv_base_b b INNER JOIN a USING(i)) SELECT * FROM v'); WITH ai AS (INSERT INTO mv_base_a VALUES (1,11),(2,22) RETURNING 0), bi AS (INSERT INTO mv_base_b VALUES (1,111),(3,133) RETURNING 0), bd AS (DELETE FROM mv_base_b WHERE i = 4 RETURNING 0) SELECT; SELECT * FROM mv_ivm_nested_cte ORDER BY i,j,k; ROLLBACK; -- Multiply-referenced CTE BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (1, 10), (2, 20), (3, 30); SELECT pgivm.create_immv('mv_cte_multi(v1, v2)', 'WITH t AS (SELECT * FROM base_t) SELECT t1.v, t2.v FROM t AS t1 JOIN t AS t2 ON t1.i = t2.i'); SELECT * FROM mv_cte_multi ORDER BY v1; INSERT INTO base_t VALUES (4,40); DELETE FROM base_t WHERE i = 1; UPDATE base_t SET v = v*10 WHERE i=2; SELECT * FROM mv_cte_multi ORDER BY v1; WITH ins_t1 AS (INSERT INTO base_t VALUES (5,50) RETURNING 1), ins_t2 AS (INSERT INTO base_t VALUES (6,60) RETURNING 1), upd_t AS (UPDATE base_t SET v = v + 100 RETURNING 1), dlt_t AS (DELETE FROM base_t WHERE i IN (4,5) RETURNING 1) SELECT NULL; SELECT * FROM mv_cte_multi ORDER BY v1; ROLLBACK; --- disallow not-simple CTE SELECT pgivm.create_immv('mv_cte_fail', 'WITH b AS (SELECT i, COUNT(*) FROM mv_base_b GROUP BY i) SELECT a.i,a.j FROM mv_base_a a, b WHERE a.i = b.i'); SELECT pgivm.create_immv('mv_cte_fail', 'WITH b AS (SELECT DISTINCT i FROM mv_base_b) SELECT a.i,a.j FROM mv_base_a a, b WHERE a.i = b.i'); SELECT pgivm.create_immv('mv_cte_fail', 'WITH a AS (SELECT i, j FROM mv_base_a) SELECT a.i,a.j FROM a WHERE EXISTS(WITH b AS (SELECT i FROM mv_base_b) SELECT 1 FROM b WHERE a.i = b.i)'); -- unreferenced CTE SELECT pgivm.create_immv('mv_cte_fail', 'WITH b AS (SELECT * FROM mv_base_b) SELECT * FROM mv_base_a a'); -- views including NULL BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (1,10),(2, NULL); SELECT pgivm.create_immv('mv', 'SELECT * FROM base_t'); SELECT * FROM mv ORDER BY i; UPDATE base_t SET v = 20 WHERE i = 2; SELECT * FROM mv ORDER BY i; ROLLBACK; BEGIN; CREATE TABLE base_t (i int); SELECT pgivm.create_immv('mv', 'SELECT * FROM base_t'); SELECT * FROM mv ORDER BY i; INSERT INTO base_t VALUES (1),(NULL); SELECT * FROM mv ORDER BY i; ROLLBACK; BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (NULL, 1), (NULL, 2), (1, 10), (1, 20); SELECT pgivm.create_immv('mv', 'SELECT i, sum(v) FROM base_t GROUP BY i'); SELECT * FROM mv ORDER BY i; UPDATE base_t SET v = v * 10; SELECT * FROM mv ORDER BY i; ROLLBACK; BEGIN; CREATE TABLE base_t (i int, v int); INSERT INTO base_t VALUES (NULL, 1), (NULL, 2), (NULL, 3), (NULL, 4), (NULL, 5); SELECT pgivm.create_immv('mv', 'SELECT i, min(v), max(v) FROM base_t GROUP BY i'); SELECT * FROM mv ORDER BY i; DELETE FROM base_t WHERE v = 1; SELECT * FROM mv ORDER BY i; DELETE FROM base_t WHERE v = 3; SELECT * FROM mv ORDER BY i; DELETE FROM base_t WHERE v = 5; SELECT * FROM mv ORDER BY i; ROLLBACK; -- IMMV containing user defined type BEGIN; CREATE TYPE mytype; CREATE FUNCTION mytype_in(cstring) RETURNS mytype AS 'int4in' LANGUAGE INTERNAL STRICT IMMUTABLE; CREATE FUNCTION mytype_out(mytype) RETURNS cstring AS 'int4out' LANGUAGE INTERNAL STRICT IMMUTABLE; CREATE TYPE mytype ( LIKE = int4, INPUT = mytype_in, OUTPUT = mytype_out ); CREATE FUNCTION mytype_eq(mytype, mytype) RETURNS bool AS 'int4eq' LANGUAGE INTERNAL STRICT IMMUTABLE; CREATE FUNCTION mytype_lt(mytype, mytype) RETURNS bool AS 'int4lt' LANGUAGE INTERNAL STRICT IMMUTABLE; CREATE FUNCTION mytype_cmp(mytype, mytype) RETURNS integer AS 'btint4cmp' LANGUAGE INTERNAL STRICT IMMUTABLE; CREATE OPERATOR = ( leftarg = mytype, rightarg = mytype, procedure = mytype_eq); CREATE OPERATOR < ( leftarg = mytype, rightarg = mytype, procedure = mytype_lt); CREATE OPERATOR CLASS mytype_ops DEFAULT FOR TYPE mytype USING btree AS OPERATOR 1 <, OPERATOR 3 = , FUNCTION 1 mytype_cmp(mytype,mytype); CREATE TABLE t_mytype (x mytype); SELECT pgivm.create_immv('mv_mytype', 'SELECT * FROM t_mytype'); INSERT INTO t_mytype VALUES ('1'::mytype); SELECT * FROM mv_mytype; ROLLBACK; -- contain system column SELECT pgivm.create_immv('mv_ivm01', 'SELECT i,j,xmin FROM mv_base_a'); SELECT pgivm.create_immv('mv_ivm02', 'SELECT i,j FROM mv_base_a WHERE xmin = ''610'''); SELECT pgivm.create_immv('mv_ivm03', 'SELECT i,j,xmin::text AS x_min FROM mv_base_a'); SELECT pgivm.create_immv('mv_ivm04', 'SELECT i,j,xidsend(xmin) AS x_min FROM mv_base_a'); -- targetlist or WHERE clause without EXISTS contain subquery SELECT pgivm.create_immv('mv_ivm05', 'SELECT i,j FROM mv_base_a WHERE i IN (SELECT i FROM mv_base_b WHERE k < 103 )'); SELECT pgivm.create_immv('mv_ivm05', 'SELECT i,j, (SELECT k FROM mv_base_b b WHERE a.i = b.i) FROM mv_base_a a'); -- contain ORDER BY SELECT pgivm.create_immv('mv_ivm07', 'SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) ORDER BY i,j,k'); -- contain HAVING SELECT pgivm.create_immv('mv_ivm08', 'SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) GROUP BY i,j,k HAVING SUM(i) > 5'); -- contain GROUP BY without aggregate SELECT pgivm.create_immv('mv_ivm08', 'SELECT i,j FROM mv_base_a GROUP BY i,j'); -- contain view or materialized view CREATE VIEW b_view AS SELECT i,k FROM mv_base_b; CREATE MATERIALIZED VIEW b_mview AS SELECT i,k FROM mv_base_b; SELECT pgivm.create_immv('mv_ivm07', 'SELECT a.i,a.j FROM mv_base_a a,b_view b WHERE a.i = b.i'); SELECT pgivm.create_immv('mv_ivm08', 'SELECT a.i,a.j FROM mv_base_a a,b_mview b WHERE a.i = b.i'); -- contain mutable functions SELECT pgivm.create_immv('mv_ivm12', 'SELECT i,j FROM mv_base_a WHERE i = random()::int'); -- LIMIT/OFFSET is not supported SELECT pgivm.create_immv('mv_ivm13', 'SELECT i,j FROM mv_base_a LIMIT 10 OFFSET 5'); -- DISTINCT ON is not supported SELECT pgivm.create_immv('mv_ivm14', 'SELECT DISTINCT ON(i) i, j FROM mv_base_a'); -- TABLESAMPLE clause is not supported SELECT pgivm.create_immv('mv_ivm15', 'SELECT i, j FROM mv_base_a TABLESAMPLE SYSTEM(50)'); -- window functions are not supported SELECT pgivm.create_immv('mv_ivm16', 'SELECT i, j FROM (SELECT *, cume_dist() OVER (ORDER BY i) AS rank FROM mv_base_a) AS t'); -- aggregate function with some options is not supported SELECT pgivm.create_immv('mv_ivm17', 'SELECT COUNT(*) FILTER(WHERE i < 3) FROM mv_base_a'); SELECT pgivm.create_immv('mv_ivm18', 'SELECT COUNT(DISTINCT i) FROM mv_base_a'); SELECT pgivm.create_immv('mv_ivm19', 'SELECT array_agg(j ORDER BY i DESC) FROM mv_base_a'); SELECT pgivm.create_immv('mv_ivm20', 'SELECT i,SUM(j) FROM mv_base_a GROUP BY GROUPING SETS((i),())'); -- inheritance parent is not supported BEGIN; CREATE TABLE parent (i int, v int); CREATE TABLE child_a(options text) INHERITS(parent); SELECT pgivm.create_immv('mv_ivm21', 'SELECT * FROM parent'); ROLLBACK; -- UNION statement is not supported SELECT pgivm.create_immv('mv_ivm22', 'SELECT i,j FROM mv_base_a UNION ALL SELECT i,k FROM mv_base_b'); -- DISTINCT clause in nested query are not supported SELECT pgivm.create_immv('mv_ivm23', 'SELECT * FROM (SELECT DISTINCT i,j FROM mv_base_a) AS tmp');; -- empty target list is not allowed with IVM SELECT pgivm.create_immv('mv_ivm25', 'SELECT FROM mv_base_a'); -- FOR UPDATE/SHARE is not supported SELECT pgivm.create_immv('mv_ivm26', 'SELECT i,j FROM mv_base_a FOR UPDATE'); SELECT pgivm.create_immv('mv_ivm27', 'SELECT * FROM (SELECT i,j FROM mv_base_a FOR UPDATE) AS tmp;'); -- tartget list cannot contain ivm column that start with '__ivm' SELECT pgivm.create_immv('mv_ivm28', 'SELECT i AS "__ivm_count__" FROM mv_base_a'); -- expressions specified in GROUP BY must appear in the target list. SELECT pgivm.create_immv('mv_ivm29', 'SELECT COUNT(i) FROM mv_base_a GROUP BY i;'); -- experssions containing an aggregate is not supported SELECT pgivm.create_immv('mv_ivm30', 'SELECT sum(i)*0.5 FROM mv_base_a'); SELECT pgivm.create_immv('mv_ivm31', 'SELECT sum(i)/sum(j) FROM mv_base_a'); -- VALUES is not supported SELECT pgivm.create_immv('mv_ivm_only_values1', 'values(1)'); SELECT pgivm.create_immv('mv_ivm_only_values2', 'SELECT * FROM (values(1)) AS tmp'); -- column of parent query specified in EXISTS clause must appear in the target list. SELECT pgivm.create_immv('mv_ivm32', 'SELECT a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i)'); -- views containing base tables with Row Level Security DROP USER IF EXISTS regress_ivm_admin; DROP USER IF EXISTS regress_ivm_user; CREATE USER regress_ivm_admin; CREATE USER regress_ivm_user; --- create a table with RLS SET SESSION AUTHORIZATION regress_ivm_admin; CREATE TABLE rls_tbl(id int, data text, owner name); INSERT INTO rls_tbl VALUES (1,'foo','regress_ivm_user'), (2,'bar','postgres'); CREATE TABLE num_tbl(id int, num text); INSERT INTO num_tbl VALUES (1,'one'), (2,'two'), (3,'three'), (4,'four'), (5,'five'), (6,'six'); --- Users can access only their own rows CREATE POLICY rls_tbl_policy ON rls_tbl FOR SELECT TO PUBLIC USING(owner = current_user); ALTER TABLE rls_tbl ENABLE ROW LEVEL SECURITY; GRANT ALL on rls_tbl TO PUBLIC; GRANT ALL on num_tbl TO PUBLIC; --- create a view owned by regress_ivm_user SET SESSION AUTHORIZATION regress_ivm_user; SELECT pgivm.create_immv('ivm_rls', 'SELECT * FROM rls_tbl'); SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3; RESET SESSION AUTHORIZATION; --- inserts rows owned by different users INSERT INTO rls_tbl VALUES (3,'baz','regress_ivm_user'), (4,'qux','postgres'); SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3; --- combination of diffent kinds of commands WITH i AS (INSERT INTO rls_tbl VALUES(5,'quux','postgres'), (6,'corge','regress_ivm_user')), u AS (UPDATE rls_tbl SET owner = 'postgres' WHERE id = 1), u2 AS (UPDATE rls_tbl SET owner = 'regress_ivm_user' WHERE id = 2) SELECT; SELECT id, data, owner FROM ivm_rls ORDER BY 1,2,3; --- SET SESSION AUTHORIZATION regress_ivm_user; SELECT pgivm.create_immv('ivm_rls2', 'SELECT * FROM rls_tbl JOIN num_tbl USING(id)'); RESET SESSION AUTHORIZATION; WITH x AS (UPDATE rls_tbl SET data = data || '_2' where id in (3,4)), y AS (UPDATE num_tbl SET num = num || '_2' where id in (3,4)) SELECT; SELECT * FROM ivm_rls2 ORDER BY 1,2,3; DROP TABLE rls_tbl CASCADE; DROP TABLE num_tbl CASCADE; DROP USER regress_ivm_user; DROP USER regress_ivm_admin; -- automatic index creation BEGIN; CREATE TABLE base_a (i int primary key, j int); CREATE TABLE base_b (i int primary key, j int); --- group by: create an index SELECT pgivm.create_immv('mv_idx1', 'SELECT i, sum(j) FROM base_a GROUP BY i'); --- distinct: create an index SELECT pgivm.create_immv('mv_idx2', 'SELECT DISTINCT j FROM base_a'); --- with all pkey columns: create an index SELECT pgivm.create_immv('mv_idx3(i_a, i_b)', 'SELECT a.i, b.i FROM base_a a, base_b b'); --- missing some pkey columns: no index SELECT pgivm.create_immv('mv_idx4', 'SELECT j FROM base_a'); SELECT pgivm.create_immv('mv_idx5', 'SELECT a.i, b.j FROM base_a a, base_b b'); --- subqueries: create an index SELECT pgivm.create_immv('mv_idx6(i_a, i_b)', 'SELECT a.i, b.i FROM (SELECT * FROM base_a) a, (SELECT * FROM base_b) b'); --- with set-returning function: no index SELECT pgivm.create_immv('mv_idx7', 'SELECT i FROM base_a, generate_series(1,10)'); ROLLBACK; -- type that doesn't have default operator class for access method btree BEGIN; CREATE TABLE table_json (j json); SELECT pgivm.create_immv('mv_json', 'SELECT * from table_json'); ROLLBACK; -- prevent IMMV chanages INSERT INTO mv_ivm_1 VALUES(1,1,1); UPDATE mv_ivm_1 SET k = 1 WHERE i = 1; DELETE FROM mv_ivm_1; TRUNCATE mv_ivm_1; -- get_immv_def function SELECT immvrelid, pgivm.get_immv_def(immvrelid) FROM pgivm.pg_ivm_immv ORDER BY 1; -- mv_base_b is not immv SELECT 'mv_base_b'::regclass, pgivm.get_immv_def('mv_base_b'); DROP TABLE mv_base_b CASCADE; DROP TABLE mv_base_a CASCADE; pg_ivm-1.13/sql/refresh_immv.sql000066400000000000000000000022411507512346500167340ustar00rootroot00000000000000CREATE TABLE t (i int PRIMARY KEY); INSERT INTO t SELECT generate_series(1, 5); SELECT pgivm.create_immv('mv', 'SELECT * FROM t'); SELECT immvrelid, ispopulated FROM pgivm.pg_ivm_immv ORDER BY 1; -- Refresh IMMV with data SELECT pgivm.refresh_immv('mv', true); SELECT immvrelid, ispopulated FROM pgivm.pg_ivm_immv ORDER BY 1; INSERT INTO t VALUES(6); SELECT i FROM mv ORDER BY 1; -- Make IMMV unpopulated SELECT pgivm.refresh_immv('mv', false); SELECT immvrelid, ispopulated FROM pgivm.pg_ivm_immv ORDER BY 1; SELECT i FROM mv ORDER BY 1; -- Immediate maintenance is disabled. IMMV can be scannable and is empty. INSERT INTO t VALUES(7); SELECT i FROM mv ORDER BY 1; -- Refresh the IMMV and make it populated. SELECT pgivm.refresh_immv('mv', true); SELECT immvrelid, ispopulated FROM pgivm.pg_ivm_immv ORDER BY 1; SELECT i FROM mv ORDER BY 1; -- Immediate maintenance is enabled. INSERT INTO t VALUES(8); SELECT i FROM mv ORDER BY 1; -- Use qualified name SELECT pgivm.refresh_immv('public.mv', true); -- Use not existing IMMV SELECT pgivm.refresh_immv('mv_not_existing', true); -- Try to refresh a normal table -- error SELECT pgivm.refresh_immv('t', true); DROP TABLE mv; pg_ivm-1.13/subselect.c000066400000000000000000000057011507512346500150670ustar00rootroot00000000000000/*------------------------------------------------------------------------- * * subselect.c * incremental view maintenance extension * Routines for CTE support. * * Portions Copyright (c) 2023, IVM Development Group * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * *------------------------------------------------------------------------- */ #include "postgres.h" #include "nodes/nodeFuncs.h" #include "rewrite/rewriteManip.h" #include "pg_ivm.h" typedef struct inline_cte_walker_context { const char *ctename; /* name and relative level of target CTE */ int levelsup; Query *ctequery; /* query to substitute */ } inline_cte_walker_context; static bool inline_cte_walker(Node *node, inline_cte_walker_context *context); /* * inline_cte: convert RTE_CTE references to given CTE into RTE_SUBQUERYs */ void inline_cte(PlannerInfo *root, CommonTableExpr *cte) { struct inline_cte_walker_context context; context.ctename = cte->ctename; /* Start at levelsup = -1 because we'll immediately increment it */ context.levelsup = -1; context.ctequery = castNode(Query, cte->ctequery); (void) inline_cte_walker((Node *) root->parse, &context); } static bool inline_cte_walker(Node *node, inline_cte_walker_context *context) { if (node == NULL) return false; if (IsA(node, Query)) { Query *query = (Query *) node; context->levelsup++; /* * Visit the query's RTE nodes after their contents; otherwise * query_tree_walker would descend into the newly inlined CTE query, * which we don't want. */ (void) query_tree_walker(query, inline_cte_walker, context, QTW_EXAMINE_RTES_AFTER); context->levelsup--; return false; } else if (IsA(node, RangeTblEntry)) { RangeTblEntry *rte = (RangeTblEntry *) node; if (rte->rtekind == RTE_CTE && strcmp(rte->ctename, context->ctename) == 0 && rte->ctelevelsup == context->levelsup) { /* * Found a reference to replace. Generate a copy of the CTE query * with appropriate level adjustment for outer references (e.g., * to other CTEs). */ Query *newquery = copyObject(context->ctequery); if (context->levelsup > 0) IncrementVarSublevelsUp((Node *) newquery, context->levelsup, 1); /* * Convert the RTE_CTE RTE into a RTE_SUBQUERY. * * Historically, a FOR UPDATE clause has been treated as extending * into views and subqueries, but not into CTEs. We preserve this * distinction by not trying to push rowmarks into the new * subquery. */ rte->rtekind = RTE_SUBQUERY; rte->subquery = newquery; rte->security_barrier = false; /* Zero out CTE-specific fields */ rte->ctename = NULL; rte->ctelevelsup = 0; rte->self_reference = false; rte->coltypes = NIL; rte->coltypmods = NIL; rte->colcollations = NIL; } return false; } return expression_tree_walker(node, inline_cte_walker, context); }