diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index ac586d8..ac555e6 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -487,4 +487,28 @@ protected function indexCommand($type, $columns, $index, $algorithm = null) return $command; } + + /** + * @inheritDoc + * @param list $columns + */ + protected function createIndexName($type, array $columns) + { + [$schema, $table] = $this->connection + ->getSchemaBuilder() + ->parseSchemaAndTable($this->table); + + if ($this->connection->getConfig('prefix_indexes')) { + $table = $this->connection->getTablePrefix() . $table; + } + + $index = strtolower($table . '_' . implode('_', $columns) . '_' . $type); + + if ($type !== 'foreign' && $schema !== null) { + $index = $schema . '.' . $index; + } + + return str_replace('-', '_', $index); + } + } diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index 6e573aa..608451c 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -60,6 +60,17 @@ public function setDatabaseOptions(array $options): void $connection->statement("ALTER DATABASE `{$name}` SET OPTIONS ({$line})"); } + /** + * Create a named schema with the given name. + * + * @param string $name + * @return void + */ + public function createNamedSchema(string $name): void + { + $this->connection->statement("CREATE SCHEMA {$this->grammar->wrap($name)}"); + } + /** * @deprecated Use Blueprint::dropIndex() instead. Will be removed in v10.0. * @@ -127,7 +138,7 @@ public function dropAllTables() // add parents counter foreach ($tables as $table) { - $sortedTables[$table['name']] = ['parents' => 0, ...$table]; + $sortedTables[$table['schema_qualified_name']] = ['parents' => 0, ...$table]; } // loop through all tables and count how many parents they have @@ -150,9 +161,9 @@ public function dropAllTables() // drop foreign keys first (otherwise index queries will include them) $queries = []; foreach ($sortedTables as $tableData) { - $tableName = $tableData['name']; - $foreigns = $this->getForeignKeys($tableName); - $blueprint = $this->createBlueprint($tableName); + $sqn = $tableData['schema_qualified_name']; + $foreigns = $this->getForeignKeys($sqn); + $blueprint = $this->createBlueprint($sqn); foreach ($foreigns as $foreign) { $blueprint->dropForeign($foreign['name']); } @@ -164,18 +175,23 @@ public function dropAllTables() // drop indexes and tables $queries = []; foreach ($sortedTables as $tableData) { - $tableName = $tableData['name']; - $indexes = $this->getIndexListing($tableName); - $blueprint = $this->createBlueprint($tableName); + $schema = $tableData['schema'] ?? null; + $sqn = $tableData['schema_qualified_name']; + $indexes = $this->getIndexListing($sqn); + $blueprint = $this->createBlueprint($sqn); foreach ($indexes as $index) { if ($index === 'PRIMARY_KEY') { continue; } + if ($schema !== null) { + $index = $schema . '.' . $index; + } $blueprint->dropIndex($index); } $blueprint->drop(); array_push($queries, ...$blueprint->toSql()); } + $connection->runDdlBatch($queries); } } diff --git a/src/Schema/Grammar.php b/src/Schema/Grammar.php index 1aab25c..3745e40 100644 --- a/src/Schema/Grammar.php +++ b/src/Schema/Grammar.php @@ -40,31 +40,30 @@ class Grammar extends BaseGrammar protected $modifiers = ['Nullable', 'Default', 'GeneratedAs', 'Invisible', 'Increment', 'UseSequence']; /** - * Compile the query to determine the tables. - * - * @param $schema - * @return string + * @inheritDoc */ public function compileTables($schema) { return implode(' ', [ 'select', implode(', ', [ - 'table_name as name', + 'table_name as `name`', 'table_schema as `schema`', - 'parent_table_name as parent', + 'parent_table_name as `parent`', ]), - 'from information_schema.tables', - 'where table_type = \'BASE TABLE\'', - 'and table_schema = \'\'', + 'from `information_schema`.`tables`', + 'where `table_type` = \'BASE TABLE\'', + (match (true) { + is_array($schema) => 'and `table_schema` in (' . $this->quoteString($schema) . ')', + !is_null($schema) => 'and `table_schema` = ' . $this->quoteString($schema), + default => '', + }), + 'order by `table_schema`, `table_name`' ]); } /** - * Compile the query to determine the columns. - * - * @param string $table - * @return string + * @inheritDoc */ public function compileColumns($schema, $table) { @@ -76,17 +75,15 @@ public function compileColumns($schema, $table) 'is_nullable as `nullable`', 'column_default as `default`', ]), - 'from information_schema.columns', - 'where table_name = ' . $this->quoteString($table), + 'from `information_schema`.`columns`', + 'where `table_name` = ' . $this->quoteString($table), + 'and table_schema = ' . $this->quoteString($schema ?? ''), + 'order by `ordinal_position` asc', ]); } /** - * Compile the query to determine the list of indexes. - * - * @param string|null $schema - * @param $table - * @return string + * @inheritDoc */ public function compileIndexes($schema, $table) { @@ -100,18 +97,14 @@ public function compileIndexes($schema, $table) ]), 'from information_schema.indexes as i', 'join information_schema.index_columns as c on i.table_schema = c.table_schema and i.table_name = c.table_name and i.index_name = c.index_name', - 'where i.table_schema = ' . $this->quoteString(''), - 'and i.table_name = ' . $this->quoteString($table), - 'group by i.index_name, i.index_type, i.is_unique', + 'where i.table_name = ' . $this->quoteString($table), + 'and i.table_schema = ' . $this->quoteString($schema ?? ''), + 'group by i.index_name, i.index_type, i.is_unique, i.table_schema', ]); } /** - * Compile the query to determine the list of foreign keys. - * - * @param string|null $schema - * @param $table - * @return string + * @inheritDoc */ public function compileForeignKeys($schema, $table) { @@ -129,8 +122,8 @@ public function compileForeignKeys($schema, $table) 'from information_schema.key_column_usage kc', 'join information_schema.referential_constraints rc on kc.constraint_name = rc.constraint_name', 'join information_schema.constraint_column_usage cc on kc.constraint_name = cc.constraint_name', - 'where kc.table_schema = ""', - 'and kc.table_name = ' . $this->quoteString($table), + 'where kc.table_name = ' . $this->quoteString($table), + 'and kc.table_schema = ' . $this->quoteString($schema ?? ''), 'group by kc.constraint_name, cc.table_schema, cc.table_name, rc.update_rule, rc.delete_rule', ]); } diff --git a/tests/Schema/BuilderTestLast.php b/tests/Schema/BuilderTestLast.php index cee5386..b336da1 100644 --- a/tests/Schema/BuilderTestLast.php +++ b/tests/Schema/BuilderTestLast.php @@ -203,18 +203,26 @@ public function testCreateInterleavedTable(): void $this->assertTrue($sb->hasTable(self::TABLE_NAME_RELATION_CHILD_INTERLEAVED)); } - public function test_getTables(): void + public function test_getTables__for_default_schema(): void { $conn = $this->getDefaultConnection(); $sb = $conn->getSchemaBuilder(); + $schema = $this->generateNamedSchemaName(); $table = $this->generateTableName(class_basename(__CLASS__)); + $sqn = "{$schema}.{$table}"; $sb->create($table, function (Blueprint $table) { $table->uuid('id'); $table->primary('id'); }); - $row = Arr::first($sb->getTables(), fn($row) => $row['name'] === $table); + $sb->createNamedSchema($schema); + $sb->create($sqn, function (Blueprint $table) { + $table->uuid('id'); + $table->primary('id'); + }); + + $row = Arr::sole($sb->getTables(''), fn($row) => $row['name'] === $table); $this->assertSame([ 'name' => $table, @@ -228,6 +236,77 @@ public function test_getTables(): void ], $row); } + public function test_getTables__for_named_schema(): void + { + $conn = $this->getDefaultConnection(); + $sb = $conn->getSchemaBuilder(); + $schema = $this->generateNamedSchemaName(); + $table = $this->generateTableName(class_basename(__CLASS__)); + + $sb->createNamedSchema($schema); + $sb->create("{$schema}.{$table}", function (Blueprint $table) { + $table->uuid('id'); + $table->primary('id'); + }); + + $tables = $sb->getTables($schema); + $row = Arr::first($tables, fn($row) => $row['name'] === $table); + + $this->assertSame([ + 'name' => $table, + 'schema' => $schema, + 'schema_qualified_name' => "{$schema}.{$table}", + 'parent' => null, + 'size' => null, + 'comment' => null, + 'collation' => null, + 'engine' => null, + ], $row); + } + + public function test_getTables__for_default_and_named_schema(): void + { + $conn = $this->getDefaultConnection(); + $sb = $conn->getSchemaBuilder(); + $schema = $this->generateNamedSchemaName(); + $table = $this->generateTableName(class_basename(__CLASS__)); + + $sb->create($table, function (Blueprint $table) { + $table->uuid('id'); + $table->primary('id'); + }); + + $sb->createNamedSchema($schema); + $sb->create("{$schema}.{$table}", function (Blueprint $table) { + $table->uuid('id'); + $table->primary('id'); + }); + + $tables = $sb->getTables(); + + $this->assertSame([ + 'name' => $table, + 'schema' => null, + 'schema_qualified_name' => $table, + 'parent' => null, + 'size' => null, + 'comment' => null, + 'collation' => null, + 'engine' => null, + ], Arr::sole($tables, fn($row) => $row['schema'] === null && $row['name'] === $table)); + + $this->assertSame([ + 'name' => $table, + 'schema' => $schema, + 'schema_qualified_name' => "{$schema}.{$table}", + 'parent' => null, + 'size' => null, + 'comment' => null, + 'collation' => null, + 'engine' => null, + ], Arr::sole($tables, fn($row) => $row['schema'] === $schema && $row['name'] === $table)); + } + public function test_getColumns_with_nullable(): void { $conn = $this->getDefaultConnection(); @@ -274,10 +353,37 @@ public function test_getColumns_with_default(): void ], Arr::first($sb->getColumns($table))); } - public function test_getTableListing(): void + public function test_getColumns__for_named_schema_table(): void { $conn = $this->getDefaultConnection(); $sb = $conn->getSchemaBuilder(); + $schema = $this->generateNamedSchemaName(); + $table = $this->generateTableName(class_basename(__CLASS__)); + $fqtn = "{$schema}.{$table}"; + + $sb->createNamedSchema($schema); + $sb->create($fqtn, function (Blueprint $table) { + $table->string('id', 1)->default('a')->primary(); + }); + + $this->assertSame([ + 'name' => 'id', + 'type_name' => 'STRING', + 'type' => 'STRING(1)', + 'collation' => null, + 'nullable' => false, + 'default' => '"a"', + 'auto_increment' => false, + 'comment' => null, + 'generation' => null, + ], Arr::sole($sb->getColumns($fqtn))); + } + + public function test_getTableListing__for_default_schema(): void + { + $conn = $this->getDefaultConnection(); + $sb = $conn->getSchemaBuilder(); + $schema = $this->generateNamedSchemaName(); $table = $this->generateTableName(class_basename(__CLASS__)); $sb->create($table, function (Blueprint $table) { @@ -285,10 +391,37 @@ public function test_getTableListing(): void $table->primary('id'); }); - $this->assertContains($table, $sb->getTableListing()); + $sb->createNamedSchema($schema); + $fqtn = "{$schema}.{$table}"; + $sb->create($fqtn, function (Blueprint $table) { + $table->uuid('id'); + $table->primary('id'); + }); + $listings = $sb->getTableListing(); + + $this->assertContains($table, $listings); + $this->assertContains($fqtn, $listings); } - public function test_getIndexes(): void + public function test_getTableListing__for_named_schema(): void + { + $conn = $this->getDefaultConnection(); + $sb = $conn->getSchemaBuilder(); + $schema = $this->generateNamedSchemaName(); + $table = $this->generateTableName(class_basename(__CLASS__)); + $fqtn = "{$schema}.{$table}"; + + $sb->createNamedSchema($schema); + $sb->create($fqtn, function (Blueprint $table) { + $table->uuid('id'); + $table->primary('id'); + }); + + $this->assertContains($fqtn, $sb->getTableListing($schema)); + $this->assertContains($table, $sb->getTableListing($schema, false)); + } + + public function test_getIndexes__for_default_schema(): void { $conn = $this->getDefaultConnection(); $sb = $conn->getSchemaBuilder(); @@ -299,6 +432,15 @@ public function test_getIndexes(): void $table->index('something'); }); + $schema = $this->generateNamedSchemaName(); + $fqtn = "{$schema}.{$table}"; + $sb->createNamedSchema($schema); + $sb->create($fqtn, function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->uuid('something'); + $table->index('something'); + }); + $this->assertSame([ [ 'name' => strtolower($table) . '_something_index', @@ -317,6 +459,39 @@ public function test_getIndexes(): void ], $sb->getIndexes($table)); } + public function test_getIndexes__with_named_schema(): void + { + $conn = $this->getDefaultConnection(); + $sb = $conn->getSchemaBuilder(); + $schema = $this->generateNamedSchemaName(); + $table = $this->generateTableName(class_basename(__CLASS__)); + $fqtn = "{$schema}.{$table}"; + + $sb->createNamedSchema($schema); + $sb->create($fqtn, function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->uuid('something'); + $table->index('something'); + }); + + $this->assertSame([ + [ + 'name' => strtolower($table) . '_something_index', + 'columns' => ['something'], + 'type' => 'index', + 'unique' => false, + 'primary' => false, + ], + [ + 'name' => 'PRIMARY_KEY', + 'columns' => ['id'], + 'type' => 'primary_key', + 'unique' => true, + 'primary' => true, + ], + ], $sb->getIndexes($fqtn)); + } + public function test_getIndexListing(): void { $conn = $this->getDefaultConnection(); @@ -334,6 +509,27 @@ public function test_getIndexListing(): void ], $sb->getIndexListing($table)); } + public function test_getIndexListing__with_named_schema(): void + { + $conn = $this->getDefaultConnection(); + $sb = $conn->getSchemaBuilder(); + $schema = $this->generateNamedSchemaName(); + $table = $this->generateTableName(class_basename(__CLASS__)); + $fqtn = "{$schema}.{$table}"; + + $sb->createNamedSchema($schema); + $sb->create($fqtn, function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->uuid('something'); + $table->index('something'); + }); + + $this->assertSame([ + strtolower($table) . '_something_index', + 'PRIMARY_KEY', + ], $sb->getIndexListing($fqtn)); + } + public function test_getForeignKeys(): void { $conn = $this->getDefaultConnection(); @@ -364,6 +560,41 @@ public function test_getForeignKeys(): void ]], $sb->getForeignKeys($table2)); } + public function test_getForeignKeys__with_named_schema(): void + { + $conn = $this->getDefaultConnection(); + $sb = $conn->getSchemaBuilder(); + $schema = $this->generateNamedSchemaName(); + $table1 = $this->generateTableName(class_basename(__CLASS__) . '_1'); + $fqtn1 = "{$schema}.{$table1}"; + + $sb->createNamedSchema($schema); + $sb->create($fqtn1, function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->uuid('something'); + $table->index('something'); + }); + + $table2 = $this->generateTableName(class_basename(__CLASS__) . '_2'); + $fqtn2 = "{$schema}.{$table2}"; + $sb->create($fqtn2, function (Blueprint $table) use ($table1, $fqtn1) { + $table->uuid('table2_id')->primary(); + $table->uuid('other_id'); + $table->index('other_id'); + $table->foreign('other_id')->references('id')->on($fqtn1); + }); + + $this->assertSame([[ + 'name' => strtolower($table2) . '_other_id_foreign', + 'columns' => ['other_id'], + 'foreign_schema' => $schema, + 'foreign_table' => $table1, + 'foreign_columns' => ['id'], + 'on_update' => "no action", + 'on_delete' => "no action", + ]], $sb->getForeignKeys($fqtn2)); + } + public function test_dropAllTables(): void { $conn = $this->getDefaultConnection(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 3a6c71a..32f6473 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -29,7 +29,9 @@ use Google\Cloud\Spanner\SpannerClient; use Illuminate\Foundation\Application; use Illuminate\Support\Carbon; +use Illuminate\Support\Str; use Ramsey\Uuid\Uuid; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; /** * @property Application $app @@ -64,6 +66,11 @@ protected function generateTableName(string $prefix = 'Temp'): string return $prefix . '_' . Carbon::now()->format('Ymd_His_v'); } + protected function generateNamedSchemaName(): string + { + return 'sch' . time() . Str::random(8); + } + /** * @return string */ @@ -125,11 +132,18 @@ protected function getAlternativeConnection(): Connection */ protected function setUpEmulatorInstance(Connection $conn): void { - $spanner = new SpannerClient((array) $conn->getConfig('client')); + $spanner = new SpannerClient((array) $conn->getConfig('client') + [ + 'authCache' => new FilesystemAdapter( + namespace: $conn->getName() . '_auth', + directory: $this->app->storagePath('framework/spanner'), + ), + ]); $name = (string) $conn->getConfig('instance'); if (!$spanner->instance($name)->exists()) { $config = $spanner->instanceConfiguration('emulator-config'); - $spanner->createInstance($config, $name)->pollUntilComplete(); + $spanner->createInstance($config, $name)->pollUntilComplete([ + 'pollingIntervalSeconds' => 0.001, + ]); logger()?->debug('Created Spanner Emulator Instance: ' . $name); } }