From bc60339806d6acdead87f8b616ffd02993d477b4 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 4 Aug 2026 22:58:51 +0800 Subject: [PATCH] fix(postgres): gate index INCLUDE by server version Closes #5357 --- .../structure/TableStructureEditor.vue | 6 ++-- .../table/tableStructureCapabilities.spec.ts | 36 ++++++++++++++++++- .../lib/table/tableStructureCapabilities.ts | 26 +++++++++++++- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index df7a701692..6822672595 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -36,7 +36,7 @@ import { getPostgresDataTypeHelp } from "@/lib/table/postgresDataTypeHelp"; import { getSqliteDataTypeHelp } from "@/lib/table/sqliteDataTypeHelp"; import { getTableMetadataCapabilities, firstStructureMetadataTab, isStructureMetadataTabSupported } from "@/lib/table/tableMetadataCapabilities"; import { hasTableStructureRefreshWork, unloadedTableStructureRefreshScope, visibleTableStructureRefreshScope, type TableStructureRefreshScope } from "@/lib/table/tableStructureMetadataLoading"; -import { canAddTableStructureColumn, getTableStructureCapabilities, hasLocalTableColumnOrderChange, isPhysicalTableColumnOrderChange, supportsLocalTableColumnReorder } from "@/lib/table/tableStructureCapabilities"; +import { canAddTableStructureColumn, getTableStructureCapabilities, hasLocalTableColumnOrderChange, isPhysicalTableColumnOrderChange, sanitizeStructureIndexesForCapabilities, supportsLocalTableColumnReorder } from "@/lib/table/tableStructureCapabilities"; import { orderedColumnIndexes, uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder"; import { loadTableDataGridColumnOrder, notifyTableDataGridColumnOrderChanged, removeTableDataGridColumnOrder, saveTableDataGridColumnOrder, tableDataGridColumnOrderScopeKey } from "@/lib/dataGrid/dataGridColumnLayoutStorage"; import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; @@ -620,7 +620,7 @@ function onIndexColResize(e: MouseEvent, col: number) { const connection = computed(() => (props.connectionId ? store.getConfig(props.connectionId) : undefined)); const databaseType = computed(() => tableStructureDatabaseTypeForConnection(connection.value)); const usesMysql8SafeDefaults = computed(() => databaseType.value === "mysql" && connection.value?.db_type === "mysql" && connection.value.driver_profile === "mysql"); -const structureCapabilities = computed(() => getTableStructureCapabilities(databaseType.value, connection.value?.db_type)); +const structureCapabilities = computed(() => getTableStructureCapabilities(databaseType.value, connection.value?.db_type, connection.value?.database_info?.productVersion)); const tableMetadataCapabilities = computed(() => getTableMetadataCapabilities(databaseType.value)); const structureDialect = computed(() => structureCapabilities.value.dialect); const isTableCommentDisabled = computed(() => !structureCapabilities.value.comment); @@ -1143,7 +1143,7 @@ function structureChangeOptions(): BuildTableStructureChangeSqlOptions { schema: props.schema, tableName: isCreateMode.value ? newTableName.value.trim() : props.tableName || "", columns: columns.value, - indexes: indexes.value, + indexes: sanitizeStructureIndexesForCapabilities(indexes.value, structureCapabilities.value), foreignKeys: foreignKeys.value, triggers: triggers.value, tableComment: tableComment.value, diff --git a/apps/desktop/src/lib/__tests__/table/tableStructureCapabilities.spec.ts b/apps/desktop/src/lib/__tests__/table/tableStructureCapabilities.spec.ts index 421612900d..050c3a0415 100644 --- a/apps/desktop/src/lib/__tests__/table/tableStructureCapabilities.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableStructureCapabilities.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { getTableStructureCapabilities, hasLocalTableColumnOrderChange, isPhysicalTableColumnOrderChange, supportsLocalTableColumnReorder } from "@/lib/table/tableStructureCapabilities"; +import { getTableStructureCapabilities, hasLocalTableColumnOrderChange, isPhysicalTableColumnOrderChange, sanitizeStructureIndexesForCapabilities, supportsLocalTableColumnReorder } from "@/lib/table/tableStructureCapabilities"; describe("tableStructureCapabilities", () => { it("uses table rebuilds only for native SQLite connections", () => { @@ -28,6 +28,40 @@ describe("tableStructureCapabilities", () => { expect(getTableStructureCapabilities("postgres", "postgres").alterStrategy).toBe("direct"); }); + it("enables PostgreSQL index INCLUDE only for known supported versions", () => { + expect(getTableStructureCapabilities("postgres", "postgres", "PostgreSQL 10.15 (Transwarp) on x86_64-pc-linux-gnu").indexInclude).toBe(false); + expect(getTableStructureCapabilities("postgres", "postgres", "9.6.24").indexInclude).toBe(false); + expect(getTableStructureCapabilities("postgres", "postgres", "11.0").indexInclude).toBe(true); + expect(getTableStructureCapabilities("postgres", "postgres", "PostgreSQL 16.14").indexInclude).toBe(true); + expect(getTableStructureCapabilities("postgres", "postgres", undefined).indexInclude).toBe(true); + expect(getTableStructureCapabilities("postgres", "postgres", "unknown").indexInclude).toBe(true); + expect(getTableStructureCapabilities("sqlserver", "sqlserver", "10.0").indexInclude).toBe(true); + }); + + it("removes unsupported included columns before SQL generation", () => { + const index = { + id: "new:index", + name: "example_idx", + columns: ["key_column"], + isUnique: false, + isPrimary: false, + filter: "", + indexType: "BTREE", + includedColumns: ["included_column"], + comment: "", + markedForDrop: false, + }; + + const indexes = [index]; + const postgres10Indexes = sanitizeStructureIndexesForCapabilities(indexes, getTableStructureCapabilities("postgres", "postgres", "10.15")); + expect(postgres10Indexes).not.toBe(indexes); + expect(postgres10Indexes[0].includedColumns).toEqual([]); + expect(index.includedColumns).toEqual(["included_column"]); + + const postgres11Indexes = sanitizeStructureIndexesForCapabilities(indexes, getTableStructureCapabilities("postgres", "postgres", "11.0")); + expect(postgres11Indexes).toBe(indexes); + }); + it("disables persisted comment editing for IRIS without disabling other structure changes", () => { expect(getTableStructureCapabilities("iris", "iris")).toMatchObject({ comment: false, diff --git a/apps/desktop/src/lib/table/tableStructureCapabilities.ts b/apps/desktop/src/lib/table/tableStructureCapabilities.ts index a48822d862..26f20db813 100644 --- a/apps/desktop/src/lib/table/tableStructureCapabilities.ts +++ b/apps/desktop/src/lib/table/tableStructureCapabilities.ts @@ -1,4 +1,5 @@ import type { DatabaseType } from "@/types/database"; +import type { EditableStructureIndex } from "@/lib/table/tableStructureEditorSql"; export type TableStructureDialect = "mysql" | "postgres" | "sqlite" | "duckdb" | "sqlserver" | "oracle" | "h2" | "clickhouse" | "informix" | "influxdb" | "unsupported"; export type TableStructureAlterStrategy = "none" | "direct" | "sqlite-rebuild"; @@ -111,6 +112,11 @@ const postgresCapabilities = capabilities({ foreignKey: true, }); +const postgresBefore11Capabilities = capabilities({ + ...postgresCapabilities, + indexInclude: false, +}); + const redshiftCapabilities = capabilities({ ...postgresCapabilities, createIndex: false, @@ -343,11 +349,29 @@ const capabilityByType: Partial manticoresearch: manticoreSearchCapabilities, }; -export function getTableStructureCapabilities(dbType?: DatabaseType, connectionDbType?: DatabaseType): TableStructureCapabilities { +function postgresMajorVersion(productVersion?: string): number | undefined { + const normalized = productVersion?.trim(); + if (!normalized) return undefined; + const match = normalized.match(/\bPostgreSQL\s+(\d+)(?:\.\d+)?\b/i) ?? normalized.match(/^(\d+)(?:\.\d+)?\b/); + if (!match) return undefined; + const majorVersion = Number.parseInt(match[1], 10); + return Number.isFinite(majorVersion) ? majorVersion : undefined; +} + +export function getTableStructureCapabilities(dbType?: DatabaseType, connectionDbType?: DatabaseType, productVersion?: string): TableStructureCapabilities { if (dbType === "sqlite" && connectionDbType === "sqlite") return nativeSqliteCapabilities; + if (dbType === "postgres") { + const majorVersion = postgresMajorVersion(productVersion); + if (majorVersion !== undefined && majorVersion < 11) return postgresBefore11Capabilities; + } return dbType ? (capabilityByType[dbType] ?? unsupportedCapabilities) : unsupportedCapabilities; } +export function sanitizeStructureIndexesForCapabilities(indexes: EditableStructureIndex[], capabilities: Pick): EditableStructureIndex[] { + if (capabilities.indexInclude || indexes.every((index) => index.includedColumns.length === 0)) return indexes; + return indexes.map((index) => (index.includedColumns.length === 0 ? index : { ...index, includedColumns: [] })); +} + export function canEditTableStructure(dbType?: DatabaseType): boolean { const caps = getTableStructureCapabilities(dbType); return caps.createTable || caps.addColumn || caps.alterExistingColumn || caps.createIndex || caps.dropIndex;