[tracker] Add SPARQL query engine
- From: Jürg Billeter <juergbi src gnome org>
- To: svn-commits-list gnome org
- Subject: [tracker] Add SPARQL query engine
- Date: Thu, 16 Apr 2009 10:37:28 -0400 (EDT)
commit 0ce92d07efc761d31ffce987971d550b5ab9dfa3
Author: Jürg Billeter <j bitron ch>
Date: Thu Apr 9 10:15:31 2009 +0200
Add SPARQL query engine
---
configure.ac | 3 +
data/dbus/tracker-indexer.xml | 6 +
data/dbus/tracker-resources.xml | 15 +
po/POTFILES.in | 1 +
src/libtracker-common/Makefile.am | 5 +-
src/libtracker-common/libtracker-common.vapi | 72 ++
src/libtracker-data/.gitignore | 2 +
src/libtracker-data/Makefile.am | 20 +-
src/libtracker-data/libtracker-data.vapi | 31 +
src/libtracker-data/tracker-data-query.c | 12 +-
src/libtracker-data/tracker-data-update.c | 9 +-
src/libtracker-data/tracker-sparql-query.vala | 1168 +++++++++++++++++++++++++
src/libtracker-db/Makefile.am | 5 +-
src/libtracker-db/libtracker-db.vapi | 55 ++
src/libtracker/tracker.c | 48 +
src/libtracker/tracker.h | 4 +
src/rasqal/rasqal.vapi | 210 +++++
src/tracker-indexer/tracker-indexer.c | 36 +
src/tracker-indexer/tracker-indexer.h | 4 +
src/tracker-utils/.gitignore | 1 +
src/tracker-utils/Makefile.am | 6 +-
src/tracker-utils/tracker-info.c | 291 ++-----
src/tracker-utils/tracker-search.c | 175 +---
src/tracker-utils/tracker-sparql.c | 181 ++++
src/trackerd/tracker-resources.c | 80 ++
src/trackerd/tracker-resources.h | 8 +
26 files changed, 2087 insertions(+), 361 deletions(-)
diff --git a/configure.ac b/configure.ac
index dfa89a8..36dd472 100644
--- a/configure.ac
+++ b/configure.ac
@@ -239,6 +239,9 @@ AM_CONDITIONAL(RASQAL_QUERY_RDQL, true)
AM_CONDITIONAL(RASQAL_QUERY_LAQRS, true)
AM_CONDITIONAL(RASQAL_QUERY_SPARQL, true)
+AC_PATH_PROG(VALAC, valac, valac)
+AC_SUBST(VALAC)
+
# Check we have the DBUS binding tool we need
AC_PATH_PROG(DBUSBINDINGTOOL, dbus-binding-tool)
if test -z $DBUSBINDINGTOOL; then
diff --git a/data/dbus/tracker-indexer.xml b/data/dbus/tracker-indexer.xml
index a42f963..0c26ef9 100644
--- a/data/dbus/tracker-indexer.xml
+++ b/data/dbus/tracker-indexer.xml
@@ -65,6 +65,12 @@
<arg type="s" name="object" direction="in" />
</method>
+ <!-- SPARQL Update extensions, allows bulk insert and delete -->
+ <method name="SparqlUpdate">
+ <annotation name="org.freedesktop.DBus.GLib.Async" value="true"/>
+ <arg type="s" name="query" direction="in" />
+ </method>
+
<method name="Pause">
<annotation name="org.freedesktop.DBus.GLib.Async" value="true"/>
</method>
diff --git a/data/dbus/tracker-resources.xml b/data/dbus/tracker-resources.xml
index 74f8e25..233fc64 100644
--- a/data/dbus/tracker-resources.xml
+++ b/data/dbus/tracker-resources.xml
@@ -25,5 +25,20 @@
<arg type="s" name="uri" direction="in" />
</method>
+ <!-- SPARQL Query without updates -->
+ <method name="SparqlQuery">
+ <annotation name="org.freedesktop.DBus.GLib.Async" value="true"/>
+ <annotation name="com.trolltech.QtDBus.QtTypeName.Out0"
+ value="QVector<QStringList>"/>
+ <arg type="s" name="query" direction="in" />
+ <arg type="aas" name="result" direction="out" />
+ </method>
+
+ <!-- SPARQL Update extensions, allows bulk insert and delete -->
+ <method name="SparqlUpdate">
+ <annotation name="org.freedesktop.DBus.GLib.Async" value="true"/>
+ <arg type="s" name="query" direction="in" />
+ </method>
+
</interface>
</node>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 076264c..73db1d1 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -29,6 +29,7 @@ src/tracker-utils/tracker-info.c
src/tracker-utils/tracker-meta-folder.c
src/tracker-utils/tracker-processes.c
src/tracker-utils/tracker-search.c
+src/tracker-utils/tracker-sparql.c
src/tracker-utils/tracker-stats.c
src/tracker-utils/tracker-status.c
src/tracker-utils/tracker-tag.c
diff --git a/src/libtracker-common/Makefile.am b/src/libtracker-common/Makefile.am
index a267173..d4a9335 100644
--- a/src/libtracker-common/Makefile.am
+++ b/src/libtracker-common/Makefile.am
@@ -112,4 +112,7 @@ BUILT_SOURCES = \
CLEANFILES = $(BUILT_SOURCES)
-EXTRA_DIST = tracker-marshal.list
+EXTRA_DIST = \
+ tracker-marshal.list \
+ libtracker-common.vapi
+
diff --git a/src/libtracker-common/libtracker-common.vapi b/src/libtracker-common/libtracker-common.vapi
new file mode 100644
index 0000000..1939efc
--- /dev/null
+++ b/src/libtracker-common/libtracker-common.vapi
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2008-2009, Nokia
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU 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 {
+ [CCode (cheader_filename = "libtracker-common/tracker-class.h")]
+ public class Class : GLib.Object {
+ public string name { get; set; }
+ public string uri { get; set; }
+ }
+
+ [CCode (cheader_filename = "libtracker-common/tracker-namespace.h")]
+ public class Namespace : GLib.Object {
+ public string prefix { get; set; }
+ public string uri { get; set; }
+ }
+
+ [CCode (cheader_filename = "libtracker-common/tracker-property.h")]
+ public class Property : GLib.Object {
+ public string name { get; set; }
+ public string uri { get; set; }
+ public PropertyType data_type { get; set; }
+ public Class domain { get; set; }
+ public Class range { get; set; }
+ public bool multiple_values { get; set; }
+ }
+
+ [CCode (cheader_filename = "libtracker-common/tracker-property.h")]
+ public enum PropertyType {
+ STRING,
+ BOOLEAN,
+ INTEGER,
+ DOUBLE,
+ DATE,
+ DATETIME,
+ BLOB,
+ STRUCT,
+ RESOURCE,
+ FULLTEXT
+ }
+
+ [CCode (cheader_filename = "libtracker-common/tracker-ontology.h")]
+ namespace Ontology {
+ public weak Class get_class_by_uri (string class_uri);
+ public weak Property get_property_by_uri (string property_uri);
+ [CCode (array_length = false, array_null_terminated = true)]
+ public weak Namespace[] get_namespaces ();
+ [CCode (array_length = false, array_null_terminated = true)]
+ public weak Class[] get_classes ();
+ [CCode (array_length = false, array_null_terminated = true)]
+ public weak Property[] get_properties ();
+ }
+
+ [CCode (cheader_filename = "libtracker-common/tracker-type-utils.h")]
+ public int string_to_date (string date_string);
+}
+
diff --git a/src/libtracker-data/.gitignore b/src/libtracker-data/.gitignore
new file mode 100644
index 0000000..0275713
--- /dev/null
+++ b/src/libtracker-data/.gitignore
@@ -0,0 +1,2 @@
+tracker-sparql-query.c
+tracker-sparql-query.h
diff --git a/src/libtracker-data/Makefile.am b/src/libtracker-data/Makefile.am
index caf61ad..4318784 100644
--- a/src/libtracker-data/Makefile.am
+++ b/src/libtracker-data/Makefile.am
@@ -10,11 +10,17 @@ INCLUDES = \
$(DBUS_CFLAGS) \
$(UUID_CFLAGS) \
$(RAPTOR_CFLAGS) \
+ -I$(top_srcdir)/src/rasqal \
$(GCOV_CFLAGS)
+BUILT_SOURCES = libtracker-data.vala.stamp
+
libtracker_datadir = $(libdir)/tracker-$(TRACKER_API_VERSION)
libtracker_data_LTLIBRARIES = libtracker-data.la
+libtracker_data_la_VALASOURCES = \
+ tracker-sparql-query.vala
+
libtracker_data_la_SOURCES = \
tracker-data-backup.c \
tracker-data-manager.c \
@@ -22,7 +28,9 @@ libtracker_data_la_SOURCES = \
tracker-data-search.c \
tracker-data-update.c \
tracker-query-tree.c \
- tracker-turtle.c
+ libtracker-data.vala.stamp \
+ tracker-turtle.c \
+ $(libtracker_data_la_VALASOURCES:.vala=.c)
noinst_HEADERS = \
tracker-data-backup.h \
@@ -31,8 +39,13 @@ noinst_HEADERS = \
tracker-data-search.h \
tracker-data-update.h \
tracker-query-tree.h \
+ tracker-sparql-query.h \
tracker-turtle.h
+libtracker-data.vala.stamp: $(libtracker_data_la_VALASOURCES)
+ $(VALAC) -C -H tracker-sparql-query.h ../rasqal/rasqal.vapi ../libtracker-common/libtracker-common.vapi libtracker-data.vapi ../libtracker-db/libtracker-db.vapi $^
+ touch $@
+
libtracker_data_la_LDFLAGS = \
-version-info $(LT_CURRENT):$(LT_REVISION):$(LT_AGE)
@@ -43,6 +56,11 @@ libtracker_data_la_LIBADD = \
$(GLIB2_LIBS) \
$(UUID_LIBS) \
$(RAPTOR_LIBS) \
+ $(top_builddir)/src/rasqal/librasqal.la \
$(GCOV_LIBS) \
-lz
+EXTRA_DIST = $(libtracker_data_la_VALASOURCES) \
+ libtracker-data.vala.stamp \
+ libtracker-data.vapi
+
diff --git a/src/libtracker-data/libtracker-data.vapi b/src/libtracker-data/libtracker-data.vapi
new file mode 100644
index 0000000..db0aba8
--- /dev/null
+++ b/src/libtracker-data/libtracker-data.vapi
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2009, Nokia
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU 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 {
+ [CCode (cheader_filename = "libtracker-data/tracker-data-query.h,libtracker-data/tracker-data-search.h,libtracker-data/tracker-data-update.h")]
+ namespace Data {
+ public int query_resource_id (string uri);
+ public void begin_transaction ();
+ public void commit_transaction ();
+ public void delete_statement (string subject, string predicate, string object);
+ public void insert_statement (string subject, string predicate, string object);
+ public int[] search_get_matches (string search_string);
+ }
+}
+
diff --git a/src/libtracker-data/tracker-data-query.c b/src/libtracker-data/tracker-data-query.c
index 748126c..7f08bc8 100644
--- a/src/libtracker-data/tracker-data-query.c
+++ b/src/libtracker-data/tracker-data-query.c
@@ -40,6 +40,7 @@
#include "tracker-data-manager.h"
#include "tracker-data-query.h"
+#include "tracker-sparql-query.h"
static gchar *
get_string_for_value (GValue *value)
@@ -429,10 +430,17 @@ TrackerDBResultSet *
tracker_data_query_sparql (const gchar *query,
GError **error)
{
+ TrackerSparqlQuery *sparql_query;
+ TrackerDBResultSet *result_set;
+
g_return_val_if_fail (query != NULL, NULL);
- /* TODO */
+ sparql_query = tracker_sparql_query_new (query);
+
+ result_set = tracker_sparql_query_execute (sparql_query, error);
- return NULL;
+ g_object_unref (sparql_query);
+
+ return result_set;
}
diff --git a/src/libtracker-data/tracker-data-update.c b/src/libtracker-data/tracker-data-update.c
index 6e9e3b2..7c4e8c8 100644
--- a/src/libtracker-data/tracker-data-update.c
+++ b/src/libtracker-data/tracker-data-update.c
@@ -37,6 +37,7 @@
#include "tracker-data-manager.h"
#include "tracker-data-update.h"
#include "tracker-data-query.h"
+#include "tracker-sparql-query.h"
#define RDF_PREFIX TRACKER_RDF_PREFIX
#define RDFS_PREFIX TRACKER_RDFS_PREFIX
@@ -1572,8 +1573,14 @@ void
tracker_data_update_sparql (const gchar *update,
GError **error)
{
+ TrackerSparqlQuery *sparql_query;
+
g_return_if_fail (update != NULL);
- /* TODO */
+ sparql_query = tracker_sparql_query_new_update (update);
+
+ tracker_sparql_query_execute (sparql_query, error);
+
+ g_object_unref (sparql_query);
}
diff --git a/src/libtracker-data/tracker-sparql-query.vala b/src/libtracker-data/tracker-sparql-query.vala
new file mode 100644
index 0000000..b9ba507
--- /dev/null
+++ b/src/libtracker-data/tracker-sparql-query.vala
@@ -0,0 +1,1168 @@
+/*
+ * Copyright (C) 2008-2009, Nokia
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU 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.
+ */
+
+public errordomain Tracker.SparqlError {
+ PARSE,
+ UNKNOWN_CLASS,
+ UNKNOWN_PROPERTY,
+ TYPE,
+ INTERNAL
+}
+
+public class Tracker.SparqlQuery : Object {
+ // Represents a SQL table
+ class DataTable : Object {
+ public string sql_db_tablename; // as in db schema
+ public string sql_query_tablename; // temp. name, generated
+ public PredicateVariable predicate_variable;
+ }
+
+ abstract class DataBinding : Object {
+ public bool is_uri;
+ public bool is_boolean;
+ public bool is_datetime;
+ public DataTable table;
+ public string sql_db_column_name;
+ }
+
+ // Represents a mapping of a SPARQL literal to a SQL table and column
+ class LiteralBinding : DataBinding {
+ public bool is_fts_match;
+ public string literal;
+ public Rasqal.Literal.Type literal_type;
+ }
+
+ // Represents a mapping of a SPARQL variable to a SQL table and column
+ class VariableBinding : DataBinding {
+ public string variable;
+ // Specified whether SQL column may contain NULL entries
+ public bool maybe_null;
+ }
+
+ class VariableBindingList : Object {
+ public List<VariableBinding> list;
+ }
+
+ // Represents a variable used as a predicate
+ class PredicateVariable : Object {
+ public string? subject;
+ public string? object;
+
+ public Class? domain;
+
+ public string get_sql_query () throws Error {
+ 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 \"rdfs: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 = Ontology.get_class_by_uri (value.get_string ());
+
+ foreach (Property prop in Ontology.get_properties ()) {
+ if (prop.domain == domain) {
+ if (first) {
+ first = false;
+ } else {
+ sql.append (" UNION ");
+ }
+ sql.append_printf ("SELECT ID, (SELECT ID FROM \"rdfs:Resource\" WHERE Uri = '%s') AS \"predicate\", ", prop.uri);
+
+ if (prop.data_type == PropertyType.RESOURCE) {
+ sql.append_printf ("(SELECT Uri FROM \"rdfs:Resource\" WHERE ID = \"%s\")", prop.name);
+ } else if (prop.data_type == PropertyType.INTEGER || prop.data_type == PropertyType.DOUBLE) {
+ sql.append_printf ("CAST (\"%s\" AS TEXT)", prop.name);
+ } else if (prop.data_type == PropertyType.BOOLEAN) {
+ sql.append_printf ("CASE \"%s\" WHEN 1 THEN 'true' WHEN 0 THEN 'false' ELSE NULL END", prop.name);
+ } else if (prop.data_type == PropertyType.DATETIME) {
+ sql.append_printf ("strftime (\"%%Y-%%m-%%dT%%H:%%M:%%S\", \"%s\", \"unixepoch\")", prop.name);
+ } else {
+ sql.append_printf ("\"%s\"", prop.name);
+ }
+
+ sql.append (" AS \"object\" FROM ");
+ if (prop.multiple_values) {
+ sql.append_printf ("\"%s_%s\"", prop.domain.name, prop.name);
+ } else {
+ sql.append_printf ("\"%s\"", prop.domain.name);
+ }
+
+ sql.append_printf (" WHERE ID = %d", subject_id);
+ }
+ }
+ } 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 \"rdfs: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 = Ontology.get_class_by_uri (value.get_string ());
+
+ foreach (Property prop in Ontology.get_properties ()) {
+ if (prop.range == range) {
+ if (first) {
+ first = false;
+ } else {
+ sql.append (" UNION ");
+ }
+ sql.append_printf ("SELECT ID, (SELECT ID FROM \"rdfs:Resource\" WHERE Uri = '%s') AS \"predicate\", ", prop.uri);
+
+ if (prop.data_type == PropertyType.RESOURCE) {
+ sql.append_printf ("(SELECT Uri FROM \"rdfs:Resource\" WHERE ID = \"%s\")", prop.name);
+ } else if (prop.data_type == PropertyType.INTEGER || prop.data_type == PropertyType.DOUBLE) {
+ sql.append_printf ("CAST (\"%s\" AS TEXT)", prop.name);
+ } else if (prop.data_type == PropertyType.BOOLEAN) {
+ sql.append_printf ("CASE \"%s\" WHEN 1 THEN 'true' WHEN 0 THEN 'false' ELSE NULL END", prop.name);
+ } else if (prop.data_type == PropertyType.DATETIME) {
+ sql.append_printf ("strftime (\"%%Y-%%m-%%dT%%H:%%M:%%S\", \"%s\", \"unixepoch\")", prop.name);
+ } else {
+ sql.append_printf ("\"%s\"", prop.name);
+ }
+
+ sql.append (" AS \"object\" FROM ");
+ if (prop.multiple_values) {
+ sql.append_printf ("\"%s_%s\"", prop.domain.name, prop.name);
+ } else {
+ sql.append_printf ("\"%s\"", prop.domain.name);
+ }
+ }
+ }
+ } while (result_set.iter_next ());
+ }
+ } else if (domain != null) {
+ // any subject, predicates limited to a specific domain
+ bool first = true;
+ foreach (Property prop in Ontology.get_properties ()) {
+ if (prop.domain == domain) {
+ if (first) {
+ first = false;
+ } else {
+ sql.append (" UNION ");
+ }
+ sql.append_printf ("SELECT ID, (SELECT ID FROM \"rdfs:Resource\" WHERE Uri = '%s') AS \"predicate\", ", prop.uri);
+
+ if (prop.data_type == PropertyType.RESOURCE) {
+ sql.append_printf ("(SELECT Uri FROM \"rdfs:Resource\" WHERE ID = \"%s\")", prop.name);
+ } else if (prop.data_type == PropertyType.INTEGER || prop.data_type == PropertyType.DOUBLE) {
+ sql.append_printf ("CAST (\"%s\" AS TEXT)", prop.name);
+ } else if (prop.data_type == PropertyType.BOOLEAN) {
+ sql.append_printf ("CASE \"%s\" WHEN 1 THEN 'true' WHEN 0 THEN 'false' ELSE NULL END", prop.name);
+ } else if (prop.data_type == PropertyType.DATETIME) {
+ sql.append_printf ("strftime (\"%%Y-%%m-%%dT%%H:%%M:%%S\", \"%s\", \"unixepoch\")", prop.name);
+ } else {
+ sql.append_printf ("\"%s\"", prop.name);
+ }
+
+ sql.append (" AS \"object\" FROM ");
+ if (prop.multiple_values) {
+ sql.append_printf ("\"%s_%s\"", prop.domain.name, prop.name);
+ } else {
+ sql.append_printf ("\"%s\"", prop.domain.name);
+ }
+ }
+ }
+ } else {
+ // UNION over all properties would exceed SQLite limits
+ throw new SparqlError.INTERNAL ("Unrestricted predicate variables not supported");
+ }
+ return sql.str;
+ }
+ }
+
+ string query_string;
+ bool update_extensions;
+
+ StringBuilder pattern_sql = new StringBuilder ();
+
+ // All SQL tables
+ List<DataTable> tables;
+ HashTable<string,DataTable> table_map;
+
+ // All SPARQL literals
+ List<LiteralBinding> bindings;
+ List<LiteralBinding> pattern_bindings;
+
+ // All SPARQL variables
+ HashTable<string,VariableBinding> var_map = new HashTable<string,VariableBinding>.full (str_hash, str_equal, g_free, g_object_unref);
+ List<string> pattern_variables;
+ HashTable<string,VariableBindingList> pattern_var_map;
+
+ // Variables used as predicates
+ HashTable<string,PredicateVariable> predicate_variable_map = new HashTable<string,VariableBinding>.full (str_hash, str_equal, g_free, g_object_unref);
+
+ int counter;
+
+ int bnodeid = 0;
+ // base UUID used for blank nodes
+ uchar[] base_uuid;
+
+ string error_message;
+
+ public SparqlQuery (string query) {
+ this.query_string = query;
+ }
+
+ public SparqlQuery.update (string query) {
+ this (query);
+ this.update_extensions = true;
+ }
+
+ string get_sql_for_literal (Rasqal.Literal literal) {
+ assert (literal.type == Rasqal.Literal.Type.VARIABLE);
+
+ string variable_name = literal.as_variable ().name;
+
+ return "\"%s\"".printf (variable_name);
+ }
+
+ string get_sql_for_expression (Rasqal.Expression expr) {
+ if (expr.op == Rasqal.Op.COUNT) {
+ return "COUNT(%s)".printf (get_sql_for_expression (expr.arg1));
+ } else if (expr.op == Rasqal.Op.SUM) {
+ return "SUM(%s)".printf (get_sql_for_expression (expr.arg1));
+ } else if (expr.op == Rasqal.Op.AVG) {
+ return "AVG(%s)".printf (get_sql_for_expression (expr.arg1));
+ } else if (expr.op == Rasqal.Op.MIN) {
+ return "MIN(%s)".printf (get_sql_for_expression (expr.arg1));
+ } else if (expr.op == Rasqal.Op.MAX) {
+ return "MAX(%s)".printf (get_sql_for_expression (expr.arg1));
+ } else if (expr.op == Rasqal.Op.VARSTAR) {
+ return "*";
+ } else if (expr.op == Rasqal.Op.LITERAL) {
+ return get_sql_for_literal (expr.literal);
+ }
+ return "NULL";
+ }
+
+ string generate_bnodeid_handler (Rasqal.Query? query, string? user_bnodeid) {
+ // user_bnodeid is NULL for anonymous nodes
+ if (user_bnodeid == null) {
+ return ":%d".printf (++bnodeid);
+ } else {
+ var checksum = new Checksum (ChecksumType.SHA1);
+ // base UUID, unique per file
+ checksum.update (base_uuid, 16);
+ // node ID
+ checksum.update ((uchar[]) user_bnodeid, -1);
+
+ string sha1 = checksum.get_string ();
+
+ // generate name based uuid
+ return "urn:uuid:%.8s-%.4s-%.4s-%.4s-%.12s".printf (
+ sha1, sha1.offset (8), sha1.offset (12), sha1.offset (16), sha1.offset (20));
+ }
+ }
+
+ void error_handler (Raptor.Locator? locator, string message) {
+ if (error_message == null) {
+ // return first, not last, error message
+ error_message = message;
+ }
+ }
+
+ public DBResultSet? execute () throws Error {
+ var world = new Rasqal.World ();
+ world.open ();
+
+ // use LAQRS - extension to SPARQL - to support aggregation
+ var query = new Rasqal.Query (world, "laqrs", null);
+
+ foreach (Namespace ns in Ontology.get_namespaces ()) {
+ query.add_prefix (new Rasqal.Prefix (world, ns.prefix, ns.uri));
+ }
+
+ query.declare_prefixes ();
+
+ base_uuid = new uchar[16];
+ uuid_generate (base_uuid);
+ query.set_generate_bnodeid_handler (generate_bnodeid_handler);
+
+ query.set_warning_handler (error_handler);
+ query.set_error_handler (error_handler);
+ query.set_fatal_error_handler (error_handler);
+
+ query.prepare (this.query_string, null);
+ if (error_message != null) {
+ throw new SparqlError.PARSE (error_message);
+ }
+
+ if (!update_extensions) {
+ if (query.get_verb () == Rasqal.QueryVerb.SELECT) {
+ return execute_select (query);
+ } else if (query.get_verb () == Rasqal.QueryVerb.CONSTRUCT) {
+ throw new SparqlError.INTERNAL ("CONSTRUCT is not supported");
+ } else if (query.get_verb () == Rasqal.QueryVerb.DESCRIBE) {
+ throw new SparqlError.INTERNAL ("DESCRIBE is not supported");
+ } else if (query.get_verb () == Rasqal.QueryVerb.ASK) {
+ throw new SparqlError.INTERNAL ("ASK is not supported");
+ } else {
+ throw new SparqlError.PARSE ("DELETE and INSERT are not supported in query mode");
+ }
+ } else {
+ if (query.get_verb () == Rasqal.QueryVerb.INSERT) {
+ execute_insert (query);
+ return null;
+ } else if (query.get_verb () == Rasqal.QueryVerb.DELETE) {
+ execute_delete (query);
+ return null;
+ } else {
+ throw new SparqlError.PARSE ("SELECT, CONSTRUCT, DESCRIBE, and ASK are not supported in update mode");
+ }
+ }
+ }
+
+ string get_sql_for_variable (string variable_name) {
+ var binding = var_map.lookup (variable_name);
+ assert (binding != null);
+ if (binding.is_uri) {
+ return "(SELECT Uri FROM \"rdfs:Resource\" WHERE ID = \"%s\")".printf (variable_name);
+ } else if (binding.is_boolean) {
+ return "(CASE \"%s\" WHEN 1 THEN 'true' WHEN 0 THEN 'false' ELSE NULL END)".printf (variable_name);
+ } else if (binding.is_datetime) {
+ return "strftime (\"%%Y-%%m-%%dT%%H:%%M:%%S\", \"%s\", \"unixepoch\")".printf (variable_name);
+ } else {
+ return "\"%s\"".printf (variable_name);
+ }
+ }
+
+ DBResultSet? exec_sql (string sql) throws Error {
+ var iface = DBManager.get_db_interface ();
+ var stmt = iface.create_statement ("%s", sql);
+
+ // set literals specified in query
+ int i = 0;
+ foreach (LiteralBinding binding in bindings) {
+ if (binding.is_boolean) {
+ if (binding.literal == "true" || binding.literal == "1") {
+ stmt.bind_int (i, 1);
+ } else if (binding.literal == "false" || binding.literal == "0") {
+ stmt.bind_int (i, 0);
+ } else {
+ throw new SparqlError.TYPE ("`%s' is not a valid boolean".printf (binding.literal));
+ }
+ } else if (binding.is_datetime) {
+ stmt.bind_int (i, string_to_date (binding.literal));
+ } else if (binding.literal_type == Rasqal.Literal.Type.INTEGER) {
+ stmt.bind_int (i, binding.literal.to_int ());
+ } else {
+ stmt.bind_text (i, binding.literal);
+ }
+ i++;
+ }
+
+ return stmt.execute ();
+ }
+
+ DBResultSet? execute_select (Rasqal.Query query) throws Error {
+ // SELECT query
+
+ // process WHERE clause
+ visit_graph_pattern (query.get_query_graph_pattern ());
+
+ // build SQL
+ var sql = new StringBuilder ();
+
+ sql.append ("SELECT ");
+ if (query.get_distinct ()) {
+ sql.append ("DISTINCT ");
+ }
+ bool first = true;
+ for (int var_idx = 0; true; var_idx++) {
+ weak Rasqal.Variable variable = query.get_variable (var_idx);
+ if (variable == null) {
+ break;
+ }
+
+ if (!first) {
+ sql.append (", ");
+ } else {
+ first = false;
+ }
+
+ if (variable.expression != null) {
+ // LAQRS aggregate expression
+ sql.append (get_sql_for_expression (variable.expression));
+ } else {
+ sql.append (get_sql_for_variable (variable.name));
+ }
+ }
+
+ // select from results of WHERE clause
+ sql.append (" FROM (");
+ sql.append (pattern_sql.str);
+ sql.append (")");
+
+ // GROUP BY (SPARQL extension, LAQRS)
+ first = true;
+ for (int group_idx = 0; true; group_idx++) {
+ weak Rasqal.Expression group = query.get_group_condition (group_idx);
+ if (group == null) {
+ break;
+ }
+
+ if (!first) {
+ sql.append (", ");
+ } else {
+ sql.append (" GROUP BY ");
+ first = false;
+ }
+ assert (group.op == Rasqal.Op.GROUP_COND_ASC || group.op == Rasqal.Op.GROUP_COND_DESC);
+ assert (group.arg1.op == Rasqal.Op.LITERAL);
+ assert (group.arg1.literal.type == Rasqal.Literal.Type.VARIABLE);
+ string variable_name = group.arg1.literal.as_variable ().name;
+
+ sql.append (get_sql_for_variable (variable_name));
+
+ if (group.op == Rasqal.Op.GROUP_COND_DESC) {
+ sql.append (" DESC");
+ }
+ }
+
+ // ORDER BY
+ first = true;
+ for (int order_idx = 0; true; order_idx++) {
+ weak Rasqal.Expression order = query.get_order_condition (order_idx);
+ if (order == null) {
+ break;
+ }
+
+ if (!first) {
+ sql.append (", ");
+ } else {
+ sql.append (" ORDER BY ");
+ first = false;
+ }
+ assert (order.op == Rasqal.Op.ORDER_COND_ASC || order.op == Rasqal.Op.ORDER_COND_DESC);
+ assert (order.arg1.op == Rasqal.Op.LITERAL);
+ assert (order.arg1.literal.type == Rasqal.Literal.Type.VARIABLE);
+ string variable_name = order.arg1.literal.as_variable ().name;
+
+ sql.append (get_sql_for_variable (variable_name));
+
+ if (order.op == Rasqal.Op.ORDER_COND_DESC) {
+ sql.append (" DESC");
+ }
+ }
+
+ // LIMIT and OFFSET
+ if (query.get_limit () >= 0) {
+ sql.append_printf (" LIMIT %d", query.get_limit ());
+ if (query.get_offset () >= 0) {
+ sql.append_printf (" OFFSET %d", query.get_offset ());
+ }
+ }
+
+ return exec_sql (sql.str);
+ }
+
+ void execute_insert (Rasqal.Query query) throws Error {
+ execute_update (query, false);
+ }
+
+ void execute_delete (Rasqal.Query query) throws Error {
+ execute_update (query, true);
+ }
+
+ void execute_update (Rasqal.Query query, bool delete_statements) throws Error {
+ // INSERT or DELETE
+
+ var sql = new StringBuilder ();
+
+ // process WHERE clause
+ if (query.get_query_graph_pattern () != null) {
+ visit_graph_pattern (query.get_query_graph_pattern ());
+
+ // build SQL
+ sql.append ("SELECT ");
+ bool first = true;
+ foreach (VariableBinding binding in var_map.get_values ()) {
+ if (!first) {
+ sql.append (", ");
+ } else {
+ first = false;
+ }
+
+ sql.append (get_sql_for_variable (binding.variable));
+ }
+
+ // select from results of WHERE clause
+ sql.append (" FROM (");
+ sql.append (pattern_sql.str);
+ sql.append (")");
+ } else {
+ sql.append ("SELECT 1");
+ }
+
+ var result_set = exec_sql (sql.str);
+
+ // all updates should be committed in one transaction
+ Data.begin_transaction ();
+
+ // iterate over all solutions
+ if (result_set != null) {
+ do {
+ // get values of all variables to be bound
+ var var_value_map = new HashTable<string,string>.full (str_hash, str_equal, g_free, g_free);
+ int var_idx = 0;
+ foreach (string var_name in var_map.get_keys ()) {
+ Value value;
+ result_set._get_value (var_idx++, out value);
+ var_value_map.insert (var_name, get_string_for_value (value));
+ }
+
+ // iterate over each triple in the template
+ for (int triple_idx = 0; true; triple_idx++) {
+ weak Rasqal.Triple triple = query.get_construct_triple (triple_idx);
+ if (triple == null) {
+ break;
+ }
+
+ string subject, predicate, object;
+
+ if (triple.subject.type == Rasqal.Literal.Type.VARIABLE) {
+ subject = var_value_map.lookup (triple.subject.as_variable ().name);
+ } else {
+ subject = get_string_from_literal (triple.subject);
+ }
+
+ if (triple.predicate.type == Rasqal.Literal.Type.VARIABLE) {
+ predicate = var_value_map.lookup (triple.predicate.as_variable ().name);
+ } else {
+ predicate = get_string_from_literal (triple.predicate);
+ }
+
+ if (triple.object.type == Rasqal.Literal.Type.VARIABLE) {
+ object = var_value_map.lookup (triple.object.as_variable ().name);
+ } else {
+ object = get_string_from_literal (triple.object);
+ }
+
+ if (delete_statements) {
+ // delete triple from database
+ Data.delete_statement (subject, predicate, object);
+ } else {
+ // insert triple into database
+ Data.insert_statement (subject, predicate, object);
+ }
+ }
+ } while (result_set.iter_next ());
+ }
+
+ Data.commit_transaction ();
+ }
+
+ void visit_graph_pattern (Rasqal.GraphPattern graph_pattern) throws SparqlError {
+ bool first_where = true;
+ if (graph_pattern.get_operator () == Rasqal.GraphPattern.Operator.BASIC) {
+ tables = new List<DataTable> ();
+ table_map = new HashTable<string,DataTable>.full (str_hash, str_equal, g_free, g_object_unref);
+
+ pattern_variables = new List<string> ();
+ pattern_var_map = new HashTable<string,VariableBindingList>.full (str_hash, str_equal, g_free, g_object_unref);
+
+ pattern_bindings = new List<LiteralBinding> ();
+
+ pattern_sql.append ("SELECT ");
+
+ for (int triple_idx = 0; true; triple_idx++) {
+ weak Rasqal.Triple triple = graph_pattern.get_triple (triple_idx);
+ if (triple == null) {
+ break;
+ }
+
+ visit_triple (triple);
+ }
+
+ // remove last comma and space
+ pattern_sql.truncate (pattern_sql.len - 2);
+
+ pattern_sql.append (" FROM ");
+ bool first = true;
+ foreach (DataTable table in tables) {
+ if (!first) {
+ pattern_sql.append (", ");
+ } else {
+ first = false;
+ }
+ if (table.sql_db_tablename != null) {
+ pattern_sql.append_printf ("\"%s\"", table.sql_db_tablename);
+ } else {
+ pattern_sql.append_printf ("(%s)", table.predicate_variable.get_sql_query ());
+ }
+ pattern_sql.append_printf (" AS \"%s\"", table.sql_query_tablename);
+ }
+
+ foreach (string variable in pattern_variables) {
+ bool maybe_null = true;
+ string last_name = null;
+ foreach (VariableBinding binding in pattern_var_map.lookup (variable).list) {
+ string name = "\"%s\".\"%s\"".printf (binding.table.sql_query_tablename, binding.sql_db_column_name);
+ if (last_name != null) {
+ if (!first_where) {
+ pattern_sql.append (" AND ");
+ } else {
+ pattern_sql.append (" WHERE ");
+ first_where = false;
+ }
+ pattern_sql.append (last_name);
+ pattern_sql.append (" = ");
+ pattern_sql.append (name);
+ }
+ last_name = name;
+ if (!binding.maybe_null) {
+ maybe_null = false;
+ }
+ }
+
+ if (maybe_null) {
+ // ensure that variable is bound in case it could return NULL in SQL
+ // assuming SPARQL variable is not optional
+ if (!first_where) {
+ pattern_sql.append (" AND ");
+ } else {
+ pattern_sql.append (" WHERE ");
+ first_where = false;
+ }
+ pattern_sql.append_printf ("%s IS NOT NULL", variable);
+ }
+ }
+ foreach (LiteralBinding binding in pattern_bindings) {
+ if (!first_where) {
+ pattern_sql.append (" AND ");
+ } else {
+ pattern_sql.append (" WHERE ");
+ first_where = false;
+ }
+ pattern_sql.append ("\"");
+ pattern_sql.append (binding.table.sql_query_tablename);
+ pattern_sql.append ("\".\"");
+ pattern_sql.append (binding.sql_db_column_name);
+ pattern_sql.append ("\"");
+ if (binding.is_fts_match) {
+ pattern_sql.append (" IN (");
+
+ // include matches from fulltext search
+ first = true;
+ foreach (int match_id in Data.search_get_matches (binding.literal)) {
+ if (!first) {
+ pattern_sql.append (",");
+ } else {
+ first = false;
+ }
+ pattern_sql.append_printf ("%d", match_id);
+ }
+
+ pattern_sql.append (")");
+ } else {
+ pattern_sql.append (" = ");
+ if (binding.is_uri) {
+ pattern_sql.append ("(SELECT ID FROM \"rdfs:Resource\" WHERE Uri = ?)");
+ } else {
+ pattern_sql.append ("?");
+ }
+ }
+ }
+
+ tables = null;
+ table_map = null;
+ pattern_variables = null;
+ pattern_var_map = null;
+ pattern_bindings = null;
+ } else if (graph_pattern.get_operator () == Rasqal.GraphPattern.Operator.GROUP
+ || graph_pattern.get_operator () == Rasqal.GraphPattern.Operator.OPTIONAL) {
+ pattern_sql.append ("SELECT * FROM (");
+ long subgraph_start = pattern_sql.len;
+ int subgraph_idx = 0;
+ for (int pattern_idx = 0; true; pattern_idx++) {
+ weak Rasqal.GraphPattern sub_graph_pattern = graph_pattern.get_sub_graph_pattern (pattern_idx);
+ if (sub_graph_pattern == null) {
+ break;
+ }
+
+ if (sub_graph_pattern.get_operator () == Rasqal.GraphPattern.Operator.FILTER) {
+ // ignore filters, processed later
+ continue;
+ }
+
+ if (subgraph_idx > 1) {
+ // additional (SELECT * FROM ...) necessary
+ // when using more than two subgraphs to
+ // work around SQLite bug with NATURAL JOINs
+ pattern_sql.insert (subgraph_start, "(SELECT * FROM ");
+ pattern_sql.append (")");
+ }
+
+ if (subgraph_idx > 0) {
+ if (sub_graph_pattern.get_operator () == Rasqal.GraphPattern.Operator.OPTIONAL) {
+ pattern_sql.append (" NATURAL LEFT JOIN ");
+ } else {
+ pattern_sql.append (" NATURAL CROSS JOIN ");
+ }
+ }
+ pattern_sql.append ("(");
+ visit_graph_pattern (sub_graph_pattern);
+ pattern_sql.append (")");
+
+ // differs from pattern_idx due to filters
+ subgraph_idx++;
+ }
+ pattern_sql.append (")");
+ } else if (graph_pattern.get_operator () == Rasqal.GraphPattern.Operator.UNION) {
+ for (int pattern_idx = 0; true; pattern_idx++) {
+ weak Rasqal.GraphPattern sub_graph_pattern = graph_pattern.get_sub_graph_pattern (pattern_idx);
+ if (sub_graph_pattern == null) {
+ break;
+ }
+
+ if (sub_graph_pattern.get_operator () == Rasqal.GraphPattern.Operator.FILTER) {
+ // ignore filters, processed later
+ continue;
+ }
+
+ if (pattern_idx > 0) {
+ pattern_sql.append (" UNION ");
+ }
+ visit_graph_pattern (sub_graph_pattern);
+ }
+ }
+
+ // process filters
+ for (int pattern_idx = 0; true; pattern_idx++) {
+ weak Rasqal.GraphPattern sub_graph_pattern = graph_pattern.get_sub_graph_pattern (pattern_idx);
+ if (sub_graph_pattern == null) {
+ break;
+ }
+
+ if (sub_graph_pattern.get_operator () != Rasqal.GraphPattern.Operator.FILTER) {
+ // ignore non-filter subgraphs
+ continue;
+ }
+
+ weak Rasqal.Expression filter = sub_graph_pattern.get_filter_expression ();
+
+ if (!first_where) {
+ pattern_sql.append (" AND ");
+ } else {
+ pattern_sql.append (" WHERE ");
+ first_where = false;
+ }
+
+ visit_filter (filter);
+ }
+ }
+
+ string get_string_from_literal (Rasqal.Literal lit, bool is_subject = false) {
+ if (lit.type == Rasqal.Literal.Type.BLANK) {
+ if (!is_subject && lit.as_string ().has_prefix (":")) {
+ // anonymous blank node, libtracker-data will
+ // generate appropriate uri
+ return lit.as_string ();
+ } else {
+ return generate_bnodeid_handler (null, lit.as_string ());
+ }
+ } else {
+ return lit.as_string ();
+ }
+ }
+
+ void visit_triple (Rasqal.Triple triple) throws SparqlError {
+ string subject;
+ if (triple.subject.type == Rasqal.Literal.Type.VARIABLE) {
+ subject = "?" + triple.subject.as_variable ().name;
+ } else {
+ subject = get_string_from_literal (triple.subject, true);
+ }
+
+ string db_table;
+ bool rdftype = false;
+ bool share_table = true;
+
+ bool newtable;
+ DataTable table;
+ Property prop = null;
+
+ if (triple.predicate.type == Rasqal.Literal.Type.URI) {
+ prop = Ontology.get_property_by_uri (triple.predicate.as_string ());
+
+ if (triple.predicate.as_string () == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
+ && triple.object.type == Rasqal.Literal.Type.URI) {
+ // rdf:type query
+ rdftype = true;
+ var cl = Ontology.get_class_by_uri (triple.object.as_string ());
+ if (cl == null) {
+ throw new SparqlError.UNKNOWN_CLASS ("Unknown class `%s'".printf (triple.object.as_string ()));
+ }
+ db_table = cl.name;
+ } else if (prop == null) {
+ if (triple.predicate.as_string () == "http://www.tracker-project.org/ontologies/fts#match") {
+ // fts:match
+ db_table = "rdfs:Resource";
+ } else {
+ throw new SparqlError.UNKNOWN_PROPERTY ("Unknown property `%s'".printf (triple.predicate.as_string ()));
+ }
+ } else {
+ if (triple.predicate.as_string () == "http://www.w3.org/2000/01/rdf-schema#domain"
+ && triple.subject.type == Rasqal.Literal.Type.VARIABLE
+ && triple.object.type == Rasqal.Literal.Type.URI) {
+ // rdfs:domain
+ var domain = Ontology.get_class_by_uri (triple.object.as_string ());
+ if (domain == null) {
+ throw new SparqlError.UNKNOWN_CLASS ("Unknown class `%s'".printf (triple.object.as_string ()));
+ }
+ var pv = predicate_variable_map.lookup (triple.subject.as_variable ().name);
+ if (pv == null) {
+ pv = new PredicateVariable ();
+ predicate_variable_map.insert (triple.subject.as_variable ().name, pv);
+ }
+ pv.domain = domain;
+ }
+
+ if (prop.multiple_values) {
+ db_table = "%s_%s".printf (prop.domain.name, prop.name);
+ // we can never share the table with multiple triples
+ // for multi value properties as a property may consist of multiple rows
+ share_table = false;
+ } else {
+ db_table = prop.domain.name;
+ }
+ }
+ table = get_table (subject, db_table, share_table, out newtable);
+ } else {
+ // variable in predicate
+ newtable = true;
+ table = new DataTable ();
+ table.predicate_variable = predicate_variable_map.lookup (triple.predicate.as_variable ().name);
+ if (table.predicate_variable == null) {
+ table.predicate_variable = new PredicateVariable ();
+ predicate_variable_map.insert (triple.predicate.as_variable ().name, table.predicate_variable);
+ }
+ if (triple.subject.type == Rasqal.Literal.Type.URI) {
+ // single subject
+ table.predicate_variable.subject = subject;
+ }
+ if (triple.object.type == Rasqal.Literal.Type.URI) {
+ // single object
+ table.predicate_variable.object = get_string_from_literal (triple.object);
+ }
+ table.sql_query_tablename = triple.predicate.as_variable ().name + (++counter).to_string ();
+ tables.append (table);
+
+ // add to variable list
+ var binding = new VariableBinding ();
+ binding.is_uri = true;
+ binding.variable = triple.predicate.as_variable ().name;
+ binding.table = table;
+ binding.sql_db_column_name = "predicate";
+ var binding_list = pattern_var_map.lookup (binding.variable);
+ if (binding_list == null) {
+ binding_list = new VariableBindingList ();
+ pattern_variables.append (binding.variable);
+ pattern_var_map.insert (binding.variable, binding_list);
+
+ pattern_sql.append_printf ("\"%s\".\"%s\" AS \"%s\", ",
+ binding.table.sql_query_tablename,
+ binding.sql_db_column_name,
+ binding.variable);
+ }
+ binding_list.list.append (binding);
+ if (var_map.lookup (binding.variable) == null) {
+ var_map.insert (binding.variable, binding);
+ }
+ }
+
+ if (newtable) {
+ if (triple.subject.type == Rasqal.Literal.Type.VARIABLE) {
+ var binding = new VariableBinding ();
+ binding.is_uri = true;
+ binding.variable = triple.subject.as_variable ().name;
+ binding.table = table;
+ binding.sql_db_column_name = "ID";
+ var binding_list = pattern_var_map.lookup (binding.variable);
+ if (binding_list == null) {
+ binding_list = new VariableBindingList ();
+ pattern_variables.append (binding.variable);
+ pattern_var_map.insert (binding.variable, binding_list);
+
+ pattern_sql.append_printf ("\"%s\".\"%s\" AS \"%s\", ",
+ binding.table.sql_query_tablename,
+ binding.sql_db_column_name,
+ binding.variable);
+ }
+ binding_list.list.append (binding);
+ if (var_map.lookup (binding.variable) == null) {
+ var_map.insert (binding.variable, binding);
+ }
+ } else {
+ var binding = new LiteralBinding ();
+ binding.is_uri = true;
+ binding.literal = get_string_from_literal (triple.subject);
+ binding.literal_type = triple.subject.type;
+ binding.table = table;
+ binding.sql_db_column_name = "ID";
+ pattern_bindings.append (binding);
+ bindings.append (binding);
+ }
+ }
+
+ if (!rdftype) {
+ if (triple.object.type == Rasqal.Literal.Type.VARIABLE) {
+ var binding = new VariableBinding ();
+ binding.variable = triple.object.as_variable ().name;
+ binding.table = table;
+ if (prop != null) {
+ if (prop.data_type == PropertyType.RESOURCE) {
+ binding.is_uri = true;
+ } else if (prop.data_type == PropertyType.BOOLEAN) {
+ binding.is_boolean = true;
+ } else if (prop.data_type == PropertyType.DATE) {
+ binding.is_datetime = true;
+ } else if (prop.data_type == PropertyType.DATETIME) {
+ binding.is_datetime = true;
+ }
+ 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;
+ }
+ } else {
+ // variable as predicate
+ binding.sql_db_column_name = "object";
+ binding.maybe_null = true;
+ }
+
+ var binding_list = pattern_var_map.lookup (binding.variable);
+ if (binding_list == null) {
+ binding_list = new VariableBindingList ();
+ pattern_variables.append (binding.variable);
+ pattern_var_map.insert (binding.variable, binding_list);
+
+ pattern_sql.append_printf ("\"%s\".\"%s\" AS %s, ",
+ binding.table.sql_query_tablename,
+ binding.sql_db_column_name,
+ binding.variable);
+ }
+ binding_list.list.append (binding);
+ if (var_map.lookup (binding.variable) == null) {
+ var_map.insert (binding.variable, binding);
+ }
+ } else if (triple.predicate.as_string () == "http://www.tracker-project.org/ontologies/fts#match") {
+ var binding = new LiteralBinding ();
+ binding.is_fts_match = true;
+ binding.literal = triple.object.as_string ();
+ binding.literal_type = triple.object.type;
+ binding.table = table;
+ binding.sql_db_column_name = "ID";
+ pattern_bindings.append (binding);
+ } else {
+ var binding = new LiteralBinding ();
+ binding.literal = triple.object.as_string ();
+ binding.literal_type = triple.object.type;
+ binding.table = table;
+ if (prop != null) {
+ if (prop.data_type == PropertyType.RESOURCE) {
+ binding.is_uri = true;
+ binding.literal = get_string_from_literal (triple.object);
+ } else if (prop.data_type == PropertyType.BOOLEAN) {
+ binding.is_boolean = true;
+ } else if (prop.data_type == PropertyType.DATE) {
+ binding.is_datetime = true;
+ } else if (prop.data_type == PropertyType.DATETIME) {
+ binding.is_datetime = true;
+ }
+ binding.sql_db_column_name = prop.name;
+ } else {
+ // variable as predicate
+ binding.sql_db_column_name = "object";
+ }
+ pattern_bindings.append (binding);
+ bindings.append (binding);
+ }
+ }
+ }
+
+ 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 = 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 ();
+ tables.append (table);
+ table_map.insert (tablestring, table);
+ }
+ return table;
+ }
+
+ string sql_operator (Rasqal.Op op) {
+ switch (op) {
+ case Rasqal.Op.AND: return " AND ";
+ case Rasqal.Op.OR: return " OR ";
+ case Rasqal.Op.EQ: return " = ";
+ case Rasqal.Op.NEQ: return " <> ";
+ case Rasqal.Op.LT: return " < ";
+ case Rasqal.Op.GT: return " > ";
+ case Rasqal.Op.LE: return " <= ";
+ case Rasqal.Op.GE: return " >= ";
+ case Rasqal.Op.PLUS: return " + ";
+ case Rasqal.Op.MINUS: return " - ";
+ case Rasqal.Op.STAR: return " * ";
+ case Rasqal.Op.SLASH: return " / ";
+ case Rasqal.Op.REM: return " % ";
+ case Rasqal.Op.STR_EQ: return " = ";
+ case Rasqal.Op.STR_NEQ: return " <> ";
+ }
+ return "";
+ }
+
+ bool is_datetime_variable (Rasqal.Expression expr) {
+ if (expr.op == Rasqal.Op.LITERAL) {
+ if (expr.literal.type == Rasqal.Literal.Type.VARIABLE) {
+ string variable_name = expr.literal.as_variable ().name;
+ var binding = var_map.lookup (variable_name);
+ if (binding != null && binding.is_datetime) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ void visit_filter (Rasqal.Expression expr, bool is_datetime = false) {
+ switch (expr.op) {
+ case Rasqal.Op.AND:
+ case Rasqal.Op.OR:
+ case Rasqal.Op.EQ:
+ case Rasqal.Op.NEQ:
+ case Rasqal.Op.LT:
+ case Rasqal.Op.GT:
+ case Rasqal.Op.LE:
+ case Rasqal.Op.GE:
+ case Rasqal.Op.PLUS:
+ case Rasqal.Op.MINUS:
+ case Rasqal.Op.STAR:
+ case Rasqal.Op.SLASH:
+ case Rasqal.Op.REM:
+ case Rasqal.Op.STR_EQ:
+ case Rasqal.Op.STR_NEQ:
+ pattern_sql.append ("(");
+ visit_filter (expr.arg1, is_datetime_variable (expr.arg2));
+ pattern_sql.append (sql_operator (expr.op));
+ visit_filter (expr.arg2, is_datetime_variable (expr.arg1));
+ pattern_sql.append (")");
+ break;
+ case Rasqal.Op.UMINUS:
+ pattern_sql.append ("-(");
+ visit_filter (expr.arg1);
+ pattern_sql.append (")");
+ break;
+ case Rasqal.Op.BANG:
+ pattern_sql.append ("NOT (");
+ visit_filter (expr.arg1);
+ pattern_sql.append (")");
+ break;
+ case Rasqal.Op.LITERAL:
+ if (expr.literal.type == Rasqal.Literal.Type.VARIABLE) {
+ string variable_name = expr.literal.as_variable ().name;
+ pattern_sql.append (variable_name);
+ } else {
+ if (expr.literal.type == Rasqal.Literal.Type.URI) {
+ pattern_sql.append ("(SELECT ID FROM \"rdfs:Resource\" WHERE Uri = ?)");
+ } else {
+ pattern_sql.append ("?");
+ }
+
+ var binding = new LiteralBinding ();
+ binding.literal = expr.literal.as_string ();
+ binding.literal_type = expr.literal.type;
+ binding.is_datetime = is_datetime;
+ bindings.append (binding);
+ }
+ break;
+ case Rasqal.Op.BOUND:
+ pattern_sql.append ("(");
+ visit_filter (expr.arg1);
+ pattern_sql.append (") IS NOT NULL");
+ break;
+ case Rasqal.Op.REGEX:
+ pattern_sql.append ("SparqlRegex(");
+ visit_filter (expr.arg1);
+ pattern_sql.append (", ");
+ visit_filter (expr.arg2);
+ pattern_sql.append (", ");
+ if (expr.arg3 != null) {
+ visit_filter (expr.arg3);
+ } else {
+ pattern_sql.append ("''");
+ }
+ pattern_sql.append (")");
+ break;
+ }
+ }
+
+ static string? get_string_for_value (Value value)
+ {
+ switch (value.type ()) {
+ case typeof (int):
+ return value.get_int ().to_string ();
+ case typeof (double):
+ return value.get_double ().to_string ();
+ case typeof (string):
+ return value.get_string ();
+ default:
+ return null;
+ }
+ }
+
+ [CCode (cname = "uuid_generate")]
+ public extern static void uuid_generate ([CCode (array_length = false)] uchar[] uuid);
+}
+
diff --git a/src/libtracker-db/Makefile.am b/src/libtracker-db/Makefile.am
index a6203bb..4cfb835 100644
--- a/src/libtracker-db/Makefile.am
+++ b/src/libtracker-db/Makefile.am
@@ -47,4 +47,7 @@ libtracker_db_la_LIBADD = \
$(DBUS_LIBS) \
$(GCOV_LIBS) \
$(GLIB2_LIBS) \
- -lz
\ No newline at end of file
+ -lz
+
+EXTRA_DIST = libtracker-db.vapi
+
diff --git a/src/libtracker-db/libtracker-db.vapi b/src/libtracker-db/libtracker-db.vapi
new file mode 100644
index 0000000..8936e0d
--- /dev/null
+++ b/src/libtracker-db/libtracker-db.vapi
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008-2009, Nokia
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU 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 {
+ [CCode (cheader_filename = "libtracker-db/tracker-db-manager.h")]
+ public enum DB {
+ UNKNOWN,
+ COMMON,
+ CACHE,
+ METADATA,
+ CONTENTS
+ }
+
+ [CCode (cheader_filename = "libtracker-db/tracker-db-interface.h")]
+ public interface DBInterface : GLib.Object {
+ [PrintfFormat]
+ public abstract DBStatement create_statement (string query, ...);
+ }
+
+ [CCode (cheader_filename = "libtracker-db/tracker-db-manager.h")]
+ namespace DBManager {
+ public weak DBInterface get_db_interface ();
+ }
+
+ [CCode (cheader_filename = "libtracker-db/tracker-db-interface.h")]
+ public class DBResultSet : GLib.Object {
+ public void _get_value (uint column, out GLib.Value value);
+ public bool iter_next ();
+ }
+
+ [CCode (cheader_filename = "libtracker-db/tracker-db-interface.h")]
+ public interface DBStatement : GLib.Object {
+ public abstract void bind_double (int index, double value);
+ public abstract void bind_int (int index, int value);
+ public abstract void bind_text (int index, string value);
+ public abstract DBResultSet execute () throws GLib.Error;
+ }
+}
+
diff --git a/src/libtracker/tracker.c b/src/libtracker/tracker.c
index 8f8c106..b9750bf 100644
--- a/src/libtracker/tracker.c
+++ b/src/libtracker/tracker.c
@@ -289,6 +289,26 @@ tracker_resources_load (TrackerClient *client, const char *uri, GError **error)
}
+GPtrArray *
+tracker_resources_sparql_query (TrackerClient *client, const char *query, GError **error)
+{
+ GPtrArray *table;
+
+ if (!org_freedesktop_Tracker_Resources_sparql_query (client->proxy_resources, query, &table, &*error)) {
+ return NULL;
+ }
+
+ return table;
+}
+
+
+void
+tracker_resources_sparql_update (TrackerClient *client, const char *query, GError **error)
+{
+ org_freedesktop_Tracker_Resources_sparql_update (client->proxy_resources, query, &*error);
+}
+
+
char *
tracker_search_get_snippet (TrackerClient *client, const char *uri, const char *search_text, GError **error)
{
@@ -425,6 +445,34 @@ tracker_resources_load_async (TrackerClient *client, const char *uri, TrackerVoi
void
+tracker_resources_sparql_query_async (TrackerClient *client, const char *query, TrackerGPtrArrayReply callback, gpointer user_data)
+{
+ GPtrArrayCallBackStruct *callback_struct;
+
+ callback_struct = g_new (GPtrArrayCallBackStruct, 1);
+ callback_struct->callback = callback;
+ callback_struct->data = user_data;
+
+ client->last_pending_call = org_freedesktop_Tracker_Resources_sparql_query_async (client->proxy_resources, query, tracker_GPtrArray_reply, callback_struct);
+
+}
+
+
+void
+tracker_resources_sparql_update_async (TrackerClient *client, const char *query, TrackerVoidReply callback, gpointer user_data)
+{
+ VoidCallBackStruct *callback_struct;
+
+ callback_struct = g_new (VoidCallBackStruct, 1);
+ callback_struct->callback = callback;
+ callback_struct->data = user_data;
+
+ client->last_pending_call = org_freedesktop_Tracker_Resources_sparql_update_async (client->proxy_resources, query, tracker_void_reply, callback_struct);
+
+}
+
+
+void
tracker_search_get_snippet_async (TrackerClient *client, const char *uri, const char *search_text, TrackerStringReply callback, gpointer user_data)
{
StringCallBackStruct *callback_struct;
diff --git a/src/libtracker/tracker.h b/src/libtracker/tracker.h
index 56934b3..9e9f0f9 100644
--- a/src/libtracker/tracker.h
+++ b/src/libtracker/tracker.h
@@ -70,6 +70,8 @@ void tracker_shutdown (TrackerClient *client, gboolean reindex, GError **err
void tracker_prompt_index_signals (TrackerClient *client, GError **error);
void tracker_resources_load (TrackerClient *client, const char *uri, GError **error);
+GPtrArray * tracker_resources_sparql_query (TrackerClient *client, const char *query, GError **error);
+void tracker_resources_sparql_update (TrackerClient *client, const char *query, GError **error);
char * tracker_search_get_snippet (TrackerClient *client, const char *uri, const char *search_text, GError **error);
@@ -89,6 +91,8 @@ void tracker_shutdown_async (TrackerClient *client, gboolean reindex, Track
void tracker_prompt_index_signals_async (TrackerClient *client, TrackerVoidReply callback, gpointer user_data);
void tracker_resources_load_async (TrackerClient *client, const char *uri, TrackerVoidReply callback, gpointer user_data);
+void tracker_resources_sparql_query_async (TrackerClient *client, const char *query, TrackerGPtrArrayReply callback, gpointer user_data);
+void tracker_resources_sparql_update_async (TrackerClient *client, const char *query, TrackerVoidReply callback, gpointer user_data);
void tracker_search_get_snippet_async (TrackerClient *client, const char *uri, const char *search_text, TrackerStringReply callback, gpointer user_data);
void tracker_search_suggest_async (TrackerClient *client, const char *search_text, int maxdist, TrackerStringReply callback, gpointer user_data);
diff --git a/src/rasqal/rasqal.vapi b/src/rasqal/rasqal.vapi
new file mode 100644
index 0000000..b9d5350
--- /dev/null
+++ b/src/rasqal/rasqal.vapi
@@ -0,0 +1,210 @@
+/* rasqal.vapi
+ *
+ * Copyright (C) 2008-2009 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
+ *
+ * Author:
+ * Jürg Billeter <j bitron ch>
+ */
+
+[CCode (cheader_filename = "raptor.h")]
+namespace Raptor {
+ [Compact]
+ [CCode (cname = "raptor_locator")]
+ public class Locator {
+ public weak string file;
+ public int line;
+ public int column;
+ public int byte;
+ }
+
+ [CCode (cname = "raptor_message_handler", instance_pos = 0)]
+ public delegate void MessageHandler (Locator locator, string message);
+}
+
+[CCode (cheader_filename = "rasqal.h")]
+namespace Rasqal {
+ [Compact]
+ [CCode (cname = "rasqal_graph_pattern")]
+ public class GraphPattern {
+ public enum Operator {
+ BASIC,
+ OPTIONAL,
+ UNION,
+ GROUP,
+ GRAPH,
+ FILTER
+ }
+
+ public weak Expression get_filter_expression ();
+ public Operator get_operator ();
+ public weak GraphPattern get_sub_graph_pattern (int idx);
+ public weak Triple get_triple (int idx);
+ public void print (GLib.FileStream fh);
+ }
+
+ [CCode (cname = "rasqal_op", cprefix = "RASQAL_EXPR_")]
+ public enum Op {
+ AND,
+ OR,
+ EQ,
+ NEQ,
+ LT,
+ GT,
+ LE,
+ GE,
+ UMINUS,
+ PLUS,
+ MINUS,
+ STAR,
+ SLASH,
+ REM,
+ STR_EQ,
+ STR_NEQ,
+ STR_MATCH,
+ STR_NMATCH,
+ TILDE,
+ BANG,
+ LITERAL,
+ FUNCTION,
+ BOUND,
+ STR,
+ LANG,
+ DATATYPE,
+ ISURI,
+ ISBLANK,
+ ISLITERAL,
+ CAST,
+ ORDER_COND_ASC,
+ ORDER_COND_DESC,
+ LANGMATCHES,
+ REGEX,
+ GROUP_COND_ASC,
+ GROUP_COND_DESC,
+ COUNT,
+ VARSTAR,
+ SAMETERM,
+ SUM,
+ AVG,
+ MIN,
+ MAX
+ }
+
+ [Compact]
+ [CCode (cname = "rasqal_expression", free_function = "rasqal_free_expression")]
+ public class Expression {
+ public Op op;
+ public Expression? arg1;
+ public Expression? arg2;
+ public Expression? arg3;
+ public Literal? literal;
+ }
+
+ [CCode (cname = "rasqal_generate_bnodeid_handler", instance_pos = 1.1)]
+ public delegate string GenerateBnodeidHandler (Rasqal.Query query, string? user_bnodeid);
+
+ [Compact]
+ [CCode (cname = "rasqal_literal", free_function = "rasqal_free_literal")]
+ public class Literal {
+ [CCode (cname = "rasqal_literal_type", cprefix = "RASQAL_LITERAL_")]
+ public enum Type {
+ BLANK,
+ URI,
+ STRING,
+ BOOLEAN,
+ INTEGER,
+ DOUBLE,
+ FLOAT,
+ DECIMAL,
+ DATETIME,
+ PATTERN,
+ QNAME,
+ VARIABLE
+ }
+
+ public Type type;
+
+ public weak string? as_string ();
+ public weak Variable? as_variable ();
+ }
+
+ [Compact]
+ [CCode (cname = "rasqal_prefix", free_function = "rasqal_free_prefix")]
+ public class Prefix {
+ [CCode (cname = "rasqal_new_prefix")]
+ public Prefix (World world, string# prefix, string# uri);
+ }
+
+ [Compact]
+ [CCode (cname = "rasqal_query", free_function = "rasqal_free_query")]
+ public class Query {
+ [CCode (cname = "rasqal_new_query")]
+ public Query (World world, string? name, string? uri);
+ public void add_prefix (Prefix# prefix);
+ public void declare_prefixes ();
+ public weak Triple get_construct_triple (int idx);
+ public bool get_distinct ();
+ public int get_limit ();
+ public int get_offset ();
+ public QueryVerb get_verb ();
+ public weak Expression? get_group_condition (int idx);
+ public weak Expression? get_order_condition (int idx);
+ public weak GraphPattern get_query_graph_pattern ();
+ public weak Variable? get_variable (int idx);
+ public int prepare (string? query_string, string? base_uri);
+ public void print (GLib.FileStream fh);
+ public void set_error_handler ([CCode (delegate_target_pos = 0.9)] Raptor.MessageHandler handler);
+ public void set_fatal_error_handler ([CCode (delegate_target_pos = 0.9)] Raptor.MessageHandler handler);
+ public void set_generate_bnodeid_handler ([CCode (delegate_target_pos = 0.9)] GenerateBnodeidHandler handler);
+ public void set_warning_handler ([CCode (delegate_target_pos = 0.9)] Raptor.MessageHandler handler);
+ }
+
+ public enum QueryVerb {
+ SELECT,
+ CONSTRUCT,
+ DESCRIBE,
+ ASK,
+ DELETE,
+ INSERT
+ }
+
+ [Compact]
+ [CCode (cname = "rasqal_triple", free_function = "rasqal_free_triple")]
+ public class Triple {
+ public Literal subject;
+ public Literal predicate;
+ public Literal object;
+ public Literal origin;
+
+ public void print (GLib.FileStream fh);
+ }
+
+ [Compact]
+ [CCode (cname = "rasqal_variable", free_function = "rasqal_free_variable")]
+ public class Variable {
+ public weak string? name;
+ public Expression? expression;
+ }
+
+ [Compact]
+ [CCode (cname = "rasqal_world", free_function = "rasqal_free_world")]
+ public class World {
+ [CCode (cname = "rasqal_new_world")]
+ public World ();
+ public void open ();
+ }
+}
+
diff --git a/src/tracker-indexer/tracker-indexer.c b/src/tracker-indexer/tracker-indexer.c
index 1e54281..5153325 100644
--- a/src/tracker-indexer/tracker-indexer.c
+++ b/src/tracker-indexer/tracker-indexer.c
@@ -2492,6 +2492,42 @@ tracker_indexer_delete_statement (TrackerIndexer *indexer,
tracker_dbus_request_success (request_id);
}
+void
+tracker_indexer_sparql_update (TrackerIndexer *indexer,
+ const gchar *update,
+ DBusGMethodInvocation *context,
+ GError **error)
+{
+ GError *actual_error = NULL;
+ guint request_id;
+
+ request_id = tracker_dbus_get_next_request_id ();
+
+ tracker_dbus_async_return_if_fail (update != NULL, context);
+
+ tracker_dbus_request_new (request_id,
+ "DBus request for SPARQL Update, "
+ "update:'%s'",
+ update);
+
+ schedule_flush (indexer, TRUE);
+
+ tracker_data_update_sparql (update, &actual_error);
+
+ if (actual_error) {
+ tracker_dbus_request_failed (request_id,
+ &actual_error,
+ NULL);
+ dbus_g_method_return_error (context, actual_error);
+ g_error_free (actual_error);
+ return;
+ }
+
+ dbus_g_method_return (context);
+
+ tracker_dbus_request_success (request_id);
+}
+
static void
restore_backup_cb (const gchar *subject,
const gchar *predicate,
diff --git a/src/tracker-indexer/tracker-indexer.h b/src/tracker-indexer/tracker-indexer.h
index 8c78293..11740b2 100644
--- a/src/tracker-indexer/tracker-indexer.h
+++ b/src/tracker-indexer/tracker-indexer.h
@@ -149,6 +149,10 @@ void tracker_indexer_delete_statement (TrackerIndexer *ind
const gchar *object,
DBusGMethodInvocation *context,
GError **error);
+void tracker_indexer_sparql_update (TrackerIndexer *indexer,
+ const gchar *update,
+ DBusGMethodInvocation *context,
+ GError **error);
void tracker_indexer_restore_backup (TrackerIndexer *indexer,
const gchar *backup_file,
DBusGMethodInvocation *context,
diff --git a/src/tracker-utils/.gitignore b/src/tracker-utils/.gitignore
index 3cf2682..81de0cf 100644
--- a/src/tracker-utils/.gitignore
+++ b/src/tracker-utils/.gitignore
@@ -5,6 +5,7 @@ tracker-processes
tracker-query
tracker-search
tracker-services
+tracker-sparql
tracker-stats
tracker-status
tracker-tag
diff --git a/src/tracker-utils/Makefile.am b/src/tracker-utils/Makefile.am
index b817ac8..b77f8a3 100644
--- a/src/tracker-utils/Makefile.am
+++ b/src/tracker-utils/Makefile.am
@@ -25,7 +25,8 @@ bin_PROGRAMS = \
tracker-tag \
tracker-status \
tracker-info \
- tracker-processes
+ tracker-processes \
+ tracker-sparql
tracker_search_SOURCES = tracker-search.c
tracker_search_LDADD = $(libs)
@@ -45,3 +46,6 @@ tracker_info_LDADD = $(libs)
tracker_processes_SOURCES = tracker-processes.c
tracker_processes_LDADD = $(libs)
+tracker_sparql_SOURCES = tracker-sparql.c
+tracker_sparql_LDADD = $(libs)
+
diff --git a/src/tracker-utils/tracker-info.c b/src/tracker-utils/tracker-info.c
index efdca89..ea57857 100644
--- a/src/tracker-utils/tracker-info.c
+++ b/src/tracker-utils/tracker-info.c
@@ -1,7 +1,7 @@
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* Copyright (C) 2006, Mr Jamie McCracken (jamiemcc gnome org)
- * Copyright (C) 2008, Nokia
+ * Copyright (C) 2008-2009, Nokia
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
@@ -28,75 +28,56 @@
#include <glib.h>
#include <glib/gi18n.h>
-#include <gio/gio.h>
#include <libtracker/tracker.h>
#include <libtracker-common/tracker-common.h>
-static gchar *service;
-static gchar **metadata;
-static gchar **uris;
+static gchar **filenames = NULL;
static GOptionEntry entries[] = {
- { "service", 's', 0, G_OPTION_ARG_STRING, &service,
- N_("Service type of the file"),
- N_("Files")
- },
- { "metadata", 'm', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_STRING_ARRAY, &metadata,
- N_("Metadata to request (optional, multiple calls allowed)"),
- N_("File:Size")
- },
- { G_OPTION_REMAINING, 0, G_OPTION_FLAG_FILENAME, G_OPTION_ARG_FILENAME_ARRAY, &uris,
- N_("FILE..."),
- N_("FILE")
- },
+ { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames,
+ N_("FILE"),
+ N_("FILE")},
{ NULL }
};
static void
-print_property_value (gpointer data,
- gpointer user_data)
+print_property_value (gpointer value)
{
- GStrv results;
- GStrv uris_used;
-
- results = data;
- uris_used = user_data;
-
- if (uris_used) {
- static gint uri_id = 0;
- GStrv p;
- gint i;
-
- g_print ("%s\n", uris_used[uri_id]);
-
- if (!results) {
- g_print (" %s\n", _("No metadata available"));
- return;
- }
+ gchar **pair;
- for (p = results, i = 0; *p; p++, i++) {
- g_print (" '%s' = '%s'\n", metadata[i], *p);
- }
+ pair = value;
- uri_id++;
- } else {
- g_print (" '%s' = '%s'\n", results[0], results[1]);
+ g_print (" '%s' = '%s'\n", pair[0], pair[1]);
+}
+
+static gboolean
+has_valid_uri_scheme (const gchar *uri)
+{
+ const gchar *s;
+
+ s = uri;
+
+ if (!g_ascii_isalpha (*s)) {
+ return FALSE;
}
+
+ do {
+ s++;
+ } while (g_ascii_isalnum (*s) || *s == '+' || *s == '.' || *s == '-');
+
+ return (*s == ':');
}
int
main (int argc, char **argv)
{
TrackerClient *client;
- GFile *file;
- gchar *summary;
- gchar *path;
+ gchar *query;
GOptionContext *context;
GError *error = NULL;
- guint count;
- gint i;
- gint exit_result = EXIT_SUCCESS;
+ GPtrArray *results;
+ char *uri;
setlocale (LC_ALL, "");
@@ -110,34 +91,14 @@ main (int argc, char **argv)
/* Translators: this message will appear after the usage string */
/* and before the list of options. */
- summary = g_strconcat (_("For a list of services and metadata that "
- "can be used here, see tracker-services."),
- NULL);
-
- g_option_context_set_summary (context, summary);
g_option_context_add_main_entries (context, entries, NULL);
g_option_context_parse (context, &argc, &argv, NULL);
- g_free (summary);
- if (!uris) {
+ if (!filenames) {
gchar *help;
- g_printerr (_("URI missing"));
- g_printerr ("\n\n");
-
- help = g_option_context_get_help (context, TRUE, NULL);
- g_option_context_free (context);
- g_printerr ("%s", help);
- g_free (help);
-
- return EXIT_FAILURE;
- }
-
- if (!metadata && g_strv_length (uris) > 1) {
- gchar *help;
-
- g_printerr (_("Requesting ALL information about multiple files is not supported"));
- g_printerr ("\n\n");
+ g_printerr ("%s\n\n",
+ _("File missing"));
help = g_option_context_get_help (context, TRUE, NULL);
g_option_context_free (context);
@@ -152,173 +113,61 @@ main (int argc, char **argv)
client = tracker_connect (FALSE);
if (!client) {
- g_printerr (_("Could not establish a DBus connection to Tracker"));
- g_printerr ("\n");
-
+ g_printerr ("%s\n",
+ _("Could not establish a DBus connection to Tracker"));
return EXIT_FAILURE;
}
- /* TODO: Port to SPARQL */
-#if 0
- if (!service) {
- g_print (_("Defaulting to 'files' service"));
- g_print ("\n");
-
- type = SERVICE_FILES;
+ /* support both, URIs and local file paths */
+ if (has_valid_uri_scheme (filenames[0])) {
+ uri = g_strdup (filenames[0]);
} else {
- type = tracker_class_name_to_type (service);
+ GFile *file;
- if (type == SERVICE_OTHER_FILES && g_ascii_strcasecmp (service, "Other")) {
- g_printerr (_("Service type not recognized, using 'Other' ..."));
- g_printerr ("\n");
- }
+ file = g_file_new_for_commandline_arg (filenames[0]);
+ uri = g_file_get_uri (file);
+ g_object_unref (file);
}
- count = g_strv_length (uris);
+ query = g_strdup_printf ("SELECT ?predicate ?object WHERE { <%s> ?predicate ?object }", uri);
- if (count > 1 && metadata != NULL) {
- gchar **strv;
- GPtrArray *results;
+ results = tracker_resources_sparql_query (client, query, &error);
- strv = g_new (gchar*, count + 1);
+ g_free (uri);
+ g_free (query);
- /* Convert all files to real paths */
- for (i = 0; i < count; i++) {
- file = g_file_new_for_commandline_arg (uris[i]);
- path = g_file_get_path (file);
- g_object_unref (file);
+ if (error) {
+ g_printerr ("%s, %s\n",
+ _("Unable to retrieve data for uri"),
+ error->message);
- strv[i] = path;
- }
-
- strv[i] = NULL;
+ g_error_free (error);
+ tracker_disconnect (client);
- results = tracker_metadata_get_multiple (client,
- type,
- (const gchar **) strv,
- (const gchar **) metadata,
- &error);
-
- if (error) {
- g_printerr (tracker_dngettext (NULL,
- _("Unable to retrieve data for %d uri"),
- _("Unable to retrieve data for %d uris"),
- count),
- count);
- g_printerr (", %s\n",
- error->message);
-
- g_error_free (error);
-
- exit_result = EXIT_FAILURE;
- } else if (!results) {
- g_printerr (tracker_dngettext (NULL,
- _("No metadata available for all %d uri"),
- _("No metadata available for all %d uris"),
- count),
- count);
- g_print ("\n");
- } else {
- /* NOTE: This should be the same as count was before */
- count = g_strv_length ((gchar**) results);
-
- g_print (tracker_dngettext (NULL,
- _("Result:"),
- _("Results:"),
- count),
- count);
- g_print ("\n");
-
- g_ptr_array_foreach (results, print_property_value, strv);
- g_ptr_array_foreach (results, (GFunc) g_strfreev, NULL);
- g_ptr_array_free (results, TRUE);
- }
-
- g_strfreev (strv);
+ return EXIT_FAILURE;
+ }
+
+ if (!results) {
+ g_print ("%s\n",
+ _("No metadata available for that uri"));
} else {
- GPtrArray *results;
+ gint length;
- file = g_file_new_for_commandline_arg (uris[0]);
- path = g_file_get_path (file);
-
- if (G_LIKELY (!metadata)) {
- results = tracker_metadata_get_all (client,
- type,
- path,
- &error);
-
- if (error) {
- g_printerr ("%s, %s\n",
- _("Unable to retrieve data for uri"),
- error->message);
-
- g_error_free (error);
- exit_result = EXIT_FAILURE;
- } else if (!results) {
- g_print (_("No metadata available for that uri"));
- g_print ("\n");
- } else {
- gint length;
-
- length = results->len;
-
- g_print (tracker_dngettext (NULL,
- _("Result: %d for '%s'"),
- _("Results: %d for '%s'"),
- length),
- length,
- path);
- g_print ("\n");
-
- g_ptr_array_foreach (results, print_property_value, NULL);
- g_ptr_array_foreach (results, (GFunc) g_strfreev, NULL);
- g_ptr_array_free (results, TRUE);
- }
- } else {
- GStrv results;
-
- results = tracker_metadata_get (client,
- type,
- path,
- (const gchar **) metadata,
- &error);
- if (error) {
- g_printerr ("%s, %s\n",
- _("Unable to retrieve data for uri"),
- error->message);
-
- g_error_free (error);
- exit_result = EXIT_FAILURE;
- } else if (!results) {
- g_print (_("No metadata available for that uri"));
- g_print ("\n");
- } else {
- gint i;
-
- count = g_strv_length (results);
-
- g_print (tracker_dngettext (NULL,
- _("Result:"),
- _("Results:"),
- count),
- count);
- g_print ("\n");
-
- for (i = 0; i < count; i++) {
- g_print (" '%s' = '%s'\n",
- metadata[i], results[i]);
- }
-
- g_strfreev (results);
- }
- }
+ length = results->len;
- g_object_unref (file);
- g_free (path);
+ g_print (tracker_dngettext (NULL,
+ _("Result: %d"),
+ _("Results: %d"),
+ length),
+ length);
+ g_print ("\n");
+
+ g_ptr_array_foreach (results, (GFunc) print_property_value, NULL);
+ g_ptr_array_foreach (results, (GFunc) g_strfreev, NULL);
+ g_ptr_array_free (results, TRUE);
}
-#endif
tracker_disconnect (client);
- return exit_result;
+ return EXIT_SUCCESS;
}
diff --git a/src/tracker-utils/tracker-search.c b/src/tracker-utils/tracker-search.c
index 2dacd71..5580805 100644
--- a/src/tracker-utils/tracker-search.c
+++ b/src/tracker-utils/tracker-search.c
@@ -64,46 +64,35 @@ static GOptionEntry entries[] = {
};
static void
-get_meta_table_data (gpointer value)
+get_meta_table_data (gpointer value, gpointer user_data)
{
+ gboolean detailed = GPOINTER_TO_INT (user_data);
gchar **meta;
gchar **p;
- gchar *str;
gint i;
meta = value;
for (p = meta, i = 0; *p; p++, i++) {
- switch (i) {
- case 0:
- str = g_filename_from_utf8 (*p, -1, NULL, NULL, NULL);
- g_print (" %s:'%s'", _("Path"), str);
- g_free (str);
- break;
- case 1:
- g_print (", %s:'%s'", _("Service"), *p);
- break;
- case 2:
- g_print (", %s:'%s'", _("MIME-type"), *p);
- break;
- default:
- break;
+ if (i == 0) {
+ g_print (" %s", *p);
+ } else if (detailed) {
+ g_print (", %s", *p);
}
}
g_print ("\n");
}
+
int
main (int argc, char **argv)
{
TrackerClient *client;
GOptionContext *context;
GError *error = NULL;
- gchar *search;
+ gchar *search, *temp, *query;
gchar *summary;
- gchar **strv;
- gchar **p;
GPtrArray *array;
setlocale (LC_ALL, "");
@@ -175,129 +164,49 @@ main (int argc, char **argv)
limit = 512;
}
- /* TODO: Port to SPARQL */
-#if 0
- if (!service) {
- g_print ("%s\n",
- _("Defaulting to 'files' service"));
+ temp = g_strjoinv (" ", terms);
+ search = g_strdup (temp); /* replace with escape function */
+ g_free (temp);
- type = SERVICE_FILES;
+ if (detailed) {
+ query = g_strdup_printf ("SELECT ?s ?type ?mimeType WHERE { ?s fts:match \"%s\" ; rdf:type ?type . "
+ "OPTIONAL { ?s nie:mimeType ?mimeType } } OFFSET %d LIMIT %d",
+ search, offset, limit);
} else {
- type = tracker_class_name_to_type (service);
-
- if (type == SERVICE_OTHER_FILES && g_ascii_strcasecmp (service, "Other")) {
- g_printerr ("%s\n",
- _("Service not recognized, searching in other files..."));
- }
+ query = g_strdup_printf ("SELECT ?s WHERE { ?s fts:match \"%s\" } OFFSET %d LIMIT %d",
+ search, offset, limit);
}
- search = g_strjoinv (" ", terms);
+ array = tracker_resources_sparql_query (client, query, &error);
- if (detailed) {
- array = tracker_search_text_detailed (client,
- time (NULL),
- type,
- search,
- offset,
- limit,
- &error);
- g_free (search);
-
- if (error) {
- g_printerr ("%s, %s\n",
- _("Could not get find detailed results by text"),
- error->message);
-
- g_error_free (error);
- tracker_disconnect (client);
-
- return EXIT_FAILURE;
- }
+ g_free (search);
- if (!array) {
- g_print ("%s\n",
- _("No results found matching your query"));
- } else {
- g_print (tracker_dngettext (NULL,
- _("Result: %d"),
- _("Results: %d"),
- array->len),
- array->len);
- g_print ("\n");
-
- g_ptr_array_foreach (array, (GFunc) get_meta_table_data, NULL);
- g_ptr_array_free (array, TRUE);
- }
- } else {
- strv = tracker_search_text (client,
- time (NULL),
- type,
- search,
- offset,
- limit,
- &error);
- g_free (search);
-
- if (error) {
- g_printerr ("%s, %s\n",
- _("Could not get find results by text"),
- error->message);
-
- g_error_free (error);
- tracker_disconnect (client);
-
- return EXIT_FAILURE;
- }
+ if (error) {
+ g_printerr ("%s, %s\n",
+ _("Could not get find detailed results by text"),
+ error->message);
- if (!strv) {
- g_print ("%s\n",
- _("No results found matching your query"));
- } else {
- gint length;
-
- length = g_strv_length (strv);
-
- g_print (tracker_dngettext (NULL,
- _("Result: %d"),
- _("Results: %d"),
- length),
- length);
- g_print ("\n");
-
- for (p = strv; *p; p++) {
- gchar *s;
-
- s = g_locale_from_utf8 (*p, -1, NULL, NULL, NULL);
-
- if (!s) {
- continue;
- }
-
- g_print (" %s\n", s);
- g_free (s);
- }
-
-
- if (length >= limit) {
- /* Display '...' so the user thinks there is
- * more items.
- */
- g_print (" ...\n");
-
- /* Display warning so the user knows this is
- * not the WHOLE data set.
- */
- g_printerr ("\n"
- "%s\n",
- _("NOTE: Limit was reached, there are more items in the database not listed here"));
- }
-
- g_free (strv);
- }
+ g_error_free (error);
+ tracker_disconnect (client);
+
+ return EXIT_FAILURE;
}
-#endif
- tracker_disconnect (client);
+ if (!array) {
+ g_print ("%s\n",
+ _("No results found matching your query"));
+ } else {
+ g_print (tracker_dngettext (NULL,
+ _("Result: %d"),
+ _("Results: %d"),
+ array->len),
+ array->len);
+ g_print ("\n");
+ g_ptr_array_foreach (array, get_meta_table_data,
+ GINT_TO_POINTER (detailed));
+ g_ptr_array_free (array, TRUE);
+ }
+ tracker_disconnect (client);
return EXIT_SUCCESS;
}
diff --git a/src/tracker-utils/tracker-sparql.c b/src/tracker-utils/tracker-sparql.c
new file mode 100644
index 0000000..a3c6214
--- /dev/null
+++ b/src/tracker-utils/tracker-sparql.c
@@ -0,0 +1,181 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2006, Mr Jamie McCracken (jamiemcc gnome org)
+ * Copyright (C) 2008, Nokia
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library 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.
+ */
+
+#include "config.h"
+
+#include <sys/param.h>
+#include <stdlib.h>
+#include <time.h>
+#include <locale.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+
+#include <libtracker/tracker.h>
+
+#ifdef G_OS_WIN32
+#include <trackerd/mingw-compat.h>
+#endif /* G_OS_WIN32 */
+
+static gchar *path;
+static gchar *query;
+static gboolean update;
+
+static GOptionEntry entries[] = {
+ { "path", 'p', 0, G_OPTION_ARG_FILENAME, &path,
+ N_("Path to use in query"),
+ NULL,
+ },
+ { "query", 'q', 0, G_OPTION_ARG_STRING, &query,
+ N_("SPARQL query"),
+ NULL
+ },
+ { "update", 'u', 0, G_OPTION_ARG_NONE, &update,
+ N_("SPARQL update extensions"),
+ NULL
+ },
+ { NULL }
+};
+
+static void
+get_meta_table_data (gpointer value)
+{
+ gchar **meta;
+ gchar **p;
+ gint i;
+
+ meta = value;
+
+ for (p = meta, i = 0; *p; p++, i++) {
+ if (i == 0) {
+ g_print (" %s", *p);
+ } else {
+ g_print (", %s", *p);
+ }
+ }
+
+ g_print ("\n");
+}
+
+int
+main (int argc, char **argv)
+{
+ TrackerClient *client;
+ GOptionContext *context;
+ GError *error = NULL;
+ gchar *path_in_utf8;
+ gsize size;
+ GPtrArray *array;
+
+ setlocale (LC_ALL, "");
+
+ bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ textdomain (GETTEXT_PACKAGE);
+
+ context = g_option_context_new (_("- Query using SPARQL"));
+
+ g_option_context_add_main_entries (context, entries, NULL);
+ g_option_context_parse (context, &argc, &argv, NULL);
+
+ if ((!path && !query)
+ || (path && query)) {
+ gchar *help;
+
+ g_printerr ("%s\n\n",
+ _("Either path or query needs to be specified"));
+
+ help = g_option_context_get_help (context, TRUE, NULL);
+ g_option_context_free (context);
+ g_printerr ("%s", help);
+ g_free (help);
+
+ return EXIT_FAILURE;
+ }
+
+ g_option_context_free (context);
+
+ client = tracker_connect (FALSE);
+
+ if (!client) {
+ g_printerr ("%s\n",
+ _("Could not establish a DBus connection to Tracker"));
+ return EXIT_FAILURE;
+ }
+
+ if (path) {
+ path_in_utf8 = g_filename_to_utf8 (path, -1, NULL, NULL, &error);
+ if (error) {
+ g_printerr ("%s:'%s', %s\n",
+ _("Could not get UTF-8 path from path"),
+ path,
+ error->message);
+ g_error_free (error);
+ tracker_disconnect (client);
+
+ return EXIT_FAILURE;
+ }
+
+ g_file_get_contents (path_in_utf8, &query, &size, &error);
+ if (error) {
+ g_printerr ("%s:'%s', %s\n",
+ _("Could not read file"),
+ path_in_utf8,
+ error->message);
+ g_error_free (error);
+ g_free (path_in_utf8);
+ tracker_disconnect (client);
+
+ return EXIT_FAILURE;
+ }
+
+ g_free (path_in_utf8);
+ }
+
+ if (!update) {
+ array = tracker_resources_sparql_query (client, query, &error);
+ } else {
+ tracker_resources_sparql_update (client, query, &error);
+ }
+
+ if (error) {
+ g_printerr ("%s, %s\n",
+ _("Could not query search"),
+ error->message);
+ g_error_free (error);
+
+ return EXIT_FAILURE;
+ }
+
+ if (!update) {
+ if (!array) {
+ g_print ("%s\n",
+ _("No results found matching your query"));
+ } else {
+ g_ptr_array_foreach (array, (GFunc) get_meta_table_data, NULL);
+ g_ptr_array_free (array, TRUE);
+ }
+ }
+
+ tracker_disconnect (client);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/trackerd/tracker-resources.c b/src/trackerd/tracker-resources.c
index c066564..a822cd5 100644
--- a/src/trackerd/tracker-resources.c
+++ b/src/trackerd/tracker-resources.c
@@ -181,3 +181,83 @@ tracker_resources_load (TrackerResources *object,
tracker_dbus_request_success (request_id);
}
+void
+tracker_resources_sparql_query (TrackerResources *self,
+ const gchar *query,
+ DBusGMethodInvocation *context,
+ GError **error)
+{
+ TrackerDBResultSet *result_set;
+ GError *actual_error = NULL;
+ guint request_id;
+ GPtrArray *values;
+
+ request_id = tracker_dbus_get_next_request_id ();
+
+ tracker_dbus_async_return_if_fail (query != NULL, context);
+
+ tracker_dbus_request_new (request_id,
+ "DBus request for SPARQL Query, "
+ "query:'%s'",
+ query);
+
+ result_set = tracker_data_query_sparql (query, &actual_error);
+
+ if (actual_error) {
+ tracker_dbus_request_failed (request_id,
+ &actual_error,
+ NULL);
+ dbus_g_method_return_error (context, actual_error);
+ g_error_free (actual_error);
+ return;
+ }
+
+ values = tracker_dbus_query_result_to_ptr_array (result_set);
+
+ dbus_g_method_return (context, values);
+
+ tracker_dbus_results_ptr_array_free (&values);
+
+ if (result_set) {
+ g_object_unref (result_set);
+ }
+
+ tracker_dbus_request_success (request_id);
+}
+
+void
+tracker_resources_sparql_update (TrackerResources *self,
+ const gchar *update,
+ DBusGMethodInvocation *context,
+ GError **error)
+{
+ GError *actual_error = NULL;
+ guint request_id;
+
+ request_id = tracker_dbus_get_next_request_id ();
+
+ tracker_dbus_async_return_if_fail (update != NULL, context);
+
+ tracker_dbus_request_new (request_id,
+ "DBus request for SPARQL Update, "
+ "update:'%s'",
+ update);
+
+ org_freedesktop_Tracker_Indexer_sparql_update (tracker_dbus_indexer_get_proxy (),
+ update,
+ &actual_error);
+
+ if (actual_error) {
+ tracker_dbus_request_failed (request_id,
+ &actual_error,
+ NULL);
+ dbus_g_method_return_error (context, actual_error);
+ g_error_free (actual_error);
+ return;
+ }
+
+ dbus_g_method_return (context);
+
+ tracker_dbus_request_success (request_id);
+}
+
diff --git a/src/trackerd/tracker-resources.h b/src/trackerd/tracker-resources.h
index 61d0316..a673b62 100644
--- a/src/trackerd/tracker-resources.h
+++ b/src/trackerd/tracker-resources.h
@@ -68,6 +68,14 @@ void tracker_resources_load (TrackerResources *object,
const gchar *uri,
DBusGMethodInvocation *context,
GError **error);
+void tracker_resources_sparql_query (TrackerResources *object,
+ const gchar *query,
+ DBusGMethodInvocation *context,
+ GError **error);
+void tracker_resources_sparql_update (TrackerResources *object,
+ const gchar *update,
+ DBusGMethodInvocation *context,
+ GError **error);
G_END_DECLS
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]