From c3893bf314610083d6fc3836b4595c241eef228d Mon Sep 17 00:00:00 2001 From: jischeng <49861575+jischeng@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:36:22 +0800 Subject: [PATCH] feat(spark): add Apache Spark database driver with catalog support * feat(spark): add Apache Spark database driver Spark Thrift Server speaks the HiveServer2 protocol, so the Spark driver reuses the hive-jdbc agent runtime. Registers spark (agentKey=spark, defaultPort=10015) across the manifest, DatabaseType enum, agent catalog, SQL dialects, connection dialog, and a SparkAgent built on AbstractJdbcAgent. * feat(spark): add catalog support and official logo SparkAgent now supports Spark 3.4+ multi-catalog (Paimon, Lance, Iceberg) via catalog= in url_params, mirroring the StarRocks catalog flow: - afterConnect switches to the configured catalog via USE - listDatabases returns the catalog name when SHOW DATABASES is empty (catalogs like Lance expose tables at the catalog root with no databases) - listTables uses SHOW TABLES IN [.] uniformly, working for both catalog-root tables (Lance) and catalog.schema tables (Paimon) - setSchemaSQL qualifies USE with the catalog to prevent resetting to spark_catalog on schema switches - buildJdbcUrl omits database from the URL path Also replaces the placeholder lightning icon with the official Apache Spark logo (spark-logo.png from spark.apache.org). * fix(spark): use catalog-qualified DESCRIBE for column retrieval getColumnsFromDescribe used USE + DESCRIBE , which generated USE `catalog`.`catalog` when the sidebar passed the catalog name as the schema (Lance case where the catalog is the only node). This failed and fell back to JDBC metadata which also returned nothing, resulting in an empty DDL (CREATE TABLE ... ()). Now uses DESCRIBE `catalog`.`table` directly when a catalog is configured, bypassing USE entirely. Also fixes setSchemaSQL to treat schema == catalog as catalog-only (no .schema suffix). * fix(ci): add spark to metadata coverage matrix and BRIDGE_REQUIRED_TYPES - agents/metadata-constraint-coverage.tsv: register spark as intentional-fallback (SHOW TABLES/SHOW DATABASES, like Hive/TDengine) - packages/node-core/src/diagnostics.ts: add spark to BRIDGE_REQUIRED_TYPES (mcpMode=bridge, matches manifest) * fix(ci): add spark to DBX_CONNECTION_TYPE_DESCRIPTION in mcp-server The driver-manifest test asserts every manifest dbType appears in the MCP server's connection type description string. --- agents/drivers/spark/build.gradle | 11 + .../java/com/dbx/agent/spark/SparkAgent.java | 308 ++++++++++++++++++ agents/metadata-constraint-coverage.tsv | 1 + agents/settings.gradle | 2 +- agents/versions.json | 1 + .../public/icons/database/spark-logo.png | Bin 0 -> 26999 bytes .../connection/ConnectionDialog.vue | 7 +- .../src/components/icons/DatabaseIcon.vue | 1 + .../connection/connectionAttemptTimeout.ts | 1 + .../lib/database/databaseCapabilitySets.ts | 1 + .../lib/database/databaseNamespaceCreation.ts | 1 + .../database/databaseObjectCapabilities.ts | 1 + .../lib/database/databasePropertyEditing.ts | 1 + apps/desktop/src/lib/table/tableSelectSql.ts | 2 +- apps/desktop/src/stores/connectionStore.ts | 1 + apps/desktop/src/types/database.ts | 1 + .../assets/database-drivers.manifest.json | 30 ++ crates/dbx-core/src/agent_catalog.rs | 7 + crates/dbx-core/src/connection.rs | 1 + crates/dbx-core/src/models/connection.rs | 5 + .../dbx-core/src/sql_dialect/capabilities.rs | 1 + .../dbx-core/src/sql_dialect/identifiers.rs | 2 + packages/mcp-server/src/index.ts | 2 +- packages/node-core/src/diagnostics.ts | 1 + 24 files changed, 385 insertions(+), 4 deletions(-) create mode 100644 agents/drivers/spark/build.gradle create mode 100644 agents/drivers/spark/src/main/java/com/dbx/agent/spark/SparkAgent.java create mode 100644 apps/desktop/public/icons/database/spark-logo.png diff --git a/agents/drivers/spark/build.gradle b/agents/drivers/spark/build.gradle new file mode 100644 index 0000000000..868be72134 --- /dev/null +++ b/agents/drivers/spark/build.gradle @@ -0,0 +1,11 @@ +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'org.apache.hive:hive-jdbc:4.0.1:standalone' + implementation 'org.slf4j:slf4j-nop:1.7.30' +} + +tasks.named('shadowJar') { + manifest { + attributes('Agent-Label': 'Apache Spark', 'Main-Class': 'com.dbx.agent.spark.SparkAgent') + } +} diff --git a/agents/drivers/spark/src/main/java/com/dbx/agent/spark/SparkAgent.java b/agents/drivers/spark/src/main/java/com/dbx/agent/spark/SparkAgent.java new file mode 100644 index 0000000000..fc0d4e9a0b --- /dev/null +++ b/agents/drivers/spark/src/main/java/com/dbx/agent/spark/SparkAgent.java @@ -0,0 +1,308 @@ +package com.dbx.agent.spark; + +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.JdbcIdentifiers; +import com.dbx.agent.JsonRpcServer; +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.List; +import java.util.Locale; + +// Spark Thrift Server speaks the HiveServer2 protocol, so the Spark agent reuses +// the Hive JDBC driver and mirrors the Hive agent's metadata queries (SHOW +// DATABASES / SHOW TABLES / DESCRIBE), which Spark SQL supports as well. +// +// Spark 3.4+ supports multiple catalogs (Paimon, Lance, Iceberg, ...). When the +// user supplies `catalog=` in url_params, the agent switches to that catalog +// after connecting and uses `SHOW TABLES IN ` for listing — which works +// uniformly for catalogs that have databases (Paimon) and those that expose tables +// at the catalog root with no databases (Lance). Mirrors the StarRocks catalog flow. +public final class SparkAgent extends AbstractJdbcAgent { + private String configuredCatalog; + + @Override + protected String driverClass() { + return "org.apache.hive.jdbc.HiveDriver"; + } + + @Override + protected String buildJdbcUrl(ConnectParams params) { + return "jdbc:hive2://" + params.getHost() + ":" + params.getPort() + "/"; + } + + @Override + protected void afterConnect(ConnectParams params, Connection connection) throws Exception { + String catalog = extractCatalogParam(params.getUrl_params()); + configuredCatalog = catalog; + if (catalog != null && !catalog.isEmpty()) { + try (java.sql.Statement stmt = connection.createStatement()) { + stmt.execute("USE " + JdbcIdentifiers.INSTANCE.backtick(catalog)); + } + } + } + + @Override + public List listDatabases() { + return unchecked(() -> { + List result = new ArrayList<>(); + try (java.sql.Statement stmt = requireConnected().createStatement(); + ResultSet rs = stmt.executeQuery("SHOW DATABASES")) { + while (rs.next()) { + result.add(new DatabaseInfo(rs.getString(1))); + } + } + // Catalogs like Lance expose tables at the catalog root with no + // databases — SHOW DATABASES returns nothing. Return the catalog + // name itself as the database node so the sidebar can drill in. + if (result.isEmpty() && configuredCatalog != null && !configuredCatalog.isEmpty()) { + result.add(new DatabaseInfo(configuredCatalog)); + } + result.sort(Comparator.comparing(DatabaseInfo::getName)); + return result; + }); + } + + @Override + public List listSchemas() { + List result = new ArrayList<>(); + for (DatabaseInfo database : listDatabases()) { + result.add(database.getName()); + } + return result; + } + + @Override + public List listTables(String schema) { + return unchecked(() -> { + List result = new ArrayList<>(); + String sql = buildShowTablesSql(schema); + try (java.sql.Statement stmt = requireConnected().createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) { + String name = readTableName(rs); + if (name != null && !name.isEmpty()) { + result.add(new TableInfo(name, "TABLE", null)); + } + } + } + result.sort(Comparator.comparing(TableInfo::getName)); + return result; + }); + } + + // With a catalog configured, use `SHOW TABLES IN [.]` which + // works for both catalog-root tables (Lance) and catalog.schema tables (Paimon). + // Without a catalog, fall back to the Hive-style `USE ` + SHOW TABLES. + private String buildShowTablesSql(String schema) throws Exception { + if (configuredCatalog != null && !configuredCatalog.isEmpty()) { + String target = JdbcIdentifiers.INSTANCE.backtick(configuredCatalog); + if (schema != null && !schema.isEmpty() && !schema.equals(configuredCatalog)) { + target += "." + JdbcIdentifiers.INSTANCE.backtick(schema); + } + return "SHOW TABLES IN " + target; + } + useSchema(schema); + return "SHOW TABLES"; + } + + // `SHOW TABLES` (Hive style) returns a single tabName column, while + // `SHOW TABLES IN ` (Spark SQL) returns (database, tableName, + // isTemporary). Pick tableName from whichever column it lives in. + private static String readTableName(ResultSet rs) throws Exception { + int columns = rs.getMetaData().getColumnCount(); + return columns > 1 ? rs.getString(2) : rs.getString(1); + } + + @Override + public List getColumns(String schema, String table) { + return unchecked(() -> { + try { + return getColumnsFromDescribe(schema, table); + } catch (Exception ignored) { + return getColumnsFromMetadata(requireConnected(), schema, table); + } + }); + } + + @Override + public List listIndexes(String schema, String table) { + return Collections.emptyList(); + } + + @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) { + // When a catalog is configured, qualify the USE statement with the + // catalog so switching schemas does not reset the catalog back to the + // default (spark_catalog). + if (configuredCatalog != null && !configuredCatalog.isEmpty()) { + if (schema == null || schema.isEmpty() || schema.equals(configuredCatalog)) { + return "USE " + JdbcIdentifiers.INSTANCE.backtick(configuredCatalog); + } + return "USE " + JdbcIdentifiers.INSTANCE.backtick(configuredCatalog) + + "." + JdbcIdentifiers.INSTANCE.backtick(schema); + } + return "USE " + JdbcIdentifiers.INSTANCE.backtick(schema); + } + + private static String extractCatalogParam(String urlParams) { + if (urlParams == null || urlParams.isEmpty()) { + return null; + } + for (String segment : urlParams.split("&")) { + int eq = segment.indexOf('='); + if (eq <= 0) { + continue; + } + if (segment.substring(0, eq).trim().equalsIgnoreCase("catalog")) { + String value = segment.substring(eq + 1).trim(); + return value.isEmpty() ? null : value; + } + } + return null; + } + + @Override + protected Object resultValue(ResultSet rs, int index, int sqlType) { + return unchecked(() -> { + Object value = rs.getObject(index); + return rs.wasNull() ? null : value == null ? null : value.toString(); + }); + } + + private void useSchema(String schema) throws Exception { + try (java.sql.Statement stmt = requireConnected().createStatement()) { + stmt.execute(setSchemaSQL(schema)); + } + } + + private List getColumnsFromDescribe(String schema, String table) throws Exception { + List result = new ArrayList<>(); + // Use catalog-qualified DESCRIBE when a catalog is configured, avoiding + // USE which would generate USE `catalog`.`catalog` when the + // sidebar passes the catalog name as the schema (Lance case). + String describeTarget; + if (configuredCatalog != null && !configuredCatalog.isEmpty()) { + describeTarget = JdbcIdentifiers.INSTANCE.backtick(configuredCatalog); + if (schema != null && !schema.isEmpty() && !schema.equals(configuredCatalog)) { + describeTarget += "." + JdbcIdentifiers.INSTANCE.backtick(schema); + } + describeTarget += "." + JdbcIdentifiers.INSTANCE.backtick(table); + } else { + useSchema(schema); + describeTarget = JdbcIdentifiers.INSTANCE.backtick(table); + } + try (java.sql.Statement stmt = requireConnected().createStatement(); + ResultSet rs = stmt.executeQuery("DESCRIBE " + describeTarget)) { + while (rs.next()) { + String colName = trimToNull(rs.getString(1)); + if (colName == null || colName.startsWith("#")) { + continue; + } + result.add(new ColumnInfo( + colName, + trimToEmpty(rs.getString(2)), + true, + null, + false, + null, + optionalComment(rs), + null, + null, + null + )); + } + } + return result; + } + + private static List getColumnsFromMetadata(Connection conn, String schema, String table) throws Exception { + List result = new ArrayList<>(); + DatabaseMetaData meta = conn.getMetaData(); + try (ResultSet rs = meta.getColumns(null, trimToNull(schema), table, "%")) { + while (rs.next()) { + String name = rs.getString("COLUMN_NAME"); + if (trimToNull(name) == null) { + continue; + } + result.add(new ColumnInfo( + name, + trimToEmpty(rs.getString("TYPE_NAME")), + rs.getInt("NULLABLE") != DatabaseMetaData.columnNoNulls, + rs.getString("COLUMN_DEF"), + false, + null, + rs.getString("REMARKS"), + intOrNull(rs, "COLUMN_SIZE"), + intOrNull(rs, "DECIMAL_DIGITS"), + characterLength(rs) + )); + } + } + return result; + } + + private static String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + private static String trimToEmpty(String value) { + return value == null ? "" : value.trim(); + } + + 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 Integer characterLength(ResultSet rs) throws Exception { + String typeName = rs.getString("TYPE_NAME"); + if (typeName == null) { + return null; + } + String normalized = typeName.toLowerCase(Locale.ROOT); + if (!normalized.contains("char") && !normalized.contains("text") && !normalized.contains("string")) { + return null; + } + return intOrNull(rs, "COLUMN_SIZE"); + } + + private static String optionalComment(ResultSet rs) { + return unchecked(() -> { + try { + String comment = trimToNull(rs.getString(3)); + return comment; + } catch (Exception e) { + return null; + } + }); + } + + public static void main(String[] args) { + new JsonRpcServer(new SparkAgent()).run(); + } +} diff --git a/agents/metadata-constraint-coverage.tsv b/agents/metadata-constraint-coverage.tsv index 7a00fc1079..e8b59c4cc9 100644 --- a/agents/metadata-constraint-coverage.tsv +++ b/agents/metadata-constraint-coverage.tsv @@ -17,6 +17,7 @@ neo4j intentional-fallback java-graph-metadata Uses JDBC metadata with Cypher fa oceanbase-oracle native-pushdown java-sql Uses Oracle-compatible metadata SQL with type, filter, stable order, and ROWNUM paging. oracle-go native-pushdown native-go Uses Oracle-compatible metadata SQL with type, filter, stable order, and ROWNUM paging, with legacy profile guard. snowflake native-pushdown java-sql Uses INFORMATION_SCHEMA with type, filter, stable order, and literal LIMIT/OFFSET. +spark intentional-fallback java-show-metadata Uses SHOW TABLES/SHOW DATABASES; stable server-side fuzzy paging is not portable, common constraints filter locally. sundb native-pushdown java-sql Uses information_schema with type, filter, stable order, and LIMIT/OFFSET. tdengine intentional-fallback java-show-metadata Uses SHOW STABLES/TABLES; stable server-side fuzzy paging is not portable, common constraints filter locally. trino native-pushdown java-sql Uses information_schema.tables with type, filter, stable order, and literal LIMIT/OFFSET. diff --git a/agents/settings.gradle b/agents/settings.gradle index 5d0be1832f..083223780d 100644 --- a/agents/settings.gradle +++ b/agents/settings.gradle @@ -4,7 +4,7 @@ def infrastructureModules = ['common', 'test-support'] def driverModules = [ 'access', 'dameng', 'kingbase', 'vastbase', 'goldendb', 'databend', 'databricks', 'saphana', 'teradata', 'vertica', 'firebird', 'exasol', 'oceanbase-oracle', 'gbase8a', 'gbase8s', - 'bigquery', 'kylin', 'sundb', 'h2', 'snowflake', 'trino', 'hive', + 'bigquery', 'kylin', 'sundb', 'h2', 'snowflake', 'trino', 'hive', 'spark', 'db2', 'informix', 'neo4j', 'cassandra', 'mongodb', 'highgo', 'tdengine', 'yashandb', 'oscar', 'iris', 'iotdb', 'etcd', 'zookeeper', 'kafka' ] diff --git a/agents/versions.json b/agents/versions.json index 1c85c991b3..fa697d10a7 100644 --- a/agents/versions.json +++ b/agents/versions.json @@ -13,6 +13,7 @@ "goldendb": "0.1.28", "h2": "0.1.28", "hive": "0.1.28", + "spark": "0.1.0", "informix": "0.1.29", "kingbase": "0.1.28", "kylin": "0.1.28", diff --git a/apps/desktop/public/icons/database/spark-logo.png b/apps/desktop/public/icons/database/spark-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..16702a9f022a301c3098122d2dd1e4f5a8595679 GIT binary patch literal 26999 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 = { trino: "trino", prestosql: "prestosql", hive: "hive", + spark: "spark-logo.png", db2: "db2", informix: "informix", dremio: "dremio", @@ -1593,6 +1595,7 @@ const dbOptions: DbOption[] = [ { value: "trino", label: "Trino" }, { value: "prestosql", label: "PrestoSQL" }, { value: "hive", label: "Hive" }, + { value: "spark", label: "Apache Spark" }, { value: "db2", label: "DB2" }, { value: "informix", label: "Informix" }, { value: "neo4j", label: "Neo4j" }, @@ -4422,7 +4425,9 @@ function openExternalUrl(url: string) { ? 'OAuthType=0;OAuthServiceAcctEmail=svc@project.iam.gserviceaccount.com;OAuthPvtKeyPath=/path/key.json' : form.db_type === 'informix' ? 'CLIENT_LOCALE=en_US.utf8;DB_LOCALE=en_US.utf8' - : 'sslmode=disable' + : form.db_type === 'spark' + ? 'catalog=paimon_catalog' + : 'sslmode=disable' " /> diff --git a/apps/desktop/src/components/icons/DatabaseIcon.vue b/apps/desktop/src/components/icons/DatabaseIcon.vue index c712df2d0f..9cac04e3c8 100644 --- a/apps/desktop/src/components/icons/DatabaseIcon.vue +++ b/apps/desktop/src/components/icons/DatabaseIcon.vue @@ -47,6 +47,7 @@ const assetIcons: Record = { presto: "presto", prestosql: "presto", hive: "hive", + spark: "spark-logo.png", apache_kylin: "apache_kylin", sundb: "sundb", trino: "trino", diff --git a/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts b/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts index fc3ddfc6dc..0b6f6709f5 100644 --- a/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts +++ b/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts @@ -29,6 +29,7 @@ const DRIVER_STARTUP_FLOOR_TYPES = new Set([ "prestosql", "jdbc", "hive", + "spark", "db2", "informix", "neo4j", diff --git a/apps/desktop/src/lib/database/databaseCapabilitySets.ts b/apps/desktop/src/lib/database/databaseCapabilitySets.ts index 5ef1ffc871..7155c88ce4 100644 --- a/apps/desktop/src/lib/database/databaseCapabilitySets.ts +++ b/apps/desktop/src/lib/database/databaseCapabilitySets.ts @@ -26,6 +26,7 @@ export const SCHEMA_AWARE_TYPES = new Set([ "trino", "prestosql", "hive", + "spark", "databend", "db2", "informix", diff --git a/apps/desktop/src/lib/database/databaseNamespaceCreation.ts b/apps/desktop/src/lib/database/databaseNamespaceCreation.ts index 462e4b4404..63cdde552b 100644 --- a/apps/desktop/src/lib/database/databaseNamespaceCreation.ts +++ b/apps/desktop/src/lib/database/databaseNamespaceCreation.ts @@ -60,6 +60,7 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = { trino: { database: "schema" }, prestosql: { database: "schema" }, hive: { deferred: "Hive database creation needs agent metadata validation first" }, + spark: { deferred: "Spark database creation needs agent metadata validation first" }, db2: { database: "schema" }, informix: { database: "schema" }, neo4j: { deferred: "database creation depends on edition/admin privileges" }, diff --git a/apps/desktop/src/lib/database/databaseObjectCapabilities.ts b/apps/desktop/src/lib/database/databaseObjectCapabilities.ts index 51b5beca8f..381a4cab8c 100644 --- a/apps/desktop/src/lib/database/databaseObjectCapabilities.ts +++ b/apps/desktop/src/lib/database/databaseObjectCapabilities.ts @@ -40,6 +40,7 @@ const DATABASE_TYPE_OBJECTS = new Map([ ["doris", TABLE_VIEW_OBJECTS], ["starrocks", TABLE_VIEW_OBJECTS], ["hive", TABLE_VIEW_OBJECTS], + ["spark", TABLE_VIEW_OBJECTS], ["trino", TABLE_VIEW_OBJECTS], ["prestosql", TABLE_VIEW_OBJECTS], ["cassandra", TABLE_VIEW_OBJECTS], diff --git a/apps/desktop/src/lib/database/databasePropertyEditing.ts b/apps/desktop/src/lib/database/databasePropertyEditing.ts index f7f1947e7c..9012d3d832 100644 --- a/apps/desktop/src/lib/database/databasePropertyEditing.ts +++ b/apps/desktop/src/lib/database/databasePropertyEditing.ts @@ -61,6 +61,7 @@ export const DATABASE_PROPERTY_EDITING_MATRIX = { trino: { deferred: "catalog properties are connector-managed" }, prestosql: { deferred: "catalog properties are connector-managed" }, hive: { deferred: "database properties need agent metadata validation first" }, + spark: { deferred: "database properties need agent metadata validation first" }, db2: { deferred: "schema properties need product-specific handling" }, informix: { deferred: "schema properties need product-specific handling" }, neo4j: { deferred: "database properties depend on edition/admin privileges" }, diff --git a/apps/desktop/src/lib/table/tableSelectSql.ts b/apps/desktop/src/lib/table/tableSelectSql.ts index 690312c7ae..54bfa78275 100644 --- a/apps/desktop/src/lib/table/tableSelectSql.ts +++ b/apps/desktop/src/lib/table/tableSelectSql.ts @@ -23,7 +23,7 @@ export function quoteTableIdentifier(databaseType: DatabaseType | undefined, nam // JDBC connections use the driver-reported identifier quote string // (DatabaseMetaData.getIdentifierQuoteString()) — pass through unquoted. if (databaseType === "jdbc") return name; - if (databaseType === "mysql" || databaseType === "clickhouse" || databaseType === "hive" || databaseType === "databend" || databaseType === "tdengine" || databaseType === "access") return `\`${name.replace(/`/g, "``")}\``; + if (databaseType === "mysql" || databaseType === "clickhouse" || databaseType === "hive" || databaseType === "spark" || databaseType === "databend" || databaseType === "tdengine" || databaseType === "access") return `\`${name.replace(/`/g, "``")}\``; if (databaseType === "informix" && /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)) return name; if (databaseType === "neo4j") return quoteCypherIdentifier(name); if (databaseType === "sqlserver") return `[${name.replace(/\]/g, "]]")}]`; diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 83beb1a8e5..2d710c99df 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -787,6 +787,7 @@ export const useConnectionStore = defineStore("connection", () => { trino: "Trino", prestosql: "PrestoSQL", hive: "Hive", + spark: "Apache Spark", db2: "DB2", informix: "Informix", neo4j: "Neo4j", diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index b12fc0eed8..cddc9c2d9b 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -44,6 +44,7 @@ export type DatabaseType = | "trino" | "prestosql" | "hive" + | "spark" | "db2" | "informix" | "neo4j" diff --git a/crates/dbx-core/assets/database-drivers.manifest.json b/crates/dbx-core/assets/database-drivers.manifest.json index 20c0d6336c..2d757b5308 100644 --- a/crates/dbx-core/assets/database-drivers.manifest.json +++ b/crates/dbx-core/assets/database-drivers.manifest.json @@ -1820,6 +1820,36 @@ "userAdmin": false, "driverManagement": false } + }, + { + "dbType": "spark", + "label": "Apache Spark", + "runtimeMode": "agent", + "mcpMode": "bridge", + "agentKey": "spark", + "singleConnectionPool": false, + "metadataConnectionScoped": false, + "skipTcpProbe": true, + "defaultPort": 10015, + "supportLevel": "operate", + "capabilities": { + "queryExecution": true, + "metadataBrowse": true, + "objectBrowser": true, + "objectSource": false, + "schemaSearch": true, + "diagram": false, + "tableDataEdit": true, + "tableStructureEdit": false, + "tableImport": false, + "dataTransfer": true, + "sqlFileExecution": true, + "databaseCreate": false, + "fieldLineage": false, + "sqlExplain": false, + "userAdmin": false, + "driverManagement": true + } } ] } diff --git a/crates/dbx-core/src/agent_catalog.rs b/crates/dbx-core/src/agent_catalog.rs index 1528c96dd3..cfe9e7e768 100644 --- a/crates/dbx-core/src/agent_catalog.rs +++ b/crates/dbx-core/src/agent_catalog.rs @@ -173,6 +173,13 @@ const AGENT_CATALOG: &[AgentCatalogEntry] = &[ store_visible: true, profiles: &[], }, + AgentCatalogEntry { + db_type: DatabaseType::Spark, + key: "spark", + label: "Apache Spark", + store_visible: true, + profiles: &[], + }, AgentCatalogEntry { db_type: DatabaseType::Db2, key: "db2", label: "IBM DB2", store_visible: true, profiles: &[] }, AgentCatalogEntry { db_type: DatabaseType::Informix, diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index 886aab8f12..96f2a31338 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -143,6 +143,7 @@ macro_rules! agent_connection_pool_database_type { | DatabaseType::Snowflake | DatabaseType::Trino | DatabaseType::Hive + | DatabaseType::Spark | DatabaseType::Db2 | DatabaseType::Informix | DatabaseType::Neo4j diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 5b458f6529..b2b5a33970 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -337,6 +337,7 @@ pub enum DatabaseType { #[serde(rename = "prestosql")] PrestoSql, Hive, + Spark, #[serde(rename = "db2")] Db2, Informix, @@ -845,6 +846,7 @@ impl ConnectionConfig { DatabaseType::Trino => format!("trino://{host}:{port}{db_part}"), DatabaseType::PrestoSql => format!("prestosql://{host}:{port}{db_part}"), DatabaseType::Hive => format!("hive://{host}:{port}{db_part}"), + DatabaseType::Spark => format!("spark://{host}:{port}{db_part}"), DatabaseType::Db2 => format!("db2://{host}:{port}{db_part}"), DatabaseType::Informix => format!("informix://{host}:{port}{db_part}"), DatabaseType::Neo4j => format!("neo4j://{host}:{port}{db_part}"), @@ -1034,6 +1036,9 @@ impl ConnectionConfig { DatabaseType::Hive => { format!("hive://{}:{}@{host}:{port}{db_part}", username, password) } + DatabaseType::Spark => { + format!("spark://{}:{}@{host}:{port}{db_part}", username, password) + } DatabaseType::Db2 => { format!("db2://{}:{}@{host}:{port}{db_part}", username, password) } diff --git a/crates/dbx-core/src/sql_dialect/capabilities.rs b/crates/dbx-core/src/sql_dialect/capabilities.rs index 228501bb1d..cf4bd26626 100644 --- a/crates/dbx-core/src/sql_dialect/capabilities.rs +++ b/crates/dbx-core/src/sql_dialect/capabilities.rs @@ -52,6 +52,7 @@ pub fn is_schema_aware(database_type: DatabaseType) -> bool { | DatabaseType::Trino | DatabaseType::PrestoSql | DatabaseType::Hive + | DatabaseType::Spark | DatabaseType::Db2 | DatabaseType::Tdengine | DatabaseType::Xugu diff --git a/crates/dbx-core/src/sql_dialect/identifiers.rs b/crates/dbx-core/src/sql_dialect/identifiers.rs index 52fde76c9d..a00944aad7 100644 --- a/crates/dbx-core/src/sql_dialect/identifiers.rs +++ b/crates/dbx-core/src/sql_dialect/identifiers.rs @@ -86,6 +86,7 @@ pub fn quote_table_identifier(database_type: Option, name: &str) - | DatabaseType::StarRocks | DatabaseType::ManticoreSearch | DatabaseType::Hive + | DatabaseType::Spark | DatabaseType::Databend | DatabaseType::Tdengine | DatabaseType::Access @@ -119,6 +120,7 @@ pub(crate) fn quote_transfer_identifier(name: &str, database_type: &DatabaseType | DatabaseType::Doris | DatabaseType::StarRocks | DatabaseType::Hive + | DatabaseType::Spark | DatabaseType::Questdb => format!("`{}`", name.replace('`', "``")), DatabaseType::SqlServer => format!("[{}]", name.replace(']', "]]")), _ => format!("\"{}\"", name.replace('\"', "\"\"")), diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 7b1e441714..ab88b1650e 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -68,7 +68,7 @@ function formatRedisCommandToolResult(result: RedisCommandResult) { } export const DBX_CONNECTION_TYPE_DESCRIPTION = - "Database type: postgres, mysql, sqlite, rqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, milvus, qdrant, weaviate, chromadb, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, questdb, gbase, h2, snowflake, trino, prestosql, hive, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, oscar, tdengine, iotdb, xugu, zookeeper, jdbc, access, mq"; + "Database type: postgres, mysql, sqlite, rqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, milvus, qdrant, weaviate, chromadb, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, questdb, gbase, h2, snowflake, trino, prestosql, hive, spark, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, oscar, tdengine, iotdb, xugu, zookeeper, jdbc, access, mq"; const FILE_CAPABLE_CONNECTION_TYPES = new Set(["sqlite", "duckdb", "access", "h2"]); interface McpScope { diff --git a/packages/node-core/src/diagnostics.ts b/packages/node-core/src/diagnostics.ts index 1c11ef6285..0cc620cc45 100644 --- a/packages/node-core/src/diagnostics.ts +++ b/packages/node-core/src/diagnostics.ts @@ -49,6 +49,7 @@ export const BRIDGE_REQUIRED_TYPES = [ "trino", "prestosql", "hive", + "spark", "db2", "informix", "iris",