From d0023c482a25c16604d8d5da8d14be8fd8a3a644 Mon Sep 17 00:00:00 2001 From: Noor Adiana Date: Wed, 11 Mar 2020 02:15:07 +0700 Subject: [PATCH] Add support for postgres schema (#237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for postgres schema * wip * Apply fixes from StyleCI * revert to db as default for pgsql * Move separate_by to database * Fixing testing * Fixing style * Reverted change * Store string instead of Connection instance * Remove use statement * Add use statement for DB facade * mysql -> pgsql Co-authored-by: Samuel Ć tancl --- assets/config.php | 2 + src/DatabaseManager.php | 26 +++- .../PostgreSQLSchemaManager.php | 47 ++++++ tests/DatabaseSchemaManagerTest.php | 143 ++++++++++++++++++ tests/TenantDatabaseManagerTest.php | 2 + tests/TestCase.php | 3 +- 6 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 src/TenantDatabaseManagers/PostgreSQLSchemaManager.php create mode 100644 tests/DatabaseSchemaManagerTest.php diff --git a/assets/config.php b/assets/config.php index d15fb08d..9155dae4 100644 --- a/assets/config.php +++ b/assets/config.php @@ -30,6 +30,7 @@ return [ 'based_on' => null, // The connection that will be used as a base for the dynamically created tenant connection. Set to null to use the default connection. 'prefix' => 'tenant', 'suffix' => '', + 'separate_by' => 'database', // database or schema (only supported by pgsql) ], 'redis' => [ 'prefix_base' => 'tenant', @@ -61,6 +62,7 @@ return [ 'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class, 'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class, 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class, + // 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database ], 'database_manager_connections' => [ // Connections used by TenantDatabaseManagers. This tells, for example, the diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index 9890e588..0004ea69 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Database\DatabaseManager as BaseDatabaseManager; use Illuminate\Foundation\Application; use Stancl\Tenancy\Contracts\Future\CanSetConnection; +use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException; use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException; use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; @@ -101,7 +102,9 @@ class DatabaseManager // Change database name. $databaseName = $this->getDriver($connectionName) === 'sqlite' ? database_path($databaseName) : $databaseName; - $this->app['config']["database.connections.$connectionName.database"] = $databaseName; + $separateBy = $this->separateBy($connectionName); + + $this->app['config']["database.connections.$connectionName.$separateBy"] = $databaseName; } /** @@ -147,6 +150,8 @@ class DatabaseManager * @param Tenant $tenant * @return void * @throws TenantCannotBeCreatedException + * @throws DatabaseManagerNotRegisteredException + * @throws TenantDatabaseAlreadyExistsException */ public function ensureTenantCanBeCreated(Tenant $tenant): void { @@ -161,6 +166,7 @@ class DatabaseManager * @param Tenant $tenant * @param ShouldQueue[]|callable[] $afterCreating * @return void + * @throws DatabaseManagerNotRegisteredException */ public function createDatabase(Tenant $tenant, array $afterCreating = []) { @@ -202,6 +208,7 @@ class DatabaseManager * * @param Tenant $tenant * @return void + * @throws DatabaseManagerNotRegisteredException */ public function deleteDatabase(Tenant $tenant) { @@ -224,6 +231,7 @@ class DatabaseManager * * @param Tenant $tenant * @return TenantDatabaseManager + * @throws DatabaseManagerNotRegisteredException */ public function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager { @@ -243,4 +251,20 @@ class DatabaseManager return $databaseManager; } + + /** + * What key on the connection config should be used to separate tenants. + * + * @param string $connectionName + * @return string + */ + public function separateBy(string $connectionName): string + { + if ($this->getDriver($this->getBaseConnection($connectionName)) === 'pgsql' + && $this->app['config']['tenancy.database.separate_by'] === 'schema') { + return 'schema'; + } + + return 'database'; + } } diff --git a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php new file mode 100644 index 00000000..a93ed901 --- /dev/null +++ b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php @@ -0,0 +1,47 @@ +connection = $config->get('tenancy.database_manager_connections.pgsql'); + } + + protected function database(): Connection + { + return DB::connection($this->connection); + } + + public function setConnection(string $connection): void + { + $this->connection = $connection; + } + + public function createDatabase(string $name): bool + { + return $this->database()->statement("CREATE SCHEMA \"$name\""); + } + + public function deleteDatabase(string $name): bool + { + return $this->database()->statement("DROP SCHEMA \"$name\""); + } + + public function databaseExists(string $name): bool + { + return (bool) $this->database()->select("SELECT schema_name FROM information_schema.schemata WHERE schema_name = '$name'"); + } +} diff --git a/tests/DatabaseSchemaManagerTest.php b/tests/DatabaseSchemaManagerTest.php new file mode 100644 index 00000000..5f9589e0 --- /dev/null +++ b/tests/DatabaseSchemaManagerTest.php @@ -0,0 +1,143 @@ +set([ + 'database.default' => 'pgsql', + 'database.connections.pgsql.database' => 'main', + 'database.connections.pgsql.schema' => 'public', + 'tenancy.database.based_on' => null, + 'tenancy.database.suffix' => '', + 'tenancy.database.separate_by' => 'schema', + 'tenancy.database_managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, + ]); + } + + /** @test */ + public function reconnect_method_works() + { + $old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); + + tenancy()->init('test.localhost'); + + app(\Stancl\Tenancy\DatabaseManager::class)->reconnect(); + + $new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); + + $this->assertSame($old_connection_name, $new_connection_name); + } + + /** @test */ + public function the_default_db_is_used_when_based_on_is_null() + { + config(['database.default' => 'pgsql']); + + $this->assertSame('pgsql', config('database.default')); + config([ + 'database.connections.pgsql.foo' => 'bar', + 'tenancy.database.based_on' => null, + ]); + + tenancy()->init('test.localhost'); + + $this->assertSame('tenant', config('database.default')); + $this->assertSame('bar', config('database.connections.' . config('database.default') . '.foo')); + } + + /** @test */ + public function make_sure_using_schema_connection() + { + $tenant = tenancy()->create(['schema.localhost']); + tenancy()->init('schema.localhost'); + + $this->assertSame($tenant->getDatabaseName(), config('database.connections.' . config('database.default') . '.schema')); + } + + /** @test */ + public function databases_are_separated_using_schema_and_not_database() + { + tenancy()->create('foo.localhost'); + tenancy()->init('foo.localhost'); + $this->assertSame('tenant', config('database.default')); + $this->assertSame('main', config('database.connections.tenant.database')); + + $schema1 = config('database.connections.' . config('database.default') . '.schema'); + $database1 = config('database.connections.' . config('database.default') . '.database'); + + tenancy()->create('bar.localhost'); + tenancy()->init('bar.localhost'); + $this->assertSame('tenant', config('database.default')); + $this->assertSame('main', config('database.connections.tenant.database')); + + $schema2 = config('database.connections.' . config('database.default') . '.schema'); + $database2 = config('database.connections.' . config('database.default') . '.database'); + + $this->assertSame($database1, $database2); + $this->assertNotSame($schema1, $schema2); + } + + /** @test */ + public function schemas_are_separated() + { + // copied from DataSeparationTest + + $tenant1 = Tenant::create('tenant1.localhost'); + $tenant2 = Tenant::create('tenant2.localhost'); + \Artisan::call('tenants:migrate', [ + '--tenants' => [$tenant1['id'], $tenant2['id']], + ]); + + tenancy()->init('tenant1.localhost'); + User::create([ + 'name' => 'foo', + 'email' => 'foo@bar.com', + 'email_verified_at' => now(), + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + 'remember_token' => Str::random(10), + ]); + $this->assertSame('foo', User::first()->name); + + tenancy()->init('tenant2.localhost'); + $this->assertSame(null, User::first()); + + User::create([ + 'name' => 'xyz', + 'email' => 'xyz@bar.com', + 'email_verified_at' => now(), + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + 'remember_token' => Str::random(10), + ]); + + $this->assertSame('xyz', User::first()->name); + $this->assertSame('xyz@bar.com', User::first()->email); + + tenancy()->init('tenant1.localhost'); + $this->assertSame('foo', User::first()->name); + $this->assertSame('foo@bar.com', User::first()->email); + + $tenant3 = Tenant::create('tenant3.localhost'); + \Artisan::call('tenants:migrate', [ + '--tenants' => [$tenant1['id'], $tenant3['id']], + ]); + + tenancy()->init('tenant3.localhost'); + $this->assertSame(null, User::first()); + + tenancy()->init('tenant1.localhost'); + \DB::table('users')->where('id', 1)->update(['name' => 'xxx']); + $this->assertSame('xxx', User::first()->name); + } +} diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index fc3c34f4..89c0bbd7 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -10,6 +10,7 @@ use Stancl\Tenancy\Jobs\QueuedTenantDatabaseDeleter; use Stancl\Tenancy\Tenant; use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager; +use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager; use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager; class TenantDatabaseManagerTest extends TestCase @@ -78,6 +79,7 @@ class TenantDatabaseManagerTest extends TestCase ['mysql', MySQLDatabaseManager::class], ['sqlite', SQLiteDatabaseManager::class], ['pgsql', PostgreSQLDatabaseManager::class], + ['pgsql', PostgreSQLSchemaManager::class], ]; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 336bff07..257964d1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -24,11 +24,12 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase Redis::connection('tenancy')->flushdb(); Redis::connection('cache')->flushdb(); + $originalConnection = config('database.default'); $this->loadMigrationsFrom([ '--path' => realpath(__DIR__ . '/../assets/migrations'), '--database' => 'central', ]); - config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom + config(['database.default' => $originalConnection]); // fix issue caused by loadMigrationsFrom if ($this->autoCreateTenant) { $this->createTenant();