5 Things This Playwright SQL Fixture Does So You Don't Have To
If you write Playwright tests that touch a database, you know the boilerplate: connect, seed, clean up, close. Repeat for every file. Hope nothing leaks.
@playwright-labs/fixture-sql handles that boilerplate. Here is exactly what it takes off your plate.
1. Opens and closes connections automatically
Without the fixture, every test file that needs a database connection looks like this:
let client: SqlClient;
beforeEach(async () => { client = await createConnection(); });
afterEach(async () => { await client.close(); });
With the fixture, it is one line:
test.use({ sqlAdapter: sqliteAdapter(':memory:') });
test('my test', async ({ sql: db }) => {
// db is ready — no setup needed
// db.close() is called by the fixture after this function, even if it throws
});
The connection lifecycle is entirely handled by Playwright's fixture system.
2. Prevents connection leaks when tests fail
When a test throws an error, code in afterEach might not run correctly depending on how the error propagates. Fixtures are different — Playwright guarantees their teardown phase runs regardless of test outcome.
Every connection opened through sql or useSql is tracked and closed in teardown. You cannot leak a connection through a failing test.
3. Catches SQL errors at compile time
The fixture integrates with @playwright-labs/sql-core's compile-time SQL validator. Write your queries with sql("…") instead of a raw string:
// ❌ runtime error
await db.query('SELECT * WHERE id = ?', [1]);
// ✅ compile error — TypeScript catches missing FROM before tests run
await db.query(sql("SELECT * WHERE id = ?"), [1]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ never
The type system models SQL grammar as a finite-state machine. Invalid structure → never. Wrong number of params → compile error.
const q = sql("SELECT name FROM users WHERE id = ?");
// typeof q → SqlStatement<[unknown]>
await db.query(q, [1]); // ✅
await db.query(q); // ❌ params required
await db.query(q, [1, 2, 3]); // ❌ too many params
4. Handles multiple connections with one call
Need to test replication, or talk to two databases in one test? useSql opens additional connections and registers them all for teardown:
test('primary → replica', async ({ useSql }) => {
const primary = await useSql(pgAdapter(process.env.PRIMARY_URL!));
const replica = await useSql(pgAdapter(process.env.REPLICA_URL!));
await primary.execute(sql("INSERT INTO events (type) VALUES ($1)"), ['login']);
const { rows } = await replica.query(sql("SELECT * FROM events WHERE type = $1"), ['login']);
expect(rows).toHaveLength(1);
});
// both connections closed automatically
No manual cleanup. No try/finally.
5. Works with PostgreSQL, MySQL, and SQLite out of the box
Same API, three drivers:
// SQLite — in-memory, zero infrastructure
import { sqliteAdapter } from '@playwright-labs/fixture-sql/sqlite';
test.use({ sqlAdapter: sqliteAdapter(':memory:') });
// PostgreSQL
import { pgAdapter } from '@playwright-labs/fixture-sql/pg';
test.use({ sqlAdapter: pgAdapter(process.env.DATABASE_URL!) });
// MySQL / MariaDB
import { mysqlAdapter } from '@playwright-labs/fixture-sql/mysql';
test.use({ sqlAdapter: mysqlAdapter(process.env.MYSQL_URL!) });
Drivers are optional peer dependencies — install only the one you use.
Bonus: editor autocomplete
Pair with @playwright-labs/ts-plugin-sql and add a schema file to get table/column autocomplete and diagnostics inside sql templates directly in your editor.
# generate schema types from your live DB
pnpm playwright-labs-sql-core pull --adapter pg --url postgresql://localhost/mydb --out ./src/db-types.ts
// tsconfig.json
{
"compilerOptions": {
"plugins": [{ "name": "@playwright-labs/ts-plugin-sql", "schemaFile": "./src/db-types.ts" }]
}
}
Get started
pnpm add -D @playwright-labs/fixture-sql
pnpm add -D better-sqlite3 # or pg / mysql2
Source: github.com/vitalics/playwright-labs — MIT license.