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
Type-Safe SQL for Rust
Build explicit, immutable queries and execute them with async PostgreSQL or SQLite drivers
Overview • Features • Quick Start • Database Drivers • Architecture • Development
§Overview
A3S ORM is a type-safe SQL query builder for Rust, inspired by Kysely. Rust table definitions constrain columns, values, and decoded results at compile time. Queries compile into SQL plus bound parameters and execute through an async, driver-neutral interface.
Despite the name, this is not an Active Record framework. Records do not own persistence behavior, queries stay explicit, and runtime values are never interpolated into generated SQL.
§Basic usage
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",
}
}
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)?;
assert_eq!(
query.sql,
"select \"person\".\"id\", \"person\".\"name\" from \"person\" where (\"person\".\"age\" >= $1) order by \"person\".\"name\" asc limit $2"
);§Features
- Typed Schema: Catch invalid columns, values, and assignments at compile time
- Immutable Queries: Build SELECT, INSERT, UPDATE, and DELETE statements explicitly
- Safe Parameters: Keep runtime values out of generated SQL
- Advanced SQL: Use joins, CTEs, subqueries, aggregates, windows, set operations, functions, casts, and PostgreSQL row/table locks
- Typed Results: Decode scalar, tuple, nullable, array, and extended database values
- Async Drivers: Run non-blocking SQLite and pooled PostgreSQL operations on Tokio
- Safe Transactions: Roll back scoped work on errors and task cancellation
- PostgreSQL HA Controls: Select transaction semantics, bound pool waits, classify retryable failures, observe health, and rotate verified TLS pools
- Migrations: Apply locked, atomic, checksummed migrations
- Extensible Runtime: Add another database through the public
Executorcontract
§Support matrix
| Capability | PostgreSQL | SQLite | MySQL |
|---|---|---|---|
| SQL compilation | Yes | Yes | Yes |
| Bundled async driver | Yes | Yes | No |
RETURNING | Yes | Yes | Rejected |
ON CONFLICT | Yes | Yes | Rejected |
FOR UPDATE, NOWAIT, SKIP LOCKED | Yes | Rejected | Rejected |
| Transactions | Yes | Yes | — |
| Locked migrations | Advisory lock | BEGIN IMMEDIATE | — |
| UUID, JSON, temporal, decimal, arrays | Yes | SQLite-native subset | — |
MySQL support currently means SQL generation only; it does not imply a bundled runtime driver. See Production Readiness for the precise supported scope and limitations.
§Quick Start
§Installation
Pin the released Git tag:
[dependencies]
a3s-orm = { git = "https://github.com/A3S-Lab/ORM", tag = "v0.2.0" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }SQLite is enabled by default. For a compile-only query builder without a bundled driver:
a3s-orm = { git = "https://github.com/A3S-Lab/ORM", tag = "v0.2.0", default-features = false }For PostgreSQL:
a3s-orm = { git = "https://github.com/A3S-Lab/ORM", tag = "v0.2.0", default-features = false, features = ["postgres"] }The postgres feature includes UUID, JSON/JSONB, Chrono date/time types,
rust_decimal::Decimal, and SqlArray<T>.
§Insert, update, and delete
let insert = insert_into::<Person>()
.value(Person::id(), 1)
.value(Person::name(), "Ada")
.returning(Person::id())
.compile(&PostgresDialect)?;
let update = update_table::<Person>()
.set(Person::name(), "Ada Lovelace")
.filter(Person::id().eq(1))
.compile(&PostgresDialect)?;
let delete = delete_from::<Person>()
.filter(Person::id().eq(1))
.compile(&PostgresDialect)?;Multi-row inserts use typed InsertRow<T> values. PostgreSQL and SQLite also
support conflict targets, DO NOTHING, bound updates, and values from the
excluded row.
§Expressions and PostgreSQL locks
Scalar functions and casts stay inside the typed expression AST. Function and SQL type names are validated; runtime values remain parameters. Typed scalar subqueries and column comparisons can be composed into filters and ordering:
let query = select_from::<Person>()
.select(Person::id())
.filter(Person::id().ne_column(Person::manager_id()))
.filter(
sql_function::<i64>("length", [Person::name().expression()])
.gt(3),
)
.filter(
cast::<String, i64>(
cast::<String, String>(bound::<String>("18"), "text"),
"bigint",
)
.gte(18),
)
.order_by_expression(
coalesce::<i64>([
scalar_subquery(select_from::<Person>().select(min(Person::id())))
.expression(),
Person::id().expression(),
]),
OrderDirection::Asc,
)
.for_share_of::<Person>()
.skip_locked()
.compile(&PostgresDialect)?;
assert!(query.sql.ends_with("for share of \"person\" skip locked"));for_update, for_no_key_update, for_share, and for_key_share each have
targeted *_of variants and support no_wait or skip_locked. PostgreSQL
table locks use a schema marker instead of a string:
let query = lock_table::<DomainClaim>(PostgresTableLockMode::ShareRowExclusive)
.no_wait()
.compile(&PostgresDialect)?;
assert_eq!(
query.sql,
"lock table \"domain_claims\" in share row exclusive mode nowait",
);Row and table locks are rejected by unsupported dialects. PostgreSQL
transactions also expose advisory_xact_lock(namespace, key) for
parameterized logical locks whose target row does not exist yet.
§Typed results
A selection determines its Rust output type. fetch_all_as, fetch_optional_as,
and fetch_one_as decode that type and enforce the requested cardinality.
Checked integer conversion reports overflow with the result-column index.
For exceptional SQL outside the typed AST, sql_query::<Output> accepts
reviewed static SQL while runtime data enters through bind. Prefer extending
the typed AST when an application needs a reusable missing capability.
§Database Drivers
§SQLite
use a3s_orm::{Database, SqliteDialect, SqliteExecutor};
let executor = SqliteExecutor::open("app.db").await?;
let database = Database::new(SqliteDialect, executor);File databases default to WAL journaling, foreign-key enforcement, and a
five-second busy timeout. SqliteExecutor::open_with_options allows each policy
to be changed. In-memory databases use memory journaling.
The driver serializes access to its connection without blocking Tokio. Scoped transactions and nested savepoints retain the connection gate until cancellation cleanup completes.
§PostgreSQL
use a3s_orm::{Database, PostgresDialect, PostgresExecutor};
let executor = PostgresExecutor::connect_no_tls(
"postgres://postgres:postgres@127.0.0.1/app",
16,
)?;
let database = Database::new(PostgresDialect, executor);connect_no_tls is intended for local or separately secured connections.
Production applications can use connect_tls with in-memory
PostgresTlsOptions, then atomically install verified replacement certificate
material through rotate_tls.
PostgresTransactionOptions selects isolation, read-only mode, and
transaction-local statement, lock, and idle timeouts. PostgresPoolOptions
bounds pool acquisition/creation/recycling. Stable label-free snapshots expose
pool saturation, acquisition latency, health, failure classes, and certificate
pool generations. See PostgreSQL HA Controls for the
complete deployment and retry contract.
§Migrations
Migrations are ordered 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};
let executor = SqliteExecutor::open("app.db").await?;
let report = Migrator::new(executor)
.run([Migration::new(
"001",
"create people",
"create table person (id integer primary key, name text not null)",
)])
.await?;
println!("applied: {:?}", report.applied);SQLite coordinates migrators through its connection gate and
BEGIN IMMEDIATE. PostgreSQL uses a transaction-scoped advisory lock with a
bounded configurable deadline. The migration SQL and history entry commit
atomically. Production rolling deployments should follow the documented
expand/migrate/verify/contract phases.
§Architecture
The query API does not depend on a database client:
typed schema + expressions
│
immutable query AST
│
dialect compiler
│
CompiledQuery
│
async Executor / driverSource is split by responsibility under compiler/, query/, drivers/, and
migration/. See Architecture for module ownership and
extension points.
§Development
The integration suite executes SQL against real databases. SQLite tests use actual in-memory and temporary file databases. PostgreSQL tests run against PostgreSQL 17 services and exercise schema creation, prepared queries, typed round trips, migrations, row and advisory locks, transactions, rollback, cancellation cleanup, concurrent serializable writers, pool exhaustion, failover-like disconnects, migration contention, mixed-version expand/contract compatibility, and generated-CA TLS rotation.
CI runs the full feature matrix with cargo llvm-cov and fails when line
coverage falls below 90%.
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-depsTo run PostgreSQL integration tests locally:
A3S_ORM_POSTGRES_URL=postgres://postgres:postgres@127.0.0.1:5432/a3s_orm \
cargo test --all-featuresSee Roadmap for planned schema builders, plugins, additional codecs, code generation, and the MySQL runtime driver.
§License
MIT
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::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§
Macros§
- orm_
table - Define a typed table marker and its columns.