Suprnova's database layer wraps SeaORM with a Laravel-shaped DB facade:
raw query escapes, a model-less query builder, transactions with
savepoints and retry-on-deadlock, connection registry for read replicas
and shards, and a full observability surface that mirrors Laravel 13's
DB::listen / QueryExecuted / query log API.
The Eloquent ORM (use suprnova::eloquent::*) builds on top of this
layer and lives in eloquent.md. When you want a typed
model, go there; when you want a raw query against an unmodeled table
or want to observe every query the framework runs, this is the page.
Configuration
use ;
// In bootstrap.rs
register;
DBinit.await.expect;
DatabaseConfig::from_env reads DATABASE_URL and (optionally) the
pool tunables DB_MAX_CONNECTIONS, DB_MIN_CONNECTIONS,
DB_CONNECT_TIMEOUT, DB_LOGGING. When DATABASE_URL is unset the
config falls back to sqlite://./database.db - convenient for
zero-setup development; production boots refuse the fallback via
validate_for_environment so you can't accidentally ship a SQLite
file in APP_ENV=production.
URL → driver detection:
postgres://user:pass@host/db → DatabaseType::Postgres
postgresql://user:pass@host/db → DatabaseType::Postgres
mysql://user:pass@host/db → DatabaseType::Mysql
mariadb://user:pass@host/db → DatabaseType::Mysql
sqlite://./file.db → DatabaseType::Sqlite
sqlite::memory: → DatabaseType::Sqlite
A mariadb:// URL selects the MySQL driver, because MariaDB speaks the
MySQL protocol. Wherever a URL reaches the driver, the framework hands it
over with the mariadb scheme written as mysql: the primary connection,
each named connection and read replica, the migrator, and
MariaDbVectorDriver::from_url. Only the scheme changes. DB::driver_title()
answers MariaDB for a mariadb:// URL and MySQL for a mysql:// one,
so a MariaDB server reached through mysql:// reports as MySQL.
Character set and collation
On a mysql:// or mariadb:// URL, DatabaseConfig carries a character
set and a collation, as Laravel's mysql connection does: DB_CHARSET,
utf8mb4 by default, and DB_COLLATION, utf8mb4_unicode_ci by default.
A charset or collation parameter in the URL wins over the variables,
and the builder's charset and collation win over both. On a Postgres or
SQLite URL both are None.
Every connection the pool opens, the default one and each named one, sends
SET NAMES <charset> COLLATE <collation>, and every table
Schema::create makes takes them as its defaults unless its blueprint
names its own:
use Schema;
create
.await?;
DATABASE_URL=mysql://app:secret@db.internal/shop?collation=utf8mb4_general_ci
A table Suprnova creates then compares and sorts text the way a table Laravel creates beside it does. Only letters, digits and underscores make a name; anything else fails the connection or the migration, since the names reach SQL unquoted.
Tables created before you upgraded keep the server's default
collation, utf8mb4_0900_ai_ci on MySQL 8, and MySQL refuses to compare
string columns of two collations ("Illegal mix of collations"). If your
tables use the server's default, keep it by setting DB_COLLATION to that
collation:
DB_COLLATION=utf8mb4_0900_ai_ci
SHOW TABLE STATUS lists each table's collation.
Pool liveness
A NAT gateway, a load balancer, or a firewall will silently drop a TCP connection that has been idle too long. The pool does not find out. The next query on that connection fails, and it fails on a request that had nothing to do with the outage.
Laravel answers this with libpq's keepalives, keepalives_idle,
keepalives_interval and keepalives_count DSN options, which keep the
socket warm. Those are not reachable from Suprnova. sqlx 0.9 parses
only sslmode, application_name, options and the statement-cache
size out of a Postgres URL, and carries no TCP keepalive setter at any
layer, so there is nowhere to forward them to.
What Suprnova gives you instead is the pool-side answer: stop trusting old connections.
# Close a connection that has been idle for two minutes.
DB_IDLE_TIMEOUT=120
# Recycle every connection after fifteen minutes regardless.
DB_MAX_LIFETIME=900
# Ping a connection before handing it out, but only once it has been
# idle for thirty seconds. Hot connections skip the round trip.
DB_PING_AFTER_IDLE=30
Or programmatically:
register;
Every knob is unset by default, which means the pool keeps sqlx's own
defaults: connections close after 600 idle seconds, recycle after 1800
seconds, and are pinged before every checkout. Set
DB_IDLE_TIMEOUT=0 or DB_MAX_LIFETIME=0 to turn off that form of
reaping entirely.
DB_PING_AFTER_IDLE and DB_TEST_BEFORE_ACQUIRE are alternatives, not
a pair: setting a threshold turns the per-checkout ping off, because
running both would ping on every acquire and make the threshold
meaningless.
Why Suprnova diverges
Keepalives and pool recycling solve the same failure from opposite ends. Keepalives keep a middlebox from expiring the connection; recycling accepts that it will and makes sure the pool never hands out a connection old enough to have been expired. The second is what the driver stack exposes, and it also covers failures keepalives do not - a failed-over replica, a rotated credential, a server-side idle disconnect. If you need the libpq options specifically, that is a change to sqlx, not to Suprnova.
Raw queries
The DB facade ships the full Laravel 13 raw escape surface. Every
helper goes through the same instrumented executor - every call fires
QueryExecuted (see Observability).
Bindings are sea_orm::Value - one of the few sea_orm types the
framework intentionally does NOT re-mask, because every value that hits
the wire goes through it. Value::from(...) works for every primitive
the database understands.
use DB;
use Value;
// SELECT - all rows as DynamicRow.
let users = DBselect.await?;
// SELECT - first row only.
let alice = DBselect_one.await?;
// SELECT - first column of first row as a typed value.
let count: i64 = DBscalar.await?;
// INSERT - returns bool (true when at least one row was affected).
DBinsert.await?;
// UPDATE / DELETE - return the rows-affected count.
let updated = DBupdate.await?;
let deleted = DBdelete.await?;
// Any prepared statement with bindings.
DBstatement.await?;
// DDL with no bindings - `unprepared` mirrors Laravel's
// `DB::unprepared` for statements (CREATE INDEX, ALTER TABLE, VACUUM)
// that reject placeholder binding.
DBunprepared.await?;
// affecting_statement is the explicit form used by update/delete
// internally - drop to it directly for ops that don't fit either name
// (e.g. INSERT...ON CONFLICT DO UPDATE).
let affected = DBaffecting_statement.await?;
Placeholder syntax
? for SQLite + MySQL. $1, $2, ... for Postgres. The active
backend is auto-detected from DatabaseConfig::url.
DynamicRow
Untyped rows materialise as DynamicRow - a serde_json::Map newtype
with typed accessors:
for row in users
get_* errors when the column is absent OR null. get_optional_*
errors only when absent and returns Ok(None) for SQL NULL. The full
accessor list is get_int / get_string / get_bool / get_float /
get_value / get_as<T> plus get_optional_string /
get_optional_int; for nullable types without a dedicated
get_optional_* reach for get_value + a serde_json::Value match,
or get_as::<Option<T>>.
Model-less query builder - DB::table
For ad-hoc queries against tables you haven't bothered to model with
#[suprnova::model], DB::table(...) returns a chainable builder
shaped like the Eloquent Builder<M> but materialising rows as
DynamicRow:
use ;
let rows = DBtable
.select
.filter
.filter_op
.order_by_desc
.limit
.get
.await?;
let first = DBtable
.filter
.first
.await?;
let count = DBtable
.filter
.count
.await?;
let id = DBtable
.insert
.await?;
let updated = DBtable
.filter
.update
.await?;
let deleted = DBtable
.filter
.delete
.await?;
Trust boundary on identifiers
Table names, column names, ORDER BY directions, and SQL operators are
interpolated INTO the SQL string verbatim - they are NOT bound as
parameters (SQL doesn't allow placeholder-bound identifiers). Treat
every impl Into<String> argument as a TRUSTED literal:
// Safe - the column name is a constant.
DBtable.filter.get.await?;
// UNSAFE - never splice user input into a column name.
DBtable.filter.get.await?;
Values (the right-hand side of filter / filter_op) ARE bound as
parameters and safe for user input.
The framework enforces a strict allowlist on identifiers
([A-Za-z_][A-Za-z0-9_]* with one optional schema. prefix) and
operators (=, <>, <, <=, >, >=, LIKE, NOT LIKE,
ILIKE, NOT ILIKE, IS, IS NOT). Violations error at the I/O
boundary before the SQL string is rendered.
Full-text search
where_full_text(columns, text) searches one or more text columns for
words, on DB::table and on the Eloquent builder. or_where_full_text
adds the search with OR. The text is a bound parameter, so it can come
from the request:
use DB;
let rows = DBtable
.where_full_text
.get
.await?;
let search = request.query_param.unwrap_or_default;
let posts = query
.filter
.or_where_full_text
.get
.await?;
The Eloquent builder also takes the Rust names filter_full_text and
or_filter_full_text.
Each engine writes its own search:
| Engine | SQL |
|---|---|
| MySQL and MariaDB | MATCH (title, body) AGAINST (? IN NATURAL LANGUAGE MODE) |
| Postgres | (to_tsvector('english', title) || to_tsvector('english', body)) @@ plainto_tsquery('english', ?) |
| SQLite | an error that names SQLite and the call, before any I/O |
MySQL and MariaDB need a FULLTEXT index over exactly the columns you
search, or the query fails. Postgres runs the search without an index, and
answers it from a GIN index over the same to_tsvector expression when
one exists. Blueprint::full_text creates the right index on each engine;
see Full-text indexes.
Search options
The _with methods take a FullTextOptions: the mode, the Postgres
language, and MySQL's query expansion. They are the options array of
Laravel's whereFullText:
use ;
let rows = DBtable
.where_full_text_with
.get
.await?;
Each mode renders on the engine that has it. On the other engine the text is read as natural language, as Laravel does:
| Mode | MySQL and MariaDB | Postgres |
|---|---|---|
FullTextMode::NaturalLanguage (the default) |
IN NATURAL LANGUAGE MODE |
plainto_tsquery |
FullTextMode::Boolean |
IN BOOLEAN MODE |
plainto_tsquery |
FullTextMode::Websearch |
IN NATURAL LANGUAGE MODE |
websearch_to_tsquery |
.language(name)names the Postgres text search configuration, such assimpleorfrench. The default isenglish. Use the language theGINindex was built with, or Postgres can't use the index. MySQL and MariaDB ignore it..expanded()addsWITH QUERY EXPANSIONin natural-language mode on MySQL and MariaDB. Boolean mode and Postgres ignore it.
Why Suprnova diverges
- Laravel replaces a language it doesn't know with
english. Suprnova takes any plain name (letters, digits, and_, with an optionalschema.prefix), so a text search configuration you created works. Anything else is an error before the query runs, because the name is written into the SQL. - An empty column list is an error. Laravel writes
MATCH (), which the database refuses. - Rust has no optional arguments, so the options go to the
_withforms instead of a third argument.
Transactions
Three entry points, each with the QueryExecuted /
TransactionBeginning / TransactionCommitted /
TransactionRolledBack observation hooks wired in.
Closure form
use DB;
DBtransaction.await?;
Commit on Ok(_). Rollback + propagate the error on Err(_).
An Err is not always a rollback. If an
after-commit callback fails, the commit has
already landed and is durable; DB::transaction still returns Err, and the
message reads after-commit callback failed (the transaction itself committed): <the callback's error>. The closure's return value is lost, its
writes are not, and only a deferred dispatch failed. Every registered callback
still runs and the first error is the one you get.
DB::transaction_with_attempts never retries that error, however
deadlock-shaped it reads: re-running a closure whose writes are already durable
would apply them twice.
Operations inside the closure automatically pick up the active
transaction via a tokio::task_local - you do NOT have to thread a
&tx handle through every model call. You can nest DB::transaction:
your inner call opens a SeaORM savepoint on the same connection. An
inner error rolls back that savepoint alone, so you can handle the error
and continue your outer transaction. A successful inner call releases
its savepoint; an outer rollback still undoes its rows.
You read your ambient depth with DB::transaction_level(): 0 outside
a closure transaction, 1 in its outer closure and 2 in a nested closure.
Manual handles do not install an ambient transaction, so they do not
change this depth.
The closure form is also the only form that can defer work to the commit. A
job whose type declares Job::after_commit() (or a dispatch made with
Queue::push_after_commit) waits inside this closure and only reaches the
queue driver once the commit succeeds; a rollback discards it. See
After-commit dispatch.
For typed aggregate or custom SQL that must execute on the same pinned connection, use the transaction handle directly:
use ;
DBtransaction.await?;
query_all emits normal QueryExecuted observations and returns typed
SeaORM QueryResult rows. Use bound Statement::from_sql_and_values for
dynamic values; do not interpolate untrusted input.
Retry on deadlock
DBtransaction_with_attempts.await?;
Manual form
use ;
let tx = DBbegin_transaction.await?;
// Per-model: the `*_with_tx` shims pin one CRUD op to the manual tx.
create_with_tx.await?;
create_with_tx.await?;
// Per-query: `Builder::with_tx(&tx)` pins a builder chain.
let stale = query
.filter
.with_tx
.get
.await?;
if some_condition else
Manual mode does NOT install the task-local - every operation that
should run inside the transaction has to opt in, either via
Builder::with_tx(&tx) on a chained query or one of the
Model::*_with_tx shims (create_with_tx, save_with_tx,
delete_with_tx, etc.). Operations that forget to opt in run against
the global pool and are NOT part of the transaction.
Holding a Transaction handle pins one pool connection for its
lifetime; pre-load any rows you need to read BEFORE the
begin_transaction() call, especially on SQLite (single shared
connection).
The same holds for after-commit work. Code that doesn't name the handle runs
outside the manual transaction, so DB::after_commit and an
after-commit job pushed that way run at
once. Register on the handle instead, as described in
After-commit callbacks.
Savepoints
DBtransaction.await?;
All three first-class backends support SAVEPOINT / ROLLBACK TO SAVEPOINT - SQLite included.
A savepoint rollback also unwinds the
after-commit registry. A queue push deferred
to the commit inside the savepoint is discarded along with the rows it
described, and the compensation registered with it runs immediately, so a
deferred push_unique's dedupe lock goes back and a re-dispatch inside the same
transaction can win it. Anything registered before the savepoint is untouched,
and a savepoint you release or simply never roll back keeps everything
registered inside it.
Repeating a savepoint name is allowed, and the registry follows the database:
ROLLBACK TO SAVEPOINT x unwinds to the most recent x and destroys the
savepoints established after it. A manual transaction's savepoints unwind
the handle's own registry, the one tx.after_commit fills, the same way.
Only Transaction::savepoint marks the registry. A savepoint you create with
raw SQL is invisible to it, so rollback_to rolls those rows back, logs a
warning, and leaves every deferred dispatch registered inside it in place -
discarding one on a guess would be the worse failure. Use
Transaction::savepoint when the deferred dispatches are meant to unwind with
the rows.
After-commit callbacks
DB::after_commit runs a callback once the transaction around it commits,
like Laravel's DB::afterCommit. Use it for work that must wait until the
rows it describes are durable, such as a webhook or a message to another
service:
use ;
DBtransaction.await?;
The callback is an async closure that returns Result<(), FrameworkError>,
the shape the after-commit queue already runs, so it can await database,
cache, or HTTP work. It runs as follows:
- Inside
DB::transaction, it runs after the commit, outside the transaction, in the order the callbacks were registered. It never runs if the transaction rolls back. - With no transaction open, it runs at once, and
DB::after_commitreturns its result. tx.rollback_to(name)drops the callbacks registered since that savepoint, the same unwinding Savepoints describes for a deferred queue push. Callbacks registered before the savepoint, after the rollback, or inside a savepoint you keep still run at the commit.- If it fails after the commit,
DB::transactionreturns theafter-commit callback failed (the transaction itself committed)error that Closure form describes. The other callbacks still run, and the commit stands.
You keep a successful inner transaction's after-commit callbacks until the outermost commit. An inner rollback discards only its callbacks.
Inside a transaction you started with DB::begin_transaction, register on
the handle. That transaction is not ambient: code that doesn't name the
handle runs outside it, and its writes don't join it either. So
DB::after_commit beside it runs at once, while tx.after_commit and
Queue::push_after_commit_with_tx wait for tx.commit():
use ;
let tx = DBbegin_transaction.await?;
let order = create_with_tx.await?;
let id = order.id;
tx.after_commit;
push_after_commit_with_tx.await?;
tx.commit.await?;
A callback or push on the handle follows the rules above against that
handle. It runs after tx.commit() and is discarded by tx.rollback(), by
dropping the handle without committing it, and by tx.rollback_to(name) to
a savepoint taken before it. If one fails after the commit, tx.commit()
returns the same after-commit callback failed (the transaction itself committed) error.
Why Suprnova diverges
In Laravel, DB::afterCommit inside DB::beginTransaction() waits for
DB::commit(), because the connection itself is in the transaction. A
Suprnova manual transaction is a handle that only the calls naming it use,
so the after-commit work that waits for it names it too:
tx.after_commit(callback) rather than DB::after_commit(callback). When
you port DB::beginTransaction() code, change each DB::afterCommit in it
to tx.after_commit: a DB::after_commit there runs at once, before the
commit, even when the transaction then rolls back.
Rollback callbacks
You register compensation with DB::after_rollback(callback).await?.
You use the same async closure returning Result<(), FrameworkError>
as after_commit. Your callback runs when its enclosing transaction
rolls back and never when it commits. An inner rollback runs only that
scope's callbacks; after a successful inner scope, your callback also
runs if the outer transaction later rolls back. With no ambient
transaction, you discard the callback because no rollback occurs.
You can query the still open outer transaction from an inner rollback callback. For an outer rollback, you run outside the rolled back transaction. Callback failures are logged and do not replace the original transaction error.
Before starting a transaction
You register a synchronous guard before opening transactions:
DBbefore_starting_transaction;
You run each listener in registration order before BEGIN, including
begin_transaction, and before each nested savepoint. Returning an
error prevents that transaction from starting and gives the caller the
same error. Your listeners belong to the active test container in tests,
and otherwise to the application.
Observability
Laravel 13's DB::listen / QueryExecuted / query log surface, ported
to Rust through Suprnova's event dispatcher.
DB::listen - direct callback
use ;
// In bootstrap.rs (or a service provider).
DBlisten?;
Listeners run synchronously inside the executor helper. A slow
listener slows the query - keep direct callbacks light. For anything
that can fail, prefer the EventFacade path below; it runs through
dispatch_best_effort and tolerates errors.
EventFacade dispatch path
QueryExecuted is a real suprnova::Event - listen through the
dispatcher to get queued, fakeable, fail-tolerant delivery:
use ;
use Arc;
;
// In bootstrap.rs.
.await;
Listeners on this path:
- Run through
dispatch_best_effort- a failing listener does NOT fail the query. - Are short-circuited when they themselves issue a query (re-entrancy guard).
- Can use
Event::fake()in tests to assert dispatch without actually running listeners.
In-memory query log
DBenable_query_log?;
query.filter.get.await?;
query.count.await?;
let log = DBget_query_log?;
for query in &log
DBflush_query_log?; // drop entries, keep enabled
DBdisable_query_log?; // stop capturing
let still_capturing = DBlogging;
The log is unbounded - every captured query grows it until the
process exits, flush_query_log() runs, or disable_query_log() is
called. Use it for development, not as a long-running production
profiler.
Listeners and the query log in tests
Inside a test container, DB::listen callbacks and the query log
belong to that container. TestDatabase::fresh,
TestDatabase::sqlite_memory, and TestContainer::fake each start
one, so a test that counts queries counts only the queries it runs,
even when cargo test runs other tests in the same process. The
callbacks and the log end with the container.
use TestDatabase;
use DB;
async
A callback that you register outside any test container, as
bootstrap.rs does, receives every query of the process, including
the queries that tests run inside their containers.
Transaction lifecycle events
TransactionBeginning, TransactionCommitted, and
TransactionRolledBack are real suprnova::Event types - listen for
them through EventFacade::listen to drive auditing, distributed
locks, or compensation logic.
.await;
.await;
All three transaction entry points
(DB::transaction / DB::transaction_with_attempts /
DB::begin_transaction + Transaction::commit/rollback) fire the
events. A leaked manual Transaction handle that gets dropped without
explicit commit/rollback emits no event - SeaORM's Drop impl is
synchronous and can't reach the async dispatcher.
Busy database - DB::monitor and db:monitor
DatabaseBusy is a real suprnova::Event. It reports that the database
server has as many connections as you allow, or more. Its fields are
connection_name and connections. Nothing dispatches it by itself.
Two calls dispatch it:
DB::monitor(max)asks the server of every connection the application has, the default one and each named one. It dispatchesDatabaseBusyfor each server withmaxconnections or more, and returns the events it dispatched.maxmust be at least 1.- The
db:monitor --max <n>console command does the same and prints the count for each connection. Without--maxit prints the counts and dispatches nothing.
The server does the counting, from every client of it, so the answer is the same from every process. That lets you run the check from the scheduler, which is a process of its own:
use Schedule;
A listener decides what a busy database means: a page, a metric, a log line.
use Arc;
use ;
;
// In bootstrap.rs.
.await;
A listener that fails does not stop the check. The count comes from the server:
| Engine | Count |
|---|---|
| PostgreSQL | The rows of pg_stat_activity, which include the server's own workers |
| MySQL and MariaDB | threads_connected |
| SQLite | None. SQLite has no server, so it is never busy. |
DB::connection_counts() returns the counts as a Vec<ConnectionCount>,
each with a connection_name and connections: Option<u32>, and
DbConnection::server_connections() returns the count of one connection.
DbConnection::connections_in_use() is a different number: the
connections of this process's own pool that are out of the pool now. It
says nothing about another process.
QueryExecuted payload
to_raw_sql() substitutes the captured bindings into the SQL for
display:
let query = /* captured from a listener */;
println!;
// SELECT * FROM users WHERE id = 42 AND active = true
The substitution is debug-format (not SQL-safe escaping) and is
intended for log output only. Never feed the result back into a query.
When the bindings do not match the placeholders - one is missing, or
one is left over - to_raw_sql() returns the SQL unchanged rather than
a half-substituted statement.
Coverage scope
QueryExecuted fires for every query:
- Every raw helper on
DB(select/select_one/scalar/insert/update/delete/statement/affecting_statement/unprepared). - Every terminal method on
DbTableBuilder(the model-less builder). DB::transaction/DB::begin_transactionBEGIN / COMMIT / ROLLBACK fire transaction events.DbConnection::connectfiresConnectionEstablished.- Every Eloquent read and write:
Builder<M>::get/first/count/ pagination / chunking,Model::find/all, and model CRUD, including theIN (...)queries behind eager loading.
Connection metadata
let name = DBdatabase_name?; // "myapp" for postgres://.../myapp
let driver = DBdriver_name?; // "postgres" | "mysql" | "sqlite"
let title = DBdriver_title?; // "Postgres" | "MySQL" | "MariaDB" | "SQLite"
let version = DBserver_version.await?; // "15.5" | "8.0.36" | "3.42.0"
server_version issues a backend-specific introspection query
(SELECT VERSION() for Postgres + MySQL, SELECT sqlite_version()
for SQLite). Cache the result if you call it often - every call is a
round trip.
Named connections
For read replicas, sharded shards, or per-model warehouse pools:
// In bootstrap.rs
DBregister_named.await?;
DBregister_named.await?;
// Per-query routing:
let rows = query.on.get.await?;
let warehouse_rows = DBtable.on.get.await?;
let raw = DBselect_on.await?;
The __read_replica__ name is well-known: when registered, every
read-shape terminal method auto-routes through it. Writes ignore the
replica and target the primary. Use Builder::on_write_connection
(per query) or #[model(connection = "...")] (per model default) to
opt back to the primary for specific operations.
Reserved names:
__primary__- the default pool. Cannot be registered (it's the return value ofDB::connection()).__read_replica__- well-known read replica. ANY connection registered under this name takes over read routing.
See eloquent.md → Multi-connection routing for the
full precedence chain (builder tx override → ambient tx → builder
on(name) → model default → __read_replica__ → primary).
Testing
TestDatabase builds an in-memory SQLite database, registers it in
the test container so DB::connection() resolves to it, and runs your
migrations:
use TestDatabase;
use crateMigrator;
async
// `test_database!()` is the macro shortcut.
let db = test_database!;
For tests that build their own ad-hoc schema:
let db = sqlite_memory.await.unwrap;
db.execute_unprepared.await.unwrap;
When a TestDatabase is dropped, the test container is cleared and
the connection registry is wiped - no cross-test leakage. Tests that
mutate process-wide state, such as the connection registry, should be
annotated #[serial_test::serial] so they don't collide. DB::listen
callbacks and the query log aren't process-wide inside a test: see
Listeners and the query log in tests.
Next
- Eloquent - the typed
#[suprnova::model]ORM that sits on top of this layer - Migrations -
Migrator,make:migration, and thedb:syncworkflow - Database Testing -
TestDatabase, fixture loading, and serial-test annotations - Events - the dispatcher behind
QueryExecuted/TransactionCommittedlisteners - Configuration - registering
DatabaseConfigalongside the rest of your typed config
Surface index
| Surface | Laravel analogue |
|---|---|
DB::init / DB::init_with / DB::connection / DB::is_connected / DB::get |
DB::connection() |
DB::table(name) → DbTableBuilder |
DB::table($name) |
where_full_text / or_where_full_text / where_full_text_with / or_where_full_text_with / FullTextOptions / FullTextMode |
whereFullText / orWhereFullText |
DB::select / select_one / scalar / insert / update / delete / statement / affecting_statement / unprepared |
DB::select / selectOne / scalar / insert / update / delete / statement / affectingStatement / unprepared |
DB::transaction / transaction_with_attempts / begin_transaction |
DB::transaction($cb, $attempts) / DB::beginTransaction |
DB::transaction_level() / DB::before_starting_transaction(listener) |
transactionLevel / beforeStartingTransaction |
DB::after_rollback(callback) |
afterRollBack |
DB::after_commit(callback) / Transaction::after_commit(callback) |
DB::afterCommit |
Transaction::commit / rollback / savepoint / rollback_to |
DB::commit / rollBack / savepoint helpers |
DB::listen(callback) |
DB::listen |
DB::monitor / connection_counts / DbConnection::server_connections / connections_in_use |
db:monitor |
DB::enable_query_log / disable_query_log / get_query_log / flush_query_log / logging |
DB::enableQueryLog / disableQueryLog / getQueryLog / flushQueryLog / logging |
DB::database_name / driver_name / driver_title / server_version |
getDatabaseName / getDriverName / getDriverTitle / getServerVersion |
DB::register_named / named / select_on / table_on / statement_on / affecting_statement_on |
multi-connection DB::connection($name) |
QueryExecuted / TransactionBeginning / TransactionCommitted / TransactionRolledBack / ConnectionEstablished / DatabaseBusy |
Illuminate\Database\Events\* |
DatabaseConfig::builder() / from_env / validate_for_environment / idle_timeout / max_lifetime / acquire_timeout / test_before_acquire / ping_after_idle / charset / collation |
config/database.php |
TestDatabase::fresh::<M> / sqlite_memory / execute_unprepared / fetch_one / fetch_all |
RefreshDatabase testing trait |
