import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql' 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('postgres integration', () => { let container: StartedPostgreSqlContainer let dir: string let registry: Registry let manager: Manager beforeAll(async () => { container = await new PostgreSqlContainer('postgres:17-alpine').start() dir = mkdtempSync(join(tmpdir(), 'dbmole-int-pg-')) registry = createRegistry({ storePath: join(dir, 'connections.json'), env: {} }) manager = createManager(registry) registry.add({ name: 'pg', type: 'postgres', host: container.getHost(), port: container.getPort(), user: container.getUsername(), password: container.getPassword(), database: container.getDatabase(), readonly: false }) registry.add({ name: 'pg-ro', type: 'postgres', host: container.getHost(), port: container.getPort(), user: container.getUsername(), password: container.getPassword(), database: container.getDatabase(), readonly: true }) }) afterAll(async () => { await manager.disposeAll() await container.stop() rmSync(dir, { recursive: true, force: true }) }) it('runs DDL, DML and SELECT with params', async () => { const { driver } = await manager.get('pg') await driver.query({ sql: 'CREATE TABLE users (id serial PRIMARY KEY, name text NOT NULL)', rowLimit: 10 }) const insert = await driver.query({ sql: "INSERT INTO users (name) VALUES ('ada'), ('bob')", rowLimit: 10 }) expect(insert.rowCount).toBe(2) const select = await driver.query({ sql: 'SELECT name FROM users WHERE id = $1', params: [1], rowLimit: 10 }) expect(select.columns).toEqual(['name']) expect(select.rows).toEqual([['ada']]) }) it('truncates SELECT results at rowLimit', async () => { const { driver } = await manager.get('pg') const result = await driver.query({ sql: 'SELECT generate_series(1, 10)', rowLimit: 3 }) expect(result.rows).toHaveLength(3) expect(result.truncated).toBe(true) }) it('rejects multi-statement and session-level sql', async () => { const { driver } = await manager.get('pg') await expect(driver.query({ sql: 'SELECT 1; SELECT 2', rowLimit: 10 })).rejects.toThrow( /one SQL statement/ ) await expect(driver.query({ sql: 'BEGIN', rowLimit: 10 })).rejects.toThrow(/session-level/) }) it('lists databases, tables and describes a table', async () => { const { driver } = await manager.get('pg') const databases = await driver.listDatabases() expect(databases.map((d) => d.name)).toContain(container.getDatabase()) const tables = await driver.listTables({}) expect(tables).toContainEqual(expect.objectContaining({ schema: 'public', name: 'users' })) await driver.query({ sql: `CREATE TABLE orders ( id serial PRIMARY KEY, user_id integer NOT NULL REFERENCES users(id), note text )`, rowLimit: 10 }) await driver.query({ sql: 'CREATE INDEX orders_user_idx ON orders (user_id)', rowLimit: 10 }) 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.columns[1].nullable).toBe(false) expect(description.indexes).toContainEqual( expect.objectContaining({ name: 'orders_user_idx', columns: ['user_id'], unique: false }) ) expect(description.foreignKeys).toContainEqual( expect.objectContaining({ columns: ['user_id'], referencedTable: 'users' }) ) }) it('describes composite foreign keys with paired columns', async () => { const { driver } = await manager.get('pg') await driver.query({ sql: `CREATE TABLE parents ( a integer NOT NULL, b integer NOT NULL, PRIMARY KEY (a, b) )`, rowLimit: 10 }) await driver.query({ sql: `CREATE TABLE children ( id serial PRIMARY KEY, pa integer NOT NULL, pb integer NOT NULL, FOREIGN KEY (pa, pb) REFERENCES parents (a, b) )`, rowLimit: 10 }) const description = await driver.describeTable({ table: 'children' }) expect(description.foreignKeys).toHaveLength(1) expect(description.foreignKeys[0].columns).toEqual(['pa', 'pb']) expect(description.foreignKeys[0].referencedTable).toBe('parents') expect(description.foreignKeys[0].referencedColumns).toEqual(['a', 'b']) }) it('describe_table of a missing table fails clearly', async () => { const { driver } = await manager.get('pg') 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('pg-ro') const select = await driver.query({ sql: 'SELECT count(*) FROM users', rowLimit: 10 }) expect(select.rows[0][0]).toBe('2') await expect( driver.query({ sql: "INSERT INTO users (name) VALUES ('mallory')", rowLimit: 10 }) ).rejects.toThrow(/read-only/) await expect( driver.query({ sql: 'CREATE TABLE hacked (id int)', rowLimit: 10 }) ).rejects.toThrow(/read-only/) }) it('reports server version', async () => { const { driver } = await manager.get('pg') expect(await driver.serverVersion()).toMatch(/^17\./) }) })