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:
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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user