diff --git a/cmd/optional-driver-agent/provider_trino.go b/cmd/optional-driver-agent/provider_trino.go new file mode 100644 index 0000000..77bfade --- /dev/null +++ b/cmd/optional-driver-agent/provider_trino.go @@ -0,0 +1,12 @@ +//go:build gonavi_trino_driver + +package main + +import "GoNavi-Wails/internal/db" + +func init() { + agentDriverType = "trino" + agentDatabaseFactory = func() db.Database { + return &db.TrinoDB{} + } +} diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index ab3a346..f9704a3 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -24,7 +24,7 @@ import { import { extractQueryResultTableRef, type QueryResultTableRef } from '../utils/queryResultTable'; import { quoteIdentPart, quoteQualifiedIdent } from '../utils/sql'; import { formatSqlExecutionError } from '../utils/sqlErrorSemantics'; -import { shouldUseSqlEditorManagedTransaction } from '../utils/sqlEditorTransaction'; +import { shouldUseSqlEditorManagedTransactionForType } from '../utils/sqlEditorTransaction'; import { findSqlStatementRanges, resolveCurrentSqlStatementRange, resolveExecutableSql } from '../utils/sqlStatementSelection'; import { isMacLikePlatform } from '../utils/appearance'; import { splitSidebarQualifiedName } from '../utils/sidebarLocate'; @@ -3205,13 +3205,13 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc setActiveResultKey(''); return; } - const useManagedTransaction = shouldUseSqlEditorManagedTransaction(sourceStatements); + const useManagedTransaction = shouldUseSqlEditorManagedTransactionForType(connCaps.type, sourceStatements); if (useManagedTransaction && pendingSqlTransactionRef.current) { message.warning(translate('query_editor.transaction.message.pending_managed_transaction')); return; } const managedTransactionStatementCount = sourceStatements - .filter((statement) => shouldUseSqlEditorManagedTransaction([statement])) + .filter((statement) => shouldUseSqlEditorManagedTransactionForType(connCaps.type, [statement])) .length || sourceStatements.length; const forceReadOnlyResult = connCaps.forceReadOnlyQueryResult; diff --git a/frontend/src/components/connectionModal/ConnectionModalStep2.tsx b/frontend/src/components/connectionModal/ConnectionModalStep2.tsx index 2e4f27f..92b2fb4 100644 --- a/frontend/src/components/connectionModal/ConnectionModalStep2.tsx +++ b/frontend/src/components/connectionModal/ConnectionModalStep2.tsx @@ -1191,7 +1191,8 @@ const renderStep2 = () => { dbType === "highgo" || dbType === "vastbase" || dbType === "opengauss" || - dbType === "gaussdb") && + dbType === "gaussdb" || + dbType === "trino") && renderConfigSectionCard({ sectionKey: "service", icon: , diff --git a/frontend/src/components/connectionModal/connectionModalUri.test.ts b/frontend/src/components/connectionModal/connectionModalUri.test.ts new file mode 100644 index 0000000..ef67e85 --- /dev/null +++ b/frontend/src/components/connectionModal/connectionModalUri.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; + +import { + buildUriFromValues, + getConnectionParamsPlaceholder, + getUriPlaceholder, + parseTrinoUriToValues, + parseUriToValues, +} from './connectionModalUri'; + +describe('connectionModalUri trino support', () => { + it('parses catalog and schema from a Trino URI into the database field', () => { + expect(parseTrinoUriToValues('https://alice@127.0.0.1:8443?catalog=hive&schema=default&source=GoNavi&query_timeout=30s')) + .toMatchObject({ + host: '127.0.0.1', + port: 8443, + user: 'alice', + database: 'hive.default', + useSSL: true, + sslMode: 'required', + connectionParams: 'source=GoNavi&query_timeout=30s', + }); + }); + + it('routes generic URI parsing through the Trino parser', () => { + expect(parseUriToValues('http://alice@127.0.0.1:8080?catalog=iceberg&schema=ods', 'trino')) + .toMatchObject({ + host: '127.0.0.1', + port: 8080, + user: 'alice', + database: 'iceberg.ods', + }); + }); + + it('builds a Trino URI with catalog and schema in query parameters', () => { + expect(buildUriFromValues({ + type: 'trino', + host: '127.0.0.1', + port: 8080, + user: 'alice', + database: 'hive.default', + connectionParams: 'query_timeout=45s', + })).toBe('http://alice@127.0.0.1:8080?query_timeout=45s&catalog=hive&schema=default&source=GoNavi'); + }); + + it('keeps dedicated Trino placeholders concise', () => { + expect(getUriPlaceholder('trino')).toBe('http://user@127.0.0.1:8080?catalog=hive&schema=default&source=GoNavi'); + expect(getConnectionParamsPlaceholder('trino', 'mysql')).toBe('session_properties=query_max_execution_time:30m&query_timeout=30s'); + }); +}); diff --git a/frontend/src/components/connectionModal/connectionModalUri.ts b/frontend/src/components/connectionModal/connectionModalUri.ts index 48c84de..86640d6 100644 --- a/frontend/src/components/connectionModal/connectionModalUri.ts +++ b/frontend/src/components/connectionModal/connectionModalUri.ts @@ -342,9 +342,9 @@ const parseSingleHostUri = ( }; }; -export const parseClickHouseHTTPUriToValues = ( - uriText: string, - fallbackPort?: number, +export const parseClickHouseHTTPUriToValues = ( + uriText: string, + fallbackPort?: number, ): Record | null => { const trimmed = String(uriText || "").trim(); const lower = trimmed.toLowerCase(); @@ -379,8 +379,71 @@ export const parseClickHouseHTTPUriToValues = ( sslMode: isHttps ? (skipVerify ? "skip-verify" : "required") : "disable", ...extractSSLPathValuesFromParams(parsed.params, "clickhouse"), connectionParams: serializeConnectionParams(parsed.params), - }; -}; + }; +}; + +const splitTrinoNamespace = ( + raw: unknown, +): { catalog: string; schema: string } => { + const text = String(raw || "").trim(); + if (!text) { + return { catalog: "", schema: "" }; + } + const [catalog, schema = ""] = text.split(".", 2); + return { + catalog: String(catalog || "").trim(), + schema: String(schema || "").trim(), + }; +}; + +const joinTrinoNamespace = (catalog: string, schema: string) => { + const safeCatalog = String(catalog || "").trim(); + const safeSchema = String(schema || "").trim(); + if (!safeCatalog) return safeSchema; + if (!safeSchema) return safeCatalog; + return `${safeCatalog}.${safeSchema}`; +}; + +export const parseTrinoUriToValues = ( + uriText: string, +): Record | null => { + const trimmed = String(uriText || "").trim(); + const parsed = parseSingleHostUri( + trimmed, + ["trino", "http", "https"], + getDefaultPortByType("trino"), + ); + if (!parsed) { + return null; + } + const params = new URLSearchParams(parsed.params); + const catalog = String(params.get("catalog") || "").trim(); + const schema = String(params.get("schema") || "").trim(); + params.delete("catalog"); + params.delete("schema"); + + const skipVerify = normalizeUriBool( + params.get("skip_verify") || params.get("skipVerify"), + ); + params.delete("skip_verify"); + params.delete("skipVerify"); + + const namespace = + joinTrinoNamespace(catalog, schema) || String(parsed.database || "").trim(); + return { + host: parsed.host, + port: parsed.port, + user: parsed.username, + password: parsed.password, + database: namespace, + useSSL: trimmed.toLowerCase().startsWith("https://"), + sslMode: trimmed.toLowerCase().startsWith("https://") + ? (skipVerify ? "skip-verify" : "required") + : "disable", + ...extractSSLPathValuesFromParams(params, "trino"), + connectionParams: serializeConnectionParams(params), + }; +}; const firstConnectionParamValue = ( params: URLSearchParams, @@ -827,8 +890,8 @@ export const parseUriToValues = ( }; } - if (type === "rabbitmq") { - const defaultPort = getDefaultPortByType(type); + if (type === "rabbitmq") { + const defaultPort = getDefaultPortByType(type); const parsed = parseSingleHostUri( trimmedUri, ["rabbitmq", "http", "https"], @@ -864,11 +927,15 @@ export const parseUriToValues = ( Number.isFinite(timeoutValue) && timeoutValue > 0 ? Math.min(MAX_TIMEOUT_SECONDS, Math.trunc(timeoutValue)) : undefined, - }; - } - - if (type === "clickhouse") { - const httpValues = parseClickHouseHTTPUriToValues(trimmedUri); + }; + } + + if (type === "trino") { + return parseTrinoUriToValues(trimmedUri); + } + + if (type === "clickhouse") { + const httpValues = parseClickHouseHTTPUriToValues(trimmedUri); if (httpValues) { return httpValues; } @@ -1070,10 +1137,13 @@ export const getUriPlaceholder = (dbType: string) => { if (dbType === "mongodb") { return "mongodb+srv://user:pass@cluster0.example.com/db_name?authSource=admin&authMechanism=SCRAM-SHA-256"; } - if (dbType === "clickhouse") { - return "clickhouse://default:pass@127.0.0.1:9000/default"; - } - if (dbType === "chroma") { + if (dbType === "clickhouse") { + return "clickhouse://default:pass@127.0.0.1:9000/default"; + } + if (dbType === "trino") { + return "http://user@127.0.0.1:8080?catalog=hive&schema=default&source=GoNavi"; + } + if (dbType === "chroma") { return "http://127.0.0.1:8000/default_database?tenant=default_tenant"; } if (dbType === "qdrant") { @@ -1140,10 +1210,12 @@ export const getConnectionParamsPlaceholder = ( return "app name=GoNavi&packet size=32767"; case "iris": return "timeout=30"; - case "clickhouse": - return "max_execution_time=60&compress=lz4"; - case "mongodb": - return "retryWrites=true&readPreference=secondaryPreferred"; + case "clickhouse": + return "max_execution_time=60&compress=lz4"; + case "trino": + return "session_properties=query_max_execution_time:30m&query_timeout=30s"; + case "mongodb": + return "retryWrites=true&readPreference=secondaryPreferred"; case "chroma": return "tenant=default_tenant&apiKey=..."; case "qdrant": @@ -1167,9 +1239,9 @@ export const getConnectionParamsPlaceholder = ( } }; -export const buildUriFromValues = (values: any) => { - const type = String(values.type || "") - .trim() +export const buildUriFromValues = (values: any) => { + const type = String(values.type || "") + .trim() .toLowerCase(); const defaultPort = getDefaultPortByType(type); const host = String(values.host || "localhost").trim(); @@ -1178,11 +1250,49 @@ export const buildUriFromValues = (values: any) => { const password = String(values.password || ""); const database = String(values.database || "").trim(); const timeout = Number(values.timeout || 30); - const encodedAuth = user - ? `${encodeURIComponent(user)}${password ? `:${encodeURIComponent(password)}` : ""}@` - : ""; - - if (isMySQLCompatibleType(type)) { + const encodedAuth = user + ? `${encodeURIComponent(user)}${password ? `:${encodeURIComponent(password)}` : ""}@` + : ""; + + if (type === "trino") { + const params = new URLSearchParams(); + mergeConnectionParams(params, values.connectionParams); + + const { catalog, schema } = splitTrinoNamespace(values.database); + if (catalog) { + params.set("catalog", catalog); + } else { + params.delete("catalog"); + } + if (schema) { + params.set("schema", schema); + } else { + params.delete("schema"); + } + if (!String(params.get("source") || "").trim()) { + params.set("source", "GoNavi"); + } + + if (values.useSSL) { + const mode = String(values.sslMode || "required") + .trim() + .toLowerCase(); + if (mode === "skip-verify" || mode === "preferred") { + params.set("skip_verify", "true"); + } else { + params.delete("skip_verify"); + } + appendSSLPathParamsForUri(params, type, values); + } else { + params.delete("skip_verify"); + } + + const query = params.toString(); + const scheme = values.useSSL ? "https" : "http"; + return `${scheme}://${encodedAuth}${toAddress(host, port, defaultPort)}${query ? `?${query}` : ""}`; + } + + if (isMySQLCompatibleType(type)) { const selectedOceanBaseProtocol = type === "oceanbase" ? normalizeOceanBaseProtocolValue(values.oceanBaseProtocol) diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 6c9d0c3..a1ec64f 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -315,6 +315,7 @@ const SUPPORTED_CONNECTION_TYPES = new Set([ "starrocks", "sphinx", "clickhouse", + "trino", "postgres", "redis", "tdengine", @@ -346,6 +347,7 @@ const SSL_SUPPORTED_CONNECTION_TYPES = new Set([ "sphinx", "dameng", "clickhouse", + "trino", "postgres", "sqlserver", "oracle", @@ -382,6 +384,8 @@ const getDefaultPortByType = (type: string): number => { return 9306; case "clickhouse": return 9000; + case "trino": + return 8080; case "postgres": case "vastbase": case "opengauss": diff --git a/frontend/src/utils/connectionModalPresentation.test.ts b/frontend/src/utils/connectionModalPresentation.test.ts index e43bf0f..38b280f 100644 --- a/frontend/src/utils/connectionModalPresentation.test.ts +++ b/frontend/src/utils/connectionModalPresentation.test.ts @@ -111,6 +111,7 @@ describe('connectionModalPresentation', () => { 'starrocks', 'sphinx', 'clickhouse', + 'trino', 'postgres', 'sqlserver', 'sqlite', @@ -231,6 +232,14 @@ describe('connectionModalPresentation', () => { 'credentials', 'databaseScope', ]); + expect(resolveConnectionConfigLayout('trino').sections).toEqual([ + 'identity', + 'uri', + 'target', + 'service', + 'credentials', + 'databaseScope', + ]); expect(resolveConnectionConfigLayout('gaussdb').sections).toEqual([ 'identity', 'uri', diff --git a/frontend/src/utils/connectionModalPresentation.ts b/frontend/src/utils/connectionModalPresentation.ts index 48be115..65b8c14 100644 --- a/frontend/src/utils/connectionModalPresentation.ts +++ b/frontend/src/utils/connectionModalPresentation.ts @@ -300,6 +300,19 @@ export const resolveConnectionConfigLayout = ( ], }; } + if (type === 'trino') { + return { + kind: 'generic-sql', + sections: [ + 'identity', + 'uri', + 'target', + 'service', + 'credentials', + 'databaseScope', + ], + }; + } if (postgresCompatibleTypes.has(type)) { return { kind: 'postgres-compatible', diff --git a/frontend/src/utils/connectionTypeCapabilities.test.ts b/frontend/src/utils/connectionTypeCapabilities.test.ts index e60f39b..1e1d62f 100644 --- a/frontend/src/utils/connectionTypeCapabilities.test.ts +++ b/frontend/src/utils/connectionTypeCapabilities.test.ts @@ -16,6 +16,7 @@ describe('connectionTypeCapabilities', () => { expect(singleHostUriSchemesByType.postgres).toEqual(['postgresql', 'postgres']); expect(singleHostUriSchemesByType.opengauss).toContain('jdbc:opengauss'); expect(singleHostUriSchemesByType.gaussdb).toEqual(['gaussdb', 'postgresql', 'postgres']); + expect(singleHostUriSchemesByType.trino).toEqual(['trino', 'http', 'https']); expect(singleHostUriSchemesByType.dameng).toEqual(['dameng', 'dm']); expect(singleHostUriSchemesByType.elasticsearch).toEqual(['http', 'https']); expect(singleHostUriSchemesByType.chroma).toEqual(['http', 'https', 'chroma']); @@ -28,6 +29,7 @@ describe('connectionTypeCapabilities', () => { expect(supportsSSLForType('redis')).toBe(true); expect(supportsSSLForType('MongoDB')).toBe(true); expect(supportsSSLForType('elasticsearch')).toBe(true); + expect(supportsSSLForType('trino')).toBe(true); expect(supportsSSLForType('gaussdb')).toBe(true); expect(supportsSSLForType('greatdb')).toBe(true); expect(supportsSSLForType('chroma')).toBe(true); @@ -45,7 +47,9 @@ describe('connectionTypeCapabilities', () => { expect(supportsSSLCAPathForType('gaussdb')).toBe(true); expect(supportsSSLClientCertificateForType('gaussdb')).toBe(true); expect(supportsSSLCAPathForType('sqlserver')).toBe(true); + expect(supportsSSLCAPathForType('trino')).toBe(true); expect(supportsSSLClientCertificateForType('sqlserver')).toBe(false); + expect(supportsSSLClientCertificateForType('trino')).toBe(true); expect(supportsSSLCAPathForType('redis')).toBe(true); expect(supportsSSLClientCertificateForType('redis')).toBe(true); expect(supportsSSLCAPathForType('chroma')).toBe(true); @@ -80,6 +84,7 @@ describe('connectionTypeCapabilities', () => { expect(supportsConnectionParamsForType('gdb')).toBe(true); expect(supportsConnectionParamsForType('postgres')).toBe(true); expect(supportsConnectionParamsForType('gaussdb')).toBe(true); + expect(supportsConnectionParamsForType('trino')).toBe(true); expect(supportsConnectionParamsForType('oracle')).toBe(true); expect(supportsConnectionParamsForType('mongodb')).toBe(true); expect(supportsConnectionParamsForType('dameng')).toBe(true); diff --git a/frontend/src/utils/connectionTypeCapabilities.ts b/frontend/src/utils/connectionTypeCapabilities.ts index d001cd4..a88cf87 100644 --- a/frontend/src/utils/connectionTypeCapabilities.ts +++ b/frontend/src/utils/connectionTypeCapabilities.ts @@ -3,6 +3,7 @@ export const singleHostUriSchemesByType: Record = { opengauss: ["opengauss", "jdbc:opengauss", "postgresql", "postgres"], gaussdb: ["gaussdb", "postgresql", "postgres"], clickhouse: ["clickhouse"], + trino: ["trino", "http", "https"], oracle: ["oracle"], sqlserver: ["sqlserver"], iris: ["iris", "intersystems"], @@ -55,6 +56,7 @@ const sslSupportedTypes = new Set([ "sphinx", "dameng", "clickhouse", + "trino", "postgres", "sqlserver", "oracle", @@ -86,6 +88,7 @@ const sslCAPathSupportedTypes = new Set([ "starrocks", "sphinx", "clickhouse", + "trino", "postgres", "sqlserver", "kingbase", @@ -113,6 +116,7 @@ const sslClientCertificateSupportedTypes = new Set([ "sphinx", "dameng", "clickhouse", + "trino", "postgres", "kingbase", "highgo", @@ -167,6 +171,7 @@ export const supportsConnectionParamsForType = (type: string) => type === "sqlserver" || type === "iris" || type === "clickhouse" || + type === "trino" || type === "mongodb" || type === "dameng" || type === "tdengine" || diff --git a/frontend/src/utils/connectionTypeCatalog.test.ts b/frontend/src/utils/connectionTypeCatalog.test.ts index 3a85f9c..a0a5157 100644 --- a/frontend/src/utils/connectionTypeCatalog.test.ts +++ b/frontend/src/utils/connectionTypeCatalog.test.ts @@ -24,6 +24,7 @@ describe('connectionTypeCatalog', () => { expect(keys).toContain('oceanbase'); expect(keys).toContain('gaussdb'); expect(keys).toContain('goldendb'); + expect(keys).toContain('trino'); expect(keys).toContain('mongodb'); expect(keys).toContain('redis'); expect(keys).toContain('elasticsearch'); @@ -46,6 +47,7 @@ describe('connectionTypeCatalog', () => { expect(getConnectionTypeDefaultPort('redis')).toBe(6379); expect(getConnectionTypeDefaultPort('oracle')).toBe(1521); expect(getConnectionTypeDefaultPort('mongodb')).toBe(27017); + expect(getConnectionTypeDefaultPort('trino')).toBe(8080); expect(getConnectionTypeDefaultPort('elasticsearch')).toBe(9200); expect(getConnectionTypeDefaultPort('chroma')).toBe(8000); expect(getConnectionTypeDefaultPort('qdrant')).toBe(6333); @@ -66,6 +68,7 @@ describe('connectionTypeCatalog', () => { expect(getConnectionTypeHint('kafka')).toContain('Consumer Group'); expect(getConnectionTypeHint('oceanbase')).toBe('MySQL / Oracle 租户'); expect(getConnectionTypeHint('goldendb')).toBe('MySQL 兼容 / 分布式事务'); + expect(getConnectionTypeHint('trino')).toBe('HTTP / HTTPS / catalog.schema'); expect(getConnectionTypeHint('duckdb')).toBe('本地文件连接'); expect(getConnectionTypeHint('mysql')).toBe('标准连接配置'); }); diff --git a/frontend/src/utils/connectionTypeCatalog.ts b/frontend/src/utils/connectionTypeCatalog.ts index 696f34b..40e9f53 100644 --- a/frontend/src/utils/connectionTypeCatalog.ts +++ b/frontend/src/utils/connectionTypeCatalog.ts @@ -18,6 +18,7 @@ export const CONNECTION_TYPE_GROUPS: ConnectionTypeCatalogGroup[] = [ { key: 'starrocks', name: 'StarRocks' }, { key: 'sphinx', name: 'Sphinx' }, { key: 'clickhouse', name: 'ClickHouse' }, + { key: 'trino', name: 'Trino' }, { key: 'postgres', name: 'PostgreSQL' }, { key: 'sqlserver', name: 'SQL Server' }, { key: 'iris', name: 'InterSystems IRIS' }, @@ -97,6 +98,8 @@ export const getConnectionTypeDefaultPort = (type: string): number => { return 9306; case 'clickhouse': return 9000; + case 'trino': + return 8080; case 'postgres': case 'opengauss': case 'gaussdb': @@ -180,6 +183,8 @@ export const getConnectionTypeHint = (type: string): string => { case 'sqlite': case 'duckdb': return '本地文件连接'; + case 'trino': + return 'HTTP / HTTPS / catalog.schema'; default: return '标准连接配置'; } diff --git a/frontend/src/utils/dataSourceCapabilities.test.ts b/frontend/src/utils/dataSourceCapabilities.test.ts index a82ebf3..a2674a2 100644 --- a/frontend/src/utils/dataSourceCapabilities.test.ts +++ b/frontend/src/utils/dataSourceCapabilities.test.ts @@ -58,6 +58,19 @@ describe('dataSourceCapabilities', () => { }); }); + it('treats Trino as an editable SQL datasource without database-level DDL shortcuts', () => { + expect(getDataSourceCapabilities({ type: 'trino' })).toMatchObject({ + type: 'trino', + supportsQueryEditor: true, + supportsSqlQueryExport: true, + supportsCopyInsert: true, + supportsCreateDatabase: false, + supportsRenameDatabase: false, + supportsDropDatabase: false, + forceReadOnlyQueryResult: false, + }); + }); + it('keeps InterSystems IRIS as an editable SQL datasource capability', () => { expect(getDataSourceCapabilities({ type: 'iris' })).toMatchObject({ type: 'iris', diff --git a/frontend/src/utils/dataSourceCapabilities.ts b/frontend/src/utils/dataSourceCapabilities.ts index 7a64616..737e93b 100644 --- a/frontend/src/utils/dataSourceCapabilities.ts +++ b/frontend/src/utils/dataSourceCapabilities.ts @@ -111,6 +111,7 @@ const SQL_QUERY_EXPORT_TYPES = new Set([ 'dameng', 'tdengine', 'clickhouse', + 'trino', ]); const COPY_INSERT_TYPES = new Set([ @@ -135,6 +136,7 @@ const COPY_INSERT_TYPES = new Set([ 'dameng', 'tdengine', 'clickhouse', + 'trino', ]); const QUERY_EDITOR_DISABLED_TYPES = new Set(['redis']); diff --git a/frontend/src/utils/sqlEditorTransaction.test.ts b/frontend/src/utils/sqlEditorTransaction.test.ts index efd76e5..a0cdb73 100644 --- a/frontend/src/utils/sqlEditorTransaction.test.ts +++ b/frontend/src/utils/sqlEditorTransaction.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'; import { resolveSqlEditorOperationKeyword, shouldUseSqlEditorManagedTransaction, + shouldUseSqlEditorManagedTransactionForType, } from './sqlEditorTransaction'; describe('sqlEditorTransaction', () => { @@ -44,4 +45,10 @@ describe('sqlEditorTransaction', () => { 'DELETE FROM users WHERE id = 1', ])).toBe(false); }); + + it('keeps Trino DML on the plain multi-statement execution path', () => { + expect(shouldUseSqlEditorManagedTransactionForType('trino', [ + 'UPDATE hive.default.orders SET status = \'done\'', + ])).toBe(false); + }); }); diff --git a/frontend/src/utils/sqlEditorTransaction.ts b/frontend/src/utils/sqlEditorTransaction.ts index 6cc505f..69ef545 100644 --- a/frontend/src/utils/sqlEditorTransaction.ts +++ b/frontend/src/utils/sqlEditorTransaction.ts @@ -249,7 +249,13 @@ const isSqlEditorTransactionControlStatement = (statement: string): boolean => { return keyword === 'start' && /\btransaction\b/i.test(statement); }; -export const shouldUseSqlEditorManagedTransaction = (statements: string[]): boolean => { +export const shouldUseSqlEditorManagedTransactionForType = ( + type: string, + statements: string[], +): boolean => { + if (String(type || '').trim().toLowerCase() === 'trino') { + return false; + } let hasManagedWrite = false; for (const statement of statements) { const trimmed = String(statement || '').trim(); @@ -265,3 +271,6 @@ export const shouldUseSqlEditorManagedTransaction = (statements: string[]): bool } return hasManagedWrite; }; + +export const shouldUseSqlEditorManagedTransaction = (statements: string[]): boolean => + shouldUseSqlEditorManagedTransactionForType('', statements); diff --git a/go.mod b/go.mod index b49cb10..ee928f7 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/segmentio/kafka-go v0.4.51 github.com/sijms/go-ora/v2 v2.9.0 github.com/taosdata/driver-go/v3 v3.7.8 + github.com/trinodb/trino-go-client v0.333.0 github.com/wailsapp/wails/v2 v2.11.0 github.com/xuri/excelize/v2 v2.10.0 go.mongodb.org/mongo-driver v1.17.9 @@ -44,10 +45,18 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/mock v1.5.0 // indirect github.com/google/jsonschema-go v0.4.3 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/segmentio/encoding v0.5.4 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/tidwall/gjson v1.14.2 // indirect diff --git a/go.sum b/go.sum index b9ecdff..215ae28 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gitea.com/kingbase/gokb v0.0.0-20201021123113-29bd62a876c3 h1:QjslQNaH5Nuap5i4nijS0OYV6GMk5kqrAmgU90zBKd4= @@ -19,6 +21,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZb github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -30,6 +34,12 @@ github.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLR github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g= github.com/HuaweiCloudDeveloper/gaussdb-go v1.0.0-rc1 h1:OIZ83SgbK0ImF/vKFSfNoCAQWQySx5sYhj0RgsWbMbA= github.com/HuaweiCloudDeveloper/gaussdb-go v1.0.0-rc1/go.mod h1:Xf3AtRet+/ygewEMfzt0FbIFydPkv47Hx5Xz3w07+yo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:3YVZUqkoev4mL+aCwVOSWV4M7pN+NURHL38Z2zq5JKA= +github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:ymXt5bw5uSNu4jveerFxE0vNYxF8ncqbptntMaFMg3k= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/apache/arrow-go/v18 v18.5.1 h1:yaQ6zxMGgf9YCYw4/oaeOU3AULySDlAYDOcnr4LdHdI= @@ -41,12 +51,52 @@ github.com/apache/rocketmq-client-go/v2 v2.1.2/go.mod h1:6I6vgxHR3hzrvn+6n/4mrhS github.com/apache/thrift v0.15.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4= +github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00= +github.com/aws/aws-sdk-go-v2/config v1.31.8 h1:kQjtOLlTU4m4A64TsRcqwNChhGCwaPBt+zCQt/oWsHU= +github.com/aws/aws-sdk-go-v2/config v1.31.8/go.mod h1:QPpc7IgljrKwH0+E6/KolCgr4WPLerURiU592AYzfSY= +github.com/aws/aws-sdk-go-v2/credentials v1.18.12 h1:zmc9e1q90wMn8wQbjryy8IwA6Q4XlaL9Bx2zIqdNNbk= +github.com/aws/aws-sdk-go-v2/credentials v1.18.12/go.mod h1:3VzdRDR5u3sSJRI4kYcOSIBbeYsgtVk7dG5R/U6qLWY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 h1:Is2tPmieqGS2edBnmOJIbdvOA6Op+rRpaYR60iBAwXM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7/go.mod h1:F1i5V5421EGci570yABvpIXgRIBPb5JM+lSkHF6Dq5w= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 h1:UCxq0X9O3xrlENdKf1r9eRJoKz/b0AfGkpp3a7FPlhg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7/go.mod h1:rHRoJUNUASj5Z/0eqI4w32vKvC7atoWR0jC+IkmVH8k= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 h1:Y6DTZUn7ZUC4th9FMBbo8LVE+1fyq3ofw+tRwkUd3PY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7/go.mod h1:x3XE6vMnU9QvHN/Wrx2s44kwzV2o2g5x/siw4ZUJ9g8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.7 h1:BszAktdUo2xlzmYHjWMq70DqJ7cROM8iBd3f6hrpuMQ= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.7/go.mod h1:XJ1yHki/P7ZPuG4fd3f0Pg/dSGA2cTQBCLw82MH2H48= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.7 h1:zmZ8qvtE9chfhBPuKB2aQFxW5F/rpwXUgmcVCgQzqRw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.7/go.mod h1:vVYfbpd2l+pKqlSIDIOgouxNsGu5il9uDp0ooWb0jys= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 h1:mLgc5QIgOy26qyh5bvW+nDoAppxgn3J2WV3m9ewq7+8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7/go.mod h1:wXb/eQnqt8mDQIQTTmcw58B5mYGxzLGZGK8PWNFZ0BA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.7 h1:u3VbDKUCWarWiU+aIUK4gjTr/wQFXV17y3hgNno9fcA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.7/go.mod h1:/OuMQwhSyRapYxq6ZNpPer8juGNrB4P5Oz8bZ2cgjQE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.88.1 h1:+RpGuaQ72qnU83qBKVwxkznewEdAGhIWo/PQCmkhhog= +github.com/aws/aws-sdk-go-v2/service/s3 v1.88.1/go.mod h1:xajPTguLoeQMAOE44AAP2RQoUhF8ey1g5IFHARv71po= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 h1:7PKX3VYsZ8LUWceVRuv0+PU+E7OtQb1lgmi5vmUE9CM= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.3/go.mod h1:Ql6jE9kyyWI5JHn+61UT/Y5Z0oyVJGmgmJbZD5g4unY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 h1:e0XBRn3AptQotkyBFrHAxFB8mDhAIOfsG+7KyJ0dg98= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4/go.mod h1:XclEty74bsGBCr1s0VSaA11hQ4ZidK4viWK7rRfO88I= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 h1:PR00NXRYgY4FWHqOGx3fC3lhVKjsp1GdloDv2ynMSd8= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.4/go.mod h1:Z+Gd23v97pX9zK97+tX4ppAgqCt3Z2dIXB02CtBncK8= +github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= +github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -56,6 +106,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -64,6 +116,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY= +github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/duckdb/duckdb-go-bindings v0.3.3 h1:lXogtCY8hiGLQvTfK55HcgvaA3K2MrwKeZGqhIin35U= github.com/duckdb/duckdb-go-bindings v0.3.3/go.mod h1:zS7OpBP8zwVlP38OljRZOnqWYlNd4KLcVfMoA1JFzpk= github.com/duckdb/duckdb-go-bindings/lib/darwin-amd64 v0.3.3 h1:ue8BtIOSt+2Bt2fEfTAvBcQLxzBFhgfCcyzPtqQWTRA= @@ -94,6 +154,8 @@ github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FM github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= @@ -161,16 +223,25 @@ github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -188,6 +259,18 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -242,6 +325,12 @@ github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpsp github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU= github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -268,11 +357,21 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runc v1.3.1 h1:c/yY0oh2wK7tzDuD56REnSxyU8ubh8hoAIOLGLrm4SM= +github.com/opencontainers/runc v1.3.1/go.mod h1:9wbWt42gV+KRxKRVVugNP6D5+PQciRbenB4fLVsqGPs= +github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= +github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s= github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= +github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -325,11 +424,13 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= @@ -349,6 +450,8 @@ github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/trinodb/trino-go-client v0.333.0 h1:+bsW8/uLFNF00MEL9JZJym94LlUnle25VgDlWGPEZos= +github.com/trinodb/trino-go-client v0.333.0/go.mod h1:91okdYtRUZoj3XJu/tqdzu11sNliQuN4A+vMFEB8GVE= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= @@ -367,6 +470,12 @@ github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4= @@ -413,6 +522,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -437,6 +547,7 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -446,6 +557,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -483,6 +596,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= @@ -490,6 +604,7 @@ golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/T golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -498,6 +613,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/app/db_context.go b/internal/app/db_context.go index 3930394..a440852 100644 --- a/internal/app/db_context.go +++ b/internal/app/db_context.go @@ -26,7 +26,7 @@ func normalizeRunConfig(config connection.ConnectionConfig, dbName string) conne if !isOceanBaseOracleProtocol(config) { runConfig.Database = name } - case "mysql", "mariadb", "goldendb", "greatdb", "gdb", "diros", "starrocks", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "gaussdb", "sqlserver", "iris", "intersystems", "intersystemsiris", "inter-systems", "inter-systems-iris", "mongodb", "tdengine", "iotdb", "clickhouse", "rabbitmq", "rabbit-mq", "rabbit_mq": + case "mysql", "mariadb", "goldendb", "greatdb", "gdb", "diros", "starrocks", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "gaussdb", "sqlserver", "iris", "intersystems", "intersystemsiris", "inter-systems", "inter-systems-iris", "mongodb", "tdengine", "iotdb", "clickhouse", "trino", "rabbitmq", "rabbit-mq", "rabbit_mq": // 这些类型的 dbName 表示"数据库",需要写入连接配置以选择目标库。 runConfig.Database = name case "dameng": @@ -57,7 +57,7 @@ func normalizeSchemaAndTable(config connection.ConnectionConfig, dbName string, // Elasticsearch:索引名可能含多个点(如 iot_pro_biz_operate_log.index.20240626), // 不能按点分割,直接返回原始数据库名和完整表名。 - if dbType == "elasticsearch" || dbType == "iotdb" || dbType == "rocketmq" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" { + if dbType == "elasticsearch" || dbType == "iotdb" || dbType == "rocketmq" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" || dbType == "trino" { return rawDB, rawTable } @@ -116,7 +116,7 @@ func normalizeSchemaAndTable(config connection.ConnectionConfig, dbName string, func normalizeMetadataSchemaAndTable(config connection.ConnectionConfig, dbName string, tableName string) (string, string) { schema, table := normalizeSchemaAndTable(config, dbName, tableName) switch resolveDDLDBType(config) { - case "rocketmq", "mqtt", "kafka", "rabbitmq": + case "rocketmq", "mqtt", "kafka", "rabbitmq", "trino": return schema, table case "postgres", "kingbase", "highgo", "vastbase", "opengauss", "gaussdb": rawTable := strings.TrimSpace(tableName) diff --git a/internal/app/db_context_test.go b/internal/app/db_context_test.go index 31a11cd..f1a3bdc 100644 --- a/internal/app/db_context_test.go +++ b/internal/app/db_context_test.go @@ -50,6 +50,18 @@ func TestNormalizeSchemaAndTable_PostgresStillSplitsQualifiedName(t *testing.T) } } +func TestNormalizeSchemaAndTable_TrinoPreservesDottedTableName(t *testing.T) { + t.Parallel() + + schema, table := normalizeSchemaAndTable(connection.ConnectionConfig{ + Type: "trino", + }, "hive.default", "daily.events.v1") + + if schema != "hive.default" || table != "daily.events.v1" { + t.Fatalf("expected trino table name to stay intact, got %q.%q", schema, table) + } +} + func TestNormalizeSchemaAndTable_KingbaseNormalizesEscapedQualifiedName(t *testing.T) { t.Parallel() @@ -158,6 +170,18 @@ func TestNormalizeMetadataSchemaAndTable_NonPGLikeKeepsNormalBehavior(t *testing } } +func TestNormalizeMetadataSchemaAndTable_TrinoPreservesDottedTableName(t *testing.T) { + t.Parallel() + + schema, table := normalizeMetadataSchemaAndTable(connection.ConnectionConfig{ + Type: "trino", + }, "iceberg.analytics", "ods.orders.v1") + + if schema != "iceberg.analytics" || table != "ods.orders.v1" { + t.Fatalf("expected trino metadata table to stay intact, got %q.%q", schema, table) + } +} + func TestNormalizeSchemaAndTable_PGLikePureTableStillSplitsKingbaseSearchPathOnlyInMetadata(t *testing.T) { t.Parallel() @@ -216,6 +240,19 @@ func TestNormalizeRunConfig_StarRocksUsesDatabaseFromTree(t *testing.T) { } } +func TestNormalizeRunConfig_TrinoUsesNamespaceFromTree(t *testing.T) { + t.Parallel() + + runConfig := normalizeRunConfig(connection.ConnectionConfig{ + Type: "trino", + Database: "hive.default", + }, "iceberg.analytics") + + if runConfig.Database != "iceberg.analytics" { + t.Fatalf("expected trino namespace from tree, got %q", runConfig.Database) + } +} + func TestNormalizeRunConfig_GoldenDBUsesDatabaseFromTree(t *testing.T) { t.Parallel() diff --git a/internal/app/db_proxy.go b/internal/app/db_proxy.go index fd07e80..22156eb 100644 --- a/internal/app/db_proxy.go +++ b/internal/app/db_proxy.go @@ -233,6 +233,8 @@ func defaultPortByType(driverType string) int { return 27017 case "clickhouse": return 9000 + case "trino": + return 8080 case "highgo": return 5866 case "iris": diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index 1a0a484..6c79970 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -496,8 +496,8 @@ func normalizeSchemaAndTableByType(dbType string, dbName string, tableName strin return rawDB, rawTable } - // Elasticsearch / RocketMQ / MQTT / RabbitMQ / Kafka:对象名可能含多个点或路径,不能按点分割 - if dbType == "elasticsearch" || dbType == "rocketmq" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" { + // Elasticsearch / RocketMQ / MQTT / RabbitMQ / Kafka / Trino:对象名可能含多个点或路径,不能按点分割 + if dbType == "elasticsearch" || dbType == "rocketmq" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" || dbType == "trino" { return rawDB, rawTable } @@ -575,12 +575,35 @@ func resolveCreateStatementTargets(config connection.ConnectionConfig, dbType st func quoteTableIdentByType(dbType string, schema string, table string) string { s := strings.TrimSpace(schema) t := strings.TrimSpace(table) + if dbType == "trino" { + catalog, namespace := splitTrinoNamespace(s) + switch { + case catalog == "" && namespace == "": + return quoteIdentByType(dbType, t) + case namespace == "": + return fmt.Sprintf("%s.%s", quoteIdentByType(dbType, catalog), quoteIdentByType(dbType, t)) + default: + return fmt.Sprintf("%s.%s.%s", quoteIdentByType(dbType, catalog), quoteIdentByType(dbType, namespace), quoteIdentByType(dbType, t)) + } + } if s == "" { return quoteIdentByType(dbType, t) } return fmt.Sprintf("%s.%s", quoteIdentByType(dbType, s), quoteIdentByType(dbType, t)) } +func splitTrinoNamespace(raw string) (string, string) { + text := strings.TrimSpace(raw) + if text == "" { + return "", "" + } + parts := strings.SplitN(text, ".", 2) + if len(parts) == 1 { + return strings.TrimSpace(parts[0]), "" + } + return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) +} + func buildRunConfigForDDL(config connection.ConnectionConfig, dbType string, dbName string) connection.ConnectionConfig { runConfig := normalizeRunConfig(config, dbName) if strings.EqualFold(strings.TrimSpace(config.Type), "custom") { diff --git a/internal/app/methods_db_create_statement_test.go b/internal/app/methods_db_create_statement_test.go index 46d07fa..9323ff2 100644 --- a/internal/app/methods_db_create_statement_test.go +++ b/internal/app/methods_db_create_statement_test.go @@ -193,6 +193,24 @@ func TestNormalizeSchemaAndTableByType_RabbitMQPreservesDottedQueueName(t *testi } } +func TestNormalizeSchemaAndTableByType_TrinoPreservesDottedTableName(t *testing.T) { + t.Parallel() + + schema, table := normalizeSchemaAndTableByType("trino", "hive.default", "orders.events.v1") + if schema != "hive.default" || table != "orders.events.v1" { + t.Fatalf("expected trino table name to stay intact, got %q.%q", schema, table) + } +} + +func TestQuoteTableIdentByType_TrinoKeepsCatalogSchemaAndDottedTable(t *testing.T) { + t.Parallel() + + got := quoteTableIdentByType("trino", "hive.default", "orders.events.v1") + if got != `"hive"."default"."orders.events.v1"` { + t.Fatalf("unexpected trino quoted table: %s", got) + } +} + func TestBuildRunConfigForDDL_CustomHighGoUsesDatabase(t *testing.T) { t.Parallel() diff --git a/internal/app/methods_db_transaction.go b/internal/app/methods_db_transaction.go index a3c39ef..6089737 100644 --- a/internal/app/methods_db_transaction.go +++ b/internal/app/methods_db_transaction.go @@ -278,6 +278,9 @@ func executeManagedSQLTransactionStatements(ctx context.Context, session db.Stat } func shouldUseManagedSQLTransaction(dbType string, query string) bool { + if strings.EqualFold(strings.TrimSpace(dbType), "trino") { + return false + } statements := splitSQLStatements(query) hasManagedWrite := false for _, stmt := range statements { diff --git a/internal/app/methods_db_transaction_test.go b/internal/app/methods_db_transaction_test.go new file mode 100644 index 0000000..23037e5 --- /dev/null +++ b/internal/app/methods_db_transaction_test.go @@ -0,0 +1,14 @@ +package app + +import "testing" + +func TestShouldUseManagedSQLTransaction_TrinoAlwaysUsesPlainExecution(t *testing.T) { + t.Parallel() + + if shouldUseManagedSQLTransaction("trino", "UPDATE hive.default.orders SET status = 'done'") { + t.Fatal("expected trino DML to skip SQL editor managed transactions") + } + if shouldUseManagedSQLTransaction("trino", "BEGIN; UPDATE hive.default.orders SET status = 'done'; COMMIT;") { + t.Fatal("expected trino explicit transactions to stay unmanaged") + } +} diff --git a/internal/app/methods_driver.go b/internal/app/methods_driver.go index ac13650..9aa9c83 100644 --- a/internal/app/methods_driver.go +++ b/internal/app/methods_driver.go @@ -395,7 +395,8 @@ const builtinDriverManifestJSON = `{ "tdengine": { "engine": "go", "version": "3.7.8", "checksumPolicy": "off", "downloadUrl": "builtin://activate/tdengine" }, "iotdb": { "engine": "go", "version": "1.3.7", "checksumPolicy": "off", "downloadUrl": "builtin://activate/iotdb" }, "clickhouse": { "engine": "go", "version": "2.43.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/clickhouse" }, - "elasticsearch": { "engine": "go", "version": "8.19.6", "checksumPolicy": "off", "downloadUrl": "builtin://activate/elasticsearch" } + "elasticsearch": { "engine": "go", "version": "8.19.6", "checksumPolicy": "off", "downloadUrl": "builtin://activate/elasticsearch" }, + "trino": { "engine": "go", "version": "0.333.0", "checksumPolicy": "off", "downloadUrl": "builtin://activate/trino" } } }` @@ -462,6 +463,7 @@ var latestDriverVersionMap = map[string]string{ "iotdb": "1.3.7", "clickhouse": "2.43.1", "elasticsearch": "8.19.6", + "trino": "0.333.0", "oracle": "2.9.0", "postgres": "1.11.2", "redis": "9.17.3", @@ -489,6 +491,7 @@ var driverGoModulePathMap = map[string]string{ "iotdb": "github.com/apache/iotdb-client-go", "clickhouse": "github.com/ClickHouse/clickhouse-go/v2", "elasticsearch": "github.com/elastic/go-elasticsearch/v8", + "trino": "github.com/trinodb/trino-go-client", } var driverGoModuleAliasPathMap = map[string][]string{ @@ -1745,6 +1748,7 @@ func allDriverDefinitionsWithPackages(packages map[string]pinnedDriverPackage) [ buildOptionalGoDriverDefinition("iotdb", "Apache IoTDB", packages), buildOptionalGoDriverDefinition("clickhouse", "ClickHouse", packages), buildOptionalGoDriverDefinition("elasticsearch", "Elasticsearch", packages), + buildOptionalGoDriverDefinition("trino", "Trino", packages), } } @@ -4330,6 +4334,8 @@ func optionalDriverBuildTag(driverType string, selectedVersion string) (string, return "gonavi_clickhouse_driver", nil case "elasticsearch": return "gonavi_elasticsearch_driver", nil + case "trino": + return "gonavi_trino_driver", nil default: return "", fmt.Errorf("未配置驱动构建标签:%s", driverType) } diff --git a/internal/app/methods_driver_agent_revision_test.go b/internal/app/methods_driver_agent_revision_test.go index be16c7b..5bd2a27 100644 --- a/internal/app/methods_driver_agent_revision_test.go +++ b/internal/app/methods_driver_agent_revision_test.go @@ -231,6 +231,7 @@ func optionalDriverAgentRevisionTestDrivers(t *testing.T) []string { "iotdb", "clickhouse", "elasticsearch", + "trino", } for _, driverType := range drivers { if db.OptionalDriverAgentRevision(driverType) == "" { diff --git a/internal/app/methods_driver_version_test.go b/internal/app/methods_driver_version_test.go index 8e29651..f875f5d 100644 --- a/internal/app/methods_driver_version_test.go +++ b/internal/app/methods_driver_version_test.go @@ -504,6 +504,39 @@ func TestElasticsearchDriverDefinitionUsesOptionalAgent(t *testing.T) { } } +func TestTrinoDriverDefinitionUsesOptionalAgent(t *testing.T) { + definition, ok := resolveDriverDefinition("trino") + if !ok { + t.Fatal("expected trino driver definition") + } + if definition.Name != "Trino" { + t.Fatalf("unexpected trino driver name: %q", definition.Name) + } + if definition.BuiltIn { + t.Fatal("expected trino to be an optional driver agent") + } + if driverGoModulePathMap["trino"] != "github.com/trinodb/trino-go-client" { + t.Fatalf("unexpected trino go module path: %q", driverGoModulePathMap["trino"]) + } + if definition.PinnedVersion != "0.333.0" { + t.Fatalf("unexpected trino definition pinned version: %q", definition.PinnedVersion) + } + if definition.DefaultDownloadURL != "builtin://activate/trino" { + t.Fatalf("unexpected trino default download URL: %q", definition.DefaultDownloadURL) + } + if latestDriverVersionMap["trino"] != "0.333.0" { + t.Fatalf("unexpected trino pinned version: %q", latestDriverVersionMap["trino"]) + } + + tags, err := optionalDriverBuildTags("trino", "") + if err != nil { + t.Fatalf("resolve trino build tags failed: %v", err) + } + if tags != "gonavi_trino_driver" { + t.Fatalf("unexpected trino build tag: %q", tags) + } +} + func TestIoTDBDriverDefinitionUsesOptionalAgent(t *testing.T) { definition, ok := resolveDriverDefinition("iotdb") if !ok { diff --git a/internal/app/methods_file.go b/internal/app/methods_file.go index ca0b924..59e4aa3 100644 --- a/internal/app/methods_file.go +++ b/internal/app/methods_file.go @@ -2850,6 +2850,20 @@ func quoteQualifiedIdentByType(dbType string, ident string) string { } dbType = resolveDDLDBType(connection.ConnectionConfig{Type: dbType}) + if dbType == "trino" { + parts := strings.Split(raw, ".") + switch { + case len(parts) >= 3: + catalog := strings.TrimSpace(parts[0]) + schema := strings.TrimSpace(parts[1]) + table := strings.TrimSpace(strings.Join(parts[2:], ".")) + if catalog != "" && schema != "" && table != "" { + return quoteIdentByType(dbType, catalog) + "." + quoteIdentByType(dbType, schema) + "." + quoteIdentByType(dbType, table) + } + case len(parts) <= 2: + return quoteIdentByType(dbType, raw) + } + } if dbType == "kingbase" { schema, table := db.SplitKingbaseQualifiedName(raw) if table == "" { diff --git a/internal/db/database_optional_factories_full.go b/internal/db/database_optional_factories_full.go index bc90d16..05338b2 100644 --- a/internal/db/database_optional_factories_full.go +++ b/internal/db/database_optional_factories_full.go @@ -23,4 +23,5 @@ func registerOptionalDatabaseFactories() { registerDatabaseFactory(newOptionalDriverAgentDatabase("iotdb"), "iotdb", "apache-iotdb", "apache_iotdb") registerDatabaseFactory(newOptionalDriverAgentDatabase("clickhouse"), "clickhouse") registerDatabaseFactory(newOptionalDriverAgentDatabase("elasticsearch"), "elasticsearch", "elastic") + registerDatabaseFactory(newOptionalDriverAgentDatabase("trino"), "trino") } diff --git a/internal/db/database_optional_factories_lite.go b/internal/db/database_optional_factories_lite.go index 29bd12f..9db6ec8 100644 --- a/internal/db/database_optional_factories_lite.go +++ b/internal/db/database_optional_factories_lite.go @@ -23,4 +23,5 @@ func registerOptionalDatabaseFactories() { registerDatabaseFactory(newOptionalDriverAgentDatabase("iotdb"), "iotdb", "apache-iotdb", "apache_iotdb") registerDatabaseFactory(newOptionalDriverAgentDatabase("clickhouse"), "clickhouse") registerDatabaseFactory(newOptionalDriverAgentDatabase("elasticsearch"), "elasticsearch", "elastic") + registerDatabaseFactory(newOptionalDriverAgentDatabase("trino"), "trino") } diff --git a/internal/db/driver_agent_revisions_gen.go b/internal/db/driver_agent_revisions_gen.go index 01d1c81..e05b8c9 100644 --- a/internal/db/driver_agent_revisions_gen.go +++ b/internal/db/driver_agent_revisions_gen.go @@ -24,5 +24,6 @@ func init() { "iotdb": "src-5ba9da13c6a272f9", "clickhouse": "src-99c8babfefdf142c", "elasticsearch": "src-36b2e2b5f49db9d1", + "trino": "src-d264ceca132c185c", } } diff --git a/internal/db/driver_support.go b/internal/db/driver_support.go index 664fd19..0ca7b58 100644 --- a/internal/db/driver_support.go +++ b/internal/db/driver_support.go @@ -48,6 +48,7 @@ var optionalGoDrivers = map[string]struct{}{ "iotdb": {}, "clickhouse": {}, "elasticsearch": {}, + "trino": {}, } // optionalDriverAgentRevisions 记录 GoNavi 对各可选 driver-agent 包装逻辑的兼容版本。 @@ -150,6 +151,8 @@ func driverDisplayName(driverType string) string { return "ClickHouse" case "elasticsearch": return "Elasticsearch" + case "trino": + return "Trino" case "chroma": return "Chroma" case "qdrant": diff --git a/internal/db/trino_impl.go b/internal/db/trino_impl.go new file mode 100644 index 0000000..2cd0603 --- /dev/null +++ b/internal/db/trino_impl.go @@ -0,0 +1,661 @@ +//go:build gonavi_full_drivers || gonavi_trino_driver + +package db + +import ( + "context" + "database/sql" + "fmt" + "net" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "GoNavi-Wails/internal/connection" + "GoNavi-Wails/internal/logger" + "GoNavi-Wails/internal/ssh" + + trinodriver "github.com/trinodb/trino-go-client/trino" +) + +const ( + defaultTrinoPort = 8080 + defaultTrinoSource = "GoNavi" +) + +type TrinoDB struct { + conn *sql.DB + pingTimeout time.Duration + forwarder *ssh.LocalForwarder + namespace string + customClientName string +} + +func normalizeTrinoConfig(config connection.ConnectionConfig) connection.ConnectionConfig { + normalized := applyTrinoURI(config) + normalized = applyTrinoHostURI(normalized) + if strings.TrimSpace(normalized.Host) == "" { + normalized.Host = "localhost" + } + if normalized.Port <= 0 { + normalized.Port = defaultTrinoPort + } + return normalized +} + +func applyTrinoURI(config connection.ConnectionConfig) connection.ConnectionConfig { + return applyTrinoEndpointURI(config, config.URI, false) +} + +func applyTrinoHostURI(config connection.ConnectionConfig) connection.ConnectionConfig { + return applyTrinoEndpointURI(config, config.Host, true) +} + +func applyTrinoEndpointURI(config connection.ConnectionConfig, raw string, fromHostField bool) connection.ConnectionConfig { + uriText := strings.TrimSpace(raw) + if uriText == "" { + return config + } + parsed, err := url.Parse(uriText) + if err != nil { + return config + } + scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme)) + if scheme != "trino" && scheme != "http" && scheme != "https" { + return config + } + if strings.TrimSpace(parsed.Host) == "" { + return config + } + + if parsed.User != nil { + if strings.TrimSpace(config.User) == "" { + config.User = parsed.User.Username() + } + if pass, ok := parsed.User.Password(); ok && config.Password == "" { + config.Password = pass + } + } + + params := url.Values{} + mergeConnectionParamValues(params, parsed.Query()) + mergeConnectionParamValues(params, connectionParamsFromText(config.ConnectionParams)) + + catalog := strings.TrimSpace(parsed.Query().Get("catalog")) + schema := strings.TrimSpace(parsed.Query().Get("schema")) + if strings.TrimSpace(config.Database) == "" { + config.Database = joinTrinoNamespace(catalog, schema) + } + + if scheme == "https" { + config.UseSSL = true + if normalizeSSLModeValue(config.SSLMode) == sslModeDisable || strings.TrimSpace(config.SSLMode) == "" { + config.SSLMode = sslModeRequired + } + } + + defaultPort := config.Port + if defaultPort <= 0 { + defaultPort = defaultTrinoPort + } + if fromHostField || strings.TrimSpace(config.Host) == "" { + host, port, ok := parseHostPortWithDefault(parsed.Host, defaultPort) + if ok { + config.Host = host + config.Port = port + } + } + if config.Port <= 0 { + config.Port = defaultPort + } + config.ConnectionParams = params.Encode() + return config +} + +func splitTrinoNamespace(raw string) (string, string) { + text := strings.TrimSpace(raw) + if text == "" { + return "", "" + } + parts := strings.SplitN(text, ".", 2) + catalog := strings.TrimSpace(parts[0]) + if len(parts) == 1 { + return catalog, "" + } + return catalog, strings.TrimSpace(parts[1]) +} + +func joinTrinoNamespace(catalog, schema string) string { + c := strings.TrimSpace(catalog) + s := strings.TrimSpace(schema) + switch { + case c == "": + return s + case s == "": + return c + default: + return c + "." + s + } +} + +func resolveTrinoNamespace(raw string, fallback string) (string, string) { + catalog, schema := splitTrinoNamespace(raw) + if catalog != "" || schema != "" { + return catalog, schema + } + return splitTrinoNamespace(fallback) +} + +func quoteTrinoIdentifier(ident string) string { + return `"` + strings.ReplaceAll(strings.TrimSpace(ident), `"`, `""`) + `"` +} + +func quoteTrinoQualifiedTable(catalog, schema, table string) string { + quoted := make([]string, 0, 3) + if trimmed := strings.TrimSpace(catalog); trimmed != "" { + quoted = append(quoted, quoteTrinoIdentifier(trimmed)) + } + if trimmed := strings.TrimSpace(schema); trimmed != "" { + quoted = append(quoted, quoteTrinoIdentifier(trimmed)) + } + quoted = append(quoted, quoteTrinoIdentifier(table)) + return strings.Join(quoted, ".") +} + +func escapeTrinoSQLLiteral(value string) string { + return "'" + strings.ReplaceAll(strings.TrimSpace(value), "'", "''") + "'" +} + +func trinoRowValue(row map[string]interface{}, keys ...string) (interface{}, bool) { + if len(row) == 0 { + return nil, false + } + for _, key := range keys { + for current, value := range row { + if strings.EqualFold(strings.TrimSpace(current), strings.TrimSpace(key)) { + return value, true + } + } + } + return nil, false +} + +func trinoRowString(row map[string]interface{}, keys ...string) string { + value, ok := trinoRowValue(row, keys...) + if !ok || value == nil { + return "" + } + text := strings.TrimSpace(fmt.Sprintf("%v", value)) + if strings.EqualFold(text, "") { + return "" + } + return text +} + +func firstTrinoMapValueAsString(row map[string]interface{}) string { + for _, value := range row { + text := strings.TrimSpace(fmt.Sprintf("%v", value)) + if !strings.EqualFold(text, "") { + return text + } + } + return "" +} + +func firstTrinoRowValueAsString(data []map[string]interface{}) string { + if len(data) == 0 { + return "" + } + return firstTrinoMapValueAsString(data[0]) +} + +func (t *TrinoDB) buildTrinoHTTPClient(config connection.ConnectionConfig) (*http.Client, error) { + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: getConnectTimeout(config), + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 32, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: time.Second, + } + tlsConfig, err := resolveGenericTLSConfig(config) + if err != nil { + return nil, err + } + if tlsConfig != nil { + transport.TLSClientConfig = tlsConfig + } + return &http.Client{Transport: transport}, nil +} + +func (t *TrinoDB) registerTrinoCustomClient(config connection.ConnectionConfig) (string, error) { + client, err := t.buildTrinoHTTPClient(config) + if err != nil { + return "", err + } + name := fmt.Sprintf("gonavi-trino-%d", time.Now().UnixNano()) + if err := trinodriver.RegisterCustomClient(name, client); err != nil { + return "", err + } + return name, nil +} + +func buildTrinoDSN(config connection.ConnectionConfig, customClientName string) (string, error) { + user := strings.TrimSpace(config.User) + if user == "" { + return "", fmt.Errorf("Trino 用户名不能为空") + } + + scheme := "http" + if config.UseSSL { + scheme = "https" + } + if config.Password != "" && scheme != "https" { + return "", fmt.Errorf("Trino 启用密码认证时必须使用 HTTPS") + } + + params := connectionParamsFromText(config.ConnectionParams) + catalog, schema := resolveTrinoNamespace(config.Database, "") + if catalog != "" { + params.Set("catalog", catalog) + } + if schema != "" { + params.Set("schema", schema) + } + if strings.TrimSpace(params.Get("source")) == "" { + params.Set("source", defaultTrinoSource) + } + if strings.TrimSpace(params.Get("explicitPrepare")) == "" { + params.Set("explicitPrepare", "false") + } + if strings.TrimSpace(params.Get("query_timeout")) == "" { + params.Set("query_timeout", fmt.Sprintf("%ds", getConnectTimeoutSeconds(config))) + } + if strings.TrimSpace(customClientName) != "" { + params.Set("custom_client", strings.TrimSpace(customClientName)) + } + + endpoint := &url.URL{ + Scheme: scheme, + Host: net.JoinHostPort(strings.TrimSpace(config.Host), strconv.Itoa(config.Port)), + RawQuery: params.Encode(), + } + if config.Password != "" { + endpoint.User = url.UserPassword(user, config.Password) + } else { + endpoint.User = url.User(user) + } + return endpoint.String(), nil +} + +func (t *TrinoDB) Close() error { + if t.conn != nil { + if err := t.conn.Close(); err != nil { + return err + } + t.conn = nil + } + if t.forwarder != nil { + if err := t.forwarder.Close(); err != nil { + logger.Warnf("关闭 Trino SSH 端口转发失败:%v", err) + } + t.forwarder = nil + } + if t.customClientName != "" { + trinodriver.DeregisterCustomClient(t.customClientName) + t.customClientName = "" + } + t.namespace = "" + return nil +} + +func (t *TrinoDB) Connect(config connection.ConnectionConfig) error { + _ = t.Close() + + runConfig := normalizeTrinoConfig(config) + t.pingTimeout = getConnectTimeout(runConfig) + + if runConfig.UseSSH { + forwarder, err := ssh.GetOrCreateLocalForwarder(runConfig.SSH, runConfig.Host, runConfig.Port) + if err != nil { + return fmt.Errorf("创建 SSH 隧道失败:%w", err) + } + t.forwarder = forwarder + + host, portText, err := net.SplitHostPort(forwarder.LocalAddr) + if err != nil { + _ = t.Close() + return fmt.Errorf("解析本地转发地址失败:%w", err) + } + port, err := strconv.Atoi(portText) + if err != nil { + _ = t.Close() + return fmt.Errorf("解析本地端口失败:%w", err) + } + runConfig.Host = host + runConfig.Port = port + runConfig.UseSSH = false + logger.Infof("Trino 通过本地端口转发连接:%s -> %s:%d", forwarder.LocalAddr, config.Host, config.Port) + } + + customClientName, err := t.registerTrinoCustomClient(runConfig) + if err != nil { + _ = t.Close() + return fmt.Errorf("注册 Trino 自定义 HTTP 客户端失败:%w", err) + } + t.customClientName = customClientName + + dsn, err := buildTrinoDSN(runConfig, customClientName) + if err != nil { + _ = t.Close() + return err + } + conn, err := sql.Open("trino", dsn) + if err != nil { + _ = t.Close() + return err + } + t.conn = conn + t.namespace = strings.TrimSpace(runConfig.Database) + if err := t.Ping(); err != nil { + _ = t.Close() + return err + } + return nil +} + +func (t *TrinoDB) Ping() error { + if t.conn == nil { + return fmt.Errorf("连接未打开") + } + timeout := t.pingTimeout + if timeout <= 0 { + timeout = 10 * time.Second + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + rows, err := t.conn.QueryContext(ctx, "SELECT 1") + if err != nil { + return err + } + defer rows.Close() + if !rows.Next() { + if err := rows.Err(); err != nil { + return err + } + return fmt.Errorf("连接查询验证未返回结果") + } + var value sql.NullInt64 + if err := rows.Scan(&value); err != nil { + return err + } + return rows.Err() +} + +func (t *TrinoDB) QueryContext(ctx context.Context, query string) ([]map[string]interface{}, []string, error) { + if t.conn == nil { + return nil, nil, fmt.Errorf("连接未打开") + } + rows, err := t.conn.QueryContext(ctx, query) + if err != nil { + return nil, nil, err + } + defer rows.Close() + return scanRows(rows) +} + +func (t *TrinoDB) Query(query string) ([]map[string]interface{}, []string, error) { + return t.QueryContext(context.Background(), query) +} + +func (t *TrinoDB) StreamQueryContext(ctx context.Context, query string, consumer QueryStreamConsumer) error { + if t.conn == nil { + return fmt.Errorf("连接未打开") + } + rows, err := t.conn.QueryContext(ctx, query) + if err != nil { + return err + } + defer rows.Close() + return streamRows(rows, consumer) +} + +func (t *TrinoDB) StreamQuery(query string, consumer QueryStreamConsumer) error { + return t.StreamQueryContext(context.Background(), query, consumer) +} + +func (t *TrinoDB) ExecContext(ctx context.Context, query string) (int64, error) { + if t.conn == nil { + return 0, fmt.Errorf("连接未打开") + } + res, err := t.conn.ExecContext(ctx, query) + if err != nil { + return 0, err + } + affected, err := res.RowsAffected() + if err != nil { + return 0, nil + } + return affected, nil +} + +func (t *TrinoDB) Exec(query string) (int64, error) { + return t.ExecContext(context.Background(), query) +} + +func (t *TrinoDB) queryTrinoSingleColumnStrings(query string) ([]string, error) { + data, _, err := t.Query(query) + if err != nil { + return nil, err + } + result := make([]string, 0, len(data)) + for _, row := range data { + text := firstTrinoMapValueAsString(row) + if text != "" { + result = append(result, text) + } + } + return result, nil +} + +func (t *TrinoDB) GetDatabases() ([]string, error) { + catalogs, err := t.queryTrinoSingleColumnStrings("SHOW CATALOGS") + if err != nil { + if strings.TrimSpace(t.namespace) != "" { + return []string{t.namespace}, nil + } + return nil, err + } + + namespaces := make([]string, 0, len(catalogs)*2) + seen := make(map[string]struct{}, len(catalogs)*4) + var lastErr error + for _, catalog := range catalogs { + query := fmt.Sprintf("SHOW SCHEMAS FROM %s", quoteTrinoIdentifier(catalog)) + schemas, schemaErr := t.queryTrinoSingleColumnStrings(query) + if schemaErr != nil { + lastErr = schemaErr + continue + } + for _, schema := range schemas { + namespace := joinTrinoNamespace(catalog, schema) + if namespace == "" { + continue + } + key := strings.ToLower(namespace) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + namespaces = append(namespaces, namespace) + } + } + + if len(namespaces) == 0 { + if strings.TrimSpace(t.namespace) != "" { + return []string{t.namespace}, nil + } + if lastErr != nil { + return nil, lastErr + } + } + sort.Strings(namespaces) + return namespaces, nil +} + +func (t *TrinoDB) GetTables(dbName string) ([]string, error) { + catalog, schema := resolveTrinoNamespace(dbName, t.namespace) + if catalog == "" || schema == "" { + return nil, fmt.Errorf("Trino 默认命名空间必须使用 catalog.schema") + } + query := fmt.Sprintf("SHOW TABLES FROM %s.%s", quoteTrinoIdentifier(catalog), quoteTrinoIdentifier(schema)) + tables, err := t.queryTrinoSingleColumnStrings(query) + if err != nil { + return nil, err + } + sort.Strings(tables) + return tables, nil +} + +func (t *TrinoDB) GetCreateStatement(dbName, tableName string) (string, error) { + catalog, schema := resolveTrinoNamespace(dbName, t.namespace) + if catalog == "" || schema == "" { + return "", fmt.Errorf("Trino 默认命名空间必须使用 catalog.schema") + } + query := fmt.Sprintf("SHOW CREATE TABLE %s", quoteTrinoQualifiedTable(catalog, schema, tableName)) + data, _, err := t.Query(query) + if err != nil { + return "", err + } + ddl := firstTrinoRowValueAsString(data) + if ddl == "" { + return "", fmt.Errorf("未返回建表语句") + } + return ddl, nil +} + +func buildTrinoColumnsQuery(catalog, schema, tableName string) string { + return fmt.Sprintf(`SELECT + column_name, + data_type, + is_nullable, + column_default +FROM %s.information_schema.columns +WHERE table_schema = %s AND table_name = %s +ORDER BY ordinal_position`, + quoteTrinoIdentifier(catalog), + escapeTrinoSQLLiteral(schema), + escapeTrinoSQLLiteral(tableName), + ) +} + +func buildTrinoColumnDefinitions(data []map[string]interface{}) []connection.ColumnDefinition { + result := make([]connection.ColumnDefinition, 0, len(data)) + for _, row := range data { + column := connection.ColumnDefinition{ + Name: trinoRowString(row, "column_name", "Column", "Field"), + Type: trinoRowString(row, "data_type", "Type"), + Nullable: strings.ToUpper(trinoRowString(row, "is_nullable", "Null")), + } + if rawDefault, ok := trinoRowValue(row, "column_default", "Default"); ok && rawDefault != nil { + def := strings.TrimSpace(fmt.Sprintf("%v", rawDefault)) + if !strings.EqualFold(def, "") && def != "" { + column.Default = &def + } + } + if column.Nullable == "" { + column.Nullable = "YES" + } + result = append(result, column) + } + return result +} + +func (t *TrinoDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) { + catalog, schema := resolveTrinoNamespace(dbName, t.namespace) + if catalog == "" || schema == "" { + return nil, fmt.Errorf("Trino 默认命名空间必须使用 catalog.schema") + } + data, _, err := t.Query(buildTrinoColumnsQuery(catalog, schema, tableName)) + if err == nil { + return buildTrinoColumnDefinitions(data), nil + } + + describeQuery := fmt.Sprintf("DESCRIBE %s", quoteTrinoQualifiedTable(catalog, schema, tableName)) + describeRows, _, describeErr := t.Query(describeQuery) + if describeErr != nil { + return nil, err + } + columns := make([]connection.ColumnDefinition, 0, len(describeRows)) + for _, row := range describeRows { + name := trinoRowString(row, "Column", "column_name", "Field") + if name == "" || strings.HasPrefix(name, "#") { + continue + } + columns = append(columns, connection.ColumnDefinition{ + Name: name, + Type: trinoRowString(row, "Type", "data_type"), + Nullable: "YES", + }) + } + return columns, nil +} + +func (t *TrinoDB) GetAllColumns(dbName string) ([]connection.ColumnDefinitionWithTable, error) { + catalog, schema := resolveTrinoNamespace(dbName, t.namespace) + if catalog == "" || schema == "" { + return nil, fmt.Errorf("Trino 默认命名空间必须使用 catalog.schema") + } + query := fmt.Sprintf(`SELECT + table_name, + column_name, + data_type +FROM %s.information_schema.columns +WHERE table_schema = %s +ORDER BY table_name, ordinal_position`, + quoteTrinoIdentifier(catalog), + escapeTrinoSQLLiteral(schema), + ) + data, _, err := t.Query(query) + if err != nil { + return nil, err + } + result := make([]connection.ColumnDefinitionWithTable, 0, len(data)) + for _, row := range data { + result = append(result, connection.ColumnDefinitionWithTable{ + TableName: trinoRowString(row, "table_name", "TABLE_NAME"), + Name: trinoRowString(row, "column_name", "COLUMN_NAME"), + Type: trinoRowString(row, "data_type", "DATA_TYPE"), + }) + } + return result, nil +} + +func (t *TrinoDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) { + return []connection.IndexDefinition{}, nil +} + +func (t *TrinoDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) { + return []connection.ForeignKeyDefinition{}, nil +} + +func (t *TrinoDB) GetTriggers(dbName, tableName string) ([]connection.TriggerDefinition, error) { + return []connection.TriggerDefinition{}, nil +} + +func (t *TrinoDB) OpenSessionExecer(ctx context.Context) (StatementExecer, error) { + if t.conn == nil { + return nil, fmt.Errorf("连接未打开") + } + conn, err := t.conn.Conn(ctx) + if err != nil { + return nil, err + } + return NewSQLConnStatementExecer(conn), nil +} diff --git a/tools/generate-driver-agent-revisions.sh b/tools/generate-driver-agent-revisions.sh index 81d41d0..5fb20c3 100755 --- a/tools/generate-driver-agent-revisions.sh +++ b/tools/generate-driver-agent-revisions.sh @@ -7,7 +7,7 @@ cd "$SCRIPT_DIR" SCRIPT_DIR_WINDOWS="$(pwd -W 2>/dev/null || true)" SCRIPT_DIR_WINDOWS="${SCRIPT_DIR_WINDOWS//\\//}" -DEFAULT_DRIVERS=(mariadb oceanbase diros starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss gaussdb iris mongodb tdengine iotdb clickhouse elasticsearch) +DEFAULT_DRIVERS=(mariadb oceanbase diros starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss gaussdb iris mongodb tdengine iotdb clickhouse elasticsearch trino) OUTPUT_FILE="internal/db/driver_agent_revisions_gen.go" usage() { @@ -31,7 +31,7 @@ normalize_driver() { opengauss|open_gauss|open-gauss) echo "opengauss" ;; gaussdb|gauss_db|gauss-db) echo "gaussdb" ;; elasticsearch|elastic) echo "elasticsearch" ;; - mariadb|diros|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse) + mariadb|diros|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse|trino) echo "$value" ;; *) @@ -139,7 +139,8 @@ tdengine:internal/db/tdengine_impl.go|\ iotdb:internal/db/iotdb_impl.go|\ clickhouse:internal/db/clickhouse_impl.go|\ elasticsearch:internal/db/elasticsearch_impl.go|\ -elasticsearch:internal/db/elasticsearch_helpers.go) +elasticsearch:internal/db/elasticsearch_helpers.go|\ +trino:internal/db/trino_impl.go) return 0 ;; esac