Quiz - Soft delete, archive, and concurrency
You’re deciding the deletion strategy for four record types. Which ones should be hard-deleted? Select all that apply.
A user soft-deletes a project named “Acme,” then tries to create a new project also named “Acme” in the same org. The database rejects it with a unique-constraint violation. What’s the fix?
(organizationId, slug) a partial unique index with WHERE deleted_at IS NULL, so uniqueness is enforced only among live rows and the soft-deleted “Acme” becomes invisible to the constraint.WHERE deleted_at IS NULL enforces uniqueness only among live rows — exactly where it belongs. Enforcing in application code reintroduces a race the database used to close; hard-deleting throws away the recovery and audit trail that soft delete exists to keep. Note the Drizzle gotcha: .unique() can’t carry a WHERE, so this is uniqueIndex().on(...).where(sql\…`)with a rawsql` predicate.The lifecycle helper’s active() returns db.select().from(invoices).where(...).$dynamic() instead of awaiting a finished query. Why a chainable builder rather than a completed query?
.orderBy(...), .limit(...), and pagination onto a pre-scoped query — without $dynamic(), Drizzle locks those clauses to one invocation and chaining onto a pre-filtered query is a compile-time error.where, so the filters wouldn’t apply..where() to merge in extra conditions, which is how the org and lifecycle filters get combined.$dynamic() is the load-bearing call that lifts Drizzle’s one-shot restriction so that chaining compiles. A finished query wouldn’t have run “before the where” — the filters are already in the helper. And the third option is the trap the lesson warns against: a second .where() does not merge cleanly, which is why extra conditions are passed into the method as an extra predicate that gets and-ed into the single where.You’re reviewing a PR with a hand-written join from invoices to invoice_lines. The where reads and(eq(invoices.organizationId, orgId), activeFilter(invoices)). The single-table helper is used everywhere else, so a teammate says the join is fine. Is it?
invoices. Live invoices come back stapled to soft-deleted line items; the fix is activeFilter(invoiceLines) in the same and(...).innerJoin condition.scoped.active().withLines() on the helper so the filter is automatic.activeFilter predicate — filtering only the driving table leaks soft-deleted children. The course deliberately does not bolt join-awareness onto the helper (every join shape would need its own method and the helper would sprawl); instead joins live in named functions in db/queries/<entity>.ts and apply the shared predicate to each table, reviewed once.Two users editing the same invoice in separate tabs is the realistic case. For ordinary web request/response editing, what’s the senior default for stopping one from silently clobbering the other, and why?
SELECT ... FOR UPDATE holds a row lock from read to save, guaranteeing no other writer can interfere while the form is open.BEFORE UPDATE trigger that serializes all writes to the row, so the second save always queues behind the first.An action’s UPDATE includes WHERE version = :clientVersion but the developer forgets version = version + 1 in the SET. What breaks?
WHERE is what detects conflicts; the increment in the SET is only there to keep the displayed version current.WHERE detects a moved version, the SET version = version + 1 is what moves it for the next writer. Forget the bump and the version stays put, so every precondition keeps matching and no race is ever caught — the silent data loss the whole mechanism exists to stop. Doing the + 1 in SQL also keeps the increment atomic, so no second writer slips between read and write.You’ve learned the version-column pattern and are tempted to add it everywhere. Which of these writes genuinely needs a version precondition?
SET count = count + 1 directly in SQL.Quiz complete
Score by topic