Can a Job Refer to a Pump We Have Never Recorded?
A row can have the right types and still contain an impossible reference. Put identity, domain and relationship rules at the database boundary.
13 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
Preserving Relationships Across Several Writers
BOTIE
SQLite’s documentation records foreign-key enforcement beginning in version 3.6.19 in 2009. It must be enabled for each connection in the configuration used here. The browser runner enables it before executing your statements. Merely seeing REFERENCES in a schema is insufficient evidence that a particular connection enforces it.
Sources for this history
The Job Exists, but Its Asset Does Not
The maintenance team opens an inspection for P99. The job has a valid-looking ID and duration, but P99 is absent from the equipment register. A later join quietly loses the job.
We need the database to preserve the relationship while accepting writes from every application that uses it. Constraints make those rules part of the table rather than a convention in one import script.
Invented maintenance records. SQL programs run in an isolated SQLite database; the workshop illustrates the operation with records rather than parsing SQL.
Step 1 of 1
Different constraints answer different questions
All of these apply to the same proposed job.
| Rule | Question | Example rejected |
|---|---|---|
| Primary key | Which job is this? | Reusing J1 |
| NOT NULL | Must duration be present? | Unknown duration |
| CHECK | Is duration in the allowed domain? | Negative minutes |
| Foreign key | Does its asset exist? | P99 absent from assets |
Primary key
- Question
- Which job is this?
- Example rejected
- Reusing J1
NOT NULL
- Question
- Must duration be present?
- Example rejected
- Unknown duration
CHECK
- Question
- Is duration in the allowed domain?
- Example rejected
- Negative minutes
Foreign key
- Question
- Does its asset exist?
- Example rejected
- P99 absent from assets
A valid field can still point to a missing parent
Membership and value checks operate on the same record before its identity is accepted.
Which proposed jobs violate the table contract?
Check duration and parent identity together, then retain the failing job IDs.
BOTIE
Rejected job IDs
- 0J2
Follow the operations
1. Proposed jobs
[ { "id": "J1", "asset_id": "P7", "minutes": 20 }, { "id": "J2", "asset_id": "P99", "minutes": 10 } ]2. Registered asset IDs
- 0P7
- 1P8
3. Check duration and parent identity
[ { "id": "J1", "valid": true }, { "id": "J2", "valid": false } ]4. Keep IDs that failed a check
- 0J2
5. Result
- 0J2
The widget illustrates selected invariants. The SQL table additionally enforces uniqueness and database-level relationships.
1. Let the database check every writer
A form can validate an asset ID, but a batch importer may bypass the form. A constraint checks the data at the shared write boundary. It also protects against two writers that both passed a preliminary application check before either committed.
Keep friendly validation in the interface so users can repair mistakes. Treat database constraint failures as expected error outcomes too. The interface cannot safely assume that the state remained unchanged between its first check and its write.
The database rejects the violating change; the application still needs to explain which request failed.
2. Separate missing values from invalid values
A CHECK expression rejects a row when it evaluates to false. An unknown result from NULL does not act like false in this setting. CHECK(minutes >= 0) therefore needs NOT NULL when every job must have a duration.
A UNIQUE constraint handles duplicate keys, but null treatment varies by dialect and declaration. Specify the intended null policy separately. Our job IDs and asset references must always exist, so both are explicitly not null.
A constraint is a precise predicate. Read its NULL behaviour rather than relying on the wording of the column name.
Check yourself
Questions on Parts 1 to 2
Question 1 of 2
What additional rule is needed if every duration must be supplied?
Use the information below.
A table declares minutes INTEGER CHECK(minutes >= 0), with no NOT NULL. The proposed minutes value is NULL.3. Make the parent key explicit
REFERENCES assets(id) connects the child’s asset_id to the parent’s declared unique identity. Insert the parent before an immediately checked child. The child column’s type and representation should match the identifier contract.
The SQLite runner enables PRAGMA foreign_keys = ON before starting learner work. Changing this pragma inside an active transaction does not enable enforcement for that transaction. Production connections need deliberate initialisation and a verification check.
A foreign key establishes existence. It does not establish that the current user is allowed to use that asset.
Attach an inspection to an existing asset
The child row stores the parent identity once.
| id |
|---|
| P7 |
| P8 |
Database setup for this example
CREATE TABLE assets(id TEXT PRIMARY KEY NOT NULL); INSERT INTO assets VALUES('P7'),('P8');CREATE TABLE visits(id TEXT PRIMARY KEY NOT NULL,asset_id TEXT NOT NULL REFERENCES assets(id));
INSERT INTO visits VALUES('V3','P8');
SELECT v.id,a.id FROM visits v JOIN assets a ON a.id=v.asset_id;| id | id |
|---|---|
| V3 | P8 |
The join can resolve the reference because the parent was inserted first.
4. Decide what deleting a parent should mean
Deleting an asset with historical jobs may need to be forbidden. ON DELETE RESTRICT expresses that policy here. CASCADE can be appropriate when dependent rows have no independent meaning, but it can also erase useful history if chosen casually.
SET NULL requires a nullable child reference and changes the meaning of the relationship. Often an equipment register should mark an asset retired rather than delete it. Choose based on the records’ lifetimes and audit requirements.
A referential action is a business decision that the database executes automatically.
5. Some rules involve more than one row
A row-level CHECK can validate minutes or a status. Rules such as one active assignment per asset may need a unique or partial unique index. More complex cross-row conditions can require carefully designed transactions or triggers.
Do not hide a lookup of another table inside an apparently local check and assume every future change will revalidate it. Use the database mechanisms whose enforcement scope matches the invariant. PostgreSQL and SQLite differ in supported forms, so inspect the chosen dialect.
Constraints are strongest when their intended scope is easy to state and test.
6. Test the rejected writes as well as the accepted one
The exercise inserts one valid job, then its tests try a missing parent, a duplicate key, a negative duration and a null duration. Each failure reveals a different missing constraint.
After a failed statement, inspect transaction state before deciding whether to continue or roll back. Engines and conflict policies differ. Our test harness isolates each attempted invalid insert in a savepoint so a rejection does not contaminate the next test.
A display containing one good row cannot prove that the next bad row will be rejected.
Check yourself
Questions on Parts 1 to 6
Question 1 of 2
Which policy preserves that history without deleting related jobs?
Use the information below.
Assets have historical jobs that must remain interpretable. An asset leaves service.Try it with Botie
Build with Botie
Write and run SQL