diff options
-rw-r--r-- | doc/src/sgml/func.sgml | 16 | ||||
-rw-r--r-- | doc/src/sgml/queries.sgml | 83 | ||||
-rw-r--r-- | src/backend/commands/indexcmds.c | 5 | ||||
-rw-r--r-- | src/backend/parser/parse_coerce.c | 64 | ||||
-rw-r--r-- | src/backend/utils/adt/rowtypes.c | 498 | ||||
-rw-r--r-- | src/include/catalog/catversion.h | 4 | ||||
-rw-r--r-- | src/include/catalog/pg_amop.h | 12 | ||||
-rw-r--r-- | src/include/catalog/pg_amproc.h | 3 | ||||
-rw-r--r-- | src/include/catalog/pg_opclass.h | 3 | ||||
-rw-r--r-- | src/include/catalog/pg_operator.h | 10 | ||||
-rw-r--r-- | src/include/catalog/pg_opfamily.h | 3 | ||||
-rw-r--r-- | src/include/catalog/pg_proc.h | 18 | ||||
-rw-r--r-- | src/include/catalog/pg_type.h | 6 | ||||
-rw-r--r-- | src/include/utils/builtins.h | 9 | ||||
-rw-r--r-- | src/test/regress/expected/rowtypes.out | 20 | ||||
-rw-r--r-- | src/test/regress/expected/with.out | 48 | ||||
-rw-r--r-- | src/test/regress/sql/rowtypes.sql | 7 | ||||
-rw-r--r-- | src/test/regress/sql/with.sql | 22 |
18 files changed, 809 insertions, 22 deletions
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 31ef6dc864d..e289326f2fb 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -1,4 +1,4 @@ -<!-- $PostgreSQL: pgsql/doc/src/sgml/func.sgml,v 1.448 2008/10/03 07:33:08 heikki Exp $ --> +<!-- $PostgreSQL: pgsql/doc/src/sgml/func.sgml,v 1.449 2008/10/13 16:25:19 tgl Exp $ --> <chapter id="functions"> <title>Functions and Operators</title> @@ -10667,6 +10667,20 @@ 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> </sect1> diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml index c2e3807920f..0232d80db71 100644 --- a/doc/src/sgml/queries.sgml +++ b/doc/src/sgml/queries.sgml @@ -1,4 +1,4 @@ -<!-- $PostgreSQL: pgsql/doc/src/sgml/queries.sgml,v 1.47 2008/10/07 19:27:03 tgl Exp $ --> +<!-- $PostgreSQL: pgsql/doc/src/sgml/queries.sgml,v 1.48 2008/10/13 16:25:19 tgl Exp $ --> <chapter id="queries"> <title>Queries</title> @@ -1604,8 +1604,85 @@ GROUP BY sub_part the recursive part of the query will eventually return no tuples, or else the query will loop indefinitely. Sometimes, using <literal>UNION</> instead of <literal>UNION ALL</> can accomplish this - by discarding rows that duplicate previous output rows; this catches - cycles that would otherwise repeat. A useful trick for testing queries + by discarding rows that duplicate previous output rows. However, often a + cycle does not involve output rows that are completely duplicate: it may be + necessary to check just one or a few fields to see if the same point has + been reached before. The standard method for handling such situations is + to compute an array of the already-visited values. For example, consider + the following query that searches a table <structname>graph</> using a + <structfield>link</> field: + +<programlisting> +WITH RECURSIVE search_graph(id, link, data, depth) AS ( + SELECT g.id, g.link, g.data, 1 + FROM graph g + UNION ALL + SELECT g.id, g.link, g.data, sg.depth + 1 + FROM graph g, search_graph sg + WHERE g.id = sg.link +) +SELECT * FROM search_graph; +</programlisting> + + This query will loop if the <structfield>link</> relationships contain + cycles. Because we require a <quote>depth</> output, just changing + <literal>UNION ALL</> to <literal>UNION</> would not eliminate the looping. + Instead we need to recognize whether we have reached the same row again + while following a particular path of links. We add two columns + <structfield>path</> and <structfield>cycle</> to the loop-prone query: + +<programlisting> +WITH RECURSIVE search_graph(id, link, data, depth, path, cycle) AS ( + SELECT g.id, g.link, g.data, 1, + ARRAY[g.id], + false + FROM graph g + UNION ALL + SELECT g.id, g.link, g.data, sg.depth + 1, + path || ARRAY[g.id], + g.id = ANY(path) + FROM graph g, search_graph sg + WHERE g.id = sg.link AND NOT cycle +) +SELECT * FROM search_graph; +</programlisting> + + Aside from preventing cycles, the array value is often useful in its own + right as representing the <quote>path</> taken to reach any particular row. + </para> + + <para> + In the general case where more than one field needs to be checked to + recognize a cycle, use an array of rows. For example, if we needed to + compare fields <structfield>f1</> and <structfield>f2</>: + +<programlisting> +WITH RECURSIVE search_graph(id, link, data, depth, path, cycle) AS ( + SELECT g.id, g.link, g.data, 1, + ARRAY[ROW(g.f1, g.f2)], + false + FROM graph g + UNION ALL + SELECT g.id, g.link, g.data, sg.depth + 1, + path || ARRAY[ROW(g.f1, g.f2)], + ROW(g.f1, g.f2) = ANY(path) + FROM graph g, search_graph sg + WHERE g.id = sg.link AND NOT cycle +) +SELECT * FROM search_graph; +</programlisting> + </para> + + <tip> + <para> + Omit the <literal>ROW()</> syntax in the common case where only one field + needs to be checked to recognize a cycle. This allows a simple array + rather than a composite-type array to be used, gaining efficiency. + </para> + </tip> + + <para> + A helpful trick for testing queries when you are not certain if they might loop is to place a <literal>LIMIT</> in the parent query. For example, this query would loop forever without the <literal>LIMIT</>: diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index cbf440fc438..c4e548becb7 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/commands/indexcmds.c,v 1.179 2008/08/25 22:42:32 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/commands/indexcmds.c,v 1.180 2008/10/13 16:25:19 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -795,7 +795,8 @@ ComputeIndexAttrs(IndexInfo *indexInfo, atttype = attform->atttypid; ReleaseSysCache(atttuple); } - else if (attribute->expr && IsA(attribute->expr, Var)) + else if (attribute->expr && IsA(attribute->expr, Var) && + ((Var *) attribute->expr)->varattno != InvalidAttrNumber) { /* Tricky tricky, he wrote (column) ... treat as simple attr */ Var *var = (Var *) attribute->expr; diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c index 69efaddfecf..7c66c638047 100644 --- a/src/backend/parser/parse_coerce.c +++ b/src/backend/parser/parse_coerce.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/parser/parse_coerce.c,v 2.168 2008/10/06 17:39:26 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/parser/parse_coerce.c,v 2.169 2008/10/13 16:25:19 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -46,6 +46,7 @@ static Node *coerce_record_to_complex(ParseState *pstate, Node *node, CoercionContext ccontext, CoercionForm cformat, int location); +static bool is_complex_array(Oid typid); /* @@ -402,6 +403,21 @@ coerce_type(ParseState *pstate, Node *node, /* NB: we do NOT want a RelabelType here */ return node; } +#ifdef NOT_USED + if (inputTypeId == RECORDARRAYOID && + is_complex_array(targetTypeId)) + { + /* Coerce record[] to a specific complex array type */ + /* not implemented yet ... */ + } +#endif + if (targetTypeId == RECORDARRAYOID && + is_complex_array(inputTypeId)) + { + /* Coerce a specific complex array type to record[] */ + /* NB: we do NOT want a RelabelType here */ + return node; + } if (typeInheritsFrom(inputTypeId, targetTypeId)) { /* @@ -492,6 +508,23 @@ can_coerce_type(int nargs, Oid *input_typeids, Oid *target_typeids, ISCOMPLEX(inputTypeId)) continue; +#ifdef NOT_USED /* not implemented yet */ + /* + * If input is record[] and target is a composite array type, + * assume we can coerce (may need tighter checking here) + */ + if (inputTypeId == RECORDARRAYOID && + is_complex_array(targetTypeId)) + continue; +#endif + + /* + * If input is a composite array type and target is record[], accept + */ + if (targetTypeId == RECORDARRAYOID && + is_complex_array(inputTypeId)) + continue; + /* * If input is a class type that inherits from target, accept */ @@ -1724,8 +1757,8 @@ IsPreferredType(TYPCATEGORY category, Oid type) * invokable, no-function-needed pg_cast entry. Also, a domain is always * binary-coercible to its base type, though *not* vice versa (in the other * direction, one must apply domain constraint checks before accepting the - * value as legitimate). We also need to special-case the polymorphic - * ANYARRAY type. + * value as legitimate). We also need to special-case various polymorphic + * types. * * This function replaces IsBinaryCompatible(), which was an inherently * symmetric test. Since the pg_cast entries aren't necessarily symmetric, @@ -1765,6 +1798,16 @@ IsBinaryCoercible(Oid srctype, Oid targettype) if (type_is_enum(srctype)) return true; + /* Also accept any composite type as coercible to RECORD */ + if (targettype == RECORDOID) + if (ISCOMPLEX(srctype)) + return true; + + /* Also accept any composite array type as coercible to RECORD[] */ + if (targettype == RECORDARRAYOID) + if (is_complex_array(srctype)) + return true; + /* Else look in pg_cast */ tuple = SearchSysCache(CASTSOURCETARGET, ObjectIdGetDatum(srctype), @@ -2002,3 +2045,18 @@ find_typmod_coercion_function(Oid typeId, return result; } + +/* + * is_complex_array + * Is this type an array of composite? + * + * Note: this will not return true for record[]; check for RECORDARRAYOID + * separately if needed. + */ +static bool +is_complex_array(Oid typid) +{ + Oid elemtype = get_element_type(typid); + + return (OidIsValid(elemtype) && ISCOMPLEX(elemtype)); +} diff --git a/src/backend/utils/adt/rowtypes.c b/src/backend/utils/adt/rowtypes.c index b7981660ef2..f08244216e8 100644 --- a/src/backend/utils/adt/rowtypes.c +++ b/src/backend/utils/adt/rowtypes.c @@ -1,14 +1,14 @@ /*------------------------------------------------------------------------- * * rowtypes.c - * I/O functions for generic composite types. + * I/O and comparison functions for generic composite types. * * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/adt/rowtypes.c,v 1.21 2008/05/12 00:00:51 alvherre Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/rowtypes.c,v 1.22 2008/10/13 16:25:19 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -42,6 +42,24 @@ typedef struct RecordIOData ColumnIOData columns[1]; /* VARIABLE LENGTH ARRAY */ } RecordIOData; +/* + * structure to cache metadata needed for record comparison + */ +typedef struct ColumnCompareData +{ + TypeCacheEntry *typentry; /* has everything we need, actually */ +} ColumnCompareData; + +typedef struct RecordCompareData +{ + int ncolumns; /* allocated length of columns[] */ + Oid record1_type; + int32 record1_typmod; + Oid record2_type; + int32 record2_typmod; + ColumnCompareData columns[1]; /* VARIABLE LENGTH ARRAY */ +} RecordCompareData; + /* * record_in - input routine for any composite type. @@ -734,3 +752,479 @@ record_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } + + +/* + * record_cmp() + * Internal comparison function for records. + * + * Returns -1, 0 or 1 + * + * Do not assume that the two inputs are exactly the same record type; + * for instance we might be comparing an anonymous ROW() construct against a + * named composite type. We will compare as long as they have the same number + * of non-dropped columns of the same types. + */ +static int +record_cmp(FunctionCallInfo fcinfo) +{ + HeapTupleHeader record1 = PG_GETARG_HEAPTUPLEHEADER(0); + HeapTupleHeader record2 = PG_GETARG_HEAPTUPLEHEADER(1); + int 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) + { + TypeCacheEntry *typentry; + FunctionCallInfoData locfcinfo; + int32 cmpresult; + + /* + * 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))); + + /* + * Lookup the comparison function if not done already + */ + typentry = my_extra->columns[j].typentry; + if (typentry == NULL || + typentry->type_id != tupdesc1->attrs[i1]->atttypid) + { + typentry = lookup_type_cache(tupdesc1->attrs[i1]->atttypid, + TYPECACHE_CMP_PROC_FINFO); + if (!OidIsValid(typentry->cmp_proc_finfo.fn_oid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_FUNCTION), + errmsg("could not identify a comparison function for type %s", + format_type_be(typentry->type_id)))); + my_extra->columns[j].typentry = typentry; + } + + /* + * We consider two NULLs equal; NULL > not-NULL. + */ + if (!nulls1[i1] || !nulls2[i2]) + { + 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 */ + InitFunctionCallInfoData(locfcinfo, &typentry->cmp_proc_finfo, 2, + NULL, NULL); + locfcinfo.arg[0] = values1[i1]; + locfcinfo.arg[1] = values2[i2]; + locfcinfo.argnull[0] = false; + locfcinfo.argnull[1] = false; + locfcinfo.isnull = false; + cmpresult = DatumGetInt32(FunctionCallInvoke(&locfcinfo)); + + 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_eq : + * compares two records for equality + * result : + * returns true if the records are equal, false otherwise. + * + * Note: we do not use record_cmp here, since equality may be meaningful in + * datatypes that don't have a total ordering (and hence no btree support). + */ +Datum +record_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) + { + TypeCacheEntry *typentry; + FunctionCallInfoData locfcinfo; + bool oprresult; + + /* + * 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))); + + /* + * Lookup the equality function if not done already + */ + typentry = my_extra->columns[j].typentry; + if (typentry == NULL || + typentry->type_id != tupdesc1->attrs[i1]->atttypid) + { + typentry = lookup_type_cache(tupdesc1->attrs[i1]->atttypid, + TYPECACHE_EQ_OPR_FINFO); + if (!OidIsValid(typentry->eq_opr_finfo.fn_oid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_FUNCTION), + errmsg("could not identify an equality operator for type %s", + format_type_be(typentry->type_id)))); + my_extra->columns[j].typentry = typentry; + } + + /* + * 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 */ + InitFunctionCallInfoData(locfcinfo, &typentry->eq_opr_finfo, 2, + NULL, NULL); + locfcinfo.arg[0] = values1[i1]; + locfcinfo.arg[1] = values2[i2]; + locfcinfo.argnull[0] = false; + locfcinfo.argnull[1] = false; + locfcinfo.isnull = false; + oprresult = DatumGetBool(FunctionCallInvoke(&locfcinfo)); + if (!oprresult) + { + result = false; + 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_ne(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(!DatumGetBool(record_eq(fcinfo))); +} + +Datum +record_lt(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(record_cmp(fcinfo) < 0); +} + +Datum +record_gt(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(record_cmp(fcinfo) > 0); +} + +Datum +record_le(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(record_cmp(fcinfo) <= 0); +} + +Datum +record_ge(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(record_cmp(fcinfo) >= 0); +} + +Datum +btrecordcmp(PG_FUNCTION_ARGS) +{ + PG_RETURN_INT32(record_cmp(fcinfo)); +} diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 3f277879922..0ab03e0578e 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -37,7 +37,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/catalog/catversion.h,v 1.496 2008/10/06 17:39:26 tgl Exp $ + * $PostgreSQL: pgsql/src/include/catalog/catversion.h,v 1.497 2008/10/13 16:25:19 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -53,6 +53,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 200810063 +#define CATALOG_VERSION_NO 200810131 #endif diff --git a/src/include/catalog/pg_amop.h b/src/include/catalog/pg_amop.h index 8368d4bbc79..f768da3f617 100644 --- a/src/include/catalog/pg_amop.h +++ b/src/include/catalog/pg_amop.h @@ -29,7 +29,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/catalog/pg_amop.h,v 1.87 2008/05/27 00:13:09 tgl Exp $ + * $PostgreSQL: pgsql/src/include/catalog/pg_amop.h,v 1.88 2008/10/13 16:25:19 tgl Exp $ * * NOTES * the genbki.sh script reads this file and generates .bki @@ -471,6 +471,16 @@ DATA(insert ( 397 2277 2277 4 1075 403 )); DATA(insert ( 397 2277 2277 5 1073 403 )); /* + * btree record_ops + */ + +DATA(insert ( 2994 2249 2249 1 2990 403 )); +DATA(insert ( 2994 2249 2249 2 2992 403 )); +DATA(insert ( 2994 2249 2249 3 2988 403 )); +DATA(insert ( 2994 2249 2249 4 2993 403 )); +DATA(insert ( 2994 2249 2249 5 2991 403 )); + +/* * btree uuid_ops */ diff --git a/src/include/catalog/pg_amproc.h b/src/include/catalog/pg_amproc.h index 5ca46600e3e..c0f91a64b60 100644 --- a/src/include/catalog/pg_amproc.h +++ b/src/include/catalog/pg_amproc.h @@ -22,7 +22,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/catalog/pg_amproc.h,v 1.73 2008/05/27 00:13:09 tgl Exp $ + * $PostgreSQL: pgsql/src/include/catalog/pg_amproc.h,v 1.74 2008/10/13 16:25:19 tgl Exp $ * * NOTES * the genbki.sh script reads this file and generates .bki @@ -111,6 +111,7 @@ DATA(insert ( 1986 19 19 1 359 )); DATA(insert ( 1988 1700 1700 1 1769 )); DATA(insert ( 1989 26 26 1 356 )); DATA(insert ( 1991 30 30 1 404 )); +DATA(insert ( 2994 2249 2249 1 2987 )); 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 7c4d95003c8..6251bc98299 100644 --- a/src/include/catalog/pg_opclass.h +++ b/src/include/catalog/pg_opclass.h @@ -28,7 +28,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/catalog/pg_opclass.h,v 1.83 2008/09/15 18:43:41 tgl Exp $ + * $PostgreSQL: pgsql/src/include/catalog/pg_opclass.h,v 1.84 2008/10/13 16:25:20 tgl Exp $ * * NOTES * the genbki.sh script reads this file and generates .bki @@ -137,6 +137,7 @@ DATA(insert OID = 1981 ( 403 oid_ops PGNSP PGUID 1989 26 t 0 )); 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 text_ops PGNSP PGUID 1994 25 t 0 )); DATA(insert ( 405 text_ops PGNSP PGUID 1995 25 t 0 )); DATA(insert ( 403 time_ops PGNSP PGUID 1996 1083 t 0 )); diff --git a/src/include/catalog/pg_operator.h b/src/include/catalog/pg_operator.h index e19bb0f9271..d1125119328 100644 --- a/src/include/catalog/pg_operator.h +++ b/src/include/catalog/pg_operator.h @@ -8,7 +8,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/catalog/pg_operator.h,v 1.163 2008/09/19 19:03:40 tgl Exp $ + * $PostgreSQL: pgsql/src/include/catalog/pg_operator.h,v 1.164 2008/10/13 16:25:20 tgl Exp $ * * NOTES * the genbki.sh script reads this file and generates .bki @@ -933,6 +933,14 @@ DATA(insert OID = 3694 ( "<@" PGNSP PGUID b f f 3615 3615 16 3693 0 tsq_m DATA(insert OID = 3762 ( "@@" PGNSP PGUID b f f 25 25 16 0 0 ts_match_tt contsel contjoinsel )); DATA(insert OID = 3763 ( "@@" PGNSP PGUID b f f 25 3615 16 0 0 ts_match_tq contsel contjoinsel )); +/* generic record comparison operators */ +DATA(insert OID = 2988 ( "=" PGNSP PGUID b t f 2249 2249 16 2988 2989 record_eq eqsel eqjoinsel )); +DATA(insert OID = 2989 ( "<>" PGNSP PGUID b f f 2249 2249 16 2989 2988 record_ne neqsel neqjoinsel )); +DATA(insert OID = 2990 ( "<" PGNSP PGUID b f f 2249 2249 16 2991 2993 record_lt scalarltsel scalarltjoinsel )); +DATA(insert OID = 2991 ( ">" PGNSP PGUID b f f 2249 2249 16 2990 2992 record_gt scalargtsel scalargtjoinsel )); +DATA(insert OID = 2992 ( "<=" PGNSP PGUID b f f 2249 2249 16 2993 2991 record_le scalarltsel scalarltjoinsel )); +DATA(insert OID = 2993 ( ">=" PGNSP PGUID b f f 2249 2249 16 2992 2990 record_ge scalargtsel scalargtjoinsel )); + /* * function prototypes diff --git a/src/include/catalog/pg_opfamily.h b/src/include/catalog/pg_opfamily.h index e6066f06566..aa6e7809e7d 100644 --- a/src/include/catalog/pg_opfamily.h +++ b/src/include/catalog/pg_opfamily.h @@ -8,7 +8,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/catalog/pg_opfamily.h,v 1.9 2008/05/27 00:13:09 tgl Exp $ + * $PostgreSQL: pgsql/src/include/catalog/pg_opfamily.h,v 1.10 2008/10/13 16:25:20 tgl Exp $ * * NOTES * the genbki.sh script reads this file and generates .bki @@ -94,6 +94,7 @@ DATA(insert OID = 1989 ( 403 oid_ops PGNSP PGUID )); 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 = 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 31449b60e0f..70dc0a319a3 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/catalog/pg_proc.h,v 1.518 2008/10/06 13:05:37 mha Exp $ + * $PostgreSQL: pgsql/src/include/catalog/pg_proc.h,v 1.519 2008/10/13 16:25:20 tgl Exp $ * * NOTES * The script catalog/genbki.sh reads this file and generates .bki @@ -4560,6 +4560,22 @@ DESCR("get set of in-progress txids in snapshot"); DATA(insert OID = 2948 ( txid_visible_in_snapshot PGNSP PGUID 12 1 0 0 f f t f i 2 16 "20 2970" _null_ _null_ _null_ txid_visible_in_snapshot _null_ _null_ _null_ )); DESCR("is txid visible in snapshot?"); +/* record comparison */ +DATA(insert OID = 2981 ( record_eq PGNSP PGUID 12 1 0 0 f f t f i 2 16 "2249 2249" _null_ _null_ _null_ record_eq _null_ _null_ _null_ )); +DESCR("record equal"); +DATA(insert OID = 2982 ( record_ne PGNSP PGUID 12 1 0 0 f f t f i 2 16 "2249 2249" _null_ _null_ _null_ record_ne _null_ _null_ _null_ )); +DESCR("record not equal"); +DATA(insert OID = 2983 ( record_lt PGNSP PGUID 12 1 0 0 f f t f i 2 16 "2249 2249" _null_ _null_ _null_ record_lt _null_ _null_ _null_ )); +DESCR("record less than"); +DATA(insert OID = 2984 ( record_gt PGNSP PGUID 12 1 0 0 f f t f i 2 16 "2249 2249" _null_ _null_ _null_ record_gt _null_ _null_ _null_ )); +DESCR("record greater than"); +DATA(insert OID = 2985 ( record_le PGNSP PGUID 12 1 0 0 f f t f i 2 16 "2249 2249" _null_ _null_ _null_ record_le _null_ _null_ _null_ )); +DESCR("record less than or equal"); +DATA(insert OID = 2986 ( record_ge PGNSP PGUID 12 1 0 0 f f t f i 2 16 "2249 2249" _null_ _null_ _null_ record_ge _null_ _null_ _null_ )); +DESCR("record greater than or equal"); +DATA(insert OID = 2987 ( btrecordcmp PGNSP PGUID 12 1 0 0 f f t f i 2 23 "2249 2249" _null_ _null_ _null_ btrecordcmp _null_ _null_ _null_ )); +DESCR("btree less-equal-greater"); + /* * Symbolic values for provolatile column: these indicate whether the result diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h index d4f4fc0968b..f3367b41b59 100644 --- a/src/include/catalog/pg_type.h +++ b/src/include/catalog/pg_type.h @@ -8,7 +8,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/catalog/pg_type.h,v 1.200 2008/09/25 03:28:56 tgl Exp $ + * $PostgreSQL: pgsql/src/include/catalog/pg_type.h,v 1.201 2008/10/13 16:25:20 tgl Exp $ * * NOTES * the genbki.sh script reads this file and generates .bki @@ -597,8 +597,10 @@ DATA(insert OID = 2949 ( _txid_snapshot PGNSP PGUID -1 f b A f t \054 0 2970 0 a * but there is now support for it in records and arrays. Perhaps we should * just treat it as a regular base type? */ -DATA(insert OID = 2249 ( record PGNSP PGUID -1 f p P f t \054 0 0 0 record_in record_out record_recv record_send - - - d x f 0 -1 0 _null_ _null_ )); +DATA(insert OID = 2249 ( record PGNSP PGUID -1 f p P f t \054 0 0 2287 record_in record_out record_recv record_send - - - d x f 0 -1 0 _null_ _null_ )); #define RECORDOID 2249 +DATA(insert OID = 2287 ( _record PGNSP PGUID -1 f p P f t \054 0 2249 0 array_in array_out array_recv array_send - - - d x f 0 -1 0 _null_ _null_ )); +#define RECORDARRAYOID 2287 DATA(insert OID = 2275 ( cstring PGNSP PGUID -2 f p P f t \054 0 0 1263 cstring_in cstring_out cstring_recv cstring_send - - - c p f 0 -1 0 _null_ _null_ )); #define CSTRINGOID 2275 DATA(insert OID = 2276 ( any PGNSP PGUID 4 t p P f t \054 0 0 0 any_in any_out - - - - - i p f 0 -1 0 _null_ _null_ )); diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h index a98f93165f9..b9efbe88b33 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/utils/builtins.h,v 1.323 2008/10/06 20:29:38 tgl Exp $ + * $PostgreSQL: pgsql/src/include/utils/builtins.h,v 1.324 2008/10/13 16:25:20 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -524,6 +524,13 @@ extern Datum record_in(PG_FUNCTION_ARGS); extern Datum record_out(PG_FUNCTION_ARGS); extern Datum record_recv(PG_FUNCTION_ARGS); extern Datum record_send(PG_FUNCTION_ARGS); +extern Datum record_eq(PG_FUNCTION_ARGS); +extern Datum record_ne(PG_FUNCTION_ARGS); +extern Datum record_lt(PG_FUNCTION_ARGS); +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); /* ruleutils.c */ extern Datum pg_get_ruledef(PG_FUNCTION_ARGS); diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out index 73ae3ce10ef..e1181a61b83 100644 --- a/src/test/regress/expected/rowtypes.out +++ b/src/test/regress/expected/rowtypes.out @@ -269,3 +269,23 @@ select ROW() = ROW(); ERROR: cannot compare rows of zero length LINE 1: select ROW() = ROW(); ^ +-- Check ability to create arrays of anonymous rowtypes +select array[ row(1,2), row(3,4), row(5,6) ]; + array +--------------------------- + {"(1,2)","(3,4)","(5,6)"} +(1 row) + +-- Check ability to compare an anonymous row to elements of an array +select row(1,1.1) = any (array[ row(7,7.7), row(1,1.1), row(0,0.0) ]); + ?column? +---------- + t +(1 row) + +select row(1,1.1) = any (array[ row(7,7.7), row(1,1.0), row(0,0.0) ]); + ?column? +---------- + f +(1 row) + diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out index fe7561065ed..e8d3e43b7b6 100644 --- a/src/test/regress/expected/with.out +++ b/src/test/regress/expected/with.out @@ -452,6 +452,54 @@ SELECT t1.id, count(t2.*) FROM t AS t1 JOIN t AS t2 ON (2 rows) -- +-- test cycle detection +-- +create temp table graph( f int, t int, label text ); +insert into graph values + (1, 2, 'arc 1 -> 2'), + (1, 3, 'arc 1 -> 3'), + (2, 3, 'arc 2 -> 3'), + (1, 4, 'arc 1 -> 4'), + (4, 5, 'arc 4 -> 5'), + (5, 1, 'arc 5 -> 1'); +with recursive search_graph(f, t, label, path, cycle) as ( + select *, array[row(g.f, g.t)], false from graph g + union all + select g.*, path || array[row(g.f, g.t)], row(g.f, g.t) = any(path) + from graph g, search_graph sg + where g.f = sg.t and not cycle +) +select * from search_graph; + f | t | label | path | cycle +---+---+------------+-------------------------------------------+------- + 1 | 2 | arc 1 -> 2 | {"(1,2)"} | f + 1 | 3 | arc 1 -> 3 | {"(1,3)"} | f + 2 | 3 | arc 2 -> 3 | {"(2,3)"} | f + 1 | 4 | arc 1 -> 4 | {"(1,4)"} | f + 4 | 5 | arc 4 -> 5 | {"(4,5)"} | f + 5 | 1 | arc 5 -> 1 | {"(5,1)"} | f + 1 | 2 | arc 1 -> 2 | {"(5,1)","(1,2)"} | f + 1 | 3 | arc 1 -> 3 | {"(5,1)","(1,3)"} | f + 1 | 4 | arc 1 -> 4 | {"(5,1)","(1,4)"} | f + 2 | 3 | arc 2 -> 3 | {"(1,2)","(2,3)"} | f + 4 | 5 | arc 4 -> 5 | {"(1,4)","(4,5)"} | f + 5 | 1 | arc 5 -> 1 | {"(4,5)","(5,1)"} | f + 1 | 2 | arc 1 -> 2 | {"(4,5)","(5,1)","(1,2)"} | f + 1 | 3 | arc 1 -> 3 | {"(4,5)","(5,1)","(1,3)"} | f + 1 | 4 | arc 1 -> 4 | {"(4,5)","(5,1)","(1,4)"} | f + 2 | 3 | arc 2 -> 3 | {"(5,1)","(1,2)","(2,3)"} | f + 4 | 5 | arc 4 -> 5 | {"(5,1)","(1,4)","(4,5)"} | f + 5 | 1 | arc 5 -> 1 | {"(1,4)","(4,5)","(5,1)"} | f + 1 | 2 | arc 1 -> 2 | {"(1,4)","(4,5)","(5,1)","(1,2)"} | f + 1 | 3 | arc 1 -> 3 | {"(1,4)","(4,5)","(5,1)","(1,3)"} | f + 1 | 4 | arc 1 -> 4 | {"(1,4)","(4,5)","(5,1)","(1,4)"} | t + 2 | 3 | arc 2 -> 3 | {"(4,5)","(5,1)","(1,2)","(2,3)"} | f + 4 | 5 | arc 4 -> 5 | {"(4,5)","(5,1)","(1,4)","(4,5)"} | t + 5 | 1 | arc 5 -> 1 | {"(5,1)","(1,4)","(4,5)","(5,1)"} | t + 2 | 3 | arc 2 -> 3 | {"(1,4)","(4,5)","(5,1)","(1,2)","(2,3)"} | f +(25 rows) + +-- -- test multiple WITH queries -- WITH RECURSIVE diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql index 7e5c554d3b7..a8520c5bd2c 100644 --- a/src/test/regress/sql/rowtypes.sql +++ b/src/test/regress/sql/rowtypes.sql @@ -113,3 +113,10 @@ order by thousand, tenthous; select ROW(); select ROW() IS NULL; select ROW() = ROW(); + +-- Check ability to create arrays of anonymous rowtypes +select array[ row(1,2), row(3,4), row(5,6) ]; + +-- Check ability to compare an anonymous row to elements of an array +select row(1,1.1) = any (array[ row(7,7.7), row(1,1.1), row(0,0.0) ]); +select row(1,1.1) = any (array[ row(7,7.7), row(1,1.0), row(0,0.0) ]); diff --git a/src/test/regress/sql/with.sql b/src/test/regress/sql/with.sql index 54d311101d1..d37f0d9723e 100644 --- a/src/test/regress/sql/with.sql +++ b/src/test/regress/sql/with.sql @@ -251,6 +251,28 @@ SELECT t1.id, count(t2.*) FROM t AS t1 JOIN t AS t2 ON ORDER BY t1.id; -- +-- test cycle detection +-- +create temp table graph( f int, t int, label text ); + +insert into graph values + (1, 2, 'arc 1 -> 2'), + (1, 3, 'arc 1 -> 3'), + (2, 3, 'arc 2 -> 3'), + (1, 4, 'arc 1 -> 4'), + (4, 5, 'arc 4 -> 5'), + (5, 1, 'arc 5 -> 1'); + +with recursive search_graph(f, t, label, path, cycle) as ( + select *, array[row(g.f, g.t)], false from graph g + union all + select g.*, path || array[row(g.f, g.t)], row(g.f, g.t) = any(path) + from graph g, search_graph sg + where g.f = sg.t and not cycle +) +select * from search_graph; + +-- -- test multiple WITH queries -- WITH RECURSIVE |