Skip to content

Commit 6f55708

Browse files
committed
Added withCount cross database support
1 parent c83f31a commit 6f55708

File tree

3 files changed

+127
-0
lines changed

3 files changed

+127
-0
lines changed

src/Eloquent/Builder.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ class Builder extends IlluminateEloquentBuilder
1010
{
1111
use IlluminateEloquentQueriesRelationships, CrossDatabaseQueriesRelationships {
1212
CrossDatabaseQueriesRelationships::addHasWhere insteadof IlluminateEloquentQueriesRelationships;
13+
CrossDatabaseQueriesRelationships::withCount insteadof IlluminateEloquentQueriesRelationships;
1314
}
1415
}

src/Eloquent/Concerns/QueriesRelationships.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Hoyvoy\CrossDatabase\CanCrossDatabaseShazaamInterface;
66
use Illuminate\Database\Eloquent\Builder;
77
use Illuminate\Database\Eloquent\Relations\Relation;
8+
use Illuminate\Support\Str;
89

910
trait QueriesRelationships
1011
{
@@ -34,4 +35,70 @@ protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator,
3435

3536
return parent::addHasWhere($hasQuery, $relation, $operator, $count, $boolean);
3637
}
38+
39+
/**
40+
* Add subselect queries to count the relations.
41+
*
42+
* @param mixed $relations
43+
*
44+
* @return $this
45+
*/
46+
public function withCount($relations)
47+
{
48+
if (empty($relations)) {
49+
return $this;
50+
}
51+
52+
if (is_null($this->query->columns)) {
53+
$this->query->select([$this->query->from.'.*']);
54+
}
55+
56+
$relations = is_array($relations) ? $relations : func_get_args();
57+
58+
foreach ($this->parseWithRelations($relations) as $name => $constraints) {
59+
// First we will determine if the name has been aliased using an "as" clause on the name
60+
// and if it has we will extract the actual relationship name and the desired name of
61+
// the resulting column. This allows multiple counts on the same relationship name.
62+
$segments = explode(' ', $name);
63+
64+
unset($alias);
65+
66+
if (count($segments) == 3 && Str::lower($segments[1]) == 'as') {
67+
list($name, $alias) = [$segments[0], $segments[2]];
68+
}
69+
70+
$relation = $this->getRelationWithoutConstraints($name);
71+
72+
// Here we will get the relationship count query and prepare to add it to the main query
73+
// as a sub-select. First, we'll get the "has" query and use that to get the relation
74+
// count query. We will normalize the relation name then append _count as the name.
75+
$query = $relation->getRelationExistenceCountQuery(
76+
$relation->getRelated()->newQuery(), $this
77+
);
78+
79+
$query->callScope($constraints);
80+
81+
$query->mergeConstraintsFrom($relation->getQuery());
82+
83+
// If connection implements CanCrossDatabaseShazaamInterface we must attach database
84+
// connection name in from to be used by grammar when query compiled
85+
if ($this->getConnection() instanceof CanCrossDatabaseShazaamInterface) {
86+
$subqueryConnection = $query->getConnection()->getDatabaseName();
87+
$queryConnection = $this->getConnection()->getDatabaseName();
88+
if ($queryConnection != $subqueryConnection) {
89+
$queryFrom = $query->getQuery()->from.'<-->'.$subqueryConnection;
90+
$query->from($queryFrom);
91+
}
92+
}
93+
94+
// Finally we will add the proper result column alias to the query and run the subselect
95+
// statement against the query builder. Then we will return the builder instance back
96+
// to the developer for further constraint chaining that needs to take place on it.
97+
$column = $alias ?? Str::snake($name.'_count');
98+
99+
$this->selectSub($query->toBase(), $column);
100+
}
101+
102+
return $this;
103+
}
37104
}

tests/Integration/DatabaseEloquentSubqueriesCrossDatabaseTest.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,65 @@ public function testWhereDoesntHaveAcrossDatabaseConnection()
178178
});
179179
$this->assertEquals('select * from "users" where not exists (select * from "posts" where "users"."id" = "posts"."user_id" and "name" like ?)', $query->toSql());
180180
}
181+
182+
public function testWithCountAcrossDatabaseConnection()
183+
{
184+
// Test MySQL cross database subquery
185+
$query = UserMysql::withCount(['orders' => function ($query) {
186+
$query->where('name', 'like', '%a%');
187+
}
188+
]);
189+
$this->assertEquals('select `users`.*, (select count(*) from `mysql2`.`orders` where `users`.`id` = `orders`.`user_id` and `name` like ?) as `orders_count` from `users`', $query->toSql());
190+
191+
// Test MySQL same database subquery
192+
$query = UserMysql::withCount(['posts' => function ($query) {
193+
$query->where('name', 'like', '%a%');
194+
}
195+
]);
196+
$this->assertEquals('select `users`.*, (select count(*) from `posts` where `users`.`id` = `posts`.`user_id` and `name` like ?) as `posts_count` from `users`', $query->toSql());
197+
198+
// Test PostgreSQL cross database subquery
199+
$query = UserPgsql::withCount(['orders' => function ($query) {
200+
$query->where('name', 'like', '%a%');
201+
}
202+
]);
203+
$this->assertEquals('select "users".*, (select count(*) from "pgsql2"."orders" where "users"."id" = "orders"."user_id" and "name" like ?) as "orders_count" from "users"', $query->toSql());
204+
205+
// Test PostgreSQL same database subquery
206+
$query = UserPgsql::withCount(['posts' => function ($query) {
207+
$query->where('name', 'like', '%a%');
208+
}
209+
]);
210+
$this->assertEquals('select "users".*, (select count(*) from "posts" where "users"."id" = "posts"."user_id" and "name" like ?) as "posts_count" from "users"', $query->toSql());
211+
212+
// Test SQL Server cross database subquery
213+
$query = UserSqlsrv::withCount(['orders' => function ($query) {
214+
$query->where('name', 'like', '%a%');
215+
}
216+
]);
217+
$this->assertEquals('select [users].*, (select count(*) from [sqlsrv2].[orders] where [users].[id] = [orders].[user_id] and [name] like ?) as [orders_count] from [users]', $query->toSql());
218+
219+
// Test SQL Server same database subquery
220+
$query = UserSqlsrv::withCount(['posts' => function ($query) {
221+
$query->where('name', 'like', '%a%');
222+
}
223+
]);
224+
$this->assertEquals('select [users].*, (select count(*) from [posts] where [users].[id] = [posts].[user_id] and [name] like ?) as [posts_count] from [users]', $query->toSql());
225+
226+
// Test SQL Server cross database subquery
227+
$query = UserSqlite::withCount(['orders' => function ($query) {
228+
$query->where('name', 'like', '%a%');
229+
}
230+
]);
231+
$this->assertEquals('select "users".*, (select count(*) from "orders" where "users"."id" = "orders"."user_id" and "name" like ?) as "orders_count" from "users"', $query->toSql());
232+
233+
// Test SQL Server same database subquery
234+
$query = UserSqlite::withCount(['posts' => function ($query) {
235+
$query->where('name', 'like', '%a%');
236+
}
237+
]);
238+
$this->assertEquals('select "users".*, (select count(*) from "posts" where "users"."id" = "posts"."user_id" and "name" like ?) as "posts_count" from "users"', $query->toSql());
239+
}
181240
}
182241

183242
class UserMysql extends Model

0 commit comments

Comments
 (0)