ferrum-orm 0.1.17__tar.gz → 0.1.19__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 (100) hide show
  1. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/Cargo.lock +6 -6
  2. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/Cargo.toml +1 -1
  3. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/PKG-INFO +64 -10
  4. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/README.md +62 -8
  5. ferrum_orm-0.1.19/crates/ferrum-core/benches/hydrate.rs +384 -0
  6. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-core/src/ir/metadata.rs +6 -0
  7. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-core/src/ir/mod.rs +4 -0
  8. ferrum_orm-0.1.19/crates/ferrum-sql/benches/compile.rs +813 -0
  9. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/emit.rs +13 -5
  10. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/pyproject.toml +5 -5
  11. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/__init__.py +11 -3
  12. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/_native.pyi +66 -11
  13. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/app.py +69 -0
  14. ferrum_orm-0.1.19/python/ferrum/cli/check_schema_cmd.py +148 -0
  15. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/migrate_cmd.py +142 -51
  16. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/migrations_cmd.py +0 -1
  17. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/connection.py +465 -27
  18. ferrum_orm-0.1.19/python/ferrum/contrib/__init__.py +12 -0
  19. ferrum_orm-0.1.19/python/ferrum/contrib/fastapi.py +554 -0
  20. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/drivers/mysql.py +60 -18
  21. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/drivers/postgres.py +250 -26
  22. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/drivers/protocol.py +52 -0
  23. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/drivers/sqlite.py +65 -12
  24. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/echo.py +2 -2
  25. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/errors.py +391 -27
  26. ferrum_orm-0.1.19/python/ferrum/expressions.py +176 -0
  27. ferrum_orm-0.1.19/python/ferrum/ext/__init__.py +17 -0
  28. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/ext/pgvector.py +132 -41
  29. ferrum_orm-0.1.19/python/ferrum/hooks.py +499 -0
  30. ferrum_orm-0.1.19/python/ferrum/migrations/autodiff.py +665 -0
  31. ferrum_orm-0.1.19/python/ferrum/migrations/base.py +85 -0
  32. ferrum_orm-0.1.19/python/ferrum/migrations/coordinator.py +1031 -0
  33. ferrum_orm-0.1.19/python/ferrum/migrations/drift.py +1488 -0
  34. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/ledger.py +138 -5
  35. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/loader.py +69 -0
  36. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/operations.py +17 -3
  37. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/orchestrator.py +887 -19
  38. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/models.py +860 -8
  39. ferrum_orm-0.1.19/python/ferrum/observability.py +554 -0
  40. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/queryset.py +388 -50
  41. ferrum_orm-0.1.19/python/ferrum/registry.py +116 -0
  42. ferrum_orm-0.1.19/python/ferrum/relations.py +683 -0
  43. ferrum_orm-0.1.19/python/ferrum/routing.py +376 -0
  44. ferrum_orm-0.1.19/python/ferrum/runtime.py +433 -0
  45. ferrum_orm-0.1.19/python/ferrum/session.py +344 -0
  46. ferrum_orm-0.1.17/crates/ferrum-core/benches/hydrate.rs +0 -101
  47. ferrum_orm-0.1.17/crates/ferrum-sql/benches/compile.rs +0 -162
  48. ferrum_orm-0.1.17/python/ferrum/contrib/__init__.py +0 -5
  49. ferrum_orm-0.1.17/python/ferrum/contrib/fastapi.py +0 -75
  50. ferrum_orm-0.1.17/python/ferrum/expressions.py +0 -82
  51. ferrum_orm-0.1.17/python/ferrum/ext/__init__.py +0 -5
  52. ferrum_orm-0.1.17/python/ferrum/hooks.py +0 -270
  53. ferrum_orm-0.1.17/python/ferrum/migrations/base.py +0 -50
  54. ferrum_orm-0.1.17/python/ferrum/migrations/drift.py +0 -377
  55. ferrum_orm-0.1.17/python/ferrum/observability.py +0 -159
  56. ferrum_orm-0.1.17/python/ferrum/registry.py +0 -55
  57. ferrum_orm-0.1.17/python/ferrum/relations.py +0 -378
  58. ferrum_orm-0.1.17/python/ferrum/runtime.py +0 -240
  59. ferrum_orm-0.1.17/python/ferrum/session.py +0 -171
  60. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/LICENSE +0 -0
  61. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-core/Cargo.toml +0 -0
  62. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-core/src/compile/mod.rs +0 -0
  63. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-core/src/error.rs +0 -0
  64. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-core/src/hydrate/mod.rs +0 -0
  65. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-core/src/lib.rs +0 -0
  66. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-core/src/migrate/mod.rs +0 -0
  67. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-pyo3/Cargo.toml +0 -0
  68. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-pyo3/src/lib.rs +0 -0
  69. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-sql/Cargo.toml +0 -0
  70. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/dialect.rs +0 -0
  71. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/fts/mod.rs +0 -0
  72. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/fts/mssql.rs +0 -0
  73. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/fts/mysql.rs +0 -0
  74. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/fts/postgres.rs +0 -0
  75. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/fts/sqlite.rs +0 -0
  76. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/lib.rs +0 -0
  77. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/__init__.py +0 -0
  78. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/bootstrap.py +0 -0
  79. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/init.py +0 -0
  80. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/inspectdb_cmd.py +0 -0
  81. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/makemigrations_cmd.py +0 -0
  82. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/resetdb_cmd.py +0 -0
  83. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/revert_cmd.py +0 -0
  84. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/showmigrations_cmd.py +0 -0
  85. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/cli/sqlmigrate_cmd.py +0 -0
  86. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/config.py +0 -0
  87. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/drivers/__init__.py +0 -0
  88. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/drivers/mssql.py +0 -0
  89. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/drivers/streaming.py +0 -0
  90. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/ext/fts.py +0 -0
  91. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/__init__.py +0 -0
  92. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/fts/__init__.py +0 -0
  93. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/fts/mssql.py +0 -0
  94. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/fts/mysql.py +0 -0
  95. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/fts/postgres.py +0 -0
  96. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/fts/sqlite.py +0 -0
  97. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/gates.py +0 -0
  98. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/introspect.py +0 -0
  99. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/migrations/tokens.py +0 -0
  100. {ferrum_orm-0.1.17 → ferrum_orm-0.1.19}/python/ferrum/py.typed +0 -0
@@ -25,9 +25,9 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
25
25
 
26
26
  [[package]]
27
27
  name = "anyhow"
28
- version = "1.0.102"
28
+ version = "1.0.104"
29
29
  source = "registry+https://github.com/rust-lang/crates.io-index"
30
- checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
30
+ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
31
31
 
32
32
  [[package]]
33
33
  name = "autocfg"
@@ -223,7 +223,7 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
223
223
 
224
224
  [[package]]
225
225
  name = "ferrum-core"
226
- version = "0.1.17"
226
+ version = "0.1.19"
227
227
  dependencies = [
228
228
  "criterion",
229
229
  "proptest",
@@ -234,7 +234,7 @@ dependencies = [
234
234
 
235
235
  [[package]]
236
236
  name = "ferrum-migrate"
237
- version = "0.1.17"
237
+ version = "0.1.19"
238
238
  dependencies = [
239
239
  "ferrum-core",
240
240
  "serde",
@@ -243,7 +243,7 @@ dependencies = [
243
243
 
244
244
  [[package]]
245
245
  name = "ferrum-pyo3"
246
- version = "0.1.17"
246
+ version = "0.1.19"
247
247
  dependencies = [
248
248
  "ferrum-core",
249
249
  "ferrum-sql",
@@ -255,7 +255,7 @@ dependencies = [
255
255
 
256
256
  [[package]]
257
257
  name = "ferrum-sql"
258
- version = "0.1.17"
258
+ version = "0.1.19"
259
259
  dependencies = [
260
260
  "criterion",
261
261
  "ferrum-core",
@@ -3,7 +3,7 @@ members = ["crates/ferrum-core", "crates/ferrum-sql", "crates/ferrum-pyo3"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.1.17"
6
+ version = "0.1.19"
7
7
  edition = "2021"
8
8
  authors = ["Ferrum Contributors"]
9
9
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ferrum-orm
3
- Version: 0.1.17
3
+ Version: 0.1.19
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Framework :: AsyncIO
6
6
  Classifier: Intended Audience :: Developers
@@ -25,7 +25,7 @@ Requires-Dist: ferrum-orm[cli] ; extra == 'all'
25
25
  Requires-Dist: ferrum-orm[dotenv] ; extra == 'all'
26
26
  Requires-Dist: typer>=0.12 ; extra == 'cli'
27
27
  Requires-Dist: rich>=13 ; extra == 'cli'
28
- Requires-Dist: ferrum-orm[pg,cli,dotenv] ; extra == 'dev'
28
+ Requires-Dist: ferrum-orm[pg,mysql,sqlite,mssql,cli,dotenv] ; extra == 'dev'
29
29
  Requires-Dist: maturin>=1.7,<2 ; extra == 'dev'
30
30
  Requires-Dist: pytest>=8 ; extra == 'dev'
31
31
  Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
@@ -96,12 +96,25 @@ Ferrum aims to provide all four.
96
96
  - Pydantic-first models
97
97
  - Django-inspired ORM experience
98
98
  - Rust-powered query engine
99
- - PostgreSQL-first architecture (MySQL, SQLite, and SQL Server via optional extras)
99
+ - PostgreSQL-first architecture. MySQL, SQLite, and SQL Server extras are best-effort thin parity — not production-supported and not production-readiness release gates
100
100
  - Type-safe query construction
101
101
  - Automatic migrations
102
102
  - High-performance result hydration
103
103
  - Production-ready observability
104
104
 
105
+ ### Production-readiness execution
106
+
107
+ Ferrum is still alpha. The remaining production work for Ticket Analyzer and an
108
+ async-refactored Org AI Platform is tracked in
109
+ [the production-readiness plan](.cursor/plans/ferrum-production-readiness_6b5f422d.plan.md).
110
+ Execution is wave-based and supports parallel agents through disjoint file ownership,
111
+ per-workstream state, immutable evidence logs, and an independent verification gate.
112
+
113
+ Every task follows `Specify → Plan → Tasks → Implement`; every executor run follows
114
+ `Load → Execute → Validate executor output → Verify independently → Update state`.
115
+ See [.agent-work/production-readiness/](.agent-work/production-readiness/) for the durable
116
+ execution contract and current aggregate state.
117
+
105
118
  ---
106
119
 
107
120
  ## Quick Example
@@ -285,7 +298,9 @@ This allows Ferrum to maintain a Pythonic API without sacrificing performance.
285
298
  ### Cross-Driver Full-Text Search
286
299
 
287
300
  Native full-text search across PostgreSQL, MySQL, SQLite FTS5, and SQL Server — one
288
- QuerySet API, dialect-specific SQL emit and migration DDL.
301
+ QuerySet API, dialect-specific SQL emit and migration DDL. Cross-dialect FTS on
302
+ MySQL / SQLite / SQL Server is a **thin-extra** capability, not a P0
303
+ production-readiness release gate. PostgreSQL FTS is the production-readiness target.
289
304
 
290
305
  **Query modes** (filter lookups and ranking):
291
306
 
@@ -366,7 +381,7 @@ and [API Reference](docs/api-reference.md) for per-dialect DDL and operator mapp
366
381
  - [x] Migrations (schema diff, apply, revert, CLI)
367
382
  - [x] Relationships (ForeignKey, OneToOne, ManyToMany)
368
383
  - [x] pgvector KNN search and HNSW/IVFFLAT index DDL
369
- - [x] Full-text search (cross-dialect: PostgreSQL, MySQL, SQLite FTS5, SQL Server)
384
+ - [x] Full-text search (PostgreSQL is the P0 gate; MySQL/SQLite/SQL Server FTS is thin-extra)
370
385
  - [x] Observability hooks (Tier A/B/C)
371
386
  - [x] CLI (`makemigrations`, `migrate`, `revert`, `showmigrations`, `inspectdb`, `resetdb`)
372
387
 
@@ -397,7 +412,11 @@ and [API Reference](docs/api-reference.md) for per-dialect DDL and operator mapp
397
412
 
398
413
  Ferrum is currently in active development.
399
414
 
400
- The API is not yet stable and breaking changes should be expected until the first public release.
415
+ The API is not yet stable. **0.x has no compatibility guarantee**: SemVer MINOR and PATCH
416
+ may both break. After 1.0, public API and migration-ops breaks require a deprecation
417
+ window of at least one minor release and 90 days, whichever is longer. Generated SQL is
418
+ never a compatibility surface. Compatibility policy: `AGENTS.md` §5a. Thin extras:
419
+ `AGENTS.md` §2.6. See also `CHANGELOG.md`.
401
420
 
402
421
  ## Installation
403
422
 
@@ -430,10 +449,45 @@ pip install ferrum-orm
430
449
  Bare `ferrum-orm` installs Pydantic and the Rust core only. Choose a driver extra
431
450
  (`pg`, `mysql`, `sqlite`, or `mssql`) before calling `ferrum.connect()`.
432
451
 
433
- MySQL, SQLite, and SQL Server are **thin-parity** backends: they support core CRUD
434
- and migrations but not transactions, upsert, `bulk_update`, RLS, or pgvector
435
- (PostgreSQL only). SQL Server connects via `aioodbc`/`pyodbc` and requires a system
436
- ODBC driver such as `msodbcsql18`; DSNs use the `mssql://` or `sqlserver://` scheme.
452
+ MySQL, SQLite, and SQL Server are **best-effort thin-parity** backends: they are **not
453
+ production-supported** and are **not production-readiness release gates**. Core CRUD is
454
+ exercised across all four drivers; advanced behavior is capability-gated rather than
455
+ emulated with incompatible SQL. The integration-suite registry
456
+ (`tests/python/integration/backends.py`) is the coverage matrix — what is tested,
457
+ not a stability or production-support claim. PostgreSQL claims every capability.
458
+ MySQL and SQL Server claim full-text search and composite primary keys only.
459
+ SQLite claims `RETURNING`, full-text search, and composite primary keys.
460
+
461
+ | Capability | PostgreSQL | MySQL | SQLite | SQL Server |
462
+ | ---------- | :--------: | :---: | :----: | :--------: |
463
+ | Transactions | ✅ | — | — | — |
464
+ | Savepoints | ✅ | — | — | — |
465
+ | Cursor streaming | ✅ | — | — | — |
466
+ | Bulk update | ✅ | — | — | — |
467
+ | Aggregates / grouping | ✅ | — | — | — |
468
+ | Upsert | ✅ | — | — | — |
469
+ | Returning rows | ✅ | — | ✅ | — |
470
+ | Row-level security helpers | ✅ | — | — | — |
471
+ | pgvector | ✅ | — | — | — |
472
+ | Full-text search | ✅ | ✅ | ✅ | ✅ |
473
+ | JSON operators | ✅ | — | — | — |
474
+ | Case-insensitive `ILIKE` | ✅ | — | — | — |
475
+ | Array columns | ✅ | — | — | — |
476
+ | Alter column | ✅ | — | — | — |
477
+ | Composite primary keys | ✅ | ✅ | ✅ | ✅ |
478
+ | Stored-function calls | ✅ | — | — | — |
479
+
480
+ Unsupported capabilities fail with a typed Ferrum error before backend SQL is
481
+ executed. Transactions, savepoints, aggregates, upsert, alter-column, and
482
+ stored-function calls are PostgreSQL-only in this matrix; thin extras raise at
483
+ the Python/compiler boundary rather than sending Postgres SQL. Pull requests
484
+ exercise PostgreSQL and SQLite, while the nightly integration matrix runs
485
+ PostgreSQL, MySQL, SQLite, and SQL Server independently. SQLite on PRs is CI
486
+ coverage, not a production-readiness gate. Cross-dialect FTS in the matrix is
487
+ thin-extra coverage, not a P0 release gate.
488
+
489
+ SQL Server connects via `aioodbc`/`pyodbc` and requires a system ODBC driver such
490
+ as `msodbcsql18`; DSNs use the `mssql://` or `sqlserver://` scheme.
437
491
 
438
492
  ### Wire format (advanced)
439
493
 
@@ -24,12 +24,25 @@ Ferrum aims to provide all four.
24
24
  - Pydantic-first models
25
25
  - Django-inspired ORM experience
26
26
  - Rust-powered query engine
27
- - PostgreSQL-first architecture (MySQL, SQLite, and SQL Server via optional extras)
27
+ - PostgreSQL-first architecture. MySQL, SQLite, and SQL Server extras are best-effort thin parity — not production-supported and not production-readiness release gates
28
28
  - Type-safe query construction
29
29
  - Automatic migrations
30
30
  - High-performance result hydration
31
31
  - Production-ready observability
32
32
 
33
+ ### Production-readiness execution
34
+
35
+ Ferrum is still alpha. The remaining production work for Ticket Analyzer and an
36
+ async-refactored Org AI Platform is tracked in
37
+ [the production-readiness plan](.cursor/plans/ferrum-production-readiness_6b5f422d.plan.md).
38
+ Execution is wave-based and supports parallel agents through disjoint file ownership,
39
+ per-workstream state, immutable evidence logs, and an independent verification gate.
40
+
41
+ Every task follows `Specify → Plan → Tasks → Implement`; every executor run follows
42
+ `Load → Execute → Validate executor output → Verify independently → Update state`.
43
+ See [.agent-work/production-readiness/](.agent-work/production-readiness/) for the durable
44
+ execution contract and current aggregate state.
45
+
33
46
  ---
34
47
 
35
48
  ## Quick Example
@@ -213,7 +226,9 @@ This allows Ferrum to maintain a Pythonic API without sacrificing performance.
213
226
  ### Cross-Driver Full-Text Search
214
227
 
215
228
  Native full-text search across PostgreSQL, MySQL, SQLite FTS5, and SQL Server — one
216
- QuerySet API, dialect-specific SQL emit and migration DDL.
229
+ QuerySet API, dialect-specific SQL emit and migration DDL. Cross-dialect FTS on
230
+ MySQL / SQLite / SQL Server is a **thin-extra** capability, not a P0
231
+ production-readiness release gate. PostgreSQL FTS is the production-readiness target.
217
232
 
218
233
  **Query modes** (filter lookups and ranking):
219
234
 
@@ -294,7 +309,7 @@ and [API Reference](docs/api-reference.md) for per-dialect DDL and operator mapp
294
309
  - [x] Migrations (schema diff, apply, revert, CLI)
295
310
  - [x] Relationships (ForeignKey, OneToOne, ManyToMany)
296
311
  - [x] pgvector KNN search and HNSW/IVFFLAT index DDL
297
- - [x] Full-text search (cross-dialect: PostgreSQL, MySQL, SQLite FTS5, SQL Server)
312
+ - [x] Full-text search (PostgreSQL is the P0 gate; MySQL/SQLite/SQL Server FTS is thin-extra)
298
313
  - [x] Observability hooks (Tier A/B/C)
299
314
  - [x] CLI (`makemigrations`, `migrate`, `revert`, `showmigrations`, `inspectdb`, `resetdb`)
300
315
 
@@ -325,7 +340,11 @@ and [API Reference](docs/api-reference.md) for per-dialect DDL and operator mapp
325
340
 
326
341
  Ferrum is currently in active development.
327
342
 
328
- The API is not yet stable and breaking changes should be expected until the first public release.
343
+ The API is not yet stable. **0.x has no compatibility guarantee**: SemVer MINOR and PATCH
344
+ may both break. After 1.0, public API and migration-ops breaks require a deprecation
345
+ window of at least one minor release and 90 days, whichever is longer. Generated SQL is
346
+ never a compatibility surface. Compatibility policy: `AGENTS.md` §5a. Thin extras:
347
+ `AGENTS.md` §2.6. See also `CHANGELOG.md`.
329
348
 
330
349
  ## Installation
331
350
 
@@ -358,10 +377,45 @@ pip install ferrum-orm
358
377
  Bare `ferrum-orm` installs Pydantic and the Rust core only. Choose a driver extra
359
378
  (`pg`, `mysql`, `sqlite`, or `mssql`) before calling `ferrum.connect()`.
360
379
 
361
- MySQL, SQLite, and SQL Server are **thin-parity** backends: they support core CRUD
362
- and migrations but not transactions, upsert, `bulk_update`, RLS, or pgvector
363
- (PostgreSQL only). SQL Server connects via `aioodbc`/`pyodbc` and requires a system
364
- ODBC driver such as `msodbcsql18`; DSNs use the `mssql://` or `sqlserver://` scheme.
380
+ MySQL, SQLite, and SQL Server are **best-effort thin-parity** backends: they are **not
381
+ production-supported** and are **not production-readiness release gates**. Core CRUD is
382
+ exercised across all four drivers; advanced behavior is capability-gated rather than
383
+ emulated with incompatible SQL. The integration-suite registry
384
+ (`tests/python/integration/backends.py`) is the coverage matrix — what is tested,
385
+ not a stability or production-support claim. PostgreSQL claims every capability.
386
+ MySQL and SQL Server claim full-text search and composite primary keys only.
387
+ SQLite claims `RETURNING`, full-text search, and composite primary keys.
388
+
389
+ | Capability | PostgreSQL | MySQL | SQLite | SQL Server |
390
+ | ---------- | :--------: | :---: | :----: | :--------: |
391
+ | Transactions | ✅ | — | — | — |
392
+ | Savepoints | ✅ | — | — | — |
393
+ | Cursor streaming | ✅ | — | — | — |
394
+ | Bulk update | ✅ | — | — | — |
395
+ | Aggregates / grouping | ✅ | — | — | — |
396
+ | Upsert | ✅ | — | — | — |
397
+ | Returning rows | ✅ | — | ✅ | — |
398
+ | Row-level security helpers | ✅ | — | — | — |
399
+ | pgvector | ✅ | — | — | — |
400
+ | Full-text search | ✅ | ✅ | ✅ | ✅ |
401
+ | JSON operators | ✅ | — | — | — |
402
+ | Case-insensitive `ILIKE` | ✅ | — | — | — |
403
+ | Array columns | ✅ | — | — | — |
404
+ | Alter column | ✅ | — | — | — |
405
+ | Composite primary keys | ✅ | ✅ | ✅ | ✅ |
406
+ | Stored-function calls | ✅ | — | — | — |
407
+
408
+ Unsupported capabilities fail with a typed Ferrum error before backend SQL is
409
+ executed. Transactions, savepoints, aggregates, upsert, alter-column, and
410
+ stored-function calls are PostgreSQL-only in this matrix; thin extras raise at
411
+ the Python/compiler boundary rather than sending Postgres SQL. Pull requests
412
+ exercise PostgreSQL and SQLite, while the nightly integration matrix runs
413
+ PostgreSQL, MySQL, SQLite, and SQL Server independently. SQLite on PRs is CI
414
+ coverage, not a production-readiness gate. Cross-dialect FTS in the matrix is
415
+ thin-extra coverage, not a P0 release gate.
416
+
417
+ SQL Server connects via `aioodbc`/`pyodbc` and requires a system ODBC driver such
418
+ as `msodbcsql18`; DSNs use the `mssql://` or `sqlserver://` scheme.
365
419
 
366
420
  ### Wire format (advanced)
367
421
 
@@ -0,0 +1,384 @@
1
+ //! Criterion benchmarks for row hydration at 1 / 10 / 100 / 1000 rows.
2
+ //!
3
+ //! Budget (ARCHITECTURE.md §14 / benches/README.md): `hydrate_rows` p99 < 5 ms @ 100 rows.
4
+ //!
5
+ //! Coverage added in W4-B:
6
+ //! - `hydrate_rows` across 1 / 10 / 100 / 1000 rows (original).
7
+ //! - `hydrate_rows_wide` — 20-column row to measure per-field validation cost
8
+ //! scaling with column count.
9
+ //! - `hydrate_rows_jsonb` — JSONB + array + vector field types to cover the
10
+ //! type mix used by ticket-analyzer workloads.
11
+ //! - `json_boundary_encode` / `json_boundary_decode` — the throwaway
12
+ //! serde_json copy the PyO3 bridge makes for Rust structural validation
13
+ //! (AGENTS.md: `_RowEncoder` serializes a copy for validation only). This
14
+ //! isolates the JSON wire-format boundary cost from hydration itself.
15
+ //! - `validation_copy` — the `validate_row` walk over non-nullable fields,
16
+ //! measured by hydrating the same rows with all-non-nullable vs
17
+ //! all-nullable metadata so the delta is the validation walk.
18
+
19
+ // Bench-harness files are test infrastructure, not shipped code. Keep
20
+ // `clippy::all` enforced; suppress the pedantic group (doc backticks,
21
+ // cast-precision on bench indices) to keep data construction readable.
22
+ #![allow(clippy::pedantic)]
23
+
24
+ use criterion::{black_box, criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion};
25
+ use ferrum_core::hydrate::hydrate_rows;
26
+ use ferrum_core::hydrate::RowPayload;
27
+ use ferrum_core::ir::metadata::{FieldMeta, FieldType, ModelMetadata};
28
+ use serde_json::json;
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Metadata builders
32
+ // ---------------------------------------------------------------------------
33
+
34
+ fn bench_metadata() -> ModelMetadata {
35
+ ModelMetadata {
36
+ model_name: "Post".into(),
37
+ table_name: "posts".into(),
38
+ fields: vec![
39
+ FieldMeta {
40
+ name: "id".into(),
41
+ column_name: "id".into(),
42
+ field_type: FieldType::Int,
43
+ allowed_operators: vec!["eq".into()],
44
+ nullable: false,
45
+ vector_dimensions: None,
46
+ fts_config: None,
47
+ fts_source_columns: None,
48
+ },
49
+ FieldMeta {
50
+ name: "title".into(),
51
+ column_name: "title".into(),
52
+ field_type: FieldType::Text,
53
+ allowed_operators: vec!["eq".into(), "icontains".into()],
54
+ nullable: false,
55
+ vector_dimensions: None,
56
+ fts_config: None,
57
+ fts_source_columns: None,
58
+ },
59
+ FieldMeta {
60
+ name: "body".into(),
61
+ column_name: "body".into(),
62
+ field_type: FieldType::Text,
63
+ allowed_operators: vec!["eq".into()],
64
+ nullable: true,
65
+ vector_dimensions: None,
66
+ fts_config: None,
67
+ fts_source_columns: None,
68
+ },
69
+ FieldMeta {
70
+ name: "published".into(),
71
+ column_name: "published".into(),
72
+ field_type: FieldType::Bool,
73
+ allowed_operators: vec!["eq".into()],
74
+ nullable: false,
75
+ vector_dimensions: None,
76
+ fts_config: None,
77
+ fts_source_columns: None,
78
+ },
79
+ ],
80
+ pk_index: 0,
81
+ pk_fields: vec![0],
82
+ full_text_indexes: vec![],
83
+ }
84
+ }
85
+
86
+ /// 20-column wide model — measures validation cost scaling with column count.
87
+ fn wide_metadata(nullable: bool) -> ModelMetadata {
88
+ let fields = (0..20)
89
+ .map(|i| FieldMeta {
90
+ name: format!("col{i}"),
91
+ column_name: format!("col{i}"),
92
+ field_type: if i % 5 == 0 {
93
+ FieldType::Int
94
+ } else if i % 5 == 1 {
95
+ FieldType::Text
96
+ } else if i % 5 == 2 {
97
+ FieldType::Bool
98
+ } else if i % 5 == 3 {
99
+ FieldType::Float
100
+ } else {
101
+ FieldType::Json
102
+ },
103
+ allowed_operators: vec!["eq".into()],
104
+ nullable,
105
+ vector_dimensions: None,
106
+ fts_config: None,
107
+ fts_source_columns: None,
108
+ })
109
+ .collect();
110
+ ModelMetadata {
111
+ model_name: "Wide".into(),
112
+ table_name: "wide".into(),
113
+ fields,
114
+ pk_index: 0,
115
+ pk_fields: vec![0],
116
+ full_text_indexes: vec![],
117
+ }
118
+ }
119
+
120
+ /// JSONB + array + vector field mix — ticket-analyzer workload shape.
121
+ fn jsonb_metadata() -> ModelMetadata {
122
+ ModelMetadata {
123
+ model_name: "Doc".into(),
124
+ table_name: "docs".into(),
125
+ fields: vec![
126
+ FieldMeta {
127
+ name: "id".into(),
128
+ column_name: "id".into(),
129
+ field_type: FieldType::Uuid,
130
+ allowed_operators: vec!["eq".into()],
131
+ nullable: false,
132
+ vector_dimensions: None,
133
+ fts_config: None,
134
+ fts_source_columns: None,
135
+ },
136
+ FieldMeta {
137
+ name: "tags".into(),
138
+ column_name: "tags".into(),
139
+ field_type: FieldType::ArrayText,
140
+ allowed_operators: vec!["eq".into(), "contains".into()],
141
+ nullable: true,
142
+ vector_dimensions: None,
143
+ fts_config: None,
144
+ fts_source_columns: None,
145
+ },
146
+ FieldMeta {
147
+ name: "meta".into(),
148
+ column_name: "meta".into(),
149
+ field_type: FieldType::Json,
150
+ allowed_operators: vec!["eq".into(), "contains".into(), "has_key".into()],
151
+ nullable: true,
152
+ vector_dimensions: None,
153
+ fts_config: None,
154
+ fts_source_columns: None,
155
+ },
156
+ FieldMeta {
157
+ name: "embedding".into(),
158
+ column_name: "embedding".into(),
159
+ field_type: FieldType::Vector,
160
+ allowed_operators: vec!["eq".into()],
161
+ nullable: true,
162
+ vector_dimensions: Some(8),
163
+ fts_config: None,
164
+ fts_source_columns: None,
165
+ },
166
+ ],
167
+ pk_index: 0,
168
+ pk_fields: vec![0],
169
+ full_text_indexes: vec![],
170
+ }
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Row builders
175
+ // ---------------------------------------------------------------------------
176
+
177
+ fn make_rows(count: usize) -> Vec<RowPayload> {
178
+ (0..count)
179
+ .map(|i| {
180
+ serde_json::from_value(json!({
181
+ "id": i64::try_from(i).expect("bench row index") + 1,
182
+ "title": format!("Post title {i}"),
183
+ "body": if i % 3 == 0 { serde_json::Value::Null } else { json!(format!("Body {i}")) },
184
+ "published": i % 2 == 0,
185
+ }))
186
+ .expect("valid row payload")
187
+ })
188
+ .collect()
189
+ }
190
+
191
+ fn make_wide_rows(count: usize) -> Vec<RowPayload> {
192
+ (0..count)
193
+ .map(|i| {
194
+ let mut obj = serde_json::Map::new();
195
+ for j in 0..20_u32 {
196
+ let key = format!("col{j}");
197
+ let val = match j % 5 {
198
+ 0 => json!((i as i64) + j as i64),
199
+ 1 => json!(format!("text-{i}-{j}")),
200
+ 2 => json!(j % 2 == 0),
201
+ 3 => json!(i as f64 + 0.5 * j as f64),
202
+ _ => json!({"k": i, "j": j}),
203
+ };
204
+ obj.insert(key, val);
205
+ }
206
+ obj
207
+ })
208
+ .collect()
209
+ }
210
+
211
+ fn make_jsonb_rows(count: usize) -> Vec<RowPayload> {
212
+ (0..count)
213
+ .map(|i| {
214
+ serde_json::from_value(json!({
215
+ "id": format!("00000000-0000-0000-0000-{:012x}", i),
216
+ "tags": ["alpha", "beta", format!("tag{i}")],
217
+ "meta": {"category": "doc", "weight": i, "nested": {"flag": true}},
218
+ "embedding": [0.1_f64 * i as f64, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
219
+ }))
220
+ .expect("valid jsonb row payload")
221
+ })
222
+ .collect()
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Benchmarks
227
+ // ---------------------------------------------------------------------------
228
+
229
+ fn hydrate_rows_bench(c: &mut Criterion) {
230
+ let metadata = bench_metadata();
231
+ let row_counts = [1_usize, 10, 100, 1000];
232
+
233
+ let mut group = c.benchmark_group("hydrate_rows");
234
+ group.sample_size(50);
235
+ group.warm_up_time(std::time::Duration::from_millis(300));
236
+ group.measurement_time(std::time::Duration::from_secs(3));
237
+
238
+ for count in row_counts {
239
+ let rows = make_rows(count);
240
+ group.bench_with_input(BenchmarkId::new("rows", count), &rows, |b, batch| {
241
+ b.iter_batched(
242
+ || batch.clone(),
243
+ |rows| hydrate_rows(black_box(&metadata), black_box(rows)).unwrap(),
244
+ BatchSize::SmallInput,
245
+ );
246
+ });
247
+ }
248
+
249
+ group.finish();
250
+ }
251
+
252
+ fn hydrate_rows_wide_bench(c: &mut Criterion) {
253
+ // All-non-nullable: every column triggers the validation lookup.
254
+ let metadata = wide_metadata(false);
255
+ let rows100 = make_wide_rows(100);
256
+
257
+ let mut group = c.benchmark_group("hydrate_rows_wide");
258
+ group.sample_size(50);
259
+ group.warm_up_time(std::time::Duration::from_millis(300));
260
+ group.measurement_time(std::time::Duration::from_secs(3));
261
+
262
+ group.bench_with_input(BenchmarkId::new("cols20", 100), &rows100, |b, batch| {
263
+ b.iter_batched(
264
+ || batch.clone(),
265
+ |rows| hydrate_rows(black_box(&metadata), black_box(rows)).unwrap(),
266
+ BatchSize::SmallInput,
267
+ );
268
+ });
269
+
270
+ group.finish();
271
+ }
272
+
273
+ fn hydrate_rows_jsonb_bench(c: &mut Criterion) {
274
+ let metadata = jsonb_metadata();
275
+ let row_counts = [1_usize, 100];
276
+
277
+ let mut group = c.benchmark_group("hydrate_rows_jsonb");
278
+ group.sample_size(50);
279
+ group.warm_up_time(std::time::Duration::from_millis(300));
280
+ group.measurement_time(std::time::Duration::from_secs(3));
281
+
282
+ for count in row_counts {
283
+ let rows = make_jsonb_rows(count);
284
+ group.bench_with_input(BenchmarkId::new("rows", count), &rows, |b, batch| {
285
+ b.iter_batched(
286
+ || batch.clone(),
287
+ |rows| hydrate_rows(black_box(&metadata), black_box(rows)).unwrap(),
288
+ BatchSize::SmallInput,
289
+ );
290
+ });
291
+ }
292
+
293
+ group.finish();
294
+ }
295
+
296
+ /// Validation-copy isolation: hydrate the same wide rows under all-nullable
297
+ /// metadata (validation walk still iterates but skips the null checks) vs
298
+ /// all-non-nullable (full validation). The delta is the validation copy cost.
299
+ fn validation_copy_bench(c: &mut Criterion) {
300
+ let meta_nullable = wide_metadata(true);
301
+ let meta_strict = wide_metadata(false);
302
+ let rows = make_wide_rows(100);
303
+
304
+ let mut group = c.benchmark_group("validation_copy");
305
+ group.sample_size(50);
306
+ group.warm_up_time(std::time::Duration::from_millis(300));
307
+ group.measurement_time(std::time::Duration::from_secs(3));
308
+
309
+ group.bench_with_input(BenchmarkId::new("nullable_meta", 100), &rows, |b, batch| {
310
+ b.iter_batched(
311
+ || batch.clone(),
312
+ |rows| hydrate_rows(black_box(&meta_nullable), black_box(rows)).unwrap(),
313
+ BatchSize::SmallInput,
314
+ );
315
+ });
316
+
317
+ group.bench_with_input(BenchmarkId::new("strict_meta", 100), &rows, |b, batch| {
318
+ b.iter_batched(
319
+ || batch.clone(),
320
+ |rows| hydrate_rows(black_box(&meta_strict), black_box(rows)).unwrap(),
321
+ BatchSize::SmallInput,
322
+ );
323
+ });
324
+
325
+ group.finish();
326
+ }
327
+
328
+ /// JSON wire-format boundary cost: the throwaway serde_json round-trip the
329
+ /// PyO3 bridge performs for Rust structural validation (AGENTS.md: `_RowEncoder`
330
+ /// serializes a copy; `model_construct` receives native driver types). This
331
+ /// isolates serialization from hydration by encoding rows to JSON bytes and
332
+ /// decoding them back, without invoking `hydrate_rows`.
333
+ fn json_boundary_bench(c: &mut Criterion) {
334
+ let row_counts = [1_usize, 100, 1000];
335
+
336
+ let mut group = c.benchmark_group("json_boundary");
337
+ group.sample_size(50);
338
+ group.warm_up_time(std::time::Duration::from_millis(300));
339
+ group.measurement_time(std::time::Duration::from_secs(3));
340
+
341
+ for count in row_counts {
342
+ let rows = make_rows(count);
343
+
344
+ group.bench_with_input(BenchmarkId::new("encode", count), &rows, |b, batch| {
345
+ b.iter_batched(
346
+ || serde_json::to_vec(batch).expect("encode"),
347
+ |bytes| {
348
+ black_box(bytes);
349
+ },
350
+ BatchSize::SmallInput,
351
+ );
352
+ });
353
+
354
+ let encoded: Vec<Vec<u8>> = rows
355
+ .iter()
356
+ .map(|r| serde_json::to_vec(r).expect("encode"))
357
+ .collect();
358
+ group.bench_with_input(BenchmarkId::new("decode", count), &encoded, |b, batch| {
359
+ b.iter_batched(
360
+ || batch.clone(),
361
+ |bytes_vec| {
362
+ let decoded: Vec<RowPayload> = bytes_vec
363
+ .iter()
364
+ .map(|b| serde_json::from_slice(b).expect("decode"))
365
+ .collect();
366
+ black_box(decoded);
367
+ },
368
+ BatchSize::SmallInput,
369
+ );
370
+ });
371
+ }
372
+
373
+ group.finish();
374
+ }
375
+
376
+ criterion_group!(
377
+ benches,
378
+ hydrate_rows_bench,
379
+ hydrate_rows_wide_bench,
380
+ hydrate_rows_jsonb_bench,
381
+ validation_copy_bench,
382
+ json_boundary_bench,
383
+ );
384
+ criterion_main!(benches);