11using System . Collections . Frozen ;
22using System . Globalization ;
3+ using SqlServerSimulator . Storage ;
34
45namespace SqlServerSimulator ;
56
7+ /// <summary>
8+ /// SQL Server's collation-precedence rank. Used by the expression layer to
9+ /// decide which collation wins when two string operands meet; mismatched
10+ /// peers raise <c>Msg 468</c> / <c>Msg 457</c>.
11+ /// </summary>
12+ /// <remarks>
13+ /// Real SQL Server defines additional ranks (<c>Coercible-default</c>,
14+ /// <c>Implicit X</c>, <c>Explicit X</c>, <c>No-collation</c>); the simulator
15+ /// collapses them to the three that drive observable behavior. Hierarchy:
16+ /// <see cref="Explicit"/> beats <see cref="Implicit"/> beats
17+ /// <see cref="CoercibleDefault"/>; peers with the same rank but different
18+ /// collations raise the conflict error.
19+ /// </remarks>
20+ internal enum Coercibility : byte
21+ {
22+ /// <summary>Literal, parameter, system-function result, CAST of a coercible-default source. Yields to <see cref="Implicit"/> / <see cref="Explicit"/> peers.</summary>
23+ CoercibleDefault = 0 ,
24+
25+ /// <summary>Column reference, CAST of a column, computed-column expression. Two implicits with different collations raise Msg 468 / 457.</summary>
26+ Implicit = 1 ,
27+
28+ /// <summary>Explicit <c>COLLATE</c> postfix. Beats both lower ranks; two explicits with different collations raise Msg 468 / 457.</summary>
29+ Explicit = 2 ,
30+ }
31+
632/// <summary>
733/// The SQL Server equivalent to .NET's <see cref="IComparer{T}"/> for strings.
834/// Three concrete subclasses match the names on the
@@ -22,21 +48,21 @@ private protected Collation()
2248 /// True when the collation name carries the <c>_CS_</c> or <c>_BIN</c>
2349 /// marker — i.e. case differences are significant. Consulted by
2450 /// <c>LIKE</c>'s regex builder to decide whether to set
25- /// <c>System.Text.RegularExpressions.RegexOptions.IgnoreCase</c>;
26- /// every other simulator string op still routes through
27- /// <see cref="Default "/> regardless of this flag (see
28- /// <c>docs/claude/database-options.md</c>) .
51+ /// <c>System.Text.RegularExpressions.RegexOptions.IgnoreCase</c>.
52+ /// Other string ops (sort, equality, hash) honor the collation
53+ /// directly through <see cref="Compare "/> / <see cref="Equals"/> /
54+ /// <see cref="GetHashCode"/> via the column type's pinned collation .
2955 /// </summary>
3056 public virtual bool CaseSensitive => false ;
3157
3258 /// <summary>
3359 /// The instance returned for "SQL_Latin1_General_CP1_CI_AS" — the
34- /// simulator's default database collation. Routed through
60+ /// simulator's default database collation. Used by
3561 /// <see cref="Database.CollationName"/> when no explicit
36- /// <c>ALTER DATABASE COLLATE</c> has been issued, and (for the time
37- /// being — see <c>docs/claude/database-options.md</c>) is also the
38- /// comparer every string op falls back to regardless of declared
39- /// column / database collation .
62+ /// <c>ALTER DATABASE COLLATE</c> has been issued, and as the fallback
63+ /// when a string-typed value's <see cref="SqlType.Collation"/> is
64+ /// <see langword="null"/> (literals built via the singleton
65+ /// <c>SqlValue.FromVarchar(string)</c> / etc. paths) .
4066 /// </summary>
4167 internal static readonly SQL_Latin1_General_CP1_CI_AS Default = new ( ) ;
4268
@@ -94,17 +120,78 @@ private protected Collation()
94120 internal static readonly Latin1_General_BIN2 Latin1GeneralBin2 = new ( ) ;
95121
96122 /// <summary>
97- /// Closed accept-list of collation names the simulator recognizes as
98- /// metadata. ALTER DATABASE COLLATE / CREATE TABLE column COLLATE accept
99- /// these names without raising; the loader records them on the
100- /// database / column for catalog-view round-trip. The comparison /
101- /// sort pipeline still routes every op through
102- /// <see cref="Default"/> regardless of declared column / database
103- /// collation; <c>LIKE</c> is the one site that honors the
104- /// <see cref="CaseSensitive"/> flag of an explicit
105- /// <c>COLLATE</c> override (see <c>docs/claude/database-options.md</c>).
106- /// Names outside this set surface as <see cref="NotSupportedException"/>
107- /// in direct SQL; the BACPAC loader catches and records on
123+ /// "Japanese_XJIS_140_CI_AS" — the modern Japanese collation that
124+ /// handles supplementary characters via the XJIS-140 mapping table.
125+ /// Comparison routes through .NET's <c>ja-JP</c> <see cref="CompareInfo"/>
126+ /// with the simulator's standard CI/AS options (case-insensitive,
127+ /// 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.
130+ /// </summary>
131+ internal static readonly CultureCollation JapaneseXJIS140CiAs = new ( "Japanese_XJIS_140_CI_AS" , "ja-JP" , caseSensitive : false ) ;
132+
133+ /// <summary>
134+ /// "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.
137+ /// </summary>
138+ internal static readonly CultureCollation ChinesePrcCiAs = new ( "Chinese_PRC_CI_AS" , "zh-CN" , caseSensitive : false ) ;
139+
140+ /// <summary>
141+ /// "Turkish_CI_AS" — Turkish collation via .NET's <c>tr-TR</c>
142+ /// <see cref="CompareInfo"/>. Notably handles the i / İ / ı / I
143+ /// 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.
146+ /// </summary>
147+ internal static readonly CultureCollation TurkishCiAs = new ( "Turkish_CI_AS" , "tr-TR" , caseSensitive : false ) ;
148+
149+ /// <summary>
150+ /// "Latin1_General_CI_AS_KS_WS" — a Latin1 variant with kana-type and
151+ /// width <em>sensitive</em>. Used in real databases for some sysname
152+ /// columns and a handful of user columns; included so BACPAC import
153+ /// doesn't warn on its presence. Comparison routes through the
154+ /// invariant culture with KS/WS enabled.
155+ /// </summary>
156+ internal static readonly CultureCollation Latin1GeneralCiAsKsWs = new ( "Latin1_General_CI_AS_KS_WS" , CultureInfo . InvariantCulture . Name , caseSensitive : false ) ;
157+
158+ /// <summary>
159+ /// "SQL_Latin1_General_CP437_CS_AS" — legacy CP437 code-page binding
160+ /// (the original IBM PC code page), case-sensitive. The simulator
161+ /// routes comparison through invariant culture case-sensitive options
162+ /// — close enough for the system-column shapes this collation actually
163+ /// appears in; non-ASCII CP437 glyphs aren't probed.
164+ /// </summary>
165+ internal static readonly CultureCollation SqlLatin1GeneralCp437CsAs = new ( "SQL_Latin1_General_CP437_CS_AS" , CultureInfo . InvariantCulture . Name , caseSensitive : true ) ;
166+
167+ /// <summary>
168+ /// "UNICODE_CODEPOINT" — pure ordinal Unicode codepoint comparison.
169+ /// Semantically equivalent to <see cref="Latin1GeneralBin2"/> at the
170+ /// value level; the name appears on a handful of system columns in
171+ /// some BACPACs (notably AdventureWorks2025). Reuses the binary
172+ /// codepath via a separate metadata-only instance.
173+ /// </summary>
174+ internal static readonly UNICODE_CODEPOINT UnicodeCodepoint = new ( ) ;
175+
176+ /// <summary>Metadata-only binary collation under the
177+ /// <c>UNICODE_CODEPOINT</c> name; behavior body is <see cref="BinaryCollation"/>.</summary>
178+ internal sealed class UNICODE_CODEPOINT : BinaryCollation
179+ {
180+ public override string Name => "UNICODE_CODEPOINT" ;
181+ }
182+
183+ /// <summary>
184+ /// Closed accept-list of collation names the simulator recognizes.
185+ /// ALTER DATABASE COLLATE / CREATE TABLE column COLLATE accept these
186+ /// names without raising; the loader records them on the database /
187+ /// column for catalog-view round-trip and pins the resolved
188+ /// <see cref="Collation"/> instance onto the column's
189+ /// <see cref="SqlType"/> at <see cref="Coercibility.Implicit"/> rank,
190+ /// so subsequent comparisons / sorts honor the declared collation.
191+ /// Cross-collation operand pairs that can't be resolved by coercibility
192+ /// raise Msg 468 (comparison) / Msg 457 (concat). Names outside this
193+ /// set surface as <see cref="NotSupportedException"/> in direct SQL;
194+ /// the BACPAC loader catches and records on
108195 /// <c>BacpacImportResult.Warnings</c>. Each entry's value is the
109196 /// human-readable description that <c>sys.fn_helpcollations()</c>
110197 /// exposes verbatim (probe-confirmed against SQL Server 2025).
@@ -118,6 +205,12 @@ private protected Collation()
118205 [ Latin1GeneralCsAs . Name ] = "Latin1-General, case-sensitive, accent-sensitive, kanatype-insensitive, width-insensitive" ,
119206 [ Latin1GeneralBin . Name ] = "Latin1-General, binary" ,
120207 [ Latin1GeneralBin2 . Name ] = "Latin1-General, binary code point comparison sort" ,
208+ [ Latin1GeneralCiAsKsWs . Name ] = "Latin1-General, case-insensitive, accent-sensitive, kanatype-sensitive, width-sensitive" ,
209+ [ SqlLatin1GeneralCp437CsAs . Name ] = "Latin1-General, case-sensitive, accent-sensitive, kanatype-insensitive, width-insensitive for Unicode Data, SQL Server Sort Order 30 on Code Page 437 for non-Unicode Data" ,
210+ [ UnicodeCodepoint . Name ] = "Unicode code point comparison sort" ,
211+ [ JapaneseXJIS140CiAs . Name ] = "Japanese-XJIS-140, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive" ,
212+ [ ChinesePrcCiAs . Name ] = "Chinese-PRC, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive" ,
213+ [ TurkishCiAs . Name ] = "Turkish, case-insensitive, accent-sensitive, kanatype-insensitive, width-insensitive" ,
121214 } . ToFrozenDictionary ( StringComparer . OrdinalIgnoreCase ) ;
122215
123216 /// <summary>
@@ -135,6 +228,12 @@ private protected Collation()
135228 [ Latin1GeneralCsAs . Name ] = Latin1GeneralCsAs ,
136229 [ Latin1GeneralBin . Name ] = Latin1GeneralBin ,
137230 [ Latin1GeneralBin2 . Name ] = Latin1GeneralBin2 ,
231+ [ Latin1GeneralCiAsKsWs . Name ] = Latin1GeneralCiAsKsWs ,
232+ [ SqlLatin1GeneralCp437CsAs . Name ] = SqlLatin1GeneralCp437CsAs ,
233+ [ UnicodeCodepoint . Name ] = UnicodeCodepoint ,
234+ [ JapaneseXJIS140CiAs . Name ] = JapaneseXJIS140CiAs ,
235+ [ ChinesePrcCiAs . Name ] = ChinesePrcCiAs ,
236+ [ TurkishCiAs . Name ] = TurkishCiAs ,
138237 } . ToFrozenDictionary ( StringComparer . OrdinalIgnoreCase ) ;
139238
140239 /// <summary>
@@ -144,6 +243,39 @@ private protected Collation()
144243 /// </summary>
145244 internal static bool IsRecognized ( string name ) => Recognized . ContainsKey ( name ) ;
146245
246+ /// <summary>
247+ /// SQL Server's collation-coercibility resolution for two operands.
248+ /// Returns the winning <see cref="Collation"/> and <see cref="SqlServerSimulator.Coercibility"/>
249+ /// when the pair is resolvable; <see langword="null"/> when both
250+ /// operands have the same rank but different collations (the caller is
251+ /// expected to raise Msg 468 / Msg 457 with operator context).
252+ /// </summary>
253+ /// <remarks>
254+ /// <para>Hierarchy: <see cref="Coercibility.Explicit"/> beats
255+ /// <see cref="Coercibility.Implicit"/> beats
256+ /// <see cref="Coercibility.CoercibleDefault"/>. Mismatched higher rank
257+ /// wins regardless of the lower-rank operand's collation. Equal-rank
258+ /// peers must share a collation; different collations at the same rank
259+ /// are unresolvable.</para>
260+ /// <para>Non-string operands return <see langword="null"/> for
261+ /// <c>Collation</c> on the SqlType, which collapses to the default
262+ /// collation under this resolution (a non-string operand is
263+ /// coercible-default by definition, and there's nothing to conflict
264+ /// with).</para>
265+ /// </remarks>
266+ internal static ( Collation Collation , Coercibility Coercibility ) ? Resolve ( SqlType a , SqlType b )
267+ {
268+ var ca = a . Coercibility ;
269+ var cb = b . Coercibility ;
270+ if ( ca > cb )
271+ return ( a . Collation ?? Default , ca ) ;
272+ if ( cb > ca )
273+ return ( b . Collation ?? Default , cb ) ;
274+ var aCol = a . Collation ?? Default ;
275+ var bCol = b . Collation ?? Default ;
276+ return StringComparer . OrdinalIgnoreCase . Equals ( aCol . Name , bCol . Name ) ? ( aCol , ca ) : null ;
277+ }
278+
147279 public abstract int Compare ( string ? x , string ? y ) ;
148280
149281 public abstract bool Equals ( string ? x , string ? y ) ;
@@ -320,4 +452,59 @@ internal sealed class Latin1_General_BIN2 : BinaryCollation
320452 {
321453 public override string Name => "Latin1_General_BIN2" ;
322454 }
455+
456+ /// <summary>
457+ /// Generic culture-based collation: pins a <see cref="CompareInfo"/>
458+ /// and a <see cref="CompareOptions"/> set, routing
459+ /// <see cref="Compare"/> / <see cref="Equals"/> / <see cref="GetHashCode"/>
460+ /// through them. Sort options layer <see cref="CompareOptions.IgnoreSymbols"/>
461+ /// on top of the equality options, matching SQL Server's sort-only
462+ /// ignore-symbols treatment (the same asymmetry <see cref="WindowsCiAs"/>
463+ /// captures for Latin1). Used for the locale-specific collations that
464+ /// don't justify their own dedicated subclass yet.
465+ /// </summary>
466+ internal sealed class CultureCollation : Collation
467+ {
468+ private readonly string name ;
469+
470+ private readonly bool caseSensitive ;
471+
472+ private readonly CompareInfo compareInfo ;
473+
474+ private readonly CompareOptions equalityOptions ;
475+
476+ private readonly CompareOptions sortOptions ;
477+
478+ internal CultureCollation ( string name , string cultureName , bool caseSensitive )
479+ {
480+ this . name = name ;
481+ this . caseSensitive = caseSensitive ;
482+ this . compareInfo = CultureInfo . GetCultureInfo ( cultureName ) . CompareInfo ;
483+ var baseOpts = caseSensitive
484+ ? CompareOptions . None
485+ : CompareOptions . IgnoreCase ;
486+ // CI_AS / CS_AS without KS / WS suffixes ignore kana type and
487+ // width by default — matches SQL Server's "*_CI_AS" semantics.
488+ baseOpts |= CompareOptions . IgnoreKanaType | CompareOptions . IgnoreWidth ;
489+ this . equalityOptions = baseOpts ;
490+ this . sortOptions = baseOpts | CompareOptions . IgnoreSymbols ;
491+ }
492+
493+ public override string Name => this . name ;
494+
495+ public override bool CaseSensitive => this . caseSensitive ;
496+
497+ public override int Compare ( string ? x , string ? y ) =>
498+ x is null
499+ ? ( y is null ? 0 : - 1 )
500+ : y is null ? 1 : this . compareInfo . Compare ( x , y , this . sortOptions ) ;
501+
502+ public override bool Equals ( string ? x , string ? y ) =>
503+ x is null
504+ ? y is null
505+ : y is not null && this . compareInfo . Compare ( x , y , this . equalityOptions ) == 0 ;
506+
507+ public override int GetHashCode ( string obj ) =>
508+ this . compareInfo . GetHashCode ( obj , this . equalityOptions ) ;
509+ }
323510}
0 commit comments