ferro-orm 0.12.1__tar.gz → 0.12.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.12.1 → ferro_orm-0.12.3}/CHANGELOG.md +21 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/Cargo.lock +1 -1
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/Cargo.toml +1 -1
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/PKG-INFO +1 -1
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/connection.md +2 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/connections.md +33 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/transactions.md +2 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/pyproject.toml +1 -1
- ferro_orm-0.12.3/src/ferro/session.py +128 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/state.py +20 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/operations.rs +3 -2
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/state.rs +64 -0
- ferro_orm-0.12.3/tests/test_session.py +555 -0
- ferro_orm-0.12.1/src/ferro/session.py +0 -45
- ferro_orm-0.12.1/tests/test_session.py +0 -130
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/PERMISSIONS.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/workflows/release.yml +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.gitignore +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.python-version +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/AGENTS.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/LICENSE +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/README.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/crates/ferro-migrate/Cargo.toml +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/crates/ferro-migrate/src/lib.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/crates/ferro-schema-ir/Cargo.toml +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/crates/ferro-schema-ir/src/lib.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/multiple_databases.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/mutations.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/pagination.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/predicates.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/predicates_annotated.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/quickstart.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/quickstart_annotated.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/raw_sql.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/relationships.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/relationships_annotated.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/soft_deletes.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/soft_deletes_annotated.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/testing_conftest.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/timestamps.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/timestamps_annotated.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/transactions.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/exceptions.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/fields.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/migrations.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/model.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/queries.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/raw-sql.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/relationships.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/transactions.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/changelog.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/architecture.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/backends.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/identity-map.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/performance.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/query-typing.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/type-safety.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/contributing.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/faq.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/getting-started/installation.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/getting-started/next-steps.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/getting-started/quickstart.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/migrations.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/models-and-fields.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/mutations.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/queries.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/raw-sql.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/relationships.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/migrating-to-v0-12-0.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/multiple-databases.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/pagination.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/soft-deletes.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/testing.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/timestamps.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/index.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/roadmap.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/stylesheets/extra.css +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/why-ferro.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/ir-first-migration-guide.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/ir-first-release-checklist.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/rfc/ir-contracts-v1.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/README.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/foreign-key-index.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/ir-invariants.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/typed-null-binds.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/justfile +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/scripts/demo_queries_pre_v012.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/backend.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/codec.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/connection.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/__init__.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/_annotation_utils.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/_core.pyi +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/_deprecations.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/_shadow_fk_types.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/base.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/composite_indexes.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/composite_uniques.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/exceptions.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/fields.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/ir/__init__.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/ir/compiler.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/metaclass.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/migrations/alembic.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/models.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/py.typed +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/query/builder.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/query/nodes.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/raw.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/relations/__init__.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/relations/descriptors.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/schema_metadata.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/hydration.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/introspect.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/lib.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/migrate.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/query.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/schema.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/schema_bind.rs +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/__init__.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/conftest.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/db_backends.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/ir_vectors/README.md +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/ir_vectors/query_user_compound_v1.json +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/shadow_reports/postgres.json +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/shadow_reports/sqlite.json +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_alembic_autogenerate.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_alembic_bridge.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_alembic_db_type.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_alembic_nullability.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_alembic_type_mapping.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_auto_migrate.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_composite_index.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_composite_unique.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_connection.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_connection_redaction.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_constraints.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_cross_emitter_parity.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_crud.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_db_backends.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_db_type_cross_emitter_parity.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_db_type_integration.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_db_type_typing.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_db_type_validation.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_deletion.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_deprecated_operator_inventory.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_deprecations.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_documentation_features.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_enum_cold_hydration.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_field_wrapper.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_framework_predicates.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_helpers.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_hydration.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_ir_vectors_contract.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_metaclass_internals.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_metadata.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_migrate_plan.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_models.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_named_connections_integration.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_one_to_one.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_query_builder.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_query_typing.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_raw_sql.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_refresh.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_relationship_engine.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_schema.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_schema_db_type_metadata.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_schema_enum_annotations.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_shadow_fk_types.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_shadow_reports.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_static_contracts.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_string_search.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_structural_types.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_transactions.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_typed_null_binds.py +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/uv.lock +0 -0
- {ferro_orm-0.12.1 → ferro_orm-0.12.3}/zensical.toml +0 -0
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.12.3 (2026-06-24)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- **session**: Reject close while transactions are active
|
|
9
|
+
([#128](https://github.com/syn54x/ferro-orm/pull/128),
|
|
10
|
+
[`e46f183`](https://github.com/syn54x/ferro-orm/commit/e46f183fba7a3bcbc635f7e66502a77aeb56861d))
|
|
11
|
+
|
|
12
|
+
- **session**: Serialize concurrent close teardown
|
|
13
|
+
([#129](https://github.com/syn54x/ferro-orm/pull/129),
|
|
14
|
+
[`7afa616`](https://github.com/syn54x/ferro-orm/commit/7afa6168abd2eb19b564fc1c2ab9956e03a20fe9))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## v0.12.2 (2026-06-23)
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
- Cross-context session teardown (Fixes #123) ([#127](https://github.com/syn54x/ferro-orm/pull/127),
|
|
22
|
+
[`6b9ae13`](https://github.com/syn54x/ferro-orm/commit/6b9ae1332becef88f14bdb4beae1d24e3a4104cd))
|
|
23
|
+
|
|
24
|
+
|
|
4
25
|
## v0.12.1 (2026-06-23)
|
|
5
26
|
|
|
6
27
|
### Bug Fixes
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Functions for managing database connections and the global model registry. `connect()` registers a (optionally named) connection pool; `reset_engine()` tears everything down; the registry helpers control schema creation and the identity map. Sessionized routing is exposed via `ferro.engines.session(name)` / `ferro.Session`. See the [Connections & Databases guide](../guide/connections.md).
|
|
4
4
|
|
|
5
|
+
::: ferro.Session
|
|
6
|
+
|
|
5
7
|
::: ferro.connect
|
|
6
8
|
|
|
7
9
|
::: ferro.PoolConfig
|
|
@@ -114,6 +114,39 @@ async with ferro.engines.session("analytics") as s:
|
|
|
114
114
|
|
|
115
115
|
Inside an active session context, convenience APIs (`User.all()`, `User.where(...)`, `ferro.execute(...)`) automatically bind to that session's connection.
|
|
116
116
|
|
|
117
|
+
### Request-scoped sessions
|
|
118
|
+
|
|
119
|
+
For short units of work (HTTP handlers, scripts, tests), use `async with`:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
async with ferro.engines.session("analytics"):
|
|
123
|
+
rows = await User.where(lambda t: t.active == True).all() # noqa: E712
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Long-lived sessions (GUI, app lifecycle)
|
|
127
|
+
|
|
128
|
+
When startup and shutdown run in different asyncio tasks (for example Textual `on_mount` / `on_unmount`), open a session handle at startup and close it explicitly at shutdown:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
class App:
|
|
132
|
+
async def on_mount(self) -> None:
|
|
133
|
+
self.session = ferro.engines.session("default")
|
|
134
|
+
await self.session.__aenter__()
|
|
135
|
+
|
|
136
|
+
async def on_unmount(self) -> None:
|
|
137
|
+
await self.session.close()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`Session.close()` is safe across asyncio contexts and idempotent. Prefer explicit `session=` routing when ambient context may be unavailable in the closing task.
|
|
141
|
+
|
|
142
|
+
Close raises `RuntimeError` if session-scoped transactions are still open — exit all `transaction()` blocks before closing the session.
|
|
143
|
+
|
|
144
|
+
After a cross-context close, the enter task may still hold a stale ambient session object until a new session is opened; ambient operations then raise `RuntimeError` with a session-closed message (including when `using=` names another connection). If nested sessions are open, close inner sessions from the same task or via cross-context `close()` before closing the outer handle in the original task.
|
|
145
|
+
|
|
146
|
+
Operations on a closed session handle (ambient or explicit `session=`) raise `RuntimeError` rather than falling back to legacy default routing.
|
|
147
|
+
|
|
148
|
+
`ferro.reset_engine()` tears down **all** connections and is intended for test isolation — not per-session app lifecycle teardown.
|
|
149
|
+
|
|
117
150
|
Legacy implicit default-connection routing (calling unqualified operations outside a session) is still temporarily supported for compatibility, but now emits a deprecation warning and is on the `v0.14.0` removal track.
|
|
118
151
|
Follow [Migrating to v0.12.0](../howto/migrating-to-v0-12-0.md) to remove these legacy call sites during the compatibility window.
|
|
119
152
|
|
|
@@ -40,6 +40,8 @@ async with ferro.engines.session("analytics"):
|
|
|
40
40
|
|
|
41
41
|
You can also pass an explicit session handle (`transaction(session=my_session)`) when ambient context is different and you need a deterministic override.
|
|
42
42
|
|
|
43
|
+
`Session.close()` raises `RuntimeError` while session-scoped transactions are still open. Exit all `transaction()` blocks before closing the session.
|
|
44
|
+
|
|
43
45
|
## Raw SQL Inside a Transaction
|
|
44
46
|
|
|
45
47
|
`transaction()` yields a `Transaction` handle exposing `execute` / `fetch_all` / `fetch_one` for raw SQL on the transaction's own connection — useful for Postgres GUCs (`set_config`, `SET LOCAL`), advisory locks, or any one-off statement that doesn't fit a model:
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Session-scoped runtime state for Ferro operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextvars
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ._core import close_session as _core_close_session
|
|
11
|
+
from ._core import open_session as _core_open_session
|
|
12
|
+
from .state import _CURRENT_SESSION
|
|
13
|
+
|
|
14
|
+
_SESSION_CLOSE_AMBIENT_MISMATCH = (
|
|
15
|
+
"Session close failed: ambient session does not match the closing session. "
|
|
16
|
+
"This usually indicates session lifecycle misuse in the current asyncio context."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class Session:
|
|
22
|
+
connection_name: str | None = None
|
|
23
|
+
session_id: str | None = None
|
|
24
|
+
_token: Any = field(default=None, repr=False, compare=False)
|
|
25
|
+
_enter_context: contextvars.Context | None = field(
|
|
26
|
+
default=None, repr=False, compare=False
|
|
27
|
+
)
|
|
28
|
+
_enter_task: asyncio.Task[Any] | None = field(
|
|
29
|
+
default=None, repr=False, compare=False
|
|
30
|
+
)
|
|
31
|
+
_close_lock: asyncio.Lock | None = field(default=None, repr=False, compare=False)
|
|
32
|
+
|
|
33
|
+
async def __aenter__(self) -> "Session":
|
|
34
|
+
self.session_id, resolved_name = _core_open_session(self.connection_name)
|
|
35
|
+
if self.connection_name is None:
|
|
36
|
+
self.connection_name = resolved_name
|
|
37
|
+
self._token = _CURRENT_SESSION.set(self)
|
|
38
|
+
self._enter_context = contextvars.copy_context()
|
|
39
|
+
self._enter_task = asyncio.current_task()
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
43
|
+
try:
|
|
44
|
+
await self.close()
|
|
45
|
+
except Exception as close_exc:
|
|
46
|
+
if exc_type is None:
|
|
47
|
+
raise
|
|
48
|
+
if exc is not None:
|
|
49
|
+
raise exc from close_exc
|
|
50
|
+
raise close_exc
|
|
51
|
+
|
|
52
|
+
async def close(self) -> None:
|
|
53
|
+
"""Close this session and release its runtime state.
|
|
54
|
+
|
|
55
|
+
Safe to call from a different asyncio context than ``__aenter__``.
|
|
56
|
+
Repeated calls are no-ops.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
RuntimeError: If the ambient session in this asyncio context does not
|
|
60
|
+
match this handle (same-context lifecycle misuse), or if
|
|
61
|
+
session-scoped transactions are still open.
|
|
62
|
+
"""
|
|
63
|
+
if self._close_lock is None:
|
|
64
|
+
self._close_lock = asyncio.Lock()
|
|
65
|
+
|
|
66
|
+
async with self._close_lock:
|
|
67
|
+
if self.session_id is None and self._token is None:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
self._assert_close_allowed()
|
|
71
|
+
|
|
72
|
+
if self.session_id is not None:
|
|
73
|
+
session_id = self.session_id
|
|
74
|
+
_core_close_session(session_id)
|
|
75
|
+
self.session_id = None
|
|
76
|
+
|
|
77
|
+
if self._token is None:
|
|
78
|
+
self._enter_context = None
|
|
79
|
+
self._enter_task = None
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
token = self._token
|
|
83
|
+
self._token = None
|
|
84
|
+
self._enter_context = None
|
|
85
|
+
self._enter_task = None
|
|
86
|
+
self._restore_ambient_session(token)
|
|
87
|
+
|
|
88
|
+
def _assert_close_allowed(self) -> None:
|
|
89
|
+
if self._token is None:
|
|
90
|
+
return
|
|
91
|
+
if asyncio.current_task() is not self._enter_task:
|
|
92
|
+
return
|
|
93
|
+
ambient = _CURRENT_SESSION.get()
|
|
94
|
+
if ambient is self:
|
|
95
|
+
return
|
|
96
|
+
entered_ambient = (
|
|
97
|
+
self._enter_context.get(_CURRENT_SESSION)
|
|
98
|
+
if self._enter_context is not None
|
|
99
|
+
else None
|
|
100
|
+
)
|
|
101
|
+
if entered_ambient is self and ambient is not None:
|
|
102
|
+
raise RuntimeError(_SESSION_CLOSE_AMBIENT_MISMATCH)
|
|
103
|
+
|
|
104
|
+
def _restore_ambient_session(self, token: Any) -> None:
|
|
105
|
+
ambient = _CURRENT_SESSION.get()
|
|
106
|
+
if ambient is not self:
|
|
107
|
+
try:
|
|
108
|
+
_CURRENT_SESSION.reset(token)
|
|
109
|
+
except ValueError:
|
|
110
|
+
return
|
|
111
|
+
raise RuntimeError(_SESSION_CLOSE_AMBIENT_MISMATCH)
|
|
112
|
+
try:
|
|
113
|
+
_CURRENT_SESSION.reset(token)
|
|
114
|
+
except ValueError:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
def query(self, model_cls):
|
|
118
|
+
from .query import Query
|
|
119
|
+
|
|
120
|
+
return Query(model_cls, session=self)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class EngineManager:
|
|
124
|
+
def session(self, name: str | None = None) -> Session:
|
|
125
|
+
return Session(connection_name=name)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
engines = EngineManager()
|
|
@@ -34,6 +34,16 @@ _LEGACY_DEFAULT_CONNECTION_REASON = (
|
|
|
34
34
|
"ORM/raw operations."
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
+
_SESSION_CLOSED_MESSAGE = (
|
|
38
|
+
"Session is closed. Open a new session with "
|
|
39
|
+
"`async with ferro.engines.session(...)` or pass an active session handle."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _ensure_active_session(session: SessionLike | None) -> None:
|
|
44
|
+
if session is not None and session.session_id is None:
|
|
45
|
+
raise RuntimeError(_SESSION_CLOSED_MESSAGE)
|
|
46
|
+
|
|
37
47
|
# Global registry for models (Python side)
|
|
38
48
|
_MODEL_REGISTRY_PY = {}
|
|
39
49
|
|
|
@@ -76,6 +86,11 @@ def resolve_operation_scope(
|
|
|
76
86
|
if explicit_session is None and using != effective_session.connection_name:
|
|
77
87
|
effective_session = None
|
|
78
88
|
|
|
89
|
+
if explicit_session is None and ambient_session is not None:
|
|
90
|
+
_ensure_active_session(ambient_session)
|
|
91
|
+
|
|
92
|
+
_ensure_active_session(effective_session)
|
|
93
|
+
|
|
79
94
|
session_id = effective_session.session_id if effective_session is not None else None
|
|
80
95
|
|
|
81
96
|
if tx_id is not None:
|
|
@@ -124,6 +139,11 @@ def resolve_transaction_scope(
|
|
|
124
139
|
if explicit_session is None and using != effective_session.connection_name:
|
|
125
140
|
effective_session = None
|
|
126
141
|
|
|
142
|
+
if explicit_session is None and ambient_session is not None:
|
|
143
|
+
_ensure_active_session(ambient_session)
|
|
144
|
+
|
|
145
|
+
_ensure_active_session(effective_session)
|
|
146
|
+
|
|
127
147
|
if parent_tx_id is not None:
|
|
128
148
|
# Nested tx route is always inherited from parent.
|
|
129
149
|
return parent_tx_id, None, (
|
|
@@ -9,8 +9,8 @@ use crate::backend::{
|
|
|
9
9
|
use crate::query::{QueryDef, query_def_from_ir_payload};
|
|
10
10
|
use crate::state::{
|
|
11
11
|
IDENTITY_MAP, MODEL_REGISTRY, SqlDialect, TRANSACTION_REGISTRY,
|
|
12
|
-
TransactionConnection, TransactionHandle, connection_for_route,
|
|
13
|
-
register_session, session_state, unregister_session,
|
|
12
|
+
TransactionConnection, TransactionHandle, connection_for_route, ensure_session_idle_for_close,
|
|
13
|
+
engine_for_connection, register_session, session_state, unregister_session,
|
|
14
14
|
};
|
|
15
15
|
use ferro_schema_ir::{IrEnvelope, QueryIrPayload};
|
|
16
16
|
use pyo3::prelude::*;
|
|
@@ -833,6 +833,7 @@ pub fn open_session(using: Option<String>) -> PyResult<(String, String)> {
|
|
|
833
833
|
|
|
834
834
|
#[pyfunction]
|
|
835
835
|
pub fn close_session(session_id: String) -> PyResult<()> {
|
|
836
|
+
ensure_session_idle_for_close(&session_id)?;
|
|
836
837
|
if !unregister_session(&session_id) {
|
|
837
838
|
return Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
|
|
838
839
|
"Session '{}' is not active",
|
|
@@ -199,6 +199,20 @@ pub fn session_state(session_id: &str) -> PyResult<Arc<SessionState>> {
|
|
|
199
199
|
})
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
pub(crate) const SESSION_CLOSE_ACTIVE_TRANSACTIONS_MSG: &str =
|
|
203
|
+
"Cannot close session while transactions are active. \
|
|
204
|
+
Exit all transaction() blocks before closing the session.";
|
|
205
|
+
|
|
206
|
+
pub fn ensure_session_idle_for_close(session_id: &str) -> PyResult<()> {
|
|
207
|
+
let session = session_state(session_id)?;
|
|
208
|
+
if !session.transaction_registry.is_empty() {
|
|
209
|
+
return Err(pyo3::exceptions::PyRuntimeError::new_err(
|
|
210
|
+
SESSION_CLOSE_ACTIVE_TRANSACTIONS_MSG,
|
|
211
|
+
));
|
|
212
|
+
}
|
|
213
|
+
Ok(())
|
|
214
|
+
}
|
|
215
|
+
|
|
202
216
|
/// A Rust-native representation of database values.
|
|
203
217
|
///
|
|
204
218
|
/// Used during the GIL-free parsing phase to move data from the database
|
|
@@ -262,3 +276,53 @@ impl RustValue {
|
|
|
262
276
|
}
|
|
263
277
|
}
|
|
264
278
|
}
|
|
279
|
+
|
|
280
|
+
#[cfg(test)]
|
|
281
|
+
mod session_close_tests {
|
|
282
|
+
use super::{TransactionHandle, register_session, unregister_session, SESSION_REGISTRY};
|
|
283
|
+
use crate::backend::EngineHandle;
|
|
284
|
+
use sqlx::sqlite::SqlitePoolOptions;
|
|
285
|
+
|
|
286
|
+
#[tokio::test]
|
|
287
|
+
async fn close_guard_uses_transaction_registry_emptiness() {
|
|
288
|
+
let session_id = register_session("default".to_string());
|
|
289
|
+
|
|
290
|
+
let pool = SqlitePoolOptions::new()
|
|
291
|
+
.max_connections(1)
|
|
292
|
+
.connect("sqlite::memory:")
|
|
293
|
+
.await
|
|
294
|
+
.expect("sqlite memory pool");
|
|
295
|
+
let engine = EngineHandle::new_sqlite(pool);
|
|
296
|
+
let conn = engine
|
|
297
|
+
.begin_transaction_connection()
|
|
298
|
+
.await
|
|
299
|
+
.expect("begin transaction connection");
|
|
300
|
+
|
|
301
|
+
{
|
|
302
|
+
let session = SESSION_REGISTRY
|
|
303
|
+
.get(&session_id)
|
|
304
|
+
.expect("session registered")
|
|
305
|
+
.value()
|
|
306
|
+
.clone();
|
|
307
|
+
session.transaction_registry.insert(
|
|
308
|
+
"tx-test".to_string(),
|
|
309
|
+
TransactionHandle::root(conn, "default".to_string()),
|
|
310
|
+
);
|
|
311
|
+
assert!(!session.transaction_registry.is_empty());
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
SESSION_REGISTRY
|
|
315
|
+
.get(&session_id)
|
|
316
|
+
.expect("session registered")
|
|
317
|
+
.transaction_registry
|
|
318
|
+
.clear();
|
|
319
|
+
assert!(
|
|
320
|
+
SESSION_REGISTRY
|
|
321
|
+
.get(&session_id)
|
|
322
|
+
.expect("session registered")
|
|
323
|
+
.transaction_registry
|
|
324
|
+
.is_empty()
|
|
325
|
+
);
|
|
326
|
+
assert!(unregister_session(&session_id));
|
|
327
|
+
}
|
|
328
|
+
}
|