Documentation

Everything you need to lint .sql files with ESLint — install, configure, write rules.

Requirements

  • Node.js 22 or later
  • ESM-only package ("type": "module")
  • Runs on macOS, Linux and Windows. The WebAssembly binary ships inside the package.

Installation

npm install --save-dev postgresql-eslint-parser
# or
pnpm add -D postgresql-eslint-parser
# or
yarn add --dev postgresql-eslint-parser

ESLint configuration

Use ESLint's flat config and point the parser at .sql files.

// eslint.config.js
import postgresqlParser from "postgresql-eslint-parser";

export default [
  {
    files: ["**/*.sql"],
    languageOptions: {
      parser: postgresqlParser,
    },
    rules: {
      // your SQL-specific rules
    },
  },
];

ESLint directives in SQL comments

ESLint's directive scanner reads eslint-disable-style comments. The parser surfaces SQL comments as ESLint comment nodes, so the usual directives work in .sql files:

-- eslint-disable-next-line no-select-star
SELECT * FROM users;

/* eslint-disable some-rule */
SELECT id FROM users;
/* eslint-enable some-rule */
SQL commentESLint comment node
-- ...{ type: "Line", value: "..." }
/* ... */{ type: "Block", value: "..." }

Linting PL function bodies

CREATE FUNCTION and CREATE PROCEDURE bodies written in another language are exposed as EmbeddedCode nodes on the parent statement. The parser stays language-agnostic — it captures the source text, the absolute range/loc inside the SQL, the lower-cased LANGUAGE clause, and the quote style ("dollar" or "single"), and stops there. C functions written as the two-argument AS 'lib', 'sym' form are skipped because they are not source code.

To lint these bodies, plug the postgresql-eslint-parser/processor subpath into your flat config. It emits one virtual file per body (0.js, 1.py, …) and translates lint messages back to the SQL coordinate system. Any PL is supported as long as you give it an extension and configure an ESLint parser for that extension:

// eslint.config.js
import postgresqlParser from "postgresql-eslint-parser";
import { createPlProcessor } from "postgresql-eslint-parser/processor";
import tsParser from "@typescript-eslint/parser";

const plProcessor = createPlProcessor({
  languages: {
    plv8: ".js",
    plv8u: ".js",
    plpgsql: ".plpgsql",
    plpython3u: ".py",
  },
  // "skip" (default) silently drops bodies whose language is not mapped;
  // "error" throws so unhandled PLs do not pass silently.
  unknown: "skip",
});

export default [
  {
    files: ["**/*.sql"],
    languageOptions: { parser: postgresqlParser },
    processor: plProcessor,
  },
  {
    files: ["**/*.sql/*.js"],
    languageOptions: { parser: tsParser },
    rules: {
      // your JS rules for plv8 bodies
    },
  },
];

Need to enumerate bodies for tooling that isn't ESLint? Use the low-level extractEmbeddedCode helper exported from the main entry point — it returns every EmbeddedCode node in source order:

import { parseForESLint, extractEmbeddedCode } from "postgresql-eslint-parser";

const { ast } = parseForESLint(sql);
for (const body of extractEmbeddedCode(ast)) {
  console.log(`${body.language}: ${body.source.length} chars at ${body.range[0]}`);
}
Limitation. Fix-range translation runs only for dollar-quoted bodies. For single-quoted bodies that contain '' escapes, the processor drops fixes (line/column positions are still reported).

Programmatic API

parse(code: string)

Parses SQL and returns a Program AST node.

parseForESLint(code: string)

Returns { ast, visitorKeys, scopeManager } — the shape ESLint expects from a custom parser. scopeManager is currently always null.

Ast namespace

All node types (Program, SelectStmt, SQLStatementNode, SQLParseError, …) are re-exported under Ast for use in TypeScript rule authoring.

import parser, { parse, parseForESLint, type Ast } from "postgresql-eslint-parser";

const program = parse("SELECT 1");
const { ast, visitorKeys } = parseForESLint("SELECT 1");

function isSelect(node: Ast.SQLStatementNode): node is Ast.SelectStmt {
  return node.type === "SelectStmt";
}

extractEmbeddedCode(program: Program): EmbeddedCode[]

Returns every EmbeddedCode node (PL function body) in source order. Each entry carries language, source, absolute range / loc in the SQL file, and quoteStyle. See Linting PL function bodies for the full flow.

createPlProcessor(options): Processor

Exported from postgresql-eslint-parser/processor. Takes a { languages, unknown? } options object and returns a generic ESLint processor that emits one virtual file per PL body with the configured extension.

Syntax errors

parseForESLint does not throw on broken SQL. The lint run keeps going, and the program body holds a single SQLParseError node so other tooling can still introspect:

interface SQLParseError {
  type: "SQLParseError";
  range: [number, number];
  loc: Ast.SourceLocation;
  error: string;   // human-readable message from libpg-query
  raw: string;     // the original source
}

Writing custom rules

Visitor keys are exported, so the regular ESLint rule shape works:

// no-select-star.ts
import type { Rule } from "eslint";
import type { Ast } from "postgresql-eslint-parser";

export const noSelectStar: Rule.RuleModule = {
  meta: {
    type: "suggestion",
    messages: { star: "SELECT * is disallowed — list columns explicitly." },
  },
  create(context) {
    return {
      SelectStmt(node: Ast.SelectStmt) {
        const target = node.targetList ?? [];
        const hasStar = target.some((t) => t?.ResTarget?.val?.A_Star);
        if (hasStar) context.report({ node, messageId: "star" });
      },
    };
  },
};
Tip. When you're not sure about a node's shape, open the Playground and paste in some SQL — the AST tree shows every property, range and location.