From 3b446e39ea247760347c1d6324c7c58e0aedc207 Mon Sep 17 00:00:00 2001 From: Richan Fongdasen Date: Wed, 6 Nov 2024 20:45:11 +0700 Subject: [PATCH] Add support for multi Turso database connections --- README.md | 30 +++++-- composer.json | 6 +- package.json | 2 +- phpstan-baseline.neon | 1 - src/Commands/TursoSyncCommand.php | 31 +++++-- src/Database/TursoConnection.php | 48 +++++++++++ src/Database/TursoConnector.php | 2 +- src/Database/TursoPDO.php | 19 +++- src/Database/TursoPDOStatement.php | 6 +- src/Database/TursoQueryGrammar.php | 4 +- src/Database/TursoQueryProcessor.php | 4 +- src/Database/TursoSchemaGrammar.php | 3 +- src/Database/TursoSchemaState.php | 4 +- .../FeatureNotSupportedException.php | 4 +- src/Http/RequestBody.php | 12 +-- src/Jobs/TursoSyncJob.php | 12 ++- src/Queries/ExecuteQuery.php | 3 +- src/TursoClient.php | 14 ++- src/TursoLaravelServiceProvider.php | 13 +-- src/TursoManager.php | 50 ++++------- tests/Feature/MultiConnectionsTest.php | 44 ++++++++++ tests/Feature/ReadReplicaTest.php | 3 - tests/Unit/Commands/TursoSyncCommandTest.php | 18 ++++ tests/Unit/Database/TursoConnectionTest.php | 86 +++++++++++++++++-- tests/Unit/Http/RequestBodyTest.php | 4 +- tests/Unit/TursoClientTest.php | 66 +++++++++----- tests/Unit/TursoManagerTest.php | 19 ++-- 27 files changed, 359 insertions(+), 149 deletions(-) create mode 100644 tests/Feature/MultiConnectionsTest.php diff --git a/README.md b/README.md index 57245da..17bc9a8 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ You can find a demo application that uses this Turso database driver in the [ric - PHP 8.2 or higher - Laravel 11.0 or higher -- Node.js 16 or higher +- Node.js 18 or higher ## Installation @@ -131,7 +131,7 @@ The driver supports the embedded replica feature. If you're unfamiliar with this Run the sync script manually using the following Artisan command: ```bash -php artisan turso:sync +php artisan turso:sync ``` > You may encounter an error if the path to the replica database does not exist. This is expected when the replica database has not been created yet. @@ -146,11 +146,19 @@ use RichanFongdasen\Turso\Facades\Turso; if ( DB::hasModifiedRecords() ) { // Run the sync script immediately - Turso::sync(); + DB::sync(); // Run the sync script in the background - Turso::backgroundSync(); + DB::backgroundSync(); } + +// Sync on the specific connection +DB::connection('turso')->sync(); +DB::connection('turso')->backgroundSync(); + +// Sync on all of the turso database connections +Turso::sync(); +Turso::backgroundSync(); ``` ## Debugging @@ -158,12 +166,20 @@ if ( DB::hasModifiedRecords() ) { To debug the HTTP requests and responses sent and received by the Turso database client, enable the debugging feature as follows: ```php -Turso::enableQueryLog(); +// Enabling query log on default database connection +DB::enableQueryLog(); +// Enabling query log on specific connection +DB::connection('turso')->enableQueryLog(); + +// Perform some queries DB::table('users')->get(); -// Get the query log -$logs = Turso::getQueryLog(); +// Get the query log for default database connection +DB::getQueryLog(); + +// Get the query log for specific connection +DB::connection('turso')->getQueryLog(); ``` ## Changelog diff --git a/composer.json b/composer.json index 95c7909..64c4f54 100644 --- a/composer.json +++ b/composer.json @@ -35,9 +35,9 @@ "laravel/pint": "^1.14", "nunomaduro/collision": "^8.1.1", "orchestra/testbench": "^9.0.0", - "pestphp/pest": "^2.34", - "pestphp/pest-plugin-arch": "^2.7", - "pestphp/pest-plugin-laravel": "^2.3", + "pestphp/pest": "^3.5", + "pestphp/pest-plugin-arch": "^3.0", + "pestphp/pest-plugin-laravel": "^3.0", "phpstan/extension-installer": "^1.3", "phpstan/phpstan-deprecation-rules": "^1.1", "phpstan/phpstan-phpunit": "^1.3", diff --git a/package.json b/package.json index c9b89c6..d7afc15 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,6 @@ "sync": "node turso-sync.mjs" }, "devDependencies": { - "@libsql/client": "^0.6.0" + "@libsql/client": "^0.14.0" } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 717cf55..08bbea8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3,4 +3,3 @@ parameters: ignoreErrors: - identifier: missingType.iterableValue - identifier: missingType.generics - - '#Variable method call on RichanFongdasen\\Turso\\TursoClient.#' diff --git a/src/Commands/TursoSyncCommand.php b/src/Commands/TursoSyncCommand.php index 740a911..1f199f5 100644 --- a/src/Commands/TursoSyncCommand.php +++ b/src/Commands/TursoSyncCommand.php @@ -5,24 +5,25 @@ namespace RichanFongdasen\Turso\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Process; use RuntimeException; class TursoSyncCommand extends Command { - public $signature = 'turso:sync'; + public $signature = 'turso:sync {connectionName?}'; public $description = 'Sync changes from the remote database to the local replica manually.'; - protected function compileRunProcess(): string + protected function compileRunProcess(array $config): string { return sprintf( '%s %s "%s" "%s" "%s"', $this->getNodePath(), config('turso-laravel.sync_command.script_filename'), - config('database.connections.turso.db_url'), - config('database.connections.turso.access_token'), - config('database.connections.turso.db_replica'), + data_get($config, 'db_url'), + data_get($config, 'access_token'), + data_get($config, 'db_replica'), ); } @@ -41,9 +42,25 @@ public function handle(): int { $timeout = (int) config('turso-laravel.sync_command.timeout'); + $connectionName = (string) $this->argument('connectionName'); + + if (DB::connection($connectionName)->getConfig('driver') !== 'turso') { + $this->error('The specified connection is not a Turso connection.'); + + return self::FAILURE; + } + + if ((string) DB::connection($connectionName)->getConfig('db_replica') === '') { + $this->error('The specified connection does not have a read replica.'); + + return self::FAILURE; + } + $result = Process::timeout($timeout) ->path(config('turso-laravel.sync_command.script_path') ?? base_path()) - ->run($this->compileRunProcess()); + ->run($this->compileRunProcess( + DB::connection($connectionName)->getConfig() + )); if ($result->failed()) { throw new RuntimeException('Turso sync command failed: ' . $result->errorOutput()); @@ -51,6 +68,8 @@ public function handle(): int $this->info($result->output()); + DB::connection($connectionName)->forgetRecordModificationState(); + return self::SUCCESS; } } diff --git a/src/Database/TursoConnection.php b/src/Database/TursoConnection.php index 623cbe1..763e0a8 100644 --- a/src/Database/TursoConnection.php +++ b/src/Database/TursoConnection.php @@ -7,7 +7,9 @@ use Exception; use Illuminate\Database\Connection; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Facades\Artisan; use PDO; +use RichanFongdasen\Turso\Jobs\TursoSyncJob; class TursoConnection extends Connection { @@ -83,4 +85,50 @@ protected function isUniqueConstraintError(Exception $exception): bool { return boolval(preg_match('#(column(s)? .* (is|are) not unique|UNIQUE constraint failed: .*)#i', $exception->getMessage())); } + + public function sync(): void + { + Artisan::call('turso:sync', ['connectionName' => $this->getName()]); + } + + public function backgroundSync(): void + { + TursoSyncJob::dispatch((string) $this->getName()); + $this->enableQueryLog(); + } + + public function disableQueryLog(): void + { + parent::disableQueryLog(); + + $this->tursoPdo()->getClient()->disableQueryLog(); + } + + public function enableQueryLog(): void + { + parent::enableQueryLog(); + + $this->tursoPdo()->getClient()->enableQueryLog(); + } + + public function flushQueryLog(): void + { + parent::flushQueryLog(); + + $this->tursoPdo()->getClient()->flushQueryLog(); + } + + public function getQueryLog() + { + return $this->tursoPdo()->getClient()->getQueryLog()->toArray(); + } + + public function tursoPdo(): TursoPDO + { + if (! $this->pdo instanceof TursoPDO) { + throw new Exception('The current PDO instance is not an instance of TursoPDO.'); + } + + return $this->pdo; + } } diff --git a/src/Database/TursoConnector.php b/src/Database/TursoConnector.php index 4c8a319..a534d19 100644 --- a/src/Database/TursoConnector.php +++ b/src/Database/TursoConnector.php @@ -18,6 +18,6 @@ public function connect(array $config) { $options = $this->getOptions($config); - return new TursoPDO('sqlite::memory:', null, null, $options); + return new TursoPDO($config, $options); } } diff --git a/src/Database/TursoPDO.php b/src/Database/TursoPDO.php index 914d21f..fd2464c 100644 --- a/src/Database/TursoPDO.php +++ b/src/Database/TursoPDO.php @@ -5,6 +5,7 @@ namespace RichanFongdasen\Turso\Database; use PDO; +use RichanFongdasen\Turso\TursoClient; /** * Turso PDO Database Connection. @@ -19,17 +20,22 @@ */ class TursoPDO extends PDO { + protected TursoClient $client; + + protected array $config = []; + protected bool $inTransaction = false; protected array $lastInsertIds = []; public function __construct( - string $dsn = 'sqlite::memory:', - ?string $username = null, - ?string $password = null, + array $config, ?array $options = null ) { - parent::__construct($dsn, $username, $password, $options); + parent::__construct('sqlite::memory:', null, null, $options); + + $this->config = $config; + $this->client = new TursoClient($config); } public function beginTransaction(): bool @@ -56,6 +62,11 @@ public function exec(string $queryStatement): int return $statement->rowCount(); } + public function getClient(): TursoClient + { + return $this->client; + } + public function inTransaction(): bool { return $this->inTransaction; diff --git a/src/Database/TursoPDOStatement.php b/src/Database/TursoPDOStatement.php index 04e30cc..17ca02d 100644 --- a/src/Database/TursoPDOStatement.php +++ b/src/Database/TursoPDOStatement.php @@ -10,7 +10,6 @@ use PDOStatement; use RichanFongdasen\Turso\Enums\PdoParam; use RichanFongdasen\Turso\Enums\TursoType; -use RichanFongdasen\Turso\Facades\Turso; use RichanFongdasen\Turso\Http\QueryResponse; /** @@ -33,8 +32,7 @@ public function __construct( protected TursoPDO $pdo, protected string $query, protected array $options = [], - ) { - } + ) {} public function setFetchMode(int $mode, mixed ...$args): bool { @@ -61,7 +59,7 @@ public function execute(?array $params = null): bool $this->bindValue($key, $value, $type->value); }); - $this->response = Turso::query($this->query, array_values($this->bindings)); + $this->response = $this->pdo->getClient()->query($this->query, array_values($this->bindings)); $lastId = (int) $this->response->getLastInsertId(); if ($lastId > 0) { diff --git a/src/Database/TursoQueryGrammar.php b/src/Database/TursoQueryGrammar.php index d948361..5cda4c0 100644 --- a/src/Database/TursoQueryGrammar.php +++ b/src/Database/TursoQueryGrammar.php @@ -6,6 +6,4 @@ use Illuminate\Database\Query\Grammars\SQLiteGrammar; -class TursoQueryGrammar extends SQLiteGrammar -{ -} +class TursoQueryGrammar extends SQLiteGrammar {} diff --git a/src/Database/TursoQueryProcessor.php b/src/Database/TursoQueryProcessor.php index 60c0bfa..a8df969 100644 --- a/src/Database/TursoQueryProcessor.php +++ b/src/Database/TursoQueryProcessor.php @@ -6,6 +6,4 @@ use Illuminate\Database\Query\Processors\SQLiteProcessor; -class TursoQueryProcessor extends SQLiteProcessor -{ -} +class TursoQueryProcessor extends SQLiteProcessor {} diff --git a/src/Database/TursoSchemaGrammar.php b/src/Database/TursoSchemaGrammar.php index 8723c68..600a2c7 100644 --- a/src/Database/TursoSchemaGrammar.php +++ b/src/Database/TursoSchemaGrammar.php @@ -30,8 +30,9 @@ public function compileDropAllViews(): string } #[Override] - public function wrap($value, $prefixAlias = false): string + public function wrap(mixed $value, mixed $prefixAlias = false): string { + /** @phpstan-ignore arguments.count */ return str_replace('"', '\'', parent::wrap($value, $prefixAlias)); } } diff --git a/src/Database/TursoSchemaState.php b/src/Database/TursoSchemaState.php index e02a5e5..e5845f3 100644 --- a/src/Database/TursoSchemaState.php +++ b/src/Database/TursoSchemaState.php @@ -6,6 +6,4 @@ use Illuminate\Database\Schema\SqliteSchemaState; -class TursoSchemaState extends SqliteSchemaState -{ -} +class TursoSchemaState extends SqliteSchemaState {} diff --git a/src/Exceptions/FeatureNotSupportedException.php b/src/Exceptions/FeatureNotSupportedException.php index b1d7bea..6876f5d 100644 --- a/src/Exceptions/FeatureNotSupportedException.php +++ b/src/Exceptions/FeatureNotSupportedException.php @@ -6,6 +6,4 @@ use LogicException; -class FeatureNotSupportedException extends LogicException -{ -} +class FeatureNotSupportedException extends LogicException {} diff --git a/src/Http/RequestBody.php b/src/Http/RequestBody.php index 08c626b..e48d5ec 100644 --- a/src/Http/RequestBody.php +++ b/src/Http/RequestBody.php @@ -4,12 +4,10 @@ namespace RichanFongdasen\Turso\Http; -use ErrorException; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Collection; use InvalidArgumentException; use RichanFongdasen\Turso\Contracts\TursoQuery; -use RichanFongdasen\Turso\Database\TursoConnection; use RichanFongdasen\Turso\Database\TursoSchemaGrammar; use RichanFongdasen\Turso\Queries\CloseQuery; use RichanFongdasen\Turso\Queries\ExecuteQuery; @@ -72,7 +70,7 @@ public function withoutCloseRequest(): self return $this; } - public function withForeignKeyConstraints(): self + public function withForeignKeyConstraints(bool $constraintsEnabled): self { // Make sure that the foreign key constraints statement // is getting executed only once. @@ -80,13 +78,9 @@ public function withForeignKeyConstraints(): self return $this; } - $grammar = app(TursoConnection::class)->getSchemaGrammar(); + $grammar = app(TursoSchemaGrammar::class); - if (! ($grammar instanceof TursoSchemaGrammar)) { - throw new ErrorException('The registered schema grammar is not an instance of TursoSchemaGrammar.'); - } - - $statement = (bool) config('database.connections.turso.foreign_key_constraints') + $statement = $constraintsEnabled ? $grammar->compileEnableForeignKeyConstraints() : $grammar->compileDisableForeignKeyConstraints(); diff --git a/src/Jobs/TursoSyncJob.php b/src/Jobs/TursoSyncJob.php index 701acbf..fedf243 100644 --- a/src/Jobs/TursoSyncJob.php +++ b/src/Jobs/TursoSyncJob.php @@ -10,7 +10,6 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Artisan; -use Illuminate\Support\Facades\DB; class TursoSyncJob implements ShouldQueue { @@ -19,13 +18,18 @@ class TursoSyncJob implements ShouldQueue use Queueable; use SerializesModels; + protected string $connectionName; + + public function __construct(string $connectionName = 'turso') + { + $this->connectionName = $connectionName; + } + /** * Execute the job. */ public function handle(): void { - Artisan::call('turso:sync'); - - DB::forgetRecordModificationState(); + Artisan::call('turso:sync', ['connectionName' => $this->connectionName]); } } diff --git a/src/Queries/ExecuteQuery.php b/src/Queries/ExecuteQuery.php index d75d9e1..f105f2c 100644 --- a/src/Queries/ExecuteQuery.php +++ b/src/Queries/ExecuteQuery.php @@ -11,8 +11,7 @@ class ExecuteQuery extends Query public function __construct( protected string $statement, protected array $bindings = [] - ) { - } + ) {} public function getBindings(): array { diff --git a/src/TursoClient.php b/src/TursoClient.php index 7d61cb8..11ec15c 100755 --- a/src/TursoClient.php +++ b/src/TursoClient.php @@ -36,7 +36,7 @@ public function __construct(array $config = []) $this->queryLog = new Collection(); - $this->enableQueryLog(); + $this->disableQueryLog(); $this->resetHttpClientState(); } @@ -85,6 +85,11 @@ public function enableQueryLog(): void $this->loggingQueries = true; } + public function flushQueryLog(): void + { + $this->queryLog = new Collection(); + } + public function freshHttpRequest(): PendingRequest { $this->httpRequest = $this->createHttpRequest(); @@ -107,12 +112,17 @@ public function getQueryLog(): Collection return $this->queryLog; } + public function logging(): bool + { + return $this->loggingQueries; + } + public function query(string $statement, array $bindingValues = []): QueryResponse { $query = new ExecuteQuery($statement, $bindingValues); $requestBody = RequestBody::create($this->baton) - ->withForeignKeyConstraints() + ->withForeignKeyConstraints((bool) $this->config->get('foreign_key_constraints')) ->push($query); $httpResponse = $this->httpRequest() diff --git a/src/TursoLaravelServiceProvider.php b/src/TursoLaravelServiceProvider.php index 4f7f6c1..56386fe 100644 --- a/src/TursoLaravelServiceProvider.php +++ b/src/TursoLaravelServiceProvider.php @@ -8,7 +8,6 @@ use Illuminate\Console\Events\CommandStarting; use Illuminate\Database\Connection; use Illuminate\Database\DatabaseManager; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use RichanFongdasen\Turso\Commands\TursoSyncCommand; use RichanFongdasen\Turso\Database\TursoConnection; @@ -23,13 +22,6 @@ public function boot(): void { parent::boot(); - if ( - (config('database.default') !== 'turso') || - ((string) config('database.connections.turso.db_replica') === '') - ) { - return; - } - Event::listen(function (CommandStarting $event) { if (! app()->bound('running-artisan-command')) { app()->instance('running-artisan-command', data_get($event, 'command')); @@ -42,7 +34,6 @@ public function boot(): void } if ( - DB::hasModifiedRecords() && (app('running-artisan-command') === data_get($event, 'command')) ) { Turso::sync(); @@ -72,7 +63,7 @@ public function register(): void parent::register(); $this->app->scoped(TursoManager::class, function () { - return new TursoManager(config('database.connections.turso', [])); + return new TursoManager(); }); $this->app->extend(DatabaseManager::class, function (DatabaseManager $manager) { @@ -81,8 +72,6 @@ public function register(): void $pdo = $connector->connect($config); $connection = new TursoConnection($pdo, $database ?? 'turso', $prefix, $config); - app()->instance(TursoConnection::class, $connection); - $connection->createReadPdo($config); return $connection; diff --git a/src/TursoManager.php b/src/TursoManager.php index 9ab84b6..f344a54 100644 --- a/src/TursoManager.php +++ b/src/TursoManager.php @@ -4,51 +4,33 @@ namespace RichanFongdasen\Turso; -use BadMethodCallException; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; -use LogicException; use RichanFongdasen\Turso\Jobs\TursoSyncJob; class TursoManager { - protected TursoClient $client; - - protected Collection $config; - - public function __construct(array $config = []) - { - $this->config = new Collection($config); - $this->client = new TursoClient($config); - } - public function backgroundSync(): void { - if ((string) $this->config->get('db_replica') === '') { - throw new LogicException('Turso Error: You cannot sync the data when the read replica is not enabled.'); - } - - TursoSyncJob::dispatch(); + collect((array) config('database.connections')) + ->filter(fn ($config) => $config['driver'] === 'turso') + ->filter(fn ($config) => (string) $config['db_replica'] !== '') + ->each(function ($config, $connectionName) { + if (DB::connection($connectionName)->hasModifiedRecords()) { + TursoSyncJob::dispatch($connectionName); + } + }); } public function sync(): void { - if ((string) $this->config->get('db_replica') === '') { - throw new LogicException('Turso Error: You cannot sync the data when the read replica is not enabled.'); - } - - Artisan::call('turso:sync'); - - DB::forgetRecordModificationState(); - } - - public function __call(string $method, array $arguments = []): mixed - { - if (! method_exists($this->client, $method)) { - throw new BadMethodCallException('Call to undefined method ' . static::class . '::' . $method . '()'); - } - - return $this->client->$method(...$arguments); + collect((array) config('database.connections')) + ->filter(fn ($config) => $config['driver'] === 'turso') + ->filter(fn ($config) => (string) $config['db_replica'] !== '') + ->each(function ($config, $connectionName) { + if (DB::connection($connectionName)->hasModifiedRecords()) { + Artisan::call('turso:sync', ['connectionName' => $connectionName]); + } + }); } } diff --git a/tests/Feature/MultiConnectionsTest.php b/tests/Feature/MultiConnectionsTest.php new file mode 100644 index 0000000..f22eb41 --- /dev/null +++ b/tests/Feature/MultiConnectionsTest.php @@ -0,0 +1,44 @@ +set('database.connections.otherdb', [ + 'driver' => 'turso', + 'db_url' => env('DB_URL', 'http://127.0.0.1:8080'), + 'db_replica' => env('DB_REPLICA'), + 'database' => null, + 'prefix' => env('DB_PREFIX', ''), + 'access_token' => 'your-access-token', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'sticky' => env('DB_STICKY', true), + ]); + + migrateTables('projects'); + + $this->project1 = Project::factory()->create(); + $this->project2 = Project::factory()->create(); + $this->project3 = Project::factory()->create(); +}); + +afterEach(function () { + Schema::dropAllTables(); +}); + +test('each connection has its own turso client instance', function () { + $client1 = DB::connection('turso')->getPdo()->getClient(); + $client2 = DB::connection('otherdb')->getPdo()->getClient(); + + expect($client1)->not->toBe($client2); +})->group('MultiConnectionsTest', 'FeatureTest'); + +test('it can get all rows from the projects table through the otherdb connection', function () { + $projects = DB::connection('otherdb')->table('projects')->get(); + + expect($projects)->toHaveCount(3) + ->and($projects[0]->name)->toEqual($this->project1->name) + ->and($projects[1]->name)->toEqual($this->project2->name) + ->and($projects[2]->name)->toEqual($this->project3->name); +})->group('MultiConnectionsTest', 'FeatureTest'); diff --git a/tests/Feature/ReadReplicaTest.php b/tests/Feature/ReadReplicaTest.php index 5e63a4a..86db336 100644 --- a/tests/Feature/ReadReplicaTest.php +++ b/tests/Feature/ReadReplicaTest.php @@ -3,7 +3,6 @@ use Illuminate\Http\Client\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; -use RichanFongdasen\Turso\Facades\Turso; beforeEach(function () { $this->pdo = new \PDO('sqlite::memory:'); @@ -25,8 +24,6 @@ test('it will use the primary database connection for data manipulation operation', function () { fakeHttpRequest(); - Turso::freshHttpRequest(); - DB::table('users')->insert([ 'name' => 'June Monroe', ]); diff --git a/tests/Unit/Commands/TursoSyncCommandTest.php b/tests/Unit/Commands/TursoSyncCommandTest.php index 7917290..e688978 100644 --- a/tests/Unit/Commands/TursoSyncCommandTest.php +++ b/tests/Unit/Commands/TursoSyncCommandTest.php @@ -4,6 +4,24 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Process; +test('it will fail if the specified connection is not a Turso connection', function () { + Process::fake(); + + $result = Artisan::call('turso:sync', ['connectionName' => 'mysql']); + + expect($result)->toBe(1); + expect(Artisan::output())->toContain('The specified connection is not a Turso connection.'); +})->group('TursoSyncCommandTest', 'UnitTest'); + +test('it will fail if the specified connection does not have a read replica', function () { + Process::fake(); + + $result = Artisan::call('turso:sync', ['connectionName' => 'turso']); + + expect($result)->toBe(1); + expect(Artisan::output())->toContain('The specified connection does not have a read replica.'); +})->group('TursoSyncCommandTest', 'UnitTest'); + test('it can run the cli script to sync the database', function () { Process::fake(); diff --git a/tests/Unit/Database/TursoConnectionTest.php b/tests/Unit/Database/TursoConnectionTest.php index cc8a96b..b085c8e 100644 --- a/tests/Unit/Database/TursoConnectionTest.php +++ b/tests/Unit/Database/TursoConnectionTest.php @@ -1,29 +1,97 @@ connection = DB::connection('turso'); -}); +use RichanFongdasen\Turso\Jobs\TursoSyncJob; test('it can create a PDO object for read replica database connection', function () { - expect($this->connection->getReadPdo())->toBeInstanceOf(TursoPDO::class); + expect(DB::connection('turso')->getReadPdo())->toBeInstanceOf(TursoPDO::class); - $pdo = $this->connection->createReadPdo([ + $pdo = DB::connection('turso')->createReadPdo([ 'db_replica' => '/dev/null', ]); expect($pdo)->toBeInstanceOf(\PDO::class) - ->and($this->connection->getReadPdo())->toBe($pdo); + ->and(DB::connection('turso')->getReadPdo())->toBe($pdo); })->group('TursoConnectionTest', 'UnitTest'); test('it will return null when trying to create read PDO with no replica database path configured', function () { - expect($this->connection->createReadPdo())->toBeNull(); + expect(DB::connection('turso')->createReadPdo())->toBeNull(); })->group('TursoConnectionTest', 'UnitTest'); test('it can escape binary data and convert it into string type', function () { - $actual = $this->connection->escape('Hello world!', true); + $actual = DB::connection('turso')->escape('Hello world!', true); expect($actual)->toBe("x'48656c6c6f20776f726c6421'"); })->group('TursoConnectionTest', 'UnitTest'); + +test('it can trigger the sync command to synchronize the database', function () { + Process::fake(); + + config([ + 'database.connections.turso.db_replica' => '/tmp/turso.sqlite', + 'turso-laravel.sync_command.node_path' => '/dev/null', + ]); + + DB::connection('turso')->sync(); + + Process::assertRan(function (PendingProcess $process) { + $expectedPath = realpath(__DIR__ . '/../../..'); + + expect($process->command)->toBe('/dev/null turso-sync.mjs "http://127.0.0.1:8080" "your-access-token" "/tmp/turso.sqlite"') + ->and($process->timeout)->toBe(60) + ->and($process->path)->toBe($expectedPath); + + return true; + }); +})->group('TursoConnectionTest', 'UnitTest'); + +test('it can dispatch the sync job to synchronize the database', function () { + Bus::fake(); + + config([ + 'database.connections.turso.db_replica' => '/tmp/turso.sqlite', + 'turso-laravel.sync_command.node_path' => '/dev/null', + ]); + + DB::connection('turso')->backgroundSync(); + + Bus::assertDispatched(TursoSyncJob::class); +})->group('TursoConnectionTest', 'UnitTest'); + +test('it can enable query logging feature', function () { + DB::connection('turso')->enableQueryLog(); + + expect(DB::connection('turso')->logging())->toBeTrue() + ->and(DB::connection('turso')->tursoPdo()->getClient()->logging())->toBeTrue(); +})->group('TursoConnectionTest', 'UnitTest'); + +test('it can disable query logging feature', function () { + DB::connection('turso')->disableQueryLog(); + + expect(DB::connection('turso')->logging())->toBeFalse() + ->and(DB::connection('turso')->tursoPdo()->getClient()->logging())->toBeFalse(); +})->group('TursoConnectionTest', 'UnitTest'); + +test('it can get the query log', function () { + DB::connection('turso')->enableQueryLog(); + + $log = DB::connection('turso')->getQueryLog(); + + expect($log)->toBeArray() + ->and($log)->toHaveCount(0); +})->group('TursoConnectionTest', 'UnitTest'); + +test('it can flush the query log', function () { + DB::connection('turso')->enableQueryLog(); + + DB::connection('turso')->flushQueryLog(); + + $log = DB::connection('turso')->getQueryLog(); + + expect($log)->toBeArray() + ->and($log)->toHaveCount(0); +})->group('TursoConnectionTest', 'UnitTest'); diff --git a/tests/Unit/Http/RequestBodyTest.php b/tests/Unit/Http/RequestBodyTest.php index 9386b95..6673457 100644 --- a/tests/Unit/Http/RequestBodyTest.php +++ b/tests/Unit/Http/RequestBodyTest.php @@ -7,7 +7,7 @@ $this->baton = null; $this->request = RequestBody::create($this->baton) ->withCloseRequest() - ->withForeignKeyConstraints() + ->withForeignKeyConstraints(true) ->push(new ExecuteQuery('SELECT * FROM "users"')); }); @@ -110,7 +110,7 @@ test('it can convert itself into an array with baton value being set', function () { $this->request = RequestBody::create('some-baton-string') ->withCloseRequest() - ->withForeignKeyConstraints() + ->withForeignKeyConstraints(true) ->push(new ExecuteQuery('SELECT * FROM "users"')); expect($this->request->toArray())->toBe([ diff --git a/tests/Unit/TursoClientTest.php b/tests/Unit/TursoClientTest.php index 1bf30d8..ddf3899 100644 --- a/tests/Unit/TursoClientTest.php +++ b/tests/Unit/TursoClientTest.php @@ -3,14 +3,18 @@ use Illuminate\Http\Client\RequestException; use Illuminate\Support\Facades\Http; use RichanFongdasen\Turso\Exceptions\TursoQueryException; -use RichanFongdasen\Turso\Facades\Turso; +use RichanFongdasen\Turso\TursoClient; + +beforeEach(function () { + $this->client = new TursoClient(config('database.connections.turso')); +}); test('it can reset client state', function () { - Turso::resetHttpClientState(); + $this->client->resetHttpClientState(); - expect(Turso::getBaseUrl())->toBe('http://127.0.0.1:8080') - ->and(Turso::getBaton())->toBeNull(); -})->group('TursoClient', 'UnitTest'); + expect($this->client->getBaseUrl())->toBe('http://127.0.0.1:8080') + ->and($this->client->getBaton())->toBeNull(); +})->group('TursoClientTest', 'UnitTest'); test('it can log queries', function () { fakeHttpRequest(); @@ -67,24 +71,46 @@ ], ]; - Turso::enableQueryLog(); - Turso::freshHttpRequest(); + $this->client->enableQueryLog(); + $this->client->freshHttpRequest(); + + $this->client->query($statement, $bindings); + + expect($this->client->getQueryLog()->count())->toBe(1) + ->and($this->client->getQueryLog()->first())->toBe($expectedLog); +})->group('TursoClientTest', 'UnitTest'); + +test('it can flush the query log', function () { + fakeHttpRequest(); + + $statement = 'SELECT * FROM "users" WHERE "id" = ?'; + $bindings = [ + [ + 'type' => 'integer', + 'value' => 1, + ], + ]; + + $this->client->enableQueryLog(); + $this->client->freshHttpRequest(); + + $this->client->query($statement, $bindings); - Turso::query($statement, $bindings); + $this->client->flushQueryLog(); - expect(Turso::getQueryLog()->count())->toBe(1) - ->and(Turso::getQueryLog()->first())->toBe($expectedLog); -})->group('TursoClient', 'UnitTest'); + expect($this->client->getQueryLog()->count())->toBe(0) + ->and($this->client->getQueryLog()->first())->toBeNull(); +})->group('TursoClientTest', 'UnitTest'); test('it raises exception on any HTTP errors', function () { Http::fake([ '*' => Http::response(['message' => 'Internal Server Error'], 500), ]); - Turso::freshHttpRequest(); + $this->client->freshHttpRequest(); - Turso::query('SELECT * FROM "users"'); -})->throws(RequestException::class)->group('TursoClient', 'UnitTest'); + $this->client->query('SELECT * FROM "users"'); +})->throws(RequestException::class)->group('TursoClientTest', 'UnitTest'); test('it raises TursoQueryException when the query response has any error in it', function () { Http::fake([ @@ -104,8 +130,8 @@ ), ]); - Turso::query('SELECT * FROM "users"'); -})->throws(TursoQueryException::class)->group('TursoClient', 'UnitTest'); + $this->client->query('SELECT * FROM "users"'); +})->throws(TursoQueryException::class)->group('TursoClientTest', 'UnitTest'); test('it can replace the base url with the one that suggested by turso response', function () { fakeHttpRequest([ @@ -134,8 +160,8 @@ ], ]); - Turso::freshHttpRequest(); - Turso::query('SELECT * FROM "users"'); + $this->client->freshHttpRequest(); + $this->client->query('SELECT * FROM "users"'); - expect(Turso::getBaseUrl())->toBe('http://base-url-example.turso.io'); -})->group('TursoClient', 'UnitTest'); + expect($this->client->getBaseUrl())->toBe('http://base-url-example.turso.io'); +})->group('TursoClientTest', 'UnitTest'); diff --git a/tests/Unit/TursoManagerTest.php b/tests/Unit/TursoManagerTest.php index ff1e416..e619152 100644 --- a/tests/Unit/TursoManagerTest.php +++ b/tests/Unit/TursoManagerTest.php @@ -2,22 +2,11 @@ use Illuminate\Process\PendingProcess; use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Process; use RichanFongdasen\Turso\Facades\Turso; use RichanFongdasen\Turso\Jobs\TursoSyncJob; -test('it raises exception on calling an undefined method', function () { - Turso::undefinedMethod(); -})->throws(\BadMethodCallException::class)->group('TursoManagerTest', 'UnitTest'); - -test('it raises exception on calling the sync() method without configuring the read replica', function () { - Turso::sync(); -})->throws(\LogicException::class)->group('TursoManagerTest', 'UnitTest'); - -test('it raises exception on calling the backgroundSync() method without configuring the read replica', function () { - Turso::backgroundSync(); -})->throws(\LogicException::class)->group('TursoManagerTest', 'UnitTest'); - test('it can trigger the sync command immediately', function () { Process::fake(); @@ -26,6 +15,8 @@ 'turso-laravel.sync_command.node_path' => '/dev/null', ]); + DB::connection('turso')->setRecordModificationState(true); + Turso::sync(); Process::assertRan(function (PendingProcess $process) { @@ -44,6 +35,8 @@ config(['database.connections.turso.db_replica' => '/tmp/turso.sqlite']); + DB::connection('turso')->setRecordModificationState(true); + Turso::backgroundSync(); Bus::assertDispatched(TursoSyncJob::class); @@ -57,6 +50,8 @@ 'turso-laravel.sync_command.node_path' => '/dev/null', ]); + DB::connection('turso')->setRecordModificationState(true); + Turso::backgroundSync(); Process::assertRan(function (PendingProcess $process) {