11using System . Diagnostics ;
22using System . Diagnostics . CodeAnalysis ;
3+ using System . Globalization ;
4+ using System . Text ;
5+ using SqlServerSimulator . Parser . Expressions ;
36using SqlServerSimulator . Parser . Tokens ;
47using SqlServerSimulator . Storage ;
58
@@ -29,6 +32,98 @@ private protected BooleanExpression()
2932 {
3033 }
3134
35+ /// <summary>
36+ /// Renders this predicate into the normalized form SQL Server stores in
37+ /// <c>sys.indexes.filter_definition</c> for a filtered index — columns
38+ /// bracketed, numeric constants parenthesized, strings quoted, operators
39+ /// space-free (<c>[status]=(1)</c>), and <c>AND</c> / <c>IS [NOT] NULL</c> /
40+ /// <c>IN</c> uppercase-spaced — or returns <c>null</c> when the predicate
41+ /// falls outside the renderable filtered-predicate grammar (an <c>AND</c> of
42+ /// comparison / <c>IS [NOT] NULL</c> / <c>IN</c> over <c>column <op>
43+ /// constant</c>). Those excluded shapes — <c>OR</c>, <c>NOT</c>, <c>BETWEEN</c>,
44+ /// function calls — are exactly the ones real SQL Server rejects in a filtered
45+ /// index, so reporting <c>null</c> for them never hides a definition a real
46+ /// server would have stored. The whole predicate is wrapped in one outer pair
47+ /// of parens.
48+ /// </summary>
49+ internal string ? RenderFilterDefinition ( BatchContext batch )
50+ {
51+ var sb = new StringBuilder ( "(" ) ;
52+ if ( ! this . TryAppendFilterDefinition ( sb , batch ) )
53+ return null ;
54+ _ = sb . Append ( ')' ) ;
55+ return sb . ToString ( ) ;
56+ }
57+
58+ // Appends this predicate's canonical fragment to `sb`, or returns false when
59+ // the node isn't part of the renderable filtered-predicate grammar. Default:
60+ // not renderable (OR / NOT / DISTINCT FROM / EXISTS / BETWEEN / quantified).
61+ private protected virtual bool TryAppendFilterDefinition ( StringBuilder sb , BatchContext batch ) => false ;
62+
63+ // Renders one comparison / IN operand: a single-part column reference as
64+ // [name], or an otherwise constant-foldable side as its literal. Returns
65+ // false for anything that isn't a bare column or a constant.
66+ private static bool TryAppendFilterOperand ( StringBuilder sb , Expression operand , BatchContext batch )
67+ {
68+ while ( operand is Parenthesized paren )
69+ operand = paren . Wrapped ;
70+
71+ if ( operand is Reference reference && reference . ReferencedName . Count == 1 )
72+ {
73+ _ = sb . Append ( '[' ) . Append ( reference . ReferencedName . Leaf ) . Append ( ']' ) ;
74+ return true ;
75+ }
76+
77+ SqlValue value ;
78+ try
79+ {
80+ value = operand . Run ( new RuntimeContext ( static _ => throw new NotSupportedException ( ) , batch ) ) ;
81+ }
82+ catch ( Exception ex ) when ( ex is SimulatedSqlException or NotSupportedException )
83+ {
84+ return false ;
85+ }
86+
87+ if ( FormatFilterLiteral ( value ) is not { } literal )
88+ return false ;
89+ _ = sb . Append ( literal ) ;
90+ return true ;
91+ }
92+
93+ // Formats a constant as SQL Server renders it inside filter_definition:
94+ // strings quoted (N-prefixed when the literal is national / Unicode), numerics
95+ // parenthesized in invariant culture preserving the literal's scale. Returns
96+ // null for a NULL literal or a type with no canonical filter rendering
97+ // (dates / binary / guid — rare in a filtered predicate), bailing the whole
98+ // render to a null definition.
99+ private static string ? FormatFilterLiteral ( SqlValue value )
100+ {
101+ if ( value . IsNull )
102+ return null ;
103+
104+ var type = value . Type ;
105+ if ( type . Category == SqlTypeCategory . String )
106+ {
107+ var prefix = type is NVarcharSqlType or NCharSqlType ? "N" : string . Empty ;
108+ return $ "{ prefix } '{ value . AsString . Replace ( "'" , "''" , StringComparison . Ordinal ) } '";
109+ }
110+
111+ var text = type switch
112+ {
113+ _ when type == SqlType . Int32 => value . AsInt32 . ToString ( CultureInfo . InvariantCulture ) ,
114+ _ when type == SqlType . BigInt => value . AsInt64 . ToString ( CultureInfo . InvariantCulture ) ,
115+ _ when type == SqlType . SmallInt => value . AsInt16 . ToString ( CultureInfo . InvariantCulture ) ,
116+ _ when type == SqlType . TinyInt => value . AsByte . ToString ( CultureInfo . InvariantCulture ) ,
117+ _ when type == SqlType . Bit => value . AsBoolean ? "1" : "0" ,
118+ _ when type == SqlType . Money || type == SqlType . SmallMoney => value . AsMoney . ToString ( "F4" , CultureInfo . InvariantCulture ) ,
119+ _ when type == SqlType . Float => value . AsDouble . ToString ( "G15" , CultureInfo . InvariantCulture ) ,
120+ _ when type == SqlType . Real => value . AsSingle . ToString ( "G7" , CultureInfo . InvariantCulture ) ,
121+ DecimalSqlType d => value . AsDecimal . ToString ( $ "F{ d . scale } ", CultureInfo . InvariantCulture ) ,
122+ _ => null ,
123+ } ;
124+ return text is null ? null : $ "({ text } )";
125+ }
126+
32127 /// <summary>
33128 /// Parses a full boolean predicate using SQL Server's standard
34129 /// precedence: <c>OR</c> binds loosest, then <c>AND</c>, then <c>NOT</c>,
@@ -601,6 +696,14 @@ internal override void CollectConjuncts(List<BooleanExpression> sink)
601696 left . CollectConjuncts ( sink ) ;
602697 right . CollectConjuncts ( sink ) ;
603698 }
699+
700+ private protected override bool TryAppendFilterDefinition ( StringBuilder sb , BatchContext batch )
701+ {
702+ if ( ! left . TryAppendFilterDefinition ( sb , batch ) )
703+ return false ;
704+ _ = sb . Append ( " AND " ) ;
705+ return right . TryAppendFilterDefinition ( sb , batch ) ;
706+ }
604707 }
605708
606709 /// <summary>
@@ -685,6 +788,14 @@ private sealed class IsNullExpression(Expression source, bool negated) : Boolean
685788 internal override string DebugDisplay ( ) => $ "{ source . DebugDisplay ( ) } IS { ( negated ? "NOT NULL" : "NULL" ) } ";
686789
687790 internal override void VisitOperandExpressions ( Action < Expression > visitor ) => visitor ( source ) ;
791+
792+ private protected override bool TryAppendFilterDefinition ( StringBuilder sb , BatchContext batch )
793+ {
794+ if ( ! TryAppendFilterOperand ( sb , source , batch ) )
795+ return false ;
796+ _ = sb . Append ( negated ? " IS NOT NULL" : " IS NULL" ) ;
797+ return true ;
798+ }
688799 }
689800
690801 /// <summary>
@@ -766,6 +877,24 @@ internal override void VisitOperandExpressions(Action<Expression> visitor)
766877 visitor ( candidate ) ;
767878 }
768879
880+ private protected override bool TryAppendFilterDefinition ( StringBuilder sb , BatchContext batch )
881+ {
882+ // NOT IN isn't part of the filtered-index grammar (real SQL Server
883+ // rejects it), so only the positive form renders.
884+ if ( negated || candidates . Length == 0 || ! TryAppendFilterOperand ( sb , source , batch ) )
885+ return false ;
886+ _ = sb . Append ( " IN (" ) ;
887+ for ( var i = 0 ; i < candidates . Length ; i ++ )
888+ {
889+ if ( i > 0 )
890+ _ = sb . Append ( ", " ) ;
891+ if ( ! TryAppendFilterOperand ( sb , candidates [ i ] , batch ) )
892+ return false ;
893+ }
894+ _ = sb . Append ( ')' ) ;
895+ return true ;
896+ }
897+
769898 // `source IN (c1, c2, ...)` is logically `source=c1 OR source=c2 OR ...`,
770899 // so for the equality-seek path it decomposes into one pair per candidate
771900 // against the shared LHS. NOT IN doesn't decompose into positive
@@ -1029,6 +1158,19 @@ internal override void VisitOperandExpressions(Action<Expression> visitor)
10291158 visitor ( this . left ) ;
10301159 visitor ( this . right ) ;
10311160 }
1161+
1162+ // The space-free operator token this comparison renders into a filtered-
1163+ // index definition (=, <>, >, >=, <, <=), or null for shapes with no
1164+ // canonical filter rendering (LIKE) — those bail the whole render.
1165+ protected virtual string ? FilterOperator => null ;
1166+
1167+ private protected override bool TryAppendFilterDefinition ( StringBuilder sb , BatchContext batch )
1168+ {
1169+ if ( this . FilterOperator is not { } op || ! TryAppendFilterOperand ( sb , this . left , batch ) )
1170+ return false ;
1171+ _ = sb . Append ( op ) ;
1172+ return TryAppendFilterOperand ( sb , this . right , batch ) ;
1173+ }
10321174 }
10331175
10341176 /// <summary>
@@ -1078,6 +1220,8 @@ private sealed class EqualityExpression(Expression left, Expression right) : Com
10781220
10791221 internal override string DebugDisplay ( ) => $ "{ left . DebugDisplay ( ) } = { right . DebugDisplay ( ) } ";
10801222
1223+ protected override string ? FilterOperator => "=" ;
1224+
10811225 internal override bool TryGetEqualityOperands ( [ NotNullWhen ( true ) ] out Expression ? l , [ NotNullWhen ( true ) ] out Expression ? r )
10821226 {
10831227 l = left ;
@@ -1092,6 +1236,8 @@ private sealed class InequalityExpression(Expression left, Expression right) : C
10921236 ComparePromoted ( left , right , runtime , "not equal to" , static ( l , r ) => ! l . Equals ( r ) ) ;
10931237
10941238 internal override string DebugDisplay ( ) => $ "{ left . DebugDisplay ( ) } <> { right . DebugDisplay ( ) } ";
1239+
1240+ protected override string ? FilterOperator => "<>" ;
10951241 }
10961242
10971243 private sealed class GreaterThanExpression ( Expression left , Expression right ) : CompareExpression ( left , right )
@@ -1101,6 +1247,8 @@ private sealed class GreaterThanExpression(Expression left, Expression right) :
11011247
11021248 internal override string DebugDisplay ( ) => $ "{ left . DebugDisplay ( ) } > { right . DebugDisplay ( ) } ";
11031249
1250+ protected override string ? FilterOperator => ">" ;
1251+
11041252 internal override bool TryGetRangeOperands ( [ NotNullWhen ( true ) ] out Expression ? l , out RangeComparison op , [ NotNullWhen ( true ) ] out Expression ? r )
11051253 {
11061254 ( l , op , r ) = ( left , RangeComparison . Greater , right ) ;
@@ -1115,6 +1263,8 @@ private sealed class GreaterThanOrEqualExpression(Expression left, Expression ri
11151263
11161264 internal override string DebugDisplay ( ) => $ "{ left . DebugDisplay ( ) } >= { right . DebugDisplay ( ) } ";
11171265
1266+ protected override string ? FilterOperator => ">=" ;
1267+
11181268 internal override bool TryGetRangeOperands ( [ NotNullWhen ( true ) ] out Expression ? l , out RangeComparison op , [ NotNullWhen ( true ) ] out Expression ? r )
11191269 {
11201270 ( l , op , r ) = ( left , RangeComparison . GreaterOrEqual , right ) ;
@@ -1129,6 +1279,8 @@ private sealed class LessThanExpression(Expression left, Expression right) : Com
11291279
11301280 internal override string DebugDisplay ( ) => $ "{ left . DebugDisplay ( ) } < { right . DebugDisplay ( ) } ";
11311281
1282+ protected override string ? FilterOperator => "<" ;
1283+
11321284 internal override bool TryGetRangeOperands ( [ NotNullWhen ( true ) ] out Expression ? l , out RangeComparison op , [ NotNullWhen ( true ) ] out Expression ? r )
11331285 {
11341286 ( l , op , r ) = ( left , RangeComparison . Less , right ) ;
@@ -1143,6 +1295,8 @@ private sealed class LessThanOrEqualExpression(Expression left, Expression right
11431295
11441296 internal override string DebugDisplay ( ) => $ "{ left . DebugDisplay ( ) } <= { right . DebugDisplay ( ) } ";
11451297
1298+ protected override string ? FilterOperator => "<=" ;
1299+
11461300 internal override bool TryGetRangeOperands ( [ NotNullWhen ( true ) ] out Expression ? l , out RangeComparison op , [ NotNullWhen ( true ) ] out Expression ? r )
11471301 {
11481302 ( l , op , r ) = ( left , RangeComparison . LessOrEqual , right ) ;
0 commit comments