An easy local data persistence layer, backed by SQLite.
struct
sINSERT
/SELECT
/UPDATE
/DELETE
operations```rust use turbosql::{Turbosql, select, execute};
struct Person {
rowid: Option
fn main() -> Result<(), Box
let name = "Joe";
// INSERT a row
let rowid = Person {
name: Some(name.to_string()),
age: Some(42),
..Default::default()
}.insert()?;
// SELECT all rows
let people = select!(Vec<Person>)?;
// SELECT multiple rows with a predicate
let people = select!(Vec<Person> "WHERE age > " 21)?;
// SELECT a single row with a predicate
let mut person = select!(Person "WHERE name = " name)?;
// UPDATE based on rowid, rewrites all fields in database row
person.age = Some(43);
person.update()?;
// UPDATE with manual SQL
execute!("UPDATE person SET age = " 44 " WHERE name = " name)?;
// DELETE
execute!("DELETE FROM person WHERE rowid = " 1)?;
Ok(())
} ```
See integration_test.rs
or trevyn/turbo for more usage examples!
Turbosql generates a SQLite schema and prepared queries for each struct:
```rust use turbosql::Turbosql;
struct Person {
rowid: Option
↓ auto-generates and validates the schema
```sql CREATE TABLE person ( rowid INTEGER PRIMARY KEY, name TEXT, age INTEGER, image_jpg BLOB, ) STRICT
INSERT INTO person (rowid, name, age, image_jpg) VALUES (?, ?, ?, ?)
SELECT rowid, name, age, image_jpg FROM person ```
Queries with SQL predicates are also assembled and validated at compile time. Note that SQL types vs Rust types for parameter bindings are not currently checked at compile time.
rust,ignore
let people = select!(Vec<Person> "WHERE age > ?", 21);
↓
sql
SELECT rowid, name, age, image_jpg FROM person WHERE age > ?
At compile time, the #[derive(Turbosql)]
macro runs and creates a migrations.toml
file in your project root that describes the database schema.
Each time you change a struct
declaration and the macro is re-run (e.g. by cargo
or rust-analyzer
), migration SQL statements are generated that update the database schema. These new statements are recorded in migrations.toml
, and are automatically embedded in your binary.
```rust
struct Person {
rowid: Option
↓ auto-generates migrations.toml
toml
migrations_append_only = [
'CREATE TABLE person(rowid INTEGER PRIMARY KEY) STRICT',
'ALTER TABLE person ADD COLUMN name TEXT',
]
output_generated_schema_for_your_information_do_not_edit = '''
CREATE TABLE person (
rowid INTEGER PRIMARY KEY,
name TEXT
) STRICT
'''
When your schema changes, any new version of your binary will automatically migrate any older database file to the current schema by applying the appropriate migrations in sequence.
This migration process is a one-way ratchet: Old versions of the binary run on a database file with a newer schema will detect a schema mismatch and will be blocked from operating on the futuristically-schema'd database file.
Unused or reverted migrations that are created during development can be manually removed from migrations.toml
before being released, but any database files that have already applied these deleted migrations will error and must be rebuilt. Proceed with care. When in doubt, refrain from manually editing migrations.toml
, and everything should work fine.
struct
s.migrations.toml
file that is generated in your project root to see what's happening.The SQLite database file is created in the directory returned by directories_next::ProjectDirs::data_dir()
+ your executable's filename stem, which resolves to something like:
Linux | `$XDG_DATA_HOME`/`{exe_name}` or `$HOME`/.local/share/`{exe_name}` _/home/alice/.local/share/fooapp/fooapp.sqlite_ |
macOS | `$HOME`/Library/Application Support/`{exe_name}` _/Users/Alice/Library/Application Support/org.fooapp.fooapp/fooapp.sqlite_ |
Windows | `{FOLDERID_LocalAppData}`\\`{exe_name}`\\data _C:\Users\Alice\AppData\Local\fooapp\fooapp\data\fooapp.sqlite_ |
Primitive type | ```rust,ignore let result = select!(String "SELECT name FROM person")?; ``` Returns one value cast to specified type, returns `Error` if no rows available. ```rust,ignore let result = select!(String "name FROM person WHERE rowid = ?", rowid)?; ``` `SELECT` keyword is **always optional** when using `select!`; it's added automatically as needed. Parameter binding is straightforward. |
Vec<_> | ```rust,ignore let result = select!(Vec |
Option<_> | ```rust,ignore let result = select!(Option |
Your struct | ```rust,ignore let result = select!(Person "WHERE name = ?", name)?; ``` Column list and table name are optional if type is a `#[derive(Turbosql)]` struct. ```rust,ignore let result = select!(Vec Implement `Default` to avoid specifying unused column names. (And, of course, you can put it all in a `Vec` or `Option` as well.) ```rust,ignore let result = select!(Vec |
Your choice, but you definitely do not want to capitalize any of the other letters in the name! ;)