MockQueryRunner¶
This general-purpose query runner enables controlled mocking of query execution in a fully predictable environment. It is primarily used in tests and simulations, where you can inspect the queries being issued and manually specify the expected return values.
Supported databases
Tip
MockQueryRunner supports synchronous query execution. See the Synchronous query runners for more information.
Do not share connections between requests
A ts-sql-query connection object — along with the query runner instances passed to its constructor — represents a dedicated connection to the database.
Therefore, you must not share the same connection object between concurrent HTTP requests. Instead, create a new connection object for each request, along with its own query runners.
Even if the query runner internally uses a connection pool, the ts-sql-query connection still represents a single active connection, acquired from the pool. It must be treated as such and never reused across requests.
Usage Example¶
import { MockQueryRunner } from "ts-sql-query/queryRunners/MockQueryRunner";
async function main() {
const connection = new DBConnection(new MockQueryRunner(
(type, query, params, index) => {
// MockQueryRunner: verify your queries here
}
));
// Do your queries here
connection // ...
}
API Overview¶
The MockQueryRunner receives a function as argument to the constructor, this function returns the result of the query execution and receives the following arguments:
type: QueryType: type of the query to be executed. TheQueryTypeis defined as:
// Types of queries that can be intercepted and mocked
type QueryType = 'selectOneRow' | 'selectManyRows' | 'selectOneColumnOneRow' | 'selectOneColumnManyRows' |
'insert' | 'insertReturningLastInsertedId' | 'insertReturningMultipleLastInsertedId' |
'insertReturningOneRow' | 'insertReturningManyRows' | 'insertReturningOneColumnOneRow' | 'insertReturningOneColumnManyRows' |
'update' | 'updateReturningOneRow' | 'updateReturningManyRows' | 'updateReturningOneColumnOneRow' | 'updateReturningOneColumnManyRows' |
'delete' | 'deleteReturningOneRow' | 'deleteReturningManyRows' | 'deleteReturningOneColumnOneRow' | 'deleteReturningOneColumnManyRows' |
'executeProcedure' | 'executeFunction' | 'beginTransaction' | 'commit' | 'rollback' |
'executeDatabaseSchemaModification' | 'executeConnectionConfiguration'
query: string: query required to be executedparams: any[]: parameters received by the queryindex: number: this is a counter of queries executed by the connection; that means, when the first query is executed the value is 0, when the second query is executed the value is 1, etc.
Example of usage
test('my test', async () => {
const connection = new DBConnection(new MockQueryRunner((type, query, params, index) => {
switch (index) {
case 0:
expect(type).toEqual('insertReturningLastInsertedId');
expect(query).toEqual('insert into company (name) values ($1) returning id');
expect(params).toEqual([ 'ACME' ]);
// Return the result of the query execution, in this case the inserted id
return 12;
case 1:
expect(type).toEqual('selectOneRow');
expect(query).toEqual('select id as id, name as name from company where id = $1');
expect(params).toEqual([ 12 ]);
// Return the result of the query execution, in this case the requested row
return { id: 12, name: 'ACME' };
}
throw new Error('Unexpected query in the test case');
}));
const testCompanyId = await connection
.insertInto(tCompany)
.values({ name: 'ACME' })
.returningLastInsertedId()
.executeInsert();
expect(testCompanyId).toEqual(12);
let testCompany = await connection
.selectFrom(tCompany)
.where(tCompany.id.equals(testCompanyId))
.select({
id: tCompany.id,
name: tCompany.name
})
.executeSelectOne();
expect(testCompany).toEqual({ id: 12, name: 'ACME' });
});
What the runner validates¶
The value your executor returns goes through two stages before it reaches the calling code.
The first stage is a shape gate inside the runner. For each queryType, the runner checks that the value matches the rough shape a real driver would produce — a plain object for a single-row select, an array of plain objects for a many-row select, a number for affected-row counts, and so on. A non-conforming value is rejected with a TsSqlProcessingError whose reason is INVALID_MOCKED_VALUE and that carries the offending queryType and index, so the bad executor branch is easy to locate.
Past that gate the value flows through the same result-projection pipeline a real driver's response would: type adapters convert each field, mandatory columns are checked for null/undefined, and aggregated-array JSON is parsed and validated. The mock is impersonating a database response from then on, so projector-level errors fire identically to real-DB mode:
- A structurally valid row that omits a column the
select({...})projects as required surfacesMANDATORY_VALUE_NOT_RECEIVED_FROM_DATABASE. - A value whose runtime type does not match the declared column type surfaces
INVALID_VALUE_RECEIVED_FROM_DATABASE. - A non-array or unparseable JSON string for a JSON-aggregated column surfaces
INVALID_JSON_RECEIVED_FROM_DATABASE.
These are the same errors a real driver would trigger against the same payload — that is the documented invariant, not a separate mock-specific error. The fix in test code is to include every projected column in the mocked row (or, when the test only asserts the emitted SQL, to return [] or undefined so the projector has nothing to walk).
Configuration options¶
The second constructor argument accepts either a DatabaseType string (shown above) or a configuration object with the following fields:
interface MockQueryRunnerConfig {
database?: DatabaseType
promise?: PromiseProvider
isSqlError?: (error: unknown) => boolean
}
database: dialect name used to format placeholders ($1,?,:0, …). Defaults to'noopDB'. Must match the connection class you wrap the runner with.promise: alternativePromiseprovider, used by synchronous query runners (e.g.synchronous-promise). Defaults to the nativePromise.isSqlError: customizes how the connection classifies a thrown value as a SQL error. The default classifies everything as a SQL error, which causes the connection's transaction handling to wrap your thrown value inside aTsSqlQueryExecutionError. Returningfalsefor a thrown value makes the connection propagate it as-is instead — useful when test code uses a sentinel error to roll back a transaction and wants the sentinel to bubble up to the surroundingtry/catchunchanged.
class Rollback extends Error {}
const runner = new MockQueryRunner(executor, {
database: 'postgreSql',
isSqlError: (e) => !(e instanceof Rollback),
})
// inside a test:
try {
await connection.transaction(async () => {
// ...do the thing under test...
throw new Rollback() // propagates as Rollback, not as TsSqlQueryExecutionError
})
} catch (e) {
if (!(e instanceof Rollback)) throw e
}
Transaction-state guards¶
MockQueryRunner mirrors real-driver transaction semantics. The runner keeps an internal transaction-depth counter that executeBeginTransaction increments and executeCommit / executeRollback decrement, and isTransactionActive() is answered from that counter. The transaction-lifecycle runtime guards on the connection therefore engage in mock mode exactly as they do for a real driver:
commit()orrollback()outside a transaction throwsNOT_IN_TRANSACTION.- Registering
executeBeforeNextCommit/executeAfterNextCommit/executeAfterNextRollbackoutside a transaction throwsNOT_IN_TRANSACTION. - Calling
getTransactionMetadata()outside a transaction throwsNOT_IN_TRANSACTION. - A nested
transaction(...)against a runner that reportsnestedTransactionsSupported(): falsethrowsNESTED_TRANSACTION_NOT_SUPPORTED.
isMocked() continues to return true on MockQueryRunner. It is a diagnostic for external consumer code that needs to discriminate "the connection is a mock" from "the connection is real" at runtime (e.g. to skip integration-only assertions) — it no longer controls any guard.
const runner = new MockQueryRunner(executor, { database: 'postgreSql' })
const connection = new DBConnection(runner)
await connection.beginTransaction()
await connection.commit()
// The second commit hits the same NOT_IN_TRANSACTION guard the real
// driver would, because the mock's depth counter is back at zero.
await expect(connection.commit()).rejects.toThrow(/NOT_IN_TRANSACTION/)
Resetting between test cases¶
MockQueryRunner keeps an internal counter that increments on every executor call and is what gets passed as the index argument (0 for the first query of the runner's lifetime, then 1, 2, …). When you reuse a single MockQueryRunner instance across many test cases — each priming its own sequence of responses keyed by index — call runner.reset() between cases to bring the counter back to 0:
const runner = new MockQueryRunner(executor, 'postgreSql')
const connection = new DBConnection(runner)
beforeEach(() => {
runner.reset()
})
If you create a fresh MockQueryRunner per test case (and therefore a fresh connection too), reset() is unnecessary — the counter is per-instance.