/* -*- 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 .
*/
/**
* SECTION: e-cache
* @include: libebackend/libebackend.h
* @short_description: An SQLite data cache
*
* The #ECache is an abstract class which consists of the common
* parts which can be used by its descendants. It also allows
* storing offline state for the stored objects.
*
* The API is thread safe, with special considerations to be made
* around e_cache_lock() and e_cache_unlock() for
* the sake of isolating transactions across threads.
**/
#include "evolution-data-server-config.h"
#include
#include
#include
#include
#include
#include
#include
#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
/* How many rows to read when e_cache_foreach_update() */
#define E_CACHE_UPDATE_BATCH_SIZE 100
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 */
guint32 revision_change_frozen;
gint revision_counter;
gint64 last_revision_time;
gboolean needs_revision_change;
};
enum {
BEFORE_PUT,
BEFORE_REMOVE,
REVISION_CHANGED,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL];
G_DEFINE_QUARK (e-cache-error-quark, e_cache_error)
G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (ECache, e_cache, G_TYPE_OBJECT)
G_DEFINE_BOXED_TYPE (ECacheColumnValues, e_cache_column_values, e_cache_column_values_copy, e_cache_column_values_free)
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_column_values_new:
*
* Creates a new #ECacheColumnValues to store values for additional columns.
* The column names are compared case insensitively.
*
* Returns: (transfer full): a new #ECacheColumnValues. Free with e_cache_column_values_free(),
* when no longer needed.
*
* Since: 3.26
**/
ECacheColumnValues *
e_cache_column_values_new (void)
{
return (ECacheColumnValues *) g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, g_free);
}
/**
* e_cache_column_values_copy:
* @other_columns: (nullable): an #ECacheColumnValues
*
* Returns: (transfer full): Copy of the @other_columns. Free with
* e_cache_column_values_free(), when no longer needed.
*
* Since: 3.26
**/
ECacheColumnValues *
e_cache_column_values_copy (ECacheColumnValues *other_columns)
{
GHashTableIter iter;
gpointer name, value;
ECacheColumnValues *copy;
if (!other_columns)
return NULL;
copy = e_cache_column_values_new ();
e_cache_column_values_init_iter (other_columns, &iter);
while (g_hash_table_iter_next (&iter, &name, &value)) {
e_cache_column_values_put (copy, name, value);
}
return copy;
}
/**
* e_cache_column_values_free:
* @other_columns: (nullable): an #ECacheColumnValues
*
* Frees previously allocated @other_columns with
* e_cache_column_values_new() or e_cache_column_values_copy().
*
* Since: 3.26
**/
void
e_cache_column_values_free (ECacheColumnValues *other_columns)
{
if (other_columns)
g_hash_table_destroy ((GHashTable *) other_columns);
}
/**
* e_cache_column_values_put:
* @other_columns: an #ECacheColumnValues
* @name: a column name
* @value: (nullable): a column value
*
* Puts the @value for column @name. If contains a value for the same
* column, then it is replaced. This creates a copy of both @name
* and @value.
*
* Since: 3.26
**/
void
e_cache_column_values_put (ECacheColumnValues *other_columns,
const gchar *name,
const gchar *value)
{
GHashTable *hash_table = (GHashTable *) other_columns;
g_return_if_fail (other_columns != NULL);
g_return_if_fail (name != NULL);
g_hash_table_insert (hash_table, g_strdup (name), g_strdup (value));
}
/**
* e_cache_column_values_take_value:
* @other_columns: an #ECacheColumnValues
* @name: a column name
* @value: (nullable) (in) (transfer full): a column value
*
* Puts the @value for column @name. If contains a value for the same
* column, then it is replaced. This creates a copy of the @name, but
* takes owner ship of the @value.
*
* Since: 3.26
**/
void
e_cache_column_values_take_value (ECacheColumnValues *other_columns,
const gchar *name,
gchar *value)
{
GHashTable *hash_table = (GHashTable *) other_columns;
g_return_if_fail (other_columns != NULL);
g_return_if_fail (name != NULL);
g_hash_table_insert (hash_table, g_strdup (name), value);
}
/**
* e_cache_column_values_take:
* @other_columns: an #ECacheColumnValues
* @name: (in) (transfer full): a column name
* @value: (nullable) (in) (transfer full): a column value
*
* Puts the @value for column @name. If contains a value for the same
* column, then it is replaced. This creates takes ownership of both
* the @name and the @value.
*
* Since: 3.26
**/
void
e_cache_column_values_take (ECacheColumnValues *other_columns,
gchar *name,
gchar *value)
{
GHashTable *hash_table = (GHashTable *) other_columns;
g_return_if_fail (other_columns != NULL);
g_return_if_fail (name != NULL);
g_hash_table_insert (hash_table, name, value);
}
/**
* e_cache_column_values_contains:
* @other_columns: an #ECacheColumnValues
* @name: a column name
*
* Returns: Whether @other_columns contains column named @name.
*
* Since: 3.26
**/
gboolean
e_cache_column_values_contains (ECacheColumnValues *other_columns,
const gchar *name)
{
GHashTable *hash_table = (GHashTable *) other_columns;
g_return_val_if_fail (other_columns != NULL, FALSE);
g_return_val_if_fail (name != NULL, FALSE);
return g_hash_table_contains (hash_table, name);
}
/**
* e_cache_column_values_remove:
* @other_columns: an #ECacheColumnValues
* @name: a column name
*
* Removes value for the column named @name from @other_columns.
*
* Returns: Whether such column existed and had been removed.
*
* Since: 3.26
**/
gboolean
e_cache_column_values_remove (ECacheColumnValues *other_columns,
const gchar *name)
{
GHashTable *hash_table = (GHashTable *) other_columns;
g_return_val_if_fail (other_columns != NULL, FALSE);
g_return_val_if_fail (name != NULL, FALSE);
return g_hash_table_remove (hash_table, name);
}
/**
* e_cache_column_values_remove_all:
* @other_columns: an #ECacheColumnValues
*
* Removes all values from the @other_columns, leaving it empty.
*
* Since: 3.26
**/
void
e_cache_column_values_remove_all (ECacheColumnValues *other_columns)
{
GHashTable *hash_table = (GHashTable *) other_columns;
g_return_if_fail (other_columns != NULL);
g_hash_table_remove_all (hash_table);
}
/**
* e_cache_column_values_lookup:
* @other_columns: an #ECacheColumnValues
* @name: a column name
*
* Looks up currently stored value for the column named @name.
* As the values can be %NULL one cannot distinguish between
* a column which doesn't have stored any value and a column
* which has stored %NULL value. Use e_cache_column_values_contains()
* to check whether such column exitst in the @other_columns.
* The returned pointer is owned by @other_columns and is valid until
* the value is overwritten of the @other_columns freed.
*
* Returns: (nullable): Stored value for the column named @name,
* or %NULL, if no such column values is stored.
*
* Since: 3.26
**/
const gchar *
e_cache_column_values_lookup (ECacheColumnValues *other_columns,
const gchar *name)
{
GHashTable *hash_table = (GHashTable *) other_columns;
g_return_val_if_fail (other_columns != NULL, NULL);
g_return_val_if_fail (name != NULL, NULL);
return g_hash_table_lookup (hash_table, name);
}
/**
* e_cache_column_values_get_size:
* @other_columns: an #ECacheColumnValues
*
* Returns: How many columns are stored in the @other_columns.
*
* Since: 3.26
**/
guint
e_cache_column_values_get_size (ECacheColumnValues *other_columns)
{
GHashTable *hash_table = (GHashTable *) other_columns;
g_return_val_if_fail (other_columns != NULL, 0);
return g_hash_table_size (hash_table);
}
/**
* e_cache_column_values_init_iter:
* @other_columns: an #ECacheColumnValues
* @iter: a #GHashTableIter
*
* Initialized the @iter, thus the @other_columns can be traversed
* with g_hash_table_iter_next(). The key is a column name and
* the value is the corresponding column value.
*
* Since: 3.26
**/
void
e_cache_column_values_init_iter (ECacheColumnValues *other_columns,
GHashTableIter *iter)
{
GHashTable *hash_table = (GHashTable *) other_columns;
g_return_if_fail (other_columns != NULL);
g_return_if_fail (iter != NULL);
g_hash_table_iter_init (iter, hash_table);
}
/**
* e_cache_offline_change_new:
* @uid: a unique object identifier
* @revision: (nullable): a revision of the object
* @object: (nullable): object itself
* @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,
const gchar *revision,
const gchar *object,
EOfflineState state)
{
ECacheOfflineChange *change;
g_return_val_if_fail (uid != NULL, NULL);
change = g_slice_new0 (ECacheOfflineChange);
change->uid = g_strdup (uid);
change->revision = g_strdup (revision);
change->object = g_strdup (object);
change->state = state;
return change;
}
/**
* e_cache_offline_change_copy:
* @change: (nullable): a source #ECacheOfflineChange to copy, or %NULL
*
* Returns: (transfer full) (nullable): 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->revision, change->object, 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->revision);
g_free (chng->object);
g_slice_free (ECacheOfflineChange, 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_slice_new0 (ECacheColumnInfo);
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) (nullable): 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_slice_free (ECacheColumnInfo, nfo);
}
}
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++;
g_clear_pointer (&errmsg, sqlite3_free);
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) {
if (ret == SQLITE_CONSTRAINT) {
g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_CONSTRAINT, errmsg);
} else if (ret == SQLITE_ABORT || ret == SQLITE_INTERRUPT) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Operation cancelled: %s", errmsg);
} else if (ret == SQLITE_CORRUPT && cache->priv->filename && *cache->priv->filename) {
g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_CORRUPT,
"%s (%s)", errmsg, cache->priv->filename);
} else {
gchar *valid_utf8 = e_util_utf8_make_valid (stmt);
g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE,
"SQLite error code '%d': %s (statement:%s)", ret, errmsg, valid_utf8 ? valid_utf8 : stmt);
g_free (valid_utf8);
}
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 KEY,"
"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 KEY,"
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_c (objects_stmt, ',');
g_string_append (objects_stmt, info->name);
g_string_append_c (objects_stmt, ' ');
g_string_append (objects_stmt, info->type);
}
g_string_append_c (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 not meant to be
* used by the descendants, because the revision is updated automatically
* when needed. The descendants can listen to "revision-changed" signal.
*
* 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);
g_signal_emit (cache, signals[REVISION_CHANGED], 0, NULL);
}
/**
* e_cache_change_revision:
* @cache: an #ECache
*
* Instructs the @cache to change its revision. In case the revision
* change is frozen with e_cache_freeze_revision_change() it notes to
* change the revision once the revision change is fully thaw.
*
* Since: 3.26
**/
void
e_cache_change_revision (ECache *cache)
{
g_return_if_fail (E_IS_CACHE (cache));
g_rec_mutex_lock (&cache->priv->lock);
if (e_cache_is_revision_change_frozen (cache)) {
cache->priv->needs_revision_change = TRUE;
} else {
gchar time_string[100] = { 0 };
const struct tm *tm = NULL;
time_t t;
gint64 revision_time;
gchar *revision;
revision_time = g_get_real_time () / (1000 * 1000);
t = (time_t) revision_time;
if (revision_time != cache->priv->last_revision_time) {
cache->priv->revision_counter = 0;
cache->priv->last_revision_time = revision_time;
}
tm = gmtime (&t);
if (tm)
strftime (time_string, 100, "%Y-%m-%dT%H:%M:%SZ", tm);
revision = g_strdup_printf ("%s(%d)", time_string, cache->priv->revision_counter++);
e_cache_set_revision (cache, revision);
g_free (revision);
}
g_rec_mutex_unlock (&cache->priv->lock);
}
/**
* e_cache_freeze_revision_change:
* @cache: an #ECache
*
* Freezes automatic revision change for the @cache. The function
* can be called multiple times, but each such call requires its
* pair function e_cache_thaw_revision_change() call. See also
* e_cache_change_revision().
*
* Since: 3.26
**/
void
e_cache_freeze_revision_change (ECache *cache)
{
g_return_if_fail (E_IS_CACHE (cache));
g_rec_mutex_lock (&cache->priv->lock);
cache->priv->revision_change_frozen++;
g_warn_if_fail (cache->priv->revision_change_frozen != 0);
g_rec_mutex_unlock (&cache->priv->lock);
}
/**
* e_cache_thaw_revision_change:
* @cache: an #ECache
*
* Thaws automatic revision change for the @cache. It's the pair
* function of e_cache_freeze_revision_change().
*
* Since: 3.26
**/
void
e_cache_thaw_revision_change (ECache *cache)
{
g_return_if_fail (E_IS_CACHE (cache));
g_rec_mutex_lock (&cache->priv->lock);
if (!cache->priv->revision_change_frozen) {
g_warn_if_fail (cache->priv->revision_change_frozen > 0);
} else {
cache->priv->revision_change_frozen--;
if (!cache->priv->revision_change_frozen &&
cache->priv->needs_revision_change) {
cache->priv->needs_revision_change = FALSE;
e_cache_change_revision (cache);
}
}
g_rec_mutex_unlock (&cache->priv->lock);
}
/**
* e_cache_is_revision_change_frozen:
* @cache: an #ECache
*
* Returns: Whether automatic revision change for the @cache
* is currently frozen.
*
* Since: 3.26
**/
gboolean
e_cache_is_revision_change_frozen (ECache *cache)
{
gboolean frozen;
g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
g_rec_mutex_lock (&cache->priv->lock);
frozen = cache->priv->revision_change_frozen > 0;
g_rec_mutex_unlock (&cache->priv->lock);
return frozen;
}
/**
* 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 *klass;
g_return_if_fail (E_IS_CACHE (cache));
if (!cache->priv->db)
return;
klass = E_CACHE_GET_CLASS (cache);
g_return_if_fail (klass != NULL);
if (klass->erase)
klass->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;
}
/**
* e_cache_contains:
* @cache: an #ECache
* @uid: a unique identifier of an object
* @deleted_flag: one of #ECacheDeletedFlag enum
*
* Checkes whether the @cache contains an object with
* the given @uid.
*
* Returns: Whether the object had been found.
*
* Since: 3.26
**/
gboolean
e_cache_contains (ECache *cache,
const gchar *uid,
ECacheDeletedFlag deleted_flag)
{
guint nrows = 0;
g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
g_return_val_if_fail (uid != NULL, FALSE);
if (deleted_flag == E_CACHE_INCLUDE_DELETED) {
e_cache_sqlite_exec_printf (cache,
"SELECT " E_CACHE_COLUMN_UID " FROM " E_CACHE_TABLE_OBJECTS
" WHERE " E_CACHE_COLUMN_UID " = %Q"
" LIMIT 2",
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"
" LIMIT 2",
e_cache_count_rows_cb, &nrows, NULL, NULL,
uid, E_OFFLINE_STATE_LOCALLY_DELETED);
}
g_warn_if_fail (nrows <= 1);
return nrows > 0;
}
struct GetObjectData {
gchar *object;
gchar **out_revision;
ECacheColumnValues **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 = e_cache_column_values_new ();
e_cache_column_values_put (*gd->out_other_columns, column_names[ii], 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;
}
static gchar *
e_cache_get_object_internal (ECache *cache,
gboolean include_deleted,
const gchar *uid,
gchar **out_revision,
ECacheColumnValues **out_other_columns,
GCancellable *cancellable,
GError **error)
{
struct GetObjectData gd;
gboolean success;
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 (include_deleted) {
success = e_cache_sqlite_exec_printf (cache,
"SELECT * FROM " E_CACHE_TABLE_OBJECTS
" WHERE " E_CACHE_COLUMN_UID " = %Q",
e_cache_get_object_cb, &gd, cancellable, error,
uid);
} else {
success = 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);
}
if (success && !gd.object)
g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
return gd.object;
}
/**
* 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): an out
* variable for #ECacheColumnValues other columns, as defined when creating 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 #ECacheColumnValues with e_cache_column_values_free(), 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,
ECacheColumnValues **out_other_columns,
GCancellable *cancellable,
GError **error)
{
g_return_val_if_fail (E_IS_CACHE (cache), NULL);
g_return_val_if_fail (uid != NULL, NULL);
return e_cache_get_object_internal (cache, FALSE, uid, out_revision, out_other_columns, cancellable, error);
}
/**
* e_cache_get_object_include_deleted:
* @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): an out
* variable for #ECacheColumnValues other columns, as defined when creating the @cache, or %NULL to ignore
* @cancellable: optional #GCancellable object, or %NULL
* @error: return location for a #GError, or %NULL
*
* The same as e_cache_get(), only considers also locally deleted objects.
*
* 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.30
**/
gchar *
e_cache_get_object_include_deleted (ECache *cache,
const gchar *uid,
gchar **out_revision,
ECacheColumnValues **out_other_columns,
GCancellable *cancellable,
GError **error)
{
g_return_val_if_fail (E_IS_CACHE (cache), NULL);
g_return_val_if_fail (uid != NULL, NULL);
return e_cache_get_object_internal (cache, TRUE, uid, out_revision, out_other_columns, cancellable, error);
}
static gboolean
e_cache_put_locked (ECache *cache,
const gchar *uid,
const gchar *revision,
const gchar *object,
ECacheColumnValues *other_columns,
EOfflineState offline_state,
gboolean is_replace,
GCancellable *cancellable,
GError **error)
{
ECacheColumnValues *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 (object != NULL, FALSE);
if (!other_columns) {
my_other_columns = e_cache_column_values_new ();
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 *klass;
klass = E_CACHE_GET_CLASS (cache);
g_return_val_if_fail (klass != NULL, FALSE);
g_return_val_if_fail (klass->put_locked != NULL, FALSE);
success = klass->put_locked (cache, uid, revision, object, other_columns, offline_state, is_replace, cancellable, error);
if (success)
e_cache_change_revision (cache);
}
e_cache_column_values_free (my_other_columns);
return success;
}
/**
* e_cache_put:
* @cache: an #ECache
* @uid: a unique identifier of an object
* @revision: (nullable): a revision of the object
* @object: the object itself
* @other_columns: (nullable): an #ECacheColumnValues with other columns to set; can be %NULL
* @offline_flag: one of #ECacheOfflineFlag, whether putting this object in offline
* @cancellable: optional #GCancellable object, or %NULL
* @error: return location for a #GError, or %NULL
*
* Stores an object into the cache. Depending on @offline_flag, this update
* the object's offline state accordingly. When the @offline_flag is set
* to %E_CACHE_IS_ONLINE, then it's set to #E_OFFLINE_STATE_SYNCED, like
* to be fully synchronized with the server, regardless of its previous
* offline state. 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,
ECacheColumnValues *other_columns,
ECacheOfflineFlag offline_flag,
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 (object != NULL, FALSE);
e_cache_lock (cache, E_CACHE_LOCK_WRITE);
if (offline_flag == E_CACHE_IS_ONLINE) {
is_replace = e_cache_contains (cache, uid, E_CACHE_EXCLUDE_DELETED);
offline_state = E_OFFLINE_STATE_SYNCED;
} else {
is_replace = e_cache_contains (cache, uid, E_CACHE_INCLUDE_DELETED);
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_locked (cache, uid, revision, object, other_columns,
offline_state, is_replace, cancellable, error);
e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
return success;
}
/**
* e_cache_remove:
* @cache: an #ECache
* @uid: a unique identifier of an object
* @offline_flag: one of #ECacheOfflineFlag, whether removing the object in offline
* @cancellable: optional #GCancellable object, or %NULL
* @error: return location for a #GError, or %NULL
*
* Removes the object with the given @uid from the @cache. Based on the @offline_flag,
* it can remove also any information about locally made offline changes. Removing
* the object with %E_CACHE_IS_OFFLINE will still remember it for later use
* with e_cache_get_offline_changes().
*
* Returns: Whether succeeded.
*
* Since: 3.26
**/
gboolean
e_cache_remove (ECache *cache,
const gchar *uid,
ECacheOfflineFlag offline_flag,
GCancellable *cancellable,
GError **error)
{
ECacheClass *klass;
gboolean success = TRUE;
g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
g_return_val_if_fail (uid != NULL, FALSE);
klass = E_CACHE_GET_CLASS (cache);
g_return_val_if_fail (klass != NULL, FALSE);
g_return_val_if_fail (klass->remove_locked != NULL, FALSE);
e_cache_lock (cache, E_CACHE_LOCK_WRITE);
if (offline_flag == E_CACHE_IS_ONLINE) {
success = klass->remove_locked (cache, uid, cancellable, error);
} else {
EOfflineState offline_state;
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 = klass->remove_locked (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);
}
}
}
if (success)
e_cache_change_revision (cache);
e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
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)
{
ECacheClass *klass;
GSList *uids = NULL;
gboolean success;
g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
klass = E_CACHE_GET_CLASS (cache);
g_return_val_if_fail (klass != NULL, FALSE);
g_return_val_if_fail (klass->remove_all_locked != NULL, FALSE);
e_cache_lock (cache, E_CACHE_LOCK_WRITE);
success = e_cache_get_uids (cache, E_CACHE_INCLUDE_DELETED, &uids, NULL, cancellable, error);
if (success && uids)
success = klass->remove_all_locked (cache, uids, cancellable, error);
if (success) {
e_cache_sqlite_maybe_vacuum (cache, cancellable, NULL);
e_cache_change_revision (cache);
}
e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
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_get_count:
* @cache: an #ECache
* @deleted_flag: one of #ECacheDeletedFlag enum
* @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_get_count (ECache *cache,
ECacheDeletedFlag deleted_flag,
GCancellable *cancellable,
GError **error)
{
guint64 nobjects = 0;
g_return_val_if_fail (E_IS_CACHE (cache), 0);
if (deleted_flag == E_CACHE_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
* @deleted_flag: one of #ECacheDeletedFlag enum
* @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,
ECacheDeletedFlag deleted_flag,
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, deleted_flag, NULL,
e_cache_gather_rows_data_cb, &gr, cancellable, error);
}
/**
* e_cache_get_objects:
* @cache: an #ECache
* @deleted_flag: one of #ECacheDeletedFlag enum
* @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,
ECacheDeletedFlag deleted_flag,
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, deleted_flag, 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
* @deleted_flag: one of #ECacheDeletedFlag enum
* @where_clause: (nullable): an optional SQLite WHERE clause part, or %NULL
* @func: (scope call): 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 @deleted_flag 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,
ECacheDeletedFlag deleted_flag,
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 (deleted_flag == E_CACHE_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 (deleted_flag != E_CACHE_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;
}
struct ForeachUpdateRowData {
gchar *uid;
gchar *revision;
gchar *object;
EOfflineState offline_state;
gint ncols;
GPtrArray *column_values;
};
static void
foreach_update_row_data_free (gpointer ptr)
{
struct ForeachUpdateRowData *fr = ptr;
if (fr) {
g_free (fr->uid);
g_free (fr->revision);
g_free (fr->object);
g_ptr_array_free (fr->column_values, TRUE);
g_slice_free (struct ForeachUpdateRowData, fr);
}
}
struct ForeachUpdateData {
gint uid_index;
gint revision_index;
gint object_index;
gint state_index;
GSList *rows; /* struct ForeachUpdateRowData * */
GPtrArray *column_names;
};
static gboolean
e_cache_foreach_update_cb (ECache *cache,
gint ncols,
const gchar *column_names[],
const gchar *column_values[],
gpointer user_data)
{
struct ForeachUpdateData *fu = user_data;
struct ForeachUpdateRowData *rd;
EOfflineState offline_state;
GPtrArray *cnames, *cvalues;
gint ii;
g_return_val_if_fail (fu != NULL, FALSE);
g_return_val_if_fail (column_names != NULL, FALSE);
g_return_val_if_fail (column_values != NULL, FALSE);
if (fu->uid_index == -1 ||
fu->revision_index == -1 ||
fu->object_index == -1 ||
fu->state_index == -1) {
gint ii;
for (ii = 0; ii < ncols && (fu->uid_index == -1 ||
fu->revision_index == -1 ||
fu->object_index == -1 ||
fu->state_index == -1); ii++) {
if (!column_names[ii])
continue;
if (fu->uid_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) == 0) {
fu->uid_index = ii;
} else if (fu->revision_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_REVISION) == 0) {
fu->revision_index = ii;
} else if (fu->object_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_OBJECT) == 0) {
fu->object_index = ii;
} else if (fu->state_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_STATE) == 0) {
fu->state_index = ii;
}
}
}
g_return_val_if_fail (fu->uid_index >= 0 && fu->uid_index < ncols, FALSE);
g_return_val_if_fail (fu->revision_index >= 0 && fu->revision_index < ncols, FALSE);
g_return_val_if_fail (fu->object_index >= 0 && fu->object_index < ncols, FALSE);
g_return_val_if_fail (fu->state_index >= 0 && fu->state_index < ncols, FALSE);
if (!column_values[fu->state_index])
offline_state = E_OFFLINE_STATE_UNKNOWN;
else
offline_state = g_ascii_strtoull (column_values[fu->state_index], NULL, 10);
cnames = fu->column_names ? NULL : g_ptr_array_new_full (ncols, g_free);
cvalues = g_ptr_array_new_full (ncols, g_free);
for (ii = 0; ii < ncols; ii++) {
if (fu->uid_index == ii ||
fu->revision_index == ii ||
fu->object_index == ii ||
fu->state_index == ii) {
continue;
}
if (cnames)
g_ptr_array_add (cnames, g_strdup (column_names[ii]));
g_ptr_array_add (cvalues, g_strdup (column_values[ii]));
}
rd = g_slice_new0 (struct ForeachUpdateRowData);
rd->uid = g_strdup (column_values[fu->uid_index]);
rd->revision = g_strdup (column_values[fu->revision_index]);
rd->object = g_strdup (column_values[fu->object_index]);
rd->offline_state = offline_state;
rd->ncols = cvalues->len;
rd->column_values = cvalues;
if (cnames)
fu->column_names = cnames;
fu->rows = g_slist_prepend (fu->rows, rd);
g_return_val_if_fail (fu->column_names && (gint) fu->column_names->len == rd->ncols, FALSE);
return TRUE;
}
/**
* e_cache_foreach_update:
* @cache: an #ECache
* @deleted_flag: one of #ECacheDeletedFlag enum
* @where_clause: (nullable): an optional SQLite WHERE clause part, or %NULL
* @func: (scope call): an #ECacheUpdateFunc 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
* @deleted_flag and @where_clause, letting the caller update values where
* necessary. The return value of @func is used to determine whether the call
* was successful, not whether there are any changes to be saved. If anything
* fails during the call then the all changes are reverted.
*
* When there are requested any changes by the @func, this function also
* calls e_cache_copy_missing_to_column_values() to ensure no descendant
* column data is lost.
*
* Returns: Whether succeeded.
*
* Since: 3.26
**/
gboolean
e_cache_foreach_update (ECache *cache,
ECacheDeletedFlag deleted_flag,
const gchar *where_clause,
ECacheUpdateFunc func,
gpointer user_data,
GCancellable *cancellable,
GError **error)
{
GString *stmt_begin;
gchar *uid = NULL;
gint n_results;
gboolean has_where = TRUE;
gboolean success;
g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
g_return_val_if_fail (func, FALSE);
e_cache_lock (cache, E_CACHE_LOCK_WRITE);
stmt_begin = g_string_new ("SELECT * FROM " E_CACHE_TABLE_OBJECTS);
if (where_clause) {
g_string_append (stmt_begin, " WHERE ");
if (deleted_flag == E_CACHE_INCLUDE_DELETED) {
g_string_append (stmt_begin, where_clause);
} else {
g_string_append_printf (stmt_begin, E_CACHE_COLUMN_STATE "!=%d AND (%s)",
E_OFFLINE_STATE_LOCALLY_DELETED, where_clause);
}
} else if (deleted_flag != E_CACHE_INCLUDE_DELETED) {
g_string_append_printf (stmt_begin, " WHERE " E_CACHE_COLUMN_STATE "!=%d", E_OFFLINE_STATE_LOCALLY_DELETED);
} else {
has_where = FALSE;
}
do {
GString *stmt;
GSList *link;
struct ForeachUpdateData fu;
fu.uid_index = -1;
fu.revision_index = -1;
fu.object_index = -1;
fu.state_index = -1;
fu.rows = NULL;
fu.column_names = NULL;
stmt = g_string_new (stmt_begin->str);
if (uid) {
if (has_where)
g_string_append (stmt, " AND ");
else
g_string_append (stmt, " WHERE ");
e_cache_sqlite_stmt_append_printf (stmt, E_CACHE_COLUMN_UID ">%Q", uid);
}
g_string_append_printf (stmt, " ORDER BY " E_CACHE_COLUMN_UID " ASC LIMIT %d", E_CACHE_UPDATE_BATCH_SIZE);
success = e_cache_sqlite_exec_internal (cache, stmt->str, e_cache_foreach_update_cb, &fu, cancellable, error);
g_string_free (stmt, TRUE);
if (success) {
n_results = 0;
fu.rows = g_slist_reverse (fu.rows);
for (link = fu.rows; success && link; link = g_slist_next (link), n_results++) {
struct ForeachUpdateRowData *fr = link->data;
success = fr && fr->column_values && fu.column_names;
if (success) {
gchar *new_revision = NULL;
gchar *new_object = NULL;
EOfflineState new_offline_state = fr->offline_state;
ECacheColumnValues *new_other_columns = NULL;
success = func (cache, fr->uid, fr->revision, fr->object, fr->offline_state,
fr->ncols, (const gchar **) fu.column_names->pdata,
(const gchar **) fr->column_values->pdata,
&new_revision, &new_object, &new_offline_state, &new_other_columns,
user_data);
if (success && (
(new_revision && g_strcmp0 (new_revision, fr->revision) != 0) ||
(new_object && g_strcmp0 (new_object, fr->object) != 0) ||
(new_offline_state != fr->offline_state) ||
(new_other_columns && e_cache_column_values_get_size (new_other_columns) > 0))) {
if (!new_other_columns)
new_other_columns = e_cache_column_values_new ();
e_cache_copy_missing_to_column_values (cache, fr->ncols,
(const gchar **) fu.column_names->pdata,
(const gchar **) fr->column_values->pdata,
new_other_columns);
success = e_cache_put_locked (cache,
fr->uid,
new_revision ? new_revision : fr->revision,
new_object ? new_object : fr->object,
new_other_columns,
new_offline_state,
TRUE, cancellable, error);
}
g_free (new_revision);
g_free (new_object);
e_cache_column_values_free (new_other_columns);
if (!g_slist_next (link)) {
g_free (uid);
uid = g_strdup (fr->uid);
}
}
}
}
g_slist_free_full (fu.rows, foreach_update_row_data_free);
if (fu.column_names)
g_ptr_array_free (fu.column_names, TRUE);
} while (success && n_results == E_CACHE_UPDATE_BATCH_SIZE);
g_string_free (stmt_begin, TRUE);
g_free (uid);
e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
return success;
}
/**
* e_cache_copy_missing_to_column_values:
* @cache: an #ECache
* @ncols: count of columns, items in column_names and column_values
* @column_names: (array length=ncols) (element-type utf8): column names
* @column_values: (array length=ncols) (element-type utf8): column values
* @other_columns: (inout): an #ECacheColumnValues to fill
*
* Adds every column value which is not part of the @other_columns to it,
* except of E_CACHE_COLUMN_UID, E_CACHE_COLUMN_REVISION, E_CACHE_COLUMN_OBJECT
* and E_CACHE_COLUMN_STATE columns.
*
* This can be used within the callback of e_cache_foreach_update().
*
* Since: 3.32
**/
void
e_cache_copy_missing_to_column_values (ECache *cache,
gint ncols,
const gchar *column_names[],
const gchar *column_values[],
ECacheColumnValues *other_columns)
{
gint ii;
g_return_if_fail (E_IS_CACHE (cache));
g_return_if_fail (column_names != NULL);
g_return_if_fail (column_values != NULL);
g_return_if_fail (other_columns != NULL);
for (ii = 0; ii < ncols; ii++) {
if (column_names[ii] && column_values[ii] &&
!e_cache_column_values_contains (other_columns, column_names[ii]) &&
g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) != 0 &&
g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_REVISION) != 0 &&
g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_OBJECT) != 0 &&
g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_STATE) != 0) {
e_cache_column_values_put (other_columns, column_names[ii], column_values[ii]);
}
}
}
/**
* 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 = offline_state;
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_contains (cache, uid, E_CACHE_INCLUDE_DELETED)) {
g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
return offline_state;
}
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);
if (!e_cache_contains (cache, uid, E_CACHE_INCLUDE_DELETED)) {
g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
return 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, revision, object, 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, E_CACHE_INCLUDE_DELETED, 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)
{
ECacheClass *klass;
gboolean success;
g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
klass = E_CACHE_GET_CLASS (cache);
g_return_val_if_fail (klass != NULL, FALSE);
g_return_val_if_fail (klass->clear_offline_changes_locked != NULL, FALSE);
e_cache_lock (cache, E_CACHE_LOCK_WRITE);
success = klass->clear_offline_changes_locked (cache, cancellable, error);
e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
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 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: (scope call): 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_append_printf:
* @stmt: a #GString statement to append to
* @format: a printf-like format
* @...: arguments for the @format
*
* Appends an SQLite statement fragment based on the @format and
* its arguments to the @stmt.
* The @format can contain any values recognized by sqlite3_mprintf().
*
* Since: 3.26
**/
void
e_cache_sqlite_stmt_append_printf (GString *stmt,
const gchar *format,
...)
{
va_list args;
gchar *tmp_stmt;
g_return_if_fail (stmt != NULL);
g_return_if_fail (format != NULL);
va_start (args, format);
tmp_stmt = sqlite3_vmprintf (format, args);
va_end (args);
g_string_append (stmt, tmp_stmt);
sqlite3_free (tmp_stmt);
}
/**
* e_cache_sqlite_stmt_printf:
* @format: a printf-like format
* @...: arguments for the @format
*
* Creates an 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,
ECacheColumnValues *other_columns,
EOfflineState offline_state,
gboolean is_replace,
GCancellable *cancellable,
GError **error)
{
GString *statement, *other_names = NULL, *other_values = NULL;
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 (object != NULL, FALSE);
statement = g_string_sized_new (255);
e_cache_sqlite_stmt_append_printf (statement, "INSERT OR REPLACE INTO %Q ("
E_CACHE_COLUMN_UID ","
E_CACHE_COLUMN_REVISION ","
E_CACHE_COLUMN_OBJECT ","
E_CACHE_COLUMN_STATE,
E_CACHE_TABLE_OBJECTS);
if (other_columns) {
GHashTableIter iter;
gpointer key, value;
e_cache_column_values_init_iter (other_columns, &iter);
while (g_hash_table_iter_next (&iter, &key, &value)) {
if (!other_names)
other_names = g_string_new ("");
g_string_append_c (other_names, ',');
e_cache_sqlite_stmt_append_printf (other_names, "%Q", key);
if (!other_values)
other_values = g_string_new ("");
g_string_append_c (other_values, ',');
if (value) {
e_cache_sqlite_stmt_append_printf (other_values, "%Q", value);
} else {
g_string_append (other_values, "NULL");
}
}
}
if (other_names)
g_string_append (statement, other_names->str);
g_string_append (statement, ") VALUES (");
e_cache_sqlite_stmt_append_printf (statement, "%Q,%Q,%Q,%d", uid, revision ? revision : "", object, offline_state);
if (other_values)
g_string_append (statement, other_values->str);
g_string_append_c (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_remove_locked_default (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;
}
static gboolean
e_cache_remove_all_locked_default (ECache *cache,
const GSList *uids,
GCancellable *cancellable,
GError **error)
{
const GSList *link;
gboolean success = TRUE;
g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
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);
}
return success;
}
static gboolean
e_cache_clear_offline_changes_locked_default (ECache *cache,
GCancellable *cancellable,
GError **error)
{
gboolean success;
g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
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);
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,
ECacheColumnValues *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;
g_clear_pointer (&cache->priv->db, sqlite3_close);
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 *klass)
{
GObjectClass *object_class;
object_class = G_OBJECT_CLASS (klass);
object_class->finalize = e_cache_finalize;
klass->put_locked = e_cache_put_locked_default;
klass->remove_locked = e_cache_remove_locked_default;
klass->remove_all_locked = e_cache_remove_all_locked_default;
klass->clear_offline_changes_locked = e_cache_clear_offline_changes_locked_default;
klass->before_put = e_cache_before_put_default;
klass->before_remove = e_cache_before_remove_default;
signals[BEFORE_PUT] = g_signal_new (
"before-put",
G_OBJECT_CLASS_TYPE (klass),
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,
E_TYPE_CACHE_COLUMN_VALUES,
G_TYPE_BOOLEAN,
G_TYPE_CANCELLABLE,
G_TYPE_POINTER);
signals[BEFORE_REMOVE] = g_signal_new (
"before-remove",
G_OBJECT_CLASS_TYPE (klass),
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_POINTER);
signals[REVISION_CHANGED] = g_signal_new (
"revision-changed",
G_OBJECT_CLASS_TYPE (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ECacheClass, revision_changed),
NULL,
NULL,
g_cclosure_marshal_generic,
G_TYPE_NONE, 0,
G_TYPE_NONE);
e_sqlite3_vfs_init ();
}
static void
e_cache_init (ECache *cache)
{
cache->priv = e_cache_get_instance_private (cache);
cache->priv->filename = NULL;
cache->priv->db = NULL;
cache->priv->cancellable = NULL;
cache->priv->in_transaction = 0;
cache->priv->revision_change_frozen = 0;
cache->priv->revision_counter = 0;
cache->priv->last_revision_time = 0;
cache->priv->needs_revision_change = FALSE;
g_rec_mutex_init (&cache->priv->lock);
}