@@ -504,5 +504,214 @@ insert t values (N'Apple'), (N'apple'), (N'APPLE')
504504 "select count(*) from (select distinct s from t) d" ) ) ;
505505 }
506506
507+ /// <summary>
508+ /// <c>varchar Latin1_General_BIN2</c> sorts by CP1252 byte order — which
509+ /// diverges from Unicode codepoint order in the 0x80-0x9F window. The
510+ /// euro sign U+20AC encodes to CP1252 byte 0x80, so it sorts BEFORE
511+ /// NBSP (U+00A0, CP1252 byte 0xA0). Probe-confirmed against SQL Server
512+ /// 2025 — order matches byte order across the test fixture.
513+ /// </summary>
514+ [ TestMethod ]
515+ public void OrderBy_VarcharBin2_UsesCp1252ByteOrder ( )
516+ {
517+ var sim = new Simulation ( ) ;
518+ _ = sim . ExecuteNonQuery ( """
519+ create table t (s varchar(2) collate Latin1_General_BIN2);
520+ insert t values
521+ (cast(nchar(161) as varchar(2))),
522+ (cast(nchar(8364) as varchar(2))),
523+ (cast(nchar(402) as varchar(2))),
524+ (cast(nchar(376) as varchar(2))),
525+ (cast(nchar(160) as varchar(2)))
526+ """ ) ;
527+ using var reader = sim . CreateCommand ( "select ascii(s) from t order by s" ) . ExecuteReader ( ) ;
528+ var rows = new List < int > ( ) ;
529+ while ( reader . Read ( ) )
530+ rows . Add ( reader . GetInt32 ( 0 ) ) ;
531+ CollectionAssert . AreEqual ( new [ ] { 0x80 , 0x83 , 0x9F , 0xA0 , 0xA1 } , rows ) ;
532+ }
533+
534+ /// <summary>
535+ /// <c>nvarchar Latin1_General_BIN2</c> sorts by UTF-16 code unit
536+ /// (16-bit big-endian value), <em>not</em> code point. The Microsoft
537+ /// docs say "BIN2 = code point sort" but the empirical behavior on
538+ /// SQL Server 2025 is code-unit — probe-confirmed with the
539+ /// supplementary char U+1F600 (emoji, encoded as surrogate pair
540+ /// D83D DE00) sorting BEFORE U+E000, which can only happen if the
541+ /// compare is code-unit (D83D < E000) and not code-point
542+ /// (U+1F600 > U+E000). For the BMP-only inputs here the two
543+ /// orderings happen to agree, so the assertion is the same either
544+ /// way — what's exercised is that nvarchar BIN2 sorts by something
545+ /// other than the varchar BIN2 CP1252 byte order. See
546+ /// <c>docs/claude/collations.md</c> "Microsoft-docs-vs-real-behavior
547+ /// gap" for the contrast and citations.
548+ /// </summary>
549+ [ TestMethod ]
550+ public void OrderBy_NvarcharBin2_UsesCodeUnitOrder ( )
551+ {
552+ var sim = new Simulation ( ) ;
553+ _ = sim . ExecuteNonQuery ( """
554+ create table t (s nvarchar(2) collate Latin1_General_BIN2);
555+ insert t values (nchar(161)), (nchar(8364)), (nchar(402)), (nchar(376)), (nchar(160))
556+ """ ) ;
557+ using var reader = sim . CreateCommand ( "select unicode(s) from t order by s" ) . ExecuteReader ( ) ;
558+ var rows = new List < int > ( ) ;
559+ while ( reader . Read ( ) )
560+ rows . Add ( reader . GetInt32 ( 0 ) ) ;
561+ CollectionAssert . AreEqual ( new [ ] { 160 , 161 , 376 , 402 , 8364 } , rows ) ;
562+ }
563+
564+ /// <summary>
565+ /// <c>nvarchar Latin1_General_BIN2</c> with a supplementary character
566+ /// (U+1F600, 😀, encoded as surrogate pair D83D DE00) sorts BEFORE
567+ /// the high BMP char U+E000 because D83D < E000 as 16-bit code
568+ /// units. Under a hypothetical code-point compare (which the docs
569+ /// claim) the supplementary U+1F600 would sort AFTER U+E000.
570+ /// Probe-confirmed against SQL Server 2025. This test catches any
571+ /// regression that would introduce "code-point fixing" logic into
572+ /// the binary collation body.
573+ /// </summary>
574+ [ TestMethod ]
575+ public void OrderBy_NvarcharBin2_SupplementaryCharSortsByCodeUnit ( )
576+ {
577+ var sim = new Simulation ( ) ;
578+ _ = sim . ExecuteNonQuery ( """
579+ create table t (id int identity primary key, s nvarchar(4) collate Latin1_General_BIN2);
580+ insert t (s) values
581+ (nchar(57344)), -- U+E000
582+ (nchar(55357) + nchar(56832)) -- surrogate pair for U+1F600 (😀)
583+ """ ) ;
584+ // ORDER BY s should give: emoji (D83D…) before U+E000.
585+ // We project the first code unit's UNICODE() value to make the
586+ // assertion source-encoding-independent.
587+ using var reader = sim . CreateCommand ( "select unicode(s) from t order by s" ) . ExecuteReader ( ) ;
588+ var rows = new List < int > ( ) ;
589+ while ( reader . Read ( ) )
590+ rows . Add ( reader . GetInt32 ( 0 ) ) ;
591+ // 55357 = 0xD83D (high surrogate of emoji, sorts first).
592+ // 57344 = 0xE000 (sorts second).
593+ CollectionAssert . AreEqual ( new [ ] { 55357 , 57344 } , rows ) ;
594+ }
595+
596+ /// <summary>
597+ /// <c>varchar Latin1_General_BIN</c> uses the same CP1252 byte body as
598+ /// BIN2 at the simulator's value layer — the BIN-vs-BIN2 code-page-
599+ /// prefix asymmetry isn't observable through a single-codepage value
600+ /// stack. Confirms the substitution wires up symmetrically for BIN.
601+ /// </summary>
602+ [ TestMethod ]
603+ public void OrderBy_VarcharBin_UsesCp1252ByteOrder ( )
604+ {
605+ var sim = new Simulation ( ) ;
606+ _ = sim . ExecuteNonQuery ( """
607+ create table t (s varchar(2) collate Latin1_General_BIN);
608+ insert t values
609+ (cast(nchar(8364) as varchar(2))),
610+ (cast(nchar(160) as varchar(2))),
611+ (cast(nchar(402) as varchar(2)))
612+ """ ) ;
613+ using var reader = sim . CreateCommand ( "select ascii(s) from t order by s" ) . ExecuteReader ( ) ;
614+ var rows = new List < int > ( ) ;
615+ while ( reader . Read ( ) )
616+ rows . Add ( reader . GetInt32 ( 0 ) ) ;
617+ CollectionAssert . AreEqual ( new [ ] { 0x80 , 0x83 , 0xA0 } , rows ) ;
618+ }
619+
620+ /// <summary>
621+ /// DISTINCT hash on a varchar BIN2 column agrees with the CP1252
622+ /// byte-equality contract — duplicates collapse and CP1252-distinct
623+ /// values stay in separate buckets. Covers the GetHashCode path on
624+ /// <c>Cp1252BinaryCollation</c>.
625+ /// </summary>
626+ [ TestMethod ]
627+ public void Distinct_VarcharBin2_HashRespectsCp1252Bytes ( )
628+ {
629+ var sim = new Simulation ( ) ;
630+ _ = sim . ExecuteNonQuery ( """
631+ create table t (s varchar(2) collate Latin1_General_BIN2);
632+ insert t values
633+ (cast(nchar(8364) as varchar(2))),
634+ (cast(nchar(8364) as varchar(2))),
635+ (cast(nchar(402) as varchar(2)))
636+ """ ) ;
637+ AreEqual ( 2 , sim . ExecuteScalar ( "select count(*) from (select distinct s from t) d" ) ) ;
638+ }
639+
640+ /// <summary>
641+ /// <c>char(N) Latin1_General_BIN2</c> picks up the CP1252 substitution
642+ /// through the same <c>CharSqlType.WithCollation</c> hook as varchar.
643+ /// Same input characters produce the same byte-order ranking.
644+ /// </summary>
645+ [ TestMethod ]
646+ public void OrderBy_CharBin2_UsesCp1252ByteOrder ( )
647+ {
648+ var sim = new Simulation ( ) ;
649+ _ = sim . ExecuteNonQuery ( """
650+ create table t (s char(2) collate Latin1_General_BIN2);
651+ insert t values
652+ (cast(nchar(8364) as char(2))),
653+ (cast(nchar(160) as char(2))),
654+ (cast(nchar(402) as char(2)))
655+ """ ) ;
656+ using var reader = sim . CreateCommand ( "select ascii(s) from t order by s" ) . ExecuteReader ( ) ;
657+ var rows = new List < int > ( ) ;
658+ while ( reader . Read ( ) )
659+ rows . Add ( reader . GetInt32 ( 0 ) ) ;
660+ CollectionAssert . AreEqual ( new [ ] { 0x80 , 0x83 , 0xA0 } , rows ) ;
661+ }
662+
663+ /// <summary>
664+ /// Pre-SQL-Server-2005 <c>Latin1_General_BIN</c> on nvarchar has an
665+ /// asymmetric sort rule: position 0 is a 16-bit code-unit compare
666+ /// (matches BIN2), but position 1+ combines surrogate pairs into
667+ /// 32-bit scalars and code-point-compares. So <c>'Z' + N'😀'</c>
668+ /// (after the shared <c>'Z'</c>, the supplementary U+1F600 vs the
669+ /// high BMP U+E000) sorts as <c>0x1F600 > 0xE000</c> — emoji-row
670+ /// LAST. Under BIN2 with the same data the comparison is code-unit
671+ /// (0xD83D < 0xE000), so emoji-row sorts FIRST — see
672+ /// <see cref="OrderBy_NvarcharBin2_Position1Plus_KeepsCodeUnitOrder"/>
673+ /// for the companion contrast.
674+ /// </summary>
675+ [ TestMethod ]
676+ public void OrderBy_NvarcharBin_Position1Plus_UsesCodePointOrder ( )
677+ {
678+ var sim = new Simulation ( ) ;
679+ _ = sim . ExecuteNonQuery ( """
680+ create table t (id int identity primary key, s nvarchar(4) collate Latin1_General_BIN);
681+ insert t (s) values
682+ (N'Z' + nchar(55357) + nchar(56832)), -- 'Z' + emoji U+1F600
683+ (N'Z' + nchar(57344)) -- 'Z' + U+E000
684+ """ ) ;
685+ using var reader = sim . CreateCommand ( "select id from t order by s" ) . ExecuteReader ( ) ;
686+ var ids = new List < int > ( ) ;
687+ while ( reader . Read ( ) )
688+ ids . Add ( reader . GetInt32 ( 0 ) ) ;
689+ // 'Z'+U+E000 (id=2) sorts first; 'Z'+emoji (id=1) sorts second.
690+ CollectionAssert . AreEqual ( new [ ] { 2 , 1 } , ids ) ;
691+ }
692+
693+ /// <summary>
694+ /// Contrast to <see cref="OrderBy_NvarcharBin_Position1Plus_UsesCodePointOrder"/>:
695+ /// the same data under BIN2 sorts the opposite way because BIN2 is
696+ /// code-unit throughout (0xD83D < 0xE000 at position 1).
697+ /// </summary>
698+ [ TestMethod ]
699+ public void OrderBy_NvarcharBin2_Position1Plus_KeepsCodeUnitOrder ( )
700+ {
701+ var sim = new Simulation ( ) ;
702+ _ = sim . ExecuteNonQuery ( """
703+ create table t (id int identity primary key, s nvarchar(4) collate Latin1_General_BIN2);
704+ insert t (s) values
705+ (N'Z' + nchar(55357) + nchar(56832)),
706+ (N'Z' + nchar(57344))
707+ """ ) ;
708+ using var reader = sim . CreateCommand ( "select id from t order by s" ) . ExecuteReader ( ) ;
709+ var ids = new List < int > ( ) ;
710+ while ( reader . Read ( ) )
711+ ids . Add ( reader . GetInt32 ( 0 ) ) ;
712+ // 'Z'+emoji (id=1) sorts first under code-unit; 'Z'+U+E000 (id=2) second.
713+ CollectionAssert . AreEqual ( new [ ] { 1 , 2 } , ids ) ;
714+ }
715+
507716 private static void IsNull ( object ? value ) => Microsoft . VisualStudio . TestTools . UnitTesting . Assert . IsNull ( value is DBNull ? null : value ) ;
508717}
0 commit comments