import { beforeEach, describe, expect, it, vi } from 'vitest' const mysqlState = vi.hoisted(() => { const state = { pools: [] as FakeMysqlPool[], nextResult: undefined as unknown, nextFields: undefined as unknown } class FakeConnection { queries: unknown[] = [] released = 0 async query(args: unknown) { this.queries.push(args) return [state.nextResult ?? [], state.nextFields ?? []] } release() { this.released += 1 } } class FakeMysqlPool { options: Record connection = new FakeConnection() ended = false constructor(options: Record) { this.options = options state.pools.push(this) } async getConnection() { return this.connection } async end() { this.ended = true } } return { state, FakeMysqlPool } }) vi.mock('mysql2/promise', () => ({ default: { createPool: (options: Record) => new mysqlState.FakeMysqlPool(options) } })) import type { ConnectionConfig } from '../../../src/config/types.js' import { createMysqlDriver } from '../../../src/db/mysql.js' const config = (extra: Partial = {}): ConnectionConfig => ({ name: 'my', type: 'mysql', host: 'real-host', user: 'root', password: 'pw', database: 'main', readonly: false, ...extra }) const target = (extra: Partial = {}) => ({ config: config(extra), host: '127.0.0.1', port: 13306 }) describe('createMysqlDriver', () => { beforeEach(() => { mysqlState.state.pools.length = 0 mysqlState.state.nextResult = undefined mysqlState.state.nextFields = undefined }) it('creates pools with multipleStatements disabled', async () => { const driver = createMysqlDriver(target()) await driver.query({ sql: 'select 1', rowLimit: 10 }) expect(mysqlState.state.pools[0].options).toMatchObject({ host: '127.0.0.1', port: 13306, database: 'main', connectionLimit: 4, multipleStatements: false }) }) it('reuses pools per database', async () => { const driver = createMysqlDriver(target()) await driver.query({ sql: 'select 1', rowLimit: 10 }) await driver.query({ sql: 'select 2', rowLimit: 10 }) await driver.query({ sql: 'select 3', database: 'other', rowLimit: 10 }) expect(mysqlState.state.pools).toHaveLength(2) }) it('applies SET SESSION TRANSACTION READ ONLY once per connection when readonly', async () => { const driver = createMysqlDriver(target({ readonly: true })) await driver.query({ sql: 'select 1', rowLimit: 10 }) await driver.query({ sql: 'select 2', rowLimit: 10 }) const queries = mysqlState.state.pools[0].connection.queries const readonlySets = queries.filter((q) => q === 'SET SESSION TRANSACTION READ ONLY') expect(readonlySets).toHaveLength(1) expect(queries[0]).toBe('SET SESSION TRANSACTION READ ONLY') }) it('does not set readonly for writable connections', async () => { const driver = createMysqlDriver(target()) await driver.query({ sql: 'select 1', rowLimit: 10 }) const queries = mysqlState.state.pools[0].connection.queries expect(queries.some((q) => q === 'SET SESSION TRANSACTION READ ONLY')).toBe(false) }) it('maps SELECT results with columns and normalized cells', async () => { mysqlState.state.nextResult = [[1n, 'x']] mysqlState.state.nextFields = [{ name: 'id' }, { name: 'label' }] const driver = createMysqlDriver(target()) const result = await driver.query({ sql: 'select * from t', rowLimit: 10 }) expect(result).toEqual({ columns: ['id', 'label'], rows: [['1', 'x']], rowCount: 1, truncated: false }) }) it('maps DML results to rowCount and lastInsertId', async () => { mysqlState.state.nextResult = { affectedRows: 3, insertId: 7 } const driver = createMysqlDriver(target()) const result = await driver.query({ sql: 'insert into t values (1)', rowLimit: 10 }) expect(result).toEqual({ columns: [], rows: [], rowCount: 3, truncated: false, lastInsertId: '7' }) }) it('omits lastInsertId when zero', async () => { mysqlState.state.nextResult = { affectedRows: 1, insertId: 0 } const driver = createMysqlDriver(target()) const result = await driver.query({ sql: 'update t set a=1', rowLimit: 10 }) expect(result.lastInsertId).toBeUndefined() }) it('listTables resolves schema > database > connection default', async () => { mysqlState.state.nextResult = [['main', 'users', 10]] mysqlState.state.nextFields = [] const driver = createMysqlDriver(target()) await driver.listTables({}) await driver.listTables({ database: 'db-param' }) await driver.listTables({ schema: 'schema-param' }) const calls = mysqlState.state.pools.flatMap((pool) => pool.connection.queries.filter( (q): q is { sql: string; values: unknown[] } => typeof q === 'object' && q !== null && 'values' in (q as object) ) ) expect(calls[0].values).toEqual(['main']) expect(calls[1].values).toEqual(['db-param']) expect(calls[2].values).toEqual(['schema-param']) }) it('dispose ends all pools', async () => { const driver = createMysqlDriver(target()) await driver.query({ sql: 'select 1', rowLimit: 1 }) await driver.listDatabases() await driver.dispose() expect(mysqlState.state.pools.every((pool) => pool.ended)).toBe(true) }) })