Parse SQL into an instance exposing dialect-neutral AST inspection, mutation, and extraction.
On a direct-parse failure, applies preprocessing fallbacks — splitting a
trailing OPTION (...) clause and aliasing bracket-quoted identifiers
([Active People], [my-cte], bracket-quoted CTE names) — so a wider
class of SQL becomes AST-addressable. ToSQL
transparently restores both transforms.
Never throws on unparseable SQL — check IsValid.
Whether ANY top-level statement is a write — a DML mutation (INSERT/UPDATE/DELETE/MERGE/REPLACE), DDL (DROP/CREATE/ALTER/TRUNCATE/ RENAME), or EXEC/CALL/GRANT/REVOKE/USE.
Unlike StatementKind (which classifies only the first statement),
this inspects EVERY top-level statement, so a stacked payload such as
SELECT 1; DROP TABLE x is detected. Benign session prefixes (SET,
DECLARE) and SELECTs are not writes. Matching is on the AST statement
type, so the REPLACE() string function inside a SELECT is never
mistaken for a REPLACE statement.
The bracket identifiers the fallback parse renamed, as the name in AST mapped to the
original bracketed text (for example [My CTE]). Null when the direct parse succeeded, so
every identifier in the tree is as written. A name in this map is in the tree only where the
fallback put it: no such name occurs in the SQL, and none runs into the text next to it.
The outermost row cap on the parsed SELECT, dialect-neutral, or null
when none is present. Read from the AST field the dialect's grammar uses, so the
caller never needs to know whether the cap was a TOP or LIMIT.
High-level classification of the parsed statement:
'select' | 'select-into' | 'set-op' | 'mutation' | 'other'.
Removes the top-level ORDER BY from the parsed statement, following the
set-op (UNION / INTERSECT / EXCEPT) chain so an ORDER BY on any
branch is cleared. No-op when there is none. Dialect-universal — the
orderby field has the same shape across dialects.
Used by the count-SQL builder: ORDER BY is irrelevant for a COUNT and is illegal inside a SQL Server CTE without TOP.
Removes the outermost row cap (TOP on SQL Server, LIMIT on PostgreSQL) from the parsed SELECT. No-op when the root isn't a SELECT.
Used by the count-SQL builder (queryPagingEngine.stripCountBody) so a
paged query's count reflects the full set rather than the capped subset.
It clears both forms, so a PostgreSQL count drops an explicit LIMIT —
consistent with how SQL Server's TOP is dropped.
Extract all column references from this instance's SQL. Convenience delegate to the static overload.
Extract CTE definitions from this instance's SQL. Convenience delegate to the static overload.
Extract SELECT clause columns from this instance's SQL. Convenience delegate to the static overload.
Extract all table/view references from this instance's SQL. Convenience delegate to the static overload.
Serialize the (possibly mutated) AST back to SQL, restoring any
preprocessing transforms applied at construction (bracket-identifier
aliases, then the trailing OPTION (...) clause).
Throws if the SQL was not parseable (check IsValid first).
StaticAnalyzeTokenize and return a summary with boolean flags. Useful for quick checks like "does this SQL have MJ extensions?"
StaticAstifyParse MJ SQL into an extended AST.
Pipeline:
The MJ SQL string to parse
SQL dialect ('TransactSQL' | 'PostgresQL', default: 'TransactSQL')
StaticExprConvert a single AST expression node to a SQL string. Useful for extracting ORDER BY terms, column expressions, etc.
node-sql-parser's exprToSQL always produces backtick-quoted identifiers
regardless of dialect. This method converts backticks to the dialect's
native identifier quoting via dialect.QuoteIdentifier().
StaticExtractEvery identifier path that can be a column, in every clause (select list, joins, WHERE,
GROUP BY, HAVING, ORDER BY, window and WITHIN GROUP clauses, subqueries), as
{ ColumnName, TableQualifier }, one entry per written qualifier.column. A template is
read branch by branch. Returns [] when the SQL cannot be read completely; use
ExtractColumnRefsChecked to tell that apart from SQL with no column references.
StaticExtractExtractColumnRefs, with whether the SQL could be read completely: Complete: false
(and no columns) means the parse failed, not that the SQL names no column.
StaticExtractEvery {{query:"…"}} composition reference, with its category path, query name and
parameters. A reference inside a SQL comment is inert and is not reported.
StaticExtractThe {% if %} blocks, each with its if/elif/else branches: the condition text and the
branch body's exact source text. A tag inside a SQL comment is inert.
StaticExtractSplits a statement that starts with a WITH clause (after any leading comments) into its CTE definitions and the main statement, as written.
Given: WITH A AS (SELECT 1), B AS (SELECT 2) SELECT A.x, B.y FROM A, B
Returns: CTEDefinitions: ["A AS (SELECT 1)", "B AS (SELECT 2)"], MainStatement: "SELECT A.x, B.y FROM A, B"
Each definition is the exact text of name [(columns)] AS (body), quoting included. The
main statement is the rest of the text, trimmed. UsedASTParsing is true when the
statement parsed completely. Returns null when the SQL does not start with WITH.
StaticExtractThe parameters a template reads, with the type its SQL filters imply, whether it is required, its default and its filters:
{% if %} block and has no
default(…); a name used only inside conditional blocks, or only in {% if %}
conditions, is optional;{% for %} is an array parameter; the loop variable, loop.*,
{% set %} names and Context.* tokens are not parameters;sqlString → string, sqlNumber → number, …);
a name read only by conditions is a string;default(…) filter, or else from a {% else %} branch that
compares with a single literal.StaticExtractExtracts the first statement's SELECT columns with their output names, source columns, and table qualifiers (for a set operation, the first branch's):
u.Name → { OutputName: "Name", SourceColumn: "Name", TableQualifier: "u" }AS a, a = expr on SQL Server): e.Name AS EntityName → { OutputName: "EntityName", SourceColumn: "Name", TableQualifier: "e" }COUNT(*) → { OutputName: "COUNT(...)", SourceColumn: "COUNT(...)", IsExpression: true }; other unnamed expressions are EXPR* and t.* → OutputName *, with t as the qualifierA template is read through its base render (every first branch). Returns [] when the SQL
cannot be read completely; use ExtractSelectColumnsChecked to tell that apart from a
statement with no select list.
StaticExtractExtractSelectColumns, with whether the SQL could be read completely: Complete: false (and no columns) means the parse failed, not that the statement has no select list.
StaticExtractThe tables and views the SQL reads, anywhere in the statement (subqueries and CTE bodies included), one entry per schema and table:
FROM words of
EXTRACT(… FROM …), TRIM(… FROM …) and IS DISTINCT FROM;dbo on SQL Server, public on
PostgreSQL), so SchemaName is always set;{{query:"…"}} composition is not a table.StaticExtractEvery {{ variable | filter }} expression, with its variable name and filter chain. A tag
inside a SQL comment is inert and is not reported.
StaticHasToken-aware detection of stacked statements: a statement-separating semicolon that has real content after it (outside string literals, quoted identifiers, and comments). A single trailing semicolon — or a run of them followed only by whitespace/comments — is allowed.
Reads the tokens LexSQL produces for the dialect, so every quoted
form the database recognizes hides its contents: […] identifiers on SQL
Server, E'…' strings with backslash escapes and dollar-quoted strings on
PostgreSQL, and nested block comments on both.
Unlike an AST check, this fires even when the trailing payload makes the
SQL unparseable (SELECT 1; EXEC xp_cmdshell '…', SELECT 1; WAITFOR DELAY '…'), which is exactly the stacked-injection class an AST scan
misses (the whole string fails to parse, so the AST is null). A rendered
read query must be a single statement, so any internal ; is rejected.
StaticHasWhether the statement ends in a clause that cannot legally appear inside a derived table,
so wrapping it in SELECT ... FROM (<sql>) AS t would produce invalid SQL:
FOR JSON … / FOR XML … at the top levelOPTION (…) on SQL Server)Only the top level of the statement counts. The same words inside a subquery (a correlated
(SELECT … FOR JSON PATH) column), a string literal, a quoted identifier or a comment do
not stop a wrap.
StaticIsTrue when name has the form of a name the fallback parse gives a renamed bracket
identifier (IdentifierAliases), ignoring case: _mjid_<n>, or _mjid<k>_<n> when
the SQL already contains _mjid_.
StaticParseParses SQL and extracts its table and column references. A template is read branch by branch: the result is the union of every branch's references.
Tables: as ExtractTableRefs.Columns: as ExtractColumnRefs (empty when the SQL could not be read completely).UsedASTParsing: true when every reading of the SQL parsed completely.Parse options, or just a SQL string
Optionaldialect: SQLParserDialectThe dialect, when options is a string; SQL Server when omitted
StaticParseParse plain SQL into a node-sql-parser AST. Handles FOR XML multi-directive workaround automatically. Returns null if parsing fails.
StaticParseThe same as Parse, which reads templates itself.
Parse options, or just a SQL string
Optionaldialect: SQLParserDialectThe dialect, when options is a string; SQL Server when omitted
StaticRenameRenames a template variable throughout the SQL: in every {{ variable | filters }}
expression, preserving the filter chain, and in every read of it inside {% if %},
{% elif %}, {% for %} and {% set %} tags.
Example: RenameTemplateVariable("WHERE x = {{ region | sqlString }}", "region", "userRegion")
→ "WHERE x = {{userRegion | sqlString}}"
StaticSqlifyReconstruct MJ SQL from an Astify result.
For plain SQL: uses node-sql-parser's sqlify for normalized output. For MJ SQL: reconstructs from tokens, preserving all Nunjucks and composition syntax verbatim.
StaticSqlifyConvert a node-sql-parser AST (or array of ASTs) back to a SQL string. This is a thin wrapper around node-sql-parser's sqlify.
StaticStripStrip line and block comments from SQL, preserving content inside string literals and quoted identifiers. Block comments nest. A block comment is replaced by a space, so two tokens it separated with no whitespace stay apart; a line comment's ending newline is kept.
The dialect determines which literal and identifier forms are recognized: SQL Server
[…], PostgreSQL E'…' and dollar-quoted strings ($$…$$, $tag$…$tag$), MySQL
`…`. Double-quoted identifiers are honored on every dialect.
StaticSubstituteReplace MJ tokens with SQL-safe placeholders.
Context-aware: sqlString → string literal, sqlNumber → numeric literal,
sqlIdentifier → bare identifier. Block tags and comments are stripped.
Returns a position map for reversing the substitution.
StaticSubstituteSubstitutes a template variable with a literal value in all {{ variable | filters }} expressions.
The entire expression (including filters) is replaced with the literal value, since filters
are irrelevant when injecting a concrete value.
Example: SubstituteTemplateVariable("WHERE x = {{ region | sqlString }}", "region", "'West'")
→ "WHERE x = 'West'"
When tagLiteral is given, reads of the variable inside {% if %}, {% elif %},
{% for %} and {% set %} tags are replaced with it too, so conditions see the value.
It must be a template-language literal ("West", 42), not a SQL one.
OptionaltagLiteral: stringStaticTokenizeTokenize MJ SQL into an ordered list of tokens. Returns MJ tokens (template expressions, composition refs, block tags, comments) interleaved with SQL_TEXT tokens for the plain SQL segments.
StaticWalkWalk the AST from an Astify result and annotate where MJ placeholders appear.
Returns an MJASTWalkResult with annotations mapping each placeholder to its SQL clause context (SELECT, WHERE, ORDER BY, etc.) and the resolved MJ node.
This enables queries like:
The MJAstifyResult from Astify()
Walk result with annotations, or empty result if AST parsing failed
Unified parser for MemberJunction's SQL superset.
Handles standard SQL, Nunjucks template expressions (
{{ var | filter }}), Nunjucks block tags ({% if %}...{% endif %}), composition tokens ({{query:"Path/Name(params)"}}), and template comments ({# ... #}).Two-tier API:
new SQLParser(sql, dialect)) — parses once (with a preprocessing fallback) and exposes typed, dialect-neutral AST inspection/mutation (StatementKind,OuterCap,SetOuterCap,ToSQL) plus extraction helpers (ExtractCTEs,ExtractTableRefs,ExtractColumnRefs,ExtractSelectColumns).StripComments,Tokenize,Analyze,ParseSQL,SqlifyAST,HasUnwrappableTrailingClause, the MJ-template helpers, etc.).