summaryrefslogtreecommitdiff
path: root/src/db
diff options
context:
space:
mode:
Diffstat (limited to 'src/db')
-rw-r--r--src/db/DatabaseTable.vala384
-rw-r--r--src/db/Db.vala366
-rw-r--r--src/db/EventTable.vala235
-rw-r--r--src/db/PhotoTable.vala1245
-rw-r--r--src/db/SavedSearchDBTable.vala641
-rw-r--r--src/db/TagTable.vala248
-rw-r--r--src/db/TombstoneTable.vala146
-rw-r--r--src/db/VersionTable.vala98
-rw-r--r--src/db/VideoTable.vala462
-rw-r--r--src/db/mk/db.mk35
10 files changed, 3860 insertions, 0 deletions
diff --git a/src/db/DatabaseTable.vala b/src/db/DatabaseTable.vala
new file mode 100644
index 0000000..55d440d
--- /dev/null
+++ b/src/db/DatabaseTable.vala
@@ -0,0 +1,384 @@
+/* Copyright 2009-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public errordomain DatabaseError {
+ ERROR,
+ BACKING,
+ MEMORY,
+ ABORT,
+ LIMITS,
+ TYPESPEC
+}
+
+public abstract class DatabaseTable {
+ /***
+ * This number should be incremented every time any database schema is altered.
+ *
+ * NOTE: Adding or removing tables or removing columns do not need a new schema version, because
+ * tables are created on demand and tables and columns are easily ignored when already present.
+ * However, the change should be noted in upgrade_database() as a comment.
+ ***/
+ public const int SCHEMA_VERSION = 20;
+
+ protected static Sqlite.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));
+ }
+ }
+ }
+
+ 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) {
+ db = null;
+
+ string backup_path = filename + ".bak";
+ string cmdline = "cp " + backup_path + " " + filename;
+ Posix.system(cmdline);
+
+ prepare_db(filename);
+ }
+
+ // 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() {
+ // freeing the database closes it
+ db = null;
+ }
+
+ // XXX: errmsg() is global, and so this will not be accurate in a threaded situation
+ protected static void fatal(string op, int res) {
+ error("%s: [%d] %s", op, res, db.errmsg());
+ }
+
+ // XXX: errmsg() is global, and so this will not be accurate in a threaded situation
+ protected static void warning(string op, int res) {
+ GLib.warning("%s: [%d] %s", op, res, db.errmsg());
+ }
+
+ protected void set_table_name(string table_name) {
+ this.table_name = table_name;
+ }
+
+ // 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 {
+ string msg = "(%s) [%d] - %s".printf(method, res, db.errmsg());
+
+ switch (res) {
+ case Sqlite.OK:
+ case Sqlite.DONE:
+ case Sqlite.ROW:
+ return;
+
+ case Sqlite.PERM:
+ case Sqlite.BUSY:
+ case Sqlite.READONLY:
+ case Sqlite.IOERR:
+ case Sqlite.CORRUPT:
+ case Sqlite.CANTOPEN:
+ case Sqlite.NOLFS:
+ case Sqlite.AUTH:
+ case Sqlite.FORMAT:
+ case Sqlite.NOTADB:
+ throw new DatabaseError.BACKING(msg);
+
+ case Sqlite.NOMEM:
+ throw new DatabaseError.MEMORY(msg);
+
+ case Sqlite.ABORT:
+ case Sqlite.LOCKED:
+ case Sqlite.INTERRUPT:
+ throw new DatabaseError.ABORT(msg);
+
+ case Sqlite.FULL:
+ case Sqlite.EMPTY:
+ case Sqlite.TOOBIG:
+ case Sqlite.CONSTRAINT:
+ case Sqlite.RANGE:
+ throw new DatabaseError.LIMITS(msg);
+
+ case Sqlite.SCHEMA:
+ case Sqlite.MISMATCH:
+ throw new DatabaseError.TYPESPEC(msg);
+
+ case Sqlite.ERROR:
+ case Sqlite.INTERNAL:
+ case Sqlite.MISUSE:
+ default:
+ throw new DatabaseError.ERROR(msg);
+ }
+ }
+
+ 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)
+ 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) {
+ string sql = "SELECT %s FROM %s WHERE id=?".printf(columns, table_name);
+
+ int res = db.prepare_v2(sql, -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)
+ fatal("select_by_id [%s] %s %s".printf(id.to_string(), table_name, columns), res);
+
+ return (res == Sqlite.ROW);
+ }
+
+ // Caller needs to bind value #1 before calling execute_update_by_id()
+ private void prepare_update_by_id(int64 id, string column, out Sqlite.Statement stmt) {
+ string sql = "UPDATE %s SET %s=? WHERE id=?".printf(table_name, column);
+
+ int res = db.prepare_v2(sql, -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(2, id);
+ assert(res == Sqlite.OK);
+ }
+
+ private bool execute_update_by_id(Sqlite.Statement stmt) {
+ int res = stmt.step();
+ if (res != Sqlite.DONE) {
+ fatal("execute_update_by_id", res);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ protected bool update_text_by_id(int64 id, string column, string text) {
+ Sqlite.Statement stmt;
+ prepare_update_by_id(id, column, out stmt);
+
+ int res = stmt.bind_text(1, text);
+ assert(res == Sqlite.OK);
+
+ return execute_update_by_id(stmt);
+ }
+
+ protected void update_text_by_id_2(int64 id, string column, string text) throws DatabaseError {
+ Sqlite.Statement stmt;
+ prepare_update_by_id(id, column, out stmt);
+
+ int res = stmt.bind_text(1, text);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("DatabaseTable.update_text_by_id_2 %s.%s".printf(table_name, column), res);
+ }
+
+ protected bool update_int_by_id(int64 id, string column, int value) {
+ Sqlite.Statement stmt;
+ prepare_update_by_id(id, column, out stmt);
+
+ int res = stmt.bind_int(1, value);
+ assert(res == Sqlite.OK);
+
+ return execute_update_by_id(stmt);
+ }
+
+ protected void update_int_by_id_2(int64 id, string column, int value) throws DatabaseError {
+ Sqlite.Statement stmt;
+ prepare_update_by_id(id, column, out stmt);
+
+ int res = stmt.bind_int(1, value);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("DatabaseTable.update_int_by_id_2 %s.%s".printf(table_name, column), res);
+ }
+
+ protected bool update_int64_by_id(int64 id, string column, int64 value) {
+ Sqlite.Statement stmt;
+ prepare_update_by_id(id, column, out stmt);
+
+ int res = stmt.bind_int64(1, value);
+ assert(res == Sqlite.OK);
+
+ return execute_update_by_id(stmt);
+ }
+
+ protected void update_int64_by_id_2(int64 id, string column, int64 value) throws DatabaseError {
+ Sqlite.Statement stmt;
+ prepare_update_by_id(id, column, out stmt);
+
+ int res = stmt.bind_int64(1, value);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("DatabaseTable.update_int64_by_id_2 %s.%s".printf(table_name, column), res);
+ }
+
+ protected void delete_by_id(int64 id) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("DELETE 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.DONE)
+ throw_error("%s.remove".printf(table_name), res);
+ }
+
+ public static bool has_column(string table_name, string column_name) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("PRAGMA table_info(%s)".printf(table_name), -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ break;
+ } else if (res != Sqlite.ROW) {
+ fatal("has_column %s".printf(table_name), res);
+
+ break;
+ } else {
+ string column = stmt.column_text(1);
+ if (column != null && column == column_name)
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static bool has_table(string table_name) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("PRAGMA table_info(%s)".printf(table_name), -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+
+ return (res != Sqlite.DONE);
+ }
+
+ public static bool add_column(string table_name, string column_name, string column_constraints) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("ALTER TABLE %s ADD COLUMN %s %s".printf(table_name, column_name,
+ column_constraints), -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ critical("Unable to add column %s %s %s: (%d) %s", table_name, column_name, column_constraints,
+ res, db.errmsg());
+
+ return false;
+ }
+
+ return true;
+ }
+
+ // This method will only add the column if a table exists (relying on the table object
+ // to build a new one when first referenced) and only if the column does not exist. In essence,
+ // it's a cleaner way to run has_table(), has_column(), and add_column().
+ public static bool ensure_column(string table_name, string column_name, string column_constraints,
+ string upgrade_msg) {
+ if (!has_table(table_name) || has_column(table_name, column_name))
+ return true;
+
+ message("%s", upgrade_msg);
+
+ return add_column(table_name, column_name, column_constraints);
+ }
+
+ public int get_row_count() {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT COUNT(id) AS RowCount FROM %s".printf(table_name), -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.ROW) {
+ critical("Unable to retrieve row count on %s: (%d) %s", table_name, res, db.errmsg());
+
+ return 0;
+ }
+
+ return stmt.column_int(0);
+ }
+
+ // This is not thread-safe.
+ public static void begin_transaction() {
+ if (in_transaction++ != 0)
+ return;
+
+ int res = db.exec("BEGIN TRANSACTION");
+ assert(res == Sqlite.OK);
+ }
+
+ // This is not thread-safe.
+ public static void commit_transaction() throws DatabaseError {
+ assert(in_transaction > 0);
+ if (--in_transaction != 0)
+ return;
+
+ int res = db.exec("COMMIT TRANSACTION");
+ if (res != Sqlite.DONE)
+ throw_error("commit_transaction", res);
+ }
+}
+
diff --git a/src/db/Db.vala b/src/db/Db.vala
new file mode 100644
index 0000000..ced530a
--- /dev/null
+++ b/src/db/Db.vala
@@ -0,0 +1,366 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+namespace Db {
+
+public static const string IN_MEMORY_NAME = ":memory:";
+
+private string? filename = null;
+
+// Passing null as the db_file will create an in-memory, non-persistent database.
+public void preconfigure(File? db_file) {
+ filename = (db_file != null) ? db_file.get_path() : IN_MEMORY_NAME;
+}
+
+public void init() throws Error {
+ assert(filename != null);
+
+ DatabaseTable.init(filename);
+}
+
+public void terminate() {
+ DatabaseTable.terminate();
+}
+
+public enum VerifyResult {
+ OK,
+ FUTURE_VERSION,
+ UPGRADE_ERROR,
+ NO_UPGRADE_AVAILABLE
+}
+
+public VerifyResult verify_database(out string app_version, out int schema_version) {
+ VersionTable version_table = VersionTable.get_instance();
+ schema_version = version_table.get_version(out app_version);
+
+ if (schema_version >= 0)
+ debug("Database schema version %d created by app version %s", schema_version, app_version);
+
+ if (schema_version == -1) {
+ // no version set, do it now (tables will be created on demand)
+ debug("Creating database schema version %d for app version %s", DatabaseTable.SCHEMA_VERSION,
+ Resources.APP_VERSION);
+ version_table.set_version(DatabaseTable.SCHEMA_VERSION, Resources.APP_VERSION);
+ app_version = Resources.APP_VERSION;
+ schema_version = DatabaseTable.SCHEMA_VERSION;
+ } else if (schema_version > DatabaseTable.SCHEMA_VERSION) {
+ // Back to the future
+ return Db.VerifyResult.FUTURE_VERSION;
+ } else if (schema_version < DatabaseTable.SCHEMA_VERSION) {
+ // Past is present
+ VerifyResult result = upgrade_database(schema_version);
+ if (result != VerifyResult.OK)
+ return result;
+ }
+
+ return VerifyResult.OK;
+}
+
+private VerifyResult upgrade_database(int input_version) {
+ assert(input_version < DatabaseTable.SCHEMA_VERSION);
+
+ int version = input_version;
+
+ // No upgrade available from version 1.
+ if (version == 1)
+ return VerifyResult.NO_UPGRADE_AVAILABLE;
+
+ message("Upgrading database from schema version %d to %d", version, DatabaseTable.SCHEMA_VERSION);
+
+ //
+ // Version 2: For all intents and purposes, the baseline schema version.
+ // * Removed start_time and end_time from EventsTable
+ //
+
+ //
+ // Version 3:
+ // * Added flags column to PhotoTable
+ //
+
+ if (!DatabaseTable.has_column("PhotoTable", "flags")) {
+ message("upgrade_database: adding flags column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "flags", "INTEGER DEFAULT 0"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 3;
+
+ //
+ // ThumbnailTable(s) removed.
+ //
+
+ //
+ // Version 4:
+ // * Added file_format column to PhotoTable
+ //
+
+ if (!DatabaseTable.has_column("PhotoTable", "file_format")) {
+ message("upgrade_database: adding file_format column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "file_format", "INTEGER DEFAULT 0"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 4;
+
+ //
+ // Version 5:
+ // * Added title column to PhotoTable
+ //
+
+ if (!DatabaseTable.has_column("PhotoTable", "title")) {
+ message("upgrade_database: adding title column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "title", "TEXT"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 5;
+
+ //
+ // Version 6:
+ // * Added backlinks column to PhotoTable
+ //
+
+ if (!DatabaseTable.has_column("PhotoTable", "backlinks")) {
+ message("upgrade_database: adding backlinks column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "backlinks", "TEXT"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 6;
+
+ //
+ // * Ignore the exif_md5 column from PhotoTable. Because removing columns with SQLite is
+ // painful, simply ignoring the column for now. Keeping it up-to-date when possible in
+ // case a future requirement is discovered.
+ //
+
+ //
+ // Version 7:
+ // * Added BackingPhotoTable (which creates itself if needed)
+ // * Added time_reimported and editable_id columns to PhotoTable
+ //
+
+ if (!DatabaseTable.has_column("PhotoTable", "time_reimported")) {
+ message("upgrade_database: adding time_reimported column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "time_reimported", "INTEGER"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ if (!DatabaseTable.has_column("PhotoTable", "editable_id")) {
+ message("upgrade_database: adding editable_id column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "editable_id", "INTEGER DEFAULT -1"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 7;
+
+ //
+ // * Ignore the orientation column in BackingPhotoTable. (See note above about removing
+ // columns from tables.)
+ //
+
+ //
+ // Version 8:
+ // * Added rating column to PhotoTable
+ //
+
+ if (!DatabaseTable.has_column("PhotoTable", "rating")) {
+ message("upgrade_database: adding rating column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "rating", "INTEGER DEFAULT 0"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ //
+ // Version 9:
+ // * Added metadata_dirty flag to PhotoTable. Default to 1 rather than 0 on upgrades so
+ // changes to metadata prior to upgrade will be caught by MetadataWriter.
+ //
+
+ if (!DatabaseTable.has_column("PhotoTable", "metadata_dirty")) {
+ message("upgrade_database: adding metadata_dirty column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "metadata_dirty", "INTEGER DEFAULT 1"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 9;
+
+ //
+ // Version 10:
+ // * Added flags column to VideoTable
+ //
+
+ if (DatabaseTable.has_table("VideoTable") && !DatabaseTable.has_column("VideoTable", "flags")) {
+ message("upgrade_database: adding flags column to VideoTable");
+ if (!DatabaseTable.add_column("VideoTable", "flags", "INTEGER DEFAULT 0"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 10;
+
+ //
+ // Version 11:
+ // * Added primary_source_id column to EventTable
+ //
+
+ if (!DatabaseTable.has_column("EventTable", "primary_source_id")) {
+ message("upgrade_database: adding primary_source_id column to EventTable");
+ if (!DatabaseTable.add_column("EventTable", "primary_source_id", "INTEGER DEFAULT 0"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 11;
+
+ //
+ // Version 12:
+ // * Added reason column to TombstoneTable
+ //
+
+ if (!DatabaseTable.ensure_column("TombstoneTable", "reason", "INTEGER DEFAULT 0",
+ "upgrade_database: adding reason column to TombstoneTable")) {
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 12;
+
+ //
+ // Version 13:
+ // * Added RAW development columns to Photo table.
+ //
+
+ if (!DatabaseTable.has_column("PhotoTable", "developer")) {
+ message("upgrade_database: adding developer column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "developer", "TEXT"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ if (!DatabaseTable.has_column("PhotoTable", "develop_shotwell_id")) {
+ message("upgrade_database: adding develop_shotwell_id column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "develop_shotwell_id", "INTEGER DEFAULT -1"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ if (!DatabaseTable.has_column("PhotoTable", "develop_camera_id")) {
+ message("upgrade_database: adding develop_camera_id column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "develop_camera_id", "INTEGER DEFAULT -1"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ if (!DatabaseTable.has_column("PhotoTable", "develop_embedded_id")) {
+ message("upgrade_database: adding develop_embedded_id column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "develop_embedded_id", "INTEGER DEFAULT -1"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 13;
+
+ //
+ // Version 14:
+ // * Upgrades tag names in the TagTable for hierarchical tag support
+ //
+
+ if (input_version < 14)
+ TagTable.upgrade_for_htags();
+
+ version = 14;
+
+ //
+ // Version 15:
+ // * Upgrades the version number to prevent Shotwell 0.11 users from opening
+ // Shotwell 0.12 databases. While the database schema hasn't changed,
+ // straighten was only partially implemented in 0.11 but is fully
+ // implemented in 0.12, so when 0.11 users open an 0.12 database with
+ // straightening information, they see partially and/or incorrectly
+ // rotated photos.
+ //
+
+ version = 15;
+
+ //
+ // Version 16:
+ // * Migration of dconf settings data from /apps/shotwell to /org/yorba/shotwell.
+ //
+ // The database itself doesn't change; this is to force the path migration to
+ // occur.
+ //
+
+ if (input_version < 16) {
+ // Run the settings migrator to copy settings data from /apps/shotwell to /org/yorba/shotwell.
+ // Please see https://mail.gnome.org/archives/desktop-devel-list/2011-February/msg00064.html
+ GSettingsConfigurationEngine.run_gsettings_migrator();
+ }
+
+ version = 16;
+
+ //
+ // Version 17:
+ // * Added comment column to PhotoTable and VideoTable
+ //
+
+ if (!DatabaseTable.has_column("PhotoTable", "comment")) {
+ message("upgrade_database: adding comment column to PhotoTable");
+ if (!DatabaseTable.add_column("PhotoTable", "comment", "TEXT"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+ if (!DatabaseTable.has_column("VideoTable", "comment")) {
+ message("upgrade_database: adding comment column to VideoTable");
+ if (!DatabaseTable.add_column("VideoTable", "comment", "TEXT"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 17;
+
+ //
+ // Version 18:
+ // * Added comment column to EventTable
+ //
+
+ if (!DatabaseTable.has_column("EventTable", "comment")) {
+ message("upgrade_database: adding comment column to EventTable");
+ if (!DatabaseTable.add_column("EventTable", "comment", "TEXT"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 18;
+
+ //
+ // Version 19:
+ // * Deletion and regeneration of camera-raw thumbnails from previous versions,
+ // since they're likely to be incorrect.
+ //
+ // The database itself doesn't change; this is to force the thumbnail fixup to
+ // occur.
+ //
+
+ if (input_version < 19) {
+ Application.get_instance().set_raw_thumbs_fix_required(true);
+ }
+
+ version = 19;
+
+ //
+ // Version 20:
+ // * No change to database schema but fixing issue #6541 ("Saved searches should be aware of
+ // comments") added a new enumeration value that is stored in the SavedSearchTable. The
+ // presence of this heretofore unseen enumeration value will cause prior versions of
+ // Shotwell to yarf, so we bump the version here to ensure this doesn't happen
+ //
+
+ version = 20;
+
+ //
+ // Finalize the upgrade process
+ //
+
+ assert(version == DatabaseTable.SCHEMA_VERSION);
+ VersionTable.get_instance().update_version(version, Resources.APP_VERSION);
+
+ message("Database upgrade to schema version %d successful", version);
+
+ return VerifyResult.OK;
+}
+
+}
+
diff --git a/src/db/EventTable.vala b/src/db/EventTable.vala
new file mode 100644
index 0000000..016fa00
--- /dev/null
+++ b/src/db/EventTable.vala
@@ -0,0 +1,235 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public struct EventID {
+ public const int64 INVALID = -1;
+
+ public int64 id;
+
+ public EventID(int64 id = INVALID) {
+ this.id = id;
+ }
+
+ public bool is_invalid() {
+ return (id == INVALID);
+ }
+
+ public bool is_valid() {
+ return (id != INVALID);
+ }
+}
+
+public class EventRow {
+ public EventID event_id;
+ public string? name;
+ public time_t time_created;
+ public string? primary_source_id;
+ public string? comment;
+}
+
+public class EventTable : DatabaseTable {
+ private static EventTable instance = null;
+
+ private EventTable() {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS EventTable ("
+ + "id INTEGER PRIMARY KEY, "
+ + "name TEXT, "
+ + "primary_photo_id INTEGER, "
+ + "time_created INTEGER,"
+ + "primary_source_id TEXT,"
+ + "comment TEXT"
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create photo table", res);
+
+ set_table_name("EventTable");
+ }
+
+ public static EventTable get_instance() {
+ if (instance == null)
+ instance = new EventTable();
+
+ return instance;
+ }
+
+ // Returns a valid source ID, creating one from a legacy primary photo ID when needed.
+ private string? source_id_upgrade(int64 primary_photo_id, string? primary_source_id) {
+ if (MediaCollectionRegistry.get_instance().is_valid_source_id(primary_source_id)) {
+ return primary_source_id;
+ }
+ if (primary_photo_id != PhotoID.INVALID) {
+ // Upgrade to source_id from photo_id.
+ return PhotoID.upgrade_photo_id_to_source_id(PhotoID(primary_photo_id));
+ }
+ return null;
+ }
+
+ public EventRow create(string? primary_source_id, string? comment) throws DatabaseError {
+ assert(primary_source_id != null && primary_source_id != "");
+
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "INSERT INTO EventTable (primary_source_id, time_created, comment) VALUES (?, ?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ time_t time_created = (time_t) now_sec();
+
+ res = stmt.bind_text(1, primary_source_id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, time_created);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(3, comment);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("EventTable.create", res);
+
+ EventRow row = new EventRow();
+ row.event_id = EventID(db.last_insert_rowid());
+ row.name = null;
+ row.primary_source_id = primary_source_id;
+ row.time_created = time_created;
+ row.comment = comment;
+
+ return row;
+ }
+
+ // NOTE: The event_id in EventRow is ignored here. No checking is done to prevent
+ // against creating duplicate events or for the validity of other fields in the row (i.e.
+ // the primary photo ID).
+ public EventID create_from_row(EventRow row) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO EventTable (name, primary_photo_id, primary_source_id, time_created, comment) VALUES (?, ?, ?, ?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(1, row.name);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, PhotoID.INVALID);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(3, row.primary_source_id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(4, row.time_created);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(5, row.comment);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ fatal("Event create_from_row", res);
+
+ return EventID();
+ }
+
+ return EventID(db.last_insert_rowid());
+ }
+
+ public EventRow? get_row(EventID event_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "SELECT name, primary_photo_id, primary_source_id, time_created, comment FROM EventTable WHERE id=?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, event_id.id);
+ assert(res == Sqlite.OK);
+
+ if (stmt.step() != Sqlite.ROW)
+ return null;
+
+ EventRow row = new EventRow();
+ row.event_id = event_id;
+ row.name = stmt.column_text(0);
+ if (row.name != null && row.name.length == 0)
+ row.name = null;
+ row.primary_source_id = source_id_upgrade(stmt.column_int64(1), stmt.column_text(2));
+ row.time_created = (time_t) stmt.column_int64(3);
+ row.comment = stmt.column_text(4);
+
+ return row;
+ }
+
+ public void remove(EventID event_id) throws DatabaseError {
+ delete_by_id(event_id.id);
+ }
+
+ public Gee.ArrayList<EventRow?> get_events() {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id, name, primary_photo_id, primary_source_id, time_created, comment FROM EventTable",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ Gee.ArrayList<EventRow?> event_rows = new Gee.ArrayList<EventRow?>();
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ break;
+ } else if (res != Sqlite.ROW) {
+ fatal("get_events", res);
+
+ break;
+ }
+
+ EventRow row = new EventRow();
+
+ row.event_id = EventID(stmt.column_int64(0));
+ row.name = stmt.column_text(1);
+ row.primary_source_id = source_id_upgrade(stmt.column_int64(2), stmt.column_text(3));
+ row.time_created = (time_t) stmt.column_int64(4);
+ row.comment = stmt.column_text(5);
+
+ event_rows.add(row);
+ }
+
+ return event_rows;
+ }
+
+ public bool rename(EventID event_id, string? name) {
+ return update_text_by_id(event_id.id, "name", name != null ? name : "");
+ }
+
+ public string? get_name(EventID event_id) {
+ Sqlite.Statement stmt;
+ if (!select_by_id(event_id.id, "name", out stmt))
+ return null;
+
+ string name = stmt.column_text(0);
+
+ return (name != null && name.length > 0) ? name : null;
+ }
+
+ public string? get_primary_source_id(EventID event_id) {
+ Sqlite.Statement stmt;
+ if (!select_by_id(event_id.id, "primary_source_id", out stmt))
+ return null;
+
+ return stmt.column_text(0);
+ }
+
+ public bool set_primary_source_id(EventID event_id, string primary_source_id) {
+ return update_text_by_id(event_id.id, "primary_source_id", primary_source_id);
+ }
+
+ public time_t get_time_created(EventID event_id) {
+ Sqlite.Statement stmt;
+ if (!select_by_id(event_id.id, "time_created", out stmt))
+ return 0;
+
+ return (time_t) stmt.column_int64(0);
+ }
+
+ public bool set_comment(EventID event_id, string new_comment) {
+ return update_text_by_id(event_id.id, "comment", new_comment != null ? new_comment : "");
+ }
+
+}
+
+
diff --git a/src/db/PhotoTable.vala b/src/db/PhotoTable.vala
new file mode 100644
index 0000000..9891fe6
--- /dev/null
+++ b/src/db/PhotoTable.vala
@@ -0,0 +1,1245 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public struct PhotoID {
+ public const int64 INVALID = -1;
+
+ public int64 id;
+
+ public PhotoID(int64 id = INVALID) {
+ this.id = id;
+ }
+
+ public bool is_invalid() {
+ return (id == INVALID);
+ }
+
+ public bool is_valid() {
+ return (id != INVALID);
+ }
+
+ public uint hash() {
+ return int64_hash(id);
+ }
+
+ public static bool equal(void *a, void *b) {
+ return ((PhotoID *) a)->id == ((PhotoID *) b)->id;
+ }
+
+ public static string upgrade_photo_id_to_source_id(PhotoID photo_id) {
+ return ("%s%016" + int64.FORMAT_MODIFIER + "x").printf(Photo.TYPENAME, photo_id.id);
+ }
+}
+
+public struct ImportID {
+ public const int64 INVALID = 0;
+
+ public int64 id;
+
+ public ImportID(int64 id = INVALID) {
+ this.id = id;
+ }
+
+ public static ImportID generate() {
+ TimeVal timestamp = TimeVal();
+ timestamp.get_current_time();
+ int64 id = timestamp.tv_sec;
+
+ return ImportID(id);
+ }
+
+ public bool is_invalid() {
+ return (id == INVALID);
+ }
+
+ public bool is_valid() {
+ return (id != INVALID);
+ }
+
+ public static int compare_func(ImportID? a, ImportID? b) {
+ assert (a != null && b != null);
+ return (int) (a.id - b.id);
+ }
+
+ public static int64 comparator(void *a, void *b) {
+ return ((ImportID *) a)->id - ((ImportID *) b)->id;
+ }
+}
+
+public class PhotoRow {
+ public PhotoID photo_id;
+ public BackingPhotoRow master;
+ public time_t exposure_time;
+ public ImportID import_id;
+ public EventID event_id;
+ public Orientation orientation;
+ public Gee.HashMap<string, KeyValueMap>? transformations;
+ public string md5;
+ public string thumbnail_md5;
+ public string exif_md5;
+ public time_t time_created;
+ public uint64 flags;
+ public Rating rating;
+ public string title;
+ public string comment;
+ public string? backlinks;
+ public time_t time_reimported;
+ public BackingPhotoID editable_id;
+ public bool metadata_dirty;
+
+ // Currently selected developer (RAW only)
+ public RawDeveloper developer;
+
+ // Currently selected developer (RAW only)
+ public BackingPhotoID[] development_ids;
+
+
+ public PhotoRow() {
+ master = new BackingPhotoRow();
+ editable_id = BackingPhotoID();
+ development_ids = new BackingPhotoID[RawDeveloper.as_array().length];
+ foreach (RawDeveloper d in RawDeveloper.as_array())
+ development_ids[d] = BackingPhotoID();
+ }
+}
+
+public class PhotoTable : DatabaseTable {
+ private static PhotoTable instance = null;
+
+ private PhotoTable() {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS PhotoTable ("
+ + "id INTEGER PRIMARY KEY, "
+ + "filename TEXT UNIQUE NOT NULL, "
+ + "width INTEGER, "
+ + "height INTEGER, "
+ + "filesize INTEGER, "
+ + "timestamp INTEGER, "
+ + "exposure_time INTEGER, "
+ + "orientation INTEGER, "
+ + "original_orientation INTEGER, "
+ + "import_id INTEGER, "
+ + "event_id INTEGER, "
+ + "transformations TEXT, "
+ + "md5 TEXT, "
+ + "thumbnail_md5 TEXT, "
+ + "exif_md5 TEXT, "
+ + "time_created INTEGER, "
+ + "flags INTEGER DEFAULT 0, "
+ + "rating INTEGER DEFAULT 0, "
+ + "file_format INTEGER DEFAULT 0, "
+ + "title TEXT, "
+ + "backlinks TEXT, "
+ + "time_reimported INTEGER, "
+ + "editable_id INTEGER DEFAULT -1, "
+ + "metadata_dirty INTEGER DEFAULT 0, "
+ + "developer TEXT, "
+ + "develop_shotwell_id INTEGER DEFAULT -1, "
+ + "develop_camera_id INTEGER DEFAULT -1, "
+ + "develop_embedded_id INTEGER DEFAULT -1, "
+ + "comment TEXT"
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create photo table", res);
+
+ // index on event_id
+ Sqlite.Statement stmt2;
+ int res2 = db.prepare_v2("CREATE INDEX IF NOT EXISTS PhotoEventIDIndex ON PhotoTable (event_id)",
+ -1, out stmt2);
+ assert(res2 == Sqlite.OK);
+
+ res2 = stmt2.step();
+ if (res2 != Sqlite.DONE)
+ fatal("create photo table", res2);
+
+ set_table_name("PhotoTable");
+ }
+
+ public static PhotoTable get_instance() {
+ if (instance == null)
+ instance = new PhotoTable();
+
+ return instance;
+ }
+
+ // PhotoRow.photo_id, event_id, master.orientation, flags, and time_created are ignored on input.
+ // All fields are set on exit with values stored in the database. editable_id field is ignored.
+ public PhotoID add(PhotoRow photo_row) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "INSERT INTO PhotoTable (filename, width, height, filesize, timestamp, exposure_time, "
+ + "orientation, original_orientation, import_id, event_id, md5, thumbnail_md5, "
+ + "exif_md5, time_created, file_format, title, rating, editable_id, developer, comment) "
+ + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ ulong time_created = now_sec();
+
+ res = stmt.bind_text(1, photo_row.master.filepath);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(2, photo_row.master.dim.width);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(3, photo_row.master.dim.height);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(4, photo_row.master.filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(5, photo_row.master.timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(6, photo_row.exposure_time);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(7, photo_row.master.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(8, photo_row.master.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(9, photo_row.import_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(10, EventID.INVALID);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(11, photo_row.md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(12, photo_row.thumbnail_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(13, photo_row.exif_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(14, time_created);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(15, photo_row.master.file_format.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(16, photo_row.title);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(17, photo_row.rating.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(18, BackingPhotoID.INVALID);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(19, photo_row.developer.to_string());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(20, photo_row.comment);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ if (res != Sqlite.CONSTRAINT)
+ fatal("add_photo", res);
+
+ return PhotoID();
+ }
+
+ // fill in ignored fields with database values
+ photo_row.photo_id = PhotoID(db.last_insert_rowid());
+ photo_row.orientation = photo_row.master.original_orientation;
+ photo_row.event_id = EventID();
+ photo_row.time_created = (time_t) time_created;
+ photo_row.flags = 0;
+
+ return photo_row.photo_id;
+ }
+
+ // The only fields recognized in the PhotoRow are photo_id, dimensions,
+ // filesize, timestamp, exposure_time, original_orientation, file_format,
+ // and the md5 fields. When the method returns, time_reimported and master.orientation has been
+ // updated. editable_id is ignored. transformations are untouched; use
+ // remove_all_transformations() if necessary.
+ public void reimport(PhotoRow row) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "UPDATE PhotoTable SET width = ?, height = ?, filesize = ?, timestamp = ?, "
+ + "exposure_time = ?, orientation = ?, original_orientation = ?, md5 = ?, "
+ + "exif_md5 = ?, thumbnail_md5 = ?, file_format = ?, title = ?, time_reimported = ? "
+ + "WHERE id = ?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ time_t time_reimported = (time_t) now_sec();
+
+ res = stmt.bind_int(1, row.master.dim.width);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(2, row.master.dim.height);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(3, row.master.filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(4, row.master.timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(5, row.exposure_time);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(6, row.master.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(7, row.master.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(8, row.md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(9, row.exif_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(10, row.thumbnail_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(11, row.master.file_format.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(12, row.title);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(13, time_reimported);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(14, row.photo_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("PhotoTable.reimport_master", res);
+
+ row.time_reimported = time_reimported;
+ row.orientation = row.master.original_orientation;
+ }
+
+ public bool master_exif_updated(PhotoID photoID, int64 filesize, long timestamp,
+ string md5, string? exif_md5, string? thumbnail_md5, PhotoRow row) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "UPDATE PhotoTable SET filesize = ?, timestamp = ?, md5 = ?, exif_md5 = ?,"
+ + "thumbnail_md5 =? WHERE id = ?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(3, md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(4, exif_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(5, thumbnail_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(6, photoID.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ if (res != Sqlite.CONSTRAINT)
+ fatal("write_update_photo", res);
+
+ return false;
+ }
+
+ row.master.filesize = filesize;
+ row.master.timestamp = timestamp;
+ row.md5 = md5;
+ row.exif_md5 = exif_md5;
+ row.thumbnail_md5 = thumbnail_md5;
+
+ return true;
+ }
+
+ // Force corrupted orientations to a safe value.
+ //
+ // In previous versions of Shotwell, this field could be written to
+ // the DB as a zero due to Vala 0.14 breaking the way it handled
+ // objects passed as 'ref' arguments to methods.
+ //
+ // For further details, please see http://redmine.yorba.org/issues/4354 and
+ // https://bugzilla.gnome.org/show_bug.cgi?id=663818 .
+ private void validate_orientation(PhotoRow row) {
+ if ((row.orientation < Orientation.MIN) ||
+ (row.orientation > Orientation.MAX)) {
+ // orientation was corrupted; set it to top left.
+ set_orientation(row.photo_id, Orientation.MIN);
+ row.orientation = Orientation.MIN;
+ }
+ }
+
+ public PhotoRow? get_row(PhotoID photo_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "SELECT filename, width, height, filesize, timestamp, exposure_time, orientation, "
+ + "original_orientation, import_id, event_id, transformations, md5, thumbnail_md5, "
+ + "exif_md5, time_created, flags, rating, file_format, title, backlinks, "
+ + "time_reimported, editable_id, metadata_dirty, developer, develop_shotwell_id, "
+ + "develop_camera_id, develop_embedded_id, comment "
+ + "FROM PhotoTable WHERE id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, photo_id.id);
+ assert(res == Sqlite.OK);
+
+ if (stmt.step() != Sqlite.ROW)
+ return null;
+
+ PhotoRow row = new PhotoRow();
+ row.photo_id = photo_id;
+ row.master.filepath = stmt.column_text(0);
+ row.master.dim = Dimensions(stmt.column_int(1), stmt.column_int(2));
+ row.master.filesize = stmt.column_int64(3);
+ row.master.timestamp = (time_t) stmt.column_int64(4);
+ row.exposure_time = (time_t) stmt.column_int64(5);
+ row.orientation = (Orientation) stmt.column_int(6);
+ row.master.original_orientation = (Orientation) stmt.column_int(7);
+ row.import_id.id = stmt.column_int64(8);
+ row.event_id.id = stmt.column_int64(9);
+ row.transformations = marshall_all_transformations(stmt.column_text(10));
+ row.md5 = stmt.column_text(11);
+ row.thumbnail_md5 = stmt.column_text(12);
+ row.exif_md5 = stmt.column_text(13);
+ row.time_created = (time_t) stmt.column_int64(14);
+ row.flags = stmt.column_int64(15);
+ row.rating = Rating.unserialize(stmt.column_int(16));
+ row.master.file_format = PhotoFileFormat.unserialize(stmt.column_int(17));
+ row.title = stmt.column_text(18);
+ row.backlinks = stmt.column_text(19);
+ row.time_reimported = (time_t) stmt.column_int64(20);
+ row.editable_id = BackingPhotoID(stmt.column_int64(21));
+ row.metadata_dirty = stmt.column_int(22) != 0;
+ row.developer = stmt.column_text(23) != null ? RawDeveloper.from_string(stmt.column_text(23)) :
+ RawDeveloper.CAMERA;
+ row.development_ids[RawDeveloper.SHOTWELL] = BackingPhotoID(stmt.column_int64(24));
+ row.development_ids[RawDeveloper.CAMERA] = BackingPhotoID(stmt.column_int64(25));
+ row.development_ids[RawDeveloper.EMBEDDED] = BackingPhotoID(stmt.column_int64(26));
+ row.comment = stmt.column_text(27);
+
+ return row;
+ }
+
+ public Gee.ArrayList<PhotoRow?> get_all() {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "SELECT id, filename, width, height, filesize, timestamp, exposure_time, orientation, "
+ + "original_orientation, import_id, event_id, transformations, md5, thumbnail_md5, "
+ + "exif_md5, time_created, flags, rating, file_format, title, backlinks, time_reimported, "
+ + "editable_id, metadata_dirty, developer, develop_shotwell_id, develop_camera_id, "
+ + "develop_embedded_id, comment FROM PhotoTable",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ Gee.ArrayList<PhotoRow?> all = new Gee.ArrayList<PhotoRow?>();
+
+ while ((res = stmt.step()) == Sqlite.ROW) {
+ PhotoRow row = new PhotoRow();
+ row.photo_id.id = stmt.column_int64(0);
+ row.master.filepath = stmt.column_text(1);
+ row.master.dim = Dimensions(stmt.column_int(2), stmt.column_int(3));
+ row.master.filesize = stmt.column_int64(4);
+ row.master.timestamp = (time_t) stmt.column_int64(5);
+ row.exposure_time = (time_t) stmt.column_int64(6);
+ row.orientation = (Orientation) stmt.column_int(7);
+ row.master.original_orientation = (Orientation) stmt.column_int(8);
+ row.import_id.id = stmt.column_int64(9);
+ row.event_id.id = stmt.column_int64(10);
+ row.transformations = marshall_all_transformations(stmt.column_text(11));
+ row.md5 = stmt.column_text(12);
+ row.thumbnail_md5 = stmt.column_text(13);
+ row.exif_md5 = stmt.column_text(14);
+ row.time_created = (time_t) stmt.column_int64(15);
+ row.flags = stmt.column_int64(16);
+ row.rating = Rating.unserialize(stmt.column_int(17));
+ row.master.file_format = PhotoFileFormat.unserialize(stmt.column_int(18));
+ row.title = stmt.column_text(19);
+ row.backlinks = stmt.column_text(20);
+ row.time_reimported = (time_t) stmt.column_int64(21);
+ row.editable_id = BackingPhotoID(stmt.column_int64(22));
+ row.metadata_dirty = stmt.column_int(23) != 0;
+ row.developer = stmt.column_text(24) != null ? RawDeveloper.from_string(stmt.column_text(24)) :
+ RawDeveloper.CAMERA;
+ row.development_ids[RawDeveloper.SHOTWELL] = BackingPhotoID(stmt.column_int64(25));
+ row.development_ids[RawDeveloper.CAMERA] = BackingPhotoID(stmt.column_int64(26));
+ row.development_ids[RawDeveloper.EMBEDDED] = BackingPhotoID(stmt.column_int64(27));
+ row.comment = stmt.column_text(28);
+
+ validate_orientation(row);
+
+ all.add(row);
+ }
+
+ return all;
+ }
+
+ // Create a duplicate of the specified row. A new byte-for-byte duplicate (including filesystem
+ // metadata) of PhotoID's file needs to back this duplicate and its editable (if exists).
+ public PhotoID duplicate(PhotoID photo_id, string new_filename, BackingPhotoID editable_id,
+ BackingPhotoID develop_shotwell, BackingPhotoID develop_camera_id,
+ BackingPhotoID develop_embedded_id) {
+ // get a copy of the original row, duplicating most (but not all) of it
+ PhotoRow original = get_row(photo_id);
+
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO PhotoTable (filename, width, height, filesize, "
+ + "timestamp, exposure_time, orientation, original_orientation, import_id, event_id, "
+ + "transformations, md5, thumbnail_md5, exif_md5, time_created, flags, rating, "
+ + "file_format, title, editable_id, developer, develop_shotwell_id, develop_camera_id, "
+ + "develop_embedded_id, comment) "
+ + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(1, new_filename);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(2, original.master.dim.width);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(3, original.master.dim.height);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(4, original.master.filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(5, original.master.timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(6, original.exposure_time);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(7, original.orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(8, original.master.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(9, original.import_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(10, original.event_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(11, unmarshall_all_transformations(original.transformations));
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(12, original.md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(13, original.thumbnail_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(14, original.exif_md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(15, now_sec());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(16, (int64) original.flags);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(17, original.rating.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(18, original.master.file_format.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(19, original.title);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(20, editable_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(21, original.developer.to_string());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(22, develop_shotwell.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(23, develop_camera_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(24, develop_embedded_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(25, original.comment);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ if (res != Sqlite.CONSTRAINT)
+ fatal("duplicate", res);
+
+ return PhotoID();
+ }
+
+ return PhotoID(db.last_insert_rowid());
+ }
+
+ public bool set_title(PhotoID photo_id, string? new_title) {
+ return update_text_by_id(photo_id.id, "title", new_title != null ? new_title : "");
+ }
+
+ public bool set_comment(PhotoID photo_id, string? new_comment) {
+ return update_text_by_id(photo_id.id, "comment", new_comment != null ? new_comment : "");
+ }
+
+ public void set_filepath(PhotoID photo_id, string filepath) throws DatabaseError {
+ update_text_by_id_2(photo_id.id, "filename", filepath);
+ }
+
+ public void update_timestamp(PhotoID photo_id, time_t timestamp) throws DatabaseError {
+ update_int64_by_id_2(photo_id.id, "timestamp", timestamp);
+ }
+
+ public bool set_exposure_time(PhotoID photo_id, time_t time) {
+ return update_int64_by_id(photo_id.id, "exposure_time", (int64) time);
+ }
+
+ public void set_import_id(PhotoID photo_id, ImportID import_id) throws DatabaseError {
+ update_int64_by_id_2(photo_id.id, "import_id", import_id.id);
+ }
+
+ public bool remove_by_file(File file) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("DELETE FROM PhotoTable WHERE filename=?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(1, file.get_path());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ warning("remove", res);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public void remove(PhotoID photo_id) throws DatabaseError {
+ delete_by_id(photo_id.id);
+ }
+
+ public Gee.ArrayList<PhotoID?> get_photos() {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id FROM PhotoTable", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ Gee.ArrayList<PhotoID?> photo_ids = new Gee.ArrayList<PhotoID?>();
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ break;
+ } else if (res != Sqlite.ROW) {
+ fatal("get_photos", res);
+
+ break;
+ }
+
+ photo_ids.add(PhotoID(stmt.column_int64(0)));
+ }
+
+ return photo_ids;
+ }
+
+ public bool set_orientation(PhotoID photo_id, Orientation orientation) {
+ return update_int_by_id(photo_id.id, "orientation", (int) orientation);
+ }
+
+ public bool replace_flags(PhotoID photo_id, uint64 flags) {
+ return update_int64_by_id(photo_id.id, "flags", (int64) flags);
+ }
+
+ public bool set_rating(PhotoID photo_id, Rating rating) {
+ return update_int_by_id(photo_id.id, "rating", rating.serialize());
+ }
+
+ public int get_event_photo_count(EventID event_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id FROM PhotoTable WHERE event_id = ?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, event_id.id);
+ assert(res == Sqlite.OK);
+
+ int count = 0;
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ break;
+ } else if (res != Sqlite.ROW) {
+ fatal("get_event_photo_count", res);
+
+ break;
+ }
+
+ count++;
+ }
+
+ return count;
+ }
+
+ public Gee.ArrayList<string> get_event_source_ids(EventID event_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id FROM PhotoTable WHERE event_id = ?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, event_id.id);
+ assert(res == Sqlite.OK);
+
+ Gee.ArrayList<string> result = new Gee.ArrayList<string>();
+ for(;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ break;
+ } else if (res != Sqlite.ROW) {
+ fatal("get_event_source_ids", res);
+
+ break;
+ }
+
+ result.add(PhotoID.upgrade_photo_id_to_source_id(PhotoID(stmt.column_int64(0))));
+ }
+
+ return result;
+ }
+
+ public bool event_has_photos(EventID event_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id FROM PhotoTable WHERE event_id = ? LIMIT 1", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, event_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ return false;
+ } else if (res != Sqlite.ROW) {
+ fatal("event_has_photos", res);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool drop_event(EventID event_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE PhotoTable 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("drop_event", res);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool set_event(PhotoID photo_id, EventID event_id) {
+ return update_int64_by_id(photo_id.id, "event_id", event_id.id);
+ }
+
+ private string? get_raw_transformations(PhotoID photo_id) {
+ Sqlite.Statement stmt;
+ if (!select_by_id(photo_id.id, "transformations", out stmt))
+ return null;
+
+ string trans = stmt.column_text(0);
+ if (trans == null || trans.length == 0)
+ return null;
+
+ return trans;
+ }
+
+ private bool set_raw_transformations(PhotoID photo_id, string trans) {
+ return update_text_by_id(photo_id.id, "transformations", trans);
+ }
+
+ public bool set_transformation_state(PhotoID photo_id, Orientation orientation,
+ Gee.HashMap<string, KeyValueMap>? transformations) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE PhotoTable SET orientation = ?, transformations = ? WHERE id = ?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int(1, orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(2, unmarshall_all_transformations(transformations));
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(3, photo_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ fatal("set_transformation_state", res);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public static Gee.HashMap<string, KeyValueMap>? marshall_all_transformations(string? trans) {
+ if (trans == null || trans.length == 0)
+ return null;
+
+ try {
+ KeyFile keyfile = new KeyFile();
+ if (!keyfile.load_from_data(trans, trans.length, KeyFileFlags.NONE))
+ return null;
+
+ Gee.HashMap<string, KeyValueMap> map = new Gee.HashMap<string, KeyValueMap>();
+
+ string[] objects = keyfile.get_groups();
+ foreach (string object in objects) {
+ string[] keys = keyfile.get_keys(object);
+ if (keys == null || keys.length == 0)
+ continue;
+
+ KeyValueMap key_map = new KeyValueMap(object);
+ for (int ctr = 0; ctr < keys.length; ctr++)
+ key_map.set_string(keys[ctr], keyfile.get_string(object, keys[ctr]));
+
+ map.set(object, key_map);
+ }
+
+ return map;
+ } catch (Error err) {
+ error("%s", err.message);
+ }
+ }
+
+ public static string? unmarshall_all_transformations(Gee.HashMap<string, KeyValueMap>? transformations) {
+ if (transformations == null || transformations.keys.size == 0)
+ return null;
+
+ KeyFile keyfile = new KeyFile();
+
+ foreach (string object in transformations.keys) {
+ KeyValueMap map = transformations.get(object);
+
+ foreach (string key in map.get_keys()) {
+ string? value = map.get_string(key, null);
+ assert(value != null);
+
+ keyfile.set_string(object, key, value);
+ }
+ }
+
+ size_t length;
+ string unmarshalled = keyfile.to_data(out length);
+ assert(unmarshalled != null);
+ assert(unmarshalled.length > 0);
+
+ return unmarshalled;
+ }
+
+ public bool set_transformation(PhotoID photo_id, KeyValueMap map) {
+ string trans = get_raw_transformations(photo_id);
+
+ try {
+ KeyFile keyfile = new KeyFile();
+ if (trans != null) {
+ if (!keyfile.load_from_data(trans, trans.length, KeyFileFlags.NONE))
+ return false;
+ }
+
+ Gee.Set<string> keys = map.get_keys();
+ foreach (string key in keys) {
+ string value = map.get_string(key, null);
+ assert(value != null);
+
+ keyfile.set_string(map.get_group(), key, value);
+ }
+
+ size_t length;
+ trans = keyfile.to_data(out length);
+ assert(trans != null);
+ assert(trans.length > 0);
+ } catch (Error err) {
+ error("%s", err.message);
+ }
+
+ return set_raw_transformations(photo_id, trans);
+ }
+
+ public bool remove_transformation(PhotoID photo_id, string object) {
+ string trans = get_raw_transformations(photo_id);
+ if (trans == null)
+ return true;
+
+ try {
+ KeyFile keyfile = new KeyFile();
+ if (!keyfile.load_from_data(trans, trans.length, KeyFileFlags.NONE))
+ return false;
+
+ if (!keyfile.has_group(object))
+ return true;
+
+ keyfile.remove_group(object);
+
+ size_t length;
+ trans = keyfile.to_data(out length);
+ assert(trans != null);
+ } catch (Error err) {
+ error("%s", err.message);
+ }
+
+ return set_raw_transformations(photo_id, trans);
+ }
+
+ public bool remove_all_transformations(PhotoID photo_id) {
+ if (get_raw_transformations(photo_id) == null)
+ return false;
+
+ return update_text_by_id(photo_id.id, "transformations", "");
+ }
+
+ // Use PhotoFileFormat.UNKNOWN if not to search for matching file format; it's only used if
+ // searching for MD5 duplicates.
+ private Sqlite.Statement get_duplicate_stmt(File? file, string? thumbnail_md5, string? md5,
+ PhotoFileFormat file_format) {
+ assert(file != null || thumbnail_md5 != null || md5 != null);
+
+ string sql = "SELECT id FROM PhotoTable WHERE";
+ bool first = true;
+
+ if (file != null) {
+ sql += " filename=?";
+ first = false;
+ }
+
+ if (thumbnail_md5 != null || md5 != null) {
+ if (first)
+ sql += " ((";
+ else
+ sql += " OR ((";
+ first = false;
+
+ if (thumbnail_md5 != null)
+ sql += " thumbnail_md5=?";
+
+ if (md5 != null) {
+ if (thumbnail_md5 == null)
+ sql += " md5=?";
+ else
+ sql += " OR md5=?";
+ }
+
+ sql += ")";
+
+ if (file_format != PhotoFileFormat.UNKNOWN)
+ sql += " AND file_format=?";
+
+ sql += ")";
+ }
+
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(sql, -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ int col = 1;
+
+ if (file != null) {
+ res = stmt.bind_text(col++, file.get_path());
+ assert(res == Sqlite.OK);
+ }
+
+ if (thumbnail_md5 != null) {
+ res = stmt.bind_text(col++, thumbnail_md5);
+ assert(res == Sqlite.OK);
+ }
+
+ if (md5 != null) {
+ res = stmt.bind_text(col++, md5);
+ assert(res == Sqlite.OK);
+ }
+
+ if ((thumbnail_md5 != null || md5 != null) && file_format != PhotoFileFormat.UNKNOWN) {
+ res = stmt.bind_int(col++, file_format.serialize());
+ assert(res == Sqlite.OK);
+ }
+
+ return stmt;
+ }
+
+ public bool has_duplicate(File? file, string? thumbnail_md5, string? md5, PhotoFileFormat file_format) {
+ Sqlite.Statement stmt = get_duplicate_stmt(file, thumbnail_md5, md5, file_format);
+ int res = stmt.step();
+
+ if (res == Sqlite.DONE) {
+ // not found
+ return false;
+ } else if (res == Sqlite.ROW) {
+ // at least one found
+ return true;
+ } else {
+ fatal("has_duplicate", res);
+
+ return false;
+ }
+ }
+
+ public PhotoID[] get_duplicate_ids(File? file, string? thumbnail_md5, string? md5,
+ PhotoFileFormat file_format) {
+ Sqlite.Statement stmt = get_duplicate_stmt(file, thumbnail_md5, md5, file_format);
+
+ PhotoID[] ids = new PhotoID[0];
+
+ int res = stmt.step();
+ while (res == Sqlite.ROW) {
+ ids += PhotoID(stmt.column_int64(0));
+ res = stmt.step();
+ }
+
+ return ids;
+ }
+
+ public void update_backlinks(PhotoID photo_id, string? backlinks) throws DatabaseError {
+ update_text_by_id_2(photo_id.id, "backlinks", backlinks != null ? backlinks : "");
+ }
+
+ public void attach_editable(PhotoRow row, BackingPhotoID editable_id) throws DatabaseError {
+ update_int64_by_id_2(row.photo_id.id, "editable_id", editable_id.id);
+
+ row.editable_id = editable_id;
+ }
+
+ public void detach_editable(PhotoRow row) throws DatabaseError {
+ update_int64_by_id_2(row.photo_id.id, "editable_id", BackingPhotoID.INVALID);
+
+ row.editable_id = BackingPhotoID();
+ }
+
+ public void set_metadata_dirty(PhotoID photo_id, bool dirty) throws DatabaseError {
+ update_int_by_id_2(photo_id.id, "metadata_dirty", dirty ? 1 : 0);
+ }
+
+ public void update_raw_development(PhotoRow row, RawDeveloper rd, BackingPhotoID backing_photo_id)
+ throws DatabaseError {
+
+ string col;
+ switch (rd) {
+ case RawDeveloper.SHOTWELL:
+ col = "develop_shotwell_id";
+ break;
+
+ case RawDeveloper.CAMERA:
+ col = "develop_camera_id";
+ break;
+
+ case RawDeveloper.EMBEDDED:
+ col = "develop_embedded_id";
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ row.development_ids[rd] = backing_photo_id;
+ update_int64_by_id_2(row.photo_id.id, col, backing_photo_id.id);
+
+ if (backing_photo_id.id != BackingPhotoID.INVALID)
+ update_text_by_id_2(row.photo_id.id, "developer", rd.to_string());
+ }
+
+ public void remove_development(PhotoRow row, RawDeveloper rd) throws DatabaseError {
+ update_raw_development(row, rd, BackingPhotoID());
+ }
+
+}
+
+//
+// BackingPhotoTable
+//
+// BackingPhotoTable is designed to hold any number of alternative backing photos
+// for a Photo. In the first implementation it was designed for editable photos (Edit with
+// External Editor), but if other such alternates are needed, this is where to store them.
+//
+// Note that no transformations are held here.
+//
+
+public struct BackingPhotoID {
+ public const int64 INVALID = -1;
+
+ public int64 id;
+
+ public BackingPhotoID(int64 id = INVALID) {
+ this.id = id;
+ }
+
+ public bool is_invalid() {
+ return (id == INVALID);
+ }
+
+ public bool is_valid() {
+ return (id != INVALID);
+ }
+}
+
+public class BackingPhotoRow {
+ public BackingPhotoID id;
+ public time_t time_created;
+ public string? filepath = null;
+ public int64 filesize;
+ public time_t timestamp;
+ public PhotoFileFormat file_format;
+ public Dimensions dim;
+ public Orientation original_orientation;
+
+ public bool matches_file_info(FileInfo info) {
+ if (filesize != info.get_size())
+ return false;
+
+ return timestamp == info.get_modification_time().tv_sec;
+ }
+
+ public bool is_touched(FileInfo info) {
+ if (filesize != info.get_size())
+ return false;
+
+ return timestamp != info.get_modification_time().tv_sec;
+ }
+
+ // Copies another backing photo row into this one.
+ public void copy_from(BackingPhotoRow from) {
+ id = from.id;
+ time_created = from.time_created;
+ filepath = from.filepath;
+ filesize = from.filesize;
+ timestamp = from.timestamp;
+ file_format = from.file_format;
+ dim = from.dim;
+ original_orientation = from.original_orientation;
+ }
+}
+
+public class BackingPhotoTable : DatabaseTable {
+ private static BackingPhotoTable instance = null;
+
+ private BackingPhotoTable() {
+ set_table_name("BackingPhotoTable");
+
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ + "BackingPhotoTable "
+ + "("
+ + "id INTEGER PRIMARY KEY, "
+ + "filepath TEXT UNIQUE NOT NULL, "
+ + "timestamp INTEGER, "
+ + "filesize INTEGER, "
+ + "width INTEGER, "
+ + "height INTEGER, "
+ + "original_orientation INTEGER, "
+ + "file_format INTEGER, "
+ + "time_created INTEGER "
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create PhotoBackingTable", res);
+ }
+
+ public static BackingPhotoTable get_instance() {
+ if (instance == null)
+ instance = new BackingPhotoTable();
+
+ return instance;
+ }
+
+ public void add(BackingPhotoRow state) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO BackingPhotoTable "
+ + "(filepath, timestamp, filesize, width, height, original_orientation, "
+ + "file_format, time_created) "
+ + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ time_t time_created = (time_t) now_sec();
+
+ res = stmt.bind_text(1, state.filepath);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, state.timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(3, state.filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(4, state.dim.width);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(5, state.dim.height);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(6, state.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(7, state.file_format.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(8, (int64) time_created);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("PhotoBackingTable.add", res);
+
+ state.id = BackingPhotoID(db.last_insert_rowid());
+ state.time_created = time_created;
+ }
+
+ public BackingPhotoRow? fetch(BackingPhotoID id) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT filepath, timestamp, filesize, width, height, "
+ + "original_orientation, file_format, time_created FROM BackingPhotoTable WHERE id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ return null;
+ else if (res != Sqlite.ROW)
+ throw_error("BackingPhotoTable.fetch_for_photo", res);
+
+ BackingPhotoRow row = new BackingPhotoRow();
+ row.id = id;
+ row.filepath = stmt.column_text(0);
+ row.timestamp = (time_t) stmt.column_int64(1);
+ row.filesize = stmt.column_int64(2);
+ row.dim = Dimensions(stmt.column_int(3), stmt.column_int(4));
+ row.original_orientation = (Orientation) stmt.column_int(5);
+ row.file_format = PhotoFileFormat.unserialize(stmt.column_int(6));
+ row.time_created = (time_t) stmt.column_int64(7);
+
+ return row;
+ }
+
+ // Everything but filepath is updated.
+ public void update(BackingPhotoRow row) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE BackingPhotoTable SET timestamp=?, filesize=?, "
+ + "width=?, height=?, original_orientation=?, file_format=? "
+ + "WHERE id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, row.timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, row.filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(3, row.dim.width);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(4, row.dim.height);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(5, row.original_orientation);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(6, row.file_format.serialize());
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(7, row.id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("BackingPhotoTable.update", res);
+ }
+
+ public void update_attributes(BackingPhotoID id, time_t timestamp, int64 filesize) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE BackingPhotoTable SET timestamp=?, filesize=? WHERE id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(3, id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("BackingPhotoTable.update_attributes", res);
+ }
+
+ public void remove(BackingPhotoID backing_id) throws DatabaseError {
+ delete_by_id(backing_id.id);
+ }
+
+ public void set_filepath(BackingPhotoID id, string filepath) throws DatabaseError {
+ update_text_by_id_2(id.id, "filepath", filepath);
+ }
+
+ public void update_timestamp(BackingPhotoID id, time_t timestamp) throws DatabaseError {
+ update_int64_by_id_2(id.id, "timestamp", timestamp);
+ }
+}
+
diff --git a/src/db/SavedSearchDBTable.vala b/src/db/SavedSearchDBTable.vala
new file mode 100644
index 0000000..d986038
--- /dev/null
+++ b/src/db/SavedSearchDBTable.vala
@@ -0,0 +1,641 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public struct SavedSearchID {
+ public const int64 INVALID = -1;
+
+ public int64 id;
+
+ public SavedSearchID(int64 id = INVALID) {
+ this.id = id;
+ }
+
+ public bool is_invalid() {
+ return (id == INVALID);
+ }
+
+ public bool is_valid() {
+ return (id != INVALID);
+ }
+}
+
+public class SavedSearchRow {
+ public SavedSearchID search_id;
+
+ public string name;
+ public SearchOperator operator;
+ public Gee.List<SearchCondition> conditions;
+}
+
+public class SavedSearchDBTable : DatabaseTable {
+ private static SavedSearchDBTable instance = null;
+
+ private SavedSearchDBTable() {
+ set_table_name("SavedSearchDBTable");
+
+ // Create main search table.
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ + "SavedSearchDBTable "
+ + "("
+ + "id INTEGER PRIMARY KEY, "
+ + "name TEXT UNIQUE NOT NULL, "
+ + "operator TEXT NOT NULL"
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable", res);
+
+ // Create search text table.
+ res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ + "SavedSearchDBTable_Text "
+ + "("
+ + "id INTEGER PRIMARY KEY, "
+ + "search_id INTEGER NOT NULL, "
+ + "search_type TEXT NOT NULL, "
+ + "context TEXT NOT NULL, "
+ + "text TEXT"
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_Text", res);
+
+ // Create search media type table.
+ res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ + "SavedSearchDBTable_MediaType "
+ + "("
+ + "id INTEGER PRIMARY KEY, "
+ + "search_id INTEGER NOT NULL, "
+ + "search_type TEXT NOT NULL, "
+ + "context TEXT NOT NULL, "
+ + "type TEXT NOT_NULL"
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_MediaType", res);
+
+ // Create flagged search table.
+ res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ + "SavedSearchDBTable_Flagged "
+ + "("
+ + "id INTEGER PRIMARY KEY, "
+ + "search_id INTEGER NOT NULL, "
+ + "search_type TEXT NOT NULL, "
+ + "flag_state TEXT NOT NULL"
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_Flagged", res);
+
+ // Create modified search table.
+ res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ + "SavedSearchDBTable_Modified "
+ + "("
+ + "id INTEGER PRIMARY KEY, "
+ + "search_id INTEGER NOT NULL, "
+ + "search_type TEXT NOT NULL, "
+ + "context TEXT NOT NULL, "
+ + "modified_state TEXT NOT NULL"
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_Modified", res);
+
+ // Create rating search table.
+ res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ + "SavedSearchDBTable_Rating "
+ + "("
+ + "id INTEGER PRIMARY KEY, "
+ + "search_id INTEGER NOT NULL, "
+ + "search_type TEXT NOT NULL, "
+ + "rating INTEGER NOT_NULL, "
+ + "context TEXT NOT NULL"
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_Rating", res);
+
+ // Create date search table.
+ res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ + "SavedSearchDBTable_Date "
+ + "("
+ + "id INTEGER PRIMARY KEY, "
+ + "search_id INTEGER NOT NULL, "
+ + "search_type TEXT NOT NULL, "
+ + "context TEXT NOT NULL, "
+ + "date_one INTEGER NOT_NULL, "
+ + "date_two INTEGER NOT_NULL"
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_Rating", res);
+
+ // Create indexes.
+ res = db.prepare_v2("CREATE INDEX IF NOT EXISTS "
+ + "SavedSearchDBTable_Text_Index "
+ + "ON SavedSearchDBTable_Text(search_id)", -1, out stmt);
+ assert(res == Sqlite.OK);
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_Text_Index", res);
+
+ res = db.prepare_v2("CREATE INDEX IF NOT EXISTS "
+ + "SavedSearchDBTable_MediaType_Index "
+ + "ON SavedSearchDBTable_MediaType(search_id)", -1, out stmt);
+ assert(res == Sqlite.OK);
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_MediaType_Index", res);
+
+ res = db.prepare_v2("CREATE INDEX IF NOT EXISTS "
+ + "SavedSearchDBTable_Flagged_Index "
+ + "ON SavedSearchDBTable_Flagged(search_id)", -1, out stmt);
+ assert(res == Sqlite.OK);
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_Flagged_Index", res);
+
+ res = db.prepare_v2("CREATE INDEX IF NOT EXISTS "
+ + "SavedSearchDBTable_Modified_Index "
+ + "ON SavedSearchDBTable_Modified(search_id)", -1, out stmt);
+ assert(res == Sqlite.OK);
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_Modified_Index", res);
+
+ res = db.prepare_v2("CREATE INDEX IF NOT EXISTS "
+ + "SavedSearchDBTable_Rating_Index "
+ + "ON SavedSearchDBTable_Rating(search_id)", -1, out stmt);
+ assert(res == Sqlite.OK);
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_Rating_Index", res);
+
+ res = db.prepare_v2("CREATE INDEX IF NOT EXISTS "
+ + "SavedSearchDBTable_Date_Index "
+ + "ON SavedSearchDBTable_Date(search_id)", -1, out stmt);
+ assert(res == Sqlite.OK);
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create SavedSearchDBTable_Date_Index", res);
+ }
+
+ public static SavedSearchDBTable get_instance() {
+ if (instance == null)
+ instance = new SavedSearchDBTable();
+
+ return instance;
+ }
+
+ public SavedSearchRow add(string name, SearchOperator operator,
+ Gee.ArrayList<SearchCondition> conditions) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO SavedSearchDBTable (name, operator) VALUES (?, ?)", -1,
+ out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(1, name);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(2, operator.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("SavedSearchDBTable.add", res);
+
+ SavedSearchRow row = new SavedSearchRow();
+ row.search_id = SavedSearchID(db.last_insert_rowid());
+ row.name = name;
+ row.operator = operator;
+ row.conditions = conditions;
+
+ foreach (SearchCondition sc in conditions) {
+ add_condition(row.search_id, sc);
+ }
+
+ return row;
+ }
+
+ private void add_condition(SavedSearchID id, SearchCondition condition) throws DatabaseError {
+ if (condition is SearchConditionText) {
+ SearchConditionText text = condition as SearchConditionText;
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO SavedSearchDBTable_Text (search_id, search_type, context, "
+ + "text) VALUES (?, ?, ?, ?)", -1,
+ out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(2, text.search_type.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(3, text.context.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(4, text.text);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("SavedSearchDBTable_Text.add", res);
+ } else if (condition is SearchConditionMediaType) {
+ SearchConditionMediaType media_type = condition as SearchConditionMediaType;
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO SavedSearchDBTable_MediaType (search_id, search_type, context, "
+ + "type) VALUES (?, ?, ?, ?)", -1,
+ out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(2, media_type.search_type.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(3, media_type.context.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(4, media_type.media_type.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("SavedSearchDBTable_MediaType.add", res);
+ } else if (condition is SearchConditionFlagged) {
+ SearchConditionFlagged flag_state = condition as SearchConditionFlagged;
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO SavedSearchDBTable_Flagged (search_id, search_type, "
+ + "flag_state) VALUES (?, ?, ?)", -1,
+ out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(2, flag_state.search_type.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(3, flag_state.state.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("SavedSearchDBTable_Flagged.add", res);
+ } else if (condition is SearchConditionModified) {
+ SearchConditionModified modified_state = condition as SearchConditionModified;
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO SavedSearchDBTable_Modified (search_id, search_type, context, "
+ + "modified_state) VALUES (?, ?, ?, ?)", -1,
+ out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(2, modified_state.search_type.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(3, modified_state.context.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(4, modified_state.state.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("SavedSearchDBTable_Modified.add", res);
+ } else if (condition is SearchConditionRating) {
+ SearchConditionRating rating = condition as SearchConditionRating;
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO SavedSearchDBTable_Rating (search_id, search_type, rating, "
+ + "context) VALUES (?, ?, ?, ?)", -1,
+ out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(2, rating.search_type.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int(3, rating.rating.serialize());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(4, rating.context.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("SavedSearchDBTable_Rating.add", res);
+ } else if (condition is SearchConditionDate) {
+ SearchConditionDate date = condition as SearchConditionDate;
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO SavedSearchDBTable_Date (search_id, search_type, "
+ + "context, date_one, date_two) VALUES (?, ?, ?, ?, ?)", -1,
+ out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(2, date.search_type.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(3, date.context.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(4, date.date_one.to_unix());
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(5, date.date_two.to_unix());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("SavedSearchDBTable_Date.add", res);
+ } else {
+ assert_not_reached();
+ }
+ }
+
+ // Removes the conditions of a search. Used on delete.
+ private void remove_conditions_for_search_id(SavedSearchID search_id) throws DatabaseError {
+ remove_conditions_for_table("SavedSearchDBTable_Text", search_id);
+ remove_conditions_for_table("SavedSearchDBTable_MediaType", search_id);
+ remove_conditions_for_table("SavedSearchDBTable_Flagged", search_id);
+ remove_conditions_for_table("SavedSearchDBTable_Modified", search_id);
+ remove_conditions_for_table("SavedSearchDBTable_Rating", search_id);
+ remove_conditions_for_table("SavedSearchDBTable_Date", search_id);
+ }
+
+ private void remove_conditions_for_table(string table_name, SavedSearchID search_id)
+ throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("DELETE FROM %s WHERE search_id=?".printf(table_name), -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, search_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("%s.remove".printf(table_name), res);
+ }
+
+ // Returns all conditions for a given search. Used on loading a search.
+ private Gee.List<SearchCondition> get_conditions_for_id(SavedSearchID search_id)
+ throws DatabaseError {
+ Gee.List<SearchCondition> list = new Gee.ArrayList<SearchCondition>();
+ Sqlite.Statement stmt;
+ int res;
+
+ // Get all text conditions.
+ res = db.prepare_v2("SELECT search_type, context, text FROM SavedSearchDBTable_Text "
+ + "WHERE search_id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, search_id.id);
+ assert(res == Sqlite.OK);
+
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ break;
+ else if (res != Sqlite.ROW)
+ throw_error("SavedSearchDBTable_Text.get_all_rows", res);
+
+ SearchConditionText condition = new SearchConditionText(
+ SearchCondition.SearchType.from_string(stmt.column_text(0)),
+ stmt.column_text(2),
+ SearchConditionText.Context.from_string(stmt.column_text(1)));
+
+ list.add(condition);
+ }
+
+ // Get all media type conditions.
+ res = db.prepare_v2("SELECT search_type, context, type FROM SavedSearchDBTable_MediaType "
+ + "WHERE search_id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, search_id.id);
+ assert(res == Sqlite.OK);
+
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ break;
+ else if (res != Sqlite.ROW)
+ throw_error("SavedSearchDBTable_MediaType.get_all_rows", res);
+
+ SearchConditionMediaType condition = new SearchConditionMediaType(
+ SearchCondition.SearchType.from_string(stmt.column_text(0)),
+ SearchConditionMediaType.Context.from_string(stmt.column_text(1)),
+ SearchConditionMediaType.MediaType.from_string(stmt.column_text(2)));
+
+ list.add(condition);
+ }
+
+ // Get all flagged state conditions.
+ res = db.prepare_v2("SELECT search_type, flag_state FROM SavedSearchDBTable_Flagged "
+ + "WHERE search_id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, search_id.id);
+ assert(res == Sqlite.OK);
+
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ break;
+ else if (res != Sqlite.ROW)
+ throw_error("SavedSearchDBTable_Flagged.get_all_rows", res);
+
+ SearchConditionFlagged condition = new SearchConditionFlagged(
+ SearchCondition.SearchType.from_string(stmt.column_text(0)),
+ SearchConditionFlagged.State.from_string(stmt.column_text(1)));
+
+ list.add(condition);
+ }
+
+ // Get all modified state conditions.
+ res = db.prepare_v2("SELECT search_type, context, modified_state FROM SavedSearchDBTable_Modified "
+ + "WHERE search_id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, search_id.id);
+ assert(res == Sqlite.OK);
+
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ break;
+ else if (res != Sqlite.ROW)
+ throw_error("SavedSearchDBTable_Modified.get_all_rows", res);
+
+ SearchConditionModified condition = new SearchConditionModified(
+ SearchCondition.SearchType.from_string(stmt.column_text(0)),
+ SearchConditionModified.Context.from_string(stmt.column_text(1)),
+ SearchConditionModified.State.from_string(stmt.column_text(2)));
+
+ list.add(condition);
+ }
+
+ // Get all rating conditions.
+ res = db.prepare_v2("SELECT search_type, rating, context FROM SavedSearchDBTable_Rating "
+ + "WHERE search_id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, search_id.id);
+ assert(res == Sqlite.OK);
+
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ break;
+ else if (res != Sqlite.ROW)
+ throw_error("SavedSearchDBTable_Rating.get_all_rows", res);
+
+ SearchConditionRating condition = new SearchConditionRating(
+ SearchCondition.SearchType.from_string(stmt.column_text(0)),
+ Rating.unserialize(stmt.column_int(1)),
+ SearchConditionRating.Context.from_string(stmt.column_text(2)));
+
+ list.add(condition);
+ }
+
+ // Get all date conditions.
+ res = db.prepare_v2("SELECT search_type, context, date_one, date_two FROM SavedSearchDBTable_Date "
+ + "WHERE search_id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, search_id.id);
+ assert(res == Sqlite.OK);
+
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ break;
+ else if (res != Sqlite.ROW)
+ throw_error("SavedSearchDBTable_Date.get_all_rows", res);
+
+ SearchConditionDate condition = new SearchConditionDate(
+ SearchCondition.SearchType.from_string(stmt.column_text(0)),
+ SearchConditionDate.Context.from_string(stmt.column_text(1)),
+ new DateTime.from_unix_local(stmt.column_int64(2)),
+ new DateTime.from_unix_local(stmt.column_int64(3)));
+ list.add(condition);
+ }
+
+ return list;
+ }
+
+ // All fields but search_id are respected in SavedSearchRow.
+ public SavedSearchID create_from_row(SavedSearchRow row) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO SavedSearchDBTable (name, operator) VALUES (?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(1, row.name);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(2, row.operator.to_string());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("SavedSearchDBTable.create_from_row", res);
+
+ SavedSearchID search_id = SavedSearchID(db.last_insert_rowid());
+
+ foreach (SearchCondition sc in row.conditions) {
+ add_condition(search_id, sc);
+ }
+
+ return search_id;
+ }
+
+ public void remove(SavedSearchID search_id) throws DatabaseError {
+ remove_conditions_for_search_id(search_id);
+ delete_by_id(search_id.id);
+ }
+
+ public SavedSearchRow? get_row(SavedSearchID search_id) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT name, operator FROM SavedSearchDBTable WHERE id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, search_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ return null;
+ else if (res != Sqlite.ROW)
+ throw_error("SavedSearchDBTable.get_row", res);
+
+ SavedSearchRow row = new SavedSearchRow();
+ row.search_id = search_id;
+ row.name = stmt.column_text(0);
+ row.operator = SearchOperator.from_string(stmt.column_text(1));
+
+ return row;
+ }
+
+ public Gee.List<SavedSearchRow?> get_all_rows() throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id, name, operator FROM SavedSearchDBTable", -1,
+ out stmt);
+ assert(res == Sqlite.OK);
+
+ Gee.List<SavedSearchRow?> rows = new Gee.ArrayList<SavedSearchRow?>();
+
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ break;
+ else if (res != Sqlite.ROW)
+ throw_error("SavedSearchDBTable.get_all_rows", res);
+
+ SavedSearchRow row = new SavedSearchRow();
+ row.search_id = SavedSearchID(stmt.column_int64(0));
+ row.name = stmt.column_text(1);
+ row.operator = SearchOperator.from_string(stmt.column_text(2));
+ row.conditions = get_conditions_for_id(row.search_id);
+
+ rows.add(row);
+ }
+
+ return rows;
+ }
+
+ public void rename(SavedSearchID search_id, string new_name) throws DatabaseError {
+ update_text_by_id_2(search_id.id, "name", new_name);
+ }
+}
+
diff --git a/src/db/TagTable.vala b/src/db/TagTable.vala
new file mode 100644
index 0000000..a0fade8
--- /dev/null
+++ b/src/db/TagTable.vala
@@ -0,0 +1,248 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public struct TagID {
+ public const int64 INVALID = -1;
+
+ public int64 id;
+
+ public TagID(int64 id = INVALID) {
+ this.id = id;
+ }
+
+ public bool is_invalid() {
+ return (id == INVALID);
+ }
+
+ public bool is_valid() {
+ return (id != INVALID);
+ }
+}
+
+public class TagRow {
+ public TagID tag_id;
+ public string name;
+ public Gee.Set<string>? source_id_list;
+ public time_t time_created;
+}
+
+public class TagTable : DatabaseTable {
+ private static TagTable instance = null;
+
+ private TagTable() {
+ set_table_name("TagTable");
+
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ + "TagTable "
+ + "("
+ + "id INTEGER PRIMARY KEY, "
+ + "name TEXT UNIQUE NOT NULL, "
+ + "photo_id_list TEXT, "
+ + "time_created INTEGER"
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create TagTable", res);
+ }
+
+ public static TagTable get_instance() {
+ if (instance == null)
+ instance = new TagTable();
+
+ return instance;
+ }
+
+ public static void upgrade_for_htags() {
+ TagTable table = get_instance();
+
+ try {
+ Gee.List<TagRow?> rows = table.get_all_rows();
+
+ foreach (TagRow row in rows) {
+ row.name = row.name.replace(Tag.PATH_SEPARATOR_STRING, "-");
+ table.rename(row.tag_id, row.name);
+ }
+ } catch (DatabaseError e) {
+ error ("TagTable: can't upgrade tag names for hierarchical tag support: %s", e.message);
+ }
+ }
+
+ public TagRow add(string name) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO TagTable (name, time_created) VALUES (?, ?)", -1,
+ out stmt);
+ assert(res == Sqlite.OK);
+
+ time_t time_created = (time_t) now_sec();
+
+ res = stmt.bind_text(1, name);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, time_created);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("TagTable.add", res);
+
+ TagRow row = new TagRow();
+ row.tag_id = TagID(db.last_insert_rowid());
+ row.name = name;
+ row.source_id_list = null;
+ row.time_created = time_created;
+
+ return row;
+ }
+
+ // All fields but tag_id are respected in TagRow.
+ public TagID create_from_row(TagRow row) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO TagTable (name, photo_id_list, time_created) VALUES (?, ?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(1, row.name);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(2, serialize_source_ids(row.source_id_list));
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(3, row.time_created);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("TagTable.create_from_row", res);
+
+ return TagID(db.last_insert_rowid());
+ }
+
+ public void remove(TagID tag_id) throws DatabaseError {
+ delete_by_id(tag_id.id);
+ }
+
+ public string? get_name(TagID tag_id) throws DatabaseError {
+ Sqlite.Statement stmt;
+ if (!select_by_id(tag_id.id, "name", out stmt))
+ return null;
+
+ return stmt.column_text(0);
+ }
+
+ public TagRow? get_row(TagID tag_id) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT name, photo_id_list, time_created FROM TagTable WHERE id=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, tag_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ return null;
+ else if (res != Sqlite.ROW)
+ throw_error("TagTable.get_row", res);
+
+ TagRow row = new TagRow();
+ row.tag_id = tag_id;
+ row.name = stmt.column_text(0);
+ row.source_id_list = unserialize_source_ids(stmt.column_text(1));
+ row.time_created = (time_t) stmt.column_int64(2);
+
+ return row;
+ }
+
+ public Gee.List<TagRow?> get_all_rows() throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id, name, photo_id_list, time_created FROM TagTable", -1,
+ out stmt);
+ assert(res == Sqlite.OK);
+
+ Gee.List<TagRow?> rows = new Gee.ArrayList<TagRow?>();
+
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ break;
+ else if (res != Sqlite.ROW)
+ throw_error("TagTable.get_all_rows", res);
+
+ // res == Sqlite.ROW
+ TagRow row = new TagRow();
+ row.tag_id = TagID(stmt.column_int64(0));
+ row.name = stmt.column_text(1);
+ row.source_id_list = unserialize_source_ids(stmt.column_text(2));
+ row.time_created = (time_t) stmt.column_int64(3);
+
+ rows.add(row);
+ }
+
+ return rows;
+ }
+
+ public void rename(TagID tag_id, string new_name) throws DatabaseError {
+ update_text_by_id_2(tag_id.id, "name", new_name);
+ }
+
+ public void set_tagged_sources(TagID tag_id, Gee.Collection<string> source_ids) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE TagTable SET photo_id_list=? WHERE id=?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(1, serialize_source_ids(source_ids));
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, tag_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("TagTable.set_tagged_photos", res);
+ }
+
+ private string? serialize_source_ids(Gee.Collection<string>? source_ids) {
+ if (source_ids == null)
+ return null;
+
+ StringBuilder result = new StringBuilder();
+
+ foreach (string source_id in source_ids) {
+ result.append(source_id);
+ result.append(",");
+ }
+
+ return (result.len != 0) ? result.str : null;
+ }
+
+ private Gee.Set<string> unserialize_source_ids(string? text_list) {
+ Gee.Set<string> result = new Gee.HashSet<string>();
+
+ if (text_list == null)
+ return result;
+
+ string[] split = text_list.split(",");
+ foreach (string token in split) {
+ if (is_string_empty(token))
+ continue;
+
+ // handle current and legacy encoding of source ids -- in the past, we only stored
+ // LibraryPhotos in tags so we only needed to store the numeric database key of the
+ // photo to uniquely identify it. Now, however, tags can store arbitrary MediaSources,
+ // so instead of simply storing a number we store the source id, a string that contains
+ // a typename followed by an identifying number (e.g., "video-022354").
+ if (token[0].isdigit()) {
+ // this is a legacy entry
+ result.add(PhotoID.upgrade_photo_id_to_source_id(PhotoID(parse_int64(token, 10))));
+ } else if (token[0].isalpha()) {
+ // this is a modern entry
+ result.add(token);
+ }
+ }
+
+ return result;
+ }
+}
+
diff --git a/src/db/TombstoneTable.vala b/src/db/TombstoneTable.vala
new file mode 100644
index 0000000..9e108bd
--- /dev/null
+++ b/src/db/TombstoneTable.vala
@@ -0,0 +1,146 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public struct TombstoneID {
+ public const int64 INVALID = -1;
+
+ public int64 id;
+
+ public TombstoneID(int64 id = INVALID) {
+ this.id = id;
+ }
+
+ public bool is_invalid() {
+ return (id == INVALID);
+ }
+
+ public bool is_valid() {
+ return (id != INVALID);
+ }
+}
+
+public class TombstoneRow {
+ public TombstoneID id;
+ public string filepath;
+ public int64 filesize;
+ public string? md5;
+ public time_t time_created;
+ public Tombstone.Reason reason;
+}
+
+public class TombstoneTable : DatabaseTable {
+ private static TombstoneTable instance = null;
+
+ private TombstoneTable() {
+ set_table_name("TombstoneTable");
+
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS "
+ + "TombstoneTable "
+ + "("
+ + "id INTEGER PRIMARY KEY, "
+ + "filepath TEXT NOT NULL, "
+ + "filesize INTEGER, "
+ + "md5 TEXT, "
+ + "time_created INTEGER, "
+ + "reason INTEGER DEFAULT 0 "
+ + ")", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create TombstoneTable", res);
+ }
+
+ public static TombstoneTable get_instance() {
+ if (instance == null)
+ instance = new TombstoneTable();
+
+ return instance;
+ }
+
+ public TombstoneRow add(string filepath, int64 filesize, string? md5, Tombstone.Reason reason)
+ throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("INSERT INTO TombstoneTable "
+ + "(filepath, filesize, md5, time_created, reason) "
+ + "VALUES (?, ?, ?, ?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ time_t time_created = (time_t) now_sec();
+
+ res = stmt.bind_text(1, filepath);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(3, md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(4, (int64) time_created);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(5, reason.serialize());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("TombstoneTable.add", res);
+
+ TombstoneRow row = new TombstoneRow();
+ row.id = TombstoneID(db.last_insert_rowid());
+ row.filepath = filepath;
+ row.filesize = filesize;
+ row.md5 = md5;
+ row.time_created = time_created;
+ row.reason = reason;
+
+ return row;
+ }
+
+ public TombstoneRow[]? fetch_all() throws DatabaseError {
+ int row_count = get_row_count();
+ if (row_count == 0)
+ return null;
+
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id, filepath, filesize, md5, time_created, reason "
+ + "FROM TombstoneTable", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ TombstoneRow[] rows = new TombstoneRow[row_count];
+
+ int index = 0;
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ break;
+ else if (res != Sqlite.ROW)
+ throw_error("TombstoneTable.fetch_all", res);
+
+ TombstoneRow row = new TombstoneRow();
+ row.id = TombstoneID(stmt.column_int64(0));
+ row.filepath = stmt.column_text(1);
+ row.filesize = stmt.column_int64(2);
+ row.md5 = stmt.column_text(3);
+ row.time_created = (time_t) stmt.column_int64(4);
+ row.reason = Tombstone.Reason.unserialize(stmt.column_int(5));
+
+ rows[index++] = row;
+ }
+
+ assert(index == row_count);
+
+ return rows;
+ }
+
+ public void update_file(TombstoneID tombstone_id, string filepath) throws DatabaseError {
+ update_text_by_id_2(tombstone_id.id, "filepath", filepath);
+ }
+
+ public void remove(TombstoneID tombstone_id) throws DatabaseError {
+ delete_by_id(tombstone_id.id);
+ }
+}
+
diff --git a/src/db/VersionTable.vala b/src/db/VersionTable.vala
new file mode 100644
index 0000000..9003b1b
--- /dev/null
+++ b/src/db/VersionTable.vala
@@ -0,0 +1,98 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public class VersionTable : DatabaseTable {
+ private static VersionTable instance = null;
+
+ private VersionTable() {
+ 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);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("create version table", res);
+
+ set_table_name("VersionTable");
+ }
+
+ public static VersionTable get_instance() {
+ if (instance == null)
+ instance = new VersionTable();
+
+ return instance;
+ }
+
+ public int get_version(out string app_version) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT schema_version, app_version FROM VersionTable ORDER BY schema_version DESC LIMIT 1",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.ROW) {
+ if (res != Sqlite.DONE)
+ fatal("get_version", res);
+
+ app_version = null;
+
+ return -1;
+ }
+
+ app_version = stmt.column_text(1);
+
+ return stmt.column_int(0);
+ }
+
+ public void set_version(int version, string app_version, string? user_data = null) {
+ Sqlite.Statement stmt;
+
+ string bitbucket;
+ if (get_version(out bitbucket) != -1) {
+ // overwrite existing row
+ int res = db.prepare_v2("UPDATE VersionTable SET schema_version=?, app_version=?, user_data=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+ } else {
+ // insert new row
+ int res = db.prepare_v2("INSERT INTO VersionTable (schema_version, app_version, user_data) VALUES (?,?, ?)",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+ }
+
+ int res = stmt.bind_int(1, version);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(2, app_version);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(3, user_data);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("set_version %d %s %s".printf(version, app_version, user_data), res);
+ }
+
+ public void update_version(int version, string app_version) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE VersionTable SET schema_version=?, app_version=?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int(1, version);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(2, app_version);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ fatal("update_version %d".printf(version), res);
+ }
+}
+
diff --git a/src/db/VideoTable.vala b/src/db/VideoTable.vala
new file mode 100644
index 0000000..c681dc2
--- /dev/null
+++ b/src/db/VideoTable.vala
@@ -0,0 +1,462 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public struct VideoID {
+ public const int64 INVALID = -1;
+
+ public int64 id;
+
+ public VideoID(int64 id = INVALID) {
+ this.id = id;
+ }
+
+ public bool is_invalid() {
+ return (id == INVALID);
+ }
+
+ public bool is_valid() {
+ return (id != INVALID);
+ }
+
+ public static uint hash(VideoID? a) {
+ return int64_hash(a.id);
+ }
+
+ public static bool equal(void *a, void *b) {
+ return ((VideoID *) a)->id == ((VideoID *) b)->id;
+ }
+
+ public static string upgrade_video_id_to_source_id(VideoID video_id) {
+ return ("%s-%016" + int64.FORMAT_MODIFIER + "x").printf(Video.TYPENAME, video_id.id);
+ }
+}
+
+public class VideoRow {
+ public VideoID video_id;
+ public string filepath;
+ public int64 filesize;
+ public time_t timestamp;
+ public int width;
+ public int height;
+ public double clip_duration;
+ public bool is_interpretable;
+ public time_t exposure_time;
+ public ImportID import_id;
+ public EventID event_id;
+ public string md5;
+ public time_t time_created;
+ public Rating rating;
+ public string title;
+ public string? backlinks;
+ public time_t time_reimported;
+ public uint64 flags;
+ public string comment;
+}
+
+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);
+
+ 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);
+
+ set_table_name("VideoTable");
+ }
+
+ public static VideoTable get_instance() {
+ if (instance == null)
+ instance = new VideoTable();
+
+ return instance;
+ }
+
+ // 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(
+ "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);
+
+ ulong time_created = now_sec();
+
+ res = stmt.bind_text(1, video_row.filepath);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(2, video_row.width);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(3, video_row.height);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_double(4, video_row.clip_duration);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int(5, (video_row.is_interpretable) ? 1 : 0);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(6, video_row.filesize);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(7, video_row.timestamp);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(8, video_row.exposure_time);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(9, video_row.import_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(10, EventID.INVALID);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(11, video_row.md5);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(12, time_created);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(13, video_row.title);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(14, video_row.comment);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ if (res != Sqlite.CONSTRAINT)
+ throw_error("VideoTable.add", res);
+ }
+
+ // fill in ignored fields with database values
+ video_row.video_id = VideoID(db.last_insert_rowid());
+ video_row.event_id = EventID();
+ video_row.time_created = (time_t) time_created;
+ video_row.flags = 0;
+
+ 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;
+ }
+
+ return true;
+ }
+
+ public VideoRow? get_row(VideoID video_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "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=?",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, video_id.id);
+ assert(res == Sqlite.OK);
+
+ if (stmt.step() != Sqlite.ROW)
+ return null;
+
+ VideoRow row = new VideoRow();
+ row.video_id = video_id;
+ row.filepath = stmt.column_text(0);
+ row.width = stmt.column_int(1);
+ row.height = stmt.column_int(2);
+ row.clip_duration = stmt.column_double(3);
+ row.is_interpretable = (stmt.column_int(4) == 1);
+ row.filesize = stmt.column_int64(5);
+ row.timestamp = (time_t) stmt.column_int64(6);
+ row.exposure_time = (time_t) stmt.column_int64(7);
+ row.import_id.id = stmt.column_int64(8);
+ row.event_id.id = stmt.column_int64(9);
+ row.md5 = stmt.column_text(10);
+ row.time_created = (time_t) stmt.column_int64(11);
+ row.rating = Rating.unserialize(stmt.column_int(12));
+ row.title = stmt.column_text(13);
+ row.backlinks = stmt.column_text(14);
+ row.time_reimported = (time_t) stmt.column_int64(15);
+ row.flags = stmt.column_int64(16);
+ row.comment = stmt.column_text(17);
+
+ return row;
+ }
+
+ public Gee.ArrayList<VideoRow?> get_all() {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(
+ "SELECT id, 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",
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ Gee.ArrayList<VideoRow?> all = new Gee.ArrayList<VideoRow?>();
+
+ while ((res = stmt.step()) == Sqlite.ROW) {
+ VideoRow row = new VideoRow();
+ row.video_id.id = stmt.column_int64(0);
+ row.filepath = stmt.column_text(1);
+ row.width = stmt.column_int(2);
+ row.height = stmt.column_int(3);
+ row.clip_duration = stmt.column_double(4);
+ row.is_interpretable = (stmt.column_int(5) == 1);
+ row.filesize = stmt.column_int64(6);
+ row.timestamp = (time_t) stmt.column_int64(7);
+ row.exposure_time = (time_t) stmt.column_int64(8);
+ row.import_id.id = stmt.column_int64(9);
+ row.event_id.id = stmt.column_int64(10);
+ row.md5 = stmt.column_text(11);
+ row.time_created = (time_t) stmt.column_int64(12);
+ row.rating = Rating.unserialize(stmt.column_int(13));
+ row.title = stmt.column_text(14);
+ row.backlinks = stmt.column_text(15);
+ row.time_reimported = (time_t) stmt.column_int64(16);
+ row.flags = stmt.column_int64(17);
+ row.comment = stmt.column_text(18);
+
+ all.add(row);
+ }
+
+ return all;
+ }
+
+ public void set_filepath(VideoID video_id, string filepath) throws DatabaseError {
+ update_text_by_id_2(video_id.id, "filename", filepath);
+ }
+
+ public void set_title(VideoID video_id, string? new_title) throws DatabaseError {
+ update_text_by_id_2(video_id.id, "title", new_title != null ? new_title : "");
+ }
+
+ public void set_comment(VideoID video_id, string? new_comment) throws DatabaseError {
+ update_text_by_id_2(video_id.id, "comment", new_comment != null ? new_comment : "");
+ }
+
+ public void set_exposure_time(VideoID video_id, time_t time) throws DatabaseError {
+ update_int64_by_id_2(video_id.id, "exposure_time", (int64) time);
+ }
+
+ public void set_rating(VideoID video_id, Rating rating) throws DatabaseError {
+ update_int64_by_id_2(video_id.id, "rating", rating.serialize());
+ }
+
+ public void set_flags(VideoID video_id, uint64 flags) throws DatabaseError {
+ update_int64_by_id_2(video_id.id, "flags", (int64) flags);
+ }
+
+ public void update_backlinks(VideoID video_id, string? backlinks) throws DatabaseError {
+ update_text_by_id_2(video_id.id, "backlinks", backlinks != null ? backlinks : "");
+ }
+
+ public void update_is_interpretable(VideoID video_id, bool is_interpretable) throws DatabaseError {
+ update_int_by_id_2(video_id.id, "is_interpretable", (is_interpretable) ? 1 : 0);
+ }
+
+ public bool set_event(VideoID video_id, EventID event_id) {
+ return update_int64_by_id(video_id.id, "event_id", event_id.id);
+ }
+
+ public void remove_by_file(File file) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("DELETE FROM VideoTable WHERE filename=?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(1, file.get_path());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("VideoTable.remove_by_file", res);
+ }
+
+ public void remove(VideoID videoID) throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("DELETE FROM VideoTable WHERE id=?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, videoID.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("VideoTable.remove", res);
+ }
+
+ public bool is_video_stored(File file) {
+ return get_id(file).is_valid();
+ }
+
+ public VideoID get_id(File file) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT ID FROM VideoTable WHERE filename=?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_text(1, file.get_path());
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+
+ return (res == Sqlite.ROW) ? VideoID(stmt.column_int64(0)) : VideoID();
+ }
+
+ public Gee.ArrayList<VideoID?> get_videos() throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id FROM VideoTable", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ Gee.ArrayList<VideoID?> video_ids = new Gee.ArrayList<VideoID?>();
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ break;
+ } else if (res != Sqlite.ROW) {
+ throw_error("VideoTable.get_videos", res);
+ }
+
+ video_ids.add(VideoID(stmt.column_int64(0)));
+ }
+
+ return video_ids;
+ }
+
+ private Sqlite.Statement get_duplicate_stmt(File? file, string? md5) {
+ assert(file != null || md5 != null);
+
+ string sql = "SELECT id FROM VideoTable WHERE";
+ bool first = true;
+
+ if (file != null) {
+ sql += " filename=?";
+ first = false;
+ }
+
+ if (md5 != null) {
+ if (!first)
+ sql += " OR ";
+
+ sql += " md5=?";
+ }
+
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2(sql, -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ int col = 1;
+
+ if (file != null) {
+ res = stmt.bind_text(col++, file.get_path());
+ assert(res == Sqlite.OK);
+ }
+
+ if (md5 != null) {
+ res = stmt.bind_text(col++, md5);
+ assert(res == Sqlite.OK);
+ }
+
+ return stmt;
+ }
+
+ public bool has_duplicate(File? file, string? md5) {
+ Sqlite.Statement stmt = get_duplicate_stmt(file, md5);
+ int res = stmt.step();
+
+ if (res == Sqlite.DONE) {
+ // not found
+ return false;
+ } else if (res == Sqlite.ROW) {
+ // at least one found
+ return true;
+ } else {
+ fatal("VideoTable.has_duplicate", res);
+ }
+
+ return false;
+ }
+
+ public VideoID[] get_duplicate_ids(File? file, string? md5) {
+ Sqlite.Statement stmt = get_duplicate_stmt(file, md5);
+
+ VideoID[] ids = new VideoID[0];
+
+ int res = stmt.step();
+ while (res == Sqlite.ROW) {
+ ids += VideoID(stmt.column_int64(0));
+ res = stmt.step();
+ }
+
+ return ids;
+ }
+
+ public Gee.ArrayList<string> get_event_source_ids(EventID event_id) {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id FROM VideoTable WHERE event_id = ?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ res = stmt.bind_int64(1, event_id.id);
+ assert(res == Sqlite.OK);
+
+ Gee.ArrayList<string> result = new Gee.ArrayList<string>();
+ for(;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE) {
+ break;
+ } else if (res != Sqlite.ROW) {
+ fatal("get_event_source_ids", res);
+
+ break;
+ }
+
+ result.add(VideoID.upgrade_video_id_to_source_id(VideoID(stmt.column_int64(0))));
+ }
+
+ return result;
+ }
+
+ public void set_timestamp(VideoID video_id, time_t timestamp) throws DatabaseError {
+ update_int64_by_id_2(video_id.id, "timestamp", (int64) timestamp);
+ }
+}
+
diff --git a/src/db/mk/db.mk b/src/db/mk/db.mk
new file mode 100644
index 0000000..421961a
--- /dev/null
+++ b/src/db/mk/db.mk
@@ -0,0 +1,35 @@
+
+# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with
+# a init() and terminate() function declared in the namespace.
+UNIT_NAME := Db
+
+# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all
+# lowercase. The name of this file should be UNIT_DIR.mk.
+UNIT_DIR := db
+
+# All Vala files in the unit should be listed here with no subdirectory prefix.
+#
+# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala.
+UNIT_FILES := \
+ DatabaseTable.vala \
+ PhotoTable.vala \
+ EventTable.vala \
+ TagTable.vala \
+ TombstoneTable.vala \
+ VideoTable.vala \
+ VersionTable.vala \
+ SavedSearchDBTable.vala
+
+# Any unit this unit relies upon (and should be initialized before it's initialized) should
+# be listed here using its Vala namespace.
+#
+# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here.
+UNIT_USES :=
+
+# List any additional files that are used in the build process as a part of this unit that should
+# be packaged in the tarball. File names should be relative to the unit's home directory.
+UNIT_RC :=
+
+# unitize.mk must be called at the end of each UNIT_DIR.mk file.
+include unitize.mk
+