From 72be9c27032a095ced9d7a39a32a0dcfefcc0e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 15 Jul 2024 16:15:17 +0200 Subject: [PATCH 1/4] PHPORM-215 Implement Schema::getColumns and getIndexes --- CHANGELOG.md | 3 +- src/Schema/Builder.php | 70 ++++++++++++++++++++++++++++++++++++++++++ tests/SchemaTest.php | 67 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae1bc0adc..4525b0848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to this project will be documented in this file. ## [4.9.0] - coming soon * 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::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) ## [4.6.0] - 2024-07-09 diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index cc016a345..084a62a50 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -6,11 +6,17 @@ use Closure; use MongoDB\Model\CollectionInfo; +use MongoDB\Model\IndexInfo; +use stdClass; +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 @@ -146,6 +152,70 @@ public function getTableListing() return $collections; } + public function getColumns($table) + { + $stats = $this->connection->getMongoDB()->selectCollection($table)->aggregate([ + ['$project' => ['fields' => ['$objectToArray' => '$$ROOT']]], + ['$unwind' => '$fields'], + [ + '$group' => [ + '_id' => '$fields.k', + 'total' => ['$count' => new stdClass()], + 'types' => ['$addToSet' => ['$type' => '$fields.v']], + ], + ], + ['$sort' => ['_id' => 1]], + ], ['typeMap' => ['array' => 'array']])->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) { diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 88951233e..1d99627ce 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -6,8 +6,11 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use MongoDB\BSON\Binary; +use MongoDB\BSON\UTCDateTime; use MongoDB\Laravel\Schema\Blueprint; +use function collect; use function count; class SchemaTest extends TestCase @@ -416,6 +419,70 @@ public function testGetTableListing() $this->assertContains('newcollection_two', $tables); } + public function testGetColumns() + { + $collection = DB::connection('mongodb')->collection('newcollection'); + $collection->insert(['text' => 'value', 'mixed' => ['key' => 'value']]); + $collection->insert(['date' => new UTCDateTime(), 'binary' => new Binary('binary'), 'mixed' => true]); + + $columns = Schema::getColumns('newcollection'); + $this->assertIsArray($columns); + $this->assertCount(5, $columns); + + $columns = collect($columns)->keyBy('name'); + + $columns->each(function ($column) { + $this->assertIsString($column['name']); + $this->assertEquals($column['type'], $column['type_name']); + $this->assertNull($column['collation']); + $this->assertIsBool($column['nullable']); + $this->assertNull($column['default']); + $this->assertFalse($column['auto_increment']); + $this->assertIsString($column['comment']); + }); + + $this->assertEquals('objectId', $columns->get('_id')['type']); + $this->assertEquals('objectId', $columns->get('_id')['generation']['type']); + $this->assertNull($columns->get('text')['generation']); + $this->assertEquals('string', $columns->get('text')['type']); + $this->assertEquals('date', $columns->get('date')['type']); + $this->assertEquals('binData', $columns->get('binary')['type']); + $this->assertEquals('bool, object', $columns->get('mixed')['type']); + $this->assertEquals('2 occurrences', $columns->get('mixed')['comment']); + + // Non-existent collection + $columns = Schema::getColumns('missing'); + $this->assertSame([], $columns); + } + + public function testGetIndexes() + { + Schema::create('newcollection', function (Blueprint $collection) { + $collection->index('mykey1'); + $collection->string('mykey2')->unique('unique_index'); + $collection->string('mykey3')->index(); + }); + $indexes = Schema::getIndexes('newcollection'); + $this->assertIsArray($indexes); + $this->assertCount(4, $indexes); + + $indexes = collect($indexes)->keyBy('name'); + + $indexes->each(function ($index) { + $this->assertIsString($index['name']); + $this->assertIsString($index['type']); + $this->assertIsArray($index['columns']); + $this->assertIsBool($index['unique']); + $this->assertIsBool($index['primary']); + }); + $this->assertTrue($indexes->get('_id_')['primary']); + $this->assertTrue($indexes->get('unique_index_1')['unique']); + + // Non-existent collection + $indexes = Schema::getIndexes('missing'); + $this->assertSame([], $indexes); + } + protected function getIndex(string $collection, string $name) { $collection = DB::getCollection($collection); From 79b429a7cf6a853f5d392480a2e348f0acc0e2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 15 Jul 2024 16:24:28 +0200 Subject: [PATCH 2/4] Fix compatibility with MongoDB 4.4 --- src/Schema/Builder.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index 084a62a50..0787fb79a 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -7,7 +7,6 @@ use Closure; use MongoDB\Model\CollectionInfo; use MongoDB\Model\IndexInfo; -use stdClass; use function array_keys; use function assert; @@ -160,7 +159,7 @@ public function getColumns($table) [ '$group' => [ '_id' => '$fields.k', - 'total' => ['$count' => new stdClass()], + 'total' => ['$sum' => 1], 'types' => ['$addToSet' => ['$type' => '$fields.v']], ], ], From cf3eb716e1e7c6c0f6b614deb6da15e7e187266a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 16 Jul 2024 09:59:29 +0200 Subject: [PATCH 3/4] Use $sample to extract field names --- src/Schema/Builder.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index 0787fb79a..b50195563 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -154,8 +154,13 @@ public function getTableListing() public function getColumns($table) { $stats = $this->connection->getMongoDB()->selectCollection($table)->aggregate([ + // Sample 10,000 documents to get a representative sample of the collection + ['$sample' => ['size' => 10_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', @@ -163,7 +168,10 @@ public function getColumns($table) 'types' => ['$addToSet' => ['$type' => '$fields.v']], ], ], + // Sort by field name ['$sort' => ['_id' => 1]], + // Limit to 1,000 fields + ['$limit' => 1000], ], ['typeMap' => ['array' => 'array']])->toArray(); $columns = []; From cda02a9862db04c12d90079eb70831c950a406ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 16 Jul 2024 15:21:31 +0200 Subject: [PATCH 4/4] Reduce $sample size to fit with Atlas M0 --- src/Schema/Builder.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index b50195563..32fc9f482 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -154,8 +154,8 @@ public function getTableListing() public function getColumns($table) { $stats = $this->connection->getMongoDB()->selectCollection($table)->aggregate([ - // Sample 10,000 documents to get a representative sample of the collection - ['$sample' => ['size' => 10_000]], + // 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 @@ -168,11 +168,16 @@ public function getColumns($table) '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]], - // Limit to 1,000 fields - ['$limit' => 1000], - ], ['typeMap' => ['array' => 'array']])->toArray(); + ], [ + 'typeMap' => ['array' => 'array'], + 'allowDiskUse' => true, + ])->toArray(); $columns = []; foreach ($stats as $stat) {