Importing Quizzes by Position Instead of by ID: A Cardinal Mistake
The first quiz bug we caught after launch wasn't about grading at all — it was about which question a student was actually seeing. Our importer was wiring questions to answers by position (the cardinal number) instead of by the real ProQuiz question ID. Some students got the wrong question on the wrong screen.
The shape of the bug
On some imported quizzes, the questions displayed correctly but the answer-feedback came from a different question. A student who saw 'What is the term for LOUD in music?' would correctly select 'forte' and the system would report it wrong — because the system was scoring against question #2's answer set instead of question #1's. Not every quiz was affected. The pattern was: any quiz where the import order didn't happen to match the post-creation order ended up with mismatched mappings.
Why it happened
When the importer created ProQuiz questions, it iterated through the Thinkific export's question array and used the loop index (essentially 1, 2, 3…) as a placeholder for the question identifier when it built the answer mapping. The intent was to come back and substitute the real ProQuiz question IDs after WordPress inserted them and assigned auto-increment values. The substitution step worked correctly when WordPress happened to assign IDs in a monotonically increasing run matching our positions. It silently failed when anything else used the auto-increment counter between our inserts — which happens any time you run an importer with hooks unsuspended, custom-field plugins active, or anything else that creates posts as a side effect.
The fix
Two changes. First, immediately capture the actual ProQuiz question ID returned by the insert (`$wpdb->insert_id` on each `wp_learndash_pro_quiz_question` write) and use that real ID when building the answer mapping for that same question — never trust position as a substitute. Second, after every quiz's questions are written, run a verification query: SELECT the question and answer rows for the just-created quiz, confirm the count matches the export, confirm every answer's `question_id` foreign key points to a question that exists. If verification fails, the importer aborts that quiz rather than committing a broken mapping. We backfilled the production database by re-deriving the correct ID mapping from the export's `position` field and re-pointing the answer rows.
Why position-as-ID is seductive and wrong
It's seductive because the source data (Thinkific's export) genuinely uses position as a stable identifier within a quiz — every question has a `position: 0, 1, 2…` field, and you can rebuild the quiz's order from that. It's wrong as soon as you move that data into a foreign key relationship, because foreign keys want real IDs and positions are not stable across re-imports, partial reruns, or any reordering. Anywhere your code is about to use position as a stand-in for ID, look for the actual primary key the database is going to assign and use that instead — even if it costs an extra round-trip per row.
How we noticed it (and why earlier QA didn't)
A user reported it. We hadn't browser-completed the quizzes during pre-launch QA — we'd verified that the questions and answers existed and that each answer's `correct` flag matched the source, but we hadn't actually taken a quiz from a student's perspective and confirmed the feedback. After this incident, our QA checklist includes a full quiz attempt per course, not just data verification — same lesson we re-learned during the ProQuiz points bug a week later. Bugs that survive logical verification but fail visual verification are not rare; they're the default failure mode of LMS migration.