Files
dbmole-mcp/test/unit/db/mysql.test.ts
T
2026-06-11 23:42:21 +05:00

175 lines
5.9 KiB
TypeScript

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<string, unknown>
connection = new FakeConnection()
ended = false
constructor(options: Record<string, unknown>) {
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<string, unknown>) => new mysqlState.FakeMysqlPool(options)
}
}))
import type { ConnectionConfig } from '../../../src/config/types.js'
import { createMysqlDriver } from '../../../src/db/mysql.js'
const config = (extra: Partial<ConnectionConfig> = {}): ConnectionConfig => ({
name: 'my',
type: 'mysql',
host: 'real-host',
user: 'root',
password: 'pw',
database: 'main',
readonly: false,
...extra
})
const target = (extra: Partial<ConnectionConfig> = {}) => ({
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)
})
})