/*--------------------------------------------------------------------- * * sparql.c * SPARQL-related functions for RDF data manipulation. * * Implements SPARQL 1.1 string functions, accessor functions, and * type checking. * * Copyright (C) 2022-2026 Jim Jones * *--------------------------------------------------------------------- */ #include "postgres.h" #include "rdf_fdw.h" #include "rdf_utils.h" #include "rdfnode.h" #include "sparql.h" #include "lib/stringinfo.h" #include "catalog/pg_collation.h" #include "mb/pg_wchar.h" #include "utils/builtins.h" #include "utils/timestamp.h" #include #include #include /* * lex * --- * * Extracts the lexical value of a given RDF literal. Input that is not a * quoted literal is returned unchanged. * * input: RDF literal * * returns: lexical value of an RDF literal */ char *lex(char *input) { StringInfoData output; const char *start = input; int len; Assert(input != NULL); len = strlen(input); initStringInfo(&output); elog(DEBUG3, "%s called: input='%s'", __func__, input); if (len == 0) return ""; /* Handle quoted literal */ if (start[0] == '"') { const char *p; start++; /* skip opening quote */ p = start; while (*p) { if (*p == '"') { /* Check for doubled quote escape ("") */ if (*(p + 1) && *(p + 1) == '"') { /* Escaped quote: append one quote and skip both */ appendStringInfoChar(&output, '"'); p += 2; continue; } /* Unescaped quote: closing quote found */ break; } if (*p == '\\' && *(p + 1)) { /* * A backslash escapes exactly the byte that follows it, so * escape pairs are consumed two at a time. This is what makes * a lookbehind ("is the previous byte a backslash?") both * unnecessary and wrong: after an escaped backslash the byte * before a quote is a backslash even though that quote is not * escaped, which used to hide the closing quote of values * ending in an even-length backslash run. */ appendStringInfoChar(&output, *p); p++; } appendStringInfoChar(&output, *p); p++; } /* No closing quote found — malformed, return whole string */ if (*p != '"') { resetStringInfo(&output); appendStringInfoString(&output, input); return output.data; } /* Successful: return parsed inside quotes */ return output.data; } /* * Anything else -- an IRI, a blank node, or the bare lexical content that * strlang(), strdt(), iri() and the string functions pass around -- is * returned as it stands, the way rdfnode_in() reads an unquoted string: * only a quoted literal carries an annotation. Cutting an unquoted string * at the first '@' or '^^' that could start one mangled content that * merely contains either -- 'x^^y' became "x", and the IRI body * https://foo/@bar became . */ appendStringInfoString(&output, start); return output.data; } /* * lang * ---- * * Extracts the language tag from an RDF literal, if present. Returns an * empty string if no language tag is found or if the input is invalid/empty. * * input: Null-terminated C string representing an RDF literal (e.g., * "abc"@en, "123"^^xsd:int) * * returns: Null-terminated C string representing the language tag (e.g., * "en") or empty string */ char *lang(char *input) { StringInfoData buf; const char *ptr; const char *end; elog(DEBUG3, "%s called: input='%s'", __func__, input ? input : "(null)"); if (!input || strlen(input) == 0) return ""; ptr = input; end = input + strlen(input); /* * Find the end of the lexical form in the original input. * * For a quoted literal this has to be done on the input itself. lex() * does not return a substring of its argument there: it collapses * doubled quotes, and for a literal with no closing quote it returns the * whole input, opening quote included. Its length therefore says nothing * about how many bytes of the input the lexical form occupies, and using * it to advance a pointer into the input walks past the terminator. */ if (*ptr == '"') { ptr++; /* skip opening quote */ /* scan for the closing quote, honouring the escapes lex() knows */ while (ptr < end) { if (*ptr == '"') { /* a doubled quote is an escaped quote, not the end */ if (ptr + 1 < end && *(ptr + 1) == '"') { ptr += 2; continue; } break; } if (*ptr == '\\' && ptr + 1 < end) ptr++; /* skip the escaped character */ ptr++; } /* no closing quote: the literal is malformed and has no tag */ if (ptr == end) return ""; ptr++; /* skip closing quote */ } else { /* * Unquoted: an IRI, a blank node or bare lexical content, none of * which carries a language tag -- lex() returns all of it. */ elog(DEBUG3, "%s exit: returning empty string (not a quoted literal)", __func__); return ""; } /* check for language tag */ if (ptr < end && *ptr == '@') { const char *tag_start = ptr + 1; const char *tag_end = tag_start; while (tag_end < end && (isalnum((unsigned char)*tag_end) || *tag_end == '-' || *tag_end == '_')) tag_end++; initStringInfo(&buf); appendBinaryStringInfo(&buf, tag_start, tag_end - tag_start); elog(DEBUG3, "%s exit: returning => '%s'", __func__, buf.data); return buf.data; } elog(DEBUG3, "%s exit: returning empty string", __func__); return ""; } /* * strlang * ------- * * Constructs an RDF literal by combining a lexical value with a specified * language tag. The result is formatted as a language-tagged RDF literal. * * literal: Null-terminated C string representing an RDF literal or lexical * value (e.g., "abc") * language: Null-terminated C string representing the language tag (e.g., * "en") * * returns: Null-terminated C string formatted as a language-tagged RDF * literal (e.g., "abc"@en) */ char *strlang(char *literal, char *language) { StringInfoData buf; char *lex_language; char *lex_literal; char *tag; /* * STRICT: Executor handles NULL from SQL but * internal calls should not pass NULL. */ Assert(literal != NULL); Assert(language != NULL); if (isBlank(literal)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid argument: %s", literal), errdetail("Blank nodes cannot have language tags."))); lex_language = lex(language); lex_literal = lex(literal); /* lex() always returns a non-NULL string */ Assert(lex_language != NULL); Assert(lex_literal != NULL); elog(DEBUG3, "%s called: literal='%s', language='%s'", __func__, literal, language); if (strlen(lex_language) == 0) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("language tag cannot be empty"))); /* * A language tag is case-insensitive, and RDF 1.1 Concepts 3.3 puts its * value space in lower case: "Lexical representations of language tags MAY * be converted to lower case. The value space of language tags is always * in lower case." So the whole tag is lowered, not just the primary * subtag. Lowering part of it left one tag reaching storage as several * terms -- @EN-GB as en-GB, @en-gb as en-gb -- which value equality called * equal and the operator class, which compares the stored term, did not. */ tag = pstrdup(lex_language); for (char *p = tag; *p; p++) *p = pg_tolower((unsigned char)*p); initStringInfo(&buf); if (strlen(lex_literal) == 0) appendStringInfo(&buf, "\"\"@%s", tag); else appendStringInfo(&buf, "%s@%s", str(literal), tag); elog(DEBUG3, "%s exit: returning => '%s'", __func__, buf.data); return buf.data; } /* * strstarts * --------- * * Implements the core logic for the SPARQL STRSTARTS function, returning true * if the lexical form of the first argument (string) starts with the lexical * form of the second argument (substring), or false if arguments are * incompatible or the condition fails. An empty substring is considered a * prefix of any string, per SPARQL behavior. * * str: Null-terminated C string representing an RDF literal or value * (e.g., "foobar") * substr: Null-terminated C string representing an RDF literal or value * (e.g., "foo") * * returns: C boolean (true if string starts with substring, false otherwise * or if incompatible) */ bool strstarts(char *str, char *substr) { char *str_lexical; char *substr_lexical; size_t str_len; size_t substr_len; int result; /* * STRICT: executor handles NULL from SQL but * internal calls should not pass NULL. */ Assert(str != NULL); Assert(substr != NULL); /* compare the values, not their escapes (see DecodeLexicalForm()) */ str_lexical = DecodeLexicalForm(lex(str)); substr_lexical = DecodeLexicalForm(lex(substr)); /* lex() always returns a non-NULL string */ Assert(str_lexical != NULL); Assert(substr_lexical != NULL); str_len = strlen(str_lexical); substr_len = strlen(substr_lexical); elog(DEBUG3, "%s called: str='%s', substr='%s'", __func__, str, substr); if (!LiteralsCompatible(str, substr)) { elog(DEBUG3, "%s exit: returning 'false' (incompatible literals)", __func__); return false; } if (substr_len == 0) { elog(DEBUG3, "%s exit: returning 'true' (empty substring is a prefix of any string)", __func__); return true; } if (substr_len > str_len) { elog(DEBUG3, "%s exit: returning 'false' (substring longer than string cannot be a prefix)", __func__); return false; } result = strncmp(str_lexical, substr_lexical, substr_len); elog(DEBUG3, "%s exit: returning '%s'", __func__, result == 0 ? "true" : "false"); return result == 0; } /* * strends * ------- * * Implements the core logic for the SPARQL STRENDS function, returning true * if the lexical form of the first argument (string) ends with the lexical * form of the second argument (substring), or false if arguments are * incompatible or the condition fails. An empty substring is considered a * suffix of any string, per SPARQL behavior. * * str: Null-terminated C string representing an RDF literal or value * (e.g., "foobar") * substr: Null-terminated C string representing an RDF literal or value * (e.g., "bar") * * returns: C boolean (true if string ends with substring, false otherwise * or if incompatible) */ bool strends(char *str, char *substr) { char *str_lexical; char *substr_lexical; size_t str_len; size_t substr_len; int result; /* * STRICT: executor handles NULL from SQL but * internal calls should not pass NULL. */ Assert(str != NULL); Assert(substr != NULL); /* compare the values, not their escapes (see DecodeLexicalForm()) */ str_lexical = DecodeLexicalForm(lex(str)); substr_lexical = DecodeLexicalForm(lex(substr)); /* lex() always returns a non-NULL string */ Assert(str_lexical != NULL); Assert(substr_lexical != NULL); str_len = strlen(str_lexical); substr_len = strlen(substr_lexical); elog(DEBUG3, "%s called: str='%s', substr='%s'", __func__, str, substr); if (!LiteralsCompatible(str, substr)) { elog(DEBUG3, "%s exit: returning 'false' (incompatible literals)", __func__); return false; } if (substr_len == 0) { elog(DEBUG3, "%s exit: returning 'true' (an empty substring is a suffix of any string)", __func__); return true; } if (substr_len > str_len) { elog(DEBUG3, "%s exit: returning 'false' (substring longer than string cannot be a suffix)", __func__); return false; } result = strncmp(str_lexical + (str_len - substr_len), substr_lexical, substr_len); elog(DEBUG3, "%s exit: returning '%s'", __func__, result == 0 ? "true" : "false"); return result == 0; } /* * strdt * ----- * * Constructs an RDF literal by combining a lexical value with a specified * datatype IRI. Uses ExpandDatatypePrefix to handle prefix expansion * (e.g., "xsd:" to full URI) or retain prefixed/bare forms without angle * brackets unless fully expanded. * * literal: Null-terminated C string representing an RDF literal or lexical * value (e.g., "123") * datatype: Null-terminated C string representing the datatype IRI * (e.g., "xsd:int", "foo:bar") * * returns: Null-terminated C string formatted as a datatype-tagged RDF * literal (e.g., "123"^^, * "foo"^^foo:bar) */ char *strdt(char *literal, char *datatype) { StringInfoData buf; char *lex_datatype; Assert(literal != NULL); Assert(datatype != NULL); if (isBlank(literal)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid argument: %s", literal), errdetail("Blank nodes cannot have data types."))); lex_datatype = lex(datatype); Assert(lex_datatype != NULL); elog(DEBUG3, "%s called: literal='%s', datatype='%s'", __func__, literal, datatype); if (strlen(lex_datatype) == 0) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("datatype IRI cannot be empty"))); if (ContainsWhitespaces(datatype)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("datatype IRI cannot contain whitespaces"))); initStringInfo(&buf); if (isIRI(datatype)) appendStringInfo(&buf, "%s^^%s", str(literal), datatype); else { char *expanded_datatype; elog(DEBUG2, "%s: data type not an IRI", __func__); expanded_datatype = ExpandDatatypePrefix(lex_datatype); appendStringInfo(&buf, "%s^^%s", str(literal), iri(expanded_datatype)); } elog(DEBUG3, "%s exit: returning => '%s'", __func__, buf.data); return buf.data; } /* * str * --- * * Extracts the lexical value of an RDF literal or the string form of an IRI * and returns it as a new RDF literal. If the input is empty or null, * returns an empty RDF literal. * * input: Null-terminated C string representing an RDF literal or IRI * (e.g., "abc"@en, "") * * returns: Null-terminated C string formatted as an RDF literal * (e.g., "abc", "http://example.org") */ char *str(char *input) { StringInfoData buf; char *result; elog(DEBUG3, "%s called: input='%s'", __func__, input ? input : "(null)"); if (!input || input[0] == '\0') { elog(DEBUG3, "%s exit: returning empty literal", __func__); return "\"\""; } if (isIRI(input)) { size_t len = strlen(input); initStringInfo(&buf); appendStringInfo(&buf, "\"%.*s\"", (int)(len - 2), input + 1); /* skip '<' and trim '>' */ elog(DEBUG3, "%s exit: returning IRI '%s'", __func__, buf.data); return buf.data; } result = cstring_to_rdfliteral(lex(input)); elog(DEBUG3, "%s exit: returning literal '%s'", __func__, result); return result; } /* * iri * --- * * Converts a string to an IRI by wrapping it in angle brackets (< >), * mimicking SPARQL's IRI() function. Strips quotes and any language tags or * datatypes if present *only* for quoted literals. Raw strings and * pre-wrapped IRIs are preserved. * * input: Null-terminated C string representing an RDF literal, bare string, * or pre-formed IRI (e.g., "http://example.org", "\"http://example.org\"") * * returns: Null-terminated C string wrapped in angle brackets * (e.g., ""), or "<>" for empty/NULL input */ char *iri(char *input) { StringInfoData buf; char *lexical; elog(DEBUG3, "%s called: input='%s'", __func__, input ? input : "(null)"); if (!input || *input == '\0') return "<>"; if (isBlank(input)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid argument: %s", input), errdetail("Blank nodes cannot be converted to IRIs."))); if (isIRI(input)) return pstrdup(input); initStringInfo(&buf); lexical = lex(input); appendStringInfo(&buf, "<%s>", lexical); /* * Wrapping a lexical form in angle brackets does not make it an IRI: the * body may still carry a character grammar rule [139] excludes, and '>' in * particular would close the IRI early and leave the rest of the body as * further tokens in whatever query the term reaches. Unlike a term that * arrives through rdfnode_in(), which falls back to a quoted literal, one * built here has been asked for as an IRI, so an invalid body is a type * error -- which is what SPARQL 1.1 17.4.2.8 specifies for IRI(). * * "<>" is the empty relative IRI and is allowed, matching the empty-input * case handled above. */ if (strcmp(buf.data, "<>") != 0 && !isIRI(buf.data)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid IRI: %s", buf.data), errdetail("An IRI cannot contain <, >, \", {, }, |, ^, `, \\ " "or a character in #x00-#x20."))); elog(DEBUG3, "%s exit: returning wrapped IRI '%s'", __func__, buf.data); return pstrdup(buf.data); } /* * bnode * ----- * * Implements SPARQL’s BNODE function. Without arguments (input = NULL), * generates a unique blank node (e.g., "_:b123"). With a string argument, * returns a blank node based on the lexical form of the input (e.g., * BNODE("xyz") → "_:xyz"). Invalid inputs (e.g., IRIs, blank nodes, empty * literals) return NULL. * * input: Null-terminated C string (literal or bare string) for BNODE(str), * or NULL for BNODE(). * * returns: Null-terminated C string representing a blank node (e.g., * "_:xyz"), or NULL for invalid inputs. */ char *bnode(char *input) { StringInfoData buf; static uint64 counter = 0; /* Ensure uniqueness for BNODE() */ elog(DEBUG3, "%s called: input='%s'", __func__, input ? input : "(null)"); initStringInfo(&buf); if (input == NULL) { /* * BNODE(): every call must yield a distinct blank node. * * The counter never repeats within a backend, so it alone carries that * guarantee; the timestamp only tells concurrent backends apart, which * matters once the identifiers reach a shared triplestore. Both are * emitted as separate fields, with a separator. Folding them into one * number would not do: arithmetic can map two distinct pairs onto the * same value, and without the separator "1" and "23" produce the same * digits as "12" and "3". */ TimestampTz ts = GetCurrentTimestamp(); appendStringInfo(&buf, "_:b%llu_%llu", (unsigned long long)ts, (unsigned long long)counter++); } else { StringInfoData input_buf; char *normalized_input; char *lexical; /* Reject IRIs explicitly */ if (isIRI(input)) { elog(DEBUG3, "%s exit: returning NULL (input is an IRI)", __func__); return NULL; } /* If input is already a blank node, return it as-is (idempotent behavior) */ if (isBlank(input)) { elog(DEBUG3, "%s exit: returning input as-is (already a blank node)", __func__); appendStringInfoString(&buf, input); return buf.data; } /* Normalize input: quote bare strings */ initStringInfo(&input_buf); if (*input != '"' && !strstr(input, "^^") && !strstr(input, "@")) { appendStringInfoChar(&input_buf, '"'); appendStringInfoString(&input_buf, input); appendStringInfoChar(&input_buf, '"'); } else { appendStringInfoString(&input_buf, input); } normalized_input = input_buf.data; /* Validate input is a literal */ if (!isLiteral(normalized_input)) { elog(DEBUG3, "%s exit: returning NULL (input is not a literal)", __func__); return NULL; } /* Extract lexical form */ lexical = lex(normalized_input); if (!lexical || strlen(lexical) == 0) { elog(DEBUG3, "%s exit: returning NULL (lexical value either NULL or an empty string)", __func__); return NULL; } /* Create blank node ID, sanitizing lexical form (alphanumeric or underscore) */ appendStringInfoString(&buf, "_:"); for (char *p = lexical; *p; p++) { if (isalnum((unsigned char)*p)) appendStringInfoChar(&buf, *p); else appendStringInfoChar(&buf, '_'); } } elog(DEBUG3, "%s exit: returning '%s'", __func__, buf.data); return buf.data; } /* * concat * ------ * * Implements the SPARQL CONCAT function. Concatenates two RDF literals while * preserving compatible language tags or datatype annotations (specifically * xsd:string). If both inputs share the same language tag, the result carries * that tag. If both inputs are typed as xsd:string, the result is typed as * xsd:string. Mixing a simple literal with a language-tagged or * xsd:string-typed value, or conflicting language tags, yields a plain * literal without type or language tag. * * left: Null-terminated C string representing the first RDF literal or bare * string (e.g., "foo", "\"foo\"@en") * right: Null-terminated C string representing the second RDF literal or * bare string (e.g., "bar", "\"bar\"@en") * * returns: Null-terminated C string representing the concatenated RDF * literal (e.g., "\"foobar\"@en") */ char *concat(char *left, char *right) { char *left_lexical, *right_lexical; char *left_language, *right_language; char *left_datatype, *right_datatype; char *result; StringInfoData buf; elog(DEBUG3, "%s called: left='%s', right='%s'", __func__, left ? left : "(null)", right ? right : "(null)"); if (!left || !right) ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), errmsg("CONCAT arguments cannot be NULL"))); if (isIRI(left) || isIRI(right) || isBlank(left) || isBlank(right)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("CONCAT not allowed on IRI or blank node"))); left_lexical = lex(left); right_lexical = lex(right); left_language = lang(left); right_language = lang(right); left_datatype = datatype(left); right_datatype = datatype(right); elog(DEBUG3, "%s: left_lexical='%s', right_lexical='%s', left_language='%s', right_language='%s'", __func__, left_lexical, right_lexical, left_language, right_language); initStringInfo(&buf); appendStringInfo(&buf, "%s%s", left_lexical, right_lexical); /* Per SPARQL 1.1 spec: * - If both have identical language tags, preserve the tag * - If both have no language tag, return simple literal * - Otherwise (including conflicting tags), return simple literal */ if (strlen(left_language) > 0 && strlen(right_language) > 0) { if (strcmp(left_language, right_language) == 0) { /* Identical language tags - preserve them */ result = strlang(buf.data, left_language); } else { /* Conflicting language tags - return simple literal (no tag) */ elog(DEBUG3, "%s: conflicting language tags '%s' and '%s', returning simple literal", __func__, left_language, right_language); result = cstring_to_rdfliteral(buf.data); } } else if (strlen(left_language) > 0 || strlen(right_language) > 0) { /* One has language tag, other doesn't - return simple literal */ elog(DEBUG3, "%s: mixed language tags, returning simple literal", __func__); result = cstring_to_rdfliteral(buf.data); } else if (strlen(left_datatype) > 0 && strlen(right_datatype) > 0 && strcmp(left_datatype, right_datatype) == 0) { /* Both have same datatype - preserve it */ result = strdt(buf.data, left_datatype); } else { /* No language tags or mixed datatypes - return simple literal */ result = cstring_to_rdfliteral(buf.data); } pfree(buf.data); elog(DEBUG3, "%s exit: returning '%s'", __func__, result); return result; } /* * isIRI * ----- * * Checks if a string is an RDF IRI. A valid IRI must start with '<' and end * with '>', and its body must satisfy the SPARQL 1.1 grammar rule [139]: * * IRIREF ::= '<' ([^<>"{}|^`\] - [#x00-#x20])* '>' * * so the characters '<', '>', '"', '{', '}', '|', '^', '`', '\' and every * character in #x00-#x20 (the ASCII controls and the space) are forbidden * inside it. Both absolute (e.g., ) and relative * (e.g., ) IRIs are accepted. * * Rejecting these matters beyond classification: the deparser and the * SPARQL Update builder copy an IRI term into the query verbatim, trusting it * to be a single token. A body carrying '>' would close the IRI early and turn * the trailing bytes into further tokens, so a value that fails this test is * treated by rdfnode_in() as a plain string literal instead, which quotes it. * * input: Null-terminated C string representing an RDF term * (e.g., "", "\"hello\"", "_:b1") * * returns: bool (true if input is a valid IRI, false otherwise) */ bool isIRI(char *input) { size_t len; size_t i; if (input == NULL || (len = strlen(input)) < 3) return false; /* Must be enclosed in <...> */ if (input[0] != '<' || input[len - 1] != '>') return false; /* Reject every character grammar rule [139] excludes from an IRIREF. */ for (i = 1; i < len - 1; i++) { unsigned char c = (unsigned char) input[i]; if (c <= 0x20 || c == '<' || c == '>' || c == '"' || c == '{' || c == '}' || c == '|' || c == '^' || c == '`' || c == '\\') return false; } /* All checks passed — valid IRI (absolute or relative) */ return true; } /* * isBlank * ------- * * Mimics SPARQL's isBlank function. Checks if the input is a blank node label: * "_:" followed by a label that conforms to the SPARQL 1.1 grammar rule [142]: * * BLANK_NODE_LABEL ::= '_:' (PN_CHARS_U | [0-9]) * ((PN_CHARS | '.')* PN_CHARS)? * * The label must be non-empty, may not begin or end with '.', and may contain * only PN_CHARS: ASCII letters, digits, '_', '-', '.', and (as the rest of the * extension treats UTF-8) any byte at or above 0x80. Everything else -- in * particular whitespace, '<', '>', '"' and the ASCII controls -- is rejected. * * As with isIRI(), this is not only classification: a blank node label is * copied into a generated FILTER verbatim, so a label carrying such a character * would break out of the token. A term that fails this test is handled by * rdfnode_in() as a plain string literal, which quotes it. * * term: Null-terminated C string, an RDF term (e.g., "_:b1", "", "\"hello\"") * * returns: Boolean (true if blank node, false otherwise) */ bool isBlank(char *term) { size_t len; size_t i; elog(DEBUG3, "%s called: term='%s'", __func__, term ? term : "(null)"); /* Must start with "_:" and carry a non-empty label. */ if (!term || strncmp(term, "_:", 2) != 0 || (len = strlen(term)) <= 2) { elog(DEBUG3, "%s exit: returning 'false' (invalid input)", __func__); return false; } /* The label may not begin or end with '.' (rule [142]). */ if (term[2] == '.' || term[len - 1] == '.') { elog(DEBUG3, "%s exit: returning 'false' (label starts or ends with '.')", __func__); return false; } /* Every label character must be PN_CHARS or '.'. */ for (i = 2; i < len; i++) { unsigned char c = (unsigned char) term[i]; if (isalnum(c) || c == '_' || c == '-' || c == '.' || c >= 0x80) continue; elog(DEBUG3, "%s exit: returning 'false' (illegal character in label)", __func__); return false; } elog(DEBUG3, "%s exit: returning 'true'", __func__); return true; } /* * isLiteral * --------- * * Checks if an RDF term is a literal per SPARQL 1.1 spec. Returns true for simple * literals (e.g., "\"hello\""), language-tagged literals (e.g., "\"hello\"@en"), * or typed literals (e.g., "\"12\"^^xsd:integer"). Returns false for IRIs * (e.g., ""), blank nodes (e.g., "_:bnode"), bare numbers * (e.g., "123"), empty strings, or invalid inputs. * * term: Null-terminated C string representing an RDF term * * returns: bool (true if term is a literal, false otherwise) */ bool isLiteral(char *term) { const char *ptr; const char *suffix; elog(DEBUG3, "%s called: term='%s'", __func__, term ? term : "(null)"); if (!term || *term == '\0') { elog(DEBUG3, "%s exit: returning 'false' (term either NULL or has no '\\0')", __func__); return false; } /* Exclude IRIs and blank nodes first */ if (isIRI(term) || isBlank(term)) { elog(DEBUG3, "%s exit: returning 'false' (either an IRI or a blank node)", __func__); return false; } /* Normalize input */ ptr = cstring_to_rdfliteral(term); /* * What kind of literal this is depends on what follows its closing quote, * not on where the first '@' or '^^' is: the lexical form may contain * either. */ suffix = LiteralSuffix(ptr); if (suffix != NULL) { /* Simple literal: nothing after the closing quote */ if (*suffix == '\0') { elog(DEBUG3, "%s exit: returning 'true' (simple literal - no ^^ or @)", __func__); return true; } /* Typed literal: has ^^ followed by datatype */ if (suffix[0] == '^' && suffix[1] == '^') { const char *dt_start = suffix + 2; if (*dt_start != '\0' && (*dt_start != '<' || *(dt_start + 1) != '>')) { elog(DEBUG3, "%s exit: returning 'true' (valid datatype)", __func__); return true; } /* Valid datatype */ } /* Language-tagged literal: has @ with language tag */ else if (*suffix == '@' && *(suffix + 1) != '\0') { elog(DEBUG3, "%s exit: returning 'true' (literal has a language tag)", __func__); return true; } } /* Invalid or non-literal */ elog(DEBUG3, "%s exit: returning 'false' (invalid or non-literal)", __func__); return false; } /* * langmatches * ----------- * * Mimics SPARQL's LANGMATCHES function. Compares a language tag against a pattern, * supporting basic matching and wildcards (*). Case-insensitive per RFC 4647. * Returns true if the language tag matches the pattern, false otherwise. * * lang_tag: Null-terminated C string, typically a language tag (e.g., "en" from lang()) * pattern: Null-terminated C string, language range (e.g., "en", "en-*", "*") * * returns: Boolean (true if lang_tag matches pattern, false otherwise) */ bool langmatches(char *lang_tag, char *pattern) { char *tag; char *pat; bool result; elog(DEBUG3, "%s called: lang_tag='%s', pattern='%s'", __func__, lang_tag ? lang_tag : "(null)", pattern ? pattern : "(null)"); if (!lang_tag || !pattern) { elog(DEBUG3, "%s exit: returning 'false' (NULL input)", __func__); return false; } tag = lex(lang_tag); pat = lex(pattern); Assert(tag != NULL); Assert(pat != NULL); /* * Per SPARQL 1.1 spec and RFC 4647 basic filtering: * "*" matches any NON-EMPTY language tag. An empty tag (from a literal * with no language tag) never matches anything, including "*". */ if (strlen(tag) == 0) { elog(DEBUG3, "%s exit: returning 'false' (empty tag)", __func__); return false; } /* "*" matches any non-empty tag */ if (strcasecmp(pat, "*") == 0) { result = true; } /* Exact match (case-insensitive), e.g. "en" matches "en", "EN-US" matches "en-us" */ else if (strcasecmp(tag, pat) == 0) { result = true; } /* * Prefix subtag match per RFC 4647 basic filtering: * pattern "en" matches tag "en-US", "en-Latn-US", etc. * The tag must start with the pattern followed by '-'. */ else if (strncasecmp(tag, pat, strlen(pat)) == 0 && tag[strlen(pat)] == '-') { result = true; } /* * Wildcard suffix pattern, e.g. "en-*" matches "en" and "en-US". * Strip the trailing "-*" and match the prefix. */ else if (strlen(pat) >= 2 && pat[strlen(pat) - 1] == '*' && pat[strlen(pat) - 2] == '-') { size_t prefix_len = strlen(pat) - 2; /* length of "en" in "en-*" */ result = (strncasecmp(tag, pat, prefix_len) == 0 && (tag[prefix_len] == '\0' || tag[prefix_len] == '-')); } else { result = false; } elog(DEBUG3, "%s exit: returning '%s' (tag='%s', pat='%s')", __func__, result ? "true" : "false", tag, pat); return result; } /* * datatype * -------- * * Extracts the datatype URI of an RDF literal, following SPARQL 1.1 conventions. * Returns "" for simple literals and language-tagged literals (unbound per spec). * For typed literals (e.g., xsd: types), constructs the full URI using RDF_XSD_BASE_URI. * Returns "" for invalid or unrecognized inputs. * * input: Null-terminated C string representing an RDF literal (e.g., "123"^^xsd:int, "abc"@en, "xyz") * * returns: Null-terminated C string representing the datatype URI (e.g., "http://www.w3.org/2001/XMLSchema#int") */ char *datatype(char *input) { StringInfoData buf; const char *ptr; const char *suffix; elog(DEBUG3, "%s called: input='%s'", __func__, input ? input : "(null)"); /* Handle NULL or empty input */ if (input == NULL || *input == '\0') { elog(DEBUG3, "%s exit: returning empty string for NULL or empty input", __func__); return ""; } ptr = cstring_to_rdfliteral(input); Assert(ptr != NULL); initStringInfo(&buf); /* * The datatype annotation is whatever follows the closing quote, so that * is where "^^" has to be, not merely somewhere in the input: the lexical * form may contain '@' and '^^' as well. */ suffix = LiteralSuffix(ptr); if (suffix != NULL) { /* check for datatype first */ if (suffix[0] == '^' && suffix[1] == '^') { const char *dt_start = suffix + 2; /* skip ^^ */ const char *dt_end = dt_start; /* find the end of the datatype */ if (*dt_start == '<') { while (*dt_end && *dt_end != '>') dt_end++; if (*dt_end != '>') /* ensure proper closing */ { elog(DEBUG3, "%s exit: returning empty string (malformed datatype IRI, missing '>')", __func__); return ""; } dt_end++; /* include > */ } else { while (*dt_end && *dt_end != ' ' && *dt_end != '>' && *dt_end != '@') dt_end++; } if (dt_start < dt_end) { char *res = ""; /* handle xsd: prefix */ if (strncmp(dt_start, "xsd:", 4) == 0 && dt_end - dt_start > 4) { appendStringInfoString(&buf, RDF_XSD_BASE_URI); appendBinaryStringInfo(&buf, dt_start + 4, dt_end - (dt_start + 4)); } else if (*dt_start == '<' && *(dt_end - 1) == '>' && strncmp(dt_start + 1, "xsd:", 4) == 0 && dt_end - dt_start > 6) { appendStringInfoString(&buf, RDF_XSD_BASE_URI); appendBinaryStringInfo(&buf, dt_start + 5, dt_end - (dt_start + 6)); } else if (*dt_start == '<' && *(dt_end - 1) == '>') { appendBinaryStringInfo(&buf, dt_start + 1, dt_end - dt_start - 2); } else { appendBinaryStringInfo(&buf, dt_start, dt_end - dt_start); } /* ensure no trailing junk */ if (*dt_end != '\0') { elog(DEBUG3, "%s exit: returning empty string (trailing chars after datatype)", __func__); return res; } res = iri(buf.data); elog(DEBUG3, "%s exit: returning '%s'", __func__, res); return res; } } /* simple or language-tagged literal */ if (*suffix == '\0' || *suffix == '@') { elog(DEBUG3, "%s exit: returning empty string (simple/language-tagged literal)", __func__); return ""; } } /* Not a valid literal */ elog(DEBUG3, "%s exit: returning empty string (not a valid literal)", __func__); return ""; } /* * encode_for_uri * -------------- * * Encodes a string for use in a URI by percent-encoding all characters except * those defined as unreserved in RFC 3986 (alphanumeric, hyphen, period, * underscore, and tilde). If the input starts with a quote, it is treated as an * RDF literal and processed accordingly. * * str: Null-terminated C string to encode (e.g., "hello world", "\"example\"@en") * * returns: Null-terminated C string with URI-encoded result, formatted as an RDF literal */ char *encode_for_uri(char *str_in) { const char *unreserved = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"; size_t in_len; char *res; StringInfoData buf; /* * STRICT: executor handles NULL from SQL but * internal calls should not pass NULL */ Assert(str_in != NULL); elog(DEBUG3, "%s called: str='%s'", __func__, str_in); initStringInfo(&buf); /* the bytes of the value, not of its escapes (see DecodeLexicalForm()) */ str_in = DecodeLexicalForm(lex(str_in)); in_len = strlen(str_in); elog(DEBUG2, "%s: encoding string: '%s', length: %zu", __func__, str_in, in_len); for (size_t i = 0; i < in_len; i++) { unsigned char c = (unsigned char)str_in[i]; if (strchr(unreserved, c)) appendStringInfoChar(&buf, c); else appendStringInfo(&buf, "%%%02X", c); } res = cstring_to_rdfliteral(buf.data); elog(DEBUG3, "%s exit: returning => '%s'", __func__, res); return res; } /* * generate_uuid_v4 * ---------------- * Generates a version 4 (random) UUID per RFC 4122. Returns a lowercase string * in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, where y is 8, 9, A, or B. * Uses timestamp and counter for entropy, no external dependencies. * * Returns: Null-terminated C string (e.g., "123e4567-e89b-12d3-a456-426614174000") */ char *generate_uuid_v4(void) { StringInfoData buf; static uint64 counter = 0; uint64 seed; uint8_t bytes[16]; char *result; int i; elog(DEBUG3, "%s called", __func__); initStringInfo(&buf); /* * Seed from the timestamp and a per-backend call counter. The counter is * scaled so that it moves bits the timestamp's own increments do not * reach: combined at the same magnitude, one microsecond of elapsed time * and one call cancel each other out, and two calls receive the same seed * and therefore the same UUID. */ seed = (uint64)GetCurrentTimestamp() + counter++ * UINT64CONST(0x9E3779B97F4A7C15); /* Generate 16 bytes of pseudo-random data */ for (i = 0; i < 16; i++) { seed = (seed * 1103515245 + 12345) & 0x7fffffff; /* Linear congruential generator */ bytes[i] = (uint8_t)(seed >> 16); } /* Set version (4) and variant (y = 8, 9, A, B) */ bytes[6] = (bytes[6] & 0x0F) | 0x40; /* Version 4 */ bytes[8] = (bytes[8] & 0x3F) | 0x80; /* Variant: 10xx */ /* Format as xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx */ appendStringInfo(&buf, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]); result = pstrdup(buf.data); pfree(buf.data); elog(DEBUG3, "%s exit: returning '%s'", __func__, result); return result; } /* * substr_sparql * ------------- * Implements SPARQL's SUBSTR(str, start, length) function. * Converts RDF literal or bare string into substring while preserving language/datatype tag. * * str : Input RDF literal or bare string. * start : 1-based index (inclusive), following XPath rounding semantics. * length : Optional substring length. * * Returns a new RDF literal string with the appropriate tag preserved. */ char *substr_sparql(char *str, int start, int length) { char *lexical; char *str_datatype; char *str_language; char *result; text *input_text; text *substr_text; int str_len; int pg_start; int pg_length; elog(DEBUG3, "%s called: str='%s', start=%d, length=%d", __func__, str ? str : "(null)", start, length); if (!str) ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), errmsg("SUBSTR cannot be NULL"))); if (isIRI(str) || isBlank(str)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("SUBSTR not allowed on IRI or blank node: %s", str))); /* positions count the characters of the value (see DecodeLexicalForm()) */ lexical = DecodeLexicalForm(lex(str)); str_datatype = datatype(str); str_language = lang(str); elog(DEBUG3, "%s: lexical='%s', datatype='%s', language='%s'", __func__, lexical, str_datatype, str_language); str_len = pg_mbstrlen(lexical); if (length >= 0) { int64 end_before = (int64) start + length; pg_start = Max(start, 1); pg_length = (int) Min(Max(end_before - pg_start, 0), str_len); } else { pg_start = Max(start, 1); pg_length = str_len - pg_start + 1; } if (pg_start > str_len || pg_length <= 0) { if (strlen(str_language) > 0) return strlang("", str_language); else if (strlen(str_datatype) > 0) return strdt("", str_datatype); else return cstring_to_rdfliteral(""); } /* Use PostgreSQL's text_substr which handles UTF-8 correctly */ input_text = cstring_to_text(lexical); substr_text = DatumGetTextP(DirectFunctionCall3( text_substr, PointerGetDatum(input_text), Int32GetDatum(pg_start), Int32GetDatum(pg_length))); lexical = EncodeLexicalForm(text_to_cstring(substr_text)); if (strlen(str_language) > 0) result = strlang(lexical, str_language); else if (strlen(str_datatype) > 0) result = strdt(lexical, str_datatype); else result = cstring_to_rdfliteral(lexical); pfree(input_text); pfree(substr_text); elog(DEBUG3, "%s exit: returning '%s'", __func__, result); return result; } /* * lcase * ----- * * Implements SPARQL’s LCASE function. Converts the lexical form of a string literal * (simple, xsd:string, or language-tagged) to lowercase (ASCII A-Z to a-z, non-ASCII * preserved). Preserves the original datatype or language tag. Errors on IRIs, blank * nodes, non-string literals, or invalid inputs. Bare strings are treated as simple literals. * * str: Null-terminated C string (RDF literal or bare string, e.g., "BAR", "\"BAR\"@en") * * returns: Null-terminated C string (lowercase RDF literal) */ char *lcase(char *str) { char *lexical; char *str_datatype; char *str_language; char *result; elog(DEBUG3, "%s called: str='%s'", __func__, str ? str : "(null)"); if (!str) ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), errmsg("LCASE cannot be NULL"))); if (strlen(str) == 0) { elog(DEBUG3, "%s exit: returning empty literal (str is an empty string)", __func__); return cstring_to_rdfliteral(""); } /* Check for IRIs or blank nodes */ if (isIRI(str)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("LCASE does not allow IRIs: %s", str))); if (isBlank(str)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("LCASE does not allow blank nodes: %s", str))); /* the case of the value, not of its escapes (see DecodeLexicalForm()) */ lexical = DecodeLexicalForm(lex(str)); /* this shouldn't happen */ Assert(lexical != NULL); str_datatype = datatype(str); if (strlen(str_datatype) != 0 && !IsRDFStringLiteral(str_datatype)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("LCASE does not allow non-string literals: %s", str_datatype))); str_language = lang(str); elog(DEBUG3, "%s: lexical='%s', datatype='%s', language='%s'", __func__, lexical, str_datatype, str_language); /* Convert to lowercase using PostgreSQL's multibyte-aware function */ { text *input_text = cstring_to_text(lexical); Datum lower_datum = DirectFunctionCall3Coll( lower, DEFAULT_COLLATION_OID, PointerGetDatum(input_text), BoolGetDatum(false), (Datum)0); text *lower_text = DatumGetTextP(lower_datum); char *lowercase = text_to_cstring(lower_text); char *encoded = EncodeLexicalForm(lowercase); if (strlen(str_language) != 0) result = strlang(encoded, str_language); else if (strlen(str_datatype) != 0) result = strdt(encoded, str_datatype); else result = cstring_to_rdfliteral(encoded); pfree(lowercase); pfree(lower_text); pfree(input_text); } elog(DEBUG3, "%s exit: returning '%s'", __func__, result); return result; } /* * ucase * ----- * * Implements SPARQL’s UCASE function. Converts the lexical form of a string literal * (simple, xsd:string, or language-tagged) to uppercase (ASCII a-z to A-Z, non-ASCII * preserved). Preserves the original datatype or language tag. Errors on IRIs, blank * nodes, non-string literals, or invalid inputs. Bare strings are treated as simple literals. * * str: Null-terminated C string (RDF literal or bare string, e.g., "bar", "\"bar\"@en") * * returns: Null-terminated C string (uppercase RDF literal) */ char *ucase(char *str) { char *lexical; char *str_datatype; char *str_language; char *result; elog(DEBUG3, "%s called: str='%s'", __func__, str ? str : "(null)"); if (!str) ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), errmsg("UCASE cannot be NULL"))); if (strlen(str) == 0) { elog(DEBUG3, "%s exit: returning empty literal (str is an empty string)", __func__); return cstring_to_rdfliteral(""); } /* Check for IRIs or blank nodes */ if (isIRI(str)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("UCASE does not allow IRIs: %s", str))); if (isBlank(str)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("UCASE does not allow blank nodes: %s", str))); /* the case of the value, not of its escapes (see DecodeLexicalForm()) */ lexical = DecodeLexicalForm(lex(str)); /* this shouldn't happen */ Assert(lexical != NULL); str_datatype = datatype(str); if (strlen(str_datatype) != 0 && !IsRDFStringLiteral(str_datatype)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("UCASE does not allow non-string literals: %s", str_datatype))); str_language = lang(str); elog(DEBUG3, "%s: lexical='%s', datatype='%s', language='%s'", __func__, lexical, str_datatype, str_language); /* Convert to uppercase using PostgreSQL's multibyte-aware function */ { text *input_text = cstring_to_text(lexical); Datum upper_datum = DirectFunctionCall3Coll( upper, DEFAULT_COLLATION_OID, PointerGetDatum(input_text), BoolGetDatum(false), (Datum)0); text *upper_text = DatumGetTextP(upper_datum); char *uppercase = text_to_cstring(upper_text); char *encoded = EncodeLexicalForm(uppercase); if (strlen(str_language) != 0) result = strlang(encoded, str_language); else if (strlen(str_datatype) != 0) result = strdt(encoded, str_datatype); else result = cstring_to_rdfliteral(encoded); pfree(uppercase); pfree(upper_text); pfree(input_text); } elog(DEBUG3, "%s exit: returning => '%s'", __func__, result); return result; } /* * isNumeric * --------- * * Checks if an RDF term is numeric per SPARQL spec. Returns true if the term is a * bare number (e.g., "12") or a literal with a numeric datatype (e.g., xsd:integer, * xsd:nonNegativeInteger) and valid numeric lexical form. Returns false otherwise. * * term: Null-terminated C string representing an RDF term (e.g., "12", "12"^^xsd:integer) * * returns: Boolean indicating if the term is numeric */ /* * The lexical spaces of the XSD numeric datatypes, as SPARQL 1.1 needs them to * decide isNumeric() and how a literal orders. These validate the lexical form * only; they do not range-check a subtype such as xsd:byte. * * strtod(), which this code used before, is the wrong tool: it also accepts * spellings that are in no XSD numeric lexical space -- "0x10" (hex), "nan", * "inf"/"infinity", and a leading run of whitespace -- so an ill-typed literal * such as "0x10"^^xsd:integer was treated as the number 16. */ /* xsd:integer and its subtypes: [+-]? [0-9]+ */ static bool is_xsd_integer_lexical(const char *s) { bool digits = false; if (s == NULL) return false; if (*s == '+' || *s == '-') s++; while (isdigit((unsigned char) *s)) { s++; digits = true; } return digits && *s == '\0'; } /* xsd:decimal: [+-]? ( [0-9]+ ('.' [0-9]*)? | '.' [0-9]+ ) */ static bool is_xsd_decimal_lexical(const char *s) { bool digits = false; if (s == NULL) return false; if (*s == '+' || *s == '-') s++; while (isdigit((unsigned char) *s)) { s++; digits = true; } if (*s == '.') { s++; while (isdigit((unsigned char) *s)) { s++; digits = true; } } return digits && *s == '\0'; } /* * The numeral part of the xsd:double / xsd:float lexical space, without the * special representations: * [+-]? ( [0-9]+ ('.' [0-9]*)? | '.' [0-9]+ ) ([eE] [+-]? [0-9]+)? * * Kept separate from is_xsd_double_lexical() because xsd:decimal borrows the * exponent form from it (see isNumeric) but has no INF or NaN in its value * space at all. */ static bool is_xsd_double_numeral(const char *s) { bool digits = false; if (s == NULL) return false; if (*s == '+' || *s == '-') s++; while (isdigit((unsigned char) *s)) { s++; digits = true; } if (*s == '.') { s++; while (isdigit((unsigned char) *s)) { s++; digits = true; } } if (!digits) return false; if (*s == 'e' || *s == 'E') { s++; if (*s == '+' || *s == '-') s++; if (!isdigit((unsigned char) *s)) return false; while (isdigit((unsigned char) *s)) s++; } return *s == '\0'; } /* the full xsd:double / xsd:float lexical space, specials included */ static bool is_xsd_double_lexical(const char *s) { if (s == NULL) return false; if (strcmp(s, "NaN") == 0 || strcmp(s, "INF") == 0 || strcmp(s, "+INF") == 0 || strcmp(s, "-INF") == 0) return true; return is_xsd_double_numeral(s); } /* * xsd_collapse * ------------ * * XSD fixes whiteSpace="collapse" on every numeric datatype, so the lexical * form is trimmed before it is matched against the lexical space: " 12" is a * valid xsd:integer whose value is 12. Fuseki and Virtuoso both read it that * way -- each answers true for isNumeric(" 12"^^xsd:integer) and makes it * equal to "12"^^xsd:integer -- so trimming here is what keeps such a literal * numeric. * * Returns a palloc'd copy without leading or trailing whitespace. Whitespace * left inside still fails the validators, which is correct: collapsing cannot * join two numerals into one. */ static char * xsd_collapse(const char *s) { const char *start = s; const char *end; char *out; size_t len; while (*start == ' ' || *start == '\t' || *start == '\n' || *start == '\r') start++; end = start + strlen(start); while (end > start && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\n' || end[-1] == '\r')) end--; len = (size_t) (end - start); out = palloc(len + 1); memcpy(out, start, len); out[len] = '\0'; return out; } /* * xsd_integer_in_range * -------------------- * * The integer subtypes share xsd:integer's lexical space but each restricts * its value space, and a literal outside that space is ill-typed rather than * numeric. XSD 1.1 Part 2 bounds xsd:byte at -128..127, xsd:unsignedByte at * 0..255 and so on, while xsd:nonNegativeInteger and its three siblings * constrain only the sign and stay unbounded. Fuseki and Virtuoso both report * a literal outside the range as non-numeric. * * s : the collapsed lexical form, already known to match [+-]?[0-9]+ * dtype : the literal's datatype URI */ static bool xsd_integer_in_range(const char *s, const char *dtype) { bool negative = (*s == '-'); const char *digits = s + ((*s == '+' || *s == '-') ? 1 : 0); bool all_zero = true; const char *d; for (d = digits; *d != '\0'; d++) { if (*d != '0') { all_zero = false; break; } } /* sign-constrained, but unbounded in the other direction */ if (strcmp(dtype, RDF_XSD_NONNEGATIVEINTEGER) == 0) return !negative || all_zero; if (strcmp(dtype, RDF_XSD_POSITIVEINTEGER) == 0) return !negative && !all_zero; if (strcmp(dtype, RDF_XSD_NONPOSITIVEINTEGER) == 0) return negative || all_zero; if (strcmp(dtype, RDF_XSD_NEGATIVEINTEGER) == 0) return negative && !all_zero; /* the signed, bounded types */ if (strcmp(dtype, RDF_XSD_BYTE) == 0 || strcmp(dtype, RDF_XSD_SHORT) == 0 || strcmp(dtype, RDF_XSD_INT) == 0 || strcmp(dtype, RDF_XSD_LONG) == 0) { long long v; char *end; errno = 0; v = strtoll(s, &end, 10); if (errno == ERANGE || *end != '\0') return false; /* past xsd:long, so past every one of them */ if (strcmp(dtype, RDF_XSD_BYTE) == 0) return v >= -128LL && v <= 127LL; if (strcmp(dtype, RDF_XSD_SHORT) == 0) return v >= -32768LL && v <= 32767LL; if (strcmp(dtype, RDF_XSD_INT) == 0) return v >= -2147483648LL && v <= 2147483647LL; return true; /* xsd:long: strtoll already bounded it */ } /* the unsigned, bounded types */ if (strcmp(dtype, RDF_XSD_UNSIGNEDBYTE) == 0 || strcmp(dtype, RDF_XSD_UNSIGNEDSHORT) == 0 || strcmp(dtype, RDF_XSD_UNSIGNEDINT) == 0 || strcmp(dtype, RDF_XSD_UNSIGNEDLONG) == 0) { unsigned long long v; char *end; /* "-0" is in the lexical space of the non-negative types */ if (negative && !all_zero) return false; errno = 0; v = strtoull(digits, &end, 10); if (errno == ERANGE || *end != '\0') return false; if (strcmp(dtype, RDF_XSD_UNSIGNEDBYTE) == 0) return v <= 255ULL; if (strcmp(dtype, RDF_XSD_UNSIGNEDSHORT) == 0) return v <= 65535ULL; if (strcmp(dtype, RDF_XSD_UNSIGNEDINT) == 0) return v <= 4294967295ULL; return true; /* xsd:unsignedLong: strtoull already bounded it */ } return true; /* xsd:integer is unbounded */ } bool isNumeric(char *term) { char *lexical; char *datatype_uri; bool is_bare_number = false; elog(DEBUG3, "%s called: term='%s'", __func__, term ? term : "(null)"); if (!term || strlen(term) == 0) { elog(DEBUG3, "%s exit: returning 'false' (term either NULL or an empty string)", __func__); return false; } /* Check if term is a bare number (e.g., "12") */ if (term[0] != '"' && !strstr(term, "^^") && !strstr(term, "@")) { lexical = term; is_bare_number = true; } else { /* Extract lexical value using datatype’s helper */ lexical = lex(term); /* From datatype/strdt codebase */ } if (!lexical || strlen(lexical) == 0) { elog(DEBUG3, "%s exit: returning 'false' (lexical value either NULL or an empty string)", __func__); return false; } /* every XSD numeric datatype fixes whiteSpace="collapse" */ lexical = xsd_collapse(lexical); if (*lexical == '\0') { elog(DEBUG3, "%s exit: returning 'false' (lexical value is only whitespace)", __func__); return false; } /* * A bare number carries no datatype, so accept any of the three numeric * lexical spaces (integer, decimal or double). */ if (is_bare_number) { bool numeric = is_xsd_integer_lexical(lexical) || is_xsd_decimal_lexical(lexical) || is_xsd_double_lexical(lexical); elog(DEBUG3, "%s exit: returning '%s' (bare number)", __func__, numeric ? "true" : "false"); return numeric; } /* Get datatype using datatype function */ datatype_uri = datatype(term); if (strlen(datatype_uri) == 0) { elog(DEBUG3, "%s exit: returning 'false' (no datatype or invalid literal)", __func__); return false; } /* No datatype or invalid literal (e.g., "12") */ /* * The literal is numeric only if its lexical form is valid for its own * numeric datatype, and -- for the integer subtypes, which share one * lexical space but not one value space -- its value lies in that * datatype's range. */ if (strcmp(datatype_uri, RDF_XSD_INTEGER) == 0 || strcmp(datatype_uri, RDF_XSD_NONNEGATIVEINTEGER) == 0 || strcmp(datatype_uri, RDF_XSD_POSITIVEINTEGER) == 0 || strcmp(datatype_uri, RDF_XSD_NEGATIVEINTEGER) == 0 || strcmp(datatype_uri, RDF_XSD_NONPOSITIVEINTEGER) == 0 || strcmp(datatype_uri, RDF_XSD_LONG) == 0 || strcmp(datatype_uri, RDF_XSD_INT) == 0 || strcmp(datatype_uri, RDF_XSD_BYTE) == 0 || strcmp(datatype_uri, RDF_XSD_SHORT) == 0 || strcmp(datatype_uri, RDF_XSD_UNSIGNEDLONG) == 0 || strcmp(datatype_uri, RDF_XSD_UNSIGNEDINT) == 0 || strcmp(datatype_uri, RDF_XSD_UNSIGNEDSHORT) == 0 || strcmp(datatype_uri, RDF_XSD_UNSIGNEDBYTE) == 0) { bool ok = is_xsd_integer_lexical(lexical) && xsd_integer_in_range(lexical, datatype_uri); elog(DEBUG3, "%s exit: returning '%s' (integer family)", __func__, ok ? "true" : "false"); return ok; } else if (strcmp(datatype_uri, RDF_XSD_DECIMAL) == 0) { /* * xsd:decimal's lexical space has no exponent and no INF or NaN: XSD * 1.1 Part 2 3.3.3 admits only an optional sign, digits and at most one * '.'. A literal written any other way is ill-typed and has no value, * whatever produced it. */ bool ok = is_xsd_decimal_lexical(lexical); elog(DEBUG3, "%s exit: returning '%s' (decimal)", __func__, ok ? "true" : "false"); return ok; } else if (strcmp(datatype_uri, RDF_XSD_DOUBLE) == 0 || strcmp(datatype_uri, RDF_XSD_FLOAT) == 0) { bool ok = is_xsd_double_lexical(lexical); elog(DEBUG3, "%s exit: returning '%s' (double/float)", __func__, ok ? "true" : "false"); return ok; } elog(DEBUG3, "%s exit: returning 'false'", __func__); return false; } /* * contains * -------- * * Implements SPARQL’s CONTAINS(str, substr) function. Returns true if the lexical * form of str contains the lexical form of substr as a contiguous subsequence; * false otherwise. Matching is case-sensitive per SPARQL. * * str_in : Null-terminated C string representing an RDF term or bare string * substr_in : Null-terminated C string representing an RDF term or bare string * * returns: Boolean (true if substr occurs within str’s lexical form; false on * mismatch, incompatible language tags, or invalid/empty input) */ bool contains(char *str_in, char *substr_in) { char *str_lex; char *substr_lex; char *lang_str; bool result; elog(DEBUG3, "%s called: str='%s', substr='%s'", __func__, str_in ? str_in : "(null)", substr_in ? substr_in : "(null)"); /* handle NULL or empty inputs */ if (!str_in || !substr_in || strlen(str_in) == 0 || strlen(substr_in) == 0) { elog(DEBUG3, "%s exit: returning 'false' (invalid input)", __func__); return false; } lang_str = lang(str_in); if (strlen(lang_str) != 0) { char *lang_substr = lang(substr_in); if (strlen(lang_substr) != 0 && pg_strcasecmp(lang_str, lang_substr) != 0) { elog(DEBUG3, "%s exit: returning 'false' (string and substring have different language tags)", __func__); return false; } } /* extract the values (strips quotes, tags and escapes, see DecodeLexicalForm()) */ str_lex = DecodeLexicalForm(lex(str_in)); substr_lex = DecodeLexicalForm(lex(substr_in)); /* check if substr is in str using strstr */ result = (strstr(str_lex, substr_lex) != NULL); elog(DEBUG3, "%s exit: returning => '%s' (str_lexical='%s', substr_lexical='%s')", __func__, result ? "true" : "false", str_lex, substr_lex); return result; } /* * strbefore * --------- * * Implements the SPARQL STRBEFORE function, returning the substring of the first * argument before the first occurrence of the second argument (delimiter). The * result preserves the language tag or datatype of the first argument as present * in the input syntax. Simple literals remain simple in the output. * * str: the input string (e.g., "abc"@en, "abc"^^xsd:string) * delimiter: the delimiter string (e.g., "b", "b"@en) * * returns: cstring representing the RDF literal before the delimiter */ char *strbefore(char *str, char *delimiter) { char *str_lexical; char *delimiter_lexical; char *lang1; char *dt1 = ""; char *pos; char *result; elog(DEBUG3, "%s called: str='%s', delimiter='%s'", __func__, str, delimiter); /* * STRICT: executor handles NULL from SQL but * internal calls should not pass NULL */ Assert(str != NULL); Assert(delimiter != NULL); /* search the values, not their escapes (see DecodeLexicalForm()) */ str_lexical = DecodeLexicalForm(lex(str)); delimiter_lexical = DecodeLexicalForm(lex(delimiter)); lang1 = lang(str); Assert(str_lexical != NULL); Assert(delimiter_lexical != NULL); Assert(lang1 != NULL); /* extract datatypes if no language tags */ if (strlen(lang1) == 0) dt1 = datatype(str); if (!LiteralsCompatible(str, delimiter)) { elog(DEBUG3, "%s exit: returning NULL (literals not compatible)", __func__); return NULL; } if ((pos = strstr(str_lexical, delimiter_lexical)) != NULL) { size_t before_len = pos - str_lexical; StringInfoData buf; initStringInfo(&buf); if (strlen(lang1) > 0) { appendBinaryStringInfo(&buf, str_lexical, before_len); result = strlang(EncodeLexicalForm(buf.data), lang1); elog(DEBUG3, "%s exit: returning => '%s'", __func__, result); return result; } else if (strlen(dt1) > 0 && /* only for explicit ^^ */ (strcmp(dt1, RDF_SIMPLE_LITERAL_DATATYPE_PREFIXED) == 0 || strcmp(dt1, RDF_SIMPLE_LITERAL_DATATYPE) == 0)) { appendBinaryStringInfo(&buf, str_lexical, before_len); result = cstring_to_rdfliteral(EncodeLexicalForm(buf.data)); if (strstr(result, "^^") == NULL) { result = strdt(EncodeLexicalForm(buf.data), dt1); } elog(DEBUG3, "%s exit: returning => '%s'", __func__, result); return result; } else { /* simple literal or implicit xsd:string */ appendBinaryStringInfo(&buf, str_lexical, before_len); result = cstring_to_rdfliteral(EncodeLexicalForm(buf.data)); elog(DEBUG3, "%s exit: returning => '%s'", __func__, result); return result; } } /* delimiter not found */ if (strlen(dt1) > 0 && (strcmp(dt1, RDF_SIMPLE_LITERAL_DATATYPE_PREFIXED) == 0 || strcmp(dt1, RDF_SIMPLE_LITERAL_DATATYPE) == 0)) { result = cstring_to_rdfliteral(""); if (strstr(result, "^^") == NULL) { StringInfoData typed_buf; initStringInfo(&typed_buf); appendStringInfo(&typed_buf, "%s", strdt("", RDF_SIMPLE_LITERAL_DATATYPE_PREFIXED)); result = typed_buf.data; } elog(DEBUG3, "%s exit: returning => '%s'", __func__, result); return result; } result = cstring_to_rdfliteral(""); elog(DEBUG3, "%s exit: returning => '%s'", __func__, result); return result; } /* * strafter * -------- * * Implements the SPARQL STRAFTER function, returning the substring of the first * argument after the first occurrence of the second argument (delimiter). The * result preserves the language tag or datatype of the first argument as present * in the input syntax, always wrapped in double quotes as a valid RDF literal. * Returns an empty simple literal if the delimiter is not found. * * str: the input string (e.g., "abc"@en, "abc"^^xsd:string) * delimiter: the delimiter string (e.g., "b", "b"@en) * * returns: a cstring representing the RDF literal after the delimiter */ char *strafter(char *str, char *delimiter) { char *lexstr; char *lexdelimiter; char *lang1; char *dt1 = ""; char *pos; bool has_explicit_datatype = false; char *result; elog(DEBUG3, "%s called: str='%s', delimiter='%s'", __func__, str, delimiter); /* * STRICT: executor handles NULL from SQL but * internal calls should not pass NULL */ Assert(str != NULL); Assert(delimiter != NULL); /* search the values, not their escapes (see DecodeLexicalForm()) */ lexstr = DecodeLexicalForm(lex(str)); lexdelimiter = DecodeLexicalForm(lex(delimiter)); lang1 = lang(str); Assert(lexstr != NULL); Assert(lexdelimiter != NULL); Assert(lang1 != NULL); /* extract datatype if no language tag */ if (strlen(lang1) == 0) dt1 = datatype(str); /* check if arg1 has an explicit datatype in the input syntax */ if (strlen(lang1) == 0 && strstr(str, "^^") != NULL) has_explicit_datatype = true; if ((pos = strstr(lexstr, lexdelimiter)) != NULL) { size_t delimiter_len = strlen(lexdelimiter); char *after_start = pos + delimiter_len; size_t after_len = strlen(lexstr) - (after_start - lexstr); StringInfoData buf; initStringInfo(&buf); if (strlen(lang1) > 0) { appendBinaryStringInfo(&buf, after_start, after_len); result = strlang(EncodeLexicalForm(buf.data), lang1); pfree(buf.data); elog(DEBUG3, "%s exit: returning => '%s'", __func__, result); return result; } else if (has_explicit_datatype && strlen(dt1) > 0 && (strcmp(dt1, RDF_SIMPLE_LITERAL_DATATYPE_PREFIXED) == 0 || strcmp(dt1, RDF_SIMPLE_LITERAL_DATATYPE) == 0)) { appendBinaryStringInfo(&buf, after_start, after_len); result = cstring_to_rdfliteral(EncodeLexicalForm(buf.data)); if (strstr(result, "^^") == NULL) { result = strdt(EncodeLexicalForm(buf.data), dt1); } pfree(buf.data); elog(DEBUG3, "%s exit: returning => '%s'", __func__, result); return result; } else { /* simple literal or implicit xsd:string */ appendBinaryStringInfo(&buf, after_start, after_len); result = cstring_to_rdfliteral(EncodeLexicalForm(buf.data)); pfree(buf.data); elog(DEBUG3, "%s exit: returning => '%s'", __func__, result); return result; } } /* delimiter not found */ if (has_explicit_datatype && strlen(dt1) > 0 && (strcmp(dt1, RDF_SIMPLE_LITERAL_DATATYPE_PREFIXED) == 0 || strcmp(dt1, RDF_SIMPLE_LITERAL_DATATYPE) == 0)) { result = cstring_to_rdfliteral(""); if (strstr(result, "^^") == NULL) { StringInfoData typed_buf; initStringInfo(&typed_buf); appendStringInfo(&typed_buf, "%s", strdt("", RDF_SIMPLE_LITERAL_DATATYPE_PREFIXED)); result = typed_buf.data; } elog(DEBUG3, "%s exit: returning => '%s'", __func__, result); return result; } result = cstring_to_rdfliteral(""); elog(DEBUG3, "%s exit: returning => '%s'", __func__, result); return result; } /* * count_utf8_chars * ---------------- * * Counts Unicode characters (code points) in a UTF-8 encoded string by * skipping continuation bytes (0x80–0xBF), returning the number of * characters rather than bytes. * * str: Null-terminated UTF-8 encoded C string * * returns: Number of Unicode code points in str */ static int count_utf8_chars(const char *str) { int char_count = 0; elog(DEBUG3, "%s called: str='%s'", __func__, str); while (*str) { /* Skip continuation bytes (0x80-0xBF) */ if ((*str & 0xC0) != 0x80) char_count++; str++; } elog(DEBUG3, "%s exit: returning '%d'", __func__, char_count); return char_count; } /* * strlen_rdf * ---------- * * Implements SPARQL's STRLEN function. Returns the number of Unicode * characters (code points) in the lexical form of a string literal. Errors * on IRIs, blank nodes, and non-string-typed literals. * * str: Null-terminated C string representing an RDF literal or bare string * (e.g., "\"hello\"", "\"café\"@fr", "hello") * * returns: Number of Unicode characters in the lexical form */ int strlen_rdf(char *str) { char *lexical; char *dt; int result; elog(DEBUG3, "%s called: str='%s'", __func__, str ? str : "(null)"); if (!str) ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), errmsg("STRLEN cannot be NULL"))); if (strlen(str) == 0) return 0; /* Check for IRIs or blank nodes */ if (isIRI(str)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("STRLEN does not allow IRIs: %s", str))); if (isBlank(str)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("STRLEN does not allow blank nodes: %s", str))); dt = datatype(str); /* Validate string literal */ if (!IsRDFStringLiteral(dt)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("STRLEN does not allow non-string literals: %s", dt))); /* the characters of the value, not of its escapes (see DecodeLexicalForm()) */ lexical = DecodeLexicalForm(lex(str)); result = count_utf8_chars(lexical); elog(DEBUG3, "%s exit: returning '%d'", __func__, result); return result; } /* * get_xsd_numeric_type * -------------------- * * Determines the XSD numeric type from an rdfnode's datatype URI, used for * type promotion in numeric aggregates (SUM, AVG). All xsd:integer subtypes * map to XSD_TYPE_INTEGER. * * dtype: Null-terminated C string containing the full XSD datatype URI * (e.g., RDF_XSD_INTEGER, RDF_XSD_DOUBLE) * * returns: XsdNumericType value in the promotion hierarchy: * XSD_TYPE_INTEGER < XSD_TYPE_DECIMAL < XSD_TYPE_FLOAT < XSD_TYPE_DOUBLE */ XsdNumericType get_xsd_numeric_type(const char *dtype) { /* Handle all integer subtypes */ if (strcmp(dtype, RDF_XSD_INTEGER) == 0 || strcmp(dtype, RDF_XSD_INT) == 0 || strcmp(dtype, RDF_XSD_LONG) == 0 || strcmp(dtype, RDF_XSD_SHORT) == 0 || strcmp(dtype, RDF_XSD_BYTE) == 0 || strcmp(dtype, RDF_XSD_POSITIVEINTEGER) == 0 || strcmp(dtype, RDF_XSD_NEGATIVEINTEGER) == 0 || strcmp(dtype, RDF_XSD_NONNEGATIVEINTEGER) == 0 || strcmp(dtype, RDF_XSD_NONPOSITIVEINTEGER) == 0 || strcmp(dtype, RDF_XSD_UNSIGNEDLONG) == 0 || strcmp(dtype, RDF_XSD_UNSIGNEDINT) == 0 || strcmp(dtype, RDF_XSD_UNSIGNEDSHORT) == 0 || strcmp(dtype, RDF_XSD_UNSIGNEDBYTE) == 0) return XSD_TYPE_INTEGER; if (strcmp(dtype, RDF_XSD_DECIMAL) == 0) return XSD_TYPE_DECIMAL; if (strcmp(dtype, RDF_XSD_FLOAT) == 0) return XSD_TYPE_FLOAT; if (strcmp(dtype, RDF_XSD_DOUBLE) == 0) return XSD_TYPE_DOUBLE; /* Default to decimal for unknown numeric types */ return XSD_TYPE_DECIMAL; } /* * get_xsd_datatype_uri * -------------------- * * Returns the XSD datatype URI constant for a given numeric type level, * used when formatting aggregate results (SUM, AVG) as typed rdfnode * literals. Falls back to RDF_XSD_DECIMAL for unrecognised inputs. * * type: XsdNumericType promotion level * (e.g., XSD_TYPE_INTEGER, XSD_TYPE_DOUBLE) * * returns: Null-terminated C string containing the full XSD datatype URI * (e.g., RDF_XSD_INTEGER, RDF_XSD_DECIMAL, etc.) */ const char *get_xsd_datatype_uri(XsdNumericType type) { switch (type) { case XSD_TYPE_INTEGER: return RDF_XSD_INTEGER; case XSD_TYPE_DECIMAL: return RDF_XSD_DECIMAL; case XSD_TYPE_FLOAT: return RDF_XSD_FLOAT; case XSD_TYPE_DOUBLE: return RDF_XSD_DOUBLE; default: return RDF_XSD_DECIMAL; } } /* * rdfnode_lexical_is_infinity * --------------------------- * * Reports whether a numeric term's lexical form is one of the infinities, and * through 'negative' which one. * * XSD 1.1 Part 2 3.3.5 admits exactly "INF", "+INF" and "-INF", and * isNumeric() has already refused any other spelling, so an exact match is * enough. "NaN" is not among them: numeric carries it on every supported * version, and numeric_add() propagates it as IEEE asks. * * lex : the collapsed lexical form * negative: set to true for "-INF" * * returns true if the term is an infinity */ static bool rdfnode_lexical_is_infinity(const char *lex, bool *negative) { if (strcmp(lex, "INF") == 0 || strcmp(lex, "+INF") == 0) { *negative = false; return true; } if (strcmp(lex, "-INF") == 0) { *negative = true; return true; } return false; } /* * rdfnode_infinite_sum_lexical * ---------------------------- * * Gives the lexical form of a sum that has an infinity in it, or NULL if it * has none. * * IEEE 754 makes the sum of +INF and -INF a NaN, and an infinity of either * sign swallows every finite value, so the accumulated numeric does not enter * into it. This is kept out of that accumulator because numeric has no * infinity before PostgreSQL 14: numeric_in() refuses "INF" there, and a * group holding one failed outright rather than summing to the INF every * endpoint answers. * * state: the aggregate state * * returns "INF", "-INF", "NaN", or NULL when no infinity was seen */ static const char * rdfnode_infinite_sum_lexical(const RdfnodeAggState *state) { if (state->has_pos_inf && state->has_neg_inf) return "NaN"; if (state->has_pos_inf) return "INF"; if (state->has_neg_inf) return "-INF"; return NULL; } /* * sum_rdfnode_sfunc * ----------------- * Aggregate transition function for SUM(rdfnode). * Converts rdfnode to numeric and accumulates the sum. * * Strict numeric-only policy: * - If any non-numeric value is present, returns NULL (unbound). * - Type promotion: integer < decimal < float < double. * - Example: SUM({1, 2, 3}) = 6; SUM({1, 2, "string"}) = NULL. * * State is stored as RdfnodeAggState to track both sum and result type. * * Note: Aggregate context validation is handled by the wrapper in * rdf_fdw.c */ Datum sum_rdfnode_sfunc(PG_FUNCTION_ARGS) { RdfnodeAggState *aggstate; MemoryContext aggcontext; MemoryContext oldcontext; rdfnode *node; rdfnode_info parsed; Datum rdf_numeric; XsdNumericType inputType; /* Get the aggregate memory context */ AggCheckCallContext(fcinfo, &aggcontext); /* Get current state (NULL on first call) */ if (PG_ARGISNULL(0)) aggstate = NULL; else aggstate = (RdfnodeAggState *)PG_GETARG_POINTER(0); /* Skip NULL input values */ if (PG_ARGISNULL(1)) { if (aggstate == NULL) PG_RETURN_NULL(); PG_RETURN_POINTER(aggstate); } /* Get the rdfnode and parse it */ node = (rdfnode *)PG_GETARG_TEXT_PP(1); parsed = parse_rdfnode(node); /* * Mark that we received input (even if non-numeric). * This distinguishes SUM({}) from SUM({"string"}) per SPARQL 1.1. */ if (aggstate == NULL) { /* Initialize state to track that we saw input */ oldcontext = MemoryContextSwitchTo(aggcontext); aggstate = (RdfnodeAggState *)palloc0(sizeof(RdfnodeAggState)); aggstate->has_input = true; aggstate->has_non_numeric = false; MemoryContextSwitchTo(oldcontext); } else { aggstate->has_input = true; } /* * Per SPARQL 1.1 spec Section 18.5.1.3: SUM returns an error if any * values are not numeric. Errors are excluded from the aggregate, so * if any non-numeric values are present, the entire SUM aggregate * returns unbound (NULL). Examples: * - SUM({1, 2, 3}) = 6 (all numeric) * - SUM({1, 2, "string"}) = NULL (mixed types cause error) * - SUM({"string"}) = NULL (all non-numeric) */ if (!parsed.isNumeric) { /* Non-numeric value - mark as error and skip it */ aggstate->has_non_numeric = true; PG_RETURN_POINTER(aggstate); } /* Determine the XSD type of this input */ inputType = get_xsd_numeric_type(parsed.dtype); /* * An infinity is recorded beside the accumulator rather than in it. * numeric has none before PostgreSQL 14, so numeric_in() refuses "INF" * there and the whole group failed; and from 14 on it would still have to * be taken back out, since the datatype's own output spells it a way XSD * does not admit. Either way the finite terms make no difference to the * answer once one is present. */ { bool negative; if (rdfnode_lexical_is_infinity(parsed.lex, &negative)) { if (negative) aggstate->has_neg_inf = true; else aggstate->has_pos_inf = true; aggstate->count++; if (inputType > aggstate->maxType) aggstate->maxType = inputType; PG_RETURN_POINTER(aggstate); } } /* Convert rdfnode lexical value to numeric */ rdf_numeric = DirectFunctionCall3(numeric_in, CStringGetDatum(parsed.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); /* Initialize or update numeric accumulator */ if (aggstate->numeric_value == NULL) { /* * First finite value. The promoted type is merged rather than * assigned: an infinity may have come before this one and carries it. * maxType starts at XSD_TYPE_INTEGER, the lowest, so the merge is * also right when nothing came before. */ oldcontext = MemoryContextSwitchTo(aggcontext); aggstate->numeric_value = DatumGetNumeric( DirectFunctionCall1(numeric_uplus, rdf_numeric)); if (inputType > aggstate->maxType) aggstate->maxType = inputType; MemoryContextSwitchTo(oldcontext); } else { /* Add to accumulator - need to be in aggcontext for result */ oldcontext = MemoryContextSwitchTo(aggcontext); aggstate->numeric_value = DatumGetNumeric( DirectFunctionCall2(numeric_add, NumericGetDatum(aggstate->numeric_value), rdf_numeric)); /* * Track the highest type seen (type promotion: * integer < decimal < float < double) */ if (inputType > aggstate->maxType) aggstate->maxType = inputType; MemoryContextSwitchTo(oldcontext); } PG_RETURN_POINTER(aggstate); } /* * sum_rdfnode_finalfunc * --------------------- * Final function for SUM(rdfnode). * Converts the accumulated numeric sum back to rdfnode with proper type promotion. * * Note: NULL state handling is done by the wrapper in rdf_fdw.c */ /* * rdfnode_numeric_to_float8 * ------------------------- * * Reads a numeric accumulator as the IEEE double it stands for. * * strtod() is what carries the value across, because it answers with an * infinity for an accumulator that has run past the largest finite double -- * the answer IEEE gives -- where numeric's own conversion raises "out of * range" instead. It also reads back the "Infinity" and "NaN" that numeric_out * writes. * * n: the accumulated value * * returns its value as a double */ static double rdfnode_numeric_to_float8(Numeric n) { char *str = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(n))); double val = strtod(str, NULL); pfree(str); return val; } /* * rdfnode_float_lexical * --------------------- * * Writes a double as the lexical form of an xsd:double or an xsd:float. * * PostgreSQL spells the infinities "Infinity" and "-Infinity", which are in * neither datatype's lexical space: XSD 1.1 Part 2 3.3.5 fixes them as "INF" * and "-INF". This is the normalisation rdfnode_numeric_arith() already * applies to the result of an arithmetic operator, for the same reason. * * val : the value to write * single: true for xsd:float, false for xsd:double * * returns a palloc'd lexical form */ static char * rdfnode_float_lexical(double val, bool single) { char *result; if (single) result = DatumGetCString(DirectFunctionCall1(float4out, Float4GetDatum((float4) val))); else result = DatumGetCString(DirectFunctionCall1(float8out, Float8GetDatum(val))); if (strcmp(result, "Infinity") == 0) return pstrdup("INF"); if (strcmp(result, "-Infinity") == 0) return pstrdup("-INF"); return result; } Datum sum_rdfnode_finalfunc(PG_FUNCTION_ARGS) { RdfnodeAggState *aggstate; char *sum_str; char *result; const char *datatype_uri; /* Get the state (already validated as non-NULL by wrapper) */ aggstate = (RdfnodeAggState *)PG_GETARG_POINTER(0); /* If state is NULL (no rows), return zero per SPARQL Sum({}). */ if (aggstate == NULL) PG_RETURN_TEXT_P(cstring_to_text(strdt("0", RDF_XSD_INTEGER))); /* If no numeric values were summed, return NULL (unbound per SPARQL) */ if (aggstate->has_non_numeric || (aggstate->numeric_value == NULL && !aggstate->has_pos_inf && !aggstate->has_neg_inf)) PG_RETURN_NULL(); /* an infinity among the inputs decides the sum on its own */ { const char *infinite = rdfnode_infinite_sum_lexical(aggstate); if (infinite != NULL) PG_RETURN_TEXT_P(cstring_to_text( strdt((char *) infinite, (char *) get_xsd_datatype_uri(aggstate->maxType)))); } /* * A sum promoted to xsd:double or xsd:float is an IEEE value and has to * be written as one; the exact datatypes keep numeric's own output. */ if (aggstate->maxType == XSD_TYPE_DOUBLE || aggstate->maxType == XSD_TYPE_FLOAT) sum_str = rdfnode_float_lexical(rdfnode_numeric_to_float8(aggstate->numeric_value), aggstate->maxType == XSD_TYPE_FLOAT); else sum_str = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(aggstate->numeric_value))); /* Get the appropriate XSD datatype based on type promotion */ datatype_uri = get_xsd_datatype_uri(aggstate->maxType); /* Format as typed literal rdfnode using strdt() */ result = strdt(sum_str, (char *)datatype_uri); pfree(sum_str); PG_RETURN_TEXT_P(cstring_to_text(result)); } /* * avg_rdfnode_sfunc * ----------------- * Aggregate transition function for AVG(rdfnode). * Accumulates sum and count for computing average. * * Note: Aggregate context validation and NULL input handling done by wrapper in rdf_fdw.c */ Datum avg_rdfnode_sfunc(PG_FUNCTION_ARGS) { RdfnodeAggState *aggstate; MemoryContext aggcontext; MemoryContext oldcontext; rdfnode *node; rdfnode_info parsed; Datum rdf_numeric; XsdNumericType inputType; /* Get the aggregate memory context */ AggCheckCallContext(fcinfo, &aggcontext); /* Get current state (NULL on first call) */ if (PG_ARGISNULL(0)) aggstate = NULL; else aggstate = (RdfnodeAggState *)PG_GETARG_POINTER(0); /* Skip NULL input values */ if (PG_ARGISNULL(1)) { if (aggstate == NULL) PG_RETURN_NULL(); PG_RETURN_POINTER(aggstate); } /* Get the rdfnode and parse it */ node = (rdfnode *)PG_GETARG_TEXT_PP(1); parsed = parse_rdfnode(node); /* Mark that we received input (even if non-numeric). * This distinguishes AVG({}) from AVG({"string"}) per SPARQL 1.1 spec. */ if (aggstate == NULL) { /* Initialize state to track that we saw input */ oldcontext = MemoryContextSwitchTo(aggcontext); aggstate = (RdfnodeAggState *)palloc0(sizeof(RdfnodeAggState)); aggstate->has_input = true; aggstate->has_non_numeric = false; MemoryContextSwitchTo(oldcontext); } else { aggstate->has_input = true; } /* * Per SPARQL 1.1 spec Section 18.5.1.4: AVG returns an error if any values are not numeric. * Errors are excluded from the aggregate, so if any non-numeric values are present, * the entire AVG aggregate returns unbound (NULL). Examples: * - AVG({10, 20, 30}) = 20 (all numeric) * - AVG({10, 20, "string"}) = NULL (mixed types cause error) * - AVG({"string"}) = NULL (all non-numeric) */ if (!parsed.isNumeric) { /* Non-numeric value - mark as error and skip it */ aggstate->has_non_numeric = true; PG_RETURN_POINTER(aggstate); } /* Determine the XSD type of this input */ inputType = get_xsd_numeric_type(parsed.dtype); /* * An infinity is recorded beside the accumulator rather than in it. * numeric has none before PostgreSQL 14, so numeric_in() refuses "INF" * there and the whole group failed; and from 14 on it would still have to * be taken back out, since the datatype's own output spells it a way XSD * does not admit. Either way the finite terms make no difference to the * answer once one is present. */ { bool negative; if (rdfnode_lexical_is_infinity(parsed.lex, &negative)) { if (negative) aggstate->has_neg_inf = true; else aggstate->has_pos_inf = true; aggstate->count++; if (inputType > aggstate->maxType) aggstate->maxType = inputType; PG_RETURN_POINTER(aggstate); } } /* Convert rdfnode lexical value to numeric */ rdf_numeric = DirectFunctionCall3(numeric_in, CStringGetDatum(parsed.lex), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1)); /* Initialize or update numeric accumulator */ if (aggstate->numeric_value == NULL) { /* * First finite value. The count and the promoted type are merged rather * than assigned: an infinity may have come before this one and carries it. * maxType starts at XSD_TYPE_INTEGER, the lowest, so the merge is * also right when nothing came before. */ oldcontext = MemoryContextSwitchTo(aggcontext); aggstate->numeric_value = DatumGetNumeric(DirectFunctionCall1(numeric_uplus, rdf_numeric)); aggstate->count++; if (inputType > aggstate->maxType) aggstate->maxType = inputType; MemoryContextSwitchTo(oldcontext); } else { /* Add to accumulator */ oldcontext = MemoryContextSwitchTo(aggcontext); aggstate->numeric_value = DatumGetNumeric(DirectFunctionCall2(numeric_add, NumericGetDatum(aggstate->numeric_value), rdf_numeric)); aggstate->count++; /* Track the highest type seen (type promotion: integer < decimal < float < double) */ if (inputType > aggstate->maxType) aggstate->maxType = inputType; MemoryContextSwitchTo(oldcontext); } PG_RETURN_POINTER(aggstate); } /* * avg_rdfnode_finalfunc * --------------------- * Final function for AVG(rdfnode). * Computes average by dividing sum by count, with proper type promotion. * * Note: NULL state handling is done by the wrapper in rdf_fdw.c */ Datum avg_rdfnode_finalfunc(PG_FUNCTION_ARGS) { RdfnodeAggState *aggstate; Numeric count_numeric; Numeric avg_numeric; Numeric avg_trunc0; char *avg_str; char *result; const char *datatype_uri; XsdNumericType outType; bool is_exact_integer = false; /* Get the state (already validated as non-NULL by wrapper) */ aggstate = (RdfnodeAggState *)PG_GETARG_POINTER(0); /* If state is NULL (no rows), return zero per SPARQL Avg({}). */ if (aggstate == NULL) PG_RETURN_TEXT_P(cstring_to_text(strdt("0", RDF_XSD_INTEGER))); /* If no numeric values were aggregated, return NULL (unbound per SPARQL) */ if (aggstate->has_non_numeric || (aggstate->numeric_value == NULL && !aggstate->has_pos_inf && !aggstate->has_neg_inf)) PG_RETURN_NULL(); /* * 18.5.1.4 divides the Sum by the count, and an infinity or a NaN divided * by a positive integer is itself, so the Sum's own answer stands. */ { const char *infinite = rdfnode_infinite_sum_lexical(aggstate); if (infinite != NULL) PG_RETURN_TEXT_P(cstring_to_text( strdt((char *) infinite, (char *) get_xsd_datatype_uri(aggstate->maxType)))); } /* Convert count to numeric for division */ count_numeric = DatumGetNumeric(DirectFunctionCall1(int8_numeric, Int64GetDatum(aggstate->count))); /* Compute average: sum / count */ avg_numeric = DatumGetNumeric(DirectFunctionCall2(numeric_div, NumericGetDatum(aggstate->numeric_value), NumericGetDatum(count_numeric))); /* Determine output type for AVG: * - If any double was seen, use xsd:double * - else if any float was seen, use xsd:float * - else use xsd:decimal (even if the average is an exact integer) * This ensures AVG over integer-only inputs yields xsd:decimal, e.g., 42.0 */ outType = aggstate->maxType; /* Check exact-integer condition by truncating scale to 0 and comparing */ avg_trunc0 = DatumGetNumeric(DirectFunctionCall2(numeric_trunc, NumericGetDatum(avg_numeric), Int32GetDatum(0))); is_exact_integer = DatumGetBool(DirectFunctionCall2(numeric_eq, NumericGetDatum(avg_numeric), NumericGetDatum(avg_trunc0))); if (outType == XSD_TYPE_DOUBLE) { /* keep double */ } else if (outType == XSD_TYPE_FLOAT) { /* keep float */ } else { /* For integer-only or decimal inputs, return decimal */ outType = XSD_TYPE_DECIMAL; } /* Convert result to string. * For xsd:decimal and exact-integer values, append ".0" to match common SPARQL engine output. */ if (outType == XSD_TYPE_DECIMAL) { if (is_exact_integer) { char *int_str = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(avg_trunc0))); StringInfoData buf; initStringInfo(&buf); appendStringInfo(&buf, "%s.0", int_str); avg_str = buf.data; pfree(int_str); } else { avg_str = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(avg_numeric))); } } else if (outType == XSD_TYPE_FLOAT) { /* the Sum is an xsd:float, so the division happens at that width */ float4 sum = (float4) rdfnode_numeric_to_float8(aggstate->numeric_value); avg_str = rdfnode_float_lexical((double) (sum / (float4) aggstate->count), true); } else { /* * 18.5.1.4 makes Avg the Sum divided by the count, and a Sum promoted * to xsd:double or xsd:float is an IEEE value: the division has to * happen there too. Dividing the numeric accumulator and converting * afterwards gives a different answer whenever the sum itself is not * representable -- two values just under the datatype's maximum * average back to one of them, where Fuseki and GraphDB answer INF. */ avg_str = rdfnode_float_lexical(rdfnode_numeric_to_float8(aggstate->numeric_value) / (double) aggstate->count, false); } /* Map chosen type to XSD URI */ datatype_uri = get_xsd_datatype_uri(outType); /* Format as typed literal rdfnode using strdt() */ result = strdt(avg_str, (char *)datatype_uri); pfree(avg_str); PG_RETURN_TEXT_P(cstring_to_text(result)); } /* * get_rdfnode_category_rank * ------------------------- * Returns a category rank for an rdfnode to support * mixed-type aggregate ordering. Lower rank = lower * priority for MAX, higher priority for MIN. * * Category order (low → high): * -2: blank nodes * -1: IRIs * 0: string-like (plain literal, xsd:string, language-tagged) * 1: numeric (xsd:integer, xsd:decimal, xsd:float, etc.) * 2: dateTime * 3: date * 4: time * 5: duration * 6: other */ static int get_rdfnode_category_rank(rdfnode_info parsed) { if (parsed.isBlank) return -2; if (parsed.isIRI) return -1; if (strlen(parsed.lang) > 0 || parsed.isPlainLiteral || parsed.isString) return 0; if (parsed.isNumeric) return 1; if (parsed.isDateTime) return 2; if (parsed.isDate) return 3; if (parsed.isTime) return 4; if (parsed.isDuration) return 5; return 6; } /* * min_rdfnode_sfunc * ----------------- * Aggregate transition function for MIN(rdfnode). * Compares rdfnode values and keeps track of the minimum. * * Mixed-type policy (Fuseki-compatible): * - Assigns each term to a category (string-like < numeric < temporal). * - MIN selects the lowest category present; ties resolved by comparator. * - Example: MIN({"zebra"^^xsd:string, 42, "mango"^^xsd:string}) → * "mango"^^xsd:string (string category wins; lexical minimum among * strings). * * Note: Aggregate context validation and NULL input handling done by * wrapper in rdf_fdw.c */ Datum min_rdfnode_sfunc(PG_FUNCTION_ARGS) { RdfnodeAggState *aggstate; MemoryContext aggcontext; MemoryContext oldcontext; text *input_node; rdfnode_info input_parsed; rdfnode_info current_parsed; /* Get the aggregate memory context */ AggCheckCallContext(fcinfo, &aggcontext); /* Get current state (NULL on first call) */ if (PG_ARGISNULL(0)) aggstate = NULL; else aggstate = (RdfnodeAggState *)PG_GETARG_POINTER(0); /* Skip NULL input values */ if (PG_ARGISNULL(1)) { if (aggstate == NULL) PG_RETURN_NULL(); PG_RETURN_POINTER(aggstate); } /* Get and parse the input rdfnode */ input_node = PG_GETARG_TEXT_PP(1); input_parsed = parse_rdfnode((rdfnode *)input_node); if (aggstate == NULL) { /* First row: allocate state and store the rdfnode */ oldcontext = MemoryContextSwitchTo(aggcontext); aggstate = (RdfnodeAggState *)palloc(sizeof(RdfnodeAggState)); aggstate->rdfnode_value = (text *)PG_DETOAST_DATUM_COPY(PointerGetDatum(input_node)); MemoryContextSwitchTo(oldcontext); PG_RETURN_POINTER(aggstate); } /* Parse current value for category-based comparison */ current_parsed = parse_rdfnode((rdfnode *)aggstate->rdfnode_value); /* * Choose the smallest category present, then the minimum within * that category. */ { int rank_in = get_rdfnode_category_rank(input_parsed); int rank_cur = get_rdfnode_category_rank(current_parsed); if (rank_in < rank_cur) { /* Input has lower category → new minimum */ oldcontext = MemoryContextSwitchTo(aggcontext); pfree(aggstate->rdfnode_value); aggstate->rdfnode_value = (text *)PG_DETOAST_DATUM_COPY( PointerGetDatum(input_node)); MemoryContextSwitchTo(oldcontext); } else if (rank_in == rank_cur) { /* Same category → use comparator */ int cmp = rdfnode_cmp_for_aggregate( (rdfnode *)input_node, (rdfnode *)aggstate->rdfnode_value); if (cmp < 0) { oldcontext = MemoryContextSwitchTo(aggcontext); pfree(aggstate->rdfnode_value); aggstate->rdfnode_value = (text *)PG_DETOAST_DATUM_COPY( PointerGetDatum(input_node)); MemoryContextSwitchTo(oldcontext); } } /* rank_in > rank_cur: keep current (higher category) */ } PG_RETURN_POINTER(aggstate); } /* * min_rdfnode_finalfunc * --------------------- * Final function for MIN(rdfnode). * Returns the minimum rdfnode value stored as text. * * Note: NULL state handling is done by the wrapper in rdf_fdw.c */ Datum min_rdfnode_finalfunc(PG_FUNCTION_ARGS) { RdfnodeAggState *aggstate; /* Get the state (already validated as non-NULL by wrapper) */ aggstate = (RdfnodeAggState *)PG_GETARG_POINTER(0); if (aggstate == NULL || aggstate->rdfnode_value == NULL) PG_RETURN_NULL(); /* Return the stored minimum rdfnode */ PG_RETURN_TEXT_P(aggstate->rdfnode_value); } /* * max_rdfnode_sfunc * ----------------- * Aggregate transition function for MAX(rdfnode). * Compares rdfnode values and keeps track of the maximum. * * Mixed-type policy: * - Assigns each term to a category (string-like < numeric < temporal). * - MAX selects the highest category present; ties resolved by comparator. * - Example: MAX({42, "2023-01-01"^^xsd:date}) → "2023-01-01"^^xsd:date * (date category wins over numeric). * * Note: Aggregate context validation and NULL input handling done by * wrapper in rdf_fdw.c */ Datum max_rdfnode_sfunc(PG_FUNCTION_ARGS) { RdfnodeAggState *aggstate; MemoryContext aggcontext; MemoryContext oldcontext; text *input_node; rdfnode_info input_parsed; rdfnode_info current_parsed; /* Get the aggregate memory context */ AggCheckCallContext(fcinfo, &aggcontext); /* Get current state (NULL on first call) */ if (PG_ARGISNULL(0)) aggstate = NULL; else aggstate = (RdfnodeAggState *)PG_GETARG_POINTER(0); /* Skip NULL input values */ if (PG_ARGISNULL(1)) { if (aggstate == NULL) PG_RETURN_NULL(); PG_RETURN_POINTER(aggstate); } /* Get and parse the input rdfnode */ input_node = PG_GETARG_TEXT_PP(1); input_parsed = parse_rdfnode((rdfnode *)input_node); if (aggstate == NULL) { /* First row: allocate state and store the rdfnode */ oldcontext = MemoryContextSwitchTo(aggcontext); aggstate = (RdfnodeAggState *)palloc(sizeof(RdfnodeAggState)); aggstate->rdfnode_value = (text *)PG_DETOAST_DATUM_COPY(PointerGetDatum(input_node)); MemoryContextSwitchTo(oldcontext); PG_RETURN_POINTER(aggstate); } current_parsed = parse_rdfnode((rdfnode *)aggstate->rdfnode_value); /* * Choose the largest category present, then the maximum within * that category. */ { int rank_in = get_rdfnode_category_rank(input_parsed); int rank_cur = get_rdfnode_category_rank(current_parsed); if (rank_in > rank_cur) { /* Input has higher category → new maximum */ oldcontext = MemoryContextSwitchTo(aggcontext); pfree(aggstate->rdfnode_value); aggstate->rdfnode_value = (text *)PG_DETOAST_DATUM_COPY( PointerGetDatum(input_node)); MemoryContextSwitchTo(oldcontext); } else if (rank_in == rank_cur) { /* Same category → use comparator */ int cmp = rdfnode_cmp_for_aggregate( (rdfnode *)input_node, (rdfnode *)aggstate->rdfnode_value); if (cmp > 0) { oldcontext = MemoryContextSwitchTo(aggcontext); pfree(aggstate->rdfnode_value); aggstate->rdfnode_value = (text *)PG_DETOAST_DATUM_COPY( PointerGetDatum(input_node)); MemoryContextSwitchTo(oldcontext); } } /* rank_in < rank_cur: keep current (higher category) */ } PG_RETURN_POINTER(aggstate); } /* * max_rdfnode_finalfunc * --------------------- * Final function for MAX(rdfnode). * Returns the maximum rdfnode value stored as text, or NULL if no values were aggregated. * * Note: NULL state handling is done by the wrapper in rdf_fdw.c */ Datum max_rdfnode_finalfunc(PG_FUNCTION_ARGS) { RdfnodeAggState *aggstate; /* Get the state (already validated as non-NULL by wrapper) */ aggstate = (RdfnodeAggState *)PG_GETARG_POINTER(0); if (aggstate == NULL || aggstate->rdfnode_value == NULL) PG_RETURN_NULL(); /* Return the stored maximum rdfnode */ PG_RETURN_TEXT_P(aggstate->rdfnode_value); } /* * sample_rdfnode_sfunc * -------------------- * Aggregate transition function for SAMPLE(rdfnode). * Returns an arbitrary value from the aggregate group. * * Per SPARQL 1.1 Section 18.5.1.8, SAMPLE returns an "arbitrary value" * from the multiset passed to it. The spec explicitly states the result * is non-deterministic. * * This implementation follows the common industry practice of returning * the first non-NULL value encountered. While deterministic, this is * acceptable as the spec allows implementation-defined behavior for * "arbitrary". */ Datum sample_rdfnode_sfunc(PG_FUNCTION_ARGS) { RdfnodeAggState *aggstate; MemoryContext aggcontext; MemoryContext oldcontext; text *input_node; /* Get the aggregate memory context */ AggCheckCallContext(fcinfo, &aggcontext); /* Get current state (NULL on first call) */ if (PG_ARGISNULL(0)) aggstate = NULL; else aggstate = (RdfnodeAggState *)PG_GETARG_POINTER(0); /* Skip NULL input values */ if (PG_ARGISNULL(1)) { if (aggstate == NULL) PG_RETURN_NULL(); PG_RETURN_POINTER(aggstate); } /* If we already have a value, keep it (first value wins) */ if (aggstate != NULL) PG_RETURN_POINTER(aggstate); /* Get the input rdfnode */ input_node = PG_GETARG_TEXT_PP(1); /* First non-NULL value: allocate state and store it */ oldcontext = MemoryContextSwitchTo(aggcontext); aggstate = (RdfnodeAggState *)palloc(sizeof(RdfnodeAggState)); aggstate->rdfnode_value = (text *)PG_DETOAST_DATUM_COPY(PointerGetDatum(input_node)); MemoryContextSwitchTo(oldcontext); PG_RETURN_POINTER(aggstate); } /* * sample_rdfnode_finalfunc * ------------------------ * Final function for SAMPLE(rdfnode). * Returns the arbitrary value stored (first non-NULL * value encountered). * * Note: NULL state handling is done by the wrapper in * rdf_fdw.c */ Datum sample_rdfnode_finalfunc(PG_FUNCTION_ARGS) { RdfnodeAggState *aggstate; /* Get the state (already validated as non-NULL by wrapper) */ aggstate = (RdfnodeAggState *)PG_GETARG_POINTER(0); if (aggstate == NULL || aggstate->rdfnode_value == NULL) PG_RETURN_NULL(); /* Return the stored sample value */ PG_RETURN_TEXT_P(aggstate->rdfnode_value); } /* * group_concat_sfunc * ------------------ * Transition function for GROUP_CONCAT(rdfnode [, separator]). * * Accumulates string representations of RDF terms, separated by a * delimiter. Per SPARQL 1.1 Section 18.5.1.7, the default separator * is a single space character. * * RDF term serialization follows SPARQL rules: * - Typed literals: extract lexical value only (strip ^^datatype) * - Language-tagged: extract lexical value only (strip @lang) * - IRIs: use URI string (strip angle brackets) * - Plain literals: use as-is * * NULL/unbound values are skipped during aggregation. */ Datum group_concat_sfunc(PG_FUNCTION_ARGS) { MemoryContext aggcontext; MemoryContext oldcontext; RdfnodeAggState *aggstate; text *input_node; rdfnode_info parsed; char *str_value; if (!AggCheckCallContext(fcinfo, &aggcontext)) ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("aggregate function called in non-aggregate context"))); /* Get the current state */ aggstate = PG_ARGISNULL(0) ? NULL : (RdfnodeAggState *)PG_GETARG_POINTER(0); /* Skip NULL input values */ if (PG_ARGISNULL(1)) { if (aggstate == NULL) PG_RETURN_NULL(); PG_RETURN_POINTER(aggstate); } /* Get the input rdfnode */ input_node = PG_GETARG_TEXT_PP(1); parsed = parse_rdfnode((rdfnode *)input_node); /* Extract lexical value based on RDF term type */ if (parsed.isIRI) { /* For IRIs, remove angle brackets: → http://example.org */ size_t len = strlen(parsed.raw); if (len > 2 && parsed.raw[0] == '<' && parsed.raw[len - 1] == '>') { str_value = palloc(len - 1); memcpy(str_value, parsed.raw + 1, len - 2); str_value[len - 2] = '\0'; } else { str_value = pstrdup(parsed.raw); } } else { /* For literals, use the lexical value (already extracted by parse_rdfnode) */ str_value = parsed.lex; } /* Initialize state on first value */ if (aggstate == NULL) { oldcontext = MemoryContextSwitchTo(aggcontext); aggstate = (RdfnodeAggState *)palloc(sizeof(RdfnodeAggState)); aggstate->result_str = makeStringInfo(); /* Get separator (arg 2), default to space if not provided */ if (PG_NARGS() > 2 && !PG_ARGISNULL(2)) { /* Copy the separator into aggregate memory context */ aggstate->separator = PG_GETARG_TEXT_P_COPY(2); } else aggstate->separator = cstring_to_text(" "); /* SPARQL 1.1 default */ aggstate->has_input = false; MemoryContextSwitchTo(oldcontext); } /* Add separator if not the first value */ oldcontext = MemoryContextSwitchTo(aggcontext); if (aggstate->has_input) { appendStringInfoString(aggstate->result_str, text_to_cstring(aggstate->separator)); } /* Append the string value */ appendStringInfoString(aggstate->result_str, str_value); aggstate->has_input = true; MemoryContextSwitchTo(oldcontext); PG_RETURN_POINTER(aggstate); } /* * group_concat_finalfunc * ---------------------- * Final function for GROUP_CONCAT(rdfnode [, separator]). * * Returns the concatenated string as a simple literal (plain literal * without datatype or language tag), matching SPARQL 1.1 semantics. * Returns empty string for empty result sets (per SPARQL 1.1). * * Note: NULL state handling is done by the wrapper in rdf_fdw.c */ Datum group_concat_finalfunc(PG_FUNCTION_ARGS) { RdfnodeAggState *aggstate; char *literal; text *result; /* Get the state (already validated as non-NULL by wrapper) */ aggstate = (RdfnodeAggState *)PG_GETARG_POINTER(0); if (aggstate == NULL || aggstate->result_str == NULL) { /* No input values: return empty simple literal */ result = cstring_to_text(cstring_to_rdfliteral("")); PG_RETURN_TEXT_P(result); } /* Convert to simple literal (plain literal without datatype) */ literal = cstring_to_rdfliteral(aggstate->result_str->data); /* Return as rdfnode (text type) */ result = cstring_to_text(literal); PG_RETURN_TEXT_P(result); }