ferro-orm 0.16.0__tar.gz → 0.16.1__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 (361) hide show
  1. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/CHANGELOG.md +8 -0
  2. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/Cargo.lock +1 -1
  3. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/Cargo.toml +1 -1
  4. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/PKG-INFO +1 -1
  5. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/performance.md +2 -2
  6. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/mutations.md +3 -1
  7. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/pyproject.toml +1 -1
  8. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/backend.rs +13 -1
  9. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/models.py +6 -0
  10. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/operations.rs +106 -19
  11. ferro_orm-0.16.1/tests/test_bulk_create_chunking.py +167 -0
  12. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  13. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  14. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/PERMISSIONS.md +0 -0
  15. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/PYPI_CHECKLIST.md +0 -0
  16. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/PYPI_SETUP.md +0 -0
  17. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/generated/wheels.generated.yml +0 -0
  18. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/pull_request_template.md +0 -0
  19. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/workflows/ci.yml +0 -0
  20. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/workflows/packaging-smoke.yml +0 -0
  21. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/workflows/publish-docs.yml +0 -0
  22. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/workflows/publish.yml +0 -0
  23. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/workflows/release.yml +0 -0
  24. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.gitignore +0 -0
  25. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.pre-commit-config.yaml +0 -0
  26. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.python-version +0 -0
  27. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/AGENTS.md +0 -0
  28. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/CONTEXT.md +0 -0
  29. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/CONTRIBUTING.md +0 -0
  30. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/LICENSE +0 -0
  31. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/README.md +0 -0
  32. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/README.md +0 -0
  33. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/__init__.py +0 -0
  34. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/__main__.py +0 -0
  35. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/backends.py +0 -0
  36. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/baselines/postgres.json +0 -0
  37. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/baselines/sqlite.json +0 -0
  38. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/compare.py +0 -0
  39. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/harness.py +0 -0
  40. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/model.py +0 -0
  41. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/run.py +0 -0
  42. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
  43. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-ddl-lowering/src/lib.rs +0 -0
  44. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-migrate/Cargo.toml +0 -0
  45. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-migrate/src/emit.rs +0 -0
  46. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-migrate/src/lib.rs +0 -0
  47. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-migrate/src/tests.rs +0 -0
  48. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-schema-ir/Cargo.toml +0 -0
  49. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-schema-ir/src/lib.rs +0 -0
  50. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0001-resolved-epoch-registration.md +0 -0
  51. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
  52. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
  53. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
  54. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
  55. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
  56. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
  57. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0008-populated-relations-include.md +0 -0
  58. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
  59. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/agents/domain.md +0 -0
  60. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/agents/issue-tracker.md +0 -0
  61. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/agents/triage-labels.md +0 -0
  62. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/architecture-review-2026-07-08.html +0 -0
  63. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
  64. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
  65. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
  66. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
  67. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
  68. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
  69. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
  70. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
  71. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-26-ir-p8.5-143-db-type-drop-requirements.md +0 -0
  72. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
  73. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-28-ir-p8.5-141-single-schema-ir-producer-requirements.md +0 -0
  74. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-29-ir-p8.6-146-dialect-enum-unification-requirements.md +0 -0
  75. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-29-ir-p8.6-153-create-path-ir-requirements.md +0 -0
  76. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
  77. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
  78. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-07-01-ir-p8.6-154-datetime-tz-coarseness-requirements.md +0 -0
  79. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-07-01-ir-p8.6-162-typed-save-bind-requirements.md +0 -0
  80. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-07-01-ir-p8.6-165-blob-introspection-false-positive-requirements.md +0 -0
  81. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
  82. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
  83. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/aggregations.py +0 -0
  84. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/aggregations_annotated.py +0 -0
  85. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/multiple_databases.py +0 -0
  86. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/mutations.py +0 -0
  87. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/pagination.py +0 -0
  88. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/partial_selects.py +0 -0
  89. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/partial_selects_annotated.py +0 -0
  90. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/populated_relations.py +0 -0
  91. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/populated_relations_annotated.py +0 -0
  92. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/predicates.py +0 -0
  93. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/predicates_annotated.py +0 -0
  94. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/quickstart.py +0 -0
  95. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/quickstart_annotated.py +0 -0
  96. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/raw_sql.py +0 -0
  97. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/relationships.py +0 -0
  98. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/relationships_annotated.py +0 -0
  99. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/soft_deletes.py +0 -0
  100. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/soft_deletes_annotated.py +0 -0
  101. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/testing_conftest.py +0 -0
  102. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/timestamps.py +0 -0
  103. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/timestamps_annotated.py +0 -0
  104. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/transactions.py +0 -0
  105. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/traversal.py +0 -0
  106. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/traversal_annotated.py +0 -0
  107. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/connection.md +0 -0
  108. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/exceptions.md +0 -0
  109. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/fields.md +0 -0
  110. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/migrations.md +0 -0
  111. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/model.md +0 -0
  112. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/queries.md +0 -0
  113. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/raw-sql.md +0 -0
  114. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/relationships.md +0 -0
  115. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/transactions.md +0 -0
  116. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/changelog.md +0 -0
  117. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/architecture.md +0 -0
  118. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/backends.md +0 -0
  119. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/identity-map.md +0 -0
  120. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/query-typing.md +0 -0
  121. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/type-safety.md +0 -0
  122. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/contributing.md +0 -0
  123. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/faq.md +0 -0
  124. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/getting-started/installation.md +0 -0
  125. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/getting-started/next-steps.md +0 -0
  126. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/getting-started/quickstart.md +0 -0
  127. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/aggregations.md +0 -0
  128. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/connections.md +0 -0
  129. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/migrations.md +0 -0
  130. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/models-and-fields.md +0 -0
  131. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/queries.md +0 -0
  132. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/raw-sql.md +0 -0
  133. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/relationships.md +0 -0
  134. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/transactions.md +0 -0
  135. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
  136. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/multiple-databases.md +0 -0
  137. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/pagination.md +0 -0
  138. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/soft-deletes.md +0 -0
  139. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/testing.md +0 -0
  140. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/timestamps.md +0 -0
  141. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/upgrade-guide.md +0 -0
  142. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/index.md +0 -0
  143. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/roadmap.md +0 -0
  144. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/stylesheets/extra.css +0 -0
  145. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/why-ferro.md +0 -0
  146. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
  147. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
  148. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
  149. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
  150. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
  151. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
  152. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
  153. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
  154. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-24-001-feat-ir-p8-119-wire-automigrate-plan.md +0 -0
  155. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-25-001-feat-ir-p8-120-parity-gate-plan.md +0 -0
  156. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-26-001-feat-ir-p8.5-140-shared-lowering-plan.md +0 -0
  157. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-26-002-feat-ir-p8.5-143-db-type-drop-plan.md +0 -0
  158. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-26-003-feat-ir-p8.5-144-reconcile-indexes-plan.md +0 -0
  159. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-28-001-feat-ir-p8.5-141-single-schema-ir-producer-plan.md +0 -0
  160. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-29-001-refactor-ir-p8.6-153-create-path-unification-plan.md +0 -0
  161. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-29-002-refactor-ir-p8.6-146-dialect-enum-unification-plan.md +0 -0
  162. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-30-001-refactor-ir-p8.6-158-check-renderer-plan.md +0 -0
  163. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-30-002-fix-ir-p8.6-155-deferred-annotations-plan.md +0 -0
  164. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-01-001-feat-ir-p8.6-162-typed-save-bind-plan.md +0 -0
  165. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-01-002-fix-ir-p8.6-154-datetime-tz-coarseness-plan.md +0 -0
  166. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-01-003-fix-ir-p8.6-165-blob-introspection-plan.md +0 -0
  167. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
  168. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-02-002-feat-ff-a-172-typed-exceptions-plan.md +0 -0
  169. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
  170. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +0 -0
  171. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
  172. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
  173. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
  174. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
  175. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-009-ff-f-query-builder-1.0-design.md +0 -0
  176. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-010-ff-f-query-builder-1.0-plan.md +0 -0
  177. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
  178. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
  179. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
  180. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
  181. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
  182. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/ir-first-migration-guide.md +0 -0
  183. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/ir-first-release-checklist.md +0 -0
  184. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/rfc/ir-contracts-v1.md +0 -0
  185. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/README.md +0 -0
  186. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
  187. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
  188. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
  189. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
  190. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
  191. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
  192. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
  193. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
  194. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
  195. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
  196. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
  197. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
  198. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
  199. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/foreign-key-index.md +0 -0
  200. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
  201. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/ir-invariants.md +0 -0
  202. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/persistence-state.md +0 -0
  203. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
  204. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
  205. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/typed-null-binds.md +0 -0
  206. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/justfile +0 -0
  207. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/scripts/demo_queries.py +0 -0
  208. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/scripts/demo_queries_pre_v012.py +0 -0
  209. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/codec.rs +0 -0
  210. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/codec_plan.rs +0 -0
  211. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/connection.rs +0 -0
  212. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/errors.rs +0 -0
  213. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/__init__.py +0 -0
  214. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/_annotation_utils.py +0 -0
  215. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/_bind_payload.py +0 -0
  216. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/_core.pyi +0 -0
  217. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/_deprecations.py +0 -0
  218. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/_shadow_fk_types.py +0 -0
  219. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/base.py +0 -0
  220. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/columns.py +0 -0
  221. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/composite_indexes.py +0 -0
  222. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/composite_uniques.py +0 -0
  223. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/exceptions.py +0 -0
  224. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/fields.py +0 -0
  225. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/ir/__init__.py +0 -0
  226. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/ir/compiler.py +0 -0
  227. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/metaclass.py +0 -0
  228. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/migrations/__init__.py +0 -0
  229. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/migrations/alembic.py +0 -0
  230. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/py.typed +0 -0
  231. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/query/__init__.py +0 -0
  232. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/query/builder.py +0 -0
  233. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/query/nodes.py +0 -0
  234. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/query/rows.py +0 -0
  235. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/raw.py +0 -0
  236. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/relations/__init__.py +0 -0
  237. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/relations/descriptors.py +0 -0
  238. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/session.py +0 -0
  239. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/state.py +0 -0
  240. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/hydration.rs +0 -0
  241. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/introspect.rs +0 -0
  242. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/lib.rs +0 -0
  243. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/migrate.rs +0 -0
  244. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/naming_ffi.rs +0 -0
  245. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/query.rs +0 -0
  246. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/schema.rs +0 -0
  247. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/schema_bind.rs +0 -0
  248. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/state.rs +0 -0
  249. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/__init__.py +0 -0
  250. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/conftest.py +0 -0
  251. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/db_backends.py +0 -0
  252. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/README.md +0 -0
  253. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
  254. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_aggregate_v5.json +0 -0
  255. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v5.json +0 -0
  256. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_include_v5.json +0 -0
  257. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_left_join_v5.json +0 -0
  258. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_record_v5.json +0 -0
  259. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_traversal_v5.json +0 -0
  260. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_traversed_record_v5.json +0 -0
  261. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_user_compound_v5.json +0 -0
  262. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
  263. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
  264. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
  265. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/bad_includes.py +0 -0
  266. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/bad_predicates.py +0 -0
  267. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/bad_projections.py +0 -0
  268. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/good_includes.py +0 -0
  269. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/good_predicates.py +0 -0
  270. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/good_projections.py +0 -0
  271. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_aggregation.py +0 -0
  272. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_alembic_autogenerate.py +0 -0
  273. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_alembic_bridge.py +0 -0
  274. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_alembic_db_type.py +0 -0
  275. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_alembic_nullability.py +0 -0
  276. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_alembic_type_mapping.py +0 -0
  277. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_annotation_utils.py +0 -0
  278. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_auto_migrate.py +0 -0
  279. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_bulk_install.py +0 -0
  280. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_bulk_update.py +0 -0
  281. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_catalog_cache.py +0 -0
  282. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_codec_plan.py +0 -0
  283. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_column_specs.py +0 -0
  284. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_composite_index.py +0 -0
  285. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_composite_unique.py +0 -0
  286. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_connection.py +0 -0
  287. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_connection_redaction.py +0 -0
  288. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_constraints.py +0 -0
  289. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_cross_emitter_parity.py +0 -0
  290. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_crud.py +0 -0
  291. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_db_backends.py +0 -0
  292. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_db_type_cross_emitter_parity.py +0 -0
  293. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_db_type_integration.py +0 -0
  294. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_db_type_typing.py +0 -0
  295. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_db_type_validation.py +0 -0
  296. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_deletion.py +0 -0
  297. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_deprecations.py +0 -0
  298. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_docs_examples.py +0 -0
  299. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_documentation_features.py +0 -0
  300. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_enum_cold_hydration.py +0 -0
  301. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_exception_mapping.py +0 -0
  302. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_exceptions.py +0 -0
  303. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_field_wrapper.py +0 -0
  304. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_fk_target_pk.py +0 -0
  305. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_generation_counter_dirty_tracking.py +0 -0
  306. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_global_aggregates.py +0 -0
  307. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_grouped_aggregates.py +0 -0
  308. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_helpers.py +0 -0
  309. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_hydration.py +0 -0
  310. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_hydration_equivalence.py +0 -0
  311. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_identity_memory.py +0 -0
  312. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_identity_refresh.py +0 -0
  313. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_identity_scoped_invalidation.py +0 -0
  314. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_identity_weakref.py +0 -0
  315. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_import_budget.py +0 -0
  316. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_include.py +0 -0
  317. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_ir_vectors_contract.py +0 -0
  318. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_metaclass_internals.py +0 -0
  319. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_metadata.py +0 -0
  320. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_migrate_plan.py +0 -0
  321. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_model_identity.py +0 -0
  322. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_models.py +0 -0
  323. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_mutation_pagination_guard.py +0 -0
  324. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_named_connections_integration.py +0 -0
  325. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_naming_single_source.py +0 -0
  326. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_one_to_one.py +0 -0
  327. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_operation_seam_sync.py +0 -0
  328. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_partial_selects.py +0 -0
  329. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_provisional_import.py +0 -0
  330. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_query_builder.py +0 -0
  331. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_query_column_validation.py +0 -0
  332. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_query_immutability.py +0 -0
  333. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_query_joins.py +0 -0
  334. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_query_typing.py +0 -0
  335. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_raw_sql.py +0 -0
  336. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_refresh.py +0 -0
  337. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_registry_entrypoints.py +0 -0
  338. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_relation_specs.py +0 -0
  339. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_relationship_engine.py +0 -0
  340. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_relationship_resolution_errors.py +0 -0
  341. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_route_single_site.py +0 -0
  342. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_routing_errors.py +0 -0
  343. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_save_pk_edges.py +0 -0
  344. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_save_semantics.py +0 -0
  345. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_schema.py +0 -0
  346. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_schema_constraints.py +0 -0
  347. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_schema_db_type_metadata.py +0 -0
  348. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_schema_enum_annotations.py +0 -0
  349. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_session.py +0 -0
  350. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_shadow_fk_types.py +0 -0
  351. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
  352. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_static_contracts.py +0 -0
  353. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_string_search.py +0 -0
  354. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_structural_types.py +0 -0
  355. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_temporal_types.py +0 -0
  356. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_transactions.py +0 -0
  357. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_traversed_projection.py +0 -0
  358. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_typed_null_binds.py +0 -0
  359. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_typed_save_bind.py +0 -0
  360. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/uv.lock +0 -0
  361. {ferro_orm-0.16.0 → ferro_orm-0.16.1}/zensical.toml +0 -0
@@ -1,6 +1,14 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.16.1 (2026-07-13)
5
+
6
+ ### Bug Fixes
7
+
8
+ - Chunk bulk_create under backend bind-parameter limits
9
+ ([`38da62d`](https://github.com/syn54x/ferro-orm/commit/38da62d9ab9fab6b36a9eae0722c1d741b976d15))
10
+
11
+
4
12
  ## v0.16.0 (2026-07-13)
5
13
 
6
14
  ### Chores
@@ -327,7 +327,7 @@ dependencies = [
327
327
 
328
328
  [[package]]
329
329
  name = "ferro"
330
- version = "0.16.0"
330
+ version = "0.16.1"
331
331
  dependencies = [
332
332
  "chrono",
333
333
  "dashmap",
@@ -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.16.0"
6
+ version = "0.16.1"
7
7
  edition = "2024"
8
8
  readme = "README.md"
9
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ferro-orm
3
- Version: 0.16.0
3
+ Version: 0.16.1
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'
@@ -6,7 +6,7 @@ Ferro moves SQL generation, parameter binding, and row hydration out of Python a
6
6
 
7
7
  The Rust core helps most where a traditional ORM spends significant CPU time in Python:
8
8
 
9
- **Bulk inserts.** `bulk_create` serializes and binds an entire batch in Rust and writes it as a single statement. The per-row Python overhead — building parameter lists, driver round-trips, object bookkeeping — largely disappears:
9
+ **Bulk inserts.** `bulk_create` serializes and binds an entire batch in Rust and writes it in as few statements as the backend's bind-parameter limit allows. The per-row Python overhead — building parameter lists, driver round-trips, object bookkeeping — largely disappears:
10
10
 
11
11
  ```python
12
12
  users = [
@@ -43,7 +43,7 @@ for i in range(1000):
43
43
  await User.bulk_create([User(username=f"user_{i}") for i in range(1000)])
44
44
  ```
45
45
 
46
- Batches in the low thousands (roughly 1,000–5,000 rows) are a good unit of work — large enough to amortize overhead, small enough to keep statements and memory reasonable. Note that `bulk_create` skips the [identity map](identity-map.md) by design.
46
+ Batch size is unbounded — Ferro splits large batches under the backend's bind-parameter limit automatically and keeps the whole call all-or-nothing — so size batches by what your application can hold in memory, not by statement limits. Note that `bulk_create` skips the [identity map](identity-map.md) by design.
47
47
 
48
48
  **Use batch update/delete instead of instance loops.** Push the work into one SQL statement:
49
49
 
@@ -14,12 +14,14 @@ Ferro's write surface is three verbs with three distinct intents — none of the
14
14
 
15
15
  ### create
16
16
 
17
- `Model.create(**fields)` validates, inserts, and returns the persisted instance in one call. For inserting many rows, `Model.bulk_create(instances)` batches them into a single statement and returns the inserted count:
17
+ `Model.create(**fields)` validates, inserts, and returns the persisted instance in one call. For inserting many rows, `Model.bulk_create(instances)` writes the whole batch in bulk and returns the inserted count:
18
18
 
19
19
  ```python
20
20
  --8<-- "docs/examples/quickstart.py:create"
21
21
  ```
22
22
 
23
+ Batch size is unbounded. Both backends cap how many parameters one statement may bind (SQLite 32,766; PostgreSQL 65,535), so Ferro splits a large batch across as many INSERT statements as the active backend requires — an implementation detail you never see. Atomicity is preserved either way: inside a [`transaction()`](transactions.md) block that transaction is the boundary, and a bare `bulk_create` call is all-or-nothing — if any row fails (say, a unique violation late in the batch), no rows from the batch are inserted.
24
+
23
25
  `create()` is a plain INSERT — it never updates an existing row. A duplicate primary key or unique value raises [`UniqueViolationError`](../api/exceptions.md), and the existing row is left untouched:
24
26
 
25
27
  ```python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ferro-orm"
3
- version = "0.16.0"
3
+ version = "0.16.1"
4
4
  description = "A high-performance, Rust-backed ORM for Python."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -415,6 +415,19 @@ impl EngineHandle {
415
415
  self.backend
416
416
  }
417
417
 
418
+ /// Maximum bind parameters one statement may carry on this backend.
419
+ ///
420
+ /// SQLite: `SQLITE_MAX_VARIABLE_NUMBER`, 32,766 by default since 3.32.
421
+ /// Postgres: the wire protocol's `Bind` message carries an `int16`
422
+ /// parameter count, so 65,535 is a hard protocol ceiling.
423
+ #[must_use]
424
+ pub fn max_bind_params(&self) -> usize {
425
+ match self.backend {
426
+ Dialect::Sqlite => 32_766,
427
+ Dialect::Postgres => 65_535,
428
+ }
429
+ }
430
+
418
431
  /// Record one catalog-introspection round-trip (called at the single
419
432
  /// choke point that executes catalog SQL).
420
433
  pub fn record_catalog_query(&self) {
@@ -611,7 +624,6 @@ impl EngineHandle {
611
624
  }
612
625
  }
613
626
 
614
- #[allow(dead_code)]
615
627
  pub async fn begin_transaction_connection(&self) -> Result<EngineConnection, sqlx::Error> {
616
628
  match &self.pool_snapshot() {
617
629
  BackendPool::Sqlite(pool) => {
@@ -639,6 +639,12 @@ class Model(BaseModel, metaclass=ModelMetaclass):
639
639
  ) -> int:
640
640
  """Persist multiple instances in a single bulk operation
641
641
 
642
+ Batch size is unbounded: batches larger than the backend's
643
+ bind-parameter limit allows in one statement are split internally.
644
+ The call is atomic either way — inside an ambient ``transaction()``
645
+ that transaction is the atomicity boundary; a bare call is
646
+ all-or-nothing (a failure on any row inserts none).
647
+
642
648
  Args:
643
649
  instances: Model instances to persist.
644
650
 
@@ -2296,7 +2296,12 @@ pub fn update_record<'py>(
2296
2296
  })
2297
2297
  }
2298
2298
 
2299
- /// Persist multiple model instances in a single batch `INSERT`.
2299
+ /// Persist multiple model instances in as few batch `INSERT`s as the
2300
+ /// backend's bind-parameter limit allows (#298).
2301
+ ///
2302
+ /// Atomicity: with an ambient transaction every statement runs inside it;
2303
+ /// without one, a multi-statement batch runs in its own transaction so the
2304
+ /// call stays all-or-nothing.
2300
2305
  ///
2301
2306
  /// Args:
2302
2307
  /// name (str): Model class name.
@@ -2340,24 +2345,32 @@ pub fn save_bulk_records<'py>(
2340
2345
 
2341
2346
  let catalog = postgres_table_catalog(&table_name, &engine, exec, backend).await?;
2342
2347
  let TableCatalog { enum_udt, uuid_columns, ts_cast } = &*catalog;
2343
- let (sql, bind_values) = {
2348
+
2349
+ // Columns come from the first row's order (skip a null auto-pk).
2350
+ let mut column_names: Vec<String> = Vec::new();
2351
+ for (key, input) in &record_inputs[0] {
2352
+ let is_pk = pk_col.as_deref() == Some(key.as_str());
2353
+ if is_pk && pk_is_auto && input.is_json_null() {
2354
+ continue;
2355
+ }
2356
+ column_names.push(key.clone());
2357
+ }
2358
+
2359
+ // One multi-row INSERT binds rows × columns parameters, and both
2360
+ // backends cap binds per statement — split the batch so every
2361
+ // statement stays within the active backend's budget (#298).
2362
+ let rows_per_stmt =
2363
+ bulk_insert_rows_per_statement(engine.max_bind_params(), column_names.len());
2364
+
2365
+ let mut statements: Vec<(String, sea_query::Values)> = Vec::new();
2366
+ for chunk in record_inputs.chunks(rows_per_stmt) {
2344
2367
  let mut insert_stmt = InsertStatement::new()
2345
2368
  .into_table(Alias::new(&table_name))
2346
2369
  .to_owned();
2347
-
2348
- // Columns come from the first row's order (skip a null auto-pk).
2349
- let mut column_names: Vec<String> = Vec::new();
2350
- for (key, input) in &record_inputs[0] {
2351
- let is_pk = pk_col.as_deref() == Some(key.as_str());
2352
- if is_pk && pk_is_auto && input.is_json_null() {
2353
- continue;
2354
- }
2355
- column_names.push(key.clone());
2356
- }
2357
2370
  insert_stmt.columns(column_names.iter().map(|c| Alias::new(c)));
2358
2371
 
2359
2372
  let null_input = BindInput::Json(serde_json::Value::Null);
2360
- for row in &record_inputs {
2373
+ for row in chunk {
2361
2374
  let lookup: std::collections::HashMap<&str, &BindInput> =
2362
2375
  row.iter().map(|(k, v)| (k.as_str(), v)).collect();
2363
2376
  let mut row_values = Vec::with_capacity(column_names.len());
@@ -2377,20 +2390,94 @@ pub fn save_bulk_records<'py>(
2377
2390
  }
2378
2391
 
2379
2392
  let (s, values) = sea_query_build_for_backend!(insert_stmt, backend);
2380
- (s, values)
2393
+ statements.push((s, values));
2394
+ }
2395
+
2396
+ // A bare multi-statement call opens its own transaction: the single
2397
+ // statement it replaces was atomic, and chunking must not regress
2398
+ // that. With an ambient transaction (or a single statement) the
2399
+ // existing execution path already gives the right boundary.
2400
+ let own_tx: Option<TransactionConnection> = if tx_conn.is_none() && statements.len() > 1 {
2401
+ let conn = engine.begin_transaction_connection().await.map_err(|e| {
2402
+ crate::errors::map_db_error(&format!("Bulk save failed for '{}'", name), e)
2403
+ })?;
2404
+ Some(Arc::new(tokio::sync::Mutex::new(conn)))
2405
+ } else {
2406
+ None
2381
2407
  };
2408
+ let exec_conn = tx_conn.as_ref().or(own_tx.as_ref());
2409
+
2410
+ let mut rows_affected: u64 = 0;
2411
+ for (sql, bind_values) in &statements {
2412
+ let executed =
2413
+ execute_statement_with_optional_tx(&engine, exec_conn, sql, &bind_values.0).await;
2414
+ match executed {
2415
+ Ok(n) => rows_affected += n,
2416
+ Err(e) => {
2417
+ if let Some(own_tx) = &own_tx {
2418
+ // Surface the insert failure even if ROLLBACK also
2419
+ // fails — a dead connection aborts the transaction
2420
+ // server-side, so the root cause is the honest error.
2421
+ let _ = execute_transaction_sql(own_tx, "ROLLBACK").await;
2422
+ }
2423
+ return Err(crate::errors::map_db_error(
2424
+ &format!("Bulk save failed for '{}'", name),
2425
+ e,
2426
+ ));
2427
+ }
2428
+ }
2429
+ }
2382
2430
 
2383
- let rows_affected =
2384
- execute_statement_with_optional_tx(&engine, tx_conn.as_ref(), &sql, &bind_values.0)
2431
+ if let Some(own_tx) = &own_tx {
2432
+ execute_transaction_sql(own_tx, "COMMIT")
2385
2433
  .await
2386
- .map_err(|e| {
2387
- crate::errors::map_db_error(&format!("Bulk save failed for '{}'", name), e)
2388
- })?;
2434
+ .map_err(|e| crate::errors::map_db_error("Failed to COMMIT", e))?;
2435
+ }
2389
2436
 
2390
2437
  Ok(rows_affected)
2391
2438
  })
2392
2439
  }
2393
2440
 
2441
+ /// Rows per INSERT statement so `rows × columns` never exceeds the backend's
2442
+ /// bind-parameter budget. Always at least one row: a single row wider than
2443
+ /// the budget cannot be split further, so it passes through to the backend
2444
+ /// (whose error is then the honest answer). A zero-column statement binds
2445
+ /// nothing, so the whole batch fits in one statement.
2446
+ fn bulk_insert_rows_per_statement(max_bind_params: usize, n_columns: usize) -> usize {
2447
+ if n_columns == 0 {
2448
+ return usize::MAX;
2449
+ }
2450
+ (max_bind_params / n_columns).max(1)
2451
+ }
2452
+
2453
+ #[cfg(test)]
2454
+ mod bulk_chunking_tests {
2455
+ use super::bulk_insert_rows_per_statement;
2456
+
2457
+ #[test]
2458
+ fn floors_to_the_bind_budget() {
2459
+ // sqlite budget, 5 columns: 6,553 rows bind 32,765 of 32,766.
2460
+ assert_eq!(bulk_insert_rows_per_statement(32_766, 5), 6_553);
2461
+ // Postgres budget, 12 columns: 5,461 rows bind 65,532 of 65,535.
2462
+ assert_eq!(bulk_insert_rows_per_statement(65_535, 12), 5_461);
2463
+ }
2464
+
2465
+ #[test]
2466
+ fn exact_division_uses_the_full_budget() {
2467
+ assert_eq!(bulk_insert_rows_per_statement(65_535, 5), 13_107);
2468
+ }
2469
+
2470
+ #[test]
2471
+ fn one_row_minimum_even_when_a_single_row_exceeds_the_budget() {
2472
+ assert_eq!(bulk_insert_rows_per_statement(10, 20), 1);
2473
+ }
2474
+
2475
+ #[test]
2476
+ fn zero_columns_never_split() {
2477
+ assert_eq!(bulk_insert_rows_per_statement(32_766, 0), usize::MAX);
2478
+ }
2479
+ }
2480
+
2394
2481
  /// Fetches records for a given model class based on a QueryIR-defined query.
2395
2482
  ///
2396
2483
  /// The query's materialization plan (ADR-0007) selects the result shape:
@@ -0,0 +1,167 @@
1
+ """bulk_create internal chunking under backend bind-parameter limits (#298).
2
+
3
+ All tests exercise the public seam — ``Model.bulk_create`` over the backend
4
+ matrix. Chunking is an implementation detail: the only observable contract is
5
+ that any batch size succeeds, the returned count is the total inserted, and
6
+ atomicity is preserved with and without an ambient transaction.
7
+
8
+ Bind budget arithmetic used throughout: ``ChunkedItem`` renders 5 columns per
9
+ row (the unset autoincrement pk is skipped), so 13,200 rows bind 66,000
10
+ parameters — over sqlite's 32,766 (SQLITE_MAX_VARIABLE_NUMBER) and Postgres's
11
+ 65,535 (int16 parameter count in the wire protocol's Bind message).
12
+ """
13
+
14
+ import pytest
15
+ from typing import Annotated
16
+
17
+ from ferro import FerroField, Model, connect, engines, transaction
18
+ from ferro.exceptions import IntegrityError
19
+
20
+ pytestmark = pytest.mark.backend_matrix
21
+
22
+ STRADDLE_ROWS = 13_200 # 13,200 rows x 5 columns = 66,000 binds > both limits
23
+
24
+
25
+ class ChunkedItem(Model):
26
+ id: Annotated[int | None, FerroField(primary_key=True)] = None
27
+ label: str
28
+ quantity: int
29
+ price: float
30
+ is_active: bool
31
+ note: str
32
+
33
+
34
+ def make_items(n: int) -> list[ChunkedItem]:
35
+ return [
36
+ ChunkedItem(
37
+ label=f"item-{i}",
38
+ quantity=i,
39
+ price=i * 0.5,
40
+ is_active=i % 2 == 0,
41
+ note=f"note-{i}",
42
+ )
43
+ for i in range(n)
44
+ ]
45
+
46
+
47
+ @pytest.mark.asyncio
48
+ async def test_bulk_create_succeeds_beyond_bind_limits(db_url):
49
+ """A batch binding more parameters than either backend allows inserts fully."""
50
+
51
+ await connect(db_url, auto_migrate=True)
52
+ async with engines.session():
53
+ inserted = await ChunkedItem.bulk_create(make_items(STRADDLE_ROWS))
54
+ assert inserted == STRADDLE_ROWS
55
+
56
+ assert (
57
+ await ChunkedItem.where(lambda item: item.quantity >= 0).count()
58
+ == STRADDLE_ROWS
59
+ )
60
+
61
+ # Spot-check a row from the final chunk survived with its values intact.
62
+ last = await ChunkedItem.where(
63
+ lambda item: item.label == f"item-{STRADDLE_ROWS - 1}"
64
+ ).first()
65
+ assert last is not None
66
+ assert last.quantity == STRADDLE_ROWS - 1
67
+ assert last.note == f"note-{STRADDLE_ROWS - 1}"
68
+
69
+
70
+ class UniqueChunkedItem(Model):
71
+ id: Annotated[int | None, FerroField(primary_key=True)] = None
72
+ label: Annotated[str, FerroField(unique=True)]
73
+ quantity: int
74
+ price: float
75
+ is_active: bool
76
+ note: str
77
+
78
+
79
+ def make_unique_items(n: int) -> list[UniqueChunkedItem]:
80
+ return [
81
+ UniqueChunkedItem(
82
+ label=f"item-{i}",
83
+ quantity=i,
84
+ price=i * 0.5,
85
+ is_active=i % 2 == 0,
86
+ note=f"note-{i}",
87
+ )
88
+ for i in range(n)
89
+ ]
90
+
91
+
92
+ @pytest.mark.asyncio
93
+ async def test_bare_bulk_create_is_all_or_nothing_across_chunks(db_url):
94
+ """A late-chunk failure in a bare call must leave no rows from the batch.
95
+
96
+ The collision sits at index 13,150 — past every chunk boundary either
97
+ backend produces for a 5-column row budget — so earlier chunks have
98
+ already executed when the violation fires.
99
+ """
100
+
101
+ await connect(db_url, auto_migrate=True)
102
+ async with engines.session():
103
+ await UniqueChunkedItem.create(
104
+ label="item-13150", quantity=0, price=0.0, is_active=True, note="seed"
105
+ )
106
+
107
+ with pytest.raises(IntegrityError):
108
+ await UniqueChunkedItem.bulk_create(make_unique_items(STRADDLE_ROWS))
109
+
110
+ survivors = await UniqueChunkedItem.all()
111
+ assert len(survivors) == 1
112
+ assert survivors[0].note == "seed"
113
+
114
+
115
+ @pytest.mark.asyncio
116
+ async def test_ambient_transaction_is_the_atomicity_boundary(db_url):
117
+ """An aborted ambient transaction() rolls back every internal chunk."""
118
+
119
+ await connect(db_url, auto_migrate=True)
120
+ async with engines.session():
121
+ try:
122
+ async with transaction():
123
+ inserted = await ChunkedItem.bulk_create(make_items(STRADDLE_ROWS))
124
+ assert inserted == STRADDLE_ROWS
125
+ raise RuntimeError("abort after chunked bulk_create")
126
+ except RuntimeError:
127
+ pass
128
+
129
+ assert await ChunkedItem.where(lambda item: item.quantity >= 0).count() == 0
130
+
131
+
132
+ @pytest.mark.asyncio
133
+ async def test_late_chunk_failure_inside_ambient_transaction_leaves_zero_rows(db_url):
134
+ """A mid-batch unique violation propagates and aborts the ambient transaction."""
135
+
136
+ await connect(db_url, auto_migrate=True)
137
+ async with engines.session():
138
+ with pytest.raises(IntegrityError):
139
+ async with transaction():
140
+ await UniqueChunkedItem.create(
141
+ label="item-13150",
142
+ quantity=0,
143
+ price=0.0,
144
+ is_active=True,
145
+ note="seed",
146
+ )
147
+ await UniqueChunkedItem.bulk_create(make_unique_items(STRADDLE_ROWS))
148
+
149
+ assert await UniqueChunkedItem.all() == []
150
+
151
+
152
+ @pytest.mark.asyncio
153
+ @pytest.mark.parametrize(
154
+ "rows",
155
+ [
156
+ 6_553, # 32,765 binds — one under sqlite's 32,766 ceiling
157
+ 6_554, # 32,770 binds — the 0.16.0 sqlite failure threshold (#298)
158
+ 13_107, # 65,535 binds — exactly Postgres's ceiling
159
+ ],
160
+ )
161
+ async def test_bulk_create_at_exact_bind_limit_boundaries(db_url, rows):
162
+ """Row counts straddling each backend's exact bind ceiling insert fully."""
163
+
164
+ await connect(db_url, auto_migrate=True)
165
+ async with engines.session():
166
+ assert await ChunkedItem.bulk_create(make_items(rows)) == rows
167
+ assert await ChunkedItem.where(lambda item: item.quantity >= 0).count() == rows
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes