Skip to content

[BE] Skip foreign-key existence queries in CompetencyCriterion.save() #898

Open

Description

@javoconsultant

User Story

As a course author bulk-editing competency criteria, I want the update to skip redundant database reads for each criterion, in order to keep large batches fast.

Description

CompetencyCriterion.save() calls full_clean() with no exclude. As a result, Django runs a separate SELECT on every save to check that each foreign key's row still exists. We need to exclude the foreign keys from that call and let the database's foreign-key constraints enforce them.

Context

Acceptance Criteria

Scenario: Saving a criterion doesn't re-read the rows it points at
  Given an existing criterion
  When its rule is changed and it is saved
  Then the save writes the criterion and its history record
  And it does not read the criterion's group, tag association, or rule profile

Scenario: Bulk update cost no longer includes the foreign-key reads
  Given a batch of criteria in one group whose rule the update changes
  When the bulk update applies override values, and again when it names a rule profile
  Then each changed criterion adds 2 queries to the batch, in both cases

Scenario: Override payload validation still runs on save
  Given a criterion
  When it is saved with override values whose payload doesn't fit the rule type
  Then the save is refused with a validation error, as before

Scenario: A reference to a missing row is still refused
  Given a criterion that points at a group, tag association, or rule profile that doesn't exist
  When it is saved
  Then the database refuses the write
Technical Details

This is a suggested approach, not the source of truth. The User Story, Description, and Acceptance Criteria define what must be true when the work is done.

Approach

  1. In CompetencyCriterion.save() in src/openedx_learning/applets/cbe/models/criteria.py, pass exclude=["group", "object_tag", "rule_profile"] to full_clean(). _validate_rule_source() in api.py already uses the same exclude. clean() still runs, so override payload validation is unchanged.
  2. Excluding a field also skips its not-null check. A dangling or missing foreign key now fails as an IntegrityError from the database instead of a ValidationError from save(). The API paths look up the group and tag before they build a criterion, so they shouldn't notice. Confirm that no caller catches the ValidationError for this case.
  3. On SQLite and PostgreSQL, Django creates foreign-key constraints as DEFERRABLE INITIALLY DEFERRED, so the database checks a dangling reference at commit, not at save(). A test for the last scenario needs to force the check, for example with connection.check_constraints(). MySQL checks immediately.
  4. Pin the per-criterion cost with a django_assert_num_queries test on bulk_update_competency_criteria(): a fixed number of queries, plus 2 per changed criterion, for both override values and a named profile.

Additional context

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions