Load table schemas lazily and in bulk in Registry - #83
Conversation
linkTable() no longer introspects the table right away: it only records the database/table pair, and the first getTableSchema() call loads every pending table per database through Database::getSchemas() — one constant-cost batch instead of a full introspection per table (cycle/orm#466). On cycle/database without getSchemas() the old per-table path is used, so the required version stays ^2.20. Entities sharing a table still receive the same AbstractTable instance, and a table linked after the first load (embedded relations) reuses the already loaded schema. Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Covers the paths the lazy loading introduced: schema sharing between entities on one table, a table linked after the first bulk load reusing the loaded instance, the per-table fallback for cycle/database without getSchemas() (via a LegacyDatabase fixture), and the exception on a table implementation without getSchema(). Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 2.x #83 +/- ##
============================================
+ Coverage 94.97% 95.27% +0.29%
- Complexity 595 602 +7
============================================
Files 46 46
Lines 1711 1734 +23
============================================
+ Hits 1625 1652 +27
+ Misses 86 82 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR updates Cycle\Schema\Registry to defer table schema introspection until first use and to bulk-load pending table schemas per database (when supported), reducing catalog round-trips during schema compilation while preserving a per-table fallback for older database implementations.
Changes:
- Make
Registry::linkTable()record(database, table)without introspecting immediately; schemas are loaded on-demand ingetTableSchema(). - Add bulk schema loading via
Database::getSchemas()with a per-table fallback when bulk loading is unavailable. - Add unit tests and legacy fixtures to cover schema sharing, late linking, fallback behavior, and error paths.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Registry.php | Implements lazy + bulk schema loading and refactors table association access through a helper. |
| tests/Schema/RegistryTest.php | Adds test coverage for lazy loading, schema sharing, late linking reuse, and legacy fallback/error behavior. |
| tests/Schema/Fixtures/LegacyDatabase.php | Test double emulating a database without getSchemas() (and optionally without getSchema() on tables). |
| tests/Schema/Fixtures/LegacyTable.php | Test double for a table implementation lacking getSchema() to ensure clear exceptions. |
| composer.json | Raises minimum cycle/database requirement to ^2.23. |
| psalm-baseline.xml | Updates baseline entries impacted by the Registry refactor. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The existing tests only exercised entities sharing one table, so the schema-to-table mapping across distinct tables and databases was never checked, and nothing pinned the actual point of the patch: that linkTable() defers introspection and the first getTableSchema() loads every pending table through a single getSchemas() batch. Adds mapping tests for distinct tables, multiple databases, and the no-reload-of-loaded-table path (plus the legacy per-table fallback), and a SpyDatabase fixture that records getSchemas() calls to assert the deferral and one-batch-per-load contract directly (SQLite introspection issues no logged queries, so the query counter cannot verify batching). Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🔍 What was changed
Registry::linkTable()no longer introspects the table immediately — it only records the database/table pair.getTableSchema()call loads every pending table per database throughDatabase::getSchemas()(perf(Schema): bulk table introspection (getSchemas) database#265): a constant number of catalog queries per database instead of a full introspection per table.getSchemas()the Registry falls back to the old per-table path, so the behaviour is only faster, never different.How it works
linkTable()storesschema: null; all linked tables stay pending until a schema is first requested.getTableSchema()groups pending tables by database and loads each group in one bulk call; entities sharing a table receive the sameAbstractTableinstance, missing tables come back asSTATUS_NEWschemas.Why?
Fixing cycle/orm#466: on a remote database with 60–100 ms round-trip latency, schema compilation of hundreds of entities takes minutes because each table is introspected separately. Benchmarked against Postgres with 200 tables (13 columns, 2 indexes, FK each): 1200 queries / 3.7 s per-table vs 6 queries / 0.6 s bulk; at 100 ms RTT that is ~2 minutes vs ~0.6 s.
One behavioural note: introspection errors now surface at the first
getTableSchema()call instead of insidelinkTable().Checklist
LegacyDatabasefixture, error paths)