Conversation
There was a problem hiding this comment.
Pull request overview
Introduces a first Laravel Schema/Migration “driver” foundation for SurrealDB so migrations can target Surreal-backed application data via Laravel’s Schema API.
Changes:
- Registers a new
surrealdatabase driver/connection for schema operations. - Adds Surreal schema grammar/builder/manager to support create/add/drop columns and create/drop tables for a narrow set of column types.
- Adds a Surreal-backed Pest feature test and documents current scope/limitations in the README.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
config/database.php |
Adds a surreal connection entry pointing at the new surreal driver. |
app/Providers/AppServiceProvider.php |
Registers the surreal database driver via DatabaseManager::extend(). |
app/Services/Surreal/Schema/SurrealSchemaConnection.php |
New Laravel Connection implementation used for Surreal schema operations. |
app/Services/Surreal/Schema/SurrealSchemaBuilder.php |
New schema builder that delegates table/column introspection & drops to Surreal. |
app/Services/Surreal/Schema/SurrealSchemaGrammar.php |
New schema grammar compiling Laravel schema commands into Surreal DEFINE/REMOVE statements. |
app/Services/Surreal/Schema/SurrealSchemaManager.php |
New manager running INFO queries and executing schema statements through the Surreal CLI. |
tests/Feature/SurrealSchemaDriverTest.php |
End-to-end test proving create/alter/drop via Schema::connection('surreal'). |
README.md |
Documents how to use Surreal-backed migrations and clarifies current scope. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $config, | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
SurrealSchemaConnection is constructed with an in-memory SQLite PDO. That means any accidental use of DB::connection('surreal')->select()/insert()/transaction() (or other non-schema operations that hit the base Connection implementation) would silently run against SQLite instead of SurrealDB, producing incorrect behavior without obvious failures. Consider explicitly disabling query/transaction APIs on this connection (override the relevant methods to throw a clear RuntimeException), and/or override getDriverName() to surreal to avoid Laravel treating it as SQLite anywhere internally.
| public function getDriverName(): string | |
| { | |
| return 'surreal'; | |
| } | |
| /** | |
| * SurrealSchemaConnection is schema-only; data queries are not supported. | |
| * | |
| * @param string $query | |
| * @param array<int, mixed> $bindings | |
| * @param bool $useReadPdo | |
| * @return array<int, mixed> | |
| */ | |
| public function select($query, $bindings = [], $useReadPdo = true) | |
| { | |
| throw new \RuntimeException('SurrealSchemaConnection does not support select queries. Use SurrealConnection for data access.'); | |
| } | |
| /** | |
| * SurrealSchemaConnection is schema-only; data queries are not supported. | |
| * | |
| * @param string $query | |
| * @param array<int, mixed> $bindings | |
| * @param bool $useReadPdo | |
| * @return \Generator | |
| */ | |
| public function cursor($query, $bindings = [], $useReadPdo = true) | |
| { | |
| throw new \RuntimeException('SurrealSchemaConnection does not support cursors. Use SurrealConnection for data access.'); | |
| } | |
| /** | |
| * SurrealSchemaConnection is schema-only; data queries are not supported. | |
| * | |
| * @param string $query | |
| * @param array<int, mixed> $bindings | |
| * @return bool | |
| */ | |
| public function insert($query, $bindings = []) | |
| { | |
| throw new \RuntimeException('SurrealSchemaConnection does not support insert queries. Use SurrealConnection for data access.'); | |
| } | |
| /** | |
| * SurrealSchemaConnection is schema-only; data queries are not supported. | |
| * | |
| * @param string $query | |
| * @param array<int, mixed> $bindings | |
| * @return int | |
| */ | |
| public function update($query, $bindings = []) | |
| { | |
| throw new \RuntimeException('SurrealSchemaConnection does not support update queries. Use SurrealConnection for data access.'); | |
| } | |
| /** | |
| * SurrealSchemaConnection is schema-only; data queries are not supported. | |
| * | |
| * @param string $query | |
| * @param array<int, mixed> $bindings | |
| * @return int | |
| */ | |
| public function delete($query, $bindings = []) | |
| { | |
| throw new \RuntimeException('SurrealSchemaConnection does not support delete queries. Use SurrealConnection for data access.'); | |
| } | |
| /** | |
| * SurrealSchemaConnection is schema-only; transactions are not supported. | |
| * | |
| * @param \Closure $callback | |
| * @param int $attempts | |
| * @return mixed | |
| */ | |
| public function transaction(\Closure $callback, $attempts = 1) | |
| { | |
| throw new \RuntimeException('SurrealSchemaConnection does not support transactions. Use SurrealConnection for data access.'); | |
| } | |
| /** | |
| * SurrealSchemaConnection is schema-only; transactions are not supported. | |
| */ | |
| public function beginTransaction() | |
| { | |
| throw new \RuntimeException('SurrealSchemaConnection does not support transactions. Use SurrealConnection for data access.'); | |
| } | |
| /** | |
| * SurrealSchemaConnection is schema-only; transactions are not supported. | |
| */ | |
| public function commit() | |
| { | |
| throw new \RuntimeException('SurrealSchemaConnection does not support transactions. Use SurrealConnection for data access.'); | |
| } | |
| /** | |
| * SurrealSchemaConnection is schema-only; transactions are not supported. | |
| * | |
| * @param int|null $toLevel | |
| */ | |
| public function rollBack($toLevel = null) | |
| { | |
| throw new \RuntimeException('SurrealSchemaConnection does not support transactions. Use SurrealConnection for data access.'); | |
| } |
| function retryStartingSurrealSchemaServer(SurrealCliClient $client, string $storagePath, int $attempts = 3): array | ||
| { | ||
| $lastException = null; | ||
|
|
||
| for ($attempt = 1; $attempt <= $attempts; $attempt++) { | ||
| $port = reserveSurrealSchemaPort(); | ||
| $endpoint = sprintf('ws://127.0.0.1:%d', $port); | ||
| $process = $client->startLocalServer( | ||
| bindAddress: sprintf('127.0.0.1:%d', $port), | ||
| datastorePath: $storagePath, | ||
| username: 'root', | ||
| password: 'root', | ||
| storageEngine: 'surrealkv', | ||
| ); | ||
|
|
||
| if ($client->waitUntilReady($endpoint)) { | ||
| return [ | ||
| 'endpoint' => $endpoint, | ||
| 'port' => $port, | ||
| 'process' => $process, | ||
| ]; | ||
| } | ||
|
|
||
| $process->stop(1); | ||
| $lastException = new RuntimeException(sprintf('SurrealDB did not become ready on %s.', $endpoint)); | ||
| } | ||
|
|
||
| throw $lastException ?? new RuntimeException('Unable to start the SurrealDB schema test runtime.'); | ||
| } | ||
|
|
||
| function reserveSurrealSchemaPort(): int | ||
| { | ||
| $socket = stream_socket_server('tcp://127.0.0.1:0', $errorCode, $errorMessage); | ||
|
|
||
| if ($socket === false) { | ||
| throw new RuntimeException(sprintf('Unable to reserve a free TCP port: %s (%d)', $errorMessage, $errorCode)); | ||
| } | ||
|
|
||
| $address = stream_socket_get_name($socket, false); | ||
|
|
||
| fclose($socket); | ||
|
|
||
| if ($address === false) { | ||
| throw new RuntimeException('Unable to determine the reserved TCP port.'); | ||
| } | ||
|
|
||
| $port = (int) ltrim((string) strrchr($address, ':'), ':'); | ||
|
|
||
| if ($port <= 0) { | ||
| throw new RuntimeException(sprintf('Unable to parse the reserved TCP port from [%s].', $address)); | ||
| } | ||
|
|
||
| return $port; | ||
| } |
There was a problem hiding this comment.
This test duplicates the same port-reservation + retry-start-server helpers already present in other Surreal feature tests (e.g. retryStartingWorkspaceServer / reserveWorkspacePort). Consider extracting these helpers into a shared test support file/trait (or a Pest helper) to reduce duplication and keep the startup logic consistent across tests.
e9ee757 to
0045594
Compare
0045594 to
8d9664b
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| config()->set('database.default', 'surreal'); | ||
| config()->set('database.migrations.connection', null); | ||
|
|
||
| config()->set('surreal.host', '127.0.0.1'); | ||
| config()->set('surreal.port', $server['port']); | ||
| config()->set('surreal.endpoint', $server['endpoint']); | ||
| config()->set('surreal.username', 'root'); | ||
| config()->set('surreal.password', 'root'); | ||
| config()->set('surreal.namespace', 'katra'); | ||
| config()->set('surreal.database', 'schema_driver_migrate_test'); | ||
| config()->set('surreal.storage_engine', 'surrealkv'); | ||
| config()->set('surreal.storage_path', $storagePath); | ||
| config()->set('surreal.runtime', 'local'); | ||
| config()->set('surreal.autostart', false); |
There was a problem hiding this comment.
This test mutates global config (database.default and database.migrations.connection) but never restores the previous values. Because Pest runs tests in the same process, this can leak into later tests (e.g. those using RefreshDatabase) and cause them to run against the Surreal connection unexpectedly. Capture the original values before overriding and restore them in the finally block (or in an afterEach).
| return parent::repositoryExists(); | ||
| } | ||
|
|
||
| return $this->schemaManager()->hasTable($this->table); |
There was a problem hiding this comment.
repositoryExists() uses $this->table directly, while the rest of the Surreal-specific statements go through normalizedTable() (which also validates the identifier). Using the raw table name here can lead to inconsistent behavior and delayed failures when a custom migrations table name contains unsupported characters. Consider using normalizedTable() here as well so validation is consistent and errors surface early.
| return $this->schemaManager()->hasTable($this->table); | |
| return $this->schemaManager()->hasTable($this->normalizedTable()); |
Summary
Testing
Closes #113