ferro-orm 0.17.1__tar.gz → 0.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (386) hide show
  1. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/AGENTS.md +10 -0
  2. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/CHANGELOG.md +32 -0
  3. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/CONTEXT.md +10 -2
  4. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/Cargo.lock +41 -51
  5. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/Cargo.toml +1 -1
  6. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/PKG-INFO +1 -1
  7. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-ddl-lowering/src/lib.rs +106 -0
  8. ferro_orm-0.18.0/docs/adr/0011-label-addition-update-gated-append-only.md +65 -0
  9. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/migrations.md +71 -0
  10. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/pyproject.toml +1 -1
  11. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/_core.pyi +12 -0
  12. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/migrations/alembic.py +138 -1
  13. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/introspect.rs +29 -0
  14. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/lib.rs +1 -0
  15. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/migrate.rs +73 -3
  16. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/naming_ffi.rs +23 -0
  17. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_cross_emitter_parity.py +20 -0
  18. ferro_orm-0.18.0/tests/test_label_addition.py +380 -0
  19. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  20. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  21. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/PERMISSIONS.md +0 -0
  22. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/PYPI_CHECKLIST.md +0 -0
  23. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/PYPI_SETUP.md +0 -0
  24. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/generated/wheels.generated.yml +0 -0
  25. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/pull_request_template.md +0 -0
  26. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/workflows/ci.yml +0 -0
  27. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/workflows/packaging-smoke.yml +0 -0
  28. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/workflows/publish-docs.yml +0 -0
  29. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/workflows/publish.yml +0 -0
  30. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/workflows/release.yml +0 -0
  31. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.gitignore +0 -0
  32. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.pre-commit-config.yaml +0 -0
  33. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.python-version +0 -0
  34. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/CONTRIBUTING.md +0 -0
  35. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/LICENSE +0 -0
  36. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/README.md +0 -0
  37. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/README.md +0 -0
  38. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/__init__.py +0 -0
  39. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/__main__.py +0 -0
  40. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/backends.py +0 -0
  41. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/baselines/postgres.json +0 -0
  42. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/baselines/sqlite.json +0 -0
  43. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/compare.py +0 -0
  44. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/harness.py +0 -0
  45. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/model.py +0 -0
  46. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/run.py +0 -0
  47. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
  48. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-migrate/Cargo.toml +0 -0
  49. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-migrate/src/emit.rs +0 -0
  50. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-migrate/src/lib.rs +0 -0
  51. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-migrate/src/order.rs +0 -0
  52. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-migrate/src/tests.rs +0 -0
  53. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-schema-ir/Cargo.toml +0 -0
  54. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-schema-ir/src/lib.rs +0 -0
  55. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0001-resolved-epoch-registration.md +0 -0
  56. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
  57. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
  58. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
  59. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
  60. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
  61. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
  62. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0007-reverse-relations-tested-not-traversed.md +0 -0
  63. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0008-populated-relations-include.md +0 -0
  64. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0008-predicate-negation-uniform-not.md +0 -0
  65. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
  66. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0010-create-pass-owns-only-missing-tables.md +0 -0
  67. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/agents/domain.md +0 -0
  68. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/agents/issue-tracker.md +0 -0
  69. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/agents/triage-labels.md +0 -0
  70. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/architecture-review-2026-07-08.html +0 -0
  71. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
  72. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
  73. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
  74. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
  75. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
  76. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
  77. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
  78. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
  79. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-26-ir-p8.5-143-db-type-drop-requirements.md +0 -0
  80. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
  81. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-28-ir-p8.5-141-single-schema-ir-producer-requirements.md +0 -0
  82. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-29-ir-p8.6-146-dialect-enum-unification-requirements.md +0 -0
  83. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-29-ir-p8.6-153-create-path-ir-requirements.md +0 -0
  84. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
  85. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
  86. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-07-01-ir-p8.6-154-datetime-tz-coarseness-requirements.md +0 -0
  87. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-07-01-ir-p8.6-162-typed-save-bind-requirements.md +0 -0
  88. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-07-01-ir-p8.6-165-blob-introspection-false-positive-requirements.md +0 -0
  89. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
  90. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
  91. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/aggregations.py +0 -0
  92. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/aggregations_annotated.py +0 -0
  93. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/existence_tests.py +0 -0
  94. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/existence_tests_annotated.py +0 -0
  95. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/multiple_databases.py +0 -0
  96. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/mutations.py +0 -0
  97. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/pagination.py +0 -0
  98. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/partial_selects.py +0 -0
  99. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/partial_selects_annotated.py +0 -0
  100. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/populated_relations.py +0 -0
  101. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/populated_relations_annotated.py +0 -0
  102. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/predicates.py +0 -0
  103. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/predicates_annotated.py +0 -0
  104. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/quickstart.py +0 -0
  105. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/quickstart_annotated.py +0 -0
  106. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/raw_sql.py +0 -0
  107. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/relationships.py +0 -0
  108. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/relationships_annotated.py +0 -0
  109. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/soft_deletes.py +0 -0
  110. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/soft_deletes_annotated.py +0 -0
  111. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/testing_conftest.py +0 -0
  112. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/timestamps.py +0 -0
  113. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/timestamps_annotated.py +0 -0
  114. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/transactions.py +0 -0
  115. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/traversal.py +0 -0
  116. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/traversal_annotated.py +0 -0
  117. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/connection.md +0 -0
  118. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/exceptions.md +0 -0
  119. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/fields.md +0 -0
  120. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/migrations.md +0 -0
  121. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/model.md +0 -0
  122. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/queries.md +0 -0
  123. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/raw-sql.md +0 -0
  124. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/relationships.md +0 -0
  125. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/transactions.md +0 -0
  126. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/changelog.md +0 -0
  127. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/architecture.md +0 -0
  128. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/backends.md +0 -0
  129. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/identity-map.md +0 -0
  130. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/performance.md +0 -0
  131. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/query-typing.md +0 -0
  132. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/type-safety.md +0 -0
  133. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/contributing.md +0 -0
  134. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/faq.md +0 -0
  135. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/getting-started/installation.md +0 -0
  136. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/getting-started/next-steps.md +0 -0
  137. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/getting-started/quickstart.md +0 -0
  138. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/aggregations.md +0 -0
  139. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/connections.md +0 -0
  140. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/models-and-fields.md +0 -0
  141. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/mutations.md +0 -0
  142. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/queries.md +0 -0
  143. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/raw-sql.md +0 -0
  144. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/relationships.md +0 -0
  145. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/transactions.md +0 -0
  146. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
  147. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/multiple-databases.md +0 -0
  148. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/pagination.md +0 -0
  149. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/soft-deletes.md +0 -0
  150. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/testing.md +0 -0
  151. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/timestamps.md +0 -0
  152. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/upgrade-guide.md +0 -0
  153. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/index.md +0 -0
  154. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/roadmap.md +0 -0
  155. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/stylesheets/extra.css +0 -0
  156. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/why-ferro.md +0 -0
  157. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
  158. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
  159. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
  160. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
  161. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
  162. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
  163. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
  164. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
  165. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-24-001-feat-ir-p8-119-wire-automigrate-plan.md +0 -0
  166. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-25-001-feat-ir-p8-120-parity-gate-plan.md +0 -0
  167. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-26-001-feat-ir-p8.5-140-shared-lowering-plan.md +0 -0
  168. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-26-002-feat-ir-p8.5-143-db-type-drop-plan.md +0 -0
  169. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-26-003-feat-ir-p8.5-144-reconcile-indexes-plan.md +0 -0
  170. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-28-001-feat-ir-p8.5-141-single-schema-ir-producer-plan.md +0 -0
  171. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-29-001-refactor-ir-p8.6-153-create-path-unification-plan.md +0 -0
  172. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-29-002-refactor-ir-p8.6-146-dialect-enum-unification-plan.md +0 -0
  173. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-30-001-refactor-ir-p8.6-158-check-renderer-plan.md +0 -0
  174. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-30-002-fix-ir-p8.6-155-deferred-annotations-plan.md +0 -0
  175. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-01-001-feat-ir-p8.6-162-typed-save-bind-plan.md +0 -0
  176. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-01-002-fix-ir-p8.6-154-datetime-tz-coarseness-plan.md +0 -0
  177. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-01-003-fix-ir-p8.6-165-blob-introspection-plan.md +0 -0
  178. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
  179. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-02-002-feat-ff-a-172-typed-exceptions-plan.md +0 -0
  180. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
  181. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +0 -0
  182. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
  183. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
  184. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
  185. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
  186. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-009-ff-f-query-builder-1.0-design.md +0 -0
  187. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-010-ff-f-query-builder-1.0-plan.md +0 -0
  188. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
  189. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
  190. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
  191. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
  192. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
  193. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-18-016-existence-tests-design.md +0 -0
  194. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/ir-first-migration-guide.md +0 -0
  195. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/ir-first-release-checklist.md +0 -0
  196. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/rfc/ir-contracts-v1.md +0 -0
  197. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/README.md +0 -0
  198. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
  199. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
  200. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
  201. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
  202. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
  203. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
  204. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/self-fk-evicts-component-from-create-order.md +0 -0
  205. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
  206. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
  207. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
  208. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
  209. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
  210. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
  211. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
  212. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/foreign-key-index.md +0 -0
  213. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
  214. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/ir-invariants.md +0 -0
  215. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/persistence-state.md +0 -0
  216. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
  217. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
  218. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/typed-null-binds.md +0 -0
  219. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/justfile +0 -0
  220. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/scripts/demo_queries.py +0 -0
  221. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/scripts/demo_queries_pre_v012.py +0 -0
  222. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/backend.rs +0 -0
  223. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/codec.rs +0 -0
  224. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/codec_plan.rs +0 -0
  225. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/connection.rs +0 -0
  226. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/errors.rs +0 -0
  227. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/__init__.py +0 -0
  228. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/_annotation_utils.py +0 -0
  229. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/_bind_payload.py +0 -0
  230. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/_deprecations.py +0 -0
  231. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/_shadow_fk_types.py +0 -0
  232. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/base.py +0 -0
  233. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/columns.py +0 -0
  234. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/composite_indexes.py +0 -0
  235. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/composite_uniques.py +0 -0
  236. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/exceptions.py +0 -0
  237. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/fields.py +0 -0
  238. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/ir/__init__.py +0 -0
  239. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/ir/compiler.py +0 -0
  240. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/metaclass.py +0 -0
  241. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/migrations/__init__.py +0 -0
  242. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/models.py +0 -0
  243. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/py.typed +0 -0
  244. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/query/__init__.py +0 -0
  245. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/query/builder.py +0 -0
  246. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/query/nodes.py +0 -0
  247. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/query/rows.py +0 -0
  248. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/query/wire.py +0 -0
  249. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/raw.py +0 -0
  250. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/registry.py +0 -0
  251. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/relations/__init__.py +0 -0
  252. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/relations/descriptors.py +0 -0
  253. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/session.py +0 -0
  254. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/state.py +0 -0
  255. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/hydration.rs +0 -0
  256. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/operations.rs +0 -0
  257. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/query.rs +0 -0
  258. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/schema.rs +0 -0
  259. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/schema_bind.rs +0 -0
  260. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/state.rs +0 -0
  261. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/__init__.py +0 -0
  262. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/conftest.py +0 -0
  263. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/db_backends.py +0 -0
  264. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/README.md +0 -0
  265. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
  266. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_account_exists_v7.json +0 -0
  267. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_account_scoped_exists_v7.json +0 -0
  268. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_owner_nested_exists_v7.json +0 -0
  269. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_owner_not_exists_v7.json +0 -0
  270. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_aggregate_v7.json +0 -0
  271. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v7.json +0 -0
  272. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_include_v7.json +0 -0
  273. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_left_join_v7.json +0 -0
  274. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_record_v7.json +0 -0
  275. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_traversal_v7.json +0 -0
  276. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_traversed_record_v7.json +0 -0
  277. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_user_compound_v7.json +0 -0
  278. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_user_m2m_exists_v7.json +0 -0
  279. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_user_not_compound_v7.json +0 -0
  280. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_user_not_leaf_v7.json +0 -0
  281. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
  282. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
  283. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
  284. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/bad_includes.py +0 -0
  285. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/bad_predicates.py +0 -0
  286. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/bad_projections.py +0 -0
  287. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/good_includes.py +0 -0
  288. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/good_predicates.py +0 -0
  289. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/good_projections.py +0 -0
  290. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_aggregation.py +0 -0
  291. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_alembic_autogenerate.py +0 -0
  292. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_alembic_bridge.py +0 -0
  293. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_alembic_db_type.py +0 -0
  294. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_alembic_nullability.py +0 -0
  295. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_alembic_type_mapping.py +0 -0
  296. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_annotation_utils.py +0 -0
  297. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_auto_migrate.py +0 -0
  298. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_bulk_create_chunking.py +0 -0
  299. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_bulk_install.py +0 -0
  300. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_bulk_update.py +0 -0
  301. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_catalog_cache.py +0 -0
  302. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_codec_plan.py +0 -0
  303. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_column_specs.py +0 -0
  304. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_compiled_query.py +0 -0
  305. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_composite_index.py +0 -0
  306. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_composite_unique.py +0 -0
  307. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_connection.py +0 -0
  308. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_connection_redaction.py +0 -0
  309. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_constraints.py +0 -0
  310. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_crud.py +0 -0
  311. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_db_backends.py +0 -0
  312. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_db_type_cross_emitter_parity.py +0 -0
  313. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_db_type_integration.py +0 -0
  314. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_db_type_typing.py +0 -0
  315. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_db_type_validation.py +0 -0
  316. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_deletion.py +0 -0
  317. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_deprecations.py +0 -0
  318. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_docs_examples.py +0 -0
  319. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_documentation_features.py +0 -0
  320. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_enum_cold_hydration.py +0 -0
  321. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_exception_mapping.py +0 -0
  322. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_exceptions.py +0 -0
  323. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_field_wrapper.py +0 -0
  324. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_fk_target_pk.py +0 -0
  325. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_generation_counter_dirty_tracking.py +0 -0
  326. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_global_aggregates.py +0 -0
  327. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_grouped_aggregates.py +0 -0
  328. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_helpers.py +0 -0
  329. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_hydration.py +0 -0
  330. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_hydration_equivalence.py +0 -0
  331. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_identity_memory.py +0 -0
  332. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_identity_refresh.py +0 -0
  333. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_identity_scoped_invalidation.py +0 -0
  334. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_identity_weakref.py +0 -0
  335. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_import_budget.py +0 -0
  336. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_include.py +0 -0
  337. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_ir_vectors_contract.py +0 -0
  338. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_metaclass_internals.py +0 -0
  339. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_metadata.py +0 -0
  340. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_migrate_plan.py +0 -0
  341. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_model_identity.py +0 -0
  342. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_models.py +0 -0
  343. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_mutation_pagination_guard.py +0 -0
  344. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_named_connections_integration.py +0 -0
  345. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_naming_single_source.py +0 -0
  346. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_negation.py +0 -0
  347. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_one_to_one.py +0 -0
  348. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_operation_seam_sync.py +0 -0
  349. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_partial_selects.py +0 -0
  350. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_primary_key_facts.py +0 -0
  351. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_provisional_import.py +0 -0
  352. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_builder.py +0 -0
  353. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_column_validation.py +0 -0
  354. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_exists.py +0 -0
  355. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_immutability.py +0 -0
  356. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_joins.py +0 -0
  357. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_typing.py +0 -0
  358. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_wire_vectors.py +0 -0
  359. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_raw_sql.py +0 -0
  360. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_refresh.py +0 -0
  361. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_registry_entrypoints.py +0 -0
  362. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_registry_owner.py +0 -0
  363. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_relation_specs.py +0 -0
  364. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_relationship_engine.py +0 -0
  365. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_relationship_resolution_errors.py +0 -0
  366. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_route_single_site.py +0 -0
  367. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_routing_errors.py +0 -0
  368. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_save_pk_edges.py +0 -0
  369. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_save_semantics.py +0 -0
  370. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_schema.py +0 -0
  371. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_schema_constraints.py +0 -0
  372. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_schema_db_type_metadata.py +0 -0
  373. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_schema_enum_annotations.py +0 -0
  374. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_session.py +0 -0
  375. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_shadow_fk_types.py +0 -0
  376. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
  377. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_static_contracts.py +0 -0
  378. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_string_search.py +0 -0
  379. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_structural_types.py +0 -0
  380. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_temporal_types.py +0 -0
  381. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_transactions.py +0 -0
  382. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_traversed_projection.py +0 -0
  383. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_typed_null_binds.py +0 -0
  384. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_typed_save_bind.py +0 -0
  385. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/uv.lock +0 -0
  386. {ferro_orm-0.17.1 → ferro_orm-0.18.0}/zensical.toml +0 -0
@@ -50,6 +50,16 @@ For a single model, every emitter must agree on:
50
50
  and `db_check_constraint_name` (Rust).
51
51
  9. **Default values** — server-side defaults must serialize identically.
52
52
  10. **Nullability** — must agree.
53
+ 11. **Enum label additions** — the label-addition decision (which model-declared
54
+ labels a live enum type is missing, and which live labels are extra) and
55
+ the rendered `ALTER TYPE ... ADD VALUE IF NOT EXISTS` statement are decided
56
+ by ONE pair of functions: `ferro_ddl_lowering::missing_enum_labels` /
57
+ `extra_enum_labels` + `render_pg_enum_add_value`. The auto-migrate
58
+ reconciliation pass consumes them directly; the Alembic autogenerate
59
+ comparator consumes them over FFI (`_core._plan_enum_label_addition`) and
60
+ executes the byte-identical statements. Pinned by
61
+ `tests/test_cross_emitter_parity.py` and the ferro-ddl-lowering unit pins.
62
+ See ADR-0011 (append-only; update-gated; warn-never-act for extras).
53
63
 
54
64
  ### Why this invariant exists
55
65
 
@@ -1,6 +1,38 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.18.0 (2026-08-05)
5
+
6
+ ### Documentation
7
+
8
+ - **adr**: ADR-0011 label addition + glossary terms (enum label, label addition)
9
+ ([`449616f`](https://github.com/syn54x/ferro-orm/commit/449616f07aefe473fe098f45f6873eb2003cf8e5))
10
+
11
+ - **migrate**: Label addition — the auto_migrate trap, the migrate_updates contract, the ordering
12
+ caveat ([#334](https://github.com/syn54x/ferro-orm/pull/334),
13
+ [`482911b`](https://github.com/syn54x/ferro-orm/commit/482911b441eeeed21b13d3927c9492797bb1eba5))
14
+
15
+ ### Features
16
+
17
+ - **alembic**: Autogenerate comparator emits label additions from the shared diff
18
+ ([#333](https://github.com/syn54x/ferro-orm/pull/333),
19
+ [`1579fc3`](https://github.com/syn54x/ferro-orm/commit/1579fc38a7f0d2b94c4ea51aa7daac652df30abc))
20
+
21
+ - **migrate**: Label addition tracer — migrate_updates appends missing enum labels
22
+ ([#330](https://github.com/syn54x/ferro-orm/pull/330),
23
+ [`e929a6a`](https://github.com/syn54x/ferro-orm/commit/e929a6ae854fd435c6aac3277e476c15b106f73a))
24
+
25
+ - **migrate**: Warn-never-act for extra enum labels; create pass stays silent
26
+ ([#331](https://github.com/syn54x/ferro-orm/pull/331),
27
+ [`a73d142`](https://github.com/syn54x/ferro-orm/commit/a73d142e2ff22ef8e47c2a8b8c34ca92372d0487))
28
+
29
+ ### Testing
30
+
31
+ - **migrate**: Label addition edges — shared types, default-in-same-run, ordering, idempotence
32
+ ([#332](https://github.com/syn54x/ferro-orm/pull/332),
33
+ [`1215f0c`](https://github.com/syn54x/ferro-orm/commit/1215f0c0e52367eafd91411b7b7891220edf318e))
34
+
35
+
4
36
  ## v0.17.1 (2026-07-19)
5
37
 
6
38
  ### Bug Fixes
@@ -129,13 +129,21 @@ The auto-migrate step that brings missing tables into existence — the table, i
129
129
  _Avoid_: Bootstrap, ensure-tables, table sync
130
130
 
131
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.
132
+ The `migrate_updates` step that alters existing schema objects — tables and ferro-owned enum types — to match the registered models; the only authority for DDL against an object that already exists. Within one table, column changes land before the indexes and constraints that reference them; label additions land before any table's changes.
133
133
  _Avoid_: Update pass, schema sync, drift repair
134
134
 
135
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.
136
+ A schema object ferro may reconcile to match the declared model. Indexes and constraints are ferro-owned by naming (`idx_`, `uq_`, `fk_`, `ck_`); native enum types are ferro-owned by derivation — the type's name matches the name ferro derives from the model. Artifacts owned neither way belong to the user and are never altered or dropped.
137
137
  _Avoid_: Managed index, system constraint, internal index
138
138
 
139
+ **Enum label**:
140
+ One storable value of a native Postgres enum type, mirrored from a Python `StrEnum` member's value. Members are the Python-side declaration; labels are what the database accepts and stores.
141
+ _Avoid_: Enum value, variant, choice
142
+
143
+ **Label addition**:
144
+ The reconciliation-pass operation appending model-declared labels missing from a live ferro-owned enum type. Append-only and metadata-only: rows are never touched, and labels the database has but the model lacks are warned about loudly and never removed — removal and rename are reviewed-migration territory.
145
+ _Avoid_: Enum sync, label reconciliation, enum evolution
146
+
139
147
  **Constraint rebuild**:
140
148
  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
149
  _Avoid_: Constraint alter, FK patch, in-place constraint update
@@ -10,9 +10,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
10
10
 
11
11
  [[package]]
12
12
  name = "android_system_properties"
13
- version = "0.1.5"
13
+ version = "0.1.6"
14
14
  source = "registry+https://github.com/rust-lang/crates.io-index"
15
- checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
15
+ checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
16
16
  dependencies = [
17
17
  "libc",
18
18
  ]
@@ -95,9 +95,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
95
95
 
96
96
  [[package]]
97
97
  name = "cc"
98
- version = "1.3.0"
98
+ version = "1.4.0"
99
99
  source = "registry+https://github.com/rust-lang/crates.io-index"
100
- checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
100
+ checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
101
101
  dependencies = [
102
102
  "find-msvc-tools",
103
103
  "shlex",
@@ -120,15 +120,6 @@ dependencies = [
120
120
  "windows-link",
121
121
  ]
122
122
 
123
- [[package]]
124
- name = "concurrent-queue"
125
- version = "2.5.0"
126
- source = "registry+https://github.com/rust-lang/crates.io-index"
127
- checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
128
- dependencies = [
129
- "crossbeam-utils",
130
- ]
131
-
132
123
  [[package]]
133
124
  name = "const-oid"
134
125
  version = "0.9.6"
@@ -263,13 +254,13 @@ dependencies = [
263
254
 
264
255
  [[package]]
265
256
  name = "displaydoc"
266
- version = "0.2.6"
257
+ version = "0.2.7"
267
258
  source = "registry+https://github.com/rust-lang/crates.io-index"
268
- checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
259
+ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
269
260
  dependencies = [
270
261
  "proc-macro2",
271
262
  "quote",
272
- "syn 2.0.119",
263
+ "syn 3.0.3",
273
264
  ]
274
265
 
275
266
  [[package]]
@@ -280,9 +271,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
280
271
 
281
272
  [[package]]
282
273
  name = "either"
283
- version = "1.16.0"
274
+ version = "1.17.0"
284
275
  source = "registry+https://github.com/rust-lang/crates.io-index"
285
- checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
276
+ checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
286
277
  dependencies = [
287
278
  "serde",
288
279
  ]
@@ -316,18 +307,17 @@ dependencies = [
316
307
 
317
308
  [[package]]
318
309
  name = "event-listener"
319
- version = "5.4.1"
310
+ version = "5.4.2"
320
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
321
- checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
312
+ checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
322
313
  dependencies = [
323
- "concurrent-queue",
324
314
  "parking",
325
315
  "pin-project-lite",
326
316
  ]
327
317
 
328
318
  [[package]]
329
319
  name = "ferro"
330
- version = "0.17.1"
320
+ version = "0.18.0"
331
321
  dependencies = [
332
322
  "chrono",
333
323
  "dashmap",
@@ -778,7 +768,7 @@ checksum = "bee2c455ca60511a054699102d40ce7153621cb2429558f9eaa600e4499b5984"
778
768
  dependencies = [
779
769
  "proc-macro2",
780
770
  "quote",
781
- "syn 3.0.0",
771
+ "syn 3.0.3",
782
772
  ]
783
773
 
784
774
  [[package]]
@@ -809,9 +799,9 @@ dependencies = [
809
799
 
810
800
  [[package]]
811
801
  name = "libc"
812
- version = "0.2.186"
802
+ version = "0.2.189"
813
803
  source = "registry+https://github.com/rust-lang/crates.io-index"
814
- checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
804
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
815
805
 
816
806
  [[package]]
817
807
  name = "libm"
@@ -821,14 +811,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
821
811
 
822
812
  [[package]]
823
813
  name = "libredox"
824
- version = "0.1.18"
814
+ version = "0.1.19"
825
815
  source = "registry+https://github.com/rust-lang/crates.io-index"
826
- checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
816
+ checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa"
827
817
  dependencies = [
828
818
  "bitflags",
829
819
  "libc",
830
820
  "plain",
831
- "redox_syscall 0.9.0",
821
+ "redox_syscall 0.9.1",
832
822
  ]
833
823
 
834
824
  [[package]]
@@ -1206,9 +1196,9 @@ dependencies = [
1206
1196
 
1207
1197
  [[package]]
1208
1198
  name = "redox_syscall"
1209
- version = "0.9.0"
1199
+ version = "0.9.1"
1210
1200
  source = "registry+https://github.com/rust-lang/crates.io-index"
1211
- checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759"
1201
+ checksum = "07507be7b4a5f9f26eeb41eeaebb1f5a7ff29dfb29739facc21d35bf8b11c21e"
1212
1202
  dependencies = [
1213
1203
  "bitflags",
1214
1204
  ]
@@ -1249,9 +1239,9 @@ dependencies = [
1249
1239
 
1250
1240
  [[package]]
1251
1241
  name = "rustls"
1252
- version = "0.23.42"
1242
+ version = "0.23.43"
1253
1243
  source = "registry+https://github.com/rust-lang/crates.io-index"
1254
- checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
1244
+ checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
1255
1245
  dependencies = [
1256
1246
  "once_cell",
1257
1247
  "ring",
@@ -1263,9 +1253,9 @@ dependencies = [
1263
1253
 
1264
1254
  [[package]]
1265
1255
  name = "rustls-pki-types"
1266
- version = "1.15.0"
1256
+ version = "1.15.1"
1267
1257
  source = "registry+https://github.com/rust-lang/crates.io-index"
1268
- checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
1258
+ checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
1269
1259
  dependencies = [
1270
1260
  "zeroize",
1271
1261
  ]
@@ -1351,14 +1341,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
1351
1341
  dependencies = [
1352
1342
  "proc-macro2",
1353
1343
  "quote",
1354
- "syn 3.0.0",
1344
+ "syn 3.0.3",
1355
1345
  ]
1356
1346
 
1357
1347
  [[package]]
1358
1348
  name = "serde_json"
1359
- version = "1.0.150"
1349
+ version = "1.0.151"
1360
1350
  source = "registry+https://github.com/rust-lang/crates.io-index"
1361
- checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
1351
+ checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
1362
1352
  dependencies = [
1363
1353
  "itoa",
1364
1354
  "memchr",
@@ -1709,9 +1699,9 @@ dependencies = [
1709
1699
 
1710
1700
  [[package]]
1711
1701
  name = "syn"
1712
- version = "3.0.0"
1702
+ version = "3.0.3"
1713
1703
  source = "registry+https://github.com/rust-lang/crates.io-index"
1714
- checksum = "f2fac314a64dc9a36e61a9eb4261a5e9bbfbc922b27e518af97bc32b926cf967"
1704
+ checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
1715
1705
  dependencies = [
1716
1706
  "proc-macro2",
1717
1707
  "quote",
@@ -1752,7 +1742,7 @@ checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
1752
1742
  dependencies = [
1753
1743
  "proc-macro2",
1754
1744
  "quote",
1755
- "syn 3.0.0",
1745
+ "syn 3.0.3",
1756
1746
  ]
1757
1747
 
1758
1748
  [[package]]
@@ -1782,9 +1772,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
1782
1772
 
1783
1773
  [[package]]
1784
1774
  name = "tokio"
1785
- version = "1.53.0"
1775
+ version = "1.53.1"
1786
1776
  source = "registry+https://github.com/rust-lang/crates.io-index"
1787
- checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
1777
+ checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
1788
1778
  dependencies = [
1789
1779
  "bytes",
1790
1780
  "libc",
@@ -1799,20 +1789,20 @@ dependencies = [
1799
1789
 
1800
1790
  [[package]]
1801
1791
  name = "tokio-macros"
1802
- version = "2.7.1"
1792
+ version = "2.7.2"
1803
1793
  source = "registry+https://github.com/rust-lang/crates.io-index"
1804
- checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
1794
+ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
1805
1795
  dependencies = [
1806
1796
  "proc-macro2",
1807
1797
  "quote",
1808
- "syn 2.0.119",
1798
+ "syn 3.0.3",
1809
1799
  ]
1810
1800
 
1811
1801
  [[package]]
1812
1802
  name = "tokio-stream"
1813
- version = "0.1.18"
1803
+ version = "0.1.19"
1814
1804
  source = "registry+https://github.com/rust-lang/crates.io-index"
1815
- checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
1805
+ checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
1816
1806
  dependencies = [
1817
1807
  "futures-core",
1818
1808
  "pin-project-lite",
@@ -2260,18 +2250,18 @@ dependencies = [
2260
2250
 
2261
2251
  [[package]]
2262
2252
  name = "zerocopy"
2263
- version = "0.8.54"
2253
+ version = "0.8.55"
2264
2254
  source = "registry+https://github.com/rust-lang/crates.io-index"
2265
- checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
2255
+ checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
2266
2256
  dependencies = [
2267
2257
  "zerocopy-derive",
2268
2258
  ]
2269
2259
 
2270
2260
  [[package]]
2271
2261
  name = "zerocopy-derive"
2272
- version = "0.8.54"
2262
+ version = "0.8.55"
2273
2263
  source = "registry+https://github.com/rust-lang/crates.io-index"
2274
- checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
2264
+ checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
2275
2265
  dependencies = [
2276
2266
  "proc-macro2",
2277
2267
  "quote",
@@ -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.1"
6
+ version = "0.18.0"
7
7
  edition = "2024"
8
8
  readme = "README.md"
9
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ferro-orm
3
- Version: 0.17.1
3
+ Version: 0.18.0
4
4
  Requires-Dist: pydantic>=2.0
5
5
  Requires-Dist: alembic>=1.18.1 ; extra == 'alembic'
6
6
  Requires-Dist: sqlalchemy>=2.0.46 ; extra == 'alembic'
@@ -498,6 +498,63 @@ pub fn render_pg_enum_create_type(type_name: &str, labels: &[String]) -> String
498
498
  )
499
499
  }
500
500
 
501
+ /// The label-addition decision (ADR-0011): which model-declared labels a live
502
+ /// ferro-owned enum type is missing, in declared order. This is the single
503
+ /// decision table for enum label drift — the auto-migrate planner and the
504
+ /// Alembic autogenerate comparator both consume it mechanically (AGENTS.md
505
+ /// § I-1); neither side re-derives it. Append-only by construction: labels the
506
+ /// live type has but the model lacks are not this function's concern.
507
+ pub fn missing_enum_labels(declared: &[String], live: &[String]) -> Vec<String> {
508
+ declared
509
+ .iter()
510
+ .filter(|label| !live.contains(label))
511
+ .cloned()
512
+ .collect()
513
+ }
514
+
515
+ /// The warn-never-act half of the label-addition decision (ADR-0011): labels
516
+ /// the live type carries that the model no longer declares, in live (enum
517
+ /// sort) order. Rows may still hold these labels and older code may still be
518
+ /// running against the schema, so callers warn loudly and never remove —
519
+ /// removal and rename are reviewed-migration territory.
520
+ pub fn extra_enum_labels(declared: &[String], live: &[String]) -> Vec<String> {
521
+ live.iter()
522
+ .filter(|label| !declared.contains(label))
523
+ .cloned()
524
+ .collect()
525
+ }
526
+
527
+ /// The warn-never-act message for one drifted enum type, or `None` when
528
+ /// nothing is extra. Single-sourced like [`refused_conversion_warning`]:
529
+ /// callers emit it verbatim, never re-derive the wording.
530
+ pub fn extra_enum_labels_warning(type_name: &str, extra: &[String]) -> Option<String> {
531
+ if extra.is_empty() {
532
+ return None;
533
+ }
534
+ let listed: Vec<String> = extra.iter().map(|l| format!("'{l}'")).collect();
535
+ Some(format!(
536
+ "Enum type '{}' has label(s) {} that the model no longer declares. \
537
+ Label addition is append-only: ferro never removes enum labels \
538
+ (existing rows may still hold them). Remove or rename labels with \
539
+ a reviewed Alembic migration.",
540
+ type_name,
541
+ listed.join(", ")
542
+ ))
543
+ }
544
+
545
+ /// One `ALTER TYPE ... ADD VALUE IF NOT EXISTS` for a label addition.
546
+ /// `IF NOT EXISTS` makes concurrent boots and shared-type replans harmless;
547
+ /// executed outside transactions (autocommit) so the statement is legal on
548
+ /// every supported Postgres version and the label is committed before any
549
+ /// table plan that references it.
550
+ pub fn render_pg_enum_add_value(type_name: &str, label: &str) -> String {
551
+ format!(
552
+ "ALTER TYPE {} ADD VALUE IF NOT EXISTS '{}'",
553
+ quote_ident(type_name),
554
+ label.replace('\'', "''"),
555
+ )
556
+ }
557
+
501
558
  /// Detect a refused conversion from a live column to a resolved storage
502
559
  /// target. Extends [`refused_scalar_conversion`] with the native-enum case:
503
560
  /// a live non-enum (varchar/text) column targeted at a native Postgres enum
@@ -1464,6 +1521,55 @@ mod tests {
1464
1521
  );
1465
1522
  }
1466
1523
 
1524
+ #[test]
1525
+ fn missing_enum_labels_returns_additions_in_declared_order() {
1526
+ let declared = vec!["plaid".to_string(), "mx".to_string(), "teller".to_string()];
1527
+ let live = vec!["plaid".to_string()];
1528
+ assert_eq!(missing_enum_labels(&declared, &live), vec!["mx", "teller"]);
1529
+ }
1530
+
1531
+ #[test]
1532
+ fn missing_enum_labels_empty_when_live_covers_declared() {
1533
+ let declared = vec!["plaid".to_string()];
1534
+ let live = vec!["plaid".to_string(), "legacy".to_string()];
1535
+ assert!(missing_enum_labels(&declared, &live).is_empty());
1536
+ }
1537
+
1538
+ #[test]
1539
+ fn extra_enum_labels_returns_undeclared_live_labels_in_live_order() {
1540
+ let declared = vec!["plaid".to_string()];
1541
+ let live = vec!["legacy".to_string(), "plaid".to_string(), "old".to_string()];
1542
+ assert_eq!(extra_enum_labels(&declared, &live), vec!["legacy", "old"]);
1543
+ assert!(extra_enum_labels(&live, &live).is_empty());
1544
+ }
1545
+
1546
+ #[test]
1547
+ fn extra_enum_labels_warning_is_pinned_and_names_the_exit() {
1548
+ assert_eq!(
1549
+ extra_enum_labels_warning("provider", &["legacy".to_string()]),
1550
+ Some(
1551
+ "Enum type 'provider' has label(s) 'legacy' that the model no longer \
1552
+ declares. Label addition is append-only: ferro never removes enum \
1553
+ labels (existing rows may still hold them). Remove or rename labels \
1554
+ with a reviewed Alembic migration."
1555
+ .to_string()
1556
+ )
1557
+ );
1558
+ assert_eq!(extra_enum_labels_warning("provider", &[]), None);
1559
+ }
1560
+
1561
+ #[test]
1562
+ fn render_pg_enum_add_value_is_pinned_and_escapes() {
1563
+ assert_eq!(
1564
+ render_pg_enum_add_value("provider", "mx"),
1565
+ "ALTER TYPE \"provider\" ADD VALUE IF NOT EXISTS 'mx'"
1566
+ );
1567
+ assert_eq!(
1568
+ render_pg_enum_add_value("od'd", "it's"),
1569
+ "ALTER TYPE \"od'd\" ADD VALUE IF NOT EXISTS 'it''s'"
1570
+ );
1571
+ }
1572
+
1467
1573
  #[test]
1468
1574
  fn render_pg_enum_create_type_escapes_quotes() {
1469
1575
  let sql = render_pg_enum_create_type("od'd", &["it's".to_string()]);
@@ -0,0 +1,65 @@
1
+ # Label addition is update-gated and append-only
2
+
3
+ A native Postgres enum type that already exists is an existing schema object,
4
+ so evolving its label set belongs to the **reconciliation pass**
5
+ (`migrate_updates`), not the create pass — the same line ADR-0010 drew for
6
+ indexes. The reconciliation pass performs **label addition** (`CONTEXT.md`):
7
+ it appends model-declared labels missing from a live ferro-owned enum type,
8
+ and does nothing else. Labels the database has but the model lacks are warned
9
+ about loudly and never removed — rows may still hold them, and old code may
10
+ still be running against the new schema during a rolling deploy. A type is
11
+ ferro-owned by **derivation**: its name matches the name ferro derives from
12
+ the model (enum types carry no `idx_`/`uq_`-style prefix, so prefix doctrine
13
+ cannot apply). The additive-only scope is what makes derivation-based
14
+ ownership safe: the worst misattribution appends a label; it never drops or
15
+ rewrites anything.
16
+
17
+ Both migration doors consume one decision: the label diff (model labels +
18
+ live `pg_enum` labels → additions + warnings) lives in the Rust core and is
19
+ consumed mechanically by the auto-migrate planner and by an Alembic
20
+ autogenerate comparator in the bridge — the same one-decision-table seam as
21
+ `resolve_column_storage` (AGENTS.md I-1). Alembic autogenerate has no
22
+ `migrate_updates` gate: running autogenerate is itself the request for a
23
+ diff, so the comparator always reports; parity is in the decision, not the
24
+ gate. `ALTER TYPE ... ADD VALUE` executes outside transactions on both paths
25
+ (an autocommit pre-pass before the per-table transactions in auto-migrate;
26
+ `autocommit_block()` in generated revisions) — Prisma shipped the
27
+ in-transaction version and it is a graveyard (prisma#7251, #5290, #8424).
28
+
29
+ Decision by owner (2026-08-05), grilling #328.
30
+
31
+ Rejected alternatives:
32
+
33
+ - **Ensure semantics under plain `auto_migrate`** ("the type guard already
34
+ runs in the create pass; label addition is just a stronger ensure"):
35
+ re-opens the two-owners divergence ADR-0010 closed — an existing object
36
+ whose shape plain `auto_migrate` sometimes changes. The create pass stays
37
+ introspection-free and silent about drift of every kind, enums included.
38
+ - **Automatic removal/rename** (full sync, as `alembic-postgresql-enum`
39
+ does): removal requires a type-replacement dance and human judgment about
40
+ rows holding the removed label; every implementation that automated it
41
+ grew a bug tracker around it. Reviewed-migration territory, permanently.
42
+ - **Recommending `alembic-postgresql-enum`** instead of our own comparator:
43
+ it autogenerates removals, contradicting append-only — the two doors would
44
+ disagree about what the same model means.
45
+ - **Ownership markers** (`COMMENT ON TYPE` stamped at create time): sound
46
+ provenance, but every existing deployment's types are unmarked, and the
47
+ backfill machinery buys nothing additive-only doesn't already guarantee.
48
+
49
+ ## Consequences
50
+
51
+ - Under plain `auto_migrate=True`, an evolved `StrEnum` still fails at first
52
+ use with the database's `invalid input value for enum` error — silently at
53
+ boot, by design. The documented answer to drift, enum drift included, is
54
+ `migrate_updates=True`. Auto-migrate docs must carry this trap loudly:
55
+ fresh schemas always get the complete label set, so no app-side test
56
+ against a throwaway schema can catch it (#328).
57
+ - A new table created under plain `auto_migrate` that references an existing
58
+ stale type gets the stale label set — the create-pass type guard only
59
+ fires for missing types. Same flag fixes it.
60
+ - Appended labels land at the end of the Postgres enum ordering regardless of
61
+ their position in the Python declaration; `ORDER BY` on an enum column
62
+ follows database order, not declaration order.
63
+ - Live introspection must read `pg_enum` labels (today it records only
64
+ "is an enum"), and the cross-emitter parity tests extend to pin the label
65
+ diff across both consumers.
@@ -48,6 +48,8 @@ What it covers is capability-relative per backend:
48
48
  | Change column type | ⚠️ `UserWarning`, no DDL (SQLite type affinity makes drift mostly cosmetic) | ✅ `ALTER COLUMN ... TYPE ... USING` cast |
49
49
  | Change nullability | ⚠️ `UserWarning`, no DDL | ✅ `SET NOT NULL` / `DROP NOT NULL` |
50
50
  | Drop orphaned Ferro-named index (`idx_*` / `uq_*`) | ✅ with `migrate_destructive=True` | ✅ with `migrate_destructive=True` |
51
+ | Add a missing enum label (a `StrEnum` grew a member) | ✅ nothing to do — enums store as text | ✅ `ALTER TYPE ... ADD VALUE` *0.18.0+* |
52
+ | Remove or rename an enum label | ✅ nothing to do | ⚠️ `UserWarning`, no DDL — Alembic territory |
51
53
  | Inline single-column `UNIQUE` on existing column, index option changes | ❌ never — Alembic territory | ❌ never |
52
54
  | Rename column/table, change primary key, drop table | ❌ never — Alembic territory | ❌ never |
53
55
 
@@ -59,6 +61,74 @@ Rules worth knowing:
59
61
  - **Postgres type changes take an exclusive lock** and fail the connect if existing data does not cast cleanly — fine for a development flag, but worth knowing.
60
62
  - **The pool refreshes after any schema change**, so no cached statement or stale identity-mapped instance can observe the pre-migration schema.
61
63
 
64
+ ### Evolving enums: label addition
65
+
66
+ *Added in 0.18.0.* On PostgreSQL, `StrEnum` fields create a **native enum type**, and a type that already exists in the database does not learn new members on its own. When a `StrEnum` grows, `migrate_updates=True` performs **label addition**: it compares the model's members against the live type and appends what's missing with `ALTER TYPE ... ADD VALUE IF NOT EXISTS`.
67
+
68
+ !!! danger "This gap is invisible to your tests"
69
+ Under plain `auto_migrate=True` (without `migrate_updates`), an existing enum type is **never** updated — like every existing object, it belongs to the update pass. The failure mode is nasty: every test suite that creates its schema fresh gets the complete enum and stays green, while every *existing* database rejects the new member at runtime with `invalid input value for enum`. No app-side test against a throwaway schema can catch this. If your models' enums evolve, run with `migrate_updates=True` (or generate the migration with Alembic — the [autogenerate bridge](#alembic-for-production) sees the same drift).
70
+
71
+ === "Assignment"
72
+
73
+ ```python
74
+ from enum import StrEnum
75
+
76
+ import ferro
77
+ from ferro import Model
78
+
79
+
80
+ class Provider(StrEnum):
81
+ PLAID = "plaid"
82
+ MX = "mx" # new member — the live type only has 'plaid'
83
+
84
+
85
+ class Feed(Model):
86
+ id: int | None = ferro.Field(primary_key=True, default=None)
87
+ provider: Provider
88
+
89
+
90
+ await ferro.connect("postgres://...", migrate_updates=True)
91
+ # → ALTER TYPE "provider" ADD VALUE IF NOT EXISTS 'mx'
92
+
93
+ feed = await Feed.create(provider=Provider.MX)
94
+ recent = await Feed.where(lambda feed: feed.provider == Provider.MX).all()
95
+ ```
96
+
97
+ === "Annotated"
98
+
99
+ ```python
100
+ from enum import StrEnum
101
+ from typing import Annotated
102
+
103
+ import ferro
104
+ from ferro import FerroField, Model
105
+
106
+
107
+ class Provider(StrEnum):
108
+ PLAID = "plaid"
109
+ MX = "mx" # new member — the live type only has 'plaid'
110
+
111
+
112
+ class Feed(Model):
113
+ id: Annotated[int | None, FerroField(primary_key=True)] = None
114
+ provider: Provider
115
+
116
+
117
+ await ferro.connect("postgres://...", migrate_updates=True)
118
+ # → ALTER TYPE "provider" ADD VALUE IF NOT EXISTS 'mx'
119
+
120
+ feed = await Feed.create(provider=Provider.MX)
121
+ recent = await Feed.where(lambda feed: feed.provider == Provider.MX).all()
122
+ ```
123
+
124
+ The contract, precisely:
125
+
126
+ - **Append-only, metadata-only.** Label addition adds labels and does nothing else; rows are never touched. A shared `StrEnum` used by several models is one type and reconciles once.
127
+ - **Removals and renames are never automatic.** A live label the model no longer declares raises a `UserWarning` naming the type and labels — rows may still hold that label, and older code may still be running against the schema mid-deploy — and the label stays. Remove or rename labels in a reviewed Alembic migration.
128
+ - **Labels commit before table changes.** Additions run as their own autocommit statements ahead of the per-table plans, so a new column whose literal default is a brand-new member works in a single deploy, on every supported PostgreSQL version.
129
+ - **Appended labels sort last.** `ADD VALUE` appends: a member inserted mid-enum in Python lands at the end of the database ordering, and `ORDER BY` on an enum column follows *database* order, not declaration order.
130
+ - **SQLite is unaffected.** Enums store as text there; a new member needs no DDL.
131
+
62
132
  ### Destructive drops with `migrate_destructive`
63
133
 
64
134
  *Added in 0.11.0.* Also **drop** live columns that no longer exist on the model (never whole tables):
@@ -134,6 +204,7 @@ target_metadata = get_metadata()
134
204
  - **Composite constraints** (`__ferro_composite_uniques__`, `__ferro_composite_indexes__`) emit matching `UniqueConstraint` / `Index` objects, including the automatic constraints on many-to-many join tables.
135
205
  - **One-to-one** relations (`ForeignKey(unique=True)`) emit the same `UNIQUE` on the shadow column that `auto_migrate` creates at runtime.
136
206
  - **Enums** map to named `sqlalchemy.Enum` types (class name lowercased, e.g. `UserRole` → `userrole`) so revisions compile on PostgreSQL, which rejects anonymous enum types.
207
+ - **Enum label drift is diffed.** *0.18.0+.* Alembic core is blind to enum value changes; ferro's bridge registers an autogenerate comparator that diffs each named enum type against the live PostgreSQL catalog — the same decision (and the same rendered SQL) the auto-migrate pass uses. A grown `StrEnum` generates `ALTER TYPE ... ADD VALUE IF NOT EXISTS` inside an `autocommit_block()` (placed before table operations, runnable on every supported PostgreSQL version); a live label the model no longer declares generates a comment in the revision telling you removal needs a hand-written step. Models in sync generate nothing.
137
208
 
138
209
  ### Autogenerate
139
210
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ferro-orm"
3
- version = "0.17.1"
3
+ version = "0.18.0"
4
4
  description = "A high-performance, Rust-backed ORM for Python."
5
5
  readme = "README.md"
6
6
  authors = [