Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Drizzle RS
A type-safe SQL query builder and ORM for Rust, inspired by Drizzle ORM.
[!WARNING] This project is still evolving. Expect breaking changes.
Contents
- Getting Started
- Feature Flags
- Migrations
- Porting from drizzle-orm (TypeScript)
- Generated Models
- Querying
- Expressions
- Relational Queries
- Transactions
- Prepared Statements
- Seeding
- PostgreSQL
- MySQL
- CLI Reference
- License
Getting Started
1. Install
[]
= { = "0.3", = ["rusqlite"] }
= { = "0.39", = ["bundled"] }
Pick the driver feature that matches the client your application already uses. You create and own the connection; drizzle wraps it.
Install the CLI with the drivers it should connect through:
Individual driver features (rusqlite, postgres-sync, mysql-async, ...)
work too. Without a driver, generate still works, but migrate, push, and
introspect stop with a "No driver available" error.
See Feature Flags for every driver and optional column type.
2. Initialize
This creates drizzle.config.toml. Point it at your schema and database:
= "sqlite"
= "src/schema.rs"
= "./drizzle"
[]
= "./dev.db"
3. Define Your Schema
#
#
#
#
Each table also gets a module of column types named after it: users::Name is the type of Users::name, should you need to name it. The module keeps these types apart from your own, so a User table can have a role: UserRole column.
If you already have a database, run drizzle introspect to reverse-engineer the schema instead of writing it by hand.
4. Connect & Query
#
#
#
#
Drizzle::new builds the schema value itself, and the pattern that
destructures it names its type. When nothing else names it, put the type on
the call: Drizzle::<Schema>::new(conn) (MySQL: Drizzle::<_, Schema>::new(conn)).
Use let (db, ()) = Drizzle::new(conn) to run queries with no schema.
[!NOTE] See
examples/rusqlite.rsfor a full runnable example.
Feature Flags
| Feature | What it enables |
|---|---|
rusqlite, libsql, turso |
SQLite drivers |
d1, durable |
Cloudflare D1 and Durable Object SQLite (wasm32 only) |
postgres-sync, tokio-postgres |
PostgreSQL drivers |
hyperdrive |
Cloudflare Hyperdrive over tokio-postgres (wasm32 only) |
aws-data-api |
AWS Aurora Serverless Data API (PostgreSQL over HTTP) |
mysql-sync, mysql-async |
MySQL drivers (mysql / mysql_async) |
query |
Relational queries (db.query(...)) |
serde |
JSON columns |
uuid, chrono, time, jiff, rust-decimal |
Column types from those crates |
arrayvec, compact-str, bytes, smallvec-types |
Inline and zero-copy string/byte column types |
cidr, geo-types, bit-vec |
PostgreSQL network, geometric, and bit-string types |
math |
SQLite math functions (see Expressions) |
tracing, profiling |
Query spans and puffin profiling scopes |
Migrations
You have two workflows for keeping migration files in sync with your schema. Pick one — both produce the same committed SQL; the difference is whether you regenerate by hand or let cargo do it.
| Workflow | Generate migrations | Best for |
|---|---|---|
| Manual | Run drizzle generate yourself |
Teams that want explicit control over when migrations are produced |
| Automatic | Regenerated when watched schema/config inputs change during cargo build |
Solo dev or small teams who want schema and migrations to stay in lockstep |
Both workflows apply migrations the same way — either with the CLI at deploy time, or from your app at startup. For local iteration without committed files at all, see Push (Dev Only).
Manual: Generate with the CLI
Run drizzle generate whenever you change your schema, then commit the resulting SQL files:
After a git merge brings in migrations generated on another branch,
generate (like drizzle-kit) diffs against the combined result of both
branches and records both as the new migration's parents (prevIds). If the
branches changed the same objects it stops with a conflict report; regenerate
one branch's migration on top of the other, or pass --ignore-conflicts to
diff against the newest migration only.
If a change could be a rename, drizzle generate asks (or, without a terminal, reads --hints); see Renames.
Automatic: Generate from build.rs
Add drizzle-migrations as a build dependency, then point it at your existing drizzle.config.toml. Migration files regenerate themselves whenever your schema changes — you commit them the same way as the manual workflow, you just never run drizzle generate by hand.
[]
= { = "0.3", = ["rusqlite"] }
= "0.3"
= { = "0.39", = ["bundled"] }
use ;
cfg.watch() tells cargo to rerun build.rs whenever a schema file, drizzle.config.toml, or a referenced env var changes.
Applying Migrations
Once migration files exist, apply them one of three ways. They all use the same SQL files and tracking table — pick whichever fits your environment.
At deploy time, with the CLI:
At app startup, from your code:
use drizzle::migrations::Tracking;
let migrations = drizzle::include_migrations!("./drizzle");
db.migrate(&migrations, Tracking::SQLITE)?;
Use Tracking::POSTGRES for PostgreSQL and Tracking::MYSQL for MySQL. All
three record applied migrations in a __drizzle_migrations table, which
PostgreSQL keeps in a drizzle schema. Override the tracking table or schema
when you need to:
db.migrate(
&migrations,
Tracking::POSTGRES
.schema("ops")
.table("schema_migrations"),
)?;
[!NOTE] PostgreSQL transaction scope differs from drizzle-orm. drizzle-orm's PostgreSQL
migrateruns every pending migration inside one transaction, so a failure leaves none of them applied. drizzle-rs's PostgreSQLdb.migrate(postgres-sync,tokio-postgres) commits each migration in its own transaction together with its tracking row: when a later migration fails, the earlier ones from the same call stay applied, and the next call resumes at the failed one.drizzle migrateon PostgreSQL keeps drizzle-orm's single transaction.CREATE/DROP INDEX CONCURRENTLYcannot run in a transaction at all:db.migrateruns a migration containing it statement by statement behind a dirty marker (anddrizzle migratedoes that for every migration in such a run); finish an interrupted one withdrizzle migrate --repairormigrate_with_repair. On SQLite, rusqlite'sdb.migratealso uses one transaction for the batch.
Migration files are split the way drizzle-orm splits them: a file with
--> statement-breakpoint markers runs one chunk at a time, each chunk
exactly as written (PostgreSQL and MySQL accept several statements in one
chunk; SQLite takes one statement per chunk). A hand-written file without
markers is split on top-level semicolons, honoring the dialect's quoting,
comment, and stored-program syntax.
MySQL DDL implicitly commits, so MySQL migrations do not pretend to be transactional. The CLI takes a database-scoped advisory lock, writes a durable dirty marker before the first statement, and marks the migration complete only after every statement succeeds. If a migration fails or is interrupted, inspect the partially applied DDL before retrying; automatic MySQL repair is deliberately unsupported because the server may already have committed some statements.
During cargo build, by extending the build.rs from above. Set DRIZZLE_MIGRATE=1 in your dev environment and your local database stays in lockstep with the schema:
use drizzle::sqlite::rusqlite::Drizzle;
use drizzle_migrations::{MigrateOutcome, MigrationDir};
// `cfg.watch()` does not watch this flag; without this line, cargo would not
// rerun build.rs when you set or unset it.
println!("cargo:rerun-if-env-changed=DRIZZLE_MIGRATE");
if std::env::var("DRIZZLE_MIGRATE").is_ok() {
let conn = rusqlite::Connection::open(cfg.url()?)?;
let (db, ()) = Drizzle::new(conn);
let migrations = MigrationDir::new(cfg.out_dir()).discover()?;
if let MigrateOutcome::Applied { tags } = db.migrate(&migrations, cfg.tracking())? {
println!("cargo:warning=applied {} migration(s)", tags.len());
}
}
cfg.tracking() returns the same Tracking value the runtime path uses — just sourced from drizzle.config.toml instead of hardcoded.
migrate creates the tracking schema/table if needed and skips migrations that have already been applied. Without DRIZZLE_MIGRATE, cargo build only generates files and never touches the database.
Push (Dev Only)
#
#
#
#
push skips migration files entirely and applies the live schema diff directly.
[!CAUTION]
pushis for local iteration only. It bypasses the migration tracking table and offers no audit trail. Never run it against a production database.
Porting from drizzle-orm (TypeScript)
drizzle-kit records your TypeScript schema in every migration's snapshot, so
drizzle import writes the Rust schema from those snapshots. It reads no
TypeScript. Point it at the folder drizzle-kit writes to (out in
drizzle.config.ts):
It takes either migration layout drizzle-kit writes (meta/_journal.json from
drizzle-kit 0.x, or one folder per migration from 1.x) and uses the newest
snapshot. The dialect comes from the snapshot; --dialect turso imports a
SQLite snapshot for Turso. The schema goes to --out, else the config's
schema, else src/schema.rs, and an existing file is only replaced with
--force. Field names are snake_case unless --casing camel (or
[introspect] casing in the config) says otherwise; the SQL names are kept
with name = "..." wherever they differ. The command prints what it could not
express and the steps below.
Keep the Migration History
drizzle-rs uses drizzle-kit's migration folders and tracking table, so the migrations you already ran stay applied:
-
Point
outindrizzle.config.tomlat the existing folder (or pass--init-configtodrizzle importto write a starter config that does):= "postgresql" = "src/schema.rs" = "./drizzle" -
If the folder has
meta/_journal.json(drizzle-kit 0.x), convert it to the folder layout withdrizzle up(or pass--upgradetodrizzle import). This moves the files in place; the SQL is not changed. -
drizzle migratereads the__drizzle_migrationstable drizzle-orm wrote (in thedrizzleschema on PostgreSQL) and runs only migrations that are not recorded there. Ifdrizzle.config.tssetmigrations.tableormigrations.schema, set the same under[migrations]. -
drizzle generateshould now report no changes. From here on, edit the Rust schema and generate migrations as usual.
What to Port by Hand
Snapshots describe the database, not the TypeScript around it:
relations(): drizzle-rs derives relations from foreign keys, including one-to-one and many-to-many, and names them (see Relation Names). Anauthor_idcolumn with#[column(references = Users::id)]givesposts.author()andusers.author_posts()in the relational query API.$type<T>()and column modes ({ mode: 'json' | 'timestamp' | 'bigint' }): fields get the column's storage type. Switch to your Rust type, for example#[column(json)]with aserdetype, or an enum derivingSQLiteEnum.$default(),$defaultFn(),$onUpdate(): values computed in JavaScript. Use#[column(default_fn = path)]or set them in code.customType(): the column keeps its SQL type; give it a Rust type that implements the dialect's column trait (DrizzlePostgresColumnand friends).- PostgreSQL sequences,
numericandintervalcolumns, and index ordering (.desc(),.nullsFirst()) have no Rust schema equivalent yet; the import warns about each one, anddrizzle generatewould drop or change them.
Schema Cheat Sheet
| drizzle-orm | drizzle-rs |
|---|---|
sqliteTable('users', {...}), pgTable(...), mysqlTable(...) |
#[SQLiteTable], #[PostgresTable], #[MySQLTable] on a struct; name = "..." when the table is not the struct name in snake_case |
pgSchema('auth').table(...) |
#[PostgresTable(schema = "auth")] |
text('created_at') with a different key |
a field plus #[column(name = "created_at")] |
.notNull() / nullable |
a plain field / Option<T> |
.primaryKey() |
#[column(primary)] |
primaryKey({ columns: [t.a, t.b], name }) |
#[column(primary)] on each field; on PostgreSQL #[PostgresTable(primary_key(name = "..."))] |
integer().primaryKey({ autoIncrement: true }) |
#[column(primary, autoincrement)] (SQLite), #[column(primary, auto_increment)] (MySQL) |
serial(), bigserial() |
#[column(serial)] on i32, #[column(bigserial)] on i64 (PostgreSQL); #[column(serial)] on u64 (MySQL) |
.generatedAlwaysAsIdentity({ startWith: 100 }) |
#[column(identity(always, start = 100))], identity(by_default) |
.default('member'), .defaultNow(), .default(sql`...`) |
#[column(default = "member")], #[column(default = now())], #[column(default = expression)] |
.unique() |
#[column(unique)] |
unique('name').on(t.a, t.b) |
#[...Table(unique(columns(a, b), name = "name"))] |
.references(() => users.id, { onDelete: 'cascade' }) |
#[column(references = Users::id, on_delete = CASCADE)]; on PostgreSQL fk_name = "..." keeps a constraint name |
foreignKey({ columns, foreignColumns, name }) |
#[...Table(foreign_key(columns(a, b), references(Parent, x, y), on_delete = "CASCADE"))]; name = "..." on PostgreSQL and MySQL |
index('name').on(t.a), uniqueIndex(...) |
#[SQLiteIndex] pub struct UsersEmailIdx(Users::email); and #[PostgresIndex(unique)], #[MySQLIndex(unique)]; where = "..." for partial indexes |
check('name', sql`...`) |
#[...Table(check(name = "name", expr = "..."))] or #[column(check = "...")] |
.generatedAlwaysAs(sql`...`) |
#[column(generated(stored, "expr"))] or generated(virtual, "expr") |
pgEnum('role', ['admin', 'member']) |
#[derive(PostgresEnum)] on an enum whose name and variants are the SQL type and values, and #[column(enum)] on the field; #[postgres_enum(schema = "auth")] for another schema |
mysqlEnum('role', [...]) |
#[derive(MySQLEnum)] and #[column(ENUM)] |
text({ enum: [...] }) (SQLite) |
#[derive(SQLiteEnum)] and #[column(enum)], or keep a String |
json(), jsonb() |
#[column(json)] / #[column(jsonb)] with a serde type (serde_json::Value maps to JSONB) |
pgView('v').as(...), sqliteView, mysqlView |
#[PostgresView(definition = "...")] (materialized for pgMaterializedView), #[SQLiteView(...)], #[MySQLView(...)] on a struct with the view's columns |
relations(...) |
not needed; see above |
drizzle(client, { schema }) |
#[derive(SQLiteSchema)] / PostgresSchema / MySQLSchema on a struct listing the tables, enums, indexes and views |
Generated Models
Given the schema above, each #[SQLiteTable], #[PostgresTable], or
#[MySQLTable] generates four helper types:
| Model | Purpose | Fields |
|---|---|---|
SelectUsers |
Full-row query results | One field per column, with the declared type |
InsertUsers |
Insert rows | new(name, age) requires non-default fields; with_email(...) for optional ones |
UpdateUsers |
Update rows | default() starts empty; with_age(27) sets fields to update |
PartialSelectUsers |
Partial-column query results | All fields Option<T>; populated by db.query(users).columns(...) (see Relational Queries) |
Insert
new() takes only the required fields (columns without a default or autoincrement). Chain with_* for optional fields:
#
#
#
#
Update
Start from default() and set only the fields you want to change. The query won't compile unless at least one field is set:
#
#
#
#
JSON Columns
With the serde feature, any Serialize + Deserialize type can be stored in a
JSON column (json; json or jsonb on PostgreSQL; JSON on MySQL). The
field keeps its own type in the generated models, and one payload type can back
columns in several tables. The macro implements nothing on the payload type,
and your crate does not need a serde_json dependency. Generated models
implement Debug, Clone, PartialEq and Default whenever every field type
does, so a payload type only needs the traits you actually use.
#
#
#
#
Selecting a JSON column on its own (db.select(profiles.settings)) yields
Json<Settings>; use .0 or .into_inner() to get the payload.
Querying
Comparison and expression functions such as eq, gt, and, and count live
in drizzle::core::expr. Ordering helpers such as asc and desc live in
drizzle::core.
Select
#
#
#
#
Combining Conditions
A tuple of conditions is a condition, so lists stay flat instead of nesting
and(a, and(b, c)). all and any combine the same lists explicitly.
#
#
#
#
Elements may be Options — None contributes nothing, which makes dynamic
filters composable:
#
#
#
#
When every element is absent the list renders as its operator's identity:
TRUE for a tuple or all (matches everything, like an absent WHERE) and
FALSE for any (matches nothing, so a fully-optional any fails closed).
Bare tuples hold up to 8 conditions; all/any and nesting cover longer lists.
Ordering, Limiting, Pagination
#
#
#
#
Group By
#
#
#
#
Insert
#
#
#
#
[!IMPORTANT] In a multi-row insert, every row must set the same set of optional fields. Mixing
with_email(...)on some rows but not others is a compile error.
Update
#
#
#
#
Delete
#
#
#
#
A delete or update without .r#where(...) does not compile, so a forgotten condition cannot empty or rewrite a table. To change every row on purpose, write .r#where(true).
Joins
Use #[derive(SQLiteFromRow)] to map columns from multiple tables into a flat struct. #[from(Users)] sets the default source table for unannotated fields:
#
#
#
#
Subqueries & Set Operations
SELECT builders are expressions — pass them directly into comparisons or IN:
#
#
#
#
Combine queries with union, union_all, intersect, and except. union removes duplicates; union_all keeps them:
#
#
#
#
Aliases
Use a Tag to alias a table for self-joins:
#
#
#
#
Expressions
Aggregate functions and common SQL expressions:
#
#
#
#
Available in drizzle::core::expr:
- Comparisons —
eq,neq,gt,gte,lt,lte - Boolean —
and,or,not,all,any(and tuples, which mean AND) - Aggregates —
count,sum,avg,min,max - Null handling —
coalesce,is_null,is_not_null - Strings —
upper,lower,length - Math —
abs,round,sign,mod_;ceil,floor,trunc,sqrt,power,exp,ln,log,log10,log2,pi(see the SQLite note below)
The ordering helpers asc and desc are in drizzle::core, not
drizzle::core::expr.
SQLite only has ceil through pi when it is compiled with
SQLITE_ENABLE_MATH_FUNCTIONS, so on SQLite those functions compile only with
drizzle's math feature. Enabling math is a promise about the SQLite you link:
- rusqlite (with
bundled) and libsql compile their own SQLite and readLIBSQLITE3_FLAGSwhile doing so. SetLIBSQLITE3_FLAGS="-DSQLITE_ENABLE_MATH_FUNCTIONS"in the build environment, for example under[env]in.cargo/config.toml. - turso implements these functions itself and needs no flag.
If the linked SQLite lacks them, the calls still compile under math, and the
query fails at runtime with no such function.
Type Casting
cast(expr, target) takes a type marker from the dialect's types module
(drizzle::sqlite::types, drizzle::postgres::types, or
drizzle::mysql::types). The marker supplies both the SQL type name and the
result type. You can pass a SQL type name as a string instead, but a string
carries no result type, so name the marker with a turbofish:
cast::<_, _, Real>(expr, "DOUBLE"). SQLite and PostgreSQL only allow casts
between compatible types, such as integer to real.
#
#
#
#
Relational Queries
Requires the query feature. Fetches a table with its relations in a single query — no manual joins.
Relation methods are generated from foreign keys. Given Posts.author_id → Users.id, posts.author() is the forward (many-to-one) and users.author_posts() the reverse (one-to-many). Relation Names explains how the names are chosen.
#
#
#
#
.find_first() returns Option<...>:
#
#
#
#
Nest relations:
#
#
#
#
Filter and paginate the root query:
#
#
#
#
Relation Names
Every foreign key gives relation accessors named from the schema, so you rarely name one yourself:
| Relation | Example | Loads | Named after |
|---|---|---|---|
| Forward, on the table with the key | posts.author() |
the row, or an Option when the key is nullable |
the column without _id (author_id gives author) |
| Reverse, on the referenced table | users.author_posts() |
a Vec |
the plural of the struct, after the column's role |
| One-to-one reverse, when the key alone is unique | users.profile() |
an Option |
the singular of the struct |
| Many-to-many, through a link table | posts.tags() |
a Vec |
the plural of the other column, plus the link's own name |
The struct name counts, not the SQL table name.
Roles. A column named after the table it references (user_id to
Users, or to AppUsers) plays no role, so Posts.user_id gives
users.posts(). Any other name is a role, and the reverse accessor starts
with it: author_id and editor_id give users.author_posts() and
users.editor_posts(), and a self-reference parent_id gives
categories.parent_categories(). A reverse name depends only on its own
column, so adding a foreign key never renames another accessor.
Link tables. A table is a link when it has exactly two foreign keys and
its rows are that pair: the pair is its primary key or a
UNIQUE(columns(...)) constraint, or the table has no other column except a
single-column primary key. Neither key may be unique on its own, which would
make it a one-to-one. A table with two foreign keys beside columns of its
own, such as a comment with an author and a post, is an entity and gets no
many-to-many pair, so its rows are never loaded twice.
Each side of a link gets an accessor to the other, named after the other
column. Both keys may reference one table: Fans with fan_id and idol_id
gives users.idols() and users.fans(). A link named only after what it
links (PostTags, UsersToGroups, or GroupMembers with a member_id
column) gives plain names, posts.tags() and tags.posts(). A link with a
name of its own adds it, so two links between the same tables never clash:
PostLikes and PostBookmarks give users.posts_via_likes() and
users.posts_via_bookmarks(). The link's rows stay available as
users.post_likes() and post_likes.post().
Composite keys. A table-level foreign_key(columns(...), references(...))
gives the same relations. Its forward accessor is the singular of the
referenced struct.
Naming by hand. relation = "..." names the reverse accessor and
many_to_many = "..." names a link side's many-to-many accessor; both also
work inside foreign_key(...), and many_to_many makes any table with two
foreign keys a link. You need them only to choose another name, or when two
tables still give a third the same accessor, as a direct key and a plain link
between the same tables can (Posts.tag_id and PostTags both give
tags.posts()). rustc then reports the duplicate at both columns.
#
#
#
#
Selecting Specific Columns
.columns(...) / .omit(...) return PartialSelectUsers — same shape as SelectUsers, but every field is Option<T>:
#
#
#
#
Result Types
.with(users.author_posts()) returns UsersWithAuthorPosts — base columns via deref, relation data on fields like user.author_posts:
#
#
#
#
Transactions
[!TIP] Transactions auto-rollback on error or panic. Return
Ok(value)to commit,Err(...)to rollback. No manual cleanup needed.
#
#
#
#
Savepoints nest inside transactions — a failed savepoint rolls back without aborting the outer transaction:
#
#
#
#
Cloudflare D1 is the exception: the platform exposes no transaction handles, so
the D1 driver has no transaction method. Its batch method submits several
statements that D1 applies atomically.
Prepared Statements
[!TIP] Placeholders are typed by the column they came from. Binding the wrong type fails at compile time, not at runtime.
#
#
#
#
Placeholders work in update (and insert) models too:
#
#
#
#
Use .prepare().into_owned() to convert a prepared statement into a self-contained value that can be stored or moved freely.
Seeding
drizzle-seed fills a database with deterministic test data: the same seed gives the same rows. Values follow the column types and names (emails for email, timestamps for created_at, enum variants for enums, ...), foreign keys point at seeded parent rows, and UNIQUE columns get distinct values.
With the schema macros, pass the schema; tables and columns are checked at compile time:
use ;
let schema = new;
for statement in sqlite
.seed
.count
.relation // 3 posts per user
.generator
.generate
Without a Rust schema, drizzle seed reads the tables from the database itself:
From Rust, build the seed schema from a live database (db.introspect()) or a migration's snapshot.json with drizzle_seed::schema::Schema::from_snapshot (the migrations feature), then configure it by name with count_by_name, relation_by_name and generator_by_name. try_generate_script() returns the whole seed as SQL with its values written inline.
PostgreSQL
The query API above works the same way with #[PostgresTable],
#[derive(PostgresSchema)], #[derive(PostgresFromRow)], and
drizzle::postgres::{sync,tokio}::Drizzle. The tokio driver's calls are
async, and the blocking driver runs prepared statements on db.conn_mut().
Two things in the SQLite examples do not carry over: autoincrement (use
serial, bigserial, smallserial, or identity(...) columns) and
TransactionConfig::Deferred, which is a SQLite transaction mode.
PostgreSQL's TransactionConfig::default() uses server defaults. Its
typestated builder keeps DEFERRABLE on the combination where it has meaning:
#
#
#
#
#
#
#
#
MySQL
MySQL uses the same select, insert, update, delete, prepared-statement,
relational-query, transaction, migration, and seed workflows as the other
dialects. Migration generation, push, and apply are available through the
Drizzle CLI, and both adapters expose migrate(&migrations, Tracking::MYSQL).
MySQL applies each statement in autocommit mode because its DDL can commit
implicitly. A failed migration stays marked as interrupted until you reconcile
the partial schema and its tracking row manually. Enable
mysql-sync for the blocking mysql
client or mysql-async for mysql_async.
The driver crates remain explicit dependencies because your application creates
and owns the connection or pool.
[]
= { = "0.3", = ["mysql-sync"] }
= "28"
#
#
#
#
The async adapter accepts either an owned mysql_async::Conn or a lazy pool:
[]
= { = "0.3", = ["mysql-async"] }
= "0.37"
= { = "1", = ["macros", "rt-multi-thread"] }
#
#
# async
#
#
Start the documented MySQL 8.4 container and run the complete adapter matrix or the runnable blocking example:
DRIZZLE_MYSQL_URL overrides the example and test URL. First-class support
targets Oracle MySQL 8.0.31 or newer; CI runs both MySQL 8.0.31 and 8.4. MariaDB
and SingleStore compatibility are not promised. TLS configuration belongs to
the upstream mysql/mysql_async options supplied to Drizzle. The workspace
enables their native-TLS backends, and Drizzle neither disables certificate
validation nor silently changes the caller's transport policy.
Before its first typed query on a connection, the adapter sets the session time
zone to UTC and removes NO_UNSIGNED_SUBTRACTION and REAL_AS_FLOAT from the
session SQL mode. Those invariants keep temporal decoding, unsigned arithmetic,
and REAL columns consistent with the Rust types; using conn_mut() causes
them to be restored before the next Drizzle query.
Transactions use TransactionConfig for isolation level, access mode, and
consistent snapshots. The typestated builder only exposes .snapshot() after
.repeatable_read(). Runtime-derived values can use the enum setters instead.
The blocking adapter takes a closure synchronously; the async connection and
pool adapters expose the same transaction configuration on their async methods.
MySQL upserts use the native
.on_duplicate_key_update(...) builder (or .ignore()), not PostgreSQL's
.on_conflict(...) spelling.
#
#
#
#
Transactions stay scoped to the callback. Returning Ok commits; returning
Err rolls back. Dropping or cancelling an async transaction future also
prevents the active transaction from being reused without rollback.
The type surface deliberately leaves unsupported SQL unavailable:
- MySQL mutations return
MySQLMutationResultmetadata, not SQLRETURNINGrows. - Full joins and partial-index predicates are rejected; MySQL does not support them.
.offset(n)without an explicit limit renders aLIMITofi64::MAXfirst, because MySQL has no standaloneOFFSETsyntax. (MySQL's manual suggests18446744073709551615, but after aUNIONthat value overflows and the query returns no rows.)- String concatenation uses
concat(...); the builder never emits||, whose default MySQL meaning is logical OR.
See examples/mysql.rs for the complete blocking example.
CLI Reference
Most projects only need these:
| Command | Description |
|---|---|
drizzle init |
Create drizzle.config.toml |
drizzle generate |
Diff schema and emit SQL migration files |
drizzle migrate |
Apply pending migrations |
drizzle push |
Apply schema diff directly without migration files |
drizzle introspect |
Reverse-engineer schema from a live database |
drizzle seed |
Fill a live database with deterministic test data |
Other useful commands:
| Command | Description |
|---|---|
drizzle new |
Interactive schema builder |
drizzle status |
List local migration folders and whether each has a snapshot (it does not read the database; use drizzle migrate --plan for that) |
drizzle check |
Validate config |
drizzle export |
Print schema as raw SQL |
drizzle up |
Upgrade migration snapshots to the latest format |
drizzle import <folder> |
Write the Rust schema of a drizzle-orm (TypeScript) project from its drizzle-kit snapshots (see Porting from drizzle-orm) |
drizzle pull is an alias for introspect. Commands that read the config accept -c <path> for a custom config file and --db <name> for multi-database configs.
Renames
When a change drops one table, column, view, or PostgreSQL schema, enum, index, or constraint (unique, check, primary key, foreign key) and adds another of the same kind (in the same schema, and for columns, indexes, and constraints the same table), the diff cannot tell a rename from a drop plus a create. drizzle generate and drizzle push never guess; they ask, the way drizzle-kit does.
With a terminal, they ask once per new entity that has dropped candidates, schemas first, then enums, tables, columns, unique constraints, checks, indexes, primary keys, foreign keys, and views:
? Is accounts table created or renamed from another table?
> + accounts create table
~ users › accounts rename table
~ people › accounts rename table
Picking a rename uses up that candidate, and column questions use the tables' new names. push --force does not skip these questions; it only approves data loss.
A constraint or index whose name was derived rather than written out keeps the name it has in the database when only that derived name changes, for example users_pkey after users becomes accounts. As in drizzle-kit, nothing is asked or planned for it, and the new snapshot records the kept name.
Without a terminal (CI, scripts), answer with drizzle-kit's hints, inline or from a file:
rename turns a drop plus a create into a rename; create keeps them separate. Identifiers are [name] for schemas, [schema, name] for tables, views, and enums, and [schema, table, name] for columns, indexes, and constraints (unique, check, primary_key, foreign key). SQLite and MySQL use public as the schema, as drizzle-kit does. A column's table is its new name. Hints that match nothing in the current diff are ignored, so one file can be reused. If a schema, enum, table, or column question has no hint, the command changes nothing: it prints each unresolved decision with the hints that would answer it and exits with code 2. An unhinted index, constraint, or view is dropped and created, which loses no data.
The Rust API never guesses either. drizzle_migrations::diff and diff_with, build::run, and the drivers' db.push stop with MigrationError::UnansweredRenames when a schema, enum, table, or column may have been renamed, and the error gives the hint for each answer:
cannot tell a rename from a drop plus a create, and guessing wrong loses data. Answer each question with a hint on `RenameHints` or `DiffOptions` (the `drizzle` CLI asks them interactively):
column `users.full_name`: created, or renamed from `name`?
renamed from `name`: .rename_column("users", "name", "full_name")
created: .create(CreateHint::new(RenameKind::Column, "full_name").on_table("users"))
Pass the answer where the diff runs:
use RenameHints;
let renames = new.rename_column;
// build.rs
let cfg = new
.file
.renames;
// at runtime
db.push_with?;
// in memory
diff_with?;
A hint that matches nothing in a later diff is ignored, so it can stay in build.rs. db.push also stops before it drops a table or column that holds rows, applying nothing, where drizzle-kit's push would ask; dropping an empty one goes ahead. An index, constraint, or view without an answer is dropped and created, which loses no data; the CLI does the same without a terminal. drizzle_migrations::rename_questions lists the questions, for tools that ask them their own way.
License
MIT. See LICENSE.