Skip to content

Commit ebdd81a

Browse files
committed
Collations Latin1_General_BIN and Latin1_General_BIN2 now accurately match their SQL Server implementations, bugs and quirks included.
1 parent 062ee0e commit ebdd81a

6 files changed

Lines changed: 433 additions & 60 deletions

File tree

SqlServerSimulator.Tests/CollationDeclaredColumnTests.cs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 &lt; E000) and not code-point
542+
/// (U+1F600 &gt; 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 &lt; 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 &gt; 0xE000</c> — emoji-row
670+
/// LAST. Under BIN2 with the same data the comparison is code-unit
671+
/// (0xD83D &lt; 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 &lt; 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

Comments
 (0)