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)
+
 
  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)
+
 
 
 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;
 
 
   <para>
    See <xref linkend="row-wise-comparison"> for details about the meaning
-   of a row-wise comparison.
+   of a row constructor comparison.
   </para>
   </sect2>
 
 
   <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>
 
   <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>
    <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>
   </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>
    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><></>,
    <literal><</>,
    <literal><=</>,
    <literal>></> or
-   <literal>>=</>,
-   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>>=</>.
+   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><></> cases work slightly differently
    from the others.  Two rows are considered
    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><></>,
+   <literal><</>,
+   <literal><=</>,
+   <literal>></> or
+   <literal>>=</>,
+   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>*<></>,
+   <literal>*<</>,
+   <literal>*<=</>,
+   <literal>*></>, and
+   <literal>*>=</>.
+   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>
 
 
                     "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);
                /*
                 * 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;
                 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)
 
 #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"
 {
    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));
+}
 
  */
 
 /*                         yyyymmddN */
-#define CATALOG_VERSION_NO 201309051
+#define CATALOG_VERSION_NO 201310091
 
 #endif
 
 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
  */
 
 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 ));
 
 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 ));
 
 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");
 
 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 ));
 
 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_ ));
 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");
 
 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;
 
 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);
 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
 
 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 | =
        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
 
 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);
 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;