[evolution-data-server/wip/offline-cache] An ECache base structure



commit 71f43aaf87cae36cb4a52b2977094159079d25ba
Author: Milan Crha <mcrha redhat com>
Date:   Wed Jan 25 11:34:02 2017 +0100

    An ECache base structure

 po/POTFILES.in                    |    1 +
 src/libebackend/CMakeLists.txt    |    2 +
 src/libebackend/e-backend-enums.h |   43 +
 src/libebackend/e-cache.c         | 2324 +++++++++++++++++++++++++++++++++++++
 src/libebackend/e-cache.h         |  388 ++++++
 src/libebackend/libebackend.h     |    1 +
 6 files changed, 2759 insertions(+), 0 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 4cdf92c..7ea5e94 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -176,6 +176,7 @@ data/org.gnome.evolution-data-server.calendar.gschema.xml.in
 data/org.gnome.evolution-data-server.gschema.xml.in
 data/org.gnome.evolution.shell.network-config.gschema.xml.in
 src/libebackend/e-backend.c
+src/libebackend/e-cache.c
 src/libebackend/e-collection-backend.c
 src/libebackend/e-data-factory.c
 src/libebackend/e-server-side-source.c
diff --git a/src/libebackend/CMakeLists.txt b/src/libebackend/CMakeLists.txt
index f328f0b..844bcbc 100644
--- a/src/libebackend/CMakeLists.txt
+++ b/src/libebackend/CMakeLists.txt
@@ -10,6 +10,7 @@ set(DEPENDENCIES
 set(SOURCES
        e-backend.c
        e-backend-factory.c
+       e-cache.c
        e-cache-reaper.c
        e-cache-reaper-utils.c
        e-cache-reaper-utils.h
@@ -36,6 +37,7 @@ set(HEADERS
        e-backend.h
        e-backend-enums.h
        e-backend-factory.h
+       e-cache.h
        e-cache-reaper.h
        e-collection-backend.h
        e-collection-backend-factory.h
diff --git a/src/libebackend/e-backend-enums.h b/src/libebackend/e-backend-enums.h
index 9b53344..5c36569 100644
--- a/src/libebackend/e-backend-enums.h
+++ b/src/libebackend/e-backend-enums.h
@@ -82,4 +82,47 @@ typedef enum { /*< flags >*/
        E_SOURCE_PERMISSION_REMOVABLE = 1 << 1
 } ESourcePermissionFlags;
 
+/**
+ * EOfflineState:
+ * @E_OFFLINE_STATE_UNKNOWN: Unknown offline state.
+ * @E_OFFLINE_STATE_SYNCED: The object if synchnized with no local changes.
+ * @E_OFFLINE_STATE_LOCALLY_CREATED: The object is locally created.
+ * @E_OFFLINE_STATE_LOCALLY_MODIFIED: The object is locally modified.
+ * @E_OFFLINE_STATE_LOCALLY_DELETED: The object is locally deleted.
+ *
+ * Defines offline state of an object. Locally changed objects require
+ * synchronization with their remote storage.
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+       E_OFFLINE_STATE_UNKNOWN = -1,
+       E_OFFLINE_STATE_SYNCED,
+       E_OFFLINE_STATE_LOCALLY_CREATED,
+       E_OFFLINE_STATE_LOCALLY_MODIFIED,
+       E_OFFLINE_STATE_LOCALLY_DELETED
+} EOfflineState;
+
+/**
+ * EConflictResolution:
+ * @E_CONFLICT_RESOLUTION_FAIL: Fail when a write-conflict occurs.
+ * @E_CONFLICT_RESOLUTION_USE_NEWER: Use newer version of the object,
+ *    which can be either the server version or the local version of it.
+ * @E_CONFLICT_RESOLUTION_KEEP_SERVER: Keep server object on conflict.
+ * @E_CONFLICT_RESOLUTION_KEEP_LOCAL: Write local version of the object on conflict.
+ * @E_CONFLICT_RESOLUTION_WRITE_COPY: Create a new copy of the object on conflict.
+ *
+ * Defines what to do when a conflict between the locally stored and
+ * remotely stored object versions happen during object modify or remove.
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+       E_CONFLICT_RESOLUTION_FAIL = 0,
+       E_CONFLICT_RESOLUTION_USE_NEWER,
+       E_CONFLICT_RESOLUTION_KEEP_SERVER,
+       E_CONFLICT_RESOLUTION_KEEP_LOCAL,
+       E_CONFLICT_RESOLUTION_WRITE_COPY
+} EConflictResolution;
+
 #endif /* E_BACKEND_ENUMS_H */
diff --git a/src/libebackend/e-cache.c b/src/libebackend/e-cache.c
new file mode 100644
index 0000000..307901c
--- /dev/null
+++ b/src/libebackend/e-cache.c
@@ -0,0 +1,2324 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * 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.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <errno.h>
+#include <sqlite3.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+
+#include <camel/camel.h>
+
+#include "e-sqlite3-vfs.h"
+
+#include "e-cache.h"
+
+#define E_CACHE_KEY_VERSION    "version"
+#define E_CACHE_KEY_REVISION   "revision"
+
+/* The number of SQLite virtual machine instructions that are
+ * evaluated at a time, the user passed GCancellable is
+ * checked between each batch of evaluated instructions.
+ */
+#define E_CACHE_CANCEL_BATCH_SIZE      200
+
+struct _ECachePrivate {
+       gchar *filename;
+       sqlite3 *db;
+
+       GRecMutex lock;                 /* Main API lock */
+       guint32 in_transaction;         /* Nested transaction counter */
+       ECacheLockType lock_type;       /* The lock type acquired for the current transaction */
+       GCancellable *cancellable;      /* User passed GCancellable, we abort an operation if cancelled */
+};
+
+enum {
+       BEFORE_PUT,
+       BEFORE_REMOVE,
+       LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_QUARK (e-cache-error-quark, e_cache_error)
+
+G_DEFINE_ABSTRACT_TYPE (ECache, e_cache, G_TYPE_OBJECT)
+
+G_DEFINE_BOXED_TYPE (ECacheOfflineChange, e_cache_offline_change, e_cache_offline_change_copy, 
e_cache_offline_change_free)
+G_DEFINE_BOXED_TYPE (ECacheColumnInfo, e_cache_column_info, e_cache_column_info_copy, 
e_cache_column_info_free)
+
+/**
+ * e_cache_offline_change_new:
+ * @uid: a unique object identifier
+ * @state: an #EOfflineState
+ *
+ * Creates a new #ECacheOfflineChange with the offline @state
+ * information for the given @uid.
+ *
+ * Returns: (transfer full): A new #ECacheOfflineChange. Free it with
+ *    e_cache_offline_change_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECacheOfflineChange *
+e_cache_offline_change_new (const gchar *uid,
+                           EOfflineState state)
+{
+       ECacheOfflineChange *change;
+
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       change = g_new0 (ECacheOfflineChange, 1);
+       change->uid = g_strdup (uid);
+       change->state = state;
+
+       return change;
+}
+
+/**
+ * e_cache_offline_change_copy:
+ * @change: (nullable): a source #ECacheOfflineChange to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @change. Free it with
+ *    e_cache_offline_change_free() when no longer needed.
+ *    If the @change is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECacheOfflineChange *
+e_cache_offline_change_copy (const ECacheOfflineChange *change)
+{
+       if (!change)
+               return NULL;
+
+       return e_cache_offline_change_new (change->uid, change->state);
+}
+
+/**
+ * e_cache_offline_change_free:
+ * @change: (nullable): an #ECacheOfflineChange
+ *
+ * Frees the @change structure, previously allocated with e_cache_offline_change_new()
+ * or e_cache_offline_change_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_offline_change_free (gpointer change)
+{
+       ECacheOfflineChange *chng = change;
+
+       if (chng) {
+               g_free (chng->uid);
+               g_free (chng);
+       }
+}
+
+/**
+ * e_cache_column_info_new:
+ * @name: a column name
+ * @type: a column type
+ * @index_name: (nullable): an index name for this column, or %NULL
+ *
+ * Returns: (transfer full): A new #ECacheColumnInfo. Free it with
+ *    e_cache_column_info_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECacheColumnInfo *
+e_cache_column_info_new (const gchar *name,
+                        const gchar *type,
+                        const gchar *index_name)
+{
+       ECacheColumnInfo *info;
+
+       g_return_val_if_fail (name != NULL, NULL);
+       g_return_val_if_fail (type != NULL, NULL);
+
+       info = g_new0 (ECacheColumnInfo, 1);
+       info->name = g_strdup (name);
+       info->type = g_strdup (type);
+       info->index_name = g_strdup (index_name);
+
+       return info;
+}
+
+/**
+ * e_cache_column_info_copy:
+ * @info: (nullable): a source #ECacheColumnInfo to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @info. Free it with
+ *    e_cache_column_info_free() when no longer needed.
+ *    If the @info is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECacheColumnInfo *
+e_cache_column_info_copy (const ECacheColumnInfo *info)
+{
+       if (!info)
+               return NULL;
+
+       return e_cache_column_info_new (info->name, info->type, info->index_name);
+}
+
+/**
+ * e_cache_column_info_free:
+ * @info: (nullable): an #ECacheColumnInfo
+ *
+ * Frees the @info structure, previously allocated with e_cache_column_info_new()
+ * or e_cache_column_info_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_info_free (gpointer info)
+{
+       ECacheColumnInfo *nfo = info;
+
+       if (nfo) {
+               g_free (nfo->name);
+               g_free (nfo->type);
+               g_free (nfo->index_name);
+               g_free (nfo);
+       }
+}
+
+#define E_CACHE_SET_ERROR_FROM_SQLITE(error, code, message) \
+       G_STMT_START { \
+               if (code == SQLITE_CONSTRAINT) { \
+                       g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_CONSTRAINT, message); \
+               } else if (code == SQLITE_ABORT) { \
+                       g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Operation cancelled: %s", 
message); \
+               } else { \
+                       g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE, \
+                               "SQLite error code '%d': %s", code, message); \
+               } \
+       } G_STMT_END
+
+struct CacheSQLiteExecData {
+       ECache *cache;
+       ECacheSelectFunc callback;
+       gpointer user_data;
+};
+
+static gint
+e_cache_sqlite_exec_cb (gpointer user_data,
+                       gint ncols,
+                       gchar **column_values,
+                       gchar **column_names)
+{
+       struct CacheSQLiteExecData *cse = user_data;
+
+       g_return_val_if_fail (cse != NULL, SQLITE_MISUSE);
+       g_return_val_if_fail (cse->callback != NULL, SQLITE_MISUSE);
+
+       if (!cse->callback (cse->cache, ncols, (const gchar **) column_names, (const gchar **) column_values, 
cse->user_data))
+               return SQLITE_ABORT;
+
+       return SQLITE_OK;
+}
+
+static gboolean
+e_cache_sqlite_exec_internal (ECache *cache,
+                             const gchar *stmt,
+                             ECacheSelectFunc callback,
+                             gpointer user_data,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       struct CacheSQLiteExecData cse;
+       GCancellable *previous_cancellable;
+       gchar *errmsg = NULL;
+       gint ret = -1, retries = 0;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (stmt != NULL, FALSE);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       previous_cancellable = cache->priv->cancellable;
+       if (cancellable)
+               cache->priv->cancellable = cancellable;
+
+       cse.cache = cache;
+       cse.callback = callback;
+       cse.user_data = user_data;
+
+       ret = sqlite3_exec (cache->priv->db, stmt, callback ? e_cache_sqlite_exec_cb : NULL, &cse, &errmsg);
+
+       while (ret == SQLITE_BUSY || ret == SQLITE_LOCKED || ret == -1) {
+               /* try for ~15 seconds, then give up */
+               if (retries > 150)
+                       break;
+               retries++;
+
+               if (errmsg) {
+                       sqlite3_free (errmsg);
+                       errmsg = NULL;
+               }
+               g_thread_yield ();
+               g_usleep (100 * 1000); /* Sleep for 100 ms */
+
+               ret = sqlite3_exec (cache->priv->db, stmt, callback ? e_cache_sqlite_exec_cb : NULL, &cse, 
&errmsg);
+       }
+
+       cache->priv->cancellable = previous_cancellable;
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       if (ret != SQLITE_OK) {
+               E_CACHE_SET_ERROR_FROM_SQLITE (error, ret, errmsg);
+               sqlite3_free (errmsg);
+               return FALSE;
+       }
+
+       if (errmsg)
+               sqlite3_free (errmsg);
+
+       return TRUE;
+}
+
+static gboolean
+e_cache_sqlite_exec_printf (ECache *cache,
+                           const gchar *format,
+                           ECacheSelectFunc callback,
+                           gpointer user_data,
+                           GCancellable *cancellable,
+                           GError **error,
+                           ...)
+{
+       gboolean success;
+       va_list args;
+       gchar *stmt;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (format != NULL, FALSE);
+
+       va_start (args, error);
+       stmt = sqlite3_vmprintf (format, args);
+
+       success = e_cache_sqlite_exec_internal (cache, stmt, callback, user_data, cancellable, error);
+
+       sqlite3_free (stmt);
+       va_end (args);
+
+       return success;
+}
+
+static gboolean
+e_cache_read_key_value (ECache *cache,
+                       gint ncols,
+                       const gchar **column_names,
+                       const gchar **column_values,
+                       gpointer user_data)
+{
+       gchar **pvalue = user_data;
+
+       g_return_val_if_fail (ncols != 1, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+       g_return_val_if_fail (pvalue != NULL, FALSE);
+
+       if (!*pvalue)
+               *pvalue = g_strdup (column_values[0]);
+
+       return TRUE;
+}
+
+static gchar *
+e_cache_build_user_key (const gchar *key)
+{
+       return g_strconcat ("user::", key, NULL);
+}
+
+static gboolean
+e_cache_set_key_internal (ECache *cache,
+                         gboolean is_user_key,
+                         const gchar *key,
+                         const gchar *value,
+                         GError **error)
+{
+       gchar *tmp = NULL;
+       const gchar *usekey;
+       gboolean success;
+
+       if (is_user_key) {
+               tmp = e_cache_build_user_key (key);
+               usekey = tmp;
+       } else {
+               usekey = key;
+       }
+
+       if (value) {
+               success = e_cache_sqlite_exec_printf (cache,
+                       "INSERT or REPLACE INTO " E_CACHE_TABLE_KEYS " (key, value) values (%Q, %Q)",
+                       NULL, NULL, NULL, error,
+                       usekey, value);
+       } else {
+               success = e_cache_sqlite_exec_printf (cache,
+                       "DELETE FROM " E_CACHE_TABLE_KEYS " WHERE key = %Q",
+                       NULL, NULL, NULL, error,
+                       usekey);
+       }
+
+       g_free (tmp);
+
+       return success;
+}
+
+static gchar *
+e_cache_dup_key_internal (ECache *cache,
+                         gboolean is_user_key,
+                         const gchar *key,
+                         GError **error)
+{
+       gchar *tmp = NULL;
+       const gchar *usekey;
+       gchar *value = NULL;
+
+       if (is_user_key) {
+               tmp = e_cache_build_user_key (key);
+               usekey = tmp;
+       } else {
+               usekey = key;
+       }
+
+       if (!e_cache_sqlite_exec_printf (cache,
+               "SELECT value FROM " E_CACHE_TABLE_KEYS " WHERE key = %Q",
+               e_cache_read_key_value, &value, NULL, error,
+               usekey)) {
+               g_warn_if_fail (value == NULL);
+       }
+
+       g_free (tmp);
+
+       return value;
+}
+
+static gint
+e_cache_check_cancelled_cb (gpointer user_data)
+{
+       ECache *cache = user_data;
+
+       /* Do not use E_IS_CACHE() here, for performance reasons */
+       g_return_val_if_fail (cache != NULL, SQLITE_ABORT);
+
+       if (cache->priv->cancellable &&
+           g_cancellable_is_cancelled (cache->priv->cancellable)) {
+               return SQLITE_ABORT;
+       }
+
+       return SQLITE_OK;
+}
+
+static gboolean
+e_cache_init_sqlite (ECache *cache,
+                    const gchar *filename,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       gint ret;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (filename != NULL, FALSE);
+       g_return_val_if_fail (cache->priv->filename == NULL, FALSE);
+
+       cache->priv->filename = g_strdup (filename);
+
+       ret = sqlite3_open (filename, &cache->priv->db);
+       if (ret != SQLITE_OK) {
+               if (!cache->priv->db) {
+                       g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_LOAD, _("Out of memory"));
+               } else {
+                       const gchar *errmsg = sqlite3_errmsg (cache->priv->db);
+
+                       g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE,
+                               _("Can't open database %s: %s"), filename, errmsg);
+
+                       sqlite3_close (cache->priv->db);
+                       cache->priv->db = NULL;
+               }
+
+               return FALSE;
+       }
+
+       /* Handle GCancellable */
+       sqlite3_progress_handler (
+               cache->priv->db,
+               E_CACHE_CANCEL_BATCH_SIZE,
+               e_cache_check_cancelled_cb,
+               cache);
+
+       return e_cache_sqlite_exec_internal (cache, "ATTACH DATABASE ':memory:' AS mem", NULL, NULL, 
cancellable, error) &&
+               e_cache_sqlite_exec_internal (cache, "PRAGMA foreign_keys = ON",          NULL, NULL, 
cancellable, error) &&
+               e_cache_sqlite_exec_internal (cache, "PRAGMA case_sensitive_like = ON",   NULL, NULL, 
cancellable, error);
+}
+
+static gboolean
+e_cache_garther_column_names_cb (ECache *cache,
+                                gint ncols,
+                                const gchar *column_names[],
+                                const gchar *column_values[],
+                                gpointer user_data)
+{
+       GHashTable *known_columns = user_data;
+       gint ii;
+
+       g_return_val_if_fail (known_columns != NULL, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+
+       for (ii = 0; ii < ncols; ii++) {
+               if (column_names[ii] && camel_strcase_equal (column_names[ii], "name")) {
+                       if (column_values[ii])
+                               g_hash_table_insert (known_columns, g_strdup (column_values[ii]), NULL);
+                       break;
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean
+e_cache_init_tables (ECache *cache,
+                    const GSList *other_columns,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       GHashTable *known_columns;
+       GString *objects_stmt;
+       const GSList *link;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (cache->priv->db != NULL, FALSE);
+
+       if (!e_cache_sqlite_exec_internal (cache,
+               "CREATE TABLE IF NOT EXISTS " E_CACHE_TABLE_KEYS " ("
+               "key TEXT PRIMARY INDEX,"
+               "value TEXT)",
+               NULL, NULL, cancellable, error)) {
+               return FALSE;
+       }
+
+       objects_stmt = g_string_new ("");
+
+       g_string_append (objects_stmt, "CREATE TABLE IF NOT EXISTS " E_CACHE_TABLE_OBJECTS " ("
+               E_CACHE_COLUMN_UID " TEXT PRIMARY INDEX,"
+               E_CACHE_COLUMN_REVISION " TEXT,"
+               E_CACHE_COLUMN_OBJECT " TEXT,"
+               E_CACHE_COLUMN_STATE " INTEGER");
+
+       for (link = other_columns; link; link = g_slist_next (link)) {
+               const ECacheColumnInfo *info = link->data;
+
+               if (!info)
+                       continue;
+
+               g_string_append (objects_stmt, ",");
+               g_string_append (objects_stmt, info->name);
+               g_string_append (objects_stmt, " ");
+               g_string_append (objects_stmt, info->type);
+       }
+
+       g_string_append (objects_stmt, ")");
+
+       if (!e_cache_sqlite_exec_internal (cache, objects_stmt->str, NULL, NULL, cancellable, error)) {
+               g_string_free (objects_stmt, TRUE);
+
+               return FALSE;
+       }
+
+       g_string_free (objects_stmt, TRUE);
+
+       /* Verify that all other columns are there and remove those unused */
+       known_columns = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, NULL);
+
+       if (!e_cache_sqlite_exec_internal (cache, "PRAGMA table_info (" E_CACHE_TABLE_OBJECTS ")",
+               e_cache_garther_column_names_cb, known_columns, cancellable, error)) {
+               g_string_free (objects_stmt, TRUE);
+
+               return FALSE;
+       }
+
+       g_hash_table_remove (known_columns, E_CACHE_COLUMN_UID);
+       g_hash_table_remove (known_columns, E_CACHE_COLUMN_REVISION);
+       g_hash_table_remove (known_columns, E_CACHE_COLUMN_OBJECT);
+       g_hash_table_remove (known_columns, E_CACHE_COLUMN_STATE);
+
+       for (link = other_columns; link; link = g_slist_next (link)) {
+               const ECacheColumnInfo *info = link->data;
+
+               if (!info)
+                       continue;
+
+               if (g_hash_table_remove (known_columns, info->name))
+                       continue;
+
+               if (!e_cache_sqlite_exec_printf (cache,
+                       "ALTER TABLE " E_CACHE_TABLE_OBJECTS " ADD COLUMN %Q %s",
+                       NULL, NULL, cancellable, error,
+                       info->name, info->type)) {
+                       g_hash_table_destroy (known_columns);
+
+                       return FALSE;
+               }
+       }
+
+       g_hash_table_destroy (known_columns);
+
+       for (link = other_columns; link; link = g_slist_next (link)) {
+               const ECacheColumnInfo *info = link->data;
+
+               if (!info || !info->index_name)
+                       continue;
+
+               if (!e_cache_sqlite_exec_printf (cache,
+                       "CREATE INDEX IF NOT EXISTS %Q ON " E_CACHE_TABLE_OBJECTS " (%s)",
+                       NULL, NULL, cancellable, error,
+                       info->index_name, info->name)) {
+                       return FALSE;
+               }
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_cache_initialize_sync:
+ * @cache: an #ECache
+ * @filename: a filename of an SQLite database to use
+ * @other_columns: (element-type ECacheColumnInfo) (nullable): an optional
+ *    #GSList with additional columns to add to the objects table
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Initializes the @cache and opens the @filename database.
+ * This should be called by the descendant.
+ *
+ * The @other_columns are added to the objects table (@E_CACHE_TABLE_OBJECTS).
+ * Values for these columns are returned by e_cache_get()
+ * and can be stored with e_cache_put().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_initialize_sync (ECache *cache,
+                        const gchar *filename,
+                        const GSList *other_columns,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       gchar *dirname;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (cache->priv->filename == NULL, FALSE);
+
+       /* Ensure existance of the directories leading up to 'filename' */
+       dirname = g_path_get_dirname (filename);
+       if (g_mkdir_with_parents (dirname, 0777) < 0) {
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_LOAD,
+                       _("Can not make parent directory: %s"),
+                       g_strerror (errno));
+               g_free (dirname);
+
+               return FALSE;
+       }
+
+       g_free (dirname);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       success = e_cache_init_sqlite (cache, filename, cancellable, error) &&
+               e_cache_init_tables (cache, other_columns, cancellable, error);
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       return success;
+}
+
+/**
+ * e_cache_get_filename:
+ * @cache: an #ECache
+ *
+ * Returns: a filename of the @cache, with which it had been initialized.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_cache_get_filename (ECache *cache)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+       return cache->priv->filename;
+}
+
+/**
+ * e_cache_get_version:
+ * @cache: an #ECache
+ *
+ * Returns: A cache data version. This is meant to be used by the descendants.
+ *
+ * Since: 3.26
+ **/
+gint
+e_cache_get_version (ECache *cache)
+{
+       gchar *value;
+       gint version = -1;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), -1);
+
+       value = e_cache_dup_key_internal (cache, FALSE, E_CACHE_KEY_VERSION, NULL);
+
+       if (value) {
+               version = g_ascii_strtoll (value, NULL, 10);
+               g_free (value);
+       }
+
+       return version;
+}
+
+/**
+ * e_cache_set_version:
+ * @cache: an #ECache
+ * @version: a cache data version to set
+ *
+ * Sets a cache data version. This is meant to be used by the descendants.
+ * The @version should be greater than zero.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_set_version (ECache *cache,
+                    gint version)
+{
+       gchar *value;
+
+       g_return_if_fail (E_IS_CACHE (cache));
+       g_return_if_fail (version > 0);
+
+       value = g_strdup_printf ("%d", version);
+       e_cache_set_key_internal (cache, FALSE, E_CACHE_KEY_VERSION, value, NULL);
+       g_free (value);
+}
+
+/**
+ * e_cache_dup_revision:
+ * @cache: an #ECache
+ *
+ * Returns: (transfer full): A revision of the whole @cache. This is meant to be
+ *    used by the descendants. Free the returned pointer with g_free(), when no
+ *    longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_dup_revision (ECache *cache)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+       return e_cache_dup_key_internal (cache, FALSE, E_CACHE_KEY_REVISION, NULL);
+}
+
+/**
+ * e_cache_set_revision:
+ * @cache: an #ECache
+ * @revision: (nullable): a revision to set; use %NULL to unset it
+ *
+ * Sets the @revision of the whole @cache. This is meant to be
+ * used by the descendants.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_set_revision (ECache *cache,
+                     const gchar *revision)
+{
+       g_return_if_fail (E_IS_CACHE (cache));
+
+       e_cache_set_key_internal (cache, FALSE, E_CACHE_KEY_REVISION, revision, NULL);
+}
+
+/**
+ * e_cache_erase:
+ * @cache: an #ECache
+ *
+ * Erases the cache and all of its content from the disk.
+ * The only valid operation after this is to free the @cache.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_erase (ECache *cache)
+{
+       ECacheClass *class;
+
+       g_return_if_fail (E_IS_CACHE (cache));
+
+       if (!cache->priv->db)
+               return;
+
+       class = E_CACHE_GET_CLASS (cache);
+       g_return_if_fail (class != NULL);
+
+       if (class->erase)
+               class->erase (cache);
+
+       sqlite3_close (cache->priv->db);
+       cache->priv->db = NULL;
+
+       g_unlink (cache->priv->filename);
+
+       g_free (cache->priv->filename);
+       cache->priv->filename = NULL;
+}
+
+static gboolean
+e_cache_count_rows_cb (ECache *cache,
+                      gint ncols,
+                      const gchar **column_names,
+                      const gchar **column_values,
+                      gpointer user_data)
+{
+       guint *pnrows = user_data;
+
+       g_return_val_if_fail (pnrows != NULL, FALSE);
+
+       *pnrows = (*pnrows) + 1;
+
+       return TRUE;
+}
+
+static gboolean
+e_cache_contains_internal (ECache *cache,
+                          const gchar *uid,
+                          gboolean include_deleted)
+{
+       guint nrows = 0;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       if (include_deleted) {
+               e_cache_sqlite_exec_printf (cache,
+                       "SELECT " E_CACHE_COLUMN_UID " FROM " E_CACHE_TABLE_OBJECTS
+                       " WHERE " E_CACHE_COLUMN_UID " = %Q",
+                       e_cache_count_rows_cb, &nrows, NULL, NULL,
+                       uid);
+       } else {
+               e_cache_sqlite_exec_printf (cache,
+                       "SELECT " E_CACHE_COLUMN_UID " FROM " E_CACHE_TABLE_OBJECTS
+                       " WHERE " E_CACHE_COLUMN_UID " = %Q AND " E_CACHE_COLUMN_STATE " != %d",
+                       e_cache_count_rows_cb, &nrows, NULL, NULL,
+                       uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+       }
+
+       g_warn_if_fail (nrows <= 1);
+
+       return nrows > 0;
+}
+
+/**
+ * e_cache_contains:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ *
+ * Checkes whether the @cache contains an object with
+ * the given @uid. The objects deleted in offline are
+ * not considered. Use e_cache_get_offline_changes()
+ * to get those.
+ *
+ * Returns: Whether the cache contains an object with the given @uid.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_contains (ECache *cache,
+                 const gchar *uid)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       return e_cache_contains_internal (cache, uid, FALSE);
+}
+
+struct GetObjectData {
+       gchar *object;
+       gchar **out_revision;
+       GHashTable **out_other_columns;
+};
+
+static gboolean
+e_cache_get_object_cb (ECache *cache,
+                      gint ncols,
+                      const gchar **column_names,
+                      const gchar **column_values,
+                      gpointer user_data)
+{
+       struct GetObjectData *gd = user_data;
+       gint ii;
+
+       g_return_val_if_fail (gd != NULL, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+
+       for (ii = 0; ii < ncols; ii++) {
+               if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) == 0 ||
+                   g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_STATE) == 0) {
+                       /* Skip these two */
+               } else if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_REVISION) == 0) {
+                       if (gd->out_revision)
+                               *gd->out_revision = g_strdup (column_values[ii]);
+               } else if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_OBJECT) == 0) {
+                       gd->object = g_strdup (column_values[ii]);
+               } else if (gd->out_other_columns) {
+                       if (!*gd->out_other_columns)
+                               *gd->out_other_columns = g_hash_table_new_full (camel_strcase_hash, 
camel_strcase_equal, g_free, g_free);
+
+                       g_hash_table_insert (*gd->out_other_columns, g_strdup (column_names[ii]), g_strdup 
(column_values[ii]));
+               } else if (gd->object && (!gd->out_revision || *gd->out_revision)) {
+                       /* Short-break the cycle when the other columns are not requested and
+                          the object/revision values were already read. */
+                       break;
+               }
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_cache_get:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @out_revision: (out) (nullable) (transfer full): an out variable for a revision
+ *    of the object, or %NULL to ignore
+ * @out_other_columns: (out) (nullable) (transfer full) (element-type utf8 utf8): an out
+ *    variable for other columns, as defined when crating the @cache, or %NULL to ignore
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns an object with the given @uid. This function does not consider locally
+ * deleted objects. The @out_revision is set to the object revision, if not %NULL.
+ * Free it with g_free() when no longer needed. Similarly the @out_other_columns
+ * contains a column name to column value strings for additional columns which had
+ * been requested when calling e_cache_initialize_sync(), if not %NULL.
+ * Free the returned #GHashTable with g_hash_table_unref(), when no longer needed.
+ *
+ * Returns: (nullable) (transfer full): An object with the given @uid. Free it
+ *    with g_free(), when no longer needed. Returns %NULL on error, like when
+ *    the object could not be found.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_get (ECache *cache,
+            const gchar *uid,
+            gchar **out_revision,
+            GHashTable **out_other_columns,
+            GCancellable *cancellable,
+            GError **error)
+{
+       struct GetObjectData gd;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       if (out_revision)
+               *out_revision = NULL;
+
+       if (out_other_columns)
+               *out_other_columns = NULL;
+
+       gd.object = NULL;
+       gd.out_revision = out_revision;
+       gd.out_other_columns = out_other_columns;
+
+       if (e_cache_sqlite_exec_printf (cache,
+               "SELECT * FROM " E_CACHE_TABLE_OBJECTS
+               " WHERE " E_CACHE_COLUMN_UID " = %Q AND " E_CACHE_COLUMN_STATE " != %d",
+               e_cache_get_object_cb, &gd, cancellable, error,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED) &&
+           !gd.object) {
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object ā€œ%sā€ not found"), uid);
+       }
+
+       return gd.object;
+}
+
+static gboolean
+e_cache_put_with_offline_state (ECache *cache,
+                               const gchar *uid,
+                               const gchar *revision,
+                               const gchar *object,
+                               const GHashTable *other_columns,
+                               EOfflineState offline_state,
+                               gboolean is_replace,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       GHashTable *my_other_columns = NULL;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (revision != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       if (!other_columns) {
+               my_other_columns = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, 
g_free);
+               other_columns = my_other_columns;
+       }
+
+       g_signal_emit (cache,
+                      signals[BEFORE_PUT],
+                      0,
+                      uid, revision, object, other_columns,
+                      is_replace, cancellable, error,
+                      &success);
+
+       if (success) {
+               ECacheClass *class;
+
+               class = E_CACHE_GET_CLASS (cache);
+               g_return_val_if_fail (class != NULL, FALSE);
+               g_return_val_if_fail (class->put_locked != NULL, FALSE);
+
+               success = class->put_locked (cache, uid, revision, object, other_columns, offline_state, 
is_replace, cancellable, error);
+       }
+
+       if (my_other_columns)
+               g_hash_table_unref (my_other_columns);
+
+       return success;
+}
+
+/**
+ * e_cache_put:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @revision: a revision of the object
+ * @object: the object itself
+ * @other_columns: (nullable) (element-type utf8 utf8): what other columns to set; can be %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Stores an object into the cache. This sets the object's offline state
+ * to #E_OFFLINE_STATE_SYNCED, like to be fully synchronized with the server,
+ * regardless of its previous offline state. Use e_cache_put_offline() to
+ * add objects in offline mode. Overwriting locally deleted object behaves
+ * like an addition of a completely new object.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_put (ECache *cache,
+            const gchar *uid,
+            const gchar *revision,
+            const gchar *object,
+            GHashTable *other_columns,
+            GCancellable *cancellable,
+            GError **error)
+{
+       gboolean success, is_replace;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (revision != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       is_replace = e_cache_contains_internal (cache, uid, FALSE);
+
+       success = e_cache_put_with_offline_state (cache, uid, revision, object, other_columns,
+               E_OFFLINE_STATE_SYNCED, is_replace, cancellable, error);
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       return success;
+}
+
+/**
+ * e_cache_remove:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes the object with the given @uid from the @cache. It removes also any
+ * information about locally made offline changes. Use e_cache_remove_offline()
+ * to still remember the removed object for later use with e_cache_get_offline_changes().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_remove (ECache *cache,
+               const gchar *uid,
+               GCancellable *cancellable,
+               GError **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       g_signal_emit (cache,
+                      signals[BEFORE_REMOVE],
+                      0,
+                      uid, cancellable, error,
+                      &success);
+
+       success = success && e_cache_sqlite_exec_printf (cache,
+               "DELETE FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_UID " = %Q",
+               NULL, NULL, cancellable, error,
+               uid);
+
+       return success;
+}
+
+/**
+ * e_cache_remove_all:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes all objects from the @cache in one call.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_remove_all (ECache *cache,
+                   GCancellable *cancellable,
+                   GError **error)
+{
+       GSList *uids = NULL, *link;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       success = e_cache_get_uids (cache, TRUE, &uids, NULL, cancellable, error);
+
+       for (link = uids; link && success; link = g_slist_next (link)) {
+               const gchar *uid = link->data;
+
+               g_signal_emit (cache,
+                              signals[BEFORE_REMOVE],
+                              0,
+                              uid, cancellable, error,
+                              &success);
+       }
+
+       if (success) {
+               success = e_cache_sqlite_exec_printf (cache,
+                       "DELETE FROM " E_CACHE_TABLE_OBJECTS,
+                       NULL, NULL, cancellable, error);
+       }
+
+       if (success)
+               e_cache_sqlite_maybe_vacuum (cache, cancellable, NULL);
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       g_slist_free_full (uids, g_free);
+
+       return success;
+}
+
+static gboolean
+e_cache_get_uint64_cb (ECache *cache,
+                      gint ncols,
+                      const gchar **column_names,
+                      const gchar **column_values,
+                      gpointer user_data)
+{
+       guint64 *pui64 = user_data;
+
+       g_return_val_if_fail (pui64 != NULL, FALSE);
+
+       if (ncols == 1) {
+               *pui64 = column_values[0] ? g_ascii_strtoull (column_values[0], NULL, 10) : 0;
+       } else {
+               *pui64 = 0;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+e_cache_get_int64_cb (ECache *cache,
+                     gint ncols,
+                     const gchar **column_names,
+                     const gchar **column_values,
+                     gpointer user_data)
+{
+       gint64 *pi64 = user_data;
+
+       g_return_val_if_fail (pi64 != NULL, FALSE);
+
+       if (ncols == 1) {
+               *pi64 = column_values[0] ? g_ascii_strtoll (column_values[0], NULL, 10) : 0;
+       } else {
+               *pi64 = 0;
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_cache_count:
+ * @cache: an #ECache
+ * @include_deleted: set to %TRUE, when count also objects marked as locally deleted
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: Count of objects stored in the @cache.
+ *
+ * Since: 3.26
+ **/
+guint
+e_cache_count (ECache *cache,
+              gboolean include_deleted,
+              GCancellable *cancellable,
+              GError **error)
+{
+       guint64 nobjects = 0;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), 0);
+
+       if (include_deleted) {
+               e_cache_sqlite_exec_printf (cache,
+                       "SELECT COUNT(*) FROM " E_CACHE_TABLE_OBJECTS,
+                       e_cache_get_uint64_cb, &nobjects, cancellable, error);
+       } else {
+               e_cache_sqlite_exec_printf (cache,
+                       "SELECT COUNT(*) FROM " E_CACHE_TABLE_OBJECTS
+                       " WHERE " E_CACHE_COLUMN_STATE " != %d",
+                       e_cache_get_uint64_cb, &nobjects, NULL, NULL,
+                       E_OFFLINE_STATE_LOCALLY_DELETED);
+       }
+
+       return nobjects;
+}
+
+struct GatherRowsData {
+       GSList **out_uids;
+       GSList **out_revisions;
+       GSList **out_objects;
+};
+
+static gboolean
+e_cache_gather_rows_data_cb (ECache *cache,
+                            const gchar *uid,
+                            const gchar *revision,
+                            const gchar *object,
+                            EOfflineState offline_state,
+                            gint ncols,
+                            const gchar *column_names[],
+                            const gchar *column_values[],
+                            gpointer user_data)
+{
+       struct GatherRowsData *gd = user_data;
+
+       g_return_val_if_fail (gd != NULL, FALSE);
+
+       if (gd->out_uids)
+               *gd->out_uids = g_slist_prepend (*gd->out_uids, g_strdup (uid));
+
+       if (gd->out_revisions)
+               *gd->out_revisions = g_slist_prepend (*gd->out_revisions, g_strdup (revision));
+
+       if (gd->out_objects)
+               *gd->out_objects = g_slist_prepend (*gd->out_objects, g_strdup (object));
+
+       return TRUE;
+}
+
+/**
+ * e_cache_get_uids:
+ * @cache: an #ECache
+ * @include_deleted: set to %TRUE, when consider also objects marked as locally deleted
+ * @out_uids: (out) (transfer full) (element-type utf8): a pointer to #GSList to store the found uid to
+ * @out_revisions: (out) (transfer full) (element-type utf8) (nullable): a pointer to #GSList to store
+ *    the found revisions to, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of unique object identifiers stored in the @cache, optionally
+ * together with their revisions. The uids are not returned in any particular
+ * order, but the position between @out_uids and @out_revisions matches
+ * the same object.
+ *
+ * Both @out_uids and @out_revisions contain newly allocated #GSList, which
+ * should be freed with g_slist_free_full (slist, g_free); when no longer needed.
+ *
+ * Returns: Whether succeeded. It doesn't necessarily mean that there was
+ *    any object stored in the @cache.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_get_uids (ECache *cache,
+                 gboolean include_deleted,
+                 GSList **out_uids,
+                 GSList **out_revisions,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       struct GatherRowsData gr;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (out_uids, FALSE);
+
+       gr.out_uids = out_uids;
+       gr.out_revisions = out_revisions;
+       gr.out_objects = NULL;
+
+       return e_cache_foreach (cache, include_deleted, NULL,
+               e_cache_gather_rows_data_cb, &gr, cancellable, error);
+}
+
+/**
+ * e_cache_get_objects:
+ * @cache: an #ECache
+ * @include_deleted: set to %TRUE, when consider also objects marked as locally deleted
+ * @out_objects: (out) (transfer full) (element-type utf8): a pointer to #GSList to store the found objects 
to
+ * @out_revisions: (out) (transfer full) (element-type utf8) (nullable): a pointer to #GSList to store
+ *    the found revisions to, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of objects stored in the @cache, optionally together with
+ * their revisions. The uids are not returned in any particular order,
+ * but the position between @out_objects and @out_revisions matches
+ * the same object.
+ *
+ * Both @out_objects and @out_revisions contain newly allocated #GSList, which
+ * should be freed with g_slist_free_full (slist, g_free); when no longer needed.
+ *
+ * Returns: Whether succeeded. It doesn't necessarily mean that there was
+ *    any object stored in the @cache.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_get_objects (ECache *cache,
+                    gboolean include_deleted,
+                    GSList **out_objects,
+                    GSList **out_revisions,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       struct GatherRowsData gr;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (out_objects, FALSE);
+
+       gr.out_uids = NULL;
+       gr.out_revisions = out_revisions;
+       gr.out_objects = out_objects;
+
+       return e_cache_foreach (cache, include_deleted, NULL,
+               e_cache_gather_rows_data_cb, &gr, cancellable, error);
+}
+
+struct ForeachData {
+       gint uid_index;
+       gint revision_index;
+       gint object_index;
+       gint state_index;
+       ECacheForeachFunc func;
+       gpointer user_data;
+};
+
+static gboolean
+e_cache_foreach_cb (ECache *cache,
+                   gint ncols,
+                   const gchar *column_names[],
+                   const gchar *column_values[],
+                   gpointer user_data)
+{
+       struct ForeachData *fe = user_data;
+       EOfflineState offline_state;
+
+       g_return_val_if_fail (fe != NULL, FALSE);
+       g_return_val_if_fail (fe->func != NULL, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+
+       if (fe->uid_index == -1 ||
+           fe->revision_index == -1 ||
+           fe->object_index == -1 ||
+           fe->state_index == -1) {
+               gint ii;
+
+               for (ii = 0; ii < ncols && (fe->uid_index == -1 ||
+                    fe->revision_index == -1 ||
+                    fe->object_index == -1 ||
+                    fe->state_index == -1); ii++) {
+                       if (!column_names[ii])
+                               continue;
+
+                       if (fe->uid_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) 
== 0) {
+                               fe->uid_index = ii;
+                       } else if (fe->revision_index == -1 && g_ascii_strcasecmp (column_names[ii], 
E_CACHE_COLUMN_REVISION) == 0) {
+                               fe->revision_index = ii;
+                       } else if (fe->object_index == -1 && g_ascii_strcasecmp (column_names[ii], 
E_CACHE_COLUMN_OBJECT) == 0) {
+                               fe->object_index = ii;
+                       } else if (fe->state_index == -1 && g_ascii_strcasecmp (column_names[ii], 
E_CACHE_COLUMN_STATE) == 0) {
+                               fe->state_index = ii;
+                       }
+               }
+       }
+
+       g_return_val_if_fail (fe->uid_index >= 0 && fe->uid_index < ncols, FALSE);
+       g_return_val_if_fail (fe->revision_index >= 0 && fe->revision_index < ncols, FALSE);
+       g_return_val_if_fail (fe->object_index >= 0 && fe->object_index < ncols, FALSE);
+       g_return_val_if_fail (fe->state_index >= 0 && fe->state_index < ncols, FALSE);
+
+       if (!column_values[fe->state_index])
+               offline_state = E_OFFLINE_STATE_UNKNOWN;
+       else
+               offline_state = g_ascii_strtoull (column_values[fe->state_index], NULL, 10);
+
+       return fe->func (cache, column_values[fe->uid_index], column_values[fe->revision_index], 
column_values[fe->object_index],
+               offline_state, ncols, column_names, column_values, fe->user_data);
+}
+
+/**
+ * e_cache_foreach:
+ * @cache: an #ECache
+ * @include_deleted: set to %TRUE, when consider also objects marked as locally deleted
+ * @where_clause: (nullable): an optional SQLite WHERE clause part, or %NULL
+ * @func: an #ECacheForeachFunc function to call for each object
+ * @user_data: user data for the @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Calls @func for each found object, which satisfies the criteria
+ * for both @include_deleted and @where_clause.
+ *
+ * Note the @func should not call any SQLite commands, because it's invoked
+ * within a SELECT statement execution.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_foreach (ECache *cache,
+                gboolean include_deleted,
+                const gchar *where_clause,
+                ECacheForeachFunc func,
+                gpointer user_data,
+                GCancellable *cancellable,
+                GError **error)
+{
+       struct ForeachData fe;
+       GString *stmt;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (func, FALSE);
+
+       stmt = g_string_new ("SELECT * FROM " E_CACHE_TABLE_OBJECTS);
+
+       if (where_clause) {
+               g_string_append (stmt, " WHERE ");
+
+               if (include_deleted) {
+                       g_string_append (stmt, where_clause);
+               } else {
+                       g_string_append_printf (stmt, E_CACHE_COLUMN_STATE "!=%d AND (%s)",
+                               E_OFFLINE_STATE_LOCALLY_DELETED, where_clause);
+               }
+       } else if (!include_deleted) {
+               g_string_append_printf (stmt, " WHERE " E_CACHE_COLUMN_STATE "!=%d", 
E_OFFLINE_STATE_LOCALLY_DELETED);
+       }
+
+       fe.func = func;
+       fe.user_data = user_data;
+       fe.uid_index = -1;
+       fe.revision_index = -1;
+       fe.object_index = -1;
+       fe.state_index = -1;
+
+       success = e_cache_sqlite_exec_internal (cache, stmt->str, e_cache_foreach_cb, &fe, cancellable, 
error);
+
+       g_string_free (stmt, TRUE);
+
+       return success;
+}
+
+/**
+ * e_cache_put_offline:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @revision: a revision of the object
+ * @object: the object itself
+ * @other_columns: (nullable) (element-type utf8 utf8): what other columns to set; can be %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Stores an object into the cache with an appropriate offline state.
+ * Use e_cache_put() to store an object into the @cache as fully
+ * synchronized. Use e_cache_get_offline_changes() to get list of offline changes.
+ *
+ * Returns: Whether succeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_put_offline (ECache *cache,
+                    const gchar *uid,
+                    const gchar *revision,
+                    const gchar *object,
+                    const GHashTable *other_columns,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       EOfflineState offline_state;
+       gboolean success = TRUE, is_replace;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (revision != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       is_replace = e_cache_contains_internal (cache, uid, TRUE);
+       if (is_replace) {
+               GError *local_error = NULL;
+
+               offline_state = e_cache_get_offline_state (cache, uid, cancellable, &local_error);
+
+               if (local_error) {
+                       success = FALSE;
+                       g_propagate_error (error, local_error);
+               } else if (offline_state != E_OFFLINE_STATE_LOCALLY_CREATED) {
+                       offline_state = E_OFFLINE_STATE_LOCALLY_MODIFIED;
+               }
+       } else {
+               offline_state = E_OFFLINE_STATE_LOCALLY_CREATED;
+       }
+
+       success = success && e_cache_put_with_offline_state (cache, uid, revision, object, other_columns,
+               offline_state, is_replace, cancellable, error);
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       return success;
+}
+
+/**
+ * e_cache_remove_offline:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Marks the object with the given @uid as removed offline, eventually removes
+ * it when it had been created in the offline. Use e_cache_get_offline_changes()
+ * to get list of offline changes.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_remove_offline (ECache *cache,
+                       const gchar *uid,
+                       GCancellable *cancellable,
+                       GError **error)
+{
+       EOfflineState offline_state;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       offline_state = e_cache_get_offline_state (cache, uid, cancellable, error);
+       if (offline_state == E_OFFLINE_STATE_UNKNOWN) {
+               success = FALSE;
+       } else if (offline_state == E_OFFLINE_STATE_LOCALLY_CREATED) {
+               success = e_cache_remove (cache, uid, cancellable, error);
+       } else {
+               g_signal_emit (cache,
+                              signals[BEFORE_REMOVE],
+                              0,
+                              uid, cancellable, error,
+                              &success);
+
+               if (success) {
+                       success = e_cache_set_offline_state (cache, uid,
+                               E_OFFLINE_STATE_LOCALLY_DELETED, cancellable, error);
+               }
+       }
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       return success;
+}
+
+/**
+ * e_cache_get_offline_state:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: Current offline state #EOfflineState for the given object.
+ *    It returns %E_OFFLINE_STATE_UNKNOWN when the object could not be
+ *    found or other error happened.
+ *
+ * Since: 3.26
+ **/
+EOfflineState
+e_cache_get_offline_state (ECache *cache,
+                          const gchar *uid,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       EOfflineState offline_state = E_OFFLINE_STATE_UNKNOWN;
+       gint64 value;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), E_OFFLINE_STATE_UNKNOWN);
+       g_return_val_if_fail (uid != NULL, E_OFFLINE_STATE_UNKNOWN);
+
+       if (e_cache_sqlite_exec_printf (cache,
+               "SELECT " E_CACHE_COLUMN_STATE " FROM " E_CACHE_TABLE_OBJECTS
+               " WHERE " E_CACHE_COLUMN_UID " = %Q",
+               e_cache_get_int64_cb, &value, cancellable, error,
+               uid)) {
+               offline_state = value;
+       }
+
+       return offline_state;
+}
+
+/**
+ * e_cache_set_offline_state:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @state: an #EOfflineState to set
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets an offline @state for the object identified by @uid.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_set_offline_state (ECache *cache,
+                          const gchar *uid,
+                          EOfflineState state,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       return e_cache_sqlite_exec_printf (cache,
+               "UPDATE " E_CACHE_TABLE_OBJECTS " SET " E_CACHE_COLUMN_STATE "=%d"
+               " WHERE " E_CACHE_COLUMN_UID " = %Q",
+               NULL, NULL, cancellable, error,
+               state, uid);
+}
+
+static gboolean
+e_cache_get_offline_changes_cb (ECache *cache,
+                               const gchar *uid,
+                               const gchar *revision,
+                               const gchar *object,
+                               EOfflineState offline_state,
+                               gint ncols,
+                               const gchar *column_names[],
+                               const gchar *column_values[],
+                               gpointer user_data)
+{
+       GSList **pchanges = user_data;
+
+       g_return_val_if_fail (pchanges != NULL, FALSE);
+
+       if (offline_state == E_OFFLINE_STATE_LOCALLY_CREATED ||
+           offline_state == E_OFFLINE_STATE_LOCALLY_MODIFIED ||
+           offline_state == E_OFFLINE_STATE_LOCALLY_DELETED) {
+               *pchanges = g_slist_prepend (*pchanges, e_cache_offline_change_new (uid, offline_state));
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_cache_get_offline_changes:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gathers the list of all offline changes being done so far.
+ * The returned #GSList contains #ECacheOfflineChange structure.
+ * Use e_cache_clear_offline_changes() to clear all offline
+ * changes at once.
+ *
+ * Returns: (transfer full) (element-type ECacheOfflineChange): A newly allocated list of all
+ *    offline changes. Free it with g_slist_free_full (slist, e_cache_offline_change_free);
+ *    when no longer needed.
+ *
+ * Since: 3.26
+ **/
+GSList *
+e_cache_get_offline_changes (ECache *cache,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       GSList *changes = NULL;
+       gchar *stmt;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+       stmt = e_cache_sqlite_stmt_printf (E_CACHE_COLUMN_STATE "!=%d", E_OFFLINE_STATE_SYNCED);
+
+       if (!e_cache_foreach (cache, TRUE, stmt, e_cache_get_offline_changes_cb, &changes, cancellable, 
error)) {
+               g_slist_free_full (changes, e_cache_offline_change_free);
+               changes = NULL;
+       }
+
+       e_cache_sqlite_stmt_free (stmt);
+
+       return changes;
+}
+
+/**
+ * e_cache_clear_offline_changes:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Marks all objects as being fully synchronized with the server and
+ * removes those which are marked as locally deleted.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_clear_offline_changes (ECache *cache,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       success = e_cache_sqlite_exec_printf (cache,
+               "DELETE FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_STATE "=%d",
+               NULL, NULL, cancellable, error,
+               E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       success = success && e_cache_sqlite_exec_printf (cache,
+               "UPDATE " E_CACHE_TABLE_OBJECTS " SET " E_CACHE_COLUMN_STATE "=%d"
+               " WHERE " E_CACHE_COLUMN_STATE "!=%d",
+               NULL, NULL, cancellable, error,
+               E_OFFLINE_STATE_SYNCED, E_OFFLINE_STATE_SYNCED);
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       return success;
+}
+
+/**
+ * e_cache_set_key:
+ * @cache: an #ECache
+ * @key: a key name
+ * @value: (nullable): a value to set, or %NULL to delete the key
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets a @value of the user @key, or deletes it, if the @value is %NULL.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_set_key (ECache *cache,
+                const gchar *key,
+                const gchar *value,
+                GError **error)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (key != NULL, FALSE);
+
+       return e_cache_set_key_internal (cache, TRUE, key, value, error);
+}
+
+/**
+ * e_cache_dup_key:
+ * @cache: an #ECache
+ * @key: a key name
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: (transfer full): a value of the @key. Free the returned string
+ *    with g_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_dup_key (ECache *cache,
+                const gchar *key,
+                GError **error)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+       g_return_val_if_fail (key != NULL, NULL);
+
+       return e_cache_dup_key_internal (cache, TRUE, key, error);
+}
+
+/**
+ * e_cache_set_key_int:
+ * @cache: an #ECache
+ * @key: a key name
+ * @value: an integer value to set
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets an integer @value for the user @key.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_set_key_int (ECache *cache,
+                    const gchar *key,
+                    gint value,
+                    GError **error)
+{
+       gchar *str_value;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (key != NULL, FALSE);
+
+       str_value = g_strdup_printf ("%d", value);
+       success = e_cache_set_key (cache, key, str_value, error);
+       g_free (str_value);
+
+       return success;
+}
+
+/**
+ * e_cache_get_key_int:
+ * @cache: an #ECache
+ * @key: a key name
+ * @error: return location for a #GError, or %NULL
+ *
+ * Reads the user @key value as an integer.
+ *
+ * Returns: The user @key value or -1 on error.
+ *
+ * Since: 3.26
+ **/
+gint
+e_cache_get_key_int (ECache *cache,
+                    const gchar *key,
+                    GError **error)
+{
+       gchar *str_value;
+       gint value;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), -1);
+
+       str_value = e_cache_dup_key (cache, key, error);
+       if (!str_value)
+               return -1;
+
+       value = g_ascii_strtoll (str_value, NULL, 10);
+       g_free (str_value);
+
+       return value;
+}
+
+/**
+ * e_cache_lock:
+ * @cache: an #ECache
+ * @lock_type: an #ECacheLockType
+ *
+ * Locks the @cache thus other threads cannot use it.
+ * This can be called recursively within one thread.
+ * Each call should have its pair e_cache_unlock().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_lock (ECache *cache,
+             ECacheLockType lock_type)
+{
+       g_return_if_fail (E_IS_CACHE (cache));
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       cache->priv->in_transaction++;
+       g_return_if_fail (cache->priv->in_transaction > 0);
+
+       if (cache->priv->in_transaction == 1) {
+               /* It's important to make the distinction between a
+                * transaction which will read or one which will write.
+                *
+                * While it's not well documented, when receiving the SQLITE_BUSY
+                * error status, one can only safely retry at the beginning of
+                * the transaction.
+                *
+                * If a transaction is 'upgraded' to require a writer lock
+                * half way through the transaction and SQLITE_BUSY is returned,
+                * the whole transaction would need to be retried from the beginning.
+                */
+               cache->priv->lock_type = lock_type;
+
+               switch (lock_type) {
+               case E_CACHE_LOCK_READ:
+                       e_cache_sqlite_exec_internal (cache, "BEGIN", NULL, NULL, NULL, NULL);
+                       break;
+               case E_CACHE_LOCK_WRITE:
+                       e_cache_sqlite_exec_internal (cache, "BEGIN IMMEDIATE", NULL, NULL, NULL, NULL);
+                       break;
+               }
+       } else {
+               /* Warn about cases where where a read transaction might be upgraded */
+               if (lock_type == E_CACHE_LOCK_WRITE && cache->priv->lock_type == E_CACHE_LOCK_READ)
+                       g_warning (
+                               "A nested transaction wants to write, "
+                               "but the outermost transaction was started "
+                               "without a writer lock.");
+       }
+}
+
+/**
+ * e_cache_unlock:
+ * @cache: an #ECache
+ * @action: an #ECacheUnlockAction
+ *
+ * Unlocks the cache which was previouly locked with e_cache_lock().
+ * The cache locked with #E_CACHE_LOCK_WRITE should use either
+ * @action #E_CACHE_UNLOCK_COMMIT or #E_CACHE_UNLOCK_ROLLBACK,
+ * while the #E_CACHE_LOCK_READ should use #E_CACHE_UNLOCK_NONE @action.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_unlock (ECache *cache,
+               ECacheUnlockAction action)
+{
+       g_return_if_fail (E_IS_CACHE (cache));
+       g_return_if_fail (cache->priv->in_transaction > 0);
+
+       cache->priv->in_transaction--;
+
+       if (cache->priv->in_transaction == 0) {
+               switch (action) {
+               case E_CACHE_UNLOCK_NONE:
+               case E_CACHE_UNLOCK_COMMIT:
+                       e_cache_sqlite_exec_internal (cache, "COMMIT", NULL, NULL, NULL, NULL);
+                       break;
+               case E_CACHE_UNLOCK_ROLLBACK:
+                       e_cache_sqlite_exec_internal (cache, "ROLLBACK", NULL, NULL, NULL, NULL);
+                       break;
+               }
+       }
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+}
+
+/**
+ * e_cache_get_sqlitedb:
+ * @cache: an #ECache
+ *
+ * Returns: (transfer none): An SQLite3 database pointer. It is owned by the @cache.
+ *
+ * Since: 3.26
+ **/
+gpointer
+e_cache_get_sqlitedb (ECache *cache)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+       return cache->priv->db;
+}
+
+/**
+ * e_cache_sqlite_exec:
+ * @cache: an #ECache
+ * @sql_stmt: an SQLite statement to execute
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Executes an SQLite statement. Use e_cache_sqlite_select() for
+ * SELECT statements.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_sqlite_exec (ECache *cache,
+                    const gchar *sql_stmt,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       return e_cache_sqlite_exec_internal (cache, sql_stmt, NULL, NULL, cancellable, error);
+}
+
+/**
+ * e_cache_sqlite_select:
+ * @cache: an #ECache
+ * @sql_stmt: an SQLite SELECT statement to execute
+ * @func: an #ECacheSelectFunc function to call for each row
+ * @user_data: user data for @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Executes a SELECT statement @sql_stmt and calls @func for each row of the result.
+ * Use e_cache_sqlite_exec() for statements which do not return row sets.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_sqlite_select (ECache *cache,
+                      const gchar *sql_stmt,
+                      ECacheSelectFunc func,
+                      gpointer user_data,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (sql_stmt, FALSE);
+       g_return_val_if_fail (func, FALSE);
+
+       return e_cache_sqlite_exec_internal (cache, sql_stmt, func, user_data, cancellable, error);
+}
+
+/**
+ * e_cache_sqlite_stmt_printf:
+ * @format: a printf-like format
+ * @...: arguments for the @format
+ *
+ * Creates and SQLite statement based on the @format and its arguments.
+ * The @format can contain any values recognized by sqlite3_mprintf().
+ *
+ * Returns: (transfer full): A new SQLite statement. Free the returned
+ *    string with e_cache_sqlite_stmt_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_sqlite_stmt_printf (const gchar *format,
+                           ...)
+{
+       va_list args;
+       gchar *stmt;
+
+       g_return_val_if_fail (format != NULL, NULL);
+
+       va_start (args, format);
+       stmt = sqlite3_vmprintf (format, args);
+       va_end (args);
+
+       return stmt;
+}
+
+/**
+ * e_cache_sqlite_stmt_free:
+ * @stmt: a statement to free
+ *
+ * Frees a statement previously constructed with e_cache_sqlite_stmt_printf().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_sqlite_stmt_free (gchar *stmt)
+{
+       if (stmt)
+               sqlite3_free (stmt);
+}
+
+/**
+ * e_cache_sqlite_maybe_vacuum:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Runs vacuum (compacts the database file), if needed.
+ *
+ * Returns: Whether succeeded. It doesn't mean that the vacuum had been run,
+ *    only that no error happened during the call.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_sqlite_maybe_vacuum (ECache *cache,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       guint64 page_count = 0, page_size = 0, freelist_count = 0;
+       gboolean success = FALSE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       if (e_cache_sqlite_exec_internal (cache, "PRAGMA page_count;", e_cache_get_uint64_cb, &page_count, 
cancellable, &local_error) &&
+           e_cache_sqlite_exec_internal (cache, "PRAGMA page_size;", e_cache_get_uint64_cb, &page_size, 
cancellable, &local_error) &&
+           e_cache_sqlite_exec_internal (cache, "PRAGMA freelist_count;", e_cache_get_uint64_cb, 
&freelist_count, cancellable, &local_error)) {
+               /* Vacuum, if there's more than 5% of the free pages, or when free pages use more than 10MB */
+               success = !page_count || !freelist_count ||
+                       (freelist_count * page_size < 1024 * 1024 * 10 && freelist_count * 1000 / page_count 
<= 50) ||
+                       e_cache_sqlite_exec_internal (cache, "vacuum;", NULL, NULL, cancellable, 
&local_error);
+       }
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       if (local_error) {
+               g_propagate_error (error, local_error);
+               success = FALSE;
+       }
+
+       return success;
+}
+
+static gboolean
+e_cache_put_locked_default (ECache *cache,
+                           const gchar *uid,
+                           const gchar *revision,
+                           const gchar *object,
+                           const GHashTable *other_columns,
+                           EOfflineState offline_state,
+                           gboolean is_replace,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       GString *statement, *other_names = NULL, *other_values = NULL;
+       gchar *tmp;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (revision != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       tmp = e_cache_sqlite_stmt_printf ("INSERT OR REPLACE INTO %Q ("
+               E_CACHE_COLUMN_UID ","
+               E_CACHE_COLUMN_REVISION ","
+               E_CACHE_COLUMN_OBJECT ","
+               E_CACHE_COLUMN_STATE);
+
+       statement = g_string_new (tmp);
+
+       e_cache_sqlite_stmt_free (tmp);
+
+       if (other_columns) {
+               GHashTableIter iter;
+               gpointer key, value;
+
+               g_hash_table_iter_init (&iter, (GHashTable *) other_columns);
+               while (g_hash_table_iter_next (&iter, &key, &value)) {
+                       if (!other_names)
+                               other_names = g_string_new ("");
+                       g_string_append (other_names, ",");
+
+                       tmp = e_cache_sqlite_stmt_printf ("%Q", key);
+                       g_string_append (other_names, tmp);
+                       e_cache_sqlite_stmt_free (tmp);
+
+                       if (!other_values)
+                               other_values = g_string_new ("");
+
+                       g_string_append (other_values, ",");
+                       if (value) {
+                               tmp = e_cache_sqlite_stmt_printf ("%Q", value);
+                               g_string_append (other_values, tmp);
+                               e_cache_sqlite_stmt_free (tmp);
+                       } else {
+                               g_string_append (other_values, "NULL");
+                       }
+               }
+       }
+
+       if (other_names)
+               g_string_append (statement, other_names->str);
+
+       g_string_append (statement, ") VALUES (");
+
+       tmp = e_cache_sqlite_stmt_printf ("%Q,%Q,%Q,%d", uid, revision, object, offline_state);
+       g_string_append (statement, tmp);
+       e_cache_sqlite_stmt_free (tmp);
+
+       if (other_values)
+               g_string_append (statement, other_values->str);
+
+       g_string_append (statement, ")");
+
+       success = e_cache_sqlite_exec_internal (cache, statement->str, NULL, NULL, cancellable, error);
+
+       if (other_names)
+               g_string_free (other_names, TRUE);
+       if (other_values)
+               g_string_free (other_values, TRUE);
+       g_string_free (statement, TRUE);
+
+       return success;
+}
+
+static gboolean
+e_cache_signals_accumulator (GSignalInvocationHint *ihint,
+                            GValue *return_accu,
+                            const GValue *handler_return,
+                            gpointer data)
+{
+       gboolean handler_result;
+
+       handler_result = g_value_get_boolean (handler_return);
+       g_value_set_boolean (return_accu, handler_result);
+
+       return handler_result;
+}
+
+static gboolean
+e_cache_before_put_default (ECache *cache,
+                           const gchar *uid,
+                           const gchar *revision,
+                           const gchar *object,
+                           GHashTable *other_columns,
+                           gboolean is_replace,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       return TRUE;
+}
+
+static gboolean
+e_cache_before_remove_default (ECache *cache,
+                              const gchar *uid,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       return TRUE;
+}
+
+static void
+e_cache_finalize (GObject *object)
+{
+       ECache *cache = E_CACHE (object);
+
+       g_free (cache->priv->filename);
+       cache->priv->filename = NULL;
+
+       if (cache->priv->db) {
+               sqlite3_close (cache->priv->db);
+               cache->priv->db = NULL;
+       }
+
+       g_rec_mutex_clear (&cache->priv->lock);
+
+       g_warn_if_fail (cache->priv->cancellable == NULL);
+       g_clear_object (&cache->priv->cancellable);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cache_parent_class)->finalize (object);
+}
+
+static void
+e_cache_class_init (ECacheClass *class)
+{
+       GObjectClass *object_class;
+
+       g_type_class_add_private (class, sizeof (ECachePrivate));
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->finalize = e_cache_finalize;
+
+       class->put_locked = e_cache_put_locked_default;
+       class->before_put = e_cache_before_put_default;
+       class->before_remove = e_cache_before_remove_default;
+
+       signals[BEFORE_PUT] = g_signal_new (
+               "before-put",
+               G_OBJECT_CLASS_TYPE (class),
+               G_SIGNAL_RUN_LAST,
+               G_STRUCT_OFFSET (ECacheClass, before_put),
+               e_cache_signals_accumulator,
+               NULL,
+               g_cclosure_marshal_generic,
+               G_TYPE_BOOLEAN, 7,
+               G_TYPE_STRING,
+               G_TYPE_STRING,
+               G_TYPE_STRING,
+               G_TYPE_HASH_TABLE,
+               G_TYPE_BOOLEAN,
+               G_TYPE_CANCELLABLE,
+               G_TYPE_ERROR);
+
+       signals[BEFORE_REMOVE] = g_signal_new (
+               "before-remove",
+               G_OBJECT_CLASS_TYPE (class),
+               G_SIGNAL_RUN_LAST,
+               G_STRUCT_OFFSET (ECacheClass, before_remove),
+               e_cache_signals_accumulator,
+               NULL,
+               g_cclosure_marshal_generic,
+               G_TYPE_BOOLEAN, 3,
+               G_TYPE_STRING,
+               G_TYPE_CANCELLABLE,
+               G_TYPE_ERROR);
+
+       e_sqlite3_vfs_init ();
+}
+
+static void
+e_cache_init (ECache *cache)
+{
+       cache->priv = G_TYPE_INSTANCE_GET_PRIVATE (cache, E_TYPE_CACHE, ECachePrivate);
+
+       cache->priv->filename = NULL;
+       cache->priv->db = NULL;
+       cache->priv->cancellable = NULL;
+       cache->priv->in_transaction = 0;
+
+       g_rec_mutex_init (&cache->priv->lock);
+}
diff --git a/src/libebackend/e-cache.h b/src/libebackend/e-cache.h
new file mode 100644
index 0000000..54039a3
--- /dev/null
+++ b/src/libebackend/e-cache.h
@@ -0,0 +1,388 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * 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.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEBACKEND_H_INSIDE__) && !defined (LIBEBACKEND_COMPILATION)
+#error "Only <libebackend/libebackend.h> should be included directly."
+#endif
+
+#ifndef E_CACHE_H
+#define E_CACHE_H
+
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <libebackend/e-backend-enums.h>
+
+/* Standard GObject macros */
+#define E_TYPE_CACHE \
+       (e_cache_get_type ())
+#define E_CACHE(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_CACHE, ECache))
+#define E_CACHE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_CACHE, ECacheClass))
+#define E_IS_CACHE(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_CACHE))
+#define E_IS_CACHE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_CACHE))
+#define E_CACHE_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_CACHE, ECacheClass))
+
+G_BEGIN_DECLS
+
+#define E_CACHE_COLUMN_UID     "ECacheUID"
+#define E_CACHE_COLUMN_REVISION        "ECacheREV"
+#define E_CACHE_COLUMN_OBJECT  "ECacheOBJ"
+#define E_CACHE_COLUMN_STATE   "ECacheState"
+
+#define E_CACHE_TABLE_OBJECTS  "ECacheObjects"
+#define E_CACHE_TABLE_KEYS     "ECacheKeys"
+
+/**
+ * E_CACHE_ERROR:
+ *
+ * Error domain for #ECache operations.
+ *
+ * Since: 3.26
+ **/
+#define E_CACHE_ERROR (e_cache_error_quark ())
+
+GQuark         e_cache_error_quark     (void);
+
+/**
+ * ECacheError:
+ * @E_CACHE_ERROR_ENGINE: An error was reported from the SQLite engine
+ * @E_CACHE_ERROR_CONSTRAINT: The error occurred due to an explicit constraint, like
+ *    when attempting to add two objects with the same UID.
+ * @E_CACHE_ERROR_NOT_FOUND: An object was not found by UID (this is
+ *    different from a query that returns no results, which is not an error).
+ * @E_CACHE_ERROR_INVALID_QUERY: A query was invalid.
+ * @E_CACHE_ERROR_UNSUPPORTED_QUERY: A query was not supported.
+ * @E_CACHE_ERROR_END_OF_LIST: An attempt was made to fetch results past the end of a the list.
+ * @E_CACHE_ERROR_LOAD: An error occured while loading or creating the database.
+ *
+ * Defines the types of possible errors reported by the #ECache
+ *
+ * Since: 3.26
+ */
+typedef enum {
+       E_CACHE_ERROR_ENGINE,
+       E_CACHE_ERROR_CONSTRAINT,
+       E_CACHE_ERROR_NOT_FOUND,
+       E_CACHE_ERROR_INVALID_QUERY,
+       E_CACHE_ERROR_UNSUPPORTED_QUERY,
+       E_CACHE_ERROR_END_OF_LIST,
+       E_CACHE_ERROR_LOAD
+} ECacheError;
+
+typedef struct {
+       gchar *uid;
+       EOfflineState state;
+} ECacheOfflineChange;
+
+#define E_TYPE_CACHE_OFFLINE_CHANGE (e_cache_offline_change_get_type ())
+
+GType          e_cache_offline_change_get_type (void) G_GNUC_CONST;
+ECacheOfflineChange *
+               e_cache_offline_change_new      (const gchar *uid,
+                                                EOfflineState state);
+ECacheOfflineChange *
+               e_cache_offline_change_copy     (const ECacheOfflineChange *change);
+void           e_cache_offline_change_free     (/* ECacheOfflineChange */ gpointer change);
+
+typedef struct {
+       gchar *name;
+       gchar *type;
+       gchar *index_name;
+} ECacheColumnInfo;
+
+#define E_TYPE_CACHE_COLUMN_INFO (e_cache_column_info_get_type ())
+GType          e_cache_column_info_get_type    (void) G_GNUC_CONST;
+ECacheColumnInfo *
+               e_cache_column_info_new         (const gchar *name,
+                                                const gchar *type,
+                                                const gchar *index_name);
+ECacheColumnInfo *
+               e_cache_column_info_copy        (const ECacheColumnInfo *info);
+void           e_cache_column_info_free        (/* ECacheColumnInfo */ gpointer info);
+
+/**
+ * ECacheLockType:
+ * @E_CACHE_LOCK_READ: Obtain a lock for reading.
+ * @E_CACHE_LOCK_WRITE: Obtain a lock for writing. This also starts a transaction.
+ *
+ * Indicates the type of lock requested in e_cache_lock().
+ *
+ * Since: 3.24
+ **/
+typedef enum {
+       E_CACHE_LOCK_READ,
+       E_CACHE_LOCK_WRITE
+} ECacheLockType;
+
+/**
+ * ECacheUnlockAction:
+ * @E_CACHE_UNLOCK_NONE: Just unlock, this is appropriate for locks which were obtained with 
%E_CACHE_LOCK_READ.
+ * @E_CACHE_UNLOCK_COMMIT: Commit any modifications which were made while the lock was held.
+ * @E_CACHE_UNLOCK_ROLLBACK: Rollback any modifications which were made while the lock was held.
+ *
+ * Indicates what type of action to take while unlocking the cache with e_cache_unlock().
+ *
+ * Since: 3.24
+ **/
+typedef enum {
+       E_CACHE_UNLOCK_NONE,
+       E_CACHE_UNLOCK_COMMIT,
+       E_CACHE_UNLOCK_ROLLBACK
+} ECacheUnlockAction;
+
+typedef struct _ECache ECache;
+typedef struct _ECacheClass ECacheClass;
+typedef struct _ECachePrivate ECachePrivate;
+
+/**
+ * ECacheForeachFunc:
+ * @cache: an #ECache
+ * @uid: a unique object identifier
+ * @revision: the object revision
+ * @object: the object itself
+ * @offline_state: objects offline state, one of #EOfflineState
+ * @ncols: count of columns, items in column_names and column_values
+ * @column_names: column names
+ * @column_values: column values
+ * @user_data: user data, as used in e_cache_foreach()
+ *
+ * A callback called for each object row when using e_cache_foreach() function.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* ECacheForeachFunc) (ECache *cache,
+                                        const gchar *uid,
+                                        const gchar *revision,
+                                        const gchar *object,
+                                        EOfflineState offline_state,
+                                        gint ncols,
+                                        const gchar *column_names[],
+                                        const gchar *column_values[],
+                                        gpointer user_data);
+
+/**
+ * ECacheSelectFunc:
+ * @cache: an #ECache
+ * @ncols: count of columns, items in column_names and column_values
+ * @column_names: column names
+ * @column_values: column values
+ * @user_data: user data, as used in e_cache_sqlite_select()
+ *
+ * A callback called for each row of a SELECT statement executed
+ * with e_cache_sqlite_select() function.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* ECacheSelectFunc)  (ECache *cache,
+                                        gint ncols,
+                                        const gchar *column_names[],
+                                        const gchar *column_values[],
+                                        gpointer user_data);
+
+/**
+ * ECache:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.24
+ **/
+struct _ECache {
+       /*< private >*/
+       GObject parent;
+       ECachePrivate *priv;
+};
+
+struct _ECacheClass {
+       GObjectClass parent_class;
+
+       /* Virtual methods */
+       gboolean        (* put_locked)          (ECache *cache,
+                                                const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                const GHashTable *other_columns,
+                                                EOfflineState offline_state,
+                                                gboolean is_replace,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       void            (* erase)               (ECache *cache);
+
+       /* Signals */
+       gboolean        (* before_put)          (ECache *cache,
+                                                const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                GHashTable *other_columns,
+                                                gboolean is_replace,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* before_remove)       (ECache *cache,
+                                                const gchar *uid,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+       gpointer reserved[10];
+};
+
+GType          e_cache_get_type                (void) G_GNUC_CONST;
+
+gboolean       e_cache_initialize_sync         (ECache *cache,
+                                                const gchar *filename,
+                                                const GSList *other_columns, /* ECacheColumnInfo * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+const gchar *  e_cache_get_filename            (ECache *cache);
+gint           e_cache_get_version             (ECache *cache);
+void           e_cache_set_version             (ECache *cache,
+                                                gint version);
+gchar *                e_cache_dup_revision            (ECache *cache);
+void           e_cache_set_revision            (ECache *cache,
+                                                const gchar *revision);
+void           e_cache_erase                   (ECache *cache);
+gboolean       e_cache_contains                (ECache *cache,
+                                                const gchar *uid);
+gchar *                e_cache_get                     (ECache *cache,
+                                                const gchar *uid,
+                                                gchar **out_revision,
+                                                GHashTable **out_other_columns,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_put                     (ECache *cache,
+                                                const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                GHashTable *other_columns,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_remove                  (ECache *cache,
+                                                const gchar *uid,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_remove_all              (ECache *cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+guint          e_cache_count                   (ECache *cache,
+                                                gboolean include_deleted,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_get_uids                (ECache *cache,
+                                                gboolean include_deleted,
+                                                GSList **out_uids,
+                                                GSList **out_revisions,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_get_objects             (ECache *cache,
+                                                gboolean include_deleted,
+                                                GSList **out_objects,
+                                                GSList **out_revisions,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_foreach                 (ECache *cache,
+                                                gboolean include_deleted,
+                                                const gchar *where_clause,
+                                                ECacheForeachFunc func,
+                                                gpointer user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+/* Offline support */
+gboolean       e_cache_put_offline             (ECache *cache,
+                                                const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                const GHashTable *other_columns,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_remove_offline          (ECache *cache,
+                                                const gchar *uid,
+                                                GCancellable *cancellable,
+                                                GError **error);
+EOfflineState  e_cache_get_offline_state       (ECache *cache,
+                                                const gchar *uid,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_set_offline_state       (ECache *cache,
+                                                const gchar *uid,
+                                                EOfflineState state,
+                                                GCancellable *cancellable,
+                                                GError **error);
+GSList *       e_cache_get_offline_changes     (ECache *cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_clear_offline_changes   (ECache *cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+/* Custom keys */
+gboolean       e_cache_set_key                 (ECache *cache,
+                                                const gchar *key,
+                                                const gchar *value,
+                                                GError **error);
+gchar *                e_cache_dup_key                 (ECache *cache,
+                                                const gchar *key,
+                                                GError **error);
+gboolean       e_cache_set_key_int             (ECache *cache,
+                                                const gchar *key,
+                                                gint value,
+                                                GError **error);
+gint           e_cache_get_key_int             (ECache *cache,
+                                                const gchar *key,
+                                                GError **error);
+
+/* Locking */
+void           e_cache_lock                    (ECache *cache,
+                                                ECacheLockType lock_type);
+void           e_cache_unlock                  (ECache *cache,
+                                                ECacheUnlockAction action);
+
+/* Low-level SQLite functions */
+gpointer       e_cache_get_sqlitedb            (ECache *cache);
+gboolean       e_cache_sqlite_exec             (ECache *cache,
+                                                const gchar *sql_stmt,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_sqlite_select           (ECache *cache,
+                                                const gchar *sql_stmt,
+                                                ECacheSelectFunc func,
+                                                gpointer user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_sqlite_maybe_vacuum     (ECache *cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+gchar *                e_cache_sqlite_stmt_printf      (const gchar *format,
+                                                ...);
+void           e_cache_sqlite_stmt_free        (gchar *stmt);
+
+G_END_DECLS
+
+#endif /* E_CACHE_H */
diff --git a/src/libebackend/libebackend.h b/src/libebackend/libebackend.h
index c286f18..d1559fd 100644
--- a/src/libebackend/libebackend.h
+++ b/src/libebackend/libebackend.h
@@ -26,6 +26,7 @@
 #include <libebackend/e-backend-enumtypes.h>
 #include <libebackend/e-backend-factory.h>
 #include <libebackend/e-backend.h>
+#include <libebackend/e-cache.h>
 #include <libebackend/e-cache-reaper.h>
 #include <libebackend/e-collection-backend-factory.h>
 #include <libebackend/e-collection-backend.h>


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