ferro-orm 0.16.1__tar.gz → 0.16.3__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 (370) hide show
  1. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/CHANGELOG.md +30 -0
  2. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/CONTEXT.md +20 -0
  3. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/Cargo.lock +17 -17
  4. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/Cargo.toml +1 -1
  5. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/PKG-INFO +1 -1
  6. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/crates/ferro-migrate/src/emit.rs +13 -37
  7. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/crates/ferro-migrate/src/lib.rs +2 -0
  8. ferro_orm-0.16.3/crates/ferro-migrate/src/order.rs +56 -0
  9. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/crates/ferro-migrate/src/tests.rs +33 -0
  10. ferro_orm-0.16.3/docs/solutions/issues/self-fk-evicts-component-from-create-order.md +59 -0
  11. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/pyproject.toml +1 -1
  12. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/__init__.py +25 -30
  13. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/_shadow_fk_types.py +5 -6
  14. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/columns.py +30 -2
  15. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/ir/compiler.py +35 -40
  16. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/metaclass.py +7 -6
  17. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/models.py +32 -27
  18. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/query/builder.py +60 -388
  19. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/query/nodes.py +2 -2
  20. ferro_orm-0.16.3/src/ferro/query/wire.py +575 -0
  21. ferro_orm-0.16.3/src/ferro/registry.py +328 -0
  22. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/relations/__init__.py +19 -25
  23. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/relations/descriptors.py +13 -10
  24. ferro_orm-0.16.3/src/ferro/state.py +176 -0
  25. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/schema.rs +34 -39
  26. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/conftest.py +8 -29
  27. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_alembic_autogenerate.py +6 -12
  28. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_alembic_bridge.py +2 -4
  29. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_alembic_db_type.py +3 -9
  30. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_alembic_nullability.py +2 -4
  31. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_alembic_type_mapping.py +2 -4
  32. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_auto_migrate.py +67 -46
  33. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_catalog_cache.py +2 -4
  34. ferro_orm-0.16.3/tests/test_compiled_query.py +81 -0
  35. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_composite_index.py +3 -7
  36. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_composite_unique.py +3 -7
  37. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_connection.py +2 -2
  38. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_cross_emitter_parity.py +5 -11
  39. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_db_type_cross_emitter_parity.py +3 -11
  40. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_db_type_integration.py +4 -6
  41. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_documentation_features.py +4 -4
  42. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_enum_cold_hydration.py +6 -6
  43. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_fk_target_pk.py +6 -14
  44. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_generation_counter_dirty_tracking.py +16 -14
  45. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_hydration_equivalence.py +3 -7
  46. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_include.py +5 -2
  47. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_ir_vectors_contract.py +13 -10
  48. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_model_identity.py +13 -25
  49. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_mutation_pagination_guard.py +4 -2
  50. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_named_connections_integration.py +2 -2
  51. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_naming_single_source.py +8 -12
  52. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_one_to_one.py +2 -3
  53. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_operation_seam_sync.py +3 -3
  54. ferro_orm-0.16.3/tests/test_primary_key_facts.py +77 -0
  55. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_provisional_import.py +9 -9
  56. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_query_builder.py +15 -17
  57. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_query_column_validation.py +5 -2
  58. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_query_immutability.py +7 -3
  59. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_query_joins.py +7 -5
  60. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_query_typing.py +2 -3
  61. ferro_orm-0.16.3/tests/test_query_wire_vectors.py +288 -0
  62. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_registry_entrypoints.py +69 -74
  63. ferro_orm-0.16.3/tests/test_registry_owner.py +119 -0
  64. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_relationship_engine.py +2 -3
  65. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_relationship_resolution_errors.py +3 -7
  66. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_session.py +2 -2
  67. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_shadow_fk_types.py +4 -6
  68. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_sqlite_alembic_reconnect_hydration.py +2 -4
  69. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_static_contracts.py +21 -8
  70. ferro_orm-0.16.1/src/ferro/state.py +0 -355
  71. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  72. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  73. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/PERMISSIONS.md +0 -0
  74. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/PYPI_CHECKLIST.md +0 -0
  75. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/PYPI_SETUP.md +0 -0
  76. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/generated/wheels.generated.yml +0 -0
  77. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/pull_request_template.md +0 -0
  78. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/workflows/ci.yml +0 -0
  79. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/workflows/packaging-smoke.yml +0 -0
  80. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/workflows/publish-docs.yml +0 -0
  81. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/workflows/publish.yml +0 -0
  82. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.github/workflows/release.yml +0 -0
  83. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.gitignore +0 -0
  84. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.pre-commit-config.yaml +0 -0
  85. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/.python-version +0 -0
  86. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/AGENTS.md +0 -0
  87. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/CONTRIBUTING.md +0 -0
  88. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/LICENSE +0 -0
  89. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/README.md +0 -0
  90. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/benchmarks/README.md +0 -0
  91. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/benchmarks/__init__.py +0 -0
  92. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/benchmarks/__main__.py +0 -0
  93. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/benchmarks/backends.py +0 -0
  94. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/benchmarks/baselines/postgres.json +0 -0
  95. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/benchmarks/baselines/sqlite.json +0 -0
  96. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/benchmarks/compare.py +0 -0
  97. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/benchmarks/harness.py +0 -0
  98. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/benchmarks/model.py +0 -0
  99. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/benchmarks/run.py +0 -0
  100. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
  101. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/crates/ferro-ddl-lowering/src/lib.rs +0 -0
  102. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/crates/ferro-migrate/Cargo.toml +0 -0
  103. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/crates/ferro-schema-ir/Cargo.toml +0 -0
  104. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/crates/ferro-schema-ir/src/lib.rs +0 -0
  105. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/adr/0001-resolved-epoch-registration.md +0 -0
  106. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
  107. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
  108. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
  109. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
  110. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
  111. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
  112. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/adr/0008-populated-relations-include.md +0 -0
  113. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
  114. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/agents/domain.md +0 -0
  115. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/agents/issue-tracker.md +0 -0
  116. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/agents/triage-labels.md +0 -0
  117. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/architecture-review-2026-07-08.html +0 -0
  118. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
  119. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
  120. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
  121. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
  122. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
  123. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
  124. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
  125. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
  126. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-26-ir-p8.5-143-db-type-drop-requirements.md +0 -0
  127. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
  128. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-28-ir-p8.5-141-single-schema-ir-producer-requirements.md +0 -0
  129. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-29-ir-p8.6-146-dialect-enum-unification-requirements.md +0 -0
  130. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-29-ir-p8.6-153-create-path-ir-requirements.md +0 -0
  131. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
  132. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
  133. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-07-01-ir-p8.6-154-datetime-tz-coarseness-requirements.md +0 -0
  134. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-07-01-ir-p8.6-162-typed-save-bind-requirements.md +0 -0
  135. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-07-01-ir-p8.6-165-blob-introspection-false-positive-requirements.md +0 -0
  136. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
  137. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
  138. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/aggregations.py +0 -0
  139. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/aggregations_annotated.py +0 -0
  140. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/multiple_databases.py +0 -0
  141. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/mutations.py +0 -0
  142. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/pagination.py +0 -0
  143. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/partial_selects.py +0 -0
  144. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/partial_selects_annotated.py +0 -0
  145. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/populated_relations.py +0 -0
  146. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/populated_relations_annotated.py +0 -0
  147. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/predicates.py +0 -0
  148. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/predicates_annotated.py +0 -0
  149. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/quickstart.py +0 -0
  150. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/quickstart_annotated.py +0 -0
  151. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/raw_sql.py +0 -0
  152. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/relationships.py +0 -0
  153. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/relationships_annotated.py +0 -0
  154. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/soft_deletes.py +0 -0
  155. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/soft_deletes_annotated.py +0 -0
  156. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/testing_conftest.py +0 -0
  157. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/timestamps.py +0 -0
  158. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/timestamps_annotated.py +0 -0
  159. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/transactions.py +0 -0
  160. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/traversal.py +0 -0
  161. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/examples/traversal_annotated.py +0 -0
  162. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/api/connection.md +0 -0
  163. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/api/exceptions.md +0 -0
  164. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/api/fields.md +0 -0
  165. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/api/migrations.md +0 -0
  166. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/api/model.md +0 -0
  167. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/api/queries.md +0 -0
  168. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/api/raw-sql.md +0 -0
  169. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/api/relationships.md +0 -0
  170. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/api/transactions.md +0 -0
  171. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/changelog.md +0 -0
  172. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/concepts/architecture.md +0 -0
  173. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/concepts/backends.md +0 -0
  174. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/concepts/identity-map.md +0 -0
  175. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/concepts/performance.md +0 -0
  176. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/concepts/query-typing.md +0 -0
  177. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/concepts/type-safety.md +0 -0
  178. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/contributing.md +0 -0
  179. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/faq.md +0 -0
  180. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/getting-started/installation.md +0 -0
  181. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/getting-started/next-steps.md +0 -0
  182. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/getting-started/quickstart.md +0 -0
  183. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/guide/aggregations.md +0 -0
  184. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/guide/connections.md +0 -0
  185. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/guide/migrations.md +0 -0
  186. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/guide/models-and-fields.md +0 -0
  187. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/guide/mutations.md +0 -0
  188. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/guide/queries.md +0 -0
  189. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/guide/raw-sql.md +0 -0
  190. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/guide/relationships.md +0 -0
  191. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/guide/transactions.md +0 -0
  192. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
  193. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/howto/multiple-databases.md +0 -0
  194. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/howto/pagination.md +0 -0
  195. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/howto/soft-deletes.md +0 -0
  196. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/howto/testing.md +0 -0
  197. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/howto/timestamps.md +0 -0
  198. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/howto/upgrade-guide.md +0 -0
  199. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/index.md +0 -0
  200. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/roadmap.md +0 -0
  201. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/stylesheets/extra.css +0 -0
  202. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/pages/why-ferro.md +0 -0
  203. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
  204. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
  205. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
  206. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
  207. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
  208. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
  209. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
  210. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
  211. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-06-24-001-feat-ir-p8-119-wire-automigrate-plan.md +0 -0
  212. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-06-25-001-feat-ir-p8-120-parity-gate-plan.md +0 -0
  213. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-06-26-001-feat-ir-p8.5-140-shared-lowering-plan.md +0 -0
  214. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-06-26-002-feat-ir-p8.5-143-db-type-drop-plan.md +0 -0
  215. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-06-26-003-feat-ir-p8.5-144-reconcile-indexes-plan.md +0 -0
  216. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-06-28-001-feat-ir-p8.5-141-single-schema-ir-producer-plan.md +0 -0
  217. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-06-29-001-refactor-ir-p8.6-153-create-path-unification-plan.md +0 -0
  218. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-06-29-002-refactor-ir-p8.6-146-dialect-enum-unification-plan.md +0 -0
  219. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-06-30-001-refactor-ir-p8.6-158-check-renderer-plan.md +0 -0
  220. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-06-30-002-fix-ir-p8.6-155-deferred-annotations-plan.md +0 -0
  221. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-01-001-feat-ir-p8.6-162-typed-save-bind-plan.md +0 -0
  222. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-01-002-fix-ir-p8.6-154-datetime-tz-coarseness-plan.md +0 -0
  223. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-01-003-fix-ir-p8.6-165-blob-introspection-plan.md +0 -0
  224. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
  225. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-02-002-feat-ff-a-172-typed-exceptions-plan.md +0 -0
  226. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
  227. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +0 -0
  228. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
  229. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
  230. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
  231. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
  232. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-06-009-ff-f-query-builder-1.0-design.md +0 -0
  233. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-06-010-ff-f-query-builder-1.0-plan.md +0 -0
  234. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
  235. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
  236. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
  237. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
  238. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
  239. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/ir-first-migration-guide.md +0 -0
  240. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/plans/ir-first-release-checklist.md +0 -0
  241. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/rfc/ir-contracts-v1.md +0 -0
  242. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/README.md +0 -0
  243. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
  244. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
  245. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
  246. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
  247. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
  248. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
  249. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
  250. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
  251. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
  252. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
  253. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
  254. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
  255. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
  256. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/patterns/foreign-key-index.md +0 -0
  257. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
  258. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/patterns/ir-invariants.md +0 -0
  259. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/patterns/persistence-state.md +0 -0
  260. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
  261. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
  262. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/docs/solutions/patterns/typed-null-binds.md +0 -0
  263. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/justfile +0 -0
  264. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/scripts/demo_queries.py +0 -0
  265. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/scripts/demo_queries_pre_v012.py +0 -0
  266. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/backend.rs +0 -0
  267. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/codec.rs +0 -0
  268. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/codec_plan.rs +0 -0
  269. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/connection.rs +0 -0
  270. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/errors.rs +0 -0
  271. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/_annotation_utils.py +0 -0
  272. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/_bind_payload.py +0 -0
  273. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/_core.pyi +0 -0
  274. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/_deprecations.py +0 -0
  275. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/base.py +0 -0
  276. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/composite_indexes.py +0 -0
  277. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/composite_uniques.py +0 -0
  278. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/exceptions.py +0 -0
  279. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/fields.py +0 -0
  280. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/ir/__init__.py +0 -0
  281. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/migrations/__init__.py +0 -0
  282. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/migrations/alembic.py +0 -0
  283. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/py.typed +0 -0
  284. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/query/__init__.py +0 -0
  285. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/query/rows.py +0 -0
  286. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/raw.py +0 -0
  287. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/ferro/session.py +0 -0
  288. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/hydration.rs +0 -0
  289. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/introspect.rs +0 -0
  290. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/lib.rs +0 -0
  291. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/migrate.rs +0 -0
  292. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/naming_ffi.rs +0 -0
  293. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/operations.rs +0 -0
  294. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/query.rs +0 -0
  295. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/schema_bind.rs +0 -0
  296. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/src/state.rs +0 -0
  297. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/__init__.py +0 -0
  298. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/db_backends.py +0 -0
  299. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/README.md +0 -0
  300. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
  301. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_aggregate_v5.json +0 -0
  302. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v5.json +0 -0
  303. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_include_v5.json +0 -0
  304. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_left_join_v5.json +0 -0
  305. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_record_v5.json +0 -0
  306. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_traversal_v5.json +0 -0
  307. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_traversed_record_v5.json +0 -0
  308. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_user_compound_v5.json +0 -0
  309. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
  310. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
  311. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
  312. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/static_fixtures/bad_includes.py +0 -0
  313. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/static_fixtures/bad_predicates.py +0 -0
  314. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/static_fixtures/bad_projections.py +0 -0
  315. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/static_fixtures/good_includes.py +0 -0
  316. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/static_fixtures/good_predicates.py +0 -0
  317. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/static_fixtures/good_projections.py +0 -0
  318. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_aggregation.py +0 -0
  319. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_annotation_utils.py +0 -0
  320. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_bulk_create_chunking.py +0 -0
  321. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_bulk_install.py +0 -0
  322. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_bulk_update.py +0 -0
  323. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_codec_plan.py +0 -0
  324. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_column_specs.py +0 -0
  325. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_connection_redaction.py +0 -0
  326. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_constraints.py +0 -0
  327. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_crud.py +0 -0
  328. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_db_backends.py +0 -0
  329. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_db_type_typing.py +0 -0
  330. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_db_type_validation.py +0 -0
  331. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_deletion.py +0 -0
  332. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_deprecations.py +0 -0
  333. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_docs_examples.py +0 -0
  334. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_exception_mapping.py +0 -0
  335. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_exceptions.py +0 -0
  336. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_field_wrapper.py +0 -0
  337. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_global_aggregates.py +0 -0
  338. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_grouped_aggregates.py +0 -0
  339. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_helpers.py +0 -0
  340. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_hydration.py +0 -0
  341. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_identity_memory.py +0 -0
  342. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_identity_refresh.py +0 -0
  343. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_identity_scoped_invalidation.py +0 -0
  344. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_identity_weakref.py +0 -0
  345. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_import_budget.py +0 -0
  346. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_metaclass_internals.py +0 -0
  347. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_metadata.py +0 -0
  348. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_migrate_plan.py +0 -0
  349. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_models.py +0 -0
  350. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_partial_selects.py +0 -0
  351. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_raw_sql.py +0 -0
  352. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_refresh.py +0 -0
  353. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_relation_specs.py +0 -0
  354. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_route_single_site.py +0 -0
  355. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_routing_errors.py +0 -0
  356. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_save_pk_edges.py +0 -0
  357. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_save_semantics.py +0 -0
  358. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_schema.py +0 -0
  359. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_schema_constraints.py +0 -0
  360. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_schema_db_type_metadata.py +0 -0
  361. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_schema_enum_annotations.py +0 -0
  362. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_string_search.py +0 -0
  363. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_structural_types.py +0 -0
  364. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_temporal_types.py +0 -0
  365. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_transactions.py +0 -0
  366. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_traversed_projection.py +0 -0
  367. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_typed_null_binds.py +0 -0
  368. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/tests/test_typed_save_bind.py +0 -0
  369. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/uv.lock +0 -0
  370. {ferro_orm-0.16.1 → ferro_orm-0.16.3}/zensical.toml +0 -0
@@ -1,6 +1,36 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.16.3 (2026-07-16)
5
+
6
+ ### Bug Fixes
7
+
8
+ - Derive the primary-key fact once and reject multi-PK models at class definition
9
+ ([`ef91fa0`](https://github.com/syn54x/ferro-orm/commit/ef91fa09e09d5e7444e8d659b831921d40bcb365))
10
+
11
+ ### Refactoring
12
+
13
+ - Compile_query returns one CompiledQuery artifact carrying the hop-class map
14
+ ([`fc7084c`](https://github.com/syn54x/ferro-orm/commit/fc7084c4ad89b135815f74852263989f23868143))
15
+
16
+ - Single Registry owner for Python-side registration state
17
+ ([`1179fcc`](https://github.com/syn54x/ferro-orm/commit/1179fcc07cc9e4305b7e7a8c141b8b29264dcd26))
18
+
19
+
20
+ ## v0.16.2 (2026-07-15)
21
+
22
+ ### Bug Fixes
23
+
24
+ - Self-referential FKs no longer evict their component from CREATE TABLE order
25
+ ([#303](https://github.com/syn54x/ferro-orm/pull/303),
26
+ [`d1a55f6`](https://github.com/syn54x/ferro-orm/commit/d1a55f6ae241101d98ee659e6651fa33abaf4ec5))
27
+
28
+ ### Refactoring
29
+
30
+ - Compile QueryIR payloads at a single wire choke point
31
+ ([`851b3f4`](https://github.com/syn54x/ferro-orm/commit/851b3f4e1f077d70481ebca643ff0ffff81645d8))
32
+
33
+
4
34
  ## v0.16.1 (2026-07-13)
5
35
 
6
36
  ### Bug Fixes
@@ -80,6 +80,18 @@ _Avoid_: Eager-loaded field, select_related, prefetched attribute, joined attrib
80
80
  A query's declaration of what its result columns become: complete root instances (every query today), a projected record of named fields, or — in the future — a populated instance graph. Every query carries exactly one plan; the plan travels with the query rather than being inferred from its column list.
81
81
  _Avoid_: Select list, projection spec, hydration mode flag
82
82
 
83
+ **QueryIR payload**:
84
+ 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
+ _Avoid_: Query dict, query def, payload dict
86
+
87
+ **Compiled query**:
88
+ 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.
89
+ _Avoid_: payload + kwargs, hop-class side-channel, wire tuple
90
+
91
+ **Golden vector**:
92
+ 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.
93
+ _Avoid_: Snapshot, fixture dump, example payload
94
+
83
95
  **Aggregate projection**:
84
96
  A projection containing at least one aggregate field. Each group collapses to exactly one projected record: every non-aggregate field is a group key, so grouping is derived from the projection and never declared separately. With no non-aggregate fields, the whole result collapses to a single record. Grouping collapses rows — bucketing complete instances by a key ("partitioning") is a different, client-side operation and is not grouping.
85
97
  _Avoid_: Group-by query, summary query, rollup, partition
@@ -92,6 +104,14 @@ _Avoid_: Nested select, join column, related-field pull
92
104
  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.
93
105
  _Avoid_: Column alias, AS label, join alias
94
106
 
107
+ **Primary key fact**:
108
+ 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.
109
+ _Avoid_: PK lookup, PK scan, first primary-key column
110
+
111
+ **Registry**:
112
+ 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`.
113
+ _Avoid_: state module, global dicts, model cache
114
+
95
115
  **Provisional registration**:
96
116
  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.
97
117
  _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.1"
330
+ version = "0.16.3"
331
331
  dependencies = [
332
332
  "chrono",
333
333
  "dashmap",
@@ -890,9 +890,9 @@ dependencies = [
890
890
 
891
891
  [[package]]
892
892
  name = "mio"
893
- version = "1.2.1"
893
+ version = "1.2.2"
894
894
  source = "registry+https://github.com/rust-lang/crates.io-index"
895
- checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
895
+ checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
896
896
  dependencies = [
897
897
  "libc",
898
898
  "wasi",
@@ -1249,9 +1249,9 @@ dependencies = [
1249
1249
 
1250
1250
  [[package]]
1251
1251
  name = "rustls"
1252
- version = "0.23.41"
1252
+ version = "0.23.42"
1253
1253
  source = "registry+https://github.com/rust-lang/crates.io-index"
1254
- checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
1254
+ checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
1255
1255
  dependencies = [
1256
1256
  "once_cell",
1257
1257
  "ring",
@@ -1444,9 +1444,9 @@ dependencies = [
1444
1444
 
1445
1445
  [[package]]
1446
1446
  name = "socket2"
1447
- version = "0.6.4"
1447
+ version = "0.6.5"
1448
1448
  source = "registry+https://github.com/rust-lang/crates.io-index"
1449
- checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
1449
+ checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
1450
1450
  dependencies = [
1451
1451
  "libc",
1452
1452
  "windows-sys 0.61.2",
@@ -1454,9 +1454,9 @@ dependencies = [
1454
1454
 
1455
1455
  [[package]]
1456
1456
  name = "spin"
1457
- version = "0.9.8"
1457
+ version = "0.9.9"
1458
1458
  source = "registry+https://github.com/rust-lang/crates.io-index"
1459
- checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
1459
+ checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
1460
1460
  dependencies = [
1461
1461
  "lock_api",
1462
1462
  ]
@@ -1698,9 +1698,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
1698
1698
 
1699
1699
  [[package]]
1700
1700
  name = "syn"
1701
- version = "2.0.118"
1701
+ version = "2.0.119"
1702
1702
  source = "registry+https://github.com/rust-lang/crates.io-index"
1703
- checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
1703
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
1704
1704
  dependencies = [
1705
1705
  "proc-macro2",
1706
1706
  "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",
@@ -2329,6 +2329,6 @@ dependencies = [
2329
2329
 
2330
2330
  [[package]]
2331
2331
  name = "zmij"
2332
- version = "1.0.22"
2332
+ version = "1.0.23"
2333
2333
  source = "registry+https://github.com/rust-lang/crates.io-index"
2334
- checksum = "bd2f034a4bebf216c9e4b7083603e024cf930873fd67830cfb083c9fa33129d9"
2334
+ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
@@ -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.1"
6
+ version = "0.16.3"
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.1
3
+ Version: 0.16.3
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'
@@ -255,47 +255,23 @@ fn post_create_artifacts(
255
255
 
256
256
  /// Order `AddTable` models so each table's FK targets are created before it.
257
257
  ///
258
- /// A FK dependency on a table that is *not* part of this add set (already
259
- /// exists in the live DB) does not constrain ordering. Cycles fall through in
260
- /// arbitrary order — the inline FKs are anonymous, so a cyclic create still
261
- /// runs (SQLite is lenient; Postgres `CREATE TABLE` with a forward inline FK
262
- /// reference is the pre-existing behavior preserved here).
258
+ /// Delegates to [`crate::order_by_dependencies`] for the dependency
259
+ /// semantics: a self-referential FK and a FK to a table outside this add set
260
+ /// do not constrain ordering, and genuine cross-table cycles fall through in
261
+ /// input order (SQLite tolerates the forward references; Postgres rejects
262
+ /// them — #302 follow-up).
263
263
  pub fn order_models_for_create<'a>(models: &[&'a SchemaModel]) -> Vec<&'a SchemaModel> {
264
- let mut remaining: Vec<&SchemaModel> = models.to_vec();
265
- let mut ordered = Vec::new();
266
- let mut created = std::collections::HashSet::new();
267
-
268
- while !remaining.is_empty() {
269
- let available: std::collections::HashSet<String> = remaining
270
- .iter()
271
- .map(|m| m.table_name.clone())
272
- .collect();
273
- let mut progress = false;
274
- let mut index = 0;
275
- while index < remaining.len() {
276
- let deps: Vec<String> = remaining[index]
264
+ crate::order_by_dependencies(
265
+ models.to_vec(),
266
+ |model| model.table_name.clone(),
267
+ |model| {
268
+ model
277
269
  .foreign_keys
278
270
  .iter()
279
271
  .map(|fk| fk.to_table.clone())
280
- .collect();
281
- if deps
282
- .iter()
283
- .all(|dep| created.contains(dep) || !available.contains(dep))
284
- {
285
- let model = remaining.remove(index);
286
- created.insert(model.table_name.clone());
287
- ordered.push(model);
288
- progress = true;
289
- } else {
290
- index += 1;
291
- }
292
- }
293
- if !progress {
294
- ordered.append(&mut remaining);
295
- break;
296
- }
297
- }
298
- ordered
272
+ .collect()
273
+ },
274
+ )
299
275
  }
300
276
 
301
277
  fn emit_add_table_passes(
@@ -4,12 +4,14 @@
4
4
  //! [`emit_sql_with_ir`] lowers structural ops to executable backend-specific DDL.
5
5
 
6
6
  mod emit;
7
+ mod order;
7
8
 
8
9
  use ferro_ddl_lowering::schema_columns_storage_drift;
9
10
  use ferro_schema_ir::{IrEnvelope, SchemaIrPayload, SchemaModel};
10
11
  use std::collections::{BTreeMap, BTreeSet};
11
12
 
12
13
  pub use emit::{emit_sql_with_ir, order_models_for_create, render_create_table, CreateTableEmission};
14
+ pub use order::order_by_dependencies;
13
15
  pub use ferro_ddl_lowering::Dialect;
14
16
 
15
17
  /// Executable SQL plus non-fatal warnings from [`emit_sql_with_ir`].
@@ -0,0 +1,56 @@
1
+ //! Dependency ordering shared by every path that emits or reconciles tables.
2
+
3
+ use std::collections::HashSet;
4
+
5
+ /// Order `items` so every FK dependency of an item is emitted before it.
6
+ ///
7
+ /// This is the single dependency-order primitive: both the CREATE TABLE
8
+ /// emitter ([`crate::order_models_for_create`]) and the runtime's migrate
9
+ /// reconcile loop funnel through it, so the semantics live in one place:
10
+ ///
11
+ /// - A dependency on the item's **own** table (a self-referential FK) is not
12
+ /// an ordering constraint — the inline `REFERENCES` resolves against the
13
+ /// table its own `CREATE TABLE` statement is creating (#302).
14
+ /// - A dependency on a table **outside** `items` (already live in the DB)
15
+ /// does not constrain ordering.
16
+ /// - Genuine cross-table cycles cannot be dependency-ordered; the remaining
17
+ /// items are appended in input order. SQLite tolerates the resulting
18
+ /// forward references; Postgres rejects them at CREATE time (#302
19
+ /// follow-up: defer those FKs to post-create `ALTER TABLE ADD CONSTRAINT`).
20
+ ///
21
+ /// Ordering is deterministic: input order is preserved except where a
22
+ /// dependency forces an item later.
23
+ pub fn order_by_dependencies<T>(
24
+ items: Vec<T>,
25
+ table_of: impl Fn(&T) -> String,
26
+ deps_of: impl Fn(&T) -> Vec<String>,
27
+ ) -> Vec<T> {
28
+ let mut remaining = items;
29
+ let mut ordered = Vec::with_capacity(remaining.len());
30
+ let mut created: HashSet<String> = HashSet::new();
31
+
32
+ while !remaining.is_empty() {
33
+ let available: HashSet<String> = remaining.iter().map(&table_of).collect();
34
+ let mut progress = false;
35
+ let mut index = 0;
36
+ while index < remaining.len() {
37
+ let table = table_of(&remaining[index]);
38
+ let satisfied = deps_of(&remaining[index])
39
+ .iter()
40
+ .all(|dep| dep == &table || created.contains(dep) || !available.contains(dep));
41
+ if satisfied {
42
+ let item = remaining.remove(index);
43
+ created.insert(table);
44
+ ordered.push(item);
45
+ progress = true;
46
+ } else {
47
+ index += 1;
48
+ }
49
+ }
50
+ if !progress {
51
+ ordered.append(&mut remaining);
52
+ break;
53
+ }
54
+ }
55
+ ordered
56
+ }
@@ -1459,3 +1459,36 @@ fn emit_sql_with_ir_add_column_db_check_postgres_quoted_and_sqlite_warns() {
1459
1459
  lite.warnings
1460
1460
  );
1461
1461
  }
1462
+
1463
+ #[test]
1464
+ fn order_models_for_create_self_fk_is_not_an_ordering_constraint() {
1465
+ // #302: `znode` carries a self-referential FK; `areferrer` arrives first
1466
+ // (mirroring the alphabetical incoming order) and requires `znode`. The
1467
+ // self-loop is satisfied by the table's own CREATE, so it must not evict
1468
+ // the component from the dependency order.
1469
+ let znode = SchemaModel {
1470
+ foreign_keys: vec![SchemaForeignKey {
1471
+ column: "parent_id".to_string(),
1472
+ to_table: "znode".to_string(),
1473
+ to_column: "id".to_string(),
1474
+ on_delete: None,
1475
+ name: None,
1476
+ }],
1477
+ ..schema_model("znode", vec![col("id", "uuid", false)])
1478
+ };
1479
+ let areferrer = SchemaModel {
1480
+ foreign_keys: vec![SchemaForeignKey {
1481
+ column: "node_id".to_string(),
1482
+ to_table: "znode".to_string(),
1483
+ to_column: "id".to_string(),
1484
+ on_delete: None,
1485
+ name: None,
1486
+ }],
1487
+ ..schema_model("areferrer", vec![col("id", "uuid", false)])
1488
+ };
1489
+
1490
+ let models = vec![&areferrer, &znode];
1491
+ let ordered = order_models_for_create(&models);
1492
+ let tables: Vec<&str> = ordered.iter().map(|m| m.table_name.as_str()).collect();
1493
+ assert_eq!(tables, vec!["znode", "areferrer"]);
1494
+ }
@@ -0,0 +1,59 @@
1
+ ---
2
+ title: Self-referential FK evicted its whole component from CREATE TABLE order
3
+ type: issue
4
+ tags: [rust, schema, migrations, gotcha, postgres]
5
+ related_files:
6
+ - crates/ferro-migrate/src/order.rs
7
+ - crates/ferro-migrate/src/emit.rs
8
+ - src/schema.rs
9
+ related_issues: [302]
10
+ captured: 2026-07-15
11
+ ---
12
+
13
+ # Self-referential FK evicted its whole component from CREATE TABLE order
14
+
15
+ **Problem:** On a fresh Postgres schema, `auto_migrate` created a referrer
16
+ table before its FK target whenever the target carried a self-referential FK —
17
+ the inline `REFERENCES` failed with `relation "..." does not exist`.
18
+
19
+ **Takeaway:** In a dependency sort, an edge to your own table is already
20
+ satisfied by your own `CREATE TABLE`; treating it as a pending dependency makes
21
+ the table permanently ineligible, stalls everything downstream of it, and drops
22
+ the whole component into the cycle fallback (input/alphabetical order).
23
+
24
+ ## What happened
25
+
26
+ Both ordering loops (`order_models_for_create` in `ferro-migrate` and
27
+ `order_models_for_migration` in `src/schema.rs`) only scheduled a table once
28
+ every `fk.to_table` was in the `created` set. A self-FK's target can never be
29
+ in `created` before the table itself, so the self-FK table never became
30
+ eligible, its dependents stalled behind it, and the no-progress fallback
31
+ appended the remainder in incoming order. Any referrer sorting before its
32
+ target then failed on Postgres. Existing schemas survived by alphabetical
33
+ luck.
34
+
35
+ Two things let the bug survive review:
36
+
37
+ 1. The ordering loop existed in **two hand-rolled copies** with the same flaw.
38
+ Both now delegate to one primitive:
39
+ `ferro_migrate::order_by_dependencies` (`crates/ferro-migrate/src/order.rs`),
40
+ where the self-edge rule lives exactly once.
41
+ 2. A doc comment claimed cycles were harmless because "Postgres `CREATE TABLE`
42
+ with a forward inline FK reference is the pre-existing behavior" — false.
43
+ Postgres rejects forward references at CREATE time; only SQLite tolerates
44
+ them. Don't trust ordering-doesn't-matter claims without a Postgres test.
45
+
46
+ Genuine cross-table cycles (A→B→A) still fall through in input order and fail
47
+ on Postgres — a real fix needs post-create `ALTER TABLE ADD CONSTRAINT`
48
+ deferral (follow-up tracked from #302).
49
+
50
+ ## How to recognize
51
+
52
+ - `relation "X" does not exist` during `auto_migrate` on a fresh Postgres
53
+ schema, where X *is* among the registered models.
54
+ - The failure appears/disappears when a self-FK is added/removed anywhere in
55
+ the dependency component, or when table names are renamed across the
56
+ alphabetical boundary.
57
+ - SQLite passes while Postgres fails: SQLite accepts forward FK references, so
58
+ ordering bugs are invisible there — regression tests for CREATE order must
59
+ run on the Postgres matrix.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ferro-orm"
3
- version = "0.16.1"
3
+ version = "0.16.3"
4
4
  description = "A high-performance, Rust-backed ORM for Python."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -73,13 +73,13 @@ async def _ensure_rust_registration_synced_for_operation() -> None:
73
73
  contention the event loop blocks for one resolve+install per generation; do
74
74
  not offload this to a thread pool — that would reintroduce interleaving.
75
75
  """
76
- from .state import is_modelset_dirty
76
+ from .registry import REGISTRY
77
77
 
78
- if not is_modelset_dirty():
78
+ if not REGISTRY.is_dirty():
79
79
  return
80
80
 
81
81
  with _OPERATION_REGISTRATION_SYNC_LOCK:
82
- if is_modelset_dirty():
82
+ if REGISTRY.is_dirty():
83
83
  _ensure_rust_registration_synced()
84
84
 
85
85
 
@@ -97,25 +97,26 @@ def ensure_resolved_modelset() -> dict[str, Any]:
97
97
  path.
98
98
  """
99
99
  from .ir.compiler import compile_registry_schema_ir
100
+ from .registry import REGISTRY
100
101
  from .relations import resolve_relationships
101
- from .state import is_modelset_dirty
102
102
 
103
- if not is_modelset_dirty():
103
+ if not REGISTRY.is_dirty():
104
104
  return compile_registry_schema_ir()
105
105
 
106
106
  with _RESOLVED_MODELSET_LOCK:
107
- if is_modelset_dirty():
107
+ if REGISTRY.is_dirty():
108
108
  resolve_relationships()
109
109
  return compile_registry_schema_ir()
110
110
 
111
111
 
112
112
  def _ensure_rust_registration_synced() -> None:
113
113
  """Resolve the modelset if needed, then push it to the Rust runtime."""
114
- from . import state as ferro_state
114
+ from .registry import REGISTRY
115
115
 
116
116
  envelope = ensure_resolved_modelset()
117
+ cached = REGISTRY.modelset()
117
118
  _push_registration_to_rust(
118
- envelope, fingerprint=ferro_state._SCHEMA_IR_MODELSET_FINGERPRINT
119
+ envelope, fingerprint=cached[1] if cached is not None else None
119
120
  )
120
121
 
121
122
 
@@ -131,24 +132,21 @@ def clear_registry() -> None:
131
132
  tolerated by SQLite but rejected by Postgres (``relation ... does not
132
133
  exist``). (#153)
133
134
 
134
- The Python model registry (``_MODEL_REGISTRY_PY``) is intentionally **not**
135
- cleared here: clearing the Rust registry while keeping the declared Python
136
- models is what allows cold re-hydration after ``reset_engine`` (see
137
- ``tests/test_enum_cold_hydration.py``). Callers that want a full Python-side
138
- reset clear ``_MODEL_REGISTRY_PY`` / ``_PENDING_RELATIONS`` themselves.
139
-
140
- Each purged join table's compiled SchemaIR envelope is also evicted from the
141
- per-model envelope cache (via ``evict_model_envelope`` — envelope-only, so the
142
- model registry stays intact per the promise above), keeping the two stores the
143
- #153 guard depends on in agreement: a lingering join envelope would let a
144
- future assemble step resurrect the stale join table.
135
+ The Python model registry is intentionally **not** cleared here: clearing
136
+ the Rust registry while keeping the declared Python models is what allows
137
+ cold re-hydration after ``reset_engine`` (see
138
+ ``tests/test_enum_cold_hydration.py``). Callers that want a full
139
+ Python-side reset use ``REGISTRY.reset_for_test()``.
140
+
141
+ Each purged join table's compiled SchemaIR envelope is evicted with it —
142
+ ``Registry.clear_join_tables`` owns that agreement (#153): a lingering
143
+ join envelope would let a future assemble step resurrect the stale join
144
+ table.
145
145
  """
146
146
  _core_clear_registry()
147
- from .state import _JOIN_TABLE_REGISTRY, evict_model_envelope
147
+ from .registry import REGISTRY
148
148
 
149
- for join_table in list(_JOIN_TABLE_REGISTRY):
150
- evict_model_envelope(join_table)
151
- _JOIN_TABLE_REGISTRY.clear()
149
+ REGISTRY.clear_join_tables()
152
150
 
153
151
 
154
152
  def _push_registration_to_rust(
@@ -172,18 +170,15 @@ def _push_registration_to_rust(
172
170
  """
173
171
  import json as _json
174
172
 
175
- from . import state as ferro_state
176
173
  from .ir.compiler import compile_registry_schema_ir, schema_ir_fingerprint
174
+ from .registry import REGISTRY
177
175
 
178
176
  if envelope is None:
179
177
  envelope = compile_registry_schema_ir()
180
178
  if fingerprint is None:
181
- cached = ferro_state._SCHEMA_IR_MODELSET
182
- if (
183
- cached is envelope
184
- and ferro_state._SCHEMA_IR_MODELSET_FINGERPRINT is not None
185
- ):
186
- fingerprint = ferro_state._SCHEMA_IR_MODELSET_FINGERPRINT
179
+ cached = REGISTRY.modelset()
180
+ if cached is not None and cached[0] is envelope:
181
+ fingerprint = cached[1]
187
182
  else:
188
183
  fingerprint = schema_ir_fingerprint(envelope)
189
184
  _install_registration(_json.dumps(envelope), fingerprint)
@@ -27,13 +27,12 @@ def _scalar_part_of_annotation(ann: Any) -> Any:
27
27
 
28
28
  def pk_python_type_for_model(target: type[Any]) -> Any | None:
29
29
  """Return the PK column's scalar python type from the target's specs."""
30
- specs = getattr(target, "__ferro_columns__", None)
31
- if not specs:
30
+ pk_field = getattr(target, "__ferro_pk__", None)
31
+ if pk_field is None:
32
32
  return None
33
- for spec in specs.values():
34
- if spec.primary_key:
35
- return spec.python_type
36
- return None
33
+ specs = getattr(target, "__ferro_columns__", None) or {}
34
+ spec = specs.get(pk_field)
35
+ return spec.python_type if spec is not None else None
37
36
 
38
37
 
39
38
  def shadow_annotation_for_pk(pk_ann: Any) -> Any:
@@ -31,9 +31,37 @@ __all__ = [
31
31
  "build_relation_specs",
32
32
  "fk_shadow_spec",
33
33
  "pk_spec",
34
+ "primary_key_field_name",
34
35
  ]
35
36
 
36
37
 
38
+ def primary_key_field_name(model_name: str, specs: dict[str, ColumnSpec]) -> str | None:
39
+ """Derive the model's single PK field name from its column specs.
40
+
41
+ The one derivation site for "which column is the primary key" — cached on
42
+ the class as ``__ferro_pk__`` at the compile choke point, so every
43
+ consumer (save, descriptors, shadow-FK typing, traversal) reads the cached
44
+ fact instead of re-looping specs. At most one ``primary_key=True`` column
45
+ is legal; zero is a valid declaration (``None``) and the operations that
46
+ need a PK raise their own errors.
47
+
48
+ Raises:
49
+ TypeError: If more than one column declares ``primary_key=True`` —
50
+ at class definition time, naming the model and offending fields,
51
+ rather than resolving the ambiguity silently by declaration order
52
+ (the follow-up ADR-0002 asked for). ``TypeError`` is the
53
+ declaration-error contract the metaclass surfaces unwrapped
54
+ (ADR-0003's db_type validation sets the precedent).
55
+ """
56
+ pks = [name for name, spec in specs.items() if spec.primary_key]
57
+ if len(pks) > 1:
58
+ raise TypeError(
59
+ f"Model {model_name!r} declares multiple primary-key columns: "
60
+ f"{sorted(pks)!r}. At most one primary_key=True column is supported."
61
+ )
62
+ return pks[0] if pks else None
63
+
64
+
37
65
  @dataclass(frozen=True, slots=True)
38
66
  class ForeignKeyRef:
39
67
  to_table: str
@@ -126,13 +154,13 @@ def _property_is_integer(prop: dict[str, Any]) -> bool:
126
154
 
127
155
 
128
156
  def _target_table_name(target: Any) -> str:
129
- from .state import resolve_model_reference
157
+ from .registry import REGISTRY
130
158
 
131
159
  if isinstance(target, ForwardRef):
132
160
  target = target.__forward_arg__
133
161
  if isinstance(target, str):
134
162
  try:
135
- model = resolve_model_reference(target, default=None)
163
+ model = REGISTRY.resolve_reference(target, default=None)
136
164
  except RuntimeError:
137
165
  # Ambiguous short ref during first-pass schema build: defer the
138
166
  # loud, candidate-listing error to resolve_relationships (the