diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index 692dfdee2f67..da95adbbd800 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -1650,17 +1650,19 @@ public function withTablePrefix(Grammar $grammar) * Execute the given callback without table prefix. * * @param \Closure $callback - * @return void + * @return mixed */ - public function withoutTablePrefix(Closure $callback): void + public function withoutTablePrefix(Closure $callback): mixed { $tablePrefix = $this->getTablePrefix(); $this->setTablePrefix(''); - $callback($this); - - $this->setTablePrefix($tablePrefix); + try { + return $callback($this); + } finally { + $this->setTablePrefix($tablePrefix); + } } /** diff --git a/src/Illuminate/Database/Console/DatabaseInspectionCommand.php b/src/Illuminate/Database/Console/DatabaseInspectionCommand.php index 00fc9257690e..8faab04147ab 100644 --- a/src/Illuminate/Database/Console/DatabaseInspectionCommand.php +++ b/src/Illuminate/Database/Console/DatabaseInspectionCommand.php @@ -47,20 +47,4 @@ protected function getConfigFromDatabase($database) return Arr::except(config('database.connections.'.$database), ['password']); } - - /** - * Remove the table prefix from a table name, if it exists. - * - * @param \Illuminate\Database\ConnectionInterface $connection - * @param string $table - * @return string - */ - protected function withoutTablePrefix(ConnectionInterface $connection, string $table) - { - $prefix = $connection->getTablePrefix(); - - return str_starts_with($table, $prefix) - ? substr($table, strlen($prefix)) - : $table; - } } diff --git a/src/Illuminate/Database/Console/ShowCommand.php b/src/Illuminate/Database/Console/ShowCommand.php index 3abc69bbe83e..64c80572b927 100644 --- a/src/Illuminate/Database/Console/ShowCommand.php +++ b/src/Illuminate/Database/Console/ShowCommand.php @@ -8,7 +8,6 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Number; -use Illuminate\Support\Stringable; use Symfony\Component\Console\Attribute\AsCommand; #[AsCommand(name: 'db:show')] @@ -80,9 +79,10 @@ protected function tables(ConnectionInterface $connection, Builder $schema) return (new Collection($schema->getTables()))->map(fn ($table) => [ 'table' => $table['name'], 'schema' => $table['schema'], + 'schema_qualified_name' => $table['schema_qualified_name'], 'size' => $table['size'], 'rows' => $this->option('counts') - ? ($connection->table($table['schema'] ? $table['schema'].'.'.$table['name'] : $table['name'])->count()) + ? $connection->withoutTablePrefix(fn ($connection) => $connection->table($table['schema_qualified_name'])->count()) : null, 'engine' => $table['engine'], 'collation' => $table['collation'], @@ -100,11 +100,10 @@ protected function tables(ConnectionInterface $connection, Builder $schema) protected function views(ConnectionInterface $connection, Builder $schema) { return (new Collection($schema->getViews())) - ->reject(fn ($view) => (new Stringable($view['name']))->startsWith(['pg_catalog', 'information_schema', 'spt_'])) ->map(fn ($view) => [ 'view' => $view['name'], 'schema' => $view['schema'], - 'rows' => $connection->table($view['schema'] ? $view['schema'].'.'.$view['name'] : $view['name'])->count(), + 'rows' => $connection->withoutTablePrefix(fn ($connection) => $connection->table($view['schema_qualified_name'])->count()), ]); } diff --git a/src/Illuminate/Database/Console/TableCommand.php b/src/Illuminate/Database/Console/TableCommand.php index ccde5c29680c..fde40a78f8a3 100644 --- a/src/Illuminate/Database/Console/TableCommand.php +++ b/src/Illuminate/Database/Console/TableCommand.php @@ -39,10 +39,8 @@ class TableCommand extends DatabaseInspectionCommand public function handle(ConnectionResolverInterface $connections) { $connection = $connections->connection($this->input->getOption('database')); - $schema = $connection->getSchemaBuilder(); - $tables = (new Collection($schema->getTables())) - ->keyBy(fn ($table) => $table['schema'] ? $table['schema'].'.'.$table['name'] : $table['name']) - ->all(); + $tables = (new Collection($connection->getSchemaBuilder()->getTables())) + ->keyBy('schema_qualified_name')->all(); $tableName = $this->argument('table') ?: select( 'Which table would you like to inspect?', @@ -57,16 +55,22 @@ public function handle(ConnectionResolverInterface $connections) return 1; } - $tableName = ($table['schema'] ? $table['schema'].'.' : '').$this->withoutTablePrefix($connection, $table['name']); + [$columns, $indexes, $foreignKeys] = $connection->withoutTablePrefix(function ($connection) use ($table) { + $schema = $connection->getSchemaBuilder(); + $tableName = $table['schema_qualified_name']; - $columns = $this->columns($schema, $tableName); - $indexes = $this->indexes($schema, $tableName); - $foreignKeys = $this->foreignKeys($schema, $tableName); + return [ + $this->columns($schema, $tableName), + $this->indexes($schema, $tableName), + $this->foreignKeys($schema, $tableName), + ]; + }); $data = [ 'table' => [ 'schema' => $table['schema'], 'name' => $table['name'], + 'schema_qualified_name' => $table['schema_qualified_name'], 'columns' => count($columns), 'size' => $table['size'], 'comment' => $table['comment'], @@ -205,7 +209,7 @@ protected function displayForCli(array $data) $this->newLine(); - $this->components->twoColumnDetail(''.($table['schema'] ? $table['schema'].'.'.$table['name'] : $table['name']).'', $table['comment'] ? ''.$table['comment'].'' : null); + $this->components->twoColumnDetail(''.$table['schema_qualified_name'].'', $table['comment'] ? ''.$table['comment'].'' : null); $this->components->twoColumnDetail('Columns', $table['columns']); if (! is_null($table['size'])) { diff --git a/src/Illuminate/Database/Grammar.php b/src/Illuminate/Database/Grammar.php index 28c1a2f10f24..187d1348ad30 100755 --- a/src/Illuminate/Database/Grammar.php +++ b/src/Illuminate/Database/Grammar.php @@ -40,9 +40,10 @@ public function wrapArray(array $values) * Wrap a table in keyword identifiers. * * @param \Illuminate\Contracts\Database\Query\Expression|string $table + * @param string|null $prefix * @return string */ - public function wrapTable($table) + public function wrapTable($table, $prefix = null) { if ($this->isExpression($table)) { return $this->getValue($table); @@ -55,18 +56,20 @@ public function wrapTable($table) return $this->wrapAliasedTable($table); } + $prefix ??= $this->tablePrefix; + // If the table being wrapped has a custom schema name specified, we need to // prefix the last segment as the table name then wrap each segment alone // and eventually join them both back together using the dot connector. if (str_contains($table, '.')) { - $table = substr_replace($table, '.'.$this->tablePrefix, strrpos($table, '.'), 1); + $table = substr_replace($table, '.'.$prefix, strrpos($table, '.'), 1); return (new Collection(explode('.', $table))) ->map($this->wrapValue(...)) ->implode('.'); } - return $this->wrapValue($this->tablePrefix.$table); + return $this->wrapValue($prefix.$table); } /** diff --git a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php index 0999f4ec6f86..6dc65a07c034 100755 --- a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php @@ -439,8 +439,12 @@ protected function compileDeleteWithJoinsOrLimit(Builder $query) */ public function compileTruncate(Builder $query) { + [$schema, $table] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($query->from); + + $schema = $schema ? $this->wrapValue($schema).'.' : ''; + return [ - 'delete from sqlite_sequence where name = ?' => [$this->getTablePrefix().$query->from], + 'delete from '.$schema.'sqlite_sequence where name = ?' => [$this->getTablePrefix().$table], 'delete from '.$this->wrapTable($query->from) => [], ]; } diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index ff1ac5cd3e68..0b2dd381c1ab 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -557,12 +557,13 @@ protected function wrapJsonBooleanValue($value) * Wrap a table in keyword identifiers. * * @param \Illuminate\Contracts\Database\Query\Expression|string $table + * @param string|null $prefix * @return string */ - public function wrapTable($table) + public function wrapTable($table, $prefix = null) { if (! $this->isExpression($table)) { - return $this->wrapTableValuedFunction(parent::wrapTable($table)); + return $this->wrapTableValuedFunction(parent::wrapTable($table, $prefix)); } return $this->getValue($table); diff --git a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php index 80babf37fb6b..bedf9a4213ef 100755 --- a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php +++ b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php @@ -44,6 +44,7 @@ public function processTypes($results) return [ 'name' => $result->name, 'schema' => $result->schema, + 'schema_qualified_name' => $result->schema.'.'.$result->name, 'implicit' => (bool) $result->implicit, 'type' => match (strtolower($result->type)) { 'b' => 'base', diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 936e6245b170..c2b1e8b782e9 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -36,6 +36,25 @@ public function processInsertGetId(Builder $query, $sql, $values, $sequence = nu return is_numeric($id) ? (int) $id : $id; } + /** + * Process the results of a schemas query. + * + * @param array $results + * @return array + */ + public function processSchemas($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'path' => $result->path ?? null, // SQLite Only... + 'default' => (bool) $result->default, + ]; + }, $results); + } + /** * Process the results of a tables query. * @@ -49,7 +68,8 @@ public function processTables($results) return [ 'name' => $result->name, - 'schema' => $result->schema ?? null, // PostgreSQL and SQL Server + 'schema' => $result->schema ?? null, + 'schema_qualified_name' => isset($result->schema) ? $result->schema.'.'.$result->name : $result->name, 'size' => isset($result->size) ? (int) $result->size : null, 'comment' => $result->comment ?? null, // MySQL and PostgreSQL 'collation' => $result->collation ?? null, // MySQL only @@ -71,7 +91,8 @@ public function processViews($results) return [ 'name' => $result->name, - 'schema' => $result->schema ?? null, // PostgreSQL and SQL Server + 'schema' => $result->schema ?? null, + 'schema_qualified_name' => isset($result->schema) ? $result->schema.'.'.$result->name : $result->name, 'definition' => $result->definition, ]; }, $results); diff --git a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php index dadeaeb467ad..7062c2cd1d7b 100644 --- a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php @@ -104,7 +104,7 @@ public function processForeignKeys($results) return [ 'name' => null, 'columns' => explode(',', $result->columns), - 'foreign_schema' => null, + 'foreign_schema' => $result->foreign_schema, 'foreign_table' => $result->foreign_table, 'foreign_columns' => explode(',', $result->foreign_columns), 'on_update' => strtolower($result->on_update), diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 38cadfbd2794..a42f9f28802b 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -147,6 +147,18 @@ public function dropDatabaseIfExists($name) throw new LogicException('This database driver does not support dropping databases.'); } + /** + * Get the schemas that belong to the connection. + * + * @return array + */ + public function getSchemas() + { + return $this->connection->getPostProcessor()->processSchemas( + $this->connection->selectFromWriteConnection($this->grammar->compileSchemas()) + ); + } + /** * Determine if the given table exists. * @@ -155,9 +167,15 @@ public function dropDatabaseIfExists($name) */ public function hasTable($table) { + [$schema, $table] = $this->parseSchemaAndTable($table); + $table = $this->connection->getTablePrefix().$table; - foreach ($this->getTables() as $value) { + if ($sql = $this->grammar->compileTableExists($schema, $table)) { + return (bool) $this->connection->scalar($sql); + } + + foreach ($this->getTables($schema ?? $this->getCurrentSchemaName()) as $value) { if (strtolower($table) === strtolower($value['name'])) { return true; } @@ -174,9 +192,11 @@ public function hasTable($table) */ public function hasView($view) { + [$schema, $view] = $this->parseSchemaAndTable($view); + $view = $this->connection->getTablePrefix().$view; - foreach ($this->getViews() as $value) { + foreach ($this->getViews($schema ?? $this->getCurrentSchemaName()) as $value) { if (strtolower($view) === strtolower($value['name'])) { return true; } @@ -186,47 +206,57 @@ public function hasView($view) } /** - * Get the tables that belong to the database. + * Get the tables that belong to the connection. * + * @param string|string[]|null $schema * @return array */ - public function getTables() + public function getTables($schema = null) { return $this->connection->getPostProcessor()->processTables( - $this->connection->selectFromWriteConnection($this->grammar->compileTables()) + $this->connection->selectFromWriteConnection($this->grammar->compileTables($schema)) ); } /** - * Get the names of the tables that belong to the database. + * Get the names of the tables that belong to the connection. * + * @param string|string[]|null $schema + * @param bool $schemaQualified * @return array */ - public function getTableListing() + public function getTableListing($schema = null, $schemaQualified = true) { - return array_column($this->getTables(), 'name'); + return array_column( + $this->getTables($schema), + $schemaQualified ? 'schema_qualified_name' : 'name' + ); } /** - * Get the views that belong to the database. + * Get the views that belong to the connection. * + * @param string|string[]|null $schema * @return array */ - public function getViews() + public function getViews($schema = null) { return $this->connection->getPostProcessor()->processViews( - $this->connection->selectFromWriteConnection($this->grammar->compileViews()) + $this->connection->selectFromWriteConnection($this->grammar->compileViews($schema)) ); } /** - * Get the user-defined types that belong to the database. + * Get the user-defined types that belong to the connection. * + * @param string|string[]|null $schema * @return array */ - public function getTypes() + public function getTypes($schema = null) { - throw new LogicException('This database driver does not support user-defined types.'); + return $this->connection->getPostProcessor()->processTypes( + $this->connection->selectFromWriteConnection($this->grammar->compileTypes($schema)) + ); } /** @@ -333,10 +363,14 @@ public function getColumnListing($table) */ public function getColumns($table) { + [$schema, $table] = $this->parseSchemaAndTable($table); + $table = $this->connection->getTablePrefix().$table; return $this->connection->getPostProcessor()->processColumns( - $this->connection->selectFromWriteConnection($this->grammar->compileColumns($table)) + $this->connection->selectFromWriteConnection( + $this->grammar->compileColumns($schema, $table) + ) ); } @@ -348,10 +382,14 @@ public function getColumns($table) */ public function getIndexes($table) { + [$schema, $table] = $this->parseSchemaAndTable($table); + $table = $this->connection->getTablePrefix().$table; return $this->connection->getPostProcessor()->processIndexes( - $this->connection->selectFromWriteConnection($this->grammar->compileIndexes($table)) + $this->connection->selectFromWriteConnection( + $this->grammar->compileIndexes($schema, $table) + ) ); } @@ -400,10 +438,14 @@ public function hasIndex($table, $index, $type = null) */ public function getForeignKeys($table) { + [$schema, $table] = $this->parseSchemaAndTable($table); + $table = $this->connection->getTablePrefix().$table; return $this->connection->getPostProcessor()->processForeignKeys( - $this->connection->selectFromWriteConnection($this->grammar->compileForeignKeys($table)) + $this->connection->selectFromWriteConnection( + $this->grammar->compileForeignKeys($schema, $table) + ) ); } @@ -597,6 +639,55 @@ protected function createBlueprint($table, ?Closure $callback = null) return Container::getInstance()->make(Blueprint::class, compact('connection', 'table', 'callback', 'prefix')); } + /** + * Get the names of the current schemas for the connection. + * + * @return string[]|null + */ + public function getCurrentSchemaListing() + { + return null; + } + + /** + * Get the default schema name for the connection. + * + * @return string|null + */ + public function getCurrentSchemaName() + { + return $this->getCurrentSchemaListing()[0] ?? null; + } + + /** + * Parse the given database object reference and extract the schema and table. + * + * @param string $reference + * @param string|bool|null $withDefaultSchema + * @return array + */ + public function parseSchemaAndTable($reference, $withDefaultSchema = null) + { + $segments = explode('.', $reference); + + if (count($segments) > 2) { + throw new InvalidArgumentException( + "Using three-part references is not supported, you may use `Schema::connection('{$segments[0]}')` instead." + ); + } + + $table = $segments[1] ?? $segments[0]; + + $schema = match (true) { + isset($segments[1]) => $segments[0], + is_string($withDefaultSchema) => $withDefaultSchema, + $withDefaultSchema => $this->getCurrentSchemaName(), + default => null, + }; + + return [$schema, $table]; + } + /** * Get the database connection instance. * diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index b21fcf23b65d..fd78be04e96c 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -64,6 +64,109 @@ public function compileDropDatabaseIfExists($name) throw new LogicException('This database driver does not support dropping databases.'); } + /** + * Compile the query to determine the schemas. + * + * @return string + */ + public function compileSchemas() + { + throw new RuntimeException('This database driver does not support retrieving schemas.'); + } + + /** + * Compile the query to determine if the given table exists. + * + * @param string|null $schema + * @param string $table + * @return string|null + */ + public function compileTableExists($schema, $table) + { + // + } + + /** + * Compile the query to determine the tables. + * + * @param string|string[]|null $schema + * @return string + * + * @throws \RuntimeException + */ + public function compileTables($schema) + { + throw new RuntimeException('This database driver does not support retrieving tables.'); + } + + /** + * Compile the query to determine the views. + * + * @param string|string[]|null $schema + * @return string + * + * @throws \RuntimeException + */ + public function compileViews($schema) + { + throw new RuntimeException('This database driver does not support retrieving views.'); + } + + /** + * Compile the query to determine the user-defined types. + * + * @param string|string[]|null $schema + * @return string + * + * @throws \RuntimeException + */ + public function compileTypes($schema) + { + throw new RuntimeException('This database driver does not support retrieving user-defined types.'); + } + + /** + * Compile the query to determine the columns. + * + * @param string|null $schema + * @param string $table + * @return string + * + * @throws \RuntimeException + */ + public function compileColumns($schema, $table) + { + throw new RuntimeException('This database driver does not support retrieving columns.'); + } + + /** + * Compile the query to determine the indexes. + * + * @param string|null $schema + * @param string $table + * @return string + * + * @throws \RuntimeException + */ + public function compileIndexes($schema, $table) + { + throw new RuntimeException('This database driver does not support retrieving indexes.'); + } + + /** + * Compile the query to determine the foreign keys. + * + * @param string|null $schema + * @param string $table + * @return string + * + * @throws \RuntimeException + */ + public function compileForeignKeys($schema, $table) + { + throw new RuntimeException('This database driver does not support retrieving foreign keys.'); + } + /** * Compile a rename column command. * @@ -343,12 +446,14 @@ public function prefixArray($prefix, array $values) * Wrap a table in keyword identifiers. * * @param mixed $table + * @param string|null $prefix * @return string */ - public function wrapTable($table) + public function wrapTable($table, $prefix = null) { return parent::wrapTable( - $table instanceof Blueprint ? $table->getTable() : $table + $table instanceof Blueprint ? $table->getTable() : $table, + $prefix ); } diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 456210c08da7..5012fa4d48d3 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -77,19 +77,31 @@ public function compileDropDatabaseIfExists($name) ); } + /** + * Compile the query to determine the schemas. + * + * @return string + */ + public function compileSchemas() + { + return 'select schema_name as name, schema_name = schema() as `default` from information_schema.schemata where ' + .$this->compileSchemaWhereClause(null, 'schema_name') + .' order by schema_name'; + } + /** * Compile the query to determine if the given table exists. * - * @param string $database + * @param string|null $schema * @param string $table * @return string */ - public function compileTableExists($database, $table) + public function compileTableExists($schema, $table) { return sprintf( 'select exists (select 1 from information_schema.tables where ' ."table_schema = %s and table_name = %s and table_type in ('BASE TABLE', 'SYSTEM VERSIONED')) as `exists`", - $this->quoteString($database), + $schema ? $this->quoteString($schema) : 'schema()', $this->quoteString($table) ); } @@ -97,44 +109,59 @@ public function compileTableExists($database, $table) /** * Compile the query to determine the tables. * - * @param string $database + * @param string|string[]|null $schema * @return string */ - public function compileTables($database) + public function compileTables($schema) { return sprintf( - 'select table_name as `name`, (data_length + index_length) as `size`, ' + 'select table_name as `name`, table_schema as `schema`, (data_length + index_length) as `size`, ' .'table_comment as `comment`, engine as `engine`, table_collation as `collation` ' - ."from information_schema.tables where table_schema = %s and table_type in ('BASE TABLE', 'SYSTEM VERSIONED') " - .'order by table_name', - $this->quoteString($database) + ."from information_schema.tables where table_type in ('BASE TABLE', 'SYSTEM VERSIONED') and " + .$this->compileSchemaWhereClause($schema, 'table_schema') + .' order by table_schema, table_name', + $this->quoteString($schema) ); } /** * Compile the query to determine the views. * - * @param string $database + * @param string|string[]|null $schema * @return string */ - public function compileViews($database) + public function compileViews($schema) { - return sprintf( - 'select table_name as `name`, view_definition as `definition` ' - .'from information_schema.views where table_schema = %s ' - .'order by table_name', - $this->quoteString($database) - ); + return 'select table_name as `name`, table_schema as `schema`, view_definition as `definition` ' + .'from information_schema.views where ' + .$this->compileSchemaWhereClause($schema, 'table_schema') + .' order by table_schema, table_name'; + } + + /** + * Compile the query to compare the schema. + * + * @param string|string[]|null $schema + * @param string $column + * @return string + */ + protected function compileSchemaWhereClause($schema, $column) + { + return $column.(match (true) { + ! empty($schema) && is_array($schema) => ' in ('.$this->quoteString($schema).')', + ! empty($schema) => ' = '.$this->quoteString($schema), + default => " not in ('information_schema', 'mysql', 'ndbinfo', 'performance_schema', 'sys')", + }); } /** * Compile the query to determine the columns. * - * @param string $database + * @param string|null $schema * @param string $table * @return string */ - public function compileColumns($database, $table) + public function compileColumns($schema, $table) { return sprintf( 'select column_name as `name`, data_type as `type_name`, column_type as `type`, ' @@ -143,7 +170,7 @@ public function compileColumns($database, $table) .'generation_expression as `expression`, extra as `extra` ' .'from information_schema.columns where table_schema = %s and table_name = %s ' .'order by ordinal_position asc', - $this->quoteString($database), + $schema ? $this->quoteString($schema) : 'schema()', $this->quoteString($table) ); } @@ -151,18 +178,18 @@ public function compileColumns($database, $table) /** * Compile the query to determine the indexes. * - * @param string $database + * @param string|null $schema * @param string $table * @return string */ - public function compileIndexes($database, $table) + public function compileIndexes($schema, $table) { return sprintf( 'select index_name as `name`, group_concat(column_name order by seq_in_index) as `columns`, ' .'index_type as `type`, not non_unique as `unique` ' .'from information_schema.statistics where table_schema = %s and table_name = %s ' .'group by index_name, index_type, non_unique', - $this->quoteString($database), + $schema ? $this->quoteString($schema) : 'schema()', $this->quoteString($table) ); } @@ -170,11 +197,11 @@ public function compileIndexes($database, $table) /** * Compile the query to determine the foreign keys. * - * @param string $database + * @param string|null $schema * @param string $table * @return string */ - public function compileForeignKeys($database, $table) + public function compileForeignKeys($schema, $table) { return sprintf( 'select kc.constraint_name as `name`, ' @@ -188,7 +215,7 @@ public function compileForeignKeys($database, $table) .'on kc.constraint_schema = rc.constraint_schema and kc.constraint_name = rc.constraint_name ' .'where kc.table_schema = %s and kc.table_name = %s and kc.referenced_table_name is not null ' .'group by kc.constraint_name, kc.referenced_table_schema, kc.referenced_table_name, rc.update_rule, rc.delete_rule', - $this->quoteString($database), + $schema ? $this->quoteString($schema) : 'schema()', $this->quoteString($table) ); } diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index c9a579fdad2e..8a2ccb2cff1a 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -69,10 +69,22 @@ public function compileDropDatabaseIfExists($name) ); } + /** + * Compile the query to determine the schemas. + * + * @return string + */ + public function compileSchemas() + { + return 'select nspname as name, nspname = current_schema() as "default" from pg_namespace where ' + .$this->compileSchemaWhereClause(null, 'nspname') + .' order by nspname'; + } + /** * Compile the query to determine if the given table exists. * - * @param string $schema + * @param string|null $schema * @param string $table * @return string */ @@ -81,7 +93,7 @@ public function compileTableExists($schema, $table) return sprintf( 'select exists (select 1 from pg_class c, pg_namespace n where ' ."n.nspname = %s and c.relname = %s and c.relkind in ('r', 'p') and n.oid = c.relnamespace)", - $this->quoteString($schema), + $schema ? $this->quoteString($schema) : 'current_schema()', $this->quoteString($table) ); } @@ -89,32 +101,38 @@ public function compileTableExists($schema, $table) /** * Compile the query to determine the tables. * + * @param string|string[]|null $schema * @return string */ - public function compileTables() + public function compileTables($schema) { return 'select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, ' ."obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " - ."where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema') " - .'order by c.relname'; + ."where c.relkind in ('r', 'p') and n.oid = c.relnamespace and " + .$this->compileSchemaWhereClause($schema, 'n.nspname') + .' order by n.nspname, c.relname'; } /** * Compile the query to determine the views. * + * @param string|string[]|null $schema * @return string */ - public function compileViews() + public function compileViews($schema) { - return "select viewname as name, schemaname as schema, definition from pg_views where schemaname not in ('pg_catalog', 'information_schema') order by viewname"; + return 'select viewname as name, schemaname as schema, definition from pg_views where ' + .$this->compileSchemaWhereClause($schema, 'schemaname') + .' order by schemaname, viewname'; } /** * Compile the query to determine the user-defined types. * + * @param string|string[]|null $schema * @return string */ - public function compileTypes() + public function compileTypes($schema) { return 'select t.typname as name, n.nspname as schema, t.typtype as type, t.typcategory as category, ' ."((t.typinput = 'array_in'::regproc and t.typoutput = 'array_out'::regproc) or t.typtype = 'm') as implicit " @@ -123,14 +141,30 @@ public function compileTypes() .'left join pg_type el on el.oid = t.typelem ' .'left join pg_class ce on ce.oid = el.typrelid ' ."where ((t.typrelid = 0 and (ce.relkind = 'c' or ce.relkind is null)) or c.relkind = 'c') " - ."and not exists (select 1 from pg_depend d where d.objid in (t.oid, t.typelem) and d.deptype = 'e') " - ."and n.nspname not in ('pg_catalog', 'information_schema')"; + ."and not exists (select 1 from pg_depend d where d.objid in (t.oid, t.typelem) and d.deptype = 'e') and " + .$this->compileSchemaWhereClause($schema, 'n.nspname'); + } + + /** + * Compile the query to compare the schema. + * + * @param string|string[]|null $schema + * @param string $column + * @return string + */ + protected function compileSchemaWhereClause($schema, $column) + { + return $column.(match (true) { + ! empty($schema) && is_array($schema) => ' in ('.$this->quoteString($schema).')', + ! empty($schema) => ' = '.$this->quoteString($schema), + default => " <> 'information_schema' and $column not like 'pg\_%'", + }); } /** * Compile the query to determine the columns. * - * @param string $schema + * @param string|null $schema * @param string $table * @return string */ @@ -147,14 +181,14 @@ public function compileColumns($schema, $table) .'where c.relname = %s and n.nspname = %s and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and n.oid = c.relnamespace ' .'order by a.attnum', $this->quoteString($table), - $this->quoteString($schema) + $schema ? $this->quoteString($schema) : 'current_schema()' ); } /** * Compile the query to determine the indexes. * - * @param string $schema + * @param string|null $schema * @param string $table * @return string */ @@ -173,14 +207,14 @@ public function compileIndexes($schema, $table) .'where tc.relname = %s and tn.nspname = %s ' .'group by ic.relname, am.amname, i.indisunique, i.indisprimary', $this->quoteString($table), - $this->quoteString($schema) + $schema ? $this->quoteString($schema) : 'current_schema()' ); } /** * Compile the query to determine the foreign keys. * - * @param string $schema + * @param string|null $schema * @param string $table * @return string */ @@ -203,7 +237,7 @@ public function compileForeignKeys($schema, $table) ."where c.contype = 'f' and tc.relname = %s and tn.nspname = %s " .'group by c.conname, fn.nspname, fc.relname, c.confupdtype, c.confdeltype', $this->quoteString($table), - $this->quoteString($schema) + $schema ? $this->quoteString($schema) : 'current_schema()' ); } diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index b40f6cf1f755..dc37b26d5730 100644 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -47,15 +47,17 @@ public function getAlterCommands(Connection $connection) /** * Compile the query to determine the SQL text that describes the given object. * + * @param string|null $schema * @param string $name * @param string $type * @return string */ - public function compileSqlCreateStatement($name, $type = 'table') + public function compileSqlCreateStatement($schema, $name, $type = 'table') { - return sprintf('select "sql" from sqlite_master where type = %s and name = %s', + return sprintf('select "sql" from %s.sqlite_master where type = %s and name = %s', + $this->wrapValue($schema ?? 'main'), $this->quoteString($type), - $this->quoteString(str_replace('.', '__', $name)) + $this->quoteString($name) ); } @@ -69,95 +71,155 @@ public function compileDbstatExists() return "select exists (select 1 from pragma_compile_options where compile_options = 'ENABLE_DBSTAT_VTAB') as enabled"; } + /** + * Compile the query to determine the schemas. + * + * @return string + */ + public function compileSchemas() + { + return 'select name, file as path, name = \'main\' as "default" from pragma_database_list order by name'; + } + /** * Compile the query to determine if the given table exists. * + * @param string|null $schema * @param string $table * @return string */ - public function compileTableExists($table) + public function compileTableExists($schema, $table) { return sprintf( - 'select exists (select 1 from sqlite_master where name = %s and type = \'table\') as "exists"', - $this->quoteString(str_replace('.', '__', $table)) + 'select exists (select 1 from %s.sqlite_master where name = %s and type = \'table\') as "exists"', + $this->wrapValue($schema ?? 'main'), + $this->quoteString($table) ); } /** * Compile the query to determine the tables. * + * @param string|string[]|null $schema * @param bool $withSize * @return string */ - public function compileTables($withSize = false) + public function compileTables($schema, $withSize = false) + { + return 'select tl.name as name, tl.schema as schema' + .($withSize ? ', (select sum(s.pgsize) ' + .'from (select tl.name as name union select il.name as name from pragma_index_list(tl.name, tl.schema) as il) as es ' + .'join dbstat(tl.schema) as s on s.name = es.name) as size' : '') + .' from pragma_table_list as tl where' + .(match (true) { + ! empty($schema) && is_array($schema) => ' tl.schema in ('.$this->quoteString($schema).') and', + ! empty($schema) => ' tl.schema = '.$this->quoteString($schema).' and', + default => '', + }) + ." tl.type in ('table', 'virtual') and tl.name not like 'sqlite\_%' escape '\' " + .'order by tl.schema, tl.name'; + } + + /** + * Compile the query for legacy versions of SQLite to determine the tables. + * + * @param string|string[]|null $schema + * @param bool $withSize + * @return string + */ + public function compileLegacyTables($schema, $withSize = false) { return $withSize - ? 'select m.tbl_name as name, sum(s.pgsize) as size from sqlite_master as m ' - .'join dbstat as s on s.name = m.name ' - ."where m.type in ('table', 'index') and m.tbl_name not like 'sqlite_%' " - .'group by m.tbl_name ' - .'order by m.tbl_name' - : "select name from sqlite_master where type = 'table' and name not like 'sqlite_%' order by name"; + ? sprintf( + 'select m.tbl_name as name, %s as schema, sum(s.pgsize) as size from %s.sqlite_master as m ' + .'join dbstat(%s) as s on s.name = m.name ' + ."where m.type in ('table', 'index') and m.tbl_name not like 'sqlite\_%%' escape '\' " + .'group by m.tbl_name ' + .'order by m.tbl_name', + $this->quoteString($schema), + $this->wrapValue($schema), + $this->quoteString($schema) + ) + : sprintf( + 'select name, %s as schema from %s.sqlite_master ' + ."where type = 'table' and name not like 'sqlite\_%%' escape '\' order by name", + $this->quoteString($schema), + $this->wrapValue($schema) + ); } /** * Compile the query to determine the views. * + * @param string $schema * @return string */ - public function compileViews() + public function compileViews($schema) { - return "select name, sql as definition from sqlite_master where type = 'view' order by name"; + return sprintf( + "select name, %s as schema, sql as definition from %s.sqlite_master where type = 'view' order by name", + $this->quoteString($schema), + $this->wrapValue($schema) + ); } /** * Compile the query to determine the columns. * + * @param string|null $schema * @param string $table * @return string */ - public function compileColumns($table) + public function compileColumns($schema, $table) { return sprintf( 'select name, type, not "notnull" as "nullable", dflt_value as "default", pk as "primary", hidden as "extra" ' - .'from pragma_table_xinfo(%s) order by cid asc', - $this->quoteString(str_replace('.', '__', $table)) + .'from pragma_table_xinfo(%s, %s) order by cid asc', + $this->quoteString($table), + $this->quoteString($schema ?? 'main') ); } /** * Compile the query to determine the indexes. * + * @param string|null $schema * @param string $table * @return string */ - public function compileIndexes($table) + public function compileIndexes($schema, $table) { return sprintf( 'select \'primary\' as name, group_concat(col) as columns, 1 as "unique", 1 as "primary" ' - .'from (select name as col from pragma_table_info(%s) where pk > 0 order by pk, cid) group by name ' + .'from (select name as col from pragma_table_xinfo(%s, %s) where pk > 0 order by pk, cid) group by name ' .'union select name, group_concat(col) as columns, "unique", origin = \'pk\' as "primary" ' - .'from (select il.*, ii.name as col from pragma_index_list(%s) il, pragma_index_info(il.name) ii order by il.seq, ii.seqno) ' + .'from (select il.*, ii.name as col from pragma_index_list(%s, %s) il, pragma_index_info(il.name, %s) ii order by il.seq, ii.seqno) ' .'group by name, "unique", "primary"', - $table = $this->quoteString(str_replace('.', '__', $table)), - $table + $table = $this->quoteString($table), + $schema = $this->quoteString($schema ?? 'main'), + $table, + $schema, + $schema ); } /** * Compile the query to determine the foreign keys. * + * @param string|null $schema * @param string $table * @return string */ - public function compileForeignKeys($table) + public function compileForeignKeys($schema, $table) { return sprintf( - 'select group_concat("from") as columns, "table" as foreign_table, ' + 'select group_concat("from") as columns, %s as foreign_schema, "table" as foreign_table, ' .'group_concat("to") as foreign_columns, on_update, on_delete ' - .'from (select * from pragma_foreign_key_list(%s) order by id desc, seq) ' + .'from (select * from pragma_foreign_key_list(%s, %s) order by id desc, seq) ' .'group by id, "table", on_update, on_delete', - $this->quoteString(str_replace('.', '__', $table)) + $schema = $this->quoteString($schema ?? 'main'), + $this->quoteString($table), + $schema ); } @@ -292,7 +354,8 @@ public function compileAlter(Blueprint $blueprint, Fluent $command, Connection $ ->map(fn ($index) => $this->{'compile'.ucfirst($index->name)}($blueprint, $index)) ->all(); - $tempTable = $this->wrap('__temp__'.$blueprint->getPrefix().$blueprint->getTable()); + [, $tableName] = $connection->getSchemaBuilder()->parseSchemaAndTable($blueprint->getTable()); + $tempTable = $this->wrapTable($blueprint, '__temp__'.$connection->getTablePrefix()); $table = $this->wrapTable($blueprint); $columnNames = implode(', ', $columnNames); @@ -308,7 +371,7 @@ public function compileAlter(Blueprint $blueprint, Fluent $command, Connection $ ), sprintf('insert into %s (%s) select %s from %s', $tempTable, $columnNames, $columnNames, $table), sprintf('drop table %s', $table), - sprintf('alter table %s rename to %s', $tempTable, $table), + sprintf('alter table %s rename to %s', $tempTable, $this->wrapTable($tableName)), ], $indexes, [$foreignKeyConstraintsEnabled ? $this->compileEnableForeignKeyConstraints() : null])); } @@ -348,9 +411,12 @@ public function compilePrimary(Blueprint $blueprint, Fluent $command) */ public function compileUnique(Blueprint $blueprint, Fluent $command) { - return sprintf('create unique index %s on %s (%s)', + [$schema, $table] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($blueprint->getTable()); + + return sprintf('create unique index %s%s on %s (%s)', + $schema ? $this->wrapValue($schema).'.' : '', $this->wrap($command->index), - $this->wrapTable($blueprint), + $this->wrapTable($table), $this->columnize($command->columns) ); } @@ -364,9 +430,12 @@ public function compileUnique(Blueprint $blueprint, Fluent $command) */ public function compileIndex(Blueprint $blueprint, Fluent $command) { - return sprintf('create index %s on %s (%s)', + [$schema, $table] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($blueprint->getTable()); + + return sprintf('create index %s%s on %s (%s)', + $schema ? $this->wrapValue($schema).'.' : '', $this->wrap($command->index), - $this->wrapTable($blueprint), + $this->wrapTable($table), $this->columnize($command->columns) ); } @@ -424,21 +493,27 @@ public function compileDropIfExists(Blueprint $blueprint, Fluent $command) /** * Compile the SQL needed to drop all tables. * + * @param string|null $schema * @return string */ - public function compileDropAllTables() + public function compileDropAllTables($schema = null) { - return "delete from sqlite_master where type in ('table', 'index', 'trigger')"; + return sprintf("delete from %s.sqlite_master where type in ('table', 'index', 'trigger')", + $this->wrapValue($schema ?? 'main') + ); } /** * Compile the SQL needed to drop all views. * + * @param string|null $schema * @return string */ - public function compileDropAllViews() + public function compileDropAllViews($schema = null) { - return "delete from sqlite_master where type in ('view')"; + return sprintf("delete from %s.sqlite_master where type in ('view')", + $this->wrapValue($schema ?? 'main') + ); } /** @@ -495,9 +570,7 @@ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) */ public function compileDropUnique(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap($command->index); - - return "drop index {$index}"; + return $this->compileDropIndex($blueprint, $command); } /** @@ -509,9 +582,12 @@ public function compileDropUnique(Blueprint $blueprint, Fluent $command) */ public function compileDropIndex(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap($command->index); + [$schema] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($blueprint->getTable()); - return "drop index {$index}"; + return sprintf('drop index %s%s', + $schema ? $this->wrapValue($schema).'.' : '', + $this->wrap($command->index) + ); } /** diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index 81258f2e1036..6333b23e6118 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -37,16 +37,6 @@ class SqlServerGrammar extends Grammar */ protected $fluentCommands = ['Default']; - /** - * Compile a query to determine the name of the default schema. - * - * @return string - */ - public function compileDefaultSchema() - { - return 'select schema_name()'; - } - /** * Compile a create database command. * @@ -76,6 +66,17 @@ public function compileDropDatabaseIfExists($name) ); } + /** + * Compile the query to determine the schemas. + * + * @return string + */ + public function compileSchemas() + { + return 'select name, iif(schema_id = schema_id(), 1, 0) as [default] from sys.schemas ' + ."where name not in ('information_schema', 'sys') and name not like 'db[_]%' order by name"; + } + /** * Compile the query to determine if the given table exists. * @@ -94,34 +95,56 @@ public function compileTableExists($schema, $table) /** * Compile the query to determine the tables. * + * @param string|string[]|null $schema * @return string */ - public function compileTables() + public function compileTables($schema) { return 'select t.name as name, schema_name(t.schema_id) as [schema], sum(u.total_pages) * 8 * 1024 as size ' .'from sys.tables as t ' .'join sys.partitions as p on p.object_id = t.object_id ' .'join sys.allocation_units as u on u.container_id = p.hobt_id ' - .'group by t.name, t.schema_id ' - .'order by t.name'; + ."where t.is_ms_shipped = 0 and t.name <> 'sysdiagrams'" + .$this->compileSchemaWhereClause($schema, 'schema_name(t.schema_id)') + .' group by t.name, t.schema_id ' + .'order by [schema], t.name'; } /** * Compile the query to determine the views. * + * @param string|string[]|null $schema * @return string */ - public function compileViews() + public function compileViews($schema) { return 'select name, schema_name(v.schema_id) as [schema], definition from sys.views as v ' .'inner join sys.sql_modules as m on v.object_id = m.object_id ' - .'order by name'; + .'where v.is_ms_shipped = 0' + .$this->compileSchemaWhereClause($schema, 'schema_name(v.schema_id)') + .' order by [schema], name'; + } + + /** + * Compile the query to compare the schema. + * + * @param string|string[]|null $schema + * @param string $column + * @return string + */ + protected function compileSchemaWhereClause($schema, $column) + { + return match (true) { + ! empty($schema) && is_array($schema) => " and $column in (".$this->quoteString($schema).')', + ! empty($schema) => " and $column = ".$this->quoteString($schema), + default => '', + }; } /** * Compile the query to determine the columns. * - * @param string $schema + * @param string|null $schema * @param string $table * @return string */ @@ -151,7 +174,7 @@ public function compileColumns($schema, $table) /** * Compile the query to determine the indexes. * - * @param string $schema + * @param string|null $schema * @param string $table * @return string */ @@ -175,7 +198,7 @@ public function compileIndexes($schema, $table) /** * Compile the query to determine the foreign keys. * - * @param string $schema + * @param string|null $schema * @param string $table * @return string */ @@ -212,9 +235,10 @@ public function compileForeignKeys($schema, $table) */ public function compileCreate(Blueprint $blueprint, Fluent $command) { - $columns = implode(', ', $this->getColumns($blueprint)); - - return 'create table '.$this->wrapTable($blueprint)." ($columns)"; + return sprintf('create table %s (%s)', + $this->wrapTable($blueprint, $blueprint->temporary ? '#'.$this->connection->getTablePrefix() : null), + implode(', ', $this->getColumns($blueprint)) + ); } /** @@ -1040,21 +1064,6 @@ protected function modifyPersisted(Blueprint $blueprint, Fluent $column) } } - /** - * Wrap a table in keyword identifiers. - * - * @param \Illuminate\Database\Schema\Blueprint|\Illuminate\Contracts\Database\Query\Expression|string $table - * @return string - */ - public function wrapTable($table) - { - if ($table instanceof Blueprint && $table->temporary) { - $this->setTablePrefix('#'); - } - - return parent::wrapTable($table); - } - /** * Quote the given string literal. * diff --git a/src/Illuminate/Database/Schema/MySqlBuilder.php b/src/Illuminate/Database/Schema/MySqlBuilder.php index 842130503595..b55f324b69e1 100755 --- a/src/Illuminate/Database/Schema/MySqlBuilder.php +++ b/src/Illuminate/Database/Schema/MySqlBuilder.php @@ -30,102 +30,6 @@ public function dropDatabaseIfExists($name) ); } - /** - * Determine if the given table exists. - * - * @param string $table - * @return bool - */ - public function hasTable($table) - { - $table = $this->connection->getTablePrefix().$table; - - $database = $this->connection->getDatabaseName(); - - return (bool) $this->connection->scalar( - $this->grammar->compileTableExists($database, $table) - ); - } - - /** - * Get the tables for the database. - * - * @return array - */ - public function getTables() - { - return $this->connection->getPostProcessor()->processTables( - $this->connection->selectFromWriteConnection( - $this->grammar->compileTables($this->connection->getDatabaseName()) - ) - ); - } - - /** - * Get the views for the database. - * - * @return array - */ - public function getViews() - { - return $this->connection->getPostProcessor()->processViews( - $this->connection->selectFromWriteConnection( - $this->grammar->compileViews($this->connection->getDatabaseName()) - ) - ); - } - - /** - * Get the columns for a given table. - * - * @param string $table - * @return array - */ - public function getColumns($table) - { - $table = $this->connection->getTablePrefix().$table; - - $results = $this->connection->selectFromWriteConnection( - $this->grammar->compileColumns($this->connection->getDatabaseName(), $table) - ); - - return $this->connection->getPostProcessor()->processColumns($results); - } - - /** - * Get the indexes for a given table. - * - * @param string $table - * @return array - */ - public function getIndexes($table) - { - $table = $this->connection->getTablePrefix().$table; - - return $this->connection->getPostProcessor()->processIndexes( - $this->connection->selectFromWriteConnection( - $this->grammar->compileIndexes($this->connection->getDatabaseName(), $table) - ) - ); - } - - /** - * Get the foreign keys for a given table. - * - * @param string $table - * @return array - */ - public function getForeignKeys($table) - { - $table = $this->connection->getTablePrefix().$table; - - return $this->connection->getPostProcessor()->processForeignKeys( - $this->connection->selectFromWriteConnection( - $this->grammar->compileForeignKeys($this->connection->getDatabaseName(), $table) - ) - ); - } - /** * Drop all tables from the database. * @@ -133,7 +37,7 @@ public function getForeignKeys($table) */ public function dropAllTables() { - $tables = array_column($this->getTables(), 'name'); + $tables = $this->getTableListing($this->getCurrentSchemaListing()); if (empty($tables)) { return; @@ -141,11 +45,13 @@ public function dropAllTables() $this->disableForeignKeyConstraints(); - $this->connection->statement( - $this->grammar->compileDropAllTables($tables) - ); - - $this->enableForeignKeyConstraints(); + try { + $this->connection->statement( + $this->grammar->compileDropAllTables($tables) + ); + } finally { + $this->enableForeignKeyConstraints(); + } } /** @@ -155,7 +61,7 @@ public function dropAllTables() */ public function dropAllViews() { - $views = array_column($this->getViews(), 'name'); + $views = array_column($this->getViews($this->getCurrentSchemaListing()), 'schema_qualified_name'); if (empty($views)) { return; @@ -165,4 +71,14 @@ public function dropAllViews() $this->grammar->compileDropAllViews($views) ); } + + /** + * Get the names of current schemas for the connection. + * + * @return string[]|null + */ + public function getCurrentSchemaListing() + { + return [$this->connection->getDatabaseName()]; + } } diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index b39486c0e5a6..f8b88d5b003a 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -3,13 +3,10 @@ namespace Illuminate\Database\Schema; use Illuminate\Database\Concerns\ParsesSearchPath; -use InvalidArgumentException; class PostgresBuilder extends Builder { - use ParsesSearchPath { - parseSearchPath as baseParseSearchPath; - } + use ParsesSearchPath; /** * Create a database in the schema. @@ -37,57 +34,6 @@ public function dropDatabaseIfExists($name) ); } - /** - * Determine if the given table exists. - * - * @param string $table - * @return bool - */ - public function hasTable($table) - { - [$schema, $table] = $this->parseSchemaAndTable($table); - - $table = $this->connection->getTablePrefix().$table; - - return (bool) $this->connection->scalar( - $this->grammar->compileTableExists($schema, $table) - ); - } - - /** - * Determine if the given view exists. - * - * @param string $view - * @return bool - */ - public function hasView($view) - { - [$schema, $view] = $this->parseSchemaAndTable($view); - - $view = $this->connection->getTablePrefix().$view; - - foreach ($this->getViews() as $value) { - if (strtolower($view) === strtolower($value['name']) - && strtolower($schema) === strtolower($value['schema'])) { - return true; - } - } - - return false; - } - - /** - * Get the user-defined types that belong to the database. - * - * @return array - */ - public function getTypes() - { - return $this->connection->getPostProcessor()->processTypes( - $this->connection->selectFromWriteConnection($this->grammar->compileTypes()) - ); - } - /** * Drop all tables from the database. * @@ -99,14 +45,9 @@ public function dropAllTables() $excludedTables = $this->connection->getConfig('dont_drop') ?? ['spatial_ref_sys']; - $schemas = $this->getSchemas(); - - foreach ($this->getTables() as $table) { - $qualifiedName = $table['schema'].'.'.$table['name']; - - if (in_array($table['schema'], $schemas) && - empty(array_intersect([$table['name'], $qualifiedName], $excludedTables))) { - $tables[] = $qualifiedName; + foreach ($this->getTables($this->getCurrentSchemaListing()) as $table) { + if (empty(array_intersect([$table['name'], $table['schema_qualified_name']], $excludedTables))) { + $tables[] = $table['schema_qualified_name']; } } @@ -126,15 +67,7 @@ public function dropAllTables() */ public function dropAllViews() { - $views = []; - - $schemas = $this->getSchemas(); - - foreach ($this->getViews() as $view) { - if (in_array($view['schema'], $schemas)) { - $views[] = $view['schema'].'.'.$view['name']; - } - } + $views = array_column($this->getViews($this->getCurrentSchemaListing()), 'schema_qualified_name'); if (empty($views)) { return; @@ -155,14 +88,12 @@ public function dropAllTypes() $types = []; $domains = []; - $schemas = $this->getSchemas(); - - foreach ($this->getTypes() as $type) { - if (! $type['implicit'] && in_array($type['schema'], $schemas)) { + foreach ($this->getTypes($this->getCurrentSchemaListing()) as $type) { + if (! $type['implicit']) { if ($type['type'] === 'domain') { - $domains[] = $type['schema'].'.'.$type['name']; + $domains[] = $type['schema_qualified_name']; } else { - $types[] = $type['schema'].'.'.$type['name']; + $types[] = $type['schema_qualified_name']; } } } @@ -177,111 +108,19 @@ public function dropAllTypes() } /** - * Get the columns for a given table. - * - * @param string $table - * @return array - */ - public function getColumns($table) - { - [$schema, $table] = $this->parseSchemaAndTable($table); - - $table = $this->connection->getTablePrefix().$table; - - $results = $this->connection->selectFromWriteConnection( - $this->grammar->compileColumns($schema, $table) - ); - - return $this->connection->getPostProcessor()->processColumns($results); - } - - /** - * Get the indexes for a given table. - * - * @param string $table - * @return array - */ - public function getIndexes($table) - { - [$schema, $table] = $this->parseSchemaAndTable($table); - - $table = $this->connection->getTablePrefix().$table; - - return $this->connection->getPostProcessor()->processIndexes( - $this->connection->selectFromWriteConnection($this->grammar->compileIndexes($schema, $table)) - ); - } - - /** - * Get the foreign keys for a given table. - * - * @param string $table - * @return array - */ - public function getForeignKeys($table) - { - [$schema, $table] = $this->parseSchemaAndTable($table); - - $table = $this->connection->getTablePrefix().$table; - - return $this->connection->getPostProcessor()->processForeignKeys( - $this->connection->selectFromWriteConnection($this->grammar->compileForeignKeys($schema, $table)) - ); - } - - /** - * Get the schemas for the connection. + * Get the current schemas for the connection. * - * @return array + * @return string[] */ - public function getSchemas() + public function getCurrentSchemaListing() { - return $this->parseSearchPath( - $this->connection->getConfig('search_path') ?: $this->connection->getConfig('schema') ?: 'public' + return array_map( + fn ($schema) => $schema === '$user' ? $this->connection->getConfig('username') : $schema, + $this->parseSearchPath( + $this->connection->getConfig('search_path') + ?: $this->connection->getConfig('schema') + ?: 'public' + ) ); } - - /** - * Parse the database object reference and extract the schema and table. - * - * @param string $reference - * @return array - */ - public function parseSchemaAndTable($reference) - { - $parts = explode('.', $reference); - - if (count($parts) > 2) { - $database = $parts[0]; - - throw new InvalidArgumentException("Using three-part reference is not supported, you may use `Schema::connection('$database')` instead."); - } - - // We will use the default schema unless the schema has been specified in the - // query. If the schema has been specified in the query then we can use it - // instead of a default schema configured in the connection search path. - $schema = $this->getSchemas()[0]; - - if (count($parts) === 2) { - $schema = $parts[0]; - array_shift($parts); - } - - return [$schema, $parts[0]]; - } - - /** - * Parse the "search_path" configuration value into an array. - * - * @param string|array|null $searchPath - * @return array - */ - protected function parseSearchPath($searchPath) - { - return array_map(function ($schema) { - return $schema === '$user' - ? $this->connection->getConfig('username') - : $schema; - }, $this->baseParseSearchPath($searchPath)); - } } diff --git a/src/Illuminate/Database/Schema/PostgresSchemaState.php b/src/Illuminate/Database/Schema/PostgresSchemaState.php index a96894128b1d..25da812e61c5 100644 --- a/src/Illuminate/Database/Schema/PostgresSchemaState.php +++ b/src/Illuminate/Database/Schema/PostgresSchemaState.php @@ -59,7 +59,7 @@ public function load($path) */ protected function getMigrationTable(): string { - [$schema, $table] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($this->migrationTable); + [$schema, $table] = $this->connection->getSchemaBuilder()->parseSchemaAndTable($this->migrationTable, withDefaultSchema: true); return $schema.'.'.$this->connection->getTablePrefix().$table; } diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index 25111ebd3905..af49bae8126e 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Schema; use Illuminate\Database\QueryException; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\File; class SQLiteBuilder extends Builder @@ -32,39 +33,59 @@ public function dropDatabaseIfExists($name) } /** - * Determine if the given table exists. + * Get the tables that belong to the connection. * - * @param string $table - * @return bool + * @param string|string[]|null $schema + * @return array */ - public function hasTable($table) + public function getTables($schema = null) { - $table = $this->connection->getTablePrefix().$table; + try { + $withSize = $this->connection->scalar($this->grammar->compileDbstatExists()); + } catch (QueryException) { + $withSize = false; + } + + if (version_compare($this->connection->getServerVersion(), '3.37.0', '<')) { + $schema ??= array_column($this->getSchemas(), 'name'); + + $tables = []; - return (bool) $this->connection->scalar( - $this->grammar->compileTableExists($table) + foreach (Arr::wrap($schema) as $name) { + $tables = array_merge($tables, $this->connection->selectFromWriteConnection( + $this->grammar->compileLegacyTables($name, $withSize) + )); + } + + return $this->connection->getPostProcessor()->processTables($tables); + } + + return $this->connection->getPostProcessor()->processTables( + $this->connection->selectFromWriteConnection( + $this->grammar->compileTables($schema, $withSize) + ) ); } /** - * Get the tables for the database. + * Get the views that belong to the connection. * - * @param bool $withSize + * @param string|string[]|null $schema * @return array */ - public function getTables($withSize = true) + public function getViews($schema = null) { - if ($withSize) { - try { - $withSize = $this->connection->scalar($this->grammar->compileDbstatExists()); - } catch (QueryException $e) { - $withSize = false; - } + $schema ??= array_column($this->getSchemas(), 'name'); + + $views = []; + + foreach (Arr::wrap($schema) as $name) { + $views = array_merge($views, $this->connection->selectFromWriteConnection( + $this->grammar->compileViews($name) + )); } - return $this->connection->getPostProcessor()->processTables( - $this->connection->selectFromWriteConnection($this->grammar->compileTables($withSize)) - ); + return $this->connection->getPostProcessor()->processViews($views); } /** @@ -75,11 +96,13 @@ public function getTables($withSize = true) */ public function getColumns($table) { + [$schema, $table] = $this->parseSchemaAndTable($table); + $table = $this->connection->getTablePrefix().$table; return $this->connection->getPostProcessor()->processColumns( - $this->connection->selectFromWriteConnection($this->grammar->compileColumns($table)), - $this->connection->scalar($this->grammar->compileSqlCreateStatement($table)) + $this->connection->selectFromWriteConnection($this->grammar->compileColumns($schema, $table)), + $this->connection->scalar($this->grammar->compileSqlCreateStatement($schema, $table)) ); } @@ -101,7 +124,9 @@ public function dropAllTables() $this->connection->select($this->grammar->compileEnableWriteableSchema()); - $this->connection->select($this->grammar->compileDropAllTables()); + foreach ($this->getCurrentSchemaListing() as $schema) { + $this->connection->select($this->grammar->compileDropAllTables($schema)); + } $this->connection->select($this->grammar->compileDisableWriteableSchema()); @@ -117,7 +142,9 @@ public function dropAllViews() { $this->connection->select($this->grammar->compileEnableWriteableSchema()); - $this->connection->select($this->grammar->compileDropAllViews()); + foreach ($this->getCurrentSchemaListing() as $schema) { + $this->connection->select($this->grammar->compileDropAllViews($schema)); + } $this->connection->select($this->grammar->compileDisableWriteableSchema()); @@ -172,4 +199,14 @@ public function refreshDatabaseFile() { file_put_contents($this->connection->getDatabaseName(), ''); } + + /** + * Get the names of current schemas for the connection. + * + * @return string[]|null + */ + public function getCurrentSchemaListing() + { + return ['main']; + } } diff --git a/src/Illuminate/Database/Schema/SqlServerBuilder.php b/src/Illuminate/Database/Schema/SqlServerBuilder.php index 9b59ccc0ebd3..1cbc5afa7692 100644 --- a/src/Illuminate/Database/Schema/SqlServerBuilder.php +++ b/src/Illuminate/Database/Schema/SqlServerBuilder.php @@ -2,7 +2,7 @@ namespace Illuminate\Database\Schema; -use InvalidArgumentException; +use Illuminate\Support\Arr; class SqlServerBuilder extends Builder { @@ -32,46 +32,6 @@ public function dropDatabaseIfExists($name) ); } - /** - * Determine if the given table exists. - * - * @param string $table - * @return bool - */ - public function hasTable($table) - { - [$schema, $table] = $this->parseSchemaAndTable($table); - - $table = $this->connection->getTablePrefix().$table; - - return (bool) $this->connection->scalar( - $this->grammar->compileTableExists($schema, $table) - ); - } - - /** - * Determine if the given view exists. - * - * @param string $view - * @return bool - */ - public function hasView($view) - { - [$schema, $view] = $this->parseSchemaAndTable($view); - - $schema ??= $this->getDefaultSchema(); - $view = $this->connection->getTablePrefix().$view; - - foreach ($this->getViews() as $value) { - if (strtolower($view) === strtolower($value['name']) - && strtolower($schema) === strtolower($value['schema'])) { - return true; - } - } - - return false; - } - /** * Drop all tables from the database. * @@ -95,84 +55,12 @@ public function dropAllViews() } /** - * Get the columns for a given table. + * Get the default schema name for the connection. * - * @param string $table - * @return array + * @return string|null */ - public function getColumns($table) + public function getCurrentSchemaName() { - [$schema, $table] = $this->parseSchemaAndTable($table); - - $table = $this->connection->getTablePrefix().$table; - - $results = $this->connection->selectFromWriteConnection( - $this->grammar->compileColumns($schema, $table) - ); - - return $this->connection->getPostProcessor()->processColumns($results); - } - - /** - * Get the indexes for a given table. - * - * @param string $table - * @return array - */ - public function getIndexes($table) - { - [$schema, $table] = $this->parseSchemaAndTable($table); - - $table = $this->connection->getTablePrefix().$table; - - return $this->connection->getPostProcessor()->processIndexes( - $this->connection->selectFromWriteConnection($this->grammar->compileIndexes($schema, $table)) - ); - } - - /** - * Get the foreign keys for a given table. - * - * @param string $table - * @return array - */ - public function getForeignKeys($table) - { - [$schema, $table] = $this->parseSchemaAndTable($table); - - $table = $this->connection->getTablePrefix().$table; - - return $this->connection->getPostProcessor()->processForeignKeys( - $this->connection->selectFromWriteConnection($this->grammar->compileForeignKeys($schema, $table)) - ); - } - - /** - * Get the default schema for the connection. - * - * @return string - */ - protected function getDefaultSchema() - { - return $this->connection->scalar($this->grammar->compileDefaultSchema()); - } - - /** - * Parse the database object reference and extract the schema and table. - * - * @param string $reference - * @return array - */ - protected function parseSchemaAndTable($reference) - { - $parts = array_pad(explode('.', $reference, 2), -2, null); - - if (str_contains($parts[1], '.')) { - $database = $parts[0]; - - throw new InvalidArgumentException("Using three-part reference is not supported, you may use `Schema::connection('$database')` instead."); - } - - return $parts; + return Arr::first($this->getSchemas(), fn ($schema) => $schema['default'])['name']; } } diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php index 0b7f96abdd21..b72c3150c762 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php @@ -4,7 +4,6 @@ use Illuminate\Contracts\Console\Kernel; use Illuminate\Database\ConnectionInterface; -use Illuminate\Database\Schema\PostgresBuilder; use Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands; use Illuminate\Support\Collection; @@ -99,9 +98,7 @@ function (Collection $tables) use ($connection, $name) { ) ->each(function (array $table) use ($connection) { $connection->withoutTablePrefix(function ($connection) use ($table) { - $table = $connection->table( - $table['schema'] ? $table['schema'].'.'.$table['name'] : $table['name'] - ); + $table = $connection->table($table['schema_qualified_name']); if ($table->exists()) { $table->truncate(); @@ -123,12 +120,7 @@ protected function getAllTablesForConnection(ConnectionInterface $connection, ?s $schema = $connection->getSchemaBuilder(); - return static::$allTables[$name] = (new Collection($schema->getTables()))->when( - $schema instanceof PostgresBuilder ? $schema->getSchemas() : null, - fn (Collection $tables, array $schemas) => $tables->filter( - fn (array $table) => in_array($table['schema'], $schemas) - ) - )->all(); + return static::$allTables[$name] = (new Collection($schema->getTables($schema->getCurrentSchemaListing())))->all(); } /** @@ -137,7 +129,7 @@ protected function getAllTablesForConnection(ConnectionInterface $connection, ?s protected function tableExistsIn(array $table, array $tables): bool { return $table['schema'] - ? ! empty(array_intersect([$table['name'], $table['schema'].'.'.$table['name']], $tables)) + ? ! empty(array_intersect([$table['name'], $table['schema_qualified_name']], $tables)) : in_array($table['name'], $tables); } diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index cc282903ec79..3dc31d17e0c3 100644 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -104,7 +104,7 @@ * @method static string getTablePrefix() * @method static \Illuminate\Database\Connection setTablePrefix(string $prefix) * @method static \Illuminate\Database\Grammar withTablePrefix(\Illuminate\Database\Grammar $grammar) - * @method static void withoutTablePrefix(\Closure $callback) + * @method static mixed withoutTablePrefix(\Closure $callback) * @method static string getServerVersion() * @method static void resolverFor(string $driver, \Closure $callback) * @method static \Closure|null getResolver(string $driver) diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index fccccaa2fbd1..a388c6b415dc 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -10,12 +10,13 @@ * @method static void morphUsingUlids() * @method static bool createDatabase(string $name) * @method static bool dropDatabaseIfExists(string $name) + * @method static array getSchemas() * @method static bool hasTable(string $table) * @method static bool hasView(string $view) - * @method static array getTables() - * @method static array getTableListing() - * @method static array getViews() - * @method static array getTypes() + * @method static array getTables(string|string[]|null $schema = null) + * @method static array getTableListing(string|string[]|null $schema = null, bool $schemaQualified = true) + * @method static array getViews(string|string[]|null $schema = null) + * @method static array getTypes(string|string[]|null $schema = null) * @method static bool hasColumn(string $table, string $column) * @method static bool hasColumns(string $table, array $columns) * @method static void whenTableHasColumn(string $table, string $column, \Closure $callback) @@ -42,6 +43,9 @@ * @method static \Illuminate\Database\Connection getConnection() * @method static \Illuminate\Database\Schema\Builder setConnection(\Illuminate\Database\Connection $connection) * @method static void blueprintResolver(\Closure $resolver) + * @method static string[]|null getCurrentSchemaListing() + * @method static string|null getCurrentSchemaName() + * @method static array parseSchemaAndTable(string $reference, string|bool|null $withDefaultSchema = null) * @method static void macro(string $name, object|callable $macro) * @method static void mixin(object $mixin, bool $replace = true) * @method static bool hasMacro(string $name) diff --git a/tests/Database/DatabaseMariaDbSchemaBuilderTest.php b/tests/Database/DatabaseMariaDbSchemaBuilderTest.php index d8d7a64c17cc..eed08f0d5d48 100755 --- a/tests/Database/DatabaseMariaDbSchemaBuilderTest.php +++ b/tests/Database/DatabaseMariaDbSchemaBuilderTest.php @@ -38,7 +38,7 @@ public function testGetColumnListing() $connection->shouldReceive('getDatabaseName')->andReturn('db'); $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $grammar->shouldReceive('compileColumns')->with('db', 'prefix_table')->once()->andReturn('sql'); + $grammar->shouldReceive('compileColumns')->with(null, 'prefix_table')->once()->andReturn('sql'); $processor->shouldReceive('processColumns')->once()->andReturn([['name' => 'column']]); $builder = new MariaDbBuilder($connection); $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); diff --git a/tests/Database/DatabaseMySQLSchemaBuilderTest.php b/tests/Database/DatabaseMySQLSchemaBuilderTest.php index c5fa3ea273f2..78f3900317a2 100755 --- a/tests/Database/DatabaseMySQLSchemaBuilderTest.php +++ b/tests/Database/DatabaseMySQLSchemaBuilderTest.php @@ -38,7 +38,7 @@ public function testGetColumnListing() $connection->shouldReceive('getDatabaseName')->andReturn('db'); $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $grammar->shouldReceive('compileColumns')->with('db', 'prefix_table')->once()->andReturn('sql'); + $grammar->shouldReceive('compileColumns')->with(null, 'prefix_table')->once()->andReturn('sql'); $processor->shouldReceive('processColumns')->once()->andReturn([['name' => 'column']]); $builder = new MySqlBuilder($connection); $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); diff --git a/tests/Database/DatabasePostgresBuilderTest.php b/tests/Database/DatabasePostgresBuilderTest.php index c6f79c130704..ea6f77da62e3 100644 --- a/tests/Database/DatabasePostgresBuilderTest.php +++ b/tests/Database/DatabasePostgresBuilderTest.php @@ -142,7 +142,7 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathMissing() $connection->shouldReceive('getConfig')->with('schema')->andReturn(null); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumns')->with('public', 'foo')->andReturn('sql'); + $grammar->shouldReceive('compileColumns')->with(null, 'foo')->andReturn('sql'); $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]); $connection->shouldReceive('getTablePrefix'); $processor = m::mock(PostgresProcessor::class); @@ -159,7 +159,7 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathFilled() $connection->shouldReceive('getConfig')->with('search_path')->andReturn('myapp,public'); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumns')->with('myapp', 'foo')->andReturn('sql'); + $grammar->shouldReceive('compileColumns')->with(null, 'foo')->andReturn('sql'); $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]); $connection->shouldReceive('getTablePrefix'); $processor = m::mock(PostgresProcessor::class); @@ -177,7 +177,7 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathIsUserVari $connection->shouldReceive('getConfig')->with('search_path')->andReturn('$user'); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumns')->with('foouser', 'foo')->andReturn('sql'); + $grammar->shouldReceive('compileColumns')->with(null, 'foo')->andReturn('sql'); $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]); $connection->shouldReceive('getTablePrefix'); $processor = m::mock(PostgresProcessor::class); @@ -228,8 +228,8 @@ public function testDropAllTablesWhenSearchPathIsString() $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); $connection->shouldReceive('getPostProcessor')->andReturn($processor); $grammar->shouldReceive('compileTables')->andReturn('sql'); - $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'public']]); - $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'public']]); + $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'public', 'schema_qualified_name' => 'public.users']]); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'public', 'schema_qualified_name' => 'public.users']]); $grammar->shouldReceive('compileDropAllTables')->with(['public.users'])->andReturn('drop table "public"."users" cascade'); $connection->shouldReceive('statement')->with('drop table "public"."users" cascade'); $builder = $this->getBuilder($connection); @@ -247,9 +247,9 @@ public function testDropAllTablesWhenSearchPathIsStringOfMany() $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'foouser', 'schema_qualified_name' => 'foouser.users']]); $grammar->shouldReceive('compileTables')->andReturn('sql'); - $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'foouser', 'schema_qualified_name' => 'foouser.users']]); $grammar->shouldReceive('compileDropAllTables')->with(['foouser.users'])->andReturn('drop table "foouser"."users" cascade'); $connection->shouldReceive('statement')->with('drop table "foouser"."users" cascade'); $builder = $this->getBuilder($connection); @@ -272,9 +272,9 @@ public function testDropAllTablesWhenSearchPathIsArrayOfMany() $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'foouser', 'schema_qualified_name' => 'foouser.users']]); $grammar->shouldReceive('compileTables')->andReturn('sql'); - $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'foouser', 'schema_qualified_name' => 'foouser.users']]); $grammar->shouldReceive('compileDropAllTables')->with(['foouser.users'])->andReturn('drop table "foouser"."users" cascade'); $connection->shouldReceive('statement')->with('drop table "foouser"."users" cascade'); $builder = $this->getBuilder($connection); diff --git a/tests/Database/DatabasePostgresSchemaBuilderTest.php b/tests/Database/DatabasePostgresSchemaBuilderTest.php index c38c7d183614..0b1124463981 100755 --- a/tests/Database/DatabasePostgresSchemaBuilderTest.php +++ b/tests/Database/DatabasePostgresSchemaBuilderTest.php @@ -21,8 +21,6 @@ public function testHasTable() $connection = m::mock(Connection::class); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); - $connection->shouldReceive('getConfig')->with('schema')->andReturn('schema'); - $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); $builder = new PostgresBuilder($connection); $grammar->shouldReceive('compileTableExists')->twice()->andReturn('sql'); $connection->shouldReceive('getTablePrefix')->twice()->andReturn('prefix_'); @@ -39,10 +37,7 @@ public function testGetColumnListing() $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $connection->shouldReceive('getConfig')->with('database')->andReturn('db'); - $connection->shouldReceive('getConfig')->with('schema')->andReturn('schema'); - $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); - $grammar->shouldReceive('compileColumns')->with('public', 'prefix_table')->once()->andReturn('sql'); + $grammar->shouldReceive('compileColumns')->with(null, 'prefix_table')->once()->andReturn('sql'); $processor->shouldReceive('processColumns')->once()->andReturn([['name' => 'column']]); $builder = new PostgresBuilder($connection); $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index 27ac6b24f795..0a3764d4837b 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -4255,10 +4255,12 @@ public function testDeleteWithJoinMethod() public function testTruncateMethod() { $builder = $this->getBuilder(); - $builder->getConnection()->shouldReceive('statement')->once()->with('truncate table "users"', []); + $connection = $builder->getConnection(); + $connection->shouldReceive('statement')->once()->with('truncate table "users"', []); + $connection->shouldReceive('getSchemaBuilder->parseSchemaAndTable')->andReturn([null, 'users']); $builder->from('users')->truncate(); - $sqlite = new SQLiteGrammar; + $sqlite = (new SQLiteGrammar)->setConnection($connection); $builder = $this->getBuilder(); $builder->from('users'); $this->assertEquals([ @@ -4271,10 +4273,12 @@ public function testTruncateMethodWithPrefix() { $builder = $this->getBuilder(); $builder->getGrammar()->setTablePrefix('prefix_'); - $builder->getConnection()->shouldReceive('statement')->once()->with('truncate table "prefix_users"', []); + $connection = $builder->getConnection(); + $connection->shouldReceive('statement')->once()->with('truncate table "prefix_users"', []); + $connection->shouldReceive('getSchemaBuilder->parseSchemaAndTable')->andReturn([null, 'users']); $builder->from('users')->truncate(); - $sqlite = new SQLiteGrammar; + $sqlite = (new SQLiteGrammar)->setConnection($connection); $sqlite->setTablePrefix('prefix_'); $builder = $this->getBuilder(); $builder->from('users'); @@ -4284,6 +4288,25 @@ public function testTruncateMethodWithPrefix() ], $sqlite->compileTruncate($builder)); } + public function testTruncateMethodWithPrefixAndSchema() + { + $builder = $this->getBuilder(); + $builder->getGrammar()->setTablePrefix('prefix_'); + $connection = $builder->getConnection(); + $connection->shouldReceive('statement')->once()->with('truncate table "my_schema"."prefix_users"', []); + $connection->shouldReceive('getSchemaBuilder->parseSchemaAndTable')->andReturn(['my_schema', 'users']); + $builder->from('my_schema.users')->truncate(); + + $sqlite = (new SQLiteGrammar)->setConnection($connection); + $sqlite->setTablePrefix('prefix_'); + $builder = $this->getBuilder(); + $builder->from('my_schema.users'); + $this->assertEquals([ + 'delete from "my_schema".sqlite_sequence where name = ?' => ['prefix_users'], + 'delete from "my_schema"."prefix_users"' => [], + ], $sqlite->compileTruncate($builder)); + } + public function testPreserveAddsClosureToArray() { $builder = $this->getBuilder(); diff --git a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php index 245fd70467ef..0b9c5dbbc348 100755 --- a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php +++ b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php @@ -98,6 +98,16 @@ public function testDropIndex() $this->assertSame('drop index "foo"', $statements[0]); } + public function testDropIndexWithSchema() + { + $blueprint = new Blueprint($this->getConnection(), 'my_schema.users'); + $blueprint->dropIndex('foo'); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('drop index "my_schema"."foo"', $statements[0]); + } + public function testDropColumn() { $db = new Manager; @@ -225,6 +235,22 @@ public function testAddingIndex() $this->assertSame('create index "baz" on "users" ("foo", "bar")', $statements[0]); } + public function testAddingUniqueKeyWithSchema() + { + $blueprint = new Blueprint($this->getConnection(), 'foo.users'); + $blueprint->unique('foo', 'bar'); + + $this->assertSame(['create unique index "foo"."bar" on "users" ("foo")'], $blueprint->toSql()); + } + + public function testAddingIndexWithSchema() + { + $blueprint = new Blueprint($this->getConnection(), 'foo.users'); + $blueprint->index(['foo', 'bar'], 'baz'); + + $this->assertSame(['create index "foo"."baz" on "users" ("foo", "bar")'], $blueprint->toSql()); + } + public function testAddingSpatialIndex() { $this->expectException(RuntimeException::class); @@ -992,28 +1018,89 @@ public function testDroppingColumnsWorks() $this->assertEquals(['alter table "users" drop column "name"'], $blueprint->toSql()); } + public function testRenamingAndChangingColumnsWork() + { + $builder = mock(SQLiteBuilder::class) + ->makePartial() + ->shouldReceive('getColumns')->andReturn([ + ['name' => 'name', 'type_name' => 'varchar', 'type' => 'varchar', 'collation' => null, 'nullable' => false, 'default' => null, 'auto_increment' => false, 'comment' => null, 'generation' => null], + ['name' => 'age', 'type_name' => 'varchar', 'type' => 'varchar', 'collation' => null, 'nullable' => false, 'default' => null, 'auto_increment' => false, 'comment' => null, 'generation' => null], + ]) + ->shouldReceive('getIndexes')->andReturn([]) + ->shouldReceive('getForeignKeys')->andReturn([]) + ->getMock(); + + $connection = $this->getConnection(builder: $builder); + $connection->shouldReceive('scalar')->with('pragma foreign_keys')->andReturn(false); + + $blueprint = new Blueprint($connection, 'users'); + $blueprint->renameColumn('name', 'first_name'); + $blueprint->integer('age')->change(); + + $this->assertEquals([ + 'alter table "users" rename column "name" to "first_name"', + 'create table "__temp__users" ("first_name" varchar not null, "age" integer not null)', + 'insert into "__temp__users" ("first_name", "age") select "first_name", "age" from "users"', + 'drop table "users"', + 'alter table "__temp__users" rename to "users"', + ], $blueprint->toSql()); + } + + public function testRenamingAndChangingColumnsWorkWithSchema() + { + $builder = mock(SQLiteBuilder::class) + ->makePartial() + ->shouldReceive('getColumns')->andReturn([ + ['name' => 'name', 'type_name' => 'varchar', 'type' => 'varchar', 'collation' => null, 'nullable' => false, 'default' => null, 'auto_increment' => false, 'comment' => null, 'generation' => null], + ['name' => 'age', 'type_name' => 'varchar', 'type' => 'varchar', 'collation' => null, 'nullable' => false, 'default' => null, 'auto_increment' => false, 'comment' => null, 'generation' => null], + ]) + ->shouldReceive('getIndexes')->andReturn([]) + ->shouldReceive('getForeignKeys')->andReturn([]) + ->getMock(); + + $connection = $this->getConnection(builder: $builder); + $connection->shouldReceive('scalar')->with('pragma foreign_keys')->andReturn(false); + + $blueprint = new Blueprint($connection, 'my_schema.users'); + $blueprint->renameColumn('name', 'first_name'); + $blueprint->integer('age')->change(); + + $this->assertEquals([ + 'alter table "my_schema"."users" rename column "name" to "first_name"', + 'create table "my_schema"."__temp__users" ("first_name" varchar not null, "age" integer not null)', + 'insert into "my_schema"."__temp__users" ("first_name", "age") select "first_name", "age" from "my_schema"."users"', + 'drop table "my_schema"."users"', + 'alter table "my_schema"."__temp__users" rename to "users"', + ], $blueprint->toSql()); + } + protected function getConnection( ?SQLiteGrammar $grammar = null, ?SQLiteBuilder $builder = null, + $prefix = '' ) { - $grammar ??= $this->getGrammar(); + $connection = m::mock(Connection::class); + $grammar ??= $this->getGrammar($connection); $builder ??= $this->getBuilder(); - return m::mock(Connection::class) + return $connection + ->shouldReceive('getTablePrefix')->andReturn($prefix) + ->shouldReceive('getConfig')->andReturn(null) ->shouldReceive('getSchemaGrammar')->andReturn($grammar) ->shouldReceive('getSchemaBuilder')->andReturn($builder) ->shouldReceive('getServerVersion')->andReturn('3.35') ->getMock(); } - public function getGrammar() + public function getGrammar(?Connection $connection = null) { - return new SQLiteGrammar(); + return (new SQLiteGrammar())->setConnection($connection ?? $this->getConnection()); } public function getBuilder() { return mock(SQLiteBuilder::class) + ->makePartial() ->shouldReceive('getColumns')->andReturn([]) ->shouldReceive('getIndexes')->andReturn([]) ->shouldReceive('getForeignKeys')->andReturn([]) diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index 55e64b7d77a6..d069d530dbce 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -236,6 +236,28 @@ public function testDropColumn() $this->assertStringContainsString('alter table "users" drop column "foo"', $getSql(new SqlServerGrammar)[0]); } + public function testNativeColumnModifyingOnMySql() + { + $blueprint = $this->getBlueprint(new MySqlGrammar, 'users', function ($table) { + $table->double('amount')->nullable()->invisible()->after('name')->change(); + $table->timestamp('added_at', 4)->nullable(false)->useCurrent()->useCurrentOnUpdate()->change(); + $table->enum('difficulty', ['easy', 'hard'])->default('easy')->charset('utf8mb4')->collation('unicode')->change(); + $table->geometry('positions', 'multipolygon', 1234)->storedAs('expression')->change(); + $table->string('old_name', 50)->renameTo('new_name')->change(); + $table->bigIncrements('id')->first()->from(10)->comment('my comment')->change(); + }); + + $this->assertEquals([ + 'alter table `users` modify `amount` double null invisible after `name`', + 'alter table `users` modify `added_at` timestamp(4) not null default CURRENT_TIMESTAMP(4) on update CURRENT_TIMESTAMP(4)', + "alter table `users` modify `difficulty` enum('easy', 'hard') character set utf8mb4 collate 'unicode' not null default 'easy'", + 'alter table `users` modify `positions` multipolygon srid 1234 as (expression) stored', + 'alter table `users` change `old_name` `new_name` varchar(50) not null', + "alter table `users` modify `id` bigint unsigned not null auto_increment comment 'my comment' first", + 'alter table `users` auto_increment = 10', + ], $blueprint->toSql()); + } + public function testMacroable() { Blueprint::macro('foo', function () { diff --git a/tests/Database/DatabaseSchemaBuilderTest.php b/tests/Database/DatabaseSchemaBuilderTest.php index e5ffd52d3d4e..e2f03e36dcc2 100644 --- a/tests/Database/DatabaseSchemaBuilderTest.php +++ b/tests/Database/DatabaseSchemaBuilderTest.php @@ -47,11 +47,12 @@ public function testDropDatabaseIfExists() public function testHasTableCorrectlyCallsGrammar() { $connection = m::mock(Connection::class); - $grammar = m::mock(stdClass::class); + $grammar = m::mock(Grammar::class); $processor = m::mock(Processor::class); $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); $connection->shouldReceive('getPostProcessor')->andReturn($processor); $builder = new Builder($connection); + $grammar->shouldReceive('compileTableExists'); $grammar->shouldReceive('compileTables')->once()->andReturn('sql'); $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'prefix_table']]); $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); @@ -82,7 +83,7 @@ public function testGetColumnTypeAddsPrefix() $processor->shouldReceive('processColumns')->once()->andReturn([['name' => 'id', 'type_name' => 'integer']]); $builder = new Builder($connection); $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); - $grammar->shouldReceive('compileColumns')->once()->with('prefix_users')->andReturn('sql'); + $grammar->shouldReceive('compileColumns')->once()->with(null, 'prefix_users')->andReturn('sql'); $connection->shouldReceive('selectFromWriteConnection')->once()->with('sql')->andReturn([['name' => 'id', 'type_name' => 'integer']]); $this->assertSame('integer', $builder->getColumnType('users', 'id')); diff --git a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php index a22bf69c095f..8877e450770a 100755 --- a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php +++ b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php @@ -53,7 +53,9 @@ public function testBasicCreateTable() public function testCreateTemporaryTable() { - $blueprint = new Blueprint($this->getConnection(), 'users'); + $connection = $this->getConnection(); + $connection->shouldReceive('getTablePrefix')->andReturn(''); + $blueprint = new Blueprint($connection, 'users'); $blueprint->create(); $blueprint->temporary(); $blueprint->increments('id'); @@ -64,6 +66,21 @@ public function testCreateTemporaryTable() $this->assertSame('create table "#users" ("id" int not null identity primary key, "email" nvarchar(255) not null)', $statements[0]); } + public function testCreateTemporaryTableWithPrefix() + { + $connection = $this->getConnection(); + $connection->shouldReceive('getTablePrefix')->andReturn('prefix_'); + $blueprint = new Blueprint($connection, 'users'); + $blueprint->create(); + $blueprint->temporary(); + $blueprint->increments('id'); + $blueprint->string('email'); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('create table "#prefix_users" ("id" int not null identity primary key, "email" nvarchar(255) not null)', $statements[0]); + } + public function testDropTable() { $blueprint = new Blueprint($this->getConnection(), 'users'); @@ -953,18 +970,20 @@ protected function getConnection( ?SqlServerGrammar $grammar = null, ?SqlServerBuilder $builder = null ) { - $grammar ??= $this->getGrammar(); + $connection = m::mock(Connection::class); + $grammar ??= $this->getGrammar($connection); $builder ??= $this->getBuilder(); - return m::mock(Connection::class) + return $connection ->shouldReceive('getSchemaGrammar')->andReturn($grammar) ->shouldReceive('getSchemaBuilder')->andReturn($builder) ->getMock(); } - public function getGrammar() + public function getGrammar(?Connection $connection = null) { - return new SqlServerGrammar; + return ($grammar = new SqlServerGrammar) + ->setConnection($connection ?? $this->getConnection(grammar: $grammar)); } public function getBuilder() diff --git a/tests/Foundation/Testing/DatabaseTruncationTest.php b/tests/Foundation/Testing/DatabaseTruncationTest.php index 8b28d69cab76..1f972145cdbd 100644 --- a/tests/Foundation/Testing/DatabaseTruncationTest.php +++ b/tests/Foundation/Testing/DatabaseTruncationTest.php @@ -47,8 +47,8 @@ protected function tearDown(): void public function testTruncateTables() { $connection = $this->arrangeConnection($truncatedTables, [ - ['schema' => null, 'name' => 'foo'], - ['schema' => null, 'name' => 'bar'], + ['schema' => null, 'name' => 'foo', 'schema_qualified_name' => 'foo'], + ['schema' => null, 'name' => 'bar', 'schema_qualified_name' => 'bar'], ]); $this->truncateTablesForConnection($connection, 'test'); @@ -61,10 +61,10 @@ public function testTruncateTablesWithTablesToTruncateProperty() $this->tablesToTruncate = ['foo', 'bar', 'qux']; $connection = $this->arrangeConnection($truncatedTables, [ - ['schema' => null, 'name' => 'migrations'], - ['schema' => null, 'name' => 'foo'], - ['schema' => null, 'name' => 'bar'], - ['schema' => null, 'name' => 'baz'], + ['schema' => null, 'name' => 'migrations', 'schema_qualified_name' => 'migrations'], + ['schema' => null, 'name' => 'foo', 'schema_qualified_name' => 'foo'], + ['schema' => null, 'name' => 'bar', 'schema_qualified_name' => 'bar'], + ['schema' => null, 'name' => 'baz', 'schema_qualified_name' => 'baz'], ]); $this->truncateTablesForConnection($connection, 'test'); @@ -77,10 +77,10 @@ public function testTruncateTablesWithExceptTablesProperty() $this->exceptTables = ['baz', 'qux']; $connection = $this->arrangeConnection($truncatedTables, [ - ['schema' => null, 'name' => 'migrations'], - ['schema' => null, 'name' => 'foo'], - ['schema' => null, 'name' => 'bar'], - ['schema' => null, 'name' => 'baz'], + ['schema' => null, 'name' => 'migrations', 'schema_qualified_name' => 'migrations'], + ['schema' => null, 'name' => 'foo', 'schema_qualified_name' => 'foo'], + ['schema' => null, 'name' => 'bar', 'schema_qualified_name' => 'bar'], + ['schema' => null, 'name' => 'baz', 'schema_qualified_name' => 'baz'], ]); $this->truncateTablesForConnection($connection, 'test'); @@ -91,12 +91,12 @@ public function testTruncateTablesWithExceptTablesProperty() public function testTruncateTablesWithSchema() { $connection = $this->arrangeConnection($truncatedTables, [ - ['schema' => 'public', 'name' => 'migrations'], - ['schema' => 'public', 'name' => 'foo'], - ['schema' => 'public', 'name' => 'bar'], - ['schema' => 'private', 'name' => 'migrations'], - ['schema' => 'private', 'name' => 'foo'], - ['schema' => 'private', 'name' => 'baz'], + ['schema' => 'public', 'name' => 'migrations', 'schema_qualified_name' => 'public.migrations'], + ['schema' => 'public', 'name' => 'foo', 'schema_qualified_name' => 'public.foo'], + ['schema' => 'public', 'name' => 'bar', 'schema_qualified_name' => 'public.bar'], + ['schema' => 'private', 'name' => 'migrations', 'schema_qualified_name' => 'private.migrations'], + ['schema' => 'private', 'name' => 'foo', 'schema_qualified_name' => 'private.foo'], + ['schema' => 'private', 'name' => 'baz', 'schema_qualified_name' => 'private.baz'], ]); $this->truncateTablesForConnection($connection, 'test'); @@ -109,13 +109,13 @@ public function testTruncateTablesWithSchemaTablesToTruncateProperty() $this->tablesToTruncate = ['foo', 'public.bar']; $connection = $this->arrangeConnection($truncatedTables, [ - ['schema' => 'public', 'name' => 'migrations'], - ['schema' => 'public', 'name' => 'foo'], - ['schema' => 'public', 'name' => 'bar'], - ['schema' => 'public', 'name' => 'baz'], - ['schema' => 'private', 'name' => 'migrations'], - ['schema' => 'private', 'name' => 'foo'], - ['schema' => 'private', 'name' => 'bar'], + ['schema' => 'public', 'name' => 'migrations', 'schema_qualified_name' => 'public.migrations'], + ['schema' => 'public', 'name' => 'foo', 'schema_qualified_name' => 'public.foo'], + ['schema' => 'public', 'name' => 'bar', 'schema_qualified_name' => 'public.bar'], + ['schema' => 'public', 'name' => 'baz', 'schema_qualified_name' => 'public.baz'], + ['schema' => 'private', 'name' => 'migrations', 'schema_qualified_name' => 'private.migrations'], + ['schema' => 'private', 'name' => 'foo', 'schema_qualified_name' => 'private.foo'], + ['schema' => 'private', 'name' => 'bar', 'schema_qualified_name' => 'private.bar'], ]); $this->truncateTablesForConnection($connection, 'test'); @@ -128,13 +128,13 @@ public function testTruncateTablesWithSchemaAndExceptTablesProperty() $this->exceptTables = ['foo', 'public.bar']; $connection = $this->arrangeConnection($truncatedTables, [ - ['schema' => 'public', 'name' => 'migrations'], - ['schema' => 'public', 'name' => 'foo'], - ['schema' => 'public', 'name' => 'bar'], - ['schema' => 'public', 'name' => 'baz'], - ['schema' => 'private', 'name' => 'migrations'], - ['schema' => 'private', 'name' => 'foo'], - ['schema' => 'private', 'name' => 'bar'], + ['schema' => 'public', 'name' => 'migrations', 'schema_qualified_name' => 'public.migrations'], + ['schema' => 'public', 'name' => 'foo', 'schema_qualified_name' => 'public.foo'], + ['schema' => 'public', 'name' => 'bar', 'schema_qualified_name' => 'public.bar'], + ['schema' => 'public', 'name' => 'baz', 'schema_qualified_name' => 'public.baz'], + ['schema' => 'private', 'name' => 'migrations', 'schema_qualified_name' => 'private.migrations'], + ['schema' => 'private', 'name' => 'foo', 'schema_qualified_name' => 'private.foo'], + ['schema' => 'private', 'name' => 'bar', 'schema_qualified_name' => 'private.bar'], ]); $this->truncateTablesForConnection($connection, 'test'); @@ -145,11 +145,11 @@ public function testTruncateTablesWithSchemaAndExceptTablesProperty() public function testTruncateTablesWithConnectionPrefix() { $connection = $this->arrangeConnection($truncatedTables, [ - ['schema' => 'public', 'name' => 'my_migrations'], - ['schema' => 'public', 'name' => 'my_foo'], - ['schema' => 'public', 'name' => 'my_baz'], - ['schema' => 'private', 'name' => 'my_migrations'], - ['schema' => 'private', 'name' => 'my_foo'], + ['schema' => 'public', 'name' => 'my_migrations', 'schema_qualified_name' => 'public.my_migrations'], + ['schema' => 'public', 'name' => 'my_foo', 'schema_qualified_name' => 'public.my_foo'], + ['schema' => 'public', 'name' => 'my_baz', 'schema_qualified_name' => 'public.my_baz'], + ['schema' => 'private', 'name' => 'my_migrations', 'schema_qualified_name' => 'private.my_migrations'], + ['schema' => 'private', 'name' => 'my_foo', 'schema_qualified_name' => 'private.my_foo'], ], 'my_'); $this->truncateTablesForConnection($connection, 'test'); @@ -160,14 +160,14 @@ public function testTruncateTablesWithConnectionPrefix() public function testTruncateTablesOnPgsqlWithSearchPath() { $connection = $this->arrangeConnection($truncatedTables, [ - ['schema' => 'public', 'name' => 'migrations'], - ['schema' => 'public', 'name' => 'foo'], - ['schema' => 'public', 'name' => 'bar'], - ['schema' => 'my_schema', 'name' => 'foo'], - ['schema' => 'my_schema', 'name' => 'baz'], - ['schema' => 'private', 'name' => 'migrations'], - ['schema' => 'private', 'name' => 'foo'], - ['schema' => 'private', 'name' => 'baz'], + ['schema' => 'public', 'name' => 'migrations', 'schema_qualified_name' => 'public.migrations'], + ['schema' => 'public', 'name' => 'foo', 'schema_qualified_name' => 'public.foo'], + ['schema' => 'public', 'name' => 'bar', 'schema_qualified_name' => 'public.bar'], + ['schema' => 'my_schema', 'name' => 'foo', 'schema_qualified_name' => 'my_schema.foo'], + ['schema' => 'my_schema', 'name' => 'baz', 'schema_qualified_name' => 'my_schema.baz'], + ['schema' => 'private', 'name' => 'migrations', 'schema_qualified_name' => 'private.migrations'], + ['schema' => 'private', 'name' => 'foo', 'schema_qualified_name' => 'private.foo'], + ['schema' => 'private', 'name' => 'baz', 'schema_qualified_name' => 'private.baz'], ], '', PostgresBuilder::class, ['my_schema', 'public']); $this->truncateTablesForConnection($connection, 'test'); @@ -181,11 +181,12 @@ private function arrangeConnection( $actual = []; $schema = m::mock($builder ?? Builder::class); - $schema->shouldReceive('getTables')->once()->andReturn($allTables); - - if ($builder === PostgresBuilder::class && $schemas) { - $schema->shouldReceive('getSchemas')->once()->andReturn($schemas); - } + $schema->shouldReceive('getTables')->with($schemas)->once()->andReturn( + empty($schemas) + ? $allTables + : array_filter($allTables, fn ($table) => in_array($table['schema'], $schemas)) + ); + $schema->shouldReceive('getCurrentSchemaListing')->once()->andReturn($schemas); $connection = m::mock(Connection::class); $connection->shouldReceive('getTablePrefix')->andReturn($prefix); diff --git a/tests/Integration/Database/SchemaBuilderSchemaNameTest.php b/tests/Integration/Database/SchemaBuilderSchemaNameTest.php index b88b1964aadd..93bd151f4b1b 100644 --- a/tests/Integration/Database/SchemaBuilderSchemaNameTest.php +++ b/tests/Integration/Database/SchemaBuilderSchemaNameTest.php @@ -8,17 +8,42 @@ use Orchestra\Testbench\Attributes\RequiresDatabase; use PHPUnit\Framework\Attributes\DataProvider; -#[RequiresDatabase(['pgsql', 'sqlsrv'])] class SchemaBuilderSchemaNameTest extends DatabaseTestCase { + protected function setUp(): void + { + parent::setUp(); + + if ($this->usesSqliteInMemoryDatabaseConnection()) { + $this->markTestSkipped('Test cannot be run using :memory: database connection, SQLite test file is here: \Illuminate\Tests\Integration\Database\Sqlite\SchemaBuilderSchemaNameTest'); + } + } + protected function defineDatabaseMigrations() { - if ($this->driver === 'pgsql') { - DB::connection('without-prefix')->statement('create schema if not exists my_schema'); - DB::connection('with-prefix')->statement('create schema if not exists my_schema'); + if (in_array($this->driver, ['mariadb', 'mysql'])) { + Schema::createDatabase('my_schema'); + } elseif ($this->driver === 'sqlite') { + DB::connection('without-prefix')->statement("attach database ':memory:' as my_schema"); + DB::connection('with-prefix')->statement("attach database ':memory:' as my_schema"); + } elseif ($this->driver === 'pgsql') { + DB::statement('create schema if not exists my_schema'); } elseif ($this->driver === 'sqlsrv') { - DB::connection('without-prefix')->statement("if schema_id('my_schema') is null begin exec('create schema my_schema') end"); - DB::connection('with-prefix')->statement("if schema_id('my_schema') is null begin exec('create schema my_schema') end"); + DB::statement("if schema_id('my_schema') is null begin exec('create schema my_schema') end"); + } + } + + protected function destroyDatabaseMigrations() + { + if (in_array($this->driver, ['mariadb', 'mysql'])) { + Schema::dropDatabaseIfExists('my_schema'); + } elseif ($this->driver === 'sqlite') { + DB::connection('without-prefix')->statement('detach database my_schema'); + DB::connection('with-prefix')->statement('detach database my_schema'); + } elseif ($this->driver === 'pgsql') { + DB::statement('drop schema if exists my_schema cascade'); + } elseif ($this->driver === 'sqlsrv') { + // DB::statement("if schema_id('my_schema') is not null begin exec('drop schema my_schema') end"); } } @@ -26,12 +51,34 @@ protected function defineEnvironment($app) { parent::defineEnvironment($app); + $connection = $app['config']->get('database.default'); + + $app['config']->set("database.connections.$connection.prefix_indexes", true); $app['config']->set('database.connections.pgsql.search_path', 'public,my_schema'); - $app['config']->set('database.connections.without-prefix', $app['config']->get('database.connections.'.$this->driver)); + $app['config']->set('database.connections.without-prefix', $app['config']->get('database.connections.'.$connection)); $app['config']->set('database.connections.with-prefix', $app['config']->get('database.connections.without-prefix')); $app['config']->set('database.connections.with-prefix.prefix', 'example_'); } + #[DataProvider('connectionProvider')] + public function testSchemas($connection) + { + $schema = Schema::connection($connection); + + $schemas = $schema->getSchemas(); + + $this->assertSame($schema->getCurrentSchemaName(), collect($schemas)->firstWhere('default')['name']); + $this->assertEqualsCanonicalizing( + match ($this->driver) { + 'mysql', 'mariadb' => ['laravel', 'my_schema'], + 'pgsql' => ['public', 'my_schema'], + 'sqlite' => ['main', 'my_schema'], + 'sqlsrv' => ['dbo', 'guest', 'my_schema'], + }, + array_column($schemas, 'name'), + ); + } + #[DataProvider('connectionProvider')] public function testCreate($connection) { @@ -43,6 +90,14 @@ public function testCreate($connection) $this->assertTrue($schema->hasTable('my_schema.table')); $this->assertFalse($schema->hasTable('table')); + + $currentSchema = $schema->getCurrentSchemaName(); + $tableName = $connection === 'with-prefix' ? 'example_table' : 'table'; + + $this->assertEqualsCanonicalizing( + [$currentSchema.'.migrations', 'my_schema.'.$tableName], + $schema->getTableListing([$currentSchema, 'my_schema']) + ); } #[DataProvider('connectionProvider')] @@ -62,7 +117,11 @@ public function testRename($connection) $this->assertTrue($schema->hasTable('table')); $this->assertFalse($schema->hasTable('my_table')); - $schema->rename('my_schema.table', 'new_table'); + if (in_array($this->driver, ['mariadb', 'mysql'])) { + $schema->rename('my_schema.table', 'my_schema.new_table'); + } else { + $schema->rename('my_schema.table', 'new_table'); + } $schema->rename('table', 'my_table'); $this->assertTrue($schema->hasTable('my_schema.new_table')); @@ -86,10 +145,23 @@ public function testDrop($connection) $this->assertTrue($schema->hasTable('my_schema.table')); $this->assertTrue($schema->hasTable('table')); + $currentSchema = $schema->getCurrentSchemaName(); + $tableName = $connection === 'with-prefix' ? 'example_table' : 'table'; + + $this->assertEqualsCanonicalizing( + [$currentSchema.'.migrations', $currentSchema.'.'.$tableName, 'my_schema.'.$tableName], + $schema->getTableListing([$currentSchema, 'my_schema']) + ); + $schema->drop('my_schema.table'); $this->assertFalse($schema->hasTable('my_schema.table')); $this->assertTrue($schema->hasTable('table')); + + $this->assertEqualsCanonicalizing( + [$currentSchema.'.migrations', $currentSchema.'.'.$tableName], + $schema->getTableListing([$currentSchema, 'my_schema']) + ); } #[DataProvider('connectionProvider')] @@ -210,8 +282,16 @@ public function testModifyColumns($connection) $this->assertStringContainsString('default title', collect($schema->getColumns('my_table'))->firstWhere('name', 'title')['default']); $this->assertEquals($this->driver === 'sqlsrv' ? 'nvarchar' : 'varchar', $schema->getColumnType('my_schema.table', 'name')); $this->assertEquals($this->driver === 'sqlsrv' ? 'nvarchar' : 'varchar', $schema->getColumnType('my_table', 'title')); - $this->assertEquals($this->driver === 'pgsql' ? 'int8' : 'bigint', $schema->getColumnType('my_schema.table', 'count')); - $this->assertEquals($this->driver === 'pgsql' ? 'int8' : 'bigint', $schema->getColumnType('my_table', 'count')); + $this->assertEquals(match ($this->driver) { + 'pgsql' => 'int8', + 'sqlite' => 'integer', + default => 'bigint', + }, $schema->getColumnType('my_schema.table', 'count')); + $this->assertEquals(match ($this->driver) { + 'pgsql' => 'int8', + 'sqlite' => 'integer', + default => 'bigint', + }, $schema->getColumnType('my_table', 'count')); } #[DataProvider('connectionProvider')] @@ -306,6 +386,7 @@ public function testIndexes($connection) } #[DataProvider('connectionProvider')] + #[RequiresDatabase(['mariadb', 'mysql', 'pgsql', 'sqlsrv'])] public function testForeignKeys($connection) { $schema = Schema::connection($connection); @@ -315,7 +396,8 @@ public function testForeignKeys($connection) }); $schema->create('my_schema.table', function (Blueprint $table) { $table->id(); - $table->foreignId('my_table_id')->constrained(); + $table->foreignId('my_table_id') + ->constrained(table: in_array($this->driver, ['mariadb', 'mysql']) ? 'laravel.my_tables' : null); }); $schema->create('table', function (Blueprint $table) { $table->unsignedBigInteger('table_id'); @@ -324,10 +406,15 @@ public function testForeignKeys($connection) $schemaTableName = $connection === 'with-prefix' ? 'example_table' : 'table'; $tableName = $connection === 'with-prefix' ? 'example_my_tables' : 'my_tables'; + $defaultSchemaName = match ($this->driver) { + 'pgsql' => 'public', + 'sqlsrv' => 'dbo', + default => 'laravel', + }; $this->assertTrue(collect($schema->getForeignKeys('my_schema.table'))->contains( fn ($foreign) => $foreign['columns'] === ['my_table_id'] - && $foreign['foreign_table'] === $tableName && in_array($foreign['foreign_schema'], ['public', 'dbo']) + && $foreign['foreign_table'] === $tableName && $foreign['foreign_schema'] === $defaultSchemaName && $foreign['foreign_columns'] === ['id'] )); @@ -348,29 +435,80 @@ public function testForeignKeys($connection) $this->assertEmpty($schema->getForeignKeys('table')); } + #[DataProvider('connectionProvider')] + #[RequiresDatabase('sqlite')] + public function testForeignKeysOnSameSchema($connection) + { + $schema = Schema::connection($connection); + + $schema->create('my_schema.my_tables', function (Blueprint $table) { + $table->id(); + }); + $schema->create('my_schema.table', function (Blueprint $table) { + $table->id(); + $table->foreignId('my_table_id')->constrained(); + }); + $schema->create('my_schema.second_table', function (Blueprint $table) { + $table->unsignedBigInteger('table_id'); + $table->foreign('table_id')->references('id')->on('table'); + }); + + $myTableName = $connection === 'with-prefix' ? 'example_my_tables' : 'my_tables'; + $tableName = $connection === 'with-prefix' ? 'example_table' : 'table'; + + $this->assertTrue(collect($schema->getForeignKeys('my_schema.table'))->contains( + fn ($foreign) => $foreign['columns'] === ['my_table_id'] + && $foreign['foreign_table'] === $myTableName && $foreign['foreign_schema'] === 'my_schema' + && $foreign['foreign_columns'] === ['id'] + )); + + $this->assertTrue(collect($schema->getForeignKeys('my_schema.second_table'))->contains( + fn ($foreign) => $foreign['columns'] === ['table_id'] + && $foreign['foreign_table'] === $tableName && $foreign['foreign_schema'] === 'my_schema' + && $foreign['foreign_columns'] === ['id'] + )); + + $schema->table('my_schema.table', function (Blueprint $table) { + $table->dropForeign(['my_table_id']); + }); + + $this->assertEmpty($schema->getForeignKeys('my_schema.table')); + } + #[DataProvider('connectionProvider')] public function testHasView($connection) { - $connection = DB::connection($connection); - $schema = $connection->getSchemaBuilder(); + $db = DB::connection($connection); + $schema = $db->getSchemaBuilder(); - $connection->statement('create view '.$connection->getSchemaGrammar()->wrapTable('my_schema.view').' (name) as select 1'); - $connection->statement('create view '.$connection->getSchemaGrammar()->wrapTable('my_view').' (name) as select 1'); + $db->statement('create view '.$db->getSchemaGrammar()->wrapTable('my_schema.view').' (name) as select 1'); + $db->statement('create view '.$db->getSchemaGrammar()->wrapTable('my_view').' (name) as select 1'); $this->assertTrue($schema->hasView('my_schema.view')); $this->assertTrue($schema->hasView('my_view')); $this->assertTrue($schema->hasColumn('my_schema.view', 'name')); $this->assertTrue($schema->hasColumn('my_view', 'name')); - $connection->statement('drop view '.$connection->getSchemaGrammar()->wrapTable('my_schema.view')); - $connection->statement('drop view '.$connection->getSchemaGrammar()->wrapTable('my_view')); + $currentSchema = $schema->getCurrentSchemaName(); + $viewName = $connection === 'with-prefix' ? 'example_view' : 'view'; + $myViewName = $connection === 'with-prefix' ? 'example_my_view' : 'my_view'; + + $this->assertEqualsCanonicalizing( + [$currentSchema.'.'.$myViewName, 'my_schema.'.$viewName], + array_column($schema->getViews([$currentSchema, 'my_schema']), 'schema_qualified_name') + ); + + $db->statement('drop view '.$db->getSchemaGrammar()->wrapTable('my_schema.view')); + $db->statement('drop view '.$db->getSchemaGrammar()->wrapTable('my_view')); $this->assertFalse($schema->hasView('my_schema.view')); $this->assertFalse($schema->hasView('my_view')); + + $this->assertEmpty($schema->getViews([$currentSchema, 'my_schema'])); } #[DataProvider('connectionProvider')] - #[RequiresDatabase('pgsql')] + #[RequiresDatabase(['mariadb', 'mysql', 'pgsql'])] public function testComment($connection) { $schema = Schema::connection($connection); @@ -386,12 +524,13 @@ public function testComment($connection) $tables = collect($schema->getTables()); $tableName = $connection === 'with-prefix' ? 'example_table' : 'table'; + $defaultSchema = $this->driver === 'pgsql' ? 'public' : 'laravel'; $this->assertEquals('comment on schema table', $tables->first(fn ($table) => $table['name'] === $tableName && $table['schema'] === 'my_schema')['comment'] ); $this->assertEquals('comment on table', - $tables->first(fn ($table) => $table['name'] === $tableName && $table['schema'] === 'public')['comment'] + $tables->first(fn ($table) => $table['name'] === $tableName && $table['schema'] === $defaultSchema)['comment'] ); $this->assertEquals('comment on schema column', collect($schema->getColumns('my_schema.table'))->firstWhere('name', 'name')['comment'] @@ -402,7 +541,7 @@ public function testComment($connection) } #[DataProvider('connectionProvider')] - #[RequiresDatabase('pgsql')] + #[RequiresDatabase(['mariadb', 'mysql', 'pgsql'])] public function testAutoIncrementStartingValue($connection) { $this->expectNotToPerformAssertions(); @@ -440,7 +579,7 @@ public function testHasTable($connection) 'database.connections.'.$connection.'.password' => 'Passw0rd', ]); - $this->assertEquals('my_schema', $db->scalar('select schema_name()')); + $this->assertEquals('my_schema', $schema->getCurrentSchemaName()); $schema->create('table', function (Blueprint $table) { $table->id(); diff --git a/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php b/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php index b8aaf1a0f818..2db84efd5aa7 100644 --- a/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php +++ b/tests/Integration/Database/Sqlite/DatabaseSchemaBlueprintTest.php @@ -74,28 +74,6 @@ public function testRenamingColumnsWorks() $this->assertTrue($schema->hasColumns('test', ['bar', 'qux'])); } - public function testNativeColumnModifyingOnMySql() - { - $blueprint = $this->getBlueprint(new MySqlGrammar, 'users', function ($table) { - $table->double('amount')->nullable()->invisible()->after('name')->change(); - $table->timestamp('added_at', 4)->nullable(false)->useCurrent()->useCurrentOnUpdate()->change(); - $table->enum('difficulty', ['easy', 'hard'])->default('easy')->charset('utf8mb4')->collation('unicode')->change(); - $table->geometry('positions', 'multipolygon', 1234)->storedAs('expression')->change(); - $table->string('old_name', 50)->renameTo('new_name')->change(); - $table->bigIncrements('id')->first()->from(10)->comment('my comment')->change(); - }); - - $this->assertEquals([ - 'alter table `users` modify `amount` double null invisible after `name`', - 'alter table `users` modify `added_at` timestamp(4) not null default CURRENT_TIMESTAMP(4) on update CURRENT_TIMESTAMP(4)', - "alter table `users` modify `difficulty` enum('easy', 'hard') character set utf8mb4 collate 'unicode' not null default 'easy'", - 'alter table `users` modify `positions` multipolygon srid 1234 as (expression) stored', - 'alter table `users` change `old_name` `new_name` varchar(50) not null', - "alter table `users` modify `id` bigint unsigned not null auto_increment comment 'my comment' first", - 'alter table `users` auto_increment = 10', - ], $blueprint->toSql()); - } - public function testNativeColumnModifyingOnPostgreSql() { $blueprint = $this->getBlueprint(new PostgresGrammar, 'users', function ($table) { @@ -542,6 +520,7 @@ protected function getBlueprint( Closure $callback, ): Blueprint { $connection = DB::connection()->setSchemaGrammar($grammar); + $grammar->setConnection($connection); return new Blueprint($connection, $table, $callback); } diff --git a/tests/Integration/Database/Sqlite/SchemaBuilderSchemaNameTest.php b/tests/Integration/Database/Sqlite/SchemaBuilderSchemaNameTest.php new file mode 100644 index 000000000000..03f52e9e875b --- /dev/null +++ b/tests/Integration/Database/Sqlite/SchemaBuilderSchemaNameTest.php @@ -0,0 +1,11 @@ +mustRun(); + remote('migrate:install'); } protected function tearDown(): void