Skip to content

Commit bd7f587

Browse files
committed
Merge branch '4.7' of github.com:mongodb/laravel-mongodb into DOCSP-41336-documentmodel-trait
2 parents 616a260 + 172c6e3 commit bd7f587

File tree

11 files changed

+446
-22
lines changed

11 files changed

+446
-22
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
## [4.7.0] - 2024-07-19
5+
6+
* Add `Query\Builder::upsert()` method by @GromNaN in [#3052](https://github.com/mongodb/laravel-mongodb/pull/3052)
7+
* Add `Connection::getServerVersion()` by @GromNaN in [#3043](https://github.com/mongodb/laravel-mongodb/pull/3043)
8+
* Add `Schema\Builder::getTables()` and `getTableListing()` by @GromNaN in [#3044](https://github.com/mongodb/laravel-mongodb/pull/3044)
9+
* Add `Schema\Builder::getColumns()` and `getIndexes()` by @GromNaN in [#3045](https://github.com/mongodb/laravel-mongodb/pull/3045)
10+
* Add `Schema\Builder::hasColumn()` and `hasColumns()` method by @Alex-Belyi in [#3001](https://github.com/mongodb/laravel-mongodb/pull/3001)
11+
* Fix unsetting a field in an embedded model by @GromNaN in [#3052](https://github.com/mongodb/laravel-mongodb/pull/3052)
12+
413
## [4.6.0] - 2024-07-09
514

615
* Add `DocumentModel` trait to use any 3rd party model with MongoDB @GromNaN in [#2580](https://github.com/mongodb/laravel-mongodb/pull/2580)

docs/includes/framework-compatibility-laravel.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- Laravel 10.x
88
- Laravel 9.x
99

10-
* - 4.2 to 4.6
10+
* - 4.2 to 4.7
1111
- ✓
1212
- ✓
1313
-

src/Connection.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,17 @@ public function __call($method, $parameters)
327327
return $this->db->$method(...$parameters);
328328
}
329329

330+
/**
331+
* Return the server version of one of the MongoDB servers: primary for
332+
* replica sets and standalone, and the selected server for sharded clusters.
333+
*
334+
* @internal
335+
*/
336+
public function getServerVersion(): string
337+
{
338+
return $this->db->command(['buildInfo' => 1])->toArray()[0]['version'];
339+
}
340+
330341
private static function getVersion(): string
331342
{
332343
return self::$version ?? self::lookupVersion();

src/Query/Builder.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,47 @@ public function update(array $values, array $options = [])
725725
return $this->performUpdate($values, $options);
726726
}
727727

728+
/** @inheritdoc */
729+
public function upsert(array $values, $uniqueBy, $update = null): int
730+
{
731+
if ($values === []) {
732+
return 0;
733+
}
734+
735+
$this->applyBeforeQueryCallbacks();
736+
737+
$options = $this->inheritConnectionOptions();
738+
$uniqueBy = array_fill_keys((array) $uniqueBy, 1);
739+
740+
// If no update fields are specified, all fields are updated
741+
if ($update !== null) {
742+
$update = array_fill_keys((array) $update, 1);
743+
}
744+
745+
$bulk = [];
746+
747+
foreach ($values as $value) {
748+
$filter = $operation = [];
749+
foreach ($value as $key => $val) {
750+
if (isset($uniqueBy[$key])) {
751+
$filter[$key] = $val;
752+
}
753+
754+
if ($update === null || array_key_exists($key, $update)) {
755+
$operation['$set'][$key] = $val;
756+
} else {
757+
$operation['$setOnInsert'][$key] = $val;
758+
}
759+
}
760+
761+
$bulk[] = ['updateOne' => [$filter, $operation, ['upsert' => true]]];
762+
}
763+
764+
$result = $this->collection->bulkWrite($bulk, $options);
765+
766+
return $result->getInsertedCount() + $result->getUpsertedCount() + $result->getModifiedCount();
767+
}
768+
728769
/** @inheritdoc */
729770
public function increment($column, $amount = 1, array $extra = [], array $options = [])
730771
{

src/Relations/EmbedsOneOrMany.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
use Throwable;
1616

1717
use function array_merge;
18+
use function assert;
1819
use function count;
1920
use function is_array;
21+
use function str_starts_with;
2022
use function throw_if;
2123

2224
abstract class EmbedsOneOrMany extends Relation
@@ -392,7 +394,12 @@ public static function getUpdateValues($array, $prepend = '')
392394
$results = [];
393395

394396
foreach ($array as $key => $value) {
395-
$results[$prepend . $key] = $value;
397+
if (str_starts_with($key, '$')) {
398+
assert(is_array($value), 'Update operator value must be an array.');
399+
$results[$key] = static::getUpdateValues($value, $prepend);
400+
} else {
401+
$results[$prepend . $key] = $value;
402+
}
396403
}
397404

398405
return $results;

src/Schema/Builder.php

Lines changed: 143 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,46 @@
66

77
use Closure;
88
use MongoDB\Model\CollectionInfo;
9+
use MongoDB\Model\IndexInfo;
910

11+
use function array_fill_keys;
12+
use function array_keys;
13+
use function assert;
1014
use function count;
1115
use function current;
16+
use function implode;
1217
use function iterator_to_array;
18+
use function sort;
19+
use function sprintf;
20+
use function usort;
1321

1422
class Builder extends \Illuminate\Database\Schema\Builder
1523
{
16-
/** @inheritdoc */
17-
public function hasColumn($table, $column)
24+
/**
25+
* Check if column exists in the collection schema.
26+
*
27+
* @param string $table
28+
* @param string $column
29+
*/
30+
public function hasColumn($table, $column): bool
1831
{
19-
return true;
32+
return $this->hasColumns($table, [$column]);
2033
}
2134

22-
/** @inheritdoc */
23-
public function hasColumns($table, array $columns)
35+
/**
36+
* Check if columns exists in the collection schema.
37+
*
38+
* @param string $table
39+
* @param string[] $columns
40+
*/
41+
public function hasColumns($table, array $columns): bool
2442
{
25-
return true;
43+
$collection = $this->connection->table($table);
44+
45+
return $collection
46+
->where(array_fill_keys($columns, ['$exists' => true]))
47+
->project(['_id' => 1])
48+
->exists();
2649
}
2750

2851
/**
@@ -107,6 +130,120 @@ public function dropAllTables()
107130
}
108131
}
109132

133+
public function getTables()
134+
{
135+
$db = $this->connection->getMongoDB();
136+
$collections = [];
137+
138+
foreach ($db->listCollectionNames() as $collectionName) {
139+
$stats = $db->selectCollection($collectionName)->aggregate([
140+
['$collStats' => ['storageStats' => ['scale' => 1]]],
141+
['$project' => ['storageStats.totalSize' => 1]],
142+
])->toArray();
143+
144+
$collections[] = [
145+
'name' => $collectionName,
146+
'schema' => null,
147+
'size' => $stats[0]?->storageStats?->totalSize ?? null,
148+
'comment' => null,
149+
'collation' => null,
150+
'engine' => null,
151+
];
152+
}
153+
154+
usort($collections, function ($a, $b) {
155+
return $a['name'] <=> $b['name'];
156+
});
157+
158+
return $collections;
159+
}
160+
161+
public function getTableListing()
162+
{
163+
$collections = iterator_to_array($this->connection->getMongoDB()->listCollectionNames());
164+
165+
sort($collections);
166+
167+
return $collections;
168+
}
169+
170+
public function getColumns($table)
171+
{
172+
$stats = $this->connection->getMongoDB()->selectCollection($table)->aggregate([
173+
// Sample 1,000 documents to get a representative sample of the collection
174+
['$sample' => ['size' => 1_000]],
175+
// Convert each document to an array of fields
176+
['$project' => ['fields' => ['$objectToArray' => '$$ROOT']]],
177+
// Unwind to get one document per field
178+
['$unwind' => '$fields'],
179+
// Group by field name, count the number of occurrences and get the types
180+
[
181+
'$group' => [
182+
'_id' => '$fields.k',
183+
'total' => ['$sum' => 1],
184+
'types' => ['$addToSet' => ['$type' => '$fields.v']],
185+
],
186+
],
187+
// Get the most seen field names
188+
['$sort' => ['total' => -1]],
189+
// Limit to 1,000 fields
190+
['$limit' => 1_000],
191+
// Sort by field name
192+
['$sort' => ['_id' => 1]],
193+
], [
194+
'typeMap' => ['array' => 'array'],
195+
'allowDiskUse' => true,
196+
])->toArray();
197+
198+
$columns = [];
199+
foreach ($stats as $stat) {
200+
sort($stat->types);
201+
$type = implode(', ', $stat->types);
202+
$columns[] = [
203+
'name' => $stat->_id,
204+
'type_name' => $type,
205+
'type' => $type,
206+
'collation' => null,
207+
'nullable' => $stat->_id !== '_id',
208+
'default' => null,
209+
'auto_increment' => false,
210+
'comment' => sprintf('%d occurrences', $stat->total),
211+
'generation' => $stat->_id === '_id' ? ['type' => 'objectId', 'expression' => null] : null,
212+
];
213+
}
214+
215+
return $columns;
216+
}
217+
218+
public function getIndexes($table)
219+
{
220+
$indexes = $this->connection->getMongoDB()->selectCollection($table)->listIndexes();
221+
222+
$indexList = [];
223+
foreach ($indexes as $index) {
224+
assert($index instanceof IndexInfo);
225+
$indexList[] = [
226+
'name' => $index->getName(),
227+
'columns' => array_keys($index->getKey()),
228+
'primary' => $index->getKey() === ['_id' => 1],
229+
'type' => match (true) {
230+
$index->isText() => 'text',
231+
$index->is2dSphere() => '2dsphere',
232+
$index->isTtl() => 'ttl',
233+
default => 'default',
234+
},
235+
'unique' => $index->isUnique(),
236+
];
237+
}
238+
239+
return $indexList;
240+
}
241+
242+
public function getForeignKeys($table)
243+
{
244+
return [];
245+
}
246+
110247
/** @inheritdoc */
111248
protected function createBlueprint($table, ?Closure $callback = null)
112249
{

tests/ConnectionTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,10 @@ public function testPingMethod()
299299
$instance = new Connection($config);
300300
$instance->ping();
301301
}
302+
303+
public function testServerVersion()
304+
{
305+
$version = DB::connection('mongodb')->getServerVersion();
306+
$this->assertIsString($version);
307+
}
302308
}

tests/EmbeddedRelationsTest.php

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@
1010
use Mockery;
1111
use MongoDB\BSON\ObjectId;
1212
use MongoDB\Laravel\Tests\Models\Address;
13-
use MongoDB\Laravel\Tests\Models\Book;
14-
use MongoDB\Laravel\Tests\Models\Client;
15-
use MongoDB\Laravel\Tests\Models\Group;
16-
use MongoDB\Laravel\Tests\Models\Item;
17-
use MongoDB\Laravel\Tests\Models\Photo;
18-
use MongoDB\Laravel\Tests\Models\Role;
1913
use MongoDB\Laravel\Tests\Models\User;
2014

2115
use function array_merge;
@@ -25,14 +19,7 @@ class EmbeddedRelationsTest extends TestCase
2519
public function tearDown(): void
2620
{
2721
Mockery::close();
28-
2922
User::truncate();
30-
Book::truncate();
31-
Item::truncate();
32-
Role::truncate();
33-
Client::truncate();
34-
Group::truncate();
35-
Photo::truncate();
3623
}
3724

3825
public function testEmbedsManySave()
@@ -951,4 +938,36 @@ public function testGetQueueableRelationsEmbedsOne()
951938
$this->assertEquals(['father'], $user->getQueueableRelations());
952939
$this->assertEquals([], $user->father->getQueueableRelations());
953940
}
941+
942+
public function testUnsetPropertyOnEmbed()
943+
{
944+
$user = User::create(['name' => 'John Doe']);
945+
$user->addresses()->save(new Address(['city' => 'New York']));
946+
$user->addresses()->save(new Address(['city' => 'Tokyo']));
947+
948+
// Set property
949+
$user->addresses->first()->city = 'Paris';
950+
$user->addresses->first()->save();
951+
952+
$user = User::where('name', 'John Doe')->first();
953+
$this->assertSame('Paris', $user->addresses->get(0)->city);
954+
$this->assertSame('Tokyo', $user->addresses->get(1)->city);
955+
956+
// Unset property
957+
unset($user->addresses->first()->city);
958+
$user->addresses->first()->save();
959+
960+
$user = User::where('name', 'John Doe')->first();
961+
$this->assertNull($user->addresses->get(0)->city);
962+
$this->assertSame('Tokyo', $user->addresses->get(1)->city);
963+
964+
// Unset and reset property
965+
unset($user->addresses->get(1)->city);
966+
$user->addresses->get(1)->city = 'Kyoto';
967+
$user->addresses->get(1)->save();
968+
969+
$user = User::where('name', 'John Doe')->first();
970+
$this->assertNull($user->addresses->get(0)->city);
971+
$this->assertSame('Kyoto', $user->addresses->get(1)->city);
972+
}
954973
}

0 commit comments

Comments
 (0)