151 lines
5.7 KiB
TypeScript
151 lines
5.7 KiB
TypeScript
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'
|
|
|
|
describe('mysql integration', () => {
|
|
let container: StartedMySqlContainer
|
|
let dir: string
|
|
let registry: Registry
|
|
let manager: Manager
|
|
|
|
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()
|
|
await container.stop()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
it('runs DDL, DML with lastInsertId and SELECT with params', async () => {
|
|
const { driver } = await manager.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 manager.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 manager.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 manager.get('my')
|
|
const databases = await driver.listDatabases()
|
|
expect(databases.map((d) => d.name)).toContain(container.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 manager.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 manager.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 manager.get('my')
|
|
expect(await driver.serverVersion()).toMatch(/^8\./)
|
|
})
|
|
})
|