diff --git a/cmd/optional-driver-agent/provider_hive.go b/cmd/optional-driver-agent/provider_hive.go new file mode 100644 index 000000000..2f91fd410 --- /dev/null +++ b/cmd/optional-driver-agent/provider_hive.go @@ -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{} + } +} diff --git a/frontend/public/db-icons/hive.svg b/frontend/public/db-icons/hive.svg new file mode 100644 index 000000000..87677e7f7 --- /dev/null +++ b/frontend/public/db-icons/hive.svg @@ -0,0 +1,7 @@ + + Apache Hive + + + + + diff --git a/frontend/src/components/connectionModal/connectionModalUriParse.ts b/frontend/src/components/connectionModal/connectionModalUriParse.ts index cc453ba25..cf8417dc7 100644 --- a/frontend/src/components/connectionModal/connectionModalUriParse.ts +++ b/frontend/src/components/connectionModal/connectionModalUriParse.ts @@ -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(); diff --git a/frontend/src/components/connectionModal/connectionModalUriSchemes.ts b/frontend/src/components/connectionModal/connectionModalUriSchemes.ts index 126d28237..f60f8deca 100644 --- a/frontend/src/components/connectionModal/connectionModalUriSchemes.ts +++ b/frontend/src/components/connectionModal/connectionModalUriSchemes.ts @@ -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, }; }; diff --git a/frontend/src/utils/dataSourceRegistry/hive.test.ts b/frontend/src/utils/dataSourceRegistry/hive.test.ts new file mode 100644 index 000000000..43ca8113f --- /dev/null +++ b/frontend/src/utils/dataSourceRegistry/hive.test.ts @@ -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' ''")).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]); + } + }); +}); diff --git a/frontend/src/utils/dataSourceRegistry/uriScheme.ts b/frontend/src/utils/dataSourceRegistry/uriScheme.ts index 78d9c0787..2f0cceb8c 100644 --- a/frontend/src/utils/dataSourceRegistry/uriScheme.ts +++ b/frontend/src/utils/dataSourceRegistry/uriScheme.ts @@ -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, diff --git a/frontend/src/utils/sqlErrorSemantics.ts b/frontend/src/utils/sqlErrorSemantics.ts index 9fa0641ad..9593f271b 100644 --- a/frontend/src/utils/sqlErrorSemantics.ts +++ b/frontend/src/utils/sqlErrorSemantics.ts @@ -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, ], }, { @@ -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, ], }, { @@ -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, ], }, { diff --git a/internal/app/datasource_registry_bridge.go b/internal/app/datasource_registry_bridge.go index ab1ea7fc0..3dac07638 100644 --- a/internal/app/datasource_registry_bridge.go +++ b/internal/app/datasource_registry_bridge.go @@ -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 diff --git a/internal/app/methods_file_export_sql_dump.go b/internal/app/methods_file_export_sql_dump.go index d19c4c099..3646ca025 100644 --- a/internal/app/methods_file_export_sql_dump.go +++ b/internal/app/methods_file_export_sql_dump.go @@ -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 diff --git a/internal/app/methods_file_export_sql_header.go b/internal/app/methods_file_export_sql_header.go index cf80aac02..0e951c2cd 100644 --- a/internal/app/methods_file_export_sql_header.go +++ b/internal/app/methods_file_export_sql_header.go @@ -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) diff --git a/internal/datasource/specs/hive.json b/internal/datasource/specs/hive.json new file mode 100644 index 000000000..1857738b3 --- /dev/null +++ b/internal/datasource/specs/hive.json @@ -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 + } + } +} diff --git a/internal/db/data_source_capability_contract.json b/internal/db/data_source_capability_contract.json index 5e1e24344..368f82723 100644 --- a/internal/db/data_source_capability_contract.json +++ b/internal/db/data_source_capability_contract.json @@ -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}}, @@ -1497,6 +1498,7 @@ } }, "drivers": { + "hive": "sql-hive", "firebird": "sql-firebird", "yashandb": "sql-yashandb", "gbase8s": "sql-gbase8s", diff --git a/internal/db/driver_agent_revisions_gen.go b/internal/db/driver_agent_revisions_gen.go index a43bb6452..7b31f4ddd 100644 --- a/internal/db/driver_agent_revisions_gen.go +++ b/internal/db/driver_agent_revisions_gen.go @@ -4,49 +4,50 @@ package db func init() { optionalDriverAgentRevisions = map[string]string{ - "mariadb": "src-47e754c5477d8c5c", - "oceanbase": "src-2dfbdb0ec1118342", - "diros": "src-84271b9877a878a2", - "starrocks": "src-051943dd82739ccd", - "sphinx": "src-5f6c088bb33c7afc", - "sqlserver": "src-05b2713c868ec747", - "sqlite": "src-7c0ef9af425e6e5f", - "duckdb": "src-4609724c8fdaa0ff", - "dameng": "src-ec287b1dbce72cd2", - "kingbase": "src-394196650cc74985", - "highgo": "src-ffe45b7292745afa", - "vastbase": "src-303fed14c8a73790", - "opengauss": "src-a9717c58f6e9dacf", - "gaussdb": "src-f5de71344da4234f", - "iris": "src-9fca5bec64569c0d", - "cache": "src-f45f09af7574a31a", - "mongodb": "src-d2c7bf5ad5eaef25", - "tdengine": "src-35ce749d51e600dd", - "iotdb": "src-b679b15ec9b2a00d", - "clickhouse": "src-f0f299fd19299d30", - "elasticsearch": "src-24a65eee75612650", - "trino": "src-3f6e68269c9f3071", - "kafka": "src-018af0dfaf84cfd5", - "rocketmq": "src-de65a93400d7304f", - "pulsar": "src-8a48137f26982d59", - "cockroachdb": "src-09e7c44631d9d449", - "etcd": "src-3986b1d4209c5a79", - "firebird": "src-e373e5ce82dad42f", - "gbase8a": "src-fe2d5e1926e8b75d", - "gbase8c": "src-bf76118076fd55ac", - "gbase8s": "src-388bf81a7fc10e87", - "greptimedb": "src-4e86d44b261672ba", - "influxdb": "src-64b6cd7c6af487d3", - "kwdb": "src-d4dd9d7c8491b7e0", - "meilisearch": "src-11d58641ef95bc17", - "opensearch": "src-cfa1155bd2340343", - "presto": "src-f092f767c09df7b2", - "questdb": "src-ac52fe0d9517ff2d", - "tidb": "src-6e2751312e88fbca", - "timescaledb": "src-e63a3e816b6fa038", - "typesense": "src-3766928647fbf2e3", - "weaviate": "src-802aa3f86493c80b", - "yashandb": "src-52d286249eed49de", - "zookeeper": "src-bd49910a4b234bf1", + "mariadb": "src-24cff0b0cd004883", + "oceanbase": "src-dc9e3f44d8caafab", + "diros": "src-28223cf02d14100b", + "starrocks": "src-38a128dc1b50ca54", + "sphinx": "src-c538ef14dd64a40c", + "sqlserver": "src-e9c6c0e9a9c52bc4", + "sqlite": "src-37bcd2b4d75acfb4", + "duckdb": "src-2ffa40f69d925e57", + "dameng": "src-0b967b8da063ee87", + "kingbase": "src-8480fbd0e09ba859", + "highgo": "src-dec92de8bbcc4d66", + "vastbase": "src-602bc2581f4cea81", + "opengauss": "src-6814ac3e6bea9624", + "gaussdb": "src-cc7d0d36310300ed", + "iris": "src-9e3cff0db0f702d4", + "cache": "src-1786fdb7306ec582", + "mongodb": "src-c22c9fc1b0cfbfc2", + "tdengine": "src-d6acb6ef39fee3d5", + "iotdb": "src-e04624a4eb5a57ff", + "clickhouse": "src-14c6ab3d1f166779", + "elasticsearch": "src-288b8d6834473ab6", + "trino": "src-53750718cddb0291", + "kafka": "src-0e8523f49163f232", + "rocketmq": "src-163ff11e0b98bbd4", + "pulsar": "src-ab58d3bd006e53ca", + "cockroachdb": "src-47b9936b051b5188", + "etcd": "src-8c447a1e7de740a4", + "firebird": "src-3ac03713bf32027d", + "gbase8a": "src-a3daa1ad621497b1", + "gbase8c": "src-7e9d2a0d761eae12", + "gbase8s": "src-fe69da7b44959db4", + "greptimedb": "src-671145b8d3776449", + "hive": "src-c4c6eb11b5d5619f", + "influxdb": "src-fbabe6656f67e3d6", + "kwdb": "src-a8d668b42cde50aa", + "meilisearch": "src-63ac1be1e33ef2dd", + "opensearch": "src-ad3a98f99af600f2", + "presto": "src-2ee5b16533ecc679", + "questdb": "src-09be0c048d81e013", + "tidb": "src-42930ff8006797d1", + "timescaledb": "src-53a3b6f332aa11fc", + "typesense": "src-c19583286cfeae57", + "weaviate": "src-120be62de08d06cd", + "yashandb": "src-cb925fc83f05f259", + "zookeeper": "src-a461a35109794886", } } diff --git a/internal/db/hive_impl.go b/internal/db/hive_impl.go new file mode 100644 index 000000000..98e1e243c --- /dev/null +++ b/internal/db/hive_impl.go @@ -0,0 +1,368 @@ +//go:build gonavi_full_drivers || gonavi_hive_driver + +package db + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/url" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "GoNavi-Wails/internal/connection" + proxytunnel "GoNavi-Wails/internal/proxy" + "GoNavi-Wails/internal/ssh" +) + +const ( + defaultHivePort = 10000 + defaultHiveDatabase = "default" + defaultHiveUser = "hive" +) + +var hiveVersionPattern = regexp.MustCompile(`(\d+\.\d+(?:\.\d+)?)`) + +// HiveDB 通过 HiveServer2 TCLIService Thrift Binary 协议访问 Apache Hive(3.x / 4.x)。 +type HiveDB struct { + driverVariantState + transport *hiveTransport + sessionID hiveHandleID + serverProto int32 + database string + currentDB string + forwarder *ssh.LocalForwarder + timeout time.Duration + execMu sync.Mutex +} + +var ( + _ Database = (*HiveDB)(nil) + _ QueryContexter = (*HiveDB)(nil) + _ ExecContexter = (*HiveDB)(nil) + _ StreamQueryExecer = (*HiveDB)(nil) + _ QueryArgsContexter = (*HiveDB)(nil) + _ ExecArgsContexter = (*HiveDB)(nil) + _ BatchApplierContext = (*HiveDB)(nil) + _ DriverVariantReporter = (*HiveDB)(nil) +) + +type hiveResolvedTarget struct { + host string + port int + user string + password string + database string + useSSL bool + params url.Values +} + +func resolveHiveTarget(config connection.ConnectionConfig) hiveResolvedTarget { + target := hiveResolvedTarget{ + host: strings.TrimSpace(config.Host), + port: config.Port, + user: strings.TrimSpace(config.User), + password: config.Password, + database: strings.TrimSpace(config.Database), + useSSL: config.UseSSL, + params: parseHiveConnectionParams(config.ConnectionParams), + } + if uri := strings.TrimSpace(config.URI); uri != "" { + applyHiveURI(&target, uri) + } + if target.host == "" { + target.host = "127.0.0.1" + } + if target.port <= 0 { + target.port = defaultHivePort + } + if target.user == "" { + target.user = firstNonEmptyParam(target.params, "user", "username") + } + if target.user == "" { + target.user = defaultHiveUser + } + if target.password == "" { + target.password = firstNonEmptyParam(target.params, "password") + } + if target.database == "" { + target.database = firstNonEmptyParam(target.params, "database", "db", "schema") + } + if target.database == "" { + target.database = defaultHiveDatabase + } + if sslParam := strings.ToLower(strings.TrimSpace(firstNonEmptyParam(target.params, "ssl", "useSSL", "tls"))); sslParam != "" { + switch sslParam { + case "true", "1", "required", "skip-verify", "preferred": + target.useSSL = true + } + } + return target +} + +func parseHiveConnectionParams(raw string) url.Values { + values := url.Values{} + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return values + } + normalized := strings.ReplaceAll(trimmed, ";", "&") + parsed, err := url.ParseQuery(normalized) + if err != nil { + return values + } + for k, v := range parsed { + if key := strings.TrimSpace(k); key != "" { + values[key] = v + } + } + return values +} + +func applyHiveURI(target *hiveResolvedTarget, rawURI string) { + text := strings.TrimSpace(rawURI) + if len(text) > len("jdbc:") && strings.EqualFold(text[:len("jdbc:")], "jdbc:") { + text = text[len("jdbc:"):] + } + var semiPart string + if semiIdx := strings.Index(text, ";"); semiIdx >= 0 { + semiPart = text[semiIdx+1:] + text = text[:semiIdx] + } + parsed, err := url.Parse(text) + if err != nil { + return + } + if host := strings.TrimSpace(parsed.Hostname()); host != "" { + target.host = host + } + if portText := strings.TrimSpace(parsed.Port()); portText != "" { + if port, err := strconv.Atoi(portText); err == nil && port > 0 { + target.port = port + } + } + if parsed.User != nil { + if u := strings.TrimSpace(parsed.User.Username()); u != "" && target.user == "" { + target.user = u + } + if p, ok := parsed.User.Password(); ok && target.password == "" { + target.password = p + } + } + if dbPath := strings.Trim(strings.TrimSpace(parsed.Path), "/"); dbPath != "" && target.database == "" { + target.database = dbPath + } + for k, v := range parsed.Query() { + if _, exists := target.params[k]; !exists { + target.params[k] = v + } + } + for k, v := range parseHiveConnectionParams(semiPart) { + if _, exists := target.params[k]; !exists { + target.params[k] = v + } + } +} + +func isHiveNoSASL(params url.Values) bool { + auth := strings.ToLower(strings.TrimSpace(firstNonEmptyParam(params, "auth", "authentication"))) + if auth == "nosasl" || auth == "no_sasl" || auth == "none_nosasl" || auth == "raw" { + return true + } + sasl := strings.ToLower(strings.TrimSpace(firstNonEmptyParam(params, "sasl", "useSasl", "use_sasl"))) + return sasl == "false" || sasl == "0" || sasl == "no" +} + +func buildHiveSessionConfiguration(database string, params url.Values) map[string]string { + conf := map[string]string{ + "set:hiveconf:hive.resultset.use.unique.column.names": "false", + } + if db := strings.TrimSpace(database); db != "" { + conf["use:database"] = db + } + for key, values := range params { + if len(values) == 0 { + continue + } + val := strings.TrimSpace(values[len(values)-1]) + lower := strings.ToLower(strings.TrimSpace(key)) + switch { + case strings.HasPrefix(lower, "hiveconf:"): + conf["set:"+strings.TrimSpace(key)] = val + case strings.HasPrefix(lower, "hivevar:"): + conf["set:"+strings.TrimSpace(key)] = val + case strings.HasPrefix(lower, "set:"): + conf[strings.TrimSpace(key)] = val + case strings.HasPrefix(lower, "hive.") || strings.HasPrefix(lower, "tez.") || strings.HasPrefix(lower, "mapreduce."): + conf["set:hiveconf:"+strings.TrimSpace(key)] = val + } + } + return conf +} + +func (h *HiveDB) Connect(config connection.ConnectionConfig) (err error) { + _ = h.Close() + defer func() { + if err != nil { + _ = h.Close() + } + }() + + target := resolveHiveTarget(config) + h.timeout = getConnectTimeout(config) + h.database = target.database + serverHost := target.host + dialHost := target.host + dialPort := target.port + + if config.UseSSH { + forwarder, sshErr := ssh.AcquireLocalForwarder(config.SSH, target.host, target.port) + if sshErr != nil { + return fmt.Errorf("创建 Hive SSH 隧道失败:%w", sshErr) + } + h.forwarder = forwarder + host, portText, splitErr := net.SplitHostPort(forwarder.LocalAddr) + if splitErr != nil { + return fmt.Errorf("解析本地转发地址失败:%w", splitErr) + } + port, portErr := strconv.Atoi(portText) + if portErr != nil { + return fmt.Errorf("解析本地转发端口失败:%w", portErr) + } + dialHost, dialPort = host, port + } + + ctx, cancel := context.WithTimeout(context.Background(), h.timeout) + defer cancel() + + address := net.JoinHostPort(dialHost, strconv.Itoa(dialPort)) + var rawConn net.Conn + if config.UseProxy { + rawConn, err = proxytunnel.DialContext(ctx, config.Proxy, "tcp", address) + } else { + rawConn, err = (&net.Dialer{KeepAlive: 30 * time.Second}).DialContext(ctx, "tcp", address) + } + if err != nil { + return err + } + + if target.useSSL { + sslConfig := config + sslConfig.UseSSL = true + if strings.TrimSpace(sslConfig.SSLMode) == "" { + sslConfig.SSLMode = "required" + } + tlsCfg, tlsErr := resolveGenericTLSConfig(sslConfig) + if tlsErr != nil { + _ = rawConn.Close() + return tlsErr + } + if tlsCfg == nil { + tlsCfg = &tls.Config{MinVersion: tls.VersionTLS12} + } else { + tlsCfg = tlsCfg.Clone() + } + if tlsCfg.ServerName == "" { + tlsCfg.ServerName = serverHost + } + tlsConn := tls.Client(rawConn, tlsCfg) + if err := tlsConn.HandshakeContext(ctx); err != nil { + _ = rawConn.Close() + return err + } + rawConn = tlsConn + } + + noSASL := isHiveNoSASL(target.params) + framed := !noSASL || strings.EqualFold(firstNonEmptyParam(target.params, "transport", "transportMode"), "framed") + h.transport = newHiveTransport(rawConn, framed) + if !noSASL { + if err := h.transport.handshakeSASLPlain(ctx, target.user, target.password); err != nil { + return err + } + } + + sessionConf := buildHiveSessionConfiguration(target.database, target.params) + sessionID, serverProto, err := h.transport.openSession(ctx, hiveProtocolV11, target.user, target.password, sessionConf) + if err != nil { + return err + } + h.sessionID = sessionID + h.serverProto = serverProto + h.currentDB = target.database + + version, verErr := h.detectVersion(ctx) + if verErr != nil { + return verErr + } + if err := h.resolve("hive", config, version); err != nil { + return err + } + if target.database != "" && !strings.EqualFold(target.database, defaultHiveDatabase) { + if _, err := h.execStatementOnSession(ctx, "USE "+quoteHiveIdent(target.database)); err != nil { + return err + } + h.currentDB = target.database + } + return nil +} + +func (h *HiveDB) detectVersion(ctx context.Context) (string, error) { + rows, _, err := h.queryStatementOnSession(ctx, "SELECT version() AS version", nil) + if err != nil { + return "", err + } + raw := FirstQueryRowValue(rows) + return parseHiveVersion(raw), nil +} + +func parseHiveVersion(banner string) string { + match := hiveVersionPattern.FindStringSubmatch(strings.TrimSpace(banner)) + if len(match) < 2 { + return "" + } + return match[1] +} + +func (h *HiveDB) Close() error { + h.execMu.Lock() + defer h.execMu.Unlock() + var firstErr error + if h.transport != nil { + if len(h.sessionID.guid) > 0 { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + _ = h.transport.closeSession(ctx, h.sessionID) + cancel() + h.sessionID = hiveHandleID{} + } + if err := h.transport.Close(); err != nil && firstErr == nil { + firstErr = err + } + h.transport = nil + } + if h.forwarder != nil { + if err := h.forwarder.Close(); err != nil && firstErr == nil { + firstErr = err + } + h.forwarder = nil + } + return firstErr +} + +func (h *HiveDB) Ping() error { + ctx, cancel := context.WithTimeout(context.Background(), h.timeoutOrDefault()) + defer cancel() + _, _, err := h.QueryContext(ctx, "SELECT 1") + return err +} + +func (h *HiveDB) timeoutOrDefault() time.Duration { + if h.timeout > 0 { + return h.timeout + } + return 15 * time.Second +} diff --git a/internal/db/hive_impl_query.go b/internal/db/hive_impl_query.go new file mode 100644 index 000000000..1e5fa49ef --- /dev/null +++ b/internal/db/hive_impl_query.go @@ -0,0 +1,382 @@ +//go:build gonavi_full_drivers || gonavi_hive_driver + +package db + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + "time" +) + +const hiveCancelTimeout = 5 * time.Second + +// normalizeHiveStatement 去掉语句末尾的分号:HiveServer2 Thrift ExecuteStatement 只接受单条不含尾部分号的 HQL。 +func normalizeHiveStatement(query string) string { + return strings.TrimRight(strings.TrimSpace(query), "; \t\r\n") +} + +func (h *HiveDB) QueryContext(ctx context.Context, query string) ([]map[string]interface{}, []string, error) { + h.execMu.Lock() + defer h.execMu.Unlock() + return h.queryStatementOnSession(ctx, query, RowBudgetFromContext(ctx)) +} + +func (h *HiveDB) Query(query string) ([]map[string]interface{}, []string, error) { + return h.QueryContext(metadataContextFor(h), query) +} + +func (h *HiveDB) StreamQueryContext(ctx context.Context, query string, consumer QueryStreamConsumer) error { + h.execMu.Lock() + defer h.execMu.Unlock() + if h.transport == nil { + return localizedDatabaseRuntimeError("db.backend.error.connection_not_open", nil) + } + sink := &hiveStreamSink{consumer: consumer} + _, err := h.runOperation(ctx, query, sink) + return err +} + +func (h *HiveDB) StreamQuery(query string, consumer QueryStreamConsumer) error { + return h.StreamQueryContext(metadataContextFor(h), query, consumer) +} + +func (h *HiveDB) ExecContext(ctx context.Context, query string) (int64, error) { + h.execMu.Lock() + defer h.execMu.Unlock() + return h.execStatementOnSession(ctx, query) +} + +func (h *HiveDB) Exec(query string) (int64, error) { + return h.ExecContext(metadataContextFor(h), query) +} + +func (h *HiveDB) QueryContextWithArgs(ctx context.Context, query string, args []any) ([]map[string]interface{}, []string, error) { + bound, err := hiveBindArgs(query, args) + if err != nil { + return nil, nil, err + } + return h.QueryContext(ctx, bound) +} + +func (h *HiveDB) ExecContextWithArgs(ctx context.Context, query string, args []any) (int64, error) { + bound, err := hiveBindArgs(query, args) + if err != nil { + return 0, err + } + return h.ExecContext(ctx, bound) +} + +func (h *HiveDB) queryStatementOnSession(ctx context.Context, query string, budget *RowBudget) ([]map[string]interface{}, []string, error) { + if h.transport == nil { + return nil, nil, localizedDatabaseRuntimeError("db.backend.error.connection_not_open", nil) + } + collector := &hiveRowCollector{budget: budget} + if _, err := h.runOperation(ctx, query, collector); err != nil { + return nil, nil, err + } + if collector.data == nil { + collector.data = make([]map[string]interface{}, 0) + } + return collector.data, collector.names, nil +} + +func (h *HiveDB) execStatementOnSession(ctx context.Context, query string) (int64, error) { + if h.transport == nil { + return 0, localizedDatabaseRuntimeError("db.backend.error.connection_not_open", nil) + } + return h.runOperation(ctx, query, nil) +} + +func (h *HiveDB) runOperation(ctx context.Context, rawQuery string, sink hiveRowSink) (int64, error) { + statement := normalizeHiveStatement(rawQuery) + if statement == "" { + return 0, nil + } + op, err := h.transport.executeStatement(ctx, h.sessionID, statement, nil, true, 0) + if err != nil { + return 0, err + } + closed := false + closeOp := func() { + if closed || len(op.id.guid) == 0 { + return + } + closed = true + cctx, cancel := context.WithTimeout(context.Background(), hiveCancelTimeout) + defer cancel() + _ = h.transport.closeOperation(cctx, op) + } + defer closeOp() + + cancelOp := func() { + if len(op.id.guid) == 0 { + return + } + cctx, cancel := context.WithTimeout(context.Background(), hiveCancelTimeout) + defer cancel() + _ = h.transport.cancelOperation(cctx, op) + } + + hasResultSet, err := h.waitOperationFinished(ctx, op, cancelOp) + if err != nil { + return 0, err + } + if op.hasResultSet { + hasResultSet = true + } + + var affected int64 + if op.hasModifiedCount && op.modifiedRowCount > 0 { + affected = int64(op.modifiedRowCount) + } + + if !hasResultSet || sink == nil { + return affected, nil + } + + cols, err := h.transport.getResultSetMetadata(ctx, op) + if err != nil { + if ctx.Err() != nil { + cancelOp() + return 0, ctx.Err() + } + return 0, err + } + names := uniqueHiveColumnNames(cols) + if err := sink.onColumns(names); err != nil { + return affected, err + } + + for { + batch, fetchErr := h.transport.fetchResults(ctx, op, hiveFetchNext, defaultHiveFetchBatchSize) + if fetchErr != nil { + if ctx.Err() != nil { + cancelOp() + return 0, ctx.Err() + } + return 0, fetchErr + } + if len(batch) == 0 { + break + } + for _, row := range batch { + if err := sink.onRow(row); err != nil { + if errors.Is(err, errHiveStopFetch) { + cancelOp() + return affected, nil + } + cancelOp() + return 0, err + } + } + } + return affected, nil +} + +func (h *HiveDB) waitOperationFinished(ctx context.Context, op hiveOperationHandle, cancelOp func()) (bool, error) { + pollWait := 20 * time.Millisecond + for { + st, err := h.transport.getOperationStatus(ctx, op) + if err != nil { + if ctx.Err() != nil { + cancelOp() + return false, ctx.Err() + } + return false, err + } + switch st.operationState { + case hiveOpFinished: + return st.hasResultSet, nil + case hiveOpInitialized, hiveOpPending, hiveOpRunning: + timer := time.NewTimer(pollWait) + select { + case <-ctx.Done(): + timer.Stop() + cancelOp() + return false, ctx.Err() + case <-timer.C: + if pollWait < 200*time.Millisecond { + pollWait *= 2 + } + } + case hiveOpCanceled: + return false, context.Canceled + case hiveOpTimedout: + return false, context.DeadlineExceeded + case hiveOpError, hiveOpClosed, hiveOpUnknown: + msg := strings.TrimSpace(st.errorMessage) + if msg == "" { + msg = fmt.Sprintf("Hive operation failed in state %d", st.operationState) + } + return false, &hiveQueryError{ + sqlState: st.sqlState, + errorCode: st.errorCode, + message: msg, + } + default: + return st.hasResultSet, nil + } + } +} + +var errHiveStopFetch = errors.New("stop hive fetch") + +type hiveRowCollector struct { + names []string + data []map[string]interface{} + budget *RowBudget +} + +func (c *hiveRowCollector) onColumns(names []string) error { + c.names = names + return nil +} + +func (c *hiveRowCollector) onRow(values []any) error { + if !c.budget.CanMaterializeRow(len(c.data)) { + return errHiveStopFetch + } + row := make(map[string]interface{}, len(c.names)) + for i, name := range c.names { + if i < len(values) { + row[name] = normalizeQueryValue(values[i]) + } else { + row[name] = nil + } + } + row, fieldTruncated := boundQueryRowFields(row, c.budget.MaxFieldBytes()) + if fieldTruncated { + c.budget.MarkFieldTruncated() + } + if !c.budget.ConsumeRow(estimateQueryRowBytes(row)) { + return errHiveStopFetch + } + c.data = append(c.data, row) + return nil +} + +type hiveStreamSink struct { + consumer QueryStreamConsumer + names []string +} + +func (s *hiveStreamSink) onColumns(names []string) error { + s.names = names + return s.consumer.SetColumns(names) +} + +func (s *hiveStreamSink) onRow(values []any) error { + row := make(map[string]interface{}, len(s.names)) + for i, name := range s.names { + if i < len(values) { + row[name] = values[i] + } else { + row[name] = nil + } + } + return s.consumer.ConsumeRow(row) +} + +// hiveBindArgs 将 SQL 中不在引号或注释内的 ? 占位符替换为 HiveQL 字面量。 +func hiveBindArgs(query string, args []any) (string, error) { + if len(args) == 0 { + return query, nil + } + var out strings.Builder + out.Grow(len(query) + len(args)*16) + argIdx := 0 + var quote rune + for i := 0; i < len(query); i++ { + ch := rune(query[i]) + if quote != 0 { + out.WriteByte(query[i]) + if ch == '\\' && i+1 < len(query) { + i++ + out.WriteByte(query[i]) + continue + } + if ch == quote { + if i+1 < len(query) && rune(query[i+1]) == quote { + i++ + out.WriteByte(query[i]) + continue + } + quote = 0 + } + continue + } + if ch == '\'' || ch == '"' || ch == '`' { + quote = ch + out.WriteByte(query[i]) + continue + } + if ch == '-' && i+1 < len(query) && query[i+1] == '-' { + end := strings.IndexByte(query[i:], '\n') + if end < 0 { + out.WriteString(query[i:]) + break + } + out.WriteString(query[i : i+end+1]) + i += end + continue + } + if ch == '/' && i+1 < len(query) && query[i+1] == '*' { + end := strings.Index(query[i+2:], "*/") + if end < 0 { + out.WriteString(query[i:]) + break + } + out.WriteString(query[i : i+2+end+2]) + i += 2 + end + 1 + continue + } + if ch == '?' { + if argIdx >= len(args) { + return "", fmt.Errorf("insufficient arguments for query placeholders: got %d", len(args)) + } + out.WriteString(hiveLiteral(args[argIdx])) + argIdx++ + continue + } + out.WriteByte(query[i]) + } + if argIdx != len(args) { + return "", fmt.Errorf("placeholder count %d does not match argument count %d", argIdx, len(args)) + } + return out.String(), nil +} + +func hiveLiteral(value any) string { + switch v := value.(type) { + case nil: + return "NULL" + case bool: + if v { + return "TRUE" + } + return "FALSE" + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return fmt.Sprintf("%d", v) + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32) + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case time.Time: + return "'" + v.Format("2006-01-02 15:04:05") + "'" + case []byte: + return hiveQuoteString(string(v)) + case string: + return hiveQuoteString(v) + default: + return hiveQuoteString(fmt.Sprint(v)) + } +} + +func hiveQuoteString(s string) string { + escaped := strings.ReplaceAll(s, `\`, `\\`) + escaped = strings.ReplaceAll(escaped, `'`, `\'`) + return "'" + escaped + "'" +} diff --git a/internal/db/hive_impl_schema.go b/internal/db/hive_impl_schema.go new file mode 100644 index 000000000..b69272b5d --- /dev/null +++ b/internal/db/hive_impl_schema.go @@ -0,0 +1,387 @@ +//go:build gonavi_full_drivers || gonavi_hive_driver + +package db + +import ( + "context" + "fmt" + "strings" + + "GoNavi-Wails/internal/connection" +) + +func quoteHiveIdent(name string) string { + cleaned := strings.Trim(strings.TrimSpace(name), "`") + return "`" + strings.ReplaceAll(cleaned, "`", "``") + "`" +} + +func quoteHiveTable(dbName, tableName string) string { + db, table := resolveHiveTableTarget(dbName, tableName, "") + if db == "" { + return quoteHiveIdent(table) + } + return quoteHiveIdent(db) + "." + quoteHiveIdent(table) +} + +func resolveHiveTableTarget(dbName, tableName, defaultDB string) (string, string) { + rawDB := strings.Trim(strings.TrimSpace(dbName), "`") + rawTable := strings.TrimSpace(tableName) + if schema, table := SplitSQLQualifiedNameForDialect(rawTable, "mysql"); table != "" { + if schema != "" { + return strings.Trim(schema, "`"), strings.Trim(table, "`") + } + rawTable = strings.Trim(table, "`") + } else { + rawTable = strings.Trim(rawTable, "`") + } + if rawDB == "" { + rawDB = strings.Trim(strings.TrimSpace(defaultDB), "`") + } + return rawDB, rawTable +} + +func (h *HiveDB) effectiveDatabase(dbName string) string { + if db := strings.Trim(strings.TrimSpace(dbName), "`"); db != "" { + return db + } + if db := strings.Trim(strings.TrimSpace(h.database), "`"); db != "" { + return db + } + return defaultHiveDatabase +} + +func (h *HiveDB) GetDatabases() ([]string, error) { + rows, _, err := h.QueryContext(metadataContextFor(h), "SHOW DATABASES") + if err != nil { + return nil, err + } + databases := make([]string, 0, len(rows)) + for _, row := range rows { + name := rowText(row, "database_name", "namespace", "database") + if name == "" { + name = firstRowValueText(row) + } + if name != "" { + databases = append(databases, name) + } + } + return databases, nil +} + +func (h *HiveDB) GetTables(dbName string) ([]string, error) { + ctx := metadataContextFor(h) + targetDB := h.effectiveDatabase(dbName) + rows, _, err := h.QueryContext(ctx, "SHOW TABLES IN "+quoteHiveIdent(targetDB)) + if err != nil { + return nil, err + } + viewSet := make(map[string]struct{}) + if viewRows, _, viewErr := h.QueryContext(ctx, "SHOW VIEWS IN "+quoteHiveIdent(targetDB)); viewErr == nil { + for _, row := range viewRows { + vName := rowText(row, "tab_name", "view_name", "table_name") + if vName == "" { + vName = firstRowValueText(row) + } + if vName != "" { + viewSet[strings.ToLower(vName)] = struct{}{} + } + } + } + tables := make([]string, 0, len(rows)) + for _, row := range rows { + name := rowText(row, "tab_name", "table_name", "tableName", "name") + if name == "" { + name = firstRowValueText(row) + } + if name == "" { + continue + } + if _, isView := viewSet[strings.ToLower(name)]; isView { + continue + } + tables = append(tables, name) + } + return tables, nil +} + +func (h *HiveDB) GetCreateStatement(dbName, tableName string) (string, error) { + targetDB, targetTable := resolveHiveTableTarget(dbName, tableName, h.database) + if targetTable == "" { + return "", localizedDatabaseRuntimeError("db.backend.error.table_name_required", nil) + } + rows, _, err := h.QueryContext(metadataContextFor(h), "SHOW CREATE TABLE "+quoteHiveTable(targetDB, targetTable)) + if err != nil { + return "", err + } + var lines []string + for _, row := range rows { + rawVal := firstMapValueOf(row, "createtab_stmt", "Create Table", "create_table") + var line string + if rawVal != nil { + line = strings.TrimRight(fmt.Sprint(rawVal), "\r\n") + } + if strings.TrimSpace(line) == "" { + line = firstRowValueText(row) + } + if strings.TrimSpace(line) != "" { + lines = append(lines, line) + } + } + ddl := strings.TrimSpace(strings.Join(lines, "\n")) + if ddl == "" { + return "", localizedDatabaseRuntimeError("db.backend.error.hive_ddl_empty", map[string]any{"name": targetTable}) + } + return ddl, nil +} + +func (h *HiveDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) { + targetDB, targetTable := resolveHiveTableTarget(dbName, tableName, h.database) + if targetTable == "" { + return nil, localizedDatabaseRuntimeError("db.backend.error.table_name_required", nil) + } + rows, _, err := h.QueryContext(metadataContextFor(h), "DESCRIBE "+quoteHiveTable(targetDB, targetTable)) + if err != nil { + return nil, err + } + return parseHiveDescribeRows(rows), nil +} + +// parseHiveDescribeRows 解析 Hive DESCRIBE 的输出行,识别普通列与 # Partition Information 分区键列。 +func parseHiveDescribeRows(rows []map[string]interface{}) []connection.ColumnDefinition { + columns := make([]connection.ColumnDefinition, 0, len(rows)) + indexByName := make(map[string]int, len(rows)) + inPartitionSection := false + + for _, row := range rows { + colName := strings.TrimSpace(rowText(row, "col_name", "name", "column_name", "field")) + dataType := strings.TrimSpace(rowText(row, "data_type", "type")) + comment := strings.TrimSpace(rowText(row, "comment", "remarks")) + if comment == "from deserializer" || strings.EqualFold(comment, "null") { + comment = "" + } + + if colName == "" { + continue + } + if strings.HasPrefix(colName, "#") { + lowerHeader := strings.ToLower(colName) + switch { + case strings.Contains(lowerHeader, "partition information"): + inPartitionSection = true + case strings.Contains(lowerHeader, "col_name"): + // 表头行跳过,保持当前段落状态 + default: + inPartitionSection = false + } + continue + } + if dataType == "" { + continue + } + + lowerName := strings.ToLower(colName) + if inPartitionSection { + if idx, exists := indexByName[lowerName]; exists { + columns[idx].Extra = "PARTITION KEY" + if comment != "" && columns[idx].Comment == "" { + columns[idx].Comment = comment + } + } else { + indexByName[lowerName] = len(columns) + columns = append(columns, connection.ColumnDefinition{ + Name: colName, + Type: dataType, + Nullable: "YES", + Comment: comment, + Extra: "PARTITION KEY", + }) + } + continue + } + + if _, exists := indexByName[lowerName]; exists { + continue + } + indexByName[lowerName] = len(columns) + columns = append(columns, connection.ColumnDefinition{ + Name: colName, + Type: dataType, + Nullable: "YES", + Comment: comment, + }) + } + return columns +} + +func (h *HiveDB) GetAllColumns(dbName string) ([]connection.ColumnDefinitionWithTable, error) { + ctx := metadataContextFor(h) + targetDB := h.effectiveDatabase(dbName) + if cols, err := h.fetchAllColumnsViaThrift(ctx, targetDB); err == nil && len(cols) > 0 { + return cols, nil + } + tables, err := h.GetTables(targetDB) + if err != nil { + return nil, err + } + var all []connection.ColumnDefinitionWithTable + for _, table := range tables { + cols, colErr := h.GetColumns(targetDB, table) + if colErr != nil { + continue + } + for _, col := range cols { + all = append(all, connection.ColumnDefinitionWithTable{ + TableName: table, + Name: col.Name, + Type: col.Type, + }) + } + } + return all, nil +} + +func (h *HiveDB) fetchAllColumnsViaThrift(ctx context.Context, dbName string) ([]connection.ColumnDefinitionWithTable, error) { + h.execMu.Lock() + defer h.execMu.Unlock() + if h.transport == nil { + return nil, localizedDatabaseRuntimeError("db.backend.error.connection_not_open", nil) + } + var op hiveOperationHandle + var status hiveStatus + err := h.transport.call(ctx, "GetColumns", func(w *hiveWriter) { + w.writeFieldBegin(thriftTypeStruct, 1) // req + w.writeFieldBegin(thriftTypeStruct, 1) // sessionHandle + writeHiveSessionHandle(w, h.sessionID) + w.writeFieldBegin(thriftTypeString, 3) // schemaName + w.writeString(dbName) + w.writeFieldBegin(thriftTypeString, 4) // tableName + w.writeString("%") + w.writeFieldBegin(thriftTypeString, 5) // columnName + w.writeString("%") + w.writeFieldStop() + }, func(r *hiveReader) error { + return readSingleStructField(r, func(r *hiveReader) error { + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return wrapHiveProtocolError(err) + } + if ft == thriftTypeStop { + return nil + } + switch { + case fid == 1 && ft == thriftTypeStruct: + if status, err = readHiveStatus(r); err != nil { + return wrapHiveProtocolError(err) + } + case fid == 2 && ft == thriftTypeStruct: + if op, err = readHiveOperationHandle(r); err != nil { + return wrapHiveProtocolError(err) + } + default: + if err := r.skip(ft); err != nil { + return wrapHiveProtocolError(err) + } + } + } + }) + }) + if err != nil { + return nil, err + } + if err := status.toError(); err != nil { + return nil, err + } + defer func() { + cctx, cancel := context.WithTimeout(context.Background(), hiveCancelTimeout) + defer cancel() + _ = h.transport.closeOperation(cctx, op) + }() + cancelOp := func() { + cctx, cancel := context.WithTimeout(context.Background(), hiveCancelTimeout) + defer cancel() + _ = h.transport.cancelOperation(cctx, op) + } + if _, err := h.waitOperationFinished(ctx, op, cancelOp); err != nil { + return nil, err + } + + collector := &hiveRowCollector{} + colsMeta, err := h.transport.getResultSetMetadata(ctx, op) + if err != nil { + return nil, err + } + if err := collector.onColumns(uniqueHiveColumnNames(colsMeta)); err != nil { + return nil, err + } + for { + batch, fetchErr := h.transport.fetchResults(ctx, op, hiveFetchNext, defaultHiveFetchBatchSize) + if fetchErr != nil { + return nil, fetchErr + } + if len(batch) == 0 { + break + } + for _, row := range batch { + if err := collector.onRow(row); err != nil { + return nil, err + } + } + } + result := make([]connection.ColumnDefinitionWithTable, 0, len(collector.data)) + for _, row := range collector.data { + tableName := rowText(row, "TABLE_NAME", "table_name") + colName := rowText(row, "COLUMN_NAME", "column_name") + typeName := rowText(row, "TYPE_NAME", "type_name", "DATA_TYPE") + if tableName != "" && colName != "" { + result = append(result, connection.ColumnDefinitionWithTable{ + TableName: tableName, + Name: colName, + Type: typeName, + }) + } + } + return result, nil +} + +func (h *HiveDB) GetIndexes(string, string) ([]connection.IndexDefinition, error) { + return []connection.IndexDefinition{}, nil +} + +func (h *HiveDB) GetForeignKeys(string, string) ([]connection.ForeignKeyDefinition, error) { + return []connection.ForeignKeyDefinition{}, nil +} + +func (h *HiveDB) GetTriggers(string, string) ([]connection.TriggerDefinition, error) { + return []connection.TriggerDefinition{}, nil +} + +func (h *HiveDB) ApplyChangesContext(ctx context.Context, tableName string, changes connection.ChangeSet) error { + reject := localizedDatabaseRuntimeError("db.backend.error.hive_row_edit_unsupported", nil) + if len(changes.Updates) > 0 || len(changes.Deletes) > 0 { + return reject + } + targetDB, targetTable := resolveHiveTableTarget("", tableName, h.database) + columns, _ := h.GetColumns(targetDB, targetTable) + return applyInsertOnlyChanges(ctx, h.ExecContext, columns, targetTable, changes, insertOnlyDialect{ + quoteTable: func(name string) string { return quoteHiveTable(targetDB, name) }, + quoteIdent: quoteHiveIdent, + escapeBackslash: true, + rowsPerStatement: 200, + }, reject) +} + +func (h *HiveDB) ApplyChanges(tableName string, changes connection.ChangeSet) error { + return h.ApplyChangesContext(context.Background(), tableName, changes) +} + +func firstRowValueText(row map[string]interface{}) string { + for _, value := range row { + if value != nil { + if text := strings.TrimSpace(rowText(map[string]interface{}{"v": value}, "v")); text != "" { + return text + } + } + } + return "" +} diff --git a/internal/db/hive_impl_test.go b/internal/db/hive_impl_test.go new file mode 100644 index 000000000..f30f40f5d --- /dev/null +++ b/internal/db/hive_impl_test.go @@ -0,0 +1,146 @@ +//go:build gonavi_full_drivers || gonavi_hive_driver + +package db + +import ( + "bytes" + "testing" + + "GoNavi-Wails/internal/connection" +) + +func TestResolveHiveTargetAndURI(t *testing.T) { + defaultTarget := resolveHiveTarget(connection.ConnectionConfig{Type: "hive"}) + if defaultTarget.host != "127.0.0.1" || defaultTarget.port != 10000 || defaultTarget.user != "hive" || defaultTarget.database != "default" { + t.Fatalf("unexpected default target: %+v", defaultTarget) + } + + jdbcTarget := resolveHiveTarget(connection.ConnectionConfig{ + Type: "hive", + URI: "jdbc:hive2://hs2.example.com:10001/dw;auth=noSasl;hive.execution.engine=tez", + }) + if jdbcTarget.host != "hs2.example.com" || jdbcTarget.port != 10001 || jdbcTarget.database != "dw" { + t.Fatalf("unexpected jdbc target: %+v", jdbcTarget) + } + if !isHiveNoSASL(jdbcTarget.params) { + t.Fatalf("expected auth=noSasl to enable noSASL: %+v", jdbcTarget.params) + } + conf := buildHiveSessionConfiguration(jdbcTarget.database, jdbcTarget.params) + if conf["use:database"] != "dw" || conf["set:hiveconf:hive.execution.engine"] != "tez" || conf["set:hiveconf:hive.resultset.use.unique.column.names"] != "false" { + t.Fatalf("unexpected session configuration: %+v", conf) + } + + uriTarget := resolveHiveTarget(connection.ConnectionConfig{ + Type: "hive", + URI: "hive://analyst:secret@10.0.0.8:10000/analytics?ssl=true&hiveconf:mapreduce.job.queuename=prod", + }) + if uriTarget.host != "10.0.0.8" || uriTarget.user != "analyst" || uriTarget.password != "secret" || uriTarget.database != "analytics" || !uriTarget.useSSL { + t.Fatalf("unexpected uri target: %+v", uriTarget) + } +} + +func TestParseHiveVersionAndVariants(t *testing.T) { + cases := map[string]struct { + version string + variant string + }{ + "4.0.1 r3af4517eb8cfd9407ad34ed78a0b48b57dfaa264": {version: "4.0.1", variant: "v4"}, + "3.1.3000.7.1.8.0-801 r123456": {version: "3.1.3000", variant: "v3"}, + } + for banner, want := range cases { + gotVer := parseHiveVersion(banner) + if gotVer != want.version { + t.Fatalf("parseHiveVersion(%q) = %q, want %q", banner, gotVer, want.version) + } + var state driverVariantState + if err := state.resolve("hive", connection.ConnectionConfig{Type: "hive"}, gotVer); err != nil { + t.Fatalf("resolve(%q): %v", gotVer, err) + } + gotVariant, _ := state.DriverVariantInfo() + if gotVariant != want.variant { + t.Fatalf("variant for %q = %q, want %q", banner, gotVariant, want.variant) + } + } +} + +func TestParseHiveDescribeRows(t *testing.T) { + rows := []map[string]interface{}{ + {"col_name": "order_id", "data_type": "string", "comment": "订单号"}, + {"col_name": "qty", "data_type": "int", "comment": "from deserializer"}, + {"col_name": "dt", "data_type": "string", "comment": "分区日期"}, + {"col_name": "", "data_type": nil, "comment": nil}, + {"col_name": "# Partition Information", "data_type": nil, "comment": nil}, + {"col_name": "# col_name", "data_type": "data_type", "comment": "comment"}, + {"col_name": "dt", "data_type": "string", "comment": "分区日期"}, + } + cols := parseHiveDescribeRows(rows) + if len(cols) != 3 { + t.Fatalf("expected 3 columns, got %d: %#v", len(cols), cols) + } + if cols[0].Name != "order_id" || cols[0].Comment != "订单号" || cols[0].Extra != "" { + t.Fatalf("unexpected first column: %#v", cols[0]) + } + if cols[1].Name != "qty" || cols[1].Comment != "" { + t.Fatalf("expected 'from deserializer' comment to be stripped: %#v", cols[1]) + } + if cols[2].Name != "dt" || cols[2].Extra != "PARTITION KEY" { + t.Fatalf("expected dt to be marked as PARTITION KEY: %#v", cols[2]) + } +} + +func TestHiveBindArgsAndNormalizeStatement(t *testing.T) { + if got := normalizeHiveStatement("SELECT * FROM `dw`.`ods_orders`; \n\t"); got != "SELECT * FROM `dw`.`ods_orders`" { + t.Fatalf("unexpected normalized statement: %q", got) + } + bound, err := hiveBindArgs("/* comment ? */ SELECT * FROM `t?` WHERE note = '?' AND id = ? AND active = ? AND tag = ?", []any{42, true, "O'Reilly"}) + if err != nil { + t.Fatalf("hiveBindArgs error: %v", err) + } + want := `/* comment ? */ SELECT * FROM ` + "`t?`" + ` WHERE note = '?' AND id = 42 AND active = TRUE AND tag = 'O\'Reilly'` + if bound != want { + t.Fatalf("hiveBindArgs = %q, want %q", bound, want) + } +} + +func TestHiveThriftColumnarRowSetDecode(t *testing.T) { + w := newHiveWriter() + w.writeFieldBegin(thriftTypeI64, 1) // startRowOffset + w.writeI64(0) + w.writeFieldBegin(thriftTypeList, 3) // columns + w.buf.WriteByte(thriftTypeStruct) + w.writeI32(2) // 2 columns + + // Column 0: i32Val [10, 20], second is NULL + w.writeFieldBegin(thriftTypeStruct, 4) // i32Val + w.writeFieldBegin(thriftTypeList, 1) + w.buf.WriteByte(thriftTypeI32) + w.writeI32(2) + w.writeI32(10) + w.writeI32(20) + w.writeFieldBegin(thriftTypeString, 2) // nulls bitmap: row 1 is null (bit 1 -> 0x02) + w.writeBytes([]byte{0x02}) + w.writeFieldStop() + w.writeFieldStop() + + // Column 1: stringVal ["alpha", "beta"], none NULL + w.writeFieldBegin(thriftTypeStruct, 7) // stringVal + w.writeFieldBegin(thriftTypeList, 1) + w.buf.WriteByte(thriftTypeString) + w.writeI32(2) + w.writeString("alpha") + w.writeString("beta") + w.writeFieldBegin(thriftTypeString, 2) + w.writeBytes([]byte{0x00}) + w.writeFieldStop() + w.writeFieldStop() + + w.writeFieldStop() // end TRowSet + + rows, err := readHiveRowSet(newHiveReader(bytes.NewReader(w.bytes()))) + if err != nil { + t.Fatalf("readHiveRowSet error: %v", err) + } + if len(rows) != 2 || rows[0][0] != int64(10) || rows[0][1] != "alpha" || rows[1][0] != nil || rows[1][1] != "beta" { + t.Fatalf("unexpected decoded rows: %#v", rows) + } +} diff --git a/internal/db/hive_live_test.go b/internal/db/hive_live_test.go new file mode 100644 index 000000000..3585a3c56 --- /dev/null +++ b/internal/db/hive_live_test.go @@ -0,0 +1,187 @@ +//go:build gonavi_full_drivers || gonavi_hive_driver + +package db + +import ( + "os" + "path/filepath" + "slices" + "strconv" + "strings" + "testing" + + "GoNavi-Wails/internal/connection" +) + +func TestHiveLiveSmoke(t *testing.T) { + configs := hiveLiveConfigs(t) + for _, tc := range configs { + t.Run(tc.name, func(t *testing.T) { + client := &HiveDB{} + if err := client.Connect(tc.config); err != nil { + t.Fatalf("connect: %v", err) + } + defer client.Close() + + variant, version := client.DriverVariantInfo() + t.Logf("Hive server %s resolved to variant %s", version, variant) + if variant == "" || version == "" { + t.Fatal("variant and version must be resolved from SELECT version()") + } + + if err := client.Ping(); err != nil { + t.Fatalf("ping: %v", err) + } + + databases, err := client.GetDatabases() + if err != nil || !slices.Contains(databases, "default") { + t.Fatalf("databases %v: %v", databases, err) + } + + targetDB := tc.config.Database + if slices.Contains(databases, "dw") { + targetDB = "dw" + } + tables, err := client.GetTables(targetDB) + if err != nil { + t.Fatalf("GetTables(%s): %v", targetDB, err) + } + t.Logf("tables in %s: %v", targetDB, tables) + + if slices.Contains(tables, "ods_orders") { + cols, err := client.GetColumns(targetDB, "ods_orders") + if err != nil || len(cols) == 0 { + t.Fatalf("GetColumns(ods_orders) %#v: %v", cols, err) + } + var foundPartition bool + for _, col := range cols { + if col.Name == "dt" && col.Extra == "PARTITION KEY" { + foundPartition = true + } + } + if !foundPartition { + t.Fatalf("expected dt column to have PARTITION KEY extra, got %#v", cols) + } + + ddl, err := client.GetCreateStatement(targetDB, "ods_orders") + if err != nil || !strings.Contains(strings.ToUpper(ddl), "CREATE") { + t.Fatalf("GetCreateStatement(ods_orders) %q: %v", ddl, err) + } + + rows, colNames, err := client.Query("SELECT order_id, user_id, qty FROM `" + targetDB + "`.`ods_orders` LIMIT 5 OFFSET 0") + if err != nil { + t.Fatalf("Query ods_orders: %v", err) + } + t.Logf("queried %d rows from ods_orders, columns=%v", len(rows), colNames) + if slices.Contains(colNames, "ods_orders.order_id") { + t.Fatalf("expected unqualified column names, got %v", colNames) + } + + allCols, err := client.GetAllColumns(targetDB) + if err != nil || len(allCols) == 0 { + t.Fatalf("GetAllColumns(%s) %v: %v", targetDB, allCols, err) + } + t.Logf("GetAllColumns(%s) returned %d columns", targetDB, len(allCols)) + } + + _, _ = client.Exec("DROP VIEW IF EXISTS `default`.`gonavi_hive_smoke_v`") + _, _ = client.Exec("DROP TABLE IF EXISTS `default`.`gonavi_hive_smoke_t`") + mustExec(t, client, "CREATE TABLE `default`.`gonavi_hive_smoke_t` (`id` INT COMMENT '主键', `name` STRING COMMENT '名称')") + defer func() { _, _ = client.Exec("DROP TABLE IF EXISTS `default`.`gonavi_hive_smoke_t`") }() + + if err := client.ApplyChanges("`default`.`gonavi_hive_smoke_t`", connection.ChangeSet{ + Inserts: []map[string]interface{}{ + {"id": 1, "name": "alice"}, + {"id": 2, "name": "bob"}, + }, + }); err != nil { + t.Fatalf("ApplyChanges insert: %v", err) + } + if err := client.ApplyChanges("`default`.`gonavi_hive_smoke_t`", connection.ChangeSet{ + Deletes: []map[string]interface{}{{"id": 1}}, + }); err == nil { + t.Fatal("expected single-row delete in ApplyChanges to be rejected for Hive") + } + + mustExec(t, client, "CREATE VIEW `default`.`gonavi_hive_smoke_v` AS SELECT `id`, `name` FROM `default`.`gonavi_hive_smoke_t` WHERE `id` >= 1") + defer func() { _, _ = client.Exec("DROP VIEW IF EXISTS `default`.`gonavi_hive_smoke_v`") }() + + defaultTables, err := client.GetTables("default") + if err != nil || !slices.Contains(defaultTables, "gonavi_hive_smoke_t") || slices.Contains(defaultTables, "gonavi_hive_smoke_v") { + t.Fatalf("default tables (must include table and exclude view) %v: %v", defaultTables, err) + } + + viewDDL, err := client.GetCreateStatement("default", "gonavi_hive_smoke_v") + if err != nil || !strings.Contains(strings.ToUpper(viewDDL), "CREATE VIEW") { + t.Fatalf("GetCreateStatement(view) %q: %v", viewDDL, err) + } + + paged, _, err := client.QueryContextWithArgs(t.Context(), "SELECT `name` FROM `default`.`gonavi_hive_smoke_t` WHERE `id` >= ? ORDER BY `id` LIMIT 1 OFFSET 1", []any{1}) + if err != nil || len(paged) != 1 || paged[0]["name"] != "bob" { + t.Fatalf("paged query with args %v: %v", paged, err) + } + }) + } +} + +type namedHiveLiveConfig struct { + name string + config connection.ConnectionConfig +} + +func hiveLiveConfigs(t *testing.T) []namedHiveLiveConfig { + t.Helper() + var list []namedHiveLiveConfig + if rawAddrs := strings.TrimSpace(os.Getenv("GONAVI_HIVE_TEST_ADDRS")); rawAddrs != "" { + for _, addr := range strings.Split(rawAddrs, ",") { + cfg := liveConfig(t, "hive", addr, "hive", "default") + cfg.Timeout = 30 + list = append(list, namedHiveLiveConfig{name: addr, config: cfg}) + } + } + if sshSpec := strings.TrimSpace(os.Getenv("GONAVI_HIVE_TEST_SSH")); sshSpec != "" { + // 格式:user@host:port(例如 lskam@frp-fun.com:49692) + user := "root" + hostPort := sshSpec + if at := strings.Index(sshSpec, "@"); at > 0 { + user = sshSpec[:at] + hostPort = sshSpec[at+1:] + } + sshHost, sshPortText, ok := strings.Cut(hostPort, ":") + sshPort := 22 + if ok { + if p, err := strconv.Atoi(sshPortText); err == nil && p > 0 { + sshPort = p + } + } else { + sshHost = hostPort + } + home, _ := os.UserHomeDir() + keyPath := os.Getenv("GONAVI_HIVE_TEST_SSH_KEY") + if keyPath == "" && home != "" { + keyPath = filepath.Join(home, ".ssh", "id_ed25519") + } + list = append(list, namedHiveLiveConfig{ + name: "ssh-" + sshSpec, + config: connection.ConnectionConfig{ + Type: "hive", + Host: "127.0.0.1", + Port: 10000, + User: "hive", + Database: "dw", + Timeout: 30, + UseSSH: true, + SSH: connection.SSHConfig{ + Host: sshHost, + Port: sshPort, + User: user, + KeyPath: keyPath, + }, + }, + }) + } + if len(list) == 0 { + t.Skip("set GONAVI_HIVE_TEST_ADDRS=host:port or GONAVI_HIVE_TEST_SSH=user@host:port to run Hive live tests") + } + return list +} diff --git a/internal/db/hive_protocol.go b/internal/db/hive_protocol.go new file mode 100644 index 000000000..1d287034d --- /dev/null +++ b/internal/db/hive_protocol.go @@ -0,0 +1,564 @@ +//go:build gonavi_full_drivers || gonavi_hive_driver + +package db + +import ( + "context" + "fmt" + "strings" +) + +// HiveServer2 TCLIService 协议常量定义。 +const ( + hiveProtocolV10 int32 = 9 // HIVE_CLI_SERVICE_PROTOCOL_V10 (Hive 3.x) + hiveProtocolV11 int32 = 10 // HIVE_CLI_SERVICE_PROTOCOL_V11 (Hive 4.x) + + hiveStatusSuccess int32 = 0 + hiveStatusSuccessWithInfo int32 = 1 + hiveStatusStillExecuting int32 = 2 + hiveStatusError int32 = 3 + hiveStatusInvalidHandle int32 = 4 + + hiveOpInitialized int32 = 0 + hiveOpRunning int32 = 1 + hiveOpFinished int32 = 2 + hiveOpCanceled int32 = 3 + hiveOpClosed int32 = 4 + hiveOpError int32 = 5 + hiveOpUnknown int32 = 6 + hiveOpPending int32 = 7 + hiveOpTimedout int32 = 8 + + hiveFetchNext int32 = 0 + hiveFetchFirst int32 = 4 + + defaultHiveFetchBatchSize int64 = 1000 +) + +type hiveHandleID struct { + guid []byte + secret []byte +} + +type hiveOperationHandle struct { + id hiveHandleID + opType int32 + hasResultSet bool + modifiedRowCount float64 + hasModifiedCount bool +} + +type hiveStatus struct { + statusCode int32 + infoMessages []string + sqlState string + errorCode int32 + errorMessage string +} + +type hiveOperationStatus struct { + status hiveStatus + operationState int32 + sqlState string + errorCode int32 + errorMessage string + hasResultSet bool +} + +// hiveQueryError 表示 HiveServer2 服务端返回的 SQL 执行错误。 +type hiveQueryError struct { + sqlState string + errorCode int32 + message string +} + +func (e *hiveQueryError) Error() string { + state := strings.TrimSpace(e.sqlState) + if state == "" { + state = "HY000" + } + msg := strings.TrimSpace(e.message) + if msg == "" { + msg = "unknown Hive error" + } + return localizedDatabaseRuntimeError("db.backend.error.hive_query_failed", map[string]any{ + "sqlState": state, + "code": e.errorCode, + "message": msg, + }).Error() +} + +func (s hiveStatus) toError() error { + if s.statusCode == hiveStatusSuccess || s.statusCode == hiveStatusSuccessWithInfo || s.statusCode == hiveStatusStillExecuting { + return nil + } + msg := strings.TrimSpace(s.errorMessage) + if msg == "" && len(s.infoMessages) > 0 { + msg = strings.TrimSpace(strings.Join(s.infoMessages, "; ")) + } + if msg == "" { + msg = fmt.Sprintf("Hive status code %d", s.statusCode) + } + return &hiveQueryError{ + sqlState: s.sqlState, + errorCode: s.errorCode, + message: msg, + } +} + +func writeHiveHandleIdentifier(w *hiveWriter, id hiveHandleID) { + w.writeFieldBegin(thriftTypeString, 1) + w.writeBytes(id.guid) + w.writeFieldBegin(thriftTypeString, 2) + w.writeBytes(id.secret) + w.writeFieldStop() +} + +func writeHiveSessionHandle(w *hiveWriter, sessionID hiveHandleID) { + w.writeFieldBegin(thriftTypeStruct, 1) + writeHiveHandleIdentifier(w, sessionID) + w.writeFieldStop() +} + +func writeHiveOperationHandle(w *hiveWriter, op hiveOperationHandle) { + w.writeFieldBegin(thriftTypeStruct, 1) + writeHiveHandleIdentifier(w, op.id) + w.writeFieldBegin(thriftTypeI32, 2) + w.writeI32(op.opType) + w.writeFieldBegin(thriftTypeBool, 3) + w.writeBool(op.hasResultSet) + if op.hasModifiedCount { + w.writeFieldBegin(thriftTypeDouble, 4) + w.writeDouble(op.modifiedRowCount) + } + w.writeFieldStop() +} + +func readHiveHandleIdentifier(r *hiveReader) (hiveHandleID, error) { + var id hiveHandleID + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return id, err + } + if ft == thriftTypeStop { + return id, nil + } + switch { + case fid == 1 && ft == thriftTypeString: + if id.guid, err = r.readBytes(); err != nil { + return id, err + } + case fid == 2 && ft == thriftTypeString: + if id.secret, err = r.readBytes(); err != nil { + return id, err + } + default: + if err := r.skip(ft); err != nil { + return id, err + } + } + } +} + +func readHiveSessionHandle(r *hiveReader) (hiveHandleID, error) { + var id hiveHandleID + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return id, err + } + if ft == thriftTypeStop { + return id, nil + } + if fid == 1 && ft == thriftTypeStruct { + if id, err = readHiveHandleIdentifier(r); err != nil { + return id, err + } + } else if err := r.skip(ft); err != nil { + return id, err + } + } +} + +func readHiveOperationHandle(r *hiveReader) (hiveOperationHandle, error) { + var op hiveOperationHandle + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return op, err + } + if ft == thriftTypeStop { + return op, nil + } + switch { + case fid == 1 && ft == thriftTypeStruct: + if op.id, err = readHiveHandleIdentifier(r); err != nil { + return op, err + } + case fid == 2 && ft == thriftTypeI32: + if op.opType, err = r.readI32(); err != nil { + return op, err + } + case fid == 3 && ft == thriftTypeBool: + if op.hasResultSet, err = r.readBool(); err != nil { + return op, err + } + case fid == 4 && ft == thriftTypeDouble: + if op.modifiedRowCount, err = r.readDouble(); err != nil { + return op, err + } + op.hasModifiedCount = true + default: + if err := r.skip(ft); err != nil { + return op, err + } + } + } +} + +func readHiveStatus(r *hiveReader) (hiveStatus, error) { + var st hiveStatus + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return st, err + } + if ft == thriftTypeStop { + return st, nil + } + switch { + case fid == 1 && ft == thriftTypeI32: + if st.statusCode, err = r.readI32(); err != nil { + return st, err + } + case fid == 2 && ft == thriftTypeList: + et, size, err := r.readListBegin() + if err != nil { + return st, err + } + for i := 0; i < size; i++ { + if et == thriftTypeString { + s, err := r.readString() + if err != nil { + return st, err + } + st.infoMessages = append(st.infoMessages, s) + } else if err := r.skip(et); err != nil { + return st, err + } + } + case fid == 3 && ft == thriftTypeString: + if st.sqlState, err = r.readString(); err != nil { + return st, err + } + case fid == 4 && ft == thriftTypeI32: + if st.errorCode, err = r.readI32(); err != nil { + return st, err + } + case fid == 5 && ft == thriftTypeString: + if st.errorMessage, err = r.readString(); err != nil { + return st, err + } + default: + if err := r.skip(ft); err != nil { + return st, err + } + } + } +} + +func readSingleStructField(r *hiveReader, decode func(*hiveReader) error) error { + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return wrapHiveProtocolError(err) + } + if ft == thriftTypeStop { + return nil + } + if fid == 0 && ft == thriftTypeStruct { + if err := decode(r); err != nil { + return err + } + } else if err := r.skip(ft); err != nil { + return wrapHiveProtocolError(err) + } + } +} + +// openSession 调用 TCLIService.OpenSession 打开会话并协商协议版本。 +func (t *hiveTransport) openSession(ctx context.Context, clientProtocol int32, username, password string, conf map[string]string) (hiveHandleID, int32, error) { + var sessionID hiveHandleID + var serverProtocol int32 + var status hiveStatus + err := t.call(ctx, "OpenSession", func(w *hiveWriter) { + w.writeFieldBegin(thriftTypeStruct, 1) // req + w.writeFieldBegin(thriftTypeI32, 1) // client_protocol + w.writeI32(clientProtocol) + if username != "" { + w.writeFieldBegin(thriftTypeString, 2) + w.writeString(username) + } + if password != "" { + w.writeFieldBegin(thriftTypeString, 3) + w.writeString(password) + } + if len(conf) > 0 { + w.writeFieldBegin(thriftTypeMap, 4) + w.writeStringMap(conf) + } + w.writeFieldStop() + }, func(r *hiveReader) error { + return readSingleStructField(r, func(r *hiveReader) error { + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return wrapHiveProtocolError(err) + } + if ft == thriftTypeStop { + return nil + } + switch { + case fid == 1 && ft == thriftTypeStruct: + if status, err = readHiveStatus(r); err != nil { + return wrapHiveProtocolError(err) + } + case fid == 2 && ft == thriftTypeI32: + if serverProtocol, err = r.readI32(); err != nil { + return wrapHiveProtocolError(err) + } + case fid == 3 && ft == thriftTypeStruct: + if sessionID, err = readHiveSessionHandle(r); err != nil { + return wrapHiveProtocolError(err) + } + default: + if err := r.skip(ft); err != nil { + return wrapHiveProtocolError(err) + } + } + } + }) + }) + if err != nil { + return sessionID, 0, err + } + if err := status.toError(); err != nil { + return sessionID, 0, err + } + return sessionID, serverProtocol, nil +} + +func (t *hiveTransport) closeSession(ctx context.Context, sessionID hiveHandleID) error { + var status hiveStatus + err := t.call(ctx, "CloseSession", func(w *hiveWriter) { + w.writeFieldBegin(thriftTypeStruct, 1) // req + w.writeFieldBegin(thriftTypeStruct, 1) // sessionHandle + writeHiveSessionHandle(w, sessionID) + w.writeFieldStop() + }, func(r *hiveReader) error { + return readSingleStructField(r, func(r *hiveReader) error { + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return wrapHiveProtocolError(err) + } + if ft == thriftTypeStop { + return nil + } + if fid == 1 && ft == thriftTypeStruct { + if status, err = readHiveStatus(r); err != nil { + return wrapHiveProtocolError(err) + } + } else if err := r.skip(ft); err != nil { + return wrapHiveProtocolError(err) + } + } + }) + }) + if err != nil { + return err + } + return status.toError() +} + +func (t *hiveTransport) executeStatement(ctx context.Context, sessionID hiveHandleID, statement string, confOverlay map[string]string, runAsync bool, queryTimeoutSec int64) (hiveOperationHandle, error) { + var op hiveOperationHandle + var status hiveStatus + err := t.call(ctx, "ExecuteStatement", func(w *hiveWriter) { + w.writeFieldBegin(thriftTypeStruct, 1) // req + w.writeFieldBegin(thriftTypeStruct, 1) // sessionHandle + writeHiveSessionHandle(w, sessionID) + w.writeFieldBegin(thriftTypeString, 2) // statement + w.writeString(statement) + if len(confOverlay) > 0 { + w.writeFieldBegin(thriftTypeMap, 3) + w.writeStringMap(confOverlay) + } + w.writeFieldBegin(thriftTypeBool, 4) // runAsync + w.writeBool(runAsync) + if queryTimeoutSec > 0 { + w.writeFieldBegin(thriftTypeI64, 5) // queryTimeout + w.writeI64(queryTimeoutSec) + } + w.writeFieldStop() + }, func(r *hiveReader) error { + return readSingleStructField(r, func(r *hiveReader) error { + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return wrapHiveProtocolError(err) + } + if ft == thriftTypeStop { + return nil + } + switch { + case fid == 1 && ft == thriftTypeStruct: + if status, err = readHiveStatus(r); err != nil { + return wrapHiveProtocolError(err) + } + case fid == 2 && ft == thriftTypeStruct: + if op, err = readHiveOperationHandle(r); err != nil { + return wrapHiveProtocolError(err) + } + default: + if err := r.skip(ft); err != nil { + return wrapHiveProtocolError(err) + } + } + } + }) + }) + if err != nil { + return op, err + } + if err := status.toError(); err != nil { + return op, err + } + return op, nil +} + +func (t *hiveTransport) getOperationStatus(ctx context.Context, op hiveOperationHandle) (hiveOperationStatus, error) { + var resp hiveOperationStatus + err := t.call(ctx, "GetOperationStatus", func(w *hiveWriter) { + w.writeFieldBegin(thriftTypeStruct, 1) + w.writeFieldBegin(thriftTypeStruct, 1) + writeHiveOperationHandle(w, op) + w.writeFieldStop() + }, func(r *hiveReader) error { + return readSingleStructField(r, func(r *hiveReader) error { + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return wrapHiveProtocolError(err) + } + if ft == thriftTypeStop { + return nil + } + switch { + case fid == 1 && ft == thriftTypeStruct: + if resp.status, err = readHiveStatus(r); err != nil { + return wrapHiveProtocolError(err) + } + case fid == 2 && ft == thriftTypeI32: + if resp.operationState, err = r.readI32(); err != nil { + return wrapHiveProtocolError(err) + } + case fid == 3 && ft == thriftTypeString: + if resp.sqlState, err = r.readString(); err != nil { + return wrapHiveProtocolError(err) + } + case fid == 4 && ft == thriftTypeI32: + if resp.errorCode, err = r.readI32(); err != nil { + return wrapHiveProtocolError(err) + } + case fid == 5 && ft == thriftTypeString: + if resp.errorMessage, err = r.readString(); err != nil { + return wrapHiveProtocolError(err) + } + case fid == 9 && ft == thriftTypeBool: + if resp.hasResultSet, err = r.readBool(); err != nil { + return wrapHiveProtocolError(err) + } + default: + if err := r.skip(ft); err != nil { + return wrapHiveProtocolError(err) + } + } + } + }) + }) + if err != nil { + return resp, err + } + if err := resp.status.toError(); err != nil { + return resp, err + } + return resp, nil +} + +func (t *hiveTransport) cancelOperation(ctx context.Context, op hiveOperationHandle) error { + var status hiveStatus + err := t.call(ctx, "CancelOperation", func(w *hiveWriter) { + w.writeFieldBegin(thriftTypeStruct, 1) + w.writeFieldBegin(thriftTypeStruct, 1) + writeHiveOperationHandle(w, op) + w.writeFieldStop() + }, func(r *hiveReader) error { + return readSingleStructField(r, func(r *hiveReader) error { + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return wrapHiveProtocolError(err) + } + if ft == thriftTypeStop { + return nil + } + if fid == 1 && ft == thriftTypeStruct { + if status, err = readHiveStatus(r); err != nil { + return wrapHiveProtocolError(err) + } + } else if err := r.skip(ft); err != nil { + return wrapHiveProtocolError(err) + } + } + }) + }) + if err != nil { + return err + } + return status.toError() +} + +func (t *hiveTransport) closeOperation(ctx context.Context, op hiveOperationHandle) error { + var status hiveStatus + err := t.call(ctx, "CloseOperation", func(w *hiveWriter) { + w.writeFieldBegin(thriftTypeStruct, 1) + w.writeFieldBegin(thriftTypeStruct, 1) + writeHiveOperationHandle(w, op) + w.writeFieldStop() + }, func(r *hiveReader) error { + return readSingleStructField(r, func(r *hiveReader) error { + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return wrapHiveProtocolError(err) + } + if ft == thriftTypeStop { + return nil + } + if fid == 1 && ft == thriftTypeStruct { + if status, err = readHiveStatus(r); err != nil { + return wrapHiveProtocolError(err) + } + } else if err := r.skip(ft); err != nil { + return wrapHiveProtocolError(err) + } + } + }) + }) + if err != nil { + return err + } + return status.toError() +} diff --git a/internal/db/hive_rows.go b/internal/db/hive_rows.go new file mode 100644 index 000000000..1df15b906 --- /dev/null +++ b/internal/db/hive_rows.go @@ -0,0 +1,561 @@ +//go:build gonavi_full_drivers || gonavi_hive_driver + +package db + +import ( + "context" + "fmt" + "strings" +) + +type hiveColumnMeta struct { + name string + typeName string + position int32 + comment string +} + +type hiveRowSink interface { + onColumns(names []string) error + onRow(values []any) error +} + +func (t *hiveTransport) getResultSetMetadata(ctx context.Context, op hiveOperationHandle) ([]hiveColumnMeta, error) { + var cols []hiveColumnMeta + var status hiveStatus + err := t.call(ctx, "GetResultSetMetadata", func(w *hiveWriter) { + w.writeFieldBegin(thriftTypeStruct, 1) + w.writeFieldBegin(thriftTypeStruct, 1) + writeHiveOperationHandle(w, op) + w.writeFieldStop() + }, func(r *hiveReader) error { + return readSingleStructField(r, func(r *hiveReader) error { + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return wrapHiveProtocolError(err) + } + if ft == thriftTypeStop { + return nil + } + switch { + case fid == 1 && ft == thriftTypeStruct: + if status, err = readHiveStatus(r); err != nil { + return wrapHiveProtocolError(err) + } + case fid == 2 && ft == thriftTypeStruct: + if cols, err = readHiveTableSchema(r); err != nil { + return wrapHiveProtocolError(err) + } + default: + if err := r.skip(ft); err != nil { + return wrapHiveProtocolError(err) + } + } + } + }) + }) + if err != nil { + return nil, err + } + if err := status.toError(); err != nil { + return nil, err + } + return cols, nil +} + +func (t *hiveTransport) fetchResults(ctx context.Context, op hiveOperationHandle, orientation int32, maxRows int64) ([][]any, error) { + var rows [][]any + var status hiveStatus + err := t.call(ctx, "FetchResults", func(w *hiveWriter) { + w.writeFieldBegin(thriftTypeStruct, 1) // req + w.writeFieldBegin(thriftTypeStruct, 1) // operationHandle + writeHiveOperationHandle(w, op) + w.writeFieldBegin(thriftTypeI32, 2) // orientation + w.writeI32(orientation) + w.writeFieldBegin(thriftTypeI64, 3) // maxRows + w.writeI64(maxRows) + w.writeFieldBegin(thriftTypeI16, 4) // fetchType = 0 (query output) + w.writeI16(0) + w.writeFieldStop() + }, func(r *hiveReader) error { + return readSingleStructField(r, func(r *hiveReader) error { + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return wrapHiveProtocolError(err) + } + if ft == thriftTypeStop { + return nil + } + switch { + case fid == 1 && ft == thriftTypeStruct: + if status, err = readHiveStatus(r); err != nil { + return wrapHiveProtocolError(err) + } + case fid == 3 && ft == thriftTypeStruct: + if rows, err = readHiveRowSet(r); err != nil { + return wrapHiveProtocolError(err) + } + default: + if err := r.skip(ft); err != nil { + return wrapHiveProtocolError(err) + } + } + } + }) + }) + if err != nil { + return nil, err + } + if err := status.toError(); err != nil { + return nil, err + } + return rows, nil +} + +func readHiveTableSchema(r *hiveReader) ([]hiveColumnMeta, error) { + var cols []hiveColumnMeta + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return nil, err + } + if ft == thriftTypeStop { + return cols, nil + } + if fid == 1 && ft == thriftTypeList { + et, size, err := r.readListBegin() + if err != nil { + return nil, err + } + cols = make([]hiveColumnMeta, 0, size) + for i := 0; i < size; i++ { + if et != thriftTypeStruct { + if err := r.skip(et); err != nil { + return nil, err + } + continue + } + col, err := readHiveColumnDesc(r) + if err != nil { + return nil, err + } + cols = append(cols, col) + } + } else if err := r.skip(ft); err != nil { + return nil, err + } + } +} + +func readHiveColumnDesc(r *hiveReader) (hiveColumnMeta, error) { + var col hiveColumnMeta + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return col, err + } + if ft == thriftTypeStop { + return col, nil + } + switch { + case fid == 1 && ft == thriftTypeString: + if col.name, err = r.readString(); err != nil { + return col, err + } + case fid == 2 && ft == thriftTypeStruct: + if col.typeName, err = readHiveTypeDesc(r); err != nil { + return col, err + } + case fid == 3 && ft == thriftTypeI32: + if col.position, err = r.readI32(); err != nil { + return col, err + } + case fid == 4 && ft == thriftTypeString: + if col.comment, err = r.readString(); err != nil { + return col, err + } + default: + if err := r.skip(ft); err != nil { + return col, err + } + } + } +} + +func readHiveTypeDesc(r *hiveReader) (string, error) { + var typeName string + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return "", err + } + if ft == thriftTypeStop { + return typeName, nil + } + if fid == 1 && ft == thriftTypeList { + et, size, err := r.readListBegin() + if err != nil { + return "", err + } + for i := 0; i < size; i++ { + if et != thriftTypeStruct { + if err := r.skip(et); err != nil { + return "", err + } + continue + } + name, err := readHiveTypeEntry(r) + if err != nil { + return "", err + } + if i == 0 { + typeName = name + } + } + } else if err := r.skip(ft); err != nil { + return "", err + } + } +} + +func readHiveTypeEntry(r *hiveReader) (string, error) { + var typeName string + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return "", err + } + if ft == thriftTypeStop { + return typeName, nil + } + if fid == 1 && ft == thriftTypeStruct { + for { + pft, pfid, err := r.readFieldBegin() + if err != nil { + return "", err + } + if pft == thriftTypeStop { + break + } + if pfid == 1 && pft == thriftTypeI32 { + typeID, err := r.readI32() + if err != nil { + return "", err + } + typeName = hiveTypeIDName(typeID) + } else if err := r.skip(pft); err != nil { + return "", err + } + } + } else if err := r.skip(ft); err != nil { + return "", err + } + } +} + +func hiveTypeIDName(id int32) string { + switch id { + case 0: + return "BOOLEAN" + case 1: + return "TINYINT" + case 2: + return "SMALLINT" + case 3: + return "INT" + case 4: + return "BIGINT" + case 5: + return "FLOAT" + case 6: + return "DOUBLE" + case 7: + return "STRING" + case 8: + return "TIMESTAMP" + case 9: + return "BINARY" + case 10: + return "ARRAY" + case 11: + return "MAP" + case 12: + return "STRUCT" + case 13: + return "UNIONTYPE" + case 15: + return "DECIMAL" + case 17: + return "DATE" + case 18: + return "VARCHAR" + case 19: + return "CHAR" + case 20: + return "INTERVAL_YEAR_MONTH" + case 21: + return "INTERVAL_DAY_TIME" + case 22: + return "TIMESTAMPLOCALTZ" + default: + return "STRING" + } +} + +func readHiveRowSet(r *hiveReader) ([][]any, error) { + var legacyRows [][]any + var columns [][]any + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return nil, err + } + if ft == thriftTypeStop { + break + } + switch { + case fid == 2 && ft == thriftTypeList: + et, size, err := r.readListBegin() + if err != nil { + return nil, err + } + for i := 0; i < size; i++ { + if et == thriftTypeStruct { + row, err := readHiveLegacyRow(r) + if err != nil { + return nil, err + } + legacyRows = append(legacyRows, row) + } else if err := r.skip(et); err != nil { + return nil, err + } + } + case fid == 3 && ft == thriftTypeList: + et, size, err := r.readListBegin() + if err != nil { + return nil, err + } + columns = make([][]any, 0, size) + for i := 0; i < size; i++ { + if et == thriftTypeStruct { + colVals, err := readHiveColumn(r) + if err != nil { + return nil, err + } + columns = append(columns, colVals) + } else if err := r.skip(et); err != nil { + return nil, err + } + } + default: + if err := r.skip(ft); err != nil { + return nil, err + } + } + } + if len(columns) > 0 { + rowCount := len(columns[0]) + rows := make([][]any, rowCount) + for rowIdx := 0; rowIdx < rowCount; rowIdx++ { + row := make([]any, len(columns)) + for colIdx, col := range columns { + if rowIdx < len(col) { + row[colIdx] = col[rowIdx] + } + } + rows[rowIdx] = row + } + return rows, nil + } + return legacyRows, nil +} + +func readHiveColumn(r *hiveReader) ([]any, error) { + var values []any + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return nil, err + } + if ft == thriftTypeStop { + return values, nil + } + if ft != thriftTypeStruct || fid < 1 || fid > 8 { + if err := r.skip(ft); err != nil { + return nil, err + } + continue + } + vals, nulls, err := readHiveTypedColumn(r, fid) + if err != nil { + return nil, err + } + for i := range vals { + if isHiveNull(nulls, i) { + vals[i] = nil + } + } + values = vals + } +} + +func readHiveTypedColumn(r *hiveReader, kind int16) ([]any, []byte, error) { + var vals []any + var nulls []byte + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return nil, nil, err + } + if ft == thriftTypeStop { + return vals, nulls, nil + } + switch { + case fid == 1 && ft == thriftTypeList: + _, size, err := r.readListBegin() + if err != nil { + return nil, nil, err + } + vals = make([]any, size) + for i := 0; i < size; i++ { + val, err := readHiveColumnElement(r, kind) + if err != nil { + return nil, nil, err + } + vals[i] = val + } + case fid == 2 && ft == thriftTypeString: + if nulls, err = r.readBytes(); err != nil { + return nil, nil, err + } + default: + if err := r.skip(ft); err != nil { + return nil, nil, err + } + } + } +} + +func readHiveColumnElement(r *hiveReader, kind int16) (any, error) { + switch kind { + case 1: + return r.readBool() + case 2: + b, err := r.readByte() + return int64(int8(b)), err + case 3: + v, err := r.readI16() + return int64(v), err + case 4: + v, err := r.readI32() + return int64(v), err + case 5: + return r.readI64() + case 6: + return r.readDouble() + case 7: + return r.readString() + case 8: + b, err := r.readBytes() + return string(b), err + default: + return nil, fmt.Errorf("unsupported Hive column kind %d", kind) + } +} + +func isHiveNull(nulls []byte, rowIdx int) bool { + byteIdx := rowIdx / 8 + if byteIdx >= len(nulls) { + return false + } + return (nulls[byteIdx] & (1 << (uint(rowIdx) % 8))) != 0 +} + +func readHiveLegacyRow(r *hiveReader) ([]any, error) { + var row []any + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return nil, err + } + if ft == thriftTypeStop { + return row, nil + } + if fid == 1 && ft == thriftTypeList { + et, size, err := r.readListBegin() + if err != nil { + return nil, err + } + row = make([]any, 0, size) + for i := 0; i < size; i++ { + if et == thriftTypeStruct { + val, err := readHiveLegacyColumnValue(r) + if err != nil { + return nil, err + } + row = append(row, val) + } else if err := r.skip(et); err != nil { + return nil, err + } + } + } else if err := r.skip(ft); err != nil { + return nil, err + } + } +} + +func readHiveLegacyColumnValue(r *hiveReader) (any, error) { + var result any + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return nil, err + } + if ft == thriftTypeStop { + return result, nil + } + if ft != thriftTypeStruct { + if err := r.skip(ft); err != nil { + return nil, err + } + continue + } + for { + vft, vfid, err := r.readFieldBegin() + if err != nil { + return nil, err + } + if vft == thriftTypeStop { + break + } + if vfid == 1 { + result, err = readHiveColumnElement(r, fid) + if err != nil { + return nil, err + } + } else if err := r.skip(vft); err != nil { + return nil, err + } + } + } +} + +func uniqueHiveColumnNames(cols []hiveColumnMeta) []string { + names := make([]string, len(cols)) + used := make(map[string]int, len(cols)) + for i, col := range cols { + name := strings.TrimSpace(col.name) + if name == "" { + name = fmt.Sprintf("_c%d", i) + } + count := used[name] + used[name] = count + 1 + if count > 0 { + name = fmt.Sprintf("%s_%d", name, count+1) + } + names[i] = name + } + return names +} diff --git a/internal/db/hive_thrift.go b/internal/db/hive_thrift.go new file mode 100644 index 000000000..5db258b1f --- /dev/null +++ b/internal/db/hive_thrift.go @@ -0,0 +1,598 @@ +//go:build gonavi_full_drivers || gonavi_hive_driver + +package db + +import ( + "bufio" + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + "net" + "strings" + "sync" + "sync/atomic" + "time" +) + +// Thrift Binary Protocol 常量定义(对应 Apache Thrift TBinaryProtocol 与 TSaslTransport)。 +const ( + thriftVersionMask uint32 = 0xffff0000 + thriftVersion1 uint32 = 0x80010000 + + thriftMsgCall byte = 1 + thriftMsgReply byte = 2 + thriftMsgException byte = 3 + + thriftTypeStop byte = 0 + thriftTypeVoid byte = 1 + thriftTypeBool byte = 2 + thriftTypeByte byte = 3 + thriftTypeDouble byte = 4 + thriftTypeI16 byte = 6 + thriftTypeI32 byte = 8 + thriftTypeI64 byte = 10 + thriftTypeString byte = 11 + thriftTypeStruct byte = 12 + thriftTypeMap byte = 13 + thriftTypeSet byte = 14 + thriftTypeList byte = 15 + + saslStatusStart byte = 1 + saslStatusOK byte = 2 + saslStatusBad byte = 3 + saslStatusError byte = 4 + saslStatusComplete byte = 5 + + maxThriftFrameBytes = 256 * 1024 * 1024 + maxThriftItemCount = 16 * 1024 * 1024 +) + +// hiveTransport 封装 HiveServer2 的底层 TCP/TLS 连接,支持 SASL PLAIN 帧传输与 NOSASL 原始传输。 +type hiveTransport struct { + mu sync.Mutex + conn net.Conn + reader *bufio.Reader + framed bool + seqID atomic.Int32 +} + +func newHiveTransport(conn net.Conn, framed bool) *hiveTransport { + return &hiveTransport{ + conn: conn, + reader: bufio.NewReaderSize(conn, 64*1024), + framed: framed, + } +} + +func (t *hiveTransport) Close() error { + t.mu.Lock() + defer t.mu.Unlock() + if t.conn == nil { + return nil + } + err := t.conn.Close() + t.conn = nil + return err +} + +// handshakeSASLPlain 执行 TSaslClientTransport 的 PLAIN 握手(适用 NONE / PLAIN / LDAP 认证)。 +func (t *hiveTransport) handshakeSASLPlain(ctx context.Context, username, password string) error { + t.mu.Lock() + defer t.mu.Unlock() + if t.conn == nil { + return localizedDatabaseRuntimeError("db.backend.error.connection_not_open", nil) + } + stopWatch := watchContextDeadline(ctx, t.conn) + defer stopWatch() + + user := strings.TrimSpace(username) + if user == "" { + user = "anonymous" + } + pass := password + if pass == "" { + pass = "anonymous" + } + + var buf bytes.Buffer + writeSASLMessage(&buf, saslStatusStart, []byte("PLAIN")) + authPayload := make([]byte, 0, len(user)+len(pass)+2) + authPayload = append(authPayload, 0) + authPayload = append(authPayload, user...) + authPayload = append(authPayload, 0) + authPayload = append(authPayload, pass...) + writeSASLMessage(&buf, saslStatusOK, authPayload) + + if _, err := t.conn.Write(buf.Bytes()); err != nil { + return wrapHiveProtocolError(err) + } + + for { + status, payload, err := readSASLMessage(t.reader) + if err != nil { + return wrapHiveProtocolError(err) + } + switch status { + case saslStatusComplete: + return nil + case saslStatusOK: + // Challenge-response 后续轮次对 PLAIN 为空响应 + var next bytes.Buffer + writeSASLMessage(&next, saslStatusOK, nil) + if _, err := t.conn.Write(next.Bytes()); err != nil { + return wrapHiveProtocolError(err) + } + case saslStatusBad, saslStatusError: + detail := strings.TrimSpace(string(payload)) + if detail == "" { + detail = fmt.Sprintf("SASL status %d", status) + } + return localizedDatabaseRuntimeError("db.backend.error.hive_auth_failed", map[string]any{"detail": detail}) + default: + return wrapHiveProtocolError(fmt.Errorf("unexpected SASL negotiation status %d", status)) + } + } +} + +func writeSASLMessage(buf *bytes.Buffer, status byte, payload []byte) { + buf.WriteByte(status) + var lenBuf [4]byte + binary.BigEndian.PutUint32(lenBuf[:], uint32(len(payload))) + buf.Write(lenBuf[:]) + buf.Write(payload) +} + +func readSASLMessage(r io.Reader) (byte, []byte, error) { + var header [5]byte + if _, err := io.ReadFull(r, header[:]); err != nil { + return 0, nil, err + } + status := header[0] + length := binary.BigEndian.Uint32(header[1:5]) + if length > maxThriftFrameBytes { + return 0, nil, fmt.Errorf("SASL frame size %d exceeds limit", length) + } + payload := make([]byte, length) + if length > 0 { + if _, err := io.ReadFull(r, payload); err != nil { + return 0, nil, err + } + } + return status, payload, nil +} + +// call 发送一次 Thrift RPC 请求并读取响应结构。 +func (t *hiveTransport) call(ctx context.Context, method string, encodeArgs func(*hiveWriter), decodeResult func(*hiveReader) error) error { + if err := ctx.Err(); err != nil { + return err + } + t.mu.Lock() + defer t.mu.Unlock() + if t.conn == nil { + return localizedDatabaseRuntimeError("db.backend.error.connection_not_open", nil) + } + stopWatch := watchContextDeadline(ctx, t.conn) + defer stopWatch() + + seq := t.seqID.Add(1) + w := newHiveWriter() + w.writeMessageBegin(method, thriftMsgCall, seq) + encodeArgs(w) + w.writeFieldStop() + + payload := w.bytes() + if t.framed { + var frameHdr [4]byte + binary.BigEndian.PutUint32(frameHdr[:], uint32(len(payload))) + if _, err := t.conn.Write(frameHdr[:]); err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + return err + } + } + if _, err := t.conn.Write(payload); err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + return err + } + + var r *hiveReader + if t.framed { + var frameHdr [4]byte + if _, err := io.ReadFull(t.reader, frameHdr[:]); err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + return wrapHiveProtocolError(err) + } + frameLen := binary.BigEndian.Uint32(frameHdr[:]) + if frameLen > maxThriftFrameBytes { + return wrapHiveProtocolError(fmt.Errorf("frame length %d exceeds limit", frameLen)) + } + frame := make([]byte, frameLen) + if _, err := io.ReadFull(t.reader, frame); err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + return wrapHiveProtocolError(err) + } + r = newHiveReader(bytes.NewReader(frame)) + } else { + r = newHiveReader(t.reader) + } + + _, msgType, _, err := r.readMessageBegin() + if err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + return wrapHiveProtocolError(err) + } + if msgType == thriftMsgException { + appErr, exErr := r.readApplicationException() + if exErr != nil { + return wrapHiveProtocolError(exErr) + } + return appErr + } + if msgType != thriftMsgReply { + return wrapHiveProtocolError(fmt.Errorf("unexpected Thrift message type %d", msgType)) + } + return decodeResult(r) +} + +func watchContextDeadline(ctx context.Context, conn net.Conn) func() { + if deadline, ok := ctx.Deadline(); ok { + _ = conn.SetDeadline(deadline) + } else { + _ = conn.SetDeadline(time.Time{}) + } + done := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + _ = conn.SetDeadline(time.Now()) + case <-done: + } + }() + return func() { + close(done) + _ = conn.SetDeadline(time.Time{}) + } +} + +func wrapHiveProtocolError(err error) error { + if err == nil { + return nil + } + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } + return localizedDatabaseRuntimeError("db.backend.error.hive_protocol_error", map[string]any{"detail": err.Error()}) +} + +// hiveWriter 编码 Thrift Binary 协议字节流。 +type hiveWriter struct { + buf bytes.Buffer +} + +func newHiveWriter() *hiveWriter { + return &hiveWriter{} +} + +func (w *hiveWriter) bytes() []byte { + return w.buf.Bytes() +} + +func (w *hiveWriter) writeMessageBegin(name string, msgType byte, seqID int32) { + w.writeI32(int32(thriftVersion1 | uint32(msgType))) + w.writeString(name) + w.writeI32(seqID) +} + +func (w *hiveWriter) writeFieldBegin(fieldType byte, fieldID int16) { + w.buf.WriteByte(fieldType) + w.writeI16(fieldID) +} + +func (w *hiveWriter) writeFieldStop() { + w.buf.WriteByte(thriftTypeStop) +} + +func (w *hiveWriter) writeBool(v bool) { + if v { + w.buf.WriteByte(1) + } else { + w.buf.WriteByte(0) + } +} + +func (w *hiveWriter) writeI16(v int16) { + var b [2]byte + binary.BigEndian.PutUint16(b[:], uint16(v)) + w.buf.Write(b[:]) +} + +func (w *hiveWriter) writeI32(v int32) { + var b [4]byte + binary.BigEndian.PutUint32(b[:], uint32(v)) + w.buf.Write(b[:]) +} + +func (w *hiveWriter) writeI64(v int64) { + var b [8]byte + binary.BigEndian.PutUint64(b[:], uint64(v)) + w.buf.Write(b[:]) +} + +func (w *hiveWriter) writeDouble(v float64) { + w.writeI64(int64(math.Float64bits(v))) +} + +func (w *hiveWriter) writeString(s string) { + w.writeI32(int32(len(s))) + w.buf.WriteString(s) +} + +func (w *hiveWriter) writeBytes(b []byte) { + w.writeI32(int32(len(b))) + w.buf.Write(b) +} + +func (w *hiveWriter) writeStringMap(m map[string]string) { + w.buf.WriteByte(thriftTypeString) + w.buf.WriteByte(thriftTypeString) + w.writeI32(int32(len(m))) + for k, v := range m { + w.writeString(k) + w.writeString(v) + } +} + +// hiveReader 解码 Thrift Binary 协议字节流。 +type hiveReader struct { + r io.Reader +} + +func newHiveReader(r io.Reader) *hiveReader { + return &hiveReader{r: r} +} + +func (r *hiveReader) readMessageBegin() (string, byte, int32, error) { + sizeAndVersion, err := r.readI32() + if err != nil { + return "", 0, 0, err + } + u := uint32(sizeAndVersion) + if (u & thriftVersionMask) != thriftVersion1 { + return "", 0, 0, fmt.Errorf("unsupported Thrift binary version 0x%x", u) + } + msgType := byte(u & 0x000000ff) + name, err := r.readString() + if err != nil { + return "", 0, 0, err + } + seqID, err := r.readI32() + if err != nil { + return "", 0, 0, err + } + return name, msgType, seqID, nil +} + +func (r *hiveReader) readFieldBegin() (byte, int16, error) { + fieldType, err := r.readByte() + if err != nil { + return 0, 0, err + } + if fieldType == thriftTypeStop { + return thriftTypeStop, 0, nil + } + fieldID, err := r.readI16() + if err != nil { + return 0, 0, err + } + return fieldType, fieldID, nil +} + +func (r *hiveReader) readByte() (byte, error) { + var b [1]byte + if _, err := io.ReadFull(r.r, b[:]); err != nil { + return 0, err + } + return b[0], nil +} + +func (r *hiveReader) readBool() (bool, error) { + b, err := r.readByte() + return b != 0, err +} + +func (r *hiveReader) readI16() (int16, error) { + var b [2]byte + if _, err := io.ReadFull(r.r, b[:]); err != nil { + return 0, err + } + return int16(binary.BigEndian.Uint16(b[:])), nil +} + +func (r *hiveReader) readI32() (int32, error) { + var b [4]byte + if _, err := io.ReadFull(r.r, b[:]); err != nil { + return 0, err + } + return int32(binary.BigEndian.Uint32(b[:])), nil +} + +func (r *hiveReader) readI64() (int64, error) { + var b [8]byte + if _, err := io.ReadFull(r.r, b[:]); err != nil { + return 0, err + } + return int64(binary.BigEndian.Uint64(b[:])), nil +} + +func (r *hiveReader) readDouble() (float64, error) { + bits, err := r.readI64() + if err != nil { + return 0, err + } + return math.Float64frombits(uint64(bits)), nil +} + +func (r *hiveReader) readBytes() ([]byte, error) { + size, err := r.readI32() + if err != nil { + return nil, err + } + if size < 0 || size > maxThriftFrameBytes { + return nil, fmt.Errorf("invalid Thrift binary length %d", size) + } + if size == 0 { + return []byte{}, nil + } + buf := make([]byte, size) + if _, err := io.ReadFull(r.r, buf); err != nil { + return nil, err + } + return buf, nil +} + +func (r *hiveReader) readString() (string, error) { + b, err := r.readBytes() + if err != nil { + return "", err + } + return string(b), nil +} + +func (r *hiveReader) readListBegin() (byte, int, error) { + elemType, err := r.readByte() + if err != nil { + return 0, 0, err + } + size, err := r.readI32() + if err != nil { + return 0, 0, err + } + if size < 0 || size > maxThriftItemCount { + return 0, 0, fmt.Errorf("invalid Thrift list size %d", size) + } + return elemType, int(size), nil +} + +func (r *hiveReader) readMapBegin() (byte, byte, int, error) { + keyType, err := r.readByte() + if err != nil { + return 0, 0, 0, err + } + valType, err := r.readByte() + if err != nil { + return 0, 0, 0, err + } + size, err := r.readI32() + if err != nil { + return 0, 0, 0, err + } + if size < 0 || size > maxThriftItemCount { + return 0, 0, 0, fmt.Errorf("invalid Thrift map size %d", size) + } + return keyType, valType, int(size), nil +} + +func (r *hiveReader) skip(fieldType byte) error { + switch fieldType { + case thriftTypeBool, thriftTypeByte: + _, err := r.readByte() + return err + case thriftTypeI16: + _, err := r.readI16() + return err + case thriftTypeI32: + _, err := r.readI32() + return err + case thriftTypeI64, thriftTypeDouble: + _, err := r.readI64() + return err + case thriftTypeString: + _, err := r.readBytes() + return err + case thriftTypeStruct: + for { + ft, _, err := r.readFieldBegin() + if err != nil { + return err + } + if ft == thriftTypeStop { + return nil + } + if err := r.skip(ft); err != nil { + return err + } + } + case thriftTypeMap: + kt, vt, size, err := r.readMapBegin() + if err != nil { + return err + } + for i := 0; i < size; i++ { + if err := r.skip(kt); err != nil { + return err + } + if err := r.skip(vt); err != nil { + return err + } + } + return nil + case thriftTypeList, thriftTypeSet: + et, size, err := r.readListBegin() + if err != nil { + return err + } + for i := 0; i < size; i++ { + if err := r.skip(et); err != nil { + return err + } + } + return nil + default: + return fmt.Errorf("unknown Thrift field type %d", fieldType) + } +} + +func (r *hiveReader) readApplicationException() (error, error) { + var msg string + var exType int32 + for { + ft, fid, err := r.readFieldBegin() + if err != nil { + return nil, err + } + if ft == thriftTypeStop { + break + } + switch { + case fid == 1 && ft == thriftTypeString: + if msg, err = r.readString(); err != nil { + return nil, err + } + case fid == 2 && ft == thriftTypeI32: + if exType, err = r.readI32(); err != nil { + return nil, err + } + default: + if err := r.skip(ft); err != nil { + return nil, err + } + } + } + if strings.TrimSpace(msg) == "" { + msg = fmt.Sprintf("Thrift application exception (type %d)", exType) + } + return errors.New(msg), nil +} diff --git a/internal/db/registry_rows.go b/internal/db/registry_rows.go index b122e12f1..7473e61e1 100644 --- a/internal/db/registry_rows.go +++ b/internal/db/registry_rows.go @@ -1,4 +1,4 @@ -//go:build gonavi_full_drivers || gonavi_cockroachdb_driver || gonavi_kwdb_driver || gonavi_questdb_driver || gonavi_greptimedb_driver || gonavi_timescaledb_driver || gonavi_presto_driver +//go:build gonavi_full_drivers || gonavi_cockroachdb_driver || gonavi_kwdb_driver || gonavi_questdb_driver || gonavi_greptimedb_driver || gonavi_timescaledb_driver || gonavi_presto_driver || gonavi_hive_driver package db diff --git a/shared/i18n/catalog.zip b/shared/i18n/catalog.zip index 89b8e45da..3a3c9e794 100644 Binary files a/shared/i18n/catalog.zip and b/shared/i18n/catalog.zip differ diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index feb5a0578..444c9be2d 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -13457,5 +13457,13 @@ "plugin.market.category.other": "Sonstige", "plugin.market.anonymous_usage": "Öffentliche Plugins ohne Anmeldung durchsuchen und installieren", "plugin.market.ratings_on_website": "Bewertungen und Kontoverwaltung erfolgen auf der Marktplatz-Website. GoNavi zeigt öffentliche Bewertungen nur an.", - "plugin.market.open_website": "Marktplatz-Website öffnen" + "plugin.market.open_website": "Marktplatz-Website öffnen", + "datasource.variant.hive.v3": "Hive 3.x: Spaltenbasiertes HiveServer2-V10-Ergebnisprotokoll mit LIMIT/OFFSET-Seitenumbruch und Metadaten für externe Tabellen", + "datasource.variant.hive.v4": "Hive 4.x und neuer: Spaltenbasiertes HiveServer2-V11-Ergebnisprotokoll mit Iceberg-/Transaktionstabellen und vollständigen DDL-Metadaten", + "connection_modal.step1.hint.hive": "Data Warehouse / HiveServer2 Thrift", + "db.backend.error.hive_auth_failed": "HiveServer2-Authentifizierung fehlgeschlagen: {{detail}}", + "db.backend.error.hive_query_failed": "Hive-Abfrage fehlgeschlagen (SQLState {{sqlState}}, Fehlercode {{code}}): {{message}}", + "db.backend.error.hive_protocol_error": "HiveServer2-Thrift-Protokollantwort konnte nicht gelesen werden (prüfen Sie, ob der Port ein binärer HiveServer2-Thrift-Port ist): {{detail}}", + "db.backend.error.hive_ddl_empty": "Hive hat keine CREATE-Anweisung für „{{name}}“ zurückgegeben", + "db.backend.error.hive_row_edit_unsupported": "Hive-Tabellen unterstützen im Datenraster kein Aktualisieren oder Löschen einzelner Zeilen; führen Sie INSERT oder ALTER TABLE DROP PARTITION im SQL-Editor aus (nur Zeileneinfügungen werden unterstützt)" } diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index 4da7104bf..c08c2cd23 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -13457,5 +13457,13 @@ "plugin.market.category.other": "Other", "plugin.market.anonymous_usage": "Browse and install public plugins without signing in", "plugin.market.ratings_on_website": "Ratings, reviews and account actions are managed on the market website. GoNavi only displays public ratings.", - "plugin.market.open_website": "Open market website" + "plugin.market.open_website": "Open market website", + "datasource.variant.hive.v3": "Hive 3.x: HiveServer2 V10 columnar result protocol with LIMIT / OFFSET paging and external table metadata", + "datasource.variant.hive.v4": "Hive 4.x and later: HiveServer2 V11 columnar result protocol with Iceberg / transactional table and full DDL metadata support", + "connection_modal.step1.hint.hive": "Data warehouse / HiveServer2 Thrift", + "db.backend.error.hive_auth_failed": "HiveServer2 authentication failed: {{detail}}", + "db.backend.error.hive_query_failed": "Hive query failed (SQLState {{sqlState}}, error code {{code}}): {{message}}", + "db.backend.error.hive_protocol_error": "HiveServer2 Thrift protocol response could not be parsed (verify the port is a HiveServer2 binary Thrift port): {{detail}}", + "db.backend.error.hive_ddl_empty": "Hive returned no CREATE statement for \"{{name}}\"", + "db.backend.error.hive_row_edit_unsupported": "Hive tables do not support single-row update or delete by key in the data grid; run INSERT or ALTER TABLE DROP PARTITION in the SQL editor (only row inserts are supported)" } diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index 907404d6d..336a36665 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -13457,5 +13457,13 @@ "plugin.market.category.other": "その他", "plugin.market.anonymous_usage": "公開プラグインはログインせずに閲覧・インストールできます", "plugin.market.ratings_on_website": "評価・レビュー・アカウント操作はマーケットサイトで行います。GoNavi は公開評価の表示のみを行います。", - "plugin.market.open_website": "マーケットサイトを開く" + "plugin.market.open_website": "マーケットサイトを開く", + "datasource.variant.hive.v3": "Hive 3.x:HiveServer2 V10 カラムナ結果プロトコル、LIMIT / OFFSET ページングと外部テーブルメタデータに対応", + "datasource.variant.hive.v4": "Hive 4.x 以降:HiveServer2 V11 カラムナ結果プロトコル、Iceberg / トランザクションテーブルと完全な DDL メタデータに対応", + "connection_modal.step1.hint.hive": "分散データウェアハウス / HiveServer2 Thrift", + "db.backend.error.hive_auth_failed": "HiveServer2 認証に失敗しました:{{detail}}", + "db.backend.error.hive_query_failed": "Hive クエリが失敗しました(SQLState {{sqlState}}、エラーコード {{code}}):{{message}}", + "db.backend.error.hive_protocol_error": "HiveServer2 Thrift プロトコル応答を解析できませんでした(ポートが HiveServer2 バイナリ Thrift ポートか確認してください):{{detail}}", + "db.backend.error.hive_ddl_empty": "Hive から「{{name}}」の CREATE 文が返されませんでした", + "db.backend.error.hive_row_edit_unsupported": "Hive テーブルはデータグリッドでの単一行の更新・削除に対応していません。SQL エディタで INSERT または ALTER TABLE DROP PARTITION を実行してください(行の追加のみ対応)" } diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index 49a98fd8d..ea40fb017 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -13457,5 +13457,13 @@ "plugin.market.category.other": "Другие", "plugin.market.anonymous_usage": "Просматривайте и устанавливайте публичные плагины без входа", "plugin.market.ratings_on_website": "Оценки, отзывы и учётная запись управляются на сайте каталога. GoNavi только показывает публичные оценки.", - "plugin.market.open_website": "Открыть сайт каталога" + "plugin.market.open_website": "Открыть сайт каталога", + "datasource.variant.hive.v3": "Hive 3.x: колоночный протокол результатов HiveServer2 V10 с постраничным выводом LIMIT / OFFSET и метаданными внешних таблиц", + "datasource.variant.hive.v4": "Hive 4.x и новее: колоночный протокол результатов HiveServer2 V11 с поддержкой таблиц Iceberg / транзакционных таблиц и полных метаданных DDL", + "connection_modal.step1.hint.hive": "Хранилище данных / HiveServer2 Thrift", + "db.backend.error.hive_auth_failed": "Ошибка аутентификации HiveServer2: {{detail}}", + "db.backend.error.hive_query_failed": "Ошибка запроса Hive (SQLState {{sqlState}}, код ошибки {{code}}): {{message}}", + "db.backend.error.hive_protocol_error": "Не удалось разобрать ответ протокола HiveServer2 Thrift (убедитесь, что указан бинарный порт Thrift HiveServer2): {{detail}}", + "db.backend.error.hive_ddl_empty": "Hive не вернул оператор CREATE для «{{name}}»", + "db.backend.error.hive_row_edit_unsupported": "Таблицы Hive не поддерживают построчное изменение или удаление в таблице данных; выполните INSERT или ALTER TABLE DROP PARTITION в редакторе SQL (поддерживается только добавление строк)" } diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index 086610836..3d996f4a9 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -13457,5 +13457,13 @@ "plugin.market.category.other": "其他", "plugin.market.anonymous_usage": "公开插件无需登录即可浏览和安装", "plugin.market.ratings_on_website": "评分、评价和账号操作统一在市场网站处理,GoNavi 仅展示公开评分。", - "plugin.market.open_website": "打开市场网站" + "plugin.market.open_website": "打开市场网站", + "datasource.variant.hive.v3": "Hive 3.x:使用 HiveServer2 V10 列式结果集协议,支持 LIMIT / OFFSET 分页与外表元数据", + "datasource.variant.hive.v4": "Hive 4.x 及更新版本:使用 HiveServer2 V11 列式结果集协议,支持 Iceberg / 事务表与完整 DDL 元数据", + "connection_modal.step1.hint.hive": "分布式数仓 / HiveServer2 Thrift", + "db.backend.error.hive_auth_failed": "HiveServer2 认证失败:{{detail}}", + "db.backend.error.hive_query_failed": "Hive 查询执行失败(SQLState {{sqlState}},错误码 {{code}}):{{message}}", + "db.backend.error.hive_protocol_error": "HiveServer2 Thrift 协议响应无法解析(请确认端口为 HiveServer2 二进制 Thrift 端口):{{detail}}", + "db.backend.error.hive_ddl_empty": "Hive 未返回对象「{{name}}」的建表语句", + "db.backend.error.hive_row_edit_unsupported": "Hive 表默认不支持按主键定位单行更新或删除,请在查询编辑器中执行 INSERT / ALTER TABLE DROP PARTITION(仅支持新增行导入)" } diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index 12e534c30..8de8d7820 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -13457,5 +13457,13 @@ "plugin.market.category.other": "其他", "plugin.market.anonymous_usage": "公開外掛無須登入即可瀏覽與安裝", "plugin.market.ratings_on_website": "評分、評價和帳號操作統一在市集網站處理,GoNavi 僅顯示公開評分。", - "plugin.market.open_website": "開啟市集網站" + "plugin.market.open_website": "開啟市集網站", + "datasource.variant.hive.v3": "Hive 3.x:使用 HiveServer2 V10 列式結果集協定,支援 LIMIT / OFFSET 分頁與外表元資料", + "datasource.variant.hive.v4": "Hive 4.x 及更新版本:使用 HiveServer2 V11 列式結果集協定,支援 Iceberg / 交易表與完整 DDL 元資料", + "connection_modal.step1.hint.hive": "分散式數倉 / HiveServer2 Thrift", + "db.backend.error.hive_auth_failed": "HiveServer2 驗證失敗:{{detail}}", + "db.backend.error.hive_query_failed": "Hive 查詢執行失敗(SQLState {{sqlState}},錯誤碼 {{code}}):{{message}}", + "db.backend.error.hive_protocol_error": "HiveServer2 Thrift 協定回應無法解析(請確認連接埠為 HiveServer2 二進位 Thrift 連接埠):{{detail}}", + "db.backend.error.hive_ddl_empty": "Hive 未傳回物件「{{name}}」的建表語句", + "db.backend.error.hive_row_edit_unsupported": "Hive 表預設不支援按主鍵定位單行更新或刪除,請在查詢編輯器中執行 INSERT / ALTER TABLE DROP PARTITION(僅支援新增行匯入)" }