fix: improve iris schema metadata completion

This commit is contained in:
t8y2 2026-06-04 15:09:56 +08:00
parent c8539bcc32
commit 9935f8e7fe
5 changed files with 167 additions and 41 deletions

View File

@ -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,
() => {

View File

@ -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"

View File

@ -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"

View File

@ -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<JdbcDriverQuirkRule> 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<String> 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<String> 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(

View File

@ -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