From 09350485591208554a54f4f03251692c8f40d31b Mon Sep 17 00:00:00 2001 From: Mario Aguilar Alves Date: Wed, 7 Feb 2018 08:52:21 +0100 Subject: [PATCH] First commit --- .gitignore | 3 + .travis.yml | 20 + composer.json | 34 ++ phpunit.xml | 37 ++ src/CanCrossDatabaseShazaamInterface.php | 7 + src/Connectors/ConnectionFactory.php | 40 ++ src/CrossDatabaseServiceProvider.php | 33 ++ src/Eloquent/Builder.php | 14 + .../Concerns/QueriesRelationships.php | 37 ++ src/Eloquent/Model.php | 20 + src/EloquentBuilder.php | 14 + src/MySqlConnection.php | 20 + src/PostgresConnection.php | 20 + src/Query/Grammars/MysqlGrammar.php | 26 ++ src/Query/Grammars/PostgresGrammar.php | 26 ++ src/Query/Grammars/SqlServerGrammar.php | 36 ++ src/SqlServerConnection.php | 20 + ...aseEloquentSubqueriesCrossDatabaseTest.php | 346 ++++++++++++++++++ tests/TestCase.php | 116 ++++++ 19 files changed, 869 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 src/CanCrossDatabaseShazaamInterface.php create mode 100644 src/Connectors/ConnectionFactory.php create mode 100644 src/CrossDatabaseServiceProvider.php create mode 100644 src/Eloquent/Builder.php create mode 100644 src/Eloquent/Concerns/QueriesRelationships.php create mode 100644 src/Eloquent/Model.php create mode 100644 src/EloquentBuilder.php create mode 100644 src/MySqlConnection.php create mode 100644 src/PostgresConnection.php create mode 100644 src/Query/Grammars/MysqlGrammar.php create mode 100644 src/Query/Grammars/PostgresGrammar.php create mode 100644 src/Query/Grammars/SqlServerGrammar.php create mode 100644 src/SqlServerConnection.php create mode 100644 tests/Integration/DatabaseEloquentSubqueriesCrossDatabaseTest.php create mode 100644 tests/TestCase.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a436d29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor/ +build/ +composer.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..517e244 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: php +php: + - "7.0.21" + - "7.1" + - "7.2" + +before_script: + - curl -sS https://getcomposer.org/installer | php -- --filename=composer + - chmod +x composer + - composer install -n + +script: + - php vendor/bin/phpunit + +after_script: + - php vendor/bin/codacycoverage clover build/logs/clover.xml + +branches: + only: + - master \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3ad92de --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "hoyvoy/laravel-cross-database-subqueries", + "description": "Eloquent cross database compatibility in subqueries", + "type": "library", + "require": { + "php": ">=7.0.0", + "illuminate/support": "^5.5", + "illuminate/container": "^5.5", + "illuminate/database": "^5.5", + "illuminate/events": "^5.5" + }, + "require-dev": { + "phpunit/phpunit": "~6.0", + "orchestra/testbench": "3.5.x", + "codacy/coverage": "dev-master" + }, + "autoload": { + "psr-4": { + "Hoyvoy\\CrossDatabase\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Hoyvoy\\Tests\\" : "tests" + } + }, + "extra": { + "laravel": { + "providers": [ + "Hoyvoy\\CrossDatabase\\CrossDatabaseServiceProvider" + ] + } + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..5e2fe93 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,37 @@ + + + + + ./tests/ + + + + + + + + + + + + ./src/ + + ./vendor/ + + + + + + \ No newline at end of file diff --git a/src/CanCrossDatabaseShazaamInterface.php b/src/CanCrossDatabaseShazaamInterface.php new file mode 100644 index 0000000..5f12972 --- /dev/null +++ b/src/CanCrossDatabaseShazaamInterface.php @@ -0,0 +1,7 @@ +app['db']); + + Model::setEventDispatcher($this->app['events']); + } + + /** + * Register the service provider. + */ + public function register() + { + // The connection factory is used to create the actual connection instances on + // the database. We will inject the factory into the manager so that it may + // make the connections while they are actually needed and not of before. + $this->app->singleton('db.factory', function ($app) { + return new ConnectionFactory($app); + }); + } +} diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php new file mode 100644 index 0000000..7f3a726 --- /dev/null +++ b/src/Eloquent/Builder.php @@ -0,0 +1,14 @@ +getConnection() instanceof CanCrossDatabaseShazaamInterface) { + $subqueryConnection = $hasQuery->getConnection()->getDatabaseName(); + $queryConnection = $this->getConnection()->getDatabaseName(); + if ($queryConnection != $subqueryConnection) { + $queryFrom = $hasQuery->getQuery()->from.'<-->'.$subqueryConnection; + $hasQuery->from($queryFrom); + } + } + + return parent::addHasWhere($hasQuery, $relation, $operator, $count, $boolean); + } +} diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php new file mode 100644 index 0000000..732d860 --- /dev/null +++ b/src/Eloquent/Model.php @@ -0,0 +1,20 @@ +withTablePrefix(new MySqlQueryGrammar); + } +} diff --git a/src/PostgresConnection.php b/src/PostgresConnection.php new file mode 100644 index 0000000..ecf8cc8 --- /dev/null +++ b/src/PostgresConnection.php @@ -0,0 +1,20 @@ +withTablePrefix(new PostgresQueryGrammar); + } +} diff --git a/src/Query/Grammars/MysqlGrammar.php b/src/Query/Grammars/MysqlGrammar.php new file mode 100644 index 0000000..3fc8d32 --- /dev/null +++ b/src/Query/Grammars/MysqlGrammar.php @@ -0,0 +1,26 @@ +') !== false) { + list($table, $database) = explode('<-->', $table); + return 'from '.$this->wrap($database).'.'.$this->wrapTable($table); + } + return 'from '.$this->wrapTable($table); + } +} diff --git a/src/Query/Grammars/PostgresGrammar.php b/src/Query/Grammars/PostgresGrammar.php new file mode 100644 index 0000000..ba347d0 --- /dev/null +++ b/src/Query/Grammars/PostgresGrammar.php @@ -0,0 +1,26 @@ +') !== false) { + list($table, $database) = explode('<-->', $table); + return 'from '.$this->wrap($database).'.'.$this->wrapTable($table); + } + return 'from '.$this->wrapTable($table); + } +} diff --git a/src/Query/Grammars/SqlServerGrammar.php b/src/Query/Grammars/SqlServerGrammar.php new file mode 100644 index 0000000..7fe13f0 --- /dev/null +++ b/src/Query/Grammars/SqlServerGrammar.php @@ -0,0 +1,36 @@ +wrapTable($table); + // Check for cross database query to attach database name + if (strpos($table, '<-->') !== false) { + list($table, $database) = explode('<-->', $table); + $from = 'from '.$this->wrap($database).'.'.$this->wrapTable($table); + } + + if (is_string($query->lock)) { + return $from.' '.$query->lock; + } + + if (! is_null($query->lock)) { + return $from.' with(rowlock,'.($query->lock ? 'updlock,' : '').'holdlock)'; + } + + return $from; + } +} diff --git a/src/SqlServerConnection.php b/src/SqlServerConnection.php new file mode 100644 index 0000000..870ad3c --- /dev/null +++ b/src/SqlServerConnection.php @@ -0,0 +1,20 @@ +withTablePrefix(new SqlServerQueryGrammar); + } +} diff --git a/tests/Integration/DatabaseEloquentSubqueriesCrossDatabaseTest.php b/tests/Integration/DatabaseEloquentSubqueriesCrossDatabaseTest.php new file mode 100644 index 0000000..506833e --- /dev/null +++ b/tests/Integration/DatabaseEloquentSubqueriesCrossDatabaseTest.php @@ -0,0 +1,346 @@ +where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from `users` where exists (select * from `mysql2`.`orders` where `users`.`id` = `orders`.`user_id` and `name` like ?)', $query->toSql()); + + // Test MySQL same database subquery + $query = UserMysql::whereHas('posts', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from `users` where exists (select * from `posts` where `users`.`id` = `posts`.`user_id` and `name` like ?)', $query->toSql()); + + // Test PostgreSQL cross database subquery + $query = UserPgsql::whereHas('orders', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from "users" where exists (select * from "pgsql2"."orders" where "users"."id" = "orders"."user_id" and "name" like ?)', $query->toSql()); + + // Test PostgreSQL same database subquery + $query = UserPgsql::whereHas('posts', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from "users" where exists (select * from "posts" where "users"."id" = "posts"."user_id" and "name" like ?)', $query->toSql()); + + // Test SQL Server cross database subquery + $query = UserSqlsrv::whereHas('orders', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from [users] where exists (select * from [sqlsrv2].[orders] where [users].[id] = [orders].[user_id] and [name] like ?)', $query->toSql()); + + // Test SQL Server same database subquery + $query = UserSqlsrv::whereHas('posts', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from [users] where exists (select * from [posts] where [users].[id] = [posts].[user_id] and [name] like ?)', $query->toSql()); + + // Test SQL Server cross database subquery + $query = UserSqlite::whereHas('orders', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from "users" where exists (select * from "orders" where "users"."id" = "orders"."user_id" and "name" like ?)', $query->toSql()); + + // Test SQL Server same database subquery + $query = UserSqlite::whereHas('posts', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from "users" where exists (select * from "posts" where "users"."id" = "posts"."user_id" and "name" like ?)', $query->toSql()); + } + + public function testHasAcrossDatabaseConnection() + { + // Test MySQL cross database subquery + $query = UserMysql::has('orders'); + $this->assertEquals('select * from `users` where exists (select * from `mysql2`.`orders` where `users`.`id` = `orders`.`user_id`)', $query->toSql()); + + // Test MySQL same database subquery + $query = UserMysql::has('posts'); + $this->assertEquals('select * from `users` where exists (select * from `posts` where `users`.`id` = `posts`.`user_id`)', $query->toSql()); + + // Test PostgreSQL cross database subquery + $query = UserPgsql::has('orders'); + $this->assertEquals('select * from "users" where exists (select * from "pgsql2"."orders" where "users"."id" = "orders"."user_id")', $query->toSql()); + + // Test PostgreSQL same database subquery + $query = UserPgsql::has('posts'); + $this->assertEquals('select * from "users" where exists (select * from "posts" where "users"."id" = "posts"."user_id")', $query->toSql()); + + // Test SQL Server cross database subquery + $query = UserSqlsrv::has('orders'); + $this->assertEquals('select * from [users] where exists (select * from [sqlsrv2].[orders] where [users].[id] = [orders].[user_id])', $query->toSql()); + + // Test SQL Server same database subquery + $query = UserSqlsrv::has('posts'); + $this->assertEquals('select * from [users] where exists (select * from [posts] where [users].[id] = [posts].[user_id])', $query->toSql()); + + // Test SQL Server cross database subquery + $query = UserSqlite::has('orders'); + $this->assertEquals('select * from "users" where exists (select * from "orders" where "users"."id" = "orders"."user_id")', $query->toSql()); + + // Test SQL Server same database subquery + $query = UserSqlite::has('posts'); + $this->assertEquals('select * from "users" where exists (select * from "posts" where "users"."id" = "posts"."user_id")', $query->toSql()); + } + + public function testDoesntHasAcrossDatabaseConnection() + { + // Test MySQL cross database subquery + $query = UserMysql::doesntHave('orders'); + $this->assertEquals('select * from `users` where not exists (select * from `mysql2`.`orders` where `users`.`id` = `orders`.`user_id`)', $query->toSql()); + + // Test MySQL same database subquery + $query = UserMysql::doesntHave('posts'); + $this->assertEquals('select * from `users` where not exists (select * from `posts` where `users`.`id` = `posts`.`user_id`)', $query->toSql()); + + // Test PostgreSQL cross database subquery + $query = UserPgsql::doesntHave('orders'); + $this->assertEquals('select * from "users" where not exists (select * from "pgsql2"."orders" where "users"."id" = "orders"."user_id")', $query->toSql()); + + // Test PostgreSQL same database subquery + $query = UserPgsql::doesntHave('posts'); + $this->assertEquals('select * from "users" where not exists (select * from "posts" where "users"."id" = "posts"."user_id")', $query->toSql()); + + // Test SQL Server cross database subquery + $query = UserSqlsrv::doesntHave('orders'); + $this->assertEquals('select * from [users] where not exists (select * from [sqlsrv2].[orders] where [users].[id] = [orders].[user_id])', $query->toSql()); + + // Test SQL Server same database subquery + $query = UserSqlsrv::doesntHave('posts'); + $this->assertEquals('select * from [users] where not exists (select * from [posts] where [users].[id] = [posts].[user_id])', $query->toSql()); + + // Test SQL Server cross database subquery + $query = UserSqlite::doesntHave('orders'); + $this->assertEquals('select * from "users" where not exists (select * from "orders" where "users"."id" = "orders"."user_id")', $query->toSql()); + + // Test SQL Server same database subquery + $query = UserSqlite::doesntHave('posts'); + $this->assertEquals('select * from "users" where not exists (select * from "posts" where "users"."id" = "posts"."user_id")', $query->toSql()); + } + + public function testWhereDoesntHaveAcrossDatabaseConnection() + { + // Test MySQL cross database subquery + $query = UserMysql::whereDoesntHave('orders', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from `users` where not exists (select * from `mysql2`.`orders` where `users`.`id` = `orders`.`user_id` and `name` like ?)', $query->toSql()); + + // Test MySQL same database subquery + $query = UserMysql::whereDoesntHave('posts', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from `users` where not exists (select * from `posts` where `users`.`id` = `posts`.`user_id` and `name` like ?)', $query->toSql()); + + // Test PostgreSQL cross database subquery + $query = UserPgsql::whereDoesntHave('orders', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from "users" where not exists (select * from "pgsql2"."orders" where "users"."id" = "orders"."user_id" and "name" like ?)', $query->toSql()); + + // Test PostgreSQL same database subquery + $query = UserPgsql::whereDoesntHave('posts', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from "users" where not exists (select * from "posts" where "users"."id" = "posts"."user_id" and "name" like ?)', $query->toSql()); + + // Test SQL Server cross database subquery + $query = UserSqlsrv::whereDoesntHave('orders', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from [users] where not exists (select * from [sqlsrv2].[orders] where [users].[id] = [orders].[user_id] and [name] like ?)', $query->toSql()); + + // Test SQL Server same database subquery + $query = UserSqlsrv::whereDoesntHave('posts', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from [users] where not exists (select * from [posts] where [users].[id] = [posts].[user_id] and [name] like ?)', $query->toSql()); + + // Test SQL Server cross database subquery + $query = UserSqlite::whereDoesntHave('orders', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from "users" where not exists (select * from "orders" where "users"."id" = "orders"."user_id" and "name" like ?)', $query->toSql()); + + // Test SQL Server same database subquery + $query = UserSqlite::whereDoesntHave('posts', function ($query) { + $query->where('name', 'like', '%a%'); + }); + $this->assertEquals('select * from "users" where not exists (select * from "posts" where "users"."id" = "posts"."user_id" and "name" like ?)', $query->toSql()); + } +} + +class UserMysql extends Model +{ + protected $connection = 'mysql1'; + protected $table = 'users'; + protected $guarded = []; + + public function orders() + { + return $this->hasMany('Hoyvoy\Tests\Integration\OrderMysql', 'user_id'); + } + + public function posts() + { + return $this->hasMany('Hoyvoy\Tests\Integration\PostMysql', 'user_id'); + } +} + +class PostMysql extends Model +{ + protected $connection = 'mysql1'; + protected $table = 'posts'; + protected $guarded = []; + + public function user() + { + return $this->belongsTo('Hoyvoy\Tests\Integration\UserMysql', 'user_id'); + } +} + +class OrderMysql extends Model +{ + protected $connection = 'mysql2'; + protected $table = 'orders'; + protected $guarded = []; + + public function user() + { + return $this->belongsTo('Hoyvoy\Tests\Integration\UserMysql', 'user_id'); + } +} + +class UserPgsql extends Model +{ + protected $connection = 'pgsql1'; + protected $table = 'users'; + protected $guarded = []; + + public function orders() + { + return $this->hasMany('Hoyvoy\Tests\Integration\OrderPgsql', 'user_id'); + } + + public function posts() + { + return $this->hasMany('Hoyvoy\Tests\Integration\PostPgsql', 'user_id'); + } +} + +class PostPgsql extends Model +{ + protected $connection = 'pgsql1'; + protected $table = 'posts'; + protected $guarded = []; + + public function user() + { + return $this->belongsTo('Hoyvoy\Tests\Integration\UserPgsql', 'user_id'); + } +} + +class OrderPgsql extends Model +{ + protected $connection = 'pgsql2'; + protected $table = 'orders'; + protected $guarded = []; + + public function user() + { + return $this->belongsTo('Hoyvoy\Tests\Integration\UserPgsql', 'user_id'); + } +} + +class UserSqlsrv extends Model +{ + protected $connection = 'sqlsrv1'; + protected $table = 'users'; + protected $guarded = []; + + public function orders() + { + return $this->hasMany('Hoyvoy\Tests\Integration\OrderSqlsrv', 'user_id'); + } + + public function posts() + { + return $this->hasMany('Hoyvoy\Tests\Integration\PostSqlsrv', 'user_id'); + } +} + +class PostSqlsrv extends Model +{ + protected $connection = 'sqlsrv1'; + protected $table = 'posts'; + protected $guarded = []; + + public function user() + { + return $this->belongsTo('Hoyvoy\Tests\Integration\UserSqlsrv', 'user_id'); + } +} + +class OrderSqlsrv extends Model +{ + protected $connection = 'sqlsrv2'; + protected $table = 'orders'; + protected $guarded = []; + + public function user() + { + return $this->belongsTo('Hoyvoy\Tests\Integration\UserSqlsrv', 'user_id'); + } +} + +class UserSqlite extends Model +{ + protected $connection = 'sqlite1'; + protected $table = 'users'; + protected $guarded = []; + + public function orders() + { + return $this->hasMany('Hoyvoy\Tests\Integration\OrderSqlite', 'user_id'); + } + + public function posts() + { + return $this->hasMany('Hoyvoy\Tests\Integration\PostSqlite', 'user_id'); + } +} + +class PostSqlite extends Model +{ + protected $connection = 'sqlite1'; + protected $table = 'posts'; + protected $guarded = []; + + public function user() + { + return $this->belongsTo('Hoyvoy\Tests\Integration\UserSqlite', 'user_id'); + } +} + +class OrderSqlite extends Model +{ + protected $connection = 'sqlite2'; + protected $table = 'orders'; + protected $guarded = []; + + public function user() + { + return $this->belongsTo('Hoyvoy\Tests\Integration\UserSqlite', 'user_id'); + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..8a06e82 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,116 @@ +set('database.connections', [ + 'mysql1' => [ + 'driver' => 'mysql', + 'host' => '127.0.0.1', + 'port' => '3306', + 'database' => 'mysql1', + 'username' => 'test', + 'password' => 'test', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'strict' => true, + 'engine' => null, + ], + 'mysql2' => [ + 'driver' => 'mysql', + 'host' => '127.0.0.1', + 'port' => '3306', + 'database' => 'mysql2', + 'username' => 'test', + 'password' => 'test', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'strict' => true, + 'engine' => null, + ], + 'pgsql1' => [ + 'driver' => 'pgsql', + 'host' => '127.0.0.1', + 'port' => '3306', + 'database' => 'pgsql1', + 'username' => 'test', + 'password' => 'test', + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + 'sslmode' => 'prefer', + ], + 'pgsql2' => [ + 'driver' => 'pgsql', + 'host' => '127.0.0.1', + 'port' => '3306', + 'database' => 'pgsql2', + 'username' => 'test', + 'password' => 'test', + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + 'sslmode' => 'prefer', + ] + , + 'sqlsrv1' => [ + 'driver' => 'sqlsrv', + 'host' => '127.0.0.1', + 'port' => '3306', + 'database' => 'sqlsrv1', + 'username' => 'test', + 'password' => 'test', + 'charset' => 'utf8', + 'prefix' => '', + ], + 'sqlsrv2' => [ + 'driver' => 'sqlsrv', + 'host' => '127.0.0.1', + 'port' => '3306', + 'database' => 'sqlsrv2', + 'username' => 'test', + 'password' => 'test', + 'charset' => 'utf8', + 'prefix' => '', + ], + 'sqlite1' => [ + 'driver' => 'sqlite', + 'database' => __DIR__.'/database/sqlite1.sqlite', + 'prefix' => '', + ], + 'sqlite2' => [ + 'driver' => 'sqlite', + 'database' => __DIR__.'/database/sqlite2.sqlite', + 'prefix' => '', + ] + ]); + } +}