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.
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/AGENTS.md +10 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/CHANGELOG.md +32 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/CONTEXT.md +10 -2
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/Cargo.lock +41 -51
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/Cargo.toml +1 -1
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/PKG-INFO +1 -1
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-ddl-lowering/src/lib.rs +106 -0
- ferro_orm-0.18.0/docs/adr/0011-label-addition-update-gated-append-only.md +65 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/migrations.md +71 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/pyproject.toml +1 -1
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/_core.pyi +12 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/migrations/alembic.py +138 -1
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/introspect.rs +29 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/lib.rs +1 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/migrate.rs +73 -3
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/naming_ffi.rs +23 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_cross_emitter_parity.py +20 -0
- ferro_orm-0.18.0/tests/test_label_addition.py +380 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/PERMISSIONS.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.github/workflows/release.yml +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.gitignore +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/.python-version +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/LICENSE +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/README.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/README.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/__init__.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/__main__.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/backends.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/baselines/postgres.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/baselines/sqlite.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/compare.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/harness.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/model.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/benchmarks/run.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-migrate/Cargo.toml +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-migrate/src/emit.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-migrate/src/lib.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-migrate/src/order.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-migrate/src/tests.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-schema-ir/Cargo.toml +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/crates/ferro-schema-ir/src/lib.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0001-resolved-epoch-registration.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0007-reverse-relations-tested-not-traversed.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0008-populated-relations-include.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0008-predicate-negation-uniform-not.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/adr/0010-create-pass-owns-only-missing-tables.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/agents/domain.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/agents/issue-tracker.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/agents/triage-labels.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/architecture-review-2026-07-08.html +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/aggregations.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/aggregations_annotated.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/existence_tests.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/existence_tests_annotated.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/multiple_databases.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/mutations.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/pagination.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/partial_selects.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/partial_selects_annotated.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/populated_relations.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/populated_relations_annotated.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/predicates.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/predicates_annotated.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/quickstart.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/quickstart_annotated.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/raw_sql.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/relationships.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/relationships_annotated.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/soft_deletes.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/soft_deletes_annotated.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/testing_conftest.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/timestamps.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/timestamps_annotated.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/transactions.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/traversal.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/examples/traversal_annotated.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/connection.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/exceptions.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/fields.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/migrations.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/model.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/queries.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/raw-sql.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/relationships.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/api/transactions.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/changelog.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/architecture.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/backends.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/identity-map.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/performance.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/query-typing.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/concepts/type-safety.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/contributing.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/faq.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/getting-started/installation.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/getting-started/next-steps.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/getting-started/quickstart.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/aggregations.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/connections.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/models-and-fields.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/mutations.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/queries.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/raw-sql.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/relationships.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/guide/transactions.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/multiple-databases.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/pagination.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/soft-deletes.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/testing.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/timestamps.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/howto/upgrade-guide.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/index.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/roadmap.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/stylesheets/extra.css +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/pages/why-ferro.md +0 -0
- {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
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
- {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
- {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
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
- {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
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
- {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
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
- {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
- {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
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/2026-07-18-016-existence-tests-design.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/ir-first-migration-guide.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/plans/ir-first-release-checklist.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/rfc/ir-contracts-v1.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/README.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/self-fk-evicts-component-from-create-order.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/foreign-key-index.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/ir-invariants.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/persistence-state.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/docs/solutions/patterns/typed-null-binds.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/justfile +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/scripts/demo_queries_pre_v012.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/backend.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/codec.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/codec_plan.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/connection.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/errors.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/__init__.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/_annotation_utils.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/_bind_payload.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/_deprecations.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/_shadow_fk_types.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/base.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/columns.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/composite_indexes.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/composite_uniques.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/exceptions.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/fields.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/ir/__init__.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/ir/compiler.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/metaclass.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/models.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/py.typed +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/query/builder.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/query/nodes.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/query/rows.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/query/wire.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/raw.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/registry.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/relations/__init__.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/relations/descriptors.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/session.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/ferro/state.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/hydration.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/operations.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/query.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/schema.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/schema_bind.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/src/state.rs +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/__init__.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/conftest.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/db_backends.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/README.md +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_account_exists_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_account_scoped_exists_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_owner_nested_exists_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_owner_not_exists_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_aggregate_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_include_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_left_join_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_record_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_traversal_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_transaction_traversed_record_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_user_compound_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_user_m2m_exists_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_user_not_compound_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/query_user_not_leaf_v7.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/bad_includes.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/bad_predicates.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/bad_projections.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/good_includes.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/good_predicates.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/static_fixtures/good_projections.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_alembic_autogenerate.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_alembic_bridge.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_alembic_db_type.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_alembic_nullability.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_alembic_type_mapping.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_annotation_utils.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_auto_migrate.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_bulk_create_chunking.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_bulk_install.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_catalog_cache.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_codec_plan.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_column_specs.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_compiled_query.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_composite_index.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_composite_unique.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_connection.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_connection_redaction.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_constraints.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_crud.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_db_backends.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_db_type_cross_emitter_parity.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_db_type_integration.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_db_type_typing.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_db_type_validation.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_deletion.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_deprecations.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_documentation_features.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_enum_cold_hydration.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_exception_mapping.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_exceptions.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_field_wrapper.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_fk_target_pk.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_generation_counter_dirty_tracking.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_global_aggregates.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_grouped_aggregates.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_helpers.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_hydration.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_hydration_equivalence.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_identity_memory.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_identity_refresh.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_identity_scoped_invalidation.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_identity_weakref.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_import_budget.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_include.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_ir_vectors_contract.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_metaclass_internals.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_metadata.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_migrate_plan.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_model_identity.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_models.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_mutation_pagination_guard.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_named_connections_integration.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_naming_single_source.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_negation.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_one_to_one.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_operation_seam_sync.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_partial_selects.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_primary_key_facts.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_provisional_import.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_builder.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_column_validation.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_exists.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_immutability.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_joins.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_typing.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_query_wire_vectors.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_raw_sql.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_refresh.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_registry_entrypoints.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_registry_owner.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_relation_specs.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_relationship_engine.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_relationship_resolution_errors.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_route_single_site.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_routing_errors.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_save_pk_edges.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_save_semantics.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_schema.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_schema_db_type_metadata.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_schema_enum_annotations.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_session.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_shadow_fk_types.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_static_contracts.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_string_search.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_structural_types.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_transactions.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_traversed_projection.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_typed_null_binds.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/tests/test_typed_save_bind.py +0 -0
- {ferro_orm-0.17.1 → ferro_orm-0.18.0}/uv.lock +0 -0
- {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
|
|
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
|
-
|
|
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.
|
|
13
|
+
version = "0.1.6"
|
|
14
14
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
-
checksum = "
|
|
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.
|
|
98
|
+
version = "1.4.0"
|
|
99
99
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
100
|
-
checksum = "
|
|
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.
|
|
257
|
+
version = "0.2.7"
|
|
267
258
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
268
|
-
checksum = "
|
|
259
|
+
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
|
269
260
|
dependencies = [
|
|
270
261
|
"proc-macro2",
|
|
271
262
|
"quote",
|
|
272
|
-
"syn
|
|
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.
|
|
274
|
+
version = "1.17.0"
|
|
284
275
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
285
|
-
checksum = "
|
|
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.
|
|
310
|
+
version = "5.4.2"
|
|
320
311
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
321
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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.
|
|
802
|
+
version = "0.2.189"
|
|
813
803
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
814
|
-
checksum = "
|
|
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.
|
|
814
|
+
version = "0.1.19"
|
|
825
815
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
826
|
-
checksum = "
|
|
816
|
+
checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa"
|
|
827
817
|
dependencies = [
|
|
828
818
|
"bitflags",
|
|
829
819
|
"libc",
|
|
830
820
|
"plain",
|
|
831
|
-
"redox_syscall 0.9.
|
|
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.
|
|
1199
|
+
version = "0.9.1"
|
|
1210
1200
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1211
|
-
checksum = "
|
|
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.
|
|
1242
|
+
version = "0.23.43"
|
|
1253
1243
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1254
|
-
checksum = "
|
|
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.
|
|
1256
|
+
version = "1.15.1"
|
|
1267
1257
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1268
|
-
checksum = "
|
|
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.
|
|
1344
|
+
"syn 3.0.3",
|
|
1355
1345
|
]
|
|
1356
1346
|
|
|
1357
1347
|
[[package]]
|
|
1358
1348
|
name = "serde_json"
|
|
1359
|
-
version = "1.0.
|
|
1349
|
+
version = "1.0.151"
|
|
1360
1350
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1361
|
-
checksum = "
|
|
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.
|
|
1702
|
+
version = "3.0.3"
|
|
1713
1703
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1714
|
-
checksum = "
|
|
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.
|
|
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.
|
|
1775
|
+
version = "1.53.1"
|
|
1786
1776
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1787
|
-
checksum = "
|
|
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.
|
|
1792
|
+
version = "2.7.2"
|
|
1803
1793
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1804
|
-
checksum = "
|
|
1794
|
+
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
|
1805
1795
|
dependencies = [
|
|
1806
1796
|
"proc-macro2",
|
|
1807
1797
|
"quote",
|
|
1808
|
-
"syn
|
|
1798
|
+
"syn 3.0.3",
|
|
1809
1799
|
]
|
|
1810
1800
|
|
|
1811
1801
|
[[package]]
|
|
1812
1802
|
name = "tokio-stream"
|
|
1813
|
-
version = "0.1.
|
|
1803
|
+
version = "0.1.19"
|
|
1814
1804
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1815
|
-
checksum = "
|
|
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.
|
|
2253
|
+
version = "0.8.55"
|
|
2264
2254
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2265
|
-
checksum = "
|
|
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.
|
|
2262
|
+
version = "0.8.55"
|
|
2273
2263
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2274
|
-
checksum = "
|
|
2264
|
+
checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
|
|
2275
2265
|
dependencies = [
|
|
2276
2266
|
"proc-macro2",
|
|
2277
2267
|
"quote",
|
|
@@ -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
|
|