Today I released NanoSQL, a small and ergonomic data mapper for SQLite.
What?
- It's just that, a data mapper. Not a full ORM, more like half of it. It doesn't attempt to type check your queries; that's a Hard™ problem. (But I'm working on it!) Think Dapper vs. Entity Framework.
- Yes, it's SQLite-only. Abstracting over DB engines would be possible, but it's much easier to release a working, convenient, and correct piece of software if one can bake in some assumptions. Also, you may have noticed that I love SQLite.
How?
It's like Uber Serde but for SQL queries. The short story is just 4 steps:
- You define a schema by creating
struct
s that implement theTable
trait. This describes the structure of the table, e.g. column names and types, indexes, constraints, etc. - You define a typed query that implements the
Query
trait. This specifies associated types for the input (parameters) and the output (result set). It also writes the raw SQL query to afmt::Formatter
. - You open a
Connection
and use it to compile theQuery
into aCompiledStatement
. The compiled statement is now fully statically typed. - You use the
CompiledStatement
to invoke the underlying prepared statement with the correct input and output types. The input must implement theParam
trait, the output must implementResultSet
.
Highlights:
Param
,ResultRecord
, andTable
can all be#[derive]
d. TheTable
impl can be customized extensively. (There are also additional, convenience derive macros; see the docs.)- The
Table
trait/macro supports most advanced features of the SQLite DDL, such as partial indexes, foreign keys, and generated columns. ResultSet
is implemented forstd
and custom collections with an item type that implementsResultRecord
. The ones you'll use the most areSingle
,Option
, andVec
, for 1, 0 or 1, or any rows, respectively.- The
define_query!
declarative macro provides a shorter way of definingQuery
types. - It tries very hard to do the right thing by default:
- Foreign keys are automatically indexed and
DEFERRED
, so you can easily insert and look up complex data structures within a transaction. - Attributes that require raw SQL to be interpolated are checked for syntax errors at compile time to avoid emitting corrupted SQL, using the
sqlparser
crate. - Transactions are used for all library-provided bulk operations.
- When using the provided factory methods, NanoSQL will apply the recommended, modern settings of SQLite, e.g. turn on foreign key constraints and activate WAL mode.
- If lifetimes get you in trouble, feel free to re-compile queries more frequently, because NanoSQL internally uses cached statements with a reasonably large cache size, so subsequent calls to
compile()
won't actually re-prepare the query. - Parameters and result columns are ensured to have the correct names and cardinality at runtime.
- Foreign keys are automatically indexed and
- Enums with unit-like variants are also supported by the derive macros (when sensible), to make domain modeling easier.
- The
ConnectionExt
andTransactionExt
traits provide convenience methods for frequent tasks, such as creating a table and its indexes, batch insertion of records, orEXPLAIN QUERY PLAN
(which outputs a structured tree, pretty printable viaDisplay
).
Examples
- The basics are demonstrated in the README.
- The
realistic
example showcases a more complex use case, inspired by a real data modeling problem.
Enjoy!