@saralsql/tsql-parser
v0.4.7
Published
High-fidelity T-SQL parser for LSP tooling
Maintainers
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-parserQuick 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_referencesingle_scope_ownersingle_candidate_promotionambiguous_candidatesunresolved_externalnon_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_memberlocal_untyped_membershape_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,ToStringGEOMETRY(core):STSrid,STDistance,STIntersects,STContains,STWithin,STBuffer,STArea,STLength,STAsText,ToStringXML(core):value,query,exist,nodes,modifyhierarchyid(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_serverabsorbsAT/the server name as bogus call arguments;EXECUTE AS USER = '...'misreadsASas the procedure name; aMASKED WITH (...)column attribute gets merged into the preceding column's data type and then hallucinates an extra column namedWITH). 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
VAR001undeclared variableVAR002declared but unused variableVAR003declared but unused parameterVAR004variable read before it is ever assigned a value (no initializer, no preceding SET)VAR005invalid schema-qualified table-variable reference (e.g.dbo.@TableVar)
Columns
COL001unknown column on a resolvable table/alias shapeNAM001unbracketed keyword-like column name (e.g. an unbracketed column namedORDER)
DML Safety
DML001UPDATE without a WHERE clauseDML002DELETE without a WHERE clauseDML003INSERT without an explicit column listDML004UPDATE target table usesWITH (NOLOCK)SEL001SELECT *SEL002SELECT * inside a viewLOG001self-comparison such asu.Id = u.Id
Hints & Query Options
JOIN001join hint usage (HASH / MERGE / LOOP)HINT001table hint usage (NOLOCK, READPAST, INDEX(...), TABLOCK, etc.)OPT001OPTION clause query-hint guidance (RECOMPILE, MAXDOP, FORCE_ORDER, etc.)CUR001cursor usage
DDL & Structure
DDL001missing comma before a table-level constraintDDL002unnamed PRIMARY KEY / UNIQUE constraintDDL003unnamed DEFAULT constraintDDL004CREATE/ALTER PROCEDURE, FUNCTION, VIEW, or TRIGGER is not the first statement in its batch
Duplicates
DUP001duplicate variable/parameter declared in the same scopeDUP002duplicate CTE name within a WITH clauseDUP003duplicate 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
WHEREExpected 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.IdDiagnostics:
[
{
"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
→ DiagnosticEngineDesign Principles
Parse once
Enrich in layers
Reuse semantic graph
Avoid duplicate logic
Recover locally
Keep editor tooling aliveNon-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
