ferro-orm 0.6.0__tar.gz → 0.7.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.
Files changed (157) hide show
  1. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/CHANGELOG.md +17 -0
  2. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/Cargo.lock +18 -18
  3. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/Cargo.toml +1 -1
  4. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/PKG-INFO +1 -1
  5. ferro_orm-0.7.0/docs/plans/2026-05-07-001-refactor-generic-model-connection-plan.md +192 -0
  6. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/pyproject.toml +1 -1
  7. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/backend.rs +17 -0
  8. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/connection.rs +4 -2
  9. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/__init__.py +6 -0
  10. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/_core.pyi +2 -0
  11. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/models.py +25 -20
  12. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/operations.rs +67 -38
  13. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_alembic_type_mapping.py +21 -0
  14. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_connection.py +1 -0
  15. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_crud.py +21 -0
  16. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_named_connections_integration.py +38 -1
  17. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_structural_types.py +42 -0
  18. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/uv.lock +1 -1
  19. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/agent-native.json +0 -0
  20. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/api-contract.json +0 -0
  21. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/correctness.json +0 -0
  22. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/data-migrations.json +0 -0
  23. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/kieran-python.json +0 -0
  24. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/maintainability.json +0 -0
  25. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/project-standards.json +0 -0
  26. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.context/compound-engineering/ce-code-review/20260428-081705-bdeab5a2/testing.json +0 -0
  27. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  28. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  29. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/PERMISSIONS.md +0 -0
  30. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/PYPI_CHECKLIST.md +0 -0
  31. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/PYPI_SETUP.md +0 -0
  32. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/generated/wheels.generated.yml +0 -0
  33. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/pull_request_template.md +0 -0
  34. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/workflows/ci.yml +0 -0
  35. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/workflows/packaging-smoke.yml +0 -0
  36. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/workflows/publish-docs.yml +0 -0
  37. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/workflows/publish.yml +0 -0
  38. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.github/workflows/release.yml +0 -0
  39. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.gitignore +0 -0
  40. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.pre-commit-config.yaml +0 -0
  41. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/.python-version +0 -0
  42. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/AGENTS.md +0 -0
  43. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/CONTRIBUTING.md +0 -0
  44. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/LICENSE +0 -0
  45. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/README.md +0 -0
  46. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/TEST_RESULTS.md +0 -0
  47. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/api/fields.md +0 -0
  48. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/api/model.md +0 -0
  49. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/api/query.md +0 -0
  50. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/api/raw-sql.md +0 -0
  51. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/api/relationships.md +0 -0
  52. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/api/transactions.md +0 -0
  53. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/api/utilities.md +0 -0
  54. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/brainstorms/2026-04-29-named-connections-role-routing-requirements.md +0 -0
  55. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/changelog.md +0 -0
  56. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/coming-soon.md +0 -0
  57. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/concepts/architecture.md +0 -0
  58. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/concepts/identity-map.md +0 -0
  59. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/concepts/performance.md +0 -0
  60. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/concepts/type-safety.md +0 -0
  61. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/contributing.md +0 -0
  62. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/faq.md +0 -0
  63. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/getting-started/installation.md +0 -0
  64. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/getting-started/next-steps.md +0 -0
  65. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/getting-started/tutorial.md +0 -0
  66. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/guide/backend.md +0 -0
  67. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/guide/database.md +0 -0
  68. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/guide/migrations.md +0 -0
  69. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/guide/models-and-fields.md +0 -0
  70. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/guide/mutations.md +0 -0
  71. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/guide/queries.md +0 -0
  72. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/guide/relationships.md +0 -0
  73. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/guide/transactions.md +0 -0
  74. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/howto/multiple-databases.md +0 -0
  75. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/howto/pagination.md +0 -0
  76. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/howto/soft-deletes.md +0 -0
  77. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/howto/testing.md +0 -0
  78. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/howto/timestamps.md +0 -0
  79. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/index.md +0 -0
  80. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/migration-sqlalchemy.md +0 -0
  81. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +0 -0
  82. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/plans/2026-04-29-001-typed-null-binds-plan.md +0 -0
  83. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/plans/2026-04-29-002-feat-named-connections-plan.md +0 -0
  84. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/solutions/README.md +0 -0
  85. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/solutions/issues/python-3.14-deferred-annotation-typeerror-swallow.md +0 -0
  86. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/solutions/issues/sa-pk-column-nullable-divergence.md +0 -0
  87. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/solutions/issues/sa-vs-rust-unique-constraint-shape.md +0 -0
  88. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/solutions/patterns/cross-emitter-ddl-parity.md +0 -0
  89. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/solutions/patterns/foreign-key-index.md +0 -0
  90. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/solutions/patterns/index-unique-redundancy.md +0 -0
  91. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/solutions/patterns/shadow-fk-columns.md +0 -0
  92. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/solutions/patterns/typed-null-binds.md +0 -0
  93. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/stylesheets/extra.css +0 -0
  94. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/docs/why-ferro.md +0 -0
  95. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/justfile +0 -0
  96. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/mkdocs.yml +0 -0
  97. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/scripts/demo_queries.py +0 -0
  98. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/_annotation_utils.py +0 -0
  99. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/_shadow_fk_types.py +0 -0
  100. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/base.py +0 -0
  101. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/composite_indexes.py +0 -0
  102. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/composite_uniques.py +0 -0
  103. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/fields.py +0 -0
  104. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/metaclass.py +0 -0
  105. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/migrations/__init__.py +0 -0
  106. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/migrations/alembic.py +0 -0
  107. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/py.typed +0 -0
  108. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/query/__init__.py +0 -0
  109. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/query/builder.py +0 -0
  110. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/query/nodes.py +0 -0
  111. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/raw.py +0 -0
  112. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/relations/__init__.py +0 -0
  113. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/relations/descriptors.py +0 -0
  114. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/schema_metadata.py +0 -0
  115. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/ferro/state.py +0 -0
  116. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/lib.rs +0 -0
  117. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/query.rs +0 -0
  118. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/schema.rs +0 -0
  119. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/src/state.rs +0 -0
  120. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/__init__.py +0 -0
  121. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/conftest.py +0 -0
  122. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/db_backends.py +0 -0
  123. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_aggregation.py +0 -0
  124. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_alembic_autogenerate.py +0 -0
  125. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_alembic_bridge.py +0 -0
  126. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_alembic_nullability.py +0 -0
  127. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_auto_migrate.py +0 -0
  128. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_bulk_update.py +0 -0
  129. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_composite_index.py +0 -0
  130. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_composite_unique.py +0 -0
  131. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_connection_redaction.py +0 -0
  132. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_constraints.py +0 -0
  133. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_cross_emitter_parity.py +0 -0
  134. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_db_backends.py +0 -0
  135. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_deletion.py +0 -0
  136. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_docs_examples.py +0 -0
  137. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_documentation_features.py +0 -0
  138. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_field_wrapper.py +0 -0
  139. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_helpers.py +0 -0
  140. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_hydration.py +0 -0
  141. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_metaclass_internals.py +0 -0
  142. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_metadata.py +0 -0
  143. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_models.py +0 -0
  144. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_one_to_one.py +0 -0
  145. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_query_builder.py +0 -0
  146. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_raw_sql.py +0 -0
  147. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_refresh.py +0 -0
  148. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_relationship_engine.py +0 -0
  149. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_schema.py +0 -0
  150. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_schema_constraints.py +0 -0
  151. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_schema_enum_annotations.py +0 -0
  152. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_shadow_fk_types.py +0 -0
  153. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_static_contracts.py +0 -0
  154. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_string_search.py +0 -0
  155. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_temporal_types.py +0 -0
  156. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_transactions.py +0 -0
  157. {ferro_orm-0.6.0 → ferro_orm-0.7.0}/tests/test_typed_null_binds.py +0 -0
@@ -1,6 +1,23 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.7.0 (2026-05-08)
5
+
6
+ ### Features
7
+
8
+ - Per-connection identity_map on connect ([#47](https://github.com/syn54x/ferro-orm/pull/47),
9
+ [`0a1d629`](https://github.com/syn54x/ferro-orm/commit/0a1d62926538cde14fdd4f4deece21a59a1ede69))
10
+
11
+
12
+ ## v0.6.1 (2026-05-07)
13
+
14
+ ### Refactoring
15
+
16
+ - Make ModelConnection generic to preserve model typing through .using()
17
+ ([#46](https://github.com/syn54x/ferro-orm/pull/46),
18
+ [`50d6b68`](https://github.com/syn54x/ferro-orm/commit/50d6b683059ce1d2b00942efd7267836db00eefd))
19
+
20
+
4
21
  ## v0.6.0 (2026-04-30)
5
22
 
6
23
  ### Features
@@ -79,9 +79,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
79
79
 
80
80
  [[package]]
81
81
  name = "cc"
82
- version = "1.2.61"
82
+ version = "1.2.62"
83
83
  source = "registry+https://github.com/rust-lang/crates.io-index"
84
- checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
84
+ checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
85
85
  dependencies = [
86
86
  "find-msvc-tools",
87
87
  "shlex",
@@ -294,7 +294,7 @@ dependencies = [
294
294
 
295
295
  [[package]]
296
296
  name = "ferro"
297
- version = "0.6.0"
297
+ version = "0.7.0"
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.97"
714
+ version = "0.3.98"
715
715
  source = "registry+https://github.com/rust-lang/crates.io-index"
716
- checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"
716
+ checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
717
717
  dependencies = [
718
718
  "cfg-if",
719
719
  "futures-util",
@@ -757,7 +757,7 @@ dependencies = [
757
757
  "bitflags",
758
758
  "libc",
759
759
  "plain",
760
- "redox_syscall 0.7.4",
760
+ "redox_syscall 0.7.5",
761
761
  ]
762
762
 
763
763
  [[package]]
@@ -1136,9 +1136,9 @@ dependencies = [
1136
1136
 
1137
1137
  [[package]]
1138
1138
  name = "redox_syscall"
1139
- version = "0.7.4"
1139
+ version = "0.7.5"
1140
1140
  source = "registry+https://github.com/rust-lang/crates.io-index"
1141
- checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a"
1141
+ checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b"
1142
1142
  dependencies = [
1143
1143
  "bitflags",
1144
1144
  ]
@@ -1699,9 +1699,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
1699
1699
 
1700
1700
  [[package]]
1701
1701
  name = "tokio"
1702
- version = "1.52.1"
1702
+ version = "1.52.3"
1703
1703
  source = "registry+https://github.com/rust-lang/crates.io-index"
1704
- checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
1704
+ checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
1705
1705
  dependencies = [
1706
1706
  "bytes",
1707
1707
  "libc",
@@ -1892,9 +1892,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
1892
1892
 
1893
1893
  [[package]]
1894
1894
  name = "wasm-bindgen"
1895
- version = "0.2.120"
1895
+ version = "0.2.121"
1896
1896
  source = "registry+https://github.com/rust-lang/crates.io-index"
1897
- checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
1897
+ checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
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.120"
1908
+ version = "0.2.121"
1909
1909
  source = "registry+https://github.com/rust-lang/crates.io-index"
1910
- checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
1910
+ checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
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.120"
1918
+ version = "0.2.121"
1919
1919
  source = "registry+https://github.com/rust-lang/crates.io-index"
1920
- checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
1920
+ checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
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.120"
1931
+ version = "0.2.121"
1932
1932
  source = "registry+https://github.com/rust-lang/crates.io-index"
1933
- checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
1933
+ checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
1934
1934
  dependencies = [
1935
1935
  "unicode-ident",
1936
1936
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ferro"
3
- version = "0.6.0"
3
+ version = "0.7.0"
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.6.0
3
+ Version: 0.7.0
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'
@@ -0,0 +1,192 @@
1
+ ---
2
+ title: "refactor: Make ModelConnection generic to preserve model typing through .using()"
3
+ type: refactor
4
+ status: active
5
+ date: 2026-05-07
6
+ ---
7
+
8
+ # refactor: Make ModelConnection generic to preserve model typing through .using()
9
+
10
+ ## Summary
11
+
12
+ `ModelConnection` (the object returned by `Model.using(name)`) is not generic, so every method that should return the bound model class falls back to the unbound `Model` base — `Transcript.using(SERVICE).get("...")` resolves to `Model | None` instead of `Transcript | None`. This plan parameterizes `ModelConnection` over `M: Model` using PEP 695 syntax, propagates `M` through every method return type, and renames the conflicting `self.using` instance attribute so the class isn't shadowing its own classmethod entrypoint. No behavior changes — pure typing + naming hygiene.
13
+
14
+ ---
15
+
16
+ ## Requirements
17
+
18
+ - R1. `Transcript.using(SERVICE).get(pk)` resolves to `Transcript | None` under Pyright/Pylance.
19
+ - R2. Every other `ModelConnection` method preserves the model type: `create -> M`, `all -> list[M]`, `select -> Query[M]`, `where -> Query[M]`, `bulk_create -> int`, `get_or_create -> tuple[M, bool]`, `update_or_create -> tuple[M, bool]`.
20
+ - R3. The instance attribute previously named `self.using` no longer shadows the `Model.using` classmethod name.
21
+ - R4. Existing runtime behavior is preserved — every test under `tests/test_connection.py` and `tests/test_named_connections_integration.py` continues to pass without modification.
22
+ - R5. Public surface is unchanged: `Model.using("name")` still returns a `ModelConnection`-shaped object with all the same methods and call signatures.
23
+
24
+ ---
25
+
26
+ ## Scope Boundaries
27
+
28
+ - No widening of `Model.using()` to accept transaction handles, connection objects, role hints, or anything other than the existing `name: str`.
29
+ - No audit of unrelated typing erasure elsewhere in the ORM (relation descriptors, raw queries, query builder internals). Anything discovered during this work that is out-of-scope routes to deferred follow-up rather than expanding this plan.
30
+ - No changes to `Query`, `Model`, or any FFI signatures — `Query` is already generic and that infrastructure is sufficient.
31
+ - No new type-checker wiring into CI. The repo currently has no Pyright/mypy hook (see `.pre-commit-config.yaml` lines 45–51, type checking is commented out). Adding CI enforcement for static typing is a separate, larger discussion.
32
+
33
+ ### Deferred to Follow-Up Work
34
+
35
+ - Wiring Pyright into pre-commit / CI so `assert_type` regressions fail builds: separate plan, larger scope (config, ignore lists, baseline noise).
36
+ - Audit of `relations/descriptors.py` and other Model-returning APIs for similar erasure: separate plan if/when surfaced.
37
+
38
+ ---
39
+
40
+ ## Context & Research
41
+
42
+ ### Relevant Code and Patterns
43
+
44
+ - `src/ferro/models.py` lines 452–455: `Model.using` classmethod, already annotated `-> ModelConnection[Self]` (the annotation is aspirational today — `ModelConnection` is not actually subscriptable).
45
+ - `src/ferro/models.py` lines 557–619: `ModelConnection` class definition. All eight methods to retype live here.
46
+ - `src/ferro/query/builder.py` line 29: `class Query(Generic[T])` — already generic. `select()` and `where()` should return `Query[M]` after this refactor; the constructor `Query(self.model_cls, using=...)` already infers `T` from the class argument.
47
+ - `src/ferro/relations/descriptors.py` line 112: `await self._target_model.using(origin).get(id_val)` — only external caller of `ModelConnection`'s instance-level methods we found in repo. Will benefit automatically from the typing fix; no change needed.
48
+
49
+ ### Internal Usages of `self.using` (the instance attribute being renamed)
50
+
51
+ Confirmed via grep — all references are inside `ModelConnection`'s own method bodies:
52
+
53
+ - `models.py:566` — `await instance.save(using=self.using)`
54
+ - `models.py:570` — `using=self.using`
55
+ - `models.py:573` — `Query(..., using=self.using)`
56
+ - `models.py:588` — `bulk_create(..., using=self.using)`
57
+ - `models.py:593` — `Query(..., using=self.using)`
58
+ - `models.py:607` — `Query(..., using=self.using)`
59
+ - `models.py:615` — `await instance.save(using=self.using)`
60
+
61
+ No external code reads `instance.using` on a `ModelConnection` object. Renaming is safe.
62
+
63
+ ### Institutional Learnings
64
+
65
+ - `.cursorrules` §4 — TDD workflow: write a failing test first, then implement. Applies here even though the "test" for the typing fix is a static `assert_type` rather than runtime assertion. The runtime regression test for the rename is conventional pytest.
66
+ - `AGENTS.md` I-3 — no `unwrap()` across FFI. Not applicable here (Python-only change), but reinforces "this should not need to touch Rust."
67
+ - `AGENTS.md` I-5 — `docs/solutions/` is institutional memory. Worth adding a short note under `docs/solutions/patterns/` if the PEP 695 generic syntax is the first instance in the codebase, so future agents have a reference.
68
+
69
+ ### External References
70
+
71
+ - PEP 695 (Type Parameter Syntax, Python 3.12+) — Ferro's `requires-python = ">=3.13"` makes this the modern default over `Generic[M]`. No external lookup needed; the syntax is well-known.
72
+
73
+ ---
74
+
75
+ ## Key Technical Decisions
76
+
77
+ - **Use PEP 695 syntax (`class ModelConnection[M: Model]:`) over `Generic[M]`.** Project pins Python 3.13+ (`pyproject.toml:9`), so the older syntax has no compatibility benefit and adds an import (`Generic`, `TypeVar`). PEP 695 is also what `Query` should eventually move to for consistency, but that migration is out of scope here.
78
+ - **Rename `self.using` to `self._connection_name`.** Underscore-prefix signals "internal — don't read this from outside the class," which matches how the attribute is actually used. Avoids the `Model.using` / `ModelConnection.using` name collision that prompted this work in the first place.
79
+ - **Test typing via `typing.assert_type` co-located with existing connection tests, not in a new `tests/typing/` directory.** No type checker runs in CI today, so a separate typing test directory would be a discoverability problem (no signal points at it). Inline `assert_type` calls in the existing test files make the intent visible to readers, run as no-ops at runtime, and can be picked up later if/when Pyright is wired into CI.
80
+
81
+ ---
82
+
83
+ ## Open Questions
84
+
85
+ ### Resolved During Planning
86
+
87
+ - *Should we use `Generic[M]` or PEP 695 `[M: Model]`?* — PEP 695, see Key Technical Decisions.
88
+ - *What to rename `self.using` to?* — `self._connection_name`, see Key Technical Decisions.
89
+ - *Do we need to update `src/ferro/_core.pyi`?* — No. `ModelConnection` is a pure-Python class; no FFI signatures cross.
90
+
91
+ ### Deferred to Implementation
92
+
93
+ - *Does `Query`'s existing generic propagation flow through the new `M` cleanly when `ModelConnection.select()` returns `Query[M]`?* — Should "just work" since `Query(model_cls, ...)` infers `T` from `type[T]`, and after the refactor `self.model_cls: type[M]`. Verified at implementation time by writing the `assert_type` calls and running Pyright locally.
94
+
95
+ ---
96
+
97
+ ## Implementation Units
98
+
99
+ - U1. **Add typing regression coverage for `.using()`**
100
+
101
+ **Goal:** Lock in the desired post-refactor types via `typing.assert_type` calls so the typing improvement is observable and won't silently regress.
102
+
103
+ **Requirements:** R1, R2
104
+
105
+ **Dependencies:** None.
106
+
107
+ **Files:**
108
+ - Modify: `tests/test_connection.py` (or `tests/test_named_connections_integration.py` — pick the file that already exercises the same model)
109
+ - Test: same file (typing assertions co-located with runtime tests)
110
+
111
+ **Approach:**
112
+ - Add a small block of `typing.assert_type(...)` calls against an existing test model (e.g., `ConnectionRouteMarker` or `NamedSmokeMarker`).
113
+ - Cover the full surface called out in R2: `create`, `all`, `select`, `where`, `bulk_create`, `get_or_create`, `update_or_create`, `get`.
114
+ - These are no-ops at runtime; Pyright/Pylance will flag any future regression. Document at the top of the block that they're for static type checking.
115
+
116
+ **Execution note:** Test-first per `.cursorrules` §4. Write these assertions before touching `models.py` — Pyright should report errors on every line until U2 lands.
117
+
118
+ **Patterns to follow:**
119
+ - Existing test file structure under `tests/`.
120
+
121
+ **Test scenarios:**
122
+ - Happy path: `assert_type(ConnectionRouteMarker.using("service"), ModelConnection[ConnectionRouteMarker])` — proves the classmethod's annotation is now truthful.
123
+ - Happy path: `assert_type(await ConnectionRouteMarker.using("service").get(1), ConnectionRouteMarker | None)` — covers R1 directly.
124
+ - Happy path: one `assert_type` per remaining method in R2 (`create`, `all`, `select`, `where`, `bulk_create`, `get_or_create`, `update_or_create`).
125
+ - Test expectation at runtime: these statements should execute without error. `assert_type` is a runtime no-op.
126
+
127
+ **Verification:**
128
+ - Before U2 lands: `uv run pytest` passes (assert_type doesn't fail at runtime), but Pyright on the test file reports type errors on each new line.
129
+ - After U2 lands: Pyright on the test file is clean.
130
+
131
+ ---
132
+
133
+ - U2. **Make `ModelConnection` generic and rename the connection-name attribute**
134
+
135
+ **Goal:** Parameterize `ModelConnection` over `M: Model` and rename `self.using` → `self._connection_name`.
136
+
137
+ **Requirements:** R1, R2, R3, R5
138
+
139
+ **Dependencies:** U1.
140
+
141
+ **Files:**
142
+ - Modify: `src/ferro/models.py` (lines 557–619 — the `ModelConnection` class body)
143
+
144
+ **Approach:**
145
+ - Change class header to `class ModelConnection[M: Model]:`.
146
+ - `__init__` signature: `model_cls: type[M], connection_name: str` (parameter name change is internal — callers go through `Model.using()` which positional-passes the string).
147
+ - `self.model_cls: type[M] = model_cls`; `self._connection_name: str = connection_name`.
148
+ - Replace all seven `self.using` references in method bodies with `self._connection_name`.
149
+ - Method return-type annotations:
150
+ - `create(...) -> M`
151
+ - `all() -> list[M]`
152
+ - `select() -> Query[M]`
153
+ - `where(node) -> Query[M]`
154
+ - `get(pk) -> M | None`
155
+ - `bulk_create(instances: list[M]) -> int`
156
+ - `get_or_create(...) -> tuple[M, bool]`
157
+ - `update_or_create(...) -> tuple[M, bool]`
158
+ - `Model.using` classmethod (lines 452–455) stays as-is — its `-> "ModelConnection[Self]"` annotation now resolves correctly.
159
+
160
+ **Patterns to follow:**
161
+ - `src/ferro/query/builder.py:29` (`class Query(Generic[T])`) for how a generic ORM-side class is wired, though we use the newer PEP 695 syntax here.
162
+
163
+ **Test scenarios:**
164
+ - Covers R4. Happy path: full existing `tests/test_connection.py` and `tests/test_named_connections_integration.py` suites pass unchanged. The rename is purely internal so no behavior changes.
165
+ - Covers R1, R2. Happy path: every `assert_type` call added in U1 type-checks clean under Pyright after this unit lands.
166
+ - Edge case: confirm that `Model.using("name").select().where(...).first()` chains all the way through — each link preserves `M`. Already covered indirectly by the U1 `select` and `where` assertions plus existing query tests.
167
+
168
+ **Verification:**
169
+ - `uv run pytest tests/test_connection.py tests/test_named_connections_integration.py` is green.
170
+ - `uv run maturin develop && uv run pytest` is green (full suite — confirms no relation-descriptor or other internal caller broke).
171
+ - Manually run Pyright on `tests/test_connection.py` (or whichever file got the U1 assertions): no errors on the typing block.
172
+ - Visual check: `Transcript.using(SERVICE).get("…")` in a fresh editor session shows `Transcript | None` in the hover tooltip.
173
+
174
+ ---
175
+
176
+ ## Risks & Dependencies
177
+
178
+ | Risk | Mitigation |
179
+ |------|------------|
180
+ | Renaming `self.using` accidentally breaks an external caller we missed in the grep. | Pre-flight grep confirmed all references are internal to the class body. The full pytest suite runs in U2 verification. |
181
+ | `Query[M]` doesn't propagate as cleanly as expected because `Query`'s `__init__` infers `T` differently than expected from `type[M]`. | Verified at implementation time via the `assert_type(self.select(), Query[M])` assertion in U1. If it fails, fall back to an explicit cast or constructor annotation; document the fix in `docs/solutions/patterns/`. |
182
+ | The typing improvement degrades inside generic mixins or subclasses of `Model` due to `Self` interaction. | `Model.using` returns `ModelConnection[Self]`; this is the standard pattern and shouldn't fight `M: Model`. If a subclass-specific issue surfaces, scope the fix to that subclass — don't expand U2. |
183
+
184
+ ---
185
+
186
+ ## Sources & References
187
+
188
+ - Brainstorm conversation in this session establishing scope (generic + rename, no broader audit).
189
+ - `src/ferro/models.py:452-619` — current `Model.using` and `ModelConnection` definitions.
190
+ - `src/ferro/query/builder.py:29` — existing generic ORM class as reference pattern.
191
+ - `pyproject.toml:9` — `requires-python = ">=3.13"`, justifying PEP 695 syntax.
192
+ - PEP 695 — Type Parameter Syntax.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ferro-orm"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  description = "A high-performance, Rust-backed ORM for Python."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -30,6 +30,8 @@ impl BackendKind {
30
30
  pub struct EngineHandle {
31
31
  backend: BackendKind,
32
32
  pool: BackendPool,
33
+ /// When false, Ferro skips the identity map for this connection (no lookup/register on load).
34
+ identity_map_enabled: bool,
33
35
  }
34
36
 
35
37
  #[derive(Clone, Debug)]
@@ -116,6 +118,7 @@ impl EngineHandle {
116
118
  Self {
117
119
  backend: BackendKind::Sqlite,
118
120
  pool: BackendPool::Sqlite(Arc::new(pool)),
121
+ identity_map_enabled: true,
119
122
  }
120
123
  }
121
124
 
@@ -123,9 +126,23 @@ impl EngineHandle {
123
126
  Self {
124
127
  backend: BackendKind::Postgres,
125
128
  pool: BackendPool::Postgres(Arc::new(pool)),
129
+ identity_map_enabled: true,
126
130
  }
127
131
  }
128
132
 
133
+ /// Returns whether this connection uses the identity map (singleton instances per PK).
134
+ #[must_use]
135
+ pub fn is_identity_map_enabled(&self) -> bool {
136
+ self.identity_map_enabled
137
+ }
138
+
139
+ /// Sets identity-map behavior for this handle (used by `connect(identity_map=...)`).
140
+ #[must_use]
141
+ pub fn with_identity_map_enabled(mut self, enabled: bool) -> Self {
142
+ self.identity_map_enabled = enabled;
143
+ self
144
+ }
145
+
129
146
  pub fn backend(&self) -> BackendKind {
130
147
  self.backend
131
148
  }
@@ -187,7 +187,7 @@ async fn connect_engine_handle(
187
187
  /// # Errors
188
188
  /// Returns a `PyErr` if the connection fails or if auto-migration fails.
189
189
  #[pyfunction]
190
- #[pyo3(signature = (url, auto_migrate=false, name=None, default=false, max_connections=5, min_connections=0))]
190
+ #[pyo3(signature = (url, auto_migrate=false, name=None, default=false, max_connections=5, min_connections=0, identity_map=true))]
191
191
  pub fn connect(
192
192
  py: Python<'_>,
193
193
  url: String,
@@ -196,6 +196,7 @@ pub fn connect(
196
196
  default: bool,
197
197
  max_connections: u32,
198
198
  min_connections: u32,
199
+ identity_map: bool,
199
200
  ) -> PyResult<Bound<'_, PyAny>> {
200
201
  let (connection_url, search_path) = split_search_path(&url);
201
202
  let redacted_url = redact_connection_url(&connection_url);
@@ -247,7 +248,8 @@ pub fn connect(
247
248
  "DB Connection failed for {}: {}",
248
249
  redacted_url, e
249
250
  ))
250
- })?;
251
+ })?
252
+ .with_identity_map_enabled(identity_map);
251
253
 
252
254
  let engine_handle = Arc::new(engine_handle);
253
255
 
@@ -60,6 +60,8 @@ async def connect(
60
60
  name: str | None = None,
61
61
  default: bool = False,
62
62
  pool: PoolConfig | None = None,
63
+ *,
64
+ identity_map: bool = True,
63
65
  ) -> None:
64
66
  """
65
67
  Establish a connection to the database.
@@ -70,6 +72,9 @@ async def connect(
70
72
  name: Optional connection name. Omitted connections register as "default".
71
73
  default: If True, make this named connection the default for unqualified operations.
72
74
  pool: Optional per-connection pool configuration.
75
+ identity_map: If True (default), keep a per-connection identity map so the same primary
76
+ key maps to a single Python instance. If False, each load returns fresh instances and
77
+ the map is not consulted (lower memory use; no ``a is b`` guarantees across loads).
73
78
  """
74
79
  from .relations import resolve_relationships
75
80
 
@@ -83,6 +88,7 @@ async def connect(
83
88
  default=default,
84
89
  max_connections=pool_config.max_connections,
85
90
  min_connections=pool_config.min_connections,
91
+ identity_map=identity_map,
86
92
  )
87
93
 
88
94
 
@@ -8,6 +8,8 @@ async def connect(
8
8
  default: bool = False,
9
9
  max_connections: int = 5,
10
10
  min_connections: int = 0,
11
+ *,
12
+ identity_map: bool = True,
11
13
  ) -> None: ...
12
14
  async def create_tables(using: Optional[str] = None) -> None: ...
13
15
  async def fetch_all(
@@ -554,28 +554,33 @@ class Model(BaseModel, metaclass=ModelMetaclass):
554
554
  return await cls.create(**params), True
555
555
 
556
556
 
557
- class ModelConnection:
558
- """Connection-bound ORM entrypoint returned by ``Model.using(name)``."""
557
+ class ModelConnection[M: Model]:
558
+ """Connection-bound ORM entrypoint returned by ``Model.using(name)``.
559
559
 
560
- def __init__(self, model_cls: type[Model], using: str) -> None:
561
- self.model_cls = model_cls
562
- self.using = using
560
+ Generic over the concrete model class so that every accessor preserves
561
+ the bound type — e.g. ``Transcript.using("service").get(pk)`` resolves
562
+ to ``Transcript | None`` rather than ``Model | None``.
563
+ """
564
+
565
+ def __init__(self, model_cls: type[M], connection_name: str) -> None:
566
+ self.model_cls: type[M] = model_cls
567
+ self._connection_name: str = connection_name
563
568
 
564
- async def create(self, **fields: Any) -> Model:
569
+ async def create(self, **fields: Any) -> M:
565
570
  instance = self.model_cls(**fields)
566
- await instance.save(using=self.using)
571
+ await instance.save(using=self._connection_name)
567
572
  return instance
568
573
 
569
- async def all(self) -> list[Model]:
570
- return await self.model_cls.all(using=self.using)
574
+ async def all(self) -> list[M]:
575
+ return await self.model_cls.all(using=self._connection_name)
571
576
 
572
- def select(self) -> Query[Model]:
573
- return Query(self.model_cls, using=self.using)
577
+ def select(self) -> Query[M]:
578
+ return Query(self.model_cls, using=self._connection_name)
574
579
 
575
- def where(self, node: QueryNode) -> Query[Model]:
580
+ def where(self, node: QueryNode) -> Query[M]:
576
581
  return self.select().where(node)
577
582
 
578
- async def get(self, pk: Any) -> Model | None:
583
+ async def get(self, pk: Any) -> M | None:
579
584
  pk_field_name = self.model_cls._primary_key_field_name()
580
585
  if pk_field_name is None:
581
586
  raise RuntimeError(
@@ -584,13 +589,13 @@ class ModelConnection:
584
589
 
585
590
  return await self.where(getattr(self.model_cls, pk_field_name) == pk).first()
586
591
 
587
- async def bulk_create(self, instances: list[Model]) -> int:
588
- return await self.model_cls.bulk_create(instances, using=self.using)
592
+ async def bulk_create(self, instances: list[M]) -> int:
593
+ return await self.model_cls.bulk_create(instances, using=self._connection_name)
589
594
 
590
595
  async def get_or_create(
591
596
  self, defaults: dict[str, Any] | None = None, **fields: Any
592
- ) -> tuple[Model, bool]:
593
- query = Query(self.model_cls, using=self.using)
597
+ ) -> tuple[M, bool]:
598
+ query = Query(self.model_cls, using=self._connection_name)
594
599
  for key, val in fields.items():
595
600
  query = query.where(getattr(self.model_cls, key) == val)
596
601
 
@@ -603,8 +608,8 @@ class ModelConnection:
603
608
 
604
609
  async def update_or_create(
605
610
  self, defaults: dict[str, Any] | None = None, **fields: Any
606
- ) -> tuple[Model, bool]:
607
- query = Query(self.model_cls, using=self.using)
611
+ ) -> tuple[M, bool]:
612
+ query = Query(self.model_cls, using=self._connection_name)
608
613
  for key, val in fields.items():
609
614
  query = query.where(getattr(self.model_cls, key) == val)
610
615
 
@@ -612,7 +617,7 @@ class ModelConnection:
612
617
  if instance:
613
618
  for key, val in (defaults or {}).items():
614
619
  setattr(instance, key, val)
615
- await instance.save(using=self.using)
620
+ await instance.save(using=self._connection_name)
616
621
  return instance, False
617
622
 
618
623
  params = {**fields, **(defaults or {})}