# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.12.0] - 2026-08-22
### Breaking
- **workspace**: MSRV raised from 1.89 to 1.93.1, which is why this release is
0.12.0 rather than the 0.11.2 it was prepared as. `postgres_rustls` 0.1.5
raised its own `rust-version` to 1.93.1 in a patch release, and the `tls`
feature pins it exactly, so cargo's resolver rejects the workspace under
rustc 1.89. Raising the floor keeps the crate on a supported TLS stack; the
alternative was freezing `postgres_rustls` at 0.1.4 (MSRV 1.86). The floor is
1.93.1 and not 1.93 because cargo reads `1.93` as 1.93.0, which
`postgres_rustls` still rejects.
0.11.2 was tagged and merged but never published, so no released version ever
carried the inconsistent `rust-version = "1.89"` against a `=0.1.5` pin.
### Added
- **postgres**: `sslrootcert` support, as a URL parameter
(`?sslrootcert=/path/to/ca.pem`), a `PgConfig::ssl_root_cert` field and a
`PgConfigBuilder::ssl_root_cert` setter. The certificates in the bundle
*replace* the Mozilla root store rather than adding to it, matching libpq: a
pool addresses one server, so "trust exactly this bundle" is the stricter and
more predictable reading, and a mistyped path cannot silently fall back to
public trust. `prax_postgres::tls::make_tls_connector_with_root_cert` exposes
the same behaviour to downstream tooling; `make_tls_connector` is unchanged.
This closes a gap that made some managed databases unreachable. Verification
has always been against `webpki-roots`, so a server whose CA is deliberately
not publicly trusted could not be verified at all — and with no
encrypt-without-verify mode there was no working configuration. Amazon RDS is
the common case: its `rds-ca-*` authorities are Amazon-operated and absent
from the Mozilla store, so with `rds.force_ssl` enabled every `sslmode` value
failed, the TLS-requiring ones on chain verification and the plaintext ones by
server refusal.
A bad bundle now fails when the pool is built, naming the file, rather than
surfacing later as an opaque handshake error.
### Fixed
- `Cargo.lock` still pinned the workspace crates at 0.11.0 after the 0.11.1
release bumped `Cargo.toml`.
## [0.11.1] - 2026-07-26
## [0.11.0] - 2026-07-24
This release is the result of a full-project conformance audit against the
documented contracts (README, rustdoc, CLAUDE.md conventions) followed by a
multi-dimensional code review. Roughly 230 audit findings and ~65 review
findings were reconciled. **It contains breaking changes and security fixes
— read the Breaking and Security sections before upgrading.**
### Breaking
- **MongoDB collection resolution uses `Model::TABLE_NAME`.** Collections were
previously resolved by naively pluralizing the Rust type name
(`Category` → `categorys`), ignoring `#[prax(table = "…")]`. Existing
deployments whose collections were named by the old heuristic must rename
collections or pin the table name.
- **MongoDB `filter_value_to_bson` no longer coerces 24-hex strings to
`ObjectId`.** Coercion is now explicit via
`filter_value_to_bson_with_object_id` (used by the engine for `_id` only).
- **prax-sqlx `with_transaction_{pg,mysql,sqlite}` signature changed.** The
closure now receives `&mut sqlx::Transaction` and returns
`BoxFuture<'c, SqlxResult<T>>`, enabling real commit-on-Ok/rollback-on-Err
(the previous by-value closure could never commit). The `transaction`
module itself is newly public (it was never wired into `lib.rs` before).
- **prax-migrate `SqlDialect` is no longer a unit struct** (carries a
`SqlBackend`); `SqlBackend` is re-exported at the crate root.
- **prax-duckdb `DatabasePath::as_str` / `DuckDbConfig::path_str` return
`&OsStr`** (non-UTF-8 paths no longer silently open a fresh `:memory:` DB).
`ThreadMode` removed (was inert).
- **prax-codegen: legacy generated `Query`/`Actions` builders removed** from
`prax_schema!` model modules (unfiltered `to_select_sql` was a footgun);
generated view `Query` no longer accepts raw-string `where_conditions`;
unknown `#[prax(...)]` attribute keys are now compile errors (typo'd keys
like `unqiue` were silently ignored); `#[prax(schema = "…")]` and nested
`include:` blocks are explicit "not yet supported" errors instead of
silent no-ops.
- **`MongoEngine` no longer declares `SupportsNestedWrites`** (the capability
could never succeed — nested writes reject the `NotSql` dialect).
- **Fake-success paths now fail loudly** (previously silent no-ops reporting
success): `MigrationEngine::migrate/rollback` (never executed SQL but
recorded applied/rolled-back; now `ExecutionUnavailable`), `dev()`/
`rollback_with_event()` (`NotImplemented`), the Redis cache backend
(constructs and errors with `CacheError::Backend` until a real client
lands), CLI `migrate dev/deploy/reset/resolve/rollback/history`,
`db push/execute` (now non-zero "not implemented"), and
`ShadowDatabase`'s lifecycle.
- **Emitted SQL changed on many paths** (placeholder numbering fixes,
dialect-correct identifier quoting, parameterized `HAVING`, `ESCAPE` on
LIKE filters, MySQL `INSERT IGNORE`, `DISTINCT ON` gated per dialect).
SQL snapshot tests will need updates.
- **prax-orm root features are real now.** `postgres`/`mysql`/`sqlite`/
`mssql`/`mongodb`/`duckdb`/`scylladb`/`cassandra`/`sqlx`/`pgvector` map to
optional dependencies with gated re-exports; the default build now
actually compiles `prax-postgres` (and the rustls stack below).
- **`TlsConfig::default().verify_hostname` (prax-cassandra) is now `true`**
(docs always claimed so; derived default was `false`).
- **Connect-time hard errors replace silent downgrades**: `ssl_enabled` on
prax-scylladb without the `ssl` feature, TLS on prax-cassandra config
(now supported — see Added), invalid keyspace identifiers, `min > max`
pool constraints, and invalid savepoint names all error instead of
proceeding insecurely.
### Security
- **Tenant middleware rewritten against a real SQL scanner.** The tenant
value is validated per column type (UUID/`i64` parsed, strings restricted
to `^[A-Za-z0-9_\-\:.@]+$`), the existing predicate is parenthesized
(a `WHERE a OR b` can no longer bypass the tenant filter), clause
placement handles `GROUP BY`/`HAVING`, unrecognized statement shapes
(CTEs, comments, `MERGE`, `REPLACE`) are rejected, and INSERT/UPDATE/
DELETE writes are validated against the tenant column. Task-local tenant
resolution now takes precedence over the shared middleware slot.
- **TLS landed for Postgres**: `sslmode=require`/`verify-ca`/`verify-full`
now establish rustls-encrypted connections verified against the Mozilla
root store (new default `tls` feature on prax-postgres; `prefer` keeps
tokio-postgres's plaintext fallback). Without the feature, TLS-requiring
modes fail at pool build — never a silent downgrade. MySQL `SslMode`s are
now wired to real `SslOpts` (`Required`/`VerifyCa`/`VerifyIdentity` were
plaintext no-ops). ScyllaDB TLS via openssl behind the `ssl` feature.
Cassandra TLS via cdrs-tokio `rust-tls` (CA cert, mTLS,
`verify_hostname=false` is encrypt-only with a loud warning). The `prax
db pull` introspector uses TLS for non-`disable` modes.
- **Cassandra SASL authentication works** (previously silently connected
with no auth): async `SaslMechanism`s are bridged to cdrs-tokio via an
eagerly-prepared authenticator (`PreparedSaslAuthenticatorProvider`).
- **Injection hardening across the workspace**: savepoint names validated
(pg/mssql/duckdb), `sp_set_session_context` getter escaped, hybrid-search
`language` whitelisted, pgvector index names validated/escaped, MongoDB
filter parser rejects `$`-prefixed/dot field names and match-all
accidents, LIKE patterns escape `%`/`_` with a dialect-aware `ESCAPE`
clause, GUC `options` packing validates keys/values, seed/introspection/
raw-builder identifiers escaped per dialect, `SET search_path` input
validated.
- **`JwtClaimExtractor` renamed to `UnverifiedJwtClaimExtractor`** (with a
deprecated alias): it decodes without verifying signatures and must sit
behind a verifying middleware.
### Added
- **prax-postgres TLS** (above) with `SslMode::{Require, VerifyCa,
VerifyFull}` and URL parsing.
- **Real transactions for prax-sqlx** (engine override with commit/
rollback/nested-refusal/finalize-guard), `aggregate_query`, and
type-dispatched row decoding (timestamps, UUID, JSON, numerics no longer
decode to `Null`; nullable non-text fields decode correctly).
- **DuckDB transactions and aggregate queries** via the engine (was
`SupportsNestedWrites` with non-atomic fall-through); pool acquire
timeouts; panic-safe rollback guards on the sqlite/duckdb/mssql/postgres
transaction paths.
- **Cursor-based pagination** in `find_many` (was stored but never emitted);
`DISTINCT ON` gated on dialect support; `create_many` bulk insert is now
linear-time; query-cache LRU actually tracks access.
- **Migration differ coverage**: enum variant diffs, index add/drop on
existing models, default-value changes, vector column info, top-level
`@@sql` view definitions, deterministic ordering; SQLite native
`ADD`/`DROP COLUMN`; MySQL/MSSQL nullability/default alter support;
per-backend generator routing via `SqlBackend`; introspection emits
`@@index`, FK relations, `@map`, and views; drift detection compares
indexes; `@@sql` names parse unquoted (parser fix).
- **SeaORM import**: `belongs_to` relations (list-form attributes),
`has_many`/`has_one` back-relations, model naming from table names,
`i64` → `BigInt`, dedicated error variant. **Prisma import**: composite
`@@id`, `env("VAR")` → `url_env`, enum variant attributes, doc comments.
**Diesel import**: custom types to sensible mappings, implicit `id` PKs,
parent-PK-aware `joinable!` references.
- **`aggregate!`/`group_by!` support in schema-path (`prax_schema!`)
codegen** (was derive-only); `#[prax(default = …)]` marks `CreateInput`
fields optional; macro e2e tests for the aggregate chain.
### Fixed
- Placeholder off-by-one in nested writes, aggregates, group-bys, and
legacy builders (emitted `$N` skipping a slot on Postgres; invalid on
MySQL); identifiers now quoted through the dialect instead of PG-style
everywhere.
- `PgEngine::query_one` zero-row → `NotFound` mapping was dead code
(string-matched the wrong error text); SQLSTATE categorization now
applies on the engine hot path; `Decimal` fields readable via `RowRef`.
- CQL `date` encode/decode used the wrong epoch (corrupt writes, failed
reads for modern dates); Cassandra engine binds parameters (was sending
placeholders with no values) and `count` returns the real count (was
always `0`); ScyllaDB config options (consistency, timeouts, pool size)
are applied; LWT `[applied]` checks fail loudly.
- MySQL `execute_update` WHERE-splitting broke on subqueries; raw-engine
inserts fabricated result rows; SQLite pool honors `connection_timeout`;
MySQL pool honors lifetime/idle/acquire timeouts.
- MySQL/MSSQL `alter_column` preserves `NOT NULL` on type-only changes;
CQL alters emit reversible `down` statements with irreversibility
warnings; expired resolutions no longer skip/baseline/force-apply;
bootstrap V1→V2 event casing matches the V2 constraint.
- MSSQL RLS policies bind block predicates to functions built from their
own `CHECK` expressions (was a silent RLS no-op); error classification
uses structured error numbers.
- `db seed --reset` works for JSON/TOML seeds; `prax format` preserves the
schema's actual provider; publish.sh no longer races crates.io's index
within a tier (pgvector moved to Tier 3); CLI version/docs metadata
corrected.
### Deprecated
- `ColumnType::format_value` (use `try_format_value`),
`current_tenant_id_str`, `POSTGRES_INIT_SQL` (V1 migration schema).
### Deferred (loud failures, no silent pretending)
Migration SQL execution in `MigrationEngine`/`prax migrate` (needs an
executor), Redis cache backend, shadow databases, `prax db push`,
`prax db execute`, `generate --watch`, MongoDB introspection,
`#[prax(schema = "…")]`, nested `include:` filters, ScyllaDB
`application_name` (driver lacks the API), MySQL `connect_timeout`
(driver lacks the API), Cassandra per-query consistency/request timeouts
(driver lacks the API). Each errors or warns explicitly; follow
https://github.com/quinnjr/prax/issues for tracking.
## [0.10.0] - 2026-05-26
### Fixed
- **Postgres `String`↔non-TEXT column round-tripping.** `FilterValue::String`
was bound straight through as a Rust `String`, which tokio-postgres rejects
against `UUID`/`TIMESTAMPTZ`/`TIMESTAMP`/`DATE`/`TIME`/`ENUM` columns with
`WrongType`. Binding now goes through a `PgString` `ToSql` shim that inspects
the target column type and re-parses to the correct Rust type (e.g. `uuid::Uuid`,
`chrono::DateTime<Utc>`). On the read side, `PgRow::get_string`/`get_string_opt`
decode `UUID` and user-defined `ENUM` columns that codegen emits as `String`,
and `PgRow::is_null` uses a type-agnostic null probe so `Option<T>` works on
any column type, not just TEXT. Adds an `#[ignore]`/`PRAX_E2E`-gated
`uuid_binding` regression test. (Recovered from the never-merged
`fix/postgres-uuid-string-binding` branch; the placeholder commit it also
carried is superseded by the `Filter::to_sql` fix below.)
- **`Filter::to_sql` emitted mis-numbered bind placeholders.** Every leaf
arm advanced the parameter index with `param_idx += params.len()`, which
accumulates as the shared `params` vector grows instead of stepping by
one. Any filter binding more than one parameter produced non-sequential,
out-of-range placeholders — e.g. `IN ($1, $3, $6)` for three values and
`($1) AND ($3)` for two ANDed conditions — which fail at execution on
Postgres/positional dialects (`there is no parameter $N`). Leaf arms now
emit `param_idx + 1` (and `param_idx + i + 1` for `IN`/`NOT IN` lists),
matching the contract the `ScalarSubquery` arm already documented. A
related latent bug in the `And`/`Or` arms was also fixed: they forwarded
`param_idx + params.len()` to children, which double-counts once the
`And`/`Or` is itself nested (e.g. an `Or` inside an `And` emitted
`("a" = $1 AND ("b" = $3 OR "c" = $4))` instead of `$2, $3`). They now
forward `base + params.len()` where `base = param_idx - params.len()` is
the original offset. Single-condition and single-level filters were
unaffected, which is why existing unit tests (asserting only column
quoting) and the feature-gated live DB tests never caught it. Added
regression tests asserting exact sequential numbering across nested
boolean groups, the `NotIn`/`Or`/`LIKE` arms, non-zero offsets, and the
SQLite/MySQL dialects.
- **`prax_schema!` now compiles for schemas with relations.** The
schema-path model generator nests each model's struct inside
`pub mod <model>`, but its relation-referencing codegen emitted
paths calibrated for the flat `#[derive(Model)]` layout, producing
E0433 ("too many leading `super`") and a cascade (E0425/E0063/E0599/
E0277) on any relation. Fixed: relation field types are qualified
(`super::<target>::<Target>`), `FromRow` defaults relation fields,
`IncludeParam` variants are unit and module-correct, per-relation
field modules expose `include()` instead of invalid scalar
`select()`/filters, and relations are excluded from the legacy
`WhereParam`. This unblocks the macro DSL end-to-end against
schema-defined models with relations (previously every macro-DSL
e2e test had to fall back to derive-style models + `RecordingEngine`,
and the workspace fixture schema was kept relation-free).
- **`prax_schema!` relation follow-up hardening.** Code-review fixes
on top of the above: single relations now generate
`Option<Box<Target>>` (was `Option<Target>`) so required relations
default cleanly via `FromRow` and self-/mutually-recursive relations
stay finitely sized (previously E0072/E0391); each schema-path model
emits a `ModelRelationLoader<E>` impl so `.exec()` compiles (the bound
is required unconditionally — without it the macro DSL could not
execute at all), with includes erroring loudly until functional
relation loading lands; relation field modules expose `fetch()`
returning an `IncludeSpec` (aligning with the derive path) instead of
an `include()` returning the divergent `IncludeParam`, and no longer
emit misleading `COLUMN`/`IS_OPTIONAL`/`IS_LIST` consts; the
`IncludeParam` default sentinel was renamed `__None` to avoid
colliding with a relation field named `none`; and a self-relation
whose field name collides with the model's own module name is now
rejected with a clear diagnostic. New fixtures cover a multi-word
model name (`BlogPost`) and a self-relation (`Category`).
### Added
- **Aggregate macros (phase 6).** Three new macros over the existing
`AggregateOperation` / `GroupByOperation` runtime:
- `count!` gains a `select:` block for Prisma-style per-column
non-null counts (`count!(c.user, { select: { _all: true, email:
true } })`). Without `select:`, behavior is unchanged (returns
`i64`).
- `aggregate!` — returns a per-model `<Model>AggregateResult` with
`_sum` / `_avg` / `_min` / `_max` / `_count` substructs populated
only when their `_<agg>:` block is supplied. Requires at least one
aggregate block.
- `group_by!` — `by:`, `where:`, the five aggregate blocks, and
`having:`. Returns `Vec<<Model>GroupByResult>`.
- Per-model codegen surface: `<Model>{Count,Sum,Avg,Min,Max}Select`
inputs, matching `*Result` outputs, `<Model>AggregateResult`,
`<Model>GroupByResult`, `<Model>GroupByColumn` enum,
`<Model>AggregateArgs`, `<Model>GroupByArgs`, plus `aggregate()` /
`group_by_columns()` accessors and `with_aggregate_args` /
`with_group_by_args` extension methods.
- `HavingCondition` gained `{count,sum,avg,min,max}_{gt,gte,lt,lte,eq,ne}`
constructors (previously only a partial `count_*` set).
- Macro-time diagnostics: `_sum`/`_avg` on a non-numeric column,
aggregate on a relation or another aggregate field, unknown column
(did-you-mean), empty `by:`, unknown by-column, empty aggregate
block, `aggregate!` with no aggregate blocks, unsupported `having`
operator. Locked via trybuild fixtures.
- **Aggregate macro follow-ups.** `count!` / `aggregate!` `_count`
blocks accept `{ col: { distinct: true } }` for `COUNT(DISTINCT col)`
(new `prax_query::CountSelectMode` enum; `<Model>CountSelect` columns
are `Option<CountSelectMode>`; `GroupByOperation` gains
`count_column` / `count_distinct` builders). `group_by!` now supports
`order_by: { _sum: { views: desc }, <by_col>: asc }`, ordering by
aggregate SELECT-list aliases or group-by columns (removes the
phase-6 deferral). New diagnostics (distinct on `_all`, distinct in a
non-count block, order-by of an unselected aggregate, order-by of a
non-`by:` bare column) locked via trybuild.
### Fixed
- `AggregateResult::from_row` now hydrates per-column non-null counts
(`count_columns`) and distinct counts (`count_distinct`) from the
`_count_<col>` / `_count_distinct_<col>` aliases that
`AggregateField::alias` emits (previously dropped). New
`count_of(col)` / `count_distinct_of(col)` accessors. `GroupByResult`
grouped per-column counts hydrate via the same path.
### Known limitations
- The aggregate macros lower into per-model `AggregateArgs` /
`GroupByArgs` structs emitted by the schema-path codegen
(`prax_schema!`). The schema-path `relation_helpers` bug (documented
since phase 5b) prevents exercising the macro DSL end-to-end against
models defined in a workspace test crate, so the e2e and live-PG
tests drive the runtime `AggregateOperation` / `GroupByOperation`
directly. The macro front-end is covered by trybuild fixtures and
codegen unit tests.
- The runtime `AggregateResult` now exposes per-column counts, but
mapping them into the typed `<Model>CountSelectResult` struct is
still gated on the schema-path `relation_helpers` fix.
- `having:` thresholds are inlined as numeric literals (SQL-safe — they
are `f64`, never user strings), not bound parameters.
- MongoDB (`$group`) and CQL (`GROUP BY`) engines are out of scope —
separate follow-ups.
- `_min`/`_max` against multiple columns in one call, and ordering a
`group_by!` by a column that is neither in `by:` nor aggregated, are
deferred follow-ups.
### Changed
- **`NestedWriteOp::Upsert` now emits a single statement on dialects
that support it** (Postgres `ON CONFLICT (pk) DO UPDATE SET ...`,
SQLite, DuckDB, MySQL `ON DUPLICATE KEY UPDATE ...`). Halves the
round-trips for nested upserts on those engines. MSSQL and CQL keep
the existing two-statement fallback since neither has a clean
single-statement upsert (MSSQL would need `MERGE`, CQL is
last-write-wins by default and doesn't surface ON CONFLICT).
Behavior unchanged on the fallback path.
- `NestedWriteOp::ConnectOrCreate` continues to use the two-statement
form regardless of dialect — its conflict-column extraction from
arbitrary `where:` filters is more nuanced and deferred to a
follow-up.
### Added
- **Computed and virtual fields (phase 5.5).** Three new schema-level
field classes:
- `@generated("expr") @stored|@virtual` — DB-side computed columns,
with per-dialect DDL emit: `GENERATED ALWAYS AS (...) STORED|VIRTUAL`
on Postgres / SQLite / DuckDB, `AS (...) [STORED|VIRTUAL]` on MySQL,
`AS (...) [PERSISTED]` on MSSQL. CQL engines reject `@generated`
at migrate time. New `SupportsGeneratedColumns` capability marker
implemented by all five SQL generators. Postgres `@virtual` falls
back to `STORED` with a warning (PG ≥ 17 native virtual columns are
a deferred follow-up).
- `@count(rel)` and `@sum`/`@avg`/`@min`/`@max(rel.field)` — relation
aggregate virtuals. Result-struct types: Count → `i64`,
Avg → `Option<f64>`, others → `Option<T>` matching the underlying
column. WHERE / ORDER BY lower via the existing
`Filter::ScalarSubquery` IR variant; SELECT lowers via a new
`ScalarProjection` runtime type in `prax-query`. New
`SupportsScalarSubqueryInSelect` capability marker — implemented by
all five SQL engines plus the SQLx-routed engine, not by MongoDB or
CQL engines.
- `select: { _count: { rel: true } }` ad-hoc accessor — emits one
`ScalarProjection` per listed relation with alias `_count_<rel>`.
Compile-time error against models with zero outgoing to-many
relations.
- Synthetic `<Model>Count` struct emitted for every model with one or
more outgoing relations (`pub <rel>: Option<i64>` per relation).
- `Model` trait: defaulted associated constants `GENERATED_FIELDS` and
`AGGREGATE_FIELDS` carrying per-model metadata for downstream consumers.
- `#[prax(generated = "expr", stored)]` / `#[prax(generated = "expr",
r#virtual)]` and `#[prax(count(rel))]` / `#[prax(sum(rel.field))]`
/ `#[prax(avg/min/max(...))]` derive-attribute syntax mirroring the
`.prax` directives.
- All `@generated` and aggregate fields are excluded from
`<Model>CreateInput` and `<Model>UpdateInput`; included in
`<Model>WhereInput`, `<Model>SelectInput`, `<Model>OrderByInput`.
### Known limitations
- Postgres rejects `@virtual` and emits `STORED` instead with a warning
(PG ≥ 17 support deferred).
- The `_count` ad-hoc accessor only supports counts; sum/avg/min/max
require a schema-level `@sum/@avg/...` attribute.
- The `_count` macro accessor and schema-level aggregate macro lowering
target schema-defined (`.prax`) models. Derive-style models can
declare aggregates and have them appear on the result struct, but
must use the runtime `.with_scalar_projection(...)` builder API
rather than the macro DSL.
- MongoDB engines fail to compile against scalar-projection operations
until the `$lookup` follow-up ships.
- Aggregate fields filtered in `where:` support only comparison
operators (`equals`, `not_equals`, `lt`, `lte`, `gt`, `gte`, `in`,
`not_in`). String filter operators are rejected at compile time.
- `include: { _count: … }` is not wired; use `select: { _count: … }`.
- **Nested writes inside `update!` and `upsert!` macros.** `update!`'s
`data:` block and `upsert!`'s `create:`/`update:` branches now accept
the full Prisma nested-write operator set (`create`, `connect`,
`disconnect`, `delete`, `delete_many`, `update`, `update_many`,
`upsert`, `connect_or_create`, `set`).
- `UpdateOperation::with(NestedWriteOp)` and
`UpsertOperation::with_create_nested(NestedWriteOp)` /
`with_update_nested(NestedWriteOp)` runtime builders, gated on
`SupportsNestedWrites`.
- `UpsertOperation` dispatches nested ops by branch: `update_nested`
fires when the existing-row UPDATE matched; `create_nested` fires
when the row was newly inserted. The slow path runs a two-statement
engine-agnostic upsert (UPDATE, then INSERT if zero rows affected),
re-fetching the post-update row via SELECT so the caller still sees
the merged columns.
### Known limitations
- Nested writes inside `update!` / `upsert!` currently require the
`where:` clause to equal-match the primary-key column. Non-PK unique
columns (e.g. `where: { email: "..." }`) error with a clear
diagnostic. Lifting this restriction is a separate follow-up — it
needs a SELECT-then-update pattern to capture the row's PK.
- **Nested `set:` full-relation replacement inside `create!`'s `data:`
(phase 5e).** New `NestedWriteOp::Set` variant with two-statement
engine-agnostic executor: disconnect (`UPDATE child SET fk = NULL
WHERE fk = $parent AND pk NOT IN (...)`) followed by connect
(`UPDATE child SET fk = $parent WHERE pk IN (...)`). Empty
`set: []` special-cases to a plain disconnect-all (no invalid
`NOT IN ()` clause). Pre-existing FK values on listed rows are
overwritten — `set:` claims rows for this parent regardless of prior
ownership, matching Prisma's relation-replacement semantics.
### Changed
- **The nested-write operator surface inside `create!`'s `data:` is
now complete.** Every Prisma-style operator ships: `create`,
`connect`, `disconnect`, `delete`, `delete_many`, `update`,
`update_many`, `upsert`, `connect_or_create`, `set`. Single-statement
vendor-specific upsert/connect_or_create (Postgres `ON CONFLICT`,
MySQL `ON DUPLICATE KEY`, MSSQL `MERGE`) remains a separate
optimization phase. The unknown-operator did-you-mean candidate list
grows to include `set`. The `nested_set_phase_5e` trybuild fixture is
removed — the operator it tested now ships.
- **Nested `connect_or_create` inside `create!`'s `data:` (phase 5d).**
New `NestedWriteOp::ConnectOrCreate` variant with two-statement
engine-agnostic executor: `UPDATE child SET fk WHERE <filter>`
(connect path); if zero affected rows, `INSERT INTO child (... + fk)
VALUES (...)` (create path). Behaves correctly even when the where
matches multiple rows — every match gets its FK pointed at the
parent. Defensively rejects an empty (`Filter::None`) where to avoid
the UPDATE matching every row in the child table. Single-statement
vendor-specific upsert (Postgres `ON CONFLICT`, MySQL
`ON DUPLICATE KEY`, MSSQL `MERGE`) remains a separate optimization
phase.
- **Nested `update` / `update_many` / `upsert` inside `create!`'s
`data:` (phase 5c-mutations).** Three new `NestedWriteOp` variants
with executors. Update + UpdateMany emit standard `UPDATE SET ...
WHERE ...` with `WriteOp` fragments (`set`, `increment`, `decrement`,
`multiply`, `divide`, `unset` are all supported). Upsert uses a
two-statement engine-agnostic path: UPDATE first, INSERT (with FK
spliced in) when affected_rows == 0. Single-statement upsert via
vendor-specific syntax (Postgres `ON CONFLICT` etc.) ships alongside
`connect_or_create` in phase 5d.
- **Nested `disconnect` / `delete` / `delete_many` inside `create!`'s
`data:` (phase 5c).** Three new `NestedWriteOp` variants —
`Disconnect` (`UPDATE child SET fk = NULL WHERE pk = $1`), `Delete`
(`DELETE FROM child WHERE pk = $1`), and `DeleteMany`
(`DELETE FROM child WHERE fk = $parent_pk AND <filter>`). DeleteMany's
AND-with-parent-FK clause is a safety bound enforced at SQL emit time
— user filters cannot remove rows belonging to other parents. The
`Delete` variant returns `QueryError::not_found` when affected_rows
!= 1, matching the Connect-batch affected-rows contract.
### Changed
- Inside `create!`'s `data:` block, only `set:` (phase 5e) remains a
deferred nested operator. The unknown-operator did-you-mean
candidate list grows to include `connect_or_create`.
- The `nested_connect_or_create_phase_5d` trybuild fixture is removed
— the operator it tested now ships.
- The phase-5c deferral arm for mutation operators is gone; `update`,
`update_many`, `upsert` are now first-class. Only `set:` (phase 5e)
and `connect_or_create` (phase 5d) remain deferred. The
unknown-operator did-you-mean candidate list grows to include
`update`, `update_many`, `upsert`.
- The `nested_unknown_op_phase_5c` trybuild fixture is removed — the
operator it tested (`update:`) now ships.
- The phase-5b "phase 5c deferral" arm narrows to mutation operators
only: `update`, `update_many`, `upsert` now hit a renamed
`phase_5c_mutations_deferral` with clearer wording. `set:` gets its
own `phase_5e_deferral` pointing at `disconnect`/`delete` as
available alternatives. The unknown-operator did-you-mean candidate
list grows to include `disconnect`, `delete`, `delete_many`.
- **Nested create/connect inside `create!` (phase 5b).** The `data:`
block of `prax::create!` now accepts relation keys with
`{ create: [...], connect: [...] }`, building a single transaction
that inserts the parent row, inserts the nested children with the
parent's returned PK spliced into their FK column, and updates the
FK of any existing child rows targeted by `connect`. The lowering
recognises `create:` and `connect:` operators; everything else
(`update`/`upsert`/`delete`/`delete_many`/`disconnect`/`set`)
returns a "phase 5c" deferral diagnostic, and `connect_or_create`
returns "phase 5d". Unknown operators get a did-you-mean against
`[create, connect]`.
- **`NestedWriteOp::Connect` executor is now functional.** Extended
the variant with `target_table`, `foreign_key`, and `target_pk`
metadata so the executor can emit
`UPDATE <target_table> SET <fk> = $1 WHERE <target_pk> = $2`.
Identifier components flow from codegen-emitted `&'static str`
constants on the per-relation `RelationMeta` / `Model` types;
only the PK values are parameterized. The codegen-emitted
`<relation>::connect()` helper fills the new fields from the target
model's `TABLE_NAME` / `PRIMARY_KEY[0]` constants.
- **`CreateOperation::with(...)` is type-gated on
`SupportsNestedWrites`.** SQL engines and MongoDB already impl the
marker trait; CQL engines (ScyllaDB, Cassandra) intentionally do
not, so nested writes against CQL fail to compile with the
`#[diagnostic::on_unimplemented]` message on the trait.
### Deferred to phase 5c+
- `update`, `update_many`, `upsert`, `delete`, `delete_many`,
`disconnect`, `set` operators inside relation blocks — phase 5c.
- `connect_or_create` (engine-specific lowerings) — phase 5d.
- Diff-based full-relation replacement via `set:` — phase 5e.
- Nested writes inside `update!` / `upsert!` `data:` blocks — phase 5c.
- Typed `<RelatedModel>CreateWithout<Owner>Input` /
`<Model><Relation>CreateNestedInput` wrapper structs — phase 5b
lowers the DSL inline; the typed wrappers land in phase 5c
alongside the update/upsert nested-write surface if still useful.
- **Flat write macros (phase 5a).** Five new schema-aware proc-macros
that lower a Prisma-style brace-block DSL into chained
`with_*_input(...)` calls on the existing write operations:
`prax::create!`, `prax::update!`, `prax::upsert!`,
`prax::create_many!`, `prax::update_many!`. Each accepts a `data:`
block of scalar fields and supports atomic update operators on the
update path (`{ increment: N }`, `{ decrement: N }`, `{ multiply: N }`,
`{ divide: N }`, `{ set: V }`, `{ unset: true }`). `create!` /
`update!` / `upsert!` also accept `include` xor `select` for the
return shape, matching the read-macro contract.
- **Runtime builder methods**: `with_create_input` on `CreateOperation`
and `UpsertOperation`; `with_create_inputs` on `CreateManyOperation`;
`with_update_input` on `UpdateOperation`, `UpdateManyOperation`, and
`UpsertOperation`. New `prax_query::inputs::WriteOp` enum carries
`Set` / `Increment` / `Decrement` / `Multiply` / `Divide` / `Unset`
variants; the SQL emitter renders the arithmetic variants as
`col = col <op> $n` and `Unset` as `col = NULL`. New `CreatePayload`
and `UpdatePayload` type aliases pin the `Data` associated type for
the codegen-emitted `<Model>CreateInput` / `<Model>UpdateInput`
trait impls.
- **Accessor methods**: `create_many` / `update_many` on the
`ModelAccessor` trait return fresh batch-write operations from a
clone of the engine. Codegen-emitted `Client<E>` accessors already
expose these methods directly.
- **Codegen**: `<Model>CreateInput` / `<Model>UpdateInput` now ship
with `impl CreateInput` / `impl UpdateInput` trait impls that lower
the struct to the matching runtime payload. Optional fields
(`Option<T>` slots) are skipped when unset; required scalars always
emit a row in the payload. Update wrappers (`IntFieldUpdate`,
`StringNullableFieldUpdate`, etc.) project each set field to the
matching `WriteOp` variant.
### Deferred to phase 5b
- Relation operators inside `data:` blocks (nested writes:
`create` / `connect` / `disconnect` / `set` / `update` /
`update_many` / `upsert` / `delete` / `delete_many` /
`connect_or_create`). Codegen emits a clear "phase 5b" diagnostic
pointing at the relation key with the deferred operator list.
- `NestedWritePlan` IR + executor in `prax-query`.
- `SupportsNestedWrites` per-engine declarations and CQL
capability-gap diagnostics.
- `set: [...]` full-relation-replacement semantics.
- Generated `<Model><Relation>CreateNestedInput` /
`<Model><Relation>UpdateNestedInput` and `WithoutXxx` variants.
- **Shape macros (phase 4).** Five new schema-aware proc-macros
that return the corresponding phase-2 typed input struct **as a
value**: `prax::r#where!`, `prax::include!`, `prax::select!`,
`prax::order_by!`, `prax::cursor!`. Each takes `(Model, { ... })`
(or `[ ... ]` for `order_by!`'s multi-key form) and emits a
reusable filter / include / select / order / cursor value that
composes with the phase-3 read macros via `..spread` inside the
DSL block or via the builder methods (`with_where_input`,
`with_include_input`, `with_select_input`, `order_by`). The
shape macros inherit the full phase-3 DSL surface — spread,
conditional, bare-ident enum resolution, "did you mean"
suggestions, schema-aware validation. `r#where` is exported as
a raw identifier because `where` is a Rust keyword; callers
invoke it as `prax::r#where!(...)`.
- **Read-operation macros (phase 3).** Six new schema-aware
proc-macros expand a Prisma-style brace-block DSL into chained
`with_*_input(...)` calls on the existing fluent-builder
operations: `prax::find_unique!`, `prax::find_first!`,
`prax::find_many!`, `prax::count!`, `prax::delete!`,
`prax::delete_many!`. The DSL grammar supports nested scalar
filters, logical `and` / `or` / `not`, relation operators (`some`
/ `every` / `none` / `is` / `is_not` / `is_null`), `..spread`
and `..move spread` for struct-update composition, `#[if(cond)]`
/ `#[else_if]` / `#[else]` conditional fields, bare-ident enum
resolution, and `@(expr)` Rust-expression escapes. Unknown
fields produce a "did you mean" diagnostic computed via
Jaro-Winkler against the actual model. Schema discovery walks
up from `CARGO_MANIFEST_DIR` looking for `prax.toml`, with a
`PRAX_SCHEMA` env override; parsed schemas are cached per
process so repeat macro invocations within a single crate
compile in microseconds.
- **Typed input codegen (phase 2).** `#[derive(Model)]` and the
`prax_schema!` macro now emit seven new types per model:
`<Model>WhereInput`, `<Model>WhereUniqueInput`, `<Model>Include`,
`<Model>Select`, `<Model>OrderBy`, `<Model>CreateInput`, and
`<Model>UpdateInput`. Each implements the corresponding trait from
`prax_query::inputs` (where applicable) and lowers to the existing
runtime IR. Per-relation `<Model><Relation>FilterMeta` marker
structs are emitted alongside, supplying the table/column constants
for EXISTS / NOT EXISTS subquery lowering.
- **Engine capability declarations.** Six SQL/NoSQL engine crates
(`prax-postgres`, `prax-mysql`, `prax-sqlite`, `prax-mssql`,
`prax-duckdb`, `prax-mongodb`) declare which
`prax_query::capabilities::Supports*` marker traits they
implement. CQL engines (`prax-scylladb`, `prax-cassandra`)
intentionally implement none; trybuild compile-fail tests pin the
gap so a future regression is caught at test time.
- **`#[prax(relation(child_table = "..."))]` override** on the derive
macro, required when a relation target uses `#[prax(table = "...")]`
to remap the SQL table name.
- **Schema-attribute identifier validation.** `@map` / `@@map` values
are now validated as ASCII-safe SQL identifiers
(`[A-Za-z0-9_.]`) at schema-validation time, per the
`.cursor/rules/sql-safety.mdc` trust-boundary contract.
- **Multi-file schemas.** Point `[schema].path` in `prax.toml` at a directory
instead of a single file and prax recursively loads every `*.prax` under
it, merging them into one cohesive schema. Discovery is sorted
lexicographically by relative path for deterministic codegen output; hidden
entries, symlinks, and `target/` directories are skipped. The new
`prax_schema::load(path)` entry point auto-detects file vs. directory and
returns `LoadedSchema { schema, sources }` (or `LoadError { error, sources }`
on failure, with the partial source map preserved). Wired through
`prax generate`, `prax validate`, `prax migrate`, `prax db`, `prax format`
(per-file walk) and `prax-codegen`'s schema reader, so the `prax::client!`
macro also picks up directory inputs.
- **Cross-file collision detection.** `Schema::try_merge` reports every
duplicate model/enum/type/view/serverGroup/policy/generator/raw_sql plus
multiple-datasource as a `SchemaError::DuplicateAcrossFiles` /
`MultipleDatasource` with both `SourceLoc`s, collected without
short-circuiting so users see every conflict in one run.
- **`SchemaError::ParseInFile` and `EmptySchemaDirectory`** variants for
multi-file diagnostics. Every top-level AST item gains an additive
`source_id: Option<SourceId>` so the renderer can resolve errors back to
file paths.
- **Multi-file Prisma import.** `prax import --from prisma --input <dir>`
now mirrors Prisma's `prismaSchemaFolder` layouts into a Prax directory
tree: each `.prisma` becomes a `.prax` at the matching relative path, the
merged AST resolves cross-file relations cleanly, and duplicate models /
multiple datasource blocks across files are hard errors. Default output
directory is `./prax/schema`; `--force` is required to overwrite an
existing non-empty output directory. Single-file `prax import` behavior
is unchanged.
### Changed
- `prax_query::base64` re-export so generated code emitted by
`prax-codegen` for `Bytes`-typed `@unique` columns resolves
without requiring downstream users to add `base64` to their own
`Cargo.toml`.
- **Nested-create error diagnostics are now batch-level.** With the
multi-VALUES INSERT batching for `NestedWriteOp::Create`, a failing
child row surfaces as a single error for the whole batch rather than
pointing at the specific offending row. A failing batch still rolls
back the parent transaction; only the per-row attribution is lost.
### Deprecated
- `Schema::merge` (silent overwrite). Use `Schema::try_merge` for
collision-aware merging. The old method stays one release for compatibility.
## [0.9.7] - 2026-05-01
### Changed
- **`prax-cli generate` — emit prettyplease-formatted Rust.** Every
`.rs` the generator writes now round-trips through
`syn::parse_file` → `prettyplease::unparse`, so consumer repos can
run `cargo fmt --check` in CI without excluding the generated
tree via `rustfmt.toml`. `prettyplease` (the same library rustc's
codegen uses) produces byte-identical output across rustfmt
versions — exactly the determinism codegen needs.
### Fixed
- **`prax-cli` tests no longer hard-code a workspace version.**
`test_version_command` and `test_global_options` used to pin
`"0.9.0"` and went red on every workspace bump. They now read
`env!("CARGO_PKG_VERSION")` so the assertion always reflects
the version the CLI actually reports.
## [0.9.6] - 2026-04-30
### Fixed
- **`prax-cli generate` — lint-clean generated code.** The generator
emits a superset of each schema's shape — every consumer only
touches a fraction of the per-model accessors. Added a module-level
`#![allow(...)]` preamble on the generated `mod.rs` covering the
four lint categories the current codegen consistently trips
(`dead_code`, `clippy::derivable_impls`, `clippy::needless_update`,
`clippy::too_many_arguments`) so consumers running
`cargo clippy -- -D warnings` don't drown in noise from code they
never call.
## [0.9.5] - 2026-04-30
### Added
- **`prax-cli generate` — emit `transaction()` on the generated
`PraxClient<E>`.** Mirrors `prax_orm::PraxClient::transaction(|tx|
async { ... })`: commits on `Ok`, rolls back on `Err`, dispatches
through `QueryEngine::transaction`. Services ported from raw
tokio-postgres that did `BEGIN ... SELECT ... FOR UPDATE ...
UPDATE ... COMMIT` against the schema-generated client had no way
to express the transactional scope before.
- **`prax-cli generate` — emit `impl ModelRelationLoader<E>` on every
schema-generated model.** `FindManyOperation::exec` /
`FindUniqueOperation::exec_optional` / `FindFirstOperation` each
require `M: ModelRelationLoader<E>`, so every schema-generated
model failed the bound without this. Shipping an always-errors
impl keeps the uniform requirement satisfied while leaving real
relation loading on the schema-gen path for a follow-up.
### Motivation
Surfaced by the LX-33 port of `services/core` in
lexmata-admin-backend: `Inspectable::inspect` needs
`find_unique().exec_optional()` (pulls in `ModelRelationLoader`);
`Configurable::set` needs `transaction()` for the
`SELECT … FOR UPDATE` + `UPDATE` sequence on `users.page_credits`.
Both now compile end to end on the schema-generated client.
## [0.9.4] - 2026-04-30
### Added
- **`prax-cli generate` — emit `query_raw` / `execute_raw` / `engine()`
on the generated `PraxClient<E>`.** The schema-generated top-level
client previously exposed only per-model accessors, so consumers that
needed to reach beyond the fluent builder (JOINs, subqueries, CTEs,
multi-table aggregates, pgvector operators, window functions, vendor
extensions) had to reach around the generated client entirely. The
derive-path `prax_orm::PraxClient` already had these three methods;
now the generated version matches. `query_raw<T>` routes rows
through the same `FromRow` bridge the per-model `find_many` uses so
raw queries still return typed records.
### Motivation
Surfaced porting lexmata-admin-backend's user_view service under
LX-33: queries like "user profile with firm summary + aggregate
document/demand-letter counts per case" use JOINs and subqueries
the fluent builder doesn't model, and the generated client was a
dead end. With this release they compile through
`client.query_raw::<Case>(Sql::new("SELECT …"))` — the same shape
the derive-path client already supported.
## [0.9.3] - 2026-04-30
### Added
- **`prax-query` — blanket `FromColumn` / `ToFilterValue` for
`Option<T>`.** Every `T: FromColumn` now satisfies
`Option<T>: FromColumn` via a single blanket impl that probes
nullability with the new `RowRef::is_null` method. Unblocks
schema-generated clients from hitting the orphan rule when a
Prisma column is an `Enum?`: `Option<MyEnum>: FromColumn` now
works out of the box without the consumer crate having to write
its own (which orphan rules would reject). Replaced the dozen
concrete `Option<primitive>` impls — behavior-preserving since
every driver backend either honored the existing `_opt` path or
falls back to the new `is_null` + inner decode.
- **`prax-query` — `FromColumn` / `ToFilterValue` for `Vec<f32>`.**
Pgvector-typed columns in the schema emit `Vec<f32>` on the
generated struct; those now decode via `RowRef::get_vector` (new
base method, drivers implement) and encode as
`FilterValue::List(Float, Float, …)`.
- **`prax-cli generate` — enum round-trip impls.** Every enum
emitted by `prax generate` now carries `FromStr`, `FromColumn`,
and `ToFilterValue` impls alongside the existing `Display` /
`Default`. Schema-generated structs with enum fields
now compile without needing handwritten decoder code per enum.
### Fixed
- **`prax-cli generate` — escape Rust reserved keywords.** Columns
named `type`, `match`, `use`, `loop`, `move`, etc. (common —
`documents.type`, `notifications.type`, `email_verification.type`
all exist in Prisma schemas) were emitted as plain field
identifiers, producing output that fails to parse with
`expected identifier, found keyword \`type\``. Snake-cased field
names whose result is a Rust keyword are now prefixed with `r#`.
The four keywords Rust refuses as raw identifiers (`crate`,
`self`, `Self`, `super`) stay un-escaped — a column literally
named `self` still fails to compile, which is correct behavior.
SQL column-name strings (serde rename values,
`FromColumn::from_column(row, "col")` literals) use plain
snake_case because they're opaque text, not identifiers.
- **`prax-cli generate` — qualify `VectorFilter` path; replace
unshipped filter types with `ScalarFilter<T>`.** The bare
`VectorFilter` reference only compiled if the consumer's
`filters.rs` happened to have the right import, and
`SparseVectorFilter` / `BitFilter` were invented names that don't
exist anywhere in `prax-pgvector`. Fully qualified the vector
filter as `prax_pgvector::filter::VectorFilter`, and swapped
sparse + bit to `ScalarFilter<Vec<(u32, f32)>>` /
`ScalarFilter<Vec<u8>>` until dedicated filters ship.
### Motivation
Ports the `prax generate` runtime-client output from "compiles on
toy examples" to "compiles on real Prisma schemas." Surfaced by the
LX-33 migration of lexmata-admin-backend's 71-model shared schema
(33 enums, 1149 fields, pgvector + nullable-enum + reserved-keyword
columns all represented). Every fix is covered by a regression test.
## [0.9.2] - 2026-04-30
### Fixed
- **`prax-codegen` — `snake_ident` escapes Rust reserved keywords.**
Columns named `type`, `match`, `use`, `loop`, `move`, `where`, and
similar (common in Prisma schemas) previously emitted verbatim as
field and variable identifiers, producing output that fails to parse
with `expected identifier, found keyword \`type\``. `snake_ident`
now prefixes matches with `r#` so `pub r#type: …` round-trips
through `rustc`. Four keywords Rust forbids as raw identifiers
(`crate`, `self`, `Self`, `super`) are intentionally not escaped;
a column literally named `self` would still fail to compile, which
is the correct behavior (the schema should be fixed).
- **`prax-cli generate` — qualify `VectorFilter` path and drop
references to unshipped filter types.** `field_to_filter_type`
emitted bare `"VectorFilter"` (only compiled if the consumer's
`filters.rs` happened to have the right import) and referenced
`SparseVectorFilter` / `BitFilter` types that do not exist in
`prax-pgvector`. Fully qualified the vector filter path as
`prax_pgvector::filter::VectorFilter`, and fell back to
`ScalarFilter<Vec<(u32, f32)>>` / `ScalarFilter<Vec<u8>>` for
sparse and bit vectors until dedicated filter types ship.
Both fixes surfaced porting lexmata-admin-backend's 71-model shared
schema to the generated Prax client under LX-33. Each is covered by
regression tests.
## [0.9.1] - 2026-04-30
Forward-ports three correctness fixes that shipped in 0.8.2 on the
`release/0.8.1` branch but never landed on `develop` before 0.9.0
cut. All three were required to import the Lexmata application schema
(71 models, 33 enums) round-trippably through `prax import --from
prisma`; each is covered by a regression test.
### Fixed
- **`prax-import` (Prisma) — `@default` value round-trip.** String
literal defaults no longer double-quote
(`@default("standard")` ↛ `@default(""standard"")`); bare-identifier
defaults on enum-typed fields map to `AttributeValue::Ident`
rather than `AttributeValue::String` so the emitter doesn't
render them as quoted strings; `dbgenerated("…")` arguments unwrap
their Prisma source quotes uniformly.
- **`prax-import` (Prisma) — pgvector `Unsupported(…)`.** Prisma's
`Unsupported("vector(N)")` / `halfvec(N)` / `sparsevec(N)` / `bit(N)`
escape hatch now maps to the matching `ScalarType::Vector(…)` /
`HalfVector(…)` / `SparseVector(…)` / `Bit(…)` variants. The CLI
emitter prints the dimension via the `@dim(N)` attribute that the
schema parser already accepts.
- **`prax validate` — diagnostic rendering.** Schema errors now render
via `miette::Report` with the source attached, so the
`prax::schema::invalid_field` / `unknown_type` / etc. diagnostic
text and location are visible. Previously every parse or validation
failure surfaced as a bare "syntax error in schema" string, hiding
the actionable detail.
- **`prax-schema` validator — `Json` default values.** Accept
`String`, `Array`, `Boolean`, `Int`, and `Float` payloads as the
`@default` of a `Json`-typed field. Prisma encodes JSON defaults as
quoted text literals (`@default("[]")`, `@default("{}")`), which
are valid because Postgres parses the text into `jsonb` at insert
time — the old validator only accepted `String` defaults on
`String`-typed fields, rejecting every JSON default outright.
### Housekeeping
- `.gitignore` excludes `docs/superpowers/` and
`tests/qualified_test.rs` (local scratch test, broken compile) so
`cargo publish` doesn't require `--allow-dirty` for these
pre-existing artifacts.
## [0.9.0] - 2026-04-30
### Added
- **`prax generate` now emits a runtime-ready client.** Generated
model modules carry the trait impls the runtime needs to actually
run queries, matching the surface produced by `#[derive(Model)]`:
- `impl prax_query::row::FromRow` decodes scalar columns via
`FromColumn` and default-initializes relation fields, so
`find_many` and friends round-trip rows back into the generated
structs at runtime.
- `impl prax_query::traits::ModelWithPk` exposes `pk_value()` and
`get_column_value()` for nested writes, upsert, and composite
primary keys.
- The per-model operations struct is named `Client<E>` (was
`{Name}Operations<E>`), so `prax_orm::client!(User, Post, ...)`
can resolve `<snake_name>::Client<E>` by path the same way it
does for the derive path. The full CRUD surface — `find_many`,
`find_unique`, `find_first`, `create`, `create_many`, `update`,
`update_many`, `upsert`, `delete`, `delete_many`, `count` — is
emitted on `Client<E>`.
- Non-list relation fields are emitted as `Option<T>` (or
`Option<Box<T>>` when boxing is needed to break a cycle)
regardless of the schema modifier, so the FromRow default-init
has a `None` to write into. The relation executor populates
`Some(T)` on the `.include` path.
### Fixed
- **`prax-migrate` — CREATE TABLE emission respects FK dependencies.**
Before, `SchemaDiffer` populated `create_models` by iterating a
HashMap, leaving the resulting CREATE TABLE order
non-deterministic. A schema where `tracks` and `playlists`
reference `sync_sources` could emit `sync_sources` last; SQLite
tolerated it because FK targets are resolved at row-write time,
but strict engines (Postgres, MySQL with FK enforcement, MSSQL)
and any deferred-constraint bootstrap would fail to apply the
migration. `SchemaDiff::ordered_create_models` now does Kahn's
algorithm topo sort over the FK graph: referenced tables emit
before their dependents, self-references and FKs that point at
out-of-batch tables don't constrain ordering, and cycles fall
back to original order. All five SQL generators (Postgres,
MySQL, SQLite, MSSQL, DuckDB) route through it and emit drops
in the reverse direction so rollbacks drop dependents before
parents.
### Changed
- **Workspace clippy gate is back online.** The husky pre-commit
hook had silently been bypassed in environments that override
`core.hookspath` with no project-local `pre-commit`; develop had
accumulated 200+ clippy errors under `-D warnings`. Cleared every
diagnostic so
`cargo clippy --workspace --all-targets --all-features -- -D warnings`
passes again. API-shape lints (`result_large_err`,
`new_ret_no_self`, `should_implement_trait`, pedantic noise in
`prax-scylladb`) are suppressed at crate level with rationale;
bug-shaped lints (`manual_checked_ops`, `manual_strip`,
`manual_clamp`, `manual_sort_by_key`, `&PathBuf` → `&Path`,
`mixed_attributes_style`, `unnecessary_unwrap`) are fixed in
place.
- **`prax-sqlite` — vector tests skip cleanly when the loader is
unconfigured.** The two unit tests in `vector/register.rs` and
the three integration tests in `tests/vector_integration.rs` no
longer fail under `cargo test -- --include-ignored` (used by CI)
in environments that haven't provisioned the sqlite-vector-rs
cdylib. They detect the missing library at runtime via
`SQLITE_VECTOR_RS_LIB` and bail out with a "skipping" message
instead of panicking on the loader error.
## [0.8.0] - 2026-04-30
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>` and `prax::client!(Model, ...)` macro** — top-level
client grouping per-model accessors. The macro emits a sealed
`PraxClientExt` trait and implements it for `PraxClient<E>` so
callers write `client.user()` / `client.post()` without inherent
`impl` blocks on a foreign type.
- **Per-model `Client<E>` emitted by `#[derive(Model)]` and by
`prax_schema!`** — exposes `find_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::SqlDialect` trait** — new module with
`Postgres` / `Sqlite` / `Mysql` / `Mssql` / `NotSql` implementations.
Attached to `QueryEngine::dialect()`. Each dialect drives placeholder
syntax (`$1` / `?` / `?N` / `@P1`), `RETURNING` vs `OUTPUT INSERTED`,
upsert clause shape, transaction statements, and identifier quoting.
Marked `#[non_exhaustive]` so additional dialects can be added
without a breaking release.
- **`ToFilterValue` trait + `ModelWithPk`** — reverse of `FromColumn`
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)` on `find_many` / `find_unique` / `find_first`** —
eager-loads BelongsTo / HasOne / HasMany relations with one
follow-up `IN (…)` query per relation.
- **Real transactions on all four SQL drivers**:
`PraxClient::transaction(|tx| async { ... }).await` commits on `Ok`
and rolls back on `Err`. Nested `transaction()` on the same engine
currently returns `QueryError::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
`FromRow` bridge the derived models use, so the result stays typed.
- **`prax-query::row::FromRow` + `RowRef`** — expanded with
default-erroring getters for `chrono::DateTime<Utc>`,
`chrono::NaiveDateTime`, `chrono::NaiveDate`, `chrono::NaiveTime`,
`uuid::Uuid`, `rust_decimal::Decimal`, `serde_json::Value` and their
`Option<T>` variants. Drivers override the ones they support
natively.
- **`prax-query::row::into_row_error`** — helper for driver `RowRef`
bridges that maps any `Display` error 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
`QueryEngine` trait with typed row decoding via `FromRow`.
- **`#[derive(Model)]`** — emits `impl prax_query::traits::Model` and
`impl prax_query::row::FromRow` alongside the legacy `PraxModel`
marker. Also emits per-field filter operator constructors
(`user::age::gt(18)`, etc.) that classify field types into
Numeric / String / Boolean / Other buckets.
- **`FilterValue` `From` impls** — 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 on `PRAX_E2E=1` + `#[ignore]` so the default
`cargo test` run 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-typegen` v0.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.
- `CreateInput` and `UpdateInput` variants for each model.
- Lazy `z.lazy()` references for relation fields.
- CLI binary installable via `cargo install prax-typegen`.
- **Schema Generator Blocks** (`prax-schema`) — first-class `generator`
block support in `.prax` files.
- `generate = env("VAR")` toggle: enable/disable generators via
environment variables.
- `generate = true/false` literal toggle.
- Parsed into `Generator` AST with `provider`, `output`, `generate`,
and arbitrary properties.
- `Schema::enabled_generators()` for runtime filtering.
### Changed (BREAKING)
- **`prax-query::traits::QueryEngine`** — row-returning methods now
require `T: FromRow`. Add `#[derive(Model)]` (which emits `FromRow`)
or a hand-written `impl FromRow for MyModel`. Every operation
builder propagates the bound. Driver impls route rows through the
`RowRef` bridge instead of JSON.
- **`prax-query::traits::QueryEngine`** — new `dialect()` method on the
trait. Has a default returning the inert `NotSql` dialect, 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 SqlDialect` parameter. Callers must pass their
engine's dialect (or a literal `&prax_query::dialect::Postgres` if
wedded to that backend).
- **`prax-query::filter::Filter::to_sql`** — column names are now
quoted through `dialect.quote_ident` before being interpolated into
SQL (SQL-injection fix). Generated SQL now reads `"col" = $1` on
Postgres (was `col = $1`), `` `col` = ? `` on MySQL, `[col] = @P1`
on MSSQL. Tests that matched the unquoted form need updating.
- **`prax-mysql` / `prax-sqlite` engines** — rewritten to return typed
rows (`T: FromRow`) instead of JSON blobs. The legacy JSON surface
moved to `prax_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::MysqlEngine` inherent 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 the `QueryEngine` trait methods `query_many::<T>`,
`query_one::<T>`, `query_optional::<T>`, each of which requires
`T: Model + FromRow`. Callers consuming raw `RowData` /
`serde_json::Value` must either migrate to a typed model via
`#[derive(Model)]`, bridge through `prax_mysql::row_ref::MysqlRowRef`
in a hand-written `FromRow`, or switch to
`prax_mysql::raw::MysqlRawEngine` for the legacy JSON API.
Side-effecting SQL that returns no rows should call
`QueryEngine::execute_raw`.
- **`prax-sqlite::SqliteEngine` inherent methods removed** — same
breakage as `MysqlEngine`. The old `query` / `query_one` /
`query_opt` are gone; use `query_many::<T>` / `query_one::<T>` /
`query_optional::<T>` with `T: Model + FromRow`, bridge via
`prax_sqlite::row_ref::SqliteRowRef::from_rusqlite` for ad-hoc typed
rows, or fall back to `prax_sqlite::raw::SqliteRawEngine` for 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 emits `FromRow` in addition to `Model`** —
the derive expands to *both* `impl prax_query::traits::Model for …`
and `impl prax_query::row::FromRow for …`. If you had a
hand-written `impl Model for …` or `impl 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 struct `User` plus a local
`mod user { … }`) will see an `E0428` duplicate-definition error.
Rename one of them.
- **`FilterValue::from::<u64>`** — values greater than `i64::MAX` now
panic instead of silently clamping (previously an auth-bypass
footgun). Callers that pass untrusted `u64` inputs 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::Int` is
narrowed to the target column width at bind time (INT2 / INT4 /
INT8). Eliminates `WrongType { postgres: Int4, rust: "i64" }`
errors when filtering on integer PKs.
- **MSSQL `OUTPUT INSERTED.*` clause order** — rearranged into the
correct T-SQL position (between `(cols)` and `VALUES` on
`INSERT`; between `SET` and `WHERE` on `UPDATE`).
- **MySQL stopped emitting `RETURNING`** — MySQL 8.0 doesn't support
it (that's a MariaDB extension). The engine now re-`SELECT`s after
`INSERT` via `LAST_INSERT_ID()`.
### Removed
- **Legacy `Actions` / `Query` inert helpers** emitted by the codegen
— they returned SQL strings without an attached engine and are
fully subsumed by the new executable `Client<E>`.
- **`#[derive(Model)]` phantom `increment` / `decrement` helpers** —
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:
1. Add `fn dialect(&self) -> &dyn SqlDialect { &prax_query::dialect::Postgres }` (or the dialect you target).
2. Ensure every type passed to `query_many::<T>`, `query_one::<T>`, etc. implements `FromRow`. 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` / `SqliteRawEngine` from the `raw` module.
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:
```rust
// 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.
## [0.6.0] - 2026-02-13
### Added
- **pgvector Support** (`prax-pgvector`) - New crate for vector similarity search
- Dense vector embeddings via `Embedding` type wrapping `pgvector::Vector`
- Sparse vector support via `SparseEmbedding` wrapping `pgvector::SparseVector`
- Binary vector support via `BinaryVector` wrapping `pgvector::Bit`
- Half-precision vectors via `HalfEmbedding` (feature-gated `halfvec`)
- Distance metrics: L2, inner product, cosine, L1, Hamming, Jaccard
- IVFFlat and HNSW index management with tuning parameters
- Fluent `VectorSearchBuilder` for nearest-neighbor queries
- `HybridSearchBuilder` for combined vector + full-text search (RRF scoring)
- Vector filter integration for prax-query WHERE/ORDER BY clauses
- Extension management SQL helpers (CREATE/DROP/CHECK pgvector)
- Client-side vector math: L2 norm, normalization, dot product, cosine similarity
- 99 unit tests + 10 doc tests + 36 integration tests
## [0.5.0] - 2026-01-07
### Added
- **Schema Import from Prisma, Diesel, and SeaORM** (`prax-import`)
- Parse Prisma schema files (`.prisma`) and convert to Prax
- Parse Diesel schema files (`table!` macros) and convert to Prax
- Parse SeaORM entity files (`DeriveEntityModel`) and convert to Prax
- Automatic type mapping between ORM schemas
- Relation preservation and foreign key conversion
- Model attribute conversion (@@map, @@index, @@unique)
- Field attribute conversion (@id, @unique, @default, @relation)
- Enum definition conversion
- CLI integration via `prax import --from <prisma|diesel|sea-orm>`
- Comprehensive test coverage for all import paths (13 tests)
- Performance benchmarks with Criterion.rs
### Performance
- **Import Performance Optimization** (`prax-import`)
- Regex compilation caching using `once_cell::sync::Lazy`
- 42-57% faster Prisma imports (2.31x speedup on small schemas)
- 15-45% faster Diesel imports (1.80x speedup on small schemas)
- Throughput: ~7,675 Prisma schemas/sec, ~8,135 Diesel schemas/sec, ~7,799 SeaORM schemas/sec
- Comprehensive benchmark suite with small/medium/large test cases
## [0.4.0] - 2025-12-28
### Added
- **ScyllaDB Support** (`prax-scylladb`)
- High-performance Cassandra-compatible database driver
- Built on the official `scylla` async driver
- Connection pooling with automatic reconnection
- Prepared statement caching
- Lightweight Transactions (LWT) support for conditional updates
- Batch operations (logged, unlogged, counter)
- Full CQL type mapping to Rust types
- URL-based configuration parsing
- **DuckDB Support** (`prax-duckdb`)
- Analytical database driver optimized for OLAP workloads
- In-process database with no server required
- Parquet, CSV, JSON file reading/writing
- Window functions, aggregations, analytical queries
- Connection pooling with semaphore-based limiting
- **Multi-Tenancy Support** (`prax-query/src/tenant/`)
- Zero-allocation task-local tenant context
- PostgreSQL Row-Level Security (RLS) integration
- LRU tenant cache with TTL and sharded cache for high concurrency
- Per-tenant connection pools and statement caching
- **Data Caching Layer** (`prax-query/src/data_cache/`)
- In-memory LRU cache with TTL
- Redis distributed cache with connection pooling
- Tiered L1 (memory) + L2 (Redis) caching
- Pattern-based and tag-based cache invalidation
- **Async Optimizations** (`prax-query/src/async_optimize/`)
- `ConcurrentExecutor` for parallel task execution
- `ConcurrentIntrospector` for parallel database schema introspection
- Bulk insert/update pipelines for batched operations
- **Memory Optimizations** (`prax-query/src/mem_optimize/`)
- Global and scoped string interning
- Arena allocation for query builders
- Lazy schema parsing for on-demand introspection
- **Memory Profiling** (`prax-query/src/profiling/`)
- Allocation tracking with size histograms
- Memory snapshots and diff analysis
- Leak detection with severity classification
- **New Benchmarks**
- `async_bench`, `mem_optimize_bench`, `database_bench`
- `throughput_bench`, `memory_profile_bench`
- `duckdb_operations`, `scylladb_operations`
- **CI Workflows**
- `.github/workflows/benchmarks.yml` - Regression detection
- `.github/workflows/memory-check.yml` - Valgrind leak detection
- **Cursor Development Rules**
- SQL safety, benchmarking, error handling, performance
- Multi-tenancy, caching, profiling guidelines
### Changed
- Renamed project from `prax` to `prax-orm`
- Renamed CLI from `prax-cli` to `prax-orm-cli`
- Cleaned up TODO.md to concise feature reference (~200 lines)
- Updated all documentation URLs to `prax-orm`
### Fixed
- **ScyllaDB** - Resolved API compatibility issues with scylla driver v0.14
- Fixed `Compression` enum usage (use `Option<Compression>`)
- Fixed `ErrorCode` mapping to actual prax-query variants
- Fixed `FilterValue` conversion for `Json` and `List` types
- Fixed `Decimal` conversion using `mantissa()` and `scale()`
- Added `BatchValues` trait bound for batch execution
- Imported chrono `Datelike` and `Timelike` traits
## [0.3.3] - 2025-12-28
### Added
- **DuckDB Support** (`prax-duckdb`)
- New analytical database driver optimized for OLAP workloads
- In-process database with no server required
- Parquet, CSV, JSON file reading/writing
- Window functions, aggregations, analytical queries
- Connection pooling with semaphore-based limiting
- Async interface via `spawn_blocking`
- **Multi-Tenancy Support** (`prax-query/src/tenant/`)
- Zero-allocation task-local tenant context (`task_local.rs`)
- PostgreSQL Row-Level Security (RLS) integration (`rls.rs`)
- LRU tenant cache with TTL and sharded cache for high concurrency (`cache.rs`)
- Per-tenant connection pools (`pool.rs`)
- Prepared statement caching (global and per-tenant) (`prepared.rs`)
- **Data Caching Layer** (`prax-query/src/data_cache/`)
- In-memory LRU cache with TTL (`memory.rs`)
- Redis distributed cache with connection pooling (`redis.rs`)
- Tiered L1 (memory) + L2 (Redis) caching (`tiered.rs`)
- Pattern-based and tag-based cache invalidation (`invalidation.rs`)
- Cache metrics and hit rate tracking (`stats.rs`)
- **Async Optimizations** (`prax-query/src/async_optimize/`)
- `ConcurrentExecutor` for parallel task execution with configurable limits
- `ConcurrentIntrospector` for parallel database schema introspection
- `QueryPipeline`, `BulkInsertPipeline`, `BulkUpdatePipeline` for batched operations
- **Memory Optimizations** (`prax-query/src/mem_optimize/`)
- Global and scoped string interning (`GlobalInterner`, `ScopedInterner`)
- Arena allocation for query builders (`QueryArena`, `ArenaScope`)
- Lazy schema parsing for on-demand introspection (`LazySchema`, `LazyTable`)
- **Memory Profiling** (`prax-query/src/profiling/`)
- Allocation tracking with size histograms
- Memory snapshots and diff analysis
- Leak detection with severity classification
- Heap profiling integration
- CI workflow for Valgrind and AddressSanitizer checks
- **New Benchmarks**
- `async_bench` - Concurrent execution and pipeline performance
- `mem_optimize_bench` - Interning, arena, lazy parsing benchmarks
- `database_bench` - Database-specific SQL generation
- `throughput_bench` - Queries-per-second measurements
- `memory_profile_bench` - Memory profiling benchmarks
- `duckdb_operations` - DuckDB analytical query benchmarks
- **CI Workflows**
- `.github/workflows/benchmarks.yml` - Regression detection with baseline comparison
- `.github/workflows/memory-check.yml` - Memory leak detection via Valgrind
- **Cursor Rules**
- `sql-safety.mdc` - SQL injection prevention guidelines
- `benchmarking.mdc` - Criterion.rs benchmarking standards
- `error-handling.mdc` - Error handling best practices
- `performance.mdc` - Performance optimization guidelines
- `api-design.mdc` - API design principles
- `multi-tenancy.mdc` - Multi-tenant application patterns
- `caching.mdc` - Cache layer usage guidelines
- `profiling.mdc` - Memory profiling documentation
### Changed
- Cleaned up TODO.md from 869 lines to ~150 lines (concise feature reference)
- Updated architecture to include `prax-duckdb`
## [0.3.2] - 2025-12-24
### Added
- **GraphQL Model Style Configuration** (`prax-codegen`, `prax-schema`)
- New `model_style` option in `prax.toml`: `"standard"` (default) or `"graphql"`
- When set to `"graphql"`, model structs generate with `#[derive(async_graphql::SimpleObject)]`
- `CreateInput` and `UpdateInput` types generate with `#[derive(async_graphql::InputObject)]`
- Auto-enables GraphQL plugins when `graphql` style is selected
- Configuration example:
```toml
[generator.client]
model_style = "graphql"
```
## [0.3.1] - 2025-12-21
### Added
- **MySQL Execution Benchmarks** (`benches/database_execution.rs`)
- Prax MySQL benchmarks with connection pooling
- SQLx MySQL benchmarks for comparison
- SELECT by ID, filtered SELECT, and COUNT operations
- **SQLite Execution Benchmarks** (`benches/database_execution.rs`)
- Prax SQLite benchmarks with in-memory database seeding
- SQLx SQLite benchmarks for comparison
- Complete benchmark coverage across all three databases
### Fixed
- Resolved all clippy warnings across the codebase
- Renamed `from_str` methods to `parse` to avoid trait confusion
- Fixed `Include::add` → `Include::with` naming
- Fixed `PooledBuffer::as_mut` → `PooledBuffer::as_mut_str` naming
- Added proper allow attributes for API modules with intentionally unused code
### Changed
- Enabled sqlx `mysql` and `sqlite` features for benchmarks
- Added `prax-mysql`, `prax-sqlite`, `rusqlite` as dev-dependencies
## [0.3.0] - 2025-12-21
### Added
- **Zero-Copy Row Deserialization** (`prax-query`)
- `RowRef` trait for borrowing string data directly from database rows
- `FromRowRef<'a>` trait for zero-allocation struct deserialization
- `FromRow` trait for traditional owning deserialization
- `FromColumn` trait for type-specific column extraction
- `RowData` enum for borrowed/owned string data (like `Cow`)
- `impl_from_row!` macro for easy struct implementation
- **Batch & Pipeline Execution** (`prax-query`)
- `Pipeline` and `PipelineBuilder` for grouping multiple queries
- Execute multiple queries with minimal round-trips
- `PipelineResult` with per-query status tracking
- Enhanced `Batch::to_combined_sql()` for multi-row INSERT optimization
- **Query Plan Caching** (`prax-query`)
- `ExecutionPlanCache` for caching query plans with metrics
- `ExecutionPlan` with SQL, hints, and execution time tracking
- `PlanHint` enum: `IndexScan`, `SeqScan`, `Parallel`, `Timeout`, etc.
- `record_execution()` for automatic timing collection
- `slowest_queries()` and `most_used()` for performance analysis
- **Type-Level Filter Optimizations** (`prax-query`)
- `InI64Slice`, `InStrSlice` for zero-allocation IN filters
- `NotInI64Slice`, `NotInStrSlice` for NOT IN filters
- `And5` struct with `DirectSql` implementation
- Pre-computed PostgreSQL IN patterns (`POSTGRES_IN_FROM_1`) for 1-32 elements
- **Documentation Website**
- New "Advanced Performance" page with comprehensive examples
- Updated Performance page with latest benchmark results
- Added batch execution, zero-copy, and plan caching documentation
### Changed
- Optimized `write_postgres_in_pattern` for faster IN clause generation
- Updated benchmark results showing Prax matching Diesel for type-level filters
- Improved performance page with database execution benchmarks
### Performance
- Type-level `And5` filter: **~5.1ns** (matches Diesel!)
- `IN(10)` SQL generation: **~3.8ns** (5.8x faster with pre-computed patterns)
- `IN(32)` SQL generation: **~5.0ns** (uses pre-computed pattern lookup)
- Database SELECT by ID: **193µs** (30% faster than SQLx)
## [0.2.0] - 2025-12-20
### Added
- Initial project structure and configuration
- Dual MIT/Apache-2.0 licensing
- Project README with API examples and documentation
- Implementation roadmap (TODO.md)
- Git hooks via cargo-husky:
- Pre-commit hook for formatting and linting
- Pre-push hook for test suite validation
- Commit-msg hook for Conventional Commits enforcement
- Contributing guidelines (CONTRIBUTING.md)
- Schema definition language (SDL) parser (`prax-schema`)
- Custom `.prax` schema files with Prisma-like syntax
- AST types for models, fields, relations, enums, views
- Schema validation and semantic analysis
- Documentation comments with validation directives (`@validate`)
- Field metadata and visibility controls (`@hidden`, `@deprecated`, etc.)
- GraphQL and async-graphql support with federation
- Proc-macro code generation (`prax-codegen`)
- `#[derive(Model)]` and `prax_schema!` macros
- Plugin system for extensible code generation
- Built-in plugins: debug, JSON Schema, GraphQL, serde, validator
- Type-safe query builder (`prax-query`)
- Fluent API: `findMany`, `findUnique`, `findFirst`, `create`, `update`, `delete`, `upsert`, `count`
- Filtering system with WHERE clauses, AND/OR/NOT combinators
- Scalar filters: equals, in, contains, startsWith, endsWith, lt, gt, etc.
- Sorting with `orderBy`, pagination with `skip`/`take` and cursor-based
- Aggregation queries: `count`, `sum`, `avg`, `min`, `max`, `groupBy` with `HAVING`
- Raw SQL escape hatch with type interpolation via `Sql` builder
- Ergonomic create API with `data!` macro and builder pattern
- Middleware/hooks system for query interception (logging, metrics, timing, retry)
- Connection string parsing and multi-database configuration
- Comprehensive error types with error codes, suggestions, and colored output
- Multi-tenant support (row-level, schema-based, database-based isolation)
- Async query engines
- PostgreSQL via `tokio-postgres` with `deadpool-postgres` connection pool (`prax-postgres`)
- MySQL via `mysql_async` driver (`prax-mysql`)
- SQLite via `tokio-rusqlite` (`prax-sqlite`)
- SQLx alternative backend with compile-time checked queries (`prax-sqlx`)
- Relation loading (eager/lazy)
- `include` and `select` operations for related data
- Nested writes: create/connect/disconnect/set relations
- Transaction API with async closures, savepoints, isolation levels
- Migration engine (`prax-migrate`)
- Schema diffing and SQL generation
- Migration history tracking
- Database introspection (reverse engineer existing databases)
- Shadow database support for safe migration testing
- View migration support (CREATE/DROP/ALTER VIEW, materialized views)
- Migration resolution system (checksum handling, skip, baseline)
- CLI tool (`prax-cli`)
- Commands: `init`, `generate`, `migrate`, `db`, `validate`, `format`
- User-friendly colored output and error handling
- Documentation website with Angular
- Docker setup for testing with real databases
- Benchmarking suite with Criterion
- Profiling support (CPU, memory, tracing)
- Fuzzing infrastructure
### Planned
- Framework integrations (Armature, Axum, Actix-web)
- Integration test suite expansion
---
## Release History
[Unreleased]: https://github.com/quinnjr/prax/compare/v0.11.0...HEAD
[0.11.0]: https://github.com/quinnjr/prax/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/quinnjr/prax/compare/v0.6.0...v0.10.0
[0.6.0]: https://github.com/quinnjr/prax/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/quinnjr/prax/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/quinnjr/prax/compare/v0.3.3...v0.4.0
[0.3.3]: https://github.com/quinnjr/prax/compare/v0.3.2...v0.3.3
[0.3.2]: https://github.com/quinnjr/prax/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/quinnjr/prax/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/quinnjr/prax/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/quinnjr/prax/releases/tag/v0.2.0