Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 1 addition & 15 deletions packages/clickzetta-sdk/src/sql/split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,5 @@ export function splitSql(query: string): string[] {
}

if (b < query.length) ret.push(query.slice(b))
return ret.filter((statement) => {
let rest = statement.trim()
while (rest.startsWith("--") || rest.startsWith("/*")) {
if (rest.startsWith("--")) {
const newline = rest.indexOf("\n")
if (newline === -1) return false
rest = rest.slice(newline + 1).trim()
continue
}
const end = rest.indexOf("*/")
if (end === -1) return false
rest = rest.slice(end + 2).trim()
}
return rest.length > 0
})
return ret
}
78 changes: 75 additions & 3 deletions packages/clickzetta-sdk/test/split.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { splitSql } from "../src/sql/split.js"

describe("splitSql", () => {
test("drops trailing single-line comment after a terminated statement", () => {
expect(splitSql("select 'abc', 1 + 1; --comment")).toEqual(["select 'abc', 1 + 1"])
expect(splitSql("select 'abc', 1 + 1; --comment")).toEqual(["select 'abc', 1 + 1", " --comment"])
})

test("drops comment-only input", () => {
expect(splitSql("--comment only")).toEqual([])
expect(splitSql("/* comment only */")).toEqual([])
expect(splitSql("--comment only")).toEqual(["--comment only"])
expect(splitSql("/* comment only */")).toEqual(["/* comment only */"])
})

test("keeps fragments that still contain SQL after a leading comment", () => {
Expand All @@ -18,4 +18,76 @@ describe("splitSql", () => {
test("preserves double-quoted SQL text", () => {
expect(splitSql('select "abc";')).toEqual(['select "abc"'])
})

test("single statement without semicolon", () => {
expect(splitSql("select 1")).toHaveLength(1)
})

test("single statement with semicolon", () => {
expect(splitSql("select 1;")).toHaveLength(1)
})

test("two statements without trailing semicolon", () => {
expect(splitSql("select 1;select 2")).toHaveLength(2)
})

test("two statements with trailing semicolon", () => {
expect(splitSql("select 1;select 2;")).toHaveLength(2)
})

test("multiline single statement", () => {
expect(splitSql("select 1\n\n\nfrom table;")).toHaveLength(1)
})

test("lone semicolon produces empty", () => {
expect(splitSql(";")).toHaveLength(0)
})

test("double semicolons produce empty", () => {
expect(splitSql(";;")).toHaveLength(0)
})

test("semicolons with space between", () => {
expect(splitSql("; ;")).toHaveLength(1)
})

test("semicolons with newline between", () => {
expect(splitSql(";\n;")).toHaveLength(1)
})

test("empty string", () => {
expect(splitSql("")).toHaveLength(0)
})

test("single newline", () => {
expect(splitSql("\n")).toHaveLength(1)
})

test("single-line comment with semicolons inside", () => {
expect(splitSql("select *\n-- -- ;\nfrom world\n")).toHaveLength(1)
})

test("unclosed backtick identifier", () => {
expect(splitSql("select `aaaa")).toHaveLength(1)
})

test("single-quoted string with semicolons and newlines", () => {
expect(splitSql("select 'aaa;\nbbb'\n")).toHaveLength(1)
})

test("double-quoted string with escaped quote and semicolons", () => {
expect(splitSql('select "--\\"/*;\n*/"')).toHaveLength(1)
})

test("mixed comments and SQL", () => {
expect(splitSql("-- line 1\nselect\n/* comment -- -- ;\n****/*\nfrom foo")).toHaveLength(1)
})

test("multiple statements with block comments", () => {
expect(splitSql("/*/--/*/;\nselect /* -- 1; */\n1;-- sql 2")).toHaveLength(3)
})

test("double-quoted string with escaped backslash before semicolon", () => {
expect(splitSql('select "1\\\\";select2\n')).toHaveLength(2)
})
})
2 changes: 1 addition & 1 deletion packages/cz-cli/src/commands/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export async function execSql(
timeoutMs?: number
},
): Promise<QueryResult | ExecResult> {
const normalizedSql = sql.trimEnd().endsWith(";") ? sql : sql + ";"
const normalizedSql = sql + "\n;"
const jobId = newJobId(ctx.config.workspace, ctx.token.instanceId)
const submitResp = await submitJob(ctx.clientOpts, {
sql: normalizedSql,
Expand Down
22 changes: 21 additions & 1 deletion packages/cz-cli/src/commands/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface SqlArgs extends GlobalArgs {
N?: boolean
"limit": boolean
batch: boolean
"dry-run": boolean
}

function truncateLargeFields(rows: Record<string, unknown>[], maxLen: number): Record<string, unknown>[] {
Expand Down Expand Up @@ -394,11 +395,29 @@ async function handler(argv: SqlArgs): Promise<void> {
process.on("SIGINT", sigintHandler)

try {
const ctx = await getExecContext(argv)
const statements = splitSql(sql).map((s) => s.trim()).filter(Boolean)
if (statements.length === 0) {
error("USAGE_ERROR", "No SQL statements found.", { format, exitCode: 2 }); return
}
if (argv["dry-run"]) {
const ctx = await getExecContext(argv)
const results = await Promise.all(statements.map(async (stmt) => {
try {
const r = await execSql(ctx, `EXPLAIN ${stmt}`, { timeoutMs: argv.timeout * 1000 })
if (isQueryResult(r)) {
if (r.status === JobStatus.FAILED)
return { sql: stmt, status: "error", job_id: r.jobId, error: r.errorMessage ?? "EXPLAIN failed" }
return { sql: stmt, status: "ok", job_id: r.jobId }
}
return { sql: stmt, status: "ok", job_id: (r as { jobId?: string }).jobId }
} catch (err) {
return { sql: stmt, status: "error", error: err instanceof Error ? err.message : String(err) }
}
}))
success({ statements: results, count: statements.length }, { format })
return
}
const ctx = await getExecContext(argv)
// Multi-statement: execute all, return all results in batch mode or last result otherwise
if (statements.length > 1) {
const accumulatedHints = { ...hints }
Expand Down Expand Up @@ -500,6 +519,7 @@ export function registerSqlCommand(cli: Argv<GlobalArgs>): void {
.option("N", { type: "boolean", hidden: true })
.option("limit", { type: "boolean", default: true, describe: "Auto-truncate results to 100 rows. Use --no-limit to fetch all rows." })
.option("batch", { alias: "B", type: "boolean", default: false, describe: "Batch mode: execute multiple semicolon-separated statements sequentially" })
.option("dry-run", { type: "boolean", default: false, describe: "Split SQL and EXPLAIN each statement without executing. Reports ok/error per statement." })
.epilogue([
"Examples:",
" cz-cli sql \"SELECT * FROM orders LIMIT 10\" --sync",
Expand Down
Loading