[evolution-data-server/wip/offline-cache] An ECache base structure
- From: Milan Crha <mcrha src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [evolution-data-server/wip/offline-cache] An ECache base structure
- Date: Wed, 25 Jan 2017 10:32:56 +0000 (UTC)
commit 2c46d612d0df05d99047f21a6cc90f8da5e2d458
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]