From e3b0117459fd24b15cb5e88f563b5d87f051cfdc Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Mon, 13 Oct 2008 16:25:20 +0000 Subject: [PATCH] Implement comparison of generic records (composite types), and invent a pseudo-type record[] to represent arrays of possibly-anonymous composite types. Since composite datums carry their own type identification, no extra knowledge is needed at the array level. The main reason for doing this right now is that it is necessary to support the general case of detection of cycles in recursive queries: if you need to compare more than one column to detect a cycle, you need to compare a ROW() to an array built from ROW()s, at least if you want to do it as the spec suggests. Add some documentation and regression tests concerning the cycle detection issue. --- doc/src/sgml/func.sgml | 16 +- doc/src/sgml/queries.sgml | 83 ++++- src/backend/commands/indexcmds.c | 5 +- src/backend/parser/parse_coerce.c | 64 +++- src/backend/utils/adt/rowtypes.c | 498 ++++++++++++++++++++++++- src/include/catalog/catversion.h | 4 +- src/include/catalog/pg_amop.h | 12 +- src/include/catalog/pg_amproc.h | 3 +- src/include/catalog/pg_opclass.h | 3 +- src/include/catalog/pg_operator.h | 10 +- src/include/catalog/pg_opfamily.h | 3 +- src/include/catalog/pg_proc.h | 18 +- src/include/catalog/pg_type.h | 6 +- src/include/utils/builtins.h | 9 +- src/test/regress/expected/rowtypes.out | 20 + src/test/regress/expected/with.out | 48 +++ src/test/regress/sql/rowtypes.sql | 7 + 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 31ef6dc864..e289326f2f 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -1,4 +1,4 @@ - + Functions and Operators @@ -10667,6 +10667,20 @@ 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. + + + diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml index c2e3807920..0232d80db7 100644 --- a/doc/src/sgml/queries.sgml +++ b/doc/src/sgml/queries.sgml @@ -1,4 +1,4 @@ - + Queries @@ -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 UNION instead of 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 graph using a + link field: + + +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; + + + This query will loop if the link relationships contain + cycles. Because we require a depth output, just changing + UNION ALL to 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 + path and cycle to the loop-prone query: + + +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; + + + Aside from preventing cycles, the array value is often useful in its own + right as representing the path taken to reach any particular row. + + + + 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 f1 and f2: + + +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; + + + + + + Omit the 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. + + + + + A helpful trick for testing queries when you are not certain if they might loop is to place a LIMIT in the parent query. For example, this query would loop forever without the LIMIT: diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index cbf440fc43..c4e548becb 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 69efaddfec..7c66c63804 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 b7981660ef..f08244216e 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 3f27787992..0ab03e0578 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 8368d4bbc7..f768da3f61 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 @@ -470,6 +470,16 @@ DATA(insert ( 397 2277 2277 3 1070 403 )); 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 5ca46600e3..c0f91a64b6 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 7c4d95003c..6251bc9829 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 e19bb0f927..d112511932 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 e6066f0656..aa6e7809e7 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 31449b60e0..70dc0a319a 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 d4f4fc0968..f3367b41b5 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 a98f93165f..b9efbe88b3 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 73ae3ce10e..e1181a61b8 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 fe7561065e..e8d3e43b7b 100644 --- a/src/test/regress/expected/with.out +++ b/src/test/regress/expected/with.out @@ -451,6 +451,54 @@ SELECT t1.id, count(t2.*) FROM t AS t1 JOIN t AS t2 ON 3 | 7 (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 -- diff --git a/src/test/regress/sql/rowtypes.sql b/src/test/regress/sql/rowtypes.sql index 7e5c554d3b..a8520c5bd2 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 54d311101d..d37f0d9723 100644 --- a/src/test/regress/sql/with.sql +++ b/src/test/regress/sql/with.sql @@ -250,6 +250,28 @@ SELECT t1.id, count(t2.*) FROM t AS t1 JOIN t AS t2 ON GROUP BY t1.id 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 --