ferro-orm 0.8.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.gitignore +1 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/CHANGELOG.md +19 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/Cargo.lock +1 -1
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/Cargo.toml +1 -1
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/PKG-INFO +1 -1
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/TEST_RESULTS.md +1 -1
- ferro_orm-0.9.0/docs/api/exceptions.md +15 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/api/model.md +3 -1
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/api/utilities.md +4 -1
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/changelog.md +7 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/concepts/type-safety.md +5 -2
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/guide/mutations.md +17 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/guide/queries.md +23 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/howto/multiple-databases.md +4 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/migration-sqlalchemy.md +20 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/mkdocs.yml +1 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/pyproject.toml +1 -1
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/__init__.py +2 -0
- ferro_orm-0.9.0/src/ferro/exceptions.py +17 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/models.py +40 -10
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/relations/descriptors.py +2 -2
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_alembic_bridge.py +74 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_connection.py +1 -1
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_crud.py +7 -4
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_deletion.py +2 -2
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_documentation_features.py +1 -2
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_named_connections_integration.py +3 -2
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_transactions.py +1 -1
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/uv.lock +1 -1
- ferro_orm-0.8.0/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/agent-native.json +0 -87
- ferro_orm-0.8.0/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/api-contract.json +0 -40
- ferro_orm-0.8.0/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/correctness.json +0 -15
- ferro_orm-0.8.0/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/data-migrations.json +0 -72
- ferro_orm-0.8.0/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/kieran-python.json +0 -44
- ferro_orm-0.8.0/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/maintainability.json +0 -79
- ferro_orm-0.8.0/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/project-standards.json +0 -45
- ferro_orm-0.8.0/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/testing.json +0 -108
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/PERMISSIONS.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/PYPI_CHECKLIST.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/PYPI_SETUP.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/generated/wheels.generated.yml +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/pull_request_template.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/workflows/ci.yml +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/workflows/packaging-smoke.yml +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/workflows/publish-docs.yml +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/workflows/publish.yml +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.github/workflows/release.yml +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.pre-commit-config.yaml +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/.python-version +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/AGENTS.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/CONTRIBUTING.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/LICENSE +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/README.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/api/fields.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/api/query.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/api/raw-sql.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/api/relationships.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/api/transactions.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/brainstorms/2026-05-08-typed-query-predicates-requirements.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/coming-soon.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/concepts/architecture.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/concepts/identity-map.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/concepts/performance.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/concepts/query-typing.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/contributing.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/faq.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/getting-started/installation.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/getting-started/next-steps.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/getting-started/tutorial.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/guide/backend.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/guide/database.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/guide/migrations.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/guide/models-and-fields.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/guide/relationships.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/guide/transactions.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/howto/pagination.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/howto/soft-deletes.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/howto/testing.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/howto/timestamps.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/index.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/plans/2026-05-08-001-feat-typed-query-predicates-plan.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/solutions/README.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/solutions/patterns/foreign-key-index.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/solutions/patterns/typed-null-binds.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/stylesheets/extra.css +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/docs/why-ferro.md +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/justfile +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/scripts/demo_queries.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/backend.rs +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/connection.rs +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/_annotation_utils.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/_core.pyi +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/_shadow_fk_types.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/base.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/composite_indexes.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/composite_uniques.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/fields.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/metaclass.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/migrations/__init__.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/migrations/alembic.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/py.typed +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/query/__init__.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/query/builder.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/query/nodes.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/raw.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/relations/__init__.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/schema_metadata.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/ferro/state.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/lib.rs +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/operations.rs +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/query.rs +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/schema.rs +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/src/state.rs +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/__init__.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/conftest.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/db_backends.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_aggregation.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_alembic_autogenerate.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_alembic_nullability.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_alembic_type_mapping.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_auto_migrate.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_bulk_update.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_composite_index.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_composite_unique.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_connection_redaction.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_constraints.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_cross_emitter_parity.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_db_backends.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_docs_examples.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_field_wrapper.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_helpers.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_hydration.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_metaclass_internals.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_metadata.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_models.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_one_to_one.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_query_builder.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_query_typing.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_raw_sql.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_refresh.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_relationship_engine.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_schema.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_schema_constraints.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_schema_enum_annotations.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_shadow_fk_types.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_static_contracts.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_string_search.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_structural_types.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_temporal_types.py +0 -0
- {ferro_orm-0.8.0 → ferro_orm-0.9.0}/tests/test_typed_null_binds.py +0 -0
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.9.0 (2026-05-09)
|
|
5
|
+
|
|
6
|
+
### Chores
|
|
7
|
+
|
|
8
|
+
- Gitignore .context and untrack committed artifacts
|
|
9
|
+
([#49](https://github.com/syn54x/ferro-orm/pull/49),
|
|
10
|
+
[`cac6330`](https://github.com/syn54x/ferro-orm/commit/cac63304bbcf6bb3fad22037c33e6e60dcb8946e))
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- Add get_or_none method
|
|
15
|
+
([`0c81e9f`](https://github.com/syn54x/ferro-orm/commit/0c81e9f414074c9a3631eb94f3a8077d16671e41))
|
|
16
|
+
|
|
17
|
+
### Testing
|
|
18
|
+
|
|
19
|
+
- Add tests for explicit shadow fields
|
|
20
|
+
([`78a3471`](https://github.com/syn54x/ferro-orm/commit/78a3471d55afec3b9b7da9f44e497ec521f7d1c0))
|
|
21
|
+
|
|
22
|
+
|
|
4
23
|
## v0.8.0 (2026-05-09)
|
|
5
24
|
|
|
6
25
|
### Features
|
|
@@ -36,7 +36,7 @@ All field types and constraints work as documented:
|
|
|
36
36
|
All documented CRUD operations work correctly:
|
|
37
37
|
|
|
38
38
|
- `Model.create()` ✅
|
|
39
|
-
- `Model.get()` ✅
|
|
39
|
+
- `Model.get()` / `Model.get_or_none()` ✅
|
|
40
40
|
- `Model.all()` ✅
|
|
41
41
|
- `instance.save()` ✅
|
|
42
42
|
- `instance.delete()` ✅
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Exceptions
|
|
2
|
+
|
|
3
|
+
Public exception types raised by Ferro’s ORM layer.
|
|
4
|
+
|
|
5
|
+
::: ferro.exceptions.ModelDoesNotExist
|
|
6
|
+
options:
|
|
7
|
+
show_source: false
|
|
8
|
+
heading_level: 2
|
|
9
|
+
show_root_heading: true
|
|
10
|
+
|
|
11
|
+
## See Also
|
|
12
|
+
|
|
13
|
+
- [Queries](../guide/queries.md) — `Model.get` vs `Model.get_or_none`
|
|
14
|
+
- [Model API](model.md)
|
|
15
|
+
- [Mutations](../guide/mutations.md) — error handling patterns
|
|
@@ -8,6 +8,7 @@ Complete reference for the `Model` base class and related methods.
|
|
|
8
8
|
- create
|
|
9
9
|
- bulk_create
|
|
10
10
|
- get
|
|
11
|
+
- get_or_none
|
|
11
12
|
- where
|
|
12
13
|
- select
|
|
13
14
|
- all
|
|
@@ -25,5 +26,6 @@ Complete reference for the `Model` base class and related methods.
|
|
|
25
26
|
## See Also
|
|
26
27
|
|
|
27
28
|
- [Models & Fields Guide](../guide/models-and-fields.md)
|
|
28
|
-
- [Queries Guide](../guide/queries.md)
|
|
29
|
+
- [Queries Guide](../guide/queries.md) — fetch by primary key (`get` / `get_or_none`)
|
|
30
|
+
- [Exceptions](exceptions.md) — `ModelDoesNotExist`
|
|
29
31
|
- [Mutations Guide](../guide/mutations.md)
|
|
@@ -57,14 +57,17 @@ from ferro import evict_instance
|
|
|
57
57
|
# Evict user with ID=1
|
|
58
58
|
evict_instance("User", 1)
|
|
59
59
|
|
|
60
|
-
# Next fetch will hit database
|
|
60
|
+
# Next fetch will hit database (raises ModelDoesNotExist if row was removed)
|
|
61
61
|
user = await User.get(1)
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
If the row may no longer exist, use `User.get_or_none(1)` or handle [`ModelDoesNotExist`](exceptions.md).
|
|
65
|
+
|
|
64
66
|
See [Identity Map Concept](../concepts/identity-map.md) for when and why to evict instances.
|
|
65
67
|
|
|
66
68
|
## See Also
|
|
67
69
|
|
|
68
70
|
- [Database Setup Guide](../guide/database.md) - Connection configuration
|
|
69
71
|
- [Identity Map Concept](../concepts/identity-map.md) - Instance caching details
|
|
72
|
+
- [Exceptions](exceptions.md) - `ModelDoesNotExist` and related types
|
|
70
73
|
- [Schema Management](../guide/migrations.md) - Production migrations
|
|
@@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Breaking
|
|
11
|
+
|
|
12
|
+
- `Model.get` and `Model.using(...).get` now return the concrete model type and raise `ModelDoesNotExist` when no row exists (previously they returned `T | None`). Use the new `get_or_none` for the old optional behavior.
|
|
13
|
+
|
|
10
14
|
### Added
|
|
15
|
+
|
|
16
|
+
- `Model.get_or_none` and `Model.using(...).get_or_none` for primary-key lookup without raising.
|
|
17
|
+
- `ModelDoesNotExist` (`LookupError` subclass with `.model` and `.pk`), exported from `ferro`. Documented under [Exceptions](api/exceptions.md).
|
|
11
18
|
- Typed query predicates: `col()` wrapper and lambda predicate API on `Query.where`, `Relation.where`, and `Model.where` for static-typing-clean predicates without model annotation changes ([#48](https://github.com/syn54x/ferro-orm/pull/48)). See [Typed Query Predicates](concepts/query-typing.md).
|
|
12
19
|
- `FieldProxy` is now generic (`FieldProxy[T]`); operator overloads are typed `T | FieldProxy[T] -> QueryNode`, `.like()` is gated to `FieldProxy[str]`.
|
|
13
20
|
- New public symbols re-exported from `ferro.query`: `col`, `QueryProxy`, `Predicate`.
|
|
@@ -57,16 +57,19 @@ class User(Model):
|
|
|
57
57
|
username: str
|
|
58
58
|
age: int
|
|
59
59
|
|
|
60
|
-
#
|
|
60
|
+
# Fetch by PK: definite model instance (raises ModelDoesNotExist if missing)
|
|
61
61
|
user: User = await User.get(1)
|
|
62
62
|
|
|
63
|
+
# Optional PK lookup
|
|
64
|
+
maybe_user: User | None = await User.get_or_none(999)
|
|
65
|
+
|
|
63
66
|
# Autocomplete works
|
|
64
67
|
user.username # ✓ Known attribute
|
|
65
68
|
user.invalid # ✗ Type error
|
|
66
69
|
|
|
67
70
|
# Query results are typed
|
|
68
71
|
users: list[User] = await User.all()
|
|
69
|
-
first: User | None = await User.first()
|
|
72
|
+
first: User | None = await User.where(User.username == "alice").first()
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
## IDE Autocomplete
|
|
@@ -381,6 +381,23 @@ await User.bulk_create(users)
|
|
|
381
381
|
!!! note "Exception Types"
|
|
382
382
|
The documentation references exception types like `IntegrityError` and `ValidationError`. These exceptions come from the underlying database driver or Pydantic. Import paths may vary. Catch general `Exception` or check your specific database driver's exceptions.
|
|
383
383
|
|
|
384
|
+
### Primary key lookup (`Model.get`)
|
|
385
|
+
|
|
386
|
+
`Model.get(pk)` and `Model.using(...).get(pk)` raise [`ModelDoesNotExist`](../api/exceptions.md) when no row exists. Import it from `ferro`. For “maybe there is a row” flows (for example verifying a delete), use `get_or_none` instead of catching the exception.
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
from ferro import ModelDoesNotExist
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
user = await User.get(user_id)
|
|
393
|
+
except ModelDoesNotExist:
|
|
394
|
+
# e.model, e.pk
|
|
395
|
+
...
|
|
396
|
+
|
|
397
|
+
# Or, when None is the natural result:
|
|
398
|
+
user = await User.get_or_none(user_id)
|
|
399
|
+
```
|
|
400
|
+
|
|
384
401
|
### Unique Constraint Violations
|
|
385
402
|
|
|
386
403
|
```python
|
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
Ferro provides a fluent, type-safe API for constructing and executing database queries. All queries are constructed in Python and executed by the high-performance Rust engine.
|
|
4
4
|
|
|
5
|
+
## Fetch by primary key
|
|
6
|
+
|
|
7
|
+
`Model.get(pk)` loads exactly one row by primary key and returns **your model type** (not `YourModel | None`). If no row exists, Ferro raises [`ModelDoesNotExist`](../api/exceptions.md), a subclass of `LookupError` with `.model` and `.pk` set—useful for HTTP 404s or structured logging.
|
|
8
|
+
|
|
9
|
+
When a missing row is a normal outcome, use `Model.get_or_none(pk)`, which returns `YourModel | None` and never raises for “not found”. The same pair exists on [`Model.using("name")`](../howto/multiple-databases.md) for named connections.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from ferro import ModelDoesNotExist
|
|
13
|
+
|
|
14
|
+
user = await User.get(42)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
user = await User.get(client_supplied_id)
|
|
18
|
+
except ModelDoesNotExist:
|
|
19
|
+
... # e.g. return 404 from your HTTP layer
|
|
20
|
+
|
|
21
|
+
draft = await User.get_or_none(999) # None if no such row
|
|
22
|
+
|
|
23
|
+
replica_view = await User.using("replica").get_or_none(1)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Lazy forward relations (e.g. `await post.author`) use optional fetch internally so a broken or missing FK still resolves to `None` instead of raising.
|
|
27
|
+
|
|
5
28
|
## Basic Filtering
|
|
6
29
|
|
|
7
30
|
Use standard Python comparison operators on model fields to create filter conditions:
|
|
@@ -33,6 +33,10 @@ users = await User.all()
|
|
|
33
33
|
# Specific database
|
|
34
34
|
replica_users = await User.using("replica").all()
|
|
35
35
|
analytics_data = await Metric.using("analytics").all()
|
|
36
|
+
|
|
37
|
+
# Primary-key fetch on a named connection (same semantics as Model.get / get_or_none)
|
|
38
|
+
user = await User.using("replica").get(1) # raises ModelDoesNotExist if missing
|
|
39
|
+
maybe = await User.using("replica").get_or_none(unknown_id)
|
|
36
40
|
```
|
|
37
41
|
|
|
38
42
|
## Transactions
|
|
@@ -69,6 +69,26 @@ users = result.scalars().all()
|
|
|
69
69
|
users = await User.where(User.age >= 18).all()
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
### Get by primary key
|
|
73
|
+
|
|
74
|
+
SQLAlchemy’s `session.get(User, pk)` returns `None` when the row is missing. Ferro’s `await User.get(pk)` returns `User` and raises `ModelDoesNotExist` when absent. Use `await User.get_or_none(pk)` for the optional pattern.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# SQLAlchemy
|
|
78
|
+
user = await session.get(User, 1)
|
|
79
|
+
|
|
80
|
+
# Ferro — raises if missing
|
|
81
|
+
from ferro import ModelDoesNotExist
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
user = await User.get(1)
|
|
85
|
+
except ModelDoesNotExist:
|
|
86
|
+
user = None
|
|
87
|
+
|
|
88
|
+
# Ferro — optional (like session.get when no row)
|
|
89
|
+
user = await User.get_or_none(1)
|
|
90
|
+
```
|
|
91
|
+
|
|
72
92
|
## Relationships
|
|
73
93
|
|
|
74
94
|
### One-to-Many
|
|
@@ -22,6 +22,7 @@ from ._core import (
|
|
|
22
22
|
connect as _core_connect,
|
|
23
23
|
)
|
|
24
24
|
from .base import FerroField, FerroNullable, ForeignKey
|
|
25
|
+
from .exceptions import ModelDoesNotExist
|
|
25
26
|
from .fields import BackRef, Field, ManyToMany
|
|
26
27
|
from .models import Model, transaction
|
|
27
28
|
from .query import Relation
|
|
@@ -96,6 +97,7 @@ __all__ = [
|
|
|
96
97
|
"connect",
|
|
97
98
|
"PoolConfig",
|
|
98
99
|
"Model",
|
|
100
|
+
"ModelDoesNotExist",
|
|
99
101
|
"FerroField",
|
|
100
102
|
"FerroNullable",
|
|
101
103
|
"Field",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""ORM-specific exceptions."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ModelDoesNotExist(LookupError):
|
|
7
|
+
"""Raised when :meth:`~ferro.models.Model.get` finds no row for the primary key."""
|
|
8
|
+
|
|
9
|
+
model: type
|
|
10
|
+
pk: Any
|
|
11
|
+
|
|
12
|
+
def __init__(self, model_cls: type, pk: Any) -> None:
|
|
13
|
+
self.model = model_cls
|
|
14
|
+
self.pk = pk
|
|
15
|
+
super().__init__(
|
|
16
|
+
f"No {model_cls.__name__} record found for primary key {pk!r}"
|
|
17
|
+
)
|
|
@@ -31,6 +31,7 @@ from ._core import (
|
|
|
31
31
|
transaction_connection_name,
|
|
32
32
|
)
|
|
33
33
|
from .base import ForeignKey, foreign_key_allows_none
|
|
34
|
+
from .exceptions import ModelDoesNotExist
|
|
34
35
|
from .metaclass import ModelMetaclass
|
|
35
36
|
from .query import Query, QueryNode
|
|
36
37
|
from .state import _CURRENT_TRANSACTION, _CURRENT_TRANSACTION_CONNECTION
|
|
@@ -260,7 +261,7 @@ class Model(BaseModel, metaclass=ModelMetaclass):
|
|
|
260
261
|
None
|
|
261
262
|
|
|
262
263
|
Examples:
|
|
263
|
-
>>> user = await User.
|
|
264
|
+
>>> user = await User.get_or_none(1)
|
|
264
265
|
>>> if user:
|
|
265
266
|
... await user.delete()
|
|
266
267
|
"""
|
|
@@ -361,20 +362,39 @@ class Model(BaseModel, metaclass=ModelMetaclass):
|
|
|
361
362
|
return results
|
|
362
363
|
|
|
363
364
|
@classmethod
|
|
364
|
-
async def get(cls, pk: Any) -> Self
|
|
365
|
-
"""Fetch one record by primary key value
|
|
365
|
+
async def get(cls, pk: Any) -> Self:
|
|
366
|
+
"""Fetch one record by primary key value.
|
|
366
367
|
|
|
367
368
|
Args:
|
|
368
369
|
pk: Primary key value to fetch a single record.
|
|
369
370
|
|
|
370
371
|
Returns:
|
|
371
|
-
The matching model instance
|
|
372
|
+
The matching model instance.
|
|
373
|
+
|
|
374
|
+
Raises:
|
|
375
|
+
ModelDoesNotExist: When no row exists for this primary key. Use
|
|
376
|
+
:meth:`get_or_none` if you need optional lookup without raising.
|
|
372
377
|
|
|
373
378
|
Examples:
|
|
374
379
|
>>> user = await User.get(1)
|
|
375
|
-
>>>
|
|
380
|
+
>>> isinstance(user, User)
|
|
376
381
|
True
|
|
377
382
|
"""
|
|
383
|
+
instance = await cls.get_or_none(pk)
|
|
384
|
+
if instance is None:
|
|
385
|
+
raise ModelDoesNotExist(cls, pk)
|
|
386
|
+
return instance
|
|
387
|
+
|
|
388
|
+
@classmethod
|
|
389
|
+
async def get_or_none(cls, pk: Any) -> Self | None:
|
|
390
|
+
"""Fetch one record by primary key, or return None if no row exists.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
pk: Primary key value to fetch a single record.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
The matching model instance, or None when no record exists.
|
|
397
|
+
"""
|
|
378
398
|
pk_field_name = cls._primary_key_field_name()
|
|
379
399
|
if pk_field_name is None:
|
|
380
400
|
raise RuntimeError(f"Model {cls.__name__} does not define a primary key")
|
|
@@ -395,8 +415,7 @@ class Model(BaseModel, metaclass=ModelMetaclass):
|
|
|
395
415
|
|
|
396
416
|
Examples:
|
|
397
417
|
>>> user = await User.get(1)
|
|
398
|
-
>>>
|
|
399
|
-
... await user.refresh()
|
|
418
|
+
>>> await user.refresh()
|
|
400
419
|
"""
|
|
401
420
|
pk_field_name = self.__class__._primary_key_field_name()
|
|
402
421
|
pk_val = getattr(self, pk_field_name) if pk_field_name is not None else None
|
|
@@ -579,7 +598,7 @@ class ModelConnection[M: Model]:
|
|
|
579
598
|
|
|
580
599
|
Generic over the concrete model class so that every accessor preserves
|
|
581
600
|
the bound type — e.g. ``Transcript.using("service").get(pk)`` resolves
|
|
582
|
-
to ``Transcript
|
|
601
|
+
to ``Transcript`` rather than ``Model``.
|
|
583
602
|
"""
|
|
584
603
|
|
|
585
604
|
def __init__(self, model_cls: type[M], connection_name: str) -> None:
|
|
@@ -600,14 +619,25 @@ class ModelConnection[M: Model]:
|
|
|
600
619
|
def where(self, node: QueryNode) -> Query[M]:
|
|
601
620
|
return self.select().where(node)
|
|
602
621
|
|
|
603
|
-
async def get(self, pk: Any) -> M
|
|
622
|
+
async def get(self, pk: Any) -> M:
|
|
623
|
+
instance = await self.get_or_none(pk)
|
|
624
|
+
if instance is None:
|
|
625
|
+
raise ModelDoesNotExist(self.model_cls, pk)
|
|
626
|
+
return instance
|
|
627
|
+
|
|
628
|
+
async def get_or_none(self, pk: Any) -> M | None:
|
|
604
629
|
pk_field_name = self.model_cls._primary_key_field_name()
|
|
605
630
|
if pk_field_name is None:
|
|
606
631
|
raise RuntimeError(
|
|
607
632
|
f"Model {self.model_cls.__name__} does not define a primary key"
|
|
608
633
|
)
|
|
609
634
|
|
|
610
|
-
|
|
635
|
+
instance = await self.where(
|
|
636
|
+
getattr(self.model_cls, pk_field_name) == pk
|
|
637
|
+
).first()
|
|
638
|
+
if instance:
|
|
639
|
+
self.model_cls._fix_types(instance)
|
|
640
|
+
return instance
|
|
611
641
|
|
|
612
642
|
async def bulk_create(self, instances: list[M]) -> int:
|
|
613
643
|
return await self.model_cls.bulk_create(instances, using=self._connection_name)
|
|
@@ -109,7 +109,7 @@ class ForwardDescriptor(BaseModel):
|
|
|
109
109
|
|
|
110
110
|
origin = _instance_origin_outside_transaction(instance)
|
|
111
111
|
if origin is not None:
|
|
112
|
-
return await self._target_model.using(origin).
|
|
113
|
-
return await self._target_model.
|
|
112
|
+
return await self._target_model.using(origin).get_or_none(id_val)
|
|
113
|
+
return await self._target_model.get_or_none(id_val)
|
|
114
114
|
|
|
115
115
|
return _fetch()
|
|
@@ -16,6 +16,7 @@ from ferro import (
|
|
|
16
16
|
reset_engine,
|
|
17
17
|
)
|
|
18
18
|
from ferro.migrations import get_metadata
|
|
19
|
+
from ferro.schema_metadata import build_model_schema
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
@pytest.fixture(autouse=True)
|
|
@@ -179,3 +180,76 @@ def test_on_delete_translation():
|
|
|
179
180
|
fk = list(product_table.c.category_id.foreign_keys)[0]
|
|
180
181
|
assert fk.ondelete == "SET NULL"
|
|
181
182
|
assert product_table.c.category_id.nullable is True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_explicit_foreign_key_shadow_id_no_duplicate_alembic_columns():
|
|
186
|
+
"""Declaring ``{relation}_id`` for static checkers must not duplicate DDL columns.
|
|
187
|
+
|
|
188
|
+
Ferro injects a shadow ``*_id`` for every :class:`~ferro.base.ForeignKey`. Some
|
|
189
|
+
type checkers (for example Ty) do not see metaclass-injected fields, so users may
|
|
190
|
+
duplicate the declaration in the class body. That must still produce exactly one
|
|
191
|
+
JSON-schema property and one SQLAlchemy column for Alembic autogenerate.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
class TyShadowFkJobRole(Model):
|
|
195
|
+
id: Annotated[int | None, FerroField(primary_key=True)] = None
|
|
196
|
+
name: str
|
|
197
|
+
scorecards: Relation[list["TyShadowFkScorecard"]] = BackRef()
|
|
198
|
+
|
|
199
|
+
class TyShadowFkScorecard(Model):
|
|
200
|
+
id: Annotated[int | None, FerroField(primary_key=True)] = None
|
|
201
|
+
title: str
|
|
202
|
+
job_role: Annotated[TyShadowFkJobRole, ForeignKey(related_name="scorecards")]
|
|
203
|
+
job_role_id: int | None = None
|
|
204
|
+
|
|
205
|
+
assert list(TyShadowFkScorecard.model_fields).count("job_role_id") == 1
|
|
206
|
+
|
|
207
|
+
schema = build_model_schema(TyShadowFkScorecard)
|
|
208
|
+
props = schema["properties"]
|
|
209
|
+
assert "job_role_id" in props
|
|
210
|
+
assert sum(1 for k in props if k == "job_role_id") == 1
|
|
211
|
+
fk_meta = props["job_role_id"].get("foreign_key") or {}
|
|
212
|
+
assert fk_meta.get("to_table") == "tyshadowfkjobrole"
|
|
213
|
+
|
|
214
|
+
metadata = get_metadata()
|
|
215
|
+
tbl = metadata.tables["tyshadowfkscorecard"]
|
|
216
|
+
assert list(tbl.columns.keys()).count("job_role_id") == 1
|
|
217
|
+
col = tbl.c.job_role_id
|
|
218
|
+
fks = list(col.foreign_keys)
|
|
219
|
+
assert len(fks) == 1
|
|
220
|
+
assert fks[0].target_fullname == "tyshadowfkjobrole.id"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@pytest.mark.asyncio
|
|
224
|
+
@pytest.mark.backend_matrix
|
|
225
|
+
async def test_explicit_foreign_key_shadow_id_auto_migrate_roundtrip(db_url):
|
|
226
|
+
"""Runtime migrate + ORM must treat explicit ``*_id`` as the single FK column."""
|
|
227
|
+
|
|
228
|
+
from ferro import connect
|
|
229
|
+
|
|
230
|
+
class TyRoundJobRole(Model):
|
|
231
|
+
id: Annotated[int | None, FerroField(primary_key=True)] = None
|
|
232
|
+
name: str
|
|
233
|
+
scorecards: Relation[list["TyRoundScorecard"]] = BackRef()
|
|
234
|
+
|
|
235
|
+
class TyRoundScorecard(Model):
|
|
236
|
+
id: Annotated[int | None, FerroField(primary_key=True)] = None
|
|
237
|
+
title: str
|
|
238
|
+
job_role: Annotated[TyRoundJobRole, ForeignKey(related_name="scorecards")]
|
|
239
|
+
job_role_id: int | None = None
|
|
240
|
+
|
|
241
|
+
await connect(db_url, auto_migrate=True)
|
|
242
|
+
|
|
243
|
+
role = await TyRoundJobRole.create(name="ic")
|
|
244
|
+
card = await TyRoundScorecard.create(title="card-a", job_role=role)
|
|
245
|
+
assert card.job_role_id == role.id
|
|
246
|
+
|
|
247
|
+
by_attr = await TyRoundScorecard.where(
|
|
248
|
+
TyRoundScorecard.job_role_id == role.id
|
|
249
|
+
).first()
|
|
250
|
+
assert by_attr is not None and by_attr.id == card.id
|
|
251
|
+
|
|
252
|
+
by_lambda = await TyRoundScorecard.where(
|
|
253
|
+
lambda s: s.job_role_id == role.id
|
|
254
|
+
).first()
|
|
255
|
+
assert by_lambda is not None and by_lambda.id == card.id
|
|
@@ -289,7 +289,7 @@ async def test_service_loaded_instance_delete_uses_origin_connection(tmp_path):
|
|
|
289
289
|
await service_row.delete()
|
|
290
290
|
|
|
291
291
|
assert await ConnectionRouteMarker.get(1) is not None
|
|
292
|
-
assert await ConnectionRouteMarker.using("service").
|
|
292
|
+
assert await ConnectionRouteMarker.using("service").get_or_none(1) is None
|
|
293
293
|
|
|
294
294
|
|
|
295
295
|
@pytest.mark.asyncio
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from pydantic import Field
|
|
3
3
|
import ferro
|
|
4
|
-
from ferro import Model
|
|
4
|
+
from ferro import Model, ModelDoesNotExist
|
|
5
5
|
|
|
6
6
|
pytestmark = pytest.mark.backend_matrix
|
|
7
7
|
|
|
@@ -158,7 +158,7 @@ async def test_model_get_invalid_usage(db_url):
|
|
|
158
158
|
|
|
159
159
|
@pytest.mark.asyncio
|
|
160
160
|
async def test_model_get_not_found(db_url):
|
|
161
|
-
"""Test that get()
|
|
161
|
+
"""Test that get() raises when the record does not exist."""
|
|
162
162
|
|
|
163
163
|
class CrudUser(Model):
|
|
164
164
|
id: int = Field(default=None, json_schema_extra={"primary_key": True})
|
|
@@ -166,5 +166,8 @@ async def test_model_get_not_found(db_url):
|
|
|
166
166
|
email: str
|
|
167
167
|
|
|
168
168
|
await ferro.connect(db_url, auto_migrate=True)
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
with pytest.raises(ModelDoesNotExist) as exc_info:
|
|
170
|
+
await CrudUser.get(9999)
|
|
171
|
+
assert exc_info.value.model is CrudUser
|
|
172
|
+
assert exc_info.value.pk == 9999
|
|
173
|
+
assert await CrudUser.get_or_none(9999) is None
|
|
@@ -27,7 +27,7 @@ async def test_instance_delete(db_url):
|
|
|
27
27
|
await user.delete()
|
|
28
28
|
|
|
29
29
|
# Verify it's gone from DB
|
|
30
|
-
assert await DeletableUser.
|
|
30
|
+
assert await DeletableUser.get_or_none(user_id) is None
|
|
31
31
|
|
|
32
32
|
# Verify it's gone from Identity Map (fetching again should return None)
|
|
33
33
|
# Note: the 'user' object still exists in Python memory, but it's disconnected from the DB.
|
|
@@ -84,4 +84,4 @@ async def test_delete_evicts_identity_map(db_url):
|
|
|
84
84
|
await DeletableUser.where(DeletableUser.id == user_id).delete()
|
|
85
85
|
|
|
86
86
|
# A fresh 'get' should NOT return the old 'user' object (it should be None)
|
|
87
|
-
assert await DeletableUser.
|
|
87
|
+
assert await DeletableUser.get_or_none(user_id) is None
|
|
@@ -35,7 +35,8 @@ if TYPE_CHECKING:
|
|
|
35
35
|
bound.where(NamedSmokeMarker.id == 1), # type: ignore[arg-type]
|
|
36
36
|
"Query[NamedSmokeMarker]",
|
|
37
37
|
)
|
|
38
|
-
assert_type(await bound.get(1), NamedSmokeMarker
|
|
38
|
+
assert_type(await bound.get(1), NamedSmokeMarker)
|
|
39
|
+
assert_type(await bound.get_or_none(1), NamedSmokeMarker | None)
|
|
39
40
|
assert_type(await bound.bulk_create([]), int)
|
|
40
41
|
assert_type(
|
|
41
42
|
await bound.get_or_create(label="x"),
|
|
@@ -96,7 +97,7 @@ async def test_named_connections_smoke_matrix_sqlite(tmp_path):
|
|
|
96
97
|
|
|
97
98
|
assert await delete_record("NamedSmokeMarker", "1", using="service") is True
|
|
98
99
|
assert await NamedSmokeMarker.get(1) is app_row
|
|
99
|
-
assert await NamedSmokeMarker.using("service").
|
|
100
|
+
assert await NamedSmokeMarker.using("service").get_or_none(1) is None
|
|
100
101
|
|
|
101
102
|
|
|
102
103
|
@pytest.mark.asyncio
|
|
@@ -135,7 +135,7 @@ async def test_instance_methods_loaded_inside_transaction_inherit_transaction(db
|
|
|
135
135
|
|
|
136
136
|
await loaded.delete()
|
|
137
137
|
|
|
138
|
-
assert await TxInstanceMethodUser.
|
|
138
|
+
assert await TxInstanceMethodUser.get_or_none(1) is None
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
@pytest.mark.asyncio
|