diff --git a/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts index 1330a28a19..8af3c4e19a 100644 --- a/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts +++ b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts @@ -843,6 +843,38 @@ SELECT @value AS Message;`; expect(executedSql).toContain("where fp.create_at < @date_start"); }); + it("sends MySQL SELECT INTO user variables without opening the parameter dialog", async () => { + const sql = ` + select project_id, + year(date_sub(review_date, interval 1 month)), + month(date_sub(review_date, interval 1 month)) + into @project_id, @year, @month + from cms_dynamic_cost_review + where id = '9f03cb27-a553-11f1-8af2-48dc2d090a1c'; + `; + const activeTab = ref(queryTab("app")); + const activeConnection = ref(connection("mysql")); + const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); + const queryStore = useQueryStore(); + const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { + if (activeTab.value) activeTab.value.result = { columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }; + }); + vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); + + const execution = useSqlExecution({ + activeTab: computed(() => activeTab.value), + activeConnection: computed(() => activeConnection.value), + executableSql: computed(() => sql), + activeOutputView, + }); + + await execution.tryExecute(); + + expect(execution.showSqlParameterDialog.value).toBe(false); + expect(execution.sqlParameterNames.value).toEqual([]); + expect(executeCurrentSql).toHaveBeenCalledWith(sql, {}); + }); + it("executes MySQL cursor procedures with compact labels without opening the parameter dialog", async () => { const sql = ` CREATE PROCEDURE process_orders() diff --git a/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts index e590b89505..634e422553 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlParameters.spec.ts @@ -269,6 +269,29 @@ describe("extractSqlParameters", () => { expect(extractSqlParameters(sql)).toEqual(["tenant_id"]); }); + it("ignores MySQL user variables targeted by SELECT INTO", () => { + const sql = ` + select project_id, + year(date_sub(review_date, interval 1 month)), + month(date_sub(review_date, interval 1 month)) + into @project_id, @year, @month + from cms_dynamic_cost_review + where id = '9f03cb27-a553-11f1-8af2-48dc2d090a1c'; + select @project_id, @year, @month; + `; + + expect(extractSqlParameters(sql, { databaseType: "mysql" })).toEqual([]); + }); + + it("keeps ordinary MySQL template parameters around SELECT INTO targets", () => { + const sql = ` + select project_id from cms_dynamic_cost_review where id = @input_id into @project_id; + select @project_id where @tenant_id > 0; + `; + + expect(extractSqlParameters(sql, { databaseType: "mysql" })).toEqual(["input_id", "tenant_id"]); + }); + it("ignores SQL Server procedure parameters declared in routine definitions", () => { const sql = ` create procedure dbo.search_orders diff --git a/apps/desktop/src/lib/sql/sqlParameters.ts b/apps/desktop/src/lib/sql/sqlParameters.ts index 2b3749a23b..db4d85818c 100644 --- a/apps/desktop/src/lib/sql/sqlParameters.ts +++ b/apps/desktop/src/lib/sql/sqlParameters.ts @@ -1374,12 +1374,13 @@ function collectSetStatementVariables(sql: string, start: number, declared: Set< function collectSelectAssignmentVariables(sql: string, start: number, declared: Set, databaseType?: DatabaseType): number { let i = start; + let depth = 0; while (i < sql.length) { const ch = sql[i]; const next = sql[i + 1]; - if (ch === ";") return i + 1; - if (isLineStatementStart(sql, i) && isSqlStatementKeyword(sql, i)) return i; - if (matchesWord(sql, i, "from")) return i; + if (ch === ";" && depth === 0) return i + 1; + if (depth === 0 && isLineStatementStart(sql, i) && isSqlStatementKeyword(sql, i)) return i; + if (databaseType !== "mysql" && matchesWord(sql, i, "from")) return i; if (ch === "'" || ch === '"' || ch === "`") { i = skipQuoted(sql, i, ch); continue; @@ -1400,6 +1401,23 @@ function collectSelectAssignmentVariables(sql: string, start: number, declared: i = skipLine(sql, i + 1); continue; } + if (databaseType === "mysql" && ch === "(") { + depth += 1; + i += 1; + continue; + } + if (databaseType === "mysql" && ch === ")") { + depth = Math.max(0, depth - 1); + i += 1; + continue; + } + if (databaseType === "mysql" && depth === 0 && matchesWord(sql, i, "into")) { + const targetEnd = collectMysqlSelectIntoTargets(sql, i + "into".length, declared); + if (targetEnd !== null) { + i = targetEnd; + continue; + } + } if (ch === "@") { const name = readParameterName(sql, i + 1); if (name && next !== "@" && sql[i - 1] !== "@" && isSetAssignmentTarget(sql, i + 1 + name.length)) { @@ -1413,6 +1431,21 @@ function collectSelectAssignmentVariables(sql: string, start: number, declared: return i; } +function collectMysqlSelectIntoTargets(sql: string, start: number, declared: Set): number | null { + let i = skipSqlWhitespaceAndComments(sql, start); + let found = false; + while (i < sql.length && sql[i] === "@" && sql[i + 1] !== "@" && sql[i - 1] !== "@") { + const name = readParameterName(sql, i + 1); + if (!name) break; + declared.add(name.toLowerCase()); + found = true; + i = skipSqlWhitespaceAndComments(sql, i + 1 + name.length); + if (sql[i] !== ",") break; + i = skipSqlWhitespaceAndComments(sql, i + 1); + } + return found ? i : null; +} + function collectRoutineDefinitionVariables(sql: string, start: number, declared: Set, databaseType?: DatabaseType): number { let i = start; while (i < sql.length) {