GOOD-ORMNING

Good-ormning is an ORM, probably? In a nutshell:

  1. Define schemas and queries in build.rs
  2. Good-ormning generates a function to set up/migrate the database
  3. Good-ormning generates functions for each query

Why you want it

Features

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.

Current status

Supported databases

Getting started

First time

  1. You'll need the following runtime dependencies:

    And build.rs dependencies:

    And you must enable one (or more) of the database features:

    plus maybe chrono for DateTime support.

  2. Create a build.rs and define your initial schema version and queries

  3. Call goodormning::generate() to output the generated code
  4. In your code, after creating a database connection, call migrate

Schema changes

  1. Copy your previous version schema, leaving the old schema version untouched. Modify the new schema and queries as you wish.
  2. Pass both the old and new schema versions to goodormning::generate(), which will generate the new migration statements.
  3. At runtime, the migrate call will make sure the database is updated to the new schema version.

Example

This build.rs file

```rust use std::{ path::PathBuf, env, }; use good_ormning::sqlite::{ Version, schema::{ field::, constraint::, }, query::{ expr::, select::, }, * };

fn main() { println!("cargo:rerun-if-changed=build.rs"); let root = PathBuf::from(&env::var("CARGOMANIFESTDIR").unwrap()); let mut latestversion = Version::default(); let users = latestversion.table("zQLEK3CT0", "users"); let id = users.rowidfield(&mut latestversion, None); let name = users.field(&mut latestversion, "zLQI9HQUQ", "name", fieldstr().build()); let points = users.field(&mut latestversion, "zLAPH3H29", "points", fieldi64().build()); goodormning::sqlite::generate(&root.join("tests/sqlitegenhelloworld.rs"), vec![ // Versions (0usize, latestversion) ], vec![ // Latest version queries newinsert(&users, vec![(name.clone(), Expr::Param { name: "name".into(), type: name.type.type.clone(), }), (points.clone(), Expr::Param { name: "points".into(), type: points.type.type.clone(), })]).buildquery("createuser", QueryResCount::None),

    new_select(&users).where_(Expr::BinOp {
        left: Box::new(Expr::Field(id.clone())),
        op: BinOp::Equals,
        right: Box::new(Expr::Param {
            name: "id".into(),
            type_: id.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 something like:

```rust,ignore pub fn migrate(db: &mut rusqlite::Connection) -> Result<(), GoodError> { // ... }

pub fn create_user(db: &mut rusqlite::Connection, name: &str, points: i64) -> Result<(), GoodError> { // ... }

pub struct DbRes1 { pub name: String, pub points: i64, }

pub fn get_user(db: &mut rusqlite::Connection, id: i64) -> Result { // ... }

pub fn list_users(db: &mut rusqlite::Connection) -> Result, GoodError> { // ... } ```

And can be used like:

```rust,ignore fn main() { use sqlitegenhello_world as queries;

let mut db = rusqlite::Connection::open_in_memory().unwrap();
queries::migrate(&db).unwrap();
queries::create_user(&db, "rust human", 0).unwrap();
for user_id in queries::list_users(&db).unwrap() {
    let user = queries::get_user(&db, user_id).unwrap();
    println!("User {}: {}", user_id, user.name);
}
Ok(())

} ```

markdown User 1: rust human

Usage details

Features

Schema IDs and IDs

"Schema IDs" are internal ids used for matching fields across versions, to identify renames, deletes, etc. Schema IDs must not change once used in a version. I recommend using randomly generated IDs, via a key macro. Changing Schema IDs will result in a delete followed by a create.

"IDs" are used both in SQL (for fields) and Rust (in parameters and returned data structures), so must be valid in both (however, some munging is automatically applied to ids in Rust if they clash with keywords). Depending on the database, you can change IDs arbitrarily between schema versions but swapping IDs in consecutive versions isn't currently supported - if you need to do swaps do it over three different versions (ex: v0: A and B, v1: A_ and B, v2: B and A).

Query, expression and fields types

Use type_* field_* functions to get type builders for use in expressions/fields.

Use new_insert/select/update/delete to create query builders.

There are also some helper functions for building queries, see

for the database you're using.

Custom types

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).

The type must have methods to convert to/from the native SQL types. There are traits to guide the implementation:

```rust pub struct MyString(pub String);

impl goodormningruntime::pg::GoodOrmningCustomString for MyString { fn to_sql(value: &MyString) -> &str { &value.0 }

fn from_sql(s: String) -> Result<MyString, String> {
    Ok(Self(s))
}

} ```

Parameters and return types

Parameters with the same name are deduplicated - if you define a query with multiple parameters of the same name but different types you'll get an error.

Different queries with the same multiple-field returns will use the same return type.

Comparisons

Vs Diesel

Good-ormning is functionally most similar to Diesel.

Diesel

Good-ormning

Vs SQLx

SQLx

Good-ormning

Vs SeaORM

SeaORM focuses on runtime checks rather than compile time checks.

A few words on the future

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.