ferro-orm 0.13.0__tar.gz → 0.14.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 (344) hide show
  1. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/workflows/ci.yml +4 -0
  2. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/AGENTS.md +13 -8
  3. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/CHANGELOG.md +100 -0
  4. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/Cargo.lock +148 -13
  5. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/Cargo.toml +9 -2
  6. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/PKG-INFO +2 -2
  7. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/README.md +1 -1
  8. ferro_orm-0.14.0/benchmarks/README.md +133 -0
  9. ferro_orm-0.14.0/benchmarks/__init__.py +11 -0
  10. ferro_orm-0.14.0/benchmarks/__main__.py +10 -0
  11. ferro_orm-0.14.0/benchmarks/backends.py +88 -0
  12. ferro_orm-0.14.0/benchmarks/baselines/postgres.json +49 -0
  13. ferro_orm-0.14.0/benchmarks/baselines/sqlite.json +49 -0
  14. ferro_orm-0.14.0/benchmarks/compare.py +98 -0
  15. ferro_orm-0.14.0/benchmarks/harness.py +99 -0
  16. ferro_orm-0.14.0/benchmarks/model.py +84 -0
  17. ferro_orm-0.14.0/benchmarks/run.py +270 -0
  18. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/crates/ferro-ddl-lowering/src/lib.rs +680 -46
  19. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/crates/ferro-migrate/src/emit.rs +155 -83
  20. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/crates/ferro-migrate/src/tests.rs +291 -45
  21. ferro_orm-0.14.0/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +150 -0
  22. ferro_orm-0.14.0/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +222 -0
  23. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/examples/multiple_databases.py +9 -7
  24. ferro_orm-0.14.0/docs/examples/mutations.py +112 -0
  25. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/examples/pagination.py +11 -9
  26. ferro_orm-0.14.0/docs/examples/predicates.py +84 -0
  27. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/examples/predicates_annotated.py +12 -10
  28. ferro_orm-0.14.0/docs/examples/quickstart.py +112 -0
  29. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/examples/quickstart_annotated.py +7 -6
  30. ferro_orm-0.14.0/docs/examples/raw_sql.py +45 -0
  31. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/examples/relationships.py +37 -36
  32. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/examples/relationships_annotated.py +18 -17
  33. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/examples/soft_deletes.py +11 -10
  34. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/examples/soft_deletes_annotated.py +8 -7
  35. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/examples/testing_conftest.py +3 -2
  36. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/examples/timestamps.py +10 -9
  37. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/examples/timestamps_annotated.py +7 -6
  38. ferro_orm-0.14.0/docs/examples/transactions.py +59 -0
  39. ferro_orm-0.14.0/docs/pages/api/exceptions.md +57 -0
  40. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/api/migrations.md +1 -1
  41. ferro_orm-0.14.0/docs/pages/api/queries.md +9 -0
  42. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/concepts/architecture.md +2 -2
  43. ferro_orm-0.14.0/docs/pages/concepts/identity-map.md +227 -0
  44. ferro_orm-0.14.0/docs/pages/concepts/query-typing.md +56 -0
  45. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/concepts/type-safety.md +2 -2
  46. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/faq.md +3 -1
  47. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/getting-started/quickstart.md +3 -3
  48. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/guide/connections.md +2 -2
  49. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/guide/models-and-fields.md +64 -1
  50. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/guide/mutations.md +72 -16
  51. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/guide/queries.md +47 -40
  52. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/howto/migrate-from-sqlalchemy.md +4 -4
  53. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/howto/multiple-databases.md +5 -3
  54. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/howto/pagination.md +2 -2
  55. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/howto/soft-deletes.md +1 -1
  56. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/howto/testing.md +7 -5
  57. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/howto/timestamps.md +1 -1
  58. ferro_orm-0.14.0/docs/pages/howto/upgrade-guide.md +446 -0
  59. ferro_orm-0.14.0/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +449 -0
  60. ferro_orm-0.14.0/docs/plans/2026-07-02-002-feat-ff-a-172-typed-exceptions-plan.md +37 -0
  61. ferro_orm-0.14.0/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +105 -0
  62. ferro_orm-0.14.0/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +159 -0
  63. ferro_orm-0.14.0/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +207 -0
  64. ferro_orm-0.14.0/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +1474 -0
  65. ferro_orm-0.14.0/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +267 -0
  66. ferro_orm-0.14.0/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +1584 -0
  67. ferro_orm-0.14.0/docs/plans/2026-07-06-009-ff-f-query-builder-1.0-design.md +152 -0
  68. ferro_orm-0.14.0/docs/plans/2026-07-06-010-ff-f-query-builder-1.0-plan.md +1290 -0
  69. ferro_orm-0.14.0/docs/plans/2026-07-06-011-ff-g-hardening-design.md +268 -0
  70. ferro_orm-0.14.0/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +985 -0
  71. ferro_orm-0.14.0/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +193 -0
  72. ferro_orm-0.14.0/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +1203 -0
  73. ferro_orm-0.14.0/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +518 -0
  74. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/ir-first-migration-guide.md +11 -0
  75. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +10 -1
  76. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +13 -1
  77. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/patterns/cross-emitter-ddl-parity.md +9 -8
  78. ferro_orm-0.14.0/docs/solutions/patterns/derived-type-and-naming-decision-table.md +161 -0
  79. ferro_orm-0.14.0/docs/solutions/patterns/persistence-state.md +76 -0
  80. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/justfile +6 -0
  81. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/pyproject.toml +2 -4
  82. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/backend.rs +211 -4
  83. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/codec.rs +185 -273
  84. ferro_orm-0.14.0/src/codec_plan.rs +591 -0
  85. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/connection.rs +48 -20
  86. ferro_orm-0.14.0/src/errors.rs +181 -0
  87. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/__init__.py +39 -6
  88. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/_core.pyi +74 -64
  89. ferro_orm-0.14.0/src/ferro/exceptions.py +134 -0
  90. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/ir/compiler.py +42 -23
  91. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/metaclass.py +102 -24
  92. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/migrations/alembic.py +90 -81
  93. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/models.py +218 -140
  94. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/query/__init__.py +1 -2
  95. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/query/builder.py +174 -195
  96. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/query/nodes.py +60 -128
  97. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/raw.py +17 -24
  98. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/relations/__init__.py +36 -17
  99. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/relations/descriptors.py +3 -11
  100. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/schema_metadata.py +20 -1
  101. ferro_orm-0.14.0/src/ferro/state.py +217 -0
  102. ferro_orm-0.14.0/src/hydration.rs +286 -0
  103. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/introspect.rs +4 -3
  104. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/lib.rs +20 -1
  105. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/migrate.rs +398 -272
  106. ferro_orm-0.14.0/src/naming_ffi.rs +92 -0
  107. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/operations.rs +1425 -1186
  108. ferro_orm-0.14.0/src/query.rs +558 -0
  109. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/schema.rs +125 -72
  110. ferro_orm-0.14.0/src/schema_bind.rs +23 -0
  111. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/state.rs +270 -44
  112. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/conftest.py +25 -0
  113. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +118 -7
  114. ferro_orm-0.14.0/tests/fixtures/shadow_reports/postgres.json +32 -0
  115. ferro_orm-0.14.0/tests/fixtures/shadow_reports/sqlite.json +32 -0
  116. ferro_orm-0.14.0/tests/static_fixtures/bad_predicates.py +19 -0
  117. ferro_orm-0.14.0/tests/static_fixtures/good_predicates.py +20 -0
  118. ferro_orm-0.14.0/tests/test_aggregation.py +74 -0
  119. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_alembic_autogenerate.py +13 -10
  120. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_alembic_bridge.py +19 -17
  121. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_auto_migrate.py +398 -273
  122. ferro_orm-0.14.0/tests/test_bulk_update.py +66 -0
  123. ferro_orm-0.14.0/tests/test_catalog_cache.py +244 -0
  124. ferro_orm-0.14.0/tests/test_codec_plan.py +81 -0
  125. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_composite_index.py +21 -17
  126. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_composite_unique.py +19 -17
  127. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_connection.py +92 -31
  128. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_connection_redaction.py +2 -2
  129. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_constraints.py +6 -7
  130. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_cross_emitter_parity.py +63 -74
  131. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_crud.py +86 -59
  132. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_db_type_cross_emitter_parity.py +114 -6
  133. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_db_type_integration.py +56 -52
  134. ferro_orm-0.14.0/tests/test_deletion.py +90 -0
  135. ferro_orm-0.14.0/tests/test_documentation_features.py +874 -0
  136. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_enum_cold_hydration.py +11 -8
  137. ferro_orm-0.14.0/tests/test_exception_mapping.py +160 -0
  138. ferro_orm-0.14.0/tests/test_exceptions.py +126 -0
  139. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_field_wrapper.py +17 -11
  140. ferro_orm-0.14.0/tests/test_fk_target_pk.py +69 -0
  141. ferro_orm-0.14.0/tests/test_helpers.py +136 -0
  142. ferro_orm-0.14.0/tests/test_hydration.py +204 -0
  143. ferro_orm-0.14.0/tests/test_hydration_equivalence.py +275 -0
  144. ferro_orm-0.14.0/tests/test_identity_memory.py +80 -0
  145. ferro_orm-0.14.0/tests/test_identity_refresh.py +79 -0
  146. ferro_orm-0.14.0/tests/test_identity_scoped_invalidation.py +86 -0
  147. ferro_orm-0.14.0/tests/test_identity_weakref.py +37 -0
  148. ferro_orm-0.14.0/tests/test_import_budget.py +38 -0
  149. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_metadata.py +25 -22
  150. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_migrate_plan.py +14 -14
  151. ferro_orm-0.14.0/tests/test_model_identity.py +234 -0
  152. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_models.py +4 -1
  153. ferro_orm-0.14.0/tests/test_mutation_pagination_guard.py +99 -0
  154. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_named_connections_integration.py +52 -32
  155. ferro_orm-0.14.0/tests/test_naming_single_source.py +315 -0
  156. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_one_to_one.py +22 -20
  157. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_query_builder.py +90 -112
  158. ferro_orm-0.14.0/tests/test_query_column_validation.py +151 -0
  159. ferro_orm-0.14.0/tests/test_query_immutability.py +101 -0
  160. ferro_orm-0.14.0/tests/test_query_typing.py +173 -0
  161. ferro_orm-0.14.0/tests/test_raw_sql.py +528 -0
  162. ferro_orm-0.14.0/tests/test_refresh.py +55 -0
  163. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_relationship_engine.py +10 -2
  164. ferro_orm-0.14.0/tests/test_relationship_resolution_errors.py +47 -0
  165. ferro_orm-0.14.0/tests/test_route_single_site.py +33 -0
  166. ferro_orm-0.14.0/tests/test_routing_errors.py +64 -0
  167. ferro_orm-0.14.0/tests/test_save_pk_edges.py +53 -0
  168. ferro_orm-0.14.0/tests/test_save_semantics.py +358 -0
  169. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_schema.py +6 -6
  170. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_session.py +14 -18
  171. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_shadow_fk_types.py +58 -54
  172. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_shadow_reports.py +16 -43
  173. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_sqlite_alembic_reconnect_hydration.py +21 -11
  174. ferro_orm-0.14.0/tests/test_static_contracts.py +191 -0
  175. ferro_orm-0.14.0/tests/test_string_search.py +54 -0
  176. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_structural_types.py +152 -140
  177. ferro_orm-0.14.0/tests/test_temporal_types.py +75 -0
  178. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_transactions.py +134 -117
  179. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_typed_null_binds.py +130 -115
  180. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_typed_save_bind.py +75 -67
  181. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/uv.lock +28 -1
  182. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/zensical.toml +2 -2
  183. ferro_orm-0.13.0/docs/examples/mutations.py +0 -49
  184. ferro_orm-0.13.0/docs/examples/predicates.py +0 -92
  185. ferro_orm-0.13.0/docs/examples/quickstart.py +0 -111
  186. ferro_orm-0.13.0/docs/examples/raw_sql.py +0 -43
  187. ferro_orm-0.13.0/docs/examples/transactions.py +0 -58
  188. ferro_orm-0.13.0/docs/pages/api/exceptions.md +0 -5
  189. ferro_orm-0.13.0/docs/pages/api/queries.md +0 -11
  190. ferro_orm-0.13.0/docs/pages/concepts/identity-map.md +0 -151
  191. ferro_orm-0.13.0/docs/pages/concepts/query-typing.md +0 -106
  192. ferro_orm-0.13.0/docs/pages/howto/migrating-to-v0-12-0.md +0 -153
  193. ferro_orm-0.13.0/src/ferro/exceptions.py +0 -17
  194. ferro_orm-0.13.0/src/ferro/state.py +0 -166
  195. ferro_orm-0.13.0/src/hydration.rs +0 -85
  196. ferro_orm-0.13.0/src/query.rs +0 -917
  197. ferro_orm-0.13.0/src/schema_bind.rs +0 -49
  198. ferro_orm-0.13.0/tests/fixtures/shadow_reports/postgres.json +0 -66
  199. ferro_orm-0.13.0/tests/fixtures/shadow_reports/sqlite.json +0 -66
  200. ferro_orm-0.13.0/tests/test_aggregation.py +0 -70
  201. ferro_orm-0.13.0/tests/test_bulk_update.py +0 -62
  202. ferro_orm-0.13.0/tests/test_deletion.py +0 -87
  203. ferro_orm-0.13.0/tests/test_deprecated_operator_inventory.py +0 -51
  204. ferro_orm-0.13.0/tests/test_documentation_features.py +0 -833
  205. ferro_orm-0.13.0/tests/test_framework_predicates.py +0 -72
  206. ferro_orm-0.13.0/tests/test_helpers.py +0 -129
  207. ferro_orm-0.13.0/tests/test_hydration.py +0 -167
  208. ferro_orm-0.13.0/tests/test_query_typing.py +0 -295
  209. ferro_orm-0.13.0/tests/test_raw_sql.py +0 -493
  210. ferro_orm-0.13.0/tests/test_refresh.py +0 -53
  211. ferro_orm-0.13.0/tests/test_static_contracts.py +0 -8
  212. ferro_orm-0.13.0/tests/test_string_search.py +0 -52
  213. ferro_orm-0.13.0/tests/test_temporal_types.py +0 -71
  214. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  215. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  216. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/PERMISSIONS.md +0 -0
  217. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/PYPI_CHECKLIST.md +0 -0
  218. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/PYPI_SETUP.md +0 -0
  219. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/generated/wheels.generated.yml +0 -0
  220. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/pull_request_template.md +0 -0
  221. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/workflows/packaging-smoke.yml +0 -0
  222. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/workflows/publish-docs.yml +0 -0
  223. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/workflows/publish.yml +0 -0
  224. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.github/workflows/release.yml +0 -0
  225. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.gitignore +0 -0
  226. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.pre-commit-config.yaml +0 -0
  227. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/.python-version +0 -0
  228. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/CONTRIBUTING.md +0 -0
  229. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/LICENSE +0 -0
  230. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
  231. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/crates/ferro-migrate/Cargo.toml +0 -0
  232. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/crates/ferro-migrate/src/lib.rs +0 -0
  233. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/crates/ferro-schema-ir/Cargo.toml +0 -0
  234. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/crates/ferro-schema-ir/src/lib.rs +0 -0
  235. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
  236. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
  237. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
  238. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
  239. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
  240. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
  241. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
  242. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
  243. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-06-26-ir-p8.5-143-db-type-drop-requirements.md +0 -0
  244. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
  245. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-06-28-ir-p8.5-141-single-schema-ir-producer-requirements.md +0 -0
  246. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-06-29-ir-p8.6-146-dialect-enum-unification-requirements.md +0 -0
  247. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-06-29-ir-p8.6-153-create-path-ir-requirements.md +0 -0
  248. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
  249. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
  250. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-07-01-ir-p8.6-154-datetime-tz-coarseness-requirements.md +0 -0
  251. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-07-01-ir-p8.6-162-typed-save-bind-requirements.md +0 -0
  252. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/brainstorms/2026-07-01-ir-p8.6-165-blob-introspection-false-positive-requirements.md +0 -0
  253. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/api/connection.md +0 -0
  254. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/api/fields.md +0 -0
  255. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/api/model.md +0 -0
  256. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/api/raw-sql.md +0 -0
  257. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/api/relationships.md +0 -0
  258. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/api/transactions.md +0 -0
  259. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/changelog.md +0 -0
  260. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/concepts/backends.md +0 -0
  261. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/concepts/performance.md +0 -0
  262. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/contributing.md +0 -0
  263. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/getting-started/installation.md +0 -0
  264. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/getting-started/next-steps.md +0 -0
  265. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/guide/migrations.md +0 -0
  266. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/guide/raw-sql.md +0 -0
  267. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/guide/relationships.md +0 -0
  268. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/guide/transactions.md +0 -0
  269. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/index.md +0 -0
  270. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/roadmap.md +0 -0
  271. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/stylesheets/extra.css +0 -0
  272. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/pages/why-ferro.md +0 -0
  273. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
  274. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
  275. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
  276. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
  277. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
  278. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
  279. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
  280. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
  281. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-06-24-001-feat-ir-p8-119-wire-automigrate-plan.md +0 -0
  282. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-06-25-001-feat-ir-p8-120-parity-gate-plan.md +0 -0
  283. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-06-26-001-feat-ir-p8.5-140-shared-lowering-plan.md +0 -0
  284. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-06-26-002-feat-ir-p8.5-143-db-type-drop-plan.md +0 -0
  285. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-06-26-003-feat-ir-p8.5-144-reconcile-indexes-plan.md +0 -0
  286. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-06-28-001-feat-ir-p8.5-141-single-schema-ir-producer-plan.md +0 -0
  287. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-06-29-001-refactor-ir-p8.6-153-create-path-unification-plan.md +0 -0
  288. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-06-29-002-refactor-ir-p8.6-146-dialect-enum-unification-plan.md +0 -0
  289. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-06-30-001-refactor-ir-p8.6-158-check-renderer-plan.md +0 -0
  290. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-06-30-002-fix-ir-p8.6-155-deferred-annotations-plan.md +0 -0
  291. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-07-01-001-feat-ir-p8.6-162-typed-save-bind-plan.md +0 -0
  292. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-07-01-002-fix-ir-p8.6-154-datetime-tz-coarseness-plan.md +0 -0
  293. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/2026-07-01-003-fix-ir-p8.6-165-blob-introspection-plan.md +0 -0
  294. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/plans/ir-first-release-checklist.md +0 -0
  295. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/rfc/ir-contracts-v1.md +0 -0
  296. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/README.md +0 -0
  297. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
  298. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
  299. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
  300. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
  301. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
  302. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
  303. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
  304. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
  305. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
  306. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/patterns/foreign-key-index.md +0 -0
  307. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
  308. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/patterns/ir-invariants.md +0 -0
  309. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
  310. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
  311. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/docs/solutions/patterns/typed-null-binds.md +0 -0
  312. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/scripts/demo_queries.py +0 -0
  313. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/scripts/demo_queries_pre_v012.py +0 -0
  314. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/_annotation_utils.py +0 -0
  315. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/_bind_payload.py +0 -0
  316. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/_deprecations.py +0 -0
  317. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/_shadow_fk_types.py +0 -0
  318. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/base.py +0 -0
  319. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/composite_indexes.py +0 -0
  320. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/composite_uniques.py +0 -0
  321. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/fields.py +0 -0
  322. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/ir/__init__.py +0 -0
  323. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/migrations/__init__.py +0 -0
  324. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/py.typed +0 -0
  325. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/src/ferro/session.py +0 -0
  326. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/__init__.py +0 -0
  327. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/db_backends.py +0 -0
  328. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/fixtures/ir_vectors/README.md +0 -0
  329. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
  330. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/fixtures/ir_vectors/query_user_compound_v1.json +0 -0
  331. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
  332. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_alembic_db_type.py +0 -0
  333. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_alembic_nullability.py +0 -0
  334. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_alembic_type_mapping.py +0 -0
  335. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_db_backends.py +0 -0
  336. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_db_type_typing.py +0 -0
  337. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_db_type_validation.py +0 -0
  338. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_deprecations.py +0 -0
  339. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_docs_examples.py +0 -0
  340. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_ir_vectors_contract.py +0 -0
  341. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_metaclass_internals.py +0 -0
  342. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_schema_constraints.py +0 -0
  343. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_schema_db_type_metadata.py +0 -0
  344. {ferro_orm-0.13.0 → ferro_orm-0.14.0}/tests/test_schema_enum_annotations.py +0 -0
@@ -152,6 +152,10 @@ jobs:
152
152
  run: |
153
153
  uv run maturin develop
154
154
 
155
+ - name: Static type gate (ty, scoped)
156
+ run: |
157
+ uv run ty check src/ferro/query tests/test_query_typing.py tests/test_static_contracts.py
158
+
155
159
  - name: Run IR vector contract harness
156
160
  run: |
157
161
  uv run pytest -v tests/test_ir_vectors_contract.py
@@ -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)
@@ -1,6 +1,106 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.14.0 (2026-07-07)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **ff-g**: Make Postgres db_check ADD CONSTRAINT idempotent (G6, #176)
9
+ ([#181](https://github.com/syn54x/ferro-orm/pull/181),
10
+ [`2c0e4cf`](https://github.com/syn54x/ferro-orm/commit/2c0e4cf6c922a73454198f6aa0aa75b30eb1f0c8))
11
+
12
+ ### Chores
13
+
14
+ - **benchmarks**: Pinned async benchmark suite over the rich-type hot path
15
+ ([#196](https://github.com/syn54x/ferro-orm/pull/196),
16
+ [`d9a656b`](https://github.com/syn54x/ferro-orm/commit/d9a656b320bd7fb6b613f4408a796161e9badc41))
17
+
18
+ ### Documentation
19
+
20
+ - Consolidate migration guides into one evergreen upgrade guide
21
+ ([#234](https://github.com/syn54x/ferro-orm/pull/234),
22
+ [`15c83db`](https://github.com/syn54x/ferro-orm/commit/15c83dbc6b1c057a78dfc425d70441e50c359f5d))
23
+
24
+ - Fixes roadmap
25
+ ([`5718d02`](https://github.com/syn54x/ferro-orm/commit/5718d02630b6c84d3ced6f8a3080adf4e7fe2383))
26
+
27
+ - **fable-fixes**: Fold #176 into Epic FF-G as sub-task G6
28
+ ([`b966e65`](https://github.com/syn54x/ferro-orm/commit/b966e65739d7c33ed666a7fd14086a27688516dd))
29
+
30
+ - **ff-a**: A5 — docs & migration guide for the mutation-surface changes
31
+ ([#180](https://github.com/syn54x/ferro-orm/pull/180),
32
+ [`ffc5476`](https://github.com/syn54x/ferro-orm/commit/ffc5476f9ba72f03a35048f283b11390f813856b))
33
+
34
+ - **ff-b**: Tick FF-B sub-task and exit-gate boxes in the fable-fixes roadmap
35
+ ([`29f9172`](https://github.com/syn54x/ferro-orm/commit/29f91724e2b6886f8299bbb42940c804c8e88141))
36
+
37
+ ### Features
38
+
39
+ - **ff-a**: Create() is a real INSERT; save() distinguishes INSERT from UPDATE (A3+A4)
40
+ ([#179](https://github.com/syn54x/ferro-orm/pull/179),
41
+ [`de3ec30`](https://github.com/syn54x/ferro-orm/commit/de3ec30196202ee14c42afc5b02f163f82a68451))
42
+
43
+ - **ff-a**: Reject limit/offset on mutating queries
44
+ ([#178](https://github.com/syn54x/ferro-orm/pull/178),
45
+ [`67faf42`](https://github.com/syn54x/ferro-orm/commit/67faf421d49984c5be16d2163982f13ab86cc5e4))
46
+
47
+ - **ff-a**: Typed DBAPI-shaped exception hierarchy mapped from sqlx errors
48
+ ([#177](https://github.com/syn54x/ferro-orm/pull/177),
49
+ [`9c306c5`](https://github.com/syn54x/ferro-orm/commit/9c306c549e716dd4a5158d655265aa7890e63c0b))
50
+
51
+ - **ff-b**: B1 canonical derived-type & naming decision table + refusal-rail scaffolding
52
+ ([`8c879f7`](https://github.com/syn54x/ferro-orm/commit/8c879f77833a7e5a983d53012efc8332cca5b852))
53
+
54
+ - **ff-b**: B2+B6 one derived-type decision table; native PG enums + timestamptz/time parity; delete
55
+ bridge mirrors
56
+ ([`bfdc1fd`](https://github.com/syn54x/ferro-orm/commit/bfdc1fda1eceebe70ae711e38b785af88b46e11e))
57
+
58
+ - **ff-b**: B3+B4 single-source artifact naming; both emitters emit named fk_/uq_ artifacts
59
+ ([`6b771f3`](https://github.com/syn54x/ferro-orm/commit/6b771f38933c72e8f848013a5ae93708fabf5c7a))
60
+
61
+ - **ff-c**: C1 — per-model ColumnCodec plan; delete codec.rs schema sniffing (F5)
62
+ ([#197](https://github.com/syn54x/ferro-orm/pull/197),
63
+ [`0e8e572`](https://github.com/syn54x/ferro-orm/commit/0e8e57252f4cca2d2bc9fffd06d9155338056cf3))
64
+
65
+ - **ff-c**: C2 — schema-epoch catalog cache; zero catalog queries on steady-state CRUD
66
+ ([#200](https://github.com/syn54x/ferro-orm/pull/200),
67
+ [`8c6d6ed`](https://github.com/syn54x/ferro-orm/commit/8c6d6edf578cf55ed7ada0d49359a7e09528da1e))
68
+
69
+ - **ff-c**: C3+C4 — native typed Postgres decode; plan-driven enum hydration replaces _fix_types
70
+ ([#198](https://github.com/syn54x/ferro-orm/pull/198),
71
+ [`05a008c`](https://github.com/syn54x/ferro-orm/commit/05a008cb73f6c4dc8b2478a441c99282d07bd41d))
72
+
73
+ - **ff-d**: Session-scoped weak identity map with refresh-on-load; single-handle routing
74
+ ([#201](https://github.com/syn54x/ferro-orm/pull/201),
75
+ [`a328cfc`](https://github.com/syn54x/ferro-orm/commit/a328cfcd107e2cac998ddc8af50845857ec669a5))
76
+
77
+ - **ff-e**: Registry & model identity — qualified keys, configurable tables, O(N) import
78
+ ([#209](https://github.com/syn54x/ferro-orm/pull/209),
79
+ [`6391805`](https://github.com/syn54x/ferro-orm/commit/6391805c1b4ee46c0590513ac6684237c18f7430))
80
+
81
+ - **ff-f**: Query builder 1.0 shape — immutable chaining, build-time column validation, lambda-only
82
+ predicates, QueryIR-only Rust ([#223](https://github.com/syn54x/ferro-orm/pull/223),
83
+ [`eb7fece`](https://github.com/syn54x/ferro-orm/commit/eb7fece8fbc2231220ecf9111616f803f2f987eb))
84
+
85
+ - **ff-g-a**: Hardening — hydration ABI guard, transactional PG migrate, correctness edges,
86
+ decode-path caching ([#232](https://github.com/syn54x/ferro-orm/pull/232),
87
+ [`22617e1`](https://github.com/syn54x/ferro-orm/commit/22617e1cfe5e2bc9d0a3d7a1fed5acf4d8589cef))
88
+
89
+ ### Refactoring
90
+
91
+ - **ff-g-b**: Operations.rs dedup — ModelMeta + Executor (G2)
92
+ ([#233](https://github.com/syn54x/ferro-orm/pull/233),
93
+ [`09c2c62`](https://github.com/syn54x/ferro-orm/commit/09c2c6214af9e2f9510d39d037935c53bd2e796d))
94
+
95
+ ### Testing
96
+
97
+ - **ff-b**: B5 I-1 sentinel on the full backend matrix with a full-type fixture, zero filters
98
+ ([`1141eb4`](https://github.com/syn54x/ferro-orm/commit/1141eb437b4843303e01556a5b24ddee9d0e0279))
99
+
100
+ - **ff-b**: Force psycopg v3 driver in the Postgres sentinel regardless of URL scheme
101
+ ([`64f9845`](https://github.com/syn54x/ferro-orm/commit/64f98454bdb5e090a41bd214de06b7670974f4eb))
102
+
103
+
4
104
  ## v0.13.0 (2026-07-02)
5
105
 
6
106
  ### Bug Fixes
@@ -8,6 +8,15 @@ version = "0.2.21"
8
8
  source = "registry+https://github.com/rust-lang/crates.io-index"
9
9
  checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
10
10
 
11
+ [[package]]
12
+ name = "android_system_properties"
13
+ version = "0.1.5"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
16
+ dependencies = [
17
+ "libc",
18
+ ]
19
+
11
20
  [[package]]
12
21
  name = "atoi"
13
22
  version = "2.0.0"
@@ -35,6 +44,19 @@ version = "1.8.3"
35
44
  source = "registry+https://github.com/rust-lang/crates.io-index"
36
45
  checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
37
46
 
47
+ [[package]]
48
+ name = "bigdecimal"
49
+ version = "0.4.10"
50
+ source = "registry+https://github.com/rust-lang/crates.io-index"
51
+ checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695"
52
+ dependencies = [
53
+ "autocfg",
54
+ "libm",
55
+ "num-bigint",
56
+ "num-integer",
57
+ "num-traits",
58
+ ]
59
+
38
60
  [[package]]
39
61
  name = "bitflags"
40
62
  version = "2.13.0"
@@ -73,9 +95,9 @@ checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
73
95
 
74
96
  [[package]]
75
97
  name = "cc"
76
- version = "1.2.65"
98
+ version = "1.2.66"
77
99
  source = "registry+https://github.com/rust-lang/crates.io-index"
78
- checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
100
+ checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996"
79
101
  dependencies = [
80
102
  "find-msvc-tools",
81
103
  "shlex",
@@ -87,6 +109,17 @@ version = "1.0.4"
87
109
  source = "registry+https://github.com/rust-lang/crates.io-index"
88
110
  checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
89
111
 
112
+ [[package]]
113
+ name = "chrono"
114
+ version = "0.4.45"
115
+ source = "registry+https://github.com/rust-lang/crates.io-index"
116
+ checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
117
+ dependencies = [
118
+ "iana-time-zone",
119
+ "num-traits",
120
+ "windows-link",
121
+ ]
122
+
90
123
  [[package]]
91
124
  name = "concurrent-queue"
92
125
  version = "2.5.0"
@@ -102,6 +135,12 @@ version = "0.9.6"
102
135
  source = "registry+https://github.com/rust-lang/crates.io-index"
103
136
  checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
104
137
 
138
+ [[package]]
139
+ name = "core-foundation-sys"
140
+ version = "0.8.7"
141
+ source = "registry+https://github.com/rust-lang/crates.io-index"
142
+ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
143
+
105
144
  [[package]]
106
145
  name = "cpufeatures"
107
146
  version = "0.2.17"
@@ -128,18 +167,18 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
128
167
 
129
168
  [[package]]
130
169
  name = "crossbeam-queue"
131
- version = "0.3.12"
170
+ version = "0.3.13"
132
171
  source = "registry+https://github.com/rust-lang/crates.io-index"
133
- checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
172
+ checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
134
173
  dependencies = [
135
174
  "crossbeam-utils",
136
175
  ]
137
176
 
138
177
  [[package]]
139
178
  name = "crossbeam-utils"
140
- version = "0.8.21"
179
+ version = "0.8.22"
141
180
  source = "registry+https://github.com/rust-lang/crates.io-index"
142
- checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
181
+ checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
143
182
 
144
183
  [[package]]
145
184
  name = "crypto-common"
@@ -288,8 +327,9 @@ dependencies = [
288
327
 
289
328
  [[package]]
290
329
  name = "ferro"
291
- version = "0.13.0"
330
+ version = "0.14.0"
292
331
  dependencies = [
332
+ "chrono",
293
333
  "dashmap",
294
334
  "ferro-ddl-lowering",
295
335
  "ferro-migrate",
@@ -578,6 +618,30 @@ dependencies = [
578
618
  "windows-sys 0.61.2",
579
619
  ]
580
620
 
621
+ [[package]]
622
+ name = "iana-time-zone"
623
+ version = "0.1.65"
624
+ source = "registry+https://github.com/rust-lang/crates.io-index"
625
+ checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
626
+ dependencies = [
627
+ "android_system_properties",
628
+ "core-foundation-sys",
629
+ "iana-time-zone-haiku",
630
+ "js-sys",
631
+ "log",
632
+ "wasm-bindgen",
633
+ "windows-core",
634
+ ]
635
+
636
+ [[package]]
637
+ name = "iana-time-zone-haiku"
638
+ version = "0.1.2"
639
+ source = "registry+https://github.com/rust-lang/crates.io-index"
640
+ checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
641
+ dependencies = [
642
+ "cc",
643
+ ]
644
+
581
645
  [[package]]
582
646
  name = "icu_collections"
583
647
  version = "2.2.0"
@@ -835,6 +899,16 @@ dependencies = [
835
899
  "windows-sys 0.61.2",
836
900
  ]
837
901
 
902
+ [[package]]
903
+ name = "num-bigint"
904
+ version = "0.4.8"
905
+ source = "registry+https://github.com/rust-lang/crates.io-index"
906
+ checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
907
+ dependencies = [
908
+ "num-integer",
909
+ "num-traits",
910
+ ]
911
+
838
912
  [[package]]
839
913
  name = "num-bigint-dig"
840
914
  version = "0.8.6"
@@ -1210,9 +1284,9 @@ dependencies = [
1210
1284
 
1211
1285
  [[package]]
1212
1286
  name = "rustversion"
1213
- version = "1.0.22"
1287
+ version = "1.0.23"
1214
1288
  source = "registry+https://github.com/rust-lang/crates.io-index"
1215
- checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
1289
+ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
1216
1290
 
1217
1291
  [[package]]
1218
1292
  name = "ryu"
@@ -1418,7 +1492,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1418
1492
  checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
1419
1493
  dependencies = [
1420
1494
  "base64",
1495
+ "bigdecimal",
1421
1496
  "bytes",
1497
+ "chrono",
1422
1498
  "crc",
1423
1499
  "crossbeam-queue",
1424
1500
  "either",
@@ -1494,9 +1570,11 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
1494
1570
  dependencies = [
1495
1571
  "atoi",
1496
1572
  "base64",
1573
+ "bigdecimal",
1497
1574
  "bitflags",
1498
1575
  "byteorder",
1499
1576
  "bytes",
1577
+ "chrono",
1500
1578
  "crc",
1501
1579
  "digest",
1502
1580
  "dotenvy",
@@ -1537,8 +1615,10 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
1537
1615
  dependencies = [
1538
1616
  "atoi",
1539
1617
  "base64",
1618
+ "bigdecimal",
1540
1619
  "bitflags",
1541
1620
  "byteorder",
1621
+ "chrono",
1542
1622
  "crc",
1543
1623
  "dotenvy",
1544
1624
  "etcetera",
@@ -1553,6 +1633,7 @@ dependencies = [
1553
1633
  "log",
1554
1634
  "md-5",
1555
1635
  "memchr",
1636
+ "num-bigint",
1556
1637
  "once_cell",
1557
1638
  "rand",
1558
1639
  "serde",
@@ -1574,6 +1655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1574
1655
  checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
1575
1656
  dependencies = [
1576
1657
  "atoi",
1658
+ "chrono",
1577
1659
  "flume",
1578
1660
  "futures-channel",
1579
1661
  "futures-core",
@@ -1930,12 +2012,65 @@ dependencies = [
1930
2012
  "wasite",
1931
2013
  ]
1932
2014
 
2015
+ [[package]]
2016
+ name = "windows-core"
2017
+ version = "0.62.2"
2018
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2019
+ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
2020
+ dependencies = [
2021
+ "windows-implement",
2022
+ "windows-interface",
2023
+ "windows-link",
2024
+ "windows-result",
2025
+ "windows-strings",
2026
+ ]
2027
+
2028
+ [[package]]
2029
+ name = "windows-implement"
2030
+ version = "0.60.2"
2031
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2032
+ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
2033
+ dependencies = [
2034
+ "proc-macro2",
2035
+ "quote",
2036
+ "syn",
2037
+ ]
2038
+
2039
+ [[package]]
2040
+ name = "windows-interface"
2041
+ version = "0.59.3"
2042
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2043
+ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
2044
+ dependencies = [
2045
+ "proc-macro2",
2046
+ "quote",
2047
+ "syn",
2048
+ ]
2049
+
1933
2050
  [[package]]
1934
2051
  name = "windows-link"
1935
2052
  version = "0.2.1"
1936
2053
  source = "registry+https://github.com/rust-lang/crates.io-index"
1937
2054
  checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
1938
2055
 
2056
+ [[package]]
2057
+ name = "windows-result"
2058
+ version = "0.4.1"
2059
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2060
+ checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
2061
+ dependencies = [
2062
+ "windows-link",
2063
+ ]
2064
+
2065
+ [[package]]
2066
+ name = "windows-strings"
2067
+ version = "0.5.1"
2068
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2069
+ checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
2070
+ dependencies = [
2071
+ "windows-link",
2072
+ ]
2073
+
1939
2074
  [[package]]
1940
2075
  name = "windows-sys"
1941
2076
  version = "0.48.0"
@@ -2115,18 +2250,18 @@ dependencies = [
2115
2250
 
2116
2251
  [[package]]
2117
2252
  name = "zerocopy"
2118
- version = "0.8.52"
2253
+ version = "0.8.53"
2119
2254
  source = "registry+https://github.com/rust-lang/crates.io-index"
2120
- checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
2255
+ checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1"
2121
2256
  dependencies = [
2122
2257
  "zerocopy-derive",
2123
2258
  ]
2124
2259
 
2125
2260
  [[package]]
2126
2261
  name = "zerocopy-derive"
2127
- version = "0.8.52"
2262
+ version = "0.8.53"
2128
2263
  source = "registry+https://github.com/rust-lang/crates.io-index"
2129
- checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
2264
+ checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71"
2130
2265
  dependencies = [
2131
2266
  "proc-macro2",
2132
2267
  "quote",
@@ -3,7 +3,7 @@ members = ["crates/ferro-schema-ir", "crates/ferro-ddl-lowering", "crates/ferro-
3
3
 
4
4
  [package]
5
5
  name = "ferro"
6
- version = "0.13.0"
6
+ version = "0.14.0"
7
7
  edition = "2024"
8
8
  readme = "README.md"
9
9
 
@@ -30,11 +30,18 @@ serde = { version = "1.0", features = ["derive"] }
30
30
  serde_json = "1.0"
31
31
  once_cell = "1.21"
32
32
  dashmap = "6.1"
33
- sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "postgres", "any", "tls-rustls-ring-webpki", "uuid"]}
33
+ # chrono/bigdecimal/json enable native typed decode of Postgres temporal,
34
+ # numeric, and json wire values (FF-C C3). bigdecimal (not rust_decimal)
35
+ # because PG `numeric` is arbitrary-precision: rust_decimal caps at 28
36
+ # significant digits and would silently corrupt wider values.
37
+ sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "postgres", "any", "tls-rustls-ring-webpki", "uuid", "chrono", "bigdecimal", "json"]}
34
38
  sea-query = { version = "0.32", features = ["with-uuid"] }
35
39
  tokio = { version = "1.49", features = ["full"] }
36
40
  pyo3-async-runtimes = { version = "0.27", features = ["tokio-runtime"] }
37
41
  uuid = { version = "1.11", features = ["v4"] }
42
+ # Same crate sqlx re-exports; direct dep only for formatting helpers
43
+ # (SecondsFormat) sqlx does not re-export.
44
+ chrono = { version = "0.4", default-features = false, features = ["std"] }
38
45
  ferro-schema-ir = { path = "crates/ferro-schema-ir" }
39
46
  ferro-ddl-lowering = { path = "crates/ferro-ddl-lowering" }
40
47
  ferro-migrate = { path = "crates/ferro-migrate" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ferro-orm
3
- Version: 0.13.0
3
+ Version: 0.14.0
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'
@@ -54,7 +54,7 @@ pip install ferro-orm
54
54
  pip install "ferro-orm[alembic]"
55
55
  ```
56
56
 
57
- Ferro currently supports SQLite and PostgreSQL. Register multiple named connections with `connect(..., name="...")` when a process needs more than one database — see the [connections guide](https://syn54x.github.io/ferro-orm/guide/connections/).
57
+ Ferro currently supports SQLite and PostgreSQL. Register multiple named connections with `connect(..., name="...")` when a process needs more than one database — see the [connections guide](https://ferro-orm.x54.sh/guide/connections/).
58
58
 
59
59
  ## Quick Start
60
60
 
@@ -41,7 +41,7 @@ pip install ferro-orm
41
41
  pip install "ferro-orm[alembic]"
42
42
  ```
43
43
 
44
- Ferro currently supports SQLite and PostgreSQL. Register multiple named connections with `connect(..., name="...")` when a process needs more than one database — see the [connections guide](https://syn54x.github.io/ferro-orm/guide/connections/).
44
+ Ferro currently supports SQLite and PostgreSQL. Register multiple named connections with `connect(..., name="...")` when a process needs more than one database — see the [connections guide](https://ferro-orm.x54.sh/guide/connections/).
45
45
 
46
46
  ## Quick Start
47
47
 
@@ -0,0 +1,133 @@
1
+ # Ferro hot-path benchmarks
2
+
3
+ A small, pinned, repeatable suite that measures Ferro's Python-facing async hot
4
+ path — **single save**, **bulk save**, and a **filtered fetch of ~10k rows** — on
5
+ both backends, over a rich-type fixture model. It exists so the before/after of
6
+ Epic **FF-C** (per-model codec plans, catalog cache, native Postgres decode, enum
7
+ hydration) is quantifiable and future hot-path regressions are visible.
8
+
9
+ It is **purely additive**: it imports the public Ferro API and changes no
10
+ production code. It is **not** part of `just test`, so it never slows the
11
+ functional matrix.
12
+
13
+ ## What it measures
14
+
15
+ Four cases per backend, over `BenchRecord` (see `model.py`):
16
+
17
+ | benchmark | operation | rows |
18
+ | ------------------------- | ----------------------------------------------------- | ---- |
19
+ | `single_save` | `record.save()` — one INSERT | 1 |
20
+ | `bulk_save` | `BenchRecord.bulk_create([...])` — one bulk INSERT | 10k |
21
+ | `fetch_10k_identity_on` | `BenchRecord.where(...).all()`, identity map **on** | 10k |
22
+ | `fetch_10k_identity_off` | `BenchRecord.where(...).all()`, identity map **off** | 10k |
23
+
24
+ Only **wall-clock** is recorded (median / min / mean / stdev / p95, in ms).
25
+
26
+ ### Why the rich-type fixture is load-bearing
27
+
28
+ A plain `int`/`str` model would show *nothing* from FF-C. Every `BenchRecord`
29
+ field maps to a concrete cost C1–C4 removes, so the suite makes that work
30
+ visible:
31
+
32
+ - `created_at: datetime` (`db_type="timestamptz"`) → **C3** native typed decode
33
+ (today a `CAST(... AS text)` projection on Postgres — finding **F8**).
34
+ - `balance: Decimal` → **C1/F5** per-value codec (the `pattern_looks_decimal`
35
+ heuristic C1 deletes).
36
+ - `external_id: uuid.UUID` (`db_type="uuid"`) → **C3** typed decode / UUID casing.
37
+ - `status: Status` (`str, Enum`, no `db_type`) → native Postgres enum, **C4**
38
+ enum hydration (removes the `_fix_types` Python post-pass).
39
+ - `attributes: dict[str, str]` → JSON codec.
40
+ - `name`, `score` → the plain-typed baseline columns.
41
+
42
+ Catalog round-trips (**F4**) are incurred by all of the above during
43
+ save/fetch/migrate, so they are exercised implicitly.
44
+
45
+ ## Running
46
+
47
+ ```bash
48
+ just bench # SQLite always; Postgres if FERRO_POSTGRES_URL is set
49
+ # or directly:
50
+ uv run python -m benchmarks.run
51
+ ```
52
+
53
+ Postgres is included automatically when a URL is configured (via
54
+ `FERRO_POSTGRES_URL` or a repo-root `.env`, reusing `tests/db_backends.py`); it is
55
+ skipped with a printed note otherwise.
56
+
57
+ Useful flags: `--rows N`, `--iters N`, `--warmup N`, `--seed N`,
58
+ `--backends sqlite,postgres`, `--out DIR`.
59
+
60
+ Each run writes `baselines/<backend>.json` (per-backend, keyed by benchmark name)
61
+ and prints a summary table.
62
+
63
+ ## Capturing a before/after around a change
64
+
65
+ ```bash
66
+ # 1. Baseline the current code into a scratch dir
67
+ uv run python -m benchmarks.run --out /tmp/before
68
+
69
+ # 2. Make your hot-path change, rebuild the Rust core
70
+ uv run maturin develop
71
+
72
+ # 3. Re-run into a second dir
73
+ uv run python -m benchmarks.run --out /tmp/after
74
+
75
+ # 4. Diff the medians (per backend)
76
+ uv run python -m benchmarks.compare /tmp/before/sqlite.json /tmp/after/sqlite.json
77
+ uv run python -m benchmarks.compare /tmp/before/postgres.json /tmp/after/postgres.json
78
+ ```
79
+
80
+ `compare` prints each benchmark's `old → new` median with **Δms** and **Δ%**.
81
+ Negative % is a speed-up (green); positive is a regression (red).
82
+
83
+ ## Reading the deltas
84
+
85
+ - Compare **same backend, same machine** only. Absolute numbers vary wildly by
86
+ backend and hardware; the deliverable is a stable *per-backend delta*, never a
87
+ SQLite-vs-Postgres comparison.
88
+ - The checked-in `baselines/*.json` were captured on a developer machine and are
89
+ a reference point, not a CI gate. Re-capture your own baseline before measuring
90
+ a change so both sides come from the same box.
91
+ - Watch the **median** for the headline and **stdev/p95** for noise. If stdev is
92
+ a large fraction of the median, raise `--warmup`/`--iters` until it settles
93
+ before trusting a delta.
94
+
95
+ ## Stability knobs
96
+
97
+ Fixed row counts, deterministic seed data (`random.Random(seed)`), discarded
98
+ warmup iterations, a fresh database per case (temp SQLite file / dropped-and-
99
+ recreated PG schema), a pinned pool (`max_connections=5`), and a single
100
+ persistent event loop across all cases. The fetch cases are measured with the
101
+ identity map both on and off because that bookkeeping is real hot-path cost the
102
+ FF-C rewrite interacts with.
103
+
104
+ ## FF-C epic results (recorded 2026-07-02)
105
+
106
+ Full before/after of Epic FF-C — **before** is the pre-C1 baseline capture
107
+ (`d9a656b`), **after** is post-C2 (catalog cache; C1 codec plan, C3 native
108
+ decode, and C4 enum hydration included). Same machine, release-profile `.so`
109
+ (sha-verified), medians in ms.
110
+
111
+ **Postgres**
112
+
113
+ | benchmark | pre-C1 | post-C2 | Δ % | C2-only Δ % (vs post-C3) |
114
+ | ------------------------ | ------ | ------- | ------ | ------------------------ |
115
+ | single_save | 1.708 | 0.399 | −76.6% | −66.9% |
116
+ | bulk_save | 389.5 | 92.1 | −76.4% | −11.8% |
117
+ | fetch_10000_identity_on | 189.5 | 16.1 | −91.5% | −16.9% |
118
+ | fetch_10000_identity_off | 285.9 | 67.3 | −76.5% | +0.0% |
119
+
120
+ **SQLite**
121
+
122
+ | benchmark | pre-C1 | post-C2 | Δ % | C2-only Δ % (vs post-C3) |
123
+ | ------------------------ | ------ | ------- | ------ | ------------------------ |
124
+ | single_save | 0.352 | 0.277 | −21.2% | −9.8% |
125
+ | bulk_save | 221.5 | 54.6 | −75.4% | −0.1% |
126
+ | fetch_10000_identity_on | 175.5 | 34.5 | −80.4% | +1.0% |
127
+ | fetch_10000_identity_off | 271.2 | 80.9 | −70.2% | +0.4% |
128
+
129
+ The C2 column isolates the catalog cache: on Postgres the save path drops
130
+ sharply (3 catalog round-trips per op eliminated — F4; single_save
131
+ 1.21 → 0.40 ms), while SQLite deltas are noise, as required (the cache is a
132
+ no-op off Postgres). The checked-in `baselines/*.json` are the post-C2
133
+ capture.
@@ -0,0 +1,11 @@
1
+ """Ferro hot-path benchmark suite (Epic FF-C, sub-task C5).
2
+
3
+ A small, pinned, repeatable suite measuring the Python-facing async hot path —
4
+ single save, bulk save, and a filtered fetch of ~10k rows — on both backends
5
+ (SQLite always; Postgres when ``FERRO_POSTGRES_URL`` is configured), over a
6
+ rich-type fixture model that exercises the type-decision path FF-C's C1–C4
7
+ rewrite touches.
8
+
9
+ The suite is purely additive: it imports the public Ferro API and never changes
10
+ production code. Run it with ``just bench`` (see ``benchmarks/README.md``).
11
+ """
@@ -0,0 +1,10 @@
1
+ """``python -m benchmarks`` → run the suite (alias for ``benchmarks.run``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from benchmarks.run import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())