[evolution-data-server/wip/offline-cache] Prototype an EBookCache
- From: Milan Crha <mcrha src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [evolution-data-server/wip/offline-cache] Prototype an EBookCache
- Date: Tue, 21 Mar 2017 17:31:38 +0000 (UTC)
commit c6e0c507f1581d21970b36574a822b3401738df5
Author: Milan Crha <mcrha redhat com>
Date: Wed Feb 1 15:09:50 2017 +0100
Prototype an EBookCache
Most of the code is Tristan's awesome EBookSqlite adopted to ECache.
po/POTFILES.in | 1 +
src/addressbook/libedata-book/CMakeLists.txt | 2 +
src/addressbook/libedata-book/e-book-cache.c | 5702 +++++++++++++++++++++++++
src/addressbook/libedata-book/e-book-cache.h | 276 ++
src/addressbook/libedata-book/libedata-book.h | 1 +
src/libebackend/e-cache.c | 487 ++-
src/libebackend/e-cache.h | 71 +-
7 files changed, 6468 insertions(+), 72 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 7ea5e94..2f82f57 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -18,6 +18,7 @@ src/addressbook/libebook/e-destination.c
src/addressbook/libedata-book/e-book-backend.c
src/addressbook/libedata-book/e-book-backend-sexp.c
src/addressbook/libedata-book/e-book-backend-sqlitedb.c
+src/addressbook/libedata-book/e-book-cache.c
src/addressbook/libedata-book/e-book-sqlite.c
src/addressbook/libedata-book/e-data-book.c
src/addressbook/libedata-book/e-data-book-cursor.c
diff --git a/src/addressbook/libedata-book/CMakeLists.txt b/src/addressbook/libedata-book/CMakeLists.txt
index a247c7b..8935769 100644
--- a/src/addressbook/libedata-book/CMakeLists.txt
+++ b/src/addressbook/libedata-book/CMakeLists.txt
@@ -16,6 +16,7 @@ set(SOURCES
e-book-backend-cache.c
e-book-backend-sqlitedb.c
e-book-backend.c
+ e-book-cache.c
e-book-sqlite.c
e-data-book.c
e-data-book-cursor.c
@@ -33,6 +34,7 @@ set(HEADERS
e-book-backend-sexp.h
e-book-backend-summary.h
e-book-backend.h
+ e-book-cache.h
e-data-book-factory.h
e-data-book-view.h
e-data-book.h
diff --git a/src/addressbook/libedata-book/e-book-cache.c b/src/addressbook/libedata-book/e-book-cache.c
new file mode 100644
index 0000000..881bff8
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-cache.c
@@ -0,0 +1,5702 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ *
+ * 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/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+/**
+ * SECTION: e-book-cache
+ * @include: libedata-book/libedata-book.h
+ * @short_description: An #ECache descendant for addressbooks
+ *
+ * The #EBookCache is an API for storing and looking up #EContacts
+ * in an #ECache. It also supports cursors.
+ *
+ * The API is thread safe, in the similar was as the #ECache is.
+ *
+ * Any operations which can take a lot of time to complete (depending
+ * on the size of your addressbook) can be cancelled using a #GCancellable.
+ *
+ * Depending on your summary configuration, your mileage will vary. Refer
+ * to the #ESourceBackendSummarySetup for configuring your addressbook
+ * for the type of usage you mean to make of it.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <locale.h>
+#include <string.h>
+#include <errno.h>
+#include <sqlite3.h>
+
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+
+#include "e-book-backend-sexp.h"
+
+#include "e-book-cache.h"
+
+#define E_BOOK_CACHE_VERSION 1
+#define INSERT_MULTI_STMT_BYTES 128
+#define COLUMN_DEFINITION_BYTES 32
+#define GENERATED_QUERY_BYTES 1024
+
+/* We use a 64 bitmask to track which auxiliary tables
+ * are needed to satisfy a query, it's doubtful that
+ * anyone will need an addressbook with 64 fields configured
+ * in the summary.
+ */
+#define EBC_MAX_SUMMARY_FIELDS 64
+
+/* 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 EBC_CANCEL_BATCH_SIZE 200
+
+#define EBC_ESCAPE_SEQUENCE "ESCAPE '^'"
+
+/* Names for custom functions */
+#define EBC_FUNC_COMPARE_VCARD "compare_vcard"
+#define EBC_FUNC_EQPHONE_EXACT "eqphone_exact"
+#define EBC_FUNC_EQPHONE_NATIONAL "eqphone_national"
+#define EBC_FUNC_EQPHONE_SHORT "eqphone_short"
+
+/* Fallback collations are generated as with a prefix and an EContactField name */
+#define EBC_COLLATE_PREFIX "book_cache_"
+
+/* A special vcard attribute that we use only for private vcards */
+#define EBC_VCARD_SORT_KEY "X-EVOLUTION-SORT-KEY"
+
+/* Key names for e_cache_dup/set_key{_int} functions */
+#define EBC_KEY_MULTIVALUES "multivalues"
+#define EBC_KEY_LC_COLLATE "lc_collate"
+#define EBC_KEY_COUNTRYCODE "countrycode"
+
+/* Suffixes for column names used to store specialized data */
+#define EBC_SUFFIX_REVERSE "reverse"
+#define EBC_SUFFIX_SORT_KEY "localized"
+#define EBC_SUFFIX_PHONE "phone"
+#define EBC_SUFFIX_COUNTRY "country"
+
+/* Track EBookIndexType's in a bit mask */
+#define INDEX_FLAG(type) (1 << E_BOOK_INDEX_##type)
+
+#define EBC_COLUMN_EXTRA "bdata"
+
+typedef struct {
+ EContactField field_id; /* The EContact field */
+ GType type; /* The GType (only support string or gboolean) */
+ const gchar *dbname; /* The key for this field in the sqlite3 table */
+ gint index; /* Types of searches this field should support (see EBookIndexType) */
+ gchar *dbname_idx_suffix; /* dbnames for various indexes; can be NULL */
+ gchar *dbname_idx_phone;
+ gchar *dbname_idx_country;
+ gchar *dbname_idx_sort_key;
+ gchar *aux_table; /* Name of auxiliary table for this field, for multivalued fields
only */
+ gchar *aux_table_symbolic; /* Symbolic name of auxiliary table used in queries */
+} SummaryField;
+
+struct _EBookCachePrivate {
+ ESource *source; /* Optional, can be %NULL */
+
+ /* Parameters and settings */
+ gchar *locale; /* The current locale */
+ gchar *region_code; /* Region code (for phone number parsing) */
+
+ /* Summary configuration */
+ SummaryField *summary_fields;
+ gint n_summary_fields;
+
+ ECollator *collator; /* The ECollator to create sort keys for any sortable fields */
+};
+
+enum {
+ PROP_0,
+ PROP_LOCALE
+};
+
+G_DEFINE_TYPE_WITH_CODE (EBookCache, e_book_cache, E_TYPE_CACHE,
+ G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL))
+
+G_DEFINE_BOXED_TYPE (EBookCacheSearchData, e_book_cache_search_data, e_book_cache_search_data_copy,
e_book_cache_search_data_free)
+
+/**
+ * e_book_cache_search_data_new:
+ * @uid: a contact UID; cannot be %NULL
+ * @vcard: the contact as a vCard string; cannot be %NULL
+ * @extra: (nullable): any extra data stoed with the contact, or %NULL
+ *
+ * Creates a new EBookCacheSearchData prefilled with the given values.
+ *
+ * Returns: (transfer full): A new #EBookCacheSearchData. Free it with
+ * e_book_cache_search_data_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EBookCacheSearchData *
+e_book_cache_search_data_new (const gchar *uid,
+ const gchar *vcard,
+ const gchar *extra)
+{
+ EBookCacheSearchData *data;
+
+ g_return_val_if_fail (uid != NULL, NULL);
+ g_return_val_if_fail (vcard != NULL, NULL);
+
+ data = g_new0 (EBookCacheSearchData, 1);
+ data->uid = g_strdup (uid);
+ data->vcard = g_strdup (vcard);
+ data->extra = g_strdup (extra);
+
+ return data;
+}
+
+/**
+ * e_book_cache_search_data_copy:
+ * @data: (nullable): a source #EBookCacheSearchData to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @data. Free it with
+ * e_book_cache_search_data_free() when no longer needed.
+ * If the @data is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+EBookCacheSearchData *
+e_book_cache_search_data_copy (const EBookCacheSearchData *data)
+{
+ if (!data)
+ return NULL;
+
+ return e_book_cache_search_data_new (data->uid, data->vcard, data->extra);
+}
+
+/**
+ * e_book_cache_search_data_free:
+ * @data: (nullable): an #EBookCacheSearchData
+ *
+ * Frees the @data structure, previously allocated with e_book_cache_search_data_new()
+ * or e_book_cache_search_data_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_book_cache_search_data_free (EBookCacheSearchData *data)
+{
+ if (data) {
+ g_free (data->uid);
+ g_free (data->vcard);
+ g_free (data->extra);
+ g_free (data);
+ }
+}
+
+/* Default summary configuration */
+static EContactField default_summary_fields[] = {
+ E_CONTACT_UID,
+ E_CONTACT_REV,
+ E_CONTACT_FILE_AS,
+ E_CONTACT_NICKNAME,
+ E_CONTACT_FULL_NAME,
+ E_CONTACT_GIVEN_NAME,
+ E_CONTACT_FAMILY_NAME,
+ E_CONTACT_EMAIL,
+ E_CONTACT_TEL,
+ E_CONTACT_IS_LIST,
+ E_CONTACT_LIST_SHOW_ADDRESSES,
+ E_CONTACT_WANTS_HTML,
+ E_CONTACT_X509_CERT
+};
+
+/* Create indexes on full_name and email fields as autocompletion
+ * queries would mainly rely on this.
+ *
+ * Add sort keys for name fields as those are likely targets for
+ * cursor usage.
+ */
+static EContactField default_indexed_fields[] = {
+ E_CONTACT_FULL_NAME,
+ E_CONTACT_NICKNAME,
+ E_CONTACT_FILE_AS,
+ E_CONTACT_GIVEN_NAME,
+ E_CONTACT_FAMILY_NAME,
+ E_CONTACT_EMAIL,
+ E_CONTACT_FILE_AS,
+ E_CONTACT_FAMILY_NAME,
+ E_CONTACT_GIVEN_NAME
+};
+
+static EBookIndexType default_index_types[] = {
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_SORT_KEY,
+ E_BOOK_INDEX_SORT_KEY,
+ E_BOOK_INDEX_SORT_KEY
+};
+
+/******************************************************
+ * Summary Fields *
+ ******************************************************/
+
+static ECacheColumnInfo *
+column_info_new (SummaryField *field,
+ const gchar *column_name,
+ const gchar *column_type,
+ const gchar *idx_prefix)
+{
+ ECacheColumnInfo *info;
+ gchar *index = NULL;
+
+ g_return_val_if_fail (column_name != NULL, NULL);
+
+ if (!column_type) {
+ if (field->type == G_TYPE_STRING)
+ column_type = "TEXT";
+ else if (field->type == G_TYPE_BOOLEAN || field->type == E_TYPE_CONTACT_CERT)
+ column_type = "INTEGER";
+ else if (field->type == E_TYPE_CONTACT_ATTR_LIST)
+ column_type = "TEXT";
+ else
+ g_warn_if_reached ();
+ }
+
+ if (idx_prefix)
+ index = g_strconcat (idx_prefix, "_", field->dbname, NULL);
+
+ info = e_cache_column_info_new (column_name, column_type, index);
+
+ g_free (index);
+
+ return info;
+}
+
+static gint
+summary_field_array_index (GArray *array,
+ EContactField field)
+{
+ gint ii;
+
+ for (ii = 0; ii < array->len; ii++) {
+ SummaryField *iter = &g_array_index (array, SummaryField, ii);
+ if (field == iter->field_id)
+ return ii;
+ }
+
+ return -1;
+}
+
+static SummaryField *
+summary_field_append (GArray *array,
+ EContactField field_id,
+ GError **error)
+{
+ const gchar *dbname = NULL;
+ GType type = G_TYPE_INVALID;
+ gint idx;
+ SummaryField new_field = { 0, };
+
+ if (field_id < 1 || field_id >= E_CONTACT_FIELD_LAST) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_UNSUPPORTED_FIELD,
+ _("Unsupported contact field “%d” specified in summary"),
+ field_id);
+
+ return NULL;
+ }
+
+ /* Avoid including the same field twice in the summary */
+ idx = summary_field_array_index (array, field_id);
+ if (idx >= 0)
+ return &g_array_index (array, SummaryField, idx);
+
+ /* Resolve some exceptions, we store these
+ * specific contact fields with different names
+ * than those found in the EContactField table
+ */
+ switch (field_id) {
+ case E_CONTACT_UID:
+ case E_CONTACT_REV:
+ /* Skip these, it's already in the ECache */
+ return NULL;
+ case E_CONTACT_IS_LIST:
+ dbname = "is_list";
+ break;
+ default:
+ dbname = e_contact_field_name (field_id);
+ break;
+ }
+
+ type = e_contact_field_type (field_id);
+
+ if (type != G_TYPE_STRING &&
+ type != G_TYPE_BOOLEAN &&
+ type != E_TYPE_CONTACT_CERT &&
+ type != E_TYPE_CONTACT_ATTR_LIST) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_UNSUPPORTED_FIELD,
+ _("Contact field “%s” of type “%s” specified in summary, "
+ "but only boolean, string and string list field types are supported"),
+ e_contact_pretty_name (field_id), g_type_name (type));
+
+ return NULL;
+ }
+
+ if (type == E_TYPE_CONTACT_ATTR_LIST) {
+ new_field.aux_table = g_strconcat ("attrlist", "_", dbname, "_list", NULL);
+ new_field.aux_table_symbolic = g_strconcat (dbname, "_list", NULL);
+ }
+
+ new_field.field_id = field_id;
+ new_field.dbname = dbname;
+ new_field.type = type;
+ new_field.index = 0;
+
+ g_array_append_val (array, new_field);
+
+ return &g_array_index (array, SummaryField, array->len - 1);
+}
+
+static void
+summary_fields_add_indexes (GArray *array,
+ EContactField *indexes,
+ EBookIndexType *index_types,
+ gint n_indexes)
+{
+ gint ii, jj;
+
+ for (ii = 0; ii < array->len; ii++) {
+ SummaryField *sfield = &g_array_index (array, SummaryField, ii);
+
+ for (jj = 0; jj < n_indexes; jj++) {
+ if (sfield->field_id == indexes[jj])
+ sfield->index |= (1 << index_types[jj]);
+
+ }
+ }
+}
+
+static inline gint
+summary_field_get_index (EBookCache *book_cache,
+ EContactField field_id)
+{
+ gint ii;
+
+ for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+ if (book_cache->priv->summary_fields[ii].field_id == field_id)
+ return ii;
+ }
+
+ return -1;
+}
+
+static inline SummaryField *
+summary_field_get (EBookCache *book_cache,
+ EContactField field_id)
+{
+ gint index;
+
+ index = summary_field_get_index (book_cache, field_id);
+ if (index >= 0)
+ return &(book_cache->priv->summary_fields[index]);
+
+ return NULL;
+}
+
+static void
+summary_field_init_dbnames (SummaryField *field)
+{
+ if (field->type == G_TYPE_STRING && (field->index & INDEX_FLAG (SORT_KEY))) {
+ field->dbname_idx_sort_key = g_strconcat (field->dbname, "_", EBC_SUFFIX_SORT_KEY, NULL);
+ }
+
+ if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+ (field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ field->dbname_idx_suffix = g_strconcat (field->dbname, "_", EBC_SUFFIX_REVERSE, NULL);
+ }
+
+ if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+ (field->index & INDEX_FLAG (PHONE)) != 0) {
+ field->dbname_idx_phone = g_strconcat (field->dbname, "_", EBC_SUFFIX_PHONE, NULL);
+ field->dbname_idx_country = g_strconcat (field->dbname, "_", EBC_SUFFIX_COUNTRY, NULL);
+ }
+}
+
+static void
+summary_field_prepend_columns (SummaryField *field,
+ GSList **out_columns)
+{
+ ECacheColumnInfo *info;
+
+ /* Doesn't hurt to verify a bit more here, this shouldn't happen though */
+ g_return_if_fail (
+ field->type == G_TYPE_STRING ||
+ field->type == G_TYPE_BOOLEAN ||
+ field->type == E_TYPE_CONTACT_CERT ||
+ field->type == E_TYPE_CONTACT_ATTR_LIST);
+
+ /* Normal / default column */
+ info = column_info_new (field, field->dbname, NULL,
+ (field->index & INDEX_FLAG (PREFIX)) != 0 ? "INDEX" : NULL);
+ *out_columns = g_slist_prepend (*out_columns, info);
+
+ /* Localized column, for storing sort keys */
+ if (field->type == G_TYPE_STRING && (field->index & INDEX_FLAG (SORT_KEY))) {
+ info = column_info_new (field, field->dbname_idx_sort_key, "TEXT", "SINDEX");
+ *out_columns = g_slist_prepend (*out_columns, info);
+ }
+
+ /* Suffix match column */
+ if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+ (field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ info = column_info_new (field, field->dbname_idx_suffix, "TEXT", "RINDEX");
+ *out_columns = g_slist_prepend (*out_columns, info);
+ }
+
+ /* Phone match columns */
+ if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+ (field->index & INDEX_FLAG (PHONE)) != 0) {
+
+ /* One indexed column for storing the national number */
+ info = column_info_new (field, field->dbname_idx_phone, "TEXT", "PINDEX");
+ *out_columns = g_slist_prepend (*out_columns, info);
+
+ /* One integer column for storing the country code */
+ info = column_info_new (field, field->dbname_idx_country, "INTEGER DEFAULT 0", NULL);
+ *out_columns = g_slist_prepend (*out_columns, info);
+ }
+}
+
+static void
+summary_fields_array_free (SummaryField *fields,
+ gint n_fields)
+{
+ gint ii;
+
+ for (ii = 0; ii < n_fields; ii++) {
+ g_free (fields[ii].dbname_idx_suffix);
+ g_free (fields[ii].dbname_idx_phone);
+ g_free (fields[ii].dbname_idx_country);
+ g_free (fields[ii].dbname_idx_sort_key);
+ g_free (fields[ii].aux_table);
+ g_free (fields[ii].aux_table_symbolic);
+ }
+
+ g_free (fields);
+}
+
+/******************************************************
+ * Functions installed into the SQLite *
+ ******************************************************/
+
+/* Implementation for REGEXP keyword */
+static void
+ebc_regexp (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ GRegex *regex;
+ const gchar *expression;
+ const gchar *text;
+
+ /* Reuse the same GRegex for all REGEXP queries with the same expression */
+ regex = sqlite3_get_auxdata (context, 0);
+ if (!regex) {
+ GError *error = NULL;
+
+ expression = (const gchar *) sqlite3_value_text (argv[0]);
+
+ regex = g_regex_new (expression, 0, 0, &error);
+
+ if (!regex) {
+ sqlite3_result_error (
+ context,
+ error ? error->message :
+ _("Error parsing regular expression"),
+ -1);
+ g_clear_error (&error);
+ return;
+ }
+
+ /* SQLite will take care of freeing the GRegex when we're done with the query */
+ sqlite3_set_auxdata (context, 0, regex, (GDestroyNotify) g_regex_unref);
+ }
+
+ /* Now perform the comparison */
+ text = (const gchar *) sqlite3_value_text (argv[1]);
+ if (text != NULL) {
+ gboolean match;
+
+ match = g_regex_match (regex, text, 0, NULL);
+ sqlite3_result_int (context, match ? 1 : 0);
+ }
+}
+
+/* Implementation of EBC_FUNC_COMPARE_VCARD (fallback for non-summary queries) */
+static void
+ebc_compare_vcard (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ EBookBackendSExp *sexp = NULL;
+ const gchar *text;
+ const gchar *vcard;
+
+ /* Reuse the same sexp for all queries with the same search expression */
+ sexp = sqlite3_get_auxdata (context, 0);
+ if (!sexp) {
+
+ /* The first argument will be reused for many rows */
+ text = (const gchar *) sqlite3_value_text (argv[0]);
+ if (text) {
+ sexp = e_book_backend_sexp_new (text);
+ sqlite3_set_auxdata (
+ context, 0,
+ sexp,
+ g_object_unref);
+ }
+
+ /* This shouldn't happen, catch invalid sexp in preflight */
+ if (!sexp) {
+ sqlite3_result_int (context, 0);
+ return;
+ }
+
+ }
+
+ /* Reuse the same vcard as much as possible (it can be referred to more than
+ * once in the query, so it can be reused for multiple comparisons on the same row)
+ */
+ vcard = sqlite3_get_auxdata (context, 1);
+ if (!vcard) {
+ vcard = (const gchar *) sqlite3_value_text (argv[1]);
+
+ if (vcard)
+ sqlite3_set_auxdata (context, 1, g_strdup (vcard), g_free);
+ }
+
+ /* A NULL vcard can never match */
+ if (!vcard || !*vcard) {
+ sqlite3_result_int (context, 0);
+ return;
+ }
+
+ /* Compare this vcard */
+ if (e_book_backend_sexp_match_vcard (sexp, vcard))
+ sqlite3_result_int (context, 1);
+ else
+ sqlite3_result_int (context, 0);
+}
+
+static void
+ebc_eqphone (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv,
+ EPhoneNumberMatch requested_match)
+{
+ EBookCache *ebc = sqlite3_user_data (context);
+ EPhoneNumber *input_phone = NULL, *row_phone = NULL;
+ EPhoneNumberMatch match = E_PHONE_NUMBER_MATCH_NONE;
+ const gchar *text;
+
+ /* Reuse the same phone number for all queries with the same phone number argument */
+ input_phone = sqlite3_get_auxdata (context, 0);
+ if (!input_phone) {
+
+ /* The first argument will be reused for many rows */
+ text = (const gchar *) sqlite3_value_text (argv[0]);
+ if (text) {
+
+ /* Ignore errors, they are fine for phone numbers */
+ input_phone = e_phone_number_from_string (text, ebc->priv->region_code, NULL);
+
+ /* SQLite will take care of freeing the EPhoneNumber when we're done with the
expression */
+ if (input_phone)
+ sqlite3_set_auxdata (
+ context, 0,
+ input_phone,
+ (GDestroyNotify) e_phone_number_free);
+ }
+ }
+
+ /* This shouldn't happen, as we catch invalid phone number queries in preflight
+ */
+ if (!input_phone) {
+ sqlite3_result_int (context, 0);
+ return;
+ }
+
+ /* Parse the phone number for this row */
+ text = (const gchar *) sqlite3_value_text (argv[1]);
+ if (text != NULL) {
+ row_phone = e_phone_number_from_string (text, ebc->priv->region_code, NULL);
+
+ /* And perform the comparison */
+ if (row_phone) {
+ match = e_phone_number_compare (input_phone, row_phone);
+
+ e_phone_number_free (row_phone);
+ }
+ }
+
+ /* Now report the result */
+ if (match != E_PHONE_NUMBER_MATCH_NONE &&
+ match <= requested_match)
+ sqlite3_result_int (context, 1);
+ else
+ sqlite3_result_int (context, 0);
+}
+
+/* Exact phone number match function: EBC_FUNC_EQPHONE_EXACT */
+static void
+ebc_eqphone_exact (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ ebc_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_EXACT);
+}
+
+/* National phone number match function: EBC_FUNC_EQPHONE_NATIONAL */
+static void
+ebc_eqphone_national (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ ebc_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_NATIONAL);
+}
+
+/* Short phone number match function: EBC_FUNC_EQPHONE_SHORT */
+static void
+ebc_eqphone_short (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ ebc_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_SHORT);
+}
+
+typedef void (*EBCCustomFunc) (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv);
+
+typedef struct {
+ const gchar *name;
+ EBCCustomFunc func;
+ gint arguments;
+} EBCCustomFuncTab;
+
+static EBCCustomFuncTab ebc_custom_functions[] = {
+ { "regexp", ebc_regexp, 2 }, /* regexp (expression, column_data) */
+ { EBC_FUNC_COMPARE_VCARD, ebc_compare_vcard, 2 }, /* compare_vcard (sexp, vcard) */
+ { EBC_FUNC_EQPHONE_EXACT, ebc_eqphone_exact, 2 }, /* eqphone_exact (search_input, column_data)
*/
+ { EBC_FUNC_EQPHONE_NATIONAL, ebc_eqphone_national, 2 }, /* eqphone_national (search_input,
column_data) */
+ { EBC_FUNC_EQPHONE_SHORT, ebc_eqphone_short, 2 }, /* eqphone_national (search_input,
column_data) */
+};
+
+/******************************************************
+ * Fallback Collation Sequences *
+ ******************************************************
+ *
+ * The fallback simply compares vcards, vcards which have been
+ * stored on the cursor will have a preencoded key (these
+ * utilities encode & decode that key).
+ */
+static gchar *
+ebc_encode_vcard_sort_key (const gchar *sort_key)
+{
+ EVCard *vcard = e_vcard_new ();
+ gchar *base64;
+ gchar *encoded;
+
+ /* Encode this otherwise e-vcard messes it up */
+ base64 = g_base64_encode ((const guchar *) sort_key, strlen (sort_key));
+ e_vcard_append_attribute_with_value (
+ vcard,
+ e_vcard_attribute_new (NULL, EBC_VCARD_SORT_KEY),
+ base64);
+ encoded = e_vcard_to_string (vcard, EVC_FORMAT_VCARD_30);
+
+ g_free (base64);
+ g_object_unref (vcard);
+
+ return encoded;
+}
+
+static gchar *
+ebc_decode_vcard_sort_key_from_vcard (EVCard *vcard)
+{
+ EVCardAttribute *attr;
+ GList *values = NULL;
+ gchar *sort_key = NULL;
+ gchar *base64 = NULL;
+
+ attr = e_vcard_get_attribute (vcard, EBC_VCARD_SORT_KEY);
+ if (attr)
+ values = e_vcard_attribute_get_values (attr);
+
+ if (values && values->data) {
+ gsize len;
+
+ base64 = g_strdup (values->data);
+
+ sort_key = (gchar *) g_base64_decode (base64, &len);
+ g_free (base64);
+ }
+
+ return sort_key;
+}
+
+static gchar *
+ebc_decode_vcard_sort_key (const gchar *encoded)
+{
+ EVCard *vcard;
+ gchar *sort_key;
+
+ vcard = e_vcard_new_from_string (encoded);
+ sort_key = ebc_decode_vcard_sort_key_from_vcard (vcard);
+ g_object_unref (vcard);
+
+ return sort_key;
+}
+
+static gchar *
+convert_phone (const gchar *normal,
+ const gchar *region_code,
+ gint *out_country_code)
+{
+ EPhoneNumber *number = NULL;
+ gchar *national_number = NULL;
+ gint country_code = 0;
+
+ /* Don't warn about erronous phone number strings, it's a perfectly normal
+ * use case for users to enter notes instead of phone numbers in the phone
+ * number contact fields, such as "Ask Jenny for Lisa's phone number"
+ */
+ if (normal && e_phone_number_is_supported ())
+ number = e_phone_number_from_string (normal, region_code, NULL);
+
+ if (number) {
+ EPhoneNumberCountrySource source;
+
+ national_number = e_phone_number_get_national_number (number);
+ country_code = e_phone_number_get_country_code (number, &source);
+ e_phone_number_free (number);
+
+ if (source == E_PHONE_NUMBER_COUNTRY_FROM_DEFAULT)
+ country_code = 0;
+ }
+
+ if (out_country_code)
+ *out_country_code = country_code;
+
+ return national_number;
+}
+
+static gchar *
+remove_leading_zeros (gchar *number)
+{
+ gchar *trimmed = NULL;
+ gchar *tmp = number;
+
+ g_return_val_if_fail (NULL != number, NULL);
+
+ while ('0' == *tmp)
+ tmp++;
+ trimmed = g_strdup (tmp);
+ g_free (number);
+
+ return trimmed;
+}
+
+static void
+ebc_fill_other_columns (EBookCache *book_cache,
+ EContact *contact,
+ GHashTable *other_columns)
+{
+ gint ii;
+
+ g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+ g_return_if_fail (E_IS_CONTACT (contact));
+ g_return_if_fail (other_columns != NULL);
+
+ for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+
+ if (field->field_id == E_CONTACT_UID ||
+ field->field_id == E_CONTACT_REV) {
+ continue;
+ }
+
+ if (field->type == G_TYPE_STRING) {
+ gchar *val;
+ gchar *normal;
+ gchar *str;
+
+ val = e_contact_get (contact, field->field_id);
+ normal = e_util_utf8_normalize (val);
+
+ g_hash_table_insert (other_columns, (gpointer) field->dbname, normal);
+
+ if ((field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ if (val)
+ str = e_collator_generate_key (book_cache->priv->collator, val, NULL);
+ else
+ str = g_strdup ("");
+
+ g_hash_table_insert (other_columns, field->dbname_idx_sort_key, str);
+ }
+
+ if ((field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ if (normal)
+ str = g_utf8_strreverse (normal, -1);
+ else
+ str = NULL;
+
+ g_hash_table_insert (other_columns, field->dbname_idx_suffix, str);
+ }
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+ gint country_code;
+
+ str = convert_phone (normal, book_cache->priv->region_code, &country_code);
+ str = remove_leading_zeros (str);
+
+ g_hash_table_insert (other_columns, field->dbname_idx_phone, str);
+
+ str = g_strdup_printf ("%d", country_code);
+
+ g_hash_table_insert (other_columns, field->dbname_idx_country, str);
+ }
+
+ g_free (val);
+ } else if (field->type == G_TYPE_BOOLEAN) {
+ gboolean val;
+
+ val = e_contact_get (contact, field->field_id) ? TRUE : FALSE;
+
+ g_hash_table_insert (other_columns, (gpointer) field->dbname, g_strdup_printf ("%d",
val ? 1 : 0));
+ } else if (field->type == E_TYPE_CONTACT_CERT) {
+ EContactCert *cert = NULL;
+
+ cert = e_contact_get (contact, field->field_id);
+
+ /* We don't actually store the cert; only a boolean to indicate
+ * that is *has* a cert. */
+ g_hash_table_insert (other_columns, (gpointer) field->dbname, g_strdup_printf ("%d",
cert ? 1 : 0));
+ e_contact_cert_free (cert);
+ } else if (field->type != E_TYPE_CONTACT_ATTR_LIST) {
+ g_warn_if_reached ();
+ }
+ }
+}
+
+static inline void
+format_column_declaration (GString *string,
+ ECacheColumnInfo *info)
+{
+ g_string_append (string, info->name);
+ g_string_append_c (string, ' ');
+
+ g_string_append (string, info->type);
+
+}
+
+static gboolean
+ebc_init_aux_tables (EBookCache *book_cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GString *string;
+ gboolean success = TRUE;
+ gchar *tmp;
+ gint ii;
+
+ for (ii = 0; success && ii < book_cache->priv->n_summary_fields; ii++) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+ GSList *aux_columns = NULL, *link;
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ summary_field_prepend_columns (field, &aux_columns);
+ if (!aux_columns)
+ continue;
+
+ /* Create the auxiliary table for this multi valued field */
+ string = g_string_sized_new (
+ COLUMN_DEFINITION_BYTES * 3 +
+ COLUMN_DEFINITION_BYTES * g_slist_length (aux_columns));
+
+ e_cache_sqlite_stmt_append_printf (string, "CREATE TABLE IF NOT EXISTS %Q (uid TEXT NOT NULL
REFERENCES " E_CACHE_TABLE_OBJECTS
+ " (" E_CACHE_COLUMN_UID ")",
+ field->aux_table);
+ for (link = aux_columns; link; link = g_slist_next (link)) {
+ ECacheColumnInfo *info = link->data;
+
+ g_string_append (string, ", ");
+ format_column_declaration (string, info);
+ }
+ g_string_append_c (string, ')');
+
+ success = e_cache_sqlite_exec (E_CACHE (book_cache), string->str, cancellable, error);
+ g_string_free (string, TRUE);
+
+ if (success) {
+ /* Create an index on the implied 'uid' column, this is important
+ * when replacing (modifying) contacts, since we need to remove
+ * all rows in an auxiliary table which matches a given UID.
+ *
+ * This index speeds up the constraint in a statement such as:
+ *
+ * DELETE from email_list WHERE email_list.uid = 'contact uid'
+ */
+ tmp = e_cache_sqlite_stmt_printf ("CREATE INDEX IF NOT EXISTS UID_INDEX_%s_%s ON %Q
(uid)",
+ field->dbname, field->aux_table, field->aux_table);
+ success = e_cache_sqlite_exec (E_CACHE (book_cache), tmp, cancellable, error);
+ e_cache_sqlite_stmt_free (tmp);
+ }
+
+ /* Add indexes to columns in this auxiliary table
+ */
+ for (link = aux_columns; success && link; link = g_slist_next (link)) {
+ ECacheColumnInfo *info = link->data;
+
+ if (info->index_name) {
+ tmp = e_cache_sqlite_stmt_printf ("CREATE INDEX IF NOT EXISTS %Q ON %Q (%s)",
+ info->index_name, field->aux_table, info->name);
+ success = e_cache_sqlite_exec (E_CACHE (book_cache), tmp, cancellable, error);
+ e_cache_sqlite_stmt_free (tmp);
+ }
+ }
+
+ g_slist_free_full (aux_columns, e_cache_column_info_free);
+ }
+
+ return success;
+}
+
+static gboolean
+ebc_run_multi_insert_one (ECache *cache,
+ SummaryField *field,
+ const gchar *uid,
+ const gchar *value,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GString *stmt, *values;
+ gchar *normal;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (field != NULL, FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ stmt = g_string_sized_new (INSERT_MULTI_STMT_BYTES);
+ values = g_string_sized_new (INSERT_MULTI_STMT_BYTES);
+
+ normal = e_util_utf8_normalize (value);
+
+ e_cache_sqlite_stmt_append_printf (stmt, "INSERT INTO %Q (uid, %s", field->aux_table, field->dbname);
+
+ if ((field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ g_string_append (stmt, ", value_" EBC_SUFFIX_REVERSE);
+
+ if (normal) {
+ gchar *str;
+
+ str = g_utf8_strreverse (normal, -1);
+
+ e_cache_sqlite_stmt_append_printf (values, ", %Q", str);
+
+ g_free (str);
+ } else {
+ g_string_append (values, ", NULL");
+ }
+ }
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+ EBookCache *book_cache;
+ gint country_code = 0;
+ gchar *str;
+
+ g_string_append (stmt, ", value_" EBC_SUFFIX_PHONE);
+ g_string_append (stmt, ", value_" EBC_SUFFIX_COUNTRY);
+
+ book_cache = E_BOOK_CACHE (cache);
+ str = convert_phone (normal, book_cache->priv->region_code, &country_code);
+ str = remove_leading_zeros (str);
+
+ if (str) {
+ e_cache_sqlite_stmt_append_printf (values, ", %Q", str);
+ } else {
+ g_string_append (values, ",NULL");
+ }
+
+ g_string_append_printf (values, ",%d", country_code);
+ }
+
+ e_cache_sqlite_stmt_append_printf (stmt, ") VALUES (%Q, %Q", uid, normal);
+ g_free (normal);
+
+ g_string_append (stmt, values->str);
+ g_string_append_c (stmt, ')');
+
+ success = e_cache_sqlite_exec (cache, stmt->str, cancellable, error);
+
+ g_string_free (stmt, TRUE);
+ g_string_free (values, TRUE);
+
+ return success;
+}
+
+static gboolean
+ebc_run_multi_insert (ECache *cache,
+ SummaryField *field,
+ const gchar *uid,
+ EContact *contact,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GList *values, *link;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (field != NULL, FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ values = e_contact_get (contact, field->field_id);
+
+ for (link = values; success && link; link = g_list_next (link)) {
+ const gchar *value = link->data;
+
+ success = ebc_run_multi_insert_one (cache, field, uid, value, cancellable, error);
+ }
+
+ /* Free the list of allocated strings */
+ e_contact_attr_list_free (values);
+
+ return success;
+}
+
+static gboolean
+ebc_run_multi_delete (ECache *cache,
+ SummaryField *field,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (field != NULL, FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ stmt = e_cache_sqlite_stmt_printf ("DELETE FROM %Q WHERE uid=%Q", field->aux_table, uid);
+ success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+ e_cache_sqlite_stmt_free (stmt);
+
+ return success;
+}
+
+static gboolean
+ebc_update_aux_tables (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+ EContact *contact = NULL;
+ gint ii;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+
+ book_cache = E_BOOK_CACHE (cache);
+
+ for (ii = 0; ii < book_cache->priv->n_summary_fields && success; ii++) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ if (!contact) {
+ contact = e_contact_new_from_vcard_with_uid (object, uid);
+ success = contact != NULL;
+ }
+
+ success = success && ebc_run_multi_delete (cache, field, uid, cancellable, error);
+ success = success && ebc_run_multi_insert (cache, field, uid, contact, cancellable, error);
+ }
+
+ g_clear_object (&contact);
+
+ return success;
+}
+
+static gboolean
+ebc_delete_from_aux_tables (ECache *cache,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+ gint ii;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ book_cache = E_BOOK_CACHE (cache);
+
+ for (ii = 0; ii < book_cache->priv->n_summary_fields && success; ii++) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ success = success && ebc_run_multi_delete (cache, field, uid, cancellable, error);
+ }
+
+ return success;
+}
+
+static gboolean
+ebc_empty_aux_tables (ECache *cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+ gint ii;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+
+ book_cache = E_BOOK_CACHE (cache);
+
+ for (ii = 0; ii < book_cache->priv->n_summary_fields && success; ii++) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+ gchar *stmt;
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ stmt = e_cache_sqlite_stmt_printf ("DELETE FROM %Q", field->aux_table);
+ success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+ e_cache_sqlite_stmt_free (stmt);
+ }
+
+ return success;
+}
+
+static gboolean
+ebc_upgrade_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[],
+ gchar **out_revision,
+ gchar **out_object,
+ EOfflineState *out_offline_state,
+ GHashTable **out_other_columns,
+ gpointer user_data)
+{
+ EContact *contact;
+ GHashTable *other_columns;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+
+ contact = e_contact_new_from_vcard_with_uid (object, uid);
+
+ /* Ignore broken rows? */
+ if (!contact)
+ return TRUE;
+
+ other_columns = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, NULL, g_free);
+
+ ebc_fill_other_columns (E_BOOK_CACHE (cache), contact, other_columns);
+
+ g_clear_object (&contact);
+
+ if (g_hash_table_size (other_columns) > 0) {
+ gint ii;
+
+ for (ii = 0; ii < ncols; ii++) {
+ if (!column_names[ii] ||
+ !g_hash_table_contains (other_columns, column_names[ii]))
+ continue;
+
+ if (g_strcmp0 (g_hash_table_lookup (other_columns, column_names[ii]),
column_values[ii]) == 0) {
+ /* Do not try to store values which did not change */
+ g_hash_table_remove (other_columns, column_names[ii]);
+ }
+ }
+ }
+
+ if (g_hash_table_size (other_columns) > 0) {
+ *out_other_columns = other_columns;
+ } else {
+ g_hash_table_destroy (other_columns);
+ }
+
+ return TRUE;
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebc_upgrade (EBookCache *book_cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ success = e_cache_foreach_update (E_CACHE (book_cache), FALSE, NULL,
+ ebc_upgrade_cb, NULL, cancellable, error);
+
+ /* Store the new locale & country code */
+ success = success && e_cache_set_key (E_CACHE (book_cache), EBC_KEY_LC_COLLATE,
book_cache->priv->locale, error);
+ success = success && e_cache_set_key (E_CACHE (book_cache), EBC_KEY_COUNTRYCODE,
book_cache->priv->region_code, error);
+
+ return success;
+}
+
+static gboolean
+ebc_set_locale_internal (EBookCache *book_cache,
+ const gchar *locale,
+ GError **error)
+{
+ ECollator *collator;
+
+ g_return_val_if_fail (locale && locale[0], FALSE);
+
+ if (g_strcmp0 (book_cache->priv->locale, locale) != 0) {
+ gchar *country_code = NULL;
+
+ collator = e_collator_new_interpret_country (locale, &country_code, error);
+ if (collator == NULL)
+ return FALSE;
+
+ /* Assign region code parsed from the locale by ICU */
+ g_free (book_cache->priv->region_code);
+ book_cache->priv->region_code = country_code;
+
+ /* Assign locale */
+ g_free (book_cache->priv->locale);
+ book_cache->priv->locale = g_strdup (locale);
+
+ /* Assign collator */
+ if (book_cache->priv->collator)
+ e_collator_unref (book_cache->priv->collator);
+ book_cache->priv->collator = collator;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+ebc_init_locale (EBookCache *book_cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stored_lc_collate;
+ gchar *stored_region_code;
+ const gchar *lc_collate;
+ gboolean success = TRUE;
+ gboolean relocalize_needed = FALSE;
+
+ /* Get the locale setting for this addressbook */
+ stored_lc_collate = e_cache_dup_key (E_CACHE (book_cache), EBC_KEY_LC_COLLATE, NULL);
+ stored_region_code = e_cache_dup_key (E_CACHE (book_cache), EBC_KEY_COUNTRYCODE, NULL);
+
+ lc_collate = stored_lc_collate;
+
+ /* When creating a new addressbook, or upgrading from a version
+ * where we did not have any locale setting; default to system locale,
+ * we must absolutely always have a locale set.
+ */
+ if (!lc_collate || !lc_collate[0])
+ lc_collate = setlocale (LC_COLLATE, NULL);
+ if (!lc_collate || !lc_collate[0])
+ lc_collate = setlocale (LC_ALL, NULL);
+ if (!lc_collate || !lc_collate[0])
+ lc_collate = "en_US.utf8";
+
+ /* Before touching any data, make sure we have a valid ECollator,
+ * this will also resolve our region code
+ */
+ if (success)
+ success = ebc_set_locale_internal (book_cache, lc_collate, error);
+
+ /* Check if we need to relocalize */
+ if (success) {
+ /* We may need to relocalize for a country code change */
+ if (g_strcmp0 (book_cache->priv->region_code, stored_region_code) != 0)
+ relocalize_needed = TRUE;
+ }
+
+ /* Reinsert all contacts with new locale & country code */
+ if (success && relocalize_needed)
+ success = ebc_upgrade (book_cache, cancellable, error);
+
+ g_free (stored_region_code);
+ g_free (stored_lc_collate);
+
+ return success;
+}
+
+typedef struct {
+ EBookCache *book_cache;
+ EContactField field;
+} EBCCollData;
+
+static gint
+ebc_fallback_collator (gpointer ref,
+ gint len1,
+ gconstpointer data1,
+ gint len2,
+ gconstpointer data2)
+{
+ EBCCollData *data = ref;
+ EBookCache *book_cache;
+ EContact *contact1, *contact2;
+ const gchar *str1, *str2;
+ gchar *key1, *key2;
+ gchar *tmp;
+ gint result = 0;
+
+ book_cache = data->book_cache;
+
+ str1 = (const gchar *) data1;
+ str2 = (const gchar *) data2;
+
+ /* Construct 2 contacts (we're comparing vcards) */
+ contact1 = e_contact_new ();
+ contact2 = e_contact_new ();
+ e_vcard_construct_full (E_VCARD (contact1), str1, len1, NULL);
+ e_vcard_construct_full (E_VCARD (contact2), str2, len2, NULL);
+
+ /* Extract first key */
+ key1 = ebc_decode_vcard_sort_key_from_vcard (E_VCARD (contact1));
+ if (!key1) {
+ tmp = e_contact_get (contact1, data->field);
+ if (tmp)
+ key1 = e_collator_generate_key (book_cache->priv->collator, tmp, NULL);
+ g_free (tmp);
+ }
+ if (!key1)
+ key1 = g_strdup ("");
+
+ /* Extract second key */
+ key2 = ebc_decode_vcard_sort_key_from_vcard (E_VCARD (contact2));
+ if (!key2) {
+ tmp = e_contact_get (contact2, data->field);
+ if (tmp)
+ key2 = e_collator_generate_key (book_cache->priv->collator, tmp, NULL);
+ g_free (tmp);
+ }
+ if (!key2)
+ key2 = g_strdup ("");
+
+ result = strcmp (key1, key2);
+
+ g_free (key1);
+ g_free (key2);
+ g_object_unref (contact1);
+ g_object_unref (contact2);
+
+ return result;
+}
+
+static EBCCollData *
+ebc_coll_data_new (EBookCache *book_cache,
+ EContactField field)
+{
+ EBCCollData *data = g_slice_new (EBCCollData);
+
+ data->book_cache = book_cache;
+ data->field = field;
+
+ return data;
+}
+
+static void
+ebc_coll_data_free (EBCCollData *data)
+{
+ if (data)
+ g_slice_free (EBCCollData, data);
+}
+
+/* COLLATE functions are generated on demand only */
+static void
+ebc_generate_collator (gpointer ref,
+ sqlite3 *db,
+ gint eTextRep,
+ const gchar *coll_name)
+{
+ EBookCache *book_cache = ref;
+ EBCCollData *data;
+ EContactField field;
+ const gchar *field_name;
+
+ field_name = coll_name + strlen (EBC_COLLATE_PREFIX);
+ field = e_contact_field_id (field_name);
+
+ /* This should be caught before reaching here, just an extra check */
+ if (field == 0 || field >= E_CONTACT_FIELD_LAST ||
+ e_contact_field_type (field) != G_TYPE_STRING) {
+ g_warning ("Specified collation on invalid contact field");
+ return;
+ }
+
+ data = ebc_coll_data_new (book_cache, field);
+ sqlite3_create_collation_v2 (
+ db, coll_name, SQLITE_UTF8,
+ data, ebc_fallback_collator,
+ (GDestroyNotify) ebc_coll_data_free);
+}
+
+/***************************************************************
+ * Structures and utilities for preflight and query generation *
+ ***************************************************************/
+
+/* This enumeration is ordered by severity, higher values
+ * of PreflightStatus take precedence in error reporting.
+ */
+typedef enum {
+ PREFLIGHT_OK = 0,
+ PREFLIGHT_LIST_ALL,
+ PREFLIGHT_NOT_SUMMARIZED,
+ PREFLIGHT_INVALID,
+ PREFLIGHT_UNSUPPORTED,
+} PreflightStatus;
+
+/* Whether we can satisfy the constraints or whether we
+ * need to do a fallback, we still need to call
+ * ebc_generate_constraints()
+ */
+#define EBC_STATUS_GEN_CONSTRAINTS(status) \
+ ((status) == PREFLIGHT_OK || \
+ (status) == PREFLIGHT_NOT_SUMMARIZED)
+
+/* Internal extension of the EBookQueryTest enumeration */
+enum {
+ /* 'exists' is a supported query on a field, but not part of EBookQueryTest */
+ BOOK_QUERY_EXISTS = E_BOOK_QUERY_LAST,
+ BOOK_QUERY_EXISTS_VCARD,
+
+ /* From here the compound types start */
+ BOOK_QUERY_SUB_AND,
+ BOOK_QUERY_SUB_OR,
+ BOOK_QUERY_SUB_NOT,
+ BOOK_QUERY_SUB_END,
+
+ BOOK_QUERY_SUB_FIRST = BOOK_QUERY_SUB_AND,
+};
+
+#define EBC_QUERY_TYPE_STR(query) \
+ ((query) == BOOK_QUERY_EXISTS ? "exists" : \
+ (query) == BOOK_QUERY_EXISTS_VCARD ? "exists_vcard" : \
+ (query) == BOOK_QUERY_SUB_AND ? "AND" : \
+ (query) == BOOK_QUERY_SUB_OR ? "OR" : \
+ (query) == BOOK_QUERY_SUB_NOT ? "NOT" : \
+ (query) == BOOK_QUERY_SUB_END ? "END" : \
+ (query) == E_BOOK_QUERY_IS ? "is" : \
+ (query) == E_BOOK_QUERY_CONTAINS ? "contains" : \
+ (query) == E_BOOK_QUERY_BEGINS_WITH ? "begins-with" : \
+ (query) == E_BOOK_QUERY_ENDS_WITH ? "ends-with" : \
+ (query) == E_BOOK_QUERY_EQUALS_PHONE_NUMBER ? "eqphone" : \
+ (query) == E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER ? "eqphone-national" : \
+ (query) == E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER ? "eqphone-short" : \
+ (query) == E_BOOK_QUERY_REGEX_NORMAL ? "regex-normal" : \
+ (query) == E_BOOK_QUERY_REGEX_NORMAL ? "regex-raw" : "(unknown)")
+
+#define EBC_FIELD_ID_STR(field_id) \
+ ((field_id) == E_CONTACT_FIELD_LAST ? "x-evolution-any-field" : \
+ (field_id) == 0 ? "(not an EContactField)" : \
+ e_contact_field_name (field_id))
+
+#define IS_QUERY_PHONE(query) \
+ ((query) == E_BOOK_QUERY_EQUALS_PHONE_NUMBER || \
+ (query) == E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER || \
+ (query) == E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER)
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+} QueryElement;
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+} QueryDelimiter;
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+
+ EContactField field_id; /* The EContactField to compare */
+ SummaryField *field; /* The summary field for 'field' */
+ gchar *value; /* The value to compare with */
+
+} QueryFieldTest;
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+
+ /* Common fields from QueryFieldTest */
+ EContactField field_id; /* The EContactField to compare */
+ SummaryField *field; /* The summary field for 'field' */
+ gchar *value; /* The value to compare with */
+
+ /* Extension */
+ gchar *region; /* Region code from the query input */
+ gchar *national; /* Parsed national number */
+ gint country; /* Parsed country code */
+} QueryPhoneTest;
+
+/* Stack initializer for the PreflightContext struct below */
+#define PREFLIGHT_CONTEXT_INIT { PREFLIGHT_OK, NULL, 0, FALSE }
+
+typedef struct {
+ PreflightStatus status; /* result status */
+ GPtrArray *constraints; /* main query; may be NULL */
+ guint64 aux_mask; /* Bitmask of which auxiliary tables are needed in the query */
+ guint64 left_join_mask; /* Do we need to use a LEFT JOIN */
+} PreflightContext;
+
+static QueryElement *
+query_delimiter_new (guint query)
+{
+ QueryDelimiter *delim;
+
+ g_return_val_if_fail (query >= BOOK_QUERY_SUB_FIRST, NULL);
+
+ delim = g_slice_new (QueryDelimiter);
+ delim->query = query;
+
+ return (QueryElement *) delim;
+}
+
+static QueryFieldTest *
+query_field_test_new (guint query,
+ EContactField field)
+{
+ QueryFieldTest *test;
+
+ g_return_val_if_fail (query < BOOK_QUERY_SUB_FIRST, NULL);
+ g_return_val_if_fail (IS_QUERY_PHONE (query) == FALSE, NULL);
+
+ test = g_slice_new (QueryFieldTest);
+ test->query = query;
+ test->field_id = field;
+
+ /* Instead of g_slice_new0, NULL them out manually */
+ test->field = NULL;
+ test->value = NULL;
+
+ return test;
+}
+
+static QueryPhoneTest *
+query_phone_test_new (guint query,
+ EContactField field)
+{
+ QueryPhoneTest *test;
+
+ g_return_val_if_fail (IS_QUERY_PHONE (query), NULL);
+
+ test = g_slice_new (QueryPhoneTest);
+ test->query = query;
+ test->field_id = field;
+
+ /* Instead of g_slice_new0, NULL them out manually */
+ test->field = NULL;
+ test->value = NULL;
+
+ /* Extra QueryPhoneTest fields */
+ test->region = NULL;
+ test->national = NULL;
+ test->country = 0;
+
+ return test;
+}
+
+static void
+query_element_free (QueryElement *element)
+{
+ if (element) {
+
+ if (element->query >= BOOK_QUERY_SUB_FIRST) {
+ QueryDelimiter *delim = (QueryDelimiter *) element;
+
+ g_slice_free (QueryDelimiter, delim);
+ } else if (IS_QUERY_PHONE (element->query)) {
+ QueryPhoneTest *test = (QueryPhoneTest *) element;
+
+ g_free (test->value);
+ g_free (test->region);
+ g_free (test->national);
+ g_slice_free (QueryPhoneTest, test);
+ } else {
+ QueryFieldTest *test = (QueryFieldTest *) element;
+
+ g_free (test->value);
+ g_slice_free (QueryFieldTest, test);
+ }
+ }
+}
+
+/* We use ptr arrays for the QueryElement vectors */
+static inline void
+constraints_insert (GPtrArray *array,
+ gint idx,
+ gpointer data)
+{
+ g_return_if_fail ((idx >= -1) && (idx < (gint) array->len + 1));
+
+ if (idx < 0)
+ idx = array->len;
+
+ g_ptr_array_add (array, NULL);
+
+ if (idx != (array->len - 1))
+ memmove (
+ &(array->pdata[idx + 1]),
+ &(array->pdata[idx]),
+ ((array->len - 1) - idx) * sizeof (gpointer));
+
+ array->pdata[idx] = data;
+}
+
+static inline void
+constraints_insert_delimiter (GPtrArray *array,
+ gint idx,
+ guint query)
+{
+ QueryElement *delim;
+
+ delim = query_delimiter_new (query);
+ constraints_insert (array, idx, delim);
+}
+
+static inline void
+constraints_insert_field_test (GPtrArray *array,
+ gint idx,
+ SummaryField *field,
+ guint query,
+ const gchar *value)
+{
+ QueryFieldTest *test;
+
+ test = query_field_test_new (query, field->field_id);
+ test->field = field;
+ test->value = g_strdup (value);
+
+ constraints_insert (array, idx, test);
+}
+
+static void
+preflight_context_clear (PreflightContext *context)
+{
+ if (context) {
+ /* Free any allocated data, but leave the context values in place */
+ if (context->constraints)
+ g_ptr_array_free (context->constraints, TRUE);
+ context->constraints = NULL;
+ }
+}
+
+/* A small API to track the current sub-query context.
+ *
+ * I.e. sub contexts can be OR, AND, or NOT, in which
+ * field tests or other sub contexts are nested.
+ *
+ * The 'count' field is a simple counter of how deep the contexts are nested.
+ *
+ * The 'cond_count' field is to be used by the caller for its own purposes;
+ * it is incremented in sub_query_context_push() only if the inc_cond_count
+ * parameter is TRUE. This is used by query_preflight_check() in a complex
+ * fashion which is described there.
+ */
+typedef GQueue SubQueryContext;
+
+typedef struct {
+ guint sub_type; /* The type of this sub context */
+ guint count; /* The number of field tests so far in this context */
+ guint cond_count; /* User-specific conditional counter */
+} SubQueryData;
+
+#define sub_query_context_new g_queue_new
+#define sub_query_context_free(ctx) g_queue_free (ctx)
+
+static inline void
+sub_query_context_push (SubQueryContext *ctx,
+ guint sub_type,
+ gboolean inc_cond_count)
+{
+ SubQueryData *data, *prev;
+
+ prev = g_queue_peek_tail (ctx);
+
+ data = g_slice_new (SubQueryData);
+ data->sub_type = sub_type;
+ data->count = 0;
+ data->cond_count = prev ? prev->cond_count : 0;
+ if (inc_cond_count)
+ data->cond_count++;
+
+ g_queue_push_tail (ctx, data);
+}
+
+static inline void
+sub_query_context_pop (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_pop_tail (ctx);
+ g_slice_free (SubQueryData, data);
+}
+
+static inline guint
+sub_query_context_peek_type (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_peek_tail (ctx);
+
+ return data->sub_type;
+}
+
+static inline guint
+sub_query_context_peek_cond_counter (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_peek_tail (ctx);
+
+ if (data)
+ return data->cond_count;
+ else
+ return 0;
+}
+
+/* Returns the context field test count before incrementing */
+static inline guint
+sub_query_context_increment (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_peek_tail (ctx);
+
+ if (data) {
+ data->count++;
+
+ return (data->count - 1);
+ }
+
+ /* If we're not in a sub context, just return 0 */
+ return 0;
+}
+
+/**********************************************************
+ * Querying preflighting *
+ **********************************************************
+ *
+ * The preflight checks are performed before a query might
+ * take place in order to evaluate whether the given query
+ * can be performed with the current summary configuration.
+ *
+ * After preflighting, all relevant data has been extracted
+ * from the search expression and the search expression need
+ * not be parsed again.
+ */
+
+/* The PreflightSubCallback is expected to return TRUE
+ * to keep iterating and FALSE to abort iteration.
+ *
+ * The sub_level is the counter of how deep the 'element'
+ * is nested in sub elements, the offset is the real offset
+ * of 'element' in the array passed to query_preflight_foreach_sub().
+ */
+typedef gboolean (* PreflightSubCallback) (QueryElement *element,
+ gint sub_level,
+ gint offset,
+ gpointer user_data);
+
+static void
+query_preflight_foreach_sub (QueryElement **elements,
+ gint n_elements,
+ gint offset,
+ gboolean include_delim,
+ PreflightSubCallback callback,
+ gpointer user_data)
+{
+ gint sub_counter = 1, ii;
+
+ g_return_if_fail (offset >= 0 && offset < n_elements);
+ g_return_if_fail (elements[offset]->query >= BOOK_QUERY_SUB_FIRST);
+ g_return_if_fail (callback != NULL);
+
+ if (include_delim && !callback (elements[offset], 0, offset, user_data))
+ return;
+
+ for (ii = (offset + 1); sub_counter > 0 && ii < n_elements; ii++) {
+
+ if (elements[ii]->query >= BOOK_QUERY_SUB_FIRST) {
+
+ if (elements[ii]->query == BOOK_QUERY_SUB_END)
+ sub_counter--;
+ else
+ sub_counter++;
+
+ if (include_delim &&
+ !callback (elements[ii], sub_counter, ii, user_data))
+ break;
+ } else {
+
+ if (!callback (elements[ii], sub_counter, ii, user_data))
+ break;
+ }
+ }
+}
+
+/* Table used in ESExp parsing below */
+static const struct {
+ const gchar *name; /* Name of the symbol to match for this parse phase */
+ gboolean subset; /* TRUE for the subset ESExpIFunc, otherwise the field check ESExpFunc */
+ guint test; /* Extended EBookQueryTest value */
+} check_symbols[] = {
+ { "and", TRUE, BOOK_QUERY_SUB_AND },
+ { "or", TRUE, BOOK_QUERY_SUB_OR },
+ { "not", TRUE, BOOK_QUERY_SUB_NOT },
+
+ { "contains", FALSE, E_BOOK_QUERY_CONTAINS },
+ { "is", FALSE, E_BOOK_QUERY_IS },
+ { "beginswith", FALSE, E_BOOK_QUERY_BEGINS_WITH },
+ { "endswith", FALSE, E_BOOK_QUERY_ENDS_WITH },
+ { "eqphone", FALSE, E_BOOK_QUERY_EQUALS_PHONE_NUMBER },
+ { "eqphone_national", FALSE, E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER },
+ { "eqphone_short", FALSE, E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER },
+ { "regex_normal", FALSE, E_BOOK_QUERY_REGEX_NORMAL },
+ { "regex_raw", FALSE, E_BOOK_QUERY_REGEX_RAW },
+ { "exists", FALSE, BOOK_QUERY_EXISTS },
+ { "exists_vcard", FALSE, BOOK_QUERY_EXISTS_VCARD }
+};
+
+/* Cheat our way into passing mode data to these funcs */
+static ESExpResult *
+func_check_subset (ESExp *f,
+ gint argc,
+ struct _ESExpTerm **argv,
+ gpointer data)
+{
+ ESExpResult *result, *sub_result;
+ GPtrArray *result_array;
+ QueryElement *element, **sub_elements;
+ gint ii, jj, len;
+ guint query_type;
+
+ query_type = GPOINTER_TO_UINT (data);
+
+ /* The compound query delimiter is the first element in this return array */
+ result_array = g_ptr_array_new_with_free_func ((GDestroyNotify) query_element_free);
+ element = query_delimiter_new (query_type);
+ g_ptr_array_add (result_array, element);
+
+ for (ii = 0; ii < argc; ii++) {
+ sub_result = e_sexp_term_eval (f, argv[ii]);
+
+ if (sub_result->type == ESEXP_RES_ARRAY_PTR) {
+ /* Steal the elements directly from the sub result */
+ sub_elements = (QueryElement **) sub_result->value.ptrarray->pdata;
+ len = sub_result->value.ptrarray->len;
+
+ for (jj = 0; jj < len; jj++) {
+ element = sub_elements[jj];
+ sub_elements[jj] = NULL;
+
+ g_ptr_array_add (result_array, element);
+ }
+ }
+ e_sexp_result_free (f, sub_result);
+ }
+
+ /* The last element in this return array is the sub end delimiter */
+ element = query_delimiter_new (BOOK_QUERY_SUB_END);
+ g_ptr_array_add (result_array, element);
+
+ result = e_sexp_result_new (f, ESEXP_RES_ARRAY_PTR);
+ result->value.ptrarray = result_array;
+
+ return result;
+}
+
+static ESExpResult *
+func_check (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ ESExpResult *result;
+ GPtrArray *result_array;
+ QueryElement *element = NULL;
+ EContactField field_id = 0;
+ const gchar *query_name = NULL;
+ const gchar *query_value = NULL;
+ const gchar *query_extra = NULL;
+ guint query_type;
+
+ query_type = GPOINTER_TO_UINT (data);
+
+ if (argc == 1 && query_type == BOOK_QUERY_EXISTS &&
+ argv[0]->type == ESEXP_RES_STRING) {
+ query_name = argv[0]->value.string;
+
+ field_id = e_contact_field_id (query_name);
+ } else if (argc == 2 &&
+ argv[0]->type == ESEXP_RES_STRING &&
+ argv[1]->type == ESEXP_RES_STRING) {
+ query_name = argv[0]->value.string;
+ query_value = argv[1]->value.string;
+
+ /* We use E_CONTACT_FIELD_LAST to hold the special case of "x-evolution-any-field" */
+ if (g_strcmp0 (query_name, "x-evolution-any-field") == 0)
+ field_id = E_CONTACT_FIELD_LAST;
+ else
+ field_id = e_contact_field_id (query_name);
+
+ } else if (argc == 3 &&
+ argv[0]->type == ESEXP_RES_STRING &&
+ argv[1]->type == ESEXP_RES_STRING &&
+ argv[2]->type == ESEXP_RES_STRING) {
+ query_name = argv[0]->value.string;
+ query_value = argv[1]->value.string;
+ query_extra = argv[2]->value.string;
+
+ field_id = e_contact_field_id (query_name);
+ }
+
+ if (IS_QUERY_PHONE (query_type)) {
+ QueryPhoneTest *test;
+
+ /* Collect data from this field test */
+ test = query_phone_test_new (query_type, field_id);
+ test->value = g_strdup (query_value);
+ test->region = g_strdup (query_extra);
+
+ element = (QueryElement *) test;
+ } else {
+ QueryFieldTest *test;
+
+ /* Collect data from this field test */
+ test = query_field_test_new (query_type, field_id);
+ test->value = g_strdup (query_value);
+
+ element = (QueryElement *) test;
+ }
+
+ /* Return an array with only one element, for lack of a pointer type ESExpResult */
+ result_array = g_ptr_array_new_with_free_func ((GDestroyNotify) query_element_free);
+ g_ptr_array_add (result_array, element);
+
+ result = e_sexp_result_new (f, ESEXP_RES_ARRAY_PTR);
+ result->value.ptrarray = result_array;
+
+ return result;
+}
+
+/* Initial stage of preflighting:
+ *
+ * o Parse the search expression and generate our array of QueryElements
+ * o Collect lengths of query terms
+ */
+static void
+query_preflight_initialize (PreflightContext *context,
+ const gchar *sexp)
+{
+ ESExp *sexp_parser;
+ ESExpResult *result;
+ gint esexp_error, ii;
+
+ if (!sexp || !*sexp) {
+ context->status = PREFLIGHT_LIST_ALL;
+ return;
+ }
+
+ sexp_parser = e_sexp_new ();
+
+ for (ii = 0; ii < G_N_ELEMENTS (check_symbols); ii++) {
+ if (check_symbols[ii].subset) {
+ e_sexp_add_ifunction (
+ sexp_parser, 0, check_symbols[ii].name,
+ func_check_subset,
+ GUINT_TO_POINTER (check_symbols[ii].test));
+ } else {
+ e_sexp_add_function (
+ sexp_parser, 0, check_symbols[ii].name,
+ func_check,
+ GUINT_TO_POINTER (check_symbols[ii].test));
+ }
+ }
+
+ e_sexp_input_text (sexp_parser, sexp, strlen (sexp));
+ esexp_error = e_sexp_parse (sexp_parser);
+
+ if (esexp_error == -1) {
+ context->status = PREFLIGHT_INVALID;
+ } else {
+ result = e_sexp_eval (sexp_parser);
+ if (result) {
+ if (result->type == ESEXP_RES_ARRAY_PTR) {
+ /* Just steal the array away from the ESexpResult */
+ context->constraints = result->value.ptrarray;
+ result->value.ptrarray = NULL;
+ } else {
+ context->status = PREFLIGHT_INVALID;
+ }
+ }
+
+ e_sexp_result_free (sexp_parser, result);
+ }
+
+ g_object_unref (sexp_parser);
+}
+
+typedef struct {
+ EBookCache *book_cache;
+ SummaryField *field;
+ gboolean condition;
+} AttrListCheckData;
+
+static gboolean
+check_has_attr_list_cb (QueryElement *element,
+ gint sub_level,
+ gint offset,
+ gpointer user_data)
+{
+ QueryFieldTest *test = (QueryFieldTest *) element;
+ AttrListCheckData *data = (AttrListCheckData *) user_data;
+
+ /* We havent resolved all the fields at this stage yet */
+ if (!test->field)
+ test->field = summary_field_get (data->book_cache, test->field_id);
+
+ if (test->field && test->field->type == E_TYPE_CONTACT_ATTR_LIST)
+ data->condition = TRUE;
+
+ /* Keep looping until we find one */
+ return !data->condition;
+}
+
+static gboolean
+check_different_fields_cb (QueryElement *element,
+ gint sub_level,
+ gint offset,
+ gpointer user_data)
+{
+ QueryFieldTest *test = (QueryFieldTest *) element;
+ AttrListCheckData *data = (AttrListCheckData *) user_data;
+
+ /* We havent resolved all the fields at this stage yet */
+ if (!test->field)
+ test->field = summary_field_get (data->book_cache, test->field_id);
+
+ if (test->field && data->field && test->field != data->field)
+ data->condition = TRUE;
+ else
+ data->field = test->field;
+
+ /* Keep looping until we find one */
+ return !data->condition;
+}
+
+/* What is done in this pass:
+ * o Viability of the query is analyzed, i.e. can it be done with the summary columns.
+ * o Phone numbers are parsed and loaded onto QueryPhoneTests
+ * o Bitmask of auxiliary tables is collected
+ */
+static void
+query_preflight_check (PreflightContext *context,
+ EBookCache *book_cache)
+{
+ gint ii, n_elements;
+ QueryElement **elements;
+ SubQueryContext *ctx;
+
+ context->status = PREFLIGHT_OK;
+
+ if (context->constraints != NULL) {
+ elements = (QueryElement **) context->constraints->pdata;
+ n_elements = context->constraints->len;
+ } else {
+ elements = NULL;
+ n_elements = 0;
+ }
+
+ ctx = sub_query_context_new ();
+
+ for (ii = 0; ii < n_elements; ii++) {
+ QueryFieldTest *test;
+ guint field_test;
+
+ if (elements[ii]->query >= BOOK_QUERY_SUB_FIRST) {
+ AttrListCheckData data = { book_cache, NULL, FALSE };
+
+ switch (elements[ii]->query) {
+ case BOOK_QUERY_SUB_OR:
+ /* An OR doesn't have to force us to use a LEFT JOIN, as long
+ as all its sub-conditions are on the same field. */
+ query_preflight_foreach_sub (elements,
+ n_elements,
+ ii, FALSE,
+ check_different_fields_cb,
+ &data);
+ case BOOK_QUERY_SUB_AND:
+ sub_query_context_push (ctx, elements[ii]->query, data.condition);
+ break;
+ case BOOK_QUERY_SUB_END:
+ sub_query_context_pop (ctx);
+ break;
+
+ /* It's too complicated to properly perform
+ * the unary NOT operator on a constraint which
+ * accesses attribute lists.
+ *
+ * Hint, if the contact has a "%.com" email address
+ * and a "%.org" email address, what do we return
+ * for (not (endswith "email" ".com") ?
+ *
+ * Currently we rely on DISTINCT to sort out
+ * muliple results from the attribute list tables,
+ * this breaks down with NOT.
+ */
+ case BOOK_QUERY_SUB_NOT:
+ query_preflight_foreach_sub (elements,
+ n_elements,
+ ii, FALSE,
+ check_has_attr_list_cb,
+ &data);
+
+ if (data.condition) {
+ context->status = MAX (
+ context->status,
+ PREFLIGHT_NOT_SUMMARIZED);
+ }
+ break;
+
+ default:
+ g_warn_if_reached ();
+ }
+
+ continue;
+ }
+
+ test = (QueryFieldTest *) elements[ii];
+ field_test = (EBookQueryTest) test->query;
+
+ if (!test->field)
+ test->field = summary_field_get (book_cache, test->field_id);
+
+ /* Even if the field is not in the summary, we need to
+ * retport unsupported errors if phone number queries are
+ * issued while libphonenumber is unavailable
+ */
+ if (!test->field) {
+ /* Special case for e_book_query_any_field_contains().
+ *
+ * We interpret 'x-evolution-any-field' as E_CONTACT_FIELD_LAST
+ */
+ if (test->field_id == E_CONTACT_FIELD_LAST) {
+ /* If we search for a NULL or zero length string, it
+ * means 'get all contacts', that is considered a summary
+ * query but is handled differently (i.e. we just drop the
+ * field tests and run a regular query).
+ *
+ * This is only true if the 'any field contains' query is
+ * the only test in the constraints, however.
+ */
+ if (n_elements == 1 && (!test->value || !test->value[0])) {
+
+ context->status = MAX (context->status, PREFLIGHT_LIST_ALL);
+ } else {
+
+ /* Searching for a value with 'x-evolution-any-field' is
+ * not a summary query.
+ */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ }
+ } else {
+ /* Couldnt resolve the field, it's not a summary query */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ }
+ }
+
+ if (test->field && test->field->type == E_TYPE_CONTACT_CERT) {
+ /* For certificates, and later potentially other fields,
+ * the only information in the summary is the fact that
+ * they exist, or not. So the only check we can do from
+ * the summary is BOOK_QUERY_EXISTS. */
+ if (field_test != BOOK_QUERY_EXISTS) {
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ }
+ /* Bypass the other checks below which are not appropriate. */
+ continue;
+ }
+
+ switch (field_test) {
+ case E_BOOK_QUERY_IS:
+ break;
+
+ case BOOK_QUERY_EXISTS:
+ case E_BOOK_QUERY_CONTAINS:
+ case E_BOOK_QUERY_BEGINS_WITH:
+ case E_BOOK_QUERY_ENDS_WITH:
+ case E_BOOK_QUERY_REGEX_NORMAL:
+ /* All of these queries can only apply to string fields,
+ * or fields which hold multiple strings
+ */
+ if (test->field) {
+ if (test->field->type != G_TYPE_STRING &&
+ test->field->type != E_TYPE_CONTACT_ATTR_LIST) {
+ context->status = MAX (context->status, PREFLIGHT_INVALID);
+ }
+ }
+
+ break;
+
+ case BOOK_QUERY_EXISTS_VCARD:
+ /* Exists vCard queries only supported in the fallback */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ break;
+
+ case E_BOOK_QUERY_REGEX_RAW:
+ /* Raw regex queries only supported in the fallback */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ break;
+
+ case E_BOOK_QUERY_EQUALS_PHONE_NUMBER:
+ case E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER:
+ case E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER:
+ /* Phone number queries are supported so long as they are in the summary,
+ * libphonenumber is available, and the phone number string is a valid one
+ */
+ if (!e_phone_number_is_supported ()) {
+ context->status = MAX (context->status, PREFLIGHT_UNSUPPORTED);
+ } else {
+ QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+ EPhoneNumberCountrySource source;
+ EPhoneNumber *number;
+ const gchar *region_code;
+
+ if (phone_test->region)
+ region_code = phone_test->region;
+ else
+ region_code = book_cache->priv->region_code;
+
+ number = e_phone_number_from_string (
+ phone_test->value,
+ region_code, NULL);
+
+ if (number == NULL) {
+ context->status = MAX (context->status, PREFLIGHT_INVALID);
+ } else {
+ /* Collect values we'll need later while generating field
+ * tests, no need to parse the phone number more than once
+ */
+ phone_test->national = e_phone_number_get_national_number (number);
+ phone_test->country = e_phone_number_get_country_code (number,
&source);
+ phone_test->national = remove_leading_zeros (phone_test->national);
+
+ if (source == E_PHONE_NUMBER_COUNTRY_FROM_DEFAULT)
+ phone_test->country = 0;
+
+ e_phone_number_free (number);
+ }
+ }
+ break;
+ }
+
+ if (test->field &&
+ test->field->type == E_TYPE_CONTACT_ATTR_LIST) {
+ gint aux_index = summary_field_get_index (book_cache, test->field_id);
+
+ /* It's really improbable that we ever get 64 fields in the summary
+ * In any case we warn about this in e_book_sqlite_new_full().
+ */
+ g_warn_if_fail (aux_index >= 0 && aux_index < EBC_MAX_SUMMARY_FIELDS);
+ context->aux_mask |= (1 << aux_index);
+
+ /* If this condition is a *requirement* for the overall query to
+ match a given record (i.e. there's no surrounding 'OR' but
+ only 'AND'), then we can use an inner join for the query and
+ it will be a lot more efficient. If records without this
+ condition can also match the overall condition, then we must
+ use LEFT JOIN. */
+ if (sub_query_context_peek_cond_counter (ctx)) {
+ context->left_join_mask |= (1 << aux_index);
+ }
+ }
+ }
+
+ sub_query_context_free (ctx);
+}
+
+/* Handle special case of E_CONTACT_FULL_NAME
+ *
+ * For any query which accesses the full name field,
+ * we need to also OR it with any of the related name
+ * fields, IF those are found in the summary as well.
+ */
+static void
+query_preflight_substitute_full_name (PreflightContext *context,
+ EBookCache *book_cache)
+{
+ gint ii, jj;
+
+ for (ii = 0; context->constraints != NULL && ii < context->constraints->len; ii++) {
+ SummaryField *family_name, *given_name, *nickname;
+ QueryElement *element;
+ QueryFieldTest *test;
+
+ element = g_ptr_array_index (context->constraints, ii);
+
+ if (element->query >= BOOK_QUERY_SUB_FIRST)
+ continue;
+
+ test = (QueryFieldTest *) element;
+ if (test->field_id != E_CONTACT_FULL_NAME)
+ continue;
+
+ family_name = summary_field_get (book_cache, E_CONTACT_FAMILY_NAME);
+ given_name = summary_field_get (book_cache, E_CONTACT_GIVEN_NAME);
+ nickname = summary_field_get (book_cache, E_CONTACT_NICKNAME);
+
+ /* If any of these are in the summary, then we'll construct
+ * a grouped OR statment for this E_CONTACT_FULL_NAME test */
+ if (family_name || given_name || nickname) {
+ /* Add the OR directly before the E_CONTACT_FULL_NAME test */
+ constraints_insert_delimiter (context->constraints, ii, BOOK_QUERY_SUB_OR);
+
+ jj = ii + 2;
+
+ if (family_name)
+ constraints_insert_field_test (
+ context->constraints, jj++,
+ family_name, test->query,
+ test->value);
+
+ if (given_name)
+ constraints_insert_field_test (
+ context->constraints, jj++,
+ given_name, test->query,
+ test->value);
+
+ if (nickname)
+ constraints_insert_field_test (
+ context->constraints, jj++,
+ nickname, test->query,
+ test->value);
+
+ constraints_insert_delimiter (context->constraints, jj, BOOK_QUERY_SUB_END);
+
+ ii = jj;
+ }
+ }
+}
+
+static void
+query_preflight (PreflightContext *context,
+ EBookCache *book_cache,
+ const gchar *sexp)
+{
+ query_preflight_initialize (context, sexp);
+
+ if (context->status == PREFLIGHT_OK) {
+ query_preflight_check (context, book_cache);
+
+ /* No need to change the constraints if we're not
+ * going to generate statements with it
+ */
+ if (context->status == PREFLIGHT_OK) {
+ /* Handle E_CONTACT_FULL_NAME substitutions */
+ query_preflight_substitute_full_name (context, book_cache);
+ } else {
+ /* We might use this context to perform a fallback query,
+ * so let's clear out all the constraints now
+ */
+ preflight_context_clear (context);
+ }
+ }
+}
+
+/**********************************************************
+ * Field Test Generators *
+ **********************************************************
+ *
+ * This section contains the field test generators for
+ * various EBookQueryTest types. When implementing new
+ * query types, a new GenerateFieldTest needs to be created
+ * and added to the table below.
+ */
+
+typedef void (* GenerateFieldTest) (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test);
+
+/* Appends an identifier suitable to identify the
+ * column to test in the context of a query.
+ *
+ * The suffix is for special indexed columns (such as
+ * reverse values, sort keys, phone numbers, etc).
+ */
+static void
+ebc_string_append_column (GString *string,
+ SummaryField *field,
+ const gchar *suffix)
+{
+ if (field->aux_table) {
+ g_string_append (string, field->aux_table_symbolic);
+ g_string_append (string, ".value");
+ } else {
+ g_string_append (string, "summary.");
+ g_string_append (string, field->dbname);
+ }
+
+ if (suffix) {
+ g_string_append_c (string, '_');
+ g_string_append (string, suffix);
+ }
+}
+
+/* This function escapes characters which need escaping
+ * for LIKE statements as well as the single quotes.
+ *
+ * The return value is not suitable to be formatted
+ * with %Q or %q
+ */
+static gchar *
+ebc_normalize_for_like (QueryFieldTest *test,
+ gboolean reverse_string,
+ gboolean *escape_needed)
+{
+ GString *str;
+ size_t len;
+ gchar cc;
+ gboolean escape_modifier_needed = FALSE;
+ const gchar *normal = NULL;
+ const gchar *ptr;
+ const gchar *str_to_escape;
+ gchar *reverse = NULL;
+ gchar *freeme = NULL;
+
+ if (test->field_id == E_CONTACT_UID ||
+ test->field_id == E_CONTACT_REV) {
+ normal = test->value;
+ } else {
+ freeme = e_util_utf8_normalize (test->value);
+ normal = freeme;
+ }
+
+ if (reverse_string) {
+ reverse = g_utf8_strreverse (normal, -1);
+ str_to_escape = reverse;
+ } else
+ str_to_escape = normal;
+
+ /* Just assume each character must be escaped. The result of this function
+ * is discarded shortly after calling this function. Therefore it's
+ * acceptable to possibly allocate twice the memory needed.
+ */
+ len = strlen (str_to_escape);
+ str = g_string_sized_new (2 * len + 4 + strlen (EBC_ESCAPE_SEQUENCE) - 1);
+
+ ptr = str_to_escape;
+ while ((cc = *ptr++)) {
+ if (cc == '\'') {
+ g_string_append_c (str, '\'');
+ } else if (cc == '%' || cc == '_' || cc == '^') {
+ g_string_append_c (str, '^');
+ escape_modifier_needed = TRUE;
+ }
+
+ g_string_append_c (str, cc);
+ }
+
+ if (escape_needed)
+ *escape_needed = escape_modifier_needed;
+
+ g_free (freeme);
+ g_free (reverse);
+
+ return g_string_free (str, FALSE);
+}
+
+static void
+field_test_query_is (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gchar *normal;
+
+ ebc_string_append_column (string, field, NULL);
+
+ if (test->field_id == E_CONTACT_UID ||
+ test->field_id == E_CONTACT_REV) {
+ /* UID & REV fields are not normalized in the summary */
+ e_cache_sqlite_stmt_append_printf (string, " = %Q", test->value);
+ } else {
+ normal = e_util_utf8_normalize (test->value);
+ e_cache_sqlite_stmt_append_printf (string, " = %Q", normal);
+ g_free (normal);
+ }
+}
+
+static void
+field_test_query_contains (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gboolean need_escape;
+ gchar *escaped;
+
+ escaped = ebc_normalize_for_like (test, FALSE, &need_escape);
+
+ g_string_append_c (string, '(');
+
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " IS NOT NULL AND ");
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " LIKE '%");
+ g_string_append (string, escaped);
+ g_string_append (string, "%'");
+
+ if (need_escape)
+ g_string_append (string, EBC_ESCAPE_SEQUENCE);
+
+ g_string_append_c (string, ')');
+
+ g_free (escaped);
+}
+
+static void
+field_test_query_begins_with (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gboolean need_escape;
+ gchar *escaped;
+
+ escaped = ebc_normalize_for_like (test, FALSE, &need_escape);
+
+ g_string_append_c (string, '(');
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " IS NOT NULL AND ");
+
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " LIKE \'");
+ g_string_append (string, escaped);
+ g_string_append (string, "%\'");
+
+ if (need_escape)
+ g_string_append (string, EBC_ESCAPE_SEQUENCE);
+ g_string_append_c (string, ')');
+
+ g_free (escaped);
+}
+
+static void
+field_test_query_ends_with (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gboolean need_escape;
+ gchar *escaped;
+
+ if ((field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ escaped = ebc_normalize_for_like (test, TRUE, &need_escape);
+
+ g_string_append_c (string, '(');
+ ebc_string_append_column (string, field, EBC_SUFFIX_REVERSE);
+ g_string_append (string, " IS NOT NULL AND ");
+
+ ebc_string_append_column (string, field, EBC_SUFFIX_REVERSE);
+ g_string_append (string, " LIKE \'");
+ g_string_append (string, escaped);
+ g_string_append (string, "%\'");
+ } else {
+ escaped = ebc_normalize_for_like (test, FALSE, &need_escape);
+ g_string_append_c (string, '(');
+
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " IS NOT NULL AND ");
+
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " LIKE \'%");
+ g_string_append (string, escaped);
+ g_string_append (string, "\'");
+ }
+
+ if (need_escape)
+ g_string_append (string, EBC_ESCAPE_SEQUENCE);
+
+ g_string_append_c (string, ')');
+ g_free (escaped);
+}
+
+static void
+field_test_query_eqphone (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+ g_string_append_c (string, '(');
+ ebc_string_append_column (string, field, EBC_SUFFIX_PHONE);
+ e_cache_sqlite_stmt_append_printf (string, " = %Q AND ", phone_test->national);
+
+ /* For exact matches, a country code qualifier is required by both
+ * query input and row input
+ */
+ ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+ g_string_append (string, " != 0 AND ");
+
+ ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+ e_cache_sqlite_stmt_append_printf (string, " = %d", phone_test->country);
+ g_string_append_c (string, ')');
+ } else {
+ /* No indexed columns available, perform the fallback */
+ g_string_append (string, EBC_FUNC_EQPHONE_EXACT " (");
+ ebc_string_append_column (string, field, NULL);
+ e_cache_sqlite_stmt_append_printf (string, ", %Q)", test->value);
+ }
+}
+
+static void
+field_test_query_eqphone_national (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+
+ SummaryField *field = test->field;
+ QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+ /* Only a compound expression if there is a country code */
+ if (phone_test->country)
+ g_string_append_c (string, '(');
+
+ /* Generate: phone = %Q */
+ ebc_string_append_column (string, field, EBC_SUFFIX_PHONE);
+ e_cache_sqlite_stmt_append_printf (string, " = %Q", phone_test->national);
+
+ /* When doing a national search, no need to check country
+ * code unless the query number also has a country code
+ */
+ if (phone_test->country) {
+ /* Generate: (phone = %Q AND (country = 0 OR country = %d)) */
+ g_string_append (string, " AND (");
+ ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+ g_string_append (string, " = 0 OR ");
+ ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+ e_cache_sqlite_stmt_append_printf (string, " = %d))", phone_test->country);
+ }
+ } else {
+ /* No indexed columns available, perform the fallback */
+ g_string_append (string, EBC_FUNC_EQPHONE_NATIONAL " (");
+ ebc_string_append_column (string, field, NULL);
+ e_cache_sqlite_stmt_append_printf (string, ", %Q)", test->value);
+ }
+}
+
+static void
+field_test_query_eqphone_short (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+
+ /* No quick way to do the short match */
+ g_string_append (string, EBC_FUNC_EQPHONE_SHORT " (");
+ ebc_string_append_column (string, field, NULL);
+ e_cache_sqlite_stmt_append_printf (string, ", %Q)", test->value);
+}
+
+static void
+field_test_query_regex_normal (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gchar *normal;
+
+ normal = e_util_utf8_normalize (test->value);
+
+ if (field->aux_table) {
+ e_cache_sqlite_stmt_append_printf (
+ string, "%s.value REGEXP %Q",
+ field->aux_table_symbolic,
+ normal);
+ } else {
+ e_cache_sqlite_stmt_append_printf (
+ string, "summary.%s REGEXP %Q",
+ field->dbname,
+ normal);
+ }
+
+ g_free (normal);
+}
+
+static void
+field_test_query_exists (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+
+ ebc_string_append_column (string, field, NULL);
+
+ if (test->field->type == E_TYPE_CONTACT_CERT)
+ e_cache_sqlite_stmt_append_printf (string, " IS NOT '0'");
+ else
+ e_cache_sqlite_stmt_append_printf (string, " IS NOT NULL");
+}
+
+/* Lookup table for field test generators per EBookQueryTest,
+ *
+ * WARNING: This must stay in line with the EBookQueryTest definition.
+ */
+static const GenerateFieldTest field_test_func_table[] = {
+ field_test_query_is, /* E_BOOK_QUERY_IS */
+ field_test_query_contains, /* E_BOOK_QUERY_CONTAINS */
+ field_test_query_begins_with, /* E_BOOK_QUERY_BEGINS_WITH */
+ field_test_query_ends_with, /* E_BOOK_QUERY_ENDS_WITH */
+ field_test_query_eqphone, /* E_BOOK_QUERY_EQUALS_PHONE_NUMBER */
+ field_test_query_eqphone_national, /* E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER */
+ field_test_query_eqphone_short, /* E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER */
+ field_test_query_regex_normal, /* E_BOOK_QUERY_REGEX_NORMAL */
+ NULL /* Requires fallback */, /* E_BOOK_QUERY_REGEX_RAW */
+ field_test_query_exists, /* BOOK_QUERY_EXISTS */
+ NULL /* Requires fallback */ /* BOOK_QUERY_EXISTS_VCARD */
+};
+
+/**********************************************************
+ * Querying Contacts *
+ **********************************************************/
+
+/* The various search types indicate what should be fetched
+ */
+typedef enum {
+ SEARCH_FULL, /* Get a list of EBookCacheSearchData*/
+ SEARCH_UID_AND_REV, /* Get a list of EBookCacheSearchData, with shallow vcards only containing UID
& REV */
+ SEARCH_UID, /* Get a list of UID strings */
+ SEARCH_COUNT, /* Get the number of matching rows */
+} SearchType;
+
+static void
+ebc_generate_constraints (EBookCache *book_cache,
+ GString *string,
+ GPtrArray *constraints,
+ const gchar *sexp)
+{
+ SubQueryContext *ctx;
+ QueryDelimiter *delim;
+ QueryFieldTest *test;
+ QueryElement **elements;
+ gint n_elements, ii;
+
+ /* If there are no constraints, we generate the fallback constraint for 'sexp' */
+ if (constraints == NULL) {
+ e_cache_sqlite_stmt_append_printf (
+ string,
+ EBC_FUNC_COMPARE_VCARD " (%Q,summary." E_CACHE_COLUMN_OBJECT ")",
+ sexp);
+ return;
+ }
+
+ elements = (QueryElement **) constraints->pdata;
+ n_elements = constraints->len;
+
+ ctx = sub_query_context_new ();
+
+ for (ii = 0; ii < n_elements; ii++) {
+ GenerateFieldTest generate_test_func = NULL;
+
+ /* Seperate field tests with the appropriate grouping */
+ if (elements[ii]->query != BOOK_QUERY_SUB_END &&
+ sub_query_context_increment (ctx) > 0) {
+ guint delim_type = sub_query_context_peek_type (ctx);
+
+ switch (delim_type) {
+ case BOOK_QUERY_SUB_AND:
+ g_string_append (string, " AND ");
+ break;
+
+ case BOOK_QUERY_SUB_OR:
+ g_string_append (string, " OR ");
+ break;
+
+ case BOOK_QUERY_SUB_NOT:
+ /* Nothing to do between children of NOT,
+ * there should only ever be one child of NOT anyway
+ */
+ break;
+
+ case BOOK_QUERY_SUB_END:
+ default:
+ g_warn_if_reached ();
+ }
+ }
+
+ if (elements[ii]->query >= BOOK_QUERY_SUB_FIRST) {
+ delim = (QueryDelimiter *) elements[ii];
+
+ switch (delim->query) {
+ case BOOK_QUERY_SUB_NOT:
+ /* NOT is a unary operator and as such
+ * comes before the opening parenthesis
+ */
+ g_string_append (string, "NOT ");
+
+ /* Fall through */
+
+ case BOOK_QUERY_SUB_AND:
+ case BOOK_QUERY_SUB_OR:
+ /* Open a grouped statement and push the context */
+ sub_query_context_push (ctx, delim->query, FALSE);
+ g_string_append_c (string, '(');
+ break;
+
+ case BOOK_QUERY_SUB_END:
+ /* Close a grouped statement and pop the context */
+ g_string_append_c (string, ')');
+ sub_query_context_pop (ctx);
+ break;
+ default:
+ g_warn_if_reached ();
+ }
+
+ continue;
+ }
+
+ /* Find the appropriate field test generator */
+ test = (QueryFieldTest *) elements[ii];
+ if (test->query < G_N_ELEMENTS (field_test_func_table))
+ generate_test_func = field_test_func_table[test->query];
+
+ /* These should never happen, if it does it should be
+ * fixed in the preflight checks
+ */
+ g_warn_if_fail (generate_test_func != NULL);
+ g_warn_if_fail (test->field != NULL);
+
+ /* Generate the field test */
+ /* coverity[var_deref_op] */
+ generate_test_func (book_cache, string, test);
+ }
+
+ sub_query_context_free (ctx);
+}
+
+static void
+ebc_search_meta_contacts_cb (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ gpointer out_value)
+{
+ GSList **out_list = out_value;
+ EBookCacheSearchData *sd;
+ EContact *contact;
+ gchar *vcard;
+
+ g_return_if_fail (out_list != NULL);
+
+ contact = e_contact_new ();
+
+ e_contact_set (contact, E_CONTACT_UID, uid);
+ if (revision)
+ e_contact_set (contact, E_CONTACT_REV, revision);
+
+ vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+
+ g_object_unref (contact);
+
+ sd = e_book_cache_search_data_new (uid, vcard, extra);
+
+ *out_list = g_slist_prepend (*out_list, sd);
+
+ g_free (vcard);
+}
+
+static void
+ebc_search_full_contacts_cb (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ gpointer out_value)
+{
+ GSList **out_list = out_value;
+ EBookCacheSearchData *sd;
+
+ g_return_if_fail (out_list != NULL);
+
+ sd = e_book_cache_search_data_new (uid, object, extra);
+
+ *out_list = g_slist_prepend (*out_list, sd);
+}
+
+static void
+ebc_search_uids_cb (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ gpointer out_value)
+{
+ GSList **out_list = out_value;
+
+ g_return_if_fail (out_list != NULL);
+
+ *out_list = g_slist_prepend (*out_list, g_strdup (uid));
+}
+
+typedef void (* EBookCacheSearchFunc) (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ gpointer out_value);
+
+/* Generates the SELECT portion of the query, this will take care of
+ * preparing the context of the query, and add the needed JOIN statements
+ * based on which fields are referenced in the query expression.
+ *
+ * This also handles getting the correct callback and asking for the
+ * right data depending on the 'search_type'
+ */
+static EBookCacheSearchFunc
+ebc_generate_select (EBookCache *book_cache,
+ GString *string,
+ SearchType search_type,
+ PreflightContext *context,
+ GError **error)
+{
+ EBookCacheSearchFunc callback = NULL;
+ gboolean add_auxiliary_tables = FALSE;
+ gint ii;
+
+ if (context->status == PREFLIGHT_OK &&
+ context->aux_mask != 0)
+ add_auxiliary_tables = TRUE;
+
+ g_string_append (string, "SELECT ");
+ if (add_auxiliary_tables)
+ g_string_append (string, "DISTINCT ");
+
+ switch (search_type) {
+ case SEARCH_FULL:
+ callback = ebc_search_full_contacts_cb;
+ g_string_append (string, "summary." E_CACHE_COLUMN_UID ",");
+ g_string_append (string, "summary." E_CACHE_COLUMN_OBJECT ",");
+ g_string_append (string, "summary." EBC_COLUMN_EXTRA " ");
+ break;
+ case SEARCH_UID_AND_REV:
+ callback = ebc_search_meta_contacts_cb;
+ g_string_append (string, "summary." E_CACHE_COLUMN_UID ", summary." E_CACHE_COLUMN_REVISION
", summary." EBC_COLUMN_EXTRA " ");
+ break;
+ case SEARCH_UID:
+ callback = ebc_search_uids_cb;
+ g_string_append (string, "summary." E_CACHE_COLUMN_UID " ");
+ break;
+ case SEARCH_COUNT:
+ if (context->aux_mask != 0)
+ g_string_append (string, "count (DISTINCT summary." E_CACHE_COLUMN_UID ") ");
+ else
+ g_string_append (string, "count (*) ");
+ break;
+ }
+
+ e_cache_sqlite_stmt_append_printf (string, "FROM %Q AS summary", E_CACHE_TABLE_OBJECTS);
+
+ /* Add any required auxiliary tables into the query context */
+ if (add_auxiliary_tables) {
+ for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+
+ /* We cap this at EBC_MAX_SUMMARY_FIELDS (64 bits) at creation time */
+ if ((context->aux_mask & (1 << ii)) != 0) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+ gboolean left_join = (context->left_join_mask >> ii) & 1;
+
+ /* Note the '+' in the JOIN statement.
+ *
+ * This plus makes the uid's index ineligable to participate
+ * in any indexing.
+ *
+ * Without this, the indexes which we prefer for prefix or
+ * suffix matching in the auxiliary tables are ignored and
+ * only considered on exact matches.
+ *
+ * This is crucial to ensure that the uid index does not
+ * compete with the value index in constraints such as:
+ *
+ * WHERE email_list.value LIKE "boogieman%"
+ */
+ e_cache_sqlite_stmt_append_printf (
+ string, " %sJOIN %Q AS %s ON %s%s.uid = summary." E_CACHE_COLUMN_UID,
+ left_join ? "LEFT " : "",
+ field->aux_table,
+ field->aux_table_symbolic,
+ left_join ? "" : "+",
+ field->aux_table_symbolic);
+ }
+ }
+ }
+
+ return callback;
+}
+
+static gboolean
+ebc_is_autocomplete_query (PreflightContext *context)
+{
+ QueryFieldTest *test;
+ QueryElement **elements;
+ gint n_elements, ii;
+ int non_aux_fields = 0;
+
+ if (context->status != PREFLIGHT_OK || context->aux_mask == 0)
+ return FALSE;
+
+ elements = (QueryElement **) context->constraints->pdata;
+ n_elements = context->constraints->len;
+
+ for (ii = 0; ii < n_elements; ii++) {
+ test = (QueryFieldTest *) elements[ii];
+
+ /* For these, check if the field being operated on is
+ an auxiliary field or not. */
+ if (elements[ii]->query == E_BOOK_QUERY_BEGINS_WITH ||
+ elements[ii]->query == E_BOOK_QUERY_ENDS_WITH ||
+ elements[ii]->query == E_BOOK_QUERY_IS ||
+ elements[ii]->query == BOOK_QUERY_EXISTS ||
+ elements[ii]->query == E_BOOK_QUERY_CONTAINS) {
+ if (test->field->type != E_TYPE_CONTACT_ATTR_LIST)
+ non_aux_fields++;
+ continue;
+ }
+
+ /* Nothing else is allowed other than "(or" ... ")" */
+ if (elements[ii]->query != BOOK_QUERY_SUB_OR &&
+ elements[ii]->query != BOOK_QUERY_SUB_END)
+ return FALSE;
+ }
+
+ /* If there were no non-aux fields being queried, don't bother */
+ return non_aux_fields != 0;
+}
+
+static EBookCacheSearchFunc
+ebc_generate_autocomplete_query (EBookCache *book_cache,
+ GString *string,
+ SearchType search_type,
+ PreflightContext *context,
+ GError **error)
+{
+ QueryElement **elements;
+ gint n_elements, ii;
+ guint64 aux_mask = context->aux_mask;
+ guint64 left_join_mask = context->left_join_mask;
+ EBookCacheSearchFunc callback;
+ gboolean first = TRUE;
+
+ elements = (QueryElement **) context->constraints->pdata;
+ n_elements = context->constraints->len;
+
+ /* First the queries which use aux tables. */
+ for (ii = 0; ii < n_elements; ii++) {
+ GenerateFieldTest generate_test_func = NULL;
+ QueryFieldTest *test;
+ gint aux_index;
+
+ if (elements[ii]->query == BOOK_QUERY_SUB_OR ||
+ elements[ii]->query == BOOK_QUERY_SUB_END)
+ continue;
+
+ test = (QueryFieldTest *) elements[ii];
+ if (test->field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ aux_index = summary_field_get_index (book_cache, test->field_id);
+ g_warn_if_fail (aux_index >= 0 && aux_index < EBC_MAX_SUMMARY_FIELDS);
+ context->aux_mask = (1 << aux_index);
+ context->left_join_mask = 0;
+
+ callback = ebc_generate_select (book_cache, string, search_type, context, error);
+ g_string_append (string, " WHERE ");
+ context->aux_mask = aux_mask;
+ context->left_join_mask = left_join_mask;
+ if (!callback)
+ return NULL;
+
+ generate_test_func = field_test_func_table[test->query];
+ generate_test_func (book_cache, string, test);
+
+ g_string_append (string, " UNION ");
+ }
+
+ /* Finally, generate the SELECT for the primary fields. */
+ context->aux_mask = 0;
+ callback = ebc_generate_select (book_cache, string, search_type, context, error);
+ context->aux_mask = aux_mask;
+ if (!callback)
+ return NULL;
+
+ g_string_append (string, " WHERE ");
+
+ for (ii = 0; ii < n_elements; ii++) {
+ GenerateFieldTest generate_test_func = NULL;
+ QueryFieldTest *test;
+
+ if (elements[ii]->query == BOOK_QUERY_SUB_OR ||
+ elements[ii]->query == BOOK_QUERY_SUB_END)
+ continue;
+
+ test = (QueryFieldTest *) elements[ii];
+ if (test->field->type == E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ if (!first)
+ g_string_append (string, " OR ");
+ else
+ first = FALSE;
+
+ generate_test_func = field_test_func_table[test->query];
+ generate_test_func (book_cache, string, test);
+ }
+
+ return callback;
+}
+
+struct EBCSearchData {
+ gint uid_index;
+ gint revision_index;
+ gint object_index;
+ gint extra_index;
+
+ EBookCacheSearchFunc func;
+ gpointer out_value;
+};
+
+static gboolean
+ebc_search_select_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ struct EBCSearchData *sd = user_data;
+ const gchar *object = NULL, *extra = NULL;
+
+ g_return_val_if_fail (sd != NULL, FALSE);
+ g_return_val_if_fail (sd->func != NULL, FALSE);
+ g_return_val_if_fail (sd->out_value != NULL, FALSE);
+
+ if (sd->uid_index == -1 ||
+ sd->revision_index == -1 ||
+ sd->object_index == -1 ||
+ sd->extra_index == -1) {
+ gint ii;
+
+ for (ii = 0; ii < ncols && (sd->uid_index == -1 ||
+ sd->revision_index == -1 ||
+ sd->object_index == -1 ||
+ sd->extra_index == -1); ii++) {
+ const gchar *cname = column_names[ii];
+
+ if (!cname)
+ continue;
+
+ if (g_str_has_prefix (cname, "summary."))
+ cname += 8;
+
+ if (sd->uid_index == -1 && g_ascii_strcasecmp (cname, E_CACHE_COLUMN_UID) == 0) {
+ sd->uid_index = ii;
+ } else if (sd->revision_index == -1 && g_ascii_strcasecmp (cname,
E_CACHE_COLUMN_REVISION) == 0) {
+ sd->revision_index = ii;
+ } else if (sd->object_index == -1 && g_ascii_strcasecmp (cname,
E_CACHE_COLUMN_OBJECT) == 0) {
+ sd->object_index = ii;
+ } else if (sd->extra_index == -1 && g_ascii_strcasecmp (cname, EBC_COLUMN_EXTRA) ==
0) {
+ sd->extra_index = ii;
+ }
+ }
+ }
+
+ g_return_val_if_fail (sd->uid_index >= 0 && sd->uid_index < ncols, FALSE);
+ g_return_val_if_fail (sd->revision_index >= 0 && sd->revision_index < ncols, FALSE);
+
+ if (sd->object_index != -2) {
+ g_return_val_if_fail (sd->object_index >= 0 && sd->object_index < ncols, FALSE);
+ object = column_values[sd->object_index];
+ }
+
+ if (sd->extra_index != -2) {
+ g_return_val_if_fail (sd->extra_index >= 0 && sd->extra_index < ncols, FALSE);
+ extra = column_values[sd->extra_index];
+ }
+
+ sd->func (cache, column_values[sd->uid_index], column_values[sd->revision_index], object, extra,
sd->out_value);
+
+ return TRUE;
+}
+
+static gboolean
+ebc_do_search_query (EBookCache *book_cache,
+ PreflightContext *context,
+ const gchar *sexp,
+ SearchType search_type,
+ gpointer out_value,
+ GCancellable *cancellable,
+ GError **error)
+{
+ struct EBCSearchData sd;
+ GString *stmt;
+ gboolean success = FALSE;
+
+ /* We might calculate a reasonable estimation of bytes
+ * during the preflight checks */
+ stmt = g_string_sized_new (GENERATED_QUERY_BYTES);
+
+ /* Extra special case. For the common case of the email composer's
+ addressbook autocompletion, we really want the most optimal query.
+ So check for it and use a basically hand-crafted one. */
+ if (ebc_is_autocomplete_query (context)) {
+ sd.func = ebc_generate_autocomplete_query (book_cache, stmt, search_type, context, error);
+ } else {
+ /* Generate the leading SELECT statement */
+ sd.func = ebc_generate_select (book_cache, stmt, search_type, context, error);
+
+ if (sd.func && EBC_STATUS_GEN_CONSTRAINTS (context->status)) {
+ /*
+ * Now generate the search expression on the main contacts table
+ */
+ g_string_append (stmt, " WHERE ");
+ ebc_generate_constraints (book_cache, stmt, context->constraints, sexp);
+ }
+ }
+
+ if (sd.func) {
+ sd.uid_index = -1;
+ sd.revision_index = -1;
+ sd.object_index = search_type == SEARCH_FULL ? -1 : -2;
+ sd.extra_index = search_type == SEARCH_UID ? -2 : -1;
+ sd.out_value = out_value;
+
+ success = e_cache_sqlite_select (E_CACHE (book_cache), stmt->str,
+ ebc_search_select_cb, &sd, cancellable, error);
+ }
+
+ g_string_free (stmt, TRUE);
+
+ return success;
+}
+
+static gboolean
+ebc_search_internal (EBookCache *book_cache,
+ const gchar *sexp,
+ SearchType search_type,
+ gpointer out_value,
+ GCancellable *cancellable,
+ GError **error)
+{
+ PreflightContext context = PREFLIGHT_CONTEXT_INIT;
+ gboolean success = FALSE;
+
+ /* Now start with the query preflighting */
+ query_preflight (&context, book_cache, sexp);
+
+ switch (context.status) {
+ case PREFLIGHT_OK:
+ case PREFLIGHT_LIST_ALL:
+ case PREFLIGHT_NOT_SUMMARIZED:
+ /* No errors, let's really search */
+ success = ebc_do_search_query (
+ book_cache, &context, sexp,
+ search_type, out_value,
+ cancellable, error);
+ break;
+
+ case PREFLIGHT_INVALID:
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+ _("Invalid query: %s"), sexp);
+ break;
+
+ case PREFLIGHT_UNSUPPORTED:
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_UNSUPPORTED_QUERY,
+ _("Query contained unsupported elements"));
+ break;
+ }
+
+ preflight_context_clear (&context);
+
+ return success;
+}
+
+/******************************************************************
+ * EBookCacheCursor Implementation *
+ ******************************************************************/
+typedef struct _CursorState CursorState;
+
+struct _CursorState {
+ gchar **values; /* The current cursor position, results will be returned after this
position */
+ gchar *last_uid; /* The current cursor contact UID position, used as a tie breaker */
+ EBookCacheCursorOrigin position;/* The position is updated with the cursor state and is used to
distinguish
+ * between the beginning and the ending of the cursor's contact list.
+ * While the cursor is in a non-null state, the position will be
+ * E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT.
+ */
+};
+
+struct _EBookCacheCursor {
+ EBookBackendSExp *sexp; /* An EBookBackendSExp based on the query, used by
e_book_sqlite_cursor_compare () */
+ gchar *select_vcards; /* The first fragment when querying results */
+ gchar *select_count; /* The first fragment when querying contact counts */
+ gchar *query; /* The SQL query expression derived from the passed search expression */
+ gchar *order; /* The normal order SQL query fragment to append at the end, containing
ORDER BY etc */
+ gchar *reverse_order; /* The reverse order SQL query fragment to append at the end,
containing ORDER BY etc */
+
+ EContactField *sort_fields; /* The fields to sort in a query in the order or sort priority */
+ EBookCursorSortType *sort_types; /* The sort method to use for each field */
+ gint n_sort_fields; /* The amound of sort fields */
+
+ CursorState state;
+};
+
+static CursorState *cursor_state_copy (EBookCacheCursor *cursor,
+ CursorState *state);
+static void cursor_state_free (EBookCacheCursor *cursor,
+ CursorState *state);
+static void cursor_state_clear (EBookCacheCursor *cursor,
+ CursorState *state,
+ EBookCacheCursorOrigin position);
+static void cursor_state_set_from_contact (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ CursorState *state,
+ EContact *contact);
+static void cursor_state_set_from_vcard (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ CursorState *state,
+ const gchar *vcard);
+
+static CursorState *
+cursor_state_copy (EBookCacheCursor *cursor,
+ CursorState *state)
+{
+ CursorState *copy;
+ gint ii;
+
+ copy = g_slice_new0 (CursorState);
+ copy->values = g_new0 (gchar *, cursor->n_sort_fields);
+
+ for (ii = 0; ii < cursor->n_sort_fields; ii++) {
+ copy->values[ii] = g_strdup (state->values[ii]);
+ }
+
+ copy->last_uid = g_strdup (state->last_uid);
+ copy->position = state->position;
+
+ return copy;
+}
+
+static void
+cursor_state_free (EBookCacheCursor *cursor,
+ CursorState *state)
+{
+ if (state) {
+ cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+ g_free (state->values);
+ g_slice_free (CursorState, state);
+ }
+}
+
+static void
+cursor_state_clear (EBookCacheCursor *cursor,
+ CursorState *state,
+ EBookCacheCursorOrigin position)
+{
+ gint ii;
+
+ for (ii = 0; ii < cursor->n_sort_fields; ii++) {
+ g_free (state->values[ii]);
+ state->values[ii] = NULL;
+ }
+
+ g_free (state->last_uid);
+ state->last_uid = NULL;
+ state->position = position;
+}
+
+static void
+cursor_state_set_from_contact (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ CursorState *state,
+ EContact *contact)
+{
+ gint ii;
+
+ cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+
+ for (ii = 0; ii < cursor->n_sort_fields; ii++) {
+ const gchar *string = e_contact_get_const (contact, cursor->sort_fields[ii]);
+ SummaryField *field;
+ gchar *sort_key;
+
+ if (string)
+ sort_key = e_collator_generate_key (book_cache->priv->collator, string, NULL);
+ else
+ sort_key = g_strdup ("");
+
+ field = summary_field_get (book_cache, cursor->sort_fields[ii]);
+
+ if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ state->values[ii] = sort_key;
+ } else {
+ state->values[ii] = ebc_encode_vcard_sort_key (sort_key);
+ g_free (sort_key);
+ }
+ }
+
+ state->last_uid = e_contact_get (contact, E_CONTACT_UID);
+ state->position = E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT;
+}
+
+static void
+cursor_state_set_from_vcard (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ CursorState *state,
+ const gchar *vcard)
+{
+ EContact *contact;
+
+ contact = e_contact_new_from_vcard (vcard);
+ cursor_state_set_from_contact (book_cache, cursor, state, contact);
+ g_object_unref (contact);
+}
+
+static gboolean
+ebc_cursor_setup_query (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ const gchar *sexp,
+ GError **error)
+{
+ PreflightContext context = PREFLIGHT_CONTEXT_INIT;
+ GString *string;
+
+ /* Preflighting and error checking */
+ if (sexp) {
+ query_preflight (&context, book_cache, sexp);
+
+ if (context.status > PREFLIGHT_NOT_SUMMARIZED) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+ _("Invalid query for a book cursor"));
+
+ preflight_context_clear (&context);
+ return FALSE;
+ }
+ }
+
+ /* Now we caught the errors, let's generate our queries and get out of here ... */
+ g_free (cursor->select_vcards);
+ g_free (cursor->select_count);
+ g_free (cursor->query);
+ g_clear_object (&(cursor->sexp));
+
+ /* Generate the leading SELECT portions that we need */
+ string = g_string_new ("");
+ ebc_generate_select (book_cache, string, SEARCH_FULL, &context, NULL);
+ cursor->select_vcards = g_string_free (string, FALSE);
+
+ string = g_string_new ("");
+ ebc_generate_select (book_cache, string, SEARCH_COUNT, &context, NULL);
+ cursor->select_count = g_string_free (string, FALSE);
+
+ if (!sexp || context.status == PREFLIGHT_LIST_ALL) {
+ cursor->query = NULL;
+ cursor->sexp = NULL;
+ } else {
+ /* Generate the constraints for our queries
+ */
+ string = g_string_new (NULL);
+ ebc_generate_constraints (book_cache, string, context.constraints, sexp);
+ cursor->query = g_string_free (string, FALSE);
+ cursor->sexp = e_book_backend_sexp_new (sexp);
+ }
+
+ preflight_context_clear (&context);
+
+ return TRUE;
+}
+
+static gchar *
+ebc_cursor_order_by_fragment (EBookCache *book_cache,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ gboolean reverse)
+{
+ GString *string;
+ gint ii;
+
+ string = g_string_new ("ORDER BY ");
+
+ for (ii = 0; ii < n_sort_fields; ii++) {
+ SummaryField *field = summary_field_get (book_cache, sort_fields[ii]);
+
+ if (ii > 0)
+ g_string_append (string, ", ");
+
+ if (field &&
+ (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ g_string_append (string, "summary.");
+ g_string_append (string, field->dbname);
+ g_string_append (string, "_" EBC_SUFFIX_SORT_KEY " ");
+ } else {
+ g_string_append (string, "summary." E_CACHE_COLUMN_OBJECT);
+ g_string_append (string, " COLLATE ");
+ g_string_append (string, EBC_COLLATE_PREFIX);
+ g_string_append (string, e_contact_field_name (sort_fields[ii]));
+ g_string_append_c (string, ' ');
+ }
+
+ if (reverse)
+ g_string_append (string, (sort_types[ii] == E_BOOK_CURSOR_SORT_ASCENDING ? "DESC" :
"ASC"));
+ else
+ g_string_append (string, (sort_types[ii] == E_BOOK_CURSOR_SORT_ASCENDING ? "ASC" :
"DESC"));
+ }
+
+ /* Also order the UID, since it's our tie breaker */
+ if (n_sort_fields > 0)
+ g_string_append (string, ", ");
+
+ g_string_append (string, "summary." E_CACHE_COLUMN_UID " ");
+ g_string_append (string, reverse ? "DESC" : "ASC");
+
+ return g_string_free (string, FALSE);
+}
+
+static EBookCacheCursor *
+ebc_cursor_new (EBookCache *book_cache,
+ const gchar *sexp,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields)
+{
+ EBookCacheCursor *cursor = g_slice_new0 (EBookCacheCursor);
+
+ cursor->order = ebc_cursor_order_by_fragment (book_cache, sort_fields, sort_types, n_sort_fields,
FALSE);
+ cursor->reverse_order = ebc_cursor_order_by_fragment (book_cache, sort_fields, sort_types,
n_sort_fields, TRUE);
+
+ /* Sort parameters */
+ cursor->n_sort_fields = n_sort_fields;
+ cursor->sort_fields = g_memdup (sort_fields, sizeof (EContactField) * n_sort_fields);
+ cursor->sort_types = g_memdup (sort_types, sizeof (EBookCursorSortType) * n_sort_fields);
+
+ /* Cursor state */
+ cursor->state.values = g_new0 (gchar *, n_sort_fields);
+ cursor->state.last_uid = NULL;
+ cursor->state.position = E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN;
+
+ return cursor;
+}
+
+static void
+ebc_cursor_free (EBookCacheCursor *cursor)
+{
+ if (cursor) {
+ cursor_state_clear (cursor, &(cursor->state), E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+ g_free (cursor->state.values);
+
+ g_clear_object (&(cursor->sexp));
+ g_free (cursor->select_vcards);
+ g_free (cursor->select_count);
+ g_free (cursor->query);
+ g_free (cursor->order);
+ g_free (cursor->reverse_order);
+ g_free (cursor->sort_fields);
+ g_free (cursor->sort_types);
+
+ g_slice_free (EBookCacheCursor, cursor);
+ }
+}
+
+#define GREATER_OR_LESS(cursor, idx, reverse) \
+ (reverse ? \
+ (((EBookCacheCursor *) cursor)->sort_types[idx] == E_BOOK_CURSOR_SORT_ASCENDING ? '<' : '>') : \
+ (((EBookCacheCursor *) cursor)->sort_types[idx] == E_BOOK_CURSOR_SORT_ASCENDING ? '>' : '<'))
+
+static inline void
+ebc_cursor_format_equality (EBookCache *book_cache,
+ GString *string,
+ EContactField field_id,
+ const gchar *value,
+ gchar equality)
+{
+ SummaryField *field = summary_field_get (book_cache, field_id);
+
+ if (field &&
+ (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ g_string_append (string, "summary.");
+ g_string_append (string, field->dbname);
+ g_string_append (string, "_" EBC_SUFFIX_SORT_KEY " ");
+
+ e_cache_sqlite_stmt_append_printf (string, "%c %Q", equality, value);
+ } else {
+ e_cache_sqlite_stmt_append_printf (string, "(summary." E_CACHE_COLUMN_OBJECT " %c %Q ",
equality, value);
+
+ g_string_append (string, "COLLATE " EBC_COLLATE_PREFIX);
+ g_string_append (string, e_contact_field_name (field_id));
+ g_string_append_c (string, ')');
+ }
+}
+
+static gchar *
+ebc_cursor_constraints (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ CursorState *state,
+ gboolean reverse,
+ gboolean include_current_uid)
+{
+ GString *string;
+ gint ii, jj;
+
+ /* Example for:
+ * ORDER BY family_name ASC, given_name DESC
+ *
+ * Where current cursor values are:
+ * family_name = Jackson
+ * given_name = Micheal
+ *
+ * With reverse = FALSE
+ *
+ * (summary.family_name > 'Jackson') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name < 'Micheal') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name = 'Micheal' AND summary.uid >
'last-uid')
+ *
+ * With reverse = TRUE (needed for moving the cursor backwards through results)
+ *
+ * (summary.family_name < 'Jackson') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name > 'Micheal') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name = 'Micheal' AND summary.uid <
'last-uid')
+ *
+ */
+ string = g_string_new (NULL);
+
+ for (ii = 0; ii <= cursor->n_sort_fields; ii++) {
+ /* Break once we hit a NULL value */
+ if ((ii < cursor->n_sort_fields && state->values[ii] == NULL) ||
+ (ii == cursor->n_sort_fields && state->last_uid == NULL))
+ break;
+
+ /* Between each qualifier, add an 'OR' */
+ if (ii > 0)
+ g_string_append (string, " OR ");
+
+ /* Begin qualifier */
+ g_string_append_c (string, '(');
+
+ /* Create the '=' statements leading up to the current tie breaker */
+ for (jj = 0; jj < ii; jj++) {
+ ebc_cursor_format_equality (book_cache, string,
+ cursor->sort_fields[jj],
+ state->values[jj], '=');
+ g_string_append (string, " AND ");
+ }
+
+ if (ii == cursor->n_sort_fields) {
+ /* The 'include_current_uid' clause is used for calculating
+ * the current position of the cursor, inclusive of the
+ * current position.
+ */
+ if (include_current_uid)
+ g_string_append_c (string, '(');
+
+ /* Append the UID tie breaker */
+ e_cache_sqlite_stmt_append_printf (
+ string,
+ "summary." E_CACHE_COLUMN_UID " %c %Q",
+ reverse ? '<' : '>',
+ state->last_uid);
+
+ if (include_current_uid)
+ e_cache_sqlite_stmt_append_printf (
+ string,
+ " OR summary." E_CACHE_COLUMN_UID " = %Q)",
+ state->last_uid);
+ } else {
+ /* SPECIAL CASE: If we have a parially set cursor state, then we must
+ * report next results that are inclusive of the final qualifier.
+ *
+ * This allows one to set the cursor with the family name set to 'J'
+ * and include the results for contact's Mr & Miss 'J'.
+ */
+ gboolean include_exact_match =
+ (reverse == FALSE &&
+ ((ii + 1 < cursor->n_sort_fields && state->values[ii + 1] == NULL) ||
+ (ii + 1 == cursor->n_sort_fields && state->last_uid == NULL)));
+
+ if (include_exact_match)
+ g_string_append_c (string, '(');
+
+ /* Append the final qualifier for this field */
+ ebc_cursor_format_equality (book_cache, string,
+ cursor->sort_fields[ii],
+ state->values[ii],
+ GREATER_OR_LESS (cursor, ii, reverse));
+
+ if (include_exact_match) {
+ g_string_append (string, " OR ");
+ ebc_cursor_format_equality (book_cache, string,
+ cursor->sort_fields[ii],
+ state->values[ii], '=');
+ g_string_append_c (string, ')');
+ }
+ }
+
+ /* End qualifier */
+ g_string_append_c (string, ')');
+ }
+
+ return g_string_free (string, FALSE);
+}
+
+static gboolean
+ebc_get_int_cb (ECache *cache,
+ gint ncols,
+ const gchar **column_names,
+ const gchar **column_values,
+ gpointer user_data)
+{
+ gint *pint = user_data;
+
+ g_return_val_if_fail (pint != NULL, FALSE);
+
+ if (ncols == 1) {
+ *pint = column_values[0] ? g_ascii_strtoll (column_values[0], NULL, 10) : 0;
+ } else {
+ *pint = 0;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+cursor_count_total_locked (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint *out_total,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GString *query;
+ gboolean success;
+
+ query = g_string_new (cursor->select_count);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Execute the query */
+ success = e_cache_sqlite_select (E_CACHE (book_cache), query->str, ebc_get_int_cb, out_total,
cancellable, error);
+
+ g_string_free (query, TRUE);
+
+ return success;
+}
+
+static gboolean
+cursor_count_position_locked (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint *out_position,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GString *query;
+ gboolean success;
+
+ query = g_string_new (cursor->select_count);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Add the cursor constraints (if any) */
+ if (cursor->state.values[0] != NULL) {
+ gchar *constraints = NULL;
+
+ if (!cursor->query)
+ g_string_append (query, " WHERE ");
+ else
+ g_string_append (query, " AND ");
+
+ /* Here we do a reverse query, we're looking for all the
+ * results leading up to the current cursor value, including
+ * the cursor value
+ */
+ constraints = ebc_cursor_constraints (book_cache, cursor, &(cursor->state), TRUE, TRUE);
+
+ g_string_append_c (query, '(');
+ g_string_append (query, constraints);
+ g_string_append_c (query, ')');
+
+ g_free (constraints);
+ }
+
+ /* Execute the query */
+ success = e_cache_sqlite_select (E_CACHE (book_cache), query->str, ebc_get_int_cb, out_position,
cancellable, error);
+
+ g_string_free (query, TRUE);
+
+ return success;
+}
+
+static gboolean
+e_book_cache_get_string (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 gboolean
+e_book_cache_get_old_contacts_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ GSList **pold_contacts = user_data;
+
+ g_return_val_if_fail (pold_contacts != NULL, FALSE);
+ g_return_val_if_fail (ncols == 3, FALSE);
+
+ if (column_values[0] && column_values[1]) {
+ *pold_contacts = g_slist_prepend (*pold_contacts,
+ e_book_cache_search_data_new (column_values[0], column_values[1], column_values[2]));
+ }
+
+ return TRUE;
+}
+
+static gboolean
+e_book_cache_gather_table_names_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ GSList **ptables = user_data;
+
+ g_return_val_if_fail (ptables != NULL, FALSE);
+ g_return_val_if_fail (ncols == 1, FALSE);
+
+ *ptables = g_slist_prepend (*ptables, g_strdup (column_values[0]));
+
+ return TRUE;
+}
+
+static gboolean
+e_book_cache_migrate (ECache *cache,
+ gint from_version,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache = E_BOOK_CACHE (cache);
+ gboolean success = TRUE;
+
+ /* Migration from EBookSqlite database */
+ if (from_version <= 0) {
+ GSList *tables = NULL, *old_contacts = NULL, *link;
+
+ if (e_cache_sqlite_select (cache, "SELECT uid,vcard,bdata FROM folder_id ORDER BY uid",
+ e_book_cache_get_old_contacts_cb, &old_contacts, cancellable, NULL)) {
+
+ old_contacts = g_slist_reverse (old_contacts);
+
+ for (link = old_contacts; link && success; link = g_slist_next (link)) {
+ EBookCacheSearchData *data = link->data;
+ EContact *contact;
+
+ if (!data)
+ continue;
+
+ contact = e_contact_new_from_vcard_with_uid (data->vcard, data->uid);
+ if (!contact)
+ continue;
+
+ success = e_book_cache_put_contact (book_cache, contact, data->extra, FALSE,
cancellable, error);
+ }
+ }
+
+ /* Delete obsolete tables */
+ success = success && e_cache_sqlite_select (cache,
+ "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'folder_id%'",
+ e_book_cache_gather_table_names_cb, &tables, cancellable, error);
+
+ for (link = tables; link && success; link = g_slist_next (link)) {
+ const gchar *name = link->data;
+ gchar *stmt;
+
+ if (!link)
+ continue;
+
+ stmt = e_cache_sqlite_stmt_printf ("DROP TABLE IF EXISTS %Q", name);
+ success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+ e_cache_sqlite_stmt_free (stmt);
+ }
+
+ g_slist_free_full (tables, g_free);
+
+ success = success && e_cache_sqlite_exec (cache, "DROP TABLE IF EXISTS keys", cancellable,
error);
+ success = success && e_cache_sqlite_exec (cache, "DROP TABLE IF EXISTS folders", cancellable,
error);
+ success = success && e_cache_sqlite_exec (cache, "DROP TABLE IF EXISTS folder_id",
cancellable, error);
+
+ if (success) {
+ /* Save the changes by finishing the transaction */
+ e_cache_unlock (cache, E_CACHE_UNLOCK_COMMIT);
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ /* Try to vacuum, but do not claim any error if failed */
+ e_cache_sqlite_maybe_vacuum (cache, cancellable, NULL);
+ }
+
+ g_slist_free_full (old_contacts, (GDestroyNotify) e_book_cache_search_data_free);
+ }
+
+ /* Add any version-related changes here */
+ /*if (from_version < E_BOOK_CACHE_VERSION) {
+ }*/
+
+ return success;
+}
+
+static gboolean
+e_book_cache_populate_other_columns (EBookCache *book_cache,
+ ESourceBackendSummarySetup *setup,
+ GSList **out_columns, /* ECacheColumnInfo * */
+ GError **error)
+{
+ GSList *columns = NULL;
+ gboolean use_default;
+ gboolean success = TRUE;
+ gint ii;
+
+ g_return_val_if_fail (out_columns != NULL, FALSE);
+
+ #define add_column(_name, _type, _index_name) G_STMT_START { \
+ columns = g_slist_prepend (columns, e_cache_column_info_new (_name, _type, _index_name)); \
+ } G_STMT_END
+
+ add_column (EBC_COLUMN_EXTRA, "TEXT", NULL);
+
+ use_default = !setup;
+
+ if (setup) {
+ EContactField *fields;
+ EContactField *indexed_fields;
+ EBookIndexType *index_types = NULL;
+ gint n_fields = 0, n_indexed_fields = 0, ii;
+
+ fields = e_source_backend_summary_setup_get_summary_fields (setup, &n_fields);
+ indexed_fields = e_source_backend_summary_setup_get_indexed_fields (setup, &index_types,
&n_indexed_fields);
+
+ if (n_fields <= 0 || n_fields >= EBC_MAX_SUMMARY_FIELDS) {
+ if (n_fields)
+ g_warning ("EBookCache refused to create cache with more than %d summary
fields", EBC_MAX_SUMMARY_FIELDS);
+ use_default = TRUE;
+ } else {
+ GArray *summary_fields;
+
+ summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+
+ /* Ensure the non-optional fields first */
+ summary_field_append (summary_fields, E_CONTACT_UID, error);
+ summary_field_append (summary_fields, E_CONTACT_REV, error);
+
+ for (ii = 0; ii < n_fields; ii++) {
+ if (!summary_field_append (summary_fields, fields[ii], error)) {
+ success = FALSE;
+ break;
+ }
+ }
+
+ if (!success) {
+ gint n_sfields;
+ SummaryField *sfields;
+
+ /* Properly free the array */
+ n_sfields = summary_fields->len;
+ sfields = (SummaryField *) g_array_free (summary_fields, FALSE);
+ summary_fields_array_free (sfields, n_sfields);
+
+ g_free (fields);
+ g_free (index_types);
+ g_free (indexed_fields);
+
+ g_slist_free_full (columns, e_cache_column_info_free);
+
+ return FALSE;
+ }
+
+ /* Add the 'indexed' flag to the SummaryField structs */
+ summary_fields_add_indexes (summary_fields, indexed_fields, index_types,
n_indexed_fields);
+
+ book_cache->priv->n_summary_fields = summary_fields->len;
+ book_cache->priv->summary_fields = (SummaryField *) g_array_free (summary_fields,
FALSE);
+ }
+
+ g_free (fields);
+ g_free (index_types);
+ g_free (indexed_fields);
+ }
+
+ if (use_default) {
+ GArray *summary_fields;
+
+ g_warn_if_fail (book_cache->priv->n_summary_fields == 0);
+
+ /* Create the default summary structs */
+ summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+ for (ii = 0; ii < G_N_ELEMENTS (default_summary_fields); ii++) {
+ summary_field_append (summary_fields, default_summary_fields[ii], NULL);
+ }
+
+ /* Add the default index flags */
+ summary_fields_add_indexes (
+ summary_fields,
+ default_indexed_fields,
+ default_index_types,
+ G_N_ELEMENTS (default_indexed_fields));
+
+ book_cache->priv->n_summary_fields = summary_fields->len;
+ book_cache->priv->summary_fields = (SummaryField *) g_array_free (summary_fields, FALSE);
+ }
+
+ #undef add_column
+
+ if (success) {
+ for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+ SummaryField *fld = &(book_cache->priv->summary_fields[ii]);
+
+ summary_field_init_dbnames (fld);
+
+ if (fld->type != E_TYPE_CONTACT_ATTR_LIST)
+ summary_field_prepend_columns (fld, &columns);
+ }
+ }
+
+ *out_columns = columns;
+
+ return success;
+}
+
+static gboolean
+e_book_cache_initialize (EBookCache *book_cache,
+ const gchar *filename,
+ ESource *source,
+ ESourceBackendSummarySetup *setup,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECache *cache;
+ GSList *other_columns = NULL;
+ sqlite3 *db;
+ gint ii, sqret;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (filename != NULL, FALSE);
+ if (source)
+ g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+ if (setup)
+ g_return_val_if_fail (E_IS_SOURCE_BACKEND_SUMMARY_SETUP (setup), FALSE);
+
+ if (source)
+ book_cache->priv->source = g_object_ref (source);
+
+ cache = E_CACHE (book_cache);
+
+ success = e_book_cache_populate_other_columns (book_cache, setup, &other_columns, error);
+ if (!success)
+ goto exit;
+
+ success = e_cache_initialize_sync (cache, filename, other_columns, cancellable, error);
+ if (!success)
+ goto exit;
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ db = e_cache_get_sqlitedb (cache);
+ sqret = SQLITE_OK;
+
+ /* Install our custom functions */
+ for (ii = 0; sqret == SQLITE_OK && ii < G_N_ELEMENTS (ebc_custom_functions); ii++) {
+ sqret = sqlite3_create_function (
+ db,
+ ebc_custom_functions[ii].name,
+ ebc_custom_functions[ii].arguments,
+ SQLITE_UTF8, book_cache,
+ ebc_custom_functions[ii].func,
+ NULL, NULL);
+ }
+
+ /* Fallback COLLATE implementations generated on demand */
+ if (sqret == SQLITE_OK)
+ sqret = sqlite3_collation_needed (db, book_cache, ebc_generate_collator);
+
+ if (sqret != SQLITE_OK) {
+ if (!db) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_LOAD, _("Insufficient
memory"));
+ } else {
+ const gchar *errmsg = sqlite3_errmsg (db);
+
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE, _("Can't open database %s:
%s"), filename, errmsg);
+ }
+
+ success = FALSE;
+ }
+
+ success = success && ebc_init_locale (book_cache, cancellable, error);
+
+ success = success && ebc_init_aux_tables (book_cache, cancellable, error);
+
+ /* Check for data migration */
+ success = success && e_book_cache_migrate (cache, e_cache_get_version (cache), cancellable, error);
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ if (!success)
+ goto exit;
+
+ if (e_cache_get_version (cache) != E_BOOK_CACHE_VERSION)
+ e_cache_set_version (cache, E_BOOK_CACHE_VERSION);
+
+ exit:
+ g_slist_free_full (other_columns, e_cache_column_info_free);
+
+ return success;
+}
+
+/**
+ * e_book_cache_new:
+ * @filename: file name to load or create the new cache
+ * @source: (nullable): an optional #ESource, associated with the #EBookCache, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EBookCache with the default summary configuration.
+ *
+ * Aside from the mandatory fields %E_CONTACT_UID, %E_CONTACT_REV,
+ * the default configuration stores the following fields for quick
+ * performance of searches: %E_CONTACT_FILE_AS, %E_CONTACT_NICKNAME,
+ * %E_CONTACT_FULL_NAME, %E_CONTACT_GIVEN_NAME, %E_CONTACT_FAMILY_NAME,
+ * %E_CONTACT_EMAIL, %E_CONTACT_TEL, %E_CONTACT_IS_LIST, %E_CONTACT_LIST_SHOW_ADDRESSES,
+ * and %E_CONTACT_WANTS_HTML.
+ *
+ * The fields %E_CONTACT_FULL_NAME and %E_CONTACT_EMAIL are configured
+ * to respond extra quickly with the %E_BOOK_INDEX_PREFIX index flag.
+ *
+ * The fields %E_CONTACT_FILE_AS, %E_CONTACT_FAMILY_NAME and
+ * %E_CONTACT_GIVEN_NAME are configured to perform well with
+ * the #EBookCacheCursor, using the %E_BOOK_INDEX_SORT_KEY
+ * index flag.
+ *
+ * Returns: (transfer full) (nullable): A new #EBookCache or %NULL on error
+ *
+ * Since: 3.26
+ **/
+EBookCache *
+e_book_cache_new (const gchar *filename,
+ ESource *source,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (filename != NULL, NULL);
+
+ return e_book_cache_new_full (filename, source, NULL, cancellable, error);
+}
+
+/**
+ * e_book_cache_new_full:
+ * @filename: file name to load or create the new cache
+ * @source: (nullable): an optional #ESource, associated with the #EBookCache, or %NULL
+ * @setup: (nullable): an #ESourceBackendSummarySetup describing how the summary should be setup, or %NULL
to use the default
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EBookCache with the given or the default summary configuration.
+ *
+ * Like e_book_sqlite_new(), but allows configuration of which contact fields
+ * will be stored for quick reference in the summary. The configuration indicated by
+ * @setup will only be taken into account when initially creating the underlying table,
+ * further configurations will be ignored.
+ *
+ * The fields %E_CONTACT_UID and %E_CONTACT_REV are not optional,
+ * they will be stored in the summary regardless of this function's parameters.
+ * Only #EContactFields with the type %G_TYPE_STRING, %G_TYPE_BOOLEAN or
+ * %E_TYPE_CONTACT_ATTR_LIST are currently supported.
+ *
+ * Returns: (transfer full) (nullable): A new #EBookCache or %NULL on error
+ *
+ * Since: 3.26
+ **/
+EBookCache *
+e_book_cache_new_full (const gchar *filename,
+ ESource *source,
+ ESourceBackendSummarySetup *setup,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+
+ g_return_val_if_fail (filename != NULL, NULL);
+
+ book_cache = g_object_new (E_TYPE_BOOK_CACHE, NULL);
+
+ if (!e_book_cache_initialize (book_cache, filename, source, setup, cancellable, error)) {
+ g_object_unref (book_cache);
+ book_cache = NULL;
+ }
+
+ return book_cache;
+}
+
+/**
+ * e_book_cache_ref_source:
+ * @book_cache: An #EBookCache
+ *
+ * References the #ESource to which @book_cache is paired,
+ * use g_object_unref() when no longer needed.
+ * It can be %NULL in some cases, like when running tests.
+ *
+ * Returns: (transfer full): A reference to the #ESource to which @book_cache
+ * is paired, or %NULL.
+ *
+ * Since: 3.26
+ **/
+ESource *
+e_book_cache_ref_source (EBookCache *book_cache)
+{
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+ if (book_cache->priv->source)
+ return g_object_ref (book_cache->priv->source);
+
+ return NULL;
+}
+
+/**
+ * e_book_cache_set_locale:
+ * @book_cache: An #EBookCache
+ * @lc_collate: The new locale for the cache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Relocalizes any locale specific data in the specified
+ * new @lc_collate locale.
+ *
+ * The @lc_collate locale setting is stored and remembered on
+ * subsequent accesses of the cache, changing the locale will
+ * store the new locale and will modify sort keys and any
+ * locale specific data in the cache.
+ *
+ * As a side effect, it's possible that changing the locale
+ * will cause stored vCard-s to change.
+ *
+ * Returns: Whether the new locale was successfully set.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_set_locale (EBookCache *book_cache,
+ const gchar *lc_collate,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECache *cache;
+ gboolean success, changed = FALSE;
+ gchar *stored_lc_collate = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+
+ cache = E_CACHE (book_cache);
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ success = ebc_set_locale_internal (book_cache, lc_collate, error);
+
+ if (success)
+ stored_lc_collate = e_cache_dup_key (cache, EBC_KEY_LC_COLLATE, NULL);
+
+ if (success && g_strcmp0 (stored_lc_collate, lc_collate) != 0)
+ success = ebc_upgrade (book_cache, cancellable, error);
+
+ /* If for some reason we failed, then reset the collator to use the old locale */
+ if (!success && stored_lc_collate && stored_lc_collate[0]) {
+ ebc_set_locale_internal (book_cache, stored_lc_collate, NULL);
+ changed = TRUE;
+ }
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ g_free (stored_lc_collate);
+
+ if (success && changed)
+ g_object_notify (G_OBJECT (book_cache), "locale");
+
+ return success;
+}
+
+/**
+ * e_book_cache_dup_locale:
+ * @book_cache: An #EBookCache
+ *
+ * Returns: (transfer full): A new string containing the current local
+ * being used by the @book_cache. Free it with g_free(), when no
+ * longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_book_cache_dup_locale (EBookCache *book_cache)
+{
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+ return g_strdup (book_cache->priv->locale);
+}
+
+/**
+ * e_book_cache_ref_collator:
+ * @book_cache: An #EBookCache
+ *
+ * References the currently active #ECollator for @book_cache,
+ * use e_collator_unref() when finished using the returned collator.
+ *
+ * Note that the active collator will change with the active locale setting.
+ *
+ * Returns: (transfer full): A reference to the active collator.
+ *
+ * Since: 3.26
+ **/
+ECollator *
+e_book_cache_ref_collator (EBookCache *book_cache)
+{
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+ return e_collator_ref (book_cache->priv->collator);
+}
+
+/**
+ * e_book_cache_put_contact:
+ * @book_cache: An #EBookCache
+ * @contact: EContact to be added
+ * @extra: Extra data to store in association with this contact
+ * @is_offline: Whether putting this contact in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * This is a convenience wrapper for e_book_cache_put_contacts(),
+ * which is the preferred way to add or modify multiple contacts when possible.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_put_contact (EBookCache *book_cache,
+ EContact *contact,
+ const gchar *extra,
+ gboolean is_offline,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *contacts, *extras;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ contacts = g_slist_append (NULL, contact);
+ extras = g_slist_append (NULL, (gpointer) extra);
+
+ success = e_book_cache_put_contacts (book_cache, contacts, extras, is_offline, cancellable, error);
+
+ g_slist_free (contacts);
+ g_slist_free (extras);
+
+ return success;
+}
+
+/**
+ * e_book_cache_put_contacts:
+ * @book_cache: An #EBookCache
+ * @contacts: (element-type EContact): A list of contacts to add to @book_cache
+ * @extras: (nullable) (element-type utf8): A list of extra data to store in association with the @contacts
+ * @is_offline: Whether putting these contacts in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Adds or replaces contacts in @book_cache.
+ *
+ * If @extras is specified, it must have an equal length as the @contacts list. Each element
+ * from the @extras list will be stored in association with its corresponding contact
+ * in the @contacts list.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_put_contacts (EBookCache *book_cache,
+ const GSList *contacts,
+ const GSList *extras,
+ gboolean is_offline,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const GSList *clink, *elink;
+ ECache *cache;
+ GHashTable *other_columns;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (contacts != NULL, FALSE);
+ g_return_val_if_fail (extras == NULL || g_slist_length ((GSList *) extras) == g_slist_length ((GSList
*) contacts), FALSE);
+
+ cache = E_CACHE (book_cache);
+ other_columns = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, NULL, g_free);
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ for (clink = contacts, elink = extras; clink; clink = g_slist_next (clink), elink = g_slist_next
(elink)) {
+ EContact *contact = clink->data;
+ const gchar *extra = elink ? elink->data : NULL;
+ gchar *uid, *rev, *vcard;
+
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+ g_return_val_if_fail (vcard != NULL, FALSE);
+
+ g_hash_table_remove_all (other_columns);
+
+ if (extra)
+ g_hash_table_insert (other_columns, (gpointer) EBC_COLUMN_EXTRA, g_strdup (extra));
+
+ uid = e_contact_get (contact, E_CONTACT_UID);
+ rev = e_contact_get (contact, E_CONTACT_REV);
+
+ ebc_fill_other_columns (book_cache, contact, other_columns);
+
+ if (is_offline)
+ success = e_cache_put_offline (cache, uid, rev, vcard, other_columns, cancellable,
error);
+ else
+ success = e_cache_put (cache, uid, rev, vcard, other_columns, cancellable, error);
+
+ g_free (vcard);
+ g_free (rev);
+ g_free (uid);
+
+ if (!success)
+ break;
+ }
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ g_hash_table_destroy (other_columns);
+
+ return success;
+}
+
+/**
+ * e_book_cache_remove_contact:
+ * @book_cache: An #EBookCache
+ * @uid: the uid of the contact to remove
+ * @is_offline: Whether removing this contact in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes the contact identified by @uid from @book_cache.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_remove_contact (EBookCache *book_cache,
+ const gchar *uid,
+ gboolean is_offline,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *uids;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ uids = g_slist_append (NULL, (gpointer) uid);
+
+ success = e_book_cache_remove_contacts (book_cache, uids, is_offline, cancellable, error);
+
+ g_slist_free (uids);
+
+ return success;
+}
+
+/**
+ * e_book_cache_remove_contacts:
+ * @book_cache: An #EBookCache
+ * @uids: (element-type utf8): a #GSList of uids indicating which contacts to remove
+ * @is_offline: Whether removing this contact in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes the contacts indicated by @uids from @book_cache.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_remove_contacts (EBookCache *book_cache,
+ const GSList *uids,
+ gboolean is_offline,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECache *cache;
+ const GSList *link;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uids != NULL, FALSE);
+
+ cache = E_CACHE (book_cache);
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ for (link = uids; success && link; link = g_slist_next (link)) {
+ const gchar *uid = link->data;
+
+ if (is_offline)
+ success = e_cache_remove_offline (cache, uid, cancellable, error);
+ else
+ success = e_cache_remove (cache, uid, cancellable, error);
+ }
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ return success;
+}
+
+/**
+ * e_book_cache_get_contact:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to fetch
+ * @meta_contact: Whether an entire contact is desired, or only the metadata
+ * @out_contact: (out) (transfer full): Return location to store the fetched contact
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Fetch the #EContact specified by @uid in @book_cache.
+ *
+ * If @meta_contact is specified, then a shallow #EContact will be created
+ * holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_get_contact (EBookCache *book_cache,
+ const gchar *uid,
+ gboolean meta_contact,
+ EContact **out_contact,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *vcard = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_contact != NULL, FALSE);
+
+ *out_contact = NULL;
+
+ if (!e_book_cache_get_vcard (book_cache, uid, meta_contact, &vcard, cancellable, error) ||
+ !vcard) {
+ return FALSE;
+ }
+
+ *out_contact = e_contact_new_from_vcard_with_uid (vcard, uid);
+
+ g_free (vcard);
+
+ return TRUE;
+}
+
+/**
+ * e_book_cache_get_vcard:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to fetch
+ * @meta_contact: Whether an entire contact is desired, or only the metadata
+ * @out_vcard: (out) (transfer full): Return location to store the fetched vCard string
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Fetch a vCard string for @uid in @book_cache.
+ *
+ * If @meta_contact is specified, then a shallow vCard representation will be
+ * created holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_get_vcard (EBookCache *book_cache,
+ const gchar *uid,
+ gboolean meta_contact,
+ gchar **out_vcard,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *full_vcard, *revision = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_vcard != NULL, FALSE);
+
+ *out_vcard = NULL;
+
+ full_vcard = e_cache_get (E_CACHE (book_cache), uid,
+ meta_contact ? &revision : NULL,
+ NULL, cancellable, error);
+
+ if (!full_vcard) {
+ g_warn_if_fail (revision == NULL);
+ return FALSE;
+ }
+
+ if (meta_contact) {
+ EContact *contact = e_contact_new ();
+
+ e_contact_set (contact, E_CONTACT_UID, uid);
+ if (revision)
+ e_contact_set (contact, E_CONTACT_REV, revision);
+
+ *out_vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+
+ g_object_unref (contact);
+ g_free (full_vcard);
+ } else {
+ *out_vcard = full_vcard;
+ }
+
+ g_free (revision);
+
+ return TRUE;
+}
+
+/**
+ * e_book_cache_set_contact_extra:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to set the extra data for
+ * @extra: (nullable): The extra data to set
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets or replaces the extra data associated with @uid.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_set_contact_extra (EBookCache *book_cache,
+ const gchar *uid,
+ const gchar *extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ if (extra) {
+ stmt = e_cache_sqlite_stmt_printf (
+ "UPDATE " E_CACHE_TABLE_OBJECTS " SET " EBC_COLUMN_EXTRA "=%Q"
+ " WHERE " E_CACHE_COLUMN_UID "=%Q",
+ extra, uid);
+ } else {
+ stmt = e_cache_sqlite_stmt_printf (
+ "UPDATE " E_CACHE_TABLE_OBJECTS " SET " EBC_COLUMN_EXTRA "=NULL"
+ " WHERE " E_CACHE_COLUMN_UID "=%Q",
+ uid);
+ }
+
+ success = e_cache_sqlite_exec (E_CACHE (book_cache), stmt, cancellable, error);
+
+ e_cache_sqlite_stmt_free (stmt);
+
+ return success;
+}
+
+/**
+ * e_book_cache_get_contact_extra:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to fetch the extra data for
+ * @out_extra: (out) (transfer full): Return location to store the extra data
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Fetches the extra data previously set for @uid, either with
+ * e_book_cache_set_contact_extra() or when adding contacts.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_get_contact_extra (EBookCache *book_cache,
+ const gchar *uid,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ stmt = e_cache_sqlite_stmt_printf (
+ "SELECT " EBC_COLUMN_EXTRA " FROM " E_CACHE_TABLE_OBJECTS
+ " WHERE " E_CACHE_COLUMN_UID "=%Q",
+ uid);
+
+ success = e_cache_sqlite_select (E_CACHE (book_cache), stmt, e_book_cache_get_string, out_extra,
cancellable, error);
+
+ e_cache_sqlite_stmt_free (stmt);
+
+ return success;
+}
+
+/**
+ * e_book_cache_search:
+ * @book_cache: An #EBookCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to list all stored contacts
+ * @meta_contacts: Whether entire contacts are desired, or only the metadata
+ * @out_list: (out) (transfer full) (element-type EBookCacheSearchData): Return location
+ * to store a #GSList of #EBookCacheSearchData structures
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches @book_cache for contacts matching the search expression @sexp.
+ *
+ * When @sexp refers only to #EContactFields configured in the summary of @book_cache,
+ * the search should always be quick, when searching for other #EContactFields
+ * a fallback will be used.
+ *
+ * The returned @out_list list should be freed with g_slist_free_full(list, e_book_cache_search_data_free)
+ * when no longer needed.
+ *
+ * If @meta_contact is specified, then shallow vCard representations will be
+ * created holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_search (EBookCache *book_cache,
+ const gchar *sexp,
+ gboolean meta_contacts,
+ GSList **out_list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (out_list != NULL, FALSE);
+
+ return ebc_search_internal (book_cache, sexp,
+ meta_contacts ? SEARCH_UID_AND_REV : SEARCH_FULL,
+ out_list, cancellable, error);
+}
+
+/**
+ * e_book_cache_search_uids:
+ * @book_cache: An #EBookCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to get all stored contacts
+ * @out_list: (out) (transfer full) (element-type utf8): Return location to store a #GSList of contact uids
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Similar to e_book_cache_search(), but fetches only a list of contact UIDs.
+ *
+ * The returned @out_list list should be freed with g_slist_free_full(list, g_free)
+ * when no longer needed.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_search_uids (EBookCache *book_cache,
+ const gchar *sexp,
+ GSList **out_list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (out_list != NULL, FALSE);
+
+ return ebc_search_internal (book_cache, sexp, SEARCH_UID, out_list, cancellable, error);
+}
+
+/**
+ * e_book_cache_cursor_new:
+ * @book_cache: An #EBookCache
+ * @sexp: search expression; use %NULL or an empty string to get all stored contacts
+ * @sort_fields: (array length=n_sort_fields): An array of #EContactField-s as sort keys in order of priority
+ * @sort_types: (array length=n_sort_fields): An array of #EBookCursorSortTypes, one for each field in
@sort_fields
+ * @n_sort_fields: The number of fields to sort results by
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EBookCacheCursor.
+ *
+ * The cursor should be freed with e_book_cache_cursor_free() when
+ * no longer needed.
+ *
+ * Returns: (transfer full): A newly created #EBookCacheCursor
+ *
+ * Since: 3.26
+ **/
+EBookCacheCursor *
+e_book_cache_cursor_new (EBookCache *book_cache,
+ const gchar *sexp,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ GError **error)
+{
+ EBookCacheCursor *cursor;
+ gint ii;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+ /* We don't like '\0' sexps, prefer NULL */
+ if (sexp && !*sexp)
+ sexp = NULL;
+
+ e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+ /* Need one sort key ... */
+ if (n_sort_fields == 0) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+ _("At least one sort field must be specified to use a cursor"));
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return NULL;
+ }
+
+ /* We only support string fields to sort the cursor */
+ for (ii = 0; ii < n_sort_fields; ii++) {
+ if (e_contact_field_type (sort_fields[ii]) != G_TYPE_STRING) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+ _("Cannot sort by a field that is not a string type"));
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return NULL;
+ }
+ }
+
+ /* Now we need to create the cursor instance before setting up the query
+ * (not really true, but more convenient that way).
+ */
+ cursor = ebc_cursor_new (book_cache, sexp, sort_fields, sort_types, n_sort_fields);
+
+ /* Setup the cursor's query expression which might fail */
+ if (!ebc_cursor_setup_query (book_cache, cursor, sexp, error)) {
+ ebc_cursor_free (cursor);
+ cursor = NULL;
+ }
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+ return cursor;
+}
+
+/**
+ * e_book_cache_cursor_free:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to free
+ *
+ * Frees the @cursor, previously allocated with e_book_cache_cursor_new().
+ *
+ * Since: 3.26
+ **/
+void
+e_book_cache_cursor_free (EBookCache *book_cache,
+ EBookCacheCursor *cursor)
+{
+ g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+ g_return_if_fail (cursor != NULL);
+
+ ebc_cursor_free (cursor);
+}
+
+typedef struct {
+ gint uid_index;
+ gint object_index;
+ gint extra_index;
+
+ GSList *results;
+ gchar *alloc_vcard;
+ const gchar *last_vcard;
+
+ gboolean collect_results;
+ gint n_results;
+} CursorCollectData;
+
+static gboolean
+ebc_collect_results_for_cursor_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ CursorCollectData *data = user_data;
+ const gchar *object = NULL, *extra = NULL;
+
+ if (data->uid_index == -1 ||
+ data->object_index == -1 ||
+ data->extra_index == -1) {
+ gint ii;
+
+ for (ii = 0; ii < ncols && (data->uid_index == -1 ||
+ data->object_index == -1 ||
+ data->extra_index == -1); ii++) {
+ const gchar *cname = column_names[ii];
+
+ if (!cname)
+ continue;
+
+ if (g_str_has_prefix (cname, "summary."))
+ cname += 8;
+
+ if (data->uid_index == -1 && g_ascii_strcasecmp (cname, E_CACHE_COLUMN_UID) == 0) {
+ data->uid_index = ii;
+ } else if (data->object_index == -1 && g_ascii_strcasecmp (cname,
E_CACHE_COLUMN_OBJECT) == 0) {
+ data->object_index = ii;
+ } else if (data->extra_index == -1 && g_ascii_strcasecmp (cname, EBC_COLUMN_EXTRA) ==
0) {
+ data->extra_index = ii;
+ }
+ }
+
+ if (data->object_index == -1)
+ data->object_index = -2;
+
+ if (data->extra_index == -1)
+ data->extra_index = -2;
+ }
+
+ g_return_val_if_fail (data->uid_index >= 0 && data->uid_index < ncols, FALSE);
+
+ if (data->object_index != -2) {
+ g_return_val_if_fail (data->object_index >= 0 && data->object_index < ncols, FALSE);
+ object = column_values[data->object_index];
+ }
+
+ if (data->extra_index != -2) {
+ g_return_val_if_fail (data->extra_index >= 0 && data->extra_index < ncols, FALSE);
+ extra = column_values[data->extra_index];
+ }
+
+ if (data->collect_results) {
+ EBookCacheSearchData *search_data;
+
+ search_data = e_book_cache_search_data_new (column_values[data->uid_index], object, extra);
+
+ data->results = g_slist_prepend (data->results, search_data);
+
+ data->last_vcard = search_data->vcard;
+ } else {
+ g_free (data->alloc_vcard);
+ data->alloc_vcard = g_strdup (object);
+
+ data->last_vcard = data->alloc_vcard;
+ }
+
+ data->n_results++;
+
+ return TRUE;
+}
+
+/**
+ * e_book_cache_cursor_step:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to use
+ * @flags: The #EBookCacheCursorStepFlags for this step
+ * @origin: The #EBookCacheCursorOrigin from whence to step
+ * @count: A positive or negative amount of contacts to try and fetch
+ * @out_results: (out) (nullable) (element-type EBookCacheSearchData) (transfer full):
+ * A return location to store the results, or %NULL if %E_BOOK_CACHE_CURSOR_STEP_FETCH is not specified in
@flags.
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Steps @cursor through its sorted query by a maximum of @count contacts
+ * starting from @origin.
+ *
+ * If @count is negative, then the cursor will move through the list in reverse.
+ *
+ * If @cursor reaches the beginning or end of the query results, then the
+ * returned list might not contain the amount of desired contacts, or might
+ * return no results if the cursor currently points to the last contact.
+ * Reaching the end of the list is not considered an error condition. Attempts
+ * to step beyond the end of the list after having reached the end of the list
+ * will however trigger an %E_CACHE_ERROR_END_OF_LIST error.
+ *
+ * If %E_BOOK_CACHE_CURSOR_STEP_FETCH is specified in @flags, a pointer to
+ * a %NULL #GSList pointer should be provided for the @out_results parameter.
+ *
+ * The result list will be stored to @out_results and should be freed
+ * with g_slist_free_full (results, (GDestroyNotify) e_book_cache_search_data_free);
+ * when no longer needed.
+ *
+ * Returns: The number of contacts traversed if successful, otherwise -1 is
+ * returned and the @error is set.
+ *
+ * Since: 3.26
+ **/
+gint
+e_book_cache_cursor_step (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ EBookCacheCursorStepFlags flags,
+ EBookCacheCursorOrigin origin,
+ gint count,
+ GSList **out_results,
+ GCancellable *cancellable,
+ GError **error)
+{
+ CursorCollectData data = { -1, -1, -1, NULL, NULL, NULL, FALSE, 0 };
+ CursorState *state;
+ GString *query;
+ gboolean success;
+ EBookCacheCursorOrigin try_position;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), -1);
+ g_return_val_if_fail (cursor != NULL, -1);
+ g_return_val_if_fail ((flags & E_BOOK_CACHE_CURSOR_STEP_FETCH) == 0 ||
+ (out_results != NULL), -1);
+
+ if (out_results)
+ *out_results = NULL;
+
+ e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return -1;
+ }
+
+ /* Check if this step should result in an end of list error first */
+ try_position = cursor->state.position;
+ if (origin != E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT)
+ try_position = origin;
+
+ /* Report errors for requests to run off the end of the list */
+ if (try_position == E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN && count < 0) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_END_OF_LIST,
+ _("Tried to step a cursor in reverse, "
+ "but cursor is already at the beginning of the contact list"));
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return -1;
+ } else if (try_position == E_BOOK_CACHE_CURSOR_ORIGIN_END && count > 0) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_END_OF_LIST,
+ _("Tried to step a cursor forwards, "
+ "but cursor is already at the end of the contact list"));
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return -1;
+ }
+
+ /* Nothing to do, silently return */
+ if (count == 0 && try_position == E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT) {
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return 0;
+ }
+
+ /* If we're not going to modify the position, just use
+ * a copy of the current cursor state.
+ */
+ if ((flags & E_BOOK_CACHE_CURSOR_STEP_MOVE) != 0)
+ state = &(cursor->state);
+ else
+ state = cursor_state_copy (cursor, &(cursor->state));
+
+ /* Every query starts with the STATE_CURRENT position, first
+ * fix up the cursor state according to 'origin'
+ */
+ switch (origin) {
+ case E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT:
+ /* Do nothing, normal operation */
+ break;
+
+ case E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN:
+ case E_BOOK_CACHE_CURSOR_ORIGIN_END:
+
+ /* Prepare the state before executing the query */
+ cursor_state_clear (cursor, state, origin);
+ break;
+ }
+
+ /* If count is 0 then there is no need to run any
+ * query, however it can be useful if you just want
+ * to move the cursor to the beginning or ending of
+ * the list.
+ */
+ if (count == 0) {
+ /* Free the state copy if need be */
+ if ((flags & E_BOOK_CACHE_CURSOR_STEP_MOVE) == 0)
+ cursor_state_free (cursor, state);
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return 0;
+ }
+
+ query = g_string_new (cursor->select_vcards);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Add the cursor constraints (if any) */
+ if (state->values[0] != NULL) {
+ gchar *constraints = NULL;
+
+ if (!cursor->query)
+ g_string_append (query, " WHERE ");
+ else
+ g_string_append (query, " AND ");
+
+ constraints = ebc_cursor_constraints (book_cache, cursor, state, count < 0, FALSE);
+
+ g_string_append_c (query, '(');
+ g_string_append (query, constraints);
+ g_string_append_c (query, ')');
+
+ g_free (constraints);
+ }
+
+ /* Add the sort order */
+ g_string_append_c (query, ' ');
+ if (count > 0)
+ g_string_append (query, cursor->order);
+ else
+ g_string_append (query, cursor->reverse_order);
+
+ /* Add the limit */
+ g_string_append_printf (query, " LIMIT %d", ABS (count));
+
+ /* Specify whether we really want results or not */
+ data.collect_results = (flags & E_BOOK_CACHE_CURSOR_STEP_FETCH) != 0;
+
+ /* Execute the query */
+ success = e_cache_sqlite_select (E_CACHE (book_cache), query->str,
+ ebc_collect_results_for_cursor_cb, &data,
+ cancellable, error);
+
+ /* Lock was obtained above */
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+ g_string_free (query, TRUE);
+
+ /* If there was no error, update the internal cursor state */
+ if (success) {
+ if (data.n_results < ABS (count)) {
+ /* We've reached the end, clear the current state */
+ if (count < 0)
+ cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+ else
+ cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_END);
+
+ } else if (data.last_vcard) {
+ /* Set the cursor state to the last result */
+ cursor_state_set_from_vcard (book_cache, cursor, state, data.last_vcard);
+ } else {
+ /* Should never get here */
+ g_warn_if_reached ();
+ }
+
+ /* Assign the results to return (if any) */
+ if (out_results) {
+ /* Correct the order of results at the last minute */
+ *out_results = g_slist_reverse (data.results);
+ data.results = NULL;
+ }
+ }
+
+ /* Cleanup what was allocated by collect_results_for_cursor_cb() */
+ if (data.results)
+ g_slist_free_full (data.results, (GDestroyNotify) e_book_cache_search_data_free);
+ g_free (data.alloc_vcard);
+
+ /* Free the copy state if we were working with a copy */
+ if ((flags & E_BOOK_CACHE_CURSOR_STEP_MOVE) == 0)
+ cursor_state_free (cursor, state);
+
+ if (success)
+ return data.n_results;
+
+ return -1;
+}
+
+/**
+ * e_book_cache_cursor_set_target_alphabetic_index:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to modify
+ * @idx: The alphabetic index
+ *
+ * Sets the @cursor position to an
+ * <link linkend="cursor-alphabet">Alphabetic Index</link>
+ * into the alphabet active in @book_cache's locale.
+ *
+ * After setting the target to an alphabetic index, for example the
+ * index for letter 'E', then further calls to e_book_cache_cursor_step()
+ * will return results starting with the letter 'E' (or results starting
+ * with the last result in 'D', if moving in a negative direction).
+ *
+ * The passed index must be a valid index in the active locale, knowledge
+ * on the currently active alphabet index must be obtained using #ECollator
+ * APIs.
+ *
+ * Use e_book_cahce_ref_collator() to obtain the active collator for @book_cache.
+ *
+ * Since: 3.26
+ **/
+void
+e_book_cache_cursor_set_target_alphabetic_index (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint idx)
+{
+ gint n_labels = 0;
+
+ g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+ g_return_if_fail (cursor != NULL);
+ g_return_if_fail (idx >= 0);
+
+ e_collator_get_index_labels (book_cache->priv->collator, &n_labels, NULL, NULL, NULL);
+ g_return_if_fail (idx < n_labels);
+
+ cursor_state_clear (cursor, &(cursor->state), E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT);
+ if (cursor->n_sort_fields > 0) {
+ SummaryField *field;
+ gchar *index_key;
+
+ index_key = e_collator_generate_key_for_index (book_cache->priv->collator, idx);
+ field = summary_field_get (book_cache, cursor->sort_fields[0]);
+
+ if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ cursor->state.values[0] = index_key;
+ } else {
+ cursor->state.values[0] = ebc_encode_vcard_sort_key (index_key);
+ g_free (index_key);
+ }
+ }
+}
+
+/**
+ * e_book_cache_cursor_set_sexp:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to modify
+ * @sexp: The new query expression for @cursor
+ * @error: return location for a #GError, or %NULL
+ *
+ * Modifies the current query expression for @cursor. This will not
+ * modify @cursor's state, but will change the outcome of any further
+ * calls to e_book_cache_cursor_step() or e_book_cache_cursor_calculate().
+ *
+ * Returns: %TRUE if the expression was valid and accepted by @cursor
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_cursor_set_sexp (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ const gchar *sexp,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (cursor != NULL, FALSE);
+
+ /* We don't like '\0' sexps, prefer NULL */
+ if (sexp && !*sexp)
+ sexp = NULL;
+
+ e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+ success = ebc_cursor_setup_query (book_cache, cursor, sexp, error);
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+ return success;
+}
+
+/**
+ * e_book_cache_cursor_calculate:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor
+ * @out_total: (out) (nullable): A return location to store the total result set for this cursor
+ * @out_position: (out) (nullable): A return location to store the cursor position
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Calculates the @out_total amount of results for the @cursor's query expression,
+ * as well as the current @out_position of @cursor in the results. The @out_position is
+ * represented as the amount of results which lead up to the current value
+ * of @cursor, if @cursor currently points to an exact contact, the position
+ * also includes the cursor contact.
+ *
+ * Returns: Whether @out_total and @out_position were successfully calculated.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_cursor_calculate (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint *out_total,
+ gint *out_position,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success = TRUE;
+ gint local_total = 0;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (cursor != NULL, FALSE);
+
+ /* If we're in a clear cursor state, then the position is 0 */
+ if (out_position && cursor->state.values[0] == NULL) {
+ if (cursor->state.position == E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN) {
+ /* Mark the local pointer NULL, no need to calculate this anymore */
+ *out_position = 0;
+ out_position = NULL;
+ } else if (cursor->state.position == E_BOOK_CACHE_CURSOR_ORIGIN_END) {
+ /* Make sure that we look up the total so we can
+ * set the position to 'total + 1'
+ */
+ if (!out_total)
+ out_total = &local_total;
+ }
+ }
+
+ /* Early return if there is nothing to do */
+ if (!out_total && !out_position)
+ return TRUE;
+
+ e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return FALSE;
+ }
+
+ if (out_total)
+ success = cursor_count_total_locked (book_cache, cursor, out_total, cancellable, error);
+
+ if (success && out_position)
+ success = cursor_count_position_locked (book_cache, cursor, out_position, cancellable, error);
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+ /* In the case we're at the end, we just set the position
+ * to be the total + 1
+ */
+ if (success && out_position && out_total &&
+ cursor->state.position == E_BOOK_CACHE_CURSOR_ORIGIN_END)
+ *out_position = *out_total + 1;
+
+ return success;
+}
+
+/**
+ * e_book_cache_cursor_compare_contact:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor
+ * @contact: The #EContact to compare
+ * @out_matches_sexp: (out) (nullable): Whether the contact matches the cursor's search expression
+ *
+ * Compares @contact with @cursor and returns whether @contact is less than, equal to, or greater
+ * than @cursor.
+ *
+ * Returns: A value that is less than, equal to, or greater than zero if @contact is found,
+ * respectively, to be less than, to match, or be greater than the current value of @cursor.
+ *
+ * Since: 3.26
+ **/
+gint
+e_book_cache_cursor_compare_contact (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ EContact *contact,
+ gboolean *out_matches_sexp)
+{
+ gint ii;
+ gint comparison = 0;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), -1);
+ g_return_val_if_fail (cursor != NULL, -1);
+ g_return_val_if_fail (E_IS_CONTACT (contact), -1);
+
+ if (out_matches_sexp) {
+ if (!cursor->sexp)
+ *out_matches_sexp = TRUE;
+ else
+ *out_matches_sexp = e_book_backend_sexp_match_contact (cursor->sexp, contact);
+ }
+
+ for (ii = 0; ii < cursor->n_sort_fields && comparison == 0; ii++) {
+ SummaryField *field;
+ gchar *contact_key = NULL;
+ const gchar *cursor_key = NULL;
+ const gchar *field_value;
+ gchar *freeme = NULL;
+
+ field_value = e_contact_get_const (contact, cursor->sort_fields[ii]);
+ if (field_value)
+ contact_key = e_collator_generate_key (book_cache->priv->collator, field_value, NULL);
+
+ field = summary_field_get (book_cache, cursor->sort_fields[ii]);
+
+ if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ cursor_key = cursor->state.values[ii];
+ } else {
+
+ if (cursor->state.values[ii])
+ freeme = ebc_decode_vcard_sort_key (cursor->state.values[ii]);
+
+ cursor_key = freeme;
+ }
+
+ /* Empty state sorts below any contact value, which means the contact sorts above cursor */
+ if (cursor_key == NULL)
+ comparison = 1;
+ else
+ /* Check if contact sorts below, equal to, or above the cursor */
+ comparison = g_strcmp0 (contact_key, cursor_key);
+
+ g_free (contact_key);
+ g_free (freeme);
+ }
+
+ /* UID tie-breaker */
+ if (comparison == 0) {
+ const gchar *uid;
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+
+ if (cursor->state.last_uid == NULL)
+ comparison = 1;
+ else if (uid == NULL)
+ comparison = -1;
+ else
+ comparison = strcmp (uid, cursor->state.last_uid);
+ }
+
+ return comparison;
+}
+
+static gboolean
+e_book_cache_put_locked (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ GHashTable *other_columns,
+ EOfflineState offline_state,
+ gboolean is_replace,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (E_CACHE_CLASS (e_book_cache_parent_class)->put_locked != NULL, FALSE);
+
+ success = E_CACHE_CLASS (e_book_cache_parent_class)->put_locked (cache, uid, revision, object,
other_columns, offline_state,
+ is_replace, cancellable, error);
+
+ success = success && ebc_update_aux_tables (cache, uid, revision, object, cancellable, error);
+
+ return success;
+}
+
+static gboolean
+e_book_cache_remove_locked (ECache *cache,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (E_CACHE_CLASS (e_book_cache_parent_class)->remove_locked != NULL, FALSE);
+
+ success = ebc_delete_from_aux_tables (cache, uid, cancellable, error);
+
+ success = success && E_CACHE_CLASS (e_book_cache_parent_class)->remove_locked (cache, uid,
cancellable, error);
+
+ return success;
+}
+
+static gboolean
+e_book_cache_remove_all_locked (ECache *cache,
+ const GSList *uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (E_CACHE_CLASS (e_book_cache_parent_class)->remove_all_locked != NULL, FALSE);
+
+ success = ebc_empty_aux_tables (cache, cancellable, error);
+
+ success = success && E_CACHE_CLASS (e_book_cache_parent_class)->remove_all_locked (cache, uids,
cancellable, error);
+
+ return success;
+}
+
+static void
+e_book_cache_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_LOCALE:
+ g_value_take_string (
+ value,
+ e_book_cache_dup_locale (E_BOOK_CACHE (object)));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_book_cache_finalize (GObject *object)
+{
+ EBookCache *book_cache = E_BOOK_CACHE (object);
+
+ g_clear_object (&book_cache->priv->source);
+
+ if (book_cache->priv->collator) {
+ e_collator_unref (book_cache->priv->collator);
+ book_cache->priv->collator = NULL;
+ }
+
+ g_free (book_cache->priv->locale);
+ g_free (book_cache->priv->region_code);
+ g_free (book_cache->priv->summary_fields);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_cache_parent_class)->finalize (object);
+}
+
+static void
+e_book_cache_class_init (EBookCacheClass *class)
+{
+ GObjectClass *object_class;
+ ECacheClass *cache_class;
+
+ g_type_class_add_private (class, sizeof (EBookCachePrivate));
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->get_property = e_book_cache_get_property;
+ object_class->finalize = e_book_cache_finalize;
+
+ cache_class = E_CACHE_CLASS (class);
+ cache_class->put_locked = e_book_cache_put_locked;
+ cache_class->remove_locked = e_book_cache_remove_locked;
+ cache_class->remove_all_locked = e_book_cache_remove_all_locked;
+
+ g_object_class_install_property (
+ object_class,
+ PROP_LOCALE,
+ g_param_spec_string (
+ "locale",
+ "Locate",
+ "The locale currently being used",
+ NULL,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+e_book_cache_init (EBookCache *book_cache)
+{
+ book_cache->priv = G_TYPE_INSTANCE_GET_PRIVATE (book_cache, E_TYPE_BOOK_CACHE, EBookCachePrivate);
+}
diff --git a/src/addressbook/libedata-book/e-book-cache.h b/src/addressbook/libedata-book/e-book-cache.h
new file mode 100644
index 0000000..c8ad69c
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-cache.h
@@ -0,0 +1,276 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ *
+ * 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/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_CACHE_H
+#define E_BOOK_CACHE_H
+
+#include <libebackend/libebackend.h>
+#include <libebook-contacts/libebook-contacts.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_CACHE \
+ (e_book_cache_get_type ())
+#define E_BOOK_CACHE(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_BOOK_CACHE, EBookCache))
+#define E_BOOK_CACHE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_BOOK_CACHE, EBookCacheClass))
+#define E_IS_BOOK_CACHE(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_BOOK_CACHE))
+#define E_IS_BOOK_CACHE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_BOOK_CACHE))
+#define E_BOOK_CACHE_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_BOOK_CACHE, EBookCacheClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EBookCache EBookCache;
+typedef struct _EBookCacheClass EBookCacheClass;
+typedef struct _EBookCachePrivate EBookCachePrivate;
+
+/**
+ * EBookCacheSearchData:
+ * @uid: The %E_CONTACT_UID field of this contact
+ * @vcard: The vcard string
+ * @extra: Any extra data associated with the vcard
+ *
+ * This structure is used to represent contacts returned
+ * by the #EBookCache from various functions
+ * such as e_book_cache_search().
+ *
+ * The @extra parameter will contain any data which was
+ * previously passed for this contact in e_book_cache_add_contact().
+ *
+ * These should be freed with e_book_cache_search_data_free().
+ *
+ * Since: 3.26
+ **/
+typedef struct {
+ gchar *uid;
+ gchar *vcard;
+ gchar *extra;
+} EBookCacheSearchData;
+
+#define E_TYPE_BOOK_CACHE_SEARCH_DATA (e_book_cache_search_data_get_type ())
+
+GType e_book_cache_search_data_get_type
+ (void) G_GNUC_CONST;
+EBookCacheSearchData *
+ e_book_cache_search_data_new (const gchar *uid,
+ const gchar *vcard,
+ const gchar *extra);
+EBookCacheSearchData *
+ e_book_cache_search_data_copy (const EBookCacheSearchData *data);
+void e_book_cache_search_data_free (EBookCacheSearchData *data);
+
+/**
+ * EBookCache:
+ *
+ * Contains only private data that should be read and manipulated using
+ * the functions below.
+ *
+ * Since: 3.26
+ **/
+struct _EBookCache {
+ /*< private >*/
+ ECache parent;
+ EBookCachePrivate *priv;
+};
+
+/**
+ * EBookCacheClass:
+ *
+ * Class structure for the #EBookCache class.
+ *
+ * Since: 3.26
+ */
+struct _EBookCacheClass {
+ /*< private >*/
+ ECacheClass parent_class;
+
+ /* Padding for future expansion */
+ gpointer reserved[10];
+};
+
+/**
+ * EBookCacheCursor:
+ *
+ * An opaque cursor pointer
+ *
+ * Since: 3.26
+ */
+typedef struct _EBookCacheCursor EBookCacheCursor;
+
+/**
+ * EBookCacheCursorOrigin:
+ * @E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT: The current cursor position.
+ * @E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN: The beginning of the cursor results.
+ * @E_BOOK_CACHE_CURSOR_ORIGIN_END: The end of the cursor results.
+ *
+ * Specifies the start position to in the list of traversed contacts
+ * in calls to e_book_cache_cursor_step().
+ *
+ * When an #EBookCacheCursor is created, the current position implied by %E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT
+ * is the same as %E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN.
+ *
+ * Since: 3.26
+ */
+typedef enum {
+ E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT = 0,
+ E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+ E_BOOK_CACHE_CURSOR_ORIGIN_END
+} EBookCacheCursorOrigin;
+
+/**
+ * EBookCacheCursorStepFlags:
+ * @E_BOOK_CACHE_CURSOR_STEP_MOVE: The cursor position should be modified while stepping.
+ * @E_BOOK_CACHE_CURSOR_STEP_FETCH: Traversed contacts should be listed and returned while stepping.
+ *
+ * Defines the behaviour of e_book_cache_cursor_step().
+ *
+ * Since: 3.26
+ */
+typedef enum {
+ E_BOOK_CACHE_CURSOR_STEP_MOVE = (1 << 0),
+ E_BOOK_CACHE_CURSOR_STEP_FETCH = (1 << 1)
+} EBookCacheCursorStepFlags;
+
+GType e_book_cache_get_type (void) G_GNUC_CONST;
+
+EBookCache * e_book_cache_new (const gchar *filename,
+ ESource *source,
+ GCancellable *cancellable,
+ GError **error);
+EBookCache * e_book_cache_new_full (const gchar *filename,
+ ESource *source,
+ ESourceBackendSummarySetup *setup,
+ GCancellable *cancellable,
+ GError **error);
+ESource * e_book_cache_ref_source (EBookCache *book_cache);
+gboolean e_book_cache_set_locale (EBookCache *book_cache,
+ const gchar *lc_collate,
+ GCancellable *cancellable,
+ GError **error);
+gchar * e_book_cache_dup_locale (EBookCache *book_cache);
+
+ECollator * e_book_cache_ref_collator (EBookCache *book_cache);
+
+/* Adding / Removing / Searching contacts */
+gboolean e_book_cache_put_contact (EBookCache *book_cache,
+ EContact *contact,
+ const gchar *extra,
+ gboolean is_offline,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_put_contacts (EBookCache *book_cache,
+ const GSList *contacts,
+ const GSList *extras,
+ gboolean is_offline,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_remove_contact (EBookCache *book_cache,
+ const gchar *uid,
+ gboolean is_offline,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_remove_contacts (EBookCache *book_cache,
+ const GSList *uids,
+ gboolean is_offline,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_get_contact (EBookCache *book_cache,
+ const gchar *uid,
+ gboolean meta_contact,
+ EContact **out_contact,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_get_vcard (EBookCache *book_cache,
+ const gchar *uid,
+ gboolean meta_contact,
+ gchar **out_vcard,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_set_contact_extra (EBookCache *book_cache,
+ const gchar *uid,
+ const gchar *extra,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_get_contact_extra (EBookCache *book_cache,
+ const gchar *uid,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_search (EBookCache *book_cache,
+ const gchar *sexp,
+ gboolean meta_contacts,
+ GSList **out_list,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_search_uids (EBookCache *book_cache,
+ const gchar *sexp,
+ GSList **out_list,
+ GCancellable *cancellable,
+ GError **error);
+/* Cursor API */
+EBookCacheCursor *
+ e_book_cache_cursor_new (EBookCache *book_cache,
+ const gchar *sexp,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ GError **error);
+void e_book_cache_cursor_free (EBookCache *book_cache,
+ EBookCacheCursor *cursor);
+gint e_book_cache_cursor_step (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ EBookCacheCursorStepFlags flags,
+ EBookCacheCursorOrigin origin,
+ gint count,
+ GSList **out_results,
+ GCancellable *cancellable,
+ GError **error);
+void e_book_cache_cursor_set_target_alphabetic_index
+ (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint idx);
+gboolean e_book_cache_cursor_set_sexp (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ const gchar *sexp,
+ GError **error);
+gboolean e_book_cache_cursor_calculate (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint *out_total,
+ gint *out_position,
+ GCancellable *cancellable,
+ GError **error);
+gint e_book_cache_cursor_compare_contact
+ (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ EContact *contact,
+ gboolean *out_matches_sexp);
+
+#endif /* E_BOOK_CACHE_H */
diff --git a/src/addressbook/libedata-book/libedata-book.h b/src/addressbook/libedata-book/libedata-book.h
index ff070ec..af797cb 100644
--- a/src/addressbook/libedata-book/libedata-book.h
+++ b/src/addressbook/libedata-book/libedata-book.h
@@ -29,6 +29,7 @@
#include <libedata-book/e-book-backend-sqlitedb.h>
#include <libedata-book/e-book-backend-summary.h>
#include <libedata-book/e-book-backend.h>
+#include <libedata-book/e-book-cache.h>
#include <libedata-book/e-book-sqlite.h>
#include <libedata-book/e-data-book-cursor.h>
#include <libedata-book/e-data-book-cursor-sqlite.h>
diff --git a/src/libebackend/e-cache.c b/src/libebackend/e-cache.c
index 307901c..7fe3762 100644
--- a/src/libebackend/e-cache.c
+++ b/src/libebackend/e-cache.c
@@ -15,6 +15,20 @@
* along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
+/**
+ * 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 <errno.h>
@@ -39,6 +53,9 @@
*/
#define E_CACHE_CANCEL_BATCH_SIZE 200
+/* How many rows to read when e_cache_foreach_update() */
+#define E_CACHE_UPDATE_BATCH_SIZE 200
+
struct _ECachePrivate {
gchar *filename;
sqlite3 *db;
@@ -201,7 +218,7 @@ e_cache_column_info_free (gpointer info)
}
}
-#define E_CACHE_SET_ERROR_FROM_SQLITE(error, code, message) \
+#define E_CACHE_SET_ERROR_FROM_SQLITE(error, code, message, stmt) \
G_STMT_START { \
if (code == SQLITE_CONSTRAINT) { \
g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_CONSTRAINT, message); \
@@ -209,7 +226,7 @@ e_cache_column_info_free (gpointer info)
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); \
+ "SQLite error code '%d': %s (statement:%s)", code, message, stmt); \
} \
} G_STMT_END
@@ -285,7 +302,7 @@ e_cache_sqlite_exec_internal (ECache *cache,
g_rec_mutex_unlock (&cache->priv->lock);
if (ret != SQLITE_OK) {
- E_CACHE_SET_ERROR_FROM_SQLITE (error, ret, errmsg);
+ E_CACHE_SET_ERROR_FROM_SQLITE (error, ret, errmsg, stmt);
sqlite3_free (errmsg);
return FALSE;
}
@@ -332,7 +349,7 @@ e_cache_read_key_value (ECache *cache,
{
gchar **pvalue = user_data;
- g_return_val_if_fail (ncols != 1, FALSE);
+ 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);
@@ -512,7 +529,7 @@ e_cache_init_tables (ECache *cache,
if (!e_cache_sqlite_exec_internal (cache,
"CREATE TABLE IF NOT EXISTS " E_CACHE_TABLE_KEYS " ("
- "key TEXT PRIMARY INDEX,"
+ "key TEXT PRIMARY KEY,"
"value TEXT)",
NULL, NULL, cancellable, error)) {
return FALSE;
@@ -521,7 +538,7 @@ e_cache_init_tables (ECache *cache,
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_UID " TEXT PRIMARY KEY,"
E_CACHE_COLUMN_REVISION " TEXT,"
E_CACHE_COLUMN_OBJECT " TEXT,"
E_CACHE_COLUMN_STATE " INTEGER");
@@ -967,15 +984,15 @@ e_cache_get (ECache *cache,
}
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)
+e_cache_put_locked (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ GHashTable *other_columns,
+ EOfflineState offline_state,
+ gboolean is_replace,
+ GCancellable *cancellable,
+ GError **error)
{
GHashTable *my_other_columns = NULL;
gboolean success = TRUE;
@@ -1053,7 +1070,7 @@ e_cache_put (ECache *cache,
is_replace = e_cache_contains_internal (cache, uid, FALSE);
- success = e_cache_put_with_offline_state (cache, uid, revision, object, other_columns,
+ success = e_cache_put_locked (cache, uid, revision, object, other_columns,
E_OFFLINE_STATE_SYNCED, is_replace, cancellable, error);
g_rec_mutex_unlock (&cache->priv->lock);
@@ -1082,21 +1099,21 @@ e_cache_remove (ECache *cache,
GCancellable *cancellable,
GError **error)
{
- gboolean success = TRUE;
+ ECacheClass *klass;
+ gboolean success;
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);
+ klass = E_CACHE_GET_CLASS (cache);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->remove_locked != NULL, FALSE);
- 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);
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ success = klass->remove_locked (cache, uid, cancellable, error);
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
return success;
}
@@ -1118,35 +1135,27 @@ e_cache_remove_all (ECache *cache,
GCancellable *cancellable,
GError **error)
{
- GSList *uids = NULL, *link;
+ ECacheClass *klass;
+ GSList *uids = NULL;
gboolean success;
g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
- g_rec_mutex_lock (&cache->priv->lock);
+ 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);
- success = e_cache_get_uids (cache, TRUE, &uids, NULL, cancellable, error);
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
- 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);
- }
+ success = e_cache_get_uids (cache, TRUE, &uids, NULL, cancellable, error);
- if (success) {
- success = e_cache_sqlite_exec_printf (cache,
- "DELETE FROM " E_CACHE_TABLE_OBJECTS,
- NULL, 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);
- g_rec_mutex_unlock (&cache->priv->lock);
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
g_slist_free_full (uids, g_free);
@@ -1478,6 +1487,271 @@ e_cache_foreach (ECache *cache,
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_free (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_new0 (struct ForeachUpdateRowData, 1);
+ 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 = ncols;
+ rd->column_values = cvalues;
+
+ if (cnames)
+ fu->column_names = cnames;
+
+ fu->rows = g_slist_prepend (fu->rows, rd);
+
+ g_return_val_if_fail ((gint) fu->column_names->len != ncols, FALSE);
+
+ return TRUE;
+}
+
+/**
+ * e_cache_foreach_update:
+ * @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 #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
+ * @include_deleted 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.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_foreach_update (ECache *cache,
+ gboolean include_deleted,
+ 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 (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 (!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;
+ GHashTable *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 && g_hash_table_size (new_other_columns) >
0))) {
+ 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);
+ if (new_other_columns)
+ g_hash_table_unref (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_put_offline:
* @cache: an #ECache
@@ -1501,7 +1775,7 @@ e_cache_put_offline (ECache *cache,
const gchar *uid,
const gchar *revision,
const gchar *object,
- const GHashTable *other_columns,
+ GHashTable *other_columns,
GCancellable *cancellable,
GError **error)
{
@@ -1531,7 +1805,7 @@ e_cache_put_offline (ECache *cache,
offline_state = E_OFFLINE_STATE_LOCALLY_CREATED;
}
- success = success && e_cache_put_with_offline_state (cache, uid, revision, object, other_columns,
+ success = success && e_cache_put_locked (cache, uid, revision, object, other_columns,
offline_state, is_replace, cancellable, error);
g_rec_mutex_unlock (&cache->priv->lock);
@@ -2034,11 +2308,43 @@ e_cache_sqlite_select (ECache *cache,
}
/**
+ * 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);
+
+ g_free (tmp_stmt);
+}
+
+/**
* 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.
+ * 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
@@ -2127,14 +2433,13 @@ e_cache_put_locked_default (ECache *cache,
const gchar *uid,
const gchar *revision,
const gchar *object,
- const GHashTable *other_columns,
+ 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);
@@ -2142,15 +2447,14 @@ e_cache_put_locked_default (ECache *cache,
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 ("
+ 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);
-
- statement = g_string_new (tmp);
-
- e_cache_sqlite_stmt_free (tmp);
+ E_CACHE_COLUMN_STATE,
+ E_CACHE_TABLE_OBJECTS);
if (other_columns) {
GHashTableIter iter;
@@ -2162,18 +2466,14 @@ e_cache_put_locked_default (ECache *cache,
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);
+ e_cache_sqlite_stmt_append_printf (other_names, "%Q", key);
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);
+ e_cache_sqlite_stmt_append_printf (other_values, "%Q", value);
} else {
g_string_append (other_values, "NULL");
}
@@ -2185,9 +2485,7 @@ e_cache_put_locked_default (ECache *cache,
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);
+ e_cache_sqlite_stmt_append_printf (statement, "%Q,%Q,%Q,%d", uid, revision, object, offline_state);
if (other_values)
g_string_append (statement, other_values->str);
@@ -2206,6 +2504,61 @@ e_cache_put_locked_default (ECache *cache,
}
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_signals_accumulator (GSignalInvocationHint *ihint,
GValue *return_accu,
const GValue *handler_return,
@@ -2274,6 +2627,8 @@ e_cache_class_init (ECacheClass *class)
object_class->finalize = e_cache_finalize;
class->put_locked = e_cache_put_locked_default;
+ class->remove_locked = e_cache_remove_locked_default;
+ class->remove_all_locked = e_cache_remove_all_locked_default;
class->before_put = e_cache_before_put_default;
class->before_remove = e_cache_before_remove_default;
@@ -2292,7 +2647,7 @@ e_cache_class_init (ECacheClass *class)
G_TYPE_HASH_TABLE,
G_TYPE_BOOLEAN,
G_TYPE_CANCELLABLE,
- G_TYPE_ERROR);
+ G_TYPE_POINTER);
signals[BEFORE_REMOVE] = g_signal_new (
"before-remove",
@@ -2305,7 +2660,7 @@ e_cache_class_init (ECacheClass *class)
G_TYPE_BOOLEAN, 3,
G_TYPE_STRING,
G_TYPE_CANCELLABLE,
- G_TYPE_ERROR);
+ G_TYPE_POINTER);
e_sqlite3_vfs_init ();
}
diff --git a/src/libebackend/e-cache.h b/src/libebackend/e-cache.h
index 54039a3..57f91a6 100644
--- a/src/libebackend/e-cache.h
+++ b/src/libebackend/e-cache.h
@@ -47,14 +47,14 @@
G_BEGIN_DECLS
+#define E_CACHE_TABLE_OBJECTS "ECacheObjects"
+#define E_CACHE_TABLE_KEYS "ECacheKeys"
+
#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:
*
@@ -74,6 +74,7 @@ GQuark e_cache_error_quark (void);
* @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_FIELD: A field requested for inclusion in summary is not supported.
* @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.
@@ -87,6 +88,7 @@ typedef enum {
E_CACHE_ERROR_CONSTRAINT,
E_CACHE_ERROR_NOT_FOUND,
E_CACHE_ERROR_INVALID_QUERY,
+ E_CACHE_ERROR_UNSUPPORTED_FIELD,
E_CACHE_ERROR_UNSUPPORTED_QUERY,
E_CACHE_ERROR_END_OF_LIST,
E_CACHE_ERROR_LOAD
@@ -186,6 +188,43 @@ typedef gboolean (* ECacheForeachFunc) (ECache *cache,
gpointer user_data);
/**
+ * ECacheUpdateFunc:
+ * @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
+ * @out_revision: (out): the new object revision to set; keep it untouched to not change
+ * @out_object: (out): the new object to set; keep it untouched to not change
+ * @out_offline_state: (out): the offline state to set; the default is the same as @offline_state
+ * @out_other_columns: (out) (element-type utf8 utf8) (transfer full): other columns to set; keep it
untouched to not change any
+ * @user_data: user data, as used in e_cache_foreach_update()
+ *
+ * A callback called for each object row when using e_cache_foreach_update() function.
+ * When all out parameters are left untouched, then the row is not changed.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* ECacheUpdateFunc) (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ EOfflineState offline_state,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gchar **out_revision,
+ gchar **out_object,
+ EOfflineState *out_offline_state,
+ GHashTable **out_other_columns,
+ gpointer user_data);
+
+/**
* ECacheSelectFunc:
* @cache: an #ECache
* @ncols: count of columns, items in column_names and column_values
@@ -212,7 +251,7 @@ typedef gboolean (* ECacheSelectFunc) (ECache *cache,
* Contains only private data that should be read and manipulated using the
* functions below.
*
- * Since: 3.24
+ * Since: 3.26
**/
struct _ECache {
/*< private >*/
@@ -228,11 +267,19 @@ struct _ECacheClass {
const gchar *uid,
const gchar *revision,
const gchar *object,
- const GHashTable *other_columns,
+ GHashTable *other_columns,
EOfflineState offline_state,
gboolean is_replace,
GCancellable *cancellable,
GError **error);
+ gboolean (* remove_locked) (ECache *cache,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* remove_all_locked) (ECache *cache,
+ const GSList *uids,
+ GCancellable *cancellable,
+ GError **error);
void (* erase) (ECache *cache);
/* Signals */
@@ -249,6 +296,7 @@ struct _ECacheClass {
GCancellable *cancellable,
GError **error);
+ /* Padding for future expansion */
gpointer reserved[10];
};
@@ -312,13 +360,20 @@ gboolean e_cache_foreach (ECache *cache,
gpointer user_data,
GCancellable *cancellable,
GError **error);
+gboolean e_cache_foreach_update (ECache *cache,
+ gboolean include_deleted,
+ const gchar *where_clause,
+ ECacheUpdateFunc 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,
+ GHashTable *other_columns,
GCancellable *cancellable,
GError **error);
gboolean e_cache_remove_offline (ECache *cache,
@@ -379,6 +434,10 @@ gboolean e_cache_sqlite_maybe_vacuum (ECache *cache,
GCancellable *cancellable,
GError **error);
+void e_cache_sqlite_stmt_append_printf
+ (GString *stmt,
+ const gchar *format,
+ ...);
gchar * e_cache_sqlite_stmt_printf (const gchar *format,
...);
void e_cache_sqlite_stmt_free (gchar *stmt);
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]