fix(gaussdb): support M-mode type metadata

This commit is contained in:
Andrew
2026-08-12 02:05:54 +08:00
committed by GitHub
parent dbe3375739
commit 26bb10dad0
13 changed files with 904 additions and 146 deletions
@@ -115,7 +115,7 @@ public abstract class ConfiguredJdbcAgent extends AbstractJdbcAgent {
@Override
public List<IndexInfo> listIndexes(String schema, String table) {
return StandardJdbcMetadata.INSTANCE.listIndexes(requireConnection(), schema, table);
return StandardJdbcMetadata.INSTANCE.listIndexes(requireConnection(), profile, configuredDatabase, schema, table);
}
@Override
@@ -207,12 +207,28 @@ public final class StandardJdbcMetadata {
});
}
public List<IndexInfo> listIndexes(Connection conn, String schema, String table) {
public List<IndexInfo> listIndexes(
Connection conn,
JdbcAgentProfile profile,
String configuredDatabase,
String schema,
String table
) {
return unchecked(() -> {
List<IndexInfo> result = listIndexesInternal(conn, null, schema, table);
if (result.isEmpty() && profile.getCatalogFallbackEnabled() && hasConfiguredDatabase(configuredDatabase)) {
result = listIndexesInternal(conn, configuredDatabase, schema, table);
}
return result;
});
}
private List<IndexInfo> listIndexesInternal(Connection conn, String catalog, String schema, String table) {
return unchecked(() -> {
DatabaseMetaData meta = conn.getMetaData();
Map<String, List<IndexColumn>> indexes = new LinkedHashMap<>();
Map<String, Boolean> unique = new LinkedHashMap<>();
try (ResultSet rs = meta.getIndexInfo(null, blankToNull(schema), table, false, false)) {
try (ResultSet rs = meta.getIndexInfo(blankToNull(catalog), blankToNull(schema), table, false, false)) {
while (rs.next()) {
String name = rs.getString("INDEX_NAME");
String column = rs.getString("COLUMN_NAME");
@@ -419,13 +419,62 @@ class StandardJdbcMetadataTest {
rows()
);
List<IndexInfo> indexes = StandardJdbcMetadata.INSTANCE.listIndexes(conn, "APP", "ORDERS");
List<IndexInfo> indexes = StandardJdbcMetadata.INSTANCE.listIndexes(conn, profile, null, "APP", "ORDERS");
assertEquals(1, indexes.size());
assertEquals(Arrays.asList("A", "B"), indexes.get(0).getColumns());
assertFalse(indexes.get(0).getIs_unique());
}
@Test
void listIndexesUsesConfiguredCatalogOnlyWhenProfileAllowsFallback() {
Connection enabledConn = catalogIndexFallbackConnection(
"TENANTDB",
rows(),
rows(row("INDEX_NAME", "IDX_ORDERS", "COLUMN_NAME", "ID", "ORDINAL_POSITION", (short) 1, "NON_UNIQUE", false))
);
List<IndexInfo> enabledIndexes = StandardJdbcMetadata.INSTANCE.listIndexes(
enabledConn,
profile,
"TENANTDB",
"APP",
"ORDERS"
);
assertEquals(1, enabledIndexes.size());
JdbcAgentProfile fallbackDisabledProfile = new JdbcAgentProfile(
"example.Driver",
"jdbc:example://{host}:{port}/{database}",
0,
false,
Collections.emptySet(),
Collections.singletonList("TABLE"),
"\"",
"SET SCHEMA",
false,
false,
false,
false
);
Connection disabledConn = catalogIndexFallbackConnection(
"TENANTDB",
rows(),
rows(row("INDEX_NAME", "IDX_ORDERS", "COLUMN_NAME", "ID", "ORDINAL_POSITION", (short) 1, "NON_UNIQUE", false))
);
List<IndexInfo> disabledIndexes = StandardJdbcMetadata.INSTANCE.listIndexes(
disabledConn,
fallbackDisabledProfile,
"TENANTDB",
"APP",
"ORDERS"
);
assertTrue(disabledIndexes.isEmpty());
}
@Test
void mapsForeignKeysAndEmptyTriggers() {
Connection conn = connection(
@@ -667,6 +716,31 @@ class StandardJdbcMetadataTest {
});
}
private static Connection catalogIndexFallbackConnection(
String fallbackCatalog,
ResultSet defaultIndexes,
ResultSet fallbackIndexes
) {
DatabaseMetaData meta = proxy(DatabaseMetaData.class, new MethodHandler() {
@Override
public Object handle(Method method, Object[] args) {
if ("getIndexInfo".equals(method.getName())) {
return fallbackCatalog.equals(args[0]) ? fallbackIndexes : defaultIndexes;
}
return defaultValue(method.getReturnType());
}
});
return proxy(Connection.class, new MethodHandler() {
@Override
public Object handle(Method method, Object[] args) {
if ("getMetaData".equals(method.getName())) {
return meta;
}
return defaultValue(method.getReturnType());
}
});
}
private static Connection catalogPrimaryKeyFallbackConnection(
String fallbackCatalog,
ResultSet defaultPrimaryKeys,
+1 -1
View File
@@ -90,7 +90,7 @@ fn tdengine_websocket_live_compatibility() {
format!("CREATE DATABASE IF NOT EXISTS {database}"),
format!("CREATE STABLE IF NOT EXISTS {database}.meters (ts TIMESTAMP, current FLOAT, voltage INT, active BOOL, note NCHAR(32)) TAGS (location NCHAR(64), group_id INT)"),
format!("CREATE TABLE IF NOT EXISTS {database}.d1001 USING {database}.meters TAGS ('California.SanFrancisco', 2)"),
format!("INSERT INTO {database}.d1001 VALUES ('2026-08-04 10:00:00.001', 10.1, 220, true, 'first') ('2026-08-04 10:00:00.002', 10.2, 221, false, 'second') ('2026-08-04 10:00:00.003', 10.3, 222, true, 'third')")
format!("INSERT INTO {database}.d1001 VALUES (1785808800001, 10.1, 220, true, 'first') (1785808800002, 10.2, 221, false, 'second') (1785808800003, 10.3, 222, true, 'third')")
]
}),
);
+1
View File
@@ -10,6 +10,7 @@ gbase8s native-pushdown java-sql Uses systables with type, filter, stable order,
goldendb native-pushdown java-sql Uses information_schema with type, filter, stable order, and LIMIT/OFFSET.
h2 native-pushdown java-sql Uses INFORMATION_SCHEMA with type, filter, stable order, and LIMIT/OFFSET.
hive intentional-fallback java-sql Uses Hive metadata paths where stable server-side fuzzy paging is not portable, common constraints filter locally.
hive-go shared-fallback native-go Uses HiveServer2 metadata APIs, then applies stable filtering and paging in the native agent with a SHOW TABLES fallback.
informix native-pushdown java-sql Uses systables/sysprocedures with type, filter, stable order, and SKIP/FIRST.
iotdb shared-fallback native-go Uses SHOW DEVICES or SHOW TABLES DETAILS for Tree/Table metadata, then applies stable filtering and paging in the native agent.
kingbase-go shared-fallback native-go Uses sys_catalog/information_schema for compatibility-aware metadata, then applies stable filtering and paging in the native agent.
1 driver strategy scope reason
10 goldendb native-pushdown java-sql Uses information_schema with type, filter, stable order, and LIMIT/OFFSET.
11 h2 native-pushdown java-sql Uses INFORMATION_SCHEMA with type, filter, stable order, and LIMIT/OFFSET.
12 hive intentional-fallback java-sql Uses Hive metadata paths where stable server-side fuzzy paging is not portable, common constraints filter locally.
13 hive-go shared-fallback native-go Uses HiveServer2 metadata APIs, then applies stable filtering and paging in the native agent with a SHOW TABLES fallback.
14 informix native-pushdown java-sql Uses systables/sysprocedures with type, filter, stable order, and SKIP/FIRST.
15 iotdb shared-fallback native-go Uses SHOW DEVICES or SHOW TABLES DETAILS for Tree/Table metadata, then applies stable filtering and paging in the native agent.
16 kingbase-go shared-fallback native-go Uses sys_catalog/information_schema for compatibility-aware metadata, then applies stable filtering and paging in the native agent.
@@ -5,6 +5,8 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { useToast } from "@/composables/useToast";
import { copyToClipboard } from "@/lib/common/clipboard";
import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp";
import type { DatabaseType } from "@/types/database";
export interface ColumnInfo {
name: string;
@@ -17,10 +19,12 @@ export interface ColumnInfo {
extra?: string | null;
}
defineProps<{
const props = defineProps<{
columns: ColumnInfo[];
loading: boolean;
error?: string;
databaseType?: DatabaseType;
isGaussdbM?: boolean;
}>();
const emit = defineEmits<{
@@ -30,6 +34,14 @@ const emit = defineEmits<{
const { t } = useI18n();
const { toast } = useToast();
function displayDataType(dataType: string | undefined): string {
if (!dataType) return "—";
if (props.databaseType === "gaussdb" && props.isGaussdbM) {
return gaussdbMTypeDisplayName(dataType);
}
return dataType;
}
async function copyText(text: string) {
try {
await copyToClipboard(text);
@@ -81,8 +93,8 @@ async function copyText(text: string) {
<div class="rounded-md border border-border divide-y divide-border text-xs">
<div class="flex items-center justify-between px-2 py-1">
<span class="text-muted-foreground">{{ t("grid.columnType") }}</span>
<span class="font-medium max-w-[180px] truncate" :title="col.dataType">
{{ col.dataType || "—" }}
<span class="font-medium max-w-[180px] truncate" :title="displayDataType(col.dataType)">
{{ displayDataType(col.dataType) }}
</span>
</div>
<div class="flex items-center justify-between px-2 py-1">
File diff suppressed because it is too large Load Diff
@@ -1040,7 +1040,15 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
@click-column="onHandleClickColumn"
@close-column-panel="onHandleCloseColumnPanel"
/>
<ColumnInfoPanel v-if="showColumnInfo" :columns="columnInfoColumns" :loading="columnInfoLoading" :error="columnInfoError" @close="closeColumnInfo" />
<ColumnInfoPanel
v-if="showColumnInfo"
:columns="columnInfoColumns"
:loading="columnInfoLoading"
:error="columnInfoError"
:database-type="activeEffectiveDatabaseType"
:is-gaussdb-m="activeEffectiveDatabaseType === 'gaussdb' && activeResultConnection?.driver_profile?.toLowerCase() === 'gaussdb-m'"
@close="closeColumnInfo"
/>
<Button v-if="hasQueryOutput && !resultsPaneOpen" variant="secondary" size="sm" class="absolute bottom-3 right-3 z-20 h-7 gap-1.5 rounded-full border bg-background/95 px-3 text-xs shadow-lg hover:bg-accent" @click="resultsPaneOpen = true">
<ChevronUp class="h-3.5 w-3.5" />
{{ t("editor.showResultsPane") }}
@@ -125,6 +125,7 @@ import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableI
import { createSidePanelRequestGuard } from "@/lib/table/sidePanelRequestGuard";
import { runBatchTableTruncate } from "@/lib/table/batchTableTruncate";
import { tableColumnDefaultDisplayValue } from "@/lib/table/tableColumnDefaultPresentation";
import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp";
import { cacheObjectBrowserRows, createObjectBrowserRowsCacheWriteToken, getCachedObjectBrowserRows, type ObjectBrowserRowsCacheScope, type ObjectBrowserRowsCacheWriteToken } from "@/lib/table/objectBrowserRowsCache";
import { createObjectBrowserRowsLoadGuard, type ObjectBrowserRowsLoadHandle } from "@/lib/table/objectBrowserRowsLoadGuard";
@@ -202,8 +203,16 @@ const sidePanelGuard = createSidePanelRequestGuard();
const sidePanelRef = ref<InstanceType<typeof CustomTypeInfoPanel> | null>(null);
const tableMetadataCapabilities = computed<TableMetadataCapabilities>(() => getTableMetadataCapabilities(effectiveDatabaseType.value));
const effectiveDatabaseType = computed(() => effectiveDatabaseTypeForConnection(props.connection) ?? props.connection.db_type);
const isGaussdbM = computed(() => effectiveDatabaseType.value === "gaussdb" && props.connection.driver_profile?.toLowerCase() === "gaussdb-m");
const isVictoriaMetrics = computed(() => effectiveDatabaseType.value === "victoriametrics");
const objectRowsLabel = computed(() => t(isVictoriaMetrics.value ? "objects.series" : "objects.rows"));
function gaussdbMColumnType(dataType: string): string {
if (isGaussdbM.value) {
return gaussdbMTypeDisplayName(dataType);
}
return dataType;
}
const tableStructureDatabaseType = computed(() => tableStructureDatabaseTypeForConnection(props.connection) ?? props.connection.db_type);
const sourceEditableText = ref("");
const sourceDraft = ref("");
@@ -3200,7 +3209,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
{{ column.comment }}
</div>
</td>
<td class="px-3 py-2 font-mono text-[11px] text-muted-foreground">{{ column.data_type }}</td>
<td class="px-3 py-2 font-mono text-[11px] text-muted-foreground">{{ gaussdbMColumnType(column.data_type) }}</td>
<td class="px-3 py-2">{{ column.is_nullable ? "YES" : "NO" }}</td>
<td data-table-info-column-default class="max-w-56 px-3 py-2 font-mono text-[11px]" :class="{ 'text-muted-foreground/70': column.column_default == null }" :title="column.column_default ?? undefined">
<span class="block max-w-56 truncate">{{ tableColumnDefaultDisplayValue(column.column_default) }}</span>
@@ -32,7 +32,7 @@ import { invalidateTableMetadataCache } from "@/lib/metadata/tableMetadataCache"
import { type BuildTableStructureChangeSqlOptions, type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql";
import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/lib/table/tableColumnTemplates";
import { getMysqlDataTypeHelp } from "@/lib/table/mysqlDataTypeHelp";
import { getPostgresDataTypeHelp } from "@/lib/table/postgresDataTypeHelp";
import { getPostgresDataTypeHelp, gaussdbMTypeDisplayName } 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";
@@ -1079,6 +1079,16 @@ function dataTypeTooltip(option: string): string | undefined {
return undefined;
}
function gaussdbMDataTypeDisplayName(option: string): string {
if (databaseType.value === "gaussdb") {
const conn = connection.value;
if (conn?.driver_profile?.toLowerCase() === "gaussdb-m") {
return gaussdbMTypeDisplayName(option);
}
}
return option;
}
async function loadDynamicDataTypeOptions() {
const requestId = ++dataTypeOptionsRequestId;
const connectionId = props.connectionId;
@@ -2801,10 +2811,11 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
:loading-text="t('common.loading')"
:allow-custom="true"
:option-tooltip="dataTypeTooltip"
:display-name="gaussdbMDataTypeDisplayName"
:trigger-class="[structureMonoControlClass, 'w-full']"
@update:model-value="(v: string) => updateColumnDataType(column, v)"
/>
<Input v-else :model-value="splitDataType(column.dataType).baseType" :class="[structureMonoControlClass, 'w-full']" disabled />
<Input v-else :model-value="gaussdbMDataTypeDisplayName(splitDataType(column.dataType).baseType)" :class="[structureMonoControlClass, 'w-full']" disabled />
</td>
<td v-if="columnEditorControls.length" :class="structureCellClass">
<Popover v-if="isMysqlEnumDataType(databaseType, column.dataType)">
@@ -9,6 +9,7 @@ import * as api from "@/lib/backend/api";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { copyToClipboard } from "@/lib/common/clipboard";
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp";
import { joinExportedDdls } from "@/lib/export/ddlExport";
import { translateBackendError } from "@/i18n/backend-errors";
import { sidebarStructureExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
@@ -107,8 +108,12 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
return includeTable ? [t("contextMenu.structureDocTable"), ...headers] : headers;
}
function columnDocCells(target: TreeNode, column: ColumnInfo, includeTable: boolean): unknown[] {
const cells = [column.name, column.data_type, column.is_primary_key ? t("contextMenu.structureDocYes") : t("contextMenu.structureDocNo"), column.is_nullable ? t("contextMenu.structureDocYes") : t("contextMenu.structureDocNo"), column.column_default, column.comment];
function columnDocCells(target: TreeNode & { connectionId: string }, column: ColumnInfo, includeTable: boolean): unknown[] {
const config = connectionStore.getConfig(target.connectionId);
const isGaussdbM = effectiveDatabaseTypeForConnection(config) === "gaussdb" && config?.driver_profile?.toLowerCase() === "gaussdb-m";
const sourceDataType = column.data_type;
const dataType = isGaussdbM && sourceDataType ? gaussdbMTypeDisplayName(sourceDataType) : sourceDataType;
const cells = [column.name, dataType, column.is_primary_key ? t("contextMenu.structureDocYes") : t("contextMenu.structureDocNo"), column.is_nullable ? t("contextMenu.structureDocYes") : t("contextMenu.structureDocNo"), column.column_default, column.comment];
return includeTable ? [structureTargetName(target), ...cells] : cells;
}
@@ -153,3 +153,26 @@ function floatHelpKey(normalizedType: string): "real" | "double" | undefined {
if (value >= 1 && value <= 24) return "real";
return value >= 25 && value <= 53 ? "double" : undefined;
}
/**
* Converts a GaussDB M-mode PostgreSQL internal type name to its MySQL-style
* display name. Returns the original name if no conversion applies.
*
* GaussDB M mode returns PostgreSQL internal type names (e.g. bpchar, int4,
* numeric, int1) but should display MySQL-compatible names.
*/
export function gaussdbMTypeDisplayName(typeName: string): string {
const s = typeName.trim().toLowerCase();
if (s === "bpchar") return "char";
if (s === "int4" || s === "integer") return "int";
if (s === "int2" || s === "smallint") return "smallint";
if (s === "int8" || s === "bigint") return "bigint";
if (s === "int1") return "tinyint";
if (s === "numeric") return "decimal";
if (s === "float4" || s === "real") return "float";
if (s === "float8" || s === "double precision") return "double";
if (s === "bool" || s === "boolean") return "boolean";
if (s === "character varying") return "varchar";
if (s === "character") return "char";
return typeName;
}
+4 -1
View File
@@ -73,6 +73,7 @@ import { shouldMarkDisconnected } from "@/lib/connection/connectionHealth";
import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout";
import { loadTimeoutInheritanceBackup, saveTimeoutInheritanceBackup } from "@/lib/connection/timeoutInheritanceBackup";
import { migrateSqlServerLegacyCompatibilityConfig, requiresSqlServerLegacyCompatibilityComponent, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp";
import { deleteTabResultSnapshotsForOwner } from "@/lib/tabs/tabResultCache";
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterSchemaNamesForConnection, filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases";
import {
@@ -5066,11 +5067,13 @@ export const useConnectionStore = defineStore("connection", () => {
const columns = await api.getColumns(connectionId, database, querySchema, table, catalog);
const targetNode = treeNodeLoadTarget(load);
if (!targetNode) return;
const connConfig = getConfig(connectionId);
const isGaussdbM = effectiveDatabaseTypeForConnection(connConfig) === "gaussdb" && connConfig?.driver_profile?.toLowerCase() === "gaussdb-m";
setChildren(
targetNode,
columns.map((col) => ({
id: `${parentId}:${col.name}`,
label: `${col.name} (${col.data_type})`,
label: `${col.name} (${isGaussdbM ? gaussdbMTypeDisplayName(col.data_type) : col.data_type})`,
type: "column" as const,
connectionId,
database,