/*--------------------------------------------------------------------- * * rdfnode.c * rdfnode comparison operators, type ordering, and literal parsing. * * Copyright (C) 2022-2026 Jim Jones * *--------------------------------------------------------------------- */ #include "postgres.h" #include "rdf_fdw.h" #include "rdf_utils.h" #include "rdfnode.h" #include "sparql.h" #include "utils/builtins.h" #if PG_VERSION_NUM >= 190000 #include "varatt.h" #endif #include "utils/date.h" #include "utils/numeric.h" #include "utils/timestamp.h" #include "utils/datetime.h" #if PG_VERSION_NUM >= 100000 #include "utils/varlena.h" #endif #include "catalog/pg_collation.h" #include "nodes/makefuncs.h" #include /* * rdfnode_numeric_is_nan * ---------------------- * * Reports whether a numeric term is NaN. Every comparison has to ask, because * XPath gives NaN no place in the ordering: op:numeric-equal and the rest are * false whenever either side is NaN, including NaN against itself, so a * comparison answers before reaching the arithmetic. * * node: the parsed term to examine * * returns true if the term is numeric and its lexical form is NaN */ static bool rdfnode_numeric_is_nan(const rdfnode_info *node) { return node->isNumeric && pg_strcasecmp(node->lex, "NaN") == 0; } /* * rdfnode_numeric_arith * --------------------- * * Applies a SPARQL arithmetic operator to two numeric terms. * * SPARQL 1.1 §17.3 maps +, -, * and / over two numerics onto op:numeric-add * and its siblings, which compute in the wider of the two datatypes -- the * same XPath promotion the comparison operators use, so that the answer is * decided by the pair rather than by whichever term is written first. * * Division is the exception the table calls out: two xsd:integers give an * xsd:decimal, because the quotient of two integers need not be one. * * left, right: the terms to combine, both numeric * op : one of '+', '-', '*', '/' * * returns a palloc'd typed literal carrying the result and its datatype */ static char * rdfnode_numeric_arith(const rdfnode_info *left, const rdfnode_info *right, char op) { XsdNumericType leftType = get_xsd_numeric_type(left->dtype); XsdNumericType rightType = get_xsd_numeric_type(right->dtype); XsdNumericType commonType = leftType > rightType ? leftType : rightType; char *value; if (commonType == XSD_TYPE_DOUBLE || commonType == XSD_TYPE_FLOAT) { float8 l = DatumGetFloat8(DirectFunctionCall1(float8in, CStringGetDatum(left->lex))); float8 r = DatumGetFloat8(DirectFunctionCall1(float8in, CStringGetDatum(right->lex))); float8 result; switch (op) { case '+': result = l + r; break; case '-': result = l - r; break; case '*': result = l * r; break; default: /* * Division by zero is not an error here. XPath 4.3.6 * op:numeric-divide raises FOAR0001 only when both operands * are xs:decimal or xs:integer; for xs:float and xs:double it * asks for IEEE 754 division, which answers INF, -INF or NaN * according to the signs of the two zeros. Fuseki and GraphDB * both answer INF for 1.0e0 / 0.0e0. The numeric branch below * keeps raising, which is the decimal and integer case. */ result = l / r; break; } /* * Back through the type's own output, so an xsd:float keeps the digits * its value space holds rather than the ones a double would print. */ if (commonType == XSD_TYPE_FLOAT) value = DatumGetCString(DirectFunctionCall1(float4out, Float4GetDatum((float4) result))); else value = DatumGetCString(DirectFunctionCall1(float8out, Float8GetDatum(result))); /* * PostgreSQL's float output spells the infinities "Infinity" and * "-Infinity", which are not in the xsd:double / xsd:float lexical * space: XSD 1.1 Part 2 3.3.5 fixes them as "INF" and "-INF". The cast * from a PostgreSQL float (float8_to_rdfnode) already normalises to * these, and arithmetic must agree. "NaN" already coincides. */ if (strcmp(value, "Infinity") == 0) value = "INF"; else if (strcmp(value, "-Infinity") == 0) value = "-INF"; } else { Datum l = DirectFunctionCall3(numeric_in, CStringGetDatum(left->lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum r = DirectFunctionCall3(numeric_in, CStringGetDatum(right->lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum result; switch (op) { case '+': result = DirectFunctionCall2(numeric_add, l, r); break; case '-': result = DirectFunctionCall2(numeric_sub, l, r); break; case '*': result = DirectFunctionCall2(numeric_mul, l, r); break; default: result = DirectFunctionCall2(numeric_div, l, r); /* two integers divide into a decimal, per the operator table */ commonType = XSD_TYPE_DECIMAL; break; } value = DatumGetCString(DirectFunctionCall1(numeric_out, result)); /* * numeric_div picks its own scale, so 1 / 2 comes back as * 0.50000000000000000000. That is the same number as 0.5 but not the * same term, and the operator class compares terms. XSD's canonical * decimal keeps no trailing zeros, and it is what Fuseki and GraphDB * answer with, so the fraction is trimmed back to it. */ if (strchr(value, '.') != NULL) { char *end = value + strlen(value) - 1; while (end > value && *end == '0') *end-- = '\0'; if (end > value && *end == '.') *end = '\0'; } } return strdt(value, (char *) get_xsd_datatype_uri(commonType)); } /* * rdfnode_arith * ------------- * * Shared body of the rdfnode arithmetic operators. * * Arithmetic is defined over numerics only. A term that is not a numeric * literal -- an IRI, a blank node, a string, an ill-typed literal -- has no * number to combine, which SPARQL reports as a type error and which is raised * here, as the string functions do for the same reason. * * n1, n2: the terms to combine * op : one of '+', '-', '*', '/' * * returns the resulting term */ char *rdfnode_arith(rdfnode *n1, rdfnode *n2, char op) { rdfnode_info a = parse_rdfnode(n1); rdfnode_info b = parse_rdfnode(n2); if (!a.isNumeric) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("arithmetic is not defined for the term: %s", a.raw), errdetail("SPARQL defines %c over numeric literals.", op))); if (!b.isNumeric) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("arithmetic is not defined for the term: %s", b.raw), errdetail("SPARQL defines %c over numeric literals.", op))); return rdfnode_numeric_arith(&a, &b, op); } /* * rdfnode_numeric_cmp_promoted * ---------------------------- * * Compares two numeric terms of any datatypes, in the wider of the two. XPath * promotes xs:integer and xs:decimal to xs:float, and either of those to * xs:double, so the arithmetic is decided by the pair rather than by whichever * term is written first -- comparing in the narrower type, or in the type of * the left operand alone, gives an answer that depends on how the comparison * was written. * * A value the promoted type cannot represent compares as the value it becomes: * against an xsd:float, 16777217 and 16777216 are one number. * * left, right : the terms to compare, both numeric * * returns -1, 0 or 1 as left orders before, with, or after right */ static int rdfnode_numeric_cmp_promoted(const rdfnode_info *left, const rdfnode_info *right) { XsdNumericType leftType = get_xsd_numeric_type(left->dtype); XsdNumericType rightType = get_xsd_numeric_type(right->dtype); XsdNumericType commonType = leftType > rightType ? leftType : rightType; if (commonType == XSD_TYPE_DOUBLE) { float8 leftVal = DatumGetFloat8(DirectFunctionCall1(float8in, CStringGetDatum(left->lex))); float8 rightVal = DatumGetFloat8(DirectFunctionCall1(float8in, CStringGetDatum(right->lex))); return (leftVal < rightVal) ? -1 : (leftVal > rightVal) ? 1 : 0; } if (commonType == XSD_TYPE_FLOAT) { float4 leftVal = DatumGetFloat4(DirectFunctionCall1(float4in, CStringGetDatum(left->lex))); float4 rightVal = DatumGetFloat4(DirectFunctionCall1(float4in, CStringGetDatum(right->lex))); return (leftVal < rightVal) ? -1 : (leftVal > rightVal) ? 1 : 0; } return DatumGetInt32(DirectFunctionCall2( numeric_cmp, DirectFunctionCall3(numeric_in, CStringGetDatum(left->lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)), DirectFunctionCall3(numeric_in, CStringGetDatum(right->lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)))); } /* * time_has_tz * ----------- * Returns true if the XSD time lexical form contains a timezone designator * (Z, +HH:MM, or -HH:MM). Since there is no date portion, the entire string * is scanned, but the leading digits and ':' characters of the time value * mean any 'Z', '+', or '-' must be a timezone designator. * * Note: '-' cannot appear in the time portion itself (HH:MM:SS.sss), * so any '-' in the string is unambiguously a timezone offset. */ static bool time_has_tz(const char *lex) { return strchr(lex, 'Z') != NULL || strchr(lex, '+') != NULL || strchr(lex, '-') != NULL; } /* * datetime_has_tz * --------------- * Returns true if the XSD dateTime lexical form contains a timezone designator * (Z, +HH:MM, or -HH:MM). The search is restricted to the portion of the * string after the 'T' (or space) date/time separator so that the '-' * characters in the date portion (e.g. 2025-04-25) are not mistaken for * a timezone offset. */ static bool datetime_has_tz(const char *lex) { const char *sep = strchr(lex, 'T'); if (sep == NULL) sep = strchr(lex, ' '); /* accept space-separated variant */ if (sep == NULL) return false; /* After the separator the time part uses only digits and ':', so any * 'Z', '+', or '-' that follows must be a timezone designator. */ return strchr(sep, 'Z') != NULL || strchr(sep, '+') != NULL || strchr(sep, '-') != NULL; } /* * rdfnode_eq * ---------- * Returns true if two rdfnode values are SPARQL-equal. * * Implements RDF 1.1 / SPARQL 1.1 equality semantics: * - Term equality fast path: byte-identical normalized forms are equal, * even if the lexical is ill-typed (SPARQL §17.4.1.7). * - IRIs and blank nodes compare by raw string. * - Plain literals and xsd:string compare by codepoint. * - Numeric, date, time, dateTime, and duration delegate to PostgreSQL's * value-space comparators. * - Falls back to lexical comparison for unrecognised datatypes. * * n1, n2: the rdfnode operands * * returns true if n1 = n2 under SPARQL 1.1 semantics */ bool rdfnode_eq(rdfnode *n1, rdfnode *n2) { rdfnode_info a, b; elog(DEBUG3, "%s called", __func__); /* * === RDF 1.1 term-equality fast path === * * Two RDF terms are equal if their normalized lexical form, datatype * IRI, and language tag are identical. rdfnode_in() stores literals * in canonical form (datatype IRIs always expanded to ), * so byte-identical varlena payloads imply identical RDF terms. * * This MUST short-circuit before any value-space comparison: SPARQL * §17.4.1.7 (RDFterm-equal) requires identical ill-typed literals to * compare equal rather than raise a type error. Without this, * '"invalid"^^xsd:dateTime' = '"invalid"^^xsd:dateTime' * would call timestamptz_in("invalid") and ERROR instead of returning * TRUE. * * It's also a performance win for the common case of comparing a * literal to itself or to its own canonicalized form. * * Note: unlike the ordering operators, this function does not use * LiteralsComparable(). That function raises ERROR on type mismatches, * but SPARQL §17.4.1.7 requires equality to return false instead. */ if (VARSIZE_ANY_EXHDR(n1) == VARSIZE_ANY_EXHDR(n2) && memcmp(VARDATA_ANY(n1), VARDATA_ANY(n2), VARSIZE_ANY_EXHDR(n1)) == 0) { /* * One term is not equal to itself: a numeric NaN. op:numeric-equal * is false whenever either side is NaN, itself included, so the * shortcut has to stand aside for it. * * The lexical form is matched exactly, unlike rdfnode_numeric_is_nan() * below. Only "NaN" is in the lexical space of xsd:float and * xsd:double; a literal spelled any other way is ill-typed, has no * value, and is therefore governed by RDFterm-equal rather than by * op:numeric-equal -- which is what this shortcut implements, and * which makes it equal to itself. */ a = parse_rdfnode(n1); if (a.isNumeric && strcmp(a.lex, "NaN") == 0) return false; return true; } a = parse_rdfnode(n1); b = parse_rdfnode(n2); elog(DEBUG4, "%s: a.lex='%s', a.dtype='%s', a.lang='%s', a.isNumeric='%d'", __func__, a.lex, a.dtype ? a.dtype : "(null)", a.lang ? a.lang : "(null)", a.isNumeric); elog(DEBUG4, "%s: b.lex='%s', b.dtype='%s', b.lang='%s', b.isNumeric='%d'", __func__, b.lex, b.dtype ? b.dtype : "(null)", b.lang ? b.lang : "(null)", b.isNumeric); if (a.isIRI || b.isIRI) return a.isIRI && b.isIRI && strcmp(a.raw, b.raw) == 0; if (a.isBlank || b.isBlank) return a.isBlank && b.isBlank && strcmp(a.raw, b.raw) == 0; /* * Plain literals (no language or datatype) and xsd:string literals are * value-equal per RDF 1.1, so compare their lexical forms directly. */ if ((a.isPlainLiteral || a.isString) && (b.isPlainLiteral || b.isString)) return strcmp(a.lex, b.lex) == 0; /* * A plain literal can only compare equal to another plain literal or * an xsd:string. Anything else is inequal. */ if ((a.isPlainLiteral && !b.isPlainLiteral && !b.isString) || (b.isPlainLiteral && !a.isPlainLiteral && !a.isString)) return false; /* If one has a language tag, both must. */ if ((strlen(a.lang) != 0) != (strlen(b.lang) != 0)) return false; /* Language tags must match (case-insensitive per BCP 47). */ if (strlen(a.lang) != 0 && pg_strcasecmp(a.lang, b.lang) != 0) return false; /* Numeric and non-numeric literals cannot be compared. */ if (a.isNumeric != b.isNumeric) return false; /* * For non-numeric datatyped literals, datatypes must match. (Numeric * subtypes such as xsd:int / xsd:short / xsd:integer are interchangeable.) */ if (!a.isNumeric && !b.isNumeric && strlen(a.dtype) != 0 && strlen(b.dtype) != 0 && strcmp(a.dtype, b.dtype) != 0) return false; /* === Value-space comparisons === */ if (a.isNumeric && b.isNumeric) { /* * SPARQL 1.1 (via IEEE 754) requires false for comparisons involving NaN, * as stated at 4.3.1 "If $arg1 or $arg2 is NaN, the function returns false." * * 4.3.1 op:numeric-equal * https://www.w3.org/TR/xpath-functions/#func-numeric-equal */ if (rdfnode_numeric_is_nan(&a) || rdfnode_numeric_is_nan(&b)) return false; return rdfnode_numeric_cmp_promoted(&a, &b) == 0; } if (a.isDate && b.isDate) { Datum a_val = DirectFunctionCall3(date_in, CStringGetDatum(a.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(date_in, CStringGetDatum(b.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(date_eq, a_val, b_val)); } if (a.isDateTime && b.isDateTime) { bool a_has_tz = datetime_has_tz(a.lex); bool b_has_tz = datetime_has_tz(b.lex); /* * Per XSD §3.2.7.4 / SPARQL 1.1 §17.3: a timezone-aware dateTime and * a timezone-naive one are not equal (incomparable value spaces). */ if (a_has_tz != b_has_tz) return false; if (a_has_tz) { /* Both timezone-aware: normalise to UTC and compare. */ Datum a_val = DirectFunctionCall3(timestamptz_in, CStringGetDatum(a.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(timestamptz_in, CStringGetDatum(b.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return timestamptz_cmp_internal(DatumGetTimestampTz(a_val), DatumGetTimestampTz(b_val)) == 0; } else { /* Both timezone-naive: compare without any TZ conversion. */ Datum a_val = DirectFunctionCall3(timestamp_in, CStringGetDatum(a.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(timestamp_in, CStringGetDatum(b.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(timestamp_eq, a_val, b_val)); } } /* xsd:time literals */ if (a.isTime && b.isTime) { bool a_has_tz = time_has_tz(a.lex); bool b_has_tz = time_has_tz(b.lex); /* * Per XSD §3.2.8 / SPARQL 1.1 §17.3: a timezone-aware time and * a timezone-naive one are not equal (incomparable value spaces). */ if (a_has_tz != b_has_tz) return false; if (a_has_tz) { Datum a_val = DirectFunctionCall3(timetz_in, CStringGetDatum(a.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(timetz_in, CStringGetDatum(b.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(timetz_eq, a_val, b_val)); } else { Datum a_val = DirectFunctionCall3(time_in, CStringGetDatum(a.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(time_in, CStringGetDatum(b.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(time_eq, a_val, b_val)); } } if (a.isBoolean && b.isBoolean) { Datum a_val = DirectFunctionCall1(boolin, CStringGetDatum(a.lex)); Datum b_val = DirectFunctionCall1(boolin, CStringGetDatum(b.lex)); return DatumGetBool(DirectFunctionCall2(booleq, a_val, b_val)); } if (a.isDuration && b.isDuration) { Datum a_val, b_val; bool a_neg = (a.lex[0] == '-'); bool b_neg = (b.lex[0] == '-'); a_val = DirectFunctionCall3(interval_in, CStringGetDatum(a_neg ? a.lex + 1 : a.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); b_val = DirectFunctionCall3(interval_in, CStringGetDatum(b_neg ? b.lex + 1 : b.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); if (a_neg) a_val = DirectFunctionCall1(interval_um, a_val); if (b_neg) b_val = DirectFunctionCall1(interval_um, b_val); return DatumGetBool(DirectFunctionCall2(interval_eq, a_val, b_val)); } elog(DEBUG4, "%s: fallback lexical comparison", __func__); return strcmp(a.lex, b.lex) == 0; } /* * rdfnode_ge * ---------- * Returns true if n1 >= n2 under SPARQL 1.1 comparison semantics. * Per IEEE 754 / SPARQL 1.1 §17.3, any comparison involving NaN returns false. * dateTime literals that lack a timezone offset are treated as incomparable. * * n1, n2: the rdfnode operands * * returns true if n1 >= n2 */ bool rdfnode_ge(rdfnode *n1, rdfnode *n2) { Datum arg1, arg2; rdfnode_info rdfnode1 = parse_rdfnode(n1); rdfnode_info rdfnode2 = parse_rdfnode(n2); if (!LiteralsComparable(n1, n2)) return false; /* unreachable due to error in LiteralsComparable, but kept for safety */ /* string and plain literals */ if ((rdfnode1.isString || rdfnode1.isPlainLiteral) && (rdfnode2.isString || rdfnode2.isPlainLiteral)) return strcmp(rdfnode1.lex, rdfnode2.lex) >= 0; /* unicode codepoint order */ /* numeric literals */ if (rdfnode1.isNumeric && rdfnode2.isNumeric) { /* * SPARQL 1.1 (via IEEE 754) requires false for comparisons involving NaN, * as stated at 4.3.3 "The function call op:numeric-greater-than($A, $B) * is defined to return the same result as op:numeric-less-than($B, $A)", * which says "If $arg1 or $arg2 is NaN, the function returns false." -- * equally stated at 4.3.1. * * 4.3.3 op:numeric-greater-than * https://www.w3.org/TR/xpath-functions/#func-numeric-greater-than * 4.3.2 op:numeric-less-than * https://www.w3.org/TR/xpath-functions/#func-numeric-less-than * 4.3.1 op:numeric-equal * https://www.w3.org/TR/xpath-functions/#func-numeric-equal */ if (rdfnode_numeric_is_nan(&rdfnode1) || rdfnode_numeric_is_nan(&rdfnode2)) return false; return rdfnode_numeric_cmp_promoted(&rdfnode1, &rdfnode2) >= 0; } /* xsd:date literals */ if (rdfnode1.isDate && rdfnode2.isDate) { arg1 = DirectFunctionCall3(date_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(date_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(date_ge, arg1, arg2)); } if (rdfnode1.isDateTime && rdfnode2.isDateTime) { bool has_tz1 = datetime_has_tz(rdfnode1.lex); bool has_tz2 = datetime_has_tz(rdfnode2.lex); /* Mixed timezone: per SPARQL 1.1 §17.3, incomparable. */ if (has_tz1 != has_tz2) return false; if (has_tz1) { TimestampTz ts1 = DatumGetTimestampTz(DirectFunctionCall3(timestamptz_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1))); TimestampTz ts2 = DatumGetTimestampTz(DirectFunctionCall3(timestamptz_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1))); return timestamptz_cmp_internal(ts1, ts2) >= 0; } else { arg1 = DirectFunctionCall3(timestamp_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(timestamp_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(timestamp_ge, arg1, arg2)); } } /* xsd:time literals */ if (rdfnode1.isTime && rdfnode2.isTime) { bool a_has_tz = time_has_tz(rdfnode1.lex); bool b_has_tz = time_has_tz(rdfnode2.lex); /* * Per XSD §3.2.8 / SPARQL 1.1 §17.3: a timezone-aware time and * a timezone-naive one are not equal (incomparable value spaces). */ if (a_has_tz != b_has_tz) return false; if (a_has_tz) { Datum a_val = DirectFunctionCall3(timetz_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(timetz_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(timetz_ge, a_val, b_val)); } else { Datum a_val = DirectFunctionCall3(time_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(time_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(time_ge, a_val, b_val)); } } /* xsd:boolean literals */ if (rdfnode1.isBoolean && rdfnode2.isBoolean) { arg1 = DirectFunctionCall1(boolin, CStringGetDatum(rdfnode1.lex)); arg2 = DirectFunctionCall1(boolin, CStringGetDatum(rdfnode2.lex)); return DatumGetBool(DirectFunctionCall2(boolge, arg1, arg2)); } /* xsd:duration literals */ if (rdfnode1.isDuration && rdfnode2.isDuration) { bool neg1 = (rdfnode1.lex[0] == '-'); bool neg2 = (rdfnode2.lex[0] == '-'); arg1 = DirectFunctionCall3(interval_in, CStringGetDatum(neg1 ? rdfnode1.lex + 1 : rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(interval_in, CStringGetDatum(neg2 ? rdfnode2.lex + 1 : rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); if (neg1) arg1 = DirectFunctionCall1(interval_um, arg1); if (neg2) arg2 = DirectFunctionCall1(interval_um, arg2); return DatumGetBool(DirectFunctionCall2(interval_ge, arg1, arg2)); } return false; } /* * rdfnode_le * ---------- * Returns true if n1 <= n2 under SPARQL 1.1 comparison semantics. * Per IEEE 754 / SPARQL 1.1 §17.3, any comparison involving NaN returns false. * dateTime literals that lack a timezone offset are treated as incomparable. * * n1, n2: the rdfnode operands * * returns true if n1 <= n2 */ bool rdfnode_le(rdfnode *n1, rdfnode *n2) { Datum arg1, arg2; rdfnode_info rdfnode1 = parse_rdfnode(n1); rdfnode_info rdfnode2 = parse_rdfnode(n2); if (!LiteralsComparable(n1, n2)) return false; /* unreachable due to error in LiteralsComparable, but kept for safety */ /* string and plain literals */ if ((rdfnode1.isString || rdfnode1.isPlainLiteral) && (rdfnode2.isString || rdfnode2.isPlainLiteral)) { return strcmp(rdfnode1.lex, rdfnode2.lex) <= 0; /* unicode codepoint order */ } /* numeric literals */ if (rdfnode1.isNumeric && rdfnode2.isNumeric) { /* * SPARQL 1.1 (via IEEE 754) requires false for comparisons involving NaN, * as stated at 4.3.1 "If $arg1 or $arg2 is NaN, the function returns false." * * 4.3.2 op:numeric-less-than * https://www.w3.org/TR/xpath-functions/#func-numeric-less-than * 4.3.1 op:numeric-equal * https://www.w3.org/TR/xpath-functions/#func-numeric-equal */ if (rdfnode_numeric_is_nan(&rdfnode1) || rdfnode_numeric_is_nan(&rdfnode2)) return false; return rdfnode_numeric_cmp_promoted(&rdfnode1, &rdfnode2) <= 0; } /* xsd:date literals */ if (rdfnode1.isDate && rdfnode2.isDate) { arg1 = DirectFunctionCall3(date_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(date_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(date_le, arg1, arg2)); } if (rdfnode1.isDateTime && rdfnode2.isDateTime) { bool has_tz1 = datetime_has_tz(rdfnode1.lex); bool has_tz2 = datetime_has_tz(rdfnode2.lex); /* Mixed timezone: per SPARQL 1.1 §17.3, incomparable. */ if (has_tz1 != has_tz2) return false; if (has_tz1) { TimestampTz ts1 = DatumGetTimestampTz(DirectFunctionCall3(timestamptz_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1))); TimestampTz ts2 = DatumGetTimestampTz(DirectFunctionCall3(timestamptz_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1))); return timestamptz_cmp_internal(ts1, ts2) <= 0; } else { arg1 = DirectFunctionCall3(timestamp_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(timestamp_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(timestamp_le, arg1, arg2)); } } /* xsd:time literals */ if (rdfnode1.isTime && rdfnode2.isTime) { bool a_has_tz = time_has_tz(rdfnode1.lex); bool b_has_tz = time_has_tz(rdfnode2.lex); /* * Per XSD §3.2.8 / SPARQL 1.1 §17.3: a timezone-aware time and * a timezone-naive one are not equal (incomparable value spaces). */ if (a_has_tz != b_has_tz) return false; if (a_has_tz) { Datum a_val = DirectFunctionCall3(timetz_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(timetz_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(timetz_le, a_val, b_val)); } else { Datum a_val = DirectFunctionCall3(time_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(time_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(time_le, a_val, b_val)); } } /* xsd:boolean literals */ if (rdfnode1.isBoolean && rdfnode2.isBoolean) { arg1 = DirectFunctionCall1(boolin, CStringGetDatum(rdfnode1.lex)); arg2 = DirectFunctionCall1(boolin, CStringGetDatum(rdfnode2.lex)); return DatumGetBool(DirectFunctionCall2(boolle, arg1, arg2)); } /* xsd:duration literals */ if (rdfnode1.isDuration && rdfnode2.isDuration) { bool neg1 = (rdfnode1.lex[0] == '-'); bool neg2 = (rdfnode2.lex[0] == '-'); arg1 = DirectFunctionCall3(interval_in, CStringGetDatum(neg1 ? rdfnode1.lex + 1 : rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(interval_in, CStringGetDatum(neg2 ? rdfnode2.lex + 1 : rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); if (neg1) arg1 = DirectFunctionCall1(interval_um, arg1); if (neg2) arg2 = DirectFunctionCall1(interval_um, arg2); return DatumGetBool(DirectFunctionCall2(interval_le, arg1, arg2)); } return false; } /* * rdfnode_gt * ---------- * Returns true if n1 > n2 under SPARQL 1.1 comparison semantics. * Per IEEE 754 / SPARQL 1.1 §17.3, any comparison involving NaN returns false. * dateTime literals that lack a timezone offset are treated as incomparable. * * n1, n2: the rdfnode operands * * returns true if n1 > n2 */ bool rdfnode_gt(rdfnode *n1, rdfnode *n2) { Datum arg1, arg2; rdfnode_info rdfnode1 = parse_rdfnode(n1); rdfnode_info rdfnode2 = parse_rdfnode(n2); if (!LiteralsComparable(n1, n2)) return false; /* unreachable due to error in LiteralsComparable, but kept for safety */ /* string and plain literals */ if ((rdfnode1.isString || rdfnode1.isPlainLiteral) && (rdfnode2.isString || rdfnode2.isPlainLiteral)) { return strcmp(rdfnode1.lex, rdfnode2.lex) > 0; /* unicode codepoint order */ } /* numeric literals */ if (rdfnode1.isNumeric && rdfnode2.isNumeric) { /* * SPARQL 1.1 (via IEEE 754) requires false for comparisons involving NaN, * as stated at 4.3.3 "The function call op:numeric-greater-than($A, $B) * is defined to return the same result as op:numeric-less-than($B, $A)", * which says "If $arg1 or $arg2 is NaN, the function returns false." * * 4.3.3 op:numeric-greater-than * https://www.w3.org/TR/xpath-functions/#func-numeric-greater-than * 4.3.2 op:numeric-less-than * https://www.w3.org/TR/xpath-functions/#func-numeric-less-than */ if (rdfnode_numeric_is_nan(&rdfnode1) || rdfnode_numeric_is_nan(&rdfnode2)) return false; return rdfnode_numeric_cmp_promoted(&rdfnode1, &rdfnode2) > 0; } /* xsd:date literals */ if (rdfnode1.isDate && rdfnode2.isDate) { arg1 = DirectFunctionCall3(date_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(date_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(date_gt, arg1, arg2)); } /* xsd:dateTime literals */ if (rdfnode1.isDateTime && rdfnode2.isDateTime) { bool has_tz1 = datetime_has_tz(rdfnode1.lex); bool has_tz2 = datetime_has_tz(rdfnode2.lex); /* Mixed timezone: per SPARQL 1.1 §17.3, incomparable. */ if (has_tz1 != has_tz2) return false; if (has_tz1) { TimestampTz ts1 = DatumGetTimestampTz(DirectFunctionCall3(timestamptz_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1))); TimestampTz ts2 = DatumGetTimestampTz(DirectFunctionCall3(timestamptz_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1))); return timestamptz_cmp_internal(ts1, ts2) > 0; } else { arg1 = DirectFunctionCall3(timestamp_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(timestamp_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(timestamp_gt, arg1, arg2)); } } /* xsd:time literals */ if (rdfnode1.isTime && rdfnode2.isTime) { bool a_has_tz = time_has_tz(rdfnode1.lex); bool b_has_tz = time_has_tz(rdfnode2.lex); /* * Per XSD §3.2.8 / SPARQL 1.1 §17.3: a timezone-aware time and * a timezone-naive one are not equal (incomparable value spaces). */ if (a_has_tz != b_has_tz) return false; if (a_has_tz) { Datum a_val = DirectFunctionCall3(timetz_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(timetz_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(timetz_gt, a_val, b_val)); } else { Datum a_val = DirectFunctionCall3(time_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(time_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(time_gt, a_val, b_val)); } } /* xsd:boolean literals */ if (rdfnode1.isBoolean && rdfnode2.isBoolean) { arg1 = DirectFunctionCall1(boolin, CStringGetDatum(rdfnode1.lex)); arg2 = DirectFunctionCall1(boolin, CStringGetDatum(rdfnode2.lex)); return DatumGetBool(DirectFunctionCall2(boolgt, arg1, arg2)); } /* xsd:duration literals */ if (rdfnode1.isDuration && rdfnode2.isDuration) { bool neg1 = (rdfnode1.lex[0] == '-'); bool neg2 = (rdfnode2.lex[0] == '-'); arg1 = DirectFunctionCall3(interval_in, CStringGetDatum(neg1 ? rdfnode1.lex + 1 : rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(interval_in, CStringGetDatum(neg2 ? rdfnode2.lex + 1 : rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); if (neg1) arg1 = DirectFunctionCall1(interval_um, arg1); if (neg2) arg2 = DirectFunctionCall1(interval_um, arg2); return DatumGetBool(DirectFunctionCall2(interval_gt, arg1, arg2)); } return false; } /* * rdfnode_lt * ---------- * Returns true if n1 < n2 under SPARQL 1.1 comparison semantics. * Per IEEE 754 / SPARQL 1.1 §17.3, any comparison involving NaN returns false. * dateTime literals that lack a timezone offset are treated as incomparable. * * n1, n2: the rdfnode operands * * returns true if n1 < n2 */ bool rdfnode_lt(rdfnode *n1, rdfnode *n2) { Datum arg1, arg2; rdfnode_info rdfnode1 = parse_rdfnode(n1); rdfnode_info rdfnode2 = parse_rdfnode(n2); if (!LiteralsComparable(n1, n2)) return false; /* unreachable due to error in LiteralsComparable, but kept for safety */ /* string and plain literals */ if ((rdfnode1.isString || rdfnode1.isPlainLiteral) && (rdfnode2.isString || rdfnode2.isPlainLiteral)) { return strcmp(rdfnode1.lex, rdfnode2.lex) < 0; /* unicode codepoint order */ } /* numeric literals */ if (rdfnode1.isNumeric && rdfnode2.isNumeric) { /* * SPARQL 1.1 (via IEEE 754) requires false for comparisons involving NaN, * as stated at 4.3.2 "If $arg1 or $arg2 is NaN, the function returns false." * * 4.3.2 op:numeric-less-than * https://www.w3.org/TR/xpath-functions/#func-numeric-less-than */ if (rdfnode_numeric_is_nan(&rdfnode1) || rdfnode_numeric_is_nan(&rdfnode2)) return false; return rdfnode_numeric_cmp_promoted(&rdfnode1, &rdfnode2) < 0; } /* xsd:date literals */ if (rdfnode1.isDate && rdfnode2.isDate) { arg1 = DirectFunctionCall3(date_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(date_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(date_lt, arg1, arg2)); } /* xsd:dateTime literals */ if (rdfnode1.isDateTime && rdfnode2.isDateTime) { bool has_tz1 = datetime_has_tz(rdfnode1.lex); bool has_tz2 = datetime_has_tz(rdfnode2.lex); /* Mixed timezone: per SPARQL 1.1 §17.3, incomparable. */ if (has_tz1 != has_tz2) return false; if (has_tz1) { TimestampTz ts1 = DatumGetTimestampTz(DirectFunctionCall3(timestamptz_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1))); TimestampTz ts2 = DatumGetTimestampTz(DirectFunctionCall3(timestamptz_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1))); return timestamptz_cmp_internal(ts1, ts2) < 0; } else { arg1 = DirectFunctionCall3(timestamp_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(timestamp_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(timestamp_lt, arg1, arg2)); } } /* xsd:time literals */ if (rdfnode1.isTime && rdfnode2.isTime) { bool a_has_tz = time_has_tz(rdfnode1.lex); bool b_has_tz = time_has_tz(rdfnode2.lex); /* * Per XSD §3.2.8 / SPARQL 1.1 §17.3: a timezone-aware time and * a timezone-naive one are not equal (incomparable value spaces). */ if (a_has_tz != b_has_tz) return false; if (a_has_tz) { Datum a_val = DirectFunctionCall3(timetz_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(timetz_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(timetz_lt, a_val, b_val)); } else { Datum a_val = DirectFunctionCall3(time_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum b_val = DirectFunctionCall3(time_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetBool(DirectFunctionCall2(time_lt, a_val, b_val)); } } /* xsd:boolean literals */ if (rdfnode1.isBoolean && rdfnode2.isBoolean) { arg1 = DirectFunctionCall1(boolin, CStringGetDatum(rdfnode1.lex)); arg2 = DirectFunctionCall1(boolin, CStringGetDatum(rdfnode2.lex)); return DatumGetBool(DirectFunctionCall2(boollt, arg1, arg2)); } /* xsd:duration literals */ if (rdfnode1.isDuration && rdfnode2.isDuration) { bool neg1 = (rdfnode1.lex[0] == '-'); bool neg2 = (rdfnode2.lex[0] == '-'); arg1 = DirectFunctionCall3(interval_in, CStringGetDatum(neg1 ? rdfnode1.lex + 1 : rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(interval_in, CStringGetDatum(neg2 ? rdfnode2.lex + 1 : rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); if (neg1) arg1 = DirectFunctionCall1(interval_um, arg1); if (neg2) arg2 = DirectFunctionCall1(interval_um, arg2); return DatumGetBool(DirectFunctionCall2(interval_lt, arg1, arg2)); } return false; } /* * getSparqlTermOrder * * Helper function for SPARQL 1.1 term ordering (Section 15.1). * * Returns an integer representing the RDF term's position in SPARQL * ordering. Lower values come first in MIN and ORDER BY ASC, later in * MAX and ORDER BY DESC. * * SPARQL 1.1 Query Language Recommendation (21 March 2013) * https://www.w3.org/TR/sparql11-query/#modOrderBy * * Section 15.1 ORDER BY states: * "SPARQL also fixes an order between some kinds of RDF terms that * would not otherwise be ordered: * 1. (Lowest) no value assigned to the variable or expression in * this solution. * 2. Blank nodes * 3. IRIs * 4. RDF literals" * * For literals, the spec further states: * "A plain literal is lower than an RDF literal with type xsd:string * of the same lexical form." * * The relative ordering of different typed literals is * implementation-dependent but must be consistent. This implementation * orders typed literals by their datatype semantics for practical * query results: * - Numerics before temporal types before strings * - Within temporal types: dateTime < date < time < duration * * This ordering ensures that MIN/MAX aggregates produce intuitive * results when mixing datatypes, e.g.: * MIN(42, "hello") returns 42 (numeric < string, so 42 is minimum) * MAX(42, "hello") returns "hello" (string > numeric, so "hello" is * maximum) */ static int getSparqlTermOrder(rdfnode_info *node) { if (node->isBlank) return 0; /* Blank nodes (SPARQL 1.1 Section 15.1 #2) */ if (node->isIRI) return 1; /* IRIs (SPARQL 1.1 Section 15.1 #3) */ if (strlen(node->lang) > 0) return 2; /* Language-tagged literals (part of RDF literals) */ if (node->isPlainLiteral) return 3; /* Simple literals (SPARQL 1.1: lower than xsd:string) */ if (node->isNumeric) return 4; /* Numeric types (implementation choice: before temporal) */ if (node->isDateTime) return 5; /* xsd:dateTime (implementation choice: within temporal) */ if (node->isDate) return 6; /* xsd:date (implementation choice: within temporal) */ if (node->isTime) return 7; /* xsd:time (implementation choice: within temporal) */ if (node->isDuration) return 8; /* xsd:duration (implementation choice: within temporal) */ if (node->isString) return 9; /* xsd:string (SPARQL 1.1: higher than plain literals) */ return 10; /* Other typed literals (implementation choice: last) */ } /* * rdfnode_cmp_for_aggregate * * Comparison function for SPARQL aggregates (MIN/MAX) that implements * SPARQL term ordering, allowing comparison across different * datatypes. * * Returns: -1 if n1 < n2, 0 if n1 == n2, 1 if n1 > n2 * * SPARQL 1.1 term ordering (Section 17.3.1): * 1. IRIs < Blank Nodes < Literals * 2. Within literals: * - Language-tagged literals < Simple literals < Typed literals * - Typed literals ordered by: boolean < numeric < datetime < * date < time < duration < string < other * 3. Within same type: use type-specific comparison * * Note: This comparator defines a consistent cross-type order only. * Aggregate functions MIN/MAX may apply additional mixed-type policies * (e.g., Fuseki-style: MIN prefers non-numeric when mixed; MAX prefers * numeric when mixed). Within a chosen category, this comparator is * still used to compare values. */ int rdfnode_cmp_for_aggregate(rdfnode *n1, rdfnode *n2) { rdfnode_info rdfnode1 = parse_rdfnode(n1); rdfnode_info rdfnode2 = parse_rdfnode(n2); Datum arg1, arg2; int typeOrder1, typeOrder2; elog(DEBUG3, "%s called", __func__); elog(DEBUG4, "%s: n1='%s', n2='%s'", __func__, rdfnode1.raw, rdfnode2.raw); /* Get SPARQL term order for both nodes */ typeOrder1 = getSparqlTermOrder(&rdfnode1); typeOrder2 = getSparqlTermOrder(&rdfnode2); /* Different type categories: compare by SPARQL term order */ if (typeOrder1 != typeOrder2) return (typeOrder1 < typeOrder2) ? -1 : 1; /* Same type category: use type-specific comparison */ /* IRIs: lexical comparison */ if (rdfnode1.isIRI && rdfnode2.isIRI) return strcmp(rdfnode1.raw, rdfnode2.raw); /* Blank nodes: lexical comparison */ if (rdfnode1.isBlank && rdfnode2.isBlank) return strcmp(rdfnode1.raw, rdfnode2.raw); /* Language-tagged literals: compare by lang tag, then by lexical value */ if (strlen(rdfnode1.lang) > 0 && strlen(rdfnode2.lang) > 0) { int langCmp = pg_strcasecmp(rdfnode1.lang, rdfnode2.lang); if (langCmp != 0) return langCmp; return strcmp(rdfnode1.lex, rdfnode2.lex); /* unicode codepoint order */ } /* Simple literals and xsd:string: lexical comparison */ if ((rdfnode1.isPlainLiteral || rdfnode1.isString) && (rdfnode2.isPlainLiteral || rdfnode2.isString)) return strcmp(rdfnode1.lex, rdfnode2.lex); /* unicode codepoint order */ /* Numeric literals: use numeric comparison */ if (rdfnode1.isNumeric && rdfnode2.isNumeric) { return rdfnode_numeric_cmp_promoted(&rdfnode1, &rdfnode2); } /* xsd:date literals */ if (rdfnode1.isDate && rdfnode2.isDate) { DateADT date1, date2; arg1 = DirectFunctionCall3(date_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(date_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); date1 = DatumGetDateADT(arg1); date2 = DatumGetDateADT(arg2); return (date1 < date2) ? -1 : (date1 > date2) ? 1 : 0; } /* xsd:dateTime literals */ if (rdfnode1.isDateTime && rdfnode2.isDateTime) { bool has_tz1 = datetime_has_tz(rdfnode1.lex); bool has_tz2 = datetime_has_tz(rdfnode2.lex); if (has_tz1 && has_tz2) { TimestampTz ts1 = DatumGetTimestampTz(DirectFunctionCall3(timestamptz_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1))); TimestampTz ts2 = DatumGetTimestampTz(DirectFunctionCall3(timestamptz_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1))); return timestamptz_cmp_internal(ts1, ts2); } else if (!has_tz1 && !has_tz2) { Datum d1 = DirectFunctionCall3(timestamp_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); Datum d2 = DirectFunctionCall3(timestamp_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); return DatumGetInt32(DirectFunctionCall2(timestamp_cmp, d1, d2)); } else { /* Mixed: timezone-aware sorts after timezone-naive for a stable order. */ return has_tz1 ? 1 : -1; } } /* xsd:time literals */ if (rdfnode1.isTime && rdfnode2.isTime) { TimeADT time1, time2; arg1 = DirectFunctionCall3(time_in, CStringGetDatum(rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(time_in, CStringGetDatum(rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); time1 = DatumGetTimeADT(arg1); time2 = DatumGetTimeADT(arg2); return (time1 < time2) ? -1 : (time1 > time2) ? 1 : 0; } /* xsd:duration literals */ if (rdfnode1.isDuration && rdfnode2.isDuration) { /* * XSD 1.1 Part 2 3.3.6 admits a leading '-', and PostgreSQL's * interval_in() does not: it is stripped and the value negated * afterwards, exactly as the comparison operators above do. Handing * "-P1D" straight to interval_in() raised "invalid input syntax", * so a MIN or MAX over a group holding one negative duration failed * although every operator on the same pair answers. */ bool neg1 = (rdfnode1.lex[0] == '-'); bool neg2 = (rdfnode2.lex[0] == '-'); arg1 = DirectFunctionCall3(interval_in, CStringGetDatum(neg1 ? rdfnode1.lex + 1 : rdfnode1.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); arg2 = DirectFunctionCall3(interval_in, CStringGetDatum(neg2 ? rdfnode2.lex + 1 : rdfnode2.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); if (neg1) arg1 = DirectFunctionCall1(interval_um, arg1); if (neg2) arg2 = DirectFunctionCall1(interval_um, arg2); return DatumGetInt32(DirectFunctionCall2(interval_cmp, arg1, arg2)); } /* Other typed literals: lexical comparison as fallback */ return strcmp(rdfnode1.lex, rdfnode2.lex); } /* * LiteralsComparable * ------------------ * Checks whether two rdfnode values can be ordered with relational operators * (<, <=, >=, >) under SPARQL 1.1 rules. Language-tagged literals are never * comparable. All other pairs must share the same type category (both numeric, * both temporal, etc.). * * Raises an ERROR rather than returning false when operands are incompatible, * matching SPARQL semantics (a type error, not a NULL result). * * n1, n2: the rdfnode operands to check * * returns true if the values can be ordered; raises ERROR otherwise */ bool LiteralsComparable(rdfnode *n1, rdfnode *n2) { rdfnode_info rdfnode1 = parse_rdfnode(n1); rdfnode_info rdfnode2 = parse_rdfnode(n2); /* identify the shared type category between the two literals */ bool bothNumeric = rdfnode1.isNumeric && rdfnode2.isNumeric; bool bothDate = rdfnode1.isDate && rdfnode2.isDate; bool bothDateTime = rdfnode1.isDateTime && rdfnode2.isDateTime; bool bothTime = rdfnode1.isTime && rdfnode2.isTime; bool bothDuration = rdfnode1.isDuration && rdfnode2.isDuration; bool bothString = (rdfnode1.isString || rdfnode1.isPlainLiteral) && (rdfnode2.isString || rdfnode2.isPlainLiteral); bool bothBoolean = rdfnode1.isBoolean && rdfnode2.isBoolean; /* check for language-tagged literals (not comparable) */ if (strlen(rdfnode1.lang) != 0 || strlen(rdfnode2.lang) != 0) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("cannot compare language-tagged literals"))); /* * literals are comparable only if both are of the same comparable category: * numeric, date, dateTime, or duration */ if (bothNumeric || bothDate || bothDateTime || bothDuration || bothString || bothTime || bothBoolean) return true; /* * Two literals can be incomparable for either of two reasons, and saying * "different datatypes" for both of them describes only the first. A pair * that shares a datatype the operator table of SPARQL 1.1 17.3 does not * list -- xsd:anyURI, or a datatype of the application's own -- has no * ordering defined for it at all, and reporting that as a difference * between the two sends the reader looking for one that is not there. */ if (strlen(rdfnode1.dtype) != 0 && strcmp(rdfnode1.dtype, rdfnode2.dtype) == 0) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("cannot compare literals of datatype %s", rdfnode1.dtype), errdetail("SPARQL 1.1 defines the ordering operators over numeric, " "boolean, string, date, dateTime, time and duration " "literals."))); ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("cannot compare literals of different datatypes"))); } /* * parse_rdfnode * ------------- * Decodes a raw rdfnode varlena value into an rdfnode_info struct, extracting * the lexical value (with Unicode escapes resolved), datatype URI, language tag, * and setting the type-classification flags used by all comparison and aggregate * functions. * * node: the rdfnode value to parse * * returns a populated rdfnode_info; all flag fields reflect the node's datatype */ rdfnode_info parse_rdfnode(rdfnode *node) { rdfnode_info result = {NULL, NULL, NULL, false}; char *raw = rdfnode_to_cstring(node); char *lexical = lex(raw); elog(DEBUG3, "%s called: input='%s'", __func__, raw); result.raw = raw; result.lex = unescape_unicode(lexical); result.dtype = datatype(raw); result.lang = lang(raw); /* initialize all flags */ result.isPlainLiteral = false; result.isDate = false; result.isDateTime = false; result.isString = false; result.isNumeric = false; result.isDuration = false; result.isTime = false; result.isIRI = false; result.isBlank = false; result.isBoolean = false; if (isIRI(raw)) result.isIRI = true; else if (isBlank(raw)) result.isBlank = true; /* flag the literal as simple if there is no language or data type */ else if (strlen(result.dtype) == 0 && strlen(result.lang) == 0) result.isPlainLiteral = true; else if (strcmp(result.dtype, RDF_XSD_STRING) == 0) result.isString = true; else if ((result.isNumeric = isNumeric(raw))) elog(DEBUG4, "literal '%s' is numeric ", result.raw); else if (strcmp(result.dtype, RDF_XSD_DATE) == 0) result.isDate = true; else if (strcmp(result.dtype, RDF_XSD_DATETIME) == 0) result.isDateTime = true; else if (strcmp(result.dtype, RDF_XSD_DURATION) == 0) result.isDuration = true; else if (strcmp(result.dtype, RDF_XSD_TIME) == 0) result.isTime = true; else if (strcmp(result.dtype, RDF_XSD_BOOLEAN) == 0) result.isBoolean = true; /* * An xsd:anyURI literal used to be flagged as a plain literal, on the * grounds that SPARQL treats it as an xsd:string. It does not. RDF 1.1 * Concepts 3.3 makes two literals the same term only when their lexical * form, datatype IRI and language tag all agree, and SPARQL 1.1 17.3 does * not list xsd:anyURI among the datatypes '=' and the ordering operators * are defined over, so a pair carrying it falls to RDFterm-equal in * 17.4.1.7. Fuseki and GraphDB report both '=' against an xsd:string and * '<' between two xsd:anyURIs as type errors. * * Left unflagged, the term reaches the same paths as any other typed * literal: equal to a term written exactly as it is, unequal to one * carrying a different datatype, and not ordered against anything. */ elog(DEBUG4, "literal '%s' is %s ", result.raw, result.dtype); elog(DEBUG3, "%s exit", __func__); return result; }