Skip to content

Commit febeae6

Browse files
committed
Collation metadata round-trip (per-DB + per-column whitelist). WWI's last remaining Skipped category — SqlDatabaseOptions (the Latin1_General_100_CI_AS declaration) — now lands on Database.CollationName and surfaces through every standard catalog (sys.databases.collation_name, DATABASEPROPERTYEX(name, 'Collation'), sys.columns.collation_name, INFORMATION_SCHEMA.COLUMNS.COLLATION_NAME, sys.fn_helpcollations()). WWI now loads with **zero Skipped categories**. Per-column COLLATE name clauses work end-to-end too. **Important non-goal**: comparison / sort / LIKE / = semantics are unchanged — every string op still routes through Collation.Default (SQL_Latin1_General_CP1_CI_AS rules). The declared name is metadata that round-trips through schema import/export; fully modeling per-collation comparison/sort algorithms is a separate, much larger project.
**Storage**: - `Database.CollationName : string` (mutable, default `SQL_Latin1_General_CP1_CI_AS`). - `HeapColumn.Collation : string?` (immutable readonly; null = inherit from database). **Whitelist** (`Collation.cs`): new `internal static readonly FrozenDictionary<string, string> Recognized` seeded with the two collation names we've encountered in AW + WWI (`SQL_Latin1_General_CP1_CI_AS`, `Latin1_General_100_CI_AS`), each carrying its probe-confirmed human description (consumed by `sys.fn_helpcollations`). Case-insensitive lookup via `StringComparer.OrdinalIgnoreCase`. New `IsRecognized(name)` helper. First FrozenDictionary use in the simulator. **Parser**: - `ALTER DATABASE name COLLATE name` (`Simulation.Alter.cs::TryParseAlterDatabaseCollate`) — previously hard-errored on anything other than the default; now validates against the whitelist, stores on `Database.CollationName`, raises `NotSupportedException` on unrecognized names. - CREATE TABLE column-level `COLLATE name` (`Simulation.Create.cs::ParseOneColumnIntoLists`) — new switch arm in the column-attribute loop accepts the clause after the type position, validates against the whitelist, threads the parsed name to the `HeapColumn` ctor. **Catalog surface**: - New `sys.databases` view (9-column subset that tooling actually queries: name / database_id / compatibility_level / collation_name / snapshot_isolation_state / state, plus a single row for the current database — multi-database expansion comes when `USE <db>` lands). - New `sys.fn_helpcollations()` TVF (`name sysname, description nvarchar(1000)`) — emits one row per `Collation.Recognized` entry. Real SQL Server emits ~5540 rows here; the simulator's shorter list is honest about which names round-trip. - New `DATABASEPROPERTYEX(db_name, property_name)` built-in scalar in length-18 dispatch — handles the 9 properties common tooling queries (`Collation`, `Status`, `UserAccess`, `IsAutoClose`, `IsAutoShrink`, `Recovery`, `SnapshotIsolationState`, `IsReadCommittedSnapshotOn`, `Version`). NULL db / NULL property / unknown db / unknown property all return NULL (probe-confirmed against SQL Server 2025). - `sys.columns.collation_name` (`BuiltInResources.EnumerateColumns`) — replaced the captured `defaultCollation` SqlValue with a per-row `CollationFor(col)` helper that consults per-column override → DB default → NULL for non-string types. Same change cascades through INFORMATION_SCHEMA.COLUMNS. **Loader** (`Storage/Bacpac/ModelXmlReader.cs`): - `EmitDatabaseOptions` now emits `ALTER DATABASE [simulated] COLLATE name` when the bacpac's `Collation` property is on the whitelist; non-whitelisted names land on `BacpacLoadResult.Warnings` (graceful degradation matches the loader's best-effort contract). - `TranslateSimpleColumn` reads the bacpac column's `Collation` property and emits a `COLLATE name` clause in the generated CREATE TABLE column DDL when recognized; non-whitelisted names land on Warnings with the clause dropped (column inherits the database default). **Test additions**: - `SqlServerSimulator.Tests/CollationMetadataTests.cs` (new, 15 tests): DB collation default + ALTER round-trip via sys.databases + DATABASEPROPERTYEX; column-level COLLATE round-trip via sys.columns + INFORMATION_SCHEMA; per-column inheritance from DB; non-string types return NULL collation_name; column-level unrecognized COLLATE → NotSupportedException; DATABASEPROPERTYEX unknown property / unknown db / NULL args; sys.fn_helpcollations row shape; sys.databases row shape. - `BacpacLoaderTests.Load_WWI_Database_Collation_Round_Trips` confirms the WWI declaration lands on both `sys.databases.collation_name` and `DATABASEPROPERTYEX`. - `Load_WWI_Known_Gaps_Recorded_In_Skipped` now asserts `IsFalse(grouped.ContainsKey("SqlDatabaseOptions"))` — zero Skipped categories remaining. - Existing `Collate_NonDefault_RaisesNotSupported` adjusted to assert the new "recognized list" wording instead of the old "only SQL_Latin1_General_CP1_CI_AS" form. **Doc updates**: - `docs/claude/bacpac-prerequisites.md` step 11 captures the bundle in full (storage + whitelist + parser + catalog + loader + non-goal); the WWI gap inventory in step 14 strikes through the `SqlDatabaseOptions` entry; the Status section reads "Zero Skipped categories remain" for WWI. - `CLAUDE.md` deep-dive pointer updated: WWI now lists zero Skipped categories with an explicit note on the metadata-vs-semantics divergence.
1 parent 6ac3196 commit febeae6

14 files changed

Lines changed: 481 additions & 40 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
164164
- **Touching `XmlSqlType` (singleton in `Storage/XmlType.cs`, SystemTypeId=241, IsLob=true), `XmlSchemaCollection` storage class + `Schema.XmlSchemaCollections` dict + `Database.AllocateXmlCollectionId` (seeds at 65536 per probe), `HeapColumn.XmlSchemaCollection` per-column binding, the `xml`-recognizing arms of `SqlType.ResolveSimpleKeyword` / `Precedence` / `SystemTypeId`, the `xml(name)` / `xml(CONTENT name)` / `xml(DOCUMENT name)` column-type parsing in `ParseOneColumnIntoLists` (via `PeekIsXmlSchemaArgument` / `ParseXmlSchemaCollectionArgument`), `Simulation.Xml.cs` partial (CREATE XML SCHEMA COLLECTION / DROP XML SCHEMA COLLECTION / CREATE [PRIMARY] XML INDEX / FOR PATH|VALUE|PROPERTY secondary form), the `Xml` contextual keyword + `Keyword.Primary` dispatch in `TryParseCreate`, `XmlIndex` / `XmlSecondaryIndexType` storage on `HeapTable.XmlIndexes`, the `XmlMethodCall` expression in `Parser/Expressions/XmlMethodCall.cs` (closed accept-list of `value` / `nodes` / `query` / `exist` / `modify`, parses cleanly, throws `NotSupportedException` at `Run`), or the `sys.xml_schema_collections` / `sys.xml_indexes` catalog views** → [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) (the "`xml` data type + XML schema collections + XML methods + XML indexes" section carries the implementation detail).
165165
- **Touching `GeographySqlType` / `GeometrySqlType` (singletons in `Storage/SpatialType.cs`, both inherit `SpatialSqlType : SqlType(SqlTypeCategory.String)`, SystemTypeId=240, UserTypeId=130/129, IsLob=true), `SqlValue.FromGeography` / `FromGeometry` factories + the spatial branches in `FromString`, the `geography` / `geometry`-recognizing arms of `SqlType.ResolveSimpleKeyword` / `Precedence` / `SystemTypeId` / `UserTypeId`, the `SpatialMethodCall` expression in `Parser/Expressions/SpatialMethodCall.cs` (broad ~70-name accept-list of OGC + Microsoft-extension methods; parses cleanly + throws `NotSupportedException` at `Run` except `.ToString()` returns the stored WKT), the `SpatialStaticCall` expression in `Parser/Expressions/SpatialStaticCall.cs` (`geography::Parse(wkt)` / `geography::STGeomFromText(wkt, srid)` / `geometry::Point(x, y, srid)` work; other static methods throw), the `::` dispatch for `geography`/`geometry` in `Expression.cs` alongside `hierarchyid::`, the `.ToString()` polymorphic delegation in `HierarchyIdMethodCall.Run` (spatial receivers return WKT through the spatial path), the `SpatialIndex` storage class + `SpatialIndexKind` enum on `HeapTable.SpatialIndexes`, the `Simulation.Spatial.cs` partial (CREATE SPATIAL INDEX with USING / BOUNDING_BOX / GRIDS / CELLS_PER_OBJECT parsing + default tessellation_scheme), the `Spatial` contextual keyword routing in `TryParseCreate`, or the `sys.spatial_indexes` (23-col probe-confirmed) / `sys.spatial_index_tessellations` (16-col probe-confirmed) / `sys.spatial_reference_systems` (6-col, empty seed) catalog views** → [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) (the "`geography` / `geometry` data types" section carries the implementation detail).
166166
- **Adding a new top-level statement parser or changing the dispatch loop's statement-separator rules**[`docs/claude/grammar.md`](docs/claude/grammar.md) + [`docs/claude/control-flow.md`](docs/claude/control-flow.md).
167-
- **Working on BACPAC import (`Simulation.FromBacpac(string, out BacpacLoadResult)` + Stream overload, internal until externally consumed) — the model.xml dispatcher in `SqlServerSimulator/Storage/Bacpac/ModelXmlReader.cs` (9 phases: DB options + schemas + UDDTs + **sequences + roles + table types + filegroups (skip)** → tables → constraints → FKs → (unused) → views → programmable objects → **deferred computed columns + indexes** → extended properties → BCP data), the `BacpacReader` + `BcpRowReader` + `HierarchyIdWireDecoder` data path, the `BacpacLoadResult.Skipped` + `ElementCounts` + `Warnings` + `TableColumnIsAlias` diagnostics carriers, the resilient-loader contract (per-element exceptions land on Skipped with `"Load failed: …"` prefix and don't abort the load; computed-column failures use `"Deferred: …"` prefix so the AW guard stays meaningful), the **BCP-filters-computed-columns contract** (`BacpacReader.LoadRowsFromBcp` strips columns with `HeapColumn.Computed != null` before passing to `BcpRowReader`/`RowEncoder` — DACFx-emitted BCP files exclude computed columns from the wire layout regardless of PERSISTED; the loader emits computed columns *without* the PERSISTED qualifier so the simulator recomputes on every read), the wire-format matrix (fixed-raw / 1-byte-prefix / 2-byte-prefix / 8-byte-prefix tiers — see [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) "BCP wire format" section)** → [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md). Loader baseline ships (2026-05-15) with AdventureWorks2025 at **100% row coverage** (760,167 / 760,167; zero BCP-file failures); WideWorldImporters-Standard loads end-to-end with **48/48 tables + 26/26 sequences + 9/9 roles + 4/4 table types + 8/8 computed columns + 42/42 procedures + 94/94 indexes + 3/3 views + 1/1 scalar function + 4.7M rows**, remaining Skipped categories cataloged in `Load_WWI_Known_Gaps_Recorded_In_Skipped` (1 SqlDatabaseOptions-Collation — the last category not feature-modeled). MAX types (varchar(MAX) / nvarchar(MAX) / varbinary(MAX)) + xml + CLR-UDT family all use the simple inline 8-byte-prefix wire shape (NOT TDS-PLP chunked form); plain `bit` uses 1-byte length prefix regardless of nullability (probe-confirmed via Production.Document.FolderFlag, the sole non-UDDT bit column in AW). Hierarchyid wire decoder covers AW's [0..79] positive-ordinal envelope via a 4-prefix order-preserving prefix code (`01` / `100` / `101` / `110`); negative ordinals + ordinals ≥ 80 + dotted sub-ordinals raise `NotSupportedException` for a follow-up bundle to extend. Geography rows load with `SpatialLocation = NULL` (WKB→WKT decoding deferred).
167+
- **Working on BACPAC import (`Simulation.FromBacpac(string, out BacpacLoadResult)` + Stream overload, internal until externally consumed) — the model.xml dispatcher in `SqlServerSimulator/Storage/Bacpac/ModelXmlReader.cs` (9 phases: DB options + schemas + UDDTs + **sequences + roles + table types + filegroups (skip)** → tables → constraints → FKs → (unused) → views → programmable objects → **deferred computed columns + indexes** → extended properties → BCP data), the `BacpacReader` + `BcpRowReader` + `HierarchyIdWireDecoder` data path, the `BacpacLoadResult.Skipped` + `ElementCounts` + `Warnings` + `TableColumnIsAlias` diagnostics carriers, the resilient-loader contract (per-element exceptions land on Skipped with `"Load failed: …"` prefix and don't abort the load; computed-column failures use `"Deferred: …"` prefix so the AW guard stays meaningful), the **BCP-filters-computed-columns contract** (`BacpacReader.LoadRowsFromBcp` strips columns with `HeapColumn.Computed != null` before passing to `BcpRowReader`/`RowEncoder` — DACFx-emitted BCP files exclude computed columns from the wire layout regardless of PERSISTED; the loader emits computed columns *without* the PERSISTED qualifier so the simulator recomputes on every read), the wire-format matrix (fixed-raw / 1-byte-prefix / 2-byte-prefix / 8-byte-prefix tiers — see [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md) "BCP wire format" section)** → [`docs/claude/bacpac-prerequisites.md`](docs/claude/bacpac-prerequisites.md). Loader baseline ships (2026-05-15) with AdventureWorks2025 at **100% row coverage** (760,167 / 760,167; zero BCP-file failures); WideWorldImporters-Standard loads end-to-end with **48/48 tables + 26/26 sequences + 9/9 roles + 4/4 table types + 8/8 computed columns + 42/42 procedures + 94/94 indexes + 3/3 views + 1/1 scalar function + 4.7M rows**, zero remaining Skipped categories — every WWI element type loads (collation metadata stored as declaration via `Database.CollationName`, though comparison semantics still route through the default collation per `Collation.Default`). MAX types (varchar(MAX) / nvarchar(MAX) / varbinary(MAX)) + xml + CLR-UDT family all use the simple inline 8-byte-prefix wire shape (NOT TDS-PLP chunked form); plain `bit` uses 1-byte length prefix regardless of nullability (probe-confirmed via Production.Document.FolderFlag, the sole non-UDDT bit column in AW). Hierarchyid wire decoder covers AW's [0..79] positive-ordinal envelope via a 4-prefix order-preserving prefix code (`01` / `100` / `101` / `110`); negative ordinals + ordinals ≥ 80 + dotted sub-ordinals raise `NotSupportedException` for a follow-up bundle to extend. Geography rows load with `SpatialLocation = NULL` (WKB→WKT decoding deferred).
168168

169169
## Not modeled
170170

SqlServerSimulator.Tests.Internal/Storage/BacpacLoaderTests.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ public void Load_WWI_Known_Gaps_Recorded_In_Skipped()
616616
IsFalse(grouped.ContainsKey("SqlCheckConstraint"), "Paren-wrapped value LHS in boolean parser landed; WWI's CK_Sales_SpecialDeals_Exactly_One_NOT_NULL_Pricing_Option_Is_Required loads.");
617617
IsFalse(grouped.ContainsKey("SqlPermissionStatement"), "SqlPermissionStatement dispatcher entry landed; the 2 encryption-key VIEW grants emit through the GRANT parser.");
618618
IsFalse(grouped.ContainsKey("SqlExtendedProperty"), "Extended-property host-routing landed for SqlIndexBase + SqlConstraint; all 414 WWI extended properties load.");
619-
AreEqual(1, grouped["SqlDatabaseOptions"], "Non-default collation deferred.");
619+
IsFalse(grouped.ContainsKey("SqlDatabaseOptions"), "Collation metadata round-trip landed; Latin1_General_100_CI_AS recognized and stored on Database.CollationName.");
620620
}
621621

622622
[TestMethod]
@@ -635,6 +635,30 @@ public void Load_WWI_Computed_Columns_Land_With_is_computed_Set()
635635
AreEqual(8, reader.GetInt32(0));
636636
}
637637

638+
[TestMethod]
639+
public void Load_WWI_Database_Collation_Round_Trips()
640+
{
641+
// WWI declares Latin1_General_100_CI_AS in its SqlDatabaseOptions
642+
// element. The 2026-05-16 collation metadata bundle stores that on
643+
// Database.CollationName and surfaces it through both sys.databases
644+
// and DATABASEPROPERTYEX. Comparison semantics still route through
645+
// the simulator's default collation — the metadata is honest about
646+
// the declaration without claiming full fidelity.
647+
var simulation = LoadWideWorldImporters(out _);
648+
using var connection = (SimulatedDbConnection)simulation.CreateDbConnection();
649+
connection.Open();
650+
using (var command = connection.CreateCommand())
651+
{
652+
command.CommandText = "SELECT collation_name FROM sys.databases";
653+
AreEqual("Latin1_General_100_CI_AS", command.ExecuteScalar());
654+
}
655+
using (var command = connection.CreateCommand())
656+
{
657+
command.CommandText = "SELECT DATABASEPROPERTYEX('simulated', 'Collation')";
658+
AreEqual("Latin1_General_100_CI_AS", command.ExecuteScalar());
659+
}
660+
}
661+
638662
[TestMethod]
639663
public void Load_WWI_Extended_Properties_Cover_Index_And_Constraint_Hosts()
640664
{

SqlServerSimulator.Tests/AlterDatabaseOptionsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public void Collate_NonDefault_RaisesNotSupported()
104104
var ex = Throws<NotSupportedException>(() =>
105105
new Simulation().ExecuteNonQuery("ALTER DATABASE claude COLLATE Japanese_CI_AS"));
106106
Contains("Japanese_CI_AS", ex.Message);
107-
Contains("SQL_Latin1_General_CP1_CI_AS", ex.Message);
107+
Contains("recognized list", ex.Message);
108108
}
109109

110110
[TestMethod]
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
3+
namespace SqlServerSimulator;
4+
5+
/// <summary>
6+
/// Exercises the collation metadata surface — per-database
7+
/// (<c>ALTER DATABASE … COLLATE name</c>, <c>sys.databases.collation_name</c>,
8+
/// <c>DATABASEPROPERTYEX</c>), per-column (<c>CREATE TABLE col TYPE COLLATE name</c>,
9+
/// <c>sys.columns.collation_name</c>, <c>INFORMATION_SCHEMA.COLUMNS.COLLATION_NAME</c>),
10+
/// the recognized-collation whitelist (<c>sys.fn_helpcollations</c>), and the
11+
/// failure path for unrecognized names. Comparison / sort / LIKE still route
12+
/// through the simulator's default collation regardless of declared metadata —
13+
/// these tests cover the round-trip, not the semantic divergence (the
14+
/// fidelity gap is documented in <c>docs/claude/bacpac-prerequisites.md</c>
15+
/// step 13 collation entry).
16+
/// </summary>
17+
[TestClass]
18+
public sealed class CollationMetadataTests
19+
{
20+
[TestMethod]
21+
public void DefaultDatabase_CollationName_IsDefault()
22+
=> AreEqual("SQL_Latin1_General_CP1_CI_AS", new Simulation().ExecuteScalar(
23+
"SELECT collation_name FROM sys.databases"));
24+
25+
[TestMethod]
26+
public void AlterDatabase_Collate_RecognizedName_RoundTripsViaSysDatabases()
27+
{
28+
var sim = new Simulation();
29+
_ = sim.ExecuteNonQuery("ALTER DATABASE simulated COLLATE Latin1_General_100_CI_AS");
30+
AreEqual("Latin1_General_100_CI_AS", sim.ExecuteScalar(
31+
"SELECT collation_name FROM sys.databases"));
32+
}
33+
34+
[TestMethod]
35+
public void AlterDatabase_Collate_RoundTripsViaDatabasePropertyEx()
36+
{
37+
var sim = new Simulation();
38+
_ = sim.ExecuteNonQuery("ALTER DATABASE simulated COLLATE Latin1_General_100_CI_AS");
39+
AreEqual("Latin1_General_100_CI_AS", sim.ExecuteScalar(
40+
"SELECT DATABASEPROPERTYEX('simulated', 'Collation')"));
41+
}
42+
43+
[TestMethod]
44+
public void DatabasePropertyEx_UnknownProperty_ReturnsNull()
45+
=> AreEqual(DBNull.Value, new Simulation().ExecuteScalar(
46+
"SELECT DATABASEPROPERTYEX('simulated', 'NotARealProperty')"));
47+
48+
[TestMethod]
49+
public void DatabasePropertyEx_UnknownDatabase_ReturnsNull()
50+
=> AreEqual(DBNull.Value, new Simulation().ExecuteScalar(
51+
"SELECT DATABASEPROPERTYEX('no_such_db', 'Collation')"));
52+
53+
[TestMethod]
54+
public void DatabasePropertyEx_NullArgs_ReturnNull()
55+
{
56+
var sim = new Simulation();
57+
AreEqual(DBNull.Value, sim.ExecuteScalar("SELECT DATABASEPROPERTYEX(NULL, 'Collation')"));
58+
AreEqual(DBNull.Value, sim.ExecuteScalar("SELECT DATABASEPROPERTYEX('simulated', NULL)"));
59+
}
60+
61+
[TestMethod]
62+
public void Column_NoCollate_InheritsDatabaseDefault()
63+
{
64+
var sim = new Simulation();
65+
_ = sim.ExecuteNonQuery("CREATE TABLE t (name nvarchar(50))");
66+
AreEqual("SQL_Latin1_General_CP1_CI_AS", sim.ExecuteScalar(
67+
"SELECT collation_name FROM sys.columns WHERE name = 'name'"));
68+
}
69+
70+
[TestMethod]
71+
public void Column_NoCollate_FollowsDatabaseAfterAlter()
72+
{
73+
var sim = new Simulation();
74+
_ = sim.ExecuteNonQuery("""
75+
ALTER DATABASE simulated COLLATE Latin1_General_100_CI_AS;
76+
CREATE TABLE t (name nvarchar(50));
77+
""");
78+
// Per-column collation_name reports the db default when no override.
79+
AreEqual("Latin1_General_100_CI_AS", sim.ExecuteScalar(
80+
"SELECT collation_name FROM sys.columns WHERE name = 'name'"));
81+
}
82+
83+
[TestMethod]
84+
public void Column_WithCollate_RoundTrips()
85+
{
86+
var sim = new Simulation();
87+
_ = sim.ExecuteNonQuery("CREATE TABLE t (a nvarchar(50), b nvarchar(50) COLLATE Latin1_General_100_CI_AS)");
88+
// Column a inherits db default; column b carries the override.
89+
AreEqual("SQL_Latin1_General_CP1_CI_AS", sim.ExecuteScalar(
90+
"SELECT collation_name FROM sys.columns WHERE name = 'a'"));
91+
AreEqual("Latin1_General_100_CI_AS", sim.ExecuteScalar(
92+
"SELECT collation_name FROM sys.columns WHERE name = 'b'"));
93+
}
94+
95+
[TestMethod]
96+
public void Column_NonStringType_CollationNameIsNull()
97+
{
98+
var sim = new Simulation();
99+
_ = sim.ExecuteNonQuery("CREATE TABLE t (i int, n nvarchar(50), d datetime, b bit)");
100+
AreEqual(DBNull.Value, sim.ExecuteScalar("SELECT collation_name FROM sys.columns WHERE name = 'i'"));
101+
AreEqual("SQL_Latin1_General_CP1_CI_AS", sim.ExecuteScalar(
102+
"SELECT collation_name FROM sys.columns WHERE name = 'n'"));
103+
AreEqual(DBNull.Value, sim.ExecuteScalar("SELECT collation_name FROM sys.columns WHERE name = 'd'"));
104+
AreEqual(DBNull.Value, sim.ExecuteScalar("SELECT collation_name FROM sys.columns WHERE name = 'b'"));
105+
}
106+
107+
[TestMethod]
108+
public void Column_WithCollate_RoundTripsViaInformationSchema()
109+
{
110+
var sim = new Simulation();
111+
_ = sim.ExecuteNonQuery("CREATE TABLE t (b nvarchar(50) COLLATE Latin1_General_100_CI_AS)");
112+
AreEqual("Latin1_General_100_CI_AS", sim.ExecuteScalar(
113+
"SELECT COLLATION_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE COLUMN_NAME = 'b'"));
114+
}
115+
116+
[TestMethod]
117+
public void Column_Collate_UnrecognizedName_RaisesNotSupported()
118+
{
119+
var ex = Throws<NotSupportedException>(() => new Simulation().ExecuteNonQuery(
120+
"CREATE TABLE t (a nvarchar(50) COLLATE Japanese_CI_AS)"));
121+
Contains("Japanese_CI_AS", ex.Message);
122+
Contains("recognized list", ex.Message);
123+
}
124+
125+
[TestMethod]
126+
public void FnHelpCollations_ListsRecognized()
127+
=> AreEqual(2, new Simulation().ExecuteScalar(
128+
"SELECT COUNT(*) FROM sys.fn_helpcollations()"));
129+
130+
[TestMethod]
131+
public void FnHelpCollations_ColumnsAreNameAndDescription()
132+
{
133+
// The view's row shape mirrors real SQL Server's
134+
// (name sysname NULL, description nvarchar(1000) NULL).
135+
var sim = new Simulation();
136+
AreEqual("Latin1_General_100_CI_AS", sim.ExecuteScalar(
137+
"SELECT name FROM sys.fn_helpcollations() WHERE name = 'Latin1_General_100_CI_AS'"));
138+
}
139+
140+
[TestMethod]
141+
public void SysDatabases_RowShape_CarriesCompatibilityAndIsolation()
142+
{
143+
var sim = new Simulation();
144+
AreEqual("simulated", sim.ExecuteScalar("SELECT name FROM sys.databases"));
145+
AreEqual((short)1, sim.ExecuteScalar("SELECT database_id FROM sys.databases"));
146+
AreEqual("ONLINE", sim.ExecuteScalar("SELECT state_desc FROM sys.databases"));
147+
}
148+
}

0 commit comments

Comments
 (0)