diff --git a/agents/README.md b/agents/README.md index 98b5ab6356..ac43364f8a 100644 --- a/agents/README.md +++ b/agents/README.md @@ -37,6 +37,7 @@ Each agent runs as a standalone process and communicates with DBX via stdin/stdo | cassandra | Apache Cassandra 2.1+ | Apache cassandra-gocql-driver native agent | | bigquery | Google BigQuery | BigQuery JDBC | | kylin | Apache Kylin | Kylin JDBC | +| ignite | Apache Ignite | Ignite JDBC thin (ignite-core) | | sundb | SunDB | SunDB JDBC | | tdengine | TDengine 2.4+ | taos-connector-rust native WebSocket agent | | yashandb | 崖山 YashanDB | YashanDB JDBC | diff --git a/agents/README.zh-CN.md b/agents/README.zh-CN.md index 35dd43acf8..32af0ec3c7 100644 --- a/agents/README.zh-CN.md +++ b/agents/README.zh-CN.md @@ -37,6 +37,7 @@ DBX 的 Agent 驱动 —— 通过 JDBC 和原生数据库驱动支持各种数 | cassandra | Apache Cassandra 2.1+ | Apache cassandra-gocql-driver 原生 Agent | | bigquery | Google BigQuery | BigQuery JDBC | | kylin | Apache Kylin | Kylin JDBC | +| ignite | Apache Ignite | Ignite JDBC thin(ignite-core) | | sundb | SunDB | SunDB JDBC | | tdengine | TDengine 2.4+ | taos-connector-rust 原生 WebSocket agent | | yashandb | 崖山 YashanDB | YashanDB JDBC | diff --git a/agents/build.gradle b/agents/build.gradle index 6fa647d60a..c62a96422d 100644 --- a/agents/build.gradle +++ b/agents/build.gradle @@ -7,7 +7,7 @@ def legacyStandaloneProjects = ['mongodb', 'kafka'] as Set def pooledJdbcProjects = [ 'access', 'bigquery', 'dameng', 'databend', 'databricks', 'db2', 'exasol', 'firebird', 'gbase8a', 'gbase8s', 'goldendb', 'h2', 'h2-legacy', 'highgo', - 'informix', 'iris', 'kylin', 'oceanbase-oracle', 'oscar', 'saphana', + 'ignite', 'informix', 'iris', 'kylin', 'oceanbase-oracle', 'oscar', 'saphana', 'snowflake', 'spark', 'sqlserver-legacy', 'sundb', 'teradata', 'trino', 'uxdb', 'vertica', 'yashandb' ] as Set diff --git a/agents/drivers/ignite/build.gradle b/agents/drivers/ignite/build.gradle new file mode 100644 index 0000000000..6dc49b90ca --- /dev/null +++ b/agents/drivers/ignite/build.gradle @@ -0,0 +1,10 @@ +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'org.apache.ignite:ignite-core:2.18.0' +} + +tasks.named('shadowJar') { + manifest { + attributes('Agent-Label': 'Apache Ignite', 'Main-Class': 'com.dbx.agent.ignite.IgniteAgent') + } +} diff --git a/agents/drivers/ignite/src/main/java/com/dbx/agent/ignite/IgniteAgent.java b/agents/drivers/ignite/src/main/java/com/dbx/agent/ignite/IgniteAgent.java new file mode 100644 index 0000000000..ee1abe99a7 --- /dev/null +++ b/agents/drivers/ignite/src/main/java/com/dbx/agent/ignite/IgniteAgent.java @@ -0,0 +1,297 @@ +package com.dbx.agent.ignite; + +import com.dbx.agent.AbstractJdbcAgent; +import com.dbx.agent.ColumnInfo; +import com.dbx.agent.ConnectParams; +import com.dbx.agent.DatabaseInfo; +import com.dbx.agent.ForeignKeyInfo; +import com.dbx.agent.IndexInfo; +import com.dbx.agent.MultiSessionJsonRpcServer; +import com.dbx.agent.TableInfo; +import com.dbx.agent.TriggerInfo; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class IgniteAgent extends AbstractJdbcAgent { + + private String databaseName = ""; + + @Override + protected String driverClass() { + return "org.apache.ignite.IgniteJdbcThinDriver"; + } + + @Override + protected String buildJdbcUrl(ConnectParams params) { + return buildUrl(params); + } + + @Override + protected void afterConnect(ConnectParams params, Connection connection) { + // Ignite has no catalogs; name the virtual database after the connection's + // real default schema (e.g. PUBLIC) so any database-as-schema context that + // leaks into setSchema targets a schema that actually exists. + try { + String schema = connection.getSchema(); + databaseName = schema == null || schema.isBlank() ? params.getDatabase() : schema; + } catch (Exception e) { + databaseName = params.getDatabase(); + } + } + + @Override + public List listDatabases() { + // Ignite has no catalogs; expose the connected schema group as a single + // virtual database so the object browser stays schema-oriented like H2. + return List.of(new DatabaseInfo(databaseName.isBlank() ? "default" : databaseName)); + } + + @Override + public List listSchemas() { + return unchecked(() -> { + List result = new ArrayList<>(); + try (ResultSet rs = requireConnected().getMetaData().getSchemas()) { + while (rs.next()) { + result.add(rs.getString("TABLE_SCHEM")); + } + } + result.sort(Comparator.naturalOrder()); + return result; + }); + } + + @Override + public List listTables(String schema) { + return unchecked(() -> { + List result = new ArrayList<>(); + try (ResultSet rs = requireConnected().getMetaData().getTables(null, schema, null, null)) { + while (rs.next()) { + result.add(new TableInfo( + rs.getString("TABLE_NAME"), + normalizeTableType(rs.getString("TABLE_TYPE")), + null + )); + } + } + result.sort(Comparator.comparing(TableInfo::getName)); + return result; + }); + } + + @Override + public List getColumns(String schema, String table) { + return unchecked(() -> { + Set primaryKeys = new LinkedHashSet<>(); + try (ResultSet rs = requireConnected().getMetaData().getPrimaryKeys(null, schema, table)) { + while (rs.next()) { + primaryKeys.add(rs.getString("COLUMN_NAME")); + } + } + + List result = new ArrayList<>(); + try (ResultSet rs = requireConnected().getMetaData().getColumns(null, schema, table, null)) { + while (rs.next()) { + String colName = rs.getString("COLUMN_NAME"); + result.add(new ColumnInfo( + colName, + rs.getString("TYPE_NAME"), + "YES".equals(rs.getString("IS_NULLABLE")), + rs.getString("COLUMN_DEF"), + primaryKeys.contains(colName), + null, + emptyToNull(rs.getString("REMARKS")), + intOrNull(rs, "COLUMN_SIZE"), + intOrNull(rs, "DECIMAL_DIGITS"), + null + )); + } + } + return result; + }); + } + + @Override + public List listIndexes(String schema, String table) { + return unchecked(() -> { + DatabaseMetaData metadata = requireConnected().getMetaData(); + Set primaryIndexNames = new LinkedHashSet<>(); + List primaryColumns = new ArrayList<>(); + try (ResultSet rs = metadata.getPrimaryKeys(null, schema, table)) { + while (rs.next()) { + String name = rs.getString("PK_NAME"); + if (name != null && !name.isBlank()) { + primaryIndexNames.add(name); + } + String column = rs.getString("COLUMN_NAME"); + if (column != null && !column.isBlank()) { + primaryColumns.add(new IndexColumn(rs.getShort("KEY_SEQ"), column)); + } + } + } + + List rows = new ArrayList<>(); + try (ResultSet rs = metadata.getIndexInfo(null, schema, table, false, false)) { + while (rs.next()) { + if (rs.getShort("TYPE") == DatabaseMetaData.tableIndexStatistic) { + continue; + } + String name = rs.getString("INDEX_NAME"); + String column = rs.getString("COLUMN_NAME"); + if (name == null || name.isBlank() || column == null || column.isBlank()) { + continue; + } + rows.add(new IndexRow( + name, + column, + rs.getShort("ORDINAL_POSITION"), + !rs.getBoolean("NON_UNIQUE"), + indexType(rs.getShort("TYPE")) + )); + } + } + return assembleIndexes(rows, primaryIndexNames, primaryColumns); + }); + } + + static List assembleIndexes( + List rows, + Set primaryIndexNames, + List primaryColumns + ) { + Map> grouped = new LinkedHashMap<>(); + for (IndexRow row : rows) { + grouped.computeIfAbsent(row.name, ignored -> new ArrayList<>()).add(row); + } + List orderedPrimary = new ArrayList<>(primaryColumns); + orderedPrimary.sort(Comparator.comparingInt(column -> column.ordinal)); + List orderedPrimaryColumns = new ArrayList<>(); + for (IndexColumn column : orderedPrimary) { + orderedPrimaryColumns.add(column.name); + } + + List result = new ArrayList<>(); + for (Map.Entry> entry : grouped.entrySet()) { + List indexRows = entry.getValue(); + indexRows.sort(Comparator.comparingInt(row -> row.ordinal)); + List columns = new ArrayList<>(); + for (IndexRow row : indexRows) { + columns.add(row.column); + } + IndexRow first = indexRows.get(0); + boolean primary = primaryIndexNames.stream().anyMatch(name -> name.equalsIgnoreCase(entry.getKey())) + || sameColumns(columns, orderedPrimaryColumns); + result.add(new IndexInfo( + entry.getKey(), + columns, + first.unique, + primary, + null, + first.indexType, + null, + null + )); + } + result.sort(Comparator.comparing(IndexInfo::getName)); + return result; + } + + private static boolean sameColumns(List left, List right) { + if (left.size() != right.size() || left.isEmpty()) { + return false; + } + for (int index = 0; index < left.size(); index++) { + if (!left.get(index).equalsIgnoreCase(right.get(index))) { + return false; + } + } + return true; + } + + private static String indexType(short type) { + if (type == DatabaseMetaData.tableIndexClustered) return "CLUSTERED"; + if (type == DatabaseMetaData.tableIndexHashed) return "HASHED"; + if (type == DatabaseMetaData.tableIndexOther) return "OTHER"; + return null; + } + + static final class IndexRow { + final String name; + final String column; + final int ordinal; + final boolean unique; + final String indexType; + + IndexRow(String name, String column, int ordinal, boolean unique, String indexType) { + this.name = name; + this.column = column; + this.ordinal = ordinal; + this.unique = unique; + this.indexType = indexType; + } + } + + static final class IndexColumn { + final int ordinal; + final String name; + + IndexColumn(int ordinal, String name) { + this.ordinal = ordinal; + this.name = name; + } + } + + @Override + public List listForeignKeys(String schema, String table) { + return Collections.emptyList(); + } + + @Override + public List listTriggers(String schema, String table) { + return Collections.emptyList(); + } + + @Override + public String setSchemaSQL(String schema) { + // JdbcThinConnection implements Connection.setSchema, so no SQL switch is needed. + return ""; + } + + private static String buildUrl(ConnectParams params) { + String connectionString = params.getConnection_string(); + if (connectionString != null && !connectionString.trim().isEmpty()) { + return connectionString.trim(); + } + String base = "jdbc:ignite:thin://" + params.getHost() + ":" + params.getPort(); + String database = params.getDatabase(); + return database == null || database.isBlank() ? base : base + "/" + database; + } + + private static String normalizeTableType(String type) { + if ("BASE TABLE".equals(type)) { + return "TABLE"; + } + return type; + } + + private static Integer intOrNull(ResultSet rs, String column) throws Exception { + Object value = rs.getObject(column); + return value instanceof Number ? ((Number) value).intValue() : null; + } + + private static String emptyToNull(String value) { + return value == null || value.isEmpty() ? null : value; + } + + public static void main(String[] args) { + new MultiSessionJsonRpcServer(IgniteAgent::new).run(); + } +} diff --git a/agents/drivers/ignite/src/test/java/com/dbx/agent/ignite/IgniteAgentTest.java b/agents/drivers/ignite/src/test/java/com/dbx/agent/ignite/IgniteAgentTest.java new file mode 100644 index 0000000000..d6897a76b9 --- /dev/null +++ b/agents/drivers/ignite/src/test/java/com/dbx/agent/ignite/IgniteAgentTest.java @@ -0,0 +1,56 @@ +package com.dbx.agent.ignite; + +import com.dbx.agent.DatabaseAgent; +import com.dbx.agent.IndexInfo; +import com.dbx.agent.test.JdbcFakeExecutionBehaviorTest; +import java.sql.DatabaseMetaData; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class IgniteAgentTest extends JdbcFakeExecutionBehaviorTest { + @Override + protected DatabaseAgent createAgent() { + return new IgniteAgent(); + } + + @Override + protected String resultSetSql() { + return "SELECT 1"; + } + + @Test + void aggregatesOrdinalColumnsAndPrimaryStatus() { + List indexes = IgniteAgent.assembleIndexes( + List.of( + new IgniteAgent.IndexRow("IDX_NAME", "last_name", 2, false, "OTHER"), + new IgniteAgent.IndexRow("PK_PERSON", "tenant_id", 1, true, "HASHED"), + new IgniteAgent.IndexRow("IDX_NAME", "first_name", 1, false, "OTHER"), + new IgniteAgent.IndexRow("PK_PERSON", "person_id", 2, true, "HASHED") + ), + Set.of("PK_PERSON"), + List.of( + new IgniteAgent.IndexColumn(2, "person_id"), + new IgniteAgent.IndexColumn(1, "tenant_id") + ) + ); + + assertEquals(2, indexes.size()); + IndexInfo secondary = indexes.get(0); + assertEquals("IDX_NAME", secondary.getName()); + assertEquals(List.of("first_name", "last_name"), secondary.getColumns()); + assertFalse(secondary.getIs_unique()); + assertFalse(secondary.getIs_primary()); + + IndexInfo primary = indexes.get(1); + assertEquals("PK_PERSON", primary.getName()); + assertEquals(List.of("tenant_id", "person_id"), primary.getColumns()); + assertTrue(primary.getIs_unique()); + assertTrue(primary.getIs_primary()); + assertEquals("HASHED", primary.getIndex_type()); + } +} diff --git a/agents/metadata-constraint-coverage.tsv b/agents/metadata-constraint-coverage.tsv index e4b890588e..22285dc1d1 100644 --- a/agents/metadata-constraint-coverage.tsv +++ b/agents/metadata-constraint-coverage.tsv @@ -10,6 +10,7 @@ gbase8s native-pushdown java-sql Uses systables with type, filter, stable order, goldendb native-pushdown java-sql Uses information_schema with type, filter, stable order, and LIMIT/OFFSET. h2 native-pushdown java-sql Uses INFORMATION_SCHEMA with type, filter, stable order, and LIMIT/OFFSET. hive-go shared-fallback native-go Uses HiveServer2 metadata APIs, then applies stable filtering and paging in the native agent with a SHOW TABLES fallback. +ignite intentional-fallback java-jdbc-metadata Uses JDBC metadata from the Ignite thin driver; no portable server-side paging API, common constraints filter locally. informix native-pushdown java-sql Uses systables/sysprocedures with type, filter, stable order, and SKIP/FIRST. iotdb shared-fallback native-go Uses SHOW DEVICES or SHOW TABLES DETAILS for Tree/Table metadata, then applies stable filtering and paging in the native agent. kingbase-go shared-fallback native-go Uses sys_catalog/information_schema for compatibility-aware metadata, then applies stable filtering and paging in the native agent. diff --git a/agents/settings.gradle b/agents/settings.gradle index a2420f51d7..131542186c 100644 --- a/agents/settings.gradle +++ b/agents/settings.gradle @@ -4,7 +4,7 @@ def infrastructureModules = ['common', 'test-support'] def driverModules = [ 'access', 'dameng', 'goldendb', 'databend', 'databricks', 'saphana', 'teradata', 'vertica', 'firebird', 'exasol', 'oceanbase-oracle', 'gbase8a', 'gbase8s', - 'bigquery', 'kylin', 'sundb', 'h2', 'h2-legacy', 'snowflake', 'trino', 'spark', + 'bigquery', 'kylin', 'ignite', 'sundb', 'h2', 'h2-legacy', 'snowflake', 'trino', 'spark', 'db2', 'informix', 'mongodb', 'highgo', 'uxdb', 'yashandb', 'oscar', 'iris', 'etcd', 'kafka', 'sqlserver-legacy' ] diff --git a/agents/versions.json b/agents/versions.json index c4214520ca..e25df6f4fd 100644 --- a/agents/versions.json +++ b/agents/versions.json @@ -15,6 +15,7 @@ "h2": "0.1.46", "h2-legacy": "0.1.16", "hive": "0.1.49", + "ignite": "0.1.0", "spark": "0.1.18", "informix": "0.1.47", "kingbase": "0.1.49", diff --git a/apps/desktop/public/icons/database/apache_ignite.svg b/apps/desktop/public/icons/database/apache_ignite.svg new file mode 100644 index 0000000000..0e3a157692 --- /dev/null +++ b/apps/desktop/public/icons/database/apache_ignite.svg @@ -0,0 +1 @@ + diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 847dcf167f..bdab408317 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -1234,6 +1234,7 @@ const driverProfiles: Record< host: "https://www.googleapis.com/bigquery/v2", }, kylin: { type: "kylin", port: 7070, user: "ADMIN", label: "Apache Kylin", icon: "kylin" }, + ignite: { type: "ignite", port: 10800, user: "", label: "Apache Ignite", icon: "ignite" }, sundb: { type: "sundb", port: 22000, user: "root", label: "科蓝 SUNDB", icon: "sundb" }, oscar: { type: "oscar", port: 2003, user: "SYSDBA", label: "神通 OSCAR", icon: "oscar" }, jdbc: { type: "jdbc", port: 0, user: "", label: "JDBC", icon: "jdbc" }, @@ -2973,6 +2974,7 @@ const iconTypeMap: Record = { cassandra: "cassandra", bigquery: "bigquery", kylin: "kylin", + ignite: "ignite", sundb: "sundb", oscar: "oscar", influxdb: "influxdb", @@ -3054,6 +3056,7 @@ const dbOptions: DbOption[] = [ { value: "cassandra", label: "Cassandra" }, { value: "bigquery", label: "BigQuery" }, { value: "kylin", label: "Kylin" }, + { value: "ignite", label: "Apache Ignite" }, { value: "sundb", label: "科蓝 SUNDB" }, { value: "oscar", label: "神通 OSCAR" }, { value: "xugu", label: "虚谷 XuguDB" }, @@ -3092,7 +3095,7 @@ const dbCategoryDefinitions: Array<{ { key: "analytics", titleKey: "connection.databaseCategoryAnalytics", - optionValues: ["cloudberry", "clickhouse", "doris", "starrocks", "databend", "selectdb", "databricks", "saphana", "teradata", "vertica", "exasol", "redshift", "snowflake", "trino", "prestosql", "hive", "kyuubi", "impala", "spark", "bigquery", "kylin", "dremio"], + optionValues: ["cloudberry", "clickhouse", "doris", "starrocks", "databend", "selectdb", "databricks", "saphana", "teradata", "vertica", "exasol", "redshift", "snowflake", "trino", "prestosql", "hive", "kyuubi", "impala", "spark", "bigquery", "kylin", "ignite", "dremio"], }, { key: "domestic", diff --git a/apps/desktop/src/components/icons/DatabaseIcon.vue b/apps/desktop/src/components/icons/DatabaseIcon.vue index e34630bd29..c6c67b22f7 100644 --- a/apps/desktop/src/components/icons/DatabaseIcon.vue +++ b/apps/desktop/src/components/icons/DatabaseIcon.vue @@ -63,9 +63,11 @@ const assetIcons: Record = { phoenix: "phoenix", spark: "spark-logo.png", apache_kylin: "apache_kylin", + apache_ignite: "apache_ignite", sundb: "sundb", trino: "trino", kylin: "apache_kylin", + ignite: "apache_ignite", cockroachdb: "cockroachdb", db2: "db2", dremio: "dremio", diff --git a/apps/desktop/src/lib/connection/__tests__/driverCategoryDefinitions.spec.ts b/apps/desktop/src/lib/connection/__tests__/driverCategoryDefinitions.spec.ts index accd4e553c..254a83c1b7 100644 --- a/apps/desktop/src/lib/connection/__tests__/driverCategoryDefinitions.spec.ts +++ b/apps/desktop/src/lib/connection/__tests__/driverCategoryDefinitions.spec.ts @@ -88,6 +88,7 @@ describe("AGENT_DRIVER_CATEGORY_MAP integrity", () => { "databricks", "exasol", "hive", + "ignite", "kylin", "phoenix", "prestosql", diff --git a/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts b/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts index 1696b5fd29..8ed16defec 100644 --- a/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts +++ b/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts @@ -39,6 +39,7 @@ const DRIVER_STARTUP_FLOOR_TYPES = new Set([ "cassandra", "bigquery", "kylin", + "ignite", "sundb", "oscar", "tdengine", diff --git a/apps/desktop/src/lib/connection/driver-category-definitions.ts b/apps/desktop/src/lib/connection/driver-category-definitions.ts index 5707d89774..ab989cd05b 100644 --- a/apps/desktop/src/lib/connection/driver-category-definitions.ts +++ b/apps/desktop/src/lib/connection/driver-category-definitions.ts @@ -42,6 +42,7 @@ export const AGENT_DRIVER_CATEGORY_MAP: Readonly([ "gbase", "jdbc", "h2", + "ignite", "snowflake", "trino", "prestosql", @@ -75,6 +76,7 @@ export const TREE_SCHEMA_TYPES = new Set([ "trino", "prestosql", "h2", + "ignite", "informix", "xugu", "oscar", diff --git a/apps/desktop/src/lib/database/databaseNamespaceCreation.ts b/apps/desktop/src/lib/database/databaseNamespaceCreation.ts index 7266dd1b35..0a80ca7a50 100644 --- a/apps/desktop/src/lib/database/databaseNamespaceCreation.ts +++ b/apps/desktop/src/lib/database/databaseNamespaceCreation.ts @@ -76,6 +76,7 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = { cassandra: { deferred: "keyspace creation requires replication options" }, bigquery: { deferred: "dataset creation needs project/location options" }, kylin: { deferred: "project/model lifecycle is not SQL database creation" }, + ignite: { deferred: "schema lifecycle is managed through cluster/cache configuration" }, sundb: { deferred: "creation semantics not verified for first pass" }, oscar: { database: "schema" }, tdengine: { connection: "database" }, diff --git a/apps/desktop/src/lib/database/databaseObjectCapabilities.ts b/apps/desktop/src/lib/database/databaseObjectCapabilities.ts index 935769e93b..983bd6ae8b 100644 --- a/apps/desktop/src/lib/database/databaseObjectCapabilities.ts +++ b/apps/desktop/src/lib/database/databaseObjectCapabilities.ts @@ -77,6 +77,7 @@ const DATABASE_TYPE_OBJECTS = new Map([ ["cassandra", TABLE_VIEW_OBJECTS], ["bigquery", TABLE_VIEW_OBJECTS], ["kylin", TABLE_VIEW_OBJECTS], + ["ignite", TABLE_VIEW_OBJECTS], ["tdengine", TABLE_VIEW_OBJECTS], ["iotdb", TABLE_VIEW_OBJECTS], ["neo4j", TABLE_VIEW_OBJECTS], diff --git a/apps/desktop/src/lib/database/databasePropertyEditing.ts b/apps/desktop/src/lib/database/databasePropertyEditing.ts index 6d68de740b..fd752f3910 100644 --- a/apps/desktop/src/lib/database/databasePropertyEditing.ts +++ b/apps/desktop/src/lib/database/databasePropertyEditing.ts @@ -76,6 +76,7 @@ export const DATABASE_PROPERTY_EDITING_MATRIX = { cassandra: { deferred: "keyspace properties require replication option handling" }, bigquery: { deferred: "dataset properties need project/location-specific handling" }, kylin: { deferred: "project/model lifecycle is not SQL database property editing" }, + ignite: { deferred: "schema properties need product-specific handling" }, sundb: { deferred: "property semantics not verified for first pass" }, oscar: { deferred: "schema properties need product-specific handling" }, tdengine: { deferred: "database property editing not verified for first pass" }, diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 9afc099b64..4e06eff1e5 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -1132,6 +1132,7 @@ export const useConnectionStore = defineStore("connection", () => { cassandra: "Cassandra", bigquery: "BigQuery", kylin: "Kylin", + ignite: "Apache Ignite", sundb: "科蓝 SUNDB", oscar: "神通 OSCAR", influxdb: "InfluxDB", diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 8b22819cac..426df2aeac 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -63,6 +63,7 @@ export type DatabaseType = | "cassandra" | "bigquery" | "kylin" + | "ignite" | "sundb" | "oscar" | "tdengine" diff --git a/crates/dbx-cli/src/main.rs b/crates/dbx-cli/src/main.rs index a12dcdaef7..4a0e690573 100644 --- a/crates/dbx-cli/src/main.rs +++ b/crates/dbx-cli/src/main.rs @@ -66,6 +66,7 @@ const BRIDGE_REQUIRED_TYPES: &[&str] = &[ "cassandra", "bigquery", "kylin", + "ignite", "sundb", "oscar", "xugu", diff --git a/crates/dbx-core/assets/database-drivers.manifest.json b/crates/dbx-core/assets/database-drivers.manifest.json index 0e0801d837..d856f83f05 100644 --- a/crates/dbx-core/assets/database-drivers.manifest.json +++ b/crates/dbx-core/assets/database-drivers.manifest.json @@ -1728,6 +1728,36 @@ "driverManagement": true } }, + { + "dbType": "ignite", + "label": "Apache Ignite", + "runtimeMode": "agent", + "mcpMode": "bridge", + "agentKey": "ignite", + "singleConnectionPool": false, + "metadataConnectionScoped": false, + "skipTcpProbe": true, + "defaultPort": 10800, + "supportLevel": "understand", + "capabilities": { + "queryExecution": true, + "metadataBrowse": true, + "objectBrowser": true, + "objectSource": false, + "schemaSearch": true, + "diagram": false, + "tableDataEdit": false, + "tableStructureEdit": false, + "tableImport": false, + "dataTransfer": false, + "sqlFileExecution": true, + "databaseCreate": false, + "fieldLineage": false, + "sqlExplain": false, + "userAdmin": false, + "driverManagement": true + } + }, { "dbType": "sundb", "label": "科蓝 SUNDB", diff --git a/crates/dbx-core/src/agent_catalog.rs b/crates/dbx-core/src/agent_catalog.rs index e30bef9047..0bc9a3e77a 100644 --- a/crates/dbx-core/src/agent_catalog.rs +++ b/crates/dbx-core/src/agent_catalog.rs @@ -260,6 +260,13 @@ const AGENT_CATALOG: &[AgentCatalogEntry] = &[ store_visible: true, profiles: &[], }, + AgentCatalogEntry { + db_type: DatabaseType::Ignite, + key: "ignite", + label: "Apache Ignite", + store_visible: true, + profiles: &[], + }, AgentCatalogEntry { db_type: DatabaseType::Sundb, key: "sundb", diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index 224e788178..bbfb41e08e 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -182,6 +182,7 @@ macro_rules! agent_connection_pool_database_type { | DatabaseType::Cassandra | DatabaseType::Bigquery | DatabaseType::Kylin + | DatabaseType::Ignite | DatabaseType::Sundb | DatabaseType::Oscar | DatabaseType::Tdengine diff --git a/crates/dbx-core/src/db/agent_driver.rs b/crates/dbx-core/src/db/agent_driver.rs index 61b1c2b509..0b501625ea 100644 --- a/crates/dbx-core/src/db/agent_driver.rs +++ b/crates/dbx-core/src/db/agent_driver.rs @@ -2897,6 +2897,23 @@ fn agent_java_args_with_extra_opts( args.push("-Djava.net.preferIPv4Stack=true".to_string()); } + // Ignite's GridUnsafe/BinaryContext reflect into JDK internals; without these + // opens every connect fails with ExceptionInInitializerError on JDK 9+. + if agent_jar_path_matches_key(jar_path, "ignite") { + args.extend( + [ + "--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED", + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", + ] + .into_iter() + .map(str::to_string), + ); + } + // Hive/Kerberos JDBC drivers read JAAS and krb5 settings during JVM startup, // so users need a process-level escape hatch before the agent jar is loaded. if let Some(extra) = extra_opts { @@ -3625,6 +3642,22 @@ mod tests { assert!(!args.iter().any(|arg| arg == "-Djava.net.preferIPv4Stack=true")); } + #[test] + fn agent_java_args_open_jdk_internals_for_ignite() { + let args = agent_java_args("/tmp/dbx/drivers/ignite/agent.jar"); + + assert!(args.iter().any(|arg| arg == "--add-opens=java.base/java.nio=ALL-UNNAMED")); + assert!(args.iter().any(|arg| arg == "--add-opens=java.base/java.util=ALL-UNNAMED")); + assert!(args.iter().any(|arg| arg == "--add-opens=java.base/java.lang=ALL-UNNAMED")); + } + + #[test] + fn agent_java_args_do_not_open_jdk_internals_for_other_agents() { + let args = agent_java_args("/tmp/dbx/drivers/kylin/agent.jar"); + + assert!(!args.iter().any(|arg| arg == "--add-opens=java.base/java.nio=ALL-UNNAMED")); + } + #[test] fn agent_java_args_include_custom_jvm_options_before_jar() { let args = agent_java_args_with_extra( diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 491d77c8f2..846b5901a8 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -600,6 +600,7 @@ pub enum DatabaseType { VictoriaMetrics, #[serde(rename = "questdb")] Questdb, + Ignite, Jdbc, /// Message queue admin connection (Pulsar / Kafka / RocketMQ). The specific /// system is determined by `external_config.systemKind`. @@ -1143,6 +1144,7 @@ impl ConnectionConfig { } } DatabaseType::Questdb => format!("questdb://{host}:{port}{db_part}"), + DatabaseType::Ignite => format!("ignite://{host}:{port}{db_part}"), DatabaseType::Gbase => format!("gbase://{host}:{port}{db_part}"), DatabaseType::H2 => format!("h2://{host}:{port}{db_part}"), DatabaseType::Snowflake => format!("snowflake://{host}/{db_part}"), @@ -1347,6 +1349,9 @@ impl ConnectionConfig { DatabaseType::Questdb => { format!("questdb://{}:{}@{host}:{port}{db_part}", username, password) } + DatabaseType::Ignite => { + format!("ignite://{}:{}@{host}:{port}{db_part}", username, password) + } DatabaseType::Gbase => { format!("gbase://{}:{}@{host}:{port}{db_part}", username, password) } diff --git a/crates/dbx-core/src/sql_dialect/ddl_profile.rs b/crates/dbx-core/src/sql_dialect/ddl_profile.rs index 05f9b8d40f..1b44edda7a 100644 --- a/crates/dbx-core/src/sql_dialect/ddl_profile.rs +++ b/crates/dbx-core/src/sql_dialect/ddl_profile.rs @@ -670,7 +670,7 @@ pub fn profile_for(db_type: DatabaseType) -> DdlDialectProfile { ClickHouse => clickhouse_profile(), DuckDb | Questdb | SapHana | Teradata | Snowflake | Trino | PrestoSql | Hive | Kyuubi | Impala | Spark - | Db2 | Informix | Bigquery | Kylin | Oscar | Tdengine | Iotdb | Databricks | Jdbc => { + | Db2 | Informix | Bigquery | Kylin | Ignite | Oscar | Tdengine | Iotdb | Databricks | Jdbc => { conservative_ansi(db_type) } diff --git a/docs/data/databaseSupport.ts b/docs/data/databaseSupport.ts index 62802d81b1..e039628c3c 100644 --- a/docs/data/databaseSupport.ts +++ b/docs/data/databaseSupport.ts @@ -71,6 +71,7 @@ export const databaseSupport: DatabaseSupportItem[] = [ { id: "neo4j", name: "Neo4j", icon: "/icons/database/neo4j.svg", tone: "#018bff" }, { id: "cassandra", name: "Cassandra", icon: "/icons/database/cassandra.svg", tone: "#1287b1" }, { id: "kylin", name: "Kylin", icon: "/icons/database/apache_kylin.svg", tone: "#fb8c00" }, + { id: "ignite", name: "Ignite", icon: "/icons/database/apache_ignite.svg", tone: "#ef2c2c" }, { id: "dremio", name: "Dremio", icon: "/icons/database/dremio.svg", tone: "#30bdbe" }, { id: "oscar", name: "OSCAR", icon: "/icons/database/oscar.png", tone: "#1b8dff" }, { id: "influxdb", name: "InfluxDB", icon: "/icons/database/influxdb.svg", tone: "#22adf6" }, diff --git a/docs/lib/agentRegistry.ts b/docs/lib/agentRegistry.ts index 1c78e56b3a..691cf5798e 100644 --- a/docs/lib/agentRegistry.ts +++ b/docs/lib/agentRegistry.ts @@ -119,6 +119,7 @@ const driverLabels: Record = { h2: "H2", highgo: "HighGo", hive: "Apache Hive", + ignite: "Apache Ignite", informix: "Informix", iotdb: "Apache IoTDB", iris: "InterSystems IRIS", diff --git a/docs/public/icons/database/apache_ignite.svg b/docs/public/icons/database/apache_ignite.svg new file mode 100644 index 0000000000..0e3a157692 --- /dev/null +++ b/docs/public/icons/database/apache_ignite.svg @@ -0,0 +1 @@ +