Last Updated: 3/20/2026
Plugins
Plugins let you transform queries before they’re executed and transform results after they’re returned. The KyselyPlugin interface (defined in src/plugin/kysely-plugin.ts) has two methods: transformQuery and transformResult.
The Plugin Interface
A plugin implements this interface:
interface KyselyPlugin {
transformQuery(args: PluginTransformQueryArgs): RootOperationNode
transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>
}transformQueryis called for each query before it’s compiled and executed. You receive the query’s operation node tree and can return a modified tree.transformResultis called for each query after it’s executed. You receive the result and can return a modified result.
Both methods receive a queryId that you can use to pass data between them. Use a WeakMap to avoid memory leaks (see the example in src/plugin/kysely-plugin.ts).
Installing Plugins
Pass plugins to the plugins array when creating a Kysely instance:
import { Kysely, CamelCasePlugin } from 'kysely'
const db = new Kysely<Database>({
dialect: myDialect,
plugins: [new CamelCasePlugin()]
})You can also add plugins to an existing instance using withPlugin():
const dbWithPlugin = db.withPlugin(new CamelCasePlugin())This returns a new Kysely instance with the plugin installed. The original instance is unchanged (immutability).
Built-In Plugins
Kysely ships with several useful plugins:
CamelCasePlugin
Converts snake_case identifiers in the database to camelCase in JavaScript. When using this plugin, define everything in camelCase in your TypeScript code—table names, columns, schemas, everything. Kysely works as if the database was defined in camelCase.
import { CamelCasePlugin } from 'kysely'
interface CamelCasedDatabase {
personTable: {
firstName: string
lastName: string
}
}
const db = new Kysely<CamelCasedDatabase>({
dialect: myDialect,
plugins: [new CamelCasePlugin()]
})
const person = await db
.selectFrom('personTable')
.where('firstName', '=', 'Arnold')
.select(['firstName', 'lastName'])
.executeTakeFirst()The generated SQL (PostgreSQL):
select "first_name", "last_name" from "person_table" where "first_name" = $1The plugin has several options (see src/plugin/camel-case/camel-case-plugin.ts):
upperCase— Iftrue, converts to UPPER_SNAKE_CASE instead of snake_case.underscoreBeforeDigits— Iftrue, adds an underscore before digits (foo12Bar→foo_12_bar).underscoreBetweenUppercaseLetters— Iftrue, adds underscores between consecutive uppercase letters (fooBAR→foo_b_a_r).maintainNestedObjectKeys— Iftrue, nested object keys are not converted.
You can also extend the plugin and override the snakeCase and camelCase methods for custom conversion logic.
DeduplicateJoinsPlugin
Removes duplicate join clauses from queries. Useful when composing queries from reusable helpers that might add the same join multiple times.
import { DeduplicateJoinsPlugin } from 'kysely'
const db = new Kysely<Database>({
dialect: myDialect,
plugins: [new DeduplicateJoinsPlugin()]
})See src/plugin/deduplicate-joins/deduplicate-joins-plugin.ts for implementation details.
HandleEmptyInListsPlugin
Handles empty arrays in IN and NOT IN clauses. By default, an empty array in an IN clause would produce invalid SQL. This plugin replaces empty IN lists with false and empty NOT IN lists with true.
import { HandleEmptyInListsPlugin } from 'kysely'
const db = new Kysely<Database>({
dialect: myDialect,
plugins: [new HandleEmptyInListsPlugin()]
})
const ids: number[] = [] // Empty array
// Without the plugin, this would produce invalid SQL
const result = await db
.selectFrom('person')
.where('id', 'in', ids)
.selectAll()
.execute()The generated SQL (PostgreSQL):
select * from "person" where falseSee src/plugin/handle-empty-in-lists/handle-empty-in-lists-plugin.ts for implementation details.
ParseJSONResultsPlugin
Parses JSON strings in result rows into JavaScript objects. Some database drivers return JSON columns as strings instead of parsing them automatically. This plugin walks the result rows and parses any string that looks like JSON.
import { ParseJSONResultsPlugin } from 'kysely'
const db = new Kysely<Database>({
dialect: myDialect,
plugins: [new ParseJSONResultsPlugin()]
})See src/plugin/parse-json-results/parse-json-results-plugin.ts for implementation details.
WithSchemaPlugin
Adds a schema prefix to all table references in a query. Useful when working with databases that use schemas (like PostgreSQL).
import { WithSchemaPlugin } from 'kysely'
const dbWithSchema = db.withPlugin(new WithSchemaPlugin('my_schema'))
const result = await dbWithSchema
.selectFrom('person')
.selectAll()
.execute()The generated SQL (PostgreSQL):
select * from "my_schema"."person"You can also use the withSchema() method on Kysely, which is a shortcut for installing this plugin:
const result = await db
.withSchema('my_schema')
.selectFrom('person')
.selectAll()
.execute()See src/plugin/with-schema/with-schema-plugin.ts for implementation details.
Writing a Custom Plugin
To write a custom plugin, implement the KyselyPlugin interface. Here’s a simple example that logs all queries:
import type {
KyselyPlugin,
PluginTransformQueryArgs,
PluginTransformResultArgs,
QueryResult,
RootOperationNode,
UnknownRow
} from 'kysely'
class LoggingPlugin implements KyselyPlugin {
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
console.log('Query node:', args.node)
return args.node
}
async transformResult(
args: PluginTransformResultArgs
): Promise<QueryResult<UnknownRow>> {
console.log('Result:', args.result)
return args.result
}
}For more complex transformations, you’ll want to use an OperationNodeTransformer to walk and modify the operation node tree. Study the built-in plugins (especially CamelCasePlugin and its SnakeCaseTransformer in src/plugin/camel-case/camel-case-transformer.ts) for examples.
Passing Data Between Methods
If you need to pass data from transformQuery to transformResult, use a WeakMap with args.queryId as the key:
class MyPlugin implements KyselyPlugin {
#data = new WeakMap<any, { startTime: number }>()
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
this.#data.set(args.queryId, { startTime: Date.now() })
return args.node
}
async transformResult(
args: PluginTransformResultArgs
): Promise<QueryResult<UnknownRow>> {
const data = this.#data.get(args.queryId)
if (data) {
const duration = Date.now() - data.startTime
console.log(`Query took ${duration}ms`)
}
return args.result
}
}Use a WeakMap instead of a Map because transformQuery is not always matched by a call to transformResult (e.g., if the query fails to compile). A WeakMap allows the garbage collector to clean up orphaned entries.
Plugin Order
Plugins are executed in the order they’re installed. The first plugin’s transformQuery is called first, then the second plugin’s, and so on. For transformResult, the order is reversed: the last plugin’s transformResult is called first.
const db = new Kysely<Database>({
dialect: myDialect,
plugins: [pluginA, pluginB, pluginC]
})
// transformQuery order: A -> B -> C
// transformResult order: C -> B -> AWhat’s Next
- Dialects — Learn how dialects work and how to configure them.
- Logging — Use the
logoption to log queries and errors without writing a plugin. - Reusable Helpers — Build reusable query helpers that work with plugins.