Get started with SQLite
One crate for the builder, one for the database, one struct, one query. SQLite needs no server, so it is the shortest path from an empty crate to a row you can read back.
Add the crates
besu enables derive and migrate by default: derive gives you #[derive(Table)], and
migrate gives you push, introspect and migrate. besu-sqlite enables sqlx and
runtime-tokio by default; turning sqlx off leaves the dialect and the DDL without a driver.
Define a table
A table is a plain struct. Field names become column names, Rust types become SQL types, and
Option<T> becomes a nullable column.
The derive emits the row type, a users handle whose fields are typed column references, a
UserInsert struct, and a &'static TableDef that migrations diff. The full list is in
schema declaration.
Connect
Sqlx::new("app.db") opens or creates a file with foreign keys enforced and WAL journalling.
Sqlx::connect("sqlite://app.db") parses a connection string, and Sqlx::memory() is the
in-memory shorthand. See connecting to SQLite for pool options.
Create the table
push introspects the database first and applies only what is missing, so it is safe to run
on every boot.
It sends one statement (reformatted here):
That needs the migrate feature. If you would rather write the DDL yourself, run it through
sql!, which counts its placeholders against its arguments at compile time.
Insert
users.insert(..) takes exactly the columns the database cannot fill in: not the generated
id, not the nullable bio, and not a column with a default. The statement is:
.returning(users) adds RETURNING with the whole column list, which is how id comes back.
Columns you leave alone are absent from the statement, so the database applies its default
rather than receiving an explicit value.
Select
The builder produces the SQL you would have written:
Every column name, column type and table membership in that builder was checked before this ran; the SQL is the output, not a description of it.
Check the SQL without a database
A dialect can render a query on its own, which is how query shapes are tested:
Awaiting a query built this way returns an error rather than panicking, so a dry database cannot quietly pretend a write succeeded.
Next steps
- Schema declaration — every
#[besu(...)]attribute. - SQLite column types — what each Rust type round-trips to.
- Indexes and constraints — uniqueness, foreign keys, checks.
- Connecting to SQLite — pools, files and transactions.
- Get started with PostgreSQL or MySQL.
Gotchas
- The handle comes from the table name, not the struct name.
#[besu(table = "users")]generatesusers; a struct calledUserProfilewith no attribute generatesuser_profile. - A table must be a struct with named fields and no generic parameters. Enums, tuples and unit structs are rejected at compile time.
- Row decoding follows declaration order. A hand-written
SELECT *has to list the columns in the same order as the struct, or decoding fails at runtime. pushis idempotent because it diffs first;create_schemais not, and fails if a table already exists.- The
?in generated SQL is SQLite's placeholder spelling. A test that asserts on SQL text is dialect-specific even though the builder is not. - A
Sqlx::memory()database lives on exactly one connection because the driver pins the pool tomax_connections(1); a pool over:memory:would otherwise open several empty databases.
