Skip to content

Commit 062ee0e

Browse files
committed
Recognized collation catalog grew from 12 to 27 (+ Korean / Greek / Cyrillic / German PhoneBook / French / Modern Spanish in both pre-v100 and v100 forms, + the Latin1 _100_*_UTF8 trio). New locales reuse the CultureCollation comparer body; UTF-8 variants share the existing Latin1 bodies (UTF-8 is storage-encoding-only at the simulator's UTF-16 value layer). Probed sort-parity for the three previously-shipped locale comparers against SQL Server 2025 and documented the divergence shape (equality / kana-folding / Turkish-i align; secondary sort tiebreakers within equivalence classes don't — varchar especially diverges where the underlying codepage differs). New "Locale-comparer sort-parity gap" section in docs/claude/collations.md captures the per-locale findings; the broader-locale-catalog gap removed.
1 parent 7d31854 commit 062ee0e

3 files changed

Lines changed: 186 additions & 13 deletions

File tree

SqlServerSimulator.Tests/CollationMetadataTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public void Column_Collate_UnrecognizedName_RaisesNotSupported()
125125

126126
[TestMethod]
127127
public void FnHelpCollations_ListsRecognized()
128-
=> AreEqual(12, new Simulation().ExecuteScalar(
128+
=> AreEqual(27, new Simulation().ExecuteScalar(
129129
"SELECT COUNT(*) FROM sys.fn_helpcollations()"));
130130

131131
[TestMethod]

SqlServerSimulator/Collation.cs

Lines changed: 145 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,24 +125,35 @@ private protected Collation()
125125
/// Comparison routes through .NET's <c>ja-JP</c> <see cref="CompareInfo"/>
126126
/// with the simulator's standard CI/AS options (case-insensitive,
127127
/// accent-sensitive, kana-type-insensitive, width-insensitive).
128-
/// Probe-confirmed presence in real-database column-collation profiles;
129-
/// per-name sort-order parity with real SQL Server is not yet probed.
128+
/// Equality + kana-type / width folding match SQL Server end-to-end;
129+
/// the secondary sort tiebreaker inside the hiragana / full-width
130+
/// katakana / half-width katakana equivalence classes diverges
131+
/// (probe-confirmed against SQL Server 2025: ~half the positions
132+
/// reorder on nvarchar; varchar essentially unusable since CP1252
133+
/// can't represent Japanese where real SQL Server uses CP932). See
134+
/// <c>docs/claude/collations.md</c> "Locale-comparer sort-parity gap".
130135
/// </summary>
131136
internal static readonly CultureCollation JapaneseXJIS140CiAs = new("Japanese_XJIS_140_CI_AS", "ja-JP", caseSensitive: false);
132137

133138
/// <summary>
134139
/// "Chinese_PRC_CI_AS" — Simplified Chinese (pinyin sort) via .NET's
135-
/// <c>zh-CN</c> <see cref="CompareInfo"/>. Per-name sort-order parity
136-
/// with real SQL Server is not yet probed.
140+
/// <c>zh-CN</c> <see cref="CompareInfo"/>. Internal pinyin ordering
141+
/// mostly aligns with SQL Server 2025; the .NET vs SQL Server
142+
/// convention for Latin-vs-CJK block position is reversed (.NET puts
143+
/// CJK before Latin, SQL Server puts Latin before CJK), so any
144+
/// mixed-script ORDER BY will shift every position. See
145+
/// <c>docs/claude/collations.md</c> "Locale-comparer sort-parity gap".
137146
/// </summary>
138147
internal static readonly CultureCollation ChinesePrcCiAs = new("Chinese_PRC_CI_AS", "zh-CN", caseSensitive: false);
139148

140149
/// <summary>
141150
/// "Turkish_CI_AS" — Turkish collation via .NET's <c>tr-TR</c>
142151
/// <see cref="CompareInfo"/>. Notably handles the i / İ / ı / I
143152
/// folding that catches non-Turkish-aware code (the "Turkish-i
144-
/// problem"). Per-name sort-order parity with real SQL Server is not
145-
/// yet probed.
153+
/// problem"). Equality / case-folding match SQL Server end-to-end on
154+
/// nvarchar; tiebreaker within case-equivalence classes (e.g. `çay`
155+
/// vs `Çay`) differs (~2 / 19 position drift on probed inputs). See
156+
/// <c>docs/claude/collations.md</c> "Locale-comparer sort-parity gap".
146157
/// </summary>
147158
internal static readonly CultureCollation TurkishCiAs = new("Turkish_CI_AS", "tr-TR", caseSensitive: false);
148159

@@ -180,6 +191,104 @@ internal sealed class UNICODE_CODEPOINT : BinaryCollation
180191
public override string Name => "UNICODE_CODEPOINT";
181192
}
182193

194+
/// <summary>"Korean_100_CI_AS" — Korean (Hangul) v100 sort via .NET's
195+
/// <c>ko-KR</c> <see cref="CompareInfo"/>. Same sort-tiebreaker
196+
/// divergence caveat as the other locale collations
197+
/// (see <c>docs/claude/collations.md</c>).</summary>
198+
internal static readonly CultureCollation Korean100CiAs = new("Korean_100_CI_AS", "ko-KR", caseSensitive: false);
199+
200+
/// <summary>"Korean_Wansung_CI_AS" — legacy Korean Wansung code-page
201+
/// binding. Behavior body identical to <see cref="Korean100CiAs"/> at
202+
/// the simulator's value layer; the Wansung-vs-v100 distinction is a
203+
/// non-Unicode codepage detail that the simulator's UTF-16 storage
204+
/// doesn't materialize.</summary>
205+
internal static readonly CultureCollation KoreanWansungCiAs = new("Korean_Wansung_CI_AS", "ko-KR", caseSensitive: false);
206+
207+
/// <summary>"Greek_CI_AS" — Greek collation via .NET's <c>el-GR</c>
208+
/// <see cref="CompareInfo"/>. Tonos / dialytika fold under accent-
209+
/// sensitive rules; final-sigma (ς) vs medial-sigma (σ) treated as
210+
/// case-insensitive peers (matches real SQL Server).</summary>
211+
internal static readonly CultureCollation GreekCiAs = new("Greek_CI_AS", "el-GR", caseSensitive: false);
212+
213+
/// <summary>"Greek_100_CI_AS" — v100 Greek collation. Same culture as
214+
/// <see cref="GreekCiAs"/>; the v100 update touches Unicode-table
215+
/// ordering for supplementary characters not relevant to most Greek
216+
/// text.</summary>
217+
internal static readonly CultureCollation Greek100CiAs = new("Greek_100_CI_AS", "el-GR", caseSensitive: false);
218+
219+
/// <summary>"Cyrillic_General_CI_AS" — pan-Cyrillic collation routed
220+
/// through .NET's <c>ru-RU</c> <see cref="CompareInfo"/>. Covers
221+
/// Russian, Ukrainian, Bulgarian, Serbian, etc. at the same fidelity
222+
/// bar as the other locale collations — equality / case folding align;
223+
/// secondary sort tiebreakers may differ.</summary>
224+
internal static readonly CultureCollation CyrillicGeneralCiAs = new("Cyrillic_General_CI_AS", "ru-RU", caseSensitive: false);
225+
226+
/// <summary>"Cyrillic_General_100_CI_AS" — v100 Cyrillic. Same culture
227+
/// as <see cref="CyrillicGeneralCiAs"/>.</summary>
228+
internal static readonly CultureCollation CyrillicGeneral100CiAs = new("Cyrillic_General_100_CI_AS", "ru-RU", caseSensitive: false);
229+
230+
/// <summary>"German_PhoneBook_CI_AS" — German with phonebook sort
231+
/// (ä → ae, ö → oe, ü → ue, ß → ss equivalence at sort time). The
232+
/// simulator routes through .NET's <c>de-DE</c> default ordering
233+
/// (umlaut-as-letter, not phonebook). Recognized for BACPAC quiet-
234+
/// loading; sort order for the umlauted letters diverges from real
235+
/// SQL Server. Apps that rely on phonebook ordering hit the broader
236+
/// locale-comparer sort-parity gap documented in collations.md.</summary>
237+
internal static readonly CultureCollation GermanPhoneBookCiAs = new("German_PhoneBook_CI_AS", "de-DE", caseSensitive: false);
238+
239+
/// <summary>"German_PhoneBook_100_CI_AS" — v100 German phonebook.
240+
/// Same routing as <see cref="GermanPhoneBookCiAs"/>; same phonebook
241+
/// divergence applies.</summary>
242+
internal static readonly CultureCollation GermanPhoneBook100CiAs = new("German_PhoneBook_100_CI_AS", "de-DE", caseSensitive: false);
243+
244+
/// <summary>"French_CI_AS" — French via .NET's <c>fr-FR</c>
245+
/// <see cref="CompareInfo"/>. Note: real SQL Server's French
246+
/// collation sorts accents from the END of the string (a French-
247+
/// specific rule); .NET's <c>fr-FR</c> default doesn't, so accented
248+
/// strings near each other sort differently. Same fidelity bar as
249+
/// the other locale collations.</summary>
250+
internal static readonly CultureCollation FrenchCiAs = new("French_CI_AS", "fr-FR", caseSensitive: false);
251+
252+
/// <summary>"French_100_CI_AS" — v100 French.</summary>
253+
internal static readonly CultureCollation French100CiAs = new("French_100_CI_AS", "fr-FR", caseSensitive: false);
254+
255+
/// <summary>"Modern_Spanish_CI_AS" — Spanish (modern, no ch/ll as
256+
/// separate letters) via .NET's <c>es-ES</c> <see cref="CompareInfo"/>.
257+
/// .NET's default Spanish sort already follows the modern convention,
258+
/// so equality / sort alignment is closer here than for the other
259+
/// European locales.</summary>
260+
internal static readonly CultureCollation ModernSpanishCiAs = new("Modern_Spanish_CI_AS", "es-ES", caseSensitive: false);
261+
262+
/// <summary>"Modern_Spanish_100_CI_AS" — v100 modern Spanish.</summary>
263+
internal static readonly CultureCollation ModernSpanish100CiAs = new("Modern_Spanish_100_CI_AS", "es-ES", caseSensitive: false);
264+
265+
/// <summary>"Latin1_General_100_CI_AS_SC_UTF8" — Latin1 v100 CI_AS
266+
/// with supplementary-character support and UTF-8 varchar storage.
267+
/// UTF-8 is a storage encoding only; sort / compare semantics are
268+
/// identical to <see cref="Latin1General100CiAs"/>. .NET's
269+
/// <see cref="CompareInfo"/> handles surrogate pairs natively, so the
270+
/// SC marker doesn't require special handling either.</summary>
271+
internal static readonly CultureCollation Latin1General100CiAsScUtf8 = new(
272+
"Latin1_General_100_CI_AS_SC_UTF8", CultureInfo.InvariantCulture.Name, caseSensitive: false);
273+
274+
/// <summary>"Latin1_General_100_CS_AS_SC_UTF8" — case-sensitive UTF-8
275+
/// variant. Sort / compare matches <see cref="Latin1GeneralCsAs"/>;
276+
/// UTF-8 / SC distinctions are storage-layer only.</summary>
277+
internal static readonly CultureCollation Latin1General100CsAsScUtf8 = new(
278+
"Latin1_General_100_CS_AS_SC_UTF8", CultureInfo.InvariantCulture.Name, caseSensitive: true);
279+
280+
/// <summary>"Latin1_General_100_BIN2_UTF8" — binary UTF-8 variant.
281+
/// Pure codepoint comparison via <see cref="BinaryCollation"/>;
282+
/// metadata-only instance for catalog recognition.</summary>
283+
internal static readonly Latin1_General_100_BIN2_UTF8 Latin1General100Bin2Utf8 = new();
284+
285+
/// <summary>Metadata-only binary collation under the
286+
/// <c>Latin1_General_100_BIN2_UTF8</c> name.</summary>
287+
internal sealed class Latin1_General_100_BIN2_UTF8 : BinaryCollation
288+
{
289+
public override string Name => "Latin1_General_100_BIN2_UTF8";
290+
}
291+
183292
/// <summary>
184293
/// Closed accept-list of collation names the simulator recognizes.
185294
/// ALTER DATABASE COLLATE / CREATE TABLE column COLLATE accept these
@@ -211,6 +320,21 @@ internal sealed class UNICODE_CODEPOINT : BinaryCollation
211320
[JapaneseXJIS140CiAs.Name] = "Japanese-XJIS-140, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
212321
[ChinesePrcCiAs.Name] = "Chinese-PRC, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
213322
[TurkishCiAs.Name] = "Turkish, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
323+
[Korean100CiAs.Name] = "Korean-100, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
324+
[KoreanWansungCiAs.Name] = "Korean-Wansung, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
325+
[GreekCiAs.Name] = "Greek, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
326+
[Greek100CiAs.Name] = "Greek-100, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
327+
[CyrillicGeneralCiAs.Name] = "Cyrillic-General, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
328+
[CyrillicGeneral100CiAs.Name] = "Cyrillic-General-100, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
329+
[GermanPhoneBookCiAs.Name] = "German-PhoneBook, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
330+
[GermanPhoneBook100CiAs.Name] = "German-PhoneBook-100, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
331+
[FrenchCiAs.Name] = "French, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
332+
[French100CiAs.Name] = "French-100, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
333+
[ModernSpanishCiAs.Name] = "Modern-Spanish, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
334+
[ModernSpanish100CiAs.Name] = "Modern-Spanish-100, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive",
335+
[Latin1General100CiAsScUtf8.Name] = "Latin1-General-100, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive, supplementary characters, UTF8",
336+
[Latin1General100CsAsScUtf8.Name] = "Latin1-General-100, case-sensitive, accent-sensitive, kanatype-insensitive, width-insensitive, supplementary characters, UTF8",
337+
[Latin1General100Bin2Utf8.Name] = "Latin1-General-100, binary code point comparison sort, UTF8",
214338
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
215339

216340
/// <summary>
@@ -234,6 +358,21 @@ internal sealed class UNICODE_CODEPOINT : BinaryCollation
234358
[JapaneseXJIS140CiAs.Name] = JapaneseXJIS140CiAs,
235359
[ChinesePrcCiAs.Name] = ChinesePrcCiAs,
236360
[TurkishCiAs.Name] = TurkishCiAs,
361+
[Korean100CiAs.Name] = Korean100CiAs,
362+
[KoreanWansungCiAs.Name] = KoreanWansungCiAs,
363+
[GreekCiAs.Name] = GreekCiAs,
364+
[Greek100CiAs.Name] = Greek100CiAs,
365+
[CyrillicGeneralCiAs.Name] = CyrillicGeneralCiAs,
366+
[CyrillicGeneral100CiAs.Name] = CyrillicGeneral100CiAs,
367+
[GermanPhoneBookCiAs.Name] = GermanPhoneBookCiAs,
368+
[GermanPhoneBook100CiAs.Name] = GermanPhoneBook100CiAs,
369+
[FrenchCiAs.Name] = FrenchCiAs,
370+
[French100CiAs.Name] = French100CiAs,
371+
[ModernSpanishCiAs.Name] = ModernSpanishCiAs,
372+
[ModernSpanish100CiAs.Name] = ModernSpanish100CiAs,
373+
[Latin1General100CiAsScUtf8.Name] = Latin1General100CiAsScUtf8,
374+
[Latin1General100CsAsScUtf8.Name] = Latin1General100CsAsScUtf8,
375+
[Latin1General100Bin2Utf8.Name] = Latin1General100Bin2Utf8,
237376
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
238377

239378
/// <summary>

0 commit comments

Comments
 (0)