toddlevy/tl-agent-skills

tl-kysely-patterns

- Type-safe SQL query building with Kysely for PostgreSQL.

Voir la source
Document Skill original

Rendu depuis le dépôt source en conservant titres, exemples, code, tableaux, liens et images.

<!-- Copyright (c) 2026 Todd Levy. Licensed under MIT. SPDX-License-Identifier: MIT -->

Kysely: Type-Safe SQL Patterns

Kysely (pronounced "Key-Seh-Lee") is a type-safe TypeScript SQL query builder. It generates plain SQL with zero runtime ORM overhead. Every query is validated at compile time with full autocompletion.

Kysely is not an ORM -- no relations, no lazy loading, no magic. Just SQL with types.

When to Use

  • "write a Kysely query"
  • "create database migration"
  • "add a new table"
  • "query with joins / subqueries / CTEs"
  • "JSONB or array column operations"
  • Working with an existing Kysely + PostgreSQL codebase
  • Debugging Kysely type inference issues

Outcomes

  • Artifact: Type-safe queries using ExpressionBuilder patterns
  • Artifact: Migration files via kysely-ctl
  • Decision: When to use query builder vs sql template tag

Core Philosophy

Prefer Kysely's query builder for everything it can express. Fall back to sql template tag only when the builder lacks support.

Use CaseApproach
Schema definitionsKysely migrations (db.schema.createTable)
Simple CRUDQuery builder (selectFrom, insertInto, updateTable, deleteFrom)
JOINs (any complexity)Query builder (callback format for complex joins)
Aggregations / GROUP BYQuery builder with eb.fn
CTEsQuery builder (.with())
Relations / nested JSONjsonArrayFrom / jsonObjectFrom helpers
Conditional queries$if() or dynamic filter arrays
Reusable fragmentsExpression<T> helper functions
Dynamic columns/tablesdb.dynamic.ref() / db.dynamic.table() with allowlisted values
Dynamic SQL fragmentssql.raw() with allowlisted values, sql.join() for arrays
Dialect-specific syntaxsql template tag
Unsupported operatorssql template tag
Need a query?
  Can Kysely's builder express it?
    YES -> Use the query builder (type-safe, composable)
    NO  -> Use sql`` template tag (always type your output: sql<Type>`...`)

ExpressionBuilder (eb) Cheat Sheet

The eb callback parameter is the foundation of type-safe query building:

MethodPurposeExample
eb.ref("col")Column referenceeb.ref("user.email")
eb.val(value)Parameterized value ($1)eb.val("hello")
eb.lit(value)SQL literal (numbers, bools, null only)eb.lit(0), eb.lit(null)
eb.fn<T>("name", [...])Typed function calleb.fn<string>("upper", [eb.ref("email")])
eb.fn.count("col")COUNT aggregateeb.fn.count("id").as("count")
eb.fn.sum / avg / min / maxOther aggregateseb.fn.sum("amount").as("total")
eb.fn.coalesce(col, fallback)COALESCEeb.fn.coalesce("col", eb.val(0))
eb.case().when().then().else().end()CASE expressionsee query-patterns.md
eb.and([...]) / eb.or([...])Combine conditionseb.or([eb("a","=",1), eb("b","=",2)])
eb.exists(subquery)EXISTS checkeb.exists(db.selectFrom(...))
eb.not(expr)Negate expressioneb.not(eb.exists(...))
eb.cast(expr, "type")SQL CASTeb.cast(eb.val("x"), "text")
eb(left, op, right)Binary expressioneb("qty", "*", eb.ref("price"))

For full query examples, see references/query-patterns.md.

Database Types

typescript
import { Generated, Insertable, Selectable, Updateable } from "kysely"

interface Database {
  users: UsersTable
  posts: PostsTable
}

interface UsersTable {
  id: Generated<number>
  email: string
  name: string
  created_at: Generated<Date>
}

// Helper types make Generated fields optional for inserts/updates
type NewUser = Insertable<UsersTable>
type UserUpdate = Updateable<UsersTable>
type User = Selectable<UsersTable>

Use kysely-codegen to generate these types from your database. See references/migrations.md.

Pitfalls

These are the most common mistakes when writing Kysely code.

1. eb.val() vs eb.lit() confusion

eb.val() creates parameterized values ($1) -- use for user input. eb.lit() creates SQL literals -- only accepts numbers, booleans, null (not strings). For string literals, use sql\'value'\``.

typescript
eb.val("safe input")              // $1 -- parameterized, safe
eb.lit(42)                        // 42 -- literal in SQL
eb.lit("text")                    // THROWS "unsafe immediate value"
eb.cast(eb.val("text"), "text")   // $1::text -- workaround for typed string params

2. Forgetting .execute()

Queries are lazy builders. Without an execute method, nothing runs.

typescript
db.selectFrom("user").selectAll()                  // does nothing
await db.selectFrom("user").selectAll().execute()   // runs the query

3. .where() vs .whereRef() for column comparisons

.where("a", "=", "b") compares column a to the string "b". Use .whereRef() for column-to-column comparisons.

typescript
.where("table.col", "=", "other.col")       // compares to string literal
.whereRef("table.col", "=", "other.col")    // compares two columns

4. Always type sql`` template literals

sql template literals infer as unknown. Always provide an explicit type parameter.

typescript
sql`now()`                      // Expression<unknown> -- bad
sql<Date>`now()`                // Expression<Date> -- good

5. selectAll() breaks nested json helper type inference (#1059)

Bare .selectAll() inside json helper subqueries merges outer table columns into the type. Use table-qualified .selectAll("table_name") instead. See references/relations-helpers.md.

6. DATE columns cause timezone drift

The pg driver converts DATE to JS Date, causing timezone issues. Parse DATE as string instead. See references/migrations.md.

7. "Type instantiation is excessively deep"

Complex queries with many CTEs can exceed TypeScript's type depth. Use $assertType<T>() on intermediate CTEs. See references/relations-helpers.md.

8. PostgreSQL does NOT auto-index foreign keys

Always create indexes on FK columns manually in migrations. See references/migrations.md.

9. CamelCasePlugin causes drift with raw SQL

CamelCasePlugin converts snakecase DB columns to camelCase in the builder. But raw `sql` template queries bypass the plugin, creating inconsistent naming between builder and raw queries in the same codebase. If you use significant raw SQL alongside the builder, avoid this plugin and keep snakecase throughout. See references/migrations.md.

10. JSONB inserts need JSON.stringify only in sql templates (#209)

The pg driver auto-serializes objects for .values()/.set() JSONB params (pg types). You only need explicit JSON.stringify inside sql template expressions or with non-pg drivers. See references/jsonb-arrays.md.

11. Pool queries use different connections (API, #330)

Each query may use a different pooled connection. SET, session variables, and RLS context do not persist across queries. Use db.transaction() or db.connection() to pin multiple statements to one connection. See references/advanced-patterns.md.

12. WHERE does not narrow result types (#310)

.where('col', 'is not', null) does not remove null from the result type. Use $narrowType to manually assert the narrowed shape. See references/advanced-patterns.md.

13. Team migration ordering (#697)

Migrations added on parallel branches may fail strict ordering when merged. Set allowUnorderedMigrations: true on the Migrator. See references/migrations.md.

14. JSON aggregation changes runtime types (#1412)

Date columns inside jsonArrayFrom/jsonObjectFrom/json_agg results become strings at runtime because JSON has no Date type. TypeScript types still say Date. Parse dates manually at the boundary. See references/jsonb-arrays.md.

Official Resources

ResourceURL
LLM-friendly docs (full)https://kysely.dev/llms-full.txt
API documentationhttps://kysely-org.github.io/kysely-apidoc
Playgroundhttps://kyse.link
GitHubhttps://github.com/kysely-org/kysely
Awesome Kysely (ecosystem)https://github.com/kysely-org/awesome-kysely

When using Cursor @Docs, reference https://kysely.dev/llms-full.txt for the most complete context.

Reference Files

Consult these for detailed code patterns:

ReferenceWhen to Use
query-patterns.mdSELECT, WHERE, JOINs, aggregations, ORDER BY, mutations, $if, subqueries, transactions
jsonb-arrays.mdJSONB columns, array columns, JSONPath, querying JSON/array data
relations-helpers.mdjsonArrayFrom, jsonObjectFrom, reusable Expression<T> helpers, CTEs, compile/InferResult
migrations.mdkysely-ctl setup, migration files, column types, type generation, plugins, Neon dialect, DATE fix
advanced-patterns.mdDynamic columns, withSchema, connection pinning, RLS, $narrowType, streaming, MERGE, views, FTS, testing
ecosystem.mdPagination, auth adapters, Fastify plugin, community dialects
du même dépôt

Autres Skills

Tous les Skills
toddlevy
Communauté

tl-agent-plan-execute

Execute a verified plan document. Consumes verification receipts from tl-agent-plan-audit to avoid redundant re-verification. Defines the trust model, staleness protocol, and exit gate execution process. Use when executing a .plan.md file, starting plan implementation, or when the user says "implement the plan" or "execute the plan".

installations
1
GitHub Stars
0
Mis à jour
8 sept.
toddlevy
Communauté

tl-docs-viewer-create

Create a React admin UI for browsing documentation folders with tree navigation, markdown rendering, Mermaid diagrams, and TOC generation. Use when adding a docs viewer to an admin interface.

installations
1
GitHub Stars
0
Mis à jour
8 sept.
toddlevy
Communauté

tl-live-music-data

Reference documentation for live music data APIs and ID mapping between services. Use when integrating MusicBrainz, Setlist.fm, JamBase, Bandsintown, Ticketmaster, or other concert/artist APIs.

installations
1
GitHub Stars
0
Mis à jour
8 sept.
toddlevy
Communauté

tl-schema-org

The full Schema.org vocabulary -- all 800+ types, 1500+ properties -- with production patterns for JSON-LD rendering, database modeling, API interoperability, extension governance, and rich results. Not just SEO markup. Use when working with structured data, Schema.org types, JSON-LD, or designing data models and APIs grounded in Schema.org.

installations
1
GitHub Stars
0
Mis à jour
8 sept.