using DataLayer; using LibationFileManager; using Serilog; using System; using System.Collections.Generic; using System.IO; using System.Threading; namespace ApplicationServices; public static class DbContexts { private static bool _sqliteDbValidated; private static readonly object _initialDatabaseStatisticsCaptureLock = new(); /// /// True after initial DB statistics were read and either written to Serilog or stored for . /// False if capture has not run yet or the last attempt threw (a later may retry). /// private static bool _initialDatabaseStatisticsCaptured; /// Shape of the initial DB statistics log event; edit here only when changing what is logged. private sealed class InitialDatabaseStatistics { public required int LibraryBooksNotInTrash { get; init; } public required int LibraryBooksInTrash { get; init; } public required int BookRecords { get; init; } } private static InitialDatabaseStatistics? _pendingInitialDbStats; /// Use for fully functional context, incl. SaveChanges(). For query-only, use the other method public static LibationContext GetContext() { var context = !string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString) ? LibationContextFactory.CreatePostgres(Configuration.Instance.PostgresqlConnectionString) : LibationContextFactory.CreateSqlite(SqliteStorage.ConnectionString); try { LibationContextFactory.ApplyMigrations( context, string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString) ? SqliteStorage.DatabasePath : null); } catch (InvalidOperationException ex) when (ex.Message.Contains("Libation cannot write its SQLite database", StringComparison.Ordinal)) { Log.Error( ex, "SQLite migrations failed (read-only or blocked). LibationFiles={LibationFiles} DatabasePath={DatabasePath} AppsettingsJson={AppsettingsJson}", Configuration.Instance.LibationFiles.Location, SqliteStorage.DatabasePath, Configuration.Instance.LibationFiles.AppsettingsJsonFile ?? "(null, LIBATION_FILES_DIR may be set)"); throw; } // Validate SQLite DB file was created and is accessible (once per process; OS may delay availability) if (!_sqliteDbValidated && string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString)) { EssentialFileValidator.ValidateCreatedAndReport(SqliteStorage.DatabasePath); _sqliteDbValidated = true; } TryCaptureInitialDatabaseStatistics(context); return context; } private static void TryCaptureInitialDatabaseStatistics(LibationContext context) { lock (_initialDatabaseStatisticsCaptureLock) { if (_initialDatabaseStatisticsCaptured) return; try { var (notInTrash, inTrash) = context.GetLibraryBookCountsByTrashFlag(); var bookRecords = context.GetBookCount(); var stats = new InitialDatabaseStatistics { LibraryBooksNotInTrash = notInTrash, LibraryBooksInTrash = inTrash, BookRecords = bookRecords, }; if (Configuration.Instance.SerilogInitialized) LogInitialDatabaseStatistics(stats); else _pendingInitialDbStats = stats; _initialDatabaseStatisticsCaptured = true; } catch (Exception ex) { if (Configuration.Instance.SerilogInitialized) Log.Warning(ex, "Could not capture initial database statistics"); } } } /// /// Writes initial DB statistics that were captured before Serilog was configured (e.g. WinForms early library load). /// Call once after . /// public static void TryEmitPendingInitialDatabaseStatistics() { var pending = Interlocked.Exchange(ref _pendingInitialDbStats, null); if (pending is not null) LogInitialDatabaseStatistics(pending); } private static void LogInitialDatabaseStatistics(InitialDatabaseStatistics stats) => Log.Logger.Information("Initial database statistics. {@DbStats}", stats); /// Use for full library querying. No lazy loading public static List GetLibrary_Flat_NoTracking(bool includeParents = false) { using var context = GetContext(); return context.GetLibrary_Flat_NoTracking(includeParents); } public static List GetUnliberated_Flat_NoTracking() { using var context = GetContext(); return context.GetUnLiberated_Flat_NoTracking(); } public static List GetDeletedLibraryBooks() { using var context = GetContext(); return context.GetDeletedLibraryBooks(); } public static LibraryBook? GetLibraryBook_Flat_NoTracking(string productId, bool caseSensative = true) { using var context = GetContext(); return context.GetLibraryBook_Flat_NoTracking(productId, caseSensative); } }