diff --git a/Core/Models/Tag/Tag.cs b/Core/Models/Tag/Tag.cs index 16d88a4..13f20a1 100644 --- a/Core/Models/Tag/Tag.cs +++ b/Core/Models/Tag/Tag.cs @@ -52,6 +52,17 @@ public partial class Tag /// public List TagBindings { get; set; } = []; + /// + /// Gets or sets the date the tag was created. + /// Defaults to null to cater to migrated databases to prevent inaccurate information. + /// + public DateTime? CreatedAt { get; set; } = null; + + /// + /// Gets or sets the date the tag was modified. + /// + public DateTime UpdatedAt { get; set; } + /// public override string ToString() { diff --git a/Core/Models/TagDatabase/TagDatabase.ImportMethods.cs b/Core/Models/TagDatabase/TagDatabase.ImportMethods.cs index 5f817f1..5bc69ce 100644 --- a/Core/Models/TagDatabase/TagDatabase.ImportMethods.cs +++ b/Core/Models/TagDatabase/TagDatabase.ImportMethods.cs @@ -27,8 +27,6 @@ public partial class TagDatabase ?? await this.SelectTagFromDatabase(tag.Name).ConfigureAwait(false); if (currentTag is null) throw new InvalidOperationException(ErrorMessages.TagNotFound); - - await this.SaveTagAliases(transaction, currentTag.Id, tag.Aliases).ConfigureAwait(false); await this.SaveTagParents(transaction, currentTag.Id, tag.Parents, currentTag).ConfigureAwait(false); } @@ -50,8 +48,8 @@ public partial class TagDatabase SqliteCommand addCommand = this.currentConnection.CreateCommand(); addCommand.Transaction = transaction; addCommand.CommandText = """ - INSERT INTO tag (name, notes, top_level, tags_to_bind, also_known_as) - VALUES (@name, @notes, @is_top_level, @tags_to_bind, @aliases) + INSERT INTO tag (name, notes, top_level, tags_to_bind, also_known_as, date_modified) + VALUES (@name, @notes, @is_top_level, @tags_to_bind, @aliases, CURRENT_TIMESTAMP) """; addCommand.Parameters.AddWithValue("@name", tag.Name); addCommand.Parameters.AddWithValue("@name_normalised", StringNormaliser.FormatStringForSearch(tag.Name)); diff --git a/Core/Models/TagDatabase/TagDatabase.Initialisation.cs b/Core/Models/TagDatabase/TagDatabase.Initialisation.cs index cf70131..79fbd17 100644 --- a/Core/Models/TagDatabase/TagDatabase.Initialisation.cs +++ b/Core/Models/TagDatabase/TagDatabase.Initialisation.cs @@ -96,28 +96,22 @@ public partial class TagDatabase command.CommandText = """ CREATE TABLE "tag" ( - "id" INTEGER NOT NULL, + "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL UNIQUE, "notes" TEXT DEFAULT '', "top_level" INTEGER NOT NULL DEFAULT 0, "tags_to_bind" TEXT, "also_known_as" TEXT DEFAULT '', - PRIMARY KEY("id" AUTOINCREMENT) - ); - - CREATE TABLE "alias" ( - "id" INTEGER NOT NULL, - "tag_id" INTEGER NOT NULL, - "name" TEXT, - PRIMARY KEY("id" AUTOINCREMENT), - FOREIGN KEY("tag_id") REFERENCES "tag"("id") ON DELETE CASCADE + "date_created" DATETIME, + "date_modified" DATETIME ); CREATE TABLE "tag_parent_link" ( "target_tag_id" INT NOT NULL, - "parent_tag_id" INT NOT NULL CHECK("parent_tag_id" != "target_tag_id"), + "parent_tag_id" INT NOT NULL, FOREIGN KEY("parent_tag_id") REFERENCES "tag"("id") ON DELETE CASCADE, - FOREIGN KEY("target_tag_id") REFERENCES "tag"("id") ON DELETE CASCADE + FOREIGN KEY("target_tag_id") REFERENCES "tag"("id") ON DELETE CASCADE, + CHECK("parent_tag_id" != "target_tag_id") ); CREATE TABLE "settings" ( @@ -125,13 +119,13 @@ public partial class TagDatabase "value" TEXT NOT NULL ); - INSERT INTO "main"."settings" ("key", "value") VALUES ('version', '1'); + INSERT INTO "main"."settings" ("key", "value") VALUES ('version', '2'); INSERT INTO "main"."settings" ("key", "value") VALUES ('default_tag_bind', 'genre'); CREATE TRIGGER DoNotChangeRequiredKeys BEFORE UPDATE ON settings FOR EACH ROW WHEN OLD.key IN ('version', 'default_tag_bind') AND OLD.key != NEW.key - BEGIN + BEGIN SELECT RAISE(ABORT,'CANNOT_CHANGE_REQUIRED_KEY'); END; @@ -143,7 +137,16 @@ public partial class TagDatabase SELECT RAISE(ABORT, 'CANNOT_DELETE_REQUIRED_KEY'); END; """; - await command.ExecuteNonQueryAsync().ConfigureAwait(false); + try + { + await command.ExecuteNonQueryAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + this.Logger.Error(ex, "SQLite failed executing statement:\n{SqlStatement}", command.CommandText); + throw; + } + await this.FinishInitialisationAsync(tagsToImport).ConfigureAwait(false); } catch (Exception ex) @@ -183,15 +186,16 @@ public partial class TagDatabase } } } - + this.FilePath = this.currentConnection.DataSource; this.Name = this.currentConnection.DataSource != InMemoryDbPath ? Path.GetFileNameWithoutExtension(this.currentConnection.DataSource) : InMemoryDbName; + + this.PerformNeededMigrations(); this.Initialised = true; - if (tagsToImport is not null) try { @@ -202,9 +206,11 @@ public partial class TagDatabase this.Close(); throw; } - + if (tagsToImport is null) this.Tags = await this.GetAllTagsFromDatabase(); + + this.OnInitialisationComplete(EventArgs.Empty); Debug.WriteLine( diff --git a/Core/Models/TagDatabase/TagDatabase.Migration.cs b/Core/Models/TagDatabase/TagDatabase.Migration.cs new file mode 100644 index 0000000..d288d59 --- /dev/null +++ b/Core/Models/TagDatabase/TagDatabase.Migration.cs @@ -0,0 +1,54 @@ +using Microsoft.Data.Sqlite; + +namespace TagHierarchyManager.Models; + +public partial class TagDatabase +{ + /// + /// Performs necessary migrations from older database versions. + /// + /// Thrown if the database's current connection is null. + private async Task PerformNeededMigrations() + { + if (this.currentConnection is null) + throw new InvalidOperationException(ErrorMessages.DbNotInitialised); + switch (this.Version) + { + case < 2: + // version 2 adds a date column to tag and removes the aliases table. + var transaction = + (SqliteTransaction) + await this.currentConnection.BeginTransactionAsync().ConfigureAwait(false); + var addDateAddedCommand = this.currentConnection.CreateCommand(); + addDateAddedCommand.Transaction = transaction; + addDateAddedCommand.CommandText = + $""" + ALTER TABLE tag ADD COLUMN date_created DATETIME DEFAULT NULL; + ALTER TABLE tag ADD COLUMN date_modified DATETIME DEFAULT NULL; + """; + addDateAddedCommand.ExecuteNonQuery(); + + var updateModifiedCommand = this.currentConnection.CreateCommand(); + updateModifiedCommand.Transaction = transaction; + updateModifiedCommand.CommandText = "UPDATE tag SET date_modified = CURRENT_TIMESTAMP;"; + updateModifiedCommand.ExecuteNonQuery(); + + var deleteOldTableCommand = this.currentConnection.CreateCommand() ?? + throw new InvalidOperationException(ErrorMessages.DbNotInitialised); + deleteOldTableCommand.Transaction = transaction; + deleteOldTableCommand.CommandText = + "DROP TABLE alias;"; + deleteOldTableCommand.ExecuteNonQuery(); + + var versionBumpCommand = this.currentConnection.CreateCommand(); + versionBumpCommand.Transaction = transaction; + versionBumpCommand.CommandText = "UPDATE settings SET value = 2 WHERE key = 'version'"; + versionBumpCommand.ExecuteNonQuery(); + transaction.Commit(); + + break; + } + + this.Version = LatestVersion; + } +} \ No newline at end of file diff --git a/Core/Models/TagDatabase/TagDatabase.QueryProcessorHandler.cs b/Core/Models/TagDatabase/TagDatabase.QueryProcessorHandler.cs index 0093a12..7315f2b 100644 --- a/Core/Models/TagDatabase/TagDatabase.QueryProcessorHandler.cs +++ b/Core/Models/TagDatabase/TagDatabase.QueryProcessorHandler.cs @@ -16,7 +16,9 @@ public partial class TagDatabase parent.top_level, parent.notes, parent.tags_to_bind, - parent.also_known_as + parent.also_known_as, + parent.date_created, + parent.date_modified FROM tag_parent_link LEFT JOIN tag parent ON tag_parent_link.parent_tag_id = parent.id @@ -44,8 +46,8 @@ public partial class TagDatabase { // if tag.Id is null, it's assumed it's a new tag to be added. command.CommandText = """ - INSERT INTO tag (name, notes, top_level, tags_to_bind, also_known_as) - VALUES (@name, @notes, @is_top_level, @tags_to_bind, @aliases) + INSERT INTO tag (name, notes, top_level, tags_to_bind, also_known_as, date_created, date_modified) + VALUES (@name, @notes, @is_top_level, @tags_to_bind, @aliases, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) RETURNING id; """; } @@ -59,7 +61,8 @@ public partial class TagDatabase notes = @notes, top_level = @is_top_level, tags_to_bind = @tags_to_bind, - also_known_as = @aliases + also_known_as = @aliases, + date_modified = CURRENT_TIMESTAMP WHERE id = @target_id RETURNING id; """; diff --git a/Core/Models/TagDatabase/TagDatabase.ReadMethods.cs b/Core/Models/TagDatabase/TagDatabase.ReadMethods.cs index aff36c3..91e13c7 100644 --- a/Core/Models/TagDatabase/TagDatabase.ReadMethods.cs +++ b/Core/Models/TagDatabase/TagDatabase.ReadMethods.cs @@ -28,6 +28,8 @@ public partial class TagDatabase tag.notes, tag.tags_to_bind, tag.also_known_as, + tag.date_created, + tag.date_modified, GROUP_CONCAT(tag_parent_link.parent_tag_id, ';') AS parent_ids FROM tag LEFT JOIN tag_parent_link ON tag.id = tag_parent_link.target_tag_id @@ -102,6 +104,8 @@ public partial class TagDatabase tag.notes, tag.tags_to_bind, tag.also_known_as, + tag.date_created, + tag.date_modified, GROUP_CONCAT(tag_parent_link.parent_tag_id, ';') AS parent_ids FROM tag LEFT JOIN tag_parent_link ON tag.id = tag_parent_link.target_tag_id @@ -142,6 +146,8 @@ public partial class TagDatabase tag.notes, tag.tags_to_bind, tag.also_known_as, + tag.date_created, + tag.date_modified, GROUP_CONCAT(tag_parent_link.parent_tag_id, ';') AS parent_ids FROM tag LEFT JOIN tag_parent_link ON tag.id = tag_parent_link.target_tag_id @@ -196,6 +202,14 @@ public partial class TagDatabase string altNameList = reader.GetString(reader.GetOrdinal(AliasesColumnName)); if (!string.IsNullOrEmpty(altNameList)) addedTag.Aliases = altNameList.Split(';').ToList(); + if (!reader.IsDBNull(reader.GetOrdinal(DateCreatedColumnName))) + { + var dbCreatedAt = reader.GetDateTime(reader.GetOrdinal(DateCreatedColumnName)); + addedTag.CreatedAt = dbCreatedAt; + } + + var dbUpdatedAt = reader.GetDateTime(reader.GetOrdinal(DateModifiedColumnName)); + addedTag.UpdatedAt = dbUpdatedAt; if (fetchParents && !reader.IsDBNull(reader.GetOrdinal(ParentIdsColumnName))) { diff --git a/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs b/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs index b4e0ad1..b3a8c26 100644 --- a/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs +++ b/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs @@ -37,7 +37,6 @@ public partial class TagDatabase tag.Id = Convert.ToInt32(await addCommand.ExecuteScalarAsync().ConfigureAwait(false), CultureInfo.InvariantCulture); - await this.SaveTagAliases(transaction, tag.Id, tag.Aliases).ConfigureAwait(false); await this.SaveTagParents(transaction, tag.Id, tag.Parents, tag).ConfigureAwait(false); int index = this.Tags.FindIndex(t => t.Id == tag.Id); @@ -67,29 +66,6 @@ public partial class TagDatabase if (isTransactionOwner) await transaction.DisposeAsync().ConfigureAwait(false); } } - - private async Task SaveTagAliases(SqliteTransaction transaction, int id, IReadOnlyCollection aliases) - { - if (aliases.Count == 0) return; - this.CheckInitialisation(); - - SqliteCommand command = this.currentConnection.CreateCommand(); - command.Transaction = transaction; - command.CommandText = - "INSERT INTO alias (tag_id, name) VALUES (@tag_id, @name)"; - command.Parameters.Clear(); - command.Parameters.AddWithValue("@tag_id", id); - command.Parameters.Add("@name", SqliteType.Text); - command.Parameters.Add("@name_normalised", SqliteType.Text); - await command.PrepareAsync(); - - foreach (string alias in aliases) - { - command.Parameters["@name"].Value = alias; - command.Parameters["@name_normalised"].Value = StringNormaliser.FormatStringForSearch(alias); - await command.ExecuteNonQueryAsync().ConfigureAwait(false); - } - } private async Task SaveTagParents(SqliteTransaction transaction, int id, IReadOnlyCollection parents, Tag? tag = null) diff --git a/Core/Models/TagDatabase/TagDatabase.cs b/Core/Models/TagDatabase/TagDatabase.cs index 125fbff..0f2e60b 100644 --- a/Core/Models/TagDatabase/TagDatabase.cs +++ b/Core/Models/TagDatabase/TagDatabase.cs @@ -16,6 +16,9 @@ public partial class TagDatabase private const string ParentIdsColumnName = "parent_ids"; private const string TagBindingsColumnName = "tags_to_bind"; private const string TopLevelColumnName = "top_level"; + private const string DateCreatedColumnName = "date_created"; + private const string DateModifiedColumnName = "date_modified"; + private const int LatestVersion = 2; private SqliteConnection? currentConnection; private List defaultBindings = ["genre"];