mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 07:11:06 -04:00
feat(server): migrate v4 drawing library data to schema v5
The v4 upgrade now keeps existing library data: each distinct package blob is stored once by hash, every revision and current row is copied with default nesting settings, and the v4 drawing tables are dropped, all in the migration transaction. Pre-v4 databases get the v5 tables directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5a374aa3f4
commit
8f73704bef
4 files changed
+137
-28
No files matched your search
@@ -32,28 +32,46 @@ public sealed class DrawingLibraryDatabaseTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void V4Upgrade_ReplacesTheDrawingTablesAndKeepsNests()
|
||||
public void V4Upgrade_KeepsEveryRevisionAndStoresEachPackageOnce()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
CreateVersion3Database(factory.DatabasePath);
|
||||
// The v4 layout: one column set with the package blob copied into every revision.
|
||||
Execute(factory.DatabasePath, """
|
||||
CREATE TABLE drawings (id TEXT NOT NULL, revision INTEGER NOT NULL, name TEXT NOT NULL, package BLOB NOT NULL, PRIMARY KEY (id));
|
||||
CREATE TABLE drawing_revisions (id TEXT NOT NULL, revision INTEGER NOT NULL, name TEXT NOT NULL, package BLOB NOT NULL);
|
||||
INSERT INTO drawings VALUES ('00000000-0000-0000-0000-000000000001', 1, 'Old', x'504B');
|
||||
INSERT INTO drawing_revisions SELECT * FROM drawings;
|
||||
PRAGMA user_version=4;
|
||||
""");
|
||||
CreateVersion4Database(factory.DatabasePath);
|
||||
var before = NestDigest(factory.DatabasePath);
|
||||
var bracket = Guid.Parse("00000000-0000-0000-0000-00000000000a");
|
||||
var plate = Guid.Parse("00000000-0000-0000-0000-00000000000b");
|
||||
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
|
||||
{
|
||||
Assert.Equal(5L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(before, NestDigest(factory.DatabasePath));
|
||||
// Two distinct blobs across four revisions: the bracket's notes edit and the
|
||||
// plate both reuse the bracket's first package.
|
||||
Assert.Equal(2L, Scalar(factory.DatabasePath, "SELECT COUNT(*) FROM drawing_packages"));
|
||||
Assert.Equal(0L, Scalar(factory.DatabasePath, "SELECT COUNT(*) FROM sqlite_master WHERE name LIKE '%_v4'"));
|
||||
|
||||
Assert.Equal(5L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(before, NestDigest(factory.DatabasePath));
|
||||
Assert.Empty(database.QueryDrawings(new DrawingLibraryQuery { IncludeArchived = true }).Items);
|
||||
var record = Record();
|
||||
database.InsertDrawing(record.Id, record, Package());
|
||||
Assert.Equal(1L, Scalar(factory.DatabasePath, "SELECT COUNT(*) FROM drawing_packages"));
|
||||
var current = database.GetDrawing(bracket)!;
|
||||
Assert.Equal((3, "Synthetic bracket", "Edited", Sha256(V4PackageB)),
|
||||
(current.Revision, current.Name, current.Notes, current.PackageSha256));
|
||||
Assert.Equal(new DrawingNestingSettings(), current.Settings);
|
||||
var history = database.DrawingHistory(bracket, 0, 100)!;
|
||||
Assert.Equal(new[] { 3, 2, 1 }, history.Items.Select(r => r.Revision));
|
||||
Assert.Equal(new[] { "Edited", "Edited", "Original" }, history.Items.Select(r => r.Notes));
|
||||
Assert.Equal(V4PackageA, database.GetDrawingRevisionPackage(bracket, 1));
|
||||
Assert.Equal(V4PackageA, database.GetDrawingRevisionPackage(bracket, 2));
|
||||
Assert.Equal(V4PackageB, database.GetDrawingPackage(bracket));
|
||||
Assert.True(database.GetDrawing(plate)!.Archived);
|
||||
Assert.Equal(V4PackageA, database.GetDrawingPackage(plate));
|
||||
Assert.Single(database.QueryDrawings(new DrawingLibraryQuery { Customer = " synthetic customer " }).Items);
|
||||
|
||||
// The upgraded drawing keeps working: a settings-only revision adds no blob.
|
||||
var edit = database.GetDrawing(bracket)!;
|
||||
edit.Settings = new DrawingNestingSettings { CutFirst = true };
|
||||
Assert.Equal(4, database.UpdateDrawing(bracket, edit, null, 3)!.Revision);
|
||||
Assert.Equal(2L, Scalar(factory.DatabasePath, "SELECT COUNT(*) FROM drawing_packages"));
|
||||
}
|
||||
|
||||
using var reopened = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
Assert.True(reopened.GetDrawing(bracket)!.Settings.CutFirst);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -220,6 +238,65 @@ public sealed class DrawingLibraryDatabaseTests
|
||||
Execute(path, "DROP TABLE drawing_revisions; DROP TABLE drawings; DROP TABLE drawing_packages; PRAGMA user_version=3;");
|
||||
}
|
||||
|
||||
private static readonly byte[] V4PackageA = Package("v4 original");
|
||||
private static readonly byte[] V4PackageB = Package("v4 changed");
|
||||
|
||||
/// <summary>
|
||||
/// A schema-v4 file in the v4 layout: the same columns in both drawing tables and the
|
||||
/// package blob copied into every revision. The bracket has a package, a notes edit
|
||||
/// and a new package; the archived plate shares the bracket's first package.
|
||||
/// </summary>
|
||||
private static void CreateVersion4Database(string path)
|
||||
{
|
||||
CreateVersion3Database(path);
|
||||
const string columns = """
|
||||
id TEXT NOT NULL, revision INTEGER NOT NULL, name TEXT NOT NULL, customer TEXT NOT NULL DEFAULT '',
|
||||
nameKey TEXT NOT NULL, customerKey TEXT NOT NULL DEFAULT '', materialName TEXT NOT NULL DEFAULT '',
|
||||
materialKey TEXT NOT NULL DEFAULT '', materialGrade TEXT NOT NULL DEFAULT '', gradeKey TEXT NOT NULL DEFAULT '',
|
||||
thickness REAL NOT NULL, units TEXT NOT NULL, cutType TEXT NOT NULL, notes TEXT NOT NULL DEFAULT '',
|
||||
archived INTEGER NOT NULL DEFAULT 0, packageSha256 TEXT NOT NULL, packageSize INTEGER NOT NULL,
|
||||
savedAt TEXT NOT NULL, savedBy TEXT NOT NULL DEFAULT '', package BLOB NOT NULL
|
||||
""";
|
||||
Execute(path, $"""
|
||||
CREATE TABLE drawings ({columns}, PRIMARY KEY (id));
|
||||
CREATE TABLE drawing_revisions ({columns});
|
||||
CREATE UNIQUE INDEX ix_drawing_revisions_key ON drawing_revisions(id, revision);
|
||||
CREATE INDEX ix_drawings_customer_name ON drawings(customerKey, nameKey);
|
||||
CREATE INDEX ix_drawings_material ON drawings(materialKey);
|
||||
CREATE INDEX ix_drawings_thickness ON drawings(thickness);
|
||||
CREATE INDEX ix_drawings_savedAt ON drawings(savedAt DESC);
|
||||
PRAGMA user_version=4;
|
||||
""");
|
||||
|
||||
using var connection = new SqliteConnection($"Data Source={path};Pooling=False");
|
||||
connection.Open();
|
||||
void Revision(string id, int revision, string name, string notes, bool archived, byte[] package)
|
||||
{
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = """
|
||||
INSERT INTO drawing_revisions VALUES ($id, $revision, $name, 'Synthetic customer', $nameKey,
|
||||
'SYNTHETIC CUSTOMER', 'Synthetic Steel', 'SYNTHETIC STEEL', 'Grade A', 'GRADE A', 0.25, 'Inches',
|
||||
'Laser', $notes, $archived, $sha, $size, '2026-01-01T00:00:00.0000000', 'Synthetic operator', $package);
|
||||
INSERT OR REPLACE INTO drawings SELECT * FROM drawing_revisions WHERE id=$id AND revision=$revision;
|
||||
""";
|
||||
command.Parameters.AddWithValue("$id", id);
|
||||
command.Parameters.AddWithValue("$revision", revision);
|
||||
command.Parameters.AddWithValue("$name", name);
|
||||
command.Parameters.AddWithValue("$nameKey", SharedListNames.Key(name));
|
||||
command.Parameters.AddWithValue("$notes", notes);
|
||||
command.Parameters.AddWithValue("$archived", archived ? 1 : 0);
|
||||
command.Parameters.AddWithValue("$sha", Sha256(package));
|
||||
command.Parameters.AddWithValue("$size", package.LongLength);
|
||||
command.Parameters.AddWithValue("$package", package);
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
Revision("00000000-0000-0000-0000-00000000000a", 1, "Synthetic bracket", "Original", false, V4PackageA);
|
||||
Revision("00000000-0000-0000-0000-00000000000a", 2, "Synthetic bracket", "Edited", false, V4PackageA);
|
||||
Revision("00000000-0000-0000-0000-00000000000a", 3, "Synthetic bracket", "Edited", false, V4PackageB);
|
||||
Revision("00000000-0000-0000-0000-00000000000b", 1, "Synthetic plate", "", true, V4PackageA);
|
||||
}
|
||||
|
||||
private static string NestDigest(string path)
|
||||
{
|
||||
using var connection = new SqliteConnection($"Data Source={path};Pooling=False");
|
||||
|
||||
@@ -43,11 +43,8 @@ public sealed partial class NestDatabase
|
||||
savedBy TEXT NOT NULL DEFAULT ''
|
||||
""";
|
||||
|
||||
// Schema v5 replaces the v4 drawing tables (which copied the blob into every revision).
|
||||
private const string DrawingLibrarySchema = $"""
|
||||
DROP TABLE IF EXISTS drawings;
|
||||
DROP TABLE IF EXISTS drawing_revisions;
|
||||
DROP TABLE IF EXISTS drawing_packages;
|
||||
// The v5 drawing tables: created directly on a pre-v4 database and by the v4 upgrade.
|
||||
private const string DrawingLibraryTables = $"""
|
||||
CREATE TABLE drawing_packages (
|
||||
sha256 TEXT NOT NULL PRIMARY KEY,
|
||||
size INTEGER NOT NULL,
|
||||
@@ -59,6 +56,34 @@ public sealed partial class NestDatabase
|
||||
CREATE INDEX ix_drawings_thickness ON drawings(thickness);
|
||||
CREATE INDEX ix_drawings_cutFirst ON drawings(cutFirst);
|
||||
CREATE INDEX ix_drawings_savedAt ON drawings(savedAt DESC);
|
||||
""";
|
||||
|
||||
private const string DrawingLibrarySchema = DrawingLibraryTables + "PRAGMA user_version=5;";
|
||||
|
||||
// v4 kept one column set in both tables plus a package blob copied into every
|
||||
// revision. The upgrade stores each distinct blob once and copies every revision
|
||||
// and current row as it was; nesting settings take their defaults (v4 had none).
|
||||
private const string V4CopiedColumns = """
|
||||
id, revision, name, customer, nameKey, customerKey, materialName, materialKey,
|
||||
materialGrade, gradeKey, thickness, units, cutType, notes, archived,
|
||||
packageSha256, packageSize, savedAt, savedBy
|
||||
""";
|
||||
|
||||
private const string DrawingLibraryUpgradeFromV4 = $"""
|
||||
DROP INDEX ix_drawing_revisions_key;
|
||||
DROP INDEX ix_drawings_customer_name;
|
||||
DROP INDEX ix_drawings_material;
|
||||
DROP INDEX ix_drawings_thickness;
|
||||
DROP INDEX ix_drawings_savedAt;
|
||||
ALTER TABLE drawings RENAME TO drawings_v4;
|
||||
ALTER TABLE drawing_revisions RENAME TO drawing_revisions_v4;
|
||||
{DrawingLibraryTables}
|
||||
INSERT OR IGNORE INTO drawing_packages (sha256, size, package)
|
||||
SELECT packageSha256, packageSize, package FROM drawing_revisions_v4;
|
||||
INSERT INTO drawing_revisions ({V4CopiedColumns}) SELECT {V4CopiedColumns} FROM drawing_revisions_v4;
|
||||
INSERT INTO drawings ({V4CopiedColumns}) SELECT {V4CopiedColumns} FROM drawings_v4;
|
||||
DROP TABLE drawings_v4;
|
||||
DROP TABLE drawing_revisions_v4;
|
||||
PRAGMA user_version=5;
|
||||
""";
|
||||
|
||||
|
||||
@@ -346,13 +346,18 @@ public sealed partial class NestDatabase : IDisposable
|
||||
""";
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
if (version < 5)
|
||||
if (version < 4)
|
||||
{
|
||||
// v5 replaces the v4 drawing tables outright (no production library data
|
||||
// existed); the nest tables are untouched.
|
||||
// No drawing tables yet: create the current (v5) layout directly.
|
||||
command.CommandText = DrawingLibrarySchema;
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
else if (version == 4)
|
||||
{
|
||||
// Packages become content-addressed; the nest tables are untouched.
|
||||
command.CommandText = DrawingLibraryUpgradeFromV4;
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,9 +211,11 @@ each nest.
|
||||
Schema v5 keeps three drawing tables in the same file: `drawing_packages`
|
||||
(`sha256`, `size`, `package`), `drawing_revisions` (every immutable revision's
|
||||
details, settings, archived flag and package hash) and `drawings` (each
|
||||
drawing's current revision, indexed for browsing). Upgrading replaces the v4
|
||||
drawing tables outright, discarding any v4 library data (no production library
|
||||
existed), and leaves the nest tables untouched. A server older than v5 refuses
|
||||
drawing's current revision, indexed for browsing). Upgrading from v4 runs in
|
||||
one transaction: each distinct v4 blob is stored once by its hash, every
|
||||
revision and current row is copied as it was with default nesting settings
|
||||
(v4 had none), and the v4 drawing tables are dropped. A pre-v4 database gets
|
||||
the v5 tables directly. The nest tables are untouched. A server older than v5 refuses
|
||||
to open a v5 database, so rolling back after the upgrade needs the pre-upgrade backup (see
|
||||
[Backup, restore, and upgrade](#backup-restore-and-upgrade)). The stopped-volume
|
||||
`tar` backup includes the drawing tables, but `nest_manifest.py` and
|
||||
|
||||
Reference in new issue
Block a user