ferro-orm 0.16.3__tar.gz → 0.17.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.16.3 → ferro_orm-0.17.0}/CHANGELOG.md +29 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/CONTEXT.md +4 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/Cargo.lock +28 -28
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/Cargo.toml +1 -1
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/PKG-INFO +1 -1
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/crates/ferro-schema-ir/src/lib.rs +263 -16
- ferro_orm-0.17.0/docs/adr/0007-reverse-relations-tested-not-traversed.md +62 -0
- ferro_orm-0.17.0/docs/adr/0008-predicate-negation-uniform-not.md +30 -0
- ferro_orm-0.17.0/docs/examples/existence_tests.py +195 -0
- ferro_orm-0.17.0/docs/examples/existence_tests_annotated.py +101 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/predicates.py +42 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/predicates_annotated.py +22 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/api/queries.md +4 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/guide/queries.md +172 -4
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/roadmap.md +2 -2
- ferro_orm-0.17.0/docs/plans/2026-07-18-016-existence-tests-design.md +161 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/pyproject.toml +1 -1
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/columns.py +94 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/ir/compiler.py +5 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/models.py +4 -1
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/query/builder.py +48 -9
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/query/nodes.py +484 -24
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/query/wire.py +14 -7
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/operations.rs +147 -29
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/query.rs +572 -4
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/README.md +7 -5
- ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_account_exists_v7.json +40 -0
- ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_account_scoped_exists_v7.json +74 -0
- ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_owner_nested_exists_v7.json +47 -0
- ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_owner_not_exists_v7.json +37 -0
- ferro_orm-0.16.3/tests/fixtures/ir_vectors/query_transaction_aggregate_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_aggregate_v7.json +2 -2
- ferro_orm-0.16.3/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_global_aggregate_v7.json +2 -2
- ferro_orm-0.16.3/tests/fixtures/ir_vectors/query_transaction_include_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_include_v7.json +2 -2
- ferro_orm-0.16.3/tests/fixtures/ir_vectors/query_transaction_left_join_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_left_join_v7.json +2 -2
- ferro_orm-0.16.3/tests/fixtures/ir_vectors/query_transaction_record_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_record_v7.json +2 -2
- ferro_orm-0.16.3/tests/fixtures/ir_vectors/query_transaction_traversal_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_traversal_v7.json +2 -2
- ferro_orm-0.16.3/tests/fixtures/ir_vectors/query_transaction_traversed_record_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_transaction_traversed_record_v7.json +2 -2
- ferro_orm-0.16.3/tests/fixtures/ir_vectors/query_user_compound_v5.json → ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_user_compound_v7.json +2 -2
- ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_user_m2m_exists_v7.json +51 -0
- ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_user_not_compound_v7.json +49 -0
- ferro_orm-0.17.0/tests/fixtures/ir_vectors/query_user_not_leaf_v7.json +44 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_ir_vectors_contract.py +58 -7
- ferro_orm-0.17.0/tests/test_negation.py +226 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_query_builder.py +1 -1
- ferro_orm-0.17.0/tests/test_query_exists.py +707 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_query_joins.py +13 -1
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_query_wire_vectors.py +82 -10
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/PERMISSIONS.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.github/workflows/release.yml +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.gitignore +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/.python-version +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/AGENTS.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/LICENSE +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/README.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/benchmarks/README.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/benchmarks/__init__.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/benchmarks/__main__.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/benchmarks/backends.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/benchmarks/baselines/postgres.json +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/benchmarks/baselines/sqlite.json +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/benchmarks/compare.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/benchmarks/harness.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/benchmarks/model.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/benchmarks/run.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/crates/ferro-ddl-lowering/src/lib.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/crates/ferro-migrate/Cargo.toml +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/crates/ferro-migrate/src/emit.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/crates/ferro-migrate/src/lib.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/crates/ferro-migrate/src/order.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/crates/ferro-migrate/src/tests.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/crates/ferro-schema-ir/Cargo.toml +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/adr/0001-resolved-epoch-registration.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/adr/0008-populated-relations-include.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/agents/domain.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/agents/issue-tracker.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/agents/triage-labels.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/architecture-review-2026-07-08.html +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-26-ir-p8.5-143-db-type-drop-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-28-ir-p8.5-141-single-schema-ir-producer-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-29-ir-p8.6-146-dialect-enum-unification-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-29-ir-p8.6-153-create-path-ir-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-01-ir-p8.6-154-datetime-tz-coarseness-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-01-ir-p8.6-162-typed-save-bind-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-01-ir-p8.6-165-blob-introspection-false-positive-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/aggregations.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/aggregations_annotated.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/multiple_databases.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/mutations.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/pagination.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/partial_selects.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/partial_selects_annotated.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/populated_relations.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/populated_relations_annotated.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/quickstart.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/quickstart_annotated.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/raw_sql.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/relationships.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/relationships_annotated.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/soft_deletes.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/soft_deletes_annotated.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/testing_conftest.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/timestamps.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/timestamps_annotated.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/transactions.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/traversal.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/examples/traversal_annotated.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/api/connection.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/api/exceptions.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/api/fields.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/api/migrations.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/api/model.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/api/raw-sql.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/api/relationships.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/api/transactions.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/changelog.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/concepts/architecture.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/concepts/backends.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/concepts/identity-map.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/concepts/performance.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/concepts/query-typing.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/concepts/type-safety.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/contributing.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/faq.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/getting-started/installation.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/getting-started/next-steps.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/getting-started/quickstart.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/guide/aggregations.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/guide/connections.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/guide/migrations.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/guide/models-and-fields.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/guide/mutations.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/guide/raw-sql.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/guide/relationships.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/guide/transactions.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/howto/multiple-databases.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/howto/pagination.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/howto/soft-deletes.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/howto/testing.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/howto/timestamps.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/howto/upgrade-guide.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/index.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/stylesheets/extra.css +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/pages/why-ferro.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-06-24-001-feat-ir-p8-119-wire-automigrate-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-06-25-001-feat-ir-p8-120-parity-gate-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-06-26-001-feat-ir-p8.5-140-shared-lowering-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-06-26-002-feat-ir-p8.5-143-db-type-drop-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-06-26-003-feat-ir-p8.5-144-reconcile-indexes-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-06-28-001-feat-ir-p8.5-141-single-schema-ir-producer-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-06-29-001-refactor-ir-p8.6-153-create-path-unification-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-06-29-002-refactor-ir-p8.6-146-dialect-enum-unification-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-06-30-001-refactor-ir-p8.6-158-check-renderer-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-06-30-002-fix-ir-p8.6-155-deferred-annotations-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-01-001-feat-ir-p8.6-162-typed-save-bind-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-01-002-fix-ir-p8.6-154-datetime-tz-coarseness-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-01-003-fix-ir-p8.6-165-blob-introspection-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-02-002-feat-ff-a-172-typed-exceptions-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-06-009-ff-f-query-builder-1.0-design.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-06-010-ff-f-query-builder-1.0-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/ir-first-migration-guide.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/plans/ir-first-release-checklist.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/rfc/ir-contracts-v1.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/README.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/issues/self-fk-evicts-component-from-create-order.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/patterns/foreign-key-index.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/patterns/ir-invariants.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/patterns/persistence-state.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/docs/solutions/patterns/typed-null-binds.md +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/justfile +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/scripts/demo_queries_pre_v012.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/backend.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/codec.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/codec_plan.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/connection.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/errors.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/__init__.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/_annotation_utils.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/_bind_payload.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/_core.pyi +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/_deprecations.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/_shadow_fk_types.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/base.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/composite_indexes.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/composite_uniques.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/exceptions.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/fields.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/ir/__init__.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/metaclass.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/migrations/alembic.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/py.typed +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/query/rows.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/raw.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/registry.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/relations/__init__.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/relations/descriptors.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/session.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/ferro/state.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/hydration.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/introspect.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/lib.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/migrate.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/naming_ffi.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/schema.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/schema_bind.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/src/state.rs +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/__init__.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/conftest.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/db_backends.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/static_fixtures/bad_includes.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/static_fixtures/bad_predicates.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/static_fixtures/bad_projections.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/static_fixtures/good_includes.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/static_fixtures/good_predicates.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/static_fixtures/good_projections.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_alembic_autogenerate.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_alembic_bridge.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_alembic_db_type.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_alembic_nullability.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_alembic_type_mapping.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_annotation_utils.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_auto_migrate.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_bulk_create_chunking.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_bulk_install.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_catalog_cache.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_codec_plan.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_column_specs.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_compiled_query.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_composite_index.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_composite_unique.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_connection.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_connection_redaction.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_constraints.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_cross_emitter_parity.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_crud.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_db_backends.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_db_type_cross_emitter_parity.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_db_type_integration.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_db_type_typing.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_db_type_validation.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_deletion.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_deprecations.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_documentation_features.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_enum_cold_hydration.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_exception_mapping.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_exceptions.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_field_wrapper.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_fk_target_pk.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_generation_counter_dirty_tracking.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_global_aggregates.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_grouped_aggregates.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_helpers.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_hydration.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_hydration_equivalence.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_identity_memory.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_identity_refresh.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_identity_scoped_invalidation.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_identity_weakref.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_import_budget.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_include.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_metaclass_internals.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_metadata.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_migrate_plan.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_model_identity.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_models.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_mutation_pagination_guard.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_named_connections_integration.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_naming_single_source.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_one_to_one.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_operation_seam_sync.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_partial_selects.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_primary_key_facts.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_provisional_import.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_query_column_validation.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_query_immutability.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_query_typing.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_raw_sql.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_refresh.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_registry_entrypoints.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_registry_owner.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_relation_specs.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_relationship_engine.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_relationship_resolution_errors.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_route_single_site.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_routing_errors.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_save_pk_edges.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_save_semantics.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_schema.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_schema_db_type_metadata.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_schema_enum_annotations.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_session.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_shadow_fk_types.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_static_contracts.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_string_search.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_structural_types.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_transactions.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_traversed_projection.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_typed_null_binds.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/tests/test_typed_save_bind.py +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/uv.lock +0 -0
- {ferro_orm-0.16.3 → ferro_orm-0.17.0}/zensical.toml +0 -0
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.17.0 (2026-07-18)
|
|
5
|
+
|
|
6
|
+
### Documentation
|
|
7
|
+
|
|
8
|
+
- Design existence tests (.exists()) and uniform ~ negation
|
|
9
|
+
([`c68a477`](https://github.com/syn54x/ferro-orm/commit/c68a47780cd5fe6d8ecfcb4f7e0f9c8ec2b798fe))
|
|
10
|
+
|
|
11
|
+
- **query**: Document universal ~ negation and three-valued logic
|
|
12
|
+
([`6061d66`](https://github.com/syn54x/ferro-orm/commit/6061d66e96c99f089339f7a4a83b9424101da8be))
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- **query**: Bare .exists() on reverse FK — existence-test tracer bullet
|
|
17
|
+
([#319](https://github.com/syn54x/ferro-orm/pull/319),
|
|
18
|
+
[`2538e06`](https://github.com/syn54x/ferro-orm/commit/2538e06b217e449d10dd564687f5ca3581d5d08d))
|
|
19
|
+
|
|
20
|
+
- **query**: Existence-test error surfaces and docs — the feature's edges
|
|
21
|
+
([`be88aed`](https://github.com/syn54x/ferro-orm/commit/be88aed35f0975f9333bf0dabacaffc8a82f8f34))
|
|
22
|
+
|
|
23
|
+
- **query**: M2M existence tests — the two-hop correlation path
|
|
24
|
+
([`336b529`](https://github.com/syn54x/ferro-orm/commit/336b5298d0e7beca8231452655544fcc7a15ad6a))
|
|
25
|
+
|
|
26
|
+
- **query**: Scoped existence tests — inner predicates, nesting, cross-scope guard
|
|
27
|
+
([`df11625`](https://github.com/syn54x/ferro-orm/commit/df11625b90e01d293f4863bec8afafb9110a1ac3))
|
|
28
|
+
|
|
29
|
+
- **query**: Uniform predicate negation — prefix ~ as a NOT wire node
|
|
30
|
+
([`bfa3126`](https://github.com/syn54x/ferro-orm/commit/bfa3126914b5a1b5d93a65ee3fe2f066bb314b1f))
|
|
31
|
+
|
|
32
|
+
|
|
4
33
|
## v0.16.3 (2026-07-16)
|
|
5
34
|
|
|
6
35
|
### Bug Fixes
|
|
@@ -56,6 +56,10 @@ _Avoid_: Join inference, nested filter, path lookup, string path
|
|
|
56
56
|
The ordered sequence of forward-FK hops a traversal walks (`account`, or `account → owner`). A path is the identity of a join: the same path referenced anywhere in a query is one join, and distinct paths to the same model are distinct joins. Left-join requests apply to a whole path.
|
|
57
57
|
_Avoid_: Join alias, lookup chain, dotted path string
|
|
58
58
|
|
|
59
|
+
**Existence test**:
|
|
60
|
+
The only predicate form on a reverse or many-to-many relation — `t.lines.exists(...)` — asking whether at least one related row exists, optionally scoped by a full inner predicate over the related model. Renders as a correlated EXISTS; the result stays root-shaped (shape-preserving query), so it composes with any other predicate, ordering, and paging. Reverse relations are *tested*, never *traversed*: traversal remains a forward-FK concept.
|
|
61
|
+
_Avoid_: membership filter, semi-join filter, reverse traversal, subquery filter, `.any()`
|
|
62
|
+
|
|
59
63
|
**Shape-preserving query**:
|
|
60
64
|
The invariant that filtering and ordering never change what a query returns — a query over Transaction yields Transaction instances regardless of which relations its predicates traverse. Only an explicit projection operation may change the result shape.
|
|
61
65
|
_Avoid_: Implicit projection, row narrowing
|
|
@@ -327,7 +327,7 @@ dependencies = [
|
|
|
327
327
|
|
|
328
328
|
[[package]]
|
|
329
329
|
name = "ferro"
|
|
330
|
-
version = "0.
|
|
330
|
+
version = "0.17.0"
|
|
331
331
|
dependencies = [
|
|
332
332
|
"chrono",
|
|
333
333
|
"dashmap",
|
|
@@ -412,9 +412,9 @@ dependencies = [
|
|
|
412
412
|
|
|
413
413
|
[[package]]
|
|
414
414
|
name = "futures"
|
|
415
|
-
version = "0.3.
|
|
415
|
+
version = "0.3.33"
|
|
416
416
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
417
|
-
checksum = "
|
|
417
|
+
checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
|
|
418
418
|
dependencies = [
|
|
419
419
|
"futures-channel",
|
|
420
420
|
"futures-core",
|
|
@@ -427,9 +427,9 @@ dependencies = [
|
|
|
427
427
|
|
|
428
428
|
[[package]]
|
|
429
429
|
name = "futures-channel"
|
|
430
|
-
version = "0.3.
|
|
430
|
+
version = "0.3.33"
|
|
431
431
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
432
|
-
checksum = "
|
|
432
|
+
checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
|
|
433
433
|
dependencies = [
|
|
434
434
|
"futures-core",
|
|
435
435
|
"futures-sink",
|
|
@@ -437,15 +437,15 @@ dependencies = [
|
|
|
437
437
|
|
|
438
438
|
[[package]]
|
|
439
439
|
name = "futures-core"
|
|
440
|
-
version = "0.3.
|
|
440
|
+
version = "0.3.33"
|
|
441
441
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
442
|
-
checksum = "
|
|
442
|
+
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
|
|
443
443
|
|
|
444
444
|
[[package]]
|
|
445
445
|
name = "futures-executor"
|
|
446
|
-
version = "0.3.
|
|
446
|
+
version = "0.3.33"
|
|
447
447
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
448
|
-
checksum = "
|
|
448
|
+
checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
|
|
449
449
|
dependencies = [
|
|
450
450
|
"futures-core",
|
|
451
451
|
"futures-task",
|
|
@@ -465,15 +465,15 @@ dependencies = [
|
|
|
465
465
|
|
|
466
466
|
[[package]]
|
|
467
467
|
name = "futures-io"
|
|
468
|
-
version = "0.3.
|
|
468
|
+
version = "0.3.33"
|
|
469
469
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
470
|
-
checksum = "
|
|
470
|
+
checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
|
|
471
471
|
|
|
472
472
|
[[package]]
|
|
473
473
|
name = "futures-macro"
|
|
474
|
-
version = "0.3.
|
|
474
|
+
version = "0.3.33"
|
|
475
475
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
476
|
-
checksum = "
|
|
476
|
+
checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
|
|
477
477
|
dependencies = [
|
|
478
478
|
"proc-macro2",
|
|
479
479
|
"quote",
|
|
@@ -482,21 +482,21 @@ dependencies = [
|
|
|
482
482
|
|
|
483
483
|
[[package]]
|
|
484
484
|
name = "futures-sink"
|
|
485
|
-
version = "0.3.
|
|
485
|
+
version = "0.3.33"
|
|
486
486
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
487
|
-
checksum = "
|
|
487
|
+
checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
|
|
488
488
|
|
|
489
489
|
[[package]]
|
|
490
490
|
name = "futures-task"
|
|
491
|
-
version = "0.3.
|
|
491
|
+
version = "0.3.33"
|
|
492
492
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
493
|
-
checksum = "
|
|
493
|
+
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
|
|
494
494
|
|
|
495
495
|
[[package]]
|
|
496
496
|
name = "futures-util"
|
|
497
|
-
version = "0.3.
|
|
497
|
+
version = "0.3.33"
|
|
498
498
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
499
|
-
checksum = "
|
|
499
|
+
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
|
|
500
500
|
dependencies = [
|
|
501
501
|
"futures-channel",
|
|
502
502
|
"futures-core",
|
|
@@ -1045,9 +1045,9 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
|
|
1045
1045
|
|
|
1046
1046
|
[[package]]
|
|
1047
1047
|
name = "portable-atomic"
|
|
1048
|
-
version = "1.
|
|
1048
|
+
version = "1.14.0"
|
|
1049
1049
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1050
|
-
checksum = "
|
|
1050
|
+
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
|
|
1051
1051
|
|
|
1052
1052
|
[[package]]
|
|
1053
1053
|
name = "potential_utf"
|
|
@@ -1771,9 +1771,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|
|
1771
1771
|
|
|
1772
1772
|
[[package]]
|
|
1773
1773
|
name = "tokio"
|
|
1774
|
-
version = "1.
|
|
1774
|
+
version = "1.53.0"
|
|
1775
1775
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1776
|
-
checksum = "
|
|
1776
|
+
checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
|
|
1777
1777
|
dependencies = [
|
|
1778
1778
|
"bytes",
|
|
1779
1779
|
"libc",
|
|
@@ -1788,9 +1788,9 @@ dependencies = [
|
|
|
1788
1788
|
|
|
1789
1789
|
[[package]]
|
|
1790
1790
|
name = "tokio-macros"
|
|
1791
|
-
version = "2.7.
|
|
1791
|
+
version = "2.7.1"
|
|
1792
1792
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1793
|
-
checksum = "
|
|
1793
|
+
checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
|
|
1794
1794
|
dependencies = [
|
|
1795
1795
|
"proc-macro2",
|
|
1796
1796
|
"quote",
|
|
@@ -1989,14 +1989,14 @@ version = "0.26.11"
|
|
|
1989
1989
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1990
1990
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
|
1991
1991
|
dependencies = [
|
|
1992
|
-
"webpki-roots 1.0.
|
|
1992
|
+
"webpki-roots 1.0.9",
|
|
1993
1993
|
]
|
|
1994
1994
|
|
|
1995
1995
|
[[package]]
|
|
1996
1996
|
name = "webpki-roots"
|
|
1997
|
-
version = "1.0.
|
|
1997
|
+
version = "1.0.9"
|
|
1998
1998
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1999
|
-
checksum = "
|
|
1999
|
+
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
|
2000
2000
|
dependencies = [
|
|
2001
2001
|
"rustls-pki-types",
|
|
2002
2002
|
]
|
|
@@ -137,13 +137,15 @@ pub struct SchemaCheck {
|
|
|
137
137
|
/// Query IR: filter, sort, pagination, joins, materialization plan, and
|
|
138
138
|
/// optional M2M join context.
|
|
139
139
|
///
|
|
140
|
-
/// `ir_version:
|
|
141
|
-
/// #269, #278, #285, #292). `joins` is required and always
|
|
142
|
-
/// the query traverses no relation); every leaf and
|
|
143
|
-
/// `path` (required, `[]` = root model);
|
|
144
|
-
/// plan travels with the query as data
|
|
145
|
-
/// column list. v5
|
|
146
|
-
///
|
|
140
|
+
/// `ir_version: 7` (unconditional, no earlier version emitted anywhere —
|
|
141
|
+
/// #269, #278, #285, #292, #310, #314). `joins` is required and always
|
|
142
|
+
/// present (`[]` when the query traverses no relation); every leaf and
|
|
143
|
+
/// `order_by` entry carries a `path` (required, `[]` = root model);
|
|
144
|
+
/// `materialization` is required — the plan travels with the query as data
|
|
145
|
+
/// (ADR-0007), never inferred from a column list. v5 gave `record` fields
|
|
146
|
+
/// expression sources (#292, ADR-0009); v6 gave predicate trees the
|
|
147
|
+
/// recursive `not` node kind (uniform negation, #310, ADR-0008); v7 adds the
|
|
148
|
+
/// recursive `exists` node kind (existence tests, #314, ADR-0007).
|
|
147
149
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
148
150
|
pub struct QueryIrPayload {
|
|
149
151
|
/// Model class name the query targets.
|
|
@@ -333,7 +335,9 @@ pub struct QueryOrderBy {
|
|
|
333
335
|
pub path: Vec<String>,
|
|
334
336
|
}
|
|
335
337
|
|
|
336
|
-
/// Predicate tree node in query IR
|
|
338
|
+
/// Predicate tree node in query IR: leaf comparison, compound AND/OR, a
|
|
339
|
+
/// recursive NOT wrapping any child (uniform negation, v6, ADR-0008), or a
|
|
340
|
+
/// recursive existence test over a reverse/M2M relation (v7, ADR-0007).
|
|
337
341
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
338
342
|
#[serde(tag = "node_kind")]
|
|
339
343
|
pub enum QueryNode {
|
|
@@ -360,6 +364,37 @@ pub enum QueryNode {
|
|
|
360
364
|
/// Right subtree.
|
|
361
365
|
right: Box<QueryNode>,
|
|
362
366
|
},
|
|
367
|
+
/// Uniform negation (`~`, ADR-0008): SQL `NOT (...)` over the child.
|
|
368
|
+
/// Fully general — the child may be any node kind, at any depth; there
|
|
369
|
+
/// are no per-operator negative forms on the wire.
|
|
370
|
+
#[serde(rename = "not")]
|
|
371
|
+
Not {
|
|
372
|
+
/// The negated subtree.
|
|
373
|
+
child: Box<QueryNode>,
|
|
374
|
+
},
|
|
375
|
+
/// Existence test on a reverse or M2M relation (ADR-0007): a correlated
|
|
376
|
+
/// `EXISTS (SELECT 1 ...)` at every cardinality. Carries no negation
|
|
377
|
+
/// flag — NOT EXISTS is the `not` node over this one.
|
|
378
|
+
#[serde(rename = "exists")]
|
|
379
|
+
Exists {
|
|
380
|
+
/// Correlation hop path in the `joins`-section hop-fact shape: one
|
|
381
|
+
/// hop for a reverse FK (`FROM child WHERE child.fk = root.pk`), two
|
|
382
|
+
/// for M2M (join table, then target). The first hop's table is the
|
|
383
|
+
/// subquery FROM, correlated to the enclosing scope's alias;
|
|
384
|
+
/// remaining hops render as inner joins inside the subquery.
|
|
385
|
+
hops: Vec<QueryJoinHop>,
|
|
386
|
+
/// Ordinary inner condition tree over the relation's target model
|
|
387
|
+
/// (`[]` = bare test). May itself contain `exists`/`not` nodes —
|
|
388
|
+
/// nesting is recursion, not a second mechanism.
|
|
389
|
+
#[serde(rename = "where")]
|
|
390
|
+
where_clause: Vec<QueryNode>,
|
|
391
|
+
/// Forward-traversal joins the inner tree references (#315),
|
|
392
|
+
/// rendered INSIDE the subquery as inner joins (ADR-0006 traversal
|
|
393
|
+
/// semantics are unchanged there; `left_join` has no inner-lambda
|
|
394
|
+
/// spelling). Absent on the wire when empty — pinned bytes.
|
|
395
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
396
|
+
joins: Vec<QueryJoin>,
|
|
397
|
+
},
|
|
363
398
|
}
|
|
364
399
|
|
|
365
400
|
/// Typed literal or parameter value on the RHS of a leaf predicate.
|
|
@@ -438,7 +473,7 @@ mod tests {
|
|
|
438
473
|
#[test]
|
|
439
474
|
fn query_fixture_roundtrip() {
|
|
440
475
|
let fixture =
|
|
441
|
-
include_str!("../../../tests/fixtures/ir_vectors/
|
|
476
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_user_compound_v7.json");
|
|
442
477
|
let parsed: serde_json::Value =
|
|
443
478
|
serde_json::from_str(fixture).expect("query fixture must parse");
|
|
444
479
|
let ir = parsed
|
|
@@ -456,12 +491,224 @@ mod tests {
|
|
|
456
491
|
assert_eq!(encoded, ir, "query round-trip must not drift");
|
|
457
492
|
}
|
|
458
493
|
|
|
494
|
+
#[test]
|
|
495
|
+
fn query_not_leaf_fixture_roundtrip() {
|
|
496
|
+
// The not-over-leaf golden vector (#312, ADR-0008): uniform negation
|
|
497
|
+
// travels as one recursive `not` node wrapping its child — here a
|
|
498
|
+
// leaf `IN` comparison (the NOT IN spelling) — and must survive a
|
|
499
|
+
// deserialize/serialize round-trip without drift.
|
|
500
|
+
let fixture =
|
|
501
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_user_not_leaf_v7.json");
|
|
502
|
+
let parsed: serde_json::Value =
|
|
503
|
+
serde_json::from_str(fixture).expect("query not-leaf fixture must parse");
|
|
504
|
+
let ir = parsed
|
|
505
|
+
.get("ir")
|
|
506
|
+
.cloned()
|
|
507
|
+
.expect("fixture must contain ir envelope");
|
|
508
|
+
let envelope: IrEnvelope<QueryIrPayload> =
|
|
509
|
+
serde_json::from_value(ir.clone()).expect("query not-leaf IR must deserialize");
|
|
510
|
+
// The wire carries a `not` node whose child is the IN leaf.
|
|
511
|
+
match &envelope.payload.where_clause[0] {
|
|
512
|
+
QueryNode::Not { child } => match child.as_ref() {
|
|
513
|
+
QueryNode::Leaf { operator, .. } => assert_eq!(operator, "IN"),
|
|
514
|
+
other => panic!("not child must be the IN leaf, got {other:?}"),
|
|
515
|
+
},
|
|
516
|
+
other => panic!("where[0] must be a not node, got {other:?}"),
|
|
517
|
+
}
|
|
518
|
+
let encoded = serde_json::to_value(&envelope).expect("query not-leaf IR must serialize");
|
|
519
|
+
assert_eq!(encoded, ir, "query not-leaf round-trip must not drift");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
#[test]
|
|
523
|
+
fn query_not_compound_fixture_roundtrip() {
|
|
524
|
+
// The not-over-compound golden vector (#313, ADR-0008): `~` wraps an
|
|
525
|
+
// OR compound whole — no De Morgan expansion on the wire — and must
|
|
526
|
+
// survive a deserialize/serialize round-trip without drift.
|
|
527
|
+
let fixture =
|
|
528
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_user_not_compound_v7.json");
|
|
529
|
+
let parsed: serde_json::Value =
|
|
530
|
+
serde_json::from_str(fixture).expect("query not-compound fixture must parse");
|
|
531
|
+
let ir = parsed
|
|
532
|
+
.get("ir")
|
|
533
|
+
.cloned()
|
|
534
|
+
.expect("fixture must contain ir envelope");
|
|
535
|
+
let envelope: IrEnvelope<QueryIrPayload> =
|
|
536
|
+
serde_json::from_value(ir.clone()).expect("query not-compound IR must deserialize");
|
|
537
|
+
match &envelope.payload.where_clause[0] {
|
|
538
|
+
QueryNode::Not { child } => match child.as_ref() {
|
|
539
|
+
QueryNode::Compound { operator, .. } => assert_eq!(operator, "OR"),
|
|
540
|
+
other => panic!("not child must be the OR compound, got {other:?}"),
|
|
541
|
+
},
|
|
542
|
+
other => panic!("where[0] must be a not node, got {other:?}"),
|
|
543
|
+
}
|
|
544
|
+
let encoded =
|
|
545
|
+
serde_json::to_value(&envelope).expect("query not-compound IR must serialize");
|
|
546
|
+
assert_eq!(encoded, ir, "query not-compound round-trip must not drift");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#[test]
|
|
550
|
+
fn query_exists_fixture_roundtrip() {
|
|
551
|
+
// The bare 1-hop existence-test golden vector (#314, ADR-0007): an
|
|
552
|
+
// `exists` node carries its correlation hop path plus an empty inner
|
|
553
|
+
// condition tree, and must survive a deserialize/serialize
|
|
554
|
+
// round-trip without drift.
|
|
555
|
+
let fixture =
|
|
556
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_account_exists_v7.json");
|
|
557
|
+
let parsed: serde_json::Value =
|
|
558
|
+
serde_json::from_str(fixture).expect("query exists fixture must parse");
|
|
559
|
+
let ir = parsed
|
|
560
|
+
.get("ir")
|
|
561
|
+
.cloned()
|
|
562
|
+
.expect("fixture must contain ir envelope");
|
|
563
|
+
let envelope: IrEnvelope<QueryIrPayload> =
|
|
564
|
+
serde_json::from_value(ir.clone()).expect("query exists IR must deserialize");
|
|
565
|
+
match &envelope.payload.where_clause[0] {
|
|
566
|
+
QueryNode::Exists {
|
|
567
|
+
hops, where_clause, ..
|
|
568
|
+
} => {
|
|
569
|
+
assert_eq!(hops.len(), 1);
|
|
570
|
+
assert_eq!(hops[0].relation, "transactions");
|
|
571
|
+
assert_eq!(hops[0].from_column, "id");
|
|
572
|
+
assert_eq!(hops[0].to_table, "transaction");
|
|
573
|
+
assert_eq!(hops[0].to_column, "account_id");
|
|
574
|
+
assert!(where_clause.is_empty(), "bare test carries no inner tree");
|
|
575
|
+
}
|
|
576
|
+
other => panic!("where[0] must be an exists node, got {other:?}"),
|
|
577
|
+
}
|
|
578
|
+
let encoded = serde_json::to_value(&envelope).expect("query exists IR must serialize");
|
|
579
|
+
assert_eq!(encoded, ir, "query exists round-trip must not drift");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
#[test]
|
|
583
|
+
fn query_not_exists_fixture_roundtrip() {
|
|
584
|
+
// NOT EXISTS on the wire is the ordinary `not` node over an `exists`
|
|
585
|
+
// node — the exists node carries no negation flag (ADR-0008
|
|
586
|
+
// composition; #314).
|
|
587
|
+
let fixture =
|
|
588
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_owner_not_exists_v7.json");
|
|
589
|
+
let parsed: serde_json::Value =
|
|
590
|
+
serde_json::from_str(fixture).expect("query not-exists fixture must parse");
|
|
591
|
+
let ir = parsed
|
|
592
|
+
.get("ir")
|
|
593
|
+
.cloned()
|
|
594
|
+
.expect("fixture must contain ir envelope");
|
|
595
|
+
let envelope: IrEnvelope<QueryIrPayload> =
|
|
596
|
+
serde_json::from_value(ir.clone()).expect("query not-exists IR must deserialize");
|
|
597
|
+
match &envelope.payload.where_clause[0] {
|
|
598
|
+
QueryNode::Not { child } => match child.as_ref() {
|
|
599
|
+
QueryNode::Exists { hops, .. } => assert_eq!(hops[0].relation, "accounts"),
|
|
600
|
+
other => panic!("not child must be the exists node, got {other:?}"),
|
|
601
|
+
},
|
|
602
|
+
other => panic!("where[0] must be a not node, got {other:?}"),
|
|
603
|
+
}
|
|
604
|
+
let encoded = serde_json::to_value(&envelope).expect("query not-exists IR must serialize");
|
|
605
|
+
assert_eq!(encoded, ir, "query not-exists round-trip must not drift");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
#[test]
|
|
609
|
+
fn query_scoped_exists_fixture_roundtrip() {
|
|
610
|
+
// The scoped existence-test golden vector (#315): the inner tree is
|
|
611
|
+
// an ordinary condition tree, and a traversed inner leaf's hop facts
|
|
612
|
+
// ride the exists node's own `joins` section (rendered INSIDE the
|
|
613
|
+
// subquery). Must survive a deserialize/serialize round-trip.
|
|
614
|
+
let fixture =
|
|
615
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_account_scoped_exists_v7.json");
|
|
616
|
+
let parsed: serde_json::Value =
|
|
617
|
+
serde_json::from_str(fixture).expect("query scoped-exists fixture must parse");
|
|
618
|
+
let ir = parsed
|
|
619
|
+
.get("ir")
|
|
620
|
+
.cloned()
|
|
621
|
+
.expect("fixture must contain ir envelope");
|
|
622
|
+
let envelope: IrEnvelope<QueryIrPayload> =
|
|
623
|
+
serde_json::from_value(ir.clone()).expect("query scoped-exists IR must deserialize");
|
|
624
|
+
match &envelope.payload.where_clause[0] {
|
|
625
|
+
QueryNode::Exists {
|
|
626
|
+
hops,
|
|
627
|
+
where_clause,
|
|
628
|
+
joins,
|
|
629
|
+
} => {
|
|
630
|
+
assert_eq!(hops.len(), 1);
|
|
631
|
+
assert_eq!(where_clause.len(), 1);
|
|
632
|
+
assert_eq!(joins.len(), 1);
|
|
633
|
+
assert_eq!(joins[0].join_type, "inner");
|
|
634
|
+
assert_eq!(joins[0].path[0].relation, "account");
|
|
635
|
+
}
|
|
636
|
+
other => panic!("where[0] must be an exists node, got {other:?}"),
|
|
637
|
+
}
|
|
638
|
+
let encoded =
|
|
639
|
+
serde_json::to_value(&envelope).expect("query scoped-exists IR must serialize");
|
|
640
|
+
assert_eq!(encoded, ir, "query scoped-exists round-trip must not drift");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
#[test]
|
|
644
|
+
fn query_nested_exists_fixture_roundtrip() {
|
|
645
|
+
// Nested exists-in-exists (#315): depth is recursion, not a second
|
|
646
|
+
// mechanism, and the bare inner node omits `joins` entirely (absent,
|
|
647
|
+
// not empty — pinned wire bytes via skip_serializing_if).
|
|
648
|
+
let fixture =
|
|
649
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_owner_nested_exists_v7.json");
|
|
650
|
+
let parsed: serde_json::Value =
|
|
651
|
+
serde_json::from_str(fixture).expect("query nested-exists fixture must parse");
|
|
652
|
+
let ir = parsed
|
|
653
|
+
.get("ir")
|
|
654
|
+
.cloned()
|
|
655
|
+
.expect("fixture must contain ir envelope");
|
|
656
|
+
let envelope: IrEnvelope<QueryIrPayload> =
|
|
657
|
+
serde_json::from_value(ir.clone()).expect("query nested-exists IR must deserialize");
|
|
658
|
+
match &envelope.payload.where_clause[0] {
|
|
659
|
+
QueryNode::Exists { where_clause, .. } => match &where_clause[0] {
|
|
660
|
+
QueryNode::Exists { hops, joins, .. } => {
|
|
661
|
+
assert_eq!(hops[0].relation, "transactions");
|
|
662
|
+
assert!(joins.is_empty(), "bare nested test carries no joins");
|
|
663
|
+
}
|
|
664
|
+
other => panic!("inner where[0] must be an exists node, got {other:?}"),
|
|
665
|
+
},
|
|
666
|
+
other => panic!("where[0] must be an exists node, got {other:?}"),
|
|
667
|
+
}
|
|
668
|
+
let encoded =
|
|
669
|
+
serde_json::to_value(&envelope).expect("query nested-exists IR must serialize");
|
|
670
|
+
assert_eq!(encoded, ir, "query nested-exists round-trip must not drift");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
#[test]
|
|
674
|
+
fn query_m2m_exists_fixture_roundtrip() {
|
|
675
|
+
// The two-hop M2M existence-test golden vector (#316): the SAME
|
|
676
|
+
// exists node, a two-hop correlation path — join table first, then
|
|
677
|
+
// the target — with the scoped inner tree over the target model.
|
|
678
|
+
// Must survive a deserialize/serialize round-trip without drift.
|
|
679
|
+
let fixture =
|
|
680
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_user_m2m_exists_v7.json");
|
|
681
|
+
let parsed: serde_json::Value =
|
|
682
|
+
serde_json::from_str(fixture).expect("query m2m-exists fixture must parse");
|
|
683
|
+
let ir = parsed
|
|
684
|
+
.get("ir")
|
|
685
|
+
.cloned()
|
|
686
|
+
.expect("fixture must contain ir envelope");
|
|
687
|
+
let envelope: IrEnvelope<QueryIrPayload> =
|
|
688
|
+
serde_json::from_value(ir.clone()).expect("query m2m-exists IR must deserialize");
|
|
689
|
+
match &envelope.payload.where_clause[0] {
|
|
690
|
+
QueryNode::Exists {
|
|
691
|
+
hops, where_clause, ..
|
|
692
|
+
} => {
|
|
693
|
+
assert_eq!(hops.len(), 2, "M2M correlates through two hops");
|
|
694
|
+
assert_eq!(hops[0].to_table, "tag_users");
|
|
695
|
+
assert_eq!(hops[0].to_column, "user_id");
|
|
696
|
+
assert_eq!(hops[1].from_column, "tag_id");
|
|
697
|
+
assert_eq!(hops[1].to_table, "tag");
|
|
698
|
+
assert_eq!(where_clause.len(), 1);
|
|
699
|
+
}
|
|
700
|
+
other => panic!("where[0] must be an exists node, got {other:?}"),
|
|
701
|
+
}
|
|
702
|
+
let encoded = serde_json::to_value(&envelope).expect("query m2m-exists IR must serialize");
|
|
703
|
+
assert_eq!(encoded, ir, "query m2m-exists round-trip must not drift");
|
|
704
|
+
}
|
|
705
|
+
|
|
459
706
|
#[test]
|
|
460
707
|
fn query_traversal_fixture_roundtrip() {
|
|
461
708
|
// Multi-hop `joins` section + path-carrying leaves must survive a
|
|
462
709
|
// deserialize/serialize round-trip without drift (#270 wire stability).
|
|
463
710
|
let fixture = include_str!(
|
|
464
|
-
"../../../tests/fixtures/ir_vectors/
|
|
711
|
+
"../../../tests/fixtures/ir_vectors/query_transaction_traversal_v7.json"
|
|
465
712
|
);
|
|
466
713
|
let parsed: serde_json::Value =
|
|
467
714
|
serde_json::from_str(fixture).expect("query traversal fixture must parse");
|
|
@@ -487,7 +734,7 @@ mod tests {
|
|
|
487
734
|
// deserialize/serialize round-trip without drift, and the join_type
|
|
488
735
|
// tokens must reach Rust exactly as written on the wire.
|
|
489
736
|
let fixture =
|
|
490
|
-
include_str!("../../../tests/fixtures/ir_vectors/
|
|
737
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_transaction_left_join_v7.json");
|
|
491
738
|
let parsed: serde_json::Value =
|
|
492
739
|
serde_json::from_str(fixture).expect("query left_join fixture must parse");
|
|
493
740
|
let ir = parsed
|
|
@@ -513,7 +760,7 @@ mod tests {
|
|
|
513
760
|
// payload — predicate, order, limit, and a two-field record plan —
|
|
514
761
|
// survives a deserialize/serialize round-trip without drift.
|
|
515
762
|
let fixture =
|
|
516
|
-
include_str!("../../../tests/fixtures/ir_vectors/
|
|
763
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_transaction_record_v7.json");
|
|
517
764
|
let parsed: serde_json::Value =
|
|
518
765
|
serde_json::from_str(fixture).expect("query record fixture must parse");
|
|
519
766
|
let ir = parsed
|
|
@@ -712,7 +959,7 @@ mod tests {
|
|
|
712
959
|
// identity) — survives a deserialize/serialize round-trip without
|
|
713
960
|
// drift. No group keys: the whole result collapses to one record.
|
|
714
961
|
let fixture = include_str!(
|
|
715
|
-
"../../../tests/fixtures/ir_vectors/
|
|
962
|
+
"../../../tests/fixtures/ir_vectors/query_transaction_global_aggregate_v7.json"
|
|
716
963
|
);
|
|
717
964
|
let parsed: serde_json::Value =
|
|
718
965
|
serde_json::from_str(fixture).expect("global aggregate fixture must parse");
|
|
@@ -750,7 +997,7 @@ mod tests {
|
|
|
750
997
|
// joins section included — survives a deserialize/serialize
|
|
751
998
|
// round-trip without drift.
|
|
752
999
|
let fixture = include_str!(
|
|
753
|
-
"../../../tests/fixtures/ir_vectors/
|
|
1000
|
+
"../../../tests/fixtures/ir_vectors/query_transaction_traversed_record_v7.json"
|
|
754
1001
|
);
|
|
755
1002
|
let parsed: serde_json::Value =
|
|
756
1003
|
serde_json::from_str(fixture).expect("query traversed record fixture must parse");
|
|
@@ -788,7 +1035,7 @@ mod tests {
|
|
|
788
1035
|
// a deserialize/serialize round-trip without drift. GROUP BY does not
|
|
789
1036
|
// travel: the renderer derives it from the non-expr fields (ADR-0009).
|
|
790
1037
|
let fixture = include_str!(
|
|
791
|
-
"../../../tests/fixtures/ir_vectors/
|
|
1038
|
+
"../../../tests/fixtures/ir_vectors/query_transaction_aggregate_v7.json"
|
|
792
1039
|
);
|
|
793
1040
|
let parsed: serde_json::Value =
|
|
794
1041
|
serde_json::from_str(fixture).expect("query aggregate fixture must parse");
|
|
@@ -856,7 +1103,7 @@ mod tests {
|
|
|
856
1103
|
// payload — root predicate, empty `joins`, and a one-path instances
|
|
857
1104
|
// plan — survives a deserialize/serialize round-trip without drift.
|
|
858
1105
|
let fixture =
|
|
859
|
-
include_str!("../../../tests/fixtures/ir_vectors/
|
|
1106
|
+
include_str!("../../../tests/fixtures/ir_vectors/query_transaction_include_v7.json");
|
|
860
1107
|
let parsed: serde_json::Value =
|
|
861
1108
|
serde_json::from_str(fixture).expect("query include fixture must parse");
|
|
862
1109
|
let ir = parsed
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Reverse relations are tested, not traversed
|
|
2
|
+
|
|
3
|
+
A reverse (BackRef) or many-to-many relation appears in a query predicate in
|
|
4
|
+
exactly one form: the **existence test** — `t.lines.exists(...)`, optionally
|
|
5
|
+
scoped by an inner predicate over the related model, negated with `~`. It
|
|
6
|
+
renders as a correlated `EXISTS` subquery at every cardinality (one-to-one
|
|
7
|
+
BackRefs included) and for every relation kind (reverse FK and M2M, the latter
|
|
8
|
+
correlating through the join table). The result stays root-shaped: an
|
|
9
|
+
existence test never multiplies root rows and composes with any other
|
|
10
|
+
predicate, ordering, and paging.
|
|
11
|
+
|
|
12
|
+
The proxy for a reverse relation exposes `.exists()` and nothing else. Column
|
|
13
|
+
access (`t.lines.category_id`), comparison (`t.transfer_out != None`), and
|
|
14
|
+
`in_` all raise at build time with the supported spelling in the message.
|
|
15
|
+
There is deliberately no implicit path traversal onto reverse edges — the
|
|
16
|
+
spelling sketched by the originating external PRDs (#307/#308, from Pinch).
|
|
17
|
+
Implicit traversal has an unanswerable grouping ambiguity: in
|
|
18
|
+
`(t.lines.a == 1) & (t.lines.b == 2)` nothing visible says whether one child
|
|
19
|
+
row must match both conditions or some child may match each. Django chose
|
|
20
|
+
implicit and the resulting confusion is a documented hazard of that API. The
|
|
21
|
+
explicit combinator makes grouping the user's choice —
|
|
22
|
+
`t.lines.exists(lambda l: A & B)` versus
|
|
23
|
+
`t.lines.exists(A-test) & t.lines.exists(B-test)` — both spellable, both
|
|
24
|
+
unambiguous. This is the same determinism-over-friendliness ground on which
|
|
25
|
+
ADR-0006 rejected auto-LEFT.
|
|
26
|
+
|
|
27
|
+
The inner predicate is a full ferro predicate over the related model: same
|
|
28
|
+
operators, forward traversal (rendered as joins inside the subquery, ADR-0006
|
|
29
|
+
semantics unchanged), and nested existence tests to arbitrary depth. What it
|
|
30
|
+
may not do (v1) is reference any scope other than its own parameter —
|
|
31
|
+
cross-scope column comparison is rejected at build time and tracked as a
|
|
32
|
+
possible future extension.
|
|
33
|
+
|
|
34
|
+
Rejected alternatives:
|
|
35
|
+
|
|
36
|
+
- **`.any()`/`.has()` cardinality split (SQLAlchemy)** — two names for one
|
|
37
|
+
mechanism, and call sites break when a FK's `unique=` changes. One concept,
|
|
38
|
+
one verb: `.exists()` at every cardinality.
|
|
39
|
+
- **`in_(single-column ProjectedQuery)`** — declined, not deferred. The
|
|
40
|
+
workloads it serves are existence-test workloads; it forces callers to
|
|
41
|
+
hand-correlate on id columns and leaks the join column into every call site.
|
|
42
|
+
- **LEFT JOIN + IS NOT NULL specialization for unique BackRefs** — safe but a
|
|
43
|
+
second render path with no user-visible benefit; planners already rewrite
|
|
44
|
+
correlated EXISTS into semi-joins. May become an invisible planner-side
|
|
45
|
+
optimization later if measured.
|
|
46
|
+
- **`!= None` / `== None` sugar for bare existence** — forward `== None`
|
|
47
|
+
desugars to IS NULL on a shadow column that physically lives on the root
|
|
48
|
+
table (ADR-0006); a reverse relation has no root-side column, so the sugar
|
|
49
|
+
would be silent implicit traversal.
|
|
50
|
+
|
|
51
|
+
## Consequences
|
|
52
|
+
|
|
53
|
+
- The glossary term "Relation traversal" stays forward-FK-only, and that
|
|
54
|
+
narrowness is now load-bearing rather than incidental.
|
|
55
|
+
- `include()` (population) and `.exists()` (membership) remain distinct axes;
|
|
56
|
+
BackRef population is still a separate future mechanism.
|
|
57
|
+
- The wire IR gains one recursive node kind for the existence test, carrying a
|
|
58
|
+
correlation hop path (1 hop for reverse FK, 2 for M2M) — the Rust renderer
|
|
59
|
+
handles both with one loop, so M2M support is test surface, not mechanism.
|
|
60
|
+
- `left_join()` on a reverse edge stays rejected (it would reopen the
|
|
61
|
+
"join never multiplies root rows" property) but the error now names
|
|
62
|
+
`.exists()` as the supported spelling.
|