import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { MySqlContainer, type StartedMySqlContainer } from '@testcontainers/mysql' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import type { Registry } from '../../src/config/registry.js' import { createRegistry } from '../../src/config/registry.js' import type { Manager } from '../../src/db/manager.js' import { createManager } from '../../src/db/manager.js' // Ordering contract: vitest runs in-file `it` blocks sequentially in // declaration order, and these tests share tables across blocks by design // (e.g. `users` created early is read by later cases). Do not reorder. describe('mysql integration', () => { let container: StartedMySqlContainer | undefined let dir: string | undefined let registry: Registry let manager: Manager | undefined beforeAll(async () => { container = await new MySqlContainer('mysql:8.4').start() dir = mkdtempSync(join(tmpdir(), 'dbmole-int-my-')) registry = createRegistry({ storePath: join(dir, 'connections.json'), env: {} }) manager = createManager(registry) registry.add({ name: 'my', type: 'mysql', host: container.getHost(), port: container.getPort(), user: container.getUsername(), password: container.getUserPassword(), database: container.getDatabase(), readonly: false }) registry.add({ name: 'my-ro', type: 'mysql', host: container.getHost(), port: container.getPort(), user: container.getUsername(), password: container.getUserPassword(), database: container.getDatabase(), readonly: true }) }) afterAll(async () => { await manager?.disposeAll().catch(() => {}) await container?.stop().catch(() => {}) if (dir) { rmSync(dir, { recursive: true, force: true }) } }) const db = (): Manager => { if (!manager) { throw new Error('setup failed: manager not initialized') } return manager } const my = (): StartedMySqlContainer => { if (!container) { throw new Error('setup failed: container not initialized') } return container } it('runs DDL, DML with lastInsertId and SELECT with params', async () => { const { driver } = await db().get('my') await driver.query({ sql: 'CREATE TABLE users (id int AUTO_INCREMENT PRIMARY KEY, name varchar(64) NOT NULL)', rowLimit: 10 }) const insert = await driver.query({ sql: "INSERT INTO users (name) VALUES ('ada'), ('bob')", rowLimit: 10 }) expect(insert.rowCount).toBe(2) expect(insert.lastInsertId).toBe('1') const select = await driver.query({ sql: 'SELECT name FROM users WHERE id = ?', params: [2], rowLimit: 10 }) expect(select.columns).toEqual(['name']) expect(select.rows).toEqual([['bob']]) }) it('preserves BIGINT precision beyond 2^53', async () => { const { driver } = await db().get('my') await driver.query({ sql: 'CREATE TABLE big (v bigint)', rowLimit: 10 }) await driver.query({ sql: 'INSERT INTO big (v) VALUES (9007199254740993)', rowLimit: 10 }) const result = await driver.query({ sql: 'SELECT v FROM big', rowLimit: 10 }) expect(result.rows[0][0]).toBe('9007199254740993') }) it('rejects multi-statement and session-level sql', async () => { const { driver } = await db().get('my') await expect(driver.query({ sql: 'SELECT 1; SELECT 2', rowLimit: 10 })).rejects.toThrow( /one SQL statement/ ) await expect( driver.query({ sql: 'SET SESSION TRANSACTION READ WRITE', rowLimit: 10 }) ).rejects.toThrow(/session-level/) }) it('lists databases and tables, describes a table', async () => { const { driver } = await db().get('my') const databases = await driver.listDatabases() expect(databases.map((d) => d.name)).toContain(my().getDatabase()) await driver.query({ sql: `CREATE TABLE orders ( id int AUTO_INCREMENT PRIMARY KEY, user_id int NOT NULL, note text, KEY orders_user_idx (user_id), CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id) )`, rowLimit: 10 }) const tables = await driver.listTables({}) expect(tables.map((t) => t.name)).toEqual(expect.arrayContaining(['orders', 'users'])) const description = await driver.describeTable({ table: 'orders' }) expect(description.primaryKey).toEqual(['id']) expect(description.columns.map((c) => c.name)).toEqual(['id', 'user_id', 'note']) expect(description.indexes).toContainEqual( expect.objectContaining({ name: 'orders_user_idx', unique: false }) ) expect(description.foreignKeys).toContainEqual( expect.objectContaining({ name: 'fk_orders_user', columns: ['user_id'], referencedTable: 'users' }) ) }) it('describe_table of a missing table fails clearly', async () => { const { driver } = await db().get('my') await expect(driver.describeTable({ table: 'ghost' })).rejects.toThrow(/not found/) }) it('enforces readonly: INSERT and DDL fail, SELECT works', async () => { const { driver } = await db().get('my-ro') const select = await driver.query({ sql: 'SELECT count(*) FROM users', rowLimit: 10 }) expect(Number(select.rows[0][0])).toBe(2) await expect( driver.query({ sql: "INSERT INTO users (name) VALUES ('mallory')", rowLimit: 10 }) ).rejects.toThrow(/READ ONLY/i) await expect( driver.query({ sql: 'CREATE TABLE hacked (id int)', rowLimit: 10 }) ).rejects.toThrow(/READ ONLY/i) }) it('reports server version', async () => { const { driver } = await db().get('my') expect(await driver.serverVersion()).toMatch(/^8\./) }) })