Re: Alias collision in `refresh materialized view concurrently` - Mailing list pgsql-hackers

From Tom Lane
Subject Re: Alias collision in `refresh materialized view concurrently`
Date
Msg-id 2557084.1628281502@sss.pgh.pa.us
Whole thread Raw
In response to Re: Alias collision in `refresh materialized view concurrently`  (Tom Lane <tgl@sss.pgh.pa.us>)
List pgsql-hackers
I wrote:
> I just came across this issue while preparing the release notes.
> ISTM that people have expended a great deal of effort on a fundamentally
> unreliable solution, when a reliable one is easily available.

Concretely, I propose the attached.

            regards, tom lane

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 9493b227b4..512b00bc65 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -537,9 +537,12 @@ transientrel_destroy(DestReceiver *self)
 /*
  * Given a qualified temporary table name, append an underscore followed by
  * the given integer, to make a new table name based on the old one.
+ * The result is a palloc'd string.
  *
- * This leaks memory through palloc(), which won't be cleaned up until the
- * current memory context is freed.
+ * As coded, this would fail to make a valid SQL name if the given name were,
+ * say, "FOO"."BAR".  Currently, the table name portion of the input will
+ * never be double-quoted because it's of the form "pg_temp_NNN", cf
+ * make_new_heap().  But we might have to work harder someday.
  */
 static char *
 make_temptable_name_n(char *tempname, int n)
@@ -627,16 +630,20 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
      * that in a way that allows showing the first duplicated row found.  Even
      * after we pass this test, a unique index on the materialized view may
      * find a duplicate key problem.
+     *
+     * Note: here and below, we use "tablename.*::tablerowtype" as a hack to
+     * keep ".*" from being expanded into multiple columns in a SELECT list.
+     * Compare ruleutils.c's get_variable().
      */
     resetStringInfo(&querybuf);
     appendStringInfo(&querybuf,
-                     "SELECT _$newdata FROM %s _$newdata "
-                     "WHERE _$newdata IS NOT NULL AND EXISTS "
-                     "(SELECT 1 FROM %s _$newdata2 WHERE _$newdata2 IS NOT NULL "
-                     "AND _$newdata2 OPERATOR(pg_catalog.*=) _$newdata "
-                     "AND _$newdata2.ctid OPERATOR(pg_catalog.<>) "
-                     "_$newdata.ctid)",
-                     tempname, tempname);
+                     "SELECT newdata.*::%s FROM %s newdata "
+                     "WHERE newdata.* IS NOT NULL AND EXISTS "
+                     "(SELECT 1 FROM %s newdata2 WHERE newdata2.* IS NOT NULL "
+                     "AND newdata2.* OPERATOR(pg_catalog.*=) newdata.* "
+                     "AND newdata2.ctid OPERATOR(pg_catalog.<>) "
+                     "newdata.ctid)",
+                     tempname, tempname, tempname);
     if (SPI_execute(querybuf.data, false, 1) != SPI_OK_SELECT)
         elog(ERROR, "SPI_exec failed: %s", querybuf.data);
     if (SPI_processed > 0)
@@ -663,9 +670,9 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
     resetStringInfo(&querybuf);
     appendStringInfo(&querybuf,
                      "CREATE TEMP TABLE %s AS "
-                     "SELECT _$mv.ctid AS tid, _$newdata "
-                     "FROM %s _$mv FULL JOIN %s _$newdata ON (",
-                     diffname, matviewname, tempname);
+                     "SELECT mv.ctid AS tid, newdata.*::%s AS newdata "
+                     "FROM %s mv FULL JOIN %s newdata ON (",
+                     diffname, tempname, matviewname, tempname);

     /*
      * Get the list of index OIDs for the table from the relcache, and look up
@@ -757,9 +764,9 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
                 if (foundUniqueIndex)
                     appendStringInfoString(&querybuf, " AND ");

-                leftop = quote_qualified_identifier("_$newdata",
+                leftop = quote_qualified_identifier("newdata",
                                                     NameStr(attr->attname));
-                rightop = quote_qualified_identifier("_$mv",
+                rightop = quote_qualified_identifier("mv",
                                                      NameStr(attr->attname));

                 generate_operator_clause(&querybuf,
@@ -787,8 +794,8 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
     Assert(foundUniqueIndex);

     appendStringInfoString(&querybuf,
-                           " AND _$newdata OPERATOR(pg_catalog.*=) _$mv) "
-                           "WHERE _$newdata IS NULL OR _$mv IS NULL "
+                           " AND newdata.* OPERATOR(pg_catalog.*=) mv.*) "
+                           "WHERE newdata.* IS NULL OR mv.* IS NULL "
                            "ORDER BY tid");

     /* Create the temporary "diff" table. */
@@ -814,10 +821,10 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
     /* Deletes must come before inserts; do them first. */
     resetStringInfo(&querybuf);
     appendStringInfo(&querybuf,
-                     "DELETE FROM %s _$mv WHERE ctid OPERATOR(pg_catalog.=) ANY "
-                     "(SELECT _$diff.tid FROM %s _$diff "
-                     "WHERE _$diff.tid IS NOT NULL "
-                     "AND _$diff._$newdata IS NULL)",
+                     "DELETE FROM %s mv WHERE ctid OPERATOR(pg_catalog.=) ANY "
+                     "(SELECT diff.tid FROM %s diff "
+                     "WHERE diff.tid IS NOT NULL "
+                     "AND diff.newdata IS NULL)",
                      matviewname, diffname);
     if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE)
         elog(ERROR, "SPI_exec failed: %s", querybuf.data);
@@ -825,8 +832,8 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
     /* Inserts go last. */
     resetStringInfo(&querybuf);
     appendStringInfo(&querybuf,
-                     "INSERT INTO %s SELECT (_$diff._$newdata).* "
-                     "FROM %s _$diff WHERE tid IS NULL",
+                     "INSERT INTO %s SELECT (diff.newdata).* "
+                     "FROM %s diff WHERE tid IS NULL",
                      matviewname, diffname);
     if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT)
         elog(ERROR, "SPI_exec failed: %s", querybuf.data);
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 4b3a2e0cb7..313c72a268 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -551,7 +551,15 @@ NOTICE:  drop cascades to materialized view mvtest_mv_v
 -- make sure running as superuser works when MV owned by another role (bug #11208)
 CREATE ROLE regress_user_mvtest;
 SET ROLE regress_user_mvtest;
-CREATE TABLE mvtest_foo_data AS SELECT i, md5(random()::text)
+-- this test case also checks for ambiguity in the queries issued by
+-- refresh_by_match_merge(), by choosing column names that intentionally
+-- duplicate all the aliases used in those queries
+CREATE TABLE mvtest_foo_data AS SELECT i,
+  i+1 AS tid,
+  md5(random()::text) AS mv,
+  md5(random()::text) AS newdata,
+  md5(random()::text) AS newdata2,
+  md5(random()::text) AS diff
   FROM generate_series(1, 10) i;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index 4a4bd0d6b6..68b9ccfd45 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -211,7 +211,15 @@ DROP TABLE mvtest_v CASCADE;
 -- make sure running as superuser works when MV owned by another role (bug #11208)
 CREATE ROLE regress_user_mvtest;
 SET ROLE regress_user_mvtest;
-CREATE TABLE mvtest_foo_data AS SELECT i, md5(random()::text)
+-- this test case also checks for ambiguity in the queries issued by
+-- refresh_by_match_merge(), by choosing column names that intentionally
+-- duplicate all the aliases used in those queries
+CREATE TABLE mvtest_foo_data AS SELECT i,
+  i+1 AS tid,
+  md5(random()::text) AS mv,
+  md5(random()::text) AS newdata,
+  md5(random()::text) AS newdata2,
+  md5(random()::text) AS diff
   FROM generate_series(1, 10) i;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;

pgsql-hackers by date:

Previous
From: Dean Rasheed
Date:
Subject: Re: Numeric x^y for negative x
Next
From: "Bossart, Nathan"
Date:
Subject: Re: Pre-allocating WAL files