diff --git a/src/Database/DatabaseManager.php b/src/Database/DatabaseManager.php index edde7515..e250cd2f 100644 --- a/src/Database/DatabaseManager.php +++ b/src/Database/DatabaseManager.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\DatabaseManager as BaseDatabaseManager; use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; +use Stancl\Tenancy\Database\TenantDatabaseManagers\SQLiteDatabaseManager; /** * @internal Class is subject to breaking changes in minor and patch versions. @@ -71,7 +72,9 @@ class DatabaseManager $manager = $tenant->database()->manager(); if ($manager->databaseExists($database = $tenant->database()->getName())) { - throw new Exceptions\TenantDatabaseAlreadyExistsException($database); + if (! $manager instanceof SQLiteDatabaseManager || ! SQLiteDatabaseManager::isInMemory($database)) { + throw new Exceptions\TenantDatabaseAlreadyExistsException($database); + } } if ($manager instanceof Contracts\ManagesDatabaseUsers) { diff --git a/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php b/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php index 29aa9831..d7fb8da2 100644 --- a/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\TenantDatabaseManagers; use AssertionError; +use Closure; +use Illuminate\Database\Eloquent\Model; use PDO; use Stancl\Tenancy\Database\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; @@ -24,10 +26,71 @@ class SQLiteDatabaseManager implements TenantDatabaseManager */ public static bool $WAL = true; + /* + * If this isn't null, a connection to the tenant DB will be created + * and passed to the provided closure, for the purpose of keeping the + * connection alive for the desired lifetime. This means it's the + * closure's job to store the connection in a place that lives as + * long as the connection should live. + * + * The closure is called in makeConnectionConfig() -- a method normally + * called shortly before a connection is established. + * + * NOTE: The closure is called EVERY time makeConnectionConfig() + * is called, therefore it's up to the closure to discard + * the connection if a connection to the same database is already persisted. + * + * The closure also receives the DSN used to create the PDO connection, + * since the PDO connection driver makes it a bit hard to recover DB names + * from PDO instances. That should make it easier to match these with + * tenant instances passed to $closeInMemoryConnectionUsing closures, + * if you're setting that property as well. + * + * @property Closure(PDO, string)|null + */ + public static Closure|null $persistInMemoryConnectionUsing = null; + + /* + * The opposite of $persistInMemoryConnectionUsing. This closure + * is called when the tenant is deleted, to clear the database + * in case a tenant with the same ID should be created within + * the lifetime of the $persistInMemoryConnectionUsing logic. + * + * NOTE: The parameter provided to the closure is the Tenant + * instance, not a PDO connection. + * + * @property Closure(Tenant)|null + */ + public static Closure|null $closeInMemoryConnectionUsing = null; + public function createDatabase(TenantWithDatabase $tenant): bool { + /** @var TenantWithDatabase&Model $tenant */ + $name = $tenant->database()->getName(); + + if ($this->isInMemory($name)) { + // If :memory: is used, we update the tenant with a *named* in-memory SQLite connection. + // + // This makes use of the package feasible with in-memory SQLite. Pure :memory: isn't + // sufficient since the tenant creation process involves constant creation and destruction + // of the tenant connection, always clearing the memory (like migrations). Additionally, + // tenancy()->central() calls would close the database since at the moment we close the + // tenant connection (to prevent accidental references to it in the central context) when + // tenancy is ended. + // + // Note that named in-memory databases DO NOT have process lifetime. You need an open + // PDO connection to keep the memory from being cleaned up. It's up to the user how they + // handle this, common solutions may involve storing the connection in the service container + // or creating a closure holding a reference to it and passing that to register_shutdown_function(). + + $name = '_tenancy_inmemory_' . $tenant->getTenantKey(); + $tenant->update(['tenancy_db_name' => "file:$name?mode=memory&cache=shared"]); + + return true; + } + try { - if (file_put_contents($path = $this->getPath($tenant->database()->getName()), '') === false) { + if (file_put_contents($path = $this->getPath($name), '') === false) { return false; } @@ -49,8 +112,18 @@ class SQLiteDatabaseManager implements TenantDatabaseManager public function deleteDatabase(TenantWithDatabase $tenant): bool { + $name = $tenant->database()->getName(); + + if ($this->isInMemory($name)) { + if (static::$closeInMemoryConnectionUsing) { + (static::$closeInMemoryConnectionUsing)($tenant); + } + + return true; + } + try { - return unlink($this->getPath($tenant->database()->getName())); + return unlink($this->getPath($name)); } catch (Throwable) { return false; } @@ -58,12 +131,21 @@ class SQLiteDatabaseManager implements TenantDatabaseManager public function databaseExists(string $name): bool { - return file_exists($this->getPath($name)); + return $this->isInMemory($name) || file_exists($this->getPath($name)); } public function makeConnectionConfig(array $baseConfig, string $databaseName): array { - $baseConfig['database'] = database_path($databaseName); + if ($this->isInMemory($databaseName)) { + $baseConfig['database'] = $databaseName; + + if (static::$persistInMemoryConnectionUsing !== null) { + $dsn = "sqlite:$databaseName"; + (static::$persistInMemoryConnectionUsing)(new PDO($dsn), $dsn); + } + } else { + $baseConfig['database'] = database_path($databaseName); + } return $baseConfig; } @@ -81,4 +163,9 @@ class SQLiteDatabaseManager implements TenantDatabaseManager return database_path($name); } + + public static function isInMemory(string $name): bool + { + return $name === ':memory:' || str_contains($name, '_tenancy_inmemory_'); + } }