laravel-aurora-dsql maintained by multiapi
laravel-aurora-dsql
Laravel (12+) database driver for Amazon Aurora DSQL.
- Connects with an IAM auth token through the official
awslabs/aurora-dsql-pdo-pgsqlconnector (SSLverify-full, OCC retries). - DSQL-compatible schema builder: regular Laravel migrations just work.
primary()andforeign()/constrained()are emitted inline inCREATE TABLE.index()/unique()→CREATE [UNIQUE] INDEX ASYNC, waiting for the index build to finish.id()/increments()→bigint generated by default as identity (cache 65536).- No schema transactions (DSQL allows a single DDL statement per transaction).
Schema::getTables(),db:show,db:wipeandmigrate:freshwork.
DsqlMigrationbase class for raw SQL migrations.
Nested transactions
Aurora DSQL does not support SAVEPOINT, so the connection reports supportsSavepoints() === false and a
nested DB::transaction() runs inside the outer transaction. An exception caught inside the nested
closure does not undo that closure's writes; one that reaches the outermost transaction rolls back
everything (and an OCC conflict retries the outermost closure).
Installation
composer require multiapi/laravel-aurora-dsql
Moved from
jgamboa/laravel-aurora-dsql(now abandoned, versions up to v0.2.2 stay installable). To switch,composer remove jgamboa/laravel-aurora-dsql && composer require multiapi/laravel-aurora-dsql. Since v0.3.0 the classes live underMultiapi\LaravelAuroraDsql; the oldJGamboa\LaravelAuroraDsqlnames keep working as aliases until 1.0. The configuration is unchanged.
The service provider is auto-discovered. Add a connection to config/database.php:
'dsql' => [
'driver' => 'dsql',
'host' => env('DSQL_HOST'), // xxxx.dsql.us-east-1.on.aws
'region' => env('AWS_REGION', 'us-east-1'),
'username' => env('DSQL_USER', 'admin'), // 'admin' => dsql:DbConnectAdmin; other roles => dsql:DbConnect
'database' => 'postgres', // DSQL has a single database per cluster
'profile' => env('DSQL_PROFILE'), // local AWS profile (e.g. SSO). Leave empty on Lambda/EC2/ECS
'sslrootcert' => env('DSQL_SSLROOTCERT', 'system'), // 'system' requires libpq 17+
'occ_max_retries' => 3,
'wait_for_indexes' => true, // wait for CREATE INDEX ASYNC jobs
'prefix' => '',
'prefix_indexes' => true,
'search_path' => env('DSQL_SEARCH_PATH', 'public'),
],
The driver applies search_path (or schema) to the session on connect, like Laravel's pgsql driver; leave both unset to keep the role's default search_path.
On Aurora DSQL a non-admin role cannot create objects in public (permission denied for schema public, and GRANT … ON SCHEMA public fails with "feature not supported on system entity"). An app that connects with its own role therefore uses its own schema and sets DSQL_SEARCH_PATH to it. Arco Cloud sets it automatically for its managed databases.
Usage
// A regular migration (no raw SQL)
return new class extends Migration
{
protected $connection = 'dsql';
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('category_id')->constrained()->cascadeOnDelete();
$table->string('sku')->unique();
$table->timestamps();
});
}
};
// Model: UUID v7 primary keys with HasUuids (Laravel 12+)
class Product extends Model
{
use HasUuids;
protected $connection = 'dsql';
}
Optimistic concurrency
DSQL uses optimistic concurrency control: a conflicting write fails with SQLSTATE 40001 (OC000/OC001), frequently at COMMIT. This package retries those conflicts for you, with exponential backoff (base 50 ms, cap 1000 ms, jitter):
DB::connection('dsql')->transaction(fn () => ...)re-runs the whole closure, up toocc_max_retries + 1attempts (default 4). Passingattempts:explicitly overrides this. The closure must be idempotent: it can run several times, so avoid side effects such as sending emails or dispatching non-transactional jobs inside it.- Autocommit writes (
INSERT/UPDATE/DELETEoutside a transaction, e.g. Eloquentsave()) are retried the same way. - Writes inside an open transaction are not retried individually: the conflict bubbles up to the outermost
transaction()call, which retries the whole closure. Nested transactions do not retry on their own. - Other errors are never retried, and the original exception is rethrown once attempts are exhausted.
Configure it per connection with 'occ_max_retries' => 3.
DSQL limitations (verified against a live cluster)
| Not supported | Alternative |
|---|---|
| Adding a PK / FK / UNIQUE / CHECK to an existing table | Declare them in Schema::create(); for unique use ->unique() (an index) |
ADD COLUMN with DEFAULT, NOT NULL or a FK |
Add it nullable → SET DEFAULT → backfill |
->change() (changing a column type), SET NOT NULL |
New table + copy + rename |
| Nested transactions (SAVEPOINT) | Avoid DB::transaction() inside another one |
TRUNCATE |
delete() |
Temporary tables, triggers, plpgsql, extensions, advisory locks, sharedLock() |
— |
| Full-text search with a language config | Only simple is available |
CREATE DATABASE |
One cluster per database |
Supported: lockForUpdate, upsert (needs a unique index), JSON/JSONB, CTEs, window functions, views, sequences, language sql functions.
Operational notes: connections last at most 60 minutes (long-running processes must reconnect); the IAM token is signed locally (no network call).
Testing
vendor/bin/phpunit --testsuite Unit
DSQL_HOST=xxxx.dsql.us-east-1.on.aws DSQL_PROFILE=my-profile vendor/bin/phpunit --testsuite Integration
License
MIT