using Microsoft.Data.Sqlite; namespace AIStudio.Tools.Databases; /// /// Copies a SQLite database while AI Studio keeps using it, e.g., for a backup. /// /// /// VACUUM INTO writes a consistent snapshot of the database, including what the write-ahead log /// still holds, and in WAL mode, reading it holds back no writer.

/// /// It runs on connections of their own, although the databases guide has raw SQL go through /// GetDbConnection. The connections of a context share one cache, and a shared cache locks whole /// tables: reading the whole database for the copy would hold back every writer until the copy is /// done. Without pooling, nothing keeps the copy open afterward, so the caller can delete it on /// every OS.

/// /// SQLite runs its async methods synchronously, and copying and checking a large database takes a /// while. The callers therefore run this on a thread of their own, so that it never holds up theirs. ///
internal static class SqliteDatabaseCopy { /// /// Copies a database into a file and checks the copy. /// /// /// A copy which fails its check is removed, and an InvalidOperationException says so. /// /// The database to copy. /// The file of the copy. A file there is replaced. /// The cancellation token. public static async Task CopyAndCheckAsync(string databasePath, string targetPath, CancellationToken token) { Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); // VACUUM INTO refuses to write into a file which holds anything: if (File.Exists(targetPath)) File.Delete(targetPath); try { await using (var connection = new SqliteConnection(BuildConnectionString(databasePath, SqliteOpenMode.ReadWrite))) { await connection.OpenAsync(token); await using var command = connection.CreateCommand(); command.CommandText = "VACUUM INTO $targetPath;"; command.Parameters.AddWithValue("$targetPath", targetPath); await command.ExecuteNonQueryAsync(token); } await using (var connection = new SqliteConnection(BuildConnectionString(targetPath, SqliteOpenMode.ReadOnly))) { await connection.OpenAsync(token); await using var command = connection.CreateCommand(); command.CommandText = "PRAGMA quick_check;"; var checkResult = (await command.ExecuteScalarAsync(token))?.ToString(); if (checkResult is not "ok") throw new InvalidOperationException($"The copy of the database at '{targetPath}' failed its check: '{checkResult}'."); } } catch { if (File.Exists(targetPath)) File.Delete(targetPath); throw; } } /// /// Builds the connection string for a connection of its own, which shares no cache and stays in no pool. /// /// The database. /// How to open it. /// The connection string. public static string BuildConnectionString(string path, SqliteOpenMode mode) => new SqliteConnectionStringBuilder { DataSource = path, Mode = mode, Cache = SqliteCacheMode.Private, Pooling = false, DefaultTimeout = 30, }.ToString(); }