diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index bf33853135..cb51527263 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -7465,6 +7465,7 @@ const { sql: computed(() => props.sql), exportSql: computed(() => props.exportSql), tableMeta: computed(() => (props.tableMeta ? { ...props.tableMeta } : undefined)), + includeDatabaseName: computed(() => settingsStore.editorSettings.generateSqlIncludeDatabaseName), copyInsertTargetLabel: computed(() => props.tableMeta?.tableName ?? props.customSaveHandler?.targetLabel), mongoUpdateTarget: computed(() => props.mongoUpdateTarget), databaseType: computed(() => props.databaseType), diff --git a/apps/desktop/src/composables/useDataGridExport.ts b/apps/desktop/src/composables/useDataGridExport.ts index 3784a825bd..c479edc52f 100644 --- a/apps/desktop/src/composables/useDataGridExport.ts +++ b/apps/desktop/src/composables/useDataGridExport.ts @@ -80,6 +80,8 @@ export interface UseDataGridExportOptions { sql: ComputedRef; exportSql?: ComputedRef; tableMeta: ComputedRef; + /** Editor setting "Include database name in generated SQL" — passed through to SQL extractors. */ + includeDatabaseName?: ComputedRef; copyInsertTargetLabel?: ComputedRef; mongoUpdateTarget?: ComputedRef; databaseType: ComputedRef; @@ -722,6 +724,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) { databaseType, identifierQuote, tableMeta, + includeDatabaseName: options.includeDatabaseName, hasCellSelection, selectedCells, selectedCellMatrix, diff --git a/apps/desktop/src/composables/useDataGridExtractor.ts b/apps/desktop/src/composables/useDataGridExtractor.ts index 7f126e1f15..4f8310e670 100644 --- a/apps/desktop/src/composables/useDataGridExtractor.ts +++ b/apps/desktop/src/composables/useDataGridExtractor.ts @@ -21,6 +21,7 @@ import { import type { DataGridTableMeta } from "@/lib/dataGrid/dataGridSql"; import { binaryCellClipboardText } from "@/lib/dataGrid/binaryCellDownload"; import { formatError } from "@/lib/backend/errorUtils"; +import { tableMetaWithoutOptionalDatabaseQualifier } from "@/lib/table/tableSelectSql"; import type { DatabaseType } from "@/types/database"; interface ExtractorRowItem { @@ -50,6 +51,8 @@ interface UseDataGridExtractorOptions { databaseType: ComputedRef; identifierQuote?: ComputedRef; tableMeta: ComputedRef; + /** Editor setting "Include database name in generated SQL". When false, SQL extractors omit optional schema/database qualifiers. */ + includeDatabaseName?: ComputedRef; hasCellSelection: ComputedRef; selectedCells: ComputedRef; selectedCellMatrix: ComputedRef; @@ -216,9 +219,13 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) { const rows = rawRows.map((row) => row.map((value, index) => extractorCellValue(value, columnTypes[index], normalizeValues, presentBinaryText, index))); const tableMeta = descriptor.category === "sql" - ? compactTableMeta( - options.tableMeta.value, - columns.map((column) => column.sourceName ?? column.displayName), + ? tableMetaWithoutOptionalDatabaseQualifier( + compactTableMeta( + options.tableMeta.value, + columns.map((column) => column.sourceName ?? column.displayName), + ), + options.databaseType.value, + options.includeDatabaseName?.value, ) : undefined; const request: DataGridExtractRequest = { diff --git a/apps/desktop/src/lib/__tests__/table/tableSelectSql.spec.ts b/apps/desktop/src/lib/__tests__/table/tableSelectSql.spec.ts index 4fe35f3222..5995d4f8a7 100644 --- a/apps/desktop/src/lib/__tests__/table/tableSelectSql.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableSelectSql.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { encodeSqlServerLinkedSchema } from "@/lib/database/sqlServerLinkedServers"; -import { qualifiedTableName, qualifyTableReferencesInSql, quoteTableDataIdentifier, quoteTableIdentifier, quoteTableIdentifierIfNeeded } from "@/lib/table/tableSelectSql"; +import { qualifiedTableName, qualifyTableReferencesInSql, quoteTableDataIdentifier, quoteTableIdentifier, quoteTableIdentifierIfNeeded, tableMetaWithoutOptionalDatabaseQualifier } from "@/lib/table/tableSelectSql"; describe("qualifiedTableName — Doris/StarRocks multi-catalog", () => { it("prefixes external catalog for Doris (no schema)", () => { @@ -167,6 +167,46 @@ describe("qualifiedTableName — includeDatabaseName on quoted-identifier paths }); }); +describe("tableMetaWithoutOptionalDatabaseQualifier — copy extractors (#9326)", () => { + const mysqlMeta = { schema: "analytics", database: "analytics", tableName: "events" }; + + it("strips MySQL schema/database when includeDatabaseName is false", () => { + expect(tableMetaWithoutOptionalDatabaseQualifier(mysqlMeta, "mysql", false)).toEqual({ schema: undefined, database: undefined, tableName: "events" }); + }); + + it("keeps MySQL schema when includeDatabaseName is true or unset", () => { + expect(tableMetaWithoutOptionalDatabaseQualifier(mysqlMeta, "mysql", true)).toBe(mysqlMeta); + expect(tableMetaWithoutOptionalDatabaseQualifier(mysqlMeta, "mysql")).toBe(mysqlMeta); + }); + + it("keeps schema for dialects that require fully qualified names", () => { + expect(tableMetaWithoutOptionalDatabaseQualifier({ schema: "dbo", tableName: "users" }, "sqlserver", false)).toEqual({ schema: "dbo", tableName: "users" }); + }); + + it("matches SELECT-template dropsSchemaQualifier for Oracle/Postgres", () => { + // Same rule as #9110: when the setting is off, optional schema prefixes are + // dropped so the active connection schema applies. + expect(tableMetaWithoutOptionalDatabaseQualifier({ schema: "SCOTT", tableName: "EMP" }, "oracle", false)).toEqual({ schema: undefined, tableName: "EMP" }); + expect(tableMetaWithoutOptionalDatabaseQualifier({ schema: "public", tableName: "users" }, "postgres", false)).toEqual({ schema: undefined, tableName: "users" }); + }); + + it("returns undefined tableMeta unchanged", () => { + expect(tableMetaWithoutOptionalDatabaseQualifier(undefined, "mysql", false)).toBeUndefined(); + }); + + it("keeps qualifiers for Doris/StarRocks external catalogs", () => { + // External-catalog tables are only addressable as catalog.database.table, + // mirroring the SELECT-template rule in qualifiedTableName. + const externalCatalogMeta = { catalog: "ext", database: "db", tableName: "t" }; + expect(tableMetaWithoutOptionalDatabaseQualifier(externalCatalogMeta, "doris", false)).toBe(externalCatalogMeta); + expect(tableMetaWithoutOptionalDatabaseQualifier(externalCatalogMeta, "starrocks", false)).toBe(externalCatalogMeta); + }); + + it("still strips the internal-catalog database qualifier", () => { + expect(tableMetaWithoutOptionalDatabaseQualifier({ catalog: "internal", database: "db", tableName: "t" }, "doris", false)).toEqual({ catalog: "internal", schema: undefined, database: undefined, tableName: "t" }); + }); +}); + describe("qualifiedTableName — GBase 8s", () => { it("omits the metadata owner for GBase 8s table data", () => { expect(qualifiedTableName({ databaseType: "informix", driverProfile: "gbase8s", identifierQuote: "", schema: "gbasedbt", tableName: "connection_smoke" })).toBe("connection_smoke"); diff --git a/apps/desktop/src/lib/table/tableSelectSql.ts b/apps/desktop/src/lib/table/tableSelectSql.ts index ba2659c3e0..03f36b55b8 100644 --- a/apps/desktop/src/lib/table/tableSelectSql.ts +++ b/apps/desktop/src/lib/table/tableSelectSql.ts @@ -44,10 +44,27 @@ const DATABASE_QUALIFIED_TABLE_TYPES = new Set(["mysql", "clickhou // name" on schema-aware engines — except for databases that can only address // objects through their full qualified name (`catalog.schema.table` / // `database.schema.table`), where dropping it would break the query. -function dropsSchemaQualifier(databaseType: DatabaseType | undefined, includeDatabaseName?: boolean): boolean { +export function dropsSchemaQualifier(databaseType: DatabaseType | undefined, includeDatabaseName?: boolean): boolean { return includeDatabaseName === false && databaseType !== undefined && !DATABASE_SCHEMA_QUALIFIED_TYPES.has(databaseType); } +/** + * Strip optional schema/database qualifiers from table metadata used to build + * generated SQL. Mirrors `dropsSchemaQualifier` so copy-as-INSERT/UPDATE + * extractors honor "Include database name in generated SQL" the same way + * SELECT templates do (#9326). + */ +export function tableMetaWithoutOptionalDatabaseQualifier(tableMeta: T | undefined, databaseType: DatabaseType | undefined, includeDatabaseName?: boolean): T | undefined { + if (!tableMeta || !dropsSchemaQualifier(databaseType, includeDatabaseName)) return tableMeta; + // Doris/StarRocks external-catalog tables are only addressable through the + // 3-part `catalog.database.table` form; stripping the middle segment would + // retarget the generated SQL, so keep the qualifiers (same rule as + // `qualifiedTableName`). + if (tableMeta.catalog && tableMeta.catalog !== "internal" && (databaseType === "doris" || databaseType === "starrocks")) return tableMeta; + if (tableMeta.schema === undefined && tableMeta.database === undefined) return tableMeta; + return { ...tableMeta, schema: undefined, database: undefined }; +} + function sqlStatementSpans(sql: string, dialectId: string): Array<{ start: number; end: number }> { const spans: Array<{ start: number; end: number }> = []; let start = 0;