Skip to content

Commit 681bf4d

Browse files
author
MPCoreDeveloper
committed
perf(net11): reuse cached tokenized Parts in SqlParser hot paths (#4 - tokenizer cache-reuse)
Execute / ExecuteQuery / ExecuteQuery(noEncrypt) now reuse the query cache's tokenized Parts for non-parameterized queries instead of re-running Trim().Split() (string[] + one string per token) on every call. Parameterized queries keep re-tokenizing the bound text (values differ per call). The Parts array is never mutated, so sharing is safe.\n\nMeasured (single-file point lookup, repeated identical SQL): 3,212 -> 2,447 B/op (-24% allocations), throughput ~19% higher. Validated: 1,509/1,509 tests green.
1 parent 524c611 commit 681bf4d

1 file changed

Lines changed: 40 additions & 29 deletions

File tree

‎src/SharpCoreDB/Services/SqlParser.Core.cs‎

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)