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.
Files changed (229) hide show
  1. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/CHANGELOG.md +21 -0
  2. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/Cargo.lock +1 -1
  3. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/Cargo.toml +1 -1
  4. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/PKG-INFO +1 -1
  5. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/connection.md +2 -0
  6. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/connections.md +33 -0
  7. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/transactions.md +2 -0
  8. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/pyproject.toml +1 -1
  9. ferro_orm-0.12.3/src/ferro/session.py +128 -0
  10. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/state.py +20 -0
  11. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/operations.rs +3 -2
  12. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/state.rs +64 -0
  13. ferro_orm-0.12.3/tests/test_session.py +555 -0
  14. ferro_orm-0.12.1/src/ferro/session.py +0 -45
  15. ferro_orm-0.12.1/tests/test_session.py +0 -130
  16. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  18. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/PERMISSIONS.md +0 -0
  19. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/PYPI_CHECKLIST.md +0 -0
  20. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/PYPI_SETUP.md +0 -0
  21. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/generated/wheels.generated.yml +0 -0
  22. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/pull_request_template.md +0 -0
  23. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/workflows/ci.yml +0 -0
  24. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/workflows/packaging-smoke.yml +0 -0
  25. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/workflows/publish-docs.yml +0 -0
  26. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/workflows/publish.yml +0 -0
  27. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.github/workflows/release.yml +0 -0
  28. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.gitignore +0 -0
  29. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.pre-commit-config.yaml +0 -0
  30. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/.python-version +0 -0
  31. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/AGENTS.md +0 -0
  32. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/CONTRIBUTING.md +0 -0
  33. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/LICENSE +0 -0
  34. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/README.md +0 -0
  35. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/crates/ferro-migrate/Cargo.toml +0 -0
  36. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/crates/ferro-migrate/src/lib.rs +0 -0
  37. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/crates/ferro-schema-ir/Cargo.toml +0 -0
  38. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/crates/ferro-schema-ir/src/lib.rs +0 -0
  39. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
  40. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
  41. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
  42. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
  43. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
  44. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/multiple_databases.py +0 -0
  45. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/mutations.py +0 -0
  46. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/pagination.py +0 -0
  47. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/predicates.py +0 -0
  48. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/predicates_annotated.py +0 -0
  49. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/quickstart.py +0 -0
  50. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/quickstart_annotated.py +0 -0
  51. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/raw_sql.py +0 -0
  52. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/relationships.py +0 -0
  53. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/relationships_annotated.py +0 -0
  54. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/soft_deletes.py +0 -0
  55. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/soft_deletes_annotated.py +0 -0
  56. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/testing_conftest.py +0 -0
  57. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/timestamps.py +0 -0
  58. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/timestamps_annotated.py +0 -0
  59. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/examples/transactions.py +0 -0
  60. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/exceptions.md +0 -0
  61. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/fields.md +0 -0
  62. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/migrations.md +0 -0
  63. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/model.md +0 -0
  64. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/queries.md +0 -0
  65. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/raw-sql.md +0 -0
  66. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/relationships.md +0 -0
  67. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/api/transactions.md +0 -0
  68. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/changelog.md +0 -0
  69. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/architecture.md +0 -0
  70. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/backends.md +0 -0
  71. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/identity-map.md +0 -0
  72. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/performance.md +0 -0
  73. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/query-typing.md +0 -0
  74. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/concepts/type-safety.md +0 -0
  75. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/contributing.md +0 -0
  76. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/faq.md +0 -0
  77. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/getting-started/installation.md +0 -0
  78. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/getting-started/next-steps.md +0 -0
  79. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/getting-started/quickstart.md +0 -0
  80. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/migrations.md +0 -0
  81. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/models-and-fields.md +0 -0
  82. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/mutations.md +0 -0
  83. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/queries.md +0 -0
  84. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/raw-sql.md +0 -0
  85. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/guide/relationships.md +0 -0
  86. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
  87. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/migrating-to-v0-12-0.md +0 -0
  88. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/multiple-databases.md +0 -0
  89. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/pagination.md +0 -0
  90. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/soft-deletes.md +0 -0
  91. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/testing.md +0 -0
  92. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/howto/timestamps.md +0 -0
  93. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/index.md +0 -0
  94. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/roadmap.md +0 -0
  95. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/stylesheets/extra.css +0 -0
  96. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/pages/why-ferro.md +0 -0
  97. {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
  98. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
  99. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
  100. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
  101. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
  102. {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
  103. {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
  104. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
  105. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/ir-first-migration-guide.md +0 -0
  106. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/plans/ir-first-release-checklist.md +0 -0
  107. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/rfc/ir-contracts-v1.md +0 -0
  108. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/README.md +0 -0
  109. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
  110. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
  111. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
  112. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
  113. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
  114. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
  115. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
  116. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
  117. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
  118. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
  119. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
  120. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/foreign-key-index.md +0 -0
  121. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
  122. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/ir-invariants.md +0 -0
  123. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
  124. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
  125. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/docs/solutions/patterns/typed-null-binds.md +0 -0
  126. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/justfile +0 -0
  127. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/scripts/demo_queries.py +0 -0
  128. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/scripts/demo_queries_pre_v012.py +0 -0
  129. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/backend.rs +0 -0
  130. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/codec.rs +0 -0
  131. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/connection.rs +0 -0
  132. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/__init__.py +0 -0
  133. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/_annotation_utils.py +0 -0
  134. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/_core.pyi +0 -0
  135. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/_deprecations.py +0 -0
  136. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/_shadow_fk_types.py +0 -0
  137. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/base.py +0 -0
  138. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/composite_indexes.py +0 -0
  139. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/composite_uniques.py +0 -0
  140. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/exceptions.py +0 -0
  141. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/fields.py +0 -0
  142. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/ir/__init__.py +0 -0
  143. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/ir/compiler.py +0 -0
  144. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/metaclass.py +0 -0
  145. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/migrations/__init__.py +0 -0
  146. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/migrations/alembic.py +0 -0
  147. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/models.py +0 -0
  148. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/py.typed +0 -0
  149. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/query/__init__.py +0 -0
  150. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/query/builder.py +0 -0
  151. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/query/nodes.py +0 -0
  152. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/raw.py +0 -0
  153. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/relations/__init__.py +0 -0
  154. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/relations/descriptors.py +0 -0
  155. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/ferro/schema_metadata.py +0 -0
  156. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/hydration.rs +0 -0
  157. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/introspect.rs +0 -0
  158. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/lib.rs +0 -0
  159. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/migrate.rs +0 -0
  160. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/query.rs +0 -0
  161. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/schema.rs +0 -0
  162. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/src/schema_bind.rs +0 -0
  163. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/__init__.py +0 -0
  164. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/conftest.py +0 -0
  165. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/db_backends.py +0 -0
  166. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/ir_vectors/README.md +0 -0
  167. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
  168. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/ir_vectors/query_user_compound_v1.json +0 -0
  169. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
  170. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
  171. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/shadow_reports/postgres.json +0 -0
  172. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/fixtures/shadow_reports/sqlite.json +0 -0
  173. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_aggregation.py +0 -0
  174. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_alembic_autogenerate.py +0 -0
  175. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_alembic_bridge.py +0 -0
  176. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_alembic_db_type.py +0 -0
  177. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_alembic_nullability.py +0 -0
  178. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_alembic_type_mapping.py +0 -0
  179. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_auto_migrate.py +0 -0
  180. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_bulk_update.py +0 -0
  181. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_composite_index.py +0 -0
  182. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_composite_unique.py +0 -0
  183. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_connection.py +0 -0
  184. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_connection_redaction.py +0 -0
  185. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_constraints.py +0 -0
  186. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_cross_emitter_parity.py +0 -0
  187. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_crud.py +0 -0
  188. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_db_backends.py +0 -0
  189. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_db_type_cross_emitter_parity.py +0 -0
  190. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_db_type_integration.py +0 -0
  191. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_db_type_typing.py +0 -0
  192. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_db_type_validation.py +0 -0
  193. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_deletion.py +0 -0
  194. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_deprecated_operator_inventory.py +0 -0
  195. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_deprecations.py +0 -0
  196. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_docs_examples.py +0 -0
  197. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_documentation_features.py +0 -0
  198. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_enum_cold_hydration.py +0 -0
  199. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_field_wrapper.py +0 -0
  200. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_framework_predicates.py +0 -0
  201. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_helpers.py +0 -0
  202. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_hydration.py +0 -0
  203. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_ir_vectors_contract.py +0 -0
  204. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_metaclass_internals.py +0 -0
  205. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_metadata.py +0 -0
  206. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_migrate_plan.py +0 -0
  207. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_models.py +0 -0
  208. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_named_connections_integration.py +0 -0
  209. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_one_to_one.py +0 -0
  210. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_query_builder.py +0 -0
  211. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_query_typing.py +0 -0
  212. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_raw_sql.py +0 -0
  213. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_refresh.py +0 -0
  214. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_relationship_engine.py +0 -0
  215. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_schema.py +0 -0
  216. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_schema_constraints.py +0 -0
  217. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_schema_db_type_metadata.py +0 -0
  218. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_schema_enum_annotations.py +0 -0
  219. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_shadow_fk_types.py +0 -0
  220. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_shadow_reports.py +0 -0
  221. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
  222. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_static_contracts.py +0 -0
  223. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_string_search.py +0 -0
  224. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_structural_types.py +0 -0
  225. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_temporal_types.py +0 -0
  226. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_transactions.py +0 -0
  227. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/tests/test_typed_null_binds.py +0 -0
  228. {ferro_orm-0.12.1 → ferro_orm-0.12.3}/uv.lock +0 -0
  229. {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
@@ -288,7 +288,7 @@ dependencies = [
288
288
 
289
289
  [[package]]
290
290
  name = "ferro"
291
- version = "0.12.1"
291
+ version = "0.12.3"
292
292
  dependencies = [
293
293
  "dashmap",
294
294
  "ferro-migrate",
@@ -3,7 +3,7 @@ members = ["crates/ferro-schema-ir", "crates/ferro-migrate"]
3
3
 
4
4
  [package]
5
5
  name = "ferro"
6
- version = "0.12.1"
6
+ version = "0.12.3"
7
7
  edition = "2024"
8
8
  readme = "README.md"
9
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ferro-orm
3
- Version: 0.12.1
3
+ Version: 0.12.3
4
4
  Requires-Dist: pydantic>=2.0
5
5
  Requires-Dist: alembic>=1.18.1 ; extra == 'alembic'
6
6
  Requires-Dist: sqlalchemy>=2.0.46 ; extra == 'alembic'
@@ -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:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ferro-orm"
3
- version = "0.12.1"
3
+ version = "0.12.3"
4
4
  description = "A high-performance, Rust-backed ORM for Python."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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, engine_for_connection,
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
+ }