From 026e0b539d383f7d4ef26290f428fd2b4bf41f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E6=A1=A5=E5=87=89=E4=BB=8B?= <113153113+HighBigSky@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:55:15 +0800 Subject: [PATCH] fix(grid): honor extractor database-name option when copying SQL --- .../grid/DataGridExtractorDialog.vue | 4 +- .../__tests__/DataGridExtractorDialog.spec.ts | 14 +++ .../__tests__/useDataGridExport.spec.ts | 16 ++++ ...aGridExtractor.includeDatabaseName.spec.ts | 89 +++++++++++++++++++ .../src/composables/useDataGridExtractor.ts | 13 ++- .../dataGrid/dataGridCopyExtractor.spec.ts | 12 +-- .../__tests__/table/tableSelectSql.spec.ts | 14 +++ .../src/lib/dataGrid/dataGridCopyExtractor.ts | 8 +- .../stores/__tests__/settingsStore.spec.ts | 17 ++++ apps/desktop/src/stores/settingsStore.ts | 16 +++- 10 files changed, 188 insertions(+), 15 deletions(-) create mode 100644 apps/desktop/src/composables/__tests__/useDataGridExtractor.includeDatabaseName.spec.ts diff --git a/apps/desktop/src/components/grid/DataGridExtractorDialog.vue b/apps/desktop/src/components/grid/DataGridExtractorDialog.vue index 1ea6c5e255..006e50433e 100644 --- a/apps/desktop/src/components/grid/DataGridExtractorDialog.vue +++ b/apps/desktop/src/components/grid/DataGridExtractorDialog.vue @@ -211,9 +211,7 @@ function save() { - +
diff --git a/apps/desktop/src/components/grid/__tests__/DataGridExtractorDialog.spec.ts b/apps/desktop/src/components/grid/__tests__/DataGridExtractorDialog.spec.ts index a526339337..925bf9ba08 100644 --- a/apps/desktop/src/components/grid/__tests__/DataGridExtractorDialog.spec.ts +++ b/apps/desktop/src/components/grid/__tests__/DataGridExtractorDialog.spec.ts @@ -103,6 +103,20 @@ describe("extractor draft preview", () => { expect(host.querySelector("pre")?.textContent).toBe("initial"); }); + it("offers the database-name option for SQL updates as well (#11195)", async () => { + const { props, host, save } = mountDialog(); + props.preference = "sql-updates"; + props.items = [{ value: "sql-updates", label: "UPDATE" }]; + props.open = true; + await advancePreview(); + + // 勾选后保存应带上 includeDatabaseName(复制为 UPDATE 也支持带库名/模式名) + await toggle(host, "copyExtractorIncludeDatabaseName"); + const button = Array.from(host.querySelectorAll("button")).find((item) => item.textContent?.includes("common.save"))!; + button.click(); + expect(save).toHaveBeenCalledWith(expect.objectContaining({ options: expect.objectContaining({ sql: expect.objectContaining({ includeDatabaseName: true }) }) })); + }); + it("previews unsaved quote and temporal options without mutating saved settings", async () => { const { props, host, preview, save } = mountDialog(); props.open = true; diff --git a/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts index 8cd808e8e6..0abcecf613 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts @@ -1289,6 +1289,22 @@ describe("useDataGridExport prepared row statements", () => { expect(vi.mocked(extractDataGridSelection).mock.lastCall?.[0].options.sql).toEqual(saved.sql); }); + it("follows the extractor option for the copied SQL table qualifier (#11195)", async () => { + const tableMeta: DataGridTableMeta = { ...editableTable, schema: "SYSDBA" }; + const matrix: CellSelectionMatrix = { rowIndexes: [0], columnIndexes: [0, 1], columns: ["id", "name"], rows: [[1, "Ada"]] }; + vi.mocked(extractDataGridSelection).mockResolvedValue({ text: "copied", mimeType: "text/plain", fileExtension: "sql", rowCount: 1, columnCount: 2 }); + const withDatabaseName = { ...DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, sql: { ...DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS.sql, includeDatabaseName: true } }; + + const enabled = createExportState(tableMeta, ["id", "name"], matrix, [1, "Ada"], undefined, matrix.rows, [], withDatabaseName, false, undefined, false, undefined, undefined, "dameng"); + await enabled.copyWithExtractor("sql-inserts"); + expect(vi.mocked(extractDataGridSelection).mock.lastCall?.[0].tableMeta).toMatchObject({ schema: "SYSDBA", tableName: "users" }); + + // 未勾选(默认)时保持既有行为:把可选的库名/模式名前缀交给后端省略 + const disabled = createExportState(tableMeta, ["id", "name"], matrix, [1, "Ada"], undefined, matrix.rows, [], DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, false, undefined, false, undefined, undefined, "dameng"); + await disabled.copyWithExtractor("sql-inserts"); + expect(vi.mocked(extractDataGridSelection).mock.lastCall?.[0].tableMeta?.schema).toBeUndefined(); + }); + it("does not report success when an extractor export save is cancelled", async () => { const matrix: CellSelectionMatrix = { rowIndexes: [0], columnIndexes: [1], columns: ["name"], rows: [["Ada"]] }; vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: '[{"name":"Ada"}]', mimeType: "application/json", fileExtension: "json", rowCount: 1, columnCount: 1 }); diff --git a/apps/desktop/src/composables/__tests__/useDataGridExtractor.includeDatabaseName.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridExtractor.includeDatabaseName.spec.ts new file mode 100644 index 0000000000..b984e25219 --- /dev/null +++ b/apps/desktop/src/composables/__tests__/useDataGridExtractor.includeDatabaseName.spec.ts @@ -0,0 +1,89 @@ +import { computed, ref } from "vue"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { useDataGridExtractor } from "@/composables/useDataGridExtractor"; +import { DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, type DataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor"; +import type { DataGridTableMeta } from "@/lib/dataGrid/dataGridSql"; +import type { DatabaseType } from "@/types/database"; + +const mocks = vi.hoisted(() => ({ extract: vi.fn() })); +vi.mock("@/lib/backend/api", () => ({ extractDataGridSelection: mocks.extract })); +vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) })); +vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) })); + +function createState(databaseType: DatabaseType, tableMeta: DataGridTableMeta, extractorOptions?: DataGridExtractorOptions, includeDatabaseName = false) { + const items = [{ id: 1, data: [1, "Ada"], isDraft: false }]; + return useDataGridExtractor({ + columns: computed(() => ["id", "name"]), + allColumns: computed(() => ["id", "name"]), + displayItems: computed(() => items), + allDisplayItems: computed(() => items), + allSourceColumns: computed(() => undefined), + visibleColumnIndexes: computed(() => [0, 1]), + columnTypes: computed(() => undefined), + extractorOptions: computed(() => extractorOptions ?? DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS), + databaseType: computed(() => databaseType), + identifierQuote: computed(() => undefined), + tableMeta: computed(() => tableMeta), + includeDatabaseName: computed(() => includeDatabaseName), + hasCellSelection: computed(() => false), + selectedCells: computed(() => ({ columns: [], rows: [] })), + selectedCellMatrix: computed(() => null), + hasRowSelection: computed(() => true), + hasColumnSelection: computed(() => false), + selectedRowIds: ref(new Set([1])), + contextCell: ref(null), + contextSelectionIsSynthetic: ref(false), + copyText: vi.fn(async () => true), + canCopySqlInsert: () => true, + buildMongoInsert: vi.fn(async () => undefined), + }); +} + +function qualifiedMeta(schema?: string, database?: string): DataGridTableMeta { + return { + tableName: "users", + schema, + database, + primaryKeys: ["id"], + columns: [ + { name: "id", data_type: "int" }, + { name: "name", data_type: "varchar" }, + ], + }; +} + +async function capturedRequest(databaseType: DatabaseType, tableMeta: DataGridTableMeta, extractorOptions?: DataGridExtractorOptions, includeDatabaseName = false) { + const state = createState(databaseType, tableMeta, extractorOptions, includeDatabaseName); + const extraction = await state.extractWithExtractor("sql-inserts"); + expect(extraction).not.toBeNull(); + return mocks.extract.mock.calls[mocks.extract.mock.calls.length - 1]![0]!; +} + +describe("useDataGridExtractor includeDatabaseName payload", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.extract.mockResolvedValue({ text: "INSERT INTO users VALUES (1, 'Ada');", rowCount: 1 }); + }); + + it.each([ + ["sqlserver", "dbo", "app"], + ["snowflake", "PUBLIC", undefined], + ] as const)("keeps the namespace flag true for %s even when the option is off", async (databaseType, schema, database) => { + const request = await capturedRequest(databaseType, qualifiedMeta(schema, database)); + expect(request.tableMeta.schema).toBe(schema); + expect(request.options.sql.includeDatabaseName).toBe(true); + }); + + it("strips optional qualifiers and keeps the flag off for non schema-qualified dialects", async () => { + const request = await capturedRequest("mysql", qualifiedMeta(undefined, "mydb")); + expect(request.tableMeta.database).toBeUndefined(); + expect(request.tableMeta.schema).toBeUndefined(); + expect(request.options.sql.includeDatabaseName).toBe(false); + }); + + it("preserves an explicit opt-in for non schema-qualified dialects", async () => { + const request = await capturedRequest("mysql", qualifiedMeta(undefined, "mydb"), { ...DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, sql: { ...DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS.sql, includeDatabaseName: true } }); + expect(request.tableMeta.database).toBe("mydb"); + expect(request.options.sql.includeDatabaseName).toBe(true); + }); +}); diff --git a/apps/desktop/src/composables/useDataGridExtractor.ts b/apps/desktop/src/composables/useDataGridExtractor.ts index ad5b0e3dc9..fbd446617f 100644 --- a/apps/desktop/src/composables/useDataGridExtractor.ts +++ b/apps/desktop/src/composables/useDataGridExtractor.ts @@ -230,6 +230,10 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) { const presentBinaryText = descriptor.category !== "sql"; const rawRows = sourceRows.map((row) => requiredSourceIndexes.map((sourceIndex) => row[sourceIndex])); const rows = rawRows.map((row) => row.map((value, index) => extractorCellValue(value, columnTypes[index], normalizeValues, keepUnsafeJsonText, presentBinaryText, index))); + // 「包含数据库名称」:复制/导出 SQL 时,提取器自身的勾选框与编辑器全局设置 + // (`generateSqlIncludeDatabaseName`)任一开启都保留库名/模式名;后者还会影响 + // 保存 SQL、侧栏导出等其它路径,因此这里取“或”,不覆盖全局偏好。 + const extractorOptionValues = normalizeDataGridExtractorOptions(extractorOptions); const tableMeta = descriptor.category === "sql" ? tableMetaWithoutOptionalDatabaseQualifier( @@ -238,7 +242,7 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) { columns.map((column) => column.sourceName ?? column.displayName), ), options.databaseType.value, - options.includeDatabaseName?.value, + options.includeDatabaseName?.value === true || extractorOptionValues.sql.includeDatabaseName === true, ) : undefined; const request: DataGridExtractRequest = { @@ -251,7 +255,12 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) { selectedColumnIndexes, rows, selectionKind, - options: normalizeDataGridExtractorOptions(extractorOptions), + // For dialects whose tableMeta keeps the schema qualifier regardless of + // the flag (DATABASE_SCHEMA_QUALIFIED_TYPES), the backend strips every + // namespace prefix when include_database_name=false, which would regress + // the default output to a bare table name; keep the flag aligned with + // the qualified tableMeta we intentionally preserved. + options: tableMeta && (tableMeta.schema !== undefined || tableMeta.database !== undefined) ? { ...extractorOptionValues, sql: { ...extractorOptionValues.sql, includeDatabaseName: true } } : extractorOptionValues, }; requestSources.set(request, { rowIds: sourceRowIds, diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridCopyExtractor.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridCopyExtractor.spec.ts index 6b587a7543..5fef626940 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/dataGridCopyExtractor.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridCopyExtractor.spec.ts @@ -12,11 +12,13 @@ describe("data-grid extractor options", () => { expect(normalizeDataGridExtractorOptions({ sql: { quoteIdentifiers: "false", temporalFormat: "custom" } }).sql).toMatchObject({ quoteIdentifiers: true, temporalFormat: "native" }); }); - it("keeps database qualification for legacy options and persists explicit opt-out", () => { - expect(normalizeDataGridExtractorOptions({ sql: {} }).sql.includeDatabaseName).toBe(true); - const configured = normalizeDataGridExtractorOptions({ sql: { includeDatabaseName: false } }); - expect(normalizeDataGridExtractorOptions(JSON.parse(JSON.stringify(configured))).sql.includeDatabaseName).toBe(false); - expect(normalizeDataGridExtractorOptions({ sql: { includeDatabaseName: "false" } }).sql.includeDatabaseName).toBe(true); + it("defaults to dropping database qualification and keeps an explicit opt-in", () => { + expect(DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS.sql.includeDatabaseName).toBe(false); + expect(normalizeDataGridExtractorOptions({ sql: {} }).sql.includeDatabaseName).toBe(false); + const configured = normalizeDataGridExtractorOptions({ sql: { includeDatabaseName: true } }); + expect(configured.sql.includeDatabaseName).toBe(true); + expect(normalizeDataGridExtractorOptions(JSON.parse(JSON.stringify(configured))).sql.includeDatabaseName).toBe(true); + expect(normalizeDataGridExtractorOptions({ sql: { includeDatabaseName: "true" } }).sql.includeDatabaseName).toBe(false); }); it("defaults DSV NULL output to an empty spreadsheet field", () => { diff --git a/apps/desktop/src/lib/__tests__/table/tableSelectSql.spec.ts b/apps/desktop/src/lib/__tests__/table/tableSelectSql.spec.ts index 1239c7f587..307206adde 100644 --- a/apps/desktop/src/lib/__tests__/table/tableSelectSql.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableSelectSql.spec.ts @@ -239,6 +239,20 @@ describe("tableMetaWithoutOptionalDatabaseQualifier — copy extractors (#9326)" expect(tableMetaWithoutOptionalDatabaseQualifier({ schema: "public", tableName: "users" }, "postgres", false)).toEqual({ schema: undefined, tableName: "users" }); }); + it("covers the copy-SQL dialects reported in #11195", () => { + // Dameng exposes schemas as databases; a copied INSERT/UPDATE must still be + // able to opt into the schema prefix, so the qualifier only drops when the + // extractor option is off. + const damengMeta = { schema: "SYSDBA", tableName: "PEOPLE" }; + expect(tableMetaWithoutOptionalDatabaseQualifier(damengMeta, "dameng", false)).toEqual({ schema: undefined, tableName: "PEOPLE" }); + expect(tableMetaWithoutOptionalDatabaseQualifier(damengMeta, "dameng", true)).toBe(damengMeta); + // MySQL family and ClickHouse address tables as database.table. + const clickhouseMeta = { database: "analytics", tableName: "events" }; + expect(tableMetaWithoutOptionalDatabaseQualifier(clickhouseMeta, "clickhouse", false)).toEqual({ database: undefined, tableName: "events" }); + expect(tableMetaWithoutOptionalDatabaseQualifier(clickhouseMeta, "clickhouse", true)).toBe(clickhouseMeta); + expect(tableMetaWithoutOptionalDatabaseQualifier({ database: "appdb", tableName: "people" }, "goldendb", false)).toEqual({ database: undefined, tableName: "people" }); + }); + it("returns undefined tableMeta unchanged", () => { expect(tableMetaWithoutOptionalDatabaseQualifier(undefined, "mysql", false)).toBeUndefined(); }); diff --git a/apps/desktop/src/lib/dataGrid/dataGridCopyExtractor.ts b/apps/desktop/src/lib/dataGrid/dataGridCopyExtractor.ts index c05221a59d..ddeabbb54a 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridCopyExtractor.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridCopyExtractor.ts @@ -29,7 +29,9 @@ export const DATA_GRID_DEFAULT_COPY_PREFERENCES: readonly DataGridCopyPreference ]; export const DATA_GRID_EXTRACTOR_CONTRACT_VERSION = 1 as const; -export const DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION = 1 as const; +// v2:复制/导出 SQL 的「包含数据库名称」改为由提取器自身的勾选框决定,默认关闭(不带库名/模式名), +// 历史持久化的 `true` 由 settingsStore 的迁移归位到新默认。 +export const DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION = 2 as const; export const DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS: Record = { raw: { category: "raw" }, @@ -151,7 +153,7 @@ export const DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS: DataGridExtractorOptions = { skipGeneratedColumns: true, insertMode: "merged", excludePrimaryKeysFromInsert: false, - includeDatabaseName: true, + includeDatabaseName: false, quoteIdentifiers: true, temporalFormat: "native", }, @@ -184,7 +186,7 @@ export function normalizeDataGridExtractorOptions(value: unknown): DataGridExtra skipGeneratedColumns: sql.skipGeneratedColumns !== false, insertMode: sql.insertMode === "row-by-row" ? "row-by-row" : "merged", excludePrimaryKeysFromInsert: sql.excludePrimaryKeysFromInsert === true, - includeDatabaseName: sql.includeDatabaseName !== false, + includeDatabaseName: sql.includeDatabaseName === true, quoteIdentifiers: sql.quoteIdentifiers !== false, temporalFormat: sql.temporalFormat === "string" ? "string" : "native", }, diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts index a81b7ccb67..5dad4f0a31 100644 --- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts +++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts @@ -611,6 +611,23 @@ describe("normalizeEditorSettings", () => { expect(configured.dataGridExtractorOptions.dsv.nullText).toBe("NULL"); }); + it("resets the copy extractor database-name opt-in once when migrating to v2", () => { + const legacy = normalizeEditorSettings({ + dataGridExtractorOptionsMigrationVersion: 1, + dataGridExtractorOptions: { sql: { includeDatabaseName: true } }, + }); + // 历史持久化的「包含数据库名称」归位到新默认:复制/导出 SQL 默认不带库名/模式名 + expect(legacy.dataGridExtractorOptions.sql.includeDatabaseName).toBe(false); + expect(legacy.dataGridExtractorOptionsMigrationVersion).toBe(DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION); + + const current = normalizeEditorSettings({ + dataGridExtractorOptionsMigrationVersion: DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION, + dataGridExtractorOptions: { sql: { includeDatabaseName: true } }, + }); + // 迁移之后用户的显式勾选必须被保留 + expect(current.dataGridExtractorOptions.sql.includeDatabaseName).toBe(true); + }); + it("defaults retained result runs to tiled tabs and preserves list mode", () => { expect(normalizeEditorSettings({}).resultRunDisplayMode).toBe("tabs"); expect(normalizeEditorSettings({ resultRunDisplayMode: "list" }).resultRunDisplayMode).toBe("list"); diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 0ebe090294..2ed92aaa88 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -1783,7 +1783,14 @@ export function normalizeEditorSettings(settings: Partial, exist const savedExtractorMigrationVersion = settings.dataGridExtractorOptionsMigrationVersion; const normalizedExtractorOptions = normalizeDataGridExtractorOptions(settings.dataGridExtractorOptions); const isLegacyExtractorOptions = typeof savedExtractorMigrationVersion !== "number" || savedExtractorMigrationVersion < DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION; - const dataGridExtractorOptions = isLegacyExtractorOptions && normalizedExtractorOptions.dsv.nullText === "NULL" ? { ...normalizedExtractorOptions, dsv: { ...normalizedExtractorOptions.dsv, nullText: "" } } : normalizedExtractorOptions; + const dataGridExtractorOptions = (() => { + if (!isLegacyExtractorOptions) return normalizedExtractorOptions; + // v1:旧的 "NULL" 文本迁移为空串 + const nullTextMigrated = normalizedExtractorOptions.dsv.nullText === "NULL" ? { ...normalizedExtractorOptions, dsv: { ...normalizedExtractorOptions.dsv, nullText: "" } } : normalizedExtractorOptions; + // v2:复制/导出 SQL 的「包含数据库名称」改为由提取器勾选框决定且默认关闭, + // 历史持久化的 true 一并归位到新默认(想带库名/模式名的用户重新勾选即可) + return nullTextMigrated.sql.includeDatabaseName ? { ...nullTextMigrated, sql: { ...nullTextMigrated.sql, includeDatabaseName: false } } : nullTextMigrated; + })(); // Preserve the explicit intent behind the legacy update controls. Disabling // update reminders was a full opt-out; disabling automatic downloads only // opted out of downloading the DBX package itself. @@ -2302,7 +2309,12 @@ export const useSettingsStore = defineStore("settings", () => { const needsWelcomePageDefaultMigration = typeof savedSettings.welcomePageModeDefaultVersion !== "number" || savedSettings.welcomePageModeDefaultVersion < WELCOME_PAGE_DEFAULT_VERSION; const needsTabNavigationShortcutMigration = needsTabNavigationHistoryShortcutMigration(savedSettings.shortcuts); const savedNullText = (savedSettings.dataGridExtractorOptions as Partial | undefined)?.dsv?.nullText; - const needsDataGridExtractorOptionsMigration = (typeof savedSettings.dataGridExtractorOptionsMigrationVersion !== "number" || savedSettings.dataGridExtractorOptionsMigrationVersion < DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION) && savedNullText === "NULL"; + // v2 also resets a historically persisted includeDatabaseName=true, so + // trigger the eager persist for that stale disk value even when the + // v1 "NULL" marker is already migrated away. + const savedSqlIncludeDatabaseName = (savedSettings.dataGridExtractorOptions as Partial | undefined)?.sql?.includeDatabaseName; + const needsDataGridExtractorOptionsMigration = + (typeof savedSettings.dataGridExtractorOptionsMigrationVersion !== "number" || savedSettings.dataGridExtractorOptionsMigrationVersion < DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION) && (savedNullText === "NULL" || savedSqlIncludeDatabaseName === true); const savedUpdateDownloadSource = (saved as { updateDownloadSource?: unknown }).updateDownloadSource; if (savedUpdateDownloadSource === "atomgit" || needsExecuteModeDefaultMigration || needsWelcomePageDefaultMigration || needsTabNavigationShortcutMigration || needsSidebarBrowseObjectsMigration || needsDataGridExtractorOptionsMigration) { // Persist one-time migrations so removed or unsafe defaults cannot reappear.