@@ -86,16 +86,22 @@ public virtual bool Execute(string sql, ImmutableArray<SqlParameter> members = d
8686 {
8787 Flags |= SqlParseOutputFlags . SqlAdjustedForDapperSyntax ;
8888 }
89+
90+ var sqlProcessingContext = new SqlProcessingContext ( _visitor , fixedSql ) ;
91+
92+ var memberNamesSet = new HashSet < string > ( members . Select ( x => x . Name ) , CaseSensitive ? StringComparer . InvariantCulture : StringComparer . InvariantCultureIgnoreCase ) ;
93+ sqlProcessingContext . MarkPseudoPositionalVariablesUsed ( memberNamesSet ) ;
94+
8995 var parser = new TSql160Parser ( true , SqlEngineType . All ) ;
9096 TSqlFragment tree ;
9197 using ( var reader = new StringReader ( fixedSql ) )
9298 {
9399 tree = parser . Parse ( reader , out var errors ) ;
94100 if ( errors is not null && errors . Count != 0 )
95101 {
96- Flags |= SqlParseOutputFlags . SyntaxError ;
97- foreach ( var error in errors )
102+ foreach ( var error in sqlProcessingContext . GetErrorsToReport ( errors ) )
98103 {
104+ Flags |= SqlParseOutputFlags . SyntaxError ;
99105 OnParseError ( error , new Location ( error . Line , error . Column , error . Offset , 0 ) ) ;
100106 }
101107 }
@@ -118,7 +124,7 @@ public virtual bool Execute(string sql, ImmutableArray<SqlParameter> members = d
118124 {
119125 if ( _visitor . KnownParameters ) OnVariableNotDeclared ( variable ) ;
120126 }
121- else if ( variable . IsUnconsumed && ! variable . IsTable && ! variable . IsOutputParameter )
127+ else if ( variable is { IsUnconsumed : true , IsTable : false , IsOutputParameter : false } )
122128 {
123129 if ( _visitor . AssignmentTracking ) OnVariableValueNotConsumed ( variable ) ;
124130 }
@@ -173,7 +179,10 @@ protected virtual void OnTableVariableUsedAsScalar(Variable variable)
173179 => OnError ( $ "Table variable { variable . Name } is used like a scalar", variable . Location ) ;
174180
175181 protected virtual void OnNullLiteralComparison ( Location location )
176- => OnError ( $ "Null literals should not be used in binary comparisons; prefer 'is null' and 'is not null'", location ) ;
182+ => OnError ( "Null literals should not be used in binary comparisons; prefer 'is null' and 'is not null'" , location ) ;
183+
184+ protected virtual void OnPseudoPositionalParameter ( Location location )
185+ => OnError ( "It is more like Dapper will incorrectly treat this literal as a pseudo-positional parameter" , location ) ;
177186
178187 private void OnSimplifyExpression ( Location location , int ? value )
179188 => OnSimplifyExpression ( location , value is null ? "null" : value . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
@@ -186,77 +195,77 @@ private void OnSimplifyExpression(Location location, decimal? value)
186195 } ) ;
187196
188197 protected virtual void OnDivideByZero ( Location location )
189- => OnError ( $ "Division by zero detected", location ) ;
198+ => OnError ( "Division by zero detected" , location ) ;
190199 protected virtual void OnTrivialOperand ( Location location )
191- => OnError ( $ "Operand makes this calculation trivial; it can be simplified", location ) ;
200+ => OnError ( "Operand makes this calculation trivial; it can be simplified" , location ) ;
192201 protected virtual void OnInvalidNullExpression ( Location location )
193- => OnError ( $ "Operation requires non-null operand", location ) ;
202+ => OnError ( "Operation requires non-null operand" , location ) ;
194203
195204 protected virtual void OnSimplifyExpression ( Location location , string value )
196205 => OnError ( $ "Expression can be simplified to '{ value } '", location ) ;
197206
198207 protected virtual void OnAdditionalBatch ( Location location )
199- => OnError ( $ "Multiple batches are not permitted", location ) ;
208+ => OnError ( "Multiple batches are not permitted" , location ) ;
200209
201210 protected virtual void OnGlobalIdentity ( Location location )
202- => OnError ( $ "@@identity should not be used; use SCOPE_IDENTITY() instead", location ) ;
211+ => OnError ( "@@identity should not be used; use SCOPE_IDENTITY() instead" , location ) ;
203212
204213 protected virtual void OnSelectScopeIdentity ( Location location )
205- => OnError ( $ "Consider OUTPUT INSERTED.yourid on the INSERT instead of SELECT SCOPE_IDENTITY()", location ) ;
214+ => OnError ( "Consider OUTPUT INSERTED.yourid on the INSERT instead of SELECT SCOPE_IDENTITY()" , location ) ;
206215
207216 protected virtual void OnExecComposedSql ( Location location )
208- => OnError ( $ "EXEC with composed SQL may be susceptible to SQL injection; consider EXEC sp_executesql with parameters", location ) ;
217+ => OnError ( "EXEC with composed SQL may be susceptible to SQL injection; consider EXEC sp_executesql with parameters" , location ) ;
209218
210219 protected virtual void OnTableVariableOutputParameter ( Variable variable )
211220 => OnError ( $ "Table variable { variable . Name } cannot be used as an output parameter", variable . Location ) ;
212221
213222 protected virtual void OnInsertColumnsNotSpecified ( Location location )
214- => OnError ( $ "Target columns for INSERT should be explicitly specified", location ) ;
223+ => OnError ( "Target columns for INSERT should be explicitly specified" , location ) ;
215224
216225 protected virtual void OnInsertColumnMismatch ( Location location )
217- => OnError ( $ "The columns specified in the INSERT do not match the source columns provided", location ) ;
226+ => OnError ( "The columns specified in the INSERT do not match the source columns provided" , location ) ;
218227 protected virtual void OnInsertColumnsUnbalanced ( Location location )
219- => OnError ( $ "The rows specified in the INSERT have differing widths", location ) ;
228+ => OnError ( "The rows specified in the INSERT have differing widths" , location ) ;
220229
221230 protected virtual void OnSelectStar ( Location location )
222- => OnError ( $ "SELECT columns should be explicitly specified", location ) ;
231+ => OnError ( "SELECT columns should be explicitly specified" , location ) ;
223232 protected virtual void OnSelectEmptyColumnName ( Location location , int column )
224233 => OnError ( $ "SELECT statement with missing column name: { column } ", location ) ;
225234 protected virtual void OnSelectDuplicateColumnName ( Location location , string name )
226235 => OnError ( $ "SELECT statement with duplicate column name: { name } ", location ) ;
227236 protected virtual void OnSelectAssignAndRead ( Location location )
228- => OnError ( $ "SELECT statement has both assignment and results", location ) ;
237+ => OnError ( "SELECT statement has both assignment and results" , location ) ;
229238
230239 protected virtual void OnDeleteWithoutWhere ( Location location )
231- => OnError ( $ "DELETE statement without WHERE clause", location ) ;
240+ => OnError ( "DELETE statement without WHERE clause" , location ) ;
232241 protected virtual void OnUpdateWithoutWhere ( Location location )
233- => OnError ( $ "UPDATE statement without WHERE clause", location ) ;
242+ => OnError ( "UPDATE statement without WHERE clause" , location ) ;
234243
235244 protected virtual void OnSelectSingleTopError ( Location location )
236- => OnError ( $ "SELECT for Single* should use TOP 2; if you do not need to test over-read, use First*", location ) ;
245+ => OnError ( "SELECT for Single* should use TOP 2; if you do not need to test over-read, use First*" , location ) ;
237246 protected virtual void OnSelectFirstTopError ( Location location )
238- => OnError ( $ "SELECT for First* should use TOP 1", location ) ;
247+ => OnError ( "SELECT for First* should use TOP 1" , location ) ;
239248 protected virtual void OnSelectSingleRowWithoutWhere ( Location location )
240- => OnError ( $ "SELECT for single row without WHERE or (TOP and ORDER BY)", location ) ;
249+ => OnError ( "SELECT for single row without WHERE or (TOP and ORDER BY)" , location ) ;
241250 protected virtual void OnSelectAggregateAndNonAggregate ( Location location )
242- => OnError ( $ "SELECT has mixture of aggregate and non-aggregate expressions", location ) ;
251+ => OnError ( "SELECT has mixture of aggregate and non-aggregate expressions" , location ) ;
243252 protected virtual void OnNonPositiveTop ( Location location )
244- => OnError ( $ "TOP literals should be positive", location ) ;
253+ => OnError ( "TOP literals should be positive" , location ) ;
245254 protected virtual void OnNonPositiveFetch ( Location location )
246- => OnError ( $ "FETCH literals should be positive", location ) ;
255+ => OnError ( "FETCH literals should be positive" , location ) ;
247256 protected virtual void OnNegativeOffset ( Location location )
248- => OnError ( $ "OFFSET literals should be non-negative", location ) ;
257+ => OnError ( "OFFSET literals should be non-negative" , location ) ;
249258 protected virtual void OnNonIntegerTop ( Location location )
250- => OnError ( $ "TOP literals should be integers", location ) ;
259+ => OnError ( "TOP literals should be integers" , location ) ;
251260 protected virtual void OnFromMultiTableMissingAlias ( Location location )
252- => OnError ( $ "FROM expressions with multiple elements should use aliases", location ) ;
261+ => OnError ( "FROM expressions with multiple elements should use aliases" , location ) ;
253262 protected virtual void OnFromMultiTableUnqualifiedColumn ( Location location , string name )
254263 => OnError ( $ "FROM expressions with multiple elements should qualify all columns; it is unclear where '{ name } ' is located", location ) ;
255264
256265 protected virtual void OnInvalidDatepartToken ( Location location )
257- => OnError ( $ "Valid datepart token expected", location ) ;
266+ => OnError ( "Valid datepart token expected" , location ) ;
258267 protected virtual void OnTopWithOffset ( Location location )
259- => OnError ( $ "TOP cannot be used when OFFSET is specified", location ) ;
268+ => OnError ( "TOP cannot be used when OFFSET is specified" , location ) ;
260269
261270
262271 internal readonly struct Location
@@ -364,7 +373,8 @@ private Variable(scoped in Variable source, Location location)
364373 public Variable WithLocation ( TSqlFragment node ) => new ( in this , node ) ;
365374 public Variable WithName ( string name ) => new ( in this , name ) ;
366375 }
367- class LoggingVariableTrackingVisitor : VariableTrackingVisitor
376+
377+ internal class LoggingVariableTrackingVisitor : VariableTrackingVisitor
368378 {
369379 private readonly Action < string > log ;
370380 public LoggingVariableTrackingVisitor ( SqlParseInputFlags flags , TSqlProcessor parser , Action < string > log ) : base ( flags , parser )
@@ -406,7 +416,8 @@ public override void Visit(TSqlFragment node)
406416 base . Visit ( node ) ;
407417 }
408418 }
409- class VariableTrackingVisitor : TSqlFragmentVisitor
419+
420+ internal class VariableTrackingVisitor : TSqlFragmentVisitor
410421 {
411422 // important note for anyone maintaining this;
412423 //
@@ -474,7 +485,19 @@ private bool SlowTryGetVariable(string name, out Variable variable)
474485 return false ;
475486 }
476487
477- private Variable SetVariable ( Variable variable ) => _variables [ variable . Name ] = variable ;
488+ internal Variable SetVariable ( Variable variable ) => _variables [ variable . Name ] = variable ;
489+
490+ internal bool TryGetByName ( string name , out Variable variable )
491+ {
492+ if ( _variables . TryGetValue ( name , out var value ) )
493+ {
494+ variable = value ;
495+ return true ;
496+ }
497+
498+ variable = default ;
499+ return false ;
500+ }
478501
479502 private readonly Dictionary < string , Variable > _variables ;
480503 private int batchCount ;
@@ -623,6 +646,32 @@ public override void Visit(BooleanComparisonExpression node)
623646 base . Visit ( node ) ;
624647 }
625648
649+ public override void Visit ( WhereClause whereClause )
650+ {
651+ base . Visit ( whereClause ) ;
652+ }
653+
654+ public override void Visit ( StringLiteral node )
655+ {
656+ foreach ( var token in node . ScriptTokenStream )
657+ {
658+ if ( token . TokenType is not TSqlTokenType . AsciiStringLiteral )
659+ {
660+ continue ;
661+ }
662+
663+ int firstAppearance ;
664+ if ( ( firstAppearance = token . Text . IndexOf ( '?' ) ) >= 0
665+ && token . Text . IndexOf ( '?' , firstAppearance + 1 ) >= 0 // if there are 2 appearances of `?`
666+ && CompiledRegex . PseudoPositional . IsMatch ( node . Value ) )
667+ {
668+ parser . OnPseudoPositionalParameter ( node ) ;
669+ }
670+ }
671+
672+ base . Visit ( node ) ;
673+ }
674+
626675 public override void ExplicitVisit ( QuerySpecification node )
627676 {
628677 var oldDemandAlias = _demandAliases ;
@@ -647,6 +696,11 @@ public override void ExplicitVisit(QuerySpecification node)
647696 }
648697 }
649698
699+ public override void Visit ( QueryExpression expression )
700+ {
701+ base . Visit ( expression ) ;
702+ }
703+
650704 public override void ExplicitVisit ( SelectSetVariable node )
651705 {
652706 Visit ( node ) ;
0 commit comments