Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cmd/optional-driver-agent/provider_hive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//go:build gonavi_hive_driver

package main

import "GoNavi-Wails/internal/db"

func init() {
agentDriverType = "hive"
agentDatabaseFactory = func() db.Database {
return &db.HiveDB{}
}
}
7 changes: 7 additions & 0 deletions frontend/public/db-icons/hive.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const parseUriToValues = (
parsed.params.get("topology") || "",
).toLowerCase();
const tlsValue = String(
parsed.params.get("tls") || parsed.params.get("useSSL") || "",
parsed.params.get("tls") || parsed.params.get("useSSL") || parsed.params.get("ssl") || "",
)
.trim()
.toLowerCase();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,31 @@ export const parseMultiHostUri = (uriText: string, expectedScheme: string) => {
.map((item) => item.trim())
.filter(Boolean);

const params = new URLSearchParams(queryText);
if (pathText.includes(";")) {
const [dbSegment, ...semiSegments] = pathText.split(";");
if (semiSegments.some((segment) => segment.includes("="))) {
pathText = dbSegment;
for (const segment of semiSegments) {
const eqIdx = segment.indexOf("=");
if (eqIdx > 0) {
const k = safeDecode(segment.slice(0, eqIdx).trim());
const v = safeDecode(segment.slice(eqIdx + 1).trim());
if (k) {
params.set(k, v);
}
}
}
}
}

return {
username,
password,
hosts,
database: safeDecode(pathText),
hasExplicitPath,
params: new URLSearchParams(queryText),
params,
};
};

Expand Down
101 changes: 101 additions & 0 deletions frontend/src/utils/dataSourceRegistry/hive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, expect, it } from 'vitest';

import { buildUriFromValues, getUriPlaceholder, parseUriToValues } from '../../components/connectionModal/connectionModalUri';
import { buildViewsMetadataQuerySpecs } from '../../components/sidebar/sidebarMetadataQuerySpecs';
import { supportsTableClearAction } from '../../components/tableDataDangerActions';
import { CONNECTION_TYPE_GROUPS, getConnectionTypeDefaultPort } from '../connectionTypeCatalog';
import { findPotentiallyMutatingConnectionStatements, supportsConnectionReadOnlyMode } from '../connectionReadOnly';
import { getDataSourceCapabilities, getDataSourceCapabilityContract } from '../dataSourceCapabilities';
import { buildPaginatedSelectSQL, quoteIdentPart } from '../sql';
import { isMysqlFamilyDialect, resolveSqlDialect } from '../sqlDialectCore';
import { formatSqlExecutionError } from '../sqlErrorSemantics';
import { resolveSidebarMetadataDialect } from '../sidebarMetadata';
import {
canonicalizeDataSourceType,
getDataSourceSpec,
getDataSourceVariants,
getDefaultDataSourceVariant,
supportsAutoDataSourceVariant,
} from './index';

const hive = { type: 'hive' } as never;

describe('Apache Hive registry behavior', () => {
it('joins the big data group on port 10000 with its own dialect and MySQL-family quoting', () => {
const group = CONNECTION_TYPE_GROUPS.find((item) => item.labelKey.endsWith('.bigdata'));
expect(group?.items.map((item) => item.key)).toContain('hive');
expect(getConnectionTypeDefaultPort('hive')).toBe(10000);
expect(canonicalizeDataSourceType('Apache-Hive')).toBe('hive');
expect(canonicalizeDataSourceType('hiveserver2')).toBe('hive');
expect(canonicalizeDataSourceType('hs2')).toBe('hive');
expect(resolveSqlDialect('hive')).toBe('hive');
expect(isMysqlFamilyDialect('hive')).toBe(true);
expect(quoteIdentPart('hive', 'ods_orders')).toBe('`ods_orders`');
expect(buildPaginatedSelectSQL('hive', 'SELECT * FROM `dw`.`ods_orders`', ' ORDER BY `order_id`', 50, 100)).toBe(
'SELECT * FROM `dw`.`ods_orders` ORDER BY `order_id` LIMIT 50 OFFSET 100',
);
});

it('declares v3 and v4 driver variants with auto detection', () => {
expect(getDataSourceSpec('hive')?.ui?.defaultUser).toBe('hive');
expect(supportsAutoDataSourceVariant('hive')).toBe(true);
expect(getDefaultDataSourceVariant('hive')).toBe('auto');
expect(getDataSourceVariants('hive').map((item) => item.id)).toEqual(['v3', 'v4']);
});

it('parses hive:// and jdbc:hive2:// connection strings including semicolon parameters', () => {
expect(parseUriToValues('hive://hive:secret@hs2.example.com:10000/dw?auth=PLAIN', 'hive')).toMatchObject({
host: 'hs2.example.com',
port: 10000,
user: 'hive',
password: 'secret',
database: 'dw',
connectionParams: 'auth=PLAIN',
});
expect(parseUriToValues('jdbc:hive2://hs2.example.com:10000/dw;auth=noSasl;ssl=true', 'hive')).toMatchObject({
host: 'hs2.example.com',
port: 10000,
database: 'dw',
useSSL: true,
sslMode: 'required',
});
expect(buildUriFromValues({ type: 'hive', host: '10.0.0.8', port: 10000, user: 'hive', database: 'dw', timeout: 30 })).toBe(
'hive://hive@10.0.0.8:10000/dw?timeout=30',
);
expect(getUriPlaceholder('hive')).toContain('hive://user:pass@127.0.0.1:10000');
});

it('lists views with SHOW VIEWS and exposes Hive capability profile', () => {
const sqls = buildViewsMetadataQuerySpecs(resolveSidebarMetadataDialect('hive'), 'dw').map((spec) => spec.sql);
expect(sqls).toEqual(['SHOW VIEWS IN `dw`', 'SHOW VIEWS']);
const contract = getDataSourceCapabilityContract(hive);
expect(contract.transaction?.supported).toBe(false);
const capabilities = getDataSourceCapabilities(hive);
expect(capabilities.supportsUserManagement).toBe(false);
expect(capabilities.supportsExplainDiagnosis).toBe(true);
expect(capabilities.supportsSqlQueryExport).toBe(true);
expect(supportsTableClearAction('hive')).toBe(true);
});

it('explains Hive SemanticException and ParseException messages', () => {
expect(formatSqlExecutionError("Error while compiling statement: FAILED: SemanticException [Error 10001]: Line 1:14 Table not found 'nope'")).toContain(
'Table or object does not exist',
);
expect(
formatSqlExecutionError("Error while compiling statement: FAILED: SemanticException [Error 10004]: Line 1:7 Invalid table alias or column reference 'nope'"),
).toContain('Column does not exist');
expect(formatSqlExecutionError("Error while compiling statement: FAILED: ParseException line 1:7 cannot recognize input near 'FORM' 't' '<EOF>'")).toContain(
'SQL syntax error',
);
});

it('classifies writes for read-only protection', () => {
expect(supportsConnectionReadOnlyMode(hive)).toBe(true);
for (const statement of ['SELECT * FROM dw.ods_orders', 'SHOW TABLES', 'DESCRIBE FORMATTED dw.ods_orders']) {
expect(findPotentiallyMutatingConnectionStatements(hive, statement), statement).toEqual([]);
}
for (const statement of ['INSERT INTO dw.ods_orders VALUES (1)', 'DROP TABLE dw.ods_orders', 'CREATE TABLE dw.t AS SELECT 1 AS id']) {
expect(findPotentiallyMutatingConnectionStatements(hive, statement), statement).toEqual([statement]);
}
});
});
6 changes: 5 additions & 1 deletion frontend/src/utils/dataSourceRegistry/uriScheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export const withRegistryUriScheme = (type: unknown, uri: string): string => {
/** 连接串使用了描述表声明的哪个 scheme,供借用家族解析的分支(如 MySQL 多主机解析)追加识别。 */
export const findRegistryUriScheme = (type: unknown, uri: unknown): string | undefined => {
const text = normalize(uri);
return listRegistryUriSchemes(type).find((scheme) => text.startsWith(`${scheme}://`));
for (const scheme of listRegistryUriSchemes(type)) {
if (text.startsWith(`${scheme}://`)) return scheme;
if (text.startsWith(`jdbc:${scheme}://`)) return `jdbc:${scheme}`;
}
return undefined;
};

/** 使用 Trino 连接表单的类型(Trino 本身与 ui.layout 为 trino 的描述表类型,如 Presto):默认库是 catalog.schema,
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/utils/sqlErrorSemantics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const SQL_ERROR_RULES: SqlErrorSemanticRule[] = [
/you have an error in your sql syntax/i,
// Presto / Trino:line 1:8: mismatched input 'FORM'
/mismatched input/i,
// Hive:ParseException line 1:7 cannot recognize input near 'FORM'
/cannot recognize input near/i,
],
},
{
Expand All @@ -56,6 +58,9 @@ const SQL_ERROR_RULES: SqlErrorSemanticRule[] = [
/object\s+.+does not exist/i,
// Presto / Trino:Table 'hive.sales.orders' does not exist、Schema / Catalog … does not exist
/\b(?:table|view|schema|catalog)\s+.+does not exist/i,
// Hive:Table not found 'nope'、Database does not exist: nope
/\b(?:table|view|database)\s+not\s+found\b/i,
/\bdatabase\s+does\s+not\s+exist\b/i,
],
},
{
Expand All @@ -82,6 +87,8 @@ const SQL_ERROR_RULES: SqlErrorSemanticRule[] = [
/no such column/i,
// Presto / Trino:Column 'amount' cannot be resolved
/column\s+.+cannot be resolved/i,
// Hive:Invalid table alias or column reference 'amount'
/invalid table alias or column reference/i,
],
},
{
Expand Down
2 changes: 1 addition & 1 deletion internal/app/datasource_registry_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func registryUsesBooleanKeywords(dbType string) bool {
return false
}
switch spec.Type {
case "questdb", "greptimedb", "firebird":
case "questdb", "greptimedb", "firebird", "hive":
return true
default:
return false
Expand Down
2 changes: 1 addition & 1 deletion internal/app/methods_file_export_sql_dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func escapeSQLLiteral(value string) string {
func dialectEscapesBackslashInStringLiteral(dbType string) bool {
switch strings.ToLower(strings.TrimSpace(dbType)) {
case "mysql", "mariadb", "tidb", "oceanbase", "diros", "doris", "starrocks",
"clickhouse", "tdengine", "taos", "greptimedb":
"clickhouse", "tdengine", "taos", "greptimedb", "hive":
return true
default:
return false
Expand Down
2 changes: 1 addition & 1 deletion internal/app/methods_file_export_sql_header.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func quoteIdentByType(dbType string, ident string) string {
}

switch dbType {
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "tdengine", "clickhouse":
case "mysql", "mariadb", "oceanbase", "diros", "starrocks", "sphinx", "tdengine", "clickhouse", "greptimedb", "hive":
return "`" + strings.ReplaceAll(ident, "`", "``") + "`"
case "kingbase":
return db.QuoteKingbaseIdentifier(ident)
Expand Down
94 changes: 94 additions & 0 deletions internal/datasource/specs/hive.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{
"type": "hive",
"aliases": [
"apache-hive",
"hiveserver2",
"hs2"
],
"displayName": "Apache Hive",
"group": "bigdata",
"order": 10,
"defaultPort": 10000,
"wire": "thrift",
"dialect": "hive",
"family": "mysql",
"objectKind": "table",
"protection": true,
"excelImport": true,
"sqlFileImport": true,
"versionQuery": "SELECT version() AS version",
"sync": {
"source": true,
"target": false,
"model": "relational"
},
"agent": {
"buildTag": "gonavi_hive_driver",
"version": "1.0.0",
"sourcePrefixes": [
"hive",
"registry"
]
},
"variants": {
"auto": true,
"default": "auto",
"items": [
{
"id": "v3",
"label": "Hive 3.x",
"descriptionKey": "datasource.variant.hive.v3",
"minServer": "3.0",
"maxServer": "4.0"
},
{
"id": "v4",
"label": "Hive 4.x +",
"descriptionKey": "datasource.variant.hive.v4",
"minServer": "4.0"
}
]
},
"ui": {
"defaultUser": "hive",
"usernameOptional": true,
"formatDdl": false,
"uriSchemes": [
"hive",
"hive2",
"hiveserver2"
],
"hiddenObjectGroups": [
"routines",
"triggers",
"sequences",
"events",
"packages"
],
"metadataQueries": {
"views": [
"SHOW VIEWS IN `{{database}}`",
"SHOW VIEWS"
]
},
"objectStatements": {
"renameTable": "ALTER TABLE {old} RENAME TO {new}",
"dropTable": "DROP TABLE {table}",
"dropView": "DROP VIEW {view}",
"truncateTable": "TRUNCATE TABLE {table}",
"clearTable": "TRUNCATE TABLE {table}"
},
"quoting": "backtick",
"pagination": "limit-offset",
"ssl": true,
"sslCAPath": true,
"sslClientCert": true,
"connectionParams": true,
"hint": "Data warehouse / HiveServer2 Thrift",
"icon": {
"color": "#F5A623",
"text": "Hv",
"scale": 0.78
}
}
}
2 changes: 2 additions & 0 deletions internal/db/data_source_capability_contract.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"version": 2,
"profiles": {
"sql-hive": {"query": {"supported": true}, "metadata": {"supported": true}, "transaction": {"supported": false, "reason": "transaction_not_supported", "alternative": "execute-statement-directly", "messageKey": "query_editor.message.unsupported_source"}, "pagination": {"supported": true}, "cancel": {"supported": true, "runtimeProbe": true}, "schema": {"supported": false, "reason": "schema_designer_not_supported", "alternative": "use-database-metadata", "messageKey": "query_editor.message.unsupported_source"}, "sampling": {"supported": true}, "streaming": {"supported": false, "reason": "not_streaming_data_source", "alternative": "use-paginated-query", "messageKey": "query_editor.message.unsupported_source"}, "dangerousOperations": {"supported": true}, "ui": {"forceReadOnlyQueryResult": true, "forceReadOnlyStructureDesigner": true, "explainDiagnosis": true, "sqlQueryExport": true, "copyInsert": true, "mockData": true, "createDatabase": true, "dropDatabase": true, "parameterBinding": true}, "navigation": {"primaryVisibilitySupported": true, "primaryKind": "database", "secondarySchemaVisibilitySupported": false, "schemaIdentifierCaseSensitive": false}},
"sql-gbase8s": {"query": {"supported": true}, "metadata": {"supported": true}, "transaction": {"supported": true}, "pagination": {"supported": true}, "cancel": {"supported": true, "runtimeProbe": true}, "schema": {"supported": true}, "sampling": {"supported": true}, "streaming": {"supported": false, "reason": "not_streaming_data_source", "alternative": "use-paginated-query", "messageKey": "query_editor.message.unsupported_source"}, "dangerousOperations": {"supported": true}, "ui": {"userManagement": false, "parameterBinding": false, "explainDiagnosis": false, "sqlQueryExport": true, "copyInsert": true, "mockData": true, "copyTable": false, "createDatabase": false, "createDatabaseCharset": false, "dropDatabase": false}, "navigation": {"primaryVisibilitySupported": true, "primaryKind": "database", "secondarySchemaVisibilitySupported": false, "schemaIdentifierCaseSensitive": false}},
"sql-gbase8a": {"query": {"supported": true}, "metadata": {"supported": true}, "transaction": {"supported": true}, "pagination": {"supported": true}, "cancel": {"supported": true, "runtimeProbe": true}, "schema": {"supported": true}, "sampling": {"supported": true}, "streaming": {"supported": false, "reason": "not_streaming_data_source", "alternative": "use-paginated-query", "messageKey": "query_editor.message.unsupported_source"}, "dangerousOperations": {"supported": true}, "ui": {"userManagement": false, "parameterBinding": true, "explainDiagnosis": true, "sqlQueryExport": true, "copyInsert": true, "mockData": true, "copyTable": true, "createDatabase": true, "createDatabaseCharset": true, "dropDatabase": true}, "navigation": {"primaryVisibilitySupported": true, "primaryKind": "database", "secondarySchemaVisibilitySupported": false, "schemaIdentifierCaseSensitive": false}},
"sql-yashandb": {"query": {"supported": true}, "metadata": {"supported": true}, "transaction": {"supported": true}, "pagination": {"supported": true}, "cancel": {"supported": true, "runtimeProbe": true}, "schema": {"supported": true}, "sampling": {"supported": true}, "streaming": {"supported": false, "reason": "not_streaming_data_source", "alternative": "use-paginated-query", "messageKey": "query_editor.message.unsupported_source"}, "dangerousOperations": {"supported": true}, "ui": {"userManagement": false, "parameterBinding": true, "explainDiagnosis": true, "sqlQueryExport": true, "copyInsert": true, "mockData": true, "preferManualTotalCount": true, "supportsApproximateTableCount": true}, "navigation": {"primaryVisibilitySupported": true, "primaryKind": "owner", "secondarySchemaVisibilitySupported": false, "schemaIdentifierCaseSensitive": false}},
Expand Down Expand Up @@ -1497,6 +1498,7 @@
}
},
"drivers": {
"hive": "sql-hive",
"firebird": "sql-firebird",
"yashandb": "sql-yashandb",
"gbase8s": "sql-gbase8s",
Expand Down
Loading