diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index cb83b23fe1..72e3efc69a 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -21,6 +21,7 @@ import { useSettingsStore, type StructureEditorDensity } from "@/stores/settings import { useTheme } from "@/composables/useTheme"; import { useToast } from "@/composables/useToast"; import { type SqlHighlighter, createShikiSqlHighlighter } from "@/lib/sql/sqlHighlighter"; +import { joinSqlStatementsForScript } from "@/lib/sql/sqlBatchScript"; import { copyToClipboard } from "@/lib/common/clipboard"; import { formatSqlForDisplay, sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter"; import { queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout"; @@ -99,10 +100,10 @@ onMounted(async () => { const highlightedSql = computed(() => { if (!pendingStatements.value.length) return ""; - const sql = pendingStatements.value.join("\n"); + const sql = previewSqlText.value; return sqlHighlighter.value?.(sql) ?? sql; }); -const previewSqlText = computed(() => pendingStatements.value.join("\n")); +const previewSqlText = computed(() => joinSqlStatementsForScript(pendingStatements.value, databaseType.value)); const props = defineProps<{ connectionId: string; diff --git a/apps/desktop/src/lib/sql/sqlBatchScript.ts b/apps/desktop/src/lib/sql/sqlBatchScript.ts new file mode 100644 index 0000000000..1ea607dd6c --- /dev/null +++ b/apps/desktop/src/lib/sql/sqlBatchScript.ts @@ -0,0 +1,12 @@ +import type { DatabaseType } from "@/types/database"; + +/** + * Joins generated DDL statements into a script for display/copy. SQL Server statements + * are separated with GO so tools that execute the copied script (SSMS, sqlcmd, DBX's + * GO-aware runner) treat each statement as an independent batch. GO only defines batch + * boundaries; whether execution continues after an error depends on the client policy. + */ +export function joinSqlStatementsForScript(statements: readonly string[], databaseType?: DatabaseType): string { + if (databaseType !== "sqlserver") return statements.join("\n"); + return statements.map((statement) => statement.trimEnd()).join("\nGO\n"); +} diff --git a/packages/app-tests/sqlBatchScript.test.ts b/packages/app-tests/sqlBatchScript.test.ts new file mode 100644 index 0000000000..39fc110c87 --- /dev/null +++ b/packages/app-tests/sqlBatchScript.test.ts @@ -0,0 +1,39 @@ +import { strict as assert } from "node:assert"; +import { test } from "vitest"; +import { joinSqlStatementsForScript } from "../../apps/desktop/src/lib/sql/sqlBatchScript.ts"; + +const statements = [ + "ALTER TABLE [dbo].[GOODS] ADD [FYLKH] nvarchar(255);", + "IF EXISTS (SELECT 1 FROM sys.extended_properties WHERE name = N'MS_Description') EXEC sys.sp_dropextendedproperty @name=N'MS_Description';", + "EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'用料款号';", +]; + +test("separates SQL Server statements with GO batch separators", () => { + const script = joinSqlStatementsForScript(statements, "sqlserver"); + assert.equal(script, statements.join("\nGO\n")); +}); + +test("does not append a trailing GO after the last SQL Server statement", () => { + const script = joinSqlStatementsForScript(statements, "sqlserver"); + assert.ok(!script.endsWith("GO")); + assert.equal(script.split("\nGO\n").length, statements.length); +}); + +test("keeps a single SQL Server statement without GO", () => { + assert.equal(joinSqlStatementsForScript([statements[0]], "sqlserver"), statements[0]); +}); + +test("joins statements with newlines for other database types", () => { + assert.equal(joinSqlStatementsForScript(statements, "mysql"), statements.join("\n")); + assert.equal(joinSqlStatementsForScript(statements, undefined), statements.join("\n")); +}); + +test("returns an empty script for an empty statement list", () => { + assert.equal(joinSqlStatementsForScript([], "sqlserver"), ""); + assert.equal(joinSqlStatementsForScript([], "mysql"), ""); +}); + +test("keeps GO on its own line when statements carry trailing whitespace", () => { + const script = joinSqlStatementsForScript(["SELECT 1;\n", "SELECT 2; "], "sqlserver"); + assert.equal(script, "SELECT 1;\nGO\nSELECT 2;"); +});