toolu-orm-connection 0.1.2

Connection pooling and driver adapters (libsql, rusqlite, Postgres) for toolu-orm
Documentation

๐Ÿ—„๏ธ toolu-orm

Schema-first Rust ORM โ€” one struct, three databases, migrations you can read.

Define a table once as a Rust struct. Get a typed query builder, a FromRow mapper, and a schema snapshot back. Diff the snapshot into a plain-SQL migration, apply it with a SHA-256-checked journal, and run the same code against libsql (local file, in-memory, or Turso), rusqlite, or Postgres. No runtime reflection, no macro-generated SQL you can't read.

crates.io docs.rs CI Docs License: MIT Rust 1.94 No unwrap

Documentation ยท Why ยท Features ยท How it works ยท Install ยท Quickstart ยท Tables ยท Queries ยท Relations ยท Migrations ยท Drivers ยท Contributing


Why toolu-orm?

Most Rust database layers make you pick a side:

  • Query builders give you type-safe SQL but leave schema evolution to you. The migration folder drifts from the structs, and nobody notices until prod.
  • Full ORMs own the schema but hide the SQL behind a runtime, a DSL, or a code generator you have to re-run and re-learn.

toolu-orm keeps the struct as the single source of truth and generates everything else from it. #[table] produces a TableDef. A SchemaRegistry of those defs is diffed against the last JSON snapshot to write the next NNNN_name.sql migration โ€” plain SQL you can read in review. A journal records each file's SHA-256 so a migration edited after it shipped fails loudly instead of silently diverging.

The same struct also hands you typed Column<T> constants, select() / insert() / update() / delete() builder factories, and an async executor that speaks ?1 to SQLite and $1 to Postgres. Swap the driver by flipping a Cargo feature; the application code does not change.

Extracted from a production backend where it drives Turso embedded replicas in the field and Postgres in the cloud, from one set of table structs.


Features

๐Ÿงฑ Schema as code #[table] turns a struct into a TableDef with primary keys, defaults, foreign keys with on_delete / on_update, strict tables, and #[index] / #[unique_index].
๐Ÿ” Diff-driven migrations run_generate diffs your registry against the last *.snapshot.json and writes numbered SQL with a --> statement-breakpoint separator. run_migrate applies pending files in one transaction each; get_status lists applied and pending.
๐Ÿ” Tamper-evident journal _journal.json stores a sha256: hash per migration. A file that changed after it was recorded stops the run with MigrateError::HashMismatch.
๐Ÿงฎ Typed columns, typed expressions Generated Column<T> constants (users::email) build Expr trees: eq / ne / in_list / not_in / is_null on every column, like on text, gt / lt / gte / lte / between on numbers, combined with .and() / .or(). Table-qualified, always quoted.
๐Ÿ—๏ธ Four builders, one executor SelectBuilder, InsertBuilder (with or_ignore / or_replace), UpdateBuilder (set / set_expr), DeleteBuilder. All share .execute(); select adds fetch_all, fetch_one, fetch_optional, count, exists.
๐ŸŒ Dialect-aware SQL to_sql_for(Dialect::Sqlite) emits ?N placeholders; Dialect::Postgres emits $N, ON CONFLICT ... DO UPDATE SET ... = EXCLUDED, and LEFT JOIN LATERAL + json_agg for relations.
๐Ÿ•ธ๏ธ Relational loads without N+1 #[derive(Relational)] with #[has_many], #[belongs_to], #[many_to_many]; RelationalQuery fetches parent + children as JSON arrays in a single statement per dialect.
๐Ÿงฌ Enums and views #[derive(ColumnEnum)] stores a Rust enum as text; #[view(Name, pick(...))] / omit(...) generates subset structs from a table.
๐Ÿ”„ Transactions `conn.run_transaction(
๐Ÿ”Œ Three drivers, one trait DbConnection over libsql (async, Turso embedded replica with sync retry), rusqlite (sync, wrapped in spawn_blocking), and Postgres (deadpool-postgres pool, rustls TLS).
๐Ÿ›ก๏ธ Panic-free src/ Workspace-wide clippy::unwrap_used, expect_used, panic, indexing_slicing are deny. No #[allow] anywhere.

How it works

Five crates. orm-core is the foundation; every other crate depends on it, and only orm-cli depends on orm-connection.

                 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                 โ”‚      toolu-orm-core     โ”‚  TableDef ยท ColumnType ยท Value ยท Expr
                 โ”‚  schema ยท snapshot ยท    โ”‚  Column<T> ยท Snapshot ยท Journal
                 โ”‚  diff ยท dialect ยท row   โ”‚  Dialect { Sqlite, Postgres }
                 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ–ผ                    โ–ผ                     โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ toolu-orm-    โ”‚   โ”‚ toolu-orm-     โ”‚   โ”‚ toolu-orm-connection โ”‚
โ”‚ macros        โ”‚   โ”‚ query          โ”‚   โ”‚ Database ยท DbConnection
โ”‚ #[table]      โ”‚   โ”‚ Select/Insert/ โ”‚   โ”‚ libsql ยท rusqlite ยท   โ”‚
โ”‚ FromRow       โ”‚   โ”‚ Update/Delete  โ”‚   โ”‚ PgDatabase (pool+TLS) โ”‚
โ”‚ Relational    โ”‚   โ”‚ RelationalQueryโ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚ ColumnEnum    โ”‚   โ”‚ Executor ยท tx  โ”‚              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ–ผ
                                          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                                          โ”‚    toolu-orm-cli      โ”‚
                                          โ”‚ run_generate          โ”‚
                                          โ”‚ run_migrate           โ”‚
                                          โ”‚ get_status            โ”‚
                                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The migration loop in one line:

structs โ”€โ”€#[table]โ”€โ”€โ–ถ TableDef โ”€โ”€SchemaRegistryโ”€โ”€โ–ถ diff vs last snapshot โ”€โ”€โ–ถ NNNN_name.sql + snapshot.json + _journal.json

Install

The toolu-orm facade pulls in the whole stack behind one version and one feature list:

[dependencies]
toolu-orm      = { version = "0.1", features = ["libsql"] }
toolu-orm-core = { version = "0.1", default-features = false, features = ["libsql"] }
tokio          = { version = "1", features = ["rt-multi-thread", "macros"] }

It re-exports toolu_orm::core, toolu_orm::query, toolu_orm::connection and the proc macros. The macros expand to paths that name toolu_orm_core and toolu_orm_query directly, so glob-import the prelude in every module that uses #[table] or a derive:

use toolu_orm::prelude::*;

toolu-orm-core is listed a second time because the prelude cannot cover one case: #[table] also generates the companion column module, and the toolu_orm_core paths inside that nested mod resolve against the crate's extern prelude โ€” which Cargo fills from direct dependencies only, so a use in the parent module never reaches them. Without it the expansion fails with error[E0433]: cannot find module or crate toolu_orm_core. Everything else resolves through the prelude. Emitting facade-relative paths (proc-macro-crate) would remove both the glob and this extra dependency, and is tracked in #15. crates/orm/tests/facade_test.rs does not catch it: that package depends on the four crates directly.

toolu-orm-cli is not re-exported by the facade. Add it as a normal dependency when you generate or apply migrations from your own binary โ€” it is a library crate with no [[bin]] of its own:

toolu-orm-cli = { version = "0.1", default-features = false, features = ["libsql"] }

Keep toolu-orm and toolu-orm-core on the same version: they share one workspace version, and a mismatch means two different toolu_orm_core crates in the graph, whose types do not interoperate.

Depending on the crates directly

Every crate exposes the same driver features (libsql, rusqlite, postgres) and forwards them to toolu-orm-core. Enable the drivers you need on every crate you depend on so Cargo unifies them into one shape.

[dependencies]
toolu-orm-core       = { version = "0.1", default-features = false, features = ["libsql"] }
toolu-orm-macros     = { version = "0.1", features = ["libsql"] }
toolu-orm-query      = { version = "0.1", features = ["libsql"] }
toolu-orm-connection = { version = "0.1", features = ["libsql"] }
toolu-orm-cli        = { version = "0.1", default-features = false, features = ["libsql"] }
tokio                = { version = "1", features = ["rt-multi-thread", "macros"] }

For Postgres, replace "libsql" with "postgres". toolu-orm-core and toolu-orm-cli default to libsql; the other crates have no default driver.

Heads-up on #[derive(FromRow)]. The derive currently emits the postgres + libsql decoder shape, so it compiles only when toolu-orm-core has both features on. With a single driver, implement FromRow by hand (a few lines, see Defining tables). Making the derive follow the active driver set is tracked as a follow-up.


Quickstart

Define a table, generate and apply a migration, insert, and read back โ€” against an in-memory libsql database.

[dependencies]
toolu-orm      = { version = "0.1", features = ["libsql"] }
toolu-orm-core = { version = "0.1", default-features = false, features = ["libsql"] }
toolu-orm-cli  = { version = "0.1", default-features = false, features = ["libsql"] }
tokio          = { version = "1", features = ["rt-multi-thread", "macros"] }

toolu-orm-cli is a plain library crate despite the name โ€” it ships no binary, so run_generate, run_migrate and get_status are called from your own code (see Migrations for the usual bin/migrate.rs).

use toolu_orm::connection::Database;
use toolu_orm::core::column::{Integer, Text};
use toolu_orm::core::dialect::Dialect;
use toolu_orm::core::error::DbCoreError;
use toolu_orm::core::query_column::CommonOps;
use toolu_orm::core::row::FromRow;
use toolu_orm::core::schema::SchemaRegistry;
use toolu_orm::core::table::TableSchema;
use toolu_orm::prelude::*;                   // required: the macros expand to
                                             // toolu_orm_core / toolu_orm_query paths

#[table(name = "users")]
pub struct UsersTable {
  #[column(primary_key)]
  pub id: Text,
  #[column(not_null)]
  pub email: Text,
  #[column(not_null, default = "unixepoch()")]
  pub created_at: Integer,
}

pub struct User {
  pub id: String,
  pub email: String,
  pub created_at: i64,
}

// One driver is active (libsql), so `FromRow` asks for a single `from_row` โ€”
// `#[derive(FromRow)]` emits the postgres+libsql shape and does NOT compile
// here. See "Row mapping" for the derive and the other driver shapes.
impl FromRow for User {
  const REQUIRED_COLUMNS: &'static [&'static str] = &["id", "email", "created_at"];

  fn from_row(row: &libsql::Row) -> Result<Self, DbCoreError> {
    let col = |i: i32, e: libsql::Error| DbCoreError::RowMapping(format!("col {i}: {e}"));
    Ok(Self {
      id: row.get(0).map_err(|e| col(0, e))?,
      email: row.get(1).map_err(|e| col(1, e))?,
      created_at: row.get(2).map_err(|e| col(2, e))?,
    })
  }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
  // 1. Schema โ†’ migration file (writes migrations/0001_init.sql + snapshot + journal)
  std::fs::create_dir_all("migrations")?;
  let registry = SchemaRegistry::from_tables(vec![UsersTable::table_def()]);
  toolu_orm_cli::generate::run_generate(&registry, "migrations", "init", Dialect::Sqlite)?;

  // 2. Connect and apply whatever is pending
  let db = Database::init_local(":memory:").await?;
  let conn = db.connect()?;
  let applied = toolu_orm_cli::migrate::run_migrate(&conn, "migrations", Dialect::Sqlite).await?;
  println!("applied {applied} migration(s)");

  // 3. Typed writes and reads through the generated builders.
  //    The builders run on the driver connection, which the wrapper exposes.
  let exec = conn.inner_conn();   // &libsql::Connection โ€” borrows `conn`, so
                                  // keep `conn` alive for as long as `exec` is used

  UsersTable::insert()
    .set(&users::id, "u_1")
    .set(&users::email, "ada@example.com")
    .execute(exec)
    .await?;

  let found: Vec<User> = UsersTable::select_for::<User>()
    .filter(users::email.eq("ada@example.com"))
    .fetch_all(exec)
    .await?;
  println!("{} user(s)", found.len());
  Ok(())
}

#[table] generated everything used above: the users companion module with one Column<T> per field, UsersTable::table_def(), and the select() / select_for::<T>() / insert() / update() / delete() factories.

Two connection surfaces show up there. DbConnection โ€” what db.connect() returns โ€” is what run_migrate and get_status take. Executor is what the query builders run on, and it is implemented for the driver's own connection type (libsql::Connection, rusqlite::Connection, tokio_postgres::Client, PgTransaction), which conn.inner_conn() hands out.


Defining tables

use toolu_orm_macros::{table, ColumnEnum};

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, ColumnEnum)]
#[serde(rename_all = "snake_case")]
pub enum RunStatus { Pending, Running, Success, Failed }

#[table(name = "pipeline_runs", strict = true)]
#[index("idx_runs_pipeline", pipeline_id)]
#[index("idx_runs_status", status)]
pub struct PipelineRun {
  #[column(primary_key, default = "uuid4_str()")]
  pub id: Uuid,
  #[column(not_null, references = "pipelines(id)", on_delete = "cascade")]
  pub pipeline_id: Uuid,
  #[column(not_null, default = "'pending'")]
  pub status: RunStatus,
  #[column(not_null, default = "datetime('now')")]
  pub created_at: Timestamp,
}
Attribute Effect
#[table(name = "...", strict = true)] Table name; strict switches column SQL types to the SQLite / Turso STRICT set.
#[column(primary_key)] Primary key.
#[column(not_null)] NOT NULL; omit it for a nullable column.
#[column(default = "...")] Raw SQL default, e.g. "unixepoch()", "'pending'", "uuid4_str()".
#[column(references = "t(col)", on_delete = "cascade", on_update = "...")] Foreign key with referential actions.
#[column(as_text)] Store an enum or custom type as TEXT.
#[index("name", col, ...)] / #[unique_index("name", col)] Secondary indexes on the table; unique_index is how you express uniqueness.
#[view(Name, pick(a, b))] / #[view(Name, omit(c))] Generate a subset struct from the table.

Field types map to ColumnType: Text, Integer, Real, Blob, Uuid, Boolean, Timestamp, Date, Time, Json, plus Postgres-flavoured BigInt, SmallInt, Varchar(n), Serial, BigSerial, Jsonb, Numeric, Char(n), Array.

Row mapping. #[derive(FromRow)] fills REQUIRED_COLUMNS from the field names in declaration order and reads each field positionally at its own index, so select_for::<T>() picks exactly the columns T needs, in the order it decodes them. A hand-written impl is a few lines when you run a single driver:

use toolu_orm_core::{error::DbCoreError, row::FromRow};

impl FromRow for User {
  const REQUIRED_COLUMNS: &'static [&'static str] = &["id", "email", "created_at"];

  fn from_row(row: &libsql::Row) -> Result<Self, DbCoreError> {
    let col = |i: i32, e: libsql::Error| DbCoreError::RowMapping(format!("col {i}: {e}"));
    Ok(Self {
      id: row.get(0).map_err(|e| col(0, e))?,
      email: row.get(1).map_err(|e| col(1, e))?,
      created_at: row.get(2).map_err(|e| col(2, e))?,
    })
  }
}

That is the single-driver shape. With two drivers unified on toolu-orm-core the trait asks for one method per driver instead โ€” from_pg_row, from_libsql_row, from_rusqlite_row โ€” which is the shape #[derive(FromRow)] emits.


Query builders

The snippets below name the crates directly (toolu_orm_core::โ€ฆ, toolu_orm_query::โ€ฆ); through the facade the same items are toolu_orm::core::โ€ฆ and toolu_orm::query::โ€ฆ.

Every builder renders with to_sql() (current dialect) or to_sql_for(Dialect::โ€ฆ) and returns (String, Vec<Value>). Column references are always table-qualified and quoted. The comparison methods come from three traits in toolu_orm_core::query_column: CommonOps (eq, ne, in_list, not_in, is_null, is_not_null), TextOps (like), and NumericOps (gt, lt, gte, lte, between).

use toolu_orm_core::query_column::{CommonOps, NumericOps};
use toolu_orm_query::{delete::DeleteBuilder, insert::InsertBuilder, select::SelectBuilder, update::UpdateBuilder};

// SELECT with filters, join, ordering, paging
let (sql, params) = SelectBuilder::new("users")
  .columns_raw(&["id", "email"])
  .filter(users::org_id.eq("org123"))
  .filter(users::created_at.gt(0i32))
  .join("pipelines", users::id.equals(&pipelines::user_id))   // INNER JOIN; left_join() too
  .order_by(users::created_at.desc())
  .limit(10)
  .offset(0)
  .to_sql_for(Dialect::Sqlite);
// SELECT "id", "email" FROM "users"
//   INNER JOIN "pipelines" ON "users"."id" = "pipelines"."user_id"
//   WHERE "users"."org_id" = ?1 AND "users"."created_at" > ?2
//   ORDER BY "users"."created_at" DESC LIMIT ?3 OFFSET ?4

// INSERT, with upsert flavours that render per dialect
InsertBuilder::new("seeds").or_ignore().set(&seeds::id, "seed-1").to_sql_for(Dialect::Postgres);
// INSERT INTO "seeds" ("id") VALUES ($1) ON CONFLICT DO NOTHING
InsertBuilder::new("run_status").or_replace().set(&run_status::run_id, "run-1").set(&run_status::status, "running")
  .to_sql_for(Dialect::Postgres);
// ... ON CONFLICT ("run_id") DO UPDATE SET "status" = EXCLUDED."status"

// UPDATE with a bound value and a raw SQL expression
UpdateBuilder::new("users").set(&users::email, "new@example.com").set_expr(&users::updated_at, "unixepoch()")
  .filter(users::id.eq("user-1")).to_sql_for(Dialect::Sqlite);
// UPDATE "users" SET "email" = ?1, "updated_at" = unixepoch() WHERE "users"."id" = ?2

// DELETE
DeleteBuilder::new("users").filter(users::id.eq("user-1")).to_sql_for(Dialect::Postgres);
// DELETE FROM "users" WHERE "users"."id" = $1

Executing. All four builders share .execute(exec) -> u64, where exec is the driver connection (&libsql::Connection, &rusqlite::Connection, &tokio_postgres::Client, or a transaction) โ€” not the DbConnection wrapper. Select adds:

let users: Vec<User> = SelectBuilder::new("users").columns_raw(&["id", "email", "created_at"]).fetch_all(exec).await?;
let one: User        = UsersTable::select_for::<User>().filter(users::id.eq("u_1")).fetch_one(exec).await?;   // QueryError::NotFound if empty
let maybe: Option<User> = UsersTable::select_for::<User>().filter(users::id.eq("nope")).fetch_optional(exec).await?;
let n: i64           = UsersTable::select().count(exec).await?;
let any: bool        = UsersTable::select().exists(exec).await?;

On the rusqlite driver these are synchronous: same names, no .await.

Also available: to_count_sql_for, to_exists_sql_for, SelectBuilder::raw().column_expr(expr, alias), and columns_typed(&[&dyn ColumnRef]).

Transactions. Anything that errors inside the closure rolls the whole block back.

use toolu_orm_query::transaction::TransactionExt;

// `conn` is a libsql::Connection โ€” TransactionExt is implemented on the driver type.
conn.run_transaction(|tx| async move {
  InsertBuilder::new("users").set(&users::id, "tx1").set(&users::email, "tx@example.com").execute(&tx).await?;
  UpdateBuilder::new("counters").set_expr(&counters::users, "users + 1").execute(&tx).await?;
  Ok(())
}).await?;

Relations

Declare the shape you want back; the query is one statement per dialect.

use toolu_orm_macros::Relational;

#[derive(Relational)]
#[relational(table = "users")]
struct UserWithPosts {
  pub id: String,
  pub name: String,
  #[has_many(table = "posts", foreign_key = "author_id", columns = ["id", "title"])]
  pub posts: Vec<PostRow>,
}
// #[belongs_to(...)]    โ†’ field is Option<T>, same keys
// #[many_to_many(...)]  โ†’ adds through = "post_tags", local_key = "post_id"

The typed builder tracks the result tuple at compile time:

use toolu_orm_query::relational_builder::RelationalQuery;

let q = RelationalQuery::<(UserRow,)>::new("users", &["id", "name"])
  .with_many::<PostRow>("posts", "posts", "id", "author_id", &["id", "title"])
  .with_one::<ProfileRow>("profile", "profiles", "id", "user_id", &["id", "bio"]);
let _: RelationalQuery<(UserRow, Vec<PostRow>, Option<ProfileRow>)> = q;
  • SQLite renders correlated subqueries with json_group_array.
  • Postgres renders LEFT JOIN LATERAL with json_agg / json_build_array.

An untyped RelationalSelectBuilder exposes the same with_many / with_one plus to_sql_sqlite() / to_sql_postgres() when you only want the SQL.


Migrations

use toolu_orm_cli::{generate::run_generate, migrate::run_migrate, status::get_status};

let wrote = run_generate(&registry, "migrations", "add_posts", Dialect::Postgres)?;
//  โ†’ Some("0002_add_posts.sql"), or None when the schema did not change

let applied = run_migrate(&conn, "migrations", Dialect::Postgres).await?;   // u32 files applied

let status = get_status(&conn, "migrations", Dialect::Postgres).await?;
println!("applied: {:?}, pending: {:?}", status.applied, status.pending);

What lands on disk:

migrations/
โ”œโ”€โ”€ _journal.json                # order + "sha256:โ€ฆ" per file
โ”œโ”€โ”€ 0001_init.sql
โ”œโ”€โ”€ 0001_init.snapshot.json      # schema state after this migration
โ”œโ”€โ”€ 0002_add_posts.sql
โ””โ”€โ”€ 0002_add_posts.snapshot.json
  • Files apply in name order; the applied set is tracked in a _migrations table on the target database.
  • A file holding several statements separates them with --> statement-breakpoint. Each file runs inside BEGIN / COMMIT.
  • The journal hash is verified before a file runs. Edit a shipped migration and run_migrate stops with MigrateError::HashMismatch.
  • Snapshots are plain JSON (version, dialect, id, prev_id, tables, enums, meta), so a schema diff is reviewable in the PR alongside the SQL.

Drivers

Feature Backing crate Mode Open with
libsql libsql async; local file, :memory:, or Turso embedded replica Database::init_local(path) ยท Database::init_remote(RemoteConfig)
rusqlite rusqlite (bundled) sync, wrapped in spawn_blocking RusqliteConnection::open(path) ยท ::open_in_memory()
postgres tokio-postgres + deadpool async pool, rustls TLS PgDatabase::init(&PgConfig) then .connect()
// Turso embedded replica: local file kept in sync with the remote
let db = Database::init_remote(RemoteConfig {
  replica_path: "data/app.db".into(),
  url: "libsql://my-db.turso.io".into(),
  auth_token: std::env::var("TURSO_TOKEN")?,
  sync_interval_secs: 5,
  max_sync_attempts: 5,
}).await?;

// Postgres pool
let pg = PgDatabase::init(&PgConfig {
  host: "localhost".into(), port: 5432,
  user: "app".into(), password: std::env::var("PGPASSWORD")?, dbname: "app".into(),
  max_connections: 10, ssl: true,
}).await?;
let conn = pg.connect().await?;

All backends implement DbConnection (execute_sql, query_map<T: FromRow>, execute_batch), which is what run_migrate and get_status accept.


Contributing

Read CLAUDE.md first: it holds the workspace map and the binding rules. The short version:

  1. No .unwrap(), .expect(), panic!, unreachable!, or [] indexing in src/. Propagate with ? / ok_or. Tests may.
  2. No #[allow] / #[expect]. Fix the warning.
  3. No #[cfg(test)] in src/; tests live in each crate's tests/.
  4. โ‰ค 250 lines per file. Over that, split into a folder module whose mod.rs holds only mod, pub use, and //! docs.
  5. One concern per file. No utils.rs / helpers.rs / common.rs.
  6. cargo nextest run, never cargo test.

The quality gate is what CI runs: four feature lanes plus a docs check. Every test executes against a real database (in-memory libsql, in-memory rusqlite, or a live Postgres), so start the test Postgres first:

docker compose -f docker-compose.test.yaml up -d --wait   # postgres:16 on localhost:5434
export TEST_DB_PORT=5434                                   # for_test() defaults to 5433

cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo nextest run --workspace
cargo clippy -p toolu-orm -p toolu-orm-core -p toolu-orm-macros -p toolu-orm-query -p toolu-orm-connection -p toolu-orm-cli --features postgres --all-targets -- -D warnings
cargo nextest run -p toolu-orm -p toolu-orm-core -p toolu-orm-macros -p toolu-orm-query -p toolu-orm-connection -p toolu-orm-cli --features postgres
cargo clippy -p toolu-orm-query --features libsql --all-targets -- -D warnings
cargo nextest run -p toolu-orm-query --features libsql
cargo clippy -p toolu-orm-query --features rusqlite --all-targets -- -D warnings
cargo nextest run -p toolu-orm-query --features rusqlite
cargo clippy -p toolu-orm-connection --features rusqlite --all-targets -- -D warnings
cargo nextest run -p toolu-orm-connection --features rusqlite
bash scripts/check-scenario-docs.sh

TEST_DB_HOST, TEST_DB_PORT, TEST_DB_USER, and TEST_DB_PASSWORD point the Postgres suites at another server. Each feature scenario is documented in docs/scenarios/ with the tests that prove it on every driver; the docs check fails when a page and its tests drift apart, so update the page with the test. Use a Conventional Commits subject (feat(query): add fetch_optional).

Releases

Releases are automated with release-plz. You do not bump versions or tag by hand.

  • Merge Conventional Commits to main. release-plz maintains one release PR that bumps the shared workspace version and rewrites CHANGELOG.md.
  • Merge that PR to cut the release: the five crates publish to crates.io in dependency order, then a single vX.Y.Z tag and GitHub Release are created.

License

MIT ยฉ Falconiere Barbosa