import { trace } from '@opentelemetry/api';
import { startAsyncSpan } from "../../../../otel/src/span.js";
import { version } from "../../../../otel/src/version.js";
import { assert } from "../../../../shared/src/asserts.js";
import { CustomKeyMap } from "../../../../shared/src/custom-key-map.js";
import { CustomKeySet } from "../../../../shared/src/custom-key-set.js";
import { deepEqual, } from "../../../../shared/src/json.js";
import { sleep } from "../../../../shared/src/sleep.js";
import * as v from "../../../../shared/src/valita.js";
import { astSchema } from "../../../../zero-protocol/src/ast.js";
import { clientSchemaSchema } from "../../../../zero-protocol/src/client-schema.js";
import { ErrorKind } from "../../../../zero-protocol/src/error-kind.js";
import { DEFAULT_TTL_MS } from "../../../../zql/src/query/ttl.js";
import * as Mode from "../../db/mode-enum.js";
import { TransactionPool } from "../../db/transaction-pool.js";
import { recordRowsSynced } from "../../server/anonymous-otel-start.js";
import { ErrorForClient, ErrorWithLevel } from "../../types/error-for-client.js";
import { rowIDString } from "../../types/row-key.js";
import { cvrSchema, upstreamSchema } from "../../types/shards.js";
import { RowRecordCache } from "./row-record-cache.js";
import {} from "./schema/cvr.js";
import { cmpVersions, EMPTY_CVR_VERSION, queryRecordToQueryRow, versionFromString, versionString, } from "./schema/types.js";
import { ttlClockAsNumber, ttlClockFromNumber, } from "./ttl-clock.js";
const tracer = trace.getTracer('cvr-store', version);
function asQuery(row) {
    const maybeVersion = (s) => s === null ? undefined : versionFromString(s);
    if (row.clientAST === null) {
        // custom query
        assert(row.queryName !== null && row.queryArgs !== null, 'queryName and queryArgs must be set for custom queries');
        return {
            type: 'custom',
            id: row.queryHash,
            name: row.queryName,
            args: row.queryArgs,
            patchVersion: maybeVersion(row.patchVersion),
            clientState: {},
            transformationHash: row.transformationHash ?? undefined,
            transformationVersion: maybeVersion(row.transformationVersion),
        };
    }
    const ast = astSchema.parse(row.clientAST);
    return row.internal
        ? {
            type: 'internal',
            id: row.queryHash,
            ast,
            transformationHash: row.transformationHash ?? undefined,
            transformationVersion: maybeVersion(row.transformationVersion),
        }
        : {
            type: 'client',
            id: row.queryHash,
            ast,
            patchVersion: maybeVersion(row.patchVersion),
            clientState: {},
            transformationHash: row.transformationHash ?? undefined,
            transformationVersion: maybeVersion(row.transformationVersion),
        };
}
// The time to wait between load attempts.
const LOAD_ATTEMPT_INTERVAL_MS = 500;
// The maximum number of load() attempts if the rowsVersion is behind.
// This currently results in a maximum catchup time of ~5 seconds, after
// which we give up and consider the CVR invalid.
//
// TODO: Make this configurable with something like --max-catchup-wait-ms,
//       as it is technically application specific.
const MAX_LOAD_ATTEMPTS = 10;
export class CVRStore {
    #schema;
    #taskID;
    #id;
    #db;
    #upstreamDb;
    #writes = new Set();
    #upstreamWrites = [];
    #pendingRowRecordUpdates = new CustomKeyMap(rowIDString);
    #forceUpdates = new CustomKeySet(rowIDString);
    #rowCache;
    #loadAttemptIntervalMs;
    #maxLoadAttempts;
    #upstreamSchemaName;
    #rowCount = 0;
    constructor(lc, cvrDb, 
    // Optionally undefined to deal with custom upstreams.
    // This is temporary until we have a more principled protocol to deal with
    // custom upstreams and clearing their custom mutator responses.
    // An implementor could simply clear them after N minutes for the time being.
    upstreamDb, shard, taskID, cvrID, failService, loadAttemptIntervalMs = LOAD_ATTEMPT_INTERVAL_MS, maxLoadAttempts = MAX_LOAD_ATTEMPTS, deferredRowFlushThreshold = 100, // somewhat arbitrary
    setTimeoutFn = setTimeout) {
        this.#db = cvrDb;
        this.#upstreamDb = upstreamDb;
        this.#schema = cvrSchema(shard);
        this.#taskID = taskID;
        this.#id = cvrID;
        this.#rowCache = new RowRecordCache(lc, cvrDb, shard, cvrID, failService, deferredRowFlushThreshold, setTimeoutFn);
        this.#loadAttemptIntervalMs = loadAttemptIntervalMs;
        this.#maxLoadAttempts = maxLoadAttempts;
        this.#upstreamSchemaName = upstreamSchema(shard);
    }
    #cvr(table) {
        return this.#db(`${this.#schema}.${table}`);
    }
    load(lc, lastConnectTime) {
        return startAsyncSpan(tracer, 'cvr.load', async () => {
            let err;
            for (let i = 0; i < this.#maxLoadAttempts; i++) {
                if (i > 0) {
                    await sleep(this.#loadAttemptIntervalMs);
                }
                const result = await this.#load(lc, lastConnectTime);
                if (result instanceof RowsVersionBehindError) {
                    lc.info?.(`attempt ${i + 1}: ${String(result)}`);
                    err = result;
                    continue;
                }
                return result;
            }
            assert(err);
            throw new ErrorForClient({
                kind: ErrorKind.ClientNotFound,
                message: `max attempts exceeded waiting for CVR@${err.cvrVersion} to catch up from ${err.rowsVersion}`,
            });
        });
    }
    async #load(lc, lastConnectTime) {
        const start = Date.now();
        const id = this.#id;
        const cvr = {
            id,
            version: EMPTY_CVR_VERSION,
            lastActive: 0,
            ttlClock: ttlClockFromNumber(0), // TTL clock starts at 0, not Date.now()
            replicaVersion: null,
            clients: {},
            queries: {},
            clientSchema: null,
        };
        const [instance, clientsRows, queryRows, desiresRows] = await this.#db.begin(tx => [
            tx `SELECT cvr."version", 
                 "lastActive",
                 "ttlClock",
                 "replicaVersion", 
                 "owner", 
                 "grantedAt",
                 "clientSchema", 
                 rows."version" as "rowsVersion"
            FROM ${this.#cvr('instances')} AS cvr
            LEFT JOIN ${this.#cvr('rowsVersion')} AS rows 
            ON cvr."clientGroupID" = rows."clientGroupID"
            WHERE cvr."clientGroupID" = ${id}`,
            tx `SELECT "clientID" FROM ${this.#cvr('clients')}
           WHERE "clientGroupID" = ${id}`,
            tx `SELECT * FROM ${this.#cvr('queries')} 
          WHERE "clientGroupID" = ${id} AND deleted IS DISTINCT FROM true`,
            tx `SELECT 
          "clientGroupID",
          "clientID",
          "queryHash",
          "patchVersion",
          "deleted",
          EXTRACT(EPOCH FROM "ttl") * 1000 AS "ttl",
          "inactivatedAt"
          FROM ${this.#cvr('desires')}
          WHERE "clientGroupID" = ${id}`,
        ]);
        if (instance.length === 0) {
            // This is the first time we see this CVR.
            this.putInstance({
                version: cvr.version,
                lastActive: 0,
                ttlClock: ttlClockFromNumber(0), // TTL clock starts at 0 for new instances
                replicaVersion: null,
                clientSchema: null,
            });
        }
        else {
            assert(instance.length === 1);
            const { version, lastActive, ttlClock, replicaVersion, owner, grantedAt, rowsVersion, clientSchema, } = instance[0];
            if (owner !== this.#taskID) {
                if ((grantedAt ?? 0) > lastConnectTime) {
                    throw new OwnershipError(owner, grantedAt, lastConnectTime);
                }
                else {
                    // Fire-and-forget an ownership change to signal the current owner.
                    // Note that the query is structured such that it only succeeds in the
                    // correct conditions (i.e. gated on `grantedAt`).
                    void this.#db `
            UPDATE ${this.#cvr('instances')} 
              SET "owner"     = ${this.#taskID}, 
                  "grantedAt" = ${lastConnectTime}
              WHERE "clientGroupID" = ${this.#id} AND
                    ("grantedAt" IS NULL OR
                     "grantedAt" <= to_timestamp(${lastConnectTime / 1000}))
        `.execute();
                }
            }
            if (version !== (rowsVersion ?? EMPTY_CVR_VERSION.stateVersion)) {
                // This will cause the load() method to wait for row catchup and retry.
                // Assuming the ownership signal succeeds, the current owner will stop
                // modifying the CVR and flush its pending row changes.
                return new RowsVersionBehindError(version, rowsVersion);
            }
            cvr.version = versionFromString(version);
            cvr.lastActive = lastActive;
            cvr.ttlClock = ttlClock;
            cvr.replicaVersion = replicaVersion;
            try {
                cvr.clientSchema =
                    clientSchema === null
                        ? null
                        : v.parse(clientSchema, clientSchemaSchema);
            }
            catch (e) {
                throw new InvalidClientSchemaError(e);
            }
        }
        for (const row of clientsRows) {
            cvr.clients[row.clientID] = {
                id: row.clientID,
                desiredQueryIDs: [],
            };
        }
        for (const row of queryRows) {
            const query = asQuery(row);
            cvr.queries[row.queryHash] = query;
        }
        for (const row of desiresRows) {
            const client = cvr.clients[row.clientID];
            if (client) {
                if (!row.deleted && row.inactivatedAt === null) {
                    client.desiredQueryIDs.push(row.queryHash);
                }
            }
            else {
                // This can happen if the client was deleted but the queries are still alive.
                lc.debug?.(`Client ${row.clientID} not found`);
            }
            const query = cvr.queries[row.queryHash];
            if (query &&
                query.type !== 'internal' &&
                (!row.deleted || row.inactivatedAt !== null)) {
                query.clientState[row.clientID] = {
                    inactivatedAt: row.inactivatedAt ?? undefined,
                    ttl: row.ttl ?? DEFAULT_TTL_MS,
                    version: versionFromString(row.patchVersion),
                };
            }
        }
        lc.debug?.(`loaded cvr@${versionString(cvr.version)} (${Date.now() - start} ms)`);
        // why do we not sort `desiredQueryIDs` here?
        return cvr;
    }
    getRowRecords() {
        return this.#rowCache.getRowRecords();
    }
    putRowRecord(row) {
        this.#pendingRowRecordUpdates.set(row.id, row);
    }
    /**
     * Note: Removing a row from the CVR should be represented by a
     *       {@link putRowRecord()} with `refCounts: null` in order to properly
     *       produce the appropriate delete patch when catching up old clients.
     *
     * This `delRowRecord()` method, on the other hand, is only used:
     * - when a row record is being *replaced* by another RowRecord, which currently
     *   only happens when the columns of the row key change
     * - for "canceling" the put of a row that was not in the CVR in the first place.
     */
    delRowRecord(id) {
        this.#pendingRowRecordUpdates.set(id, null);
    }
    /**
     * Overrides the default logic that removes no-op writes and forces
     * the updates for the given row `ids`. This has no effect if there
     * are no corresponding puts or dels for the associated row records.
     */
    forceUpdates(...ids) {
        for (const id of ids) {
            this.#forceUpdates.add(id);
        }
    }
    /**
     * Updates the `ttlClock` of the CVR instance. The ttlClock starts at 0 when
     * the CVR instance is first created and increments based on elapsed time
     * since the base time established by the ViewSyncerService.
     */
    async updateTTLClock(ttlClock, lastActive) {
        await this.#db `UPDATE ${this.#cvr('instances')}
          SET "lastActive" = ${lastActive},
              "ttlClock" = ${ttlClock}
          WHERE "clientGroupID" = ${this.#id}`.execute();
    }
    /**
     * @returns This returns the current `ttlClock` of the CVR instance. The ttlClock
     *          represents elapsed time since the instance was created (starting from 0).
     *          If the CVR has never been initialized for this client group, it returns
     *          `undefined`.
     */
    async getTTLClock() {
        const result = await this.#db `
      SELECT "ttlClock" FROM ${this.#cvr('instances')}
      WHERE "clientGroupID" = ${this.#id}`.values();
        if (result.length === 0) {
            // This can happen if the CVR has not been initialized yet.
            return undefined;
        }
        assert(result.length === 1);
        return result[0][0];
    }
    putInstance({ version, replicaVersion, lastActive, clientSchema, ttlClock, }) {
        this.#writes.add({
            stats: { instances: 1 },
            write: (tx, lastConnectTime) => {
                const change = {
                    clientGroupID: this.#id,
                    version: versionString(version),
                    lastActive,
                    ttlClock,
                    replicaVersion,
                    owner: this.#taskID,
                    grantedAt: lastConnectTime,
                    clientSchema,
                };
                return tx `
        INSERT INTO ${this.#cvr('instances')} ${tx(change)} 
          ON CONFLICT ("clientGroupID") DO UPDATE SET ${tx(change)}`;
            },
        });
    }
    markQueryAsDeleted(version, queryPatch) {
        this.#writes.add({
            stats: { queries: 1 },
            write: tx => tx `UPDATE ${this.#cvr('queries')} SET ${tx({
                patchVersion: versionString(version),
                deleted: true,
                transformationHash: null,
                transformationVersion: null,
            })}
      WHERE "clientGroupID" = ${this.#id} AND "queryHash" = ${queryPatch.id}`,
        });
    }
    putQuery(query) {
        const change = queryRecordToQueryRow(this.#id, query);
        // ${JSON.stringify(change.queryArgs)}::text::json is used because postgres.js
        // gets confused if the input is `[boolean]` and throws an error saying a bool
        // cannot be converted to json.
        // https://github.com/porsager/postgres/issues/386
        this.#writes.add({
            stats: { queries: 1 },
            write: tx => tx `INSERT INTO ${this.#cvr('queries')} (
        "clientGroupID",
        "queryHash",
        "clientAST",
        "queryName",
        "queryArgs",
        "patchVersion",
        "transformationHash",
        "transformationVersion",
        "internal",
        "deleted"
      ) VALUES (
        ${change.clientGroupID},
        ${change.queryHash},
        ${change.clientAST},
        ${change.queryName},
        ${change.queryArgs === undefined ? null : JSON.stringify(change.queryArgs)}::text::json,
        ${change.patchVersion},
        ${change.transformationHash ?? null},
        ${change.transformationVersion ?? null},
        ${change.internal},
        ${change.deleted ?? false}
      )
      ON CONFLICT ("clientGroupID", "queryHash")
      DO UPDATE SET 
        "clientAST" = ${change.clientAST},
        "queryName" = ${change.queryName},
        "queryArgs" = ${change.queryArgs === undefined ? null : JSON.stringify(change.queryArgs)}::text::json,
        "patchVersion" = ${change.patchVersion},
        "transformationHash" = ${change.transformationHash ?? null},
        "transformationVersion" = ${change.transformationVersion ?? null},
        "internal" = ${change.internal},
        "deleted" = ${change.deleted ?? false}`,
        });
    }
    updateQuery(query) {
        const maybeVersionString = (v) => v ? versionString(v) : null;
        const change = {
            patchVersion: query.type === 'internal'
                ? null
                : maybeVersionString(query.patchVersion),
            transformationHash: query.transformationHash ?? null,
            transformationVersion: maybeVersionString(query.transformationVersion),
            deleted: false,
        };
        this.#writes.add({
            stats: { queries: 1 },
            write: tx => tx `UPDATE ${this.#cvr('queries')} SET ${tx(change)}
      WHERE "clientGroupID" = ${this.#id} AND "queryHash" = ${query.id}`,
        });
    }
    insertClient(client) {
        const change = {
            clientGroupID: this.#id,
            clientID: client.id,
        };
        this.#writes.add({
            stats: { clients: 1 },
            write: tx => tx `INSERT INTO ${this.#cvr('clients')} ${tx(change)}`,
        });
    }
    deleteClient(clientID) {
        this.#writes.add({
            stats: { clients: 1 },
            write: sql => sql `DELETE FROM ${this.#cvr('clients')} 
            WHERE "clientGroupID" = ${this.#id} 
              AND "clientID" = ${clientID}`,
        });
        this.#upstreamWrites.push(sql => sql `DELETE FROM ${sql(this.#upstreamSchemaName)}."mutations" 
            WHERE "clientGroupID" = ${this.#id} 
              AND "clientID" = ${clientID}`);
    }
    putDesiredQuery(newVersion, query, client, deleted, inactivatedAt, ttl) {
        const change = {
            clientGroupID: this.#id,
            clientID: client.id,
            deleted,
            inactivatedAt: inactivatedAt ?? null,
            patchVersion: versionString(newVersion),
            queryHash: query.id,
            // ttl is in ms but the postgres table uses INTERVAL which treats numbers as seconds
            ttl: ttl < 0 ? null : ttl / 1000,
        };
        this.#writes.add({
            stats: { desires: 1 },
            write: tx => tx `
      INSERT INTO ${this.#cvr('desires')} ${tx(change)}
        ON CONFLICT ("clientGroupID", "clientID", "queryHash")
        DO UPDATE SET ${tx(change)}
      `,
        });
    }
    catchupRowPatches(lc, afterVersion, upToCVR, current, excludeQueryHashes = []) {
        return this.#rowCache.catchupRowPatches(lc, afterVersion, upToCVR, current, excludeQueryHashes);
    }
    async catchupConfigPatches(lc, afterVersion, upToCVR, current) {
        if (cmpVersions(afterVersion, upToCVR.version) >= 0) {
            return [];
        }
        const startMs = Date.now();
        const start = afterVersion ? versionString(afterVersion) : '';
        const end = versionString(upToCVR.version);
        lc.debug?.(`scanning config patches for clients from ${start}`);
        const reader = new TransactionPool(lc, Mode.READONLY).run(this.#db);
        try {
            // Verify that we are reading the right version of the CVR.
            await reader.processReadTask(tx => checkVersion(tx, this.#schema, this.#id, current));
            const [allDesires, queryRows] = await reader.processReadTask(tx => Promise.all([
                tx `
      SELECT * FROM ${this.#cvr('desires')}
        WHERE "clientGroupID" = ${this.#id}
        AND "patchVersion" > ${start}
        AND "patchVersion" <= ${end}`,
                tx `
      SELECT deleted, "queryHash", "patchVersion" FROM ${this.#cvr('queries')}
        WHERE "clientGroupID" = ${this.#id}
        AND "patchVersion" > ${start}
        AND "patchVersion" <= ${end}`,
            ]));
            const patches = [];
            for (const row of queryRows) {
                const { queryHash: id } = row;
                const patch = row.deleted
                    ? { type: 'query', op: 'del', id }
                    : { type: 'query', op: 'put', id };
                const v = row.patchVersion;
                assert(v);
                patches.push({ patch, toVersion: versionFromString(v) });
            }
            for (const row of allDesires) {
                const { clientID, queryHash: id } = row;
                const patch = row.deleted
                    ? { type: 'query', op: 'del', id, clientID }
                    : { type: 'query', op: 'put', id, clientID };
                patches.push({ patch, toVersion: versionFromString(row.patchVersion) });
            }
            lc.debug?.(`${patches.length} config patches (${Date.now() - startMs} ms)`);
            return patches;
        }
        finally {
            reader.setDone();
        }
    }
    async #checkVersionAndOwnership(tx, expectedCurrentVersion, lastConnectTime) {
        const expected = versionString(expectedCurrentVersion);
        const result = await tx `SELECT "version", "owner", "grantedAt" FROM ${this.#cvr('instances')}
        WHERE "clientGroupID" = ${this.#id}
        FOR UPDATE`.execute(); // Note: execute() immediately to send the query before others.
        const { version, owner, grantedAt } = result.length > 0
            ? result[0]
            : {
                version: EMPTY_CVR_VERSION.stateVersion,
                owner: null,
                grantedAt: null,
            };
        if (owner !== this.#taskID && (grantedAt ?? 0) > lastConnectTime) {
            throw new OwnershipError(owner, grantedAt, lastConnectTime);
        }
        if (version !== expected) {
            throw new ConcurrentModificationException(expected, version);
        }
    }
    async #flush(expectedCurrentVersion, cvr, lastConnectTime) {
        const stats = {
            instances: 0,
            queries: 0,
            desires: 0,
            clients: 0,
            rows: 0,
            rowsDeferred: 0,
            statements: 0,
        };
        if (this.#pendingRowRecordUpdates.size) {
            const existingRowRecords = await this.getRowRecords();
            this.#rowCount = existingRowRecords.size;
            for (const [id, row] of this.#pendingRowRecordUpdates.entries()) {
                if (this.#forceUpdates.has(id)) {
                    continue;
                }
                const existing = existingRowRecords.get(id);
                if (
                // Don't delete or add an unreferenced row if it's not in the CVR.
                (existing === undefined && !row?.refCounts) ||
                    // Don't write a row record that exactly matches what's in the CVR.
                    deepEqual((row ?? undefined), existing)) {
                    this.#pendingRowRecordUpdates.delete(id);
                }
            }
        }
        if (this.#pendingRowRecordUpdates.size === 0 && this.#writes.size === 0) {
            return null;
        }
        // Note: The CVR instance itself is only updated if there are material
        // changes (i.e. changes to the CVR contents) to flush.
        this.putInstance(cvr);
        const rowsFlushed = await this.#db.begin(async (tx) => {
            const pipelined = [
                // #checkVersionAndOwnership() executes a `SELECT ... FOR UPDATE`
                // query to acquire a row-level lock so that version-updating
                // transactions are effectively serialized per cvr.instance.
                //
                // Note that `rowsVersion` updates, on the other hand, are not subject
                // to this lock and can thus commit / be-committed independently of
                // cvr.instances.
                this.#checkVersionAndOwnership(tx, expectedCurrentVersion, lastConnectTime),
            ];
            for (const write of this.#writes) {
                stats.instances += write.stats.instances ?? 0;
                stats.queries += write.stats.queries ?? 0;
                stats.desires += write.stats.desires ?? 0;
                stats.clients += write.stats.clients ?? 0;
                stats.rows += write.stats.rows ?? 0;
                pipelined.push(write.write(tx, lastConnectTime).execute());
                stats.statements++;
            }
            const rowUpdates = this.#rowCache.executeRowUpdates(tx, cvr.version, this.#pendingRowRecordUpdates, 'allow-defer');
            pipelined.push(...rowUpdates);
            stats.statements += rowUpdates.length;
            // Make sure Errors thrown by pipelined statements
            // are propagated up the stack.
            await Promise.all(pipelined);
            if (rowUpdates.length === 0) {
                stats.rowsDeferred = this.#pendingRowRecordUpdates.size;
                return false;
            }
            stats.rows += this.#pendingRowRecordUpdates.size;
            return true;
        });
        this.#rowCount = await this.#rowCache.apply(this.#pendingRowRecordUpdates, cvr.version, rowsFlushed);
        recordRowsSynced(this.#rowCount);
        if (this.#upstreamDb) {
            await this.#upstreamDb.begin(async (tx) => {
                await Promise.all(this.#upstreamWrites.map(write => write(tx)));
            });
        }
        return stats;
    }
    get rowCount() {
        return this.#rowCount;
    }
    async flush(lc, expectedCurrentVersion, cvr, lastConnectTime) {
        const start = performance.now();
        try {
            const stats = await this.#flush(expectedCurrentVersion, cvr, lastConnectTime);
            if (stats) {
                const elapsed = performance.now() - start;
                lc.debug?.(`flushed cvr@${versionString(cvr.version)} ` +
                    `${JSON.stringify(stats)} in (${elapsed} ms)`);
                this.#rowCache.recordSyncFlushStats(stats, elapsed);
            }
            return stats;
        }
        catch (e) {
            // Clear cached state if an error (e.g. ConcurrentModificationException) is encountered.
            this.#rowCache.clear();
            throw e;
        }
        finally {
            this.#writes.clear();
            this.#upstreamWrites.length = 0;
            this.#pendingRowRecordUpdates.clear();
            this.#forceUpdates.clear();
        }
    }
    hasPendingUpdates() {
        return this.#rowCache.hasPendingUpdates();
    }
    /** Resolves when all pending updates are flushed. */
    flushed(lc) {
        return this.#rowCache.flushed(lc);
    }
    async inspectQueries(lc, ttlClock, clientID) {
        const db = this.#db;
        const clientGroupID = this.#id;
        const reader = new TransactionPool(lc, Mode.READONLY).run(db);
        try {
            return await reader.processReadTask(tx => tx `
  SELECT DISTINCT ON (d."clientID", d."queryHash")
    d."clientID",
    d."queryHash" AS "queryID",
    COALESCE((EXTRACT(EPOCH FROM d."ttl") * 1000)::double precision, ${DEFAULT_TTL_MS}) AS "ttl",
    (EXTRACT(EPOCH FROM d."inactivatedAt") * 1000)::double precision AS "inactivatedAt",
    (SELECT COUNT(*)::INT FROM ${this.#cvr('rows')} r 
     WHERE r."clientGroupID" = d."clientGroupID" 
     AND r."refCounts" ? d."queryHash") AS "rowCount",
    q."clientAST" AS "ast",
    (q."patchVersion" IS NOT NULL) AS "got",
    COALESCE(d."deleted", FALSE) AS "deleted",
    q."queryName" AS "name",
    q."queryArgs" AS "args"
  FROM ${this.#cvr('desires')} d
  LEFT JOIN ${this.#cvr('queries')} q
    ON q."clientGroupID" = d."clientGroupID"
   AND q."queryHash" = d."queryHash"
  WHERE d."clientGroupID" = ${clientGroupID}
    ${clientID ? tx `AND d."clientID" = ${clientID}` : tx ``}
    AND NOT (d."inactivatedAt" IS NOT NULL AND d."ttl" IS NOT NULL AND d."inactivatedAt" + d."ttl" <= to_timestamp(${ttlClockAsNumber(ttlClock) / 1000}))
  ORDER BY d."clientID", d."queryHash"`);
        }
        finally {
            reader.setDone();
        }
    }
}
/**
 * This is similar to {@link CVRStore.#checkVersionAndOwnership} except
 * that it only checks the version and is suitable for snapshot reads
 * (i.e. by doing a plain `SELECT` rather than a `SELECT ... FOR UPDATE`).
 */
export async function checkVersion(tx, schema, clientGroupID, expectedCurrentVersion) {
    const expected = versionString(expectedCurrentVersion);
    const result = await tx `
    SELECT version FROM ${tx(schema)}.instances 
      WHERE "clientGroupID" = ${clientGroupID}`;
    const { version } = result.length > 0 ? result[0] : { version: EMPTY_CVR_VERSION.stateVersion };
    if (version !== expected) {
        throw new ConcurrentModificationException(expected, version);
    }
}
export class ConcurrentModificationException extends ErrorWithLevel {
    name = 'ConcurrentModificationException';
    constructor(expectedVersion, actualVersion) {
        super(`CVR has been concurrently modified. Expected ${expectedVersion}, got ${actualVersion}`, 'warn');
    }
}
export class OwnershipError extends ErrorForClient {
    name = 'OwnershipError';
    constructor(owner, grantedAt, lastConnectTime) {
        super({
            kind: ErrorKind.Rehome,
            message: `CVR ownership was transferred to ${owner} at ` +
                `${new Date(grantedAt ?? 0).toISOString()} ` +
                `(last connect time: ${new Date(lastConnectTime).toISOString()})`,
            maxBackoffMs: 0,
        }, 'info');
    }
}
export class InvalidClientSchemaError extends ErrorForClient {
    name = 'InvalidClientSchemaError';
    constructor(cause) {
        super({
            kind: ErrorKind.SchemaVersionNotSupported,
            message: `Could not parse clientSchema stored in CVR: ${String(cause)}`,
        }, 'warn', { cause });
    }
}
export class RowsVersionBehindError extends Error {
    name = 'RowsVersionBehindError';
    cvrVersion;
    rowsVersion;
    constructor(cvrVersion, rowsVersion) {
        super(`rowsVersion (${rowsVersion}) is behind CVR ${cvrVersion}`);
        this.cvrVersion = cvrVersion;
        this.rowsVersion = rowsVersion;
    }
}
//# sourceMappingURL=cvr-store.js.map