Unique Keys - nested, subtree and element keys, [RedbTags]
Unique keys beyond a root scalar (V4). Props are stored flat, one row per value, and the
storage already maintains a canonical hash for every nested class and collection - so
[RedbUnique] reaches four more shapes with the same database-enforced index:
1. A scalar INSIDE a nested class - a lambda path (d => d.Identity!.Serial) is the key.
2. A whole SUBTREE - the content of a nested class, in canonical form, is the key.
3. A whole COLLECTION - the ordered list is the key; the same numbers in another order differ.
4. ELEMENTS of a collection with a scope: UniqueScope.Collection forbids duplicates
inside one object's collection, UniqueScope.Scheme makes every element value unique
across all objects of the scheme (and probeable by GetByUniqueAsync).
Plus [RedbTags]: a free-form marker written to the scheme and structure metadata for
applications and extensions to read back.
storage already maintains a canonical hash for every nested class and collection - so
[RedbUnique] reaches four more shapes with the same database-enforced index:
1. A scalar INSIDE a nested class - a lambda path (d => d.Identity!.Serial) is the key.
2. A whole SUBTREE - the content of a nested class, in canonical form, is the key.
3. A whole COLLECTION - the ordered list is the key; the same numbers in another order differ.
4. ELEMENTS of a collection with a scope: UniqueScope.Collection forbids duplicates
inside one object's collection, UniqueScope.Scheme makes every element value unique
across all objects of the scheme (and probeable by GetByUniqueAsync).
Plus [RedbTags]: a free-form marker written to the scheme and structure metadata for
applications and extensions to read back.
await redb.SyncSchemeAsync<DeviceProps>();
// Unique per run so the example is rerunnable on a shared database.
var run = Guid.NewGuid().ToString("N")[..8];
var sw = Stopwatch.StartNew();
// 1. Key on a scalar inside a nested class: same rules as a root key.
var serial = $"SN-{run}";
var alphaId = await redb.SaveAsync(Device("alpha", d => d.Identity = new DeviceIdentity { Serial = serial, Vendor = "Acme" }));
var nestedDup = await Rejected(() => redb.SaveAsync(Device("alpha-dup", d => d.Identity = new DeviceIdentity { Serial = serial, Vendor = "Other" })));
var bySerial = await redb.GetByUniqueAsync<DeviceProps>(d => d.Identity!.Serial, serial);
// 2. Subtree key: the CONTENT of the nested class is the key. Same content - rejected;
// one field different - a different key. The lookup canonicalises the probe object
// the same way the save did.
var region = $"eu-{run}";
var betaId = await redb.SaveAsync(Device("beta", d => d.Config = new DeviceConfig { Region = region, Tier = 2 }));
var subtreeDup = await Rejected(() => redb.SaveAsync(Device("beta-dup", d => d.Config = new DeviceConfig { Region = region, Tier = 2 })));
await redb.SaveAsync(Device("beta-tier3", d => d.Config = new DeviceConfig { Region = region, Tier = 3 }));
var byConfig = await redb.GetByUniqueAsync<DeviceProps>(d => d.Config, new DeviceConfig { Region = region, Tier = 2 });
// 3. Subtree key on a collection: the ordered list is the key.
var basePort = Random.Shared.Next(10_000, 60_000);
List<long> ports = [basePort, basePort + 1, basePort + 2];
await redb.SaveAsync(Device("gamma", d => d.Ports = ports));
var listDup = await Rejected(() => redb.SaveAsync(Device("gamma-dup", d => d.Ports = [.. ports])));
await redb.SaveAsync(Device("gamma-reversed", d => d.Ports = [basePort + 2, basePort + 1, basePort]));
// 4. Element keys, Collection scope: no duplicate label inside ONE device. Another device
// may carry the same label - the scope is the collection, not the scheme.
await redb.SaveAsync(Device("delta", d => d.Labels = ["rack-1", "spare"]));
await redb.SaveAsync(Device("delta-twin", d => d.Labels = ["rack-1"]));
var elementDup = await Rejected(() => redb.SaveAsync(Device("delta-dup", d => d.Labels = ["rack-1", "rack-1"])));
// 5. Element keys, Scheme scope: a MAC address belongs to one device in the whole scheme,
// and the holder is found by the element value.
var mac = $"02:{run[..2]}:{run[2..4]}:{run[4..6]}:00:01";
// The second address comes from the same run characters as the first: the key is unique across the
// scheme, and two characters alone (256 values) collide with earlier runs of this example.
var epsilonId = await redb.SaveAsync(Device("epsilon", d => d.MacAddresses = [mac, $"02:{run[..2]}:{run[2..4]}:{run[4..6]}:00:02"]));
var macDup = await Rejected(() => redb.SaveAsync(Device("epsilon-dup", d => d.MacAddresses = [mac])));
var byMac = await redb.GetByUniqueAsync<DeviceProps>(d => d.MacAddresses, mac);
// 6. [RedbTags]: the markers are metadata - read them back from the synced scheme.
var scheme = await redb.GetSchemeByTypeAsync<DeviceProps>();
var schemeTags = scheme?.Tags;
var macTags = scheme?.GetStructureByName(nameof(DeviceProps.MacAddresses))?.Tags;
sw.Stop();
// The example is a contract probe: a duplicate that slipped through, a lookup that missed
// or a tag that did not land is a FAIL, not a line in the report.
var problems = new List<string>();
if (!nestedDup.StartsWith("rejected")) problems.Add("nested key: duplicate was not rejected");
if (bySerial?.Id != alphaId) problems.Add("nested key: lookup by lambda path missed");
if (!subtreeDup.StartsWith("rejected")) problems.Add("subtree key: duplicate content was not rejected");
if (byConfig?.Id != betaId) problems.Add("subtree key: lookup by content missed");
if (!listDup.StartsWith("rejected")) problems.Add("collection key: duplicate list was not rejected");
if (!elementDup.StartsWith("rejected")) problems.Add("element key (Collection): duplicate inside one device was not rejected");
if (!macDup.StartsWith("rejected")) problems.Add("element key (Scheme): duplicate across devices was not rejected");
if (byMac?.Id != epsilonId) problems.Add("element key (Scheme): lookup by element value missed");
if (schemeTags != "examples,e004") problems.Add($"[RedbTags] on the scheme did not land: '{schemeTags}'");
if (macTags != "network") problems.Add($"[RedbTags] on the structure did not land: '{macTags}'");
if (problems.Count > 0)
return Fail("E004", "Unique Keys - nested, subtree and element keys, [RedbTags]", ExampleTier.Free, sw.ElapsedMilliseconds, string.Join("; ", problems));
Device
private static RedbObject<DeviceProps> Device(string title, Action<DeviceProps> configure)
{
var props = new DeviceProps { Title = title };
configure(props);
return new RedbObject<DeviceProps> { name = $"e004-{title}", Props = props };
}
Rejected
private static async Task<string> Rejected(Func<Task<long>> save)
{
try
{
await save();
return "NOT rejected - this line must never print";
}
catch (RedbUniqueViolationException ex)
{
return $"rejected ({ex.Kind}, {ex.PropertyName})";
}
}
PG Pro
1354ms(9)
postgres.pro.repeat
1292ms(9)
postgres.pro.cache
1275ms(9)
MS Pro
970ms(9)
mssql.pro.repeat
1258ms(9)
mssql.pro.cache
873ms(9)
sqlite.pro
57ms(9)
sqlite.pro.repeat
74ms(9)
sqlite.pro.cache
36ms(9)