@@ -140,29 +140,27 @@ public void Execute(string sql, Dictionary<string, object?> parameters, IWAL? wa
140140 originalSql = sql ;
141141 sql = SqlParser . BindParameters ( sql , parameters ) ;
142142 }
143- // REMOVED: SanitizeSql was breaking string literals by doubling ALL quotes including delimiters
144- // For queries without parameters, we trust the input SQL as-is
145- // SQL injection protection should be handled at the application layer via parameterized queries
146143
147- // Use query cache if available
148- string cacheKey = originalSql ?? sql ;
149144 string [ ] parts ;
150-
151145 if ( this . queryCache != null )
152146 {
153- // Cache the structure - for parameterized queries, still track cache hits
154- this . queryCache . GetOrAdd ( cacheKey , key =>
155- {
156- var parsedParts = sql . Trim ( ) . Split ( [ ' ' , '\t ' , '\r ' , '\n ' ] , StringSplitOptions . RemoveEmptyEntries ) ;
157- return new QueryCache . CachedQuery
147+ // PERF: reuse the cache's tokenized Parts on the hot path. For non-parameterized
148+ // queries the cache key IS the SQL text, so the cached Parts are exactly the
149+ // tokens of `sql` — this avoids a per-call Trim().Split() (string[] + one string
150+ // per token). Parameterized queries embed distinct values into the text per call,
151+ // so the cached entry (keyed by the unbound SQL) may hold a different binding's
152+ // tokens — in that case the bound text is tokenized here (as before).
153+ var entry = this . queryCache . GetOrAdd ( originalSql ?? sql , key =>
154+ new QueryCache . CachedQuery
158155 {
159- Sql = cacheKey ,
160- Parts = parsedParts ,
156+ Sql = key ,
157+ Parts = sql . Trim ( ) . Split ( [ ' ' , ' \t ' , ' \r ' , ' \n ' ] , StringSplitOptions . RemoveEmptyEntries ) ,
161158 CachedAt = DateTime . UtcNow
162- } ;
163- } ) ;
164- // Always parse the bound SQL since it contains the actual values
165- parts = sql . Trim ( ) . Split ( [ ' ' , '\t ' , '\r ' , '\n ' ] , StringSplitOptions . RemoveEmptyEntries ) ;
159+ } ) ;
160+
161+ parts = originalSql is null
162+ ? entry . Parts
163+ : sql . Trim ( ) . Split ( [ ' ' , '\t ' , '\r ' , '\n ' ] , StringSplitOptions . RemoveEmptyEntries ) ;
166164 }
167165 else
168166 {
@@ -200,16 +198,23 @@ public void Execute(CachedQueryPlan plan, Dictionary<string, object?> parameters
200198 /// <returns>The query results.</returns>
201199 public List < Dictionary < string , object > > ExecuteQuery ( string sql , Dictionary < string , object ? > ? parameters = null )
202200 {
203- if ( parameters != null && parameters . Count > 0 )
201+ bool hasParams = parameters is { Count : > 0 } ;
202+ if ( hasParams )
204203 {
205204 sql = SqlParser . BindParameters ( sql , parameters ) ;
206205 }
207- // REMOVED: SanitizeSql was breaking string literals by doubling ALL quotes including delimiters
208- // For queries without parameters, we trust the input SQL as-is
209- // SQL injection protection should be handled at the application layer via parameterized queries
210-
211206 // ✅ CRITICAL FIX: Split on ALL whitespace (space, tab, newline, CR, etc.) to handle multi-line SQL correctly.
212- var parts = sql . Trim ( ) . Split ( [ ' ' , '\t ' , '\r ' , '\n ' ] , StringSplitOptions . RemoveEmptyEntries ) ;
207+ // PERF: reuse the cache's tokenized Parts for non-parameterized queries (the cache key
208+ // is the SQL text itself); parameterized queries re-tokenize the bound text per call.
209+ string [ ] parts = this . queryCache is not null && ! hasParams
210+ ? this . queryCache . GetOrAdd ( sql , key =>
211+ new QueryCache . CachedQuery
212+ {
213+ Sql = key ,
214+ Parts = sql . Trim ( ) . Split ( [ ' ' , '\t ' , '\r ' , '\n ' ] , StringSplitOptions . RemoveEmptyEntries ) ,
215+ CachedAt = DateTime . UtcNow
216+ } ) . Parts
217+ : sql . Trim ( ) . Split ( [ ' ' , '\t ' , '\r ' , '\n ' ] , StringSplitOptions . RemoveEmptyEntries ) ;
213218 return this . ExecuteQueryInternal ( sql , parts ) ;
214219 }
215220
@@ -222,17 +227,23 @@ public List<Dictionary<string, object>> ExecuteQuery(string sql, Dictionary<stri
222227 /// <returns>The query results.</returns>
223228 public List < Dictionary < string , object > > ExecuteQuery ( string sql , Dictionary < string , object ? > parameters , bool noEncrypt )
224229 {
225- if ( parameters != null && parameters . Count > 0 )
230+ bool hasParams = parameters is { Count : > 0 } ;
231+ if ( hasParams )
226232 {
227233 sql = SqlParser . BindParameters ( sql , parameters ) ;
228234 }
229- // REMOVED: SanitizeSql was breaking string literals by doubling ALL quotes including delimiters
230- // For queries without parameters, we trust the input SQL as-is
231- // SQL injection protection should be handled at the application layer via parameterized queries
232235 // ✅ CRITICAL FIX: Split on ALL whitespace (space, tab, newline, CR, etc.) to handle multi-line SQL correctly.
233236 // EF Core and other callers may pass SQL with newlines, which must be tokenized properly.
234-
235- var parts = sql . Trim ( ) . Split ( [ ' ' , '\t ' , '\r ' , '\n ' ] , StringSplitOptions . RemoveEmptyEntries ) ;
237+ // PERF: reuse the cache's tokenized Parts for non-parameterized queries (see ExecuteQuery above).
238+ string [ ] parts = this . queryCache is not null && ! hasParams
239+ ? this . queryCache . GetOrAdd ( sql , key =>
240+ new QueryCache . CachedQuery
241+ {
242+ Sql = key ,
243+ Parts = sql . Trim ( ) . Split ( [ ' ' , '\t ' , '\r ' , '\n ' ] , StringSplitOptions . RemoveEmptyEntries ) ,
244+ CachedAt = DateTime . UtcNow
245+ } ) . Parts
246+ : sql . Trim ( ) . Split ( [ ' ' , '\t ' , '\r ' , '\n ' ] , StringSplitOptions . RemoveEmptyEntries ) ;
236247 return this . ExecuteQueryInternal ( sql , parts , noEncrypt ) ;
237248 }
238249
0 commit comments