Stage 4 · L17
Implement CRUD, PATCH omission and stable domain errors
Core · original Session 4
Use the starter for this stage's focused examples. The cumulative transfer and solution belong at the stage-end capstone. Baseline checks pass; transfer checks initially fail. Downloads contain the matching native starter and solution for this stage.
CRUD names four families of stored operations: create, read, update and delete. Stage 03 already proved create/read; this stage adds deletion and a narrow PATCH without changing their public contracts. HTTP handlers translate inputs and status/headers; services decide operation rules and transaction success; repository functions choose database queries.
PATCH means change supplied fields, not replace every field. EquipmentPatch defaults name and daily_rate to None so omission is accepted, but model_dump(exclude_unset=True) tells the service which fields actually appeared. A name-only patch keeps the previous rate. Explicit daily_rate=null appears in changes and is rejected because this catalogue field cannot be null. An empty body is also rejected as a no-op.
The schema deliberately does not accept quantity here. Stock changes need a policy for active rentals and will be considered with capacity, not smuggled into a generic setattr loop. extra='forbid' rejects quantity, id and role before the service. The loop is safe only because changes came from this specific public schema; never apply arbitrary caller keys to an ORM object.
Deleting an unused item returns 204 with no body. Deleting an item referenced by a rental conflicts with the stored foreign key and returns 409. A repeated delete of an already missing item returns 404 under this contract. State these outcomes rather than saying delete is always harmless. The independent PATCH transfer verifies omission, explicit null, empty body, unknown keys and fresh-session persistence.
Follow the running code
Focused lesson example; see the end-of-stage capstone for the cumulative app · stage 04
changes = EquipmentPatch(name='Light tripod').model_dump(exclude_unset=True)
assert changes == {'name': 'Light tripod'}
explicit = EquipmentPatch(daily_rate=None).model_dump(exclude_unset=True)
assert explicit == {'daily_rate': None}Predict and observe this focused example using the concepts explained above. Its boundary is stated in the focused answer.
Guided lab
- Read the explanation and predict the focused example’s outcome.
- Predict a name-only PATCH, explicit null rate, empty object and quantity field. Explain delete outcomes without introducing the later layer refactor.
- Compare the observed outcome with the focused answer and state its boundary.
Expected: Name-only changes only name and preserves rate. Explicit null remains visible and is rejected by the operation; an empty patch is rejected as a no-op; quantity is outside the narrow schema and fails. Successful unused delete is 204 without a body; missing is 404; a referenced row conflicts with the foreign key and is 409.
- An unbounded setattr over raw JSON can change private fields; dependent-row deletion conflicts.
Focused exercise and answer
Complete this focused exercise before reading its answer. The full native transfer is introduced only at the end of the stage.
Your transfer task: Predict a name-only PATCH, explicit null rate, empty object and quantity field. Explain delete outcomes without introducing the later layer refactor.
- Predict a name-only PATCH, explicit null rate, empty object and quantity field. Explain delete outcomes without introducing the later layer refactor.
Inspect the matching answer
This answer addresses the focused exercise above; the cumulative implementation is shown only after the stage prerequisites.
Name-only changes only name and preserves rate. Explicit null remains visible and is rejected by the operation; an empty patch is rejected as a no-op; quantity is outside the narrow schema and fails. Successful unused delete is 204 without a body; missing is 404; a referenced row conflicts with the foreign key and is 409.Check your reasoning
Why is exclude_unset different from exclude_none?
Show the explanation
exclude_unset preserves the caller's omission decision. exclude_none would erase an explicit null and prevent the service rejecting it.
Reading progress
54 lessons remain open to guests. Marking a lesson read records reading only; it does not award assessment credit or a certificate.
Device reading marks require browser storage. Reading is always available.
Sign in or create an account to save separate account progress. Your current page is kept.
Your earlier place on this device suggests these lessons. No new lesson is marked read.