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

@@ -29,8 +29,7 @@ public class WorkspacePresetTests
];
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
WorkspacePreset.AddPresetColumns(directory.FullPath);
WorkspaceSession.Initialize(directory.FullPath).Dispose();
using SqliteConnection connection = SqliteTestHelper.OpenWorkspace(directory.FullPath);
CollectionAssert.AreEqual(expected, SqliteTestHelper.GetColumnTitles(connection));
@@ -56,8 +55,7 @@ public class WorkspacePresetTests
];
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
WorkspacePreset.AddPresetColumns(directory.FullPath);
WorkspaceSession.Initialize(directory.FullPath).Dispose();
using SqliteConnection connection = SqliteTestHelper.OpenWorkspace(directory.FullPath);
CollectionAssert.AreEqual(expected, SqliteTestHelper.GetColumnTitles(connection));
@@ -83,8 +81,7 @@ public class WorkspacePresetTests
];
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
WorkspacePreset.AddPresetColumns(directory.FullPath);
WorkspaceSession.Initialize(directory.FullPath).Dispose();
// Switching the language after adding the presets must not rewrite stored data.
Resources.Culture = new CultureInfo("zh-Hans");
@@ -57,4 +57,36 @@ public class WorkspaceSessionTests
Assert.AreEqual("bug", session.Repository.GetTags().Single().Name);
Assert.AreEqual(tag.Id, session.Repository.GetTags().Single().Id);
}
[TestMethod]
public void InitializeAddsPresetColumnsAndHoldsTheLock()
{
using var directory = new TempDirectory();
using WorkspaceSession session = WorkspaceSession.Initialize(directory.FullPath);
Assert.AreEqual(3, session.Repository.GetColumns().Count);
Assert.ThrowsExactly<WorkspaceLockException>(() => WorkspaceSession.Open(directory.FullPath));
}
[TestMethod]
public void InitializeWhileAnotherInstanceHoldsTheLockThrowsLockConflict()
{
using var directory = new TempDirectory();
Directory.CreateDirectory(WorkspacePaths.Root(directory.FullPath));
using WorkspaceLock held = WorkspaceLock.Acquire(directory.FullPath);
Assert.ThrowsExactly<WorkspaceLockException>(() => WorkspaceSession.Initialize(directory.FullPath));
Assert.IsFalse(File.Exists(WorkspacePaths.Database(directory.FullPath)));
}
[TestMethod]
public void FailedInitializeReleasesTheLock()
{
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
// The database already exists, so initialization fails after taking the lock.
Assert.ThrowsExactly<InvalidOperationException>(() => WorkspaceSession.Initialize(directory.FullPath));
using WorkspaceSession reopened = WorkspaceSession.Open(directory.FullPath);
}
}