import { assert, unreachable } from "../../shared/src/asserts.js";
import { must } from "../../shared/src/must.js";
import { makeComparator } from "../../zql/src/ivm/data.js";
import { assertOrderingIncludesPK } from "../../zql/src/query/complete-ordering.js";
import { createPredicate, transformFilters } from "../../zql/src/builder/filter.js";
import { genPushAndWriteWithSplitEdit, generateWithOverlay, generateWithOverlayUnordered, generateWithStart } from "../../zql/src/ivm/memory-source.js";
import { timeSampled } from "../../otel/src/maybe-time.js";
import { compile, format, sql } from "./internal/sql.js";
import { StatementCache } from "./internal/statement-cache.js";
import { buildSelectQuery, toSQLiteType } from "./query-builder.js";
import SQLite3Database from "@rocicorp/zero-sqlite3";
//#region ../zqlite/src/table-source.ts
var eventCount = 0;
/**
* A source that is backed by a SQLite table.
*
* Values are written to the backing table _after_ being vended by the source.
*
* This ordering of events is to ensure self joins function properly. That is,
* we can't reveal a value to an output before it has been pushed to that output.
*
* The code is fairly straightforward except for:
* 1. Dealing with a `fetch` that has a basis of `before`.
* 2. Dealing with compound orders that have differing directions (a ASC, b DESC, c ASC)
*
* See comments in relevant functions for more details.
*/
var TableSource = class {
	#dbCache = /* @__PURE__ */ new WeakMap();
	#connections = [];
	#table;
	#columns;
	#uniqueIndexes;
	#primaryKey;
	#logConfig;
	#lc;
	#shouldYield;
	#stmts;
	#overlay;
	#pushEpoch = 0;
	/**
	* @param shouldYield a function called after each row is read from the database,
	* which should return true if the source should yield the special 'yield' value
	* to yield control back to the caller at the end of the pipeline.  Can
	* also throw an error to abort the pipeline processing.
	*/
	constructor(logContext, logConfig, db, tableName, columns, primaryKey, shouldYield = () => false) {
		this.#lc = logContext;
		this.#logConfig = logConfig;
		this.#table = tableName;
		this.#columns = columns;
		this.#uniqueIndexes = getUniqueIndexes(db, tableName);
		this.#primaryKey = primaryKey;
		this.#stmts = this.#getStatementsFor(db);
		this.#shouldYield = shouldYield;
		assert(this.#uniqueIndexes.has(JSON.stringify(primaryKey.toSorted())), `primary key ${primaryKey} does not have a UNIQUE index`);
	}
	get tableSchema() {
		return {
			name: this.#table,
			columns: this.#columns,
			primaryKey: this.#primaryKey
		};
	}
	/**
	* Sets the db (snapshot) to use, to facilitate the Snapshotter leapfrog
	* algorithm for concurrent traversal of historic timelines.
	*/
	setDB(db) {
		this.#stmts = this.#getStatementsFor(db);
	}
	#getStatementsFor(db) {
		const cached = this.#dbCache.get(db);
		if (cached) return cached;
		const stmts = {
			cache: new StatementCache(db),
			insert: db.prepare(compile(sql`INSERT INTO ${sql.ident(this.#table)} (${sql.join(Object.keys(this.#columns).map((c) => sql.ident(c)), ", ")}) VALUES (${sql.__dangerous__rawValue(Array.from({ length: Object.keys(this.#columns).length }).fill("?").join(","))})`)),
			delete: db.prepare(compile(sql`DELETE FROM ${sql.ident(this.#table)} WHERE ${sql.join(this.#primaryKey.map((k) => sql`${sql.ident(k)}=?`), " AND ")}`)),
			update: Object.keys(this.#columns).length > this.#primaryKey.length ? db.prepare(compile(sql`UPDATE ${sql.ident(this.#table)} SET ${sql.join(nonPrimaryKeys(this.#columns, this.#primaryKey).map((c) => sql`${sql.ident(c)}=?`), ",")} WHERE ${sql.join(this.#primaryKey.map((k) => sql`${sql.ident(k)}=?`), " AND ")}`)) : void 0,
			checkExists: db.prepare(compile(sql`SELECT 1 AS "exists" FROM ${sql.ident(this.#table)} WHERE ${sql.join(this.#primaryKey.map((k) => sql`${sql.ident(k)}=?`), " AND ")} LIMIT 1`)),
			getExisting: db.prepare(compile(sql`SELECT * FROM ${sql.ident(this.#table)} WHERE ${sql.join(this.#primaryKey.map((k) => sql`${sql.ident(k)}=?`), " AND ")}`))
		};
		this.#dbCache.set(db, stmts);
		return stmts;
	}
	get #allColumns() {
		return sql.join(Object.keys(this.#columns).map((c) => sql.ident(c)), sql`,`);
	}
	#getSchema(connection, unordered) {
		return {
			tableName: this.#table,
			columns: this.#columns,
			primaryKey: this.#primaryKey,
			sort: unordered ? void 0 : connection.sort,
			relationships: {},
			isHidden: false,
			system: "client",
			compareRows: connection.compareRows
		};
	}
	connect(sort, filters, splitEditKeys, debug) {
		const transformedFilters = transformFilters(filters);
		const unordered = sort === void 0;
		const primaryKeySort = this.#primaryKey.map((k) => [k, "asc"]);
		const input = {
			getSchema: () => schema,
			fetch: (req) => this.#fetch(req, connection),
			setOutput: (output) => {
				connection.output = output;
			},
			destroy: () => {
				const idx = this.#connections.indexOf(connection);
				assert(idx !== -1, "Connection not found");
				this.#connections.splice(idx, 1);
			},
			fullyAppliedFilters: !transformedFilters.conditionsRemoved
		};
		const connection = {
			input,
			debug,
			output: void 0,
			sort,
			splitEditKeys,
			filters: transformedFilters.filters ? {
				condition: transformedFilters.filters,
				predicate: createPredicate(transformedFilters.filters)
			} : void 0,
			compareRows: sort ? makeComparator(sort) : makeComparator(primaryKeySort),
			lastPushedEpoch: 0
		};
		const schema = this.#getSchema(connection, unordered);
		if (!unordered) assertOrderingIncludesPK(sort, this.#primaryKey);
		this.#connections.push(connection);
		return input;
	}
	toSQLiteRow(row) {
		return Object.fromEntries(Object.entries(row).map(([key, value]) => [key, toSQLiteType(value, this.#columns[key].type)]));
	}
	*#fetch(req, connection) {
		const { sort, debug } = connection;
		const sqlAndBindings = format(this.#requestToSQL(req, connection.filters?.condition, sort));
		const cachedStatement = this.#stmts.cache.get(sqlAndBindings.text);
		cachedStatement.statement.safeIntegers(true);
		const rowIterator = cachedStatement.statement.iterate(...sqlAndBindings.values);
		try {
			debug?.initQuery(this.#table, sqlAndBindings.text);
			if (sort) {
				const comparator = makeComparator(sort, req.reverse);
				yield* generateWithStart(generateWithYields(generateWithOverlay(req.start?.row, this.#mapFromSQLiteTypes(this.#columns, rowIterator, sqlAndBindings.text, debug), req.constraint, this.#overlay, connection.lastPushedEpoch, comparator, connection.filters?.predicate), this.#shouldYield), req.start, comparator);
			} else yield* generateWithYields(generateWithOverlayUnordered(this.#mapFromSQLiteTypes(this.#columns, rowIterator, sqlAndBindings.text, debug), req.constraint, this.#overlay, connection.lastPushedEpoch, this.#primaryKey, connection.filters?.predicate), this.#shouldYield);
		} finally {
			rowIterator.return?.();
			if (debug) {
				let totalNvisit = 0;
				let i = 0;
				while (true) {
					const nvisit = cachedStatement.statement.scanStatus(i++, SQLite3Database.SQLITE_SCANSTAT_NVISIT, 1);
					if (nvisit === void 0) break;
					totalNvisit += Number(nvisit);
				}
				if (totalNvisit !== 0) debug.recordNVisit(this.#table, sqlAndBindings.text, totalNvisit);
				cachedStatement.statement.scanStatusReset();
			}
			this.#stmts.cache.return(cachedStatement);
		}
	}
	*#mapFromSQLiteTypes(valueTypes, rowIterator, query, debug) {
		let result;
		do {
			result = timeSampled(this.#lc, ++eventCount, this.#logConfig.ivmSampling, () => rowIterator.next(), this.#logConfig.slowRowThreshold, () => `table-source.next took too long for ${query}. Are you missing an index?`);
			if (result.done) break;
			const row = fromSQLiteTypes(valueTypes, result.value, this.#table);
			debug?.rowVended(this.#table, query, row);
			yield row;
		} while (!result.done);
	}
	*push(change) {
		for (const result of this.genPush(change)) if (result === "yield") yield result;
	}
	*genPush(change) {
		const exists = (row) => this.#stmts.checkExists.get(...toSQLiteTypes(this.#primaryKey, row, this.#columns))?.exists === 1;
		const setOverlay = (o) => this.#overlay = o;
		const writeChange = (c) => this.#writeChange(c);
		yield* genPushAndWriteWithSplitEdit(this.#connections, change, exists, setOverlay, writeChange, () => ++this.#pushEpoch);
	}
	#writeChange(change) {
		switch (change.type) {
			case "add":
				this.#stmts.insert.run(...toSQLiteTypes(Object.keys(this.#columns), change.row, this.#columns));
				break;
			case "remove":
				this.#stmts.delete.run(...toSQLiteTypes(this.#primaryKey, change.row, this.#columns));
				break;
			case "edit":
				if (canUseUpdate(change.oldRow, change.row, this.#columns, this.#primaryKey)) {
					const mergedRow = {
						...change.oldRow,
						...change.row
					};
					const params = [...nonPrimaryValues(this.#columns, this.#primaryKey, mergedRow), ...toSQLiteTypes(this.#primaryKey, mergedRow, this.#columns)];
					must(this.#stmts.update).run(params);
				} else {
					this.#stmts.delete.run(...toSQLiteTypes(this.#primaryKey, change.oldRow, this.#columns));
					this.#stmts.insert.run(...toSQLiteTypes(Object.keys(this.#columns), change.row, this.#columns));
				}
				break;
			default: unreachable(change);
		}
	}
	#getRowStmtCache = /* @__PURE__ */ new Map();
	#getRowStmt(keyCols) {
		const keyString = JSON.stringify(keyCols);
		let stmt = this.#getRowStmtCache.get(keyString);
		if (!stmt) {
			stmt = compile(sql`SELECT ${this.#allColumns} FROM ${sql.ident(this.#table)} WHERE ${sql.join(keyCols.map((k) => sql`${sql.ident(k)}=?`), sql` AND`)}`);
			this.#getRowStmtCache.set(keyString, stmt);
		}
		return stmt;
	}
	/**
	* Retrieves a row from the backing DB by a unique key, or `undefined` if such a
	* row does not exist. This is not used in the IVM pipeline but is useful
	* for retrieving data that is consistent with the state (and type
	* semantics) of the pipeline. Note that this key may not necessarily correspond
	* to the `primaryKey` with which this TableSource.
	*/
	getRow(rowKey) {
		const keyCols = Object.keys(rowKey);
		const stmt = this.#getRowStmt(keyCols);
		const row = this.#stmts.cache.use(stmt, (cached) => cached.statement.safeIntegers(true).get(...toSQLiteTypes(keyCols, rowKey, this.#columns)));
		if (row) return fromSQLiteTypes(this.#columns, row, this.#table);
		return row;
	}
	#requestToSQL(request, filters, order) {
		return buildSelectQuery(this.#table, this.#columns, request.constraint, filters, order, request.reverse, request.start);
	}
};
function getUniqueIndexes(db, tableName) {
	const sqlAndBindings = format(sql`
    SELECT idx.name, json_group_array(col.name) as columnsJSON
      FROM sqlite_master as idx
      JOIN pragma_index_list(idx.tbl_name) AS info ON info.name = idx.name
      JOIN pragma_index_info(idx.name) as col
      WHERE idx.tbl_name = ${tableName} AND
            idx.type = 'index' AND 
            info."unique" != 0
      GROUP BY idx.name
      ORDER BY idx.name`);
	const indexes = db.prepare(sqlAndBindings.text).all(...sqlAndBindings.values);
	return new Map(indexes.map(({ columnsJSON }) => {
		const columns = JSON.parse(columnsJSON);
		const set = new Set(columns);
		return [JSON.stringify(columns.sort()), set];
	}));
}
function toSQLiteTypes(columns, row, columnTypes) {
	return columns.map((col) => toSQLiteType(row[col], columnTypes[col].type));
}
function fromSQLiteTypes(valueTypes, row, tableName) {
	const newRow = {};
	for (const key of Object.keys(row)) {
		const valueType = valueTypes[key];
		if (valueType === void 0) {
			const columnList = Object.keys(valueTypes).sort().join(", ");
			throw new Error(`Invalid column "${key}" for table "${tableName}". Synced columns include ${columnList}`);
		}
		newRow[key] = fromSQLiteType(valueType.type, row[key], key, tableName);
	}
	return newRow;
}
function fromSQLiteType(valueType, v, column, tableName) {
	if (v === null) return null;
	switch (valueType) {
		case "boolean": return !!v;
		case "number":
		case "string":
		case "null":
			if (typeof v === "bigint") {
				if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) throw new UnsupportedValueError(`value ${v} (in ${tableName}.${column}) is outside of supported bounds`);
				return Number(v);
			}
			return v;
		case "json": try {
			return JSON.parse(v);
		} catch (error) {
			throw new UnsupportedValueError(`Failed to parse JSON for ${tableName}.${column}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
		}
	}
}
var UnsupportedValueError = class extends Error {};
function canUseUpdate(oldRow, row, columns, primaryKey) {
	for (const pk of primaryKey) if (oldRow[pk] !== row[pk]) return false;
	return Object.keys(columns).length > primaryKey.length;
}
function nonPrimaryValues(columns, primaryKey, row) {
	return nonPrimaryKeys(columns, primaryKey).map((c) => toSQLiteType(row[c], columns[c].type));
}
function nonPrimaryKeys(columns, primaryKey) {
	return Object.keys(columns).filter((c) => !primaryKey.includes(c));
}
function* generateWithYields(stream, shouldYield) {
	for (const n of stream) {
		if (shouldYield()) yield "yield";
		yield n;
	}
}
//#endregion
export { TableSource, fromSQLiteTypes };

//# sourceMappingURL=table-source.js.map