ferro-orm 0.16.2__tar.gz → 0.17.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 (384) hide show
  1. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/CHANGELOG.md +45 -0
  2. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/CONTEXT.md +16 -0
  3. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/Cargo.lock +32 -32
  4. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/Cargo.toml +1 -1
  5. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/PKG-INFO +1 -1
  6. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-schema-ir/src/lib.rs +263 -16
  7. ferro_orm-0.17.0/docs/adr/0007-reverse-relations-tested-not-traversed.md +62 -0
  8. ferro_orm-0.17.0/docs/adr/0008-predicate-negation-uniform-not.md +30 -0
  9. ferro_orm-0.17.0/docs/examples/existence_tests.py +195 -0
  10. ferro_orm-0.17.0/docs/examples/existence_tests_annotated.py +101 -0
  11. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/predicates.py +42 -0
  12. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/predicates_annotated.py +22 -0
  13. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/queries.md +4 -0
  14. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/queries.md +172 -4
  15. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/roadmap.md +2 -2
  16. ferro_orm-0.17.0/docs/plans/2026-07-18-016-existence-tests-design.md +161 -0
  17. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/pyproject.toml +1 -1
  18. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/__init__.py +25 -30
  19. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/_shadow_fk_types.py +5 -6
  20. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/columns.py +124 -2
  21. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/ir/compiler.py +40 -40
  22. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/metaclass.py +7 -6
  23. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/models.py +36 -28
  24. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/query/builder.py +65 -91
  25. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/query/nodes.py +484 -24
  26. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/query/wire.py +99 -35
  27. ferro_orm-0.17.0/src/ferro/registry.py +328 -0
  28. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/relations/__init__.py +19 -25
  29. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/relations/descriptors.py +13 -10
  30. ferro_orm-0.17.0/src/ferro/state.py +176 -0
  31. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/operations.rs +147 -29
  32. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/query.rs +572 -4
  33. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/conftest.py +8 -29
  34. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/README.md +7 -5
  35. ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_account_exists_v7.json +40 -0
  36. ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_account_scoped_exists_v7.json +74 -0
  37. ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_owner_nested_exists_v7.json +47 -0
  38. ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_owner_not_exists_v7.json +37 -0
  39. ferro_orm-0.16.2/tests/fixtures/ir_vectors/query_transaction_aggregate_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_aggregate_v7.json +2 -2
  40. ferro_orm-0.16.2/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v7.json +2 -2
  41. ferro_orm-0.16.2/tests/fixtures/ir_vectors/query_transaction_include_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_include_v7.json +2 -2
  42. ferro_orm-0.16.2/tests/fixtures/ir_vectors/query_transaction_left_join_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_left_join_v7.json +2 -2
  43. ferro_orm-0.16.2/tests/fixtures/ir_vectors/query_transaction_record_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_record_v7.json +2 -2
  44. ferro_orm-0.16.2/tests/fixtures/ir_vectors/query_transaction_traversal_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_traversal_v7.json +2 -2
  45. ferro_orm-0.16.2/tests/fixtures/ir_vectors/query_transaction_traversed_record_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_traversed_record_v7.json +2 -2
  46. ferro_orm-0.16.2/tests/fixtures/ir_vectors/query_user_compound_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_user_compound_v7.json +2 -2
  47. ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_user_m2m_exists_v7.json +51 -0
  48. ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_user_not_compound_v7.json +49 -0
  49. ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_user_not_leaf_v7.json +44 -0
  50. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_alembic_autogenerate.py +6 -12
  51. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_alembic_bridge.py +2 -4
  52. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_alembic_db_type.py +3 -9
  53. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_alembic_nullability.py +2 -4
  54. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_alembic_type_mapping.py +2 -4
  55. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_auto_migrate.py +25 -46
  56. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_catalog_cache.py +2 -4
  57. ferro_orm-0.17.0/tests/test_compiled_query.py +81 -0
  58. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_composite_index.py +3 -7
  59. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_composite_unique.py +3 -7
  60. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_connection.py +2 -2
  61. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_cross_emitter_parity.py +5 -11
  62. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_db_type_cross_emitter_parity.py +3 -11
  63. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_db_type_integration.py +4 -6
  64. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_documentation_features.py +4 -4
  65. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_enum_cold_hydration.py +6 -6
  66. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_fk_target_pk.py +6 -14
  67. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_generation_counter_dirty_tracking.py +16 -14
  68. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_hydration_equivalence.py +3 -7
  69. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_include.py +1 -1
  70. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_ir_vectors_contract.py +71 -17
  71. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_model_identity.py +13 -25
  72. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_mutation_pagination_guard.py +1 -1
  73. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_named_connections_integration.py +2 -2
  74. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_naming_single_source.py +8 -12
  75. ferro_orm-0.17.0/tests/test_negation.py +226 -0
  76. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_one_to_one.py +2 -3
  77. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_operation_seam_sync.py +3 -3
  78. ferro_orm-0.17.0/tests/test_primary_key_facts.py +77 -0
  79. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_provisional_import.py +9 -9
  80. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_builder.py +4 -4
  81. ferro_orm-0.17.0/tests/test_query_exists.py +707 -0
  82. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_joins.py +15 -3
  83. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_typing.py +2 -3
  84. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_wire_vectors.py +87 -15
  85. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_registry_entrypoints.py +69 -74
  86. ferro_orm-0.17.0/tests/test_registry_owner.py +119 -0
  87. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_relationship_engine.py +2 -3
  88. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_relationship_resolution_errors.py +3 -7
  89. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_session.py +2 -2
  90. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_shadow_fk_types.py +4 -6
  91. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_sqlite_alembic_reconnect_hydration.py +2 -4
  92. ferro_orm-0.16.2/src/ferro/state.py +0 -355
  93. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  94. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  95. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/PERMISSIONS.md +0 -0
  96. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/PYPI_CHECKLIST.md +0 -0
  97. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/PYPI_SETUP.md +0 -0
  98. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/generated/wheels.generated.yml +0 -0
  99. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/pull_request_template.md +0 -0
  100. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/workflows/ci.yml +0 -0
  101. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/workflows/packaging-smoke.yml +0 -0
  102. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/workflows/publish-docs.yml +0 -0
  103. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/workflows/publish.yml +0 -0
  104. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/workflows/release.yml +0 -0
  105. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.gitignore +0 -0
  106. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.pre-commit-config.yaml +0 -0
  107. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.python-version +0 -0
  108. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/AGENTS.md +0 -0
  109. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/CONTRIBUTING.md +0 -0
  110. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/LICENSE +0 -0
  111. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/README.md +0 -0
  112. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/README.md +0 -0
  113. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/__init__.py +0 -0
  114. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/__main__.py +0 -0
  115. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/backends.py +0 -0
  116. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/baselines/postgres.json +0 -0
  117. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/baselines/sqlite.json +0 -0
  118. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/compare.py +0 -0
  119. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/harness.py +0 -0
  120. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/model.py +0 -0
  121. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/run.py +0 -0
  122. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
  123. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-ddl-lowering/src/lib.rs +0 -0
  124. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-migrate/Cargo.toml +0 -0
  125. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-migrate/src/emit.rs +0 -0
  126. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-migrate/src/lib.rs +0 -0
  127. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-migrate/src/order.rs +0 -0
  128. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-migrate/src/tests.rs +0 -0
  129. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-schema-ir/Cargo.toml +0 -0
  130. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0001-resolved-epoch-registration.md +0 -0
  131. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
  132. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
  133. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
  134. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
  135. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
  136. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
  137. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0008-populated-relations-include.md +0 -0
  138. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
  139. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/agents/domain.md +0 -0
  140. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/agents/issue-tracker.md +0 -0
  141. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/agents/triage-labels.md +0 -0
  142. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/architecture-review-2026-07-08.html +0 -0
  143. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
  144. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
  145. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
  146. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
  147. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
  148. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
  149. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
  150. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
  151. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-26-ir-p8.5-143-db-type-drop-requirements.md +0 -0
  152. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
  153. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-28-ir-p8.5-141-single-schema-ir-producer-requirements.md +0 -0
  154. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-29-ir-p8.6-146-dialect-enum-unification-requirements.md +0 -0
  155. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-29-ir-p8.6-153-create-path-ir-requirements.md +0 -0
  156. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
  157. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
  158. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-01-ir-p8.6-154-datetime-tz-coarseness-requirements.md +0 -0
  159. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-01-ir-p8.6-162-typed-save-bind-requirements.md +0 -0
  160. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-01-ir-p8.6-165-blob-introspection-false-positive-requirements.md +0 -0
  161. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
  162. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
  163. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/aggregations.py +0 -0
  164. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/aggregations_annotated.py +0 -0
  165. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/multiple_databases.py +0 -0
  166. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/mutations.py +0 -0
  167. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/pagination.py +0 -0
  168. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/partial_selects.py +0 -0
  169. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/partial_selects_annotated.py +0 -0
  170. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/populated_relations.py +0 -0
  171. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/populated_relations_annotated.py +0 -0
  172. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/quickstart.py +0 -0
  173. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/quickstart_annotated.py +0 -0
  174. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/raw_sql.py +0 -0
  175. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/relationships.py +0 -0
  176. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/relationships_annotated.py +0 -0
  177. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/soft_deletes.py +0 -0
  178. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/soft_deletes_annotated.py +0 -0
  179. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/testing_conftest.py +0 -0
  180. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/timestamps.py +0 -0
  181. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/timestamps_annotated.py +0 -0
  182. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/transactions.py +0 -0
  183. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/traversal.py +0 -0
  184. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/traversal_annotated.py +0 -0
  185. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/connection.md +0 -0
  186. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/exceptions.md +0 -0
  187. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/fields.md +0 -0
  188. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/migrations.md +0 -0
  189. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/model.md +0 -0
  190. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/raw-sql.md +0 -0
  191. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/relationships.md +0 -0
  192. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/transactions.md +0 -0
  193. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/changelog.md +0 -0
  194. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/architecture.md +0 -0
  195. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/backends.md +0 -0
  196. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/identity-map.md +0 -0
  197. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/performance.md +0 -0
  198. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/query-typing.md +0 -0
  199. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/type-safety.md +0 -0
  200. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/contributing.md +0 -0
  201. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/faq.md +0 -0
  202. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/getting-started/installation.md +0 -0
  203. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/getting-started/next-steps.md +0 -0
  204. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/getting-started/quickstart.md +0 -0
  205. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/aggregations.md +0 -0
  206. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/connections.md +0 -0
  207. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/migrations.md +0 -0
  208. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/models-and-fields.md +0 -0
  209. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/mutations.md +0 -0
  210. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/raw-sql.md +0 -0
  211. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/relationships.md +0 -0
  212. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/transactions.md +0 -0
  213. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
  214. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/multiple-databases.md +0 -0
  215. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/pagination.md +0 -0
  216. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/soft-deletes.md +0 -0
  217. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/testing.md +0 -0
  218. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/timestamps.md +0 -0
  219. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/upgrade-guide.md +0 -0
  220. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/index.md +0 -0
  221. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/stylesheets/extra.css +0 -0
  222. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/why-ferro.md +0 -0
  223. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
  224. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
  225. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
  226. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
  227. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
  228. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
  229. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
  230. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
  231. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-24-001-feat-ir-p8-119-wire-automigrate-plan.md +0 -0
  232. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-25-001-feat-ir-p8-120-parity-gate-plan.md +0 -0
  233. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-26-001-feat-ir-p8.5-140-shared-lowering-plan.md +0 -0
  234. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-26-002-feat-ir-p8.5-143-db-type-drop-plan.md +0 -0
  235. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-26-003-feat-ir-p8.5-144-reconcile-indexes-plan.md +0 -0
  236. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-28-001-feat-ir-p8.5-141-single-schema-ir-producer-plan.md +0 -0
  237. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-29-001-refactor-ir-p8.6-153-create-path-unification-plan.md +0 -0
  238. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-29-002-refactor-ir-p8.6-146-dialect-enum-unification-plan.md +0 -0
  239. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-30-001-refactor-ir-p8.6-158-check-renderer-plan.md +0 -0
  240. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-30-002-fix-ir-p8.6-155-deferred-annotations-plan.md +0 -0
  241. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-01-001-feat-ir-p8.6-162-typed-save-bind-plan.md +0 -0
  242. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-01-002-fix-ir-p8.6-154-datetime-tz-coarseness-plan.md +0 -0
  243. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-01-003-fix-ir-p8.6-165-blob-introspection-plan.md +0 -0
  244. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
  245. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-02-002-feat-ff-a-172-typed-exceptions-plan.md +0 -0
  246. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
  247. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +0 -0
  248. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
  249. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
  250. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
  251. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
  252. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-009-ff-f-query-builder-1.0-design.md +0 -0
  253. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-010-ff-f-query-builder-1.0-plan.md +0 -0
  254. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
  255. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
  256. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
  257. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
  258. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
  259. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/ir-first-migration-guide.md +0 -0
  260. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/ir-first-release-checklist.md +0 -0
  261. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/rfc/ir-contracts-v1.md +0 -0
  262. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/README.md +0 -0
  263. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
  264. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
  265. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
  266. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
  267. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
  268. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
  269. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/self-fk-evicts-component-from-create-order.md +0 -0
  270. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
  271. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
  272. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
  273. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
  274. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
  275. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
  276. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
  277. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/foreign-key-index.md +0 -0
  278. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
  279. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/ir-invariants.md +0 -0
  280. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/persistence-state.md +0 -0
  281. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
  282. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
  283. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/typed-null-binds.md +0 -0
  284. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/justfile +0 -0
  285. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/scripts/demo_queries.py +0 -0
  286. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/scripts/demo_queries_pre_v012.py +0 -0
  287. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/backend.rs +0 -0
  288. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/codec.rs +0 -0
  289. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/codec_plan.rs +0 -0
  290. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/connection.rs +0 -0
  291. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/errors.rs +0 -0
  292. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/_annotation_utils.py +0 -0
  293. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/_bind_payload.py +0 -0
  294. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/_core.pyi +0 -0
  295. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/_deprecations.py +0 -0
  296. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/base.py +0 -0
  297. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/composite_indexes.py +0 -0
  298. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/composite_uniques.py +0 -0
  299. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/exceptions.py +0 -0
  300. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/fields.py +0 -0
  301. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/ir/__init__.py +0 -0
  302. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/migrations/__init__.py +0 -0
  303. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/migrations/alembic.py +0 -0
  304. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/py.typed +0 -0
  305. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/query/__init__.py +0 -0
  306. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/query/rows.py +0 -0
  307. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/raw.py +0 -0
  308. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/session.py +0 -0
  309. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/hydration.rs +0 -0
  310. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/introspect.rs +0 -0
  311. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/lib.rs +0 -0
  312. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/migrate.rs +0 -0
  313. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/naming_ffi.rs +0 -0
  314. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/schema.rs +0 -0
  315. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/schema_bind.rs +0 -0
  316. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/state.rs +0 -0
  317. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/__init__.py +0 -0
  318. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/db_backends.py +0 -0
  319. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
  320. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
  321. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
  322. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
  323. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/bad_includes.py +0 -0
  324. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/bad_predicates.py +0 -0
  325. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/bad_projections.py +0 -0
  326. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/good_includes.py +0 -0
  327. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/good_predicates.py +0 -0
  328. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/good_projections.py +0 -0
  329. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_aggregation.py +0 -0
  330. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_annotation_utils.py +0 -0
  331. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_bulk_create_chunking.py +0 -0
  332. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_bulk_install.py +0 -0
  333. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_bulk_update.py +0 -0
  334. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_codec_plan.py +0 -0
  335. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_column_specs.py +0 -0
  336. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_connection_redaction.py +0 -0
  337. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_constraints.py +0 -0
  338. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_crud.py +0 -0
  339. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_db_backends.py +0 -0
  340. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_db_type_typing.py +0 -0
  341. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_db_type_validation.py +0 -0
  342. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_deletion.py +0 -0
  343. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_deprecations.py +0 -0
  344. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_docs_examples.py +0 -0
  345. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_exception_mapping.py +0 -0
  346. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_exceptions.py +0 -0
  347. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_field_wrapper.py +0 -0
  348. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_global_aggregates.py +0 -0
  349. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_grouped_aggregates.py +0 -0
  350. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_helpers.py +0 -0
  351. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_hydration.py +0 -0
  352. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_identity_memory.py +0 -0
  353. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_identity_refresh.py +0 -0
  354. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_identity_scoped_invalidation.py +0 -0
  355. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_identity_weakref.py +0 -0
  356. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_import_budget.py +0 -0
  357. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_metaclass_internals.py +0 -0
  358. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_metadata.py +0 -0
  359. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_migrate_plan.py +0 -0
  360. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_models.py +0 -0
  361. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_partial_selects.py +0 -0
  362. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_column_validation.py +0 -0
  363. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_immutability.py +0 -0
  364. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_raw_sql.py +0 -0
  365. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_refresh.py +0 -0
  366. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_relation_specs.py +0 -0
  367. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_route_single_site.py +0 -0
  368. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_routing_errors.py +0 -0
  369. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_save_pk_edges.py +0 -0
  370. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_save_semantics.py +0 -0
  371. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_schema.py +0 -0
  372. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_schema_constraints.py +0 -0
  373. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_schema_db_type_metadata.py +0 -0
  374. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_schema_enum_annotations.py +0 -0
  375. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_static_contracts.py +0 -0
  376. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_string_search.py +0 -0
  377. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_structural_types.py +0 -0
  378. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_temporal_types.py +0 -0
  379. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_transactions.py +0 -0
  380. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_traversed_projection.py +0 -0
  381. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_typed_null_binds.py +0 -0
  382. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_typed_save_bind.py +0 -0
  383. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/uv.lock +0 -0
  384. {ferro_orm-0.16.2 → ferro_orm-0.17.0}/zensical.toml +0 -0
@@ -1,6 +1,51 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.17.0 (2026-07-18)
5
+
6
+ ### Documentation
7
+
8
+ - Design existence tests (.exists()) and uniform ~ negation
9
+ ([`c68a477`](https://github.com/syn54x/ferro-orm/commit/c68a47780cd5fe6d8ecfcb4f7e0f9c8ec2b798fe))
10
+
11
+ - **query**: Document universal ~ negation and three-valued logic
12
+ ([`6061d66`](https://github.com/syn54x/ferro-orm/commit/6061d66e96c99f089339f7a4a83b9424101da8be))
13
+
14
+ ### Features
15
+
16
+ - **query**: Bare .exists() on reverse FK — existence-test tracer bullet
17
+ ([#319](https://github.com/syn54x/ferro-orm/pull/319),
18
+ [`2538e06`](https://github.com/syn54x/ferro-orm/commit/2538e06b217e449d10dd564687f5ca3581d5d08d))
19
+
20
+ - **query**: Existence-test error surfaces and docs — the feature's edges
21
+ ([`be88aed`](https://github.com/syn54x/ferro-orm/commit/be88aed35f0975f9333bf0dabacaffc8a82f8f34))
22
+
23
+ - **query**: M2M existence tests — the two-hop correlation path
24
+ ([`336b529`](https://github.com/syn54x/ferro-orm/commit/336b5298d0e7beca8231452655544fcc7a15ad6a))
25
+
26
+ - **query**: Scoped existence tests — inner predicates, nesting, cross-scope guard
27
+ ([`df11625`](https://github.com/syn54x/ferro-orm/commit/df11625b90e01d293f4863bec8afafb9110a1ac3))
28
+
29
+ - **query**: Uniform predicate negation — prefix ~ as a NOT wire node
30
+ ([`bfa3126`](https://github.com/syn54x/ferro-orm/commit/bfa3126914b5a1b5d93a65ee3fe2f066bb314b1f))
31
+
32
+
33
+ ## v0.16.3 (2026-07-16)
34
+
35
+ ### Bug Fixes
36
+
37
+ - Derive the primary-key fact once and reject multi-PK models at class definition
38
+ ([`ef91fa0`](https://github.com/syn54x/ferro-orm/commit/ef91fa09e09d5e7444e8d659b831921d40bcb365))
39
+
40
+ ### Refactoring
41
+
42
+ - Compile_query returns one CompiledQuery artifact carrying the hop-class map
43
+ ([`fc7084c`](https://github.com/syn54x/ferro-orm/commit/fc7084c4ad89b135815f74852263989f23868143))
44
+
45
+ - Single Registry owner for Python-side registration state
46
+ ([`1179fcc`](https://github.com/syn54x/ferro-orm/commit/1179fcc07cc9e4305b7e7a8c141b8b29264dcd26))
47
+
48
+
4
49
  ## v0.16.2 (2026-07-15)
5
50
 
6
51
  ### Bug Fixes
@@ -56,6 +56,10 @@ _Avoid_: Join inference, nested filter, path lookup, string path
56
56
  The ordered sequence of forward-FK hops a traversal walks (`account`, or `account → owner`). A path is the identity of a join: the same path referenced anywhere in a query is one join, and distinct paths to the same model are distinct joins. Left-join requests apply to a whole path.
57
57
  _Avoid_: Join alias, lookup chain, dotted path string
58
58
 
59
+ **Existence test**:
60
+ The only predicate form on a reverse or many-to-many relation — `t.lines.exists(...)` — asking whether at least one related row exists, optionally scoped by a full inner predicate over the related model. Renders as a correlated EXISTS; the result stays root-shaped (shape-preserving query), so it composes with any other predicate, ordering, and paging. Reverse relations are *tested*, never *traversed*: traversal remains a forward-FK concept.
61
+ _Avoid_: membership filter, semi-join filter, reverse traversal, subquery filter, `.any()`
62
+
59
63
  **Shape-preserving query**:
60
64
  The invariant that filtering and ordering never change what a query returns — a query over Transaction yields Transaction instances regardless of which relations its predicates traverse. Only an explicit projection operation may change the result shape.
61
65
  _Avoid_: Implicit projection, row narrowing
@@ -84,6 +88,10 @@ _Avoid_: Select list, projection spec, hydration mode flag
84
88
  The single typed wire artifact a query ships to the Rust runtime — model identity, predicates, ordering, paging, joins, and exactly one materialization plan, inside a versioned envelope. Compiled only by `compile_query`; no other code assembles query wire shape.
85
89
  _Avoid_: Query dict, query def, payload dict
86
90
 
91
+ **Compiled query**:
92
+ The single artifact `compile_query` returns: the QueryIR payload, its wire JSON, and the plan-scoped hop-class map, all views of one compile. The map is collected from the hop facts the payload itself carries, so wire and hop classes can never disagree; it is `None` unless the materialization plan decodes or hydrates through a hop model's class (mirroring the Rust `needs_hop_classes` guard — a both-sides double-check). No other code assembles hop classes for the FFI.
93
+ _Avoid_: payload + kwargs, hop-class side-channel, wire tuple
94
+
87
95
  **Golden vector**:
88
96
  A hand-authored JSON fixture pinning one wire shape — the independent authority both the Python emitter and the Rust decoder assert against, never regenerated from either side's output. Updating one by hand is the contract-review moment for a wire change.
89
97
  _Avoid_: Snapshot, fixture dump, example payload
@@ -100,6 +108,14 @@ _Avoid_: Nested select, join column, related-field pull
100
108
  A user-chosen name for one field of a projected record, given as the key in a dict-returning selector (`select(lambda t: {"account_name": t.account.name})`). Aliases name output fields only — never joins or tables; the relation path remains the sole join identity.
101
109
  _Avoid_: Column alias, AS label, join alias
102
110
 
111
+ **Primary key fact**:
112
+ The single cached answer to "which column is this model's primary key" — derived once at the compile choke point alongside column specs, stored as `__ferro_pk__` (`None` for a PK-less model). At most one `primary_key=True` column may be declared; a violation raises at class definition time. Operations that need a PK read the cached fact and raise clearly when it is `None` — never guess.
113
+ _Avoid_: PK lookup, PK scan, first primary-key column
114
+
115
+ **Registry**:
116
+ The single owner of Python-side registration state (`ferro.registry.REGISTRY`): model classes, per-model SchemaIR envelopes + fingerprints, join-table bundles, pending relations, the modelset artifact, and the generation counters. Its stores are private; the agreement invariants (fingerprint pairs its envelope, join-table eviction clears envelopes, one-call test reset) live inside the interface, not in caller convention. Routing/session state is not the Registry — that is `ferro.state`.
117
+ _Avoid_: state module, global dicts, model cache
118
+
103
119
  **Provisional registration**:
104
120
  The per-model state installed when a class body finishes executing — enough for runtime codec and PK metadata, but relationships may still be pending and the modelset is not yet authoritative for DDL.
105
121
  _Avoid_: Import-time registration, partial registry
@@ -59,9 +59,9 @@ dependencies = [
59
59
 
60
60
  [[package]]
61
61
  name = "bitflags"
62
- version = "2.13.0"
62
+ version = "2.13.1"
63
63
  source = "registry+https://github.com/rust-lang/crates.io-index"
64
- checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
64
+ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
65
65
  dependencies = [
66
66
  "serde_core",
67
67
  ]
@@ -327,7 +327,7 @@ dependencies = [
327
327
 
328
328
  [[package]]
329
329
  name = "ferro"
330
- version = "0.16.2"
330
+ version = "0.17.0"
331
331
  dependencies = [
332
332
  "chrono",
333
333
  "dashmap",
@@ -412,9 +412,9 @@ dependencies = [
412
412
 
413
413
  [[package]]
414
414
  name = "futures"
415
- version = "0.3.32"
415
+ version = "0.3.33"
416
416
  source = "registry+https://github.com/rust-lang/crates.io-index"
417
- checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
417
+ checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
418
418
  dependencies = [
419
419
  "futures-channel",
420
420
  "futures-core",
@@ -427,9 +427,9 @@ dependencies = [
427
427
 
428
428
  [[package]]
429
429
  name = "futures-channel"
430
- version = "0.3.32"
430
+ version = "0.3.33"
431
431
  source = "registry+https://github.com/rust-lang/crates.io-index"
432
- checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
432
+ checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
433
433
  dependencies = [
434
434
  "futures-core",
435
435
  "futures-sink",
@@ -437,15 +437,15 @@ dependencies = [
437
437
 
438
438
  [[package]]
439
439
  name = "futures-core"
440
- version = "0.3.32"
440
+ version = "0.3.33"
441
441
  source = "registry+https://github.com/rust-lang/crates.io-index"
442
- checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
442
+ checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
443
443
 
444
444
  [[package]]
445
445
  name = "futures-executor"
446
- version = "0.3.32"
446
+ version = "0.3.33"
447
447
  source = "registry+https://github.com/rust-lang/crates.io-index"
448
- checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
448
+ checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
449
449
  dependencies = [
450
450
  "futures-core",
451
451
  "futures-task",
@@ -465,15 +465,15 @@ dependencies = [
465
465
 
466
466
  [[package]]
467
467
  name = "futures-io"
468
- version = "0.3.32"
468
+ version = "0.3.33"
469
469
  source = "registry+https://github.com/rust-lang/crates.io-index"
470
- checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
470
+ checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
471
471
 
472
472
  [[package]]
473
473
  name = "futures-macro"
474
- version = "0.3.32"
474
+ version = "0.3.33"
475
475
  source = "registry+https://github.com/rust-lang/crates.io-index"
476
- checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
476
+ checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
477
477
  dependencies = [
478
478
  "proc-macro2",
479
479
  "quote",
@@ -482,21 +482,21 @@ dependencies = [
482
482
 
483
483
  [[package]]
484
484
  name = "futures-sink"
485
- version = "0.3.32"
485
+ version = "0.3.33"
486
486
  source = "registry+https://github.com/rust-lang/crates.io-index"
487
- checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
487
+ checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
488
488
 
489
489
  [[package]]
490
490
  name = "futures-task"
491
- version = "0.3.32"
491
+ version = "0.3.33"
492
492
  source = "registry+https://github.com/rust-lang/crates.io-index"
493
- checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
493
+ checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
494
494
 
495
495
  [[package]]
496
496
  name = "futures-util"
497
- version = "0.3.32"
497
+ version = "0.3.33"
498
498
  source = "registry+https://github.com/rust-lang/crates.io-index"
499
- checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
499
+ checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
500
500
  dependencies = [
501
501
  "futures-channel",
502
502
  "futures-core",
@@ -1045,9 +1045,9 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
1045
1045
 
1046
1046
  [[package]]
1047
1047
  name = "portable-atomic"
1048
- version = "1.13.1"
1048
+ version = "1.14.0"
1049
1049
  source = "registry+https://github.com/rust-lang/crates.io-index"
1050
- checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
1050
+ checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
1051
1051
 
1052
1052
  [[package]]
1053
1053
  name = "potential_utf"
@@ -1771,9 +1771,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
1771
1771
 
1772
1772
  [[package]]
1773
1773
  name = "tokio"
1774
- version = "1.52.3"
1774
+ version = "1.53.0"
1775
1775
  source = "registry+https://github.com/rust-lang/crates.io-index"
1776
- checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
1776
+ checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
1777
1777
  dependencies = [
1778
1778
  "bytes",
1779
1779
  "libc",
@@ -1788,9 +1788,9 @@ dependencies = [
1788
1788
 
1789
1789
  [[package]]
1790
1790
  name = "tokio-macros"
1791
- version = "2.7.0"
1791
+ version = "2.7.1"
1792
1792
  source = "registry+https://github.com/rust-lang/crates.io-index"
1793
- checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
1793
+ checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
1794
1794
  dependencies = [
1795
1795
  "proc-macro2",
1796
1796
  "quote",
@@ -1905,9 +1905,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
1905
1905
 
1906
1906
  [[package]]
1907
1907
  name = "uuid"
1908
- version = "1.23.5"
1908
+ version = "1.24.0"
1909
1909
  source = "registry+https://github.com/rust-lang/crates.io-index"
1910
- checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a"
1910
+ checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
1911
1911
  dependencies = [
1912
1912
  "getrandom 0.4.3",
1913
1913
  "js-sys",
@@ -1989,14 +1989,14 @@ version = "0.26.11"
1989
1989
  source = "registry+https://github.com/rust-lang/crates.io-index"
1990
1990
  checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
1991
1991
  dependencies = [
1992
- "webpki-roots 1.0.8",
1992
+ "webpki-roots 1.0.9",
1993
1993
  ]
1994
1994
 
1995
1995
  [[package]]
1996
1996
  name = "webpki-roots"
1997
- version = "1.0.8"
1997
+ version = "1.0.9"
1998
1998
  source = "registry+https://github.com/rust-lang/crates.io-index"
1999
- checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
1999
+ checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
2000
2000
  dependencies = [
2001
2001
  "rustls-pki-types",
2002
2002
  ]
@@ -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.16.2"
6
+ version = "0.17.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.16.2
3
+ Version: 0.17.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'
@@ -137,13 +137,15 @@ pub struct SchemaCheck {
137
137
  /// Query IR: filter, sort, pagination, joins, materialization plan, and
138
138
  /// optional M2M join context.
139
139
  ///
140
- /// `ir_version: 5` (unconditional, no earlier version emitted anywhere —
141
- /// #269, #278, #285, #292). `joins` is required and always present (`[]` when
142
- /// the query traverses no relation); every leaf and `order_by` entry carries a
143
- /// `path` (required, `[]` = root model); `materialization` is required — the
144
- /// plan travels with the query as data (ADR-0007), never inferred from a
145
- /// column list. v5 gives `record` fields expression sources: a field carries
146
- /// either a `column` + `path` or an `expr` (#292, ADR-0009).
140
+ /// `ir_version: 7` (unconditional, no earlier version emitted anywhere —
141
+ /// #269, #278, #285, #292, #310, #314). `joins` is required and always
142
+ /// present (`[]` when the query traverses no relation); every leaf and
143
+ /// `order_by` entry carries a `path` (required, `[]` = root model);
144
+ /// `materialization` is required — the plan travels with the query as data
145
+ /// (ADR-0007), never inferred from a column list. v5 gave `record` fields
146
+ /// expression sources (#292, ADR-0009); v6 gave predicate trees the
147
+ /// recursive `not` node kind (uniform negation, #310, ADR-0008); v7 adds the
148
+ /// recursive `exists` node kind (existence tests, #314, ADR-0007).
147
149
  #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
148
150
  pub struct QueryIrPayload {
149
151
  /// Model class name the query targets.
@@ -333,7 +335,9 @@ pub struct QueryOrderBy {
333
335
  pub path: Vec<String>,
334
336
  }
335
337
 
336
- /// Predicate tree node in query IR (leaf comparison or compound AND/OR).
338
+ /// Predicate tree node in query IR: leaf comparison, compound AND/OR, a
339
+ /// recursive NOT wrapping any child (uniform negation, v6, ADR-0008), or a
340
+ /// recursive existence test over a reverse/M2M relation (v7, ADR-0007).
337
341
  #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
338
342
  #[serde(tag = "node_kind")]
339
343
  pub enum QueryNode {
@@ -360,6 +364,37 @@ pub enum QueryNode {
360
364
  /// Right subtree.
361
365
  right: Box<QueryNode>,
362
366
  },
367
+ /// Uniform negation (`~`, ADR-0008): SQL `NOT (...)` over the child.
368
+ /// Fully general — the child may be any node kind, at any depth; there
369
+ /// are no per-operator negative forms on the wire.
370
+ #[serde(rename = "not")]
371
+ Not {
372
+ /// The negated subtree.
373
+ child: Box<QueryNode>,
374
+ },
375
+ /// Existence test on a reverse or M2M relation (ADR-0007): a correlated
376
+ /// `EXISTS (SELECT 1 ...)` at every cardinality. Carries no negation
377
+ /// flag — NOT EXISTS is the `not` node over this one.
378
+ #[serde(rename = "exists")]
379
+ Exists {
380
+ /// Correlation hop path in the `joins`-section hop-fact shape: one
381
+ /// hop for a reverse FK (`FROM child WHERE child.fk = root.pk`), two
382
+ /// for M2M (join table, then target). The first hop's table is the
383
+ /// subquery FROM, correlated to the enclosing scope's alias;
384
+ /// remaining hops render as inner joins inside the subquery.
385
+ hops: Vec<QueryJoinHop>,
386
+ /// Ordinary inner condition tree over the relation's target model
387
+ /// (`[]` = bare test). May itself contain `exists`/`not` nodes —
388
+ /// nesting is recursion, not a second mechanism.
389
+ #[serde(rename = "where")]
390
+ where_clause: Vec<QueryNode>,
391
+ /// Forward-traversal joins the inner tree references (#315),
392
+ /// rendered INSIDE the subquery as inner joins (ADR-0006 traversal
393
+ /// semantics are unchanged there; `left_join` has no inner-lambda
394
+ /// spelling). Absent on the wire when empty — pinned bytes.
395
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
396
+ joins: Vec<QueryJoin>,
397
+ },
363
398
  }
364
399
 
365
400
  /// Typed literal or parameter value on the RHS of a leaf predicate.
@@ -438,7 +473,7 @@ mod tests {
438
473
  #[test]
439
474
  fn query_fixture_roundtrip() {
440
475
  let fixture =
441
- include_str!("../../../tests/fixtures/ir_vectors/query_user_compound_v5.json");
476
+ include_str!("../../../tests/fixtures/ir_vectors/query_user_compound_v7.json");
442
477
  let parsed: serde_json::Value =
443
478
  serde_json::from_str(fixture).expect("query fixture must parse");
444
479
  let ir = parsed
@@ -456,12 +491,224 @@ mod tests {
456
491
  assert_eq!(encoded, ir, "query round-trip must not drift");
457
492
  }
458
493
 
494
+ #[test]
495
+ fn query_not_leaf_fixture_roundtrip() {
496
+ // The not-over-leaf golden vector (#312, ADR-0008): uniform negation
497
+ // travels as one recursive `not` node wrapping its child — here a
498
+ // leaf `IN` comparison (the NOT IN spelling) — and must survive a
499
+ // deserialize/serialize round-trip without drift.
500
+ let fixture =
501
+ include_str!("../../../tests/fixtures/ir_vectors/query_user_not_leaf_v7.json");
502
+ let parsed: serde_json::Value =
503
+ serde_json::from_str(fixture).expect("query not-leaf fixture must parse");
504
+ let ir = parsed
505
+ .get("ir")
506
+ .cloned()
507
+ .expect("fixture must contain ir envelope");
508
+ let envelope: IrEnvelope<QueryIrPayload> =
509
+ serde_json::from_value(ir.clone()).expect("query not-leaf IR must deserialize");
510
+ // The wire carries a `not` node whose child is the IN leaf.
511
+ match &envelope.payload.where_clause[0] {
512
+ QueryNode::Not { child } => match child.as_ref() {
513
+ QueryNode::Leaf { operator, .. } => assert_eq!(operator, "IN"),
514
+ other => panic!("not child must be the IN leaf, got {other:?}"),
515
+ },
516
+ other => panic!("where[0] must be a not node, got {other:?}"),
517
+ }
518
+ let encoded = serde_json::to_value(&envelope).expect("query not-leaf IR must serialize");
519
+ assert_eq!(encoded, ir, "query not-leaf round-trip must not drift");
520
+ }
521
+
522
+ #[test]
523
+ fn query_not_compound_fixture_roundtrip() {
524
+ // The not-over-compound golden vector (#313, ADR-0008): `~` wraps an
525
+ // OR compound whole — no De Morgan expansion on the wire — and must
526
+ // survive a deserialize/serialize round-trip without drift.
527
+ let fixture =
528
+ include_str!("../../../tests/fixtures/ir_vectors/query_user_not_compound_v7.json");
529
+ let parsed: serde_json::Value =
530
+ serde_json::from_str(fixture).expect("query not-compound fixture must parse");
531
+ let ir = parsed
532
+ .get("ir")
533
+ .cloned()
534
+ .expect("fixture must contain ir envelope");
535
+ let envelope: IrEnvelope<QueryIrPayload> =
536
+ serde_json::from_value(ir.clone()).expect("query not-compound IR must deserialize");
537
+ match &envelope.payload.where_clause[0] {
538
+ QueryNode::Not { child } => match child.as_ref() {
539
+ QueryNode::Compound { operator, .. } => assert_eq!(operator, "OR"),
540
+ other => panic!("not child must be the OR compound, got {other:?}"),
541
+ },
542
+ other => panic!("where[0] must be a not node, got {other:?}"),
543
+ }
544
+ let encoded =
545
+ serde_json::to_value(&envelope).expect("query not-compound IR must serialize");
546
+ assert_eq!(encoded, ir, "query not-compound round-trip must not drift");
547
+ }
548
+
549
+ #[test]
550
+ fn query_exists_fixture_roundtrip() {
551
+ // The bare 1-hop existence-test golden vector (#314, ADR-0007): an
552
+ // `exists` node carries its correlation hop path plus an empty inner
553
+ // condition tree, and must survive a deserialize/serialize
554
+ // round-trip without drift.
555
+ let fixture =
556
+ include_str!("../../../tests/fixtures/ir_vectors/query_account_exists_v7.json");
557
+ let parsed: serde_json::Value =
558
+ serde_json::from_str(fixture).expect("query exists fixture must parse");
559
+ let ir = parsed
560
+ .get("ir")
561
+ .cloned()
562
+ .expect("fixture must contain ir envelope");
563
+ let envelope: IrEnvelope<QueryIrPayload> =
564
+ serde_json::from_value(ir.clone()).expect("query exists IR must deserialize");
565
+ match &envelope.payload.where_clause[0] {
566
+ QueryNode::Exists {
567
+ hops, where_clause, ..
568
+ } => {
569
+ assert_eq!(hops.len(), 1);
570
+ assert_eq!(hops[0].relation, "transactions");
571
+ assert_eq!(hops[0].from_column, "id");
572
+ assert_eq!(hops[0].to_table, "transaction");
573
+ assert_eq!(hops[0].to_column, "account_id");
574
+ assert!(where_clause.is_empty(), "bare test carries no inner tree");
575
+ }
576
+ other => panic!("where[0] must be an exists node, got {other:?}"),
577
+ }
578
+ let encoded = serde_json::to_value(&envelope).expect("query exists IR must serialize");
579
+ assert_eq!(encoded, ir, "query exists round-trip must not drift");
580
+ }
581
+
582
+ #[test]
583
+ fn query_not_exists_fixture_roundtrip() {
584
+ // NOT EXISTS on the wire is the ordinary `not` node over an `exists`
585
+ // node — the exists node carries no negation flag (ADR-0008
586
+ // composition; #314).
587
+ let fixture =
588
+ include_str!("../../../tests/fixtures/ir_vectors/query_owner_not_exists_v7.json");
589
+ let parsed: serde_json::Value =
590
+ serde_json::from_str(fixture).expect("query not-exists fixture must parse");
591
+ let ir = parsed
592
+ .get("ir")
593
+ .cloned()
594
+ .expect("fixture must contain ir envelope");
595
+ let envelope: IrEnvelope<QueryIrPayload> =
596
+ serde_json::from_value(ir.clone()).expect("query not-exists IR must deserialize");
597
+ match &envelope.payload.where_clause[0] {
598
+ QueryNode::Not { child } => match child.as_ref() {
599
+ QueryNode::Exists { hops, .. } => assert_eq!(hops[0].relation, "accounts"),
600
+ other => panic!("not child must be the exists node, got {other:?}"),
601
+ },
602
+ other => panic!("where[0] must be a not node, got {other:?}"),
603
+ }
604
+ let encoded = serde_json::to_value(&envelope).expect("query not-exists IR must serialize");
605
+ assert_eq!(encoded, ir, "query not-exists round-trip must not drift");
606
+ }
607
+
608
+ #[test]
609
+ fn query_scoped_exists_fixture_roundtrip() {
610
+ // The scoped existence-test golden vector (#315): the inner tree is
611
+ // an ordinary condition tree, and a traversed inner leaf's hop facts
612
+ // ride the exists node's own `joins` section (rendered INSIDE the
613
+ // subquery). Must survive a deserialize/serialize round-trip.
614
+ let fixture =
615
+ include_str!("../../../tests/fixtures/ir_vectors/query_account_scoped_exists_v7.json");
616
+ let parsed: serde_json::Value =
617
+ serde_json::from_str(fixture).expect("query scoped-exists fixture must parse");
618
+ let ir = parsed
619
+ .get("ir")
620
+ .cloned()
621
+ .expect("fixture must contain ir envelope");
622
+ let envelope: IrEnvelope<QueryIrPayload> =
623
+ serde_json::from_value(ir.clone()).expect("query scoped-exists IR must deserialize");
624
+ match &envelope.payload.where_clause[0] {
625
+ QueryNode::Exists {
626
+ hops,
627
+ where_clause,
628
+ joins,
629
+ } => {
630
+ assert_eq!(hops.len(), 1);
631
+ assert_eq!(where_clause.len(), 1);
632
+ assert_eq!(joins.len(), 1);
633
+ assert_eq!(joins[0].join_type, "inner");
634
+ assert_eq!(joins[0].path[0].relation, "account");
635
+ }
636
+ other => panic!("where[0] must be an exists node, got {other:?}"),
637
+ }
638
+ let encoded =
639
+ serde_json::to_value(&envelope).expect("query scoped-exists IR must serialize");
640
+ assert_eq!(encoded, ir, "query scoped-exists round-trip must not drift");
641
+ }
642
+
643
+ #[test]
644
+ fn query_nested_exists_fixture_roundtrip() {
645
+ // Nested exists-in-exists (#315): depth is recursion, not a second
646
+ // mechanism, and the bare inner node omits `joins` entirely (absent,
647
+ // not empty — pinned wire bytes via skip_serializing_if).
648
+ let fixture =
649
+ include_str!("../../../tests/fixtures/ir_vectors/query_owner_nested_exists_v7.json");
650
+ let parsed: serde_json::Value =
651
+ serde_json::from_str(fixture).expect("query nested-exists fixture must parse");
652
+ let ir = parsed
653
+ .get("ir")
654
+ .cloned()
655
+ .expect("fixture must contain ir envelope");
656
+ let envelope: IrEnvelope<QueryIrPayload> =
657
+ serde_json::from_value(ir.clone()).expect("query nested-exists IR must deserialize");
658
+ match &envelope.payload.where_clause[0] {
659
+ QueryNode::Exists { where_clause, .. } => match &where_clause[0] {
660
+ QueryNode::Exists { hops, joins, .. } => {
661
+ assert_eq!(hops[0].relation, "transactions");
662
+ assert!(joins.is_empty(), "bare nested test carries no joins");
663
+ }
664
+ other => panic!("inner where[0] must be an exists node, got {other:?}"),
665
+ },
666
+ other => panic!("where[0] must be an exists node, got {other:?}"),
667
+ }
668
+ let encoded =
669
+ serde_json::to_value(&envelope).expect("query nested-exists IR must serialize");
670
+ assert_eq!(encoded, ir, "query nested-exists round-trip must not drift");
671
+ }
672
+
673
+ #[test]
674
+ fn query_m2m_exists_fixture_roundtrip() {
675
+ // The two-hop M2M existence-test golden vector (#316): the SAME
676
+ // exists node, a two-hop correlation path — join table first, then
677
+ // the target — with the scoped inner tree over the target model.
678
+ // Must survive a deserialize/serialize round-trip without drift.
679
+ let fixture =
680
+ include_str!("../../../tests/fixtures/ir_vectors/query_user_m2m_exists_v7.json");
681
+ let parsed: serde_json::Value =
682
+ serde_json::from_str(fixture).expect("query m2m-exists fixture must parse");
683
+ let ir = parsed
684
+ .get("ir")
685
+ .cloned()
686
+ .expect("fixture must contain ir envelope");
687
+ let envelope: IrEnvelope<QueryIrPayload> =
688
+ serde_json::from_value(ir.clone()).expect("query m2m-exists IR must deserialize");
689
+ match &envelope.payload.where_clause[0] {
690
+ QueryNode::Exists {
691
+ hops, where_clause, ..
692
+ } => {
693
+ assert_eq!(hops.len(), 2, "M2M correlates through two hops");
694
+ assert_eq!(hops[0].to_table, "tag_users");
695
+ assert_eq!(hops[0].to_column, "user_id");
696
+ assert_eq!(hops[1].from_column, "tag_id");
697
+ assert_eq!(hops[1].to_table, "tag");
698
+ assert_eq!(where_clause.len(), 1);
699
+ }
700
+ other => panic!("where[0] must be an exists node, got {other:?}"),
701
+ }
702
+ let encoded = serde_json::to_value(&envelope).expect("query m2m-exists IR must serialize");
703
+ assert_eq!(encoded, ir, "query m2m-exists round-trip must not drift");
704
+ }
705
+
459
706
  #[test]
460
707
  fn query_traversal_fixture_roundtrip() {
461
708
  // Multi-hop `joins` section + path-carrying leaves must survive a
462
709
  // deserialize/serialize round-trip without drift (#270 wire stability).
463
710
  let fixture = include_str!(
464
- "../../../tests/fixtures/ir_vectors/query_transaction_traversal_v5.json"
711
+ "../../../tests/fixtures/ir_vectors/query_transaction_traversal_v7.json"
465
712
  );
466
713
  let parsed: serde_json::Value =
467
714
  serde_json::from_str(fixture).expect("query traversal fixture must parse");
@@ -487,7 +734,7 @@ mod tests {
487
734
  // deserialize/serialize round-trip without drift, and the join_type
488
735
  // tokens must reach Rust exactly as written on the wire.
489
736
  let fixture =
490
- include_str!("../../../tests/fixtures/ir_vectors/query_transaction_left_join_v5.json");
737
+ include_str!("../../../tests/fixtures/ir_vectors/query_transaction_left_join_v7.json");
491
738
  let parsed: serde_json::Value =
492
739
  serde_json::from_str(fixture).expect("query left_join fixture must parse");
493
740
  let ir = parsed
@@ -513,7 +760,7 @@ mod tests {
513
760
  // payload — predicate, order, limit, and a two-field record plan —
514
761
  // survives a deserialize/serialize round-trip without drift.
515
762
  let fixture =
516
- include_str!("../../../tests/fixtures/ir_vectors/query_transaction_record_v5.json");
763
+ include_str!("../../../tests/fixtures/ir_vectors/query_transaction_record_v7.json");
517
764
  let parsed: serde_json::Value =
518
765
  serde_json::from_str(fixture).expect("query record fixture must parse");
519
766
  let ir = parsed
@@ -712,7 +959,7 @@ mod tests {
712
959
  // identity) — survives a deserialize/serialize round-trip without
713
960
  // drift. No group keys: the whole result collapses to one record.
714
961
  let fixture = include_str!(
715
- "../../../tests/fixtures/ir_vectors/query_transaction_global_aggregate_v5.json"
962
+ "../../../tests/fixtures/ir_vectors/query_transaction_global_aggregate_v7.json"
716
963
  );
717
964
  let parsed: serde_json::Value =
718
965
  serde_json::from_str(fixture).expect("global aggregate fixture must parse");
@@ -750,7 +997,7 @@ mod tests {
750
997
  // joins section included — survives a deserialize/serialize
751
998
  // round-trip without drift.
752
999
  let fixture = include_str!(
753
- "../../../tests/fixtures/ir_vectors/query_transaction_traversed_record_v5.json"
1000
+ "../../../tests/fixtures/ir_vectors/query_transaction_traversed_record_v7.json"
754
1001
  );
755
1002
  let parsed: serde_json::Value =
756
1003
  serde_json::from_str(fixture).expect("query traversed record fixture must parse");
@@ -788,7 +1035,7 @@ mod tests {
788
1035
  // a deserialize/serialize round-trip without drift. GROUP BY does not
789
1036
  // travel: the renderer derives it from the non-expr fields (ADR-0009).
790
1037
  let fixture = include_str!(
791
- "../../../tests/fixtures/ir_vectors/query_transaction_aggregate_v5.json"
1038
+ "../../../tests/fixtures/ir_vectors/query_transaction_aggregate_v7.json"
792
1039
  );
793
1040
  let parsed: serde_json::Value =
794
1041
  serde_json::from_str(fixture).expect("query aggregate fixture must parse");
@@ -856,7 +1103,7 @@ mod tests {
856
1103
  // payload — root predicate, empty `joins`, and a one-path instances
857
1104
  // plan — survives a deserialize/serialize round-trip without drift.
858
1105
  let fixture =
859
- include_str!("../../../tests/fixtures/ir_vectors/query_transaction_include_v5.json");
1106
+ include_str!("../../../tests/fixtures/ir_vectors/query_transaction_include_v7.json");
860
1107
  let parsed: serde_json::Value =
861
1108
  serde_json::from_str(fixture).expect("query include fixture must parse");
862
1109
  let ir = parsed