Troubleshooting & FAQ
This guide covers common issues you might encounter when using Kysera and provides solutions and workarounds.
Troubleshooting
Common Errors
"OFFSET/FETCH NEXT is not supported without ORDER BY" (MSSQL)
Cause: Microsoft SQL Server requires an ORDER BY clause when using offset pagination.
Solution: Always include .orderBy() before calling pagination methods:
// ❌ This will fail on MSSQL
const result = await db
.selectFrom('users')
.selectAll()
.limit(10)
.offset(20)
.execute()
// ✅ This works
const result = await db
.selectFrom('users')
.selectAll()
.orderBy('id', 'asc')
.limit(10)
.offset(20)
.execute()
When using pagination helpers:
import { paginate } from '@kysera/core'
// Always provide orderBy
const result = await paginate(
db.selectFrom('users').selectAll().orderBy('created_at', 'desc'),
{ page: 1, limit: 20 }
)
"Invalid pagination cursor" errors
Cause: The cursor is from a different query or has been corrupted/tampered with.
Solution: Ensure the cursor matches the columns specified in orderBy:
// paginateCursor applies the ordering itself from `orderBy`
const firstPage = await paginateCursor(
db.selectFrom('posts').selectAll(),
{ limit: 20, orderBy: [{ column: 'created_at', direction: 'desc' }] }
)
// Use the cursor from the response
const nextPage = await paginateCursor(
db.selectFrom('posts').selectAll(),
{
limit: 20,
orderBy: [{ column: 'created_at', direction: 'desc' }],
cursor: firstPage.pagination.nextCursor // Must be from the same query
}
)
Important: Cursors are query-specific and cannot be reused across different queries or column orders.
"Cannot find module '@kysera/executor'"
Cause: Missing dependency. The executor package is required for plugin functionality.
Solution: Install the executor package:
pnpm add @kysera/executor
Ensure all packages are updated:
pnpm update @kysera/core @kysera/executor @kysera/repository @kysera/dal
Plugin not intercepting queries
Cause: Using raw Kysely instance instead of the executor.
Solution: Create and use an executor with your plugins:
import { createExecutor } from '@kysera/executor'
import { softDeletePlugin } from '@kysera/soft-delete'
// ❌ Plugins won't work with raw Kysely
const db = new Kysely<Database>({ dialect })
const users = await db.selectFrom('users').selectAll().execute()
// ✅ Use executor for plugin interception
const executor = await createExecutor(db, [softDeletePlugin()])
const users = await executor.selectFrom('users').selectAll().execute()
For Repository pattern:
import { createORM } from '@kysera/repository'
// ORM creates executor internally
const orm = await createORM(db, [softDeletePlugin()])
For DAL pattern:
import { createContext } from '@kysera/dal'
const executor = await createExecutor(db, [softDeletePlugin()])
const ctx = createContext(executor) // Pass executor, not db
Type errors with dialect-specific code
Cause: Boolean and data type handling differs between databases.
Solution: Use dialect-specific values or utilities:
// SQLite uses 1/0 for booleans
const sqliteQuery = db
.selectFrom('users')
.selectAll()
.where('is_active', '=', 1) // Not true
// PostgreSQL/MySQL use true/false
const pgQuery = db
.selectFrom('users')
.selectAll()
.where('is_active', '=', true)
// Use dialect detection for cross-database code
import { detectDialect } from '@kysera/core'
const dialect = detectDialect(db)
const activeValue = dialect === 'sqlite' ? 1 : true
const query = db
.selectFrom('users')
.selectAll()
.where('is_active', '=', activeValue)
Connection timeout with MSSQL
Cause: Database doesn't exist, incorrect connection string, or network issues.
Solution: Verify your Tedious configuration and database exists:
import { Kysely, MssqlDialect } from 'kysely'
import { Tedious } from 'tedious'
import { TediousPool } from 'tarn-tedious'
const db = new Kysely<Database>({
dialect: new MssqlDialect({
tarn: {
...TediousPool,
options: {
min: 0,
max: 10,
},
},
tedious: {
...Tedious,
connectionFactory: () => new Tedious.Connection({
server: 'localhost',
authentication: {
type: 'default',
options: {
userName: 'sa',
password: 'YourStrong@Passw0rd'
}
},
options: {
port: 1433,
database: 'kysera_test', // Ensure this database exists
trustServerCertificate: true,
requestTimeout: 30000, // Increase timeout
connectTimeout: 30000
}
})
}
})
})
Create the database if it doesn't exist:
CREATE DATABASE kysera_test;
Performance Issues
Slow offset pagination on large datasets
Problem: Offset pagination becomes slower as the offset increases because the database must scan and skip rows.
Solution: Use cursor-based pagination for large datasets:
import { paginateCursor } from '@kysera/core'
// ❌ Slow on large offsets
const result = await paginate(
db.selectFrom('posts').selectAll().orderBy('id', 'asc'),
{ page: 1000, limit: 20 } // offset = 19,980
)
// ✅ Fast and consistent performance
const result = await paginateCursor(
db.selectFrom('posts').selectAll(),
{ limit: 20, cursor: lastCursor, orderBy: [{ column: 'id', direction: 'asc' }] }
)
Benchmark: Cursor pagination maintains ~1-2ms query time regardless of position, while offset pagination can degrade to 100ms+ on large offsets.
Memory issues with large result sets
Problem: Loading thousands of rows into memory causes out-of-memory errors.
Solution: Use Kysely's native streaming or limit page sizes:
// ❌ Loads all rows into memory
const allUsers = await db.selectFrom('users').selectAll().execute()
// ✅ Use Kysely's native stream() method
const stream = db.selectFrom('users').selectAll().stream()
for await (const user of stream) {
await processUser(user)
}
// ✅ Or paginate with reasonable limits
import { paginate } from '@kysera/core'
const result = await paginate(
db.selectFrom('users').selectAll().orderBy('id', 'asc'),
{ page: 1, limit: 100 } // Process in batches
)
Note: Kysera does not provide a separate executeStream utility. Use Kysely's built-in .stream() method directly for streaming large datasets.
Slow queries with plugins
Problem: Multiple plugins chained together slow down queries.
Solution: Plugin execution order is fixed by priority (higher runs first: RLS 1000 → soft-delete 500 → timestamps 100 → audit 50) — the order of the array you pass does not matter. Optimize by:
- Using database indexes on plugin-filtered columns
- Profiling with
withDebugfrom@kysera/debug
import { createExecutor } from '@kysera/executor'
import { softDeletePlugin } from '@kysera/soft-delete'
import { rlsPlugin } from '@kysera/rls'
import { withDebug } from '@kysera/debug'
// Array order is irrelevant — execution order comes from plugin priority
const executor = await createExecutor(db, [
softDeletePlugin(),
rlsPlugin({ schema: rlsSchema })
])
// withDebug wraps a Kysely instance for profiling (it is not an executor plugin)
const debugDb = withDebug(db, { logQuery: true, slowQueryThreshold: 100 })
// Ensure indexes exist on filtered columns
// CREATE INDEX idx_users_tenant_id ON users(tenant_id);
// CREATE INDEX idx_users_deleted_at ON users(deleted_at);
FAQ
General Questions
Q: Can I use Kysera without the executor?
A: Yes, but plugins won't intercept queries. The core utilities (pagination, error parsing, logging) work with raw Kysely:
import { paginate, parseDatabaseError } from '@kysera/core'
// Works without executor
const result = await paginate(
db.selectFrom('users').selectAll().orderBy('id', 'asc'),
{ page: 1, limit: 20 }
)
try {
await db.insertInto('users').values(data).execute()
} catch (error) {
const dbError = parseDatabaseError(error, 'postgres')
// Handle error
}
For full plugin functionality (soft-delete, RLS, audit, etc.), use createExecutor().
Q: Which databases are supported?
A: Kysera supports all databases that Kysely supports:
| Database | Tested | Notes |
|---|---|---|
| PostgreSQL | ✅ | Primary development target |
| MySQL | ✅ | Full support |
| SQLite | ✅ | Boolean values use 1/0 |
| MSSQL (SQL Server) | ✅ | Requires ORDER BY for OFFSET |
| PlanetScale | ⚠️ | MySQL-compatible |
| CockroachDB | ⚠️ | PostgreSQL-compatible |
| Oracle | ❓ | Untested, should work via Kysely |
Q: How do I handle different boolean types across databases?
A: Use dialect-specific values or create abstraction utilities:
import { detectDialect } from '@kysera/core'
// Option 1: Detect dialect at runtime
const dialect = detectDialect(db)
const boolValue = (val: boolean) =>
dialect === 'sqlite' ? (val ? 1 : 0) : val
const query = db
.selectFrom('users')
.selectAll()
.where('is_active', '=', boolValue(true))
// Option 2: Use type-safe schema definitions
interface SqliteDatabase {
users: {
id: number
is_active: 0 | 1 // SQLite booleans
}
}
interface PostgresDatabase {
users: {
id: number
is_active: boolean // PostgreSQL booleans
}
}
// Option 3: Create helper functions
const isActive = (dialect: string) =>
dialect === 'sqlite'
? sql<0 | 1>`is_active = 1`
: sql<boolean>`is_active = true`
Q: Can I use multiple plugins together?
A: Yes, pass an array to createExecutor():
import { createExecutor } from '@kysera/executor'
import { softDeletePlugin } from '@kysera/soft-delete'
import { rlsPlugin } from '@kysera/rls'
import { auditPlugin } from '@kysera/audit'
import { timestampsPlugin } from '@kysera/timestamps'
const executor = await createExecutor(db, [
timestampsPlugin(), // Repository-only (extendRepository)
softDeletePlugin(), // Query interceptor: filters soft-deleted rows
rlsPlugin({ schema: rlsSchema }), // Query interceptor: row-level security
auditPlugin({ auditTable: 'audit_log' }) // Repository-only (extendRepository)
])
// Query-interceptor plugins (soft-delete, RLS) apply to executor/DAL queries
const user = await executor
.selectFrom('users')
.selectAll()
.where('id', '=', 1)
.executeTakeFirst()
Note that timestampsPlugin and auditPlugin work through extendRepository only — they do nothing on direct executor or DAL queries. To get automatic timestamps and audit logging, use createORM() and repositories created with orm.createRepository().
Array order does not matter: execution order is resolved from each plugin's priority (RLS 1000 → soft-delete 500 → timestamps 100 → audit 50).
Q: How do I run tests against multiple databases?
A: Use environment variables to enable specific databases:
# Run all database tests
TEST_POSTGRES=true TEST_MYSQL=true TEST_MSSQL=true pnpm test
# Run only PostgreSQL tests
TEST_POSTGRES=true pnpm test
# Use Docker for test databases
pnpm docker:up # Start PostgreSQL/MySQL containers
pnpm test:multi-db # Run multi-database tests
pnpm docker:down # Stop containers
In your test files:
import { describe, it, beforeAll } from 'vitest'
const shouldTestPostgres = process.env.TEST_POSTGRES === 'true'
const shouldTestMysql = process.env.TEST_MYSQL === 'true'
describe.skipIf(!shouldTestPostgres)('PostgreSQL tests', () => {
it('should work with PostgreSQL', async () => {
// Test code
})
})
describe.skipIf(!shouldTestMysql)('MySQL tests', () => {
it('should work with MySQL', async () => {
// Test code
})
})
Q: What's the maximum page size?
A: The maximum is 10,000 items per page. Higher limits are clamped internally — the cap is module-private, so it cannot be imported or configured:
import { paginate } from '@kysera/core'
// Maximum is 10,000
const result = await paginate(
db.selectFrom('users').selectAll().orderBy('id', 'asc'),
{ page: 1, limit: 15000 } // Clamped to 10,000
)
console.log(result.pagination.limit) // 10000
Why this limit?
- Prevents accidental memory issues
- Protects database from expensive queries
- Encourages cursor pagination for large datasets
To override (not recommended):
// Custom implementation without the cap
const customPaginate = async (query, { page, limit }) => {
const offset = (page - 1) * limit
return query.limit(limit).offset(offset).execute()
}
Repository & DAL Questions
Q: Should I use Repository or DAL?
A: Use both together (CQRS-lite pattern):
import type { Transaction } from 'kysely'
const orm = await createORM(db, [softDeletePlugin()])
const userRepo = orm.createRepository(createUserRepository)
await orm.transaction(async ctx => {
// Repository for writes — rebind to the transaction (repositories stay
// bound to the base executor and would otherwise run OUTSIDE it)
const txUserRepo = userRepo.withTransaction(ctx.db as Transaction<Database>)
const user = await txUserRepo.create({
name: 'Alice',
email: 'alice@example.com'
})
// DAL for complex reads (flexible queries)
const stats = await getDashboardStats(ctx, user.id)
})
Use Repository when:
- CRUD operations
- Zod validation needed
- Type-safe mutations
- Working with single entities
Use DAL when:
- Complex joins
- Aggregations
- Custom queries
- Read-only operations
Q: How do I share plugins between Repository and DAL?
A: The executor enables plugin sharing:
import { createExecutor } from '@kysera/executor'
import { createORM } from '@kysera/repository'
import { createContext } from '@kysera/dal'
// Create executor once
const executor = await createExecutor(db, [
softDeletePlugin(),
rlsPlugin({ schema: rlsSchema })
])
// Both Repository and DAL use the same executor
const orm = await createORM(executor, [])
const dalCtx = createContext(executor)
// Plugins apply to both
await orm.transaction(async ctx => {
const userRepo = orm.createRepository(createUserRepository)
const user = await userRepo.findById(1) // Soft-delete filter applied
const stats = await getStats(ctx, user.id) // Same plugins
})
Q: Can I disable plugins for specific queries?
A: Yes — bypass all plugins with the raw Kysely instance, or opt out of a single plugin's behavior with per-query metadata:
import { createExecutor } from '@kysera/executor'
const db = new Kysely<Database>({ dialect })
const executor = await createExecutor(db, [softDeletePlugin()])
// Plugins applied
const activeUsers = await executor
.selectFrom('users')
.selectAll()
.execute() // Excludes soft-deleted
// Bypass plugins
const allUsers = await db
.selectFrom('users')
.selectAll()
.execute() // Includes soft-deleted
Alternatively, use per-query plugin metadata — soft-delete skips its filter when includeDeleted is set:
import { withPluginMetadata } from '@kysera/executor'
// Soft-delete filter disabled for queries made through this wrapper;
// every other plugin stays active
const allUsers = await withPluginMetadata(executor, { includeDeleted: true })
.selectFrom('users')
.selectAll()
.execute()
Plugin Questions
Q: How do I create custom plugins?
A: Implement the Plugin interface from @kysera/executor:
import type { Plugin, QueryBuilderContext } from '@kysera/executor'
export function customPlugin(): Plugin {
return {
name: 'custom-plugin',
version: '1.0.0',
priority: 0, // Higher runs first (RLS uses 1000, soft-delete 500)
// Modify query builders before execution
interceptQuery<QB>(qb: QB, context: QueryBuilderContext): QB {
if (context.operation === 'select' && context.table === 'users') {
// Add WHERE clauses, etc. (cast qb to the specific builder type)
}
return qb
}
// Also available:
// extendRepository?(repo) — add methods to repositories (Repository pattern only)
// onInit?(db) / onDestroy?() — lifecycle hooks
}
}
// Use it
const executor = await createExecutor(db, [customPlugin()])
Kysera plugins intercept query builders (interceptQuery), not query ASTs. A transformQuery hook belongs to Kysely's own KyselyPlugin API — if you need AST-level transforms, write a Kysely plugin and attach it with db.withPlugin().
See existing plugins for examples:
@kysera/soft-delete- Simple WHERE clause injection@kysera/rls- Tenant-based filtering@kysera/audit- Repository extension methods (audit logging + restore)
Q: Do plugins work with transactions?
A: Yes, plugins are preserved in transactions:
import { withTransaction } from '@kysera/dal'
const executor = await createExecutor(db, [
softDeletePlugin(),
rlsPlugin({ schema: rlsSchema })
])
const ctx = createContext(executor)
await withTransaction(executor, async txCtx => {
// Plugins still active in transaction
const user = await getUserById(txCtx, '1')
const posts = await getUserPosts(txCtx, '1')
// Both queries have soft-delete and RLS filters
})
For Repository:
const orm = await createORM(db, [softDeletePlugin()])
const userRepo = orm.createRepository(createUserRepository)
await orm.transaction(async ctx => {
// Rebind so the write actually joins the transaction
const txUserRepo = userRepo.withTransaction(ctx.db as Transaction<Database>)
await txUserRepo.softDelete(1) // Works in transaction (rebound repo keeps plugins)
})
Additional Resources
Still having issues? Open an issue on GitHub with:
- Kysera version (
@kysera/core,@kysera/executor, etc.) - Database type and version
- Minimal reproduction code
- Error messages and stack traces