Linting Kysely Queries with Biome and Grit

I’ve been using Kysely heavily recently. It’s great for type-safety, but it has a specific pitfall: it is very easy to write a valid TypeScript expression that creates a query builder but never actually executes it.

The Solution: A Biome plugin

Biome is my linter of choice for JavaScript and TypeScript projects and i wanted to see if I could use it to catch these cases with a simple plugin. The chosen language for Biome plugins is Grit, a query language that, although it has poor documentation, is powerful if you understand how to use it.

Missing select/selectAll clauses

The first issue I wanted to solve was ensuring every select query actually has a select clause. It’s easy to forget to add .selectAll() or .select('id'), which results in no data being fetched.

// This compiles, runs, but doesn't fetch any data!
db.selectFrom('users').where("id", "==", 1).executeTakeFirst();

Standard regex search doesn't work well here because queries can be multi-line or nested. We need to understand the code structure (AST).

My solution:

language js(typescript)

`$chain.$exec()` where {
    $exec <: or { `execute`, `executeTakeFirst`, `executeTakeFirstOrThrow` },
    $chain <: contains `selectFrom`,
    ! $chain <: contains `select`,
    ! $chain <: contains `selectAll`,
    register_diagnostic(span=$chain, message="select statement must contain select() or selectAll()", severity="error")
}

view in repo

This pattern looks for any execution method (execute, executeTakeFirst, etc.) that is part of a chain containing selectFrom. It then checks if that chain does NOT contain either select or selectAll. If both conditions are met, it registers a lint error.

Missing where clauses

Another common mistake is forgetting to add a where clause to an update or delete query, which can lead to unintentional mass updates or deletions.

// the code syntax is valid, but it will delete all users in the database!
db.deleteFrom('users').execute();

Similarly to the previous case, we can write a Grit pattern to search for execution of update or delete queries that do not contain a where clause.

My solution:

language js(typescript)

`$chain.$exec()` where {
    $exec <: or { `execute`, `executeTakeFirst`, `executeTakeFirstOrThrow` },
    or { $chain <: contains `updateTable`, $chain <: contains `deleteFrom` },
    ! $chain <: contains `where`,
    register_diagnostic(span=$chain, message="update or delete statements must contain where()", severity="error")
}

view in repo

Incorrect null checks

It often happens that values in the database can be null. Kysely uses is and is not for null checks, but it’s easy to accidentally use == or !=, which will not work as intended and will not return any results.

// fine syntax but it will not return any results because '==' does not work for null checks in Kysely
db.selectFrom('users').where("optionalFlag", "==", null).executeTakeFirst()

// the correct way to check for null in Kysely is using 'is' or 'is not'
db.selectFrom('users').where("optionalFlag", "is", null).executeTakeFirst()

This is a bit more complex to catch because we need to look for any comparison operator (=, ==, !=, !==) that is comparing a value to null within a where clause.

language js

`$fn($a)` where {
	$fn <: contains `where`,
	or {
		$a <: contains `'='`,
		$a <: contains `'=='`,
		$a <: contains `'!='`,
		$a <: contains `'!=='`
	},
	$a <: contains `null`,
	register_diagnostic(span=$a, message="Comparisons to null using '=' or '==' are not allowed. Use 'is' or 'is not' instead.", severity="error")
}

view in repo

Usage

If you are already using Biome, you can add some or all of these patterns to your project and reference them in your Biome configuration.

biome.json:

{
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  },
  "plugins": [
		"biome_plugins/missing_select.grit",
		"biome_plugins/missing_where.grit",
		"biome_plugins/null.grit"
	],
}

Summary

It’s a simple implementation, but it removes a class of bugs that TypeScript static analysis misses. If you are using Kysely and want to strictly enforce query execution, the source is available here:

https://github.com/marcopedone/biome-kysely-plugin