mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 13:54:03 +00:00
Supported named in-memory SQLite databases (#69)
This PR adds support for named in-memory SQLite databases, making it feasible to use in-memory SQLite for tenant databases in tests. The usage is simply creating a tenant with 'tenancy_db_name' => ':memory:' and the bootstrapper will automatically update the tenant with a database name derived from its tenant key. There are static property hooks for keeping these DBs alive (at least one connection needs to be open, they don't have process lifetime and are essentially "refcounted") and closing them when the database is deleted. This gives the user control over the lifetimes of these databases.
This commit is contained in:
parent
85bdbd57f7
commit
48b916e182
2 changed files with 95 additions and 5 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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_');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue