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

@saralsql/tsql-parser

v0.4.7

Published

High-fidelity T-SQL parser for LSP tooling

Readme

@saralsql/tsql-parser

High-fidelity fault-tolerant parser and semantic analysis engine for Microsoft SQL Server T-SQL.

Built specifically for:

  • Language Servers (LSP)
  • editor tooling
  • diagnostics
  • autocomplete
  • lineage tracking
  • enterprise SQL static analysis
  • SQLCMD preprocessing

Why SaralSQL?

Most SQL parsers are designed for query transformation or simple AST extraction.

SaralSQL is built specifically for real SQL Server tooling workloads involving:

  • massive stored procedures
  • mixed DDL + DML batches
  • temp tables and TVPs
  • incomplete SQL during live editing
  • legacy SQL Server edge cases
  • semantic diagnostics and lineage

The parser is optimized for:

  • SQL Server grammar fidelity
  • fault-tolerant parsing
  • editor-safe recovery
  • semantic enrichment
  • high-complexity enterprise SQL

Production Validation

SaralSQL has been validated against real enterprise SQL Server codebases.

Verified against

  • ✅ 600 modern enterprise stored procedures
  • ✅ 1,790 legacy production stored procedures
  • ✅ recursive CTE-heavy workloads
  • ✅ temp-table-heavy ETL systems
  • ✅ TVP-driven workflows
  • ✅ large mixed DDL/DML deployment scripts
  • ✅ complex MERGE and OUTPUT patterns

Stability Goal

The parser is designed to:

  • never crash on malformed SQL
  • preserve partial AST state
  • continue semantic analysis after localized syntax damage

This is critical for editor and LSP scenarios.


Installation

npm install @saralsql/tsql-parser

Quick Start

import { analyze } from '@saralsql/tsql-parser';

const sql = `
SELECT Id, Name
FROM Users
WHERE Id = @Id;
`;

const result = analyze(sql);

console.log(result.diagnostics);
console.log(result.lineage);

Primary API

Use analyze(sql) for the full parser and semantic pipeline.

import { analyze } from '@saralsql/tsql-parser';

const result = analyze(sql);

Analyze Result

| Field | Description | |---|---| | ast | Parsed AST | | issues | Recoverable parser issues | | scope | Scope graph for variables, parameters, aliases, CTEs, temp tables, and table variables | | diagnostics | Semantic and safety diagnostics | | lineage | Column lineage edges plus source exposure, ambiguity metadata, and mutation target metadata | | columns | Column resolution analysis including ambiguity candidates and correlation flags where available | | typeMembers | Built-in and referenced SQL Server type-member catalog (for property/method completions and typing) |

AST Example

Sample SQL (used in all follow-on examples)

DECLARE @T TABLE (
  StoreId INT,
  GeoPoint GEOGRAPHY,
  StoreName VARCHAR(100)
);

SELECT
  t.StoreId,
  GeoPoint.Lat AS Latitude
FROM @T t
WHERE StoreId = 1;
{
  "type": "Program",
  "start": 0,
  "end": 167,
  "body": [
    {
      "type": "DeclareStatement",
      "variables": [
        {
          "name": "@T",
          "dataType": "TABLE",
          "columns": [
            { "name": "StoreId", "dataType": "INT" },
            { "name": "GeoPoint", "dataType": "GEOGRAPHY" },
            { "name": "StoreName", "dataType": "VARCHAR(100)" }
          ]
        }
      ],
      "start": 0,
      "end": 88
    },
    {
      "type": "SelectStatement",
      "columns": [
        {
          "type": "Column",
          "expression": {
            "type": "Identifier",
            "name": "t.StoreId",
            "parts": ["t", "StoreId"]
          },
          "outputName": "StoreId"
        },
        {
          "type": "Column",
          "expression": {
            "type": "Identifier",
            "name": "GeoPoint.Lat",
            "parts": ["GeoPoint", "Lat"]
          },
          "alias": "Latitude",
          "outputName": "Latitude"
        }
      ],
      "from": [{ "type": "TableReference", "table": { "type": "Identifier", "name": "@T" }, "alias": "t" }],
      "where": {
        "type": "BinaryExpression",
        "left": {
          "type": "Identifier",
          "name": "StoreId",
          "parts": ["StoreId"]
        },
        "operator": "=",
        "right": { "type": "Literal", "value": 1, "variant": "number" }
      }
    }
  ]
}

Top-Level Type Members (from analyze(...))

{
  "typeMembers": {
    "builtIn": {
      "GEOGRAPHY": [
        { "name": "Lat", "kind": "property", "returnType": "FLOAT", "returnKind": "scalar" },
        { "name": "Long", "kind": "property", "returnType": "FLOAT", "returnKind": "scalar" }
      ]
    },
    "referenced": {
      "GEOGRAPHY": [
        { "name": "Lat", "kind": "property", "returnType": "FLOAT", "returnKind": "scalar" },
        { "name": "Long", "kind": "property", "returnType": "FLOAT", "returnKind": "scalar" }
      ]
    }
  }
}

Column Resolution Decision Contract

analyze(sql).columns.resolutions[] includes a parser-native decision object for identifier ownership and ambiguity. For LSP ergonomics, the same decision fields are also surfaced at top-level on each resolution item: owner, scopeDepth, ambiguityCandidates, decisionReason. Using the sample SQL above (both selected columns):

[
  {
    "location": { "name": "t.StoreId" },
    "inputs": [{ "name": "@T.StoreId", "resolution": "resolved" }],
    "owner": "@T",
    "scopeDepth": 0,
    "decisionReason": "qualified_reference",
    "decision": {
      "owner": "@T",
      "scopeDepth": 0,
      "decisionReason": "qualified_reference"
    }
  },
  {
    "location": { "name": "GeoPoint.Lat" },
    "inputs": [{ "name": "GeoPoint.Lat", "resolution": "unresolved" }],
    "owner": "@T",
    "scopeDepth": 0,
    "decisionReason": "qualified_reference",
    "decision": {
      "owner": "@T",
      "scopeDepth": 0,
      "decisionReason": "qualified_reference"
    }
  }
]

decisionReason values:

  • qualified_reference
  • single_scope_owner
  • single_candidate_promotion
  • ambiguous_candidates
  • unresolved_external
  • non_column

This is intended as parser truth for local ownership/ambiguity so LSP clients do not need to re-run scope-walk heuristics.

Property Access Semantic Contract

analyze(sql).columns.propertyAccesses[] surfaces explicit member/property-access semantics for identifier chains. Using the sample SQL above:

{
  "location": { "name": "GeoPoint.Lat", "start": 22, "end": 34 },
  "baseExpr": "GeoPoint",
  "member": "Lat",
  "resolutionMode": "local_typed_member",
  "owner": "@T",
  "dataType": "GEOGRAPHY",
  "memberType": "FLOAT"
}

resolutionMode values:

  • local_typed_member
  • local_untyped_member
  • shape_member

This helps LSP clients classify base.member access without misclassifying all dotted identifiers as table-qualified columns.

Parser-native built-in member coverage is intentionally focused on common enterprise usage:

  • GEOGRAPHY (core): Lat, Long, STSrid, STDistance, STIntersects, STContains, STWithin, STBuffer, STAsText, ToString
  • GEOMETRY (core): STSrid, STDistance, STIntersects, STContains, STWithin, STBuffer, STArea, STLength, STAsText, ToString
  • XML (core): value, query, exist, nodes, modify
  • hierarchyid (core): GetAncestor, GetDescendant, GetLevel, IsDescendantOf, ToString

Scope Symbol Column Metadata

Scope symbols for local tabular shapes now expose structured column metadata.

  • columns: string[] (legacy compatibility)
  • localColumns: Array<{ rawName, normalizedName, dataType?, location? }> (canonical)

This applies to:

  • declared table variables (DECLARE @T TABLE (...))
  • local temp/typed table symbols created in-file
  • TVP-backed and table-variable aliases where local shape is known

Using the sample SQL above, scope.root.resolve('@T'):

{
  "sql": "DECLARE @T TABLE ( StoreId INT, GeoPoint GEOGRAPHY, StoreName VARCHAR(100) )",
  "name": "@T",
  "kind": "Table",
  "columns": ["StoreId", "GeoPoint", "StoreName"],
  "localColumns": [
    {
      "rawName": "StoreId",
      "normalizedName": "storeid",
      "dataType": "INT"
    },
    {
      "rawName": "GeoPoint",
      "normalizedName": "geopoint",
      "dataType": "GEOGRAPHY",
      "typeMembers": [
        { "name": "Lat", "kind": "property", "returnType": "FLOAT" },
        { "name": "Long", "kind": "property", "returnType": "FLOAT" },
        { "name": "STAsText", "kind": "method", "returnType": "NVARCHAR(MAX)" }
      ]
    },
    {
      "rawName": "StoreName",
      "normalizedName": "storename",
      "dataType": "VARCHAR(100)"
    }
  ]
}

Coverage Scorecard

Full T-SQL grammer coverage: ~78% Strongest areas: expressions, SELECT grammar, procedural T-SQL, error recovery Largest gaps: Azure-native DDL, advanced DDL/storage syntax, DCL, linked-server constructs

Excellent coverage for core and advanced query grammar.

Strong support for INSERT, UPDATE, DELETE, MERGE, and OUTPUT.

Near-complete expression parser with precedence handling.

Strong coverage for control flow, cursors, transactions, and error handling.

Useful practical coverage, but not full SQL Server DDL depth.

JSON support is strong; Azure-native DDL is limited.

Systematic recovery boundaries for editor resilience.

Strong everyday SQL Server parser coverage.


Grammar Coverage

SaralSQL focuses heavily on real-world SQL Server grammar used in enterprise stored procedures, ETL systems, and editor tooling.

Known issue: a few constructs marked 🟥 Missing below don't fail cleanly — they parse without a PARSE_* issue but produce structurally wrong AST instead of being rejected (e.g. EXEC ... AT linked_server absorbs AT/the server name as bogus call arguments; EXECUTE AS USER = '...' misreads AS as the procedure name; a MASKED WITH (...) column attribute gets merged into the preceding column's data type and then hallucinates an extra column named WITH). Most unsupported constructs fail loudly and recoverably as intended (OPENROWSET, TABLESAMPLE, GROUPING SETS, etc.) — these three are tracked as a separate class of bug, not just missing coverage.







Prioritized Coverage Gaps


Current Diagnostics

SaralSQL focuses on high-signal diagnostics suitable for editors and code review. Codes follow a CATEGORY### convention so editors and CI can filter/suppress by id.

Variables & Parameters

  • VAR001 undeclared variable
  • VAR002 declared but unused variable
  • VAR003 declared but unused parameter
  • VAR004 variable read before it is ever assigned a value (no initializer, no preceding SET)
  • VAR005 invalid schema-qualified table-variable reference (e.g. dbo.@TableVar)

Columns

  • COL001 unknown column on a resolvable table/alias shape
  • NAM001 unbracketed keyword-like column name (e.g. an unbracketed column named ORDER)

DML Safety

  • DML001 UPDATE without a WHERE clause
  • DML002 DELETE without a WHERE clause
  • DML003 INSERT without an explicit column list
  • DML004 UPDATE target table uses WITH (NOLOCK)
  • SEL001 SELECT *
  • SEL002 SELECT * inside a view
  • LOG001 self-comparison such as u.Id = u.Id

Hints & Query Options

  • JOIN001 join hint usage (HASH / MERGE / LOOP)
  • HINT001 table hint usage (NOLOCK, READPAST, INDEX(...), TABLOCK, etc.)
  • OPT001 OPTION clause query-hint guidance (RECOMPILE, MAXDOP, FORCE_ORDER, etc.)
  • CUR001 cursor usage

DDL & Structure

  • DDL001 missing comma before a table-level constraint
  • DDL002 unnamed PRIMARY KEY / UNIQUE constraint
  • DDL003 unnamed DEFAULT constraint
  • DDL004 CREATE/ALTER PROCEDURE, FUNCTION, VIEW, or TRIGGER is not the first statement in its batch

Duplicates

  • DUP001 duplicate variable/parameter declared in the same scope
  • DUP002 duplicate CTE name within a WITH clause
  • DUP003 duplicate SELECT output alias

Diagnostics are intentionally selective. The goal is to remain useful in enterprise SQL without overwhelming users with low-value warnings.


Fault Tolerance Example

Input SQL:

SELECT *
FROM Users
WHERE

Expected behavior:

  • partial AST is preserved
  • parser issue is recorded
  • scope graph remains usable where possible
  • completion context can still be produced
  • analysis continues for valid sections

This is a core design requirement for editor and LSP scenarios.


Diagnostics Example

Input:

UPDATE u
SET u.Name = 'Bad Update'
FROM Users u WITH(NOLOCK)
WHERE u.Id = u.Id

Diagnostics:

[
  {
    "code": "DML004",
    "severity": "error",
    "message": "UPDATE target table must not use WITH (NOLOCK)"
  },
  {
    "code": "LOG001",
    "severity": "warning",
    "message": "Condition compares 'u.Id' to itself"
  }
]

Column Lineage Example

Input:

INSERT INTO dbo.InvoiceSummary (
    CustomerId,
    InvoiceMonth,
    TotalAmount
)
SELECT
    i.CustomerId,
    i.InvoiceMonth,
    i.Subtotal + i.TaxAmount
FROM dbo.Invoices i;

analyze(sql).lineage.edges (each edge is { from: LineageNode, to: LineageNode, location }; from/to fields shown here, trimmed of location and resolution for brevity):

[
  {
    "from": { "kind": "column", "name": "dbo.Invoices.CustomerId", "source": "dbo.Invoices" },
    "to": { "kind": "result", "name": "dbo.InvoiceSummary.CustomerId" }
  },
  {
    "from": { "kind": "column", "name": "dbo.Invoices.InvoiceMonth", "source": "dbo.Invoices" },
    "to": { "kind": "result", "name": "dbo.InvoiceSummary.InvoiceMonth" }
  },
  {
    "from": { "kind": "column", "name": "dbo.Invoices.Subtotal", "source": "dbo.Invoices" },
    "to": { "kind": "result", "name": "dbo.InvoiceSummary.TotalAmount" }
  },
  {
    "from": { "kind": "column", "name": "dbo.Invoices.TaxAmount", "source": "dbo.Invoices" },
    "to": { "kind": "result", "name": "dbo.InvoiceSummary.TotalAmount" }
  }
]

Note that the multi-term expression (i.Subtotal + i.TaxAmount) correctly produces two separate edges into the same target column, since both source columns contribute to it.


Lineage Metadata Example

analyze(sql).lineage also provides source exposure, ambiguity metadata, and mutation target metadata.

{
  "sources": [
    {
      "name": "a",
      "alias": "a",
      "kind": "derived_subquery",
      "projection": [
        { "name": "SomeName" }
      ]
    }
  ],
  "ambiguities": [
    {
      "name": "Id",
      "candidates": ["Employee", "Department"]
    }
  ],
  "mutations": [
    {
      "statement": "UPDATE",
      "targetName": "e",
      "resolvedSourceName": "Employee"
    }
  ]
}

SQLCMD Preprocessing

SaralSQL natively supports SQLCMD directives (:setvar, :r) and variable expansions ($(Var)).

All AST node coordinates and diagnostic offsets are automatically mapped back to the raw, unexpanded text, ensuring perfect LSP integration.

import { analyze, SqlCmdOptions } from '@saralsql/tsql-parser';

const sql = `
:setvar TableName "Users"

SELECT Id, Name
FROM $(TableName)
WHERE Id = @Id;
`;

const options: SqlCmdOptions = { 
  initialVariables: { 
    Environment: 'PROD' 
  } 
};

const result = analyze(sql, options);

Architecture

Lexer
→ Parser
→ ScopeBuilder
→ LineageBuilder
→ ColumnAnalyzer
→ DiagnosticEngine

Design Principles

Parse once
Enrich in layers
Reuse semantic graph
Avoid duplicate logic
Recover locally
Keep editor tooling alive

Non-goals

SaralSQL is intentionally a single-document analysis engine.

It does not currently provide:

  • cross-file schema catalogs
  • workspace-wide symbol resolution
  • metadata-backed wildcard expansion
  • live database-backed type validation
  • execution-plan validation

Those belong in:

  • the host LSP
  • external metadata services
  • workspace analysis layers

Roadmap

Near Term

  • broader corpus validation
  • richer diagnostics
  • automated code fixes
  • improved DDL coverage
  • Azure SQL grammar expansion

Medium Term

  • schema-aware resolution
  • wildcard expansion
  • FK-aware navigation
  • metadata catalogs
  • standards enforcement packs

Long Term

  • semantic autocomplete
  • rename symbol
  • find references
  • impact analysis
  • safe refactors
  • AI-assisted SQL correction

Contributing

Useful bug reports should include:

  • isolated SQL sample
  • expected behavior
  • current parser output
  • parser issue or diagnostic output
  • package version

License

MIT

Built by Saral Simon Stalin