ferro-orm 0.16.2__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.2 → ferro_orm-0.17.0}/CHANGELOG.md +45 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/CONTEXT.md +16 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/Cargo.lock +32 -32
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/Cargo.toml +1 -1
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/PKG-INFO +1 -1
- {ferro_orm-0.16.2 → 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.2 → ferro_orm-0.17.0}/docs/examples/predicates.py +42 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/predicates_annotated.py +22 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/queries.md +4 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/queries.md +172 -4
- {ferro_orm-0.16.2 → 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.2 → ferro_orm-0.17.0}/pyproject.toml +1 -1
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/__init__.py +25 -30
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/_shadow_fk_types.py +5 -6
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/columns.py +124 -2
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/ir/compiler.py +40 -40
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/metaclass.py +7 -6
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/models.py +36 -28
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/query/builder.py +65 -91
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/query/nodes.py +484 -24
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/query/wire.py +99 -35
- ferro_orm-0.17.0/src/ferro/registry.py +328 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/relations/__init__.py +19 -25
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/relations/descriptors.py +13 -10
- ferro_orm-0.17.0/src/ferro/state.py +176 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/operations.rs +147 -29
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/query.rs +572 -4
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/conftest.py +8 -29
- {ferro_orm-0.16.2 → 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.2/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.2/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.2/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.2/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.2/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.2/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.2/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.2/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.2 → ferro_orm-0.17.0}/tests/test_alembic_autogenerate.py +6 -12
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_alembic_bridge.py +2 -4
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_alembic_db_type.py +3 -9
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_alembic_nullability.py +2 -4
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_alembic_type_mapping.py +2 -4
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_auto_migrate.py +25 -46
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_catalog_cache.py +2 -4
- ferro_orm-0.17.0/tests/test_compiled_query.py +81 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_composite_index.py +3 -7
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_composite_unique.py +3 -7
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_connection.py +2 -2
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_cross_emitter_parity.py +5 -11
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_db_type_cross_emitter_parity.py +3 -11
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_db_type_integration.py +4 -6
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_documentation_features.py +4 -4
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_enum_cold_hydration.py +6 -6
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_fk_target_pk.py +6 -14
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_generation_counter_dirty_tracking.py +16 -14
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_hydration_equivalence.py +3 -7
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_include.py +1 -1
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_ir_vectors_contract.py +71 -17
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_model_identity.py +13 -25
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_mutation_pagination_guard.py +1 -1
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_named_connections_integration.py +2 -2
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_naming_single_source.py +8 -12
- ferro_orm-0.17.0/tests/test_negation.py +226 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_one_to_one.py +2 -3
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_operation_seam_sync.py +3 -3
- ferro_orm-0.17.0/tests/test_primary_key_facts.py +77 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_provisional_import.py +9 -9
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_builder.py +4 -4
- ferro_orm-0.17.0/tests/test_query_exists.py +707 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_joins.py +15 -3
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_typing.py +2 -3
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_wire_vectors.py +87 -15
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_registry_entrypoints.py +69 -74
- ferro_orm-0.17.0/tests/test_registry_owner.py +119 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_relationship_engine.py +2 -3
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_relationship_resolution_errors.py +3 -7
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_session.py +2 -2
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_shadow_fk_types.py +4 -6
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_sqlite_alembic_reconnect_hydration.py +2 -4
- ferro_orm-0.16.2/src/ferro/state.py +0 -355
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/PERMISSIONS.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.github/workflows/release.yml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.gitignore +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/.python-version +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/AGENTS.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/LICENSE +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/README.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/README.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/__init__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/__main__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/backends.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/baselines/postgres.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/baselines/sqlite.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/compare.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/harness.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/model.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/benchmarks/run.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-ddl-lowering/Cargo.toml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-ddl-lowering/src/lib.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-migrate/Cargo.toml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-migrate/src/emit.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-migrate/src/lib.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-migrate/src/order.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-migrate/src/tests.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/crates/ferro-schema-ir/Cargo.toml +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0001-resolved-epoch-registration.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0002-unified-column-fact-derivation.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0003-path-blind-db-type-validation.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0004-jsonb-postgres-only-canonical.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0005-jsonb-default-on-postgres.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0006-relation-traversal-inner-joins.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0007-materialization-plan-complete-instances.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0008-populated-relations-include.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/adr/0009-aggregate-projections-derived-grouping.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/agents/domain.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/agents/issue-tracker.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/agents/triage-labels.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/architecture-review-2026-07-08.html +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-05-25-annotated-strenum-cold-hydration-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-24-ir-p8-119-wire-automigrate-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-25-ir-p8-120-parity-gate-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-26-ir-p8.5-140-shared-lowering-requirements.md +0 -0
- {ferro_orm-0.16.2 → 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.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-26-ir-p8.5-144-reconcile-indexes-requirements.md +0 -0
- {ferro_orm-0.16.2 → 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.2 → 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.2 → 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.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-30-ir-p8.6-155-deferred-annotations-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-06-30-ir-p8.6-158-check-renderer-requirements.md +0 -0
- {ferro_orm-0.16.2 → 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.2 → 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.2 → 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.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-02-ff-c1-codec-plan-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/brainstorms/2026-07-06-upgrade-guide-consolidation-requirements.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/aggregations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/aggregations_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/multiple_databases.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/mutations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/pagination.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/partial_selects.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/partial_selects_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/populated_relations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/populated_relations_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/quickstart.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/quickstart_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/raw_sql.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/relationships.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/relationships_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/soft_deletes.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/soft_deletes_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/testing_conftest.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/timestamps.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/timestamps_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/transactions.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/traversal.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/examples/traversal_annotated.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/connection.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/exceptions.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/fields.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/migrations.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/model.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/raw-sql.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/relationships.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/api/transactions.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/changelog.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/architecture.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/backends.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/identity-map.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/performance.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/query-typing.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/concepts/type-safety.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/contributing.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/faq.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/getting-started/installation.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/getting-started/next-steps.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/getting-started/quickstart.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/aggregations.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/connections.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/migrations.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/models-and-fields.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/mutations.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/raw-sql.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/relationships.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/guide/transactions.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/migrate-from-sqlalchemy.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/multiple-databases.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/pagination.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/soft-deletes.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/testing.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/timestamps.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/howto/upgrade-guide.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/index.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/stylesheets/extra.css +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/pages/why-ferro.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-05-25-001-fix-annotated-strenum-cold-hydration-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-06-19-001-ir-first-roadmap.md +0 -0
- {ferro_orm-0.16.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → ferro_orm-0.17.0}/docs/plans/2026-07-02-001-fable-fixes-roadmap.md +0 -0
- {ferro_orm-0.16.2 → 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.2 → ferro_orm-0.17.0}/docs/plans/2026-07-02-003-ff-c-native-decode-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-02-004-ff-c-c2-catalog-cache-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-02-005-ff-d-identity-routing-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-02-006-ff-d-identity-routing-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-03-007-ff-e-registry-identity-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-03-008-ff-e-registry-identity-plan.md +0 -0
- {ferro_orm-0.16.2 → 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.2 → 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.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-011-ff-g-hardening-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-012-ff-g-hardening-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-013-ff-g2-operations-dedup-design.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-014-ff-g2-operations-dedup-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/2026-07-06-015-docs-upgrade-guide-consolidation-plan.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/ir-first-migration-guide.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/plans/ir-first-release-checklist.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/rfc/ir-contracts-v1.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/README.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/architecture-patterns/ir-first-lowering-consolidation-audit.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/architecture-patterns/ir-first-merge-readiness-review.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/self-fk-evicts-component-from-create-order.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/sqlite-integer-decimal-hydrates-as-none.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/ddl-on-live-engine.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/derived-type-and-naming-decision-table.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/foreign-key-index.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/ir-invariants.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/persistence-state.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/sqlite-alembic-reconnect-hydration-tests.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/docs/solutions/patterns/typed-null-binds.md +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/justfile +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/scripts/demo_queries_pre_v012.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/backend.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/codec.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/codec_plan.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/connection.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/errors.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/_annotation_utils.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/_bind_payload.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/_core.pyi +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/_deprecations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/base.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/composite_indexes.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/composite_uniques.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/exceptions.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/fields.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/ir/__init__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/migrations/alembic.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/py.typed +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/query/rows.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/raw.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/ferro/session.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/hydration.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/introspect.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/lib.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/migrate.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/naming_ffi.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/schema.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/schema_bind.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/src/state.rs +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/__init__.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/db_backends.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/codec_registry_core_v1.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/schema_invoice_baseline_v1.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/schema_phase1_fixture_models_v1.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/fixtures/ir_vectors/schema_raw_str_pk_autoincrement_v1.json +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/bad_includes.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/bad_predicates.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/bad_projections.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/good_includes.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/good_predicates.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/static_fixtures/good_projections.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_annotation_utils.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_bulk_create_chunking.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_bulk_install.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_codec_plan.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_column_specs.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_connection_redaction.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_constraints.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_crud.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_db_backends.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_db_type_typing.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_db_type_validation.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_deletion.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_deprecations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_exception_mapping.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_exceptions.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_field_wrapper.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_global_aggregates.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_grouped_aggregates.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_helpers.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_hydration.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_identity_memory.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_identity_refresh.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_identity_scoped_invalidation.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_identity_weakref.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_import_budget.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_metaclass_internals.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_metadata.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_migrate_plan.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_models.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_partial_selects.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_column_validation.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_query_immutability.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_raw_sql.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_refresh.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_relation_specs.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_route_single_site.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_routing_errors.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_save_pk_edges.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_save_semantics.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_schema.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_schema_db_type_metadata.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_schema_enum_annotations.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_static_contracts.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_string_search.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_structural_types.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_transactions.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_traversed_projection.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_typed_null_binds.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/tests/test_typed_save_bind.py +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/uv.lock +0 -0
- {ferro_orm-0.16.2 → ferro_orm-0.17.0}/zensical.toml +0 -0
|
@@ -1,6 +1,51 @@
|
|
|
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
|
+
|
|
33
|
+
## v0.16.3 (2026-07-16)
|
|
34
|
+
|
|
35
|
+
### Bug Fixes
|
|
36
|
+
|
|
37
|
+
- Derive the primary-key fact once and reject multi-PK models at class definition
|
|
38
|
+
([`ef91fa0`](https://github.com/syn54x/ferro-orm/commit/ef91fa09e09d5e7444e8d659b831921d40bcb365))
|
|
39
|
+
|
|
40
|
+
### Refactoring
|
|
41
|
+
|
|
42
|
+
- Compile_query returns one CompiledQuery artifact carrying the hop-class map
|
|
43
|
+
([`fc7084c`](https://github.com/syn54x/ferro-orm/commit/fc7084c4ad89b135815f74852263989f23868143))
|
|
44
|
+
|
|
45
|
+
- Single Registry owner for Python-side registration state
|
|
46
|
+
([`1179fcc`](https://github.com/syn54x/ferro-orm/commit/1179fcc07cc9e4305b7e7a8c141b8b29264dcd26))
|
|
47
|
+
|
|
48
|
+
|
|
4
49
|
## v0.16.2 (2026-07-15)
|
|
5
50
|
|
|
6
51
|
### 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
|
|
@@ -84,6 +88,10 @@ _Avoid_: Select list, projection spec, hydration mode flag
|
|
|
84
88
|
The single typed wire artifact a query ships to the Rust runtime — model identity, predicates, ordering, paging, joins, and exactly one materialization plan, inside a versioned envelope. Compiled only by `compile_query`; no other code assembles query wire shape.
|
|
85
89
|
_Avoid_: Query dict, query def, payload dict
|
|
86
90
|
|
|
91
|
+
**Compiled query**:
|
|
92
|
+
The single artifact `compile_query` returns: the QueryIR payload, its wire JSON, and the plan-scoped hop-class map, all views of one compile. The map is collected from the hop facts the payload itself carries, so wire and hop classes can never disagree; it is `None` unless the materialization plan decodes or hydrates through a hop model's class (mirroring the Rust `needs_hop_classes` guard — a both-sides double-check). No other code assembles hop classes for the FFI.
|
|
93
|
+
_Avoid_: payload + kwargs, hop-class side-channel, wire tuple
|
|
94
|
+
|
|
87
95
|
**Golden vector**:
|
|
88
96
|
A hand-authored JSON fixture pinning one wire shape — the independent authority both the Python emitter and the Rust decoder assert against, never regenerated from either side's output. Updating one by hand is the contract-review moment for a wire change.
|
|
89
97
|
_Avoid_: Snapshot, fixture dump, example payload
|
|
@@ -100,6 +108,14 @@ _Avoid_: Nested select, join column, related-field pull
|
|
|
100
108
|
A user-chosen name for one field of a projected record, given as the key in a dict-returning selector (`select(lambda t: {"account_name": t.account.name})`). Aliases name output fields only — never joins or tables; the relation path remains the sole join identity.
|
|
101
109
|
_Avoid_: Column alias, AS label, join alias
|
|
102
110
|
|
|
111
|
+
**Primary key fact**:
|
|
112
|
+
The single cached answer to "which column is this model's primary key" — derived once at the compile choke point alongside column specs, stored as `__ferro_pk__` (`None` for a PK-less model). At most one `primary_key=True` column may be declared; a violation raises at class definition time. Operations that need a PK read the cached fact and raise clearly when it is `None` — never guess.
|
|
113
|
+
_Avoid_: PK lookup, PK scan, first primary-key column
|
|
114
|
+
|
|
115
|
+
**Registry**:
|
|
116
|
+
The single owner of Python-side registration state (`ferro.registry.REGISTRY`): model classes, per-model SchemaIR envelopes + fingerprints, join-table bundles, pending relations, the modelset artifact, and the generation counters. Its stores are private; the agreement invariants (fingerprint pairs its envelope, join-table eviction clears envelopes, one-call test reset) live inside the interface, not in caller convention. Routing/session state is not the Registry — that is `ferro.state`.
|
|
117
|
+
_Avoid_: state module, global dicts, model cache
|
|
118
|
+
|
|
103
119
|
**Provisional registration**:
|
|
104
120
|
The per-model state installed when a class body finishes executing — enough for runtime codec and PK metadata, but relationships may still be pending and the modelset is not yet authoritative for DDL.
|
|
105
121
|
_Avoid_: Import-time registration, partial registry
|
|
@@ -59,9 +59,9 @@ dependencies = [
|
|
|
59
59
|
|
|
60
60
|
[[package]]
|
|
61
61
|
name = "bitflags"
|
|
62
|
-
version = "2.13.
|
|
62
|
+
version = "2.13.1"
|
|
63
63
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
64
|
-
checksum = "
|
|
64
|
+
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
|
65
65
|
dependencies = [
|
|
66
66
|
"serde_core",
|
|
67
67
|
]
|
|
@@ -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",
|
|
@@ -1905,9 +1905,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|
|
1905
1905
|
|
|
1906
1906
|
[[package]]
|
|
1907
1907
|
name = "uuid"
|
|
1908
|
-
version = "1.
|
|
1908
|
+
version = "1.24.0"
|
|
1909
1909
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1910
|
-
checksum = "
|
|
1910
|
+
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
|
|
1911
1911
|
dependencies = [
|
|
1912
1912
|
"getrandom 0.4.3",
|
|
1913
1913
|
"js-sys",
|
|
@@ -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
|