Skip to content

Load table schemas lazily and in bulk in Registry - #83

Merged
roxblnfk merged 4 commits into
2.xfrom
feat/bulk-schema-loading
Aug 19, 2026
Merged

Load table schemas lazily and in bulk in Registry#83
roxblnfk merged 4 commits into
2.xfrom
feat/bulk-schema-loading

Conversation

@roxblnfk

Copy link
Copy Markdown
Member

🔍 What was changed

  • Registry::linkTable() no longer introspects the table immediately — it only records the database/table pair.
  • The first getTableSchema() call loads every pending table per database through Database::getSchemas() (perf(Schema): bulk table introspection (getSchemas) database#265): a constant number of catalog queries per database instead of a full introspection per table.
  • On a cycle/database version without getSchemas() the Registry falls back to the old per-table path, so the behaviour is only faster, never different.

How it works

  • linkTable() stores schema: null; all linked tables stay pending until a schema is first requested.
  • The first getTableSchema() groups pending tables by database and loads each group in one bulk call; entities sharing a table receive the same AbstractTable instance, missing tables come back as STATUS_NEW schemas.
  • A table linked after the first load (embedded relations do this) reuses the already loaded instance; only genuinely new tables are fetched.

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 inside linkTable().

Checklist

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

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.27%. Comparing base (cde7e2a) to head (36610e7).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in getTableSchema().
  • 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.

Comment thread composer.json
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>
@roxblnfk
roxblnfk merged commit 884cd3a into 2.x Aug 19, 2026
27 of 29 checks passed
@roxblnfk
roxblnfk deleted the feat/bulk-schema-loading branch August 19, 2026 11:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants