/*
 * 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 {
	weak Query query;

	const int MAX_VARIABLES_FOR_IN = 20;
	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#";

	enum TimeFormatType {
		SECONDS,
		MINUTES,
		HOURS
	}

	string? fts_sql;

        Data.Manager manager;

	public Expression (Query query) {
		this.query = query;
                this.manager = query.manager;
	}

	Context context {
		get { return query.context; }
	}

	Pattern pattern {
		get { return query.pattern; }
	}

	private inline bool next () throws Sparql.Error {
		return query.next ();
	}

	private inline SparqlTokenType current () {
		return query.current ();
	}

	private inline SparqlTokenType last () {
		return query.last ();
	}

	private inline bool accept (SparqlTokenType type) throws Sparql.Error {
		return query.accept (type);
	}

	private Sparql.Error get_error (string msg) {
		return query.get_error (msg);
	}

	private bool expect (SparqlTokenType type) throws Sparql.Error {
		return query.expect (type);
	}

	private string get_last_string (int strip = 0) {
		return query.get_last_string (strip);
	}

	private string escape_sql_string_literal (string literal) {
		return "'%s'".printf (string.joinv ("''", literal.split ("'")));
	}

	private bool maybe_numeric (PropertyType type) {
		return (type == PropertyType.INTEGER || type == PropertyType.DOUBLE || type == PropertyType.DATE || type == PropertyType.DATETIME || type == PropertyType.UNKNOWN);
	}

	private void append_collate (StringBuilder sql) {
		sql.append_printf (" COLLATE %s", COLLATION_NAME);
	}

	private void skip_bracketted_expression () throws Sparql.Error {
		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 Sparql.Error {
		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, int variable_index) throws Sparql.Error {
		Variable variable = null;
		bool expect_close_parens = false;
		bool as_handled = false;

		long begin = sql.len;
		var type = PropertyType.UNKNOWN;
		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 if (accept (SparqlTokenType.OPEN_PARENS)) {
			if (current () == SparqlTokenType.SELECT) {
				// no parenthesis around expression
				// deprecated but supported for backward compatibility
				sql.append ("(");
				var select_context = pattern.translate_select (sql, true, true);
				sql.append (")");

				expect (SparqlTokenType.CLOSE_PARENS);
				type = select_context.type;
			} else {
				type = translate_expression (sql);
				if (accept (SparqlTokenType.CLOSE_PARENS)) {
					// missing AS
					// deprecated but supported for backward compatibility
				} else {
					// syntax from SPARQL 1.1 Draft
					// (Expression AS Var)
					expect_close_parens = true;
				}
			}
		} else {
			// no parenthesis around expression
			// deprecated but supported for backward compatibility
			type = translate_expression (sql);
		}

		if (!subquery) {
			convert_expression_to_string (sql, type, begin);
		}

		if (accept (SparqlTokenType.AS)) {
			// (...) AS ?foo
			expect (SparqlTokenType.VAR);
			variable = context.get_variable (get_last_string ().substring (1));
			sql.append_printf (" AS %s", variable.sql_expression);
			as_handled = true;

			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 (pattern.fts_subject != null) {
			if (variable == null) {
				// FTS matches still need aliases as the outer MATCH query
				// will fetch futher values from the joined select
				variable = context.get_variable ("var%d".printf (variable_index + 1));
			}

			if (fts_sql == null) {
				pattern.fts_variables += variable.sql_expression;

				if (as_handled == false) {
					sql.append_printf (" AS %s", variable.sql_expression);
				}
			} else {
				pattern.fts_variables += fts_sql;
				pattern.queries_fts_data = true;
			}
		}

		if (expect_close_parens) {
			expect (SparqlTokenType.CLOSE_PARENS);
		}

		if (variable != null) {
			int state = context.var_set.lookup (variable);
			if (state == 0) {
				state = VariableState.BOUND;
			}
			context.select_var_set.insert (variable, state);

			((SelectContext) context).variable_names += variable.name;
		} else {
			((SelectContext) context).variable_names += "var%d".printf (variable_index + 1);
		}

		fts_sql = null;

		return type;
	}

	private void translate_expression_as_order_condition (StringBuilder sql) throws Sparql.Error {
		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 Sparql.Error {
		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);
		}
	}

	private void translate_bound_call (StringBuilder sql) throws Sparql.Error {
		expect (SparqlTokenType.BOUND);
		expect (SparqlTokenType.OPEN_PARENS);
		sql.append ("(");
		translate_expression (sql);
		sql.append (" IS NOT NULL)");
		expect (SparqlTokenType.CLOSE_PARENS);
	}

	private PropertyType translate_if_call (StringBuilder sql) throws Sparql.Error {
		expect (SparqlTokenType.IF);
		expect (SparqlTokenType.OPEN_PARENS);

		// condition
		sql.append ("(CASE ");
		translate_expression (sql);

		// if condition is true
		sql.append (" WHEN 1 THEN ");
		expect (SparqlTokenType.COMMA);
		var type = translate_expression (sql);

		// if condition is false
		sql.append (" WHEN 0 THEN ");
		expect (SparqlTokenType.COMMA);
		translate_expression (sql);

		sql.append (" ELSE NULL END)");

		expect (SparqlTokenType.CLOSE_PARENS);

		return type;
	}

	private void translate_regex (StringBuilder sql) throws Sparql.Error {
		expect (SparqlTokenType.REGEX);
		expect (SparqlTokenType.OPEN_PARENS);
		sql.append ("SparqlRegex(");
		translate_expression_as_string (sql);
		sql.append (", ");
		expect (SparqlTokenType.COMMA);
		// SQLite's sqlite3_set_auxdata doesn't work correctly with bound
		// strings for the regex in function_sparql_regex.
		// translate_expression (sql);
		sql.append (escape_sql_string_literal (parse_string_literal ()));
		sql.append (", ");
		if (accept (SparqlTokenType.COMMA)) {
			// Same as above
			// translate_expression (sql);
			sql.append (escape_sql_string_literal (parse_string_literal ()));
		} else {
			sql.append ("''");
		}
		sql.append (")");
		expect (SparqlTokenType.CLOSE_PARENS);
	}

	private void translate_exists (StringBuilder sql) throws Sparql.Error {
		sql.append ("(");
		pattern.translate_exists (sql);
		sql.append (")");
	}

	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);
	}

	private static void convert_expression_to_string (StringBuilder sql, PropertyType type, long begin) {
		switch (type) {
		case PropertyType.STRING:
		case PropertyType.INTEGER:
			// nothing to convert
			// do not use CAST to convert integers to strings as this breaks use
			// of index when sorting by variable introduced in select expression
			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.DATE:
			// ISO 8601 format
			sql.insert (begin, "strftime (\"%Y-%m-%d\", ");
			sql.append (", \"unixepoch\")");
			break;
		case PropertyType.DATETIME:
			// ISO 8601 format
			sql.insert (begin, "SparqlFormatTime (");
			sql.append (")");
			break;
		default:
			// let sqlite convert the expression to string
			sql.insert (begin, "CAST (");
			sql.append (" AS TEXT)");
			break;
		}
	}

	private void translate_expression_as_string (StringBuilder sql) throws Sparql.Error {
		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;
		}
	}

	private void translate_str (StringBuilder sql) throws Sparql.Error {
		expect (SparqlTokenType.STR);
		expect (SparqlTokenType.OPEN_PARENS);

		translate_expression_as_string (sql);

		expect (SparqlTokenType.CLOSE_PARENS);
	}

	private void translate_isuri (StringBuilder sql) throws Sparql.Error {
		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);
	}

	private void translate_datatype (StringBuilder sql) throws Sparql.Error {
		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);
	}

	private void translate_date (StringBuilder sql, string format) throws Sparql.Error {
		sql.append_printf ("strftime (\"%s\", ", format);

		if (accept (SparqlTokenType.VAR)) {
			string variable_name = get_last_string ().substring (1);
			var variable = context.get_variable (variable_name);
			sql.append (variable.get_extra_sql_expression ("localDate"));
			sql.append (" * 24 * 3600");
		} else {
			translate_primary_expression (sql);
		}

		sql.append (", \"unixepoch\")");
	}

	private void translate_time (StringBuilder sql, TimeFormatType type) throws Sparql.Error {
		sql.append ("(");
		if (accept (SparqlTokenType.VAR)) {
			string variable_name = get_last_string ().substring (1);
			var variable = context.get_variable (variable_name);
			sql.append (variable.get_extra_sql_expression ("localTime"));
		} else {
			translate_primary_expression (sql);
		}

		switch (type) {
		case TimeFormatType.SECONDS:
			sql.append ("% 60");
			break;
		case TimeFormatType.MINUTES:
			sql.append (" / 60 % 60");
			break;
		case TimeFormatType.HOURS:
			sql.append (" / 3600 % 24");
			break;
		}
		sql.append (")");
	}

	private PropertyType translate_function (StringBuilder sql, string uri) throws Sparql.Error {
		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 == TRACKER_NS + "case-fold") {
			// conversion to string
			sql.append ("SparqlCaseFold (");
			translate_expression_as_string (sql);
			sql.append (")");
			return PropertyType.STRING;
		} else if (uri == TRACKER_NS + "ascii-lower-case") {
			// conversion to string
			sql.append ("lower (");
			translate_expression_as_string (sql);
			sql.append (")");
			return PropertyType.STRING;
		} else if (uri == TRACKER_NS + "title-order") {
			translate_expression_as_string (sql);
			sql.append_printf (" COLLATE %s", TITLE_COLLATION_NAME);
			return PropertyType.STRING;
		} else if (uri == FN_NS + "lower-case") {
			// conversion to string
			sql.append ("SparqlLowerCase (");
			translate_expression_as_string (sql);
			sql.append (")");
			return PropertyType.STRING;
		} else if (uri == FN_NS + "upper-case") {
			sql.append ("SparqlUpperCase (");
			translate_expression_as_string (sql);
			sql.append (")");
			return PropertyType.STRING;
		} else if (uri == TRACKER_NS + "normalize") {
			// conversion to string
			sql.append ("SparqlNormalize (");
			translate_expression_as_string (sql);
			sql.append (", ");
			expect (SparqlTokenType.COMMA);
			translate_expression_as_string (sql);
			sql.append (")");
			return PropertyType.STRING;
		} else if (uri == TRACKER_NS + "unaccent") {
			// conversion to string
			sql.append ("SparqlUnaccent (");
			translate_expression_as_string (sql);
			sql.append (")");
			return PropertyType.STRING;
		} 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' BETWEEN 'B' AND 'B\u0010fffd'
			// 0010fffd always sorts last

			translate_expression_as_string (sql);
			sql.append (" BETWEEN ");

			expect (SparqlTokenType.COMMA);
			string prefix = parse_string_literal ();

			sql.append ("?");
			var binding = new LiteralBinding ();
			binding.literal = prefix;
			query.bindings.append (binding);

			sql.append (" AND ");

			sql.append ("?");
			binding = new LiteralBinding ();
			binding.literal = prefix + COLLATION_LAST_CHAR.to_string ();
			query.bindings.append (binding);

			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 + "substring") {
			sql.append ("substr(");
			translate_expression_as_string (sql);

			sql.append (", ");
			expect (SparqlTokenType.COMMA);
			translate_expression_as_string (sql);

			if (accept (SparqlTokenType.COMMA)) {
			      sql.append (", ");
			      translate_expression_as_string (sql);
			}

			sql.append (")");

			return PropertyType.STRING;
		} else if (uri == FN_NS + "concat") {
			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);
			}

			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") {
			translate_date (sql, "%Y");
			return PropertyType.INTEGER;
		} else if (uri == FN_NS + "month-from-dateTime") {
			translate_date (sql, "%m");
			return PropertyType.INTEGER;
		} else if (uri == FN_NS + "day-from-dateTime") {
			translate_date (sql, "%d");
			return PropertyType.INTEGER;
		} else if (uri == FN_NS + "hours-from-dateTime") {
			translate_time (sql, TimeFormatType.HOURS);
			return PropertyType.INTEGER;
		} else if (uri == FN_NS + "minutes-from-dateTime") {
			translate_time (sql, TimeFormatType.MINUTES);
			return PropertyType.INTEGER;
		} else if (uri == FN_NS + "seconds-from-dateTime") {
			translate_time (sql, TimeFormatType.SECONDS);
			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 ("CAST (");
			sql.append (variable.sql_expression);
			sql.append (" AS INTEGER)");
			sql.append (")");

			return PropertyType.INTEGER;
		} else if (uri == FN_NS + "replace") {
			sql.append ("SparqlReplace(");
			translate_expression_as_string (sql);
			sql.append (", ");

			expect (SparqlTokenType.COMMA);
			translate_expression_as_string (sql);
			sql.append (", ");

			expect (SparqlTokenType.COMMA);
			translate_expression_as_string (sql);

			if (accept (SparqlTokenType.COMMA)) {
				sql.append (", ");
				sql.append (escape_sql_string_literal (parse_string_literal ()));
			}
			sql.append (")");
			return PropertyType.STRING;
		} 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);
			var variable = context.get_variable (v);

			sql.append (variable.sql_expression);
			fts_sql = "tracker_offsets(\"fts5\")";
			return PropertyType.STRING;
		} else if (uri == FTS_NS + "snippet") {
			bool is_var;

			string v = pattern.parse_var_or_term (null, out is_var);
			var variable = context.get_variable (v);
			var fts = new StringBuilder ();

			fts.append_printf ("snippet(\"fts5\"");

			// lookup column
			fts.append (", -1");

			// "start match" text
			if (accept (SparqlTokenType.COMMA)) {
				fts.append (", ");
				translate_expression_as_string (fts);

				// "end match" text
				expect (SparqlTokenType.COMMA);
				fts.append (", ");
				translate_expression_as_string (fts);
			} else {
				fts.append(",'',''");
			}

			// "ellipsis" text
			if (accept (SparqlTokenType.COMMA)) {
				fts.append (", ");
				translate_expression_as_string (fts);
			} else {
				fts.append (", '...'");
			}

			// Approximate number of words in context
			if (accept (SparqlTokenType.COMMA)) {
				fts.append (", ");
				translate_expression_as_string (fts);
			} else {
				fts.append (", 5");
			}

			fts.append (")");

			fts_sql = fts.str;
			sql.append (variable.sql_expression);
			return PropertyType.STRING;
		} else if (uri == TRACKER_NS + "id") {
			var type = translate_expression (sql);
			if (type != PropertyType.RESOURCE) {
				throw get_error ("expected resource");
			}

			return PropertyType.INTEGER;
		} else if (uri == TRACKER_NS + "uri") {
			var type = translate_expression (sql);
			if (type != PropertyType.INTEGER) {
				throw get_error ("expected integer ID");
			}

			return PropertyType.RESOURCE;
		} 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 + "uri-is-parent") {
			sql.append ("SparqlUriIsParent(");
			translate_expression_as_string (sql);
			sql.append (", ");
			expect (SparqlTokenType.COMMA);

			translate_expression_as_string (sql);
			sql.append (")");

			return PropertyType.BOOLEAN;
		} else if (uri == TRACKER_NS + "uri-is-descendant") {
			sql.append ("SparqlUriIsDescendant(");
			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.BOOLEAN;
		} 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 ontologies = manager.get_ontologies ();
			var prop = ontologies.get_property_by_uri (uri);
			if (prop == null) {
				throw get_error ("Unknown function");
			}

			var expr = new StringBuilder ();
			translate_expression (expr);

			string value_separator = ",";
			string? graph_separator = null;

			if (accept (SparqlTokenType.COMMA)) {
				value_separator = parse_string_literal ();

				if (accept (SparqlTokenType.COMMA)) {
					graph_separator = parse_string_literal ();
				}
			}

			if (prop.multiple_values) {
				// multi-valued property
				sql.append ("(SELECT GROUP_CONCAT(");
				long begin = sql.len;
				sql.append_printf ("\"%s\"", prop.name);
				convert_expression_to_string (sql, prop.data_type, begin);
				if (graph_separator != null) {
					sql.append_printf (" || %s || COALESCE((SELECT Uri FROM Resource WHERE ID = \"%s:graph\"), '')", escape_sql_string_literal (graph_separator), prop.name);
				}
				sql.append_printf (",%s)", escape_sql_string_literal (value_separator));
				sql.append_printf (" FROM \"%s\" WHERE ID = %s)", prop.table_name, expr.str);

				return PropertyType.STRING;
			} else {
				// single-valued property
				if (graph_separator == null) {
					sql.append_printf ("(SELECT \"%s\" FROM \"%s\" WHERE ID = %s)", prop.name, prop.table_name, expr.str);

					if (prop.data_type == PropertyType.STRING) {
						append_collate (sql);
					}

					return prop.data_type;
				} else {
					sql.append ("(SELECT ");
					long begin = sql.len;
					sql.append_printf ("\"%s\"", prop.name);
					convert_expression_to_string (sql, prop.data_type, begin);
					sql.append_printf (" || %s || COALESCE((SELECT Uri FROM Resource WHERE ID = \"%s:graph\"), '')", escape_sql_string_literal (graph_separator), prop.name);
					sql.append_printf (" FROM \"%s\" WHERE ID = %s)", prop.table_name, expr.str);

					return PropertyType.STRING;
				}
			}
		}
	}

	private PropertyType parse_type_uri () throws Sparql.Error {
		string type_iri;
		PropertyType type;

		if (accept (SparqlTokenType.IRI_REF)) {
			type_iri = get_last_string (1);
		} else if (accept (SparqlTokenType.PN_PREFIX)) {
			string ns = get_last_string ();
			expect (SparqlTokenType.COLON);
			type_iri = query.resolve_prefixed_name (ns, get_last_string ().substring (1));
		} else {
			expect (SparqlTokenType.COLON);
			type_iri = query.resolve_prefixed_name ("", get_last_string ().substring (1));
		}

		if (type_iri == XSD_NS + "boolean") {
			type = PropertyType.BOOLEAN;
		} else if (type_iri == XSD_NS + "integer" ||
		           type_iri == XSD_NS + "nonPositiveInteger" ||
		           type_iri == XSD_NS + "negativeInteger" ||
		           type_iri == XSD_NS + "long" ||
		           type_iri == XSD_NS + "int" ||
		           type_iri == XSD_NS + "short" ||
		           type_iri == XSD_NS + "byte" ||
		           type_iri == XSD_NS + "nonNegativeInteger" ||
		           type_iri == XSD_NS + "unsignedLong" ||
		           type_iri == XSD_NS + "unsignedInt" ||
		           type_iri == XSD_NS + "unsignedShort" ||
		           type_iri == XSD_NS + "unsignedByte" ||
		           type_iri == XSD_NS + "positiveInteger") {
			type = PropertyType.INTEGER;
		} else if (type_iri == XSD_NS + "double") {
			type = PropertyType.DOUBLE;
		} else if (type_iri == XSD_NS + "date") {
			type = PropertyType.DATE;
		} else if (type_iri == XSD_NS + "dateTime") {
			type = PropertyType.DATETIME;
		} else {
			type = PropertyType.STRING;
		}

		return type;
	}

	internal string parse_string_literal (out PropertyType type = null) throws Sparql.Error {
		type = PropertyType.STRING;

		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.length;
			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;
					case 'u':
						char* ptr = (char*) p + 1;
						unichar c = (((unichar) ptr[0].xdigit_value () * 16 + ptr[1].xdigit_value ()) * 16 + ptr[2].xdigit_value ()) * 16 + ptr[3].xdigit_value ();
						sb.append_unichar (c);
						p += 4;
						break;
					}
					p++;
				}
			}

			if (accept (SparqlTokenType.DOUBLE_CIRCUMFLEX)) {
				// typed literal
				type = parse_type_uri ();
			}

			return sb.str;
		case SparqlTokenType.STRING_LITERAL_LONG1:
		case SparqlTokenType.STRING_LITERAL_LONG2:
			string result = get_last_string (3);

			if (accept (SparqlTokenType.DOUBLE_CIRCUMFLEX)) {
				// typed literal
				type = parse_type_uri ();
			}

			return result;
		default:
			throw get_error ("expected string literal");
		}
	}

	private PropertyType translate_uri_expression (StringBuilder sql, string uri) throws Sparql.Error {
		if (accept (SparqlTokenType.OPEN_PARENS)) {
			// function
			var result = translate_function (sql, uri);
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		} else {
			// resource
			sql.append ("COALESCE((SELECT ID FROM Resource WHERE Uri = ?), 0)");
			var binding = new LiteralBinding ();
			binding.literal = uri;
			query.bindings.append (binding);
			return PropertyType.RESOURCE;
		}
	}

	private PropertyType translate_primary_expression (StringBuilder sql) throws Sparql.Error {
		PropertyType type;

		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 ();

			if (query.no_cache) {
				sql.append (get_last_string ());
			} else {
				sql.append ("?");

				var binding = new LiteralBinding ();
				binding.literal = get_last_string ();
				query.bindings.append (binding);
			}

			return PropertyType.DOUBLE;
		case SparqlTokenType.TRUE:
			next ();

			if (query.no_cache) {
				sql.append ("1");
			} else {
				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 ();

			if (query.no_cache) {
				sql.append ("0");
			} else {
				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:
			var literal = parse_string_literal (out type);

			switch (type) {
			case PropertyType.INTEGER:
			case PropertyType.BOOLEAN:
			case PropertyType.DOUBLE:
			case PropertyType.DATE:
			case PropertyType.DATETIME:
				if (query.no_cache) {
					sql.append (escape_sql_string_literal (literal));
				} else {
					var binding = new LiteralBinding ();
					binding.literal = literal;
					binding.data_type = type;
					query.bindings.append (binding);
					sql.append ("?");
				}
				return type;
			default:
				if (query.no_cache) {
					sql.append (escape_sql_string_literal (literal));
				} else {
					var binding = new LiteralBinding ();
					binding.literal = literal;
					query.bindings.append (binding);
					sql.append ("?");
				}
				append_collate (sql);
				return PropertyType.STRING;
			}
		case SparqlTokenType.INTEGER:
			next ();

			if (query.no_cache) {
				sql.append (get_last_string ());
			} else {
				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);

			if (context.need_binding_expression && variable.binding != null) {
				sql.append (variable.binding.sql_expression);
			} else {
				sql.append (variable.sql_expression);
			}

			if (variable.binding == null) {
				return PropertyType.UNKNOWN;
			} else {
				if (variable.binding.data_type == PropertyType.STRING) {
					append_collate (sql);
				}
				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.COALESCE:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, TRACKER_NS + "coalesce");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.CONCAT:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "concat");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.CONTAINS:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "contains");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.ENCODE_FOR_URI:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			sql.append ("SparqlEncodeForUri (");
			translate_expression_as_string (sql);
			sql.append (")");
			expect (SparqlTokenType.CLOSE_PARENS);
			return PropertyType.STRING;
		case SparqlTokenType.IF:
			return translate_if_call (sql);
		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.LCASE:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "lower-case");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.UCASE:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "upper-case");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.STRLEN:
			next ();
			sql.append ("LENGTH(");
			type = translate_aggregate_expression (sql);
			sql.append (")");
			return PropertyType.INTEGER;
		case SparqlTokenType.STRSTARTS:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "starts-with");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.STRENDS:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "ends-with");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.SUBSTR:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "substring");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.STRBEFORE:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			sql.append ("SparqlStringBefore (");
			translate_expression_as_string (sql);
			expect (SparqlTokenType.COMMA);
			sql.append (",");
			translate_expression_as_string (sql);
			sql.append (")");
			expect (SparqlTokenType.CLOSE_PARENS);
			return PropertyType.STRING;
		case SparqlTokenType.STRAFTER:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			sql.append ("SparqlStringAfter (");
			translate_expression_as_string (sql);
			expect (SparqlTokenType.COMMA);
			sql.append (",");
			translate_expression_as_string (sql);
			sql.append (")");
			expect (SparqlTokenType.CLOSE_PARENS);
			return PropertyType.STRING;
		case SparqlTokenType.REGEX:
			translate_regex (sql);
			query.no_cache = true;
			return PropertyType.BOOLEAN;
		case SparqlTokenType.EXISTS:
		case SparqlTokenType.NOT:
			translate_exists (sql);
			return PropertyType.BOOLEAN;
		case SparqlTokenType.COUNT:
			next ();
			sql.append ("COUNT(");
			translate_aggregate_expression (sql);
			sql.append (")");
			return PropertyType.INTEGER;
		case SparqlTokenType.SUM:
			next ();
			sql.append ("SUM(");
			type = translate_aggregate_expression (sql);
			sql.append (")");
			return type;
		case SparqlTokenType.AVG:
			next ();
			sql.append ("AVG(");
			type = translate_aggregate_expression (sql);
			sql.append (")");
			return type;
		case SparqlTokenType.MIN:
			next ();
			sql.append ("MIN(");
			type = translate_aggregate_expression (sql);
			sql.append (")");
			return type;
		case SparqlTokenType.MAX:
			next ();
			sql.append ("MAX(");
			type = translate_aggregate_expression (sql);
			sql.append (")");
			return type;
		case SparqlTokenType.ABS:
			next ();
			sql.append ("ABS(");
			type = translate_aggregate_expression (sql);
			sql.append (")");
			return type;
		case SparqlTokenType.ROUND:
			next ();
			sql.append ("ROUND(");
			type = translate_aggregate_expression (sql);
			sql.append (")");
			return type;
		case SparqlTokenType.CEIL:
			next ();
			sql.append ("SparqlCeil(");
			type = translate_aggregate_expression (sql);
			sql.append (")");
			return type;
		case SparqlTokenType.FLOOR:
			next ();
			sql.append ("SparqlFloor(");
			type = translate_aggregate_expression (sql);
			sql.append (")");
			return type;
		case SparqlTokenType.RAND:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			expect (SparqlTokenType.CLOSE_PARENS);
			sql.append ("SparqlRand()");
			return PropertyType.DOUBLE;
		case SparqlTokenType.NOW:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			expect (SparqlTokenType.CLOSE_PARENS);
			sql.append ("strftime('%s', 'now')");
			return PropertyType.DATETIME;
		case SparqlTokenType.SECONDS:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "seconds-from-dateTime");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.MINUTES:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "minutes-from-dateTime");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.HOURS:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "hours-from-dateTime");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.DAY:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "day-from-dateTime");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.MONTH:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "month-from-dateTime");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.YEAR:
			next ();
			expect (SparqlTokenType.OPEN_PARENS);
			var result = translate_function (sql, FN_NS + "year-from-dateTime");
			expect (SparqlTokenType.CLOSE_PARENS);
			return result;
		case SparqlTokenType.MD5:
			next ();
			sql.append ("SparqlChecksum(");
			type = translate_aggregate_expression (sql);
			sql.append (", \"md5\")");
			return type;
		case SparqlTokenType.SHA1:
			next ();
			sql.append ("SparqlChecksum(");
			type = translate_aggregate_expression (sql);
			sql.append (", \"sha1\")");
			return type;
		case SparqlTokenType.SHA256:
			next ();
			sql.append ("SparqlChecksum(");
			type = translate_aggregate_expression (sql);
			sql.append (", \"sha256\")");
			return type;
		case SparqlTokenType.SHA512:
			next ();
			sql.append ("SparqlChecksum(");
			type = translate_aggregate_expression (sql);
			sql.append (", \"sha512\")");
			return type;
		case SparqlTokenType.GROUP_CONCAT:
			next ();
			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);
			return PropertyType.STRING;
		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 Sparql.Error {
		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);
	}

	private PropertyType translate_multiplicative_expression (StringBuilder sql) throws Sparql.Error {
		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;
	}

	private PropertyType translate_additive_expression (StringBuilder sql) throws Sparql.Error {
		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;
	}

	private PropertyType translate_numeric_expression (StringBuilder sql) throws Sparql.Error {
		return translate_additive_expression (sql);
	}

	private PropertyType process_relational_expression (StringBuilder sql, long begin, uint n_bindings, PropertyType op1type, string operator) throws Sparql.Error {
		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;
			}
		} else if ((op1type == PropertyType.DATE && op2type == PropertyType.STRING)
		           || (op1type == PropertyType.STRING && op2type == PropertyType.DATE)) {
			// TODO: improve performance (linked list)
			if (query.bindings.length () == n_bindings + 1) {
				// trigger string => date conversion
				query.bindings.last ().data.data_type = PropertyType.DATE;
			}
		}
		return PropertyType.BOOLEAN;
	}

	private PropertyType translate_in (StringBuilder sql, bool not) throws Sparql.Error {

		int in_variable_count = 0;

		if (not) {
			sql.append (" NOT");
		}

		expect (SparqlTokenType.OPEN_PARENS);
		sql.append (" IN (");
		if (!accept (SparqlTokenType.CLOSE_PARENS)) {
			in_variable_count++;
			translate_expression (sql);
			while (accept (SparqlTokenType.COMMA)) {
				sql.append (", ");

				in_variable_count++;

				if (in_variable_count > MAX_VARIABLES_FOR_IN && !query.no_cache) {
					query.no_cache = true;
				}

				translate_expression (sql);
			}
			expect (SparqlTokenType.CLOSE_PARENS);
		}
		sql.append (")");

		return PropertyType.BOOLEAN;
	}

	private PropertyType translate_relational_expression (StringBuilder sql) throws Sparql.Error {
		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, " > ");
		} else if (accept (SparqlTokenType.OP_IN)) {
			return translate_in (sql, false);
		} else if (accept (SparqlTokenType.NOT)) {
			expect (SparqlTokenType.OP_IN);
			return translate_in (sql, true);
		}
		return optype;
	}

	private PropertyType translate_value_logical (StringBuilder sql) throws Sparql.Error {
		return translate_relational_expression (sql);
	}

	private PropertyType translate_conditional_and_expression (StringBuilder sql) throws Sparql.Error {
		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;
	}

	private PropertyType translate_conditional_or_expression (StringBuilder sql) throws Sparql.Error {
		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 Sparql.Error {
		return translate_conditional_or_expression (sql);
	}

	private PropertyType translate_bracketted_expression (StringBuilder sql) throws Sparql.Error {
		expect (SparqlTokenType.OPEN_PARENS);

		if (current () == SparqlTokenType.SELECT) {
			// scalar subquery

			sql.append ("(");
			var select_context = pattern.translate_select (sql, true, true);
			sql.append (")");

			expect (SparqlTokenType.CLOSE_PARENS);
			return select_context.type;
		}

		var optype = translate_expression (sql);
		expect (SparqlTokenType.CLOSE_PARENS);
		return optype;
	}

	private PropertyType translate_aggregate_expression (StringBuilder sql) throws Sparql.Error {
		expect (SparqlTokenType.OPEN_PARENS);
		if (accept (SparqlTokenType.DISTINCT)) {
			sql.append ("DISTINCT ");
		}

		bool is_var = (current () == SparqlTokenType.VAR);
		var optype = translate_expression (sql);

		if (is_var) {
			var variable = context.get_variable (get_last_string ().substring (1));
			if (variable.binding == null) {
				throw get_error ("use of undefined variable `%s'".printf (variable.name));
			}
		}

		expect (SparqlTokenType.CLOSE_PARENS);
		return optype;
	}

	internal PropertyType translate_constraint (StringBuilder sql) throws Sparql.Error {
		switch (current ()) {
		case SparqlTokenType.STR:
		case SparqlTokenType.LANG:
		case SparqlTokenType.LANGMATCHES:
		case SparqlTokenType.DATATYPE:
		case SparqlTokenType.BOUND:
		case SparqlTokenType.IF:
		case SparqlTokenType.SAMETERM:
		case SparqlTokenType.ISIRI:
		case SparqlTokenType.ISURI:
		case SparqlTokenType.ISBLANK:
		case SparqlTokenType.ISLITERAL:
		case SparqlTokenType.REGEX:
		case SparqlTokenType.EXISTS:
		case SparqlTokenType.NOT:
			return translate_primary_expression (sql);
		default:
			return translate_bracketted_expression (sql);
		}
	}
}
