Skip to content

Shuozeli/quiver-orm

Repository files navigation

Quiver

An Arrow-native ORM for Rust. Schema types map 1:1 to Apache Arrow types, database connectivity goes through ADBC, and code generation produces FlatBuffers, Protobuf, Rust, TypeScript, and SQL DDL from a single .quiver schema file.

Why

Traditional ORMs have an impedance mismatch: application types are converted to ORM types, then to SQL types, then to wire format, then to database types. Every conversion is lossy.

Quiver eliminates this by using the Arrow type system end-to-end:

Quiver schema types  ==  Arrow types  ==  ADBC wire types  ==  Database types
     Int16                 Int16             Int16                SMALLINT
     Decimal128(10,2)      Decimal128(10,2)  Decimal128(10,2)     NUMERIC(10,2)
     Timestamp(us, UTC)    Timestamp(us,UTC) Timestamp(us,UTC)    TIMESTAMPTZ
     List<Utf8>            List<Utf8>        List<Utf8>           TEXT[]

Zero lossy conversions from schema to wire to database.

Quick Start

1. Define your schema

// schema.quiver
config {
    provider "postgresql"
    url      "postgresql://localhost/myapp"
}

enum Role {
    User
    Admin
    Moderator
}

model User {
    id       Int32                         PRIMARY KEY AUTOINCREMENT
    email    Utf8                          UNIQUE
    name     Utf8?
    balance  Decimal128(10, 2)             DEFAULT 0
    active   Boolean                       DEFAULT true
    created  Timestamp(Microsecond, UTC)   DEFAULT now()
    role     Role                          DEFAULT User

    FOREIGN KEY (role) REFERENCES Role (name)
    INDEX (email)
    MAP "users"
}

model Post {
    id        Int32    PRIMARY KEY AUTOINCREMENT
    title     Utf8
    content   LargeUtf8?
    published Boolean  DEFAULT false
    authorId  Int32

    FOREIGN KEY (authorId) REFERENCES User (id)
    INDEX (authorId)
}

2. Generate code

# Type-safe Rust client (recommended -- generates typed queries, filters, structs)
quiver generate schema.quiver -t rust-client -o src/generated/

# Rust serde structs + TryFrom<&Row> deserialization
quiver generate schema.quiver -t rust-serde -o generated/

# TypeScript types
quiver generate schema.quiver -t typescript -o generated/

# FlatBuffers / Protobuf (wire formats)
quiver generate schema.quiver -t flatbuffers -o generated/
quiver generate schema.quiver -t protobuf -o generated/

# SQL DDL (per dialect)
quiver generate schema.quiver -t sql-postgres -o generated/
quiver generate schema.quiver -t sql-mysql -o generated/
quiver generate schema.quiver -t sql-sqlite -o generated/

3. Manage database

# Push schema directly to database
quiver db push schema.quiver

# Or use migrations
quiver migrate create schema.quiver add_users
quiver migrate apply schema.quiver
quiver migrate status schema.quiver
quiver migrate rollback schema.quiver

# Introspect existing database
quiver db pull schema.quiver -o introspected.quiver

# Run raw SQL
quiver db execute schema.quiver "SELECT * FROM users"

4. Query with the generated client

// Generated by: quiver generate schema.quiver -t rust-client -o src/generated/
mod generated;
use generated::user;
use quiver_driver_core::{Driver, QuiverClient};
use quiver_driver_sqlite::SqliteDriver;

// Connect and wrap in QuiverClient (enforces transactional access)
let mut client = QuiverClient::new(SqliteDriver.connect(":memory:").await?);

// All data operations must be in a transaction
// Field names are compile-time constants -- typos are compile errors
let rows = client.transaction(|tx| Box::pin(async move {
    let query = user::find_many()
        .select(&[user::fields::ID, user::fields::EMAIL, user::fields::NAME])
        .filter(user::filter::active_is_true())
        .filter(user::filter::balance_gte(100.0))
        .order_by(user::order::created_asc())
        .limit(10)
        .build();

    tx.query(&query).await
})).await?;

// Typed create -- required fields enforced by the compiler
let data = user::CreateData {
    email: "alice@example.com".into(),
    name: Some("Alice".into()),
    balance: None,  // uses schema default
    // ... all required fields must be set -- compiler enforces this
};
client.transaction(|tx| Box::pin(async move {
    tx.execute(&data.to_query()).await
})).await?;

Workspace

quiver/
  quiver-schema/           Schema parser, lexer, AST, validation, Arrow type mapping
  quiver-codegen/          Code generation (FBS, Proto, Rust client, Rust serde, TS, SQL DDL)
  quiver-query/            Type-safe query builder, pagination, relations, streaming
  quiver-driver-core/      Dialect trait, generic AdbcConnection<D>, DriverPool<D>,
                           QuiverClient, Value/Row/RowStream, BoxFuture-based traits
  quiver-driver-sqlite/    SQLite driver (type alias to AdbcConnection<SqliteDialect>)
  quiver-driver-postgres/  PostgreSQL driver (type alias to AdbcConnection<PostgresDialect>)
  quiver-driver-mysql/     MySQL driver (type alias to AdbcConnection<MysqlDialect>)
  quiver-migrate/          Schema diffing, DDL generation, migration tracking, introspection
  quiver-e2e/              End-to-end integration tests (SQLite)
  quiver-cli/              CLI binary (parse, generate, migrate, db)
  quiver-error/            Shared QuiverError enum with retry detection
  arrow-adbc-rs/           Git submodule: clean-room ADBC core + drivers

Type System

Every Quiver type maps 1:1 to an arrow_schema::DataType:

Category Types
Integers (signed) Int8, Int16, Int32, Int64
Integers (unsigned) UInt8, UInt16, UInt32, UInt64
Floating point Float16, Float32, Float64
Decimal Decimal128(p, s), Decimal256(p, s)
String Utf8, LargeUtf8
Binary Binary, LargeBinary, FixedSizeBinary(n)
Boolean Boolean
Temporal Date32, Date64, Time32(unit), Time64(unit), Timestamp(unit, tz)
Nested List<T>, LargeList<T>, Map<K, V>, Struct<{...}>
Nullability Append ? to any type

Schema Attributes

Field-level attributes are placed after the type on the same line. Model-level attributes are placed on their own lines within the model block.

Attribute Level Meaning
PRIMARY KEY field Primary key
AUTOINCREMENT field Auto-increment
UNIQUE field Unique constraint
DEFAULT value field Default value (literal, now(), uuid(), cuid())
FOREIGN KEY (col) REFERENCES Model (col) model Foreign key with optional ON DELETE/ON UPDATE
INDEX (col1, col2, ...) model Database index
MAP "name" model Database table name

SQL Injection Prevention

The query builder enforces injection safety at the type level:

  • Table and column names must be &'static str (compile-time literals)
  • SafeIdent validates that identifiers contain only safe characters
  • Values are always parameterized, never interpolated into SQL
  • Filter::raw() accepts only &'static str
  • TrustedSqlBuilder in migrations uses &'static str for SQL fragments
// OK: string literals
let q = Query::table("User").find_many().filter(Filter::eq("email", email)).build();

// WON'T COMPILE: runtime String is not &'static str
let table = format!("{}; DROP TABLE users", user_input);
let q = Query::table(&table).find_many().build(); // compile error

Supported Databases

Database Driver Crate Migration DDL Introspection ADBC TLS
SQLite quiver-driver-sqlite Yes Yes N/A
PostgreSQL quiver-driver-postgres Yes Yes (information_schema) Optional (tls feature)
MySQL quiver-driver-mysql Yes Yes (information_schema) --
FlightSQL (via adbc-flightsql) -- -- Optional (tls feature)

Each driver provides two APIs:

// QuiverClient API (transactional, Value/Row types)
use quiver_driver_postgres::PostgresDriver;
use quiver_driver_core::{Driver, QuiverClient};
let client = QuiverClient::new(PostgresDriver.connect("postgresql://localhost/myapp").await?);

// ADBC API (Arrow RecordBatch, bulk ingest, COPY protocol)
use quiver_driver_postgres::adbc_postgres;

ADBC Features

The ADBC layer (arrow-adbc-rs submodule) provides:

  • Arrow-native data access -- query results as RecordBatch, no row-by-row conversion
  • Bulk ingest -- PostgreSQL uses COPY FROM STDIN for high-throughput loading
  • Compile-time SQL safety -- TrustedSql type and trusted_sql! macro prevent injection in ADBC queries
  • TLS support -- optional for PostgreSQL (native-tls) and FlightSQL (tonic TLS)

Development

cargo build --workspace
cargo test --workspace          # 407 tests
cargo clippy --all-targets -- -D warnings

Comparison

Feature Prisma (TS) Diesel SQLx SeaORM Quiver
Type system Custom (lossy) SQL-mapped SQL-mapped Custom (lossy) Arrow (lossless)
Schema language .prisma Rust macros None (SQL) Rust macros .quiver (Arrow types)
Wire format JSON DB protocol DB protocol DB protocol Arrow (columnar)
Zero-copy results No No No No Yes (FlatBuffers)
Columnar access No No No No Yes (RecordBatch)
Multi-database Yes (adapters) No Limited Limited Yes (ADBC)
Remote/distributed No No No No Yes (FlightSQL)
Nested types JSON blob Limited Limited JSON blob First-class (List/Map/Struct)
Code generation TS only Rust macros None Rust macros Typed client + FBS + Proto + TS
SQL injection prevention Runtime Compile-time Compile-time Runtime Compile-time (&'static str)

Design Principles

  1. Arrow IS the type system. No mapping layer. Schema types are Arrow types.
  2. ADBC is first-class. All drivers re-export their ADBC counterparts.
  3. Multiple backends from one schema. FlatBuffers, Protobuf, Rust serde, TypeScript, SQL DDL -- all from the same .quiver source.
  4. SQL injection impossible by construction. The type system prevents it.
  5. Opinionated transactions. All DB operations must be in a transaction.
  6. No implicit side effects. No automatic timestamps or UUIDs. Users provide values explicitly via composable helpers.
  7. Dialect-aware migrations. SQLite, PostgreSQL, and MySQL have separate DDL generation paths because their DDL is incompatible.

License

Apache-2.0

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors