orez 0.4.6 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cf-do/tx-journal.d.ts +62 -0
- package/dist/cf-do/tx-journal.d.ts.map +1 -0
- package/dist/cf-do/tx-journal.js +163 -0
- package/dist/cf-do/tx-journal.js.map +1 -0
- package/dist/cf-do/worker.d.ts +15 -2
- package/dist/cf-do/worker.d.ts.map +1 -1
- package/dist/cf-do/worker.js +46 -8
- package/dist/cf-do/worker.js.map +1 -1
- package/dist/pg-proxy-browser.d.ts.map +1 -1
- package/dist/pg-proxy-browser.js +127 -15
- package/dist/pg-proxy-browser.js.map +1 -1
- package/dist/pg-proxy-do-backend.d.ts +2 -5
- package/dist/pg-proxy-do-backend.d.ts.map +1 -1
- package/dist/pg-proxy-do-backend.js +36 -63
- package/dist/pg-proxy-do-backend.js.map +1 -1
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +73 -17
- package/dist/pg-proxy.js.map +1 -1
- package/dist/replication/change-tracker.d.ts +28 -0
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +53 -0
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts +13 -0
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +145 -36
- package/dist/replication/handler.js.map +1 -1
- package/dist/worker/local-sql-backend.d.ts +35 -0
- package/dist/worker/local-sql-backend.d.ts.map +1 -0
- package/dist/worker/local-sql-backend.js +106 -0
- package/dist/worker/local-sql-backend.js.map +1 -0
- package/dist/worker/zero-cache-embed-cf.d.ts +1 -11
- package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
- package/dist/worker/zero-cache-embed-cf.js +56 -14
- package/dist/worker/zero-cache-embed-cf.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import { deparseSync, loadModule, parseSync } from 'pgsql-parser';
|
|
3
|
+
import { TX_MANIFEST_DDL, TX_MANIFEST_TABLE } from './cf-do/tx-journal.js';
|
|
3
4
|
import { RETURNING_INTERNAL_PREFIX } from './do-sql-tracking.js';
|
|
4
5
|
import { signalReplicationChange } from './replication/handler.js';
|
|
5
6
|
import { markSQLiteKeywordIdentifiers, restoreSQLiteKeywordIdentifierMarkers, } from './sqlite-keyword-identifiers.js';
|
|
@@ -4587,10 +4588,17 @@ export class DoBackend {
|
|
|
4587
4588
|
// round-trip to durable storage after every DDL statement in a migration.
|
|
4588
4589
|
txMetadataDirty = false;
|
|
4589
4590
|
txHasTrackedWrite = false;
|
|
4591
|
+
// identifies the client process generation owning this backend's
|
|
4592
|
+
// transactions in the durable tx journal. a process that knows its previous
|
|
4593
|
+
// generation is dead (e.g. the zero-cache embed at boot) recovers orphaned
|
|
4594
|
+
// transactions for its own owner id only, so it can never roll back another
|
|
4595
|
+
// live client's in-flight transaction.
|
|
4596
|
+
txOwner;
|
|
4590
4597
|
constructor(doUrl, dbName = 'postgres', namespace = 'default', opts) {
|
|
4591
4598
|
this.doUrl = doUrl.replace(/\/+$/, '');
|
|
4592
4599
|
this.dbName = dbName;
|
|
4593
4600
|
this.namespace = namespace;
|
|
4601
|
+
this.txOwner = opts?.txOwner || 'default';
|
|
4594
4602
|
this.httpClient = new HttpClient(opts?.fetch);
|
|
4595
4603
|
this.skippedFunctionNames = getSkippedFunctionNames(dbName, namespace);
|
|
4596
4604
|
this.triggerFunctions = new Map();
|
|
@@ -4630,6 +4638,7 @@ export class DoBackend {
|
|
|
4630
4638
|
await this.doExecResult('CREATE TABLE IF NOT EXISTS "_orez___zero_watermark" (dummy INTEGER PRIMARY KEY DEFAULT 1, last_value INTEGER NOT NULL DEFAULT 1, is_called INTEGER NOT NULL DEFAULT 0)');
|
|
4631
4639
|
await this.doExecResult('INSERT OR IGNORE INTO "_orez___zero_watermark" (dummy, last_value, is_called) VALUES (1, 1, 0)');
|
|
4632
4640
|
await this.doExecResult("CREATE TABLE IF NOT EXISTS \"_orez__zero_replication_slots\" (slot_name TEXT PRIMARY KEY, restart_lsn TEXT NOT NULL DEFAULT '0/1000000', confirmed_flush_lsn TEXT NOT NULL DEFAULT '0/1000000', wal_status TEXT NOT NULL DEFAULT 'reserved', plugin TEXT NOT NULL DEFAULT 'pgoutput', slot_type TEXT NOT NULL DEFAULT 'logical', active INTEGER NOT NULL DEFAULT 0, active_pid INTEGER DEFAULT NULL, created_at INTEGER NOT NULL DEFAULT (unixepoch()))");
|
|
4641
|
+
await this.doExecResult('CREATE TABLE IF NOT EXISTS "_orez___zero_streamed_batches" (batch_lsn INTEGER PRIMARY KEY, batch_end INTEGER NOT NULL)');
|
|
4633
4642
|
}
|
|
4634
4643
|
async ensureMetadataTable() {
|
|
4635
4644
|
await this.doExecResult(metadataTableDDL());
|
|
@@ -4864,9 +4873,12 @@ export class DoBackend {
|
|
|
4864
4873
|
const shouldPersist = this.txMetadataDirty;
|
|
4865
4874
|
const shouldSignal = this.txHasTrackedWrite;
|
|
4866
4875
|
const txID = this.txID;
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4876
|
+
// ONE atomic commit point on the backend: promotes pending tracked
|
|
4877
|
+
// changes and clears the tx journal (snapshots + manifest) in a single
|
|
4878
|
+
// storage transaction. read-only transactions skip the round-trip.
|
|
4879
|
+
if (txID && (shouldSignal || this.txDataSnapshots.size > 0)) {
|
|
4880
|
+
await this.httpClient.post(this.url('/commit-tx'), JSON.stringify({ transactionID: txID }), { 'Content-Type': 'application/json' });
|
|
4881
|
+
}
|
|
4870
4882
|
this.clearTransactionState();
|
|
4871
4883
|
if (shouldPersist)
|
|
4872
4884
|
await this.persistDurableMetadata();
|
|
@@ -4881,23 +4893,19 @@ export class DoBackend {
|
|
|
4881
4893
|
const snapshot = this.txSnapshot;
|
|
4882
4894
|
const txID = this.txID;
|
|
4883
4895
|
try {
|
|
4884
|
-
|
|
4896
|
+
// ONE atomic rollback on the backend: restores snapshotted tables from
|
|
4897
|
+
// the durable tx journal and discards pending tracked changes.
|
|
4898
|
+
if (txID && (this.txHasTrackedWrite || this.txDataSnapshots.size > 0)) {
|
|
4899
|
+
await this.httpClient.post(this.url('/rollback-tx'), JSON.stringify({ transactionID: txID }), { 'Content-Type': 'application/json' });
|
|
4900
|
+
}
|
|
4885
4901
|
}
|
|
4886
4902
|
finally {
|
|
4887
|
-
if (txID)
|
|
4888
|
-
await this.rollbackTrackedTransaction(txID);
|
|
4889
4903
|
if (snapshot)
|
|
4890
4904
|
this.restoreTransactionMetadataSnapshot(snapshot);
|
|
4891
4905
|
await this.persistDurableMetadata();
|
|
4892
4906
|
this.clearTransactionState();
|
|
4893
4907
|
}
|
|
4894
4908
|
}
|
|
4895
|
-
async commitTrackedTransaction(transactionID) {
|
|
4896
|
-
await this.httpClient.post(this.url('/commit-tracked-tx'), JSON.stringify({ transactionID }), { 'Content-Type': 'application/json' });
|
|
4897
|
-
}
|
|
4898
|
-
async rollbackTrackedTransaction(transactionID) {
|
|
4899
|
-
await this.httpClient.post(this.url('/rollback-tracked-tx'), JSON.stringify({ transactionID }), { 'Content-Type': 'application/json' });
|
|
4900
|
-
}
|
|
4901
4909
|
async execProtocolRaw(message, options) {
|
|
4902
4910
|
if (!this.ready)
|
|
4903
4911
|
await this.waitReady;
|
|
@@ -5639,19 +5647,6 @@ export class DoBackend {
|
|
|
5639
5647
|
const result = await this.doExecResult("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1", [table]);
|
|
5640
5648
|
return result.rows.length > 0;
|
|
5641
5649
|
}
|
|
5642
|
-
async triggerDefinitionsForTables(tables) {
|
|
5643
|
-
const uniqueTables = [...new Set(tables)].filter(Boolean);
|
|
5644
|
-
if (uniqueTables.length === 0)
|
|
5645
|
-
return [];
|
|
5646
|
-
const placeholders = uniqueTables.map(() => '?').join(', ');
|
|
5647
|
-
const result = await this.doExecResult(`SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND tbl_name IN (${placeholders}) ORDER BY name`, uniqueTables);
|
|
5648
|
-
return result.rows
|
|
5649
|
-
.map((row) => ({
|
|
5650
|
-
name: String(row.name ?? ''),
|
|
5651
|
-
sql: String(row.sql ?? ''),
|
|
5652
|
-
}))
|
|
5653
|
-
.filter((trigger) => trigger.name && trigger.sql);
|
|
5654
|
-
}
|
|
5655
5650
|
async snapshotTransactionTable(table) {
|
|
5656
5651
|
if (!this.inTransaction || this.txDataSnapshots.has(table))
|
|
5657
5652
|
return;
|
|
@@ -5661,12 +5656,22 @@ export class DoBackend {
|
|
|
5661
5656
|
// the table — registration only happens after a successful CREATE, so its
|
|
5662
5657
|
// presence is proof the table exists. saves one /exec on the first write
|
|
5663
5658
|
// to each table per tx, which on chat's hot mutation paths matters.
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5659
|
+
const exists = this.schemaMetadata.has(table) || (await this.tableExistsInDo(table));
|
|
5660
|
+
// snapshot + manifest row land in one atomic /batch, so a DO kill at any
|
|
5661
|
+
// point leaves either no trace or a journal entry recovery can roll back.
|
|
5662
|
+
// the DDL is idempotent and rides the same batch (no extra round-trip).
|
|
5663
|
+
const snapshot = exists ? this.transactionSnapshotName(table) : null;
|
|
5664
|
+
const statements = [{ sql: TX_MANIFEST_DDL }];
|
|
5665
|
+
if (snapshot) {
|
|
5666
|
+
statements.push({
|
|
5667
|
+
sql: `CREATE TABLE ${quoteIdentifier(snapshot)} AS SELECT * FROM ${quoteIdentifier(table)}`,
|
|
5668
|
+
});
|
|
5667
5669
|
}
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
+
statements.push({
|
|
5671
|
+
sql: `INSERT INTO "${TX_MANIFEST_TABLE}" (tx_id, owner, original, snapshot) VALUES (?, ?, ?, ?)`,
|
|
5672
|
+
params: [this.txID, this.txOwner, table, snapshot],
|
|
5673
|
+
});
|
|
5674
|
+
await this.doRawBatch(statements);
|
|
5670
5675
|
this.txDataSnapshots.set(table, snapshot);
|
|
5671
5676
|
}
|
|
5672
5677
|
async snapshotTransactionChangeTables() {
|
|
@@ -5691,40 +5696,8 @@ export class DoBackend {
|
|
|
5691
5696
|
if (this.trackingForStatement(statement))
|
|
5692
5697
|
await this.snapshotTransactionChangeTables();
|
|
5693
5698
|
}
|
|
5694
|
-
async dropTransactionSnapshots() {
|
|
5695
|
-
const drops = [];
|
|
5696
|
-
for (const snapshot of this.txDataSnapshots.values()) {
|
|
5697
|
-
if (snapshot)
|
|
5698
|
-
drops.push(`DROP TABLE IF EXISTS ${quoteIdentifier(snapshot)}`);
|
|
5699
|
-
}
|
|
5700
|
-
if (drops.length > 0)
|
|
5701
|
-
await this.doRawBatch(drops);
|
|
5702
|
-
}
|
|
5703
|
-
async restoreTransactionDataSnapshots() {
|
|
5704
|
-
const entries = [...this.txDataSnapshots.entries()].reverse();
|
|
5705
|
-
const restoredTables = entries
|
|
5706
|
-
.filter(([, snapshot]) => Boolean(snapshot))
|
|
5707
|
-
.map(([table]) => table);
|
|
5708
|
-
const triggers = await this.triggerDefinitionsForTables(restoredTables);
|
|
5709
|
-
const statements = [];
|
|
5710
|
-
for (const trigger of triggers) {
|
|
5711
|
-
statements.push(`DROP TRIGGER IF EXISTS ${quoteIdentifier(trigger.name)}`);
|
|
5712
|
-
}
|
|
5713
|
-
for (const [table, snapshot] of entries) {
|
|
5714
|
-
const quotedTable = quoteIdentifier(table);
|
|
5715
|
-
if (!snapshot) {
|
|
5716
|
-
statements.push(`DROP TABLE IF EXISTS ${quotedTable}`);
|
|
5717
|
-
continue;
|
|
5718
|
-
}
|
|
5719
|
-
const quotedSnapshot = quoteIdentifier(snapshot);
|
|
5720
|
-
statements.push(`DELETE FROM ${quotedTable}`, `INSERT OR REPLACE INTO ${quotedTable} SELECT * FROM ${quotedSnapshot}`, `DROP TABLE IF EXISTS ${quotedSnapshot}`);
|
|
5721
|
-
}
|
|
5722
|
-
for (const trigger of triggers)
|
|
5723
|
-
statements.push(trigger.sql);
|
|
5724
|
-
await this.doRawBatch(statements);
|
|
5725
|
-
}
|
|
5726
5699
|
async doRawBatch(statements) {
|
|
5727
|
-
const sqls = statements.filter((
|
|
5700
|
+
const sqls = statements.filter((statement) => (typeof statement === 'string' ? statement : statement.sql).trim());
|
|
5728
5701
|
if (sqls.length === 0)
|
|
5729
5702
|
return;
|
|
5730
5703
|
await this.httpClient.post(this.url('/batch'), JSON.stringify({ statements: sqls }), {
|