ferro-orm 0.13.0__tar.gz → 0.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (398) hide show
  1. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/workflows/ci.yml +5 -91
  2. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/workflows/release.yml +19 -0
  3. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/AGENTS.md +35 -11
  4. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/CHANGELOG.md +200 -0
  5. ferro_orm-0.15.0/CONTEXT.md +89 -0
  6. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/Cargo.lock +160 -26
  7. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/Cargo.toml +9 -2
  8. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/PKG-INFO +2 -2
  9. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/README.md +1 -1
  10. ferro_orm-0.15.0/benchmarks/README.md +133 -0
  11. ferro_orm-0.15.0/benchmarks/__init__.py +11 -0
  12. ferro_orm-0.15.0/benchmarks/__main__.py +10 -0
  13. ferro_orm-0.15.0/benchmarks/backends.py +88 -0
  14. ferro_orm-0.15.0/benchmarks/baselines/postgres.json +49 -0
  15. ferro_orm-0.15.0/benchmarks/baselines/sqlite.json +49 -0
  16. ferro_orm-0.15.0/benchmarks/compare.py +98 -0
  17. ferro_orm-0.15.0/benchmarks/harness.py +99 -0
  18. ferro_orm-0.15.0/benchmarks/model.py +84 -0
  19. ferro_orm-0.15.0/benchmarks/run.py +270 -0
  20. ferro_orm-0.15.0/crates/ferro-ddl-lowering/src/lib.rs +1600 -0
  21. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/crates/ferro-migrate/src/emit.rs +155 -83
  22. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/crates/ferro-migrate/src/tests.rs +291 -45
  23. ferro_orm-0.15.0/crates/ferro-schema-ir/src/lib.rs +603 -0
  24. ferro_orm-0.15.0/docs/adr/0001-resolved-epoch-registration.md +69 -0
  25. ferro_orm-0.15.0/docs/adr/0002-unified-column-fact-derivation.md +83 -0
  26. ferro_orm-0.15.0/docs/adr/0003-path-blind-db-type-validation.md +47 -0
  27. ferro_orm-0.15.0/docs/adr/0004-jsonb-postgres-only-canonical.md +69 -0
  28. ferro_orm-0.15.0/docs/adr/0005-jsonb-default-on-postgres.md +34 -0
  29. ferro_orm-0.15.0/docs/adr/0006-relation-traversal-inner-joins.md +42 -0
  30. ferro_orm-0.15.0/docs/adr/0007-materialization-plan-complete-instances.md +53 -0
  31. ferro_orm-0.15.0/docs/adr/0008-populated-relations-include.md +74 -0
  32. ferro_orm-0.15.0/docs/agents/domain.md +51 -0
  33. ferro_orm-0.15.0/docs/agents/issue-tracker.md +45 -0
  34. ferro_orm-0.15.0/docs/agents/triage-labels.md +15 -0
  35. ferro_orm-0.15.0/docs/architecture-review-2026-07-08.html +492 -0
  36. ferro_orm-0.15.0/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +150 -0
  37. ferro_orm-0.15.0/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +222 -0
  38. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/examples/multiple_databases.py +9 -7
  39. ferro_orm-0.15.0/docs/examples/mutations.py +112 -0
  40. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/examples/pagination.py +11 -9
  41. ferro_orm-0.15.0/docs/examples/partial_selects.py +107 -0
  42. ferro_orm-0.15.0/docs/examples/partial_selects_annotated.py +43 -0
  43. ferro_orm-0.15.0/docs/examples/populated_relations.py +201 -0
  44. ferro_orm-0.15.0/docs/examples/populated_relations_annotated.py +59 -0
  45. ferro_orm-0.15.0/docs/examples/predicates.py +84 -0
  46. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/examples/predicates_annotated.py +12 -10
  47. ferro_orm-0.15.0/docs/examples/quickstart.py +112 -0
  48. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/examples/quickstart_annotated.py +7 -6
  49. ferro_orm-0.15.0/docs/examples/raw_sql.py +45 -0
  50. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/examples/relationships.py +37 -36
  51. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/examples/relationships_annotated.py +18 -17
  52. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/examples/soft_deletes.py +11 -10
  53. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/examples/soft_deletes_annotated.py +8 -7
  54. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/examples/testing_conftest.py +3 -2
  55. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/examples/timestamps.py +10 -9
  56. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/examples/timestamps_annotated.py +7 -6
  57. ferro_orm-0.15.0/docs/examples/transactions.py +59 -0
  58. ferro_orm-0.15.0/docs/examples/traversal.py +302 -0
  59. ferro_orm-0.15.0/docs/examples/traversal_annotated.py +115 -0
  60. ferro_orm-0.15.0/docs/pages/api/exceptions.md +57 -0
  61. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/api/migrations.md +0 -2
  62. ferro_orm-0.15.0/docs/pages/api/queries.md +37 -0
  63. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/concepts/architecture.md +3 -5
  64. ferro_orm-0.15.0/docs/pages/concepts/identity-map.md +227 -0
  65. ferro_orm-0.15.0/docs/pages/concepts/query-typing.md +127 -0
  66. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/concepts/type-safety.md +2 -2
  67. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/faq.md +3 -1
  68. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/getting-started/quickstart.md +3 -3
  69. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/guide/connections.md +2 -2
  70. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/guide/models-and-fields.md +128 -3
  71. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/guide/mutations.md +72 -16
  72. ferro_orm-0.15.0/docs/pages/guide/queries.md +606 -0
  73. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/guide/relationships.md +16 -2
  74. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/howto/migrate-from-sqlalchemy.md +4 -4
  75. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/howto/multiple-databases.md +5 -3
  76. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/howto/pagination.md +2 -2
  77. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/howto/soft-deletes.md +1 -1
  78. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/howto/testing.md +7 -5
  79. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/howto/timestamps.md +1 -1
  80. ferro_orm-0.15.0/docs/pages/howto/upgrade-guide.md +490 -0
  81. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/roadmap.md +2 -3
  82. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +4 -4
  83. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +5 -3
  84. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +3 -6
  85. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +1 -1
  86. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +1 -1
  87. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +2 -1
  88. ferro_orm-0.15.0/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +449 -0
  89. ferro_orm-0.15.0/docs/plans/2026-07-02-002-feat-ff-a-172-typed-exceptions-plan.md +37 -0
  90. ferro_orm-0.15.0/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +105 -0
  91. ferro_orm-0.15.0/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +159 -0
  92. ferro_orm-0.15.0/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +207 -0
  93. ferro_orm-0.15.0/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +1474 -0
  94. ferro_orm-0.15.0/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +267 -0
  95. ferro_orm-0.15.0/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +1584 -0
  96. ferro_orm-0.15.0/docs/plans/2026-07-06-009-ff-f-query-builder-1.0-design.md +152 -0
  97. ferro_orm-0.15.0/docs/plans/2026-07-06-010-ff-f-query-builder-1.0-plan.md +1290 -0
  98. ferro_orm-0.15.0/docs/plans/2026-07-06-011-ff-g-hardening-design.md +268 -0
  99. ferro_orm-0.15.0/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +985 -0
  100. ferro_orm-0.15.0/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +193 -0
  101. ferro_orm-0.15.0/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +1203 -0
  102. ferro_orm-0.15.0/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +518 -0
  103. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/ir-first-migration-guide.md +11 -0
  104. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +10 -1
  105. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +13 -1
  106. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/patterns/cross-emitter-ddl-parity.md +11 -31
  107. ferro_orm-0.15.0/docs/solutions/patterns/derived-type-and-naming-decision-table.md +161 -0
  108. ferro_orm-0.15.0/docs/solutions/patterns/persistence-state.md +76 -0
  109. ferro_orm-0.15.0/justfile +23 -0
  110. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/pyproject.toml +2 -4
  111. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/backend.rs +207 -16
  112. ferro_orm-0.15.0/src/codec.rs +535 -0
  113. ferro_orm-0.15.0/src/codec_plan.rs +613 -0
  114. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/connection.rs +48 -29
  115. ferro_orm-0.15.0/src/errors.rs +181 -0
  116. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/__init__.py +158 -27
  117. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/_annotation_utils.py +92 -0
  118. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/_core.pyi +79 -82
  119. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/_deprecations.py +0 -19
  120. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/_shadow_fk_types.py +7 -44
  121. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/base.py +2 -0
  122. ferro_orm-0.15.0/src/ferro/columns.py +494 -0
  123. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/composite_indexes.py +41 -39
  124. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/composite_uniques.py +27 -22
  125. ferro_orm-0.15.0/src/ferro/exceptions.py +134 -0
  126. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/ir/__init__.py +2 -0
  127. ferro_orm-0.15.0/src/ferro/ir/compiler.py +445 -0
  128. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/metaclass.py +121 -88
  129. ferro_orm-0.15.0/src/ferro/migrations/alembic.py +273 -0
  130. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/models.py +278 -178
  131. ferro_orm-0.15.0/src/ferro/query/__init__.py +26 -0
  132. ferro_orm-0.15.0/src/ferro/query/builder.py +1510 -0
  133. ferro_orm-0.15.0/src/ferro/query/nodes.py +581 -0
  134. ferro_orm-0.15.0/src/ferro/query/rows.py +90 -0
  135. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/raw.py +17 -24
  136. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/relations/__init__.py +71 -55
  137. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/relations/descriptors.py +7 -26
  138. ferro_orm-0.15.0/src/ferro/state.py +355 -0
  139. ferro_orm-0.15.0/src/hydration.rs +439 -0
  140. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/introspect.rs +4 -3
  141. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/lib.rs +29 -15
  142. ferro_orm-0.15.0/src/migrate.rs +618 -0
  143. ferro_orm-0.15.0/src/naming_ffi.rs +92 -0
  144. ferro_orm-0.15.0/src/operations.rs +5535 -0
  145. ferro_orm-0.15.0/src/query.rs +1607 -0
  146. ferro_orm-0.15.0/src/schema.rs +587 -0
  147. ferro_orm-0.15.0/src/schema_bind.rs +23 -0
  148. ferro_orm-0.15.0/src/state.rs +1000 -0
  149. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/conftest.py +67 -0
  150. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/fixtures/ir_vectors/README.md +7 -2
  151. ferro_orm-0.15.0/tests/fixtures/ir_vectors/query_transaction_include_v4.json +48 -0
  152. ferro_orm-0.15.0/tests/fixtures/ir_vectors/query_transaction_left_join_v4.json +64 -0
  153. ferro_orm-0.15.0/tests/fixtures/ir_vectors/query_transaction_record_v4.json +50 -0
  154. ferro_orm-0.15.0/tests/fixtures/ir_vectors/query_transaction_traversal_v4.json +91 -0
  155. ferro_orm-0.13.0/tests/fixtures/ir_vectors/query_user_compound_v1.json → ferro_orm-0.15.0/tests/fixtures/ir_vectors/query_user_compound_v4.json +19 -8
  156. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +118 -7
  157. ferro_orm-0.15.0/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +46 -0
  158. ferro_orm-0.15.0/tests/static_fixtures/bad_includes.py +36 -0
  159. ferro_orm-0.15.0/tests/static_fixtures/bad_predicates.py +45 -0
  160. ferro_orm-0.15.0/tests/static_fixtures/bad_projections.py +45 -0
  161. ferro_orm-0.15.0/tests/static_fixtures/good_includes.py +44 -0
  162. ferro_orm-0.15.0/tests/static_fixtures/good_predicates.py +87 -0
  163. ferro_orm-0.15.0/tests/test_aggregation.py +74 -0
  164. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_alembic_autogenerate.py +13 -10
  165. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_alembic_bridge.py +25 -51
  166. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_alembic_db_type.py +23 -0
  167. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_alembic_nullability.py +6 -10
  168. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_alembic_type_mapping.py +4 -2
  169. ferro_orm-0.15.0/tests/test_annotation_utils.py +24 -0
  170. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_auto_migrate.py +463 -273
  171. ferro_orm-0.15.0/tests/test_bulk_install.py +160 -0
  172. ferro_orm-0.15.0/tests/test_bulk_update.py +66 -0
  173. ferro_orm-0.15.0/tests/test_catalog_cache.py +244 -0
  174. ferro_orm-0.15.0/tests/test_codec_plan.py +81 -0
  175. ferro_orm-0.15.0/tests/test_column_specs.py +106 -0
  176. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_composite_index.py +66 -58
  177. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_composite_unique.py +19 -34
  178. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_connection.py +94 -33
  179. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_connection_redaction.py +2 -2
  180. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_constraints.py +6 -7
  181. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_cross_emitter_parity.py +63 -74
  182. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_crud.py +86 -59
  183. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_db_type_cross_emitter_parity.py +192 -9
  184. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_db_type_integration.py +56 -52
  185. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_db_type_typing.py +14 -0
  186. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_db_type_validation.py +150 -1
  187. ferro_orm-0.15.0/tests/test_deletion.py +90 -0
  188. ferro_orm-0.15.0/tests/test_deprecations.py +52 -0
  189. ferro_orm-0.15.0/tests/test_documentation_features.py +874 -0
  190. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_enum_cold_hydration.py +15 -12
  191. ferro_orm-0.15.0/tests/test_exception_mapping.py +160 -0
  192. ferro_orm-0.15.0/tests/test_exceptions.py +126 -0
  193. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_field_wrapper.py +17 -11
  194. ferro_orm-0.15.0/tests/test_fk_target_pk.py +73 -0
  195. ferro_orm-0.15.0/tests/test_generation_counter_dirty_tracking.py +150 -0
  196. ferro_orm-0.15.0/tests/test_helpers.py +136 -0
  197. ferro_orm-0.15.0/tests/test_hydration.py +204 -0
  198. ferro_orm-0.15.0/tests/test_hydration_equivalence.py +275 -0
  199. ferro_orm-0.15.0/tests/test_identity_memory.py +80 -0
  200. ferro_orm-0.15.0/tests/test_identity_refresh.py +79 -0
  201. ferro_orm-0.15.0/tests/test_identity_scoped_invalidation.py +86 -0
  202. ferro_orm-0.15.0/tests/test_identity_weakref.py +37 -0
  203. ferro_orm-0.15.0/tests/test_import_budget.py +36 -0
  204. ferro_orm-0.15.0/tests/test_include.py +820 -0
  205. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_ir_vectors_contract.py +145 -39
  206. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_metadata.py +25 -22
  207. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_migrate_plan.py +203 -15
  208. ferro_orm-0.15.0/tests/test_model_identity.py +234 -0
  209. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_models.py +4 -1
  210. ferro_orm-0.15.0/tests/test_mutation_pagination_guard.py +99 -0
  211. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_named_connections_integration.py +58 -34
  212. ferro_orm-0.15.0/tests/test_naming_single_source.py +315 -0
  213. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_one_to_one.py +22 -20
  214. ferro_orm-0.15.0/tests/test_operation_seam_sync.py +269 -0
  215. ferro_orm-0.15.0/tests/test_partial_selects.py +474 -0
  216. ferro_orm-0.15.0/tests/test_provisional_import.py +231 -0
  217. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_query_builder.py +121 -113
  218. ferro_orm-0.15.0/tests/test_query_column_validation.py +153 -0
  219. ferro_orm-0.15.0/tests/test_query_immutability.py +101 -0
  220. ferro_orm-0.15.0/tests/test_query_joins.py +1177 -0
  221. ferro_orm-0.15.0/tests/test_query_typing.py +173 -0
  222. ferro_orm-0.15.0/tests/test_raw_sql.py +528 -0
  223. ferro_orm-0.15.0/tests/test_refresh.py +55 -0
  224. ferro_orm-0.15.0/tests/test_registry_entrypoints.py +294 -0
  225. ferro_orm-0.15.0/tests/test_relation_specs.py +175 -0
  226. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_relationship_engine.py +10 -2
  227. ferro_orm-0.15.0/tests/test_relationship_resolution_errors.py +55 -0
  228. ferro_orm-0.15.0/tests/test_route_single_site.py +33 -0
  229. ferro_orm-0.15.0/tests/test_routing_errors.py +64 -0
  230. ferro_orm-0.15.0/tests/test_save_pk_edges.py +53 -0
  231. ferro_orm-0.15.0/tests/test_save_semantics.py +358 -0
  232. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_schema.py +16 -15
  233. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_schema_db_type_metadata.py +14 -14
  234. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_schema_enum_annotations.py +2 -3
  235. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_session.py +16 -20
  236. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_shadow_fk_types.py +58 -54
  237. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_sqlite_alembic_reconnect_hydration.py +21 -11
  238. ferro_orm-0.15.0/tests/test_static_contracts.py +223 -0
  239. ferro_orm-0.15.0/tests/test_string_search.py +54 -0
  240. ferro_orm-0.15.0/tests/test_structural_types.py +524 -0
  241. ferro_orm-0.15.0/tests/test_temporal_types.py +75 -0
  242. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_transactions.py +134 -117
  243. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_typed_null_binds.py +130 -115
  244. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_typed_save_bind.py +75 -67
  245. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/uv.lock +28 -1
  246. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/zensical.toml +2 -2
  247. ferro_orm-0.13.0/crates/ferro-ddl-lowering/src/lib.rs +0 -875
  248. ferro_orm-0.13.0/crates/ferro-schema-ir/src/lib.rs +0 -294
  249. ferro_orm-0.13.0/docs/examples/mutations.py +0 -49
  250. ferro_orm-0.13.0/docs/examples/predicates.py +0 -92
  251. ferro_orm-0.13.0/docs/examples/quickstart.py +0 -111
  252. ferro_orm-0.13.0/docs/examples/raw_sql.py +0 -43
  253. ferro_orm-0.13.0/docs/examples/transactions.py +0 -58
  254. ferro_orm-0.13.0/docs/pages/api/exceptions.md +0 -5
  255. ferro_orm-0.13.0/docs/pages/api/queries.md +0 -11
  256. ferro_orm-0.13.0/docs/pages/concepts/identity-map.md +0 -151
  257. ferro_orm-0.13.0/docs/pages/concepts/query-typing.md +0 -106
  258. ferro_orm-0.13.0/docs/pages/guide/queries.md +0 -177
  259. ferro_orm-0.13.0/docs/pages/howto/migrating-to-v0-12-0.md +0 -153
  260. ferro_orm-0.13.0/justfile +0 -11
  261. ferro_orm-0.13.0/src/codec.rs +0 -611
  262. ferro_orm-0.13.0/src/ferro/exceptions.py +0 -17
  263. ferro_orm-0.13.0/src/ferro/ir/compiler.py +0 -430
  264. ferro_orm-0.13.0/src/ferro/migrations/alembic.py +0 -569
  265. ferro_orm-0.13.0/src/ferro/query/__init__.py +0 -14
  266. ferro_orm-0.13.0/src/ferro/query/builder.py +0 -576
  267. ferro_orm-0.13.0/src/ferro/query/nodes.py +0 -399
  268. ferro_orm-0.13.0/src/ferro/schema_metadata.py +0 -167
  269. ferro_orm-0.13.0/src/ferro/state.py +0 -166
  270. ferro_orm-0.13.0/src/hydration.rs +0 -85
  271. ferro_orm-0.13.0/src/migrate.rs +0 -2312
  272. ferro_orm-0.13.0/src/operations.rs +0 -3617
  273. ferro_orm-0.13.0/src/query.rs +0 -917
  274. ferro_orm-0.13.0/src/schema.rs +0 -569
  275. ferro_orm-0.13.0/src/schema_bind.rs +0 -49
  276. ferro_orm-0.13.0/src/state.rs +0 -415
  277. ferro_orm-0.13.0/tests/fixtures/shadow_reports/postgres.json +0 -66
  278. ferro_orm-0.13.0/tests/fixtures/shadow_reports/sqlite.json +0 -66
  279. ferro_orm-0.13.0/tests/test_aggregation.py +0 -70
  280. ferro_orm-0.13.0/tests/test_bulk_update.py +0 -62
  281. ferro_orm-0.13.0/tests/test_deletion.py +0 -87
  282. ferro_orm-0.13.0/tests/test_deprecated_operator_inventory.py +0 -51
  283. ferro_orm-0.13.0/tests/test_deprecations.py +0 -74
  284. ferro_orm-0.13.0/tests/test_documentation_features.py +0 -833
  285. ferro_orm-0.13.0/tests/test_framework_predicates.py +0 -72
  286. ferro_orm-0.13.0/tests/test_helpers.py +0 -129
  287. ferro_orm-0.13.0/tests/test_hydration.py +0 -167
  288. ferro_orm-0.13.0/tests/test_query_typing.py +0 -295
  289. ferro_orm-0.13.0/tests/test_raw_sql.py +0 -493
  290. ferro_orm-0.13.0/tests/test_refresh.py +0 -53
  291. ferro_orm-0.13.0/tests/test_shadow_reports.py +0 -134
  292. ferro_orm-0.13.0/tests/test_static_contracts.py +0 -8
  293. ferro_orm-0.13.0/tests/test_string_search.py +0 -52
  294. ferro_orm-0.13.0/tests/test_structural_types.py +0 -428
  295. ferro_orm-0.13.0/tests/test_temporal_types.py +0 -71
  296. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  297. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  298. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/PERMISSIONS.md +0 -0
  299. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/PYPI_CHECKLIST.md +0 -0
  300. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/PYPI_SETUP.md +0 -0
  301. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/generated/wheels.generated.yml +0 -0
  302. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/pull_request_template.md +0 -0
  303. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/workflows/packaging-smoke.yml +0 -0
  304. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/workflows/publish-docs.yml +0 -0
  305. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.github/workflows/publish.yml +0 -0
  306. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.gitignore +0 -0
  307. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.pre-commit-config.yaml +0 -0
  308. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/.python-version +0 -0
  309. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/CONTRIBUTING.md +0 -0
  310. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/LICENSE +0 -0
  311. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
  312. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/crates/ferro-migrate/Cargo.toml +0 -0
  313. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/crates/ferro-migrate/src/lib.rs +0 -0
  314. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/crates/ferro-schema-ir/Cargo.toml +0 -0
  315. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
  316. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
  317. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
  318. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
  319. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
  320. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
  321. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
  322. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
  323. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-06-26-ir-p8.5-143-db-type-drop-requirements.md +0 -0
  324. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
  325. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-06-28-ir-p8.5-141-single-schema-ir-producer-requirements.md +0 -0
  326. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-06-29-ir-p8.6-146-dialect-enum-unification-requirements.md +0 -0
  327. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-06-29-ir-p8.6-153-create-path-ir-requirements.md +0 -0
  328. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
  329. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
  330. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-07-01-ir-p8.6-154-datetime-tz-coarseness-requirements.md +0 -0
  331. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-07-01-ir-p8.6-162-typed-save-bind-requirements.md +0 -0
  332. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/brainstorms/2026-07-01-ir-p8.6-165-blob-introspection-false-positive-requirements.md +0 -0
  333. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/api/connection.md +0 -0
  334. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/api/fields.md +0 -0
  335. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/api/model.md +0 -0
  336. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/api/raw-sql.md +0 -0
  337. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/api/relationships.md +0 -0
  338. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/api/transactions.md +0 -0
  339. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/changelog.md +0 -0
  340. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/concepts/backends.md +0 -0
  341. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/concepts/performance.md +0 -0
  342. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/contributing.md +0 -0
  343. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/getting-started/installation.md +0 -0
  344. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/getting-started/next-steps.md +0 -0
  345. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/guide/migrations.md +0 -0
  346. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/guide/raw-sql.md +0 -0
  347. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/guide/transactions.md +0 -0
  348. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/index.md +0 -0
  349. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/stylesheets/extra.css +0 -0
  350. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/pages/why-ferro.md +0 -0
  351. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
  352. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
  353. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-06-24-001-feat-ir-p8-119-wire-automigrate-plan.md +0 -0
  354. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-06-25-001-feat-ir-p8-120-parity-gate-plan.md +0 -0
  355. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-06-26-001-feat-ir-p8.5-140-shared-lowering-plan.md +0 -0
  356. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-06-26-002-feat-ir-p8.5-143-db-type-drop-plan.md +0 -0
  357. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-06-26-003-feat-ir-p8.5-144-reconcile-indexes-plan.md +0 -0
  358. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-06-28-001-feat-ir-p8.5-141-single-schema-ir-producer-plan.md +0 -0
  359. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-06-29-001-refactor-ir-p8.6-153-create-path-unification-plan.md +0 -0
  360. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-06-29-002-refactor-ir-p8.6-146-dialect-enum-unification-plan.md +0 -0
  361. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-06-30-001-refactor-ir-p8.6-158-check-renderer-plan.md +0 -0
  362. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-06-30-002-fix-ir-p8.6-155-deferred-annotations-plan.md +0 -0
  363. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-07-01-001-feat-ir-p8.6-162-typed-save-bind-plan.md +0 -0
  364. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-07-01-002-fix-ir-p8.6-154-datetime-tz-coarseness-plan.md +0 -0
  365. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/2026-07-01-003-fix-ir-p8.6-165-blob-introspection-plan.md +0 -0
  366. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/plans/ir-first-release-checklist.md +0 -0
  367. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/rfc/ir-contracts-v1.md +0 -0
  368. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/README.md +0 -0
  369. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
  370. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
  371. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
  372. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
  373. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
  374. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
  375. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
  376. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
  377. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
  378. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/patterns/foreign-key-index.md +0 -0
  379. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
  380. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/patterns/ir-invariants.md +0 -0
  381. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
  382. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
  383. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/docs/solutions/patterns/typed-null-binds.md +0 -0
  384. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/scripts/demo_queries.py +0 -0
  385. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/scripts/demo_queries_pre_v012.py +0 -0
  386. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/_bind_payload.py +0 -0
  387. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/fields.py +0 -0
  388. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/migrations/__init__.py +0 -0
  389. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/py.typed +0 -0
  390. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/src/ferro/session.py +0 -0
  391. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/__init__.py +0 -0
  392. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/db_backends.py +0 -0
  393. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
  394. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
  395. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_db_backends.py +0 -0
  396. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_docs_examples.py +0 -0
  397. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_metaclass_internals.py +0 -0
  398. {ferro_orm-0.13.0 → ferro_orm-0.15.0}/tests/test_schema_constraints.py +0 -0
@@ -15,33 +15,6 @@ permissions:
15
15
  contents: read
16
16
 
17
17
  jobs:
18
- changed-shadow-paths:
19
- name: Detect Shadow-Report Paths
20
- runs-on: ubuntu-latest
21
- if: github.event_name == 'pull_request'
22
- outputs:
23
- requires_shadow_reports: ${{ steps.filter.outputs.shadow }}
24
- steps:
25
- - name: Checkout repository
26
- uses: actions/checkout@v4
27
-
28
- - name: Detect planner/IR path changes
29
- id: filter
30
- uses: dorny/paths-filter@v3
31
- with:
32
- filters: |
33
- shadow:
34
- - 'src/operations.rs'
35
- - 'src/query.rs'
36
- - 'src/schema.rs'
37
- - 'src/migrate.rs'
38
- - 'src/backend.rs'
39
- - 'src/connection.rs'
40
- - 'src/ferro/ir/**'
41
- - 'crates/ferro-schema-ir/**'
42
- - 'tests/test_shadow_reports.py'
43
- - 'tests/fixtures/shadow_reports/**'
44
-
45
18
  lint-and-format:
46
19
  name: Lint & Format (Pre-commit / Prek)
47
20
  runs-on: ubuntu-latest
@@ -152,6 +125,10 @@ jobs:
152
125
  run: |
153
126
  uv run maturin develop
154
127
 
128
+ - name: Static type gate (ty, scoped)
129
+ run: |
130
+ uv run ty check src/ferro/query tests/test_query_typing.py tests/test_static_contracts.py
131
+
155
132
  - name: Run IR vector contract harness
156
133
  run: |
157
134
  uv run pytest -v tests/test_ir_vectors_contract.py
@@ -270,64 +247,6 @@ jobs:
270
247
  run: |
271
248
  uv run pytest -v -m "backend_matrix or postgres_only" --db-backends=sqlite,postgres
272
249
 
273
- test-shadow-reports-pr:
274
- name: Shadow reports (touched paths)
275
- runs-on: ubuntu-latest
276
- needs: [changed-shadow-paths]
277
- if: github.event_name == 'pull_request' && needs.changed-shadow-paths.outputs.requires_shadow_reports == 'true'
278
- services:
279
- postgres:
280
- image: postgres:17
281
- env:
282
- POSTGRES_USER: ferro
283
- POSTGRES_PASSWORD: ferro
284
- POSTGRES_DB: ferro
285
- ports:
286
- - 5432:5432
287
- options: >-
288
- --health-cmd "pg_isready -U ferro -d ferro"
289
- --health-interval 10s
290
- --health-timeout 5s
291
- --health-retries 5
292
- env:
293
- FERRO_SUPABASE_URL: postgresql://ferro:ferro@127.0.0.1:5432/ferro?sslmode=disable
294
- FERRO_SHADOW_RUNTIME: "1"
295
- FERRO_SHADOW_RUNTIME_STRICT: "1"
296
- steps:
297
- - name: Checkout repository
298
- uses: actions/checkout@v4
299
-
300
- - name: Set up Python
301
- uses: actions/setup-python@v5
302
- with:
303
- python-version: '3.13'
304
-
305
- - name: Install UV
306
- uses: astral-sh/setup-uv@v5
307
- with:
308
- enable-cache: true
309
-
310
- - name: Set up Rust
311
- uses: dtolnay/rust-toolchain@stable
312
-
313
- - name: Cache Rust build
314
- uses: Swatinem/rust-cache@v2
315
- with:
316
- prefix-key: v1
317
- cache-on-failure: true
318
-
319
- - name: Install dependencies
320
- run: |
321
- uv sync --only-group ci-test --no-install-project --python 3.13
322
-
323
- - name: Build Rust extension
324
- run: |
325
- uv run maturin develop
326
-
327
- - name: Verify stable shadow reports
328
- run: |
329
- uv run pytest -v tests/test_shadow_reports.py::test_shadow_report_fixture_stable --db-backends=sqlite,postgres
330
-
331
250
  check-conventional-commits:
332
251
  name: Check Conventional Commits
333
252
  runs-on: ubuntu-latest
@@ -377,7 +296,7 @@ jobs:
377
296
 
378
297
  all-checks:
379
298
  name: All Checks Passed
380
- needs: [changed-shadow-paths, lint-and-format, test-python-pr, test-python-main, test-python-backend-matrix, test-shadow-reports-pr, test-rust]
299
+ needs: [lint-and-format, test-python-pr, test-python-main, test-python-backend-matrix, test-rust]
381
300
  runs-on: ubuntu-latest
382
301
  if: always()
383
302
  steps:
@@ -386,15 +305,10 @@ jobs:
386
305
  run: |
387
306
  ok() { [[ "$1" == "success" || "$1" == "skipped" ]]; }
388
307
 
389
- if [[ "${{ needs.changed-shadow-paths.result }}" == "failure" ]]; then
390
- echo "Shadow path detection failed; refusing to treat shadow gate as passed."
391
- exit 1
392
- fi
393
308
  if ! ok "${{ needs.lint-and-format.result }}"; then exit 1; fi
394
309
  if ! ok "${{ needs.test-python-pr.result }}"; then exit 1; fi
395
310
  if ! ok "${{ needs.test-python-main.result }}"; then exit 1; fi
396
311
  if ! ok "${{ needs.test-python-backend-matrix.result }}"; then exit 1; fi
397
- if ! ok "${{ needs.test-shadow-reports-pr.result }}"; then exit 1; fi
398
312
  if ! ok "${{ needs.test-rust.result }}"; then exit 1; fi
399
313
 
400
314
  echo "All checks passed!"
@@ -8,6 +8,11 @@ on:
8
8
  required: false
9
9
  type: boolean
10
10
  default: false
11
+ highlights:
12
+ description: 'Markdown prepended to the top of the GitHub Release body'
13
+ required: false
14
+ type: string
15
+ default: ''
11
16
 
12
17
  concurrency:
13
18
  group: release-${{ github.workflow }}-${{ github.ref }}
@@ -185,6 +190,20 @@ jobs:
185
190
  echo "release_ref=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
186
191
  echo "Using release ref: $RELEASE_TAG"
187
192
 
193
+ - name: Prepend highlights to GitHub Release
194
+ if: steps.verify_release.outputs.release_created == 'true' && inputs.highlights != ''
195
+ env:
196
+ GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
197
+ # Passed via env (not inline ${{ }}) so backticks, $, and quotes in the
198
+ # prose can't break the script or inject shell.
199
+ HIGHLIGHTS: ${{ inputs.highlights }}
200
+ shell: bash
201
+ run: |
202
+ TAG="${{ steps.release_ref.outputs.release_ref }}"
203
+ BODY="$(gh release view "$TAG" --json body -q .body)"
204
+ { printf '%s\n' "$HIGHLIGHTS"; printf '\n---\n\n'; printf '%s\n' "$BODY"; } > /tmp/notes.md
205
+ gh release edit "$TAG" --notes-file /tmp/notes.md
206
+
188
207
  - name: Release summary
189
208
  if: always()
190
209
  shell: bash
@@ -27,18 +27,23 @@ For a single model, every emitter must agree on:
27
27
 
28
28
  1. **Table name** — already handled by `model_name.lower()`.
29
29
  2. **Column names** — including shadow `*_id` columns from `ForeignKey`.
30
- 3. **Column types** — pydantic JSON schema → SQL type mapping must be one
31
- function (or two functions whose outputs are tested for parity). This
32
- includes the canonical `db_type` vocabulary (`text`, `varchar(N)`,
33
- `smallint`, `int`, `bigint`, `uuid`, `timestamp`, `timestamptz`, `date`,
34
- `time`) — duplicated in `_db_type_to_sa_type` (Python) and
35
- `db_type_token_to_canonical` (Rust), pinned by
36
- `tests/test_db_type_cross_emitter_parity.py`.
30
+ 3. **Column types** — decided by ONE function:
31
+ `ferro_ddl_lowering::resolve_column_storage` (explicit `db_type` token →
32
+ native-enum resolution → the `canonical_from_parts` cascade). The Alembic
33
+ bridge consumes it mechanically over FFI (`_core._resolve_storage_type`)
34
+ and `_db_type_to_sa_type` is only the SA *rendering* of the shared token
35
+ vocabulary — never a second decision table. Pinned exhaustively by
36
+ `tests/test_db_type_cross_emitter_parity.py` (every token and every
37
+ derived annotation × both dialects). See
38
+ `docs/solutions/patterns/derived-type-and-naming-decision-table.md`.
37
39
  4. **Index names** — `idx_<table>_<col>` for single-column indexes,
38
40
  `idx_<table>_<col1>_<col2>...` for composite indexes.
39
41
  5. **Unique constraint names** — `uq_<table>_<col>` for single-column,
40
42
  `uq_<table>_<col1>_<col2>...` for composite.
41
- 6. **Foreign key constraint names** — when explicitly named.
43
+ 6. **Foreign key constraint names** — `fk_<table>_<col>_<to_table>`, always
44
+ emitted (both emitters render `SchemaForeignKey.name`; single-sourced in
45
+ `ferro_ddl_lowering::fk_name`). See
46
+ `docs/solutions/patterns/derived-type-and-naming-decision-table.md`.
42
47
  7. **Primary key constraint names** — when explicitly named.
43
48
  8. **Check constraint names** — `ck_<table>_<col>` for the single-column
44
49
  `db_check=True` constraint; generated by `_ck_constraint_name` (Python)
@@ -244,9 +249,12 @@ Rules:
244
249
  checking (`User.age >= 18` types as `bool`; `where()` expects
245
250
  `QueryNode | Predicate`). Docs say so explicitly wherever the style is
246
251
  shown.
247
- - **`order_by` is not a predicate** and keeps attribute style
248
- (`order_by(User.age, "desc")`). Passing a lambda to `order_by` silently
249
- produces a junk column name — never show it.
252
+ - **`order_by` is not a predicate**, but its lambda selector
253
+ (`order_by(lambda u: u.age, "desc")`) is validated and is the documented
254
+ style — and it is the ONLY way to order by a related column
255
+ (`order_by(lambda t: t.account.label)`), which relation traversal requires.
256
+ Attribute style is not accepted — `order_by` takes a column-name string or
257
+ a lambda selector (anything else raises `TypeError`).
250
258
 
251
259
  The canonical comparisons live in `docs/pages/guide/queries.md`
252
260
  ("Predicate Styles") and `docs/pages/concepts/query-typing.md`; everywhere
@@ -313,3 +321,19 @@ Rules:
313
321
  This applies to brainstorming, design discussions, PR descriptions, issue
314
322
  comments, and any explanation directed at the maintainer. It governs how work is
315
323
  communicated, not what gets built.
324
+
325
+ ---
326
+
327
+ ## Agent skills
328
+
329
+ ### Issue tracker
330
+
331
+ GitHub Issues on `syn54x/ferro-orm`; external PRs are a triage surface. See `docs/agents/issue-tracker.md`.
332
+
333
+ ### Triage labels
334
+
335
+ Canonical five-role vocabulary (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). See `docs/agents/triage-labels.md`.
336
+
337
+ ### Domain docs
338
+
339
+ Single-context — `CONTEXT.md` at repo root, ADRs in `docs/adr/`. See `docs/agents/domain.md`.
@@ -1,6 +1,206 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.15.0 (2026-07-11)
5
+
6
+ ### Chores
7
+
8
+ - ADR-0008 populated relations + include/populated-relation glossary
9
+ ([`a1b273d`](https://github.com/syn54x/ferro-orm/commit/a1b273dc71b2b0f77e08e69808281ab5c25ffaea))
10
+
11
+ - Amend registration adr with review outcomes (operation-seam sync, build-then-swap, deregistration)
12
+ ([`fcb8db3`](https://github.com/syn54x/ferro-orm/commit/fcb8db385ef5c835121420b0d90c4a9bbfd8a1f8))
13
+
14
+ - New adrs and context
15
+ ([`3ea617c`](https://github.com/syn54x/ferro-orm/commit/3ea617c2ff418aa91fdf2d72470826b0aeec7242))
16
+
17
+ - Pin failed-resolve retryability in registration adr
18
+ ([`d5895ba`](https://github.com/syn54x/ferro-orm/commit/d5895bae311cf9a2cff230b16a975aa1a1e9a2ad))
19
+
20
+ - Pin zero-DDL, single-flight, and pure-Python clean-path invariants in registration adr
21
+ ([`09673f3`](https://github.com/syn54x/ferro-orm/commit/09673f334128cba7af7d94db38c6f43acd241d37))
22
+
23
+ - Registration adr
24
+ ([`05c5732`](https://github.com/syn54x/ferro-orm/commit/05c5732125370d1e2b539f83f89e200190c67bd0))
25
+
26
+ - Relation traversal ADR and context
27
+ ([`9151856`](https://github.com/syn54x/ferro-orm/commit/9151856364ea6ccc8ca125102302b5115e6a9ae0))
28
+
29
+ - Triage and update old plan statuses
30
+ ([`2f3e3c0`](https://github.com/syn54x/ferro-orm/commit/2f3e3c0584eec37815e2520852077b9c08885546))
31
+
32
+ ### Continuous Integration
33
+
34
+ - **release**: Custom highlights atop the GitHub Release notes
35
+ ([#275](https://github.com/syn54x/ferro-orm/pull/275),
36
+ [`f705938`](https://github.com/syn54x/ferro-orm/commit/f70593839af93ec902a0537f0f1ae73a34c425c2))
37
+
38
+ ### Documentation
39
+
40
+ - ADR-0007 materialization plan + complete-instance glossary
41
+ ([`4acc879`](https://github.com/syn54x/ferro-orm/commit/4acc879b23fcd5bc540b5ed7ee7129d95124ec77))
42
+
43
+ ### Features
44
+
45
+ - Atomic bulk registration install with fingerprint gate (#244)
46
+ ([#251](https://github.com/syn54x/ferro-orm/pull/251),
47
+ [`20c6061`](https://github.com/syn54x/ferro-orm/commit/20c6061c376faab78a7e3dc2496e5dd3cc123115))
48
+
49
+ - Compile ModelCodecPlan from SchemaIR at registration
50
+ ([#239](https://github.com/syn54x/ferro-orm/pull/239),
51
+ [`7e15fdc`](https://github.com/syn54x/ferro-orm/commit/7e15fdcb9f32938857ad21838feca978591fd044))
52
+
53
+ - Generation-counter dirty tracking with assemble-not-recompile (#245)
54
+ ([#252](https://github.com/syn54x/ferro-orm/pull/252),
55
+ [`2790e56`](https://github.com/syn54x/ferro-orm/commit/2790e566437d6e67f1f5c3abe555162ebb62598d))
56
+
57
+ - Joined-row hydration — QueryIR v4 + populated relations via include()
58
+ ([#289](https://github.com/syn54x/ferro-orm/pull/289),
59
+ [`4335417`](https://github.com/syn54x/ferro-orm/commit/4335417cbfd33da95b6ea52971d6f5e805af9ad7))
60
+
61
+ - JSONB column support (#260) ([#266](https://github.com/syn54x/ferro-orm/pull/266),
62
+ [`19f8cee`](https://github.com/syn54x/ferro-orm/commit/19f8cee3b7be3a570265f1269c735ac9093ab16a))
63
+
64
+ - Partial materialization — QueryIR v3 + partial selects (Rows/Row)
65
+ ([#283](https://github.com/syn54x/ferro-orm/pull/283),
66
+ [`e44129b`](https://github.com/syn54x/ferro-orm/commit/e44129bf966fa349b5bf637a5976c99b88c644b2))
67
+
68
+ - Query-time joins — relation traversal for filter and sort (stage 1)
69
+ ([#276](https://github.com/syn54x/ferro-orm/pull/276),
70
+ [`55dc386`](https://github.com/syn54x/ferro-orm/commit/55dc386f087fd6506d1713c9333bc11103bc96ac))
71
+
72
+ - Sync registration at ORM operation seam (#247)
73
+ ([#254](https://github.com/syn54x/ferro-orm/pull/254),
74
+ [`8409322`](https://github.com/syn54x/ferro-orm/commit/84093226a86b2cdc17afdce00d36fc4782c614c4))
75
+
76
+ ### Refactoring
77
+
78
+ - Canonicalize registry keys — derive register_model key from model identity
79
+ ([#250](https://github.com/syn54x/ferro-orm/pull/250),
80
+ [`d835c96`](https://github.com/syn54x/ferro-orm/commit/d835c96f08094ca0a8f7df0947ba9440f956edc9))
81
+
82
+ - Centralize register/deregister registry entrypoints (#243)
83
+ ([#248](https://github.com/syn54x/ferro-orm/pull/248),
84
+ [`2f2be69`](https://github.com/syn54x/ferro-orm/commit/2f2be69ac9dc859621afa5938f08b7f167af1870))
85
+
86
+ - Compile ColumnSpec column facts once (#255) ([#256](https://github.com/syn54x/ferro-orm/pull/256),
87
+ [`75fe1de`](https://github.com/syn54x/ferro-orm/commit/75fe1de5f3aee8f870e1e189f639b7466445c6dd))
88
+
89
+ - Drop RegisteredModel.schema — IR-first registry cleanup
90
+ ([#241](https://github.com/syn54x/ferro-orm/pull/241),
91
+ [`fc3b9f9`](https://github.com/syn54x/ferro-orm/commit/fc3b9f9c50bbd80f3915663e6278712df35fee96))
92
+
93
+ - Remove legacy migration shims and shadow runtime
94
+ ([#237](https://github.com/syn54x/ferro-orm/pull/237),
95
+ [`c2117b3`](https://github.com/syn54x/ferro-orm/commit/c2117b3351189912436c90552e127018db438c34))
96
+
97
+ ### Testing
98
+
99
+ - Pin provisional import — Python-only registration until connect (#246)
100
+ ([#253](https://github.com/syn54x/ferro-orm/pull/253),
101
+ [`ca111b5`](https://github.com/syn54x/ferro-orm/commit/ca111b5672c647cbb021fd63bf59038f0028f303))
102
+
103
+
104
+ ## v0.14.0 (2026-07-07)
105
+
106
+ ### Bug Fixes
107
+
108
+ - **ff-g**: Make Postgres db_check ADD CONSTRAINT idempotent (G6, #176)
109
+ ([#181](https://github.com/syn54x/ferro-orm/pull/181),
110
+ [`2c0e4cf`](https://github.com/syn54x/ferro-orm/commit/2c0e4cf6c922a73454198f6aa0aa75b30eb1f0c8))
111
+
112
+ ### Chores
113
+
114
+ - **benchmarks**: Pinned async benchmark suite over the rich-type hot path
115
+ ([#196](https://github.com/syn54x/ferro-orm/pull/196),
116
+ [`d9a656b`](https://github.com/syn54x/ferro-orm/commit/d9a656b320bd7fb6b613f4408a796161e9badc41))
117
+
118
+ ### Documentation
119
+
120
+ - Consolidate migration guides into one evergreen upgrade guide
121
+ ([#234](https://github.com/syn54x/ferro-orm/pull/234),
122
+ [`15c83db`](https://github.com/syn54x/ferro-orm/commit/15c83dbc6b1c057a78dfc425d70441e50c359f5d))
123
+
124
+ - Fixes roadmap
125
+ ([`5718d02`](https://github.com/syn54x/ferro-orm/commit/5718d02630b6c84d3ced6f8a3080adf4e7fe2383))
126
+
127
+ - **fable-fixes**: Fold #176 into Epic FF-G as sub-task G6
128
+ ([`b966e65`](https://github.com/syn54x/ferro-orm/commit/b966e65739d7c33ed666a7fd14086a27688516dd))
129
+
130
+ - **ff-a**: A5 — docs & migration guide for the mutation-surface changes
131
+ ([#180](https://github.com/syn54x/ferro-orm/pull/180),
132
+ [`ffc5476`](https://github.com/syn54x/ferro-orm/commit/ffc5476f9ba72f03a35048f283b11390f813856b))
133
+
134
+ - **ff-b**: Tick FF-B sub-task and exit-gate boxes in the fable-fixes roadmap
135
+ ([`29f9172`](https://github.com/syn54x/ferro-orm/commit/29f91724e2b6886f8299bbb42940c804c8e88141))
136
+
137
+ ### Features
138
+
139
+ - **ff-a**: Create() is a real INSERT; save() distinguishes INSERT from UPDATE (A3+A4)
140
+ ([#179](https://github.com/syn54x/ferro-orm/pull/179),
141
+ [`de3ec30`](https://github.com/syn54x/ferro-orm/commit/de3ec30196202ee14c42afc5b02f163f82a68451))
142
+
143
+ - **ff-a**: Reject limit/offset on mutating queries
144
+ ([#178](https://github.com/syn54x/ferro-orm/pull/178),
145
+ [`67faf42`](https://github.com/syn54x/ferro-orm/commit/67faf421d49984c5be16d2163982f13ab86cc5e4))
146
+
147
+ - **ff-a**: Typed DBAPI-shaped exception hierarchy mapped from sqlx errors
148
+ ([#177](https://github.com/syn54x/ferro-orm/pull/177),
149
+ [`9c306c5`](https://github.com/syn54x/ferro-orm/commit/9c306c549e716dd4a5158d655265aa7890e63c0b))
150
+
151
+ - **ff-b**: B1 canonical derived-type & naming decision table + refusal-rail scaffolding
152
+ ([`8c879f7`](https://github.com/syn54x/ferro-orm/commit/8c879f77833a7e5a983d53012efc8332cca5b852))
153
+
154
+ - **ff-b**: B2+B6 one derived-type decision table; native PG enums + timestamptz/time parity; delete
155
+ bridge mirrors
156
+ ([`bfdc1fd`](https://github.com/syn54x/ferro-orm/commit/bfdc1fda1eceebe70ae711e38b785af88b46e11e))
157
+
158
+ - **ff-b**: B3+B4 single-source artifact naming; both emitters emit named fk_/uq_ artifacts
159
+ ([`6b771f3`](https://github.com/syn54x/ferro-orm/commit/6b771f38933c72e8f848013a5ae93708fabf5c7a))
160
+
161
+ - **ff-c**: C1 — per-model ColumnCodec plan; delete codec.rs schema sniffing (F5)
162
+ ([#197](https://github.com/syn54x/ferro-orm/pull/197),
163
+ [`0e8e572`](https://github.com/syn54x/ferro-orm/commit/0e8e57252f4cca2d2bc9fffd06d9155338056cf3))
164
+
165
+ - **ff-c**: C2 — schema-epoch catalog cache; zero catalog queries on steady-state CRUD
166
+ ([#200](https://github.com/syn54x/ferro-orm/pull/200),
167
+ [`8c6d6ed`](https://github.com/syn54x/ferro-orm/commit/8c6d6edf578cf55ed7ada0d49359a7e09528da1e))
168
+
169
+ - **ff-c**: C3+C4 — native typed Postgres decode; plan-driven enum hydration replaces _fix_types
170
+ ([#198](https://github.com/syn54x/ferro-orm/pull/198),
171
+ [`05a008c`](https://github.com/syn54x/ferro-orm/commit/05a008cb73f6c4dc8b2478a441c99282d07bd41d))
172
+
173
+ - **ff-d**: Session-scoped weak identity map with refresh-on-load; single-handle routing
174
+ ([#201](https://github.com/syn54x/ferro-orm/pull/201),
175
+ [`a328cfc`](https://github.com/syn54x/ferro-orm/commit/a328cfcd107e2cac998ddc8af50845857ec669a5))
176
+
177
+ - **ff-e**: Registry & model identity — qualified keys, configurable tables, O(N) import
178
+ ([#209](https://github.com/syn54x/ferro-orm/pull/209),
179
+ [`6391805`](https://github.com/syn54x/ferro-orm/commit/6391805c1b4ee46c0590513ac6684237c18f7430))
180
+
181
+ - **ff-f**: Query builder 1.0 shape — immutable chaining, build-time column validation, lambda-only
182
+ predicates, QueryIR-only Rust ([#223](https://github.com/syn54x/ferro-orm/pull/223),
183
+ [`eb7fece`](https://github.com/syn54x/ferro-orm/commit/eb7fece8fbc2231220ecf9111616f803f2f987eb))
184
+
185
+ - **ff-g-a**: Hardening — hydration ABI guard, transactional PG migrate, correctness edges,
186
+ decode-path caching ([#232](https://github.com/syn54x/ferro-orm/pull/232),
187
+ [`22617e1`](https://github.com/syn54x/ferro-orm/commit/22617e1cfe5e2bc9d0a3d7a1fed5acf4d8589cef))
188
+
189
+ ### Refactoring
190
+
191
+ - **ff-g-b**: Operations.rs dedup — ModelMeta + Executor (G2)
192
+ ([#233](https://github.com/syn54x/ferro-orm/pull/233),
193
+ [`09c2c62`](https://github.com/syn54x/ferro-orm/commit/09c2c6214af9e2f9510d39d037935c53bd2e796d))
194
+
195
+ ### Testing
196
+
197
+ - **ff-b**: B5 I-1 sentinel on the full backend matrix with a full-type fixture, zero filters
198
+ ([`1141eb4`](https://github.com/syn54x/ferro-orm/commit/1141eb437b4843303e01556a5b24ddee9d0e0279))
199
+
200
+ - **ff-b**: Force psycopg v3 driver in the Postgres sentinel regardless of URL scheme
201
+ ([`64f9845`](https://github.com/syn54x/ferro-orm/commit/64f98454bdb5e090a41bd214de06b7670974f4eb))
202
+
203
+
4
204
  ## v0.13.0 (2026-07-02)
5
205
 
6
206
  ### Bug Fixes
@@ -0,0 +1,89 @@
1
+ # Ferro ORM
2
+
3
+ A Python ORM with a Rust core. Models are Pydantic subclasses; schema compiles to SchemaIR and fans out to runtime DDL and the Alembic bridge.
4
+
5
+ ## Language
6
+
7
+ **Materialized View**:
8
+ A PostgreSQL database object that stores the result of a query and is refreshed on demand. In Ferro it is a read-only `MaterializedView` subclass — queryable through the normal ORM, not writable.
9
+ _Avoid_: Snapshot table, cache table, denormalized table
10
+
11
+ **Refresh**:
12
+ The explicit operation that repopulates a materialized view from its defining SELECT. Ferro never refreshes at connect time; the user calls `refresh()` when they want updated data.
13
+ _Avoid_: Auto-refresh, sync, rebuild
14
+
15
+ **Postgres-only schema object**:
16
+ A model artifact that Ferro emits only on PostgreSQL. On SQLite the class still registers for imports and typing, but DDL is skipped and querying raises a clear error.
17
+ _Avoid_: Dialect-specific model, PG-only table
18
+
19
+ **Redefine**:
20
+ Replacing an existing materialized view by dropping and recreating it when its defining SELECT changes. Authorized by `migrate_materialized_redefine=True`; otherwise connect fails loudly on drift.
21
+ _Avoid_: Alter, migrate, update
22
+
23
+ **Materialized view column**:
24
+ A flat, typed field on a `MaterializedView` — same declarations as `Model` fields, but no `ForeignKey`, `BackRef`, or `ManyToMany`. Reference related entities by scalar columns (`order_id: int`), not relations.
25
+ _Avoid_: Relation column, FK field
26
+
27
+ **Materialized query**:
28
+ The `ClassVar` SQL string (`__materialized_query__`) that defines what rows a materialized view stores. Declared alongside typed fields; Ferro validates that SELECT output matches the field contract.
29
+ _Avoid_: Select SQL, view definition, query body
30
+
31
+ **Read-only view**:
32
+ A `MaterializedView` that can be queried but never mutated. `save()`, `delete()`, and `create()` raise a clear error.
33
+ _Avoid_: Immutable model, snapshot model
34
+
35
+ **Storage token**:
36
+ A word in the canonical `db_type` vocabulary (`text`, `bigint`, `timestamptz`, `jsonb`, …) naming how a column is stored. One shared vocabulary feeds every emitter; a token never means different things to different emitters.
37
+ _Avoid_: SQL type string, dialect type, column type name
38
+
39
+ **Storage lowering**:
40
+ The dialect-side degrade of a storage token to the nearest type a backend supports (e.g. `jsonb` stores as plain JSON on SQLite). Lowering is silent and documented; it never changes value semantics — only the on-disk representation.
41
+ _Avoid_: Fallback type, emulation, downgrade
42
+
43
+ **Json-family field**:
44
+ A field whose values Ferro stores as JSON documents: `dict`, `list` (any element type, including nested models), or a nested Pydantic model. Only json-family fields may opt into JSON storage tokens such as `jsonb`.
45
+ _Avoid_: Object field, blob field, document column
46
+
47
+ **Column spec**:
48
+ The single authoritative record of one column's facts — identity, type, and constraints — derived exactly once from the field declaration. Provisional at class-body time; authoritative once relationship resolution completes.
49
+ _Avoid_: Column metadata, enriched schema property, field dict
50
+
51
+ **Relation traversal**:
52
+ Attribute access on a declared forward-FK field inside a query lambda (`lambda t: t.account.ledger_id`), reaching a related model's columns from the root. Traversal narrows the result to rows where the relation exists; keeping rows without the relation requires an explicit left join.
53
+ _Avoid_: Join inference, nested filter, path lookup, string path
54
+
55
+ **Relation path**:
56
+ The ordered sequence of forward-FK hops a traversal walks (`account`, or `account → owner`). A path is the identity of a join: the same path referenced anywhere in a query is one join, and distinct paths to the same model are distinct joins. Left-join requests apply to a whole path.
57
+ _Avoid_: Join alias, lookup chain, dotted path string
58
+
59
+ **Shape-preserving query**:
60
+ The invariant that filtering and ordering never change what a query returns — a query over Transaction yields Transaction instances regardless of which relations its predicates traverse. Only an explicit projection operation may change the result shape.
61
+ _Avoid_: Implicit projection, row narrowing
62
+
63
+ **Complete-instance invariant**:
64
+ A model instance always carries a complete row — there is no such thing as a partial or deferred-field model instance, anywhere. Anything narrower than a full row (a column subset, an aggregate) comes back as a projected record, never as the model type.
65
+ _Avoid_: Partial instance, deferred field, lightweight model, .only()
66
+
67
+ **Projected record**:
68
+ The result of an explicit projection — a typed record of named values that is not a model instance and cannot be saved, refreshed, or identity-mapped. Column subsets and aggregation results are projected records; complete model rows are not. Realized as `Row`, delivered in the list-like `Rows` container.
69
+ _Avoid_: Partial model, row dict, value tuple
70
+
71
+ **Include**:
72
+ The explicit request (`.include(lambda t: t.account.owner)`) that a query populate a forward-FK relation path — the data axis of a query, distinct from joins (membership) and projection (shape). Including a path populates every hop along it and never changes which rows come back.
73
+ _Avoid_: Eager load, select_related, prefetch, join-fetch
74
+
75
+ **Populated relation**:
76
+ A forward-FK field carrying its complete related instance, attached by an explicit include on the query. Attribute access returns the instance directly — no await, no query — matching the field's declared type. An unpopulated relation keeps the awaitable contract; population changes cost and attached data, never the result type (there is no separate "loaded" model type).
77
+ _Avoid_: Eager-loaded field, select_related, prefetched attribute, joined attribute
78
+
79
+ **Materialization plan**:
80
+ A query's declaration of what its result columns become: complete root instances (every query today), a projected record of named fields, or — in the future — a populated instance graph. Every query carries exactly one plan; the plan travels with the query rather than being inferred from its column list.
81
+ _Avoid_: Select list, projection spec, hydration mode flag
82
+
83
+ **Provisional registration**:
84
+ 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.
85
+ _Avoid_: Import-time registration, partial registry
86
+
87
+ **Resolved registration**:
88
+ The registry epoch after relationship resolution completes — join tables exist, shadow FK columns are wired, and the SchemaIR modelset is authoritative for DDL and auto-migrate.
89
+ _Avoid_: Final registration, committed registry