@supabase/pg-delta 1.0.0-alpha.28 → 1.0.0-alpha.29

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.
@@ -351,9 +351,14 @@ function tryBreakPublicationFkConstraintDropCycle(cycleNodeIndexes, phaseChanges
351
351
  if (!publicationTableIds.has(terminalConstraintDrop.table.stableId)) {
352
352
  return null;
353
353
  }
354
- for (const dropTable of dropTables) {
355
- if (!publicationTableIds.has(dropTable.table.stableId))
356
- return null;
354
+ // At least one dropped table must be a publication member — that's the
355
+ // publication → DropTable edge that pulls the publication change into the
356
+ // cycle (the back-edge is the terminal constraint's table, checked above).
357
+ // Don't require ALL of them: publications like supabase_realtime commonly
358
+ // contain only a subset of tables, so intermediate FK-chain tables may not
359
+ // be members (Sentry SUPABASE-API-7RS / CLI-1605).
360
+ if (!dropTables.some((dropTable) => publicationTableIds.has(dropTable.table.stableId))) {
361
+ return null;
357
362
  }
358
363
  const cycleDropTableIds = new Set(dropTables.map((change) => change.table.stableId));
359
364
  let hasFkToTerminalConstraint = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/pg-delta",
3
- "version": "1.0.0-alpha.28",
3
+ "version": "1.0.0-alpha.29",
4
4
  "description": "PostgreSQL migrations made easy",
5
5
  "keywords": [
6
6
  "diff",
@@ -79,7 +79,7 @@
79
79
  },
80
80
  "dependencies": {
81
81
  "@stricli/core": "^1.2.4",
82
- "@supabase/pg-topo": "^1.0.0-alpha.1",
82
+ "@supabase/pg-topo": "^1.0.0-alpha.2",
83
83
  "@ts-safeql/sql-tag": "^0.2.0",
84
84
  "chalk": "^5.6.2",
85
85
  "debug": "^4.3.7",
@@ -682,6 +682,132 @@ describe("tryBreakCycleByChangeInjection", () => {
682
682
  expect(broken).toContain(terminalDrop);
683
683
  });
684
684
 
685
+ test("publication FK-chain 4-cycle with partial publication membership: injects FK drops", () => {
686
+ // Sentry SUPABASE-API-7RS / CLI-1605. Same shape as the previous test,
687
+ // but the publication only contains the terminal constraint's table
688
+ // (trades) and the first dropped table (public_offering_events) — the
689
+ // intermediate FK-chain table (trade_status_events) was never a member
690
+ // of supabase_realtime. The breaker must not require every dropped
691
+ // table in the cycle to be a publication member; the pub edge only
692
+ // needs one of them.
693
+ //
694
+ // Schema:
695
+ // trades.trade_id UNIQUE (trades_trade_id_key) — table survives
696
+ // trade_status_events.trade_id REFERENCES trades(trade_id)
697
+ // public_offering_events.source_event_id REFERENCES trade_status_events(id)
698
+ // publication supabase_realtime: trades, public_offering_events only
699
+ const tableTrades = new Table({
700
+ ...baseTableProps,
701
+ name: "trades",
702
+ columns: [
703
+ { ...integerColumn("id", 1), not_null: true },
704
+ { ...integerColumn("trade_id", 2), not_null: true },
705
+ ],
706
+ constraints: [uniqueConstraint("trades_trade_id_key", "trade_id")],
707
+ });
708
+ const tableTradeStatusEvents = new Table({
709
+ ...baseTableProps,
710
+ name: "trade_status_events",
711
+ columns: [
712
+ { ...integerColumn("id", 1), not_null: true },
713
+ integerColumn("trade_id", 2),
714
+ ],
715
+ constraints: [
716
+ fkConstraint({
717
+ name: "trade_status_events_trade_id_fkey",
718
+ fkColumn: "trade_id",
719
+ targetSchema: "public",
720
+ targetTable: "trades",
721
+ targetColumn: "trade_id",
722
+ }),
723
+ ],
724
+ });
725
+ const tablePublicOfferingEvents = new Table({
726
+ ...baseTableProps,
727
+ name: "public_offering_events",
728
+ columns: [
729
+ { ...integerColumn("id", 1), not_null: true },
730
+ integerColumn("source_event_id", 2),
731
+ ],
732
+ constraints: [
733
+ fkConstraint({
734
+ name: "public_offering_events_source_event_id_fkey",
735
+ fkColumn: "source_event_id",
736
+ targetSchema: "public",
737
+ targetTable: "trade_status_events",
738
+ }),
739
+ ],
740
+ });
741
+ const publication = new Publication({
742
+ name: "supabase_realtime",
743
+ owner: "postgres",
744
+ comment: null,
745
+ all_tables: false,
746
+ publish_insert: true,
747
+ publish_update: true,
748
+ publish_delete: true,
749
+ publish_truncate: true,
750
+ publish_via_partition_root: false,
751
+ tables: [
752
+ {
753
+ schema: "public",
754
+ name: "public_offering_events",
755
+ columns: null,
756
+ row_filter: null,
757
+ },
758
+ { schema: "public", name: "trades", columns: null, row_filter: null },
759
+ ],
760
+ schemas: [],
761
+ });
762
+
763
+ const terminalDrop = new AlterTableDropConstraint({
764
+ table: tableTrades,
765
+ constraint: tableTrades.constraints[0],
766
+ });
767
+ const changes: Change[] = [
768
+ new AlterPublicationDropTables({
769
+ publication,
770
+ tables: publication.tables,
771
+ }),
772
+ new DropTable({ table: tablePublicOfferingEvents }),
773
+ new DropTable({ table: tableTradeStatusEvents }),
774
+ terminalDrop,
775
+ ];
776
+
777
+ const broken = tryBreakCycleByChangeInjection([0, 1, 2, 3], changes);
778
+ if (broken === null) throw new Error("expected breaker to fire");
779
+
780
+ const injectedDropNames = broken
781
+ .filter(
782
+ (change): change is AlterTableDropConstraint =>
783
+ change instanceof AlterTableDropConstraint && change !== terminalDrop,
784
+ )
785
+ .map((change) => change.constraint.name)
786
+ .sort();
787
+ expect(injectedDropNames).toEqual([
788
+ "public_offering_events_source_event_id_fkey",
789
+ "trade_status_events_trade_id_fkey",
790
+ ]);
791
+
792
+ for (const [tableId, constraintName] of [
793
+ [
794
+ tablePublicOfferingEvents.stableId,
795
+ "public_offering_events_source_event_id_fkey",
796
+ ],
797
+ [tableTradeStatusEvents.stableId, "trade_status_events_trade_id_fkey"],
798
+ ] as const) {
799
+ const rewrittenDrop = broken.find(
800
+ (change): change is DropTable =>
801
+ change instanceof DropTable && change.table.stableId === tableId,
802
+ );
803
+ if (!rewrittenDrop) throw new Error(`missing DropTable for ${tableId}`);
804
+ expect(
805
+ rewrittenDrop.externallyDroppedConstraints.has(constraintName),
806
+ ).toBe(true);
807
+ }
808
+ expect(broken).toContain(terminalDrop);
809
+ });
810
+
685
811
  test("returns null for a cycle with no recognised pattern (e.g. publication-only)", () => {
686
812
  // Cycle of `AlterPublicationSetOwner` changes — neither FK nor
687
813
  // publication-column shape. Breaker must bail so the formatted
@@ -418,8 +418,18 @@ function tryBreakPublicationFkConstraintDropCycle(
418
418
  return null;
419
419
  }
420
420
 
421
- for (const dropTable of dropTables) {
422
- if (!publicationTableIds.has(dropTable.table.stableId)) return null;
421
+ // At least one dropped table must be a publication member — that's the
422
+ // publication DropTable edge that pulls the publication change into the
423
+ // cycle (the back-edge is the terminal constraint's table, checked above).
424
+ // Don't require ALL of them: publications like supabase_realtime commonly
425
+ // contain only a subset of tables, so intermediate FK-chain tables may not
426
+ // be members (Sentry SUPABASE-API-7RS / CLI-1605).
427
+ if (
428
+ !dropTables.some((dropTable) =>
429
+ publicationTableIds.has(dropTable.table.stableId),
430
+ )
431
+ ) {
432
+ return null;
423
433
  }
424
434
 
425
435
  const cycleDropTableIds = new Set(