prax-orm 0.12.0

A next-generation, type-safe ORM for Rust inspired by Prisma
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
# 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

<!--
## [0.1.0] - YYYY-MM-DD

### Added
- Initial release
- Core query builder functionality
- PostgreSQL support via tokio-postgres

### Changed
- N/A

### Deprecated
- N/A

### Removed
- N/A

### Fixed
- N/A

### Security
- N/A
-->

[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