Kysely Type-safe SQL Query Builder

repository·master·Indexed 12 days ago

https://github.com/kysely-org/kysely

A type-safe SQL query builder for TypeScript that provides autocompletion and compile-time safety for database queries. It is environment agnostic, running on Node.js, Deno, Bun, Cloudflare Workers, and browsers. Kysely supports multiple dialects including PostgreSQL, MySQL, MSSQL, SQLite, and PGlite, and features advanced inference for subqueries, CTEs, and joined queries.

Tokens
43.2K
Snippets
141
Records
170
Agent score
95%

What's inside Kysely

  1. What is Kysely?

    master

    Kysely (pronounced “Key-Seh-Lee”) is a type-safe and autocompletion-friendly TypeScript SQL query builder. It is inspired by Knex.js and is designed to ensure that you only refer to tables and columns that are visible to the specific part of the query you are writing.

    Key features include:

    • Type Safety: Result types only contain the selected columns with correct types and aliases.
    • Autocompletion: Full autocompletion for tables, columns, aliases, and subqueries.
    • Advanced Inference: Automatically infers column names, aliases, and types from selected subqueries, joined subqueries, with statements, and more.
    • Environment Agnostic: While primarily developed for Node.js, it runs on Deno, Bun, Cloudflare Workers, and web browsers.

    If you encounter situations that cannot be typed at compile time, Kysely provides escape hatches such as the sql template tag and DynamicModule.

  2. What is a Dialect in Kysely

    master
    A Dialect acts as the abstraction layer (the "glue") between Kysely and the underlying database engine. It handles the specifics of how queries are executed and how data is communicated with a particular database. If you need to support a database not covered by the core or community dialects, you can implement the Dialect interface to build your own.
  3. How plugins interact with the execution flow

    master

    Kysely plugins hook into two specific stages of the execution lifecycle to modify either the query structure or the resulting data:

    • Query Transformation Stage: Before the query is compiled into SQL, the QueryExecutor calls transformQuery(QueryAST) on every registered plugin. This allows plugins to modify the internal QueryAST representation.
    • Result Transformation Stage: After the database returns results but before they are returned to your application, the QueryExecutor calls transformResults(QueryResult) on every registered plugin. This allows plugins to modify or format the final data.
  4. Understand Expression<T> in Kysely

    master
    An Expression<T> is the fundamental type-safe building block for all Kysely queries. It represents any arbitrary SQL expression, such as binary operations (e.g., a + b), function calls (e.g., concat(arg1, ' ', arg2)), or subqueries. The type parameter T represents the resulting TypeScript type of the expression's output. Most Kysely methods, including select, where, having, on, orderBy, and groupBy, accept expressions as inputs.
  5. Use Common Table Expressions (CTE) to modularize queries

    master

    Common Table Expressions (CTEs) allow you to modularize complex queries by defining multiple separate queries that can be executed within a single database roundtrip. Because CTEs are part of the main query, database engines (like PostgreSQL) can optimize them, such as by inlining the CTE into the using queries if it improves performance.

    // Note: The specific implementation details for 'simpleSelects' 
    // are contained in the associated example file.
    import { simpleSelects } from './0010-simple-selects';
    
    // simpleSelects demonstrates how to use CTEs for simple selects.
  6. How to use the Kysely plugin system

    master

    Plugins are classes that implement the KyselyPlugin interface. You can extend the functionality of your database instance by providing an array of plugin instances to the plugins option when initializing the Kysely instance.

    const db = new Kysely<Database>({
      dialect: new PostgresDialect({
        database: 'kysely_test',
        host: 'localhost',
      }),
      plugins: [new CamelCasePlugin()],
    })
  7. How to nest related rows using JSON helpers

    master

    Kysely is a query builder, not an ORM, so it does not have a built-in concept of relations. To nest related rows (e.g., fetching a person and their pets in a single query), you should use SQL JSON functions.

    Kysely provides dialect-specific helpers to simplify this process by wrapping subqueries into JSON arrays or objects. This approach is highly efficient if you have proper indices on the foreign keys used in the subqueries.

    Supported Dialects

    • PostgreSQL: Use kysely/helpers/postgres
    • MySQL: Use kysely/helpers/mysql (Requires MySQL 8.0.14+)
    • SQLite: Use kysely/helpers/sqlite
    import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'
    
    const persons = await db
      .selectFrom('person')
      .selectAll('person')
      .select((eb) => [
        // Nesting an array of related rows
        jsonArrayFrom(
          eb.selectFrom('pet')
            .select(['pet.id', 'pet.name'])
            .whereRef('pet.owner_id', '=', 'person.id')
            .orderBy('pet.name')
        ).as('pets'),
    
        // Nesting a single related object
        jsonObjectFrom(
          eb.selectFrom('person as mother')
            .select(['mother.id', 'mother.first_name'])
            .whereRef('mother.id', '=', 'person.mother_id')
        ).as('mother')
      ])
      .execute()
  8. How Kysely's execution flow works

    master

    Understanding the Kysely execution flow helps developers understand how type-safe method calls are transformed into database results. The process follows these distinct stages:

    1. Immutable Query Building: Calling methods on the QueryBuilder (like selectFrom, where) does not mutate the existing instance. Instead, each call returns a new QueryBuilder instance containing an updated, immutable QueryAST (Abstract Syntax Tree).
    2. Initiating Execution: The process begins when the final .execute() method is called on the QueryBuilder.
    3. Query Transformation: The QueryExecutor iterates through all registered plugins, calling transformQuery on each to allow modification of the QueryAST before compilation.
    4. Query Compilation: The QueryExecutor delegates the QueryAST to a dialect-specific QueryCompiler, which produces a CompiledQuery (the final SQL string and its parameters).
    5. Connection Handling: The QueryExecutor uses a Driver to acquire a DatabaseConnection. The Driver abstracts vendor-specific details (like pg or mysql2) to manage connections from a pool.
    6. Database Query: The DatabaseConnection sends the CompiledQuery to the underlying DatabaseDriver. The driver returns raw results, which the DatabaseConnection standardizes into a QueryResult.
    7. Result Transformation: The QueryExecutor passes the QueryResult through the plugin system again, calling transformResults on each plugin to allow for final modifications.
    8. Returning to the App: The final, transformed results are returned to the application, resolving the promise from the initial .execute() call.
  9. Define the Database interface for type-safety

    master

    To enable Kysely's type-safety and autocompletion, you must define a Database interface. This interface acts as the single source of truth for your database structure, where keys are table names and values are the corresponding table schema interfaces.

    Important Rules:

    • Table Interfaces: Use table interfaces (e.g., PersonTable) only as values within the Database interface. Never use them directly as the return type of a query.
    • Nullability: If a column is nullable in the database, use a union with null (e.g., string | null). Do not use TypeScript optional properties (?), as Kysely determines optionality automatically based on the types provided.
    • Runtime Types: Kysely is a TypeScript-level tool. The actual runtime types are determined by your database driver (like pg or mysql2). You are responsible for ensuring your TypeScript definitions match the driver's behavior.
    export interface Database {
      person: PersonTable
      pet: PetTable
    }
    
    export interface PersonTable {
      id: Generated<number>
      first_name: string
      last_name: string | null
    }
  10. How to create custom expressions using Expression<T>

    master

    If Kysely lacks a built-in type-safe method for a specific feature, you can implement the Expression<T> interface. This interface requires a type T (representing the data type) and a toOperationNode() method that returns instructions for SQL compilation.

    For most use cases, you don't need to implement the interface manually; you can simply wrap the sql template tag in a function that returns a RawBuilder<T>, which implements Expression<T>.

    import { Expression, Kysely, OperationNode, sql } from 'kysely'
    
    class JsonValue<T> implements Expression<T> {
      #value: T
    
      constructor(value: T) {
        this.#value = value
      }
    
      get expressionType(): T | undefined {
        return undefined
      }
    
      toOperationNode(): OperationNode {
        const json = JSON.stringify(this.#value)
        return sql`CAST(${json} AS JSONB)`.toOperationNode()
      }
    }
    
    // Simplified version using RawBuilder:
    function json<T>(value: T): RawBuilder<T> {
      return sql`CAST(${JSON.stringify(value)} AS JSONB)`
    }
  11. Architecture pattern for using Kysely in a server

    master

    The Kysely example demonstrates a three-layer abstraction pattern for building servers:

    1. Repository: This layer contains all Kysely code. It provides high-level methods for database interaction and works directly with database rows and types (e.g., UserRow).
    2. Service: This layer implements business logic. Services call repositories to interact with the database but do not leak database-specific types (like UserRow) to the rest of the application. Instead, they use domain objects (e.g., User).
    3. Controller: This layer defines the HTTP API. Controllers handle input validation, convert network data to/from application formats, and call services to execute business logic.
  12. Use ExpressionBuilder to construct queries

    master

    Expressions are typically constructed using an ExpressionBuilder<DB, TB>.

    • DB: Your database schema type.
    • TB: A union of table names currently visible in the query context.

    To ensure maximum type safety and correct context inference (so that only available columns are auto-completed), you should obtain the expression builder via a callback provided by Kysely methods. This allows Kysely to automatically infer which tables and columns are accessible in that specific part of the query.

    const person = await db
      .selectFrom('person')
      .select((eb) => [
        // Using eb.fn for SQL functions
        eb.fn('upper', ['first_name']).as('upper_first_name'),
    
        // Using eb to select a subquery
        eb.selectFrom('pet')
          .select('name')
          .whereRef('pet.owner_id', '=', 'person.id')
          .limit(1)
          .as('pet_name'),
    
        // Using eb for boolean expressions
        eb('first_name', '=', 'Jennifer').as('is_jennifer'),
    
        // Using eb.val for static values
        eb.val('Some value').as('string_value'),
    
        // Using eb.lit for literal values
        eb.lit(42).as('literal_value'),
      ])
      .executeTakeFirstOrThrow();