[tracker/sparql-refactor: 7/16] SPARQL: Move expression and pattern processing to separate files



commit 5ed47e2111e87e5a7f42e25f8be0e8c808bdfdd7
Author: Jürg Billeter <j bitron ch>
Date:   Fri Mar 26 16:52:36 2010 +0100

    SPARQL: Move expression and pattern processing to separate files

 src/libtracker-data/.gitignore                     |    2 +
 src/libtracker-data/Makefile.am                    |    2 +
 src/libtracker-data/tracker-sparql-expression.vala | 1087 +++++++++
 src/libtracker-data/tracker-sparql-pattern.vala    | 1403 ++++++++++++
 src/libtracker-data/tracker-sparql-query.vala      | 2421 +-------------------
 5 files changed, 2544 insertions(+), 2371 deletions(-)
---
diff --git a/src/libtracker-data/.gitignore b/src/libtracker-data/.gitignore
index 7b4ad3b..8ebc675 100644
--- a/src/libtracker-data/.gitignore
+++ b/src/libtracker-data/.gitignore
@@ -1,3 +1,5 @@
+tracker-sparql-expression.c
+tracker-sparql-pattern.c
 tracker-sparql-query.c
 tracker-sparql-query.h
 tracker-sparql-scanner.c
diff --git a/src/libtracker-data/Makefile.am b/src/libtracker-data/Makefile.am
index f12e8fa..f2ed878 100644
--- a/src/libtracker-data/Makefile.am
+++ b/src/libtracker-data/Makefile.am
@@ -18,6 +18,8 @@ libtracker_datadir = $(libdir)/tracker-$(TRACKER_API_VERSION)
 libtracker_data_LTLIBRARIES = libtracker-data.la
 
 libtracker_data_la_VALASOURCES = 					\
+	tracker-sparql-expression.vala					\
+	tracker-sparql-pattern.vala					\
 	tracker-sparql-query.vala					\
 	tracker-sparql-scanner.vala					\
 	tracker-turtle-reader.vala
diff --git a/src/libtracker-data/tracker-sparql-expression.vala b/src/libtracker-data/tracker-sparql-expression.vala
new file mode 100644
index 0000000..58be8eb
--- /dev/null
+++ b/src/libtracker-data/tracker-sparql-expression.vala
@@ -0,0 +1,1087 @@
+/*
+ * Copyright (C) 2008-2010, Nokia
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA  02110-1301, USA.
+ */
+
+class Tracker.Sparql.Expression : Object {
+	Query query;
+
+	const string XSD_NS = "http://www.w3.org/2001/XMLSchema#";;
+	const string FN_NS = "http://www.w3.org/2005/xpath-functions#";;
+	const string FTS_NS = "http://www.tracker-project.org/ontologies/fts#";;
+	const string TRACKER_NS = "http://www.tracker-project.org/ontologies/tracker#";;
+
+	public Expression (Query query) {
+		this.query = query;
+	}
+
+	Context context {
+		get { return query.context; }
+		set { query.context = value; }
+	}
+
+	Pattern pattern {
+		get { return query.pattern; }
+	}
+
+	inline bool next () throws SparqlError {
+		return query.next ();
+	}
+
+	inline SparqlTokenType current () {
+		return query.current ();
+	}
+
+	inline SparqlTokenType last () {
+		return query.last ();
+	}
+
+	inline bool accept (SparqlTokenType type) throws SparqlError {
+		return query.accept (type);
+	}
+
+	SparqlError get_error (string msg) {
+		return query.get_error (msg);
+	}
+
+	SparqlError get_internal_error (string msg) {
+		return query.get_internal_error (msg);
+	}
+
+	bool expect (SparqlTokenType type) throws SparqlError {
+		return query.expect (type);
+	}
+
+	SourceLocation get_location () {
+		return query.get_location ();
+	}
+
+	void set_location (SourceLocation location) {
+		query.set_location (location);
+	}
+
+	string get_last_string (int strip = 0) {
+		return query.get_last_string (strip);
+	}
+
+	string escape_sql_string_literal (string literal) {
+		return "'%s'".printf (string.joinv ("''", literal.split ("'")));
+	}
+
+	bool maybe_numeric (PropertyType type) {
+		return (type == PropertyType.INTEGER || type == PropertyType.DOUBLE || type == PropertyType.DATETIME || type == PropertyType.UNKNOWN);
+	}
+
+	void skip_bracketted_expression () throws SparqlError {
+		expect (SparqlTokenType.OPEN_PARENS);
+		while (true) {
+			switch (current ()) {
+			case SparqlTokenType.OPEN_PARENS:
+				// skip nested bracketted expression
+				skip_bracketted_expression ();
+				continue;
+			case SparqlTokenType.CLOSE_PARENS:
+			case SparqlTokenType.EOF:
+				break;
+			default:
+				next ();
+				continue;
+			}
+			break;
+		}
+		expect (SparqlTokenType.CLOSE_PARENS);
+	}
+
+	internal void skip_select_variables () throws SparqlError {
+		while (true) {
+			switch (current ()) {
+			case SparqlTokenType.OPEN_PARENS:
+				skip_bracketted_expression ();
+				continue;
+			case SparqlTokenType.FROM:
+			case SparqlTokenType.WHERE:
+			case SparqlTokenType.OPEN_BRACE:
+			case SparqlTokenType.GROUP:
+			case SparqlTokenType.ORDER:
+			case SparqlTokenType.LIMIT:
+			case SparqlTokenType.OFFSET:
+			case SparqlTokenType.EOF:
+				break;
+			default:
+				next ();
+				continue;
+			}
+			break;
+		}
+	}
+
+	internal PropertyType translate_select_expression (StringBuilder sql, bool subquery) throws SparqlError {
+		Variable variable = null;
+
+		long begin = sql.len;
+		var type = PropertyType.UNKNOWN;
+		if (accept (SparqlTokenType.COUNT)) {
+			sql.append ("COUNT(");
+			translate_aggregate_expression (sql);
+			sql.append (")");
+			type = PropertyType.INTEGER;
+		} else if (accept (SparqlTokenType.SUM)) {
+			sql.append ("SUM(");
+			type = translate_aggregate_expression (sql);
+			sql.append (")");
+		} else if (accept (SparqlTokenType.AVG)) {
+			sql.append ("AVG(");
+			type = translate_aggregate_expression (sql);
+			sql.append (")");
+		} else if (accept (SparqlTokenType.MIN)) {
+			sql.append ("MIN(");
+			type = translate_aggregate_expression (sql);
+			sql.append (")");
+		} else if (accept (SparqlTokenType.MAX)) {
+			sql.append ("MAX(");
+			type = translate_aggregate_expression (sql);
+			sql.append (")");
+		} else if (accept (SparqlTokenType.GROUP_CONCAT)) {
+			sql.append ("GROUP_CONCAT(");
+			expect (SparqlTokenType.OPEN_PARENS);
+			translate_expression_as_string (sql);
+			sql.append (", ");
+			expect (SparqlTokenType.COMMA);
+			sql.append (escape_sql_string_literal (parse_string_literal ()));
+			sql.append (")");
+			expect (SparqlTokenType.CLOSE_PARENS);
+			type = PropertyType.STRING;
+		} else if (current () == SparqlTokenType.VAR) {
+			type = translate_expression (sql);
+			// we need variable name in case of compositional subqueries
+			variable = context.get_variable (get_last_string ().substring (1));
+
+			if (variable.binding == null) {
+				throw get_error ("use of undefined variable `%s'".printf (variable.name));
+			}
+		} else {
+			type = translate_expression (sql);
+		}
+
+		if (!subquery) {
+			convert_expression_to_string (sql, type, begin);
+			type = PropertyType.STRING;
+		}
+
+		if (accept (SparqlTokenType.AS)) {
+			if (accept (SparqlTokenType.PN_PREFIX)) {
+				// deprecated but supported for backward compatibility
+				// (...) AS foo
+				variable = context.get_variable (get_last_string ());
+			} else {
+				// syntax from SPARQL 1.1 Draft
+				// (...) AS ?foo
+				expect (SparqlTokenType.VAR);
+				variable = context.get_variable (get_last_string ().substring (1));
+			}
+			sql.append_printf (" AS %s", variable.sql_expression);
+
+			if (subquery) {
+				var binding = new VariableBinding ();
+				binding.data_type = type;
+				binding.variable = variable;
+				binding.sql_expression = variable.sql_expression;
+				pattern.add_variable_binding (new StringBuilder (), binding, VariableState.BOUND);
+			}
+		}
+
+		if (variable != null) {
+			int state = context.var_set.lookup (variable);
+			if (state == 0) {
+				state = VariableState.BOUND;
+			}
+			context.select_var_set.insert (variable, state);
+		}
+
+		return type;
+	}
+
+	void translate_expression_as_order_condition (StringBuilder sql) throws SparqlError {
+		long begin = sql.len;
+		if (translate_expression (sql) == PropertyType.RESOURCE) {
+			// ID => Uri
+			sql.insert (begin, "(SELECT Uri FROM Resource WHERE ID = ");
+			sql.append (")");
+		}
+	}
+
+	internal void translate_order_condition (StringBuilder sql) throws SparqlError {
+		if (accept (SparqlTokenType.ASC)) {
+			translate_expression_as_order_condition (sql);
+			sql.append (" ASC");
+		} else if (accept (SparqlTokenType.DESC)) {
+			translate_expression_as_order_condition (sql);
+			sql.append (" DESC");
+		} else {
+			translate_expression_as_order_condition (sql);
+		}
+	}
+
+	void translate_bound_call (StringBuilder sql) throws SparqlError {
+		expect (SparqlTokenType.BOUND);
+		expect (SparqlTokenType.OPEN_PARENS);
+		sql.append ("(");
+		translate_expression (sql);
+		sql.append (" IS NOT NULL)");
+		expect (SparqlTokenType.CLOSE_PARENS);
+	}
+
+	void translate_regex (StringBuilder sql) throws SparqlError {
+		expect (SparqlTokenType.REGEX);
+		expect (SparqlTokenType.OPEN_PARENS);
+		sql.append ("SparqlRegex(");
+		translate_expression_as_string (sql);
+		sql.append (", ");
+		expect (SparqlTokenType.COMMA);
+		translate_expression (sql);
+		sql.append (", ");
+		if (accept (SparqlTokenType.COMMA)) {
+			translate_expression (sql);
+		} else {
+			sql.append ("''");
+		}
+		sql.append (")");
+		expect (SparqlTokenType.CLOSE_PARENS);
+	}
+
+	internal static void append_expression_as_string (StringBuilder sql, string expression, PropertyType type) {
+		long begin = sql.len;
+		sql.append (expression);
+		convert_expression_to_string (sql, type, begin);
+	}
+
+	static void convert_expression_to_string (StringBuilder sql, PropertyType type, long begin) {
+		switch (type) {
+		case PropertyType.STRING:
+		case PropertyType.INTEGER:
+			// nothing to convert
+			break;
+		case PropertyType.RESOURCE:
+			// ID => Uri
+			sql.insert (begin, "(SELECT Uri FROM Resource WHERE ID = ");
+			sql.append (")");
+			break;
+		case PropertyType.BOOLEAN:
+			// 0/1 => false/true
+			sql.insert (begin, "CASE ");
+			sql.append (" WHEN 1 THEN 'true' WHEN 0 THEN 'false' ELSE NULL END");
+			break;
+		case PropertyType.DATETIME:
+			// ISO 8601 format
+			sql.insert (begin, "strftime (\"%Y-%m-%dT%H:%M:%SZ\", ");
+			sql.append (", \"unixepoch\")");
+			break;
+		default:
+			// let sqlite convert the expression to string
+			sql.insert (begin, "CAST (");
+			sql.append (" AS TEXT)");
+			break;
+		}
+	}
+
+	void translate_expression_as_string (StringBuilder sql) throws SparqlError {
+		switch (current ()) {
+		case SparqlTokenType.IRI_REF:
+		case SparqlTokenType.PN_PREFIX:
+		case SparqlTokenType.COLON:
+			// handle IRI literals separately as it wouldn't work for unknown IRIs otherwise
+			var binding = new LiteralBinding ();
+			bool is_var;
+			binding.literal = pattern.parse_var_or_term (null, out is_var);
+			if (accept (SparqlTokenType.OPEN_PARENS)) {
+				// function call
+				long begin = sql.len;
+				var type = translate_function (sql, binding.literal);
+				expect (SparqlTokenType.CLOSE_PARENS);
+				convert_expression_to_string (sql, type, begin);
+			} else {
+				sql.append ("?");
+				query.bindings.append (binding);
+			}
+			break;
+		default:
+			long begin = sql.len;
+			var type = translate_expression (sql);
+			convert_expression_to_string (sql, type, begin);
+			break;
+		}
+	}
+
+	void translate_str (StringBuilder sql) throws SparqlError {
+		expect (SparqlTokenType.STR);
+		expect (SparqlTokenType.OPEN_PARENS);
+
+		translate_expression_as_string (sql);
+
+		expect (SparqlTokenType.CLOSE_PARENS);
+	}
+
+	void translate_isuri (StringBuilder sql) throws SparqlError {
+		if (!accept (SparqlTokenType.ISURI)) {
+			expect (SparqlTokenType.ISIRI);
+		}
+
+		expect (SparqlTokenType.OPEN_PARENS);
+
+		sql.append ("?");
+		var new_binding = new LiteralBinding ();
+		new_binding.data_type = PropertyType.INTEGER;
+
+		if (current() == SparqlTokenType.IRI_REF) {
+			new_binding.literal = "1";
+			next ();
+		} else if (translate_expression (new StringBuilder ()) == PropertyType.RESOURCE) {
+			new_binding.literal = "1";
+		} else {
+			new_binding.literal = "0";
+		}
+
+		query.bindings.append (new_binding);
+
+		expect (SparqlTokenType.CLOSE_PARENS);
+	}
+
+	void translate_datatype (StringBuilder sql) throws SparqlError {
+		expect (SparqlTokenType.DATATYPE);
+		expect (SparqlTokenType.OPEN_PARENS);
+
+		if (accept (SparqlTokenType.VAR)) {
+			string variable_name = get_last_string().substring(1);
+			var variable = context.get_variable (variable_name);
+
+			if (variable.binding == null) {
+				throw get_error ("`%s' is not a valid variable".printf (variable.name));
+			}
+
+			if (variable.binding.data_type == PropertyType.RESOURCE || variable.binding.type == null) {
+				throw get_error ("Invalid FILTER");
+			}
+
+			sql.append ("(SELECT ID FROM Resource WHERE Uri = ?)");
+
+			var new_binding = new LiteralBinding ();
+			new_binding.literal = variable.binding.type.uri;
+			query.bindings.append (new_binding);
+
+		} else {
+			throw get_error ("Invalid FILTER");
+		}
+
+		expect (SparqlTokenType.CLOSE_PARENS);
+	}
+
+	PropertyType translate_function (StringBuilder sql, string uri) throws SparqlError {
+		if (uri == XSD_NS + "string") {
+			// conversion to string
+			translate_expression_as_string (sql);
+
+			return PropertyType.STRING;
+		} else if (uri == XSD_NS + "integer") {
+			// conversion to integer
+			sql.append ("CAST (");
+			translate_expression_as_string (sql);
+			sql.append (" AS INTEGER)");
+
+			return PropertyType.INTEGER;
+		} else if (uri == XSD_NS + "double") {
+			// conversion to double
+			sql.append ("CAST (");
+			translate_expression_as_string (sql);
+			sql.append (" AS REAL)");
+
+			return PropertyType.DOUBLE;
+		} else if (uri == FN_NS + "contains") {
+			// fn:contains('A','B') => 'A' GLOB '*B*'
+			sql.append ("(");
+			translate_expression_as_string (sql);
+			sql.append (" GLOB ");
+			expect (SparqlTokenType.COMMA);
+
+			sql.append ("?");
+			var binding = new LiteralBinding ();
+			binding.literal = "*%s*".printf (parse_string_literal ());
+			query.bindings.append (binding);
+
+			sql.append (")");
+
+			return PropertyType.BOOLEAN;
+		} else if (uri == FN_NS + "starts-with") {
+			// fn:starts-with('A','B') => 'A' GLOB 'B*'
+			sql.append ("(");
+			translate_expression_as_string (sql);
+			sql.append (" GLOB ");
+			expect (SparqlTokenType.COMMA);
+
+			sql.append ("?");
+			var binding = new LiteralBinding ();
+			binding.literal = "%s*".printf (parse_string_literal ());
+			query.bindings.append (binding);
+
+			sql.append (")");
+
+			return PropertyType.BOOLEAN;
+		} else if (uri == FN_NS + "ends-with") {
+			// fn:ends-with('A','B') => 'A' GLOB '*B'
+			sql.append ("(");
+			translate_expression_as_string (sql);
+			sql.append (" GLOB ");
+			expect (SparqlTokenType.COMMA);
+
+			sql.append ("?");
+			var binding = new LiteralBinding ();
+			binding.literal = "*%s".printf (parse_string_literal ());
+			query.bindings.append (binding);
+
+			sql.append (")");
+
+			return PropertyType.BOOLEAN;
+		} else if (uri == FN_NS + "concat") {
+			translate_expression (sql);
+			sql.append ("||");
+			expect (SparqlTokenType.COMMA);
+			translate_expression (sql);
+			while (accept (SparqlTokenType.COMMA)) {
+			      sql.append ("||");
+			      translate_expression (sql);
+			}
+
+			return PropertyType.STRING;
+		} else if (uri == FN_NS + "string-join") {
+			sql.append ("SparqlStringJoin(");
+			expect (SparqlTokenType.OPEN_PARENS);
+
+			translate_expression_as_string (sql);
+			sql.append (", ");
+			expect (SparqlTokenType.COMMA);
+			translate_expression_as_string (sql);
+			while (accept (SparqlTokenType.COMMA)) {
+			      sql.append (", ");
+			      translate_expression_as_string (sql);
+			}
+
+			expect (SparqlTokenType.CLOSE_PARENS);
+			sql.append (",");
+			expect (SparqlTokenType.COMMA);
+			translate_expression (sql);
+			sql.append (")");
+
+			return PropertyType.STRING;
+		} else if (uri == FN_NS + "year-from-dateTime") {
+			expect (SparqlTokenType.VAR);
+			string variable_name = get_last_string ().substring (1);
+			var variable = context.get_variable (variable_name);
+
+			sql.append ("strftime (\"%Y\", ");
+			sql.append (variable.get_extra_sql_expression ("localDate"));
+			sql.append (" * 24 * 3600, \"unixepoch\")");
+
+			return PropertyType.INTEGER;
+		} else if (uri == FN_NS + "month-from-dateTime") {
+			expect (SparqlTokenType.VAR);
+			string variable_name = get_last_string ().substring (1);
+			var variable = context.get_variable (variable_name);
+
+			sql.append ("strftime (\"%m\", ");
+			sql.append (variable.get_extra_sql_expression ("localDate"));
+			sql.append (" * 24 * 3600, \"unixepoch\")");
+
+			return PropertyType.INTEGER;
+		} else if (uri == FN_NS + "day-from-dateTime") {
+			expect (SparqlTokenType.VAR);
+			string variable_name = get_last_string ().substring (1);
+			var variable = context.get_variable (variable_name);
+
+			sql.append ("strftime (\"%d\", ");
+			sql.append (variable.get_extra_sql_expression ("localDate"));
+			sql.append (" * 24 * 3600, \"unixepoch\")");
+
+			return PropertyType.INTEGER;
+		} else if (uri == FN_NS + "hours-from-dateTime") {
+			expect (SparqlTokenType.VAR);
+			string variable_name = get_last_string ().substring (1);
+			var variable = context.get_variable (variable_name);
+
+			sql.append ("(");
+			sql.append (variable.get_extra_sql_expression ("localTime"));
+			sql.append (" / 3600)");
+
+			return PropertyType.INTEGER;
+		} else if (uri == FN_NS + "minutes-from-dateTime") {
+			expect (SparqlTokenType.VAR);
+			string variable_name = get_last_string ().substring (1);
+			var variable = context.get_variable (variable_name);
+
+			sql.append ("(");
+			sql.append (variable.get_extra_sql_expression ("localTime"));
+			sql.append (" / 60 % 60)");
+
+			return PropertyType.INTEGER;
+		} else if (uri == FN_NS + "seconds-from-dateTime") {
+			expect (SparqlTokenType.VAR);
+			string variable_name = get_last_string ().substring (1);
+			var variable = context.get_variable (variable_name);
+
+			sql.append ("(");
+			sql.append (variable.get_extra_sql_expression ("localTime"));
+			sql.append ("% 60)");
+
+			return PropertyType.INTEGER;
+		} else if (uri == FN_NS + "timezone-from-dateTime") {
+			expect (SparqlTokenType.VAR);
+			string variable_name = get_last_string ().substring (1);
+			var variable = context.get_variable (variable_name);
+
+			sql.append ("(");
+			sql.append (variable.get_extra_sql_expression ("localDate"));
+			sql.append (" * 24 * 3600 + ");
+			sql.append (variable.get_extra_sql_expression ("localTime"));
+			sql.append ("- ");
+			sql.append (variable.sql_expression);
+			sql.append (")");
+
+			return PropertyType.INTEGER;
+		} else if (uri == FTS_NS + "rank") {
+			bool is_var;
+			string v = pattern.parse_var_or_term (null, out is_var);
+			sql.append_printf ("\"%s_u_rank\"", v);
+
+			return PropertyType.DOUBLE;
+		} else if (uri == FTS_NS + "offsets") {
+			bool is_var;
+			string v = pattern.parse_var_or_term (null, out is_var);
+			sql.append_printf ("\"%s_u_offsets\"", v);
+
+			return PropertyType.STRING;
+		} else if (uri == TRACKER_NS + "cartesian-distance") {
+			sql.append ("SparqlCartesianDistance(");
+			translate_expression (sql);
+			sql.append (", ");
+			expect (SparqlTokenType.COMMA);
+			translate_expression (sql);
+			sql.append (", ");
+			expect (SparqlTokenType.COMMA);
+			translate_expression (sql);
+			sql.append (", ");
+			expect (SparqlTokenType.COMMA);
+			translate_expression (sql);
+			sql.append (")");
+
+			return PropertyType.DOUBLE;
+		} else if (uri == TRACKER_NS + "haversine-distance") {
+			sql.append ("SparqlHaversineDistance(");
+			translate_expression (sql);
+			sql.append (", ");
+			expect (SparqlTokenType.COMMA);
+			translate_expression (sql);
+			sql.append (", ");
+			expect (SparqlTokenType.COMMA);
+			translate_expression (sql);
+			sql.append (", ");
+			expect (SparqlTokenType.COMMA);
+			translate_expression (sql);
+			sql.append (")");
+
+			return PropertyType.DOUBLE;
+		} else if (uri == TRACKER_NS + "coalesce") {
+			sql.append ("COALESCE(");
+			translate_expression_as_string (sql);
+			sql.append (", ");
+			expect (SparqlTokenType.COMMA);
+			translate_expression_as_string (sql);
+			while (accept (SparqlTokenType.COMMA)) {
+			      sql.append (", ");
+			      translate_expression_as_string (sql);
+			}
+			sql.append (")");
+
+			return PropertyType.STRING;
+		} else if (uri == TRACKER_NS + "string-from-filename") {
+			sql.append ("SparqlStringFromFilename(");
+			translate_expression_as_string (sql);
+			sql.append (")");
+
+			return PropertyType.STRING;
+		} else {
+			// support properties as functions
+			var prop = Ontologies.get_property_by_uri (uri);
+			if (prop == null) {
+				throw get_error ("Unknown function");
+			}
+
+			if (prop.multiple_values) {
+				sql.append ("(SELECT GROUP_CONCAT(");
+				long begin = sql.len;
+				sql.append_printf ("\"%s\"", prop.name);
+				convert_expression_to_string (sql, prop.data_type, begin);
+				sql.append_printf (",',') FROM \"%s\" WHERE ID = ", prop.table_name);
+				translate_expression (sql);
+				sql.append (")");
+
+				return PropertyType.STRING;
+			} else {
+				sql.append_printf ("(SELECT \"%s\" FROM \"%s\" WHERE ID = ", prop.name, prop.table_name);
+				translate_expression (sql);
+				sql.append (")");
+
+				return prop.data_type;
+			}
+		}
+	}
+
+	internal string parse_string_literal () throws SparqlError {
+		next ();
+		switch (last ()) {
+		case SparqlTokenType.STRING_LITERAL1:
+		case SparqlTokenType.STRING_LITERAL2:
+			var sb = new StringBuilder ();
+
+			string s = get_last_string (1);
+			string* p = s;
+			string* end = p + s.size ();
+			while ((long) p < (long) end) {
+				string* q = Posix.strchr (p, '\\');
+				if (q == null) {
+					sb.append_len (p, (long) (end - p));
+					p = end;
+				} else {
+					sb.append_len (p, (long) (q - p));
+					p = q + 1;
+					switch (((char*) p)[0]) {
+					case '\'':
+					case '"':
+					case '\\':
+						sb.append_c (((char*) p)[0]);
+						break;
+					case 'b':
+						sb.append_c ('\b');
+						break;
+					case 'f':
+						sb.append_c ('\f');
+						break;
+					case 'n':
+						sb.append_c ('\n');
+						break;
+					case 'r':
+						sb.append_c ('\r');
+						break;
+					case 't':
+						sb.append_c ('\t');
+						break;
+					}
+					p++;
+				}
+			}
+
+			if (accept (SparqlTokenType.DOUBLE_CIRCUMFLEX)) {
+				if (!accept (SparqlTokenType.IRI_REF)) {
+					accept (SparqlTokenType.PN_PREFIX);
+					expect (SparqlTokenType.COLON);
+				}
+			}
+
+			return sb.str;
+		case SparqlTokenType.STRING_LITERAL_LONG1:
+		case SparqlTokenType.STRING_LITERAL_LONG2:
+			string result = get_last_string (3);
+
+			if (accept (SparqlTokenType.DOUBLE_CIRCUMFLEX)) {
+				if (!accept (SparqlTokenType.IRI_REF)) {
+					accept (SparqlTokenType.PN_PREFIX);
+					expect (SparqlTokenType.COLON);
+				}
+			}
+
+			return result;
+		default:
+			throw get_error ("expected string literal");
+		}
+	}
+
+	PropertyType translate_uri_expression (StringBuilder sql, string uri) throws SparqlError {
+		if (accept (SparqlTokenType.OPEN_PARENS)) {
+			// function
+			var result = translate_function (sql, uri);
+			expect (SparqlTokenType.CLOSE_PARENS);
+			return result;
+		} else {
+			// resource
+			sql.append ("(SELECT ID FROM Resource WHERE Uri = ?)");
+			var binding = new LiteralBinding ();
+			binding.literal = uri;
+			query.bindings.append (binding);
+			return PropertyType.RESOURCE;
+		}
+	}
+
+	PropertyType translate_primary_expression (StringBuilder sql) throws SparqlError {
+		switch (current ()) {
+		case SparqlTokenType.OPEN_PARENS:
+			return translate_bracketted_expression (sql);
+		case SparqlTokenType.IRI_REF:
+			next ();
+			return translate_uri_expression (sql, get_last_string (1));
+		case SparqlTokenType.DECIMAL:
+		case SparqlTokenType.DOUBLE:
+			next ();
+
+			sql.append ("?");
+
+			var binding = new LiteralBinding ();
+			binding.literal = get_last_string ();
+			query.bindings.append (binding);
+
+			return PropertyType.DOUBLE;
+		case SparqlTokenType.TRUE:
+			next ();
+
+			sql.append ("?");
+
+			var binding = new LiteralBinding ();
+			binding.literal = "1";
+			binding.data_type = PropertyType.INTEGER;
+			query.bindings.append (binding);
+
+			return PropertyType.BOOLEAN;
+		case SparqlTokenType.FALSE:
+			next ();
+
+			sql.append ("?");
+
+			var binding = new LiteralBinding ();
+			binding.literal = "0";
+			binding.data_type = PropertyType.INTEGER;
+			query.bindings.append (binding);
+
+			return PropertyType.BOOLEAN;
+		case SparqlTokenType.STRING_LITERAL1:
+		case SparqlTokenType.STRING_LITERAL2:
+		case SparqlTokenType.STRING_LITERAL_LONG1:
+		case SparqlTokenType.STRING_LITERAL_LONG2:
+			sql.append ("?");
+
+			var binding = new LiteralBinding ();
+			binding.literal = parse_string_literal ();
+			query.bindings.append (binding);
+
+			return PropertyType.STRING;
+		case SparqlTokenType.INTEGER:
+			next ();
+
+			sql.append ("?");
+
+			var binding = new LiteralBinding ();
+			binding.literal = get_last_string ();
+			binding.data_type = PropertyType.INTEGER;
+			query.bindings.append (binding);
+
+			return PropertyType.INTEGER;
+		case SparqlTokenType.VAR:
+			next ();
+			string variable_name = get_last_string ().substring (1);
+			var variable = context.get_variable (variable_name);
+			sql.append (variable.sql_expression);
+
+			if (variable.binding == null) {
+				return PropertyType.UNKNOWN;
+			} else {
+				return variable.binding.data_type;
+			}
+		case SparqlTokenType.STR:
+			translate_str (sql);
+			return PropertyType.STRING;
+		case SparqlTokenType.LANG:
+			next ();
+			sql.append ("''");
+			return PropertyType.STRING;
+		case SparqlTokenType.LANGMATCHES:
+			next ();
+			sql.append ("0");
+			return PropertyType.BOOLEAN;
+		case SparqlTokenType.DATATYPE:
+			translate_datatype (sql);
+			return PropertyType.RESOURCE;
+		case SparqlTokenType.BOUND:
+			translate_bound_call (sql);
+			return PropertyType.BOOLEAN;
+		case SparqlTokenType.SAMETERM:
+			next ();
+			expect (SparqlTokenType.OPEN_PARENS);
+			sql.append ("(");
+			translate_expression (sql);
+			sql.append (" = ");
+			expect (SparqlTokenType.COMMA);
+			translate_expression (sql);
+			sql.append (")");
+			expect (SparqlTokenType.CLOSE_PARENS);
+			return PropertyType.BOOLEAN;
+		case SparqlTokenType.ISIRI:
+		case SparqlTokenType.ISURI:
+			translate_isuri (sql);
+			return PropertyType.BOOLEAN;
+		case SparqlTokenType.ISBLANK:
+			next ();
+			expect (SparqlTokenType.OPEN_PARENS);
+			next ();
+			// TODO: support ISBLANK properly
+			sql.append ("0");
+			expect (SparqlTokenType.CLOSE_PARENS);
+			return PropertyType.BOOLEAN;
+		case SparqlTokenType.ISLITERAL:
+			next ();
+			return PropertyType.BOOLEAN;
+		case SparqlTokenType.REGEX:
+			translate_regex (sql);
+			return PropertyType.BOOLEAN;
+		case SparqlTokenType.PN_PREFIX:
+			next ();
+			string ns = get_last_string ();
+			expect (SparqlTokenType.COLON);
+			string uri = query.resolve_prefixed_name (ns, get_last_string ().substring (1));
+			return translate_uri_expression (sql, uri);
+		case SparqlTokenType.COLON:
+			next ();
+			string uri = query.resolve_prefixed_name ("", get_last_string ().substring (1));
+			return translate_uri_expression (sql, uri);
+		default:
+			throw get_error ("expected primary expression");
+		}
+	}
+
+	PropertyType translate_unary_expression (StringBuilder sql) throws SparqlError {
+		if (accept (SparqlTokenType.OP_NEG)) {
+			sql.append ("NOT (");
+			var optype = translate_primary_expression (sql);
+			sql.append (")");
+			if (optype != PropertyType.BOOLEAN) {
+				throw get_error ("expected boolean expression");
+			}
+			return PropertyType.BOOLEAN;
+		} else if (accept (SparqlTokenType.PLUS)) {
+			return translate_primary_expression (sql);
+		} else if (accept (SparqlTokenType.MINUS)) {
+			sql.append ("-(");
+			var optype = translate_primary_expression (sql);
+			sql.append (")");
+			return optype;
+		}
+		return translate_primary_expression (sql);
+	}
+
+	PropertyType translate_multiplicative_expression (StringBuilder sql) throws SparqlError {
+		long begin = sql.len;
+		var optype = translate_unary_expression (sql);
+		while (true) {
+			if (accept (SparqlTokenType.STAR)) {
+				if (!maybe_numeric (optype)) {
+					throw get_error ("expected numeric operand");
+				}
+				sql.insert (begin, "(");
+				sql.append (" * ");
+				if (!maybe_numeric (translate_unary_expression (sql))) {
+					throw get_error ("expected numeric operand");
+				}
+				sql.append (")");
+			} else if (accept (SparqlTokenType.DIV)) {
+				if (!maybe_numeric (optype)) {
+					throw get_error ("expected numeric operand");
+				}
+				sql.insert (begin, "(");
+				sql.append (" / ");
+				if (!maybe_numeric (translate_unary_expression (sql))) {
+					throw get_error ("expected numeric operand");
+				}
+				sql.append (")");
+			} else {
+				break;
+			}
+		}
+		return optype;
+	}
+
+	PropertyType translate_additive_expression (StringBuilder sql) throws SparqlError {
+		long begin = sql.len;
+		var optype = translate_multiplicative_expression (sql);
+		while (true) {
+			if (accept (SparqlTokenType.PLUS)) {
+				if (!maybe_numeric (optype)) {
+					throw get_error ("expected numeric operand");
+				}
+				sql.insert (begin, "(");
+				sql.append (" + ");
+				if (!maybe_numeric (translate_multiplicative_expression (sql))) {
+					throw get_error ("expected numeric operand");
+				}
+				sql.append (")");
+			} else if (accept (SparqlTokenType.MINUS)) {
+				if (!maybe_numeric (optype)) {
+					throw get_error ("expected numeric operand");
+				}
+				sql.insert (begin, "(");
+				sql.append (" - ");
+				if (!maybe_numeric (translate_multiplicative_expression (sql))) {
+					throw get_error ("expected numeric operand");
+				}
+				sql.append (")");
+			} else {
+				break;
+			}
+		}
+		return optype;
+	}
+
+	PropertyType translate_numeric_expression (StringBuilder sql) throws SparqlError {
+		return translate_additive_expression (sql);
+	}
+
+	PropertyType process_relational_expression (StringBuilder sql, long begin, uint n_bindings, PropertyType op1type, string operator) throws SparqlError {
+		sql.insert (begin, "(");
+		sql.append (operator);
+		var op2type = translate_numeric_expression (sql);
+		sql.append (")");
+		if ((op1type == PropertyType.DATETIME && op2type == PropertyType.STRING)
+		    || (op1type == PropertyType.STRING && op2type == PropertyType.DATETIME)) {
+			// TODO: improve performance (linked list)
+			if (query.bindings.length () == n_bindings + 1) {
+				// trigger string => datetime conversion
+				query.bindings.last ().data.data_type = PropertyType.DATETIME;
+			}
+		}
+		return PropertyType.BOOLEAN;
+	}
+
+	PropertyType translate_relational_expression (StringBuilder sql) throws SparqlError {
+		long begin = sql.len;
+		// TODO: improve performance (linked list)
+		uint n_bindings = query.bindings.length ();
+		var optype = translate_numeric_expression (sql);
+		if (accept (SparqlTokenType.OP_GE)) {
+			return process_relational_expression (sql, begin, n_bindings, optype, " >= ");
+		} else if (accept (SparqlTokenType.OP_EQ)) {
+			return process_relational_expression (sql, begin, n_bindings, optype, " = ");
+		} else if (accept (SparqlTokenType.OP_NE)) {
+			return process_relational_expression (sql, begin, n_bindings, optype, " <> ");
+		} else if (accept (SparqlTokenType.OP_LT)) {
+			return process_relational_expression (sql, begin, n_bindings, optype, " < ");
+		} else if (accept (SparqlTokenType.OP_LE)) {
+			return process_relational_expression (sql, begin, n_bindings, optype, " <= ");
+		} else if (accept (SparqlTokenType.OP_GT)) {
+			return process_relational_expression (sql, begin, n_bindings, optype, " > ");
+		}
+		return optype;
+	}
+
+	PropertyType translate_value_logical (StringBuilder sql) throws SparqlError {
+		return translate_relational_expression (sql);
+	}
+
+	PropertyType translate_conditional_and_expression (StringBuilder sql) throws SparqlError {
+		long begin = sql.len;
+		var optype = translate_value_logical (sql);
+		while (accept (SparqlTokenType.OP_AND)) {
+			if (optype != PropertyType.BOOLEAN) {
+				throw get_error ("expected boolean expression");
+			}
+			sql.insert (begin, "(");
+			sql.append (" AND ");
+			optype = translate_value_logical (sql);
+			sql.append (")");
+			if (optype != PropertyType.BOOLEAN) {
+				throw get_error ("expected boolean expression");
+			}
+		}
+		return optype;
+	}
+
+	PropertyType translate_conditional_or_expression (StringBuilder sql) throws SparqlError {
+		long begin = sql.len;
+		var optype = translate_conditional_and_expression (sql);
+		while (accept (SparqlTokenType.OP_OR)) {
+			if (optype != PropertyType.BOOLEAN) {
+				throw get_error ("expected boolean expression");
+			}
+			sql.insert (begin, "(");
+			sql.append (" OR ");
+			optype = translate_conditional_and_expression (sql);
+			sql.append (")");
+			if (optype != PropertyType.BOOLEAN) {
+				throw get_error ("expected boolean expression");
+			}
+		}
+		return optype;
+	}
+
+	internal PropertyType translate_expression (StringBuilder sql) throws SparqlError {
+		return translate_conditional_or_expression (sql);
+	}
+
+	PropertyType translate_bracketted_expression (StringBuilder sql) throws SparqlError {
+		expect (SparqlTokenType.OPEN_PARENS);
+
+		if (current () == SparqlTokenType.SELECT) {
+			// scalar subquery
+
+			context = new Context.subquery (context);
+
+			sql.append ("(");
+			var type = pattern.translate_select (sql, true);
+			sql.append (")");
+
+			context = context.parent_context;
+
+			expect (SparqlTokenType.CLOSE_PARENS);
+			return type;
+		}
+
+		var optype = translate_expression (sql);
+		expect (SparqlTokenType.CLOSE_PARENS);
+		return optype;
+	}
+
+	PropertyType translate_aggregate_expression (StringBuilder sql) throws SparqlError {
+		expect (SparqlTokenType.OPEN_PARENS);
+		if (accept (SparqlTokenType.DISTINCT)) {
+			sql.append ("DISTINCT ");
+		}
+		var optype = translate_expression (sql);
+		expect (SparqlTokenType.CLOSE_PARENS);
+		return optype;
+	}
+
+	internal PropertyType translate_constraint (StringBuilder sql) throws SparqlError {
+		switch (current ()) {
+		case SparqlTokenType.STR:
+		case SparqlTokenType.LANG:
+		case SparqlTokenType.LANGMATCHES:
+		case SparqlTokenType.DATATYPE:
+		case SparqlTokenType.BOUND:
+		case SparqlTokenType.SAMETERM:
+		case SparqlTokenType.ISIRI:
+		case SparqlTokenType.ISURI:
+		case SparqlTokenType.ISBLANK:
+		case SparqlTokenType.ISLITERAL:
+		case SparqlTokenType.REGEX:
+			return translate_primary_expression (sql);
+		default:
+			return translate_bracketted_expression (sql);
+		}
+	}
+}
diff --git a/src/libtracker-data/tracker-sparql-pattern.vala b/src/libtracker-data/tracker-sparql-pattern.vala
new file mode 100644
index 0000000..458f874
--- /dev/null
+++ b/src/libtracker-data/tracker-sparql-pattern.vala
@@ -0,0 +1,1403 @@
+/*
+ * Copyright (C) 2008-2010, Nokia
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA  02110-1301, USA.
+ */
+
+namespace Tracker.Sparql {
+	// Represents a variable used as a predicate
+	class PredicateVariable : Object {
+		public string? subject;
+		public string? object;
+
+		public Class? domain;
+
+		public string get_sql_query (Query query) throws SparqlError {
+			try {
+				var sql = new StringBuilder ();
+
+				if (subject != null) {
+					// single subject
+					var subject_id = Data.query_resource_id (subject);
+
+					DBResultSet result_set = null;
+					if (subject_id > 0) {
+						var iface = DBManager.get_db_interface ();
+						var stmt = iface.create_statement ("SELECT (SELECT Uri FROM Resource WHERE ID = \"rdf:type\") FROM \"rdfs:Resource_rdf:type\" WHERE ID = ?");
+						stmt.bind_int (0, subject_id);
+						result_set = stmt.execute ();
+					}
+
+					if (result_set != null) {
+						bool first = true;
+						do {
+							Value value;
+							result_set._get_value (0, out value);
+							var domain = Ontologies.get_class_by_uri (value.get_string ());
+
+							foreach (Property prop in Ontologies.get_properties ()) {
+								if (prop.domain == domain) {
+									if (first) {
+										first = false;
+									} else {
+										sql.append (" UNION ALL ");
+									}
+									sql.append_printf ("SELECT ID, (SELECT ID FROM Resource WHERE Uri = '%s') AS \"predicate\", ", prop.uri);
+
+									Expression.append_expression_as_string (sql, "\"%s\"".printf (prop.name), prop.data_type);
+
+									sql.append (" AS \"object\" FROM ");
+									sql.append_printf ("\"%s\"", prop.table_name);
+
+									sql.append (" WHERE ID = ?");
+
+									var binding = new LiteralBinding ();
+									binding.literal = subject_id.to_string ();
+									binding.data_type = PropertyType.INTEGER;
+									query.bindings.append (binding);
+								}
+							}
+						} while (result_set.iter_next ());
+					} else {
+						/* no match */
+						sql.append ("SELECT NULL AS ID, NULL AS \"predicate\", NULL AS \"object\"");
+					}
+				} else if (object != null) {
+					// single object
+					var object_id = Data.query_resource_id (object);
+
+					var iface = DBManager.get_db_interface ();
+					var stmt = iface.create_statement ("SELECT (SELECT Uri FROM Resource WHERE ID = \"rdf:type\") FROM \"rdfs:Resource_rdf:type\" WHERE ID = ?");
+					stmt.bind_int (0, object_id);
+					var result_set = stmt.execute ();
+
+					bool first = true;
+					if (result_set != null) {
+						do {
+							Value value;
+							result_set._get_value (0, out value);
+							var range = Ontologies.get_class_by_uri (value.get_string ());
+
+							foreach (Property prop in Ontologies.get_properties ()) {
+								if (prop.range == range) {
+									if (first) {
+										first = false;
+									} else {
+										sql.append (" UNION ALL ");
+									}
+									sql.append_printf ("SELECT ID, (SELECT ID FROM Resource WHERE Uri = '%s') AS \"predicate\", ", prop.uri);
+
+									Expression.append_expression_as_string (sql, "\"%s\"".printf (prop.name), prop.data_type);
+
+									sql.append (" AS \"object\" FROM ");
+									sql.append_printf ("\"%s\"", prop.table_name);
+								}
+							}
+						} while (result_set.iter_next ());
+					} else {
+						/* no match */
+						sql.append ("SELECT NULL AS ID, NULL AS \"predicate\", NULL AS \"object\"");
+					}
+				} else if (domain != null) {
+					// any subject, predicates limited to a specific domain
+					bool first = true;
+					foreach (Property prop in Ontologies.get_properties ()) {
+						if (prop.domain == domain) {
+							if (first) {
+								first = false;
+							} else {
+								sql.append (" UNION ALL ");
+							}
+							sql.append_printf ("SELECT ID, (SELECT ID FROM Resource WHERE Uri = '%s') AS \"predicate\", ", prop.uri);
+
+							Expression.append_expression_as_string (sql, "\"%s\"".printf (prop.name), prop.data_type);
+
+							sql.append (" AS \"object\" FROM ");
+							sql.append_printf ("\"%s\"", prop.table_name);
+						}
+					}
+				} else {
+					// UNION over all properties would exceed SQLite limits
+					throw query.get_internal_error ("Unrestricted predicate variables not supported");
+				}
+				return sql.str;
+			} catch (DBInterfaceError e) {
+				throw new SparqlError.INTERNAL (e.message);
+			}
+		}
+	}
+}
+
+class Tracker.Sparql.Pattern : Object {
+	Query query;
+	Expression expression;
+
+	int counter;
+
+	int next_table_index;
+
+	string current_graph;
+	bool current_graph_is_var;
+	string current_subject;
+	bool current_subject_is_var;
+	string current_predicate;
+	bool current_predicate_is_var;
+
+	public Pattern (Query query) {
+		this.query = query;
+		this.expression = query.expression;
+	}
+
+	Context context {
+		get { return query.context; }
+		set { query.context = value; }
+	}
+
+	inline bool next () throws SparqlError {
+		return query.next ();
+	}
+
+	inline SparqlTokenType current () {
+		return query.current ();
+	}
+
+	inline SparqlTokenType last () {
+		return query.last ();
+	}
+
+	inline bool accept (SparqlTokenType type) throws SparqlError {
+		return query.accept (type);
+	}
+
+	SparqlError get_error (string msg) {
+		return query.get_error (msg);
+	}
+
+	SparqlError get_internal_error (string msg) {
+		return query.get_internal_error (msg);
+	}
+
+	bool expect (SparqlTokenType type) throws SparqlError {
+		return query.expect (type);
+	}
+
+	SourceLocation get_location () {
+		return query.get_location ();
+	}
+
+	void set_location (SourceLocation location) {
+		query.set_location (location);
+	}
+
+	string get_last_string (int strip = 0) {
+		return query.get_last_string (strip);
+	}
+
+	class TripleContext {
+		// SQL tables
+		public List<DataTable> tables;
+		public HashTable<string,DataTable> table_map;
+		// SPARQL literals
+		public List<LiteralBinding> bindings;
+		// SPARQL variables
+		public List<Variable> variables;
+		public HashTable<Variable,VariableBindingList> var_map;
+
+		public TripleContext () {
+			tables = new List<DataTable> ();
+			table_map = new HashTable<string,DataTable>.full (str_hash, str_equal, g_free, g_object_unref);
+
+			variables = new List<Variable> ();
+			var_map = new HashTable<Variable,VariableBindingList>.full (direct_hash, direct_equal, g_object_unref, g_object_unref);
+
+			bindings = new List<LiteralBinding> ();
+		}
+	}
+
+	TripleContext? triple_context;
+
+	internal PropertyType translate_select (StringBuilder sql, bool subquery = false) throws SparqlError {
+		var type = PropertyType.UNKNOWN;
+
+		var pattern_sql = new StringBuilder ();
+		var old_bindings = (owned) query.bindings;
+
+		sql.append ("SELECT ");
+
+		expect (SparqlTokenType.SELECT);
+
+		if (accept (SparqlTokenType.DISTINCT)) {
+			sql.append ("DISTINCT ");
+		} else if (accept (SparqlTokenType.REDUCED)) {
+		}
+
+		// skip select variables (processed later)
+		var select_variables_location = get_location ();
+		expression.skip_select_variables ();
+
+		if (accept (SparqlTokenType.FROM)) {
+			accept (SparqlTokenType.NAMED);
+			expect (SparqlTokenType.IRI_REF);
+		}
+
+		accept (SparqlTokenType.WHERE);
+
+		translate_group_graph_pattern (pattern_sql);
+
+		// process select variables
+		var after_where = get_location ();
+		set_location (select_variables_location);
+
+		// report use of undefined variables
+		foreach (var variable in context.var_map.get_values ()) {
+			if (variable.binding == null) {
+				throw get_error ("use of undefined variable `%s'".printf (variable.name));
+			}
+		}
+
+		var where_bindings = (owned) query.bindings;
+		query.bindings = (owned) old_bindings;
+
+		bool first = true;
+		if (accept (SparqlTokenType.STAR)) {
+			foreach (var variable in context.var_map.get_values ()) {
+				if (!first) {
+					sql.append (", ");
+				} else {
+					first = false;
+				}
+				if (subquery) {
+					// don't convert to string in subqueries
+					sql.append (variable.sql_expression);
+				} else {
+					Expression.append_expression_as_string (sql, variable.sql_expression, variable.binding.data_type);
+				}
+			}
+		} else {
+			while (true) {
+				if (!first) {
+					sql.append (", ");
+				} else {
+					first = false;
+				}
+
+				type = expression.translate_select_expression (sql, subquery);
+
+				switch (current ()) {
+				case SparqlTokenType.FROM:
+				case SparqlTokenType.WHERE:
+				case SparqlTokenType.OPEN_BRACE:
+				case SparqlTokenType.GROUP:
+				case SparqlTokenType.ORDER:
+				case SparqlTokenType.LIMIT:
+				case SparqlTokenType.OFFSET:
+				case SparqlTokenType.EOF:
+					break;
+				default:
+					continue;
+				}
+				break;
+			}
+		}
+
+		// literals in select expressions need to be bound before literals in the where clause
+		foreach (var binding in where_bindings) {
+			query.bindings.append (binding);
+		}
+
+		// select from results of WHERE clause
+		sql.append (" FROM (");
+		sql.append (pattern_sql.str);
+		sql.append (")");
+
+		set_location (after_where);
+
+		if (accept (SparqlTokenType.GROUP)) {
+			expect (SparqlTokenType.BY);
+			sql.append (" GROUP BY ");
+			bool first_group = true;
+			do {
+				if (first_group) {
+					first_group = false;
+				} else {
+					sql.append (", ");
+				}
+				expression.translate_expression (sql);
+			} while (current () != SparqlTokenType.ORDER && current () != SparqlTokenType.LIMIT && current () != SparqlTokenType.OFFSET && current () != SparqlTokenType.CLOSE_BRACE && current () != SparqlTokenType.CLOSE_PARENS && current () != SparqlTokenType.EOF);
+		}
+
+		if (accept (SparqlTokenType.ORDER)) {
+			expect (SparqlTokenType.BY);
+			sql.append (" ORDER BY ");
+			bool first_order = true;
+			do {
+				if (first_order) {
+					first_order = false;
+				} else {
+					sql.append (", ");
+				}
+				expression.translate_order_condition (sql);
+			} while (current () != SparqlTokenType.LIMIT && current () != SparqlTokenType.OFFSET && current () != SparqlTokenType.CLOSE_BRACE && current () != SparqlTokenType.CLOSE_PARENS && current () != SparqlTokenType.EOF);
+		}
+
+		int limit = -1;
+		int offset = -1;
+
+		if (accept (SparqlTokenType.LIMIT)) {
+			expect (SparqlTokenType.INTEGER);
+			limit = get_last_string ().to_int ();
+			if (accept (SparqlTokenType.OFFSET)) {
+				expect (SparqlTokenType.INTEGER);
+				offset = get_last_string ().to_int ();
+			}
+		} else if (accept (SparqlTokenType.OFFSET)) {
+			expect (SparqlTokenType.INTEGER);
+			offset = get_last_string ().to_int ();
+			if (accept (SparqlTokenType.LIMIT)) {
+				expect (SparqlTokenType.INTEGER);
+				limit = get_last_string ().to_int ();
+			}
+		}
+
+		// LIMIT and OFFSET
+		if (limit >= 0) {
+			sql.append (" LIMIT ?");
+
+			var binding = new LiteralBinding ();
+			binding.literal = limit.to_string ();
+			binding.data_type = PropertyType.INTEGER;
+			query.bindings.append (binding);
+
+			if (offset >= 0) {
+				sql.append (" OFFSET ?");
+
+				binding = new LiteralBinding ();
+				binding.literal = offset.to_string ();
+				binding.data_type = PropertyType.INTEGER;
+				query.bindings.append (binding);
+			}
+		} else if (offset >= 0) {
+			sql.append (" LIMIT -1 OFFSET ?");
+
+			var binding = new LiteralBinding ();
+			binding.literal = offset.to_string ();
+			binding.data_type = PropertyType.INTEGER;
+			query.bindings.append (binding);
+		}
+
+		return type;
+	}
+
+	internal string parse_var_or_term (StringBuilder? sql, out bool is_var) throws SparqlError {
+		string result = "";
+		is_var = false;
+		if (current () == SparqlTokenType.VAR) {
+			is_var = true;
+			next ();
+			result = get_last_string ().substring (1);
+		} else if (current () == SparqlTokenType.IRI_REF) {
+			next ();
+			result = get_last_string (1);
+		} else if (current () == SparqlTokenType.PN_PREFIX) {
+			// prefixed name with namespace foo:bar
+			next ();
+			string ns = get_last_string ();
+			expect (SparqlTokenType.COLON);
+			result = query.resolve_prefixed_name (ns, get_last_string ().substring (1));
+		} else if (current () == SparqlTokenType.COLON) {
+			// prefixed name without namespace :bar
+			next ();
+			result = query.resolve_prefixed_name ("", get_last_string ().substring (1));
+		} else if (accept (SparqlTokenType.BLANK_NODE)) {
+			// _:foo
+			expect (SparqlTokenType.COLON);
+			result = query.generate_bnodeid (get_last_string ().substring (1));
+		} else if (current () == SparqlTokenType.STRING_LITERAL1) {
+			result = expression.parse_string_literal ();
+		} else if (current () == SparqlTokenType.STRING_LITERAL2) {
+			result = expression.parse_string_literal ();
+		} else if (current () == SparqlTokenType.STRING_LITERAL_LONG1) {
+			result = expression.parse_string_literal ();
+		} else if (current () == SparqlTokenType.STRING_LITERAL_LONG2) {
+			result = expression.parse_string_literal ();
+		} else if (current () == SparqlTokenType.INTEGER) {
+			next ();
+			result = get_last_string ();
+		} else if (current () == SparqlTokenType.DECIMAL) {
+			next ();
+			result = get_last_string ();
+		} else if (current () == SparqlTokenType.DOUBLE) {
+			next ();
+			result = get_last_string ();
+		} else if (current () == SparqlTokenType.TRUE) {
+			next ();
+			result = "true";
+		} else if (current () == SparqlTokenType.FALSE) {
+			next ();
+			result = "false";
+		} else if (current () == SparqlTokenType.OPEN_BRACKET) {
+			next ();
+
+			result = query.generate_bnodeid (null);
+
+			string old_subject = current_subject;
+			bool old_subject_is_var = current_subject_is_var;
+
+			current_subject = result;
+			current_subject_is_var = true;
+			parse_property_list_not_empty (sql);
+			expect (SparqlTokenType.CLOSE_BRACKET);
+
+			current_subject = old_subject;
+			current_subject_is_var = old_subject_is_var;
+
+			is_var = true;
+		} else {
+			throw get_error ("expected variable or term");
+		}
+		return result;
+	}
+
+	void parse_object_list (StringBuilder sql, bool in_simple_optional = false) throws SparqlError {
+		while (true) {
+			parse_object (sql, in_simple_optional);
+			if (accept (SparqlTokenType.COMMA)) {
+				continue;
+			}
+			break;
+		}
+	}
+
+	void parse_property_list_not_empty (StringBuilder sql, bool in_simple_optional = false) throws SparqlError {
+		while (true) {
+			var old_predicate = current_predicate;
+			var old_predicate_is_var = current_predicate_is_var;
+
+			current_predicate = null;
+			current_predicate_is_var = false;
+			if (current () == SparqlTokenType.VAR) {
+				current_predicate_is_var = true;
+				next ();
+				current_predicate = get_last_string ().substring (1);
+			} else if (current () == SparqlTokenType.IRI_REF) {
+				next ();
+				current_predicate = get_last_string (1);
+			} else if (current () == SparqlTokenType.PN_PREFIX) {
+				next ();
+				string ns = get_last_string ();
+				expect (SparqlTokenType.COLON);
+				current_predicate = query.resolve_prefixed_name (ns, get_last_string ().substring (1));
+			} else if (current () == SparqlTokenType.COLON) {
+				next ();
+				current_predicate = query.resolve_prefixed_name ("", get_last_string ().substring (1));
+			} else if (current () == SparqlTokenType.A) {
+				next ();
+				current_predicate = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";;
+			} else {
+				throw get_error ("expected non-empty property list");
+			}
+			parse_object_list (sql, in_simple_optional);
+
+			current_predicate = old_predicate;
+			current_predicate_is_var = old_predicate_is_var;
+
+			if (accept (SparqlTokenType.SEMICOLON)) {
+				if (current () == SparqlTokenType.DOT) {
+					// semicolon before dot is allowed in both, SPARQL and Turtle
+					break;
+				}
+				continue;
+			}
+			break;
+		}
+	}
+
+	void translate_filter (StringBuilder sql) throws SparqlError {
+		expect (SparqlTokenType.FILTER);
+		expression.translate_constraint (sql);
+	}
+
+	void skip_filter () throws SparqlError {
+		expect (SparqlTokenType.FILTER);
+
+		switch (current ()) {
+		case SparqlTokenType.STR:
+		case SparqlTokenType.LANG:
+		case SparqlTokenType.LANGMATCHES:
+		case SparqlTokenType.DATATYPE:
+		case SparqlTokenType.BOUND:
+		case SparqlTokenType.SAMETERM:
+		case SparqlTokenType.ISIRI:
+		case SparqlTokenType.ISURI:
+		case SparqlTokenType.ISBLANK:
+		case SparqlTokenType.ISLITERAL:
+		case SparqlTokenType.REGEX:
+			next ();
+			break;
+		default:
+			break;
+		}
+
+		expect (SparqlTokenType.OPEN_PARENS);
+		int n_parens = 1;
+		while (n_parens > 0) {
+			if (accept (SparqlTokenType.OPEN_PARENS)) {
+				n_parens++;
+			} else if (accept (SparqlTokenType.CLOSE_PARENS)) {
+				n_parens--;
+			} else if (current () == SparqlTokenType.EOF) {
+				throw get_error ("unexpected end of query, expected )");
+			} else {
+				// ignore everything else
+				next ();
+			}
+		}
+	}
+
+	void start_triples_block (StringBuilder sql) throws SparqlError {
+		triple_context = new TripleContext ();
+
+		sql.append ("SELECT ");
+	}
+
+	void end_triples_block (StringBuilder sql, ref bool first_where, bool in_group_graph_pattern) throws SparqlError {
+		// remove last comma and space
+		sql.truncate (sql.len - 2);
+
+		sql.append (" FROM ");
+		bool first = true;
+		foreach (DataTable table in triple_context.tables) {
+			if (!first) {
+				sql.append (", ");
+			} else {
+				first = false;
+			}
+			if (table.sql_db_tablename != null) {
+				sql.append_printf ("\"%s\"", table.sql_db_tablename);
+			} else {
+				sql.append_printf ("(%s)", table.predicate_variable.get_sql_query (query));
+			}
+			sql.append_printf (" AS \"%s\"", table.sql_query_tablename);
+		}
+
+		foreach (var variable in triple_context.variables) {
+			bool maybe_null = true;
+			bool in_simple_optional = false;
+			string last_name = null;
+			foreach (VariableBinding binding in triple_context.var_map.lookup (variable).list) {
+				string name;
+				if (binding.table != null) {
+					name = binding.sql_expression;
+				} else {
+					// simple optional with inverse functional property
+					// always first in loop as variable is required to be unbound
+					name = variable.sql_expression;
+				}
+				if (last_name != null) {
+					if (!first_where) {
+						sql.append (" AND ");
+					} else {
+						sql.append (" WHERE ");
+						first_where = false;
+					}
+					sql.append (last_name);
+					sql.append (" = ");
+					sql.append (name);
+				}
+				last_name = name;
+				if (!binding.maybe_null) {
+					maybe_null = false;
+				}
+				in_simple_optional = binding.in_simple_optional;
+			}
+
+			if (maybe_null && !in_simple_optional) {
+				// ensure that variable is bound in case it could return NULL in SQL
+				// assuming SPARQL variable is not optional
+				if (!first_where) {
+					sql.append (" AND ");
+				} else {
+					sql.append (" WHERE ");
+					first_where = false;
+				}
+				sql.append_printf ("%s IS NOT NULL", variable.sql_expression);
+			}
+		}
+		foreach (LiteralBinding binding in triple_context.bindings) {
+			if (!first_where) {
+				sql.append (" AND ");
+			} else {
+				sql.append (" WHERE ");
+				first_where = false;
+			}
+			sql.append (binding.sql_expression);
+			if (binding.is_fts_match) {
+				// parameters do not work with fts MATCH
+				string escaped_literal = string.joinv ("''", binding.literal.split ("'"));
+				sql.append_printf (" MATCH '%s'", escaped_literal);
+			} else {
+				sql.append (" = ");
+				if (binding.data_type == PropertyType.RESOURCE) {
+					sql.append ("(SELECT ID FROM Resource WHERE Uri = ?)");
+				} else {
+					sql.append ("?");
+				}
+				query.bindings.append (binding);
+			}
+		}
+
+		if (in_group_graph_pattern) {
+			sql.append (")");
+		}
+
+		triple_context = null;
+	}
+
+	void parse_triples (StringBuilder sql, long group_graph_pattern_start, ref bool in_triples_block, ref bool first_where, ref bool in_group_graph_pattern, bool found_simple_optional) throws SparqlError {
+		while (true) {
+			if (current () != SparqlTokenType.VAR &&
+			    current () != SparqlTokenType.IRI_REF &&
+			    current () != SparqlTokenType.PN_PREFIX &&
+			    current () != SparqlTokenType.COLON &&
+			    current () != SparqlTokenType.OPEN_BRACKET) {
+				break;
+			}
+			if (in_triples_block && !in_group_graph_pattern && found_simple_optional) {
+				// if there is a regular triple pattern after a simple optional
+				// we need to use a separate triple block to avoid possible conflicts
+				// due to not using a JOIN for the simple optional
+				end_triples_block (sql, ref first_where, in_group_graph_pattern);
+				in_triples_block = false;
+			}
+			if (!in_triples_block) {
+				if (in_group_graph_pattern) {
+					sql.insert (group_graph_pattern_start, "SELECT * FROM (");
+					sql.append (") NATURAL INNER JOIN (");
+				}
+				in_triples_block = true;
+				first_where = true;
+				start_triples_block (sql);
+			}
+
+			current_subject = parse_var_or_term (sql, out current_subject_is_var);
+			parse_property_list_not_empty (sql);
+
+			if (!accept (SparqlTokenType.DOT)) {
+				break;
+			}
+		}
+	}
+
+	bool is_subclass (Class class1, Class class2) {
+		if (class1 == class2) {
+			return true;
+		}
+		foreach (var superclass in class1.get_super_classes ()) {
+			if (is_subclass (superclass, class2)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	bool is_simple_optional () {
+		var optional_start = get_location ();
+		try {
+			// check that we have { ?v foo:bar ?o }
+			// where ?v is an already BOUND variable
+			//       foo:bar is a single-valued property
+			//               that is known to be in domain of ?v
+			//       ?o has not been used before
+			// or
+			// where ?v has not been used before
+			//       foo:bar is an inverse functional property
+			//       ?o is an already ?BOUND variable
+
+			expect (SparqlTokenType.OPEN_BRACE);
+
+			// check subject
+			if (!accept (SparqlTokenType.VAR)) {
+				return false;
+			}
+			var left_variable = context.get_variable (get_last_string ().substring (1));
+			var left_variable_state = context.var_set.lookup (left_variable);
+
+			// check predicate
+			string predicate;
+			if (accept (SparqlTokenType.IRI_REF)) {
+				predicate = get_last_string (1);
+			} else if (accept (SparqlTokenType.PN_PREFIX)) {
+				string ns = get_last_string ();
+				expect (SparqlTokenType.COLON);
+				predicate = query.resolve_prefixed_name (ns, get_last_string ().substring (1));
+			} else if (accept (SparqlTokenType.COLON)) {
+				predicate = query.resolve_prefixed_name ("", get_last_string ().substring (1));
+			} else {
+				return false;
+			}
+			var prop = Ontologies.get_property_by_uri (predicate);
+			if (prop == null) {
+				return false;
+			}
+
+			// check object
+			if (!accept (SparqlTokenType.VAR)) {
+				return false;
+			}
+			var right_variable = context.get_variable (get_last_string ().substring (1));
+			var right_variable_state = context.var_set.lookup (right_variable);
+
+			// optional .
+			accept (SparqlTokenType.DOT);
+
+			// check it is only one triple pattern
+			if (!accept (SparqlTokenType.CLOSE_BRACE)) {
+				return false;
+			}
+
+			if (left_variable_state == VariableState.BOUND && !prop.multiple_values && right_variable_state == 0) {
+				bool in_domain = false;
+				foreach (VariableBinding binding in triple_context.var_map.lookup (left_variable).list) {
+					if (binding.type != null && is_subclass (binding.type, prop.domain)) {
+						in_domain = true;
+						break;
+					}
+				}
+
+				if (in_domain) {
+					// first valid case described in above comment
+					return true;
+				}
+			} else if (left_variable_state == 0 && prop.is_inverse_functional_property && right_variable_state == VariableState.BOUND) {
+				// second valid case described in above comment
+				return true;
+			}
+
+			// no match
+			return false;
+		} catch (SparqlError e) {
+			return false;
+		} finally {
+			// in any case, go back to the start of the optional
+			set_location (optional_start);
+		}
+	}
+
+	internal void translate_group_graph_pattern (StringBuilder sql) throws SparqlError {
+		expect (SparqlTokenType.OPEN_BRACE);
+
+		if (current () == SparqlTokenType.SELECT) {
+			translate_select (sql, true);
+
+			// only export selected variables
+			context.var_set = context.select_var_set;
+			context.select_var_set = new HashTable<Variable,int>.full (direct_hash, direct_equal, g_object_unref, null);
+
+			expect (SparqlTokenType.CLOSE_BRACE);
+			return;
+		}
+
+		SourceLocation[] filters = { };
+
+		bool in_triples_block = false;
+		bool in_group_graph_pattern = false;
+		bool first_where = true;
+		bool found_simple_optional = false;
+		long group_graph_pattern_start = sql.len;
+
+		// optional TriplesBlock
+		parse_triples (sql, group_graph_pattern_start, ref in_triples_block, ref first_where, ref in_group_graph_pattern, found_simple_optional);
+
+		while (true) {
+			// check whether we have GraphPatternNotTriples | Filter
+			if (accept (SparqlTokenType.OPTIONAL)) {
+				if (!in_group_graph_pattern && is_simple_optional ()) {
+					// perform join-less optional (like non-optional except for the IS NOT NULL check)
+					found_simple_optional = true;
+					expect (SparqlTokenType.OPEN_BRACE);
+
+					current_subject = parse_var_or_term (sql, out current_subject_is_var);
+					parse_property_list_not_empty (sql, true);
+
+					accept (SparqlTokenType.DOT);
+					expect (SparqlTokenType.CLOSE_BRACE);
+				} else {
+					if (!in_triples_block && !in_group_graph_pattern) {
+						// expand { OPTIONAL { ... } } into { { } OPTIONAL { ... } }
+						// empty graph pattern => return one result without bound variables
+						sql.append ("SELECT 1");
+					} else if (in_triples_block) {
+						end_triples_block (sql, ref first_where, in_group_graph_pattern);
+						in_triples_block = false;
+					}
+					if (!in_group_graph_pattern) {
+						in_group_graph_pattern = true;
+					}
+
+					var select = new StringBuilder ("SELECT ");
+
+					int left_index = ++next_table_index;
+					int right_index = ++next_table_index;
+
+					sql.append_printf (") AS t%d_g LEFT JOIN (", left_index);
+
+					context = new Context (context);
+
+					translate_group_graph_pattern (sql);
+
+					sql.append_printf (") AS t%d_g", right_index);
+
+					bool first = true;
+					bool first_common = true;
+					foreach (var v in context.var_set.get_keys ()) {
+						if (first) {
+							first = false;
+						} else {
+							select.append (", ");
+						}
+
+						var old_state = context.parent_context.var_set.lookup (v);
+						if (old_state == 0) {
+							// first used in optional part
+							context.parent_context.var_set.insert (v, VariableState.OPTIONAL);
+							select.append_printf ("t%d_g.%s", right_index, v.sql_expression);
+						} else {
+							if (first_common) {
+								sql.append (" ON ");
+								first_common = false;
+							} else {
+								sql.append (" AND ");
+							}
+
+							if (old_state == VariableState.BOUND) {
+								// variable definitely bound in non-optional part
+								sql.append_printf ("t%d_g.%s = t%d_g.%s", left_index, v.sql_expression, right_index, v.sql_expression);
+								select.append_printf ("t%d_g.%s", left_index, v.sql_expression);
+							} else if (old_state == VariableState.OPTIONAL) {
+								// variable maybe bound in non-optional part
+								sql.append_printf ("(t%d_g.%s IS NULL OR t%d_g.%s = t%d_g.%s)", left_index, v.sql_expression, left_index, v.sql_expression, right_index, v.sql_expression);
+								select.append_printf ("COALESCE (t%d_g.%s, t%d_g.%s) AS %s", left_index, v.sql_expression, right_index, v.sql_expression, v.sql_expression);
+							}
+						}
+					}
+					foreach (var v in context.parent_context.var_set.get_keys ()) {
+						if (context.var_set.lookup (v) == 0) {
+							// only used in non-optional part
+							if (first) {
+								first = false;
+							} else {
+								select.append (", ");
+							}
+
+							select.append_printf ("t%d_g.%s", left_index, v.sql_expression);
+						}
+					}
+					if (first) {
+						// no variables used at all
+						select.append ("1");
+					}
+
+					context = context.parent_context;
+
+					select.append (" FROM (");
+					sql.insert (group_graph_pattern_start, select.str);
+
+					// surround with SELECT * FROM (...) to avoid ambiguous column names
+					// in SQL generated for FILTER (triggered by using table aliases for join sources)
+					sql.insert (group_graph_pattern_start, "SELECT * FROM (");
+					sql.append (")");
+				}
+			} else if (accept (SparqlTokenType.GRAPH)) {
+				var old_graph = current_graph;
+				var old_graph_is_var = current_graph_is_var;
+				current_graph = parse_var_or_term (sql, out current_graph_is_var);
+
+				if (!in_triples_block && !in_group_graph_pattern) {
+					in_group_graph_pattern = true;
+					translate_group_or_union_graph_pattern (sql);
+				} else {
+					if (in_triples_block) {
+						end_triples_block (sql, ref first_where, in_group_graph_pattern);
+						in_triples_block = false;
+					}
+					if (!in_group_graph_pattern) {
+						in_group_graph_pattern = true;
+					}
+
+					sql.insert (group_graph_pattern_start, "SELECT * FROM (");
+					sql.append (") NATURAL INNER JOIN (");
+					translate_group_or_union_graph_pattern (sql);
+					sql.append (")");
+				}
+
+				current_graph = old_graph;
+				current_graph_is_var = old_graph_is_var;
+			} else if (current () == SparqlTokenType.OPEN_BRACE) {
+				if (!in_triples_block && !in_group_graph_pattern) {
+					in_group_graph_pattern = true;
+					translate_group_or_union_graph_pattern (sql);
+				} else {
+					if (in_triples_block) {
+						end_triples_block (sql, ref first_where, in_group_graph_pattern);
+						in_triples_block = false;
+					}
+					if (!in_group_graph_pattern) {
+						in_group_graph_pattern = true;
+					}
+
+					sql.insert (group_graph_pattern_start, "SELECT * FROM (");
+					sql.append (") NATURAL INNER JOIN (");
+					translate_group_or_union_graph_pattern (sql);
+					sql.append (")");
+				}
+			} else if (current () == SparqlTokenType.FILTER) {
+				filters += get_location ();
+				skip_filter ();
+			} else {
+				break;
+			}
+
+			accept (SparqlTokenType.DOT);
+
+			// optional TriplesBlock
+			parse_triples (sql, group_graph_pattern_start, ref in_triples_block, ref first_where, ref in_group_graph_pattern, found_simple_optional);
+		}
+
+		expect (SparqlTokenType.CLOSE_BRACE);
+
+		if (!in_triples_block && !in_group_graph_pattern) {
+			// empty graph pattern => return one result without bound variables
+			sql.append ("SELECT 1");
+		} else if (in_triples_block) {
+			end_triples_block (sql, ref first_where, in_group_graph_pattern);
+			in_triples_block = false;
+		}
+
+		if (in_group_graph_pattern) {
+			first_where = true;
+		}
+
+		// handle filters last, they apply to the pattern as a whole
+		if (filters.length > 0) {
+			var end = get_location ();
+
+			foreach (var filter_location in filters) {
+				if (!first_where) {
+					sql.append (" AND ");
+				} else {
+					sql.append (" WHERE ");
+					first_where = false;
+				}
+
+				set_location (filter_location);
+				translate_filter (sql);
+			}
+
+			set_location (end);
+		}
+	}
+
+	void translate_group_or_union_graph_pattern (StringBuilder sql) throws SparqlError {
+		Variable[] all_vars = { };
+		HashTable<Variable,int> all_var_set = new HashTable<Variable,int>.full (direct_hash, direct_equal, g_object_unref, null);
+
+		Context[] contexts = { };
+		long[] offsets = { };
+
+		do {
+			context = new Context (context);
+
+			contexts += context;
+			offsets += sql.len;
+			translate_group_graph_pattern (sql);
+
+			context = context.parent_context;
+		} while (accept (SparqlTokenType.UNION));
+
+		if (contexts.length > 1) {
+			// union graph pattern
+
+			// create union of all variables
+			foreach (var sub_context in contexts) {
+				foreach (var v in sub_context.var_set.get_keys ()) {
+					if (all_var_set.lookup (v) == 0) {
+						all_vars += v;
+						all_var_set.insert (v, VariableState.BOUND);
+						context.var_set.insert (v, VariableState.BOUND);
+					}
+				}
+			}
+
+			long extra_offset = 0;
+			for (int i = 0; i < contexts.length; i++) {
+				var projection = new StringBuilder ();
+				if (i > 0) {
+					projection.append (") UNION ALL ");
+				}
+				projection.append ("SELECT ");
+				foreach (var v in all_vars) {
+					if (contexts[i].var_set.lookup (v) == 0) {
+						// variable not used in this subgraph
+						// use NULL
+						projection.append ("NULL AS ");
+					}
+					projection.append_printf ("%s, ", v.sql_expression);
+				}
+				// delete last comma and space
+				projection.truncate (projection.len - 2);
+				projection.append (" FROM (");
+
+				sql.insert (offsets[i] + extra_offset, projection.str);
+				extra_offset += projection.len;
+			}
+			sql.append (")");
+		} else {
+			foreach (var key in contexts[0].var_set.get_keys ()) {
+				context.var_set.insert (key, VariableState.BOUND);
+			}
+		}
+	}
+
+	VariableBindingList? get_variable_binding_list (Variable variable) {
+		VariableBindingList binding_list = null;
+		if (triple_context != null) {
+			binding_list = triple_context.var_map.lookup (variable);
+		}
+		if (binding_list == null && context.in_scalar_subquery) {
+			// in scalar subquery: check variables of outer queries
+			var parent_context = context.parent_context;
+			while (parent_context != null) {
+				var outer_var = parent_context.var_map.lookup (variable.name);
+				if (outer_var != null && outer_var.binding != null) {
+					// capture outer variable
+					var binding = new VariableBinding ();
+					binding.data_type = outer_var.binding.data_type;
+					binding.variable = context.get_variable (variable.name);
+					binding.type = outer_var.binding.type;
+					binding.sql_expression = outer_var.sql_expression;
+					binding_list = new VariableBindingList ();
+					if (triple_context != null) {
+						triple_context.variables.append (binding.variable);
+						triple_context.var_map.insert (binding.variable, binding_list);
+					}
+
+					context.var_set.insert (binding.variable, VariableState.BOUND);
+					binding_list.list.append (binding);
+					binding.variable.binding = binding;
+					break;
+				}
+				parent_context = parent_context.parent_context;
+			}
+		}
+		return binding_list;
+	}
+
+	internal void add_variable_binding (StringBuilder sql, VariableBinding binding, VariableState variable_state) {
+		var binding_list = get_variable_binding_list (binding.variable);
+		if (binding_list == null) {
+			binding_list = new VariableBindingList ();
+			if (triple_context != null) {
+				triple_context.variables.append (binding.variable);
+				triple_context.var_map.insert (binding.variable, binding_list);
+			}
+
+			sql.append_printf ("%s AS %s, ",
+				binding.sql_expression,
+				binding.variable.sql_expression);
+
+			if (binding.data_type == PropertyType.DATETIME) {
+				sql.append_printf ("%s AS %s, ",
+					binding.get_extra_sql_expression ("localDate"),
+					binding.variable.get_extra_sql_expression ("localDate"));
+				sql.append_printf ("%s AS %s, ",
+					binding.get_extra_sql_expression ("localTime"),
+					binding.variable.get_extra_sql_expression ("localTime"));
+			}
+
+			context.var_set.insert (binding.variable, variable_state);
+		}
+		binding_list.list.append (binding);
+		if (binding.variable.binding == null) {
+			binding.variable.binding = binding;
+		}
+	}
+
+	void parse_object (StringBuilder sql, bool in_simple_optional = false) throws SparqlError {
+		bool object_is_var;
+		string object = parse_var_or_term (sql, out object_is_var);
+
+		string db_table;
+		bool rdftype = false;
+		bool share_table = true;
+		bool is_fts_match = false;
+
+		bool newtable;
+		DataTable table;
+		Property prop = null;
+
+		Class subject_type = null;
+
+		if (!current_predicate_is_var) {
+			prop = Ontologies.get_property_by_uri (current_predicate);
+
+			if (current_predicate == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
+			    && !object_is_var) {
+				// rdf:type query
+				rdftype = true;
+				var cl = Ontologies.get_class_by_uri (object);
+				if (cl == null) {
+					throw new SparqlError.UNKNOWN_CLASS ("Unknown class `%s'".printf (object));
+				}
+				db_table = cl.name;
+				subject_type = cl;
+			} else if (prop == null) {
+				if (current_predicate == "http://www.tracker-project.org/ontologies/fts#match";) {
+					// fts:match
+					db_table = "fts";
+					share_table = false;
+					is_fts_match = true;
+				} else {
+					throw new SparqlError.UNKNOWN_PROPERTY ("Unknown property `%s'".printf (current_predicate));
+				}
+			} else {
+				if (current_predicate == "http://www.w3.org/2000/01/rdf-schema#domain";
+				    && current_subject_is_var
+				    && !object_is_var) {
+					// rdfs:domain
+					var domain = Ontologies.get_class_by_uri (object);
+					if (domain == null) {
+						throw new SparqlError.UNKNOWN_CLASS ("Unknown class `%s'".printf (object));
+					}
+					var pv = context.predicate_variable_map.lookup (context.get_variable (current_subject));
+					if (pv == null) {
+						pv = new PredicateVariable ();
+						context.predicate_variable_map.insert (context.get_variable (current_subject), pv);
+					}
+					pv.domain = domain;
+				}
+
+				db_table = prop.table_name;
+				if (prop.multiple_values) {
+					// we can never share the table with multiple triples
+					// for multi value properties as a property may consist of multiple rows
+					share_table = false;
+				}
+				subject_type = prop.domain;
+
+				if (in_simple_optional && context.var_set.lookup (context.get_variable (current_subject)) == 0) {
+					// use subselect instead of join in simple optional where the subject is the unbound variable
+					// this can only happen with inverse functional properties
+					var binding = new VariableBinding ();
+					binding.data_type = PropertyType.RESOURCE;
+					binding.variable = context.get_variable (current_subject);
+
+					assert (triple_context.var_map.lookup (binding.variable) == null);
+					var binding_list = new VariableBindingList ();
+					triple_context.variables.append (binding.variable);
+					triple_context.var_map.insert (binding.variable, binding_list);
+
+					// need to use table and column name for object, can't refer to variable in nested select
+					var object_binding = triple_context.var_map.lookup (context.get_variable (object)).list.data;
+
+					sql.append_printf ("(SELECT ID FROM \"%s\" WHERE \"%s\" = %s) AS %s, ",
+						db_table,
+						prop.name,
+						object_binding.sql_expression,
+						binding.variable.sql_expression);
+
+					context.var_set.insert (binding.variable, VariableState.OPTIONAL);
+					binding_list.list.append (binding);
+
+					assert (binding.variable.binding == null);
+					binding.variable.binding = binding;
+
+					return;
+				}
+			}
+			table = get_table (current_subject, db_table, share_table, out newtable);
+		} else {
+			// variable in predicate
+			newtable = true;
+			table = new DataTable ();
+			table.predicate_variable = context.predicate_variable_map.lookup (context.get_variable (current_predicate));
+			if (table.predicate_variable == null) {
+				table.predicate_variable = new PredicateVariable ();
+				context.predicate_variable_map.insert (context.get_variable (current_predicate), table.predicate_variable);
+			}
+			if (!current_subject_is_var) {
+				// single subject
+				table.predicate_variable.subject = current_subject;
+			}
+			if (!current_subject_is_var) {
+				// single object
+				table.predicate_variable.object = object;
+			}
+			table.sql_query_tablename = current_predicate + (++counter).to_string ();
+			triple_context.tables.append (table);
+
+			// add to variable list
+			var binding = new VariableBinding ();
+			binding.data_type = PropertyType.RESOURCE;
+			binding.variable = context.get_variable (current_predicate);
+			binding.table = table;
+			binding.sql_db_column_name = "predicate";
+
+			add_variable_binding (sql, binding, VariableState.BOUND);
+		}
+
+		if (newtable) {
+			if (current_subject_is_var) {
+				var binding = new VariableBinding ();
+				binding.data_type = PropertyType.RESOURCE;
+				binding.variable = context.get_variable (current_subject);
+				binding.table = table;
+				binding.type = subject_type;
+				if (is_fts_match) {
+					binding.sql_db_column_name = "rowid";
+				} else {
+					binding.sql_db_column_name = "ID";
+				}
+
+				add_variable_binding (sql, binding, VariableState.BOUND);
+			} else {
+				var binding = new LiteralBinding ();
+				binding.data_type = PropertyType.RESOURCE;
+				binding.literal = current_subject;
+				// binding.data_type = triple.subject.type;
+				binding.table = table;
+				binding.sql_db_column_name = "ID";
+				triple_context.bindings.append (binding);
+			}
+		}
+
+		if (!rdftype) {
+			if (object_is_var) {
+				var binding = new VariableBinding ();
+				binding.variable = context.get_variable (object);
+				binding.table = table;
+				if (prop != null) {
+
+					binding.type = prop.range;
+
+					binding.data_type = prop.data_type;
+					binding.sql_db_column_name = prop.name;
+					if (!prop.multiple_values) {
+						// for single value properties, row may have NULL
+						// in any column except the ID column
+						binding.maybe_null = true;
+						binding.in_simple_optional = in_simple_optional;
+					}
+				} else {
+					// variable as predicate
+					binding.sql_db_column_name = "object";
+					binding.maybe_null = true;
+				}
+
+				VariableState state;
+				if (in_simple_optional) {
+					state = VariableState.OPTIONAL;
+				} else {
+					state = VariableState.BOUND;
+				}
+
+				add_variable_binding (sql, binding, state);
+			} else if (is_fts_match) {
+				var binding = new LiteralBinding ();
+				binding.is_fts_match = true;
+				binding.literal = object;
+				// binding.data_type = triple.object.type;
+				binding.table = table;
+				binding.sql_db_column_name = "fts";
+				triple_context.bindings.append (binding);
+
+				sql.append_printf ("rank(\"%s\".\"fts\") AS \"%s_u_rank\", ",
+					binding.table.sql_query_tablename,
+					context.get_variable (current_subject).name);
+				sql.append_printf ("offsets(\"%s\".\"fts\") AS \"%s_u_offsets\", ",
+					binding.table.sql_query_tablename,
+					context.get_variable (current_subject).name);
+			} else {
+				var binding = new LiteralBinding ();
+				binding.literal = object;
+				// binding.data_type = triple.object.type;
+				binding.table = table;
+				if (prop != null) {
+					binding.data_type = prop.data_type;
+					binding.sql_db_column_name = prop.name;
+				} else {
+					// variable as predicate
+					binding.sql_db_column_name = "object";
+				}
+				triple_context.bindings.append (binding);
+			}
+
+			if (current_graph != null && prop != null) {
+				if (current_graph_is_var) {
+					var binding = new VariableBinding ();
+					binding.variable = context.get_variable (current_graph);
+					binding.table = table;
+
+					binding.data_type = PropertyType.RESOURCE;
+					binding.sql_db_column_name = prop.name + ":graph";
+					binding.maybe_null = true;
+					binding.in_simple_optional = in_simple_optional;
+
+					VariableState state;
+					if (in_simple_optional) {
+						state = VariableState.OPTIONAL;
+					} else {
+						state = VariableState.BOUND;
+					}
+
+					add_variable_binding (sql, binding, state);
+				} else {
+					var binding = new LiteralBinding ();
+					binding.literal = current_graph;
+					binding.table = table;
+
+					binding.data_type = PropertyType.RESOURCE;
+					binding.sql_db_column_name = prop.name + ":graph";
+					triple_context.bindings.append (binding);
+				}
+			}
+		}
+
+		if (!current_subject_is_var &&
+		    !current_predicate_is_var &&
+		    !object_is_var) {
+			// no variables involved, add dummy expression to SQL
+			sql.append ("1, ");
+		}
+	}
+
+	DataTable get_table (string subject, string db_table, bool share_table, out bool newtable) {
+		string tablestring = "%s.%s".printf (subject, db_table);
+		DataTable table = null;
+		newtable = false;
+		if (share_table) {
+			table = triple_context.table_map.lookup (tablestring);
+		}
+		if (table == null) {
+			newtable = true;
+			table = new DataTable ();
+			table.sql_db_tablename = db_table;
+			table.sql_query_tablename = db_table + (++counter).to_string ();
+			triple_context.tables.append (table);
+			triple_context.table_map.insert (tablestring, table);
+		}
+		return table;
+	}
+}
diff --git a/src/libtracker-data/tracker-sparql-query.vala b/src/libtracker-data/tracker-sparql-query.vala
index f5b84d7..fccb39d 100644
--- a/src/libtracker-data/tracker-sparql-query.vala
+++ b/src/libtracker-data/tracker-sparql-query.vala
@@ -26,11 +26,7 @@ public errordomain Tracker.SparqlError {
 	UNSUPPORTED
 }
 
-public class Tracker.SparqlQuery : Object {
-	bool maybe_numeric (PropertyType type) {
-		return (type == PropertyType.INTEGER || type == PropertyType.DOUBLE || type == PropertyType.DATETIME || type == PropertyType.UNKNOWN);
-	}
-
+namespace Tracker.Sparql {
 	enum VariableState {
 		NONE,
 		BOUND,
@@ -101,128 +97,6 @@ public class Tracker.SparqlQuery : Object {
 		}
 	}
 
-	// Represents a variable used as a predicate
-	class PredicateVariable : Object {
-		public string? subject;
-		public string? object;
-
-		public Class? domain;
-
-		public string get_sql_query (SparqlQuery query) throws SparqlError {
-			try {
-				var sql = new StringBuilder ();
-
-				if (subject != null) {
-					// single subject
-					var subject_id = Data.query_resource_id (subject);
-
-					DBResultSet result_set = null;
-					if (subject_id > 0) {
-						var iface = DBManager.get_db_interface ();
-						var stmt = iface.create_statement ("SELECT (SELECT Uri FROM Resource WHERE ID = \"rdf:type\") FROM \"rdfs:Resource_rdf:type\" WHERE ID = ?");
-						stmt.bind_int (0, subject_id);
-						result_set = stmt.execute ();
-					}
-
-					if (result_set != null) {
-						bool first = true;
-						do {
-							Value value;
-							result_set._get_value (0, out value);
-							var domain = Ontologies.get_class_by_uri (value.get_string ());
-
-							foreach (Property prop in Ontologies.get_properties ()) {
-								if (prop.domain == domain) {
-									if (first) {
-										first = false;
-									} else {
-										sql.append (" UNION ALL ");
-									}
-									sql.append_printf ("SELECT ID, (SELECT ID FROM Resource WHERE Uri = '%s') AS \"predicate\", ", prop.uri);
-
-									append_expression_as_string (sql, "\"%s\"".printf (prop.name), prop.data_type);
-
-									sql.append (" AS \"object\" FROM ");
-									sql.append_printf ("\"%s\"", prop.table_name);
-
-									sql.append (" WHERE ID = ?");
-
-									var binding = new LiteralBinding ();
-									binding.literal = subject_id.to_string ();
-									binding.data_type = PropertyType.INTEGER;
-									query.bindings.append (binding);
-								}
-							}
-						} while (result_set.iter_next ());
-					} else {
-						/* no match */
-						sql.append ("SELECT NULL AS ID, NULL AS \"predicate\", NULL AS \"object\"");
-					}
-				} else if (object != null) {
-					// single object
-					var object_id = Data.query_resource_id (object);
-
-					var iface = DBManager.get_db_interface ();
-					var stmt = iface.create_statement ("SELECT (SELECT Uri FROM Resource WHERE ID = \"rdf:type\") FROM \"rdfs:Resource_rdf:type\" WHERE ID = ?");
-					stmt.bind_int (0, object_id);
-					var result_set = stmt.execute ();
-
-					bool first = true;
-					if (result_set != null) {
-						do {
-							Value value;
-							result_set._get_value (0, out value);
-							var range = Ontologies.get_class_by_uri (value.get_string ());
-
-							foreach (Property prop in Ontologies.get_properties ()) {
-								if (prop.range == range) {
-									if (first) {
-										first = false;
-									} else {
-										sql.append (" UNION ALL ");
-									}
-									sql.append_printf ("SELECT ID, (SELECT ID FROM Resource WHERE Uri = '%s') AS \"predicate\", ", prop.uri);
-
-									append_expression_as_string (sql, "\"%s\"".printf (prop.name), prop.data_type);
-
-									sql.append (" AS \"object\" FROM ");
-									sql.append_printf ("\"%s\"", prop.table_name);
-								}
-							}
-						} while (result_set.iter_next ());
-					} else {
-						/* no match */
-						sql.append ("SELECT NULL AS ID, NULL AS \"predicate\", NULL AS \"object\"");
-					}
-				} else if (domain != null) {
-					// any subject, predicates limited to a specific domain
-					bool first = true;
-					foreach (Property prop in Ontologies.get_properties ()) {
-						if (prop.domain == domain) {
-							if (first) {
-								first = false;
-							} else {
-								sql.append (" UNION ALL ");
-							}
-							sql.append_printf ("SELECT ID, (SELECT ID FROM Resource WHERE Uri = '%s') AS \"predicate\", ", prop.uri);
-
-							append_expression_as_string (sql, "\"%s\"".printf (prop.name), prop.data_type);
-
-							sql.append (" AS \"object\" FROM ");
-							sql.append_printf ("\"%s\"", prop.table_name);
-						}
-					}
-				} else {
-					// UNION over all properties would exceed SQLite limits
-					throw query.get_internal_error ("Unrestricted predicate variables not supported");
-				}
-				return sql.str;
-			} catch (DBInterfaceError e) {
-				throw new SparqlError.INTERNAL (e.message);
-			}
-		}
-	}
-
 	class Context {
 		public Context? parent_context;
 		// All SPARQL variables within a subgraph pattern (used by UNION)
@@ -269,29 +143,31 @@ public class Tracker.SparqlQuery : Object {
 			used_sql_identifiers = new HashTable<string,bool>.full (str_hash, str_equal, g_free, null);
 			in_scalar_subquery = true;
 		}
-	}
 
-	class TripleContext {
-		// SQL tables
-		public List<DataTable> tables;
-		public HashTable<string,DataTable> table_map;
-		// SPARQL literals
-		public List<LiteralBinding> bindings;
-		// SPARQL variables
-		public List<Variable> variables;
-		public HashTable<Variable,VariableBindingList> var_map;
+		internal unowned Variable get_variable (string name) {
+			unowned Variable result = this.var_map.lookup (name);
+			if (result == null) {
+				// use lowercase as SQLite is never case sensitive (not conforming to SQL)
+				string sql_identifier = "%s_u".printf (name).down ();
 
-		public TripleContext () {
-			tables = new List<DataTable> ();
-			table_map = new HashTable<string,DataTable>.full (str_hash, str_equal, g_free, g_object_unref);
+				// ensure SQL identifier is unique to avoid conflicts between
+				// case sensitive SPARQL and case insensitive SQLite
+				for (int i = 1; this.used_sql_identifiers.lookup (sql_identifier); i++) {
+					sql_identifier = "%s_%d_u".printf (name, i).down ();
+				}
+				this.used_sql_identifiers.insert (sql_identifier, true);
 
-			variables = new List<Variable> ();
-			var_map = new HashTable<Variable,VariableBindingList>.full (direct_hash, direct_equal, g_object_unref, g_object_unref);
+				var variable = new Variable (name, sql_identifier);
+				this.var_map.insert (name, variable);
 
-			bindings = new List<LiteralBinding> ();
+				result = variable;
+			}
+			return result;
 		}
 	}
+}
 
+public class Tracker.Sparql.Query : Object {
 	SparqlScanner scanner;
 
 	// token buffer
@@ -309,14 +185,14 @@ public class Tracker.SparqlQuery : Object {
 		public SourceLocation end;
 	}
 
-	const string XSD_NS = "http://www.w3.org/2001/XMLSchema#";;
 	const string FN_NS = "http://www.w3.org/2005/xpath-functions#";;
-	const string FTS_NS = "http://www.tracker-project.org/ontologies/fts#";;
-	const string TRACKER_NS = "http://www.tracker-project.org/ontologies/tracker#";;
 
 	string query_string;
 	bool update_extensions;
 
+	internal Expression expression;
+	internal Pattern pattern;
+
 	string current_graph;
 	bool current_graph_is_var;
 	string current_subject;
@@ -324,26 +200,21 @@ public class Tracker.SparqlQuery : Object {
 	string current_predicate;
 	bool current_predicate_is_var;
 
-	int next_table_index;
-
 	HashTable<string,string> prefix_map;
 
 	// All SPARQL literals
-	List<LiteralBinding> bindings;
+	internal List<LiteralBinding> bindings;
 
-	Context context;
-	TripleContext? triple_context;
+	internal Context context;
 
 	bool delete_statements;
 
-	int counter;
-
 	int bnodeid = 0;
 	// base UUID used for blank nodes
 	uchar[] base_uuid;
 	HashTable<string,string> blank_nodes;
 
-	public SparqlQuery (string query) {
+	public Query (string query) {
 		tokens = new TokenInfo[BUFFER_SIZE];
 		prefix_map = new HashTable<string,string>.full (str_hash, str_equal, g_free, g_free);
 
@@ -351,9 +222,12 @@ public class Tracker.SparqlQuery : Object {
 		uuid_generate (base_uuid);
 
 		this.query_string = query;
+
+		expression = new Expression (this);
+		pattern = new Pattern (this);
 	}
 
-	public SparqlQuery.update (string query) {
+	public Query.update (string query) {
 		this (query);
 		this.update_extensions = true;
 	}
@@ -373,7 +247,7 @@ public class Tracker.SparqlQuery : Object {
 			sha1, sha1.offset (8), sha1.offset (12), sha1.offset (16), sha1.offset (20));
 	}
 
-	string generate_bnodeid (string? user_bnodeid) {
+	internal string generate_bnodeid (string? user_bnodeid) {
 		// user_bnodeid is NULL for anonymous nodes
 		if (user_bnodeid == null) {
 			return ":%d".printf (++bnodeid);
@@ -404,7 +278,7 @@ public class Tracker.SparqlQuery : Object {
 		}
 	}
 
-	inline bool next () throws SparqlError {
+	internal bool next () throws SparqlError {
 		index = (index + 1) % BUFFER_SIZE;
 		size--;
 		if (size <= 0) {
@@ -418,16 +292,16 @@ public class Tracker.SparqlQuery : Object {
 		return (tokens[index].type != SparqlTokenType.EOF);
 	}
 
-	inline SparqlTokenType current () {
+	internal SparqlTokenType current () {
 		return tokens[index].type;
 	}
 
-	inline SparqlTokenType last () {
+	internal SparqlTokenType last () {
 		int last_index = (index + BUFFER_SIZE - 1) % BUFFER_SIZE;
 		return tokens[last_index].type;
 	}
 
-	inline bool accept (SparqlTokenType type) throws SparqlError {
+	internal bool accept (SparqlTokenType type) throws SparqlError {
 		if (current () == type) {
 			next ();
 			return true;
@@ -435,15 +309,15 @@ public class Tracker.SparqlQuery : Object {
 		return false;
 	}
 
-	SparqlError get_error (string msg) {
+	internal SparqlError get_error (string msg) {
 		return new SparqlError.PARSE ("%d.%d: syntax error, %s".printf (tokens[index].begin.line, tokens[index].begin.column, msg));
 	}
 
-	SparqlError get_internal_error (string msg) {
+	internal SparqlError get_internal_error (string msg) {
 		return new SparqlError.INTERNAL ("%d.%d: %s".printf (tokens[index].begin.line, tokens[index].begin.column, msg));
 	}
 
-	bool expect (SparqlTokenType type) throws SparqlError {
+	internal bool expect (SparqlTokenType type) throws SparqlError {
 		if (accept (type)) {
 			return true;
 		}
@@ -451,11 +325,11 @@ public class Tracker.SparqlQuery : Object {
 		throw get_error ("expected %s".printf (type.to_string ()));
 	}
 
-	inline SourceLocation get_location () {
+	internal SourceLocation get_location () {
 		return tokens[index].begin;
 	}
 
-	void set_location (SourceLocation location) {
+	internal void set_location (SourceLocation location) {
 		scanner.seek (location);
 		size = 0;
 		index = 0;
@@ -467,36 +341,11 @@ public class Tracker.SparqlQuery : Object {
 		}
 	}
 
-	string get_last_string (int strip = 0) {
+	internal string get_last_string (int strip = 0) {
 		int last_index = (index + BUFFER_SIZE - 1) % BUFFER_SIZE;
 		return ((string) (tokens[last_index].begin.pos + strip)).ndup ((tokens[last_index].end.pos - tokens[last_index].begin.pos - 2 * strip));
 	}
 
-	string escape_sql_string_literal (string literal) {
-		return "'%s'".printf (string.joinv ("''", literal.split ("'")));
-	}
-
-	unowned Variable get_variable (string name) {
-		unowned Variable result = context.var_map.lookup (name);
-		if (result == null) {
-			// use lowercase as SQLite is never case sensitive (not conforming to SQL)
-			string sql_identifier = "%s_u".printf (name).down ();
-
-			// ensure SQL identifier is unique to avoid conflicts between
-			// case sensitive SPARQL and case insensitive SQLite
-			for (int i = 1; context.used_sql_identifiers.lookup (sql_identifier); i++) {
-				sql_identifier = "%s_%d_u".printf (name, i).down ();
-			}
-			context.used_sql_identifiers.insert (sql_identifier, true);
-
-			var variable = new Variable (name, sql_identifier);
-			context.var_map.insert (name, variable);
-
-			result = variable;
-		}
-		return result;
-	}
-
 	void parse_prologue () throws SparqlError {
 		if (accept (SparqlTokenType.BASE)) {
 			expect (SparqlTokenType.IRI_REF);
@@ -622,135 +471,6 @@ public class Tracker.SparqlQuery : Object {
 		return stmt.execute ();
 	}
 
-	void skip_bracketted_expression () throws SparqlError {
-		expect (SparqlTokenType.OPEN_PARENS);
-		while (true) {
-			switch (current ()) {
-			case SparqlTokenType.OPEN_PARENS:
-				// skip nested bracketted expression
-				skip_bracketted_expression ();
-				continue;
-			case SparqlTokenType.CLOSE_PARENS:
-			case SparqlTokenType.EOF:
-				break;
-			default:
-				next ();
-				continue;
-			}
-			break;
-		}
-		expect (SparqlTokenType.CLOSE_PARENS);
-	}
-
-	void skip_select_variables () throws SparqlError {
-		while (true) {
-			switch (current ()) {
-			case SparqlTokenType.OPEN_PARENS:
-				skip_bracketted_expression ();
-				continue;
-			case SparqlTokenType.FROM:
-			case SparqlTokenType.WHERE:
-			case SparqlTokenType.OPEN_BRACE:
-			case SparqlTokenType.GROUP:
-			case SparqlTokenType.ORDER:
-			case SparqlTokenType.LIMIT:
-			case SparqlTokenType.OFFSET:
-			case SparqlTokenType.EOF:
-				break;
-			default:
-				next ();
-				continue;
-			}
-			break;
-		}
-	}
-
-	PropertyType translate_select_expression (StringBuilder sql, bool subquery) throws SparqlError {
-		Variable variable = null;
-
-		long begin = sql.len;
-		var type = PropertyType.UNKNOWN;
-		if (accept (SparqlTokenType.COUNT)) {
-			sql.append ("COUNT(");
-			translate_aggregate_expression (sql);
-			sql.append (")");
-			type = PropertyType.INTEGER;
-		} else if (accept (SparqlTokenType.SUM)) {
-			sql.append ("SUM(");
-			type = translate_aggregate_expression (sql);
-			sql.append (")");
-		} else if (accept (SparqlTokenType.AVG)) {
-			sql.append ("AVG(");
-			type = translate_aggregate_expression (sql);
-			sql.append (")");
-		} else if (accept (SparqlTokenType.MIN)) {
-			sql.append ("MIN(");
-			type = translate_aggregate_expression (sql);
-			sql.append (")");
-		} else if (accept (SparqlTokenType.MAX)) {
-			sql.append ("MAX(");
-			type = translate_aggregate_expression (sql);
-			sql.append (")");
-		} else if (accept (SparqlTokenType.GROUP_CONCAT)) {
-			sql.append ("GROUP_CONCAT(");
-			expect (SparqlTokenType.OPEN_PARENS);
-			translate_expression_as_string (sql);
-			sql.append (", ");
-			expect (SparqlTokenType.COMMA);
-			sql.append (escape_sql_string_literal (parse_string_literal ()));
-			sql.append (")");
-			expect (SparqlTokenType.CLOSE_PARENS);
-			type = PropertyType.STRING;
-		} else if (current () == SparqlTokenType.VAR) {
-			type = translate_expression (sql);
-			// we need variable name in case of compositional subqueries
-			variable = get_variable (get_last_string ().substring (1));
-
-			if (variable.binding == null) {
-				throw get_error ("use of undefined variable `%s'".printf (variable.name));
-			}
-		} else {
-			type = translate_expression (sql);
-		}
-
-		if (!subquery) {
-			convert_expression_to_string (sql, type, begin);
-			type = PropertyType.STRING;
-		}
-
-		if (accept (SparqlTokenType.AS)) {
-			if (accept (SparqlTokenType.PN_PREFIX)) {
-				// deprecated but supported for backward compatibility
-				// (...) AS foo
-				variable = get_variable (get_last_string ());
-			} else {
-				// syntax from SPARQL 1.1 Draft
-				// (...) AS ?foo
-				expect (SparqlTokenType.VAR);
-				variable = get_variable (get_last_string ().substring (1));
-			}
-			sql.append_printf (" AS %s", variable.sql_expression);
-
-			if (subquery) {
-				var binding = new VariableBinding ();
-				binding.data_type = type;
-				binding.variable = variable;
-				binding.sql_expression = variable.sql_expression;
-				add_variable_binding (new StringBuilder (), binding, VariableState.BOUND);
-			}
-		}
-
-		if (variable != null) {
-			int state = context.var_set.lookup (variable);
-			if (state == 0) {
-				state = VariableState.BOUND;
-			}
-			context.select_var_set.insert (variable, state);
-		}
-
-		return type;
-	}
-
 	DBResultSet? execute_select () throws DBInterfaceError, SparqlError, DateError {
 		// SELECT query
 
@@ -758,7 +478,7 @@ public class Tracker.SparqlQuery : Object {
 
 		// build SQL
 		var sql = new StringBuilder ();
-		translate_select (sql);
+		pattern.translate_select (sql);
 
 		expect (SparqlTokenType.EOF);
 
@@ -767,199 +487,6 @@ public class Tracker.SparqlQuery : Object {
 		return exec_sql (sql.str);
 	}
 
-	PropertyType translate_select (StringBuilder sql, bool subquery = false) throws SparqlError {
-		var type = PropertyType.UNKNOWN;
-
-		var pattern_sql = new StringBuilder ();
-		var old_bindings = (owned) bindings;
-
-		sql.append ("SELECT ");
-
-		expect (SparqlTokenType.SELECT);
-
-		if (accept (SparqlTokenType.DISTINCT)) {
-			sql.append ("DISTINCT ");
-		} else if (accept (SparqlTokenType.REDUCED)) {
-		}
-
-		// skip select variables (processed later)
-		var select_variables_location = get_location ();
-		skip_select_variables ();
-
-		if (accept (SparqlTokenType.FROM)) {
-			accept (SparqlTokenType.NAMED);
-			expect (SparqlTokenType.IRI_REF);
-		}
-
-		accept (SparqlTokenType.WHERE);
-
-		translate_group_graph_pattern (pattern_sql);
-
-		// process select variables
-		var after_where = get_location ();
-		set_location (select_variables_location);
-
-		// report use of undefined variables
-		foreach (var variable in context.var_map.get_values ()) {
-			if (variable.binding == null) {
-				throw get_error ("use of undefined variable `%s'".printf (variable.name));
-			}
-		}
-
-		var where_bindings = (owned) bindings;
-		bindings = (owned) old_bindings;
-
-		bool first = true;
-		if (accept (SparqlTokenType.STAR)) {
-			foreach (var variable in context.var_map.get_values ()) {
-				if (!first) {
-					sql.append (", ");
-				} else {
-					first = false;
-				}
-				if (subquery) {
-					// don't convert to string in subqueries
-					sql.append (variable.sql_expression);
-				} else {
-					append_expression_as_string (sql, variable.sql_expression, variable.binding.data_type);
-				}
-			}
-		} else {
-			while (true) {
-				if (!first) {
-					sql.append (", ");
-				} else {
-					first = false;
-				}
-
-				type = translate_select_expression (sql, subquery);
-
-				switch (current ()) {
-				case SparqlTokenType.FROM:
-				case SparqlTokenType.WHERE:
-				case SparqlTokenType.OPEN_BRACE:
-				case SparqlTokenType.GROUP:
-				case SparqlTokenType.ORDER:
-				case SparqlTokenType.LIMIT:
-				case SparqlTokenType.OFFSET:
-				case SparqlTokenType.EOF:
-					break;
-				default:
-					continue;
-				}
-				break;
-			}
-		}
-
-		// literals in select expressions need to be bound before literals in the where clause
-		foreach (var binding in where_bindings) {
-			bindings.append (binding);
-		}
-
-		// select from results of WHERE clause
-		sql.append (" FROM (");
-		sql.append (pattern_sql.str);
-		sql.append (")");
-
-		set_location (after_where);
-
-		if (accept (SparqlTokenType.GROUP)) {
-			expect (SparqlTokenType.BY);
-			sql.append (" GROUP BY ");
-			bool first_group = true;
-			do {
-				if (first_group) {
-					first_group = false;
-				} else {
-					sql.append (", ");
-				}
-				translate_expression (sql);
-			} while (current () != SparqlTokenType.ORDER && current () != SparqlTokenType.LIMIT && current () != SparqlTokenType.OFFSET && current () != SparqlTokenType.CLOSE_BRACE && current () != SparqlTokenType.CLOSE_PARENS && current () != SparqlTokenType.EOF);
-		}
-
-		if (accept (SparqlTokenType.ORDER)) {
-			expect (SparqlTokenType.BY);
-			sql.append (" ORDER BY ");
-			bool first_order = true;
-			do {
-				if (first_order) {
-					first_order = false;
-				} else {
-					sql.append (", ");
-				}
-				translate_order_condition (sql);
-			} while (current () != SparqlTokenType.LIMIT && current () != SparqlTokenType.OFFSET && current () != SparqlTokenType.CLOSE_BRACE && current () != SparqlTokenType.CLOSE_PARENS && current () != SparqlTokenType.EOF);
-		}
-
-		int limit = -1;
-		int offset = -1;
-
-		if (accept (SparqlTokenType.LIMIT)) {
-			expect (SparqlTokenType.INTEGER);
-			limit = get_last_string ().to_int ();
-			if (accept (SparqlTokenType.OFFSET)) {
-				expect (SparqlTokenType.INTEGER);
-				offset = get_last_string ().to_int ();
-			}
-		} else if (accept (SparqlTokenType.OFFSET)) {
-			expect (SparqlTokenType.INTEGER);
-			offset = get_last_string ().to_int ();
-			if (accept (SparqlTokenType.LIMIT)) {
-				expect (SparqlTokenType.INTEGER);
-				limit = get_last_string ().to_int ();
-			}
-		}
-
-		// LIMIT and OFFSET
-		if (limit >= 0) {
-			sql.append (" LIMIT ?");
-
-			var binding = new LiteralBinding ();
-			binding.literal = limit.to_string ();
-			binding.data_type = PropertyType.INTEGER;
-			bindings.append (binding);
-
-			if (offset >= 0) {
-				sql.append (" OFFSET ?");
-
-				binding = new LiteralBinding ();
-				binding.literal = offset.to_string ();
-				binding.data_type = PropertyType.INTEGER;
-				bindings.append (binding);
-			}
-		} else if (offset >= 0) {
-			sql.append (" LIMIT -1 OFFSET ?");
-
-			var binding = new LiteralBinding ();
-			binding.literal = offset.to_string ();
-			binding.data_type = PropertyType.INTEGER;
-			bindings.append (binding);
-		}
-
-		return type;
-	}
-
-	void translate_expression_as_order_condition (StringBuilder sql) throws SparqlError {
-		long begin = sql.len;
-		if (translate_expression (sql) == PropertyType.RESOURCE) {
-			// ID => Uri
-			sql.insert (begin, "(SELECT Uri FROM Resource WHERE ID = ");
-			sql.append (")");
-		}
-	}
-
-	void translate_order_condition (StringBuilder sql) throws SparqlError {
-		if (accept (SparqlTokenType.ASC)) {
-			translate_expression_as_order_condition (sql);
-			sql.append (" ASC");
-		} else if (accept (SparqlTokenType.DESC)) {
-			translate_expression_as_order_condition (sql);
-			sql.append (" DESC");
-		} else {
-			translate_expression_as_order_condition (sql);
-		}
-	}
-
 	DBResultSet? execute_ask () throws DBInterfaceError, SparqlError, DateError {
 		// ASK query
 
@@ -976,7 +503,7 @@ public class Tracker.SparqlQuery : Object {
 
 		accept (SparqlTokenType.WHERE);
 
-		translate_group_graph_pattern (pattern_sql);
+		pattern.translate_group_graph_pattern (pattern_sql);
 
 		// select from results of WHERE clause
 		sql.append (" FROM (");
@@ -1041,7 +568,7 @@ public class Tracker.SparqlQuery : Object {
 			var old_graph = current_graph;
 			current_graph = null;
 
-			translate_group_graph_pattern (pattern_sql);
+			pattern.translate_group_graph_pattern (pattern_sql);
 
 			current_graph = old_graph;
 		}
@@ -1061,7 +588,7 @@ public class Tracker.SparqlQuery : Object {
 			if (variable.binding == null) {
 				throw get_error ("use of undefined variable `%s'".printf (variable.name));
 			}
-			append_expression_as_string (sql, variable.sql_expression, variable.binding.data_type);
+			Expression.append_expression_as_string (sql, variable.sql_expression, variable.binding.data_type);
 		}
 
 		if (first) {
@@ -1131,7 +658,7 @@ public class Tracker.SparqlQuery : Object {
 		expect (SparqlTokenType.GRAPH);
 
 		bool is_var;
-		string url = parse_var_or_term (null, out is_var);
+		string url = pattern.parse_var_or_term (null, out is_var);
 
 		Data.delete_resource_description (url, url);
 
@@ -1139,7 +666,7 @@ public class Tracker.SparqlQuery : Object {
 		Data.update_buffer_flush ();
 	}
 
-	string resolve_prefixed_name (string prefix, string local_name) throws SparqlError {
+	internal string resolve_prefixed_name (string prefix, string local_name) throws SparqlError {
 		string ns = prefix_map.lookup (prefix);
 		if (ns == null) {
 			throw get_error ("use of undefined prefix `%s'".printf (prefix));
@@ -1147,1020 +674,6 @@ public class Tracker.SparqlQuery : Object {
 		return ns + local_name;
 	}
 
-	string parse_var_or_term (StringBuilder? sql, out bool is_var) throws SparqlError {
-		string result = "";
-		is_var = false;
-		if (current () == SparqlTokenType.VAR) {
-			is_var = true;
-			next ();
-			result = get_last_string ().substring (1);
-		} else if (current () == SparqlTokenType.IRI_REF) {
-			next ();
-			result = get_last_string (1);
-		} else if (current () == SparqlTokenType.PN_PREFIX) {
-			// prefixed name with namespace foo:bar
-			next ();
-			string ns = get_last_string ();
-			expect (SparqlTokenType.COLON);
-			result = resolve_prefixed_name (ns, get_last_string ().substring (1));
-		} else if (current () == SparqlTokenType.COLON) {
-			// prefixed name without namespace :bar
-			next ();
-			result = resolve_prefixed_name ("", get_last_string ().substring (1));
-		} else if (accept (SparqlTokenType.BLANK_NODE)) {
-			// _:foo
-			expect (SparqlTokenType.COLON);
-			result = generate_bnodeid (get_last_string ().substring (1));
-		} else if (current () == SparqlTokenType.STRING_LITERAL1) {
-			result = parse_string_literal ();
-		} else if (current () == SparqlTokenType.STRING_LITERAL2) {
-			result = parse_string_literal ();
-		} else if (current () == SparqlTokenType.STRING_LITERAL_LONG1) {
-			result = parse_string_literal ();
-		} else if (current () == SparqlTokenType.STRING_LITERAL_LONG2) {
-			result = parse_string_literal ();
-		} else if (current () == SparqlTokenType.INTEGER) {
-			next ();
-			result = get_last_string ();
-		} else if (current () == SparqlTokenType.DECIMAL) {
-			next ();
-			result = get_last_string ();
-		} else if (current () == SparqlTokenType.DOUBLE) {
-			next ();
-			result = get_last_string ();
-		} else if (current () == SparqlTokenType.TRUE) {
-			next ();
-			result = "true";
-		} else if (current () == SparqlTokenType.FALSE) {
-			next ();
-			result = "false";
-		} else if (current () == SparqlTokenType.OPEN_BRACKET) {
-			next ();
-
-			result = generate_bnodeid (null);
-
-			string old_subject = current_subject;
-			bool old_subject_is_var = current_subject_is_var;
-
-			current_subject = result;
-			current_subject_is_var = true;
-			parse_property_list_not_empty (sql);
-			expect (SparqlTokenType.CLOSE_BRACKET);
-
-			current_subject = old_subject;
-			current_subject_is_var = old_subject_is_var;
-
-			is_var = true;
-		} else {
-			throw get_error ("expected variable or term");
-		}
-		return result;
-	}
-
-	void parse_object_list (StringBuilder sql, bool in_simple_optional = false) throws SparqlError {
-		while (true) {
-			parse_object (sql, in_simple_optional);
-			if (accept (SparqlTokenType.COMMA)) {
-				continue;
-			}
-			break;
-		}
-	}
-
-	void parse_property_list_not_empty (StringBuilder sql, bool in_simple_optional = false) throws SparqlError {
-		while (true) {
-			var old_predicate = current_predicate;
-			var old_predicate_is_var = current_predicate_is_var;
-
-			current_predicate = null;
-			current_predicate_is_var = false;
-			if (current () == SparqlTokenType.VAR) {
-				current_predicate_is_var = true;
-				next ();
-				current_predicate = get_last_string ().substring (1);
-			} else if (current () == SparqlTokenType.IRI_REF) {
-				next ();
-				current_predicate = get_last_string (1);
-			} else if (current () == SparqlTokenType.PN_PREFIX) {
-				next ();
-				string ns = get_last_string ();
-				expect (SparqlTokenType.COLON);
-				current_predicate = resolve_prefixed_name (ns, get_last_string ().substring (1));
-			} else if (current () == SparqlTokenType.COLON) {
-				next ();
-				current_predicate = resolve_prefixed_name ("", get_last_string ().substring (1));
-			} else if (current () == SparqlTokenType.A) {
-				next ();
-				current_predicate = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";;
-			} else {
-				throw get_error ("expected non-empty property list");
-			}
-			parse_object_list (sql, in_simple_optional);
-
-			current_predicate = old_predicate;
-			current_predicate_is_var = old_predicate_is_var;
-
-			if (accept (SparqlTokenType.SEMICOLON)) {
-				if (current () == SparqlTokenType.DOT) {
-					// semicolon before dot is allowed in both, SPARQL and Turtle
-					break;
-				}
-				continue;
-			}
-			break;
-		}
-	}
-
-	void translate_bound_call (StringBuilder sql) throws SparqlError {
-		expect (SparqlTokenType.BOUND);
-		expect (SparqlTokenType.OPEN_PARENS);
-		sql.append ("(");
-		translate_expression (sql);
-		sql.append (" IS NOT NULL)");
-		expect (SparqlTokenType.CLOSE_PARENS);
-	}
-
-	void translate_regex (StringBuilder sql) throws SparqlError {
-		expect (SparqlTokenType.REGEX);
-		expect (SparqlTokenType.OPEN_PARENS);
-		sql.append ("SparqlRegex(");
-		translate_expression_as_string (sql);
-		sql.append (", ");
-		expect (SparqlTokenType.COMMA);
-		translate_expression (sql);
-		sql.append (", ");
-		if (accept (SparqlTokenType.COMMA)) {
-			translate_expression (sql);
-		} else {
-			sql.append ("''");
-		}
-		sql.append (")");
-		expect (SparqlTokenType.CLOSE_PARENS);
-	}
-
-	static void append_expression_as_string (StringBuilder sql, string expression, PropertyType type) {
-		long begin = sql.len;
-		sql.append (expression);
-		convert_expression_to_string (sql, type, begin);
-	}
-
-	static void convert_expression_to_string (StringBuilder sql, PropertyType type, long begin) {
-		switch (type) {
-		case PropertyType.STRING:
-		case PropertyType.INTEGER:
-			// nothing to convert
-			break;
-		case PropertyType.RESOURCE:
-			// ID => Uri
-			sql.insert (begin, "(SELECT Uri FROM Resource WHERE ID = ");
-			sql.append (")");
-			break;
-		case PropertyType.BOOLEAN:
-			// 0/1 => false/true
-			sql.insert (begin, "CASE ");
-			sql.append (" WHEN 1 THEN 'true' WHEN 0 THEN 'false' ELSE NULL END");
-			break;
-		case PropertyType.DATETIME:
-			// ISO 8601 format
-			sql.insert (begin, "strftime (\"%Y-%m-%dT%H:%M:%SZ\", ");
-			sql.append (", \"unixepoch\")");
-			break;
-		default:
-			// let sqlite convert the expression to string
-			sql.insert (begin, "CAST (");
-			sql.append (" AS TEXT)");
-			break;
-		}
-	}
-
-	void translate_expression_as_string (StringBuilder sql) throws SparqlError {
-		switch (current ()) {
-		case SparqlTokenType.IRI_REF:
-		case SparqlTokenType.PN_PREFIX:
-		case SparqlTokenType.COLON:
-			// handle IRI literals separately as it wouldn't work for unknown IRIs otherwise
-			var binding = new LiteralBinding ();
-			bool is_var;
-			binding.literal = parse_var_or_term (null, out is_var);
-			if (accept (SparqlTokenType.OPEN_PARENS)) {
-				// function call
-				long begin = sql.len;
-				var type = translate_function (sql, binding.literal);
-				expect (SparqlTokenType.CLOSE_PARENS);
-				convert_expression_to_string (sql, type, begin);
-			} else {
-				sql.append ("?");
-				bindings.append (binding);
-			}
-			break;
-		default:
-			long begin = sql.len;
-			var type = translate_expression (sql);
-			convert_expression_to_string (sql, type, begin);
-			break;
-		}
-	}
-
-	void translate_str (StringBuilder sql) throws SparqlError {
-		expect (SparqlTokenType.STR);
-		expect (SparqlTokenType.OPEN_PARENS);
-
-		translate_expression_as_string (sql);
-
-		expect (SparqlTokenType.CLOSE_PARENS);
-	}
-
-	void translate_isuri (StringBuilder sql) throws SparqlError {
-		if (!accept (SparqlTokenType.ISURI)) {
-			expect (SparqlTokenType.ISIRI);
-		}
-
-		expect (SparqlTokenType.OPEN_PARENS);
-
-		sql.append ("?");
-		var new_binding = new LiteralBinding ();
-		new_binding.data_type = PropertyType.INTEGER;
-
-		if (current() == SparqlTokenType.IRI_REF) {
-			new_binding.literal = "1";
-			next ();
-		} else if (translate_expression (new StringBuilder ()) == PropertyType.RESOURCE) {
-			new_binding.literal = "1";
-		} else {
-			new_binding.literal = "0";
-		}
-
-		bindings.append (new_binding);
-
-		expect (SparqlTokenType.CLOSE_PARENS);
-	}
-
-	void translate_datatype (StringBuilder sql) throws SparqlError {
-		expect (SparqlTokenType.DATATYPE);
-		expect (SparqlTokenType.OPEN_PARENS);
-
-		if (accept (SparqlTokenType.VAR)) {
-			string variable_name = get_last_string().substring(1);
-			var variable = get_variable (variable_name);
-
-			if (variable.binding == null) {
-				throw get_error ("`%s' is not a valid variable".printf (variable.name));
-			}
-
-			if (variable.binding.data_type == PropertyType.RESOURCE || variable.binding.type == null) {
-				throw get_error ("Invalid FILTER");
-			}
-
-			sql.append ("(SELECT ID FROM Resource WHERE Uri = ?)");
-
-			var new_binding = new LiteralBinding ();
-			new_binding.literal = variable.binding.type.uri;
-			bindings.append (new_binding);
-
-		} else {
-			throw get_error ("Invalid FILTER");
-		}
-
-		expect (SparqlTokenType.CLOSE_PARENS);
-	}
-
-	PropertyType translate_function (StringBuilder sql, string uri) throws SparqlError {
-		if (uri == XSD_NS + "string") {
-			// conversion to string
-			translate_expression_as_string (sql);
-
-			return PropertyType.STRING;
-		} else if (uri == XSD_NS + "integer") {
-			// conversion to integer
-			sql.append ("CAST (");
-			translate_expression_as_string (sql);
-			sql.append (" AS INTEGER)");
-
-			return PropertyType.INTEGER;
-		} else if (uri == XSD_NS + "double") {
-			// conversion to double
-			sql.append ("CAST (");
-			translate_expression_as_string (sql);
-			sql.append (" AS REAL)");
-
-			return PropertyType.DOUBLE;
-		} else if (uri == FN_NS + "contains") {
-			// fn:contains('A','B') => 'A' GLOB '*B*'
-			sql.append ("(");
-			translate_expression_as_string (sql);
-			sql.append (" GLOB ");
-			expect (SparqlTokenType.COMMA);
-
-			sql.append ("?");
-			var binding = new LiteralBinding ();
-			binding.literal = "*%s*".printf (parse_string_literal ());
-			bindings.append (binding);
-
-			sql.append (")");
-
-			return PropertyType.BOOLEAN;
-		} else if (uri == FN_NS + "starts-with") {
-			// fn:starts-with('A','B') => 'A' GLOB 'B*'
-			sql.append ("(");
-			translate_expression_as_string (sql);
-			sql.append (" GLOB ");
-			expect (SparqlTokenType.COMMA);
-
-			sql.append ("?");
-			var binding = new LiteralBinding ();
-			binding.literal = "%s*".printf (parse_string_literal ());
-			bindings.append (binding);
-
-			sql.append (")");
-
-			return PropertyType.BOOLEAN;
-		} else if (uri == FN_NS + "ends-with") {
-			// fn:ends-with('A','B') => 'A' GLOB '*B'
-			sql.append ("(");
-			translate_expression_as_string (sql);
-			sql.append (" GLOB ");
-			expect (SparqlTokenType.COMMA);
-
-			sql.append ("?");
-			var binding = new LiteralBinding ();
-			binding.literal = "*%s".printf (parse_string_literal ());
-			bindings.append (binding);
-
-			sql.append (")");
-
-			return PropertyType.BOOLEAN;
-		} else if (uri == FN_NS + "concat") {
-			translate_expression (sql);
-			sql.append ("||");
-			expect (SparqlTokenType.COMMA);
-			translate_expression (sql);
-			while (accept (SparqlTokenType.COMMA)) {
-			      sql.append ("||");
-			      translate_expression (sql);
-			}
-
-			return PropertyType.STRING;
-		} else if (uri == FN_NS + "string-join") {
-			sql.append ("SparqlStringJoin(");
-			expect (SparqlTokenType.OPEN_PARENS);
-
-			translate_expression_as_string (sql);
-			sql.append (", ");
-			expect (SparqlTokenType.COMMA);
-			translate_expression_as_string (sql);
-			while (accept (SparqlTokenType.COMMA)) {
-			      sql.append (", ");
-			      translate_expression_as_string (sql);
-			}
-
-			expect (SparqlTokenType.CLOSE_PARENS);
-			sql.append (",");
-			expect (SparqlTokenType.COMMA);
-			translate_expression (sql);
-			sql.append (")");
-
-			return PropertyType.STRING;
-		} else if (uri == FN_NS + "year-from-dateTime") {
-			expect (SparqlTokenType.VAR);
-			string variable_name = get_last_string ().substring (1);
-			var variable = get_variable (variable_name);
-
-			sql.append ("strftime (\"%Y\", ");
-			sql.append (variable.get_extra_sql_expression ("localDate"));
-			sql.append (" * 24 * 3600, \"unixepoch\")");
-
-			return PropertyType.INTEGER;
-		} else if (uri == FN_NS + "month-from-dateTime") {
-			expect (SparqlTokenType.VAR);
-			string variable_name = get_last_string ().substring (1);
-			var variable = get_variable (variable_name);
-
-			sql.append ("strftime (\"%m\", ");
-			sql.append (variable.get_extra_sql_expression ("localDate"));
-			sql.append (" * 24 * 3600, \"unixepoch\")");
-
-			return PropertyType.INTEGER;
-		} else if (uri == FN_NS + "day-from-dateTime") {
-			expect (SparqlTokenType.VAR);
-			string variable_name = get_last_string ().substring (1);
-			var variable = get_variable (variable_name);
-
-			sql.append ("strftime (\"%d\", ");
-			sql.append (variable.get_extra_sql_expression ("localDate"));
-			sql.append (" * 24 * 3600, \"unixepoch\")");
-
-			return PropertyType.INTEGER;
-		} else if (uri == FN_NS + "hours-from-dateTime") {
-			expect (SparqlTokenType.VAR);
-			string variable_name = get_last_string ().substring (1);
-			var variable = get_variable (variable_name);
-
-			sql.append ("(");
-			sql.append (variable.get_extra_sql_expression ("localTime"));
-			sql.append (" / 3600)");
-
-			return PropertyType.INTEGER;
-		} else if (uri == FN_NS + "minutes-from-dateTime") {
-			expect (SparqlTokenType.VAR);
-			string variable_name = get_last_string ().substring (1);
-			var variable = get_variable (variable_name);
-
-			sql.append ("(");
-			sql.append (variable.get_extra_sql_expression ("localTime"));
-			sql.append (" / 60 % 60)");
-
-			return PropertyType.INTEGER;
-		} else if (uri == FN_NS + "seconds-from-dateTime") {
-			expect (SparqlTokenType.VAR);
-			string variable_name = get_last_string ().substring (1);
-			var variable = get_variable (variable_name);
-
-			sql.append ("(");
-			sql.append (variable.get_extra_sql_expression ("localTime"));
-			sql.append ("% 60)");
-
-			return PropertyType.INTEGER;
-		} else if (uri == FN_NS + "timezone-from-dateTime") {
-			expect (SparqlTokenType.VAR);
-			string variable_name = get_last_string ().substring (1);
-			var variable = get_variable (variable_name);
-
-			sql.append ("(");
-			sql.append (variable.get_extra_sql_expression ("localDate"));
-			sql.append (" * 24 * 3600 + ");
-			sql.append (variable.get_extra_sql_expression ("localTime"));
-			sql.append ("- ");
-			sql.append (variable.sql_expression);
-			sql.append (")");
-
-			return PropertyType.INTEGER;
-		} else if (uri == FTS_NS + "rank") {
-			bool is_var;
-			string v = parse_var_or_term (null, out is_var);
-			sql.append_printf ("\"%s_u_rank\"", v);
-
-			return PropertyType.DOUBLE;
-		} else if (uri == FTS_NS + "offsets") {
-			bool is_var;
-			string v = parse_var_or_term (null, out is_var);
-			sql.append_printf ("\"%s_u_offsets\"", v);
-
-			return PropertyType.STRING;
-                } else if (uri == TRACKER_NS + "cartesian-distance") {
-                        sql.append ("SparqlCartesianDistance(");
-                        translate_expression (sql);
-                        sql.append (", ");
-                        expect (SparqlTokenType.COMMA);
-                        translate_expression (sql);
-                        sql.append (", ");
-                        expect (SparqlTokenType.COMMA);
-                        translate_expression (sql);
-                        sql.append (", ");
-                        expect (SparqlTokenType.COMMA);
-                        translate_expression (sql);
-                        sql.append (")");
-
-                        return PropertyType.DOUBLE;
-                } else if (uri == TRACKER_NS + "haversine-distance") {
-                        sql.append ("SparqlHaversineDistance(");
-                        translate_expression (sql);
-                        sql.append (", ");
-                        expect (SparqlTokenType.COMMA);
-                        translate_expression (sql);
-                        sql.append (", ");
-                        expect (SparqlTokenType.COMMA);
-                        translate_expression (sql);
-                        sql.append (", ");
-                        expect (SparqlTokenType.COMMA);
-                        translate_expression (sql);
-                        sql.append (")");
-
-                        return PropertyType.DOUBLE;
-		} else if (uri == TRACKER_NS + "coalesce") {
-			sql.append ("COALESCE(");
-			translate_expression_as_string (sql);
-			sql.append (", ");
-			expect (SparqlTokenType.COMMA);
-			translate_expression_as_string (sql);
-			while (accept (SparqlTokenType.COMMA)) {
-			      sql.append (", ");
-			      translate_expression_as_string (sql);
-			}
-			sql.append (")");
-
-			return PropertyType.STRING;
-		} else if (uri == TRACKER_NS + "string-from-filename") {
-			sql.append ("SparqlStringFromFilename(");
-			translate_expression_as_string (sql);
-			sql.append (")");
-
-			return PropertyType.STRING;
-		} else {
-			// support properties as functions
-			var prop = Ontologies.get_property_by_uri (uri);
-			if (prop == null) {
-				throw get_error ("Unknown function");
-			}
-
-			if (prop.multiple_values) {
-				sql.append ("(SELECT GROUP_CONCAT(");
-				long begin = sql.len;
-				sql.append_printf ("\"%s\"", prop.name);
-				convert_expression_to_string (sql, prop.data_type, begin);
-				sql.append_printf (",',') FROM \"%s\" WHERE ID = ", prop.table_name);
-				translate_expression (sql);
-				sql.append (")");
-
-				return PropertyType.STRING;
-			} else {
-				sql.append_printf ("(SELECT \"%s\" FROM \"%s\" WHERE ID = ", prop.name, prop.table_name);
-				translate_expression (sql);
-				sql.append (")");
-
-				return prop.data_type;
-			}
-		}
-	}
-
-	string parse_string_literal () throws SparqlError {
-		next ();
-		switch (last ()) {
-		case SparqlTokenType.STRING_LITERAL1:
-		case SparqlTokenType.STRING_LITERAL2:
-			var sb = new StringBuilder ();
-
-			string s = get_last_string (1);
-			string* p = s;
-			string* end = p + s.size ();
-			while ((long) p < (long) end) {
-				string* q = Posix.strchr (p, '\\');
-				if (q == null) {
-					sb.append_len (p, (long) (end - p));
-					p = end;
-				} else {
-					sb.append_len (p, (long) (q - p));
-					p = q + 1;
-					switch (((char*) p)[0]) {
-					case '\'':
-					case '"':
-					case '\\':
-						sb.append_c (((char*) p)[0]);
-						break;
-					case 'b':
-						sb.append_c ('\b');
-						break;
-					case 'f':
-						sb.append_c ('\f');
-						break;
-					case 'n':
-						sb.append_c ('\n');
-						break;
-					case 'r':
-						sb.append_c ('\r');
-						break;
-					case 't':
-						sb.append_c ('\t');
-						break;
-					}
-					p++;
-				}
-			}
-
-			if (accept (SparqlTokenType.DOUBLE_CIRCUMFLEX)) {
-				if (!accept (SparqlTokenType.IRI_REF)) {
-					accept (SparqlTokenType.PN_PREFIX);
-					expect (SparqlTokenType.COLON);
-				}
-			}
-
-			return sb.str;
-		case SparqlTokenType.STRING_LITERAL_LONG1:
-		case SparqlTokenType.STRING_LITERAL_LONG2:
-			string result = get_last_string (3);
-
-			if (accept (SparqlTokenType.DOUBLE_CIRCUMFLEX)) {
-				if (!accept (SparqlTokenType.IRI_REF)) {
-					accept (SparqlTokenType.PN_PREFIX);
-					expect (SparqlTokenType.COLON);
-				}
-			}
-
-			return result;
-		default:
-			throw get_error ("expected string literal");
-		}
-	}
-
-	PropertyType translate_uri_expression (StringBuilder sql, string uri) throws SparqlError {
-		if (accept (SparqlTokenType.OPEN_PARENS)) {
-			// function
-			var result = translate_function (sql, uri);
-			expect (SparqlTokenType.CLOSE_PARENS);
-			return result;
-		} else {
-			// resource
-			sql.append ("(SELECT ID FROM Resource WHERE Uri = ?)");
-			var binding = new LiteralBinding ();
-			binding.literal = uri;
-			bindings.append (binding);
-			return PropertyType.RESOURCE;
-		}
-	}
-
-	PropertyType translate_primary_expression (StringBuilder sql) throws SparqlError {
-		switch (current ()) {
-		case SparqlTokenType.OPEN_PARENS:
-			return translate_bracketted_expression (sql);
-		case SparqlTokenType.IRI_REF:
-			next ();
-			return translate_uri_expression (sql, get_last_string (1));
-		case SparqlTokenType.DECIMAL:
-		case SparqlTokenType.DOUBLE:
-			next ();
-
-			sql.append ("?");
-
-			var binding = new LiteralBinding ();
-			binding.literal = get_last_string ();
-			bindings.append (binding);
-
-			return PropertyType.DOUBLE;
-		case SparqlTokenType.TRUE:
-			next ();
-
-			sql.append ("?");
-
-			var binding = new LiteralBinding ();
-			binding.literal = "1";
-			binding.data_type = PropertyType.INTEGER;
-			bindings.append (binding);
-
-			return PropertyType.BOOLEAN;
-		case SparqlTokenType.FALSE:
-			next ();
-
-			sql.append ("?");
-
-			var binding = new LiteralBinding ();
-			binding.literal = "0";
-			binding.data_type = PropertyType.INTEGER;
-			bindings.append (binding);
-
-			return PropertyType.BOOLEAN;
-		case SparqlTokenType.STRING_LITERAL1:
-		case SparqlTokenType.STRING_LITERAL2:
-		case SparqlTokenType.STRING_LITERAL_LONG1:
-		case SparqlTokenType.STRING_LITERAL_LONG2:
-			sql.append ("?");
-
-			var binding = new LiteralBinding ();
-			binding.literal = parse_string_literal ();
-			bindings.append (binding);
-
-			return PropertyType.STRING;
-		case SparqlTokenType.INTEGER:
-			next ();
-
-			sql.append ("?");
-
-			var binding = new LiteralBinding ();
-			binding.literal = get_last_string ();
-			binding.data_type = PropertyType.INTEGER;
-			bindings.append (binding);
-
-			return PropertyType.INTEGER;
-		case SparqlTokenType.VAR:
-			next ();
-			string variable_name = get_last_string ().substring (1);
-			var variable = get_variable (variable_name);
-			sql.append (variable.sql_expression);
-
-			if (variable.binding == null) {
-				return PropertyType.UNKNOWN;
-			} else {
-				return variable.binding.data_type;
-			}
-		case SparqlTokenType.STR:
-			translate_str (sql);
-			return PropertyType.STRING;
-		case SparqlTokenType.LANG:
-			next ();
-			sql.append ("''");
-			return PropertyType.STRING;
-		case SparqlTokenType.LANGMATCHES:
-			next ();
-			sql.append ("0");
-			return PropertyType.BOOLEAN;
-		case SparqlTokenType.DATATYPE:
-			translate_datatype (sql);
-			return PropertyType.RESOURCE;
-		case SparqlTokenType.BOUND:
-			translate_bound_call (sql);
-			return PropertyType.BOOLEAN;
-		case SparqlTokenType.SAMETERM:
-			next ();
-			expect (SparqlTokenType.OPEN_PARENS);
-			sql.append ("(");
-			translate_expression (sql);
-			sql.append (" = ");
-			expect (SparqlTokenType.COMMA);
-			translate_expression (sql);
-			sql.append (")");
-			expect (SparqlTokenType.CLOSE_PARENS);
-			return PropertyType.BOOLEAN;
-		case SparqlTokenType.ISIRI:
-		case SparqlTokenType.ISURI:
-			translate_isuri (sql);
-			return PropertyType.BOOLEAN;
-		case SparqlTokenType.ISBLANK:
-			next ();
-			expect (SparqlTokenType.OPEN_PARENS);
-			next ();
-			// TODO: support ISBLANK properly
-			sql.append ("0");
-			expect (SparqlTokenType.CLOSE_PARENS);
-			return PropertyType.BOOLEAN;
-		case SparqlTokenType.ISLITERAL:
-			next ();
-			return PropertyType.BOOLEAN;
-		case SparqlTokenType.REGEX:
-			translate_regex (sql);
-			return PropertyType.BOOLEAN;
-		case SparqlTokenType.PN_PREFIX:
-			next ();
-			string ns = get_last_string ();
-			expect (SparqlTokenType.COLON);
-			string uri = resolve_prefixed_name (ns, get_last_string ().substring (1));
-			return translate_uri_expression (sql, uri);
-		case SparqlTokenType.COLON:
-			next ();
-			string uri = resolve_prefixed_name ("", get_last_string ().substring (1));
-			return translate_uri_expression (sql, uri);
-		default:
-			throw get_error ("expected primary expression");
-		}
-	}
-
-	PropertyType translate_unary_expression (StringBuilder sql) throws SparqlError {
-		if (accept (SparqlTokenType.OP_NEG)) {
-			sql.append ("NOT (");
-			var optype = translate_primary_expression (sql);
-			sql.append (")");
-			if (optype != PropertyType.BOOLEAN) {
-				throw get_error ("expected boolean expression");
-			}
-			return PropertyType.BOOLEAN;
-		} else if (accept (SparqlTokenType.PLUS)) {
-			return translate_primary_expression (sql);
-		} else if (accept (SparqlTokenType.MINUS)) {
-			sql.append ("-(");
-			var optype = translate_primary_expression (sql);
-			sql.append (")");
-			return optype;
-		}
-		return translate_primary_expression (sql);
-	}
-
-	PropertyType translate_multiplicative_expression (StringBuilder sql) throws SparqlError {
-		long begin = sql.len;
-		var optype = translate_unary_expression (sql);
-		while (true) {
-			if (accept (SparqlTokenType.STAR)) {
-				if (!maybe_numeric (optype)) {
-					throw get_error ("expected numeric operand");
-				}
-				sql.insert (begin, "(");
-				sql.append (" * ");
-				if (!maybe_numeric (translate_unary_expression (sql))) {
-					throw get_error ("expected numeric operand");
-				}
-				sql.append (")");
-			} else if (accept (SparqlTokenType.DIV)) {
-				if (!maybe_numeric (optype)) {
-					throw get_error ("expected numeric operand");
-				}
-				sql.insert (begin, "(");
-				sql.append (" / ");
-				if (!maybe_numeric (translate_unary_expression (sql))) {
-					throw get_error ("expected numeric operand");
-				}
-				sql.append (")");
-			} else {
-				break;
-			}
-		}
-		return optype;
-	}
-
-	PropertyType translate_additive_expression (StringBuilder sql) throws SparqlError {
-		long begin = sql.len;
-		var optype = translate_multiplicative_expression (sql);
-		while (true) {
-			if (accept (SparqlTokenType.PLUS)) {
-				if (!maybe_numeric (optype)) {
-					throw get_error ("expected numeric operand");
-				}
-				sql.insert (begin, "(");
-				sql.append (" + ");
-				if (!maybe_numeric (translate_multiplicative_expression (sql))) {
-					throw get_error ("expected numeric operand");
-				}
-				sql.append (")");
-			} else if (accept (SparqlTokenType.MINUS)) {
-				if (!maybe_numeric (optype)) {
-					throw get_error ("expected numeric operand");
-				}
-				sql.insert (begin, "(");
-				sql.append (" - ");
-				if (!maybe_numeric (translate_multiplicative_expression (sql))) {
-					throw get_error ("expected numeric operand");
-				}
-				sql.append (")");
-			} else {
-				break;
-			}
-		}
-		return optype;
-	}
-
-	PropertyType translate_numeric_expression (StringBuilder sql) throws SparqlError {
-		return translate_additive_expression (sql);
-	}
-
-	PropertyType process_relational_expression (StringBuilder sql, long begin, uint n_bindings, PropertyType op1type, string operator) throws SparqlError {
-		sql.insert (begin, "(");
-		sql.append (operator);
-		var op2type = translate_numeric_expression (sql);
-		sql.append (")");
-		if ((op1type == PropertyType.DATETIME && op2type == PropertyType.STRING)
-		    || (op1type == PropertyType.STRING && op2type == PropertyType.DATETIME)) {
-			if (bindings.length () == n_bindings + 1) {
-				// trigger string => datetime conversion
-				bindings.last ().data.data_type = PropertyType.DATETIME;
-			}
-		}
-		return PropertyType.BOOLEAN;
-	}
-
-	PropertyType translate_relational_expression (StringBuilder sql) throws SparqlError {
-		long begin = sql.len;
-		// TODO: improve performance
-		uint n_bindings = bindings.length ();
-		var optype = translate_numeric_expression (sql);
-		if (accept (SparqlTokenType.OP_GE)) {
-			return process_relational_expression (sql, begin, n_bindings, optype, " >= ");
-		} else if (accept (SparqlTokenType.OP_EQ)) {
-			return process_relational_expression (sql, begin, n_bindings, optype, " = ");
-		} else if (accept (SparqlTokenType.OP_NE)) {
-			return process_relational_expression (sql, begin, n_bindings, optype, " <> ");
-		} else if (accept (SparqlTokenType.OP_LT)) {
-			return process_relational_expression (sql, begin, n_bindings, optype, " < ");
-		} else if (accept (SparqlTokenType.OP_LE)) {
-			return process_relational_expression (sql, begin, n_bindings, optype, " <= ");
-		} else if (accept (SparqlTokenType.OP_GT)) {
-			return process_relational_expression (sql, begin, n_bindings, optype, " > ");
-		}
-		return optype;
-	}
-
-	PropertyType translate_value_logical (StringBuilder sql) throws SparqlError {
-		return translate_relational_expression (sql);
-	}
-
-	PropertyType translate_conditional_and_expression (StringBuilder sql) throws SparqlError {
-		long begin = sql.len;
-		var optype = translate_value_logical (sql);
-		while (accept (SparqlTokenType.OP_AND)) {
-			if (optype != PropertyType.BOOLEAN) {
-				throw get_error ("expected boolean expression");
-			}
-			sql.insert (begin, "(");
-			sql.append (" AND ");
-			optype = translate_value_logical (sql);
-			sql.append (")");
-			if (optype != PropertyType.BOOLEAN) {
-				throw get_error ("expected boolean expression");
-			}
-		}
-		return optype;
-	}
-
-	PropertyType translate_conditional_or_expression (StringBuilder sql) throws SparqlError {
-		long begin = sql.len;
-		var optype = translate_conditional_and_expression (sql);
-		while (accept (SparqlTokenType.OP_OR)) {
-			if (optype != PropertyType.BOOLEAN) {
-				throw get_error ("expected boolean expression");
-			}
-			sql.insert (begin, "(");
-			sql.append (" OR ");
-			optype = translate_conditional_and_expression (sql);
-			sql.append (")");
-			if (optype != PropertyType.BOOLEAN) {
-				throw get_error ("expected boolean expression");
-			}
-		}
-		return optype;
-	}
-
-	PropertyType translate_expression (StringBuilder sql) throws SparqlError {
-		return translate_conditional_or_expression (sql);
-	}
-
-	PropertyType translate_bracketted_expression (StringBuilder sql) throws SparqlError {
-		expect (SparqlTokenType.OPEN_PARENS);
-
-		if (current () == SparqlTokenType.SELECT) {
-			// scalar subquery
-
-			context = new Context.subquery (context);
-
-			sql.append ("(");
-			var type = translate_select (sql, true);
-			sql.append (")");
-
-			context = context.parent_context;
-
-			expect (SparqlTokenType.CLOSE_PARENS);
-			return type;
-		}
-
-		var optype = translate_expression (sql);
-		expect (SparqlTokenType.CLOSE_PARENS);
-		return optype;
-	}
-
-	PropertyType translate_aggregate_expression (StringBuilder sql) throws SparqlError {
-		expect (SparqlTokenType.OPEN_PARENS);
-		if (accept (SparqlTokenType.DISTINCT)) {
-			sql.append ("DISTINCT ");
-		}
-		var optype = translate_expression (sql);
-		expect (SparqlTokenType.CLOSE_PARENS);
-		return optype;
-	}
-
-	PropertyType translate_constraint (StringBuilder sql) throws SparqlError {
-		switch (current ()) {
-		case SparqlTokenType.STR:
-		case SparqlTokenType.LANG:
-		case SparqlTokenType.LANGMATCHES:
-		case SparqlTokenType.DATATYPE:
-		case SparqlTokenType.BOUND:
-		case SparqlTokenType.SAMETERM:
-		case SparqlTokenType.ISIRI:
-		case SparqlTokenType.ISURI:
-		case SparqlTokenType.ISBLANK:
-		case SparqlTokenType.ISLITERAL:
-		case SparqlTokenType.REGEX:
-			return translate_primary_expression (sql);
-		default:
-			return translate_bracketted_expression (sql);
-		}
-	}
-
-	void translate_filter (StringBuilder sql) throws SparqlError {
-		expect (SparqlTokenType.FILTER);
-		translate_constraint (sql);
-	}
-
-	void skip_filter () throws SparqlError {
-		expect (SparqlTokenType.FILTER);
-
-		switch (current ()) {
-		case SparqlTokenType.STR:
-		case SparqlTokenType.LANG:
-		case SparqlTokenType.LANGMATCHES:
-		case SparqlTokenType.DATATYPE:
-		case SparqlTokenType.BOUND:
-		case SparqlTokenType.SAMETERM:
-		case SparqlTokenType.ISIRI:
-		case SparqlTokenType.ISURI:
-		case SparqlTokenType.ISBLANK:
-		case SparqlTokenType.ISLITERAL:
-		case SparqlTokenType.REGEX:
-			next ();
-			break;
-		default:
-			break;
-		}
-
-		expect (SparqlTokenType.OPEN_PARENS);
-		int n_parens = 1;
-		while (n_parens > 0) {
-			if (accept (SparqlTokenType.OPEN_PARENS)) {
-				n_parens++;
-			} else if (accept (SparqlTokenType.CLOSE_PARENS)) {
-				n_parens--;
-			} else if (current () == SparqlTokenType.EOF) {
-				throw get_error ("unexpected end of query, expected )");
-			} else {
-				// ignore everything else
-				next ();
-			}
-		}
-	}
-
 	void skip_braces () throws SparqlError {
 		expect (SparqlTokenType.OPEN_BRACE);
 		int n_braces = 1;
@@ -2266,13 +779,13 @@ public class Tracker.SparqlQuery : Object {
 			next ();
 			result = "false";
 		} else if (current () == SparqlTokenType.STRING_LITERAL1) {
-			result = parse_string_literal ();
+			result = expression.parse_string_literal ();
 		} else if (current () == SparqlTokenType.STRING_LITERAL2) {
-			result = parse_string_literal ();
+			result = expression.parse_string_literal ();
 		} else if (current () == SparqlTokenType.STRING_LITERAL_LONG1) {
-			result = parse_string_literal ();
+			result = expression.parse_string_literal ();
 		} else if (current () == SparqlTokenType.STRING_LITERAL_LONG2) {
-			result = parse_string_literal ();
+			result = expression.parse_string_literal ();
 		} else if (current () == SparqlTokenType.OPEN_BRACKET) {
 
 			if (anon_blank_node_open) {
@@ -2358,840 +871,6 @@ public class Tracker.SparqlQuery : Object {
 		}
 	}
 
-	void start_triples_block (StringBuilder sql) throws SparqlError {
-		triple_context = new TripleContext ();
-
-		sql.append ("SELECT ");
-	}
-
-	void end_triples_block (StringBuilder sql, ref bool first_where, bool in_group_graph_pattern) throws SparqlError {
-		// remove last comma and space
-		sql.truncate (sql.len - 2);
-
-		sql.append (" FROM ");
-		bool first = true;
-		foreach (DataTable table in triple_context.tables) {
-			if (!first) {
-				sql.append (", ");
-			} else {
-				first = false;
-			}
-			if (table.sql_db_tablename != null) {
-				sql.append_printf ("\"%s\"", table.sql_db_tablename);
-			} else {
-				sql.append_printf ("(%s)", table.predicate_variable.get_sql_query (this));
-			}
-			sql.append_printf (" AS \"%s\"", table.sql_query_tablename);
-		}
-
-		foreach (var variable in triple_context.variables) {
-			bool maybe_null = true;
-			bool in_simple_optional = false;
-			string last_name = null;
-			foreach (VariableBinding binding in triple_context.var_map.lookup (variable).list) {
-				string name;
-				if (binding.table != null) {
-					name = binding.sql_expression;
-				} else {
-					// simple optional with inverse functional property
-					// always first in loop as variable is required to be unbound
-					name = variable.sql_expression;
-				}
-				if (last_name != null) {
-					if (!first_where) {
-						sql.append (" AND ");
-					} else {
-						sql.append (" WHERE ");
-						first_where = false;
-					}
-					sql.append (last_name);
-					sql.append (" = ");
-					sql.append (name);
-				}
-				last_name = name;
-				if (!binding.maybe_null) {
-					maybe_null = false;
-				}
-				in_simple_optional = binding.in_simple_optional;
-			}
-
-			if (maybe_null && !in_simple_optional) {
-				// ensure that variable is bound in case it could return NULL in SQL
-				// assuming SPARQL variable is not optional
-				if (!first_where) {
-					sql.append (" AND ");
-				} else {
-					sql.append (" WHERE ");
-					first_where = false;
-				}
-				sql.append_printf ("%s IS NOT NULL", variable.sql_expression);
-			}
-		}
-		foreach (LiteralBinding binding in triple_context.bindings) {
-			if (!first_where) {
-				sql.append (" AND ");
-			} else {
-				sql.append (" WHERE ");
-				first_where = false;
-			}
-			sql.append (binding.sql_expression);
-			if (binding.is_fts_match) {
-				// parameters do not work with fts MATCH
-				string escaped_literal = string.joinv ("''", binding.literal.split ("'"));
-				sql.append_printf (" MATCH '%s'", escaped_literal);
-			} else {
-				sql.append (" = ");
-				if (binding.data_type == PropertyType.RESOURCE) {
-					sql.append ("(SELECT ID FROM Resource WHERE Uri = ?)");
-				} else {
-					sql.append ("?");
-				}
-				bindings.append (binding);
-			}
-		}
-
-		if (in_group_graph_pattern) {
-			sql.append (")");
-		}
-
-		triple_context = null;
-	}
-
-	void parse_triples (StringBuilder sql, long group_graph_pattern_start, ref bool in_triples_block, ref bool first_where, ref bool in_group_graph_pattern, bool found_simple_optional) throws SparqlError {
-		while (true) {
-			if (current () != SparqlTokenType.VAR &&
-			    current () != SparqlTokenType.IRI_REF &&
-			    current () != SparqlTokenType.PN_PREFIX &&
-			    current () != SparqlTokenType.COLON &&
-			    current () != SparqlTokenType.OPEN_BRACKET) {
-				break;
-			}
-			if (in_triples_block && !in_group_graph_pattern && found_simple_optional) {
-				// if there is a regular triple pattern after a simple optional
-				// we need to use a separate triple block to avoid possible conflicts
-				// due to not using a JOIN for the simple optional
-				end_triples_block (sql, ref first_where, in_group_graph_pattern);
-				in_triples_block = false;
-			}
-			if (!in_triples_block) {
-				if (in_group_graph_pattern) {
-					sql.insert (group_graph_pattern_start, "SELECT * FROM (");
-					sql.append (") NATURAL INNER JOIN (");
-				}
-				in_triples_block = true;
-				first_where = true;
-				start_triples_block (sql);
-			}
-
-			current_subject = parse_var_or_term (sql, out current_subject_is_var);
-			parse_property_list_not_empty (sql);
-
-			if (!accept (SparqlTokenType.DOT)) {
-				break;
-			}
-		}
-	}
-
-	bool is_subclass (Class class1, Class class2) {
-		if (class1 == class2) {
-			return true;
-		}
-		foreach (var superclass in class1.get_super_classes ()) {
-			if (is_subclass (superclass, class2)) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	bool is_simple_optional () {
-		var optional_start = get_location ();
-		try {
-			// check that we have { ?v foo:bar ?o }
-			// where ?v is an already BOUND variable
-			//       foo:bar is a single-valued property
-			//               that is known to be in domain of ?v
-			//       ?o has not been used before
-			// or
-			// where ?v has not been used before
-			//       foo:bar is an inverse functional property
-			//       ?o is an already ?BOUND variable
-
-			expect (SparqlTokenType.OPEN_BRACE);
-
-			// check subject
-			if (!accept (SparqlTokenType.VAR)) {
-				return false;
-			}
-			var left_variable = get_variable (get_last_string ().substring (1));
-			var left_variable_state = context.var_set.lookup (left_variable);
-
-			// check predicate
-			string predicate;
-			if (accept (SparqlTokenType.IRI_REF)) {
-				predicate = get_last_string (1);
-			} else if (accept (SparqlTokenType.PN_PREFIX)) {
-				string ns = get_last_string ();
-				expect (SparqlTokenType.COLON);
-				predicate = resolve_prefixed_name (ns, get_last_string ().substring (1));
-			} else if (accept (SparqlTokenType.COLON)) {
-				predicate = resolve_prefixed_name ("", get_last_string ().substring (1));
-			} else {
-				return false;
-			}
-			var prop = Ontologies.get_property_by_uri (predicate);
-			if (prop == null) {
-				return false;
-			}
-
-			// check object
-			if (!accept (SparqlTokenType.VAR)) {
-				return false;
-			}
-			var right_variable = get_variable (get_last_string ().substring (1));
-			var right_variable_state = context.var_set.lookup (right_variable);
-
-			// optional .
-			accept (SparqlTokenType.DOT);
-
-			// check it is only one triple pattern
-			if (!accept (SparqlTokenType.CLOSE_BRACE)) {
-				return false;
-			}
-
-			if (left_variable_state == VariableState.BOUND && !prop.multiple_values && right_variable_state == 0) {
-				bool in_domain = false;
-				foreach (VariableBinding binding in triple_context.var_map.lookup (left_variable).list) {
-					if (binding.type != null && is_subclass (binding.type, prop.domain)) {
-						in_domain = true;
-						break;
-					}
-				}
-
-				if (in_domain) {
-					// first valid case described in above comment
-					return true;
-				}
-			} else if (left_variable_state == 0 && prop.is_inverse_functional_property && right_variable_state == VariableState.BOUND) {
-				// second valid case described in above comment
-				return true;
-			}
-
-			// no match
-			return false;
-		} catch (SparqlError e) {
-			return false;
-		} finally {
-			// in any case, go back to the start of the optional
-			set_location (optional_start);
-		}
-	}
-
-	void translate_group_graph_pattern (StringBuilder sql) throws SparqlError {
-		expect (SparqlTokenType.OPEN_BRACE);
-
-		if (current () == SparqlTokenType.SELECT) {
-			translate_select (sql, true);
-
-			// only export selected variables
-			context.var_set = context.select_var_set;
-			context.select_var_set = new HashTable<Variable,int>.full (direct_hash, direct_equal, g_object_unref, null);
-
-			expect (SparqlTokenType.CLOSE_BRACE);
-			return;
-		}
-
-		SourceLocation[] filters = { };
-
-		bool in_triples_block = false;
-		bool in_group_graph_pattern = false;
-		bool first_where = true;
-		bool found_simple_optional = false;
-		long group_graph_pattern_start = sql.len;
-
-		// optional TriplesBlock
-		parse_triples (sql, group_graph_pattern_start, ref in_triples_block, ref first_where, ref in_group_graph_pattern, found_simple_optional);
-
-		while (true) {
-			// check whether we have GraphPatternNotTriples | Filter
-			if (accept (SparqlTokenType.OPTIONAL)) {
-				if (!in_group_graph_pattern && is_simple_optional ()) {
-					// perform join-less optional (like non-optional except for the IS NOT NULL check)
-					found_simple_optional = true;
-					expect (SparqlTokenType.OPEN_BRACE);
-
-					current_subject = parse_var_or_term (sql, out current_subject_is_var);
-					parse_property_list_not_empty (sql, true);
-
-					accept (SparqlTokenType.DOT);
-					expect (SparqlTokenType.CLOSE_BRACE);
-				} else {
-					if (!in_triples_block && !in_group_graph_pattern) {
-						// expand { OPTIONAL { ... } } into { { } OPTIONAL { ... } }
-						// empty graph pattern => return one result without bound variables
-						sql.append ("SELECT 1");
-					} else if (in_triples_block) {
-						end_triples_block (sql, ref first_where, in_group_graph_pattern);
-						in_triples_block = false;
-					}
-					if (!in_group_graph_pattern) {
-						in_group_graph_pattern = true;
-					}
-
-					var select = new StringBuilder ("SELECT ");
-
-					int left_index = ++next_table_index;
-					int right_index = ++next_table_index;
-
-					sql.append_printf (") AS t%d_g LEFT JOIN (", left_index);
-
-					context = new Context (context);
-
-					translate_group_graph_pattern (sql);
-
-					sql.append_printf (") AS t%d_g", right_index);
-
-					bool first = true;
-					bool first_common = true;
-					foreach (var v in context.var_set.get_keys ()) {
-						if (first) {
-							first = false;
-						} else {
-							select.append (", ");
-						}
-
-						var old_state = context.parent_context.var_set.lookup (v);
-						if (old_state == 0) {
-							// first used in optional part
-							context.parent_context.var_set.insert (v, VariableState.OPTIONAL);
-							select.append_printf ("t%d_g.%s", right_index, v.sql_expression);
-						} else {
-							if (first_common) {
-								sql.append (" ON ");
-								first_common = false;
-							} else {
-								sql.append (" AND ");
-							}
-
-							if (old_state == VariableState.BOUND) {
-								// variable definitely bound in non-optional part
-								sql.append_printf ("t%d_g.%s = t%d_g.%s", left_index, v.sql_expression, right_index, v.sql_expression);
-								select.append_printf ("t%d_g.%s", left_index, v.sql_expression);
-							} else if (old_state == VariableState.OPTIONAL) {
-								// variable maybe bound in non-optional part
-								sql.append_printf ("(t%d_g.%s IS NULL OR t%d_g.%s = t%d_g.%s)", left_index, v.sql_expression, left_index, v.sql_expression, right_index, v.sql_expression);
-								select.append_printf ("COALESCE (t%d_g.%s, t%d_g.%s) AS %s", left_index, v.sql_expression, right_index, v.sql_expression, v.sql_expression);
-							}
-						}
-					}
-					foreach (var v in context.parent_context.var_set.get_keys ()) {
-						if (context.var_set.lookup (v) == 0) {
-							// only used in non-optional part
-							if (first) {
-								first = false;
-							} else {
-								select.append (", ");
-							}
-
-							select.append_printf ("t%d_g.%s", left_index, v.sql_expression);
-						}
-					}
-					if (first) {
-						// no variables used at all
-						select.append ("1");
-					}
-
-					context = context.parent_context;
-
-					select.append (" FROM (");
-					sql.insert (group_graph_pattern_start, select.str);
-
-					// surround with SELECT * FROM (...) to avoid ambiguous column names
-					// in SQL generated for FILTER (triggered by using table aliases for join sources)
-					sql.insert (group_graph_pattern_start, "SELECT * FROM (");
-					sql.append (")");
-				}
-			} else if (accept (SparqlTokenType.GRAPH)) {
-				var old_graph = current_graph;
-				var old_graph_is_var = current_graph_is_var;
-				current_graph = parse_var_or_term (sql, out current_graph_is_var);
-
-				if (!in_triples_block && !in_group_graph_pattern) {
-					in_group_graph_pattern = true;
-					translate_group_or_union_graph_pattern (sql);
-				} else {
-					if (in_triples_block) {
-						end_triples_block (sql, ref first_where, in_group_graph_pattern);
-						in_triples_block = false;
-					}
-					if (!in_group_graph_pattern) {
-						in_group_graph_pattern = true;
-					}
-
-					sql.insert (group_graph_pattern_start, "SELECT * FROM (");
-					sql.append (") NATURAL INNER JOIN (");
-					translate_group_or_union_graph_pattern (sql);
-					sql.append (")");
-				}
-
-				current_graph = old_graph;
-				current_graph_is_var = old_graph_is_var;
-			} else if (current () == SparqlTokenType.OPEN_BRACE) {
-				if (!in_triples_block && !in_group_graph_pattern) {
-					in_group_graph_pattern = true;
-					translate_group_or_union_graph_pattern (sql);
-				} else {
-					if (in_triples_block) {
-						end_triples_block (sql, ref first_where, in_group_graph_pattern);
-						in_triples_block = false;
-					}
-					if (!in_group_graph_pattern) {
-						in_group_graph_pattern = true;
-					}
-
-					sql.insert (group_graph_pattern_start, "SELECT * FROM (");
-					sql.append (") NATURAL INNER JOIN (");
-					translate_group_or_union_graph_pattern (sql);
-					sql.append (")");
-				}
-			} else if (current () == SparqlTokenType.FILTER) {
-				filters += get_location ();
-				skip_filter ();
-			} else {
-				break;
-			}
-
-			accept (SparqlTokenType.DOT);
-
-			// optional TriplesBlock
-			parse_triples (sql, group_graph_pattern_start, ref in_triples_block, ref first_where, ref in_group_graph_pattern, found_simple_optional);
-		}
-
-		expect (SparqlTokenType.CLOSE_BRACE);
-
-		if (!in_triples_block && !in_group_graph_pattern) {
-			// empty graph pattern => return one result without bound variables
-			sql.append ("SELECT 1");
-		} else if (in_triples_block) {
-			end_triples_block (sql, ref first_where, in_group_graph_pattern);
-			in_triples_block = false;
-		}
-
-		if (in_group_graph_pattern) {
-			first_where = true;
-		}
-
-		// handle filters last, they apply to the pattern as a whole
-		if (filters.length > 0) {
-			var end = get_location ();
-
-			foreach (var filter_location in filters) {
-				if (!first_where) {
-					sql.append (" AND ");
-				} else {
-					sql.append (" WHERE ");
-					first_where = false;
-				}
-
-				set_location (filter_location);
-				translate_filter (sql);
-			}
-
-			set_location (end);
-		}
-	}
-
-	void translate_group_or_union_graph_pattern (StringBuilder sql) throws SparqlError {
-		Variable[] all_vars = { };
-		HashTable<Variable,int> all_var_set = new HashTable<Variable,int>.full (direct_hash, direct_equal, g_object_unref, null);
-
-		Context[] contexts = { };
-		long[] offsets = { };
-
-		do {
-			context = new Context (context);
-
-			contexts += context;
-			offsets += sql.len;
-			translate_group_graph_pattern (sql);
-
-			context = context.parent_context;
-		} while (accept (SparqlTokenType.UNION));
-
-		if (contexts.length > 1) {
-			// union graph pattern
-
-			// create union of all variables
-			foreach (var sub_context in contexts) {
-				foreach (var v in sub_context.var_set.get_keys ()) {
-					if (all_var_set.lookup (v) == 0) {
-						all_vars += v;
-						all_var_set.insert (v, VariableState.BOUND);
-						context.var_set.insert (v, VariableState.BOUND);
-					}
-				}
-			}
-
-			long extra_offset = 0;
-			for (int i = 0; i < contexts.length; i++) {
-				var projection = new StringBuilder ();
-				if (i > 0) {
-					projection.append (") UNION ALL ");
-				}
-				projection.append ("SELECT ");
-				foreach (var v in all_vars) {
-					if (contexts[i].var_set.lookup (v) == 0) {
-						// variable not used in this subgraph
-						// use NULL
-						projection.append ("NULL AS ");
-					}
-					projection.append_printf ("%s, ", v.sql_expression);
-				}
-				// delete last comma and space
-				projection.truncate (projection.len - 2);
-				projection.append (" FROM (");
-
-				sql.insert (offsets[i] + extra_offset, projection.str);
-				extra_offset += projection.len;
-			}
-			sql.append (")");
-		} else {
-			foreach (var key in contexts[0].var_set.get_keys ()) {
-				context.var_set.insert (key, VariableState.BOUND);
-			}
-		}
-	}
-
-	VariableBindingList? get_variable_binding_list (Variable variable) {
-		VariableBindingList binding_list = null;
-		if (triple_context != null) {
-			binding_list = triple_context.var_map.lookup (variable);
-		}
-		if (binding_list == null && context.in_scalar_subquery) {
-			// in scalar subquery: check variables of outer queries
-			var parent_context = context.parent_context;
-			while (parent_context != null) {
-				var outer_var = parent_context.var_map.lookup (variable.name);
-				if (outer_var != null && outer_var.binding != null) {
-					// capture outer variable
-					var binding = new VariableBinding ();
-					binding.data_type = outer_var.binding.data_type;
-					binding.variable = get_variable (variable.name);
-					binding.type = outer_var.binding.type;
-					binding.sql_expression = outer_var.sql_expression;
-					binding_list = new VariableBindingList ();
-					if (triple_context != null) {
-						triple_context.variables.append (binding.variable);
-						triple_context.var_map.insert (binding.variable, binding_list);
-					}
-
-					context.var_set.insert (binding.variable, VariableState.BOUND);
-					binding_list.list.append (binding);
-					binding.variable.binding = binding;
-					break;
-				}
-				parent_context = parent_context.parent_context;
-			}
-		}
-		return binding_list;
-	}
-
-	void add_variable_binding (StringBuilder sql, VariableBinding binding, VariableState variable_state) {
-		var binding_list = get_variable_binding_list (binding.variable);
-		if (binding_list == null) {
-			binding_list = new VariableBindingList ();
-			if (triple_context != null) {
-				triple_context.variables.append (binding.variable);
-				triple_context.var_map.insert (binding.variable, binding_list);
-			}
-
-			sql.append_printf ("%s AS %s, ",
-				binding.sql_expression,
-				binding.variable.sql_expression);
-
-			if (binding.data_type == PropertyType.DATETIME) {
-				sql.append_printf ("%s AS %s, ",
-					binding.get_extra_sql_expression ("localDate"),
-					binding.variable.get_extra_sql_expression ("localDate"));
-				sql.append_printf ("%s AS %s, ",
-					binding.get_extra_sql_expression ("localTime"),
-					binding.variable.get_extra_sql_expression ("localTime"));
-			}
-
-			context.var_set.insert (binding.variable, variable_state);
-		}
-		binding_list.list.append (binding);
-		if (binding.variable.binding == null) {
-			binding.variable.binding = binding;
-		}
-	}
-
-	void parse_object (StringBuilder sql, bool in_simple_optional = false) throws SparqlError {
-		bool object_is_var;
-		string object = parse_var_or_term (sql, out object_is_var);
-
-		string db_table;
-		bool rdftype = false;
-		bool share_table = true;
-		bool is_fts_match = false;
-
-		bool newtable;
-		DataTable table;
-		Property prop = null;
-
-		Class subject_type = null;
-
-		if (!current_predicate_is_var) {
-			prop = Ontologies.get_property_by_uri (current_predicate);
-
-			if (current_predicate == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
-			    && !object_is_var) {
-				// rdf:type query
-				rdftype = true;
-				var cl = Ontologies.get_class_by_uri (object);
-				if (cl == null) {
-					throw new SparqlError.UNKNOWN_CLASS ("Unknown class `%s'".printf (object));
-				}
-				db_table = cl.name;
-				subject_type = cl;
-			} else if (prop == null) {
-				if (current_predicate == "http://www.tracker-project.org/ontologies/fts#match";) {
-					// fts:match
-					db_table = "fts";
-					share_table = false;
-					is_fts_match = true;
-				} else {
-					throw new SparqlError.UNKNOWN_PROPERTY ("Unknown property `%s'".printf (current_predicate));
-				}
-			} else {
-				if (current_predicate == "http://www.w3.org/2000/01/rdf-schema#domain";
-				    && current_subject_is_var
-				    && !object_is_var) {
-					// rdfs:domain
-					var domain = Ontologies.get_class_by_uri (object);
-					if (domain == null) {
-						throw new SparqlError.UNKNOWN_CLASS ("Unknown class `%s'".printf (object));
-					}
-					var pv = context.predicate_variable_map.lookup (get_variable (current_subject));
-					if (pv == null) {
-						pv = new PredicateVariable ();
-						context.predicate_variable_map.insert (get_variable (current_subject), pv);
-					}
-					pv.domain = domain;
-				}
-
-				db_table = prop.table_name;
-				if (prop.multiple_values) {
-					// we can never share the table with multiple triples
-					// for multi value properties as a property may consist of multiple rows
-					share_table = false;
-				}
-				subject_type = prop.domain;
-
-				if (in_simple_optional && context.var_set.lookup (get_variable (current_subject)) == 0) {
-					// use subselect instead of join in simple optional where the subject is the unbound variable
-					// this can only happen with inverse functional properties
-					var binding = new VariableBinding ();
-					binding.data_type = PropertyType.RESOURCE;
-					binding.variable = get_variable (current_subject);
-
-					assert (triple_context.var_map.lookup (binding.variable) == null);
-					var binding_list = new VariableBindingList ();
-					triple_context.variables.append (binding.variable);
-					triple_context.var_map.insert (binding.variable, binding_list);
-
-					// need to use table and column name for object, can't refer to variable in nested select
-					var object_binding = triple_context.var_map.lookup (get_variable (object)).list.data;
-
-					sql.append_printf ("(SELECT ID FROM \"%s\" WHERE \"%s\" = %s) AS %s, ",
-						db_table,
-						prop.name,
-						object_binding.sql_expression,
-						binding.variable.sql_expression);
-
-					context.var_set.insert (binding.variable, VariableState.OPTIONAL);
-					binding_list.list.append (binding);
-
-					assert (binding.variable.binding == null);
-					binding.variable.binding = binding;
-
-					return;
-				}
-			}
-			table = get_table (current_subject, db_table, share_table, out newtable);
-		} else {
-			// variable in predicate
-			newtable = true;
-			table = new DataTable ();
-			table.predicate_variable = context.predicate_variable_map.lookup (get_variable (current_predicate));
-			if (table.predicate_variable == null) {
-				table.predicate_variable = new PredicateVariable ();
-				context.predicate_variable_map.insert (get_variable (current_predicate), table.predicate_variable);
-			}
-			if (!current_subject_is_var) {
-				// single subject
-				table.predicate_variable.subject = current_subject;
-			}
-			if (!current_subject_is_var) {
-				// single object
-				table.predicate_variable.object = object;
-			}
-			table.sql_query_tablename = current_predicate + (++counter).to_string ();
-			triple_context.tables.append (table);
-
-			// add to variable list
-			var binding = new VariableBinding ();
-			binding.data_type = PropertyType.RESOURCE;
-			binding.variable = get_variable (current_predicate);
-			binding.table = table;
-			binding.sql_db_column_name = "predicate";
-
-			add_variable_binding (sql, binding, VariableState.BOUND);
-		}
-
-		if (newtable) {
-			if (current_subject_is_var) {
-				var binding = new VariableBinding ();
-				binding.data_type = PropertyType.RESOURCE;
-				binding.variable = get_variable (current_subject);
-				binding.table = table;
-				binding.type = subject_type;
-				if (is_fts_match) {
-					binding.sql_db_column_name = "rowid";
-				} else {
-					binding.sql_db_column_name = "ID";
-				}
-
-				add_variable_binding (sql, binding, VariableState.BOUND);
-			} else {
-				var binding = new LiteralBinding ();
-				binding.data_type = PropertyType.RESOURCE;
-				binding.literal = current_subject;
-				// binding.data_type = triple.subject.type;
-				binding.table = table;
-				binding.sql_db_column_name = "ID";
-				triple_context.bindings.append (binding);
-			}
-		}
-
-		if (!rdftype) {
-			if (object_is_var) {
-				var binding = new VariableBinding ();
-				binding.variable = get_variable (object);
-				binding.table = table;
-				if (prop != null) {
-
-					binding.type = prop.range;
-
-					binding.data_type = prop.data_type;
-					binding.sql_db_column_name = prop.name;
-					if (!prop.multiple_values) {
-						// for single value properties, row may have NULL
-						// in any column except the ID column
-						binding.maybe_null = true;
-						binding.in_simple_optional = in_simple_optional;
-					}
-				} else {
-					// variable as predicate
-					binding.sql_db_column_name = "object";
-					binding.maybe_null = true;
-				}
-
-				VariableState state;
-				if (in_simple_optional) {
-					state = VariableState.OPTIONAL;
-				} else {
-					state = VariableState.BOUND;
-				}
-
-				add_variable_binding (sql, binding, state);
-			} else if (is_fts_match) {
-				var binding = new LiteralBinding ();
-				binding.is_fts_match = true;
-				binding.literal = object;
-				// binding.data_type = triple.object.type;
-				binding.table = table;
-				binding.sql_db_column_name = "fts";
-				triple_context.bindings.append (binding);
-
-				sql.append_printf ("rank(\"%s\".\"fts\") AS \"%s_u_rank\", ",
-					binding.table.sql_query_tablename,
-					get_variable (current_subject).name);
-				sql.append_printf ("offsets(\"%s\".\"fts\") AS \"%s_u_offsets\", ",
-					binding.table.sql_query_tablename,
-					get_variable (current_subject).name);
-			} else {
-				var binding = new LiteralBinding ();
-				binding.literal = object;
-				// binding.data_type = triple.object.type;
-				binding.table = table;
-				if (prop != null) {
-					binding.data_type = prop.data_type;
-					binding.sql_db_column_name = prop.name;
-				} else {
-					// variable as predicate
-					binding.sql_db_column_name = "object";
-				}
-				triple_context.bindings.append (binding);
-			}
-
-			if (current_graph != null && prop != null) {
-				if (current_graph_is_var) {
-					var binding = new VariableBinding ();
-					binding.variable = get_variable (current_graph);
-					binding.table = table;
-
-					binding.data_type = PropertyType.RESOURCE;
-					binding.sql_db_column_name = prop.name + ":graph";
-					binding.maybe_null = true;
-					binding.in_simple_optional = in_simple_optional;
-
-					VariableState state;
-					if (in_simple_optional) {
-						state = VariableState.OPTIONAL;
-					} else {
-						state = VariableState.BOUND;
-					}
-
-					add_variable_binding (sql, binding, state);
-				} else {
-					var binding = new LiteralBinding ();
-					binding.literal = current_graph;
-					binding.table = table;
-
-					binding.data_type = PropertyType.RESOURCE;
-					binding.sql_db_column_name = prop.name + ":graph";
-					triple_context.bindings.append (binding);
-				}
-			}
-		}
-
-		if (!current_subject_is_var &&
-		    !current_predicate_is_var &&
-		    !object_is_var) {
-			// no variables involved, add dummy expression to SQL
-			sql.append ("1, ");
-		}
-	}
-
-	DataTable get_table (string subject, string db_table, bool share_table, out bool newtable) {
-		string tablestring = "%s.%s".printf (subject, db_table);
-		DataTable table = null;
-		newtable = false;
-		if (share_table) {
-			table = triple_context.table_map.lookup (tablestring);
-		}
-		if (table == null) {
-			newtable = true;
-			table = new DataTable ();
-			table.sql_db_tablename = db_table;
-			table.sql_query_tablename = db_table + (++counter).to_string ();
-			triple_context.tables.append (table);
-			triple_context.table_map.insert (tablestring, table);
-		}
-		return table;
-	}
-
 	static string? get_string_for_value (Value value)
 	{
 		if (value.type () == typeof (int)) {



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]