@@ -135,51 +135,76 @@ function parseRowsResult(result: ClickHouseHttpResult): ClickHouseRowsResult {
135135const READ_ONLY_STATEMENT = / ^ ( s e l e c t | w i t h | s h o w | d e s c r i b e | d e s c | e x p l a i n | e x i s t s ) \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 */
141144function ensureJsonFormat ( query : string ) : string {
142145 const trimmed = query . trim ( ) . replace ( / ; + \s * $ / , '' )
143- if ( / \b f o r m a t \s + [ a - z 0 - 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 + f o r m a t \s + [ a - z 0 - 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
185210export async function executeClickHouseQuery (
0 commit comments