Nest is a database-neutral ORM and safe SQL toolkit for the
Raven programming language. It provides
one query, result, model, and migration API over PostgreSQL, MySQL, and SQLite.
Its public Executor trait keeps the adapter boundary open to future Raven
database libraries.
Nest is under active initial development. It requires Raven 2.26.1 or later (the transaction helper passes a closure whose parameter is typed with the adapter, which earlier compilers could not resolve). The current foundation is tested end to end with Raven 2.26.1, PostgreSQL 16 and 17, MySQL 8.0.43, and bundled SQLite.
- Parameterized SELECT, INSERT, UPDATE, and DELETE builders.
- PostgreSQL, MySQL, and SQLite dialect-specific placeholders and identifier quoting.
- INNER and LEFT joins, aliases, predicates, ordering, limits, and offsets.
- IN and NOT IN predicates with parameterized lists. Empty
INmatches nothing; emptyNOT INkeeps every row. - INSERT ... RETURNING on PostgreSQL and SQLite to read generated columns.
- Transactions: begin, commit, rollback, and a transaction() helper that commits on success and rolls back on error.
- Typed text, integer, decimal, boolean, and SQL NULL values.
- A common result type with indexed and named row access.
- Compile-time model mapping through
FromRow. - Ordered, tracked, idempotent migrations and one-step rollback.
- Transactional migration application on PostgreSQL and SQLite.
- Thin adapters over
raven-postgres,raven-mysql, andraven-sqlite. - A public adapter contract for third-party database libraries.
Add Nest to rv.toml:
[dependencies]
"github.com/martian56/nest-orm" = "v0.2.0"Nest currently depends on version v0.2.0 of all three Raven database drivers.
import "github.com/martian56/raven-sqlite" { Db }
import "github.com/martian56/nest-orm" {
fetch_all,
insert_into,
insert_one,
integer,
select,
text,
}
import "github.com/martian56/nest-orm/sqlite" { sqlite }
fun main() {
match Db.open(":memory:") {
Ok(connection) -> {
let _ = connection.exec(
"CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)",
)
let db = sqlite(connection)
let _ = insert_one(db, insert_into("users").value("name", text("Ada")))
match fetch_all(
db,
select("users").column("id").column("name").where_eq("id", integer(1)),
) {
Ok(result) -> print(result.rows[0].get_named("name")),
Err(e) -> print(e.message()),
}
db.close()
},
Err(message) -> print(message),
}
}Use the corresponding adapter for a server connection:
import "github.com/martian56/nest-orm/postgres" { postgres }
import "github.com/martian56/nest-orm/mysql" { mysql }
let pg = postgres(postgres_connection)
let my = mysql(mysql_connection)The adapters own their connection wrapper. Call db.close() when finished.
The SQLite adapter also exposes db.try_close() when shutdown failures need to
be handled explicitly.
Values are always sent through driver parameters. Nest validates and quotes identifiers separately, so neither values nor identifiers can silently become arbitrary SQL syntax.
Passing null() to where_eq or where_not_eq produces IS NULL or
IS NOT NULL; ordered comparisons with NULL are rejected as mistakes.
let query = select("users")
.column("users.id")
.column_as("users.name", "display_name")
.left_join("profiles", "users.id", "profiles.user_id")
.where_eq("users.active", boolean(true))
.where_gt("users.age", integer(17))
.order_desc("users.id")
.limit(20)
.offset(40)
let users = fetch_all(db, query)?Writes use the same typed values:
let inserted = insert_one(
db,
insert_into("users")
.value("name", text("Grace"))
.value("nickname", null()),
)?
let changed = update_all(
db,
update("users").set("active", boolean(false)).where_eq("id", integer(7)),
)?
let deleted = delete_all(db, delete_from("users").where_eq("id", integer(7)))?inserted, changed, and deleted are QueryResult values. Their affected
field reports changed rows. last_insert_id is populated by MySQL and SQLite.
PostgreSQL and SQLite can return generated values in the same round trip:
let inserted = insert_one(
db,
insert_into("users").value("name", text("Ada")).returning("id"),
)?
let id = inserted.rows[0].int_named("id")?MySQL does not support INSERT ... RETURNING; Nest reports that combination at
query compilation, and MySQL-generated ids remain available as
last_insert_id.
Use transaction to commit a successful closure or roll back its error:
let result = transaction(db, fun(tx: Sqlite) -> Result<QueryResult, Error> =
insert_one(tx, insert_into("users").value("name", text("Grace")))
)?begin, commit, and rollback are also public for explicit control. The
helper preserves the closure's original error if its best-effort rollback also
fails.
The default mysql(connection) adapter matches MySQL's standard string-literal
rules. If the connection session enables NO_BACKSLASH_ESCAPES, construct it
with mysql_no_backslash_escapes(connection) so placeholders inside raw SQL are
scanned with the same rules. Builder-generated queries remain parameterized in
either mode.
Raven models implement FromRow. This makes field conversion explicit and
compile-time checked today, while leaving room for a derive macro later.
struct User {
id: Int,
name: String,
}
impl FromRow for User {
fun from_row(row: Row) -> Result<User, Error> {
let id = row.int_named("id")?
let name = row.required("name")?
return Ok(User { id, name })
}
}
let result = fetch_all(db, select("users").column("id").column("name"))?
let users: List<User> = map_rows(result)?required, optional, int_named, float_named, and bool_named report
missing columns, unexpected NULL values, and conversion failures as Nest
errors. get and get_named remain available when raw text access is useful.
Migration definitions are ordinary Raven values, so a compiled migration program contains its complete schema history:
import "github.com/martian56/nest-orm/migrations" { migrate, migration }
let migrations = [
migration(1, "create users")
.up("CREATE TABLE users (id BIGINT PRIMARY KEY, name VARCHAR(255) NOT NULL)")
.down("DROP TABLE users"),
migration(2, "index user names")
.up("CREATE INDEX users_name_idx ON users (name)")
.down("DROP INDEX users_name_idx"),
]
let report = migrate(db, migrations)?Nest creates nest_migrations, applies only missing versions, and requires the
input list to contain unique positive versions in increasing order.
rollback_last(db, migrations) reverts the latest applied migration.
Nest rejects an applied migration whose version is missing from the compiled
history or whose name changed, preventing silent history drift.
PostgreSQL and SQLite migrations run in a transaction. MySQL automatically commits many DDL statements, so Nest records a MySQL migration only after all of its statements succeed but cannot roll back already committed MySQL DDL.
This library API is designed for a small compiled migration application. An rvpm registered command can build and run that application without rvpm or Nest needing runtime plugin discovery.
An adapter wraps its driver connection and implements two methods:
trait Executor {
fun dialect(self) -> Dialect
fun run(self, query: CompiledQuery) -> Result<QueryResult, Error>
}The adapter translates CompiledQuery.params into its driver's bound
parameter representation and converts driver rows into Nest Row values. A
third-party adapter can live in its own repository; Nest's query and migration
engines do not need to know its concrete connection type.
Run fast tests, including in-memory SQLite integration tests:
rvpm fmt --check
rvpm build
rvpm test
rvpm docRun the complete PostgreSQL, MySQL, and SQLite suite with Docker:
./scripts/test-e2e.ps1./scripts/test-e2e.shPass -Keep to the PowerShell script or set KEEP_NEST_DATABASES=1 for the
shell script to keep the database containers running after tests.
- Values use the text parameter formats exposed by the three current drivers.
- Results are collected in memory; streaming cursors are not implemented.
- Upserts, aggregates, subqueries, connection pools, relations, eager loading, and derive-generated model mappings are not implemented yet.
- Migration SQL is intentionally explicit because column types and many DDL operations differ across databases.
MIT. See LICENSE.