import SQLite3Database from "@rocicorp/zero-sqlite3";
import { buildSelectQuery } from "./query-builder.js";
import { compileInline } from "./internal/sql-inline.js";
import { assert } from "../../shared/src/asserts.js";
import { must } from "../../shared/src/must.js";
import { SQLiteStatFanout } from "./sqlite-stat-fanout.js";
function createSQLiteCostModel(db, tableSpecs) {
  const fanoutEstimator = new SQLiteStatFanout(db);
  return (tableName, sort, filters, constraint) => {
    const noSubqueryFilters = filters ? removeCorrelatedSubqueries(filters) : void 0;
    const { zqlSpec } = must(tableSpecs.get(tableName));
    const query = buildSelectQuery(
      tableName,
      zqlSpec,
      constraint,
      noSubqueryFilters,
      sort,
      void 0,
      // reverse is undefined here
      void 0
      // start is undefined here
    );
    const sql = compileInline(query);
    const stmt = db.prepare(sql);
    const loops = getScanstatusLoops(stmt);
    assert(
      loops.length > 0,
      `Expected scanstatus to return at least one loop for query: ${sql}`
    );
    const ret = estimateCost(
      loops,
      (columns) => fanoutEstimator.getFanout(tableName, columns)
    );
    return ret;
  };
}
function removeCorrelatedSubqueries(condition) {
  switch (condition.type) {
    case "correlatedSubquery":
      return void 0;
    case "simple":
      return condition;
    case "and": {
      const filtered = condition.conditions.map((c) => removeCorrelatedSubqueries(c)).filter((c) => c !== void 0);
      if (filtered.length === 0) return void 0;
      if (filtered.length === 1) return filtered[0];
      return { type: "and", conditions: filtered };
    }
    case "or": {
      const filtered = condition.conditions.map((c) => removeCorrelatedSubqueries(c)).filter((c) => c !== void 0);
      if (filtered.length === 0) return void 0;
      if (filtered.length === 1) return filtered[0];
      return { type: "or", conditions: filtered };
    }
  }
}
function getScanstatusLoops(stmt) {
  const loops = [];
  for (let idx = 0; ; idx++) {
    const selectId = stmt.scanStatus(
      idx,
      SQLite3Database.SQLITE_SCANSTAT_SELECTID,
      1
    );
    if (selectId === void 0) {
      break;
    }
    loops.push({
      selectId: must(selectId),
      parentId: must(
        stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_PARENTID, 1)
      ),
      explain: must(
        stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EXPLAIN, 1)
      ),
      est: must(stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EST, 1))
    });
  }
  return loops.sort((a, b) => a.selectId - b.selectId);
}
function estimateCost(scanstats, fanout) {
  const sorted = [...scanstats].sort((a, b) => a.selectId - b.selectId);
  let totalRows = 0;
  let totalCost = 0;
  const topLevelOps = sorted.filter((s) => s.parentId === 0);
  let firstLoop = true;
  for (const op of topLevelOps) {
    if (firstLoop) {
      totalRows = op.est;
      firstLoop = false;
    } else {
      if (op.explain.includes("ORDER BY")) {
        totalCost += btreeCost(totalRows);
      }
    }
  }
  return {
    rows: totalRows,
    startupCost: totalCost,
    fanout
  };
}
function btreeCost(rows) {
  return rows * Math.log2(rows) / 10;
}
export {
  btreeCost,
  createSQLiteCostModel
};
//# sourceMappingURL=sqlite-cost-model.js.map
