modules/rdf/sparql.zzm

rdf-0.0.3 source code

=encoding utf8

=head1 NAME

rdf/sparql - SPARQL query execution.

=head1 SYNOPSIS

  from rdf/sparql import sparql_query, sparql_update;
  
  let rows := sparql_query(store, """
  SELECT ?s WHERE { ?s ?p ?o }
  """);
  
  sparql_update(store, """
  INSERT DATA { <s> <p> "o" . }
  """);


=head1 DESCRIPTION

This module parses, validates, executes, prepares, and diagnoses SPARQL
1.1 Query and Update strings over C<RDFStore>. Query results are returned
as dictionaries: C<select> results include C<variables> and C<results>,
C<ask> results include C<boolean>, and graph results include C<quads>.
Update execution returns a dictionary describing the update operations
performed.

=head1 EXPORTS

=head2 Classes

=over

=item C<SPARQLPreparedQuery>

Construct with C<source>. The query is parsed during construction and
available from C<get_ast>.

=over

=item C<< execute(RDFStore store, inherited_dataset := null) >>

Executes the prepared query against C<store>.

=back

=item C<SPARQLPreparedUpdate>

Construct with C<source>. The update is parsed during construction and
available from C<get_ast>.

=over

=item C<< execute(RDFStore store) >>

Executes the prepared update against C<store>.

=back

=back

=head2 Functions

=over

=item C<< sparql_parse(String query) >>

Parses SPARQL Query or Update syntax and returns an AST dictionary.

=item C<< sparql_parse_query(String query) >>

Alias for parsing query syntax retained for callers that want a query
entry point.

=item C<< sparql_prepare_query(String query) >>

Returns a C<SPARQLPreparedQuery>, throwing C<SPARQLError> if the source is
an update or invalid syntax.

=item C<< sparql_prepare_update(String update) >>

Returns a C<SPARQLPreparedUpdate>, throwing C<SPARQLError> if the source
is not an update or has invalid syntax.

=item C<< sparql_validate(String source) >>

Returns a validation dictionary with C<ok>, C<type>, C<syntax>, and
C<error>.

=item C<< sparql_diagnose(RDFStore store, String source) >>

Validates then executes C<source>, returning validation information plus a
C<result> on success or C<error> on failure.

=item C<< sparql_query(RDFStore store, String query, inherited_dataset := null) >>

Executes a SPARQL Query string over C<store>. Throws if given Update
syntax.

=item C<< sparql_update(RDFStore store, String update) >>

Executes a SPARQL Update string over C<store>. Throws if given Query
syntax.

=back

=head1 COPYRIGHT AND LICENCE

B<< rdf/sparql >> is copyright Toby Inkster.

It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.

=cut

from rdf/store import RDFStore;
from rdf/term import
	RDFBlank,
	RDFDefaultGraph,
	RDFIRI,
	RDFLiteral,
	RDF_NS,
	SPARQLError,
	XSD_NS,
	rdf_blank,
	rdf_default_graph,
	rdf_iri,
	rdf_literal,
	rdf_quad,
	rdf_term_equals,
	rdf_term_key;
from rdf/parser/common import RDFReader;
from rdf/parser/nquads import NQuadsParser;
from rdf/parser/ntriples import NTriplesParser;
from rdf/parser/rdfxml import RdfXmlParser;
from rdf/parser/turtle import TurtleParser;
from rdf/sparql/parser import sparql_parse_ast;
from std/data/json import JSON;
from std/digest/md5 import md5_hex;
from std/digest/sha import sha1_hex, sha256_hex, sha384_hex, sha512_hex;
from std/io import Path;
from std/internals import to_Regexp_with_flags;
from std/math import Math;
from std/net/http import UserAgent;
from std/net/url import escape as _url_escape, unescape as _url_unescape;
from std/string import contains, ends_with, index, join, replace, rindex, split, sprint, starts_with, substr, trim;
from std/time import Time, TimeZone;
from std/uuid import create_uuid;

function _sparql_eval_patterns_from;
function _sparql_filter_ok;
function _sparql_matching_paren;
function _sparql_outer_parens;
function _sparql_parse;
function _sparql_eval_group_text;
function _sparql_apply_values;
function _sparql_path_endpoints;
function _sparql_path_reverse_endpoints;
function _sparql_parse_aggregate;
function _binding_key;
function _sparql_is_term;
function _sparql_is_numeric_literal;
function _sparql_literal_datatype;
function _sparql_is_string_literal;
function _sparql_eval_expr;
function _sparql_merge_binding;
function _sparql_prefix_text;
function _sparql_decimal_lexical;
function _sparql_double_lexical;
function sparql_query;
function sparql_update;

let _sparql_bnode_counter := 0;

function _sparql_is_iri_start ( String text, Number pos ) {
	return false unless substr( text, pos, 1 ) eq "<";
	let i := pos + 1;
	return false if i >= length text;
	// "<=" is always the comparison operator; "=" later on may appear
	// inside an IRI query string.
	return false if substr( text, i, 1 ) eq "=";
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		return true if ch eq ">";
		return false if ch eq "\"" or ch ~ /\s/;
		i++;
	}
	return false;
}

function _sparql_strip_comments ( String text ) {
	let out := "";
	let quoted := false;
	let iri := false;
	let i := 0;
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		if ( quoted ) {
			out _= ch;
			if ( ch eq "\"" and substr( text, i - 1, 1 ) ne "\\" ) {
				quoted := false;
			}
			i++;
			next;
		}
		if ( iri ) {
			out _= ch;
			iri := false if ch eq ">";
			i++;
			next;
		}
		if ( ch eq "\"" ) {
			quoted := true;
			out _= ch;
			i++;
			next;
		}
		if ( _sparql_is_iri_start( text, i ) ) {
			iri := true;
			out _= ch;
			i++;
			next;
		}
		if ( ch eq "#" ) {
			while ( i < length text and substr( text, i, 1 ) ne "\n" ) {
				i++;
			}
			next;
		}
		out _= ch;
		i++;
	}
	return out;
}

function _sparql_validate_balanced ( String text ) {
	let quoted := false;
	let iri := false;
	let braces := 0;
	let parens := 0;
	let brackets := 0;
	let i := 0;
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		if ( quoted ) {
			if ( ch eq "\"" and substr( text, i - 1, 1 ) ne "\\" ) {
				quoted := false;
			}
			i++;
			next;
		}
		if ( iri ) {
			iri := false if ch eq ">";
			i++;
			next;
		}
		if ( ch eq "\"" ) {
			quoted := true;
		}
		else if ( _sparql_is_iri_start( text, i ) ) {
			iri := true;
		}
		else if ( ch eq "{" ) {
			braces++;
		}
		else if ( ch eq "}" ) {
			braces--;
		}
		else if ( ch eq "(" ) {
			parens++;
		}
		else if ( ch eq ")" ) {
			parens--;
		}
		else if ( ch eq "[" ) {
			brackets++;
		}
		else if ( ch eq "]" ) {
			brackets--;
		}
		throw new SPARQLError(message: "SPARQL syntax: unbalanced delimiters")
			if braces < 0 or parens < 0 or brackets < 0;
		i++;
	}
	throw new SPARQLError(message: "SPARQL syntax: unterminated string")
		if quoted;
	throw new SPARQLError(message: "SPARQL syntax: unterminated IRI")
		if iri;
	throw new SPARQLError(message: "SPARQL syntax: unbalanced delimiters")
		if braces != 0 or parens != 0 or brackets != 0;
}

function _sparql_matching_brace ( String text, Number open ) {
	let quoted := false;
	let iri := false;
	let depth := 0;
	let i := open;
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		if ( quoted ) {
			if ( ch eq "\"" and substr( text, i - 1, 1 ) ne "\\" ) {
				quoted := false;
			}
			i++;
			next;
		}
		if ( iri ) {
			iri := false if ch eq ">";
			i++;
			next;
		}
		if ( ch eq "\"" ) {
			quoted := true;
		}
		else if ( _sparql_is_iri_start( text, i ) ) {
			iri := true;
		}
		else if ( ch eq "{" ) {
			depth++;
		}
		else if ( ch eq "}" ) {
			depth--;
			return i if depth == 0;
		}
		i++;
	}
	return -1;
}

function _sparql_validate_syntax ( String query ) {
	return sparql_parse_ast(query){type};
}

function sparql_parse ( String query ) {
	return sparql_parse_ast(query);
}

function sparql_parse_query ( String query ) {
	return sparql_parse(query);
}

function _sparql_prefixes ( String text ) {
	let prefixes := {};
	let lines := split( _sparql_strip_comments(text), "\n" );
	let body := [];
	for ( let line in lines ) {
		let clean := trim(line);
		if ( clean ~ /^BASE\s+<([^>]+)>\s*([\s\S]*)$/i ) {
			let m := clean ~ /^BASE\s+<([^>]+)>\s*([\s\S]*)$/i;
			prefixes.set( "__base", m[1] );
			body.push(m[2]) if trim(m[2]) ne "";
		}
		else if ( clean ~ /^PREFIX\s+([A-Za-z][A-Za-z0-9_-]*|):\s*<([^>]+)>\s*([\s\S]*)$/i ) {
			let m := clean ~ /^PREFIX\s+([A-Za-z][A-Za-z0-9_-]*|):\s*<([^>]+)>\s*([\s\S]*)$/i;
			prefixes.set( m[1], m[2] );
			body.push(m[3]) if trim(m[3]) ne "";
		}
		else {
			// Keep blank lines: they may be significant inside
			// long-quoted literals.
			body.push(line);
		}
	}
	return { prefixes: prefixes, body: join( "\n", body ) };
}

function _sparql_resolve_iri ( String iri, Dict prefixes ) {
	return iri if iri ~ /^[A-Za-z][A-Za-z0-9+.-]*:/;
	let base := prefixes.get( "__base", "" );
	return iri if base eq "";
	return ( new RDFReader(source: "") ).set_base(base).resolve_iri(iri);
}

function _sparql_expand ( String token, Dict prefixes ) {
	if ( starts_with( token, "?" ) ) {
		return { variable: substr( token, 1 ) };
	}
	if ( token eq "[]" ) {
		return { wildcard: true };
	}
	if ( starts_with( token, "<" ) and substr( token, length token - 1, 1 ) eq ">" ) {
		return rdf_iri(_sparql_resolve_iri(
			substr( token, 1, length token - 2 ),
			prefixes,
		));
	}
	if ( starts_with( token, "\"" ) ) {
		return ( new RDFReader(source: token, prefixes: prefixes) ).read_literal();
	}
	if ( lc(token) eq "true" or lc(token) eq "false" ) {
		return rdf_literal( lc(token), "", rdf_iri(XSD_NS _ "boolean") );
	}
	if ( token ~ /^[+-]?[0-9]+$/ ) {
		return rdf_literal( token, "", rdf_iri(XSD_NS _ "integer") );
	}
	if ( token ~ /^[+-]?[0-9]*\.[0-9]+$/ ) {
		return rdf_literal( token, "", rdf_iri(XSD_NS _ "decimal") );
	}
	if ( token eq "a" ) {
		return rdf_iri(RDF_NS _ "type");
	}
	if ( starts_with( token, "_:" ) ) {
		// Blank node labels in query patterns behave as
		// non-selectable variables, not concrete terms.
		return { variable: "__bnode_" _ substr( token, 2 ) };
	}
	// index/substr rather than a limit-2 split: zuzu-js truncates the
	// tail, breaking local names that contain ":" themselves.
	let colon := index( token, ":" );
	if ( colon >= 0 and prefixes.exists(substr( token, 0, colon )) ) {
		return rdf_iri(
			prefixes.get(substr( token, 0, colon )) _ substr( token, colon + 1 ),
		);
	}
	throw new SPARQLError(message: "SPARQL token not understood: " _ token);
}

function _sparql_split_top ( String text, String sep ) {
	let parts := [];
	let token := "";
	let iri := false;
	let depth := 0;
	let i := 0;
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		if ( iri ) {
			token _= ch;
			iri := false if ch eq ">";
			i++;
			next;
		}
		if ( _sparql_is_iri_start( text, i ) ) {
			iri := true;
			token _= ch;
			i++;
			next;
		}
		if ( ch eq "(" ) {
			depth++;
		}
		else if ( ch eq ")" ) {
			depth--;
		}
		if ( depth == 0 and ch eq sep ) {
			parts.push(token);
			token := "";
			i++;
			next;
		}
		token _= ch;
		i++;
	}
	parts.push(token);
	return parts;
}

function _sparql_path_outer_parens ( String text ) {
	let clean := trim(text);
	while ( true ) {
		let next_clean := trim(_sparql_outer_parens(clean));
		return clean if next_clean eq clean;
		clean := next_clean;
	}
}

function _sparql_path_postfix ( String text ) {
	let clean := trim(text);
	let final_char := substr( clean, length clean - 1, 1 );
	if ( final_char eq "*" or final_char eq "+" or final_char eq "?" ) {
		return {
			op: final_char,
			body: substr( clean, 0, length clean - 1 ),
		};
	}
	if ( clean ~ /^([\s\S]+)\{([0-9]+)(,([0-9]*))?\}$/ ) {
		let m := clean ~ /^([\s\S]+)\{([0-9]+)(,([0-9]*))?\}$/;
		return {
			op: "{}",
			body: m[1],
			min: int(m[2]),
			max: m[4] eq "" ? null : int(m[4]),
		};
	}
	return null;
}

function _sparql_parse_path_set_item ( String raw, Dict prefixes ) {
	let clean := trim(raw);
	if ( starts_with( clean, "^" ) ) {
		return {
			inverse: true,
			term: _sparql_expand(substr( clean, 1 ), prefixes),
		};
	}
	return {
		inverse: false,
		term: _sparql_expand(clean, prefixes),
	};
}

function _sparql_parse_path ( String raw, Dict prefixes ) {
	let clean := trim(_sparql_path_outer_parens(raw));
	let alternatives := _sparql_split_top( clean, "|" );
	if ( alternatives.length() > 1 ) {
		let items := [];
		for ( let item in alternatives ) {
			items.push(_sparql_parse_path( item, prefixes ));
		}
		return { path: "alt", items: items };
	}
	let sequence := _sparql_split_top( clean, "/" );
	if ( sequence.length() > 1 ) {
		let items := [];
		for ( let item in sequence ) {
			items.push(_sparql_parse_path( item, prefixes ));
		}
		return { path: "seq", items: items };
	}
	let post := _sparql_path_postfix(clean);
	if ( not (post == null) ) {
		return {
			path: post{op},
			item: _sparql_parse_path( post{body}, prefixes ),
			min: post.get( "min", null ),
			max: post.get( "max", null ),
		};
	}
	if ( starts_with( clean, "^" ) ) {
		return {
			path: "inv",
			item: _sparql_parse_path( substr( clean, 1 ), prefixes ),
		};
	}
	if ( starts_with( clean, "!" ) ) {
		let body := trim(substr( clean, 1 ));
		body := _sparql_path_outer_parens(body);
		let items := [];
		for ( let item in _sparql_split_top( body, "|" ) ) {
			items.push(_sparql_parse_path_set_item( item, prefixes ));
		}
		return { path: "neg", items: items };
	}
	return _sparql_expand(clean, prefixes);
}

function _sparql_parse_predicate ( String token, Dict prefixes ) {
	return _sparql_expand(token, prefixes) if starts_with( token, "?" );
	return _sparql_parse_path( token, prefixes )
		if contains( token, "/" ) or contains( token, "|" ) or
		contains( token, "^" ) or contains( token, "!" ) or
		contains( token, "*" ) or contains( token, "+" ) or
		contains( token, "?" ) or contains( token, "{" );
	return _sparql_expand(token, prefixes);
}

function _sparql_graph_term ( graph ) {
	return graph == null ? rdf_default_graph() : graph;
}

function _sparql_parse_term_at (
	Array raw,
	Number i,
	Dict prefixes,
	graph,
	Number anon_index,
	Array patterns
) {
	let token := raw[i];
	let g := _sparql_graph_term(graph);
	let pos := i;
	let next_anon := anon_index;
	if ( token eq "(" ) {
		if ( i + 1 < raw.length() and raw[i + 1] eq ")" ) {
			return {
				spec: rdf_iri(RDF_NS _ "nil"),
				next: i + 2,
				anon_index: anon_index,
			};
		}
		let head := { variable: "__anon" _ next_anon };
		next_anon++;
		let current := head;
		pos++;
		while ( pos < raw.length() and raw[pos] ne ")" ) {
			let item := _sparql_parse_term_at(
				raw,
				pos,
				prefixes,
				graph,
				next_anon,
				patterns,
			);
			next_anon := item{anon_index};
			pos := item{next};
			let rest := pos < raw.length() and raw[pos] eq ")"
				? rdf_iri(RDF_NS _ "nil")
				: { variable: "__anon" _ next_anon };
			next_anon++ unless rest instanceof RDFIRI;
			patterns.push([
				current,
				rdf_iri(RDF_NS _ "first"),
				item{spec},
				g,
			]);
			patterns.push([
				current,
				rdf_iri(RDF_NS _ "rest"),
				rest,
				g,
			]);
			current := rest;
		}
		throw new SPARQLError(message: "SPARQL RDF collection expected")
			if pos >= raw.length();
		return { spec: head, next: pos + 1, anon_index: next_anon };
	}
	if ( token eq "[" ) {
		let anon := { variable: "__anon" _ next_anon };
		next_anon++;
		pos++;
		if ( pos < raw.length() and raw[pos] eq "]" ) {
			return { spec: anon, next: pos + 1, anon_index: next_anon };
		}
		while ( pos < raw.length() and raw[pos] ne "]" ) {
			let predicate := raw[pos];
			pos++;
			throw new SPARQLError(message: "SPARQL blank node object expected")
				if pos >= raw.length();
			while ( pos < raw.length() and raw[pos] ne ";" and raw[pos] ne "]" ) {
				let item := _sparql_parse_term_at(
					raw,
					pos,
					prefixes,
					graph,
					next_anon,
					patterns,
				);
				next_anon := item{anon_index};
				pos := item{next};
				patterns.push([
					anon,
					_sparql_parse_predicate(predicate, prefixes),
					item{spec},
					g,
				]);
				if ( pos < raw.length() and raw[pos] eq "," ) {
					pos++;
					next;
				}
				last;
			}
			pos++ if pos < raw.length() and raw[pos] eq ";";
		}
		throw new SPARQLError(message: "SPARQL blank node list expected")
			if pos >= raw.length();
		return { spec: anon, next: pos + 1, anon_index: next_anon };
	}
	return {
		spec: _sparql_expand(token, prefixes),
		next: i + 1,
		anon_index: anon_index,
	};
}

function _sparql_tokenize_patterns ( String where ) {
	let out := [];
	let token := "";
	let quoted := false;
	let iri := false;
	let i := 0;
	while ( i < length where ) {
		let ch := substr( where, i, 1 );
		if ( quoted ) {
			token _= ch;
			if ( ch eq "\"" and substr( where, i - 1, 1 ) ne "\\" ) {
				quoted := false;
			}
			i++;
			next;
		}
		if ( iri ) {
			token _= ch;
			if ( ch eq ">" ) {
				iri := false;
			}
			i++;
			next;
		}
		if ( ch eq "\"" ) {
			if ( substr( where, i, 3 ) eq "\"\"\"" ) {
				// Long string literal: normalize to a short string token.
				let close := index( substr( where, i + 3 ), "\"\"\"" );
				if ( close >= 0 ) {
					let content := substr( where, i + 3, close );
					content := replace( content, "\\\\", "\\\\", "g" );
					content := replace( content, "\"", "\\\"", "g" );
					content := replace( content, "\n", "\\n", "g" );
					content := replace( content, "\r", "\\r", "g" );
					content := replace( content, "\t", "\\t", "g" );
					token _= "\"" _ content _ "\"";
					i := i + 3 + close + 3;
					next;
				}
			}
			token _= ch;
			quoted := true;
			i++;
			next;
		}
		if ( _sparql_is_iri_start( where, i ) ) {
			token _= ch;
			iri := true;
			i++;
			next;
		}
		if ( ch ~ /\s/ ) {
			if ( token ne "" ) {
				out.push(token);
				token := "";
			}
			i++;
			next;
		}
		if ( ch eq "(" and token eq "" and
			substr( where, i + 1, 1 ) ~ /[\?\x22_\[0-9\)<]/
		) {
			out.push(ch);
			i++;
			next;
		}
		if ( ch eq ")" and token ne "" and not contains( token, "(" ) ) {
			out.push(token);
			token := "";
			out.push(ch);
			i++;
			next;
		}
		if ( ch eq ")" and token eq "" ) {
			out.push(ch);
			i++;
			next;
		}
		if ( ch eq "." or ch eq ";" or ch eq "," or ch eq "[" or ch eq "]" ) {
			if ( token ne "" ) {
				out.push(token);
				token := "";
			}
			out.push(ch);
			i++;
			next;
		}
		token _= ch;
		i++;
	}
	out.push(token) if token ne "";
	return out;
}

function _sparql_parse_patterns ( String text, Dict prefixes, graph := null ) {
	let patterns := [];
	let raw := _sparql_tokenize_patterns(text);
	let i := 0;
	let anon_index := 0;
	let subject := null;
	let predicate := null;
	while ( i < raw.length() ) {
		if ( raw[i] eq "." ) {
			subject := null;
			predicate := null;
			i++;
			next;
		}
		if ( raw[i] eq ";" ) {
			predicate := null;
			i++;
			next;
		}
		if ( raw[i] eq "," ) {
			i++;
			next;
		}
		if ( subject == null ) {
			let subject_item := _sparql_parse_term_at(
				raw,
				i,
				prefixes,
				graph,
				anon_index,
				patterns,
			);
			subject := subject_item{spec};
			anon_index := subject_item{anon_index};
			i := subject_item{next};
			if ( i >= raw.length() or raw[i] eq "." ) {
				// A blank node property list or collection may stand
				// alone as a complete triples block.
				subject := null;
				i++ if i < raw.length();
				next;
			}
			predicate := raw[i];
			i++;
		}
		else if ( predicate == null ) {
			throw new SPARQLError(message: "SPARQL predicate expected")
				if i >= raw.length();
			predicate := raw[i];
			i++;
		}
		throw new SPARQLError(message: "SPARQL object expected")
			if i >= raw.length();
		let object_item := _sparql_parse_term_at(
			raw,
			i,
			prefixes,
			graph,
			anon_index,
			patterns,
		);
		let object_spec := object_item{spec};
		anon_index := object_item{anon_index};
		i := object_item{next};
		patterns.push([
			subject,
			_sparql_parse_predicate(predicate, prefixes),
			object_spec,
			_sparql_graph_term(graph),
		]);
		if ( i < raw.length() and raw[i] eq "." ) {
			subject := null;
			predicate := null;
		}
	}
	return patterns;
}

function _sparql_value_tokens ( String text ) {
	let out := [];
	let token := "";
	let quoted := false;
	let iri := false;
	let i := 0;
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		if ( quoted ) {
			token _= ch;
			if ( ch eq "\"" and substr( text, i - 1, 1 ) ne "\\" ) {
				quoted := false;
			}
			i++;
			next;
		}
		if ( iri ) {
			token _= ch;
			iri := false if ch eq ">";
			i++;
			next;
		}
		if ( ch eq "\"" ) {
			token _= ch;
			quoted := true;
			i++;
			next;
		}
		if ( _sparql_is_iri_start( text, i ) ) {
			token _= ch;
			iri := true;
			i++;
			next;
		}
		if ( ch ~ /\s/ ) {
			if ( token ne "" ) {
				out.push(token);
				token := "";
			}
			i++;
			next;
		}
		if ( ch eq "(" or ch eq ")" ) {
			if ( token ne "" ) {
				out.push(token);
				token := "";
			}
			out.push(ch);
			i++;
			next;
		}
		token _= ch;
		i++;
	}
	out.push(token) if token ne "";
	return out;
}

function _sparql_parse_values_terms ( String text, Dict prefixes ) {
	let terms := [];
	for ( let token in _sparql_value_tokens(text) ) {
		next if token eq "(" or token eq ")";
		terms.push(token eq "UNDEF" ? null : _sparql_expand(token, prefixes));
	}
	return terms;
}

function _sparql_parse_values_blocks ( String text, Dict prefixes ) {
	let blocks := [];
	let rest := text;
	while ( rest ~ /VALUES\s+(\?[A-Za-z][A-Za-z0-9_]*)\s*\{([^{}]*)\}/i ) {
		let m := rest ~ /VALUES\s+(\?[A-Za-z][A-Za-z0-9_]*)\s*\{([^{}]*)\}/i;
		let var_name := substr( m[1], 1 );
		let rows := [];
		for ( let term in _sparql_parse_values_terms( m[2], prefixes ) ) {
			let row := {};
			row.set( var_name, term ) if not (term == null);
			rows.push(row);
		}
		blocks.push({ vars: [ var_name ], rows: rows });
		rest := replace( rest, /VALUES\s+\?[A-Za-z][A-Za-z0-9_]*\s*\{[^{}]*\}/i, "", "" );
	}
	rest := text;
	while ( rest ~ /VALUES\s*\(([^)]*)\)\s*\{([^{}]*)\}/i ) {
		let m := rest ~ /VALUES\s*\(([^)]*)\)\s*\{([^{}]*)\}/i;
		let vars := [];
		for ( let var_token in _sparql_value_tokens(m[1]) ) {
			vars.push(substr( var_token, 1 )) if starts_with( var_token, "?" );
		}
		let rows := [];
		let tokens := _sparql_value_tokens(m[2]);
		let i := 0;
		while ( i < tokens.length() ) {
			if ( tokens[i] ne "(" ) {
				i++;
				next;
			}
			i++;
			let row := {};
			let col := 0;
			while ( i < tokens.length() and tokens[i] ne ")" ) {
				if ( col < vars.length() and tokens[i] ne "UNDEF" ) {
					row.set( vars[col], _sparql_expand(tokens[i], prefixes) );
				}
				col++;
				i++;
			}
			rows.push(row);
			i++;
		}
		blocks.push({ vars: vars, rows: rows });
		rest := replace( rest, /VALUES\s*\([^)]*\)\s*\{[^{}]*\}/i, "", "" );
	}
	return blocks;
}

function _sparql_remove_values ( String text ) {
	let out := text;
	while ( out ~ /VALUES\s+(\?[A-Za-z][A-Za-z0-9_]*)\s*\{([^{}]*)\}/i ) {
		out := replace( out, /VALUES\s+\?[A-Za-z][A-Za-z0-9_]*\s*\{[^{}]*\}/i, "", "" );
	}
	while ( out ~ /VALUES\s*\(([^)]*)\)\s*\{([^{}]*)\}/i ) {
		out := replace( out, /VALUES\s*\([^)]*\)\s*\{[^{}]*\}/i, "", "" );
	}
	return out;
}

function _sparql_limit_value ( String tail, String keyword ) {
	let re := keyword eq "LIMIT"
		? /LIMIT\s+([0-9]+)/i
		: /OFFSET\s+([0-9]+)/i;
	if ( tail ~ re ) {
		let m := tail ~ re;
		return int(m[1]);
	}
	return null;
}

function _sparql_is_boundary ( String ch ) {
	return ch eq "" or ch ~ /[\s{}().;,]/;
}

function _sparql_starts_keyword ( String text, String keyword ) {
	let clean := trim(text);
	return false unless starts_with( uc(clean), keyword );
	let after := substr( clean, length keyword, 1 );
	return _sparql_is_boundary(after);
}

function _sparql_outer_parens ( String text ) {
	let clean := trim(text);
	return clean if not( starts_with( clean, "(" ) and
		substr( clean, length clean - 1, 1 ) eq ")" );
	let close := _sparql_matching_paren( clean, 0 );
	return close == length clean - 1
		? substr( clean, 1, length clean - 2 )
		: clean;
}

function _sparql_matching_paren ( String text, Number open ) {
	let quoted := false;
	let iri := false;
	let depth := 0;
	let i := open;
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		if ( quoted ) {
			quoted := false if ch eq "\"" and substr( text, i - 1, 1 ) ne "\\";
			i++;
			next;
		}
		if ( iri ) {
			iri := false if ch eq ">";
			i++;
			next;
		}
		if ( ch eq "\"" ) {
			quoted := true;
		}
		else if ( _sparql_is_iri_start( text, i ) ) {
			iri := true;
		}
		else if ( ch eq "(" ) {
			depth++;
		}
		else if ( ch eq ")" ) {
			depth--;
			return i if depth == 0;
		}
		i++;
	}
	return -1;
}

function _sparql_find_top_operator ( String text ) {
	let quoted := false;
	let iri := false;
	let parens := 0;
	let brackets := 0;
	let i := 0;
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		if ( quoted ) {
			quoted := false if ch eq "\"" and substr( text, i - 1, 1 ) ne "\\";
			i++;
			next;
		}
		if ( iri ) {
			iri := false if ch eq ">";
			i++;
			next;
		}
		if ( ch eq "\"" ) {
			quoted := true;
			i++;
			next;
		}
		if ( _sparql_is_iri_start( text, i ) ) {
			iri := true;
			i++;
			next;
		}
		parens++ if ch eq "(";
		parens-- if ch eq ")";
		brackets++ if ch eq "[";
		brackets-- if ch eq "]";
		if ( parens == 0 and brackets == 0 ) {
			return i if ch eq "{";
			let rest := substr( text, i );
			return i if _sparql_starts_keyword( rest, "OPTIONAL" ) or
					_sparql_starts_keyword( rest, "MINUS" ) or
					_sparql_starts_keyword( rest, "FILTER" ) or
					_sparql_starts_keyword( rest, "BIND" ) or
					rest ~ /^BIND\s*\(/i or
					_sparql_starts_keyword( rest, "VALUES" ) or
					_sparql_starts_keyword( rest, "SERVICE" ) or
					_sparql_starts_keyword( rest, "GRAPH" );
		}
		i++;
	}
	return -1;
}

function _sparql_extract_braced ( String text ) {
	let clean := trim(text);
	let open := index( clean, "{" );
	throw new SPARQLError(message: "SPARQL group expected") if open < 0;
	let close := _sparql_matching_brace( clean, open );
	throw new SPARQLError(message: "SPARQL group expected") if close < open;
	return {
		before: trim(substr( clean, 0, open )),
		body: substr( clean, open + 1, close - open - 1 ),
		tail: trim(substr( clean, close + 1 )),
	};
}

function _sparql_copy_binding ( binding ) {
	let out := {};
	for ( let item_key in binding.keys() ) {
		out.set( item_key, binding.get(item_key) );
	}
	return out;
}

function _sparql_compatible ( left, right ) {
	let shared := false;
	for ( let item_key in left.keys() ) {
		next unless right.exists(item_key);
		shared := true;
		return false unless rdf_term_equals( left.get(item_key), right.get(item_key) );
	}
	return shared;
}

function _sparql_numeric_value ( value ) {
	return 0 + value.get_value() if value instanceof RDFLiteral;
	return 0 + value;
}

function _sparql_safe_numeric_value ( value ) {
	try {
		return _sparql_numeric_value(value);
	}
	catch {
		return null;
	}
}

function _sparql_scalar_string ( value ) {
	return "" if value == null;
	return value.get_value()
		if value instanceof RDFIRI or value instanceof RDFBlank or
		value instanceof RDFLiteral;
	return "" _ value;
}

function _sparql_boolean_literal ( Boolean value ) {
	return rdf_literal(
		value ? "true" : "false",
		"",
		rdf_iri(XSD_NS _ "boolean"),
	);
}

function _sparql_integer_literal ( value ) {
	return rdf_literal( "" _ value, "", rdf_iri(XSD_NS _ "integer") );
}

function _sparql_decimal_expr_literal ( value ) {
	return rdf_literal( "" _ value, "", rdf_iri(XSD_NS _ "decimal") );
}

function _sparql_numeric_result_literal_like ( source, value ) {
	let datatype := _sparql_literal_datatype(source);
	if ( datatype eq XSD_NS _ "integer" or datatype eq XSD_NS _ "int" or
		datatype eq XSD_NS _ "long" or datatype eq XSD_NS _ "short" or
		datatype eq XSD_NS _ "byte"
	) {
		return rdf_literal( "" _ int(value), "", rdf_iri(datatype) );
	}
	return rdf_literal( "" _ int(value), "", rdf_iri(datatype) )
		if datatype eq XSD_NS _ "decimal";
	return rdf_literal( "" _ value, "", rdf_iri(datatype) )
		if datatype eq XSD_NS _ "double" or datatype eq XSD_NS _ "float";
	return _sparql_integer_literal(value);
}

function _sparql_effective_boolean ( value ) {
	return false if value == null;
	if ( value instanceof RDFLiteral ) {
		let text := value.get_value();
		let datatype := value.get_datatype().get_value();
		return text eq "true" or text eq "1"
			if datatype eq XSD_NS _ "boolean";
		return text ne "";
	}
	return true if value instanceof RDFIRI or value instanceof RDFBlank;
	// Raw scalars (e.g. the Boolean from BOUND): use language
	// truthiness, which works on every runtime — on zuzu.pl a bare
	// false is not `instanceof Boolean`.
	return value ? true : false;
}

function _sparql_expr_args ( String text ) {
	let args := [];
	let token := "";
	let quoted := false;
	let iri := false;
	let depth := 0;
	let i := 0;
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		if ( quoted ) {
			token _= ch;
			quoted := false if ch eq "\"" and substr( text, i - 1, 1 ) ne "\\";
			i++;
			next;
		}
		if ( iri ) {
			token _= ch;
			iri := false if ch eq ">";
			i++;
			next;
		}
		if ( ch eq "\"" ) {
			quoted := true;
			token _= ch;
			i++;
			next;
		}
		if ( _sparql_is_iri_start( text, i ) ) {
			iri := true;
			token _= ch;
			i++;
			next;
		}
		depth++ if ch eq "(";
		depth-- if ch eq ")";
		if ( depth == 0 and ch eq "," ) {
			args.push(trim(token)) if trim(token) ne "";
			token := "";
			i++;
			next;
		}
		token _= ch;
		i++;
	}
	args.push(trim(token)) if trim(token) ne "";
	return args;
}

function _sparql_operator_allowed ( String text, Number pos, String op ) {
	return true unless op eq "+" or op eq "-";
	let i := pos - 1;
	while ( i >= 0 and substr( text, i, 1 ) ~ /\s/ ) {
		i--;
	}
	return false if i < 0;
	let before := substr( text, i, 1 );
	return false if before eq "(" or before eq "," or before eq "+" or
		before eq "-" or before eq "*" or before eq "/" or before eq "=" or
		before eq "<" or before eq ">";
	return true;
}

function _sparql_find_top_expr_operator ( String text, Array ops ) {
	let quoted := false;
	let iri := false;
	let depth := 0;
	let found := null;
	let i := 0;
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		if ( quoted ) {
			quoted := false if ch eq "\"" and substr( text, i - 1, 1 ) ne "\\";
			i++;
			next;
		}
		if ( iri ) {
			iri := false if ch eq ">";
			i++;
			next;
		}
		if ( ch eq "\"" ) {
			quoted := true;
			i++;
			next;
		}
		if ( _sparql_is_iri_start( text, i ) ) {
			iri := true;
			i++;
			next;
		}
		depth++ if ch eq "(";
		depth-- if ch eq ")";
		if ( depth == 0 ) {
			let matched := false;
			for ( let op in ops ) {
				if ( substr( text, i, length op ) eq op and
					_sparql_operator_allowed( text, i, op )
				) {
					found := { op: op, pos: i };
					matched := true;
					last;
				}
			}
			if ( matched ) {
				i += length found{op};
				next;
			}
		}
		i++;
	}
	return found;
}

function _sparql_same_value ( left, right ) {
	return false if left == null or right == null;
	return rdf_term_equals( left, right )
		if _sparql_is_term(left) and _sparql_is_term(right);
	return _sparql_scalar_string(left) eq _sparql_scalar_string(right);
}

function _sparql_order_compare ( left, right ) {
	if ( _sparql_is_numeric_literal(left) and _sparql_is_numeric_literal(right) ) {
		let l := _sparql_safe_numeric_value(left);
		let r := _sparql_safe_numeric_value(right);
		return 0 if l == null or r == null;
		return -1 if l < r;
		return 1 if l > r;
		return 0;
	}
	return _sparql_scalar_string(left) cmp _sparql_scalar_string(right);
}

function _sparql_numeric_expr_literal ( left, right, String op ) {
	return null unless _sparql_is_numeric_literal(left) and
		_sparql_is_numeric_literal(right);
	let l := _sparql_safe_numeric_value(left);
	let r := _sparql_safe_numeric_value(right);
	return null if l == null or r == null;
	if ( op eq "+" ) {
		return _sparql_integer_literal(l + r);
	}
	if ( op eq "-" ) {
		return _sparql_integer_literal(l - r);
	}
	if ( op eq "*" ) {
		return _sparql_integer_literal(l * r);
	}
	return null if r == 0;
	return _sparql_decimal_expr_literal(l / r);
}

function _sparql_function_name ( String raw, Dict prefixes ) {
	if ( contains( raw, ":" ) ) {
		let term := _sparql_expand(raw, prefixes);
		return term.get_value() if term instanceof RDFIRI;
	}
	return uc(raw);
}

function _sparql_literal_datatype ( value ) {
	return "" unless value instanceof RDFLiteral;
	return value.get_datatype().get_value();
}

function _sparql_is_numeric_datatype ( String datatype ) {
	return datatype eq XSD_NS _ "integer" or datatype eq XSD_NS _ "int" or
		datatype eq XSD_NS _ "long" or datatype eq XSD_NS _ "short" or
		datatype eq XSD_NS _ "byte" or datatype eq XSD_NS _ "decimal" or
		datatype eq XSD_NS _ "double" or datatype eq XSD_NS _ "float";
}

function _sparql_is_numeric_literal ( value ) {
	return false unless value instanceof RDFLiteral;
	return _sparql_is_numeric_datatype(value.get_datatype().get_value());
}

function _sparql_source_datatype ( value ) {
	return "" unless value instanceof RDFLiteral;
	return value.get_datatype().get_value();
}

function _sparql_cast_is_string_source ( value ) {
	return false unless value instanceof RDFLiteral;
	let datatype := value.get_datatype().get_value();
	return datatype eq XSD_NS _ "string" or datatype eq RDF_NS _ "langString";
}

function _sparql_cast_decimal_lexical ( String lexical, Boolean force_fraction ) {
	let text := lexical;
	let sign := "";
	if ( starts_with( text, "+" ) ) {
		text := substr( text, 1 );
	}
	else if ( starts_with( text, "-" ) ) {
		sign := "-";
		text := substr( text, 1 );
	}
	let int_part := text;
	let frac_part := "";
	if ( contains( text, "." ) ) {
		let parts := split( text, ".", 2 );
		int_part := parts[0] eq "" ? "0" : parts[0];
		frac_part := parts[1];
	}
	while ( length int_part > 1 and starts_with( int_part, "0" ) ) {
		int_part := substr( int_part, 1 );
	}
	while ( length frac_part > ( force_fraction ? 1 : 0 ) and
		ends_with( frac_part, "0" )
	) {
		frac_part := substr( frac_part, 0, length(frac_part) - 1 );
	}
	let out := sign _ int_part;
	if ( frac_part ne "" ) {
		out _= "." _ frac_part;
	}
	else if ( force_fraction ) {
		out _= ".0";
	}
	return out;
}

function _sparql_cast_decimal_number_lexical ( value, Boolean force_fraction ) {
	return _sparql_cast_decimal_lexical(
		_sparql_decimal_lexical( value, force_fraction ),
		force_fraction,
	);
}

function _sparql_cast_string_lexical ( value ) {
	let lexical := _sparql_scalar_string(value);
	let datatype := _sparql_source_datatype(value);
	if ( datatype eq XSD_NS _ "boolean" ) {
		return "false" if lexical eq "0" or lc(lexical) eq "false";
		return "true" if lexical eq "1" or lc(lexical) eq "true";
	}
	if ( datatype eq XSD_NS _ "decimal" ) {
		return _sparql_cast_decimal_lexical(lexical, false);
	}
	if ( datatype eq XSD_NS _ "double" or datatype eq XSD_NS _ "float" ) {
		return _sparql_cast_decimal_number_lexical(
			_sparql_safe_numeric_value(value),
			false,
		);
	}
	return lexical;
}

function _sparql_cast_float_lexical ( value, String target_datatype ) {
	let lexical := _sparql_scalar_string(value);
	let source_datatype := _sparql_source_datatype(value);
	if ( source_datatype eq XSD_NS _ "boolean" ) {
		return "1.0E0" if lc(lexical) eq "true" or lexical eq "1";
		return "0E0";
	}
	if ( source_datatype eq XSD_NS _ "integer" ) {
		return "0" if lexical eq "0";
		return lexical _ ".0";
	}
	if ( source_datatype eq XSD_NS _ "decimal" ) {
		return _sparql_cast_decimal_lexical(lexical, true);
	}
	if ( source_datatype eq XSD_NS _ "double" or
		source_datatype eq XSD_NS _ "float"
	) {
		return _sparql_cast_decimal_number_lexical(
			_sparql_safe_numeric_value(value),
			true,
		);
	}
	if ( _sparql_cast_is_string_source(value) ) {
		if ( contains( uc(lexical), "E" ) ) {
			let n := _sparql_safe_numeric_value(value);
			return null if n == null;
			return "0E0" if n == 0;
			return replace( lexical, /^\+/, "", "" )
				if n == 1 and uc(lexical) ~ /^(\+)?1(\.0*)?E0$/;
			return _sparql_double_lexical( n, true );
		}
		if ( contains( lexical, "." ) ) {
			let n := _sparql_safe_numeric_value(value);
			return null if n == null;
			return "0E0" if n == 0;
			if ( n >= 10 or n <= -10 ) {
				let sign := n < 0 ? "-" : "";
				let magnitude := n < 0 ? 0 - n : n;
				let exp := 0;
				while ( magnitude >= 10 ) {
					magnitude /= 10;
					exp++;
				}
				return sign _ _sparql_decimal_lexical(magnitude, false) _
					"E" _ exp;
			}
			return _sparql_cast_decimal_lexical(lexical, false) _ "E0";
		}
		return lexical;
	}
	return null;
}

function _sparql_cast ( value, String datatype ) {
	return null if value == null;
	let lexical := _sparql_scalar_string(value);
	let source_datatype := _sparql_source_datatype(value);
	if ( datatype eq XSD_NS _ "string" ) {
		return rdf_literal(_sparql_cast_string_lexical(value), "", rdf_iri(datatype));
	}
	if ( datatype eq XSD_NS _ "boolean" ) {
		let normalized := lc(lexical);
		return rdf_literal( "true", "", rdf_iri(datatype) )
			if normalized eq "true" or normalized eq "1";
		return rdf_literal( "false", "", rdf_iri(datatype) )
			if normalized eq "false" or normalized eq "0";
		return rdf_literal(
			_sparql_numeric_value(value) == 0 ? "false" : "true",
			"",
			rdf_iri(datatype),
		) if _sparql_is_numeric_literal(value);
		return null;
	}
	if ( datatype eq XSD_NS _ "integer" or datatype eq XSD_NS _ "int" ) {
		if ( _sparql_cast_is_string_source(value) ) {
			return null unless lexical ~ /^[+-]?[0-9]+$/;
		}
		else {
			return null unless _sparql_is_numeric_literal(value) or
				source_datatype eq XSD_NS _ "boolean";
		}
		return rdf_literal(
			( lc(lexical) eq "true" or lexical eq "1" ) ? "1" : "0",
			"",
			rdf_iri(datatype),
		) if source_datatype eq XSD_NS _ "boolean";
		return rdf_literal( int(0 + lexical), "", rdf_iri(datatype) );
	}
	if ( datatype eq XSD_NS _ "decimal" ) {
		if ( _sparql_cast_is_string_source(value) ) {
			return null unless lexical ~ /^[+-]?[0-9]+$/ or
				lexical ~ /^[+-]?([0-9]+(\.[0-9]*)?|\.[0-9]+)$/;
			return rdf_literal(
				_sparql_cast_decimal_lexical(lexical, true),
				"",
				rdf_iri(datatype),
			);
		}
		return rdf_literal( "1.0", "", rdf_iri(datatype) )
			if source_datatype eq XSD_NS _ "boolean" and lc(lexical) eq "true";
		return rdf_literal( "0.0", "", rdf_iri(datatype) )
			if source_datatype eq XSD_NS _ "boolean" and
			( lc(lexical) eq "false" or lexical eq "0" );
		return null unless _sparql_is_numeric_literal(value);
		let force_fraction := source_datatype ne XSD_NS _ "integer" or
			lexical ne "0";
		return rdf_literal(
			_sparql_cast_decimal_number_lexical(
				_sparql_safe_numeric_value(value),
				true,
			),
			"",
			rdf_iri(datatype),
		) if source_datatype eq XSD_NS _ "double" or
			source_datatype eq XSD_NS _ "float";
		return rdf_literal(
			_sparql_cast_decimal_lexical(lexical, force_fraction),
			"",
			rdf_iri(datatype),
		);
	}
	if ( datatype eq XSD_NS _ "double" or datatype eq XSD_NS _ "float" ) {
		return null unless lexical ~ /^[+-]?[0-9]+$/ or
			lexical ~ /^[+-]?[0-9]*\.[0-9]+$/ or
			lexical ~ /^[+-]?[0-9]+(\.[0-9]+)?[eE][+-]?[0-9]+$/ or
			source_datatype eq XSD_NS _ "boolean";
		let out := _sparql_cast_float_lexical(value, datatype);
		return null if out == null;
		return rdf_literal( out, "", rdf_iri(datatype) );
	}
	return rdf_literal(lexical, "", rdf_iri(datatype));
}

function _sparql_lang_matches ( String lang, String range ) {
	return true if range eq "*";
	return lc(lang) eq lc(range) or starts_with( lc(lang), lc(range) _ "-" );
}

function _sparql_regex_flags ( Array args ) {
	return args.length() > 2
		? _sparql_scalar_string(args[2])
		: "";
}

function _sparql_regex_match ( String text, String pattern, String flags ) {
	return ( text ~ to_Regexp_with_flags(pattern, flags) ) ? true : false;
}

function _sparql_regex_replace (
	String text,
	String pattern,
	String replacement,
	String flags
) {
	return replace( text, pattern, replacement, "g" _ flags );
}

function _sparql_date_time ( value ) {
	return Time.parse(_sparql_scalar_string(value), timezone: TimeZone.utc());
}

function _sparql_date_component ( String lexical, String field ) {
	let m := lexical ~
		/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]+(\.[0-9]+)?)/;
	return null unless m;
	return 0 + m[1] if field eq "YEAR";
	return 0 + m[2] if field eq "MONTH";
	return 0 + m[3] if field eq "DAY";
	return 0 + m[4] if field eq "HOURS";
	return 0 + m[5] if field eq "MINUTES";
	return 0 + m[6];
}

function _sparql_timezone_text ( String lexical ) {
	return "Z" if ends_with( lexical, "Z" );
	return substr( lexical, length lexical - 6 )
		if lexical ~ /[+-][0-9][0-9]:[0-9][0-9]$/;
	return "";
}

function _sparql_string_result_like ( source, String value ) {
	if ( source instanceof RDFLiteral ) {
		return rdf_literal( value, source.get_lang() ) if source.get_lang() ne "";
		if ( source.get_datatype().get_value() eq XSD_NS _ "string" ) {
			return rdf_literal( value, "", rdf_iri(XSD_NS _ "string") );
		}
	}
	return rdf_literal(value);
}

function _sparql_string_args_compatible ( left, right ) {
	return false unless _sparql_is_string_literal(left) and
		_sparql_is_string_literal(right);
	let left_lang := left.get_lang();
	let right_lang := right.get_lang();
	return true if right_lang eq "";
	return left_lang ne "" and lc(left_lang) eq lc(right_lang);
}

function _sparql_is_string_literal ( value ) {
	return false unless value instanceof RDFLiteral;
	return true if value.get_lang() ne "";
	let datatype := value.get_datatype().get_value();
	return datatype eq XSD_NS _ "string" or datatype eq RDF_NS _ "langString";
}

function _sparql_is_simple_literal ( value ) {
	return false unless value instanceof RDFLiteral;
	return false if value.get_lang() ne "";
	return value.get_datatype().get_value() eq XSD_NS _ "string";
}

function _sparql_concat_result ( Array values, String text ) {
	let lang := null;
	for ( let value in values ) {
		return null unless _sparql_is_string_literal(value);
		if ( value.get_lang() eq "" ) {
			lang := "";
			next;
		}
		lang := value.get_lang() if lang == null;
		if ( lang ne value.get_lang() ) {
			lang := "";
			last;
		}
	}
	return rdf_literal( text, lang ) if not (lang == null) and lang ne "";
	return rdf_literal( text, "", rdf_iri(XSD_NS _ "string") );
}

function _sparql_eval_function ( binding, String name, Array args,
	Dict prefixes ) {
	let func := _sparql_function_name( name, prefixes );
	if ( starts_with( func, XSD_NS ) ) {
		return _sparql_cast(
			_sparql_eval_expr( binding, args[0], prefixes ),
			func,
		);
	}
	if ( func eq "BOUND" ) {
		return false if args.length() != 1 or not starts_with( args[0], "?" );
		return binding.exists(substr( args[0], 1 ));
	}
	if ( func eq "STR" ) {
		let value := _sparql_eval_expr( binding, args[0], prefixes );
		return rdf_literal(_sparql_scalar_string(value));
	}
	if ( func eq "DATATYPE" ) {
		let value := _sparql_eval_expr( binding, args[0], prefixes );
		return value.get_datatype() if value instanceof RDFLiteral;
		return null;
	}
	if ( func eq "LANG" ) {
		let value := _sparql_eval_expr( binding, args[0], prefixes );
		return rdf_literal(value.get_lang()) if value instanceof RDFLiteral;
		return rdf_literal("");
	}
	if ( func eq "LANGMATCHES" ) {
		let lang := _sparql_scalar_string(
			_sparql_eval_expr( binding, args[0], prefixes ),
		);
		let range := _sparql_scalar_string(
			_sparql_eval_expr( binding, args[1], prefixes ),
		);
		return _sparql_boolean_literal(_sparql_lang_matches( lang, range ));
	}
	if ( func eq "CONCAT" ) {
		let out := "";
		let values := [];
		for ( let arg in args ) {
			let value := _sparql_eval_expr( binding, arg, prefixes );
			return null unless _sparql_is_string_literal(value);
			values.push(value);
			out _= _sparql_scalar_string(value);
		}
		return _sparql_concat_result( values, out );
	}
	if ( func eq "STRDT" ) {
		let value := _sparql_eval_expr( binding, args[0], prefixes );
		return null unless _sparql_is_simple_literal(value);
		let datatype := _sparql_eval_expr( binding, args[1], prefixes );
		return null unless datatype instanceof RDFIRI;
		return rdf_literal(
			_sparql_scalar_string(value),
			"",
			datatype,
		);
	}
	if ( func eq "STRLANG" ) {
		let value := _sparql_eval_expr( binding, args[0], prefixes );
		return null unless _sparql_is_simple_literal(value);
		return rdf_literal(
			_sparql_scalar_string(value),
			_sparql_scalar_string(_sparql_eval_expr( binding, args[1], prefixes )),
		);
	}
	if ( func eq "IRI" or func eq "URI" ) {
		let value := _sparql_eval_expr( binding, args[0], prefixes );
		return value if value instanceof RDFIRI;
		return rdf_iri(_sparql_resolve_iri(_sparql_scalar_string(value), prefixes));
	}
	if ( func eq "BNODE" ) {
		if ( args.length() == 0 ) {
			_sparql_bnode_counter++;
			return rdf_blank("sparql-b" _ _sparql_bnode_counter);
		}
		return rdf_blank("sparql-" _ _sparql_scalar_string(
			_sparql_eval_expr( binding, args[0], prefixes ),
		));
	}
	if ( func eq "UCASE" or func eq "LCASE" ) {
		let source := _sparql_eval_expr( binding, args[0], prefixes );
		return null unless _sparql_is_string_literal(source);
		let value := _sparql_scalar_string(source);
		return _sparql_string_result_like(
			source,
			func eq "UCASE" ? uc(value) : lc(value),
		);
	}
	if ( func eq "STRLEN" ) {
		let source := _sparql_eval_expr( binding, args[0], prefixes );
		return null unless _sparql_is_string_literal(source);
		let value := _sparql_scalar_string(source);
		return _sparql_integer_literal(length value);
	}
	if ( func eq "CONTAINS" or func eq "STRSTARTS" or func eq "STRENDS" ) {
		let left_value := _sparql_eval_expr( binding, args[0], prefixes );
		let right_value := _sparql_eval_expr( binding, args[1], prefixes );
		return null unless _sparql_is_string_literal(left_value) and
			_sparql_is_string_literal(right_value);
		let left := _sparql_scalar_string(left_value);
		let right := _sparql_scalar_string(right_value);
		return _sparql_boolean_literal(contains( left, right ))
			if func eq "CONTAINS";
		return _sparql_boolean_literal(starts_with( left, right ))
			if func eq "STRSTARTS";
		return _sparql_boolean_literal(ends_with( left, right ));
	}
	if ( func eq "SUBSTR" ) {
		let source := _sparql_eval_expr( binding, args[0], prefixes );
		return null unless _sparql_is_string_literal(source);
		let value := _sparql_scalar_string(source);
		let start := int(_sparql_numeric_value(_sparql_eval_expr( binding, args[1], prefixes ))) - 1;
		return _sparql_string_result_like( source, substr( value, start ) )
			if args.length() < 3;
		let count := int(_sparql_numeric_value(_sparql_eval_expr( binding, args[2], prefixes )));
		return _sparql_string_result_like( source, substr( value, start, count ) );
	}
	if ( func eq "STRBEFORE" or func eq "STRAFTER" ) {
		let source := _sparql_eval_expr( binding, args[0], prefixes );
		let needle_value := _sparql_eval_expr( binding, args[1], prefixes );
		return null unless _sparql_string_args_compatible(source, needle_value);
		let value := _sparql_scalar_string(source);
		let needle := _sparql_scalar_string(needle_value);
		let pos := index( value, needle );
		return rdf_literal("") if pos < 0;
		return _sparql_string_result_like( source, substr( value, 0, pos ) )
			if func eq "STRBEFORE";
		return _sparql_string_result_like(
			source,
			substr( value, pos + length needle ),
		);
	}
	if ( func eq "ENCODE_FOR_URI" ) {
		return rdf_literal(_url_escape(_sparql_scalar_string(
			_sparql_eval_expr( binding, args[0], prefixes ),
		)));
	}
	if ( func eq "REGEX" ) {
		let values := [];
		for ( let arg in args ) {
			values.push(_sparql_eval_expr( binding, arg, prefixes ));
		}
		return null unless _sparql_is_string_literal(values[0]) and
			_sparql_is_string_literal(values[1]);
		return _sparql_boolean_literal(_sparql_regex_match(
			_sparql_scalar_string(values[0]),
			_sparql_scalar_string(values[1]),
			_sparql_regex_flags(values),
		));
	}
	if ( func eq "REPLACE" ) {
		let values := [];
		for ( let arg in args ) {
			values.push(_sparql_eval_expr( binding, arg, prefixes ));
		}
		return null unless _sparql_is_string_literal(values[0]) and
			_sparql_is_string_literal(values[1]) and
			_sparql_is_string_literal(values[2]);
		return _sparql_string_result_like(values[0], _sparql_regex_replace(
			_sparql_scalar_string(values[0]),
			_sparql_scalar_string(values[1]),
			_sparql_scalar_string(values[2]),
			values.length() > 3 ? _sparql_scalar_string(values[3]) : "",
		));
	}
	if ( func eq "MD5" or func eq "SHA1" or func eq "SHA256" or
		func eq "SHA384" or func eq "SHA512"
	) {
		let bytes := to_binary(_sparql_scalar_string(
			_sparql_eval_expr( binding, args[0], prefixes ),
		));
		return rdf_literal(md5_hex(bytes)) if func eq "MD5";
		return rdf_literal(sha1_hex(bytes)) if func eq "SHA1";
		return rdf_literal(sha256_hex(bytes)) if func eq "SHA256";
		return rdf_literal(sha384_hex(bytes)) if func eq "SHA384";
		return rdf_literal(sha512_hex(bytes));
	}
	if ( func eq "ISIRI" or func eq "ISURI" or func eq "ISBLANK" or
		func eq "ISLITERAL" or func eq "ISNUMERIC"
	) {
		let value := _sparql_eval_expr( binding, args[0], prefixes );
		return _sparql_boolean_literal(value instanceof RDFIRI)
			if func eq "ISIRI" or func eq "ISURI";
		return _sparql_boolean_literal(value instanceof RDFBlank)
			if func eq "ISBLANK";
		return _sparql_boolean_literal(value instanceof RDFLiteral)
			if func eq "ISLITERAL";
		return _sparql_boolean_literal(_sparql_is_numeric_literal(value));
	}
	if ( func eq "SAMETERM" ) {
		return _sparql_boolean_literal(rdf_term_equals(
			_sparql_eval_expr( binding, args[0], prefixes ),
			_sparql_eval_expr( binding, args[1], prefixes ),
		));
	}
	if ( func eq "UUID" or func eq "STRUUID" ) {
		let uuid := create_uuid();
		return rdf_iri("urn:uuid:" _ uuid) if func eq "UUID";
		return rdf_literal(uuid);
	}
	if ( func eq "RAND" ) {
		return rdf_literal( "" _ Math.rand(), "", rdf_iri(XSD_NS _ "double") );
	}
	if ( func eq "NOW" ) {
		return rdf_literal(
			( new Time(timezone: TimeZone.utc()) ).to_rfc3339(),
			"",
			rdf_iri(XSD_NS _ "dateTime"),
		);
	}
	if ( func eq "YEAR" or func eq "MONTH" or func eq "DAY" or
		func eq "HOURS" or func eq "MINUTES" or func eq "SECONDS"
	) {
		let lexical := _sparql_scalar_string(
			_sparql_eval_expr( binding, args[0], prefixes ),
		);
		let value := _sparql_date_component( lexical, func );
		return null if value == null;
		return _sparql_decimal_expr_literal(value) if func eq "SECONDS";
		return _sparql_integer_literal(value);
	}
	if ( func eq "TZ" or func eq "TIMEZONE" ) {
		let value := _sparql_eval_expr( binding, args[0], prefixes );
		let tz := _sparql_timezone_text(_sparql_scalar_string(value));
		return rdf_literal(tz) if func eq "TZ";
		return rdf_literal("PT0S", "", rdf_iri(XSD_NS _ "dayTimeDuration"))
			if tz eq "Z" or tz eq "+00:00" or tz eq "-00:00";
		return null if tz eq "";
		let sign := substr( tz, 0, 1 );
		let hours := int(substr( tz, 1, 2 ));
		let mins := int(substr( tz, 4, 2 ));
		return rdf_literal(
			(sign eq "-" ? "-" : "") _ "PT" _ hours _ "H" _
				( mins == 0 ? "" : mins _ "M" ),
			"",
			rdf_iri(XSD_NS _ "dayTimeDuration"),
		);
	}
	if ( func eq "ABS" or func eq "CEIL" or func eq "FLOOR" or func eq "ROUND" ) {
		let source := _sparql_eval_expr( binding, args[0], prefixes );
		let n := _sparql_numeric_value(source);
		if ( func eq "ABS" ) {
			return _sparql_numeric_result_literal_like(
				source,
				n < 0 ? 0 - n : n,
			);
		}
		let whole := int(n);
		if ( func eq "CEIL" ) {
			return _sparql_numeric_result_literal_like(
				source,
				n > whole ? whole + 1 : whole,
			);
		}
		if ( func eq "FLOOR" ) {
			return _sparql_numeric_result_literal_like(
				source,
				n < whole ? whole - 1 : whole,
			);
		}
		let rounded_base := n + 0.5;
		let rounded_whole := int(rounded_base);
		rounded_whole-- if rounded_base < rounded_whole;
		return _sparql_numeric_result_literal_like(source, rounded_whole);
	}
	if ( func eq "IF" ) {
		let condition := _sparql_eval_expr( binding, args[0], prefixes );
		return null if condition == null;
		return _sparql_eval_expr( binding, args[1], prefixes )
			if _sparql_effective_boolean(condition);
		return _sparql_eval_expr( binding, args[2], prefixes );
	}
	if ( func eq "COALESCE" ) {
		for ( let arg in args ) {
			try {
				let value := _sparql_eval_expr( binding, arg, prefixes );
				return value if not (value == null);
			}
			catch {
			}
		}
		return null;
	}
	return null;
}

function _sparql_eval_expr ( binding, String raw, Dict prefixes ) {
	let clean := trim(_sparql_outer_parens(raw));
	return null if clean eq "";
	if ( starts_with( clean, "!" ) ) {
		return _sparql_boolean_literal(not _sparql_effective_boolean(
			_sparql_eval_expr( binding, substr( clean, 1 ), prefixes ),
		));
	}
	if ( clean ~ /^([\s\S]+)\s+NOT\s+IN\s*\(([\s\S]*)\)$/i ) {
		let m := clean ~ /^([\s\S]+)\s+NOT\s+IN\s*\(([\s\S]*)\)$/i;
		let left := _sparql_eval_expr( binding, m[1], prefixes );
		for ( let arg in _sparql_expr_args(m[2]) ) {
			return _sparql_boolean_literal(false)
				if _sparql_same_value(
					left,
					_sparql_eval_expr( binding, arg, prefixes ),
				);
		}
		return _sparql_boolean_literal(true);
	}
	if ( clean ~ /^([\s\S]+)\s+IN\s*\(([\s\S]*)\)$/i ) {
		let m := clean ~ /^([\s\S]+)\s+IN\s*\(([\s\S]*)\)$/i;
		let left := _sparql_eval_expr( binding, m[1], prefixes );
		for ( let arg in _sparql_expr_args(m[2]) ) {
			return _sparql_boolean_literal(true)
				if _sparql_same_value(
					left,
					_sparql_eval_expr( binding, arg, prefixes ),
				);
		}
		return _sparql_boolean_literal(false);
	}
	if ( clean ~ /^(\x22[\s\S]*\x22\^\^)([A-Za-z][A-Za-z0-9_-]*:[A-Za-z_][A-Za-z0-9_-]*)$/ ) {
		let m := clean ~ /^(\x22[\s\S]*\x22\^\^)([A-Za-z][A-Za-z0-9_-]*:[A-Za-z_][A-Za-z0-9_-]*)$/;
		let datatype := _sparql_expand( m[2], prefixes );
		if ( datatype instanceof RDFIRI ) {
			return ( new RDFReader(
				source: m[1] _ "<" _ datatype.get_value() _ ">",
			) ).read_literal();
		}
	}
	let compare := _sparql_find_top_expr_operator(
		clean,
		[ "!=", "<=", ">=", "=", "<", ">" ],
	);
	if ( not (compare == null) ) {
		let left := _sparql_eval_expr(
			binding,
			substr( clean, 0, compare{pos} ),
			prefixes,
		);
		let right := _sparql_eval_expr(
			binding,
			substr( clean, compare{pos} + length compare{op} ),
			prefixes,
		);
		if ( compare{op} eq "=" or compare{op} eq "!=" ) {
			let same := _sparql_same_value( left, right );
			return _sparql_boolean_literal(compare{op} eq "=" ? same : not same);
		}
		let order := _sparql_order_compare(left, right);
		return _sparql_boolean_literal(order < 0) if compare{op} eq "<";
		return _sparql_boolean_literal(order > 0) if compare{op} eq ">";
		return _sparql_boolean_literal(order <= 0) if compare{op} eq "<=";
		return _sparql_boolean_literal(order >= 0);
	}
	let add := _sparql_find_top_expr_operator( clean, [ "+", "-" ] );
	if ( not (add == null) ) {
		let left := _sparql_eval_expr( binding, substr( clean, 0, add{pos} ), prefixes );
		let right := _sparql_eval_expr(
			binding,
			substr( clean, add{pos} + length add{op} ),
			prefixes,
		);
		return null if left == null or right == null;
		return _sparql_numeric_expr_literal( left, right, add{op} );
	}
	let mul := _sparql_find_top_expr_operator( clean, [ "*", "/" ] );
	if ( not (mul == null) ) {
		let left := _sparql_eval_expr( binding, substr( clean, 0, mul{pos} ), prefixes );
		let right := _sparql_eval_expr(
			binding,
			substr( clean, mul{pos} + length mul{op} ),
			prefixes,
		);
		return null if left == null or right == null;
		return _sparql_numeric_expr_literal( left, right, mul{op} );
	}
	if ( clean ~ /^([A-Za-z_][A-Za-z0-9_]*(?::[A-Za-z_][A-Za-z0-9_-]*)?)\s*\(([\s\S]*)\)$/ ) {
		let m := clean ~ /^([A-Za-z_][A-Za-z0-9_]*(?::[A-Za-z_][A-Za-z0-9_-]*)?)\s*\(([\s\S]*)\)$/;
		return _sparql_eval_function(
			binding,
			m[1],
			_sparql_expr_args(m[2]),
			prefixes,
		);
	}
	if ( starts_with( clean, "?" ) ) {
		return binding.get(substr( clean, 1 ), null);
	}
	if ( clean ~ /^[+-]?[0-9]+$/ ) {
		return rdf_literal(clean, "", rdf_iri(XSD_NS _ "integer"));
	}
	if ( clean ~ /^[+-]?[0-9]*\.[0-9]+$/ ) {
		return rdf_literal(clean, "", rdf_iri(XSD_NS _ "decimal"));
	}
	if ( clean ~ /^[+-]?[0-9]+[eE][+-]?[0-9]+$/ or
		clean ~ /^[+-]?[0-9]*\.[0-9]+[eE][+-]?[0-9]+$/ ) {
		return rdf_literal(clean, "", rdf_iri(XSD_NS _ "double"));
	}
	return _sparql_expand( clean, prefixes );
}

function _sparql_eval_filter ( RDFStore store, String raw, Dict prefixes,
	binding, graph := null ) {
	let clean := trim(_sparql_outer_parens(raw));
	if ( _sparql_starts_keyword( clean, "NOT" ) ) {
		let rest := trim(substr( clean, 3 ));
		if ( _sparql_starts_keyword( rest, "EXISTS" ) ) {
			let group := _sparql_extract_braced(substr( rest, 6 ));
			return _sparql_eval_group_text(
				store,
				group{body},
				prefixes,
				[ binding ],
				graph,
			).length() == 0;
		}
	}
	if ( _sparql_starts_keyword( clean, "EXISTS" ) ) {
		let group := _sparql_extract_braced(substr( clean, 6 ));
		return _sparql_eval_group_text(
			store,
			group{body},
			prefixes,
			[ binding ],
			graph,
		).length() > 0;
	}
	let or_op := _sparql_find_top_expr_operator( clean, [ "||" ] );
	if ( not (or_op == null) ) {
		return _sparql_eval_filter(
			store,
			substr( clean, 0, or_op{pos} ),
			prefixes,
			binding,
			graph,
		) or _sparql_eval_filter(
			store,
			substr( clean, or_op{pos} + 2 ),
			prefixes,
			binding,
			graph,
		);
	}
	let and_op := _sparql_find_top_expr_operator( clean, [ "&&" ] );
	if ( not (and_op == null) ) {
		return _sparql_eval_filter(
			store,
			substr( clean, 0, and_op{pos} ),
			prefixes,
			binding,
			graph,
		) and _sparql_eval_filter(
			store,
			substr( clean, and_op{pos} + 2 ),
			prefixes,
			binding,
			graph,
		);
	}
	return _sparql_effective_boolean(
		_sparql_eval_expr( binding, clean, prefixes ),
	);
}

function _sparql_apply_bind ( Array bindings, String raw, Dict prefixes ) {
	let clean := trim(raw);
	throw new SPARQLError(message: "SPARQL BIND expression expected")
		unless clean ~ /^BIND\s*\(([\s\S]*)\)$/i;
	let m := clean ~ /^BIND\s*\(([\s\S]*)\)$/i;
	let inner := m[1];
	throw new SPARQLError(message: "SPARQL BIND AS expected")
		unless inner ~ /^([\s\S]*)\s+AS\s+\?([A-Za-z][A-Za-z0-9_]*)\s*$/i;
	let b := inner ~ /^([\s\S]*)\s+AS\s+\?([A-Za-z][A-Za-z0-9_]*)\s*$/i;
	let out := [];
	for ( let binding in bindings ) {
		let next_binding := _sparql_copy_binding(binding);
		let value := _sparql_eval_expr( binding, b[1], prefixes );
		next_binding.set( b[2], value ) if not (value == null);
		out.push(next_binding);
	}
	return out;
}

function _sparql_eval_filter_set ( RDFStore store, Array bindings,
	String raw, Dict prefixes, graph := null ) {
	let out := [];
	for ( let binding in bindings ) {
		out.push(binding)
			if _sparql_eval_filter( store, raw, prefixes, binding, graph );
	}
	return out;
}

function _sparql_join_bindings ( Array left, Array right ) {
	let out := [];
	for ( let binding in left ) {
		for ( let row in right ) {
			let merged := _sparql_merge_binding( binding, row );
			out.push(merged) if not (merged == null);
		}
	}
	return out;
}

function _sparql_eval_optional_group ( RDFStore store, Array bindings,
	String body, Dict prefixes, graph := null ) {
	let out := [];
	for ( let binding in bindings ) {
		let matches := _sparql_eval_group_text(
			store,
			body,
			prefixes,
			[ binding ],
			graph,
		);
		if ( matches.length() == 0 ) {
			out.push(binding);
		}
		else {
			for ( let match in matches ) {
				out.push(match);
			}
		}
	}
	return out;
}

function _sparql_eval_minus_group ( RDFStore store, Array bindings,
	String body, Dict prefixes, graph := null ) {
	let out := [];
	let matches := _sparql_eval_group_text(
		store,
		body,
		prefixes,
		[ {} ],
		graph,
	);
	for ( let binding in bindings ) {
		let blocked := false;
		for ( let match in matches ) {
			if ( _sparql_compatible( binding, match ) ) {
				blocked := true;
				last;
			}
		}
		out.push(binding) unless blocked;
	}
	return out;
}

function _sparql_named_graphs ( RDFStore store, Dict prefixes ) {
	if ( prefixes.exists("__dataset_named") and
		prefixes.get("__dataset_named").length() > 0
	) {
		return prefixes.get("__dataset_named");
	}
	let seen := {};
	let out := [];
	for ( let quad in store.find() ) {
		let g := quad.get_graph();
		next if g instanceof RDFDefaultGraph;
		let key := rdf_term_key(g);
		next if seen.exists(key);
		seen.set( key, true );
		out.push(g);
	}
	return out;
}

function _sparql_named_graph_allowed ( Dict prefixes, graph ) {
	return true unless prefixes.exists("__dataset_named");
	let allowed := prefixes.get("__dataset_named");
	return true if allowed.length() == 0;
	for ( let item in allowed ) {
		return true if rdf_term_equals( item, graph );
	}
	return false;
}

function _sparql_term_sparql ( term ) {
	return term.to_String()
		if term instanceof RDFIRI or term instanceof RDFBlank or
		term instanceof RDFLiteral;
	return "UNDEF";
}

function _sparql_values_for_binding ( binding ) {
	let vars := [];
	let values := [];
	for ( let key in binding.keys() ) {
		next if starts_with( key, "__" );
		let value := binding.get(key);
		next unless value instanceof RDFIRI or value instanceof RDFBlank or
			value instanceof RDFLiteral;
		vars.push("?" _ key);
		values.push(_sparql_term_sparql(value));
	}
	return "" if vars.length() == 0;
	return "VALUES (" _ join( " ", vars ) _ ") { (" _
		join( " ", values ) _ ") }\n";
}

function _sparql_term_from_json ( item ) {
	let kind := item.get( "type", "" );
	if ( kind eq "uri" ) {
		return rdf_iri(item{value});
	}
	if ( kind eq "bnode" ) {
		return rdf_blank(item{value});
	}
	if ( kind eq "literal" or kind eq "typed-literal" ) {
		let lang := item.get( "xml:lang", item.get( "lang", "" ) );
		let datatype := item.exists("datatype")
			? rdf_iri(item{datatype})
			: null;
		return rdf_literal( item{value}, lang, datatype );
	}
	return null;
}

function _sparql_json_bindings ( data ) {
	let out := [];
	for ( let row in data{results}{bindings} ) {
		let binding := {};
		for ( let key in row.keys() ) {
			let term := _sparql_term_from_json(row.get(key));
			binding.set( key, term ) if not (term == null);
		}
		out.push(binding);
	}
	return out;
}

function _sparql_service_query ( String endpoint, String query ) {
	let ua := new UserAgent();
	let req := ua.build_request( "POST", endpoint )
		.header( "Accept", "application/sparql-results+json" )
		.header( "Content-Type", "application/x-www-form-urlencoded" )
		.body("query=" _ _url_escape(query));
	return _sparql_json_bindings(req.send(ua).expect_success().json());
}

function _sparql_eval_service_group ( Array bindings, String endpoint_token,
	String body, Dict prefixes, Boolean silent ) {
	let endpoint_spec := _sparql_expand( endpoint_token, prefixes );
	let out := [];
	for ( let binding in bindings ) {
		let endpoint := endpoint_spec instanceof Dict and
			endpoint_spec.exists("variable")
			? binding.get( endpoint_spec{variable}, null )
			: endpoint_spec;
		if ( not( endpoint instanceof RDFIRI ) ) {
			out.push(binding) if silent;
			throw new SPARQLError(message: "SPARQL SERVICE endpoint must be an IRI")
				unless silent;
			next;
		}
		let query := _sparql_prefix_text(prefixes) _
			"\nSELECT * WHERE {\n" _
			_sparql_values_for_binding(binding) _
			body _
			"\n}";
		try {
			for ( let row in _sparql_service_query( endpoint.get_value(), query ) ) {
				let merged := _sparql_merge_binding( binding, row );
				out.push(merged) if not (merged == null);
			}
		}
		catch ( e ) {
			if ( silent ) {
				out.push(binding);
			}
			else {
				throw e;
			}
		}
	}
	return out;
}

function _sparql_eval_graph_group ( RDFStore store, Array bindings,
	String graph_token, String body, Dict prefixes ) {
	let graph_spec := _sparql_expand( graph_token, prefixes );
	let out := [];
	for ( let binding in bindings ) {
		let graph_terms := [];
		if ( graph_spec instanceof Dict and graph_spec.exists("variable") ) {
			if ( binding.exists(graph_spec{variable}) ) {
				graph_terms.push(binding.get(graph_spec{variable}));
			}
			else {
				graph_terms := _sparql_named_graphs( store, prefixes );
			}
		}
		else {
			graph_terms.push(graph_spec);
		}
		for ( let graph_term in graph_terms ) {
			next unless _sparql_named_graph_allowed( prefixes, graph_term );
			let seed := binding;
			if ( graph_spec instanceof Dict and graph_spec.exists("variable") and
				not binding.exists(graph_spec{variable})
			) {
				seed := _sparql_copy_binding(binding);
				seed.set( graph_spec{variable}, graph_term );
			}
			for ( let match in _sparql_eval_group_text(
				store,
				body,
				prefixes,
				[ seed ],
				graph_term,
			) ) {
				out.push(match);
			}
		}
	}
	return out;
}

function _sparql_prefix_text ( Dict prefixes ) {
	let lines := [];
	if ( prefixes.exists("__base") ) {
		lines.push("BASE <" _ prefixes.get("__base") _ ">");
	}
	for ( let name in prefixes.keys() ) {
		next if starts_with( name, "__" );
		lines.push("PREFIX " _ name _ ": <" _ prefixes.get(name) _ ">");
	}
	return join( "\n", lines );
}

function _sparql_eval_subquery_group ( RDFStore store, Array bindings,
	String body, Dict prefixes, graph := null ) {
	let query := _sparql_prefix_text(prefixes) _ "\n" _ body;
	let defaults := prefixes.get( "__dataset_default", [] );
	if ( not (graph == null) ) {
		defaults := graph instanceof Dict and graph.exists("default_graphs")
			? graph{default_graphs}
			: [ graph ];
	}
	let result := sparql_query(
		store,
		query,
		{
			default: defaults,
			named: prefixes.get( "__dataset_named", [] ),
		},
	);
	let out := [];
	for ( let binding in bindings ) {
		for ( let row in result{results} ) {
			let merged := _sparql_merge_binding( binding, row );
			out.push(merged) if not (merged == null);
		}
	}
	return out;
}

function _sparql_eval_group_or_union ( RDFStore store, Array bindings,
	String text, Dict prefixes, graph := null ) {
	let first := _sparql_extract_braced(text);
	let tail := first{tail};
	if ( _sparql_starts_keyword( tail, "UNION" ) ) {
		let branch_rows := _sparql_eval_group_text(
			store,
			first{body},
			prefixes,
			[ {} ],
			graph,
		);
		let rest := trim(substr( tail, 5 ));
		while ( starts_with( trim(rest), "{" ) ) {
			let item := _sparql_extract_braced(rest);
			for ( let binding in _sparql_eval_group_text(
				store,
				item{body},
				prefixes,
				[ {} ],
				graph,
			) ) {
				branch_rows.push(binding);
			}
			rest := item{tail};
			if ( _sparql_starts_keyword( rest, "UNION" ) ) {
				rest := trim(substr( rest, 5 ));
			}
			else {
				last;
			}
		}
		return { bindings: _sparql_join_bindings( bindings, branch_rows ),
			tail: rest };
	}
	return {
		bindings: _sparql_join_bindings(
			bindings,
			_sparql_eval_group_text(
				store,
				first{body},
				prefixes,
				[ {} ],
				graph,
			),
		),
		tail: tail,
	};
}

function _sparql_eval_bgp_chunk ( RDFStore store, Array bindings,
	String text, Dict prefixes, graph := null ) {
	let clean := trim(text);
	return bindings if clean eq "";
	if ( clean ~ /^\.?\s*BIND\s*\(/i ) {
		clean := replace( clean, /^\.?\s*/, "", "" );
		return _sparql_apply_bind( bindings, clean, prefixes );
	}
	let patterns := _sparql_parse_patterns(
		clean,
		prefixes,
		graph == null ? null : graph,
	);
	return _sparql_eval_patterns_from( store, patterns, bindings );
}

function _sparql_eval_group_text ( RDFStore store, String text, Dict prefixes,
	Array bindings, graph := null ) {
	let rest := trim(_sparql_strip_comments(text));
	let current := bindings;
	let deferred_filters := [];
	while ( rest ne "" ) {
		if ( _sparql_starts_keyword( rest, "SELECT" ) ) {
			current := _sparql_eval_subquery_group(
				store,
				current,
				rest,
				prefixes,
				graph,
			);
			rest := "";
			next;
		}
		if ( _sparql_starts_keyword( rest, "OPTIONAL" ) ) {
			let item := _sparql_extract_braced(substr( rest, 8 ));
			current := _sparql_eval_optional_group(
				store,
				current,
				item{body},
				prefixes,
				graph,
			);
			rest := item{tail};
			next;
		}
		if ( _sparql_starts_keyword( rest, "MINUS" ) ) {
			let item := _sparql_extract_braced(substr( rest, 5 ));
			current := _sparql_eval_minus_group(
				store,
				current,
				item{body},
				prefixes,
				graph,
			);
			rest := item{tail};
			next;
		}
		if ( _sparql_starts_keyword( rest, "FILTER" ) ) {
			let after := trim(substr( rest, 6 ));
			if ( _sparql_starts_keyword( after, "NOT" ) or
				_sparql_starts_keyword( after, "EXISTS" )
			) {
				let item := _sparql_extract_braced(after);
			deferred_filters.push(substr(
				after,
				0,
				length after - length item{tail},
			));
			rest := item{tail};
			next;
		}
			let open := starts_with( after, "(" ) ? 0 : index( after, "(" );
			let close := open < 0 ? -1 : _sparql_matching_paren(after, open);
			throw new SPARQLError(message: "SPARQL FILTER expression expected")
				if close < 0;
			let expr := open == 0
				? substr( after, 1, close - 1 )
				: substr( after, 0, close + 1 );
			deferred_filters.push(expr);
			rest := trim(substr( after, close + 1 ));
			next;
		}
			if ( _sparql_starts_keyword( rest, "BIND" ) or rest ~ /^BIND\s*\(/i ) {
			let open := index( rest, "(" );
			let close := open < 0 ? -1 : _sparql_matching_paren( rest, open );
			throw new SPARQLError(message: "SPARQL BIND expression expected")
				if close < 0;
			current := _sparql_apply_bind(
				current,
				substr( rest, 0, close + 1 ),
				prefixes,
			);
			rest := trim(substr( rest, close + 1 ));
			next;
		}
		if ( _sparql_starts_keyword( rest, "VALUES" ) ) {
			let item := _sparql_extract_braced(rest);
			let values_text := substr(
				rest,
				0,
				length rest - length item{tail},
			);
			current := _sparql_apply_values(
				current,
				_sparql_parse_values_blocks(values_text, prefixes),
			);
			rest := item{tail};
			next;
		}
		if ( _sparql_starts_keyword( rest, "SERVICE" ) ) {
			let after := trim(substr( rest, 7 ));
			let silent := false;
			if ( _sparql_starts_keyword( after, "SILENT" ) ) {
				silent := true;
				after := trim(substr( after, 6 ));
			}
			let item := _sparql_extract_braced(after);
			current := _sparql_eval_service_group(
				current,
				item{before},
				item{body},
				prefixes,
				silent,
			);
			rest := item{tail};
			next;
		}
		if ( _sparql_starts_keyword( rest, "GRAPH" ) ) {
			let item := _sparql_extract_braced(substr( rest, 5 ));
			current := _sparql_eval_graph_group(
				store,
				current,
				item{before},
				item{body},
				prefixes,
			);
			rest := item{tail};
			next;
		}
		if ( starts_with( rest, "{" ) ) {
			let item := _sparql_extract_braced(rest);
			if ( starts_with( uc(trim(item{body})), "SELECT" ) ) {
				current := _sparql_eval_subquery_group(
					store,
					current,
					item{body},
					prefixes,
					graph,
				);
				rest := item{tail};
				next;
			}
			let grouped := _sparql_eval_group_or_union(
				store,
				current,
				rest,
				prefixes,
				graph,
			);
			current := grouped{bindings};
			rest := grouped{tail};
			next;
		}
		let next_op := _sparql_find_top_operator(rest);
		let chunk := next_op < 0 ? rest : substr( rest, 0, next_op );
		current := _sparql_eval_bgp_chunk(
			store,
			current,
			chunk,
			prefixes,
			graph,
		);
		rest := next_op < 0 ? "" : trim(substr( rest, next_op ));
	}
	for ( let expr in deferred_filters ) {
		current := _sparql_eval_filter_set(
			store,
			current,
			expr,
			prefixes,
			graph,
		);
	}
	return current;
}

function _sparql_parse_select_projection ( String head ) {
	let vars := [];
	let aliases := [];
	let where_pos := index( uc(head), "WHERE" );
	let select_head := where_pos >= 0 ? substr( head, 0, where_pos ) : head;
	while ( select_head ~ /FROM\s+(NAMED\s+)?(<[^>]+>|[A-Za-z][A-Za-z0-9_-]*:[^\s{}]+)/i ) {
		select_head := replace(
			select_head,
			/FROM\s+(NAMED\s+)?(<[^>]+>|[A-Za-z][A-Za-z0-9_-]*:[^\s{}]+)/i,
			"",
			"",
		);
	}
	let rest := "";
	let i := 0;
	while ( i < length select_head ) {
		let ch := substr( select_head, i, 1 );
		if ( ch eq "(" ) {
			let close := _sparql_matching_paren( select_head, i );
			if ( close > i ) {
				let inner := trim(substr( select_head, i + 1, close - i - 1 ));
				if ( inner ~ /^(.+)\s+AS\s+\?([A-Za-z][A-Za-z0-9_]*)$/i ) {
					let m := inner ~ /^(.+)\s+AS\s+\?([A-Za-z][A-Za-z0-9_]*)$/i;
					let alias := { expr: trim(m[1]), var: m[2] };
					let aggregate := _sparql_parse_aggregate(alias{expr});
					if ( not (aggregate == null) ) {
						alias.set( "aggregate", aggregate{kind} );
						alias.set( "distinct", aggregate{distinct} );
						alias.set( "arg", aggregate{arg} );
						alias.set( "separator", aggregate{separator} );
					}
					aliases.push(alias);
					vars.push(m[2]);
					i := close + 1;
					next;
				}
			}
		}
		rest _= ch;
		i++;
	}
	for ( let part in split( rest, /\s+/ ) ) {
		let clean := trim(part);
		vars.push(substr(clean, 1)) if starts_with( clean, "?" );
	}
	return { vars: vars, aliases: aliases };
}

function _sparql_parse_aggregate ( String raw ) {
	let clean := trim(raw);
	return null unless clean ~ /^([A-Za-z_][A-Za-z0-9_]*)\s*\(([\s\S]*)\)$/;
	let m := clean ~ /^([A-Za-z_][A-Za-z0-9_]*)\s*\(([\s\S]*)\)$/;
	let kind := uc(m[1]);
	return null unless kind eq "COUNT" or kind eq "SUM" or kind eq "MIN" or
		kind eq "MAX" or kind eq "AVG" or kind eq "SAMPLE" or
		kind eq "GROUP_CONCAT";
	let arg := trim(m[2]);
	let distinct := false;
	if ( _sparql_starts_keyword( arg, "DISTINCT" ) ) {
		distinct := true;
		arg := trim(substr( arg, 8 ));
	}
	let separator := " ";
	if ( kind eq "GROUP_CONCAT" and contains( uc(arg), "SEPARATOR" ) ) {
		// index/substr rather than a limit-2 split: zuzu-js truncates
		// the tail, and the separator string may itself contain ";".
		let semi := index( arg, ";" );
		let sep_clause := semi < 0 ? "" : substr( arg, semi + 1 );
		arg := trim(semi < 0 ? arg : substr( arg, 0, semi ));
		if ( sep_clause ~ /SEPARATOR\s*=\s*(\x22[^\x22]*\x22)/i ) {
			let sm := sep_clause ~ /SEPARATOR\s*=\s*(\x22[^\x22]*\x22)/i;
			separator := ( new RDFReader(source: sm[1]) ).read_literal().get_value();
		}
	}
	return {
		kind: kind,
		arg: arg,
		distinct: distinct,
		separator: separator,
	};
}

function _sparql_parse_group_by ( String tail ) {
	let vars := [];
	if ( tail ~ /GROUP\s+BY\s+([\s\S]*)/i ) {
		let m := tail ~ /GROUP\s+BY\s+([\s\S]*)/i;
		let body := m[1];
		if ( body ~ /^([\s\S]*?)\s+(HAVING|ORDER|LIMIT|OFFSET|VALUES)\b/i ) {
			let stop := body ~ /^([\s\S]*?)\s+(HAVING|ORDER|LIMIT|OFFSET|VALUES)\b/i;
			body := stop[1];
		}
		let i := 0;
		while ( i < length body ) {
			let ch := substr( body, i, 1 );
			if ( ch ~ /\s/ ) {
				i++;
				next;
			}
			if ( ch eq "?" ) {
				let rest := substr( body, i );
				if ( rest ~ /^\?([A-Za-z][A-Za-z0-9_]*)/ ) {
					let gm := rest ~ /^\?([A-Za-z][A-Za-z0-9_]*)/;
					vars.push(gm[1]);
					i += length gm[0];
					next;
				}
			}
			if ( ch eq "(" ) {
				let close := _sparql_matching_paren( body, i );
				if ( close > i ) {
					let inner := trim(substr( body, i + 1, close - i - 1 ));
					if ( inner ~ /^([\s\S]*)\s+AS\s+\?([A-Za-z][A-Za-z0-9_]*)$/i ) {
						let em := inner ~ /^([\s\S]*)\s+AS\s+\?([A-Za-z][A-Za-z0-9_]*)$/i;
						vars.push({ expr: trim(em[1]), var: em[2] });
					}
					i := close + 1;
					next;
				}
			}
			i++;
		}
	}
	return vars;
}

function _sparql_parse_having ( String tail ) {
	let out := [];
	let pos := index( uc(tail), "HAVING" );
	return out if pos < 0;
	let rest := substr( tail, pos + 6 );
	while ( trim(rest) ne "" ) {
		let clean := trim(rest);
		last if _sparql_starts_keyword( clean, "ORDER" ) or
			_sparql_starts_keyword( clean, "LIMIT" ) or
			_sparql_starts_keyword( clean, "OFFSET" ) or
			_sparql_starts_keyword( clean, "VALUES" );
		last unless starts_with( clean, "(" );
		let close := _sparql_matching_paren( clean, 0 );
		last if close < 0;
		out.push(substr( clean, 1, close - 1 ));
		rest := substr( clean, close + 1 );
	}
	return out;
}

function _sparql_parse_order_by ( String tail ) {
	let out := [];
	let pos := index( uc(tail), "ORDER" );
	return out if pos < 0;
	let rest := trim(substr( tail, pos + 5 ));
	return out unless _sparql_starts_keyword( rest, "BY" );
	rest := trim(substr( rest, 2 ));
	if ( rest ~ /^([\s\S]*?)\s+(LIMIT|OFFSET|VALUES)\b/i ) {
		let stop := rest ~ /^([\s\S]*?)\s+(LIMIT|OFFSET|VALUES)\b/i;
		rest := stop[1];
	}
	let i := 0;
	while ( i < length rest ) {
		let clean := trim(substr( rest, i ));
		last if clean eq "";
		let desc := false;
		if ( _sparql_starts_keyword( clean, "ASC" ) or
			_sparql_starts_keyword( clean, "DESC" )
		) {
			desc := _sparql_starts_keyword( clean, "DESC" );
			let open := index( clean, "(" );
			let close := open < 0 ? -1 : _sparql_matching_paren( clean, open );
			if ( close > open ) {
				out.push({
					expr: substr( clean, open + 1, close - open - 1 ),
					desc: desc,
				});
				i += close + 1;
				next;
			}
		}
		if ( starts_with( clean, "?" ) and clean ~ /^\?([A-Za-z][A-Za-z0-9_]*)/ ) {
			let m := clean ~ /^\?([A-Za-z][A-Za-z0-9_]*)/;
			out.push({ expr: m[0], desc: false });
			i += length m[0];
			next;
		}
		if ( starts_with( clean, "(" ) ) {
			let close := _sparql_matching_paren( clean, 0 );
			if ( close > 0 ) {
				out.push({ expr: substr( clean, 1, close - 1 ), desc: false });
				i += close + 1;
				next;
			}
		}
		last;
	}
	return out;
}

function _sparql_parse_dataset ( String head, Dict prefixes ) {
	let defaults := [];
	let named := [];
	let rest := head;
	while ( rest ~ /FROM\s+(NAMED\s+)?(<[^>]+>|[A-Za-z][A-Za-z0-9_-]*:[^\s{}]+)/i ) {
		let m := rest ~ /FROM\s+(NAMED\s+)?(<[^>]+>|[A-Za-z][A-Za-z0-9_-]*:[^\s{}]+)/i;
		let graph := _sparql_expand(m[2], prefixes);
		if ( trim(m[1]) eq "" ) {
			defaults.push(graph);
		}
		else {
			named.push(graph);
		}
		rest := replace(
			rest,
			/FROM\s+(NAMED\s+)?(<[^>]+>|[A-Za-z][A-Za-z0-9_-]*:[^\s{}]+)/i,
			"",
			"",
		);
	}
	return { dataset_default: defaults, dataset_named: named };
}

function _sparql_parse_describe_targets ( String head, Dict prefixes ) {
	let clean := trim(substr( trim(head), 8 ));
	let where_pos := index( uc(clean), "WHERE" );
	clean := trim(substr( clean, 0, where_pos )) if where_pos >= 0;
	while ( clean ~ /FROM\s+(NAMED\s+)?(<[^>]+>|[A-Za-z][A-Za-z0-9_-]*:[^\s{}]+)/i ) {
		clean := replace(
			clean,
			/FROM\s+(NAMED\s+)?(<[^>]+>|[A-Za-z][A-Za-z0-9_-]*:[^\s{}]+)/i,
			"",
			"",
		);
	}
	let out := [];
	let all := false;
	for ( let token in split( clean, " " ) ) {
		let item := trim(token);
		next if item eq "";
		if ( item eq "*" ) {
			all := true;
			next;
		}
		out.push(_sparql_expand( item, prefixes ));
	}
	return { all: all, targets: out };
}

function _sparql_parse ( String query ) {
	_sparql_validate_syntax(query);
	let prep := _sparql_prefixes(query);
	let text := prep{body};
	let upper := uc(text);
	let kind := starts_with( trim(upper), "ASK" )
		? "ask"
		: starts_with( trim(upper), "CONSTRUCT" )
			? "construct"
			: starts_with( trim(upper), "DESCRIBE" )
				? "describe"
				: "select";
	if ( contains( upper, "INSERT" ) or contains( upper, "DELETE" ) or
		contains( upper, "LOAD" ) or contains( upper, "CLEAR" ) or
		contains( upper, "CREATE" ) or contains( upper, "DROP" )
	) {
		throw new SPARQLError(message: "SPARQL Update is not implemented");
	}
	let open := index( text, "{" );
	let close := open < 0 ? -1 : _sparql_matching_brace( text, open );
	let head := "";
	let where := "";
	let tail := "";
	if ( kind eq "describe" and open < 0 ) {
		head := text;
		where := "";
		tail := "";
	}
	if ( kind eq "construct" and open >= 0 ) {
		let after_construct := trim(substr( trim(text), 9 ));
		if ( starts_with( after_construct, "{" ) ) {
			let template_close := _sparql_matching_brace( text, open );
			let after_template := substr( text, template_close + 1 );
			let where_open := index( after_template, "{" );
			if ( where_open >= 0 ) {
				open := template_close + 1 + where_open;
				close := _sparql_matching_brace( text, open );
				head := substr( text, 0, template_close + 1 );
				where := substr( text, open + 1, close - open - 1 );
				tail := substr( text, close + 1 );
			}
		}
	}
	if ( head eq "" ) {
		throw new SPARQLError(message: "SPARQL WHERE block expected")
			if open < 0 or close < open;
		head := substr( text, 0, open );
		where := substr( text, open + 1, close - open - 1 );
		tail := substr( text, close + 1 );
	}
	let describe := kind eq "describe"
		? _sparql_parse_describe_targets( head, prep{prefixes} )
		: { all: false, targets: [] };
	let group_text := where;
	let values := _sparql_parse_values_blocks(
		tail,
		prep{prefixes},
	);
	where := _sparql_remove_values(where);
	tail := _sparql_remove_values(tail);
		let projection := _sparql_parse_select_projection(head);
		let dataset := _sparql_parse_dataset( head, prep{prefixes} );
		let vars := projection{vars};
		let distinct := contains( uc(head), "DISTINCT" );
		let select_all := kind eq "select" and contains( head, "*" );
	let order_by := _sparql_parse_order_by(tail);
		let group_by := _sparql_parse_group_by(tail);
		let having := _sparql_parse_having(tail);
	let filters := [];
	while ( where ~ /FILTER\s*\(([^()]*)\)/i ) {
		let m := where ~ /FILTER\s*\(([^()]*)\)/i;
		filters.push(m[1]);
		where := replace( where, /FILTER\s*\([^()]*\)/i, "", "" );
	}
	let optional := [];
	while ( where ~ /OPTIONAL\s*\{([^{}]+)\}/i ) {
		let m := where ~ /OPTIONAL\s*\{([^{}]+)\}/i;
		optional.push(_sparql_parse_patterns(
			m[1],
			prep{prefixes},
		));
		where := replace( where, /OPTIONAL\s*\{[^{}]+\}/i, "", "" );
	}
	let patterns := [];
	while ( where ~ /GRAPH\s+([^\s]+)\s*\{([^{}]+)\}/i ) {
		let m := where ~ /GRAPH\s+([^\s]+)\s*\{([^{}]+)\}/i;
		let g := _sparql_expand(m[1], prep{prefixes});
		for ( let pattern in _sparql_parse_patterns( m[2], prep{prefixes}, g ) ) {
			patterns.push(pattern);
		}
		where := replace( where, /GRAPH\s+[^\s]+\s*\{[^{}]+\}/i, "", "" );
	}
		try {
			for ( let pattern in _sparql_parse_patterns( where, prep{prefixes} ) ) {
				patterns.push(pattern);
			}
		}
		catch {
			patterns := [];
		}
	return {
		kind: kind,
		vars: vars,
		patterns: patterns,
		optional: optional,
		filters: filters,
			values: values,
				where: group_text,
			tail: tail,
			aliases: projection{aliases},
			template: kind eq "construct" ? trim(head) : "",
				distinct: distinct,
				select_all: select_all,
				group_by: group_by,
			having: having,
			order_by: order_by,
			dataset_default: dataset{dataset_default},
			dataset_named: dataset{dataset_named},
			describe_all: describe{all},
			describe_targets: describe{targets},
	limit: _sparql_limit_value( tail, "LIMIT" ),
		offset: _sparql_limit_value( tail, "OFFSET" ),
		prefixes: prep{prefixes},
	};
}

function _binding_term ( binding, spec ) {
	return null if spec instanceof Dict and spec.exists("wildcard");
	return binding.get(spec{variable}, null)
		if spec instanceof Dict and spec.exists("variable");
	return spec;
}

function _sparql_graph_allows ( graph_spec, graph ) {
	if ( graph_spec instanceof Dict and graph_spec.exists("default_graphs") ) {
		for ( let item in graph_spec{default_graphs} ) {
			return true if rdf_term_equals( item, graph );
		}
		return false;
	}
	return rdf_term_equals( graph_spec, graph )
		if _sparql_is_term(graph_spec) and _sparql_is_term(graph);
	return true if graph_spec == null;
	return false;
}

function _sparql_find_quads ( RDFStore store, s, p, o, graph_spec ) {
	if ( graph_spec instanceof Dict and graph_spec.exists("default_graphs") ) {
		let out := [];
		let seen := {};
		for ( let graph in graph_spec{default_graphs} ) {
			for ( let quad in store.find( s, p, o, graph ) ) {
				let key := rdf_term_key(quad.get_subject()) _ "\n" _
					rdf_term_key(quad.get_predicate()) _ "\n" _
					rdf_term_key(quad.get_object());
				next if seen.exists(key);
				seen.set( key, true );
				out.push(quad);
			}
		}
		return out;
	}
	return store.find( s, p, o, graph_spec );
}

function _sparql_is_term ( value ) {
	return value instanceof RDFIRI or value instanceof RDFBlank or
		value instanceof RDFLiteral or value instanceof RDFDefaultGraph;
}

function _sparql_is_path_spec ( value ) {
	return value instanceof Dict and value.exists("path");
}

function _bind_match ( binding, spec, term ) {
	return binding if spec instanceof Dict and spec.exists("wildcard");
	if ( spec instanceof Dict and spec.exists("variable") ) {
		let name := spec{variable};
		if ( binding.exists(name) ) {
			return rdf_term_equals( binding.get(name), term ) ? binding : null;
		}
		let next_binding := {};
		for ( let item_key in binding.keys() ) {
			next_binding.set( item_key, binding.get(item_key) );
		}
		next_binding.set( name, term );
		return next_binding;
	}
	return binding if spec instanceof Dict and spec.exists("default_graphs") and
		_sparql_graph_allows( spec, term );
	return null unless _sparql_is_term(spec) and _sparql_is_term(term);
	return rdf_term_equals( spec, term ) ? binding : null;
}

function _sparql_push_unique_term ( Array out, seen, term ) {
	let key := rdf_term_key(term);
	return out if seen.exists(key);
	seen.set( key, true );
	out.push(term);
	return out;
}

function _sparql_term_array_has ( Array array, term ) {
	for ( let item in array ) {
		return true if rdf_term_equals( item, term );
	}
	return false;
}

function _sparql_graph_nodes ( RDFStore store, graph ) {
	let out := [];
	let seen := {};
	for ( let quad in _sparql_find_quads( store, null, null, null, graph ) ) {
		_sparql_push_unique_term( out, seen, quad.get_subject() );
		_sparql_push_unique_term( out, seen, quad.get_object() );
	}
	return out;
}

function _sparql_path_node_in_graph ( RDFStore store, term, graph ) {
	return true if _sparql_find_quads( store, term, null, null, graph ).length() > 0;
	return true if _sparql_find_quads( store, null, null, term, graph ).length() > 0;
	return false;
}

function _sparql_path_zero ( RDFStore store, start, graph ) {
	return _sparql_path_node_in_graph( store, start, graph ) ? [ start ] : [];
}

function _sparql_path_term ( item ) {
	return item instanceof Dict and item.exists("term")
		? item{term}
		: item;
}

function _sparql_path_negated ( Array items, Boolean inverse, predicate ) {
	for ( let item in items ) {
		next unless item{inverse} == inverse;
		return true if rdf_term_equals( _sparql_path_term(item), predicate );
	}
	return false;
}

function _sparql_path_direct ( RDFStore store, start, predicate, graph ) {
	let out := [];
	for ( let quad in _sparql_find_quads( store, start, predicate, null, graph ) ) {
		out.push(quad.get_object());
	}
	return out;
}

function _sparql_path_inverse ( RDFStore store, start, predicate, graph ) {
	let out := [];
	for ( let quad in _sparql_find_quads( store, null, predicate, start, graph ) ) {
		out.push(quad.get_subject());
	}
	return out;
}

function _sparql_path_negated_step ( RDFStore store, start, spec, graph ) {
	let out := [];
	let seen := {};
	let has_direct := false;
	let has_inverse := false;
	for ( let item in spec{items} ) {
		has_inverse := true if item{inverse};
		has_direct := true unless item{inverse};
	}
	if ( has_direct ) {
		for ( let quad in _sparql_find_quads( store, start, null, null, graph ) ) {
			next if _sparql_path_negated(
				spec{items},
				false,
				quad.get_predicate(),
			);
			_sparql_push_unique_term( out, seen, quad.get_object() );
		}
	}
	if ( has_inverse ) {
		for ( let quad in _sparql_find_quads( store, null, null, start, graph ) ) {
			next if _sparql_path_negated(
				spec{items},
				true,
				quad.get_predicate(),
			);
			_sparql_push_unique_term( out, seen, quad.get_subject() );
		}
	}
	return out;
}

function _sparql_path_closure ( RDFStore store, start, spec, graph,
	Boolean include_start ) {
	let out := [];
	let seen := {};
	let queue := [];
	if ( include_start and _sparql_path_node_in_graph( store, start, graph ) ) {
		_sparql_push_unique_term( out, seen, start );
	}
	for ( let term in _sparql_path_endpoints( store, start, spec, graph ) ) {
		_sparql_push_unique_term( out, seen, term );
		queue.push(term);
	}
	let i := 0;
	while ( i < queue.length() ) {
		let current := queue[i];
		for ( let term in _sparql_path_endpoints( store, current, spec, graph ) ) {
			let before := out.length();
			_sparql_push_unique_term( out, seen, term );
			queue.push(term) if out.length() > before;
		}
		i++;
	}
	return out;
}

function _sparql_path_can_be_zero ( path ) {
	return false unless _sparql_is_path_spec(path);
	if ( path{path} eq "*" or path{path} eq "?" ) {
		return true;
	}
	if ( path{path} eq "{}" ) {
		return path{min} == 0;
	}
	if ( path{path} eq "alt" ) {
		for ( let item in path{items} ) {
			return true if _sparql_path_can_be_zero(item);
		}
	}
	if ( path{path} eq "seq" ) {
		for ( let item in path{items} ) {
			return false unless _sparql_path_can_be_zero(item);
		}
		return true;
	}
	if ( path{path} eq "inv" ) {
		return _sparql_path_can_be_zero(path{item});
	}
	return false;
}

function _sparql_path_repeat ( RDFStore store, start, spec, graph,
	Number min, max ) {
	return _sparql_path_zero( store, start, graph ) if min == 0 and max == 0;
	return _sparql_path_closure( store, start, spec, graph, true )
		if min == 0 and max == null;
	return _sparql_path_closure( store, start, spec, graph, false )
		if min == 1 and max == null;
	let frontier := [ start ];
	let out := [];
	let seen := {};
	let depth := 0;
	while ( frontier.length() > 0 and ( max == null or depth < max ) ) {
		if ( depth >= min ) {
			for ( let term in frontier ) {
				_sparql_push_unique_term( out, seen, term );
			}
		}
		let next_frontier := [];
		let next_seen := {};
		for ( let term in frontier ) {
			for ( let next_term in _sparql_path_endpoints(
				store,
				term,
				spec,
				graph,
			) ) {
				_sparql_push_unique_term( next_frontier, next_seen, next_term );
			}
		}
		frontier := next_frontier;
		depth++;
	}
	if ( depth >= min ) {
		for ( let term in frontier ) {
			_sparql_push_unique_term( out, seen, term );
		}
	}
	return out;
}

function _sparql_path_endpoints ( RDFStore store, start, path, graph ) {
	return _sparql_path_direct( store, start, path, graph )
		unless _sparql_is_path_spec(path);
	if ( path{path} eq "alt" ) {
		let out := [];
		for ( let item in path{items} ) {
			for ( let term in _sparql_path_endpoints( store, start, item, graph ) ) {
				out.push(term);
			}
		}
		return out;
	}
	if ( path{path} eq "seq" ) {
		let current := [ start ];
		for ( let item in path{items} ) {
			let next_terms := [];
			for ( let term in current ) {
				for ( let next_term in _sparql_path_endpoints(
					store,
					term,
					item,
					graph,
				) ) {
					next_terms.push(next_term);
				}
			}
			current := next_terms;
		}
		return current;
	}
	if ( path{path} eq "inv" ) {
		return _sparql_path_reverse_endpoints( store, start, path{item}, graph );
	}
	if ( path{path} eq "neg" ) {
		return _sparql_path_negated_step( store, start, path, graph );
	}
	if ( path{path} eq "*" ) {
		return _sparql_path_closure( store, start, path{item}, graph, true );
	}
	if ( path{path} eq "+" ) {
		return _sparql_path_closure( store, start, path{item}, graph, false );
	}
	if ( path{path} eq "?" ) {
		let out := _sparql_path_zero( store, start, graph );
		for ( let term in _sparql_path_endpoints( store, start, path{item}, graph ) ) {
			_sparql_push_unique_term( out, {}, term )
				unless _sparql_term_array_has( out, term );
		}
		return out;
	}
	if ( path{path} eq "{}" ) {
		return _sparql_path_repeat(
			store,
			start,
			path{item},
			graph,
			path{min},
			path{max},
		);
	}
	throw new SPARQLError(message: "SPARQL property path not supported");
}

function _sparql_path_reverse_endpoints ( RDFStore store, start, path, graph ) {
	return _sparql_path_inverse( store, start, path, graph )
		unless _sparql_is_path_spec(path);
	if ( path{path} eq "alt" ) {
		let out := [];
		for ( let item in path{items} ) {
			for ( let term in _sparql_path_reverse_endpoints(
				store,
				start,
				item,
				graph,
			) ) {
				out.push(term);
			}
		}
		return out;
	}
	if ( path{path} eq "seq" ) {
		let current := [ start ];
		let i := path{items}.length() - 1;
		while ( i >= 0 ) {
			let next_terms := [];
			for ( let term in current ) {
				for ( let next_term in _sparql_path_reverse_endpoints(
					store,
					term,
					path{items}[i],
					graph,
				) ) {
					next_terms.push(next_term);
				}
			}
			current := next_terms;
			i--;
		}
		return current;
	}
	if ( path{path} eq "inv" ) {
		return _sparql_path_endpoints( store, start, path{item}, graph );
	}
	if ( path{path} eq "*" ) {
		return _sparql_path_closure( store, start, path{item}, graph, true );
	}
	if ( path{path} eq "+" ) {
		return _sparql_path_closure( store, start, path{item}, graph, false );
	}
	if ( path{path} eq "?" ) {
		let out := _sparql_path_zero( store, start, graph );
		for ( let term in _sparql_path_reverse_endpoints(
			store,
			start,
			path{item},
			graph,
		) ) {
			_sparql_push_unique_term( out, {}, term )
				unless _sparql_term_array_has( out, term );
		}
		return out;
	}
	throw new SPARQLError(message: "SPARQL inverse property path not supported");
}

function _sparql_eval_path_pattern ( RDFStore store, Array bindings,
	Array pattern ) {
	let out := [];
	for ( let binding in bindings ) {
		let s := _binding_term( binding, pattern[0] );
		let o := _binding_term( binding, pattern[2] );
		let g := _binding_term( binding, pattern[3] );
		let zero_path := _sparql_path_can_be_zero(pattern[1]);
		let starts := [];
		if ( s == null or s instanceof Dict ) {
			starts := _sparql_graph_nodes( store, g );
			if ( zero_path and not( pattern[2] instanceof Dict ) and
				not (o == null) and not( o instanceof Dict ) and
				not _sparql_term_array_has( starts, o )
			) {
				starts.push(o);
			}
		}
		else {
			starts := [ s ];
		}
		for ( let start in starts ) {
			let b1 := _bind_match( binding, pattern[0], start );
			next if b1 == null;
			let endpoints := _sparql_path_endpoints(
				store,
				start,
				pattern[1],
				g,
			);
			if ( zero_path and not _sparql_term_array_has( endpoints, start ) ) {
				if ( not( pattern[0] instanceof Dict ) or
					( not( pattern[2] instanceof Dict ) and
						not (o == null) and not( o instanceof Dict ) and
						rdf_term_equals( o, start ) )
				) {
					endpoints.unshift(start);
				}
			}
			for ( let term in endpoints ) {
				next if not (o == null) and not( o instanceof Dict ) and
					not rdf_term_equals( o, term );
				let b2 := _bind_match( b1, pattern[2], term );
				next if b2 == null;
				let b3 := _bind_match( b2, pattern[3], g );
				next if b3 == null;
				out.push(b3);
			}
		}
	}
	return out;
}

function _sparql_eval_patterns ( RDFStore store, Array patterns ) {
	let bindings := [ {} ];
	return _sparql_eval_patterns_from( store, patterns, bindings );
}

function _sparql_eval_patterns_from ( RDFStore store, Array patterns, Array bindings ) {
	let current_bindings := bindings;
	for ( let pattern in patterns ) {
		let next_bindings := [];
		if ( _sparql_is_path_spec(pattern[1]) ) {
			current_bindings := _sparql_eval_path_pattern(
				store,
				current_bindings,
				pattern,
			);
			next;
		}
		for ( let binding in current_bindings ) {
			let s := _binding_term( binding, pattern[0] );
			let p := _binding_term( binding, pattern[1] );
			let o := _binding_term( binding, pattern[2] );
			let g := _binding_term( binding, pattern[3] );
			let rows := _sparql_find_quads(
				store,
				s instanceof Dict ? null : s,
				p instanceof Dict ? null : p,
				o instanceof Dict ? null : o,
				g instanceof Dict and not g.exists("default_graphs") ? null : g,
			);
			for ( let quad in rows ) {
				let b1 := _bind_match( binding, pattern[0], quad.get_subject() );
				next if b1 == null;
				let b2 := _bind_match( b1, pattern[1], quad.get_predicate() );
				next if b2 == null;
				let b3 := _bind_match( b2, pattern[2], quad.get_object() );
				next if b3 == null;
				let b4 := _bind_match( b3, pattern[3], quad.get_graph() );
				next if b4 == null;
				next_bindings.push(b4);
			}
		}
		current_bindings := next_bindings;
	}
	return current_bindings;
}

function _sparql_filter_term ( binding, String raw, Dict prefixes ) {
	let clean := trim(raw);
	if ( starts_with( clean, "?" ) ) {
		return binding.get(substr( clean, 1 ), null);
	}
	return _sparql_expand( clean, prefixes );
}

function _sparql_filter_ok ( binding, String raw, Dict prefixes ) {
	let clean := trim(raw);
	if ( clean ~ /^bound\s*\(\s*\?([A-Za-z][A-Za-z0-9_]*)\s*\)$/i ) {
		let m := clean ~ /^bound\s*\(\s*\?([A-Za-z][A-Za-z0-9_]*)\s*\)$/i;
		return binding.exists(m[1]);
	}
	let op := "";
	let parts := [];
	if ( contains( clean, "!=" ) ) {
		op := "!=";
		parts := split( clean, "!=", 2 );
	}
	else if ( contains( clean, "=" ) ) {
		op := "=";
		parts := split( clean, "=", 2 );
	}
	else {
		throw new SPARQLError(message: "SPARQL FILTER expression not supported");
	}
	let left := _sparql_filter_term( binding, parts[0], prefixes );
	let right := _sparql_filter_term( binding, parts[1], prefixes );
	return false if left == null or right == null;
	let same := rdf_term_equals( left, right );
	return op eq "=" ? same : not same;
}

function _sparql_apply_filters ( Array bindings, Array filters, Dict prefixes ) {
	return bindings if filters.length() == 0;
	let out := [];
	for ( let binding in bindings ) {
		let ok := true;
		for ( let filter in filters ) {
			if ( not _sparql_filter_ok( binding, filter, prefixes ) ) {
				ok := false;
				last;
			}
		}
		out.push(binding) if ok;
	}
	return out;
}

function _sparql_apply_optional ( RDFStore store, Array bindings, Array groups ) {
	let current_bindings := bindings;
	for ( let group in groups ) {
		let out := [];
		for ( let binding in current_bindings ) {
			let matches := _sparql_eval_patterns_from( store, group, [ binding ] );
			if ( matches.length() == 0 ) {
				out.push(binding);
			}
			else {
				for ( let match in matches ) {
					out.push(match);
				}
			}
		}
		current_bindings := out;
	}
	return current_bindings;
}

function _sparql_merge_binding ( binding, row ) {
	let out := {};
	for ( let item_key in binding.keys() ) {
		out.set( item_key, binding.get(item_key) );
	}
	for ( let item_key in row.keys() ) {
		if ( out.exists(item_key) ) {
			let left := out.get(item_key);
			let right := row.get(item_key);
			return null if left == null or right == null;
			return null unless rdf_term_equals( left, right );
		}
		else {
			out.set( item_key, row.get(item_key) );
		}
	}
	return out;
}

function _sparql_apply_values ( Array bindings, Array blocks ) {
	let current_bindings := bindings;
	for ( let block in blocks ) {
		let out := [];
		for ( let binding in current_bindings ) {
			for ( let row in block{rows} ) {
				let merged := _sparql_merge_binding( binding, row );
				out.push(merged) if not (merged == null);
			}
		}
		current_bindings := out;
	}
	return current_bindings;
}

function _sparql_apply_aliases ( Array bindings, Array aliases, Dict prefixes ) {
	return bindings if aliases.length() == 0;
	let out := [];
	for ( let binding in bindings ) {
		let next_binding := _sparql_copy_binding(binding);
		for ( let alias in aliases ) {
			let value := _sparql_eval_expr( next_binding, alias{expr}, prefixes );
			next_binding.set( alias.get("var"), value ) if not (value == null);
		}
		out.push(next_binding);
	}
	return out;
}

function _sparql_non_aggregate_aliases ( Array aliases ) {
	let out := [];
	for ( let alias in aliases ) {
		out.push(alias) unless alias.exists("aggregate");
	}
	return out;
}

function _sparql_aggregate_aliases ( Array aliases ) {
	let out := [];
	for ( let alias in aliases ) {
		out.push(alias) if alias.exists("aggregate");
	}
	return out;
}

function _sparql_group_var_name ( spec ) {
	return spec.get("var") if spec instanceof Dict and spec.exists("var");
	return spec;
}

function _sparql_prepare_group_bindings (
	Array bindings,
	Array vars,
	Dict prefixes
) {
	return bindings if vars.length() == 0;
	let out := [];
	for ( let binding in bindings ) {
		let next_binding := _sparql_copy_binding(binding);
		for ( let spec in vars ) {
			if ( spec instanceof Dict and spec.exists("expr") ) {
				let value := _sparql_eval_expr( next_binding, spec{expr}, prefixes );
				next_binding.set( spec.get("var"), value ) if not (value == null);
			}
		}
		out.push(next_binding);
	}
	return out;
}

function _sparql_group_bindings ( Array bindings, Array vars ) {
	let groups := {};
	let order := [];
	if ( vars.length() == 0 ) {
		groups.set( "", bindings );
		order.push("");
		return { order: order, groups: groups };
	}
	for ( let binding in bindings ) {
		let key := _binding_key( binding, vars );
		if ( not groups.exists(key) ) {
			groups.set( key, [] );
			order.push(key);
		}
		groups.get(key).push(binding);
	}
	return { order: order, groups: groups };
}

function _sparql_group_seed ( Array rows, Array vars ) {
	let out := {};
	return out if rows.length() == 0;
	let first := rows[0];
	for ( let spec in vars ) {
		let var_name := _sparql_group_var_name(spec);
		out.set( var_name, first.get(var_name, null) )
			if first.exists(var_name);
	}
	return out;
}

function _sparql_aggregate_values ( Array rows, alias, Dict prefixes ) {
	let values := [];
	let seen := {};
	for ( let row in rows ) {
		if ( alias{arg} eq "*" ) {
			values.push(true);
			next;
		}
		let value := _sparql_eval_expr( row, alias{arg}, prefixes );
		next if value == null;
		if ( alias{distinct} ) {
			let key := _sparql_is_term(value) ? rdf_term_key(value) : "" _ value;
			next if seen.exists(key);
			seen.set( key, true );
		}
		values.push(value);
	}
	return values;
}

function _sparql_number_literal ( value ) {
	return rdf_literal( "" _ value, "", rdf_iri(XSD_NS _ "integer") );
}

function _sparql_decimal_literal ( value ) {
	return rdf_literal( "" _ value, "", rdf_iri(XSD_NS _ "decimal") );
}

function _sparql_numeric_rank ( value ) {
	return 0 unless value instanceof RDFLiteral;
	let datatype := value.get_datatype().get_value();
	return 3 if datatype eq XSD_NS _ "double" or
		datatype eq XSD_NS _ "float";
	return 2 if datatype eq XSD_NS _ "decimal";
	return 1 if _sparql_is_numeric_datatype(datatype);
	return 0;
}

function _sparql_decimal_lexical ( value, Boolean force_fraction ) {
	let text := sprint( "%.12f", value );
	if ( contains( text, "." ) ) {
		while ( ends_with( text, "0" ) and (
			not force_fraction or not ends_with( text, ".0" )
		) ) {
			text := substr( text, 0, length(text) - 1 );
		}
		text := substr( text, 0, length(text) - 1 )
			if ends_with( text, "." );
	}
	text _= ".0" if force_fraction and not contains( text, "." );
	return text;
}

function _sparql_double_lexical ( value, Boolean avg ) {
	let n := value < 0 ? -value : value;
	let sign := value < 0 ? "-" : "";
	if ( n > 0 and n < 1 ) {
		let exp := 0;
		while ( n < 1 ) {
			n *= 10;
			exp++;
		}
		return sign _ _sparql_decimal_lexical( n, true ) _ "E-" _ exp;
	}
	if ( n >= 10000 ) {
		let exp := 0;
		while ( n >= 10 ) {
			n /= 10;
			exp++;
		}
		return sign _ _sparql_decimal_lexical( n, true ) _ "E" _ exp;
	}
	return _sparql_decimal_lexical( value, false ) _ "E0"
		if avg and value != int(value);
	let text := "" _ value;
	return text if contains( uc(text), "E" );
	return _sparql_decimal_lexical( value, false );
}

function _sparql_numeric_result_literal ( value, Number rank, Boolean avg ) {
	return rdf_literal(
		_sparql_double_lexical(value, avg),
		"",
		rdf_iri(XSD_NS _ "double"),
	) if rank >= 3;
	return rdf_literal(
		_sparql_decimal_lexical(value, avg),
		"",
		rdf_iri(XSD_NS _ "decimal"),
	) if avg or rank == 2;
	return rdf_literal(
		"" _ int(value),
		"",
		rdf_iri(XSD_NS _ "integer"),
	);
}

function _sparql_compare_terms ( left, right ) {
	if ( _sparql_is_numeric_literal(left) and _sparql_is_numeric_literal(right) ) {
		let l := _sparql_safe_numeric_value(left);
		let r := _sparql_safe_numeric_value(right);
		return 0 if l == null or r == null;
		return -1 if l < r;
		return 1 if l > r;
		return 0;
	}
	return _sparql_scalar_string(left) cmp _sparql_scalar_string(right);
}

function _sparql_normalize_ordered_numeric ( value ) {
	return value unless value instanceof RDFLiteral;
	let datatype := value.get_datatype().get_value();
	return value unless datatype eq XSD_NS _ "double" or
		datatype eq XSD_NS _ "float";
	let lexical := value.get_value();
	return value unless contains( uc(lexical), "E" );
	let parts := split( lexical, /[eE]/ );
	return value unless parts.length() == 2;
	return value if contains( parts[0], "." );
	return rdf_literal(
		parts[0] _ ".0E" _ parts[1],
		value.get_lang(),
		value.get_datatype(),
	);
}

function _sparql_eval_aggregate_alias ( Array rows, alias, Dict prefixes ) {
	let values := _sparql_aggregate_values( rows, alias, prefixes );
	let kind := alias{aggregate};
	return _sparql_number_literal(values.length()) if kind eq "COUNT";
	return _sparql_number_literal(0) if values.length() == 0 and
		( kind eq "SUM" or kind eq "AVG" );
	if ( values.length() == 0 ) {
		return null;
	}
	if ( kind eq "SAMPLE" ) {
		return values[0];
	}
	if ( kind eq "GROUP_CONCAT" ) {
		let parts := [];
		for ( let value in values ) {
			parts.push(_sparql_scalar_string(value));
		}
		return rdf_literal(join( alias{separator}, parts ));
	}
	if ( kind eq "MIN" ) {
		let best := values[0];
		for ( let value in values ) {
			best := value if _sparql_compare_terms( value, best ) < 0;
		}
		return _sparql_normalize_ordered_numeric(best);
	}
	if ( kind eq "MAX" ) {
		let best := values[0];
		for ( let value in values ) {
			best := value if _sparql_compare_terms( value, best ) > 0;
		}
		return _sparql_normalize_ordered_numeric(best);
	}
	if ( kind eq "SUM" or kind eq "AVG" ) {
		let total := 0;
		let rank := 1;
		for ( let value in values ) {
			rank := _sparql_numeric_rank(value)
				if _sparql_numeric_rank(value) > rank;
			let n := _sparql_safe_numeric_value(value);
			return null if n == null;
			total += n;
		}
		return _sparql_numeric_result_literal(total / values.length(), rank, true)
			if kind eq "AVG";
		return _sparql_numeric_result_literal(total, rank, false);
	}
	throw new SPARQLError(message: "SPARQL aggregate not supported");
}

function _sparql_eval_aggregate_expr (
	Array rows,
	binding,
	String raw,
	Dict prefixes
) {
	let clean := trim(_sparql_outer_parens(raw));
	let or_op := _sparql_find_top_expr_operator( clean, [ "||" ] );
	if ( not (or_op == null) ) {
		return _sparql_boolean_literal(
			_sparql_effective_boolean(_sparql_eval_aggregate_expr(
				rows,
				binding,
				substr( clean, 0, or_op{pos} ),
				prefixes,
			)) or _sparql_effective_boolean(_sparql_eval_aggregate_expr(
				rows,
				binding,
				substr( clean, or_op{pos} + 2 ),
				prefixes,
			)),
		);
	}
	let and_op := _sparql_find_top_expr_operator( clean, [ "&&" ] );
	if ( not (and_op == null) ) {
		return _sparql_boolean_literal(
			_sparql_effective_boolean(_sparql_eval_aggregate_expr(
				rows,
				binding,
				substr( clean, 0, and_op{pos} ),
				prefixes,
			)) and _sparql_effective_boolean(_sparql_eval_aggregate_expr(
				rows,
				binding,
				substr( clean, and_op{pos} + 2 ),
				prefixes,
			)),
		);
	}
	let compare := _sparql_find_top_expr_operator(
		clean,
		[ "!=", "<=", ">=", "=", "<", ">" ],
	);
	if ( not (compare == null) ) {
		let left := _sparql_eval_aggregate_expr(
			rows,
			binding,
			substr( clean, 0, compare{pos} ),
			prefixes,
		);
		let right := _sparql_eval_aggregate_expr(
			rows,
			binding,
			substr( clean, compare{pos} + length compare{op} ),
			prefixes,
		);
		if ( compare{op} eq "=" or compare{op} eq "!=" ) {
			let same := _sparql_same_value( left, right );
			return _sparql_boolean_literal(compare{op} eq "=" ? same : not same);
		}
		let order := _sparql_order_compare(left, right);
		return _sparql_boolean_literal(order < 0) if compare{op} eq "<";
		return _sparql_boolean_literal(order > 0) if compare{op} eq ">";
		return _sparql_boolean_literal(order <= 0) if compare{op} eq "<=";
		return _sparql_boolean_literal(order >= 0);
	}
	let add := _sparql_find_top_expr_operator( clean, [ "+", "-" ] );
	if ( not (add == null) ) {
		let left := _sparql_eval_aggregate_expr(
			rows,
			binding,
			substr( clean, 0, add{pos} ),
			prefixes,
		);
		let right := _sparql_eval_aggregate_expr(
			rows,
			binding,
			substr( clean, add{pos} + length add{op} ),
			prefixes,
		);
		return null if left == null or right == null;
		return _sparql_numeric_expr_literal( left, right, add{op} );
	}
	let mul := _sparql_find_top_expr_operator( clean, [ "*", "/" ] );
	if ( not (mul == null) ) {
		let left := _sparql_eval_aggregate_expr(
			rows,
			binding,
			substr( clean, 0, mul{pos} ),
			prefixes,
		);
		let right := _sparql_eval_aggregate_expr(
			rows,
			binding,
			substr( clean, mul{pos} + length mul{op} ),
			prefixes,
		);
		return null if left == null or right == null;
		return _sparql_numeric_expr_literal( left, right, mul{op} );
	}
	let aggregate := _sparql_parse_aggregate(clean);
	if ( not (aggregate == null) ) {
		return _sparql_eval_aggregate_alias(
			rows,
			{
				aggregate: aggregate{kind},
				distinct: aggregate{distinct},
				arg: aggregate{arg},
				separator: aggregate{separator},
			},
			prefixes,
		);
	}
	return _sparql_eval_expr( binding, clean, prefixes );
}

function _sparql_apply_aggregates ( Array bindings, Array group_vars,
	Array aliases, Dict prefixes, Array having := [] ) {
	let aggregate_aliases := _sparql_aggregate_aliases(aliases);
	return bindings if aggregate_aliases.length() == 0 and
		group_vars.length() == 0 and having.length() == 0;
	let prepared := _sparql_prepare_group_bindings( bindings, group_vars, prefixes );
	let grouped := _sparql_group_bindings( prepared, group_vars );
	let out := [];
	for ( let key in grouped{order} ) {
		let rows := grouped{groups}.get(key);
		let binding := _sparql_group_seed( rows, group_vars );
		for ( let alias in aggregate_aliases ) {
			let value := _sparql_eval_aggregate_alias( rows, alias, prefixes );
			binding.set( alias.get("var"), value ) if not (value == null);
		}
		for ( let alias in _sparql_non_aggregate_aliases(aliases) ) {
			let value := _sparql_eval_aggregate_expr(
				rows,
				binding,
				alias{expr},
				prefixes,
			);
			binding.set( alias.get("var"), value ) if not (value == null);
		}
		let keep := true;
		for ( let expr in having ) {
			keep := false unless _sparql_effective_boolean(
				_sparql_eval_aggregate_expr( rows, binding, expr, prefixes ),
			);
		}
		next unless keep;
		out.push(binding);
	}
	return out;
}

function _binding_key ( binding, Array vars ) {
	let parts := [];
	for ( let spec in vars ) {
		let var_name := _sparql_group_var_name(spec);
		let value := binding.get(var_name, null);
		parts.push(value == null ? "U|" : rdf_term_key(value));
	}
	return join( "\n", parts );
}

function _sparql_distinct ( Array bindings, Array vars ) {
	let seen := {};
	let out := [];
	for ( let binding in bindings ) {
		let key := _binding_key( binding, vars );
		next if seen.exists(key);
		seen.set( key, true );
		out.push(binding);
	}
	return out;
}

function _sparql_all_vars ( Array bindings ) {
	let seen := {};
	let out := [];
	for ( let binding in bindings ) {
		for ( let var_name in binding.keys() ) {
			next if starts_with( var_name, "__" );
			next if seen.exists(var_name);
			seen.set( var_name, true );
			out.push(var_name);
		}
	}
	return out.sort( fn ( a, b ) -> a cmp b );
}

function _sparql_slice ( Array bindings, offset, limit ) {
	let out := [];
	let start := offset == null ? 0 : offset;
	let stop := limit == null ? bindings.length() : start + limit;
	let i := start;
	while ( i < bindings.length() and i < stop ) {
		out.push(bindings[i]);
		i++;
	}
	return out;
}

function _sparql_fresh_blank ( String label ) {
	_sparql_bnode_counter++;
	return rdf_blank("sparql-construct-" _ _sparql_bnode_counter _ "-" _ label);
}

function _sparql_construct_term ( binding, spec, blanks ) {
	if ( spec instanceof Dict and spec.exists("variable") ) {
		let var_name := spec{variable};
		if ( binding.exists(var_name) ) {
			return binding.get(var_name);
		}
		return null unless starts_with( var_name, "__" );
		let key := "var:" _ var_name;
		blanks.set( key, _sparql_fresh_blank(var_name) )
			unless blanks.exists(key);
		return blanks.get(key);
	}
	if ( spec instanceof RDFBlank ) {
		let key := "blank:" _ spec.get_value();
		blanks.set( key, _sparql_fresh_blank(spec.get_value()) )
			unless blanks.exists(key);
		return blanks.get(key);
	}
	return spec;
}

function _sparql_construct_quads ( String query, parsed, Array bindings ) {
	let text := parsed{template};
	let template := "";
	if ( _sparql_starts_keyword( text, "CONSTRUCT" ) ) {
		let after := trim(substr( text, 9 ));
		if ( starts_with( after, "{" ) ) {
			template := _sparql_extract_braced(after){body};
		}
		else {
			template := parsed{where};
		}
	}
	else {
		template := parsed{where};
	}
	while ( template ~ /FILTER\s*\([^()]*\)/i ) {
		template := replace( template, /FILTER\s*\([^()]*\)/i, "", "" );
	}
	let patterns := _sparql_parse_patterns( template, parsed{prefixes} );
	let out := [];
	let seen := {};
	for ( let binding in bindings ) {
		let blanks := {};
		for ( let pattern in patterns ) {
			let s := _sparql_construct_term( binding, pattern[0], blanks );
			let p := _sparql_construct_term( binding, pattern[1], blanks );
			let o := _sparql_construct_term( binding, pattern[2], blanks );
			let g := _sparql_construct_term( binding, pattern[3], blanks );
			next if s == null or p == null or o == null or g == null;
			next unless _sparql_is_term(s) and _sparql_is_term(p) and
				_sparql_is_term(o) and _sparql_is_term(g);
			let quad := rdf_quad( s, p, o, g );
			let key := rdf_term_key(s) _ "\t" _ rdf_term_key(p) _ "\t" _
				rdf_term_key(o) _ "\t" _ rdf_term_key(g);
			next if seen.exists(key);
			seen.set( key, true );
			out.push(quad);
		}
	}
	return out;
}

function _sparql_describe_add ( RDFStore store, Array quads, seen, term ) {
	if ( not( term instanceof RDFIRI or term instanceof RDFBlank ) ) {
		return null;
	}
	for ( let quad in store.find(term) ) {
		let key := rdf_term_key(quad.get_subject()) _ "\n" _
			rdf_term_key(quad.get_predicate()) _ "\n" _
			rdf_term_key(quad.get_object()) _ "\n" _
			rdf_term_key(quad.get_graph());
		next if seen.exists(key);
		seen.set( key, true );
		quads.push(quad);
	}
}

function _sparql_describe_quads ( RDFStore store, parsed, Array bindings ) {
	let quads := [];
	let seen := {};
	for ( let target in parsed{describe_targets} ) {
		_sparql_describe_add( store, quads, seen, target )
			unless target instanceof Dict;
	}
	for ( let binding in bindings ) {
		if ( parsed{describe_all} ) {
			for ( let item_key in binding.keys() ) {
				_sparql_describe_add(
					store,
					quads,
					seen,
					binding.get(item_key),
				);
			}
		}
		for ( let target in parsed{describe_targets} ) {
			if ( target instanceof Dict and target.exists("variable") ) {
				_sparql_describe_add(
					store,
					quads,
					seen,
					binding.get( target{variable}, null ),
				);
			}
		}
	}
	return quads;
}

function _sparql_is_update_ast ( String source ) {
	return sparql_parse_ast(source){type} eq "update";
}

function _sparql_top_level_keyword_pos ( String text, String keyword ) {
	let quoted := false;
	let iri := false;
	let braces := 0;
	let parens := 0;
	let brackets := 0;
	let i := 0;
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		if ( quoted ) {
			quoted := false if ch eq "\"" and substr( text, i - 1, 1 ) ne "\\";
			i++;
			next;
		}
		if ( iri ) {
			iri := false if ch eq ">";
			i++;
			next;
		}
		if ( ch eq "\"" ) {
			quoted := true;
			i++;
			next;
		}
		if ( _sparql_is_iri_start( text, i ) ) {
			iri := true;
			i++;
			next;
		}
		braces++ if ch eq "{";
		braces-- if ch eq "}";
		parens++ if ch eq "(";
		parens-- if ch eq ")";
		brackets++ if ch eq "[";
		brackets-- if ch eq "]";
		return i if braces == 0 and parens == 0 and brackets == 0 and
			_sparql_starts_keyword( substr( text, i ), keyword );
		i++;
	}
	return -1;
}

function _sparql_split_update_operations ( String source ) {
	let text := _sparql_prefixes(source){body};
	let out := [];
	let token := "";
	let quoted := false;
	let iri := false;
	let braces := 0;
	let parens := 0;
	let brackets := 0;
	let i := 0;
	while ( i < length text ) {
		let ch := substr( text, i, 1 );
		if ( quoted ) {
			token _= ch;
			quoted := false if ch eq "\"" and substr( text, i - 1, 1 ) ne "\\";
			i++;
			next;
		}
		if ( iri ) {
			token _= ch;
			iri := false if ch eq ">";
			i++;
			next;
		}
		if ( ch eq "\"" ) {
			quoted := true;
			token _= ch;
			i++;
			next;
		}
		if ( _sparql_is_iri_start( text, i ) ) {
			iri := true;
			token _= ch;
			i++;
			next;
		}
		braces++ if ch eq "{";
		braces-- if ch eq "}";
		parens++ if ch eq "(";
		parens-- if ch eq ")";
		brackets++ if ch eq "[";
		brackets-- if ch eq "]";
		if ( ch eq ";" and braces == 0 and parens == 0 and brackets == 0 ) {
			out.push(trim(token)) if trim(token) ne "";
			token := "";
			i++;
			next;
		}
		token _= ch;
		i++;
	}
	out.push(trim(token)) if trim(token) ne "";
	return out;
}

function _sparql_update_silent ( Dict op ) {
	let text := trim(op{text});
	let parts := split( text, " ", 2 );
	let head := uc(parts[0]);
	let rest := parts.length() > 1 ? trim(parts[1]) : "";
	if ( _sparql_starts_keyword( rest, "SILENT" ) ) {
		op{text} := head _ " " _ trim(substr( rest, 6 ));
		op{silent} := true;
	}
	return op;
}

function _sparql_update_parse_with ( String text, Dict prefixes ) {
	let clean := trim(text);
	if ( not _sparql_starts_keyword( clean, "WITH" ) ) {
		return { graph: null, text: clean };
	}
	let rest := trim(substr( clean, 4 ));
	throw new SPARQLError(message: "SPARQL Update WITH graph expected")
		unless rest ~ /^(\S+)\s+([\s\S]+)$/;
	let m := rest ~ /^(\S+)\s+([\s\S]+)$/;
	let graph := _sparql_expand( m[1], prefixes );
	throw new SPARQLError(message: "SPARQL Update WITH expects graph IRI")
		unless graph instanceof RDFIRI;
	return { graph: graph, text: trim(m[2]) };
}

function _sparql_update_graph_operand ( String text, Dict prefixes ) {
	let clean := trim(text);
	return rdf_default_graph() if _sparql_starts_keyword( clean, "DEFAULT" );
	if ( _sparql_starts_keyword( clean, "GRAPH" ) ) {
		clean := trim(replace( clean, /^GRAPH\s+/i, "", "" ));
	}
	let graph := _sparql_expand( clean, prefixes );
	throw new SPARQLError(message: "SPARQL Update graph operand must be an IRI")
		unless graph instanceof RDFIRI;
	return graph;
}

function _sparql_update_template_patterns ( String text, Dict prefixes,
	graph := null ) {
	let patterns := [];
	let rest := trim(text);
	while ( rest ~ /GRAPH\s+([^\s]+)\s*\{([^{}]*)\}/i ) {
		let m := rest ~ /GRAPH\s+([^\s]+)\s*\{([^{}]*)\}/i;
		let graph_spec := _sparql_expand( m[1], prefixes );
		for ( let pattern in _sparql_parse_patterns(
			m[2],
			prefixes,
			graph_spec,
		) ) {
			patterns.push(pattern);
		}
		rest := trim(replace(
			rest,
			/GRAPH\s+[^\s]+\s*\{[^{}]*\}/i,
			"",
			"",
		));
	}
	if ( rest ne "" ) {
		for ( let pattern in _sparql_parse_patterns( rest, prefixes, graph ) ) {
			patterns.push(pattern);
		}
	}
	return patterns;
}

function _sparql_update_has_blank_template ( pattern ) {
	return pattern[0] instanceof RDFBlank or pattern[1] instanceof RDFBlank or
		pattern[2] instanceof RDFBlank or pattern[3] instanceof RDFBlank;
}

function _sparql_update_construct_quads ( String text, Dict prefixes,
	Array bindings, graph := null, Boolean delete_template := false ) {
	let out := [];
	let seen := {};
	let patterns := _sparql_update_template_patterns( text, prefixes, graph );
	for ( let pattern in patterns ) {
		throw new SPARQLError(
			message: "SPARQL Update DELETE templates cannot contain blank nodes",
		) if delete_template and _sparql_update_has_blank_template(pattern);
	}
	for ( let binding in bindings ) {
		let blanks := {};
		for ( let pattern in patterns ) {
			let s := _sparql_construct_term( binding, pattern[0], blanks );
			let p := _sparql_construct_term( binding, pattern[1], blanks );
			let o := _sparql_construct_term( binding, pattern[2], blanks );
			let g := _sparql_construct_term( binding, pattern[3], blanks );
			next if s == null or p == null or o == null or g == null;
			throw new SPARQLError(message: "SPARQL Update template term expected")
				unless _sparql_is_term(s) and _sparql_is_term(p) and
				_sparql_is_term(o) and _sparql_is_term(g);
			let key := rdf_term_key(s) _ "\n" _ rdf_term_key(p) _ "\n" _
				rdf_term_key(o) _ "\n" _ rdf_term_key(g);
			next if seen.exists(key);
			seen.set( key, true );
			out.push(rdf_quad( s, p, o, g ));
		}
	}
	return out;
}

function _sparql_update_regraph ( Array quads, graph ) {
	return quads if graph == null;
	let out := [];
	for ( let quad in quads ) {
		out.push(rdf_quad(
			quad.get_subject(),
			quad.get_predicate(),
			quad.get_object(),
			graph,
		));
	}
	return out;
}

function _sparql_update_load_text ( String iri ) {
	if ( starts_with( lc(iri), "http://" ) or starts_with( lc(iri), "https://" ) ) {
		let ua := new UserAgent();
		return to_string(
			ua.build_request( "GET", iri )
				.header(
					"Accept",
					"text/turtle, application/n-triples, application/n-quads, application/rdf+xml",
				)
				.send(ua)
				.expect_success()
				.content(),
		);
	}
	let path := starts_with( lc(iri), "file://" )
		? _url_unescape(substr( iri, 7 ))
		: iri;
	return ( new Path(path) ).slurp_utf8();
}

function _sparql_update_load_quads ( String iri, graph ) {
	let text := _sparql_update_load_text(iri);
	let lower := lc(iri);
	let quads := [];
	if ( ends_with( lower, ".nq" ) or ends_with( lower, ".nquads" ) ) {
		quads := ( new NQuadsParser() ).parse_string(text);
	}
	else if ( ends_with( lower, ".nt" ) or ends_with( lower, ".ntriples" ) ) {
		quads := ( new NTriplesParser() ).parse_string(text);
	}
	else if ( ends_with( lower, ".rdf" ) or ends_with( lower, ".xml" ) ) {
		quads := ( new RdfXmlParser() ).parse_string(text, base: iri);
	}
	else {
		quads := ( new TurtleParser() ).parse_string(text, base: iri);
	}
	return _sparql_update_regraph( quads, graph );
}

function _sparql_update_data ( RDFStore store, String text, Dict prefixes,
	Boolean delete_data ) {
	let item := _sparql_extract_braced(text);
	let quads := _sparql_update_construct_quads(
		item{body},
		prefixes,
		[ {} ],
		null,
		delete_data,
	);
	if ( delete_data ) {
		store.remove_quads(quads);
	}
	else {
		store.add_quads(quads);
	}
	return {
		operation: delete_data ? "delete-data" : "insert-data",
		quads: quads.length(),
	};
}

function _sparql_update_clear ( RDFStore store, String text, Dict prefixes,
	String operation, Boolean silent ) {
	let rest := trim(replace( text, /^[A-Za-z]+\s*/i, "", "" ));
	rest := trim(substr( rest, 6 )) if _sparql_starts_keyword( rest, "SILENT" );
	if ( _sparql_starts_keyword( rest, "DEFAULT" ) ) {
		store.clear_graph();
		return { operation: lc(operation), graphs: 1 };
	}
	if ( _sparql_starts_keyword( rest, "ALL" ) ) {
		store.clear();
		return { operation: lc(operation), graphs: 0 };
	}
	if ( _sparql_starts_keyword( rest, "NAMED" ) ) {
		let names := store.graph_names();
		for ( let graph in names ) {
			store.clear_graph(graph);
		}
		return { operation: lc(operation), graphs: names.length() };
	}
	if ( _sparql_starts_keyword( rest, "GRAPH" ) ) {
		store.clear_graph(_sparql_update_graph_operand( rest, prefixes ));
		return { operation: lc(operation), graphs: 1 };
	}
	throw new SPARQLError(message: "SPARQL Update " _ operation _ " target expected")
		unless silent;
	return { operation: lc(operation), graphs: 0, silent: true };
}

function _sparql_update_graph_management ( RDFStore store, String text,
	Dict prefixes, String operation, Boolean silent ) {
	let rest := trim(replace( text, /^[A-Za-z]+\s*/i, "", "" ));
	rest := trim(substr( rest, 6 )) if _sparql_starts_keyword( rest, "SILENT" );
	if ( operation eq "CREATE" and _sparql_starts_keyword( rest, "GRAPH" ) ) {
		let graph := _sparql_update_graph_operand( rest, prefixes );
		if ( store.graph_exists(graph) and not silent ) {
			throw new SPARQLError(message: "SPARQL Update graph already exists");
		}
		return { operation: "create", graphs: 1, silent: silent };
	}
	if ( not ( rest ~ /^(DEFAULT|GRAPH\s+\S+)\s+TO\s+(DEFAULT|GRAPH\s+\S+)$/i ) ) {
		throw new SPARQLError(message: "SPARQL Update graph target expected")
			unless silent;
		return { operation: lc(operation), graphs: 0, silent: true };
	}
	let m := rest ~ /^(DEFAULT|GRAPH\s+\S+)\s+TO\s+(DEFAULT|GRAPH\s+\S+)$/i;
	let source := _sparql_update_graph_operand( m[1], prefixes );
	let target := _sparql_update_graph_operand( m[2], prefixes );
	if ( operation eq "ADD" ) {
		store.add_graph( source, target );
	}
	else if ( operation eq "COPY" ) {
		store.copy_graph( source, target );
	}
	else if ( operation eq "MOVE" ) {
		store.move_graph( source, target );
	}
	return { operation: lc(operation), graphs: 2, silent: silent };
}

function _sparql_update_load ( RDFStore store, String text, Dict prefixes,
	Boolean silent ) {
	let rest := trim(substr( text, 4 ));
	rest := trim(substr( rest, 6 )) if _sparql_starts_keyword( rest, "SILENT" );
	let target := null;
	let p := _sparql_top_level_keyword_pos( rest, "INTO" );
	if ( p >= 0 ) {
		target := _sparql_update_graph_operand(
			trim(substr( rest, p + 4 )),
			prefixes,
		);
		rest := trim(substr( rest, 0, p ));
	}
	let source := _sparql_expand( rest, prefixes );
	if ( not( source instanceof RDFIRI ) ) {
		throw new SPARQLError(message: "SPARQL Update LOAD source must be an IRI")
			unless silent;
		return { operation: "load", quads: 0, silent: true };
	}
	try {
		let quads := _sparql_update_load_quads( source.get_value(), target );
		store.add_quads(quads);
		return { operation: "load", quads: quads.length(), silent: silent };
	}
	catch ( Exception e ) {
		if ( not silent ) {
			throw e;
		}
		return { operation: "load", quads: 0, silent: true };
	}
}

function _sparql_update_modify ( RDFStore store, String text, Dict prefixes ) {
	let with_info := _sparql_update_parse_with( text, prefixes );
	let clean := with_info{text};
	let delete_body := "";
	let insert_body := "";
	let rest := clean;
	if ( _sparql_starts_keyword( rest, "DELETE" ) ) {
		let item := _sparql_extract_braced(substr( rest, 6 ));
		delete_body := item{body};
		rest := item{tail};
		if ( _sparql_starts_keyword( rest, "INSERT" ) ) {
			let insert_item := _sparql_extract_braced(substr( rest, 6 ));
			insert_body := insert_item{body};
			rest := insert_item{tail};
		}
	}
	else if ( _sparql_starts_keyword( rest, "INSERT" ) ) {
		let item := _sparql_extract_braced(substr( rest, 6 ));
		insert_body := item{body};
		rest := item{tail};
	}
	throw new SPARQLError(message: "SPARQL Update WHERE block expected")
		unless _sparql_starts_keyword( rest, "WHERE" );
	let where := _sparql_extract_braced(substr( rest, 5 ));
	let bindings := _sparql_eval_group_text(
		store,
		where{body},
		prefixes,
		[ {} ],
		with_info{graph},
	);
	let deleted := [];
	if ( delete_body ne "" ) {
		deleted := _sparql_update_construct_quads(
			delete_body,
			prefixes,
			bindings,
			with_info{graph},
			true,
		);
		store.remove_quads(deleted);
	}
	let inserted := [];
	if ( insert_body ne "" ) {
		inserted := _sparql_update_construct_quads(
			insert_body,
			prefixes,
			bindings,
			with_info{graph},
		);
		store.add_quads(inserted);
	}
	return {
		operation: "modify",
		bindings: bindings.length(),
		deleted: deleted.length(),
		inserted: inserted.length(),
	};
}

function _sparql_update_delete_where ( RDFStore store, String text,
	Dict prefixes ) {
	let body := _sparql_extract_braced(substr( text, 12 )){body};
	let bindings := _sparql_eval_group_text( store, body, prefixes, [ {} ] );
	let quads := _sparql_update_construct_quads(
		body,
		prefixes,
		bindings,
		null,
		true,
	);
	store.remove_quads(quads);
	return {
		operation: "delete-where",
		bindings: bindings.length(),
		deleted: quads.length(),
	};
}

function _sparql_update_operation ( RDFStore store, String operation,
	Dict prefixes ) {
	let clean := trim(operation);
	let upper := uc(clean);
	if ( _sparql_starts_keyword( upper, "INSERT DATA" ) ) {
		return _sparql_update_data(
			store,
			trim(substr( clean, 11 )),
			prefixes,
			false,
		);
	}
	if ( _sparql_starts_keyword( upper, "DELETE DATA" ) ) {
		return _sparql_update_data(
			store,
			trim(substr( clean, 11 )),
			prefixes,
			true,
		);
	}
	if ( _sparql_starts_keyword( upper, "DELETE WHERE" ) ) {
		return _sparql_update_delete_where( store, clean, prefixes );
	}
	if ( _sparql_starts_keyword( upper, "CLEAR" ) ) {
		let op := _sparql_update_silent({ text: clean, silent: false });
		return _sparql_update_clear( store, op{text}, prefixes, "CLEAR", op{silent} );
	}
	if ( _sparql_starts_keyword( upper, "DROP" ) ) {
		let op := _sparql_update_silent({ text: clean, silent: false });
		return _sparql_update_clear( store, op{text}, prefixes, "DROP", op{silent} );
	}
	if ( _sparql_starts_keyword( upper, "CREATE" ) or
		_sparql_starts_keyword( upper, "ADD" ) or
		_sparql_starts_keyword( upper, "COPY" ) or
		_sparql_starts_keyword( upper, "MOVE" )
	) {
		let keyword := split( upper, " ", 2 )[0];
		let op := _sparql_update_silent({ text: clean, silent: false });
		return _sparql_update_graph_management(
			store,
			op{text},
			prefixes,
			keyword,
			op{silent},
		);
	}
	if ( _sparql_starts_keyword( upper, "LOAD" ) ) {
		let op := _sparql_update_silent({ text: clean, silent: false });
		return _sparql_update_load( store, op{text}, prefixes, op{silent} );
	}
	if ( _sparql_starts_keyword( upper, "WITH" ) or
		_sparql_starts_keyword( upper, "DELETE" ) or
		_sparql_starts_keyword( upper, "INSERT" )
	) {
		return _sparql_update_modify( store, clean, prefixes );
	}
	throw new SPARQLError(message: "SPARQL Update operation not supported");
}

function sparql_update ( RDFStore store, String update ) {
	let ast := sparql_parse_ast(update);
	throw new SPARQLError(message: "SPARQL Update expected")
		unless ast{type} eq "update";
	let prep := _sparql_prefixes(update);
	let operations := [];
	return store.with_transaction(function ( tx ) {
		for ( let operation in _sparql_split_update_operations(update) ) {
			operations.push(_sparql_update_operation(
				tx,
				operation,
				prep{prefixes},
			));
		}
		return {
			type: "update",
			operations: operations,
			count: operations.length(),
		};
	});
}

class SPARQLPreparedQuery {
	let String source with get := "";
	let ast with get := null;

	method __build__ () {
		ast := sparql_parse_ast(source);
		throw new SPARQLError(message: "SPARQL query expected")
			if ast{type} eq "update";
	}

	method execute ( RDFStore store, inherited_dataset := null ) {
		return sparql_query( store, source, inherited_dataset );
	}
}

class SPARQLPreparedUpdate {
	let String source with get := "";
	let ast with get := null;

	method __build__ () {
		ast := sparql_parse_ast(source);
		throw new SPARQLError(message: "SPARQL Update expected")
			unless ast{type} eq "update";
	}

	method execute ( RDFStore store ) {
		return sparql_update( store, source );
	}
}

function sparql_prepare_query ( String query ) {
	return new SPARQLPreparedQuery(source: query);
}

function sparql_prepare_update ( String update ) {
	return new SPARQLPreparedUpdate(source: update);
}

function sparql_validate ( String source ) {
	try {
		let ast := sparql_parse_ast(source);
		return {
			ok: true,
			type: ast{type},
			syntax: ast{syntax},
			error: null,
		};
	}
	catch ( Exception e ) {
		return {
			ok: false,
			type: "",
			syntax: "",
			error: e.to_String(),
		};
	}
}

function sparql_diagnose ( RDFStore store, String source ) {
	let validation := sparql_validate(source);
	return validation unless validation{ok};
	try {
		let result := validation{type} eq "update"
			? sparql_update( store, source )
			: sparql_query( store, source );
		validation{result} := result;
		return validation;
	}
	catch ( Exception e ) {
		validation{ok} := false;
		validation{error} := e.to_String();
		return validation;
	}
}

function sparql_query ( RDFStore store, String query, inherited_dataset := null ) {
	throw new SPARQLError(message: "SPARQL query expected; use sparql_update")
		if _sparql_is_update_ast(query);
	let parsed := _sparql_parse(query);
	if ( not (inherited_dataset == null) ) {
		if ( parsed{dataset_default}.length() == 0 ) {
			parsed{dataset_default} := inherited_dataset.get( "default", [] );
		}
		if ( parsed{dataset_named}.length() == 0 ) {
			parsed{dataset_named} := inherited_dataset.get( "named", [] );
		}
	}
	parsed{prefixes}.set( "__dataset_default", parsed{dataset_default} );
	parsed{prefixes}.set( "__dataset_named", parsed{dataset_named} );
	let bindings := [];
	if ( parsed{dataset_default}.length() > 0 ) {
		bindings := _sparql_eval_group_text(
			store,
			parsed{where},
			parsed{prefixes},
			[ {} ],
			{ default_graphs: parsed{dataset_default} },
		);
	}
	else {
		bindings := _sparql_eval_group_text(
			store,
			parsed{where},
			parsed{prefixes},
			[ {} ],
		);
	}
	bindings := _sparql_apply_aliases(
		bindings,
		_sparql_non_aggregate_aliases(parsed{aliases}),
		parsed{prefixes},
	);
	bindings := _sparql_apply_values( bindings, parsed{values} );
	bindings := _sparql_apply_aggregates(
		bindings,
		parsed{group_by},
		parsed{aliases},
		parsed{prefixes},
		parsed{having},
	);
	if ( parsed{select_all} ) {
		parsed{vars} := _sparql_all_vars(bindings);
	}
	if ( parsed{order_by}.length() > 0 ) {
		let order_items := parsed{order_by};
		let prefixes := parsed{prefixes};
		bindings := bindings.sort( function ( left, right ) {
			for ( let item in order_items ) {
				let l := _sparql_eval_expr( left, item{expr}, prefixes );
				let r := _sparql_eval_expr( right, item{expr}, prefixes );
				let lk := l == null ? "" : rdf_term_key(l);
				let rk := r == null ? "" : rdf_term_key(r);
				if ( lk ne rk ) {
					return rk cmp lk if item{desc};
					return lk cmp rk;
				}
			}
			return 0;
		});
	}
	bindings := _sparql_distinct( bindings, parsed{vars} )
		if parsed{distinct};
	bindings := _sparql_slice( bindings, parsed{offset}, parsed{limit} );
	if ( parsed{kind} eq "ask" ) {
		return { type: "ask", boolean: bindings.length() > 0 };
	}
	if ( parsed{kind} eq "construct" ) {
		return {
			type: "construct",
			quads: _sparql_construct_quads( query, parsed, bindings ),
		};
	}
	if ( parsed{kind} eq "describe" ) {
		return {
			type: "describe",
			quads: _sparql_describe_quads( store, parsed, bindings ),
		};
	}
	let rows := [];
	for ( let binding in bindings ) {
		let row := {};
		for ( let var_name in parsed{vars} ) {
			row.set( var_name, binding.get(var_name, null) );
		}
		rows.push(row);
	}
	return { type: "select", variables: parsed{vars}, results: rows };
}