Easy SQL is a macro-first toolkit for writing SQL with strong compile-time guarantees. Based on sqlx
- Readable SQL with IDE syntax highlighting (VS Code) inside
query!andquery_lazy!. - Type-checked column and table references, plus bind validation via
{value}. - Clean bindings embedded directly into the macro input, no separate
bind()chain. - Optional migrations see #Migration system.
- Optional table name checks to prevent duplicates across files.
- Interoperable with
sqlx: useeasy-sqlmacros on SQLx connections/pools, or use migrations only. - Currently supported drivers: SQLite and Postgres.
-main/— maineasy-sqlcrate (library, drivers, tests, docs).build/—easy-sql-buildhelper crate for build-time setup and checks.compilation-data/— crate used for compile-time metadata.macros/— procedural macro crate powering derives and SQL macros.scripts/— helper scripts for tests and README tooling.
Add the easy-sql dependency, then choose drivers and the matching sqlx runtime/TLS features.
Pick the driver features you need. Checking for duplicate table names is enabled by default. See Feature flags
[dependencies]
easy-sql = { version = "0.101", features = ["sqlite", "postgres"] }SQLx requires choosing one runtime and optionally one TLS backend, plus your database driver(s). From the official SQLx install guide:
- Runtime:
runtime-tokioorruntime-async-std. - TLS: (optional)
tls-native-tlsor one of thetls-rustls-*variants. - SQLite: choose
sqlite(bundled SQLite) orsqlite-unbundled(system SQLite).
Example using Tokio + Rustls + SQLite:
[dependencies]
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls-ring-webpki", "sqlite"] }See the full SQLx installation matrix at https://github.com/launchbadge/sqlx#install.
The build helper crate is easy-sql-build.
Use it when you want:
- to not provide
#[sql(drivers = ...)]for every Table by hand, - specific driver compatibility checks
- migrations,
- duplicate table name checks (
check_duplicate_table_names), - default drivers for
query_lazy!.
Add it as a build dependency:
[build-dependencies]
easy-sql-build = "1"
regex = "1" # needed only when you want to ignore certain files/directories in the build scriptExample build.rs:
fn main() {
easy_sql_build::build(
// You can provide regex patterns to ignore certain files or directories
&[regex::Regex::new(r"tests/.*").unwrap()],
// Specify the locations of default drivers for Table setup generation and checks.
&["easy_sql::Sqlite"],
);
}
⚠️ Do not gitignoreeasy_sql.ron. It stores migration metadata + selected default drivers + list of all table names.
query! and query_lazy! give you readable SQL with compile-time validation and typed outputs.
query!executes immediately and returns ananyhow::Result<Output>.query_lazy!builds a lazy query and returns a stream on execution. Withuse_output_columnsfeature, bare column references are referring to columns from the output type, instead of the table type.
use easy_sql::sqlite::Database;
use easy_sql::{DatabaseSetup, Insert, Output, Table, query};
// DatabaseSetup lets you group tables into a single setup call.
#[derive(DatabaseSetup)]
struct PartOfDatabase {
users: UserTable,
}
#[derive(Table)]
struct UserTable {
#[sql(primary_key)]
#[sql(auto_increment)]
id: i32,
email: String,
active: bool,
}
#[derive(Insert)]
#[sql(table = UserTable)]
// Required to make sure that no fields are potentially ignored
#[sql(default = id)]
struct NewUser {
email: String,
active: bool,
}
#[derive(Output)]
#[sql(table = UserTable)]
struct UserRow {
id: i32,
#[sql(select = email || " (active = " || active || ")")]
email_label: String,
active: bool,
}
async fn main() -> anyhow::Result<()> {
let db = Database::setup::<PartOfDatabase>("app.sqlite").await?;
let mut conn = db.conn().await?;
let data = NewUser {
email: "sam@example.com".to_string(),
active: true,
};
query!(&mut conn, INSERT INTO UserTable VALUES {data}).await?;
let new_email = "sammy@example.com";
query!(&mut conn,
UPDATE UserTable SET active = false, email = {new_email} WHERE UserTable.email = "sam@example.com"
)
.await?;
let row: UserRow = query!(&mut conn,
SELECT UserRow FROM UserTable WHERE email = {new_email}
)
.await?;
println!("{} {}", row.id, row.email_label);
Ok(())
}Migrations are optional and driven by table versions in Table definitions. Use migrations feature to enable them.
- Create the table struct and set
#[sql(version = 1)]. - Save/build so the build helper can generate
#[sql(unique_id = "...")]and register the version structure ineasy_sql.ron. - Update the table (add/rename fields), then bump the version up.
- Save/build again — the migration from version 1 is automatically generated and will be applied when (driver related)
Database::setupor (table related)DatabaseSetup::setupare called.
Version tracking is stored in EasySqlTables, and you can opt out with #[sql(no_version)] (needed only when migrations feature is enabled).
table_join!for typed joins.custom_sql_function!for custom SQL functions.IN {vec}binding with automatic placeholder expansion.#[sql(select = ...)]onOutputfields.#[sql(bytes)]for binary/serde storage.- Composite primary keys and
#[sql(foreign_key = ...)]relationships.
Unless stated otherwise, feature is disabled by default.
sqlite: Enable the SQLite driver.postgres: Enable the Postgres driver.sqlite_math: Enable extra SQLite math functions. Sqlite needs to be compiled withLIBSQLITE3_FLAGS="-DSQLITE_ENABLE_MATH_FUNCTIONS"for those functions to work.migrations: Enable migration generation and tracking.check_duplicate_table_names(default: ✅): Detect duplicate table names at build time.use_output_columns: Bare columns refer to output the type, instead of the table type.bigdecimal: AddBigDecimalToDefaultsupport (SQLxbigdecimal).rust_decimal: AddDecimalToDefaultsupport (SQLxrust_decimal).uuid: AddUuidToDefaultsupport via SQLx.chrono: AddchronoToDefaultsupport via SQLx.ipnet: AddipnetToDefaultsupport via SQLx.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.