Skip to content

Fix silent insert failure in tables-based progress repositories#7917

Open
donnapep wants to merge 23 commits intotrunkfrom
fix/tables-based-repo-insert-error-handling
Open

Fix silent insert failure in tables-based progress repositories#7917
donnapep wants to merge 23 commits intotrunkfrom
fix/tables-based-repo-insert-error-handling

Conversation

@donnapep
Copy link
Copy Markdown
Member

@donnapep donnapep commented Mar 10, 2026

Summary

$wpdb->insert() returns false on failure, but none of the tables-based repositories checked this return value. When an insert failed, the code silently continued with $wpdb->insert_id of 0, constructing invalid objects and losing student data without any indication.

This PR:

  • Throws a RuntimeException when $wpdb->insert() returns false in all tables-based repositories (progress, submission, answer, grade)
  • Adds try-catch handling at all call sites so failures are logged rather than crashing
  • Wraps secondary store (dual-write sync) operations in try-catch so a sync failure doesn't block the primary store
  • Adds @throws annotations to repository interfaces and implementations

Test plan

Happy path

  • With storage set to tables (Sensei > Settings > Experimental), enroll as a student, start a lesson, complete it, take a quiz — progress saves normally
  • With storage set to comments (default), repeat the above — progress saves normally

Error handling

To simulate a database insert failure, temporarily rename the progress table:

RENAME TABLE wp_sensei_lms_progress TO wp_sensei_lms_progress_backup;

With WP_DEBUG_LOG enabled and storage set to tables:

  • Visit a lesson page as an enrolled student — should see error notices ("An error occurred while starting the course/lesson. Please try again.") and log entries in wp-content/debug.log
  • Click "Complete Lesson" — should not crash; error notices remain, progress stays at 0%
  • Restore the table and flush the cache when done:
    RENAME TABLE wp_sensei_lms_progress_backup TO wp_sensei_lms_progress;
    wp cache flush
    

🤖 Generated with Claude Code

Add error handling for failed database inserts in the lesson and quiz
tables-based progress repositories. When $wpdb->insert() fails and
returns false, throw a RuntimeException instead of silently continuing
with insert_id of 0, which would create a fake success object and
lead to data corruption.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 10, 2026 17:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a silent data-loss bug in the tables-based student progress repositories. Previously, Tables_Based_Lesson_Progress_Repository::create() and Tables_Based_Quiz_Progress_Repository::create() ignored the return value of $wpdb->insert(). A failed insert would silently continue with insert_id = 0, producing a fake progress object with a zero ID that could corrupt downstream operations. The fix stores the return value and throws a \RuntimeException immediately on failure.

Changes:

  • Add $result variable to capture $wpdb->insert() return value and throw a \RuntimeException if it equals false in both Tables_Based_Lesson_Progress_Repository and Tables_Based_Quiz_Progress_Repository.
  • Add @throws \RuntimeException PHPDoc annotation to the create() methods of both repositories.
  • Add a changelog entry for the fix.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
includes/internal/student-progress/lesson-progress/repositories/class-tables-based-lesson-progress-repository.php Captures insert() return value and throws RuntimeException on failure; adds @throws docblock
includes/internal/student-progress/quiz-progress/repositories/class-tables-based-quiz-progress-repository.php Same fix applied to the quiz progress repository
changelog/fix-tables-based-repo-insert-error-handling New changelog entry describing the patch

Note: The identical bug exists in Tables_Based_Course_Progress_Repository::create() (line 83 of class-tables-based-course-progress-repository.php) but was not addressed in this PR.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

@donnapep donnapep self-assigned this Mar 10, 2026
@donnapep donnapep added this to the 4.26.0 milestone Mar 10, 2026
@donnapep donnapep added the HPPS label Mar 10, 2026
donnapep and others added 4 commits March 11, 2026 07:56
…ries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@donnapep donnapep force-pushed the fix/tables-based-repo-insert-error-handling branch from 4de92e2 to 2a82d54 Compare March 11, 2026 12:22
donnapep and others added 10 commits March 11, 2026 09:29
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- start_user_on_course: return false (not 0) to match docblock contract
- Migration task: always soft-delete comment even on failure to prevent
  infinite retry of permanently failing records
- Document partial data risk in loop-based catch blocks (retry is the
  correct recovery path since it delete_all + re-inserts)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Exception messages are for logging, not display. esc_html() corrupts
diagnostic data (e.g. converting & to &amp; in log files). Escape at
the point of output instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@donnapep donnapep force-pushed the fix/tables-based-repo-insert-error-handling branch from d230108 to e74c457 Compare March 11, 2026 15:18
…loss, improve docblocks

Add RuntimeException when $wpdb->insert() fails in tables-based repository
create() methods, replacing silent failures with detected-and-logged ones.
Wrap secondary store operations in aggregate repositories with try-catch so
the new exceptions from secondary sync writes are caught and logged rather
than propagating up to callers. Fix critical data loss bug in legacy quiz
migration where wp_delete_comment ran even when the insert failed. Replace
uncaught throw in quiz progress save() with error_log + return since primary
save already completed. Add proper @throws annotations with correct ordering
and context-appropriate wording. Add phpcs:ignore comments for error_log
calls. Fix anonymous function spacing in grade repositories.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@donnapep donnapep force-pushed the fix/tables-based-repo-insert-error-handling branch from e74c457 to c1c8292 Compare March 11, 2026 15:38
donnapep and others added 5 commits March 11, 2026 11:49
The batch query relies on soft-delete to progress — it fetches comments
with status 'log' without offset. Keeping continue on failure would leave
the comment undeleted, causing it to be retried forever. The soft-delete
moves the comment to trash (recoverable), and the error log captures the
comment ID, quiz ID, and user ID for manual recovery if needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove continue in migration task to prevent infinite retry loop — the
batch query relies on soft-delete to progress. Clarify mid-loop failure
comments in quiz answer/grade creation loops.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These calls would never display notices because the code runs in
admin_init handlers that redirect after processing, not on Settings
API pages where settings_errors() is rendered.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

includes/update-tasks/class-sensei-update-legacy-quiz-data.php:74

  • run_batch() soft-deletes the legacy answer comment unconditionally. If the insert into the new tables fails (caught RuntimeException), the legacy comment is still deleted, which can permanently lose the only copy of the student’s answer/grade and prevents a retry on the next batch run. Only delete the comment after a successful migration, or continue from the catch block without deleting so the batch can retry later.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Use explicit ?string syntax instead of implicit string = null
to avoid PHP 8.1+ deprecation warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@donnapep
Copy link
Copy Markdown
Member Author

@merkushin This one introduces logic for handling insert failures to the custom table. Just inserts for now but will expand to include other database operations once the approach is vetted.

@donnapep donnapep requested a review from mdawaffe March 11, 2026 18:57
Copy link
Copy Markdown
Member

@mdawaffe mdawaffe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most things just error_log(). Some things throw. A few things call Sensei()->notices->add_notice(). Is there a pattern for when things should do what?

* @param int $question_id The question ID.
* @param string $value The answer value.
*
* @throws \RuntimeException If answer creation fails.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (and most of the others) never actually throw. Should they?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants