Skip to content

Commit

Permalink
Merge branch '4.7' of github.com:mongodb/laravel-mongodb into DOCSP-4…
Browse files Browse the repository at this point in the history
…1336-documentmodel-trait
  • Loading branch information
rustagir committed Jul 23, 2024
2 parents 616a260 + 172c6e3 commit bd7f587
Show file tree
Hide file tree
Showing 11 changed files with 446 additions and 22 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Changelog
All notable changes to this project will be documented in this file.

## [4.7.0] - 2024-07-19

* Add `Query\Builder::upsert()` method by @GromNaN in [#3052](https://github.com/mongodb/laravel-mongodb/pull/3052)
* Add `Connection::getServerVersion()` by @GromNaN in [#3043](https://github.com/mongodb/laravel-mongodb/pull/3043)
* Add `Schema\Builder::getTables()` and `getTableListing()` by @GromNaN in [#3044](https://github.com/mongodb/laravel-mongodb/pull/3044)
* Add `Schema\Builder::getColumns()` and `getIndexes()` by @GromNaN in [#3045](https://github.com/mongodb/laravel-mongodb/pull/3045)
* Add `Schema\Builder::hasColumn()` and `hasColumns()` method by @Alex-Belyi in [#3001](https://github.com/mongodb/laravel-mongodb/pull/3001)
* Fix unsetting a field in an embedded model by @GromNaN in [#3052](https://github.com/mongodb/laravel-mongodb/pull/3052)

## [4.6.0] - 2024-07-09

* Add `DocumentModel` trait to use any 3rd party model with MongoDB @GromNaN in [#2580](https://github.com/mongodb/laravel-mongodb/pull/2580)
Expand Down
2 changes: 1 addition & 1 deletion docs/includes/framework-compatibility-laravel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- Laravel 10.x
- Laravel 9.x

* - 4.2 to 4.6
* - 4.2 to 4.7
- ✓
- ✓
-
Expand Down
11 changes: 11 additions & 0 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,17 @@ public function __call($method, $parameters)
return $this->db->$method(...$parameters);
}

/**
* Return the server version of one of the MongoDB servers: primary for
* replica sets and standalone, and the selected server for sharded clusters.
*
* @internal
*/
public function getServerVersion(): string
{
return $this->db->command(['buildInfo' => 1])->toArray()[0]['version'];
}

private static function getVersion(): string
{
return self::$version ?? self::lookupVersion();
Expand Down
41 changes: 41 additions & 0 deletions src/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,47 @@ public function update(array $values, array $options = [])
return $this->performUpdate($values, $options);
}

/** @inheritdoc */
public function upsert(array $values, $uniqueBy, $update = null): int
{
if ($values === []) {
return 0;
}

$this->applyBeforeQueryCallbacks();

$options = $this->inheritConnectionOptions();
$uniqueBy = array_fill_keys((array) $uniqueBy, 1);

// If no update fields are specified, all fields are updated
if ($update !== null) {
$update = array_fill_keys((array) $update, 1);
}

$bulk = [];

foreach ($values as $value) {
$filter = $operation = [];
foreach ($value as $key => $val) {
if (isset($uniqueBy[$key])) {
$filter[$key] = $val;
}

if ($update === null || array_key_exists($key, $update)) {
$operation['$set'][$key] = $val;
} else {
$operation['$setOnInsert'][$key] = $val;
}
}

$bulk[] = ['updateOne' => [$filter, $operation, ['upsert' => true]]];
}

$result = $this->collection->bulkWrite($bulk, $options);

return $result->getInsertedCount() + $result->getUpsertedCount() + $result->getModifiedCount();
}

/** @inheritdoc */
public function increment($column, $amount = 1, array $extra = [], array $options = [])
{
Expand Down
9 changes: 8 additions & 1 deletion src/Relations/EmbedsOneOrMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
use Throwable;

use function array_merge;
use function assert;
use function count;
use function is_array;
use function str_starts_with;
use function throw_if;

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

foreach ($array as $key => $value) {
$results[$prepend . $key] = $value;
if (str_starts_with($key, '$')) {
assert(is_array($value), 'Update operator value must be an array.');
$results[$key] = static::getUpdateValues($value, $prepend);
} else {
$results[$prepend . $key] = $value;
}
}

return $results;
Expand Down
149 changes: 143 additions & 6 deletions src/Schema/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,46 @@

use Closure;
use MongoDB\Model\CollectionInfo;
use MongoDB\Model\IndexInfo;

use function array_fill_keys;
use function array_keys;
use function assert;
use function count;
use function current;
use function implode;
use function iterator_to_array;
use function sort;
use function sprintf;
use function usort;

class Builder extends \Illuminate\Database\Schema\Builder
{
/** @inheritdoc */
public function hasColumn($table, $column)
/**
* Check if column exists in the collection schema.
*
* @param string $table
* @param string $column
*/
public function hasColumn($table, $column): bool
{
return true;
return $this->hasColumns($table, [$column]);
}

/** @inheritdoc */
public function hasColumns($table, array $columns)
/**
* Check if columns exists in the collection schema.
*
* @param string $table
* @param string[] $columns
*/
public function hasColumns($table, array $columns): bool
{
return true;
$collection = $this->connection->table($table);

return $collection
->where(array_fill_keys($columns, ['$exists' => true]))
->project(['_id' => 1])
->exists();
}

/**
Expand Down Expand Up @@ -107,6 +130,120 @@ public function dropAllTables()
}
}

public function getTables()
{
$db = $this->connection->getMongoDB();
$collections = [];

foreach ($db->listCollectionNames() as $collectionName) {
$stats = $db->selectCollection($collectionName)->aggregate([
['$collStats' => ['storageStats' => ['scale' => 1]]],
['$project' => ['storageStats.totalSize' => 1]],
])->toArray();

$collections[] = [
'name' => $collectionName,
'schema' => null,
'size' => $stats[0]?->storageStats?->totalSize ?? null,
'comment' => null,
'collation' => null,
'engine' => null,
];
}

usort($collections, function ($a, $b) {
return $a['name'] <=> $b['name'];
});

return $collections;
}

public function getTableListing()
{
$collections = iterator_to_array($this->connection->getMongoDB()->listCollectionNames());

sort($collections);

return $collections;
}

public function getColumns($table)
{
$stats = $this->connection->getMongoDB()->selectCollection($table)->aggregate([
// Sample 1,000 documents to get a representative sample of the collection
['$sample' => ['size' => 1_000]],
// Convert each document to an array of fields
['$project' => ['fields' => ['$objectToArray' => '$$ROOT']]],
// Unwind to get one document per field
['$unwind' => '$fields'],
// Group by field name, count the number of occurrences and get the types
[
'$group' => [
'_id' => '$fields.k',
'total' => ['$sum' => 1],
'types' => ['$addToSet' => ['$type' => '$fields.v']],
],
],
// Get the most seen field names
['$sort' => ['total' => -1]],
// Limit to 1,000 fields
['$limit' => 1_000],
// Sort by field name
['$sort' => ['_id' => 1]],
], [
'typeMap' => ['array' => 'array'],
'allowDiskUse' => true,
])->toArray();

$columns = [];
foreach ($stats as $stat) {
sort($stat->types);
$type = implode(', ', $stat->types);
$columns[] = [
'name' => $stat->_id,
'type_name' => $type,
'type' => $type,
'collation' => null,
'nullable' => $stat->_id !== '_id',
'default' => null,
'auto_increment' => false,
'comment' => sprintf('%d occurrences', $stat->total),
'generation' => $stat->_id === '_id' ? ['type' => 'objectId', 'expression' => null] : null,
];
}

return $columns;
}

public function getIndexes($table)
{
$indexes = $this->connection->getMongoDB()->selectCollection($table)->listIndexes();

$indexList = [];
foreach ($indexes as $index) {
assert($index instanceof IndexInfo);
$indexList[] = [
'name' => $index->getName(),
'columns' => array_keys($index->getKey()),
'primary' => $index->getKey() === ['_id' => 1],
'type' => match (true) {
$index->isText() => 'text',
$index->is2dSphere() => '2dsphere',
$index->isTtl() => 'ttl',
default => 'default',
},
'unique' => $index->isUnique(),
];
}

return $indexList;
}

public function getForeignKeys($table)
{
return [];
}

/** @inheritdoc */
protected function createBlueprint($table, ?Closure $callback = null)
{
Expand Down
6 changes: 6 additions & 0 deletions tests/ConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,4 +299,10 @@ public function testPingMethod()
$instance = new Connection($config);
$instance->ping();
}

public function testServerVersion()
{
$version = DB::connection('mongodb')->getServerVersion();
$this->assertIsString($version);
}
}
45 changes: 32 additions & 13 deletions tests/EmbeddedRelationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@
use Mockery;
use MongoDB\BSON\ObjectId;
use MongoDB\Laravel\Tests\Models\Address;
use MongoDB\Laravel\Tests\Models\Book;
use MongoDB\Laravel\Tests\Models\Client;
use MongoDB\Laravel\Tests\Models\Group;
use MongoDB\Laravel\Tests\Models\Item;
use MongoDB\Laravel\Tests\Models\Photo;
use MongoDB\Laravel\Tests\Models\Role;
use MongoDB\Laravel\Tests\Models\User;

use function array_merge;
Expand All @@ -25,14 +19,7 @@ class EmbeddedRelationsTest extends TestCase
public function tearDown(): void
{
Mockery::close();

User::truncate();
Book::truncate();
Item::truncate();
Role::truncate();
Client::truncate();
Group::truncate();
Photo::truncate();
}

public function testEmbedsManySave()
Expand Down Expand Up @@ -951,4 +938,36 @@ public function testGetQueueableRelationsEmbedsOne()
$this->assertEquals(['father'], $user->getQueueableRelations());
$this->assertEquals([], $user->father->getQueueableRelations());
}

public function testUnsetPropertyOnEmbed()
{
$user = User::create(['name' => 'John Doe']);
$user->addresses()->save(new Address(['city' => 'New York']));
$user->addresses()->save(new Address(['city' => 'Tokyo']));

// Set property
$user->addresses->first()->city = 'Paris';
$user->addresses->first()->save();

$user = User::where('name', 'John Doe')->first();
$this->assertSame('Paris', $user->addresses->get(0)->city);
$this->assertSame('Tokyo', $user->addresses->get(1)->city);

// Unset property
unset($user->addresses->first()->city);
$user->addresses->first()->save();

$user = User::where('name', 'John Doe')->first();
$this->assertNull($user->addresses->get(0)->city);
$this->assertSame('Tokyo', $user->addresses->get(1)->city);

// Unset and reset property
unset($user->addresses->get(1)->city);
$user->addresses->get(1)->city = 'Kyoto';
$user->addresses->get(1)->save();

$user = User::where('name', 'John Doe')->first();
$this->assertNull($user->addresses->get(0)->city);
$this->assertSame('Kyoto', $user->addresses->get(1)->city);
}
}
Loading

0 comments on commit bd7f587

Please sign in to comment.