import { applyCodecsToDbRow, createColumnCodecsPlugin, createTableColumnCodecsResolver, randomId, } from '@syncular/core';
import { Kysely, CompiledQuery as KyselyCompiledQuery, } from 'kysely';
import { BaseSqliteDialect, BaseSqliteDriver } from 'kysely-generic-sqlite';
import { assertSyncularBlobPayloadLimit, syncularBlobInputSize, } from './blob-limits.js';
import { getSyncularClientStatus, SyncularClientLifecycle, } from './client.js';
import { createSyncularConsoleDiagnosticsPublisher } from './console-diagnostics.js';
import { isSyncularOfflineError } from './errors.js';
import { createMutationsApi, } from './mutations.js';
import { browserSyncularNetworkStatusSource } from './network.js';
import { assertSyncularReadonlySql } from './sql-safety.js';
import { createSyncularWorkerClient } from './worker-client.js';
export async function withSyncularSchemaWrites(database, callback) {
    const client = assertUnsafeSqlClient(database.client);
    const dialect = createSyncularDialect(client, { unsafeWrites: true });
    const db = new Kysely({ dialect });
    try {
        return await callback(db);
    }
    finally {
        await dialect.destroyLiveQueries();
        await db.destroy();
    }
}
export async function createSyncularDatabase(options) {
    const client = await createSyncularWorkerClient(options);
    const dialect = createSyncularDialect(client, {
        appTables: options.appTables,
        tableConfig: options.tableConfig,
    });
    const db = new Kysely({
        dialect,
        plugins: options.codecs
            ? [
                createColumnCodecsPlugin({
                    codecs: options.codecs,
                    dialect: 'sqlite',
                }),
            ]
            : undefined,
    });
    let closed = false;
    const localClientMode = (options.config.mode ?? 'remote') !== 'remote';
    const mutationSyncScheduler = createMutationSyncScheduler(client, {
        enabled: options.sync?.autoSyncAfterMutation ?? !localClientMode,
        debounceMs: options.sync?.mutationSyncDebounceMs ?? 10,
        network: options.sync?.network === false
            ? undefined
            : (options.sync?.network ?? browserSyncularNetworkStatusSource()),
        isClosed: () => closed,
    });
    const blobUploadScheduler = createBlobUploadScheduler(client, {
        enabled: options.sync?.autoProcessBlobUploadsAfterStore ?? false,
        debounceMs: options.sync?.blobUploadDebounceMs ?? 10,
        isClosed: () => closed,
    });
    const consoleDiagnostics = options.consoleDiagnostics === undefined ||
        options.consoleDiagnostics === false
        ? undefined
        : createSyncularConsoleDiagnosticsPublisher(client, {
            ...(options.consoleDiagnostics === true
                ? {}
                : options.consoleDiagnostics),
            config: options.config,
            isClosed: () => closed,
        });
    const mutations = createSyncularMutations({
        client,
        codecs: options.codecs,
        codecDialect: 'sqlite',
        tableConfig: options.tableConfig,
        readBaseVersion: (args) => readCurrentBaseVersion(client, args),
        afterCommit: () => mutationSyncScheduler.schedule(),
    });
    const leasedMutations = createSyncularMutations({
        client,
        requireAuthLease: true,
        codecs: options.codecs,
        codecDialect: 'sqlite',
        tableConfig: options.tableConfig,
        readBaseVersion: (args) => readCurrentBaseVersion(client, args),
        afterCommit: () => mutationSyncScheduler.schedule(),
    });
    const blobs = createSyncularBlobClient(client, {
        diagnostics: options.diagnostics,
        limits: options.blobLimits,
        afterStore: ({ options }) => {
            if (options?.immediate)
                return;
            blobUploadScheduler.schedule();
        },
    });
    const lifecycle = new SyncularClientLifecycle(client, {
        realtime: options.realtime ?? true,
        initialSync: options.lifecycle?.initialSync,
        syncOnRealtimeConnect: options.lifecycle?.syncOnRealtimeConnect,
        pollIntervalMs: options.pollIntervalMs,
        network: options.sync?.network,
    });
    const database = {
        db,
        client,
        dialect,
        mutations,
        leasedMutations,
        blobs,
        lifecycle,
        live: (query, liveOptions) => dialect.live(query, liveOptions),
        start: () => lifecycle.start(),
        stop: () => lifecycle.stop(),
        sync: () => lifecycle.sync(),
        on: (event, listener) => client.addEventListener(event, listener),
        getStatus: () => getSyncularClientStatus(client),
        setSubscriptions: (subscriptions) => client.setSubscriptions(subscriptions),
        resumeFromBackground: (syncOptions) => client.resumeFromBackground(syncOptions),
        issueAuthLease: (request) => client.issueAuthLease(request),
        upsertAuthLease: (lease) => client.upsertAuthLease(lease),
        authLease: (leaseId) => client.authLease(leaseId),
        activeAuthLeases: (actorId, nowMs) => client.activeAuthLeases(actorId, nowMs),
        diagnosticSnapshot: () => client.diagnosticSnapshot(),
        presence: {
            get: (scopeKey) => client.getPresence(scopeKey),
            join: (scopeKey, metadata) => client.joinPresence(scopeKey, metadata),
            leave: (scopeKey) => client.leavePresence(scopeKey),
            updateMetadata: (scopeKey, metadata) => client.updatePresenceMetadata(scopeKey, metadata),
            onChange: (listener) => client.addPresenceListener(listener),
        },
        conflicts: {
            list: () => client.conflictSummaries(),
            retryKeepLocal: (id) => client.retryConflictKeepLocal(id),
            resolve: (id, resolution) => client.resolveConflict(id, resolution),
        },
        async close() {
            if (closed)
                return;
            closed = true;
            mutationSyncScheduler.destroy();
            blobUploadScheduler.destroy();
            consoleDiagnostics?.destroy();
            try {
                await lifecycle.stop().catch(() => undefined);
                await dialect.destroyLiveQueries();
                await db.destroy();
            }
            finally {
                await client.close();
            }
        },
    };
    try {
        if (options.subscriptions) {
            await client.setSubscriptions(options.subscriptions);
        }
        if (options.lifecycle?.autoStart ?? !localClientMode) {
            await lifecycle.start();
        }
    }
    catch (error) {
        await database.close().catch(() => undefined);
        throw error;
    }
    return database;
}
function createMutationSyncScheduler(client, options) {
    let timer;
    let inFlight;
    let queued = false;
    const isOnline = () => options.network?.isOnline() !== false;
    const clear = () => {
        if (timer)
            clearTimeout(timer);
        timer = undefined;
    };
    const run = async () => {
        if (options.isClosed())
            return;
        if (!isOnline()) {
            queued = true;
            return;
        }
        if (inFlight) {
            queued = true;
            return;
        }
        inFlight = client
            .syncOnce()
            .then(() => undefined)
            .catch((error) => {
            if (!isOnline() || isSyncularOfflineError(error))
                queued = true;
        })
            .finally(() => {
            inFlight = undefined;
            if (queued && !options.isClosed() && isOnline()) {
                queued = false;
                schedule();
            }
        });
        await inFlight;
    };
    const schedule = () => {
        if (!options.enabled || options.isClosed())
            return;
        if (!isOnline()) {
            queued = true;
            clear();
            return;
        }
        if (inFlight) {
            queued = true;
            return;
        }
        clear();
        if (options.debounceMs === false || options.debounceMs <= 0) {
            queueMicrotask(() => void run());
            return;
        }
        timer = setTimeout(() => void run(), options.debounceMs);
    };
    const handleOnline = () => {
        if (!queued || options.isClosed())
            return;
        queued = false;
        schedule();
    };
    options.network?.addEventListener?.('online', handleOnline);
    return {
        schedule,
        destroy() {
            clear();
            queued = false;
            options.network?.removeEventListener?.('online', handleOnline);
        },
    };
}
function createBlobUploadScheduler(client, options) {
    let timer;
    let inFlight;
    let queued = false;
    const clear = () => {
        if (timer)
            clearTimeout(timer);
        timer = undefined;
    };
    const run = async () => {
        if (options.isClosed())
            return;
        if (inFlight) {
            queued = true;
            return;
        }
        inFlight = client
            .processBlobUploadQueue()
            .then(() => undefined)
            .catch(() => undefined)
            .finally(() => {
            inFlight = undefined;
            if (queued && !options.isClosed()) {
                queued = false;
                schedule();
            }
        });
        await inFlight;
    };
    const schedule = () => {
        if (!options.enabled || options.isClosed())
            return;
        if (inFlight) {
            queued = true;
            return;
        }
        clear();
        if (options.debounceMs === false || options.debounceMs <= 0) {
            queueMicrotask(() => void run());
            return;
        }
        timer = setTimeout(() => void run(), options.debounceMs);
    };
    return {
        schedule,
        destroy() {
            clear();
            queued = false;
        },
    };
}
export function createSyncularBlobClient(client, hooks = {}) {
    const assertRetrieveWithinLimit = (ref) => assertSyncularBlobPayloadLimit({
        operation: 'retrieve',
        size: ref.size,
        limits: hooks.limits,
        refHash: ref.hash,
        diagnostics: hooks.diagnostics,
    });
    return {
        async store(data, storeOptions) {
            assertSyncularBlobPayloadLimit({
                operation: 'store',
                size: syncularBlobInputSize(data),
                limits: hooks.limits,
                options: storeOptions,
                diagnostics: hooks.diagnostics,
            });
            const ref = await client.storeBlob(await toUint8Array(data), storeOptions);
            await hooks.afterStore?.({
                ref,
                ...(storeOptions === undefined ? {} : { options: storeOptions }),
            });
            return ref;
        },
        async retrieve(ref) {
            assertRetrieveWithinLimit(ref);
            return client.retrieveBlob(ref);
        },
        isLocal(hash) {
            return client.isBlobLocal(hash);
        },
        async preload(refs) {
            for (const ref of refs)
                assertRetrieveWithinLimit(ref);
            await Promise.all(refs.map((ref) => client.retrieveBlob(ref)));
        },
        processUploadQueue(options) {
            return client.processBlobUploadQueue(options);
        },
        getUploadQueueStats() {
            return client.blobUploadQueueStats();
        },
        getCacheStats() {
            return client.blobCacheStats();
        },
        pruneCache(maxBytes) {
            return client.pruneBlobCache(maxBytes);
        },
        clearCache() {
            return client.clearBlobCache();
        },
    };
}
export function createSyncularDialect(client, options = {}) {
    const driver = new SyncularDriver(client, options);
    const dialect = new BaseSqliteDialect(() => driver);
    dialect.live = (query, liveOptions) => driver.live(query, liveOptions);
    dialect.destroyLiveQueries = () => driver.destroy();
    return dialect;
}
export function createSyncularMutations(options) {
    return createMutationsApi(createSyncularCommit(options));
}
export function createSyncularCommit(options) {
    const idColumn = options.idColumn ?? 'id';
    const versionColumn = options.versionColumn ?? 'server_version';
    const omitColumns = options.omitColumns ?? [];
    const codecDialect = options.codecDialect ?? 'sqlite';
    const resolveTableCodecs = createTableColumnCodecsResolver(options.codecs, {
        dialect: codecDialect,
    });
    return async (fn) => {
        const operations = [];
        const batch = [];
        const localMutations = [];
        const txTableCache = new Map();
        const makeTxTable = (table) => {
            const cached = txTableCache.get(table);
            if (cached)
                return cached;
            const tableConfig = options.tableConfig?.[table];
            const tableIdColumn = tableConfig?.primaryKeyColumn ?? idColumn;
            const tableVersionColumn = tableConfig?.serverVersionColumn === undefined
                ? versionColumn
                : tableConfig.serverVersionColumn;
            const softDeleteColumn = tableConfig?.softDeleteColumn ?? null;
            const crdtYjsFields = serverMergeCrdtYjsFields(tableConfig?.crdtYjsFields ?? []);
            const prepareInsert = async (values) => {
                const raw = objectRecord(values);
                const rawId = raw[tableIdColumn];
                const id = typeof rawId === 'string' && rawId ? rawId : randomId();
                const row = { ...raw, [tableIdColumn]: id };
                const payload = sanitizeOperationPayload(row, {
                    omit: [
                        tableIdColumn,
                        ...(tableVersionColumn ? [tableVersionColumn] : []),
                        ...omitColumns,
                    ],
                });
                const syncPayload = stripCrdtYjsMaterializedPayloadFields(payload, crdtYjsFields);
                const localPayload = sanitizeOperationPayload(row, {
                    omit: [
                        tableIdColumn,
                        ...(tableVersionColumn ? [tableVersionColumn] : []),
                    ],
                });
                const transformedLocalPayload = await transformCrdtYjsMutationPayload({
                    client: options.client,
                    table,
                    rowId: id,
                    payload: localPayload,
                    crdtYjsFields,
                });
                const localDbPayload = applyCodecsToDbRow(transformedLocalPayload, resolveTableCodecs(table, transformedLocalPayload), codecDialect);
                const operation = {
                    table,
                    row_id: id,
                    op: 'upsert',
                    payload: syncPayload,
                    base_version: null,
                };
                return {
                    id,
                    operation,
                    localRow: { ...localDbPayload, [tableIdColumn]: id },
                };
            };
            const tableApi = {
                async insert(values) {
                    const prepared = await prepareInsert(values);
                    const { id, operation, localRow } = prepared;
                    operations.push(operation);
                    batch.push({ operation, localRow });
                    localMutations.push({ table, rowId: id, op: 'upsert' });
                    return id;
                },
                async insertMany(rows) {
                    if (rows.length === 0) {
                        throw new Error('insertMany requires at least one row');
                    }
                    const ids = [];
                    for (const row of rows) {
                        const { id, operation, localRow } = await prepareInsert(row);
                        ids.push(id);
                        operations.push(operation);
                        batch.push({ operation, localRow });
                        localMutations.push({ table, rowId: id, op: 'upsert' });
                    }
                    return ids;
                },
                async update(id, patch, opts) {
                    await applyUpsert(id, patch, opts, { mergeExistingLocalRow: true });
                },
                async delete(id, opts) {
                    const baseVersion = await resolveBaseVersion({
                        options,
                        table,
                        rowId: id,
                        idColumn: tableIdColumn,
                        versionColumn: tableVersionColumn,
                        explicit: opts?.baseVersion,
                    });
                    if (softDeleteColumn) {
                        const payload = { [softDeleteColumn]: 1 };
                        const localDbPayload = applyCodecsToDbRow(payload, resolveTableCodecs(table, payload), codecDialect);
                        const operation = {
                            table,
                            row_id: id,
                            op: 'upsert',
                            payload,
                            base_version: baseVersion,
                        };
                        operations.push(operation);
                        batch.push({
                            operation,
                            localRow: { ...localDbPayload, [tableIdColumn]: id },
                        });
                        localMutations.push({ table, rowId: id, op: 'upsert' });
                        return;
                    }
                    const operation = {
                        table,
                        row_id: id,
                        op: 'delete',
                        payload: null,
                        base_version: baseVersion,
                    };
                    operations.push(operation);
                    batch.push({ operation, localRow: null });
                    localMutations.push({ table, rowId: id, op: 'delete' });
                },
                async upsert(id, patch, opts) {
                    await applyUpsert(id, patch, opts, { mergeExistingLocalRow: false });
                },
            };
            const applyUpsert = async (id, patch, opts, behavior) => {
                const rawPatch = objectRecord(patch);
                const payload = sanitizeOperationPayload(rawPatch, {
                    omit: [
                        tableIdColumn,
                        ...(tableVersionColumn ? [tableVersionColumn] : []),
                        ...omitColumns,
                    ],
                });
                const syncPayload = stripCrdtYjsMaterializedPayloadFields(payload, crdtYjsFields);
                const existingLocalRow = behavior.mergeExistingLocalRow
                    ? await readExistingLocalRow({
                        client: options.client,
                        table,
                        rowId: id,
                        idColumn: tableIdColumn,
                    })
                    : null;
                const rawLocalPayload = existingLocalRow
                    ? { ...existingLocalRow, ...rawPatch }
                    : rawPatch;
                const localPayload = sanitizeOperationPayload(rawLocalPayload, {
                    omit: [
                        tableIdColumn,
                        ...(tableVersionColumn ? [tableVersionColumn] : []),
                    ],
                });
                const existingRow = await readCrdtYjsExistingRow({
                    client: options.client,
                    table,
                    rowId: id,
                    idColumn: tableIdColumn,
                    crdtYjsFields,
                });
                const transformedLocalPayload = await transformCrdtYjsMutationPayload({
                    client: options.client,
                    table,
                    rowId: id,
                    payload: localPayload,
                    existingRow,
                    crdtYjsFields,
                });
                const localDbPayload = applyCodecsToDbRow(transformedLocalPayload, resolveTableCodecs(table, transformedLocalPayload), codecDialect);
                const operation = {
                    table,
                    row_id: id,
                    op: 'upsert',
                    payload: syncPayload,
                    base_version: await resolveBaseVersion({
                        options,
                        table,
                        rowId: id,
                        idColumn: tableIdColumn,
                        versionColumn: tableVersionColumn,
                        explicit: opts?.baseVersion,
                    }),
                };
                operations.push(operation);
                batch.push({
                    operation,
                    localRow: { ...localDbPayload, [tableIdColumn]: id },
                });
                localMutations.push({ table, rowId: id, op: 'upsert' });
            };
            txTableCache.set(table, tableApi);
            return tableApi;
        };
        const tx = new Proxy({}, {
            get(_target, prop) {
                if (prop === 'then')
                    return undefined;
                if (typeof prop !== 'string')
                    return undefined;
                return makeTxTable(prop);
            },
        });
        const result = await fn(tx);
        if (operations.length === 0)
            throw new Error('No mutations were enqueued');
        const clientCommitId = options.requireAuthLease
            ? await requireLeasedMutationClient(options.client).applyLeasedMutationsCommit(batch)
            : await options.client.applyMutationsCommit(batch);
        const meta = {
            operations,
            localMutations,
            clientCommitIds: [clientCommitId],
        };
        await options.afterCommit?.(meta);
        return {
            result,
            receipt: { commitId: clientCommitId, clientCommitId },
            meta,
        };
    };
}
function requireLeasedMutationClient(client) {
    if (typeof client.applyLeasedMutationsCommit !== 'function') {
        throw new Error('Syncular leased mutations require applyLeasedMutationsCommit support');
    }
    if (typeof client.applyLeasedMutation !== 'function') {
        throw new Error('Syncular leased mutations require applyLeasedMutation support');
    }
    return client;
}
class SyncularDriver extends BaseSqliteDriver {
    client;
    #listeners = new Map();
    #appTables;
    #tableConfig;
    constructor(client, options = {}) {
        super(async () => {
            this.conn = new SyncularConnection(client, options.unsafeWrites ?? false);
        });
        this.client = client;
        this.#appTables =
            options.appTables == null
                ? undefined
                : new Set(normalizeLiveQueryTables(options.appTables));
        this.#tableConfig = options.tableConfig;
    }
    async live(query, options) {
        const compiled = query.compile();
        const tables = normalizeLiveQueryTables(options.tables ?? inferTablesFromCompiledQuery(compiled, this.#appTables), this.#appTables);
        if (tables.length === 0) {
            throw new Error('Could not infer live query table dependencies. Pass { tables: [...] } explicitly.');
        }
        const dependencyHints = normalizeLiveQueryDependencyHints(options.dependencyHints ??
            inferDependencyHintsFromCompiledQuery(compiled, tables, this.#tableConfig), tables, this.#appTables);
        const snapshot = await this.client.subscribeQuery(compiled.sql, compiled.parameters, tables, dependencyHints);
        const listener = (event) => {
            const typed = event;
            options.onChange(typed.rows, { ...typed, initial: false });
        };
        this.#listeners.set(snapshot.id, listener);
        this.client.addLiveQueryListener(snapshot.id, listener);
        options.onChange(snapshot.rows, {
            queryId: snapshot.id,
            version: Date.now(),
            changedRows: [],
            rows: snapshot.rows,
            initial: true,
        });
        return {
            id: snapshot.id,
            unsubscribe: () => {
                void this.#unsubscribeQuery(snapshot.id).catch(() => undefined);
            },
        };
    }
    async beginTransaction(connection) {
        await connection.executeQuery(KyselyCompiledQuery.raw('begin'));
    }
    async destroy() {
        const ids = [...this.#listeners.keys()];
        for (const id of ids)
            this.client.removeLiveQueryListener(id);
        this.#listeners.clear();
        await Promise.allSettled(ids.map((id) => this.client.unsubscribeQuery(id)));
    }
    async #unsubscribeQuery(id) {
        if (!this.#listeners.has(id))
            return;
        this.#listeners.delete(id);
        this.client.removeLiveQueryListener(id);
        await this.client.unsubscribeQuery(id);
    }
}
class SyncularConnection {
    client;
    unsafeWrites;
    constructor(client, unsafeWrites) {
        this.client = client;
        this.unsafeWrites = unsafeWrites;
    }
    async executeQuery(compiledQuery) {
        if (!this.unsafeWrites)
            assertSyncularReadonlySql(compiledQuery.sql);
        const result = this.unsafeWrites
            ? await assertUnsafeSqlClient(this.client).executeUnsafeSql(compiledQuery.sql, compiledQuery.parameters)
            : await this.client.executeSql(compiledQuery.sql, compiledQuery.parameters);
        return {
            rows: result.rows,
            numAffectedRows: result.numAffectedRows == null
                ? undefined
                : BigInt(result.numAffectedRows),
            insertId: result.insertId == null ? undefined : BigInt(result.insertId),
        };
    }
    streamQuery(_compiledQuery, _chunkSize) {
        throw new Error('syncular sqlite dialect does not support streaming');
    }
}
function assertUnsafeSqlClient(client) {
    const maybe = client;
    if (typeof maybe.executeUnsafeSql === 'function') {
        return client;
    }
    throw new Error('Syncular schema installation requires an internal unsafe SQL client.');
}
function objectRecord(value) {
    if (!value || typeof value !== 'object' || Array.isArray(value))
        return {};
    return { ...value };
}
async function toUint8Array(data) {
    if (data instanceof Uint8Array)
        return data;
    return new Uint8Array(await data.arrayBuffer());
}
function sanitizeOperationPayload(payload, args) {
    if (args.omit.length === 0)
        return payload;
    const omit = new Set(args.omit);
    const out = {};
    for (const [key, value] of Object.entries(payload)) {
        if (!omit.has(key))
            out[key] = value;
    }
    return out;
}
function serverMergeCrdtYjsFields(fields) {
    return fields.filter((field) => field.syncMode === undefined || field.syncMode === 'server-merge');
}
async function transformCrdtYjsMutationPayload(args) {
    if (args.crdtYjsFields.length === 0 || !('__yjs' in args.payload)) {
        return args.payload;
    }
    return args.client.applyYjsEnvelopeToPayload({
        table: args.table,
        rowId: args.rowId,
        payload: args.payload,
        existingRow: args.existingRow ?? null,
        rules: args.crdtYjsFields.map((field) => ({
            table: args.table,
            field: field.field,
            stateColumn: field.stateColumn,
            containerKey: field.containerKey ?? field.field,
            rowIdField: field.rowIdField ?? 'id',
            kind: field.kind ?? 'text',
        })),
        stripEnvelope: true,
    });
}
function stripCrdtYjsMaterializedPayloadFields(payload, crdtYjsFields) {
    const envelope = objectRecord(payload.__yjs);
    if (crdtYjsFields.length === 0 || Object.keys(envelope).length === 0) {
        return payload;
    }
    let next = null;
    const ensureNext = () => {
        next ??= { ...payload };
        return next;
    };
    for (const field of crdtYjsFields) {
        if (!(field.field in envelope))
            continue;
        delete ensureNext()[field.field];
        delete ensureNext()[field.stateColumn];
    }
    return next ?? payload;
}
async function readCrdtYjsExistingRow(args) {
    if (args.crdtYjsFields.length === 0)
        return null;
    const columns = new Set([args.idColumn]);
    for (const field of args.crdtYjsFields) {
        columns.add(field.field);
        columns.add(field.stateColumn);
    }
    const result = await args.client.executeSql(`select ${[...columns].map(quoteSqlIdentifier).join(', ')} from ${quoteSqlIdentifier(args.table)} where ${quoteSqlIdentifier(args.idColumn)} = ? limit 1`, [args.rowId]);
    return result.rows[0] ?? null;
}
async function readExistingLocalRow(args) {
    const result = await args.client.executeSql(`select * from ${quoteSqlIdentifier(args.table)} where ${quoteSqlIdentifier(args.idColumn)} = ? limit 1`, [args.rowId]);
    return result.rows[0] ?? null;
}
function quoteSqlIdentifier(identifier) {
    if (!identifier)
        throw new Error('SQLite identifier cannot be empty');
    return `"${identifier.replace(/"/g, '""')}"`;
}
async function readCurrentBaseVersion(client, args) {
    const result = await client.executeSql(`select ${quoteSqlIdentifier(args.versionColumn)} as version from ${quoteSqlIdentifier(args.table)} where ${quoteSqlIdentifier(args.idColumn)} = ? limit 1`, [args.rowId]);
    return coerceBaseVersion(result.rows[0]?.version);
}
function coerceBaseVersion(value) {
    if (value == null)
        return null;
    const numeric = typeof value === 'number' ? value : Number(value);
    if (!Number.isFinite(numeric) || numeric < 0)
        return null;
    return numeric;
}
async function resolveBaseVersion(args) {
    if (args.explicit !== undefined)
        return coerceBaseVersion(args.explicit);
    if (!args.versionColumn)
        return null;
    const readBaseVersion = args.options.readBaseVersion ??
        ((readArgs) => readCurrentBaseVersion(args.options.client, readArgs));
    return readBaseVersion({
        table: args.table,
        rowId: args.rowId,
        idColumn: args.idColumn,
        versionColumn: args.versionColumn,
    });
}
function inferTablesFromCompiledQuery(query, appTables) {
    const tables = new Set();
    collectTables(query.query, tables);
    return [...tables].filter((table) => appTables == null || appTables.has(table));
}
function inferDependencyHintsFromCompiledQuery(query, tables, tableConfig) {
    if (tables.length !== 1)
        return [];
    const table = tables[0];
    const primaryKey = tableConfig?.[table]?.primaryKeyColumn ?? 'id';
    const rowIds = primaryKeyEqualityValues(query.query, table, primaryKey);
    return rowIds.length > 0 ? [{ table, rowIds }] : [];
}
function normalizeLiveQueryDependencyHints(hints, tables, appTables) {
    const observedTables = new Set(tables);
    const normalized = [];
    for (const hint of hints) {
        if (hint.table.length === 0) {
            throw new Error('Live query dependency hint tables must not be empty.');
        }
        if (!observedTables.has(hint.table)) {
            throw new Error(`Live query dependency hint table ${hint.table} is not one of the observed tables.`);
        }
        if (appTables != null && !appTables.has(hint.table)) {
            throw new Error(`Live query dependency hint table ${hint.table} is not part of the generated app schema.`);
        }
        const rowIds = uniqueDefinedStrings(hint.rowIds);
        const fields = uniqueDefinedStrings(hint.fields);
        normalized.push({
            table: hint.table,
            ...(rowIds.length > 0 ? { rowIds } : {}),
            ...(fields.length > 0 ? { fields } : {}),
        });
    }
    return normalized;
}
function primaryKeyEqualityValues(query, table, primaryKey) {
    if (!isOperationNode(query) || query.kind !== 'SelectQueryNode')
        return [];
    const where = query.where;
    if (!isOperationNode(where) || where.kind !== 'WhereNode')
        return [];
    return collectConjunctivePrimaryKeyValues(where.where, table, primaryKey);
}
function collectConjunctivePrimaryKeyValues(node, table, primaryKey) {
    if (!isOperationNode(node))
        return [];
    if (node.kind === 'AndNode') {
        return uniqueStrings([
            ...collectConjunctivePrimaryKeyValues(node.left, table, primaryKey),
            ...collectConjunctivePrimaryKeyValues(node.right, table, primaryKey),
        ]);
    }
    const equality = primaryKeyEqualityValue(node, table, primaryKey);
    return equality == null ? [] : [equality];
}
function primaryKeyEqualityValue(node, table, primaryKey) {
    if (!isOperationNode(node) || node.kind !== 'BinaryOperationNode') {
        return undefined;
    }
    const operator = node.operator;
    if (!isOperationNode(operator) ||
        operator.kind !== 'OperatorNode' ||
        operator.operator !== '=') {
        return undefined;
    }
    return (equalityValueForReference(node.leftOperand, node.rightOperand, table, primaryKey) ??
        equalityValueForReference(node.rightOperand, node.leftOperand, table, primaryKey));
}
function equalityValueForReference(reference, value, table, primaryKey) {
    if (!referenceMatchesColumn(reference, table, primaryKey))
        return undefined;
    if (!isOperationNode(value) || value.kind !== 'ValueNode')
        return undefined;
    if (typeof value.value === 'string')
        return value.value;
    if (typeof value.value === 'number' && Number.isFinite(value.value)) {
        return String(value.value);
    }
    return undefined;
}
function referenceMatchesColumn(reference, table, column) {
    if (!isOperationNode(reference) || reference.kind !== 'ReferenceNode') {
        return false;
    }
    if (reference.table != null &&
        tableNameFromTableNode(reference.table) !== table) {
        return false;
    }
    const columnNode = reference.column;
    if (!isOperationNode(columnNode) || columnNode.kind !== 'ColumnNode') {
        return false;
    }
    const identifier = columnNode.column;
    return (isOperationNode(identifier) &&
        identifier.kind === 'IdentifierNode' &&
        identifier.name === column);
}
function uniqueStrings(values) {
    return [...new Set(values)];
}
function uniqueDefinedStrings(values) {
    if (values == null)
        return [];
    const unique = [];
    for (const value of values) {
        if (value.length === 0) {
            throw new Error('Live query dependency hint values must not be empty.');
        }
        if (!unique.includes(value))
            unique.push(value);
    }
    return unique;
}
function collectTables(node, tables) {
    if (Array.isArray(node)) {
        for (const item of node)
            collectTables(item, tables);
        return;
    }
    if (!isOperationNode(node))
        return;
    if (node.kind === 'TableNode')
        collectTableNode(node, tables);
    for (const value of Object.values(node))
        collectTables(value, tables);
}
function collectTableNode(node, tables) {
    const table = tableNameFromTableNode(node);
    if (table != null)
        tables.add(table);
}
function tableNameFromTableNode(node) {
    if (!isOperationNode(node))
        return undefined;
    if (node.kind === 'AliasNode')
        return tableNameFromTableNode(node.node);
    if (node.kind !== 'TableNode')
        return undefined;
    const table = node.table;
    if (!isOperationNode(table) || table.kind !== 'SchemableIdentifierNode') {
        return undefined;
    }
    const identifier = table.identifier;
    if (!isOperationNode(identifier) || identifier.kind !== 'IdentifierNode') {
        return undefined;
    }
    return typeof identifier.name === 'string' ? identifier.name : undefined;
}
function isOperationNode(value) {
    return Boolean(value && typeof value === 'object' && 'kind' in value);
}
function normalizeLiveQueryTables(tables, appTables) {
    const normalized = [];
    for (const table of tables) {
        if (table.length === 0) {
            throw new Error('Live query table dependencies must not contain empty names.');
        }
        if (appTables != null && !appTables.has(table)) {
            throw new Error(`Live query table ${table} is not part of the generated app schema.`);
        }
        if (!normalized.includes(table))
            normalized.push(table);
    }
    return normalized;
}
//# sourceMappingURL=database.js.map