ferrum-orm 0.1.16__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.
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/Cargo.lock +6 -6
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/Cargo.toml +1 -1
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/PKG-INFO +64 -10
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/README.md +62 -8
- ferrum_orm-0.1.19/crates/ferrum-core/benches/hydrate.rs +384 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-core/src/ir/metadata.rs +6 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-core/src/ir/mod.rs +4 -0
- ferrum_orm-0.1.19/crates/ferrum-sql/benches/compile.rs +813 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/emit.rs +13 -5
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/pyproject.toml +5 -5
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/__init__.py +11 -3
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/_native.pyi +66 -11
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/app.py +69 -0
- ferrum_orm-0.1.19/python/ferrum/cli/check_schema_cmd.py +148 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/migrate_cmd.py +142 -51
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/migrations_cmd.py +0 -1
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/connection.py +465 -27
- ferrum_orm-0.1.19/python/ferrum/contrib/__init__.py +12 -0
- ferrum_orm-0.1.19/python/ferrum/contrib/fastapi.py +554 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/drivers/mysql.py +60 -18
- ferrum_orm-0.1.19/python/ferrum/drivers/postgres.py +567 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/drivers/protocol.py +52 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/drivers/sqlite.py +65 -12
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/echo.py +2 -2
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/errors.py +391 -27
- ferrum_orm-0.1.19/python/ferrum/expressions.py +176 -0
- ferrum_orm-0.1.19/python/ferrum/ext/__init__.py +17 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/ext/pgvector.py +142 -64
- ferrum_orm-0.1.19/python/ferrum/hooks.py +499 -0
- ferrum_orm-0.1.19/python/ferrum/migrations/autodiff.py +665 -0
- ferrum_orm-0.1.19/python/ferrum/migrations/base.py +85 -0
- ferrum_orm-0.1.19/python/ferrum/migrations/coordinator.py +1031 -0
- ferrum_orm-0.1.19/python/ferrum/migrations/drift.py +1488 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/ledger.py +138 -5
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/loader.py +69 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/operations.py +17 -3
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/orchestrator.py +887 -19
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/models.py +860 -8
- ferrum_orm-0.1.19/python/ferrum/observability.py +554 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/queryset.py +397 -51
- ferrum_orm-0.1.19/python/ferrum/registry.py +116 -0
- ferrum_orm-0.1.19/python/ferrum/relations.py +683 -0
- ferrum_orm-0.1.19/python/ferrum/routing.py +376 -0
- ferrum_orm-0.1.19/python/ferrum/runtime.py +433 -0
- ferrum_orm-0.1.19/python/ferrum/session.py +344 -0
- ferrum_orm-0.1.16/crates/ferrum-core/benches/hydrate.rs +0 -101
- ferrum_orm-0.1.16/crates/ferrum-sql/benches/compile.rs +0 -162
- ferrum_orm-0.1.16/python/ferrum/contrib/__init__.py +0 -5
- ferrum_orm-0.1.16/python/ferrum/contrib/fastapi.py +0 -75
- ferrum_orm-0.1.16/python/ferrum/drivers/postgres.py +0 -306
- ferrum_orm-0.1.16/python/ferrum/expressions.py +0 -82
- ferrum_orm-0.1.16/python/ferrum/ext/__init__.py +0 -5
- ferrum_orm-0.1.16/python/ferrum/hooks.py +0 -270
- ferrum_orm-0.1.16/python/ferrum/migrations/base.py +0 -50
- ferrum_orm-0.1.16/python/ferrum/migrations/drift.py +0 -377
- ferrum_orm-0.1.16/python/ferrum/observability.py +0 -159
- ferrum_orm-0.1.16/python/ferrum/registry.py +0 -55
- ferrum_orm-0.1.16/python/ferrum/relations.py +0 -378
- ferrum_orm-0.1.16/python/ferrum/runtime.py +0 -240
- ferrum_orm-0.1.16/python/ferrum/session.py +0 -171
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/LICENSE +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-core/Cargo.toml +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-core/src/compile/mod.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-core/src/error.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-core/src/hydrate/mod.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-core/src/lib.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-core/src/migrate/mod.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-pyo3/Cargo.toml +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-pyo3/src/lib.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-sql/Cargo.toml +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/dialect.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/fts/mod.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/fts/mssql.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/fts/mysql.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/fts/postgres.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/fts/sqlite.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/crates/ferrum-sql/src/lib.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/__init__.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/bootstrap.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/init.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/inspectdb_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/makemigrations_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/resetdb_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/revert_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/showmigrations_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/cli/sqlmigrate_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/config.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/drivers/__init__.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/drivers/mssql.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/drivers/streaming.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/ext/fts.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/__init__.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/fts/__init__.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/fts/mssql.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/fts/mysql.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/fts/postgres.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/fts/sqlite.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/gates.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/introspect.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.19}/python/ferrum/migrations/tokens.py +0 -0
- {ferrum_orm-0.1.16 → 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.
|
|
28
|
+
version = "1.0.104"
|
|
29
29
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
30
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
258
|
+
version = "0.1.19"
|
|
259
259
|
dependencies = [
|
|
260
260
|
"criterion",
|
|
261
261
|
"ferrum-core",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ferrum-orm
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
434
|
-
and
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
362
|
-
and
|
|
363
|
-
|
|
364
|
-
|
|
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);
|