-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add surreal schema driver foundation #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ibourgeois
wants to merge
5
commits into
main
Choose a base branch
from
codex/feat-surreal-migration-driver
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
8d9664b
feat: add surreal schema driver foundation
ibourgeois 38b055a
fix: replace surreal cli query bridge
ibourgeois b2292a9
fix: tighten surreal schema safeguards
ibourgeois 25d62fa
fix: align surreal schema builder style
ibourgeois ac22081
fix: harden surreal transport safeguards
ibourgeois File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
277 changes: 277 additions & 0 deletions
277
app/Services/Surreal/Migrations/SurrealMigrationRepository.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,277 @@ | ||
| <?php | ||
|
|
||
| namespace App\Services\Surreal\Migrations; | ||
|
|
||
| use App\Services\Surreal\Schema\SurrealSchemaConnection; | ||
| use App\Services\Surreal\Schema\SurrealSchemaManager; | ||
| use App\Services\Surreal\SurrealHttpClient; | ||
| use App\Services\Surreal\SurrealRuntimeManager; | ||
| use Illuminate\Database\ConnectionResolverInterface as Resolver; | ||
| use Illuminate\Database\Migrations\DatabaseMigrationRepository; | ||
| use Illuminate\Support\Arr; | ||
| use JsonException; | ||
|
|
||
| class SurrealMigrationRepository extends DatabaseMigrationRepository | ||
| { | ||
| public function __construct(Resolver $resolver, string $table, private readonly SurrealHttpClient $client) | ||
| { | ||
| parent::__construct($resolver, $table); | ||
| } | ||
|
|
||
| public function getRan() | ||
| { | ||
| if (! $this->usesSurrealRepository()) { | ||
| return parent::getRan(); | ||
| } | ||
|
|
||
| return array_map( | ||
| static fn (array $row): string => (string) $row['migration'], | ||
| $this->selectRows(sprintf( | ||
| 'SELECT migration, batch FROM %s ORDER BY batch ASC, migration ASC;', | ||
| $this->normalizedTable(), | ||
| )), | ||
| ); | ||
| } | ||
|
|
||
| public function getMigrations($steps) | ||
| { | ||
| if (! $this->usesSurrealRepository()) { | ||
| return parent::getMigrations($steps); | ||
| } | ||
|
|
||
| return $this->selectRows(sprintf( | ||
| 'SELECT migration, batch FROM %s WHERE batch >= 1 ORDER BY batch DESC, migration DESC LIMIT %d;', | ||
| $this->normalizedTable(), | ||
| (int) $steps, | ||
| )); | ||
| } | ||
|
|
||
| public function getMigrationsByBatch($batch) | ||
| { | ||
| if (! $this->usesSurrealRepository()) { | ||
| return parent::getMigrationsByBatch($batch); | ||
| } | ||
|
|
||
| return $this->selectRows(sprintf( | ||
| 'SELECT migration, batch FROM %s WHERE batch = %d ORDER BY migration DESC;', | ||
| $this->normalizedTable(), | ||
| (int) $batch, | ||
| )); | ||
| } | ||
|
|
||
| public function getLast() | ||
| { | ||
| if (! $this->usesSurrealRepository()) { | ||
| return parent::getLast(); | ||
| } | ||
|
|
||
| $lastBatch = $this->getLastBatchNumber(); | ||
|
|
||
| if ($lastBatch === 0) { | ||
| return []; | ||
| } | ||
|
|
||
| return $this->getMigrationsByBatch($lastBatch); | ||
| } | ||
|
|
||
| public function getMigrationBatches() | ||
| { | ||
| if (! $this->usesSurrealRepository()) { | ||
| return parent::getMigrationBatches(); | ||
| } | ||
|
|
||
| $batches = []; | ||
|
|
||
| foreach ($this->selectRows(sprintf( | ||
| 'SELECT migration, batch FROM %s ORDER BY batch ASC, migration ASC;', | ||
| $this->normalizedTable(), | ||
| )) as $row) { | ||
| $batches[(string) $row['migration']] = (int) $row['batch']; | ||
| } | ||
|
|
||
| return $batches; | ||
| } | ||
|
|
||
| public function log($file, $batch) | ||
| { | ||
| if (! $this->usesSurrealRepository()) { | ||
| parent::log($file, $batch); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| $this->schemaManager()->statement(sprintf( | ||
| 'CREATE %s CONTENT %s;', | ||
| $this->normalizedTable(), | ||
| $this->jsonLiteral([ | ||
| 'migration' => (string) $file, | ||
| 'batch' => (int) $batch, | ||
| ]), | ||
| )); | ||
| } | ||
|
|
||
| public function delete($migration) | ||
| { | ||
| if (! $this->usesSurrealRepository()) { | ||
| parent::delete($migration); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| $this->schemaManager()->statement(sprintf( | ||
| 'DELETE %s WHERE migration = %s;', | ||
| $this->normalizedTable(), | ||
| $this->jsonLiteral((string) $migration->migration), | ||
| )); | ||
| } | ||
|
|
||
| public function getLastBatchNumber() | ||
| { | ||
| if (! $this->usesSurrealRepository()) { | ||
| return parent::getLastBatchNumber(); | ||
| } | ||
|
|
||
| return (int) $this->selectScalar(sprintf( | ||
| 'SELECT VALUE batch FROM %s ORDER BY batch DESC LIMIT 1;', | ||
| $this->normalizedTable(), | ||
| ), 0); | ||
| } | ||
|
|
||
| public function createRepository() | ||
| { | ||
| if (! $this->usesSurrealRepository()) { | ||
| parent::createRepository(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| $this->schemaManager()->statements([ | ||
| sprintf('DEFINE TABLE %s SCHEMAFULL;', $this->normalizedTable()), | ||
| sprintf('DEFINE FIELD migration ON TABLE %s TYPE string;', $this->normalizedTable()), | ||
| sprintf('DEFINE FIELD batch ON TABLE %s TYPE int;', $this->normalizedTable()), | ||
| sprintf('DEFINE INDEX migration_unique ON TABLE %s COLUMNS migration UNIQUE;', $this->normalizedTable()), | ||
| ]); | ||
| } | ||
|
|
||
| public function repositoryExists() | ||
| { | ||
| if (! $this->usesSurrealRepository()) { | ||
| return parent::repositoryExists(); | ||
| } | ||
|
|
||
| return $this->schemaManager()->hasTable($this->normalizedTable()); | ||
| } | ||
|
|
||
| public function deleteRepository() | ||
| { | ||
| if (! $this->usesSurrealRepository()) { | ||
| parent::deleteRepository(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| $this->schemaManager()->statement(sprintf('REMOVE TABLE %s;', $this->normalizedTable())); | ||
| } | ||
|
|
||
| private function usesSurrealRepository(): bool | ||
| { | ||
| $connectionName = $this->connection ?? $this->resolver->getDefaultConnection(); | ||
|
|
||
| if ($connectionName === null) { | ||
| return false; | ||
| } | ||
|
|
||
| return $this->connectionDriver($connectionName) === 'surreal'; | ||
| } | ||
|
|
||
| private function connectionDriver(string $connectionName): ?string | ||
| { | ||
| return $this->resolver->connection($connectionName)->getConfig('driver'); | ||
| } | ||
|
|
||
| /** | ||
| * @return list<array<string, mixed>> | ||
| */ | ||
| private function selectRows(string $query): array | ||
| { | ||
| $result = Arr::get($this->query($query), '0', []); | ||
|
|
||
| if (! is_array($result)) { | ||
| return []; | ||
| } | ||
|
|
||
| return array_values(array_filter( | ||
| $result, | ||
| static fn (mixed $row): bool => is_array($row), | ||
| )); | ||
| } | ||
|
|
||
| private function selectScalar(string $query, mixed $default = null): mixed | ||
| { | ||
| $result = Arr::get($this->query($query), '0', []); | ||
|
|
||
| if (! is_array($result) || $result === []) { | ||
| return $default; | ||
| } | ||
|
|
||
| return $result[0] ?? $default; | ||
| } | ||
|
|
||
| /** | ||
| * @return list<mixed> | ||
| */ | ||
| private function query(string $query): array | ||
| { | ||
| if (! $this->runtimeManager()->ensureReady()) { | ||
| throw new \RuntimeException('The SurrealDB runtime is not available for migration repository operations.'); | ||
| } | ||
|
|
||
| return $this->client->runQuery( | ||
| endpoint: (string) $this->surrealConfig('endpoint'), | ||
| namespace: (string) $this->surrealConfig('namespace'), | ||
| database: (string) $this->surrealConfig('database'), | ||
| username: (string) $this->surrealConfig('username'), | ||
| password: (string) $this->surrealConfig('password'), | ||
| query: $query, | ||
| ); | ||
| } | ||
|
|
||
| private function schemaManager(): SurrealSchemaManager | ||
| { | ||
| /** @var SurrealSchemaConnection $connection */ | ||
| $connection = $this->resolver->connection($this->connection ?? $this->resolver->getDefaultConnection()); | ||
|
|
||
| return $connection->schemaManager(); | ||
| } | ||
|
|
||
| private function runtimeManager(): SurrealRuntimeManager | ||
| { | ||
| /** @var SurrealSchemaConnection $connection */ | ||
| $connection = $this->resolver->connection($this->connection ?? $this->resolver->getDefaultConnection()); | ||
|
|
||
| return $connection->runtimeManager(); | ||
| } | ||
|
|
||
| private function surrealConfig(string $key): mixed | ||
| { | ||
| return $this->resolver->connection($this->connection ?? $this->resolver->getDefaultConnection())->getConfig($key); | ||
| } | ||
|
|
||
| private function normalizedTable(): string | ||
| { | ||
| if (! preg_match('/^[A-Za-z0-9_]+$/', $this->table)) { | ||
| throw new \RuntimeException(sprintf('The Surreal migration table identifier [%s] contains unsupported characters.', $this->table)); | ||
| } | ||
|
|
||
| return $this->table; | ||
| } | ||
|
|
||
| private function jsonLiteral(mixed $value): string | ||
| { | ||
| try { | ||
| return json_encode($value, JSON_THROW_ON_ERROR); | ||
| } catch (JsonException $exception) { | ||
| throw new \RuntimeException('Unable to encode the Surreal migration payload.', previous: $exception); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SurrealMigrationRepository::query()callsSurrealHttpClient::runQuery()directly without ensuring the Surreal runtime is ready / autostarted, unlikeSurrealSchemaManagerandSurrealDocumentStorewhich gate calls behindSurrealRuntimeManager::ensureReady(). This can make migration/status operations fail depending on call order (e.g. if repository methods are invoked before any schema operation has triggered startup). Consider injecting/deriving aSurrealRuntimeManager(or exposing anensureReady()on the schema connection/manager) and failing fast with a clear error when the runtime can’t be reached.