singlestore/singlestore-laravel
Official SingleStore integration for Laravel, providing a native database driver with Eloquent and query builder support. Connect Laravel apps to SingleStore for fast distributed SQL, migrations, schema management, and familiar Laravel workflows.
What you're getting. singlestore/singlestore-laravel is a first-party database driver that registers a singlestore connection driver with Laravel's DatabaseManager. Internally it subclasses Laravel's MySQL connection, query grammar, schema grammar, and post-processor, because SingleStoreDB speaks the MySQL wire protocol. Practically that means Eloquent models, the query builder, migrate, db:seed, and RefreshDatabase all work with almost no ceremony — but you are on a MySQL-shaped surface talking to a distributed HTAP engine, and that gap is where the real work lives.
Minimum steps.
mysqli PHP extension. The driver connects via mysqli, not PDO, so a stock php:8.x-fpm image without docker-php-ext-install mysqli will fail at connect time with a confusing error.composer require singlestore/singlestore-laravel — the service provider is auto-discovered; there's nothing to publish.config/database.php that mirrors your mysql block but changes the driver key:'singlestore' => [
'driver' => 'singlestore',
'host' => env('SINGLESTORE_HOST', '127.0.0.1'),
'port' => env('SINGLESTORE_PORT', '3306'),
'database' => env('SINGLESTORE_DATABASE', 'app'),
'username' => env('SINGLESTORE_USERNAME', 'root'),
'password' => env('SINGLESTORE_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
DB_CONNECTION=singlestore (or leave your primary alone and address it explicitly — see below).php artisan tinker → DB::connection('singlestore')->select('select now()'), then php artisan migrate.Where to look first. Read the driver's source top-down: Connections/SingleStoreConnection, Connectors/SingleStoreConnector, Schema/Grammars/*, Query/Grammars/*. Each is a thin subclass of the corresponding Illuminate\Database\MySql* class, which tells you exactly how far MySQL fidelity goes and where it stops. When something behaves oddly, diff the driver class against the Laravel MySQL class it extends before opening a GitHub issue.
First real use case. The classic shape is a mixed workload: keep your transactional tables in rowstore (in-memory) form and put event/fact tables in columnstore, then let a few reporting endpoints or scheduled aggregates run against the same cluster. Start by pointing one read-heavy model or one DB::connection('singlestore') report at it rather than migrating the whole app on day one.
Secondary connection, not replacement. Most teams I'd expect to run this as a second connection alongside MySQL/Postgres. Set it per-model with protected $connection = 'singlestore';, or ad hoc with Model::on('singlestore') / DB::connection('singlestore')->table(...). Eloquent relationships only work intra-connection, so treat "which data lives where" as a schema decision, not a query-time one.
Keep schema DDL explicit. The schema grammar is MySQL-derived, so $table->string(), indexes, and id() are fine, but SingleStore's defining features — SHARD KEY, SORT KEY, COLUMNSTORE, REFERENCE TABLE, ROWSTORE — are not first-class builder methods. Do them in raw statements inside migrations:
Schema::create('events', fn (Blueprint $t) => $t->bigIncrements('id')->...);
DB::statement('ALTER TABLE events ADD SHARD KEY (tenant_id)');
DB::statement('ALTER TABLE events ADD SORT KEY (created_at)');
Because ALTERs on SingleStore rewrite data, front-load these decisions in the initial create rather than bolting them on later.
Batch your writes. Columnstore tables want bulk inserts. Use DB::table('events')->insert($chunk) in slices of a few hundred to a few thousand rows, or LOAD DATA LOCAL INFILE for backfills, rather than looping Model::create(). Row-by-row Eloquent writes against a columnstore table is the single most common way to make this package look slow.
Choose table shape per table, per table's job. migrations, sessions, jobs, and small lookup tables → rowstore. High-volume append-only event/analytics tables → columnstore. Getting this inverted (a columnstore jobs table, say) produces transactional surprises rather than performance ones.
Use read/write keys deliberately. Laravel's split read/write config keys pass through, which is useful if you point reads at a different aggregator during heavy ingest, but on SingleStore both usually resolve to the same cluster. Don't assume splitting gets you much.
Test against a real node. SQLite will happily accept migrations the SingleStore grammar rejects. Run CI against the singlestoredb/singlestoredb-dev container with RefreshDatabase bound to the singlestore connection; the grammar/schema feedback loop is the main value.
Reporting queries. For window functions, CTEs, and JSON_* work, drop to DB::connection('singlestore')->select($sql, $bindings) rather than coaxing the builder. Keep those queries in dedicated repository classes so you can tune and explain them independently.
No PDO under the hood. The connection is mysqli-backed. Third-party packages that reach for $connection->getPdo() and call PDO-only methods (getAttribute, MYSQL_ATTR_LOCAL_INFILE, custom PDOStatement handling) are the most likely thing to break. Audit any package that does raw PDO work before adopting it here.
Transactions only exist on rowstore. Columnstore writes auto-commit. DB::transaction() wrapping columnstore inserts will appear to work and will not roll back on failure. Never rely on transactions for fact-table ingestion — make writes idempotent instead.
Foreign keys aren't enforced. ->foreignId()->constrained() emits the DDL but SingleStore does not enforce referential integrity. Treat FKs as documentation and enforce invariants in application code or via your ingest pipeline.
upsert() / updateOrCreate() semantics. ON DUPLICATE KEY UPDATE needs a unique key and behaves differently across table types; updateOrCreate is a read-then-write and is racy under concurrency. Test both explicitly, and check whether duplicates are silently collapsed by columnstore unique-key behavior.
lockForUpdate() / pessimistic locking. Meaningless or unsupported against columnstore, and the database queue driver (jobs table with FOR UPDATE) should stay on rowstore or move to Redis/SQS entirely. Same reasoning for Cache::store('database') and DB sessions.
strict => true may not stick. The MySQL connector issues SET SESSION sql_mode, and SingleStore's supported modes don't line up perfectly with MySQL's. If connection setup throws, flip 'strict' => false first — it's the usual culprit, and it's a config quirk, not a driver bug.
Idle connections get reaped. Long-running queue workers and scheduled reports will surface MySQL server has gone away. Raise wait_timeout if you control the cluster, and wrap long jobs with DB::reconnect('singlestore'). Persistent connections are not your friend here.
Long analytical queries hit PHP and proxy timeouts before the database's. Set explicit max_execution_time/statement timeouts in config, and be aware that an aggregator-side timeout looks like a generic query exception.
Schema introspection is expensive at scale. Schema::hasTable(), hasColumn(), and anything that hits information_schema on a large cluster is slower than on MySQL. migrate is fine; per-request introspection is not. Guard migrations so they aren't re-probing on every deploy.
Debugging toolkit. DB::listen(), ->dd()/->dump(), DB::enableQueryLog(), and Laravel Telescope all work normally through the driver — start there. EXPLAIN on SingleStore gives you shard/repartition info that has no MySQL equivalent; learn to read it before you start rewriting queries.
Extension points. You can DB::extend('singlestore', ...) with your own resolver, subclass the grammars to add builder methods for SHARD KEY/SORT KEY, or bind a custom connection class — all reasonable if you find yourself repeating raw DDL. There is no config file to publish, so connection tuning lives entirely in config/database.php.
Version discipline. The driver tracks Laravel majors closely and inherits from MySQL classes that change between minors. Pin it in composer.json and re-run the full migration suite whenever you bump Laravel; grammar inheritance failures are subtle and show up as malformed SQL, not exceptions.
How can I help you explore Laravel packages today?