[epiphany/history-rewrite-second: 2/26] Add EphyHistoryService and helper classes



commit ede505a73a8f5ddc5e9d61530213c3d0794319b3
Author: Xan Lopez <xlopez igalia com>
Date:   Fri Nov 25 13:39:50 2011 +0100

    Add EphyHistoryService and helper classes
    
    EphyHistoryService provides a high-level API to store history
    information. It will processed by a worker thread using SQLite to
    provide a fast, responsive service to the main UI.
    
    Based on the code by Martin Robinson (mrobinson igalia com) and
    Claudio Saavedra (csaavedra igalia com).

 configure.ac                                    |    1 +
 lib/Makefile.am                                 |    2 +-
 lib/history/ephy-history-service-hosts-table.c  |  316 ++++++++++
 lib/history/ephy-history-service-private.h      |   55 ++
 lib/history/ephy-history-service-urls-table.c   |  371 +++++++++++
 lib/history/ephy-history-service-visits-table.c |  207 +++++++
 lib/history/ephy-history-service.c              |  742 +++++++++++++++++++++++
 lib/history/ephy-history-service.h              |   70 +++
 lib/history/ephy-history-types.c                |  218 +++++++
 lib/history/ephy-history-types.h                |  106 ++++
 src/Makefile.am                                 |    1 +
 tests/Makefile.am                               |    6 +
 tests/ephy-history.c                            |  324 ++++++++++
 13 files changed, 2418 insertions(+), 1 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 05768cd..e255294 100644
--- a/configure.ac
+++ b/configure.ac
@@ -329,6 +329,7 @@ doc/Makefile
 doc/reference/Makefile
 lib/Makefile
 lib/egg/Makefile
+lib/history/Makefile
 lib/widgets/Makefile
 embed/Makefile
 src/Makefile
diff --git a/lib/Makefile.am b/lib/Makefile.am
index 1d7a268..82fd337 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = widgets egg
+SUBDIRS = widgets egg history
 
 noinst_LTLIBRARIES = libephymisc.la
 
diff --git a/lib/history/ephy-history-service-hosts-table.c b/lib/history/ephy-history-service-hosts-table.c
new file mode 100644
index 0000000..8dcd3d7
--- /dev/null
+++ b/lib/history/ephy-history-service-hosts-table.c
@@ -0,0 +1,316 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* vim: set sw=2 ts=2 sts=2 et: */
+/*
+ *  Copyright  2011 Igalia S.L.
+ *
+ *  This program 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, or (at your option)
+ *  any later version.
+ *
+ *  This program 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 program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include "ephy-history-service.h"
+#include "ephy-history-service-private.h"
+#include "ephy-string.h"
+#include <glib/gi18n.h>
+
+gboolean
+ephy_history_service_initialize_hosts_table (EphyHistoryService *self)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  GError *error = NULL;
+
+  if (ephy_sqlite_connection_table_exists (priv->history_database, "hosts")) {
+    return TRUE;
+  }
+  ephy_sqlite_connection_execute (priv->history_database,
+    "CREATE TABLE hosts ("
+    "id INTEGER PRIMARY KEY,"
+    "url LONGVARCAR,"
+    "title LONGVARCAR,"
+    "visit_count INTEGER DEFAULT 0 NOT NULL,"
+    "favicon_id INTEGER DEFAULT 0 NOT NULL)", &error);
+
+  if (error) {
+    g_error("Could not create hosts table: %s", error->message);
+    g_error_free (error);
+    return FALSE;
+  }
+  ephy_history_service_schedule_commit (self);
+  return TRUE;
+}
+
+void
+ephy_history_service_add_host_row (EphyHistoryService *self, EphyHistoryHost *host)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphySQLiteStatement *statement = NULL;
+  GError *error = NULL;
+
+  g_assert (priv->history_thread == g_thread_self ());
+  g_assert (priv->history_database != NULL);
+
+  statement = ephy_sqlite_connection_create_statement (priv->history_database,
+    "INSERT INTO hosts (url, title, visit_count) "
+    "VALUES (?, ?, ?)", &error);
+
+  if (error) {
+    g_error ("Could not build hosts table addition statement: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  if (ephy_sqlite_statement_bind_string (statement, 0, host->url, &error) == FALSE ||
+      ephy_sqlite_statement_bind_string (statement, 1, host->title, &error) == FALSE ||
+      ephy_sqlite_statement_bind_int (statement, 2, host->visit_count, &error) == FALSE) {
+    g_error ("Could not insert host into hosts table: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  ephy_sqlite_statement_step (statement, &error);
+  if (error) {
+    g_error ("Could not insert host into hosts table: %s", error->message);
+    g_error_free (error);
+    return;
+  } else {
+    host->id = ephy_sqlite_connection_get_last_insert_id (priv->history_database);
+  }
+
+  g_object_unref (statement);
+}
+
+void
+ephy_history_service_update_host_row (EphyHistoryService *self, EphyHistoryHost *host)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphySQLiteStatement *statement;
+  GError *error = NULL;
+
+  g_assert (priv->history_thread == g_thread_self ());
+  g_assert (priv->history_database != NULL);
+
+  statement = ephy_sqlite_connection_create_statement (priv->history_database,
+    "UPDATE hosts SET url=?, title=?, visit_count=?"
+    "WHERE id=?", &error);
+  if (error) {
+    g_error ("Could not build hosts table modification statement: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  if (ephy_sqlite_statement_bind_string (statement, 0, host->url, &error) == FALSE ||
+      ephy_sqlite_statement_bind_string (statement, 1, host->title, &error) == FALSE ||
+      ephy_sqlite_statement_bind_int (statement, 2, host->visit_count, &error) == FALSE ||
+      ephy_sqlite_statement_bind_int (statement, 3, host->id, &error) == FALSE) {
+    g_error ("Could not modify host in hosts table: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  ephy_sqlite_statement_step (statement, &error);
+  if (error) {
+    g_error ("Could not modify URL in urls table: %s", error->message);
+    g_error_free (error);
+  }
+  g_object_unref (statement);
+}
+
+EphyHistoryHost*
+ephy_history_service_get_host_row (EphyHistoryService *self, const gchar *host_string, EphyHistoryHost *host)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphySQLiteStatement *statement = NULL;
+  GError *error = NULL;
+
+  g_assert (priv->history_thread == g_thread_self ());
+  g_assert (priv->history_database != NULL);
+
+  if (host_string == NULL && host != NULL)
+    host_string = host->url;
+
+  g_assert (host_string || host->id !=-1);
+
+  if (host != NULL && host->id != -1) {
+    statement = ephy_sqlite_connection_create_statement (priv->history_database,
+        "SELECT id, url, title, visit_count FROM hosts "
+        "WHERE id=?", &error);
+  } else {
+    statement = ephy_sqlite_connection_create_statement (priv->history_database,
+        "SELECT id, url, title, visit_count FROM hosts "
+        "WHERE url=?", &error);
+  }
+
+  if (error) {
+    g_error ("Could not build hosts query statement: %s", error->message);
+    g_error_free (error);
+    return NULL;
+  }
+
+  if (host != NULL && host->id != -1)
+    ephy_sqlite_statement_bind_int (statement, 0, host->id, &error);
+  else
+    ephy_sqlite_statement_bind_string (statement, 0, host_string, &error);
+
+  if (error) {
+    g_error ("Could not build hosts table query statement: %s", error->message);
+    g_error_free (error);
+    g_object_unref (statement);
+    return NULL;
+  }
+
+  if (ephy_sqlite_statement_step (statement, &error) == FALSE) {
+    g_object_unref (statement);
+    return NULL;
+  }
+
+  if (host == NULL) {
+    host = ephy_history_host_new (NULL, NULL, 0);
+  } else {
+    if (host->url)
+      g_free (host->url);
+    if (host->title)
+      g_free (host->title);
+  }
+
+  host->id = ephy_sqlite_statement_get_column_as_int (statement, 0);
+  host->url = g_strdup (ephy_sqlite_statement_get_column_as_string (statement, 1)),
+  host->title = g_strdup (ephy_sqlite_statement_get_column_as_string (statement, 2)),
+  host->visit_count = ephy_sqlite_statement_get_column_as_int (statement, 3),
+
+  g_object_unref (statement);
+  return host;
+}
+
+static EphyHistoryHost*
+create_host_from_statement (EphySQLiteStatement *statement)
+{
+  EphyHistoryHost *host =
+    ephy_history_host_new (ephy_sqlite_statement_get_column_as_string (statement, 1),
+                           ephy_sqlite_statement_get_column_as_string (statement, 2),
+                           ephy_sqlite_statement_get_column_as_int (statement, 3));
+  host->id = ephy_sqlite_statement_get_column_as_int (statement, 0);
+
+  return host;
+}
+
+GList*
+ephy_history_service_get_all_hosts (EphyHistoryService *self)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphySQLiteStatement *statement = NULL;
+  GList *hosts = NULL;
+  GError *error = NULL;
+
+  g_assert (priv->history_thread == g_thread_self ());
+  g_assert (priv->history_database != NULL);
+
+  statement = ephy_sqlite_connection_create_statement (priv->history_database,
+      "SELECT id, url, title, visit_count FROM hosts", &error);
+
+  if (error) {
+    g_error ("Could not build hosts query statement: %s", error->message);
+    g_error_free (error);
+    return NULL;
+  }
+
+  while (ephy_sqlite_statement_step (statement, &error))
+    hosts = g_list_prepend (hosts, create_host_from_statement (statement));
+
+  hosts = g_list_reverse (hosts);
+
+  if (error) {
+    g_error ("Could not execute hosts table query statement: %s", error->message);
+    g_error_free (error);
+  }
+  g_object_unref (statement);
+
+  return hosts;
+}
+
+/* Inspired from ephy-history.c */
+static GList *
+get_hostname_and_locations (const gchar *url, gchar **hostname)
+{
+	GList *host_locations = NULL;
+	char *scheme = NULL;
+
+  if (url) {
+		scheme = g_uri_parse_scheme (url);
+		*hostname = ephy_string_get_host_name (url);
+	}
+  /* Build an host name */
+  if (scheme == NULL || *hostname == NULL) {
+    *hostname = g_strdup (_("Others"));
+    host_locations = g_list_append (host_locations,
+                                    g_strdup ("about:blank"));
+  }  else if (strcmp (scheme, "file") == 0) {
+    *hostname = g_strdup (_("Local files"));
+    host_locations = g_list_append (host_locations,
+                                    g_strdup ("file:///"));
+  } else {
+    char *location;
+    char *tmp;
+
+    if (g_str_equal (scheme, "https")) {
+      /* If scheme is https, we still fake http. */
+      location = g_strconcat ("http://";, *hostname, "/", NULL);
+      host_locations = g_list_append (host_locations, location);
+    }
+
+    /* We append the real address */
+    location = g_strconcat (scheme,
+                            "://", *hostname, "/", NULL);
+    host_locations = g_list_append (host_locations, location);
+
+    /* and also a fake www-modified address if it's http or https. */
+    if (g_str_has_prefix (scheme, "http")) {
+      if (g_str_has_prefix (*hostname, "www."))
+        tmp = g_strdup (*hostname + 4);
+      else
+        tmp = g_strconcat ("www.", *hostname, NULL);
+      location = g_strconcat ("http://";, tmp, "/", NULL);
+      g_free (tmp);
+      host_locations = g_list_append (host_locations, location);
+    }
+  }
+  return host_locations;
+}
+
+EphyHistoryHost*
+ephy_history_service_get_host_row_from_url (EphyHistoryService *self,
+                                            const gchar *url)
+{
+  GList *host_locations, *l;
+  char *hostname;
+  EphyHistoryHost *host;
+
+  host_locations = get_hostname_and_locations (url, &hostname);
+
+  for (l = host_locations; l != NULL; l = l->next) {
+    host = ephy_history_service_get_host_row (self, l->data, NULL);
+    if (host != NULL)
+      break;
+  }
+
+  if (host == NULL) {
+    host = ephy_history_host_new (host_locations->data, hostname, 0);
+    ephy_history_service_add_host_row (self, host);
+  }
+
+  g_free (hostname);
+  g_list_free_full (host_locations, (GDestroyNotify) g_free);
+
+  return host;
+}
diff --git a/lib/history/ephy-history-service-private.h b/lib/history/ephy-history-service-private.h
new file mode 100644
index 0000000..4cf88d4
--- /dev/null
+++ b/lib/history/ephy-history-service-private.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/*
+ *  Copyright  2011 Igalia S.L.
+ *
+ *  This program 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, or (at your option)
+ *  any later version.
+ *
+ *  This program 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 program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef EPHY_HISTORY_SERVICE_PRIVATE_H
+#define EPHY_HISTORY_SERVICE_PRIVATE_H
+
+#include "ephy-sqlite-connection.h"
+
+struct _EphyHistoryServicePrivate {
+  char *history_filename;
+  EphySQLiteConnection *history_database;
+  GThread *history_thread;
+  GAsyncQueue *queue;
+  GMutex history_thread_mutex;
+  gboolean active;
+  gboolean scheduled_to_quit;
+  gboolean scheduled_to_commit;
+};
+
+void                     ephy_history_service_schedule_commit         (EphyHistoryService *self); 
+gboolean                 ephy_history_service_initialize_urls_table   (EphyHistoryService *self);
+EphyHistoryURL *         ephy_history_service_get_url_row             (EphyHistoryService *self, const char *url_string, EphyHistoryURL *url);
+void                     ephy_history_service_add_url_row             (EphyHistoryService *self, EphyHistoryURL *url);
+void                     ephy_history_service_update_url_row          (EphyHistoryService *self, EphyHistoryURL *url);
+GList*                   ephy_history_service_find_url_rows           (EphyHistoryService *self, EphyHistoryQuery *query);
+void                     ephy_history_service_delete_url              (EphyHistoryService *self, EphyHistoryURL *url);
+
+gboolean                 ephy_history_service_initialize_visits_table (EphyHistoryService *self);
+void                     ephy_history_service_add_visit_row           (EphyHistoryService *self, EphyHistoryPageVisit *visit);
+GList *                  ephy_history_service_find_visit_rows         (EphyHistoryService *self, EphyHistoryQuery *query);
+
+gboolean                 ephy_history_service_initialize_hosts_table  (EphyHistoryService *self);
+void                     ephy_history_service_add_host_row            (EphyHistoryService *self, EphyHistoryHost *host);
+void                     ephy_history_service_update_host_row         (EphyHistoryService *self, EphyHistoryHost *host);
+EphyHistoryHost *        ephy_history_service_get_host_row            (EphyHistoryService *self, const gchar *url_string, EphyHistoryHost *host);
+GList *                  ephy_history_service_get_all_hosts           (EphyHistoryService *self);
+EphyHistoryHost *        ephy_history_service_get_host_row_from_url   (EphyHistoryService *self, const gchar *url);
+
+#endif /* EPHY_HISTORY_SERVICE_PRIVATE_H */
diff --git a/lib/history/ephy-history-service-urls-table.c b/lib/history/ephy-history-service-urls-table.c
new file mode 100644
index 0000000..3c7834b
--- /dev/null
+++ b/lib/history/ephy-history-service-urls-table.c
@@ -0,0 +1,371 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* vim: set sw=2 ts=2 sts=2 et: */
+/*
+ *  Copyright  2011 Igalia S.L.
+ *
+ *  This program 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, or (at your option)
+ *  any later version.
+ *
+ *  This program 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 program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include "ephy-history-service.h"
+#include "ephy-history-service-private.h"
+
+gboolean
+ephy_history_service_initialize_urls_table (EphyHistoryService *self)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  GError *error = NULL;
+
+  if (ephy_sqlite_connection_table_exists (priv->history_database, "visits")) {
+    return TRUE;
+  }
+  ephy_sqlite_connection_execute (priv->history_database,
+    "CREATE TABLE urls ("
+    "id INTEGER PRIMARY KEY,"
+    "host INTEGER NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,"
+    "url LONGVARCAR,"
+    "title LONGVARCAR,"
+    "visit_count INTEGER DEFAULT 0 NOT NULL,"
+    "typed_count INTEGER DEFAULT 0 NOT NULL,"
+    "last_visit_time INTEGER,"
+    "zoom_level REAL DEFAULT 1.0)", &error);
+
+  if (error) {
+    g_error("Could not create urls table: %s", error->message);
+    g_error_free (error);
+    return FALSE;
+  }
+  ephy_history_service_schedule_commit (self);
+  return TRUE;
+}
+
+EphyHistoryURL *
+ephy_history_service_get_url_row (EphyHistoryService *self, const char *url_string, EphyHistoryURL *url)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphySQLiteStatement *statement = NULL;  
+  GError *error = NULL;
+
+  g_assert (priv->history_thread == g_thread_self ());
+  g_assert (priv->history_database != NULL);
+
+  if (url_string == NULL && url != NULL) {
+    url_string = url->url;
+  }
+  g_assert (url_string || url->id != -1);
+
+  if (url != NULL && url->id != -1) {
+    statement = ephy_sqlite_connection_create_statement (priv->history_database,
+      "SELECT id, url, title, visit_count, typed_count, last_visit_time, zoom_level FROM urls "
+      "WHERE id=?", &error);
+  } else {
+    statement = ephy_sqlite_connection_create_statement (priv->history_database,
+      "SELECT id, url, title, visit_count, typed_count, last_visit_time, zoom_level FROM urls "
+      "WHERE url=?", &error);
+  }
+
+  if (error) {
+    g_error ("Could not build urls table query statement: %s", error->message);
+    g_error_free (error);
+    return NULL;
+  }
+
+  if (url != NULL && url->id != -1) {
+    ephy_sqlite_statement_bind_int (statement, 0, url->id, &error);
+  } else {
+    ephy_sqlite_statement_bind_string (statement, 0, url_string, &error);
+  }
+  if (error) {
+    g_error ("Could not build urls table query statement: %s", error->message);
+    g_error_free (error);
+    g_object_unref (statement);
+    return NULL;
+  }
+
+  if (ephy_sqlite_statement_step (statement, &error) == FALSE) {
+    g_object_unref (statement);
+    return NULL;
+  }
+
+  if (url == NULL) {
+    url = ephy_history_url_new (NULL, NULL, 0, 0, 0, 1.0);
+  } else {
+    if (url->url)
+      g_free (url->url);
+    if (url->title)
+      g_free (url->title);
+  }
+
+  url->id = ephy_sqlite_statement_get_column_as_int (statement, 0);
+  url->url = g_strdup (ephy_sqlite_statement_get_column_as_string (statement, 1)),
+  url->title = g_strdup (ephy_sqlite_statement_get_column_as_string (statement, 2)),
+  url->visit_count = ephy_sqlite_statement_get_column_as_int (statement, 3),
+  url->typed_count = ephy_sqlite_statement_get_column_as_int (statement, 4),
+  url->last_visit_time = ephy_sqlite_statement_get_column_as_int (statement, 5);
+  url->zoom_level = ephy_sqlite_statement_get_column_as_double (statement, 6);
+
+  g_object_unref (statement);
+  return url;
+}
+
+void
+ephy_history_service_add_url_row (EphyHistoryService *self, EphyHistoryURL *url)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphySQLiteStatement *statement = NULL;  
+  GError *error = NULL;
+
+  g_assert (priv->history_thread == g_thread_self ());
+  g_assert (priv->history_database != NULL);
+
+  statement = ephy_sqlite_connection_create_statement (priv->history_database,
+    "INSERT INTO urls (url, title, visit_count, typed_count, last_visit_time, zoom_level, host) "
+    " VALUES (?, ?, ?, ?, ?, ?, ?)", &error);
+  if (error) {
+    g_error ("Could not build urls table addition statement: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  if (ephy_sqlite_statement_bind_string (statement, 0, url->url, &error) == FALSE ||
+      ephy_sqlite_statement_bind_string (statement, 1, url->title, &error) == FALSE || 
+      ephy_sqlite_statement_bind_int (statement, 2, url->visit_count, &error) == FALSE ||
+      ephy_sqlite_statement_bind_int (statement, 3, url->typed_count, &error) == FALSE ||
+      ephy_sqlite_statement_bind_int (statement, 4, url->last_visit_time, &error) == FALSE ||
+      ephy_sqlite_statement_bind_double (statement, 5, url->zoom_level, &error) == FALSE ||
+      ephy_sqlite_statement_bind_int (statement, 6, url->host->id, &error) == FALSE) {
+    g_error ("Could not insert URL into urls table: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  ephy_sqlite_statement_step (statement, &error);
+  if (error) {
+    g_error ("Could not insert URL into urls table: %s", error->message);
+    g_error_free (error);
+  } else {
+    url->id = ephy_sqlite_connection_get_last_insert_id (priv->history_database);
+  }
+
+  g_object_unref (statement);
+}
+
+void
+ephy_history_service_update_url_row (EphyHistoryService *self, EphyHistoryURL *url)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphySQLiteStatement *statement;
+  GError *error = NULL;
+
+  g_assert (priv->history_thread == g_thread_self ());
+  g_assert (priv->history_database != NULL);
+
+  statement = ephy_sqlite_connection_create_statement (priv->history_database,
+    "UPDATE urls SET title=?, visit_count=?, typed_count=?, last_visit_time=?, zoom_level=? "
+    "WHERE id=?", &error);
+  if (error) {
+    g_error ("Could not build urls table modification statement: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  if (ephy_sqlite_statement_bind_string (statement, 0, url->title, &error) == FALSE || 
+      ephy_sqlite_statement_bind_int (statement, 1, url->visit_count, &error) == FALSE ||
+      ephy_sqlite_statement_bind_int (statement, 2, url->typed_count, &error) == FALSE ||
+      ephy_sqlite_statement_bind_int (statement, 3, url->last_visit_time, &error) == FALSE ||
+      ephy_sqlite_statement_bind_double (statement, 4, url->zoom_level, &error) == FALSE ||
+      ephy_sqlite_statement_bind_int (statement, 5, url->id, &error) == FALSE) {
+    g_error ("Could not modify URL in urls table: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  ephy_sqlite_statement_step (statement, &error);
+  if (error) {
+    g_error ("Could not modify URL in urls table: %s", error->message);
+    g_error_free (error);
+  }
+  g_object_unref (statement);
+}
+
+static EphyHistoryURL *
+create_url_from_statement (EphySQLiteStatement *statement)
+{
+  EphyHistoryURL *url = ephy_history_url_new (ephy_sqlite_statement_get_column_as_string (statement, 1),
+                                              ephy_sqlite_statement_get_column_as_string (statement, 2),
+                                              ephy_sqlite_statement_get_column_as_int (statement, 3),
+                                              ephy_sqlite_statement_get_column_as_int (statement, 4),
+                                              ephy_sqlite_statement_get_column_as_int (statement, 5),
+                                              ephy_sqlite_statement_get_column_as_int (statement, 6));
+
+  url->id = ephy_sqlite_statement_get_column_as_int (statement, 0);
+  url->host = ephy_history_host_new (NULL, NULL, 0);
+  url->host->id = ephy_sqlite_statement_get_column_as_int (statement, 7);
+
+  return url;
+}
+
+GList *
+ephy_history_service_find_url_rows (EphyHistoryService *self, EphyHistoryQuery *query)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphySQLiteStatement *statement = NULL;
+  GList *substring;
+  GString *statement_str;
+  GList *urls = NULL;
+  GError *error = NULL;
+  const char *base_statement = ""
+    "SELECT "
+      "DISTINCT urls.id, "
+      "urls.url, "
+      "urls.title, "
+      "urls.visit_count, "
+      "urls.typed_count, "
+      "urls.last_visit_time, "
+      "urls.zoom_level, "
+      "urls.host "
+    "FROM "
+      "urls JOIN visits ON visits.url = urls.id "
+    "WHERE ";
+
+  int i = 0;
+
+  g_assert (priv->history_thread == g_thread_self ());
+  g_assert (priv->history_database != NULL);
+
+  statement_str = g_string_new (base_statement);
+
+  if (query->from >= 0)
+    statement_str = g_string_append (statement_str, "visits.visit_time >= ? AND ");
+  if (query->to >= 0)
+    statement_str = g_string_append (statement_str, "visits.visit_time <= ? AND ");
+
+  for (substring = query->substring_list; substring != NULL; substring = substring->next)
+    statement_str = g_string_append (statement_str, "(urls.url LIKE ? OR urls.title LIKE ?) AND ");
+
+  statement_str = g_string_append (statement_str, "1");
+
+  statement = ephy_sqlite_connection_create_statement (priv->history_database,
+						       statement_str->str, &error);
+  g_string_free (statement_str, TRUE);
+
+  if (error) {
+    g_error ("Could not build urls table query statement: %s", error->message);
+    g_error_free (error);
+    g_object_unref (statement);
+    return NULL;
+  }
+
+  if (query->from >= 0) {
+    if (ephy_sqlite_statement_bind_int (statement, i++, (int)query->from, &error) == FALSE) {
+      g_error ("Could not build urls table query statement: %s", error->message);
+      g_error_free (error);
+      g_object_unref (statement);
+      return NULL;
+    }
+  }
+  if (query->to >= 0) {
+    if (ephy_sqlite_statement_bind_int (statement, i++, (int)query->to, &error) == FALSE) {
+      g_error ("Could not build urls table query statement: %s", error->message);
+      g_error_free (error);
+      g_object_unref (statement);
+      return NULL;
+    }
+  }
+  for (substring = query->substring_list; substring != NULL; substring = substring->next) {
+    char *string = g_strdup_printf ("%%%s%%", (char*)substring->data);
+    if (ephy_sqlite_statement_bind_string (statement, i++, string, &error) == FALSE) {
+      g_error ("Could not build urls table query statement: %s", error->message);
+      g_error_free (error);
+      g_object_unref (statement);
+      g_free (string);
+      return NULL;
+    }
+    if (ephy_sqlite_statement_bind_string (statement, i++, string, &error) == FALSE) {
+      g_error ("Could not build urls table query statement: %s", error->message);
+      g_error_free (error);
+      g_object_unref (statement);
+      g_free (string);
+      return NULL;
+    }
+    g_free (string);
+  }
+
+  while (ephy_sqlite_statement_step (statement, &error))
+    urls = g_list_prepend (urls, create_url_from_statement (statement));
+
+  urls = g_list_reverse (urls);
+
+  if (error) {
+    g_error ("Could not execute urls table query statement: %s", error->message);
+    g_error_free (error);
+    g_object_unref (statement);
+    g_list_free_full (urls, (GDestroyNotify)ephy_history_url_free);
+    return NULL;
+  }
+
+  g_object_unref (statement);
+  return urls;
+}
+
+void
+ephy_history_service_delete_url (EphyHistoryService *self, EphyHistoryURL *url)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphySQLiteStatement *statement = NULL;
+  gchar *sql_statement;
+  GError *error = NULL;
+
+  g_assert (priv->history_thread == g_thread_self ());
+  g_assert (priv->history_database != NULL);
+
+  g_assert (url->id != -1 || url->url);
+
+  if (url->id != -1)
+    sql_statement = g_strdup ("DELETE FROM urls WHERE id=?");
+  else
+    sql_statement = g_strdup ("DELETE FROM urls WHERE url=?");
+
+  statement = ephy_sqlite_connection_create_statement (priv->history_database,
+                                                       sql_statement, &error);
+  g_free (sql_statement);
+
+  if (error) {
+    g_error ("Could not build urls table query statement: %s", error->message);
+    g_error_free (error);
+    g_object_unref (statement);
+    return;
+  }
+
+  if (url->id != -1)
+    ephy_sqlite_statement_bind_int (statement, 0, url->id, &error);
+  else
+    ephy_sqlite_statement_bind_string (statement, 0, url->url, &error);
+
+  if (error) {
+    g_error ("Could not build urls table query statement: %s", error->message);
+    g_error_free (error);
+    g_object_unref (statement);
+    return;
+  }
+
+  ephy_sqlite_statement_step (statement, &error);
+  if (error) {
+    g_error ("Could not modify URL in urls table: %s", error->message);
+    g_error_free (error);
+  }
+  g_object_unref (statement);
+}
diff --git a/lib/history/ephy-history-service-visits-table.c b/lib/history/ephy-history-service-visits-table.c
new file mode 100644
index 0000000..822a336
--- /dev/null
+++ b/lib/history/ephy-history-service-visits-table.c
@@ -0,0 +1,207 @@
+/*
+ *  Copyright  2011 Igalia S.L.
+ *
+ *  This program 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, or (at your option)
+ *  any later version.
+ *
+ *  This program 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 program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include "ephy-history-service.h"
+#include "ephy-history-service-private.h"
+
+gboolean
+ephy_history_service_initialize_visits_table (EphyHistoryService *self)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  GError *error = NULL;
+
+  if (ephy_sqlite_connection_table_exists (priv->history_database, "visits"))
+    return TRUE;
+
+  ephy_sqlite_connection_execute (priv->history_database,
+    "CREATE TABLE visits ("
+    "id INTEGER PRIMARY KEY,"
+    "url INTEGER NOT NULL REFERENCES urls(id) ON DELETE CASCADE,"
+    "visit_time INTEGER NOT NULL,"
+    "visit_type INTEGER NOT NULL,"
+    "referring_visit INTEGER)", &error);
+
+  if (error) {
+    g_error("Could not create visits table: %s", error->message);
+    g_error_free (error);
+    return FALSE;
+  }
+  ephy_history_service_schedule_commit (self);
+  return TRUE;
+}
+
+void
+ephy_history_service_add_visit_row (EphyHistoryService *self, EphyHistoryPageVisit *visit)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphySQLiteStatement *statement;
+  GError *error = NULL;
+
+  g_assert (priv->history_thread == g_thread_self ());
+  g_assert (priv->history_database != NULL);
+
+  statement = ephy_sqlite_connection_create_statement (
+    priv->history_database,
+    "INSERT INTO visits (url, visit_time, visit_type) "
+    " VALUES (?, ?, ?) ", &error);
+  if (error) {
+    g_error ("Could not build visits table addition statement: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  if (ephy_sqlite_statement_bind_int (statement, 0, visit->url->id, &error) == FALSE ||
+      ephy_sqlite_statement_bind_int (statement, 1, visit->visit_time, &error) == FALSE ||
+      ephy_sqlite_statement_bind_int (statement, 2, visit->visit_type, &error) == FALSE ) {
+    g_error ("Could not build visits table addition statement: %s", error->message);
+    g_error_free (error);
+    return;
+  }
+
+  ephy_sqlite_statement_step (statement, &error);
+  if (error) {
+    g_error ("Could not insert URL into visits table: %s", error->message);
+    g_error_free (error);
+  } else {
+    visit->id = ephy_sqlite_connection_get_last_insert_id (priv->history_database);
+  }
+
+  ephy_history_service_schedule_commit (self);
+  g_object_unref (statement);
+}
+
+static EphyHistoryPageVisit *
+create_page_visit_from_statement (EphySQLiteStatement *statement)
+{
+  EphyHistoryPageVisit *visit = 
+    ephy_history_page_visit_new (NULL,
+                                 ephy_sqlite_statement_get_column_as_int (statement, 1),
+                                 ephy_sqlite_statement_get_column_as_int (statement, 2));
+  visit->url->id = ephy_sqlite_statement_get_column_as_int (statement, 0);
+  return visit;
+}
+
+GList *
+ephy_history_service_find_visit_rows (EphyHistoryService *self, EphyHistoryQuery *query)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphySQLiteStatement *statement = NULL;
+  GList *substring;
+  GString *statement_str;
+  GList *visits = NULL;
+  GError *error = NULL;
+  const char *base_statement = ""
+    "SELECT "
+      "visits.url, "
+      "visits.visit_time, "
+      "visits.visit_type ";
+  const char *from_join_statement = ""
+    "FROM "
+      "visits JOIN urls ON visits.url = urls.id ";
+  const char *from_visits_statement = ""
+    "FROM "
+      "visits ";
+
+  int i = 0;
+
+  g_assert (priv->history_thread == g_thread_self ());
+  g_assert (priv->history_database != NULL);
+
+  statement_str = g_string_new (base_statement);
+
+  if (query->substring_list)
+    statement_str = g_string_append (statement_str, from_join_statement);
+  else
+    statement_str = g_string_append (statement_str, from_visits_statement);
+
+  statement_str = g_string_append (statement_str, "WHERE ");
+
+  if (query->from >= 0)
+    statement_str = g_string_append (statement_str, "visits.visit_time >= ? AND ");
+  if (query->to >= 0)
+    statement_str = g_string_append (statement_str, "visits.visit_time <= ? AND ");
+
+  for (substring = query->substring_list; substring != NULL; substring = substring->next) {
+    statement_str = g_string_append (statement_str, "(urls.url LIKE ? OR urls.title LIKE ?) AND ");
+  }
+
+  statement_str = g_string_append (statement_str, "1");
+
+  statement = ephy_sqlite_connection_create_statement (priv->history_database,
+						       statement_str->str, &error);
+  g_string_free (statement_str, TRUE);
+
+  if (error) {
+    g_error ("Could not build visits table query statement: %s", error->message);
+    g_error_free (error);
+    g_object_unref (statement);
+  }
+
+  if (query->from >= 0) {
+    if (ephy_sqlite_statement_bind_int (statement, i++, (int)query->from, &error) == FALSE) {
+      g_error ("Could not build urls table query statement: %s", error->message);
+      g_error_free (error);
+      g_object_unref (statement);
+      return NULL;
+    }
+  }
+  if (query->to >= 0) {
+    if (ephy_sqlite_statement_bind_int (statement, i++, (int)query->to, &error) == FALSE) {
+      g_error ("Could not build urls table query statement: %s", error->message);
+      g_error_free (error);
+      g_object_unref (statement);
+      return NULL;
+    }
+  }
+  for (substring = query->substring_list; substring != NULL; substring = substring->next) {
+    char *string = g_strdup_printf ("%%%s%%", (char*)substring->data);
+    if (ephy_sqlite_statement_bind_string (statement, i++, string, &error) == FALSE) {
+      g_error ("Could not build urls table query statement: %s", error->message);
+      g_error_free (error);
+      g_object_unref (statement);
+      g_free (string);
+      return NULL;
+    }
+    if (ephy_sqlite_statement_bind_string (statement, i++, string, &error) == FALSE) {
+      g_error ("Could not build urls table query statement: %s", error->message);
+      g_error_free (error);
+      g_object_unref (statement);
+      g_free (string);
+      return NULL;
+    }
+    g_free (string);
+  }
+
+  while (ephy_sqlite_statement_step (statement, &error))
+    visits = g_list_prepend (visits, create_page_visit_from_statement (statement));
+
+  visits = g_list_reverse (visits);
+
+  if (error) {
+    g_error ("Could not execute urls table query statement: %s", error->message);
+    g_error_free (error);
+    g_object_unref (statement);
+    ephy_history_page_visit_list_free (visits);
+    return NULL;
+  }
+
+  g_object_unref (statement);
+  return visits;
+}
diff --git a/lib/history/ephy-history-service.c b/lib/history/ephy-history-service.c
new file mode 100644
index 0000000..ae62a23
--- /dev/null
+++ b/lib/history/ephy-history-service.c
@@ -0,0 +1,742 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* *  Copyright  2011 Igalia S.L.
+ *
+ *  This program 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, or (at your option)
+ *  any later version.
+ *
+ *  This program 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 program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+#include "ephy-history-service.h"
+
+#include "ephy-history-service-private.h"
+#include "ephy-history-types.h"
+#include "ephy-sqlite-connection.h"
+
+typedef gboolean (*EphyHistoryServiceMethod)                              (EphyHistoryService *self, gpointer data, gpointer *result);
+
+typedef enum {
+  /* WRITE */
+  SET_URL_TITLE,
+  SET_URL_ZOOM_LEVEL,
+  SET_URL_PROPERTY, /* We only need this SET_ ? */
+  ADD_VISIT,
+  ADD_VISITS,
+  DELETE_URLS,
+  /* QUIT */
+  QUIT,
+  /* READ */
+  GET_URL,
+  QUERY_URLS,
+  QUERY_VISITS,
+} EphyHistoryServiceMessageType;
+  
+typedef struct _EphyHistoryServiceMessage {
+  EphyHistoryService *service;
+  EphyHistoryServiceMessageType type;
+  gpointer *method_argument;
+  gboolean success;
+  gpointer result;
+  gpointer user_data;
+  GDestroyNotify method_argument_cleanup;
+  EphyHistoryJobCallback callback;
+} EphyHistoryServiceMessage;
+
+static gpointer run_history_service_thread                                (EphyHistoryService *self);
+static void ephy_history_service_process_message                          (EphyHistoryService *self, EphyHistoryServiceMessage *message);
+static gboolean ephy_history_service_execute_quit                         (EphyHistoryService *self, gpointer data, gpointer *result);
+static void ephy_history_service_quit                                     (EphyHistoryService *self, EphyHistoryJobCallback callback, gpointer user_data);
+
+enum {
+  PROP_0,
+  PROP_HISTORY_FILENAME,
+};
+
+#define EPHY_HISTORY_SERVICE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), EPHY_TYPE_HISTORY_SERVICE, EphyHistoryServicePrivate))
+
+G_DEFINE_TYPE (EphyHistoryService, ephy_history_service, G_TYPE_OBJECT);
+
+static void
+ephy_history_service_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
+{
+  EphyHistoryService *self = EPHY_HISTORY_SERVICE (object);
+
+  switch (property_id) {
+    case PROP_HISTORY_FILENAME:
+      g_free (self->priv->history_filename);
+      self->priv->history_filename = g_strdup (g_value_get_string (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (self, property_id, pspec);
+      break;
+  }
+}
+
+static void
+ephy_history_service_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
+{
+  EphyHistoryService *self = EPHY_HISTORY_SERVICE (object);
+  switch (property_id) {
+    case PROP_HISTORY_FILENAME:
+      g_value_set_string (value, self->priv->history_filename);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+  }
+}
+
+static void
+ephy_history_service_finalize (GObject *self)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+
+  ephy_history_service_quit (EPHY_HISTORY_SERVICE (self), NULL, NULL);
+
+  if (priv->history_thread)
+    g_thread_join (priv->history_thread);
+
+  g_free (priv->history_filename);
+
+  G_OBJECT_CLASS (ephy_history_service_parent_class)->finalize (self);
+}
+
+static void
+ephy_history_service_class_init (EphyHistoryServiceClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  gobject_class->finalize = ephy_history_service_finalize;
+  gobject_class->get_property = ephy_history_service_get_property;
+  gobject_class->set_property = ephy_history_service_set_property;
+
+  g_object_class_install_property (gobject_class,
+                                   PROP_HISTORY_FILENAME,
+                                   g_param_spec_string ("history-filename",
+                                                        "History filename",
+                                                        "The filename of the SQLite file holding containing history",
+                                                        NULL,
+                                                        G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
+
+  g_type_class_add_private (gobject_class, sizeof (EphyHistoryServicePrivate));
+}
+
+static void
+ephy_history_service_init (EphyHistoryService *self)
+{
+  self->priv = EPHY_HISTORY_SERVICE_GET_PRIVATE (self);
+
+  self->priv->active = TRUE;
+  self->priv->history_thread = g_thread_new ("EphyHistoryService", (GThreadFunc) run_history_service_thread, self);
+  self->priv->queue = g_async_queue_new ();
+}
+
+EphyHistoryService *
+ephy_history_service_new (const char *history_filename)
+{
+  return EPHY_HISTORY_SERVICE (g_object_new (EPHY_TYPE_HISTORY_SERVICE,
+                                             "history-filename", history_filename,
+                                              NULL));
+}
+
+static gint
+sort_messages (EphyHistoryServiceMessage* a, EphyHistoryServiceMessage* b, gpointer user_data)
+{
+  return a->type > b->type ? 1 : a->type == b->type ? 0 : -1;
+}
+
+static void
+ephy_history_service_send_message (EphyHistoryService *self, gpointer data)
+{
+  EphyHistoryServicePrivate *priv = self->priv;
+
+  g_async_queue_push_sorted (priv->queue, data, (GCompareDataFunc)sort_messages, NULL);
+}
+
+static void
+ephy_history_service_commit (EphyHistoryService *self)
+{
+  EphyHistoryServicePrivate *priv = self->priv;
+  GError *error = NULL;
+  g_assert (priv->history_thread == g_thread_self ());
+
+  if (NULL == priv->history_database)
+    return;
+
+  ephy_sqlite_connection_commit_transaction (priv->history_database, &error);
+  if (NULL != error) {
+    g_error ("Could not commit idle history database transaction: %s", error->message);
+    g_error_free (error);
+  }
+  ephy_sqlite_connection_begin_transaction (priv->history_database, &error);
+  if (NULL != error) {
+    g_error ("Could not start long-running history database transaction: %s", error->message);
+    g_error_free (error);
+  }
+
+  self->priv->scheduled_to_commit = FALSE;
+}
+
+static void
+ephy_history_service_enable_foreign_keys (EphyHistoryService *self)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  GError *error = NULL;
+
+  if (NULL == priv->history_database)
+    return;
+
+  ephy_sqlite_connection_execute (priv->history_database,
+                                  "PRAGMA foreign_keys = ON", &error);
+
+  if (error) {
+    g_error ("Could not enable foreign keys pragma: %s", error->message);
+    g_error_free (error);
+  }
+}
+
+static gboolean
+ephy_history_service_open_database_connections (EphyHistoryService *self)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  GError *error = NULL;
+
+  g_assert (priv->history_thread == g_thread_self ());
+
+  priv->history_database = ephy_sqlite_connection_new ();
+  ephy_sqlite_connection_open (priv->history_database, priv->history_filename, &error);
+  if (error) {
+    g_object_unref (priv->history_database);
+    priv->history_database = NULL;
+    g_error ("Could not open history database: %s", error->message);
+    g_error_free (error);
+    return FALSE;
+  }
+
+  ephy_history_service_enable_foreign_keys (self);
+
+  ephy_sqlite_connection_begin_transaction (priv->history_database, &error);
+  if (error) {
+    g_error ("Could not begin long running transaction in history database: %s", error->message);
+    g_error_free (error);
+    return FALSE;
+  }
+
+  if ((ephy_history_service_initialize_hosts_table (self) == FALSE) ||
+      (ephy_history_service_initialize_urls_table (self) == FALSE) ||
+      (ephy_history_service_initialize_visits_table (self) == FALSE))
+    return FALSE;
+
+  return TRUE;
+}
+
+static void
+ephy_history_service_close_database_connections (EphyHistoryService *self)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+
+  g_assert (priv->history_thread == g_thread_self ());
+
+  ephy_sqlite_connection_close (priv->history_database);
+  g_object_unref (priv->history_database);
+  priv->history_database = NULL;
+}
+
+static gboolean
+ephy_history_service_is_scheduled_to_quit (EphyHistoryService *self)
+{
+  return self->priv->scheduled_to_quit;
+}
+
+static gboolean
+ephy_history_service_is_scheduled_to_commit (EphyHistoryService *self)
+{
+  return self->priv->scheduled_to_commit;
+}
+
+void
+ephy_history_service_schedule_commit (EphyHistoryService *self)
+{
+  self->priv->scheduled_to_commit = TRUE;
+}
+
+static gboolean
+ephy_history_service_execute_quit (EphyHistoryService *self, gpointer data, gpointer *result)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  g_assert (priv->history_thread == g_thread_self ());
+
+  if (ephy_history_service_is_scheduled_to_commit (self))
+    ephy_history_service_commit (self);
+
+  g_async_queue_unref (priv->queue);
+
+  self->priv->scheduled_to_quit = TRUE;
+
+  return FALSE;
+}
+
+static gpointer
+run_history_service_thread (EphyHistoryService *self)
+{
+  EphyHistoryServicePrivate *priv = EPHY_HISTORY_SERVICE (self)->priv;
+  EphyHistoryServiceMessage *message;
+
+  g_assert (priv->history_thread == g_thread_self ());
+
+  if (ephy_history_service_open_database_connections (self) == FALSE)
+    return NULL;
+
+  do {
+    message = g_async_queue_try_pop (priv->queue);
+    if (!message) {
+      /* Schedule commit if needed. */
+      if (ephy_history_service_is_scheduled_to_commit (self))
+        ephy_history_service_commit (self);
+
+      /* Block the thread until there's data in the queue. */
+      message = g_async_queue_pop (priv->queue);
+    }
+
+    /* Process item. */
+    ephy_history_service_process_message (self, message);
+
+  } while (!ephy_history_service_is_scheduled_to_quit (self));
+
+  ephy_history_service_close_database_connections (self);
+  ephy_history_service_execute_quit (self, NULL, NULL);
+
+  return NULL;
+}
+
+static EphyHistoryServiceMessage *
+ephy_history_service_message_new (EphyHistoryService *service,
+                                  EphyHistoryServiceMessageType type,
+                                  gpointer method_argument,
+                                  GDestroyNotify method_argument_cleanup,
+                                  EphyHistoryJobCallback callback,
+                                  gpointer user_data)
+{
+  EphyHistoryServiceMessage *details = g_slice_alloc0 (sizeof (EphyHistoryServiceMessage));
+
+  details->service = service; 
+  details->type = type;
+  details->method_argument = method_argument;
+  details->method_argument_cleanup = method_argument_cleanup;
+  details->callback = callback;
+  details->user_data = user_data;
+
+  return details;
+}
+
+static void
+ephy_history_service_message_free (EphyHistoryServiceMessage *details)
+{
+  if (details->method_argument_cleanup)
+    details->method_argument_cleanup (details->method_argument);
+
+  g_slice_free1 (sizeof (EphyHistoryServiceMessage), details);
+}
+
+static gboolean
+ephy_history_service_execute_job_callback (gpointer data)
+{
+  EphyHistoryServiceMessage *details = (EphyHistoryServiceMessage*) data;
+
+  g_assert (details->callback);
+  details->callback (details->service, details->success, details->result, details->user_data);
+  ephy_history_service_message_free (details);
+
+  return FALSE;
+}
+
+static gboolean
+ephy_history_service_execute_add_visit_helper (EphyHistoryService *self, EphyHistoryPageVisit *visit)
+{
+  if (visit->url->host == NULL)
+    visit->url->host = ephy_history_service_get_host_row_from_url (self, visit->url->url);
+
+  visit->url->host->visit_count++;
+  ephy_history_service_update_host_row (self, visit->url->host);
+
+  /* A NULL return here means that the URL does not yet exist in the database */
+  if (NULL == ephy_history_service_get_url_row (self, visit->url->url, visit->url)) {
+    visit->url->last_visit_time = visit->visit_time;
+    ephy_history_service_add_url_row (self, visit->url);
+
+    if (visit->url->id == -1) {
+      g_error ("Adding visit failed after failed URL addition.");
+      return FALSE;
+    }
+
+  } else {
+    visit->url->visit_count++;
+
+    if (visit->visit_time > visit->url->last_visit_time)
+      visit->url->last_visit_time = visit->visit_time;
+
+    ephy_history_service_update_url_row (self, visit->url);
+  }
+
+  ephy_history_service_add_visit_row (self, visit);
+  return visit->id != -1;
+}
+
+static gboolean
+ephy_history_service_execute_add_visit (EphyHistoryService *self, EphyHistoryPageVisit *visit, gpointer *result)
+{
+  gboolean success;
+  g_assert (self->priv->history_thread == g_thread_self ());
+
+  success = ephy_history_service_execute_add_visit_helper (self, visit);
+  return success;
+}
+
+static gboolean
+ephy_history_service_execute_add_visits (EphyHistoryService *self, GList *visits, gpointer *result)
+{
+  gboolean success = TRUE;
+  g_assert (self->priv->history_thread == g_thread_self ());
+
+  while (visits) {
+    success = success && ephy_history_service_execute_add_visit_helper (self, (EphyHistoryPageVisit *) visits->data);
+    visits = visits->next;
+  }
+
+  ephy_history_service_schedule_commit (self);
+
+  return success;
+}
+
+static gboolean
+ephy_history_service_execute_find_visits (EphyHistoryService *self, EphyHistoryQuery *query, gpointer *result)
+{
+  GList *visits = ephy_history_service_find_visit_rows (self, query);
+  GList *current = visits;
+
+  /* FIXME: We don't have a good way to tell the difference between failures and empty returns */
+  while (current) {
+    EphyHistoryPageVisit *visit = (EphyHistoryPageVisit *) current->data;
+    if (NULL == ephy_history_service_get_url_row (self, NULL, visit->url)) {
+      ephy_history_page_visit_list_free (visits);
+      g_error ("Tried to process an orphaned page visit");
+      return FALSE;
+    }
+
+    current = current->next;
+  }
+
+  *result = visits;
+  return TRUE;
+}
+
+void
+ephy_history_service_add_visit (EphyHistoryService *self, EphyHistoryPageVisit *visit, EphyHistoryJobCallback callback, gpointer user_data)
+{
+  EphyHistoryServiceMessage *details = 
+    ephy_history_service_message_new (self, ADD_VISIT,
+                                      ephy_history_page_visit_copy (visit),
+                                      (GDestroyNotify) ephy_history_page_visit_free,
+                                      callback, user_data);
+  ephy_history_service_send_message (self, details);
+}
+
+void
+ephy_history_service_add_visits (EphyHistoryService *self, GList *visits, EphyHistoryJobCallback callback, gpointer user_data)
+{
+  EphyHistoryServiceMessage *details = 
+    ephy_history_service_message_new (self, ADD_VISITS,
+                                      ephy_history_page_visit_list_copy (visits),
+                                      (GDestroyNotify) ephy_history_page_visit_list_free,
+                                      callback, user_data);
+  ephy_history_service_send_message (self, details);
+}
+
+void
+ephy_history_service_find_visits_in_time (EphyHistoryService *self, gint64 from, gint64 to, EphyHistoryJobCallback callback, gpointer user_data)
+{
+  EphyHistoryQuery *query = ephy_history_query_new ();
+  query->from = from;
+  query->to = to;
+
+  ephy_history_service_query_visits (self, query, callback, user_data);
+  ephy_history_query_free (query);
+}
+
+void
+ephy_history_service_query_visits (EphyHistoryService *self, EphyHistoryQuery *query, EphyHistoryJobCallback callback, gpointer user_data)
+{
+  EphyHistoryServiceMessage *details;
+
+  details = ephy_history_service_message_new (self, QUERY_VISITS,
+                                              ephy_history_query_copy (query), (GDestroyNotify) ephy_history_query_free, callback, user_data);
+  ephy_history_service_send_message (self, details);
+}
+
+static gboolean
+ephy_history_service_execute_query_urls (EphyHistoryService *self, EphyHistoryQuery *query, gpointer *result)
+{
+  GList *urls = ephy_history_service_find_url_rows (self, query);
+
+  *result = urls;
+
+  return TRUE;
+}
+
+void
+ephy_history_service_query_urls (EphyHistoryService *self, EphyHistoryQuery *query, EphyHistoryJobCallback callback, gpointer user_data)
+{
+  EphyHistoryServiceMessage *details;
+
+  details = ephy_history_service_message_new (self, QUERY_URLS,
+                                              ephy_history_query_copy (query), (GDestroyNotify) ephy_history_query_free, callback, user_data);
+  ephy_history_service_send_message (self, details);
+}
+
+static gboolean
+ephy_history_service_execute_set_url_title (EphyHistoryService *self,
+                                            EphyHistoryURL *url,
+                                            gpointer *result)
+{
+  char *title = g_strdup (url->title);
+
+  if (NULL == ephy_history_service_get_url_row (self, NULL, url)) {
+    /* The URL is not yet in the database, so we can't update it.. */
+    g_free (title);
+    return FALSE;
+  } else {
+    g_free (url->title);
+    url->title = title;
+    ephy_history_service_update_url_row (self, url);
+    ephy_history_service_schedule_commit (self);
+    return TRUE;
+  }
+}
+
+void
+ephy_history_service_set_url_title (EphyHistoryService *self,
+                                    const char *orig_url,
+                                    const char *title,
+                                    EphyHistoryJobCallback callback,
+                                    gpointer user_data)
+{
+  EphyHistoryURL *url = ephy_history_url_new (orig_url, title, 0, 0, 0, 1.0);
+
+  EphyHistoryServiceMessage *details =
+    ephy_history_service_message_new (self, SET_URL_TITLE,
+                                      url, (GDestroyNotify) ephy_history_url_free,
+                                      callback, user_data);
+  ephy_history_service_send_message (self, details);
+}
+
+static gboolean
+ephy_history_service_execute_set_url_zoom_level (EphyHistoryService *self,
+                                                 EphyHistoryURL *url,
+                                                 gpointer *result)
+{
+  double zoom_level = url->zoom_level;
+
+  if (NULL == ephy_history_service_get_url_row (self, NULL, url)) {
+    /* The URL is not yet in the database, so we can't update it.. */
+    return FALSE;
+  } else {
+    url->zoom_level = zoom_level;
+    ephy_history_service_update_url_row (self, url);
+    ephy_history_service_schedule_commit (self);
+    return TRUE;
+  }
+}
+
+void
+ephy_history_service_set_url_zoom_level (EphyHistoryService *self,
+                                         const char *orig_url,
+                                         const double zoom_level,
+                                         EphyHistoryJobCallback callback,
+                                         gpointer user_data)
+{
+  EphyHistoryURL *url = ephy_history_url_new (orig_url, NULL, 0, 0, 0, zoom_level);
+
+  EphyHistoryServiceMessage *details =
+    ephy_history_service_message_new (self, SET_URL_ZOOM_LEVEL,
+                                      url, (GDestroyNotify) ephy_history_url_free,
+                                      callback, user_data);
+  ephy_history_service_send_message (self, details);
+}
+
+static gboolean
+ephy_history_service_execute_get_url (EphyHistoryService *self,
+                                      const gchar *orig_url,
+                                      gpointer *result)
+{
+  EphyHistoryURL *url;
+
+  url = ephy_history_service_get_url_row (self, orig_url, NULL);
+
+  *result = url;
+
+  return url != NULL;
+}
+
+void
+ephy_history_service_get_url (EphyHistoryService *self,
+                              const char *url,
+                              EphyHistoryJobCallback callback,
+                              gpointer user_data)
+{
+  EphyHistoryServiceMessage *details =
+    ephy_history_service_message_new (self, GET_URL,
+                                      g_strdup (url), g_free,
+                                      callback, user_data);
+  ephy_history_service_send_message (self, details);
+}
+
+static gboolean
+ephy_history_service_execute_set_url_property (EphyHistoryService *self,
+                                               GVariant *variant,
+                                               gpointer *result)
+{
+  GVariant *value, *mvalue;
+  gchar *url_string;
+  EphyHistoryURL *url;
+  EphyHistoryURLProperty property;
+
+  g_variant_get (variant, "(s(iv))", &url_string, &property, &value);
+
+  url = ephy_history_url_new (url_string, NULL, 0, 0, 0, 1.0);
+  g_free (url_string);
+
+  if (NULL == ephy_history_service_get_url_row (self, NULL, url)) {
+    g_variant_unref (value);
+    ephy_history_url_free (url);
+
+    return FALSE;
+  }
+
+  switch (property) {
+  case EPHY_HISTORY_URL_TITLE:
+    if (url->title)
+      g_free (url->title);
+    mvalue = g_variant_get_maybe (value);
+    if (mvalue) {
+      url->title = g_variant_dup_string (mvalue, NULL);
+      g_variant_unref (mvalue);
+    } else {
+      url->title = NULL;
+    }
+    break;
+  case EPHY_HISTORY_URL_ZOOM_LEVEL:
+    url->zoom_level = g_variant_get_double (value);
+    break;
+  default:
+    g_assert_not_reached();
+  }
+  g_variant_unref (value);
+
+  ephy_history_service_update_url_row (self, url);
+  ephy_history_service_schedule_commit (self);
+
+  return TRUE;
+}
+
+void
+ephy_history_service_set_url_property (EphyHistoryService *self,
+                                       const char *url,
+                                       EphyHistoryURLProperty property,
+                                       GVariant *value,
+                                       EphyHistoryJobCallback callback,
+                                       gpointer user_data)
+{
+  GVariant *variant = g_variant_new ("(s(iv))", url, property, value);
+
+  EphyHistoryServiceMessage *details =
+    ephy_history_service_message_new (self, SET_URL_PROPERTY,
+                                      variant, (GDestroyNotify)g_variant_unref,
+                                      callback, user_data);
+
+  ephy_history_service_send_message (self, details);
+}
+
+static gboolean
+ephy_history_service_execute_delete_urls (EphyHistoryService *self,
+                                          GList *urls,
+                                          gpointer *result)
+{
+  GList *l;
+  EphyHistoryURL *url;
+
+  for (l = urls; l != NULL; l = l->next) {
+    url = l->data;
+    ephy_history_service_delete_url (self, url);
+  }
+
+  ephy_history_service_schedule_commit (self);
+
+  return TRUE;
+}
+
+void
+ephy_history_service_delete_urls (EphyHistoryService *self,
+                                  GList *urls,
+                                  EphyHistoryJobCallback callback,
+                                  gpointer user_data)
+{
+  EphyHistoryServiceMessage *details =
+    ephy_history_service_message_new (self, DELETE_URLS, 
+                                      ephy_history_url_list_copy (urls), (GDestroyNotify)ephy_history_url_list_free,
+                                      callback, user_data);
+  ephy_history_service_send_message (self, details);
+}
+
+static void
+ephy_history_service_quit (EphyHistoryService *self,
+                           EphyHistoryJobCallback callback,
+                           gpointer user_data)
+{
+  EphyHistoryServiceMessage *details =
+    ephy_history_service_message_new (self, QUIT, 
+                                      NULL, NULL,
+                                      callback, user_data);
+  ephy_history_service_send_message (self, details);
+}
+
+static EphyHistoryServiceMethod methods[] = {
+  (EphyHistoryServiceMethod)ephy_history_service_execute_set_url_title,
+  (EphyHistoryServiceMethod)ephy_history_service_execute_set_url_zoom_level,
+  (EphyHistoryServiceMethod)ephy_history_service_execute_set_url_property,
+  (EphyHistoryServiceMethod)ephy_history_service_execute_add_visit,
+  (EphyHistoryServiceMethod)ephy_history_service_execute_add_visits,
+  (EphyHistoryServiceMethod)ephy_history_service_execute_delete_urls,
+  (EphyHistoryServiceMethod)ephy_history_service_execute_quit,
+  (EphyHistoryServiceMethod)ephy_history_service_execute_get_url,
+  (EphyHistoryServiceMethod)ephy_history_service_execute_query_urls,
+  (EphyHistoryServiceMethod)ephy_history_service_execute_find_visits
+};
+
+static void
+ephy_history_service_process_message (EphyHistoryService *self,
+                                      EphyHistoryServiceMessage *message)
+{
+  EphyHistoryServiceMethod method;
+
+  g_assert (self->priv->history_thread == g_thread_self ());
+
+  method = methods[message->type];
+  message->result = NULL;
+  message->success = method (message->service, message->method_argument, &message->result);
+
+  if (message->callback)
+    g_idle_add ((GSourceFunc)ephy_history_service_execute_job_callback, message);
+  else
+    ephy_history_service_message_free (message);
+
+  return;
+}
diff --git a/lib/history/ephy-history-service.h b/lib/history/ephy-history-service.h
new file mode 100644
index 0000000..601d13b
--- /dev/null
+++ b/lib/history/ephy-history-service.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* vim: set sw=2 ts=2 sts=2 et: */
+/*
+ *  Copyright  2011 Igalia S.L.
+ *
+ *  This program 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, or (at your option)
+ *  any later version.
+ *
+ *  This program 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 program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef EPHY_HISTORY_SERVICE_H
+#define EPHY_HISTORY_SERVICE_H
+
+#include <glib-object.h>
+#include "ephy-history-types.h"
+
+G_BEGIN_DECLS
+
+/* convenience macros */
+#define EPHY_TYPE_HISTORY_SERVICE             (ephy_history_service_get_type())
+#define EPHY_HISTORY_SERVICE(obj)             (G_TYPE_CHECK_INSTANCE_CAST((obj),EPHY_TYPE_HISTORY_SERVICE,EphyHistoryService))
+#define EPHY_HISTORY_SERVICE_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST((klass),EPHY_TYPE_HISTORY_SERVICE,EphyHistoryServiceClass))
+#define EPHY_IS_HISTORY_SERVICE(obj)          (G_TYPE_CHECK_INSTANCE_TYPE((obj),EPHY_TYPE_HISTORY_SERVICE))
+#define EPHY_IS_HISTORY_SERVICE_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE((klass),EPHY_TYPE_HISTORY_SERVICE))
+#define EPHY_HISTORY_SERVICE_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS((obj),EPHY_TYPE_HISTORY_SERVICE,EphyHistoryServiceClass))
+
+typedef struct _EphyHistoryService                EphyHistoryService;
+typedef struct _EphyHistoryServiceClass           EphyHistoryServiceClass;
+typedef struct _EphyHistoryServicePrivate         EphyHistoryServicePrivate;
+
+typedef void   (*EphyHistoryJobCallback)          (EphyHistoryService *service, gboolean success, gpointer result_data, gpointer user_data);
+
+struct _EphyHistoryService {
+     GObject parent;
+
+    /* private */
+    EphyHistoryServicePrivate *priv;
+};
+
+struct _EphyHistoryServiceClass {
+    GObjectClass parent_class;
+};
+
+GType                    ephy_history_service_get_type                (void);
+EphyHistoryService *     ephy_history_service_new                     (const char *history_filename);
+
+void                     ephy_history_service_add_visit               (EphyHistoryService *self, EphyHistoryPageVisit *visit, EphyHistoryJobCallback callback, gpointer user_data);
+void                     ephy_history_service_add_visits              (EphyHistoryService *self, GList *visits, EphyHistoryJobCallback callback, gpointer user_data);
+void                     ephy_history_service_find_visits_in_time     (EphyHistoryService *self, gint64 from, gint64 to, EphyHistoryJobCallback callback, gpointer user_data);
+void                     ephy_history_service_query_visits            (EphyHistoryService *self, EphyHistoryQuery *query, EphyHistoryJobCallback callback, gpointer user_data);
+void                     ephy_history_service_query_urls              (EphyHistoryService *self, EphyHistoryQuery *query, EphyHistoryJobCallback callback, gpointer user_data);
+void                     ephy_history_service_set_url_title           (EphyHistoryService *self, const char *url, const char *title, EphyHistoryJobCallback callback, gpointer user_data);
+void                     ephy_history_service_set_url_zoom_level      (EphyHistoryService *self, const char *url, const double zoom_level, EphyHistoryJobCallback callback, gpointer user_data);
+void                     ephy_history_service_set_url_property        (EphyHistoryService *self, const char *url, EphyHistoryURLProperty property, GVariant *value, EphyHistoryJobCallback callback, gpointer user_data);
+void                     ephy_history_service_get_url                 (EphyHistoryService *self, const char *url, EphyHistoryJobCallback callback, gpointer user_data);
+void                     ephy_history_service_delete_urls             (EphyHistoryService *self, GList *urls, EphyHistoryJobCallback callback, gpointer user_data);
+G_END_DECLS
+
+#endif /* EPHY_HISTORY_SERVICE_H */
+
diff --git a/lib/history/ephy-history-types.c b/lib/history/ephy-history-types.c
new file mode 100644
index 0000000..1a8fc43
--- /dev/null
+++ b/lib/history/ephy-history-types.c
@@ -0,0 +1,218 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* vim: set sw=2 ts=2 sts=2 et: */
+/*
+ *  Copyright  2011 Igalia S.L.
+ *
+ *  This program 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, or (at your option)
+ *  any later version.
+ *
+ *  This program 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 program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include <glib.h>
+
+#include "ephy-history-types.h"
+
+EphyHistoryPageVisit *
+ephy_history_page_visit_new_with_url (EphyHistoryURL *url, gint64 visit_time, EphyHistoryPageVisitType visit_type)
+{
+  EphyHistoryPageVisit *visit = g_slice_alloc0 (sizeof (EphyHistoryPageVisit));
+  visit->id = -1;
+  visit->url = url;
+  visit->visit_time = visit_time;
+  visit->visit_type = visit_type;
+  return visit;
+}
+
+EphyHistoryPageVisit *
+ephy_history_page_visit_new (const char *url, gint64 visit_time, EphyHistoryPageVisitType visit_type)
+{
+  return ephy_history_page_visit_new_with_url (ephy_history_url_new (url, "", 0, 0, 0, 1.0),
+                                               visit_time, visit_type);
+}
+
+void
+ephy_history_page_visit_free (EphyHistoryPageVisit *visit)
+{
+  if (visit == NULL)
+    return;
+
+  ephy_history_url_free (visit->url);
+  g_slice_free1 (sizeof (EphyHistoryPageVisit), visit);
+}
+
+EphyHistoryPageVisit *
+ephy_history_page_visit_copy (EphyHistoryPageVisit *visit)
+{
+  EphyHistoryPageVisit *copy = ephy_history_page_visit_new_with_url (0, visit->visit_time, visit->visit_type);
+  copy->id = visit->id;
+  copy->url = ephy_history_url_copy (visit->url);
+  return copy;
+}
+
+GList *
+ephy_history_page_visit_list_copy (GList *original)
+{
+  GList *new = g_list_copy (original);
+  GList *current = new;
+  while (current) {
+    current->data = ephy_history_page_visit_copy ((EphyHistoryPageVisit *) current->data);
+    current = current->next;
+  }
+  return new;
+}
+
+void
+ephy_history_page_visit_list_free (GList *list)
+{
+  g_list_free_full (list, (GDestroyNotify) ephy_history_page_visit_free);
+}
+
+EphyHistoryHost *
+ephy_history_host_new (const char *url, const char *title, int visit_count)
+{
+  EphyHistoryHost *host = g_slice_alloc0 (sizeof (EphyHistoryHost));
+
+  host->id = -1;
+  host->url = g_strdup (url);
+  host->title = g_strdup (title);
+  host->visit_count = visit_count;
+
+  return host;
+}
+
+EphyHistoryHost *
+ephy_history_host_copy (EphyHistoryHost *original)
+{
+  EphyHistoryHost *host;
+
+  if (original == NULL)
+    return NULL;
+
+  host = ephy_history_host_new (original->url,
+                                original->title,
+                                original->visit_count);
+  host->id = original->id;
+
+  return host;
+}
+
+void
+ephy_history_host_free (EphyHistoryHost *host)
+{
+  if (host == NULL)
+    return;
+
+  g_free (host->url);
+  g_free (host->title);
+
+  g_slice_free1 (sizeof (EphyHistoryHost), host);
+}
+
+EphyHistoryURL *
+ephy_history_url_new (const char *url, const char *title, int visit_count, int typed_count, int last_visit_time, double zoom_level)
+{
+  EphyHistoryURL *history_url = g_slice_alloc0 (sizeof (EphyHistoryURL));
+  history_url->id = -1;
+  history_url->url = g_strdup (url);
+  history_url->title = g_strdup (title);
+  history_url->visit_count = visit_count;
+  history_url->typed_count = typed_count;
+  history_url->last_visit_time = last_visit_time;
+  history_url->zoom_level = zoom_level;
+  history_url->host = NULL;
+  return history_url;
+}
+
+EphyHistoryURL *
+ephy_history_url_copy (EphyHistoryURL *url)
+{
+  EphyHistoryURL *copy;
+  if (url == NULL)
+    return NULL;
+
+  copy = ephy_history_url_new (url->url,
+                               url->title,
+                               url->visit_count,
+                               url->typed_count,
+                               url->last_visit_time,
+                               url->zoom_level);
+  copy->id = url->id;
+  copy->host = ephy_history_host_copy (url->host);
+  return copy;
+}
+
+void
+ephy_history_url_free (EphyHistoryURL *url)
+{
+  if (url == NULL)
+    return;
+
+  g_free (url->url);
+  g_free (url->title);
+  ephy_history_host_free (url->host);
+  g_slice_free1 (sizeof (EphyHistoryURL), url);
+}
+
+GList *
+ephy_history_url_list_copy (GList *original)
+{
+  GList *new = NULL, *last;
+
+  if (original) {
+    new = last = g_list_append (NULL, ephy_history_url_copy (original->data));
+    original = original->next;
+
+    while (original) {
+      last = g_list_append (last, ephy_history_url_copy (original->data));
+      last = last->next;
+      original = original->next;
+    }
+  }
+
+  return new;
+}
+
+void
+ephy_history_url_list_free (GList *list)
+{
+  g_list_free_full (list, (GDestroyNotify) ephy_history_url_free);
+}
+
+EphyHistoryQuery *
+ephy_history_query_new ()
+{
+  return (EphyHistoryQuery*) g_slice_alloc0 (sizeof (EphyHistoryQuery));
+}
+
+void
+ephy_history_query_free (EphyHistoryQuery *query)
+{
+  g_list_free_full (query->substring_list, g_free);
+  g_slice_free1 (sizeof (EphyHistoryQuery), query);
+}
+
+EphyHistoryQuery *
+ephy_history_query_copy (EphyHistoryQuery *query)
+{
+  GList *iter;
+  EphyHistoryQuery *copy = ephy_history_query_new ();
+  copy->from = query->from;
+  copy->to = query->to;
+
+  for (iter = query->substring_list; iter != NULL; iter = iter->next) {
+    copy->substring_list = g_list_prepend (copy->substring_list, g_strdup (iter->data));
+  }
+  copy->substring_list = g_list_reverse (copy->substring_list);
+
+  return copy;
+}
diff --git a/lib/history/ephy-history-types.h b/lib/history/ephy-history-types.h
new file mode 100644
index 0000000..0f50ec1
--- /dev/null
+++ b/lib/history/ephy-history-types.h
@@ -0,0 +1,106 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* vim: set sw=2 ts=2 sts=2 et: */
+/*
+ *  Copyright  2011 Igalia S.L.
+ *
+ *  This program 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, or (at your option)
+ *  any later version.
+ *
+ *  This program 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 program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef EPHY_HISTORY_TYPES_H
+#define EPHY_HISTORY_TYPES_H
+
+G_BEGIN_DECLS
+
+/*
+ * Page transition types heavily inspired by those used in Chromium. See:
+ * src/chrome/common/page_transition_types.h in the Chromium source code.
+ */
+typedef enum {
+  EPHY_PAGE_VISIT_LINK,
+  EPHY_PAGE_VISIT_TYPED,
+  EPHY_PAGE_VISIT_MANUAL_SUBFRAME,
+  EPHY_PAGE_VISIT_AUTO_SUBFRAME,
+  EPHY_PAGE_VISIT_STARTUP,
+  EPHY_PAGE_VISIT_FORM_SUBMISSION,
+  EPHY_PAGE_VISIT_FORM_RELOAD,
+} EphyHistoryPageVisitType;
+
+typedef enum {
+  EPHY_HISTORY_URL_TITLE,
+  EPHY_HISTORY_URL_ZOOM_LEVEL
+} EphyHistoryURLProperty;
+
+typedef struct
+{
+  int id;
+  char* url;
+  char* title;
+  int visit_count;
+} EphyHistoryHost;
+
+typedef struct _EphyHistoryURL
+{
+  int id;
+  char* url;
+  char* title;
+  int visit_count;
+  int typed_count;
+  int last_visit_time;
+  double zoom_level;
+  EphyHistoryHost *host;
+} EphyHistoryURL;
+
+typedef struct _EphyHistoryPageVisit
+{
+  EphyHistoryURL* url;
+  int id;
+  gint64 visit_time;
+  EphyHistoryPageVisitType visit_type;
+} EphyHistoryPageVisit;
+
+typedef struct _EphyHistoryQuery
+{
+  gint64 from;
+  gint64 to;
+  guint limit;
+  GList* substring_list;
+} EphyHistoryQuery;
+
+EphyHistoryPageVisit *          ephy_history_page_visit_new (const char *url, gint64 visit_time, EphyHistoryPageVisitType visit_type);
+EphyHistoryPageVisit *          ephy_history_page_visit_new_with_url (EphyHistoryURL *url, gint64 visit_time, EphyHistoryPageVisitType visit_type);
+EphyHistoryPageVisit *          ephy_history_page_visit_copy (EphyHistoryPageVisit *visit);
+void                            ephy_history_page_visit_free (EphyHistoryPageVisit *visit);
+
+GList *                         ephy_history_page_visit_list_copy (GList* original);
+void                            ephy_history_page_visit_list_free (GList* list);
+
+EphyHistoryHost *               ephy_history_host_new (const char *url, const char *title, int visit_count);
+EphyHistoryHost *               ephy_history_host_copy (EphyHistoryHost *original);
+void                            ephy_history_host_free (EphyHistoryHost *host);
+
+EphyHistoryURL *                ephy_history_url_new (const char *url, const char* title, int visit_count, int typed_count, int last_visit_time, double zoom_level);
+EphyHistoryURL *                ephy_history_url_copy (EphyHistoryURL *url);
+void                            ephy_history_url_free (EphyHistoryURL *url);
+
+GList *                         ephy_history_url_list_copy (GList *original);
+void                            ephy_history_url_list_free (GList *list);
+
+EphyHistoryQuery *              ephy_history_query_new (void);
+void                            ephy_history_query_free (EphyHistoryQuery *query);
+EphyHistoryQuery *              ephy_history_query_copy (EphyHistoryQuery *query);
+
+G_END_DECLS
+
+#endif /* EPHY_HISTORY_TYPES_H */
diff --git a/src/Makefile.am b/src/Makefile.am
index d6bf77f..9e48a46 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -147,6 +147,7 @@ epiphany_LDADD = \
 	$(top_builddir)/embed/libephyembed.la \
 	$(top_builddir)/lib/widgets/libephywidgets.la \
 	$(top_builddir)/lib/libephymisc.la \
+	$(top_builddir)/lib/history/libephyhistory.la \
 	$(top_builddir)/lib/egg/libegg.la \
 	$(DEPENDENCIES_LIBS) \
 	$(LIBINTL)
diff --git a/tests/Makefile.am b/tests/Makefile.am
index b277c15..f18dd16 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1,6 +1,7 @@
 noinst_PROGRAMS = \
 	test-ephy-download \
 	test-ephy-embed-single \
+	test-ephy-history \
 	test-ephy-location-entry \
 	test-ephy-search-entry \
 	test-ephy-sqlite
@@ -79,6 +80,7 @@ check-local: test-nonrecursive
 INCLUDES = \
 	-I$(top_srcdir)/embed    \
 	-I$(top_srcdir)/lib      \
+	-I$(top_srcdir)/lib/history \
 	-I$(top_srcdir)/lib/widgets \
 	-I$(top_srcdir)/src      \
 	-I$(top_srcdir)/src/bookmarks
@@ -92,6 +94,7 @@ LDADD = \
 	$(top_builddir)/src/bookmarks/libephybookmarks.la \
 	$(top_builddir)/embed/libephyembed.la \
 	$(top_builddir)/lib/widgets/libephywidgets.la \
+	$(top_builddir)/lib/history/libephyhistory.la \
 	$(top_builddir)/lib/libephymisc.la \
 	$(top_builddir)/lib/egg/libegg.la \
 	$(DBUS_LIBS) \
@@ -110,6 +113,9 @@ test_ephy_download_SOURCES = \
 test_ephy_embed_single_SOURCES = \
 	ephy-embed-single.c
 
+test_ephy_history_SOURCES = \
+	ephy-history.c
+
 test_ephy_location_entry_SOURCES = \
 	ephy-location-entry.c
 
diff --git a/tests/ephy-history.c b/tests/ephy-history.c
new file mode 100644
index 0000000..0f2d282
--- /dev/null
+++ b/tests/ephy-history.c
@@ -0,0 +1,324 @@
+/* vim: set sw=2 ts=2 sts=2 et: */
+/*
+ * ephy-sqlite-statement.c
+ * This file is part of Epiphany
+ *
+ * Copyright  2010 Igalia S.L.
+ *
+ * Epiphany 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.
+ *
+ * Epiphany 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 Epiphany; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA  02110-1301  USA
+ */
+
+#include "config.h"
+
+#include "ephy-history-service.h"
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gtk/gtk.h>
+
+static EphyHistoryService *
+ensure_empty_history (const char* filename)
+{
+  if (g_file_test (filename, G_FILE_TEST_IS_REGULAR))
+    g_unlink (filename);
+
+  return ephy_history_service_new (filename);
+}
+
+static void
+test_create_history_service (void)
+{
+  gchar *temporary_file = g_build_filename (g_get_tmp_dir (), "epiphany-history-test.db", NULL);
+  EphyHistoryService *service = ensure_empty_history (temporary_file);
+
+  g_free (temporary_file);
+  g_object_unref (service);
+}
+
+static gboolean
+destroy_history_service_and_end_main_loop (EphyHistoryService *service)
+{
+  g_object_unref (service);
+  g_assert (TRUE);
+  gtk_main_quit ();
+
+  return FALSE;
+}
+
+static void
+test_create_history_service_and_destroy_later (void)
+{
+  gchar *temporary_file = g_build_filename (g_get_tmp_dir (), "epiphany-history-test.db", NULL);
+  EphyHistoryService *service = ensure_empty_history (temporary_file);
+  g_free (temporary_file);
+  g_timeout_add (100, (GSourceFunc) destroy_history_service_and_end_main_loop, service);
+
+  gtk_main ();
+}
+
+static void
+page_vist_created (EphyHistoryService *service, gboolean success, gpointer result_data, gpointer user_data)
+{
+  g_object_unref (service);
+  g_assert (result_data == NULL);
+  g_assert (user_data == NULL);
+  g_assert (success);
+  gtk_main_quit ();
+}
+
+static void
+test_create_history_entry (void)
+{
+  gchar *temporary_file = g_build_filename (g_get_tmp_dir (), "epiphany-history-test.db", NULL);
+  EphyHistoryService *service = ensure_empty_history(temporary_file);
+
+  EphyHistoryPageVisit *visit = ephy_history_page_visit_new ("http://www.gnome.org";, 0, EPHY_PAGE_VISIT_TYPED);
+  ephy_history_service_add_visit (service, visit, page_vist_created, NULL);
+  ephy_history_page_visit_free (visit);
+  g_free (temporary_file);
+
+  gtk_main ();
+}
+
+static GList *
+create_test_page_visit_list ()
+{
+  GList *visits = NULL;
+  int i;
+  for (i = 0; i < 100; i++) {
+    visits = g_list_append (visits, ephy_history_page_visit_new ("http://www.gnome.org";, 3, EPHY_PAGE_VISIT_TYPED));
+    visits = g_list_append (visits, ephy_history_page_visit_new ("http://www.gnome.org";, 5, EPHY_PAGE_VISIT_TYPED));
+    visits = g_list_append (visits, ephy_history_page_visit_new ("http://www.cuteoverload.com";, 7, EPHY_PAGE_VISIT_TYPED));
+    visits = g_list_append (visits, ephy_history_page_visit_new ("http://www.cuteoverload.com";, 8, EPHY_PAGE_VISIT_TYPED));
+  }
+  return visits;
+}
+
+static void
+verify_create_history_entry_cb (EphyHistoryService *service, gboolean success, gpointer result_data, gpointer user_data)
+{
+  GList *visits = (GList *) result_data;
+  GList *baseline_visits = create_test_page_visit_list ();
+  GList *current = visits;
+  GList *current_baseline = baseline_visits;
+
+  g_assert (user_data == NULL);
+  g_assert (success);
+  g_assert (visits != NULL);
+  g_assert_cmpint (g_list_length (visits), ==, g_list_length (baseline_visits));
+
+  while (current_baseline) {
+    EphyHistoryPageVisit *visit, *baseline_visit;
+
+    g_assert (current);
+    visit = (EphyHistoryPageVisit *) current->data;
+    baseline_visit = (EphyHistoryPageVisit *) current_baseline->data;
+
+    g_assert_cmpstr (visit->url->url, ==, baseline_visit->url->url);
+    g_assert_cmpstr (visit->url->title, ==, baseline_visit->url->title);
+    g_assert_cmpint (visit->visit_time, ==, baseline_visit->visit_time);
+    g_assert_cmpint (visit->visit_type, ==, baseline_visit->visit_type);
+
+    current = current->next;
+    current_baseline = current_baseline->next;
+  }
+
+  ephy_history_page_visit_list_free (visits);
+  ephy_history_page_visit_list_free (baseline_visits);
+
+  g_object_unref (service);
+  gtk_main_quit ();
+}
+
+static void
+verify_create_history_entry (EphyHistoryService *service, gboolean success, gpointer result_data, gpointer user_data)
+{
+  g_assert (result_data == NULL);
+  g_assert_cmpint (42, ==, GPOINTER_TO_INT(user_data)); 
+  g_assert (success);
+  ephy_history_service_find_visits_in_time (service, 0, 8, verify_create_history_entry_cb, NULL);
+}
+
+static void
+test_create_history_entries (void)
+{
+  gchar *temporary_file = g_build_filename (g_get_tmp_dir (), "epiphany-history-test.db", NULL);
+  EphyHistoryService *service = ensure_empty_history(temporary_file);
+
+  GList *visits = create_test_page_visit_list ();
+
+  /* We use 42 here just to verify that user_data is passed properly to the callback */
+  ephy_history_service_add_visits (service, visits, verify_create_history_entry, GINT_TO_POINTER(42));
+  ephy_history_page_visit_list_free (visits);
+  g_free (temporary_file);
+
+  gtk_main ();
+}
+
+static void
+get_url (EphyHistoryService *service, gboolean success, gpointer result_data, gpointer user_data)
+{
+  EphyHistoryURL *url = (EphyHistoryURL *) result_data;
+
+  g_assert (success == TRUE);
+  g_assert (url != NULL);
+  g_assert_cmpstr (url->title, ==, "GNOME");
+
+  ephy_history_url_free (url);
+  g_object_unref (service);
+  gtk_main_quit();
+}
+
+static void
+set_url_title (EphyHistoryService *service, gboolean success, gpointer result_data, gpointer user_data)
+{
+  gboolean test_result = GPOINTER_TO_INT (user_data);
+  g_assert (success == TRUE);
+
+  if (test_result == FALSE) {
+    g_object_unref (service);
+    gtk_main_quit ();
+  } else
+    ephy_history_service_get_url (service, "http://www.gnome.org";, get_url, NULL);
+}
+
+static void
+set_url_title_visit_created (EphyHistoryService *service, gboolean success, gpointer result_data, gpointer user_data)
+{
+  ephy_history_service_set_url_title (service, "http://www.gnome.org";, "GNOME", set_url_title, user_data);
+}
+
+static void
+test_set_url_title_helper (gboolean test_results)
+{
+  gchar *temporary_file = g_build_filename (g_get_tmp_dir (), "epiphany-history-test.db", NULL);
+  EphyHistoryService *service = ensure_empty_history(temporary_file);
+
+  EphyHistoryPageVisit *visit = ephy_history_page_visit_new ("http://www.gnome.org";, 0, EPHY_PAGE_VISIT_TYPED);
+  ephy_history_service_add_visit (service, visit, set_url_title_visit_created, GINT_TO_POINTER (test_results));
+  ephy_history_page_visit_free (visit);
+  g_free (temporary_file);
+
+  gtk_main ();
+}
+
+static void
+test_set_url_title (void)
+{
+  test_set_url_title_helper (FALSE);
+}
+
+static void
+test_set_url_title_is_correct (void)
+{
+  test_set_url_title_helper (TRUE);
+}
+
+static void
+set_url_title_url_not_existent (EphyHistoryService *service, gboolean success, gpointer result_data, gpointer user_data)
+{
+  g_assert (success == FALSE);
+  g_object_unref (service);
+  gtk_main_quit ();
+}
+
+static void
+test_set_url_title_url_not_existent (void)
+{
+  gchar *temporary_file = g_build_filename (g_get_tmp_dir (), "epiphany-history-test.db", NULL);
+  EphyHistoryService *service = ensure_empty_history(temporary_file);
+  g_free (temporary_file);
+
+  ephy_history_service_set_url_title (service, "http://www.gnome.org";, "GNOME", set_url_title_url_not_existent, NULL);
+
+  gtk_main();
+}
+
+static void
+test_get_url_done (EphyHistoryService *service, gboolean success, gpointer result_data, gpointer user_data)
+{
+  EphyHistoryURL *url;
+  gboolean expected_success = GPOINTER_TO_INT (user_data);
+
+  url = (EphyHistoryURL *)result_data;
+
+  g_assert (success == expected_success);
+
+  if (expected_success == TRUE) {
+    g_assert (url != NULL);
+    g_assert_cmpstr (url->url, ==, "http://www.gnome.org";);
+    g_assert_cmpint (url->id, !=, -1);
+    ephy_history_url_free (url);
+  } else
+    g_assert (url == NULL);
+
+  g_object_unref (service);
+  gtk_main_quit ();
+}
+
+static void
+test_get_url_visit_added (EphyHistoryService *service, gboolean success, gpointer result_data, gpointer user_data)
+{
+  g_assert (success == TRUE);
+
+  ephy_history_service_get_url (service, "http://www.gnome.org";, test_get_url_done, user_data);
+}
+
+static void
+test_get_url_helper (gboolean add_entry)
+{
+  gchar *temporary_file = g_build_filename (g_get_tmp_dir (), "epiphany-history-test.db", NULL);
+  EphyHistoryService *service = ensure_empty_history(temporary_file);
+  g_free (temporary_file);
+
+  if (add_entry == TRUE) {
+    EphyHistoryPageVisit *visit = ephy_history_page_visit_new ("http://www.gnome.org";, 0, EPHY_PAGE_VISIT_TYPED);
+    ephy_history_service_add_visit (service, visit, test_get_url_visit_added, GINT_TO_POINTER (add_entry));
+    ephy_history_page_visit_free (visit);
+  } else
+    ephy_history_service_get_url (service, "http://www.gnome.org";, test_get_url_done, GINT_TO_POINTER (add_entry));
+
+  gtk_main();
+}
+
+static void
+test_get_url (void)
+{
+  test_get_url_helper (TRUE);
+}
+
+static void
+test_get_url_not_existent (void)
+{
+  test_get_url_helper (FALSE);
+}
+
+int
+main (int argc, char *argv[])
+{
+  gtk_test_init (&argc, &argv);
+
+  g_test_add_func ("/embed/history/test_create_history_service", test_create_history_service);
+  g_test_add_func ("/embed/history/test_create_history_service_and_destroy_later", test_create_history_service_and_destroy_later);
+  g_test_add_func ("/embed/history/test_create_history_entry", test_create_history_entry);
+  g_test_add_func ("/embed/history/test_create_history_entries", test_create_history_entries);
+  g_test_add_func ("/embed/history/test_set_url_title", test_set_url_title);
+  g_test_add_func ("/embed/history/test_set_url_title_is_correct", test_set_url_title_is_correct);
+  g_test_add_func ("/embed/history/test_set_url_title_url_not_existent", test_set_url_title_url_not_existent);
+  g_test_add_func ("/embed/history/test_get_url", test_get_url);
+  g_test_add_func ("/embed/history/test_get_url_not_existent", test_get_url_not_existent);
+
+  return g_test_run ();
+}



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