Skip to content

mod_ratingallocate: get_record() found more than one record #330

@gjalam

Description

@gjalam
Hello !

Since upgrading mod_ratingallocate, creating/syncing groups from an allocation sometimes fails with: Error: mdb->get_record() found more than one record!

This happens even though there are no visible duplicates (groups, groupings, allocations) in the course DB.

Environment

  • Moodle: 4.5.6
  • PHP: 8.2.x (PHP-FPM)
  • DB: MariaDB 10.6.21 (InnoDB)
  • mod_ratingallocate V4.5-r1 (2025021900)
  • OS: Debian 11

What changed

Previously, the plugin deleted/recreated groups/groupings when (re)generating them. In recent versions it reuses existing groups and only populates them. We suspect the lookup criteria are too loose and can match multiple rows.

Steps to reproduce (minimal)

  1. Create a ratingallocate activity with several choices (titles below).
  2. In the same course, ensure groups exist that correspond to these choice titles (or leave groups pre-existing from a previous run).
  3. Trigger “create/populate groups” (synchronize allocation to grouping).
  4. Observe a fatal error.

Actual result

Error: mdb->get_record() found more than one record!
line 1699 of /lib/dml/moodle_database.php: call to debugging()
line 1659 of /lib/dml/moodle_database.php: call to moodle_database->get_record_sql()
line 1638 of /lib/dml/moodle_database.php: call to moodle_database->get_record_select()
line 1503 of /mod/ratingallocate/locallib.php: call to moodle_database->get_record()
line 1178 of /mod/ratingallocate/locallib.php: call to ratingallocate->synchronize_allocation_and_grouping()
line 1313 of /mod/ratingallocate/locallib.php: call to ratingallocate->process_action_allocation_to_grouping()
line 59 of /mod/ratingallocate/view.php: call to ratingallocate->handle_view()

Expected result

Groups and (optional) grouping are created or reused without fatal error.

Data sanity checks we ran (all OK)

  • No duplicate group names in the course:
SELECT name, COUNT(*) AS c
FROM mdl_groups
WHERE courseid = :courseid
GROUP BY name
HAVING c > 1;
  • No duplicate grouping names in the course:
SELECT name, COUNT(*) AS c
FROM mdl_groupings
WHERE courseid = :courseid
GROUP BY name
HAVING c > 1;
  • No duplicate group↔grouping mappings:
SELECT groupingid, groupid, COUNT(*) AS c
FROM mdl_groupings_groups
GROUP BY groupingid, groupid
HAVING c > 1;
  • No users duplicated in the same group:
SELECT groupid, userid, COUNT(*) AS c
FROM mdl_groups_members
GROUP BY groupid, userid
HAVING c > 1;
  • No duplicate allocations for the same activity/user:
SELECT ratingallocateid, userid, COUNT(*) AS c
FROM mdl_ratingallocate_allocations
GROUP BY ratingallocateid, userid
HAVING c > 1;
  • Choices are unique by title for the activity:
SELECT title, COUNT(*) AS c
FROM mdl_ratingallocate_choices
WHERE ratingallocateid = :id
GROUP BY title
HAVING c > 1;

Hypothesis

In locallib.php the code uses $DB->get_record() (with MUST_EXIST) to fetch a groups or groupings row using non-unique criteria (e.g., by courseid + name, or with empty/duplicate idnumber). If multiple rows match, Moodle throws the “found more than one record” exception.

Workaround we confirmed

Creating a dedicated grouping for the activity and ensuring exactly one group per choice, each with a unique idnumber, avoids the crash (the lookups become unambiguous).

Proposed fix (minimal, backwards compatible)

  1. Tighten the lookup criteria to guarantee uniqueness (e.g., include courseid and prefer matching by idnumber when set).
  2. Or use a tolerant fetch when uniqueness cannot be enforced, logging a developer warning instead of fatal’ing.

Example helper (drop-in, keeps MUST_EXIST semantics when 0 rows):

<?php
/**
 * Safe wrapper: returns the first match (id ASC) if multiple exist, logs a dev warning.
 * Throws dml_missing_record_exception when nothing matches.
 */
function ra_get_record_safe(string $table, array $conditions, string $fields='*') {
    global $DB;
    $records = $DB->get_records($table, $conditions, 'id ASC', $fields);
    if (!$records) {
        throw new \dml_missing_record_exception($table);
    }
    if (count($records) > 1) {
        debugging('mod_ratingallocate: multiple records in '.$table.' for '. json_encode($conditions).' — taking the first.', DEBUG_DEVELOPER);
    }
    return reset($records);
}

Then replace fragile calls like:

$group = $DB->get_record('groups', [
    'courseid' => $courseid,
    'name'     => $groupname
], '*', MUST_EXIST);

with:

$group = ra_get_record_safe('groups', [
    'courseid' => $courseid,
    'name'     => $groupname
]);

Extra info

  • Affected activity instance on our site: cmid=114099, ratingallocateid=141.
  • Sample choice titles (actual data used):
    • Grpe 1 - Filière Légumineuses
    • Grpe 2 - Filière Pommes de Bretagne
    • Grpe 3 - Filière Méthanisation
    • Grpe 4 - Filière Œufs
    • Grpe 5 - Filière Lait
    • Grpe 6 - Filière Semences
    • Grpe 7 - Filière Pêche
    • Grpe 8 - Filière Races bovines à faible effectif
  • We can provide full anonymized stack traces / DB snapshots if needed.
Best regards `

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions