Problem/Motivation

After upgrading from Group 2.3.2 to Group 3.3.5, the group_members view and other Views using the group_relationship_to_entity relationship plugin return zero results, even though group relationships exist in the database.

Root Cause

The GroupRelationshipToEntityBase::query() method validates the configured group_relation_plugins option by creating a WHERE condition using plugin IDs. However, after migration from Group 2.3.2, the database type column in group_relationship tables contains legacy Group 2 hashed IDs (e.g., group_content_type_195ff066e6abd) instead of Group 3 derived IDs (e.g., default-group-membership).

When the Views relationship plugin tries to filter by plugin ID like group_membership, it creates a WHERE condition that expects the database to contain these plugin IDs, but instead finds hashed legacy IDs, causing the query to return zero results.

Code Location

In src/Plugin/views/relationship/GroupRelationshipToEntityBase.php (lines 148-156):

// Add the plugin IDs to the query if any were selected.
$plugin_ids = array_filter($this->options['group_relation_plugins']);

// If none were selected, we still need to build a list of plugin IDs to
// make sure we do not show content using plugins that do not handle the
// entity type this views plugin was configured for.
$def['extra'][] = [
  $this->getJoinFieldType() => 'plugin_id',
  'value' => $plugin_ids ?: array_keys($this->getValidPlugins()),
];

This creates a SQL condition like:

WHERE group_relationship_field_data.plugin_id IN ('group_membership')

But the validation logic appears to be checking against the wrong column or incorrectly mapping plugin IDs to relationship type IDs, resulting in queries that add WHERE (1 = 0).

Database State After Migration

The plugin_id column correctly contains 'group_membership', but the type column (which is the bundle/entity type ID) contains legacy hashed IDs:

mysql> SELECT id, type, plugin_id, gid
       FROM group_relationship_field_data
       WHERE plugin_id = 'group_membership'
       LIMIT 5;
+----+-----------------------------------+-----------------+-----+
| id | type                              | plugin_id       | gid |
+----+-----------------------------------+-----------------+-----+
| 18 | group_content_type_195ff066e6abd  | group_membership| 1   |
| 19 | group_content_type_195ff066e6abd  | group_membership| 1   |
| 259| group_content_type_195ff066e6abd  | group_membership| 1   |
+----+-----------------------------------+-----------------+-----+

The corresponding group_relationship_type config entities have the same legacy hashed IDs:

mysql> SELECT id, plugin_id
       FROM group_relationship_type
       WHERE plugin_id = 'group_membership';
+-----------------------------------+-----------------+
| id                                | plugin_id       |
+-----------------------------------+-----------------+
| group_content_type_195ff066e6abd  | group_membership|
| visitor_content-group_membership  | group_membership|
+-----------------------------------+-----------------+

Secondary Issue: group_roles Field

Additionally, the group_roles field from the group_relationship__group_roles table (added by the gcontent_moderation module or custom fields) doesn't appear properly in the Views UI for adding to displays post-migration, suggesting Views data definitions aren't fully updated for dedicated field storage tables.

Impact

  • Severity: Major - breaks core Views functionality after migration
  • Affected users: Any site upgrading from Group 2.3.2 to 3.3.5 that uses Views with the group_relationship_to_entity relationship plugin configured with group_relation_plugins
  • Data loss: None - data is intact but not accessible via Views
  • Workaround: Remove group_relation_plugins configuration from the relationship (partial solution)

Steps to reproduce

  1. Install Drupal with Group 2.3.2
  2. Create a group type (e.g., "default")
  3. Add group members via the group_membership plugin
  4. Verify the group_members view at /group/{gid}/members displays members
  5. Export configuration (optional, if using config management)
  6. Update Group module to 3.3.5 via composer: composer require drupal/group:^3.3
  7. Run database updates: drush updb
  8. Visit /group/{gid}/members

Expected result

The group_members view displays all group members with their roles.

Actual result

The view displays "No members available" even though members exist in the database.

Debugging

Enable Views SQL query logging. The generated SQL query may contain WHERE (1 = 0) conditions or fail to match records due to the legacy hashed IDs in the type column not matching the expected plugin ID filtering.

Proposed resolution

Option 1: Update Views Relationship Plugin Validation Logic (Recommended)

Modify GroupRelationshipToEntityBase::query() to properly map configured plugin IDs to actual group_relationship_type entity IDs, accounting for legacy hashed IDs that may exist after migration:

public function query() {
  $this->ensureMyTable();

  // Build the join definition.
  $def = $this->definition;
  $def['table'] = $this->definition['base'];
  $def['field'] = $this->definition['base field'];
  $def['left_table'] = $this->tableAlias;
  $def['left_field'] = $this->realField;
  $def['adjusted'] = TRUE;

  // Change the join to INNER if the relationship is required.
  if (!empty($this->options['required'])) {
    $def['type'] = 'INNER';
  }

  // If there were extra join conditions added in the definition, use them.
  if (!empty($this->definition['extra'])) {
    $def['extra'] = $this->definition['extra'];
  }

  // Add the plugin IDs to the query if any were selected.
  $plugin_ids = array_filter($this->options['group_relation_plugins']);

  // Map plugin IDs to actual relationship type IDs to handle legacy migrations.
  if (!empty($plugin_ids)) {
    $relationship_type_storage = \Drupal::entityTypeManager()
      ->getStorage('group_relationship_type');
    $type_ids = [];

    foreach ($plugin_ids as $plugin_id) {
      // Load all relationship types that use this plugin.
      $types = $relationship_type_storage->loadByProperties([
        'plugin_id' => $plugin_id,
      ]);
      foreach ($types as $type) {
        $type_ids[] = $type->id();
      }
    }

    if (!empty($type_ids)) {
      $def['extra'][] = [
        'left_field' => 'type',
        'value' => $type_ids,
      ];
    }
  }
  else {
    // If no specific plugins selected, filter by valid plugin IDs for this entity type.
    $def['extra'][] = [
      $this->getJoinFieldType() => 'plugin_id',
      'value' => array_keys($this->getValidPlugins()),
    ];
  }

  // Use the standard join plugin unless instructed otherwise.
  $join_id = !empty($def['join_id']) ? $def['join_id'] : 'standard';
  $join = $this->joinManager->createInstance($join_id, $def);

  // Add the join using a more verbose alias.
  assert($this->query instanceof Sql);
  $alias = $def['table'] . '_' . $this->table;
  $this->alias = $this->query->addRelationship($alias, $join, $this->definition['base'], $this->relationship);

  // Add access tags if the base table provides it.
  $table_data = $this->viewsData->get($def['table']);
  if (empty($this->query->options['disable_sql_rewrite']) && isset($table_data['table']['base']['access query tag'])) {
    $access_tag = $table_data['table']['base']['access query tag'];
    $this->query->addTag($access_tag);
  }
}

Benefits:

  • Fixes the issue for all migrated sites without requiring database changes
  • Maintains backward compatibility
  • Handles both legacy hashed IDs and new derived IDs
  • No config changes required

Option 2: Add Post-Migration Update Hook

Add a new update hook (e.g., group_update_10310()) that reconciles the type column in group_relationship tables to use Group 3 derived IDs consistently:

/**
 * Reconcile relationship type IDs to use Group 3 derived format.
 */
function group_update_10310(&$sandbox) {
  if (!\Drupal::state()->get('group_update_10300_detected_legacy_version', FALSE)) {
    return t('Not needed - already on v3.');
  }

  $database = \Drupal::database();
  $relationship_type_storage = \Drupal::entityTypeManager()
    ->getStorage('group_relationship_type');
  $types = $relationship_type_storage->loadMultiple();
  $mapping = [];

  foreach ($types as $type) {
    $group_type_id = $type->getGroupTypeId();
    $plugin_id = $type->getPluginId();
    $entity_id = $type->id();

    // Derive what the Group 3 ID should be.
    $derived_id = $relationship_type_storage->getRelationshipTypeId(
      $group_type_id,
      $plugin_id
    );

    // If entity ID doesn't match derived ID, we have a legacy ID.
    if ($derived_id !== $entity_id) {
      $mapping[$entity_id] = $derived_id;

      // Rename the config entity.
      $config_name = 'group.relationship_type.' . $entity_id;
      $new_config_name = 'group.relationship_type.' . $derived_id;

      $config_factory = \Drupal::configFactory();
      $old_config = $config_factory->getEditable($config_name);

      if (!$old_config->isNew()) {
        $data = $old_config->getRawData();
        $data['id'] = $derived_id;

        $config_factory->getEditable($new_config_name)
          ->setData($data)
          ->save(TRUE);
        $old_config->delete();
      }
    }
  }

  if (empty($mapping)) {
    return t('No type column reconciliation needed.');
  }

  // Update database type columns.
  $updated = 0;
  foreach ($mapping as $old_id => $new_id) {
    $updated += $database->update('group_relationship')
      ->fields(['type' => $new_id])
      ->condition('type', $old_id)
      ->execute();

    $updated += $database->update('group_relationship_field_data')
      ->fields(['type' => $new_id])
      ->condition('type', $old_id)
      ->execute();
  }

  return t('Reconciled @count rows in type column.', ['@count' => $updated]);
}

Benefits:

  • Standardizes all sites to use consistent derived IDs
  • Simplifies future code that relies on predictable ID format
  • Aligns with Group 3's intended ID derivation algorithm

Drawbacks:

  • Requires sites to run an additional update hook
  • May conflict with sites that have already exported config with legacy IDs

Option 3: Fix Views Data for Dedicated Field Storage Tables

Enhance group_views_data_alter() in group.views.inc to ensure dedicated field storage tables (like group_relationship__group_roles) are properly exposed after migration:

function group_views_data_alter(array &$data) {
  // Existing code for relationship definitions...

  // Ensure dedicated field storage tables are properly integrated.
  $field_storage_configs = \Drupal::entityTypeManager()
    ->getStorage('field_storage_config')
    ->loadByProperties(['entity_type' => 'group_relationship']);

  foreach ($field_storage_configs as $field_storage) {
    /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
    $field_name = $field_storage->getName();
    $table_mapping = \Drupal::entityTypeManager()
      ->getStorage('group_relationship')
      ->getTableMapping();

    if ($table_mapping->requiresDedicatedTableStorage($field_storage)) {
      $table_name = $table_mapping->getDedicatedDataTableName($field_storage);

      // Ensure the table has proper Views integration.
      if (!isset($data[$table_name])) {
        // Views should auto-generate this, but force it if missing.
        \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
      }
    }
  }
}

Remaining tasks

  1. Confirm the issue: Maintainer review and confirmation that this is indeed a bug in the Views relationship plugin validation logic
  2. Decide on approach: Choose between Option 1 (update validation logic) vs Option 2 (standardize IDs via update hook) vs both
  3. Create patch: Implement the chosen solution
  4. Write tests: Add test coverage for:
    • Views relationships with plugin filtering after Group 2.3.2 → 3.3.5 migration
    • Views that use dedicated field storage tables like group_roles
    • Sites with both legacy hashed IDs and derived IDs coexisting
  5. Update documentation: Add notes to upgrade guide about:
    • Expected behavior for Views after migration
    • How plugin filtering works with relationship type IDs
    • Steps to troubleshoot if Views return zero results
  6. Test on real migration: Test patch against actual Group 2.3.2 → 3.3.5 upgrade scenarios
  7. Review and commit

User interface changes

Before fix:

  • Views like group_members display "No members available" even when members exist
  • Views UI may show group_roles field as available but attempting to configure it produces errors
  • Editing the relationship in Views UI shows group_relation_plugins checkboxes, but selecting them causes zero results

After fix:

  • Views correctly display group relationships (members, content, etc.)
  • Plugin filtering via group_relation_plugins works as expected
  • group_roles and other dedicated storage fields appear and function correctly in Views UI
  • No visible UI changes to the Views configuration interface itself

API changes

Changed Methods

GroupRelationshipToEntityBase::query()

  • Change: Internal logic modified to map plugin IDs to relationship type IDs before adding to SQL WHERE conditions
  • BC impact: None - this is an internal implementation detail, not a public API
  • Signature: No change to method signature

New Update Hooks (if Option 2 is chosen)

group_update_10310()

  • Purpose: Reconcile relationship type IDs to use Group 3 derived format
  • When: Runs only on sites that upgraded from Group 2.3.2 (checked via group_update_10300_detected_legacy_version state)
  • Effect: Updates type column in group_relationship tables and renames config entities

Behavior Changes

After the fix, GroupRelationshipToEntityBase::query() will:

  1. When group_relation_plugins is configured, load group_relationship_type entities matching those plugin IDs
  2. Use the actual entity IDs (which may be legacy hashed IDs) in the SQL WHERE condition
  3. Filter by type column instead of assuming plugin IDs can be used directly

This makes the plugin work correctly with both:

  • Fresh Group 3 installations (derived IDs)
  • Migrated sites (legacy hashed IDs)
  • Mixed scenarios (some legacy, some derived)

Data model changes

No Schema Changes Required

The database schema remains unchanged. The issue is with the data in the type column, not the schema itself.

Current State (After Group 2.3.2 → 3.3.5 Migration)

group_relationship table:

id   | type                              | uuid | langcode
-----|-----------------------------------|------|----------
18   | group_content_type_195ff066e6abd  | ...  | en

group_relationship_field_data table:

id   | type                              | plugin_id        | gid | entity_id
-----|-----------------------------------|------------------|-----|----------
18   | group_content_type_195ff066e6abd  | group_membership | 1   | 42

group_relationship_type config:

id: group_content_type_195ff066e6abd
plugin_id: group_membership
group_type: default
# ... other config

Expected State (Group 3 Fresh Install)

group_relationship table:

id   | type                      | uuid | langcode
-----|---------------------------|------|----------
18   | default-group-membership  | ...  | en

group_relationship_field_data table:

id   | type                      | plugin_id        | gid | entity_id
-----|---------------------------|------------------|-----|----------
18   | default-group-membership  | group_membership | 1   | 42

group_relationship_type config:

id: default-group-membership
plugin_id: group_membership
group_type: default
# ... other config

Key Differences

Field Group 2.3.2 (Hashed) Group 3.3.5 (Derived) Algorithm
type column group_content_type_195ff066e6abd default-group-membership MD5 hash vs concatenation
plugin_id column group_membership group_membership Unchanged
Config entity ID group_content_type_195ff066e6abd default-group-membership Matches type column

Derivation Algorithm (Group 3)

Group 3 uses GroupRelationshipTypeStorage::getRelationshipTypeId() to derive IDs:

$preferred_id = $group_type_id . '-' . str_replace(':', '-', $plugin_id);

if (strlen($preferred_id) > 32) {
  if (32 - strlen($group_type_id) > 8) {
    $hashed_id = $group_type_id . '-' . md5($plugin_id);
  }
  else {
    $hashed_id = 'grt_' . md5($preferred_id);
  }
  $preferred_id = substr($hashed_id, 0, 32);
}

return $preferred_id;

Example: getRelationshipTypeId('default', 'group_membership') returns 'default-group-membership'

Why Legacy IDs Persist After Migration

The Group 2 → 3 upgrade path (group_update_10300) renames config entities and updates entity type definitions, but it does not update the actual type values in existing database records or rename the config entity IDs themselves. This is likely intentional to avoid breaking references, but it causes Views validation to fail.

Impact of Fix

With Option 1 (validation logic update): No data changes required. The code adapts to handle both ID formats.

With Option 2 (update hook): Data would be updated to standardize on derived IDs, making the data model consistent across fresh installs and migrated sites.


Testing

Environment:

  • Group 3.3
  • Drupal 10.3 / 11.x
  • multisite installation
  • Custom modules: gcontent_moderation 3.0.0-beta1

Claude Code was used to help generate this detailed analysis of the issue and the efforts I tried to resolve the views roles output.

Issue fork group-3618552

Command icon 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

shelane created an issue.

kristiaanvandeneynde’s picture

Hey there, thanks for the report, but the things Claude are reporting seem completely wrong. The views plugin it's complaining about is filtering on a DB column (plugin_id) that hasn't changed at all in the upgrade path. The data is exactly the same. At no point does that plugin use relationship type IDs to filter on (the type column).

There's bound to be something wrong that's causing you issues, but this really seems like one of those cases where Claude is overconfident and flat-out wrong.

kristiaanvandeneynde’s picture

I just wrote a test locally proving that the view is not broken after the update. The query returns the same results. The only time I got a "No members available" outcome was when I did not log in as a user with sufficient permissions during the test. After fixing that using the account switcher, everything works fine.