fix: improve iris schema metadata completion
This commit is contained in:
parent
c8539bcc32
commit
9935f8e7fe
|
|
@ -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,
|
||||
() => {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue