feat(argo): add argodb connection type with routine browsing

This commit is contained in:
wangfengxiang
2026-09-01 00:21:10 +08:00
committed by GitHub
parent 5eb071bc22
commit 2f71a1a8aa
34 changed files with 492 additions and 48 deletions
+159 -9
View File
@@ -438,21 +438,129 @@ func showViewsUnsupported(err error) bool {
}
func (server *server) listObjects(schema string, constraints metadataListConstraints) ([]objectInfo, error) {
if !acceptsHiveTable(constraints.ObjectTypes) {
if !acceptsHiveTable(constraints.ObjectTypes) && !(server.supportsRoutines() && acceptsHiveRoutine(constraints.ObjectTypes)) {
return []objectInfo{}, nil
}
tables, err := server.listTables(schema, constraints)
if err != nil {
return nil, err
}
schema = firstNonEmpty(schema, server.config.Database)
values := make([]objectInfo, 0, len(tables))
for _, table := range tables {
values = append(values, objectInfo{Name: table.Name, ObjectType: table.TableType, Schema: schema, Comment: table.Comment})
values := make([]objectInfo, 0)
if acceptsHiveTable(constraints.ObjectTypes) {
tables, err := server.listTables(schema, constraints)
if err != nil {
return nil, err
}
for _, table := range tables {
values = append(values, objectInfo{Name: table.Name, ObjectType: table.TableType, Schema: schema, Comment: table.Comment})
}
}
if server.supportsRoutines() && acceptsRoutineType(constraints.ObjectTypes, "PROCEDURE") {
procedures, err := server.listRoutines(schema, constraints, "PROCEDURE")
if err != nil {
return nil, err
}
values = append(values, procedures...)
}
if server.supportsRoutines() && acceptsRoutineType(constraints.ObjectTypes, "FUNCTION") {
functions, err := server.listRoutines(schema, constraints, "FUNCTION")
if err != nil {
return nil, err
}
values = append(values, functions...)
}
sort.Slice(values, func(first, second int) bool { return values[first].Name < values[second].Name })
return applyMetadataWindow(values, constraints.Offset, constraints.Limit), nil
}
// supportsRoutines reports whether the connected server is known to expose the
// Hive procedure / function catalog views (system.procedures_v /
// system.functions_v). ArgoDB / Inceptor / Transwarp forks ship these views;
// vanilla Apache Hive does not, so routine listing is gated on the connection's
// database_type to avoid firing unsupported queries against plain Hive.
func (server *server) supportsRoutines() bool {
switch strings.ToLower(strings.TrimSpace(server.params.DatabaseType)) {
case "argo", "inceptor", "inceptor2", "transwarp":
return true
default:
return false
}
}
// listRoutines queries the server's procedure / function catalog views when the
// driver supports them. Hive and most forks (ArgoDB, Inceptor, Transwarp) expose
// stored procedures / functions through the system.procedures_v / system.functions_v
// views, with columns (procedure_name | function_name, database_name, full_text, ...).
// The full_text column carries the routine source used by getObjectSource.
//
// The query is best-effort: when the view is missing or the server rejects it
// (older Hive without procedure support), the call returns an empty slice and
// nil error so the caller can fall back to listing tables.
func (server *server) listRoutines(schema string, constraints metadataListConstraints, routineType string) ([]objectInfo, error) {
nameColumn := "procedure_name"
viewName := "system.procedures_v"
if strings.EqualFold(routineType, "FUNCTION") {
nameColumn = "function_name"
viewName = "system.functions_v"
}
// Routine listings are pinned to the active schema (or the connection's default
// database when the caller passes an empty schema). system.procedures_v /
// system.functions_v only contain rows for one specific database per query;
// there is no wildcard form. Falling back to the connection default keeps
// callers like the sidebar schema tree happy when they pass "" as the schema.
targetSchema := firstNonEmpty(schema, server.config.Database)
likePattern := buildRoutineLikePattern(constraints.Filter)
// database_name is a string column, so the schema filter must be a single-quoted
// literal — not a backtick-quoted identifier. ArgoDB/Inceptor reject lower(`ods`)
// against system.procedures_v with an ERROR_STATUS, while lower('ods') works.
schemaLiteral := "'" + strings.ReplaceAll(targetSchema, "'", "''") + "'"
sql := "SELECT " + nameColumn + " FROM " + viewName +
" WHERE lower(database_name) = lower(" + schemaLiteral + ")" +
" AND lower(" + nameColumn + ") LIKE " + likePattern +
" ORDER BY " + nameColumn
result, err := server.executeQuery(queryOptions{SQL: sql, MaxRows: metadataQueryLimit})
if err != nil {
// View missing or not supported — silently return empty list.
return []objectInfo{}, nil
}
values := make([]objectInfo, 0, len(result.Rows))
for _, row := range result.Rows {
name := rowString(row, 0)
if name == "" {
continue
}
values = append(values, objectInfo{Name: name, ObjectType: strings.ToUpper(routineType), Schema: schema, Comment: nil})
}
return values, nil
}
// buildRoutineLikePattern turns a user-supplied filter into a Hive-safe LIKE
// literal: wraps with %, escapes \, %, _ (the LIKE metacharacters), and quotes
// the whole literal so it can be concatenated directly into SQL.
func buildRoutineLikePattern(filter string) string {
escaped := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(filter)
return "'%" + escaped + "%'"
}
// acceptsHiveRoutine reports whether the requested object types include any
// routine (PROCEDURE / FUNCTION) that hive-go needs to surface through listObjects.
func acceptsHiveRoutine(objectTypes []string) bool {
for _, objectType := range objectTypes {
if strings.EqualFold(objectType, "PROCEDURE") || strings.EqualFold(objectType, "FUNCTION") {
return true
}
}
return false
}
// acceptsRoutineType reports whether the requested object types include the
// given routine kind (case-insensitive).
func acceptsRoutineType(objectTypes []string, routineType string) bool {
for _, objectType := range objectTypes {
if strings.EqualFold(objectType, routineType) {
return true
}
}
return false
}
func (server *server) getColumns(schema, table string) ([]columnInfo, error) {
if strings.TrimSpace(table) == "" {
return nil, errors.New("table is required")
@@ -585,7 +693,18 @@ func (server *server) getTableDDL(schema, table string) (string, error) {
}
func (server *server) getObjectSource(schema, name, objectType string) (objectSource, error) {
source, err := server.getTableDDL(schema, name)
schema = firstNonEmpty(schema, server.config.Database)
var source string
var err error
switch strings.ToUpper(objectType) {
case "PROCEDURE", "FUNCTION":
if !server.supportsRoutines() {
return objectSource{}, fmt.Errorf("routine source is not supported for %s connections", server.params.DatabaseType)
}
source, err = server.getRoutineSource(schema, name, strings.ToUpper(objectType))
default:
source, err = server.getTableDDL(schema, name)
}
if err != nil {
return objectSource{}, err
}
@@ -597,6 +716,37 @@ func (server *server) getObjectSource(schema, name, objectType string) (objectSo
}, nil
}
// getRoutineSource fetches a procedure or function's full source from the
// server's system.procedures_v / system.functions_v view. Returns an empty
// string when the view is missing or the routine is not found, so callers can
// fall back to other sources. full_text may span multiple rows (the underlying
// query driver splits long strings), so we join them like getTableDDL does.
func (server *server) getRoutineSource(schema, name, routineType string) (string, error) {
nameColumn := "procedure_name"
viewName := "system.procedures_v"
if strings.EqualFold(routineType, "FUNCTION") {
nameColumn = "function_name"
viewName = "system.functions_v"
}
sql := "SELECT full_text FROM " + viewName +
" WHERE lower(database_name) = lower('" + strings.ReplaceAll(schema, "'", "''") + "')" +
" AND " + nameColumn + " = '" + strings.ReplaceAll(name, "'", "''") + "'"
result, err := server.executeQuery(queryOptions{SQL: sql, MaxRows: metadataQueryLimit})
if err != nil {
return "", nil
}
lines := make([]string, 0, len(result.Rows))
for _, row := range result.Rows {
if line := firstRowValue(row); line != "" {
lines = append(lines, line)
}
}
if len(lines) == 0 {
return "", nil
}
return strings.Join(lines, "\n") + "\n", nil
}
func (server *server) getExplainInfo(sqlText string) (string, error) {
sqlText = trimStatementSQL(sqlText)
if sqlText == "" {
+157
View File
@@ -479,3 +479,160 @@ func TestListViewsReturnsFallbackErrorWhenShowViewsIsUnsupported(t *testing.T) {
func metadataResult(columns []string, rows ...[]driver.Value) gohive.MetadataResult {
return gohive.MetadataResult{Columns: columns, Rows: rows}
}
func TestListObjectsIncludesProceduresAndFunctionsFromSystemViews(t *testing.T) {
proceduresQuery := "SELECT procedure_name FROM system.procedures_v WHERE lower(database_name) = lower('ods') AND lower(procedure_name) LIKE '%sp%' ORDER BY procedure_name"
functionsQuery := "SELECT function_name FROM system.functions_v WHERE lower(database_name) = lower('ods') AND lower(function_name) LIKE '%sp%' ORDER BY function_name"
behavior := &scriptedBehavior{
query: func(_ context.Context, query string) (driver.Rows, error) {
switch query {
case proceduresQuery:
return newScriptedRows(context.Background(), []string{"procedure_name"}, []string{"STRING"}, [][]driver.Value{
{"sp_daily_etl"}, {"sp_hourly_agg"},
}), nil
case functionsQuery:
return newScriptedRows(context.Background(), []string{"function_name"}, []string{"STRING"}, [][]driver.Value{
{"fn_clean"},
}), nil
default:
return nil, errors.New("unexpected query: " + query)
}
},
}
server := newScriptedServer(t, behavior)
server.params.DatabaseType = "argo"
defer server.disconnect()
values, err := server.listObjects("ods", metadataListConstraints{
ObjectTypes: []string{"PROCEDURE", "FUNCTION"},
Filter: "sp", // "sp" overlaps the procedure filter; both lists get "%sp%" applied
})
if err != nil {
t.Fatalf("listObjects: %v", err)
}
t.Logf("values: %+v", values)
// listObjects applies the same filter pattern to procedures and functions;
// we passed "sp" so procedures match and functions don't (because the function
// list returns rows regardless of filter — that is, listObjects calls each
// listRoutines call with the same Filter). Verify both queries ran and
// the procedure name is present.
queries, _, _, _ := behavior.snapshot()
t.Logf("queries count: %d", len(queries))
for i, q := range queries {
t.Logf("query[%d]: %q", i, q)
}
if len(queries) != 2 {
t.Fatalf("expected exactly 2 routine queries, got %d: %v", len(queries), queries)
}
if values == nil {
t.Fatal("expected non-nil object list")
}
names := make([]string, len(values))
for i, v := range values {
names[i] = v.Name + ":" + v.ObjectType
}
joined := strings.Join(names, ",")
if !strings.Contains(joined, "sp_daily_etl:PROCEDURE") {
t.Fatalf("expected procedures in result, got %v", values)
}
if !strings.Contains(joined, "fn_clean:FUNCTION") {
t.Fatalf("expected functions in result, got %v", values)
}
}
func TestGetObjectSourceRoutesProceduresToSystemProceduresView(t *testing.T) {
expectedSQL := "SELECT full_text FROM system.procedures_v WHERE lower(database_name) = lower('ods') AND procedure_name = 'sp_daily_etl'"
behavior := &scriptedBehavior{
query: func(_ context.Context, query string) (driver.Rows, error) {
if query != expectedSQL {
return nil, errors.New("unexpected query: " + query)
}
return newScriptedRows(context.Background(), []string{"full_text"}, []string{"STRING"}, [][]driver.Value{
{"-- daily ETL pipeline"},
{"INSERT OVERWRITE TABLE ods.daily_summary SELECT * FROM staging.events"},
}), nil
},
}
server := newScriptedServer(t, behavior)
server.params.DatabaseType = "argo"
defer server.disconnect()
result, _, err := server.dispatch("get_object_source", map[string]json.RawMessage{
"schema": json.RawMessage(`"ods"`),
"name": json.RawMessage(`"sp_daily_etl"`),
"object_type": json.RawMessage(`"PROCEDURE"`),
})
if err != nil {
t.Fatal(err)
}
source, ok := result.(objectSource)
if !ok {
t.Fatalf("get_object_source returned %T instead of objectSource", result)
}
if source.Name != "sp_daily_etl" || source.ObjectType != "PROCEDURE" {
t.Fatalf("unexpected object source metadata: %#v", source)
}
expected := "-- daily ETL pipeline\nINSERT OVERWRITE TABLE ods.daily_summary SELECT * FROM staging.events\n"
if source.Source != expected {
t.Fatalf("unexpected procedure source: %q", source.Source)
}
}
func TestGetObjectSourceRoutesFunctionsToSystemFunctionsView(t *testing.T) {
expectedSQL := "SELECT full_text FROM system.functions_v WHERE lower(database_name) = lower('ods') AND function_name = 'fn_clean'"
behavior := &scriptedBehavior{
query: func(_ context.Context, query string) (driver.Rows, error) {
if query != expectedSQL {
return nil, errors.New("unexpected query: " + query)
}
return newScriptedRows(context.Background(), []string{"full_text"}, []string{"STRING"}, [][]driver.Value{
{"-- cleanup helper"},
}), nil
},
}
server := newScriptedServer(t, behavior)
server.params.DatabaseType = "argo"
defer server.disconnect()
result, _, err := server.dispatch("get_object_source", map[string]json.RawMessage{
"schema": json.RawMessage(`"ods"`),
"name": json.RawMessage(`"fn_clean"`),
"object_type": json.RawMessage(`"FUNCTION"`),
})
if err != nil {
t.Fatal(err)
}
source, ok := result.(objectSource)
if !ok {
t.Fatalf("get_object_source returned %T instead of objectSource", result)
}
if source.Source != "-- cleanup helper\n" {
t.Fatalf("unexpected function source: %q", source.Source)
}
}
func TestListObjectsDoesNotQueryRoutinesForVanillaHive(t *testing.T) {
behavior := &scriptedBehavior{
query: func(ctx context.Context, query string) (driver.Rows, error) {
if strings.Contains(strings.ToUpper(query), "PROCEDURES_V") || strings.Contains(strings.ToUpper(query), "FUNCTIONS_V") {
t.Fatalf("vanilla Hive must not query routine views, got: %s", query)
}
return newScriptedRows(ctx, []string{"tab_name"}, []string{"STRING"}, [][]driver.Value{{"events"}}), nil
},
}
server := newScriptedServer(t, behavior)
server.params.DatabaseType = "hive"
defer server.disconnect()
values, err := server.listObjects("ods", metadataListConstraints{
ObjectTypes: []string{"PROCEDURE", "FUNCTION"},
})
if err != nil {
t.Fatalf("listObjects: %v", err)
}
for _, v := range values {
if v.ObjectType == "PROCEDURE" || v.ObjectType == "FUNCTION" {
t.Fatalf("vanilla Hive should not surface routines, got: %+v", v)
}
}
}
@@ -2428,7 +2428,7 @@ function applyProfile(val: string, preserveConnectionFields = false) {
form.value.connection_string = undefined;
form.value.url_params = "";
}
resetHiveKerberosFields(profile.type === "hive" || profile.type === "kyuubi" || profile.type === "impala" ? form.value : undefined);
resetHiveKerberosFields(profile.type === "hive" || profile.type === "argo" || profile.type === "kyuubi" || profile.type === "impala" ? form.value : undefined);
}
if (profile.type === "meilisearch") {
syncMeilisearchHostInput(form.value);
@@ -2566,7 +2566,7 @@ watch(
resetVictoriaMetricsFields();
}
resetElasticsearchProxyFields(config.db_type === "elasticsearch" ? config.external_config : undefined);
resetHiveKerberosFields(config.db_type === "hive" || config.db_type === "kyuubi" || config.db_type === "impala" ? config : undefined);
resetHiveKerberosFields(config.db_type === "hive" || config.db_type === "argo" || config.db_type === "kyuubi" || config.db_type === "impala" ? config : undefined);
resetDamengJvmOptions(config.db_type === "dameng" ? config : undefined);
h2ConnectionMode.value = h2ConnectionModeForConfig(config);
customColorInput.value = config.color || "";
@@ -3790,7 +3790,7 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
config.ssl = !!config.ssl || damengSsl.enabled;
config.url_params = applyDamengSslUrlParams(config.url_params, config.ssl, damengSsl.sslFilesPath, damengSsl.sslKeystorePassword, damengSsl.sslProtocol);
}
if (config.db_type === "hive" || config.db_type === "kyuubi" || config.db_type === "impala") {
if (config.db_type === "hive" || config.db_type === "argo" || config.db_type === "kyuubi" || config.db_type === "impala") {
if (hiveAuthMode.value === "kerberos" && !hivePrincipal.value.trim()) {
throw new Error(t("connection.hiveKerberosPrincipalRequired"));
}
@@ -58,6 +58,7 @@ const assetIcons: Record<string, string> = {
presto: "presto",
prestosql: "presto",
hive: "hive",
argo: "hive",
kyuubi: "kyuubi.png",
impala: "impala",
hbase: "hbase",
@@ -33,7 +33,7 @@ export function hasInstalledAgentVersion(drivers: readonly AgentDriverInstallSta
export function agentDriverInstallKey(dbType: DatabaseType | undefined, driverProfile?: string, context?: AgentDriverInstallContext): string | undefined {
if (dbType === "sqlite") return context?.ssh ? "sqlite-worker" : undefined;
if (dbType === "kyuubi" || dbType === "impala") return "hive";
if (dbType === "kyuubi" || dbType === "impala" || dbType === "argo") return "hive";
if (dbType === "oracle") return "oracle";
if (dbType === "h2") return "h2";
if (dbType === "mongodb") return "mongodb";
@@ -34,6 +34,7 @@ const DRIVER_STARTUP_FLOOR_TYPES = new Set<DatabaseType>([
"hive",
"kyuubi",
"impala",
"argo",
"spark",
"db2",
"informix",
+1 -1
View File
@@ -168,7 +168,7 @@ export function buildHiveTablePropertiesSql(options: HiveTablePropertiesSqlOptio
export function normalizeDataGridSaveError(databaseType: DatabaseType | undefined, error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
if (databaseType === "hive" && /Attempt to do update or delete|Error 10294/i.test(message)) {
if ((databaseType === "hive" || databaseType === "argo") && /Attempt to do update or delete|Error 10294/i.test(message)) {
return "Hive UPDATE/DELETE are not enabled for this table or server. Add rows with INSERT, or enable ACID transactional tables in Hive before editing/deleting existing rows.";
}
return message;
@@ -4,7 +4,7 @@ import { manifestDatabaseTypes } from "@/lib/database/databaseDriverManifest";
import { supportsTransfer } from "@/lib/database/databaseFeatureSupport";
import type { DatabaseType } from "@/types/database";
const TABLE_ONLY_TRANSFER_DATABASES: DatabaseType[] = ["sqlite", "rqlite", "turso", "cloudflare-d1", "duckdb", "clickhouse", "mongodb", "goldendb", "questdb", "hive", "kyuubi", "impala", "spark"];
const TABLE_ONLY_TRANSFER_DATABASES: DatabaseType[] = ["sqlite", "rqlite", "turso", "cloudflare-d1", "duckdb", "clickhouse", "mongodb", "goldendb", "questdb", "hive", "argo", "kyuubi", "impala", "spark"];
describe("transferObjectKinds", () => {
it("groups databases into transfer families", () => {
@@ -68,6 +68,7 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = {
trino: { database: "schema" },
prestosql: { database: "schema" },
hive: { deferred: "Hive database creation needs agent metadata validation first" },
argo: { deferred: "ArgoDB database creation needs agent metadata validation first" },
kyuubi: { deferred: "Kyuubi database creation needs dedicated metadata validation first" },
impala: { deferred: "Impala database creation needs dedicated metadata validation first" },
spark: { deferred: "Spark database creation needs agent metadata validation first" },
@@ -71,6 +71,9 @@ const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
["starrocks", TABLE_VIEW_MV_OBJECTS],
// Inceptor/Hive routines can be listed via JDBC plugin fallbacks (system.procedures_v/functions_v).
["hive", ROUTINE_OBJECTS],
// ArgoDB (Transwarp) shares the Hive agent; its catalog views
// (system.procedures_v / system.functions_v) expose routines natively.
["argo", ROUTINE_OBJECTS],
["kyuubi", TABLE_VIEW_OBJECTS],
["impala", TABLE_VIEW_OBJECTS],
["spark", TABLE_VIEW_OBJECTS],
@@ -68,6 +68,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" },
argo: { deferred: "database properties need agent metadata validation first" },
kyuubi: { deferred: "database properties need Kyuubi-specific validation first" },
impala: { deferred: "database properties need Impala-specific validation first" },
spark: { deferred: "database properties need agent metadata validation first" },
@@ -123,6 +123,16 @@ const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<Databa
transaction: false,
},
},
argo: {
tableData: {
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: true,
transaction: false,
},
},
jdbc: {
tableData: {
insert: false,
+1 -1
View File
@@ -15,7 +15,7 @@ const GAUSSDB_COUNT_QUERY_DOP_KEY = "gaussdbCountQueryDop";
export const GAUSSDB_M_JDBC_DRIVER_PROFILE = "gaussdb-m";
export const GAUSSDB_M_JDBC_DRIVER_CLASS = "com.huawei.gaussdb.jdbc.Driver";
const DATABASE_AS_EXECUTION_SCHEMA_TYPES = new Set<DatabaseType>(["hive", "kyuubi", "impala", "spark"]);
const DATABASE_AS_EXECUTION_SCHEMA_TYPES = new Set<DatabaseType>(["hive", "kyuubi", "impala", "argo", "spark"]);
const CONNECTION_ROOT_SCHEMA_TYPES = new Set<DatabaseType>(["oracle", "dameng", "oceanbase-oracle"]);
const JDBC_DIALECT_MATCHERS: Array<{ type: DatabaseType; patterns: RegExp[] }> = [
@@ -3,7 +3,7 @@ import { qualifiedTableName, quoteTableIdentifier } from "@/lib/table/tableSelec
import { requiresMysqlIdentifierQuote, requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier.ts";
/** 智能引号使用反引号的方言族。 */
const SMART_QUOTE_BACKTICK_TYPES = new Set<DatabaseType>(["mysql", "clickhouse", "hive", "kyuubi", "impala", "spark", "databricks", "databend", "tdengine", "access", "doris", "starrocks", "goldendb"]);
const SMART_QUOTE_BACKTICK_TYPES = new Set<DatabaseType>(["mysql", "clickhouse", "hive", "argo", "kyuubi", "impala", "spark", "databricks", "databend", "tdengine", "access", "doris", "starrocks", "goldendb"]);
/** 智能引号使用双引号的方言族(SQL Server 方括号除外)。 */
const SMART_QUOTE_DOUBLE_TYPES = new Set<DatabaseType>(["postgres", "gaussdb", "opengauss"]);
/** SQL Server 族智能引号使用方括号。 */
+3 -3
View File
@@ -1308,7 +1308,7 @@ type SqlCompletionApplyDialect = "mysql" | "postgres" | "sqlserver" | "oracle" |
// QueryEditor may use another dialect as a CodeMirror syntax fallback.
// Completion apply text must still follow the connected database's identifier
// folding and quoting rules.
const MYSQL_LIKE_IDENTIFIER_DATABASES = new Set<DatabaseType>(["mysql", "clickhouse", "hive", "kyuubi", "impala", "spark", "databend", "tdengine", "access", "doris", "starrocks"]);
const MYSQL_LIKE_IDENTIFIER_DATABASES = new Set<DatabaseType>(["mysql", "clickhouse", "hive", "argo", "kyuubi", "impala", "spark", "databend", "tdengine", "access", "doris", "starrocks"]);
const POSTGRES_LIKE_IDENTIFIER_DATABASES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb", "kingbase", "highgo", "uxdb", "vastbase", "kwdb", "opengauss"]);
const ORACLE_COMPAT_IDENTIFIER_DATABASES = new Set<DatabaseType>(["oracle", "oceanbase-oracle", "yashandb", "oscar", "xugu"]);
const UPPER_FOLDING_IDENTIFIER_DATABASES = new Set<DatabaseType>(["dameng", "db2"]);
@@ -2867,7 +2867,7 @@ function lastTopLevelKeywordIndex(sql: string, keyword: string): number {
// unquoted token as a table reference there is a false-positive risk. Everything else defaults
// to Unicode support (MySQL/Postgres/SQL Server/SQLite/DuckDB and the many Chinese-vendor
// engines this product supports legitimately use non-ASCII unquoted identifiers).
const ASCII_ONLY_UNQUOTED_IDENTIFIER_DATABASES = new Set<DatabaseType>(["clickhouse", "snowflake", "bigquery", "hive", "spark", "trino", "prestosql", "impala", "db2", "teradata"]);
const ASCII_ONLY_UNQUOTED_IDENTIFIER_DATABASES = new Set<DatabaseType>(["clickhouse", "snowflake", "bigquery", "hive", "argo", "spark", "trino", "prestosql", "impala", "db2", "teradata"]);
// Dialects where a bare "--" needs trailing whitespace/EOL to start a comment, since MySQL
// reserves unspaced "--" for double-negation (e.g. `SELECT 1--1`). Scoped to mysql itself plus
@@ -2887,7 +2887,7 @@ const MYSQL_DASH_COMMENT_DIALECTS = new Set<DatabaseType>(["mysql", "doris", "st
// why that's unsafe (a trailing backslash before a closing quote in a dialect that doesn't escape
// it, e.g. a Postgres Windows-path string literal, would misread the real closing quote as
// escaped and swallow the rest of the query).
const BACKSLASH_ESCAPE_STRING_DIALECTS = new Set<DatabaseType>(["mysql", "doris", "starrocks", "hive", "impala", "spark"]);
const BACKSLASH_ESCAPE_STRING_DIALECTS = new Set<DatabaseType>(["mysql", "doris", "starrocks", "hive", "argo", "impala", "spark"]);
// Table/schema/db unquoted-identifier continue class needs @ and # in addition to what
// SQL_IDENTIFIER_CONTINUE_CHAR covers, so splice its inner class body into a locally-built class
+1 -1
View File
@@ -272,7 +272,7 @@ export function importDataTypeForDatabase(inferredType: ImportInferredType, data
if (databaseType === "sqlserver") return "NVARCHAR(MAX)";
if (databaseType === "oracle" || databaseType === "oceanbase-oracle" || databaseType === "dameng") return "CLOB";
if (databaseType === "clickhouse") return "String";
if (["hive", "trino", "prestosql", "databricks"].includes(databaseType || "")) return "STRING";
if (["hive", "argo", "trino", "prestosql", "databricks"].includes(databaseType || "")) return "STRING";
return "TEXT";
}
}
@@ -69,6 +69,7 @@ export function quoteTableIdentifier(databaseType: DatabaseType | undefined, nam
databaseType === "mysql" ||
databaseType === "clickhouse" ||
databaseType === "hive" ||
databaseType === "argo" ||
databaseType === "kyuubi" ||
databaseType === "impala" ||
databaseType === "spark" ||
@@ -86,6 +86,7 @@ export const CONNECTION_PROFILES = {
prestosql: { type: "prestosql", port: 8080, user: "", label: "PrestoSQL", icon: "presto" },
hive: { type: "hive", port: 10000, user: "", label: "Apache Hive", icon: "hive" },
kyuubi: { type: "kyuubi", port: 10009, user: "", label: "Apache Kyuubi", icon: "kyuubi", urlParams: "auth=NONE" },
argo: { type: "argo", port: 10000, user: "", label: "ArgoDB (Transwarp)", icon: "hive" },
impala: { type: "impala", port: 21050, user: "", label: "Apache Impala", icon: "impala", urlParams: "auth=noSasl" },
spark: { type: "spark", port: 10015, user: "", label: "Apache Spark", icon: "spark" },
db2: { type: "db2", port: 50000, user: "db2inst1", label: "IBM DB2", icon: "db2" },
@@ -189,6 +190,7 @@ export const CONNECTION_PROFILE_ICONS = {
prestosql: "prestosql",
hive: "hive",
kyuubi: "kyuubi",
argo: "hive",
impala: "impala",
spark: "spark",
db2: "db2",
@@ -287,6 +289,7 @@ export const CONNECTION_PICKER_OPTIONS = [
{ value: "prestosql", label: "PrestoSQL", category: "analytics" },
{ value: "hive", label: "Hive", category: "analytics" },
{ value: "kyuubi", label: "Apache Kyuubi", category: "analytics" },
{ value: "argo", label: "ArgoDB", category: "domestic" },
{ value: "impala", label: "Apache Impala", category: "analytics" },
{ value: "spark", label: "Apache Spark", category: "analytics" },
{ value: "db2", label: "DB2", category: "sql" },
@@ -51,6 +51,7 @@ export const DATABASE_TYPES = [
"trino",
"prestosql",
"hive",
"argo",
"kyuubi",
"impala",
"db2",
@@ -1842,6 +1842,41 @@
"driverManagement": true
}
},
{
"dbType": "argo",
"label": "ArgoDB (Transwarp)",
"runtimeMode": "agent",
"mcpMode": "bridge",
"agentKey": "hive",
"driverStoreVisible": false,
"driverStoreOrder": 23,
"singleConnectionPool": false,
"metadataConnectionScoped": false,
"skipTcpProbe": true,
"defaultPort": 10000,
"traits": {
"schemaAware": true
},
"supportLevel": "operate",
"capabilities": {
"queryExecution": true,
"metadataBrowse": true,
"objectBrowser": true,
"objectSource": true,
"schemaSearch": true,
"diagram": false,
"tableDataEdit": true,
"tableStructureEdit": false,
"tableImport": false,
"dataTransfer": true,
"sqlFileExecution": true,
"databaseCreate": false,
"fieldLineage": false,
"sqlExplain": false,
"userAdmin": false,
"driverManagement": true
}
},
{
"dbType": "kyuubi",
"label": "Apache Kyuubi",
+2 -1
View File
@@ -661,7 +661,8 @@ fn append_agent_url_params(base: String, params: Option<&str>) -> String {
}
pub fn hive_uses_zookeeper_discovery(config: &ConnectionConfig) -> bool {
if !matches!(config.db_type, DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala) {
if !matches!(config.db_type, DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala | DatabaseType::Argo)
{
return false;
}
+1
View File
@@ -249,6 +249,7 @@ macro_rules! agent_connection_pool_database_type {
| DatabaseType::Hive
| DatabaseType::Kyuubi
| DatabaseType::Impala
| DatabaseType::Argo
| DatabaseType::Spark
| DatabaseType::Db2
| DatabaseType::Informix
+5
View File
@@ -2052,6 +2052,10 @@ impl AgentDriverClient {
}
if let Some(object_types) = object_types {
params["object_types"] = serde_json::json!(object_types);
// Agent drivers (hive-go, oracle-go, etc.) read the object-type
// filter from a camelCase field; keep both spellings so existing
// agents don't need a coordinated rollout.
params["objectTypes"] = serde_json::json!(object_types);
}
self.call_method_with_timeout(AgentMethod::ListTables, params, timeout_duration).await
}
@@ -2087,6 +2091,7 @@ impl AgentDriverClient {
}
if let Some(object_types) = object_types {
params["object_types"] = serde_json::json!(object_types);
params["objectTypes"] = serde_json::json!(object_types);
}
self.call_method_with_timeout(AgentMethod::ListObjects, params, timeout_duration).await
}
+17 -14
View File
@@ -538,6 +538,7 @@ pub fn build_empty_table_sql(options: TableAdminSqlOptions) -> String {
DatabaseType::Cassandra
| DatabaseType::Hive
| DatabaseType::Kyuubi
| DatabaseType::Argo
| DatabaseType::Kylin
| DatabaseType::Questdb,
) => {
@@ -739,20 +740,22 @@ pub fn build_duplicate_table_structure_sql(options: DuplicateTableStructureSqlOp
);
let target =
qualified_duplicate_target_name(options.database_type, options.schema.as_deref(), &options.target_name);
let structure_sql =
if matches!(options.database_type, Some(DatabaseType::Mysql | DatabaseType::Kyuubi | DatabaseType::Impala)) {
format!("CREATE TABLE {target} LIKE {source};")
} else if options.database_type == Some(DatabaseType::Questdb) {
format!("CREATE TABLE {target} (LIKE {source});")
} else if options.database_type.is_some_and(is_postgres_like_structure_copy) {
format!("CREATE TABLE {target} (LIKE {source} INCLUDING ALL);")
} else if options.database_type == Some(DatabaseType::SqlServer) {
format!("SELECT TOP 0 * INTO {target} FROM {source};")
} else if options.database_type.is_some_and(uses_false_predicate_duplicate_structure) {
format!("CREATE TABLE {target} AS SELECT * FROM {source} WHERE 1=0")
} else {
format!("CREATE TABLE {target} AS SELECT * FROM {source} WHERE 0;")
};
let structure_sql = if matches!(
options.database_type,
Some(DatabaseType::Mysql | DatabaseType::Kyuubi | DatabaseType::Impala | DatabaseType::Argo)
) {
format!("CREATE TABLE {target} LIKE {source};")
} else if options.database_type == Some(DatabaseType::Questdb) {
format!("CREATE TABLE {target} (LIKE {source});")
} else if options.database_type.is_some_and(is_postgres_like_structure_copy) {
format!("CREATE TABLE {target} (LIKE {source} INCLUDING ALL);")
} else if options.database_type == Some(DatabaseType::SqlServer) {
format!("SELECT TOP 0 * INTO {target} FROM {source};")
} else if options.database_type.is_some_and(uses_false_predicate_duplicate_structure) {
format!("CREATE TABLE {target} AS SELECT * FROM {source} WHERE 1=0")
} else {
format!("CREATE TABLE {target} AS SELECT * FROM {source} WHERE 0;")
};
let mut comment_sql = Vec::new();
if let Some(database_type) =
+2 -2
View File
@@ -1089,7 +1089,7 @@ impl ConnectionConfig {
DatabaseType::Snowflake => format!("snowflake://{host}/{db_part}"),
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::Hive | DatabaseType::Argo => format!("hive://{host}:{port}{db_part}"),
DatabaseType::Kyuubi => format!("kyuubi://{host}:{port}{db_part}"),
DatabaseType::Impala => format!("impala://{host}:{port}{db_part}"),
DatabaseType::Spark => format!("spark://{host}:{port}{db_part}"),
@@ -1315,7 +1315,7 @@ impl ConnectionConfig {
DatabaseType::PrestoSql => {
format!("prestosql://{}:{}@{host}:{port}{db_part}", username, password)
}
DatabaseType::Hive => {
DatabaseType::Hive | DatabaseType::Argo => {
format!("hive://{}:{}@{host}:{port}{db_part}", username, password)
}
DatabaseType::Kyuubi => {
@@ -55,6 +55,7 @@ pub fn is_schema_aware(database_type: DatabaseType) -> bool {
| DatabaseType::Hive
| DatabaseType::Kyuubi
| DatabaseType::Impala
| DatabaseType::Argo
| DatabaseType::Spark
| DatabaseType::Db2
| DatabaseType::Informix
@@ -707,9 +707,9 @@ 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 | Spanner | Kylin | Ignite | Ignite3 | Oscar | Tdengine | Iotdb | Databricks
| Jdbc => conservative_ansi(db_type),
DuckDb | Questdb | SapHana | Teradata | Snowflake | Trino | PrestoSql | Hive | Kyuubi | Impala | Argo
| Spark | Db2 | Informix | Bigquery | Spanner | Kylin | Ignite | Ignite3 | Oscar | Tdengine | Iotdb
| Databricks | Jdbc => conservative_ansi(db_type),
// Non-tabular / not applicable for relational CREATE TABLE
Redis | MongoDb | DynamoDb | Elasticsearch | Easysearch | Meilisearch | Qdrant | Milvus | Weaviate
@@ -131,6 +131,7 @@ pub fn quote_table_identifier(database_type: Option<DatabaseType>, name: &str) -
| DatabaseType::Hive
| DatabaseType::Kyuubi
| DatabaseType::Impala
| DatabaseType::Argo
| DatabaseType::Spark
| DatabaseType::Databricks
| DatabaseType::Databend
@@ -388,6 +389,7 @@ pub(crate) fn quote_transfer_identifier(name: &str, database_type: &DatabaseType
| DatabaseType::Hive
| DatabaseType::Kyuubi
| DatabaseType::Impala
| DatabaseType::Argo
| DatabaseType::Spark
| DatabaseType::Questdb => format!("`{}`", name.replace('`', "``")),
DatabaseType::SqlServer => format!("[{}]", name.replace(']', "]]")),
@@ -610,7 +610,13 @@ pub(super) fn build_select_columns(
}
if !matches!(
database_type,
Some(DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala | DatabaseType::InfluxDb)
Some(
DatabaseType::Hive
| DatabaseType::Kyuubi
| DatabaseType::Impala
| DatabaseType::Argo
| DatabaseType::InfluxDb
)
) {
return "*".to_string();
}
+5
View File
@@ -35,6 +35,11 @@ fn quotes_identifiers_by_database_type() {
assert_eq!(quote_table_identifier(Some(DatabaseType::Jdbc), "user name"), "user name");
assert_eq!(quote_table_identifier(Some(DatabaseType::Iotdb), "root.test.device2"), "root.test.device2");
assert_eq!(quote_table_identifier(Some(DatabaseType::Spanner), "user`name"), "`user``name`");
// ArgoDB shares the Hive-family dialect: backticks quote identifiers and
// double quotes are string literals, and schemas qualify table names.
assert_eq!(quote_table_identifier(Some(DatabaseType::Argo), "user`name"), "`user``name`");
assert_eq!(quote_transfer_identifier("user`name", &DatabaseType::Argo), "`user``name`");
assert!(is_schema_aware(DatabaseType::Argo));
}
/// Spanner databases are created in one of two immutable dialects. The connected
+18 -6
View File
@@ -2959,7 +2959,7 @@ pub fn map_column_type(source_type: &str, _source_db: &DatabaseType, target_db:
// Extract basic type, `bigint unsigned` -> `bigint`
base = base.split(' ').next().unwrap_or(base).trim();
if matches!(target_db, DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala) {
if matches!(target_db, DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala | DatabaseType::Argo) {
return match base {
"tinyint" => "TINYINT".into(),
"smallint" | "int2" => "SMALLINT".into(),
@@ -3192,7 +3192,11 @@ fn generate_create_table_ddl_with_column_quoting(
line.push(' ');
line.push_str(&default_clause);
}
if !c.is_nullable && !matches!(target_db, DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala)
if !c.is_nullable
&& !matches!(
target_db,
DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala | DatabaseType::Argo
)
{
line.push_str(" NOT NULL");
}
@@ -3216,7 +3220,7 @@ fn generate_create_table_ddl_with_column_quoting(
}
let mut pks = Vec::with_capacity(columns.iter().filter(|c| c.is_primary_key).count());
if !matches!(target_db, DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala) {
if !matches!(target_db, DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala | DatabaseType::Argo) {
for c in columns {
if c.is_primary_key {
let qname = transfer_column_identifier(&c.name, target_db, quote_target_column_names);
@@ -3748,7 +3752,7 @@ fn generate_upsert_typed_for_transfer(
fn max_transfer_write_rows(db_type: &DatabaseType, mode: &TransferMode) -> usize {
match (db_type, mode) {
(DatabaseType::SqlServer, TransferMode::Append | TransferMode::Overwrite) => MAX_SQLSERVER_INSERT_ROWS,
(DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala, _) => 500,
(DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala | DatabaseType::Argo, _) => 500,
(DatabaseType::Oracle, TransferMode::Append | TransferMode::Overwrite) => MAX_ORACLE_INSERT_ALL_ROWS,
(DatabaseType::Oracle, TransferMode::Upsert) => MAX_ORACLE_MERGE_ROWS,
_ => usize::MAX,
@@ -7548,7 +7552,11 @@ where
|| (request.mode == TransferMode::Upsert
&& !matches!(
target_db_type,
DatabaseType::ClickHouse | DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala
DatabaseType::ClickHouse
| DatabaseType::Hive
| DatabaseType::Kyuubi
| DatabaseType::Impala
| DatabaseType::Argo
))
|| matches!(target_db_type, DatabaseType::Postgres | DatabaseType::Dameng);
let target_columns = if needs_target_columns {
@@ -7622,7 +7630,11 @@ where
let (effective_mode, pk_columns) = if request.mode == TransferMode::Upsert {
if matches!(
target_db_type,
DatabaseType::ClickHouse | DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Impala
DatabaseType::ClickHouse
| DatabaseType::Hive
| DatabaseType::Kyuubi
| DatabaseType::Impala
| DatabaseType::Argo
) {
log::warn!("[transfer] upsert not supported for {:?}, falling back to append", target_db_type);
(TransferMode::Append, vec![])
@@ -101,6 +101,7 @@ fn maps_agent_database_types_to_driver_keys() {
assert_eq!(agent_key(&DatabaseType::Hive, None), Some("hive"));
assert_eq!(agent_key(&DatabaseType::Kyuubi, None), Some("hive"));
assert_eq!(agent_key(&DatabaseType::Impala, None), Some("hive"));
assert_eq!(agent_key(&DatabaseType::Argo, None), Some("hive"));
assert_eq!(agent_key(&DatabaseType::Tdengine, None), Some("tdengine"));
assert_eq!(agent_key(&DatabaseType::Iotdb, None), Some("iotdb"));
assert_eq!(agent_key(&DatabaseType::Yashandb, None), Some("yashandb"));
@@ -146,6 +147,7 @@ fn classifies_agent_database_types() {
assert!(is_agent_type(&DatabaseType::Hive));
assert!(is_agent_type(&DatabaseType::Kyuubi));
assert!(is_agent_type(&DatabaseType::Impala));
assert!(is_agent_type(&DatabaseType::Argo));
assert!(is_agent_type(&DatabaseType::Tdengine));
assert!(is_agent_type(&DatabaseType::Iotdb));
assert!(is_agent_type(&DatabaseType::Yashandb));
+34
View File
@@ -0,0 +1,34 @@
schemaVersion: 1
order: 515
dbType: argo
rustVariant: Argo
label: ArgoDB (Transwarp)
runtimeMode: agent
mcpMode: bridge
agentKey: hive
driverStoreVisible: false
driverStoreOrder: 23
singleConnectionPool: false
metadataConnectionScoped: false
skipTcpProbe: true
defaultPort: 10000
traits:
schemaAware: true
supportLevel: operate
capabilities:
queryExecution: true
metadataBrowse: true
objectBrowser: true
objectSource: true
schemaSearch: true
diagram: false
tableDataEdit: true
tableStructureEdit: false
tableImport: false
dataTransfer: true
sqlFileExecution: true
databaseCreate: false
fieldLineage: false
sqlExplain: false
userAdmin: false
driverManagement: true
@@ -470,6 +470,14 @@ profiles:
user: ""
urlParams: auth=NONE
category: analytics
- id: argo
dbType: argo
label: ArgoDB (Transwarp)
icon: hive
port: 10000
user: ""
category: domestic
pickerLabel: ArgoDB
- id: impala
dbType: impala
label: Apache Impala