Skip to content

Commit 73c1420

Browse files
committed
UDDTs / scalar alias types (CREATE TYPE name FROM <builtin> [NULL|NOT NULL]) — second bacpac prerequisite. Real-feature implementation, not loader-only inline expansion. New AliasType class + Schema.AliasTypes ConcurrentDictionary sharing the type-name namespace with TableTypes (Msg 219 spans both dicts). Simulation.CreateType.cs gains a top-level AS-vs-FROM dispatch after the type name; the FROM branch reads the base type (length / scale via the standard (N[, S]) form), the optional NULL / NOT NULL marker (bare / explicit NULL both → nullable; NOT NULL → non-nullable, probe-confirmed against SQL Server 2025 on 2026-05-14), and stores into Schema.AliasTypes. Unknown base type raises Msg 222 verbatim (The base type "X" is not a valid base type for the alias data type.); namespace collision raises Msg 219.
Multi-part type-reference parsing extends every consumer site — CREATE TABLE column, DECLARE @v, ALTER TABLE ALTER COLUMN, CREATE PROCEDURE / FUNCTION / SEQUENCE parameter, OPENJSON column, sp_executesql parameter — to accept 1- or 2-part dotted names (`[dbo].[AccountNumber]`). Each site routes through the new `Simulation.ResolveTypeReference(BatchContext, MultiPartName, Name leaf, …)` helper which checks `Schema.AliasTypes` first and falls back to `SqlType.GetByName` for built-ins. Length specifier at an alias-usage site raises Msg 2716 St 3 verbatim with the alias's fully-qualified name (probe-confirmed: real SQL Server uses State 3 for the alias variant, distinct from State 1 used by built-ins; new factory `CannotSpecifyColumnWidthOnAlias` sits alongside the existing `CannotSpecifyColumnWidth`). Nullability inheritance ships at the column-declaration sites (CREATE TABLE column, ALTER TABLE ALTER COLUMN): when the column references an alias and has no explicit `NULL` / `NOT NULL` of its own, the alias's stored `IsNullable` propagates as the default. An explicit column-side marker always wins. ALTER COLUMN's precedence chain is: explicit marker → alias default → preserve existing column nullability. DECLARE @v with a 2-part alias-typed reference falls through from the table-type binding path to the scalar parser when `TryResolveAliasType` matches (cursor restored via checkpoint). DROP TYPE handles both dicts: alias drops remove from `Schema.AliasTypes` (Msg 218 on missing without IF EXISTS; IF EXISTS no-ops a miss); the existing table-type branch including Msg 3732 enforcement is untouched. `sys.types` augmented with alias rows via `EnumerateSysTypes`: `system_type_id` from the underlying built-in (e.g. 56 for int-of-…, 231 for nvarchar-of-…), `user_type_id` from the alias's per-database allocation, `schema_id` from the owning schema, `is_user_defined=1`, `is_table_type=0`, `is_nullable` from the alias's stored marker. 18 new tests in `AliasTypeTests.cs` cover: 4 nullability shapes (NOT NULL inheritance, bare = nullable, explicit NULL = nullable, column-NULL overrides alias-NOT-NULL), unqualified + bracketed-qualified column references, Msg 2716 / 222 / 219 / 218 paths, DROP TYPE happy + missing + IF EXISTS, sys.types row content for both NOT-NULL and bare aliases, DECLARE @v with alias, cross-schema (HR.EmployeeId), and a full AW-style 6-alias smoke test that declares AccountNumber / Flag / Name / NameStyle / OrderNumber / Phone and verifies `is_nullable=0` propagation for the three NOT-NULL aliases on a Customer table. Full suite 4622 → 4640, all green Debug + Release. `docs/claude/bacpac-prerequisites.md` flips the UDDT bullet from `[ ]` to `[x]` with implementation detail and renumbers the order-of-operations sequence (Extended properties is now next). CLAUDE.md gains a sibling Feature-reference index entry next to the existing `CREATE TYPE … AS TABLE` one pointing back to the bacpac-prerequisites doc for the UDDT implementation detail. Two known fidelity gaps explicitly documented + deferred (not load-bearing for the bacpac baseline): `HeapColumn` doesn't retain a back-pointer to its declaring `AliasType` (so `sys.columns.user_type_id` surfaces the underlying built-in's id and `DROP TYPE` on alias doesn't enforce Msg 3732), and `sys.types.max_length` isn't emitted in the catalog view's shipped subset (pre-existing gap from before this bundle).
1 parent c5df9d2 commit 73c1420

19 files changed

Lines changed: 729 additions & 50 deletions

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
147147
- **Touching `#foo` routing, DROP TABLE, TRUNCATE TABLE**[`docs/claude/temp-tables.md`](docs/claude/temp-tables.md).
148148
- **Touching `DECLARE @t TABLE`, table-variable DML routing, `OUTPUT … INTO <target>` (`@t` or regular)**[`docs/claude/table-variables.md`](docs/claude/table-variables.md).
149149
- **Touching `CREATE TYPE … AS TABLE`, `DECLARE @t MyType`, TVP procedure parameters / `READONLY`, or the ADO.NET `DbParameter.TypeName` extension (DataTable / IDataReader as a TVP value source)**[`docs/claude/table-valued-parameters.md`](docs/claude/table-valued-parameters.md).
150+
- **Touching `CREATE TYPE name FROM <builtin> [NULL|NOT NULL]` (scalar alias types / UDDTs), `Schema.AliasTypes` dict, multi-part type-reference parsing (`[schema].[typename]` in column / parameter / `DECLARE @v` / `ALTER COLUMN` / OPENJSON / sp_executesql positions), the `Simulation.ResolveTypeReference` helper, nullability inheritance from the alias default, Msg 222 / 2716 St 3 / 219 enforcement, or `sys.types` alias-type rows**[`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) (the section "UDDTs / alias types" carries the implementation detail; this is one of the bacpac-loader prerequisite bundles).
150151
- **Touching `CREATE SEQUENCE` / `ALTER SEQUENCE` / `DROP SEQUENCE`, `NEXT VALUE FOR`, the per-row dedup mechanism (`BatchContext.CurrentRowStamp` / `SequenceRowCache`), or `sys.sequences`**[`docs/claude/sequences.md`](docs/claude/sequences.md).
151152
- **Touching `CREATE TRIGGER` / `ALTER TRIGGER` / `DROP TRIGGER` / `DISABLE`/`ENABLE TRIGGER`, the `INSERTED` / `DELETED` pseudo-table materialization, the `TriggerFrame` / `FiringTriggerIds` recursion guard, `TRIGGER_NESTLEVEL()`, or `sys.triggers`**[`docs/claude/triggers.md`](docs/claude/triggers.md).
152153
- **Touching `FOREIGN KEY` parsing (`REFERENCES` inline / `FOREIGN KEY (cols) REFERENCES other(cols)` table-level), referential-action wiring (`ON DELETE` / `ON UPDATE` × `NO ACTION` / `CASCADE` / `SET NULL` / `SET DEFAULT`), the FK enforcement loop in `Simulation.ForeignKeys.cs`, the `HeapTable.OutgoingForeignKeys` / `IncomingForeignKeys` lists, cascade-cycle detection (Msg 1785), DROP-TABLE protection (Msg 3726), or `sys.foreign_keys` / `sys.foreign_key_columns`**[`docs/claude/foreign-keys.md`](docs/claude/foreign-keys.md).
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
3+
namespace SqlServerSimulator;
4+
5+
/// <summary>
6+
/// Exercises scalar user-defined alias types (UDDTs) created via
7+
/// <c>CREATE TYPE schema.name FROM &lt;builtin&gt;[(N[, S])] [NULL | NOT NULL]</c>.
8+
/// Alias types are the second bacpac prerequisite (after the database-options
9+
/// expansion); AdventureWorks2025 declares 6 of them (<c>AccountNumber</c>,
10+
/// <c>Flag</c>, <c>Name</c>, <c>NameStyle</c>, <c>OrderNumber</c>,
11+
/// <c>Phone</c>). Behavior probed against SQL Server 2025 (2026-05-14).
12+
/// </summary>
13+
[TestClass]
14+
public class AliasTypeTests
15+
{
16+
[TestMethod]
17+
public void CreateAlias_NotNull_Then_ColumnInheritsNotNullByDefault()
18+
{
19+
// Probe-confirmed: column with no explicit nullability marker inherits
20+
// NOT NULL from the alias.
21+
var ex = new Simulation().AssertSqlError("""
22+
CREATE TYPE dbo.AccountNumber FROM nvarchar(15) NOT NULL;
23+
CREATE TABLE t (c dbo.AccountNumber);
24+
INSERT INTO t (c) VALUES (NULL);
25+
""", 515);
26+
Contains("does not allow nulls", ex.Message);
27+
}
28+
29+
/// <summary>
30+
/// Bare CREATE TYPE (no NULL/NOT NULL marker) → alias is nullable; the
31+
/// column declaration without its own marker stays nullable.
32+
/// </summary>
33+
[TestMethod]
34+
public void CreateAlias_Bare_ColumnIsNullable()
35+
=> AreEqual(1, new Simulation().ExecuteScalar("""
36+
CREATE TYPE dbo.Probe FROM int;
37+
CREATE TABLE t (c dbo.Probe);
38+
INSERT INTO t (c) VALUES (NULL);
39+
SELECT COUNT(*) FROM t WHERE c IS NULL
40+
"""));
41+
42+
[TestMethod]
43+
public void CreateAlias_ExplicitNullKeyword_AliasIsNullable()
44+
=> AreEqual(1, new Simulation().ExecuteScalar("""
45+
CREATE TYPE dbo.Probe FROM int NULL;
46+
CREATE TABLE t (c dbo.Probe);
47+
INSERT INTO t (c) VALUES (NULL);
48+
SELECT COUNT(*) FROM t WHERE c IS NULL
49+
"""));
50+
51+
/// <summary>
52+
/// Column-level explicit NULL overrides alias-defined NOT NULL — probe-
53+
/// confirmed (real SQL Server treats the column-side marker as
54+
/// authoritative when present).
55+
/// </summary>
56+
[TestMethod]
57+
public void ColumnNullOverride_TrumpsAliasNotNull()
58+
=> AreEqual(1, new Simulation().ExecuteScalar("""
59+
CREATE TYPE dbo.Tight FROM int NOT NULL;
60+
CREATE TABLE t (c dbo.Tight NULL);
61+
INSERT INTO t (c) VALUES (NULL);
62+
SELECT COUNT(*) FROM t WHERE c IS NULL
63+
"""));
64+
65+
/// <summary>
66+
/// Probe-confirmed: alias can be referenced without the schema qualifier
67+
/// when it's in dbo (the default schema).
68+
/// </summary>
69+
[TestMethod]
70+
public void UnqualifiedReference_Works()
71+
=> AreEqual(42, new Simulation().ExecuteScalar("""
72+
CREATE TYPE dbo.Probe FROM int;
73+
CREATE TABLE t (c Probe);
74+
INSERT INTO t (c) VALUES (42);
75+
SELECT c FROM t
76+
"""));
77+
78+
[TestMethod]
79+
public void QualifiedReference_Works()
80+
=> AreEqual(42, new Simulation().ExecuteScalar("""
81+
CREATE TYPE dbo.Probe FROM int;
82+
CREATE TABLE t (c [dbo].[Probe]);
83+
INSERT INTO t (c) VALUES (42);
84+
SELECT c FROM t
85+
"""));
86+
87+
[TestMethod]
88+
public void LengthAtUsageSite_RaisesMsg2716()
89+
{
90+
// Probe-confirmed verbatim: Msg 2716 St 3 with the alias's fully-
91+
// qualified name in the message.
92+
var ex = new Simulation().AssertSqlError("""
93+
CREATE TYPE dbo.Name FROM nvarchar(50) NOT NULL;
94+
CREATE TABLE t (c dbo.Name(100));
95+
""", 2716);
96+
Contains("dbo.Name", ex.Message);
97+
Contains("Cannot specify a column width", ex.Message);
98+
}
99+
100+
[TestMethod]
101+
public void DuplicateTypeName_RaisesMsg219()
102+
{
103+
var ex = new Simulation().AssertSqlError("""
104+
CREATE TYPE dbo.Probe FROM int;
105+
CREATE TYPE dbo.Probe FROM int;
106+
""", 219);
107+
Contains("dbo.Probe", ex.Message);
108+
}
109+
110+
[TestMethod]
111+
public void AliasName_CollidesWithTableType_RaisesMsg219()
112+
{
113+
// Alias + table types share one type-name namespace.
114+
var ex = new Simulation().AssertSqlError("""
115+
CREATE TYPE dbo.Shared AS TABLE (id int);
116+
CREATE TYPE dbo.Shared FROM int;
117+
""", 219);
118+
Contains("dbo.Shared", ex.Message);
119+
}
120+
121+
[TestMethod]
122+
public void InvalidBaseType_RaisesMsg222()
123+
{
124+
var ex = new Simulation().AssertSqlError(
125+
"CREATE TYPE dbo.Bogus FROM not_a_type",
126+
222);
127+
Contains("not_a_type", ex.Message);
128+
Contains("not a valid base type", ex.Message);
129+
}
130+
131+
[TestMethod]
132+
public void DropType_OnAlias_Removes()
133+
{
134+
var sim = new Simulation();
135+
_ = sim.ExecuteNonQuery("CREATE TYPE dbo.Probe FROM int");
136+
AreEqual(1, sim.ExecuteScalar(
137+
"SELECT COUNT(*) FROM sys.types WHERE name = 'Probe' AND is_user_defined = 1"));
138+
_ = sim.ExecuteNonQuery("DROP TYPE dbo.Probe");
139+
AreEqual(0, sim.ExecuteScalar(
140+
"SELECT COUNT(*) FROM sys.types WHERE name = 'Probe' AND is_user_defined = 1"));
141+
}
142+
143+
[TestMethod]
144+
public void DropType_OnMissingAlias_RaisesMsg218()
145+
{
146+
_ = new Simulation().AssertSqlError("DROP TYPE dbo.NoSuchAlias", 218);
147+
}
148+
149+
[TestMethod]
150+
public void DropType_IfExists_OnMissingAlias_Succeeds()
151+
=> AreEqual(-1, new Simulation().ExecuteNonQuery("DROP TYPE IF EXISTS dbo.NoSuchAlias"));
152+
153+
[TestMethod]
154+
public void SysTypes_Row_ShipsCorrectShape()
155+
{
156+
var sim = new Simulation();
157+
_ = sim.ExecuteNonQuery("CREATE TYPE dbo.AccountNumber FROM nvarchar(15) NOT NULL");
158+
using var conn = sim.CreateOpenConnection();
159+
using var cmd = conn.CreateCommand(
160+
"SELECT system_type_id, is_user_defined, is_table_type, is_nullable FROM sys.types WHERE name = 'AccountNumber'");
161+
using var reader = cmd.ExecuteReader();
162+
IsTrue(reader.Read());
163+
// nvarchar's system_type_id is 231 per probe.
164+
AreEqual((byte)231, reader.GetByte(0));
165+
IsTrue(reader.GetBoolean(1)); // is_user_defined
166+
IsFalse(reader.GetBoolean(2)); // is_table_type
167+
IsFalse(reader.GetBoolean(3)); // is_nullable (alias was NOT NULL)
168+
}
169+
170+
[TestMethod]
171+
public void SysTypes_BareAlias_IsNullable()
172+
{
173+
var sim = new Simulation();
174+
_ = sim.ExecuteNonQuery("CREATE TYPE dbo.Probe FROM int");
175+
IsTrue((bool)sim.ExecuteScalar(
176+
"SELECT is_nullable FROM sys.types WHERE name = 'Probe'")!);
177+
}
178+
179+
[TestMethod]
180+
public void Declare_AliasTypedVariable_Works()
181+
=> AreEqual(42, new Simulation().ExecuteScalar("""
182+
CREATE TYPE dbo.Probe FROM int;
183+
DECLARE @v dbo.Probe;
184+
SET @v = 42;
185+
SELECT @v
186+
"""));
187+
188+
[TestMethod]
189+
public void AdventureWorksAliasTypes_LoadSuccessfully()
190+
{
191+
// Smoke test for the AW alias-type set — all six should be declarable
192+
// and usable as column types end-to-end.
193+
var sim = new Simulation();
194+
_ = sim.ExecuteNonQuery("""
195+
CREATE TYPE dbo.AccountNumber FROM nvarchar(15) NOT NULL;
196+
CREATE TYPE dbo.Flag FROM bit NOT NULL;
197+
CREATE TYPE dbo.Name FROM nvarchar(50) NOT NULL;
198+
CREATE TYPE dbo.NameStyle FROM bit NOT NULL;
199+
CREATE TYPE dbo.OrderNumber FROM nvarchar(25) NOT NULL;
200+
CREATE TYPE dbo.Phone FROM nvarchar(25);
201+
CREATE TABLE dbo.Customer (
202+
AccountNumber [dbo].[AccountNumber],
203+
Title [dbo].[Name],
204+
NameStyle [dbo].[NameStyle],
205+
Phone [dbo].[Phone]);
206+
INSERT INTO dbo.Customer (AccountNumber, Title, NameStyle, Phone)
207+
VALUES ('AW-001', 'Ms.', 0, '555-1234');
208+
""");
209+
AreEqual(1, sim.ExecuteScalar("SELECT COUNT(*) FROM dbo.Customer"));
210+
// Verify NOT NULL inheritance from alias: AccountNumber, Title,
211+
// NameStyle should all be NOT NULL; Phone is nullable.
212+
AreEqual(3, sim.ExecuteScalar(
213+
"SELECT COUNT(*) FROM sys.columns WHERE object_id = OBJECT_ID('dbo.Customer') AND is_nullable = 0"));
214+
}
215+
216+
[TestMethod]
217+
public void CrossSchema_Alias_Resolves()
218+
{
219+
var sim = new Simulation();
220+
_ = sim.ExecuteNonQuery("""
221+
CREATE SCHEMA HR;
222+
""");
223+
_ = sim.ExecuteNonQuery("""
224+
CREATE TYPE HR.EmployeeId FROM int NOT NULL;
225+
CREATE TABLE HR.Employee (Id HR.EmployeeId);
226+
INSERT INTO HR.Employee (Id) VALUES (1);
227+
""");
228+
AreEqual(1, sim.ExecuteScalar("SELECT Id FROM HR.Employee"));
229+
}
230+
}

SqlServerSimulator/AliasType.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using SqlServerSimulator.Storage;
2+
3+
namespace SqlServerSimulator;
4+
5+
/// <summary>
6+
/// One scalar user-defined alias type (UDDT). Created via
7+
/// <c>CREATE TYPE schema.name FROM &lt;builtin&gt;[(N[, S])] [NULL | NOT NULL]</c>,
8+
/// dropped via <c>DROP TYPE [IF EXISTS] schema.name</c>. Lives in its owning
9+
/// <see cref="Schema"/>'s <see cref="Schema.AliasTypes"/> dict; shares the
10+
/// type-name namespace with <see cref="TableType"/> entries (Msg 219 on
11+
/// cross-kind dup-type-name collision).
12+
/// </summary>
13+
/// <remarks>
14+
/// <para>
15+
/// At resolution time the alias expands to its <see cref="UnderlyingType"/>;
16+
/// the <see cref="DeclaredMaxLength"/> applies to the underlying for
17+
/// length-bearing types (varchar / nvarchar / varbinary / char / nchar /
18+
/// binary / datetime2 / time / datetimeoffset / decimal / numeric).
19+
/// <see cref="IsNullable"/> carries the nullability marker declared on the
20+
/// alias itself (bare and explicit <c>NULL</c> are both nullable;
21+
/// <c>NOT NULL</c> is not). When a column / variable references an alias
22+
/// without an explicit nullability marker of its own, this default
23+
/// propagates — matches probe behavior against SQL Server 2025.
24+
/// </para>
25+
/// <para>
26+
/// Restrictions enforced at usage time (probe-confirmed against SQL Server
27+
/// 2025): a length parameter at the usage site raises Msg 2716 verbatim
28+
/// (<c>"Cannot specify a column width on data type X."</c>).
29+
/// </para>
30+
/// </remarks>
31+
internal sealed class AliasType(
32+
Schema schema,
33+
string name,
34+
SqlType underlyingType,
35+
int? declaredMaxLength,
36+
int? declaredPrecision,
37+
int? declaredScale,
38+
bool isNullable,
39+
int userTypeId,
40+
DateTime createDate)
41+
{
42+
public readonly Schema Schema = schema;
43+
44+
public readonly string Name = name;
45+
46+
/// <summary>
47+
/// The resolved built-in type the alias wraps (e.g. <c>nvarchar(50)</c>'s
48+
/// underlying is the simulator's <c>NVarchar</c> singleton). The fully-
49+
/// resolved <c>SqlType</c> instance — declared length / precision / scale
50+
/// from the CREATE TYPE source are captured in
51+
/// <see cref="DeclaredMaxLength"/> / <see cref="DeclaredPrecision"/> /
52+
/// <see cref="DeclaredScale"/> alongside since the singleton itself is
53+
/// dimension-agnostic for most variable-length types.
54+
/// </summary>
55+
public readonly SqlType UnderlyingType = underlyingType;
56+
57+
/// <summary>
58+
/// Declared length in characters / bytes (for variable-length string /
59+
/// binary types) or the precision (for <c>datetime2</c> / <c>time</c> /
60+
/// <c>datetimeoffset</c> / <c>decimal</c> / <c>numeric</c>'s precision
61+
/// parameter). Null when the underlying is fixed-shape (e.g. <c>int</c>,
62+
/// <c>bit</c>).
63+
/// </summary>
64+
public readonly int? DeclaredMaxLength = declaredMaxLength;
65+
66+
/// <summary>
67+
/// Surfaced as the <c>sys.types.precision</c> column for the alias row.
68+
/// Set from the underlying type's intrinsic precision (e.g. 10 for
69+
/// <c>int</c>, 0 for <c>nvarchar</c>); not the CREATE TYPE
70+
/// numeric-precision argument (that lands in
71+
/// <see cref="DeclaredMaxLength"/> for <c>decimal</c> /
72+
/// <c>numeric</c> / <c>datetime2</c> / <c>time</c> /
73+
/// <c>datetimeoffset</c>).
74+
/// </summary>
75+
public readonly int? DeclaredPrecision = declaredPrecision;
76+
77+
/// <summary>
78+
/// Decimal / numeric scale parameter. Null for non-decimal underlyings.
79+
/// </summary>
80+
public readonly int? DeclaredScale = declaredScale;
81+
82+
/// <summary>
83+
/// Nullability marker from the CREATE TYPE declaration. Bare <c>CREATE
84+
/// TYPE T FROM int</c> and explicit <c>NULL</c> both set this to true;
85+
/// <c>NOT NULL</c> sets false. Used to default a column / variable's
86+
/// nullability when the consumer has no explicit marker — see the
87+
/// nullability-inheritance contract above.
88+
/// </summary>
89+
public readonly bool IsNullable = isNullable;
90+
91+
/// <summary>
92+
/// Per-database <c>user_type_id</c> allocated via
93+
/// <see cref="Database.AllocateUserTypeId"/>. Surfaces in
94+
/// <c>sys.types.user_type_id</c> and <c>sys.columns.user_type_id</c>
95+
/// (for columns whose declared type is this alias).
96+
/// </summary>
97+
public readonly int UserTypeId = userTypeId;
98+
99+
public readonly DateTime CreateDate = createDate;
100+
}

SqlServerSimulator/BuiltInResources.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,27 @@ private static IEnumerable<SqlValue[]> EnumerateSysTypes(Parser.BatchContext bat
870870
];
871871
}
872872
}
873+
// Scalar alias types (UDDTs): probe-confirmed against SQL Server 2025
874+
// — `system_type_id` is the **underlying** built-in's id (e.g. 56 for
875+
// an alias of int, 231 for an alias of nvarchar), `is_user_defined`
876+
// is true, `is_table_type` is false, and `is_nullable` reflects the
877+
// alias-defined NULL/NOT NULL marker from CREATE TYPE.
878+
foreach (var schema in batch.CurrentDatabase.Schemas.Values)
879+
{
880+
var schemaId = SqlValue.FromInt32(schema.SchemaId);
881+
foreach (var alias in schema.AliasTypes.Values.OrderBy(a => a.UserTypeId))
882+
{
883+
yield return [
884+
SqlValue.FromSystemName(alias.Name),
885+
SqlValue.FromByte(alias.UnderlyingType.SystemTypeId),
886+
SqlValue.FromInt32(alias.UserTypeId),
887+
schemaId,
888+
trueBit,
889+
falseBit,
890+
alias.IsNullable ? trueBit : falseBit,
891+
];
892+
}
893+
}
873894
}
874895

875896
private static IEnumerable<SqlValue[]> EnumerateSysTableTypes(Parser.BatchContext batch)

SqlServerSimulator/Errors/SimulatedSqlException.SchemaErrors.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,26 @@ internal static SimulatedSqlException SequenceExhausted(string fullName) =>
196196
internal static SimulatedSqlException TypeAlreadyExists(string fullName) =>
197197
new($"The type '{fullName}' already exists, or you do not have permission to create it.", 219, 16, 1);
198198

199+
/// <summary>
200+
/// Mimics SQL Server error 222: <c>CREATE TYPE name FROM &lt;basetype&gt;</c>
201+
/// referenced a base type that doesn't resolve to a built-in. Probe-
202+
/// confirmed verbatim wording against SQL Server 2025 — the base-type name
203+
/// appears in double-quotes inside the message text.
204+
/// </summary>
205+
internal static SimulatedSqlException InvalidBaseTypeForAlias(string baseTypeName) =>
206+
new($"The base type \"{baseTypeName}\" is not a valid base type for the alias data type.", 222, 16, 1);
207+
208+
/// <summary>
209+
/// Alias-type variant of Msg 2716: an alias-typed column / parameter /
210+
/// variable declaration carries a width specifier (e.g.
211+
/// <c>c dbo.MyAlias(100)</c>). Probe-confirmed State 3 against SQL Server
212+
/// 2025 — distinct from <c>CannotSpecifyColumnWidth</c> (State 1, used
213+
/// for builtin types). The alias's fully-qualified name
214+
/// (<c>schema.name</c>) lands in the message verbatim.
215+
/// </summary>
216+
internal static SimulatedSqlException CannotSpecifyColumnWidthOnAlias(string aliasFullName, int index) =>
217+
new($"Column, parameter, or variable #{index}: Cannot specify a column width on data type {aliasFullName}.", 2716, 16, 3);
218+
199219
/// <summary>
200220
/// Mimics SQL Server error 218: <c>DROP TYPE</c> targeted a name that
201221
/// doesn't exist (suppressed by <c>IF EXISTS</c>). Probe-confirmed

0 commit comments

Comments
 (0)