diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 68e7cef97276..5e72687cb53e 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -1011,7 +1011,7 @@ protected function addUpdatedAtColumn(array $values) $qualifiedColumn = end($segments).'.'.$column; - $values[$qualifiedColumn] = $values[$column]; + $values[$qualifiedColumn] = Arr::get($values, $qualifiedColumn, $values[$column]); unset($values[$column]); diff --git a/src/Illuminate/Database/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index dbbaf805918a..361747c6e85d 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -387,11 +387,11 @@ protected function runMigration($migration, $method) $migration->getConnection() ); - $callback = function () use ($migration, $method) { + $callback = function () use ($connection, $migration, $method) { if (method_exists($migration, $method)) { $this->fireMigrationEvent(new MigrationStarted($migration, $method)); - $migration->{$method}(); + $this->runMethod($connection, $migration, $method); $this->fireMigrationEvent(new MigrationEnded($migration, $method)); } @@ -447,13 +447,34 @@ protected function getQueries($migration, $method) $migration->getConnection() ); - return $db->pretend(function () use ($migration, $method) { + return $db->pretend(function () use ($db, $migration, $method) { if (method_exists($migration, $method)) { - $migration->{$method}(); + $this->runMethod($db, $migration, $method); } }); } + /** + * Run a migration method on the given connection. + * + * @param \Illuminate\Database\Connection $connection + * @param object $migration + * @param string $method + * @return void + */ + protected function runMethod($connection, $migration, $method) + { + $previousConnection = $this->resolver->getDefaultConnection(); + + try { + $this->resolver->setDefaultConnection($connection->getName()); + + $migration->{$method}(); + } finally { + $this->resolver->setDefaultConnection($previousConnection); + } + } + /** * Resolve a migration instance from a file. * diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index 018f04ab902b..69560074d2b4 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -205,6 +205,15 @@ class Builder implements BuilderContract 'not similar to', 'not ilike', '~~*', '!~~*', ]; + /** + * All of the available bitwise operators. + * + * @var string[] + */ + public $bitwiseOperators = [ + '&', '|', '^', '<<', '>>', '&~', + ]; + /** * Whether to use write pdo for the select. * @@ -758,6 +767,10 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' } } + if ($this->isBitwiseOperator($operator)) { + $type = 'Bitwise'; + } + // Now that we are working with just a simple query we can put the elements // in our array and add the query binding to our array of bindings that // will be bound to each SQL statements when it is finally executed. @@ -841,6 +854,18 @@ protected function invalidOperator($operator) ! in_array(strtolower($operator), $this->grammar->getOperators(), true)); } + /** + * Determine if the operator is a bitwise operator. + * + * @param string $operator + * @return bool + */ + protected function isBitwiseOperator($operator) + { + return in_array(strtolower($operator), $this->bitwiseOperators, true) || + in_array(strtolower($operator), $this->grammar->getBitwiseOperators(), true); + } + /** * Add an "or where" clause to the query. * @@ -1927,6 +1952,10 @@ public function having($column, $operator = null, $value = null, $boolean = 'and [$value, $operator] = [$operator, '=']; } + if ($this->isBitwiseOperator($operator)) { + $type = 'Bitwise'; + } + $this->havings[] = compact('type', 'column', 'operator', 'value', 'boolean'); if (! $value instanceof Expression) { diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index e3ee52a2815e..91b8af7d2bca 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -18,6 +18,13 @@ class Grammar extends BaseGrammar */ protected $operators = []; + /** + * The grammar specific bitwise operators. + * + * @var array + */ + protected $bitwiseOperators = []; + /** * The components that make up a select clause. * @@ -255,6 +262,18 @@ protected function whereBasic(Builder $query, $where) return $this->wrap($where['column']).' '.$operator.' '.$value; } + /** + * Compile a bitwise operator where clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function whereBitwise(Builder $query, $where) + { + return $this->whereBasic($query, $where); + } + /** * Compile a "where in" clause. * @@ -1331,4 +1350,14 @@ public function getOperators() { return $this->operators; } + + /** + * Get the grammar specific bitwise operators. + * + * @return array + */ + public function getBitwiseOperators() + { + return $this->bitwiseOperators; + } } diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index 7c4ef0751fbe..6683e14452be 100755 --- a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php @@ -21,11 +21,11 @@ class PostgresGrammar extends Grammar ]; /** - * The grammar specific bit operators. + * The grammar specific bitwise operators. * * @var array */ - protected $bitOperators = [ + protected $bitwiseOperators = [ '~', '&', '|', '#', '<<', '>>', '<<=', '>>=', ]; @@ -50,6 +50,22 @@ protected function whereBasic(Builder $query, $where) return parent::whereBasic($query, $where); } + /** + * Compile a bitwise operator where clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function whereBitwise(Builder $query, $where) + { + $value = $this->parameter($where['value']); + + $operator = str_replace('?', '??', $where['operator']); + + return '('.$this->wrap($where['column']).' '.$operator.' '.$value.')::bool'; + } + /** * Compile a "where date" clause. * @@ -214,6 +230,36 @@ protected function compileJsonLength($column, $operator, $value) return 'jsonb_array_length(('.$column.')::jsonb) '.$operator.' '.$value; } + /** + * Compile a single having clause. + * + * @param array $having + * @return string + */ + protected function compileHaving(array $having) + { + if ($having['type'] === 'Bitwise') { + return $this->compileHavingBitwise($having); + } + + return parent::compileHaving($having); + } + + /** + * Compile a having clause involving a bitwise operator. + * + * @param array $having + * @return string + */ + protected function compileHavingBitwise($having) + { + $column = $this->wrap($having['column']); + + $parameter = $this->parameter($having['value']); + + return '('.$column.' '.$having['operator'].' '.$parameter.')::bool'; + } + /** * Compile the lock into SQL. * diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index 3fce201bd28c..e71705189a83 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -96,6 +96,22 @@ protected function compileFrom(Builder $query, $table) return $from; } + /** + * {@inheritdoc} + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $where + * @return string + */ + protected function whereBitwise(Builder $query, $where) + { + $value = $this->parameter($where['value']); + + $operator = str_replace('?', '??', $where['operator']); + + return '('.$this->wrap($where['column']).' '.$operator.' '.$value.') != 0'; + } + /** * Compile a "where date" clause. * @@ -164,6 +180,36 @@ protected function compileJsonLength($column, $operator, $value) return '(select count(*) from openjson('.$field.$path.')) '.$operator.' '.$value; } + /** + * Compile a single having clause. + * + * @param array $having + * @return string + */ + protected function compileHaving(array $having) + { + if ($having['type'] === 'Bitwise') { + return $this->compileHavingBitwise($having); + } + + return parent::compileHaving($having); + } + + /** + * Compile a having clause involving a bitwise operator. + * + * @param array $having + * @return string + */ + protected function compileHavingBitwise($having) + { + $column = $this->wrap($having['column']); + + $parameter = $this->parameter($having['value']); + + return '('.$column.' '.$having['operator'].' '.$parameter.') != 0'; + } + /** * Create a full ANSI offset clause for the query. * diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index d58d28c4ecd4..eb4a31b20151 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -35,7 +35,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '9.1.0'; + const VERSION = '9.2.0'; /** * The base path for the Laravel installation. diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 2edfbd739a7f..13a85ea8567f 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -1691,6 +1691,20 @@ public function testUpdateWithTimestampValue() $this->assertEquals(1, $result); } + public function testUpdateWithQualifiedTimestampValue() + { + $query = new BaseBuilder(m::mock(ConnectionInterface::class), new Grammar, m::mock(Processor::class)); + $builder = new Builder($query); + $model = new EloquentBuilderTestStub; + $this->mockConnectionForModel($model, ''); + $builder->setModel($model); + $builder->getConnection()->shouldReceive('update')->once() + ->with('update "table" set "table"."foo" = ?, "table"."updated_at" = ?', ['bar', null])->andReturn(1); + + $result = $builder->update(['table.foo' => 'bar', 'table.updated_at' => null]); + $this->assertEquals(1, $result); + } + public function testUpdateWithoutTimestamp() { $query = new BaseBuilder(m::mock(ConnectionInterface::class), new Grammar, m::mock(Processor::class)); @@ -1721,6 +1735,24 @@ public function testUpdateWithAlias() $this->assertEquals(1, $result); } + public function testUpdateWithAliasWithQualifiedTimestampValue() + { + Carbon::setTestNow($now = '2017-10-10 10:10:10'); + + $query = new BaseBuilder(m::mock(ConnectionInterface::class), new Grammar, m::mock(Processor::class)); + $builder = new Builder($query); + $model = new EloquentBuilderTestStub; + $this->mockConnectionForModel($model, ''); + $builder->setModel($model); + $builder->getConnection()->shouldReceive('update')->once() + ->with('update "table" as "alias" set "foo" = ?, "alias"."updated_at" = ?', ['bar', null])->andReturn(1); + + $result = $builder->from('table as alias')->update(['foo' => 'bar', 'alias.updated_at' => null]); + $this->assertEquals(1, $result); + + Carbon::setTestNow(null); + } + public function testUpsert() { Carbon::setTestNow($now = '2017-10-10 10:10:10'); diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index d15c8b108798..48a6e315d6b8 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -2144,6 +2144,7 @@ protected function addMockConnection($model) $model->setConnectionResolver($resolver = m::mock(ConnectionResolverInterface::class)); $resolver->shouldReceive('connection')->andReturn($connection = m::mock(Connection::class)); $connection->shouldReceive('getQueryGrammar')->andReturn($grammar = m::mock(Grammar::class)); + $grammar->shouldReceive('getBitwiseOperators')->andReturn([]); $connection->shouldReceive('getPostProcessor')->andReturn($processor = m::mock(Processor::class)); $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { return new BaseBuilder($connection, $grammar, $processor); @@ -2431,6 +2432,7 @@ public function getConnection() { $mock = m::mock(Connection::class); $mock->shouldReceive('getQueryGrammar')->andReturn($grammar = m::mock(Grammar::class)); + $grammar->shouldReceive('getBitwiseOperators')->andReturn([]); $mock->shouldReceive('getPostProcessor')->andReturn($processor = m::mock(Processor::class)); $mock->shouldReceive('getName')->andReturn('name'); $mock->shouldReceive('query')->andReturnUsing(function () use ($mock, $grammar, $processor) { diff --git a/tests/Database/DatabaseMigratorIntegrationTest.php b/tests/Database/DatabaseMigratorIntegrationTest.php index 907ea3a9e107..f144170da0a1 100644 --- a/tests/Database/DatabaseMigratorIntegrationTest.php +++ b/tests/Database/DatabaseMigratorIntegrationTest.php @@ -9,6 +9,7 @@ use Illuminate\Database\Migrations\Migrator; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Facades\Facade; +use Illuminate\Support\Str; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -36,6 +37,11 @@ protected function setUp(): void 'database' => ':memory:', ], 'sqlite2'); + $db->addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:', + ], 'sqlite3'); + $db->setAsGlobal(); $container = new Container; @@ -86,6 +92,55 @@ public function testBasicMigrationOfSingleFolder() $this->assertTrue(str_contains($ran[1], 'password_resets')); } + public function testMigrationsDefaultConnectionCanBeChanged() + { + $ran = $this->migrator->usingConnection('sqlite2', function () { + return $this->migrator->run([__DIR__.'/migrations/one'], ['database' => 'sqllite3']); + }); + + $this->assertFalse($this->db->schema()->hasTable('users')); + $this->assertFalse($this->db->schema()->hasTable('password_resets')); + $this->assertTrue($this->db->schema('sqlite2')->hasTable('users')); + $this->assertTrue($this->db->schema('sqlite2')->hasTable('password_resets')); + $this->assertFalse($this->db->schema('sqlite3')->hasTable('users')); + $this->assertFalse($this->db->schema('sqlite3')->hasTable('password_resets')); + + $this->assertTrue(Str::contains($ran[0], 'users')); + $this->assertTrue(Str::contains($ran[1], 'password_resets')); + } + + public function testMigrationsCanEachDefineConnection() + { + $ran = $this->migrator->run([__DIR__.'/migrations/connection_configured']); + + $this->assertFalse($this->db->schema()->hasTable('failed_jobs')); + $this->assertFalse($this->db->schema()->hasTable('jobs')); + $this->assertFalse($this->db->schema('sqlite2')->hasTable('failed_jobs')); + $this->assertFalse($this->db->schema('sqlite2')->hasTable('jobs')); + $this->assertTrue($this->db->schema('sqlite3')->hasTable('failed_jobs')); + $this->assertTrue($this->db->schema('sqlite3')->hasTable('jobs')); + + $this->assertTrue(Str::contains($ran[0], 'failed_jobs')); + $this->assertTrue(Str::contains($ran[1], 'jobs')); + } + + public function testMigratorCannotChangeDefinedMigrationConnection() + { + $ran = $this->migrator->usingConnection('sqlite2', function () { + return $this->migrator->run([__DIR__.'/migrations/connection_configured']); + }); + + $this->assertFalse($this->db->schema()->hasTable('failed_jobs')); + $this->assertFalse($this->db->schema()->hasTable('jobs')); + $this->assertFalse($this->db->schema('sqlite2')->hasTable('failed_jobs')); + $this->assertFalse($this->db->schema('sqlite2')->hasTable('jobs')); + $this->assertTrue($this->db->schema('sqlite3')->hasTable('failed_jobs')); + $this->assertTrue($this->db->schema('sqlite3')->hasTable('jobs')); + + $this->assertTrue(Str::contains($ran[0], 'failed_jobs')); + $this->assertTrue(Str::contains($ran[1], 'jobs')); + } + public function testMigrationsCanBeRolledBack() { $this->migrator->run([__DIR__.'/migrations/one']); diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index 255048f5ad9f..c49d88259bab 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -3332,6 +3332,41 @@ public function testMySqlSoundsLikeOperator() $this->assertEquals(['John Doe'], $builder->getBindings()); } + public function testBitwiseOperators() + { + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->where('bar', '&', 1); + $this->assertSame('select * from "users" where "bar" & ?', $builder->toSql()); + + $builder = $this->getPostgresBuilder(); + $builder->select('*')->from('users')->where('bar', '#', 1); + $this->assertSame('select * from "users" where ("bar" # ?)::bool', $builder->toSql()); + + $builder = $this->getPostgresBuilder(); + $builder->select('*')->from('users')->where('range', '>>', '[2022-01-08 00:00:00,2022-01-09 00:00:00)'); + $this->assertSame('select * from "users" where ("range" >> ?)::bool', $builder->toSql()); + + $builder = $this->getSqlServerBuilder(); + $builder->select('*')->from('users')->where('bar', '&', 1); + $this->assertSame('select * from [users] where ([bar] & ?) != 0', $builder->toSql()); + + $builder = $this->getBuilder(); + $builder->select('*')->from('users')->having('bar', '&', 1); + $this->assertSame('select * from "users" having "bar" & ?', $builder->toSql()); + + $builder = $this->getPostgresBuilder(); + $builder->select('*')->from('users')->having('bar', '#', 1); + $this->assertSame('select * from "users" having ("bar" # ?)::bool', $builder->toSql()); + + $builder = $this->getPostgresBuilder(); + $builder->select('*')->from('users')->having('range', '>>', '[2022-01-08 00:00:00,2022-01-09 00:00:00)'); + $this->assertSame('select * from "users" having ("range" >> ?)::bool', $builder->toSql()); + + $builder = $this->getSqlServerBuilder(); + $builder->select('*')->from('users')->having('bar', '&', 1); + $this->assertSame('select * from [users] having ([bar] & ?) != 0', $builder->toSql()); + } + public function testMergeWheresCanMergeWheresAndBindings() { $builder = $this->getBuilder(); diff --git a/tests/Database/migrations/connection_configured/2022_02_21_000000_create_failed_jobs_table.php b/tests/Database/migrations/connection_configured/2022_02_21_000000_create_failed_jobs_table.php new file mode 100644 index 000000000000..c95b6f0e527d --- /dev/null +++ b/tests/Database/migrations/connection_configured/2022_02_21_000000_create_failed_jobs_table.php @@ -0,0 +1,42 @@ +id(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/tests/Database/migrations/connection_configured/2022_02_21_120000_create_jobs_table.php b/tests/Database/migrations/connection_configured/2022_02_21_120000_create_jobs_table.php new file mode 100644 index 000000000000..a4f3c54a59c1 --- /dev/null +++ b/tests/Database/migrations/connection_configured/2022_02_21_120000_create_jobs_table.php @@ -0,0 +1,36 @@ +create('jobs', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::connection('sqlite3')->dropIfExists('jobs'); + } +}; diff --git a/tests/Integration/Auth/ForgotPasswordTest.php b/tests/Integration/Auth/ForgotPasswordTest.php index 267f21d5b0e3..8d7298e90c77 100644 --- a/tests/Integration/Auth/ForgotPasswordTest.php +++ b/tests/Integration/Auth/ForgotPasswordTest.php @@ -12,6 +12,14 @@ class ForgotPasswordTest extends TestCase { + protected function tearDown(): void + { + ResetPassword::$createUrlCallback = null; + ResetPassword::$toMailCallback = null; + + parent::tearDown(); + } + protected function defineEnvironment($app) { $app['config']->set('auth.providers.users.model', AuthenticationTestUser::class); diff --git a/tests/Integration/Auth/ForgotPasswordWithoutDefaultRoutesTest.php b/tests/Integration/Auth/ForgotPasswordWithoutDefaultRoutesTest.php new file mode 100644 index 000000000000..787483f027d6 --- /dev/null +++ b/tests/Integration/Auth/ForgotPasswordWithoutDefaultRoutesTest.php @@ -0,0 +1,126 @@ +set('auth.providers.users.model', AuthenticationTestUser::class); + } + + protected function defineDatabaseMigrations() + { + $this->loadLaravelMigrations(); + } + + protected function defineRoutes($router) + { + $router->get('custom/password/reset/{token}', function ($token) { + return 'Custom reset password!'; + })->name('custom.password.reset'); + } + + /** @test */ + public function it_cannot_send_forgot_password_email() + { + $this->expectException('Symfony\Component\Routing\Exception\RouteNotFoundException'); + $this->expectExceptionMessage('Route [password.reset] not defined.'); + + Notification::fake(); + + UserFactory::new()->create(); + + $user = AuthenticationTestUser::first(); + + Password::broker()->sendResetLink([ + 'email' => $user->email, + ]); + + Notification::assertSentTo( + $user, + function (ResetPassword $notification, $channels) use ($user) { + $message = $notification->toMail($user); + + return ! is_null($notification->token) + && $message->actionUrl === route('custom.password.reset', ['token' => $notification->token, 'email' => $user->email]); + } + ); + } + + /** @test */ + public function it_can_send_forgot_password_email_via_create_url_using() + { + Notification::fake(); + + ResetPassword::createUrlUsing(function ($user, string $token) { + return route('custom.password.reset', $token); + }); + + UserFactory::new()->create(); + + $user = AuthenticationTestUser::first(); + + Password::broker()->sendResetLink([ + 'email' => $user->email, + ]); + + Notification::assertSentTo( + $user, + function (ResetPassword $notification, $channels) use ($user) { + $message = $notification->toMail($user); + + return ! is_null($notification->token) + && $message->actionUrl === route('custom.password.reset', ['token' => $notification->token]); + } + ); + } + + /** @test */ + public function it_can_send_forgot_password_email_via_to_mail_using() + { + Notification::fake(); + + ResetPassword::toMailUsing(function ($notifiable, $token) { + return (new MailMessage) + ->subject(__('Reset Password Notification')) + ->line(__('You are receiving this email because we received a password reset request for your account.')) + ->action(__('Reset Password'), route('custom.password.reset', $token)) + ->line(__('If you did not request a password reset, no further action is required.')); + }); + + UserFactory::new()->create(); + + $user = AuthenticationTestUser::first(); + + Password::broker()->sendResetLink([ + 'email' => $user->email, + ]); + + Notification::assertSentTo( + $user, + function (ResetPassword $notification, $channels) use ($user) { + $message = $notification->toMail($user); + + return ! is_null($notification->token) + && $message->actionUrl === route('custom.password.reset', ['token' => $notification->token]); + } + ); + } +}