ferro-orm 0.3.2__tar.gz → 0.3.3__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 (131) hide show
  1. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/workflows/ci.yml +56 -1
  2. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/CHANGELOG.md +51 -0
  3. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/Cargo.lock +7 -7
  4. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/Cargo.toml +1 -1
  5. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/PKG-INFO +3 -1
  6. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/README.md +2 -0
  7. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/api/utilities.md +5 -10
  8. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/concepts/performance.md +3 -6
  9. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/faq.md +4 -7
  10. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/getting-started/installation.md +4 -8
  11. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/guide/database.md +13 -35
  12. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/guide/models-and-fields.md +1 -1
  13. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/howto/multiple-databases.md +1 -1
  14. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/howto/testing.md +66 -9
  15. ferro_orm-0.3.3/docs/plans/2026-04-24-001-refactor-multi-db-backend-architecture-plan.md +473 -0
  16. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/pyproject.toml +3 -1
  17. ferro_orm-0.3.3/src/backend.rs +108 -0
  18. ferro_orm-0.3.3/src/connection.rs +128 -0
  19. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/_core.pyi +4 -2
  20. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/_shadow_fk_types.py +30 -0
  21. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/metaclass.py +2 -48
  22. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/migrations/alembic.py +2 -45
  23. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/models.py +29 -31
  24. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/query/nodes.py +14 -7
  25. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/relations/__init__.py +9 -36
  26. ferro_orm-0.3.3/src/ferro/schema_metadata.py +138 -0
  27. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/lib.rs +1 -0
  28. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/operations.rs +733 -210
  29. ferro_orm-0.3.3/src/query.rs +282 -0
  30. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/schema.rs +141 -34
  31. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/state.rs +49 -4
  32. ferro_orm-0.3.3/tests/__init__.py +1 -0
  33. ferro_orm-0.3.3/tests/conftest.py +182 -0
  34. ferro_orm-0.3.3/tests/db_backends.py +67 -0
  35. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_aggregation.py +1 -10
  36. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_alembic_bridge.py +23 -0
  37. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_alembic_nullability.py +1 -0
  38. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_auto_migrate.py +8 -6
  39. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_bulk_update.py +1 -10
  40. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_composite_unique.py +61 -13
  41. ferro_orm-0.3.3/tests/test_connection.py +37 -0
  42. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_constraints.py +33 -14
  43. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_crud.py +4 -18
  44. ferro_orm-0.3.3/tests/test_db_backends.py +77 -0
  45. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_deletion.py +1 -10
  46. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_documentation_features.py +3 -15
  47. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_field_wrapper.py +15 -10
  48. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_helpers.py +1 -10
  49. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_hydration.py +1 -11
  50. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_metadata.py +1 -9
  51. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_one_to_one.py +60 -20
  52. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_query_builder.py +1 -10
  53. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_refresh.py +1 -10
  54. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_schema.py +4 -3
  55. ferro_orm-0.3.3/tests/test_schema_constraints.py +137 -0
  56. ferro_orm-0.3.3/tests/test_schema_enum_annotations.py +26 -0
  57. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_shadow_fk_types.py +10 -8
  58. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_string_search.py +1 -10
  59. ferro_orm-0.3.3/tests/test_structural_types.py +277 -0
  60. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_temporal_types.py +1 -10
  61. ferro_orm-0.3.3/tests/test_transactions.py +161 -0
  62. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/uv.lock +51 -1
  63. ferro_orm-0.3.2/src/connection.rs +0 -68
  64. ferro_orm-0.3.2/src/query.rs +0 -112
  65. ferro_orm-0.3.2/tests/conftest.py +0 -48
  66. ferro_orm-0.3.2/tests/test_connection.py +0 -27
  67. ferro_orm-0.3.2/tests/test_schema_constraints.py +0 -69
  68. ferro_orm-0.3.2/tests/test_structural_types.py +0 -106
  69. ferro_orm-0.3.2/tests/test_transactions.py +0 -93
  70. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  71. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  72. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/PERMISSIONS.md +0 -0
  73. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/PYPI_CHECKLIST.md +0 -0
  74. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/PYPI_SETUP.md +0 -0
  75. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/generated/wheels.generated.yml +0 -0
  76. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/pull_request_template.md +0 -0
  77. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/workflows/packaging-smoke.yml +0 -0
  78. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/workflows/publish-docs.yml +0 -0
  79. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/workflows/publish.yml +0 -0
  80. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.github/workflows/release.yml +0 -0
  81. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.gitignore +0 -0
  82. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.pre-commit-config.yaml +0 -0
  83. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/.python-version +0 -0
  84. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/CONTRIBUTING.md +0 -0
  85. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/LICENSE +0 -0
  86. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/TEST_RESULTS.md +0 -0
  87. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/api/fields.md +0 -0
  88. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/api/model.md +0 -0
  89. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/api/query.md +0 -0
  90. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/api/relationships.md +0 -0
  91. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/api/transactions.md +0 -0
  92. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/changelog.md +0 -0
  93. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/coming-soon.md +0 -0
  94. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/concepts/architecture.md +0 -0
  95. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/concepts/identity-map.md +0 -0
  96. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/concepts/type-safety.md +0 -0
  97. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/contributing.md +0 -0
  98. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/getting-started/next-steps.md +0 -0
  99. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/getting-started/tutorial.md +0 -0
  100. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/guide/migrations.md +0 -0
  101. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/guide/mutations.md +0 -0
  102. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/guide/queries.md +0 -0
  103. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/guide/relationships.md +0 -0
  104. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/guide/transactions.md +0 -0
  105. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/howto/pagination.md +0 -0
  106. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/howto/soft-deletes.md +0 -0
  107. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/howto/timestamps.md +0 -0
  108. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/index.md +0 -0
  109. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/migration-sqlalchemy.md +0 -0
  110. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/stylesheets/extra.css +0 -0
  111. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/docs/why-ferro.md +0 -0
  112. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/justfile +0 -0
  113. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/mkdocs.yml +0 -0
  114. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/scripts/demo_queries.py +0 -0
  115. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/__init__.py +0 -0
  116. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/_annotation_utils.py +0 -0
  117. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/base.py +0 -0
  118. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/composite_uniques.py +0 -0
  119. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/fields.py +0 -0
  120. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/migrations/__init__.py +0 -0
  121. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/py.typed +0 -0
  122. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/query/__init__.py +0 -0
  123. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/query/builder.py +0 -0
  124. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/relations/descriptors.py +0 -0
  125. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/src/ferro/state.py +0 -0
  126. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_alembic_autogenerate.py +0 -0
  127. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_alembic_type_mapping.py +0 -0
  128. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_docs_examples.py +0 -0
  129. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_metaclass_internals.py +0 -0
  130. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_models.py +0 -0
  131. {ferro_orm-0.3.2 → ferro_orm-0.3.3}/tests/test_relationship_engine.py +0 -0
@@ -171,6 +171,60 @@ jobs:
171
171
  name: python-${{ matrix.python-version }}
172
172
  continue-on-error: true
173
173
 
174
+ test-python-backend-matrix:
175
+ name: Python backend matrix (SQLite + Postgres)
176
+ runs-on: ubuntu-latest
177
+ services:
178
+ postgres:
179
+ image: postgres:17
180
+ env:
181
+ POSTGRES_USER: ferro
182
+ POSTGRES_PASSWORD: ferro
183
+ POSTGRES_DB: ferro
184
+ ports:
185
+ - 5432:5432
186
+ options: >-
187
+ --health-cmd "pg_isready -U ferro -d ferro"
188
+ --health-interval 10s
189
+ --health-timeout 5s
190
+ --health-retries 5
191
+ env:
192
+ FERRO_SUPABASE_URL: postgresql://ferro:ferro@127.0.0.1:5432/ferro?sslmode=disable
193
+ steps:
194
+ - name: Checkout repository
195
+ uses: actions/checkout@v4
196
+
197
+ - name: Set up Python
198
+ uses: actions/setup-python@v5
199
+ with:
200
+ python-version: '3.13'
201
+
202
+ - name: Install UV
203
+ uses: astral-sh/setup-uv@v5
204
+ with:
205
+ enable-cache: true
206
+
207
+ - name: Set up Rust
208
+ uses: dtolnay/rust-toolchain@stable
209
+
210
+ - name: Cache Rust build
211
+ uses: Swatinem/rust-cache@v2
212
+ with:
213
+ prefix-key: v1
214
+ cache-on-failure: true
215
+
216
+ - name: Install dependencies
217
+ run: |
218
+ uv sync --only-group ci-test --no-install-project --python 3.13
219
+
220
+ - name: Build Rust extension
221
+ run: |
222
+ uv run maturin develop
223
+
224
+ - name: Run backend matrix tests
225
+ run: |
226
+ uv run pytest -v -m "backend_matrix or postgres_only" --db-backends=sqlite,postgres
227
+
174
228
  check-conventional-commits:
175
229
  name: Check Conventional Commits
176
230
  runs-on: ubuntu-latest
@@ -220,7 +274,7 @@ jobs:
220
274
 
221
275
  all-checks:
222
276
  name: All Checks Passed
223
- needs: [lint-and-format, test-python-pr, test-python-main, test-rust]
277
+ needs: [lint-and-format, test-python-pr, test-python-main, test-python-backend-matrix, test-rust]
224
278
  runs-on: ubuntu-latest
225
279
  if: always()
226
280
  steps:
@@ -232,6 +286,7 @@ jobs:
232
286
  if ! ok "${{ needs.lint-and-format.result }}"; then exit 1; fi
233
287
  if ! ok "${{ needs.test-python-pr.result }}"; then exit 1; fi
234
288
  if ! ok "${{ needs.test-python-main.result }}"; then exit 1; fi
289
+ if ! ok "${{ needs.test-python-backend-matrix.result }}"; then exit 1; fi
235
290
  if ! ok "${{ needs.test-rust.result }}"; then exit 1; fi
236
291
 
237
292
  echo "All checks passed!"
@@ -1,6 +1,57 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.3.3 (2026-04-24)
5
+
6
+ ### Bug Fixes
7
+
8
+ - Cast NULL and strings to ::uuid for Postgres using catalog
9
+ ([`f5cb4f0`](https://github.com/syn54x/ferro-orm/commit/f5cb4f08ceaf0763a29c3b78d4d077ca1119fc1c))
10
+
11
+ - Catalog casts for date/timestamp columns on Postgres
12
+ ([`95ef5ca`](https://github.com/syn54x/ferro-orm/commit/95ef5cadc28eb26481c38b51dbca1b370a883d10))
13
+
14
+ - Clean up rebase conflicts with main
15
+ ([`716511c`](https://github.com/syn54x/ferro-orm/commit/716511c829021ee6d2390bb85c877e670c1d7631))
16
+
17
+ - Enum OIDs
18
+ ([`a9867be`](https://github.com/syn54x/ferro-orm/commit/a9867beac242a9d630aeb7e49b718a4234c541ec))
19
+
20
+ - Postgres native enums on save and StrEnum schema registration
21
+ ([`44277e1`](https://github.com/syn54x/ferro-orm/commit/44277e1922182b020c17d9a7a2a9e99dd62061e5))
22
+
23
+ - Use Postgres SQL dialect when connecting to postgres URLs
24
+ ([`c627ac8`](https://github.com/syn54x/ferro-orm/commit/c627ac8e4fa84555e0cc7250f73ce6f0858125a3))
25
+
26
+ - **postgres**: Add dual-db ORM test matrix
27
+ ([`1fa657f`](https://github.com/syn54x/ferro-orm/commit/1fa657fe4335d41214fcb24b1eac5dcf3138273f))
28
+
29
+ - **postgres**: Bind boolean writes as booleans
30
+ ([`346441a`](https://github.com/syn54x/ferro-orm/commit/346441a073a540c857a8aaa67bf4029cb4099535))
31
+
32
+ - **postgres**: Cast uuid columns to text in SELECT for Any decode
33
+ ([`df957c0`](https://github.com/syn54x/ferro-orm/commit/df957c0202d32608843d6a24ae4c924ed5b9381d))
34
+
35
+ - **postgres**: Cast UUID filter params for sqlx Any compatibility
36
+ ([`889cf8b`](https://github.com/syn54x/ferro-orm/commit/889cf8b61131c2d53e8414a76ca7b2dbc7868c23))
37
+
38
+ - **postgres**: Decode native enum columns via text cast
39
+ ([`1270f9d`](https://github.com/syn54x/ferro-orm/commit/1270f9dcd1cc5aa19cf484c3d9c3bb3a82255a05))
40
+
41
+ ### Refactoring
42
+
43
+ - Expand db matrix coverage and harden postgres paths
44
+ ([`b82f3ac`](https://github.com/syn54x/ferro-orm/commit/b82f3ac886459861cdfde122b99b880b85c09a61))
45
+
46
+ - Multi db architecture with true sqlite and postgres support
47
+ ([`459a0c5`](https://github.com/syn54x/ferro-orm/commit/459a0c5f9c8a95ecacc9ba552137252d34de4824))
48
+
49
+ ### Testing
50
+
51
+ - Expand schema constraints into db matrix
52
+ ([`24a7f0a`](https://github.com/syn54x/ferro-orm/commit/24a7f0ad38b90e98a41cf32fe2777d988ff7047f))
53
+
54
+
4
55
  ## v0.3.2 (2026-04-24)
5
56
 
6
57
  ### Bug Fixes
@@ -79,9 +79,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
79
79
 
80
80
  [[package]]
81
81
  name = "cc"
82
- version = "1.2.60"
82
+ version = "1.2.61"
83
83
  source = "registry+https://github.com/rust-lang/crates.io-index"
84
- checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
84
+ checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
85
85
  dependencies = [
86
86
  "find-msvc-tools",
87
87
  "shlex",
@@ -128,9 +128,9 @@ dependencies = [
128
128
 
129
129
  [[package]]
130
130
  name = "crc-catalog"
131
- version = "2.4.0"
131
+ version = "2.5.0"
132
132
  source = "registry+https://github.com/rust-lang/crates.io-index"
133
- checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
133
+ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
134
134
 
135
135
  [[package]]
136
136
  name = "crossbeam-queue"
@@ -294,7 +294,7 @@ dependencies = [
294
294
 
295
295
  [[package]]
296
296
  name = "ferro"
297
- version = "0.3.2"
297
+ version = "0.3.3"
298
298
  dependencies = [
299
299
  "dashmap",
300
300
  "once_cell",
@@ -1191,9 +1191,9 @@ dependencies = [
1191
1191
 
1192
1192
  [[package]]
1193
1193
  name = "rustls-pki-types"
1194
- version = "1.14.0"
1194
+ version = "1.14.1"
1195
1195
  source = "registry+https://github.com/rust-lang/crates.io-index"
1196
- checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
1196
+ checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
1197
1197
  dependencies = [
1198
1198
  "zeroize",
1199
1199
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ferro"
3
- version = "0.3.2"
3
+ version = "0.3.3"
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.3.2
3
+ Version: 0.3.3
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'
@@ -54,6 +54,8 @@ pip install ferro-orm
54
54
  pip install "ferro-orm[alembic]"
55
55
  ```
56
56
 
57
+ Ferro currently supports SQLite and PostgreSQL as runtime backends. Named multi-database routing and custom connection-pool kwargs are planned, but not part of the current public API.
58
+
57
59
  ## Quick Start
58
60
 
59
61
  ```python
@@ -41,6 +41,8 @@ pip install ferro-orm
41
41
  pip install "ferro-orm[alembic]"
42
42
  ```
43
43
 
44
+ Ferro currently supports SQLite and PostgreSQL as runtime backends. Named multi-database routing and custom connection-pool kwargs are planned, but not part of the current public API.
45
+
44
46
  ## Quick Start
45
47
 
46
48
  ```python
@@ -17,24 +17,19 @@ await connect("sqlite:example.db?mode=rwc")
17
17
  # PostgreSQL
18
18
  await connect("postgresql://user:password@localhost/dbname")
19
19
 
20
- # With options
21
- await connect(
22
- "postgresql://localhost/dbname",
23
- max_connections=20,
24
- auto_migrate=True # Development only
25
- )
20
+ # Auto-migrate during development
21
+ await connect("postgresql://localhost/dbname", auto_migrate=True)
26
22
  ```
27
23
 
28
24
  See [Database Setup Guide](../guide/database.md) for complete connection options.
29
25
 
30
26
  ### disconnect()
31
27
 
32
- Close the database connection.
28
+ This function is not implemented yet.
33
29
 
34
30
  ```python
35
- from ferro import disconnect
36
-
37
- await disconnect()
31
+ # Current pattern: connect once during startup
32
+ await connect("sqlite:example.db?mode=rwc")
38
33
  ```
39
34
 
40
35
  ### create_tables()
@@ -137,14 +137,11 @@ for post in posts:
137
137
  posts = await Post.select().prefetch_related("author").all()
138
138
  ```
139
139
 
140
- ### 6. Use Connection Pooling
140
+ ### 6. Reuse a Long-Lived Connection
141
141
 
142
142
  ```python
143
- await ferro.connect(
144
- "postgresql://localhost/db",
145
- max_connections=50, # Tune for your load
146
- min_connections=10
147
- )
143
+ # Current API: connect once during startup and reuse it.
144
+ await ferro.connect("postgresql://localhost/db")
148
145
  ```
149
146
 
150
147
  ### 7. Keep Transactions Short
@@ -137,7 +137,7 @@ Check your Ferro version's API for raw SQL support. Most versions provide an esc
137
137
 
138
138
  ### Does Ferro support multiple databases?
139
139
 
140
- Multi-database support varies by version. Check your version's documentation for `using()` or similar APIs.
140
+ Not yet. Ferro currently supports a single active database connection per application process.
141
141
 
142
142
  See [How-To: Multiple Databases](howto/multiple-databases.md).
143
143
 
@@ -198,14 +198,11 @@ Check the error message for details.
198
198
  ### How do I reset the database?
199
199
 
200
200
  ```python
201
- # Drop all tables
202
- await ferro.drop_all_tables()
203
-
204
- # Recreate
205
- await ferro.create_tables()
201
+ # Reconnect to a fresh SQLite test database
202
+ await ferro.connect("sqlite::memory:", auto_migrate=True)
206
203
  ```
207
204
 
208
- Or use Alembic migrations:
205
+ For persistent environments, use Alembic migrations:
209
206
 
210
207
  ```bash
211
208
  alembic downgrade base
@@ -4,7 +4,7 @@
4
4
 
5
5
  - Python 3.10 or higher
6
6
  - Supported platforms: macOS, Linux, Windows
7
- - Database: SQLite, PostgreSQL, or MySQL
7
+ - Database: SQLite or PostgreSQL
8
8
 
9
9
  ## Install Ferro
10
10
 
@@ -30,7 +30,7 @@ This installs Alembic and SQLAlchemy (used only for migration generation, not at
30
30
 
31
31
  ## Database Drivers
32
32
 
33
- Ferro uses SQLx under the hood, which includes drivers for all supported databases. No additional database-specific packages are required.
33
+ Ferro uses SQLx under the hood. SQLite and PostgreSQL support are built into Ferro's published packages, so no additional database-specific packages are required for those backends.
34
34
 
35
35
  ### SQLite
36
36
 
@@ -40,10 +40,6 @@ No additional setup needed. SQLite is embedded in Ferro.
40
40
 
41
41
  No additional setup needed. PostgreSQL support is built into Ferro.
42
42
 
43
- ### MySQL
44
-
45
- No additional setup needed. MySQL/MariaDB support is built into Ferro.
46
-
47
43
 
48
44
  ## Optional Dependencies
49
45
 
@@ -52,10 +48,10 @@ No additional setup needed. MySQL/MariaDB support is built into Ferro.
52
48
  For running tests and linting:
53
49
 
54
50
  ```bash
55
- pip install "ferro-orm[dev]"
51
+ uv sync --group dev
56
52
  ```
57
53
 
58
- This includes pytest, ruff, mypy, and other development tools.
54
+ This workspace group includes pytest, maturin, docs tooling, and other development dependencies used in this repository.
59
55
 
60
56
  ## Building from Source
61
57
 
@@ -15,7 +15,7 @@ async def main():
15
15
 
16
16
  ## Connection Strings
17
17
 
18
- Ferro supports SQLite, PostgreSQL, and MySQL. The connection string format follows standard URL patterns:
18
+ Ferro currently supports SQLite and PostgreSQL. The connection string format follows standard URL patterns:
19
19
 
20
20
  ### SQLite
21
21
 
@@ -45,10 +45,10 @@ await ferro.connect("postgresql://user:password@localhost:5432/dbname")
45
45
  # With SSL
46
46
  await ferro.connect("postgresql://user:password@localhost:5432/dbname?sslmode=require")
47
47
 
48
- # Connection pooling (custom pool size)
48
+ # Development connection with auto-migrate
49
49
  await ferro.connect(
50
50
  "postgresql://user:password@localhost:5432/dbname",
51
- max_connections=20
51
+ auto_migrate=True,
52
52
  )
53
53
  ```
54
54
 
@@ -83,16 +83,6 @@ Supabase’s pooler hostname often looks like `*.pooler.supabase.com`; the datab
83
83
 
84
84
  If you assemble the URI yourself, percent-encode reserved characters in the password (for example `%24` for `$`, `%5E` for `^`) per [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1) userinfo rules. Many drivers accept unencoded passwords until one character breaks parsing; encoding avoids surprises.
85
85
 
86
- ### MySQL
87
-
88
- ```python
89
- # Basic connection
90
- await ferro.connect("mysql://user:password@localhost:3306/dbname")
91
-
92
- # With charset
93
- await ferro.connect("mysql://user:password@localhost:3306/dbname?charset=utf8mb4")
94
- ```
95
-
96
86
  ## Connection Options
97
87
 
98
88
  ### Auto-Migration (Development)
@@ -166,8 +156,7 @@ DATABASE_URL = os.getenv(
166
156
  async def init_db():
167
157
  await connect(
168
158
  DATABASE_URL,
169
- max_connections=int(os.getenv("DB_POOL_SIZE", "10")),
170
- connect_timeout=int(os.getenv("DB_TIMEOUT", "30"))
159
+ auto_migrate=os.getenv("ENV") != "production"
171
160
  )
172
161
  ```
173
162
 
@@ -207,12 +196,12 @@ async def on_shutdown():
207
196
  !!! note "disconnect() Not Available"
208
197
  The `disconnect()` function is not yet implemented. Connection cleanup happens automatically on process exit. See [Coming Soon](../coming-soon.md#disconnect) for more information.
209
198
 
210
- ### Use Connection Pooling
199
+ ### Use One Long-Lived Connection
211
200
 
212
201
  !!! note
213
- Advanced connection pool parameters may not be fully supported. See [Coming Soon](../coming-soon.md#connection-pool-configuration).
202
+ Advanced pool configuration such as `max_connections`, `min_connections`, and `connect_timeout` is not exposed by Ferro's current Python API. See [Coming Soon](../coming-soon.md#connection-pool-configuration).
214
203
 
215
- For web applications with basic connection support:
204
+ For web applications, connect once at startup and reuse that engine:
216
205
 
217
206
  ```python
218
207
  # Basic connection for production
@@ -225,10 +214,7 @@ await ferro.connect("postgresql://localhost/proddb")
225
214
  import os
226
215
 
227
216
  if os.getenv("ENV") == "production":
228
- await ferro.connect(
229
- "postgresql://prodhost/proddb",
230
- max_connections=50
231
- )
217
+ await ferro.connect("postgresql://prodhost/proddb")
232
218
  else:
233
219
  await ferro.connect(
234
220
  "sqlite:dev.db?mode=rwc",
@@ -255,7 +241,6 @@ except Exception as e:
255
241
  # Error: Connection refused at localhost:5432
256
242
  # Solution: Check database is running
257
243
  # PostgreSQL: sudo service postgresql start
258
- # MySQL: sudo service mysql start
259
244
  ```
260
245
 
261
246
  ### Authentication Failed
@@ -285,22 +270,15 @@ Ferro’s default build enables PostgreSQL TLS via SQLx (`tls-rustls-ring-webpki
285
270
 
286
271
  If the server requires TLS but the URL omits it, add `?sslmode=require` (or `&sslmode=require` after other query parameters) as shown in the Supabase subsection above.
287
272
 
288
- ### Pool Exhaustion
273
+ ### Unsupported connect() kwargs
289
274
 
290
275
  ```python
291
- # Error: Too many connections
292
- # Solution: Increase max_connections or fix connection leaks
293
- await ferro.connect(
294
- "postgresql://localhost/dbname",
295
- max_connections=100 # Increase pool size
296
- )
297
-
298
- # Also ensure connections are released:
299
- # - Use context managers (async with)
300
- # - Close connections after use
301
- # - Fix stuck transactions
276
+ # Example of kwargs Ferro does not currently accept:
277
+ # await ferro.connect("postgresql://localhost/dbname", max_connections=100)
302
278
  ```
303
279
 
280
+ If you need custom pool sizing or timeout controls today, Ferro does not expose them yet through `connect()`.
281
+
304
282
  ## See Also
305
283
 
306
284
  - [Schema Management](migrations.md) - Alembic migrations
@@ -155,7 +155,7 @@ class OrgMembership(Model):
155
155
  - You can list several groups for multiple composite uniques on one model.
156
156
  - Invalid or unknown column names raise when the model is registered.
157
157
 
158
- **Null semantics (SQLite):** With the default local SQLite engine, `UNIQUE` treats `NULL` as distinct from other `NULL` values for multi-column constraints unless columns are `NOT NULL`. Ferro maps nullability from your types and defaults like other fields; optional composite columns can therefore allow multiple rows that differ only by `NULL` in a nullable column. Prefer `NOT NULL` on composite members when you need strict “at most one row per pair” semantics. Other databases can differ; consult your backends documentation when you target PostgreSQL, MySQL, and so on.
158
+ **Null semantics (SQLite):** With the default local SQLite engine, `UNIQUE` treats `NULL` as distinct from other `NULL` values for multi-column constraints unless columns are `NOT NULL`. Ferro maps nullability from your types and defaults like other fields; optional composite columns can therefore allow multiple rows that differ only by `NULL` in a nullable column. Prefer `NOT NULL` on composite members when you need strict “at most one row per pair” semantics. Other databases can differ; consult your backend's documentation when you target PostgreSQL or another backend with different unique/null behavior.
159
159
 
160
160
  **Wire format:** Declarations use nested tuples in Python; the schema JSON sent to the Rust engine uses nested lists (`ferro_composite_uniques`) because JSON has no tuple type.
161
161
 
@@ -3,7 +3,7 @@
3
3
  !!! warning "Feature Not Implemented"
4
4
  **Multi-database support is not currently available in Ferro.** This documentation describes planned features. See [Coming Soon](../coming-soon.md#multiple-database-support) for more information.
5
5
 
6
- Ferro currently supports only a single database connection per application. The examples below show the planned API.
6
+ Ferro currently supports only a single active database connection per application process. The examples below show the planned API, not something you can call today.
7
7
 
8
8
  ---
9
9
 
@@ -2,6 +2,64 @@
2
2
 
3
3
  Test your Ferro applications with pytest and test database isolation strategies.
4
4
 
5
+ ## Ferro Test Matrix
6
+
7
+ The repository test suite supports two database modes:
8
+
9
+ - **Default SQLite run** for the full fast suite
10
+ - **Dual-backend matrix** for ORM coverage on both SQLite and PostgreSQL/Supabase
11
+
12
+ The matrix is opt-in so day-to-day test runs stay quick and deterministic.
13
+
14
+ ### Local Setup
15
+
16
+ Install the development dependencies used by the matrix:
17
+
18
+ ```bash
19
+ uv sync --group dev
20
+ uv run maturin develop
21
+ ```
22
+
23
+ Set `FERRO_SUPABASE_URL` to a PostgreSQL connection string. A root `.env` file works well for local development:
24
+
25
+ ```bash
26
+ FERRO_SUPABASE_URL='postgresql://...'
27
+ ```
28
+
29
+ The Postgres matrix reads `FERRO_SUPABASE_URL` from either the environment or the project `.env` file. Tests create a dedicated schema per test and use that schema as the search path so one shared Supabase database can still run isolated tests safely.
30
+
31
+ ### Run The Default Suite
32
+
33
+ Run the normal SQLite-first suite:
34
+
35
+ ```bash
36
+ uv run pytest -q
37
+ ```
38
+
39
+ ### Run The Dual-Backend ORM Matrix
40
+
41
+ Run the backend-matrix and Postgres-specific tests on both SQLite and PostgreSQL:
42
+
43
+ ```bash
44
+ uv run pytest -m "backend_matrix or postgres_only" --db-backends=sqlite,postgres -q
45
+ ```
46
+
47
+ If you only want the PostgreSQL side of the matrix:
48
+
49
+ ```bash
50
+ uv run pytest -m "backend_matrix or postgres_only" --db-backends=postgres -q
51
+ ```
52
+
53
+ ### Test Markers
54
+
55
+ The repository uses three database markers:
56
+
57
+ - `backend_matrix`: run this test once per selected backend
58
+ - `sqlite_only`: keep SQLite-specific catalog, file-path, or pragma assertions on SQLite
59
+ - `postgres_only`: run Postgres/Supabase-specific assertions only when `FERRO_SUPABASE_URL` is configured
60
+
61
+ If `FERRO_SUPABASE_URL` is not set, `postgres_only` tests are skipped and `backend_matrix` tests run only on SQLite.
62
+
5
63
  ## Basic Setup
6
64
 
7
65
  ```python
@@ -9,25 +67,24 @@ Test your Ferro applications with pytest and test database isolation strategies.
9
67
  import pytest
10
68
  import ferro
11
69
 
12
- @pytest.fixture(scope="session")
70
+ @pytest.fixture
13
71
  async def db():
14
- """Connect to test database once per session."""
72
+ """Connect to a fresh test database for one test."""
15
73
  await ferro.connect("sqlite::memory:", auto_migrate=True)
16
74
  yield
17
- await ferro.disconnect()
75
+ ferro.reset_engine()
18
76
 
19
77
  @pytest.fixture
20
78
  async def db_transaction(db):
21
- """Wrap each test in a transaction that rolls back."""
22
- from ferro import begin_transaction, rollback_transaction
79
+ """Wrap each test in Ferro's transaction() helper."""
80
+ from ferro import transaction
23
81
 
24
- tx_id = await begin_transaction()
25
- try:
82
+ async with transaction():
26
83
  yield
27
- finally:
28
- await rollback_transaction(tx_id)
29
84
  ```
30
85
 
86
+ For backend-matrix tests, Ferro's own suite uses `--db-backends=sqlite,postgres` together with `backend_matrix` / `postgres_only` markers and a `FERRO_SUPABASE_URL` environment variable.
87
+
31
88
  ## Test Example
32
89
 
33
90
  ```python