ferro-orm 0.19.0__tar.gz → 0.20.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (405) hide show
  1. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/CHANGELOG.md +14 -0
  2. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/Cargo.lock +35 -35
  3. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/Cargo.toml +1 -1
  4. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/PKG-INFO +1 -1
  5. ferro_orm-0.20.0/context7.json +4 -0
  6. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-schema-ir/src/lib.rs +63 -0
  7. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/predicates.py +38 -0
  8. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/predicates_annotated.py +27 -0
  9. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/queries.md +1 -1
  10. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/queries.md +27 -3
  11. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/pyproject.toml +1 -1
  12. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/query/builder.py +52 -11
  13. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/query/wire.py +6 -2
  14. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/operations.rs +155 -7
  15. ferro_orm-0.20.0/tests/fixtures/ir_vectors/query_card_nulls_v7.json +44 -0
  16. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_ir_vectors_contract.py +5 -0
  17. ferro_orm-0.20.0/tests/test_order_by_nulls.py +209 -0
  18. ferro_orm-0.20.0/tests/test_order_by_nulls_wire.py +102 -0
  19. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_column_validation.py +118 -0
  20. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_wire_vectors.py +20 -0
  21. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  22. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/PERMISSIONS.md +0 -0
  24. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/PYPI_CHECKLIST.md +0 -0
  25. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/PYPI_SETUP.md +0 -0
  26. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/generated/wheels.generated.yml +0 -0
  27. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/pull_request_template.md +0 -0
  28. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/workflows/ci.yml +0 -0
  29. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/workflows/packaging-smoke.yml +0 -0
  30. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/workflows/publish-docs.yml +0 -0
  31. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/workflows/publish.yml +0 -0
  32. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/workflows/release.yml +0 -0
  33. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.gitignore +0 -0
  34. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.pre-commit-config.yaml +0 -0
  35. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.python-version +0 -0
  36. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/AGENTS.md +0 -0
  37. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/CONTEXT.md +0 -0
  38. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/CONTRIBUTING.md +0 -0
  39. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/LICENSE +0 -0
  40. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/README.md +0 -0
  41. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/README.md +0 -0
  42. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/__init__.py +0 -0
  43. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/__main__.py +0 -0
  44. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/backends.py +0 -0
  45. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/baselines/postgres.json +0 -0
  46. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/baselines/sqlite.json +0 -0
  47. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/compare.py +0 -0
  48. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/harness.py +0 -0
  49. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/model.py +0 -0
  50. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/run.py +0 -0
  51. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
  52. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-ddl-lowering/src/lib.rs +0 -0
  53. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-migrate/Cargo.toml +0 -0
  54. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-migrate/src/emit.rs +0 -0
  55. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-migrate/src/lib.rs +0 -0
  56. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-migrate/src/order.rs +0 -0
  57. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-migrate/src/tests.rs +0 -0
  58. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-schema-ir/Cargo.toml +0 -0
  59. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0001-resolved-epoch-registration.md +0 -0
  60. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
  61. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
  62. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
  63. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
  64. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
  65. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
  66. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0007-reverse-relations-tested-not-traversed.md +0 -0
  67. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0008-populated-relations-include.md +0 -0
  68. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0008-predicate-negation-uniform-not.md +0 -0
  69. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
  70. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0010-create-pass-owns-only-missing-tables.md +0 -0
  71. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0011-label-addition-update-gated-append-only.md +0 -0
  72. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0012-table-checks-are-lambdas.md +0 -0
  73. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0013-table-check-reconciliation.md +0 -0
  74. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0014-table-checks-sqlite-create-only.md +0 -0
  75. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0015-table-check-body-drift.md +0 -0
  76. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0016-check-predicates-literals-only.md +0 -0
  77. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/agents/domain.md +0 -0
  78. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/agents/issue-tracker.md +0 -0
  79. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/agents/triage-labels.md +0 -0
  80. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/architecture-review-2026-07-08.html +0 -0
  81. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
  82. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
  83. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
  84. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
  85. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
  86. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
  87. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
  88. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
  89. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-26-ir-p8.5-143-db-type-drop-requirements.md +0 -0
  90. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
  91. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-28-ir-p8.5-141-single-schema-ir-producer-requirements.md +0 -0
  92. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-29-ir-p8.6-146-dialect-enum-unification-requirements.md +0 -0
  93. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-29-ir-p8.6-153-create-path-ir-requirements.md +0 -0
  94. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
  95. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
  96. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-07-01-ir-p8.6-154-datetime-tz-coarseness-requirements.md +0 -0
  97. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-07-01-ir-p8.6-162-typed-save-bind-requirements.md +0 -0
  98. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-07-01-ir-p8.6-165-blob-introspection-false-positive-requirements.md +0 -0
  99. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
  100. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
  101. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/aggregations.py +0 -0
  102. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/aggregations_annotated.py +0 -0
  103. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/existence_tests.py +0 -0
  104. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/existence_tests_annotated.py +0 -0
  105. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/multiple_databases.py +0 -0
  106. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/mutations.py +0 -0
  107. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/pagination.py +0 -0
  108. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/partial_selects.py +0 -0
  109. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/partial_selects_annotated.py +0 -0
  110. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/populated_relations.py +0 -0
  111. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/populated_relations_annotated.py +0 -0
  112. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/quickstart.py +0 -0
  113. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/quickstart_annotated.py +0 -0
  114. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/raw_sql.py +0 -0
  115. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/relationships.py +0 -0
  116. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/relationships_annotated.py +0 -0
  117. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/soft_deletes.py +0 -0
  118. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/soft_deletes_annotated.py +0 -0
  119. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/table_checks.py +0 -0
  120. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/table_checks_annotated.py +0 -0
  121. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/testing_conftest.py +0 -0
  122. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/timestamps.py +0 -0
  123. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/timestamps_annotated.py +0 -0
  124. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/transactions.py +0 -0
  125. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/traversal.py +0 -0
  126. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/traversal_annotated.py +0 -0
  127. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/connection.md +0 -0
  128. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/exceptions.md +0 -0
  129. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/fields.md +0 -0
  130. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/migrations.md +0 -0
  131. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/model.md +0 -0
  132. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/raw-sql.md +0 -0
  133. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/relationships.md +0 -0
  134. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/transactions.md +0 -0
  135. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/changelog.md +0 -0
  136. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/architecture.md +0 -0
  137. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/backends.md +0 -0
  138. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/identity-map.md +0 -0
  139. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/performance.md +0 -0
  140. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/query-typing.md +0 -0
  141. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/type-safety.md +0 -0
  142. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/contributing.md +0 -0
  143. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/faq.md +0 -0
  144. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/getting-started/installation.md +0 -0
  145. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/getting-started/next-steps.md +0 -0
  146. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/getting-started/quickstart.md +0 -0
  147. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/aggregations.md +0 -0
  148. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/connections.md +0 -0
  149. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/migrations.md +0 -0
  150. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/models-and-fields.md +0 -0
  151. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/mutations.md +0 -0
  152. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/raw-sql.md +0 -0
  153. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/relationships.md +0 -0
  154. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/transactions.md +0 -0
  155. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
  156. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/multiple-databases.md +0 -0
  157. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/pagination.md +0 -0
  158. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/soft-deletes.md +0 -0
  159. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/testing.md +0 -0
  160. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/timestamps.md +0 -0
  161. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/upgrade-guide.md +0 -0
  162. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/index.md +0 -0
  163. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/roadmap.md +0 -0
  164. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/stylesheets/extra.css +0 -0
  165. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/why-ferro.md +0 -0
  166. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
  167. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
  168. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
  169. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
  170. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
  171. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
  172. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
  173. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
  174. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-24-001-feat-ir-p8-119-wire-automigrate-plan.md +0 -0
  175. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-25-001-feat-ir-p8-120-parity-gate-plan.md +0 -0
  176. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-26-001-feat-ir-p8.5-140-shared-lowering-plan.md +0 -0
  177. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-26-002-feat-ir-p8.5-143-db-type-drop-plan.md +0 -0
  178. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-26-003-feat-ir-p8.5-144-reconcile-indexes-plan.md +0 -0
  179. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-28-001-feat-ir-p8.5-141-single-schema-ir-producer-plan.md +0 -0
  180. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-29-001-refactor-ir-p8.6-153-create-path-unification-plan.md +0 -0
  181. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-29-002-refactor-ir-p8.6-146-dialect-enum-unification-plan.md +0 -0
  182. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-30-001-refactor-ir-p8.6-158-check-renderer-plan.md +0 -0
  183. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-30-002-fix-ir-p8.6-155-deferred-annotations-plan.md +0 -0
  184. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-01-001-feat-ir-p8.6-162-typed-save-bind-plan.md +0 -0
  185. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-01-002-fix-ir-p8.6-154-datetime-tz-coarseness-plan.md +0 -0
  186. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-01-003-fix-ir-p8.6-165-blob-introspection-plan.md +0 -0
  187. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
  188. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-002-feat-ff-a-172-typed-exceptions-plan.md +0 -0
  189. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
  190. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +0 -0
  191. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
  192. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
  193. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
  194. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
  195. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-009-ff-f-query-builder-1.0-design.md +0 -0
  196. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-010-ff-f-query-builder-1.0-plan.md +0 -0
  197. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
  198. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
  199. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
  200. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
  201. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
  202. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-18-016-existence-tests-design.md +0 -0
  203. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/ir-first-migration-guide.md +0 -0
  204. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/ir-first-release-checklist.md +0 -0
  205. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/rfc/ir-contracts-v1.md +0 -0
  206. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/README.md +0 -0
  207. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
  208. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
  209. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/pg18-restrict-sqlstate-23001.md +0 -0
  210. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
  211. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
  212. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
  213. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
  214. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/self-fk-evicts-component-from-create-order.md +0 -0
  215. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
  216. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
  217. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
  218. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
  219. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
  220. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
  221. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
  222. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/foreign-key-index.md +0 -0
  223. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
  224. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/ir-invariants.md +0 -0
  225. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/persistence-state.md +0 -0
  226. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
  227. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
  228. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/typed-null-binds.md +0 -0
  229. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/justfile +0 -0
  230. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/scripts/demo_queries.py +0 -0
  231. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/scripts/demo_queries_pre_v012.py +0 -0
  232. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/backend.rs +0 -0
  233. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/codec.rs +0 -0
  234. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/codec_plan.rs +0 -0
  235. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/connection.rs +0 -0
  236. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/errors.rs +0 -0
  237. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/__init__.py +0 -0
  238. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/_annotation_utils.py +0 -0
  239. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/_bind_payload.py +0 -0
  240. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/_core.pyi +0 -0
  241. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/_deprecations.py +0 -0
  242. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/_shadow_fk_types.py +0 -0
  243. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/base.py +0 -0
  244. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/checks.py +0 -0
  245. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/columns.py +0 -0
  246. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/composite_indexes.py +0 -0
  247. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/composite_uniques.py +0 -0
  248. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/exceptions.py +0 -0
  249. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/fields.py +0 -0
  250. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/ir/__init__.py +0 -0
  251. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/ir/compiler.py +0 -0
  252. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/metaclass.py +0 -0
  253. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/migrations/__init__.py +0 -0
  254. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/migrations/alembic.py +0 -0
  255. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/models.py +0 -0
  256. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/py.typed +0 -0
  257. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/query/__init__.py +0 -0
  258. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/query/nodes.py +0 -0
  259. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/query/rows.py +0 -0
  260. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/raw.py +0 -0
  261. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/registry.py +0 -0
  262. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/relations/__init__.py +0 -0
  263. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/relations/descriptors.py +0 -0
  264. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/session.py +0 -0
  265. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/state.py +0 -0
  266. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/hydration.rs +0 -0
  267. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/introspect.rs +0 -0
  268. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/lib.rs +0 -0
  269. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/migrate.rs +0 -0
  270. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/naming_ffi.rs +0 -0
  271. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/query.rs +0 -0
  272. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/schema.rs +0 -0
  273. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/schema_bind.rs +0 -0
  274. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/state.rs +0 -0
  275. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/__init__.py +0 -0
  276. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/conftest.py +0 -0
  277. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/db_backends.py +0 -0
  278. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/README.md +0 -0
  279. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
  280. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_account_exists_v7.json +0 -0
  281. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_account_scoped_exists_v7.json +0 -0
  282. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_owner_nested_exists_v7.json +0 -0
  283. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_owner_not_exists_v7.json +0 -0
  284. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_aggregate_v7.json +0 -0
  285. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v7.json +0 -0
  286. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_include_v7.json +0 -0
  287. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_left_join_v7.json +0 -0
  288. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_record_v7.json +0 -0
  289. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_traversal_v7.json +0 -0
  290. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_traversed_record_v7.json +0 -0
  291. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_user_compound_v7.json +0 -0
  292. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_user_m2m_exists_v7.json +0 -0
  293. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_user_not_compound_v7.json +0 -0
  294. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_user_not_leaf_v7.json +0 -0
  295. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
  296. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
  297. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
  298. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/bad_includes.py +0 -0
  299. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/bad_predicates.py +0 -0
  300. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/bad_projections.py +0 -0
  301. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/good_includes.py +0 -0
  302. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/good_predicates.py +0 -0
  303. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/good_projections.py +0 -0
  304. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_aggregation.py +0 -0
  305. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_alembic_autogenerate.py +0 -0
  306. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_alembic_bridge.py +0 -0
  307. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_alembic_db_type.py +0 -0
  308. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_alembic_nullability.py +0 -0
  309. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_alembic_type_mapping.py +0 -0
  310. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_annotation_utils.py +0 -0
  311. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_auto_migrate.py +0 -0
  312. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_bulk_create_chunking.py +0 -0
  313. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_bulk_install.py +0 -0
  314. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_bulk_update.py +0 -0
  315. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_catalog_cache.py +0 -0
  316. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_check_predicate_dialect.py +0 -0
  317. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_codec_plan.py +0 -0
  318. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_column_specs.py +0 -0
  319. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_compiled_query.py +0 -0
  320. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_composite_index.py +0 -0
  321. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_composite_unique.py +0 -0
  322. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_connection.py +0 -0
  323. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_connection_redaction.py +0 -0
  324. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_constraints.py +0 -0
  325. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_cross_emitter_parity.py +0 -0
  326. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_crud.py +0 -0
  327. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_db_backends.py +0 -0
  328. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_db_type_cross_emitter_parity.py +0 -0
  329. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_db_type_integration.py +0 -0
  330. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_db_type_typing.py +0 -0
  331. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_db_type_validation.py +0 -0
  332. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_deletion.py +0 -0
  333. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_deprecations.py +0 -0
  334. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_docs_examples.py +0 -0
  335. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_documentation_features.py +0 -0
  336. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_enum_cold_hydration.py +0 -0
  337. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_exception_mapping.py +0 -0
  338. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_exceptions.py +0 -0
  339. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_field_wrapper.py +0 -0
  340. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_fk_target_pk.py +0 -0
  341. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_generation_counter_dirty_tracking.py +0 -0
  342. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_global_aggregates.py +0 -0
  343. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_grouped_aggregates.py +0 -0
  344. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_helpers.py +0 -0
  345. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_hydration.py +0 -0
  346. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_hydration_equivalence.py +0 -0
  347. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_identity_memory.py +0 -0
  348. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_identity_refresh.py +0 -0
  349. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_identity_scoped_invalidation.py +0 -0
  350. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_identity_weakref.py +0 -0
  351. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_import_budget.py +0 -0
  352. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_include.py +0 -0
  353. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_label_addition.py +0 -0
  354. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_live_table_checks.py +0 -0
  355. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_metaclass_internals.py +0 -0
  356. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_metadata.py +0 -0
  357. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_migrate_plan.py +0 -0
  358. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_model_identity.py +0 -0
  359. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_models.py +0 -0
  360. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_mutation_pagination_guard.py +0 -0
  361. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_named_connections_integration.py +0 -0
  362. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_naming_single_source.py +0 -0
  363. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_negation.py +0 -0
  364. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_one_to_one.py +0 -0
  365. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_operation_seam_sync.py +0 -0
  366. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_partial_selects.py +0 -0
  367. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_primary_key_facts.py +0 -0
  368. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_provisional_import.py +0 -0
  369. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_builder.py +0 -0
  370. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_exists.py +0 -0
  371. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_immutability.py +0 -0
  372. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_joins.py +0 -0
  373. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_typing.py +0 -0
  374. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_raw_sql.py +0 -0
  375. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_refresh.py +0 -0
  376. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_registry_entrypoints.py +0 -0
  377. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_registry_owner.py +0 -0
  378. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_relation_specs.py +0 -0
  379. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_relationship_engine.py +0 -0
  380. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_relationship_resolution_errors.py +0 -0
  381. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_route_single_site.py +0 -0
  382. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_routing_errors.py +0 -0
  383. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_save_pk_edges.py +0 -0
  384. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_save_semantics.py +0 -0
  385. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_schema.py +0 -0
  386. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_schema_constraints.py +0 -0
  387. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_schema_db_type_metadata.py +0 -0
  388. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_schema_enum_annotations.py +0 -0
  389. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_session.py +0 -0
  390. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_shadow_fk_types.py +0 -0
  391. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
  392. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_static_contracts.py +0 -0
  393. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_string_search.py +0 -0
  394. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_structural_types.py +0 -0
  395. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_table_check_orphans.py +0 -0
  396. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_table_check_rebuild.py +0 -0
  397. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_table_check_reconcile.py +0 -0
  398. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_table_checks.py +0 -0
  399. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_temporal_types.py +0 -0
  400. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_transactions.py +0 -0
  401. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_traversed_projection.py +0 -0
  402. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_typed_null_binds.py +0 -0
  403. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_typed_save_bind.py +0 -0
  404. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/uv.lock +0 -0
  405. {ferro_orm-0.19.0 → ferro_orm-0.20.0}/zensical.toml +0 -0
@@ -1,6 +1,20 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.20.0 (2026-08-29)
5
+
6
+ ### Chores
7
+
8
+ - Add Context7 library config ([#357](https://github.com/syn54x/ferro-orm/pull/357),
9
+ [`ab8e79b`](https://github.com/syn54x/ferro-orm/commit/ab8e79b799e07fd64f7da8db09daa278af488877))
10
+
11
+ ### Features
12
+
13
+ - **query**: Order_by nulls= first/last placement
14
+ ([#369](https://github.com/syn54x/ferro-orm/pull/369),
15
+ [`ef6c8b4`](https://github.com/syn54x/ferro-orm/commit/ef6c8b4c458d47a88ace4407a5fb368a8d91200a))
16
+
17
+
4
18
  ## v0.19.0 (2026-08-18)
5
19
 
6
20
  ### Bug Fixes
@@ -95,9 +95,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
95
95
 
96
96
  [[package]]
97
97
  name = "cc"
98
- version = "1.4.3"
98
+ version = "1.4.4"
99
99
  source = "registry+https://github.com/rust-lang/crates.io-index"
100
- checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
100
+ checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
101
101
  dependencies = [
102
102
  "find-msvc-tools",
103
103
  "shlex",
@@ -260,7 +260,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
260
260
  dependencies = [
261
261
  "proc-macro2",
262
262
  "quote",
263
- "syn 3.0.3",
263
+ "syn 3.0.4",
264
264
  ]
265
265
 
266
266
  [[package]]
@@ -271,9 +271,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
271
271
 
272
272
  [[package]]
273
273
  name = "either"
274
- version = "1.17.0"
274
+ version = "1.18.0"
275
275
  source = "registry+https://github.com/rust-lang/crates.io-index"
276
- checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
276
+ checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
277
277
  dependencies = [
278
278
  "serde",
279
279
  ]
@@ -317,7 +317,7 @@ dependencies = [
317
317
 
318
318
  [[package]]
319
319
  name = "ferro"
320
- version = "0.19.0"
320
+ version = "0.20.0"
321
321
  dependencies = [
322
322
  "chrono",
323
323
  "dashmap",
@@ -467,7 +467,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
467
467
  dependencies = [
468
468
  "proc-macro2",
469
469
  "quote",
470
- "syn 3.0.3",
470
+ "syn 3.0.4",
471
471
  ]
472
472
 
473
473
  [[package]]
@@ -702,9 +702,9 @@ checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
702
702
 
703
703
  [[package]]
704
704
  name = "icu_provider"
705
- version = "2.3.0"
705
+ version = "2.3.1"
706
706
  source = "registry+https://github.com/rust-lang/crates.io-index"
707
- checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428"
707
+ checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
708
708
  dependencies = [
709
709
  "displaydoc",
710
710
  "icu_locale_core",
@@ -744,9 +744,9 @@ dependencies = [
744
744
 
745
745
  [[package]]
746
746
  name = "indexmap"
747
- version = "2.14.0"
747
+ version = "2.14.1"
748
748
  source = "registry+https://github.com/rust-lang/crates.io-index"
749
- checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
749
+ checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
750
750
  dependencies = [
751
751
  "equivalent",
752
752
  "hashbrown 0.17.1",
@@ -769,7 +769,7 @@ checksum = "bee2c455ca60511a054699102d40ce7153621cb2429558f9eaa600e4499b5984"
769
769
  dependencies = [
770
770
  "proc-macro2",
771
771
  "quote",
772
- "syn 3.0.3",
772
+ "syn 3.0.4",
773
773
  ]
774
774
 
775
775
  [[package]]
@@ -812,14 +812,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
812
812
 
813
813
  [[package]]
814
814
  name = "libredox"
815
- version = "0.1.20"
815
+ version = "0.1.21"
816
816
  source = "registry+https://github.com/rust-lang/crates.io-index"
817
- checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a"
817
+ checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96"
818
818
  dependencies = [
819
819
  "bitflags",
820
820
  "libc",
821
821
  "plain",
822
- "redox_syscall 0.9.2",
822
+ "redox_syscall 0.9.3",
823
823
  ]
824
824
 
825
825
  [[package]]
@@ -850,9 +850,9 @@ dependencies = [
850
850
 
851
851
  [[package]]
852
852
  name = "log"
853
- version = "0.4.33"
853
+ version = "0.4.34"
854
854
  source = "registry+https://github.com/rust-lang/crates.io-index"
855
- checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
855
+ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
856
856
 
857
857
  [[package]]
858
858
  name = "md-5"
@@ -1158,9 +1158,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
1158
1158
 
1159
1159
  [[package]]
1160
1160
  name = "rand"
1161
- version = "0.8.7"
1161
+ version = "0.8.8"
1162
1162
  source = "registry+https://github.com/rust-lang/crates.io-index"
1163
- checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
1163
+ checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
1164
1164
  dependencies = [
1165
1165
  "libc",
1166
1166
  "rand_chacha",
@@ -1197,9 +1197,9 @@ dependencies = [
1197
1197
 
1198
1198
  [[package]]
1199
1199
  name = "redox_syscall"
1200
- version = "0.9.2"
1200
+ version = "0.9.3"
1201
1201
  source = "registry+https://github.com/rust-lang/crates.io-index"
1202
- checksum = "f1c93da5bb2c5d4e6c0ef7abeead62c89169a0a4882bfb83ac892f2423aea2fe"
1202
+ checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5"
1203
1203
  dependencies = [
1204
1204
  "bitflags",
1205
1205
  ]
@@ -1263,9 +1263,9 @@ dependencies = [
1263
1263
 
1264
1264
  [[package]]
1265
1265
  name = "rustls-webpki"
1266
- version = "0.103.14"
1266
+ version = "0.103.15"
1267
1267
  source = "registry+https://github.com/rust-lang/crates.io-index"
1268
- checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
1268
+ checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
1269
1269
  dependencies = [
1270
1270
  "ring",
1271
1271
  "rustls-pki-types",
@@ -1342,7 +1342,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
1342
1342
  dependencies = [
1343
1343
  "proc-macro2",
1344
1344
  "quote",
1345
- "syn 3.0.3",
1345
+ "syn 3.0.4",
1346
1346
  ]
1347
1347
 
1348
1348
  [[package]]
@@ -1700,9 +1700,9 @@ dependencies = [
1700
1700
 
1701
1701
  [[package]]
1702
1702
  name = "syn"
1703
- version = "3.0.3"
1703
+ version = "3.0.4"
1704
1704
  source = "registry+https://github.com/rust-lang/crates.io-index"
1705
- checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
1705
+ checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
1706
1706
  dependencies = [
1707
1707
  "proc-macro2",
1708
1708
  "quote",
@@ -1743,7 +1743,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
1743
1743
  dependencies = [
1744
1744
  "proc-macro2",
1745
1745
  "quote",
1746
- "syn 3.0.3",
1746
+ "syn 3.0.4",
1747
1747
  ]
1748
1748
 
1749
1749
  [[package]]
@@ -1796,7 +1796,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
1796
1796
  dependencies = [
1797
1797
  "proc-macro2",
1798
1798
  "quote",
1799
- "syn 3.0.3",
1799
+ "syn 3.0.4",
1800
1800
  ]
1801
1801
 
1802
1802
  [[package]]
@@ -1907,9 +1907,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
1907
1907
 
1908
1908
  [[package]]
1909
1909
  name = "uuid"
1910
- version = "1.24.1"
1910
+ version = "1.26.0"
1911
1911
  source = "registry+https://github.com/rust-lang/crates.io-index"
1912
- checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9"
1912
+ checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
1913
1913
  dependencies = [
1914
1914
  "getrandom 0.4.3",
1915
1915
  "js-sys",
@@ -2309,9 +2309,9 @@ dependencies = [
2309
2309
 
2310
2310
  [[package]]
2311
2311
  name = "zerovec"
2312
- version = "0.11.7"
2312
+ version = "0.11.8"
2313
2313
  source = "registry+https://github.com/rust-lang/crates.io-index"
2314
- checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8"
2314
+ checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
2315
2315
  dependencies = [
2316
2316
  "yoke",
2317
2317
  "zerofrom",
@@ -2320,13 +2320,13 @@ dependencies = [
2320
2320
 
2321
2321
  [[package]]
2322
2322
  name = "zerovec-derive"
2323
- version = "0.11.5"
2323
+ version = "0.11.6"
2324
2324
  source = "registry+https://github.com/rust-lang/crates.io-index"
2325
- checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9"
2325
+ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
2326
2326
  dependencies = [
2327
2327
  "proc-macro2",
2328
2328
  "quote",
2329
- "syn 3.0.3",
2329
+ "syn 3.0.4",
2330
2330
  ]
2331
2331
 
2332
2332
  [[package]]
@@ -3,7 +3,7 @@ members = ["crates/ferro-schema-ir", "crates/ferro-ddl-lowering", "crates/ferro-
3
3
 
4
4
  [package]
5
5
  name = "ferro"
6
- version = "0.19.0"
6
+ version = "0.20.0"
7
7
  edition = "2024"
8
8
  readme = "README.md"
9
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ferro-orm
3
- Version: 0.19.0
3
+ Version: 0.20.0
4
4
  Requires-Dist: pydantic>=2.0
5
5
  Requires-Dist: alembic>=1.18.1 ; extra == 'alembic'
6
6
  Requires-Dist: sqlalchemy>=2.0.46 ; extra == 'alembic'
@@ -0,0 +1,4 @@
1
+ {
2
+ "url": "https://context7.com/syn54x/ferro-orm",
3
+ "public_key": "pk_D3FotZlCoANprl1K80IM9"
4
+ }
@@ -412,6 +412,11 @@ pub struct QueryOrderBy {
412
412
  /// Relation field names from the root model to the ordered column's table;
413
413
  /// `[]` = root model (#269 requires this empty until #270 renders joins).
414
414
  pub path: Vec<String>,
415
+ /// Optional NULLS placement (`"first"` / `"last"`, case-insensitive in the
416
+ /// planner). Omitted on the wire when unset so existing fixtures keep
417
+ /// byte-identical round-trips (#361).
418
+ #[serde(default, skip_serializing_if = "Option::is_none")]
419
+ pub nulls: Option<String>,
415
420
  }
416
421
 
417
422
  /// Predicate tree node in query IR: leaf comparison, compound AND/OR, a
@@ -1313,6 +1318,64 @@ mod tests {
1313
1318
  );
1314
1319
  }
1315
1320
 
1321
+ #[test]
1322
+ fn query_order_by_nulls_optional_wire() {
1323
+ // #361: optional `nulls` on an order_by term — present key round-trips;
1324
+ // absent key deserializes as None and is omitted on serialize.
1325
+ let with: QueryOrderBy = serde_json::from_value(serde_json::json!({
1326
+ "column": "pinned_at",
1327
+ "direction": "desc",
1328
+ "path": [],
1329
+ "nulls": "last"
1330
+ }))
1331
+ .expect("order_by with nulls must deserialize");
1332
+ assert_eq!(with.nulls.as_deref(), Some("last"));
1333
+ let encoded = serde_json::to_value(&with).expect("serialize with nulls");
1334
+ assert_eq!(encoded["nulls"], "last");
1335
+
1336
+ let without: QueryOrderBy = serde_json::from_value(serde_json::json!({
1337
+ "column": "pinned_at",
1338
+ "direction": "desc",
1339
+ "path": []
1340
+ }))
1341
+ .expect("order_by without nulls must deserialize");
1342
+ assert_eq!(without.nulls, None);
1343
+ let encoded = serde_json::to_value(&without).expect("serialize without nulls");
1344
+ assert!(
1345
+ encoded.get("nulls").is_none(),
1346
+ "unset nulls must be omitted from the wire: {encoded}"
1347
+ );
1348
+ }
1349
+
1350
+ #[test]
1351
+ fn query_card_nulls_fixture_roundtrip() {
1352
+ // #363: golden vector with nulls on the first order_by term and the
1353
+ // key absent on the second — deserialize, pin shape, round-trip.
1354
+ let fixture =
1355
+ include_str!("../../../tests/fixtures/ir_vectors/query_card_nulls_v7.json");
1356
+ let parsed: serde_json::Value =
1357
+ serde_json::from_str(fixture).expect("query card-nulls fixture must parse");
1358
+ let ir = parsed
1359
+ .get("ir")
1360
+ .cloned()
1361
+ .expect("fixture must contain ir envelope");
1362
+ let envelope: IrEnvelope<QueryIrPayload> =
1363
+ serde_json::from_value(ir.clone()).expect("query card-nulls IR must deserialize");
1364
+ assert_eq!(envelope.ir_version, 7);
1365
+ assert_eq!(envelope.payload.order_by.len(), 2);
1366
+ assert_eq!(
1367
+ envelope.payload.order_by[0].nulls.as_deref(),
1368
+ Some("last")
1369
+ );
1370
+ assert!(
1371
+ envelope.payload.order_by[1].nulls.is_none(),
1372
+ "second term must omit nulls"
1373
+ );
1374
+ let encoded =
1375
+ serde_json::to_value(&envelope).expect("query card-nulls IR must serialize");
1376
+ assert_eq!(encoded, ir, "query card-nulls round-trip must not drift");
1377
+ }
1378
+
1316
1379
  #[test]
1317
1380
  fn codec_fixture_roundtrip() {
1318
1381
  let fixture =
@@ -1,6 +1,7 @@
1
1
  """Runnable companion to the Queries guide (docs/pages/guide/queries.md)."""
2
2
 
3
3
  import asyncio
4
+ from datetime import UTC, datetime
4
5
 
5
6
  # --8<-- [start:setup]
6
7
  from ferro import Field, Model, connect, engines
@@ -23,6 +24,15 @@ class Invoice(Model):
23
24
  # --8<-- [end:nullable-model]
24
25
 
25
26
 
27
+ # --8<-- [start:card-model]
28
+ class Card(Model):
29
+ id: int | None = Field(default=None, primary_key=True)
30
+ title: str
31
+ pinned_at: datetime | None = None
32
+ updated_at: datetime
33
+ # --8<-- [end:card-model]
34
+
35
+
26
36
  async def main() -> None:
27
37
  await connect("sqlite::memory:", auto_migrate=True)
28
38
 
@@ -108,6 +118,34 @@ async def main() -> None:
108
118
  assert oldest_first[0].name == "carol"
109
119
  assert len(second_page) == 2
110
120
 
121
+ t0 = datetime(2026, 1, 1, tzinfo=UTC)
122
+ t1 = datetime(2026, 2, 1, tzinfo=UTC)
123
+ t2 = datetime(2026, 3, 1, tzinfo=UTC)
124
+ await Card.bulk_create(
125
+ [
126
+ Card(title="unpinned-old", pinned_at=None, updated_at=t0),
127
+ Card(title="pinned-early", pinned_at=t0, updated_at=t0),
128
+ Card(title="unpinned-new", pinned_at=None, updated_at=t2),
129
+ Card(title="pinned-late", pinned_at=t1, updated_at=t1),
130
+ ]
131
+ )
132
+
133
+ # --8<-- [start:nulls-ordering]
134
+ cards = (
135
+ await Card.select()
136
+ .order_by(lambda card: card.pinned_at, "desc", nulls="last")
137
+ .order_by(lambda card: card.updated_at, "desc")
138
+ .order_by(lambda card: card.id, "desc")
139
+ .all()
140
+ )
141
+ # --8<-- [end:nulls-ordering]
142
+ assert [card.title for card in cards] == [
143
+ "pinned-late",
144
+ "pinned-early",
145
+ "unpinned-new",
146
+ "unpinned-old",
147
+ ]
148
+
111
149
  # --8<-- [start:terminals]
112
150
  everyone = await User.all()
113
151
  first_admin = await User.where(lambda user: user.role == "admin").first()
@@ -1,6 +1,7 @@
1
1
  """Annotated-style companion to predicates.py (AGENTS.md I-7)."""
2
2
 
3
3
  import asyncio
4
+ from datetime import UTC, datetime
4
5
 
5
6
  # --8<-- [start:setup]
6
7
  from typing import Annotated
@@ -25,6 +26,15 @@ class Invoice(Model):
25
26
  # --8<-- [end:nullable-model]
26
27
 
27
28
 
29
+ # --8<-- [start:card-model]
30
+ class Card(Model):
31
+ id: Annotated[int | None, Field(default=None, primary_key=True)]
32
+ title: str
33
+ pinned_at: datetime | None = None
34
+ updated_at: datetime
35
+ # --8<-- [end:card-model]
36
+
37
+
28
38
  async def main() -> None:
29
39
  await connect("sqlite::memory:", auto_migrate=True)
30
40
 
@@ -53,6 +63,23 @@ async def main() -> None:
53
63
  assert len(await Invoice.where(lambda invoice: invoice.amount > 100).all()) == 1
54
64
  assert len(await Invoice.where(lambda invoice: ~(invoice.amount > 100)).all()) == 0
55
65
 
66
+ t0 = datetime(2026, 1, 1, tzinfo=UTC)
67
+ t1 = datetime(2026, 2, 1, tzinfo=UTC)
68
+ await Card.bulk_create(
69
+ [
70
+ Card(title="unpinned", pinned_at=None, updated_at=t1),
71
+ Card(title="pinned", pinned_at=t0, updated_at=t0),
72
+ ]
73
+ )
74
+ cards = (
75
+ await Card.select()
76
+ .order_by(lambda card: card.pinned_at, "desc", nulls="last")
77
+ .order_by(lambda card: card.updated_at, "desc")
78
+ .order_by(lambda card: card.id, "desc")
79
+ .all()
80
+ )
81
+ assert [card.title for card in cards] == ["pinned", "unpinned"]
82
+
56
83
  print("predicates_annotated example ran successfully")
57
84
 
58
85
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  Prefix `~` negates **any** predicate — leaf comparison or `&`/`|` compound — rendering as SQL `NOT (...)` over the condition it wraps (ADR-0008). It is the universal negation rule: there are no per-operator negative forms (`~t.role.in_([...])` is NOT IN, `~t.email.like(p)` is NOT LIKE), and double negation nests. Like SQL `NOT` and the `!=` operator, a negated comparison excludes rows where the compared column is `NULL` — see [Negation and NULL values](../guide/queries.md#negation-and-null-values).
6
6
 
7
- `where()` and `order_by()` lambdas may **traverse** a forward-FK relation (`lambda t: t.account.ledger_id == 1`): each hop renders one INNER join, deduplicated by relation path (ADR-0006). `join()` forces a join on a relation path (a bare `join()` is an existence filter on a nullable relation), and `left_join()` marks the whole path LEFT to keep relation-less rows. See the [Querying Across Relationships](../guide/queries.md#querying-across-relationships) guide for worked examples.
7
+ `where()` and `order_by()` lambdas may **traverse** a forward-FK relation (`lambda t: t.account.ledger_id == 1`): each hop renders one INNER join, deduplicated by relation path (ADR-0006). `join()` forces a join on a relation path (a bare `join()` is an existence filter on a nullable relation), and `left_join()` marks the whole path LEFT to keep relation-less rows. `order_by(..., nulls="first" | "last")` pins `NULL` placement when a sort key is nullable (dialect defaults otherwise — see [Ordering, Limit & Offset](../guide/queries.md#ordering-limit--offset)). See the [Querying Across Relationships](../guide/queries.md#querying-across-relationships) guide for worked examples.
8
8
 
9
9
  A reverse (`BackRef`) or many-to-many relation in a predicate supports exactly one verb — the **existence test** `t.rel.exists(inner_lambda=None)` (ADR-0007). It renders as a correlated `EXISTS` at every cardinality (never a join, so the result stays root-shaped and each matching root returns once), negates with `~`, and the optional inner lambda is a full ferro predicate over the related model (operators, `&`/`|`/`~`, forward traversal rendered inside the subquery, nested tests). Everything else on a reverse edge — column access, comparisons (including `!= None`), `in_` (including a query RHS), `join()`/`left_join()` — raises at build time naming `.exists()`; an inner lambda referencing any scope but its own parameter is likewise rejected ([#309](https://github.com/syn54x/ferro-orm/issues/309)). See [Existence Tests](../guide/queries.md#existence-tests-on-reverse-many-to-many-relations) for worked examples.
10
10
 
@@ -166,7 +166,31 @@ For a row where `amount` is `NULL`, `amount > 100` is unknown, `NOT unknown` is
166
166
 
167
167
  ## Ordering, Limit & Offset
168
168
 
169
- Sort with `.order_by(field, direction)` (direction defaults to ascending; pass `"desc"` to reverse) and slice with `.limit()` / `.offset()`. `field` is a lambda naming the column (`order_by(lambda u: u.created_at, "desc")`, matching the `where()` predicate style) or a column-name string (`order_by("created_at", "desc")`). Both forms are validated against the model's queryable columns at build time:
169
+ Sort with `.order_by(field, direction, *, nulls=...)` (direction defaults to ascending; pass `"desc"` to reverse) and slice with `.limit()` / `.offset()`. Pass `nulls="first"` or `nulls="last"` to pin where `NULL` sort keys land. `field` is a lambda naming the column (`order_by(lambda u: u.created_at, "desc")`, matching the `where()` predicate style) or a column-name string (`order_by("created_at", "desc")`). Both forms are validated against the model's queryable columns at build time.
170
+
171
+ A pinned-first list is the usual reason to care — put unpinned cards (`pinned_at IS NULL`) after pinned ones, then break ties by recency:
172
+
173
+ === "Assignment"
174
+
175
+ ```python
176
+ --8<-- "docs/examples/predicates.py:card-model"
177
+ ```
178
+
179
+ === "Annotated"
180
+
181
+ ```python
182
+ --8<-- "docs/examples/predicates_annotated.py:card-model"
183
+ ```
184
+
185
+ ```python
186
+ --8<-- "docs/examples/predicates.py:nulls-ordering"
187
+ ```
188
+
189
+ ```sql
190
+ ORDER BY pinned_at DESC NULLS LAST, updated_at DESC, id DESC
191
+ ```
192
+
193
+ Omitting `nulls=` on a nullable sort key leaves placement to the dialect: PostgreSQL `DESC` puts `NULL`s first; SQLite `DESC` puts them last. Pass `nulls=` when you care.
170
194
 
171
195
  ```python
172
196
  --8<-- "docs/examples/predicates.py:ordering-slicing"
@@ -342,8 +366,8 @@ When you want the relation-less rows *kept* rather than filtered out, opt into a
342
366
 
343
367
  A bare `left_join` on a path also traversed by `where()` lifts the shared edge to LEFT — an explicit LEFT always beats an implicit INNER on the same edge (declaring `join` **and** `left_join` on one edge is a build-time `ValueError`).
344
368
 
345
- !!! note "NULL ordering diverges by dialect under `left_join`"
346
- Once relation-less rows survive into an `order_by` on a related column, their `NULL` sort key lands in a dialect-specific spot: **PostgreSQL sorts `NULL`s last on an ascending sort; SQLite sorts them first.** This divergence only appears because you opted into `left_join` — plain INNER traversal drops those rows, so it never surfaces (ADR-0006).
369
+ !!! note "NULL ordering is dialect-defined unless you set `nulls=`"
370
+ Relation-less rows under `left_join` land as `NULL` sort keys on a related column — the same dialect-default split that hits a nullable **root** column. Pass `nulls="first"` or `nulls="last"` when you care — see [Ordering, Limit & Offset](#ordering-limit--offset).
347
371
 
348
372
  ### Two foreign keys to the same table
349
373
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ferro-orm"
3
- version = "0.19.0"
3
+ version = "0.20.0"
4
4
  description = "A high-performance, Rust-backed ORM for Python."
5
5
  readme = "README.md"
6
6
  authors = [