orez 0.4.29 → 0.4.31

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.
Files changed (49) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.d.ts +5 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +8 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/config.d.ts +4 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +5 -3
  9. package/dist/config.js.map +1 -1
  10. package/dist/fk-cascade.d.ts +102 -0
  11. package/dist/fk-cascade.d.ts.map +1 -0
  12. package/dist/fk-cascade.js +233 -0
  13. package/dist/fk-cascade.js.map +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +101 -49
  16. package/dist/index.js.map +1 -1
  17. package/dist/pg-proxy-do-backend.d.ts +1 -0
  18. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  19. package/dist/pg-proxy-do-backend.js +114 -0
  20. package/dist/pg-proxy-do-backend.js.map +1 -1
  21. package/dist/pglite-manager.d.ts +12 -9
  22. package/dist/pglite-manager.d.ts.map +1 -1
  23. package/dist/pglite-manager.js +61 -31
  24. package/dist/pglite-manager.js.map +1 -1
  25. package/dist/recovery.d.ts +10 -12
  26. package/dist/recovery.d.ts.map +1 -1
  27. package/dist/recovery.js +24 -13
  28. package/dist/recovery.js.map +1 -1
  29. package/dist/replication/change-tracker.d.ts.map +1 -1
  30. package/dist/replication/change-tracker.js +36 -5
  31. package/dist/replication/change-tracker.js.map +1 -1
  32. package/dist/replication/handler.d.ts.map +1 -1
  33. package/dist/replication/handler.js +20 -17
  34. package/dist/replication/handler.js.map +1 -1
  35. package/dist/worker/cf-patches.d.ts +4 -2
  36. package/dist/worker/cf-patches.d.ts.map +1 -1
  37. package/dist/worker/cf-patches.js +15 -2
  38. package/dist/worker/cf-patches.js.map +1 -1
  39. package/dist/worker/shims/postgres.d.ts.map +1 -1
  40. package/dist/worker/shims/postgres.js +112 -0
  41. package/dist/worker/shims/postgres.js.map +1 -1
  42. package/dist/worker/zero-cache-embed.d.ts.map +1 -1
  43. package/dist/worker/zero-cache-embed.js +4 -0
  44. package/dist/worker/zero-cache-embed.js.map +1 -1
  45. package/dist/zero-changelog-cleanup-patch.d.ts +18 -0
  46. package/dist/zero-changelog-cleanup-patch.d.ts.map +1 -0
  47. package/dist/zero-changelog-cleanup-patch.js +63 -0
  48. package/dist/zero-changelog-cleanup-patch.js.map +1 -0
  49. package/package.json +2 -2
@@ -2,6 +2,7 @@
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
4
  import { RETURNING_INTERNAL_PREFIX } from './do-sql-tracking.js';
5
+ import { expandDelete, FkCascadeRegistry, recordAlterTableForeignKeys, recordCreateTableForeignKeys, } from './fk-cascade.js';
5
6
  import { Mutex } from './mutex.js';
6
7
  import { signalReplicationChange } from './replication/handler.js';
7
8
  import { markSQLiteKeywordIdentifiers, restoreSQLiteKeywordIdentifierMarkers, } from './sqlite-keyword-identifiers.js';
@@ -939,6 +940,14 @@ function flattenSchemaName(schema, name) {
939
940
  return `_zero_${name}`;
940
941
  return `${schema}_${name}`;
941
942
  }
943
+ // canonical FK-registry key for a table: schema-qualified PG name (un-flattened),
944
+ // quoted. used for BOTH capture (CREATE TABLE child + parent) and lookup (DELETE
945
+ // target), so they always agree. expansion emits PG SQL under these names and
946
+ // each child re-enters rewriteParsedStatement, which flattens + tracks it exactly
947
+ // like a normal delete — so cascade tracking is identical to hand-written deletes.
948
+ function fkTableKey(ref) {
949
+ return `${quoteIdentifier(ref.schemaname ?? 'public')}.${quoteIdentifier(ref.relname)}`;
950
+ }
942
951
  function flattenRangeVar(rangeVar) {
943
952
  if (!rangeVar?.schemaname)
944
953
  return rangeVar?.relname;
@@ -3085,6 +3094,28 @@ function deparseExpressionSQL(version, expr) {
3085
3094
  return null;
3086
3095
  return sql.slice(selectIndex + 'SELECT'.length).trim();
3087
3096
  }
3097
+ // expand a DELETE on `target` into its cascade child statements (leaves-first).
3098
+ // expansion emits PG SQL under un-flattened names, then each child re-enters
3099
+ // rewriteParsedStatement (suppressFkCascade so it doesn't re-expand) — so each
3100
+ // returns a normal RewrittenStatement, flattened + change-tracked identically to
3101
+ // a hand-written delete. `whereClause` is the parent's already-normalized clause
3102
+ // (deparsed back to SQL, $N params intact); its $N bind to the parent's params
3103
+ // at execute time. unconditional deletes (no WHERE) cascade every child row.
3104
+ function buildCascadeStatements(version, target, whereClause, context) {
3105
+ const whereSql = whereClause ? deparseExpressionSQL(version, whereClause) : null;
3106
+ const childContext = { ...context, suppressFkCascade: true };
3107
+ const out = [];
3108
+ for (const childSql of expandDelete(target, whereSql, context.fkRegistry)) {
3109
+ for (const raw of parseSync(childSql).stmts) {
3110
+ const rewritten = rewriteParsedStatement(version, raw, childContext);
3111
+ if (Array.isArray(rewritten))
3112
+ out.push(...rewritten.filter((s) => !!s));
3113
+ else if (rewritten)
3114
+ out.push(rewritten);
3115
+ }
3116
+ }
3117
+ return out;
3118
+ }
3088
3119
  function parseExpressionTarget(source) {
3089
3120
  try {
3090
3121
  const parsed = parseSync(`SELECT ${source}`);
@@ -3439,7 +3470,14 @@ function rewriteParsedStatement(version, rawStmt, context) {
3439
3470
  let changeTracking;
3440
3471
  let writeTable = null;
3441
3472
  let serialTriggers = [];
3473
+ let cascadeStatements;
3474
+ let fkEdgesAdded = false;
3442
3475
  if (nodeType === 'AlterTableStmt') {
3476
+ // capture FK cascade/set-null edges before normalizeAlterTable drops the
3477
+ // ADD CONSTRAINT — drizzle emits FKs as a separate ALTER, not inline.
3478
+ if (context?.fkRegistry && !context.suppressFkCascade) {
3479
+ fkEdgesAdded = recordAlterTableForeignKeys(node, context.fkRegistry, fkTableKey) > 0;
3480
+ }
3443
3481
  alterMetadata = normalizeAlterTable(node);
3444
3482
  if (!node.cmds?.length) {
3445
3483
  const statements = [];
@@ -3456,6 +3494,12 @@ function rewriteParsedStatement(version, rawStmt, context) {
3456
3494
  }
3457
3495
  else if (nodeType === 'CreateStmt') {
3458
3496
  schemaColumns = schemaColumnsForCreateTable(node);
3497
+ // capture FK cascade/set-null edges BEFORE normalizeCreateTable drops the
3498
+ // CONSTR_FOREIGN nodes. flattened keys (fkTableKey) match the DELETE lookup.
3499
+ if (context?.fkRegistry) {
3500
+ fkEdgesAdded =
3501
+ recordCreateTableForeignKeys(node, context.fkRegistry, fkTableKey) > 0;
3502
+ }
3459
3503
  // capture serial columns before normalizeCreateTable rewrites the type to integer
3460
3504
  const serialCols = serialColumnNames(node);
3461
3505
  normalizeCreateTable(node);
@@ -3490,6 +3534,15 @@ function rewriteParsedStatement(version, rawStmt, context) {
3490
3534
  else if (nodeType === 'DeleteStmt') {
3491
3535
  const table = publicationTableRefForRangeVar(node.relation);
3492
3536
  writeTable = table;
3537
+ // build the cascade from the ORIGINAL delete (before normalizeDelete mutates
3538
+ // node), so each child re-enters the rewrite and is flattened + translated
3539
+ // uniformly. suppressFkCascade guards against re-expanding the children.
3540
+ if (context?.fkRegistry?.hasEdges && !context.suppressFkCascade && node.relation) {
3541
+ const cascadeTarget = fkTableKey(node.relation);
3542
+ if (context.fkRegistry.childrenOf(cascadeTarget).length) {
3543
+ cascadeStatements = buildCascadeStatements(version, cascadeTarget, node.whereClause, context);
3544
+ }
3545
+ }
3493
3546
  skipIfTableEmpty = normalizeDelete(node, context);
3494
3547
  changeTracking = changeTrackingForDML(version, stmt, nodeType, table, 'DELETE');
3495
3548
  }
@@ -3590,6 +3643,8 @@ function rewriteParsedStatement(version, rawStmt, context) {
3590
3643
  ...(skipIfColumnExists ? { skipIfColumnExists } : null),
3591
3644
  ...(skipIfColumnMissing ? { skipIfColumnMissing } : null),
3592
3645
  ...(skipIfTableEmpty ? { skipIfTableEmpty } : null),
3646
+ ...(cascadeStatements?.length ? { cascadeStatements } : null),
3647
+ ...(fkEdgesAdded ? { fkEdges: true } : null),
3593
3648
  };
3594
3649
  return serialTriggers.length ? [mainStatement, ...serialTriggers] : mainStatement;
3595
3650
  }
@@ -4735,6 +4790,7 @@ export class DoBackend {
4735
4790
  this.schemaMetadata = new Map();
4736
4791
  this.publications = new Map();
4737
4792
  this.rewriteCache = new Map();
4793
+ this.fkRegistry = new FkCascadeRegistry();
4738
4794
  }
4739
4795
  get waitReady() {
4740
4796
  return this.ensureReady();
@@ -4827,6 +4883,9 @@ export class DoBackend {
4827
4883
  if (publication)
4828
4884
  this.publications.set(key, publication);
4829
4885
  }
4886
+ else if (kind === 'fk_edge') {
4887
+ this.fkRegistry.add(key, JSON.parse(value));
4888
+ }
4830
4889
  }
4831
4890
  if (await this.repairShardMetadataPublications()) {
4832
4891
  await this.persistDurableMetadata();
@@ -4904,6 +4963,16 @@ export class DoBackend {
4904
4963
  for (const [name, publication] of this.publications) {
4905
4964
  rows.push(['publication', name, '', this.publicationToJSON(publication)]);
4906
4965
  }
4966
+ // FK cascade edges — durable so the registry survives DO eviction (the
4967
+ // app tables persist and CREATE TABLE never re-runs to repopulate it).
4968
+ for (const [parentKey, child] of this.fkRegistry.entries()) {
4969
+ rows.push([
4970
+ 'fk_edge',
4971
+ parentKey,
4972
+ `${child.table}|${child.columns.join(',')}`,
4973
+ JSON.stringify(child),
4974
+ ]);
4975
+ }
4907
4976
  if (rows.length === 0)
4908
4977
  return;
4909
4978
  // single multi-row INSERT OR REPLACE per chunk. previously this was one
@@ -5251,6 +5320,13 @@ export class DoBackend {
5251
5320
  this.applyPublicationChange(change);
5252
5321
  changed = true;
5253
5322
  }
5323
+ if (statement.fkEdges) {
5324
+ // edges were already added to this.fkRegistry at rewrite time; persist
5325
+ // them and drop cached rewrites so DELETEs issued before this CREATE
5326
+ // TABLE now expand their cascade.
5327
+ changed = true;
5328
+ this.rewriteCache.clear();
5329
+ }
5254
5330
  }
5255
5331
  if (changed) {
5256
5332
  this.publicationTableInfoCache = null;
@@ -5536,6 +5612,9 @@ export class DoBackend {
5536
5612
  const tracking = statement ? this.trackingForStatement(statement) : undefined;
5537
5613
  const bound = this.sqliteBoundSQL(tracking?.returningSQL ?? sql, portal.params, portal.arrayParamNumbers, portal.jsonParamNumbers, portal.timestampParamNumbers, portal.epochMillisParamNumbers, portal.booleanParamNumbers);
5538
5614
  const exec = await this.materializePublishedSchemaFunctions(bound.sql, statement, bound.params);
5615
+ if (statement?.cascadeStatements?.length) {
5616
+ await this.runCascadeStatements(statement.cascadeStatements, portal);
5617
+ }
5539
5618
  const result = await this.doExecResult(exec.sql, exec.params, tracking ? this.trackingRequest(tracking) : undefined);
5540
5619
  if (portal.schemaColumns?.length) {
5541
5620
  await this.applyStatementMetadata([{ sql, schemaColumns: portal.schemaColumns }]);
@@ -5628,6 +5707,9 @@ export class DoBackend {
5628
5707
  }
5629
5708
  if (statement)
5630
5709
  await this.snapshotTransactionWrite(statement);
5710
+ if (statement?.cascadeStatements?.length) {
5711
+ await this.runCascadeStatements(statement.cascadeStatements, {});
5712
+ }
5631
5713
  const tracking = statement ? this.trackingForStatement(statement) : undefined;
5632
5714
  const result = await this.doExecResult(tracking?.returningSQL ?? rewritten, undefined, tracking ? this.trackingRequest(tracking) : undefined);
5633
5715
  await this.applyStatementMetadata(statements);
@@ -5705,6 +5787,16 @@ export class DoBackend {
5705
5787
  ? this.sqliteBoundSQL(tracking.returningSQL, params, arrayParamNumbers, jsonParamNumbers, timestampParamNumbers, epochMillisParamNumbers, booleanParamNumbers)
5706
5788
  : bound;
5707
5789
  const exec = await this.materializePublishedSchemaFunctions(execBound.sql, statement, execBound.params);
5790
+ if (statement?.cascadeStatements?.length) {
5791
+ await this.runCascadeStatements(statement.cascadeStatements, {
5792
+ params,
5793
+ arrayParamNumbers,
5794
+ jsonParamNumbers,
5795
+ timestampParamNumbers,
5796
+ epochMillisParamNumbers,
5797
+ booleanParamNumbers,
5798
+ });
5799
+ }
5708
5800
  const result = await this.doExecResult(exec.sql, exec.params, tracking ? this.trackingRequest(tracking) : undefined);
5709
5801
  await this.applyStatementMetadata(statements);
5710
5802
  // metadata for the returned columns must be derived from the ORIGINAL SQL,
@@ -5743,6 +5835,7 @@ export class DoBackend {
5743
5835
  arrayParamNumbers,
5744
5836
  jsonParamNumbers,
5745
5837
  epochMillisParamNumbers,
5838
+ fkRegistry: this.fkRegistry,
5746
5839
  });
5747
5840
  if (this.canCacheRewrite(statements)) {
5748
5841
  this.rememberRewrite(key, statements);
@@ -5781,6 +5874,7 @@ export class DoBackend {
5781
5874
  arrayParamNumbers,
5782
5875
  jsonParamNumbers,
5783
5876
  epochMillisParamNumbers,
5877
+ fkRegistry: this.fkRegistry,
5784
5878
  });
5785
5879
  }
5786
5880
  normalizedHighLevelResult(sql, result) {
@@ -5969,6 +6063,9 @@ export class DoBackend {
5969
6063
  if (statement.isDDL)
5970
6064
  this.publicationTableInfoCache = null;
5971
6065
  await this.snapshotTransactionWrite(statement);
6066
+ if (statement.cascadeStatements?.length) {
6067
+ await this.runCascadeStatements(statement.cascadeStatements, {});
6068
+ }
5972
6069
  const tracking = this.trackingForStatement(statement);
5973
6070
  const exec = await this.materializePublishedSchemaFunctions(tracking?.returningSQL ?? statement.sql, statement);
5974
6071
  return this.doExecResult(exec.sql, exec.params, tracking ? this.trackingRequest(tracking) : undefined);
@@ -5980,6 +6077,23 @@ export class DoBackend {
5980
6077
  }
5981
6078
  return result;
5982
6079
  }
6080
+ // run a parent DELETE's cascade children (leaves-first) as their own bound
6081
+ // execs BEFORE the parent. each child's returningSQL embeds the full parent
6082
+ // WHERE, so the SAME params bind unchanged (sqliteBoundSQL maps $N
6083
+ // positionally); the tracking request captures every deletion so it
6084
+ // replicates like any other write. publication gating matches the parent:
6085
+ // unpublished (e.g. private) child tables cascade in the store but don't
6086
+ // stream — correct, since clients don't see those rows anyway.
6087
+ async runCascadeStatements(cascades, bind) {
6088
+ for (const child of cascades) {
6089
+ if (await this.shouldSkipStatement(child))
6090
+ continue;
6091
+ await this.snapshotTransactionWrite(child);
6092
+ const gated = this.trackingForStatement(child);
6093
+ const bound = this.sqliteBoundSQL(gated?.returningSQL ?? child.sql, bind.params, bind.arrayParamNumbers, bind.jsonParamNumbers, bind.timestampParamNumbers, bind.epochMillisParamNumbers, bind.booleanParamNumbers);
6094
+ await this.doExecResult(bound.sql, bound.params, gated ? this.trackingRequest(gated) : undefined);
6095
+ }
6096
+ }
5983
6097
  async doBatchExec(statements) {
5984
6098
  let sqls = [];
5985
6099
  const flush = async () => {