Disclosure: This bug was found by @mattlibby and pinned down by Claude Code.
Problem/Motivation
With group_permissions enabled, loading the Revisions tab of a group entity (or running any access-checked entity query against group revisions) throws:
Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S22]: Column not found:
1054 Unknown column 'base_table.type' in 'WHERE': SELECT "base_table"."revision_id" ...
FROM "groups_revision" "base_table"
INNER JOIN "groups_field_data" "groups_field_data" ON "groups_field_data"."id" = "base_table"."id"
LEFT OUTER JOIN "group_relationship_field_data" "gcfd"
ON base_table.id=gcfd.gid AND gcfd.plugin_id='group_membership' AND gcfd.entity_id=:account_id
WHERE ("groups_field_data"."id" = :db_condition_placeholder_0)
AND ((("base_table"."type" IN (:db_condition_placeholder_1)) AND ("gcfd"."entity_id" IS NULL))
OR (("groups_field_data"."type" IN (:db_condition_placeholder_2)) AND ("gcfd"."entity_id" IS NOT NULL)))
ORDER BY "base_table"."revision_id" DESC
Note the asymmetry: the outsider branch references `base_table.type` while the insider branch
references `groups_field_data.type`. The base table here is `groups_revision`, which has no `type` column — so the first branch is invalid SQL.
The group module's own `\Drupal\group\QueryAccess\GroupQueryAlter` does not have this problem,
because `group_permissions` replaces it with `GroupPermissionsGroupQueryAlter` for access-checked group queries.
Root cause
In `src/QueryAccess/GroupPermissionsGroupQueryAlter.php::addSynchronizedConditions()`, `getTableWithType()`
is called before `ensureMembershipJoin()`:
if (!empty($allowed_ids)) {
$table_with_type = $this->getTableWithType(); // <-- called first
$allowed_condition->condition("$table_with_type.type", array_unique($allowed_ids), 'IN');
}
...
$membership_alias = $this->ensureMembershipJoin(); // <-- called afterwards
`getTableWithType()` decides between the base table and the data table using `$this->isRevisionTable`:
protected function getTableWithType() {
if ($this->dataTableAlias || $this->isRevisionTable) {
return $this->ensureDataTable();
}
return $this->ensureBaseTable();
}
But `$this->isRevisionTable` is only set as a side effect of `ensureBaseTable()` (reached via `ensureMembershipJoin()` → `getMembershipJoinTable()`). On the first synchronized-scope iteration, `ensureBaseTable()` has not run yet, so `isRevisionTable === FALSE` and `getTableWithType()` returns the bare revision base table (`groups_revision`) — which lacks a `type` column. The call to `ensureBaseTable()` during that same `getTableWithType()` then sets `isRevisionTable = TRUE`, so the second iteration correctly uses the data table. Hence the asymmetric WHERE clause.
The group module's `GroupQueryAlter::addSynchronizedConditions()` avoids this by calling `ensureMembershipJoin()` first, which populates `isRevisionTable` before `getTableWithType()` is used. `group_permissions` reordered these calls (membership-join check moved after the type condition) and reintroduced the revision-table bug that group fixed.
Steps to reproduce
- Install `group` (3.3.x) + `group_permissions`, with `group_support_revisions` enabled.
- Create a group type whose members get access via synchronized outsider/insider scopes.
- As a user with view/edit access to a group, visit the group's Revisions tab, or run:
\Drupal::entityTypeManager()->getStorage('group')->getQuery()
->allRevisions()
->accessCheck(TRUE)
->condition('id', $group_id)
->execute();
→ `SQLSTATE[42S22] ... Unknown column 'base_table.type'`.
Proposed resolution
Make `getTableWithType()` self-sufficient by detecting the base table (and thus `isRevisionTable`) before branching, independent of call order:
protected function getTableWithType() {
// Make sure the base table has been detected first, since that is what flags
// $this->isRevisionTable. This class calls getTableWithType() before
// ensureMembershipJoin() in addSynchronizedConditions(), so without this the
// first invocation would see isRevisionTable === FALSE and return the bare
// revision base table (groups_revision), which has no "type" column.
$base_table = $this->ensureBaseTable();
if ($this->dataTableAlias || $this->isRevisionTable) {
return $this->ensureDataTable();
}
return $base_table;
}
`ensureBaseTable()` is idempotent (guarded by `$this->baseTableAlias === FALSE`) and was already invoked in both branches, so this only guarantees the revision flag is set before the branch decision. Verified against group 3.3.5 / group_permissions 2.0.0-alpha9 / Drupal 11.3.10: the revisions query now succeeds and non-revision group access queries are unaffected.
(An equivalent fix is to reorder `addSynchronizedConditions()` to call `ensureMembershipJoin()` before `getTableWithType()`, matching `\Drupal\group\QueryAccess\GroupQueryAlter`. The `getTableWithType()` fix is preferred because it is robust regardless of caller order.)
Remaining tasks
apply the working patch to a fork and make an MR.
Issue fork group_permissions-3594266
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
benstallings commentedComment #3
benstallings commentedComment #5
lobsterr commentedHm, strange. I have a fresh instance and I couldn't reproduce it there.
I have double checked the logic of group module and and logic of group permissions logic and I don't see the difference in terms of detection of the tables.
I tried both options your provided, with query and checking the revisions tab.
Comment #6
lobsterr commentedok, I am able to reproduce, I will check it
Comment #7
lobsterr commentedI found the issue, I change the order of ensureMembershipJoin and that was causing the issue. I rolled back your changes and moved ensureMembershipJoin on the top.
Could you please check it?
Comment #8
benstallings commentedI like your changes! Thanks, @lobsterr.
Comment #10
lobsterr commentedThank you for your contribution