Data Model: Placement

On this page

Overview

The placement service manages foster homes, child placements, kinship evaluations, and sibling group tracking. Foster home occupancy is derived automatically by a database trigger (#761): a home’s current_occupancy always equals the count of its placements in active status, recomputed on every placement insert/delete and on any update that changes foster_home_id or status (#1161 — an update changing neither is a provable no-op for the count, so the WHEN guard skips the recompute and its home-row lock instead of serializing unrelated placement writes behind open same-home transactions). It therefore stays correct across all transitions — create, planned → active, and end — and cannot drift. The trigger locks the home row (PERFORM …​ FOR UPDATE) in its own statement before the recount (#1117): under READ COMMITTED, a recompute UPDATE that instead blocked on the home row directly would resume via EvalPlanQual with its original statement snapshot — writing a COUNT that predates the very commit it waited for (the #1056 stale-clobber forensics: a third transaction re-wrote occupancy 2 after two endings had committed). Lock-first forces the wait before the counting statement begins, so the recount’s snapshot always postdates the peer’s commit. Any future COUNT-maintaining trigger must follow this lock-first shape (deterministic pin: tests/constraints/occupancy_recompute_race.rs). Activation (create-active or planned → active) is capacity-guarded under a SELECT …​ FOR UPDATE lock. As a DB-level backstop (#861), CHECK (current_occupancy >= 0 AND current_occupancy ⇐ max_capacity) and CHECK (max_capacity >= 0) reject any write — from any present or future writer — that would drive occupancy negative or past licensed capacity.

Cross-service references to cases.cases(id) and cases.persons(id) are stored as plain UUIDs — no foreign key constraints across databases.

The fleet-standard platform tables also live in this schema and are documented centrally: event_outbox / event_inbox / event_inbox_collisions (ADR-022 and the ADR-062 §A attempt machine), request_claims (ADR-062 §B), and schema_compat_floor (ADR-063).

Tables

Table Purpose

foster_homes

Licensed foster care providers with capacity, age range, and ICWA compliance tracking

foster_home_training

Training records for foster parents (pre-service, CPR, trauma-informed, etc.)

placements

Child placement records with AFCARS-required fields (removal, CTW, reasonable efforts)

kinship_options

Relative/kinship placements evaluated during the matching process

sibling_placements

Tracks whether siblings are placed together or separated

education_records

Educational enrollment and IEP/Section 504 tracking per child (Phase 4 expansion)

health_records

Health visits, immunizations, diagnoses, and overdue-appointment tracking per child (Phase 4 expansion)

home_documents

Uploaded documents (licenses, home-study reports, background checks) per foster home, stored in object storage via craig-store

upload_attempts

ADR-062 §U upload-attempt tombstones (SU2 #1248): one immutable row per client upload operation, id = the client-held request id; generation-fenced CAS state machine + replay decision table live in craig-store::attempts (shared crates); kinds here: home_document. 31 d terminal retention (reconciler-pruned); no FK — target_id is polymorphic by design.

case_assignments

#1213: the event-projected authoritative case assignment (case_id → (assigned_worker_sub, supervisor_sub, assignment_revision) — the supervisor half landed with #1084), fed by case.created (revision 0 seed) and case.assignment_changed (#1214 strictly-greater upsert; ONE revision orders the whole pair). The create handlers DERIVE assigned_worker_sub from here (FOR SHARE in the claim transaction) instead of trusting the caller’s sub, and the subsidy-eligibility S2S read serves the coherent triple to craig-financial (#1084 creation-time stamp); no FK — case_id is cross-service. As-built: ADR-060 § Amendment #1213. #1367: placements and kinship_options rows carry the same (assigned_worker_sub, supervisor_sub, assignment_revision) denorm triple (event-applied under the strict guard; creation-stamped from this projection’s FOR SHARE read), making the TX i_supervises rows and the AssignedSupervisor list scope real for placement surfaces.

ER Diagram

foster_homesuuididPKtextnametextaddresstextcountytextphonetextlicense_numberUKtextlicense_statuspending|active|suspended|revoked|expiredtextlicense_typefoster|kinship|therapeutic|groupdatelicensed_atdatelicense_expiresintegermax_capacityintegercurrent_occupancyintegeraccepts_ages_minintegeraccepts_ages_maxbooleanaccepts_sibling_groupsbooleanicwa_complianttextprimary_contactbigintversionIf-Match CAS counter (ADR-062 §G2)timestamptzcreated_attimestamptzupdated_atfoster_home_traininguuididPKuuidfoster_home_idFKtexttraining_typepre-service|in-service|CPR|first_aid|trauma_informeddatecompleted_atdateexpires_atfloathourstextverified_byplacementsuuididPKuuidcase_idcross-service FKuuidchild_idcross-service FKuuidfoster_home_idFKnullabletextplacement_typefoster|kinship|therapeutic|residential|trial_home_visit|pre_adoptivetextstatusplanned|active|endedtimestamptzstarted_attimestamptzended_attextend_reasonreunification|adoption|aging_out|transfer|disruptiondateremoval_dateAFCARStextremoval_reasonAFCARSbooleanctw_findingAFCARS: contrary-to-welfaredatectw_finding_dateAFCARSintegerctw_days_from_removalAFCARS: computedbooleanreasonable_effortsAFCARSdatereasonable_efforts_dateAFCARStextpermanency_goalAFCARStextcreated_bytimestamptzcreated_attimestamptzupdated_atkinship_optionsuuididPKuuidcase_idcross-service FKuuidchild_idcross-service FKtextrelative_nametextrelationshipbooleanevaluatedbooleanapprovedtextrejection_reasontimestamptzevaluated_attextrecorded_bysibling_placementsuuididPKuuidsibling_groupshared group IDuuidplacement_idFKbooleantogetherplaced together or separatededucation_recordsuuididPKuuidchild_idcross-service FKtextschool_nametextschool_typepublic|private|charter|homeschooltextgrade_levelbooleaniepbooleansection_504dateenrollment_datedatewithdrawal_datetextrecorded_bytimestamptzcreated_attimestamptzupdated_athealth_recordsuuididPKuuidchild_idcross-service FKtextvisit_typewell_child|sick_visit|dental|mental_health|immunizationtextprovider_namedatevisit_datetextdiagnosistextimmunizationsJSONB listdatenext_due_dateoverdue alert drivertextrecorded_bytimestamptzcreated_attimestamptzupdated_athome_documentsuuididPKuuidfoster_home_idFKtextdocument_typelicense|home_study|background_check|training_certificate|othertextobject_keycraig-store pathtextoriginal_filenamebigintsize_bytestextcontent_typeuuiduploaded_byactor subtextuploaded_by_namenullable; #1251 attempt-substrate rows leave it NULLtimestamptzuploaded_attrainedhostsattached totracked in

Key Indexes

  • idx_placements_case — find placements by case

  • idx_placements_child — find placements by child

  • idx_placements_foster_home — find placements by home

  • idx_placements_status — filter by placement status

  • idx_foster_homes_admin_unit — admin-unit-based home search (county/tribal region)

  • idx_foster_homes_status — license status filtering (matching algorithm)

  • idx_kinship_options_case — kinship options by case

  • idx_education_records_child — educational history by child

  • idx_health_records_child — health visit history by child

  • idx_health_records_next_due — partial index on next_due_date < now() for the overdue-appointments endpoint

  • idx_home_documents_foster_home — documents by foster home

Edit this page · latest