fix: Trino listTables returns empty and SSL connections not supported (#3947, #3695)

- listTables(null) no longer returns []. Uses buildSchemaFilter() to
  optionally filter by schema, matching the PostgreSQL driver pattern.
- SSL/TLS connections now work. Certificate files (CA, cert, key) are
  read and passed to trino-client. When no certs are provided,
  rejectUnauthorized defaults to false (trust server cert).
- Removed server:ssl from Trino disabledFeatures so the SSL toggle
  appears in the connection form.
- URL parser now auto-enables ssl for https:// URLs.
- Added integration tests for listTables and SSL (with HTTPS-enabled
  Trino testcontainer).
- Added unit tests for SSL config propagation.
- Updated Trino connection docs (EN + ES) with SSL section.
This commit is contained in:
Matthew Rathbone
2026-03-06 14:34:16 -06:00
committed by Day Matchullis
parent 7618aa6e63
commit ab89dcaa67
8 changed files with 391 additions and 22 deletions

View File

@@ -1,10 +1,12 @@
import rawLog from "@bksLogger"
import { readFileSync } from "fs"
import { IDbConnectionDatabase } from "@/lib/db/types"
import {
Trino as TrinoNodeClient,
BasicAuth,
QueryResult,
ConnectionOptions as TrinoConnectionOptions
ConnectionOptions as TrinoConnectionOptions,
SecureContextOptions
} from 'trino-client'
import {
BaseQueryResult,
@@ -41,6 +43,7 @@ import {
createCancelablePromise,
joinFilters
} from "@/common/utils"
import { buildSchemaFilter } from "@/lib/db/clients/utils"
import {
AlterTableSpec,
TableKey
@@ -109,8 +112,30 @@ export class TrinoClient extends BasicDatabaseClient<TrinoResult> {
catalog: this.database.database
}
// TODO: Add ssl using SecureContextOptions (https://trinodb.github.io/trino-js-client/types/ConnectionOptions.html)
if (this.server.config.ssl) {
const sslOptions: SecureContextOptions = {}
if (this.server.config.sslCaFile) {
sslOptions.ca = readFileSync(this.server.config.sslCaFile)
}
if (this.server.config.sslCertFile) {
sslOptions.cert = readFileSync(this.server.config.sslCertFile)
}
if (this.server.config.sslKeyFile) {
sslOptions.key = readFileSync(this.server.config.sslKeyFile)
}
if (!sslOptions.key && !sslOptions.ca && !sslOptions.cert) {
sslOptions.rejectUnauthorized = false
} else {
sslOptions.rejectUnauthorized = this.server.config.sslRejectUnauthorized
}
connectionObj.ssl = sslOptions
}
if ((this.server.config.user != null && this.server.config.user !== '') || (this.server.config.password != null && this.server.config.password !== '')) {
connectionObj.auth = new BasicAuth(this.server.config.user, this.server.config.password)
}
@@ -279,8 +304,9 @@ export class TrinoClient extends BasicDatabaseClient<TrinoResult> {
async listTables(filter?: FilterOptions): Promise<TableOrView[]> {
log.info('filters in listTables', filter)
if (!filter) return []
const sql = `select * from ${this.db}.information_schema.tables`
const schemaFilter = buildSchemaFilter(filter, 'table_schema')
const whereClause = schemaFilter ? `WHERE ${schemaFilter}` : ''
const sql = `select * from ${this.db}.information_schema.tables ${whereClause}`
const result = await this.driverExecuteSingle(sql)
return result.rows.map((row) => ({

View File

@@ -384,6 +384,10 @@ export class SavedConnection extends DbConnectionBase implements IConnection {
this.ssl = true
}
if (cleanedUrl.startsWith('https://')) {
this.ssl = true
}
if (parsed.params?.TrustServerCertificate && parsed.params.TrustServerCertificate === 'true') {
this.trustServerCertificate = true
}

View File

@@ -215,7 +215,6 @@ export const CLIENTS: ClientConfig[] = [
topLevelEntity: 'Catalog',
defaultPort: 8080,
disabledFeatures: [
'server:ssl',
'server:socketPath',
'cancelQuery', // TODO how to do this?
],

View File

@@ -1,6 +1,6 @@
import { Network} from "testcontainers"
import { DBTestUtil, dbtimeout } from "../../../../lib/db"
import { PostgresTestDriver, TrinoTestDriver } from './trino/container'
import { TrinoBackingPostgresDriver, TrinoHttpDriver, TrinoHttpsDriver } from './trino/container'
import { TableOrView } from "@/lib/db/models"
// import { runCommonTests, runReadOnlyTests } from "./all"
@@ -20,30 +20,30 @@ function testWith(dockerTag: TestVersion, socket = false, readonly = false) {
beforeAll(async () => {
const network = await new Network().start()
await PostgresTestDriver.start(dockerTag, socket, readonly, network)
await TrinoBackingPostgresDriver.start(dockerTag, socket, readonly, network)
dbfeeder = new DBTestUtil(PostgresTestDriver.config, "banana", PostgresTestDriver.utilOptions)
dbfeeder = new DBTestUtil(TrinoBackingPostgresDriver.config, "banana", TrinoBackingPostgresDriver.utilOptions)
await dbfeeder.setupdb()
// now set up the trino container
await TrinoTestDriver.start(dockerTag, readonly, network)
await TrinoHttpDriver.start(dockerTag, readonly, network)
util = new DBTestUtil(TrinoTestDriver.config, 'postgresql', { dialect: 'trino' })
util = new DBTestUtil(TrinoHttpDriver.config, 'postgresql', { dialect: 'trino' })
await util.connection.connect()
// Trino uses catalogs instead of databases, access PostgreSQL through 'postgresql' catalog
// util = new DBTestUtil(TrinoTestDriver.config, "postgresql", TrinoTestDriver.utilOptions)
// util = new DBTestUtil(TrinoHttpDriver.config, "postgresql", TrinoHttpDriver.utilOptions)
// await util.setupdb()
})
afterAll(async () => {
// await util.disconnect()
await TrinoTestDriver.stop()
await dbfeeder.disconnect()
if (util.connection) {
await util.connection.disconnect()
}
await TrinoHttpDriver.stop()
await dbfeeder.disconnect()
await TrinoBackingPostgresDriver.stop()
})
describe("Read Operations", () => {
@@ -57,9 +57,14 @@ function testWith(dockerTag: TestVersion, socket = false, readonly = false) {
expect(tableNames).toContain('jobs')
})
it("List tables should return empty array when filter is undefined", async () => {
const tables: TableOrView[] = await util.connection.listTables()
expect(tables).toEqual([])
it("List tables should return tables when filter is null (bug #3947)", async () => {
const tables: TableOrView[] = await util.connection.listTables(null)
expect(tables.length).toBeGreaterThanOrEqual(3)
})
it("List tables should only return tables matching the schema filter", async () => {
const tables: TableOrView[] = await util.connection.listTables({ schema: 'public' })
tables.forEach(t => expect(t.schema).toBe('public'))
})
it("List table columns should work", async () => {
@@ -257,6 +262,62 @@ function testWith(dockerTag: TestVersion, socket = false, readonly = false) {
TEST_VERSIONS.forEach(({ version, socket, readonly }) => testWith(version, socket, readonly))
describe('Trino SSL connection (bug #3695)', () => {
jest.setTimeout(dbtimeout)
// Separate instance to avoid mutating the shared TrinoBackingPostgresDriver singleton
const sslPgDriver = Object.create(TrinoBackingPostgresDriver, {
container: { value: null, writable: true },
config: { value: null, writable: true },
utilOptions: { value: null, writable: true },
})
let dbfeeder: DBTestUtil
let util: DBTestUtil
beforeAll(async () => {
const network = await new Network().start()
await sslPgDriver.start('latest', false, false, network)
dbfeeder = new DBTestUtil(sslPgDriver.config, "banana", sslPgDriver.utilOptions)
await dbfeeder.setupdb()
await TrinoHttpsDriver.start('latest', false, network)
util = new DBTestUtil(TrinoHttpsDriver.config, 'postgresql', { dialect: 'trino' })
await util.connection.connect()
})
afterAll(async () => {
if (util?.connection) {
await util.connection.disconnect()
}
await TrinoHttpsDriver.stop()
await dbfeeder.disconnect()
await sslPgDriver.container?.stop()
})
it("Should connect to Trino over HTTPS with ssl=true and CA cert", async () => {
const version = await util.connection.versionString()
expect(version).toBeDefined()
expect(typeof version).toBe('string')
expect(version.length).toBeGreaterThan(0)
})
it("Should be able to list tables over SSL", async () => {
const tables: TableOrView[] = await util.connection.listTables({ schema: 'public' })
const tableNames = tables.map(t => t.name)
expect(tables.length).toBeGreaterThanOrEqual(3)
expect(tableNames).toContain('people')
})
it("Should be able to query data over SSL", async () => {
const result = await util.connection.selectTop('people', 0, 10, [], [], 'public', [])
expect(result.result).toBeDefined()
expect(Array.isArray(result.result)).toBe(true)
})
})
// Additional util.connection tests
describe('Trino util.Connection Edge Cases', () => {
jest.setTimeout(dbtimeout)

View File

@@ -4,9 +4,10 @@ import { GenericContainer, Wait, StartedTestContainer } from "testcontainers";
import path from 'path'
import os from 'os'
import fs from 'fs'
import { execSync } from 'child_process'
import { IDbConnectionServerConfig } from "@/lib/db/types";
export const PostgresTestDriver = {
export const TrinoBackingPostgresDriver = {
container: null,
utilOptions: null,
config: null,
@@ -87,7 +88,7 @@ export const PostgresTestDriver = {
}
}
export const TrinoTestDriver = {
export const TrinoHttpDriver = {
container: null as StartedTestContainer | null,
pgContainer: null as StartedTestContainer | null,
utilOptions: null as Options | null,
@@ -146,3 +147,116 @@ connection-password=example`;
await this.container?.stop()
}
}
function generateTrinoSslFiles(tempDir: string) {
const certDir = path.join(tempDir, 'certs')
fs.mkdirSync(certDir, { recursive: true })
const keyFile = path.join(certDir, 'trino.key')
const certFile = path.join(certDir, 'trino.crt')
const p12File = path.join(certDir, 'trino.p12')
const keystorePassword = 'trinopass'
// Generate self-signed cert and private key
execSync(
`openssl req -x509 -newkey rsa:2048 -keyout ${keyFile} -out ${certFile} ` +
`-days 1 -nodes -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"`,
{ stdio: 'pipe' }
)
// Convert to PKCS12 keystore for Trino's Java TLS
execSync(
`openssl pkcs12 -export -in ${certFile} -inkey ${keyFile} ` +
`-out ${p12File} -passout pass:${keystorePassword}`,
{ stdio: 'pipe' }
)
return { keyFile, certFile, p12File, keystorePassword, certDir }
}
export const TrinoHttpsDriver = {
container: null as StartedTestContainer | null,
config: null as IDbConnectionServerConfig | null,
certFile: null as string | null,
async start(dockerTag: string, readonly: boolean, network) {
const startupTimeout = dbtimeout * 2
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trino-ssl-'))
// Generate SSL certs and keystore
const ssl = generateTrinoSslFiles(tempDir)
this.certFile = ssl.certFile
// Create catalog config for postgresql connector
const catalogDir = path.join(tempDir, 'catalog')
fs.mkdirSync(catalogDir)
fs.writeFileSync(
path.join(catalogDir, 'postgresql.properties'),
`connector.name=postgresql
connection-url=jdbc:postgresql://postgres:5432/banana
connection-user=postgres
connection-password=example`
)
// Generate a shared secret for internal communication
const sharedSecret = require('crypto').randomBytes(64).toString('base64')
// Shell script to merge SSL settings into the existing config.properties,
// then start Trino. This preserves default properties like catalog.management.
const entrypointScript = path.join(tempDir, 'entrypoint.sh')
fs.writeFileSync(entrypointScript, [
'#!/bin/bash',
'set -e',
"sed -i '/^http-server.http.port=/d' /etc/trino/config.properties",
"sed -i '/^discovery.uri=/d' /etc/trino/config.properties",
`cat >> /etc/trino/config.properties << 'SSLEOF'`,
'http-server.http.enabled=false',
'http-server.https.enabled=true',
'http-server.https.port=8443',
`http-server.https.keystore.path=/etc/trino/certs/trino.p12`,
`http-server.https.keystore.key=${ssl.keystorePassword}`,
'discovery.uri=https://localhost:8443',
'internal-communication.https.required=true',
`internal-communication.shared-secret=${sharedSecret}`,
'SSLEOF',
'exec /usr/lib/trino/bin/run-trino',
].join('\n'))
fs.chmodSync(entrypointScript, '755')
this.container = await new GenericContainer(`trinodb/trino:${dockerTag}`)
.withNetwork(network)
.withExposedPorts(8443)
.withBindMounts([
{ source: catalogDir, target: '/etc/trino/catalog', mode: 'ro' },
{ source: ssl.certDir, target: '/etc/trino/certs', mode: 'ro' },
{ source: entrypointScript, target: '/etc/trino/entrypoint.sh', mode: 'ro' },
])
.withCommand(['/etc/trino/entrypoint.sh'])
.withWaitStrategy(Wait.forLogMessage("SERVER STARTED"))
.withStartupTimeout(startupTimeout)
.start()
this.config = {
client: 'trino',
host: this.container.getHost(),
port: this.container.getMappedPort(8443),
user: 'test',
password: null,
osUser: 'foo',
ssh: null,
sslCaFile: ssl.certFile,
sslCertFile: null,
sslKeyFile: null,
sslRejectUnauthorized: false,
ssl: true,
domain: null,
socketPath: null,
socketPathEnabled: false,
readOnlyMode: readonly,
}
},
async stop() {
await this.container?.stop()
},
}

View File

@@ -0,0 +1,141 @@
jest.mock('trino-client', () => {
const mockQuery = jest.fn().mockResolvedValue({
[Symbol.asyncIterator]: async function* () {
yield {
data: [['1.0.0']],
columns: [{ name: '_col0', type: 'varchar' }]
}
}
})
return {
Trino: {
create: jest.fn().mockReturnValue({ query: mockQuery })
},
BasicAuth: jest.fn().mockImplementation((user, pass) => ({ type: 'basic', username: user, password: pass })),
}
})
import fs from 'fs'
import os from 'os'
import path from 'path'
import { Trino as TrinoNodeClient } from 'trino-client'
import { TrinoClient } from '@commercial/backend/lib/db/clients/trino'
import { IDbConnectionServer } from '@/lib/db/backendTypes'
import { IDbConnectionDatabase } from '@/lib/db/types'
function makeServer(overrides: Partial<IDbConnectionServer['config']> = {}): IDbConnectionServer {
return {
db: {},
config: {
client: 'trino',
host: 'localhost',
port: 8080,
user: 'testuser',
password: null,
readOnlyMode: false,
osUser: 'testuser',
ssh: null,
sslCaFile: null,
sslCertFile: null,
sslKeyFile: null,
sslRejectUnauthorized: false,
ssl: false,
domain: null,
socketPath: null,
socketPathEnabled: false,
...overrides,
},
} as IDbConnectionServer
}
function makeDatabase(): IDbConnectionDatabase {
return {
database: 'postgresql',
connected: false,
connecting: false,
namespace: null,
}
}
describe('TrinoClient SSL configuration (bug #3695)', () => {
let tmpDir: string
let caFile: string
let certFile: string
let keyFile: string
beforeAll(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trino-test-'))
caFile = path.join(tmpDir, 'ca.pem')
certFile = path.join(tmpDir, 'cert.pem')
keyFile = path.join(tmpDir, 'key.pem')
fs.writeFileSync(caFile, 'fake-ca')
fs.writeFileSync(certFile, 'fake-cert')
fs.writeFileSync(keyFile, 'fake-key')
})
afterAll(() => {
fs.rmSync(tmpDir, { recursive: true })
})
beforeEach(() => {
jest.clearAllMocks()
})
it('should pass ssl options to Trino.create() when ssl is true', async () => {
const server = makeServer({ ssl: true })
const client = new TrinoClient(server, makeDatabase())
await client.connect()
const createCall = (TrinoNodeClient.create as jest.Mock).mock.calls[0][0]
expect(createCall.ssl).toBeDefined()
})
it('should use https protocol when ssl is true', async () => {
const server = makeServer({ ssl: true })
const client = new TrinoClient(server, makeDatabase())
await client.connect()
const createCall = (TrinoNodeClient.create as jest.Mock).mock.calls[0][0]
expect(createCall.server).toContain('https')
})
it('should pass ssl cert/key/ca files when configured', async () => {
const server = makeServer({
ssl: true,
sslCaFile: caFile,
sslCertFile: certFile,
sslKeyFile: keyFile,
})
const client = new TrinoClient(server, makeDatabase())
await client.connect()
const createCall = (TrinoNodeClient.create as jest.Mock).mock.calls[0][0]
expect(createCall.ssl).toBeDefined()
expect(createCall.ssl.ca).toBeDefined()
expect(createCall.ssl.cert).toBeDefined()
expect(createCall.ssl.key).toBeDefined()
})
it('should set rejectUnauthorized based on config', async () => {
const server = makeServer({
ssl: true,
sslRejectUnauthorized: false,
})
const client = new TrinoClient(server, makeDatabase())
await client.connect()
const createCall = (TrinoNodeClient.create as jest.Mock).mock.calls[0][0]
expect(createCall.ssl).toBeDefined()
expect(createCall.ssl.rejectUnauthorized).toBe(false)
})
it('should not pass ssl options when ssl is false', async () => {
const server = makeServer({ ssl: false })
const client = new TrinoClient(server, makeDatabase())
await client.connect()
const createCall = (TrinoNodeClient.create as jest.Mock).mock.calls[0][0]
expect(createCall.ssl).toBeUndefined()
})
})

View File

@@ -21,11 +21,22 @@ Conectarse a una base de datos Trino desde Beekeeper Studio es sencillo. Selecci
Para conectarte a una base de datos Trino, necesitaras la siguiente informacion:
- **Host**: La direccion IP o nombre de host de tu servidor Trino.
- **Puerto**: El puerto predeterminado es 8080, pero esto se puede personalizar si tu servidor usa un puerto diferente.
- **Puerto**: El puerto predeterminado es 8080 para HTTP, o 8443 para HTTPS. Esto se puede personalizar si tu servidor usa un puerto diferente.
- **Nombre de usuario**: Tu nombre de usuario de Trino, siendo default el valor predeterminado tipico.
- **Contrasena**: Tu contrasena de Trino, si aplica.
- **Catalogo predeterminado (opcional)**: El catalogo al que quieres conectarte inicialmente al inicio
### Conexiones SSL / HTTPS
Si tu coordinador Trino esta configurado con TLS/HTTPS, habilita **SSL** en el formulario de conexion. Beekeeper Studio soporta tres modos de SSL:
1. **Confiar en el certificado del servidor** — Habilita SSL sin proporcionar archivos de certificado. Beekeeper Studio se conectara por HTTPS pero no verificara el certificado del servidor. Esta es la opcion mas sencilla y funciona con certificados autofirmados.
2. **Proporcionar un certificado CA** — Si tu servidor Trino usa un certificado firmado por una CA privada, proporciona el archivo de certificado CA. Deja "Rechazar no autorizados" desmarcado para permitir la conexion.
3. **Verificacion completa de certificados** — Proporciona el certificado CA y opcionalmente un certificado de cliente y archivo de clave, luego marca "Rechazar no autorizados" para aplicar la verificacion TLS completa.
!!! tip
Si importas una URL de conexion que comienza con `https://`, SSL se habilitara automaticamente.
### Probar tu conexion de Trino
Antes de guardar los detalles de tu conexion, Beekeeper Studio te permite probar la conexion:
@@ -40,6 +51,7 @@ Una vez que los detalles de tu conexion han sido verificados, puedes elegir guar
## Funciones soportadas
- Conexiones SSL / HTTPS (con archivos opcionales de CA, certificado de cliente y clave)
- Vista de datos de tabla
- Ordenamiento y filtrado de datos de tabla
- Vista de estructura de tabla

View File

@@ -21,11 +21,22 @@ Connecting to a Trino database from Beekeeper Studio is straightforward. SElect
To connect to a Trino database, you'll need the following information:
- **Host**: The IP address or hostname of your Trino server.
- **Port**: The default port is 8080, but this can be customized if your server uses a different port.
- **Port**: The default port is 8080 for HTTP, or 8443 for HTTPS. This can be customized if your server uses a different port.
- **Username**: Your Trino username, with default being the typical default.
- **Password**: Your Trino password, if applicable.
- **Default Catalog (optional)**: The catalog you want initially connected to at startup
### SSL / HTTPS Connections
If your Trino coordinator is configured with TLS/HTTPS, enable **SSL** in the connection form. Beekeeper Studio supports three SSL modes:
1. **Trust the server certificate** — Enable SSL without providing any certificate files. Beekeeper Studio will connect over HTTPS but will not verify the server's certificate. This is the simplest option and works with self-signed certificates.
2. **Provide a CA certificate** — If your Trino server uses a certificate signed by a private CA, provide the CA certificate file. Leave "Reject Unauthorized" unchecked to allow the connection.
3. **Full certificate verification** — Provide the CA certificate and optionally a client certificate and key file, then check "Reject Unauthorized" to enforce full TLS verification.
!!! tip
If you import a connection URL that starts with `https://`, SSL will be enabled automatically.
### Testing Your Trino Connection
Before saving your connection details, Beekeeper Studio allows you to test the connection:
@@ -40,6 +51,7 @@ Once your connection details have been verified, you can choose to save them by
## Supported Features
- SSL / HTTPS connections (with optional CA, client cert, and key files)
- Table data view
- Table data sorting, filtering
- Table structure view