import { assert } from "../../../shared/src/asserts.js";
import { mergeConstraints } from "./planner-constraint.js";
import { omitFanout } from "./planner-node.js";
class PlannerConnection {
  kind = "connection";
  // ========================================================================
  // IMMUTABLE STRUCTURE (set during construction, never changes)
  // ========================================================================
  #sort;
  #filters;
  #model;
  table;
  name;
  // Human-readable name for debugging (defaults to table name)
  #baseConstraints;
  // Constraints from parent correlation
  #baseLimit;
  // Original limit from query structure (never modified)
  selectivity;
  // Fraction of rows passing filters (1.0 = no filtering)
  #output;
  // Set once during graph construction
  // ========================================================================
  // MUTABLE PLANNING STATE (changes during plan search)
  // ========================================================================
  /**
   * Current limit during planning. Can be cleared (set to undefined) when a
   * parent join is flipped, indicating this connection is now in an outer loop
   * and should not be limited by EXISTS semantics.
   */
  limit;
  /**
   * Constraints accumulated from parent joins during planning.
   * Key is a path through the graph (e.g., "0,1" for branch pattern [0,1]).
   *
   * Undefined constraints are possible when a FO converts to UFO and only
   * a single join in the UFO is flipped - other branches report undefined.
   */
  #constraints;
  #isRoot;
  /**
   * Cached per-constraint costs to avoid redundant cost model calls.
   * Maps constraint key (branch pattern string) to computed cost.
   * Invalidated when constraints change.
   */
  #cachedConstraintCosts = /* @__PURE__ */ new Map();
  constructor(table, model, sort, filters, isRoot, baseConstraints, limit, name) {
    this.table = table;
    this.name = name ?? table;
    this.#sort = sort;
    this.#filters = filters;
    this.#model = model;
    this.#baseConstraints = baseConstraints;
    this.#baseLimit = limit;
    this.limit = limit;
    this.#constraints = /* @__PURE__ */ new Map();
    this.#isRoot = isRoot;
    if (limit !== void 0 && filters) {
      const costWithFilters = model(table, sort, filters, void 0);
      const costWithoutFilters = model(table, sort, void 0, void 0);
      this.selectivity = costWithoutFilters.rows > 0 ? costWithFilters.rows / costWithoutFilters.rows : 1;
    } else {
      this.selectivity = 1;
    }
  }
  setOutput(node) {
    this.#output = node;
  }
  get output() {
    assert(this.#output !== void 0, "Output not set");
    return this.#output;
  }
  closestJoinOrSource() {
    return "connection";
  }
  /**
   * Constraints are uniquely identified by their path through the
   * graph.
   *
   * FO represents all sub-joins as a single path.
   * UFO represents each sub-join as a separate path.
   * The first branch in a UFO will match the path of FO so no re-set needs to happen
   * when swapping from FO to UFO.
   *
   * FO swaps to UFO when a join inside FO-FI gets flipped.
   *
   * The max of the last element of the paths is the number of
   * root branches.
   */
  propagateConstraints(path, c, from, planDebugger) {
    const key = path.join(",");
    this.#constraints.set(key, c);
    this.#cachedConstraintCosts.clear();
    planDebugger?.log({
      type: "node-constraint",
      nodeType: "connection",
      node: this.name,
      branchPattern: path,
      constraint: c,
      from: from?.kind ?? "unknown"
    });
  }
  estimateCost(downstreamChildSelectivity, branchPattern, planDebugger) {
    const key = branchPattern.join(",");
    let cost = this.#cachedConstraintCosts.get(key);
    if (cost !== void 0) {
      return cost;
    }
    const constraint = this.#constraints.get(key);
    const mergedConstraint = mergeConstraints(
      this.#baseConstraints,
      constraint
    );
    const { startupCost, fanout, rows } = this.#model(
      this.table,
      this.#sort,
      this.#filters,
      mergedConstraint
    );
    cost = {
      startupCost,
      scanEst: this.limit === void 0 ? rows : Math.min(rows, this.limit / downstreamChildSelectivity),
      cost: 0,
      returnedRows: rows,
      selectivity: this.selectivity,
      limit: this.limit,
      fanout
    };
    this.#cachedConstraintCosts.set(key, cost);
    if (planDebugger) {
      planDebugger.log({
        type: "node-cost",
        nodeType: "connection",
        node: this.name,
        branchPattern,
        downstreamChildSelectivity,
        costEstimate: omitFanout(cost),
        filters: this.#filters,
        ordering: this.#sort
      });
    }
    return cost;
  }
  /**
   * Remove the limit from this connection.
   * Called when a parent join is flipped, making this connection part of an
   * outer loop that should produce all rows rather than stopping at the limit.
   */
  unlimit() {
    if (this.#isRoot) {
      return;
    }
    if (this.limit !== void 0) {
      this.limit = void 0;
    }
  }
  /**
   * Propagate unlimiting when a parent join is flipped.
   * For connections, we simply remove the limit.
   */
  propagateUnlimitFromFlippedJoin() {
    this.unlimit();
  }
  reset() {
    this.#constraints.clear();
    this.limit = this.#baseLimit;
    this.#cachedConstraintCosts.clear();
  }
  /**
   * Capture constraint state for snapshotting.
   * Used by PlannerGraph to save/restore planning state.
   */
  captureConstraints() {
    return new Map(this.#constraints);
  }
  /**
   * Restore constraint state from a snapshot.
   * Used by PlannerGraph to restore planning state.
   */
  restoreConstraints(constraints) {
    this.#constraints.clear();
    for (const [key, value] of constraints) {
      this.#constraints.set(key, value);
    }
    this.#cachedConstraintCosts.clear();
  }
  /** Get current constraints for debugging. */
  getConstraintsForDebug() {
    const record = {};
    for (const [key, value] of this.#constraints) {
      record[key] = value;
    }
    return record;
  }
  /** Get filters for debugging. */
  getFiltersForDebug() {
    return this.#filters;
  }
  /** Get sort/ordering for debugging. */
  getSortForDebug() {
    return this.#sort;
  }
  /** Get estimated cost for each constraint branch. */
  getConstraintCostsForDebug() {
    const record = {};
    for (const [key, value] of this.#cachedConstraintCosts) {
      record[key] = value;
    }
    return record;
  }
}
export {
  PlannerConnection
};
//# sourceMappingURL=planner-connection.js.map
