GRoutines + ORM — A goroutine-native async ORM for Rust with multi-database support.
- Multi-database: PostgreSQL, MySQL, SQLite
- Goroutine-native: Built on gorust, no tokio required
- Chainable API:
where_eq().limit().offset().order().find() - Transactions:
Transaction::begin()with auto-rollback on drop - Auto table creation:
create_table()generates DDL from model definitions - Index & constraints:
#[index],#[unique],#[unique_index = "name"] - JOIN support:
left_join(),inner_join(),right_join() - IN queries:
where_in("name", vec![...]) - Connection pooling: gorust channel-based connection pool
- Derive macros:
#[derive(DeriveModel)]auto-generatesModeltrait
Add to your Cargo.toml:
[dependencies]
grorm = "0.1.0"
gorust = "1.5"use grorm::{ConnectionConfig, ConnectionPool, SqliteDriverFactory, QueryBuilder, Value, Error};
use grorm::DeriveModel;
use gorust::runtime;
#[derive(Debug, DeriveModel)]
#[table = "users"]
struct User {
id: i64,
#[index]
name: String,
#[unique]
email: String,
age: i32,
}
#[runtime]
fn main() -> Result<(), Error> {
let config = ConnectionConfig::sqlite("test.db");
let pool = ConnectionPool::new(SqliteDriverFactory, config, 4);
let mut conn = pool.get()?;
let mut qb = QueryBuilder::<User>::new(conn.driver_mut());
qb.create_table()?;
let user = User { id: 0, name: "Alice".into(), email: "alice@x.com".into(), age: 30 };
qb.insert(&user)?;
let users = qb.where_eq("name", Value::from("Alice")).find()?;
println!("{:?}", users);
Ok(())
}use grorm::{ConnectionConfig, ConnectionPool, PostgresDriverFactory, QueryBuilder, Value, Error};
use grorm::DeriveModel;
use gorust::runtime;
#[derive(Debug, DeriveModel)]
#[table = "users"]
struct User {
id: i64,
name: String,
email: String,
age: i32,
}
#[runtime]
fn main() -> Result<(), Error> {
let config = ConnectionConfig::postgres("localhost", 5432, "mydb", "user", "pass");
let pool = ConnectionPool::new(PostgresDriverFactory, config, 4);
let mut conn = pool.get()?;
let mut qb = QueryBuilder::<User>::new(conn.driver_mut());
qb.create_table()?;
qb.insert(&User { id: 0, name: "Alice".into(), email: "alice@x.com".into(), age: 30 })?;
let users = qb.find_all()?;
println!("{:?}", users);
Ok(())
}use grorm::{ConnectionConfig, ConnectionPool, MysqlDriverFactory, QueryBuilder, Value, Error};
use grorm::DeriveModel;
use gorust::runtime;
#[derive(Debug, DeriveModel)]
#[table = "users"]
struct User {
id: i64,
name: String,
email: String,
age: i32,
}
#[runtime]
fn main() -> Result<(), Error> {
let config = ConnectionConfig::mysql("localhost", 3306, "mydb", "user", "pass");
let pool = ConnectionPool::new(MysqlDriverFactory, config, 4);
let mut conn = pool.get()?;
let mut qb = QueryBuilder::<User>::new(conn.driver_mut());
qb.create_table()?;
qb.insert(&User { id: 0, name: "Alice".into(), email: "alice@x.com".into(), age: 30 })?;
let users = qb.find_all()?;
println!("{:?}", users);
Ok(())
}Use #[derive(DeriveModel)] to define your model:
use grorm::DeriveModel;
#[derive(Debug, DeriveModel)]
#[table = "users"] // Override table name (default: snake_case + "s")
#[primary_key = "uuid"] // Override primary key (default: "id")
struct User {
id: i64, // Auto-increment primary key (when id = 0)
#[index] // Create a regular index
name: String,
#[unique] // Create a unique constraint
email: String,
#[unique_index = "uq_name_age"] // Composite unique index (same group name)
first_name: String,
#[unique_index = "uq_name_age"]
last_name: String,
age: i32,
}| Attribute | Scope | Description |
|---|---|---|
#[table = "name"] |
struct | Override table name |
#[primary_key = "col"] |
struct | Override primary key column |
#[index] |
field | Create a regular index |
#[unique] |
field | Create a unique constraint |
#[unique_index = "name"] |
field | Group into composite unique index |
let mut qb = QueryBuilder::<User>::new(conn.driver_mut());
qb.create_table()?;let user = User { id: 0, name: "Alice".into(), email: "alice@x.com".into(), age: 30 };
let id = qb.insert(&user)?; // Returns Option<i64> (auto-generated id)// Find all
let all = qb.find_all()?;
// Find by id
let user = qb.find_by_id(1)?;
// Find one with conditions
let user = qb.where_eq("age", Value::from(30)).find_one()?;
// Find with conditions
let users = qb.where_eq("age", Value::from(30)).find()?;
// Find with column name
let users = qb.find_where("name", Value::from("Alice"))?;
// Find with model (non-zero fields become conditions)
let filter = User { id: 0, name: "".into(), email: "".into(), age: 30 };
let users = qb.where_model(&filter).find()?;
// IN query
let users = qb.where_in("name", vec![Value::from("Alice"), Value::from("Bob")]).find()?;
// Pagination
let users = qb.order("age", true).limit(10).offset(0).find()?;
// Count
let total = qb.count()?;// Update single column
let rows = qb.where_eq("name", Value::from("Alice"))
.update_one("age", Value::from(31))?;
// Update from model (non-zero/non-empty fields)
let update = User { id: 0, name: "".into(), email: "".into(), age: 31 };
let rows = qb.where_eq("name", Value::from("Alice"))
.update_model(&update)?;// Delete with conditions
let rows = qb.where_eq("name", Value::from("Alice")).delete()?;
// Delete all
let rows = qb.delete_all()?;let mut tx = Transaction::<User>::begin(conn.driver_mut())?;
tx.insert(&User { id: 0, name: "Alice".into(), email: "alice@x.com".into(), age: 30 })?;
tx.where_eq("name", Value::from("Bob")).update_one("age", Value::from(26))?;
tx.commit()?;
// If tx goes out of scope without commit, it auto-rolls backAll QueryBuilder methods are available on Transaction:
insert,find,find_all,find_one,find_by_id,find_wherewhere_eq,where_in,where_modelupdate_one,update_modeldelete,delete_allcount,limit,offset,order
let mut qb = QueryBuilder::<User>::new(conn.driver_mut());
qb.left_join("orders", "users.id = orders.user_id")
.inner_join("profiles", "users.id = profiles.user_id")
.right_join("scores", "users.id = scores.user_id");
let results = qb.find()?;// Create pool
let config = ConnectionConfig::sqlite("test.db");
let pool = ConnectionPool::new(SqliteDriverFactory, config, 4);
// Get connection (blocks if pool exhausted)
let mut conn = pool.get()?;
// Use driver directly
let mut qb = QueryBuilder::<User>::new(conn.driver_mut());All public APIs return Result<T, grorm::Error>. The Error enum covers:
| Variant | Description |
|---|---|
Connection |
Auth, network errors |
Query |
SQL syntax, constraint violations |
Execute |
Write operation errors |
Protocol |
Wire format, parsing errors |
Model |
Serialization/deserialization errors |
Pool |
Pool exhausted, closed |
Config |
Invalid DSN, configuration |
Io |
Wrapped I/O errors |
NotFound |
Entity not found |
Transaction |
Begin, commit, rollback errors |
grorm/
├── Cargo.toml
├── README.md
├── src/
│ ├── lib.rs # Library root, re-exports
│ ├── error.rs # Unified error types
│ ├── driver/ # Database driver abstraction
│ │ ├── mod.rs # ConnectionConfig, DatabaseDriver trait
│ │ ├── postgres.rs # PostgreSQL driver
│ │ ├── mysql.rs # MySQL driver
│ │ └── sqlite.rs # SQLite driver
│ ├── protocol/ # Database wire protocols
│ │ ├── mod.rs
│ │ ├── pg.rs # PostgreSQL protocol
│ │ ├── myproto.rs # MySQL protocol
│ │ └── sqlite_proto.rs # SQLite protocol (mock)
│ ├── query/ # Low-level SQL builders
│ │ ├── mod.rs
│ │ ├── select.rs
│ │ ├── insert.rs
│ │ ├── update.rs
│ │ └── delete.rs
│ ├── types/ # Type mapping (Rust ↔ SQL)
│ │ ├── mod.rs
│ │ ├── value.rs # Value enum
│ │ ├── from_sql.rs # FromSql trait
│ │ └── to_sql.rs # ToSql trait
│ ├── orm/ # ORM core
│ │ ├── mod.rs
│ │ ├── model.rs # Model trait, ColumnInfo
│ │ ├── query.rs # QueryBuilder (chainable API)
│ │ └── transaction.rs # Transaction support
│ └── pool/ # Connection pool (gorust channels)
│ └── mod.rs
├── grorm-macros/ # Procedural macros
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs # #[derive(DeriveModel)]
└── examples/
├── sqlite_demo.rs
├── postgres_demo.rs
└── mysql_demo.rs
MIT