import { DatabasePlatform } from '@memberjunction/core'; import { type SQLDialect } from '@memberjunction/sql-dialect'; /** * Result of wrapping SQL with paging directives. */ export interface PagingWrappedSQL { /** SQL that returns the paged data rows */ DataSQL: string; /** SQL that returns the total row count (without paging) */ CountSQL: string; /** The computed offset (same as startRow input) */ Offset: number; /** The page size (same as maxRows input) */ PageSize: number; } /** * Handles server-side pagination for query SQL by applying platform-specific * paging clauses. * * **Data SQL** — appends OFFSET/FETCH (SQL Server) or LIMIT/OFFSET (PostgreSQL) * directly to the original SQL. The query is not wrapped in a CTE, so all column * scopes, ORDER BY references, and table aliases remain valid. TOP is stripped on * SQL Server since it conflicts with OFFSET. * * **Count SQL** — wraps the original SQL (minus ORDER BY) in a CTE and produces * `SELECT COUNT(*) AS TotalRowCount FROM [__count]`. ORDER BY is irrelevant for * counting and must be removed since SQL Server forbids it in CTEs without TOP. * * This approach eliminates the need for ORDER BY remapping (mapping column * references from the inner query scope to the outer CTE scope), which was the * primary source of paging bugs. */ export declare class QueryPagingEngine { /** * Produces paged DataSQL and CountSQL from resolved query SQL. * * @param resolvedSQL The fully-resolved SQL (after composition + Nunjucks) * @param startRow 0-based row offset * @param maxRows Maximum rows to return (page size) * @param platform Target database platform * @returns DataSQL for paged results and CountSQL for total row count */ static WrapWithPaging(resolvedSQL: string, startRow: number, maxRows: number, platform: DatabasePlatform): PagingWrappedSQL; /** * Applies a row cap to the outermost SELECT. * * `maxRows` is treated as a hard ceiling: the result is guaranteed to * return at most `maxRows` rows whenever the SQL shape can be capped * without corrupting the query. * * Strategy: * 1. Parse via AST. If the outermost SELECT has no existing cap, * inject `TOP N` (SQL Server) or `LIMIT N` (PostgreSQL). * 2. If an existing numeric `TOP`/`LIMIT` is present, reduce it to * `min(existing, maxRows)`. The tighter cap wins. * 3. If the AST recognizes the shape but can't inject (`TOP PERCENT`, * non-numeric `TOP`, `UNION`, `WITH TIES`, etc.), wrap with an * outer `SELECT TOP N * FROM (…) AS _mj_capped` (or LIMIT on PG). * 4. If the parser can't handle the input but the SQL is CTE-headed, * append `OFFSET 0 ROWS FETCH NEXT N ROWS ONLY` via {@link buildDataSQL}. * 5. Shapes that can't legally appear inside a derived table * (`FOR JSON`, `FOR XML`, `OPTION (...)`, `SELECT INTO`, * mutations) are returned unchanged — the cap is moot * (FOR JSON/XML return one row) or the validator should have * rejected them earlier (mutations, SELECT INTO). * * Non-positive, non-finite, or fractional `maxRows` are sanitized * (`<= 0` and non-finite are no-ops; fractional values are floored). */ static WrapWithMaxRows(resolvedSQL: string, maxRows: number, platform: DatabasePlatform): string; /** * Wraps `sql` in an outer SELECT that enforces the row cap. * Used when the AST recognises the shape but cannot inject the cap * cleanly, or when the AST can't parse the input but the SQL is known * to be wrap-safe (no FOR JSON/FOR XML/OPTION at top level). * * Strips any top-level ORDER BY before wrapping: ORDER BY is illegal * inside a derived table on SQL Server (unless TOP/OFFSET/FOR XML is * present), and the outer SELECT doesn't preserve inner ordering anyway. * The ORDER BY is moved to the outer SELECT so the final result retains * the intended sort order. */ private static outerWrap; /** * AST-based row-cap injection. * `capped` — `sql` contains the input with TOP/LIMIT injected * or reduced to `min(existing, cap)`. * `wrap` — AST recognized the shape but the cap can't be * safely injected inline (`TOP PERCENT`, non-numeric * `TOP`/`LIMIT`); caller should outer-wrap. * `pass-through` — shape can't be capped at all without corrupting * the query (SELECT INTO, mutation). * `unparseable` — parser could not handle the input; caller may * attempt a CTE-fallback or outer wrap. * * All AST shape inspection is delegated to {@link SQLParser} primitives — * this method contains no `node-sql-parser` field knowledge. */ private static applyMaxRowsViaAST; /** * Determines whether the given params indicate paging should be applied. */ static ShouldPage(startRow: number | undefined, maxRows: number | undefined): boolean; private static buildDataSQL; /** * Strips a TOP clause from the outermost SELECT statement. * Handles `TOP N` and `TOP (N)`, with or without DISTINCT. * Does not affect TOP in subqueries or CTEs. */ private static stripTopFromMainSelect; /** * Builds count SQL that wraps the (cap-free, ORDER-BY-free) query in a * `[__count]` CTE and selects `COUNT(*)`. A single instance-API path: * 1. `ExtractCTEs` hoists any user CTEs as siblings (a CTE body can't * contain its own WITH) — AST parse first, paren-depth regex fallback. * 2. `stripCountBody` removes the top-level ORDER BY (irrelevant for a * count, and illegal in a SQL Server CTE without TOP) and any outer * TOP / LIMIT (so the count reflects the full set, consistent with the * paged data query). */ private static buildCountSQL; /** * Strips the ORDER BY and any outer row cap (TOP on SQL Server, LIMIT on * PostgreSQL) from the statement being counted, via a single parser * round-trip — so the count reflects the full set rather than the capped * subset. Falls back to a string-based ORDER BY strip when the statement * can't be parsed (e.g. TRY_CAST, unresolved templates); the cap can't be * reliably removed without a parse, matching the prior regex behavior. */ private static stripCountBody; /** * Extracts the top-level ORDER BY clause from SQL, ignoring ORDER BY * inside subqueries, block comments, line comments, single-quoted strings, * and bracket/double-quoted identifiers. * * Preserves the public static API for existing callers and tests. */ static extractOrderBy(sql: string, dialect?: SQLDialect | string): { sqlWithoutOrder: string; orderByClause: string | null; }; /** * Strips a TOP N or TOP (N) clause from the beginning of a SELECT statement. */ static stripTopClause(sql: string): { sql: string; topRemoved: boolean; }; /** * ExtractCTEs returns CTE definitions with unquoted names (e.g. `myName AS (...)`). * Apply dialect-specific identifier quoting to the CTE name. */ private static quoteCteName; private static getDialect; } //# sourceMappingURL=queryPagingEngine.d.ts.map