Skip to content

Fix parameters: rewrite @name placeholders in linear time - #697

Open
polyglotAI-bot wants to merge 2 commits into
mainfrom
polyglot/at-placeholder-linear-rewrite
Open

polyglotAI-bot wants to merge 2 commits into
mainfrom
polyglot/at-placeholder-linear-rewrite

Conversation

@polyglotAI-bot

@polyglotAI-bot polyglotAI-bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #696.

Root cause. SqlPlaceholderRewriter.ReplacePlaceholders scans the SQL once. At every @ in a code position, MatchPlaceholder compared the text with every key of replacements, longest first. In a multi-row parameterized INSERT, both the count of @ characters and the count of parameters grow with the row count, so the rewrite took O(rows²) time. Each call also sorted all keys again.

Fix. At a @, read the whole run of IsWordChar characters after it and look up "@" + name once in the dictionary. The rewrite is now one pass. The longest-first sort is removed: @id cannot shadow @id_2, because the name is always the full run.

Behavior.

  • For a parameter name made only of word characters (letters, digits, _, $), the result is the same as before. The old code matched a key only when the next character was not a word character. For such a name, that is exactly the full run.
  • On a miss, the scanner still skips only the @, so a $ right after it can still open a heredoc.
  • The dictionary is the same one as before (new Dictionary<string, string>(), ordinal and case-sensitive), so case handling did not change.
  • A name that contains another character (., -, …) no longer matches. The server does not accept such a name: on 26.9, SELECT {a.b:Int32} fails with SYNTAX_ERROR ("Expected colon between name and type"). Before, @a.b was rewritten into that rejected form. Now @a.b is left as is, and the server rejects the @. If a parameter a also exists, @a.b is now read as @a followed by .b, the same way the server lexes a.b.

Changes

  • ClickHouse.Driver/ADO/Parameters/SqlPlaceholderRewriter.cs: one dictionary lookup per @ in place of the linear key scan, and no per-call sort.
  • ClickHouse.Driver.Tests/ADO/SqlPlaceholderRewriteTests.cs:
    • ReplacePlaceholders_MultiRowInsertWithManyParameters_RewritesAllQuickly: a 4,000-row x 18-column INSERT (72,000 parameters, with names such as p1/p10/p100 that are prefixes of each other). It checks the full rewritten SQL against an expected string built independently, and that the rewrite finishes in less than 3 s.
    • One Placeholders case, heredoc after an unknown placeholder's at sign. It pins the miss path: if a miss skipped the whole name, this case would fail.
  • changelog.d/696-placeholder-rewrite-speed.improvements.md.

Test

  • New test on main: fails, the rewrite took 28.9 s against the 3 s budget (the output itself was correct). With the fix: passes, about 0.2 s in isolation (0.26 s with coverlet).
  • The other 85 SqlPlaceholderRewriteTests cases, the new contrast case included, give the same results on main and on this branch.
  • dotnet test ClickHouse.Driver.Tests --framework net10.0 --filter "FullyQualifiedName~Parameter|FullyQualifiedName~Placeholder|FullyQualifiedName~Dapper|FullyQualifiedName~RowLimit|FullyQualifiedName~Sql": 4,559 passed, 0 failed, against a live 26.9 server. No existing test was changed.

BenchmarkDotNet, ad-hoc and not committed. ClickHouseParameterCollection.ReplacePlaceholders, net10.0, ShortRun:

Shape main This PR Allocated (main → PR)
WHERE with 3 params 1.702 µs 1.348 µs 1.63 KB → 1.33 KB
1 row x 18 params 8.991 µs 5.682 µs 5.83 KB → 5.65 KB
100 rows x 18 params 13,962 µs 528 µs 398 KB → 418 KB
1,000 rows x 18 params 1,572,011 µs 12,674 µs 5,032 KB → 5,306 KB

The new code allocates one Substring per @. Small queries allocate less than before, because the sort buffers are gone. Large ones allocate about 5% more. A ReadOnlySpan<char> alternate lookup could remove the substring on .NET 9+ only, but these numbers do not justify the extra #if.

Checklist

  • Unit and integration tests covering the common scenarios were added
  • A human-readable description of the changes was provided to include in CHANGELOG

Pre-PR validation gate

  • Deterministic repro: the new test fails on main every run (rewrite takes seconds instead of milliseconds)
  • Root cause documented above
  • Fix targets the root cause (per-@ linear key scan → one dictionary lookup)
  • Test fails without the fix and passes with it
  • No existing tests changed or broken
  • Fix is on the live path: both ClickHouseClient.BuildHttpRequestMessageWithQueryParams and ...WithFormData call ClickHouseParameterCollection.ReplacePlaceholders
  • No public API change. The TCP client has no @name rewrite.

Related: ClickHouse/clickhouse-java#3249 has the same symptom in clickhouse-java, from a different mechanism.

🤖 Generated with Claude Code


Note

Medium Risk
Touches the live SQL rewrite path for every @name query; behavior is unchanged for typical names but differs for placeholders containing . or -.

Overview
Fixes quadratic slowdown when rewriting many @name placeholders (e.g. large multi-row INSERTs) by replacing per-@ scans over all parameter keys—and the per-call longest-first sort—with a single dictionary lookup after reading the full placeholder name.

Matching now treats the name as the contiguous run of letters, digits, _, and $ after @. That preserves normal names like id / id_2 / id$x while changing edge cases: @a.b is no longer rewritten to a rejected {a.b:…} form (it becomes @a plus .b when a exists, or is left for the server to reject). On an unknown @, only the @ is consumed so a following $ can still start a heredoc.

Adds a 72k-parameter timing regression test, a heredoc-after-miss case, changelog entry, and HTTP docs for placeholder naming.

Reviewed by Cursor Bugbot for commit c9de2e7. Bugbot is set up for automated code reviews on this repo. Configure here.

At every @ in a code position, SqlPlaceholderRewriter compared the text
with each placeholder key in turn, longest first. A multi-row INSERT has
as many @ characters as parameters, so the rewrite took time quadratic
in the row count (4,000 rows x 18 columns: about 24 s).

Read the whole run of word characters after the @ and look it up once
in the replacements dictionary. A name made only of word characters
gets the same result as before. On a miss the scanner still skips only
the @. The per-call longest-first sort is no longer needed and is gone.

Fixes: #696

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 13:51
@codecov

codecov Bot commented Oct 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The shared HTTP query path changes token matching, and validation across all supported target frameworks remains unconfirmed.

1 open issue
What changed in this PR

This PR addresses #696 by removing quadratic @name placeholder matching from the HTTP client’s parameterized-query path. The TCP client is unchanged.

Changes:

  • Replaces sorted key scanning with whole-name parsing and a dictionary lookup.
  • Adds a 72,000-parameter regression test and coverage for heredoc handling after an unmatched @.
  • Adds a changelog fragment describing the performance improvement.
File Description
ClickHouse.Driver/​ADO/​Parameters/​SqlPlaceholderRewriter.cs Uses direct lookup instead of scanning every parameter name.
ClickHouse.Driver.Tests/​ADO/​SqlPlaceholderRewriteTests.cs Adds large-query performance and heredoc regression coverage.
changelog.d/​696-placeholder-rewrite-speed.improvements.md Records the placeholder-rewriting speed improvement.

Review effort: Balanced

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Quadratic @name placeholder rewriting makes large multi-row parameterized INSERTs very slow

2 participants