From d7681668799fa019efdb2e204e22faa8b5928087 Mon Sep 17 00:00:00 2001
From: Pauline Vos <pauline.vos@mongodb.com>
Date: Thu, 12 Jun 2025 10:45:56 +0200
Subject: [PATCH] Improve error handling on unsupported hybrid queries

Hybrid belongs-to-many relationships are not supported for query
constraints. However, the support check was done downstream of a bunch
of Eloquent stuff, resulting in the user getting an exception that
didn't tell them anything about the usage being unsupported.

This moves that check further up the chain so that the user is alerted
to the lack of support before we do anything else.
---
 src/Helpers/QueriesRelationships.php | 27 +++++++++++++++++++++++-
 tests/HybridRelationsTest.php        | 31 +++++++++++++++++++++-------
 2 files changed, 50 insertions(+), 8 deletions(-)

diff --git a/src/Helpers/QueriesRelationships.php b/src/Helpers/QueriesRelationships.php
index 1f1ffa34b..29d708e3c 100644
--- a/src/Helpers/QueriesRelationships.php
+++ b/src/Helpers/QueriesRelationships.php
@@ -12,6 +12,7 @@
 use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
 use Illuminate\Database\Eloquent\Relations\Relation;
 use Illuminate\Support\Collection;
+use LogicException;
 use MongoDB\Laravel\Eloquent\Model;
 use MongoDB\Laravel\Relations\MorphToMany;
 
@@ -104,6 +105,8 @@ protected function isAcrossConnections(Relation $relation)
      */
     public function addHybridHas(Relation $relation, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null)
     {
+        $this->assertHybridRelationSupported($relation);
+
         $hasQuery = $relation->getQuery();
         if ($callback) {
             $hasQuery->callScope($callback);
@@ -128,6 +131,26 @@ public function addHybridHas(Relation $relation, $operator = '>=', $count = 1, $
         return $this->whereIn($this->getRelatedConstraintKey($relation), $relatedIds, $boolean, $not);
     }
 
+    /**
+     * @param Relation $relation
+     *
+     * @return void
+     *
+     * @throws Exception
+     */
+    private function assertHybridRelationSupported(Relation $relation): void
+    {
+        if (
+            $relation instanceof HasOneOrMany
+            || $relation instanceof BelongsTo
+            || ($relation instanceof BelongsToMany && ! $this->isAcrossConnections($relation))
+        ) {
+            return;
+        }
+
+        throw new LogicException(class_basename($relation) . ' is not supported for hybrid query constraints.');
+    }
+
     /**
      * @param Builder  $hasQuery
      * @param Relation $relation
@@ -213,6 +236,8 @@ protected function getConstrainedRelatedIds($relations, $operator, $count)
      */
     protected function getRelatedConstraintKey(Relation $relation)
     {
+        $this->assertHybridRelationSupported($relation);
+
         if ($relation instanceof HasOneOrMany) {
             return $relation->getLocalKeyName();
         }
@@ -221,7 +246,7 @@ protected function getRelatedConstraintKey(Relation $relation)
             return $relation->getForeignKeyName();
         }
 
-        if ($relation instanceof BelongsToMany && ! $this->isAcrossConnections($relation)) {
+        if ($relation instanceof BelongsToMany) {
             return $this->model->getKeyName();
         }
 
diff --git a/tests/HybridRelationsTest.php b/tests/HybridRelationsTest.php
index 08423007c..71fb0830b 100644
--- a/tests/HybridRelationsTest.php
+++ b/tests/HybridRelationsTest.php
@@ -78,7 +78,7 @@ public function testSqlRelations()
         $this->assertEquals('John Doe', $role->sqlUser->name);
 
         // MongoDB User
-        $user       = new User();
+        $user = new User();
         $user->name = 'John Doe';
         $user->save();
 
@@ -105,7 +105,7 @@ public function testSqlRelations()
 
     public function testHybridWhereHas()
     {
-        $user      = new SqlUser();
+        $user = new SqlUser();
         $otherUser = new SqlUser();
         $this->assertInstanceOf(SqlUser::class, $user);
         $this->assertInstanceOf(SQLiteConnection::class, $user->getConnection());
@@ -114,11 +114,11 @@ public function testHybridWhereHas()
 
         // SQL User
         $user->name = 'John Doe';
-        $user->id   = 2;
+        $user->id = 2;
         $user->save();
         // Other user
         $otherUser->name = 'Other User';
-        $otherUser->id   = 3;
+        $otherUser->id = 3;
         $otherUser->save();
         // Make sure they are created
         $this->assertIsInt($user->id);
@@ -159,7 +159,7 @@ public function testHybridWhereHas()
 
     public function testHybridWith()
     {
-        $user      = new SqlUser();
+        $user = new SqlUser();
         $otherUser = new SqlUser();
         $this->assertInstanceOf(SqlUser::class, $user);
         $this->assertInstanceOf(SQLiteConnection::class, $user->getConnection());
@@ -168,11 +168,11 @@ public function testHybridWith()
 
         // SQL User
         $user->name = 'John Doe';
-        $user->id   = 2;
+        $user->id = 2;
         $user->save();
         // Other user
         $otherUser->name = 'Other User';
-        $otherUser->id   = 3;
+        $otherUser->id = 3;
         $otherUser->save();
         // Make sure they are created
         $this->assertIsInt($user->id);
@@ -268,6 +268,23 @@ public function testHybridBelongsToMany()
         $this->assertEquals(1, $check->skills->count());
     }
 
+    public function testQueryingHybridBelongsToManyRelationFails()
+    {
+        $user = new SqlUser();
+        $this->assertInstanceOf(SQLiteConnection::class, $user->getConnection());
+
+        // Create Mysql Users
+        $user->fill(['name' => 'John Doe'])->save();
+        $skill = Skill::query()->create(['name' => 'MongoDB']);
+        $user->skills()->save($skill);
+
+        $this->expectExceptionMessage('BelongsToMany is not supported for hybrid query constraints.');
+
+        SqlUser::whereHas('skills', function ($query) {
+            return $query->where('name', 'LIKE', 'MongoDB');
+        });
+    }
+
     public function testHybridMorphToManySqlModelToMongoModel()
     {
         // SqlModel -> MorphToMany -> MongoModel