Skip to content

SafeQL 🤝 Postgres.js

SafeQL is compatible with Postgres.js through @ts-safeql/plugin-postgres-js.

Using the Postgres.js Plugin (Experimental)

EXPERIMENTAL

The Postgres.js plugin is experimental and may change in future releases.

bash
npm install @ts-safeql/plugin-postgres-js
js
// eslint.config.js
import safeql from "@ts-safeql/eslint-plugin/config";
import postgresjs from "@ts-safeql/plugin-postgres-js";
import tseslint from "typescript-eslint";

export default tseslint.config(
  // ...
  safeql.configs.connections({
    databaseUrl: "postgres://user:pass@localhost:5432/db",
    plugins: [postgresjs()],
  }),
);

Once configured, SafeQL will lint normal postgres.js queries and the helper forms the plugin understands:

typescript
import postgres from "postgres";

const sql = postgres();

// Wrong column
const query = sql`SELECT idd FROM users`;
//                       ~~~ Error: column "idd" does not exist

// Missing type annotation
const fixedQuery = sql`SELECT id FROM users`;
//                 ~~~ Error: Query is missing type annotation

// Correct
const typedQuery = sql<{ id: number }[]>`SELECT id FROM users`;

Support Matrix

Legend: supported, ⚠️ partial support, unsupported.

FeatureSupportNotes
Tagged queriesPlain sql`...` queries are validated normally, including type annotation suggestions and fixes
Query modifiersQuery chains like sql`...`.values(), .raw(), .describe(), .execute(), .cursor(), and .forEach() are analyzed through the underlying tagged query
Identifier helperssql("users"), sql("id"), sql("name", "age"), and sql(["name", "age"]) are rewritten to escaped identifiers and column lists
Object helperssql(object), sql(object, "name", "age"), and sql(object, ["name", "age"] as const) are supported in common INSERT and UPDATE helper positions
Array and values helperssql(array) works in common IN (...) and VALUES (...) forms, including 1D and matrix-style input
Multi-row insert helperssql(rows) and sql(rows, "name", "age") are supported in common INSERT helper positions
Direct nested fragmentsFragment variables and inline nested tags like ${where} and ${sql\...`}` are inlined into the outer query
Typed helperssql.typed(...) and sql.typed.foo(...) are treated as parameters
Unsafe SQLsql.unsafe(...) is inlined and checked when the SQL string is statically known
Direct ordering fragmentsDirect fragments like ORDER BY ${sql\age DESC`}` are supported
Copy stream queriesCopy forms like sql`COPY ... FROM STDIN`.writable() and sql`COPY ... TO STDOUT`.readable() are validated through the underlying tagged query
Conditional fragment selectionExamples: ternary fragments, inline dynamic filters, and SQL-function fallbacks
Array-built orderingExample: ORDER BY ${Object.entries(ordering).flatMap(...)}
Identifier transformsExamples: postgres({ transform: postgres.camel }) and sql("aTest")
Multiple statements with .simple()Example: sql`SELECT 1; SELECT 2;`.simple()

Helper-position detection (INSERT, UPDATE, IN, VALUES, SELECT, etc.) mirrors postgres.js's own keyword resolution: the builder is chosen from the last matching keyword in the preceding SQL. This means the plugin reproduces postgres.js's runtime behavior, including its edge cases — for example a stray AS in CAST(x AS date) can influence which builder is selected, exactly as it would in postgres.js.

The postgres.js demo intentionally keeps unsupported cases visible.

Manual Configuration

If you prefer not to use the plugin, you can still configure SafeQL for plain postgres.js tags:

js
// eslint.config.js

import safeql from "@ts-safeql/eslint-plugin/config";
import tseslint from "typescript-eslint";

export default tseslint.config(
  // ...
  safeql.configs.connections({
    targets: [{ tag: "sql", transform: "{type}[]" }],
  }),
);

Once you've set up your configuration, you can start linting your queries:

typescript
import { sql } from "postgres";
import { myClient } from "./myClient"; // Read the note above

// Before:
const query = sql`SELECT idd FROM users`
                         ~~~ Error: column "idd" does not exist 

// After bug fix:
const query = sql`SELECT id FROM users`
              ~~~ Error: Query is missing type annotation

// After: ✅
const query = sql<{ id: number; }[]>`SELECT id FROM users`