diff --git a/apps/dbagent/src/lib/targetdb/explain.test.ts b/apps/dbagent/src/lib/targetdb/explain.test.ts index b63d6ef2..c41f9d86 100644 --- a/apps/dbagent/src/lib/targetdb/explain.test.ts +++ b/apps/dbagent/src/lib/targetdb/explain.test.ts @@ -37,6 +37,11 @@ describe('isSingleStatement', () => { expect(isSingleStatement('SELECT * FROM schema_name.table_name;')).toBe(true); }); + test('complex SELECT with CASE, JOIN, UNION ALL and parameterized queries', () => { + const complexQuery = `SELECT CASE WHEN $3 < LENGTH(CAST("public"."Post"."geoJson" AS TEXT)) THEN $4 ELSE "public"."Post"."geoJson" END AS "geoJson", CASE WHEN $5 < LENGTH(CAST("public"."Post"."runs" AS TEXT)) THEN $6 ELSE "public"."Post"."runs" END AS "runs", CASE WHEN $7 < LENGTH(CAST("public"."Post"."sprints" AS TEXT)) THEN $8 ELSE "public"."Post"."sprints" END AS "sprints" FROM "public"."Post" INNER JOIN ( (SELECT "public"."Post"."id" FROM "public"."Post" ORDER BY "public"."Post"."id" ASC LIMIT $1) UNION ALL (SELECT "public"."Post"."id" FROM "public"."Post" ORDER BY "public"."Post"."id" DESC LIMIT $2) ) AS "result" ON ("result"."id" = "public"."Post"."id")`; + expect(isSingleStatement(complexQuery)).toBe(true); + }); + test('complex multi-line statement with comments and quotes', () => { const complexQuery = ` /* multi-line comment */ diff --git a/apps/dbagent/src/lib/targetdb/explain.ts b/apps/dbagent/src/lib/targetdb/explain.ts index a25100f1..5a73c3a8 100644 --- a/apps/dbagent/src/lib/targetdb/explain.ts +++ b/apps/dbagent/src/lib/targetdb/explain.ts @@ -104,7 +104,11 @@ class SQLParser { } else if (char === '"') { this.parseDoubleQuotedString(); } else if (char === '$') { - this.parseDollarQuotedString(); + if (this.isDollarParameter()) { + this.parseDollarParameter(); + } else { + this.parseDollarQuotedString(); + } } else if (char === '/' && this.pos + 1 < this.len && this.input[this.pos + 1] === '*') { this.parseBlockComment(); } else if (char === '-' && this.pos + 1 < this.len && this.input[this.pos + 1] === '-') { @@ -195,6 +199,37 @@ class SQLParser { // Don't consume the newline, let skipWhitespace handle it } + private isDollarParameter(): boolean { + // Check if this looks like $1, $2, etc. (parameter placeholder) + // vs $tag$content$tag$ (dollar-quoted string) + if (this.pos + 1 >= this.len) { + return false; + } + + let i = this.pos + 1; // Skip the $ + + // Check if the character after $ is a digit + if (!/\d/.test(this.input[i]!)) { + return false; + } + + // Check if it's all digits until we hit a non-digit + while (i < this.len && /\d/.test(this.input[i]!)) { + i++; + } + + // If the next character after digits is not $, then it's a parameter placeholder + return i >= this.len || this.input[i] !== '$'; + } + + private parseDollarParameter(): void { + this.pos++; // Skip $ + // Skip the digits + while (this.pos < this.len && /\d/.test(this.input[this.pos]!)) { + this.pos++; + } + } + private skipWhitespace(): void { while (this.pos < this.len && /\s/.test(this.input[this.pos]!)) { this.pos++;