23 releases
Uses new Rust 2024
| new 0.3.0 | Dec 25, 2025 |
|---|---|
| 0.2.2 | Dec 24, 2025 |
| 0.1.19 | Dec 16, 2025 |
#1160 in Rust patterns
Used in 3 crates
(2 directly)
130KB
3K
SLoC
LazySql
- LazySql is a sqlite library for rust
- Has compile time guarantees
- Ergonomic
- Fast. Automatically caches and reuses preparred statements for you
- However, it follows an opinionated API design
Overview
Installation
Quick Start
use lazysql::{LazyConnection, lazy_sql};
#[lazy_sql]
struct AppDatabase {
// all create tables must be at the top before read/write logic in order to get compile time cheks
// dont have to import sql! macro. lazy_sql brings along with it
init: sql!("
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY NOT NULL,
username TEXT NOT NULL,
is_active INTEGER NOT NULL CHECK (is_active IN (0, 1)) -- the library infers this as bool. more info below
)
"),
// postgres `::` type casting is supported. Alternatively u can use CAST AS syntax
add_user: sql!("INSERT INTO users (id, username, is_active) VALUES (?::real, ?, ?)"),
get_active_users: sql!("SELECT id::real, username, is_active as active FROM users WHERE is_active = ?"),
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let conn = LazyConnection::open_memory()?;
// The 'new' constructor is generated automatically
let mut db = AppDatabase::new(&conn);
// You can now call the methods and it will run the sql commands
db.init()?;
// Types are enforced by Rust
// Respects type inference. i64 -> f64 for id (first argument)
db.add_user(0.0, "Alice", true)?;
db.add_user(1.0, "Bob", false)?;
// active_users is an iterator
let active_users = db.get_active_users(true)?;
for user in active_users {
// u can access the fields specifically if you want
// Respects Aliases (is_active -> active)
let user = user?;
println!("{} {}, {}", user.active, user.username, user.id); // note user.id is float as we type casted it in the sql stmt
}
Ok(())
}
has some nice QOL features like hover over to see sql code and good ide support

-
The type inference system and compile time check also works well for
joins,ctes,window function,datetime functionsrecursive ctes,RETURNINGand more complex scenarios. -
Since SQLite defaults to nullable columns, the type inference system defaults to Option. To use concrete types (e.g., String instead of Option), explicitly add NOT NULL to your table definitions
-
It is strongly recommended to use STRICT tables for better compile time guarantees. Recommended to use WITHOUT ROWID.
-
There will be rare scenarios when a type is impossible to infer.
LazySqlwill tell you specifically which binding parameter or expression cannot be inferred and will suggest using type casting via PostgreSQL's::operator or standard SQL'sCAST AS. Note that you can't type cast asbooleanfor now.For instance,

Connection methods
lazysql supports 3 ways to define your schema, depending on your workflow.
1. Inline Schema (Standalone)
As seen in the Quick Start. Define tables inside the struct.
#[lazy_sql]
struct App { ... }
2. SQL File
Point to a .sql file. The compile time checks will be done against this sql file (ensure that there is CREATE TABLE). lazysql watches this file; if you edit it, rust recompiles automatically to ensure type safety.
#[lazy_sql("schema.sql")]
// you dont have to create tables. Any read/write sql queries gets compile time guarantees.
struct App { ... }
3. Live Database
Point to an existing .db binary file. lazysql inspects the live metadata to validate your queries.
#[lazy_sql("production_snapshot.db")]
struct App { ... }
Note: for method 2 and 3, you can techinically CREATE TABLE as well but to ensure that they are taken into considreration for compile time check, add them at the top of your struct
Features
the lazy_sql! macro brings along sql! and sql_runtime! macro. so there is no need to import them. and they can only be used within structs defined with lazy_sql!
-
sql!MacroAlways prefer to use this. It automatically:
- Infers Inputs: Maps
?to Rust types (i64,f64,String,bool). - Generates Outputs: For
SELECTqueries, creates a struct named after the field
- Infers Inputs: Maps
-
sql_runtime!MacroUse this only when u need the sql to to be runned at runtime. And there are some additional things to take note of when using this macro
a.
SELECTYou can map a query result to any struct by deriving
SqlMapping.SqlMappingmaps columns by index, not by name. The order of fields in your struct must match the order of columns in yourSELECTstatement exactly.use lazysql::{SqlMapping, LazyConnection, lazy_sql}; #[derive(Debug, SqlMapping)] struct UserStats { total: i64, // Maps to column index 0 status: String, // Maps to column index 1 } #[lazy_sql] struct Analytics { get_stats: sql_runtime!( UserStats, // pass i the struct so u can access the fields later "SELECT count(*) as total, status FROM users WHERE id > ? AND login_count >= ? GROUP BY status", i64, // Maps to 1st '?' i64 // Maps to 2nd '?' ) } fn foo{ let conn = LazyConnection::open_memory()?; let mut db = Analytics::new(&conn); let foo = db.get_stats(100, 5)?; for i in foo{ // i.total and i.status is accessible } }b. No Return Type
For
INSERT,UPDATE, orDELETEstatements#[lazy_sql] struct Logger { log: sql_runtime!("INSERT INTO logs (msg, level) VALUES (?, ?)", String, i64) } // can continue to use it normally. -
Postgres
::type casting syntaxNote: bool type casting is not supported for now
sql!("SELECT price::text FROM items") // Compiles to: // "SELECT CAST(price AS TEXT) FROM items" -
all()andfirst()methods for iterators-
all()collects the iterator into a vector. Just a lightweight wrapper around .collect() to prevent adding type hints (Vec<_>) in codelet results = db.get_active_users(false)?; let collected_results =results.all()?; // returns a vec of owned results from the returned rows -
first()Returns the first row if available, or None if the query returned no results.let results = db.get_active_users(false)?; let first_result = results.first()?.unwrap(); // returns first column from the returned rows
-
Type Mapping
| SQLite Context | Rust Type | Notes |
|---|---|---|
TEXT |
String / &str |
- |
INTEGER |
i64 |
- |
REAL |
f64 |
Includes FLOAT, DOUBLE |
BOOLEAN |
bool |
Requires CHECK (col IN (0,1)) or Check (col = 0 OR col = 1). You could techinically use BOOL or BOOLEAN as the data type when creating table (due to sqlite felxible type nature) and it would work as well. But this is discouraged |
| Nullable | Option<T> |
When a column or expr has a possibility of returning NULL, this will be returned. its recommended to use NOT NULL when creating tables so that ergonoimic wise you dont have always use Some(T) when adding parameters |
Notes
Strict INSERT Validation
- Although standard SQL allows inserting any number of columns to a table, lazysql checks INSERT statements at compile time. If you omit any column (except for
AUTOINCREMENTandDEFAULT) code will fail to compile. This means you must either specify all columns explicitly, or use implicit insertion for all columns. This is done to prevent certain runtime errors such asNOT NULL constraint failedand more.
False positive during compile time checks
- I tried my best to support as many sql and sqlite-specific queries as possible. In the extremely rare case of a false positive (valid SQL syntax fails or type inference incorrectly fails), you can fall back to the
sql_runtime!macro. Would appreciate it if u could open an issue as well.
Cannot type cast as Boolean
- This is a limitation of sqlite since it doesn't natively have
booleantype. I may find some workaround in the future but its not guaranteed. For now if you want to type cast as bool, u have to type cast it as anintegerand add either 1 (TRUE) or 0 (False)
TODOS
- upsert
- transactions
- check_constarint field in SELECT is ignored for now. maybe in future will make use of this field
- cant cast as bool
Dependencies
~3MB
~66K SLoC