Good-ormning is an ORM, probably? In a nutshell:
build.rs
Like other Rust ORMs, Good-ormning doesn't abstract away from actual database workflows, but instead aims to enhance type checking with normal SQL.
See Comparisons, below, for information on how Good-ormning differs from other Rust ORMs.
Alpha:
You'll need the following runtime dependencies:
tokio-postgres
for PostgreSQLrusqlite
for Sqlitehex_literal
if you use byte array literals in any queriesAnd build.rs
dependencies:
good-ormning
Create a build.rs
and define your initial schema version and queries
goodormning::generate()
to output the generated codemigrate
goodormning::generate()
, which will generate the new migration statements.migrate
call will make sure the database is updated to the new schema version.This build.rs
file
```rust fn main() { println!("cargo:rerun-if-changed=build.rs");
let mut latest_version = Version::default();
let users = latest_version.table("zQLEK3CT0");
let id = users.rowid();
let name = users.field(&mut latest_version, "zLQI9HQUQ", "name", field_str().build());
let points = users.field(&mut latest_version, "zLAPH3H29", "points", field_i64().build());
goodormning::sqlite::generate(&root.join("tests/sqlite_gen_hello_world.rs"), vec![
// Versions
(0usize, latest_version)
], vec![
// Queries
new_insert(&users, vec![(name.id.clone(), Expr::Param {
name: "name".into(),
type_: name.def.type_.type_.clone(),
}), (points.id.clone(), Expr::Param {
name: "points".into(),
type_: points.def.type_.type_.clone(),
})]).build_query("create_user", QueryResCount::None),
new_select(&users).where_(Expr::BinOp {
left: Box::new(Expr::Field(id.id.clone())),
op: BinOp::Equals,
right: Box::new(Expr::Param {
name: "id".into(),
type_: id.def.type_.type_.clone(),
}),
}).return_fields(&[&name, &points]).build_query("get_user", QueryResCount::One),
new_select(&users).return_field(&id).build_query("list_users", QueryResCount::Many)
]).unwrap();
} ```
Generates this code
```rust
pub struct GoodError(pub String);
impl std::fmt::Display for GoodError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } }
impl std::error::Error for GoodError { }
impl From
pub fn migrate(db: &mut rusqlite::Connection) -> Result<(), GoodError> { let txn = db.transaction().maperr(|e| GoodError(e.tostring()))?; match (|| { txn.execute("create table if not exists _goodversion (version bigint not null);", ())?; let mut stmt = txn.prepare("select version from _goodversion limit 1")?; let mut rows = stmt.query(())?; let version = match rows.next()? { Some(r) => { let ver: i64 = r.get(0usize)?; ver }, None => { let mut stmt = txn.prepare("insert into _goodversion (version) values (-1) returning version")?; let mut rows = stmt.query(())?; let ver: i64 = rows .next()? .okorelse(|| GoodError("Insert version failed to return any values".into()))? .get(0usize)?; ver }, }; if version < 0i64 { txn.execute( "create table \"zQLEK3CT0\" ( \"zLQI9HQUQ\" text not null , \"zLAPH3H29\" integer not null )", (), )?; } txn.execute("update _goodversion set version = $1", rusqlite::params![0i64])?; let out: Result<(), GoodError> = Ok(()); out })() { Err(e) => { match txn.rollback() { Err(e1) => { return Err( GoodError( format!("{}\n\nRolling back the transaction due to the above also failed: {}", e, e1), ), ); }, Ok() => { return Err(e); }, }; }, Ok() => { match txn.commit() { Err(e) => { return Err(GoodError(format!("Error committing the migration transaction: {}", e))); }, Ok(_) => { }, }; }, } Ok(()) }
pub fn createuser(db: &mut rusqlite::Connection, name: &str, points: i64) -> Result<(), GoodError> { db .execute( "insert into \"zQLEK3CT0\" ( \"zLQI9HQUQ\" , \"zLAPH3H29\" ) values ( $1 , $2 )", rusqlite::params![name, points], ) .maperr(|e| GoodError(e.to_string()))?; Ok(()) }
pub struct DbRes1 { pub name: String, pub points: i64, }
pub fn getuser(db: &mut rusqlite::Connection, id: i64) -> Result
pub fn listusers(db: &mut rusqlite::Connection) -> Result
And can be used like
```rust fn main() { use sqlitegenhello_world as queries;
let mut db = rusqlite::Connection::open_in_memory().unwrap();
queries::migrate(&mut db).unwrap();
queries::create_user(&mut db, "rust human", 0).unwrap();
for user_id in queries::list_users(&mut db).unwrap() {
let user = queries::get_user(&mut db, user_id).unwrap();
println!("User {}: {}", user_id, user.name);
}
Ok(())
} ```
User 1: rust human
In general in this library, IDs are SQL table/field/index/constrait/etc ids, and names are what's used in generated Rust functions and structs.
IDs must be stable. Migrations are based around stable ids, so if (for example) a table ID changes, this will be considered a delete of the table with the old id, and a create of a new table with the new id.
In the example above, I used randomly generated IDs which have this property. This has the downside that it makes the SQL CLI harder to use. It's possible this will be improved upon, but if you frequently need to do things from the CLI I suggest creating a custom CLI using generated queries.
Use type_*
field_*
functions to get expression/field type builders. Use new_insert/select/update/delete
to get a query builder for the associated query type.
When defining a field in the schema, call .custom("mycrate::MyString", type_str().build())
on the field type builder (or pass it in as Some("mycreate::MyType".to_string())
if creating the type structure directly).
Custom types need to implement functions like this:
```rust pub struct MyString(pub String);
impl MyString { pub fn to_sql(&self) -> &str { &self.0 }
pub fn from_sql(s: String) -> Result<Self, MyErr> {
Ok(Self(s))
}
} ```
Any std::err::Error
can be used for the error. The to_sql
result and from_sql
arguments should correspond to the base type you specified. If you're not sure what type that is, guess, and when you compile you'll get an compiler error saying which type you need.
Good-ormning is functionally most similar to Diesel.
build.rs
filebuild.rs
SeaORM focuses on runtime checks rather than compile time checks, so the focus is quite different.
Obviously writing an SQL VM isn't great. The ideal solution would be for popular databases to expose their type checking routines as libraries so they could be imported into external programs, like how Go publishes reusable ast-parsing and type-checking libraries.