Member Junction
    Preparing search index...

    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:

    • Instance (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).
    • Static utilities — pure string/token operations that need no parsed state (StripComments, Tokenize, Analyze, ParseSQL, SqlifyAST, HasUnwrappableTrailingClause, the MJ-template helpers, etc.).
    Index

    Constructors

    • 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.

      Parameters

      Returns SQLParser

      Use SqlDocument.Parse(sql, dialect) to read a statement, and StrictFragmentValidator to screen untrusted SQL.

    Accessors

    • get AST(): SQLAst | SQLAst[] | null

      The raw AST. Prefer the typed accessors; this is an escape hatch.

      Returns SQLAst | SQLAst[] | null

      Kept unchanged, node-sql-parser AST included, for callers that walk that tree; new code reads SQL with SqlDocument.Parse(sql, dialect).

    • get HasWriteStatement(): boolean

      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.

      Returns boolean

      Must not be used to decide whether SQL is safe to run: it returns false whenever node-sql-parser cannot parse the SQL, so a write it cannot read passes. Use SqlDocument.Parse(sql, dialect).IsReadOnly() (fails closed) or StrictFragmentValidator.ValidateReadOnlyStatement.

    • get IdentifierAliases(): ReadonlyMap<string, string> | null

      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.

      Returns ReadonlyMap<string, string> | null

      Kept unchanged, node-sql-parser AST included, for callers that walk that tree; new code reads SQL with SqlDocument.Parse(sql, dialect).

    • get IsValid(): boolean

      Whether the SQL parsed into an AST (directly or via preprocessing).

      Returns boolean

      Use SqlDocument.Parse(sql, dialect).IsComplete, or StrictFragmentValidator to screen untrusted SQL.

    • get OuterCap(): RowCapInfo | null

      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.

      Returns RowCapInfo | null

      Use SqlDocument.Parse(sql, dialect).RowCaps() or AnalyzePagingShape.

    Methods

    • 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.

      Returns void

      Use the StripOrderBy transform, which removes exactly the ORDER BY span.

    • 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.

      Returns void

      Use the SetRowCap / ReplaceSelectList transforms, which edit the SQL text as written.

    • Sets the outermost row cap, in the dialect's form (TOP N on SQL Server, LIMIT N elsewhere), preserving any existing OFFSET. No-op when the root isn't a SELECT.

      Parameters

      • cap: number

      Returns void

      Use the SetRowCap transform, which edits the SQL text as written.

    • 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).

      Returns string

      Use EditSet.Apply() on the original text; this regenerates the SQL from the node-sql-parser tree.

    • Tokenize and return a summary with boolean flags. Useful for quick checks like "does this SQL have MJ extensions?"

      Parameters

      • sql: string

      Returns MJParseResult

      Kept unchanged, paired with Substitute. Use SqlTokens.Lex(sql, dialect).HasTemplate and QueryTemplate.Parse(sql, dialect).

    • Parse MJ SQL into an extended AST.

      Pipeline:

      1. MJLexer tokenizes MJ extensions
      2. MJPlaceholder replaces tokens with SQL-safe placeholders
      3. node-sql-parser produces the SQL AST

      Parameters

      • sql: string

        The MJ SQL string to parse

      • dialect: SQLParserDialect

        SQL dialect ('TransactSQL' | 'PostgresQL', default: 'TransactSQL')

      Returns MJAstifyResult

      Kept unchanged, node-sql-parser AST included, for callers that walk that tree; new code reads SQL with SqlDocument.Parse(sql, dialect).

    • Convert 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().

      Parameters

      Returns string

      Kept unchanged, node-sql-parser AST included, for callers that walk that tree; new code reads SQL with SqlDocument.Parse(sql, dialect).

    • Every 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.

      Parameters

      Returns SQLColumnReference[]

      Use SqlDocument.Parse(sql, dialect).ColumnReferences(), which also binds each reference to its source.

    • Every {{query:"…"}} composition reference, with its category path, query name and parameters. A reference inside a SQL comment is inert and is not reported.

      Parameters

      • sql: string

      Returns MJCompositionRef[]

      Use QueryTemplate.Parse(sql, dialect).CompositionReferences().

    • The {% 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.

      Parameters

      • sql: string

      Returns MJConditionalBlock[]

      Use QueryTemplate.Parse(sql, dialect): its If nodes hold every branch.

    • Splits 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.

      Parameters

      Returns SQLCTEExtraction | null

      Use SqlDocument.Parse(sql, dialect).CTEs() or SplitLeadingCTEs.

    • The parameters a template reads, with the type its SQL filters imply, whether it is required, its default and its filters:

      • required when at least one use sits outside every {% if %} block and has no default(…); a name used only inside conditional blocks, or only in {% if %} conditions, is optional;
      • the iterable of a {% for %} is an array parameter; the loop variable, loop.*, {% set %} names and Context.* tokens are not parameters;
      • the type comes from the last SQL filter (sqlString → string, sqlNumber → number, …); a name read only by conditions is a string;
      • the default comes from a default(…) filter, or else from a {% else %} branch that compares with a single literal.

      Parameters

      • sql: string

      Returns MJParameterInfo[]

      Use QueryTemplate.Parse(sql, dialect).Parameters().

    • Extracts the first statement's SELECT columns with their output names, source columns, and table qualifiers (for a set operation, the first branch's):

      • Simple columns: u.Name → { OutputName: "Name", SourceColumn: "Name", TableQualifier: "u" }
      • Aliases (AS a, a = expr on SQL Server): e.Name AS EntityName → { OutputName: "EntityName", SourceColumn: "Name", TableQualifier: "e" }
      • Expressions: COUNT(*) → { OutputName: "COUNT(...)", SourceColumn: "COUNT(...)", IsExpression: true }; other unnamed expressions are EXPR
      • Stars: * and t.* → OutputName *, with t as the qualifier

      A 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.

      Parameters

      Returns SQLSelectColumn[]

      Use SqlDocument.Parse(sql, dialect).OutputItems().

    • The tables and views the SQL reads, anywhere in the statement (subqueries and CTE bodies included), one entry per schema and table:

      • a CTE reference, a table-valued function and a derived table are not tables;
      • names inside comments and string literals are never read, nor are the FROM words of EXTRACT(… FROM …), TRIM(… FROM …) and IS DISTINCT FROM;
      • an unqualified name gets the dialect's default schema (dbo on SQL Server, public on PostgreSQL), so SchemaName is always set;
      • a template is read branch by branch, and the tables of every branch are reported; a {{query:"…"}} composition is not a table.

      Parameters

      Returns SQLTableReference[]

      Use SqlDocument.Parse(sql, dialect).TableReferences(), which returns null for an unwritten schema and marks CTE and function references by Kind.

    • Every {{ variable | filter }} expression, with its variable name and filter chain. A tag inside a SQL comment is inert and is not reported.

      Parameters

      • sql: string

      Returns MJTemplateExpr[]

      Use QueryTemplate.Parse(sql, dialect): its Nodes hold every expression, and Parameters() the names they read.

    • Token-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.

      Parameters

      Returns boolean

      Use SqlDocument.Parse(sql, dialect).HasStackedStatements().

    • Whether 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:

      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.

      Parameters

      Returns boolean

      Use SqlDocument.Parse(sql, dialect): its top-level query exposes the For and Option tail clauses.

    • True 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_.

      Parameters

      • name: string

      Returns boolean

      Kept unchanged, node-sql-parser AST included, for callers that walk that tree; new code reads SQL with SqlDocument.Parse(sql, dialect).

    • Parse plain SQL into a node-sql-parser AST. Handles FOR XML multi-directive workaround automatically. Returns null if parsing fails.

      Parameters

      Returns SQLAst | SQLAst[] | null

      Kept unchanged, node-sql-parser AST included, for callers that walk that tree; new code reads SQL with SqlDocument.Parse(sql, dialect). To refuse unparseable caller SQL, use StrictFragmentValidator.

    • Renames 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}}"

      Parameters

      • sql: string
      • oldName: string
      • newName: string

      Returns string

      Rename through the QueryTemplate tree (Parameters() gives every use's span) and an EditSet.

    • Reconstruct 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.

      Parameters

      Returns string

      Kept unchanged, node-sql-parser AST included, for callers that walk that tree; new code reads SQL with SqlDocument.Parse(sql, dialect).

    • Strip 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.

      Parameters

      Returns string

      Use SqlTokens.Lex(sql, dialect) and keep the tokens that are not comments.

    • Replace 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.

      Parameters

      • sql: string

      Returns PlaceholderSubstitutionResult

      Kept unchanged, paired with Astify. Read templates with QueryTemplate.Parse(sql, dialect).

    • Substitutes 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.

      Parameters

      • sql: string
      • variableName: string
      • literalValue: string
      • OptionaltagLiteral: string

      Returns string

      Substitute through the QueryTemplate tree (Parameters() gives every use's span) and an EditSet.

    • Tokenize 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.

      Parameters

      • sql: string

      Returns MJToken[]

      Kept unchanged for callers that splice by its offsets; it reports tags inside SQL comments too. Use QueryTemplate.Parse(sql, dialect) (its Nodes and Tokens).

    • Walk 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:

      • "Which template expressions appear in the WHERE clause?"
      • "What composition ref is in the FROM clause?"
      • "Does the ORDER BY reference an MJ token?"

      Parameters

      Returns MJASTWalkResult

      Walk result with annotations, or empty result if AST parsing failed

      Kept unchanged, node-sql-parser AST included, for callers that walk that tree; new code reads SQL with SqlDocument.Parse(sql, dialect). Template positions come from QueryTemplate.Parse(sql, dialect).