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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import { deparseSync, loadModule, parseSync } from 'pgsql-parser';
|
|
3
3
|
import { TX_MANIFEST_DDL, TX_MANIFEST_TABLE } from './cf-do/tx-journal.js';
|
|
4
|
-
import { RETURNING_INTERNAL_PREFIX } from './do-sql-tracking.js';
|
|
4
|
+
import { isSqlRowMutation, RETURNING_INTERNAL_PREFIX } from './do-sql-tracking.js';
|
|
5
5
|
import { expandDelete, FkCascadeRegistry, recordAlterTableForeignKeys, recordCreateTableForeignKeys, } from './fk-cascade.js';
|
|
6
6
|
import { Mutex } from './mutex.js';
|
|
7
|
+
import { compile as compilePgToSqlite } from './pg-sqlite-compiler/index.js';
|
|
7
8
|
import { foldCountMarkerResult, transformCountedDeleteCte, } from './pg-sqlite-compiler/passes/dml-cte.js';
|
|
8
9
|
import { signalReplicationChange } from './replication/handler.js';
|
|
9
10
|
import { markSQLiteKeywordIdentifiers, restoreSQLiteKeywordIdentifierMarkers, } from './sqlite-keyword-identifiers.js';
|
|
@@ -13,7 +14,8 @@ import { markSQLiteKeywordIdentifiers, restoreSQLiteKeywordIdentifierMarkers, }
|
|
|
13
14
|
* Translates PG wire protocol messages → SQL → DO HTTP API → PG wire protocol responses.
|
|
14
15
|
*
|
|
15
16
|
* Handles PG transactions transparently: BEGIN/COMMIT/ROLLBACK are intercepted
|
|
16
|
-
* and data writes are restored from
|
|
17
|
+
* and data writes are restored from row before-images on ROLLBACK, with table
|
|
18
|
+
* snapshots retained as a fallback for writes the SQL planner cannot identify.
|
|
17
19
|
*/
|
|
18
20
|
const textEncoder = new TextEncoder();
|
|
19
21
|
const textDecoder = new TextDecoder();
|
|
@@ -53,6 +55,22 @@ const METADATA_TABLE = '_orez_pg_metadata';
|
|
|
53
55
|
// publication picks it up quickly, long enough that a genuinely
|
|
54
56
|
// publication-less db doesn't re-query on every write.
|
|
55
57
|
const EMPTY_PUBLICATION_RELOAD_THROTTLE_MS = 1000;
|
|
58
|
+
// All DoBackend instances for one logical database share a durable trigger
|
|
59
|
+
// set. Keep one ordered schema-scan/registration queue per target in this
|
|
60
|
+
// isolate; DDL/publication changes invalidate the reusable result while
|
|
61
|
+
// preserving request order.
|
|
62
|
+
const cdcRegistrationSyncs = new Map();
|
|
63
|
+
function cdcRegistrationState(key) {
|
|
64
|
+
let state = cdcRegistrationSyncs.get(key);
|
|
65
|
+
if (!state) {
|
|
66
|
+
state = { current: null, tail: Promise.resolve() };
|
|
67
|
+
cdcRegistrationSyncs.set(key, state);
|
|
68
|
+
}
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
71
|
+
function hasCdcRegistrationSync(key) {
|
|
72
|
+
return !!cdcRegistrationSyncs.get(key)?.current;
|
|
73
|
+
}
|
|
56
74
|
// ── Utilities ─────────────────────────────────────────────────────────────
|
|
57
75
|
function concat(...parts) {
|
|
58
76
|
let total = 0;
|
|
@@ -459,7 +477,9 @@ function inferFieldsFromSQL(sql) {
|
|
|
459
477
|
: extractTopLevelSelectList(sql);
|
|
460
478
|
if (!list || list === '*')
|
|
461
479
|
return [];
|
|
462
|
-
return splitTopLevelComma(list).map((part) => ({
|
|
480
|
+
return splitTopLevelComma(list).map((part) => ({
|
|
481
|
+
name: inferFieldName(part),
|
|
482
|
+
}));
|
|
463
483
|
}
|
|
464
484
|
function columnRefTailName(value) {
|
|
465
485
|
const fields = value?.ColumnRef?.fields;
|
|
@@ -1140,9 +1160,6 @@ function isDollarQuoteTagStart(ch) {
|
|
|
1140
1160
|
function isDollarQuoteTagPart(ch) {
|
|
1141
1161
|
return ch === '_' || isAsciiAlpha(ch) || isAsciiDigit(ch);
|
|
1142
1162
|
}
|
|
1143
|
-
function currentTimestampNode() {
|
|
1144
|
-
return { SQLValueFunction: { op: 'SVFOP_CURRENT_TIMESTAMP', typmod: -1 } };
|
|
1145
|
-
}
|
|
1146
1163
|
function isTimestampOid(oid) {
|
|
1147
1164
|
return oid === PG_TYPE_TIMESTAMP || oid === PG_TYPE_TIMESTAMPTZ;
|
|
1148
1165
|
}
|
|
@@ -2005,8 +2022,6 @@ function rewriteNode(value, context) {
|
|
|
2005
2022
|
const sqliteName = SQLITE_FUNCTION_BY_PG_FUNCTION.get(name ?? '');
|
|
2006
2023
|
if (sqliteName)
|
|
2007
2024
|
value.FuncCall.funcname = [stringNode(sqliteName)];
|
|
2008
|
-
if (name === 'now')
|
|
2009
|
-
return currentTimestampNode();
|
|
2010
2025
|
if (name === 'nextval')
|
|
2011
2026
|
return intConst(1);
|
|
2012
2027
|
if (name === '_drop_zero_slot' || name === 'pg_drop_replication_slot') {
|
|
@@ -2600,7 +2615,9 @@ function normalizeRename(stmt) {
|
|
|
2600
2615
|
flattenRangeVar(relation);
|
|
2601
2616
|
if (to.table !== stmt.newname)
|
|
2602
2617
|
stmt.newname = to.table;
|
|
2603
|
-
return {
|
|
2618
|
+
return {
|
|
2619
|
+
schemaMetadataChanges: [{ action: 'renameTable', from: table, to }],
|
|
2620
|
+
};
|
|
2604
2621
|
}
|
|
2605
2622
|
if (stmt.renameType === 'OBJECT_COLUMN' && stmt.subname && stmt.newname) {
|
|
2606
2623
|
flattenRangeVar(relation);
|
|
@@ -2805,7 +2822,12 @@ function replaceSchemaSpecsFunctionCalls(sql) {
|
|
|
2805
2822
|
}
|
|
2806
2823
|
function deparseStatement(version, stmt) {
|
|
2807
2824
|
const quotedByMarker = markSQLiteKeywordIdentifiers(stmt);
|
|
2808
|
-
|
|
2825
|
+
const proxySql = stripTrailingSemicolon(restoreSQLiteKeywordIdentifierMarkers(deparseSync({ version, stmts: [{ stmt }] }), quotedByMarker).trim());
|
|
2826
|
+
// The proxy owns transport-specific metadata and execution planning, but
|
|
2827
|
+
// pg-to-sqlite is the final authority for PostgreSQL SQL semantics. Running
|
|
2828
|
+
// the deparsed statement through the package compiler here prevents the
|
|
2829
|
+
// proxy from growing a second set of function/type/syntax rewrites.
|
|
2830
|
+
return compilePgToSqlite(proxySql).sql;
|
|
2809
2831
|
}
|
|
2810
2832
|
function starTarget() {
|
|
2811
2833
|
return {
|
|
@@ -3065,7 +3087,9 @@ function compileSelectIntoNew(version, select, triggerTable, rowCondition) {
|
|
|
3065
3087
|
const targetColumn = rel.relname;
|
|
3066
3088
|
const cloned = cloneAst(select);
|
|
3067
3089
|
delete cloned.intoClause;
|
|
3068
|
-
const selectSQL = deparseStatement(version, {
|
|
3090
|
+
const selectSQL = deparseStatement(version, {
|
|
3091
|
+
SelectStmt: rewriteNode(cloned),
|
|
3092
|
+
});
|
|
3069
3093
|
return rowUpdateSQL(triggerTable, targetColumn, `(${selectSQL})`, rowCondition);
|
|
3070
3094
|
}
|
|
3071
3095
|
function deparseExpressionSQL(version, expr) {
|
|
@@ -3865,7 +3889,9 @@ function copySelectSQL(sql) {
|
|
|
3865
3889
|
stringValue(def.arg)?.toLowerCase() === 'binary');
|
|
3866
3890
|
});
|
|
3867
3891
|
return {
|
|
3868
|
-
sql: deparseStatement(parsed.version, {
|
|
3892
|
+
sql: deparseStatement(parsed.version, {
|
|
3893
|
+
SelectStmt: copy.query.SelectStmt,
|
|
3894
|
+
}),
|
|
3869
3895
|
binary,
|
|
3870
3896
|
};
|
|
3871
3897
|
}
|
|
@@ -4673,7 +4699,10 @@ function advisoryLockResult(select) {
|
|
|
4673
4699
|
: [{ name: 'pg_advisory_xact_lock', source: 'pg_advisory_xact_lock' }];
|
|
4674
4700
|
return {
|
|
4675
4701
|
rows: [Object.fromEntries(projectedFields.map((field) => [field.name, null]))],
|
|
4676
|
-
fields: projectedFields.map((field) => ({
|
|
4702
|
+
fields: projectedFields.map((field) => ({
|
|
4703
|
+
name: field.name,
|
|
4704
|
+
oid: field.oid,
|
|
4705
|
+
})),
|
|
4677
4706
|
};
|
|
4678
4707
|
}
|
|
4679
4708
|
function logicalEmitMessageResult(select) {
|
|
@@ -4741,14 +4770,15 @@ export class DoBackend {
|
|
|
4741
4770
|
// connects past the client's connect budget. cache per instance, invalidated
|
|
4742
4771
|
// on DDL / metadata / publication changes and on rollback.
|
|
4743
4772
|
publicationTableInfoCache = null;
|
|
4773
|
+
cdcRegistrationDirty = false;
|
|
4744
4774
|
readyPromise = null;
|
|
4745
4775
|
operationMutex = new Mutex();
|
|
4746
4776
|
// Transaction state. The Durable Object refuses raw SQL BEGIN/COMMIT/SAVEPOINT
|
|
4747
4777
|
// (Cloudflare requires ctx.storage.transaction()), so PG-style multi-call
|
|
4748
4778
|
// transactions can't ride on SQLite's own transaction machinery. Instead we
|
|
4749
|
-
// emulate rollback
|
|
4750
|
-
//
|
|
4751
|
-
//
|
|
4779
|
+
// emulate rollback with CDC row before-images for parsed DML. Unknown writes
|
|
4780
|
+
// fall back to snapshotting the table on its first in-tx write. Both restore
|
|
4781
|
+
// paths run atomically in the DO's storage transaction.
|
|
4752
4782
|
inTransaction = false;
|
|
4753
4783
|
txID = null;
|
|
4754
4784
|
txSnapshot = null;
|
|
@@ -5131,6 +5161,7 @@ export class DoBackend {
|
|
|
5131
5161
|
// Cached rewrites may reference metadata that the rollback just invalidated.
|
|
5132
5162
|
this.rewriteCache.clear();
|
|
5133
5163
|
this.publicationTableInfoCache = null;
|
|
5164
|
+
this.invalidateCdcRegistration();
|
|
5134
5165
|
}
|
|
5135
5166
|
clearTransactionState() {
|
|
5136
5167
|
this.inTransaction = false;
|
|
@@ -5170,14 +5201,16 @@ export class DoBackend {
|
|
|
5170
5201
|
const shouldSignal = this.txHasTrackedWrite;
|
|
5171
5202
|
const txID = this.txID;
|
|
5172
5203
|
// ONE atomic commit point on the backend: promotes pending tracked
|
|
5173
|
-
// changes and clears the tx journal (snapshots
|
|
5204
|
+
// changes and clears the tx journal (row images, snapshots, and manifest) in a single
|
|
5174
5205
|
// storage transaction. read-only transactions skip the round-trip.
|
|
5175
5206
|
if (txID && (shouldSignal || this.txDataSnapshots.size > 0)) {
|
|
5176
5207
|
await this.httpClient.post(this.url('/commit-tx'), JSON.stringify({ transactionID: txID }), { 'Content-Type': 'application/json' });
|
|
5177
5208
|
}
|
|
5178
5209
|
this.clearTransactionState();
|
|
5179
|
-
if (shouldPersist)
|
|
5210
|
+
if (shouldPersist) {
|
|
5180
5211
|
await this.persistDurableMetadata();
|
|
5212
|
+
await this.ensureCdcRegistration();
|
|
5213
|
+
}
|
|
5181
5214
|
if (shouldSignal)
|
|
5182
5215
|
signalReplicationChange();
|
|
5183
5216
|
}
|
|
@@ -5189,8 +5222,8 @@ export class DoBackend {
|
|
|
5189
5222
|
const snapshot = this.txSnapshot;
|
|
5190
5223
|
const txID = this.txID;
|
|
5191
5224
|
try {
|
|
5192
|
-
// ONE atomic rollback on the backend: restores
|
|
5193
|
-
//
|
|
5225
|
+
// ONE atomic rollback on the backend: restores row before-images and any
|
|
5226
|
+
// fallback table snapshots, then discards pending tracked changes.
|
|
5194
5227
|
if (txID && (this.txHasTrackedWrite || this.txDataSnapshots.size > 0)) {
|
|
5195
5228
|
await this.httpClient.post(this.url('/rollback-tx'), JSON.stringify({ transactionID: txID }), { 'Content-Type': 'application/json' });
|
|
5196
5229
|
}
|
|
@@ -5200,6 +5233,8 @@ export class DoBackend {
|
|
|
5200
5233
|
this.restoreTransactionMetadataSnapshot(snapshot);
|
|
5201
5234
|
await this.persistDurableMetadata();
|
|
5202
5235
|
this.clearTransactionState();
|
|
5236
|
+
if (snapshot)
|
|
5237
|
+
await this.ensureCdcRegistration();
|
|
5203
5238
|
}
|
|
5204
5239
|
}
|
|
5205
5240
|
async execProtocolRaw(message, options) {
|
|
@@ -5325,7 +5360,13 @@ export class DoBackend {
|
|
|
5325
5360
|
return this.buildCopyResponse(result, copySelect.sql, copySelect.binary);
|
|
5326
5361
|
}
|
|
5327
5362
|
// Prepare query
|
|
5328
|
-
|
|
5363
|
+
let rewrittenStatements;
|
|
5364
|
+
try {
|
|
5365
|
+
rewrittenStatements = this.rewriteSQLStatements(sql);
|
|
5366
|
+
}
|
|
5367
|
+
catch (err) {
|
|
5368
|
+
return concat(buildErrorResponse(err.message), this.readyForQuery());
|
|
5369
|
+
}
|
|
5329
5370
|
const rewritten = rewrittenSQLText(rewrittenStatements);
|
|
5330
5371
|
if (rewritten === '' || rewritten.startsWith('--')) {
|
|
5331
5372
|
await this.applyStatementMetadata(rewrittenStatements);
|
|
@@ -5357,6 +5398,13 @@ export class DoBackend {
|
|
|
5357
5398
|
}
|
|
5358
5399
|
async applyStatementMetadata(statements) {
|
|
5359
5400
|
let changed = false;
|
|
5401
|
+
let publicationChanged = false;
|
|
5402
|
+
if (statements.some((statement) => statement.isDDL)) {
|
|
5403
|
+
// DROP/CREATE INDEX and DROP TABLE may not carry column metadata deltas,
|
|
5404
|
+
// but they still invalidate both the catalog and persisted trigger set.
|
|
5405
|
+
this.publicationTableInfoCache = null;
|
|
5406
|
+
this.invalidateCdcRegistration();
|
|
5407
|
+
}
|
|
5360
5408
|
for (const statement of statements) {
|
|
5361
5409
|
for (const column of statement.schemaColumns ?? []) {
|
|
5362
5410
|
let table = this.schemaMetadata.get(column.table);
|
|
@@ -5374,6 +5422,7 @@ export class DoBackend {
|
|
|
5374
5422
|
for (const change of statement.publicationChanges ?? []) {
|
|
5375
5423
|
this.applyPublicationChange(change);
|
|
5376
5424
|
changed = true;
|
|
5425
|
+
publicationChanged = true;
|
|
5377
5426
|
}
|
|
5378
5427
|
if (statement.fkEdges) {
|
|
5379
5428
|
// edges were already added to this.fkRegistry at rewrite time; persist
|
|
@@ -5385,6 +5434,7 @@ export class DoBackend {
|
|
|
5385
5434
|
}
|
|
5386
5435
|
if (changed) {
|
|
5387
5436
|
this.publicationTableInfoCache = null;
|
|
5437
|
+
this.invalidateCdcRegistration();
|
|
5388
5438
|
// inside an explicit tx we batch the persist to commit time. chat's
|
|
5389
5439
|
// migrations open one tx and run dozens of DDL statements; persisting
|
|
5390
5440
|
// after every one was the hot path.
|
|
@@ -5392,6 +5442,12 @@ export class DoBackend {
|
|
|
5392
5442
|
this.txMetadataDirty = true;
|
|
5393
5443
|
else
|
|
5394
5444
|
await this.persistDurableMetadata();
|
|
5445
|
+
// Publication membership changes alter which tables may enter the
|
|
5446
|
+
// changefeed. Apply those immediately; ordinary schema changes are
|
|
5447
|
+
// refreshed lazily before the next row write to keep migrations fast.
|
|
5448
|
+
if (publicationChanged && !this.inTransaction) {
|
|
5449
|
+
await this.ensureCdcRegistration();
|
|
5450
|
+
}
|
|
5395
5451
|
}
|
|
5396
5452
|
}
|
|
5397
5453
|
applySchemaMetadataChange(change) {
|
|
@@ -5466,36 +5522,121 @@ export class DoBackend {
|
|
|
5466
5522
|
}
|
|
5467
5523
|
trackingForStatement(statement) {
|
|
5468
5524
|
const tracking = statement.changeTracking;
|
|
5469
|
-
if (!tracking
|
|
5525
|
+
if (!tracking)
|
|
5470
5526
|
return undefined;
|
|
5527
|
+
// zero-cache's CVR/CDB databases use the same emulated PG transactions as
|
|
5528
|
+
// the application database. Their parsed DML needs rollback row images too;
|
|
5529
|
+
// otherwise hot internal tables such as `<shard>/cdc_changeLog` fall back to
|
|
5530
|
+
// a full table copy on every transaction and produce quadratic writes.
|
|
5531
|
+
if (this.dbName !== 'postgres') {
|
|
5532
|
+
return this.inTransaction ? { ...tracking, publish: false } : undefined;
|
|
5533
|
+
}
|
|
5471
5534
|
const { table } = tracking;
|
|
5535
|
+
let published = false;
|
|
5472
5536
|
if (table.schema === 'public') {
|
|
5473
5537
|
const publications = this.publicationNames();
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5538
|
+
published =
|
|
5539
|
+
publications.length > 0 &&
|
|
5540
|
+
this.publicationsForTable(table, publications).length > 0;
|
|
5477
5541
|
}
|
|
5478
|
-
if (table.schema !== 'pg_catalog' &&
|
|
5542
|
+
else if (table.schema !== 'pg_catalog' &&
|
|
5479
5543
|
table.schema !== 'information_schema' &&
|
|
5480
5544
|
!table.schema.startsWith('pg_') &&
|
|
5481
5545
|
!table.schema.startsWith('zero_') &&
|
|
5482
5546
|
!table.schema.startsWith('_zero') &&
|
|
5483
5547
|
!table.schema.includes('/') &&
|
|
5484
5548
|
TRACKED_SHARD_TABLES.has(table.tableName)) {
|
|
5485
|
-
|
|
5549
|
+
published = true;
|
|
5486
5550
|
}
|
|
5487
|
-
|
|
5551
|
+
if (published)
|
|
5552
|
+
return { ...tracking, publish: true };
|
|
5553
|
+
// PostgreSQL transactions still need row-accurate undo for private and
|
|
5554
|
+
// internal tables. The worker captures the same full before/after images
|
|
5555
|
+
// but marks them rollback-only so they never enter `_zero_changes`.
|
|
5556
|
+
return this.inTransaction ? { ...tracking, publish: false } : undefined;
|
|
5488
5557
|
}
|
|
5489
5558
|
trackingRequest(tracking) {
|
|
5490
5559
|
const rowColumns = this.schemaMetadata.get(tracking.table.table);
|
|
5491
5560
|
return {
|
|
5492
5561
|
tableName: `${tracking.table.schema}.${tracking.table.tableName}`,
|
|
5562
|
+
physicalTableName: tracking.table.table,
|
|
5493
5563
|
operation: tracking.operation,
|
|
5494
5564
|
returnRows: tracking.returnRows,
|
|
5495
5565
|
...(rowColumns ? { rowColumns: [...rowColumns.keys()] } : null),
|
|
5496
5566
|
...(this.inTransaction ? { transactionID: this.currentTransactionID() } : null),
|
|
5567
|
+
...(tracking.publish === false ? { publish: false } : null),
|
|
5568
|
+
};
|
|
5569
|
+
}
|
|
5570
|
+
cdcRegistrationKey() {
|
|
5571
|
+
return this.url('/cdc-registration');
|
|
5572
|
+
}
|
|
5573
|
+
invalidateCdcRegistration() {
|
|
5574
|
+
this.cdcRegistrationDirty = true;
|
|
5575
|
+
const state = cdcRegistrationSyncs.get(this.cdcRegistrationKey());
|
|
5576
|
+
if (state)
|
|
5577
|
+
state.current = null;
|
|
5578
|
+
}
|
|
5579
|
+
cdcRegistrationForInfo(info) {
|
|
5580
|
+
const published = info.schema === 'public' &&
|
|
5581
|
+
this.publicationsForTable(info, this.publicationNames()).length > 0;
|
|
5582
|
+
const trackedShard = info.schema !== 'public' &&
|
|
5583
|
+
info.schema !== 'pg_catalog' &&
|
|
5584
|
+
info.schema !== 'information_schema' &&
|
|
5585
|
+
!info.schema.startsWith('pg_') &&
|
|
5586
|
+
!info.schema.startsWith('zero_') &&
|
|
5587
|
+
!info.schema.startsWith('_zero') &&
|
|
5588
|
+
!info.schema.includes('/') &&
|
|
5589
|
+
TRACKED_SHARD_TABLES.has(info.tableName);
|
|
5590
|
+
if (!published && !trackedShard)
|
|
5591
|
+
return null;
|
|
5592
|
+
return {
|
|
5593
|
+
physicalTableName: info.name,
|
|
5594
|
+
tableName: `${info.schema}.${info.tableName}`,
|
|
5497
5595
|
};
|
|
5498
5596
|
}
|
|
5597
|
+
async ensureCdcRegistration() {
|
|
5598
|
+
if (this.dbName !== 'postgres')
|
|
5599
|
+
return;
|
|
5600
|
+
const key = this.cdcRegistrationKey();
|
|
5601
|
+
const state = cdcRegistrationState(key);
|
|
5602
|
+
for (;;) {
|
|
5603
|
+
const forced = this.cdcRegistrationDirty;
|
|
5604
|
+
let pending = state.current;
|
|
5605
|
+
if (forced || !pending) {
|
|
5606
|
+
// Mark this instance's invalidation as scheduled before yielding. If
|
|
5607
|
+
// another invalidation arrives while the request is in flight it sets
|
|
5608
|
+
// the flag again, and the loop queues a fresh authoritative set.
|
|
5609
|
+
this.cdcRegistrationDirty = false;
|
|
5610
|
+
const previous = state.tail;
|
|
5611
|
+
pending = previous
|
|
5612
|
+
.catch(() => undefined)
|
|
5613
|
+
.then(async () => {
|
|
5614
|
+
const infos = await this.publicationTableInfos();
|
|
5615
|
+
const cdcTables = infos
|
|
5616
|
+
.map((info) => this.cdcRegistrationForInfo(info))
|
|
5617
|
+
.filter((registration) => !!registration);
|
|
5618
|
+
await this.httpClient.post(this.url('/batch'), JSON.stringify({ statements: [], cdcTables }), { 'Content-Type': 'application/json' });
|
|
5619
|
+
});
|
|
5620
|
+
state.tail = pending;
|
|
5621
|
+
state.current = pending;
|
|
5622
|
+
}
|
|
5623
|
+
try {
|
|
5624
|
+
await pending;
|
|
5625
|
+
}
|
|
5626
|
+
catch (error) {
|
|
5627
|
+
if (state.current === pending)
|
|
5628
|
+
state.current = null;
|
|
5629
|
+
if (forced)
|
|
5630
|
+
this.cdcRegistrationDirty = true;
|
|
5631
|
+
throw error;
|
|
5632
|
+
}
|
|
5633
|
+
// A different backend may have invalidated or queued a replacement
|
|
5634
|
+
// while this request was in flight. Do not let a caller write until the
|
|
5635
|
+
// newest known registration has also landed.
|
|
5636
|
+
if (state.current === pending && !this.cdcRegistrationDirty)
|
|
5637
|
+
return;
|
|
5638
|
+
}
|
|
5639
|
+
}
|
|
5499
5640
|
visibleResultForTracking(result, tracking) {
|
|
5500
5641
|
if (!tracking)
|
|
5501
5642
|
return result;
|
|
@@ -5614,6 +5755,12 @@ export class DoBackend {
|
|
|
5614
5755
|
const portal = this.portals.get(portalName);
|
|
5615
5756
|
if (!portal)
|
|
5616
5757
|
return buildErrorResponse(`portal "${portalName}" does not exist`);
|
|
5758
|
+
try {
|
|
5759
|
+
this.rejectTransactionalDDL(portal.rewrittenStatements ?? []);
|
|
5760
|
+
}
|
|
5761
|
+
catch (err) {
|
|
5762
|
+
return buildErrorResponse(err.message);
|
|
5763
|
+
}
|
|
5617
5764
|
if (!portal.sql?.trim()) {
|
|
5618
5765
|
await this.applyStatementMetadata([
|
|
5619
5766
|
{
|
|
@@ -5915,14 +6062,28 @@ export class DoBackend {
|
|
|
5915
6062
|
const arrayParamNumbers = new Set();
|
|
5916
6063
|
const jsonParamNumbers = new Set();
|
|
5917
6064
|
const epochMillisParamNumbers = new Set();
|
|
6065
|
+
// DDL rewriting mutates function/FK metadata before execution and DDL
|
|
6066
|
+
// execution invalidates live caches. While an emulated transaction is
|
|
6067
|
+
// active, rewrite against isolated state so rejecting DDL cannot itself
|
|
6068
|
+
// leak a metadata or cache side effect.
|
|
6069
|
+
const skippedFunctionNames = this.inTransaction
|
|
6070
|
+
? new Set(this.skippedFunctionNames)
|
|
6071
|
+
: this.skippedFunctionNames;
|
|
6072
|
+
const triggerFunctions = this.inTransaction
|
|
6073
|
+
? this.cloneTriggerFunctions(this.triggerFunctions)
|
|
6074
|
+
: this.triggerFunctions;
|
|
6075
|
+
const fkRegistry = this.inTransaction
|
|
6076
|
+
? this.cloneFkCascadeRegistry()
|
|
6077
|
+
: this.fkRegistry;
|
|
5918
6078
|
const statements = rewriteSQLStatements(sql, {
|
|
5919
|
-
skippedFunctionNames
|
|
5920
|
-
triggerFunctions
|
|
6079
|
+
skippedFunctionNames,
|
|
6080
|
+
triggerFunctions,
|
|
5921
6081
|
arrayParamNumbers,
|
|
5922
6082
|
jsonParamNumbers,
|
|
5923
6083
|
epochMillisParamNumbers,
|
|
5924
|
-
fkRegistry
|
|
6084
|
+
fkRegistry,
|
|
5925
6085
|
});
|
|
6086
|
+
this.rejectTransactionalDDL(statements);
|
|
5926
6087
|
if (this.canCacheRewrite(statements)) {
|
|
5927
6088
|
this.rememberRewrite(key, statements);
|
|
5928
6089
|
}
|
|
@@ -5931,6 +6092,22 @@ export class DoBackend {
|
|
|
5931
6092
|
}
|
|
5932
6093
|
return statements;
|
|
5933
6094
|
}
|
|
6095
|
+
cloneFkCascadeRegistry() {
|
|
6096
|
+
const cloned = new FkCascadeRegistry();
|
|
6097
|
+
for (const [parent, child] of this.fkRegistry.entries()) {
|
|
6098
|
+
cloned.add(parent, {
|
|
6099
|
+
...child,
|
|
6100
|
+
columns: [...child.columns],
|
|
6101
|
+
refColumns: [...child.refColumns],
|
|
6102
|
+
});
|
|
6103
|
+
}
|
|
6104
|
+
return cloned;
|
|
6105
|
+
}
|
|
6106
|
+
rejectTransactionalDDL(statements) {
|
|
6107
|
+
if (this.inTransaction && statements.some((statement) => statement.isDDL)) {
|
|
6108
|
+
throw new Error('DDL statements are not supported inside emulated transactions');
|
|
6109
|
+
}
|
|
6110
|
+
}
|
|
5934
6111
|
canCacheRewrite(statements) {
|
|
5935
6112
|
return (statements.length > 0 &&
|
|
5936
6113
|
statements.every((statement) => statement.sql.trim() &&
|
|
@@ -6002,6 +6179,14 @@ export class DoBackend {
|
|
|
6002
6179
|
async doExecResult(sql, params, track) {
|
|
6003
6180
|
if (!sql.trim())
|
|
6004
6181
|
return { rows: [], columns: [] };
|
|
6182
|
+
if (this.dbName === 'postgres' &&
|
|
6183
|
+
(track ||
|
|
6184
|
+
(this.ready &&
|
|
6185
|
+
isSqlRowMutation(sql) &&
|
|
6186
|
+
(this.cdcRegistrationDirty ||
|
|
6187
|
+
!hasCdcRegistrationSync(this.cdcRegistrationKey()))))) {
|
|
6188
|
+
await this.ensureCdcRegistration();
|
|
6189
|
+
}
|
|
6005
6190
|
const execSQL = sql;
|
|
6006
6191
|
let lastErr;
|
|
6007
6192
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
@@ -6010,6 +6195,9 @@ export class DoBackend {
|
|
|
6010
6195
|
sql: execSQL,
|
|
6011
6196
|
...(params?.length ? { params } : null),
|
|
6012
6197
|
...(track ? { track } : null),
|
|
6198
|
+
...(this.inTransaction
|
|
6199
|
+
? { transactionID: this.currentTransactionID() }
|
|
6200
|
+
: null),
|
|
6013
6201
|
}), { 'Content-Type': 'application/json' });
|
|
6014
6202
|
const result = JSON.parse(resp);
|
|
6015
6203
|
const rows = (result.rows ?? result ?? []);
|
|
@@ -6025,9 +6213,18 @@ export class DoBackend {
|
|
|
6025
6213
|
if (counted) {
|
|
6026
6214
|
return { ...counted, affectedRows: rows.length };
|
|
6027
6215
|
}
|
|
6028
|
-
|
|
6216
|
+
const hasCaptureCount = Object.hasOwn(result, 'capturedChanges');
|
|
6217
|
+
const capturedChanges = Number(result.capturedChanges ?? 0);
|
|
6218
|
+
if (capturedChanges > 0 ||
|
|
6219
|
+
(track && track.publish !== false && !hasCaptureCount)) {
|
|
6029
6220
|
this.signalTrackedWrite();
|
|
6030
|
-
|
|
6221
|
+
}
|
|
6222
|
+
return {
|
|
6223
|
+
rows,
|
|
6224
|
+
columns,
|
|
6225
|
+
affectedRows: result.affectedRows,
|
|
6226
|
+
capturedChanges,
|
|
6227
|
+
};
|
|
6031
6228
|
}
|
|
6032
6229
|
catch (err) {
|
|
6033
6230
|
lastErr = err;
|
|
@@ -6077,12 +6274,34 @@ export class DoBackend {
|
|
|
6077
6274
|
await this.doRawBatch(statements);
|
|
6078
6275
|
this.txDataSnapshots.set(table, snapshot);
|
|
6079
6276
|
}
|
|
6277
|
+
async journalTransactionTable(table) {
|
|
6278
|
+
if (!this.inTransaction || this.txDataSnapshots.has(table))
|
|
6279
|
+
return;
|
|
6280
|
+
const txID = this.currentTransactionID();
|
|
6281
|
+
// CDC stores full before/after row images for tracked writes. A manifest
|
|
6282
|
+
// row with an empty snapshot marks the transaction as recoverable without
|
|
6283
|
+
// copying the entire table on every short application transaction.
|
|
6284
|
+
await this.doRawBatch([
|
|
6285
|
+
{ sql: TX_MANIFEST_DDL },
|
|
6286
|
+
{
|
|
6287
|
+
sql: `INSERT INTO "${TX_MANIFEST_TABLE}" (tx_id, owner, original, snapshot) VALUES (?, ?, ?, ?)`,
|
|
6288
|
+
params: [txID, this.txOwner, table, ''],
|
|
6289
|
+
},
|
|
6290
|
+
]);
|
|
6291
|
+
this.txDataSnapshots.set(table, '');
|
|
6292
|
+
}
|
|
6080
6293
|
async snapshotTransactionWrite(statement) {
|
|
6081
6294
|
if (!this.inTransaction || !statement.isWrite)
|
|
6082
6295
|
return;
|
|
6083
6296
|
const table = statement.writeTable?.table;
|
|
6084
|
-
if (table)
|
|
6085
|
-
|
|
6297
|
+
if (!table)
|
|
6298
|
+
return;
|
|
6299
|
+
if (this.trackingForStatement(statement)) {
|
|
6300
|
+
await this.ensureCdcRegistration();
|
|
6301
|
+
await this.journalTransactionTable(table);
|
|
6302
|
+
return;
|
|
6303
|
+
}
|
|
6304
|
+
await this.snapshotTransactionTable(table);
|
|
6086
6305
|
}
|
|
6087
6306
|
async doRawBatch(statements) {
|
|
6088
6307
|
const sqls = statements.filter((statement) => (typeof statement === 'string' ? statement : statement.sql).trim());
|
|
@@ -6126,12 +6345,15 @@ export class DoBackend {
|
|
|
6126
6345
|
return false;
|
|
6127
6346
|
}
|
|
6128
6347
|
async executeRewrittenStatement(statement) {
|
|
6348
|
+
this.rejectTransactionalDDL([statement]);
|
|
6129
6349
|
if (!statement.sql.trim())
|
|
6130
6350
|
return { rows: [], columns: [] };
|
|
6131
6351
|
if (await this.shouldSkipStatement(statement))
|
|
6132
6352
|
return { rows: [], columns: [] };
|
|
6133
|
-
if (statement.isDDL)
|
|
6353
|
+
if (statement.isDDL) {
|
|
6134
6354
|
this.publicationTableInfoCache = null;
|
|
6355
|
+
this.invalidateCdcRegistration();
|
|
6356
|
+
}
|
|
6135
6357
|
await this.snapshotTransactionWrite(statement);
|
|
6136
6358
|
if (statement.cascadeStatements?.length) {
|
|
6137
6359
|
await this.runCascadeStatements(statement.cascadeStatements, {});
|
|
@@ -6170,11 +6392,30 @@ export class DoBackend {
|
|
|
6170
6392
|
if (sqls.length === 0)
|
|
6171
6393
|
return;
|
|
6172
6394
|
const hasTrackedWrite = sqls.some((statement) => typeof statement !== 'string' && Boolean(statement.track));
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6395
|
+
const hasPublishedTrackedWrite = sqls.some((statement) => typeof statement !== 'string' &&
|
|
6396
|
+
Boolean(statement.track) &&
|
|
6397
|
+
statement.track?.publish !== false);
|
|
6398
|
+
const hasRowWrite = sqls.some((statement) => isSqlRowMutation(typeof statement === 'string' ? statement : statement.sql));
|
|
6399
|
+
if (this.dbName === 'postgres' &&
|
|
6400
|
+
(hasTrackedWrite ||
|
|
6401
|
+
(hasRowWrite &&
|
|
6402
|
+
(this.cdcRegistrationDirty ||
|
|
6403
|
+
!hasCdcRegistrationSync(this.cdcRegistrationKey()))))) {
|
|
6404
|
+
await this.ensureCdcRegistration();
|
|
6405
|
+
}
|
|
6406
|
+
const response = await this.httpClient.post(this.url('/batch'), JSON.stringify({ statements: sqls }), { 'Content-Type': 'application/json' });
|
|
6407
|
+
let capturedChanges;
|
|
6408
|
+
try {
|
|
6409
|
+
const parsed = JSON.parse(response);
|
|
6410
|
+
if (Object.hasOwn(parsed, 'capturedChanges')) {
|
|
6411
|
+
capturedChanges = Number(parsed.capturedChanges ?? 0);
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
6414
|
+
catch { }
|
|
6415
|
+
if ((capturedChanges ?? 0) > 0 ||
|
|
6416
|
+
(hasPublishedTrackedWrite && capturedChanges === undefined)) {
|
|
6177
6417
|
this.signalTrackedWrite();
|
|
6418
|
+
}
|
|
6178
6419
|
sqls = [];
|
|
6179
6420
|
};
|
|
6180
6421
|
for (const statement of statements) {
|
|
@@ -6183,8 +6424,10 @@ export class DoBackend {
|
|
|
6183
6424
|
: statement;
|
|
6184
6425
|
if (!item.sql.trim())
|
|
6185
6426
|
continue;
|
|
6186
|
-
if (item.isDDL)
|
|
6427
|
+
if (item.isDDL) {
|
|
6187
6428
|
this.publicationTableInfoCache = null;
|
|
6429
|
+
this.invalidateCdcRegistration();
|
|
6430
|
+
}
|
|
6188
6431
|
if (item.skipIfColumnExists || item.skipIfColumnMissing || item.skipIfTableEmpty) {
|
|
6189
6432
|
await flush();
|
|
6190
6433
|
if (item.skipIfColumnExists &&
|
|
@@ -6202,15 +6445,22 @@ export class DoBackend {
|
|
|
6202
6445
|
}
|
|
6203
6446
|
const tracking = this.trackingForStatement(item);
|
|
6204
6447
|
const exec = await this.materializePublishedSchemaFunctions(tracking?.returningSQL ?? item.sql, item);
|
|
6205
|
-
|
|
6448
|
+
const transactionID = this.inTransaction ? this.currentTransactionID() : undefined;
|
|
6449
|
+
const request = tracking
|
|
6206
6450
|
? {
|
|
6207
6451
|
sql: exec.sql,
|
|
6208
6452
|
...(exec.params.length ? { params: exec.params } : null),
|
|
6209
6453
|
track: this.trackingRequest(tracking),
|
|
6454
|
+
...(transactionID ? { transactionID } : null),
|
|
6210
6455
|
}
|
|
6211
|
-
: exec.params.length
|
|
6212
|
-
? {
|
|
6213
|
-
|
|
6456
|
+
: exec.params.length || transactionID
|
|
6457
|
+
? {
|
|
6458
|
+
sql: exec.sql,
|
|
6459
|
+
...(exec.params.length ? { params: exec.params } : null),
|
|
6460
|
+
...(transactionID ? { transactionID } : null),
|
|
6461
|
+
}
|
|
6462
|
+
: exec.sql;
|
|
6463
|
+
sqls.push(request);
|
|
6214
6464
|
}
|
|
6215
6465
|
await flush();
|
|
6216
6466
|
}
|
|
@@ -6224,12 +6474,16 @@ export class DoBackend {
|
|
|
6224
6474
|
}
|
|
6225
6475
|
async listSqliteTables() {
|
|
6226
6476
|
const result = await this.doExecResult("SELECT name, sql FROM sqlite_master WHERE type = 'table' ORDER BY name");
|
|
6227
|
-
return result.rows
|
|
6477
|
+
return (result.rows
|
|
6228
6478
|
.map((row) => ({
|
|
6229
6479
|
name: String(row.name ?? ''),
|
|
6230
6480
|
sql: row.sql === null || row.sql === undefined ? null : String(row.sql),
|
|
6231
6481
|
}))
|
|
6232
|
-
|
|
6482
|
+
// Cloudflare owns _cf_* tables and rejects even PRAGMA introspection.
|
|
6483
|
+
// They are storage implementation details, never PostgreSQL relations.
|
|
6484
|
+
.filter((table) => table.name &&
|
|
6485
|
+
!table.name.toLowerCase().startsWith('_cf_') &&
|
|
6486
|
+
!table.name.toLowerCase().startsWith('sqlite_')));
|
|
6233
6487
|
}
|
|
6234
6488
|
tableRefForSqliteTable(name) {
|
|
6235
6489
|
const metadata = this.schemaMetadata.get(name)?.values().next().value;
|