Skip to content

Commit 4bd1957

Browse files
committed
fix(clickhouse): force JSON output on query path and ignore comments when detecting chained statements
1 parent 28d4516 commit 4bd1957

1 file changed

Lines changed: 52 additions & 27 deletions

File tree

  • apps/sim/app/api/tools/clickhouse

apps/sim/app/api/tools/clickhouse/utils.ts

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -135,51 +135,76 @@ function parseRowsResult(result: ClickHouseHttpResult): ClickHouseRowsResult {
135135
const READ_ONLY_STATEMENT = /^(select|with|show|describe|desc|explain|exists)\b/i
136136

137137
/**
138-
* Appends `FORMAT JSON` to read statements that do not already specify an output
139-
* format, so the HTTP interface returns a structured result set.
138+
* Normalizes the output format of a read statement to JSON so the HTTP response
139+
* can always be parsed into rows: strips any trailing `FORMAT <x>` (e.g. CSV or
140+
* TabSeparated, which `parseRowsResult` cannot read) and appends `FORMAT JSON`.
141+
* Non-read statements are returned untouched (their own FORMAT, e.g. JSONEachRow
142+
* for inserts, is preserved).
140143
*/
141144
function ensureJsonFormat(query: string): string {
142145
const trimmed = query.trim().replace(/;+\s*$/, '')
143-
if (/\bformat\s+[a-z0-9_]+$/i.test(trimmed)) {
144-
return trimmed
145-
}
146146
if (READ_ONLY_STATEMENT.test(trimmed)) {
147-
return `${trimmed}\nFORMAT JSON`
147+
const withoutFormat = trimmed.replace(/\s+format\s+[a-z0-9_]+\s*$/i, '')
148+
return `${withoutFormat}\nFORMAT JSON`
148149
}
149150
return trimmed
150151
}
151152

152153
/**
153-
* Detects whether a statement chains a second statement after a `;` that is not
154-
* inside a string ('...'), quoted identifier ("..." / `...`) span. A trailing
155-
* semicolon with nothing after it is allowed.
154+
* Replaces string literals ('...'), quoted identifiers ("..." / `...`), and SQL
155+
* comments (`-- …` and `/* … *​/`) with spaces so that structural scans (e.g. for
156+
* statement-chaining semicolons) only see actual SQL code, not data or comments.
156157
*/
157-
function hasChainedStatement(sql: string): boolean {
158-
let inSingle = false
159-
let inDouble = false
160-
let inBacktick = false
161-
for (let i = 0; i < sql.length; i++) {
158+
function maskSqlNoise(sql: string): string {
159+
let out = ''
160+
let i = 0
161+
while (i < sql.length) {
162162
const ch = sql[i]
163-
if (inSingle) {
164-
if (ch === '\\') i++
165-
else if (ch === "'") inSingle = false
163+
if (ch === "'" || ch === '"' || ch === '`') {
164+
out += ' '
165+
i++
166+
while (i < sql.length && sql[i] !== ch) {
167+
if (ch !== '`' && sql[i] === '\\') {
168+
out += ' '
169+
i += 2
170+
continue
171+
}
172+
out += ' '
173+
i++
174+
}
175+
if (i < sql.length) {
176+
out += ' '
177+
i++
178+
}
166179
continue
167180
}
168-
if (inDouble) {
169-
if (ch === '\\') i++
170-
else if (ch === '"') inDouble = false
181+
if (ch === '-' && sql[i + 1] === '-') {
182+
const newline = sql.indexOf('\n', i + 2)
183+
const end = newline === -1 ? sql.length : newline
184+
out += ' '.repeat(end - i)
185+
i = end
171186
continue
172187
}
173-
if (inBacktick) {
174-
if (ch === '`') inBacktick = false
188+
if (ch === '/' && sql[i + 1] === '*') {
189+
const close = sql.indexOf('*/', i + 2)
190+
const end = close === -1 ? sql.length : close + 2
191+
out += ' '.repeat(end - i)
192+
i = end
175193
continue
176194
}
177-
if (ch === "'") inSingle = true
178-
else if (ch === '"') inDouble = true
179-
else if (ch === '`') inBacktick = true
180-
else if (ch === ';' && sql.slice(i + 1).trim().length > 0) return true
195+
out += ch
196+
i++
181197
}
182-
return false
198+
return out
199+
}
200+
201+
/**
202+
* Detects whether a statement chains a second statement after a `;`, ignoring
203+
* semicolons inside string literals, quoted identifiers, and comments. A trailing
204+
* semicolon (with only whitespace/comments after it) is allowed.
205+
*/
206+
function hasChainedStatement(sql: string): boolean {
207+
return /;\s*\S/.test(maskSqlNoise(sql))
183208
}
184209

185210
export async function executeClickHouseQuery(

0 commit comments

Comments
 (0)