npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@doync/sqlite-parser

v0.3053003.0

Published

Native TypeScript SQLite SQL parser: a typed, visitable AST with JSON serialization, SQL unparsing, and tolerant parsing. Zero dependencies.

Readme

@doync/sqlite-parser

A native TypeScript SQLite SQL parser fully compatible with SQLite's own parser.

It parses any SQLite SQL into a typed, visitable, JSON-serializable AST – similar to the one generated by liteparser, as the project began as a TypeScript rewrite of it – but has since evolved past it. SQLite itself is the only upstream: the parser's grammar is woven from SQLite's own parse.y, and fidelity is enforced by a generated grammar corpus carrying acceptance verdicts baked from real SQLite's prepare() with anchored structural matchers, two seeded fuzzers (grammar-walk and tokenizer-lexeme), a dedicated tokenizer edge-case suite, and a clean-parse ratchet plus self-round-trip invariant over SQLite's own 22,888-statement test corpus.

  • Free functions, fully synchronous
  • Zero runtime dependencies, ESM + CJS, 100% tree-shakable
  • Complete SQLite grammar — SELECT (compounds, CTEs, window functions), INSERT/UPSERT, UPDATE, DELETE, RETURNING, CREATE TABLE/INDEX/VIEW/TRIGGER/VIRTUAL TABLE, ALTER, DROP, PRAGMA, transactions, savepoints, ATTACH/DETACH, VACUUM/REINDEX/ANALYZE, EXPLAIN.
  • Faithful to SQLite — the grammar is SQLite's own Lemon grammar with the Lemon generator retargeted to emit TypeScript, and the tokenizer is a hand-port of SQLite's.

Playground

Try the parser in the browser at closeio.github.io/doync — type SQL on one side and see its AST on the other, or edit the AST and watch it unparse back to SQL.

To run it locally against your own build (playground.html imports ./dist/index.js, so it reflects the last pnpm build):

pnpx local-web-server -d packages/sqlite-parser --static.index playground.html
# → http://localhost:8000

Installation

pnpm add @doync/sqlite-parser

Parsing

import {
  parse,
  parseAll,
  parseTolerant,
  SQLiteParserError,
} from '@doync/sqlite-parser'

// One statement -> typed AST node. Throws SQLiteParserError on invalid SQL.
const stmt = parse('SELECT a, b FROM t WHERE a > 5')
stmt.kind // 'STMT_SELECT'

// A semicolon-separated script -> all statements.
const stmts = parseAll('CREATE TABLE t (a); INSERT INTO t VALUES (1);')

// Tolerant / IDE mode: never throws, recovers at `;` boundaries.
const { stmts, errors } = parseTolerant('SELECT 1; GARBAGE; SELECT 2')
errors[0].code // 'syntax' | 'illegal_token' | 'incomplete' | 'stack_overflow'
errors[0].pos // { offset, line, col } — byte-exact source range start

try {
  parse('SELECT FROM')
} catch (e) {
  if (e instanceof SQLiteParserError) {
    e.code // 'syntax'
    e.pos // start of the offending token
    e.end_pos // end of the error range (exclusive)
  }
}

The AST is a discriminated union keyed by kind, so TypeScript narrows node types inside switches and handlers:

import type { Statement } from '@doync/sqlite-parser'

function whereOf(stmt: Statement) {
  if (stmt.kind === 'STMT_SELECT') return stmt.where // typed as Expr | undefined
}

Nodes carry byte-exact source positions (pos: { offset, line, col }), and optional fields are simply absent.

SQL output

import { parse, unparse } from '@doync/sqlite-parser'

const stmt = parse('SELECT a FROM t')

unparse(stmt) // 'SELECT a FROM t' — SQL text back from any AST node

The round-trip invariant — parse → unparse → parse yields a structurally equal AST (enforced over the whole corpus in tests).

The AST is plain data — JSON.stringify(stmt) serializes it (the in-memory node is the serialized shape). Key order is builder-insertion order and non-contractual, so compare structurally, never by string.

Traversal

import { parse, walk, Visit } from '@doync/sqlite-parser'

const stmt = parse(
  'WITH x AS (SELECT a FROM t) SELECT * FROM x JOIN u ON x.a = u.a',
)

// Pre-order walk with per-kind handlers; each handler gets its node narrowed.
const tables: string[] = []
walk(stmt, {
  FROM_TABLE(node) {
    tables.push(node.name ?? '') // node: FromTableNode
  },
  EXPR_SUBQUERY() {
    return Visit.Skip // don't descend into this subtree
  },
  '*'(node, ancestors) {
    // runs for every node; `ancestors` is a stable root-first snapshot,
    // e.g. for finding an enclosing WITH scope
  },
})
  • Return Visit.Skip to prune a subtree, Visit.Stop to end the walk.
  • childNodes(node) returns a node's direct children generically (and throws loudly if a future node shape drifts, instead of silently skipping).
  • match(node, handlers, fallback) is typed single-level dispatch that returns a value — the building block for lowering the AST to your own IR:
import { match, parse, type Expr } from '@doync/sqlite-parser'

const select = parse('SELECT * FROM t WHERE a > 5')
const where = select.kind === 'STMT_SELECT' ? select.where : undefined

const lowered =
  where &&
  match<Expr, string>(
    where,
    {
      EXPR_COLUMN_REF: (n) => n.column ?? '?',
      EXPR_BINARY_OP: (n) => `(${n.op})`,
    },
    (n) => `unsupported: ${n.kind}`, // every unhandled kind routes here
  )
  • assertNever(value) is the exhaustiveness guard for switch (node.kind): unhandled kinds become compile errors.

Fidelity notes

Fidelity means equivalence to real SQLite's own parser, at the exact version node:sqlite bundles (anchored by the repo's .nvmrc; currently SQLite 3.53.3): the parser accepts what SQLite accepts and rejects what SQLite rejects for everything decidable from the SQL text alone — including parse-time reduce-action rejections such as non-constant column DEFAULTs, unknown join types, or ORDER BY before a compound operator — with no divergence allowlist. A disagreement with SQLite in either direction is a bug.

Two prepare-time rejection families are deliberately not mirrored because they are schema-gated — whether SQLite rejects depends on what names resolve to, which a pure-syntax parser cannot know: RAISE(...) outside a trigger body, and qualified table names in trigger-body DML (rejected by SQLite only when the trigger resolves to a non-TEMP schema). Both sit on the safe side: the parser accepts, exactly as SQLite does when the schema resolves the other way.

Three SQLite parser quirks are reproduced on purpose (the test suites enforce them): TRUE/FALSE parse as column references — with the nuance that a quoted "true"/"false" carries quoted: true and stays a non-constant column ref in DEFAULT, mirroring SQLite's EP_Quoted gate; VALUES (...) lowers into a SELECT; and index / primary-key columns are ORDER_TERM nodes.

Versioning

@doync/sqlite-parser versions as 0.<SQLITE_VERSION_NUMBER>.<counter>, where the middle component is SQLite's own canonical numeric encoding of the fidelity target. SQLite 3.53.3 → 3053003, so 0.3053003.0 means "parses what SQLite 3.53.3 parses." The target is the node:sqlite build anchored by the repo's .nvmrc.

The last component is this package's own release counter. By convention the parser only ever receives patch changesets — a minor or major bump would corrupt the encoded SQLite version (0.3053003.x0.3053004.0 would falsely claim SQLite 3.53.4). Retargeting SQLite is a manual version edit in package.json in the same PR that moves the target; the middle component is a floor, not a signal of this package's own feature growth (read the changelog for that). The full retargeting workflow is documented in Upgrading SQLite.

/internal

@doync/sqlite-parser/internal exists so every publishable package exposes the same subpath contract. The current public surface already stands on the main entry; the internal entry is a placeholder. Anything under /internal may change in any release, including patches, with no notice. Import parse / unparse / the AST from @doync/sqlite-parser.

Contributions

Issues and pull requests welcome on closeio/doync. See the root README for install, lint, typecheck, and remaining contribution notes.