ferro-orm 0.10.1__tar.gz → 0.10.2__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.1 → ferro_orm-0.10.2}/CHANGELOG.md +9 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/Cargo.lock +1 -1
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/Cargo.toml +1 -1
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/PKG-INFO +1 -1
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md +2 -2
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/patterns/typed-null-binds.md +2 -2
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/pyproject.toml +1 -1
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/operations.rs +123 -0
- ferro_orm-0.10.2/tests/test_sqlite_alembic_reconnect_hydration.py +272 -0
- ferro_orm-0.10.1/tests/test_sqlite_null_hydration.py +0 -71
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/PERMISSIONS.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.github/workflows/release.yml +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.gitignore +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/.python-version +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/AGENTS.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/LICENSE +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/README.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/TEST_RESULTS.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/api/exceptions.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/api/fields.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/api/model.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/api/query.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/api/raw-sql.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/api/relationships.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/api/transactions.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/api/utilities.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/brainstorms/2026-05-13-configurable-column-storage-types-requirements.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/brainstorms/2026-05-14-autogenerate-support-for-db-type-requirements.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/changelog.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/coming-soon.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/concepts/architecture.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/concepts/identity-map.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/concepts/performance.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/concepts/query-typing.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/concepts/type-safety.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/contributing.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/faq.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/getting-started/installation.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/getting-started/next-steps.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/getting-started/tutorial.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/guide/backend.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/guide/database.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/guide/migrations.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/guide/models-and-fields.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/guide/mutations.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/guide/queries.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/guide/relationships.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/guide/transactions.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/howto/multiple-databases.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/howto/pagination.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/howto/soft-deletes.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/howto/testing.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/howto/timestamps.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/index.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/migration-sqlalchemy.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/plans/2026-05-13-001-feat-configurable-column-storage-types-plan.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/README.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/issues/pydantic-slots-missing-after-ferro-hydration.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/patterns/configurable-column-storage-types.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/patterns/foreign-key-index.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/stylesheets/extra.css +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/why-ferro.md +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/justfile +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/mkdocs.yml +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/backend.rs +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/connection.rs +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/__init__.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/_annotation_utils.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/_core.pyi +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/_shadow_fk_types.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/base.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/composite_indexes.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/composite_uniques.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/exceptions.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/fields.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/metaclass.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/migrations/alembic.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/models.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/py.typed +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/query/builder.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/query/nodes.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/raw.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/relations/__init__.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/relations/descriptors.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/schema_metadata.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/ferro/state.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/lib.rs +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/query.rs +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/schema.rs +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/src/state.rs +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/__init__.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/conftest.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/db_backends.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_alembic_autogenerate.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_alembic_bridge.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_alembic_db_type.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_alembic_nullability.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_alembic_type_mapping.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_auto_migrate.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_composite_index.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_composite_unique.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_connection.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_connection_redaction.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_constraints.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_cross_emitter_parity.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_crud.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_db_backends.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_db_type_cross_emitter_parity.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_db_type_integration.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_db_type_typing.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_db_type_validation.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_deletion.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_documentation_features.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_field_wrapper.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_helpers.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_hydration.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_metaclass_internals.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_metadata.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_models.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_named_connections_integration.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_one_to_one.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_query_builder.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_query_typing.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_raw_sql.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_refresh.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_relationship_engine.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_schema.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_schema_db_type_metadata.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_schema_enum_annotations.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_shadow_fk_types.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_static_contracts.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_string_search.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_structural_types.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_transactions.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/tests/test_typed_null_binds.py +0 -0
- {ferro_orm-0.10.1 → ferro_orm-0.10.2}/uv.lock +0 -0
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.10.2 (2026-05-19)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- Hydrate SQLite INTEGER-backed Decimal columns on reconnect
|
|
9
|
+
([#59](https://github.com/syn54x/ferro-orm/pull/59),
|
|
10
|
+
[`6f13906`](https://github.com/syn54x/ferro-orm/commit/6f13906850300bc9e85c1274763c49bc5b318b5d))
|
|
11
|
+
|
|
12
|
+
|
|
4
13
|
## v0.10.1 (2026-05-19)
|
|
5
14
|
|
|
6
15
|
### Bug Fixes
|
{ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/issues/sqlite-null-hydrates-as-int-zero.md
RENAMED
|
@@ -5,7 +5,7 @@ tags: [gotcha, sqlite, hydration, bridge, rust, sqlalchemy, alembic, datetime, f
|
|
|
5
5
|
related_files:
|
|
6
6
|
- src/backend.rs
|
|
7
7
|
- src/operations.rs
|
|
8
|
-
- tests/
|
|
8
|
+
- tests/test_sqlite_alembic_reconnect_hydration.py
|
|
9
9
|
related_issues: [56]
|
|
10
10
|
related_prs: [57]
|
|
11
11
|
captured: 2026-05-18
|
|
@@ -62,7 +62,7 @@ by treating `0` as missing.
|
|
|
62
62
|
|
|
63
63
|
- Rust: `engine_handle_fetches_sqlite_null_columns_as_null_not_zero`,
|
|
64
64
|
`engine_handle_fetches_sqlite_non_null_zero_integer` in `src/backend.rs`.
|
|
65
|
-
- Python: `tests/
|
|
65
|
+
- Python: `tests/test_sqlite_alembic_reconnect_hydration.py` (Alembic `create_all` + reconnect).
|
|
66
66
|
Requires `aiosqlite` and `greenlet` in `ci-test` / `dev` so CI does not skip.
|
|
67
67
|
|
|
68
68
|
## How to recognize
|
|
@@ -7,7 +7,7 @@ related_files:
|
|
|
7
7
|
- src/operations.rs
|
|
8
8
|
- src/query.rs
|
|
9
9
|
- tests/test_typed_null_binds.py
|
|
10
|
-
- tests/
|
|
10
|
+
- tests/test_sqlite_alembic_reconnect_hydration.py
|
|
11
11
|
related_issues: [38, 40, 56]
|
|
12
12
|
related_prs: []
|
|
13
13
|
captured: 2026-04-29
|
|
@@ -112,7 +112,7 @@ typed `try_get`. Non-null integer `0` is unchanged; only SQL `NULL` is affected.
|
|
|
112
112
|
| Fetch (read) | `materialize_engine_row` | `is_null()` before `decode_non_null_*`|
|
|
113
113
|
|
|
114
114
|
Rust regression: `engine_handle_fetches_sqlite_null_columns_as_null_not_zero`.
|
|
115
|
-
Integration: `tests/
|
|
115
|
+
Integration: `tests/test_sqlite_alembic_reconnect_hydration.py` (Alembic schema + reconnect).
|
|
116
116
|
|
|
117
117
|
## Raw-SQL boundary (explicit exception)
|
|
118
118
|
|
|
@@ -193,6 +193,7 @@ fn engine_value_to_rust_value(
|
|
|
193
193
|
|
|
194
194
|
if is_decimal {
|
|
195
195
|
return match value {
|
|
196
|
+
EngineValue::I64(v) => RustValue::Decimal(v.to_string()),
|
|
196
197
|
EngineValue::F64(v) => RustValue::Decimal(v.to_string()),
|
|
197
198
|
EngineValue::String(v) => RustValue::Decimal(v),
|
|
198
199
|
_ => RustValue::None,
|
|
@@ -3077,3 +3078,125 @@ mod raw_sql_tests {
|
|
|
3077
3078
|
});
|
|
3078
3079
|
}
|
|
3079
3080
|
}
|
|
3081
|
+
|
|
3082
|
+
#[cfg(test)]
|
|
3083
|
+
mod engine_value_to_rust_value_tests {
|
|
3084
|
+
use super::engine_value_to_rust_value;
|
|
3085
|
+
use crate::backend::EngineValue;
|
|
3086
|
+
use crate::state::RustValue;
|
|
3087
|
+
|
|
3088
|
+
fn decimal_schema() -> serde_json::Value {
|
|
3089
|
+
serde_json::json!({
|
|
3090
|
+
"properties": {
|
|
3091
|
+
"hours": {
|
|
3092
|
+
"format": "decimal",
|
|
3093
|
+
"anyOf": [
|
|
3094
|
+
{"type": "number"},
|
|
3095
|
+
{"type": "string", "pattern": "^-?\\d+(\\.\\d+)?$"}
|
|
3096
|
+
]
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
})
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
#[test]
|
|
3103
|
+
fn decimal_column_maps_sqlite_integer_affinity_to_decimal() {
|
|
3104
|
+
let schema = decimal_schema();
|
|
3105
|
+
let out = engine_value_to_rust_value(EngineValue::I64(3), &schema, "hours");
|
|
3106
|
+
assert!(matches!(out, RustValue::Decimal(ref s) if s == "3"));
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
#[test]
|
|
3110
|
+
fn decimal_column_maps_real_and_text() {
|
|
3111
|
+
let schema = decimal_schema();
|
|
3112
|
+
let from_real =
|
|
3113
|
+
engine_value_to_rust_value(EngineValue::F64(1.5), &schema, "hours");
|
|
3114
|
+
assert!(matches!(from_real, RustValue::Decimal(ref s) if s == "1.5"));
|
|
3115
|
+
let from_text =
|
|
3116
|
+
engine_value_to_rust_value(EngineValue::String("2.25".into()), &schema, "hours");
|
|
3117
|
+
assert!(matches!(from_text, RustValue::Decimal(ref s) if s == "2.25"));
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
fn datetime_schema() -> serde_json::Value {
|
|
3121
|
+
serde_json::json!({
|
|
3122
|
+
"properties": {
|
|
3123
|
+
"happened_at": {"type": "string", "format": "date-time"}
|
|
3124
|
+
}
|
|
3125
|
+
})
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
fn binary_schema() -> serde_json::Value {
|
|
3129
|
+
serde_json::json!({
|
|
3130
|
+
"properties": {
|
|
3131
|
+
"data": {"type": "string", "format": "binary"}
|
|
3132
|
+
}
|
|
3133
|
+
})
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
fn bool_schema() -> serde_json::Value {
|
|
3137
|
+
serde_json::json!({
|
|
3138
|
+
"properties": {
|
|
3139
|
+
"is_active": {"type": "boolean"}
|
|
3140
|
+
}
|
|
3141
|
+
})
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
fn json_schema() -> serde_json::Value {
|
|
3145
|
+
serde_json::json!({
|
|
3146
|
+
"properties": {
|
|
3147
|
+
"payload": {"type": "object"}
|
|
3148
|
+
}
|
|
3149
|
+
})
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
#[test]
|
|
3153
|
+
fn datetime_column_only_accepts_string_engine_values() {
|
|
3154
|
+
let schema = datetime_schema();
|
|
3155
|
+
let ok = engine_value_to_rust_value(
|
|
3156
|
+
EngineValue::String("2026-04-24T18:30:00+00:00".into()),
|
|
3157
|
+
&schema,
|
|
3158
|
+
"happened_at",
|
|
3159
|
+
);
|
|
3160
|
+
assert!(matches!(ok, RustValue::DateTime(_)));
|
|
3161
|
+
let from_int = engine_value_to_rust_value(EngineValue::I64(1713984600), &schema, "happened_at");
|
|
3162
|
+
assert!(matches!(from_int, RustValue::BigInt(1713984600)));
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
#[test]
|
|
3166
|
+
fn binary_column_maps_bytes_and_text() {
|
|
3167
|
+
let schema = binary_schema();
|
|
3168
|
+
let from_bytes =
|
|
3169
|
+
engine_value_to_rust_value(EngineValue::Bytes(vec![1, 2, 3]), &schema, "data");
|
|
3170
|
+
assert!(matches!(from_bytes, RustValue::Blob(v) if v == vec![1, 2, 3]));
|
|
3171
|
+
let from_text =
|
|
3172
|
+
engine_value_to_rust_value(EngineValue::String("abc".into()), &schema, "data");
|
|
3173
|
+
assert!(matches!(from_text, RustValue::Blob(v) if v == b"abc".to_vec()));
|
|
3174
|
+
let from_int = engine_value_to_rust_value(EngineValue::I64(1), &schema, "data");
|
|
3175
|
+
assert!(matches!(from_int, RustValue::None));
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
#[test]
|
|
3179
|
+
fn bool_column_maps_integer_and_bool() {
|
|
3180
|
+
let schema = bool_schema();
|
|
3181
|
+
assert!(matches!(
|
|
3182
|
+
engine_value_to_rust_value(EngineValue::I64(1), &schema, "is_active"),
|
|
3183
|
+
RustValue::Bool(true)
|
|
3184
|
+
));
|
|
3185
|
+
assert!(matches!(
|
|
3186
|
+
engine_value_to_rust_value(EngineValue::Bool(false), &schema, "is_active"),
|
|
3187
|
+
RustValue::Bool(false)
|
|
3188
|
+
));
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
#[test]
|
|
3192
|
+
fn json_column_parses_string_payload() {
|
|
3193
|
+
let schema = json_schema();
|
|
3194
|
+
let out = engine_value_to_rust_value(
|
|
3195
|
+
EngineValue::String(r#"{"k":"v"}"#.into()),
|
|
3196
|
+
&schema,
|
|
3197
|
+
"payload",
|
|
3198
|
+
);
|
|
3199
|
+
assert!(matches!(out, RustValue::Json(v) if v.get("k").and_then(|x| x.as_str()) == Some("v")));
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""SQLite + Alembic schema: typed column hydration after reset_engine() / reconnect.
|
|
2
|
+
|
|
3
|
+
Most Ferro tests use auto_migrate=True (Rust DDL) and same-session reads (identity map).
|
|
4
|
+
Bugs like #56 (NULL -> int 0) and #58 (INTEGER Decimal -> None) only appear when:
|
|
5
|
+
|
|
6
|
+
- Schema comes from Alembic ``create_all`` (``auto_migrate=False``)
|
|
7
|
+
- Rows are re-fetched on a fresh ``ferro.connect()``
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sqlite3
|
|
14
|
+
import tempfile
|
|
15
|
+
from collections.abc import AsyncIterator
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
from datetime import UTC, date, datetime
|
|
18
|
+
from decimal import Decimal
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
from uuid import UUID, uuid4
|
|
22
|
+
|
|
23
|
+
import ferro
|
|
24
|
+
import pytest
|
|
25
|
+
from ferro import Field, Model, clear_registry, reset_engine
|
|
26
|
+
from ferro.migrations.alembic import get_metadata
|
|
27
|
+
from sqlalchemy import pool
|
|
28
|
+
from sqlalchemy.engine.url import URL
|
|
29
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
30
|
+
|
|
31
|
+
pytest.importorskip("aiosqlite")
|
|
32
|
+
pytest.importorskip("greenlet")
|
|
33
|
+
|
|
34
|
+
pytestmark = pytest.mark.sqlite_only
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture(autouse=True)
|
|
38
|
+
def cleanup() -> None:
|
|
39
|
+
from ferro.state import _JOIN_TABLE_REGISTRY, _MODEL_REGISTRY_PY, _PENDING_RELATIONS
|
|
40
|
+
|
|
41
|
+
_MODEL_REGISTRY_PY.clear()
|
|
42
|
+
_PENDING_RELATIONS.clear()
|
|
43
|
+
_JOIN_TABLE_REGISTRY.clear()
|
|
44
|
+
reset_engine()
|
|
45
|
+
clear_registry()
|
|
46
|
+
yield
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _sqlite_url(path: Path) -> str:
|
|
50
|
+
return URL.create(
|
|
51
|
+
drivername="sqlite+aiosqlite",
|
|
52
|
+
database=str(path.resolve()),
|
|
53
|
+
).render_as_string(hide_password=False)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@asynccontextmanager
|
|
57
|
+
async def _alembic_sqlite_db() -> AsyncIterator[tuple[str, Path]]:
|
|
58
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
59
|
+
db_path = Path(tmp) / "app.db"
|
|
60
|
+
uri = f"sqlite:{db_path}?mode=rwc"
|
|
61
|
+
engine = create_async_engine(_sqlite_url(db_path), poolclass=pool.NullPool)
|
|
62
|
+
async with engine.begin() as conn:
|
|
63
|
+
await conn.run_sync(lambda sync_conn: get_metadata().create_all(sync_conn))
|
|
64
|
+
await engine.dispose()
|
|
65
|
+
yield uri, db_path
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def _reload_row[M: Model](model: type[M], uri: str, row_id: str) -> M:
|
|
69
|
+
reset_engine()
|
|
70
|
+
await ferro.connect(uri, auto_migrate=False)
|
|
71
|
+
row = await model.where(model.id == row_id).first() # type: ignore[attr-defined]
|
|
72
|
+
assert row is not None
|
|
73
|
+
return row
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _sqlite_typeof(db_path: Path, table: str, column: str, row_id: str) -> tuple[Any, str]:
|
|
77
|
+
with sqlite3.connect(db_path) as conn:
|
|
78
|
+
value, affinity = conn.execute(
|
|
79
|
+
f"SELECT {column}, typeof({column}) FROM {table} WHERE id = ?",
|
|
80
|
+
(row_id,),
|
|
81
|
+
).fetchone()
|
|
82
|
+
return value, affinity
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.asyncio
|
|
86
|
+
async def test_null_optional_datetime_is_none_not_zero() -> None:
|
|
87
|
+
"""Issue #56: NULL DATETIME must not hydrate as int(0)."""
|
|
88
|
+
|
|
89
|
+
class Client(Model):
|
|
90
|
+
id: str = Field(default_factory=lambda: str(uuid4()), primary_key=True, db_type="text")
|
|
91
|
+
name: str
|
|
92
|
+
archived_at: datetime | None = None
|
|
93
|
+
|
|
94
|
+
async with _alembic_sqlite_db() as (uri, db_path):
|
|
95
|
+
await ferro.connect(uri, auto_migrate=False)
|
|
96
|
+
created = await Client.create(name="Acme")
|
|
97
|
+
row = await _reload_row(Client, uri, created.id)
|
|
98
|
+
raw, affinity = _sqlite_typeof(db_path, "client", "archived_at", created.id)
|
|
99
|
+
|
|
100
|
+
assert raw is None
|
|
101
|
+
assert row.archived_at is None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.mark.asyncio
|
|
105
|
+
async def test_non_null_datetime_round_trips() -> None:
|
|
106
|
+
class Event(Model):
|
|
107
|
+
id: str = Field(default_factory=lambda: str(uuid4()), primary_key=True, db_type="text")
|
|
108
|
+
happened_at: datetime = Field(default_factory=lambda: datetime(2026, 4, 24, 18, 30, tzinfo=UTC))
|
|
109
|
+
|
|
110
|
+
async with _alembic_sqlite_db() as (uri, db_path):
|
|
111
|
+
await ferro.connect(uri, auto_migrate=False)
|
|
112
|
+
expected = datetime(2026, 4, 24, 18, 30, tzinfo=UTC)
|
|
113
|
+
created = await Event.create(happened_at=expected)
|
|
114
|
+
row = await _reload_row(Event, uri, created.id)
|
|
115
|
+
_sqlite_typeof(db_path, "event", "happened_at", created.id)
|
|
116
|
+
|
|
117
|
+
assert row.happened_at == expected
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_non_null_date_round_trips() -> None:
|
|
122
|
+
class Day(Model):
|
|
123
|
+
id: str = Field(default_factory=lambda: str(uuid4()), primary_key=True, db_type="text")
|
|
124
|
+
on_date: date
|
|
125
|
+
|
|
126
|
+
async with _alembic_sqlite_db() as (uri, db_path):
|
|
127
|
+
await ferro.connect(uri, auto_migrate=False)
|
|
128
|
+
expected = date(2026, 4, 24)
|
|
129
|
+
created = await Day.create(on_date=expected)
|
|
130
|
+
row = await _reload_row(Day, uri, created.id)
|
|
131
|
+
_sqlite_typeof(db_path, "day", "on_date", created.id)
|
|
132
|
+
|
|
133
|
+
assert row.on_date == expected
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.mark.asyncio
|
|
137
|
+
@pytest.mark.parametrize(
|
|
138
|
+
("hours", "expected"),
|
|
139
|
+
[
|
|
140
|
+
(Decimal(3), Decimal(3)),
|
|
141
|
+
(Decimal("1.5"), Decimal("1.5")),
|
|
142
|
+
(Decimal("0"), Decimal("0")),
|
|
143
|
+
],
|
|
144
|
+
ids=["integer_affinity", "real_affinity", "decimal_zero"],
|
|
145
|
+
)
|
|
146
|
+
async def test_decimal_round_trips(hours: Decimal, expected: Decimal) -> None:
|
|
147
|
+
"""Issue #58 class: NUMERIC columns must survive SQLite affinity + reconnect."""
|
|
148
|
+
|
|
149
|
+
class Widget(Model):
|
|
150
|
+
id: str = Field(default_factory=lambda: str(uuid4()), primary_key=True, db_type="text")
|
|
151
|
+
hours: Decimal
|
|
152
|
+
|
|
153
|
+
async with _alembic_sqlite_db() as (uri, db_path):
|
|
154
|
+
await ferro.connect(uri, auto_migrate=False)
|
|
155
|
+
created = await Widget.create(hours=hours)
|
|
156
|
+
row = await _reload_row(Widget, uri, created.id)
|
|
157
|
+
raw, affinity = _sqlite_typeof(db_path, "widget", "hours", created.id)
|
|
158
|
+
|
|
159
|
+
assert row.hours == expected
|
|
160
|
+
if hours == Decimal(3):
|
|
161
|
+
assert raw == 3
|
|
162
|
+
assert affinity == "integer"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@pytest.mark.asyncio
|
|
166
|
+
async def test_decimal_string_literal_round_trips() -> None:
|
|
167
|
+
"""String INSERT into NUMERIC may coerce affinity; value must still hydrate."""
|
|
168
|
+
|
|
169
|
+
class Widget(Model):
|
|
170
|
+
id: str = Field(default_factory=lambda: str(uuid4()), primary_key=True, db_type="text")
|
|
171
|
+
hours: Decimal
|
|
172
|
+
|
|
173
|
+
row_id = str(uuid4())
|
|
174
|
+
async with _alembic_sqlite_db() as (uri, db_path):
|
|
175
|
+
with sqlite3.connect(db_path) as conn:
|
|
176
|
+
conn.execute(
|
|
177
|
+
"INSERT INTO widget (id, hours) VALUES (?, ?)",
|
|
178
|
+
(row_id, "12.34"),
|
|
179
|
+
)
|
|
180
|
+
await ferro.connect(uri, auto_migrate=False)
|
|
181
|
+
row = await _reload_row(Widget, uri, row_id)
|
|
182
|
+
raw, affinity = _sqlite_typeof(db_path, "widget", "hours", row_id)
|
|
183
|
+
|
|
184
|
+
assert row.hours == Decimal("12.34")
|
|
185
|
+
assert affinity in ("text", "real")
|
|
186
|
+
assert raw in ("12.34", 12.34)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@pytest.mark.asyncio
|
|
190
|
+
async def test_optional_decimal_null_is_none() -> None:
|
|
191
|
+
class Line(Model):
|
|
192
|
+
id: str = Field(default_factory=lambda: str(uuid4()), primary_key=True, db_type="text")
|
|
193
|
+
amount: Decimal | None = None
|
|
194
|
+
|
|
195
|
+
async with _alembic_sqlite_db() as (uri, db_path):
|
|
196
|
+
await ferro.connect(uri, auto_migrate=False)
|
|
197
|
+
created = await Line.create()
|
|
198
|
+
row = await _reload_row(Line, uri, created.id)
|
|
199
|
+
raw, _ = _sqlite_typeof(db_path, "line", "amount", created.id)
|
|
200
|
+
|
|
201
|
+
assert raw is None
|
|
202
|
+
assert row.amount is None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.asyncio
|
|
206
|
+
async def test_bool_round_trips() -> None:
|
|
207
|
+
class Flag(Model):
|
|
208
|
+
id: str = Field(default_factory=lambda: str(uuid4()), primary_key=True, db_type="text")
|
|
209
|
+
is_active: bool
|
|
210
|
+
|
|
211
|
+
async with _alembic_sqlite_db() as (uri, db_path):
|
|
212
|
+
await ferro.connect(uri, auto_migrate=False)
|
|
213
|
+
created = await Flag.create(is_active=True)
|
|
214
|
+
row = await _reload_row(Flag, uri, created.id)
|
|
215
|
+
raw, affinity = _sqlite_typeof(db_path, "flag", "is_active", created.id)
|
|
216
|
+
|
|
217
|
+
assert row.is_active is True
|
|
218
|
+
assert raw in (1, True)
|
|
219
|
+
assert affinity == "integer"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@pytest.mark.asyncio
|
|
223
|
+
async def test_uuid_round_trips() -> None:
|
|
224
|
+
class Token(Model):
|
|
225
|
+
id: str = Field(default_factory=lambda: str(uuid4()), primary_key=True, db_type="text")
|
|
226
|
+
subject_id: UUID
|
|
227
|
+
|
|
228
|
+
token = uuid4()
|
|
229
|
+
async with _alembic_sqlite_db() as (uri, db_path):
|
|
230
|
+
await ferro.connect(uri, auto_migrate=False)
|
|
231
|
+
created = await Token.create(subject_id=token)
|
|
232
|
+
row = await _reload_row(Token, uri, created.id)
|
|
233
|
+
_sqlite_typeof(db_path, "token", "subject_id", created.id)
|
|
234
|
+
|
|
235
|
+
assert row.subject_id == token
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@pytest.mark.asyncio
|
|
239
|
+
async def test_json_dict_round_trips() -> None:
|
|
240
|
+
class Doc(Model):
|
|
241
|
+
id: str = Field(default_factory=lambda: str(uuid4()), primary_key=True, db_type="text")
|
|
242
|
+
payload: dict[str, str]
|
|
243
|
+
|
|
244
|
+
data = {"k": "v", "n": "2"}
|
|
245
|
+
async with _alembic_sqlite_db() as (uri, db_path):
|
|
246
|
+
await ferro.connect(uri, auto_migrate=False)
|
|
247
|
+
created = await Doc.create(payload=data)
|
|
248
|
+
row = await _reload_row(Doc, uri, created.id)
|
|
249
|
+
raw, affinity = _sqlite_typeof(db_path, "doc", "payload", created.id)
|
|
250
|
+
|
|
251
|
+
assert row.payload == data
|
|
252
|
+
assert affinity == "text"
|
|
253
|
+
assert json.loads(raw) == data
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@pytest.mark.asyncio
|
|
257
|
+
async def test_non_null_int_zero_round_trips() -> None:
|
|
258
|
+
"""Legitimate integer 0 must not be confused with NULL (#56 regression guard)."""
|
|
259
|
+
|
|
260
|
+
class Counter(Model):
|
|
261
|
+
id: str = Field(default_factory=lambda: str(uuid4()), primary_key=True, db_type="text")
|
|
262
|
+
score: int
|
|
263
|
+
|
|
264
|
+
async with _alembic_sqlite_db() as (uri, db_path):
|
|
265
|
+
await ferro.connect(uri, auto_migrate=False)
|
|
266
|
+
created = await Counter.create(score=0)
|
|
267
|
+
row = await _reload_row(Counter, uri, created.id)
|
|
268
|
+
raw, affinity = _sqlite_typeof(db_path, "counter", "score", created.id)
|
|
269
|
+
|
|
270
|
+
assert row.score == 0
|
|
271
|
+
assert raw == 0
|
|
272
|
+
assert affinity == "integer"
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""SQLite NULL columns must hydrate as None, not int 0 (issue #56)."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import sqlite3
|
|
6
|
-
import tempfile
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from uuid import uuid4
|
|
10
|
-
|
|
11
|
-
import ferro
|
|
12
|
-
import pytest
|
|
13
|
-
from ferro import Field, Model, clear_registry, reset_engine
|
|
14
|
-
from ferro.migrations.alembic import get_metadata
|
|
15
|
-
from sqlalchemy import pool
|
|
16
|
-
from sqlalchemy.engine.url import URL
|
|
17
|
-
from sqlalchemy.ext.asyncio import create_async_engine
|
|
18
|
-
|
|
19
|
-
pytest.importorskip("aiosqlite")
|
|
20
|
-
pytest.importorskip("greenlet")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@pytest.fixture(autouse=True)
|
|
24
|
-
def cleanup():
|
|
25
|
-
from ferro.state import _JOIN_TABLE_REGISTRY, _MODEL_REGISTRY_PY, _PENDING_RELATIONS
|
|
26
|
-
|
|
27
|
-
_MODEL_REGISTRY_PY.clear()
|
|
28
|
-
_PENDING_RELATIONS.clear()
|
|
29
|
-
_JOIN_TABLE_REGISTRY.clear()
|
|
30
|
-
reset_engine()
|
|
31
|
-
clear_registry()
|
|
32
|
-
yield
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _sqlite_url(path: Path) -> str:
|
|
36
|
-
return URL.create(
|
|
37
|
-
drivername="sqlite+aiosqlite",
|
|
38
|
-
database=str(path.resolve()),
|
|
39
|
-
).render_as_string(hide_password=False)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@pytest.mark.asyncio
|
|
43
|
-
async def test_sqlite_null_optional_datetime_hydrates_as_none_after_reconnect() -> None:
|
|
44
|
-
"""Alembic-created DATETIME NULL must not become int(0) on a fresh connection."""
|
|
45
|
-
|
|
46
|
-
class Client(Model):
|
|
47
|
-
id: str = Field(default_factory=lambda: str(uuid4()), primary_key=True, db_type="text")
|
|
48
|
-
name: str
|
|
49
|
-
archived_at: datetime | None = None
|
|
50
|
-
|
|
51
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
52
|
-
db_path = Path(tmp) / "app.db"
|
|
53
|
-
uri = f"sqlite:{db_path}?mode=rwc"
|
|
54
|
-
|
|
55
|
-
engine = create_async_engine(_sqlite_url(db_path), poolclass=pool.NullPool)
|
|
56
|
-
async with engine.begin() as conn:
|
|
57
|
-
await conn.run_sync(lambda sync_conn: get_metadata().create_all(sync_conn))
|
|
58
|
-
await engine.dispose()
|
|
59
|
-
|
|
60
|
-
await ferro.connect(uri, auto_migrate=False)
|
|
61
|
-
await Client.create(name="Acme")
|
|
62
|
-
ferro.reset_engine()
|
|
63
|
-
|
|
64
|
-
await ferro.connect(uri, auto_migrate=False)
|
|
65
|
-
row = (await Client.all())[0]
|
|
66
|
-
|
|
67
|
-
with sqlite3.connect(db_path) as conn:
|
|
68
|
-
raw = conn.execute("SELECT archived_at FROM client").fetchone()[0]
|
|
69
|
-
|
|
70
|
-
assert raw is None
|
|
71
|
-
assert row.archived_at is None
|
|
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
|
|
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
|
|
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
|
{ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/plans/2026-04-29-002-feat-named-connections-plan.md
RENAMED
|
File without changes
|
|
File without changes
|
{ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/issues/sa-pk-column-nullable-divergence.md
RENAMED
|
File without changes
|
{ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md
RENAMED
|
File without changes
|
{ferro_orm-0.10.1 → ferro_orm-0.10.2}/docs/solutions/patterns/configurable-column-storage-types.md
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|