ferro-orm 0.17.0__tar.gz → 0.17.1__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.17.0 → ferro_orm-0.17.1}/CHANGELOG.md +9 -0
  2. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/CONTEXT.md +16 -0
  3. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/Cargo.lock +52 -41
  4. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/Cargo.toml +1 -1
  5. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/PKG-INFO +1 -1
  6. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/crates/ferro-ddl-lowering/src/lib.rs +7 -0
  7. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/crates/ferro-migrate/src/emit.rs +89 -10
  8. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/crates/ferro-migrate/src/lib.rs +103 -1
  9. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/crates/ferro-migrate/src/tests.rs +343 -0
  10. ferro_orm-0.17.1/docs/adr/0010-create-pass-owns-only-missing-tables.md +39 -0
  11. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/concepts/architecture.md +1 -1
  12. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/faq.md +1 -1
  13. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/guide/migrations.md +3 -0
  14. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/pyproject.toml +1 -1
  15. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/__init__.py +16 -4
  16. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/introspect.rs +231 -0
  17. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/migrate.rs +51 -6
  18. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/schema.rs +13 -0
  19. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_auto_migrate.py +169 -0
  20. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_migrate_plan.py +84 -0
  21. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  22. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/PERMISSIONS.md +0 -0
  24. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/PYPI_CHECKLIST.md +0 -0
  25. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/PYPI_SETUP.md +0 -0
  26. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/generated/wheels.generated.yml +0 -0
  27. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/pull_request_template.md +0 -0
  28. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/workflows/ci.yml +0 -0
  29. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/workflows/packaging-smoke.yml +0 -0
  30. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/workflows/publish-docs.yml +0 -0
  31. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/workflows/publish.yml +0 -0
  32. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.github/workflows/release.yml +0 -0
  33. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.gitignore +0 -0
  34. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.pre-commit-config.yaml +0 -0
  35. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/.python-version +0 -0
  36. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/AGENTS.md +0 -0
  37. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/CONTRIBUTING.md +0 -0
  38. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/LICENSE +0 -0
  39. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/README.md +0 -0
  40. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/benchmarks/README.md +0 -0
  41. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/benchmarks/__init__.py +0 -0
  42. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/benchmarks/__main__.py +0 -0
  43. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/benchmarks/backends.py +0 -0
  44. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/benchmarks/baselines/postgres.json +0 -0
  45. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/benchmarks/baselines/sqlite.json +0 -0
  46. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/benchmarks/compare.py +0 -0
  47. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/benchmarks/harness.py +0 -0
  48. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/benchmarks/model.py +0 -0
  49. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/benchmarks/run.py +0 -0
  50. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
  51. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/crates/ferro-migrate/Cargo.toml +0 -0
  52. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/crates/ferro-migrate/src/order.rs +0 -0
  53. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/crates/ferro-schema-ir/Cargo.toml +0 -0
  54. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/crates/ferro-schema-ir/src/lib.rs +0 -0
  55. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/adr/0001-resolved-epoch-registration.md +0 -0
  56. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
  57. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
  58. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
  59. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
  60. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
  61. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
  62. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/adr/0007-reverse-relations-tested-not-traversed.md +0 -0
  63. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/adr/0008-populated-relations-include.md +0 -0
  64. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/adr/0008-predicate-negation-uniform-not.md +0 -0
  65. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
  66. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/agents/domain.md +0 -0
  67. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/agents/issue-tracker.md +0 -0
  68. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/agents/triage-labels.md +0 -0
  69. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/architecture-review-2026-07-08.html +0 -0
  70. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
  71. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
  72. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
  73. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
  74. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
  75. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
  76. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
  77. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
  78. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-06-26-ir-p8.5-143-db-type-drop-requirements.md +0 -0
  79. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
  80. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-06-28-ir-p8.5-141-single-schema-ir-producer-requirements.md +0 -0
  81. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-06-29-ir-p8.6-146-dialect-enum-unification-requirements.md +0 -0
  82. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-06-29-ir-p8.6-153-create-path-ir-requirements.md +0 -0
  83. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
  84. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
  85. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-07-01-ir-p8.6-154-datetime-tz-coarseness-requirements.md +0 -0
  86. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-07-01-ir-p8.6-162-typed-save-bind-requirements.md +0 -0
  87. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-07-01-ir-p8.6-165-blob-introspection-false-positive-requirements.md +0 -0
  88. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
  89. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
  90. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/aggregations.py +0 -0
  91. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/aggregations_annotated.py +0 -0
  92. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/existence_tests.py +0 -0
  93. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/existence_tests_annotated.py +0 -0
  94. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/multiple_databases.py +0 -0
  95. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/mutations.py +0 -0
  96. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/pagination.py +0 -0
  97. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/partial_selects.py +0 -0
  98. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/partial_selects_annotated.py +0 -0
  99. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/populated_relations.py +0 -0
  100. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/populated_relations_annotated.py +0 -0
  101. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/predicates.py +0 -0
  102. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/predicates_annotated.py +0 -0
  103. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/quickstart.py +0 -0
  104. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/quickstart_annotated.py +0 -0
  105. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/raw_sql.py +0 -0
  106. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/relationships.py +0 -0
  107. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/relationships_annotated.py +0 -0
  108. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/soft_deletes.py +0 -0
  109. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/soft_deletes_annotated.py +0 -0
  110. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/testing_conftest.py +0 -0
  111. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/timestamps.py +0 -0
  112. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/timestamps_annotated.py +0 -0
  113. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/transactions.py +0 -0
  114. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/traversal.py +0 -0
  115. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/examples/traversal_annotated.py +0 -0
  116. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/api/connection.md +0 -0
  117. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/api/exceptions.md +0 -0
  118. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/api/fields.md +0 -0
  119. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/api/migrations.md +0 -0
  120. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/api/model.md +0 -0
  121. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/api/queries.md +0 -0
  122. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/api/raw-sql.md +0 -0
  123. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/api/relationships.md +0 -0
  124. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/api/transactions.md +0 -0
  125. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/changelog.md +0 -0
  126. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/concepts/backends.md +0 -0
  127. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/concepts/identity-map.md +0 -0
  128. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/concepts/performance.md +0 -0
  129. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/concepts/query-typing.md +0 -0
  130. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/concepts/type-safety.md +0 -0
  131. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/contributing.md +0 -0
  132. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/getting-started/installation.md +0 -0
  133. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/getting-started/next-steps.md +0 -0
  134. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/getting-started/quickstart.md +0 -0
  135. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/guide/aggregations.md +0 -0
  136. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/guide/connections.md +0 -0
  137. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/guide/models-and-fields.md +0 -0
  138. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/guide/mutations.md +0 -0
  139. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/guide/queries.md +0 -0
  140. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/guide/raw-sql.md +0 -0
  141. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/guide/relationships.md +0 -0
  142. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/guide/transactions.md +0 -0
  143. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
  144. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/howto/multiple-databases.md +0 -0
  145. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/howto/pagination.md +0 -0
  146. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/howto/soft-deletes.md +0 -0
  147. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/howto/testing.md +0 -0
  148. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/howto/timestamps.md +0 -0
  149. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/howto/upgrade-guide.md +0 -0
  150. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/index.md +0 -0
  151. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/roadmap.md +0 -0
  152. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/stylesheets/extra.css +0 -0
  153. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/pages/why-ferro.md +0 -0
  154. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
  155. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
  156. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
  157. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
  158. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
  159. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
  160. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
  161. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
  162. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-06-24-001-feat-ir-p8-119-wire-automigrate-plan.md +0 -0
  163. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-06-25-001-feat-ir-p8-120-parity-gate-plan.md +0 -0
  164. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-06-26-001-feat-ir-p8.5-140-shared-lowering-plan.md +0 -0
  165. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-06-26-002-feat-ir-p8.5-143-db-type-drop-plan.md +0 -0
  166. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-06-26-003-feat-ir-p8.5-144-reconcile-indexes-plan.md +0 -0
  167. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-06-28-001-feat-ir-p8.5-141-single-schema-ir-producer-plan.md +0 -0
  168. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-06-29-001-refactor-ir-p8.6-153-create-path-unification-plan.md +0 -0
  169. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-06-29-002-refactor-ir-p8.6-146-dialect-enum-unification-plan.md +0 -0
  170. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-06-30-001-refactor-ir-p8.6-158-check-renderer-plan.md +0 -0
  171. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-06-30-002-fix-ir-p8.6-155-deferred-annotations-plan.md +0 -0
  172. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-01-001-feat-ir-p8.6-162-typed-save-bind-plan.md +0 -0
  173. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-01-002-fix-ir-p8.6-154-datetime-tz-coarseness-plan.md +0 -0
  174. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-01-003-fix-ir-p8.6-165-blob-introspection-plan.md +0 -0
  175. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
  176. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-02-002-feat-ff-a-172-typed-exceptions-plan.md +0 -0
  177. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
  178. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +0 -0
  179. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
  180. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
  181. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
  182. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
  183. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-06-009-ff-f-query-builder-1.0-design.md +0 -0
  184. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-06-010-ff-f-query-builder-1.0-plan.md +0 -0
  185. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
  186. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
  187. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
  188. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
  189. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
  190. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/2026-07-18-016-existence-tests-design.md +0 -0
  191. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/ir-first-migration-guide.md +0 -0
  192. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/plans/ir-first-release-checklist.md +0 -0
  193. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/rfc/ir-contracts-v1.md +0 -0
  194. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/README.md +0 -0
  195. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
  196. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
  197. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
  198. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
  199. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
  200. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
  201. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/issues/self-fk-evicts-component-from-create-order.md +0 -0
  202. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
  203. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
  204. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
  205. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
  206. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
  207. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
  208. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
  209. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/patterns/foreign-key-index.md +0 -0
  210. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
  211. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/patterns/ir-invariants.md +0 -0
  212. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/patterns/persistence-state.md +0 -0
  213. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
  214. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
  215. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/docs/solutions/patterns/typed-null-binds.md +0 -0
  216. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/justfile +0 -0
  217. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/scripts/demo_queries.py +0 -0
  218. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/scripts/demo_queries_pre_v012.py +0 -0
  219. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/backend.rs +0 -0
  220. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/codec.rs +0 -0
  221. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/codec_plan.rs +0 -0
  222. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/connection.rs +0 -0
  223. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/errors.rs +0 -0
  224. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/_annotation_utils.py +0 -0
  225. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/_bind_payload.py +0 -0
  226. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/_core.pyi +0 -0
  227. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/_deprecations.py +0 -0
  228. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/_shadow_fk_types.py +0 -0
  229. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/base.py +0 -0
  230. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/columns.py +0 -0
  231. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/composite_indexes.py +0 -0
  232. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/composite_uniques.py +0 -0
  233. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/exceptions.py +0 -0
  234. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/fields.py +0 -0
  235. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/ir/__init__.py +0 -0
  236. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/ir/compiler.py +0 -0
  237. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/metaclass.py +0 -0
  238. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/migrations/__init__.py +0 -0
  239. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/migrations/alembic.py +0 -0
  240. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/models.py +0 -0
  241. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/py.typed +0 -0
  242. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/query/__init__.py +0 -0
  243. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/query/builder.py +0 -0
  244. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/query/nodes.py +0 -0
  245. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/query/rows.py +0 -0
  246. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/query/wire.py +0 -0
  247. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/raw.py +0 -0
  248. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/registry.py +0 -0
  249. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/relations/__init__.py +0 -0
  250. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/relations/descriptors.py +0 -0
  251. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/session.py +0 -0
  252. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/ferro/state.py +0 -0
  253. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/hydration.rs +0 -0
  254. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/lib.rs +0 -0
  255. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/naming_ffi.rs +0 -0
  256. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/operations.rs +0 -0
  257. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/query.rs +0 -0
  258. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/schema_bind.rs +0 -0
  259. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/src/state.rs +0 -0
  260. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/__init__.py +0 -0
  261. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/conftest.py +0 -0
  262. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/db_backends.py +0 -0
  263. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/README.md +0 -0
  264. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
  265. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_account_exists_v7.json +0 -0
  266. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_account_scoped_exists_v7.json +0 -0
  267. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_owner_nested_exists_v7.json +0 -0
  268. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_owner_not_exists_v7.json +0 -0
  269. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_transaction_aggregate_v7.json +0 -0
  270. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v7.json +0 -0
  271. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_transaction_include_v7.json +0 -0
  272. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_transaction_left_join_v7.json +0 -0
  273. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_transaction_record_v7.json +0 -0
  274. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_transaction_traversal_v7.json +0 -0
  275. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_transaction_traversed_record_v7.json +0 -0
  276. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_user_compound_v7.json +0 -0
  277. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_user_m2m_exists_v7.json +0 -0
  278. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_user_not_compound_v7.json +0 -0
  279. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/query_user_not_leaf_v7.json +0 -0
  280. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
  281. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
  282. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
  283. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/static_fixtures/bad_includes.py +0 -0
  284. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/static_fixtures/bad_predicates.py +0 -0
  285. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/static_fixtures/bad_projections.py +0 -0
  286. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/static_fixtures/good_includes.py +0 -0
  287. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/static_fixtures/good_predicates.py +0 -0
  288. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/static_fixtures/good_projections.py +0 -0
  289. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_aggregation.py +0 -0
  290. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_alembic_autogenerate.py +0 -0
  291. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_alembic_bridge.py +0 -0
  292. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_alembic_db_type.py +0 -0
  293. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_alembic_nullability.py +0 -0
  294. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_alembic_type_mapping.py +0 -0
  295. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_annotation_utils.py +0 -0
  296. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_bulk_create_chunking.py +0 -0
  297. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_bulk_install.py +0 -0
  298. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_bulk_update.py +0 -0
  299. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_catalog_cache.py +0 -0
  300. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_codec_plan.py +0 -0
  301. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_column_specs.py +0 -0
  302. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_compiled_query.py +0 -0
  303. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_composite_index.py +0 -0
  304. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_composite_unique.py +0 -0
  305. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_connection.py +0 -0
  306. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_connection_redaction.py +0 -0
  307. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_constraints.py +0 -0
  308. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_cross_emitter_parity.py +0 -0
  309. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_crud.py +0 -0
  310. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_db_backends.py +0 -0
  311. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_db_type_cross_emitter_parity.py +0 -0
  312. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_db_type_integration.py +0 -0
  313. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_db_type_typing.py +0 -0
  314. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_db_type_validation.py +0 -0
  315. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_deletion.py +0 -0
  316. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_deprecations.py +0 -0
  317. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_docs_examples.py +0 -0
  318. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_documentation_features.py +0 -0
  319. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_enum_cold_hydration.py +0 -0
  320. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_exception_mapping.py +0 -0
  321. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_exceptions.py +0 -0
  322. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_field_wrapper.py +0 -0
  323. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_fk_target_pk.py +0 -0
  324. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_generation_counter_dirty_tracking.py +0 -0
  325. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_global_aggregates.py +0 -0
  326. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_grouped_aggregates.py +0 -0
  327. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_helpers.py +0 -0
  328. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_hydration.py +0 -0
  329. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_hydration_equivalence.py +0 -0
  330. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_identity_memory.py +0 -0
  331. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_identity_refresh.py +0 -0
  332. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_identity_scoped_invalidation.py +0 -0
  333. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_identity_weakref.py +0 -0
  334. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_import_budget.py +0 -0
  335. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_include.py +0 -0
  336. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_ir_vectors_contract.py +0 -0
  337. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_metaclass_internals.py +0 -0
  338. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_metadata.py +0 -0
  339. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_model_identity.py +0 -0
  340. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_models.py +0 -0
  341. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_mutation_pagination_guard.py +0 -0
  342. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_named_connections_integration.py +0 -0
  343. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_naming_single_source.py +0 -0
  344. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_negation.py +0 -0
  345. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_one_to_one.py +0 -0
  346. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_operation_seam_sync.py +0 -0
  347. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_partial_selects.py +0 -0
  348. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_primary_key_facts.py +0 -0
  349. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_provisional_import.py +0 -0
  350. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_query_builder.py +0 -0
  351. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_query_column_validation.py +0 -0
  352. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_query_exists.py +0 -0
  353. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_query_immutability.py +0 -0
  354. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_query_joins.py +0 -0
  355. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_query_typing.py +0 -0
  356. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_query_wire_vectors.py +0 -0
  357. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_raw_sql.py +0 -0
  358. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_refresh.py +0 -0
  359. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_registry_entrypoints.py +0 -0
  360. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_registry_owner.py +0 -0
  361. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_relation_specs.py +0 -0
  362. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_relationship_engine.py +0 -0
  363. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_relationship_resolution_errors.py +0 -0
  364. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_route_single_site.py +0 -0
  365. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_routing_errors.py +0 -0
  366. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_save_pk_edges.py +0 -0
  367. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_save_semantics.py +0 -0
  368. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_schema.py +0 -0
  369. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_schema_constraints.py +0 -0
  370. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_schema_db_type_metadata.py +0 -0
  371. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_schema_enum_annotations.py +0 -0
  372. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_session.py +0 -0
  373. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_shadow_fk_types.py +0 -0
  374. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
  375. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_static_contracts.py +0 -0
  376. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_string_search.py +0 -0
  377. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_structural_types.py +0 -0
  378. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_temporal_types.py +0 -0
  379. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_transactions.py +0 -0
  380. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_traversed_projection.py +0 -0
  381. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_typed_null_binds.py +0 -0
  382. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/tests/test_typed_save_bind.py +0 -0
  383. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/uv.lock +0 -0
  384. {ferro_orm-0.17.0 → ferro_orm-0.17.1}/zensical.toml +0 -0
@@ -1,6 +1,15 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.17.1 (2026-07-19)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **migrate**: Auto-migrate pass ownership — column-before-index sequencing and FK on_delete
9
+ reconciliation ([#326](https://github.com/syn54x/ferro-orm/pull/326),
10
+ [`3451b1d`](https://github.com/syn54x/ferro-orm/commit/3451b1dad37d8ac39709d986aa20b1de4c12542a))
11
+
12
+
4
13
  ## v0.17.0 (2026-07-18)
5
14
 
6
15
  ### Documentation
@@ -123,3 +123,19 @@ _Avoid_: Import-time registration, partial registry
123
123
  **Resolved registration**:
124
124
  The registry epoch after relationship resolution completes — join tables exist, shadow FK columns are wired, and the SchemaIR modelset is authoritative for DDL and auto-migrate.
125
125
  _Avoid_: Final registration, committed registry
126
+
127
+ **Create pass**:
128
+ The auto-migrate step that brings missing tables into existence — the table, its columns, its indexes and constraints, together. A table that already exists is left completely untouched by this pass, whatever its shape.
129
+ _Avoid_: Bootstrap, ensure-tables, table sync
130
+
131
+ **Reconciliation pass**:
132
+ The `migrate_updates` step that alters existing tables to match the registered models — the only authority for DDL against a table that already exists. Within one table, column changes land before the indexes and constraints that reference them.
133
+ _Avoid_: Update pass, schema sync, drift repair
134
+
135
+ **Ferro-owned artifact**:
136
+ An index or constraint whose name follows ferro's canonical naming (`idx_`, `uq_`, `fk_`, `ck_`), marking it as reconcilable: auto-migrate may create, rebuild, or drop it to match the declared model. Artifacts named any other way belong to the user and are never altered or dropped.
137
+ _Avoid_: Managed index, system constraint, internal index
138
+
139
+ **Constraint rebuild**:
140
+ Drop-and-recreate of a ferro-owned constraint whose live definition no longer matches the declared model — a foreign key's `on_delete`, its target, its columns. Metadata-only: rows are never touched. On a backend that cannot alter constraints, ferro warns loudly and skips; it never diverges silently.
141
+ _Avoid_: Constraint alter, FK patch, in-place constraint update
@@ -95,9 +95,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
95
95
 
96
96
  [[package]]
97
97
  name = "cc"
98
- version = "1.2.67"
98
+ version = "1.3.0"
99
99
  source = "registry+https://github.com/rust-lang/crates.io-index"
100
- checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
100
+ checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
101
101
  dependencies = [
102
102
  "find-msvc-tools",
103
103
  "shlex",
@@ -210,7 +210,7 @@ dependencies = [
210
210
  "ident_case",
211
211
  "proc-macro2",
212
212
  "quote",
213
- "syn",
213
+ "syn 2.0.119",
214
214
  ]
215
215
 
216
216
  [[package]]
@@ -221,7 +221,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
221
221
  dependencies = [
222
222
  "darling_core",
223
223
  "quote",
224
- "syn",
224
+ "syn 2.0.119",
225
225
  ]
226
226
 
227
227
  [[package]]
@@ -269,7 +269,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
269
269
  dependencies = [
270
270
  "proc-macro2",
271
271
  "quote",
272
- "syn",
272
+ "syn 2.0.119",
273
273
  ]
274
274
 
275
275
  [[package]]
@@ -327,7 +327,7 @@ dependencies = [
327
327
 
328
328
  [[package]]
329
329
  name = "ferro"
330
- version = "0.17.0"
330
+ version = "0.17.1"
331
331
  dependencies = [
332
332
  "chrono",
333
333
  "dashmap",
@@ -477,7 +477,7 @@ checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
477
477
  dependencies = [
478
478
  "proc-macro2",
479
479
  "quote",
480
- "syn",
480
+ "syn 2.0.119",
481
481
  ]
482
482
 
483
483
  [[package]]
@@ -772,13 +772,13 @@ dependencies = [
772
772
 
773
773
  [[package]]
774
774
  name = "inherent"
775
- version = "1.0.13"
775
+ version = "1.0.14"
776
776
  source = "registry+https://github.com/rust-lang/crates.io-index"
777
- checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7"
777
+ checksum = "bee2c455ca60511a054699102d40ce7153621cb2429558f9eaa600e4499b5984"
778
778
  dependencies = [
779
779
  "proc-macro2",
780
780
  "quote",
781
- "syn",
781
+ "syn 3.0.0",
782
782
  ]
783
783
 
784
784
  [[package]]
@@ -1069,9 +1069,9 @@ dependencies = [
1069
1069
 
1070
1070
  [[package]]
1071
1071
  name = "proc-macro2"
1072
- version = "1.0.106"
1072
+ version = "1.0.107"
1073
1073
  source = "registry+https://github.com/rust-lang/crates.io-index"
1074
- checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
1074
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
1075
1075
  dependencies = [
1076
1076
  "unicode-ident",
1077
1077
  ]
@@ -1134,7 +1134,7 @@ dependencies = [
1134
1134
  "proc-macro2",
1135
1135
  "pyo3-macros-backend",
1136
1136
  "quote",
1137
- "syn",
1137
+ "syn 2.0.119",
1138
1138
  ]
1139
1139
 
1140
1140
  [[package]]
@@ -1147,14 +1147,14 @@ dependencies = [
1147
1147
  "proc-macro2",
1148
1148
  "pyo3-build-config",
1149
1149
  "quote",
1150
- "syn",
1150
+ "syn 2.0.119",
1151
1151
  ]
1152
1152
 
1153
1153
  [[package]]
1154
1154
  name = "quote"
1155
- version = "1.0.46"
1155
+ version = "1.0.47"
1156
1156
  source = "registry+https://github.com/rust-lang/crates.io-index"
1157
- checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
1157
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
1158
1158
  dependencies = [
1159
1159
  "proc-macro2",
1160
1160
  ]
@@ -1320,15 +1320,15 @@ dependencies = [
1320
1320
  "heck 0.4.1",
1321
1321
  "proc-macro2",
1322
1322
  "quote",
1323
- "syn",
1323
+ "syn 2.0.119",
1324
1324
  "thiserror",
1325
1325
  ]
1326
1326
 
1327
1327
  [[package]]
1328
1328
  name = "serde"
1329
- version = "1.0.228"
1329
+ version = "1.0.229"
1330
1330
  source = "registry+https://github.com/rust-lang/crates.io-index"
1331
- checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
1331
+ checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
1332
1332
  dependencies = [
1333
1333
  "serde_core",
1334
1334
  "serde_derive",
@@ -1336,22 +1336,22 @@ dependencies = [
1336
1336
 
1337
1337
  [[package]]
1338
1338
  name = "serde_core"
1339
- version = "1.0.228"
1339
+ version = "1.0.229"
1340
1340
  source = "registry+https://github.com/rust-lang/crates.io-index"
1341
- checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
1341
+ checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
1342
1342
  dependencies = [
1343
1343
  "serde_derive",
1344
1344
  ]
1345
1345
 
1346
1346
  [[package]]
1347
1347
  name = "serde_derive"
1348
- version = "1.0.228"
1348
+ version = "1.0.229"
1349
1349
  source = "registry+https://github.com/rust-lang/crates.io-index"
1350
- checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
1350
+ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
1351
1351
  dependencies = [
1352
1352
  "proc-macro2",
1353
1353
  "quote",
1354
- "syn",
1354
+ "syn 3.0.0",
1355
1355
  ]
1356
1356
 
1357
1357
  [[package]]
@@ -1533,7 +1533,7 @@ dependencies = [
1533
1533
  "quote",
1534
1534
  "sqlx-core",
1535
1535
  "sqlx-macros-core",
1536
- "syn",
1536
+ "syn 2.0.119",
1537
1537
  ]
1538
1538
 
1539
1539
  [[package]]
@@ -1556,7 +1556,7 @@ dependencies = [
1556
1556
  "sqlx-mysql",
1557
1557
  "sqlx-postgres",
1558
1558
  "sqlx-sqlite",
1559
- "syn",
1559
+ "syn 2.0.119",
1560
1560
  "tokio",
1561
1561
  "url",
1562
1562
  ]
@@ -1707,6 +1707,17 @@ dependencies = [
1707
1707
  "unicode-ident",
1708
1708
  ]
1709
1709
 
1710
+ [[package]]
1711
+ name = "syn"
1712
+ version = "3.0.0"
1713
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1714
+ checksum = "f2fac314a64dc9a36e61a9eb4261a5e9bbfbc922b27e518af97bc32b926cf967"
1715
+ dependencies = [
1716
+ "proc-macro2",
1717
+ "quote",
1718
+ "unicode-ident",
1719
+ ]
1720
+
1710
1721
  [[package]]
1711
1722
  name = "synstructure"
1712
1723
  version = "0.13.2"
@@ -1715,7 +1726,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
1715
1726
  dependencies = [
1716
1727
  "proc-macro2",
1717
1728
  "quote",
1718
- "syn",
1729
+ "syn 2.0.119",
1719
1730
  ]
1720
1731
 
1721
1732
  [[package]]
@@ -1726,22 +1737,22 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
1726
1737
 
1727
1738
  [[package]]
1728
1739
  name = "thiserror"
1729
- version = "2.0.18"
1740
+ version = "2.0.19"
1730
1741
  source = "registry+https://github.com/rust-lang/crates.io-index"
1731
- checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
1742
+ checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
1732
1743
  dependencies = [
1733
1744
  "thiserror-impl",
1734
1745
  ]
1735
1746
 
1736
1747
  [[package]]
1737
1748
  name = "thiserror-impl"
1738
- version = "2.0.18"
1749
+ version = "2.0.19"
1739
1750
  source = "registry+https://github.com/rust-lang/crates.io-index"
1740
- checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
1751
+ checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
1741
1752
  dependencies = [
1742
1753
  "proc-macro2",
1743
1754
  "quote",
1744
- "syn",
1755
+ "syn 3.0.0",
1745
1756
  ]
1746
1757
 
1747
1758
  [[package]]
@@ -1794,7 +1805,7 @@ checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
1794
1805
  dependencies = [
1795
1806
  "proc-macro2",
1796
1807
  "quote",
1797
- "syn",
1808
+ "syn 2.0.119",
1798
1809
  ]
1799
1810
 
1800
1811
  [[package]]
@@ -1828,7 +1839,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
1828
1839
  dependencies = [
1829
1840
  "proc-macro2",
1830
1841
  "quote",
1831
- "syn",
1842
+ "syn 2.0.119",
1832
1843
  ]
1833
1844
 
1834
1845
  [[package]]
@@ -1970,7 +1981,7 @@ dependencies = [
1970
1981
  "bumpalo",
1971
1982
  "proc-macro2",
1972
1983
  "quote",
1973
- "syn",
1984
+ "syn 2.0.119",
1974
1985
  "wasm-bindgen-shared",
1975
1986
  ]
1976
1987
 
@@ -2032,7 +2043,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
2032
2043
  dependencies = [
2033
2044
  "proc-macro2",
2034
2045
  "quote",
2035
- "syn",
2046
+ "syn 2.0.119",
2036
2047
  ]
2037
2048
 
2038
2049
  [[package]]
@@ -2043,7 +2054,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
2043
2054
  dependencies = [
2044
2055
  "proc-macro2",
2045
2056
  "quote",
2046
- "syn",
2057
+ "syn 2.0.119",
2047
2058
  ]
2048
2059
 
2049
2060
  [[package]]
@@ -2243,7 +2254,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
2243
2254
  dependencies = [
2244
2255
  "proc-macro2",
2245
2256
  "quote",
2246
- "syn",
2257
+ "syn 2.0.119",
2247
2258
  "synstructure",
2248
2259
  ]
2249
2260
 
@@ -2264,7 +2275,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
2264
2275
  dependencies = [
2265
2276
  "proc-macro2",
2266
2277
  "quote",
2267
- "syn",
2278
+ "syn 2.0.119",
2268
2279
  ]
2269
2280
 
2270
2281
  [[package]]
@@ -2284,7 +2295,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
2284
2295
  dependencies = [
2285
2296
  "proc-macro2",
2286
2297
  "quote",
2287
- "syn",
2298
+ "syn 2.0.119",
2288
2299
  "synstructure",
2289
2300
  ]
2290
2301
 
@@ -2324,7 +2335,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
2324
2335
  dependencies = [
2325
2336
  "proc-macro2",
2326
2337
  "quote",
2327
- "syn",
2338
+ "syn 2.0.119",
2328
2339
  ]
2329
2340
 
2330
2341
  [[package]]
@@ -3,7 +3,7 @@ members = ["crates/ferro-schema-ir", "crates/ferro-ddl-lowering", "crates/ferro-
3
3
 
4
4
  [package]
5
5
  name = "ferro"
6
- version = "0.17.0"
6
+ version = "0.17.1"
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.17.0
3
+ Version: 0.17.1
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'
@@ -542,6 +542,13 @@ pub fn fk_name(table_lower: &str, col_name: &str, to_table: &str) -> String {
542
542
  raw
543
543
  }
544
544
 
545
+ /// Whether a live constraint name follows the ferro FK convention above — the
546
+ /// ownership test: reconciliation only ever rebuilds names ferro emits;
547
+ /// constraints named any other way belong to the user and are never altered.
548
+ pub fn is_ferro_fk_name(name: &str) -> bool {
549
+ name.starts_with("fk_")
550
+ }
551
+
545
552
  /// Single-column unique name with 63-char guard.
546
553
  pub fn single_unique_index_name(table_lower: &str, col_name: &str) -> String {
547
554
  let raw = format!("uq_{table_lower}_{col_name}");
@@ -408,16 +408,7 @@ fn emit_add_column(
408
408
 
409
409
  if let Some(fk) = model.foreign_keys.iter().find(|fk| fk.column == column) {
410
410
  if dialect == Dialect::Postgres {
411
- let on_delete = fk_action_from_str(fk.on_delete.as_deref());
412
- result.statements.push(format!(
413
- "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}) ON DELETE {}",
414
- quote_ident(table),
415
- quote_ident(&fk_constraint_name(table, fk)),
416
- quote_ident(column),
417
- quote_ident(&fk.to_table),
418
- quote_ident(&fk.to_column),
419
- fk_action_sql(on_delete),
420
- ));
411
+ result.statements.push(render_add_fk_sql(table, fk));
421
412
  } else {
422
413
  result.warnings.push(format!(
423
414
  "Added foreign-key column '{}.{}' without its FOREIGN KEY constraint \
@@ -432,6 +423,39 @@ fn emit_add_column(
432
423
  Ok(result)
433
424
  }
434
425
 
426
+ /// `ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY ... ON DELETE ...` for one
427
+ /// IR foreign key. Postgres-only — SQLite cannot add table constraints to an
428
+ /// existing table; its callers warn instead.
429
+ fn render_add_fk_sql(table: &str, fk: &ferro_schema_ir::SchemaForeignKey) -> String {
430
+ format!(
431
+ "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}) ON DELETE {}",
432
+ quote_ident(table),
433
+ quote_ident(&fk_constraint_name(table, fk)),
434
+ quote_ident(&fk.column),
435
+ quote_ident(&fk.to_table),
436
+ quote_ident(&fk.to_column),
437
+ fk_action_sql(fk_action_from_str(fk.on_delete.as_deref())),
438
+ )
439
+ }
440
+
441
+ /// Find the declared FK for `column` on `table` in the new-IR model.
442
+ fn find_foreign_key<'a>(
443
+ model: &'a SchemaModel,
444
+ table: &str,
445
+ column: &str,
446
+ ) -> Result<&'a ferro_schema_ir::SchemaForeignKey, EmissionError> {
447
+ model
448
+ .foreign_keys
449
+ .iter()
450
+ .find(|fk| fk.column == column)
451
+ .ok_or_else(|| EmissionError {
452
+ message: format!(
453
+ "Foreign-key operation for '{}.{}' has no matching FK in the declared IR",
454
+ table, column
455
+ ),
456
+ })
457
+ }
458
+
435
459
  fn emit_alter_column_type(
436
460
  table: &str,
437
461
  column: &str,
@@ -673,6 +697,61 @@ pub fn emit_sql_with_ir(
673
697
  MigrationOp::DropIndex { table: _, name } => {
674
698
  result.statements.push(format!("DROP INDEX IF EXISTS \"{}\"", name));
675
699
  }
700
+ MigrationOp::AddForeignKey { table, column } => {
701
+ let model = find_model(&new_models, table)?;
702
+ let fk = find_foreign_key(model, table, column)?;
703
+ match dialect {
704
+ Dialect::Postgres => result.statements.push(render_add_fk_sql(table, fk)),
705
+ Dialect::Sqlite => result.warnings.push(format!(
706
+ "Declared FOREIGN KEY on '{}.{}' (on_delete {}) has no live \
707
+ constraint, and SQLite cannot add table constraints to an \
708
+ existing table. Referential integrity for this column is not \
709
+ database-enforced; use Alembic if you need the constraint.",
710
+ table,
711
+ column,
712
+ fk_action_sql(fk_action_from_str(fk.on_delete.as_deref())),
713
+ )),
714
+ }
715
+ }
716
+ MigrationOp::RebuildForeignKey {
717
+ table,
718
+ column,
719
+ old_name,
720
+ } => {
721
+ let model = find_model(&new_models, table)?;
722
+ let fk = find_foreign_key(model, table, column)?;
723
+ match dialect {
724
+ Dialect::Postgres => {
725
+ result.statements.push(format!(
726
+ "ALTER TABLE {} DROP CONSTRAINT {}",
727
+ quote_ident(table),
728
+ quote_ident(old_name),
729
+ ));
730
+ result.statements.push(render_add_fk_sql(table, fk));
731
+ }
732
+ Dialect::Sqlite => {
733
+ let live_action = find_model(&old_models, table)
734
+ .ok()
735
+ .and_then(|old| {
736
+ old.foreign_keys.iter().find(|live| live.column == *column)
737
+ })
738
+ .map(|live| {
739
+ fk_action_sql(fk_action_from_str(live.on_delete.as_deref()))
740
+ })
741
+ .unwrap_or("<unknown>");
742
+ result.warnings.push(format!(
743
+ "Foreign key on '{}.{}' declares on_delete {} but the live \
744
+ constraint enforces {}; SQLite cannot alter constraints in \
745
+ place, so the live behavior remains. Migrate with Alembic to \
746
+ apply the declared action.",
747
+ table,
748
+ column,
749
+ fk_action_sql(fk_action_from_str(fk.on_delete.as_deref())),
750
+ live_action,
751
+ ));
752
+ }
753
+ }
754
+ }
676
755
  }
677
756
  }
678
757
 
@@ -6,7 +6,9 @@
6
6
  mod emit;
7
7
  mod order;
8
8
 
9
- use ferro_ddl_lowering::schema_columns_storage_drift;
9
+ use ferro_ddl_lowering::{
10
+ fk_action_from_str, fk_action_sql, fk_name, is_ferro_fk_name, schema_columns_storage_drift,
11
+ };
10
12
  use ferro_schema_ir::{IrEnvelope, SchemaIrPayload, SchemaModel};
11
13
  use std::collections::{BTreeMap, BTreeSet};
12
14
 
@@ -97,6 +99,25 @@ pub enum MigrationOp {
97
99
  /// Index name.
98
100
  name: String,
99
101
  },
102
+ /// A declared FK whose column exists live but has no FK constraint at all.
103
+ /// (FKs on newly added columns ride the `AddColumn` emission instead.)
104
+ AddForeignKey {
105
+ /// Owning table.
106
+ table: String,
107
+ /// Local FK column.
108
+ column: String,
109
+ },
110
+ /// A live ferro-owned FK whose definition (`on_delete`, target) drifted
111
+ /// from the declared FK on the same column — rebuilt as
112
+ /// `DROP CONSTRAINT` + `ADD CONSTRAINT` where the backend allows it.
113
+ RebuildForeignKey {
114
+ /// Owning table.
115
+ table: String,
116
+ /// Local FK column.
117
+ column: String,
118
+ /// Name of the live constraint to drop.
119
+ old_name: String,
120
+ },
100
121
  }
101
122
 
102
123
  /// Ordered migration operations plus non-fatal warnings collected during planning.
@@ -167,6 +188,13 @@ pub fn emit_sql(plan: &MigrationPlan, dialect: Dialect) -> Vec<String> {
167
188
  MigrationOp::DropIndex { name, .. } => {
168
189
  sql.push(format!("-- index '{}' handled by emit_sql_with_ir", name));
169
190
  }
191
+ MigrationOp::AddForeignKey { table, column }
192
+ | MigrationOp::RebuildForeignKey { table, column, .. } => {
193
+ sql.push(format!(
194
+ "-- foreign key for '{}.{}' handled by emit_sql_with_ir",
195
+ table, column
196
+ ));
197
+ }
170
198
  }
171
199
  }
172
200
  sql
@@ -205,6 +233,7 @@ pub fn plan_from_ir(
205
233
  };
206
234
  diff_model_columns(*table, old_model, new_model, dialect, &mut plan);
207
235
  diff_model_indexes(*table, old_model, new_model, &mut plan);
236
+ diff_model_foreign_keys(*table, old_model, new_model, &mut plan);
208
237
  }
209
238
 
210
239
  plan
@@ -320,5 +349,78 @@ fn diff_model_indexes(
320
349
  }
321
350
  }
322
351
 
352
+ fn diff_model_foreign_keys(
353
+ table: &str,
354
+ old_model: &SchemaModel,
355
+ new_model: &SchemaModel,
356
+ plan: &mut MigrationPlan,
357
+ ) {
358
+ let old_col_names: BTreeSet<&str> = old_model.columns.iter().map(|c| c.name.as_str()).collect();
359
+
360
+ for fk in &new_model.foreign_keys {
361
+ // An FK on a newly added column rides the AddColumn emission; the
362
+ // reconcile step only governs FKs whose column already exists live.
363
+ if !old_col_names.contains(fk.column.as_str()) {
364
+ continue;
365
+ }
366
+
367
+ let Some(live) = old_model
368
+ .foreign_keys
369
+ .iter()
370
+ .find(|live| live.column == fk.column)
371
+ else {
372
+ plan.operations.push(MigrationOp::AddForeignKey {
373
+ table: table.to_string(),
374
+ column: fk.column.clone(),
375
+ });
376
+ continue;
377
+ };
378
+
379
+ // Live `to_column` can be empty when the backend reports an
380
+ // implicit-PK reference (SQLite); only a stated target can drift.
381
+ let target_drift = live.to_table != fk.to_table
382
+ || (!live.to_column.is_empty() && live.to_column != fk.to_column);
383
+ // Compare via the canonical SQL rendering — sea-query's
384
+ // `ForeignKeyAction` has no equality of its own.
385
+ let action_drift = fk_action_sql(fk_action_from_str(live.on_delete.as_deref()))
386
+ != fk_action_sql(fk_action_from_str(fk.on_delete.as_deref()));
387
+ if !target_drift && !action_drift {
388
+ continue;
389
+ }
390
+
391
+ match live.name.as_deref() {
392
+ // A drifting constraint ferro does not own is never altered —
393
+ // but it is never silent either.
394
+ Some(name) if !is_ferro_fk_name(name) => {
395
+ plan.warnings.push(format!(
396
+ "Foreign key on '{}.{}' drifts from the model (live: REFERENCES {} \
397
+ ON DELETE {}; declared: REFERENCES {} ON DELETE {}), but the live \
398
+ constraint '{}' is not ferro-owned, so it is left untouched. \
399
+ Migrate it manually or with Alembic.",
400
+ table,
401
+ fk.column,
402
+ live.to_table,
403
+ fk_action_sql(fk_action_from_str(live.on_delete.as_deref())),
404
+ fk.to_table,
405
+ fk_action_sql(fk_action_from_str(fk.on_delete.as_deref())),
406
+ name,
407
+ ));
408
+ }
409
+ _ => {
410
+ plan.operations.push(MigrationOp::RebuildForeignKey {
411
+ table: table.to_string(),
412
+ column: fk.column.clone(),
413
+ // SQLite exposes no live constraint names; fall back to
414
+ // the canonical name (unused there — emission warns).
415
+ old_name: live
416
+ .name
417
+ .clone()
418
+ .unwrap_or_else(|| fk_name(table, &fk.column, &live.to_table)),
419
+ });
420
+ }
421
+ }
422
+ }
423
+ }
424
+
323
425
  #[cfg(test)]
324
426
  mod tests;