import { type PgExecutor } from './pg-executor.js'; import type { CompiledSchema, CompiledTable } from './schema.js'; import type { AuthoritativeQueryRequest, AuthoritativeQueryResult, ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, PartitionRegistryEntry, PrunedReactionCounts, ReactionClaimQuery, ReactionFailureUpdate, ReactionListQuery, ReactionPruneQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, StorageTransaction, StoredCommit, StoredPushResult, StoredReaction, StoredRow } from './storage.js'; /** * Schema DDL. Applied by `PostgresServerStorage.migrate()` (idempotent). * * Covering index design (the reason this file exists): * - `sync_change_scopes_pk (partition, tbl, var, value, commit_seq)` — the * candidate scan for `readCommitWindow` ranges on the (partition, tbl, * var, value IN …) prefix and returns `commit_seq` already ascending; * - `sync_row_scopes_pk (partition, tbl, var, value, row_id)` — same shape * for `scanRows`, ordered by `row_id`. * Both are the PRIMARY KEY, so they are the clustering/covering index for * their table. No secondary index is needed for the hot path. * * Blob reference index (§5.9.4) — parity with the SQLite dialect's * `sync_blob_refs`: * - PRIMARY KEY `(partition, tbl, row_id, blob_id)` — the by-row prefix * lets `setBlobRefs` replace a row's set with a single ranged DELETE and * the delete path clear it by (partition, tbl, row_id); * - the secondary `sync_blob_refs_by_blob (partition, blob_id)` index * drives `listRowsReferencingBlob` (the download-authorization candidate * set, §5.9.5) as an index range, never a scan. `postgres-explain * .test.ts` asserts an `Index` node here too. */ export declare const POSTGRES_DDL = "\nCREATE TABLE IF NOT EXISTS sync_partitions(\n partition TEXT PRIMARY KEY,\n max_commit_seq BIGINT NOT NULL DEFAULT 0,\n horizon_seq BIGINT NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS sync_partition_registry(\n partition TEXT PRIMARY KEY,\n log_epoch TEXT NOT NULL,\n epoch_required BOOLEAN NOT NULL DEFAULT FALSE,\n last_authenticated_at_ms BIGINT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS sync_row_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, row_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, row_id)\n);\nCREATE TABLE IF NOT EXISTS sync_commits(\n partition TEXT NOT NULL, commit_seq BIGINT NOT NULL,\n client_id TEXT NOT NULL, client_commit_id TEXT NOT NULL,\n actor_id TEXT NOT NULL, created_at_ms BIGINT NOT NULL,\n PRIMARY KEY(partition, commit_seq)\n);\nCREATE INDEX IF NOT EXISTS sync_commits_by_time\n ON sync_commits(partition, created_at_ms);\nCREATE TABLE IF NOT EXISTS sync_changes(\n partition TEXT NOT NULL, commit_seq BIGINT NOT NULL, idx INTEGER NOT NULL,\n tbl TEXT NOT NULL, row_id TEXT NOT NULL, op SMALLINT NOT NULL,\n row_version BIGINT, scopes JSONB NOT NULL, payload BYTEA,\n PRIMARY KEY(partition, commit_seq, idx)\n);\nCREATE INDEX IF NOT EXISTS sync_changes_by_table\n ON sync_changes(partition, commit_seq, tbl, idx);\nCREATE TABLE IF NOT EXISTS sync_change_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, commit_seq BIGINT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, commit_seq)\n);\nCREATE TABLE IF NOT EXISTS sync_push_results(\n partition TEXT NOT NULL, client_id TEXT NOT NULL,\n client_commit_id TEXT NOT NULL, result JSONB NOT NULL,\n PRIMARY KEY(partition, client_id, client_commit_id)\n);\nCREATE TABLE IF NOT EXISTS sync_reactions(\n partition TEXT NOT NULL, idempotency_key TEXT NOT NULL,\n type TEXT NOT NULL, version INTEGER NOT NULL, payload JSONB NOT NULL,\n source_client_id TEXT NOT NULL, source_client_commit_id TEXT NOT NULL,\n source_commit_seq BIGINT NOT NULL, created_at_ms BIGINT NOT NULL,\n available_at_ms BIGINT NOT NULL, status TEXT NOT NULL,\n attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,\n lease_owner TEXT, lease_expires_at_ms BIGINT, completed_at_ms BIGINT,\n last_failure JSONB,\n PRIMARY KEY(partition, idempotency_key),\n CHECK(status IN ('pending', 'leased', 'completed', 'dead-letter'))\n);\nCREATE INDEX IF NOT EXISTS sync_reactions_due\n ON sync_reactions(partition, status, available_at_ms, created_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_lease\n ON sync_reactions(partition, status, lease_expires_at_ms);\nCREATE INDEX IF NOT EXISTS sync_reactions_completed\n ON sync_reactions(partition, status, completed_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_dead_letter\n ON sync_reactions(partition, status, available_at_ms, idempotency_key);\nCREATE TABLE IF NOT EXISTS sync_clients(\n partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,\n wire_version INTEGER NOT NULL DEFAULT 1,\n cursor BIGINT NOT NULL, subscriptions JSONB NOT NULL,\n updated_at_ms BIGINT NOT NULL,\n PRIMARY KEY(partition, client_id)\n);\nALTER TABLE sync_clients\n ADD COLUMN IF NOT EXISTS wire_version INTEGER NOT NULL DEFAULT 1;\nCREATE TABLE IF NOT EXISTS sync_blob_refs(\n partition TEXT NOT NULL, tbl TEXT NOT NULL, row_id TEXT NOT NULL,\n blob_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, row_id, blob_id)\n);\nCREATE INDEX IF NOT EXISTS sync_blob_refs_by_blob\n ON sync_blob_refs(partition, blob_id);\n"; export declare class PostgresServerStorage implements ServerStorage { #private; constructor(exec: PgExecutor); /** Apply the schema DDL (idempotent). Call once before use. */ migrate(): Promise; /** Resolve a table's compiled schema; row operations require `ensureSchema`. */ table(name: string): CompiledTable; ensureSchema(schema: CompiledSchema): Promise; touchPartition(partition: string, authenticatedAtMs: number, initialLogEpoch: string): Promise; rotatePartitionLogEpoch(partition: string, logEpoch: string, authenticatedAtMs: number): Promise; listPartitionRegistry(): Promise; /** * Open a real Postgres transaction. The push handler drives the returned * `StorageTransaction` imperatively (getRow/upsert/…/commit), but the * driver seam models a transaction as a callback scope. We bridge the two: * `transaction(fn)` blocks inside `fn` on a promise that the imperative * `commit()`/`rollback()` resolves/rejects, so BEGIN…COMMIT wraps exactly * the handler's writes on one pinned connection. */ begin(partition: string): Promise; getMaxCommitSeq(partition: string): Promise; queryAuthoritative(partition: string, query: AuthoritativeQueryRequest): Promise; getHorizonSeq(partition: string): Promise; setHorizonSeq(partition: string, seq: number): Promise; pruneCommitsThrough(partition: string, seq: number): Promise; getCommitSeqBefore(partition: string, createdBeforeMs: number): Promise; getRow(partition: string, table: string, rowId: string): Promise; getPushResult(partition: string, clientId: string, clientCommitId: string): Promise; claimReactions(partition: string, query: ReactionClaimQuery): Promise; completeReaction(partition: string, idempotencyKey: string, leaseOwner: string, completedAtMs: number): Promise; extendReactionLease(partition: string, idempotencyKey: string, leaseOwner: string, leaseExpiresAtMs: number): Promise; failReaction(partition: string, idempotencyKey: string, update: ReactionFailureUpdate): Promise; retryReaction(partition: string, idempotencyKey: string, nowMs: number): Promise; getReaction(partition: string, idempotencyKey: string): Promise; listReactions(partition: string, query: ReactionListQuery): Promise; pruneReactions(partition: string, query: ReactionPruneQuery): Promise; readCommitWindow(partition: string, query: CommitWindowQuery): Promise; scanRows(partition: string, query: RowScanQuery): Promise; scanRowsByIndex(partition: string, query: IndexRowScanQuery): Promise; getClientRecord(partition: string, clientId: string): Promise; putClientRecord(partition: string, record: ClientRecord): Promise; listClientCursors(partition: string): Promise; listRowsReferencingBlob(partition: string, blobId: string): Promise<{ readonly table: string; readonly rowId: string; readonly scopes: Record; }[]>; listReferencedBlobIds(partition: string): Promise; listClientRecords(partition: string): Promise; listCommitMetadata(partition: string, query: CommitMetadataQuery): Promise; scopeActivity(partition: string, query: ScopeActivityQuery): Promise; getRowScopes(partition: string, table: string, rowId: string): Promise<{ serverVersion: number; scopes: Record; } | undefined>; listPartitions(): Promise; }