Skip to content

Populate parent_post_id in HPPS progress table#7916

Merged
donnapep merged 29 commits into
trunkfrom
add/hpps-parent-post-id
Mar 17, 2026
Merged

Populate parent_post_id in HPPS progress table#7916
donnapep merged 29 commits into
trunkfrom
add/hpps-parent-post-id

Conversation

@donnapep
Copy link
Copy Markdown
Member

@donnapep donnapep commented Mar 9, 2026

Summary

This prepares the data model for reusable lessons by storing hierarchy relationships directly in the sensei_lms_progress table, eliminating the need for postmeta joins.

This data wasn't populated prior to shipping HPPS, so it needs to be backfilled for those sites that have it enabled and are actively syncing between comments and custom tables.

  • Adds an optional $parent_post_id parameter to all progress repository create() interfaces and implementations (backward-compatible, defaults to null)
  • Updates all callers (Sensei_Utils::user_start_lesson(), Sensei_Quiz, Sensei_Grading) to look up and pass the parent ID: course ID for lesson progress, lesson ID for quiz progress
  • Updates the comments-to-tables Student_Progress_Migration to populate parent_post_id during initial migration
  • Adds a new Parent_Post_Id_Migration backfill job for existing HPPS sites that already have progress rows with NULL parent_post_id

Test plan

Automated tests

  • vendor/bin/phpunit --filter=testCreate_WhenParentPostIdProvided — verifies both lesson and quiz repositories store parent_post_id
  • vendor/bin/phpunit --filter=Parent_Post_Id_Migration_Test — 8 tests covering backfill migration (batching, idempotency, orphaned rows, completion)
  • vendor/bin/phpunit --filter=testRun_WithLessonAndQuiz_PopulatesParentPostId — verifies comments-to-tables migration populates parent_post_id
  • vendor/bin/phpunit --filter=testBackfillParentPostId — 5 tests covering upgrade handler conditions

Manual testing — new progress creation

  1. Set up a local site with HPPS enabled (Sensei > Settings > Experimental Features > High Performance Progress Storage)
  2. Create a course with a lesson and a quiz
  3. Enroll a student and have them start the lesson
  4. Check the sensei_lms_progress table:
    SELECT id, post_id, parent_post_id, type FROM wp_sensei_lms_progress ORDER BY id DESC;
  5. Verify:
    • Lesson progress row has parent_post_id = the course ID
    • Quiz progress row (if quiz was started) has parent_post_id = the lesson ID
    • Course progress row has parent_post_id = NULL

Manual testing — backfill migration

  1. On a site with HPPS enabled and existing progress data, null out parent_post_id to simulate pre-existing rows:
    UPDATE wp_sensei_lms_progress SET parent_post_id = NULL WHERE type IN ('lesson', 'quiz');
    SELECT COUNT(*) FROM wp_sensei_lms_progress WHERE parent_post_id IS NULL AND type IN ('lesson', 'quiz');
  2. Trigger the upgrade handler by decrementing the stored version:
    wp option update sensei-version 4.25.0
  3. Visit any admin page — run_updates() fires and schedules the Action Scheduler job (sensei_lms_migration_job_parent_post_id_migration)
  4. Run the scheduled action:
    wp action-scheduler run
  5. After migration completes, verify NULL count is 0 (or only orphaned records with no postmeta):
    SELECT COUNT(*) FROM wp_sensei_lms_progress WHERE parent_post_id IS NULL AND type IN ('lesson', 'quiz');
  6. Verify values are correct:
    -- Lesson progress: parent_post_id should match _lesson_course meta
    SELECT p.id, p.post_id AS lesson_id, p.parent_post_id, pm.meta_value AS expected_course_id
    FROM wp_sensei_lms_progress p
    LEFT JOIN wp_postmeta pm ON pm.post_id = p.post_id AND pm.meta_key = '_lesson_course'
    WHERE p.type = 'lesson' AND p.parent_post_id IS NOT NULL;
    
    -- Quiz progress: parent_post_id should match _quiz_lesson meta
    SELECT p.id, p.post_id AS quiz_id, p.parent_post_id, pm.meta_value AS expected_lesson_id
    FROM wp_sensei_lms_progress p
    LEFT JOIN wp_postmeta pm ON pm.post_id = p.post_id AND pm.meta_key = '_quiz_lesson'
    WHERE p.type = 'quiz' AND p.parent_post_id IS NOT NULL;
    • Confirm parent_post_id matches the expected value from postmeta in both queries

🤖 Generated with Claude Code

donnapep and others added 6 commits March 9, 2026 15:47
…terfaces

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ementations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lesson progress gets course_id from _lesson_course meta, quiz progress
gets lesson_id from the comment's post ID, course progress stays NULL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backfills NULL parent_post_id values in sensei_lms_progress. Lesson
progress gets the course ID, quiz progress gets the lesson ID. Runs
in batches via Action Scheduler after the existing migrations complete.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 9, 2026 20:00
@donnapep donnapep self-assigned this Mar 9, 2026
@donnapep donnapep added this to the 4.26.0 milestone Mar 9, 2026
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

Adds support for persisting hierarchy relationships in HPPS progress rows by populating sensei_lms_progress.parent_post_id (course for lesson progress; lesson for quiz progress) and introduces a backfill migration to populate existing rows.

Changes:

  • Extends lesson/quiz progress repository create() APIs to accept an optional $parent_post_id and stores it in table-based progress rows.
  • Updates key progress-creation callers and the comments-to-tables Student_Progress_Migration to populate parent_post_id.
  • Adds a new Parent_Post_Id_Migration and corresponding unit tests for backfilling existing HPPS rows.

Reviewed changes

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

Show a summary per file
File Description
tests/unit-tests/internal/student-progress/quiz-progress/repositories/test-class-tables-based-quiz-progress-repository.php Adds coverage that quiz progress create() persists parent_post_id.
tests/unit-tests/internal/student-progress/lesson-progress/repositories/test-class-tables-based-lesson-progress-repository.php Adds coverage that lesson progress create() persists parent_post_id.
tests/unit-tests/internal/migration/migrations/test-class-student-progress-migration.php Adds test ensuring comments-to-tables migration sets parent_post_id for course/lesson/quiz progress.
tests/unit-tests/internal/migration/migrations/test-class-parent-post-id-migration.php Introduces unit tests for the new parent_post_id backfill migration behavior.
includes/internal/student-progress/quiz-progress/repositories/class-tables-based-quiz-progress-repository.php Adds optional $parent_post_id to create() and writes it to the HPPS table.
includes/internal/student-progress/quiz-progress/repositories/class-table-reading-aggregate-quiz-progress-repository.php Threads $parent_post_id through aggregate create() and ensures lesson progress creation passes course parent.
includes/internal/student-progress/quiz-progress/repositories/class-quiz-progress-repository-interface.php Updates repository interface to accept optional $parent_post_id.
includes/internal/student-progress/quiz-progress/repositories/class-comments-based-quiz-progress-repository.php Updates method signature for interface compatibility (parameter unused).
includes/internal/student-progress/quiz-progress/repositories/class-comment-reading-aggregate-quiz-progress-repository.php Threads $parent_post_id through aggregate create().
includes/internal/student-progress/lesson-progress/repositories/class-tables-based-lesson-progress-repository.php Adds optional $parent_post_id to create() and writes it to the HPPS table.
includes/internal/student-progress/lesson-progress/repositories/class-table-reading-aggregate-lesson-progress-repository.php Threads $parent_post_id through aggregate create().
includes/internal/student-progress/lesson-progress/repositories/class-lesson-progress-repository-interface.php Updates repository interface to accept optional $parent_post_id.
includes/internal/student-progress/lesson-progress/repositories/class-comments-based-lesson-progress-repository.php Updates method signature for interface compatibility (parameter unused).
includes/internal/student-progress/lesson-progress/repositories/class-comment-reading-aggregate-lesson-progress-repository.php Threads $parent_post_id through aggregate create().
includes/internal/migration/migrations/class-student-progress-migration.php Populates parent_post_id during initial comments-to-tables migration.
includes/internal/migration/migrations/class-parent-post-id-migration.php Adds new backfill migration to populate missing parent_post_id for lesson/quiz progress rows.
includes/class-sensei.php Registers the new migration job with the migration scheduler.
includes/class-sensei-utils.php Passes course ID into lesson progress creation to populate parent_post_id.
includes/class-sensei-quiz.php Passes course/lesson parents into progress creation paths for grading/quiz start.
includes/class-sensei-grading.php Passes course ID into lesson progress creation during grading flow.
changelog/add-hpps-parent-post-id Adds changelog entry for the new HPPS parent_post_id population behavior.

💡 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.

Comment thread includes/internal/migration/migrations/class-parent-post-id-migration.php Outdated
Comment thread includes/internal/migration/migrations/class-parent-post-id-migration.php Outdated
Comment thread includes/class-sensei.php
donnapep and others added 19 commits March 10, 2026 07:57
Rename options to follow the sensei_migrated_ convention used by other
migrations. Add both options to Migration_Job_Scheduler::clear_state()
so they are properly cleaned up.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Check wpdb->update() return value and log errors via add_error(),
matching the pattern used by Student_Progress_Migration and
Quiz_Migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Match the pattern used in student progress migration: echo SELECT
queries and UPDATE statements during dry runs for inspection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sites that already completed the HPPS migration won't re-run the
scheduler on plugin update. Add an upgrade handler that detects this
case and schedules just the parent_post_id_migration job.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Follow the existing naming convention for version-gated update methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove misleading comments, redundant run() call, and add failure
messages to batch assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ogging

- Replace per-row get_post_meta with bulk SELECT ... WHERE post_id IN
- Distinguish get_results null (DB error) from empty array (no rows)
- Remove wpdb->last_error gate on update error check
- Log skipped orphaned rows via add_error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Sensei_Utils::get_lesson_course_id and get_quiz_lesson_id helpers
- Update callers to use helpers instead of inline get_post_meta
- Guard against 0 being stored as parent_post_id in tables-based repos

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap schedule_job_by_name in try-catch for RuntimeException
- Schedule job before resetting status so failure preserves COMPLETE state
- Add 5 tests covering all guard clauses and failure path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clean up LESSONS_COMPLETE_OPTION_NAME in setUp to prevent test pollution
- Add testRun_OrphanedQuiz_RemainsNull
- Add testRun_LessonAndQuizRows_BackfillsBothPhases for two-phase transition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Returning batch_size on a null $wpdb->get_results() never advanced the
cursor, causing the scheduler to re-run the same failing query forever.
Throwing RuntimeException lets Action Scheduler's retry mechanism handle
it (up to 3 retries, then STATUS_FAILED).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…vention

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both methods were nearly identical, differing only in progress type and
meta key. Consolidated into a single parameterized private method.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use %s placeholder for meta_key instead of string interpolation in SQL
- Add @throws tag to run() docblock since RuntimeException propagates
- Add clarifying comment for quiz parent_post_id in student progress migration
- Add @var docblock for $factory property in migration test
- Extract set_backfill_test_context helper to reduce duplication in update tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…uery errors

Add max(1, ...) guard on batch size to prevent zero/negative values from
filters causing an infinite no-op loop. Add null check on the second
get_results() call so a DB error fetching post meta throws a RuntimeException
instead of silently treating every row as "no meta found."

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ress migration

Extract (int) $comment->comment_post_ID into $parent_lesson_id to match the
pattern used for lesson progress ($course_id) a few lines above.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Apply the same max(1, ...) guard to Student_Progress_Migration and
Quiz_Migration that was added to Parent_Post_Id_Migration, preventing
filters from setting batch size to zero or negative which would cause
an infinite no-op loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cast int|bool values from get_lesson_id() to int before passing to
get_lesson_course_id() and create(). Remove redundant (int) cast in
sensei_start_lesson() where $lesson_id is already int.

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 25 out of 25 changed files in this pull request and generated 4 comments.


💡 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.

Comment thread includes/internal/migration/migrations/class-parent-post-id-migration.php Outdated
Comment thread includes/internal/migration/class-migration-job-scheduler.php Outdated
Comment thread includes/internal/migration/migrations/class-parent-post-id-migration.php Outdated
Comment thread includes/internal/migration/migrations/class-parent-post-id-migration.php Outdated
donnapep and others added 2 commits March 10, 2026 14:29
Reset migration status to NOT_STARTED before scheduling the job rather
than after, so concurrent requests (admin page + AJAX heartbeat) that
all see is_complete() === true don't each schedule a separate job.
Restore status to COMPLETE on failure so the backfill can be retried
on the next upgrade.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Exception messages are not rendered as HTML — escaping belongs at render
time, not throw time. Also remove noisy logging for orphaned rows (no
post meta) which are expected stale data from deleted posts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@donnapep donnapep requested a review from mdawaffe March 10, 2026 19:08
@donnapep donnapep added the HPPS label Mar 10, 2026
@donnapep
Copy link
Copy Markdown
Member Author

👋🏻 @merkushin

@merkushin
Copy link
Copy Markdown
Contributor

Sorry, almost missed the notification in my inbox 😅

The changes look good. But I haven't tested it locally.

It's interesting to notice that tests (with clear structure and names) become a better tool for understanding code. The generated code, on the other hand, becomes less readable (code nesting really affects it).

A couple of weeks ago, I read about an observation that in the modern world, code could be entirely open since the cost of writing it is extremely low, but the tests' value increases, and they might become closed source.

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.

A couple super minor things, but looks good to me. I didn't test locally :(

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@donnapep donnapep merged commit 7dfcb8d into trunk Mar 17, 2026
23 checks passed
@donnapep donnapep deleted the add/hpps-parent-post-id branch March 17, 2026 15:52
@donnapep donnapep removed the HPPS label Apr 22, 2026
@donnapep donnapep removed this from the 4.26.0 milestone Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants