feat(server): keep a nest's units on its record

A record's thickness is in the nest's units, but nothing on the record said
which, so a list of nests could not tell 9.525 mm from 9.525 inches without
reading each archive. Records now carry units, read from the uploaded
archive like the counts (absent or unknown values are inches) and kept by a
metadata-only save. Schema v12 adds the column and reads every stored
archive, current and historical, so existing millimeter nests report it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-25 18:40:43 -04:00
1 parent 610b9dafcb
commit 2d8d877482
12 files changed
+185 -20

No files matched your search

+6
View File
@@ -34,6 +34,12 @@ public sealed class NestRecord
public double Thickness { get; set; } public double Thickness { get; set; }
/// <summary>
/// The nest's units, which <see cref="Thickness"/> is in. The server reads them from the
/// archive like the counts; inches when the archive has none or the server predates them.
/// </summary>
public Units Units { get; set; }
/// <summary>quote | toBeCut | hasBeenCut (camelCase on the wire, case-insensitive readers).</summary> /// <summary>quote | toBeCut | hasBeenCut (camelCase on the wire, case-insensitive readers).</summary>
public NestStatus Status { get; set; } = NestStatus.Quote; public NestStatus Status { get; set; } = NestStatus.Quote;
+1
View File
@@ -46,6 +46,7 @@ public static class NestRecordFactory
Material = nest.Material?.Name ?? "", Material = nest.Material?.Name ?? "",
MaterialGrade = nest.Material?.Grade ?? "", MaterialGrade = nest.Material?.Grade ?? "",
Thickness = nest.Thickness, Thickness = nest.Thickness,
Units = nest.Units,
Status = nest.Status, Status = nest.Status,
// The trailing empty plate is the editor's new-plate slot, not a plate of the nest. // The trailing empty plate is the editor's new-plate slot, not a plate of the nest.
PlateCount = PlateDisplayNumbering.DisplayedPlateCount(nest.Plates), PlateCount = PlateDisplayNumbering.DisplayedPlateCount(nest.Plates),
@@ -22,7 +22,7 @@ public sealed class DrawingLibraryDatabaseTests
using (var database = new NestDatabase(factory.DatabasePath, pooling: false)) using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
Assert.Empty(database.QueryDrawings(new DrawingLibraryQuery { IncludeArchived = true }).Items); Assert.Empty(database.QueryDrawings(new DrawingLibraryQuery { IncludeArchived = true }).Items);
Assert.Equal(before, NestDigest(factory.DatabasePath)); Assert.Equal(before, NestDigest(factory.DatabasePath));
Assert.Equal(11L, Scalar(factory.DatabasePath, "PRAGMA user_version")); Assert.Equal(12L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
} }
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false); using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
@@ -43,7 +43,7 @@ public sealed class DrawingLibraryDatabaseTests
using (var database = new NestDatabase(factory.DatabasePath, pooling: false)) using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
{ {
Assert.Equal(11L, Scalar(factory.DatabasePath, "PRAGMA user_version")); Assert.Equal(12L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
Assert.Equal(before, NestDigest(factory.DatabasePath)); Assert.Equal(before, NestDigest(factory.DatabasePath));
// Two distinct blobs across four revisions: the bracket's notes edit and the // Two distinct blobs across four revisions: the bracket's notes edit and the
// plate both reuse the bracket's first package. // plate both reuse the bracket's first package.
@@ -80,7 +80,7 @@ public sealed class DrawingLibraryDatabaseTests
{ {
using var factory = new ServerFactory(); using var factory = new ServerFactory();
using (new NestDatabase(factory.DatabasePath, pooling: false)) { } using (new NestDatabase(factory.DatabasePath, pooling: false)) { }
Execute(factory.DatabasePath, "PRAGMA user_version=12"); Execute(factory.DatabasePath, "PRAGMA user_version=13");
Assert.Throws<InvalidOperationException>(() => new NestDatabase(factory.DatabasePath, pooling: false)); Assert.Throws<InvalidOperationException>(() => new NestDatabase(factory.DatabasePath, pooling: false));
} }
@@ -22,7 +22,7 @@ public sealed class DrawingSetDatabaseTests
using (var database = new NestDatabase(factory.DatabasePath, pooling: false)) using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
Assert.Empty(database.QueryDrawingSets(new DrawingSetQuery { IncludeArchived = true }).Items); Assert.Empty(database.QueryDrawingSets(new DrawingSetQuery { IncludeArchived = true }).Items);
Assert.Equal(before, Digest(factory.DatabasePath)); Assert.Equal(before, Digest(factory.DatabasePath));
Assert.Equal(11L, Scalar(factory.DatabasePath, "PRAGMA user_version")); Assert.Equal(12L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
} }
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false); using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
@@ -47,7 +47,7 @@ public sealed class DrawingSetDatabaseTests
Assert.Equal(before, Digest(factory.DatabasePath)); Assert.Equal(before, Digest(factory.DatabasePath));
Execute(factory.DatabasePath, "DROP TABLE ix_drawing_set_members_drawing"); Execute(factory.DatabasePath, "DROP TABLE ix_drawing_set_members_drawing");
using var database = new NestDatabase(factory.DatabasePath, pooling: false); using var database = new NestDatabase(factory.DatabasePath, pooling: false);
Assert.Equal(11L, Scalar(factory.DatabasePath, "PRAGMA user_version")); Assert.Equal(12L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
} }
[Fact] [Fact]
+1 -1
View File
@@ -24,7 +24,7 @@ public sealed class MaterialListTests
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false); using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
Assert.Equal(11L, Scalar(factory.DatabasePath, "PRAGMA user_version")); Assert.Equal(12L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
Assert.Empty(upgraded.ListMaterials()); Assert.Empty(upgraded.ListMaterials());
Assert.Equal((" Stainless ", "304"), (upgraded.Get(nest)!.Material, upgraded.Get(nest)!.MaterialGrade)); Assert.Equal((" Stainless ", "304"), (upgraded.Get(nest)!.Material, upgraded.Get(nest)!.MaterialGrade));
Assert.Equal(drawing.MaterialName, upgraded.GetDrawing(drawing.Id)!.MaterialName); Assert.Equal(drawing.MaterialName, upgraded.GetDrawing(drawing.Id)!.MaterialName);
@@ -74,7 +74,7 @@ public sealed class NestMaterialGradeTests
Assert.Equal("Stainless", record.Material); Assert.Equal("Stainless", record.Material);
Assert.Equal("", record.MaterialGrade); Assert.Equal("", record.MaterialGrade);
Assert.Equal("", Assert.Single(upgraded.History(id, 0, 10)!.Items).MaterialGrade); Assert.Equal("", Assert.Single(upgraded.History(id, 0, 10)!.Items).MaterialGrade);
Assert.Equal(11L, Scalar(factory.DatabasePath, "PRAGMA user_version")); Assert.Equal(12L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
} }
private static NestRecord Record(string name, string material, string grade) => new() private static NestRecord Record(string name, string material, string grade) => new()
+109
View File
@@ -0,0 +1,109 @@
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Data.Sqlite;
using OpenNest.Data;
namespace OpenNest.Server.Tests;
/// <summary>
/// Nest records carry the archive's units, which their thickness is in, so a list of nests can
/// tell a millimeter nest from an inch one without reading each archive.
/// </summary>
public sealed class NestUnitsTests
{
[Fact]
public async Task Upload_TakesUnitsFromTheArchive_NotTheClientsClaim()
{
using var factory = new ServerFactory();
using var client = factory.CreateClient();
var nest = NestApiTests.SyntheticNest();
nest.Units = Units.Millimeters;
nest.Thickness = 6.35;
using var created = await client.PostAsync("/api/nests",
NestRevisionTests.Body(new NestRecord { Name = "Metric", Units = Units.Inches }, NestApiTests.Archive(nest)));
var json = await created.Content.ReadAsStringAsync();
var id = JsonDocument.Parse(json).RootElement.GetProperty("id").GetGuid();
Assert.Equal("millimeters", JsonDocument.Parse(json).RootElement.GetProperty("units").GetString());
using var listed = JsonDocument.Parse(await client.GetStringAsync("/api/nests/query"));
var item = Assert.Single(listed.RootElement.GetProperty("items").EnumerateArray());
Assert.Equal("millimeters", item.GetProperty("units").GetString());
Assert.Equal(6.35, item.GetProperty("thickness").GetDouble());
using var record = JsonDocument.Parse(await client.GetStringAsync($"/api/nests/{id}"));
Assert.Equal("millimeters", record.RootElement.GetProperty("units").GetString());
}
[Fact]
public void MetadataOnlySave_KeepsTheArchivesUnits()
{
using var factory = new ServerFactory();
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
var id = Guid.NewGuid();
var created = database.Insert(id, Record("Metric", Units.Millimeters), [1]);
var updated = database.UpdateMetadata(id, Record("Metric renamed", Units.Inches), created.Revision)!;
Assert.Equal(Units.Millimeters, updated.Units);
Assert.Equal([Units.Millimeters, Units.Millimeters], database.History(id, 0, 10)!.Items.Select(r => r.Units));
}
[Fact]
public void Version11Database_ReadsEachStoredArchivesUnits()
{
using var factory = new ServerFactory();
var metric = Guid.NewGuid();
var inch = Guid.NewGuid();
var unreadable = Guid.NewGuid();
var metricNest = NestApiTests.SyntheticNest();
metricNest.Units = Units.Millimeters;
using (var database = new NestDatabase(factory.DatabasePath, pooling: false))
{
var created = database.Insert(metric, Record("Metric", Units.Inches), NestApiTests.Archive(metricNest));
database.UpdateMetadata(metric, Record("Metric renamed", Units.Inches), created.Revision);
database.Insert(inch, Record("Inch", Units.Inches), NestApiTests.Archive(NestApiTests.SyntheticNest()));
database.Insert(unreadable, Record("Broken", Units.Inches), [1]);
}
Execute(factory.DatabasePath, """
ALTER TABLE nests DROP COLUMN units;
ALTER TABLE nest_revisions DROP COLUMN units;
PRAGMA user_version=11;
""");
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
Assert.Equal(Units.Millimeters, upgraded.Get(metric)!.Units);
Assert.Equal([Units.Millimeters, Units.Millimeters], upgraded.History(metric, 0, 10)!.Items.Select(r => r.Units));
Assert.Equal(Units.Inches, upgraded.Get(inch)!.Units);
Assert.Equal(Units.Inches, upgraded.Get(unreadable)!.Units);
Assert.Equal(12L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
}
private static NestRecord Record(string name, Units units) => new()
{
Name = name,
Material = "Steel",
Thickness = 0.25,
Units = units,
DateCreated = new DateTime(2026, 1, 1),
DateModified = new DateTime(2026, 1, 2),
};
private static void Execute(string path, string sql)
{
using var connection = new SqliteConnection($"Data Source={path};Pooling=False");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = sql;
command.ExecuteNonQuery();
}
private static object? Scalar(string path, string sql)
{
using var connection = new SqliteConnection($"Data Source={path};Pooling=False");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = sql;
return command.ExecuteScalar();
}
}
@@ -151,7 +151,7 @@ public sealed class SetReleaseDrawingTests
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false); using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
Assert.Equal(11L, Scalar(factory.DatabasePath, "PRAGMA user_version")); Assert.Equal(12L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
var kept = upgraded.GetRelease(releaseId)!; var kept = upgraded.GetRelease(releaseId)!;
Assert.Empty(kept.Drawings); Assert.Empty(kept.Drawings);
Assert.Equal(((Guid?)kit.Id, drawing, 2), (kept.Nests[0].Items.Single().SetId, kept.Nests[0].Items.Single().DrawingId, kept.Nests[0].Items.Single().Quantity)); Assert.Equal(((Guid?)kit.Id, drawing, 2), (kept.Nests[0].Items.Single().SetId, kept.Nests[0].Items.Single().DrawingId, kept.Nests[0].Items.Single().Quantity));
+1 -1
View File
@@ -294,7 +294,7 @@ public sealed class SetReleaseTests
using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false); using var upgraded = new NestDatabase(factory.DatabasePath, pooling: false);
Assert.Equal(11L, Scalar(factory.DatabasePath, "PRAGMA user_version")); Assert.Equal(12L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
Assert.Equal(0L, Scalar(factory.DatabasePath, "SELECT COUNT(*) FROM sqlite_master WHERE name LIKE 'set_release%' OR name LIKE 'ix_set_release%'")); Assert.Equal(0L, Scalar(factory.DatabasePath, "SELECT COUNT(*) FROM sqlite_master WHERE name LIKE 'set_release%' OR name LIKE 'ix_set_release%'"));
var release = upgraded.GetRelease(releaseId)!; var release = upgraded.GetRelease(releaseId)!;
Assert.Equal(("Legacy notes", "Legacy operator", false), (release.Notes, release.ReleasedBy, release.IsUndone)); Assert.Equal(("Legacy notes", "Legacy operator", false), (release.Notes, release.ReleasedBy, release.IsUndone));
+10 -3
View File
@@ -20,7 +20,7 @@ internal static class NestArchive
private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web); private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
/// <summary> /// <summary>
/// Overwrites the record's plate count, part count, material and thickness with the /// Overwrites the record's plate count, part count, material, thickness and units with the
/// archive's own. Returns an error when the bytes are not a readable nest archive. /// archive's own. Returns an error when the bytes are not a readable nest archive.
/// </summary> /// </summary>
public static string? Apply(NestRecord record, byte[] file) public static string? Apply(NestRecord record, byte[] file)
@@ -33,6 +33,7 @@ internal static class NestArchive
// falls back to the plate defaults. Thickness falls back when not positive. // falls back to the plate defaults. Thickness falls back when not positive.
record.Material = (nest.Material ?? defaults.Material)?.Name ?? ""; record.Material = (nest.Material ?? defaults.Material)?.Name ?? "";
record.Thickness = nest.Thickness > 0 ? nest.Thickness : defaults.Thickness; record.Thickness = nest.Thickness > 0 ? nest.Thickness : defaults.Thickness;
record.Units = UnitsOf(nest);
// NestReader drops parts whose drawing is missing. Cut-off parts are never written as // NestReader drops parts whose drawing is missing. Cut-off parts are never written as
// parts, so every remaining part counts. // parts, so every remaining part counts.
@@ -106,8 +107,7 @@ internal static class NestArchive
}) })
.ToList(); .ToList();
// NestReader: absent or unknown units keep the nest default, inches. var units = UnitsOf(nest);
var units = Enum.TryParse<Units>(nest.Units, ignoreCase: true, out var parsed) ? parsed : Units.Inches;
sheets = new NestSheets sheets = new NestSheets
{ {
@@ -121,6 +121,13 @@ internal static class NestArchive
return null; return null;
} }
/// <summary>The archive's units, for backfilling records stored before they were kept; inches when unreadable.</summary>
public static Units ReadUnits(byte[] file) => Read(file, out var nest) is null ? UnitsOf(nest) : Units.Inches;
// NestReader: absent or unknown units keep the nest default, inches.
private static Units UnitsOf(NestJson nest) =>
Enum.TryParse<Units>(nest.Units, ignoreCase: true, out var parsed) ? parsed : Units.Inches;
private static string? Read(byte[] file, out NestJson nest) private static string? Read(byte[] file, out NestJson nest)
{ {
nest = null!; nest = null!;
+47 -7
View File
@@ -154,10 +154,10 @@ public sealed partial class NestDatabase : IDisposable
command.Transaction = transaction; command.Transaction = transaction;
command.CommandText = """ command.CommandText = """
INSERT INTO nests (id, name, customer, dateCreated, dateModified, material, INSERT INTO nests (id, name, customer, dateCreated, dateModified, material,
materialGrade, thickness, status, plateCount, partCount, comments, madeBy, materialGrade, thickness, units, status, plateCount, partCount, comments, madeBy,
fileSize, savedAt, file) fileSize, savedAt, file)
VALUES ($id, $name, $customer, $dateCreated, $dateModified, $material, VALUES ($id, $name, $customer, $dateCreated, $dateModified, $material,
$materialGrade, $thickness, $status, $plateCount, $partCount, $comments, $materialGrade, $thickness, $units, $status, $plateCount, $partCount, $comments,
$madeBy, $fileSize, $savedAt, $file) $madeBy, $fileSize, $savedAt, $file)
"""; """;
AddRecordParameters(command, id, record, file, updateFile: true); AddRecordParameters(command, id, record, file, updateFile: true);
@@ -186,9 +186,10 @@ public sealed partial class NestDatabase : IDisposable
if (current.Revision != expectedRevision) throw new RevisionConflictException(current); if (current.Revision != expectedRevision) throw new RevisionConflictException(current);
if (file is null) if (file is null)
{ {
// The counts describe the archive, which a metadata-only save leaves unchanged. // The counts and units describe the archive, which a metadata-only save leaves unchanged.
record.PlateCount = current.PlateCount; record.PlateCount = current.PlateCount;
record.PartCount = current.PartCount; record.PartCount = current.PartCount;
record.Units = current.Units;
} }
file ??= GetFile(id)!; file ??= GetFile(id)!;
if (SameMetadata(current, record) && GetFile(id)!.AsSpan().SequenceEqual(file)) return current; if (SameMetadata(current, record) && GetFile(id)!.AsSpan().SequenceEqual(file)) return current;
@@ -210,7 +211,7 @@ public sealed partial class NestDatabase : IDisposable
UPDATE nests SET UPDATE nests SET
name = $name, customer = $customer, dateCreated = $dateCreated, name = $name, customer = $customer, dateCreated = $dateCreated,
dateModified = $dateModified, material = $material, dateModified = $dateModified, material = $material,
materialGrade = $materialGrade, thickness = $thickness, status = $status, plateCount = $plateCount, materialGrade = $materialGrade, thickness = $thickness, units = $units, status = $status, plateCount = $plateCount,
partCount = $partCount, comments = $comments, madeBy = $madeBy, partCount = $partCount, comments = $comments, madeBy = $madeBy,
savedAt = $savedAt, revision = $revision, restoredFromRevision=$source{fileClause} savedAt = $savedAt, revision = $revision, restoredFromRevision=$source{fileClause}
WHERE id = $id WHERE id = $id
@@ -320,7 +321,7 @@ public sealed partial class NestDatabase : IDisposable
private static bool SameMetadata(NestRecord a, NestRecord b) => private static bool SameMetadata(NestRecord a, NestRecord b) =>
a.Name == b.Name && a.Customer == b.Customer && a.DateCreated == b.DateCreated && a.Name == b.Name && a.Customer == b.Customer && a.DateCreated == b.DateCreated &&
a.DateModified == b.DateModified && a.Material == b.Material && a.MaterialGrade == b.MaterialGrade && a.DateModified == b.DateModified && a.Material == b.Material && a.MaterialGrade == b.MaterialGrade &&
a.Thickness == b.Thickness && a.Status == b.Status && a.PlateCount == b.PlateCount && a.PartCount == b.PartCount && a.Thickness == b.Thickness && a.Units == b.Units && a.Status == b.Status && a.PlateCount == b.PlateCount && a.PartCount == b.PartCount &&
a.Comments == b.Comments && a.MadeBy == b.MadeBy; a.Comments == b.Comments && a.MadeBy == b.MadeBy;
private void Snapshot(Guid id, SqliteTransaction transaction) private void Snapshot(Guid id, SqliteTransaction transaction)
@@ -341,7 +342,7 @@ public sealed partial class NestDatabase : IDisposable
command.Transaction = transaction; command.Transaction = transaction;
command.CommandText = "PRAGMA user_version"; command.CommandText = "PRAGMA user_version";
var version = (long)command.ExecuteScalar()!; var version = (long)command.ExecuteScalar()!;
if (version > 11) throw new InvalidOperationException("Database schema is newer than this server."); if (version > 12) throw new InvalidOperationException("Database schema is newer than this server.");
if (version == 0) if (version == 0)
{ {
command.CommandText = """ command.CommandText = """
@@ -426,10 +427,47 @@ public sealed partial class NestDatabase : IDisposable
command.CommandText = ReleaseUpgradeToV11; command.CommandText = ReleaseUpgradeToV11;
command.ExecuteNonQuery(); command.ExecuteNonQuery();
} }
if (version < 12)
AddUnits(command);
transaction.Commit(); transaction.Commit();
} }
} }
// v12: records keep the archive's units, which their thickness is in. Every stored archive,
// current or historical, is read once so millimeter nests are not reported as inches.
private static void AddUnits(SqliteCommand command)
{
foreach (var table in new[] { "nests", "nest_revisions" })
{
command.CommandText = $"SELECT COUNT(*) FROM pragma_table_info('{table}') WHERE name='units'";
if ((long)command.ExecuteScalar()! == 0)
{
command.CommandText = $"ALTER TABLE {table} ADD COLUMN units TEXT NOT NULL DEFAULT 'Inches'";
command.ExecuteNonQuery();
}
var millimeters = new List<long>();
command.CommandText = $"SELECT rowid, file FROM {table}";
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
if (NestArchive.ReadUnits((byte[])reader.GetValue(1)) == Units.Millimeters)
millimeters.Add(reader.GetInt64(0));
}
}
command.CommandText = $"UPDATE {table} SET units='Millimeters' WHERE rowid=$rowid";
var rowid = command.Parameters.Add("$rowid", SqliteType.Integer);
foreach (var id in millimeters)
{
rowid.Value = id;
command.ExecuteNonQuery();
}
command.Parameters.Clear();
}
command.CommandText = "PRAGMA user_version=12";
command.ExecuteNonQuery();
}
/// <summary>Checks the live connection without exposing storage error details.</summary> /// <summary>Checks the live connection without exposing storage error details.</summary>
public bool IsHealthy() public bool IsHealthy()
{ {
@@ -464,7 +502,7 @@ public sealed partial class NestDatabase : IDisposable
private const string RecordColumns = """ private const string RecordColumns = """
id, name, customer, dateCreated, dateModified, material, thickness, id, name, customer, dateCreated, dateModified, material, thickness,
status, plateCount, partCount, comments, madeBy, fileSize, savedAt, revision, deleted, restoredFromRevision, status, plateCount, partCount, comments, madeBy, fileSize, savedAt, revision, deleted, restoredFromRevision,
materialGrade materialGrade, units
"""; """;
// Case-insensitive (ASCII) substring over the text columns and the status's stored // Case-insensitive (ASCII) substring over the text columns and the status's stored
@@ -557,6 +595,7 @@ public sealed partial class NestDatabase : IDisposable
command.Parameters.AddWithValue("$material", record.Material); command.Parameters.AddWithValue("$material", record.Material);
command.Parameters.AddWithValue("$materialGrade", record.MaterialGrade); command.Parameters.AddWithValue("$materialGrade", record.MaterialGrade);
command.Parameters.AddWithValue("$thickness", record.Thickness); command.Parameters.AddWithValue("$thickness", record.Thickness);
command.Parameters.AddWithValue("$units", record.Units.ToString());
command.Parameters.AddWithValue("$status", record.Status.ToString()); command.Parameters.AddWithValue("$status", record.Status.ToString());
command.Parameters.AddWithValue("$plateCount", record.PlateCount); command.Parameters.AddWithValue("$plateCount", record.PlateCount);
command.Parameters.AddWithValue("$partCount", record.PartCount); command.Parameters.AddWithValue("$partCount", record.PartCount);
@@ -594,6 +633,7 @@ public sealed partial class NestDatabase : IDisposable
Deleted = reader.GetInt32(first + 15) != 0, Deleted = reader.GetInt32(first + 15) != 0,
RestoredFromRevision = reader.IsDBNull(first + 16) ? null : reader.GetInt32(first + 16), RestoredFromRevision = reader.IsDBNull(first + 16) ? null : reader.GetInt32(first + 16),
MaterialGrade = reader.GetString(first + 17), MaterialGrade = reader.GetString(first + 17),
Units = Enum.TryParse<Units>(reader.GetString(first + 18), true, out var units) ? units : Units.Inches,
}; };
} }
} }
+3 -1
View File
@@ -83,6 +83,7 @@ entry, never programs or geometry, and takes the archive-derived fields below fr
| `material` | string | Material name (type), e.g. `Stainless`. Read from the uploaded archive (nest material, else plate defaults' on an explicit null); a metadata-only PUT may change it, and the desktop applies the record's value when opening the nest. | | `material` | string | Material name (type), e.g. `Stainless`. Read from the uploaded archive (nest material, else plate defaults' on an explicit null); a metadata-only PUT may change it, and the desktop applies the record's value when opening the nest. |
| `materialGrade` | string | Optional; default blank. Older servers drop it. A blank grade on a record saved before schema v8 is treated as unrecorded: opening keeps the archive's grade while the material name matches. | | `materialGrade` | string | Optional; default blank. Older servers drop it. A blank grade on a record saved before schema v8 is treated as unrecorded: opening keeps the archive's grade while the material name matches. |
| `thickness` | number | Read from the uploaded archive (nest thickness, else plate defaults' when not positive); editable like `material`. | | `thickness` | number | Read from the uploaded archive (nest thickness, else plate defaults' when not positive); editable like `material`. |
| `units` | `"inches"` \| `"millimeters"` | The nest's units, which `thickness` is in. Server-computed from the uploaded archive (absent or unknown values are inches, as `NestReader` reads them); ignored on upload and kept by a metadata-only PUT. Schema v12 read every stored archive, current and historical, to fill it. Older servers omit it, which readers take as inches. |
| `status` | `"draft"` \| `"quote"` \| `"toBeCut"` \| `"hasBeenCut"` | Default `quote`. | | `status` | `"draft"` \| `"quote"` \| `"toBeCut"` \| `"hasBeenCut"` | Default `quote`. |
| `plateCount` / `partCount` | integer | Server-computed from the uploaded archive (non-cutoff parts only; a trailing plate with no parts or cut-offs is not counted); ignored on upload and kept by a metadata-only PUT. | | `plateCount` / `partCount` | integer | Server-computed from the uploaded archive (non-cutoff parts only; a trailing plate with no parts or cut-offs is not counted); ignored on upload and kept by a metadata-only PUT. |
| `comments` | string | | | `comments` | string | |
@@ -247,7 +248,8 @@ only the requested page of metadata (never archive bytes):
can merge into): `status` (`draft`, `quote`, `toBeCut` or `hasBeenCut`), can merge into): `status` (`draft`, `quote`, `toBeCut` or `hasBeenCut`),
`material` and `materialGrade` (compared by `SharedListNames.Key`, so case and `material` and `materialGrade` (compared by `SharedListNames.Key`, so case and
spacing are ignored; a present but blank `materialGrade` asks for nests with no spacing are ignored; a present but blank `materialGrade` asks for nests with no
grade) and `thickness` (a number, zero or greater, matched at four decimals). grade) and `thickness` (a number, zero or greater, matched at four decimals in
each nest's own units).
The client sends them only when set, so plain browsing still works against The client sends them only when set, so plain browsing still works against
servers without them. servers without them.
- `offset` (default 0, at least 0) and `limit` (default 100, 1 to 500). - `offset` (default 0, at least 0) and `limit` (default 100, 1 to 500).