[gnome-games] application: Add support for Favorites collection



commit fee1357d1e91240b769cda7d556661e43bc78b37
Author: Neville <nevilleantony98 gmail com>
Date:   Thu Jul 2 10:58:31 2020 +0530

    application: Add support for Favorites collection
    
    Add database queries for supporting a favorites collection.
    
    Add versioning support to Migrator using the existing .version file.
    This is used to migrate the database to support favorites collections
    by altering the games table to have a new is_favorite column.
    
    Add and modify database queries to support favorites collection.
    Move query statement preparation to a public prepare_statements() which
    must be called after creation of database. This provides better database
    migration support.
    
    Migration will now be applied before creation of GameCollection.

 src/core/migrator.vala     | 115 +++++++++++++++++++++++++++++++++++++------
 src/database/database.vala | 118 +++++++++++++++++++++++++++++++++++++++------
 src/ui/application.vala    |  15 +++---
 3 files changed, 208 insertions(+), 40 deletions(-)
---
diff --git a/src/core/migrator.vala b/src/core/migrator.vala
index 44b73dbc..eab75891 100644
--- a/src/core/migrator.vala
+++ b/src/core/migrator.vala
@@ -1,9 +1,103 @@
 // This file is part of GNOME Games. License: GPL-3.0+.
 
-public class Games.Migrator : Object {
+class Games.Migrator : Object {
+       // LATEST_VERSION should match the total number of migrations
+       private const uint LATEST_VERSION = 2;
+
+       private static uint version = 0;
+       private static bool skip_migration;
+
+       public static void bump_to_latest_version () {
+               info ("[Migrator]: Skipping migration");
+               while (version < LATEST_VERSION)
+                       bump_version ();
+
+               skip_migration = true;
+       }
+
        // Returns true if the migration wasn't necessary or
        // if it was performed succesfully
-       public static bool apply_migration_if_necessary () {
+       public static bool apply_migration_if_necessary (Database database) {
+               if (skip_migration)
+                       return true;
+
+               version = get_version ();
+
+               if (version == 0) {
+                       if (!apply_data_dir_migration ())
+                               return false;
+
+                       bump_version ();
+               }
+
+               if (version < 2) {
+                       try {
+                               database.apply_favorites_migration ();
+                       }
+                       catch (Error e) {
+                               critical ("Failed to apply favorites migration: %s", e.message);
+                               return false;
+                       }
+
+                       bump_version ();
+               }
+
+               return true;
+       }
+
+       private static uint get_version () {
+               var data_dir_path = Application.get_data_dir ();
+               var data_dir = File.new_for_path (data_dir_path);
+               var version_file = data_dir.get_child (".version");
+
+               if (version_file.query_exists ()) {
+                       try {
+                               var @file_input_stream = version_file.read ();
+                               var data_input_stream = new DataInputStream (@file_input_stream);
+                               string line;
+
+                               // .version contains version number => version 2+
+                               if ((line = data_input_stream.read_line ()) != null)
+                                       return uint.parse (line);
+
+                               //has .version file but no version in it => version 1
+                               return 1;
+                       } catch (Error e) {
+                               critical ("Failed to bump version: %s", e.message);
+                       }
+               }
+
+               // no .version file => version 0
+               return 0;
+       }
+
+       public static void bump_version () {
+               var data_dir_path = Application.get_data_dir ();
+               var data_dir = File.new_for_path (data_dir_path);
+               var version_file = data_dir.get_child (".version");
+
+               if (version > 0) {
+                       try {
+                               version_file.replace_contents ((++version).to_string ().data, null, false, 
FileCreateFlags.NONE, null);
+                       } catch (Error e) {
+                               critical ("Failed to bump version to %u: %s", version--, e.message);
+                       }
+
+                       return;
+               }
+
+               try {
+                       version_file.create (FileCreateFlags.NONE);
+               }
+               catch (Error e) {
+                       critical ("Failed to create .version file: %s", e.message);
+                       return;
+               }
+
+               version++;
+       }
+
+       private static bool apply_data_dir_migration () {
                var data_dir_path = Application.get_data_dir ();
                var data_dir = File.new_for_path (data_dir_path);
 
@@ -12,14 +106,7 @@ public class Games.Migrator : Object {
                var database_path = Application.get_database_path ();
                string[] backup_excluded_files = { database_path, backup_archive_path };
 
-               var version_file = data_dir.get_child (".version");
-
-               // If the version file exists, there's no need
-               // to apply the migration
-               if (version_file.query_exists ())
-                       return true;
-
-               info ("[Migrator]: Migration is necessary");
+               info ("[Migrator]: Data directory migration is necessary");
 
                // Attempt to create a backup of the previous data
                try {
@@ -34,7 +121,7 @@ public class Games.Migrator : Object {
                try {
                        // The migration executes file I/O which may result in errors being
                        // thrown
-                       apply_migration (version_file);
+                       apply_migration ();
                }
                catch (Error e) {
                        critical ("Migration failed: %s", e.message);
@@ -62,14 +149,10 @@ public class Games.Migrator : Object {
 
                // Migration applied succesfully, deleting backup
                delete_files_no_errors (backup_archive);
-
                return true;
        }
 
-       private static void apply_migration (File version_file) throws Error {
-               // Create the version file
-               version_file.create (FileCreateFlags.NONE);
-
+       private static void apply_migration () throws Error {
                // Create the savestates dir
                var savestates_dir = File.new_for_path (get_savestates_dir_path ());
 
diff --git a/src/database/database.vala b/src/database/database.vala
index cffa937c..435a07eb 100644
--- a/src/database/database.vala
+++ b/src/database/database.vala
@@ -16,7 +16,8 @@ private class Games.Database : Object {
                        uid TEXT NOT NULL UNIQUE,
                        title TEXT NOT NULL,
                        platform TEXT NOT NULL,
-                       media_set TEXT NULL
+                       media_set TEXT NULL,
+                       is_favorite INTEGER NOT NULL DEFAULT 0
                );
        """;
 
@@ -54,11 +55,11 @@ private class Games.Database : Object {
        """;
 
        private const string GET_CACHED_GAME_QUERY = """
-               SELECT uri, title, platform, media_set FROM games JOIN uris ON games.uid == uris.uid WHERE 
games.uid == $UID;
+               SELECT uri, title, platform, media_set, is_favorite FROM games JOIN uris ON games.uid == 
uris.uid WHERE games.uid == $UID;
        """;
 
        private const string LIST_CACHED_GAMES_QUERY = """
-               SELECT games.uid, uri, title, platform, media_set FROM games JOIN uris ON games.uid == 
uris.uid ORDER BY title;
+               SELECT games.uid, uri, title, platform, media_set, is_favorite FROM games JOIN uris ON 
games.uid == uris.uid ORDER BY title;
        """;
 
        private const string ADD_GAME_RESOURCE_QUERY = """
@@ -69,6 +70,18 @@ private class Games.Database : Object {
                SELECT EXISTS (SELECT 1 FROM game_resources WHERE uri=$URI LIMIT 1);
        """;
 
+       private const string SET_IS_FAVORITE_QUERY = """
+               UPDATE games SET is_favorite = $IS_FAVORITE WHERE uid = $UID;
+       """;
+
+       private const string IS_GAME_FAVORITE_QUERY = """
+               SELECT EXISTS (SELECT 1 FROM games WHERE uid = $UID AND is_favorite = 1 LIMIT 1);
+       """;
+
+       private const string LIST_FAVORITE_GAMES_QUERY = """
+               SELECT uid FROM games WHERE is_favorite = 1;
+       """;
+
        private Sqlite.Statement add_game_query;
        private Sqlite.Statement add_game_uri_query;
        private Sqlite.Statement update_game_query;
@@ -82,6 +95,10 @@ private class Games.Database : Object {
        private Sqlite.Statement add_game_resource_query;
        private Sqlite.Statement has_uri_query;
 
+       private Sqlite.Statement set_is_favorite_query;
+       private Sqlite.Statement is_game_favorite_query;
+       private Sqlite.Statement list_favorite_games_query;
+
        public Database (string path) throws Error {
                if (Sqlite.Database.open (path, out database) != Sqlite.OK)
                        throw new DatabaseError.COULDNT_OPEN ("Couldn’t open the database for “%s”.", path);
@@ -89,19 +106,35 @@ private class Games.Database : Object {
                exec (CREATE_RESOURCES_TABLE_QUERY, null);
                exec (CREATE_GAMES_TABLE_QUERY, null);
                exec (CREATE_URIS_TABLE_QUERY, null);
+       }
+
+       public void prepare_statements () {
+               try {
+                       add_game_query = prepare (database, ADD_GAME_QUERY);
+                       add_game_uri_query = prepare (database, ADD_GAME_URI_QUERY);
+                       update_game_query = prepare (database, UPDATE_GAME_QUERY);
+                       delete_game_query = prepare (database, DELETE_GAME_QUERY);
+                       delete_uri_query = prepare (database, DELETE_URI_QUERY);
 
-               add_game_query = prepare (database, ADD_GAME_QUERY);
-               add_game_uri_query = prepare (database, ADD_GAME_URI_QUERY);
-               update_game_query = prepare (database, UPDATE_GAME_QUERY);
-               delete_game_query = prepare (database, DELETE_GAME_QUERY);
-               delete_uri_query = prepare (database, DELETE_URI_QUERY);
+                       find_game_uris_query = prepare (database, FIND_GAME_URIS_QUERY);
+                       get_cached_game_query = prepare (database, GET_CACHED_GAME_QUERY);
+                       list_cached_games_query = prepare (database, LIST_CACHED_GAMES_QUERY);
 
-               find_game_uris_query = prepare (database, FIND_GAME_URIS_QUERY);
-               get_cached_game_query = prepare (database, GET_CACHED_GAME_QUERY);
-               list_cached_games_query = prepare (database, LIST_CACHED_GAMES_QUERY);
+                       add_game_resource_query = prepare (database, ADD_GAME_RESOURCE_QUERY);
+                       has_uri_query = prepare (database, HAS_URI_QUERY);
 
-               add_game_resource_query = prepare (database, ADD_GAME_RESOURCE_QUERY);
-               has_uri_query = prepare (database, HAS_URI_QUERY);
+                       set_is_favorite_query = prepare (database, SET_IS_FAVORITE_QUERY);
+                       is_game_favorite_query = prepare (database, IS_GAME_FAVORITE_QUERY);
+                       list_favorite_games_query = prepare (database, LIST_FAVORITE_GAMES_QUERY);
+               }
+               catch (Error e) {
+                       assert_not_reached ();
+               }
+       }
+
+       public void apply_favorites_migration () throws Error {
+               info ("Applying database migration to support favorites");
+               exec ("ALTER TABLE games ADD COLUMN is_favorite INTEGER NOT NULL DEFAULT 0;", null);
        }
 
        public void add_uri (Uri uri) throws Error {
@@ -159,6 +192,17 @@ private class Games.Database : Object {
                        statement.bind_null (position);
        }
 
+       internal static void bind_int (Sqlite.Statement statement, string parameter, int? integer) throws 
Error {
+               var position = statement.bind_parameter_index (parameter);
+               if (position <= 0)
+                       throw new DatabaseError.BINDING_FAILED ("Couldn't bind text to the parameter “%s”, 
unexpected position: %d.", parameter, position);
+
+               if (integer != null)
+                       statement.bind_int (position, integer);
+               else
+                       statement.bind_null (position);
+       }
+
        private string? serialize_media_set (Game game) {
                var media_set = game.media_set;
 
@@ -290,8 +334,9 @@ private class Games.Database : Object {
                        var title = get_cached_game_query.column_text (1);
                        var platform = get_cached_game_query.column_text (2);
                        var media_set = get_cached_game_query.column_text (3);
+                       var is_favorite = get_cached_game_query.column_int (4);
 
-                       return create_game (uid, uri, title, platform, media_set);
+                       return create_game (uid, uri, title, platform, media_set, is_favorite);
                }
 
                throw new DatabaseError.EXECUTION_FAILED ("Couldn't get game for uid (%s)", uid);
@@ -306,13 +351,14 @@ private class Games.Database : Object {
                        var title = list_cached_games_query.column_text (2);
                        var platform = list_cached_games_query.column_text (3);
                        var media_set = list_cached_games_query.column_text (4);
+                       var is_favorite = list_cached_games_query.column_int (5);
 
-                       var game = create_game (uid, uri, title, platform, media_set);
+                       var game = create_game (uid, uri, title, platform, media_set, is_favorite);
                        game_callback (game);
                }
        }
 
-       private Game create_game (string uid, string uri, string title, string platform, string? media_set) {
+       private Game create_game (string uid, string uri, string title, string platform, string? media_set, 
int is_favorite) {
                var game_uid = new Uid (uid);
                var game_uri = new Uri (uri);
                var game_title = new GenericTitle (title);
@@ -322,10 +368,50 @@ private class Games.Database : Object {
                        game_platform = new DummyPlatform ();
 
                var game = new Game (game_uid, game_uri, game_title, game_platform);
+               game.is_favorite = is_favorite == 1;
 
                if (media_set != null)
                        game.media_set = new MediaSet.parse (new Variant.parsed (media_set));
 
                return game;
        }
+
+       public bool set_is_favorite (Game game) throws Error {
+               if (game.is_favorite == is_game_favorite (game))
+                       return false;
+
+               set_is_favorite_query.reset ();
+               bind_int (set_is_favorite_query, "$IS_FAVORITE", game.is_favorite ? 1 : 0);
+               bind_text (set_is_favorite_query, "$UID", game.uid.to_string ());
+
+               var result = set_is_favorite_query.step ();
+               if (result != Sqlite.DONE)
+                       throw new DatabaseError.EXECUTION_FAILED ("Failed to make %s %sfavorite",
+                                                                  game.name, game.is_favorite ? "" : "non-");
+
+               return true;
+       }
+
+       public string[] list_favorite_games () throws Error {
+               list_favorite_games_query.reset ();
+               string[] games = {};
+
+               while (list_favorite_games_query.step () == Sqlite.ROW)
+                       games += list_favorite_games_query.column_text (0);
+
+               return games;
+       }
+
+       private bool is_game_favorite (Game game) throws Error {
+               var uid = game.uid.to_string ();
+               is_game_favorite_query.reset ();
+               bind_text (is_game_favorite_query, "$UID", uid);
+
+               switch (is_game_favorite_query.step ()) {
+               case Sqlite.ROW:
+                       return is_game_favorite_query.column_int (0) == 1;
+               default:
+                       throw new DatabaseError.EXECUTION_FAILED ("Execution failed.");
+               }
+       }
 }
diff --git a/src/ui/application.vala b/src/ui/application.vala
index 3e4b732f..5243d28c 100644
--- a/src/ui/application.vala
+++ b/src/ui/application.vala
@@ -133,9 +133,7 @@ public class Games.Application : Gtk.Application {
                                return;
 
                        data_dir.make_directory_with_parents ();
-
-                       var version_file = data_dir.get_child (".version");
-                       version_file.create (FileCreateFlags.NONE);
+                       Migrator.bump_to_latest_version ();
                }
                catch (Error e) {
                        critical ("Couldn't create data dir: %s", e.message);
@@ -331,6 +329,12 @@ public class Games.Application : Gtk.Application {
                if (game_collection != null)
                        return;
 
+               // Re-organize data_dir layout if necessary
+               // This operation has to be executed _after_ the PlatformsRegister has
+               // been populated and therefore this call is placed here
+               Migrator.apply_migration_if_necessary (database);
+               database.prepare_statements ();
+
                TrackerUriSource tracker_uri_source = null;
                try {
                        var connection = Tracker.Sparql.Connection.@get ();
@@ -407,11 +411,6 @@ public class Games.Application : Gtk.Application {
                                debug ("Error: %s", e.message);
                        }
                }
-
-               // Re-organize data_dir layout if necessary
-               // This operation has to be executed _after_ the PlatformsRegister has
-               // been populated and therefore this call is placed here
-               Migrator.apply_migration_if_necessary ();
        }
 
        private Game? game_for_uris (Uri[] uris) {


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