Skip to content

fix(core): cache the Model class in isModelStatic to avoid repeated require()#18234

Open
Svehla wants to merge 3 commits into
sequelize:mainfrom
Svehla:fix/cache-model-class-in-is-model-static
Open

fix(core): cache the Model class in isModelStatic to avoid repeated require()#18234
Svehla wants to merge 3 commits into
sequelize:mainfrom
Svehla:fix/cache-model-class-in-is-model-static

Conversation

@Svehla
Copy link
Copy Markdown

@Svehla Svehla commented Jun 5, 2026

Pull Request Checklist

  • Have you added new tests to prevent regressions?
  • If a documentation update is necessary, have you opened a PR to the documentation repository? — not applicable, no behavior/API change
  • Did you update the typescript typings accordingly (if applicable)? — not applicable
  • Does the description below contain a link to an existing issue (Closes #[issue]) or a description of the issue you are solving?
  • Does the name of your PR follow our conventions?

Closes #18233

Description of Changes

The problem we hit

isModelStatic() calls require('../model') on every invocation:

export function isModelStatic<M extends Model>(val: any): val is ModelStatic<M> {
  // TODO: temporary workaround due to cyclic import.
  const { Model: TmpModel } = require('../model');

  return typeof val === 'function' && val.prototype instanceof TmpModel;
}

isModelStatic is on a hot path — it runs (directly, and via isSameInitialModel / extractModelDefinition) once per association while building queries. Although Node caches the loaded module, the require() call itself still funnels through Module._load and re-resolves the request every time.

We ran into this in production: under our Node module-resolution setup, that per-call require('../model') repeatedly walked trySelf → findParentPackageJSON → fs.stat, and on include-heavy queries (many associations) the resolution overhead showed up at the top of our CPU profiles. We were running this fix as a patch-package patch against @sequelize/core@7.0.0-alpha.47 and wanted to upstream it.

Even on a plain Node setup where each resolution is cheaper, the work is pure waste: the resolved Model class never changes once the module graph is initialized.

The fix

Cache the resolved Model class in a module-level variable and reuse it on subsequent calls. The lazy require (the cyclic-import workaround) is preserved for the first call, so module-load ordering is unchanged; only the redundant re-resolution on later calls is removed.

let cachedModelClass: ModelStatic | undefined;

export function isModelStatic<M extends Model>(val: any): val is ModelStatic<M> {
  // TODO: temporary workaround due to cyclic import. ...
  cachedModelClass ??= require('../model').Model as ModelStatic;

  return typeof val === 'function' && val.prototype instanceof cachedModelClass;
}

Tests

Added a unit test that spies on Module._load and asserts the Model module is not re-resolved across 50 isModelStatic calls after warm-up.

I verified locally that the test passes with the fix and fails without it (the uncached version resolves the module once per call — 50 times). Behavior is unchanged and remains covered by the existing isModelStatic / isSameInitialModel tests; yarn eslint and yarn lerna run test-typings --scope=@sequelize/core both pass.

List of Breaking Changes

None. This is an internal, behavior-preserving performance optimization.

Summary by CodeRabbit

  • Refactor

    • Improved performance by caching a frequently accessed class to avoid repeated module reloads.
  • Tests

    • Added unit test to verify the caching behavior and prevent repeated module resolution during repeated calls.

…equire()

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Svehla Svehla requested a review from a team as a code owner June 5, 2026 11:27
@Svehla Svehla requested review from ephys and sdepold June 5, 2026 11:27
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 5, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 96ecc495-a8c6-4b0a-8fee-9201d37f7a3e

📥 Commits

Reviewing files that changed from the base of the PR and between 1ef312e and 295ca37.

📒 Files selected for processing (1)
  • packages/core/test/unit/utils/model-utils.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/test/unit/utils/model-utils.test.ts

📝 Walkthrough

Walkthrough

isModelStatic() now caches the Model class in a module-level variable initialized via a lazy require('../model'), and the unit tests add a spy on Module._load to confirm no further model module resolution occurs after warm-up.

Changes

isModelStatic Module Cache Optimization

Layer / File(s) Summary
Module-level cache implementation
packages/core/src/utils/model-utils.ts
Added cachedModelClass module variable lazily initialized via require('../model').Model and updated isModelStatic to use the cached class for prototype/instanceof checks instead of requiring on each invocation.
Cache behavior verification test
packages/core/test/unit/utils/model-utils.test.ts
Imported Node module and sinon, primed the cache, and added a test that spies on Module._load and asserts ../model is not re-resolved across repeated isModelStatic calls.

🎯 2 (Simple) | ⏱️ ~10 minutes

🐰 A rabbit nudges the module gate,

warms the cache and stops the wait,
require called once, then off we hop,
no extra loads — the CPU can stop,
caching snug, the queries hop to crop.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and concisely describes the main change: caching the Model class to avoid repeated require() calls in isModelStatic.
Linked Issues check ✅ Passed The PR fully addresses issue #18233 by implementing module-level caching of the Model class and adding a test that verifies Module._load is not re-invoked on subsequent calls.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the stated objective: optimizing isModelStatic performance through caching, with corresponding test additions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/test/unit/utils/model-utils.test.ts`:
- Around line 14-37: This test mutates Sequelize model state but the file lacks
per-test lifecycle hooks; add a beforeEach that calls sequelize.sync({ force:
true }) (or equivalent reset) and an afterEach that closes the Sequelize
instance (sequelize.close()) to ensure isolation between tests, and update any
tests referencing MyModel or calling isModelStatic to rely on those hooks so
Sequelize instances and models are reset between tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d67f9ece-e460-46b8-9dc8-197179d3c1cb

📥 Commits

Reviewing files that changed from the base of the PR and between 8260c29 and 1ef312e.

📒 Files selected for processing (2)
  • packages/core/src/utils/model-utils.ts
  • packages/core/test/unit/utils/model-utils.test.ts

Comment thread packages/core/test/unit/utils/model-utils.test.ts
Svehla and others added 2 commits June 5, 2026 13:34
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

isModelStatic re-runs require('../model') on every call (redundant module resolution on a hot path)

1 participant