ferro-orm 0.10.2__tar.gz → 0.10.4__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.10.2 → ferro_orm-0.10.4}/CHANGELOG.md +18 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/Cargo.lock +19 -19
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/Cargo.toml +1 -1
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/PKG-INFO +1 -1
- ferro_orm-0.10.4/docs/solutions/issues/typed-where-null-panics-is-null.md +107 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/patterns/typed-null-binds.md +13 -2
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/pyproject.toml +1 -1
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/lib.rs +1 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/operations.rs +16 -37
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/query.rs +201 -4
- ferro_orm-0.10.4/src/schema_bind.rs +49 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_structural_types.py +39 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_typed_null_binds.py +64 -36
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/PERMISSIONS.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.github/workflows/release.yml +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.gitignore +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/.python-version +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/AGENTS.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/LICENSE +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/README.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/TEST_RESULTS.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/api/exceptions.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/api/fields.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/api/model.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/api/query.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/api/raw-sql.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/api/relationships.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/api/transactions.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/api/utilities.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/changelog.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/coming-soon.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/concepts/architecture.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/concepts/identity-map.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/concepts/performance.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/concepts/query-typing.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/concepts/type-safety.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/contributing.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/faq.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/getting-started/installation.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/getting-started/next-steps.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/getting-started/tutorial.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/guide/backend.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/guide/database.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/guide/migrations.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/guide/models-and-fields.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/guide/mutations.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/guide/queries.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/guide/relationships.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/guide/transactions.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/howto/multiple-databases.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/howto/pagination.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/howto/soft-deletes.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/howto/testing.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/howto/timestamps.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/index.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/migration-sqlalchemy.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/README.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/patterns/foreign-key-index.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/stylesheets/extra.css +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/docs/why-ferro.md +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/justfile +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/mkdocs.yml +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/backend.rs +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/connection.rs +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/__init__.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/_annotation_utils.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/_core.pyi +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/_shadow_fk_types.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/base.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/composite_indexes.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/composite_uniques.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/exceptions.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/fields.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/metaclass.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/migrations/alembic.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/models.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/py.typed +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/query/builder.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/query/nodes.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/raw.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/relations/__init__.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/relations/descriptors.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/schema_metadata.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/ferro/state.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/schema.rs +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/src/state.rs +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/__init__.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/conftest.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/db_backends.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_alembic_autogenerate.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_alembic_bridge.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_alembic_db_type.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_alembic_nullability.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_alembic_type_mapping.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_auto_migrate.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_composite_index.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_composite_unique.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_connection.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_connection_redaction.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_constraints.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_cross_emitter_parity.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_crud.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_db_backends.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_db_type_cross_emitter_parity.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_db_type_integration.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_db_type_typing.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_db_type_validation.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_deletion.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_documentation_features.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_field_wrapper.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_helpers.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_hydration.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_metaclass_internals.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_metadata.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_models.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_named_connections_integration.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_one_to_one.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_query_builder.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_query_typing.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_raw_sql.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_refresh.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_relationship_engine.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_schema.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_schema_db_type_metadata.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_schema_enum_annotations.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_shadow_fk_types.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_static_contracts.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_string_search.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/tests/test_transactions.py +0 -0
- {ferro_orm-0.10.2 → ferro_orm-0.10.4}/uv.lock +0 -0
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.10.4 (2026-05-24)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- **query**: Cast native Postgres enum RHS in `.where()` filters
|
|
9
|
+
([#64](https://github.com/syn54x/ferro-orm/pull/64),
|
|
10
|
+
[`7fea893`](https://github.com/syn54x/ferro-orm/commit/7fea89320fd2216a956ae32e8ae6f4829dd8fcf7))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v0.10.3 (2026-05-21)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **query**: Typed predicates `col == None` / `!= None` → IS NULL / IS NOT NULL
|
|
18
|
+
([#62](https://github.com/syn54x/ferro-orm/pull/62),
|
|
19
|
+
[`fd4b53e`](https://github.com/syn54x/ferro-orm/commit/fd4b53e26e01f8cf41a9b73de7c29b6901dee786))
|
|
20
|
+
|
|
21
|
+
|
|
4
22
|
## v0.10.2 (2026-05-19)
|
|
5
23
|
|
|
6
24
|
### Bug Fixes
|
|
@@ -25,9 +25,9 @@ dependencies = [
|
|
|
25
25
|
|
|
26
26
|
[[package]]
|
|
27
27
|
name = "autocfg"
|
|
28
|
-
version = "1.5.
|
|
28
|
+
version = "1.5.1"
|
|
29
29
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
30
|
-
checksum = "
|
|
30
|
+
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|
31
31
|
|
|
32
32
|
[[package]]
|
|
33
33
|
name = "base64"
|
|
@@ -61,9 +61,9 @@ dependencies = [
|
|
|
61
61
|
|
|
62
62
|
[[package]]
|
|
63
63
|
name = "bumpalo"
|
|
64
|
-
version = "3.20.
|
|
64
|
+
version = "3.20.3"
|
|
65
65
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
66
|
-
checksum = "
|
|
66
|
+
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
|
67
67
|
|
|
68
68
|
[[package]]
|
|
69
69
|
name = "byteorder"
|
|
@@ -247,9 +247,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
|
|
247
247
|
|
|
248
248
|
[[package]]
|
|
249
249
|
name = "either"
|
|
250
|
-
version = "1.
|
|
250
|
+
version = "1.16.0"
|
|
251
251
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
252
|
-
checksum = "
|
|
252
|
+
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
|
253
253
|
dependencies = [
|
|
254
254
|
"serde",
|
|
255
255
|
]
|
|
@@ -294,7 +294,7 @@ dependencies = [
|
|
|
294
294
|
|
|
295
295
|
[[package]]
|
|
296
296
|
name = "ferro"
|
|
297
|
-
version = "0.10.
|
|
297
|
+
version = "0.10.4"
|
|
298
298
|
dependencies = [
|
|
299
299
|
"dashmap",
|
|
300
300
|
"once_cell",
|
|
@@ -711,9 +711,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
|
711
711
|
|
|
712
712
|
[[package]]
|
|
713
713
|
name = "js-sys"
|
|
714
|
-
version = "0.3.
|
|
714
|
+
version = "0.3.99"
|
|
715
715
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
716
|
-
checksum = "
|
|
716
|
+
checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
|
|
717
717
|
dependencies = [
|
|
718
718
|
"cfg-if",
|
|
719
719
|
"futures-util",
|
|
@@ -1292,9 +1292,9 @@ dependencies = [
|
|
|
1292
1292
|
|
|
1293
1293
|
[[package]]
|
|
1294
1294
|
name = "serde_json"
|
|
1295
|
-
version = "1.0.
|
|
1295
|
+
version = "1.0.150"
|
|
1296
1296
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1297
|
-
checksum = "
|
|
1297
|
+
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
|
1298
1298
|
dependencies = [
|
|
1299
1299
|
"itoa",
|
|
1300
1300
|
"memchr",
|
|
@@ -1892,9 +1892,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
|
|
1892
1892
|
|
|
1893
1893
|
[[package]]
|
|
1894
1894
|
name = "wasm-bindgen"
|
|
1895
|
-
version = "0.2.
|
|
1895
|
+
version = "0.2.122"
|
|
1896
1896
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1897
|
-
checksum = "
|
|
1897
|
+
checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
|
|
1898
1898
|
dependencies = [
|
|
1899
1899
|
"cfg-if",
|
|
1900
1900
|
"once_cell",
|
|
@@ -1905,9 +1905,9 @@ dependencies = [
|
|
|
1905
1905
|
|
|
1906
1906
|
[[package]]
|
|
1907
1907
|
name = "wasm-bindgen-macro"
|
|
1908
|
-
version = "0.2.
|
|
1908
|
+
version = "0.2.122"
|
|
1909
1909
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1910
|
-
checksum = "
|
|
1910
|
+
checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
|
|
1911
1911
|
dependencies = [
|
|
1912
1912
|
"quote",
|
|
1913
1913
|
"wasm-bindgen-macro-support",
|
|
@@ -1915,9 +1915,9 @@ dependencies = [
|
|
|
1915
1915
|
|
|
1916
1916
|
[[package]]
|
|
1917
1917
|
name = "wasm-bindgen-macro-support"
|
|
1918
|
-
version = "0.2.
|
|
1918
|
+
version = "0.2.122"
|
|
1919
1919
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1920
|
-
checksum = "
|
|
1920
|
+
checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
|
|
1921
1921
|
dependencies = [
|
|
1922
1922
|
"bumpalo",
|
|
1923
1923
|
"proc-macro2",
|
|
@@ -1928,9 +1928,9 @@ dependencies = [
|
|
|
1928
1928
|
|
|
1929
1929
|
[[package]]
|
|
1930
1930
|
name = "wasm-bindgen-shared"
|
|
1931
|
-
version = "0.2.
|
|
1931
|
+
version = "0.2.122"
|
|
1932
1932
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1933
|
-
checksum = "
|
|
1933
|
+
checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
|
|
1934
1934
|
dependencies = [
|
|
1935
1935
|
"unicode-ident",
|
|
1936
1936
|
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Typed WHERE filters with None panic or wrong SQL (IS NULL)
|
|
3
|
+
type: issue
|
|
4
|
+
tags: [gotcha, query, filter, is-null, bridge, ffi, rust, pyo3, sea-query, serde]
|
|
5
|
+
related_files:
|
|
6
|
+
- src/query.rs
|
|
7
|
+
- src/ferro/query/nodes.py
|
|
8
|
+
- tests/test_typed_null_binds.py
|
|
9
|
+
- docs/solutions/patterns/typed-null-binds.md
|
|
10
|
+
related_issues: [41, 61]
|
|
11
|
+
related_prs: [62]
|
|
12
|
+
captured: 2026-05-20
|
|
13
|
+
last_updated: 2026-05-20
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Problem
|
|
17
|
+
|
|
18
|
+
`Model.where(lambda t: t.col == None)` or `Model.col == None` / `!= None` used to
|
|
19
|
+
panic in Rust (`node_to_condition_for_backend`) or, if it did not panic, would
|
|
20
|
+
compile to `col = NULL`, which never matches rows in SQL.
|
|
21
|
+
|
|
22
|
+
## Takeaway
|
|
23
|
+
|
|
24
|
+
Python `None` on the filter RHS becomes JSON `"value": null`, which serde
|
|
25
|
+
deserializes as `Option<serde_json::Value>::None` — not `Some(Value::Null)`.
|
|
26
|
+
For `==` / `!=`, emit `IS NULL` / `IS NOT NULL` in `node_to_condition_for_backend`;
|
|
27
|
+
never `unwrap()` `node.value` before checking for a null RHS. Bind typing for
|
|
28
|
+
other operators stays in the typed-null bind pipeline — see
|
|
29
|
+
`docs/solutions/patterns/typed-null-binds.md`.
|
|
30
|
+
|
|
31
|
+
## Explanation
|
|
32
|
+
|
|
33
|
+
**Causal chain**
|
|
34
|
+
|
|
35
|
+
1. `QueryNode.to_dict()` serializes Python `None` as JSON `null` on `value`.
|
|
36
|
+
2. Rust `QueryNode { value: Option<serde_json::Value> }` deserializes absent/null
|
|
37
|
+
as `None`, not `Some(Null)`.
|
|
38
|
+
3. Pre-fix code: `let val = node.value.as_ref().unwrap();` → panic across FFI
|
|
39
|
+
(AGENTS.md I-3).
|
|
40
|
+
4. Even without panic, `col.eq(bind_null)` yields `col = NULL`, which is always
|
|
41
|
+
unknown/false in three-valued logic — filters return no rows.
|
|
42
|
+
|
|
43
|
+
**Fix (PR #62, issue #41)**
|
|
44
|
+
|
|
45
|
+
```rust
|
|
46
|
+
let rhs_is_json_null = node.value.as_ref().map_or(true, serde_json::Value::is_null);
|
|
47
|
+
|
|
48
|
+
let expr: SimpleExpr = if rhs_is_json_null {
|
|
49
|
+
match node.operator.as_str() {
|
|
50
|
+
"==" => col.is_null(),
|
|
51
|
+
"!=" => col.is_not_null(),
|
|
52
|
+
// other ops: value_rhs_simple_expr_for_backend(..., &Value::Null, ...)
|
|
53
|
+
...
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
let val = node.value.as_ref().unwrap();
|
|
57
|
+
// existing non-null paths
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Discovery before fix (session history)**
|
|
62
|
+
|
|
63
|
+
During the April `refactor/typed-null-binds` work, the same wire shape was
|
|
64
|
+
identified and GitHub #41 was filed; `test_filter_by_none_does_not_reproduce_38`
|
|
65
|
+
stayed `xfail(strict=True)` until this fix landed on branch
|
|
66
|
+
`cursor/fix-typed-predicate-null-is-null-f3a4`.
|
|
67
|
+
|
|
68
|
+
**Tests**
|
|
69
|
+
|
|
70
|
+
| Layer | File | What it pins |
|
|
71
|
+
|-------|------|----------------|
|
|
72
|
+
| Integration | `tests/test_typed_null_binds.py` | `test_filter_by_none_does_not_reproduce_38` — FieldProxy `== None` / `!= None` |
|
|
73
|
+
| Integration | `tests/test_typed_null_binds.py` | `test_lambda_predicate_null_filter_datetime_and_json` — QueryProxy lambda on nullable datetime + JSON |
|
|
74
|
+
| Rust unit | `src/query.rs` | `json_null_deserializes_to_option_none_for_query_node_value` |
|
|
75
|
+
| Rust unit | `src/query.rs` | `where_rhs_none_emits_is_null_for_eq_sqlite` / `where_rhs_none_emits_is_not_null_for_ne_sqlite` |
|
|
76
|
+
|
|
77
|
+
## How to recognize
|
|
78
|
+
|
|
79
|
+
- Crash or opaque unwind when filtering with `== None` / `!= None` on any column
|
|
80
|
+
type (including `datetime | None` and `list | None` / JSON).
|
|
81
|
+
- Grep finds `node.value.as_ref().unwrap()` in `node_to_condition_for_backend`
|
|
82
|
+
without a prior null-RHS guard.
|
|
83
|
+
- Generated SQL contains `= null` instead of `is null` for None filters.
|
|
84
|
+
- Distinct from issue #38 (typed **bind** `null::text` on INSERT) and #56 (SQLite
|
|
85
|
+
**fetch** NULL → `int(0)`); this is the **WHERE compile** path in `src/query.rs`.
|
|
86
|
+
|
|
87
|
+
## Prevention
|
|
88
|
+
|
|
89
|
+
- Treat JSON `null` on optional Rust fields as “SQL null intent,” not as “missing
|
|
90
|
+
field” — use `map_or(true, Value::is_null)` (or equivalent) before `unwrap`.
|
|
91
|
+
- For every filter API that accepts Python `None`, add both FieldProxy and
|
|
92
|
+
QueryProxy lambda integration tests plus a Rust unit test on deserialized
|
|
93
|
+
`{"value": null}`.
|
|
94
|
+
- Keep the invariant in `typed-null-binds.md` § “IS NULL for typed `== None`”
|
|
95
|
+
when adding new schema-driven emitters.
|
|
96
|
+
|
|
97
|
+
## Related
|
|
98
|
+
|
|
99
|
+
- Pattern: `docs/solutions/patterns/typed-null-binds.md` (bind layer + IS NULL rule)
|
|
100
|
+
- Issue [#41]: https://github.com/syn54x/ferro-orm/issues/41
|
|
101
|
+
- Issue [#61]: https://github.com/syn54x/ferro-orm/issues/61 (duplicate report)
|
|
102
|
+
- PR [#62]: https://github.com/syn54x/ferro-orm/pull/62
|
|
103
|
+
- Issue [#38]: typed-null binds on Postgres (orthogonal root cause)
|
|
104
|
+
|
|
105
|
+
[#38]: https://github.com/syn54x/ferro-orm/issues/38
|
|
106
|
+
[#41]: https://github.com/syn54x/ferro-orm/issues/41
|
|
107
|
+
[#61]: https://github.com/syn54x/ferro-orm/issues/61
|
|
@@ -8,9 +8,10 @@ related_files:
|
|
|
8
8
|
- src/query.rs
|
|
9
9
|
- tests/test_typed_null_binds.py
|
|
10
10
|
- tests/test_sqlite_alembic_reconnect_hydration.py
|
|
11
|
-
related_issues: [38, 40, 56]
|
|
12
|
-
related_prs: []
|
|
11
|
+
related_issues: [38, 40, 41, 56]
|
|
12
|
+
related_prs: [62]
|
|
13
13
|
captured: 2026-04-29
|
|
14
|
+
last_updated: 2026-05-20
|
|
14
15
|
---
|
|
15
16
|
|
|
16
17
|
## Problem
|
|
@@ -72,6 +73,11 @@ digraph typed_null_flow {
|
|
|
72
73
|
| Filter null pick | `typed_null_for_column` (called by ^) | `src/query.rs` |
|
|
73
74
|
| M2M target IDs | `backend_column_value_expr` | `src/operations.rs`|
|
|
74
75
|
|
|
76
|
+
**`IS NULL` for typed `== None`:** JSON `null` on `QueryNode::value` deserializes
|
|
77
|
+
as `Option<serde_json::Value>::None` (serde), not `Some(Value::Null)`.
|
|
78
|
+
`node_to_condition_for_backend` maps `==` / `!=` with a null RHS to
|
|
79
|
+
`IS NULL` / `IS NOT NULL` — never `= NULL`, which is never true in SQL.
|
|
80
|
+
|
|
75
81
|
These functions inspect column metadata (JSON type, format, `uuid_columns`
|
|
76
82
|
introspection, `ts_cast` metadata) and emit one of the typed SeaQuery `None`
|
|
77
83
|
variants:
|
|
@@ -193,7 +199,12 @@ Ferro itself.
|
|
|
193
199
|
NULL → `int(0)` on SQLite (issue [#56]).
|
|
194
200
|
- Issue [#38]: the original bug report.
|
|
195
201
|
- Issue [#40]: temporal typed binds (deferred follow-up).
|
|
202
|
+
- Issue [#41]: filter `== None` panic / `IS NULL` compile path — debugging story in
|
|
203
|
+
`docs/solutions/issues/typed-where-null-panics-is-null.md`.
|
|
204
|
+
- PR [#62]: `fix(query): typed predicates col == None → IS NULL / IS NOT NULL`.
|
|
196
205
|
|
|
197
206
|
[#38]: https://github.com/syn54x/ferro-orm/issues/38
|
|
198
207
|
[#40]: https://github.com/syn54x/ferro-orm/issues/40
|
|
208
|
+
[#41]: https://github.com/syn54x/ferro-orm/issues/41
|
|
199
209
|
[#56]: https://github.com/syn54x/ferro-orm/issues/56
|
|
210
|
+
[#62]: https://github.com/syn54x/ferro-orm/pull/62
|
|
@@ -560,30 +560,6 @@ async fn postgres_temporal_cast_by_column(
|
|
|
560
560
|
Ok(out)
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
-
fn postgres_enum_type_name_for_column(
|
|
564
|
-
col_name: &str,
|
|
565
|
-
enum_udt: &HashMap<String, String>,
|
|
566
|
-
col_info: Option<&serde_json::Value>,
|
|
567
|
-
) -> Option<String> {
|
|
568
|
-
// db_type takes precedence over the JSON-schema enum_type_name: when the
|
|
569
|
-
// user has asked for `text` / `varchar(N)` / etc. storage, the column is
|
|
570
|
-
// no longer a native Postgres enum UDT and we must not CAST values to a
|
|
571
|
-
// non-existent type. This mirrors the Alembic-side _map_to_sa_type
|
|
572
|
-
// override. See AGENTS.md § I-1.
|
|
573
|
-
if let Some(info) = col_info
|
|
574
|
-
&& info.get("db_type").and_then(|v| v.as_str()).is_some()
|
|
575
|
-
{
|
|
576
|
-
return None;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
enum_udt.get(col_name).cloned().or_else(|| {
|
|
580
|
-
col_info?
|
|
581
|
-
.get("enum_type_name")?
|
|
582
|
-
.as_str()
|
|
583
|
-
.map(std::string::ToString::to_string)
|
|
584
|
-
})
|
|
585
|
-
}
|
|
586
|
-
|
|
587
563
|
fn schema_property<'a>(
|
|
588
564
|
schema: &'a serde_json::Value,
|
|
589
565
|
col_name: &str,
|
|
@@ -630,12 +606,10 @@ fn schema_value_expr(
|
|
|
630
606
|
|
|
631
607
|
if let serde_json::Value::String(s) = value
|
|
632
608
|
&& backend == SqlDialect::Postgres
|
|
633
|
-
&& let Some(tn) =
|
|
609
|
+
&& let Some(tn) =
|
|
610
|
+
crate::schema_bind::postgres_enum_type_name_for_column(col_name, enum_udt, col_info)
|
|
634
611
|
{
|
|
635
|
-
return Ok(
|
|
636
|
-
Expr::value(sea_query::Value::String(Some(Box::new(s.clone()))))
|
|
637
|
-
.cast_as(Alias::new(tn.as_str())),
|
|
638
|
-
);
|
|
612
|
+
return Ok(crate::schema_bind::postgres_enum_string_rhs_expr(s, &tn));
|
|
639
613
|
}
|
|
640
614
|
|
|
641
615
|
if is_uuid_pg {
|
|
@@ -1572,7 +1546,7 @@ pub fn fetch_filtered<'py>(
|
|
|
1572
1546
|
let name = cls.getattr("__name__")?.extract::<String>()?;
|
|
1573
1547
|
let cls_py = cls.unbind();
|
|
1574
1548
|
|
|
1575
|
-
let query_def: QueryDef = serde_json::from_str(&query_json).map_err(|e| {
|
|
1549
|
+
let mut query_def: QueryDef = serde_json::from_str(&query_json).map_err(|e| {
|
|
1576
1550
|
pyo3::exceptions::PyValueError::new_err(format!("Invalid query JSON: {}", e))
|
|
1577
1551
|
})?;
|
|
1578
1552
|
|
|
@@ -1582,10 +1556,10 @@ pub fn fetch_filtered<'py>(
|
|
|
1582
1556
|
let use_identity_map = engine.is_identity_map_enabled();
|
|
1583
1557
|
|
|
1584
1558
|
let table_name = name.to_lowercase();
|
|
1585
|
-
let
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1559
|
+
let postgres_enum_udt =
|
|
1560
|
+
postgres_enum_udt_by_column(&table_name, &engine, &tx_conn, backend).await?;
|
|
1561
|
+
query_def.postgres_enum_udt = postgres_enum_udt.clone();
|
|
1562
|
+
let pg_native_enum_cols: HashSet<String> = postgres_enum_udt.keys().cloned().collect();
|
|
1589
1563
|
// ...
|
|
1590
1564
|
let (sql, bind_values, pk_col, schema_for_decode) = {
|
|
1591
1565
|
let registry = MODEL_REGISTRY.read().map_err(|_| {
|
|
@@ -1762,7 +1736,7 @@ pub fn count_filtered(
|
|
|
1762
1736
|
tx_id: Option<String>,
|
|
1763
1737
|
using: Option<String>,
|
|
1764
1738
|
) -> PyResult<Bound<'_, PyAny>> {
|
|
1765
|
-
let query_def: QueryDef = serde_json::from_str(&query_json).map_err(|e| {
|
|
1739
|
+
let mut query_def: QueryDef = serde_json::from_str(&query_json).map_err(|e| {
|
|
1766
1740
|
pyo3::exceptions::PyValueError::new_err(format!("Invalid query JSON: {}", e))
|
|
1767
1741
|
})?;
|
|
1768
1742
|
|
|
@@ -1770,6 +1744,8 @@ pub fn count_filtered(
|
|
|
1770
1744
|
let (_, engine, tx_conn, backend) = active_route_for_operation(tx_id, using)?;
|
|
1771
1745
|
|
|
1772
1746
|
let table_name = name.to_lowercase();
|
|
1747
|
+
query_def.postgres_enum_udt =
|
|
1748
|
+
postgres_enum_udt_by_column(&table_name, &engine, &tx_conn, backend).await?;
|
|
1773
1749
|
// ... sql ...
|
|
1774
1750
|
let (sql, bind_values) = {
|
|
1775
1751
|
let mut select = Query::select();
|
|
@@ -1964,7 +1940,7 @@ pub fn delete_filtered(
|
|
|
1964
1940
|
tx_id: Option<String>,
|
|
1965
1941
|
using: Option<String>,
|
|
1966
1942
|
) -> PyResult<Bound<'_, PyAny>> {
|
|
1967
|
-
let query_def: QueryDef = serde_json::from_str(&query_json).map_err(|e| {
|
|
1943
|
+
let mut query_def: QueryDef = serde_json::from_str(&query_json).map_err(|e| {
|
|
1968
1944
|
pyo3::exceptions::PyValueError::new_err(format!("Invalid query JSON: {}", e))
|
|
1969
1945
|
})?;
|
|
1970
1946
|
|
|
@@ -1972,6 +1948,8 @@ pub fn delete_filtered(
|
|
|
1972
1948
|
let (_, engine, tx_conn, backend) = active_route_for_operation(tx_id, using)?;
|
|
1973
1949
|
|
|
1974
1950
|
let table_name = name.to_lowercase();
|
|
1951
|
+
query_def.postgres_enum_udt =
|
|
1952
|
+
postgres_enum_udt_by_column(&table_name, &engine, &tx_conn, backend).await?;
|
|
1975
1953
|
// ... sql ...
|
|
1976
1954
|
let (sql, bind_values) = {
|
|
1977
1955
|
let mut delete = Query::delete();
|
|
@@ -2008,7 +1986,7 @@ pub fn update_filtered(
|
|
|
2008
1986
|
tx_id: Option<String>,
|
|
2009
1987
|
using: Option<String>,
|
|
2010
1988
|
) -> PyResult<Bound<'_, PyAny>> {
|
|
2011
|
-
let query_def: QueryDef = serde_json::from_str(&query_json).map_err(|e| {
|
|
1989
|
+
let mut query_def: QueryDef = serde_json::from_str(&query_json).map_err(|e| {
|
|
2012
1990
|
pyo3::exceptions::PyValueError::new_err(format!("Invalid query JSON: {}", e))
|
|
2013
1991
|
})?;
|
|
2014
1992
|
|
|
@@ -2025,6 +2003,7 @@ pub fn update_filtered(
|
|
|
2025
2003
|
|
|
2026
2004
|
let table_name = name.to_lowercase();
|
|
2027
2005
|
let enum_udt = postgres_enum_udt_by_column(&table_name, &engine, &tx_conn, backend).await?;
|
|
2006
|
+
query_def.postgres_enum_udt = enum_udt.clone();
|
|
2028
2007
|
let uuid_columns =
|
|
2029
2008
|
postgres_uuid_column_names(&table_name, &engine, &tx_conn, backend).await?;
|
|
2030
2009
|
let ts_cast =
|