PawQL provides a DummyAdapter for testing without a real database connection. This adapter records all executed SQL so you can assert against the generated queries.
DummyAdapter is available via a separate subpath import, kept out of production code:
// ✅ Correct — import from the testing subpath
import { DummyAdapter } from 'pawql/testing';
// ❌ Wrong — not available from the main import
import { DummyAdapter } from 'pawql'; // Error!
Why is it separate? So that
DummyAdapterdoesn't end up in your production bundle. See Philosophy below.
import { test } from 'node:test';
import assert from 'node:assert';
import { createDB } from 'pawql';
import { DummyAdapter } from 'pawql/testing';
const schema = {
users: {
id: Number,
name: String,
email: String,
}
};
test('should generate correct SELECT SQL', async () => {
const adapter = new DummyAdapter(); // defaults to postgres dialect
// For dialect-specific assertions: new DummyAdapter('mysql') or new DummyAdapter('sqlite')
const db = createDB(schema, adapter);
await db.query('users')
.select('id', 'name')
.where({ id: 1 })
.execute();
// Check the generated SQL
const lastLog = adapter.logs[adapter.logs.length - 1]!;
assert.strictEqual(lastLog.sql, 'SELECT "id", "name" FROM "users" WHERE "id" = $1');
assert.deepStrictEqual(lastLog.params, [1]);
});
Dialect-aware:
new DummyAdapter('postgres'|'mysql'|'sqlite')controls quoting ("id"vs`id`),RETURNING,ILIKE/ON CONFLICT/FULL JOINguards, and DDL type mapping.
## DummyAdapter API
### `adapter.logs`
An array containing all executed queries:
```typescript
type LogEntry = {
sql: string; // The SQL string that was executed
params: any[]; // Parameter values
};
adapter.logs; // LogEntry[]
const adapter = new DummyAdapter();
const db = createDB(schema, adapter);
await db.query('users').insert({ id: 1, name: 'Alice' }).execute();
await db.query('users').where({ id: 1 }).execute();
console.log(adapter.logs);
// [
// { sql: 'INSERT INTO "users" ...', params: [1, 'Alice'] },
// { sql: 'SELECT * FROM "users" WHERE "id" = $1', params: [1] }
// ]
// Access the last log entry
const lastQuery = adapter.logs[adapter.logs.length - 1]!;
When testing transactions, DummyAdapter automatically shares logs across transaction adapters:
test('transaction logs BEGIN/COMMIT', async () => {
const adapter = new DummyAdapter();
const db = createDB(schema, adapter);
await db.transaction(async (tx) => {
await tx.query('users').insert({ id: 1 }).execute();
});
assert.strictEqual(adapter.logs[0]!.sql, 'BEGIN');
assert.ok(adapter.logs[1]!.sql.includes('INSERT'));
assert.strictEqual(adapter.logs[2]!.sql, 'COMMIT');
});
test('generates correct SQL', async () => {
const adapter = new DummyAdapter();
const db = createDB(schema, adapter);
await db.query('users')
.where({ age: { gt: 18 } })
.orderBy('name', 'ASC')
.limit(10)
.execute();
const log = adapter.logs[0]!;
assert.strictEqual(
log.sql,
'SELECT * FROM "users" WHERE "age" > $1 ORDER BY "name" ASC LIMIT 10'
);
assert.deepStrictEqual(log.params, [18]);
});
test('passes correct parameters', async () => {
const adapter = new DummyAdapter();
const db = createDB(schema, adapter);
await db.query('users')
.insert({ id: 1, name: 'Alice', email: 'alice@test.com' })
.execute();
const log = adapter.logs[0]!;
assert.strictEqual(log.params.length, 3);
assert.strictEqual(log.params[0], 1);
assert.strictEqual(log.params[1], 'Alice');
assert.strictEqual(log.params[2], 'alice@test.com');
});
// ✅ Each test gets a fresh adapter — logs don't leak between tests
test('test 1', async () => {
const adapter = new DummyAdapter();
const db = createDB(schema, adapter);
// ...
});
test('test 2', async () => {
const adapter = new DummyAdapter();
const db = createDB(schema, adapter);
// ... adapter.logs starts empty
});
test('creates correct table DDL', async () => {
const adapter = new DummyAdapter('postgres');
const db = createDB({
products: {
id: { type: Number, primaryKey: true },
name: String,
price: Number,
meta: json<{a:string}>(),
uid: uuid,
}
}, adapter);
await db.createTables();
const sql = adapter.logs[0]!.sql;
assert.ok(sql.includes('CREATE TABLE IF NOT EXISTS "products"'));
assert.ok(sql.includes('"id" INTEGER PRIMARY KEY'));
assert.ok(sql.includes('"name" TEXT NOT NULL'));
assert.ok(sql.includes('"price" INTEGER NOT NULL'));
assert.ok(sql.includes('"meta" JSONB NOT NULL')); // MySQL→JSON, SQLite→TEXT
assert.ok(sql.includes('"uid" UUID NOT NULL')); // MySQL→VARCHAR(36), SQLite→TEXT
});
test('arrayType only on postgres', async () => {
const pg = new DummyAdapter('postgres');
const mysql = new DummyAdapter('mysql');
assert.doesNotThrow(async () => {
const r = createMigrationRunner(pg);
await r.createTable('t', { tags: arrayType(String) });
});
await assert.rejects(async () => {
const r = createMigrationRunner(mysql);
await r.createTable('t', { tags: arrayType(String) });
}, /ArrayType is only supported on PostgreSQL/);
});
## Compatible Test Runners
PawQL tests use the `node:test` module, which is compatible with:
- **Node.js** 18+ (`npx tsx --test test/*.test.ts`)
- **Bun** (`bun test`)
```json
// package.json
{
"scripts": {
"test": "npx tsx --test test/*.test.ts",
"test:bun": "bun test"
}
}
DummyAdapter is separated from production exports because:
@angular/core/testing, react/test-utilsPawQL also includes comprehensive integration tests that run against a real PostgreSQL database via Docker. These tests verify every feature end-to-end.
# 1. Start PostgreSQL container
npm run test:docker:up
# 2. Run integration tests
npm run test:integration
# 3. Stop the container when done
npm run test:docker:down
The integration test suite covers:
The test database uses port 5433 (not the default 5432) to avoid conflicts with local PostgreSQL:
# docker-compose.test.yml
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: pawql_test
POSTGRES_PASSWORD: pawql_test_pass
POSTGRES_DB: pawql_test
ports:
- "5433:5432"
tmpfs:
- /var/lib/postgresql/data # RAM-based for speed
{
"test": "Unit tests only (no Docker needed)",
"test:unit": "Unit tests only",
"test:integration": "Integration tests (requires Docker)",
"test:all": "All tests (unit + integration)",
"test:docker:up": "Start test database",
"test:docker:down": "Stop test database"
}