You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Parameterised Queries: Interactive SQL Query Parameters in SQL Console #2944
When running the same SQL query repeatedly with different values in the WHERE clause, I currently have to edit the SQL each time to change the value.
For example, changing:
WHERE id =42;
to:
WHERE id =43;
every time I want to query a different record becomes tedious, especially for longer or more complex queries.
Concrete Use Case
It would be useful to define a parameter directly in the SQL:
SELECT
report_name,
report_type
FROMfineract_default.stretchy_reportWHERE id = :id;
When the query is executed, Chat2DB could detect :id and prompt the user to provide its value:
id: [ 42 ]
The same query could then be executed again with a different value without modifying the SQL.
This could also support multiple parameters:
SELECT*FROM some_table
WHERE client_id = :client_id
AND status = :status;
with Chat2DB prompting for both values when the query is run.
Product Area
SQL editor
Proposed Outcome
Add support for interactive SQL query parameters in the SQL editor.
When a query contains parameters, Chat2DB should:
Detect the parameters in the query.
Prompt the user for their values when the query is executed.
Bind the supplied values to the query.
Execute the query without requiring the SQL itself to be modified.
Named parameters such as :id would be particularly useful, although support for other parameter syntaxes could also be considered depending on the database/driver.
Alternatives Considered
No response
Contribution
I can help test the change
Submission Checklist
I searched existing issues and Discussions for duplicates.
Thanks for the detailed write-up — the use case is clear.
We mapped where this would land, and it's a real feature rather than a small fix: the console sends the statement text untouched, the backend splits/rewrites it and executes via PreparedStatement with nothing bound, and there is no existing variable or parameter mechanism on the execution path. It would need a way to carry parameter values in the execute request, a binding step at execution time, and a new prompt dialog in the console — plus care around result paging, which re-executes the original SQL text.
Would you be interested in contributing this yourself? We can share the insertion points we identified and review a PR. If not, no problem at all — we'll keep it on the list; knowing your interest just helps us plan.
Hi @openai0229, thanks for the pointers on where this would land. I've built it on a local branch, and I'd like your go-ahead before opening the PR.
My /claim didn't register, probably because the issue doesn't have a contribution/help-wanted label yet. Could you add it so I can claim it? This change also adds a field to the execute API, which the contribution boundaries say needs design approval, so here's the approach.
How it works
Detection: when you run SQL in the console, it looks for :name and ? placeholders. Anything inside string literals, quoted identifiers or comments is ignored, and the lexical rules follow each database's dialect. Only the SQL actually being run is scanned, so a selection only prompts for its own placeholders.
The dialogue: a dialogue asks for each value. A name used more than once gets one input, and NULL has to be chosen explicitly. An empty value is never turned into NULL.
Binding: the values go in a new optional parameters field on the execute request. The backend rewrites :name to ? and binds the values through PreparedStatement, so they never become part of the SQL text. It's the same execution path as today, not a second pipeline.
Paging: results keep the SQL as written, so paging and refresh run it again with the same values.
Rejected with clear errors: mixing :name and ? in one statement; parameters in a script with more than one statement; MongoDB and Redis; and SQL Server GO batches.
PostgreSQL: values are bound untyped, so WHERE id = :id works against an integer column.
Other fixes: I also fixed three places that broke on placeholders: SQL formatting (it turned id = :id into id =: id), the editor syntax check, and statement splitting. The syntax-check messages were hard-coded in Chinese, so I moved them into the existing i18n files.
Compatibility: requests without parameters behave exactly as before, and SQL without placeholders never shows the dialogue.
Tests:
New backend tests run real queries against an in-memory H2 database. They check that values reach the database through the driver and never through the SQL text, including an injection-style value.
There are also parser tests, plus a frontend test that runs as part of the existing build checks.
The focused backend tests, frontend lint, i18n validation and the Community web build all pass.
Known limitations:
Kingbase and Redshift parameter binding hasn't been tested against a real database.
The dialogue requires a value or NULL, so an empty string can't be bound.
If this direction works for you, I'll open the PR with Closes #2944 and request a review from @Aias00. Happy to change anything first.
AI Disclosure:
This was built with AI assistance; I've reviewed and tested it myself
Thanks for the detailed write-up, and for building the change before opening the PR — adding a field to the execute API is an interface change, so asking for design sign-off first is exactly the right order.
Short answer: we want this feature and your direction is right. There are five points we would like to settle first; everything else can follow your approach. The full design baseline is in the collapsed block at the end.
What we agree with
Values reach the database through PreparedStatement binding and never become part of the SQL text.
The same execution path as today, not a second pipeline.
Results keep the SQL as written, so paging and refresh re-run it with the same values.
Requests without parameters behave exactly as before, and SQL without placeholders never shows a dialog.
The three placeholder breakages you fixed (the formatter turning id = :id into id =: id, the editor syntax check, statement splitting) and moving the hard-coded syntax messages into i18n are all in scope. Please keep them as separate commits so we can review them one by one.
?: agreed, with two constraints
We originally planned :name only. Having seen your implementation we are happy to include ? as well, but let's fix its shape:
A statement uses either :name or ?, never both, and mixing them is an error — keep your rule.
? must be normalised during scanning into synthetic named parameters (__p1, __p2, … left to right) before policy, statement splitting, paging and COUNT rewriting, and turned back into ? by index just before JDBC.
Why the second one: named parameters survive rewriting by name, positional parameters survive by index, and indexes do not survive. COUNT rewriting drops the projection and ORDER BY, so the original first ? may not exist in the new statement at all, and binding in the original order is guaranteed to be wrong. After normalisation there is exactly one parameter kind downstream, and its identity is stable across rewrites.
Two things that come with it:
Request shape: parameters (map, for :name) plus positionalParameters (array, for ?); the dialog shows Parameter 1..N for ?; both syntaxes need a user-selected type.
PostgreSQL: ?| and ?& are always operators, never placeholders. Binding a bare ? operator through a prepared statement is a long-standing PgJDBC limitation and Chat2DB already goes through prepareStatement, so this is not a regression — we will point users at jsonb_exists() or the :name form.
Five points to change or confirm before the PR
Add a default-off switch (Editor Settings → Execution group; request field sqlParameters: true). A new syntax affects everybody, and off-by-default is what makes a staged rollout possible. It also has to gate detection only, not binding: otherwise a user who turns the switch off loses the values of an already-open result on paging or COUNT.
The empty string must be bindable. NULL is already an explicit choice in the dialog, so an empty input can unambiguously mean an empty string; today WHERE name = '' cannot be expressed. You list this as a known limitation and it looks like a small fix — we would rather fix it in this PR.
Confirm the COUNT path./api/rdb/dml/count re-executes the statement, so working paging does not imply working COUNT. Please confirm the count request carries the same values, including the fallback that counts the original query row by row when the COUNT rewrite fails.
Reject parameterised result export before the task is created. Export is an async task that re-runs the SQL. We are not supporting parameterised export in this round, so it should be rejected before the task exists rather than dropping the values and creating a task that can only fail.
Carry a type with each value:{ "type": "...", "value": "..." }. Once the API ships, changing the field shape is a breaking change, so the shape should be right from day one. Untyped binding also relies on driver implicit conversion — Oracle DATE is affected by the NLS format. v1 only needs STRING, NUMBER, BOOLEAN and NULL; DATE, TIME and TIMESTAMP can follow.
Fine as-is
Rejecting parameters in multi-statement scripts: fine, as long as the error is clear and it is documented as a limitation. It also satisfies our hard constraint that the first statement must never execute before we know the second one is fully parameterised.
MongoDB, Redis and SQL Server GO batches rejected: fine.
One input per name, a repeated name asked once, NULL chosen explicitly: keep.
Kingbase and Redshift untested against a real database: fine — please state the unverified scope in the PR description.
Suggestions, non-blocking
Please keep detection in one shared scanner, with dialects supplying only lexical boundary rules (strings, comments, escapes, native syntax). A per-dialect detection implementation means every new dialect re-implements detection and its test matrix. If time is short for this PR, at least make sure adding a dialect does not require rewriting detection.
Scanning only the SQL that is actually being executed — a selection only prompts for its own placeholders: your approach, keep it.
Process
I have added the contribution/help-wanted label, so /claim should register now.
Please open the PR with Closes #2944 and request a review from @Aias00.
Let's keep the design discussion in this issue; I will update the baseline here as we settle things.
The AI disclosure is fine — please keep it in the PR description too.
Next
The full design baseline is in the collapsed block below. If you disagree with any of the five points above, reply here and we will settle it before the PR.
Full design baseline — Community SQL Parameters, technical design (Markdown)
Community SQL Parameters — Technical Design
A user writes :name in the SQL console; on execute, they fill in a type and a value for each parameter. The server detects the parameters, validates them, rewrites the placeholders and binds the values before JDBC execution. The editor, the execution history and result replay always keep the parameterised SQL as written.
The frontend does not parse SQL, does not look for colons, and needs no parameter-analysis endpoint. Web and desktop both use the existing execute entry point. SQL parameters sit behind a default-off editor switch, so the rollout only affects users who turn it on.
Status: design baseline, not implemented. Related issue: #2944. Baseline: Community main @ dc2b80a3b0189bb3fd92d69ce8f52de2d4136ac8; file line numbers below refer to that commit.
1. Goal and scope
In scope: console execution, result paging and refresh, COUNT, and desktop streaming execution — all supporting :name named parameters and JDBC-style ? positional parameters.
Out of scope, and why:
Not doing
Reason
Table, column or ORDER BY direction parameters
These are identifier positions; JDBC can only bind values, and supporting them means changing the statement structure
Automatic IN-list expansion
It changes the statement structure and the parameter count — a separate feature
Procedure OUT parameters
They need CallableStatement semantics, which is not value binding
Parameterised result export
Export runs as a separate async task over its own JDBC path; parameter lifetime across task restart, retry and three export formats needs its own design
Frontend SQL parsing, parameter-analysis endpoint
An extra round trip and an extra piece of frontend state, plus a race between the analysis and the actual execution
A server-wide kill switch
See section 5
2. Key decisions
Decision
Choice
Why
Parameter syntax
One syntax for every database: :name, plus ? as positional parameters
One frontend, one backend and one test matrix; per-dialect syntax multiplies prompts, parameter tables and tests. Dialect differences stay at the lexical boundary
Positional parameters
? is mutually exclusive with :name and is normalised into synthetic named parameters during scanning
Positional indexes do not survive COUNT and paging rewrites — the projection and ORDER BY can be dropped — while a name travels with its expression
Detection
A dedicated character scanner reads the raw SQL
Each dialect's lexer defines the colon differently (Oracle bind variables, PostgreSQL ::, MySQL :=), and we cannot rely on error recovery to preserve the original text
When :name becomes ?
Kept until just before JDBC execution
COUNT and paging delete or reorder parameter references; only the final SQL determines the bind order. Replacing earlier loses parameter names in originalSql and sql
Missing-value feedback
The existing execute entry point returns a control result
No new endpoint, no extra round trip, and the frontend never needs to inspect SQL
Parameter types
A server-side enum of value kinds
For writes, the target type is decided by the column, the operator and any CAST; the user only needs to say what kind of value they typed
How parameters travel
Explicitly on the request and on the statement object
Thread-local context or a Connection proxy makes plugin branches, connection reuse and concurrency hard to reason about
Rollout
A switch in Editor Settings, off by default
The blast radius is limited to users who opt in, with no operations work
3. Syntax and detection
3.1 Rules
Parameter format: :name.
Names start with a letter or underscore and continue with letters, digits, underscores and $; Unicode is supported and names are case-sensitive.
Parameters are only used in value expressions: WHERE id = :id, VALUES (:name, :age), function arguments, ordinary scalar expressions.
A name used more than once is filled in once and bound once per occurrence.
:name inside string literals, quoted identifiers and comments is not a parameter.
::, :=, @variable, $1 and Oracle :NEW / :OLD are native syntax, not parameters.
? is a positional parameter, taking values left to right. It cannot appear in the same statement as :name; mixing the two is an explicit error.
PostgreSQL ?| and ?& are always operators, never placeholders.
The naming rules live in the scanner only; plugins do not re-implement detection, de-duplication or missing-value checks.
3.2 Scanning algorithm
The scanner tracks SQL code, string literals, quoted identifiers, line comments and block comments:
On entering a string, quoted identifier or comment, read to its end and skip any colons inside.
In ordinary SQL code, on a colon, exclude ::, := and dialect-native constructs.
Once a parameter start is confirmed, read the full name and record {name, start, end}.
Keep every occurrence; the dialog lists parameters by first appearance, de-duplicated.
Positions use Java UTF-16 offsets, the same coordinate system as substring; advancing over Unicode names goes by code point and does not reuse ANTLR's code point offsets.
Why not reuse the existing lexer: when a statement has a syntax error, the lexer does not guarantee a complete :id token, while parameter checking must locate parameters in any syntax state. The scanner only does lexical work and does not require a successful parse first.
3.3 Dialect boundaries
Dialects only supply the boundary rules for strings, comments, escapes and native syntax:
PostgreSQL: ::, dollar quotes, E-strings, nested comments, array slices and question-mark operators.
MySQL: -- comment conditions, #, backslash escapes and executable comments.
Oracle: q-quotes, :NEW, :OLD and PL/SQL blocks.
SQL Server and others: variables, batches and procedure syntax.
Looking at the single character before the colon is not enough: constructs such as ::, dollar quotes and q-quotes need context, and guessing turns native syntax into a parameter.
3.4 Division of labour with the parser
The scanner finds parameters; the existing parser handles statement splitting, statement types, object references, paging, COUNT and formatting. Parameters stay untouched through all of those stages.
The two responsibilities differ. Making the parser responsible for detection ties "can parse" to "can find parameters", and we cannot rely on parser error recovery to preserve the original text.
4. Request protocol and types
4.1 Fields
Three optional fields are added to the existing execute, paging and count requests: the sqlParameters switch, the parameters map for named parameters, and the positionalParameters array for ?:
json
{
"dataSourceId": 1,
"sql": "SELECT * FROM users WHERE id = :id AND status = :status",
"sqlParameters": true,
"parameters": {
"id": { "type": "NUMBER", "value": "42" },
"status": { "type": "STRING", "value": "active" }
}
}
The same statement written with ? uses the array, whose order is the appearance order:
json
{
"dataSourceId": 1,
"sql": "SELECT * FROM users WHERE id = ? AND status = ?",
"sqlParameters": true,
"positionalParameters": [
{ "type": "NUMBER", "value": "42" },
{ "type": "STRING", "value": "active" }
]
}
The two syntaxes are mutually exclusive: a statement uses either :name with parameters or ? with positionalParameters.
SqlParameterType and SqlParameterValue live together in the domain-api SQL model package. JSON only carries the enum name; deserialisation matches it strictly and an unknown value is a parameter validation error — it never falls back to STRING, and there is no case-insensitive or alias matching.
Why an enum instead of a string: an unknown type must fail rather than be silently treated as a string, and an enum keeps the bind call in a single switch.
4.2 Types and binding
Enum
Input
JDBC binding
STRING
Any string, including the empty string
setString
NUMBER
Decimal number text
Parsed to BigDecimal, then setBigDecimal
BOOLEAN
Strictly true or false
setBoolean
DATE
yyyy-MM-dd
setDate
TIME
HH:mm:ss[.SSS]
setTime
TIMESTAMP
yyyy-MM-dd HH:mm:ss[.SSS]
setTimestamp
NULL
JSON null
setNull; the target type is chosen by the plugin from the expression context, or the SQL casts explicitly
The type is chosen by the user and defaults to STRING; users never add SQL quotes. NULL, the empty string and "not filled in" are three distinct states. Date and time values only accept the formats above — no locale formats, no lenient parsing.
Why not the full java.sql.Types set: the JDBC target type for a parameter is decided by the column type, the operator and any CAST, and the user only needs to say what kind of value they typed. java.sql.Types has around 40 constants, including ARRAY, STRUCT, BLOB and REF, which cannot be expressed as JSON text, and setObject(i, value, targetSqlType) does not behave the same as the typed setters across drivers.
Why date and time are separate: binding date text with setString relies on implicit conversion, which differs between databases; a fixed format plus a fixed setter is testable.
parameterNames lists every distinct parameter name in this SQL, de-duplicated and ordered by first appearance; it never contains a user value. The frontend recognises it by errorCode and keeps the structured data — it does not parse error text.
Why not a separate parameter-analysis endpoint: the frontend would need an extra round trip and extra state, and it would have to handle the race between analysis and execution. Reusing the execute entry point means the frontend never inspects SQL.
The control result must be produced before the execution history, the executionId, any streaming task and any user SQL. Creating those side effects first leaves records for an execution that never happened.
5. Switch and rollout
SQL parameters are an editor-level switch, off by default, placed in Editor Settings under the execution group, next to "Error Continue" and "Default Page Size".
When it is off, behaviour is exactly as today: the frontend does not send the switch, the backend does not scan for colons, and :id goes to the database untouched. When it is on, the frontend sends sqlParameters: true with every execute request.
Request
Backend behaviour
Carries parameters or positionalParameters
Validate and bind, regardless of the switch
sqlParameters: true with no parameter values
Scan the whole script and return SQL_PARAMETERS_REQUIRED if values are missing
Anything else
No scanning at all; the existing execution flow runs unchanged
The switch only decides whether the new syntax is recognised; it does not decide whether existing parameters are bound. Otherwise turning the switch off would drop the values from an already-open result on paging or COUNT, causing an error or wrong data.
No server-side switch: off-by-default already limits the blast radius to users who opt in, while a server setting needs a deploy or a restart and adds nothing for a local web or desktop install.
The trade-off: because the frontend does not parse SQL, typing :name with the switch off produces no hint — the database reports its own native error.
Request field definitions and explicit pass-through
6. Execution flow
text
User clicks execute
→ frontend freezes SQL, data source and execution options
→ call the existing execute entry point
→ backend scans the whole SQL when the switch is on
no parameters: run the existing flow
parameters complete: validate, then continue
parameters missing: return SQL_PARAMETERS_REQUIRED, run no SQL
→ frontend shows the parameter dialog
→ user fills in a type and value per parameter
→ frontend submits the original SQL plus parameters to the same entry point
→ backend re-scans and re-validates the whole SQL
→ permission checks, policy, statement splitting, paging or COUNT rewriting
→ compile each final SQL, turn :name into ?, build the ordered bind list
→ PreparedStatement binds by type and executes
→ return results and keep the parameter snapshot
Web uses POST /api/rdb/dml/execute; desktop uses the existing JCEF sql-execute. The server stores no paused task and holds no database connection while waiting for user input.
The second submission is re-scanned and re-validated because the client payload cannot be trusted and because the data source, permissions and SQL can all change between the two requests.
The whole script is checked up front because with errorContinue=true a statement-by-statement run would commit the first UPDATE before discovering that the second statement is missing a value — an effect that cannot be rolled back.
Cancelling the dialog sends no second request, clears no existing result and writes no failure record. Network errors, timeouts and database errors never re-send the user's SQL: those failures do not mean the SQL is safe to replay, and a resend can duplicate a write.
7. SQL rewriting and JDBC binding
Before JDBC execution the binder scans the current final SQL and produces:
sql
SELECT id, name
FROM users
WHERE (id = :id OR parent_id = :id)
AND status = :status
AND remark <> ':keep';
JDBC SQL:
sql
SELECT id, name
FROM users
WHERE (id = ? OR parent_id = ?)
AND status = ?
AND remark <> ':keep';
Binding:
java
statement.setBigDecimal(1, new BigDecimal("42"));
statement.setBigDecimal(2, new BigDecimal("42"));
statement.setString(3, "active");
User values are never concatenated into SQL. jdbcSql stays in the JDBC layer: it is not returned to the frontend, not written to history and not used for result replay.
Positional ? parameters are normalised during scanning into synthetic named parameters (__p1, __p2, … left to right), so everything downstream sees a single parameter kind whose identity survives rewriting; just before JDBC they are turned back into ? by index. The two syntaxes are mutually exclusive, so synthetic names cannot collide with user names.
Each final SQL is compiled independently, and the bind order is that SQL's own parameter order — never the order from before a rewrite, and never another statement's. The bind call is dispatched by a single switch on SqlParameterType.
Why recompile per final SQL: COUNT rewriting drops the projection and ORDER BY while paging appends clauses, so the two statements do not reference the same parameter set; caching the original order binds values to the wrong positions.
8. Response fields and result snapshots
Field
Meaning
originalSql
The original parameterised SQL for this result
sql
The rewritten parameterised SQL for this result (paging or otherwise)
executeSqlParams
The frontend's replay request snapshot; not JDBC SQL
executeSqlParams contains at least:
json
{
"sql": "SELECT * FROM users WHERE id = :id",
"pageNo": 1,
"pageSize": 1000,
"parameters": {
"id": { "type": "NUMBER", "value": "42" }
}
}
Paging, refresh, filtering and COUNT reuse the same snapshot and rebuild the bind list for the current final SQL without prompting again. When COUNT rewriting drops the projection or ORDER BY, only the parameters that COUNT actually references are bound; the snapshot may contain names this COUNT does not use, because they can come from a multi-statement execution.
Why return parameterised SQL rather than JDBC SQL: ? only means something in the JDBC layer. Writing it into originalSql or sql loses the parameter names for history, replay, paging and COUNT.
Why not prompt again: the user already confirmed the values once. Prompting again is noise, and it can make the replayed result differ from what is on screen.
9. Frontend
Parameter interaction lives in the single SQL execution flow, covering the execute button, shortcuts, selected SQL, single statements and SQL files.
SQL, data source, database, schema and execution options are frozen for the duration. The parameter request only opens the dialog: it does not go through generic error handling, does not create an execution batch and does not clear existing results. On confirm, parameters or positionalParameters are explicitly passed to the execute, paging, refresh and COUNT requests. Each result keeps its own parameter snapshot, so a later execution cannot pollute an earlier result. Values are never written to the workspace, to console persistence or to the execution log.
Why one entry point: implementing the same interaction in several places produces several copies of submit, cancel and concurrency handling.
Why the parameter request must not use generic error handling: it is not an execution failure. Generic handling would show an error toast, write a failure record and leave a failed batch in the UI for an execution that never ran.
Whole-script pre-check; the statement object carries the parameter table
Single statement
The single-statement entry point uses the same binder
Paging
Recompile and bind against the paged final SQL
COUNT
Both the COUNT SQL and the row-by-row fallback carry parameters
Special plugins
SQL Server, Oracle, DM, Informix and other execution branches are wired in as well
Parameters travel explicitly on the request and the statement object, never in a thread-local or a Connection proxy: implicit passing makes plugin branches, connection reuse and concurrency hard to reason about, and cannot be constructed directly in a unit test.
11. Out of scope
This round supports console execution, result paging, refresh and COUNT. Parameterised async result export is not supported: a request that carries parameters is rejected before the export task is created, and exports without parameters keep their current behaviour.
Why not add export now: export submits a separate async task, and parameters would need a defined lifetime across task restart, retry and three export formats. Rejecting clearly is better than dropping the parameters and creating a task that can only fail.
12. Acceptance criteria
With the switch off, frontend and backend behave exactly as today; SQL_PARAMETERS_REQUIRED is only returned when the switch is on and values are missing.
SQL without parameters keeps its current behaviour.
With values missing, no user SQL runs and no history, executionId or streaming task is created.
Strings, comments, quoted identifiers and native dialect colon syntax are not misdetected.
A repeated name is filled in once and bound once per occurrence; ? takes values left to right and mixing it with :name is an explicit error.
PostgreSQL ?| and ?& are not treated as placeholders.
STRING, NUMBER, BOOLEAN, DATE, TIME, TIMESTAMP and NULL each map to exactly one bind call; empty strings, invalid values and unknown type codes are handled correctly.
JDBC receives ? SQL while the frontend and history keep the parameterised SQL.
A multi-statement script executes no user statement before every parameter has been checked.
Paging and COUNT rewrites bind correctly against the final SQL.
Paging, refresh and COUNT reuse the parameter snapshot and do not prompt again.
User values never reach concatenated SQL, the execution log or the persisted workspace.
Web and desktop streaming use the same parameter protocol.
@openai0229. I've pushed some updates addressing the review. Two design points to settle here, as you suggested:
Where detection lives. Detection currently runs in the frontend, only on the SQL being executed, and the backend re-checks it before binding. If you'd prefer backend-only detection that returns SQL_PARAMETERS_REQUIRED as in the baseline, I can switch it over in a follow-up.
PostgreSQL typing. For the PostgreSQL family (now including Kingbase), STRING and NULL are bound untyped rather than with setString. Otherwise a string value can't match a date or numeric column until DATE/TIME/TIMESTAMP exist. Is that acceptable for v1?
Activity
openai0229 commentedon Sep 23, 2026
Thanks for the detailed write-up — the use case is clear.
We mapped where this would land, and it's a real feature rather than a small fix: the console sends the statement text untouched, the backend splits/rewrites it and executes via
PreparedStatementwith nothing bound, and there is no existing variable or parameter mechanism on the execution path. It would need a way to carry parameter values in the execute request, a binding step at execution time, and a new prompt dialog in the console — plus care around result paging, which re-executes the original SQL text.Would you be interested in contributing this yourself? We can share the insertion points we identified and review a PR. If not, no problem at all — we'll keep it on the list; knowing your interest just helps us plan.
Pinchez25 commentedon Sep 24, 2026
I am interested in contributing to the code myself. Just tell me how to get started
Pinchez25 commentedon Oct 1, 2026
/claim
Pinchez25 commentedon Oct 3, 2026
Hi @openai0229, thanks for the pointers on where this would land. I've built it on a local branch, and I'd like your go-ahead before opening the PR.
My /claim didn't register, probably because the issue doesn't have a contribution/help-wanted label yet. Could you add it so I can claim it? This change also adds a field to the execute API, which the contribution boundaries say needs design approval, so here's the approach.
How it works
Detection: when you run SQL in the console, it looks for :name and ? placeholders. Anything inside string literals, quoted identifiers or comments is ignored, and the lexical rules follow each database's dialect. Only the SQL actually being run is scanned, so a selection only prompts for its own placeholders.
The dialogue: a dialogue asks for each value. A name used more than once gets one input, and NULL has to be chosen explicitly. An empty value is never turned into NULL.
Binding: the values go in a new optional parameters field on the execute request. The backend rewrites :name to ? and binds the values through PreparedStatement, so they never become part of the SQL text. It's the same execution path as today, not a second pipeline.
Paging: results keep the SQL as written, so paging and refresh run it again with the same values.
Rejected with clear errors: mixing :name and ? in one statement; parameters in a script with more than one statement; MongoDB and Redis; and SQL Server GO batches.
PostgreSQL: values are bound untyped, so WHERE id = :id works against an integer column.
Other fixes: I also fixed three places that broke on placeholders: SQL formatting (it turned id = :id into id =: id), the editor syntax check, and statement splitting. The syntax-check messages were hard-coded in Chinese, so I moved them into the existing i18n files.
Compatibility: requests without parameters behave exactly as before, and SQL without placeholders never shows the dialogue.
Tests:
New backend tests run real queries against an in-memory H2 database. They check that values reach the database through the driver and never through the SQL text, including an injection-style value.
There are also parser tests, plus a frontend test that runs as part of the existing build checks.
The focused backend tests, frontend lint, i18n validation and the Community web build all pass.
Known limitations:
Kingbase and Redshift parameter binding hasn't been tested against a real database.
The dialogue requires a value or NULL, so an empty string can't be bound.
If this direction works for you, I'll open the PR with Closes #2944 and request a review from @Aias00. Happy to change anything first.
AI Disclosure:
This was built with AI assistance; I've reviewed and tested it myself
openai0229 commentedon Oct 8, 2026
Thanks for the detailed write-up, and for building the change before opening the PR — adding a field to the execute API is an interface change, so asking for design sign-off first is exactly the right order.
Short answer: we want this feature and your direction is right. There are five points we would like to settle first; everything else can follow your approach. The full design baseline is in the collapsed block at the end.
What we agree with
PreparedStatementbinding and never become part of the SQL text.id = :idintoid =: id, the editor syntax check, statement splitting) and moving the hard-coded syntax messages into i18n are all in scope. Please keep them as separate commits so we can review them one by one.?: agreed, with two constraintsWe originally planned
:nameonly. Having seen your implementation we are happy to include?as well, but let's fix its shape::nameor?, never both, and mixing them is an error — keep your rule.?must be normalised during scanning into synthetic named parameters (__p1,__p2, … left to right) before policy, statement splitting, paging and COUNT rewriting, and turned back into?by index just before JDBC.Why the second one: named parameters survive rewriting by name, positional parameters survive by index, and indexes do not survive. COUNT rewriting drops the projection and ORDER BY, so the original first
?may not exist in the new statement at all, and binding in the original order is guaranteed to be wrong. After normalisation there is exactly one parameter kind downstream, and its identity is stable across rewrites.Two things that come with it:
parameters(map, for:name) pluspositionalParameters(array, for?); the dialog shows Parameter 1..N for?; both syntaxes need a user-selected type.?|and?&are always operators, never placeholders. Binding a bare?operator through a prepared statement is a long-standing PgJDBC limitation and Chat2DB already goes throughprepareStatement, so this is not a regression — we will point users atjsonb_exists()or the:nameform.Five points to change or confirm before the PR
sqlParameters: true). A new syntax affects everybody, and off-by-default is what makes a staged rollout possible. It also has to gate detection only, not binding: otherwise a user who turns the switch off loses the values of an already-open result on paging or COUNT.WHERE name = ''cannot be expressed. You list this as a known limitation and it looks like a small fix — we would rather fix it in this PR./api/rdb/dml/countre-executes the statement, so working paging does not imply working COUNT. Please confirm the count request carries the same values, including the fallback that counts the original query row by row when the COUNT rewrite fails.{ "type": "...", "value": "..." }. Once the API ships, changing the field shape is a breaking change, so the shape should be right from day one. Untyped binding also relies on driver implicit conversion — Oracle DATE is affected by the NLS format. v1 only needsSTRING,NUMBER,BOOLEANandNULL;DATE,TIMEandTIMESTAMPcan follow.Fine as-is
Suggestions, non-blocking
Process
contribution/help-wantedlabel, so/claimshould register now.Closes #2944and request a review from @Aias00.Next
The full design baseline is in the collapsed block below. If you disagree with any of the five points above, reply here and we will settle it before the PR.
Full design baseline — Community SQL Parameters, technical design (Markdown)
Community SQL Parameters — Technical Design
A user writes
:namein the SQL console; on execute, they fill in a type and a value for each parameter. The server detects the parameters, validates them, rewrites the placeholders and binds the values before JDBC execution. The editor, the execution history and result replay always keep the parameterised SQL as written.The frontend does not parse SQL, does not look for colons, and needs no parameter-analysis endpoint. Web and desktop both use the existing execute entry point. SQL parameters sit behind a default-off editor switch, so the rollout only affects users who turn it on.
Status: design baseline, not implemented. Related issue: #2944. Baseline: Community
main@dc2b80a3b0189bb3fd92d69ce8f52de2d4136ac8; file line numbers below refer to that commit.1. Goal and scope
In scope: console execution, result paging and refresh, COUNT, and desktop streaming execution — all supporting
:namenamed parameters and JDBC-style?positional parameters.Out of scope, and why:
2. Key decisions
:name, plus?as positional parameters?is mutually exclusive with:nameand is normalised into synthetic named parameters during scanning::, MySQL:=), and we cannot rely on error recovery to preserve the original text:namebecomes?originalSqlandsql3. Syntax and detection
3.1 Rules
:name.$; Unicode is supported and names are case-sensitive.WHERE id = :id,VALUES (:name, :age), function arguments, ordinary scalar expressions.:nameinside string literals, quoted identifiers and comments is not a parameter.::,:=,@variable,$1and Oracle:NEW/:OLDare native syntax, not parameters.?is a positional parameter, taking values left to right. It cannot appear in the same statement as:name; mixing the two is an explicit error.?|and?&are always operators, never placeholders.The naming rules live in the scanner only; plugins do not re-implement detection, de-duplication or missing-value checks.
3.2 Scanning algorithm
The scanner tracks SQL code, string literals, quoted identifiers, line comments and block comments:
::,:=and dialect-native constructs.{name, start, end}.Positions use Java UTF-16 offsets, the same coordinate system as
substring; advancing over Unicode names goes by code point and does not reuse ANTLR's code point offsets.Why not reuse the existing lexer: when a statement has a syntax error, the lexer does not guarantee a complete
:idtoken, while parameter checking must locate parameters in any syntax state. The scanner only does lexical work and does not require a successful parse first.3.3 Dialect boundaries
Dialects only supply the boundary rules for strings, comments, escapes and native syntax:
::, dollar quotes, E-strings, nested comments, array slices and question-mark operators.--comment conditions,#, backslash escapes and executable comments.:NEW,:OLDand PL/SQL blocks.Looking at the single character before the colon is not enough: constructs such as
::, dollar quotes and q-quotes need context, and guessing turns native syntax into a parameter.3.4 Division of labour with the parser
The scanner finds parameters; the existing parser handles statement splitting, statement types, object references, paging, COUNT and formatting. Parameters stay untouched through all of those stages.
The two responsibilities differ. Making the parser responsible for detection ties "can parse" to "can find parameters", and we cannot rely on parser error recovery to preserve the original text.
4. Request protocol and types
4.1 Fields
Three optional fields are added to the existing execute, paging and count requests: the
sqlParametersswitch, theparametersmap for named parameters, and thepositionalParametersarray for?:The same statement written with
?uses the array, whose order is the appearance order:The two syntaxes are mutually exclusive: a statement uses either
:namewithparametersor?withpositionalParameters.SqlParameterTypeandSqlParameterValuelive together in the domain-api SQL model package. JSON only carries the enum name; deserialisation matches it strictly and an unknown value is a parameter validation error — it never falls back toSTRING, and there is no case-insensitive or alias matching.Why an enum instead of a string: an unknown type must fail rather than be silently treated as a string, and an enum keeps the bind call in a single switch.
4.2 Types and binding
STRINGsetStringNUMBERBigDecimal, thensetBigDecimalBOOLEANtrueorfalsesetBooleanDATEyyyy-MM-ddsetDateTIMEHH:mm:ss[.SSS]setTimeTIMESTAMPyyyy-MM-dd HH:mm:ss[.SSS]setTimestampNULLnullsetNull; the target type is chosen by the plugin from the expression context, or the SQL casts explicitlyThe type is chosen by the user and defaults to
STRING; users never add SQL quotes.NULL, the empty string and "not filled in" are three distinct states. Date and time values only accept the formats above — no locale formats, no lenient parsing.Why not the full
java.sql.Typesset: the JDBC target type for a parameter is decided by the column type, the operator and any CAST, and the user only needs to say what kind of value they typed.java.sql.Typeshas around 40 constants, including ARRAY, STRUCT, BLOB and REF, which cannot be expressed as JSON text, andsetObject(i, value, targetSqlType)does not behave the same as the typed setters across drivers.Why date and time are separate: binding date text with
setStringrelies on implicit conversion, which differs between databases; a fixed format plus a fixed setter is testable.4.3 Control result for missing values
parameterNameslists every distinct parameter name in this SQL, de-duplicated and ordered by first appearance; it never contains a user value. The frontend recognises it byerrorCodeand keeps the structureddata— it does not parse error text.Why not a separate parameter-analysis endpoint: the frontend would need an extra round trip and extra state, and it would have to handle the race between analysis and execution. Reusing the execute entry point means the frontend never inspects SQL.
The control result must be produced before the execution history, the executionId, any streaming task and any user SQL. Creating those side effects first leaves records for an execution that never happened.
5. Switch and rollout
SQL parameters are an editor-level switch, off by default, placed in Editor Settings under the execution group, next to "Error Continue" and "Default Page Size".
When it is off, behaviour is exactly as today: the frontend does not send the switch, the backend does not scan for colons, and
:idgoes to the database untouched. When it is on, the frontend sendssqlParameters: truewith every execute request.parametersorpositionalParameterssqlParameters: truewith no parameter valuesSQL_PARAMETERS_REQUIREDif values are missingThe switch only decides whether the new syntax is recognised; it does not decide whether existing parameters are bound. Otherwise turning the switch off would drop the values from an already-open result on paging or COUNT, causing an error or wrong data.
No server-side switch: off-by-default already limits the blast radius to users who opt in, while a server setting needs a deploy or a restart and adds nothing for a local web or desktop install.
The trade-off: because the frontend does not parse SQL, typing
:namewith the switch off produces no hint — the database reports its own native error.Where it lands:
components/SQLEditor/type.ts:12,components/SQLEditor/constants.ts:7EditorSettingsgainssqlParameters?: boolean, defaultfalseblocks/Setting/EditorSetting/index.tsx:323(execution group)Switchform itemblocks/Setting/searchCatalog.tssettingItem('editor.sqlParameters', ...)so settings search finds iti18n/{zh-CN,en-US,ja-JP,ko-KR,es-ES}/monaco.tsservice/dmlRequest.ts:7,hooks/useSqlExecutor.ts:1096. Execution flow
Web uses
POST /api/rdb/dml/execute; desktop uses the existing JCEFsql-execute. The server stores no paused task and holds no database connection while waiting for user input.The second submission is re-scanned and re-validated because the client payload cannot be trusted and because the data source, permissions and SQL can all change between the two requests.
The whole script is checked up front because with
errorContinue=truea statement-by-statement run would commit the first UPDATE before discovering that the second statement is missing a value — an effect that cannot be rolled back.Cancelling the dialog sends no second request, clears no existing result and writes no failure record. Network errors, timeouts and database errors never re-send the user's SQL: those failures do not mean the SQL is safe to replay, and a resend can duplicate a write.
7. SQL rewriting and JDBC binding
Before JDBC execution the binder scans the current final SQL and produces:
Original SQL:
JDBC SQL:
Binding:
User values are never concatenated into SQL.
jdbcSqlstays in the JDBC layer: it is not returned to the frontend, not written to history and not used for result replay.Positional
?parameters are normalised during scanning into synthetic named parameters (__p1,__p2, … left to right), so everything downstream sees a single parameter kind whose identity survives rewriting; just before JDBC they are turned back into?by index. The two syntaxes are mutually exclusive, so synthetic names cannot collide with user names.Each final SQL is compiled independently, and the bind order is that SQL's own parameter order — never the order from before a rewrite, and never another statement's. The bind call is dispatched by a single switch on
SqlParameterType.Why recompile per final SQL: COUNT rewriting drops the projection and ORDER BY while paging appends clauses, so the two statements do not reference the same parameter set; caching the original order binds values to the wrong positions.
8. Response fields and result snapshots
originalSqlsqlexecuteSqlParamsexecuteSqlParamscontains at least:Paging, refresh, filtering and COUNT reuse the same snapshot and rebuild the bind list for the current final SQL without prompting again. When COUNT rewriting drops the projection or ORDER BY, only the parameters that COUNT actually references are bound; the snapshot may contain names this COUNT does not use, because they can come from a multi-statement execution.
Why return parameterised SQL rather than JDBC SQL:
?only means something in the JDBC layer. Writing it intooriginalSqlorsqlloses the parameter names for history, replay, paging and COUNT.Why not prompt again: the user already confirmed the values once. Prompting again is noise, and it can make the replayed result differ from what is on screen.
9. Frontend
Parameter interaction lives in the single SQL execution flow, covering the execute button, shortcuts, selected SQL, single statements and SQL files.
SQL, data source, database, schema and execution options are frozen for the duration. The parameter request only opens the dialog: it does not go through generic error handling, does not create an execution batch and does not clear existing results. On confirm,
parametersorpositionalParametersare explicitly passed to the execute, paging, refresh and COUNT requests. Each result keeps its own parameter snapshot, so a later execution cannot pollute an earlier result. Values are never written to the workspace, to console persistence or to the execution log.Why one entry point: implementing the same interaction in several places produces several copies of submit, cancel and concurrency handling.
Why the parameter request must not use generic error handling: it is not an execution failure. Generic handling would show an error toast, write a failure record and leave a failed batch in the UI for an execution that never ran.
10. Backend touch points
SqlExecuteRequestSqlExecuteRequestParameters travel explicitly on the request and the statement object, never in a thread-local or a Connection proxy: implicit passing makes plugin branches, connection reuse and concurrency hard to reason about, and cannot be constructed directly in a unit test.
11. Out of scope
This round supports console execution, result paging, refresh and COUNT. Parameterised async result export is not supported: a request that carries parameters is rejected before the export task is created, and exports without parameters keep their current behaviour.
Why not add export now: export submits a separate async task, and parameters would need a defined lifetime across task restart, retry and three export formats. Rejecting clearly is better than dropping the parameters and creating a task that can only fail.
12. Acceptance criteria
SQL_PARAMETERS_REQUIREDis only returned when the switch is on and values are missing.?takes values left to right and mixing it with:nameis an explicit error.?|and?&are not treated as placeholders.?SQL while the frontend and history keep the parameterised SQL.Pinchez25 commentedon Oct 9, 2026
/claim
github-actions commentedon Oct 9, 2026
Task claim
This task is assigned outside the claim automation and is not available.
Commands:
/claim,/unclaim,/renew,/claim statusPinchez25 commentedon Oct 9, 2026
/claim status
Pinchez25 commentedon Oct 12, 2026
@openai0229. I've pushed some updates addressing the review. Two design points to settle here, as you suggested:
feat(sql-editor): support interactive SQL query parameters
feat(sql-editor): support interactive SQL query parameters