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
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
SaralSQL has been validated against real enterprise SQL Server codebases.
- β 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
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.
npm install @saralsql/tsql-parserimport { 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);Use analyze(sql) for the full parser and semantic pipeline.
import { analyze } from '@saralsql/tsql-parser';
const result = analyze(sql);| 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) |
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" }
}
}
]
}{
"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" }
]
}
}
}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.
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 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)"
}
]
}
Everyday TSQL coverage: 92-94%
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
|
~92% Excellent coverage for core and advanced query grammar. |
~88% Strong support for INSERT, UPDATE, DELETE, MERGE, and OUTPUT. |
~95% Near-complete expression parser with precedence handling. |
~90% Strong coverage for control flow, cursors, transactions, and error handling. |
|
~62% Useful practical coverage, but not full SQL Server DDL depth. |
~30% JSON support is strong; Azure-native DDL is limited. |
~91% Systematic recovery boundaries for editor resilience. |
~78% Strong everyday SQL Server parser coverage. |
SaralSQL focuses heavily on real-world SQL Server grammar used in enterprise stored procedures, ETL systems, and editor tooling.
Legend:
π© Full Β Β
π¨ Partial Β Β
π₯ Missing
Estimated implementation coverage:
~78% Full Β· major day-to-day T-SQL covered Β· Azure-native DDL still limited
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.
|
|
|
|
|
|
|
|
|
|
|
|
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.
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)
COL001unknown column on a resolvable table/alias shapeNAM001unbracketed keyword-like column name (e.g. an unbracketed column namedORDER)
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
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
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
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.
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.
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"
}
]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.
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"
}
]
}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);Lexer
β Parser
β ScopeBuilder
β LineageBuilder
β ColumnAnalyzer
β DiagnosticEngine
Parse once
Enrich in layers
Reuse semantic graph
Avoid duplicate logic
Recover locally
Keep editor tooling alive
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
- broader corpus validation
- richer diagnostics
- automated code fixes
- improved DDL coverage
- Azure SQL grammar expansion
- schema-aware resolution
- wildcard expansion
- FK-aware navigation
- metadata catalogs
- standards enforcement packs
- semantic autocomplete
- rename symbol
- find references
- impact analysis
- safe refactors
- AI-assisted SQL correction
Useful bug reports should include:
- isolated SQL sample
- expected behavior
- current parser output
- parser issue or diagnostic output
- package version
MIT
Built by Saral Simon Stalin