import {} from "../sql-parser/nodes.js";
import { internalQueryBuilder } from "../../internal-query-builder.js";
import { getColumnName, getColumnQualifier, getIdentifierValue, isSelectAllForTable, normalizeIdentifierValue, objectNameMatches, } from "../sql-parser/ast-helpers.js";
import { visitSelectStatement, } from "../sql-parser/visitor.js";
import { normalizeSegmentedStatement } from "../sql-parser/parse.js";
import { cacheTableNameToSchemaKey, schemaKeyToCacheTableName, } from "../../../state/cache/create-schema-cache-table.js";
export const INTERNAL_STATE_VTABLE = "lix_internal_state_vtable";
const DEFAULT_ALIAS_KEY = normalizeIdentifierValue(INTERNAL_STATE_VTABLE);
const ORIGINAL_TABLE_KEY = DEFAULT_ALIAS_KEY;
const HIDDEN_SEGMENT_COLUMNS = ["_pk"];
const HIDDEN_SEGMENT_COLUMN_SET = new Set(HIDDEN_SEGMENT_COLUMNS);
const SNAPSHOT_JSON_ENTITY_PATHS = new Set(["$.id", "$.entity_id"]);
// Guarded until we can reliably map JSON paths to concrete entity identifiers.
const SNAPSHOT_ENTITY_FILTER_PUSH_ENABLED = false;
/**
 * Rewrites internal state vtable references into inline union subqueries.
 *
 * @example
 * ```ts
 * const rewritten = rewriteVtableSelects({
 *   node: statement,
 *   getCacheTables,
 *   hasOpenTransaction,
 * });
 * ```
 */
export const rewriteVtableSelects = (context) => {
    let anyChanges = false;
    const rewrittenStatements = context.statements.map((statement) => {
        const rewritten = rewriteSegmentedStatement(statement, context);
        if (rewritten !== statement) {
            anyChanges = true;
        }
        return rewritten;
    });
    return anyChanges ? rewrittenStatements : context.statements;
};
function rewriteSegmentedStatement(statement, context) {
    let changed = false;
    const segments = statement.segments.map((segment) => {
        if (segment.node_kind === "select_statement") {
            const rewritten = rewriteSelectStatement(segment, context);
            if (rewritten !== segment) {
                changed = true;
            }
            return rewritten;
        }
        if (segment.node_kind === "compound_select") {
            const rewritten = rewriteCompoundSelect(segment, context);
            if (rewritten !== segment) {
                changed = true;
            }
            return rewritten;
        }
        return segment;
    });
    if (!changed) {
        return statement;
    }
    return normalizeSegmentedStatement({
        ...statement,
        segments,
    });
}
function rewriteSelectStatement(select, context, pushdownSchemaKeys = [], pushdownPruneInheritance = false, pushdownPruneTransactions = false, pushdownFileIds = [], pushdownEntityIds = [], pushdownVersionIds = [], pushdownSnapshotFilters = []) {
    const parameters = context.parameters ?? [];
    const ctePredicateMap = collectCtePredicateMap(select, parameters, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters);
    const selectWithRewrittenCtes = rewriteSelectWithClause(select, context, ctePredicateMap, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters);
    const fallbackAliasNames = collectRelationAliasNames(selectWithRewrittenCtes);
    const fallbackSchemaSummary = collectSchemaKeyPredicates(selectWithRewrittenCtes.where_clause, fallbackAliasNames, parameters);
    const fallbackSchemaKeys = mergeStringLiterals(pushdownSchemaKeys, fallbackSchemaSummary.literals);
    const fallbackFileSummary = collectFileIdPredicates(selectWithRewrittenCtes.where_clause, fallbackAliasNames, parameters);
    const fallbackFileIds = mergeStringLiterals(pushdownFileIds, fallbackFileSummary.literals);
    const fallbackEntitySummary = collectEntityIdPredicates(selectWithRewrittenCtes.where_clause, fallbackAliasNames, parameters);
    const fallbackEntityIds = mergeStringLiterals(pushdownEntityIds, fallbackEntitySummary.literals);
    const fallbackVersionSummary = collectVersionIdPredicates(selectWithRewrittenCtes.where_clause, fallbackAliasNames, parameters);
    const fallbackVersionIds = mergeStringLiterals(pushdownVersionIds, fallbackVersionSummary.literals);
    const fallbackSnapshotSummary = collectSnapshotJsonPredicates(selectWithRewrittenCtes.where_clause, fallbackAliasNames, parameters);
    const fallbackSnapshotFilters = mergeSnapshotFilters(pushdownSnapshotFilters, fallbackSnapshotSummary);
    const fallbackPruneInheritance = pushdownPruneInheritance ||
        shouldPruneInheritance(selectWithRewrittenCtes.where_clause, fallbackAliasNames, parameters);
    const fallbackPruneTransactions = pushdownPruneTransactions ||
        shouldPruneTransactionSegment(selectWithRewrittenCtes.where_clause, fallbackAliasNames, parameters);
    const subqueryPredicateMap = collectSubqueryPredicateMap(selectWithRewrittenCtes, parameters, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters);
    const withRewrittenSubqueries = visitSelectStatement(selectWithRewrittenCtes, {
        subquery(node, visitContext) {
            const parentKind = visitContext.parent?.node_kind;
            const aliasValue = getIdentifierValue(node.alias);
            const normalizedAlias = aliasValue
                ? normalizeIdentifierValue(aliasValue)
                : DEFAULT_ALIAS_KEY;
            const predicateMetadata = parentKind === "from_clause" || parentKind === "join_clause"
                ? subqueryPredicateMap.get(normalizedAlias)
                : undefined;
            const nextSchemaKeys = predicateMetadata?.schemaKeys ?? fallbackSchemaKeys;
            const nextPrune = predicateMetadata?.pruneInheritance ?? fallbackPruneInheritance;
            const nextPruneTransactions = predicateMetadata?.pruneTransactionSegment ??
                fallbackPruneTransactions;
            const nextFileIds = predicateMetadata?.fileIds ?? fallbackFileIds;
            const nextEntityIds = predicateMetadata?.entityIds ?? fallbackEntityIds;
            const nextVersionIds = predicateMetadata?.versionIds ?? fallbackVersionIds;
            const nextSnapshotFilters = predicateMetadata?.snapshotFilters ?? fallbackSnapshotFilters;
            const rewrittenStatement = node.statement.node_kind === "compound_select"
                ? rewriteCompoundSelect(node.statement, context, nextSchemaKeys, nextPrune, nextPruneTransactions, nextFileIds, nextEntityIds, nextVersionIds, nextSnapshotFilters)
                : rewriteSelectStatement(node.statement, context, nextSchemaKeys, nextPrune, nextPruneTransactions, nextFileIds, nextEntityIds, nextVersionIds, nextSnapshotFilters);
            if (rewrittenStatement !== node.statement) {
                return {
                    ...node,
                    statement: rewrittenStatement,
                };
            }
            return node;
        },
    });
    const references = collectInternalStateReferences(withRewrittenSubqueries);
    if (references.length === 0) {
        return withRewrittenSubqueries;
    }
    const aliasList = references
        .map((reference) => reference.alias)
        .filter((alias) => alias !== null);
    const tableNamesForTrace = new Set([
        DEFAULT_ALIAS_KEY,
        ORIGINAL_TABLE_KEY,
        ...references
            .map((reference) => reference.normalizedAlias)
            .filter((alias) => alias !== null),
    ]);
    const schemaSummary = collectSchemaKeyPredicates(withRewrittenSubqueries.where_clause, tableNamesForTrace, parameters);
    const mergedSchemaKeys = mergeStringLiterals(pushdownSchemaKeys, schemaSummary.literals);
    const schemaSummaryForTrace = {
        count: schemaSummary.count,
        literals: mergedSchemaKeys,
    };
    const projectionKind = determineProjectionKind(withRewrittenSubqueries.projection, tableNamesForTrace);
    const selectedColumnsSummaryForTrace = collectSelectedColumns(withRewrittenSubqueries, tableNamesForTrace);
    const metadata = buildInlineMetadata(withRewrittenSubqueries, references, parameters, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters);
    const rewritten = visitSelectStatement(withRewrittenSubqueries, createInlineVisitor(metadata, context));
    if (context.trace) {
        const entry = buildTraceEntry({
            aliasList,
            projectionKind,
            schemaSummary: schemaSummaryForTrace,
            selectedColumns: selectedColumnsSummaryForTrace.selectedColumns,
        });
        context.trace.push(entry);
    }
    return rewritten;
}
function rewriteCompoundSelect(compound, context, pushdownSchemaKeys = [], pushdownPruneInheritance = false, pushdownPruneTransactions = false, pushdownFileIds = [], pushdownEntityIds = [], pushdownVersionIds = [], pushdownSnapshotFilters = []) {
    const rewrittenWithClause = compound.with_clause
        ? rewriteWithClauseNode(compound.with_clause, context, new Map(), pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters)
        : null;
    let changed = false;
    const first = rewriteSelectStatement(compound.first, context, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters);
    if (first !== compound.first) {
        changed = true;
    }
    const branches = compound.compounds.map((branch) => {
        const rewritten = rewriteSelectStatement(branch.select, context, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters);
        if (rewritten !== branch.select) {
            changed = true;
            return {
                ...branch,
                select: rewritten,
            };
        }
        return branch;
    });
    if (!changed) {
        return rewrittenWithClause === compound.with_clause
            ? compound
            : {
                ...compound,
                with_clause: rewrittenWithClause,
            };
    }
    return {
        ...compound,
        first,
        compounds: branches,
        with_clause: rewrittenWithClause ?? compound.with_clause,
    };
}
function collectInternalStateReferences(select) {
    const references = [];
    for (const clause of select.from_clauses) {
        const relation = clause.relation;
        if (relation.node_kind === "table_reference") {
            if (isInternalStateTable(relation)) {
                references.push({
                    relation,
                    alias: getIdentifierValue(relation.alias),
                    normalizedAlias: normalizeAlias(relation.alias),
                });
            }
        }
        for (const join of clause.joins) {
            const joinRelation = join.relation;
            if (joinRelation.node_kind === "table_reference" &&
                isInternalStateTable(joinRelation)) {
                references.push({
                    relation: joinRelation,
                    alias: getIdentifierValue(joinRelation.alias),
                    normalizedAlias: normalizeAlias(joinRelation.alias),
                });
            }
        }
    }
    return references;
}
function isInternalStateTable(table) {
    return objectNameMatches(table.name, INTERNAL_STATE_VTABLE);
}
function normalizeAlias(alias) {
    const value = getIdentifierValue(alias);
    return value ? normalizeIdentifierValue(value) : null;
}
function collectSubqueryPredicateMap(select, parameters, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters) {
    const map = new Map();
    const register = (relation, joinFilter) => {
        if (relation.node_kind !== "subquery") {
            return;
        }
        const aliasValue = getIdentifierValue(relation.alias);
        if (!aliasValue) {
            return;
        }
        const normalizedAlias = normalizeIdentifierValue(aliasValue);
        const tableNames = new Set([
            normalizedAlias,
            DEFAULT_ALIAS_KEY,
            ORIGINAL_TABLE_KEY,
        ]);
        const whereSummary = collectSchemaKeyPredicates(select.where_clause, tableNames, parameters);
        const joinSummary = collectSchemaKeyPredicatesFromExpression(joinFilter, tableNames, parameters, false);
        const localKeys = mergeStringLiterals(whereSummary.literals, joinSummary.literals);
        const fileWhereSummary = collectFileIdPredicates(select.where_clause, tableNames, parameters);
        const fileJoinSummary = collectFileIdPredicatesFromExpression(joinFilter, tableNames, parameters, false);
        const localFileIds = mergeStringLiterals(fileWhereSummary.literals, fileJoinSummary.literals);
        const entityWhereSummary = collectEntityIdPredicates(select.where_clause, tableNames, parameters);
        const entityJoinSummary = collectEntityIdPredicatesFromExpression(joinFilter, tableNames, parameters, false);
        const localEntityIds = mergeStringLiterals(entityWhereSummary.literals, entityJoinSummary.literals);
        const snapshotWhereSummary = collectSnapshotJsonPredicates(select.where_clause, tableNames, parameters);
        const snapshotJoinSummary = collectSnapshotJsonPredicatesFromExpression(joinFilter, tableNames, parameters, false);
        const localSnapshotFilters = mergeSnapshotFilters(snapshotWhereSummary, snapshotJoinSummary);
        const versionWhereSummary = collectVersionIdPredicates(select.where_clause, tableNames, parameters);
        const versionJoinSummary = collectVersionIdPredicatesFromExpression(joinFilter, tableNames, parameters, false);
        const localVersionIds = mergeStringLiterals(versionWhereSummary.literals, versionJoinSummary.literals);
        const existing = map.get(normalizedAlias);
        const merged = mergeStringLiterals(existing?.schemaKeys ?? [], pushdownSchemaKeys, localKeys);
        const mergedFileIds = mergeStringLiterals(existing?.fileIds ?? [], pushdownFileIds, localFileIds);
        const mergedEntityIds = mergeStringLiterals(existing?.entityIds ?? [], pushdownEntityIds, localEntityIds);
        const mergedSnapshotFilters = mergeSnapshotFilters(existing?.snapshotFilters ?? [], pushdownSnapshotFilters, localSnapshotFilters);
        const mergedVersionIds = mergeStringLiterals(existing?.versionIds ?? [], pushdownVersionIds, localVersionIds);
        const pruneFromWhere = requiresNullPredicateForColumn(select.where_clause, tableNames, parameters, true);
        const pruneFromJoin = requiresNullPredicateForColumn(joinFilter, tableNames, parameters, false);
        const combinedPrune = (existing?.pruneInheritance ?? false) ||
            pushdownPruneInheritance ||
            pruneFromWhere ||
            pruneFromJoin;
        const pruneTxnFromWhere = excludesColumnLiteralFromExpression(select.where_clause, tableNames, parameters, "source_tag", "T", true);
        const pruneTxnFromJoin = excludesColumnLiteralFromExpression(joinFilter, tableNames, parameters, "source_tag", "T", false);
        const combinedTxnPrune = (existing?.pruneTransactionSegment ?? false) ||
            pushdownPruneTransactions ||
            pruneTxnFromWhere ||
            pruneTxnFromJoin;
        const finalFileIds = shouldPushFileFilter(merged, mergedFileIds)
            ? mergedFileIds
            : [];
        const finalEntityIds = shouldPushEntityFilter(merged, mergedEntityIds)
            ? mergedEntityIds
            : [];
        const finalSnapshotFilters = shouldPushSnapshotFilters(merged, mergedSnapshotFilters)
            ? mergedSnapshotFilters
            : [];
        if (merged.length > 0 ||
            finalFileIds.length > 0 ||
            finalEntityIds.length > 0 ||
            finalSnapshotFilters.length > 0 ||
            mergedVersionIds.length > 0 ||
            combinedPrune ||
            combinedTxnPrune) {
            map.set(normalizedAlias, {
                schemaKeys: merged,
                fileIds: finalFileIds,
                entityIds: finalEntityIds,
                snapshotFilters: finalSnapshotFilters,
                versionIds: mergedVersionIds,
                pruneInheritance: combinedPrune,
                pruneTransactionSegment: combinedTxnPrune,
            });
        }
    };
    for (const clause of select.from_clauses) {
        register(clause.relation, null);
        for (const join of clause.joins) {
            register(join.relation, join.on_expression);
        }
    }
    return map;
}
function collectRelationAliasNames(select) {
    const aliases = new Set([DEFAULT_ALIAS_KEY, ORIGINAL_TABLE_KEY]);
    const addAlias = (alias) => {
        const value = getIdentifierValue(alias);
        if (value) {
            aliases.add(normalizeIdentifierValue(value));
        }
    };
    for (const clause of select.from_clauses) {
        addAlias(extractRelationAlias(clause.relation));
        for (const join of clause.joins) {
            addAlias(extractRelationAlias(join.relation));
        }
    }
    return aliases;
}
function extractRelationAlias(relation) {
    if ("alias" in relation) {
        const alias = relation.alias;
        return alias ?? null;
    }
    return null;
}
function collectCtePredicateMap(select, parameters, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters) {
    const withClause = select.with_clause;
    if (!withClause || withClause.ctes.length === 0) {
        return new Map();
    }
    const cteNames = new Set(withClause.ctes
        .map((cte) => normalizeIdentifierValue(cte.name.value))
        .filter((name) => !!name));
    if (cteNames.size === 0) {
        return new Map();
    }
    const map = new Map();
    const register = (table, joinFilter) => {
        const normalizedName = normalizeObjectName(table.name);
        if (!normalizedName || !cteNames.has(normalizedName)) {
            return;
        }
        const aliasValue = getIdentifierValue(table.alias);
        const tableNames = new Set([
            normalizedName,
            DEFAULT_ALIAS_KEY,
            ORIGINAL_TABLE_KEY,
        ]);
        if (aliasValue) {
            tableNames.add(normalizeIdentifierValue(aliasValue));
        }
        const whereSummary = collectSchemaKeyPredicates(select.where_clause, tableNames, parameters);
        const joinSummary = collectSchemaKeyPredicatesFromExpression(joinFilter, tableNames, parameters, false);
        const localKeys = mergeStringLiterals(whereSummary.literals, joinSummary.literals);
        const fileWhereSummary = collectFileIdPredicates(select.where_clause, tableNames, parameters);
        const fileJoinSummary = collectFileIdPredicatesFromExpression(joinFilter, tableNames, parameters, false);
        const localFileIds = mergeStringLiterals(fileWhereSummary.literals, fileJoinSummary.literals);
        const entityWhereSummary = collectEntityIdPredicates(select.where_clause, tableNames, parameters);
        const entityJoinSummary = collectEntityIdPredicatesFromExpression(joinFilter, tableNames, parameters, false);
        const localEntityIds = mergeStringLiterals(entityWhereSummary.literals, entityJoinSummary.literals);
        const snapshotWhereSummary = collectSnapshotJsonPredicates(select.where_clause, tableNames, parameters);
        const snapshotJoinSummary = collectSnapshotJsonPredicatesFromExpression(joinFilter, tableNames, parameters, false);
        const localSnapshotFilters = mergeSnapshotFilters(snapshotWhereSummary, snapshotJoinSummary);
        const versionWhereSummary = collectVersionIdPredicates(select.where_clause, tableNames, parameters);
        const versionJoinSummary = collectVersionIdPredicatesFromExpression(joinFilter, tableNames, parameters, false);
        const localVersionIds = mergeStringLiterals(versionWhereSummary.literals, versionJoinSummary.literals);
        const existing = map.get(normalizedName);
        const mergedSchemaKeys = mergeStringLiterals(existing?.schemaKeys ?? [], pushdownSchemaKeys, localKeys);
        const mergedFileIds = mergeStringLiterals(existing?.fileIds ?? [], pushdownFileIds, localFileIds);
        const mergedEntityIds = mergeStringLiterals(existing?.entityIds ?? [], pushdownEntityIds, localEntityIds);
        const mergedSnapshotFilters = mergeSnapshotFilters(existing?.snapshotFilters ?? [], pushdownSnapshotFilters, localSnapshotFilters);
        const mergedVersionIds = mergeStringLiterals(existing?.versionIds ?? [], pushdownVersionIds, localVersionIds);
        const pruneFromWhere = requiresNullPredicateForColumn(select.where_clause, tableNames, parameters, true);
        const pruneFromJoin = requiresNullPredicateForColumn(joinFilter, tableNames, parameters, false);
        const combinedPrune = (existing?.pruneInheritance ?? false) ||
            pushdownPruneInheritance ||
            pruneFromWhere ||
            pruneFromJoin;
        const pruneTxnFromWhere = excludesColumnLiteralFromExpression(select.where_clause, tableNames, parameters, "source_tag", "T", true);
        const pruneTxnFromJoin = excludesColumnLiteralFromExpression(joinFilter, tableNames, parameters, "source_tag", "T", false);
        const combinedTxnPrune = (existing?.pruneTransactionSegment ?? false) ||
            pushdownPruneTransactions ||
            pruneTxnFromWhere ||
            pruneTxnFromJoin;
        const finalFileIds = shouldPushFileFilter(mergedSchemaKeys, mergedFileIds)
            ? mergedFileIds
            : [];
        const finalEntityIds = shouldPushEntityFilter(mergedSchemaKeys, mergedEntityIds)
            ? mergedEntityIds
            : [];
        const finalSnapshotFilters = shouldPushSnapshotFilters(mergedSchemaKeys, mergedSnapshotFilters)
            ? mergedSnapshotFilters
            : [];
        const entry = {
            schemaKeys: mergedSchemaKeys,
            fileIds: finalFileIds,
            entityIds: finalEntityIds,
            snapshotFilters: finalSnapshotFilters,
            versionIds: mergedVersionIds,
            pruneInheritance: combinedPrune,
            pruneTransactionSegment: combinedTxnPrune,
        };
        map.set(normalizedName, entry);
    };
    for (const clause of select.from_clauses) {
        if (clause.relation.node_kind === "table_reference") {
            register(clause.relation, null);
        }
        for (const join of clause.joins) {
            if (join.relation.node_kind === "table_reference") {
                register(join.relation, join.on_expression);
            }
        }
    }
    return map;
}
function rewriteSelectWithClause(select, context, predicateMap, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters) {
    if (!select.with_clause) {
        return select;
    }
    const rewritten = rewriteWithClauseNode(select.with_clause, context, predicateMap, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters);
    if (rewritten === select.with_clause) {
        return select;
    }
    return {
        ...select,
        with_clause: rewritten,
    };
}
function rewriteWithClauseNode(withClause, context, predicateMap, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters) {
    let changed = false;
    const rewrittenCtes = withClause.ctes.map((cte) => {
        const cteName = normalizeIdentifierValue(cte.name.value);
        const metadata = cteName ? predicateMap.get(cteName) : undefined;
        const nextSchemaKeys = metadata?.schemaKeys ?? pushdownSchemaKeys;
        const nextPrune = metadata?.pruneInheritance ?? pushdownPruneInheritance;
        const nextPruneTransactions = metadata?.pruneTransactionSegment ?? pushdownPruneTransactions;
        const nextFileIds = metadata?.fileIds ?? pushdownFileIds;
        const nextEntityIds = metadata?.entityIds ?? pushdownEntityIds;
        const nextVersionIds = metadata?.versionIds ?? pushdownVersionIds;
        const nextSnapshotFilters = metadata?.snapshotFilters ?? pushdownSnapshotFilters;
        const statement = cte.statement.node_kind === "compound_select"
            ? rewriteCompoundSelect(cte.statement, context, nextSchemaKeys, nextPrune, nextPruneTransactions, nextFileIds, nextEntityIds, nextVersionIds, nextSnapshotFilters)
            : rewriteSelectStatement(cte.statement, context, nextSchemaKeys, nextPrune, nextPruneTransactions, nextFileIds, nextEntityIds, nextVersionIds, nextSnapshotFilters);
        if (statement !== cte.statement) {
            changed = true;
            return {
                ...cte,
                statement,
            };
        }
        return cte;
    });
    if (!changed) {
        return withClause;
    }
    return {
        ...withClause,
        ctes: rewrittenCtes,
    };
}
function buildInlineMetadata(select, references, parameters, pushdownSchemaKeys, pushdownPruneInheritance, pushdownPruneTransactions, pushdownFileIds, pushdownEntityIds, pushdownVersionIds, pushdownSnapshotFilters) {
    const metadata = new Map();
    for (const reference of references) {
        const aliasKey = reference.normalizedAlias ?? DEFAULT_ALIAS_KEY;
        if (metadata.has(aliasKey)) {
            continue;
        }
        const tableNames = new Set([
            aliasKey,
            DEFAULT_ALIAS_KEY,
            ORIGINAL_TABLE_KEY,
        ]);
        const summary = collectSchemaKeyPredicates(select.where_clause, tableNames, parameters);
        const schemaKeys = mergeStringLiterals(pushdownSchemaKeys, summary.literals);
        const fileSummary = collectFileIdPredicates(select.where_clause, tableNames, parameters);
        const mergedFileIds = mergeStringLiterals(pushdownFileIds, fileSummary.literals);
        const entitySummary = collectEntityIdPredicates(select.where_clause, tableNames, parameters);
        const mergedEntityIds = mergeStringLiterals(pushdownEntityIds, entitySummary.literals);
        const versionSummary = collectVersionIdPredicates(select.where_clause, tableNames, parameters);
        const mergedVersionIds = mergeStringLiterals(pushdownVersionIds, versionSummary.literals);
        const snapshotSummary = collectSnapshotJsonPredicates(select.where_clause, tableNames, parameters);
        const mergedSnapshotFilters = mergeSnapshotFilters(pushdownSnapshotFilters, snapshotSummary);
        const fileIds = shouldPushFileFilter(schemaKeys, mergedFileIds)
            ? mergedFileIds
            : [];
        const entityIds = shouldPushEntityFilter(schemaKeys, mergedEntityIds)
            ? mergedEntityIds
            : [];
        const columnSummary = collectSelectedColumns(select, tableNames);
        const snapshotFilters = shouldPushSnapshotFilters(schemaKeys, mergedSnapshotFilters)
            ? mergedSnapshotFilters
            : [];
        const pruneInheritance = pushdownPruneInheritance ||
            shouldPruneInheritance(select.where_clause, tableNames, parameters);
        const pruneTransactionSegment = pushdownPruneTransactions ||
            shouldPruneTransactionSegment(select.where_clause, tableNames, parameters);
        metadata.set(aliasKey, {
            schemaKeys,
            fileIds,
            entityIds,
            snapshotFilters,
            versionIds: mergedVersionIds,
            selectedColumns: columnSummary.selectedColumns,
            hiddenColumns: new Set(columnSummary.requestedHiddenColumns),
            supportColumns: new Set(columnSummary.supportColumns),
            pruneInheritance,
            pruneTransactionSegment,
        });
    }
    return metadata;
}
function shouldPruneInheritance(whereClause, tableNames, parameters) {
    return requiresNullPredicateForColumn(whereClause, tableNames, parameters, true);
}
function shouldPruneTransactionSegment(whereClause, tableNames, parameters) {
    return excludesColumnLiteralFromExpression(whereClause, tableNames, parameters, "source_tag", "T", true);
}
function requiresNullPredicateForColumn(expression, tableNames, parameters, allowUnqualified) {
    if (!expression) {
        return false;
    }
    const context = {
        tableNames,
        parameters,
        allowUnqualified,
    };
    return expressionRequiresNullForColumn(expression, "inherited_from_version_id", context);
}
function excludesColumnLiteralFromExpression(expression, tableNames, parameters, columnName, literal, allowUnqualified) {
    if (!expression) {
        return false;
    }
    const context = {
        tableNames,
        parameters,
        allowUnqualified,
    };
    return expressionExcludesColumnLiteral(expression, columnName, literal, context);
}
function expressionExcludesColumnLiteral(expression, columnName, literal, context) {
    if ("sql_text" in expression) {
        return false;
    }
    switch (expression.node_kind) {
        case "grouped_expression":
            return expressionExcludesColumnLiteral(expression.expression, columnName, literal, context);
        case "binary_expression":
            if (expression.operator === "and") {
                return (expressionExcludesColumnLiteral(expression.left, columnName, literal, context) ||
                    expressionExcludesColumnLiteral(expression.right, columnName, literal, context));
            }
            if (expression.operator === "or") {
                return (expressionExcludesColumnLiteral(expression.left, columnName, literal, context) &&
                    expressionExcludesColumnLiteral(expression.right, columnName, literal, context));
            }
            return (columnComparisonExcludes(expression, expression.left, expression.right, columnName, literal, context) ||
                columnComparisonExcludes(expression, expression.right, expression.left, columnName, literal, context));
        case "in_list_expression": {
            if (!isColumnReference(expression.operand, context, columnName)) {
                return false;
            }
            const values = expression.items
                .map((item) => extractStringLiteral(item, context))
                .filter((value) => typeof value === "string");
            if (values.length === 0) {
                return false;
            }
            if (expression.negated) {
                return values.includes(literal);
            }
            return !values.includes(literal);
        }
        default:
            return false;
    }
}
function columnComparisonExcludes(expression, columnSide, valueSide, columnName, literal, context) {
    if (!isColumnReference(columnSide, context, columnName)) {
        return false;
    }
    const value = extractStringLiteral(valueSide, context);
    if (typeof value !== "string") {
        return false;
    }
    switch (expression.operator) {
        case "=":
        case "is":
            return value !== literal;
        case "!=":
        case "<>":
        case "is_not":
            return value === literal;
        default:
            return false;
    }
}
function extractStringLiteral(expression, context) {
    const node = unwrapExpression(expression);
    if ("sql_text" in node) {
        return null;
    }
    if (node.node_kind === "literal") {
        return typeof node.value === "string" ? node.value : null;
    }
    if (node.node_kind === "parameter") {
        return resolveParameterSchemaLiteral(node, context.parameters);
    }
    return null;
}
function createInlineVisitor(metadata, context) {
    return {
        table_reference(node) {
            if (!isInternalStateTable(node)) {
                return;
            }
            const aliasValue = getIdentifierValue(node.alias);
            const aliasKey = aliasValue
                ? normalizeIdentifierValue(aliasValue)
                : DEFAULT_ALIAS_KEY;
            const metadataEntry = metadata.get(aliasKey);
            if (!metadataEntry) {
                return;
            }
            const aliasForSql = aliasValue ??
                getIdentifierValue(node.name.parts.at(-1) ?? null) ??
                INTERNAL_STATE_VTABLE;
            const inlineSql = buildVtableSelectRewrite({
                schemaKeys: metadataEntry.schemaKeys,
                fileIds: metadataEntry.fileIds,
                entityIds: metadataEntry.entityIds,
                snapshotFilters: metadataEntry.snapshotFilters,
                versionIds: metadataEntry.versionIds,
                cacheTables: context.getCacheTables(),
                selectedColumns: metadataEntry.selectedColumns,
                hiddenColumns: metadataEntry.hiddenColumns,
                supportColumns: metadataEntry.supportColumns,
                hasOpenTransaction: context.hasOpenTransaction(),
                pruneInheritance: metadataEntry.pruneInheritance,
                pruneTransactionSegment: metadataEntry.pruneTransactionSegment,
                versionInheritance: context.getVersionInheritance
                    ? context.getVersionInheritance()
                    : undefined,
            }).trimEnd();
            const fragment = {
                node_kind: "raw_fragment",
                sql_text: `${inlineSql} AS ${quoteIdentifier(aliasForSql)}`,
            };
            return fragment;
        },
    };
}
function collectSchemaKeyPredicates(whereClause, tableNames, parameters, allowUnqualified = true) {
    return collectColumnPredicates(whereClause, tableNames, parameters, "schema_key", allowUnqualified);
}
function collectSchemaKeyPredicatesFromExpression(expression, tableNames, parameters, allowUnqualified) {
    return collectColumnPredicatesFromExpression(expression, tableNames, parameters, "schema_key", allowUnqualified);
}
function collectFileIdPredicates(whereClause, tableNames, parameters, allowUnqualified = true) {
    return collectColumnPredicates(whereClause, tableNames, parameters, "file_id", allowUnqualified);
}
function collectFileIdPredicatesFromExpression(expression, tableNames, parameters, allowUnqualified) {
    return collectColumnPredicatesFromExpression(expression, tableNames, parameters, "file_id", allowUnqualified);
}
function collectEntityIdPredicates(whereClause, tableNames, parameters, allowUnqualified = true) {
    return collectColumnPredicates(whereClause, tableNames, parameters, "entity_id", allowUnqualified);
}
function collectEntityIdPredicatesFromExpression(expression, tableNames, parameters, allowUnqualified) {
    return collectColumnPredicatesFromExpression(expression, tableNames, parameters, "entity_id", allowUnqualified);
}
function collectVersionIdPredicates(whereClause, tableNames, parameters, allowUnqualified = true) {
    return collectColumnPredicates(whereClause, tableNames, parameters, "version_id", allowUnqualified);
}
function collectVersionIdPredicatesFromExpression(expression, tableNames, parameters, allowUnqualified) {
    return collectColumnPredicatesFromExpression(expression, tableNames, parameters, "version_id", allowUnqualified);
}
function collectSnapshotJsonPredicates(whereClause, tableNames, parameters, allowUnqualified = true) {
    if (!whereClause) {
        return [];
    }
    const context = {
        tableNames,
        parameters,
        allowUnqualified,
    };
    const summary = new Map();
    collectSnapshotPredicatesFromExpression(whereClause, context, summary);
    return snapshotSummaryToPredicates(summary);
}
function collectSnapshotJsonPredicatesFromExpression(expression, tableNames, parameters, allowUnqualified) {
    if (!expression) {
        return [];
    }
    const context = {
        tableNames,
        parameters,
        allowUnqualified,
    };
    const summary = new Map();
    collectSnapshotPredicatesFromExpression(expression, context, summary);
    return snapshotSummaryToPredicates(summary);
}
function collectSnapshotPredicatesFromExpression(expression, context, summary) {
    if ("sql_text" in expression) {
        return;
    }
    switch (expression.node_kind) {
        case "grouped_expression":
            collectSnapshotPredicatesFromExpression(expression.expression, context, summary);
            return;
        case "binary_expression": {
            if (isLogicalOperator(expression.operator)) {
                collectSnapshotPredicatesFromExpression(expression.left, context, summary);
                collectSnapshotPredicatesFromExpression(expression.right, context, summary);
                return;
            }
            if (!isEqualityOperator(expression.operator)) {
                return;
            }
            const leftPath = extractSnapshotJsonPath(expression.left, context);
            if (leftPath) {
                incrementSnapshotSummaryWithOperand(summary, leftPath, expression.right, context);
            }
            const rightPath = extractSnapshotJsonPath(expression.right, context);
            if (rightPath) {
                incrementSnapshotSummaryWithOperand(summary, rightPath, expression.left, context);
            }
            return;
        }
        case "in_list_expression": {
            if (expression.negated) {
                return;
            }
            const path = extractSnapshotJsonPath(expression.operand, context);
            if (!path) {
                return;
            }
            for (const item of expression.items) {
                incrementSnapshotSummaryWithOperand(summary, path, item, context);
            }
            return;
        }
        case "unary_expression":
            collectSnapshotPredicatesFromExpression(expression.operand, context, summary);
            return;
        default:
            return;
    }
}
function incrementSnapshotSummaryWithOperand(summary, path, operand, context) {
    const value = extractStringLiteral(operand, context);
    if (typeof value !== "string") {
        return;
    }
    const existing = summary.get(path) ?? new Set();
    existing.add(value);
    summary.set(path, existing);
}
function snapshotSummaryToPredicates(summary) {
    return Array.from(summary.entries()).map(([path, values]) => ({
        path,
        values: Array.from(values),
    }));
}
function extractSnapshotJsonPath(expression, context) {
    const node = unwrapExpression(expression);
    if ("sql_text" in node) {
        return null;
    }
    switch (node.node_kind) {
        case "grouped_expression":
            return extractSnapshotJsonPath(node.expression, context);
        case "function_call": {
            const functionName = node.name.value.toLowerCase();
            if (functionName !== "json_extract") {
                return null;
            }
            const [targetArg, pathArg] = node.arguments;
            if (!targetArg ||
                !pathArg ||
                targetArg.node_kind === "all_columns" ||
                pathArg.node_kind === "all_columns") {
                return null;
            }
            const referencedColumn = resolveReferencedColumn(targetArg, context);
            if (referencedColumn !== "snapshot_content") {
                return null;
            }
            return extractLiteralString(pathArg, context.parameters);
        }
        default:
            return null;
    }
}
function collectColumnPredicates(whereClause, tableNames, parameters, columnName, allowUnqualified = true) {
    if (!whereClause) {
        return { count: 0, literals: [] };
    }
    const context = {
        tableNames,
        parameters,
        allowUnqualified,
    };
    return visitExpression(whereClause, context, columnName);
}
function collectColumnPredicatesFromExpression(expression, tableNames, parameters, columnName, allowUnqualified) {
    if (!expression) {
        return { count: 0, literals: [] };
    }
    const context = {
        tableNames,
        parameters,
        allowUnqualified,
    };
    return visitExpression(expression, context, columnName);
}
function expressionRequiresNullForColumn(expression, columnName, context) {
    if ("sql_text" in expression) {
        return false;
    }
    switch (expression.node_kind) {
        case "grouped_expression":
            return expressionRequiresNullForColumn(expression.expression, columnName, context);
        case "binary_expression":
            if (expression.operator === "and") {
                return (expressionRequiresNullForColumn(expression.left, columnName, context) ||
                    expressionRequiresNullForColumn(expression.right, columnName, context));
            }
            if (expression.operator === "or") {
                return (expressionRequiresNullForColumn(expression.left, columnName, context) &&
                    expressionRequiresNullForColumn(expression.right, columnName, context));
            }
            if (expression.operator === "is") {
                const leftMatches = isColumnReference(expression.left, context, columnName);
                const rightMatches = isColumnReference(expression.right, context, columnName);
                if ((leftMatches &&
                    isNullLiteralExpression(expression.right, context.parameters)) ||
                    (rightMatches &&
                        isNullLiteralExpression(expression.left, context.parameters))) {
                    return true;
                }
            }
            return false;
        default:
            return false;
    }
}
function visitExpression(expression, context, columnName) {
    if ("sql_text" in expression) {
        return { count: 0, literals: [] };
    }
    switch (expression.node_kind) {
        case "grouped_expression":
            return visitExpression(expression.expression, context, columnName);
        case "binary_expression":
            return visitBinaryExpression(expression, context, columnName);
        case "unary_expression":
            return visitExpression(expression.operand, context, columnName);
        case "in_list_expression":
            return visitInListExpression(expression, context, columnName);
        case "between_expression":
            return visitBetweenExpression(expression, context, columnName);
        case "column_reference":
        case "literal":
        case "parameter":
        default:
            return { count: 0, literals: [] };
    }
}
function visitBinaryExpression(expression, context, columnName) {
    if (isLogicalOperator(expression.operator)) {
        return mergeSummaries(visitExpression(expression.left, context, columnName), visitExpression(expression.right, context, columnName));
    }
    if (!isEqualityOperator(expression.operator)) {
        return { count: 0, literals: [] };
    }
    let summary = {
        count: 0,
        literals: [],
    };
    const leftMatches = isColumnReference(expression.left, context, columnName);
    const rightMatches = isColumnReference(expression.right, context, columnName);
    if (leftMatches) {
        summary = incrementSummaryWithOperand(summary, expression.right, context);
    }
    if (rightMatches) {
        summary = incrementSummaryWithOperand(summary, expression.left, context);
    }
    return summary;
}
function visitInListExpression(expression, context, columnName) {
    if (!isColumnReference(expression.operand, context, columnName)) {
        return { count: 0, literals: [] };
    }
    let summary = {
        count: 1,
        literals: [],
    };
    for (const item of expression.items) {
        summary = incrementSummaryWithOperand(summary, item, context);
    }
    return summary;
}
function visitBetweenExpression(expression, context, columnName) {
    if (!isColumnReference(expression.operand, context, columnName)) {
        return { count: 0, literals: [] };
    }
    let summary = {
        count: 1,
        literals: [],
    };
    summary = incrementSummaryWithOperand(summary, expression.start, context);
    summary = incrementSummaryWithOperand(summary, expression.end, context);
    return summary;
}
function incrementSummaryWithOperand(summary, operand, context) {
    const next = { ...summary };
    const node = unwrapExpression(operand);
    switch (node.node_kind) {
        case "literal":
            if (typeof node.value === "string") {
                next.literals.push(node.value);
            }
            break;
        case "parameter": {
            const resolved = resolveParameterSchemaLiteral(node, context.parameters);
            if (typeof resolved === "string") {
                next.literals.push(resolved);
            }
            break;
        }
        default:
            break;
    }
    next.count += 1;
    return next;
}
function isNullLiteralExpression(expression, parameters) {
    const node = unwrapExpression(expression);
    if ("sql_text" in node) {
        return false;
    }
    if (node.node_kind === "literal") {
        return node.value === null;
    }
    if (node.node_kind === "parameter") {
        return resolveParameterValue(node, parameters) === null;
    }
    return false;
}
function resolveParameterSchemaLiteral(parameter, parameters) {
    const value = resolveParameterValue(parameter, parameters);
    return typeof value === "string" ? value : null;
}
function resolveParameterValue(parameter, parameters) {
    const index = parameter.position;
    if (typeof index === "number" && index >= 0 && index < parameters.length) {
        return parameters[index];
    }
    return undefined;
}
function unwrapExpression(expression) {
    if ("sql_text" in expression) {
        return expression;
    }
    if (expression.node_kind === "grouped_expression") {
        return unwrapExpression(expression.expression);
    }
    return expression;
}
function isColumnReference(expression, context, expectedColumn) {
    const column = resolveReferencedColumn(expression, context);
    return column === expectedColumn;
}
function resolveReferencedColumn(expression, context) {
    if ("sql_text" in expression) {
        return null;
    }
    switch (expression.node_kind) {
        case "column_reference": {
            const qualifier = getColumnQualifier(expression);
            const columnName = getColumnName(expression);
            if (qualifier && context.tableNames.has(qualifier)) {
                return columnName;
            }
            if (!qualifier && context.allowUnqualified) {
                return columnName;
            }
            return null;
        }
        case "grouped_expression":
            return resolveReferencedColumn(expression.expression, context);
        case "function_call":
            return resolveColumnFromFunctionCall(expression, context);
        default:
            return null;
    }
}
function resolveColumnFromFunctionCall(expression, context) {
    const functionName = expression.name.value.toLowerCase();
    if (functionName !== "json_extract") {
        return null;
    }
    const [target, pathArg] = expression.arguments;
    if (!target ||
        !pathArg ||
        target.node_kind === "all_columns" ||
        pathArg.node_kind === "all_columns") {
        return null;
    }
    const targetExpr = target;
    const referencedColumn = resolveReferencedColumn(targetExpr, context);
    if (referencedColumn !== "snapshot_content") {
        return null;
    }
    if (!SNAPSHOT_ENTITY_FILTER_PUSH_ENABLED) {
        return null;
    }
    const pathExpression = pathArg;
    const literalPath = extractLiteralString(pathExpression, context.parameters);
    if (!literalPath) {
        return null;
    }
    return mapSnapshotJsonColumnFromPath(literalPath);
}
function extractLiteralString(expression, parameters) {
    const node = unwrapExpression(expression);
    if ("sql_text" in node) {
        return null;
    }
    if (node.node_kind === "literal" && typeof node.value === "string") {
        return node.value;
    }
    if (node.node_kind === "parameter") {
        const value = resolveParameterValue(node, parameters);
        return typeof value === "string" ? value : null;
    }
    return null;
}
/**
 * Maps recognized json_extract paths to their corresponding physical columns.
 *
 * @param path - JSON path literal extracted from a json_extract() call.
 * @returns The name of the backing column or null when the path is unsupported.
 *
 * @example
 * ```ts
 * mapSnapshotJsonColumnFromPath("$.id");
 * // => "entity_id"
 * ```
 */
function mapSnapshotJsonColumnFromPath(path) {
    const trimmed = path.trim();
    const candidates = new Set([trimmed]);
    const unquoted = trimmed.replace(/\."([^"]+)"(?=\.|$)/g, (_, segment) => `.${segment}`);
    candidates.add(unquoted);
    for (const candidate of candidates) {
        if (SNAPSHOT_JSON_ENTITY_PATHS.has(candidate)) {
            return "entity_id";
        }
    }
    return null;
}
function isLogicalOperator(operator) {
    return operator === "and" || operator === "or";
}
function isEqualityOperator(operator) {
    return operator === "=" || operator === "is";
}
function mergeSummaries(...summaries) {
    return summaries.reduce((accumulator, current) => {
        accumulator.count += current.count;
        accumulator.literals.push(...current.literals);
        return accumulator;
    }, { count: 0, literals: [] });
}
function mergeStringLiterals(...lists) {
    const seen = new Set();
    const merged = [];
    for (const list of lists) {
        for (const item of list) {
            if (typeof item !== "string") {
                continue;
            }
            if (!seen.has(item)) {
                seen.add(item);
                merged.push(item);
            }
        }
    }
    return merged;
}
function mergeSnapshotFilters(...lists) {
    const merged = new Map();
    for (const list of lists) {
        if (!list) {
            continue;
        }
        for (const entry of list) {
            if (!entry || typeof entry.path !== "string") {
                continue;
            }
            const valueSet = merged.get(entry.path) ?? new Set();
            for (const value of entry.values ?? []) {
                if (typeof value === "string") {
                    valueSet.add(value);
                }
            }
            if (valueSet.size > 0) {
                merged.set(entry.path, valueSet);
            }
        }
    }
    return Array.from(merged.entries()).map(([path, values]) => ({
        path,
        values: Array.from(values),
    }));
}
function collectSelectedColumns(select, tableNames) {
    const hiddenSelections = new Set();
    const supportColumns = new Set();
    if (select.projection.length === 0) {
        return {
            selectedColumns: null,
            requestedHiddenColumns: hiddenSelections,
            supportColumns,
        };
    }
    const order = [];
    const entries = new Map();
    let hasUnresolvedProjection = false;
    let selectsAll = false;
    for (const item of select.projection) {
        if (isSelectAllForTable(item, tableNames)) {
            selectsAll = true;
            continue;
        }
        if (item.node_kind !== "select_expression") {
            hasUnresolvedProjection = true;
            continue;
        }
        const columnName = extractColumnFromExpression(item.expression, tableNames);
        if (!columnName) {
            hasUnresolvedProjection = true;
            continue;
        }
        if (HIDDEN_SEGMENT_COLUMN_SET.has(columnName)) {
            hiddenSelections.add(columnName);
        }
        const alias = getIdentifierValue(item.alias);
        if (!entries.has(columnName)) {
            order.push(columnName);
            entries.set(columnName, {
                column: columnName,
                alias: alias ?? null,
            });
        }
        else if (alias) {
            const existing = entries.get(columnName);
            if (existing.alias === null) {
                entries.set(columnName, {
                    column: columnName,
                    alias,
                });
            }
        }
    }
    const referenced = collectReferencedColumnSummary(select, tableNames);
    if (referenced.hasUnknownReferences) {
        hasUnresolvedProjection = true;
    }
    for (const column of referenced.columns) {
        if (HIDDEN_SEGMENT_COLUMN_SET.has(column)) {
            hiddenSelections.add(column);
        }
        if (!entries.has(column)) {
            supportColumns.add(column);
        }
    }
    if (hasUnresolvedProjection || selectsAll) {
        return {
            selectedColumns: null,
            requestedHiddenColumns: hiddenSelections,
            supportColumns,
        };
    }
    return {
        selectedColumns: order.map((column) => entries.get(column)),
        requestedHiddenColumns: hiddenSelections,
        supportColumns,
    };
}
function collectReferencedColumnSummary(select, tableNames) {
    const columns = new Set();
    let hasUnknownReferences = false;
    const visit = (expression) => {
        if (hasUnknownReferences || !expression) {
            return;
        }
        if ("sql_text" in expression) {
            hasUnknownReferences = true;
            return;
        }
        switch (expression.node_kind) {
            case "column_reference": {
                const qualifier = getColumnQualifier(expression);
                if (qualifier && !tableNames.has(qualifier)) {
                    return;
                }
                const columnName = getColumnName(expression);
                if (columnName) {
                    columns.add(columnName);
                }
                return;
            }
            case "binary_expression":
                visit(expression.left);
                visit(expression.right);
                return;
            case "unary_expression":
                visit(expression.operand);
                return;
            case "grouped_expression":
                visit(expression.expression);
                return;
            case "function_call":
                for (const argument of expression.arguments) {
                    if (argument.node_kind === "all_columns") {
                        continue;
                    }
                    visit(argument);
                }
                return;
            case "in_list_expression":
                visit(expression.operand);
                for (const item of expression.items) {
                    visit(item);
                }
                return;
            case "between_expression":
                visit(expression.operand);
                visit(expression.start);
                visit(expression.end);
                return;
            case "subquery_expression":
                hasUnknownReferences = true;
                return;
            case "literal":
            case "parameter":
                return;
            default:
                return;
        }
    };
    visit(select.where_clause);
    for (const clause of select.from_clauses) {
        for (const join of clause.joins) {
            visit(join.on_expression);
        }
    }
    for (const order of select.order_by) {
        visit(order.expression);
    }
    return { columns, hasUnknownReferences };
}
function extractColumnFromExpression(expression, tableNames) {
    if ("sql_text" in expression) {
        return null;
    }
    switch (expression.node_kind) {
        case "column_reference": {
            const qualifier = getColumnQualifier(expression);
            if (qualifier && !tableNames.has(qualifier)) {
                return null;
            }
            return getColumnName(expression);
        }
        case "grouped_expression":
            return extractColumnFromExpression(expression.expression, tableNames);
        case "function_call":
            return extractColumnFromFunctionCall(expression, tableNames);
        default:
            return null;
    }
}
function extractColumnFromFunctionCall(expression, tableNames) {
    let referencedColumn = null;
    for (const argument of expression.arguments) {
        if (argument.node_kind === "all_columns") {
            return null;
        }
        const extracted = extractColumnFromExpression(argument, tableNames);
        if (extracted) {
            if (referencedColumn && referencedColumn !== extracted) {
                return null;
            }
            referencedColumn = extracted;
            continue;
        }
        if (!isLiteralOrParameter(argument)) {
            return null;
        }
    }
    return referencedColumn;
}
function isLiteralOrParameter(expression) {
    if ("sql_text" in expression) {
        return false;
    }
    switch (expression.node_kind) {
        case "literal":
        case "parameter":
            return true;
        case "grouped_expression":
            return isLiteralOrParameter(expression.expression);
        default:
            return false;
    }
}
function determineProjectionKind(projection, tableNames) {
    for (const item of projection) {
        if (isSelectAllForTable(item, tableNames)) {
            return "selectAll";
        }
    }
    return "partial";
}
function buildTraceEntry(args) {
    const { aliasList, schemaSummary, projectionKind, selectedColumns } = args;
    const referenceCount = aliasList.length === 0 ? 1 : aliasList.length;
    return {
        step: "rewrite_vtable_selects",
        payload: {
            reference_count: referenceCount,
            aliases: aliasList,
            projection: projectionKind,
            schema_key_predicates: schemaSummary.count,
            schema_key_literals: schemaSummary.literals,
            selected_columns: selectedColumns
                ? selectedColumns.map((entry) => entry.column)
                : null,
        },
    };
}
function buildVtableSelectRewrite(options) {
    const schemaFilterList = options.schemaKeys ?? [];
    const requestedVersionIds = Array.from(new Set(options.versionIds ?? []));
    const wantedVersionsCteName = requestedVersionIds.length > 0 ? "wanted_versions" : null;
    const { candidateColumns, rankedColumns } = buildProjectionColumnSet(options.selectedColumns, options.hiddenColumns, options.supportColumns);
    candidateColumns.add("priority");
    rankedColumns.add("priority");
    const inheritancePlan = determineInheritancePlan({
        requestedVersionIds: options.versionIds,
        versionInheritance: options.versionInheritance,
        forcedPrune: options.pruneInheritance,
    });
    const schemaFilter = buildSchemaFilter(schemaFilterList);
    const fileFilter = options.fileIds && options.fileIds.length > 0
        ? buildFileFilter(options.fileIds)
        : null;
    const entityFilter = options.entityIds && options.entityIds.length > 0
        ? buildEntityFilter(options.entityIds)
        : null;
    const snapshotFilter = buildSnapshotContentFilter(options.snapshotFilters);
    const txnFilter = combineFilters(schemaFilter, fileFilter, entityFilter, snapshotFilter);
    const inheritedFilter = combineFilters(schemaFilter, fileFilter, entityFilter, snapshotFilter);
    const buildVersionJoin = (alias, joinAlias) => buildWantedVersionJoinClause(wantedVersionsCteName, alias, joinAlias);
    const txnVersionJoin = buildVersionJoin("txn", "wv_txn");
    const untrackedVersionJoin = buildVersionJoin("unt", "wv_unt");
    const cacheVersionJoin = buildVersionJoin("cache", "wv_cache");
    const inheritanceVersionJoin = buildVersionJoin("vi", "wv_vi");
    const parentVersionJoin = buildVersionJoin("vi", "wv_parent");
    const needsWriterJoin = candidateColumns.has("writer_key");
    if (needsWriterJoin) {
        candidateColumns.add("inherited_from_version_id");
        rankedColumns.add("inherited_from_version_id");
    }
    const needsChangeJoin = candidateColumns.has("metadata");
    const projectionSql = buildRewriteProjectionSql(options.selectedColumns, {
        supportColumns: options.supportColumns,
        needsWriterJoin,
        candidateColumns,
    });
    const cacheSource = buildCacheSource(schemaFilterList, options.cacheTables, candidateColumns);
    const rankingOrder = [
        "c.priority",
        "c.created_at DESC",
        "c.file_id",
        "c.schema_key",
        "c.entity_id",
        "c.version_id",
        "c.change_id",
    ];
    const segments = [];
    if (options.hasOpenTransaction !== false &&
        !options.pruneTransactionSegment) {
        segments.push(buildTransactionSegment(txnFilter, candidateColumns, txnVersionJoin));
    }
    segments.push(buildUntrackedSegment(txnFilter, candidateColumns, untrackedVersionJoin));
    const cacheSegment = buildCacheSegment(cacheSource, txnFilter, candidateColumns, cacheVersionJoin);
    if (cacheSegment) {
        segments.push(cacheSegment);
    }
    const shouldPruneInheritance = inheritancePlan.mode === "pruned";
    let inheritanceJoinSource = "version_inheritance vi";
    let parentJoinSource = "version_parent vi";
    if (inheritancePlan.mode === "inline") {
        inheritanceJoinSource = buildInlineInheritanceSource(inheritancePlan.versionAncestorPairs);
        parentJoinSource = buildInlineParentSource(inheritancePlan.parentPairs);
    }
    let allSegments = segments;
    if (!shouldPruneInheritance) {
        const inheritedSegments = [];
        if (cacheSegment) {
            const inheritedCache = buildInheritedCacheSegment(cacheSource, inheritedFilter, candidateColumns, inheritanceJoinSource, inheritanceVersionJoin);
            if (inheritedCache) {
                inheritedSegments.push(inheritedCache);
            }
        }
        inheritedSegments.push(buildInheritedUntrackedSegment(inheritedFilter, candidateColumns, inheritanceJoinSource, inheritanceVersionJoin));
        if (options.hasOpenTransaction !== false) {
            inheritedSegments.push(buildInheritedTransactionSegment(inheritedFilter, candidateColumns, parentJoinSource, parentVersionJoin));
        }
        allSegments = segments.concat(inheritedSegments);
    }
    const candidates = allSegments.join(`\n\n    UNION ALL\n\n`);
    const writerJoinSql = needsWriterJoin
        ? `
LEFT JOIN lix_internal_state_writer ws_dst ON
  ws_dst.file_id = w.file_id AND
  ws_dst.entity_id = w.entity_id AND
  ws_dst.schema_key = w.schema_key AND
  ws_dst.version_id = w.version_id
LEFT JOIN lix_internal_state_writer ws_src ON
  ws_src.file_id = w.file_id AND
  ws_src.entity_id = w.entity_id AND
  ws_src.schema_key = w.schema_key AND
  ws_src.version_id = w.inherited_from_version_id`
        : "";
    const changeJoinSql = needsChangeJoin
        ? `
LEFT JOIN lix_internal_change chc ON chc.id = w.change_id`
        : "";
    const transactionJoinSql = options.hasOpenTransaction !== false
        ? `
LEFT JOIN lix_internal_transaction_state itx ON itx.id = w.change_id`
        : "";
    const rankedSql = buildRankedSegment({
        projectionColumns: rankedColumns,
        rankingOrder,
        candidatesSql: candidates,
    });
    const withClauses = [];
    if (wantedVersionsCteName) {
        withClauses.push(buildWantedVersionsCte(wantedVersionsCteName, requestedVersionIds));
    }
    if (inheritancePlan.mode === "recursive") {
        withClauses.push(buildVersionDescriptorCte());
        withClauses.push(buildVersionInheritanceCte(wantedVersionsCteName));
        withClauses.push(buildVersionParentCte(wantedVersionsCteName));
    }
    const withPrefix = withClauses.length > 0
        ? `WITH
${withClauses.map((clause) => indent(clause, 2)).join(",\n")}
`
        : "";
    const body = `${withPrefix}SELECT
${indent(projectionSql, 2)}
FROM (
${indent(rankedSql, 2)}
) AS w
${writerJoinSql}
${changeJoinSql}
${transactionJoinSql}
WHERE w.rn = 1`;
    return `(\n${body}\n)\n`;
}
function buildRewriteProjectionSql(selectedColumns, options) {
    const builder = internalQueryBuilder;
    const baseQuery = builder.selectFrom(`${INTERNAL_STATE_VTABLE} as w`);
    const supportEntries = selectedColumns === null
        ? []
        : Array.from(options.supportColumns)
            .filter((column) => !selectedColumns.some((entry) => entry.column === column) &&
            !INTERNAL_ONLY_COLUMN_SET.has(column))
            .map((column) => ({
            column,
            alias: null,
        }));
    const combinedEntries = selectedColumns === null ? null : [...selectedColumns, ...supportEntries];
    const query = combinedEntries === null || combinedEntries.length === 0
        ? undefined
        : baseQuery.select((eb) => combinedEntries.map((entry) => {
            if (options.needsWriterJoin && entry.column === "writer_key") {
                return eb.fn
                    .coalesce(eb.ref("ws_dst.writer_key"), eb.ref("ws_src.writer_key"), eb.ref("w.writer_key"))
                    .as(entry.column);
            }
            return eb.ref(`w.${entry.column}`).as(entry.column);
        }));
    if (!query) {
        const defaultColumns = buildDefaultProjectionColumns(options.candidateColumns);
        return defaultColumns
            .map((column) => options.needsWriterJoin && column === "writer_key"
            ? `COALESCE(ws_dst.writer_key, ws_src.writer_key, w.writer_key) AS ${quoteIdentifier(column)}`
            : `w.${quoteIdentifier(column)} AS ${quoteIdentifier(column)}`)
            .join(",\n");
    }
    const { sql } = query.compile();
    const match = sql.match(/^select\s+(?<projection>[\s\S]+?)\s+from\s+/i);
    return match?.groups?.projection?.trim() ?? sql;
}
function buildDefaultProjectionColumns(candidateColumns) {
    const ordered = PROJECTION_COLUMN_ORDER.filter((column) => !INTERNAL_ONLY_COLUMN_SET.has(column) && candidateColumns.has(column));
    const additional = Array.from(candidateColumns).filter((column) => !INTERNAL_ONLY_COLUMN_SET.has(column) && !ordered.includes(column));
    return ordered.concat(additional.sort());
}
function buildProjectionColumnSet(selectedColumns, hiddenColumns, supportColumns) {
    const requestedHiddenColumns = new Set(hiddenColumns);
    const seedColumns = selectedColumns === null
        ? ALL_SEGMENT_COLUMNS
        : Array.from(BASE_SEGMENT_COLUMNS);
    const candidateColumns = new Set();
    const rankedColumns = new Set();
    for (const column of seedColumns) {
        if (HIDDEN_SEGMENT_COLUMN_SET.has(column) &&
            !requestedHiddenColumns.has(column)) {
            continue;
        }
        candidateColumns.add(column);
        rankedColumns.add(column);
    }
    if (selectedColumns && selectedColumns.length > 0) {
        for (const entry of selectedColumns) {
            candidateColumns.add(entry.column);
            rankedColumns.add(entry.column);
        }
    }
    for (const column of supportColumns) {
        candidateColumns.add(column);
        rankedColumns.add(column);
    }
    for (const hiddenColumn of requestedHiddenColumns) {
        candidateColumns.add(hiddenColumn);
        rankedColumns.add(hiddenColumn);
    }
    for (const hiddenColumn of HIDDEN_SEGMENT_COLUMN_SET) {
        if (!requestedHiddenColumns.has(hiddenColumn)) {
            candidateColumns.delete(hiddenColumn);
            rankedColumns.delete(hiddenColumn);
        }
    }
    // Internal rewrites rely on snapshot_content for JSON decoding and cache filtering.
    candidateColumns.add("snapshot_content");
    rankedColumns.add("snapshot_content");
    return { candidateColumns, rankedColumns };
}
const NEWLINE = "\n";
const ALL_SEGMENT_COLUMNS = [
    "_pk",
    "source_tag",
    "entity_id",
    "schema_key",
    "file_id",
    "plugin_key",
    "snapshot_content",
    "schema_version",
    "version_id",
    "created_at",
    "updated_at",
    "inherited_from_version_id",
    "change_id",
    "untracked",
    "commit_id",
    "metadata",
    "writer_key",
    "priority",
];
const BASE_SEGMENT_COLUMNS = new Set([
    "entity_id",
    "schema_key",
    "file_id",
    "version_id",
    "created_at",
    "change_id",
    "priority",
]);
const CACHE_COLUMNS = [
    "entity_id",
    "schema_key",
    "file_id",
    "plugin_key",
    "snapshot_content",
    "schema_version",
    "version_id",
    "created_at",
    "updated_at",
    "inherited_from_version_id",
    "change_id",
    "commit_id",
    "is_tombstone",
];
const PROJECTION_COLUMN_ORDER = ALL_SEGMENT_COLUMNS.filter((column) => column !== "priority");
const INTERNAL_ONLY_COLUMN_SET = new Set(["priority", "rn"]);
function buildTransactionSegment(segmentFilter, projectionColumns, versionJoinClause) {
    const rewrittenFilter = rewriteFilterForAlias(segmentFilter, "txn");
    const filterClause = rewrittenFilter ? `WHERE ${rewrittenFilter}` : "";
    const columns = [
        [
            "_pk",
            `'T' || '~' || lix_encode_pk_part(txn.file_id) || '~' || lix_encode_pk_part(txn.entity_id) || '~' || lix_encode_pk_part(txn.version_id) AS _pk`,
        ],
        ["source_tag", "'T' AS source_tag"],
        ["entity_id", "txn.entity_id AS entity_id"],
        ["schema_key", "txn.schema_key AS schema_key"],
        ["file_id", "txn.file_id AS file_id"],
        ["plugin_key", "txn.plugin_key AS plugin_key"],
        ["snapshot_content", "json(txn.snapshot_content) AS snapshot_content"],
        ["schema_version", "txn.schema_version AS schema_version"],
        ["version_id", "txn.version_id AS version_id"],
        ["created_at", "txn.created_at AS created_at"],
        ["updated_at", "txn.created_at AS updated_at"],
        ["inherited_from_version_id", "NULL AS inherited_from_version_id"],
        ["change_id", "txn.id AS change_id"],
        ["untracked", "txn.untracked AS untracked"],
        ["commit_id", "'pending' AS commit_id"],
        ["metadata", "json(txn.metadata) AS metadata"],
        ["writer_key", "txn.writer_key AS writer_key"],
        ["priority", "1 AS priority"],
    ];
    const columnSql = columns
        .filter(([column]) => projectionColumns.has(column))
        .map(([, sql]) => sql)
        .join(",\n");
    const joinClause = versionJoinClause ? `\n\t\t${versionJoinClause}` : "";
    return stripIndent(`
		SELECT
${indent(columnSql, 4)}
		FROM lix_internal_transaction_state txn${joinClause}
		${filterClause}
	`).trimEnd();
}
function buildUntrackedSegment(segmentFilter, projectionColumns, versionJoinClause) {
    const rewrittenFilter = rewriteFilterForAlias(segmentFilter, "unt");
    const columns = [
        [
            "_pk",
            `'U' || '~' || lix_encode_pk_part(unt.file_id) || '~' || lix_encode_pk_part(unt.entity_id) || '~' || lix_encode_pk_part(unt.version_id) AS _pk`,
        ],
        ["source_tag", "'U' AS source_tag"],
        ["entity_id", "unt.entity_id AS entity_id"],
        ["schema_key", "unt.schema_key AS schema_key"],
        ["file_id", "unt.file_id AS file_id"],
        ["plugin_key", "unt.plugin_key AS plugin_key"],
        ["snapshot_content", "json(unt.snapshot_content) AS snapshot_content"],
        ["schema_version", "unt.schema_version AS schema_version"],
        ["version_id", "unt.version_id AS version_id"],
        ["created_at", "unt.created_at AS created_at"],
        ["updated_at", "unt.updated_at AS updated_at"],
        ["inherited_from_version_id", "NULL AS inherited_from_version_id"],
        ["change_id", "'untracked' AS change_id"],
        ["untracked", "1 AS untracked"],
        ["commit_id", "'untracked' AS commit_id"],
        ["metadata", "NULL AS metadata"],
        ["writer_key", "NULL AS writer_key"],
        ["priority", "2 AS priority"],
    ];
    const columnSql = columns
        .filter(([column]) => projectionColumns.has(column))
        .map(([, sql]) => sql)
        .join(",\n");
    const joinClause = versionJoinClause ? `\n\t\t${versionJoinClause}` : "";
    return stripIndent(`
		SELECT
${indent(columnSql, 4)}
		FROM lix_internal_state_all_untracked unt${joinClause}
		${rewrittenFilter ? `WHERE ${rewrittenFilter}` : ""}
	`).trimEnd();
}
function buildCacheSegment(cacheSource, segmentFilter, projectionColumns, versionJoinClause) {
    if (!cacheSource) {
        return null;
    }
    const rewrittenFilter = rewriteFilterForAlias(segmentFilter, "cache");
    const sourceKeyword = cacheSource.trim().toLowerCase();
    const sourceSql = sourceKeyword.startsWith("select")
        ? `(${cacheSource})`
        : cacheSource;
    const columns = [
        [
            "_pk",
            `'C' || '~' || lix_encode_pk_part(cache.file_id) || '~' || lix_encode_pk_part(cache.entity_id) || '~' || lix_encode_pk_part(cache.version_id) AS _pk`,
        ],
        ["source_tag", "'C' AS source_tag"],
        ["entity_id", "cache.entity_id AS entity_id"],
        ["schema_key", "cache.schema_key AS schema_key"],
        ["file_id", "cache.file_id AS file_id"],
        ["plugin_key", "cache.plugin_key AS plugin_key"],
        ["snapshot_content", "json(cache.snapshot_content) AS snapshot_content"],
        ["schema_version", "cache.schema_version AS schema_version"],
        ["version_id", "cache.version_id AS version_id"],
        ["created_at", "cache.created_at AS created_at"],
        ["updated_at", "cache.updated_at AS updated_at"],
        [
            "inherited_from_version_id",
            "cache.inherited_from_version_id AS inherited_from_version_id",
        ],
        ["change_id", "cache.change_id AS change_id"],
        ["untracked", "0 AS untracked"],
        ["commit_id", "cache.commit_id AS commit_id"],
        ["metadata", "NULL AS metadata"],
        ["writer_key", "NULL AS writer_key"],
        ["priority", "3 AS priority"],
    ];
    const columnSql = columns
        .filter(([column]) => projectionColumns.has(column))
        .map(([, sql]) => sql)
        .join(",\n");
    const whereClause = rewrittenFilter ? `\n\t\tWHERE ${rewrittenFilter}` : "";
    const joinClause = versionJoinClause ? `\n\t\t${versionJoinClause}` : "";
    return stripIndent(`
		SELECT
${indent(columnSql, 4)}
		FROM ${sourceSql} cache${joinClause}${whereClause}
	`).trimEnd();
}
/**
 * Builds the base CTE extracting version descriptors for inheritance lookups.
 *
 * @returns SQL fragment for the version descriptor base CTE.
 *
 * @example
 * ```ts
 * const sql = buildVersionDescriptorCte();
 * ```
 */
function buildVersionDescriptorCte() {
    return stripIndent(`
	version_descriptor_base AS (
	  SELECT
	    json_extract(desc.snapshot_content, '$.id') AS version_id,
	    json_extract(desc.snapshot_content, '$.inherits_from_version_id') AS inherits_from_version_id
	  FROM "lix_internal_state_cache_v1_lix_version_descriptor" desc
	  WHERE desc.is_tombstone = 0
	    AND desc.snapshot_content IS NOT NULL
	)`).trim();
}
/**
 * Builds the recursive CTE that unfolds the full inheritance chain.
 *
 * @param seedCteName - Optional wanted version CTE that limits recursion seeds.
 * @returns SQL fragment for the version inheritance CTE.
 *
 * @example
 * ```ts
 * const sql = buildVersionInheritanceCte("wanted_versions");
 * ```
 */
function buildVersionInheritanceCte(seedCteName) {
    const seedJoin = seedCteName
        ? `
	  JOIN ${seedCteName} wv ON wv.version_id = vdb.version_id`
        : "";
    return stripIndent(`
	version_inheritance(version_id, ancestor_version_id) AS (
	  SELECT
	    vdb.version_id,
	    vdb.inherits_from_version_id
	  FROM version_descriptor_base vdb${seedJoin}
	  WHERE vdb.inherits_from_version_id IS NOT NULL

	  UNION ALL

	  SELECT
	    vi.version_id,
	    vdb.inherits_from_version_id
	  FROM version_inheritance vi
	  JOIN version_descriptor_base vdb ON vdb.version_id = vi.ancestor_version_id
	  WHERE vdb.inherits_from_version_id IS NOT NULL
	)`).trim();
}
/**
 * Builds the CTE mapping each version to its direct parent version.
 *
 * @param seedCteName - Optional wanted version CTE limiting descriptors.
 * @returns SQL fragment for the version parent CTE.
 *
 * @example
 * ```ts
 * const sql = buildVersionParentCte("wanted_versions");
 * ```
 */
function buildVersionParentCte(seedCteName) {
    const seedJoin = seedCteName
        ? `
	  JOIN ${seedCteName} wv ON wv.version_id = vdb.version_id`
        : "";
    return stripIndent(`
	version_parent AS (
	  SELECT
	    vdb.version_id,
	    vdb.inherits_from_version_id AS parent_version_id
	  FROM version_descriptor_base vdb${seedJoin}
	  WHERE vdb.inherits_from_version_id IS NOT NULL
	)`).trim();
}
/**
 * Builds a CTE enumerating the requested versions for join-based filters.
 *
 * @param cteName - The identifier assigned to the CTE.
 * @param versionIds - Ordered list of version identifiers to include.
 * @returns SQL fragment for the wanted versions CTE.
 *
 * @example
 * ```ts
 * const sql = buildWantedVersionsCte("wanted_versions", ["v1", "v2"]);
 * ```
 */
function buildWantedVersionsCte(cteName, versionIds) {
    const selects = versionIds
        .map((versionId) => `SELECT '${escapeSqlLiteral(versionId)}' AS version_id`)
        .join("\nUNION ALL\n");
    return stripIndent(`
${cteName} AS (
${indent(selects, 2)}
)`).trim();
}
/**
 * Builds the inherited cache candidate segment that maps ancestor versions onto
 * the target schema version.
 *
 * @param cacheSource - The SQL producing the cache rows.
 * @param segmentFilter - Optional predicate scoped to the cache alias.
 * @param projectionColumns - Columns required for the projection.
 * @param versionJoinClause - SQL equi-join tying descendants to wanted versions.
 * @returns SQL fragment or null when no cache source exists.
 *
 * @example
 * ```ts
 * const sql = buildInheritedCacheSegment(
 *   '"cache_table"',
 *   "txn.schema_key = 'demo'",
 *   new Set(),
 *   "version_inheritance vi",
 *   "JOIN wanted_versions wv ON wv.version_id = vi.version_id"
 * );
 * ```
 */
function buildInheritedCacheSegment(cacheSource, segmentFilter, projectionColumns, inheritanceJoin, versionJoinClause) {
    if (!cacheSource) {
        return null;
    }
    const rewrittenFilter = rewriteFilterForAlias(segmentFilter, "cache");
    const sourceKeyword = cacheSource.trim().toLowerCase();
    const sourceSql = sourceKeyword.startsWith("select")
        ? `(${cacheSource})`
        : cacheSource;
    const columns = [
        [
            "_pk",
            `'CI' || '~' || lix_encode_pk_part(cache.file_id) || '~' || lix_encode_pk_part(cache.entity_id) || '~' || lix_encode_pk_part(vi.version_id) AS _pk`,
        ],
        ["source_tag", "'CI' AS source_tag"],
        ["entity_id", "cache.entity_id AS entity_id"],
        ["schema_key", "cache.schema_key AS schema_key"],
        ["file_id", "cache.file_id AS file_id"],
        ["plugin_key", "cache.plugin_key AS plugin_key"],
        ["snapshot_content", "json(cache.snapshot_content) AS snapshot_content"],
        ["schema_version", "cache.schema_version AS schema_version"],
        ["version_id", "vi.version_id AS version_id"],
        ["created_at", "cache.created_at AS created_at"],
        ["updated_at", "cache.updated_at AS updated_at"],
        [
            "inherited_from_version_id",
            "cache.version_id AS inherited_from_version_id",
        ],
        ["change_id", "cache.change_id AS change_id"],
        ["untracked", "0 AS untracked"],
        ["commit_id", "cache.commit_id AS commit_id"],
        ["metadata", "NULL AS metadata"],
        ["writer_key", "NULL AS writer_key"],
        ["priority", "4 AS priority"],
    ];
    const columnSql = columns
        .filter(([column]) => projectionColumns.has(column))
        .map(([, sql]) => sql)
        .join(",\n");
    const joinClause = versionJoinClause ? `\n\t\t${versionJoinClause}` : "";
    return stripIndent(`
		SELECT
${indent(columnSql, 4)}
		FROM ${inheritanceJoin}${joinClause}
		JOIN ${sourceSql} cache ON cache.version_id = vi.ancestor_version_id
		WHERE cache.is_tombstone = 0
		  AND cache.snapshot_content IS NOT NULL${rewrittenFilter ? ` AND ${rewrittenFilter}` : ""}
	`).trimEnd();
}
/**
 * Builds the inherited segment sourcing data from `lix_internal_state_all_untracked`.
 *
 * @param segmentFilter - Optional predicate scoped to the untracked alias.
 * @param projectionColumns - Columns required for the projection.
 * @param versionJoinClause - SQL equi-join tying descendants to wanted versions.
 * @returns SQL fragment selecting inherited untracked rows.
 *
 * @example
 * ```ts
 * const sql = buildInheritedUntrackedSegment(
 *   null,
 *   new Set(["entity_id"]),
 *   "version_inheritance vi",
 *   "JOIN wanted_versions wv ON wv.version_id = vi.version_id"
 * );
 * ```
 */
function buildInheritedUntrackedSegment(segmentFilter, projectionColumns, inheritanceJoin, versionJoinClause) {
    const rewrittenFilter = rewriteFilterForAlias(segmentFilter, "unt");
    const columns = [
        [
            "_pk",
            `'UI' || '~' || lix_encode_pk_part(unt.file_id) || '~' || lix_encode_pk_part(unt.entity_id) || '~' || lix_encode_pk_part(vi.version_id) AS _pk`,
        ],
        ["source_tag", "'UI' AS source_tag"],
        ["entity_id", "unt.entity_id AS entity_id"],
        ["schema_key", "unt.schema_key AS schema_key"],
        ["file_id", "unt.file_id AS file_id"],
        ["plugin_key", "unt.plugin_key AS plugin_key"],
        ["snapshot_content", "json(unt.snapshot_content) AS snapshot_content"],
        ["schema_version", "unt.schema_version AS schema_version"],
        ["version_id", "vi.version_id AS version_id"],
        ["created_at", "unt.created_at AS created_at"],
        ["updated_at", "unt.updated_at AS updated_at"],
        [
            "inherited_from_version_id",
            "unt.version_id AS inherited_from_version_id",
        ],
        ["change_id", "'untracked' AS change_id"],
        ["untracked", "1 AS untracked"],
        ["commit_id", "'untracked' AS commit_id"],
        ["metadata", "NULL AS metadata"],
        ["writer_key", "NULL AS writer_key"],
        ["priority", "5 AS priority"],
    ];
    const columnSql = columns
        .filter(([column]) => projectionColumns.has(column))
        .map(([, sql]) => sql)
        .join(",\n");
    const joinClause = versionJoinClause ? `\n\t\t${versionJoinClause}` : "";
    return stripIndent(`
		SELECT
${indent(columnSql, 4)}
		FROM ${inheritanceJoin}${joinClause}
		JOIN lix_internal_state_all_untracked unt ON unt.version_id = vi.ancestor_version_id
		WHERE unt.is_tombstone = 0
		  AND unt.snapshot_content IS NOT NULL${rewrittenFilter ? ` AND ${rewrittenFilter}` : ""}
	`).trimEnd();
}
/**
 * Builds the inherited segment that projects transaction rows onto descendant
 * versions.
 *
 * @param segmentFilter - Optional predicate scoped to the transaction alias.
 * @param projectionColumns - Columns required for the projection.
 * @param versionJoinClause - SQL equi-join tying descendants to wanted versions.
 * @returns SQL fragment selecting inherited transaction rows.
 *
 * @example
 * ```ts
 * const sql = buildInheritedTransactionSegment(
 *   "txn.schema_key = 'demo'",
 *   new Set(["change_id"]),
 *   "version_parent vi",
 *   "JOIN wanted_versions wv ON wv.version_id = vi.version_id"
 * );
 * ```
 */
function buildInheritedTransactionSegment(segmentFilter, projectionColumns, parentJoinSource, versionJoinClause) {
    const columns = [
        [
            "_pk",
            `'TI' || '~' || lix_encode_pk_part(txn.file_id) || '~' || lix_encode_pk_part(txn.entity_id) || '~' || lix_encode_pk_part(vi.version_id) AS _pk`,
        ],
        ["source_tag", "'TI' AS source_tag"],
        ["entity_id", "txn.entity_id AS entity_id"],
        ["schema_key", "txn.schema_key AS schema_key"],
        ["file_id", "txn.file_id AS file_id"],
        ["plugin_key", "txn.plugin_key AS plugin_key"],
        ["snapshot_content", "json(txn.snapshot_content) AS snapshot_content"],
        ["schema_version", "txn.schema_version AS schema_version"],
        ["version_id", "vi.version_id AS version_id"],
        ["created_at", "txn.created_at AS created_at"],
        ["updated_at", "txn.created_at AS updated_at"],
        [
            "inherited_from_version_id",
            "vi.parent_version_id AS inherited_from_version_id",
        ],
        ["change_id", "txn.id AS change_id"],
        ["untracked", "txn.untracked AS untracked"],
        ["commit_id", "'pending' AS commit_id"],
        ["metadata", "json(txn.metadata) AS metadata"],
        ["writer_key", "txn.writer_key AS writer_key"],
        ["priority", "6 AS priority"],
    ];
    const columnSql = columns
        .filter(([column]) => projectionColumns.has(column))
        .map(([, sql]) => sql)
        .join(",\n");
    const rewrittenFilter = rewriteFilterForAlias(segmentFilter, "txn");
    const joinClause = versionJoinClause ? `\n\t\t${versionJoinClause}` : "";
    return stripIndent(`
		SELECT
${indent(columnSql, 4)}
		FROM ${parentJoinSource}${joinClause}
		JOIN lix_internal_transaction_state txn ON txn.version_id = vi.parent_version_id
		WHERE vi.parent_version_id IS NOT NULL
		  AND txn.snapshot_content IS NOT NULL${rewrittenFilter ? ` AND ${rewrittenFilter}` : ""}
	`).trimEnd();
}
function buildRankedSegment(options) {
    const columns = ALL_SEGMENT_COLUMNS.filter((column) => options.projectionColumns.has(column)).map((column) => `c.${column} AS ${column}`);
    const orderClause = options.rankingOrder.join(", ");
    const selectEntries = [
        ...columns,
        `ROW_NUMBER() OVER (
        PARTITION BY c.file_id, c.schema_key, c.entity_id, c.version_id
        ORDER BY ${orderClause}
      ) AS rn`,
    ];
    return stripIndent(`
		SELECT
${indent(selectEntries.join(",\n"), 4)}
		FROM (
${indent(options.candidatesSql, 6)}
		) AS c
	`).trimEnd();
}
function buildSchemaFilter(schemaKeys) {
    if (!schemaKeys || schemaKeys.length === 0) {
        return null;
    }
    const values = schemaKeys.map((key) => `'${key}'`).join(", ");
    return `txn.schema_key IN (${values})`;
}
function buildFileFilter(fileIds) {
    if (!fileIds || fileIds.length === 0) {
        return null;
    }
    const values = fileIds
        .map((fileId) => `'${escapeSqlLiteral(fileId)}'`)
        .join(", ");
    return `txn.file_id IN (${values})`;
}
function buildEntityFilter(entityIds) {
    if (!entityIds || entityIds.length === 0) {
        return null;
    }
    const values = entityIds
        .map((entityId) => `'${escapeSqlLiteral(entityId)}'`)
        .join(", ");
    return `txn.entity_id IN (${values})`;
}
function buildSnapshotContentFilter(filters) {
    if (!filters || filters.length === 0) {
        return null;
    }
    const clauses = filters
        .map((filter) => buildSnapshotPredicateClause(filter))
        .filter((clause) => typeof clause === "string");
    if (clauses.length === 0) {
        return null;
    }
    return clauses.map((clause) => `(${clause})`).join(" AND ");
}
function buildSnapshotPredicateClause(filter) {
    const uniqueValues = Array.from(new Set(filter.values));
    if (uniqueValues.length === 0) {
        return null;
    }
    const pathLiteral = `'${escapeSqlLiteral(filter.path)}'`;
    if (uniqueValues.length === 1) {
        const value = escapeSqlLiteral(uniqueValues[0]);
        return `json_extract(txn.snapshot_content, ${pathLiteral}) = '${value}'`;
    }
    const valueList = uniqueValues
        .map((value) => `'${escapeSqlLiteral(value)}'`)
        .join(", ");
    return `json_extract(txn.snapshot_content, ${pathLiteral}) IN (${valueList})`;
}
/**
 * Builds an equi-join clause tying a table alias to the wanted versions CTE.
 *
 * @param cteName - Name of the wanted versions CTE.
 * @param targetAlias - Table alias exposing a `version_id` column.
 * @param joinAlias - Alias assigned to the join for readability.
 * @returns SQL JOIN clause or an empty string when no CTE is provided.
 *
 * @example
 * ```ts
 * const joinSql = buildWantedVersionJoinClause("wanted_versions", "txn", "wv_txn");
 * ```
 */
function buildWantedVersionJoinClause(cteName, targetAlias, joinAlias) {
    if (!cteName) {
        return "";
    }
    return `JOIN ${cteName} ${joinAlias} ON ${joinAlias}.version_id = ${targetAlias}.version_id`;
}
function buildCacheSource(schemaKeys, cacheTables, projectionColumns) {
    const requiredColumns = new Set(projectionColumns);
    // Always keep structural columns that power tombstone filtering and ranking.
    requiredColumns.add("snapshot_content");
    requiredColumns.add("is_tombstone");
    requiredColumns.add("file_id");
    requiredColumns.add("entity_id");
    requiredColumns.add("schema_key");
    requiredColumns.add("version_id");
    requiredColumns.add("change_id");
    requiredColumns.add("created_at");
    const uniqueKeys = schemaKeys && schemaKeys.length > 0
        ? Array.from(new Set(schemaKeys))
        : Array.from(cacheTables.keys());
    const seenTables = new Set();
    const entries = uniqueKeys
        .map((key) => {
        const lookupKeys = new Set([key]);
        const sanitizedKey = cacheTableNameToSchemaKey(schemaKeyToCacheTableName(key));
        if (sanitizedKey !== key) {
            lookupKeys.add(sanitizedKey);
        }
        for (const lookupKey of lookupKeys) {
            const tableName = cacheTables.get(lookupKey);
            if (typeof tableName === "string" &&
                tableName.length > 0 &&
                !seenTables.has(tableName)) {
                seenTables.add(tableName);
                return {
                    tableName,
                    sql: buildPhysicalCacheSelect(tableName, requiredColumns),
                };
            }
        }
        return null;
    })
        .filter((value) => value !== null);
    if (entries.length === 0) {
        return null;
    }
    if (entries.length === 1) {
        return quoteIdentifier(entries[0].tableName);
    }
    return entries.map((entry) => entry.sql).join(`\nUNION ALL\n`);
}
function buildPhysicalCacheSelect(tableName, requiredColumns) {
    const projection = CACHE_COLUMNS.filter((column) => requiredColumns.has(column))
        .map((column) => quoteIdentifier(column))
        .join(",\n    ");
    return `SELECT
    ${projection}
  FROM ${quoteIdentifier(tableName)}`;
}
/**
 * Resolves the cache table name for the provided schema key, falling back to its sanitized form.
 *
 * @example
 * ```ts
 * resolveCacheTableName(new Map([["delete_cache_schema", "cache_table"]]), "delete_cache_schema");
 * // "cache_table"
 * ```
 */
function stripIndent(value) {
    const trimmed = value.replace(/^\n+/, "").replace(/\n+$/, "");
    const lines = trimmed.split(NEWLINE);
    const indents = lines
        .filter((line) => line.trim().length > 0)
        .map((line) => line.match(/^\s*/)?.[0]?.length ?? 0);
    const minIndent = indents.length > 0 ? Math.min(...indents) : 0;
    return lines.map((line) => line.slice(minIndent)).join(NEWLINE);
}
function indent(value, spaces) {
    const pad = " ".repeat(spaces);
    return value
        .split(NEWLINE)
        .map((line) => (line.length > 0 ? pad + line : line))
        .join(NEWLINE);
}
function quoteIdentifier(identifier) {
    return `"${identifier.replace(/"/g, '""')}"`;
}
function shouldPushFileFilter(schemaKeys, fileIds) {
    if (!schemaKeys || schemaKeys.length === 0)
        return false;
    if (!fileIds || fileIds.length === 0)
        return false;
    return schemaKeys.every((key) => typeof key === "string" && !key.startsWith("lix_"));
}
function shouldPushEntityFilter(schemaKeys, entityIds) {
    if (!schemaKeys || schemaKeys.length === 0)
        return false;
    if (!entityIds || entityIds.length === 0)
        return false;
    return schemaKeys.every((key) => typeof key === "string" && !key.startsWith("lix_"));
}
function shouldPushSnapshotFilters(schemaKeys, filters) {
    if (!schemaKeys || schemaKeys.length === 0)
        return false;
    if (!filters || filters.length === 0)
        return false;
    return schemaKeys.every((key) => typeof key === "string" && !key.startsWith("lix_"));
}
function combineFilters(...filters) {
    const active = filters.filter((filter) => typeof filter === "string" && filter.length > 0);
    if (active.length === 0) {
        return null;
    }
    if (active.length === 1) {
        return active[0];
    }
    return active.map((filter) => `(${filter})`).join(" AND ");
}
function rewriteFilterForAlias(filter, alias) {
    if (!filter) {
        return null;
    }
    return filter.replace(/txn\./g, `${alias}.`);
}
function normalizeObjectName(objectName) {
    if (objectName.parts.length === 0) {
        return null;
    }
    const last = objectName.parts[objectName.parts.length - 1];
    return normalizeIdentifierValue(last.value);
}
function escapeSqlLiteral(value) {
    return value.replace(/'/g, "''");
}
function determineInheritancePlan(args) {
    const requested = Array.from(new Set(args.requestedVersionIds ?? []));
    if (args.forcedPrune) {
        return { mode: "pruned" };
    }
    if (requested.length === 0) {
        return args.versionInheritance ? { mode: "recursive" } : { mode: "pruned" };
    }
    if (!args.versionInheritance || args.versionInheritance.size === 0) {
        return { mode: "pruned" };
    }
    const ancestorPairs = [];
    const parentPairs = [];
    const ancestorSeen = new Set();
    const parentSeen = new Set();
    for (const versionId of requested) {
        let current = versionId;
        const chainVisited = new Set([current]);
        while (true) {
            const node = args.versionInheritance.get(current);
            if (!node) {
                return {
                    mode: "recursive",
                };
            }
            const parentId = node.inheritsFromVersionId;
            if (!parentId) {
                break;
            }
            if (chainVisited.has(parentId)) {
                return {
                    mode: "recursive",
                };
            }
            chainVisited.add(parentId);
            const ancestorKey = `${versionId}::${parentId}`;
            if (!ancestorSeen.has(ancestorKey)) {
                ancestorSeen.add(ancestorKey);
                ancestorPairs.push({ versionId, ancestorId: parentId });
            }
            const parentKey = `${current}::${parentId}`;
            if (!parentSeen.has(parentKey)) {
                parentSeen.add(parentKey);
                parentPairs.push({ versionId: current, parentId });
            }
            current = parentId;
        }
    }
    if (ancestorPairs.length === 0) {
        return { mode: "pruned" };
    }
    return {
        mode: "inline",
        versionAncestorPairs: ancestorPairs,
        parentPairs,
    };
}
function buildInlineInheritanceSource(pairs) {
    return buildInlineValueSource(pairs.map(({ versionId, ancestorId }) => [versionId, ancestorId]), ["version_id", "ancestor_version_id"]);
}
function buildInlineParentSource(pairs) {
    return buildInlineValueSource(pairs.map(({ versionId, parentId }) => [versionId, parentId]), ["version_id", "parent_version_id"]);
}
function buildInlineValueSource(rows, columns) {
    const selects = rows
        .map(([left, right]) => `SELECT '${escapeSqlLiteral(left)}' AS ${columns[0]}, '${escapeSqlLiteral(right)}' AS ${columns[1]}`)
        .join("\nUNION ALL\n");
    return `(\n${selects}\n) AS vi`;
}
//# sourceMappingURL=rewrite-vtable-selects.js.map