import { DatabasePlatform, UserInfo, QueryDependencySpec } from "@memberjunction/core"; /** * Minimal contract for queries flowing through the composition engine. * `MJQueryEntityExtended` satisfies this structurally; synthetic inline * dependency queries satisfy it via a plain-object literal with stubs. */ export interface IComposableQuery { ID: string; Name: string; SQL: string | null; UsesTemplate: boolean | null; Reusable: boolean | null; Status: string | null; IsApproved: boolean; UserCanRun(user: UserInfo, _visited?: Set): { canRun: boolean; deniedEntities: string[]; }; GetPlatformSQL(platform: DatabasePlatform): string; } /** * Metadata about a single CTE generated during composition resolution. */ export interface CompositionCTEInfo { /** ID of the referenced query */ QueryID: string; /** Name of the referenced query */ QueryName: string; /** Category path as written in the reference */ CategoryPath: string; /** Generated CTE alias name */ CTEName: string; /** Original SQL of the referenced query before parameter resolution */ OriginalSQL: string; /** SQL after parameter values have been substituted */ ResolvedSQL: string; /** Parameter values applied (key → resolved value) */ Parameters: Record; } /** * Result returned by the composition engine after resolving all {{query:"..."}} tokens. */ export interface CompositionResult { /** The fully resolved SQL with CTEs prepended */ ResolvedSQL: string; /** Metadata about each CTE generated */ CTEs: CompositionCTEInfo[]; /** Directed dependency graph: queryId → [dependsOnQueryIds] */ DependencyGraph: Map; /** Whether any composition tokens were found and resolved */ HasCompositions: boolean; /** True if any resolved dependency query has UsesTemplate = true (depth-first, transitive) */ AnyDependencyUsesTemplates: boolean; } /** * Parsed representation of a single {{query:"..."}} token found in SQL. */ interface ParsedCompositionToken { /** The full original token text including {{ and }} */ FullToken: string; /** Category path segments (everything before the query name) */ CategorySegments: string[]; /** The query name (last path segment) */ QueryName: string; /** Full path as written (CategorySegments joined with /) */ FullPath: string; /** Parsed parameter mappings */ Parameters: ParsedParameter[]; } /** * A single parameter from a composition reference. */ interface ParsedParameter { /** Parameter name */ Name: string; /** If quoted literal: the static value. Otherwise null. */ StaticValue: string | null; /** If bare name: the pass-through parameter name from the outer query. Otherwise null. */ PassThroughName: string | null; } /** * QueryCompositionEngine resolves {{query:"CategoryPath/QueryName(params)"}} tokens * in query SQL into Common Table Expressions (CTEs). It handles: * * - Recursive resolution of nested compositions * - Cycle detection via an in-progress set * - Parameter modes: static literals and pass-through from outer query * - Deduplication of identical query+params references * - Platform-aware SQL resolution via QueryInfo.GetPlatformSQL() * * This engine runs BEFORE Nunjucks template processing, so regular {{param}} * tokens are preserved for later substitution. */ export declare class QueryCompositionEngine { /** * Checks whether SQL contains any {{query:"..."}} composition tokens. * Use this as a fast guard before calling ResolveComposition(). * Only considers tokens outside of SQL comments. */ HasCompositionTokens(sql: string): boolean; /** * Parses all {{query:"..."}} tokens from SQL without resolving them. * Useful for dependency extraction during the save pipeline. * Only considers tokens outside of SQL comments. * * Uses MJLexer for structured tokenization instead of regex, providing * full parsing of category paths, query names, and parameter lists. * * @param sql - The SQL text to parse * @returns Array of parsed token metadata */ ParseCompositionTokens(sql: string): ParsedCompositionToken[]; /** * Resolves all {{query:"..."}} composition tokens in the given SQL into CTEs. * * @param sql - The SQL containing composition tokens * @param platform - Target database platform for SQL resolution * @param contextUser - User context for permission checks on referenced queries * @param outerParams - Parameter values from the outer/parent query (for pass-through resolution) * @param inlineDependencies - Optional inline dependency specs for transient query testing. * When provided, these are checked first before falling back to Metadata.Provider.Queries. * Inline dependencies skip governance validation (Reusable, IsApproved, UserCanRun). * @returns CompositionResult with fully resolved SQL and provenance metadata * @throws Error if a referenced query is not found, not composable, or creates a cycle */ ResolveComposition(sql: string, platform: DatabasePlatform, contextUser: UserInfo, outerParams?: Record, inlineDependencies?: QueryDependencySpec[]): CompositionResult; /** * Recursively resolves composition tokens in SQL, building up CTE entries. */ private resolveTokensRecursive; /** * Looks up a query by category path + name, checking inline dependencies first, * then falling back to the metadata provider. * * For inline dependencies, creates a synthetic QueryInfo with the SQL and flags set * appropriately. Inline queries skip governance validation (Reusable, IsApproved, etc.) * since they are inherently authorized by the caller. */ private lookupQueryWithInline; /** * Finds a matching inline dependency spec for the given token. */ private findInlineDependency; /** * Builds a synthetic IComposableQuery from an inline dependency spec. * Returns a plain object (not a BaseEntity) since we cannot construct * MJQueryEntityExtended directly. Inline queries skip governance * validation, so only the fields needed for composition resolution * (ID, Name, SQL, UsesTemplate, GetPlatformSQL) are meaningful. */ private buildSyntheticQuery; /** * Looks up a query by category path + name from QueryEngine. */ private lookupQueryFromMetadata; /** * Validates that a referenced query is eligible for composition. */ private validateQueryComposable; /** * Resolves parameter values for a composition reference. * Static values are used directly; pass-through values are looked up from outer params. */ private resolveParameters; /** * Substitutes resolved parameter values into a query's SQL. * - Static values: replaces {{paramName}} with 'value' * - Pass-through values: renames {{paramName}} to {{outerParamName}} so * the downstream Nunjucks processor can resolve it from the outer query's parameters. */ /** * Substitutes resolved parameter values into a query's SQL using the SQLParser's * MJLexer-based template manipulation methods. * * - Pass-through values: renames the inner variable to the outer variable name, * preserving any Nunjucks filter chain (e.g., `{{ lookbackDays | sqlNumber }}` → `{{ numDays | sqlNumber }}`) * - Static values: replaces the entire template expression (including filters) with a literal value */ private substituteStaticParams; /** * Builds a deduplication key for a CTE based on query ID and sorted parameter values. */ private buildDeduplicationKey; /** * Generates a SQL-safe CTE name from the query name + short hash for uniqueness. */ private generateCTEName; /** * Resolves a DatabasePlatform string to the corresponding SQLDialect instance. */ private getDialect; /** * Simple string hash for generating short, deterministic suffixes. */ private simpleHash; /** * Assembles CTE entries into a WITH clause prepended to the main SQL. * * Handles the case where a dependency query's SQL itself contains a WITH clause * (inner CTEs). SQL does not allow nested WITH clauses, so inner CTEs are "hoisted" * out as sibling CTE definitions preceding the dependency's own CTE. * * Cross-dep CTE name collisions: when two different dependencies each declare * an inner CTE with the same name (e.g. both declare `PoolNameBridge`), the * naive concatenation produces a duplicate CTE error from SQL Server. We * track allocated names across the entire WITH and rename colliding inner * CTEs (and rewrite intra-dep references to the renamed CTE) so the assembled * SQL is valid. See Skip-Brain Bug C in * `__tests__/skip-failure-regressions.test.ts` and `SKIP-QUERY-RENDERING-BUGS.md`. */ private assembleCTEs; /** * Validates that no CTE body still contains an illegal top-level ORDER BY * after stripping. Throws a diagnostic error with the dep name, category * path, and specific reason — replacing the cryptic SQL Server error * "The ORDER BY clause is invalid in views, inline functions...". */ private validateCTEBodies; /** * Strips the surrounding bracket / quote characters from a CTE name so we * can compare names case-insensitively across quoting styles. `[Foo]`, * `"Foo"`, and bare `Foo` all canonicalize to `Foo`. */ private canonicalCTEName; /** * Deconflicts inner CTE names using a SymbolTable for name allocation. * The SymbolTable guarantees uniqueness at registration time — name * collisions become impossible by construction. * * Reference rewriting uses word-boundary regex over raw SQL. It does * NOT skip string literals or comments — inner-CTE names are unlikely * to appear as quoted text or column aliases in practice. */ private deconflictInnerCTEsViaSymbolTable; /** * Replaces every reference to `oldName` (bare, [bracketed], or "quoted") * with bare `newName`. Case-insensitive. Word-boundary matched so * `MyAcronymBridge` is not affected by renaming `AcronymBridge`. * * Does not attempt to skip string literals or comments — see the note on * `deconflictInnerCTEsViaSymbolTable` for the trade-off rationale. */ private renameSQLIdentifier; /** * Extracts inner CTE definitions from SQL that starts with a WITH clause. * * Delegates to {@link SQLParser.ExtractCTEs} which uses AST parsing first * (via node-sql-parser), falling back to a paren-depth regex approach when * AST parsing fails (e.g. SQL contains Nunjucks template tokens). * * @param sql SQL starting with a WITH clause * @param dialect SQL dialect for AST parsing */ private hoistInnerCTEs; /** * Strips a trailing ORDER BY clause from SQL that will be wrapped in a CTE. * * SQL Server disallows ORDER BY inside CTEs unless TOP, OFFSET, or FOR XML is present. * PostgreSQL allows ORDER BY in CTEs, so no stripping is needed there. * * Delegates to the shared `AnalyzeTopLevelOrderBy` utility (orderByAnalyzer.ts) * which implements a 2-tier strategy: * Tier 1: AST parsing (with Nunjucks preprocessing if needed) * Tier 2: MJLexer scanner with carried lexical state across token boundaries * * When both tiers fail to strip, falls back to OFFSET 0 ROWS injection as * a last resort (semantically neutral but makes ORDER BY legal in CTEs). */ private stripTrailingOrderBy; /** * Injects OFFSET 0 ROWS after the last top-level ORDER BY clause to make it * legal in a CTE without changing the result set. */ private injectOffset0Rows; /** * Strips SQL comments from the input string so that composition tokens * inside comments are not treated as real references. * Handles both single-line (-- ...) and multi-line block comments. * Preserves string literals (single-quoted) to avoid stripping inside them. */ private stripSQLComments; } export {}; //# sourceMappingURL=queryCompositionEngine.d.ts.map