fix(informix): resolve metadata owners correctly
This commit is contained in:
parent
4ee1f2b946
commit
b5206415ae
|
|
@ -27,8 +27,11 @@ import java.util.HashSet;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public final class InformixAgent extends AbstractJdbcAgent {
|
||||
private String loginOwner = "";
|
||||
|
||||
@Override
|
||||
protected String driverClass() {
|
||||
return "com.informix.jdbc.IfxDriver";
|
||||
|
|
@ -39,6 +42,16 @@ public final class InformixAgent extends AbstractJdbcAgent {
|
|||
return jdbcUrl(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterConnect(ConnectParams params, Connection connection) {
|
||||
loginOwner = normalizeOwner(params.getUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterDisconnect() {
|
||||
loginOwner = "";
|
||||
}
|
||||
|
||||
public static String jdbcUrl(ConnectParams params) {
|
||||
String rawUrlParams = params.getUrl_params();
|
||||
String extraParams = rawUrlParams == null ? "" : trimEnd(trimStart(rawUrlParams.trim(), ':', ';'), ';');
|
||||
|
|
@ -163,28 +176,61 @@ public final class InformixAgent extends AbstractJdbcAgent {
|
|||
|
||||
@Override
|
||||
public List<String> listSchemas() {
|
||||
List<String> result = new ArrayList<>();
|
||||
for (DatabaseInfo database : listDatabases()) {
|
||||
result.add(database.getName());
|
||||
return unchecked(() -> {
|
||||
List<String> catalogOwners = new ArrayList<>();
|
||||
try (java.sql.Statement stmt = requireConnected().createStatement();
|
||||
ResultSet rs = stmt.executeQuery(schemaCatalogSql())) {
|
||||
while (rs.next()) {
|
||||
String owner = normalizeOwner(rs.getString(1));
|
||||
if (!owner.isEmpty()) {
|
||||
catalogOwners.add(owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mergeSchemaOwners(catalogOwners, loginOwner);
|
||||
});
|
||||
}
|
||||
|
||||
static String schemaCatalogSql() {
|
||||
// Informix JDBC catalogs are databases; schemas are the object owners in the current database.
|
||||
return "SELECT owner FROM systables WHERE tabid >= 100 AND owner IS NOT NULL "
|
||||
+ "UNION SELECT owner FROM sysprocedures WHERE owner IS NOT NULL ORDER BY owner";
|
||||
}
|
||||
|
||||
static List<String> mergeSchemaOwners(List<String> catalogOwners, String loginOwner) {
|
||||
Set<String> owners = new TreeSet<>();
|
||||
for (String owner : catalogOwners) {
|
||||
String normalized = normalizeOwner(owner);
|
||||
if (!normalized.isEmpty()) {
|
||||
owners.add(normalized);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
String normalizedLoginOwner = normalizeOwner(loginOwner);
|
||||
if (!normalizedLoginOwner.isEmpty()) {
|
||||
owners.add(normalizedLoginOwner);
|
||||
}
|
||||
return new ArrayList<>(owners);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TableInfo> listTables(String schema) {
|
||||
return unchecked(() -> {
|
||||
List<TableInfo> result = new ArrayList<>();
|
||||
String sql = """
|
||||
List<Object> args = new ArrayList<>();
|
||||
StringBuilder sql = new StringBuilder("""
|
||||
SELECT tabname,
|
||||
CASE tabtype WHEN 'T' THEN 'TABLE' WHEN 'V' THEN 'VIEW' ELSE tabtype END
|
||||
FROM systables
|
||||
WHERE tabid >= 100
|
||||
ORDER BY tabname
|
||||
""";
|
||||
try (java.sql.Statement stmt = requireConnected().createStatement();
|
||||
ResultSet rs = stmt.executeQuery(sql.stripIndent().trim())) {
|
||||
while (rs.next()) {
|
||||
result.add(new TableInfo(rs.getString(1).trim(), rs.getString(2).trim(), null));
|
||||
""".stripIndent().trim());
|
||||
appendOwnerPredicate(sql, args, "owner", schema);
|
||||
sql.append(" ORDER BY tabname");
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql.toString())) {
|
||||
MetadataSqlSupport.bind(stmt, args);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
result.add(new TableInfo(rs.getString(1).trim(), rs.getString(2).trim(), null));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
@ -201,13 +247,13 @@ public final class InformixAgent extends AbstractJdbcAgent {
|
|||
return List.of();
|
||||
}
|
||||
try {
|
||||
return queryConstrainedTables(normalized);
|
||||
return queryConstrainedTables(schema, normalized);
|
||||
} catch (RuntimeException e) {
|
||||
return normalized.filterTables(listTables(schema));
|
||||
}
|
||||
}
|
||||
|
||||
private List<TableInfo> queryConstrainedTables(MetadataListConstraints constraints) {
|
||||
private List<TableInfo> queryConstrainedTables(String schema, MetadataListConstraints constraints) {
|
||||
return unchecked(() -> {
|
||||
List<TableInfo> result = new ArrayList<>();
|
||||
List<Object> args = new ArrayList<>();
|
||||
|
|
@ -216,6 +262,7 @@ public final class InformixAgent extends AbstractJdbcAgent {
|
|||
sql.append("tabname, CASE tabtype WHEN 'T' THEN 'TABLE' WHEN 'V' THEN 'VIEW' ELSE tabtype END ")
|
||||
.append("FROM systables WHERE tabid >= 100");
|
||||
appendInformixTableTypePredicate(sql, constraints);
|
||||
appendOwnerPredicate(sql, args, "owner", schema);
|
||||
MetadataSqlSupport.appendNameFilter(sql, args, "tabname", constraints);
|
||||
sql.append(" ORDER BY tabname");
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql.toString())) {
|
||||
|
|
@ -238,23 +285,8 @@ public final class InformixAgent extends AbstractJdbcAgent {
|
|||
result.add(new ObjectInfo(table.getName(), table.getTable_type(), schema, table.getComment()));
|
||||
}
|
||||
|
||||
try (java.sql.Statement stmt = requireConnected().createStatement();
|
||||
ResultSet rs = stmt.executeQuery(
|
||||
"SELECT procname FROM sysprocedures WHERE owner != 'informix' AND isproc = 'f' ORDER BY procname"
|
||||
)) {
|
||||
while (rs.next()) {
|
||||
result.add(new ObjectInfo(rs.getString(1).trim(), "FUNCTION", schema, null));
|
||||
}
|
||||
}
|
||||
|
||||
try (java.sql.Statement stmt = requireConnected().createStatement();
|
||||
ResultSet rs = stmt.executeQuery(
|
||||
"SELECT procname FROM sysprocedures WHERE owner != 'informix' AND isproc = 't' ORDER BY procname"
|
||||
)) {
|
||||
while (rs.next()) {
|
||||
result.add(new ObjectInfo(rs.getString(1).trim(), "PROCEDURE", schema, null));
|
||||
}
|
||||
}
|
||||
appendRoutineObjects(result, schema, "f", "FUNCTION");
|
||||
appendRoutineObjects(result, schema, "t", "PROCEDURE");
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
|
@ -283,16 +315,19 @@ public final class InformixAgent extends AbstractJdbcAgent {
|
|||
if (constraints.includesTableLikeTypes()) {
|
||||
StringBuilder tableSql = new StringBuilder("SELECT tabname AS object_name, CASE tabtype WHEN 'T' THEN 'TABLE' WHEN 'V' THEN 'VIEW' ELSE tabtype END AS object_type, 0 AS object_order FROM systables WHERE tabid >= 100");
|
||||
appendInformixTableTypePredicate(tableSql, constraints);
|
||||
appendOwnerPredicate(tableSql, args, "owner", schema);
|
||||
MetadataSqlSupport.appendNameFilter(tableSql, args, "tabname", constraints);
|
||||
branches.add(tableSql.toString());
|
||||
}
|
||||
if (constraints.objectTypeAllowed("FUNCTION")) {
|
||||
StringBuilder functionSql = new StringBuilder("SELECT procname AS object_name, 'FUNCTION' AS object_type, 1 AS object_order FROM sysprocedures WHERE owner != 'informix' AND isproc = 'f'");
|
||||
StringBuilder functionSql = new StringBuilder("SELECT procname AS object_name, 'FUNCTION' AS object_type, 1 AS object_order FROM sysprocedures WHERE isproc = 'f'");
|
||||
appendRoutineOwnerPredicate(functionSql, args, schema);
|
||||
MetadataSqlSupport.appendNameFilter(functionSql, args, "procname", constraints);
|
||||
branches.add(functionSql.toString());
|
||||
}
|
||||
if (constraints.objectTypeAllowed("PROCEDURE")) {
|
||||
StringBuilder procedureSql = new StringBuilder("SELECT procname AS object_name, 'PROCEDURE' AS object_type, 2 AS object_order FROM sysprocedures WHERE owner != 'informix' AND isproc = 't'");
|
||||
StringBuilder procedureSql = new StringBuilder("SELECT procname AS object_name, 'PROCEDURE' AS object_type, 2 AS object_order FROM sysprocedures WHERE isproc = 't'");
|
||||
appendRoutineOwnerPredicate(procedureSql, args, schema);
|
||||
MetadataSqlSupport.appendNameFilter(procedureSql, args, "procname", constraints);
|
||||
branches.add(procedureSql.toString());
|
||||
}
|
||||
|
|
@ -319,15 +354,18 @@ public final class InformixAgent extends AbstractJdbcAgent {
|
|||
@Override
|
||||
public ObjectSource getObjectSource(String schema, String name, String objectType) {
|
||||
return unchecked(() -> {
|
||||
String sql = """
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(name);
|
||||
StringBuilder sql = new StringBuilder("""
|
||||
SELECT b.data FROM sysprocbody b
|
||||
JOIN sysprocedures p ON b.procid = p.procid
|
||||
WHERE p.procname = ? AND b.datakey = 'T'
|
||||
ORDER BY b.seqno
|
||||
""";
|
||||
""".stripIndent().trim());
|
||||
appendOwnerPredicate(sql, args, "p.owner", schema);
|
||||
sql.append(" ORDER BY b.seqno");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (java.sql.PreparedStatement stmt = requireConnected().prepareStatement(sql.stripIndent().trim())) {
|
||||
stmt.setString(1, name);
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql.toString())) {
|
||||
MetadataSqlSupport.bind(stmt, args);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
String value = rs.getString(1);
|
||||
|
|
@ -343,16 +381,20 @@ public final class InformixAgent extends AbstractJdbcAgent {
|
|||
public List<ColumnInfo> getColumns(String schema, String table) {
|
||||
return unchecked(() -> {
|
||||
Connection conn = requireConnected();
|
||||
Set<Integer> primaryKeyColumns = getPrimaryKeyColumnNumbers(conn, table);
|
||||
Set<Integer> primaryKeyColumns = getPrimaryKeyColumnNumbers(conn, schema, table);
|
||||
List<ColumnInfo> result = new ArrayList<>();
|
||||
String sql = """
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(table);
|
||||
StringBuilder sql = new StringBuilder("""
|
||||
SELECT c.colname, c.coltype, c.colno
|
||||
FROM syscolumns c
|
||||
WHERE c.tabid = (SELECT tabid FROM systables WHERE tabname = ?)
|
||||
ORDER BY c.colno
|
||||
""";
|
||||
try (java.sql.PreparedStatement stmt = conn.prepareStatement(sql.stripIndent().trim())) {
|
||||
stmt.setString(1, table);
|
||||
JOIN systables t ON t.tabid = c.tabid
|
||||
WHERE t.tabname = ?
|
||||
""".stripIndent().trim());
|
||||
appendOwnerPredicate(sql, args, "t.owner", schema);
|
||||
sql.append(" ORDER BY c.colno");
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql.toString())) {
|
||||
MetadataSqlSupport.bind(stmt, args);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
String colname = rs.getString(1).trim();
|
||||
|
|
@ -376,18 +418,21 @@ public final class InformixAgent extends AbstractJdbcAgent {
|
|||
});
|
||||
}
|
||||
|
||||
private Set<Integer> getPrimaryKeyColumnNumbers(Connection conn, String table) throws SQLException {
|
||||
String sql = """
|
||||
private Set<Integer> getPrimaryKeyColumnNumbers(Connection conn, String schema, String table) throws Exception {
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(table);
|
||||
StringBuilder sql = new StringBuilder("""
|
||||
SELECT i.part1, i.part2, i.part3, i.part4, i.part5, i.part6, i.part7, i.part8,
|
||||
i.part9, i.part10, i.part11, i.part12, i.part13, i.part14, i.part15, i.part16
|
||||
FROM sysconstraints c
|
||||
JOIN sysindexes i ON i.idxname = c.idxname AND i.tabid = c.tabid
|
||||
JOIN systables t ON t.tabid = c.tabid
|
||||
WHERE t.tabname = ? AND c.constrtype = 'P'
|
||||
""";
|
||||
""".stripIndent().trim());
|
||||
appendOwnerPredicate(sql, args, "t.owner", schema);
|
||||
|
||||
try (java.sql.PreparedStatement stmt = conn.prepareStatement(sql.stripIndent().trim())) {
|
||||
stmt.setString(1, table);
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql.toString())) {
|
||||
MetadataSqlSupport.bind(stmt, args);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (!rs.next()) {
|
||||
return Collections.emptySet();
|
||||
|
|
@ -416,14 +461,17 @@ public final class InformixAgent extends AbstractJdbcAgent {
|
|||
public List<TriggerInfo> listTriggers(String schema, String table) {
|
||||
return unchecked(() -> {
|
||||
List<TriggerInfo> result = new ArrayList<>();
|
||||
String sql = """
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.add(table);
|
||||
StringBuilder sql = new StringBuilder("""
|
||||
SELECT t.trigname, t.event, 'TRIGGER'
|
||||
FROM systriggers t
|
||||
JOIN systables s ON t.tabid = s.tabid
|
||||
WHERE s.tabname = ?
|
||||
""";
|
||||
try (java.sql.PreparedStatement stmt = requireConnected().prepareStatement(sql.stripIndent().trim())) {
|
||||
stmt.setString(1, table);
|
||||
""".stripIndent().trim());
|
||||
appendOwnerPredicate(sql, args, "s.owner", schema);
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql.toString())) {
|
||||
MetadataSqlSupport.bind(stmt, args);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
result.add(new TriggerInfo(rs.getString(1).trim(), rs.getString(2).trim(), rs.getString(3).trim()));
|
||||
|
|
@ -477,6 +525,51 @@ public final class InformixAgent extends AbstractJdbcAgent {
|
|||
|| constraints.objectTypeAllowed("FUNCTION");
|
||||
}
|
||||
|
||||
private void appendRoutineObjects(List<ObjectInfo> result, String schema, String routineKind, String objectType)
|
||||
throws Exception {
|
||||
List<Object> args = new ArrayList<>();
|
||||
StringBuilder sql = new StringBuilder("SELECT procname FROM sysprocedures WHERE isproc = ?");
|
||||
args.add(routineKind);
|
||||
appendRoutineOwnerPredicate(sql, args, schema);
|
||||
sql.append(" ORDER BY procname");
|
||||
try (PreparedStatement stmt = requireConnected().prepareStatement(sql.toString())) {
|
||||
MetadataSqlSupport.bind(stmt, args);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
result.add(new ObjectInfo(rs.getString(1).trim(), objectType, schema, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void appendOwnerPredicate(StringBuilder sql, List<Object> args, String ownerColumn, String schema) {
|
||||
String owner = metadataOwner(schema);
|
||||
sql.append(" AND ").append(ownerColumn).append(" = ?");
|
||||
args.add(owner);
|
||||
}
|
||||
|
||||
private void appendRoutineOwnerPredicate(StringBuilder sql, List<Object> args, String schema) {
|
||||
String owner = metadataOwner(schema);
|
||||
sql.append(" AND owner = ?");
|
||||
args.add(owner);
|
||||
}
|
||||
|
||||
private String metadataOwner(String schema) {
|
||||
String owner = normalizeOwner(schema);
|
||||
if (!owner.isEmpty()) {
|
||||
return owner;
|
||||
}
|
||||
owner = normalizeOwner(loginOwner);
|
||||
if (!owner.isEmpty()) {
|
||||
return owner;
|
||||
}
|
||||
throw new IllegalStateException("Informix metadata owner is unavailable for an unqualified request");
|
||||
}
|
||||
|
||||
private static String normalizeOwner(String schema) {
|
||||
return schema == null ? "" : schema.trim();
|
||||
}
|
||||
|
||||
private static void appendInformixTableTypePredicate(StringBuilder sql, MetadataListConstraints constraints) {
|
||||
if (!constraints.hasObjectTypes()) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -172,18 +172,97 @@ class InformixAgentTest {
|
|||
Assertions.assertEquals("SELECT name FROM sysmaster:sysdatabases ORDER BY name", InformixAgent.databaseCatalogSql());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listsSchemasFromTableRoutineAndCurrentLoginOwners() {
|
||||
InformixAgent agent = new InformixAgent();
|
||||
java.sql.Connection connection = JdbcMetadataSqlFake.connection();
|
||||
TestSupport.setPrivateConnection(agent, connection);
|
||||
ConnectParams params = new ConnectParams();
|
||||
params.setUsername("current_owner");
|
||||
agent.afterConnect(params, connection);
|
||||
|
||||
Assertions.assertEquals(List.of("current_owner"), agent.listSchemas());
|
||||
|
||||
Assertions.assertEquals(
|
||||
List.of("SELECT owner FROM systables WHERE tabid >= 100 AND owner IS NOT NULL "
|
||||
+ "UNION SELECT owner FROM sysprocedures WHERE owner IS NOT NULL ORDER BY owner"),
|
||||
JdbcMetadataSqlFake.statements
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
List.of("current_owner", "routine_owner", "table_owner"),
|
||||
InformixAgent.mergeSchemaOwners(
|
||||
List.of("table_owner", "routine_owner", "routine_owner", " "),
|
||||
"current_owner"
|
||||
)
|
||||
);
|
||||
Assertions.assertNotEquals(InformixAgent.databaseCatalogSql(), InformixAgent.schemaCatalogSql());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unqualifiedMetadataUsesCurrentLoginOwnerWithoutCrossOwnerFallback() {
|
||||
InformixAgent agent = new InformixAgent();
|
||||
java.sql.Connection connection = JdbcMetadataSqlFake.connection();
|
||||
TestSupport.setPrivateConnection(agent, connection);
|
||||
ConnectParams params = new ConnectParams();
|
||||
params.setUsername("app_owner");
|
||||
agent.afterConnect(params, connection);
|
||||
|
||||
agent.listTables("");
|
||||
agent.listObjects(null, new MetadataListConstraints("sync", 10, null, List.of("PROCEDURE", "FUNCTION")));
|
||||
|
||||
Assertions.assertTrue(JdbcMetadataSqlFake.statements.get(0).contains("AND owner = ?"));
|
||||
Assertions.assertEquals("param:1=app_owner", JdbcMetadataSqlFake.statements.get(1));
|
||||
String objectSql = JdbcMetadataSqlFake.statements.get(2);
|
||||
Assertions.assertTrue(objectSql.contains("isproc = 'f' AND owner = ?"), objectSql);
|
||||
Assertions.assertTrue(objectSql.contains("isproc = 't' AND owner = ?"), objectSql);
|
||||
Assertions.assertFalse(objectSql.contains("owner <> 'informix'"), objectSql);
|
||||
Assertions.assertEquals("param:1=app_owner", JdbcMetadataSqlFake.statements.get(3));
|
||||
Assertions.assertEquals("param:3=app_owner", JdbcMetadataSqlFake.statements.get(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unqualifiedMetadataFailsClosedWithoutALoginOwner() {
|
||||
InformixAgent agent = new InformixAgent();
|
||||
TestSupport.setPrivateConnection(agent, JdbcMetadataSqlFake.connection());
|
||||
|
||||
IllegalStateException error = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> agent.listTables("")
|
||||
);
|
||||
|
||||
Assertions.assertTrue(error.getMessage().contains("metadata owner is unavailable"), error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unconstrainedTableMetadataFiltersByRequestedOwner() {
|
||||
InformixAgent agent = new InformixAgent();
|
||||
TestSupport.setPrivateConnection(agent, JdbcMetadataSqlFake.connection());
|
||||
|
||||
agent.listTables("xtdpcky");
|
||||
|
||||
String sql = JdbcMetadataSqlFake.statements.get(0);
|
||||
Assertions.assertTrue(sql.contains("FROM systables"), sql);
|
||||
Assertions.assertTrue(sql.contains("AND owner = ?"), sql);
|
||||
Assertions.assertEquals("param:1=xtdpcky", JdbcMetadataSqlFake.statements.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constrainedTableMetadataUsesInformixSkipFirstPushdown() {
|
||||
InformixAgent agent = new InformixAgent();
|
||||
TestSupport.setPrivateConnection(agent, JdbcMetadataSqlFake.connection());
|
||||
|
||||
agent.listTables("stores", new MetadataListConstraints("ord", 25, 50, List.of("TABLE")));
|
||||
agent.listTables("xtdpcky", new MetadataListConstraints("ord", 25, 50, List.of("TABLE")));
|
||||
|
||||
String sql = JdbcMetadataSqlFake.statements.get(0);
|
||||
Assertions.assertTrue(sql.startsWith("SELECT SKIP 50 FIRST 25 tabname"), sql);
|
||||
Assertions.assertTrue(sql.contains("tabtype IN ('T')"), sql);
|
||||
Assertions.assertTrue(sql.contains("AND owner = ?"), sql);
|
||||
Assertions.assertTrue(sql.contains("UPPER(tabname) LIKE ? ESCAPE '\\\\'"), sql);
|
||||
Assertions.assertTrue(sql.endsWith("ORDER BY tabname"), sql);
|
||||
Assertions.assertEquals(
|
||||
List.of("param:1=xtdpcky", "param:2=%O%R%D%"),
|
||||
JdbcMetadataSqlFake.statements.subList(1, 3)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -191,13 +270,60 @@ class InformixAgentTest {
|
|||
InformixAgent agent = new InformixAgent();
|
||||
TestSupport.setPrivateConnection(agent, JdbcMetadataSqlFake.connection());
|
||||
|
||||
agent.listObjects("stores", new MetadataListConstraints("sync", 10, null, List.of("PROCEDURE", "FUNCTION")));
|
||||
agent.listObjects("xtdpcky", new MetadataListConstraints("sync", 10, null, List.of("PROCEDURE", "FUNCTION")));
|
||||
|
||||
String sql = JdbcMetadataSqlFake.statements.get(0);
|
||||
Assertions.assertTrue(sql.startsWith("SELECT FIRST 10 object_name, object_type FROM ("), sql);
|
||||
Assertions.assertTrue(sql.contains("FROM sysprocedures"), sql);
|
||||
Assertions.assertTrue(sql.contains("isproc = 'f'"), sql);
|
||||
Assertions.assertTrue(sql.contains("isproc = 't'"), sql);
|
||||
Assertions.assertTrue(sql.contains("isproc = 'f' AND owner = ?"), sql);
|
||||
Assertions.assertTrue(sql.contains("isproc = 't' AND owner = ?"), sql);
|
||||
Assertions.assertTrue(sql.endsWith("ORDER BY object_order, object_name"), sql);
|
||||
Assertions.assertEquals(
|
||||
List.of("param:1=xtdpcky", "param:2=%S%Y%N%C%", "param:3=xtdpcky", "param:4=%S%Y%N%C%"),
|
||||
JdbcMetadataSqlFake.statements.subList(1, 5)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void columnAndPrimaryKeyMetadataFilterDuplicateTableNamesByOwner() {
|
||||
InformixAgent agent = new InformixAgent();
|
||||
TestSupport.setPrivateConnection(agent, JdbcMetadataSqlFake.connection());
|
||||
|
||||
agent.getColumns("xtdpcky", "orders");
|
||||
|
||||
String primaryKeySql = JdbcMetadataSqlFake.statements.get(0);
|
||||
String columnsSql = JdbcMetadataSqlFake.statements.get(3);
|
||||
Assertions.assertTrue(primaryKeySql.contains("WHERE t.tabname = ? AND c.constrtype = 'P' AND t.owner = ?"), primaryKeySql);
|
||||
Assertions.assertTrue(columnsSql.contains("WHERE t.tabname = ? AND t.owner = ?"), columnsSql);
|
||||
Assertions.assertEquals(
|
||||
List.of(
|
||||
"param:1=orders",
|
||||
"param:2=xtdpcky",
|
||||
"param:1=orders",
|
||||
"param:2=xtdpcky"
|
||||
),
|
||||
List.of(
|
||||
JdbcMetadataSqlFake.statements.get(1),
|
||||
JdbcMetadataSqlFake.statements.get(2),
|
||||
JdbcMetadataSqlFake.statements.get(4),
|
||||
JdbcMetadataSqlFake.statements.get(5)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void routineSourceAndTriggerMetadataUseRequestedOwner() {
|
||||
InformixAgent agent = new InformixAgent();
|
||||
TestSupport.setPrivateConnection(agent, JdbcMetadataSqlFake.connection());
|
||||
|
||||
agent.getObjectSource("xtdpcky", "sync_orders", "PROCEDURE");
|
||||
agent.listTriggers("xtdpcky", "orders");
|
||||
|
||||
String sourceSql = JdbcMetadataSqlFake.statements.get(0);
|
||||
String triggerSql = JdbcMetadataSqlFake.statements.get(3);
|
||||
Assertions.assertTrue(sourceSql.contains("AND p.owner = ?"), sourceSql);
|
||||
Assertions.assertTrue(triggerSql.contains("WHERE s.tabname = ? AND s.owner = ?"), triggerSql);
|
||||
Assertions.assertEquals("param:2=xtdpcky", JdbcMetadataSqlFake.statements.get(2));
|
||||
Assertions.assertEquals("param:2=xtdpcky", JdbcMetadataSqlFake.statements.get(5));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { ConnectionConfig } from "@/types/database";
|
|||
import {
|
||||
GAUSSDB_M_JDBC_DRIVER_CLASS,
|
||||
connectionObjectTreeNodeSchema,
|
||||
connectionObjectTreeQuerySchema,
|
||||
connectionQueryExecutionSchema,
|
||||
connectionShouldLoadIdentifierQuote,
|
||||
connectionUsesDatabaseObjectTreeMode,
|
||||
|
|
@ -223,4 +224,22 @@ describe("object tree node schema", () => {
|
|||
it("uses the SQLite database alias to qualify attached tables", () => {
|
||||
expect(connectionObjectTreeNodeSchema({ db_type: "sqlite" }, "analytics")).toBe("analytics");
|
||||
});
|
||||
|
||||
it("keeps unqualified Informix metadata on the login owner", () => {
|
||||
expect(connectionObjectTreeQuerySchema({ db_type: "informix" }, "prulife")).toBe("");
|
||||
expect(connectionObjectTreeNodeSchema({ db_type: "informix" }, "prulife")).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ db_type: "jdbc" as const, connection_string: "jdbc:informix-sqli://localhost:9088/prulife" },
|
||||
{ db_type: "gbase" as const, driver_profile: "gbase8s" },
|
||||
])("keeps compatible Informix metadata on the login owner", (connection) => {
|
||||
expect(connectionObjectTreeQuerySchema(connection, "prulife")).toBe("");
|
||||
expect(connectionObjectTreeNodeSchema(connection, "prulife")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves explicit Informix owners", () => {
|
||||
expect(connectionObjectTreeQuerySchema({ db_type: "informix" }, "prulife", "xtdpcky")).toBe("xtdpcky");
|
||||
expect(connectionObjectTreeNodeSchema({ db_type: "informix" }, "prulife", "xtdpcky")).toBe("xtdpcky");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ export function connectionQueryExecutionSchema(connection: JdbcDialectConnection
|
|||
export function connectionObjectTreeQuerySchema(connection: JdbcDialectConnection | undefined, database: string, schema?: string): string {
|
||||
if (connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend") return schema || database;
|
||||
if (connectionUsesDatabaseObjectTreeMode(connection)) return "";
|
||||
if (effectiveDatabaseTypeForConnection(connection) === "informix") return schema || "";
|
||||
return schema || database;
|
||||
}
|
||||
|
||||
|
|
@ -176,6 +177,7 @@ export function connectionObjectTreeNodeSchema(connection: JdbcDialectConnection
|
|||
if (connectionUsesDatabaseObjectTreeMode(connection)) return undefined;
|
||||
if (schema) return schema;
|
||||
const type = effectiveDatabaseTypeForConnection(connection);
|
||||
if (type === "informix") return undefined;
|
||||
if (type === "sqlite") return database;
|
||||
return isSchemaAware(type) ? database : undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,20 @@ describe("TreeNodeLoadRegistry", () => {
|
|||
).toBe(db);
|
||||
});
|
||||
|
||||
it("does not let a user-cancelled load reclaim ownership", () => {
|
||||
const registry = new TreeNodeLoadRegistry();
|
||||
const node: TreeNodeLike = { id: "c1:db:schema", connectionId: "c1", isLoading: false, children: [] };
|
||||
const load = registry.begin(node);
|
||||
|
||||
registry.cancelPrefix("c1:db");
|
||||
node.isLoading = false;
|
||||
const reclaimed = load.reclaim(node);
|
||||
|
||||
expect(reclaimed).toBe(load);
|
||||
expect(reclaimed.isCurrent()).toBe(false);
|
||||
expect(node.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("invalidates pruned descendant generations even when root no longer contains them", () => {
|
||||
const registry = new TreeNodeLoadRegistry();
|
||||
const db: TreeNodeLike = { id: "c1:db", connectionId: "c1", isLoading: false, children: [] };
|
||||
|
|
|
|||
|
|
@ -28,12 +28,13 @@ export type TreeNodeLoadEpoch = {
|
|||
|
||||
export class TreeNodeLoadRegistry {
|
||||
private readonly generations = new Map<string, number>();
|
||||
private readonly cancellationGenerations = new Map<string, number>();
|
||||
|
||||
begin(node: TreeNodeLike): TreeNodeLoadHandle {
|
||||
const generation = (this.generations.get(node.id) ?? 0) + 1;
|
||||
this.generations.set(node.id, generation);
|
||||
node.isLoading = true;
|
||||
return this.createHandle(node.id, generation);
|
||||
return this.createHandle(node.id, generation, this.cancellationGenerations.get(node.id) ?? 0);
|
||||
}
|
||||
|
||||
/** Snapshot the current generation without claiming ownership (no spinner). */
|
||||
|
|
@ -61,6 +62,22 @@ export class TreeNodeLoadRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User cancellation is stronger than ordinary invalidation: a load waiting for
|
||||
* connection recovery must not reclaim ownership after its node was collapsed.
|
||||
*/
|
||||
cancelPrefix(nodeId: string): void {
|
||||
const prefix = `${nodeId}:`;
|
||||
const affectedIds = new Set<string>([nodeId]);
|
||||
for (const id of this.generations.keys()) {
|
||||
if (id.startsWith(prefix)) affectedIds.add(id);
|
||||
}
|
||||
for (const id of affectedIds) {
|
||||
this.cancellationGenerations.set(id, (this.cancellationGenerations.get(id) ?? 0) + 1);
|
||||
}
|
||||
this.invalidatePrefix(nodeId);
|
||||
}
|
||||
|
||||
/** Bump recorded generations for descendants only (not the parent load itself). */
|
||||
invalidateDescendants(parentId: string): void {
|
||||
const prefix = `${parentId}:`;
|
||||
|
|
@ -90,7 +107,7 @@ export class TreeNodeLoadRegistry {
|
|||
return this.generations.get(nodeId) === generation;
|
||||
}
|
||||
|
||||
private createHandle(nodeId: string, generation: number): TreeNodeLoadHandle {
|
||||
private createHandle(nodeId: string, generation: number, cancellationGeneration: number): TreeNodeLoadHandle {
|
||||
const registry = this;
|
||||
return {
|
||||
nodeId,
|
||||
|
|
@ -99,6 +116,9 @@ export class TreeNodeLoadRegistry {
|
|||
return registry.isCurrent(nodeId, generation);
|
||||
},
|
||||
reclaim(node: TreeNodeLike) {
|
||||
if (!registry.isCurrent(nodeId, generation) && (registry.cancellationGenerations.get(nodeId) ?? 0) !== cancellationGeneration) {
|
||||
return this;
|
||||
}
|
||||
if (node.id !== nodeId) {
|
||||
return registry.begin(node);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,19 @@ function genericJdbcConnection(): ConnectionConfig {
|
|||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function informixConnection(): ConnectionConfig {
|
||||
return {
|
||||
id: "informix-1",
|
||||
name: "Informix",
|
||||
db_type: "informix",
|
||||
host: "127.0.0.1",
|
||||
port: 9088,
|
||||
username: "informix",
|
||||
password: "",
|
||||
database: "prulife",
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function procedure(name: string): ObjectInfo {
|
||||
return {
|
||||
name,
|
||||
|
|
@ -2245,6 +2258,151 @@ describe("connectionStore metadata loading", () => {
|
|||
expect(dbNode.isExpanded).toBe(false);
|
||||
});
|
||||
|
||||
it("does not re-expand an Informix schema collapsed while its metadata cache is being saved", async () => {
|
||||
let resolveCacheSave!: () => void;
|
||||
const saveSchemaCache = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveCacheSave = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache,
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
useSettingsStore().editorSettings.sidebarObjectDisplay = "grouped";
|
||||
|
||||
const connection = informixConnection();
|
||||
const databaseNode: TreeNode = {
|
||||
id: `${connection.id}:prulife`,
|
||||
label: "prulife",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "prulife",
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
const schemaNode: TreeNode = {
|
||||
id: `${connection.id}:prulife:xtdpcky`,
|
||||
label: "xtdpcky",
|
||||
type: "schema",
|
||||
connectionId: connection.id,
|
||||
database: "prulife",
|
||||
schema: "xtdpcky",
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
databaseNode.children = [schemaNode];
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [databaseNode],
|
||||
},
|
||||
];
|
||||
|
||||
const loadPromise = store.loadTables(connection.id, "prulife", "xtdpcky", { force: true });
|
||||
await vi.waitFor(() => expect(saveSchemaCache).toHaveBeenCalledTimes(1));
|
||||
expect(saveSchemaCache.mock.calls[0]?.[0]).toBe(`${connection.id}:prulife:xtdpcky:objects-grouped-v7-informix-owner-v2`);
|
||||
expect(schemaNode.isLoading).toBe(true);
|
||||
|
||||
schemaNode.isExpanded = false;
|
||||
store.cancelTreeNodeLoad(schemaNode.id);
|
||||
resolveCacheSave();
|
||||
await loadPromise;
|
||||
|
||||
expect(schemaNode.isLoading).toBe(false);
|
||||
expect(schemaNode.isExpanded).toBe(false);
|
||||
});
|
||||
|
||||
it("does not let an Informix schema load reclaim ownership after collapse during a health check", async () => {
|
||||
let resolveHealthCheck!: () => void;
|
||||
const checkConnectionHealth = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveHealthCheck = resolve;
|
||||
}),
|
||||
);
|
||||
const saveSchemaCache = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth,
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache,
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
useSettingsStore().editorSettings.sidebarObjectDisplay = "grouped";
|
||||
|
||||
const connection = informixConnection();
|
||||
const schemaNode: TreeNode = {
|
||||
id: `${connection.id}:prulife:xtdpcky`,
|
||||
label: "xtdpcky",
|
||||
type: "schema",
|
||||
connectionId: connection.id,
|
||||
database: "prulife",
|
||||
schema: "xtdpcky",
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: `${connection.id}:prulife`,
|
||||
label: "prulife",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "prulife",
|
||||
isExpanded: true,
|
||||
children: [schemaNode],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const loadPromise = store.loadTables(connection.id, "prulife", "xtdpcky", { force: true });
|
||||
await vi.waitFor(() => expect(checkConnectionHealth).toHaveBeenCalledTimes(1));
|
||||
expect(schemaNode.isLoading).toBe(true);
|
||||
|
||||
schemaNode.isExpanded = false;
|
||||
store.cancelTreeNodeLoad(schemaNode.id);
|
||||
resolveHealthCheck();
|
||||
await loadPromise;
|
||||
|
||||
expect(saveSchemaCache).not.toHaveBeenCalled();
|
||||
expect(schemaNode.isLoading).toBe(false);
|
||||
expect(schemaNode.isExpanded).toBe(false);
|
||||
});
|
||||
|
||||
it("does not apply load-more results after the parent generation is invalidated", async () => {
|
||||
const firstPage = Array.from({ length: 201 }, (_, index) => ({
|
||||
name: `t_${String(index + 1).padStart(4, "0")}`,
|
||||
|
|
|
|||
|
|
@ -1372,6 +1372,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
return parts.map((part) => encodeURIComponent(part)).join(":");
|
||||
}
|
||||
|
||||
function ownerAwareMetadataCacheVersion(config: ConnectionConfig | undefined, version: string): string {
|
||||
return config?.db_type === "informix" ? `${version}-informix-owner-v2` : version;
|
||||
}
|
||||
|
||||
function supportedSidebarObjectTypes(config?: ConnectionConfig): DatabaseObjectTreeKind[] {
|
||||
const dbType = effectiveDatabaseTypeForConnection(config);
|
||||
return sidebarObjectKindsForDatabase(dbType);
|
||||
|
|
@ -1401,7 +1405,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
|
||||
function objectGroupCacheKey(node: TreeNode): string {
|
||||
const config = node.connectionId ? getConfig(node.connectionId) : undefined;
|
||||
const cacheVersion = config?.db_type === "oracle" ? "objects-v7" : "objects-v6";
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(config, config?.db_type === "oracle" ? "objects-v7" : "objects-v6");
|
||||
return schemaCacheKey(node.connectionId || "", node.database || "", node.schema || "", node.type, cacheVersion);
|
||||
}
|
||||
|
||||
|
|
@ -1917,7 +1921,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
if (parent.type === "group-tables") return objectGroupCacheKey(parent);
|
||||
if (parent.type !== "database" && parent.type !== "schema" && parent.type !== "linked-server-schema") return null;
|
||||
const simpleObjectDisplay = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
|
||||
return schemaCacheKey(parent.connectionId, parent.database, parent.schema || "", simpleObjectDisplay ? "objects-simple-v6" : "objects-grouped-v7");
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(getConfig(parent.connectionId), simpleObjectDisplay ? "objects-simple-v6" : "objects-grouped-v7");
|
||||
return schemaCacheKey(parent.connectionId, parent.database, parent.schema || "", cacheVersion);
|
||||
}
|
||||
|
||||
function sidebarTableSearchIndexCacheKey(parent: TreeNode): string | null {
|
||||
|
|
@ -3659,8 +3664,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
await ensureConnected(connectionId);
|
||||
load = reclaimTreeNodeLoad(load, node);
|
||||
if (useCachedChildren(node, options, load)) return;
|
||||
const showSystemSchemas = getConfig(connectionId)?.show_system_schemas === true;
|
||||
const cacheKey = schemaCacheKey(connectionId, database, "schemas-v3", showSystemSchemas ? "show-system" : "hide-system");
|
||||
const config = getConfig(connectionId);
|
||||
const showSystemSchemas = config?.show_system_schemas === true;
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(config, "schemas-v3");
|
||||
const cacheKey = schemaCacheKey(connectionId, database, cacheVersion, showSystemSchemas ? "show-system" : "hide-system");
|
||||
if (!options?.force) {
|
||||
const cached = await loadPersistedTreeChildren(node, cacheKey, load);
|
||||
if (cached.hit) {
|
||||
|
|
@ -3709,7 +3716,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
if (!targetNode) return;
|
||||
setChildren(targetNode, children);
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
targetNode.isExpanded = true;
|
||||
const currentTargetNode = treeNodeLoadTarget(load);
|
||||
if (currentTargetNode) currentTargetNode.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(connectionId, e, load);
|
||||
throw e;
|
||||
|
|
@ -3748,7 +3756,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
if (!targetNode) return;
|
||||
setChildren(targetNode, children);
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
targetNode.isExpanded = true;
|
||||
const currentTargetNode = treeNodeLoadTarget(load);
|
||||
if (currentTargetNode) currentTargetNode.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(connectionId, e, load);
|
||||
throw e;
|
||||
|
|
@ -4005,7 +4014,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
if (!searchFilter && !options?.sidebarTableSearchParentId && !tableNameFilter) {
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
}
|
||||
targetNode.isExpanded = true;
|
||||
const currentTargetNode = treeNodeLoadTarget(load);
|
||||
if (currentTargetNode) currentTargetNode.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(connectionId, e, load);
|
||||
throw e;
|
||||
|
|
@ -4032,7 +4042,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
});
|
||||
if (!options?.force && simpleObjectDisplayForScope && !searchFilter && !options?.sidebarTableSearchParentId && !tableNameFilterForScope) {
|
||||
const nodeId = schema ? `${connectionId}:${database}:${schema}` : `${connectionId}:${database}`;
|
||||
const cacheKey = schemaCacheKey(connectionId, database, schema || "", "objects-simple-v6");
|
||||
const cacheKey = schemaCacheKey(connectionId, database, schema || "", ownerAwareMetadataCacheVersion(configForScope, "objects-simple-v6"));
|
||||
if (await hydrateTreeNodeFromCache(findNode(treeNodes.value, nodeId), cacheKey)) {
|
||||
void loadTables(connectionId, database, schema, { ...options, force: true }).catch(() => undefined);
|
||||
return;
|
||||
|
|
@ -4063,9 +4073,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
load = reclaimTreeNodeLoad(load, node);
|
||||
if (useCachedChildren(node, options, load)) return;
|
||||
const simpleObjectDisplay = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
|
||||
const cacheKey = schemaCacheKey(connectionId, database, schema || "", simpleObjectDisplay ? "objects-simple-v6" : "objects-grouped-v7");
|
||||
const searchFilter = activeTreeLoadSearchFilter(options);
|
||||
const config = getConfig(connectionId);
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(config, simpleObjectDisplay ? "objects-simple-v6" : "objects-grouped-v7");
|
||||
const cacheKey = schemaCacheKey(connectionId, database, schema || "", cacheVersion);
|
||||
const querySchema = connectionObjectTreeQuerySchema(config, database, schema);
|
||||
const effectiveSchema = connectionObjectTreeNodeSchema(config, database, schema);
|
||||
const tableNameFilter = activeTableNameFilterForScope({
|
||||
|
|
@ -4123,10 +4134,12 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
if (!searchFilter && !isSidebarTableSearch && !tableNameFilter) {
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
}
|
||||
targetNode.isExpanded = true;
|
||||
const currentTargetNode = treeNodeLoadTarget(load);
|
||||
if (!currentTargetNode) return;
|
||||
currentTargetNode.isExpanded = true;
|
||||
if (simpleObjectDisplay && !searchFilter && !isSidebarTableSearch && nonTableObjectTypes.length > 0) {
|
||||
void loadSimpleSupplementalObjectChildren({
|
||||
node: targetNode,
|
||||
node: currentTargetNode,
|
||||
nodeId,
|
||||
connectionId,
|
||||
database,
|
||||
|
|
@ -4258,7 +4271,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
if (!searchFilter && !isSidebarTableSearch && !tableNameFilter) {
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
}
|
||||
targetNode.isExpanded = true;
|
||||
const currentTargetNode = treeNodeLoadTarget(load);
|
||||
if (currentTargetNode) currentTargetNode.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(node.connectionId, e, load);
|
||||
throw e;
|
||||
|
|
@ -4323,8 +4337,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
const nextChildren = page.hasMore ? appendTableTreeLoadMoreNode(mergedChildren, buildLoadMoreNode(targetParent, page.nextOffset, loadMore.pageSize), page.loadMoreParent) : mergedChildren;
|
||||
targetParent.objectCount = mergedChildren.length;
|
||||
setChildren(targetParent, nextChildren);
|
||||
await savePersistedTreeChildren(schemaCacheKey(parentConnectionId, parentDatabase, parent.schema || "", "objects-simple-v6"), nextChildren);
|
||||
targetParent.isExpanded = true;
|
||||
await savePersistedTreeChildren(schemaCacheKey(parentConnectionId, parentDatabase, parent.schema || "", ownerAwareMetadataCacheVersion(config, "objects-simple-v6")), nextChildren);
|
||||
const currentTargetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
if (currentTargetParent && parentEpoch.isCurrent()) currentTargetParent.isExpanded = true;
|
||||
return;
|
||||
}
|
||||
const objectTypes = objectTypesForGroupNode(parent.type);
|
||||
|
|
@ -4374,7 +4389,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
targetParent.objectCount = mergedChildren.length;
|
||||
setChildren(targetParent, nextChildren);
|
||||
await savePersistedTreeChildren(objectGroupCacheKey(targetParent), nextChildren);
|
||||
targetParent.isExpanded = true;
|
||||
const currentTargetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
if (currentTargetParent && parentEpoch.isCurrent()) currentTargetParent.isExpanded = true;
|
||||
return;
|
||||
}
|
||||
const targetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
|
|
@ -4382,7 +4398,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
targetParent.objectCount = mergedChildren.length;
|
||||
setChildren(targetParent, nextChildren);
|
||||
await savePersistedTreeChildren(objectGroupCacheKey(targetParent), nextChildren);
|
||||
targetParent.isExpanded = true;
|
||||
const currentTargetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
if (currentTargetParent && parentEpoch.isCurrent()) currentTargetParent.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(parentConnectionId, e, load);
|
||||
throw e;
|
||||
|
|
@ -6695,8 +6712,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
function cancelTreeNodeLoad(nodeId: string): void {
|
||||
// Supersede any in-flight loader for this node so a collapse issued while
|
||||
// the load is still running (or a loader that never resolves) cannot
|
||||
// re-expand the node via its trailing `targetNode.isExpanded = true`.
|
||||
treeNodeLoads.invalidatePrefix(nodeId);
|
||||
// reclaim ownership after connection recovery or re-expand the node.
|
||||
treeNodeLoads.cancelPrefix(nodeId);
|
||||
const node = findNode(treeNodes.value, nodeId);
|
||||
if (node) node.isLoading = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ pub fn is_schema_aware(database_type: DatabaseType) -> bool {
|
|||
| DatabaseType::Hive
|
||||
| DatabaseType::Spark
|
||||
| DatabaseType::Db2
|
||||
| DatabaseType::Informix
|
||||
| DatabaseType::Tdengine
|
||||
| DatabaseType::Xugu
|
||||
| DatabaseType::Sqlite
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ fn qualifies_schema_only_for_schema_aware_databases() {
|
|||
"\"DBX_TEST\".\"PRODUCTS\""
|
||||
);
|
||||
assert_eq!(qualified_table_name(Some(DatabaseType::Oscar), Some("SYSDBA"), "EMPLOYEE"), "\"SYSDBA\".\"EMPLOYEE\"");
|
||||
assert_eq!(qualified_table_name(Some(DatabaseType::Informix), Some("xtdpcky"), "users"), "xtdpcky.users");
|
||||
assert_eq!(qualified_table_name(Some(DatabaseType::Sqlite), Some("analytics"), "users"), "\"analytics\".\"users\"");
|
||||
assert_eq!(qualified_table_name(Some(DatabaseType::Jdbc), Some("cbsdw_dwd"), "dwd_test_df"), "dwd_test_df");
|
||||
assert_eq!(qualified_table_name(Some(DatabaseType::Iotdb), Some("root.test"), "device2"), "root.test.device2");
|
||||
|
|
@ -622,7 +623,7 @@ fn builds_informix_table_data_with_skip_first_pagination() {
|
|||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Informix),
|
||||
schema: Some("ignored".to_string()),
|
||||
schema: Some("xtdpcky".to_string()),
|
||||
table_name: "users".to_string(),
|
||||
table_type: None,
|
||||
primary_keys: vec!["id".to_string()],
|
||||
|
|
@ -635,7 +636,7 @@ fn builds_informix_table_data_with_skip_first_pagination() {
|
|||
include_row_id: false,
|
||||
..Default::default()
|
||||
}),
|
||||
"SELECT SKIP 100 FIRST 50 * FROM users WHERE (active = 1)"
|
||||
"SELECT SKIP 100 FIRST 50 * FROM xtdpcky.users WHERE (active = 1)"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Reference in New Issue