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.
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/CHANGELOG.md +8 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/Cargo.lock +1 -1
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/Cargo.toml +1 -1
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/PKG-INFO +1 -1
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/performance.md +2 -2
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/mutations.md +3 -1
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/pyproject.toml +1 -1
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/backend.rs +13 -1
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/models.py +6 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/operations.rs +106 -19
- ferro_orm-0.16.1/tests/test_bulk_create_chunking.py +167 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/PERMISSIONS.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.github/workflows/release.yml +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.gitignore +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/.python-version +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/AGENTS.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/CONTEXT.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/LICENSE +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/README.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/README.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/__init__.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/__main__.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/backends.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/baselines/postgres.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/baselines/sqlite.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/compare.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/harness.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/model.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/benchmarks/run.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-ddl-lowering/src/lib.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-migrate/Cargo.toml +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-migrate/src/emit.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-migrate/src/lib.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-migrate/src/tests.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-schema-ir/Cargo.toml +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/crates/ferro-schema-ir/src/lib.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0001-resolved-epoch-registration.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0008-populated-relations-include.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/agents/domain.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/agents/issue-tracker.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/agents/triage-labels.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/architecture-review-2026-07-08.html +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/aggregations.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/aggregations_annotated.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/multiple_databases.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/mutations.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/pagination.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/partial_selects.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/partial_selects_annotated.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/populated_relations.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/populated_relations_annotated.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/predicates.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/predicates_annotated.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/quickstart.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/quickstart_annotated.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/raw_sql.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/relationships.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/relationships_annotated.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/soft_deletes.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/soft_deletes_annotated.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/testing_conftest.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/timestamps.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/timestamps_annotated.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/transactions.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/traversal.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/examples/traversal_annotated.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/connection.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/exceptions.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/fields.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/migrations.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/model.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/queries.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/raw-sql.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/relationships.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/api/transactions.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/changelog.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/architecture.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/backends.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/identity-map.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/query-typing.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/concepts/type-safety.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/contributing.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/faq.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/getting-started/installation.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/getting-started/next-steps.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/getting-started/quickstart.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/aggregations.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/connections.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/migrations.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/models-and-fields.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/queries.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/raw-sql.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/relationships.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/guide/transactions.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/multiple-databases.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/pagination.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/soft-deletes.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/testing.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/timestamps.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/howto/upgrade-guide.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/index.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/roadmap.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/stylesheets/extra.css +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/pages/why-ferro.md +0 -0
- {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
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
- {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
- {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
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
- {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
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
- {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
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
- {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
- {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
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/ir-first-migration-guide.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/plans/ir-first-release-checklist.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/rfc/ir-contracts-v1.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/README.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/foreign-key-index.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/ir-invariants.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/persistence-state.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/solutions/patterns/typed-null-binds.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/justfile +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/scripts/demo_queries_pre_v012.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/codec.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/codec_plan.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/connection.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/errors.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/__init__.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/_annotation_utils.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/_bind_payload.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/_core.pyi +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/_deprecations.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/_shadow_fk_types.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/base.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/columns.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/composite_indexes.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/composite_uniques.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/exceptions.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/fields.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/ir/__init__.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/ir/compiler.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/metaclass.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/migrations/alembic.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/py.typed +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/query/builder.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/query/nodes.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/query/rows.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/raw.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/relations/__init__.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/relations/descriptors.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/session.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/ferro/state.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/hydration.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/introspect.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/lib.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/migrate.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/naming_ffi.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/query.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/schema.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/schema_bind.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/src/state.rs +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/__init__.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/conftest.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/db_backends.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/README.md +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_aggregate_v5.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v5.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_include_v5.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_left_join_v5.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_record_v5.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_traversal_v5.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_transaction_traversed_record_v5.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/query_user_compound_v5.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/bad_includes.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/bad_predicates.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/bad_projections.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/good_includes.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/good_predicates.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/static_fixtures/good_projections.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_alembic_autogenerate.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_alembic_bridge.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_alembic_db_type.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_alembic_nullability.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_alembic_type_mapping.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_annotation_utils.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_auto_migrate.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_bulk_install.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_catalog_cache.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_codec_plan.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_column_specs.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_composite_index.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_composite_unique.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_connection.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_connection_redaction.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_constraints.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_cross_emitter_parity.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_crud.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_db_backends.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_db_type_cross_emitter_parity.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_db_type_integration.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_db_type_typing.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_db_type_validation.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_deletion.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_deprecations.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_documentation_features.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_enum_cold_hydration.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_exception_mapping.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_exceptions.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_field_wrapper.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_fk_target_pk.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_generation_counter_dirty_tracking.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_global_aggregates.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_grouped_aggregates.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_helpers.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_hydration.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_hydration_equivalence.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_identity_memory.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_identity_refresh.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_identity_scoped_invalidation.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_identity_weakref.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_import_budget.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_include.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_ir_vectors_contract.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_metaclass_internals.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_metadata.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_migrate_plan.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_model_identity.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_models.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_mutation_pagination_guard.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_named_connections_integration.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_naming_single_source.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_one_to_one.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_operation_seam_sync.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_partial_selects.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_provisional_import.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_query_builder.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_query_column_validation.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_query_immutability.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_query_joins.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_query_typing.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_raw_sql.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_refresh.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_registry_entrypoints.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_relation_specs.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_relationship_engine.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_relationship_resolution_errors.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_route_single_site.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_routing_errors.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_save_pk_edges.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_save_semantics.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_schema.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_schema_db_type_metadata.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_schema_enum_annotations.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_session.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_shadow_fk_types.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_static_contracts.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_string_search.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_structural_types.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_transactions.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_traversed_projection.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_typed_null_binds.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/tests/test_typed_save_bind.py +0 -0
- {ferro_orm-0.16.0 → ferro_orm-0.16.1}/uv.lock +0 -0
- {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
|
|
@@ -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
|
|
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
|
-
|
|
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)`
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
2384
|
-
|
|
2431
|
+
if let Some(own_tx) = &own_tx {
|
|
2432
|
+
execute_transaction_sql(own_tx, "COMMIT")
|
|
2385
2433
|
.await
|
|
2386
|
-
.map_err(|e|
|
|
2387
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0007-materialization-plan-complete-instances.md
RENAMED
|
File without changes
|
|
File without changes
|
{ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/adr/0009-aggregate-projections-derived-grouping.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ferro_orm-0.16.0 → ferro_orm-0.16.1}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md
RENAMED
|
File without changes
|
|
File without changes
|