Skip to content

Commit

Permalink
Add support for Turso's embedded replica feature
Browse files Browse the repository at this point in the history
  • Loading branch information
richan-fongdasen committed Apr 14, 2024
1 parent 6721b0e commit 131e53c
Show file tree
Hide file tree
Showing 17 changed files with 536 additions and 24 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ phpunit.xml
phpstan.neon
testbench.yaml
vendor
package-lock.json
node_modules
*.swp
*.sqlite
*.sqlite-shm
*.sqlite-wal
*.sqlite-client_wal_index
67 changes: 63 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,31 +39,90 @@ To use Turso as your database driver in Laravel, you need to append the followin
```php
'turso' => [
'driver' => 'turso',
'turso_url' => env('DB_URL', 'http://localhost:8080'),
'database' => null,
'prefix' => env('DB_PREFIX', ''),
'db_url' => env('DB_URL', 'http://localhost:8080'),
'access_token' => env('DB_ACCESS_TOKEN', null),
'db_replica' => env('DB_REPLICA'),
'database' => null, // Leave this null
'prefix' => env('DB_PREFIX', ''),
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],
```

### Publishing Configuration and Sync Script

You can publish the configuration file and sync script by running the following command:

```bash
php artisan vendor:publish --provider="RichanFongdasen\Turso\TursoLaravelServiceProvider"
```

The above command will publish the following files:

- `config/turso-laravel.php`
- `turso-sync.mjs`

### Installing Node.js Dependencies

The Turso database driver requires Node.js to run the sync script. You can install the Node.js dependencies by running the following command:

```bash
npm install @libsql/client
```

## Configuration

In Laravel application, The database driver configuration is stored in your `.env` file. Here is the list of available configuration for Turso database driver:

```bash
DB_CONNECTION=turso
DB_URL=http://localhost:8080
DB_PREFIX=
DB_ACCESS_TOKEN=
DB_REPLICA=
DB_PREFIX=
DB_FOREIGN_KEYS=true
```

| ENV Variable Name | Description |
| :---------------- | :--------------------------------------------------------------------------------------------- |
| DB_URL | The Turso database server URL. E.g: `https://[databaseName]-[organizationName].turso.io` |
| DB_ACCESS_TOKEN | (Optional) The access token to access the Turso database server. |
| DB_REPLICA | (Optional) The full path to the local embedded replica database file. E.g: `/tmp/turso.sqlite` |
| DB_PREFIX | (Optional) The database table prefix. |
| DB_FOREIGN_KEYS | Enable or disable foreign key constraints, default is `true`. |

## Usage

For local development, you can use the local Turso database server that is provided by the Turso database team for development purposes. You can find the instruction to run the local Turso database server in the [Turso CLI documentation](https://docs.turso.tech/local-development#turso-cli).

The Turso database driver should work as expected with Laravel Query Builder and Eloquent ORM.

The database driver supports the embedded replica feature. If you're not familiar with the embedded replica feature, you can read the [Turso embedded replica documentation](https://turso.tech/blog/introducing-embedded-replicas-deploy-turso-anywhere-2085aa0dc242).

### Running the sync script from artisan command

You can run the sync script manually from the artisan command by using the following command:

```bash
php artisan turso:sync
```

### Running the sync script programmatically

You can run the sync script programmatically by using the following code:

```php
use Illuminate\Support\Facades\DB;
use RichanFongdasen\Turso\Facades\Turso;

if ( DB::dataHasChanged() ) {
// Run the sync script immediately
Turso::sync();

// Run the sync script in the background
Turso::backgroundSync();
}
```

## Debugging

There is a way to debug the HTTP request and response that is sent and received by the Turso database client. Here is the example of how to enable the debugging feature:
Expand Down
8 changes: 7 additions & 1 deletion config/turso-laravel.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<?php

// config for RichanFongdasen/TursoLaravel
return [];
return [
'sync' => [
'script_filename' => 'turso-sync.mjs',
'script_path' => realpath(__DIR__ . '/..'),
'timeout' => 60,
],
];
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"private": true,
"scripts": {
"sync": "node turso-sync.js"
},
"devDependencies": {
"@libsql/client": "^0.6.0"
}
}
45 changes: 45 additions & 0 deletions src/Commands/TursoSyncCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace RichanFongdasen\Turso\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Process;

class TursoSyncCommand extends Command
{
public $signature = 'turso:sync';

public $description = 'Sync changes from the remote database to the local replica manually.';

protected function compileRunProcess(): string
{
return sprintf(
'node %s "%s" "%s" "%s"',
config('turso-laravel.sync.script_filename'),
config('database.connections.turso.db_url'),
config('database.connections.turso.access_token'),
config('database.connections.turso.db_replica'),
);
}

public function handle(): int
{
$timeout = (int) config('turso-laravel.sync.timeout');

$result = Process::timeout($timeout)
->path(config('turso-laravel.sync.script_path') ?? base_path())
->run($this->compileRunProcess());

if ($result->failed()) {
$this->error($result->errorOutput());

return self::FAILURE;
}

$this->info($result->output());

return self::SUCCESS;
}
}
52 changes: 52 additions & 0 deletions src/Database/TursoConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

class TursoConnection extends SQLiteConnection
{
protected bool $dataHasChanged = false;

public function __construct(TursoPDO $pdo, string $database = ':memory:', string $tablePrefix = '', array $config = [])
{
parent::__construct($pdo, $database, $tablePrefix, $config);
Expand Down Expand Up @@ -67,4 +69,54 @@ protected function getDefaultPostProcessor(): TursoQueryProcessor
{
return new TursoQueryProcessor();
}

/**
* Run an insert statement against the database.
*
* @param string $query
* @param array $bindings
*
* @return bool
*/
public function insert($query, $bindings = [])
{
$this->dataHasChanged = true;

return parent::insert($query, $bindings);
}

/**
* Run an update statement against the database.
*
* @param string $query
* @param array $bindings
*
* @return int
*/
public function update($query, $bindings = [])
{
$this->dataHasChanged = true;

return parent::update($query, $bindings);
}

/**
* Run a delete statement against the database.
*
* @param string $query
* @param array $bindings
*
* @return int
*/
public function delete($query, $bindings = [])
{
$this->dataHasChanged = true;

return parent::delete($query, $bindings);
}

public function dataHasChanged(): bool
{
return $this->dataHasChanged;
}
}
8 changes: 4 additions & 4 deletions src/Facades/Turso.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
namespace RichanFongdasen\Turso\Facades;

use Illuminate\Support\Facades\Facade;
use RichanFongdasen\Turso\TursoClient;
use RichanFongdasen\Turso\TursoManager;

/**
* @see \RichanFongdasen\Turso\TursoClient
* @see \RichanFongdasen\Turso\TursoHttpClient
*
* @mixin \RichanFongdasen\Turso\TursoClient
* @mixin \RichanFongdasen\Turso\TursoHttpClient
*/
class Turso extends Facade
{
protected static function getFacadeAccessor(): string
{
return TursoClient::class;
return TursoManager::class;
}
}
28 changes: 28 additions & 0 deletions src/Jobs/TursoSyncJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace RichanFongdasen\Turso\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Artisan;

class TursoSyncJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;

/**
* Execute the job.
*/
public function handle(): void
{
Artisan::call('turso:sync');
}
}
36 changes: 26 additions & 10 deletions src/TursoClient.php → src/TursoHttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,25 @@
use Illuminate\Support\Facades\Http;
use RichanFongdasen\Turso\Exceptions\TursoQueryException;

class TursoClient
class TursoHttpClient
{
protected string $baseUrl;

protected ?string $baton = null;

protected array $config = [];

protected bool $isOpen;

protected bool $loggingQueries;

protected Collection $queryLog;

protected ?PendingRequest $request = null;

public function __construct()
public function __construct(array $config = [])
{
$this->config = config('database.connections.turso', []);
$this->config = $config;

$this->queryLog = new Collection();

Expand All @@ -35,7 +37,9 @@ public function __construct()

public function __destruct()
{
$this->close();
if ($this->isOpen) {
$this->close();
}
}

public function close(): void
Expand Down Expand Up @@ -93,6 +97,15 @@ public function enableQueryLog(): void
$this->loggingQueries = true;
}

public function freshRequest(): PendingRequest
{
$this->resetClientState();

$this->request = $this->createRequest();

return $this->request;
}

public function getBaseUrl(): ?string
{
return $this->baseUrl;
Expand All @@ -119,6 +132,10 @@ public function query(string $statement, array $bindingValues = []): array
$response->throw();
}

if (! $this->isOpen) {
$this->isOpen = true;
}

$jsonResponse = $response->json();

if ($this->loggingQueries) {
Expand Down Expand Up @@ -152,16 +169,15 @@ public function query(string $statement, array $bindingValues = []): array

public function request(): PendingRequest
{
if ($this->request === null) {
$this->request = $this->createRequest();
}

return $this->request;
return ($this->request === null)
? $this->freshRequest()
: $this->request;
}

public function resetClientState(): void
{
$this->baton = null;
$this->baseUrl = data_get($this->config, 'turso_url');
$this->baseUrl = data_get($this->config, 'db_url');
$this->isOpen = false;
}
}
Loading

0 comments on commit 131e53c

Please sign in to comment.