v0.8.0
The headline of this release is the new executable client API β
Prisma-style PraxClient<E> with per-model accessors that run
(client.user().find_many()...) through a typed QueryEngine
instead of returning inert SQL strings. The driver layer was rewritten
from scratch to back it: typed row decoding via FromRow/RowRef
bridges on all four SQL drivers, a SqlDialect abstraction so filter
SQL emits the right placeholder/quoting/upsert syntax per backend,
real transactions, aggregate/group_by execution, cross-dialect
upsert, and a typed query_raw/execute_raw escape hatch on
PraxClient.
Added
PraxClient<E>andprax::client!(Model, ...)macro β top-level
client grouping per-model accessors. The macro emits a sealed
PraxClientExttrait and implements it forPraxClient<E>so
callers writeclient.user()/client.post()without inherent
implblocks on a foreign type.- Per-model
Client<E>emitted by#[derive(Model)]and by
prax_schema!β exposesfind_many,find_unique,find_first,
create,create_many,update,update_many,upsert,delete,
delete_many,count,aggregate,group_by. Each accessor clones
the engine and hands it to the matching operation builder. prax-query::dialect::SqlDialecttrait β new module with
Postgres/Sqlite/Mysql/Mssql/NotSqlimplementations.
Attached toQueryEngine::dialect(). Each dialect drives placeholder
syntax ($1/?/?N/@P1),RETURNINGvsOUTPUT INSERTED,
upsert clause shape, transaction statements, and identifier quoting.
Marked#[non_exhaustive]so additional dialects can be added
without a breaking release.ToFilterValuetrait +ModelWithPkβ reverse ofFromColumn
used by the relation executor and by upsert to extract PK/FK values.RelationMeta+ per-relation codegen modules
(user::posts::fetch(),user::posts::Relation) β declarative
relation metadata emitted from
#[prax(relation(target = ..., foreign_key = ...))]..include(spec)onfind_many/find_unique/find_firstβ
eager-loads BelongsTo / HasOne / HasMany relations with one
follow-upIN (β¦)query per relation.- Real transactions on all four SQL drivers:
PraxClient::transaction(|tx| async { ... }).awaitcommits onOk
and rolls back onErr. Nestedtransaction()on the same engine
currently returnsQueryError::internal(...)until dialect-aware
SAVEPOINT support lands. - Cross-dialect upsert:
ON CONFLICT ... DO UPDATE
(Postgres / SQLite) /ON DUPLICATE KEY UPDATE(MySQL). Routed
through the engine with the dialect's conflict clause spliced in
by the builder. - Cross-dialect aggregate + group_by execution via
QueryEngine::aggregate_query. - Nested writes:
.create().with(user::posts::create(vec![...]))
issues child inserts inside an implicit transaction. - Typed raw SQL escape hatch:
PraxClient::query_raw<T>(Sql)and
PraxClient::execute_raw(Sql). Rows route through the same
FromRowbridge the derived models use, so the result stays typed. prax-query::row::FromRow+RowRefβ expanded with
default-erroring getters forchrono::DateTime<Utc>,
chrono::NaiveDateTime,chrono::NaiveDate,chrono::NaiveTime,
uuid::Uuid,rust_decimal::Decimal,serde_json::Valueand their
Option<T>variants. Drivers override the ones they support
natively.prax-query::row::into_row_errorβ helper for driverRowRef
bridges that maps anyDisplayerror into a
RowError::TypeConversion.prax-{postgres,sqlite,mysql,mssql}row_ref modules β typed row
bridges (PgRow,SqliteRowRef,MysqlRowRef,MssqlRowRef).prax-{postgres,sqlite,mysql,mssql}::*Engineβ implement
QueryEnginetrait with typed row decoding viaFromRow.#[derive(Model)]β emitsimpl prax_query::traits::Modeland
impl prax_query::row::FromRowalongside the legacyPraxModel
marker. Also emits per-field filter operator constructors
(user::age::gt(18), etc.) that classify field types into
Numeric / String / Boolean / Other buckets.FilterValueFromimpls β signed and unsigned integer widths,
f32,chrono::DateTime<Utc>,chrono::NaiveDateTime,
chrono::NaiveDate,chrono::NaiveTime,uuid::Uuid,
rust_decimal::Decimal,serde_json::Value.- Integration tests against live Postgres, MySQL, SQLite, and MSSQL
containers, gated onPRAX_E2E=1+#[ignore]so the default
cargo testrun stays fast. Covers CRUD, upsert, aggregate,
transaction commit/rollback, and select projection. examples/client_crud_postgres.rsβ runnable end-to-end demo
that walks the full CRUD cycle against docker-compose Postgres.- TypeScript Generator (
prax-typegenv0.1.0) β standalone crate
for generating TypeScript from Prax schemas.- TypeScript interface generation for models, enums, composite
types, and views. - Zod schema generation with runtime validation and inferred types.
CreateInputandUpdateInputvariants for each model.- Lazy
z.lazy()references for relation fields. - CLI binary installable via
cargo install prax-typegen.
- TypeScript interface generation for models, enums, composite
- Schema Generator Blocks (
prax-schema) β first-classgenerator
block support in.praxfiles.generate = env("VAR")toggle: enable/disable generators via
environment variables.generate = true/falseliteral toggle.- Parsed into
GeneratorAST withprovider,output,generate,
and arbitrary properties. Schema::enabled_generators()for runtime filtering.
Changed (BREAKING)
prax-query::traits::QueryEngineβ row-returning methods now
requireT: FromRow. Add#[derive(Model)](which emitsFromRow)
or a hand-writtenimpl FromRow for MyModel. Every operation
builder propagates the bound. Driver impls route rows through the
RowRefbridge instead of JSON.prax-query::traits::QueryEngineβ newdialect()method on the
trait. Has a default returning the inertNotSqldialect, so
existing implementors continue to compile β but every SQL-backed
engine must override it or SQL building will panic at runtime.prax-query::filter::Filter::to_sqlβ signature gained a
dialect: &dyn SqlDialectparameter. Callers must pass their
engine's dialect (or a literal&prax_query::dialect::Postgresif
wedded to that backend).prax-query::filter::Filter::to_sqlβ column names are now
quoted throughdialect.quote_identbefore being interpolated into
SQL (SQL-injection fix). Generated SQL now reads"col" = $1on
Postgres (wascol = $1),`col` = ?on MySQL,[col] = @P1
on MSSQL. Tests that matched the unquoted form need updating.prax-mysql/prax-sqliteengines β rewritten to return typed
rows (T: FromRow) instead of JSON blobs. The legacy JSON surface
moved toprax_mysql::raw::MysqlRawEngine+
prax_mysql::raw::MysqlJsonRow(and the equivalent for SQLite).
Callers that wanted JSON:use prax_{mysql,sqlite}::raw::{MysqlRawEngine, MysqlJsonRow}.prax-mysql::MysqlEngineinherent methods removed β the old
query(sql, params) -> Vec<RowData>,
query_one(sql, params) -> RowData,
query_opt(sql, params) -> Option<RowData>no longer exist. They
are replaced by theQueryEnginetrait methodsquery_many::<T>,
query_one::<T>,query_optional::<T>, each of which requires
T: Model + FromRow. Callers consuming rawRowData/
serde_json::Valuemust either migrate to a typed model via
#[derive(Model)], bridge throughprax_mysql::row_ref::MysqlRowRef
in a hand-writtenFromRow, or switch to
prax_mysql::raw::MysqlRawEnginefor the legacy JSON API.
Side-effecting SQL that returns no rows should call
QueryEngine::execute_raw.prax-sqlite::SqliteEngineinherent methods removed β same
breakage asMysqlEngine. The oldquery/query_one/
query_optare gone; usequery_many::<T>/query_one::<T>/
query_optional::<T>withT: Model + FromRow, bridge via
prax_sqlite::row_ref::SqliteRowRef::from_rusqlitefor ad-hoc typed
rows, or fall back toprax_sqlite::raw::SqliteRawEnginefor the
JSON API.prax-mysql::MysqlQueryResult/prax-sqlite::SqliteQueryResult
β types removed from public re-exports. Renamed to
prax_{mysql,sqlite}::raw::{MysqlJsonRow, SqliteJsonRow}.#[derive(Model)]now emitsFromRowin addition toModelβ
the derive expands to bothimpl prax_query::traits::Model for β¦
andimpl prax_query::row::FromRow for β¦. If you had a
hand-writtenimpl Model for β¦orimpl FromRow for β¦for a type
that also carries the derive, the two impls will conflict (E0119).
Delete the hand-written impl and rely on the derive, or drop the
derive and keep the hand-written impls.#[derive(Model)]now emits a lowercase-struct module β
alongside the per-field filter constructors, the derive emits
mod <lowercase_struct_name> { pub mod <field> { fn equals, gt, lt, β¦ } }.
Crates that already define a module named the same as the lowercase
form of a derived struct (e.g., a structUserplus a local
mod user { β¦ }) will see anE0428duplicate-definition error.
Rename one of them.FilterValue::from::<u64>β values greater thani64::MAXnow
panic instead of silently clamping (previously an auth-bypass
footgun). Callers that pass untrustedu64inputs must validate
the range before conversion, or switch to
FilterValue::Int(value as i64)with their own clamp policy.- Postgres driver integer width narrowing β
FilterValue::Intis
narrowed to the target column width at bind time (INT2 / INT4 /
INT8). EliminatesWrongType { postgres: Int4, rust: "i64" }
errors when filtering on integer PKs. - MSSQL
OUTPUT INSERTED.*clause order β rearranged into the
correct T-SQL position (between(cols)andVALUESon
INSERT; betweenSETandWHEREonUPDATE). - MySQL stopped emitting
RETURNINGβ MySQL 8.0 doesn't support
it (that's a MariaDB extension). The engine now re-SELECTs after
INSERTviaLAST_INSERT_ID().
Removed
- Legacy
Actions/Queryinert helpers emitted by the codegen
β they returned SQL strings without an attached engine and are
fully subsumed by the new executableClient<E>. #[derive(Model)]phantomincrement/decrementhelpers β
the derive no longer emits helpers that called a non-existent
super::<field>::get_current_value()function.
Migration Guide
If you implement QueryEngine for a custom SQL backend:
- Add
fn dialect(&self) -> &dyn SqlDialect { &prax_query::dialect::Postgres }(or the dialect you target). - Ensure every type passed to
query_many::<T>,query_one::<T>, etc. implementsFromRow. Use#[derive(Model)].
If you use prax-mysql or prax-sqlite:
- For typed rows (new default): no change β your
find_many::<User>()etc. now return typed models. - For JSON blobs (legacy): import
MysqlRawEngine/SqliteRawEnginefrom therawmodule.
If you call Filter::to_sql directly:
- Update to
filter.to_sql(offset, &prax_query::dialect::Postgres)(or your dialect).
If you called MysqlEngine/SqliteEngine inherent methods directly:
// BEFORE (0.6)
let rows: Vec<RowData> = engine.query("SELECT * FROM users", vec![]).await?;
// AFTER (0.7) β with #[derive(Model)]
#[derive(prax_orm::Model)]
#[prax(table = "users")]
struct User {
#[prax(id)]
id: i32,
email: String,
}
let rows: Vec<User> = engine
.query_many::<User>("SELECT id, email FROM users", vec![])
.await?;
// AFTER (0.7) β ad-hoc typed row without the Model derive
use prax_mysql::row_ref::MysqlRowRef;
use prax_query::row::{FromRow, RowError, RowRef};
use prax_query::traits::Model;
struct UserSummary { id: i32, email: String }
impl Model for UserSummary {
const MODEL_NAME: &'static str = "UserSummary";
const TABLE_NAME: &'static str = "users";
// β¦ fill in the remaining associated items per the trait β¦
}
impl FromRow for UserSummary {
fn from_row(row: &dyn RowRef) -> Result<Self, RowError> {
Ok(Self {
id: row.get_i32("id")?,
email: row.get_string("email")?,
})
}
}
let rows: Vec<UserSummary> = engine
.query_many::<UserSummary>("SELECT id, email FROM users", vec![])
.await?;The SQLite bridge is identical apart from the row-ref import:
use prax_sqlite::row_ref::SqliteRowRef; and, inside a raw-row
callback, build the ref via SqliteRowRef::from_rusqlite(&row).
If you need the old untyped JSON-blob behavior, switch to
prax_mysql::raw::MysqlRawEngine / prax_sqlite::raw::SqliteRawEngine;
those retain the legacy API.
QueryEngine::query_one behavior when the SQL returns 2+ rows is driver-dependent: Postgres errors (strict), while MySQL/SQLite/MSSQL silently return the first row. Callers that require "exactly one row or error" should add LIMIT 2 (or TOP 2 on MSSQL) and check the row count themselves, or use count/query_many + assert len() == 1.
find_many().select([...]) (and find_first / find_unique) now narrows
the emitted SQL column list instead of always sending SELECT *. The
returned rows are still decoded as whole T structs, so every
non-Option field on T must appear in the SELECT list β otherwise
you'll see RowError::ColumnNotFound (or a driver-level "column does not
exist" surfaced through RowError::TypeConversion) when FromRow tries
to read the missing column. Proper partial hydration (per-field
Option<T> decoding that treats absent columns as None) is a
follow-up; this change gets the easy 50% (narrower bandwidth) with no
partial-struct complexity. Leave .select(...) unset to keep the old
SELECT * behavior.