Merge branch 'master' into arm-hosted

# Conflicts:
#	.github/workflows/studio-build-non-production.yml
#	.github/workflows/studio-publish.yml
This commit is contained in:
Matthew Rathbone
2026-03-11 14:02:25 -05:00
28 changed files with 794 additions and 362 deletions

View File

@@ -87,20 +87,24 @@ jobs:
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: yarn
- name: Cache node_modules
id: cache-node-modules
id: cache-nm
uses: actions/cache@v4
with:
path: |
node_modules
apps/studio/node_modules
apps/ui-kit/node_modules
key: node-modules-${{ matrix.os }}-${{ hashFiles('yarn.lock') }}
apps/sqltools/node_modules
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
- name: Clean cache
if: steps.cache-nm.outputs.cache-hit != 'true'
run: yarn cache clean
- name: Check dependencies
if: steps.cache-nm.outputs.cache-hit != 'true'
run: "yarn install --frozen-lockfile --network-timeout 100000"
env:
npm_config_node_gyp: ${{ github.workspace }}${{ runner.os == 'Windows' && '\node_modules\node-gyp\bin\node-gyp.js' || '/node_modules/node-gyp/bin/node-gyp.js' }}

View File

@@ -214,28 +214,32 @@ jobs:
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: yarn
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v3
if: matrix.os.type == 'linux'
- name: Cache node_modules
id: cache-node-modules
id: cache-nm
uses: actions/cache@v4
with:
path: |
node_modules
apps/studio/node_modules
apps/ui-kit/node_modules
key: node-modules-${{ matrix.os.name }}-${{ hashFiles('yarn.lock') }}
apps/sqltools/node_modules
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v3
if: matrix.os.type == 'linux'
- name: Clean cache
if: steps.cache-nm.outputs.cache-hit != 'true'
run: yarn cache clean --all
# FIXME (matthew) Windows needs retries. It sometimes fails to build
# the native oracledb package.
# But only sometimes. I cannot figure out why.
# Someone should at some point.
- name: yarn install (with retry)
if: steps.cache-node-modules.outputs.cache-hit != 'true'
if: steps.cache-nm.outputs.cache-hit != 'true'
uses: nick-fields/retry@v2
with:
timeout_minutes: 20
@@ -351,8 +355,8 @@ jobs:
- name: Cleanup artifacts Win
if: startsWith(matrix.os.name, 'windows')
run: npx rimraf "apps/studio/dist_electron/!(*.exe|*.yml)"
continue-on-error: true
shell: powershell
run: Get-ChildItem apps/studio/dist_electron -File | Where-Object { $_.Extension -notin '.exe','.yml' } | Remove-Item -Force
finalize_mac_yml:
@@ -435,7 +439,7 @@ jobs:
- name: Move release snap from edge to stable
continue-on-error: true
run: |
snapcraft promote beekeeper-studio --from-channel "edge" --to-channel "stable" --yes
snapcraft promote beekeeper-studio --from-channel "latest/edge" --to-channel "latest/stable" --yes
publish_repositories:
needs: [release, create_draft_release, identify_channel]

View File

@@ -18,10 +18,10 @@ on:
- apps/sqltools/**
jobs:
setup:
prep:
runs-on: ubuntu-24.04
outputs:
test-chunks: ${{ steps['set-test-chunks'].outputs['test-chunks'] }}
test-chunks: ${{ steps.set-test-chunks.outputs.test-chunks }}
steps:
- name: 'Setup jq'
uses: dcarbone/install-jq-action@v2
@@ -29,27 +29,62 @@ jobs:
version: "1.7"
force: true
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- id: set-test-chunks
name: Set Chunks
run: echo "::set-output name=test-chunks::$(./bin/get-db-files-as-json.sh)"
unit:
name: Unit tests
runs-on: ubuntu-24.04
steps:
- name: Check out Git repository
uses: actions/checkout@v2
run: echo "test-chunks=$(./bin/get-db-files-as-json.sh)" >> $GITHUB_OUTPUT
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: yarn
- name: Cache node_modules
id: cache-nm
uses: actions/cache@v4
with:
path: |
node_modules
apps/studio/node_modules
apps/ui-kit/node_modules
apps/sqltools/node_modules
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
run: yarn install --frozen-lockfile
if: steps.cache-nm.outputs.cache-hit != 'true'
uses: nick-fields/retry@v2
with:
timeout_minutes: 20
max_attempts: 3
command: yarn install --frozen-lockfile --network-timeout 100000
env:
npm_config_node_gyp: ${{ github.workspace }}${{ runner.os == 'Windows' && '\node_modules\node-gyp\bin\node-gyp.js' || '/node_modules/node-gyp/bin/node-gyp.js' }}
HUSKY: "0"
npm_config_node_gyp: ${{ github.workspace }}/node_modules/node-gyp/bin/node-gyp.js
unit:
name: Unit tests
runs-on: ubuntu-24.04
needs: [prep]
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
- name: Restore node_modules cache
uses: actions/cache/restore@v4
with:
path: |
node_modules
apps/studio/node_modules
apps/ui-kit/node_modules
apps/sqltools/node_modules
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
fail-on-cache-miss: true
- name: Lint
run: yarn workspace beekeeper-studio run lint
@@ -80,20 +115,30 @@ jobs:
name: 🥞 ${{ matrix.chunk[0] }}
runs-on: ubuntu-24.04
needs:
- setup
- prep
strategy:
fail-fast: false
matrix:
chunk: ${{ fromJson(needs.setup.outputs['test-chunks']) }}
chunk: ${{ fromJson(needs.prep.outputs['test-chunks']) }}
steps:
- name: Check out Git repository
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: yarn
- name: Restore node_modules cache
uses: actions/cache/restore@v4
with:
path: |
node_modules
apps/studio/node_modules
apps/ui-kit/node_modules
apps/sqltools/node_modules
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
fail-on-cache-miss: true
- name: Install libaio (for oracle)
run: sudo apt install libaio-dev
@@ -101,15 +146,6 @@ jobs:
- name: Symlink libaio v1 (for oracle)
run: sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1
- name: yarn install (with retry)
uses: nick-fields/retry@v2
with:
timeout_minutes: 20
max_attempts: 3
command: "yarn install --frozen-lockfile --network-timeout 100000"
env:
npm_config_node_gyp: ${{ github.workspace }}${{ runner.os == 'Windows' && '\node_modules\node-gyp\bin\node-gyp.js' || '/node_modules/node-gyp/bin/node-gyp.js' }}
- name: Test
uses: nick-fields/retry@v2
with:
@@ -147,21 +183,27 @@ jobs:
e2e:
name: E2E tests
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs: [prep]
steps:
- name: Check out Git repository
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
cache: yarn
- name: Install dependencies
run: yarn install --frozen-lockfile
env:
npm_config_node_gyp: ${{ github.workspace }}${{ runner.os == 'Windows' && '\node_modules\node-gyp\bin\node-gyp.js' || '/node_modules/node-gyp/bin/node-gyp.js' }}
- name: Restore node_modules cache
uses: actions/cache/restore@v4
with:
path: |
node_modules
apps/studio/node_modules
apps/ui-kit/node_modules
apps/sqltools/node_modules
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('yarn.lock') }}
fail-on-cache-miss: true
- name: Start postgres container
run: docker compose up psql15 -d

View File

@@ -36,7 +36,9 @@ test.describe("Using the context menu", () => {
});
afterEach(async () => {
await electronApp.close();
if (electronApp) {
await electronApp.close();
}
});
test("paste a query using context menu", async () => {

View File

@@ -39,7 +39,9 @@ test.describe("Copy Results Verifications", () => {
});
afterEach(async () => {
await electronApp.close();
if (electronApp) {
await electronApp.close();
}
});
test("copy as TSV / Excel", async () => {

View File

@@ -40,7 +40,9 @@ test.describe("Export Results Verifications", () => {
});
afterEach(async () => {
await electronApp.close();
if (electronApp) {
await electronApp.close();
}
});
test("downloads as CSV", async () => {

View File

@@ -39,7 +39,9 @@ test.describe("JSON Sidebar Verifications", () => {
});
afterEach(async () => {
await electronApp.close();
if (electronApp) {
await electronApp.close();
}
});
test.skip("accessing the JSON sidebar", async () => {

View File

@@ -22,7 +22,9 @@ test.describe('New Connection Tests', () => {
});
test.afterEach(async () => {
await electronApp.close();
if (electronApp) {
await electronApp.close();
}
});
test('Test a Postgres connection', async () => {

View File

@@ -23,7 +23,9 @@ test.describe("Postgres query execution", () => {
});
afterEach(async () => {
await electronApp.close();
if (electronApp) {
await electronApp.close();
}
});
test("perform a Postgres query", async () => {

View File

@@ -25,7 +25,9 @@ test.describe("Result Pane Verifications", () => {
});
afterEach(async () => {
await electronApp.close();
if (electronApp) {
await electronApp.close();
}
});
test("clicks on results columns", async () => {

View File

@@ -30,6 +30,7 @@ test.describe("Table creation", () => {
});
afterEach(async () => {
if (!electronApp) return;
const dropTableQuery = `DROP TABLE ${newTableName};`
const newQueryIndex = '1';

View File

@@ -32,6 +32,7 @@ test.describe("Table creation", () => {
});
afterEach(async () => {
if (!electronApp) return;
const dropTableQuery = `DROP TABLE ${newTableName};`
const newQueryIndex = '1';

View File

@@ -103,6 +103,11 @@ module.exports = {
name: "SQL Server URL scheme",
schemes: ["sqlserver", "microsoftsqlserver", "mssql"],
role: "Editor"
},
{
"name": "Redis URL scheme",
"schemes": ["redis", "rediss"],
"role": "Editor"
}
],
mac: {

View File

@@ -139,6 +139,11 @@ module.exports = {
name: "SQL Server URL scheme",
schemes: ["sqlserver", "microsoftsqlserver", "mssql"],
role: "Editor"
},
{
"name": "Redis URL scheme",
"schemes": ["redis", "rediss"],
"role": "Editor"
}
],
mac: {

View File

@@ -76,7 +76,7 @@
"core-js": "^3",
"dateformat": "^3.0.3",
"diff-match-patch": "^1.0.5",
"dompurify": "^3.2.4",
"dompurify": "^3.3.2",
"driver.js": "^1.3.6",
"electron-devtools-installer": "^3.2.1",
"electron-log": "^5.1.5",

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, escapeString } 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)
}
@@ -271,7 +296,7 @@ export class TrinoClient extends BasicDatabaseClient<TrinoResult> {
async listSchemas(filter: SchemaFilterOptions): Promise<string[]> {
log.info('filters in listSchemas', filter)
const sql = `show schemas from ${this.db}`
const sql = `show schemas from ${this.wrapIdentifier(this.db)}`
const result = await this.driverExecuteSingle(sql)
return result?.rows ? result.rows.map((row) => row.Schema) : []
@@ -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.wrapIdentifier(this.db)}.information_schema.tables ${whereClause}`
const result = await this.driverExecuteSingle(sql)
return result.rows.map((row) => ({
@@ -294,9 +320,9 @@ export class TrinoClient extends BasicDatabaseClient<TrinoResult> {
const sql = `
SELECT
*
FROM ${this.db}.information_schema.columns
WHERE table_schema = '${schema}'
AND table_name = '${table}'
FROM ${this.wrapIdentifier(this.db)}.information_schema.columns
WHERE table_schema = '${escapeString(schema)}'
AND table_name = '${escapeString(table)}'
ORDER BY ordinal_position
`
const result = await this.driverExecuteSingle(sql)
@@ -688,7 +714,7 @@ export class TrinoClient extends BasicDatabaseClient<TrinoResult> {
SELECT
${wrappedSelects},
ROW_NUMBER() OVER (${rowNumberOrderClause}) AS rownum
FROM ${this.db}.${tableRef}
FROM ${this.wrapIdentifier(this.db)}.${tableRef}
${filter}
)
SELECT *

View File

@@ -398,6 +398,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

@@ -1,13 +1,5 @@
<template>
<div class="trino-form">
<div class="alert alert-warning">
<i class="material-icons">warning</i>
<span>
Trino support is in alpha.
<a href="https://docs.beekeeperstudio.io/user_guide/connecting/trino">Supported features</a>,
<a href="https://github.com/beekeeper-studio/beekeeper-studio/issues/new/choose">report an issue</a>.
</span>
</div>
<div class="with-connection-type">
<common-server-inputs :config="config" />
</div>

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

@@ -48,17 +48,17 @@ export function buildSchemaFilter(filter, schemaField = 'schema_name') {
const { schema, only, ignore } = filter
if (schema) {
return `${schemaField} = '${schema}'`;
return `${schemaField} = '${escapeString(schema)}'`;
}
const where = [];
if (only && only.length) {
where.push(`${schemaField} IN (${only.map((name) => `'${name}'`).join(',')})`);
where.push(`${schemaField} IN (${only.map((name) => `'${escapeString(name)}'`).join(',')})`);
}
if (ignore && ignore.length) {
where.push(`${schemaField} NOT IN (${ignore.map((name) => `'${name}'`).join(',')})`);
where.push(`${schemaField} NOT IN (${ignore.map((name) => `'${escapeString(name)}'`).join(',')})`);
}
return where.join(' AND ');

View File

@@ -1,11 +0,0 @@
export default {
components: {}
install: function (Vue, opts) {
this.components = opts
}
}

View File

@@ -1,289 +1,262 @@
import { Network} from "testcontainers"
import { DBTestUtil, dbtimeout } from "../../../../lib/db"
import { PostgresTestDriver, TrinoTestDriver } from './trino/container'
import { TrinoBackingPostgresDriver, TrinoHttpDriver, TrinoNginxProxy } from './trino/container'
import { TableOrView } from "@/lib/db/models"
// import { runCommonTests, runReadOnlyTests } from "./all"
const TEST_VERSIONS = [
{ version: 'latest', socket: false, readonly: false }
] as const
describe('Trino integration tests', () => {
jest.setTimeout(dbtimeout)
type TestVersion = typeof TEST_VERSIONS[number]['version']
let dbfeeder: DBTestUtil
let httpUtil: DBTestUtil
let httpsUtil: DBTestUtil
function testWith(dockerTag: TestVersion, socket = false, readonly = false) {
describe(`Trino [${dockerTag} - socket? ${socket} - database read-only mode? ${readonly}]`, () => {
jest.setTimeout(dbtimeout)
beforeAll(async () => {
const network = await new Network().start()
let dbfeeder: DBTestUtil
let util: DBTestUtil
// Start backing Postgres and seed test data
await TrinoBackingPostgresDriver.start('latest', false, false, network)
dbfeeder = new DBTestUtil(TrinoBackingPostgresDriver.config, "banana", TrinoBackingPostgresDriver.utilOptions)
await dbfeeder.setupdb()
beforeAll(async () => {
const network = await new Network().start()
// Start Trino (HTTP only, on the docker network)
await TrinoHttpDriver.start('latest', false, network)
await PostgresTestDriver.start(dockerTag, socket, readonly, network)
// Direct HTTP connection to Trino
httpUtil = new DBTestUtil(TrinoHttpDriver.config, 'postgresql', { dialect: 'trino' })
await httpUtil.connection.connect()
dbfeeder = new DBTestUtil(PostgresTestDriver.config, "banana", PostgresTestDriver.utilOptions)
await dbfeeder.setupdb()
// Start nginx proxy for SSL termination
await TrinoNginxProxy.start(network)
// now set up the trino container
await TrinoTestDriver.start(dockerTag, readonly, network)
// HTTPS connection (through nginx TLS termination)
httpsUtil = new DBTestUtil(TrinoNginxProxy.httpsConfig, 'postgresql', { dialect: 'trino' })
await httpsUtil.connection.connect()
})
util = new DBTestUtil(TrinoTestDriver.config, 'postgresql', { dialect: 'trino' })
await util.connection.connect()
afterAll(async () => {
if (httpsUtil?.connection) await httpsUtil.connection.disconnect()
if (httpUtil?.connection) await httpUtil.connection.disconnect()
await TrinoNginxProxy.stop()
await TrinoHttpDriver.stop()
await dbfeeder.disconnect()
await TrinoBackingPostgresDriver.stop()
})
// Trino uses catalogs instead of databases, access PostgreSQL through 'postgresql' catalog
// util = new DBTestUtil(TrinoTestDriver.config, "postgresql", TrinoTestDriver.utilOptions)
describe("Read Operations (HTTP)", () => {
it("List tables should work", async () => {
const tables: TableOrView[] = await httpUtil.connection.listTables({ schema: 'public' })
const tableNames: string[] = tables.map((t) => t.name)
// await util.setupdb()
expect(tables.length).toBeGreaterThanOrEqual(3)
expect(tableNames).toContain('people')
expect(tableNames).toContain('addresses')
expect(tableNames).toContain('jobs')
})
afterAll(async () => {
// await util.disconnect()
await TrinoTestDriver.stop()
await dbfeeder.disconnect()
if (util.connection) {
await util.connection.disconnect()
it("List tables should return tables when filter is null (bug #3947)", async () => {
const tables: TableOrView[] = await httpUtil.connection.listTables(null)
expect(tables.length).toBeGreaterThanOrEqual(3)
})
it("List tables should only return tables matching the schema filter", async () => {
const tables: TableOrView[] = await httpUtil.connection.listTables({ schema: 'public' })
tables.forEach(t => expect(t.schema).toBe('public'))
})
it("List table columns should work", async () => {
const columns = await httpUtil.connection.listTableColumns("addresses", "public")
const columnNames = columns.map((c) => c.columnName)
expect(columns.length).toBeGreaterThan(0)
expect(columnNames).toContain('street')
expect(columnNames).toContain('city')
expect(columnNames).toContain('country')
const streetColumn = columns.find(c => c.columnName === 'street')
expect(streetColumn).toBeDefined()
expect(streetColumn.schemaName).toBe('public')
expect(streetColumn.tableName).toBe('addresses')
})
it("Get database version should work", async () => {
const version = await httpUtil.connection.versionString()
expect(version).toBeDefined()
expect(typeof version).toBe('string')
expect(version.length).toBeGreaterThan(0)
})
it("List schemas should work", async () => {
const schemas = await httpUtil.connection.listSchemas()
expect(schemas).toBeDefined()
expect(Array.isArray(schemas)).toBe(true)
if (schemas.length > 0) {
expect(schemas).toContain('public')
}
})
describe("Read Operations", () => {
it("List tables should work", async () => {
const tables: TableOrView[] = await util.connection.listTables({ schema: 'public' })
const tableNames: string[] = tables.map((t) => t.name)
it("Should be able to retrieve data from a table", async () => {
const data = await httpUtil.connection.selectTop('people', 0, 100, [], [], 'public', [])
expect(data.result).toBeDefined()
expect(Array.isArray(data.result)).toBe(true)
expect(data.fields).toBeDefined()
expect(data.fields.length).toBeGreaterThan(0)
})
expect(tables.length).toBeGreaterThanOrEqual(3)
expect(tableNames).toContain('people')
expect(tableNames).toContain('addresses')
expect(tableNames).toContain('jobs')
})
it("Should be able to filter columns", async () => {
let r = await httpUtil.connection.selectTop("jobs", 0, 10, [], [], 'public', [])
expect(r.result).toBeDefined()
expect(r.fields).toBeDefined()
it("List tables should return empty array when filter is undefined", async () => {
const tables: TableOrView[] = await util.connection.listTables()
expect(tables).toEqual([])
})
if (r.result.length > 0) {
const firstRow = r.result[0]
expect(firstRow).toBeDefined()
expect(typeof firstRow).toBe('object')
}
it("List table columns should work", async () => {
const columns = await util.connection.listTableColumns("addresses", "public")
const columnNames = columns.map((c) => c.columnName)
r = await httpUtil.connection.selectTop("jobs", 0, 10, [], [], 'public', ['title'])
expect(r.result).toBeDefined()
expect(r.fields).toBeDefined()
})
expect(columns.length).toBeGreaterThan(0)
expect(columnNames).toContain('street')
expect(columnNames).toContain('city')
expect(columnNames).toContain('country')
it("Should handle sorting correctly", async () => {
const result = await httpUtil.connection.selectTop('people', 0, 10, [], [], 'public', [])
expect(result.result).toBeDefined()
expect(result.fields).toBeDefined()
expect(result.fields.length).toBeGreaterThan(0)
expect(Array.isArray(result.result)).toBe(true)
})
// Verify column properties
const streetColumn = columns.find(c => c.columnName === 'street')
expect(streetColumn).toBeDefined()
expect(streetColumn.schemaName).toBe('public')
expect(streetColumn.tableName).toBe('addresses')
})
it("Should handle pagination correctly", async () => {
let result = await httpUtil.connection.selectTop('people', 0, 2, [], [], 'public', [])
expect(result.result).toBeDefined()
expect(Array.isArray(result.result)).toBe(true)
expect(result.result.length).toBeLessThanOrEqual(2)
it("Get database version should work", async () => {
const version = await util.connection.versionString()
expect(version).toBeDefined()
expect(typeof version).toBe('string')
expect(version.length).toBeGreaterThan(0)
})
result = await httpUtil.connection.selectTop('people', 2, 2, [], [], 'public', [])
expect(result.result).toBeDefined()
expect(Array.isArray(result.result)).toBe(true)
})
it("List schemas should work", async () => {
const schemas = await util.connection.listSchemas()
expect(schemas).toBeDefined()
expect(Array.isArray(schemas)).toBe(true)
// We know from our debugging that schemas work, so if we get an empty array
// it's likely just a timing/implementation issue, but the method works
if (schemas.length > 0) {
expect(schemas).toContain('public')
}
})
it("Should execute custom queries", async () => {
const queryResult = await httpUtil.connection.query('SELECT COUNT(*) as table_count FROM postgresql.information_schema.tables WHERE table_schema = \'public\'')
const results = await queryResult.execute()
it("Should be able to retrieve data from a table", async () => {
const data = await util.connection.selectTop('people', 0, 100, [], [], 'public', [])
expect(data.result).toBeDefined()
expect(Array.isArray(data.result)).toBe(true)
expect(data.fields).toBeDefined()
expect(data.fields.length).toBeGreaterThan(0)
})
expect(results).toBeDefined()
expect(results.length).toBeGreaterThan(0)
expect(results[0]).toBeDefined()
expect(results[0].rows).toBeDefined()
})
it("Should be able to filter columns", async () => {
// Get all columns
let r = await util.connection.selectTop("jobs", 0, 10, [], [], 'public', [])
expect(r.result).toBeDefined()
expect(r.fields).toBeDefined()
it("Should support Trino-specific features", async () => {
const catalogsQuery = await httpUtil.connection.query('SHOW CATALOGS')
const catalogsResult = await catalogsQuery.execute()
if (r.result.length > 0) {
const firstRow = r.result[0]
expect(firstRow).toBeDefined()
expect(typeof firstRow).toBe('object')
}
expect(catalogsResult).toBeDefined()
expect(catalogsResult.length).toBeGreaterThan(0)
expect(catalogsResult[0].rows).toBeDefined()
// Get only specific column if we specify it
r = await util.connection.selectTop("jobs", 0, 10, [], [], 'public', ['title'])
expect(r.result).toBeDefined()
expect(r.fields).toBeDefined()
})
const catalogNames = catalogsResult[0].rows.map(row => row.Catalog)
expect(Array.isArray(catalogNames)).toBe(true)
if (catalogNames.length > 0) {
expect(catalogNames.length).toBeGreaterThan(0)
}
})
it("Should handle sorting correctly", async () => {
// Test sorting functionality
const result = await util.connection.selectTop('people', 0, 10, [], [], 'public', [])
expect(result.result).toBeDefined()
expect(result.fields).toBeDefined()
expect(result.fields.length).toBeGreaterThan(0)
it("Should handle complex queries with joins", async () => {
const joinQuery = `
SELECT COUNT(*) as count_result
FROM postgresql.public.people p
WHERE p.id IS NOT NULL
LIMIT 5
`
// The sorting functionality should work even if no data is returned
expect(Array.isArray(result.result)).toBe(true)
})
const queryResult = await httpUtil.connection.query(joinQuery)
const results = await queryResult.execute()
it("Should handle pagination correctly", async () => {
// Page 1 (first 2 items)
let result = await util.connection.selectTop('people', 0, 2, [], [], 'public', [])
expect(result.result).toBeDefined()
expect(Array.isArray(result.result)).toBe(true)
expect(result.result.length).toBeLessThanOrEqual(2)
// Page 2 (next 2 items)
result = await util.connection.selectTop('people', 2, 2, [], [], 'public', [])
expect(result.result).toBeDefined()
expect(Array.isArray(result.result)).toBe(true)
})
it("Should execute custom queries", async () => {
const queryResult = await util.connection.query('SELECT COUNT(*) as table_count FROM postgresql.information_schema.tables WHERE table_schema = \'public\'')
const results = await queryResult.execute()
expect(results).toBeDefined()
expect(results.length).toBeGreaterThan(0)
expect(results[0]).toBeDefined()
expect(results[0].rows).toBeDefined()
})
it("Should support Trino-specific features", async () => {
// Test SHOW CATALOGS - a Trino-specific command
const catalogsQuery = await util.connection.query('SHOW CATALOGS')
const catalogsResult = await catalogsQuery.execute()
expect(catalogsResult).toBeDefined()
expect(catalogsResult.length).toBeGreaterThan(0)
expect(catalogsResult[0].rows).toBeDefined()
// The catalogs should include at least system catalogs
const catalogNames = catalogsResult[0].rows.map(row => row.Catalog)
expect(Array.isArray(catalogNames)).toBe(true)
// We know from debug that postgresql catalog should be there, but make test more flexible
if (catalogNames.length > 0) {
// Should have some catalogs (system, postgresql, etc.)
expect(catalogNames.length).toBeGreaterThan(0)
}
})
it("Should handle complex queries with joins", async () => {
// Use a simpler query that's more likely to work
const joinQuery = `
SELECT COUNT(*) as count_result
FROM postgresql.public.people p
WHERE p.id IS NOT NULL
LIMIT 5
`
const queryResult = await util.connection.query(joinQuery)
const results = await queryResult.execute()
expect(results).toBeDefined()
expect(results.length).toBeGreaterThan(0)
expect(results[0].rows).toBeDefined()
})
})
describe("Trino-specific constraints and features", () => {
it("Should indicate Trino doesn't support transactions", async () => {
expect((util.connection as any).supportsTransaction).toBe(false)
})
it("Should return null for primary keys (not supported)", async () => {
const primaryKeys = await util.connection.getPrimaryKeys()
expect(primaryKeys).toEqual([])
const singlePrimaryKey = await util.connection.getPrimaryKey('people', 'public')
expect(singlePrimaryKey).toBeNull()
})
it("Should return null for data modification operations", async () => {
const alterResult = await util.connection.alterTable({} as any)
expect(alterResult).toBeNull()
const createDbResult = await util.connection.createDatabase()
expect(createDbResult).toBeNull()
const truncateResult = await util.connection.truncateElementSql('test', 'public')
expect(truncateResult).toBeNull()
const duplicateResult = await util.connection.duplicateTable('test', 'public', 'test_copy')
expect(duplicateResult).toBeNull()
const duplicateSqlResult = await util.connection.duplicateTableSql('test', 'public', 'test_copy')
expect(duplicateSqlResult).toBeNull()
const setNameResult = await util.connection.setElementNameSql('test', 'test_new', 'public')
expect(setNameResult).toBeNull()
const builderResult = await util.connection.getBuilder('test_table', 'public')
expect(builderResult).toBeNull()
})
it("Should support selectTopSql for query generation", async () => {
const sql = await util.connection.selectTopSql('people', 0, 10,
[], [], 'public', [])
expect(sql).toBeDefined()
expect(typeof sql).toBe('string')
expect(sql.length).toBeGreaterThan(0)
expect(sql).toContain('SELECT')
expect(sql).toContain('FROM')
expect(sql).toContain('people')
})
it("Should work with catalog.schema.table format", async () => {
// Test that we can query using full Trino naming convention
const queryResult = await util.connection.query('SELECT 1 as test_column FROM postgresql.public.people LIMIT 1')
const results = await queryResult.execute()
expect(results).toBeDefined()
expect(results.length).toBeGreaterThan(0)
expect(results[0].rows).toBeDefined()
expect(results[0].rows.length).toBeGreaterThan(0)
})
expect(results).toBeDefined()
expect(results.length).toBeGreaterThan(0)
expect(results[0].rows).toBeDefined()
})
})
}
TEST_VERSIONS.forEach(({ version, socket, readonly }) => testWith(version, socket, readonly))
describe("Trino-specific constraints and features", () => {
it("Should indicate Trino doesn't support transactions", async () => {
expect((httpUtil.connection as any).supportsTransaction).toBe(false)
})
// Additional util.connection tests
describe('Trino util.Connection Edge Cases', () => {
jest.setTimeout(dbtimeout)
it("Should return null for primary keys (not supported)", async () => {
const primaryKeys = await httpUtil.connection.getPrimaryKeys()
expect(primaryKeys).toEqual([])
it('should validate util.connection configuration', async () => {
// Basic configuration validation test
const config: IDbutil.ConnectionServerConfig = {
client: 'trino',
host: 'localhost',
port: 8080,
user: 'testuser',
readOnlyMode: false,
osUser: 'testuser',
ssh: null,
sslCaFile: null,
sslCertFile: null,
sslKeyFile: null,
sslRejectUnauthorized: false,
ssl: false,
domain: null,
socketPath: null,
socketPathEnabled: false,
password: null
}
const singlePrimaryKey = await httpUtil.connection.getPrimaryKey('people', 'public')
expect(singlePrimaryKey).toBeNull()
})
expect(config.client).toBe('trino')
expect(config.port).toBe(8080)
expect(config.readOnlyMode).toBe(false)
it("Should return null for data modification operations", async () => {
const alterResult = await httpUtil.connection.alterTable({} as any)
expect(alterResult).toBeNull()
const createDbResult = await httpUtil.connection.createDatabase()
expect(createDbResult).toBeNull()
const truncateResult = await httpUtil.connection.truncateElementSql('test', 'public')
expect(truncateResult).toBeNull()
const duplicateResult = await httpUtil.connection.duplicateTable('test', 'public', 'test_copy')
expect(duplicateResult).toBeNull()
const duplicateSqlResult = await httpUtil.connection.duplicateTableSql('test', 'public', 'test_copy')
expect(duplicateSqlResult).toBeNull()
const setNameResult = await httpUtil.connection.setElementNameSql('test', 'test_new', 'public')
expect(setNameResult).toBeNull()
const builderResult = await httpUtil.connection.getBuilder('test_table', 'public')
expect(builderResult).toBeNull()
})
it("Should support selectTopSql for query generation", async () => {
const sql = await httpUtil.connection.selectTopSql('people', 0, 10,
[], [], 'public', [])
expect(sql).toBeDefined()
expect(typeof sql).toBe('string')
expect(sql.length).toBeGreaterThan(0)
expect(sql).toContain('SELECT')
expect(sql).toContain('FROM')
expect(sql).toContain('people')
})
it("Should work with catalog.schema.table format", async () => {
const queryResult = await httpUtil.connection.query('SELECT 1 as test_column FROM postgresql.public.people LIMIT 1')
const results = await queryResult.execute()
expect(results).toBeDefined()
expect(results.length).toBeGreaterThan(0)
expect(results[0].rows).toBeDefined()
expect(results[0].rows.length).toBeGreaterThan(0)
})
})
describe('SSL connection via load balancer (bug #3695)', () => {
it("Should connect to Trino over HTTPS with ssl=true and CA cert", async () => {
const version = await httpsUtil.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 httpsUtil.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 httpsUtil.connection.selectTop('people', 0, 10, [], [], 'public', [])
expect(result.result).toBeDefined()
expect(Array.isArray(result.result)).toBe(true)
})
})
})

View File

@@ -1,12 +1,13 @@
import { dbtimeout, Options } from "@tests/lib/db";
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers";
import { GenericContainer, Wait, StartedTestContainer, Network } 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,19 +88,19 @@ export const PostgresTestDriver = {
}
}
export const TrinoTestDriver = {
export const TrinoHttpDriver = {
container: null as StartedTestContainer | null,
pgContainer: null as StartedTestContainer | null,
utilOptions: null as Options | null,
config: null as IDbConnectionServerConfig | null,
async start(dockerTag: string, readonly: boolean, network) {
const startupTimeout = dbtimeout * 2;
// Create a temporary directory for catalog configuration
// Create a temporary directory for catalog and config
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trino-'));
const catalogDir = path.join(tempDir, 'catalog');
fs.mkdirSync(catalogDir);
// Create postgresql.properties file in the catalog directory
const postgresConfig = `connector.name=postgresql
connection-url=jdbc:postgresql://postgres:5432/banana
@@ -109,14 +110,36 @@ connection-password=example`;
const catalogFile = path.join(catalogDir, "postgresql.properties")
fs.writeFileSync(catalogFile, postgresConfig)
// Create config.properties with process-forwarded enabled for load balancer support
const configDir = path.join(tempDir, 'config');
fs.mkdirSync(configDir);
const configFile = path.join(configDir, 'config.properties');
fs.writeFileSync(configFile, [
'#single node install config',
'coordinator=true',
'node-scheduler.include-coordinator=true',
'http-server.http.port=8080',
'http-server.process-forwarded=true',
'discovery.uri=http://localhost:8080',
'catalog.management=${ENV:CATALOG_MANAGEMENT}',
].join('\n'));
this.container = await new GenericContainer(`trinodb/trino:${dockerTag}`)
.withNetwork(network)
.withNetworkAliases("trino")
.withExposedPorts(8080)
.withBindMounts([{
source: catalogDir,
target: '/etc/trino/catalog',
mode: 'ro'
}])
.withBindMounts([
{
source: catalogDir,
target: '/etc/trino/catalog',
mode: 'ro'
},
{
source: configFile,
target: '/etc/trino/config.properties',
mode: 'ro'
}
])
.withWaitStrategy(Wait.forLogMessage("SERVER STARTED"))
.withStartupTimeout(startupTimeout)
.start()
@@ -146,3 +169,117 @@ connection-password=example`;
await this.container?.stop()
}
}
/**
* Nginx reverse proxy in front of Trino, following Trino docs best practice
* of using a load balancer to terminate TLS.
*
* Exposes two ports:
* - 8080: HTTP passthrough to Trino
* - 8443: HTTPS termination, proxied to Trino over HTTP
*/
export const TrinoNginxProxy = {
container: null as StartedTestContainer | null,
certFile: null as string | null,
httpConfig: null as IDbConnectionServerConfig | null,
httpsConfig: null as IDbConnectionServerConfig | null,
async start(network) {
const startupTimeout = dbtimeout
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trino-nginx-'))
const certDir = path.join(tempDir, 'certs')
fs.mkdirSync(certDir)
// Generate self-signed cert for nginx
const keyFile = path.join(certDir, 'server.key')
const certFile = path.join(certDir, 'server.crt')
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' }
)
this.certFile = certFile
// Nginx config: HTTP on 8080, HTTPS on 8443, both proxy to trino:8080
const nginxConf = path.join(tempDir, 'nginx.conf')
fs.writeFileSync(nginxConf, String.raw`
events { worker_connections 64; }
http {
server {
listen 8080;
location / {
proxy_pass http://trino:8080;
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 8443 ssl;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
location / {
proxy_pass http://trino:8080;
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
}
`)
this.container = await new GenericContainer('nginx:alpine')
.withNetwork(network)
.withExposedPorts(8080, 8443)
.withBindMounts([
{ source: nginxConf, target: '/etc/nginx/nginx.conf', mode: 'ro' },
{ source: certDir, target: '/etc/nginx/certs', mode: 'ro' },
])
.withWaitStrategy(Wait.forListeningPorts())
.withStartupTimeout(startupTimeout)
.start()
const baseConfig = {
client: 'trino' as const,
user: 'test',
password: null,
osUser: 'foo',
ssh: null,
sslCertFile: null,
sslKeyFile: null,
domain: null,
socketPath: null,
socketPathEnabled: false,
readOnlyMode: false,
}
this.httpConfig = {
...baseConfig,
host: this.container.getHost(),
port: this.container.getMappedPort(8080),
ssl: false,
sslCaFile: null,
sslRejectUnauthorized: false,
}
this.httpsConfig = {
...baseConfig,
host: this.container.getHost(),
port: this.container.getMappedPort(8443),
ssl: true,
sslCaFile: certFile,
sslRejectUnauthorized: false,
}
},
async stop() {
await this.container?.stop()
},
}

View File

@@ -0,0 +1,187 @@
const capturedQueries: string[] = []
jest.mock('trino-client', () => {
const mockQuery = jest.fn().mockImplementation((sql: string) => {
capturedQueries.push(sql)
return Promise.resolve({
[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()
})
})
describe('TrinoClient SQL escaping', () => {
let client: TrinoClient
beforeEach(async () => {
jest.clearAllMocks()
capturedQueries.length = 0
client = new TrinoClient(makeServer(), makeDatabase())
await client.connect()
capturedQueries.length = 0
})
it('should wrap catalog name with identifier quoting in listSchemas', async () => {
const maliciousDb = "cat; DROP TABLE users --"
;(client as any).db = maliciousDb
await client.listSchemas(null)
const sql = capturedQueries[0]
// Catalog name must be inside double-quote identifiers
expect(sql).toContain('"cat; DROP TABLE users --"')
})
it('should wrap catalog name with identifier quoting in listTables', async () => {
const maliciousDb = "cat; DROP TABLE users --"
;(client as any).db = maliciousDb
await client.listTables(null)
const sql = capturedQueries[0]
// Catalog name must be inside double-quote identifiers
expect(sql).toContain('"cat; DROP TABLE users --".information_schema')
})
it('should escape schema and table names in listTableColumns', async () => {
await client.listTableColumns("test'; DROP TABLE users --", "public'; DROP TABLE users --")
const sql = capturedQueries[0]
// Single quotes in values must be doubled to stay inside SQL string literals
expect(sql).toContain("public''; DROP TABLE users --")
expect(sql).toContain("test''; DROP TABLE users --")
})
})

View File

@@ -1,4 +1,4 @@
import { buildSelectTopQuery, escapeString, isAllowedReadOnlyQuery } from "../../../../../src/lib/db/clients/utils";
import { buildSchemaFilter, buildSelectTopQuery, escapeString, isAllowedReadOnlyQuery } from "../../../../../src/lib/db/clients/utils";
describe('Escape String', () => {
it("should escape single quotes", () => {
@@ -82,6 +82,31 @@ describe("buildSelectTopQuery", () => {
})
})
describe('buildSchemaFilter SQL injection', () => {
it("should escape single quotes in schema name to prevent SQL injection", () => {
const malicious = "'; DROP TABLE users; --"
const result = buildSchemaFilter({ schema: malicious })
// The single quote must be doubled so the value stays inside the SQL string literal
// Safe: schema_name = '''; DROP TABLE users; --' (the '' is an escaped quote inside the literal)
// Unsafe: schema_name = ''; DROP TABLE users; --' (the ' closes the literal, rest is executable)
// Result: schema_name = '''; DROP TABLE users; --'
// In SQL: opening ', then '' (escaped quote), then rest of string, closing '
// The whole value is treated as one string literal — safe.
expect(result).toBe("schema_name = '''; DROP TABLE users; --'")
})
it("should escape single quotes in 'only' filter values", () => {
const result = buildSchemaFilter({ only: ["public", "'; DROP TABLE users; --"] })
expect(result).toContain("'public'")
expect(result).toContain("'''; DROP TABLE users; --'")
})
it("should escape single quotes in 'ignore' filter values", () => {
const result = buildSchemaFilter({ ignore: ["'; DROP TABLE users; --"] })
expect(result).toContain("'''; DROP TABLE users; --'")
})
})
describe('isAllowedReadOnly', () => {
it('Should return as a read only query', () => {
const queries = [

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

View File

@@ -8105,10 +8105,10 @@ domexception@^4.0.0:
dependencies:
webidl-conversions "^7.0.0"
dompurify@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e"
integrity sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==
dompurify@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.2.tgz#58c515d0f8508b8749452a028aa589ad80b36325"
integrity sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
@@ -9858,14 +9858,14 @@ immediate@~3.0.5:
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
immutable@^4.0.0:
version "4.3.7"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
version "4.3.8"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.8.tgz#02d183c7727fb2bb1d5d0380da0d779dce9296a7"
integrity sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==
immutable@^5.0.2:
version "5.0.3"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1"
integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==
version "5.1.5"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.5.tgz#93ee4db5c2a9ab42a4a783069f3c5d8847d40165"
integrity sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==
import-fresh@^3.0.0:
version "3.3.1"