|
| 1 | +# Union Types in SharpCoreDB — Design Pass (v2.1 Preview) |
| 2 | + |
| 3 | +> Branch: `release/v2.1.0.0-preview.1` · Reference: [Union types — C# reference](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/union) |
| 4 | +
|
| 5 | +## Status |
| 6 | + |
| 7 | +**Verified available** on .NET 11 RC1 (C# 15 preview): union types work via the `union` keyword and the |
| 8 | +`[System.Runtime.CompilerServices.Union]` attribute with `IUnion`. The runtime ships |
| 9 | +`UnionAttribute`/`IUnion` since .NET 11 Preview 5. |
| 10 | + |
| 11 | +**Recommendation:** expose unions as a **new opt-in namespace** `SharpCoreDB.Unions` (net11.0-only), |
| 12 | +**never retrofit** existing result types (that would be breaking), and **hold off shipping** until the |
| 13 | +preview syntax stabilizes at GA. This document is the ready-to-implement blueprint. |
| 14 | + |
| 15 | +## Verified syntax (compiles + runs on RC1) |
| 16 | + |
| 17 | +```csharp |
| 18 | +public record class Cat(string Name); |
| 19 | +public record class Dog(string Name); |
| 20 | + |
| 21 | +// Modern union keyword: the compiler generates a `struct : IUnion` with a `Value` property and |
| 22 | +// implicit conversions from each case type. |
| 23 | +public union Pet(Cat, Dog); |
| 24 | + |
| 25 | +// Class-based union (reference semantics / private constructors / factories): |
| 26 | +[System.Runtime.CompilerServices.Union] |
| 27 | +public class Result<T> : System.Runtime.CompilerServices.IUnion |
| 28 | +{ |
| 29 | + private readonly object? _value; |
| 30 | + public Result(T? value) { _value = value; } |
| 31 | + public object? Value => _value; |
| 32 | +} |
| 33 | + |
| 34 | +// Exhaustive switch: removing any case below is a COMPILE ERROR. |
| 35 | +string Describe(Pet pet) => pet switch |
| 36 | +{ |
| 37 | + Cat c => $"Cat {c.Name}", |
| 38 | + Dog d => $"Dog {d.Name}", |
| 39 | +}; |
| 40 | +``` |
| 41 | + |
| 42 | +Key semantics: cases can be classes, structs, interfaces, type parameters, nullable types or other unions; |
| 43 | +value-type cases are boxed into `Value`; unions are structs without equality/cloning/deconstruction |
| 44 | +(put those on the case types, e.g. `record class` cases). |
| 45 | + |
| 46 | +## Proposed API: `SharpCoreDB.Unions` (additive, opt-in, net11.0-only) |
| 47 | + |
| 48 | +```csharp |
| 49 | +#if NET11_0_OR_GREATER |
| 50 | +namespace SharpCoreDB.Unions; |
| 51 | + |
| 52 | +using System.Runtime.CompilerServices; |
| 53 | +using SharpCoreDB.DataStructures; |
| 54 | + |
| 55 | +// Query outcome — compile-time exhaustive alternative to returning nullable/boolean-or-throw. |
| 56 | +public record class QuerySuccess(IReadOnlyList<Dictionary<string, object?>> Rows); |
| 57 | +public record class QueryNotFound; |
| 58 | +public record class QueryFailure(string Message); |
| 59 | +[Union] |
| 60 | +public partial class QueryOutcome : IUnion |
| 61 | +{ |
| 62 | + private readonly object? _value; |
| 63 | + public QueryOutcome(QuerySuccess value) { _value = value; } |
| 64 | + public QueryOutcome(QueryNotFound value) { _value = value; } |
| 65 | + public QueryOutcome(QueryFailure value) { _value = value; } |
| 66 | + public object? Value => _value; |
| 67 | +} |
| 68 | + |
| 69 | +// Execute outcome (rows affected or failure). |
| 70 | +public record class ExecuteSuccess(long RowsAffected); |
| 71 | +public record class ExecuteFailure(string Message); |
| 72 | +[Union] |
| 73 | +public partial class ExecuteOutcome : IUnion |
| 74 | +{ |
| 75 | + private readonly object? _value; |
| 76 | + public ExecuteOutcome(ExecuteSuccess value) { _value = value; } |
| 77 | + public ExecuteOutcome(ExecuteFailure value) { _value = value; } |
| 78 | + public object? Value => _value; |
| 79 | +} |
| 80 | + |
| 81 | +// Table lookup (used with the extension indexer db["name"]). |
| 82 | +public record class TableFound(TableInfo Table); |
| 83 | +public record class TableMissing(string Name); |
| 84 | +[Union] |
| 85 | +public partial class TableLookup : IUnion |
| 86 | +{ |
| 87 | + private readonly object? _value; |
| 88 | + public TableLookup(TableFound value) { _value = value; } |
| 89 | + public TableLookup(TableMissing value) { _value = value; } |
| 90 | + public object? Value => _value; |
| 91 | +} |
| 92 | +#endif |
| 93 | +``` |
| 94 | + |
| 95 | +Usage (opt-in, only on net11.0): |
| 96 | + |
| 97 | +```csharp |
| 98 | +using SharpCoreDB.Unions; |
| 99 | + |
| 100 | +QueryOutcome outcome = ...; // returned by NEW opt-in methods only |
| 101 | +string text = outcome switch |
| 102 | +{ |
| 103 | + QuerySuccess s => $"rows: {s.Rows.Count}", |
| 104 | + QueryNotFound => "not found", |
| 105 | + QueryFailure f => $"error: {f.Message}", |
| 106 | +}; |
| 107 | +``` |
| 108 | + |
| 109 | +## Backward compatibility |
| 110 | + |
| 111 | +- The file is source-gated `#if NET11_0_OR_GREATER` → **compiled only for net11.0**; net10.0 consumers |
| 112 | + never see it. |
| 113 | +- Existing `QueryResult` / `ExecuteResult` / `ExecuteSQL` signatures stay **byte-identical** on both TFMs. |
| 114 | +- Union values can be inspected generically via `IUnion`: |
| 115 | + `if (outcome is IUnion { Value: null }) { ... }`. |
| 116 | + |
| 117 | +## Performance notes |
| 118 | + |
| 119 | +- Value-type cases are boxed into the `object? Value`. |
| 120 | +- Unions are structs; for rich equality/deconstruction keep `record class` case types. |
| 121 | +- The `QueryOutcome` design above uses `[Union]` **class** unions (reference semantics) to keep the |
| 122 | + API familiar; the `union` keyword equivalent (`public union QueryOutcome(QuerySuccess, QueryNotFound, QueryFailure);`) |
| 123 | + is the lighter-weight struct alternative. |
| 124 | + |
| 125 | +## Test strategy |
| 126 | + |
| 127 | +1. **Compile-time exhaustiveness** (the core value): a test that switches over `QueryOutcome` with all |
| 128 | + cases; add a 4th case type later and watch CI fail until every switch handles it. |
| 129 | +2. **Runtime**: construct every case, switch-match it, verify `IUnion.Value` boxing, verify implicit |
| 130 | + conversion from each case type. |
| 131 | +3. Existing suite stays green: net10.0 1679 / net11.0 1688 (new tests are net11-gated). |
| 132 | + |
| 133 | +## Risk & decision |
| 134 | + |
| 135 | +- Union types are still **C# 15 preview**; the implementation (visibility, `Value` boxing, member |
| 136 | + requirements) can shift between RC and GA. |
| 137 | +- Because existing APIs are untouched and the new surface is opt-in, shipping early is low-risk, but |
| 138 | + frozen syntax is higher-value. |
| 139 | +- **Decision:** keep `SharpCoreDB.Unions` as a design blueprint for the 2.1 preview; implement it once |
| 140 | + .NET 11 GA / final C# 15 confirms the syntax. |
0 commit comments