[shotwell/wip/phako/new-database: 2/3] WIP



commit e397539fcb77db58632f5ef1a14f2c650b42c660
Author: Jens Georg <mail jensge org>
Date:   Thu Feb 20 23:39:36 2020 +0100

    WIP

 src/db/DatabaseTable.vala               |  87 +++------
 src/db/VideoTable.vala                  | 116 ++++++------
 src/db/librygel-db/collate.c            |  49 +++++
 src/db/librygel-db/database-cursor.vala | 195 +++++++++++++++++++
 src/db/librygel-db/database.vala        | 319 ++++++++++++++++++++++++++++++++
 src/db/librygel-db/meson.build          |  22 +++
 src/db/librygel-db/rygel-db-2.6.deps    |   2 +
 src/db/librygel-db/sql-function.vala    |  31 ++++
 src/db/librygel-db/sql-operator.vala    |  71 +++++++
 src/meson.build                         |   3 +
 10 files changed, 778 insertions(+), 117 deletions(-)
---
diff --git a/src/db/DatabaseTable.vala b/src/db/DatabaseTable.vala
index 6e1a5b2f..6bac30e8 100644
--- a/src/db/DatabaseTable.vala
+++ b/src/db/DatabaseTable.vala
@@ -23,60 +23,44 @@ public abstract class DatabaseTable {
      ***/
     public const int SCHEMA_VERSION = 22;
 
-    protected static Sqlite.Database db;
+    protected static Rygel.Database.Database db;
 
     private static int in_transaction = 0;
 
     public string table_name = null;
 
     private static void prepare_db(string filename) {
-        // Open DB.
-        int res = Sqlite.Database.open_v2(filename, out db, Sqlite.OPEN_READWRITE | Sqlite.OPEN_CREATE, 
-            null);
-        if (res != Sqlite.OK)
-            AppWindow.panic(_("Unable to open/create photo database %s: error code %d").printf(filename,
-                res));
-        
-        // Check if we have write access to database.
-        if (filename != Db.IN_MEMORY_NAME) {
-            try {
-                File file_db = File.new_for_path(filename);
-                FileInfo info = file_db.query_info(FileAttribute.ACCESS_CAN_WRITE, FileQueryInfoFlags.NONE);
-                if (!info.get_attribute_boolean(FileAttribute.ACCESS_CAN_WRITE))
-                    AppWindow.panic(_("Unable to write to photo database file:\n %s").printf(filename));
-            } catch (Error e) {
-                AppWindow.panic(_("Error accessing database file:\n %s\n\nError was: \n%s").printf(filename,
-                    e.message));
+        try {
+            db = new Rygel.Database.Database(filename, Rygel.Database.Flavor.FOREIGN);
+            // Check if we have write access to database.
+            if (filename != Db.IN_MEMORY_NAME) {
+                try {
+                    File file_db = File.new_for_path(filename);
+                    FileInfo info = file_db.query_info(FileAttribute.ACCESS_CAN_WRITE, 
FileQueryInfoFlags.NONE);
+                    if (!info.get_attribute_boolean(FileAttribute.ACCESS_CAN_WRITE))
+                        AppWindow.panic(_("Unable to write to photo database file:\n %s").printf(filename));
+                } catch (Error e) {
+                    AppWindow.panic(_("Error accessing database file:\n %s\n\nError was: 
\n%s").printf(filename,
+                                e.message));
+                }
             }
-        }
-
-        unowned string? sql_debug = Environment.get_variable
-                                                         ("SHOTWELL_SQL_DEBUG");
-
-        if (sql_debug != null && sql_debug != "") {
-            db.trace (on_trace);
+        } catch (Rygel.Database.DatabaseError err) {
+            AppWindow.panic(_("Unable to open/create photo database %s: error code %s").printf(filename, 
err.message));
         }
     }
 
-    public static void on_trace (string message) {
-        debug ("SQLITE: %s", message);
-    }
-
     public static void init(string filename) {
         // Open DB.
         prepare_db(filename);
-        
-        // Try a query to make sure DB is intact; if not, try to use the backup
-        Sqlite.Statement stmt;
-        int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS VersionTable ("
-            + "id INTEGER PRIMARY KEY, "
-            + "schema_version INTEGER, "
-            + "app_version TEXT, "
-            + "user_data TEXT NULL"
-            + ")", -1, out stmt);
 
-        // Query on db failed, copy over backup and open it
-        if(res != Sqlite.OK) {
+        try {
+            db.exec("CREATE TABLE IF NOT EXISTS VersionTable ("
+                    + "id INTEGER PRIMARY KEY, "
+                    + "schema_version INTEGER, "
+                    + "app_version TEXT, "
+                    + "user_data TEXT NULL"
+                    + ")");
+        } catch (Rygel.Database.DatabaseError err) {
             db = null;
             
             string backup_path = filename + ".bak";
@@ -92,12 +76,6 @@ public abstract class DatabaseTable {
                 AppWindow.panic(_("Unable to restore photo database %s").printf(error.message));
             }
         }
-
-        // disable synchronized commits for performance reasons ... this is not vital, hence we
-        // don't error out if this fails
-        res = db.exec("PRAGMA synchronous=OFF");
-        if (res != Sqlite.OK)
-            warning("Unable to disable synchronous mode", res);
     }
     
     public static void terminate() {
@@ -119,6 +97,7 @@ public abstract class DatabaseTable {
         this.table_name = table_name;
     }
     
+    #if 0
     // This method will throw an error on an SQLite return code unless it's OK, DONE, or ROW, which
     // are considered normal results.
     protected static void throw_error(string method, int res) throws DatabaseError {
@@ -168,20 +147,14 @@ public abstract class DatabaseTable {
                 throw new DatabaseError.ERROR(msg);
         }
     }
+    #endif
     
     protected bool exists_by_id(int64 id) {
-        Sqlite.Statement stmt;
-        int res = db.prepare_v2("SELECT id FROM %s WHERE id=?".printf(table_name), -1, out stmt);
-        assert(res == Sqlite.OK);
-        
-        res = stmt.bind_int64(1, id);
-        assert(res == Sqlite.OK);
-        
-        res = stmt.step();
-        if (res != Sqlite.ROW && res != Sqlite.DONE)
+        try {
+            return db.query_value("SELECT count(1) FROM %s where id=?", {(Value)id}) != 0;
+        } catch (Rygel.Database.DatabaseError err) {
             fatal("exists_by_id [%s] %s".printf(id.to_string(), table_name), res);
-        
-        return (res == Sqlite.ROW);
+        }
     }
     
     protected bool select_by_id(int64 id, string columns, out Sqlite.Statement stmt) {
diff --git a/src/db/VideoTable.vala b/src/db/VideoTable.vala
index 7bd1bb70..8f7cfa6f 100644
--- a/src/db/VideoTable.vala
+++ b/src/db/VideoTable.vala
@@ -60,43 +60,35 @@ public class VideoTable : DatabaseTable {
     private static VideoTable instance = null;
     
     private VideoTable() {
-        Sqlite.Statement stmt;
-        int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS VideoTable ("
-            + "id INTEGER PRIMARY KEY, "
-            + "filename TEXT UNIQUE NOT NULL, "
-            + "width INTEGER, "
-            + "height INTEGER, "
-            + "clip_duration REAL, "
-            + "is_interpretable INTEGER, "
-            + "filesize INTEGER, "
-            + "timestamp INTEGER, "
-            + "exposure_time INTEGER, "
-            + "import_id INTEGER, "
-            + "event_id INTEGER, "
-            + "md5 TEXT, "
-            + "time_created INTEGER, "
-            + "rating INTEGER DEFAULT 0, "
-            + "title TEXT, "
-            + "backlinks TEXT, "
-            + "time_reimported INTEGER, "
-            + "flags INTEGER DEFAULT 0, "
-               + "comment TEXT "
-            + ")", -1, out stmt);
-        assert(res == Sqlite.OK);
+        try {
+            db.begin();
+            db.exec("CREATE TABLE IF NOT EXISTS VideoTable ("
+                    + "id INTEGER PRIMARY KEY, "
+                    + "filename TEXT UNIQUE NOT NULL, "
+                    + "width INTEGER, "
+                    + "height INTEGER, "
+                    + "clip_duration REAL, "
+                    + "is_interpretable INTEGER, "
+                    + "filesize INTEGER, "
+                    + "timestamp INTEGER, "
+                    + "exposure_time INTEGER, "
+                    + "import_id INTEGER, "
+                    + "event_id INTEGER, "
+                    + "md5 TEXT, "
+                    + "time_created INTEGER, "
+                    + "rating INTEGER DEFAULT 0, "
+                    + "title TEXT, "
+                    + "backlinks TEXT, "
+                    + "time_reimported INTEGER, "
+                    + "flags INTEGER DEFAULT 0, "
+                    + "comment TEXT "
+                    + ")");
 
-        res = stmt.step();
-        if (res != Sqlite.DONE)
-            fatal("VideoTable constructor", res);
-        
-        // index on event_id
-        Sqlite.Statement stmt2;
-        int res2 = db.prepare_v2("CREATE INDEX IF NOT EXISTS VideoEventIDIndex ON VideoTable (event_id)",
-            -1, out stmt2);
-        assert(res2 == Sqlite.OK);
-
-        res2 = stmt2.step();
-        if (res2 != Sqlite.DONE)
-            fatal("VideoTable constructor", res2);
+            db.exec("CREATE INDEX IF NOT EXISTS VideoEventIDIndex ON VideoTable (event_id)");
+            db.commit();
+        } catch (Rygel.Database.DatabaseError err) {
+            error("VideoTable: %s", err.message);
+        }
 
         set_table_name("VideoTable");
     }
@@ -111,14 +103,15 @@ public class VideoTable : DatabaseTable {
     // VideoRow.video_id, event_id, time_created are ignored on input. All fields are set on exit
     // with values stored in the database.
     public VideoID add(VideoRow video_row) throws DatabaseError {
-        Sqlite.Statement stmt;
-        int res = db.prepare_v2(
+        db.exec(
             "INSERT INTO VideoTable (filename, width, height, clip_duration, is_interpretable, "
             + "filesize, timestamp, exposure_time, import_id, event_id, md5, time_created, title, comment) "
             + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
-            -1, out stmt);
-        assert(res == Sqlite.OK);
+            { (GLib.Value)video_row.filepath,
+              (GLib.Value) video_row.width,
+              (GLib.Value) video_row.height});
         
+        #if 0
         ulong time_created = now_sec();
         
         res = stmt.bind_text(1, video_row.filepath);
@@ -161,36 +154,39 @@ public class VideoTable : DatabaseTable {
         video_row.event_id = EventID();
         video_row.time_created = (time_t) time_created;
         video_row.flags = 0;
-        
+       #endif 
         return video_row.video_id;
     }
     
     public bool drop_event(EventID event_id) {
-        Sqlite.Statement stmt;
-        int res = db.prepare_v2("UPDATE VideoTable SET event_id = ? WHERE event_id = ?", -1, out stmt);
-        assert(res == Sqlite.OK);
-        
-        res = stmt.bind_int64(1, EventID.INVALID);
-        assert(res == Sqlite.OK);
-        res = stmt.bind_int64(2, event_id.id);
-        assert(res == Sqlite.OK);
-        
-        res = stmt.step();
-        if (res != Sqlite.DONE) {
-            fatal("VideoTable.drop_event", res);
-            
-            return false;
+        try {
+            db.exec("UPDATE VideoTable SET event_id = ? WHERE event_id = ?",
+                    {(GLib.Value) EventID.INVALID, (GLib.Value) event_id.id });
+
+            return true;
+        } catch (Rygel.Database.DatabaseError err) {
+            error("VideoTable.drop_event: %s", err.message);
         }
-        
-        return true;
+
+        return false;
     }
 
     public VideoRow? get_row(VideoID video_id) {
-        Sqlite.Statement stmt;
-        int res = db.prepare_v2(
+        try {
+            var cursor = db.exec_cursor(
             "SELECT filename, width, height, clip_duration, is_interpretable, filesize, timestamp, "
             + "exposure_time, import_id, event_id, md5, time_created, rating, title, backlinks, "
-            + "time_reimported, flags, comment FROM VideoTable WHERE id=?", 
+            + "time_reimported, flags, comment FROM VideoTable WHERE id=?",
+            {(GLib.Value) video_id.id});
+            var column = 0;
+            VideoRow = new VideoRow();
+            row.video_id = cursor.next();
+        } catch (Rygel.Database.DatabaseError err) {
+            return null;
+        }
+
+        Sqlite.Statement stmt;
+        int res = db.prepare_v2, 
             -1, out stmt);
         assert(res == Sqlite.OK);
         
diff --git a/src/db/librygel-db/collate.c b/src/db/librygel-db/collate.c
new file mode 100644
index 00000000..210ba4d7
--- /dev/null
+++ b/src/db/librygel-db/collate.c
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel 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; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Rygel 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include <glib.h>
+
+#if HAVE_UNISTRING
+#   include <unistr.h>
+#endif
+
+gint rygel_database_utf8_collate_str (const char *a, gsize alen,
+                                      const char *b, gsize blen)
+{
+    char *a_str, *b_str;
+    gint result;
+
+    /* Make sure the passed strings are null terminated */
+    a_str = g_strndup (a, alen);
+    b_str = g_strndup (b, blen);
+
+#if HAVE_UNISTRING
+    result = u8_strcoll ((const uint8_t *) a_str, (const uint8_t *) b_str);
+#else
+    return g_utf8_collate (a_str, b_str);
+#endif
+
+    g_free (a_str);
+    g_free (b_str);
+
+    return result;
+}
diff --git a/src/db/librygel-db/database-cursor.vala b/src/db/librygel-db/database-cursor.vala
new file mode 100644
index 00000000..682f7cfc
--- /dev/null
+++ b/src/db/librygel-db/database-cursor.vala
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2011 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel 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; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Rygel 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+using Sqlite;
+
+public class Rygel.Database.Cursor : Object {
+    private Statement statement;
+    private int current_state = -1;
+    private bool dirty = true;
+    private unowned Sqlite.Database db;
+
+    /**
+     * Prepare a SQLite statement from a SQL string
+     *
+     * If @arguments is non-null, it will call bind()
+     *
+     * @param db SQLite database this cursor belongs to
+     * @param sql statement to execute
+     * @param arguments array of values to bind to the SQL statement or null if
+     * none
+     */
+    public Cursor (Sqlite.Database   db,
+                   string            sql,
+                   GLib.Value[]?     arguments) throws DatabaseError {
+        this.db = db;
+
+        this.throw_if_code_is_error (db.prepare_v2 (sql,
+                                                    -1,
+                                                    out this.statement,
+                                                    null));
+        if (arguments == null) {
+            return;
+        }
+
+        this.bind (arguments);
+    }
+
+    /**
+     * Bind new values to a cursor.
+     *
+     * The cursor will be reset.
+     *
+     * This function uses the type of the GValue passed in values to determine
+     * which _bind function to use.
+     *
+     * Supported types are: int, long, int64, uint64, string and pointer.
+     * Note: The only pointer supported is the null pointer as provided by
+     * Database.@null. This is a special value to bind a column to NULL
+     * @param arguments array of values to bind to the SQL statement or null if
+     * none
+     */
+    public void bind (GLib.Value[]? arguments) throws DatabaseError {
+        this.statement.reset ();
+        this.dirty = true;
+        this.current_state = -1;
+
+        for (var i = 1; i <= arguments.length; ++i) {
+            unowned GLib.Value current_value = arguments[i - 1];
+
+            if (current_value.holds (typeof (int))) {
+                statement.bind_int (i, current_value.get_int ());
+            } else if (current_value.holds (typeof (int64))) {
+                statement.bind_int64 (i, current_value.get_int64 ());
+            } else if (current_value.holds (typeof (uint64))) {
+                statement.bind_int64 (i, (int64) current_value.get_uint64 ());
+            } else if (current_value.holds (typeof (long))) {
+                statement.bind_int64 (i, current_value.get_long ());
+            } else if (current_value.holds (typeof (uint))) {
+                statement.bind_int64 (i, current_value.get_uint ());
+            } else if (current_value.holds (typeof (string))) {
+                statement.bind_text (i, current_value.get_string ());
+            } else if (current_value.holds (typeof (void *))) {
+                if (current_value.peek_pointer () == null) {
+                    statement.bind_null (i);
+                } else {
+                    assert_not_reached ();
+                }
+            } else {
+                var type = current_value.type ();
+                warning (_("Unsupported type %s"), type.name ());
+                assert_not_reached ();
+            }
+
+            if (this.db.errcode () != Sqlite.OK) {
+                throw new DatabaseError.BIND ("Failed to bind value %d in %s: %s",
+                                              i,
+                                              this.statement.sql (),
+                                              this.db.errmsg ());
+            }
+        }
+    }
+
+    /**
+     * Check if the cursor has more rows left
+     *
+     * @return true if more rows left, false otherwise
+     */
+    public bool has_next () throws DatabaseError {
+        if (this.dirty) {
+            this.current_state = this.statement.step ();
+            this.dirty = false;
+        }
+
+        this.throw_if_code_is_error (this.current_state);
+
+        return this.current_state == Sqlite.ROW || this.current_state == -1;
+    }
+
+    /**
+     * Get the next row of this cursor.
+     *
+     * This function uses pointers instead of unowned because var doesn't work
+     * with unowned.
+     *
+     * @return a pointer to the current row
+     */
+    public Statement* next () throws DatabaseError {
+        this.has_next ();
+        this.throw_if_code_is_error (this.current_state);
+        this.dirty = true;
+
+        return this.statement;
+    }
+
+    // convenience functions for "foreach"
+
+    /**
+     * Return an iterator to the cursor to use with foreach
+     *
+     * @return an iterator wrapping the cursor
+     */
+    public Iterator iterator () {
+        return new Iterator (this);
+    }
+
+    public class Iterator {
+        public Cursor cursor;
+
+        public Iterator (Cursor cursor) {
+            this.cursor = cursor;
+        }
+
+        public bool next () throws DatabaseError {
+            return this.cursor.has_next ();
+        }
+
+        public unowned Statement @get () throws DatabaseError {
+            return this.cursor.next ();
+        }
+    }
+
+    /**
+     * Convert a SQLite return code to a DatabaseError
+     */
+    protected void throw_if_code_is_error (int sqlite_error)
+                                           throws DatabaseError {
+        switch (sqlite_error) {
+            case Sqlite.OK:
+            case Sqlite.DONE:
+            case Sqlite.ROW:
+                return;
+            default:
+                throw new DatabaseError.SQLITE_ERROR
+                                        ("SQLite error %d: %s",
+                                         sqlite_error,
+                                         this.db.errmsg ());
+        }
+    }
+
+    /**
+     * Check if the last operation on the database was an error
+     */
+    protected void throw_if_db_has_error () throws DatabaseError {
+        this.throw_if_code_is_error (this.db.errcode ());
+    }
+}
diff --git a/src/db/librygel-db/database.vala b/src/db/librygel-db/database.vala
new file mode 100644
index 00000000..6fa34b67
--- /dev/null
+++ b/src/db/librygel-db/database.vala
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2009,2011 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel 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; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Rygel 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+using Sqlite;
+
+namespace Rygel.Database {
+
+    public errordomain DatabaseError {
+        SQLITE_ERROR, /// Error code translated from SQLite
+        OPEN,         /// Error while opening database file
+        PREPARE,      /// Error while preparing a statement
+        BIND,         /// Error while binding values to a statement
+        STEP          /// Error while running through a result set
+    }
+
+    public enum Flavor {
+        CACHE,  /// Database is a cache (will be placed in XDG_USER_CACHE
+        CONFIG, /// Database is configuration (will be placed in XDG_USER_CONFIG)
+        FOREIGN /// Database is at a custom location
+    }
+
+    public enum Flags {
+        READ_ONLY = 1, /// Database is read-only
+        WRITE_ONLY = 1 << 1, /// Database is write-only
+        /// Database can be read and updated
+        READ_WRITE = READ_ONLY | WRITE_ONLY,
+
+        /// Database is shared between several processes
+        SHARED = 1 << 2;
+    }
+
+    /// Prototype for UTF-8 collation function
+    extern static int utf8_collate_str (uint8[] a, uint8[] b);
+
+    /**
+     * Special GValue to pass to exec or exec_cursor to bind a column to
+     * NULL
+     */
+    public static GLib.Value @null () {
+        GLib.Value v = GLib.Value (typeof (void *));
+        v.set_pointer (null);
+
+        return v;
+    }
+}
+
+/**
+ * This class is a thin wrapper around SQLite's database object.
+ *
+ * It adds statement preparation based on GValue and a cancellable exec
+ * function.
+ */
+public class Rygel.Database.Database : Object, Initable {
+
+    public string name { private get; construct set; }
+    public Flavor flavor { private get; construct set; default = Flavor.CACHE; }
+    public Flags  flags {
+        private get;
+        construct set;
+        default = Flags.READ_WRITE;
+    }
+
+    /**
+     * Function to implement the custom SQL function 'contains'
+     */
+    public static void utf8_contains (Sqlite.Context context,
+                                      Sqlite.Value[] args)
+                                      requires (args.length == 2) {
+        if (args[0].to_text () == null ||
+            args[1].to_text () == null) {
+           context.result_int (0);
+
+           return;
+        }
+
+        var pattern = Regex.escape_string (args[1].to_text ());
+        if (Regex.match_simple (pattern,
+                                args[0].to_text (),
+                                RegexCompileFlags.CASELESS)) {
+            context.result_int (1);
+        } else {
+            context.result_int (0);
+        }
+    }
+
+    /**
+     * Function to implement the custom SQLite collation 'CASEFOLD'.
+     *
+     * Uses utf8 case-fold to compare the strings.
+     */
+    public static int utf8_collate (int alen, void* a, int blen, void* b) {
+        // unowned to prevent array copy
+        unowned uint8[] _a = (uint8[]) a;
+        _a.length = alen;
+
+        unowned uint8[] _b = (uint8[]) b;
+        _b.length = blen;
+
+        return utf8_collate_str (_a, _b);
+    }
+
+    private string build_path () {
+        var name_is_path = this.name == ":memory:" ||
+                           Path.is_absolute (this.name) ||
+                           this.flavor == Flavor.FOREIGN;
+
+        if (!name_is_path) {
+            var dirname = Path.build_filename (
+                                        this.flavor == Flavor.CACHE
+                                            ? Environment.get_user_cache_dir ()
+                                            : Environment.get_user_config_dir (),
+                                        "rygel");
+            DirUtils.create_with_parents (dirname, 0750);
+
+            return Path.build_filename (dirname, "%s.db".printf (this.name));
+        } else {
+            this.flavor = Flavor.FOREIGN;
+
+            return this.name;
+        }
+    }
+
+    private Sqlite.Database db;
+
+    /**
+     * Connect to a SQLite database file
+     *
+     * @param name Name of the database which is used to create the file-name
+     * @param flavor Specifies the flavor of the database
+     * @param flags How to open the database
+     */
+    public Database (string name,
+                     Flavor flavor = Flavor.CACHE,
+                     Flags  flags = Flags.READ_WRITE)
+                     throws DatabaseError, Error {
+        Object (name : name, flavor : flavor, flags : flags);
+        init ();
+    }
+
+    /**
+     * Initialize database. Implemented for Initiable interface.
+     *
+     * @param cancellable a cancellable (unused)
+     * @return true on success, false on error
+     * @throws DatabaseError if anything goes wrong
+     */
+    public bool init (Cancellable? cancellable = null) throws Error {
+        var path = this.build_path ();
+        if (flags == Flags.READ_ONLY) {
+            Sqlite.Database.open_v2 (path, out this.db, Sqlite.OPEN_READONLY);
+        } else {
+            Sqlite.Database.open (path, out this.db);
+        }
+
+        if (this.db.errcode () != Sqlite.OK) {
+            var msg = _("Error while opening SQLite database %s: %s");
+            throw new DatabaseError.OPEN (msg, path, this.db.errmsg ());
+        }
+
+        debug ("Using database file %s", path);
+
+        if (flags != Flags.READ_ONLY) {
+            this.exec ("PRAGMA synchronous = OFF");
+        }
+
+        if (Flags.SHARED in flags) {
+            this.exec ("PRAGMA journal_mode = WAL");
+        } else {
+            this.exec ("PRAGMA temp_store = MEMORY");
+        }
+
+        this.db.create_function ("contains",
+                                 2,
+                                 Sqlite.UTF8,
+                                 null,
+                                 Database.utf8_contains,
+                                 null,
+                                 null);
+
+        this.db.create_collation ("CASEFOLD",
+                                  Sqlite.UTF8,
+                                  Database.utf8_collate);
+
+        unowned string? sql_debug = Environment.get_variable
+                                                         ("SHOTWELL_SQL_DEBUG");
+
+        if (sql_debug != null && sql_debug != "") {
+            this.db.trace (on_trace);
+        }
+
+        return true;
+    }
+
+    private void on_trace (string message) {
+        debug ("SQLITE: %s", message);
+    }
+
+    /**
+     * SQL query function.
+     *
+     * Use for all queries that return a result set.
+     *
+     * @param sql The SQL query to run.
+     * @param arguments Values to bind in the SQL query or null.
+     * @throws DatabaseError if the underlying SQLite operation fails.
+     */
+    public Cursor exec_cursor (string        sql,
+                               GLib.Value[]? arguments = null)
+                               throws DatabaseError {
+        return new Cursor (this.db, sql, arguments);
+    }
+
+    /**
+     * Simple SQL query execution function.
+     *
+     * Use for all queries that don't return anything.
+     *
+     * @param sql The SQL query to run.
+     * @param arguments Values to bind in the SQL query or null.
+     * @throws DatabaseError if the underlying SQLite operation fails.
+     */
+    public void exec (string        sql,
+                      GLib.Value[]? arguments = null)
+                      throws DatabaseError {
+        if (arguments == null) {
+            this.db.exec (sql);
+            if (this.db.errcode () != Sqlite.OK) {
+                var msg = "Failed to run query %s: %s";
+                throw new DatabaseError.SQLITE_ERROR (msg, sql, this.db.errmsg ());
+            }
+
+            return;
+        }
+
+        var cursor = this.exec_cursor (sql, arguments);
+        while (cursor.has_next ()) {
+            cursor.next ();
+        }
+    }
+
+    /**
+     * Execute a SQL query that returns a single number.
+     *
+     * @param sql The SQL query to run.
+     * @param args Values to bind in the SQL query or null.
+     * @return The contents of the first row's column as an int.
+     * @throws DatabaseError if the underlying SQLite operation fails.
+     */
+    public int query_value (string        sql,
+                             GLib.Value[]? args = null)
+                             throws DatabaseError {
+        var cursor = this.exec_cursor (sql, args);
+        var statement = cursor.next ();
+        return statement->column_int (0);
+    }
+
+    /**
+     * Analyze triggers of database
+     */
+    public void analyze () {
+        this.db.exec ("ANALYZE");
+    }
+
+    /**
+     * Start a transaction
+     */
+    public void begin () throws DatabaseError {
+        this.exec ("BEGIN");
+    }
+
+    /**
+     * Commit a transaction
+     */
+    public void commit () throws DatabaseError {
+        this.exec ("COMMIT");
+    }
+
+    /**
+     * Rollback a transaction
+     */
+    public void rollback () {
+        try {
+            this.exec ("ROLLBACK");
+        } catch (DatabaseError error) {
+            critical (_("Failed to roll back transaction: %s"),
+                      error.message);
+        }
+    }
+
+    /**
+     * Check for an empty SQLite database.
+     * @return true if the file is an empty SQLite database, false otherwise
+     * @throws DatabaseError if the SQLite meta table does not exist which
+     * usually indicates that the file is not a databsae
+     */
+    public bool is_empty () throws DatabaseError {
+        return this.query_value ("SELECT count(type) FROM " +
+                                 "sqlite_master WHERE rowid = 1") == 0;
+    }
+}
diff --git a/src/db/librygel-db/meson.build b/src/db/librygel-db/meson.build
new file mode 100644
index 00000000..ec7eaf3e
--- /dev/null
+++ b/src/db/librygel-db/meson.build
@@ -0,0 +1,22 @@
+db_sources = files(
+    'database-cursor.vala',
+    'database.vala',
+    'sql-function.vala',
+    'sql-operator.vala',
+    'collate.c'
+)
+
+db_lib = library('rygel-db-2.6', db_sources,
+        dependencies : db_deps + [rygel_core],
+        include_directories: [config_include, include_directories('.')],
+        version: '2.0.4',
+        c_args : ['-DG_LOG_DOMAIN="RygelDb"'],
+        vala_header : 'rygel-db.h',
+        install: true,
+        install_dir : [true, rygel_includedir, true])
+install_data('rygel-db-2.6.deps', install_dir : rygel_vapidir)
+
+# need to add to get the current build dir as include dir
+rygel_db = declare_dependency(include_directories: include_directories('.'),
+                                dependencies: db_deps,
+                                link_with: db_lib)
diff --git a/src/db/librygel-db/rygel-db-2.6.deps b/src/db/librygel-db/rygel-db-2.6.deps
new file mode 100644
index 00000000..eb088138
--- /dev/null
+++ b/src/db/librygel-db/rygel-db-2.6.deps
@@ -0,0 +1,2 @@
+sqlite3
+gupnp-av-1.0
diff --git a/src/db/librygel-db/sql-function.vala b/src/db/librygel-db/sql-function.vala
new file mode 100644
index 00000000..8f7213a5
--- /dev/null
+++ b/src/db/librygel-db/sql-function.vala
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2010 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel 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; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Rygel 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+public class Rygel.Database.SqlFunction : SqlOperator {
+    public SqlFunction (string name, string arg) {
+        base (name, arg);
+    }
+
+    public override string to_string () {
+        return "%s(%s,?)".printf (name, arg);
+    }
+}
diff --git a/src/db/librygel-db/sql-operator.vala b/src/db/librygel-db/sql-operator.vala
new file mode 100644
index 00000000..81e869d0
--- /dev/null
+++ b/src/db/librygel-db/sql-operator.vala
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 Jens Georg <mail jensge org>.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel 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; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Rygel 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+using GUPnP;
+
+public class Rygel.Database.SqlOperator : GLib.Object {
+    protected string name;
+    protected string arg;
+    protected string collate;
+
+    public SqlOperator (string name,
+                        string arg,
+                        string collate = "") {
+        this.name = name;
+        this.arg = arg;
+        this.collate = collate;
+    }
+
+    public SqlOperator.from_search_criteria_op (SearchCriteriaOp op,
+                                                string           arg,
+                                                string           collate) {
+        string sql = null;
+        switch (op) {
+            case SearchCriteriaOp.EQ:
+                sql = "=";
+                break;
+            case SearchCriteriaOp.NEQ:
+                sql = "!=";
+                break;
+            case SearchCriteriaOp.LESS:
+                sql = "<";
+                break;
+            case SearchCriteriaOp.LEQ:
+                sql = "<=";
+                break;
+            case SearchCriteriaOp.GREATER:
+                sql = ">";
+                break;
+            case SearchCriteriaOp.GEQ:
+                sql = ">=";
+                break;
+            default:
+                assert_not_reached ();
+        }
+
+        this (sql, arg, collate);
+    }
+
+    public virtual string to_string () {
+        return "(%s %s ? %s)".printf (arg, name, collate);
+    }
+}
diff --git a/src/meson.build b/src/meson.build
index b9729445..f2d04457 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -59,6 +59,9 @@ executable('shotwell',
             'threads/Workers.vala',
             'threads/BackgroundJob.vala',
             'threads/Semaphore.vala',
+            'db/librygel-db/database.vala',
+            'db/librygel-db/database-cursor.vala',
+            'db/librygel-db/collate.c',
             'db/Db.vala',
             'db/DatabaseTable.vala',
             'db/PhotoTable.vala',


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