Skip to main content

Crate a3s_orm

Crate a3s_orm 

Source
Expand description

Type-safe SQL query building inspired by Kysely.

a3s-orm keeps schema typing, query construction, SQL compilation, and execution behind separate interfaces. It does not use an Active Record model and never performs implicit runtime value conversion.

A3S ORM turns typed Rust schemas and predicates into parameterized SQL for async PostgreSQL and SQLite execution

Explicit queries. Compile-time constraints. Async PostgreSQL and SQLite.

CI status Latest release Rust 1.85 or newer MIT license

The contract · Quick start · Capabilities · Drivers · Migrations · Architecture


A3S ORM is a type-safe, executor-neutral SQL query builder for Rust, inspired by Kysely. Table declarations constrain columns, values, assignments, and decoded results at compile time. Immutable builders compile into SQL plus bound parameters, then execute through an async driver-neutral interface.

Despite the name, this is not an Active Record framework. Records do not own persistence behavior, queries remain visible, and runtime values are never interpolated into generated SQL.

§The contract

Define the schema once, compose with typed columns, and inspect the exact query before it reaches a connection:

use a3s_orm::{orm_table, select_from, OrderDirection, PostgresDialect, Query};

orm_table! {
    pub struct Person => "person" {
        id: i64 => "id",
        name: String => "name",
        age: i32 => "age",
    }
}

fn main() -> Result<(), a3s_orm::Error> {
    let query = select_from::<Person>()
        .select((Person::id(), Person::name()))
        .filter(Person::age().gte(18))
        .order_by(Person::name(), OrderDirection::Asc)
        .limit(20)
        .compile(&PostgresDialect)?;

    println!("sql = {}", query.sql);
    println!("parameters = {:?}", query.parameters);
    assert_eq!(query.parameters.len(), 2);
    Ok(())
}
sql = select "person"."id", "person"."name" from "person" where ("person"."age" >= $1) order by "person"."name" asc limit $2
parameters = [I64(18), U64(20)]

Column ownership and Rust value families are checked before compilation. The dialect owns quoting, placeholders, and feature support; unsupported syntax is rejected instead of approximated.

§Quick start

§Install

SQLite is the default runtime. Pin the released Git tag:

[dependencies]
a3s-orm = { git = "https://github.com/A3S-Lab/ORM", tag = "v0.2.1" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Enable the bundled PostgreSQL driver instead:

a3s-orm = { git = "https://github.com/A3S-Lab/ORM", tag = "v0.2.1", default-features = false, features = ["postgres"] }

Or use the query builder and dialect compilers without a bundled runtime:

a3s-orm = { git = "https://github.com/A3S-Lab/ORM", tag = "v0.2.1", default-features = false }

The postgres feature includes UUID, JSON/JSONB, Chrono date/time types, rust_decimal::Decimal, and SqlArray<T>.

§Execute a typed SQLite round trip

The default feature is enough for a real in-memory database:

use a3s_orm::{
    insert_into, orm_table, select_from, Database, SqliteDialect, SqliteExecutor,
};

orm_table! {
    struct Person => "person" {
        id: i64 => "id",
        name: String => "name",
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let executor = SqliteExecutor::open_in_memory().await?;
    executor
        .execute_schema(
            "create table person (id integer primary key, name text not null)",
        )
        .await?;

    let database = Database::new(SqliteDialect, executor);
    database
        .execute(
            insert_into::<Person>()
                .value(Person::id(), 1)
                .value(Person::name(), "Ada"),
        )
        .await?;

    let name: String = database
        .fetch_one_as(
            select_from::<Person>()
                .select(Person::name())
                .filter(Person::id().eq(1)),
        )
        .await?;

    assert_eq!(name, "Ada");
    Ok(())
}

§Capability map

  • Typed structure — schema markers constrain columns, joins, filters, inserts, updates, and result decoding.
  • Composable SQL — immutable SELECT, INSERT, UPDATE, and DELETE builders cover joins, CTEs, UPDATE FROM, typed expression assignments, subqueries, aggregates, windows, set operations, functions, casts, and conflict handling.
  • Explicit concurrency — PostgreSQL row locks, table locks, advisory locks, transaction isolation, access mode, and timeouts remain typed operations.
  • Checked results — scalar, tuple, nullable, array, UUID, JSON, temporal, and decimal values decode through checked conversions.
  • Cancellation-safe execution — scoped SQLite and PostgreSQL transactions retain their connection until rollback cleanup completes.
  • Deterministic migrations — ordered, checksummed migrations run atomically behind a bounded database lock.
  • Controlled escape hatchsql_query::<Output> accepts reviewed static SQL while dynamic values still enter through bind.

§PostgreSQL worker queues stay typed

Lock clauses, CTEs, update sources, and expression assignments are AST nodes rather than appended SQL strings. That keeps candidate selection and lease acquisition in one parameterized statement:

use a3s_orm::{
    orm_table, select_from, update_table, OrderDirection, PostgresDialect, Query,
};

orm_table! {
    struct Job => "jobs" {
        id: i64 => "id",
        state: String => "state",
        attempt_count: i32 => "attempt_count",
    }
}

orm_table! {
    struct JobCandidate => "job_candidate" {
        id: i64 => "id",
    }
}

fn main() -> Result<(), a3s_orm::Error> {
    let candidates = select_from::<Job>()
        .select(Job::id())
        .filter(Job::state().eq("ready"))
        .order_by(Job::id(), OrderDirection::Asc)
        .limit(1)
        .for_update_of::<Job>()
        .skip_locked()
        .as_cte::<JobCandidate>();
    let query = update_table::<Job>()
        .with(candidates)
        .set(Job::state(), "leased")
        .set_expression(Job::attempt_count(), Job::attempt_count() + 1)
        .from::<JobCandidate>()
        .filter(Job::id().eq_column(JobCandidate::id()))
        .returning((Job::id(), Job::attempt_count()))
        .compile(&PostgresDialect)?;

    assert!(query.sql.contains("for update of \"jobs\" skip locked"));
    assert!(query.sql.contains("update \"jobs\""));
    assert!(query.sql.contains("from \"job_candidate\""));
    Ok(())
}

Transaction-scoped advisory_xact_lock(namespace, key) covers logical resources that do not have a row yet. Retry classification identifies serialization, deadlock, lock contention, failover, connection loss, and pool saturation without automatically replaying writes. See PostgreSQL HA Controls for the complete contract.

§Drivers and dialects

CapabilityPostgreSQLSQLiteMySQL
SQL compilationYesYesYes
Bundled async driverYesYesNo
RETURNINGYesYesRejected
ON CONFLICTYesYesRejected
UPDATE FROMYesYesRejected
Row and table locksYesRejectedRejected
TransactionsYesYes
Locked migrationsAdvisory lockBEGIN IMMEDIATE
UUID, JSON, temporal, decimal, arraysYesSQLite-native subset

SQLite uses a Tokio-safe single connection. File databases default to WAL, foreign-key enforcement, and a five-second busy timeout. Nested savepoints and scoped transactions prevent later work from racing cancellation cleanup.

PostgreSQL uses a bounded Deadpool pool with prepared-statement caching. The driver exposes typed transaction policy, stable label-free health metrics, retry classification, verified rustls connections, and health-gated atomic TLS pool rotation. connect_no_tls is intended for local or separately secured connections.

MySQL support currently means SQL generation only. It does not imply a bundled runtime driver. Read Production Readiness for the precise deployment scope and limitations.

§Migrations

Migrations are sorted by version, checksummed with SHA-256, and recorded in a3s_orm_migrations. Re-running an unchanged set is a no-op; modifying or removing an applied migration is an error.

use a3s_orm::{Migration, Migrator, SqliteExecutor};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let executor = SqliteExecutor::open_in_memory().await?;
    let report = Migrator::new(executor)
        .run([Migration::new(
            "001",
            "create people",
            "create table person (id integer primary key, name text not null)",
        )])
        .await?;

    assert_eq!(report.applied, vec!["001"]);
    Ok(())
}

SQLite coordinates migrators through its connection gate and BEGIN IMMEDIATE. PostgreSQL uses a transaction-scoped advisory lock with a bounded deadline. Migration SQL and its history entry commit atomically.

§Architecture

The query API does not depend on a database client:

typed schema + expressions
          │
    immutable query AST
          │
     dialect compiler
          │
  SQL + bound parameters
          │
 async Executor / driver

The compiler never opens a connection, and drivers never need to understand typed builder state. A new dialect implements Dialect; a new runtime implements Executor. See Architecture for module ownership and extension rules.

§Production boundaries

The library makes unsupported behavior visible rather than silently falling back:

  • the bundled SQLite executor serializes work on one connection;
  • MySQL has a compiler but no bundled runtime driver;
  • migrations are forward-only;
  • scalar function and cast result types are explicit caller assertions;
  • typed DDL builders, query plugins, custom PostgreSQL domain codecs, and schema code generation are not included yet.

Review Production Readiness before deployment, PostgreSQL HA Controls for pool and failover policy, and the Roadmap for planned work.

§Development

The test suite runs real SQLite databases and PostgreSQL 17 services. CI checks the feature matrix, compile-fail doctests, Rust 1.85 MSRV, strict Clippy, warning-free rustdoc, dependency advisories, and at least 90% line coverage.

cargo fmt --all -- --check
cargo test --no-default-features
cargo test --all-features
cargo clippy --all-targets --all-features -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps

To run PostgreSQL integration tests locally:

A3S_ORM_POSTGRES_URL=postgres://postgres:postgres@127.0.0.1:5432/a3s_orm \
  cargo test --all-features

§License

MIT License

Re-exports§

pub use compiler::CompiledQuery;
pub use compiler::Dialect;
pub use compiler::MysqlDialect;
pub use compiler::PostgresDialect;
pub use compiler::SqliteDialect;
pub use decode::DecodeError;
pub use decode::FromRow;
pub use decode::FromValue;
pub use decode::Row;
pub use drivers::sqlite::SqliteError;
pub use drivers::sqlite::SqliteExecutor;
pub use drivers::sqlite::SqliteJournalMode;
pub use drivers::sqlite::SqliteMigrationError;
pub use drivers::sqlite::SqliteOptions;
pub use drivers::sqlite::SqliteRow;
pub use drivers::sqlite::SqliteSavepoint;
pub use drivers::sqlite::SqliteSavepointError;
pub use drivers::sqlite::SqliteTransaction;
pub use drivers::sqlite::SqliteTransactionError;
pub use error::Error;
pub use error::Result;
pub use executor::Database;
pub use executor::DatabaseError;
pub use executor::ExecuteResult;
pub use executor::Executor;
pub use executor::QueryResult;
pub use executor::Transaction;
pub use executor::TransactionManager;
pub use expression::exists;
pub use expression::not;
pub use expression::Column;
pub use expression::Expression;
pub use expression::OrderDirection;
pub use expression::SelectionExt;
pub use expression::SqlComparable;
pub use expression::SqlNumeric;
pub use expression::WindowBoundary;
pub use expression::WindowFrame;
pub use expression::WindowFrameUnits;
pub use function::bound;
pub use function::cast;
pub use function::coalesce;
pub use function::count;
pub use function::count_all;
pub use function::least;
pub use function::max;
pub use function::min;
pub use function::scalar_subquery;
pub use function::sql_function;
pub use function::TypedExpression;
pub use migration::pending_migrations;
pub use migration::AppliedMigration;
pub use migration::Migration;
pub use migration::MigrationBackend;
pub use migration::MigrationError;
pub use migration::MigrationReport;
pub use migration::Migrator;
pub use migration::PreparedMigration;
pub use query::delete_from;
pub use query::insert_into;
pub use query::lock_table;
pub use query::select_from;
pub use query::select_from_as;
pub use query::sql_query;
pub use query::update_table;
pub use query::ConflictTarget;
pub use query::InsertRow;
pub use query::PostgresTableLockMode;
pub use query::Query;
pub use query::SqlQuery;
pub use query::TableLockQuery;
pub use schema::Table;
pub use schema::TableRef;
pub use value::IntoSqlValue;
pub use value::SqlArray;
pub use value::Value;
pub use window::dense_rank;
pub use window::rank;
pub use window::row_number;
pub use window::WindowExpression;

Modules§

compiler
decode
drivers
error
executor
expression
function
migration
query
schema
value
window

Macros§

orm_table
Define a typed table marker and its columns.