feat(db): m:m interest_berths junction + role flags
Introduces the multi-berth interest model from plan §3.1: a junction between interests and berths with three role flags so the same berth can be linked as the primary deal target, an EOI-bundle inclusion, or a "just exploring" link without conflating semantics. - 0028 schema migration creates interest_berths with the unique partial index "≤1 primary per interest", a unique compound on (interest_id, berth_id), and indexes for the public-map "under offer" lookup (where is_specific_interest=true). - Same migration adds desired_length_ft / desired_width_ft / desired_draft_ft to interests for the recommender. - Same migration runs the Phase 2 data migration: every interest with a non-null berth_id gets one junction row marked is_primary=true, is_specific_interest=true, and is_in_eoi_bundle = (eoi_status='signed'). Pre-flight check halts on dangling FKs (§14.3 critical case). - New service src/lib/services/interest-berths.service.ts owns reads + writes of the junction. getPrimaryBerth / getPrimaryBerthsForInterests feed list pages; upsertInterestBerth demotes the prior primary in the same transaction so the unique index is never violated. - interests.berth_id stays in place this commit so existing callers keep working; Phase 2b migrates them onto the helper service and a later migration drops the column. 53 dev rows seeded into the junction; tests still green at 996. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
61
src/lib/db/migrations/0028_interest_berths_junction.sql
Normal file
61
src/lib/db/migrations/0028_interest_berths_junction.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
CREATE TABLE "interest_berths" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"interest_id" text NOT NULL,
|
||||
"berth_id" text NOT NULL,
|
||||
"is_primary" boolean DEFAULT false NOT NULL,
|
||||
"is_specific_interest" boolean DEFAULT true NOT NULL,
|
||||
"is_in_eoi_bundle" boolean DEFAULT false NOT NULL,
|
||||
"eoi_bypass_reason" text,
|
||||
"eoi_bypassed_by" text,
|
||||
"eoi_bypassed_at" timestamp with time zone,
|
||||
"added_by" text,
|
||||
"added_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"notes" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "interests" ADD COLUMN "desired_length_ft" numeric;--> statement-breakpoint
|
||||
ALTER TABLE "interests" ADD COLUMN "desired_width_ft" numeric;--> statement-breakpoint
|
||||
ALTER TABLE "interests" ADD COLUMN "desired_draft_ft" numeric;--> statement-breakpoint
|
||||
ALTER TABLE "interest_berths" ADD CONSTRAINT "interest_berths_interest_id_interests_id_fk" FOREIGN KEY ("interest_id") REFERENCES "public"."interests"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "interest_berths" ADD CONSTRAINT "interest_berths_berth_id_berths_id_fk" FOREIGN KEY ("berth_id") REFERENCES "public"."berths"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "idx_ib_interest_berth" ON "interest_berths" USING btree ("interest_id","berth_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "idx_ib_one_primary" ON "interest_berths" USING btree ("interest_id") WHERE "interest_berths"."is_primary" = true;--> statement-breakpoint
|
||||
CREATE INDEX "idx_ib_berth" ON "interest_berths" USING btree ("berth_id");--> statement-breakpoint
|
||||
CREATE INDEX "idx_ib_specific" ON "interest_berths" USING btree ("berth_id") WHERE "interest_berths"."is_specific_interest" = true;--> statement-breakpoint
|
||||
-- Pre-flight: halt if any interests.berth_id points at a row that no
|
||||
-- longer exists. The new junction's FK is `restrict`, so a dangling
|
||||
-- value would otherwise abort the insert mid-batch with a confusing
|
||||
-- error.
|
||||
DO $$
|
||||
DECLARE
|
||||
orphan_count integer;
|
||||
BEGIN
|
||||
SELECT count(*) INTO orphan_count
|
||||
FROM interests i
|
||||
LEFT JOIN berths b ON b.id = i.berth_id
|
||||
WHERE i.berth_id IS NOT NULL
|
||||
AND b.id IS NULL;
|
||||
IF orphan_count > 0 THEN
|
||||
RAISE EXCEPTION 'interests.berth_id has % dangling references; resolve manually before re-running', orphan_count;
|
||||
END IF;
|
||||
END $$;--> statement-breakpoint
|
||||
-- Migrate existing interest.berth_id values into the junction. Every
|
||||
-- pre-existing single-berth link becomes a primary, specific-interest
|
||||
-- row. is_in_eoi_bundle = true only when the interest already has a
|
||||
-- signed EOI (the legacy "the berth is contractually committed" case).
|
||||
INSERT INTO interest_berths (
|
||||
id, interest_id, berth_id,
|
||||
is_primary, is_specific_interest, is_in_eoi_bundle,
|
||||
added_at
|
||||
)
|
||||
SELECT
|
||||
gen_random_uuid()::text,
|
||||
i.id,
|
||||
i.berth_id,
|
||||
true,
|
||||
true,
|
||||
COALESCE(i.eoi_status = 'signed', false),
|
||||
i.created_at
|
||||
FROM interests i
|
||||
WHERE i.berth_id IS NOT NULL
|
||||
ON CONFLICT (interest_id, berth_id) DO NOTHING;
|
||||
10892
src/lib/db/migrations/meta/0028_snapshot.json
Normal file
10892
src/lib/db/migrations/meta/0028_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -197,6 +197,13 @@
|
||||
"when": 1777939914252,
|
||||
"tag": "0027_backfill_nationality_iso_from_phone",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 28,
|
||||
"version": "7",
|
||||
"when": 1777940421236,
|
||||
"tag": "0028_interest_berths_junction",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user