diff --git a/contrib/citext/expected/citext.out b/contrib/citext/expected/citext.out index a6265d857dc..fea4bcbb1eb 100644 --- a/contrib/citext/expected/citext.out +++ b/contrib/citext/expected/citext.out @@ -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) + diff --git a/contrib/citext/expected/citext_1.out b/contrib/citext/expected/citext_1.out index 36342be7c1d..101f10526ea 100644 --- a/contrib/citext/expected/citext_1.out +++ b/contrib/citext/expected/citext_1.out @@ -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) + diff --git a/contrib/citext/sql/citext.sql b/contrib/citext/sql/citext.sql index 65ef05b9312..8a3532bea59 100644 --- a/contrib/citext/sql/citext.sql +++ b/contrib/citext/sql/citext.sql @@ -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; diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 7dd1ef2ea15..c3090dd2b9f 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -12739,7 +12739,7 @@ WHERE EXISTS (SELECT 1 FROM tab2 WHERE col2 = tab1.col2); See for details about the meaning - of a row-wise comparison. + of a row constructor comparison. @@ -12795,12 +12795,12 @@ WHERE EXISTS (SELECT 1 FROM tab2 WHERE col2 = tab1.col2); See for details about the meaning - of a row-wise comparison. + of a row constructor comparison. - Row-wise Comparison + Single-row Comparison comparison @@ -12823,7 +12823,7 @@ WHERE EXISTS (SELECT 1 FROM tab2 WHERE col2 = tab1.col2); See for details about the meaning - of a row-wise comparison. + of a row constructor comparison. @@ -12852,13 +12852,23 @@ WHERE EXISTS (SELECT 1 FROM tab2 WHERE col2 = tab1.col2); SOME + + composite type + comparison + + row-wise comparison comparison - row-wise + composite type + + + + comparison + row constructor @@ -13023,7 +13033,7 @@ AND - Row-wise Comparison + Row Constructor Comparison row_constructor operator row_constructor @@ -13033,20 +13043,25 @@ AND Each side is a row constructor, as described in . 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 operator is + Each side is evaluated and they are compared row-wise. Row constructor + comparisons are allowed when the operator is =, <>, <, <=, > or - >=, - 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 = member of a B-tree operator - class.) + >=. + Every row element must be of a type which has a default B-tree operator + class or the attempted comparison may generate an error. + + + Errors related to the number or types of elements might not occur if + the comparison is resolved using earlier columns. + + + The = and <> cases work slightly differently from the others. Two rows are considered @@ -13104,20 +13119,64 @@ AND be either true or false, never null. - - - 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. - PostgreSQL does this only when comparing the - results of two row constructors or comparing a row constructor to the - output of a subquery (as in ). - 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. - - + + + Composite Type Comparison + + +record operator record + + + + 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. + PostgreSQL does this only when comparing the + results of two row constructors (as in + ) or comparing a row constructor + to the output of a subquery (as in ). + 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. + + + + Each side is evaluated and they are compared row-wise. Composite type + comparisons are allowed when the operator is + =, + <>, + <, + <=, + > or + >=, + 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 = member of a B-tree operator + class.) The default behavior of the above operators is the same as for + IS [ NOT ] DISTINCT FROM for row constructors (see + ). + + + + To support matching of rows which include elements without a default + B-tree operator class, the following operators are defined for composite + type comparison: + *=, + *<>, + *<, + *<=, + *>, and + *>=. + 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. + diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 238ccc72f52..fcfc678813d 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -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) diff --git a/src/backend/utils/adt/rowtypes.c b/src/backend/utils/adt/rowtypes.c index 1bd473af657..0bcbb5d6648 100644 --- a/src/backend/utils/adt/rowtypes.c +++ b/src/backend/utils/adt/rowtypes.c @@ -17,6 +17,7 @@ #include #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)); +} diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 3a18935072e..5b2749c417e 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -53,6 +53,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 201309051 +#define CATALOG_VERSION_NO 201310091 #endif diff --git a/src/include/catalog/pg_amop.h b/src/include/catalog/pg_amop.h index d2003487472..c8a548cead1 100644 --- a/src/include/catalog/pg_amop.h +++ b/src/include/catalog/pg_amop.h @@ -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 */ diff --git a/src/include/catalog/pg_amproc.h b/src/include/catalog/pg_amproc.h index 7155cb29a01..53a3a7a6b54 100644 --- a/src/include/catalog/pg_amproc.h +++ b/src/include/catalog/pg_amproc.h @@ -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 )); diff --git a/src/include/catalog/pg_opclass.h b/src/include/catalog/pg_opclass.h index f714db567e8..0a3eb3e1f79 100644 --- a/src/include/catalog/pg_opclass.h +++ b/src/include/catalog/pg_opclass.h @@ -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 )); diff --git a/src/include/catalog/pg_operator.h b/src/include/catalog/pg_operator.h index 5f28fc31160..0350ef66871 100644 --- a/src/include/catalog/pg_operator.h +++ b/src/include/catalog/pg_operator.h @@ -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"); diff --git a/src/include/catalog/pg_opfamily.h b/src/include/catalog/pg_opfamily.h index 832f19422dc..12ba456ffe8 100644 --- a/src/include/catalog/pg_opfamily.h +++ b/src/include/catalog/pg_opfamily.h @@ -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 )); diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index f03dd0b7da4..3e523989c38 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -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"); diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h index a5a0561a4a5..ce3f00bc12d 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -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; diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out index ddaa6460953..c2bb9b0c5ef 100644 --- a/src/test/regress/expected/matview.out +++ b/src/test/regress/expected/matview.out @@ -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 diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out index 515cd9daaa8..57d614f6517 100644 --- a/src/test/regress/expected/opr_sanity.out +++ b/src/test/regress/expected/opr_sanity.out @@ -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 diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql index 620cbaca151..3ba6109d0b0 100644 --- a/src/test/regress/sql/matview.sql +++ b/src/test/regress/sql/matview.sql @@ -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;