BEGIN; -- Canonical rows are separate from the legacy snapshot. Adoption is an explicit, -- version-checked transaction; installing this schema does not migrate user data. CREATE TABLE canvas_model_projects ( project text NOT NULL, workspace_id uuid NOT NULL, root_id uuid NOT NULL, document_format_version integer NOT NULL DEFAULT 1 CHECK(document_format_version=1), migration_id uuid NOT NULL, source_versions jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY(project,workspace_id,root_id), FOREIGN KEY(project,workspace_id,root_id) REFERENCES materials ON DELETE CASCADE ); CREATE TABLE canvas_model_canvases ( project text NOT NULL, workspace_id uuid NOT NULL, root_id uuid NOT NULL, canvas_id uuid NOT NULL, parent_canvas_id uuid, entry_element_id text, version bigint NOT NULL CHECK(version>0), archived boolean NOT NULL DEFAULT false, PRIMARY KEY(project,workspace_id,canvas_id), UNIQUE(project,canvas_id), UNIQUE(project,workspace_id,root_id,canvas_id), CHECK((parent_canvas_id IS NULL)=(entry_element_id IS NULL)), CHECK(parent_canvas_id IS NOT NULL OR canvas_id=root_id), FOREIGN KEY(project,workspace_id,root_id) REFERENCES canvas_model_projects ON DELETE CASCADE, FOREIGN KEY(project,workspace_id,canvas_id) REFERENCES materials ON DELETE CASCADE, FOREIGN KEY(project,workspace_id,root_id,parent_canvas_id) REFERENCES canvas_model_canvases(project,workspace_id,root_id,canvas_id) DEFERRABLE INITIALLY DEFERRED ); CREATE UNIQUE INDEX canvas_model_active_entry ON canvas_model_canvases(project,workspace_id,entry_element_id) WHERE NOT archived; CREATE TABLE canvas_model_elements ( project text NOT NULL, workspace_id uuid NOT NULL, root_id uuid NOT NULL, element_id text NOT NULL CHECK(element_id ~ '^[A-Za-z0-9_-]{1,128}$'), owner_canvas_id uuid NOT NULL, incarnation bigint NOT NULL CHECK(incarnation>0 AND incarnation<=9007199254740991), deleted_incarnation bigint NOT NULL DEFAULT 0 CHECK(deleted_incarnation>=0 AND deleted_incarnation<=incarnation), record jsonb NOT NULL, deleted boolean NOT NULL DEFAULT false, PRIMARY KEY(project,element_id), UNIQUE(project,workspace_id,element_id), CHECK(jsonb_typeof(record)='object' AND record->>'id'=element_id AND record->>'ownerCanvasId'=owner_canvas_id::text AND (record->>'incarnation')::bigint=incarnation), CHECK(NOT deleted OR deleted_incarnation=incarnation), FOREIGN KEY(project,workspace_id,root_id) REFERENCES canvas_model_projects ON DELETE CASCADE, FOREIGN KEY(project,workspace_id,root_id,owner_canvas_id) REFERENCES canvas_model_canvases(project,workspace_id,root_id,canvas_id) DEFERRABLE INITIALLY DEFERRED ); CREATE INDEX canvas_model_owner ON canvas_model_elements(project,workspace_id,root_id,owner_canvas_id) WHERE NOT deleted; ALTER TABLE canvas_model_canvases ADD CONSTRAINT canvas_model_entry_fk FOREIGN KEY(project,workspace_id,entry_element_id) REFERENCES canvas_model_elements(project,workspace_id,element_id) DEFERRABLE INITIALLY DEFERRED; CREATE TABLE canvas_model_identity_map ( project text NOT NULL, workspace_id uuid NOT NULL, root_id uuid NOT NULL, source_canvas_id uuid NOT NULL, source_structure text NOT NULL CHECK(source_structure IN('node','connection')), source_element_id text NOT NULL, element_id text NOT NULL, PRIMARY KEY(project,workspace_id,root_id,source_canvas_id,source_structure,source_element_id), UNIQUE(project,element_id), FOREIGN KEY(project,workspace_id,root_id) REFERENCES canvas_model_projects ON DELETE CASCADE, FOREIGN KEY(project,workspace_id,element_id) REFERENCES canvas_model_elements(project,workspace_id,element_id) DEFERRABLE INITIALLY DEFERRED ); -- Logical operations cannot acquire a second successful execution under a new request key. CREATE TABLE canvas_model_requests ( project text NOT NULL, workspace_id uuid NOT NULL, account_id uuid NOT NULL, request_id uuid NOT NULL, root_id uuid NOT NULL, action text NOT NULL, protocol_version integer NOT NULL, payload_hash text NOT NULL, response_status integer NOT NULL, response_body jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY(project,workspace_id,account_id,request_id), FOREIGN KEY(project,workspace_id,root_id) REFERENCES materials ON DELETE RESTRICT, FOREIGN KEY(project,account_id) REFERENCES project_accounts ); CREATE TABLE canvas_model_operations ( project text NOT NULL, workspace_id uuid NOT NULL, operation_id uuid NOT NULL, request_id uuid NOT NULL, account_id uuid NOT NULL, PRIMARY KEY(project,workspace_id,operation_id), FOREIGN KEY(project,workspace_id,account_id,request_id) REFERENCES canvas_model_requests DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(project,account_id) REFERENCES project_accounts ); CREATE FUNCTION foundry_guard_canonical_canvas() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF (NEW.canvas_document IS DISTINCT FROM OLD.canvas_document OR NEW.canvas_version IS DISTINCT FROM OLD.canvas_version) AND EXISTS(SELECT 1 FROM canvas_model_canvases c WHERE c.project=NEW.project AND c.workspace_id=NEW.workspace_id AND c.canvas_id=NEW.id) AND current_setting('foundry.canonical_writer',true) IS DISTINCT FROM 'on' THEN RAISE EXCEPTION 'canonical_write_required' USING ERRCODE='55000'; END IF; RETURN NEW; END $$; CREATE TRIGGER canonical_canvas_write_barrier BEFORE UPDATE OF canvas_document,canvas_version ON materials FOR EACH ROW EXECUTE FUNCTION foundry_guard_canonical_canvas(); COMMIT;