]> granicus.if.org Git - postgresql/commitdiff
Add record_image_ops opclass for matview concurrent refresh.
authorKevin Grittner <kgrittn@postgresql.org>
Wed, 9 Oct 2013 19:26:09 +0000 (14:26 -0500)
committerKevin Grittner <kgrittn@postgresql.org>
Wed, 9 Oct 2013 19:26:09 +0000 (14:26 -0500)
REFRESH MATERIALIZED VIEW CONCURRENTLY was broken for any matview
containing a column of a type without a default btree operator
class.  It also did not produce results consistent with a non-
concurrent REFRESH or a normal view if any column was of a type
which allowed user-visible differences between values which
compared as equal according to the type's default btree opclass.
Concurrent matview refresh was modified to use the new operators
to solve these problems.

Documentation was added for record comparison, both for the
default btree operator class for record, and the newly added
operators.  Regression tests now check for proper behavior both
for a matview with a box column and a matview containing a citext
column.

Reviewed by Steve Singer, who suggested some of the doc language.

17 files changed:
contrib/citext/expected/citext.out
contrib/citext/expected/citext_1.out
contrib/citext/sql/citext.sql
doc/src/sgml/func.sgml
src/backend/commands/matview.c
src/backend/utils/adt/rowtypes.c
src/include/catalog/catversion.h
src/include/catalog/pg_amop.h
src/include/catalog/pg_amproc.h
src/include/catalog/pg_opclass.h
src/include/catalog/pg_operator.h
src/include/catalog/pg_opfamily.h
src/include/catalog/pg_proc.h
src/include/utils/builtins.h
src/test/regress/expected/matview.out
src/test/regress/expected/opr_sanity.out
src/test/regress/sql/matview.sql

index a6265d857dc0dfa3eaaec43ffb2d5e6daba06f49..fea4bcbb1ebe1d9aefc36e617c867a3045e5255d 100644 (file)
@@ -2276,3 +2276,44 @@ SELECT like_escape( name::text, ''::citext ) = like_escape( name::text, '' ) AS
  t
 (5 rows)
 
+-- Ensure correct behavior for citext with materialized views.
+CREATE TABLE citext_table (
+  id serial primary key,
+  name citext
+);
+INSERT INTO citext_table (name)
+  VALUES ('one'), ('two'), ('three'), (NULL), (NULL);
+CREATE MATERIALIZED VIEW citext_matview AS
+  SELECT * FROM citext_table;
+CREATE UNIQUE INDEX citext_matview_id
+  ON citext_matview (id);
+SELECT *
+  FROM citext_matview m
+  FULL JOIN citext_table t ON (t.id = m.id AND t *= m)
+  WHERE t.id IS NULL OR m.id IS NULL;
+ id | name | id | name 
+----+------+----+------
+(0 rows)
+
+UPDATE citext_table SET name = 'Two' WHERE name = 'TWO';
+SELECT *
+  FROM citext_matview m
+  FULL JOIN citext_table t ON (t.id = m.id AND t *= m)
+  WHERE t.id IS NULL OR m.id IS NULL;
+ id | name | id | name 
+----+------+----+------
+    |      |  2 | Two
+  2 | two  |    | 
+(2 rows)
+
+REFRESH MATERIALIZED VIEW CONCURRENTLY citext_matview;
+SELECT * FROM citext_matview ORDER BY id;
+ id | name  
+----+-------
+  1 | one
+  2 | Two
+  3 | three
+  4 | 
+  5 | 
+(5 rows)
+
index 36342be7c1da0c6163e2fbc8bcaff932592a14ff..101f10526ea3479fd37bdf5af8744741415e44b6 100644 (file)
@@ -2276,3 +2276,44 @@ SELECT like_escape( name::text, ''::citext ) = like_escape( name::text, '' ) AS
  t
 (5 rows)
 
+-- Ensure correct behavior for citext with materialized views.
+CREATE TABLE citext_table (
+  id serial primary key,
+  name citext
+);
+INSERT INTO citext_table (name)
+  VALUES ('one'), ('two'), ('three'), (NULL), (NULL);
+CREATE MATERIALIZED VIEW citext_matview AS
+  SELECT * FROM citext_table;
+CREATE UNIQUE INDEX citext_matview_id
+  ON citext_matview (id);
+SELECT *
+  FROM citext_matview m
+  FULL JOIN citext_table t ON (t.id = m.id AND t *= m)
+  WHERE t.id IS NULL OR m.id IS NULL;
+ id | name | id | name 
+----+------+----+------
+(0 rows)
+
+UPDATE citext_table SET name = 'Two' WHERE name = 'TWO';
+SELECT *
+  FROM citext_matview m
+  FULL JOIN citext_table t ON (t.id = m.id AND t *= m)
+  WHERE t.id IS NULL OR m.id IS NULL;
+ id | name | id | name 
+----+------+----+------
+    |      |  2 | Two
+  2 | two  |    | 
+(2 rows)
+
+REFRESH MATERIALIZED VIEW CONCURRENTLY citext_matview;
+SELECT * FROM citext_matview ORDER BY id;
+ id | name  
+----+-------
+  1 | one
+  2 | Two
+  3 | three
+  4 | 
+  5 | 
+(5 rows)
+
index 65ef05b931267fad61432ca9c7caf2553d3d75b6..8a3532bea59533f8367e6ec96d4d0f63d5ed7f2c 100644 (file)
@@ -711,3 +711,26 @@ SELECT COUNT(*) = 19::bigint AS t FROM try;
 
 SELECT like_escape( name, '' ) = like_escape( name::text, '' ) AS t FROM srt;
 SELECT like_escape( name::text, ''::citext ) = like_escape( name::text, '' ) AS t FROM srt;
+
+-- Ensure correct behavior for citext with materialized views.
+CREATE TABLE citext_table (
+  id serial primary key,
+  name citext
+);
+INSERT INTO citext_table (name)
+  VALUES ('one'), ('two'), ('three'), (NULL), (NULL);
+CREATE MATERIALIZED VIEW citext_matview AS
+  SELECT * FROM citext_table;
+CREATE UNIQUE INDEX citext_matview_id
+  ON citext_matview (id);
+SELECT *
+  FROM citext_matview m
+  FULL JOIN citext_table t ON (t.id = m.id AND t *= m)
+  WHERE t.id IS NULL OR m.id IS NULL;
+UPDATE citext_table SET name = 'Two' WHERE name = 'TWO';
+SELECT *
+  FROM citext_matview m
+  FULL JOIN citext_table t ON (t.id = m.id AND t *= m)
+  WHERE t.id IS NULL OR m.id IS NULL;
+REFRESH MATERIALIZED VIEW CONCURRENTLY citext_matview;
+SELECT * FROM citext_matview ORDER BY id;
index 7dd1ef2ea1545148f3247e6443749eb382d36ed5..c3090dd2b9f0f9d653593b9cf615bf20cea9f487 100644 (file)
@@ -12739,7 +12739,7 @@ WHERE EXISTS (SELECT 1 FROM tab2 WHERE col2 = tab1.col2);
 
   <para>
    See <xref linkend="row-wise-comparison"> for details about the meaning
-   of a row-wise comparison.
+   of a row constructor comparison.
   </para>
   </sect2>
 
@@ -12795,12 +12795,12 @@ WHERE EXISTS (SELECT 1 FROM tab2 WHERE col2 = tab1.col2);
 
   <para>
    See <xref linkend="row-wise-comparison"> for details about the meaning
-   of a row-wise comparison.
+   of a row constructor comparison.
   </para>
   </sect2>
 
   <sect2>
-   <title>Row-wise Comparison</title>
+   <title>Single-row Comparison</title>
 
    <indexterm zone="functions-subquery">
     <primary>comparison</primary>
@@ -12823,7 +12823,7 @@ WHERE EXISTS (SELECT 1 FROM tab2 WHERE col2 = tab1.col2);
 
   <para>
    See <xref linkend="row-wise-comparison"> for details about the meaning
-   of a row-wise comparison.
+   of a row constructor comparison.
   </para>
   </sect2>
  </sect1>
@@ -12852,13 +12852,23 @@ WHERE EXISTS (SELECT 1 FROM tab2 WHERE col2 = tab1.col2);
    <primary>SOME</primary>
   </indexterm>
 
+  <indexterm>
+   <primary>composite type</primary>
+   <secondary>comparison</secondary>
+  </indexterm>
+
   <indexterm>
    <primary>row-wise comparison</primary>
   </indexterm>
 
   <indexterm>
    <primary>comparison</primary>
-   <secondary>row-wise</secondary>
+   <secondary>composite type</secondary>
+  </indexterm>
+
+  <indexterm>
+   <primary>comparison</primary>
+   <secondary>row constructor</secondary>
   </indexterm>
 
   <indexterm>
@@ -13023,7 +13033,7 @@ AND
   </sect2>
 
   <sect2 id="row-wise-comparison">
-   <title>Row-wise Comparison</title>
+   <title>Row Constructor Comparison</title>
 
 <synopsis>
 <replaceable>row_constructor</replaceable> <replaceable>operator</replaceable> <replaceable>row_constructor</replaceable>
@@ -13033,20 +13043,25 @@ AND
    Each side is a row constructor,
    as described in <xref linkend="sql-syntax-row-constructors">.
    The two row values must have the same number of fields.
-   Each side is evaluated and they are compared row-wise.  Row comparisons
-   are allowed when the <replaceable>operator</replaceable> is
+   Each side is evaluated and they are compared row-wise.  Row constructor
+   comparisons are allowed when the <replaceable>operator</replaceable> is
    <literal>=</>,
    <literal>&lt;&gt;</>,
    <literal>&lt;</>,
    <literal>&lt;=</>,
    <literal>&gt;</> or
-   <literal>&gt;=</>,
-   or has semantics similar to one of these.  (To be specific, an operator
-   can be a row comparison operator if it is a member of a B-tree operator
-   class, or is the negator of the <literal>=</> member of a B-tree operator
-   class.)
+   <literal>&gt;=</>.
+   Every row element must be of a type which has a default B-tree operator
+   class or the attempted comparison may generate an error.
   </para>
 
+  <note>
+   <para>
+    Errors related to the number or types of elements might not occur if
+    the comparison is resolved using earlier columns.
+   </para>
+  </note>
+
   <para>
    The <literal>=</> and <literal>&lt;&gt;</> cases work slightly differently
    from the others.  Two rows are considered
@@ -13104,20 +13119,64 @@ AND
    be either true or false, never null.
   </para>
 
-  <note>
-   <para>
-    The SQL specification requires row-wise comparison to return NULL if the
-    result depends on comparing two NULL values or a NULL and a non-NULL.
-    <productname>PostgreSQL</productname> does this only when comparing the
-    results of two row constructors or comparing a row constructor to the
-    output of a subquery (as in <xref linkend="functions-subquery">).
-    In other contexts where two composite-type values are compared, two
-    NULL field values are considered equal, and a NULL is considered larger
-    than a non-NULL.  This is necessary in order to have consistent sorting
-    and indexing behavior for composite types.
-   </para>
-  </note>
+  </sect2>
 
+  <sect2 id="composite-type-comparison">
+   <title>Composite Type Comparison</title>
+
+<synopsis>
+<replaceable>record</replaceable> <replaceable>operator</replaceable> <replaceable>record</replaceable>
+</synopsis>
+
+  <para>
+   The SQL specification requires row-wise comparison to return NULL if the
+   result depends on comparing two NULL values or a NULL and a non-NULL.
+   <productname>PostgreSQL</productname> does this only when comparing the
+   results of two row constructors (as in
+   <xref linkend="row-wise-comparison">) or comparing a row constructor
+   to the output of a subquery (as in <xref linkend="functions-subquery">).
+   In other contexts where two composite-type values are compared, two
+   NULL field values are considered equal, and a NULL is considered larger
+   than a non-NULL.  This is necessary in order to have consistent sorting
+   and indexing behavior for composite types.
+  </para>
+
+  <para>
+   Each side is evaluated and they are compared row-wise.  Composite type
+   comparisons are allowed when the <replaceable>operator</replaceable> is
+   <literal>=</>,
+   <literal>&lt;&gt;</>,
+   <literal>&lt;</>,
+   <literal>&lt;=</>,
+   <literal>&gt;</> or
+   <literal>&gt;=</>,
+   or has semantics similar to one of these.  (To be specific, an operator
+   can be a row comparison operator if it is a member of a B-tree operator
+   class, or is the negator of the <literal>=</> member of a B-tree operator
+   class.)  The default behavior of the above operators is the same as for
+   <literal>IS [ NOT ] DISTINCT FROM</literal> for row constructors (see
+   <xref linkend="row-wise-comparison">).
+  </para>
+
+  <para>
+   To support matching of rows which include elements without a default
+   B-tree operator class, the following operators are defined for composite
+   type comparison:
+   <literal>*=</>,
+   <literal>*&lt;&gt;</>,
+   <literal>*&lt;</>,
+   <literal>*&lt;=</>,
+   <literal>*&gt;</>, and
+   <literal>*&gt;=</>.
+   These operators compare the internal binary representation of the two
+   rows.  Two rows might have a different binary representation even
+   though comparisons of the two rows with the equality operator is true. 
+   The ordering of rows under these comparision operators is deterministic
+   but not otherwise meaningful.  These operators are used internally for
+   materialized views and might be useful for other specialized purposes
+   such as replication but are not intended to be generally useful for
+   writing queries.
+  </para>
   </sect2>
  </sect1>
 
index 238ccc72f5205ae00a15e6e17f384addfa445552..fcfc678813d5ce2f02aacb31bec3b5635825c4c4 100644 (file)
@@ -562,7 +562,7 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid)
                                         "SELECT newdata FROM %s newdata "
                                         "WHERE newdata IS NOT NULL AND EXISTS "
                                         "(SELECT * FROM %s newdata2 WHERE newdata2 IS NOT NULL "
-                                        "AND newdata2 OPERATOR(pg_catalog.=) newdata "
+                                        "AND newdata2 OPERATOR(pg_catalog.*=) newdata "
                                         "AND newdata2.ctid OPERATOR(pg_catalog.<>) "
                                         "newdata.ctid) LIMIT 1",
                                         tempname, tempname);
@@ -645,9 +645,6 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid)
                                /*
                                 * Only include the column once regardless of how many times
                                 * it shows up in how many indexes.
-                                *
-                                * This is also useful later to omit columns which can not
-                                * have changed from the SET clause of the UPDATE statement.
                                 */
                                if (usedForQual[attnum - 1])
                                        continue;
@@ -682,8 +679,9 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid)
                                 errhint("Create a UNIQUE index with no WHERE clause on one or more columns of the materialized view.")));
 
        appendStringInfoString(&querybuf,
-                                                  " AND newdata = mv) WHERE newdata IS NULL OR mv IS NULL"
-                                                  " ORDER BY tid");
+                                                  " AND newdata OPERATOR(pg_catalog.*=) mv) "
+                                                  "WHERE newdata IS NULL OR mv IS NULL "
+                                                  "ORDER BY tid");
 
        /* Create the temporary "diff" table. */
        if (SPI_exec(querybuf.data, 0) != SPI_OK_UTILITY)
index 1bd473af657dd6706fedf053927296111f87fb80..0bcbb5d66485d6ce30bb1b94f48d9f4ecd0eccba 100644 (file)
@@ -17,6 +17,7 @@
 #include <ctype.h>
 
 #include "access/htup_details.h"
+#include "access/tuptoaster.h"
 #include "catalog/pg_type.h"
 #include "libpq/pqformat.h"
 #include "utils/builtins.h"
@@ -1281,3 +1282,496 @@ btrecordcmp(PG_FUNCTION_ARGS)
 {
        PG_RETURN_INT32(record_cmp(fcinfo));
 }
+
+
+/*
+ * record_image_cmp :
+ * Internal byte-oriented comparison function for records.
+ *
+ * Returns -1, 0 or 1
+ *
+ * Note: The normal concepts of "equality" do not apply here; different
+ * representation of values considered to be equal are not considered to be
+ * identical.  As an example, for the citext type 'A' and 'a' are equal, but
+ * they are not identical.
+ */
+static bool
+record_image_cmp(PG_FUNCTION_ARGS)
+{
+       HeapTupleHeader record1 = PG_GETARG_HEAPTUPLEHEADER(0);
+       HeapTupleHeader record2 = PG_GETARG_HEAPTUPLEHEADER(1);
+       int32           result = 0;
+       Oid                     tupType1;
+       Oid                     tupType2;
+       int32           tupTypmod1;
+       int32           tupTypmod2;
+       TupleDesc       tupdesc1;
+       TupleDesc       tupdesc2;
+       HeapTupleData tuple1;
+       HeapTupleData tuple2;
+       int                     ncolumns1;
+       int                     ncolumns2;
+       RecordCompareData *my_extra;
+       int                     ncols;
+       Datum      *values1;
+       Datum      *values2;
+       bool       *nulls1;
+       bool       *nulls2;
+       int                     i1;
+       int                     i2;
+       int                     j;
+
+       /* Extract type info from the tuples */
+       tupType1 = HeapTupleHeaderGetTypeId(record1);
+       tupTypmod1 = HeapTupleHeaderGetTypMod(record1);
+       tupdesc1 = lookup_rowtype_tupdesc(tupType1, tupTypmod1);
+       ncolumns1 = tupdesc1->natts;
+       tupType2 = HeapTupleHeaderGetTypeId(record2);
+       tupTypmod2 = HeapTupleHeaderGetTypMod(record2);
+       tupdesc2 = lookup_rowtype_tupdesc(tupType2, tupTypmod2);
+       ncolumns2 = tupdesc2->natts;
+
+       /* Build temporary HeapTuple control structures */
+       tuple1.t_len = HeapTupleHeaderGetDatumLength(record1);
+       ItemPointerSetInvalid(&(tuple1.t_self));
+       tuple1.t_tableOid = InvalidOid;
+       tuple1.t_data = record1;
+       tuple2.t_len = HeapTupleHeaderGetDatumLength(record2);
+       ItemPointerSetInvalid(&(tuple2.t_self));
+       tuple2.t_tableOid = InvalidOid;
+       tuple2.t_data = record2;
+
+       /*
+        * We arrange to look up the needed comparison info just once per series
+        * of calls, assuming the record types don't change underneath us.
+        */
+       ncols = Max(ncolumns1, ncolumns2);
+       my_extra = (RecordCompareData *) fcinfo->flinfo->fn_extra;
+       if (my_extra == NULL ||
+               my_extra->ncolumns < ncols)
+       {
+               fcinfo->flinfo->fn_extra =
+                       MemoryContextAlloc(fcinfo->flinfo->fn_mcxt,
+                                               sizeof(RecordCompareData) - sizeof(ColumnCompareData)
+                                                          + ncols * sizeof(ColumnCompareData));
+               my_extra = (RecordCompareData *) fcinfo->flinfo->fn_extra;
+               my_extra->ncolumns = ncols;
+               my_extra->record1_type = InvalidOid;
+               my_extra->record1_typmod = 0;
+               my_extra->record2_type = InvalidOid;
+               my_extra->record2_typmod = 0;
+       }
+
+       if (my_extra->record1_type != tupType1 ||
+               my_extra->record1_typmod != tupTypmod1 ||
+               my_extra->record2_type != tupType2 ||
+               my_extra->record2_typmod != tupTypmod2)
+       {
+               MemSet(my_extra->columns, 0, ncols * sizeof(ColumnCompareData));
+               my_extra->record1_type = tupType1;
+               my_extra->record1_typmod = tupTypmod1;
+               my_extra->record2_type = tupType2;
+               my_extra->record2_typmod = tupTypmod2;
+       }
+
+       /* Break down the tuples into fields */
+       values1 = (Datum *) palloc(ncolumns1 * sizeof(Datum));
+       nulls1 = (bool *) palloc(ncolumns1 * sizeof(bool));
+       heap_deform_tuple(&tuple1, tupdesc1, values1, nulls1);
+       values2 = (Datum *) palloc(ncolumns2 * sizeof(Datum));
+       nulls2 = (bool *) palloc(ncolumns2 * sizeof(bool));
+       heap_deform_tuple(&tuple2, tupdesc2, values2, nulls2);
+
+       /*
+        * Scan corresponding columns, allowing for dropped columns in different
+        * places in the two rows.      i1 and i2 are physical column indexes, j is
+        * the logical column index.
+        */
+       i1 = i2 = j = 0;
+       while (i1 < ncolumns1 || i2 < ncolumns2)
+       {
+               /*
+                * Skip dropped columns
+                */
+               if (i1 < ncolumns1 && tupdesc1->attrs[i1]->attisdropped)
+               {
+                       i1++;
+                       continue;
+               }
+               if (i2 < ncolumns2 && tupdesc2->attrs[i2]->attisdropped)
+               {
+                       i2++;
+                       continue;
+               }
+               if (i1 >= ncolumns1 || i2 >= ncolumns2)
+                       break;                          /* we'll deal with mismatch below loop */
+
+               /*
+                * Have two matching columns, they must be same type
+                */
+               if (tupdesc1->attrs[i1]->atttypid !=
+                       tupdesc2->attrs[i2]->atttypid)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATATYPE_MISMATCH),
+                                        errmsg("cannot compare dissimilar column types %s and %s at record column %d",
+                                                       format_type_be(tupdesc1->attrs[i1]->atttypid),
+                                                       format_type_be(tupdesc2->attrs[i2]->atttypid),
+                                                       j + 1)));
+
+               /*
+                * We consider two NULLs equal; NULL > not-NULL.
+                */
+               if (!nulls1[i1] || !nulls2[i2])
+               {
+                       int                     cmpresult;
+
+                       if (nulls1[i1])
+                       {
+                               /* arg1 is greater than arg2 */
+                               result = 1;
+                               break;
+                       }
+                       if (nulls2[i2])
+                       {
+                               /* arg1 is less than arg2 */
+                               result = -1;
+                               break;
+                       }
+
+                       /* Compare the pair of elements */
+                       if (tupdesc1->attrs[i1]->attlen == -1)
+                       {
+                               Size            len1,
+                                                       len2;
+                               struct varlena     *arg1val;
+                               struct varlena     *arg2val;
+
+                               len1 = toast_raw_datum_size(values1[i1]);
+                               len2 = toast_raw_datum_size(values2[i2]);
+                               arg1val = PG_DETOAST_DATUM_PACKED(values1[i1]);
+                               arg2val = PG_DETOAST_DATUM_PACKED(values2[i2]);
+
+                               cmpresult = memcmp(VARDATA_ANY(arg1val),
+                                                                  VARDATA_ANY(arg2val),
+                                                                  len1 - VARHDRSZ);
+                               if ((cmpresult == 0) && (len1 != len2))
+                                       cmpresult = (len1 < len2) ? -1 : 1;
+
+                               if ((Pointer) arg1val != (Pointer) values1[i1])
+                                       pfree(arg1val);
+                               if ((Pointer) arg2val != (Pointer) values2[i2])
+                                       pfree(arg2val);
+                       }
+                       else if (tupdesc1->attrs[i1]->attbyval)
+                       {
+                               cmpresult = memcmp(&(values1[i1]),
+                                                                  &(values2[i2]),
+                                                                  tupdesc1->attrs[i1]->attlen);
+                       }
+                       else
+                       {
+                               cmpresult = memcmp(DatumGetPointer(values1[i1]),
+                                                                  DatumGetPointer(values2[i2]),
+                                                                  tupdesc1->attrs[i1]->attlen);
+                       }
+
+                       if (cmpresult < 0)
+                       {
+                               /* arg1 is less than arg2 */
+                               result = -1;
+                               break;
+                       }
+                       else if (cmpresult > 0)
+                       {
+                               /* arg1 is greater than arg2 */
+                               result = 1;
+                               break;
+                       }
+               }
+
+               /* equal, so continue to next column */
+               i1++, i2++, j++;
+       }
+
+       /*
+        * If we didn't break out of the loop early, check for column count
+        * mismatch.  (We do not report such mismatch if we found unequal column
+        * values; is that a feature or a bug?)
+        */
+       if (result == 0)
+       {
+               if (i1 != ncolumns1 || i2 != ncolumns2)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATATYPE_MISMATCH),
+                                        errmsg("cannot compare record types with different numbers of columns")));
+       }
+
+       pfree(values1);
+       pfree(nulls1);
+       pfree(values2);
+       pfree(nulls2);
+       ReleaseTupleDesc(tupdesc1);
+       ReleaseTupleDesc(tupdesc2);
+
+       /* Avoid leaking memory when handed toasted input. */
+       PG_FREE_IF_COPY(record1, 0);
+       PG_FREE_IF_COPY(record2, 1);
+
+       return result;
+}
+
+/*
+ * record_image_eq :
+ *               compares two records for identical contents, based on byte images
+ * result :
+ *               returns true if the records are identical, false otherwise.
+ *
+ * Note: we do not use record_image_cmp here, since we can avoid
+ * de-toasting for unequal lengths this way.
+ */
+Datum
+record_image_eq(PG_FUNCTION_ARGS)
+{
+       HeapTupleHeader record1 = PG_GETARG_HEAPTUPLEHEADER(0);
+       HeapTupleHeader record2 = PG_GETARG_HEAPTUPLEHEADER(1);
+       bool            result = true;
+       Oid                     tupType1;
+       Oid                     tupType2;
+       int32           tupTypmod1;
+       int32           tupTypmod2;
+       TupleDesc       tupdesc1;
+       TupleDesc       tupdesc2;
+       HeapTupleData tuple1;
+       HeapTupleData tuple2;
+       int                     ncolumns1;
+       int                     ncolumns2;
+       RecordCompareData *my_extra;
+       int                     ncols;
+       Datum      *values1;
+       Datum      *values2;
+       bool       *nulls1;
+       bool       *nulls2;
+       int                     i1;
+       int                     i2;
+       int                     j;
+
+       /* Extract type info from the tuples */
+       tupType1 = HeapTupleHeaderGetTypeId(record1);
+       tupTypmod1 = HeapTupleHeaderGetTypMod(record1);
+       tupdesc1 = lookup_rowtype_tupdesc(tupType1, tupTypmod1);
+       ncolumns1 = tupdesc1->natts;
+       tupType2 = HeapTupleHeaderGetTypeId(record2);
+       tupTypmod2 = HeapTupleHeaderGetTypMod(record2);
+       tupdesc2 = lookup_rowtype_tupdesc(tupType2, tupTypmod2);
+       ncolumns2 = tupdesc2->natts;
+
+       /* Build temporary HeapTuple control structures */
+       tuple1.t_len = HeapTupleHeaderGetDatumLength(record1);
+       ItemPointerSetInvalid(&(tuple1.t_self));
+       tuple1.t_tableOid = InvalidOid;
+       tuple1.t_data = record1;
+       tuple2.t_len = HeapTupleHeaderGetDatumLength(record2);
+       ItemPointerSetInvalid(&(tuple2.t_self));
+       tuple2.t_tableOid = InvalidOid;
+       tuple2.t_data = record2;
+
+       /*
+        * We arrange to look up the needed comparison info just once per series
+        * of calls, assuming the record types don't change underneath us.
+        */
+       ncols = Max(ncolumns1, ncolumns2);
+       my_extra = (RecordCompareData *) fcinfo->flinfo->fn_extra;
+       if (my_extra == NULL ||
+               my_extra->ncolumns < ncols)
+       {
+               fcinfo->flinfo->fn_extra =
+                       MemoryContextAlloc(fcinfo->flinfo->fn_mcxt,
+                                               sizeof(RecordCompareData) - sizeof(ColumnCompareData)
+                                                          + ncols * sizeof(ColumnCompareData));
+               my_extra = (RecordCompareData *) fcinfo->flinfo->fn_extra;
+               my_extra->ncolumns = ncols;
+               my_extra->record1_type = InvalidOid;
+               my_extra->record1_typmod = 0;
+               my_extra->record2_type = InvalidOid;
+               my_extra->record2_typmod = 0;
+       }
+
+       if (my_extra->record1_type != tupType1 ||
+               my_extra->record1_typmod != tupTypmod1 ||
+               my_extra->record2_type != tupType2 ||
+               my_extra->record2_typmod != tupTypmod2)
+       {
+               MemSet(my_extra->columns, 0, ncols * sizeof(ColumnCompareData));
+               my_extra->record1_type = tupType1;
+               my_extra->record1_typmod = tupTypmod1;
+               my_extra->record2_type = tupType2;
+               my_extra->record2_typmod = tupTypmod2;
+       }
+
+       /* Break down the tuples into fields */
+       values1 = (Datum *) palloc(ncolumns1 * sizeof(Datum));
+       nulls1 = (bool *) palloc(ncolumns1 * sizeof(bool));
+       heap_deform_tuple(&tuple1, tupdesc1, values1, nulls1);
+       values2 = (Datum *) palloc(ncolumns2 * sizeof(Datum));
+       nulls2 = (bool *) palloc(ncolumns2 * sizeof(bool));
+       heap_deform_tuple(&tuple2, tupdesc2, values2, nulls2);
+
+       /*
+        * Scan corresponding columns, allowing for dropped columns in different
+        * places in the two rows.      i1 and i2 are physical column indexes, j is
+        * the logical column index.
+        */
+       i1 = i2 = j = 0;
+       while (i1 < ncolumns1 || i2 < ncolumns2)
+       {
+               /*
+                * Skip dropped columns
+                */
+               if (i1 < ncolumns1 && tupdesc1->attrs[i1]->attisdropped)
+               {
+                       i1++;
+                       continue;
+               }
+               if (i2 < ncolumns2 && tupdesc2->attrs[i2]->attisdropped)
+               {
+                       i2++;
+                       continue;
+               }
+               if (i1 >= ncolumns1 || i2 >= ncolumns2)
+                       break;                          /* we'll deal with mismatch below loop */
+
+               /*
+                * Have two matching columns, they must be same type
+                */
+               if (tupdesc1->attrs[i1]->atttypid !=
+                       tupdesc2->attrs[i2]->atttypid)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATATYPE_MISMATCH),
+                                        errmsg("cannot compare dissimilar column types %s and %s at record column %d",
+                                                       format_type_be(tupdesc1->attrs[i1]->atttypid),
+                                                       format_type_be(tupdesc2->attrs[i2]->atttypid),
+                                                       j + 1)));
+
+               /*
+                * We consider two NULLs equal; NULL > not-NULL.
+                */
+               if (!nulls1[i1] || !nulls2[i2])
+               {
+                       if (nulls1[i1] || nulls2[i2])
+                       {
+                               result = false;
+                               break;
+                       }
+
+                       /* Compare the pair of elements */
+                       if (tupdesc1->attrs[i1]->attlen == -1)
+                       {
+                               Size            len1,
+                                                       len2;
+
+                               len1 = toast_raw_datum_size(values1[i1]);
+                               len2 = toast_raw_datum_size(values2[i2]);
+                               /* No need to de-toast if lengths don't match. */
+                               if (len1 != len2)
+                                       result = false;
+                               else
+                               {
+                                       struct varlena     *arg1val;
+                                       struct varlena     *arg2val;
+
+                                       arg1val = PG_DETOAST_DATUM_PACKED(values1[i1]);
+                                       arg2val = PG_DETOAST_DATUM_PACKED(values2[i2]);
+
+                                       result = (memcmp(VARDATA_ANY(arg1val),
+                                                                        VARDATA_ANY(arg2val),
+                                                                        len1 - VARHDRSZ) == 0);
+
+                                       /* Only free memory if it's a copy made here. */
+                                       if ((Pointer) arg1val != (Pointer) values1[i1])
+                                               pfree(arg1val);
+                                       if ((Pointer) arg2val != (Pointer) values2[i2])
+                                               pfree(arg2val);
+                               }
+                       }
+                       else if (tupdesc1->attrs[i1]->attbyval)
+                       {
+                               result = (memcmp(&(values1[i1]),
+                                                                &(values2[i2]),
+                                                                tupdesc1->attrs[i1]->attlen) == 0);
+                       }
+                       else
+                       {
+                               result = (memcmp(DatumGetPointer(values1[i1]),
+                                                                DatumGetPointer(values2[i2]),
+                                                                tupdesc1->attrs[i1]->attlen) == 0);
+                       }
+                       if (!result)
+                               break;
+               }
+
+               /* equal, so continue to next column */
+               i1++, i2++, j++;
+       }
+
+       /*
+        * If we didn't break out of the loop early, check for column count
+        * mismatch.  (We do not report such mismatch if we found unequal column
+        * values; is that a feature or a bug?)
+        */
+       if (result)
+       {
+               if (i1 != ncolumns1 || i2 != ncolumns2)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_DATATYPE_MISMATCH),
+                                        errmsg("cannot compare record types with different numbers of columns")));
+       }
+
+       pfree(values1);
+       pfree(nulls1);
+       pfree(values2);
+       pfree(nulls2);
+       ReleaseTupleDesc(tupdesc1);
+       ReleaseTupleDesc(tupdesc2);
+
+       /* Avoid leaking memory when handed toasted input. */
+       PG_FREE_IF_COPY(record1, 0);
+       PG_FREE_IF_COPY(record2, 1);
+
+       PG_RETURN_BOOL(result);
+}
+
+Datum
+record_image_ne(PG_FUNCTION_ARGS)
+{
+       PG_RETURN_BOOL(!DatumGetBool(record_image_eq(fcinfo)));
+}
+
+Datum
+record_image_lt(PG_FUNCTION_ARGS)
+{
+       PG_RETURN_BOOL(record_image_cmp(fcinfo) < 0);
+}
+
+Datum
+record_image_gt(PG_FUNCTION_ARGS)
+{
+       PG_RETURN_BOOL(record_image_cmp(fcinfo) > 0);
+}
+
+Datum
+record_image_le(PG_FUNCTION_ARGS)
+{
+       PG_RETURN_BOOL(record_image_cmp(fcinfo) <= 0);
+}
+
+Datum
+record_image_ge(PG_FUNCTION_ARGS)
+{
+       PG_RETURN_BOOL(record_image_cmp(fcinfo) >= 0);
+}
+
+Datum
+btrecordimagecmp(PG_FUNCTION_ARGS)
+{
+       PG_RETURN_INT32(record_image_cmp(fcinfo));
+}
index 3a18935072e81a17eac2762902297846f5336d68..5b2749c417ebe995611a9d3cbb051b5ebcf4dcf4 100644 (file)
@@ -53,6 +53,6 @@
  */
 
 /*                                                     yyyymmddN */
-#define CATALOG_VERSION_NO     201309051
+#define CATALOG_VERSION_NO     201310091
 
 #endif
index d2003487472a0c1ba67d88cb3e9b70f2857e6a4b..c8a548cead19bdf51b4d0dac575f2e28a1d2b0c3 100644 (file)
@@ -492,6 +492,16 @@ DATA(insert (      2994  2249 2249 3 s 2988        403 0 ));
 DATA(insert (  2994  2249 2249 4 s 2993        403 0 ));
 DATA(insert (  2994  2249 2249 5 s 2991        403 0 ));
 
+/*
+ *     btree record_image_ops
+ */
+
+DATA(insert (  3194  2249 2249 1 s 3190        403 0 ));
+DATA(insert (  3194  2249 2249 2 s 3192        403 0 ));
+DATA(insert (  3194  2249 2249 3 s 3188        403 0 ));
+DATA(insert (  3194  2249 2249 4 s 3193        403 0 ));
+DATA(insert (  3194  2249 2249 5 s 3191        403 0 ));
+
 /*
  * btree uuid_ops
  */
index 7155cb29a011a8f318a7729228117ea63b06e43c..53a3a7a6b54326f14e2cb3826b8c968b539a8cbb 100644 (file)
@@ -122,6 +122,7 @@ DATA(insert (       1989   26 26 1 356 ));
 DATA(insert (  1989   26 26 2 3134 ));
 DATA(insert (  1991   30 30 1 404 ));
 DATA(insert (  2994   2249 2249 1 2987 ));
+DATA(insert (  3194   2249 2249 1 3187 ));
 DATA(insert (  1994   25 25 1 360 ));
 DATA(insert (  1996   1083 1083 1 1107 ));
 DATA(insert (  2000   1266 1266 1 1358 ));
index f714db567e806a36334632bd207cb578b1a8f7cd..0a3eb3e1f797dceadb501b2f8d0572951b6f68cf 100644 (file)
@@ -143,6 +143,7 @@ DATA(insert (       405             oid_ops                         PGNSP PGUID 1990   26 t 0 ));
 DATA(insert (  403             oidvector_ops           PGNSP PGUID 1991   30 t 0 ));
 DATA(insert (  405             oidvector_ops           PGNSP PGUID 1992   30 t 0 ));
 DATA(insert (  403             record_ops                      PGNSP PGUID 2994 2249 t 0 ));
+DATA(insert (  403             record_image_ops        PGNSP PGUID 3194 2249 f 0 ));
 DATA(insert OID = 3126 ( 403   text_ops        PGNSP PGUID 1994   25 t 0 ));
 #define TEXT_BTREE_OPS_OID 3126
 DATA(insert (  405             text_ops                        PGNSP PGUID 1995   25 t 0 ));
index 5f28fc311600d082d117a1b5b7442921c3aab623..0350ef668715fd0ef021946bcafefefdb02046f8 100644 (file)
@@ -1672,6 +1672,20 @@ DESCR("less than or equal");
 DATA(insert OID = 2993 (  ">="    PGNSP PGUID b f f 2249 2249 16 2992 2990 record_ge scalargtsel scalargtjoinsel ));
 DESCR("greater than or equal");
 
+/* byte-oriented tests for identical rows and fast sorting */
+DATA(insert OID = 3188 (  "*="    PGNSP PGUID b t f 2249 2249 16 3188 3189 record_image_eq eqsel eqjoinsel ));
+DESCR("identical");
+DATA(insert OID = 3189 (  "*<>"   PGNSP PGUID b f f 2249 2249 16 3189 3188 record_image_ne neqsel neqjoinsel ));
+DESCR("not identical");
+DATA(insert OID = 3190 (  "*<"    PGNSP PGUID b f f 2249 2249 16 3191 3193 record_image_lt scalarltsel scalarltjoinsel ));
+DESCR("less than");
+DATA(insert OID = 3191 (  "*>"    PGNSP PGUID b f f 2249 2249 16 3190 3192 record_image_gt scalargtsel scalargtjoinsel ));
+DESCR("greater than");
+DATA(insert OID = 3192 (  "*<="   PGNSP PGUID b f f 2249 2249 16 3193 3191 record_image_le scalarltsel scalarltjoinsel ));
+DESCR("less than or equal");
+DATA(insert OID = 3193 (  "*>="   PGNSP PGUID b f f 2249 2249 16 3192 3190 record_image_ge scalargtsel scalargtjoinsel ));
+DESCR("greater than or equal");
+
 /* generic range type operators */
 DATA(insert OID = 3882 (  "="     PGNSP PGUID b t t 3831 3831 16 3882 3883 range_eq eqsel eqjoinsel ));
 DESCR("equal");
index 832f19422dce9ec48d0cbc7afb3e3c82adf79f50..12ba456ffe86a93a92fccb13f92c6ac16880ded8 100644 (file)
@@ -96,6 +96,7 @@ DATA(insert OID = 1990 (      405             oid_ops                 PGNSP PGUID ));
 DATA(insert OID = 1991 (       403             oidvector_ops   PGNSP PGUID ));
 DATA(insert OID = 1992 (       405             oidvector_ops   PGNSP PGUID ));
 DATA(insert OID = 2994 (       403             record_ops              PGNSP PGUID ));
+DATA(insert OID = 3194 (       403             record_image_ops        PGNSP PGUID ));
 DATA(insert OID = 1994 (       403             text_ops                PGNSP PGUID ));
 #define TEXT_BTREE_FAM_OID 1994
 DATA(insert OID = 1995 (       405             text_ops                PGNSP PGUID ));
index f03dd0b7da46b87d6327ac309c680448c21b38fd..3e523989c389baab5776956401b8f1608fbfb341 100644 (file)
@@ -4470,7 +4470,7 @@ DESCR("get set of in-progress txids in snapshot");
 DATA(insert OID = 2948 (  txid_visible_in_snapshot     PGNSP PGUID 12 1  0 0 0 f f f f t f i 2 0 16 "20 2970" _null_ _null_ _null_ _null_ txid_visible_in_snapshot _null_ _null_ _null_ ));
 DESCR("is txid visible in snapshot?");
 
-/* record comparison */
+/* record comparison using normal comparison rules */
 DATA(insert OID = 2981 (  record_eq               PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 16 "2249 2249" _null_ _null_ _null_ _null_ record_eq _null_ _null_ _null_ ));
 DATA(insert OID = 2982 (  record_ne               PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 16 "2249 2249" _null_ _null_ _null_ _null_ record_ne _null_ _null_ _null_ ));
 DATA(insert OID = 2983 (  record_lt               PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 16 "2249 2249" _null_ _null_ _null_ _null_ record_lt _null_ _null_ _null_ ));
@@ -4480,6 +4480,16 @@ DATA(insert OID = 2986 (  record_ge                 PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0
 DATA(insert OID = 2987 (  btrecordcmp     PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 23 "2249 2249" _null_ _null_ _null_ _null_ btrecordcmp _null_ _null_ _null_ ));
 DESCR("less-equal-greater");
 
+/* record comparison using raw byte images */
+DATA(insert OID = 3181 (  record_image_eq         PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 16 "2249 2249" _null_ _null_ _null_ _null_ record_image_eq _null_ _null_ _null_ ));
+DATA(insert OID = 3182 (  record_image_ne         PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 16 "2249 2249" _null_ _null_ _null_ _null_ record_image_ne _null_ _null_ _null_ ));
+DATA(insert OID = 3183 (  record_image_lt         PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 16 "2249 2249" _null_ _null_ _null_ _null_ record_image_lt _null_ _null_ _null_ ));
+DATA(insert OID = 3184 (  record_image_gt         PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 16 "2249 2249" _null_ _null_ _null_ _null_ record_image_gt _null_ _null_ _null_ ));
+DATA(insert OID = 3185 (  record_image_le         PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 16 "2249 2249" _null_ _null_ _null_ _null_ record_image_le _null_ _null_ _null_ ));
+DATA(insert OID = 3186 (  record_image_ge         PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 16 "2249 2249" _null_ _null_ _null_ _null_ record_image_ge _null_ _null_ _null_ ));
+DATA(insert OID = 3187 (  btrecordimagecmp        PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 23 "2249 2249" _null_ _null_ _null_ _null_ btrecordimagecmp _null_ _null_ _null_ ));
+DESCR("less-equal-greater based on byte images");
+
 /* Extensions */
 DATA(insert OID = 3082 (  pg_available_extensions              PGNSP PGUID 12 10 100 0 0 f f f f t t s 0 0 2249 "" "{19,25,25}" "{o,o,o}" "{name,default_version,comment}" _null_ pg_available_extensions _null_ _null_ _null_ ));
 DESCR("list available extensions");
index a5a0561a4a5926589921ab340e7916e899dd8451..ce3f00bc12dbe68d25ce7fb07d01ebe1626ce0a8 100644 (file)
@@ -631,6 +631,13 @@ extern Datum record_gt(PG_FUNCTION_ARGS);
 extern Datum record_le(PG_FUNCTION_ARGS);
 extern Datum record_ge(PG_FUNCTION_ARGS);
 extern Datum btrecordcmp(PG_FUNCTION_ARGS);
+extern Datum record_image_eq(PG_FUNCTION_ARGS);
+extern Datum record_image_ne(PG_FUNCTION_ARGS);
+extern Datum record_image_lt(PG_FUNCTION_ARGS);
+extern Datum record_image_gt(PG_FUNCTION_ARGS);
+extern Datum record_image_le(PG_FUNCTION_ARGS);
+extern Datum record_image_ge(PG_FUNCTION_ARGS);
+extern Datum btrecordimagecmp(PG_FUNCTION_ARGS);
 
 /* ruleutils.c */
 extern bool quote_all_identifiers;
index ddaa6460953a8c6b62c7e3a2a26a1757930f5a06..c2bb9b0c5ef390cc252f2fa7b28b364695e58d0c 100644 (file)
@@ -412,7 +412,7 @@ ERROR:  new data for "mv" contains duplicate rows without any NULL columns
 DETAIL:  Row: (1,10)
 DROP TABLE foo CASCADE;
 NOTICE:  drop cascades to materialized view mv
--- make sure that all indexes covered by unique indexes works
+-- make sure that all columns covered by unique indexes works
 CREATE TABLE foo(a, b, c) AS VALUES(1, 2, 3);
 CREATE MATERIALIZED VIEW mv AS SELECT * FROM foo;
 CREATE UNIQUE INDEX ON mv (a);
@@ -424,3 +424,23 @@ REFRESH MATERIALIZED VIEW mv;
 REFRESH MATERIALIZED VIEW CONCURRENTLY mv;
 DROP TABLE foo CASCADE;
 NOTICE:  drop cascades to materialized view mv
+-- make sure that types with unusual equality tests work
+CREATE TABLE boxes (id serial primary key, b box);
+INSERT INTO boxes (b) VALUES
+  ('(32,32),(31,31)'),
+  ('(2.0000004,2.0000004),(1,1)'),
+  ('(1.9999996,1.9999996),(1,1)');
+CREATE MATERIALIZED VIEW boxmv AS SELECT * FROM boxes;
+CREATE UNIQUE INDEX boxmv_id ON boxmv (id);
+UPDATE boxes SET b = '(2,2),(1,1)' WHERE id = 2;
+REFRESH MATERIALIZED VIEW CONCURRENTLY boxmv;
+SELECT * FROM boxmv ORDER BY id;
+ id |              b              
+----+-----------------------------
+  1 | (32,32),(31,31)
+  2 | (2,2),(1,1)
+  3 | (1.9999996,1.9999996),(1,1)
+(3 rows)
+
+DROP TABLE boxes CASCADE;
+NOTICE:  drop cascades to materialized view boxmv
index 515cd9daaa8f5ebf6714d9db6232ebe4e6a9cee7..57d614f651702f08e5d9592d422ed2984674aa1d 100644 (file)
@@ -1036,13 +1036,18 @@ FROM pg_amop p1 LEFT JOIN pg_operator p2 ON amopopr = p2.oid
 ORDER BY 1, 2, 3;
  amopmethod | amopstrategy | oprname 
 ------------+--------------+---------
+        403 |            1 | *<
         403 |            1 | <
         403 |            1 | ~<~
+        403 |            2 | *<=
         403 |            2 | <=
         403 |            2 | ~<=~
+        403 |            3 | *=
         403 |            3 | =
+        403 |            4 | *>=
         403 |            4 | >=
         403 |            4 | ~>=~
+        403 |            5 | *>
         403 |            5 | >
         403 |            5 | ~>~
         405 |            1 | =
@@ -1098,7 +1103,7 @@ ORDER BY 1, 2, 3;
        4000 |           15 | >
        4000 |           16 | @>
        4000 |           18 | =
-(62 rows)
+(67 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
index 620cbaca151ba51893d7e47fdc85c0dce7a56963..3ba6109d0b0d762b17097b169e9b2ed5cff243f6 100644 (file)
@@ -143,7 +143,7 @@ REFRESH MATERIALIZED VIEW mv;
 REFRESH MATERIALIZED VIEW CONCURRENTLY mv;
 DROP TABLE foo CASCADE;
 
--- make sure that all indexes covered by unique indexes works
+-- make sure that all columns covered by unique indexes works
 CREATE TABLE foo(a, b, c) AS VALUES(1, 2, 3);
 CREATE MATERIALIZED VIEW mv AS SELECT * FROM foo;
 CREATE UNIQUE INDEX ON mv (a);
@@ -154,3 +154,16 @@ INSERT INTO foo VALUES(3, 4, 5);
 REFRESH MATERIALIZED VIEW mv;
 REFRESH MATERIALIZED VIEW CONCURRENTLY mv;
 DROP TABLE foo CASCADE;
+
+-- make sure that types with unusual equality tests work
+CREATE TABLE boxes (id serial primary key, b box);
+INSERT INTO boxes (b) VALUES
+  ('(32,32),(31,31)'),
+  ('(2.0000004,2.0000004),(1,1)'),
+  ('(1.9999996,1.9999996),(1,1)');
+CREATE MATERIALIZED VIEW boxmv AS SELECT * FROM boxes;
+CREATE UNIQUE INDEX boxmv_id ON boxmv (id);
+UPDATE boxes SET b = '(2,2),(1,1)' WHERE id = 2;
+REFRESH MATERIALIZED VIEW CONCURRENTLY boxmv;
+SELECT * FROM boxmv ORDER BY id;
+DROP TABLE boxes CASCADE;