ferro-orm 0.10.3__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.
Files changed (170) hide show
  1. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/CHANGELOG.md +9 -0
  2. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/Cargo.lock +17 -17
  3. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/Cargo.toml +1 -1
  4. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/PKG-INFO +1 -1
  5. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/pyproject.toml +1 -1
  6. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/lib.rs +1 -0
  7. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/operations.rs +16 -37
  8. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/query.rs +62 -0
  9. ferro_orm-0.10.4/src/schema_bind.rs +49 -0
  10. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_structural_types.py +39 -0
  11. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/PERMISSIONS.md +0 -0
  14. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/PYPI_CHECKLIST.md +0 -0
  15. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/PYPI_SETUP.md +0 -0
  16. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/generated/wheels.generated.yml +0 -0
  17. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/pull_request_template.md +0 -0
  18. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/workflows/ci.yml +0 -0
  19. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/workflows/packaging-smoke.yml +0 -0
  20. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/workflows/publish-docs.yml +0 -0
  21. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/workflows/publish.yml +0 -0
  22. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.github/workflows/release.yml +0 -0
  23. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.gitignore +0 -0
  24. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.pre-commit-config.yaml +0 -0
  25. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/.python-version +0 -0
  26. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/AGENTS.md +0 -0
  27. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/CONTRIBUTING.md +0 -0
  28. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/LICENSE +0 -0
  29. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/README.md +0 -0
  30. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/TEST_RESULTS.md +0 -0
  31. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/api/exceptions.md +0 -0
  32. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/api/fields.md +0 -0
  33. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/api/model.md +0 -0
  34. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/api/query.md +0 -0
  35. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/api/raw-sql.md +0 -0
  36. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/api/relationships.md +0 -0
  37. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/api/transactions.md +0 -0
  38. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/api/utilities.md +0 -0
  39. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
  40. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
  41. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
  42. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
  43. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/changelog.md +0 -0
  44. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/coming-soon.md +0 -0
  45. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/concepts/architecture.md +0 -0
  46. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/concepts/identity-map.md +0 -0
  47. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/concepts/performance.md +0 -0
  48. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/concepts/query-typing.md +0 -0
  49. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/concepts/type-safety.md +0 -0
  50. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/contributing.md +0 -0
  51. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/faq.md +0 -0
  52. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/getting-started/installation.md +0 -0
  53. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/getting-started/next-steps.md +0 -0
  54. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/getting-started/tutorial.md +0 -0
  55. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/guide/backend.md +0 -0
  56. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/guide/database.md +0 -0
  57. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/guide/migrations.md +0 -0
  58. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/guide/models-and-fields.md +0 -0
  59. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/guide/mutations.md +0 -0
  60. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/guide/queries.md +0 -0
  61. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/guide/relationships.md +0 -0
  62. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/guide/transactions.md +0 -0
  63. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/howto/multiple-databases.md +0 -0
  64. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/howto/pagination.md +0 -0
  65. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/howto/soft-deletes.md +0 -0
  66. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/howto/testing.md +0 -0
  67. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/howto/timestamps.md +0 -0
  68. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/index.md +0 -0
  69. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/migration-sqlalchemy.md +0 -0
  70. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
  71. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
  72. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
  73. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
  74. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
  75. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
  76. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/README.md +0 -0
  77. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
  78. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
  79. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
  80. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
  81. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +0 -0
  82. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/issues/typed-where-null-panics-is-null.md +0 -0
  83. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
  84. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
  85. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/patterns/foreign-key-index.md +0 -0
  86. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
  87. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
  88. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/solutions/patterns/typed-null-binds.md +0 -0
  89. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/stylesheets/extra.css +0 -0
  90. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/docs/why-ferro.md +0 -0
  91. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/justfile +0 -0
  92. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/mkdocs.yml +0 -0
  93. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/scripts/demo_queries.py +0 -0
  94. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/backend.rs +0 -0
  95. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/connection.rs +0 -0
  96. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/__init__.py +0 -0
  97. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/_annotation_utils.py +0 -0
  98. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/_core.pyi +0 -0
  99. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/_shadow_fk_types.py +0 -0
  100. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/base.py +0 -0
  101. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/composite_indexes.py +0 -0
  102. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/composite_uniques.py +0 -0
  103. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/exceptions.py +0 -0
  104. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/fields.py +0 -0
  105. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/metaclass.py +0 -0
  106. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/migrations/__init__.py +0 -0
  107. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/migrations/alembic.py +0 -0
  108. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/models.py +0 -0
  109. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/py.typed +0 -0
  110. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/query/__init__.py +0 -0
  111. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/query/builder.py +0 -0
  112. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/query/nodes.py +0 -0
  113. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/raw.py +0 -0
  114. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/relations/__init__.py +0 -0
  115. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/relations/descriptors.py +0 -0
  116. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/schema_metadata.py +0 -0
  117. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/ferro/state.py +0 -0
  118. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/schema.rs +0 -0
  119. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/src/state.rs +0 -0
  120. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/__init__.py +0 -0
  121. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/conftest.py +0 -0
  122. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/db_backends.py +0 -0
  123. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_aggregation.py +0 -0
  124. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_alembic_autogenerate.py +0 -0
  125. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_alembic_bridge.py +0 -0
  126. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_alembic_db_type.py +0 -0
  127. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_alembic_nullability.py +0 -0
  128. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_alembic_type_mapping.py +0 -0
  129. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_auto_migrate.py +0 -0
  130. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_bulk_update.py +0 -0
  131. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_composite_index.py +0 -0
  132. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_composite_unique.py +0 -0
  133. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_connection.py +0 -0
  134. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_connection_redaction.py +0 -0
  135. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_constraints.py +0 -0
  136. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_cross_emitter_parity.py +0 -0
  137. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_crud.py +0 -0
  138. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_db_backends.py +0 -0
  139. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_db_type_cross_emitter_parity.py +0 -0
  140. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_db_type_integration.py +0 -0
  141. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_db_type_typing.py +0 -0
  142. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_db_type_validation.py +0 -0
  143. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_deletion.py +0 -0
  144. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_docs_examples.py +0 -0
  145. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_documentation_features.py +0 -0
  146. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_field_wrapper.py +0 -0
  147. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_helpers.py +0 -0
  148. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_hydration.py +0 -0
  149. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_metaclass_internals.py +0 -0
  150. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_metadata.py +0 -0
  151. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_models.py +0 -0
  152. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_named_connections_integration.py +0 -0
  153. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_one_to_one.py +0 -0
  154. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_query_builder.py +0 -0
  155. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_query_typing.py +0 -0
  156. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_raw_sql.py +0 -0
  157. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_refresh.py +0 -0
  158. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_relationship_engine.py +0 -0
  159. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_schema.py +0 -0
  160. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_schema_constraints.py +0 -0
  161. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_schema_db_type_metadata.py +0 -0
  162. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_schema_enum_annotations.py +0 -0
  163. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_shadow_fk_types.py +0 -0
  164. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_sqlite_alembic_reconnect_hydration.py +0 -0
  165. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_static_contracts.py +0 -0
  166. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_string_search.py +0 -0
  167. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_temporal_types.py +0 -0
  168. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_transactions.py +0 -0
  169. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/tests/test_typed_null_binds.py +0 -0
  170. {ferro_orm-0.10.3 → ferro_orm-0.10.4}/uv.lock +0 -0
@@ -1,6 +1,15 @@
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
+
4
13
  ## v0.10.3 (2026-05-21)
5
14
 
6
15
  ### Bug Fixes
@@ -25,9 +25,9 @@ dependencies = [
25
25
 
26
26
  [[package]]
27
27
  name = "autocfg"
28
- version = "1.5.0"
28
+ version = "1.5.1"
29
29
  source = "registry+https://github.com/rust-lang/crates.io-index"
30
- checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
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.2"
64
+ version = "3.20.3"
65
65
  source = "registry+https://github.com/rust-lang/crates.io-index"
66
- checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
66
+ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
67
67
 
68
68
  [[package]]
69
69
  name = "byteorder"
@@ -294,7 +294,7 @@ dependencies = [
294
294
 
295
295
  [[package]]
296
296
  name = "ferro"
297
- version = "0.10.3"
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.98"
714
+ version = "0.3.99"
715
715
  source = "registry+https://github.com/rust-lang/crates.io-index"
716
- checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
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.149"
1295
+ version = "1.0.150"
1296
1296
  source = "registry+https://github.com/rust-lang/crates.io-index"
1297
- checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
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.121"
1895
+ version = "0.2.122"
1896
1896
  source = "registry+https://github.com/rust-lang/crates.io-index"
1897
- checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
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.121"
1908
+ version = "0.2.122"
1909
1909
  source = "registry+https://github.com/rust-lang/crates.io-index"
1910
- checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
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.121"
1918
+ version = "0.2.122"
1919
1919
  source = "registry+https://github.com/rust-lang/crates.io-index"
1920
- checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
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.121"
1931
+ version = "0.2.122"
1932
1932
  source = "registry+https://github.com/rust-lang/crates.io-index"
1933
- checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
1933
+ checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
1934
1934
  dependencies = [
1935
1935
  "unicode-ident",
1936
1936
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ferro"
3
- version = "0.10.3"
3
+ version = "0.10.4"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ferro-orm
3
- Version: 0.10.3
3
+ Version: 0.10.4
4
4
  Requires-Dist: pydantic>=2.0
5
5
  Requires-Dist: alembic>=1.18.1 ; extra == 'alembic'
6
6
  Requires-Dist: sqlalchemy>=2.0.46 ; extra == 'alembic'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ferro-orm"
3
- version = "0.10.3"
3
+ version = "0.10.4"
4
4
  description = "A high-performance, Rust-backed ORM for Python."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -9,6 +9,7 @@ mod connection;
9
9
  mod operations;
10
10
  mod query;
11
11
  mod schema;
12
+ mod schema_bind;
12
13
  mod state;
13
14
 
14
15
  use crate::state::MODEL_REGISTRY;
@@ -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) = postgres_enum_type_name_for_column(col_name, enum_udt, col_info)
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 pg_native_enum_cols: HashSet<String> = {
1586
- let m = postgres_enum_udt_by_column(&table_name, &engine, &tx_conn, backend).await?;
1587
- m.keys().cloned().collect()
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 =
@@ -2,6 +2,7 @@ use crate::state::{MODEL_REGISTRY, SqlDialect};
2
2
  use sea_query::{Alias, Condition, Expr, SimpleExpr};
3
3
  use serde::Deserialize;
4
4
  use serde_json::Value;
5
+ use std::collections::HashMap;
5
6
 
6
7
  #[derive(Debug, Deserialize)]
7
8
  pub struct QueryNode {
@@ -38,6 +39,10 @@ pub struct QueryDef {
38
39
  pub limit: Option<u64>,
39
40
  pub offset: Option<u64>,
40
41
  pub m2m: Option<M2mContext>,
42
+ /// Populated from `pg_catalog` before building filter SQL. Not part of the
43
+ /// Python query JSON payload.
44
+ #[serde(skip)]
45
+ pub postgres_enum_udt: HashMap<String, String>,
41
46
  }
42
47
 
43
48
  impl QueryDef {
@@ -202,6 +207,12 @@ impl QueryDef {
202
207
  ) -> SimpleExpr {
203
208
  if let Value::String(s) = val {
204
209
  if backend == SqlDialect::Postgres {
210
+ if let Some(tn) =
211
+ crate::schema_bind::native_postgres_enum_udt_name(col_name, &self.postgres_enum_udt)
212
+ {
213
+ return crate::schema_bind::postgres_enum_string_rhs_expr(s, tn);
214
+ }
215
+
205
216
  if let Ok(parsed) = uuid::Uuid::parse_str(s) {
206
217
  let schema_uuid = model_column_is_uuid(&self.model_name, col_name);
207
218
  if schema_uuid || infer_uuid_without_schema {
@@ -430,6 +441,7 @@ mod tests {
430
441
  use crate::backend::BackendKind;
431
442
  use sea_query::{Alias, PostgresQueryBuilder, Query, SqliteQueryBuilder, Value as SeaValue};
432
443
  use serde_json::json;
444
+ use std::collections::HashMap;
433
445
 
434
446
  fn empty_query_def(model_name: &str) -> QueryDef {
435
447
  QueryDef {
@@ -439,6 +451,7 @@ mod tests {
439
451
  limit: None,
440
452
  offset: None,
441
453
  m2m: None,
454
+ postgres_enum_udt: HashMap::new(),
442
455
  }
443
456
  }
444
457
 
@@ -479,6 +492,7 @@ mod tests {
479
492
  limit: None,
480
493
  offset: None,
481
494
  m2m: None,
495
+ postgres_enum_udt: HashMap::new(),
482
496
  };
483
497
  let mut select = Query::select();
484
498
  select
@@ -508,6 +522,7 @@ mod tests {
508
522
  limit: None,
509
523
  offset: None,
510
524
  m2m: None,
525
+ postgres_enum_udt: HashMap::new(),
511
526
  };
512
527
  let mut select = Query::select();
513
528
  select
@@ -675,6 +690,53 @@ mod tests {
675
690
  }
676
691
  }
677
692
 
693
+ #[test]
694
+ fn enum_rhs_emits_cast_to_schema_enum_type_on_postgres() {
695
+ let mut query_def = empty_query_def("WidgetColor");
696
+ query_def
697
+ .postgres_enum_udt
698
+ .insert("color".to_string(), "color".to_string());
699
+
700
+ let rhs = query_def.value_rhs_simple_expr_for_backend(
701
+ "color",
702
+ &json!("red"),
703
+ false,
704
+ BackendKind::Postgres,
705
+ );
706
+ let sql = Query::select().expr(rhs).to_string(PostgresQueryBuilder);
707
+
708
+ assert!(
709
+ sql.to_lowercase().contains("as \"color\"") || sql.to_lowercase().contains("as color"),
710
+ "enum filter rhs should CAST to the UDT name, got: {sql}"
711
+ );
712
+ }
713
+
714
+ #[test]
715
+ fn enum_rhs_skips_cast_without_native_enum_column() {
716
+ crate::state::MODEL_REGISTRY.write().unwrap().insert(
717
+ "WidgetTextColor".to_string(),
718
+ json!({
719
+ "properties": {
720
+ "color": {"enum_type_name": "color", "db_type": "text"}
721
+ }
722
+ }),
723
+ );
724
+ let query_def = empty_query_def("WidgetTextColor");
725
+
726
+ let rhs = query_def.value_rhs_simple_expr_for_backend(
727
+ "color",
728
+ &json!("red"),
729
+ false,
730
+ BackendKind::Postgres,
731
+ );
732
+ let sql = Query::select().expr(rhs).to_string(PostgresQueryBuilder);
733
+
734
+ assert!(
735
+ !sql.to_lowercase().contains("as \"color\"") && !sql.to_lowercase().contains("as color"),
736
+ "auto-migrate TEXT enum columns must not cast without catalog UDT: {sql}"
737
+ );
738
+ }
739
+
678
740
  #[test]
679
741
  fn decimal_rhs_keeps_numeric_cast_for_now() {
680
742
  // Native numeric typed binds are deferred (plan §3 Scope Boundaries);
@@ -0,0 +1,49 @@
1
+ //! Shared schema-driven bind helpers for INSERT/UPDATE and query-filter paths.
2
+
3
+ use sea_query::{Alias, Expr, SimpleExpr};
4
+ use std::collections::HashMap;
5
+
6
+ /// Native Postgres enum UDT for `col_name`, from catalog introspection only.
7
+ ///
8
+ /// Query-filter predicates use this path so auto-migrate TEXT columns (which
9
+ /// carry `enum_type_name` in schema but are not `typtype = 'e'`) keep plain
10
+ /// text binds. INSERT/UPDATE may still fall back to schema metadata via
11
+ /// [`postgres_enum_type_name_for_column`].
12
+ pub(crate) fn native_postgres_enum_udt_name<'a>(
13
+ col_name: &str,
14
+ enum_udt: &'a HashMap<String, String>,
15
+ ) -> Option<&'a str> {
16
+ enum_udt.get(col_name).map(|s| s.as_str())
17
+ }
18
+
19
+ /// Resolve the Postgres enum UDT name for a column when binding a string RHS.
20
+ ///
21
+ /// `enum_udt` comes from catalog introspection (INSERT/UPDATE). `col_info` is
22
+ /// the model field schema fragment (`enum_type_name`, `db_type`, etc.).
23
+ ///
24
+ /// When `db_type` is set, native enum casting is suppressed — the column is no
25
+ /// longer stored as a Postgres enum UDT (see AGENTS.md I-1 / Alembic parity).
26
+ pub(crate) fn postgres_enum_type_name_for_column(
27
+ col_name: &str,
28
+ enum_udt: &HashMap<String, String>,
29
+ col_info: Option<&serde_json::Value>,
30
+ ) -> Option<String> {
31
+ if let Some(info) = col_info
32
+ && info.get("db_type").and_then(|v| v.as_str()).is_some()
33
+ {
34
+ return None;
35
+ }
36
+
37
+ enum_udt.get(col_name).cloned().or_else(|| {
38
+ col_info?
39
+ .get("enum_type_name")?
40
+ .as_str()
41
+ .map(std::string::ToString::to_string)
42
+ })
43
+ }
44
+
45
+ /// RHS expression for a non-null string compared against a native Postgres enum column.
46
+ pub(crate) fn postgres_enum_string_rhs_expr(s: &str, enum_type_name: &str) -> SimpleExpr {
47
+ Expr::value(sea_query::Value::String(Some(Box::new(s.to_string()))))
48
+ .cast_as(Alias::new(enum_type_name))
49
+ }
@@ -276,6 +276,45 @@ async def test_native_postgres_enum_column_decodes_via_text_cast(
276
276
  assert fetched.format == TranscriptFormat.PDF
277
277
 
278
278
 
279
+ @pytest.mark.asyncio
280
+ @pytest.mark.postgres_only
281
+ async def test_native_postgres_enum_where_lambda_count(db_url, postgres_base_url, db_schema_name):
282
+ """Regression for #63: filter predicates must cast enum RHS to the PG UDT."""
283
+
284
+ class Color(str, Enum):
285
+ RED = "red"
286
+ BLUE = "blue"
287
+
288
+ class Widget(Model):
289
+ id: Annotated[int | None, FerroField(primary_key=True)] = None
290
+ color: Color
291
+
292
+ import psycopg
293
+
294
+ with psycopg.connect(postgres_base_url) as conn:
295
+ conn.execute(f'SET search_path TO "{db_schema_name}"')
296
+ conn.execute("CREATE TYPE color AS ENUM ('red', 'blue')")
297
+ conn.execute(
298
+ """
299
+ CREATE TABLE widget (
300
+ id integer PRIMARY KEY,
301
+ color color NOT NULL
302
+ )
303
+ """
304
+ )
305
+ conn.execute("INSERT INTO widget (id, color) VALUES (1, 'red'), (2, 'blue')")
306
+ conn.commit()
307
+
308
+ await connect(db_url, auto_migrate=False)
309
+
310
+ count = await Widget.where(lambda w, c=Color.RED: w.color == c).count()
311
+ assert count == 1
312
+
313
+ rows = await Widget.where(lambda w: w.color != Color.BLUE).all()
314
+ assert len(rows) == 1
315
+ assert rows[0].color == Color.RED
316
+
317
+
279
318
  @pytest.mark.asyncio
280
319
  @pytest.mark.postgres_only
281
320
  async def test_native_postgres_enum_plain_str_column(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes