using Journal.Core.Models; using Journal.Core.Services.Database; using Microsoft.Data.Sqlite; namespace Journal.Core.Repositories; public sealed class SqliteFragmentRepository(IDatabaseSessionService session) : IFragmentRepository { private readonly IDatabaseSessionService _session = session; public List GetAll() { var conn = _session.GetConnection(); return ReadAllFragments(conn); } public Fragment? GetById(Guid id) { var conn = _session.GetConnection(); return ReadFragment(conn, id); } public void Add(Fragment fragment) { ArgumentNullException.ThrowIfNull(fragment); Normalize(fragment); var conn = _session.GetConnection(); InsertFragment(conn, fragment); } public bool Remove(Guid id) { var conn = _session.GetConnection(); return DeleteFragment(conn, id); } public bool Update( Guid id, string? type = null, string? description = null, IEnumerable? tags = null, DateTimeOffset? time = null) { var conn = _session.GetConnection(); var existing = ReadFragment(conn, id); if (existing is null) return false; if (type != null) { if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Type cannot be empty", nameof(type)); existing.Type = type.Trim(); } if (description != null) { if (string.IsNullOrWhiteSpace(description)) throw new ArgumentException("Description cannot be empty", nameof(description)); existing.Description = description.Trim(); } if (tags != null) { existing.Tags = [.. tags.Where(t => !string.IsNullOrWhiteSpace(t)) .Select(t => t.Trim())]; } if (time.HasValue) existing.Time = time.Value; UpdateFragmentRow(conn, existing); return true; } public List GetByTag(string tag) { var q = tag?.Trim(); if (string.IsNullOrWhiteSpace(q)) return []; var conn = _session.GetConnection(); var all = ReadAllFragments(conn); return [.. all.Where(f => f.Tags.Any(t => t.Contains(q, StringComparison.OrdinalIgnoreCase)))]; } public List GetByType(string type) { var q = type?.Trim(); if (string.IsNullOrWhiteSpace(q)) return []; var conn = _session.GetConnection(); var all = ReadAllFragments(conn); return [.. all.Where(f => f.Type.Contains(q, StringComparison.OrdinalIgnoreCase))]; } public List Search(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null) { var conn = _session.GetConnection(); IEnumerable results = ReadAllFragments(conn); var qType = type?.Trim(); var qTag = tag?.Trim(); if (!string.IsNullOrWhiteSpace(qType)) results = results.Where(f => f.Type.Contains(qType, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrWhiteSpace(qTag)) results = results.Where(f => f.Tags.Any(t => t.Contains(qTag, StringComparison.OrdinalIgnoreCase))); if (timeAfter.HasValue) results = results.Where(f => f.Time > timeAfter.Value); return [.. results]; } // ── Private helpers ────────────────────────────────────────────── private static void InsertFragment(SqliteConnection conn, Fragment f) { using var tx = conn.BeginTransaction(); using var cmd = conn.CreateCommand(); cmd.CommandText = """ INSERT INTO fragments (guid, entry_id, type, description, time) VALUES (@guid, NULL, @type, @description, @time); """; cmd.Parameters.AddWithValue("@guid", f.Id.ToString("D")); cmd.Parameters.AddWithValue("@type", f.Type); cmd.Parameters.AddWithValue("@description", f.Description); cmd.Parameters.AddWithValue("@time", f.Time.ToString("O")); cmd.ExecuteNonQuery(); var fragmentRowId = GetFragmentRowId(conn, f.Id); if (fragmentRowId.HasValue) InsertTags(conn, fragmentRowId.Value, f.Tags); tx.Commit(); } private static void UpdateFragmentRow(SqliteConnection conn, Fragment f) { using var tx = conn.BeginTransaction(); using var upd = conn.CreateCommand(); upd.CommandText = """ UPDATE fragments SET type = @type, description = @description, time = @time WHERE guid = @guid AND entry_id IS NULL; """; upd.Parameters.AddWithValue("@guid", f.Id.ToString("D")); upd.Parameters.AddWithValue("@type", f.Type); upd.Parameters.AddWithValue("@description", f.Description); upd.Parameters.AddWithValue("@time", f.Time.ToString("O")); upd.ExecuteNonQuery(); var fragmentRowId = GetFragmentRowId(conn, f.Id); if (fragmentRowId.HasValue) { using var del = conn.CreateCommand(); del.CommandText = "DELETE FROM fragment_tags WHERE fragment_id = @id;"; del.Parameters.AddWithValue("@id", fragmentRowId.Value); del.ExecuteNonQuery(); InsertTags(conn, fragmentRowId.Value, f.Tags); } tx.Commit(); } private static bool DeleteFragment(SqliteConnection conn, Guid id) { using var tx = conn.BeginTransaction(); var fragmentRowId = GetFragmentRowId(conn, id); if (fragmentRowId.HasValue) { using var delTags = conn.CreateCommand(); delTags.CommandText = "DELETE FROM fragment_tags WHERE fragment_id = @id;"; delTags.Parameters.AddWithValue("@id", fragmentRowId.Value); delTags.ExecuteNonQuery(); } using var delFrag = conn.CreateCommand(); delFrag.CommandText = "DELETE FROM fragments WHERE guid = @guid AND entry_id IS NULL;"; delFrag.Parameters.AddWithValue("@guid", id.ToString("D")); var rows = delFrag.ExecuteNonQuery(); tx.Commit(); return rows > 0; } private static Fragment? ReadFragment(SqliteConnection conn, Guid id) { using var cmd = conn.CreateCommand(); cmd.CommandText = """ SELECT id, guid, type, description, time FROM fragments WHERE guid = @guid AND entry_id IS NULL; """; cmd.Parameters.AddWithValue("@guid", id.ToString("D")); using var reader = cmd.ExecuteReader(); if (!reader.Read()) return null; var fragment = MapRow(reader); fragment.Tags = ReadTags(conn, reader.GetInt64(0)); return fragment; } private static List ReadAllFragments(SqliteConnection conn) { var fragments = new List(); var rowIds = new List(); using var cmd = conn.CreateCommand(); cmd.CommandText = """ SELECT id, guid, type, description, time FROM fragments WHERE guid IS NOT NULL AND entry_id IS NULL ORDER BY time; """; using var reader = cmd.ExecuteReader(); while (reader.Read()) { fragments.Add(MapRow(reader)); rowIds.Add(reader.GetInt64(0)); } for (var i = 0; i < fragments.Count; i++) fragments[i].Tags = ReadTags(conn, rowIds[i]); return fragments; } private static List ReadTags(SqliteConnection conn, long fragmentRowId) { using var cmd = conn.CreateCommand(); cmd.CommandText = """ SELECT t.name FROM tags t INNER JOIN fragment_tags ft ON ft.tag_id = t.id WHERE ft.fragment_id = @id ORDER BY t.name; """; cmd.Parameters.AddWithValue("@id", fragmentRowId); var tags = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) tags.Add(reader.GetString(0)); return tags; } private static long? GetFragmentRowId(SqliteConnection conn, Guid guid) { using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT id FROM fragments WHERE guid = @guid;"; cmd.Parameters.AddWithValue("@guid", guid.ToString("D")); var result = cmd.ExecuteScalar(); return result is long id ? id : null; } private static void InsertTags(SqliteConnection conn, long fragmentRowId, List tags) { if (tags.Count == 0) return; foreach (var tag in tags) { // Upsert into tags table using var upsert = conn.CreateCommand(); upsert.CommandText = "INSERT OR IGNORE INTO tags (name) VALUES (@name);"; upsert.Parameters.AddWithValue("@name", tag); upsert.ExecuteNonQuery(); // Get tag id using var getTagId = conn.CreateCommand(); getTagId.CommandText = "SELECT id FROM tags WHERE name = @name;"; getTagId.Parameters.AddWithValue("@name", tag); var tagId = (long)getTagId.ExecuteScalar()!; // Link fragment to tag using var link = conn.CreateCommand(); link.CommandText = "INSERT OR IGNORE INTO fragment_tags (fragment_id, tag_id) VALUES (@fid, @tid);"; link.Parameters.AddWithValue("@fid", fragmentRowId); link.Parameters.AddWithValue("@tid", tagId); link.ExecuteNonQuery(); } } private static Fragment MapRow(SqliteDataReader reader) { // columns: id (int), guid (text), type (text), description (text), time (text) var guid = Guid.Parse(reader.GetString(1)); var type = reader.GetString(2); var description = reader.IsDBNull(3) ? "" : reader.GetString(3); var time = reader.IsDBNull(4) ? DateTimeOffset.MinValue : DateTimeOffset.Parse(reader.GetString(4)); return new Fragment(guid, type, description, time); } private static void Normalize(Fragment fragment) { fragment.Type = fragment.Type.Trim(); fragment.Description = fragment.Description.Trim(); fragment.Tags = [.. fragment.Tags.Where(t => !string.IsNullOrWhiteSpace(t)) .Select(t => t.Trim())]; } }