From 9935f8e7feedbf4ba542fe2b674fcd1f12436b20 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 4 Jun 2026 15:09:56 +0800 Subject: [PATCH] fix: improve iris schema metadata completion --- .../src/components/editor/QueryEditor.vue | 36 +++- .../src/components/layout/ContentArea.vue | 1 + .../src/components/objects/ObjectBrowser.vue | 2 + .../main/java/app/dbx/jdbc/DbxJdbcPlugin.java | 161 ++++++++++++++---- .../java/app/dbx/jdbc/DbxJdbcPluginTest.java | 8 + 5 files changed, 167 insertions(+), 41 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 94dd6df4d..17652fe23 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -83,6 +83,7 @@ const props = defineProps<{ modelValue: string; connectionId?: string; database?: string; + schema?: string; databaseType?: DatabaseType; dialect?: "mysql" | "postgres" | "sqlserver"; formatDialect?: SqlFormatDialect; @@ -451,7 +452,8 @@ function identifierRangeAt(sql: string, pos: number): { from: number; to: number } function completionCacheKey(table: { name: string; schema?: string | null }) { - return table.schema ? `${table.schema}.${table.name}` : table.name; + const schema = table.schema ?? props.schema; + return schema ? `${schema}.${table.name}` : table.name; } async function ensureColumnsForTable(table: { name: string; schema?: string | null }) { @@ -461,7 +463,7 @@ async function ensureColumnsForTable(table: { name: string; schema?: string | nu props.connectionId, props.database, table.name, - table.schema ?? undefined, + table.schema ?? props.schema, ); if (columns.length === 0) return; cachedColumnsByTable.set(cacheKey, columns); @@ -470,7 +472,7 @@ async function ensureColumnsForTable(table: { name: string; schema?: string | nu async function ensureForeignKeysForTable(table: { name: string; schema?: string | null }) { const cacheKey = completionCacheKey(table); if (cachedForeignKeysByTable.has(cacheKey) || !props.connectionId || props.database == null) return; - const querySchema = table.schema ?? props.database; + const querySchema = table.schema ?? props.schema ?? props.database; try { const foreignKeys = await api.listForeignKeys(props.connectionId, props.database, querySchema, table.name); cachedForeignKeysByTable.set( @@ -568,6 +570,7 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number) props.database, name, MAX_COMPLETION_TABLES, + props.schema, ); } @@ -578,6 +581,7 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number) props.database, name, MAX_COMPLETION_TABLES, + props.schema, ); cachedTables = [...cachedTables, ...hoverTables]; table = matchTable(identifier, hoverTables) ?? matchTable(name, hoverTables); @@ -699,6 +703,7 @@ async function enrichSemanticDiagnosticTables(tables: SqlTableReference[]) { props.database, table.name, MAX_COMPLETION_TABLES, + props.schema, ); cachedTables = [...cachedTables, ...matches]; const match = matches.find((item) => item.name.toLowerCase() === table.name.toLowerCase()); @@ -1018,12 +1023,13 @@ async function performAsyncCompletionWithResult( props.connectionId!, props.database!, completionContext.insertTable, - completionContext.insertSchema, + completionContext.insertSchema ?? props.schema, ); if (epoch !== completionEpoch) return null; if (insertCols.length > 0) { - const insertKey = completionContext.insertSchema - ? `${completionContext.insertSchema}.${completionContext.insertTable}` + const insertSchema = completionContext.insertSchema ?? props.schema; + const insertKey = insertSchema + ? `${insertSchema}.${completionContext.insertTable}` : completionContext.insertTable; insertColumnsByTable.set(insertKey, insertCols); } @@ -1041,6 +1047,7 @@ async function performAsyncCompletionWithResult( props.database!, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, + props.schema, ) : cachedTables; if (epoch !== completionEpoch) return null; @@ -1055,6 +1062,7 @@ async function performAsyncCompletionWithResult( props.database!, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, + props.schema, ) : cachedCompletionObjects; if (epoch !== completionEpoch) return null; @@ -1120,7 +1128,7 @@ async function performAsyncCompletionWithResult( if (unresolvedRefs.length > 0) { const lookupGroups = await Promise.all( unresolvedRefs.map((rt) => - connectionStore.listCompletionTables(props.connectionId!, props.database!, rt.name, 20), + connectionStore.listCompletionTables(props.connectionId!, props.database!, rt.name, 20, props.schema), ), ); if (epoch !== completionEpoch) return null; @@ -1159,7 +1167,7 @@ async function performAsyncCompletionWithResult( props.connectionId!, props.database!, refTable.name, - refTable.schema, + refTable.schema ?? props.schema, ); if (epoch !== completionEpoch) return; if (columns.length === 0) return; @@ -1576,6 +1584,7 @@ onMounted(async () => { props.database!, identifier, MAX_COMPLETION_TABLES, + props.schema, ); } @@ -1636,7 +1645,7 @@ onMounted(async () => { props.connectionId!, props.database!, refTable.name, - refTable.schema, + refTable.schema ?? props.schema, ); cachedColumnsByTable.set(cacheKey, cols); } catch { @@ -1720,6 +1729,15 @@ watch( }, ); +watch( + () => props.schema, + () => { + refreshCompletionCache(); + setSemanticDiagnostics([]); + scheduleSemanticDiagnostics(); + }, +); + watch( () => props.forceWordWrap, () => { diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index fec74697a..5c9e16974 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -366,6 +366,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget }); :model-value="activeTab.sql" :connection-id="activeTab.connectionId" :database="activeTab.database" + :schema="activeTab.schema" :database-type="activeConnection?.db_type" :dialect="editorDialect" :format-dialect="activeSqlFormatDialect" diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index df16749b5..8827db051 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -1569,6 +1569,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] { class="min-h-0 flex-1" :connection-id="props.connection.id" :database="props.database" + :schema="selectedSchema" :database-type="props.connection.db_type" :dialect="sourceDialect" :format-dialect="sourceFormatDialect" @@ -1586,6 +1587,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] { class="min-h-0 flex-1" :connection-id="props.connection.id" :database="props.database" + :schema="selectedSchema" :database-type="props.connection.db_type" :dialect="sourceDialect" :format-dialect="sourceFormatDialect" diff --git a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java index 5b8819ede..bf814f623 100644 --- a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java +++ b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java @@ -35,6 +35,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Properties; import java.util.ServiceLoader; import java.util.Set; @@ -43,10 +44,10 @@ import java.util.logging.Logger; public final class DbxJdbcPlugin { private static final ObjectMapper MAPPER = new ObjectMapper(); private static final int MAX_ROWS = 10_000; - private static final JdbcDriverQuirks DEFAULT_QUIRKS = new JdbcDriverQuirks(false, false); - private static final JdbcDriverQuirks YASHAN_QUIRKS = new JdbcDriverQuirks(true, true); - private static final JdbcDriverQuirks IRIS_QUIRKS = new JdbcDriverQuirks(true, false); - private static final JdbcDriverQuirks ORACLE_QUIRKS = new JdbcDriverQuirks(false, true); + private static final JdbcDriverQuirks DEFAULT_QUIRKS = new JdbcDriverQuirks(false, false, false); + private static final JdbcDriverQuirks YASHAN_QUIRKS = new JdbcDriverQuirks(true, true, false); + private static final JdbcDriverQuirks IRIS_QUIRKS = new JdbcDriverQuirks(true, false, true); + private static final JdbcDriverQuirks ORACLE_QUIRKS = new JdbcDriverQuirks(false, true, false); private static final List DRIVER_QUIRK_RULES = List.of( new JdbcDriverQuirkRule("jdbc:yasdb:", YASHAN_QUIRKS), new JdbcDriverQuirkRule("jdbc:iris:", IRIS_QUIRKS), @@ -57,7 +58,11 @@ public final class DbxJdbcPlugin { private static String sharedConnectionKey = ""; private static Connection sharedConnection; - record JdbcDriverQuirks(boolean skipExecutionContext, boolean useOracleMetadata) { + record JdbcDriverQuirks( + boolean skipExecutionContext, + boolean useOracleMetadata, + boolean caseInsensitiveSchemaMetadata + ) { } private record JdbcDriverQuirkRule(String urlPrefix, JdbcDriverQuirks quirks) { @@ -387,46 +392,64 @@ public final class DbxJdbcPlugin { private static JsonNode listSchemas(JsonNode connection, String database) throws SQLException { ArrayNode result = MAPPER.createArrayNode(); Connection conn = openConnection(connection); - if (driverQuirks(connection).useOracleMetadata()) { + JdbcDriverQuirks quirks = driverQuirks(connection); + if (quirks.useOracleMetadata()) { return oracleListSchemas(conn); } - DatabaseMetaData meta = conn.getMetaData(); + DatabaseMetaData meta = conn.getMetaData(); + if (quirks.caseInsensitiveSchemaMetadata()) { try (ResultSet rs = meta.getSchemas(emptyToNull(database), null)) { - appendSchemas(result, rs); + appendSchemas(result, rs, true); + } catch (SQLException ignored) { + try (ResultSet rs = meta.getSchemas()) { + appendSchemas(result, rs, true); + } + } + try (ResultSet rs = meta.getSchemas(null, null)) { + appendSchemas(result, rs, true); + } catch (SQLException ignored) { + } + } else { + try (ResultSet rs = meta.getSchemas(emptyToNull(database), null)) { + appendSchemas(result, rs, false); } catch (SQLFeatureNotSupportedException ignored) { try (ResultSet rs = meta.getSchemas()) { - appendSchemas(result, rs); + appendSchemas(result, rs, false); } } if (result.isEmpty() && database != null) { try (ResultSet rs = meta.getSchemas(null, null)) { - appendSchemas(result, rs); + appendSchemas(result, rs, false); } catch (SQLFeatureNotSupportedException ignored) { } } - if (result.isEmpty()) { - try { - String schema = conn.getSchema(); - if (schema != null) { - result.add(schema); - } - } catch (SQLFeatureNotSupportedException | AbstractMethodError ignored) { + } + if (result.isEmpty()) { + try { + String schema = conn.getSchema(); + if (schema != null) { + addSchema(result, schema, quirks.caseInsensitiveSchemaMetadata()); } + } catch (SQLFeatureNotSupportedException | AbstractMethodError ignored) { } + } return result; } private static JsonNode listTables(JsonNode connection, String database, String schema) throws SQLException { ArrayNode result = MAPPER.createArrayNode(); Connection conn = openConnection(connection); - if (driverQuirks(connection).useOracleMetadata()) { + JdbcDriverQuirks quirks = driverQuirks(connection); + if (quirks.useOracleMetadata()) { return oracleListTables(conn, oracleEffectiveSchema(conn, schema)); } String[] types = new String[] {"TABLE", "VIEW", "MATERIALIZED VIEW", "SYSTEM TABLE", "SYSTEM VIEW"}; DatabaseMetaData meta = conn.getMetaData(); - appendTables(result, meta, emptyToNull(database), emptyToNull(schema), types); - if (result.isEmpty() && database != null) { - appendTables(result, meta, null, emptyToNull(schema), types); + String catalog = quirks.caseInsensitiveSchemaMetadata() ? null : emptyToNull(database); + String schemaPattern = resolveSchemaPattern(meta, database, schema, quirks); + appendTables(result, meta, catalog, schemaPattern, types); + if (result.isEmpty() && catalog != null) { + appendTables(result, meta, null, schemaPattern, types); } return result; } @@ -438,12 +461,13 @@ public final class DbxJdbcPlugin { return oracleListObjects(conn, oracleEffectiveSchema(conn, schema), schema); } DatabaseMetaData meta = conn.getMetaData(); - String catalog = emptyToNull(database); - String schemaPattern = emptyToNull(schema); + JdbcDriverQuirks quirks = driverQuirks(connection); + String catalog = quirks.caseInsensitiveSchemaMetadata() ? null : emptyToNull(database); + String schemaPattern = resolveSchemaPattern(meta, database, schema, quirks); String[] tableTypes = new String[] {"TABLE", "VIEW", "MATERIALIZED VIEW", "SYSTEM TABLE", "SYSTEM VIEW"}; appendTableObjects(result, meta, catalog, schemaPattern, schema, tableTypes); - if (result.isEmpty() && database != null) { + if (result.isEmpty() && catalog != null) { appendTableObjects(result, meta, null, schemaPattern, schema, tableTypes); } @@ -490,22 +514,95 @@ public final class DbxJdbcPlugin { return oracleGetColumns(conn, oracleEffectiveSchema(conn, schema), table); } DatabaseMetaData meta = conn.getMetaData(); - Set primaryKeys = safePrimaryKeys(meta, database, schema, table); - appendColumns(result, meta, emptyToNull(database), emptyToNull(schema), table, primaryKeys); - if (result.isEmpty() && database != null) { - primaryKeys = safePrimaryKeys(meta, null, schema, table); - appendColumns(result, meta, null, emptyToNull(schema), table, primaryKeys); + JdbcDriverQuirks quirks = driverQuirks(connection); + String catalog = quirks.caseInsensitiveSchemaMetadata() ? null : emptyToNull(database); + String schemaPattern = resolveSchemaPattern(meta, database, schema, quirks); + Set primaryKeys = safePrimaryKeys(meta, catalog, schemaPattern, table); + appendColumns(result, meta, catalog, schemaPattern, table, primaryKeys); + if (result.isEmpty() && catalog != null) { + primaryKeys = safePrimaryKeys(meta, null, schemaPattern, table); + appendColumns(result, meta, null, schemaPattern, table, primaryKeys); } return result; } - private static void appendSchemas(ArrayNode result, ResultSet rs) throws SQLException { + private static void appendSchemas(ArrayNode result, ResultSet rs, boolean caseInsensitive) throws SQLException { while (rs.next()) { String schema = rs.getString("TABLE_SCHEM"); - if (schema != null && !schema.isBlank()) { - result.add(schema); + addSchema(result, schema, caseInsensitive); + } + } + + private static void addSchema(ArrayNode result, String schema, boolean caseInsensitive) { + if (schema == null || schema.isBlank()) { + return; + } + String key = schemaKey(schema, caseInsensitive); + for (int i = 0; i < result.size(); i++) { + String existing = result.get(i).asText(""); + if (schemaKey(existing, caseInsensitive).equals(key)) { + if (preferSchemaDisplayName(existing, schema)) { + result.set(i, MAPPER.getNodeFactory().textNode(schema)); + } + return; } } + result.add(schema); + } + + static boolean preferSchemaDisplayName(String existing, String candidate) { + return isAllUppercaseIdentifier(existing) && !isAllUppercaseIdentifier(candidate); + } + + private static boolean isAllUppercaseIdentifier(String value) { + return value != null && value.equals(value.toUpperCase(Locale.ROOT)) && !value.equals(value.toLowerCase(Locale.ROOT)); + } + + private static String schemaKey(String schema, boolean caseInsensitive) { + return caseInsensitive ? schema.toLowerCase(Locale.ROOT) : schema; + } + + private static String resolveSchemaPattern( + DatabaseMetaData meta, + String database, + String schema, + JdbcDriverQuirks quirks + ) throws SQLException { + String schemaPattern = emptyToNull(schema); + if (schemaPattern == null || !quirks.caseInsensitiveSchemaMetadata()) { + return schemaPattern; + } + String resolved = null; + try { + resolved = findSchemaPattern(meta, emptyToNull(database), schemaPattern); + } catch (SQLException ignored) { + } + if (resolved != null) { + return resolved; + } + resolved = findSchemaPattern(meta, null, schemaPattern); + return resolved == null ? schemaPattern : resolved; + } + + private static String findSchemaPattern(DatabaseMetaData meta, String catalog, String schema) throws SQLException { + try (ResultSet rs = meta.getSchemas(catalog, null)) { + String fallback = null; + while (rs.next()) { + String candidate = rs.getString("TABLE_SCHEM"); + if (candidate == null || candidate.isBlank()) { + continue; + } + if (candidate.equals(schema)) { + return candidate; + } + if (candidate.equalsIgnoreCase(schema) && (fallback == null || preferSchemaDisplayName(fallback, candidate))) { + fallback = candidate; + } + } + return fallback; + } catch (SQLFeatureNotSupportedException ignored) { + return null; + } } private static void appendTables( diff --git a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java index 6600325ca..fe169c995 100644 --- a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java +++ b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java @@ -230,8 +230,16 @@ final class DbxJdbcPluginTest { assertEquals(true, DbxJdbcPlugin.driverQuirks(yashan).useOracleMetadata()); assertEquals(true, DbxJdbcPlugin.driverQuirks(iris).skipExecutionContext()); assertEquals(false, DbxJdbcPlugin.driverQuirks(iris).useOracleMetadata()); + assertEquals(true, DbxJdbcPlugin.driverQuirks(iris).caseInsensitiveSchemaMetadata()); assertEquals(false, DbxJdbcPlugin.driverQuirks(h2).skipExecutionContext()); assertEquals(false, DbxJdbcPlugin.driverQuirks(h2).useOracleMetadata()); + assertEquals(false, DbxJdbcPlugin.driverQuirks(h2).caseInsensitiveSchemaMetadata()); + } + + @Test + void schemaDisplayNamePrefersMixedCaseOverAllUppercaseDuplicate() { + assertEquals(true, DbxJdbcPlugin.preferSchemaDisplayName("SQLUSER", "SQLUser")); + assertEquals(false, DbxJdbcPlugin.preferSchemaDisplayName("SQLUser", "SQLUSER")); } @Test