From 0222778a3669ba7c735ff256877c57fb35e37562 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Sun, 24 May 2026 00:57:36 +0200 Subject: [PATCH] feat(connectors): add mysql maxConnections --- .../src/connectors/mysql/connector.test.ts | 42 ++++++++++++++++++- .../cli/src/connectors/mysql/connector.ts | 26 +++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/connectors/mysql/connector.test.ts b/packages/cli/src/connectors/mysql/connector.test.ts index 5a21ada7..fbf898bc 100644 --- a/packages/cli/src/connectors/mysql/connector.test.ts +++ b/packages/cli/src/connectors/mysql/connector.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import type { FieldPacket, RowDataPacket } from 'mysql2/promise'; import { createMysqlLiveDatabaseIntrospection } from '../../connectors/mysql/live-database-introspection.js'; -import { isKtxMysqlConnectionConfig, KtxMysqlScanConnector, mysqlConnectionPoolConfigFromConfig, type KtxMysqlPoolFactory } from '../../connectors/mysql/connector.js'; +import { isKtxMysqlConnectionConfig, KtxMysqlScanConnector, mysqlConnectionPoolConfigFromConfig, type KtxMysqlConnectionConfig, type KtxMysqlPoolFactory } from '../../connectors/mysql/connector.js'; import { tableRefSet } from '../../context/scan/table-ref.js'; function mysqlResult(rows: Record[], fields: Array<{ name: string; type?: number }>): [RowDataPacket[], FieldPacket[]] { @@ -191,6 +191,46 @@ describe('KtxMysqlScanConnector', () => { }); }); + it('defaults and validates MySQL maxConnections', () => { + const baseConnection: KtxMysqlConnectionConfig = { + driver: 'mysql', + host: 'db.example.test', + database: 'analytics', + username: 'reader', + password: 'secret', // pragma: allowlist secret + }; + + expect( + mysqlConnectionPoolConfigFromConfig({ + connectionId: 'warehouse', + connection: baseConnection, + }), + ).toMatchObject({ connectionLimit: 10 }); + + expect( + mysqlConnectionPoolConfigFromConfig({ + connectionId: 'warehouse', + connection: { ...baseConnection, maxConnections: 25 }, + }), + ).toMatchObject({ connectionLimit: 25 }); + + expect( + mysqlConnectionPoolConfigFromConfig({ + connectionId: 'warehouse', + connection: { ...baseConnection, maxConnections: '12' as never }, + }), + ).toMatchObject({ connectionLimit: 12 }); + + for (const maxConnections of [0, -1, 1.5, Number.NaN, 'abc' as never]) { + expect(() => + mysqlConnectionPoolConfigFromConfig({ + connectionId: 'warehouse', + connection: { ...baseConnection, maxConnections }, + }), + ).toThrow('connections.warehouse.maxConnections must be a positive integer'); + } + }); + it('introspects schema, primary keys, comments, row counts, views, and foreign keys', async () => { const connector = new KtxMysqlScanConnector({ connectionId: 'warehouse', diff --git a/packages/cli/src/connectors/mysql/connector.ts b/packages/cli/src/connectors/mysql/connector.ts index 82a2384c..7e299dab 100644 --- a/packages/cli/src/connectors/mysql/connector.ts +++ b/packages/cli/src/connectors/mysql/connector.ts @@ -18,6 +18,7 @@ export interface KtxMysqlConnectionConfig { password?: string; url?: string; ssl?: boolean | { rejectUnauthorized?: boolean }; + maxConnections?: number; [key: string]: unknown; } @@ -163,6 +164,23 @@ function maybeNumber(value: unknown): number | undefined { return typeof value === 'number' && Number.isFinite(value) ? value : undefined; } +function positiveIntegerConfigValue(input: { + connection: KtxMysqlConnectionConfig; + key: keyof KtxMysqlConnectionConfig; + connectionId: string; + defaultValue: number; +}): number { + const value = input.connection[input.key]; + if (value === undefined) { + return input.defaultValue; + } + const numberValue = Number(value); + if (!Number.isInteger(numberValue) || numberValue < 1) { + throw new Error(`connections.${input.connectionId}.${String(input.key)} must be a positive integer`); + } + return numberValue; +} + function parseMysqlUrl(url: string): Partial { const parsed = new URL(url); const sslParam = parsed.searchParams.get('ssl') ?? parsed.searchParams.get('sslmode'); @@ -262,6 +280,12 @@ export function mysqlConnectionPoolConfigFromConfig(input: { const host = stringConfigValue(merged, 'host', env); const database = stringConfigValue(merged, 'database', env); const user = stringConfigValue(merged, 'username', env) ?? stringConfigValue(merged, 'user', env); + const maxConnections = positiveIntegerConfigValue({ + connection: merged, + key: 'maxConnections', + connectionId: input.connectionId, + defaultValue: 10, + }); if (!host) { throw new Error(`Native MySQL connector requires connections.${input.connectionId}.host or url`); @@ -280,7 +304,7 @@ export function mysqlConnectionPoolConfigFromConfig(input: { database, user, password: stringConfigValue(merged, 'password', env), - connectionLimit: 10, + connectionLimit: maxConnections, waitForConnections: true, ...(ssl ? { ssl: { rejectUnauthorized: ssl.rejectUnauthorized ?? false } } : {}), };