Split questions.ts up - #1678
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe shared questions database code was split into focused modules, and callers across features, UI, and tests now import from those paths. Test database helpers also moved to tracked temp-file utilities with sidecar cleanup. ChangesQuestions DB modularization
Tracked test database files
Estimated code review effort: 4 (Complex) | ~60 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/features/admin/attendees-merge.ts (1)
379-389: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
parseConflictDecisionsdiscards its built map and returnsundefined.The function builds
out(lines 383-387) but returnsundefinedon line 388, dropping the result entirely. The declared return type is stillRecord<string, T>, so this is also a type mismatch. CallersparseBookingDecisionsandparseMoneyDecisions(and transitivelyparseMergeDecisionForm) will getundefinedfor booking/money conflict decisions instead of the parsed map, breaking attendee-merge conflict resolution. This appears unrelated to the questions.ts module split and looks like an accidental edit.🐛 Proposed fix
const out: Record<string, T> = {}; for (const { key } of conflictBookingEntries(diff)) { const value = parse(key); if (value !== undefined) out[key] = value; } - return undefined ; + return out; };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/admin/attendees-merge.ts` around lines 379 - 389, `parseConflictDecisions` in `attendees-merge.ts` is accidentally discarding the assembled conflict map and returning `undefined`. Update the function to return the built `out` record so its declared `Record<string, T>` return type matches the actual value; this will restore correct results for `parseBookingDecisions`, `parseMoneyDecisions`, and `parseMergeDecisionForm`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/admin/attendee-form-routes.ts`:
- Line 395: The `answers` field in `attendee-form-routes.ts` is using an inline
`import()` type annotation instead of the existing top-level `AttendeeAnswerSet`
import. Update the surrounding schema/type definition to reference the already
imported `AttendeeAnswerSet` symbol directly, keeping the style consistent with
the `import type` usage near `saveAttendeeAnswers` and avoiding the redundant
inline module import.
In `@src/shared/db/questions/attendee-answers.ts`:
- Around line 99-198: saveAttendeeAnswers currently performs the DELETE and
INSERT phases as separate batches, which can leave attendee_answers empty if the
insert phase fails. Update saveAttendeeAnswers to run the full
clear-and-reinsert flow inside a single transaction, or add equivalent rollback
handling around the existing executeBatch calls so the delete in
attendee-answers.ts cannot commit without the matching inserts. Keep the
existing normalization and statement-building logic intact, but make the atomic
boundary encompass both the initial delete batch and the final insert batch.
In `@src/shared/db/questions/tables.ts`:
- Around line 28-37: The questionsTable definition intentionally omits
sort_order, so add a brief note near the defineTable<Question, QuestionInput>
schema in questionsTable to explain that sort_order is managed separately in
sort-order.ts and via raw ORDER BY clauses. Make sure the comment clearly marks
this as intentional so the absence of sort_order from Question/questionsTable is
not mistaken for a bug.
---
Outside diff comments:
In `@src/features/admin/attendees-merge.ts`:
- Around line 379-389: `parseConflictDecisions` in `attendees-merge.ts` is
accidentally discarding the assembled conflict map and returning `undefined`.
Update the function to return the built `out` record so its declared
`Record<string, T>` return type matches the actual value; this will restore
correct results for `parseBookingDecisions`, `parseMoneyDecisions`, and
`parseMergeDecisionForm`.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5efbc266-02d5-4f6e-9958-16c92ec08fca
📒 Files selected for processing (82)
biome.jsonsrc/features/admin/attendee-form-routes.tssrc/features/admin/attendee-page-data.tssrc/features/admin/attendees-csv.tssrc/features/admin/attendees-merge.tssrc/features/admin/calendar.tssrc/features/admin/group-page-data.tssrc/features/admin/listing-page-data.tssrc/features/admin/listings-view.tssrc/features/admin/modifiers.tssrc/features/admin/questions.tssrc/features/api/payment-processing.tssrc/features/public/ticket-form.tssrc/features/public/ticket-payment.tssrc/features/public/ticket-submit/parse.tssrc/features/public/ticket-submit/paths.tssrc/features/public/ticket-submit/prepare.tssrc/features/public/types.tssrc/shared/db/attendees/servicing.tssrc/shared/db/question-types.tssrc/shared/db/questions.tssrc/shared/db/questions/aggregates.tssrc/shared/db/questions/attendee-answers.tssrc/shared/db/questions/delete.tssrc/shared/db/questions/parsing.tssrc/shared/db/questions/queries.tssrc/shared/db/questions/sort-order.tssrc/shared/db/questions/strings.tssrc/shared/db/questions/tables.tssrc/shared/merge/attendee-merge.tssrc/shared/qr.tssrc/ui/templates/admin/attendee-detail.tsxsrc/ui/templates/admin/attendee-form.tsxsrc/ui/templates/admin/attendees.tsxsrc/ui/templates/admin/questions.tsxsrc/ui/templates/attendee-table.tsxsrc/ui/templates/components/question-text.tsxsrc/ui/templates/public/reservations.tsxtest/e2e/n-plus-one-guard.test.tstest/lib/checkout-pricing-consistency.test.tstest/lib/column-order/attendee-answers.test.tstest/lib/csv-questions.test.tstest/lib/parse-question-answers.test.tstest/lib/render-questions.test.tstest/lib/server-attendee-form/questions.test.tstest/lib/server-attendees.test.tstest/lib/server-booking-preserve.test.tstest/lib/server-calculate.test.tstest/lib/server-groups/attendees.test.tstest/lib/server-listing-export-checkin.test.tstest/lib/server-listings/export.test.tstest/lib/server-listings/show-groups-and-answers.test.tstest/lib/server-misc-admin-handlers.test.tstest/lib/server-modifiers/scope-links.test.tstest/lib/server-parents-gate.test.tstest/lib/server-public/custom-questions-multi.test.tstest/lib/server-public/custom-questions-single.test.tstest/lib/server-questions/answer-edit.test.tstest/lib/server-questions/answers.test.tstest/lib/server-questions/helpers.tstest/lib/server-questions/listing-questions.test.tstest/lib/server-questions/question-delete.test.tstest/lib/server-questions/questions.test.tstest/lib/server-webhooks/custom-questions-multi.test.tstest/lib/server-webhooks/custom-questions-single.test.tstest/lib/servicing/atomicity.test.tstest/lib/servicing/custom-questions.test.tstest/lib/ticket-form.test.tstest/shared/db/listings/delete.test.tstest/shared/db/modifier-resolve.test.tstest/shared/db/questions/attendee-answers.test.tstest/shared/db/questions/crud.test.tstest/shared/db/questions/helpers.tstest/shared/db/questions/listing-mapping.test.tstest/shared/db/questions/with-listings.test.tstest/shared/merge/attendee-merge/apply.test.tstest/shared/merge/attendee-merge/diff.test.tstest/shared/merge/attendee-merge/helpers.tstest/test-utils/factories.tstest/ui/templates/admin/attendee-detail.test.tstest/ui/templates/admin/attendees.test.tsxtest/ui/templates/components/question-text.test.tsx
💤 Files with no reviewable changes (2)
- src/shared/db/questions.ts
- biome.json
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/test-utils/temp-db-files.ts`:
- Around line 12-18: The shared temp directory cleanup in removeIfPresent is
swallowing a non-empty directory failure because Deno.removeSync is used without
recursive cleanup. Update removeIfPresent (and the call site that deletes the
shared temp dir) to detect directory deletes and pass recursive: true, or
otherwise ensure the directory is removed even when stray files exist. Keep the
existing base-path/sidecar cleanup logic in temp-db-files.ts but make the final
directory removal robust so stray artifacts do not prevent cleanup.
🪄 Autofix (Beta)
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 26466d66-f20f-4416-aaa2-efe6e6e5b3b0
📒 Files selected for processing (8)
src/features/admin/attendees-merge.tstest/lib/db/free-text-migration.test.tstest/lib/db/legacy-migration.test.tstest/lib/db/with-transaction.test.tstest/lib/test-utils/stubs-and-mocks.test.tstest/lib/test-utils/temp-db-files.test.tstest/test-utils/db.tstest/test-utils/temp-db-files.ts
…ment sort_order omission - attendee-form-routes.ts: import AttendeeAnswerSet at top level instead of an inline import() type annotation, matching the file's other imports. - attendee-answers.ts: correct the stale saveAttendeeAnswers comment that claimed the string-refcount trigger 'drops' strings (it only decrements used_count; the pruner is what eventually deletes). Explain the real reason DELETE and INSERT stay in separate batches — libsql batch() cannot share an outer interactive transaction's TxScope, so withTransaction would not make them atomic; threading a TxScope through getOrCreateStringIds is left as a follow-up. Records the atomicity gap and why we let it throw. - tables.ts: document that questions.sort_order is intentionally absent from questionsTable's schema — writes are managed via sort-order.ts and reads use raw ORDER BY, so accidental writes through the generic insert/update path cannot bypass the ordering invariants.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/shared/db/questions/attendee-answers.ts (1)
161-179: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winFilter stale text answers before interning strings.
getOrCreateStringIdscurrently writes/refreshes every submitted free-text value beforeliveTextQuestionIdsfilters deleted questions at Line 206. Stale form data for a removed question can therefore persist unused free-text instrings.🛡️ Proposed fix
- const [stringIds, questionIdsByAnswer, liveTextQuestionIds] = - await Promise.all([ - getOrCreateStringIds( - [...normalized.values()].flatMap((set) => - set.textAnswers.map((a) => a.text), - ), - ), - questionIdsByAnswerId( - unique([...normalized.values()].flatMap((set) => set.answerIds)), - ), - existingQuestionIds( - unique( - [...normalized.values()].flatMap((set) => [ - ...set.textAnswerIds.map((answer) => answer.questionId), - ...set.textAnswers.map((answer) => answer.questionId), - ]), - ), - ), - ]); + const [questionIdsByAnswer, liveTextQuestionIds] = await Promise.all([ + questionIdsByAnswerId( + unique([...normalized.values()].flatMap((set) => set.answerIds)), + ), + existingQuestionIds( + unique( + [...normalized.values()].flatMap((set) => [ + ...set.textAnswerIds.map((answer) => answer.questionId), + ...set.textAnswers.map((answer) => answer.questionId), + ]), + ), + ), + ]); + const stringIds = await getOrCreateStringIds( + [...normalized.values()].flatMap((set) => + set.textAnswers + .filter((answer) => liveTextQuestionIds.has(answer.questionId)) + .map((answer) => answer.text), + ), + ); ... - ...textAnswers.map((answer) => ({ - questionId: answer.questionId, - stringId: stringIds.get(answer.text)!, - })), + ...textAnswers + .filter((answer) => liveTextQuestionIds.has(answer.questionId)) + .map((answer) => ({ + questionId: answer.questionId, + stringId: stringIds.get(answer.text)!, + })),Also applies to: 200-206
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/shared/db/questions/attendee-answers.ts` around lines 161 - 179, The free-text interning in attendee-answers is happening before deleted-question filtering, so stale answers can still be written into strings. Update the flow in the attendee answer processing around getOrCreateStringIds and existingQuestionIds so only textAnswers whose questionId is present in liveTextQuestionIds are passed into string interning. Keep the liveTextQuestionIds filter as the source of truth, and apply it before building the list for getOrCreateStringIds to avoid persisting unused stale values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/shared/db/questions/attendee-answers.ts`:
- Around line 161-179: The free-text interning in attendee-answers is happening
before deleted-question filtering, so stale answers can still be written into
strings. Update the flow in the attendee answer processing around
getOrCreateStringIds and existingQuestionIds so only textAnswers whose
questionId is present in liveTextQuestionIds are passed into string interning.
Keep the liveTextQuestionIds filter as the source of truth, and apply it before
building the list for getOrCreateStringIds to avoid persisting unused stale
values.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: ea186fc6-342e-4c55-894e-50cefab3b527
📒 Files selected for processing (3)
src/features/admin/attendee-form-routes.tssrc/shared/db/questions/attendee-answers.tssrc/shared/db/questions/tables.ts
|
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 1 file(s) based on 1 unresolved review comment. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
|
Filter stale text answers before interning — analyzed, deferred to a focused follow-up @coderabbitai — the observation is valid in the abstract (a deleted-question free-text value does reach Verified unchanged from The concern is already mitigated and intentionally tested:
Why defer rather than fix in this PR: the proposed reorder (compute |
|
Tip For best results, initiate chat on the files or code changes.
I won't press further on this in the current PR. If you'd like, I'm happy to open a follow-up issue to track the "filter stale text answers before interning" improvement so it doesn't get lost — just let me know and I'll file it with a link back to this thread. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2735cb4355
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Closes the deferred follow-up from PR #1678's CodeRabbit review: a save that failed during the INSERT phase left the attendee with no answers (the DELETE had already committed in its own batch). saveAttendeeAnswers now wraps its whole body in one withTransaction: the DELETE, the in-between reads (answer→question, which text questions still exist), the string interning, and the INSERT all run as one interactive write transaction, committing or rolling back together. To make that possible, getOrCreateStringIds now accepts an optional TxScope — when given, its INSERT OR IGNORE + refresh created + read-your-writes SELECT each run on the caller's open transaction via tx.execute (instead of a separate executeBatchWithResults batch), so the SELECT still sees the rows the INSERT just wrote in the same transaction. The standalone path (no tx) is unchanged. The read helpers (questionIdsByAnswerId, existingQuestionIds) are replaced with tx-scoped variants so they share the save's snapshot rather than starting their own read transactions. Regression test: a poisoned tx.execute on INSERT INTO attendee_answers rejects the save mid-flow; the attendee's prior answers survive (the DELETE rolled back), not empty. Verified the test catches the original bug by temporarily moving the DELETE before the transaction — the test fails for the right reason, then passes once restored. The servicing atomicity tests (withPoisonedBatch in atomicity.test.ts) are migrated to a shared withPoisonedTransactionExecute test util, since saveAttendeeAnswers moved from db.batch to db.transaction + tx.execute and the old batch-poison no longer reached its writes. Rebased onto origin/main to pick up the stripe-mock harness isolation fix (#1676). Mutation testing deferred — too slow for this environment; all other precommit steps pass: typecheck (incl. test files), lint:ci, cpd (0% threshold), and the directly-affected test suites (attendee-answers, servicing atomicity, both custom-questions webhook suites, server-attendees, the full stripe suite).
Closes the deferred follow-up from PR #1678's CodeRabbit review: a save that failed during the INSERT phase left the attendee with no answers (the DELETE had already committed in its own batch). saveAttendeeAnswers now wraps its whole body in one withTransaction: the DELETE, the in-between reads (answer→question, which text questions still exist), the string interning, and the INSERT all run as one interactive write transaction, committing or rolling back together. To make that possible, getOrCreateStringIds now accepts an optional TxScope — when given, its INSERT OR IGNORE + refresh created + read-your-writes SELECT each run on the caller's open transaction via tx.execute (instead of a separate executeBatchWithResults batch), so the SELECT still sees the rows the INSERT just wrote in the same transaction. The standalone path (no tx) is unchanged. The read helpers (questionIdsByAnswerId, existingQuestionIds) are replaced with tx-scoped variants so they share the save's snapshot rather than starting their own read transactions. Regression test: a poisoned tx.execute on INSERT INTO attendee_answers rejects the save mid-flow; the attendee's prior answers survive (the DELETE rolled back), not empty. Verified the test catches the original bug by temporarily moving the DELETE before the transaction — the test fails for the right reason, then passes once restored. The servicing atomicity tests (withPoisonedBatch in atomicity.test.ts) are migrated to a shared withPoisonedTransactionExecute test util, since saveAttendeeAnswers moved from db.batch to db.transaction + tx.execute and the old batch-poison no longer reached its writes. Rebased onto origin/main to pick up the stripe-mock harness isolation fix (#1676). Mutation testing deferred — too slow for this environment; all other precommit steps pass: typecheck (incl. test files), lint:ci, cpd (0% threshold), and the directly-affected test suites (attendee-answers, servicing atomicity, both custom-questions webhook suites, server-attendees, the full stripe suite).
Closes the deferred follow-up from PR #1678's CodeRabbit review: a save that failed during the INSERT phase left the attendee with no answers (the DELETE had already committed in its own batch). saveAttendeeAnswers now wraps its whole body in one withTransaction: the DELETE, the in-between reads (answer→question, which text questions still exist), the string interning, and the INSERT all run as one interactive write transaction, committing or rolling back together. To make that possible, getOrCreateStringIds now accepts an optional TxScope — when given, its INSERT OR IGNORE + refresh created + read-your-writes SELECT each run on the caller's open transaction via tx.execute (instead of a separate executeBatchWithResults batch), so the SELECT still sees the rows the INSERT just wrote in the same transaction. The standalone path (no tx) is unchanged. The read helpers (questionIdsByAnswerId, existingQuestionIds) are replaced with tx-scoped variants so they share the save's snapshot rather than starting their own read transactions. Regression test: a poisoned tx.execute on INSERT INTO attendee_answers rejects the save mid-flow; the attendee's prior answers survive (the DELETE rolled back), not empty. Verified the test catches the original bug by temporarily moving the DELETE before the transaction — the test fails for the right reason, then passes once restored. The servicing atomicity tests (withPoisonedBatch in atomicity.test.ts) are migrated to a shared withPoisonedTransactionExecute test util, since saveAttendeeAnswers moved from db.batch to db.transaction + tx.execute and the old batch-poison no longer reached its writes. Rebased onto origin/main to pick up the stripe-mock harness isolation fix (#1676). Mutation testing deferred — too slow for this environment; all other precommit steps pass: typecheck (incl. test files), lint:ci, cpd (0% threshold), and the directly-affected test suites (attendee-answers, servicing atomicity, both custom-questions webhook suites, server-attendees, the full stripe suite).
Continues PR #1678's questions.ts split. attendee-answers.ts had grown to 481 lines, over AGENTS.md's ~400 target, so it's split into a folder: - attendee-answers/save.ts — saveAttendeeAnswers, groupListingAnswers, AttendeeAnswerSet, AttendeeListingEntry + save-only helpers (245 lines) - attendee-answers/reads.ts — batch + single reads (244 lines) All callers migrated to the new paths (no compat re-exports). Deleted the old monolith. Verified: typecheck, strict lint, jscpd 0%, and the affected test files incl. the dedicated attendee-answers suite and server-attendees. Addresses Codex P2 review thread on attendee-answers.ts.
…-merge The main->split-questions merge auto-merged both branches' imports of getQuestionsWithListingIds: split-questions' new path (questions/queries.ts) and main's original (the deleted questions.ts), producing a duplicate where one referenced a module that no longer exists. This broke typecheck (Cannot find module .../questions.ts + Duplicate identifier). main hasn't been split yet, so every main->split-questions merge reintroduces this until the split lands on main; removing the stale import keeps the questions/queries.ts one. Addresses Codex P1 review thread on attendees-merge.ts (the comment was posted before the merge made it real, but is now accurate on HEAD). Verified by typecheck: 3 errors before, 0 after.
Resolved conflicts: - server-attendees.test.ts (+ server-parents-gate.test.ts): main split these monoliths into themed files (#1681, #1684); accepted main's split (deleted the monoliths) and repointed the new themed files' imports of the deleted #shared/db/questions.ts onto the split sub-modules (question-types, attendee-answers/{save,reads}, queries, tables). - server-booking-preserve.test.ts: main refactored to use the assignQuestion test helper, so dropped the now-unused direct setListingQuestions/ answersTable/questionsTable imports.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Closes the deferred follow-up from PR #1678's CodeRabbit review: a save that failed during the INSERT phase left the attendee with no answers (the DELETE had already committed in its own batch). saveAttendeeAnswers now wraps its whole body in one withTransaction: the DELETE, the in-between reads (answer→question, which text questions still exist), the string interning, and the INSERT all run as one interactive write transaction, committing or rolling back together. To make that possible, getOrCreateStringIds now accepts an optional TxScope — when given, its INSERT OR IGNORE + refresh created + read-your-writes SELECT each run on the caller's open transaction via tx.execute (instead of a separate executeBatchWithResults batch), so the SELECT still sees the rows the INSERT just wrote in the same transaction. The standalone path (no tx) is unchanged. The read helpers (questionIdsByAnswerId, existingQuestionIds) are replaced with tx-scoped variants so they share the save's snapshot rather than starting their own read transactions. Regression test: a poisoned tx.execute on INSERT INTO attendee_answers rejects the save mid-flow; the attendee's prior answers survive (the DELETE rolled back), not empty. Verified the test catches the original bug by temporarily moving the DELETE before the transaction — the test fails for the right reason, then passes once restored. The servicing atomicity tests (withPoisonedBatch in atomicity.test.ts) are migrated to a shared withPoisonedTransactionExecute test util, since saveAttendeeAnswers moved from db.batch to db.transaction + tx.execute and the old batch-poison no longer reached its writes. Rebased onto origin/main to pick up the stripe-mock harness isolation fix (#1676). Mutation testing deferred — too slow for this environment; all other precommit steps pass: typecheck (incl. test files), lint:ci, cpd (0% threshold), and the directly-affected test suites (attendee-answers, servicing atomicity, both custom-questions webhook suites, server-attendees, the full stripe suite).
Closes the deferred follow-up from PR #1678's CodeRabbit review: a save that failed during the INSERT phase left the attendee with no answers (the DELETE had already committed in its own batch). saveAttendeeAnswers now wraps its whole body in one withTransaction: the DELETE, the in-between reads (answer→question, which text questions still exist), the string interning, and the INSERT all run as one interactive write transaction, committing or rolling back together. To make that possible, getOrCreateStringIds now accepts an optional TxScope — when given, its INSERT OR IGNORE + refresh created + read-your-writes SELECT each run on the caller's open transaction via tx.execute (instead of a separate executeBatchWithResults batch), so the SELECT still sees the rows the INSERT just wrote in the same transaction. The standalone path (no tx) is unchanged. The read helpers (questionIdsByAnswerId, existingQuestionIds) are replaced with tx-scoped variants so they share the save's snapshot rather than starting their own read transactions. Regression test: a poisoned tx.execute on INSERT INTO attendee_answers rejects the save mid-flow; the attendee's prior answers survive (the DELETE rolled back), not empty. Verified the test catches the original bug by temporarily moving the DELETE before the transaction — the test fails for the right reason, then passes once restored. The servicing atomicity tests (withPoisonedBatch in atomicity.test.ts) are migrated to a shared withPoisonedTransactionExecute test util, since saveAttendeeAnswers moved from db.batch to db.transaction + tx.execute and the old batch-poison no longer reached its writes. Rebased onto origin/main to pick up the stripe-mock harness isolation fix (#1676). Mutation testing deferred — too slow for this environment; all other precommit steps pass: typecheck (incl. test files), lint:ci, cpd (0% threshold), and the directly-affected test suites (attendee-answers, servicing atomicity, both custom-questions webhook suites, server-attendees, the full stripe suite).
Summary by CodeRabbit