ferro-orm 0.19.0__tar.gz → 0.20.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/CHANGELOG.md +14 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/Cargo.lock +35 -35
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/Cargo.toml +1 -1
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/PKG-INFO +1 -1
- ferro_orm-0.20.0/context7.json +4 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-schema-ir/src/lib.rs +63 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/predicates.py +38 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/predicates_annotated.py +27 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/queries.md +1 -1
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/queries.md +27 -3
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/pyproject.toml +1 -1
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/query/builder.py +52 -11
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/query/wire.py +6 -2
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/operations.rs +155 -7
- ferro_orm-0.20.0/tests/fixtures/ir_vectors/query_card_nulls_v7.json +44 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_ir_vectors_contract.py +5 -0
- ferro_orm-0.20.0/tests/test_order_by_nulls.py +209 -0
- ferro_orm-0.20.0/tests/test_order_by_nulls_wire.py +102 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_column_validation.py +118 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_wire_vectors.py +20 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/PERMISSIONS.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.github/workflows/release.yml +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.gitignore +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/.python-version +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/AGENTS.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/CONTEXT.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/LICENSE +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/README.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/README.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/__init__.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/__main__.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/backends.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/baselines/postgres.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/baselines/sqlite.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/compare.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/harness.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/model.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/benchmarks/run.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-ddl-lowering/src/lib.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-migrate/Cargo.toml +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-migrate/src/emit.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-migrate/src/lib.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-migrate/src/order.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-migrate/src/tests.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/crates/ferro-schema-ir/Cargo.toml +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0001-resolved-epoch-registration.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0007-reverse-relations-tested-not-traversed.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0008-populated-relations-include.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0008-predicate-negation-uniform-not.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0010-create-pass-owns-only-missing-tables.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0011-label-addition-update-gated-append-only.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0012-table-checks-are-lambdas.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0013-table-check-reconciliation.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0014-table-checks-sqlite-create-only.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0015-table-check-body-drift.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/adr/0016-check-predicates-literals-only.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/agents/domain.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/agents/issue-tracker.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/agents/triage-labels.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/architecture-review-2026-07-08.html +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-26-ir-p8.5-143-db-type-drop-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-28-ir-p8.5-141-single-schema-ir-producer-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-29-ir-p8.6-146-dialect-enum-unification-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-29-ir-p8.6-153-create-path-ir-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-07-01-ir-p8.6-154-datetime-tz-coarseness-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-07-01-ir-p8.6-162-typed-save-bind-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-07-01-ir-p8.6-165-blob-introspection-false-positive-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/aggregations.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/aggregations_annotated.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/existence_tests.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/existence_tests_annotated.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/multiple_databases.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/mutations.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/pagination.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/partial_selects.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/partial_selects_annotated.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/populated_relations.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/populated_relations_annotated.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/quickstart.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/quickstart_annotated.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/raw_sql.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/relationships.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/relationships_annotated.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/soft_deletes.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/soft_deletes_annotated.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/table_checks.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/table_checks_annotated.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/testing_conftest.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/timestamps.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/timestamps_annotated.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/transactions.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/traversal.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/examples/traversal_annotated.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/connection.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/exceptions.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/fields.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/migrations.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/model.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/raw-sql.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/relationships.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/api/transactions.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/changelog.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/architecture.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/backends.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/identity-map.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/performance.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/query-typing.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/concepts/type-safety.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/contributing.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/faq.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/getting-started/installation.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/getting-started/next-steps.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/getting-started/quickstart.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/aggregations.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/connections.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/migrations.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/models-and-fields.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/mutations.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/raw-sql.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/relationships.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/guide/transactions.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/multiple-databases.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/pagination.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/soft-deletes.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/testing.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/timestamps.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/howto/upgrade-guide.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/index.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/roadmap.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/stylesheets/extra.css +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/pages/why-ferro.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-24-001-feat-ir-p8-119-wire-automigrate-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-25-001-feat-ir-p8-120-parity-gate-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-26-001-feat-ir-p8.5-140-shared-lowering-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-26-002-feat-ir-p8.5-143-db-type-drop-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-26-003-feat-ir-p8.5-144-reconcile-indexes-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-28-001-feat-ir-p8.5-141-single-schema-ir-producer-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-29-001-refactor-ir-p8.6-153-create-path-unification-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-29-002-refactor-ir-p8.6-146-dialect-enum-unification-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-30-001-refactor-ir-p8.6-158-check-renderer-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-06-30-002-fix-ir-p8.6-155-deferred-annotations-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-01-001-feat-ir-p8.6-162-typed-save-bind-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-01-002-fix-ir-p8.6-154-datetime-tz-coarseness-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-01-003-fix-ir-p8.6-165-blob-introspection-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-002-feat-ff-a-172-typed-exceptions-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-009-ff-f-query-builder-1.0-design.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-010-ff-f-query-builder-1.0-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/2026-07-18-016-existence-tests-design.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/ir-first-migration-guide.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/plans/ir-first-release-checklist.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/rfc/ir-contracts-v1.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/README.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/pg18-restrict-sqlstate-23001.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/self-fk-evicts-component-from-create-order.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/foreign-key-index.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/ir-invariants.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/persistence-state.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/docs/solutions/patterns/typed-null-binds.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/justfile +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/scripts/demo_queries_pre_v012.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/backend.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/codec.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/codec_plan.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/connection.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/errors.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/__init__.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/_annotation_utils.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/_bind_payload.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/_core.pyi +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/_deprecations.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/_shadow_fk_types.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/base.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/checks.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/columns.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/composite_indexes.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/composite_uniques.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/exceptions.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/fields.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/ir/__init__.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/ir/compiler.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/metaclass.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/migrations/alembic.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/models.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/py.typed +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/query/nodes.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/query/rows.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/raw.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/registry.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/relations/__init__.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/relations/descriptors.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/session.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/ferro/state.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/hydration.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/introspect.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/lib.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/migrate.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/naming_ffi.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/query.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/schema.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/schema_bind.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/src/state.rs +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/__init__.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/conftest.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/db_backends.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/README.md +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_account_exists_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_account_scoped_exists_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_owner_nested_exists_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_owner_not_exists_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_aggregate_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_include_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_left_join_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_record_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_traversal_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_transaction_traversed_record_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_user_compound_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_user_m2m_exists_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_user_not_compound_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/query_user_not_leaf_v7.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/bad_includes.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/bad_predicates.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/bad_projections.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/good_includes.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/good_predicates.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/static_fixtures/good_projections.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_alembic_autogenerate.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_alembic_bridge.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_alembic_db_type.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_alembic_nullability.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_alembic_type_mapping.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_annotation_utils.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_auto_migrate.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_bulk_create_chunking.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_bulk_install.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_catalog_cache.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_check_predicate_dialect.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_codec_plan.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_column_specs.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_compiled_query.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_composite_index.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_composite_unique.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_connection.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_connection_redaction.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_constraints.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_cross_emitter_parity.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_crud.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_db_backends.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_db_type_cross_emitter_parity.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_db_type_integration.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_db_type_typing.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_db_type_validation.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_deletion.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_deprecations.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_documentation_features.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_enum_cold_hydration.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_exception_mapping.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_exceptions.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_field_wrapper.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_fk_target_pk.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_generation_counter_dirty_tracking.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_global_aggregates.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_grouped_aggregates.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_helpers.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_hydration.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_hydration_equivalence.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_identity_memory.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_identity_refresh.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_identity_scoped_invalidation.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_identity_weakref.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_import_budget.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_include.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_label_addition.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_live_table_checks.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_metaclass_internals.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_metadata.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_migrate_plan.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_model_identity.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_models.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_mutation_pagination_guard.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_named_connections_integration.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_naming_single_source.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_negation.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_one_to_one.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_operation_seam_sync.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_partial_selects.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_primary_key_facts.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_provisional_import.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_builder.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_exists.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_immutability.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_joins.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_query_typing.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_raw_sql.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_refresh.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_registry_entrypoints.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_registry_owner.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_relation_specs.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_relationship_engine.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_relationship_resolution_errors.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_route_single_site.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_routing_errors.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_save_pk_edges.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_save_semantics.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_schema.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_schema_db_type_metadata.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_schema_enum_annotations.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_session.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_shadow_fk_types.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_static_contracts.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_string_search.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_structural_types.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_table_check_orphans.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_table_check_rebuild.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_table_check_reconcile.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_table_checks.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_transactions.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_traversed_projection.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_typed_null_binds.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/tests/test_typed_save_bind.py +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/uv.lock +0 -0
- {ferro_orm-0.19.0 → ferro_orm-0.20.0}/zensical.toml +0 -0
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.20.0 (2026-08-29)
|
|
5
|
+
|
|
6
|
+
### Chores
|
|
7
|
+
|
|
8
|
+
- Add Context7 library config ([#357](https://github.com/syn54x/ferro-orm/pull/357),
|
|
9
|
+
[`ab8e79b`](https://github.com/syn54x/ferro-orm/commit/ab8e79b799e07fd64f7da8db09daa278af488877))
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- **query**: Order_by nulls= first/last placement
|
|
14
|
+
([#369](https://github.com/syn54x/ferro-orm/pull/369),
|
|
15
|
+
[`ef6c8b4`](https://github.com/syn54x/ferro-orm/commit/ef6c8b4c458d47a88ace4407a5fb368a8d91200a))
|
|
16
|
+
|
|
17
|
+
|
|
4
18
|
## v0.19.0 (2026-08-18)
|
|
5
19
|
|
|
6
20
|
### Bug Fixes
|
|
@@ -95,9 +95,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|
|
95
95
|
|
|
96
96
|
[[package]]
|
|
97
97
|
name = "cc"
|
|
98
|
-
version = "1.4.
|
|
98
|
+
version = "1.4.4"
|
|
99
99
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
100
|
-
checksum = "
|
|
100
|
+
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
|
|
101
101
|
dependencies = [
|
|
102
102
|
"find-msvc-tools",
|
|
103
103
|
"shlex",
|
|
@@ -260,7 +260,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
|
|
260
260
|
dependencies = [
|
|
261
261
|
"proc-macro2",
|
|
262
262
|
"quote",
|
|
263
|
-
"syn 3.0.
|
|
263
|
+
"syn 3.0.4",
|
|
264
264
|
]
|
|
265
265
|
|
|
266
266
|
[[package]]
|
|
@@ -271,9 +271,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
|
|
271
271
|
|
|
272
272
|
[[package]]
|
|
273
273
|
name = "either"
|
|
274
|
-
version = "1.
|
|
274
|
+
version = "1.18.0"
|
|
275
275
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
276
|
-
checksum = "
|
|
276
|
+
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
|
277
277
|
dependencies = [
|
|
278
278
|
"serde",
|
|
279
279
|
]
|
|
@@ -317,7 +317,7 @@ dependencies = [
|
|
|
317
317
|
|
|
318
318
|
[[package]]
|
|
319
319
|
name = "ferro"
|
|
320
|
-
version = "0.
|
|
320
|
+
version = "0.20.0"
|
|
321
321
|
dependencies = [
|
|
322
322
|
"chrono",
|
|
323
323
|
"dashmap",
|
|
@@ -467,7 +467,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
|
|
467
467
|
dependencies = [
|
|
468
468
|
"proc-macro2",
|
|
469
469
|
"quote",
|
|
470
|
-
"syn 3.0.
|
|
470
|
+
"syn 3.0.4",
|
|
471
471
|
]
|
|
472
472
|
|
|
473
473
|
[[package]]
|
|
@@ -702,9 +702,9 @@ checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
|
|
|
702
702
|
|
|
703
703
|
[[package]]
|
|
704
704
|
name = "icu_provider"
|
|
705
|
-
version = "2.3.
|
|
705
|
+
version = "2.3.1"
|
|
706
706
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
707
|
-
checksum = "
|
|
707
|
+
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
|
|
708
708
|
dependencies = [
|
|
709
709
|
"displaydoc",
|
|
710
710
|
"icu_locale_core",
|
|
@@ -744,9 +744,9 @@ dependencies = [
|
|
|
744
744
|
|
|
745
745
|
[[package]]
|
|
746
746
|
name = "indexmap"
|
|
747
|
-
version = "2.14.
|
|
747
|
+
version = "2.14.1"
|
|
748
748
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
749
|
-
checksum = "
|
|
749
|
+
checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
|
|
750
750
|
dependencies = [
|
|
751
751
|
"equivalent",
|
|
752
752
|
"hashbrown 0.17.1",
|
|
@@ -769,7 +769,7 @@ checksum = "bee2c455ca60511a054699102d40ce7153621cb2429558f9eaa600e4499b5984"
|
|
|
769
769
|
dependencies = [
|
|
770
770
|
"proc-macro2",
|
|
771
771
|
"quote",
|
|
772
|
-
"syn 3.0.
|
|
772
|
+
"syn 3.0.4",
|
|
773
773
|
]
|
|
774
774
|
|
|
775
775
|
[[package]]
|
|
@@ -812,14 +812,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
|
|
812
812
|
|
|
813
813
|
[[package]]
|
|
814
814
|
name = "libredox"
|
|
815
|
-
version = "0.1.
|
|
815
|
+
version = "0.1.21"
|
|
816
816
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
817
|
-
checksum = "
|
|
817
|
+
checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96"
|
|
818
818
|
dependencies = [
|
|
819
819
|
"bitflags",
|
|
820
820
|
"libc",
|
|
821
821
|
"plain",
|
|
822
|
-
"redox_syscall 0.9.
|
|
822
|
+
"redox_syscall 0.9.3",
|
|
823
823
|
]
|
|
824
824
|
|
|
825
825
|
[[package]]
|
|
@@ -850,9 +850,9 @@ dependencies = [
|
|
|
850
850
|
|
|
851
851
|
[[package]]
|
|
852
852
|
name = "log"
|
|
853
|
-
version = "0.4.
|
|
853
|
+
version = "0.4.34"
|
|
854
854
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
855
|
-
checksum = "
|
|
855
|
+
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
|
856
856
|
|
|
857
857
|
[[package]]
|
|
858
858
|
name = "md-5"
|
|
@@ -1158,9 +1158,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
|
|
1158
1158
|
|
|
1159
1159
|
[[package]]
|
|
1160
1160
|
name = "rand"
|
|
1161
|
-
version = "0.8.
|
|
1161
|
+
version = "0.8.8"
|
|
1162
1162
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1163
|
-
checksum = "
|
|
1163
|
+
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
|
|
1164
1164
|
dependencies = [
|
|
1165
1165
|
"libc",
|
|
1166
1166
|
"rand_chacha",
|
|
@@ -1197,9 +1197,9 @@ dependencies = [
|
|
|
1197
1197
|
|
|
1198
1198
|
[[package]]
|
|
1199
1199
|
name = "redox_syscall"
|
|
1200
|
-
version = "0.9.
|
|
1200
|
+
version = "0.9.3"
|
|
1201
1201
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1202
|
-
checksum = "
|
|
1202
|
+
checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5"
|
|
1203
1203
|
dependencies = [
|
|
1204
1204
|
"bitflags",
|
|
1205
1205
|
]
|
|
@@ -1263,9 +1263,9 @@ dependencies = [
|
|
|
1263
1263
|
|
|
1264
1264
|
[[package]]
|
|
1265
1265
|
name = "rustls-webpki"
|
|
1266
|
-
version = "0.103.
|
|
1266
|
+
version = "0.103.15"
|
|
1267
1267
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1268
|
-
checksum = "
|
|
1268
|
+
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
|
1269
1269
|
dependencies = [
|
|
1270
1270
|
"ring",
|
|
1271
1271
|
"rustls-pki-types",
|
|
@@ -1342,7 +1342,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|
|
1342
1342
|
dependencies = [
|
|
1343
1343
|
"proc-macro2",
|
|
1344
1344
|
"quote",
|
|
1345
|
-
"syn 3.0.
|
|
1345
|
+
"syn 3.0.4",
|
|
1346
1346
|
]
|
|
1347
1347
|
|
|
1348
1348
|
[[package]]
|
|
@@ -1700,9 +1700,9 @@ dependencies = [
|
|
|
1700
1700
|
|
|
1701
1701
|
[[package]]
|
|
1702
1702
|
name = "syn"
|
|
1703
|
-
version = "3.0.
|
|
1703
|
+
version = "3.0.4"
|
|
1704
1704
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1705
|
-
checksum = "
|
|
1705
|
+
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
|
|
1706
1706
|
dependencies = [
|
|
1707
1707
|
"proc-macro2",
|
|
1708
1708
|
"quote",
|
|
@@ -1743,7 +1743,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
|
|
1743
1743
|
dependencies = [
|
|
1744
1744
|
"proc-macro2",
|
|
1745
1745
|
"quote",
|
|
1746
|
-
"syn 3.0.
|
|
1746
|
+
"syn 3.0.4",
|
|
1747
1747
|
]
|
|
1748
1748
|
|
|
1749
1749
|
[[package]]
|
|
@@ -1796,7 +1796,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
|
|
1796
1796
|
dependencies = [
|
|
1797
1797
|
"proc-macro2",
|
|
1798
1798
|
"quote",
|
|
1799
|
-
"syn 3.0.
|
|
1799
|
+
"syn 3.0.4",
|
|
1800
1800
|
]
|
|
1801
1801
|
|
|
1802
1802
|
[[package]]
|
|
@@ -1907,9 +1907,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|
|
1907
1907
|
|
|
1908
1908
|
[[package]]
|
|
1909
1909
|
name = "uuid"
|
|
1910
|
-
version = "1.
|
|
1910
|
+
version = "1.26.0"
|
|
1911
1911
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1912
|
-
checksum = "
|
|
1912
|
+
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
|
|
1913
1913
|
dependencies = [
|
|
1914
1914
|
"getrandom 0.4.3",
|
|
1915
1915
|
"js-sys",
|
|
@@ -2309,9 +2309,9 @@ dependencies = [
|
|
|
2309
2309
|
|
|
2310
2310
|
[[package]]
|
|
2311
2311
|
name = "zerovec"
|
|
2312
|
-
version = "0.11.
|
|
2312
|
+
version = "0.11.8"
|
|
2313
2313
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2314
|
-
checksum = "
|
|
2314
|
+
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
|
|
2315
2315
|
dependencies = [
|
|
2316
2316
|
"yoke",
|
|
2317
2317
|
"zerofrom",
|
|
@@ -2320,13 +2320,13 @@ dependencies = [
|
|
|
2320
2320
|
|
|
2321
2321
|
[[package]]
|
|
2322
2322
|
name = "zerovec-derive"
|
|
2323
|
-
version = "0.11.
|
|
2323
|
+
version = "0.11.6"
|
|
2324
2324
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2325
|
-
checksum = "
|
|
2325
|
+
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
|
|
2326
2326
|
dependencies = [
|
|
2327
2327
|
"proc-macro2",
|
|
2328
2328
|
"quote",
|
|
2329
|
-
"syn 3.0.
|
|
2329
|
+
"syn 3.0.4",
|
|
2330
2330
|
]
|
|
2331
2331
|
|
|
2332
2332
|
[[package]]
|
|
@@ -412,6 +412,11 @@ pub struct QueryOrderBy {
|
|
|
412
412
|
/// Relation field names from the root model to the ordered column's table;
|
|
413
413
|
/// `[]` = root model (#269 requires this empty until #270 renders joins).
|
|
414
414
|
pub path: Vec<String>,
|
|
415
|
+
/// Optional NULLS placement (`"first"` / `"last"`, case-insensitive in the
|
|
416
|
+
/// planner). Omitted on the wire when unset so existing fixtures keep
|
|
417
|
+
/// byte-identical round-trips (#361).
|
|
418
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
419
|
+
pub nulls: Option<String>,
|
|
415
420
|
}
|
|
416
421
|
|
|
417
422
|
/// Predicate tree node in query IR: leaf comparison, compound AND/OR, a
|
|
@@ -1313,6 +1318,64 @@ mod tests {
|
|
|
1313
1318
|
);
|
|
1314
1319
|
}
|
|
1315
1320
|
|
|
1321
|
+
#[test]
|
|
1322
|
+
fn query_order_by_nulls_optional_wire() {
|
|
1323
|
+
// #361: optional `nulls` on an order_by term — present key round-trips;
|
|
1324
|
+
// absent key deserializes as None and is omitted on serialize.
|
|
1325
|
+
let with: QueryOrderBy = serde_json::from_value(serde_json::json!({
|
|
1326
|
+
"column": "pinned_at",
|
|
1327
|
+
"direction": "desc",
|
|
1328
|
+
"path": [],
|
|
1329
|
+
"nulls": "last"
|
|
1330
|
+
}))
|
|
1331
|
+
.expect("order_by with nulls must deserialize");
|
|
1332
|
+
assert_eq!(with.nulls.as_deref(), Some("last"));
|
|
1333
|
+
let encoded = serde_json::to_value(&with).expect("serialize with nulls");
|
|
1334
|
+
assert_eq!(encoded["nulls"], "last");
|
|
1335
|
+
|
|
1336
|
+
let without: QueryOrderBy = serde_json::from_value(serde_json::json!({
|
|
1337
|
+
"column": "pinned_at",
|
|
1338
|
+
"direction": "desc",
|
|
1339
|
+
"path": []
|
|
1340
|
+
}))
|
|
1341
|
+
.expect("order_by without nulls must deserialize");
|
|
1342
|
+
assert_eq!(without.nulls, None);
|
|
1343
|
+
let encoded = serde_json::to_value(&without).expect("serialize without nulls");
|
|
1344
|
+
assert!(
|
|
1345
|
+
encoded.get("nulls").is_none(),
|
|
1346
|
+
"unset nulls must be omitted from the wire: {encoded}"
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
#[test]
|
|
1351
|
+
fn query_card_nulls_fixture_roundtrip() {
|
|
1352
|
+
// #363: golden vector with nulls on the first order_by term and the
|
|
1353
|
+
// key absent on the second — deserialize, pin shape, round-trip.
|
|
1354
|
+
let fixture =
|
|
1355
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_card_nulls_v7.json");
|
|
1356
|
+
let parsed: serde_json::Value =
|
|
1357
|
+
serde_json::from_str(fixture).expect("query card-nulls fixture must parse");
|
|
1358
|
+
let ir = parsed
|
|
1359
|
+
.get("ir")
|
|
1360
|
+
.cloned()
|
|
1361
|
+
.expect("fixture must contain ir envelope");
|
|
1362
|
+
let envelope: IrEnvelope<QueryIrPayload> =
|
|
1363
|
+
serde_json::from_value(ir.clone()).expect("query card-nulls IR must deserialize");
|
|
1364
|
+
assert_eq!(envelope.ir_version, 7);
|
|
1365
|
+
assert_eq!(envelope.payload.order_by.len(), 2);
|
|
1366
|
+
assert_eq!(
|
|
1367
|
+
envelope.payload.order_by[0].nulls.as_deref(),
|
|
1368
|
+
Some("last")
|
|
1369
|
+
);
|
|
1370
|
+
assert!(
|
|
1371
|
+
envelope.payload.order_by[1].nulls.is_none(),
|
|
1372
|
+
"second term must omit nulls"
|
|
1373
|
+
);
|
|
1374
|
+
let encoded =
|
|
1375
|
+
serde_json::to_value(&envelope).expect("query card-nulls IR must serialize");
|
|
1376
|
+
assert_eq!(encoded, ir, "query card-nulls round-trip must not drift");
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1316
1379
|
#[test]
|
|
1317
1380
|
fn codec_fixture_roundtrip() {
|
|
1318
1381
|
let fixture =
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Runnable companion to the Queries guide (docs/pages/guide/queries.md)."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
from datetime import UTC, datetime
|
|
4
5
|
|
|
5
6
|
# --8<-- [start:setup]
|
|
6
7
|
from ferro import Field, Model, connect, engines
|
|
@@ -23,6 +24,15 @@ class Invoice(Model):
|
|
|
23
24
|
# --8<-- [end:nullable-model]
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
# --8<-- [start:card-model]
|
|
28
|
+
class Card(Model):
|
|
29
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
30
|
+
title: str
|
|
31
|
+
pinned_at: datetime | None = None
|
|
32
|
+
updated_at: datetime
|
|
33
|
+
# --8<-- [end:card-model]
|
|
34
|
+
|
|
35
|
+
|
|
26
36
|
async def main() -> None:
|
|
27
37
|
await connect("sqlite::memory:", auto_migrate=True)
|
|
28
38
|
|
|
@@ -108,6 +118,34 @@ async def main() -> None:
|
|
|
108
118
|
assert oldest_first[0].name == "carol"
|
|
109
119
|
assert len(second_page) == 2
|
|
110
120
|
|
|
121
|
+
t0 = datetime(2026, 1, 1, tzinfo=UTC)
|
|
122
|
+
t1 = datetime(2026, 2, 1, tzinfo=UTC)
|
|
123
|
+
t2 = datetime(2026, 3, 1, tzinfo=UTC)
|
|
124
|
+
await Card.bulk_create(
|
|
125
|
+
[
|
|
126
|
+
Card(title="unpinned-old", pinned_at=None, updated_at=t0),
|
|
127
|
+
Card(title="pinned-early", pinned_at=t0, updated_at=t0),
|
|
128
|
+
Card(title="unpinned-new", pinned_at=None, updated_at=t2),
|
|
129
|
+
Card(title="pinned-late", pinned_at=t1, updated_at=t1),
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# --8<-- [start:nulls-ordering]
|
|
134
|
+
cards = (
|
|
135
|
+
await Card.select()
|
|
136
|
+
.order_by(lambda card: card.pinned_at, "desc", nulls="last")
|
|
137
|
+
.order_by(lambda card: card.updated_at, "desc")
|
|
138
|
+
.order_by(lambda card: card.id, "desc")
|
|
139
|
+
.all()
|
|
140
|
+
)
|
|
141
|
+
# --8<-- [end:nulls-ordering]
|
|
142
|
+
assert [card.title for card in cards] == [
|
|
143
|
+
"pinned-late",
|
|
144
|
+
"pinned-early",
|
|
145
|
+
"unpinned-new",
|
|
146
|
+
"unpinned-old",
|
|
147
|
+
]
|
|
148
|
+
|
|
111
149
|
# --8<-- [start:terminals]
|
|
112
150
|
everyone = await User.all()
|
|
113
151
|
first_admin = await User.where(lambda user: user.role == "admin").first()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Annotated-style companion to predicates.py (AGENTS.md I-7)."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
from datetime import UTC, datetime
|
|
4
5
|
|
|
5
6
|
# --8<-- [start:setup]
|
|
6
7
|
from typing import Annotated
|
|
@@ -25,6 +26,15 @@ class Invoice(Model):
|
|
|
25
26
|
# --8<-- [end:nullable-model]
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
# --8<-- [start:card-model]
|
|
30
|
+
class Card(Model):
|
|
31
|
+
id: Annotated[int | None, Field(default=None, primary_key=True)]
|
|
32
|
+
title: str
|
|
33
|
+
pinned_at: datetime | None = None
|
|
34
|
+
updated_at: datetime
|
|
35
|
+
# --8<-- [end:card-model]
|
|
36
|
+
|
|
37
|
+
|
|
28
38
|
async def main() -> None:
|
|
29
39
|
await connect("sqlite::memory:", auto_migrate=True)
|
|
30
40
|
|
|
@@ -53,6 +63,23 @@ async def main() -> None:
|
|
|
53
63
|
assert len(await Invoice.where(lambda invoice: invoice.amount > 100).all()) == 1
|
|
54
64
|
assert len(await Invoice.where(lambda invoice: ~(invoice.amount > 100)).all()) == 0
|
|
55
65
|
|
|
66
|
+
t0 = datetime(2026, 1, 1, tzinfo=UTC)
|
|
67
|
+
t1 = datetime(2026, 2, 1, tzinfo=UTC)
|
|
68
|
+
await Card.bulk_create(
|
|
69
|
+
[
|
|
70
|
+
Card(title="unpinned", pinned_at=None, updated_at=t1),
|
|
71
|
+
Card(title="pinned", pinned_at=t0, updated_at=t0),
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
cards = (
|
|
75
|
+
await Card.select()
|
|
76
|
+
.order_by(lambda card: card.pinned_at, "desc", nulls="last")
|
|
77
|
+
.order_by(lambda card: card.updated_at, "desc")
|
|
78
|
+
.order_by(lambda card: card.id, "desc")
|
|
79
|
+
.all()
|
|
80
|
+
)
|
|
81
|
+
assert [card.title for card in cards] == ["pinned", "unpinned"]
|
|
82
|
+
|
|
56
83
|
print("predicates_annotated example ran successfully")
|
|
57
84
|
|
|
58
85
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Prefix `~` negates **any** predicate — leaf comparison or `&`/`|` compound — rendering as SQL `NOT (...)` over the condition it wraps (ADR-0008). It is the universal negation rule: there are no per-operator negative forms (`~t.role.in_([...])` is NOT IN, `~t.email.like(p)` is NOT LIKE), and double negation nests. Like SQL `NOT` and the `!=` operator, a negated comparison excludes rows where the compared column is `NULL` — see [Negation and NULL values](../guide/queries.md#negation-and-null-values).
|
|
6
6
|
|
|
7
|
-
`where()` and `order_by()` lambdas may **traverse** a forward-FK relation (`lambda t: t.account.ledger_id == 1`): each hop renders one INNER join, deduplicated by relation path (ADR-0006). `join()` forces a join on a relation path (a bare `join()` is an existence filter on a nullable relation), and `left_join()` marks the whole path LEFT to keep relation-less rows. See the [Querying Across Relationships](../guide/queries.md#querying-across-relationships) guide for worked examples.
|
|
7
|
+
`where()` and `order_by()` lambdas may **traverse** a forward-FK relation (`lambda t: t.account.ledger_id == 1`): each hop renders one INNER join, deduplicated by relation path (ADR-0006). `join()` forces a join on a relation path (a bare `join()` is an existence filter on a nullable relation), and `left_join()` marks the whole path LEFT to keep relation-less rows. `order_by(..., nulls="first" | "last")` pins `NULL` placement when a sort key is nullable (dialect defaults otherwise — see [Ordering, Limit & Offset](../guide/queries.md#ordering-limit--offset)). See the [Querying Across Relationships](../guide/queries.md#querying-across-relationships) guide for worked examples.
|
|
8
8
|
|
|
9
9
|
A reverse (`BackRef`) or many-to-many relation in a predicate supports exactly one verb — the **existence test** `t.rel.exists(inner_lambda=None)` (ADR-0007). It renders as a correlated `EXISTS` at every cardinality (never a join, so the result stays root-shaped and each matching root returns once), negates with `~`, and the optional inner lambda is a full ferro predicate over the related model (operators, `&`/`|`/`~`, forward traversal rendered inside the subquery, nested tests). Everything else on a reverse edge — column access, comparisons (including `!= None`), `in_` (including a query RHS), `join()`/`left_join()` — raises at build time naming `.exists()`; an inner lambda referencing any scope but its own parameter is likewise rejected ([#309](https://github.com/syn54x/ferro-orm/issues/309)). See [Existence Tests](../guide/queries.md#existence-tests-on-reverse-many-to-many-relations) for worked examples.
|
|
10
10
|
|
|
@@ -166,7 +166,31 @@ For a row where `amount` is `NULL`, `amount > 100` is unknown, `NOT unknown` is
|
|
|
166
166
|
|
|
167
167
|
## Ordering, Limit & Offset
|
|
168
168
|
|
|
169
|
-
Sort with `.order_by(field, direction)` (direction defaults to ascending; pass `"desc"` to reverse) and slice with `.limit()` / `.offset()`. `field` is a lambda naming the column (`order_by(lambda u: u.created_at, "desc")`, matching the `where()` predicate style) or a column-name string (`order_by("created_at", "desc")`). Both forms are validated against the model's queryable columns at build time
|
|
169
|
+
Sort with `.order_by(field, direction, *, nulls=...)` (direction defaults to ascending; pass `"desc"` to reverse) and slice with `.limit()` / `.offset()`. Pass `nulls="first"` or `nulls="last"` to pin where `NULL` sort keys land. `field` is a lambda naming the column (`order_by(lambda u: u.created_at, "desc")`, matching the `where()` predicate style) or a column-name string (`order_by("created_at", "desc")`). Both forms are validated against the model's queryable columns at build time.
|
|
170
|
+
|
|
171
|
+
A pinned-first list is the usual reason to care — put unpinned cards (`pinned_at IS NULL`) after pinned ones, then break ties by recency:
|
|
172
|
+
|
|
173
|
+
=== "Assignment"
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
--8<-- "docs/examples/predicates.py:card-model"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
=== "Annotated"
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
--8<-- "docs/examples/predicates_annotated.py:card-model"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
--8<-- "docs/examples/predicates.py:nulls-ordering"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```sql
|
|
190
|
+
ORDER BY pinned_at DESC NULLS LAST, updated_at DESC, id DESC
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Omitting `nulls=` on a nullable sort key leaves placement to the dialect: PostgreSQL `DESC` puts `NULL`s first; SQLite `DESC` puts them last. Pass `nulls=` when you care.
|
|
170
194
|
|
|
171
195
|
```python
|
|
172
196
|
--8<-- "docs/examples/predicates.py:ordering-slicing"
|
|
@@ -342,8 +366,8 @@ When you want the relation-less rows *kept* rather than filtered out, opt into a
|
|
|
342
366
|
|
|
343
367
|
A bare `left_join` on a path also traversed by `where()` lifts the shared edge to LEFT — an explicit LEFT always beats an implicit INNER on the same edge (declaring `join` **and** `left_join` on one edge is a build-time `ValueError`).
|
|
344
368
|
|
|
345
|
-
!!! note "NULL ordering
|
|
346
|
-
|
|
369
|
+
!!! note "NULL ordering is dialect-defined unless you set `nulls=`"
|
|
370
|
+
Relation-less rows under `left_join` land as `NULL` sort keys on a related column — the same dialect-default split that hits a nullable **root** column. Pass `nulls="first"` or `nulls="last"` when you care — see [Ordering, Limit & Offset](#ordering-limit--offset).
|
|
347
371
|
|
|
348
372
|
### Two foreign keys to the same table
|
|
349
373
|
|