12 unstable releases (3 breaking)
0.7.1 | Oct 25, 2023 |
---|---|
0.7.0 | Oct 16, 2023 |
0.6.3 | Oct 16, 2023 |
0.5.7 | Oct 12, 2023 |
0.0.0 | Oct 9, 2023 |
#1270 in Procedural macros
36 downloads per month
Used in exemplar
17KB
433 lines
Exemplar
A boilerplate eliminator for rusqlite
.
Getting Started
A taste of what you can do:
#[derive(Debug, PartialEq, Model)]
#[table("users")]
#[check("../tests/schema.sql")]
struct User {
username: String,
#[bind(bind_path)]
#[extr(extr_path)]
home_dir: PathBuf,
#[column("pwd")]
password: Vec<u8>,
}
fn main() -> Result<()> {
let conn = Connection::open_in_memory()?;
conn.execute_batch(
include_str!("../tests/schema.sql")
)?;
let alice = User {
username: "Alice".to_owned(),
home_dir: "/var/home/alice".into(),
password: b"hunter2".to_vec()
};
let bob = User {
username: "Bob".to_owned(),
home_dir: "/var/home/robert".into(),
password: b"password".to_vec()
};
alice.insert(&conn)?;
bob.insert(&conn)?;
let mut stmt = conn.prepare("
SELECT * FROM users ORDER BY username ASC
")?;
let mut iter = stmt.query_and_then([], User::from_row)?;
assert_eq!(alice, iter.next().unwrap()?);
assert_eq!(bob, iter.next().unwrap()?);
Ok(())
}
Exemplar is based around the Model
trait, which has its own derive macro.
- See the aformentioned macro's documentation to get started.
- For handling
enum
s in models, check out thesql_enum
macro. - For working with "anonymous" record types, look at the
record
macro.
Features
- Works with raw SQL, not against it.
- Thin, zero-cost API.
- Most of Exemplar revolves around the
Model
trait, which gets inlined and monomorphized away before runtime. The resulting code is roughly what you'd write by hand when using purerusqlite
. - Designed to be drop-in; reuses
rusqlite
's existing types where possible, including itsResult
type alias. - Supports any type that
Deref
's torusqlite::Connection
, such as transactions or pooled connections.
- Most of Exemplar revolves around the
- Optional test derivation for guarding against drift between your database schema and Rust model types.
- Macros for working with SQL-compatible
enum
s and "anonymous" record types that map to ad-hoc queries. - Some ability to reflect on/work with
dyn Model
s at runtime.
If you just need to CRUD some Rust data with sqlite
and don't want a whole ORM or enterprise-grade DBMS, then Exemplar is for you!
FAQ
"What does Exemplar not do?"
A few key things:
- Schema generation and management. Exemplar is explicitly not an ORM, and it's difficult to represent concepts like foreign keys and migrations
without falling into ORM territory.
- If this is a "must" for you, check out
diesel
orsqlx
/seaorm
, which both support SQLite.
- If this is a "must" for you, check out
- Query generation (excluding
INSERT
.) - Interface portability. Only
rusqlite
is supported.
"Is it blazing fast?"
Yes. On my machine (according to these benchmarks) Exemplar can:
- Insert a non-trivial model type in ~600 nanoseconds (1.6 million rows/sec)
- Query and reconstruct the same type in ~9 microseconds (111,000 rows/sec, using
SELECT * LIMIT 1
)
Obviously the credit for this speed goes to the SQLite and rusqlite
developers, but I can confidently say that I didn't slow things down!
"How does this compare to serde-rusqlite
?"
serde_rusqlite
is a clever hack, but it still involved too much contorting and boilerplate for my taste - that's why I created Exemplar.
The pain points I tried to fix were:
- Needing to allocate and juggle a slice of
String
column names to efficiently deserialize rows - probably due toserde
limitations?- Exemplar statically knows what columns to expect, so
from_row
requires no extra inputs and makes no superfluous allocations.
- Exemplar statically knows what columns to expect, so
- Odd design choices for field-less
enum
s - they are inefficiently serialized asTEXT
instead ofINTEGER
. This was nice for debugging, but I figured the faster option should be Exemplar's default. to_params_named(&row1).unwrap().to_slice().as_slice()
doesn't quite roll off the tongue (although this is likelyserde
weirdness showing up again.)- Equivalent to
row1.insert(&conn)
orrow1.insert_with(&stmt)
in Exemplar.
- Equivalent to
- General
serde
overhead popping up, both at compile and runtime.- Benchmarking shows that
serde_rusqlite
is ~25% slower on insert operations compared to Exemplar. - Retrieval operations are equally fast, likely because the final conversion step is nothing compared to query calculation and I/O.
- Benchmarking shows that
Acknowledgements
rusqlite
, for providing the foundation on which this library is built.- David Tolnay, for his various proc macro
incantationscrates.
Dependencies
~310–770KB
~18K SLoC