S5 language-lane spec — hardening
Engine/tooling already on main: ormgen validate --dsn, ormgen import --dsn, ormgen errors --lang (docs/errors.yaml → constants), docs/config.md (orm.toml), .github/workflows/ci.yml. Each lane delivers the following for its language. Rules: docs/lanes/s3.md §4 (plus: you may add files under clients/<lang>/** freely; keep generated files generated).
1. Deliverables
| item | what |
|---|---|
| error constants | check in the generated constants file (ormgen errors --lang <lang> --out …: Go clients/go/orm/codes.go, PHP clients/php/src/Code.php (+ autoload map line), Rust clients/rust/orm/src/codes.rs + pub mod codes;) and use the constants instead of string literals in the runtime; map driver errors: MySQL 1213/SQLSTATE 40001 → DEADLOCK, 1062/23000 → DUPLICATE_KEY (the original driver message stays in the error text) |
| schema_hash boot check | at client init (Go orm.Open / PHP Orm::init / Rust Db::connect), compare the generated code's schema hash with the engine's loaded manifest exactly once; mismatch → SCHEMA_HASH_MISMATCH error, no watching, no reload. PHP asks ormd ({"op":"hash"}), Rust reads the wasm engine's hash, Go compares gen vs engine.M.SchemaHash |
| runtime connection | The application injects one DSN URI and runtime options. The URI scheme selects the database. Runtime configuration files are not part of the public connection API. |
| on_query hook | unify the signature: (sql, binds, duration, plan_id, err) where plan_id is the plan cache key (Go uint64 → hex string; PHP the local cache key suffix; Rust the u64 hash) so logs can group by statement shape; keep secrets in binds masked as $SECRET in the hook payload |
| DSN from the environment for tests | integration tests and the conformance runner read the DSN from ORM_MYSQL_DSN_GO / ORM_MYSQL_DSN_PHP / ORM_MYSQL_URL_RUST when set, else the local /tmp/mysql.sock default (so .github/workflows/ci.yml works unchanged) |
| fixed cost (T5.3b) | measure with examples/thin-slice (3-row query, 500 iterations) and reduce the per-statement overhead: Go — cache the IR shape bytes per call site is impossible, so make shape() cheaper (encode once into a pooled buffer, FNV over bytes without allocation) and avoid the []any row → struct double pass where possible; PHP — avoid re-encoding the IR when the local (per-request) plan cache already has the shape; Rust — decode typed values directly from MySqlRow into the generated struct (skip Vec<Val> for the main step) and hash without serde_json::to_vec allocations. Target: ≤ +5% vs the native line of the demo. Record before/after numbers in your report |
| PHP only | decide PDO::ATTR_EMULATE_PREPARES by measurement (PK, 100 rows, IN of 8) and fix one value; make sure ip and JSON columns come back as expected under it |
| packaging metadata | Go: module path; PHP: clients/php/composer.json; Rust: clients/rust/orm/Cargo.toml and generated client assets. Secrets and DSNs are injected at runtime. |
2. Verification
Existing integration tests and your runner still pass; add integration coverage for the boot check (a wrong hash → SCHEMA_HASH_MISMATCH), for orm.toml loading (a relative path → CONFIG), for the driver error mapping (insert a duplicate uuid → DUPLICATE_KEY). No new conformance vectors this round (the harness records per-language runner outputs, not config).