orez 0.4.73 → 0.5.2
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/README.md +42 -1
- package/dist/cf-do/cdc.d.ts +148 -0
- package/dist/cf-do/cdc.d.ts.map +1 -0
- package/dist/cf-do/cdc.js +719 -0
- package/dist/cf-do/cdc.js.map +1 -0
- package/dist/cf-do/row-undo.d.ts +36 -0
- package/dist/cf-do/row-undo.d.ts.map +1 -0
- package/dist/cf-do/row-undo.js +209 -0
- package/dist/cf-do/row-undo.js.map +1 -0
- package/dist/cf-do/tx-journal.d.ts +32 -10
- package/dist/cf-do/tx-journal.d.ts.map +1 -1
- package/dist/cf-do/tx-journal.js +148 -31
- package/dist/cf-do/tx-journal.js.map +1 -1
- package/dist/cf-do/watermark.d.ts +7 -0
- package/dist/cf-do/watermark.d.ts.map +1 -1
- package/dist/cf-do/watermark.js +9 -0
- package/dist/cf-do/watermark.js.map +1 -1
- package/dist/cf-do/worker.d.ts +19 -1
- package/dist/cf-do/worker.d.ts.map +1 -1
- package/dist/cf-do/worker.js +218 -46
- package/dist/cf-do/worker.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +35 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/do-sql-tracking.d.ts +2 -0
- package/dist/do-sql-tracking.d.ts.map +1 -1
- package/dist/do-sql-tracking.js +11 -1
- package/dist/do-sql-tracking.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +146 -40
- package/dist/index.js.map +1 -1
- package/dist/pg-proxy-do-backend.d.ts +8 -0
- package/dist/pg-proxy-do-backend.d.ts.map +1 -1
- package/dist/pg-proxy-do-backend.js +301 -47
- package/dist/pg-proxy-do-backend.js.map +1 -1
- package/dist/pg-proxy.d.ts +22 -1
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +63 -1
- package/dist/pg-proxy.js.map +1 -1
- package/dist/pg-sqlite-compiler/index.d.ts.map +1 -1
- package/dist/pg-sqlite-compiler/index.js +9 -1
- package/dist/pg-sqlite-compiler/index.js.map +1 -1
- package/dist/pg-sqlite-compiler/passes/datetime.d.ts +2 -0
- package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -1
- package/dist/pg-sqlite-compiler/passes/datetime.js +20 -18
- package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -1
- package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -1
- package/dist/pg-sqlite-compiler/passes/index.js +6 -1
- package/dist/pg-sqlite-compiler/passes/index.js.map +1 -1
- package/dist/pg-sqlite-compiler/passes/json-functions.d.ts +3 -0
- package/dist/pg-sqlite-compiler/passes/json-functions.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/json-functions.js +57 -0
- package/dist/pg-sqlite-compiler/passes/json-functions.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/row-json.d.ts +3 -0
- package/dist/pg-sqlite-compiler/passes/row-json.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/row-json.js +230 -0
- package/dist/pg-sqlite-compiler/passes/row-json.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/schema.d.ts.map +1 -1
- package/dist/pg-sqlite-compiler/passes/schema.js +16 -1
- package/dist/pg-sqlite-compiler/passes/schema.js.map +1 -1
- package/dist/pg-sqlite-compiler/passes/string-functions.d.ts +11 -0
- package/dist/pg-sqlite-compiler/passes/string-functions.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/string-functions.js +37 -0
- package/dist/pg-sqlite-compiler/passes/string-functions.js.map +1 -0
- package/dist/worker/local-sql-backend.d.ts.map +1 -1
- package/dist/worker/local-sql-backend.js +114 -13
- package/dist/worker/local-sql-backend.js.map +1 -1
- package/package.json +2 -2
package/dist/cf-do/worker.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// @ts-nocheck — cloudflare:workers types not available in orez
|
|
2
2
|
import { DurableObject } from 'cloudflare:workers';
|
|
3
|
-
import { isSqlMutation, RollingRowWriteBudget, trackSqlCursorRowsWritten, trackedChangeRow, WriteBudgetExceededError, } from '../do-sql-tracking.js';
|
|
4
|
-
import {
|
|
3
|
+
import { isSqlMutation, isSqlRowMutation, RollingRowWriteBudget, trackSqlCursorRowsWritten, trackedChangeRow, WriteBudgetExceededError, } from '../do-sql-tracking.js';
|
|
4
|
+
import { TransactionalCdc, } from './cdc.js';
|
|
5
|
+
import { appendPendingChange, deletePendingChanges, ensurePendingChangesTable, rollbackPendingChanges, } from './row-undo.js';
|
|
6
|
+
import { commitTxJournal, recoverTxJournal, rollbackTxJournal, snapshotSideEffectWriteTables, upgradeToTableSnapshot, } from './tx-journal.js';
|
|
5
7
|
import { DurableWatermarkState } from './watermark.js';
|
|
6
8
|
const SCHEMA_VERSION = 1;
|
|
7
9
|
const SQL_ERROR_SNIPPET_RADIUS = 1600;
|
|
@@ -61,12 +63,14 @@ function sqlErrorSnippet(sql, message) {
|
|
|
61
63
|
export class ZeroDO extends DurableObject {
|
|
62
64
|
sql;
|
|
63
65
|
watermarks;
|
|
66
|
+
cdc;
|
|
64
67
|
schemaTables = new Set();
|
|
65
68
|
tableSchemas = new Map();
|
|
66
69
|
writeBudget;
|
|
67
70
|
writeBudgetDisabled;
|
|
68
71
|
writeBudgetAdminToken;
|
|
69
72
|
activeWriteMeasurements = null;
|
|
73
|
+
pendingChangesSchemaReady = false;
|
|
70
74
|
recordWriteBudgetRows(rows) {
|
|
71
75
|
const wasTripped = this.writeBudget.status().tripped;
|
|
72
76
|
try {
|
|
@@ -144,6 +148,7 @@ export class ZeroDO extends DurableObject {
|
|
|
144
148
|
this.writeBudget.restoreTrip(trippedAt);
|
|
145
149
|
});
|
|
146
150
|
}
|
|
151
|
+
this.cdc = new TransactionalCdc(this.sql);
|
|
147
152
|
this.watermarks = new DurableWatermarkState(this.sql);
|
|
148
153
|
}
|
|
149
154
|
async fetch(request) {
|
|
@@ -421,15 +426,15 @@ export class ZeroDO extends DurableObject {
|
|
|
421
426
|
const body = (await request.json());
|
|
422
427
|
sql = body.sql;
|
|
423
428
|
const params = Array.isArray(body.params) ? body.params : [];
|
|
424
|
-
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const result = await this.withSchemaProvisioningWait(() =>
|
|
431
|
-
? this.
|
|
432
|
-
: this.executeSQL(sql, params));
|
|
429
|
+
const transactionID = String(body.transactionID || body.track?.transactionID || '');
|
|
430
|
+
// CDC rows are drained into the durable change log before the storage
|
|
431
|
+
// transaction returns. Keep unrelated DDL/read calls on the fast path:
|
|
432
|
+
// wrapping every /exec adds ~2-5ms and materially slows large schemas.
|
|
433
|
+
const needsAtomicCapture = !!body.track ||
|
|
434
|
+
(this.cdc.active && (isSqlRowMutation(sql) || this.cdc.capturesSchemaChange(sql)));
|
|
435
|
+
const result = await this.withSchemaProvisioningWait(() => needsAtomicCapture
|
|
436
|
+
? this.atomically(() => this.executeSQL(sql, params, body.track, transactionID || undefined))
|
|
437
|
+
: this.executeSQL(sql, params, undefined, transactionID || undefined));
|
|
433
438
|
return Response.json(measurements ? { ...result, writeMeasurements: measurements } : result);
|
|
434
439
|
}
|
|
435
440
|
catch (err) {
|
|
@@ -449,9 +454,11 @@ export class ZeroDO extends DurableObject {
|
|
|
449
454
|
async handleBatch(request) {
|
|
450
455
|
const measurements = this.startWriteMeasurement(request);
|
|
451
456
|
try {
|
|
452
|
-
const { statements } = (await request.json());
|
|
453
|
-
const allRows = await this.withSchemaProvisioningWait(() => this.
|
|
457
|
+
const { statements, cdcTables } = (await request.json());
|
|
458
|
+
const allRows = await this.withSchemaProvisioningWait(() => this.atomically(() => {
|
|
454
459
|
const results = [];
|
|
460
|
+
if (Array.isArray(cdcTables))
|
|
461
|
+
this.cdc.syncTables(cdcTables);
|
|
455
462
|
for (const statement of statements) {
|
|
456
463
|
const item = typeof statement === 'string' ? { sql: statement } : statement;
|
|
457
464
|
if (!item?.sql?.trim())
|
|
@@ -465,7 +472,7 @@ export class ZeroDO extends DurableObject {
|
|
|
465
472
|
continue;
|
|
466
473
|
}
|
|
467
474
|
try {
|
|
468
|
-
results.push(this.executeSQL(item.sql, Array.isArray(item.params) ? item.params : [], item.track));
|
|
475
|
+
results.push(this.executeSQL(item.sql, Array.isArray(item.params) ? item.params : [], item.track, item.transactionID || item.track?.transactionID));
|
|
469
476
|
}
|
|
470
477
|
catch (err) {
|
|
471
478
|
if (err instanceof WriteBudgetExceededError)
|
|
@@ -477,6 +484,7 @@ export class ZeroDO extends DurableObject {
|
|
|
477
484
|
}));
|
|
478
485
|
return Response.json({
|
|
479
486
|
results: allRows,
|
|
487
|
+
capturedChanges: allRows.reduce((total, result) => total + Number(result.capturedChanges ?? 0), 0),
|
|
480
488
|
...(measurements ? { writeMeasurements: measurements } : null),
|
|
481
489
|
});
|
|
482
490
|
}
|
|
@@ -534,7 +542,7 @@ export class ZeroDO extends DurableObject {
|
|
|
534
542
|
const transactionID = String(body.transactionID || '');
|
|
535
543
|
if (!transactionID)
|
|
536
544
|
throw new Error('missing transactionID');
|
|
537
|
-
const count = await this.
|
|
545
|
+
const count = await this.atomically(() => {
|
|
538
546
|
const committed = this.commitPendingTrackedChanges(transactionID);
|
|
539
547
|
commitTxJournal(this.sql, transactionID);
|
|
540
548
|
return committed;
|
|
@@ -563,7 +571,8 @@ export class ZeroDO extends DurableObject {
|
|
|
563
571
|
const transactionID = String(body.transactionID || '');
|
|
564
572
|
if (!transactionID)
|
|
565
573
|
throw new Error('missing transactionID');
|
|
566
|
-
const count = await this.
|
|
574
|
+
const count = await this.atomically(() => {
|
|
575
|
+
this.rollbackPendingTrackedChanges(transactionID);
|
|
567
576
|
rollbackTxJournal(this.sql, transactionID);
|
|
568
577
|
return this.deletePendingTrackedChanges(transactionID);
|
|
569
578
|
});
|
|
@@ -593,8 +602,10 @@ export class ZeroDO extends DurableObject {
|
|
|
593
602
|
try {
|
|
594
603
|
const body = (await request.json().catch(() => ({})));
|
|
595
604
|
const owner = body.owner === undefined ? undefined : String(body.owner);
|
|
596
|
-
const transactionIDs = await this.
|
|
597
|
-
const recovered = recoverTxJournal(this.sql, owner)
|
|
605
|
+
const transactionIDs = await this.atomically(() => {
|
|
606
|
+
const recovered = recoverTxJournal(this.sql, owner, (txID) => {
|
|
607
|
+
this.rollbackPendingTrackedChanges(txID);
|
|
608
|
+
});
|
|
598
609
|
for (const txID of recovered)
|
|
599
610
|
this.deletePendingTrackedChanges(txID);
|
|
600
611
|
return recovered;
|
|
@@ -643,7 +654,7 @@ export class ZeroDO extends DurableObject {
|
|
|
643
654
|
}
|
|
644
655
|
async handleSnapshot() {
|
|
645
656
|
try {
|
|
646
|
-
return this.
|
|
657
|
+
return this.atomicallySync(() => {
|
|
647
658
|
this.ensureSchemaMetadataTable();
|
|
648
659
|
const names = this.sql
|
|
649
660
|
.exec('SELECT name FROM _zero_schema_tables ORDER BY name')
|
|
@@ -662,6 +673,42 @@ export class ZeroDO extends DurableObject {
|
|
|
662
673
|
return Response.json({ error: err.message }, { status: 500 });
|
|
663
674
|
}
|
|
664
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Run work in a storage transaction, re-deriving every in-memory schema cache
|
|
678
|
+
* from SQLite if it aborts.
|
|
679
|
+
*
|
|
680
|
+
* ctx.storage.transaction() rolls the SQLite side back on throw, but the
|
|
681
|
+
* caches are plain fields that keep asserting state SQLite no longer has: a
|
|
682
|
+
* CDC table stays "registered and verified" with no trigger left on disk, so
|
|
683
|
+
* ensureTable short-circuits and every later write to it goes silently
|
|
684
|
+
* uncaptured. The readiness flags are the same class: their CREATE TABLE is
|
|
685
|
+
* rolled back while the flag still says the table exists.
|
|
686
|
+
*/
|
|
687
|
+
async atomically(work) {
|
|
688
|
+
try {
|
|
689
|
+
return await this.ctx.storage.transaction(work);
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
this.invalidateSchemaCaches();
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
atomicallySync(work) {
|
|
697
|
+
try {
|
|
698
|
+
return this.ctx.storage.transactionSync(work);
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
this.invalidateSchemaCaches();
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
invalidateSchemaCaches() {
|
|
706
|
+
this.watermarks.invalidateCache();
|
|
707
|
+
this.pendingChangesSchemaReady = false;
|
|
708
|
+
// Reload is intentionally last because corrupt persisted CDC metadata must
|
|
709
|
+
// throw (fail closed), without preventing the other caches from invalidating.
|
|
710
|
+
this.cdc.reload();
|
|
711
|
+
}
|
|
665
712
|
// sync (storage.transaction-safe) column presence check for the /batch
|
|
666
713
|
// skipIfColumnExists/skipIfColumnMissing conditions. a missing table reads
|
|
667
714
|
// as "no columns", which makes ADD COLUMN skips behave like pg's
|
|
@@ -675,26 +722,97 @@ export class ZeroDO extends DurableObject {
|
|
|
675
722
|
return false;
|
|
676
723
|
}
|
|
677
724
|
}
|
|
678
|
-
executeSQL(sql, params = [], track) {
|
|
679
|
-
|
|
725
|
+
executeSQL(sql, params = [], track, transactionID) {
|
|
726
|
+
let capturesTrackedTable = false;
|
|
727
|
+
if (track?.physicalTableName) {
|
|
728
|
+
capturesTrackedTable = this.cdc.ensureTable({
|
|
729
|
+
physicalTableName: track.physicalTableName,
|
|
730
|
+
tableName: track.tableName,
|
|
731
|
+
...(track.publish === false ? { publish: false } : null),
|
|
732
|
+
...(track.rowColumns?.length ? { columns: track.rowColumns } : null),
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
else if (track) {
|
|
736
|
+
capturesTrackedTable = this.cdc.capturesTable(track.tableName);
|
|
737
|
+
}
|
|
738
|
+
// DoBackend already marked this table row-journaled, betting the DO could
|
|
739
|
+
// capture before/after images for it. When it cannot, that marker promises
|
|
740
|
+
// a rollback nothing can perform, so take the table snapshot the journal
|
|
741
|
+
// would otherwise have taken. It has to happen before the DML, while the
|
|
742
|
+
// table still holds its pre-transaction contents.
|
|
743
|
+
const trackedTransactionID = track ? transactionID || track.transactionID : undefined;
|
|
744
|
+
let snapshotsOwnStatement = false;
|
|
745
|
+
if (track && !capturesTrackedTable && trackedTransactionID) {
|
|
746
|
+
const physicalTableName = track.physicalTableName || track.tableName.replace(/^public\./, '');
|
|
747
|
+
if (this.tableExists(physicalTableName)) {
|
|
748
|
+
upgradeToTableSnapshot(this.sql, trackedTransactionID, physicalTableName);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (track && trackedTransactionID) {
|
|
752
|
+
const physicalTableName = track.physicalTableName || track.tableName.replace(/^public\./, '');
|
|
753
|
+
snapshotsOwnStatement = snapshotSideEffectWriteTables(this.sql, trackedTransactionID, physicalTableName);
|
|
754
|
+
}
|
|
755
|
+
const suspendedCdc = this.cdc.beginSchemaChange(sql);
|
|
756
|
+
let cursor;
|
|
757
|
+
try {
|
|
758
|
+
cursor = this.sql.exec(sql, ...params);
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
// Restore capture against the unchanged schema before propagating a DDL
|
|
762
|
+
// failure. In normal /exec and /batch paths this is also protected by the
|
|
763
|
+
// surrounding storage transaction.
|
|
764
|
+
this.cdc.finishSchemaChange(suspendedCdc);
|
|
765
|
+
throw error;
|
|
766
|
+
}
|
|
767
|
+
this.cdc.finishSchemaChange(suspendedCdc);
|
|
680
768
|
const columns = Array.isArray(cursor.columnNames) ? cursor.columnNames : [];
|
|
681
769
|
const rows = this.cursorRows(cursor, columns);
|
|
682
|
-
|
|
770
|
+
const mutation = isSqlMutation(sql);
|
|
771
|
+
if (mutation)
|
|
683
772
|
this.writeBudget.recordLogical(rows.length);
|
|
684
|
-
if (!
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
773
|
+
if (mutation && !isSqlRowMutation(sql))
|
|
774
|
+
this.cdc.invalidateSchema();
|
|
775
|
+
const captured = track || (this.cdc.active && isSqlRowMutation(sql)) ? this.cdc.drain() : [];
|
|
776
|
+
for (const change of captured) {
|
|
777
|
+
this.appendCapturedChange(change, transactionID || track?.transactionID, !snapshotsOwnStatement);
|
|
778
|
+
}
|
|
779
|
+
// Backward compatibility for callers that have not yet supplied the
|
|
780
|
+
// physical SQLite table identity needed to install a trigger. Once a
|
|
781
|
+
// table is registered, trigger capture is the sole source of truth and
|
|
782
|
+
// includes arbitrary business-trigger side effects.
|
|
783
|
+
//
|
|
784
|
+
// These rows carry no before-image, so they only feed the changefeed. Their
|
|
785
|
+
// rollback is owned by the table snapshot taken above, and `undoable: false`
|
|
786
|
+
// keeps the row-undo pass from trying to restore them from a wire image that
|
|
787
|
+
// cannot round-trip a blob or an int64.
|
|
788
|
+
if (track && !capturesTrackedTable) {
|
|
789
|
+
const physicalTableName = track.physicalTableName || track.tableName.replace(/^public\./, '');
|
|
790
|
+
for (const row of rows) {
|
|
791
|
+
const trackedRow = trackedChangeRow(row, track);
|
|
792
|
+
const isDelete = track.operation === 'DELETE';
|
|
793
|
+
this.appendTrackedChange({
|
|
794
|
+
tableName: track.tableName,
|
|
795
|
+
op: isDelete ? 'DELETE' : track.operation,
|
|
796
|
+
rowData: isDelete ? null : trackedRow,
|
|
797
|
+
oldData: isDelete ? trackedRow : null,
|
|
798
|
+
transactionID: transactionID || track.transactionID,
|
|
799
|
+
physicalTableName,
|
|
800
|
+
publish: track.publish !== false,
|
|
801
|
+
undoable: false,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
if (track.publish !== false)
|
|
805
|
+
this.appendDerivedTrackedChanges(track, rows);
|
|
692
806
|
}
|
|
693
|
-
|
|
807
|
+
const publishedCaptured = captured.filter((change) => change.publish !== false).length;
|
|
808
|
+
if (!track)
|
|
809
|
+
return { rows, columns, capturedChanges: publishedCaptured };
|
|
694
810
|
return {
|
|
695
811
|
rows: track.returnRows ? rows : [],
|
|
696
812
|
columns: track.returnRows ? columns : [],
|
|
697
813
|
affectedRows: rows.length,
|
|
814
|
+
capturedChanges: publishedCaptured ||
|
|
815
|
+
(capturesTrackedTable || track.publish === false ? 0 : rows.length),
|
|
698
816
|
};
|
|
699
817
|
}
|
|
700
818
|
cursorRows(cursor, columns) {
|
|
@@ -745,8 +863,19 @@ export class ZeroDO extends DurableObject {
|
|
|
745
863
|
const rows = this.sql
|
|
746
864
|
.exec(`SELECT * FROM ${quoteIdent(sqliteTableName)} WHERE ${quoteIdent(keyColumn)} IN (${placeholders})`, ...values)
|
|
747
865
|
.toArray();
|
|
866
|
+
// Derived notifications: the rows were written by business triggers and are
|
|
867
|
+
// captured for the changefeed only. They carry no physical table name, so
|
|
868
|
+
// the row-undo pass never sees them, and their real writes are rolled back
|
|
869
|
+
// by whichever journal entry owns the table.
|
|
748
870
|
for (const row of rows) {
|
|
749
|
-
this.appendTrackedChange(
|
|
871
|
+
this.appendTrackedChange({
|
|
872
|
+
tableName: publicTableName,
|
|
873
|
+
op: 'UPDATE',
|
|
874
|
+
rowData: row,
|
|
875
|
+
oldData: null,
|
|
876
|
+
transactionID,
|
|
877
|
+
undoable: false,
|
|
878
|
+
});
|
|
750
879
|
}
|
|
751
880
|
}
|
|
752
881
|
messageRowUpdatesChannelLatestOrder(row) {
|
|
@@ -872,14 +1001,48 @@ export class ZeroDO extends DurableObject {
|
|
|
872
1001
|
this.appendChange(tn, 'DELETE', null, existing);
|
|
873
1002
|
}
|
|
874
1003
|
appendChange(tn, op, rowData, oldData) {
|
|
875
|
-
this.appendTrackedChange(tn, op, rowData, oldData);
|
|
1004
|
+
this.appendTrackedChange({ tableName: tn, op, rowData, oldData });
|
|
1005
|
+
}
|
|
1006
|
+
/** Record a trigger-captured change, before-images and row identity included. */
|
|
1007
|
+
appendCapturedChange(change, transactionID, undoable = true) {
|
|
1008
|
+
this.appendTrackedChange({
|
|
1009
|
+
tableName: change.tableName,
|
|
1010
|
+
op: change.op,
|
|
1011
|
+
rowData: change.rowData,
|
|
1012
|
+
oldData: change.oldData,
|
|
1013
|
+
transactionID,
|
|
1014
|
+
physicalTableName: change.physicalTableName,
|
|
1015
|
+
publish: change.publish !== false,
|
|
1016
|
+
rowJournal: change.rowJournal,
|
|
1017
|
+
oldJournal: change.oldJournal,
|
|
1018
|
+
newRowid: change.newRowid,
|
|
1019
|
+
oldRowid: change.oldRowid,
|
|
1020
|
+
undoable,
|
|
1021
|
+
});
|
|
876
1022
|
}
|
|
877
|
-
appendTrackedChange(
|
|
878
|
-
|
|
879
|
-
|
|
1023
|
+
appendTrackedChange(change) {
|
|
1024
|
+
const publish = change.publish !== false;
|
|
1025
|
+
if (!publish && !change.transactionID)
|
|
1026
|
+
return;
|
|
1027
|
+
if (change.transactionID) {
|
|
1028
|
+
this.ensurePendingTrackedChangesTable();
|
|
1029
|
+
appendPendingChange(this.sql, {
|
|
1030
|
+
transactionID: change.transactionID,
|
|
1031
|
+
physicalTableName: change.physicalTableName,
|
|
1032
|
+
tableName: change.tableName,
|
|
1033
|
+
publish,
|
|
1034
|
+
op: change.op,
|
|
1035
|
+
rowData: change.rowData,
|
|
1036
|
+
oldData: change.oldData,
|
|
1037
|
+
rowJournal: change.rowJournal ?? null,
|
|
1038
|
+
oldJournal: change.oldJournal ?? null,
|
|
1039
|
+
newRowid: change.newRowid ?? null,
|
|
1040
|
+
oldRowid: change.oldRowid ?? null,
|
|
1041
|
+
undoable: change.undoable === true,
|
|
1042
|
+
});
|
|
880
1043
|
return;
|
|
881
1044
|
}
|
|
882
|
-
this.appendCommittedTrackedChange(tableName, op, rowData, oldData);
|
|
1045
|
+
this.appendCommittedTrackedChange(change.tableName, change.op, change.rowData, change.oldData);
|
|
883
1046
|
}
|
|
884
1047
|
appendCommittedTrackedChange(tableName, op, rowData, oldData) {
|
|
885
1048
|
this.watermarks.ensureTables();
|
|
@@ -888,11 +1051,14 @@ export class ZeroDO extends DurableObject {
|
|
|
888
1051
|
this.watermarks.mark(watermark);
|
|
889
1052
|
}
|
|
890
1053
|
ensurePendingTrackedChangesTable() {
|
|
891
|
-
this.
|
|
1054
|
+
if (this.pendingChangesSchemaReady)
|
|
1055
|
+
return;
|
|
1056
|
+
ensurePendingChangesTable(this.sql);
|
|
1057
|
+
this.pendingChangesSchemaReady = true;
|
|
892
1058
|
}
|
|
893
|
-
|
|
1059
|
+
rollbackPendingTrackedChanges(transactionID) {
|
|
894
1060
|
this.ensurePendingTrackedChangesTable();
|
|
895
|
-
this.sql
|
|
1061
|
+
return rollbackPendingChanges(this.sql, transactionID);
|
|
896
1062
|
}
|
|
897
1063
|
commitPendingTrackedChanges(transactionID) {
|
|
898
1064
|
this.ensurePendingTrackedChangesTable();
|
|
@@ -901,7 +1067,7 @@ export class ZeroDO extends DurableObject {
|
|
|
901
1067
|
.exec(`INSERT INTO _zero_changes (table_name, op, row_data, old_data)
|
|
902
1068
|
SELECT table_name, op, row_data, old_data
|
|
903
1069
|
FROM _zero_pending_changes
|
|
904
|
-
WHERE transaction_id = ?
|
|
1070
|
+
WHERE transaction_id = ? AND publish != 0
|
|
905
1071
|
ORDER BY id
|
|
906
1072
|
RETURNING watermark`, transactionID)
|
|
907
1073
|
.toArray();
|
|
@@ -916,10 +1082,7 @@ export class ZeroDO extends DurableObject {
|
|
|
916
1082
|
}
|
|
917
1083
|
deletePendingTrackedChanges(transactionID) {
|
|
918
1084
|
this.ensurePendingTrackedChangesTable();
|
|
919
|
-
|
|
920
|
-
.exec('DELETE FROM _zero_pending_changes WHERE transaction_id = ? RETURNING 1 AS deleted', transactionID)
|
|
921
|
-
.toArray();
|
|
922
|
-
return result.length;
|
|
1085
|
+
return deletePendingChanges(this.sql, transactionID);
|
|
923
1086
|
}
|
|
924
1087
|
readChangesSince(watermark) {
|
|
925
1088
|
this.watermarks.ensureTables();
|
|
@@ -1078,7 +1241,16 @@ export class ZeroDO extends DurableObject {
|
|
|
1078
1241
|
value === true || value === 1 || value === '1' || value === 'true';
|
|
1079
1242
|
}
|
|
1080
1243
|
else if (type === 'number') {
|
|
1081
|
-
|
|
1244
|
+
// timestamp/timestamptz columns are declared `number` in the zero
|
|
1245
|
+
// schema but stored as postgres timestamp TEXT (pg-proxy-do-backend
|
|
1246
|
+
// `postgresTimestampText`, e.g. "2026-07-11 13:34:46.000+00"). Coercing
|
|
1247
|
+
// that text with Number() yields NaN, which JSON serializes as null and
|
|
1248
|
+
// silently wipes every timestamp reaching the sync-cf-host snapshot
|
|
1249
|
+
// feed. Forward a non-numeric value untouched so the engine's
|
|
1250
|
+
// timestamp_text_to_epoch_ms decodes it, matching the /changes feed
|
|
1251
|
+
// which forwards the raw text.
|
|
1252
|
+
const numeric = Number(value);
|
|
1253
|
+
normalized[key] = Number.isFinite(numeric) ? numeric : value;
|
|
1082
1254
|
}
|
|
1083
1255
|
else if (type === 'json' && typeof value === 'string') {
|
|
1084
1256
|
try {
|