fix(storage): initialize workspaces under the lock in one transaction

Follows PLAN 4.2: create .ykanban, take the lock, then build the database.
SqliteDatabase.Create writes every migration and the preset columns in a
single transaction and deletes the file again on failure, so no empty
version-0 database is left behind. WorkspaceSession.Initialize keeps the lock
into the session instead of releasing and re-acquiring it.

A lock conflict while initializing shows the lock-conflict page.
This commit is contained in:
doyaGu committed 2026-10-03 08:12:35 -04:00
1 parent d2a5d36bd2
commit 22102cd1ea
10 files changed
+281 -58

No files matched your search

@@ -80,4 +80,48 @@ public class SqliteDatabaseTests
Assert.ThrowsExactly<ArgumentException>(() => SqliteDatabase.Open(databasePath, broken));
}
[TestMethod]
public void CreateBuildsSchemaAndSeedTogether()
{
using var directory = new TempDirectory();
string databasePath = Path.Combine(directory.FullPath, "create.db");
using SqliteConnection connection = SqliteDatabase.Create(databasePath, WorkspaceSchema.Migrations,
(seedConnection, transaction) =>
{
using SqliteCommand command = seedConnection.CreateCommand();
command.Transaction = transaction;
command.CommandText = "INSERT INTO tags (name, color) VALUES ('seed', '#000000');";
command.ExecuteNonQuery();
});
Assert.AreEqual(WorkspaceSchema.CurrentVersion, SqliteDatabase.ReadUserVersion(connection));
Assert.AreEqual(1L, SqliteTestHelper.ScalarLong(connection, "SELECT COUNT(*) FROM tags;"));
Assert.AreEqual("wal", SqliteTestHelper.ScalarString(connection, "PRAGMA journal_mode;"));
Assert.AreEqual(1L, SqliteTestHelper.ScalarLong(connection, "PRAGMA foreign_keys;"));
}
[TestMethod]
public void CreateWithFailingSeedLeavesNoDatabaseBehind()
{
using var directory = new TempDirectory();
string databasePath = Path.Combine(directory.FullPath, "failed.db");
Assert.ThrowsExactly<InvalidOperationException>(() => SqliteDatabase.Create(
databasePath, WorkspaceSchema.Migrations, (_, _) => throw new InvalidOperationException("seed failed")));
Assert.IsFalse(File.Exists(databasePath));
Assert.IsFalse(File.Exists(databasePath + "-wal"));
Assert.IsFalse(File.Exists(databasePath + "-shm"));
}
[TestMethod]
public void CreateRefusesAnExistingDatabaseAndKeepsIt()
{
using var directory = new TempDirectory();
string databasePath = Path.Combine(directory.FullPath, "existing.db");
SqliteDatabase.Create(databasePath, WorkspaceSchema.Migrations).Dispose();
Assert.ThrowsExactly<InvalidOperationException>(() => SqliteDatabase.Create(databasePath, WorkspaceSchema.Migrations));
Assert.IsTrue(File.Exists(databasePath));
}
}