import { Kysely, Transaction } from 'kysely'; import { KyseraLogger, Dialect } from '@kysera/core'; export { Dialect } from '@kysera/core'; import { QueryBuilderContext, Plugin, KyseraExecutor } from '@kysera/executor'; /** * Transaction-based testing utilities. * * @module @kysera/testing */ /** * Test in a transaction that automatically rolls back. * * This is the **fastest testing approach** - no cleanup needed! * All changes made within the transaction are automatically rolled back * after the test completes, leaving the database in its original state. * * @param db - Kysely database instance * @param fn - Test function that receives a transaction * * @example * ```typescript * import { testInTransaction } from '@kysera/testing'; * * it('creates user', async () => { * await testInTransaction(db, async (trx) => { * const user = await trx * .insertInto('users') * .values({ email: 'test@example.com' }) * .returningAll() * .executeTakeFirst(); * * expect(user?.email).toBe('test@example.com'); * }); * // Transaction automatically rolled back - database is clean! * }); * ``` */ declare function testInTransaction(db: Kysely, fn: (trx: Transaction) => Promise): Promise; /** * Test with savepoints for nested transaction testing. * * Useful for testing complex business logic that uses nested transactions. * Creates a savepoint before running the test function and rolls back * to the savepoint after completion. * * @param db - Kysely database instance * @param fn - Test function that receives a transaction * @param logger - Optional logger for warnings (defaults to silentLogger) * * @example * ```typescript * import { testWithSavepoints } from '@kysera/testing'; * * it('handles nested operations', async () => { * await testWithSavepoints(db, async (trx) => { * // Test complex nested transaction logic * await createUserWithProfile(trx, userData); * * // Verify results * const user = await trx.selectFrom('users').selectAll().executeTakeFirst(); * expect(user).toBeDefined(); * }); * }); * ``` */ declare function testWithSavepoints(db: Kysely, fn: (trx: Transaction) => Promise, logger?: KyseraLogger): Promise; /** * Isolation level for transactions. * Subset of kysely's IsolationLevel supported across dialects. */ type IsolationLevel = 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable'; /** * Test with specific transaction isolation level. * * Uses kysely's dialect-aware `setIsolationLevel`, which emits the correct * statements per database (PostgreSQL/MSSQL: inside the transaction; MySQL: * before starting it — a raw `SET TRANSACTION` inside an active MySQL * transaction would fail with ER_CANT_CHANGE_TX_CHARACTERISTICS). * SQLite: kysely's SqliteDriver accepts only 'serializable' natively; other * levels are silently ignored (the driver issues a plain `begin`), no error. * * Useful for testing behavior under different isolation levels, * such as testing for race conditions or phantom reads. * * @param db - Kysely database instance * @param isolationLevel - Transaction isolation level * @param fn - Test function that receives a transaction * * @example * ```typescript * import { testWithIsolation } from '@kysera/testing'; * * it('handles serializable isolation', async () => { * await testWithIsolation(db, 'serializable', async (trx) => { * // Test behavior under serializable isolation * }); * }); * ``` */ declare function testWithIsolation(db: Kysely, isolationLevel: IsolationLevel, fn: (trx: Transaction) => Promise): Promise; /** * Database cleanup utilities. * * @module @kysera/testing */ /** * Database cleanup strategies. */ type CleanupStrategy = 'truncate' | 'transaction' | 'delete'; /** * Options for database cleanup operations. */ interface CleanupOptions { /** * Explicitly specify the database dialect. * If not provided, will attempt to detect from Kysely instance. */ dialect?: Dialect; /** * List of tables to clean (in deletion order for 'delete' strategy). */ tables?: string[]; /** * Logger for warnings and errors. * Defaults to silentLogger (no output). */ logger?: KyseraLogger; } /** * Clean database using specified strategy. * * Different strategies have different performance characteristics: * - `transaction`: No cleanup (fastest, use with testInTransaction) * - `delete`: DELETE FROM each table (medium speed, FK-safe order required) * - `truncate`: TRUNCATE TABLE (fastest bulk clean, handles FKs automatically) * * @param db - Kysely database instance * @param strategy - Cleanup strategy * @param tablesOrOptions - List of tables to clean or cleanup options * * @example Using delete strategy * ```typescript * import { cleanDatabase } from '@kysera/testing'; * * afterEach(async () => { * // Tables in FK-safe order (children first) * await cleanDatabase(db, 'delete', ['order_items', 'orders', 'users']); * }); * ``` * * @example Using truncate strategy with explicit dialect * ```typescript * import { cleanDatabase } from '@kysera/testing'; * * afterEach(async () => { * await cleanDatabase(db, 'truncate', { * dialect: 'postgres', * tables: ['users', 'orders', 'order_items'] * }); * }); * ``` */ declare function cleanDatabase(db: Kysely, strategy?: CleanupStrategy, tablesOrOptions?: string[] | CleanupOptions): Promise; /** * Test data factory utilities. * * @module @kysera/testing */ /** * Factory function type. */ type FactoryFunction = (overrides?: Partial) => T; /** * Factory definition - values or functions that return values. */ type FactoryDefaults> = { [K in keyof T]: T[K] | (() => T[K]); }; /** * Create a generic test data factory. * * Factories allow you to create test data with sensible defaults * while still being able to override specific fields. * * @param defaults - Default values (can be values or functions) * @returns Factory function that creates test data * * @example Basic factory * ```typescript * import { createFactory } from '@kysera/testing'; * * const createUser = createFactory({ * email: () => `user-${Date.now()}@example.com`, * name: 'Test User', * role: 'user', * }); * * // Create with defaults * const user1 = createUser(); * // { email: 'user-1234567890@example.com', name: 'Test User', role: 'user' } * * // Create with overrides * const admin = createUser({ role: 'admin', name: 'Admin User' }); * // { email: 'user-1234567891@example.com', name: 'Admin User', role: 'admin' } * ``` * * @example With sequential IDs * ```typescript * let userId = 0; * const createUser = createFactory({ * id: () => ++userId, * email: () => `user-${userId}@example.com`, * name: 'Test User', * }); * * const user1 = createUser(); // { id: 1, email: 'user-1@example.com', name: 'Test User' } * const user2 = createUser(); // { id: 2, email: 'user-2@example.com', name: 'Test User' } * ``` */ declare function createFactory>(defaults: FactoryDefaults): FactoryFunction; /** * Create multiple instances using a factory. * * @param factory - Factory function * @param count - Number of instances to create * @param overridesFn - Optional function to generate overrides for each instance * @returns Array of created instances * * @example * ```typescript * import { createFactory, createMany } from '@kysera/testing'; * * const createUser = createFactory({ * email: () => `user-${Date.now()}@example.com`, * name: 'Test User', * }); * * // Create 5 users with defaults * const users = createMany(createUser, 5); * * // Create 3 users with custom overrides * const admins = createMany(createUser, 3, (i) => ({ * name: `Admin ${i + 1}`, * role: 'admin', * })); * ``` */ declare function createMany(factory: FactoryFunction, count: number, overridesFn?: (index: number) => Partial): T[]; /** * Create a factory with a sequence counter. * * Provides a built-in sequence number that increments with each call. * * @param defaults - Function that receives sequence number and returns defaults * @returns Factory function with sequence support * * @example * ```typescript * import { createSequenceFactory } from '@kysera/testing'; * * const createUser = createSequenceFactory((seq) => ({ * id: seq, * email: `user-${seq}@example.com`, * name: `User ${seq}`, * })); * * const user1 = createUser(); // { id: 1, email: 'user-1@example.com', name: 'User 1' } * const user2 = createUser(); // { id: 2, email: 'user-2@example.com', name: 'User 2' } * ``` */ declare function createSequenceFactory>(defaults: (sequence: number) => FactoryDefaults): FactoryFunction; /** * Database seeding utilities. * * @module @kysera/testing */ /** * Seed database with test data. * * Executes the seeding function within a transaction. * If the seeding function throws, the transaction is rolled back. * * @param db - Kysely database instance * @param fn - Seeding function that receives a transaction * * @example * ```typescript * import { seedDatabase } from '@kysera/testing'; * * beforeAll(async () => { * await seedDatabase(db, async (trx) => { * // Insert test users * await trx * .insertInto('users') * .values([ * { email: 'alice@example.com', name: 'Alice' }, * { email: 'bob@example.com', name: 'Bob' }, * ]) * .execute(); * * // Insert related data * await trx * .insertInto('posts') * .values([ * { user_id: 1, title: 'First Post' }, * ]) * .execute(); * }); * }); * ``` */ declare function seedDatabase(db: Kysely, fn: (trx: Transaction) => Promise): Promise; /** * Seed function type for reusable seeders. */ type SeedFunction = (trx: Transaction) => Promise; /** * Create a composable seeder. * * Allows combining multiple seeders into one. * * @param seeders - Array of seed functions * @returns Combined seed function * * @example * ```typescript * import { composeSeeders, seedDatabase } from '@kysera/testing'; * * const seedUsers: SeedFunction = async (trx) => { * await trx.insertInto('users').values([...]).execute(); * }; * * const seedPosts: SeedFunction = async (trx) => { * await trx.insertInto('posts').values([...]).execute(); * }; * * const seedAll = composeSeeders([seedUsers, seedPosts]); * * beforeAll(async () => { * await seedDatabase(db, seedAll); * }); * ``` */ declare function composeSeeders(seeders: SeedFunction[]): SeedFunction; /** * Testing helper utilities. * * @module @kysera/testing */ /** * Options for waitFor function. */ interface WaitForOptions { /** * Maximum time to wait in milliseconds. * @default 5000 */ timeout?: number; /** * Interval between condition checks in milliseconds. * @default 100 */ interval?: number; /** * Custom error message on timeout. * @default 'Condition not met within timeout' */ timeoutMessage?: string; } /** * Wait for a condition to be true. * * Useful for testing async operations like background jobs, * event handlers, or eventual consistency scenarios. * * @param condition - Function that returns true when condition is met * @param options - Configuration options * @throws {Error} If timeout is exceeded before condition is met * * @example Basic usage * ```typescript * import { waitFor } from '@kysera/testing'; * * // Wait for user to appear in database * await waitFor(async () => { * const user = await db * .selectFrom('users') * .where('email', '=', 'test@example.com') * .executeTakeFirst(); * return user !== undefined; * }); * ``` * * @example With custom options * ```typescript * import { waitFor } from '@kysera/testing'; * * await waitFor( * async () => { * const count = await getProcessedCount(); * return count >= 10; * }, * { * timeout: 10000, * interval: 200, * timeoutMessage: 'Jobs did not complete in time', * } * ); * ``` */ declare function waitFor(condition: () => Promise | boolean, options?: WaitForOptions): Promise; /** * Snapshot database state for later comparison. * * @param db - Kysely database instance * @param table - Table name to snapshot * @returns Array of all rows in the table * * @example * ```typescript * import { snapshotTable } from '@kysera/testing'; * * const before = await snapshotTable(db, 'users'); * * // Perform operations... * * const after = await snapshotTable(db, 'users'); * expect(after.length).toBe(before.length + 1); * ``` */ declare function snapshotTable(db: Kysely, table: string): Promise; /** * Count rows in a table. * * @param db - Kysely database instance * @param table - Table name * @returns Number of rows in the table * * @example * ```typescript * import { countRows } from '@kysera/testing'; * * const initialCount = await countRows(db, 'users'); * await createUser(db, userData); * const newCount = await countRows(db, 'users'); * * expect(newCount).toBe(initialCount + 1); * ``` */ declare function countRows(db: Kysely, table: string): Promise; /** * Assert that a row exists in a table. * * @param db - Kysely database instance * @param table - Table name * @param where - Conditions to match * @returns The found row * @throws {Error} If no matching row is found * * @example * ```typescript * import { assertRowExists } from '@kysera/testing'; * * const user = await assertRowExists(db, 'users', { * email: 'test@example.com', * }); * * expect(user.name).toBe('Test User'); * ``` */ declare function assertRowExists(db: Kysely, table: string, where: Record): Promise; /** * Assert that no row exists matching the conditions. * * @param db - Kysely database instance * @param table - Table name * @param where - Conditions to match * @throws {Error} If a matching row is found * * @example * ```typescript * import { assertRowNotExists } from '@kysera/testing'; * * await deleteUser(db, userId); * * await assertRowNotExists(db, 'users', { id: userId }); * ``` */ declare function assertRowNotExists(db: Kysely, table: string, where: Record): Promise; /** * Smart test-database detection for multi-dialect test suites. * * Multi-db suites historically ran a dialect only when its `TEST_POSTGRES` / * `TEST_MYSQL` / `TEST_MSSQL` env var was the literal string `'true'`. This * module keeps that contract but adds a TCP probe fallback so suites light up * automatically when the docker test stack is running: * * 1. `TEST_='true'` → available, even if the probe would fail * (`source: 'env-forced-on'`). CI service containers may still be warming * up when the suite is collected; an explicit opt-in must stay an opt-in. * 2. `TEST_` set to anything else → unavailable * (`source: 'env-forced-off'`). This matches the historical `=== 'true'` * check, where any other value kept the dialect off. * 3. Unset (or empty) → TCP probe of the dialect's host/port * (`source: 'probe'`). Host and port come from `POSTGRES_HOST` / * `POSTGRES_PORT` (with legacy `DB_PORT` honored for postgres), * `MYSQL_HOST`/`MYSQL_PORT`, `MSSQL_HOST`/`MSSQL_PORT`, defaulting to * `localhost:5432` / `localhost:3306` / `localhost:1433`. Suites whose * stack lives elsewhere (e.g. @kysera/rls on 5433/3307) pass overrides; * explicitly-set env vars always win over overrides. * * Results are cached per process (keyed by dialect + endpoint), and the first * `resolveTestDatabases()` call per process writes a one-line diagnostic to * stderr, e.g. `multi-db: postgres ✓(probe@localhost:5432) mysql ✗(env)`. * * The probe is deliberately more than a bare `connect()`: docker's userland * proxy accepts TCP connections on published ports even while the container's * service is still booting (and then resets them at once). After a successful * connect the probe therefore lingers briefly — an immediate close/error means * "port forwarded but nothing behind it" and counts as unavailable. * * Because probing auto-enables real-database suites under plain `pnpm test` * (which turbo runs with package-level parallelism), suites that talk to the * shared docker databases must serialize themselves: they drop and recreate * the same tables in the same `kysera_test` database. `acquireMultiDbLock()` * provides the cross-process mutex — acquire it in a file-level `beforeAll` * (generous timeout) and release it in `afterAll`. * * @module * * @example Gating a suite * ```typescript * const dbs = await resolveTestDatabases() * * let releaseLock: MultiDbLockRelease | undefined * beforeAll(async () => { * if (dbs.postgres.available) releaseLock = await acquireMultiDbLock() * }, 660_000) * afterAll(() => { releaseLock?.() }) * * describe.skipIf(!dbs.postgres.available)( * `pg integration (${explainAvailability(dbs.postgres)})`, * () => { ... } * ) * ``` */ /** Server dialects the docker test stack can provide (sqlite needs no detection). */ type TestDatabaseDialect = 'postgres' | 'mysql' | 'mssql'; /** Resolved availability of one test database. */ interface TestDbAvailability { readonly dialect: TestDatabaseDialect; readonly available: boolean; /** How the decision was made — explicit env always wins over probing. */ readonly source: 'env-forced-on' | 'env-forced-off' | 'probe'; /** Endpoint the decision applies to (what a probe used or would have used). */ readonly host: string; readonly port: number; } /** Per-dialect overrides for {@link detectTestDatabase}. Env vars still win. */ interface DetectOptions { /** Probe host when the dialect's `*_HOST` env var is unset. Default `localhost`. */ host?: string; /** Probe port when the dialect's `*_PORT` env var is unset. Defaults 5432/3306/1433. */ port?: number; /** TCP probe budget in milliseconds. Default 300. */ timeoutMs?: number; } /** Availability of all three server dialects, as returned by {@link resolveTestDatabases}. */ type TestDatabaseMatrix = Record; /** * Check whether a TCP endpoint accepts connections. * * Resolves `true` when the connection is established and survives a short * linger window (or the server sends data first, as MySQL does). Resolves * `false` on connection error, on timeout, or when the peer closes the * connection immediately after accepting it (docker proxy with a dead * backend). Never rejects. */ declare function probeTcp(host: string, port: number, timeoutMs?: number): Promise; /** * Decide whether a test database is available for `dialect`. * * Resolution order: explicit `TEST_` env var (both ways — see module * docs), else a TCP probe of the endpoint from env vars / `options` / * defaults. Results are cached per process; concurrent calls for the same * endpoint share one probe. */ declare function detectTestDatabase(dialect: TestDatabaseDialect, options?: DetectOptions): Promise; /** * Resolve availability of all three server dialects in parallel. * * The first call per process writes a one-line summary to stderr so skipped * suites are explainable from the test log alone. */ declare function resolveTestDatabases(options?: Partial>): Promise; /** * Human-readable one-liner for skip messages and suite titles, e.g. * `postgres not reachable at localhost:5432 (start docker or set TEST_POSTGRES=true)`. */ declare function explainAvailability(availability: TestDbAvailability): string; /** * Clear cached detection results and the diagnostic-printed marker. * * Detection reads env vars at call time but caches aggressively; call this * after mutating `TEST_*`/host/port env vars in tests. Not needed in suites. */ declare function resetDetectionCache(): void; /** Options for {@link acquireMultiDbLock}. Defaults suit the shared docker stack. */ interface MultiDbLockOptions { /** Lock identity; suites sharing a database must share a name. Default `'default'`. */ name?: string; /** How long to wait for the lock before throwing. Default 600 000 ms. */ timeoutMs?: number; /** Poll interval while waiting. Default 250 ms. */ pollIntervalMs?: number; /** * Age after which a lock without a readable owner pid is considered * abandoned (owner crashed between creating the lock and recording its * pid). Locks whose recorded owner process is dead are reclaimed * immediately regardless of age. Default 30 000 ms. */ staleMs?: number; } /** Releases a held lock. Idempotent. */ type MultiDbLockRelease = () => void; /** * Serialize suites that use the shared multi-db docker databases. * * The suites drop and recreate the same tables in the same database, so any * two of them running concurrently — parallel vitest files in one package, or * parallel packages under `turbo run test` — corrupt each other. This is a * file-based mutex in the OS temp directory: acquire it in a file-level * `beforeAll` (with a generous hook timeout — holders may legitimately run * for minutes) and release it in `afterAll`. Locks left behind by crashed * processes are detected via pid liveness and reclaimed. * * Skip it when only sqlite is in play: sqlite databases are per-process. */ declare function acquireMultiDbLock(options?: MultiDbLockOptions): Promise; /** * Plugin Testing Utilities * * Provides utilities for testing Kysera plugins in isolation * and integration scenarios. * * @module */ /** * Recorded operation from mock plugin */ interface RecordedOperation { /** The operation type */ operation: QueryBuilderContext['operation']; /** The table being operated on */ table: string; /** Timestamp when the operation was recorded */ timestamp: Date; /** Additional metadata */ metadata: Record; } /** * Plugin test result for assertions */ interface PluginTestResult { /** Whether the plugin intercepted the operation */ intercepted: boolean; /** Whether the query builder was modified */ modified: boolean; /** Error thrown by the plugin (if any) */ error?: Error; } /** * Plugin behavior assertion options */ interface PluginAssertionOptions { /** Expected operation to be intercepted */ expectedOperation?: QueryBuilderContext['operation']; /** Expected table to be affected */ expectedTable?: string; /** Whether the plugin should modify the query */ shouldModifyQuery?: boolean; } /** * Creates a mock plugin for testing plugin interactions. * * Useful for testing how plugins compose with each other * and verifying plugin execution order. * * @param name - Name of the mock plugin * @param options - Configuration options * @returns A mock plugin that records all operations * * @example * ```typescript * const mockPlugin = createMockPlugin('test-plugin', { * onIntercept: (qb, ctx) => { * console.log(`Intercepted ${ctx.operation} on ${ctx.table}`); * return qb; // Return unmodified * } * }); * * const executor = await createExecutor(db, [mockPlugin, softDeletePlugin()]); * * // Run some queries * await executor.selectFrom('users').selectAll().execute(); * * // Check recorded operations * expect(mockPlugin.operations).toHaveLength(1); * expect(mockPlugin.operations[0].operation).toBe('select'); * ``` */ declare function createMockPlugin(name: string, options?: { onIntercept?: (qb: QB, ctx: QueryBuilderContext) => QB; priority?: number; }): Plugin & { operations: RecordedOperation[]; reset: () => void; }; /** * Creates a spy wrapper for an existing plugin. * * Wraps a plugin to record all operations while preserving * the original plugin behavior. * * @param plugin - The plugin to spy on * @returns A wrapped plugin with spy capabilities * * @example * ```typescript * const spiedPlugin = spyOnPlugin(softDeletePlugin()); * * const executor = await createExecutor(db, [spiedPlugin]); * * await executor.deleteFrom('users').where('id', '=', 1).execute(); * * // Verify the plugin was called * expect(spiedPlugin.calls).toHaveLength(1); * expect(spiedPlugin.calls[0].operation).toBe('delete'); * ``` */ declare function spyOnPlugin(plugin: Plugin): Plugin & { calls: RecordedOperation[]; reset: () => void; }; /** * Asserts that a plugin behaves as expected for a given operation. * * @param plugin - The plugin to test * @param mockQb - A mock query builder object * @param context - The query builder context * @param assertions - Expected behavior assertions * @returns Test result with details * * @example * ```typescript * const plugin = softDeletePlugin({ deletedAtColumn: 'deleted_at' }); * * const result = await assertPluginBehavior( * plugin, * { where: () => mockQb }, // Mock query builder * { operation: 'select', table: 'users', metadata: {} }, * { shouldModifyQuery: true } * ); * * expect(result.modified).toBe(true); * ``` */ declare function assertPluginBehavior(plugin: Plugin, mockQb: object, context: QueryBuilderContext, assertions?: PluginAssertionOptions): PluginTestResult; /** * Creates an in-memory SQLite database for plugin testing. * * Uses SQLite in-memory mode for fast, isolated plugin tests. * * @param schema - SQL schema to create tables * @returns A Kysely instance with the schema applied * * @example * ```typescript * const db = await createInMemoryDatabase(` * CREATE TABLE users ( * id INTEGER PRIMARY KEY, * email TEXT NOT NULL, * deleted_at TEXT * ) * `); * * const executor = await createExecutor(db, [softDeletePlugin()]); * * // Run tests against in-memory database * await executor.insertInto('users').values({ email: 'test@example.com' }).execute(); * ``` */ declare function createInMemoryDatabase(schema: string): Promise>; /** * Creates a test harness for plugin integration testing. * * Provides a structured way to set up, execute, and verify * plugin behavior in integration tests. * * @param options - Test harness configuration * @returns A test harness with setup, execute, and verify methods * * @example * ```typescript * const harness = createPluginTestHarness({ * plugins: [softDeletePlugin(), timestampsPlugin()], * schema: ` * CREATE TABLE posts ( * id INTEGER PRIMARY KEY, * title TEXT, * deleted_at TEXT, * created_at TEXT, * updated_at TEXT * ) * ` * }); * * await harness.setup(); * * const result = await harness.execute(async (executor) => { * return executor.insertInto('posts') * .values({ title: 'Test Post' }) * .returningAll() * .executeTakeFirst(); * }); * * harness.verify(result, (r) => { * expect(r.created_at).toBeDefined(); * expect(r.updated_at).toBeDefined(); * }); * * await harness.teardown(); * ``` */ declare function createPluginTestHarness(options: { plugins: Plugin[]; schema: string; seedData?: (executor: Kysely) => Promise; }): { setup: () => Promise; execute: (fn: (executor: Kysely) => Promise) => Promise; verify: (result: T, assertions: (result: T) => void) => void; teardown: () => Promise; getDb: () => Kysely; }; /** * Mock executor type - useful for mocking in unit tests */ interface MockOperationContext { operation: QueryBuilderContext['operation']; table: string; executor: Kysely | Transaction; } /** * Options for creating a test executor */ interface CreateTestExecutorOptions { /** Database to wrap (e.g. from {@link createInMemoryDatabase}) */ db: Kysely; /** Plugins to apply to the executor */ plugins: Plugin[]; /** Record every intercepted operation on the returned `operations` array */ debug?: boolean; } /** * Result of {@link createTestExecutor} */ interface TestExecutorResult { /** Plugin-aware executor — queries through it are intercepted by plugins */ executor: KyseraExecutor; /** The underlying database passed in — queries through it bypass plugins */ db: Kysely; /** * Operations recorded by the debug recorder. * Empty unless `debug: true` was set. */ operations: RecordedOperation[]; /** Destroys the underlying database connection */ cleanup: () => Promise; } /** * Creates a plugin-aware executor over an existing test database. * * Thin one-shot wrapper around `createExecutor` from `@kysera/executor` for * tests that don't need the full setup/teardown lifecycle of * {@link createPluginTestHarness}. With `debug: true`, a low-priority * recorder plugin is appended that captures every intercepted operation on * the returned `operations` array (it runs after all other plugins, so it * observes the final interception order). * * @param options - Database, plugins, and debug flag * @returns Executor, the raw database, recorded operations, and cleanup * * @example * ```typescript * const db = await createInMemoryDatabase(` * CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, deleted_at TEXT) * `); * * const { executor, operations, cleanup } = await createTestExecutor({ * db, * plugins: [softDeletePlugin()], * debug: true * }); * * const users = await executor.selectFrom('users').selectAll().execute(); * expect(operations[0]?.operation).toBe('select'); * * await cleanup(); * ``` */ declare function createTestExecutor(options: CreateTestExecutorOptions): Promise>; export { type CleanupOptions, type CleanupStrategy, type CreateTestExecutorOptions, type DetectOptions, type FactoryDefaults, type FactoryFunction, type IsolationLevel, type MockOperationContext, type MultiDbLockOptions, type MultiDbLockRelease, type PluginAssertionOptions, type PluginTestResult, type RecordedOperation, type SeedFunction, type TestDatabaseDialect, type TestDatabaseMatrix, type TestDbAvailability, type TestExecutorResult, type WaitForOptions, acquireMultiDbLock, assertPluginBehavior, assertRowExists, assertRowNotExists, cleanDatabase, composeSeeders, countRows, createFactory, createInMemoryDatabase, createMany, createMockPlugin, createPluginTestHarness, createSequenceFactory, createTestExecutor, detectTestDatabase, explainAvailability, probeTcp, resetDetectionCache, resolveTestDatabases, seedDatabase, snapshotTable, spyOnPlugin, testInTransaction, testWithIsolation, testWithSavepoints, waitFor };