ferro-orm 0.16.2__tar.gz → 0.16.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/CHANGELOG.md +16 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/CONTEXT.md +12 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/Cargo.lock +5 -5
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/Cargo.toml +1 -1
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/PKG-INFO +1 -1
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/pyproject.toml +1 -1
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/__init__.py +25 -30
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/_shadow_fk_types.py +5 -6
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/columns.py +30 -2
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/ir/compiler.py +35 -40
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/metaclass.py +7 -6
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/models.py +32 -27
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/query/builder.py +17 -82
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/query/wire.py +85 -28
- ferro_orm-0.16.3/src/ferro/registry.py +328 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/relations/__init__.py +19 -25
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/relations/descriptors.py +13 -10
- ferro_orm-0.16.3/src/ferro/state.py +176 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/conftest.py +8 -29
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_alembic_autogenerate.py +6 -12
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_alembic_bridge.py +2 -4
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_alembic_db_type.py +3 -9
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_alembic_nullability.py +2 -4
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_alembic_type_mapping.py +2 -4
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_auto_migrate.py +25 -46
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_catalog_cache.py +2 -4
- ferro_orm-0.16.3/tests/test_compiled_query.py +81 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_composite_index.py +3 -7
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_composite_unique.py +3 -7
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_connection.py +2 -2
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_cross_emitter_parity.py +5 -11
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_db_type_cross_emitter_parity.py +3 -11
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_db_type_integration.py +4 -6
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_documentation_features.py +4 -4
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_enum_cold_hydration.py +6 -6
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_fk_target_pk.py +6 -14
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_generation_counter_dirty_tracking.py +16 -14
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_hydration_equivalence.py +3 -7
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_include.py +1 -1
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_ir_vectors_contract.py +13 -10
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_model_identity.py +13 -25
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_mutation_pagination_guard.py +1 -1
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_named_connections_integration.py +2 -2
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_naming_single_source.py +8 -12
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_one_to_one.py +2 -3
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_operation_seam_sync.py +3 -3
- ferro_orm-0.16.3/tests/test_primary_key_facts.py +77 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_provisional_import.py +9 -9
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_query_builder.py +3 -3
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_query_joins.py +2 -2
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_query_typing.py +2 -3
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_query_wire_vectors.py +5 -5
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_registry_entrypoints.py +69 -74
- ferro_orm-0.16.3/tests/test_registry_owner.py +119 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_relationship_engine.py +2 -3
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_relationship_resolution_errors.py +3 -7
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_session.py +2 -2
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_shadow_fk_types.py +4 -6
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_sqlite_alembic_reconnect_hydration.py +2 -4
- ferro_orm-0.16.2/src/ferro/state.py +0 -355
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/PERMISSIONS.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.github/workflows/release.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.gitignore +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/.python-version +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/AGENTS.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/LICENSE +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/README.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/benchmarks/README.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/benchmarks/__init__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/benchmarks/__main__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/benchmarks/backends.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/benchmarks/baselines/postgres.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/benchmarks/baselines/sqlite.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/benchmarks/compare.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/benchmarks/harness.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/benchmarks/model.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/benchmarks/run.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/crates/ferro-ddl-lowering/src/lib.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/crates/ferro-migrate/Cargo.toml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/crates/ferro-migrate/src/emit.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/crates/ferro-migrate/src/lib.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/crates/ferro-migrate/src/order.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/crates/ferro-migrate/src/tests.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/crates/ferro-schema-ir/Cargo.toml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/crates/ferro-schema-ir/src/lib.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/adr/0001-resolved-epoch-registration.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/adr/0008-populated-relations-include.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/agents/domain.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/agents/issue-tracker.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/agents/triage-labels.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/architecture-review-2026-07-08.html +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-26-ir-p8.5-143-db-type-drop-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-28-ir-p8.5-141-single-schema-ir-producer-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-29-ir-p8.6-146-dialect-enum-unification-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-29-ir-p8.6-153-create-path-ir-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-07-01-ir-p8.6-154-datetime-tz-coarseness-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-07-01-ir-p8.6-162-typed-save-bind-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-07-01-ir-p8.6-165-blob-introspection-false-positive-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/aggregations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/aggregations_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/multiple_databases.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/mutations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/pagination.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/partial_selects.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/partial_selects_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/populated_relations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/populated_relations_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/predicates.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/predicates_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/quickstart.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/quickstart_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/raw_sql.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/relationships.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/relationships_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/soft_deletes.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/soft_deletes_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/testing_conftest.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/timestamps.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/timestamps_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/transactions.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/traversal.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/examples/traversal_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/api/connection.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/api/exceptions.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/api/fields.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/api/migrations.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/api/model.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/api/queries.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/api/raw-sql.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/api/relationships.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/api/transactions.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/changelog.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/concepts/architecture.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/concepts/backends.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/concepts/identity-map.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/concepts/performance.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/concepts/query-typing.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/concepts/type-safety.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/contributing.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/faq.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/getting-started/installation.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/getting-started/next-steps.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/getting-started/quickstart.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/guide/aggregations.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/guide/connections.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/guide/migrations.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/guide/models-and-fields.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/guide/mutations.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/guide/queries.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/guide/raw-sql.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/guide/relationships.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/guide/transactions.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/howto/multiple-databases.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/howto/pagination.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/howto/soft-deletes.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/howto/testing.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/howto/timestamps.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/howto/upgrade-guide.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/index.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/roadmap.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/stylesheets/extra.css +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/pages/why-ferro.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-06-24-001-feat-ir-p8-119-wire-automigrate-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-06-25-001-feat-ir-p8-120-parity-gate-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-06-26-001-feat-ir-p8.5-140-shared-lowering-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-06-26-002-feat-ir-p8.5-143-db-type-drop-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-06-26-003-feat-ir-p8.5-144-reconcile-indexes-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-06-28-001-feat-ir-p8.5-141-single-schema-ir-producer-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-06-29-001-refactor-ir-p8.6-153-create-path-unification-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-06-29-002-refactor-ir-p8.6-146-dialect-enum-unification-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-06-30-001-refactor-ir-p8.6-158-check-renderer-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-06-30-002-fix-ir-p8.6-155-deferred-annotations-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-01-001-feat-ir-p8.6-162-typed-save-bind-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-01-002-fix-ir-p8.6-154-datetime-tz-coarseness-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-01-003-fix-ir-p8.6-165-blob-introspection-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-02-002-feat-ff-a-172-typed-exceptions-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-06-009-ff-f-query-builder-1.0-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-06-010-ff-f-query-builder-1.0-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/ir-first-migration-guide.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/plans/ir-first-release-checklist.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/rfc/ir-contracts-v1.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/README.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/issues/self-fk-evicts-component-from-create-order.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/patterns/foreign-key-index.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/patterns/ir-invariants.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/patterns/persistence-state.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/docs/solutions/patterns/typed-null-binds.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/justfile +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/scripts/demo_queries_pre_v012.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/backend.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/codec.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/codec_plan.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/connection.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/errors.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/_annotation_utils.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/_bind_payload.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/_core.pyi +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/_deprecations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/base.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/composite_indexes.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/composite_uniques.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/exceptions.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/fields.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/ir/__init__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/migrations/alembic.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/py.typed +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/query/nodes.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/query/rows.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/raw.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/ferro/session.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/hydration.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/introspect.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/lib.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/migrate.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/naming_ffi.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/operations.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/query.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/schema.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/schema_bind.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/src/state.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/__init__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/db_backends.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/README.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_aggregate_v5.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v5.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_include_v5.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_left_join_v5.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_record_v5.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_traversal_v5.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_transaction_traversed_record_v5.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/query_user_compound_v5.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/static_fixtures/bad_includes.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/static_fixtures/bad_predicates.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/static_fixtures/bad_projections.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/static_fixtures/good_includes.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/static_fixtures/good_predicates.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/static_fixtures/good_projections.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_annotation_utils.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_bulk_create_chunking.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_bulk_install.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_codec_plan.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_column_specs.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_connection_redaction.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_constraints.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_crud.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_db_backends.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_db_type_typing.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_db_type_validation.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_deletion.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_deprecations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_exception_mapping.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_exceptions.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_field_wrapper.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_global_aggregates.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_grouped_aggregates.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_helpers.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_hydration.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_identity_memory.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_identity_refresh.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_identity_scoped_invalidation.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_identity_weakref.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_import_budget.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_metaclass_internals.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_metadata.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_migrate_plan.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_models.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_partial_selects.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_query_column_validation.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_query_immutability.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_raw_sql.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_refresh.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_relation_specs.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_route_single_site.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_routing_errors.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_save_pk_edges.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_save_semantics.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_schema.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_schema_db_type_metadata.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_schema_enum_annotations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_static_contracts.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_string_search.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_structural_types.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_transactions.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_traversed_projection.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_typed_null_binds.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/tests/test_typed_save_bind.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/uv.lock +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.16.3}/zensical.toml +0 -0
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.16.3 (2026-07-16)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- Derive the primary-key fact once and reject multi-PK models at class definition
|
|
9
|
+
([`ef91fa0`](https://github.com/syn54x/ferro-orm/commit/ef91fa09e09d5e7444e8d659b831921d40bcb365))
|
|
10
|
+
|
|
11
|
+
### Refactoring
|
|
12
|
+
|
|
13
|
+
- Compile_query returns one CompiledQuery artifact carrying the hop-class map
|
|
14
|
+
([`fc7084c`](https://github.com/syn54x/ferro-orm/commit/fc7084c4ad89b135815f74852263989f23868143))
|
|
15
|
+
|
|
16
|
+
- Single Registry owner for Python-side registration state
|
|
17
|
+
([`1179fcc`](https://github.com/syn54x/ferro-orm/commit/1179fcc07cc9e4305b7e7a8c141b8b29264dcd26))
|
|
18
|
+
|
|
19
|
+
|
|
4
20
|
## v0.16.2 (2026-07-15)
|
|
5
21
|
|
|
6
22
|
### Bug Fixes
|
|
@@ -84,6 +84,10 @@ _Avoid_: Select list, projection spec, hydration mode flag
|
|
|
84
84
|
The single typed wire artifact a query ships to the Rust runtime — model identity, predicates, ordering, paging, joins, and exactly one materialization plan, inside a versioned envelope. Compiled only by `compile_query`; no other code assembles query wire shape.
|
|
85
85
|
_Avoid_: Query dict, query def, payload dict
|
|
86
86
|
|
|
87
|
+
**Compiled query**:
|
|
88
|
+
The single artifact `compile_query` returns: the QueryIR payload, its wire JSON, and the plan-scoped hop-class map, all views of one compile. The map is collected from the hop facts the payload itself carries, so wire and hop classes can never disagree; it is `None` unless the materialization plan decodes or hydrates through a hop model's class (mirroring the Rust `needs_hop_classes` guard — a both-sides double-check). No other code assembles hop classes for the FFI.
|
|
89
|
+
_Avoid_: payload + kwargs, hop-class side-channel, wire tuple
|
|
90
|
+
|
|
87
91
|
**Golden vector**:
|
|
88
92
|
A hand-authored JSON fixture pinning one wire shape — the independent authority both the Python emitter and the Rust decoder assert against, never regenerated from either side's output. Updating one by hand is the contract-review moment for a wire change.
|
|
89
93
|
_Avoid_: Snapshot, fixture dump, example payload
|
|
@@ -100,6 +104,14 @@ _Avoid_: Nested select, join column, related-field pull
|
|
|
100
104
|
A user-chosen name for one field of a projected record, given as the key in a dict-returning selector (`select(lambda t: {"account_name": t.account.name})`). Aliases name output fields only — never joins or tables; the relation path remains the sole join identity.
|
|
101
105
|
_Avoid_: Column alias, AS label, join alias
|
|
102
106
|
|
|
107
|
+
**Primary key fact**:
|
|
108
|
+
The single cached answer to "which column is this model's primary key" — derived once at the compile choke point alongside column specs, stored as `__ferro_pk__` (`None` for a PK-less model). At most one `primary_key=True` column may be declared; a violation raises at class definition time. Operations that need a PK read the cached fact and raise clearly when it is `None` — never guess.
|
|
109
|
+
_Avoid_: PK lookup, PK scan, first primary-key column
|
|
110
|
+
|
|
111
|
+
**Registry**:
|
|
112
|
+
The single owner of Python-side registration state (`ferro.registry.REGISTRY`): model classes, per-model SchemaIR envelopes + fingerprints, join-table bundles, pending relations, the modelset artifact, and the generation counters. Its stores are private; the agreement invariants (fingerprint pairs its envelope, join-table eviction clears envelopes, one-call test reset) live inside the interface, not in caller convention. Routing/session state is not the Registry — that is `ferro.state`.
|
|
113
|
+
_Avoid_: state module, global dicts, model cache
|
|
114
|
+
|
|
103
115
|
**Provisional registration**:
|
|
104
116
|
The per-model state installed when a class body finishes executing — enough for runtime codec and PK metadata, but relationships may still be pending and the modelset is not yet authoritative for DDL.
|
|
105
117
|
_Avoid_: Import-time registration, partial registry
|
|
@@ -59,9 +59,9 @@ dependencies = [
|
|
|
59
59
|
|
|
60
60
|
[[package]]
|
|
61
61
|
name = "bitflags"
|
|
62
|
-
version = "2.13.
|
|
62
|
+
version = "2.13.1"
|
|
63
63
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
64
|
-
checksum = "
|
|
64
|
+
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
|
65
65
|
dependencies = [
|
|
66
66
|
"serde_core",
|
|
67
67
|
]
|
|
@@ -327,7 +327,7 @@ dependencies = [
|
|
|
327
327
|
|
|
328
328
|
[[package]]
|
|
329
329
|
name = "ferro"
|
|
330
|
-
version = "0.16.
|
|
330
|
+
version = "0.16.3"
|
|
331
331
|
dependencies = [
|
|
332
332
|
"chrono",
|
|
333
333
|
"dashmap",
|
|
@@ -1905,9 +1905,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|
|
1905
1905
|
|
|
1906
1906
|
[[package]]
|
|
1907
1907
|
name = "uuid"
|
|
1908
|
-
version = "1.
|
|
1908
|
+
version = "1.24.0"
|
|
1909
1909
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1910
|
-
checksum = "
|
|
1910
|
+
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
|
|
1911
1911
|
dependencies = [
|
|
1912
1912
|
"getrandom 0.4.3",
|
|
1913
1913
|
"js-sys",
|
|
@@ -73,13 +73,13 @@ async def _ensure_rust_registration_synced_for_operation() -> None:
|
|
|
73
73
|
contention the event loop blocks for one resolve+install per generation; do
|
|
74
74
|
not offload this to a thread pool — that would reintroduce interleaving.
|
|
75
75
|
"""
|
|
76
|
-
from .
|
|
76
|
+
from .registry import REGISTRY
|
|
77
77
|
|
|
78
|
-
if not
|
|
78
|
+
if not REGISTRY.is_dirty():
|
|
79
79
|
return
|
|
80
80
|
|
|
81
81
|
with _OPERATION_REGISTRATION_SYNC_LOCK:
|
|
82
|
-
if
|
|
82
|
+
if REGISTRY.is_dirty():
|
|
83
83
|
_ensure_rust_registration_synced()
|
|
84
84
|
|
|
85
85
|
|
|
@@ -97,25 +97,26 @@ def ensure_resolved_modelset() -> dict[str, Any]:
|
|
|
97
97
|
path.
|
|
98
98
|
"""
|
|
99
99
|
from .ir.compiler import compile_registry_schema_ir
|
|
100
|
+
from .registry import REGISTRY
|
|
100
101
|
from .relations import resolve_relationships
|
|
101
|
-
from .state import is_modelset_dirty
|
|
102
102
|
|
|
103
|
-
if not
|
|
103
|
+
if not REGISTRY.is_dirty():
|
|
104
104
|
return compile_registry_schema_ir()
|
|
105
105
|
|
|
106
106
|
with _RESOLVED_MODELSET_LOCK:
|
|
107
|
-
if
|
|
107
|
+
if REGISTRY.is_dirty():
|
|
108
108
|
resolve_relationships()
|
|
109
109
|
return compile_registry_schema_ir()
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
def _ensure_rust_registration_synced() -> None:
|
|
113
113
|
"""Resolve the modelset if needed, then push it to the Rust runtime."""
|
|
114
|
-
from . import
|
|
114
|
+
from .registry import REGISTRY
|
|
115
115
|
|
|
116
116
|
envelope = ensure_resolved_modelset()
|
|
117
|
+
cached = REGISTRY.modelset()
|
|
117
118
|
_push_registration_to_rust(
|
|
118
|
-
envelope, fingerprint=
|
|
119
|
+
envelope, fingerprint=cached[1] if cached is not None else None
|
|
119
120
|
)
|
|
120
121
|
|
|
121
122
|
|
|
@@ -131,24 +132,21 @@ def clear_registry() -> None:
|
|
|
131
132
|
tolerated by SQLite but rejected by Postgres (``relation ... does not
|
|
132
133
|
exist``). (#153)
|
|
133
134
|
|
|
134
|
-
The Python model registry
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
``tests/test_enum_cold_hydration.py``). Callers that want a full
|
|
138
|
-
reset
|
|
139
|
-
|
|
140
|
-
Each purged join table's compiled SchemaIR envelope is
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
future assemble step resurrect the stale join table.
|
|
135
|
+
The Python model registry is intentionally **not** cleared here: clearing
|
|
136
|
+
the Rust registry while keeping the declared Python models is what allows
|
|
137
|
+
cold re-hydration after ``reset_engine`` (see
|
|
138
|
+
``tests/test_enum_cold_hydration.py``). Callers that want a full
|
|
139
|
+
Python-side reset use ``REGISTRY.reset_for_test()``.
|
|
140
|
+
|
|
141
|
+
Each purged join table's compiled SchemaIR envelope is evicted with it —
|
|
142
|
+
``Registry.clear_join_tables`` owns that agreement (#153): a lingering
|
|
143
|
+
join envelope would let a future assemble step resurrect the stale join
|
|
144
|
+
table.
|
|
145
145
|
"""
|
|
146
146
|
_core_clear_registry()
|
|
147
|
-
from .
|
|
147
|
+
from .registry import REGISTRY
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
evict_model_envelope(join_table)
|
|
151
|
-
_JOIN_TABLE_REGISTRY.clear()
|
|
149
|
+
REGISTRY.clear_join_tables()
|
|
152
150
|
|
|
153
151
|
|
|
154
152
|
def _push_registration_to_rust(
|
|
@@ -172,18 +170,15 @@ def _push_registration_to_rust(
|
|
|
172
170
|
"""
|
|
173
171
|
import json as _json
|
|
174
172
|
|
|
175
|
-
from . import state as ferro_state
|
|
176
173
|
from .ir.compiler import compile_registry_schema_ir, schema_ir_fingerprint
|
|
174
|
+
from .registry import REGISTRY
|
|
177
175
|
|
|
178
176
|
if envelope is None:
|
|
179
177
|
envelope = compile_registry_schema_ir()
|
|
180
178
|
if fingerprint is None:
|
|
181
|
-
cached =
|
|
182
|
-
if
|
|
183
|
-
|
|
184
|
-
and ferro_state._SCHEMA_IR_MODELSET_FINGERPRINT is not None
|
|
185
|
-
):
|
|
186
|
-
fingerprint = ferro_state._SCHEMA_IR_MODELSET_FINGERPRINT
|
|
179
|
+
cached = REGISTRY.modelset()
|
|
180
|
+
if cached is not None and cached[0] is envelope:
|
|
181
|
+
fingerprint = cached[1]
|
|
187
182
|
else:
|
|
188
183
|
fingerprint = schema_ir_fingerprint(envelope)
|
|
189
184
|
_install_registration(_json.dumps(envelope), fingerprint)
|
|
@@ -27,13 +27,12 @@ def _scalar_part_of_annotation(ann: Any) -> Any:
|
|
|
27
27
|
|
|
28
28
|
def pk_python_type_for_model(target: type[Any]) -> Any | None:
|
|
29
29
|
"""Return the PK column's scalar python type from the target's specs."""
|
|
30
|
-
|
|
31
|
-
if
|
|
30
|
+
pk_field = getattr(target, "__ferro_pk__", None)
|
|
31
|
+
if pk_field is None:
|
|
32
32
|
return None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return None
|
|
33
|
+
specs = getattr(target, "__ferro_columns__", None) or {}
|
|
34
|
+
spec = specs.get(pk_field)
|
|
35
|
+
return spec.python_type if spec is not None else None
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
def shadow_annotation_for_pk(pk_ann: Any) -> Any:
|
|
@@ -31,9 +31,37 @@ __all__ = [
|
|
|
31
31
|
"build_relation_specs",
|
|
32
32
|
"fk_shadow_spec",
|
|
33
33
|
"pk_spec",
|
|
34
|
+
"primary_key_field_name",
|
|
34
35
|
]
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def primary_key_field_name(model_name: str, specs: dict[str, ColumnSpec]) -> str | None:
|
|
39
|
+
"""Derive the model's single PK field name from its column specs.
|
|
40
|
+
|
|
41
|
+
The one derivation site for "which column is the primary key" — cached on
|
|
42
|
+
the class as ``__ferro_pk__`` at the compile choke point, so every
|
|
43
|
+
consumer (save, descriptors, shadow-FK typing, traversal) reads the cached
|
|
44
|
+
fact instead of re-looping specs. At most one ``primary_key=True`` column
|
|
45
|
+
is legal; zero is a valid declaration (``None``) and the operations that
|
|
46
|
+
need a PK raise their own errors.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
TypeError: If more than one column declares ``primary_key=True`` —
|
|
50
|
+
at class definition time, naming the model and offending fields,
|
|
51
|
+
rather than resolving the ambiguity silently by declaration order
|
|
52
|
+
(the follow-up ADR-0002 asked for). ``TypeError`` is the
|
|
53
|
+
declaration-error contract the metaclass surfaces unwrapped
|
|
54
|
+
(ADR-0003's db_type validation sets the precedent).
|
|
55
|
+
"""
|
|
56
|
+
pks = [name for name, spec in specs.items() if spec.primary_key]
|
|
57
|
+
if len(pks) > 1:
|
|
58
|
+
raise TypeError(
|
|
59
|
+
f"Model {model_name!r} declares multiple primary-key columns: "
|
|
60
|
+
f"{sorted(pks)!r}. At most one primary_key=True column is supported."
|
|
61
|
+
)
|
|
62
|
+
return pks[0] if pks else None
|
|
63
|
+
|
|
64
|
+
|
|
37
65
|
@dataclass(frozen=True, slots=True)
|
|
38
66
|
class ForeignKeyRef:
|
|
39
67
|
to_table: str
|
|
@@ -126,13 +154,13 @@ def _property_is_integer(prop: dict[str, Any]) -> bool:
|
|
|
126
154
|
|
|
127
155
|
|
|
128
156
|
def _target_table_name(target: Any) -> str:
|
|
129
|
-
from .
|
|
157
|
+
from .registry import REGISTRY
|
|
130
158
|
|
|
131
159
|
if isinstance(target, ForwardRef):
|
|
132
160
|
target = target.__forward_arg__
|
|
133
161
|
if isinstance(target, str):
|
|
134
162
|
try:
|
|
135
|
-
model =
|
|
163
|
+
model = REGISTRY.resolve_reference(target, default=None)
|
|
136
164
|
except RuntimeError:
|
|
137
165
|
# Ambiguous short ref during first-pass schema build: defer the
|
|
138
166
|
# loud, candidate-listing error to resolve_relationships (the
|
|
@@ -10,7 +10,6 @@ from __future__ import annotations
|
|
|
10
10
|
from collections.abc import Sequence
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
-
from .. import state as ferro_state
|
|
14
13
|
from .._core import (
|
|
15
14
|
_ddl_check_constraint_name,
|
|
16
15
|
_ddl_composite_index_name,
|
|
@@ -19,14 +18,15 @@ from .._core import (
|
|
|
19
18
|
_ddl_single_index_name,
|
|
20
19
|
_ddl_single_unique_name,
|
|
21
20
|
)
|
|
22
|
-
from ..columns import
|
|
21
|
+
from ..columns import (
|
|
22
|
+
ColumnSpec,
|
|
23
|
+
build_column_specs,
|
|
24
|
+
build_relation_specs,
|
|
25
|
+
primary_key_field_name,
|
|
26
|
+
)
|
|
23
27
|
from ..composite_indexes import drop_overlap_with_uniques, normalized_composite_indexes
|
|
24
28
|
from ..composite_uniques import normalized_composite_uniques
|
|
25
|
-
from ..
|
|
26
|
-
_JOIN_TABLE_REGISTRY,
|
|
27
|
-
_MODEL_REGISTRY_PY,
|
|
28
|
-
_SCHEMA_IR_BY_MODEL,
|
|
29
|
-
)
|
|
29
|
+
from ..registry import REGISTRY, ir_fingerprint
|
|
30
30
|
|
|
31
31
|
_IR_VERSION = 1
|
|
32
32
|
|
|
@@ -214,11 +214,11 @@ def compile_schema_ir_payload(
|
|
|
214
214
|
def _persist_schema_ir_envelope(model_name: str, envelope: dict[str, Any]) -> None:
|
|
215
215
|
"""Store one model's compiled SchemaIR envelope and fingerprint.
|
|
216
216
|
|
|
217
|
-
Routes through the
|
|
218
|
-
|
|
219
|
-
|
|
217
|
+
Routes through the Registry's envelope-write entrypoint so every per-model
|
|
218
|
+
store mutation lives at the store-owning layer (#243); the fingerprint is
|
|
219
|
+
derived from the envelope inside that entrypoint.
|
|
220
220
|
"""
|
|
221
|
-
|
|
221
|
+
REGISTRY.persist_envelope(model_name, envelope)
|
|
222
222
|
|
|
223
223
|
|
|
224
224
|
def _compile_and_persist_model_envelope(
|
|
@@ -300,6 +300,10 @@ def compile_model_schema_ir(
|
|
|
300
300
|
"""
|
|
301
301
|
if specs is None:
|
|
302
302
|
specs = build_column_specs(model_cls)
|
|
303
|
+
# Derive the PK fact (and run the at-most-one guard) before compile +
|
|
304
|
+
# persist: a multi-PK model raises here, at class definition time, and
|
|
305
|
+
# never persists an envelope or publishes specs.
|
|
306
|
+
pk_field = primary_key_field_name(model_name, specs)
|
|
303
307
|
column_names = frozenset(specs)
|
|
304
308
|
uniques = normalized_composite_uniques(model_cls, column_names)
|
|
305
309
|
indexes = drop_overlap_with_uniques(
|
|
@@ -318,6 +322,9 @@ def compile_model_schema_ir(
|
|
|
318
322
|
# "``__ferro_columns__`` can never go stale relative to the envelope"
|
|
319
323
|
# invariant, made atomic.
|
|
320
324
|
model_cls.__ferro_columns__ = specs
|
|
325
|
+
# Same choke point caches the PK fact: every ``__ferro_columns__`` refresh
|
|
326
|
+
# re-derives ``__ferro_pk__``, so it can never go stale relative to specs.
|
|
327
|
+
model_cls.__ferro_pk__ = pk_field
|
|
321
328
|
# Same choke point compiles relation-traversal facts (#268): every
|
|
322
329
|
# ``__ferro_columns__`` refresh (provisional class-body registration and
|
|
323
330
|
# the resolved second pass) refreshes ``__ferro_relation_specs__`` too, so
|
|
@@ -329,12 +336,12 @@ def compile_model_schema_ir(
|
|
|
329
336
|
def _model_payload_from_envelope(name: str) -> dict[str, Any]:
|
|
330
337
|
"""Return one model's SchemaIR payload object from the envelope cache.
|
|
331
338
|
|
|
332
|
-
The returned dict is a live reference into
|
|
333
|
-
assembled modelset shares these payload objects. Treat them as
|
|
334
|
-
in-place mutation corrupts the per-model cache and survives
|
|
335
|
-
re-assembles indefinitely (#245).
|
|
339
|
+
The returned dict is a live reference into the Registry's envelope cache —
|
|
340
|
+
the assembled modelset shares these payload objects. Treat them as
|
|
341
|
+
read-only; in-place mutation corrupts the per-model cache and survives
|
|
342
|
+
clean-path re-assembles indefinitely (#245).
|
|
336
343
|
"""
|
|
337
|
-
envelope =
|
|
344
|
+
envelope = REGISTRY.envelope(name)
|
|
338
345
|
if envelope is None:
|
|
339
346
|
raise RuntimeError(
|
|
340
347
|
f"Missing SchemaIR envelope for '{name}'. "
|
|
@@ -347,11 +354,11 @@ def _model_payload_from_envelope(name: str) -> dict[str, Any]:
|
|
|
347
354
|
def _assemble_modelset_envelope() -> dict[str, Any]:
|
|
348
355
|
"""Stitch per-model envelopes into one sorted modelset envelope."""
|
|
349
356
|
models: list[dict[str, Any]] = []
|
|
350
|
-
for model_name
|
|
357
|
+
for model_name in sorted(REGISTRY.models()):
|
|
351
358
|
models.append(_model_payload_from_envelope(model_name))
|
|
352
359
|
|
|
353
360
|
for table_name, bundle in sorted(
|
|
354
|
-
|
|
361
|
+
REGISTRY.join_tables().items(), key=lambda item: item[0]
|
|
355
362
|
):
|
|
356
363
|
if not isinstance(bundle, dict):
|
|
357
364
|
continue
|
|
@@ -366,22 +373,10 @@ def _assemble_modelset_envelope() -> dict[str, Any]:
|
|
|
366
373
|
},
|
|
367
374
|
}
|
|
368
375
|
|
|
369
|
-
|
|
370
|
-
ferro_state._SCHEMA_IR_MODELSET_FINGERPRINT = ferro_state.ir_fingerprint(envelope)
|
|
376
|
+
REGISTRY.set_modelset(envelope)
|
|
371
377
|
return envelope
|
|
372
378
|
|
|
373
379
|
|
|
374
|
-
def _missing_registry_envelope_names() -> list[str]:
|
|
375
|
-
missing: list[str] = []
|
|
376
|
-
for name in _MODEL_REGISTRY_PY:
|
|
377
|
-
if name not in _SCHEMA_IR_BY_MODEL:
|
|
378
|
-
missing.append(name)
|
|
379
|
-
for table_name, bundle in _JOIN_TABLE_REGISTRY.items():
|
|
380
|
-
if isinstance(bundle, dict) and table_name not in _SCHEMA_IR_BY_MODEL:
|
|
381
|
-
missing.append(table_name)
|
|
382
|
-
return missing
|
|
383
|
-
|
|
384
|
-
|
|
385
380
|
def _register_join_table_bundle(table_name: str, bundle: dict[str, Any]) -> None:
|
|
386
381
|
register_model_with_ir(
|
|
387
382
|
table_name,
|
|
@@ -393,12 +388,12 @@ def _register_join_table_bundle(table_name: str, bundle: dict[str, Any]) -> None
|
|
|
393
388
|
|
|
394
389
|
|
|
395
390
|
def _recompile_missing_registry_envelopes() -> None:
|
|
396
|
-
for model_name in
|
|
397
|
-
model_cls =
|
|
391
|
+
for model_name in REGISTRY.missing_envelope_names():
|
|
392
|
+
model_cls = REGISTRY.models().get(model_name)
|
|
398
393
|
if model_cls is not None:
|
|
399
394
|
compile_model_schema_ir(model_name, model_cls)
|
|
400
395
|
continue
|
|
401
|
-
bundle =
|
|
396
|
+
bundle = REGISTRY.join_tables().get(model_name)
|
|
402
397
|
if isinstance(bundle, dict):
|
|
403
398
|
_register_join_table_bundle(model_name, bundle)
|
|
404
399
|
|
|
@@ -411,10 +406,10 @@ def _recompile_all_registered_envelopes() -> None:
|
|
|
411
406
|
Alembic-adjacent tooling) historically relied on compile-not-assemble here.
|
|
412
407
|
The connect/reconnect clean path never hits this (#245).
|
|
413
408
|
"""
|
|
414
|
-
for model_name, model_cls in sorted(
|
|
409
|
+
for model_name, model_cls in sorted(REGISTRY.models().items()):
|
|
415
410
|
compile_model_schema_ir(model_name, model_cls)
|
|
416
411
|
for table_name, bundle in sorted(
|
|
417
|
-
|
|
412
|
+
REGISTRY.join_tables().items(), key=lambda item: item[0]
|
|
418
413
|
):
|
|
419
414
|
if isinstance(bundle, dict):
|
|
420
415
|
_register_join_table_bundle(table_name, bundle)
|
|
@@ -423,8 +418,8 @@ def _recompile_all_registered_envelopes() -> None:
|
|
|
423
418
|
def compile_registry_schema_ir() -> dict[str, Any]:
|
|
424
419
|
"""Assemble and persist a deterministic SchemaIR envelope for all models.
|
|
425
420
|
|
|
426
|
-
Stitches already-compiled per-model envelopes from
|
|
427
|
-
for every
|
|
421
|
+
Stitches already-compiled per-model envelopes from the Registry's envelope
|
|
422
|
+
cache for every registered model and join table.
|
|
428
423
|
On a dirty registry, recompiles envelopes first so direct callers that
|
|
429
424
|
bypass ``resolve_relationships()`` still observe current registry state.
|
|
430
425
|
After ``ensure_resolved_modelset()`` on the clean path, this is
|
|
@@ -433,7 +428,7 @@ def compile_registry_schema_ir() -> dict[str, Any]:
|
|
|
433
428
|
Returns:
|
|
434
429
|
The assembled model-set SchemaIR envelope, sorted by model name.
|
|
435
430
|
"""
|
|
436
|
-
if
|
|
431
|
+
if REGISTRY.is_dirty():
|
|
437
432
|
_recompile_all_registered_envelopes()
|
|
438
433
|
else:
|
|
439
434
|
_recompile_missing_registry_envelopes()
|
|
@@ -442,4 +437,4 @@ def compile_registry_schema_ir() -> dict[str, Any]:
|
|
|
442
437
|
|
|
443
438
|
def schema_ir_fingerprint(ir_envelope: dict[str, Any]) -> str:
|
|
444
439
|
"""Return a deterministic SHA-256 fingerprint for a SchemaIR envelope."""
|
|
445
|
-
return
|
|
440
|
+
return ir_fingerprint(ir_envelope)
|
|
@@ -23,8 +23,8 @@ from .base import FerroField, ForeignKey, ManyToManyRelation
|
|
|
23
23
|
from .fields import FERRO_FIELD_EXTRA_KEY
|
|
24
24
|
from .ir import compile_model_schema_ir
|
|
25
25
|
from .query import Relation
|
|
26
|
+
from .registry import REGISTRY
|
|
26
27
|
from .relations.descriptors import ForwardDescriptor
|
|
27
|
-
from .state import _MODEL_REGISTRY_PY, _PENDING_RELATIONS, register_model
|
|
28
28
|
|
|
29
29
|
_TABLE_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*\Z")
|
|
30
30
|
|
|
@@ -79,7 +79,7 @@ class ModelMetaclass(type(BaseModel)):
|
|
|
79
79
|
cls.__ferro_identity__ = f"{cls.__module__}.{cls.__qualname__}"
|
|
80
80
|
cls.__ferro_table__ = mcs._resolve_table_name(name, namespace)
|
|
81
81
|
for field_name, metadata in pending_relations:
|
|
82
|
-
|
|
82
|
+
REGISTRY.defer_relation(cls.__ferro_identity__, field_name, metadata)
|
|
83
83
|
|
|
84
84
|
mcs._register_model(cls, cls.__ferro_identity__, local_relations)
|
|
85
85
|
ferro_fields = mcs._parse_ferro_field_metadata(cls)
|
|
@@ -278,8 +278,9 @@ class ModelMetaclass(type(BaseModel)):
|
|
|
278
278
|
Returns:
|
|
279
279
|
(local_relations, fields_to_remove, pending_relations): Relationship
|
|
280
280
|
metadata, fields to hide from Pydantic, and ``(field_name, metadata)``
|
|
281
|
-
entries deferred to
|
|
282
|
-
resolved ``__ferro_identity__`` after the class
|
|
281
|
+
entries deferred to the Registry's pending-relation queue (flushed
|
|
282
|
+
under the model's resolved ``__ferro_identity__`` after the class
|
|
283
|
+
object exists).
|
|
283
284
|
"""
|
|
284
285
|
local_relations = {}
|
|
285
286
|
fields_to_remove = []
|
|
@@ -405,7 +406,7 @@ class ModelMetaclass(type(BaseModel)):
|
|
|
405
406
|
qualified identity is idempotent.
|
|
406
407
|
"""
|
|
407
408
|
table_name = cls.__ferro_table__
|
|
408
|
-
for key, other in
|
|
409
|
+
for key, other in REGISTRY.models().items():
|
|
409
410
|
if key == identity:
|
|
410
411
|
continue
|
|
411
412
|
if getattr(other, "__ferro_table__", None) == table_name:
|
|
@@ -415,7 +416,7 @@ class ModelMetaclass(type(BaseModel)):
|
|
|
415
416
|
"models cannot share a table. Set __ferro_table__ on one of "
|
|
416
417
|
"them to give it a distinct table name."
|
|
417
418
|
)
|
|
418
|
-
|
|
419
|
+
REGISTRY.register(cls)
|
|
419
420
|
cls.ferro_relations = local_relations
|
|
420
421
|
|
|
421
422
|
@staticmethod
|
|
@@ -126,9 +126,9 @@ def evict_instance(
|
|
|
126
126
|
directly with their already-resolved route instead of going through this
|
|
127
127
|
wrapper, so a route is never resolved twice for one operation.
|
|
128
128
|
"""
|
|
129
|
-
from .
|
|
129
|
+
from .registry import REGISTRY
|
|
130
130
|
|
|
131
|
-
model_cls =
|
|
131
|
+
model_cls = REGISTRY.resolve_reference(model) if isinstance(model, str) else model
|
|
132
132
|
route = resolve_operation_scope(using=using, session=session)
|
|
133
133
|
_core_evict_instance(model_cls.__ferro_identity__, pk, route)
|
|
134
134
|
|
|
@@ -211,6 +211,9 @@ class Model(BaseModel, metaclass=ModelMetaclass):
|
|
|
211
211
|
__ferro_composite_uniques__: ClassVar[tuple[tuple[str, ...], ...]] = ()
|
|
212
212
|
__ferro_composite_indexes__: ClassVar[tuple[tuple[str, ...], ...]] = ()
|
|
213
213
|
__ferro_columns__: ClassVar[dict[str, "ColumnSpec"]] = {}
|
|
214
|
+
#: The single PK field name, derived once at the compile choke point
|
|
215
|
+
#: alongside ``__ferro_columns__``; ``None`` for a PK-less model.
|
|
216
|
+
__ferro_pk__: ClassVar[str | None] = None
|
|
214
217
|
__ferro_relation_specs__: ClassVar[dict[str, "RelationSpec"]] = {}
|
|
215
218
|
_enum_fields: ClassVar[dict[str, type[Enum]]] = {}
|
|
216
219
|
|
|
@@ -247,9 +250,17 @@ class Model(BaseModel, metaclass=ModelMetaclass):
|
|
|
247
250
|
if isinstance(val, Model):
|
|
248
251
|
# Read the *target* model's PK (FF-D D5) — the source
|
|
249
252
|
# model's PK name is irrelevant to the related instance.
|
|
250
|
-
pk_field = val.__class__.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
+
pk_field = val.__class__.__ferro_pk__
|
|
254
|
+
if pk_field is None:
|
|
255
|
+
raise ValueError(
|
|
256
|
+
f"Cannot assign a {val.__class__.__name__} instance "
|
|
257
|
+
f"to relationship field {field_name!r}: "
|
|
258
|
+
f"{val.__class__.__name__} declares no primary-key "
|
|
259
|
+
"column, so there is no value to store in "
|
|
260
|
+
f"{field_name!r}_id. Pass the scalar value directly "
|
|
261
|
+
"or declare a primary_key=True column."
|
|
262
|
+
)
|
|
263
|
+
data[f"{field_name}_id"] = getattr(val, pk_field, None)
|
|
253
264
|
else:
|
|
254
265
|
# It's already an ID or something else
|
|
255
266
|
data[f"{field_name}_id"] = val
|
|
@@ -323,7 +334,7 @@ class Model(BaseModel, metaclass=ModelMetaclass):
|
|
|
323
334
|
mode="upsert",
|
|
324
335
|
)
|
|
325
336
|
elif _is_persisted(self):
|
|
326
|
-
pk_field_name = self.__class__.
|
|
337
|
+
pk_field_name = self.__class__.__ferro_pk__
|
|
327
338
|
pk_val = getattr(self, pk_field_name) if pk_field_name is not None else None
|
|
328
339
|
if pk_val is None:
|
|
329
340
|
raise ValueError(
|
|
@@ -346,16 +357,17 @@ class Model(BaseModel, metaclass=ModelMetaclass):
|
|
|
346
357
|
)
|
|
347
358
|
|
|
348
359
|
pk_val = None
|
|
349
|
-
pk_field_name =
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
360
|
+
pk_field_name = self.__class__.__ferro_pk__
|
|
361
|
+
|
|
362
|
+
if pk_field_name is not None:
|
|
363
|
+
spec = self.__class__.__ferro_columns__[pk_field_name]
|
|
364
|
+
if (
|
|
365
|
+
spec.autoincrement
|
|
366
|
+
and getattr(self, pk_field_name) is None
|
|
367
|
+
and new_id is not None
|
|
368
|
+
):
|
|
369
|
+
setattr(self, pk_field_name, new_id)
|
|
370
|
+
pk_val = getattr(self, pk_field_name)
|
|
359
371
|
|
|
360
372
|
if pk_val is not None:
|
|
361
373
|
register_instance(
|
|
@@ -380,7 +392,7 @@ class Model(BaseModel, metaclass=ModelMetaclass):
|
|
|
380
392
|
>>> if user:
|
|
381
393
|
... await user.delete()
|
|
382
394
|
"""
|
|
383
|
-
pk_field_name = self.__class__.
|
|
395
|
+
pk_field_name = self.__class__.__ferro_pk__
|
|
384
396
|
pk_val = getattr(self, pk_field_name) if pk_field_name is not None else None
|
|
385
397
|
route, _identity_using = await _instance_transaction_route(self, using, session)
|
|
386
398
|
|
|
@@ -394,13 +406,6 @@ class Model(BaseModel, metaclass=ModelMetaclass):
|
|
|
394
406
|
# The instance is transient again: a later save() re-INSERTs.
|
|
395
407
|
_set_persisted(self, False)
|
|
396
408
|
|
|
397
|
-
@classmethod
|
|
398
|
-
def _primary_key_field_name(cls) -> str | None:
|
|
399
|
-
for field_name, spec in cls.__ferro_columns__.items():
|
|
400
|
-
if spec.primary_key:
|
|
401
|
-
return field_name
|
|
402
|
-
return None
|
|
403
|
-
|
|
404
409
|
@classmethod
|
|
405
410
|
async def all(
|
|
406
411
|
cls, *, using: str | None = None, session: "Session | None" = None
|
|
@@ -454,7 +459,7 @@ class Model(BaseModel, metaclass=ModelMetaclass):
|
|
|
454
459
|
Returns:
|
|
455
460
|
The matching model instance, or None when no record exists.
|
|
456
461
|
"""
|
|
457
|
-
pk_field_name = cls.
|
|
462
|
+
pk_field_name = cls.__ferro_pk__
|
|
458
463
|
if pk_field_name is None:
|
|
459
464
|
raise RuntimeError(f"Model {cls.__name__} does not define a primary key")
|
|
460
465
|
|
|
@@ -475,7 +480,7 @@ class Model(BaseModel, metaclass=ModelMetaclass):
|
|
|
475
480
|
>>> user = await User.get(1)
|
|
476
481
|
>>> await user.refresh()
|
|
477
482
|
"""
|
|
478
|
-
pk_field_name = self.__class__.
|
|
483
|
+
pk_field_name = self.__class__.__ferro_pk__
|
|
479
484
|
pk_val = getattr(self, pk_field_name) if pk_field_name is not None else None
|
|
480
485
|
|
|
481
486
|
if pk_val is None:
|
|
@@ -772,7 +777,7 @@ class ModelConnection[M: Model]:
|
|
|
772
777
|
return instance
|
|
773
778
|
|
|
774
779
|
async def get_or_none(self, pk: Any) -> M | None:
|
|
775
|
-
pk_field_name = self.model_cls.
|
|
780
|
+
pk_field_name = self.model_cls.__ferro_pk__
|
|
776
781
|
if pk_field_name is None:
|
|
777
782
|
raise RuntimeError(
|
|
778
783
|
f"Model {self.model_cls.__name__} does not define a primary key"
|