[evolution-data-server/cursor-staging: 4/17] EBookBackendSqliteDB: Adding cursor related APIs



commit ae0e3a92ae83e9238169a5c80e9fa9ce12efec34
Author: Tristan Van Berkom <tristanvb openismus com>
Date:   Mon Apr 22 20:42:14 2013 +0900

    EBookBackendSqliteDB: Adding cursor related APIs
    
    Added the following APIs:
    
          o e_book_backend_sqlitedb_cursor_new()
    
            Creates a cursor for a given query expression and sort order
    
          o e_book_backend_sqlitedb_cursor_free()
    
            Frees a cursor and it's resources
    
          o e_book_backend_sqlitedb_cursor_move_by()
    
            Moves the cursor and fetches results
    
            Moving the cursor now has 3 possible "origins":
    
              o EBSDB_CURSOR_ORIGIN_CURRENT:
                Fetch results and move from the current
                cursor position
    
              o EBSDB_CURSOR_ORIGIN_PREVIOUS:
                Fetch results and move from the previous
                cursor position, practical for refreshing
                a result set after the addressbook changes
    
              o EBSDB_CURSOR_ORIGIN_RESET:
                Fetch results from the beginning (or end,
                if moving backwards through results).
    
            To achieve the EBSDB_CURSOR_ORIGIN_PREVIOUS origin, the cursor
            holds on to two cursor states at all times (the current cursor
            state and previous cursor state).
    
          o e_book_backend_sqlitedb_cursor_set_target_alphabetic_index()
    
            To set the cursor target by alphabetic index, also added
            e_book_backend_sqlitedb_ref_collator() to get a hold of the active
            collator which can be used to list the active alphabet attributes.
    
          o e_book_backend_sqlite_cursor_calculate()
    
            Calculates the position / total values of a cursor.
    
          o e_book_backend_sqlitedb_cursor_set_sexp()
    
            Sets the search expression for a given cursor
    
          o e_book_backend_sqlitedb_get/set_locale()
    
            Locale setting is now only ever guessed when creating a new addressbook
            before e_book_backend_sqlitedb_set_locale() is called, setting the locale
            will save the localization setting and it will be reused on subsequent
            accesses. If and when a locale setting is changed, the contact sort
            keys are regenerated with a new ECollator for the new locale.
    
          o e_book_backend_sqlitedb_cursor_compare()
    
            An api to compare the cursor with an EContact, this allows
            EDataBookCursor to track total / position when the addressbook is
            modified without constantly recalculating it with SQLite queries.

 .../libebook-contacts/e-book-contacts-types.h      |   12 +
 .../libedata-book/e-book-backend-sqlitedb.c        | 1578 ++++++++++++++++++--
 .../libedata-book/e-book-backend-sqlitedb.h        |   80 +
 3 files changed, 1562 insertions(+), 108 deletions(-)
---
diff --git a/addressbook/libebook-contacts/e-book-contacts-types.h 
b/addressbook/libebook-contacts/e-book-contacts-types.h
index 7208b67..4468781 100644
--- a/addressbook/libebook-contacts/e-book-contacts-types.h
+++ b/addressbook/libebook-contacts/e-book-contacts-types.h
@@ -135,6 +135,18 @@ typedef enum {
        E_BOOK_INDEX_PHONE
 } EBookIndexType;
 
+/**
+ * EBookSortType:
+ * @E_BOOK_SORT_ASCENDING: Sort results in ascending order
+ * @E_BOOK_SORT_DESCENDING: Sort results in descending order
+ *
+ * Specifies the sort order of an ordered query
+ */
+typedef enum {
+       E_BOOK_SORT_ASCENDING = 0,
+       E_BOOK_SORT_DESCENDING
+} EBookSortType;
+
 GQuark         e_book_client_error_quark       (void) G_GNUC_CONST;
 const gchar *  e_book_client_error_to_string   (EBookClientError code);
 
diff --git a/addressbook/libedata-book/e-book-backend-sqlitedb.c 
b/addressbook/libedata-book/e-book-backend-sqlitedb.c
index 73a67a1..15f15c9 100644
--- a/addressbook/libedata-book/e-book-backend-sqlitedb.c
+++ b/addressbook/libedata-book/e-book-backend-sqlitedb.c
@@ -59,7 +59,7 @@
 #endif
 
 #define DB_FILENAME "contacts.db"
-#define FOLDER_VERSION 6
+#define FOLDER_VERSION 7
 
 typedef enum {
        INDEX_PREFIX = (1 << 0),
@@ -89,6 +89,9 @@ struct _EBookBackendSqliteDBPrivate {
        gint            n_summary_fields;
        guint           have_attr_list : 1;
        IndexFlags      attr_list_indexes;
+
+       ECollator      *collator; /* The ECollator to create sort keys for all fields */
+       gchar          *locale;   /* The current locale */
 };
 
 G_DEFINE_TYPE (EBookBackendSqliteDB, e_book_backend_sqlitedb, G_TYPE_OBJECT)
@@ -123,14 +126,25 @@ static EBookIndexType default_index_types[] = {
        E_BOOK_INDEX_PREFIX
 };
 
+static void
+destroy_search_data (gpointer data)
+{
+       e_book_backend_sqlitedb_search_data_free (data);
+}
+
 static SummaryField * append_summary_field (GArray         *array,
                                            EContactField   field,
                                            gboolean       *have_attr_list,
                                            GError        **error);
 
-static gboolean upgrade_contacts_table (EBookBackendSqliteDB  *ebsdb,
-                                        const gchar           *folderid,
-                                        GError               **error);
+static gboolean upgrade_contacts_table (EBookBackendSqliteDB *ebsdb,
+                                       const gchar          *folderid,
+                                       const gchar          *region,
+                                       const gchar          *lc_collate,
+                                       GError              **error);
+static gboolean sqlitedb_set_locale_internal (EBookBackendSqliteDB *ebsdb,
+                                             const gchar          *locale,
+                                             GError              **error);
 
 static const gchar *
 summary_dbname_from_field (EBookBackendSqliteDB *ebsdb,
@@ -147,13 +161,10 @@ summary_dbname_from_field (EBookBackendSqliteDB *ebsdb,
 }
 
 static gint
-summary_index_from_field_name (EBookBackendSqliteDB *ebsdb,
-                               const gchar *field_name)
+summary_index_from_field (EBookBackendSqliteDB *ebsdb,
+                         EContactField         field)
 {
        gint i;
-       EContactField field;
-
-       field = e_contact_field_id (field_name);
 
        for (i = 0; i < ebsdb->priv->n_summary_fields; i++) {
                if (ebsdb->priv->summary_fields[i].field == field)
@@ -163,6 +174,17 @@ summary_index_from_field_name (EBookBackendSqliteDB *ebsdb,
        return -1;
 }
 
+static gint
+summary_index_from_field_name (EBookBackendSqliteDB *ebsdb,
+                               const gchar *field_name)
+{
+       EContactField field;
+
+       field = e_contact_field_id (field_name);
+
+       return summary_index_from_field (ebsdb, field);
+}
+
 typedef struct {
        EBookBackendSqliteDB *ebsdb;
        GSList *list;
@@ -210,6 +232,10 @@ e_book_backend_sqlitedb_finalize (GObject *object)
 
        g_free (priv->path);
        g_free (priv->summary_fields);
+       g_free (priv->locale);
+
+       if (priv->collator)
+               e_collator_unref (priv->collator);
 
        g_mutex_clear (&priv->lock);
        g_mutex_clear (&priv->updates_lock);
@@ -473,6 +499,30 @@ collect_versions_cb (gpointer ref,
        return 0;
 }
 
+typedef struct {
+       gboolean has_countrycode;
+       gboolean has_lc_collate;
+} LocaleColumns;
+
+static gint
+find_locale_columns (gpointer data,
+                    gint n_cols,
+                    gchar **cols,
+                    gchar **name)
+{
+       LocaleColumns *columns = (LocaleColumns *)data;
+       gint i;
+
+       for (i = 0; i < n_cols; i++) {
+               if (g_strcmp0 (cols[i], "countrycode") == 0)
+                       columns->has_countrycode = TRUE;
+               else if (g_strcmp0 (cols[i], "lc_collate") == 0)
+                       columns->has_lc_collate = TRUE;
+       }
+
+       return 0;
+}
+
 static gboolean
 create_folders_table (EBookBackendSqliteDB *ebsdb,
                       gint *previous_schema,
@@ -480,6 +530,7 @@ create_folders_table (EBookBackendSqliteDB *ebsdb,
 {
        gboolean success;
        gint version = 0;
+       LocaleColumns locale_columns = { FALSE, FALSE };
 
        /* sync_data points to syncronization data, it could be last_modified
         * time or a sequence number or some text depending on the backend.
@@ -561,7 +612,7 @@ create_folders_table (EBookBackendSqliteDB *ebsdb,
         * create_contacts_table() as we need introspection details for doing
         * that.
         */
-       if (version >= 3 && version < 5) {
+       if (version >= 3 && version < FOLDER_VERSION) {
                stmt = "UPDATE folders SET "
                                "multivalues = REPLACE(RTRIM(REPLACE("
                                        "multivalues || ':', ':', "
@@ -593,6 +644,36 @@ create_folders_table (EBookBackendSqliteDB *ebsdb,
        if (!success)
                goto rollback;
 
+       /* Adding countrycode column again to track the addressbook's country code.
+        * Detecting presence of this column by reflection instead of assigning a
+        * dedicated schema version number, because upstream is not going to accept
+        * a locale bound addressbook database. See discussion in bug 689622.
+        */
+       stmt = "PRAGMA table_info(folders)";
+       success = book_backend_sql_exec (
+               ebsdb->priv->db, stmt, find_locale_columns, &locale_columns, error);
+
+       if (!success)
+               goto rollback;
+
+       if (!locale_columns.has_countrycode) {
+               stmt = "ALTER TABLE folders ADD COLUMN countrycode VARCHAR(2)";
+               success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+
+               if (!success)
+                       goto rollback;
+
+       }
+
+       if (!locale_columns.has_lc_collate) {
+               stmt = "ALTER TABLE folders ADD COLUMN lc_collate TEXT";
+               success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+
+               if (!success)
+                       goto rollback;
+       }
+
+       /* Remember the schema version for later use and finish the transaction. */
        *previous_schema = version;
        return book_backend_sqlitedb_commit_transaction (ebsdb, error);
 
@@ -634,34 +715,90 @@ format_multivalues (EBookBackendSqliteDB *ebsdb)
        return g_string_free (string, FALSE);
 }
 
+static gint
+get_count_cb (gpointer ref,
+              gint n_cols,
+              gchar **cols,
+              gchar **name)
+{
+       gint64 count = 0;
+       gint *ret = ref;
+       gint i;
+
+       for (i = 0; i < n_cols; i++) {
+               if (name[i] && strncmp (name[i], "count", 5) == 0) {
+                       count = g_ascii_strtoll (cols[i], NULL, 10);
+               }
+       }
+
+       *ret = count;
+
+       return 0;
+}
+
+static gboolean
+check_folderid_exists (EBookBackendSqliteDB *ebsdb,
+                       const gchar *folderid,
+                       gboolean *exists,
+                       GError **error)
+{
+       gboolean success;
+       gint count = 0;
+       gchar *stmt;
+
+       stmt = sqlite3_mprintf ("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=%Q;", 
folderid);
+
+       success = book_backend_sql_exec (ebsdb->priv->db, stmt, get_count_cb, &count, error);
+       sqlite3_free (stmt);
+
+       *exists = (count > 0);
+
+       return success;
+}
+
 static gboolean
 add_folder_into_db (EBookBackendSqliteDB *ebsdb,
                     const gchar *folderid,
                     const gchar *folder_name,
+                   gboolean *already_exists,
                     GError **error)
 {
        gchar *stmt;
        gboolean success;
        gchar *multivalues;
+       gboolean exists = FALSE;
 
        if (!book_backend_sqlitedb_start_transaction (ebsdb, error))
                return FALSE;
 
-       multivalues = format_multivalues (ebsdb);
-
-       stmt = sqlite3_mprintf (
-               "INSERT OR IGNORE INTO "
-               "folders ( folder_id, folder_name, version, multivalues ) "
-               "VALUES ( %Q, %Q, %d, %Q ) ",
-               folderid, folder_name, FOLDER_VERSION, multivalues);
-       success = book_backend_sql_exec (
-               ebsdb->priv->db, stmt, NULL, NULL, error);
-       sqlite3_free (stmt);
-       g_free (multivalues);
-
+       success = check_folderid_exists (ebsdb, folderid, &exists, error);
        if (!success)
                goto rollback;
 
+       if (!exists) {
+               const gchar *lc_collate;
+
+               multivalues = format_multivalues (ebsdb);
+
+               lc_collate = setlocale (LC_COLLATE, NULL);
+
+               stmt = sqlite3_mprintf (
+                       "INSERT OR IGNORE INTO "
+                       "folders ( folder_id, folder_name, version, multivalues, lc_collate ) "
+                       "VALUES ( %Q, %Q, %d, %Q, %Q ) ",
+                       folderid, folder_name, FOLDER_VERSION, multivalues, lc_collate);
+               success = book_backend_sql_exec (
+                       ebsdb->priv->db, stmt, NULL, NULL, error);
+               sqlite3_free (stmt);
+               g_free (multivalues);
+
+               if (!success)
+                       goto rollback;
+       }
+
+       if (already_exists)
+               *already_exists = exists;
+
        return book_backend_sqlitedb_commit_transaction (ebsdb, error);
 
 rollback:
@@ -698,47 +835,6 @@ collect_columns_cb (gpointer ref,
        return 0;
 }
 
-static gint
-get_count_cb (gpointer ref,
-              gint n_cols,
-              gchar **cols,
-              gchar **name)
-{
-       gint64 count = 0;
-       gint *ret = ref;
-       gint i;
-
-       for (i = 0; i < n_cols; i++) {
-               if (g_strcmp0 (name[i], "count(*)") == 0) {
-                       count = g_ascii_strtoll (cols[i], NULL, 10);
-               }
-       }
-
-       *ret = count;
-
-       return 0;
-}
-
-static gboolean
-check_folderid_exists (EBookBackendSqliteDB *ebsdb,
-                       const gchar *folderid,
-                       gboolean *exists,
-                       GError **error)
-{
-       gboolean success;
-       gint count = 0;
-       gchar *stmt;
-
-       stmt = sqlite3_mprintf ("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=%Q;", 
folderid);
-
-       success = book_backend_sql_exec (ebsdb->priv->db, stmt, get_count_cb, &count, error);
-       sqlite3_free (stmt);
-
-       *exists = (count > 0);
-
-       return success;
-}
-
 static gboolean
 introspect_summary (EBookBackendSqliteDB *ebsdb,
                     const gchar *folderid,
@@ -769,6 +865,10 @@ introspect_summary (EBookBackendSqliteDB *ebsdb,
                gchar *p;
                IndexFlags computed = 0;
 
+               /* Ignore the 'localized' columns */
+               if (g_str_has_suffix (col, "_localized"))
+                       continue;
+
                /* Check if we're parsing a reverse field */
                if ((p = strstr (col, "_reverse")) != NULL) {
                        computed = INDEX_SUFFIX;
@@ -882,17 +982,24 @@ static gboolean
 create_contacts_table (EBookBackendSqliteDB *ebsdb,
                        const gchar *folderid,
                        gint previous_schema,
+                      gboolean already_exists,
                        GError **error)
 {
        gint i;
        gboolean success;
        gchar *stmt, *tmp;
        GString *string;
-       gboolean already_exists = FALSE;
+       gboolean relocalized = FALSE;
+       gchar *current_region = NULL;
+       const gchar *lc_collate = NULL;
+       gchar *stored_lc_collate = NULL;
 
-       success = check_folderid_exists (ebsdb, folderid, &already_exists, error);
-       if (!success)
-               return FALSE;
+       if (e_phone_number_is_supported ()) {
+               current_region = e_phone_number_get_default_region (error);
+
+               if (current_region == NULL)
+                       return FALSE;
+       }
 
        string = g_string_new (
                "CREATE TABLE IF NOT EXISTS %Q ( uid TEXT PRIMARY KEY, ");
@@ -901,6 +1008,15 @@ create_contacts_table (EBookBackendSqliteDB *ebsdb,
                if (ebsdb->priv->summary_fields[i].type == G_TYPE_STRING) {
                        g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
                        g_string_append (string, " TEXT, ");
+
+                       /* For any string columns (not multivalued columns), also create a localized
+                        * data column for sort ordering
+                        */
+                       if (ebsdb->priv->summary_fields[i].field != E_CONTACT_REV) {
+                               g_string_append  (string, ebsdb->priv->summary_fields[i].dbname);
+                               g_string_append  (string, "_localized TEXT, ");
+                       }
+
                } else if (ebsdb->priv->summary_fields[i].type == G_TYPE_BOOLEAN) {
                        g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
                        g_string_append (string, " INTEGER, ");
@@ -930,6 +1046,29 @@ create_contacts_table (EBookBackendSqliteDB *ebsdb,
 
        sqlite3_free (stmt);
 
+       /* Now, if we're upgrading from < version 7, we need to add the _localized columns */
+       if (success && previous_schema >= 1 && previous_schema < 7) {
+
+               tmp = sqlite3_mprintf ("ALTER TABLE %Q ADD COLUMN ", folderid);
+
+               for (i = 1; i < ebsdb->priv->n_summary_fields && success; i++) {
+
+                       if (ebsdb->priv->summary_fields[i].type == G_TYPE_STRING &&
+                           ebsdb->priv->summary_fields[i].field != E_CONTACT_REV) {
+                               string = g_string_new (tmp);
+
+                               g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+                               g_string_append (string, "_localized TEXT, ");
+
+                               success = book_backend_sql_exec (
+                                       ebsdb->priv->db, string->str, NULL, NULL , error);
+
+                               g_string_free (string, TRUE);
+                       }
+               }
+               sqlite3_free (tmp);
+       }
+
        /* Construct the create statement from the attribute list summary table */
        if (success && ebsdb->priv->have_attr_list) {
                string = g_string_new ("CREATE TABLE IF NOT EXISTS %Q ( uid TEXT NOT NULL REFERENCES %Q(uid), 
"
@@ -995,6 +1134,20 @@ create_contacts_table (EBookBackendSqliteDB *ebsdb,
                        success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
                        sqlite3_free (stmt);
                        g_free (tmp);
+
+                       /* For any indexed column, also index the localized column */
+                       if (ebsdb->priv->summary_fields[i].field != E_CONTACT_REV) {
+                               tmp = g_strdup_printf (
+                                       "INDEX_%s_localized_%s",
+                                       summary_dbname_from_field (ebsdb, 
ebsdb->priv->summary_fields[i].field),
+                                       folderid);
+                               stmt = sqlite3_mprintf (
+                                       "CREATE INDEX IF NOT EXISTS %Q ON %Q (%s_localized)", tmp, folderid,
+                                       summary_dbname_from_field (ebsdb, 
ebsdb->priv->summary_fields[i].field));
+                               success = book_backend_sql_exec (ebsdb->priv->db, stmt, NULL, NULL, error);
+                               sqlite3_free (stmt);
+                               g_free (tmp);
+                       }
                }
 
                if (success &&
@@ -1029,9 +1182,48 @@ create_contacts_table (EBookBackendSqliteDB *ebsdb,
                }
        }
 
-       /* Until version 6, the whole contacts table requires a re-normalization of the data */
-       if (success && previous_schema < 6)
-               success = upgrade_contacts_table (ebsdb, folderid, error);
+       /* Get the locale setting for this addressbook */
+       if (success && already_exists) {
+               stmt = sqlite3_mprintf ("SELECT lc_collate FROM folders WHERE folder_id = %Q", folderid);
+               success = book_backend_sql_exec (ebsdb->priv->db, stmt, get_string_cb, &stored_lc_collate, 
error);
+               sqlite3_free (stmt);
+
+               lc_collate = stored_lc_collate;
+
+       } else if (success) {
+               /* When creating a new addressbook, default to system locale */
+               lc_collate = setlocale (LC_COLLATE, NULL);
+       }
+
+       /* Before touching any data, make sure we have a valid ECollator */
+       if (success) {
+               success = sqlitedb_set_locale_internal (ebsdb, lc_collate, error);
+       }
+
+       /* Need to relocalize the whole thing if the schema has been upgraded to version 7 */
+       if (success && previous_schema >= 1 && previous_schema < 7) {
+               success = upgrade_contacts_table (ebsdb, folderid, current_region, lc_collate, error);
+               relocalized = TRUE;
+       }
+
+       /* We may need to relocalize for a country code change */
+       if (success && relocalized == FALSE && e_phone_number_is_supported ()) {
+               gchar *stored_region = NULL;
+
+               stmt = sqlite3_mprintf ("SELECT countrycode FROM folders WHERE folder_id = %Q", folderid);
+               success = book_backend_sql_exec (ebsdb->priv->db, stmt, get_string_cb, &stored_region, error);
+               sqlite3_free (stmt);
+
+               if (success && g_strcmp0 (current_region, stored_region) != 0) {
+                       success = upgrade_contacts_table (ebsdb, folderid, current_region, lc_collate, error);
+                       relocalized = TRUE;
+               }
+
+               g_free (stored_region);
+       }
+
+       g_free (current_region);
+       g_free (stored_lc_collate);
 
        return success;
 }
@@ -1377,6 +1569,7 @@ e_book_backend_sqlitedb_new_internal (const gchar *path,
        EBookBackendSqliteDB *ebsdb;
        gchar *hash_key, *filename;
        gint previous_schema = 0;
+       gboolean already_exists = FALSE;
 
        g_return_val_if_fail (path != NULL, NULL);
        g_return_val_if_fail (emailid != NULL, NULL);
@@ -1403,8 +1596,10 @@ e_book_backend_sqlitedb_new_internal (const gchar *path,
        ebsdb->priv->have_attr_list = have_attr_list;
        ebsdb->priv->attr_list_indexes = attr_list_indexes;
        ebsdb->priv->store_vcard = store_vcard;
+
        if (g_mkdir_with_parents (path, 0777) < 0) {
                g_mutex_unlock (&dbcon_lock);
+               g_object_unref (ebsdb);
                g_set_error (
                        error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_OTHER,
                        "Can not make parent directory: errno %d", errno);
@@ -1436,13 +1631,14 @@ e_book_backend_sqlitedb_new_internal (const gchar *path,
        LOCK_MUTEX (&ebsdb->priv->lock);
        g_mutex_unlock (&dbcon_lock);
 
-       if (!add_folder_into_db (ebsdb, folderid, folder_name, error)) {
+       if (!add_folder_into_db (ebsdb, folderid, folder_name,
+                                &already_exists, error)) {
                UNLOCK_MUTEX (&ebsdb->priv->lock);
                g_object_unref (ebsdb);
                return NULL;
        }
 
-       if (!create_contacts_table (ebsdb, folderid, previous_schema, error)) {
+       if (!create_contacts_table (ebsdb, folderid, previous_schema, already_exists, error)) {
                UNLOCK_MUTEX (&ebsdb->priv->lock);
                g_object_unref (ebsdb);
                return NULL;
@@ -1755,6 +1951,25 @@ e_book_backend_sqlitedb_unlock_updates (EBookBackendSqliteDB *ebsdb,
        return success;
 }
 
+/**
+ * e_book_backend_sqlitedb_ref_collator:
+ * @ebsdb: An #EBookBackendSqliteDB
+ *
+ * References the currently active #ECollator for @ebsdb,
+ * 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.
+ */
+ECollator *
+e_book_backend_sqlitedb_ref_collator(EBookBackendSqliteDB *ebsdb)
+{
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+
+       return e_collator_ref (ebsdb->priv->collator);
+}
+
 static gchar *
 mprintf_suffix (const gchar *normal)
 {
@@ -1841,32 +2056,90 @@ insert_stmt_from_contact (EBookBackendSqliteDB *ebsdb,
        gint i;
 
        str = sqlite3_mprintf (
-               "INSERT or %s INTO %Q VALUES (",
+               "INSERT or %s INTO %Q (",
                replace_existing ? "REPLACE" : "FAIL", folderid);
        string = g_string_new (str);
        sqlite3_free (str);
 
+       /*
+        * First specify the column names for the insert, since it's possible we
+        * upgraded the DB and cannot be sure the order of the columns are ordered
+        * just how we like them to be.
+        */
+       for (i = 0; i < ebsdb->priv->n_summary_fields; i++) {
+
+               /* Multi values go into a separate table/statement */
+               if (ebsdb->priv->summary_fields[i].type != E_TYPE_CONTACT_ATTR_LIST) {
+
+                       if (i > 0)
+                               g_string_append (string, ", ");
+
+                       g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+               }
+
+               if (ebsdb->priv->summary_fields[i].type == G_TYPE_STRING) {
+
+                       if (ebsdb->priv->summary_fields[i].field != E_CONTACT_UID &&
+                           ebsdb->priv->summary_fields[i].field != E_CONTACT_REV) {
+                               g_string_append (string, ", ");
+                               g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+                               g_string_append (string, "_localized");
+                       }
+
+                       if ((ebsdb->priv->summary_fields[i].index & INDEX_SUFFIX) != 0) {
+                               g_string_append (string, ", ");
+                               g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+                               g_string_append (string, "_reverse");
+                       }
+
+                       if ((ebsdb->priv->summary_fields[i].index & INDEX_PHONE) != 0) {
+                               g_string_append (string, ", ");
+                               g_string_append (string, ebsdb->priv->summary_fields[i].dbname);
+                               g_string_append (string, "_phone");
+                       }
+               }
+       }
+       g_string_append (string, ", vcard, bdata)");
+
+       /*
+        * Now specify values for all of the column names we specified.
+        */
+       g_string_append (string, " VALUES (");
        for (i = 0; i < ebsdb->priv->n_summary_fields; i++) {
                if (ebsdb->priv->summary_fields[i].type == G_TYPE_STRING) {
                        gchar *val;
                        gchar *normal;
+                       gchar *localized = NULL;
 
                        if (i > 0)
                                g_string_append (string, ", ");
 
                        val = e_contact_get (contact, ebsdb->priv->summary_fields[i].field);
 
-                       /* Special exception, never normalize the UID or REV string */
+                       /* Special exception, never normalize/localize the UID or REV string */
                        if (ebsdb->priv->summary_fields[i].field != E_CONTACT_UID &&
-                           ebsdb->priv->summary_fields[i].field != E_CONTACT_REV)
+                           ebsdb->priv->summary_fields[i].field != E_CONTACT_REV) {
                                normal = e_util_utf8_normalize (val);
-                       else
+
+                               if (val)
+                                       localized = e_collator_generate_key (ebsdb->priv->collator, val, 
NULL);
+                               else
+                                       localized = g_strdup ("");
+                       } else
                                normal = g_strdup (val);
 
                        str = sqlite3_mprintf ("%Q", normal);
                        g_string_append (string, str);
                        sqlite3_free (str);
 
+                       if (ebsdb->priv->summary_fields[i].field != E_CONTACT_UID &&
+                           ebsdb->priv->summary_fields[i].field != E_CONTACT_REV) {
+                               str = sqlite3_mprintf ("%Q", localized);
+                               g_string_append (string, ", ");
+                               g_string_append (string, str);
+                               sqlite3_free (str);
+                       }
+
                        if ((ebsdb->priv->summary_fields[i].index & INDEX_SUFFIX) != 0) {
                                str = mprintf_suffix (normal);
                                g_string_append (string, ", ");
@@ -1883,6 +2156,7 @@ insert_stmt_from_contact (EBookBackendSqliteDB *ebsdb,
 
                        g_free (normal);
                        g_free (val);
+                       g_free (localized);
                } else if (ebsdb->priv->summary_fields[i].type == G_TYPE_BOOLEAN) {
                        gboolean val;
 
@@ -2670,9 +2944,7 @@ e_book_backend_sqlitedb_get_vcard_string (EBookBackendSqliteDB *ebsdb,
                        vcard_str = s_data->vcard;
                        s_data->vcard = NULL;
 
-                       e_book_backend_sqlitedb_search_data_free (s_data);
-
-                       g_slist_free (vcards);
+                       g_slist_free_full (vcards, destroy_search_data);
                        vcards = NULL;
                }
 
@@ -4730,63 +5002,1153 @@ e_book_backend_sqlitedb_remove (EBookBackendSqliteDB *ebsdb,
        return TRUE;
 }
 
-static void
-destroy_search_data (gpointer data)
-{
-       e_book_backend_sqlitedb_search_data_free (data);
-}
-
 static gboolean
 upgrade_contacts_table (EBookBackendSqliteDB *ebsdb,
-                        const gchar *folderid,
-                        GError **error)
+                       const gchar          *folderid,
+                       const gchar          *region,
+                       const gchar          *lc_collate,
+                       GError              **error)
 {
        gchar *stmt;
        gboolean success = FALSE;
        GSList *vcard_data = NULL;
        GSList *l;
-       gchar *default_region = NULL;
 
        stmt = sqlite3_mprintf ("SELECT uid, vcard, NULL FROM %Q", folderid);
        success = book_backend_sql_exec (
                ebsdb->priv->db, stmt, addto_vcard_list_cb, &vcard_data, error);
        sqlite3_free (stmt);
 
-       if (vcard_data == NULL)
-               return TRUE;
+       for (l = vcard_data; success && l; l = l->next) {
+               EbSdbSearchData *const s_data = l->data;
+               EContact *contact = e_contact_new_from_vcard_with_uid (s_data->vcard, s_data->uid);
 
-       if (e_phone_number_is_supported ()) {
-               default_region = e_phone_number_get_default_region (error);
+               if (contact == NULL)
+                       continue;
 
-               if (default_region == NULL)
-                       success = FALSE;
+               success = insert_contact (ebsdb, contact, folderid, TRUE, region, error);
+
+               g_object_unref (contact);
        }
 
-       success = book_backend_sqlitedb_start_transaction (ebsdb, error);
+       g_slist_free_full (vcard_data, destroy_search_data);
 
        if (success) {
 
-               for (l = vcard_data; success && l; l = l->next) {
-                       EbSdbSearchData *const s_data = l->data;
-                       EContact *contact = e_contact_new_from_vcard_with_uid (s_data->vcard, s_data->uid);
+               stmt = sqlite3_mprintf (
+                       "UPDATE folders SET countrycode = %Q WHERE folder_id = %Q",
+                       region, folderid);
+               success = book_backend_sql_exec (
+                       ebsdb->priv->db, stmt, NULL, NULL, error);
+               sqlite3_free (stmt);
 
-                       if (contact == NULL)
-                               continue;
+               stmt = sqlite3_mprintf (
+                       "UPDATE folders SET lc_collate = %Q WHERE folder_id = %Q",
+                       lc_collate, folderid);
+               success = book_backend_sql_exec (
+                       ebsdb->priv->db, stmt, NULL, NULL, error);
+               sqlite3_free (stmt);
+       }
+
+       return success;
+}
 
-                       success = insert_contact (ebsdb, contact, folderid, TRUE, default_region, error);
+static gboolean
+sqlitedb_set_locale_internal (EBookBackendSqliteDB *ebsdb,
+                             const gchar          *locale,
+                             GError              **error)
+{
+       EBookBackendSqliteDBPrivate *priv = ebsdb->priv;
+       ECollator *collator;
+
+       if (g_strcmp0 (priv->locale, locale) != 0) {
+
+               collator = e_collator_new (locale, error);
+               if (!collator)
+                       return FALSE;
+
+               g_free (priv->locale);
+               priv->locale = g_strdup (locale);
+
+               if (ebsdb->priv->collator)
+                       e_collator_unref (ebsdb->priv->collator);
+
+               ebsdb->priv->collator = collator;
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_book_backend_sqlitedb_set_locale:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @lc_collate: The new locale for the addressbook
+ * @error: A location to store any error that may have occurred
+ *
+ * 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 addressbook, changing the locale
+ * will store the new locale and will modify sort keys and any
+ * locale specific data in the addressbook.
+ *
+ * Returns: Whether the new locale was successfully set.
+ *
+ * Since: 3.10
+ */
+gboolean
+e_book_backend_sqlitedb_set_locale (EBookBackendSqliteDB *ebsdb,
+                                   const gchar          *folderid,
+                                   const gchar          *lc_collate,
+                                   GError              **error)
+{
+       gboolean success;
+       gchar *stmt;
+       gchar *stored_lc_collate;
+       gchar *current_region = NULL;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+       g_return_val_if_fail (folderid && folderid[0], FALSE);
+
+       LOCK_MUTEX (&ebsdb->priv->lock);
+
+       if (e_phone_number_is_supported ()) {
+               current_region = e_phone_number_get_default_region (error);
+
+               if (current_region == NULL)
+                       return FALSE;
+       }
+
+       if (!sqlitedb_set_locale_internal (ebsdb, lc_collate, error))
+               return FALSE;
+
+       if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+               UNLOCK_MUTEX (&ebsdb->priv->lock);
+               g_free (current_region);
+               return FALSE;
+       }
+
+       stmt = sqlite3_mprintf ("SELECT lc_collate FROM folders WHERE folder_id = %Q", folderid);
+       success = book_backend_sql_exec (ebsdb->priv->db, stmt, get_string_cb, &stored_lc_collate, error);
+       sqlite3_free (stmt);
+
+       if (success && g_strcmp0 (stored_lc_collate, lc_collate) != 0)
+               success = upgrade_contacts_table (ebsdb, folderid, current_region, lc_collate, error);
+
+       /* If for some reason we failed, then reset the collator to use the old locale */
+       if (!success)
+               sqlitedb_set_locale_internal (ebsdb, stored_lc_collate, NULL);
+
+       g_free (stored_lc_collate);
+       g_free (current_region);
+
+       if (!success)
+               goto rollback;
+
+       success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+       UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+       return success;
+
+ rollback:
+       /* The GError is already set. */
+       book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+       UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+       return FALSE;
+}
 
-                       g_object_unref (contact);
+/**
+ * e_book_backend_sqlitedb_get_locale:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @locale_out: (out) (transfer full): The location to return the current locale
+ * @error: A location to store any error that may have occurred
+ *
+ * Fetches the current locale setting for the address-book indicated by @folderid.
+ *
+ * Upon success, @lc_collate_out will hold the returned locale setting,
+ * otherwise %FALSE will be returned and @error will be updated accordingly.
+ *
+ * Returns: Whether the locale was successfully fetched.
+ *
+ * Since: 3.10
+ */
+gboolean
+e_book_backend_sqlitedb_get_locale (EBookBackendSqliteDB *ebsdb,
+                                   const gchar          *folderid,
+                                   gchar               **locale_out,
+                                   GError              **error)
+{
+       gchar *stmt;
+       gboolean success;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+       g_return_val_if_fail (folderid && folderid[0], FALSE);
+       g_return_val_if_fail (locale_out != NULL && *locale_out == NULL, FALSE);
+
+       LOCK_MUTEX (&ebsdb->priv->lock);
+
+       stmt = sqlite3_mprintf (
+               "SELECT lc_collate FROM folders WHERE folder_id = %Q", folderid);
+       success = book_backend_sql_exec (
+               ebsdb->priv->db, stmt, get_string_cb, locale_out, error);
+       sqlite3_free (stmt);
+
+       if (!sqlitedb_set_locale_internal (ebsdb, *locale_out, &local_error)) {
+               g_warning ("Error loading new locale: %s", local_error->message);
+               g_clear_error (&local_error);
+       }
+
+       UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+       return success;
+}
+
+/******************************************************************
+ *                          EbSdbCursor apis                       *
+ ******************************************************************/
+typedef struct {
+       gchar        **values;        /* The cursor position, results will be returned after this position */
+       gchar         *last_uid;      /* The cursor contact UID position, used as a tie breaker */
+} CursorState;
+
+typedef enum {
+       STATE_PREVIOUS,
+       STATE_CURRENT,
+       N_CURSOR_STATES
+} CursorStateType;
+
+struct _EbSdbCursor {
+       gchar         *folderid;      /* The folderid for this cursor */
+
+       EBookBackendSExp *sexp;       /* An EBookBackendSExp based on the query, used by 
e_book_backend_sqlitedb_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 */
+       EBookSortType *sort_types;    /* The sort method to use for each field */
+       gint           n_sort_fields; /* The amound of sort fields */
+
+       CursorState    state[N_CURSOR_STATES];
+};
+
+static void
+ebsdb_cursor_setup_query (EBookBackendSqliteDB *ebsdb,
+                         EbSdbCursor          *cursor,
+                         const gchar          *sexp,
+                         gboolean              query_with_list_attrs)
+{
+       gchar *stmt;
+       gchar *count_stmt;
+
+       g_free (cursor->select_vcards);
+       g_free (cursor->select_count);
+       g_free (cursor->query);
+       g_clear_object (&(cursor->sexp));
+
+       if (query_with_list_attrs) {
+               gchar *list_table = g_strconcat (cursor->folderid, "_lists", NULL);
+
+               stmt = sqlite3_mprintf ("SELECT DISTINCT summary.uid, vcard, bdata FROM %Q AS summary "
+                                       "LEFT OUTER JOIN %Q AS multi ON summary.uid = multi.uid",
+                                       cursor->folderid, list_table);
+
+               count_stmt = sqlite3_mprintf ("SELECT count(DISTINCT summary.uid), vcard, bdata FROM %Q AS 
summary "
+                                             "LEFT OUTER JOIN %Q AS multi ON summary.uid = multi.uid",
+                                             cursor->folderid, list_table);
+               g_free (list_table);
+       } else {
+               stmt = sqlite3_mprintf ("SELECT uid, vcard, bdata FROM %Q AS summary", cursor->folderid);
+               count_stmt = sqlite3_mprintf ("SELECT count(*) FROM %Q AS summary", cursor->folderid);
+       }
+
+       cursor->select_vcards = g_strdup (stmt);
+       cursor->select_count = g_strdup (count_stmt);
+       sqlite3_free (stmt);
+       sqlite3_free (count_stmt);
+
+       if (sexp) {
+               cursor->query = sexp_to_sql_query (ebsdb, cursor->folderid, sexp);
+               cursor->sexp  = e_book_backend_sexp_new (sexp);
+       } else {
+               cursor->query = NULL;
+               cursor->sexp  = NULL;
+       }
+}
+
+static gchar *
+ebsdb_cursor_order_by_fragment (EBookBackendSqliteDB *ebsdb,
+                               EContactField        *sort_fields,
+                               EBookSortType        *sort_types,
+                               guint                 n_sort_fields,
+                               gboolean              reverse)
+{
+       GString *string;
+       const gchar *field_name;
+       gint i;
+
+       string = g_string_new ("ORDER BY ");
+
+       for (i = 0; i < n_sort_fields; i++) {
+
+               field_name = summary_dbname_from_field (ebsdb, sort_fields[i]);
+
+               if (i > 0)
+                       g_string_append (string, ", ");
+
+               g_string_append_printf (string, "summary.%s_localized %s", field_name,
+                                       reverse ?
+                                       (sort_types[i] == E_BOOK_SORT_ASCENDING ? "DESC" : "ASC") :
+                                       (sort_types[i] == E_BOOK_SORT_ASCENDING ? "ASC"  : "DESC"));
+       }
+
+       /* Also order the UID, since it's our tie breaker, we must also order the UID field */
+       if (n_sort_fields > 0)
+               g_string_append (string, ", ");
+       g_string_append_printf (string, "summary.uid %s", reverse ? "DESC" : "ASC");
+
+       return g_string_free (string, FALSE);
+}
+
+static EbSdbCursor *
+ebsdb_cursor_new (EBookBackendSqliteDB *ebsdb,
+                 const gchar          *folderid,
+                 const gchar          *sexp,
+                 gboolean              query_with_list_attrs,
+                 EContactField        *sort_fields,
+                 EBookSortType        *sort_types,
+                 guint                 n_sort_fields)
+{
+       EbSdbCursor *cursor = g_slice_new0 (EbSdbCursor);
+       gint i;
+
+       cursor->folderid = g_strdup (folderid);
+
+       /* Setup the initial query fragments */
+       ebsdb_cursor_setup_query (ebsdb, cursor, sexp, query_with_list_attrs);
+
+       cursor->order = ebsdb_cursor_order_by_fragment (ebsdb,
+                                                       sort_fields,
+                                                       sort_types,
+                                                       n_sort_fields,
+                                                       FALSE);
+       cursor->reverse_order = ebsdb_cursor_order_by_fragment (ebsdb,
+                                                               sort_fields,
+                                                               sort_types,
+                                                               n_sort_fields,
+                                                               TRUE);
+
+       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 (EBookSortType) * n_sort_fields);
+
+       for (i = 0; i < N_CURSOR_STATES; i++)
+               cursor->state[i].values = g_new0 (gchar *, n_sort_fields);
+
+       return cursor;
+}
+
+static void
+ebsdb_cursor_clear_state (EbSdbCursor      *cursor,
+                         CursorStateType   state_type)
+{
+       gint i;
+
+       for (i = 0; i < cursor->n_sort_fields; i++) {
+               g_free (cursor->state[state_type].values[i]);
+               cursor->state[state_type].values[i] = NULL;
+       }
+
+       g_free (cursor->state[state_type].last_uid);
+       cursor->state[state_type].last_uid = NULL;
+}
+
+static void
+ebsdb_cursor_swap_state (EbSdbCursor      *cursor)
+{
+       gchar **tmp_values;
+       gchar  *tmp_last_uid;
+
+       /* Swap the current values and the previous values */
+       tmp_values = cursor->state[STATE_CURRENT].values;
+       cursor->state[STATE_CURRENT].values  = cursor->state[STATE_PREVIOUS].values;
+       cursor->state[STATE_PREVIOUS].values = tmp_values;
+
+       /* Swap the current uid and the previous uid */
+       tmp_last_uid = cursor->state[STATE_CURRENT].last_uid;
+       cursor->state[STATE_CURRENT].last_uid  = cursor->state[STATE_PREVIOUS].last_uid;
+       cursor->state[STATE_PREVIOUS].last_uid = tmp_last_uid;
+}
+
+static void
+ebsdb_cursor_free (EbSdbCursor *cursor)
+{
+       gint i;
+
+       if (cursor) {
+
+               for (i = 0; i < N_CURSOR_STATES; i++) {
+                       ebsdb_cursor_clear_state (cursor, i);
+                       g_free (cursor->state[i].values);
                }
 
-               if (success)
-                       success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+               g_clear_object (&(cursor->sexp));
+               g_free (cursor->folderid);
+               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 (EbSdbCursor, cursor);
+       }
+}
+
+static void
+ebsdb_cursor_set_state_from_contact (EBookBackendSqliteDB *ebsdb,
+                                    EbSdbCursor          *cursor,
+                                    EContact             *contact)
+{
+       gint i;
+
+       /* Push the current state into the previous state and clear the current state */
+       ebsdb_cursor_swap_state (cursor);
+       ebsdb_cursor_clear_state (cursor, STATE_CURRENT);
+
+       for (i = 0; i < cursor->n_sort_fields; i++) {
+               const gchar *string = e_contact_get_const (contact, cursor->sort_fields[i]);
+
+               if (string)
+                       cursor->state[STATE_CURRENT].values[i] =
+                               e_collator_generate_key (ebsdb->priv->collator,
+                                                        string, NULL);
                else
-                       /* The GError is already set. */
-                       book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+                       cursor->state[STATE_CURRENT].values[i] = g_strdup ("");
        }
 
-       g_slist_free_full (vcard_data, destroy_search_data);
-       g_free (default_region);
+       cursor->state[STATE_CURRENT].last_uid = e_contact_get (contact, E_CONTACT_UID);
+}
+
+static void
+ebsdb_cursor_set_state (EBookBackendSqliteDB *ebsdb,
+                       EbSdbCursor          *cursor,
+                       const gchar          *vcard)
+{
+       EContact *contact;
+
+       g_assert (vcard);
+
+       contact = e_contact_new_from_vcard (vcard);
+       ebsdb_cursor_set_state_from_contact (ebsdb, cursor, contact);
+       g_object_unref (contact);
+}
+
+#define GREATER_OR_LESS(cursor, index, reverse)                                \
+       (reverse ?                                                      \
+        (((EbSdbCursor *)cursor)->sort_types[index] == E_BOOK_SORT_ASCENDING ? '<' : '>') : \
+        (((EbSdbCursor *)cursor)->sort_types[index] == E_BOOK_SORT_ASCENDING ? '>' : '<'))
+
+static gchar *
+ebsdb_cursor_constraints (EBookBackendSqliteDB *ebsdb,
+                         EbSdbCursor          *cursor,
+                         gboolean              reverse,
+                         gboolean              include_current_uid)
+{
+       GString *string;
+       const gchar *field_name;
+       gint i, j;
+
+       /* 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 (i = 0; i < (cursor->n_sort_fields + 1); i++) {
+               gchar   *stmt;
+
+               /* Break once we hit a NULL value */
+               if ((i  < cursor->n_sort_fields && cursor->state[STATE_CURRENT].values[i] == NULL) ||
+                   (i == cursor->n_sort_fields && cursor->state[STATE_CURRENT].last_uid  == NULL))
+                       break;
+
+               /* Between each qualifier, add an 'OR' */
+               if (i > 0)
+                       g_string_append (string, " OR ");
+
+               /* Begin qualifier */
+               g_string_append_c (string, '(');
+
+               /* Create the '=' statements leading up to the current tie breaker */
+               for (j = 0; j < i; j++) {
+                       field_name = summary_dbname_from_field (ebsdb, cursor->sort_fields[j]);
+
+                       stmt = sqlite3_mprintf ("summary.%s_localized = %Q",
+                                               field_name,
+                                               cursor->state[STATE_CURRENT].values[j]);
+
+                       g_string_append (string, stmt);
+                       g_string_append (string, " AND ");
+
+                       sqlite3_free (stmt);
+
+               }
+
+               if (i == 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 */
+                       stmt = sqlite3_mprintf ("summary.uid %c %Q",
+                                               reverse ? '<' : '>',
+                                               cursor->state[STATE_CURRENT].last_uid);
+                       g_string_append (string, stmt);
+                       sqlite3_free (stmt);
+
+                       if (include_current_uid) {
+                               stmt = sqlite3_mprintf (" OR summary.uid = %Q",
+                                                       cursor->state[STATE_CURRENT].last_uid);
+                               g_string_append (string, stmt);
+                               g_string_append_c (string, ')');
+                               sqlite3_free (stmt);
+                       }
+
+               } 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 &&
+                                ((i + 1 < cursor->n_sort_fields && cursor->state[STATE_CURRENT].values[i + 
1] == NULL) ||
+                                 (i + 1 == cursor->n_sort_fields && cursor->state[STATE_CURRENT].last_uid == 
NULL)));
+
+                       if (include_exact_match)
+                               g_string_append_c (string, '(');
+
+                       /* Append the final qualifier for this field */
+                       field_name = summary_dbname_from_field (ebsdb, cursor->sort_fields[i]);
+
+                       stmt = sqlite3_mprintf ("summary.%s_localized %c %Q",
+                                               field_name,
+                                               GREATER_OR_LESS (cursor, i, reverse),
+                                               cursor->state[STATE_CURRENT].values[i]);
+
+                       g_string_append (string, stmt);
+                       sqlite3_free (stmt);
+
+                       if (include_exact_match) {
+
+                               stmt = sqlite3_mprintf (" OR summary.%s_localized = %Q",
+                                                       field_name, cursor->state[STATE_CURRENT].values[i]);
+
+                               g_string_append (string, stmt);
+                               g_string_append_c (string, ')');
+                               sqlite3_free (stmt);
+                       }
+               }
+
+               /* End qualifier */
+               g_string_append_c (string, ')');
+       }
+
+       return g_string_free (string, FALSE);
+}
+
+static gboolean
+cursor_count_total_locked (EBookBackendSqliteDB *ebsdb,
+                          EbSdbCursor          *cursor,
+                          gint                 *total,
+                          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 = book_backend_sql_exec (ebsdb->priv->db, query->str,
+                                        get_count_cb, total, error);
+
+       g_string_free (query, TRUE);
+
+       return success;
+}
+
+static gboolean
+cursor_count_position_locked (EBookBackendSqliteDB *ebsdb,
+                             EbSdbCursor          *cursor,
+                             gint                 *position,
+                             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[STATE_CURRENT].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 = ebsdb_cursor_constraints (ebsdb, cursor, TRUE, TRUE);
+
+               g_string_append_c (query, '(');
+               g_string_append (query, constraints);
+               g_string_append_c (query, ')');
+
+               g_free (constraints);
+       }
+
+       /* Execute the query */
+       success = book_backend_sql_exec (ebsdb->priv->db, query->str,
+                                        get_count_cb, position, error);
+
+       g_string_free (query, TRUE);
+
+       return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_new:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @folderid: folder id of the address-book
+ * @sexp: search expression; use NULL or an empty string to get all stored contacts.
+ * @sort_fields: (array length=n_sort_fields): An array of #EContactFields as sort keys in order of priority
+ * @sort_types: (array length=n_sort_fields): An array of #EBookSortTypes, one for each field in @sort_fields
+ * @n_sort_fields: The number of fields to sort results by.
+ * @error: A return location to story any error that might be reported.
+ *
+ * Creates a new #EbSdbCursor.
+ *
+ * The cursor should be freed with e_book_backend_sqlitedb_cursor_free().
+ *
+ * Returns: (transfer full): A newly created #EbSdbCursor
+ *
+ * Since: 3.10
+ */
+EbSdbCursor *
+e_book_backend_sqlitedb_cursor_new (EBookBackendSqliteDB *ebsdb,
+                                   const gchar          *folderid,
+                                   const gchar          *sexp,
+                                   EContactField        *sort_fields,
+                                   EBookSortType        *sort_types,
+                                   guint                 n_sort_fields,
+                                   GError              **error)
+{
+       gboolean query_with_list_attrs = FALSE;
+       gint i;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), NULL);
+       g_return_val_if_fail (folderid && folderid[0], NULL);
+
+       /* We don't like '\0' sexps, prefer NULL */
+       if (sexp && !sexp[0])
+               sexp = NULL;
+
+       /* We only support cursors for summary fields in the query */
+       if (sexp && !e_book_backend_sqlitedb_check_summary_query (ebsdb, sexp, &query_with_list_attrs)) {
+               g_set_error (error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+                            _("Only summary queries are supported by EbSdbCursor"));
+               return NULL;
+       }
+
+       if (n_sort_fields == 0) {
+               g_set_error (error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+                            _("At least one sort field must be specified to use an EbSdbCursor"));
+               return NULL;
+       }
+
+       /* We only support summarized sort keys which are not multi value fields */
+       for (i = 0; i < n_sort_fields; i++) {
+
+               gint support;
+
+               support = func_check_field_test (ebsdb, e_contact_field_name (sort_fields[i]), NULL);
+
+               if ((support & CHECK_IS_SUMMARY) == 0) {
+                       g_set_error (error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+                                    _("Cannot sort by a field that is not in the summary"));
+                       return NULL;
+               }
+
+               if ((support & CHECK_IS_LIST_ATTR) != 0) {
+                       g_set_error (error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+                                    _("Cannot sort by a field which may have multiple values"));
+                       return NULL;
+               }
+       }
+
+       return ebsdb_cursor_new (ebsdb, folderid, sexp, query_with_list_attrs,
+                                sort_fields, sort_types, n_sort_fields);
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_free:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor to free
+ *
+ * Frees @cursor.
+ *
+ * Since: 3.10
+ */
+void
+e_book_backend_sqlitedb_cursor_free (EBookBackendSqliteDB *ebsdb,
+                                    EbSdbCursor          *cursor)
+{
+       g_return_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb));
+
+       ebsdb_cursor_free (cursor);
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_move_by:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor to use
+ * @origin: The #EbSdbCurorOrigin for this move
+ * @count: A positive or negative amount of contacts to try and fetch
+ * @results: (out) (allow-none) (element-type EbSdbSearchData) (transfer full):
+ *   A return location to store the results, or %NULL to move the cursor without retrieving any results.
+ * @error: A return location to story any error that might be reported.
+ *
+ * Moves @cursor through the @ebsdb by @count and fetch a maximum of @count contacts.
+ *
+ * If @count is negative, then the cursor will move backwards.
+ *
+ * If @cursor is in an empty state, or @origin is %EBSDB_CURSOR_ORIGIN_RESET,
+ * then @count contacts will be fetched from the beginning of the cursor's query
+ * results, or from the ending of the query results for a negative value of @count.
+ *
+ * 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.
+ * This is not considered an error condition.
+ *
+ * If @results is specified, it should be a pointer to a %NULL #GSList,
+ * the result list will be stored to @results and should be freed with g_slist_free()
+ * and all elements freed with e_book_backend_sqlitedb_search_data_free().
+ *
+ * Returns: %TRUE on Success, otherwise %FALSE is returned if any error occurred
+ * and @error is set to reflect the error which occurred.
+ *
+ * Since: 3.10
+ */
+gboolean
+e_book_backend_sqlitedb_cursor_move_by (EBookBackendSqliteDB *ebsdb,
+                                       EbSdbCursor          *cursor,
+                                       EbSdbCurorOrigin      origin,
+                                       gint                  count,
+                                       GSList              **results,
+                                       GError              **error)
+{
+       GSList *local_results = NULL;
+       GString *query;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+       g_return_val_if_fail (cursor != NULL, FALSE);
+       g_return_val_if_fail (count != 0 || origin == EBSDB_CURSOR_ORIGIN_RESET, FALSE);
+       g_return_val_if_fail (results == NULL || *results == NULL, FALSE);
+
+       /* Every query starts with the STATE_CURRENT position, first
+        * fix up the cursor state according to 'origin'
+        */
+       switch (origin) {
+       case EBSDB_CURSOR_ORIGIN_CURRENT:
+               /* Do nothing, normal operation */
+               break;
+       case EBSDB_CURSOR_ORIGIN_PREVIOUS:
+               /* Swap the previous state into the current state first */
+               ebsdb_cursor_swap_state (cursor);
+               break;
+       case EBSDB_CURSOR_ORIGIN_RESET:
+               /* Clear the current state before executing the query */
+               ebsdb_cursor_clear_state (cursor, STATE_CURRENT);
+               ebsdb_cursor_clear_state (cursor, STATE_PREVIOUS);
+               break;
+       }
+
+       /* Count can be 0 only for the sake of resetting the current
+        * cursor state without fetching any results
+        */
+       if (count == 0)
+               return TRUE;
+
+       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 (cursor->state[STATE_CURRENT].values[0] != NULL) {
+               gchar  *constraints = NULL;
+
+               if (!cursor->query)
+                       g_string_append (query, " WHERE ");
+               else
+                       g_string_append (query, " AND ");
+
+               constraints = ebsdb_cursor_constraints (ebsdb, cursor, 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));
+
+       /* Execute the query */
+       LOCK_MUTEX (&ebsdb->priv->lock);
+       success = book_backend_sql_exec (ebsdb->priv->db, query->str,
+                                        addto_vcard_list_cb , &local_results,
+                                        error);
+       UNLOCK_MUTEX (&ebsdb->priv->lock);
+
+       g_string_free (query, TRUE);
+
+       /* Correct the order of results, since
+        * addto_vcard_list_cb() prepends them (as it should)
+        */
+       local_results = g_slist_reverse (local_results);
+
+       /* If there was no error, update the internal cursor state */
+       if (success) {
+
+               if (g_slist_length (local_results) < ABS (count)) {
+                       /* We've reached the end, clear the current state, allow
+                        * a repeat query from the previously recorded position */
+                       ebsdb_cursor_swap_state (cursor);
+                       ebsdb_cursor_clear_state (cursor, STATE_CURRENT);
+               } else {
+                       /* Set the cursor state to the last result */
+                       GSList *last = g_slist_last (local_results);
+                       EbSdbSearchData *data = last->data;
+
+                       ebsdb_cursor_set_state (ebsdb, cursor, data->vcard);
+               }
+       }
+
+       if (results) {
+               *results = local_results;
+       } else {
+               /* Even if we are not returning the results, we have to fetch them
+                * so that we can store the state of the last returned result as the
+                * new cursor state.
+                */
+               g_slist_free_full (local_results,
+                                  (GDestroyNotify)e_book_backend_sqlitedb_search_data_free);
+       }
+
+       return success;
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_set_target_alphabetic_index:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor to modify
+ * @index: The alphabetic index
+ *
+ * Sets the current cursor position to point to an index into the
+ * alphabet active in the current locale.
+ *
+ * After setting the target to an alphabetic index, for example the
+ * index for letter 'E', then further calls to e_book_backend_sqlitedb_cursor_move_by()
+ * 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, knowlege
+ * on the currently active alphabet index must be obtained using #ECollator
+ * APIs.
+ *
+ * Use e_book_backend_sqlitedb_ref_collator() to obtain the active collator for @ebsdb.
+ *
+ * Since: 3.10
+ */
+void
+e_book_backend_sqlitedb_cursor_set_target_alphabetic_index (EBookBackendSqliteDB *ebsdb,
+                                                           EbSdbCursor          *cursor,
+                                                           gint                  index)
+{
+       gint n_labels = 0;
+
+       g_return_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb));
+       g_return_if_fail (cursor != NULL);
+       g_return_if_fail (index >= 0);
+
+       e_collator_get_index_labels (ebsdb->priv->collator, &n_labels,
+                                    NULL, NULL, NULL);
+       g_return_if_fail (index < n_labels);
+
+       ebsdb_cursor_clear_state (cursor, STATE_PREVIOUS);
+       ebsdb_cursor_clear_state (cursor, STATE_CURRENT);
+       if (cursor->n_sort_fields > 0) {
+               cursor->state[STATE_PREVIOUS].values[0] =
+                       e_collator_generate_key_for_index (ebsdb->priv->collator, index);
+               cursor->state[STATE_CURRENT].values[0] =
+                       e_collator_generate_key_for_index (ebsdb->priv->collator, index);
+       }
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_set_sexp:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor
+ * @sexp: The new query expression for @cursor
+ * @error: A return location to story any error that might be reported.
+ *
+ * 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_backend_sqlitedb_cursor_calculate() or
+ * e_book_backend_sqlitedb_cursor_move_by().
+ *
+ * Returns: %TRUE if the expression was valid and accepted by @ebsdb
+ *
+ * Since: 3.10
+ */
+gboolean
+e_book_backend_sqlitedb_cursor_set_sexp (EBookBackendSqliteDB *ebsdb,
+                                        EbSdbCursor          *cursor,
+                                        const gchar          *sexp,
+                                        GError              **error)
+{
+       gboolean query_with_list_attrs = FALSE;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+       g_return_val_if_fail (cursor != NULL, FALSE);
+
+       /* We don't like '\0' sexps, prefer NULL */
+       if (sexp && !sexp[0])
+               sexp = NULL;
+
+       /* We only support cursors for summary fields in the query */
+       if (sexp && !e_book_backend_sqlitedb_check_summary_query (ebsdb, sexp, &query_with_list_attrs)) {
+               g_set_error (error, E_BOOK_SDB_ERROR, E_BOOK_SDB_ERROR_INVALID_QUERY,
+                            _("Only summary queries are supported by EbSdbCursor"));
+               return FALSE;
+       }
+
+       ebsdb_cursor_setup_query (ebsdb, cursor, sexp, query_with_list_attrs);
+
+       return TRUE;
+}
+
+/**
+ * e_book_backend_sqlitedb_cursor_calculate:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor
+ * @total: (out) (allow-none): A return location to store the total result set for this cursor
+ * @position: (out) (allow-none): A return location to store the total results before the cursor value
+ * @error: (allow-none): A return location to story any error that might be reported.
+ *
+ * Calculates the @total amount of results for the @cursor's query expression,
+ * as well as the current @position of @cursor in the results. @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 @total and @position were successfully calculated.
+ *
+ * Since: 3.10
+ */
+gboolean
+e_book_backend_sqlitedb_cursor_calculate (EBookBackendSqliteDB *ebsdb,
+                                         EbSdbCursor          *cursor,
+                                         gint                 *total,
+                                         gint                 *position,
+                                         GError              **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+       g_return_val_if_fail (cursor != NULL, FALSE);
+
+       /* If we're in a clear cursor state, then the position is 0 */
+       if (position && cursor->state[STATE_CURRENT].values[0] == NULL) {
+               *position = 0;
+
+               /* Mark the local pointer NULL, no need to calculate this anymore */
+               position = NULL;
+       }
+
+       /* Early return if there is nothing to do */
+       if (!total && !position)
+               return TRUE;
+
+       LOCK_MUTEX (&ebsdb->priv->lock);
+
+       if (!book_backend_sqlitedb_start_transaction (ebsdb, error)) {
+               UNLOCK_MUTEX (&ebsdb->priv->lock);
+               return FALSE;
+       }
+
+       if (total)
+               success = cursor_count_total_locked (ebsdb, cursor, total, error);
+
+       if (success && position)
+               success = cursor_count_position_locked (ebsdb, cursor, position, error);
+
+       if (success)
+               success = book_backend_sqlitedb_commit_transaction (ebsdb, error);
+       else
+               /* The GError is already set. */
+               book_backend_sqlitedb_rollback_transaction (ebsdb, NULL);
+
+       UNLOCK_MUTEX (&ebsdb->priv->lock);
 
        return success;
 }
+
+/**
+ * e_book_backend_sqlitedb_cursor_compare:
+ * @ebsdb: An #EBookBackendSqliteDB
+ * @cursor: The #EbSdbCursor
+ * @contact: The #EContact to compare
+ * @matches_sexp: (out) (allow-none): 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.
+ */
+gint
+e_book_backend_sqlitedb_cursor_compare (EBookBackendSqliteDB *ebsdb,
+                                       EbSdbCursor          *cursor,
+                                       EContact             *contact,
+                                       gboolean             *matches_sexp)
+{
+       EBookBackendSqliteDBPrivate *priv;
+       gint i;
+       gint comparison = 0;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_SQLITEDB (ebsdb), FALSE);
+       g_return_val_if_fail (cursor != NULL, FALSE);
+
+       priv = ebsdb->priv;
+
+       if (matches_sexp) {
+               if (cursor->sexp == NULL)
+                       *matches_sexp = TRUE;
+               else
+                       *matches_sexp =
+                               e_book_backend_sexp_match_contact (cursor->sexp, contact);
+       }
+
+       for (i = 0; i < cursor->n_sort_fields && comparison == 0; i++) {
+
+               /* Empty state sorts below any contact value, which means the contact sorts above cursor */
+               if (cursor->state[STATE_CURRENT].values[i] == NULL) {
+                       comparison = 1;
+               } else {
+                       const gchar *field_value;
+
+                       field_value = (const gchar *)
+                               e_contact_get_const (contact, cursor->sort_fields[i]);
+
+                       /* Empty contact state sorts below any cursor value */
+                       if (field_value == NULL)
+                               comparison = -1;
+                       else {
+                               gchar *collation_key;
+
+                               /* Check of contact sorts below, equal to, or above the cursor */
+                               collation_key = e_collator_generate_key (priv->collator, field_value, NULL);
+                               comparison = strcmp (collation_key, cursor->state[STATE_CURRENT].values[i]);
+                               g_free (collation_key);
+                       }
+               }
+       }
+
+       /* UID tie-breaker */
+       if (comparison == 0) {
+               const gchar *uid;
+
+               uid = (const gchar *)e_contact_get_const (contact, E_CONTACT_UID);
+
+               if (cursor->state[STATE_CURRENT].last_uid == NULL)
+                       comparison = 1;
+               else if (uid == NULL)
+                       comparison = -1;
+               else
+                       comparison = strcmp (uid, cursor->state[STATE_CURRENT].last_uid);
+       }
+
+       return comparison;
+}
diff --git a/addressbook/libedata-book/e-book-backend-sqlitedb.h 
b/addressbook/libedata-book/e-book-backend-sqlitedb.h
index f6b3433..b058bda 100644
--- a/addressbook/libedata-book/e-book-backend-sqlitedb.h
+++ b/addressbook/libedata-book/e-book-backend-sqlitedb.h
@@ -112,6 +112,34 @@ typedef struct {
        gchar *bdata;
 } EbSdbSearchData;
 
+/**
+ * EbSdbCuror:
+ *
+ * An opaque cursor pointer
+ *
+ * Since: 3.10
+ */
+typedef struct _EbSdbCursor EbSdbCursor;
+
+/**
+ * EbSdbCurorOrigin:
+ * @EBSDB_CURSOR_ORIGIN_CURRENT:  The current cursor position
+ * @EBSDB_CURSOR_ORIGIN_PREVIOUS: The previously recorded cursor position, this can be used to repeat the 
previous query
+ * @EBSDB_CURSOR_ORIGIN_RESET:    The beginning of the cursor results (or end of the results, if navigating 
in reverse).
+ *
+ * Defines the behaviour of e_book_backend_sqlitedb_cursor_move_by().
+ *
+ * The cursor always saves the previous cursor position as well as
+ * the new cursor position after performing a move. This allows
+ * cursor queries to be repeated in the case where content may have
+ * changed but the same content window should be refreshed in a UI.
+ */
+typedef enum {
+       EBSDB_CURSOR_ORIGIN_CURRENT,
+       EBSDB_CURSOR_ORIGIN_PREVIOUS,
+       EBSDB_CURSOR_ORIGIN_RESET
+} EbSdbCurorOrigin;
+
 GType          e_book_backend_sqlitedb_get_type
                                                (void) G_GNUC_CONST;
 GQuark          e_book_backend_sqlitedb_error_quark
@@ -139,6 +167,8 @@ gboolean    e_book_backend_sqlitedb_unlock_updates
                                                (EBookBackendSqliteDB *ebsdb,
                                                 gboolean do_commit,
                                                 GError **error);
+ECollator      *e_book_backend_sqlitedb_ref_collator
+                                                 (EBookBackendSqliteDB *ebsdb);
 gboolean       e_book_backend_sqlitedb_new_contact
                                                (EBookBackendSqliteDB *ebsdb,
                                                 const gchar *folderid,
@@ -276,6 +306,56 @@ gboolean        e_book_backend_sqlitedb_check_summary_query
 gboolean        e_book_backend_sqlitedb_check_summary_fields
                                                 (EBookBackendSqliteDB *ebsdb,
                                                 GHashTable *fields_of_interest);
+gboolean        e_book_backend_sqlitedb_set_locale
+                                                (EBookBackendSqliteDB *ebsdb,
+                                                const gchar          *folderid,
+                                                const gchar          *lc_collate,
+                                                GError              **error);
+gboolean        e_book_backend_sqlitedb_get_locale
+                                                (EBookBackendSqliteDB *ebsdb,
+                                                const gchar          *folderid,
+                                                gchar               **locale_out,
+                                                GError              **error);
+
+/* Cursor API */
+EbSdbCursor    *e_book_backend_sqlitedb_cursor_new
+                                                (EBookBackendSqliteDB *ebsdb,
+                                                const gchar          *folderid,
+                                                const gchar          *sexp,
+                                                EContactField        *sort_fields,
+                                                EBookSortType        *sort_types,
+                                                guint                 n_sort_fields,
+                                                GError              **error);
+void            e_book_backend_sqlitedb_cursor_free
+                                                (EBookBackendSqliteDB *ebsdb,
+                                                EbSdbCursor          *cursor);
+gboolean        e_book_backend_sqlitedb_cursor_move_by
+                                                (EBookBackendSqliteDB *ebsdb,
+                                                EbSdbCursor          *cursor,
+                                                EbSdbCurorOrigin      origin,
+                                                gint                  count,
+                                                GSList              **results,
+                                                GError              **error);
+void            e_book_backend_sqlitedb_cursor_set_target_alphabetic_index
+                                                (EBookBackendSqliteDB *ebsdb,
+                                                EbSdbCursor          *cursor,
+                                                gint                  index);
+gboolean        e_book_backend_sqlitedb_cursor_set_sexp
+                                                (EBookBackendSqliteDB *ebsdb,
+                                                EbSdbCursor          *cursor,
+                                                const gchar          *sexp,
+                                                GError              **error);
+gboolean        e_book_backend_sqlitedb_cursor_calculate
+                                                (EBookBackendSqliteDB *ebsdb,
+                                                EbSdbCursor          *cursor,
+                                                gint                 *total,
+                                                gint                 *position,
+                                                GError              **error);
+gint            e_book_backend_sqlitedb_cursor_compare
+                                                (EBookBackendSqliteDB *ebsdb,
+                                                EbSdbCursor          *cursor,
+                                                EContact             *contact,
+                                                gboolean             *matches_sexp);
 
 #ifndef EDS_DISABLE_DEPRECATED
 gboolean       e_book_backend_sqlitedb_is_summary_query


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