@@ -979,5 +979,67 @@ public void CastAsCharN_WithPostfixCollateUtf8_MatchesByteBudget(string input, i
979979 AreEqual ( expectedHex , actualHex ) ;
980980 }
981981
982+ /// <summary>
983+ /// Scalar functions switch indexing semantics on the input value's
984+ /// collation: code-unit semantics under non-<c>_SC_</c>, codepoint
985+ /// semantics under <c>_SC_</c>. All eight affected functions
986+ /// probe-confirmed against SQL Server 2025 (2026-05-21) using a single
987+ /// supplementary character (😀 = U+1F600) as the test fixture.
988+ /// </summary>
989+ [ TestMethod ]
990+ [ DataRow ( "len(N'😀' collate Latin1_General_100_CI_AS)" , "2" , DisplayName = "LEN non-SC = 2 code units" ) ]
991+ [ DataRow ( "len(N'😀' collate Latin1_General_100_CI_AS_SC_UTF8)" , "1" , DisplayName = "LEN SC = 1 codepoint" ) ]
992+ [ DataRow ( "len(N'a😀b' collate Latin1_General_100_CI_AS)" , "4" , DisplayName = "LEN non-SC a😀b = 4" ) ]
993+ [ DataRow ( "len(N'a😀b' collate Latin1_General_100_CI_AS_SC_UTF8)" , "3" , DisplayName = "LEN SC a😀b = 3" ) ]
994+ [ DataRow ( "charindex(N'X', N'😀X' collate Latin1_General_100_CI_AS)" , "3" , DisplayName = "CHARINDEX non-SC = position 3" ) ]
995+ [ DataRow ( "charindex(N'X', N'😀X' collate Latin1_General_100_CI_AS_SC_UTF8)" , "2" , DisplayName = "CHARINDEX SC = position 2" ) ]
996+ [ DataRow ( "patindex(N'%X%', N'😀X' collate Latin1_General_100_CI_AS)" , "3" , DisplayName = "PATINDEX non-SC = 3" ) ]
997+ [ DataRow ( "patindex(N'%X%', N'😀X' collate Latin1_General_100_CI_AS_SC_UTF8)" , "2" , DisplayName = "PATINDEX SC = 2" ) ]
998+ [ DataRow ( "unicode(N'😀' collate Latin1_General_100_CI_AS)" , "55357" , DisplayName = "UNICODE non-SC = 55357 high surrogate" ) ]
999+ [ DataRow ( "unicode(N'😀' collate Latin1_General_100_CI_AS_SC_UTF8)" , "128512" , DisplayName = "UNICODE SC = 128512 codepoint" ) ]
1000+ public void SupplementaryCharFunctions_PositionAndLengthDispatch ( string expression , string expectedScalar )
1001+ => AreEqual ( int . Parse ( expectedScalar ) , new Simulation ( ) . ExecuteScalar ( $ "select { expression } ") ) ;
1002+
1003+ /// <summary>
1004+ /// SUBSTRING / LEFT / RIGHT / REVERSE / STUFF return string results
1005+ /// whose UTF-16 byte content differs between SC and non-SC. Compares
1006+ /// the .NET string char-by-char to the expected UTF-16 LE byte hex —
1007+ /// lone surrogates that real SQL Server preserves under non-SC are
1008+ /// also preserved by the simulator (the nvarchar Encode/Decode path
1009+ /// byte-copies directly, bypassing <c>Encoding.Unicode</c>'s lone-
1010+ /// surrogate replacement).
1011+ /// </summary>
1012+ [ TestMethod ]
1013+ [ DataRow ( "substring(N'😀X' collate Latin1_General_100_CI_AS, 1, 1)" , "3DD8" , DisplayName = "SUBSTRING non-SC pos 1 len 1 = lone high surrogate" ) ]
1014+ [ DataRow ( "substring(N'😀X' collate Latin1_General_100_CI_AS_SC_UTF8, 1, 1)" , "3DD800DE" , DisplayName = "SUBSTRING SC pos 1 len 1 = full emoji" ) ]
1015+ [ DataRow ( "substring(N'😀X' collate Latin1_General_100_CI_AS, 2, 1)" , "00DE" , DisplayName = "SUBSTRING non-SC pos 2 len 1 = lone low surrogate" ) ]
1016+ [ DataRow ( "substring(N'😀X' collate Latin1_General_100_CI_AS_SC_UTF8, 2, 1)" , "5800" , DisplayName = "SUBSTRING SC pos 2 len 1 = X" ) ]
1017+ [ DataRow ( "left(N'😀X' collate Latin1_General_100_CI_AS, 1)" , "3DD8" , DisplayName = "LEFT non-SC" ) ]
1018+ [ DataRow ( "left(N'😀X' collate Latin1_General_100_CI_AS_SC_UTF8, 1)" , "3DD800DE" , DisplayName = "LEFT SC" ) ]
1019+ [ DataRow ( "right(N'X😀' collate Latin1_General_100_CI_AS, 1)" , "00DE" , DisplayName = "RIGHT non-SC" ) ]
1020+ [ DataRow ( "right(N'X😀' collate Latin1_General_100_CI_AS_SC_UTF8, 1)" , "3DD800DE" , DisplayName = "RIGHT SC" ) ]
1021+ [ DataRow ( "reverse(N'😀X' collate Latin1_General_100_CI_AS)" , "580000DE3DD8" , DisplayName = "REVERSE non-SC tears surrogate pair" ) ]
1022+ [ DataRow ( "reverse(N'😀X' collate Latin1_General_100_CI_AS_SC_UTF8)" , "58003DD800DE" , DisplayName = "REVERSE SC keeps emoji intact" ) ]
1023+ [ DataRow ( "stuff(N'😀X' collate Latin1_General_100_CI_AS, 1, 1, N'Y')" , "590000DE5800" , DisplayName = "STUFF non-SC replaces 1 code unit (high surrogate)" ) ]
1024+ [ DataRow ( "stuff(N'😀X' collate Latin1_General_100_CI_AS_SC_UTF8, 1, 1, N'Y')" , "59005800" , DisplayName = "STUFF SC replaces 1 codepoint (whole emoji)" ) ]
1025+ public void SupplementaryCharFunctions_ResultByteContent ( string expression , string expectedHex )
1026+ {
1027+ var result = ( string ) new Simulation ( ) . ExecuteScalar ( $ "select { expression } ") ! ;
1028+ var actualHex = AsUtf16LeHex ( result ) ;
1029+ AreEqual ( expectedHex , actualHex ) ;
1030+ }
1031+
1032+ /// <summary>Renders a .NET string as UTF-16 LE byte hex, preserving lone surrogates.</summary>
1033+ private static string AsUtf16LeHex ( string s )
1034+ {
1035+ var bytes = new byte [ s . Length * 2 ] ;
1036+ for ( var i = 0 ; i < s . Length ; i ++ )
1037+ {
1038+ bytes [ i * 2 ] = ( byte ) ( s [ i ] & 0xFF ) ;
1039+ bytes [ ( i * 2 ) + 1 ] = ( byte ) ( ( s [ i ] >> 8 ) & 0xFF ) ;
1040+ }
1041+ return Convert . ToHexString ( bytes ) ;
1042+ }
1043+
9821044 private static void IsNull ( object ? value ) => Microsoft . VisualStudio . TestTools . UnitTesting . Assert . IsNull ( value is DBNull ? null : value ) ;
9831045}
0 commit comments