diff --git a/apps/desktop/src/components/editor/__tests__/SqlParameterDialog.spec.ts b/apps/desktop/src/components/editor/__tests__/SqlParameterDialog.spec.ts
new file mode 100644
index 0000000000..21c9257ebc
--- /dev/null
+++ b/apps/desktop/src/components/editor/__tests__/SqlParameterDialog.spec.ts
@@ -0,0 +1,166 @@
+// @vitest-environment happy-dom
+
+import { createApp, defineComponent, h, nextTick, reactive, type App } from "vue";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import i18n from "@/i18n";
+import SqlParameterDialog from "@/components/editor/SqlParameterDialog.vue";
+import type { SqlParameterDescriptor, SqlParameterInput } from "@/lib/sql/sqlParameters";
+
+const historyMocks = vi.hoisted(() => ({
+ load: vi.fn<(key: string) => SqlParameterInput[]>(),
+ remember: vi.fn<(values: Record) => Record>(),
+}));
+
+vi.mock("@/lib/sql/sqlParameterHistory", () => ({
+ loadSqlParameterHistory: historyMocks.load,
+ rememberSqlParameterValues: historyMocks.remember,
+}));
+
+vi.mock("@/composables/useSqlHighlighter", () => ({
+ useSqlHighlighter: () => ({ highlight: (sql: string) => `${sql}` }),
+}));
+
+const SQL = "SELECT ${text_value}, ${numeric_value}, ${null_value}, ${raw_value}, ${empty_value}, ${text_value}";
+const PARAMETERS: SqlParameterDescriptor[] = [
+ { key: "text_value", name: "text_value", syntax: "shell", token: "${text_value}" },
+ { key: "numeric_value", name: "numeric_value", syntax: "shell", token: "${numeric_value}" },
+ { key: "null_value", name: "null_value", syntax: "shell", token: "${null_value}" },
+ { key: "raw_value", name: "raw_value", syntax: "shell", token: "${raw_value}" },
+ { key: "empty_value", name: "empty_value", syntax: "shell", token: "${empty_value}" },
+];
+const MIXED_VALUES: Record = {
+ text_value: { kind: "string", value: "alpha beta" },
+ numeric_value: { kind: "number", value: " 42 " },
+ null_value: { kind: "null", value: "NULL" },
+ raw_value: { kind: "raw", value: "current_date" },
+ empty_value: { kind: "boolean", value: "" },
+};
+const RAW_VALUES = Object.fromEntries(Object.entries(MIXED_VALUES).map(([key, input]) => [key, { ...input, kind: "raw" as const }]));
+const RAW_SQL = "SELECT alpha beta, 42, NULL, current_date, NULL, alpha beta";
+
+const mountedApps: App[] = [];
+
+async function mountDialog(values: Record = MIXED_VALUES) {
+ historyMocks.load.mockImplementation((key) => (values[key] ? [{ ...values[key] }] : []));
+ const state = reactive({ open: true });
+ const onExecute = vi.fn();
+ const container = document.createElement("div");
+ document.body.append(container);
+ const app = createApp(
+ defineComponent({
+ setup: () => () =>
+ h(SqlParameterDialog, {
+ open: state.open,
+ sql: SQL,
+ parameters: PARAMETERS,
+ databaseType: "mysql",
+ enabledSyntaxes: ["shell"],
+ onExecute,
+ "onUpdate:open": (value: boolean) => {
+ state.open = value;
+ },
+ }),
+ }),
+ );
+ mountedApps.push(app);
+ app.use(i18n);
+ app.mount(container);
+ await nextTick();
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ return { state, onExecute };
+}
+
+function actionButton(): HTMLButtonElement | null {
+ return document.body.querySelector('[data-testid="sql-parameters-use-raw-all"]');
+}
+
+function clickRawAction() {
+ const button = actionButton();
+ expect(button).not.toBeNull();
+ button!.click();
+}
+
+function buttonWithText(text: string): HTMLButtonElement | undefined {
+ return Array.from(document.body.querySelectorAll("button")).find((button) => button.textContent?.trim() === text);
+}
+
+function parameterInputs(): HTMLInputElement[] {
+ return Array.from(document.body.querySelectorAll('input[data-form-type="other"]'));
+}
+
+beforeEach(() => {
+ historyMocks.load.mockReset();
+ historyMocks.remember.mockReset();
+ historyMocks.remember.mockImplementation((values) => Object.fromEntries(Object.entries(values).map(([key, input]) => [key, [{ ...input }]])));
+});
+
+afterEach(() => {
+ for (const app of mountedApps.splice(0)) app.unmount();
+ document.body.innerHTML = "";
+});
+
+describe("SqlParameterDialog raw parameter action", () => {
+ it("sets every displayed parameter to raw while preserving each current value", async () => {
+ const { state, onExecute } = await mountDialog();
+ const beforeValues = parameterInputs().map((input) => input.value);
+ expect(parameterInputs()[2]?.disabled).toBe(true);
+
+ clickRawAction();
+ await nextTick();
+
+ const inputs = parameterInputs();
+ expect(inputs.map((input) => input.value)).toEqual(beforeValues);
+ expect(inputs[2]?.disabled).toBe(false);
+ expect(Array.from(document.body.querySelectorAll('[role="combobox"]')).map((trigger) => trigger.textContent?.trim())).toEqual(Array(PARAMETERS.length).fill("Raw SQL"));
+ expect(document.body.querySelector("pre")?.textContent).toBe(RAW_SQL);
+ expect(state.open).toBe(true);
+ expect(onExecute).not.toHaveBeenCalled();
+ expect(historyMocks.remember).not.toHaveBeenCalled();
+ });
+
+ it("keeps the action idempotent when every displayed parameter is already raw", async () => {
+ const { state, onExecute } = await mountDialog(RAW_VALUES);
+ const beforeValues = parameterInputs().map((input) => input.value);
+ const beforePreview = document.body.querySelector("pre")?.textContent;
+
+ clickRawAction();
+ await nextTick();
+
+ expect(parameterInputs().map((input) => input.value)).toEqual(beforeValues);
+ expect(document.body.querySelector("pre")?.textContent).toBe(beforePreview);
+ expect(state.open).toBe(true);
+ expect(onExecute).not.toHaveBeenCalled();
+ expect(historyMocks.remember).not.toHaveBeenCalled();
+ });
+
+ it("keeps cancel side-effect free after applying raw to all parameters", async () => {
+ const { state, onExecute } = await mountDialog();
+
+ clickRawAction();
+ await nextTick();
+ buttonWithText("Cancel")?.click();
+ await nextTick();
+
+ expect(state.open).toBe(false);
+ expect(onExecute).not.toHaveBeenCalled();
+ expect(historyMocks.remember).not.toHaveBeenCalled();
+ });
+
+ it("remembers and emits the resolved SQL only when Execute is clicked", async () => {
+ const { state, onExecute } = await mountDialog();
+
+ clickRawAction();
+ await nextTick();
+ expect(onExecute).not.toHaveBeenCalled();
+ expect(historyMocks.remember).not.toHaveBeenCalled();
+
+ buttonWithText("Execute")?.click();
+ await nextTick();
+
+ expect(historyMocks.remember).toHaveBeenCalledOnce();
+ expect(historyMocks.remember).toHaveBeenCalledWith(RAW_VALUES);
+ expect(onExecute).toHaveBeenCalledOnce();
+ expect(onExecute).toHaveBeenCalledWith(RAW_SQL);
+ expect(state.open).toBe(false);
+ });
+});
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 5b852f0a11..3578a73642 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -5096,6 +5096,7 @@ export default {
collectionValuePlaceholder: "Enter a JSON array or comma-separated values",
preview: "SQL Preview",
execute: "Execute",
+ useRawForAll: "Use Raw SQL for All",
kind: {
string: "String",
number: "Number",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 1c69b285ab..09a574bb55 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -4860,6 +4860,7 @@ export default withEnglishFallback({
collectionValuePlaceholder: "Introduce un array JSON o valores separados por comas",
preview: "Vista previa SQL",
execute: "Ejecutar",
+ useRawForAll: "Usar SQL sin procesar para todos",
kind: {
string: "Cadena",
number: "Número",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index a94b457553..fb5f3aa6d0 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -4858,6 +4858,7 @@ export default withEnglishFallback({
collectionValuePlaceholder: "Inserisci un array JSON o valori separati da virgole",
preview: "Anteprima SQL",
execute: "Esegui",
+ useRawForAll: "Usa SQL non elaborato per tutti",
kind: {
string: "Stringa",
number: "Numero",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index e5df0fbedb..3a57435849 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -4887,6 +4887,7 @@ export default withEnglishFallback({
collectionValuePlaceholder: "JSON 配列またはカンマ区切りの値を入力",
preview: "SQLプレビュー",
execute: "実行",
+ useRawForAll: "すべてを生の SQL に設定",
kind: {
string: "文字列",
number: "数値",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index 86f8c38097..95e3d8627d 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -4531,6 +4531,7 @@ export default withEnglishFallback({
collectionValuePlaceholder: "JSON 배열 또는 쉼표로 구분된 값 입력",
preview: "SQL 미리보기",
execute: "실행",
+ useRawForAll: "모두 원시 SQL로 설정",
kind: {
string: "문자열",
number: "숫자",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index e7b3b82705..2b8abc7599 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -4860,6 +4860,7 @@ export default withEnglishFallback({
collectionValuePlaceholder: "Insira um array JSON ou valores separados por vírgula",
preview: "Prévia SQL",
execute: "Executar",
+ useRawForAll: "Usar SQL bruto para todos",
kind: {
string: "Texto",
number: "Número",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 733a9dfc80..5632c6f137 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -5081,6 +5081,7 @@ export default withEnglishFallback({
collectionValuePlaceholder: "输入 JSON 数组或逗号分隔值",
preview: "SQL 预览",
execute: "执行",
+ useRawForAll: "全部使用原始 SQL",
kind: {
string: "字符串",
number: "数字",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 428f2cc87b..2dbcab1e82 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -4184,6 +4184,7 @@ export default withEnglishFallback({
collectionValuePlaceholder: "輸入 JSON 陣列或逗號分隔值",
preview: "SQL 預覽",
execute: "執行",
+ useRawForAll: "全部使用原始 SQL",
kind: {
string: "字串",
number: "數字",