MB Relationships loses source IDs when processed query arguments are reused

Support › MB Relationships › MB Relationships loses source IDs when processed query arguments are reused

Viewing 3 posts - 1 through 3 (of 3 total)
  • Author
    Posts
  • #50250
    Stephen WalkerStephen Walker
    Participant

    Hi Meta Box team,

    I found a reproducible issue in MB Relationships 1.14.0. Reusing a relationship query’s processed arguments in another WP_Query changes the relationship direction and removes its source IDs.

    This originally surfaced as missing WP Grid Builder facets on an Etch listing. I subsequently reproduced the underlying failure with Etch, WP Grid Builder, and our compatibility workaround all excluded from the request.

    Environment

    MB Relationships 1.14.0, bundled with Meta Box AIO 3.13.1
    WordPress 7.2-beta1
    PHP 8.5.12
    WordPress Studio with SQLite

    Production MySQL has not been tested. However, you can directly observe the loss of relationship arguments before the resulting SQL runs.

    Minimal reproduction

    Our fixture has a lfe_clients_projects relationship from Clients to Projects. Client 6679 has four connected projects.

    The following is read-only. Substitute a registered relationship, valid source ID, and destination post type from your installation:

    $args = [
        'post_type'      => 'lfe_demo_project',
        'post_status'    => 'publish',
        'posts_per_page' => -1,
        'orderby'       => 'title',
        'order'         => 'ASC',
        'relationship'  => [
            'id'   => 'lfe_clients_projects',
            'from' => 6679,
        ],
    ];
    
    $first = new WP_Query($args);
    
    $normalized_relationship =
        $first->query_vars['relationship'];
    
    // Reuse the processed arguments.
    $copied = new WP_Query($first->query_vars);
    
    // Control: reuse the original arguments.
    $fresh = new WP_Query($args);
    
    echo wp_json_encode([
        'first_count'             => count($first->posts),
        'normalized_relationship' => $normalized_relationship,
        'copied_count'            => count($copied->posts),
        'copied_relationship'     =>
            $copied->query_vars['relationship'],
        'fresh_count'             => count($fresh->posts),
    ], JSON_PRETTY_PRINT);

    Expected: all three queries return the same four projects.

    Actual:

    First query: 4 projects
    Copied processed args: 0 projects
    Original args again: 4 projects

    What changes

    After the first query, the relationship arguments become:

    [
        'id'         => 'lfe_clients_projects',
        'id_field'   => 'ID',
        'direction'  => 'from',
        'items'      => [6679],
        'reciprocal' => false,
        'to'         => null,
    ]

    The original from key has been removed.

    After passing those processed arguments to another WP_Query, they become:

    [
        'id'         => 'lfe_clients_projects',
        'id_field'   => 'ID',
        'direction'  => 'to',
        'items'      => [],
        'reciprocal' => false,
        'from'       => null,
    ]

    The direction has changed from from to to, and the client ID has disappeared.

    The generated join then contains:

    INNER JOIN wp_mb_relationships AS mbr ON (
        mbr.from = wp_posts.ID
        AND mbr.type = 'lfe_clients_projects'
        AND mbr.to IN ()
    )

    Relevant implementation

    In inc/query/post.php, MBR_Query_Post::parse_query() normalizes the relationship argument and writes it back into the query variables.

    In inc/query/normalizer.php, MBR_Query_Normalizer::normalize_args() determines direction using:

    $direction = isset($args['from']) ? 'from' : 'to';

    It subsequently removes the selected source key:

    unset($args[$direction]);

    On a second pass, the original from key no longer exists. The normalizer therefore selects to and overwrites the previously valid direction and items.

    Effect on WP Grid Builder

    WP Grid Builder calculates facet choices and counts by making additional queries from the listing’s processed query variables.

    Its Objects::get_object_ids() passes those variables to Helpers::get_post_ids(), which creates another WP_Query. This exposes the repeated-normalization problem.

    In our browser test:

    The initial relationship-filtered listing shows the correct projects.

    The subsequent facet queries return no IDs.

    Facet choices and pagination controls disappear.

    Passing the original, unprocessed relationship arguments to the same WP Grid Builder helper returns the correct IDs.

    Workaround verification

    I tested a separate compatibility plugin that restores the missing source key from the normalized direction and items before Meta Box’s parse_query callback runs.

    It leaves fresh arguments unchanged and only repairs recognized, registered relationships in WP Grid Builder-marked post queries.

    With that workaround, I verified:

    Forward and reverse relationships.

    Multiple source IDs and different clients.

    Multiple distinct relationships combined with AND or OR.

    Empty intersections.

    Additional taxonomy and metadata constraints.

    Correct browser facet counts, AJAX filtering, and pagination.

    The listing still uses live relationships. It does not substitute a stored list of project IDs.

    Requested fix

    Could the normalizer recognize already-normalized relationship clauses and preserve their source IDs and direction when processing them again?

    The same handling should apply to individual clauses in supported AND/OR relationship groups.

    Alternatively, preserving the original public relationship arguments in query_vars would prevent subsequent consumers from receiving arguments that cannot safely be reused.

    #50251
    Stephen WalkerStephen Walker
    Participant

    The workaround

    /**
     * Plugin Name: Meta Box and WP Grid Builder relationship compatibility
     * Description: Automatically restores registered Meta Box relationship sources in copied WP Grid Builder post queries, including AND/OR combinations.
     * Version: 0.2.0
     */
    defined('ABSPATH') || exit;
    
    final class MB_WPGB_Relationship_Compat {
        /** Repair only normalized clauses belonging to registered relationships. */
        public static function restore_clause($args) {
            if (!is_array($args) || !is_string($args['id'] ?? null)) return $args;
            // Preserve fresh arguments, including explicitly supplied source IDs.
            if (isset($args['from']) || isset($args['to'])) return $args;
            $direction = $args['direction'] ?? '';
            if (!in_array($direction, ['from', 'to'], true) || !isset($args['items']) || !is_array($args['items'])) return $args;
            $relationship = MB_Relationships_API::get_relationship($args['id']);
            if (!$relationship || ($args['id_field'] ?? null) !== $relationship->get_db_field($direction)) return $args;
            $args[$direction] = $args['items'];
            return $args;
        }
    
        /** Meta Box 1.14.0 supports a single clause or one flat AND/OR group. */
        public static function restore($args) {
            if (!class_exists('MB_Relationships_API') || !is_array($args)) return $args;
            if (!isset($args['relation'])) return self::restore_clause($args);
            if (!in_array($args['relation'], ['AND', 'OR'], true)) return $args;
            foreach ($args as $key => $clause) {
                if ($key !== 'relation') $args[$key] = self::restore_clause($clause);
            }
            return $args;
        }
    
        public static function repair_query(WP_Query $query) {
            if (!class_exists('MB_Relationships_API')) return;
            // WPGB removes wp_grid_builder after filtering but retains wpgb_inject
            // when copying the query to calculate facet choices and counts.
            $grid = $query->get('wp_grid_builder') ?: $query->get('wpgb_inject');
            if (!is_scalar($grid) || empty($grid)) return;
            $args = $query->get('relationship');
            $restored = self::restore($args);
            if ($restored !== $args) $query->set('relationship', $restored);
        }
    }
    
    // MB Relationships 1.14.0 parses post queries at priority 20.
    add_action('parse_query', [MB_WPGB_Relationship_Compat::class, 'repair_query'], 19);
    #50253
    PeterPeter
    Moderator

    Hello Stephen,

    Thanks for the detailed report and the reproduction. It made this easy to confirm.

    I can reproduce it with MB Relationships 1.14.0 (Meta Box AIO 3.13.1) on MySQL too. On MySQL it's slightly worse than on SQLite: the reused query doesn't just return 0 posts, it also triggers a SQL syntax error because of the empty IN () clause.

    You traced it correctly. In inc/query/normalizer.php, normalize_args() picks the direction from the from/to key, then removes that key:

    unset( $args[ $direction ] );

    The normalized args are written back to query_vars, so when they're passed to a new WP_Query the source ID is gone. The second pass then falls back to to, ends up with empty items, and returns nothing. Forward, reverse, and AND/OR groups are all affected.

    I've forwarded this to the development team so it can go into the next update. Until then, your compatibility plugin is a fine workaround.

Viewing 3 posts - 1 through 3 (of 3 total)
  • You must be logged in to reply to this topic.