feat(orm): implement $is sub-model filter for delegate base models - #2559

Closed
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev
Closed

feat(orm): implement $is sub-model filter for delegate base models#2559
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev

Conversation

@motopods

@motopodsmotopods commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Adds support for the $is filter operator on delegate (polymorphic) base model where clauses, enabling queries that filter by sub-model-specific fields.

What's new

When querying from a delegate base model, use $is to filter by sub-model type and/or sub-model-specific fields. Multiple sub-model entries are combined with OR semantics.

// Return only Video assets (includes RatedVideo since it extends Video)awaitclient.asset.findMany({where: {$is: {video: {}}},});// true value is equivalent to an empty filter — same as aboveawaitclient.asset.findMany({where: {$is: {video: true}},});// Filter on a sub-model-specific fieldawaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}}}},});// Combine a base field filter with a sub-model filter (AND)awaitclient.asset.findMany({where: {viewCount: {gt: 0},$is: {video: {duration: {gt: 100}}},},});// Multiple sub-models — OR semantics// Returns: (Videos with duration > 100) OR (Images with format 'png')awaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}},image: {format: 'png'},},},});// Nested $is — for multi-level delegate hierarchies// Asset.$is.video.$is.ratedVideoawaitclient.asset.findMany({where: {$is: {video: {$is: {ratedVideo: {rating: 5}}},},},});// $is on an intermediate delegateawaitclient.video.findMany({where: {$is: {ratedVideo: {rating: 5}}},});
  • Correlated EXISTS subqueries — Filters are translated into EXISTS (SELECT 1 FROM <submodel> WHERE id = <base>.id AND <discriminator> AND <sub-fields>) SQL, ensuring correct row-level scoping.
  • Type-safe WhereInput$SubModelWhereInput types are generated in crud-types.ts so TypeScript enforces valid sub-model fields at compile time.
  • Runtime Zod validationzod/factory.ts validates $is payloads at runtime, giving clear error messages for unknown sub-models or invalid field shapes.

Files changed

FileChange
packages/orm/src/client/crud-types.tsAdded $SubModelWhereInput and wired $is into WhereInput
packages/orm/src/client/crud/dialects/base-dialect.tsAdded buildIsFilter to emit correlated EXISTS subqueries
packages/orm/src/client/zod/factory.tsAdded runtime Zod validation for $is payloads
tests/e2e/orm/client-api/delegate.test.tsEnd-to-end tests covering basic and nested $is filtering

Close#1740

Summary by CodeRabbit

  • New Features

    • Added a top-level $is filter for delegate (polymorphic) base models to filter by sub-model type, including nested $is conditions, OR-combined multi-sub-model queries, and correlated-subquery semantics for inherited/base fields. Input validation now recognizes $is in where/filter schemas.
  • Tests

    • Added e2e coverage validating $is behavior across base models, nested $is paths, nested predicates, and multi-sub-model queries.

@coderabbitai

coderabbitaiBot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a $is polymorphic sub-model filter: type definitions, query-predicate construction, Zod where-schema generation, and end-to-end tests enabling base-model queries narrowed by concrete sub-model conditions (including OR semantics and nested $is).

Changes

Cohort / File(s)Summary
Type System
packages/orm/src/client/crud-types.ts
Added SubModelWhereInput and extended WhereInput for delegate models to optionally include $is?: SubModelWhereInput<...>, where each sub-model key maps to true or a nested WhereInput.
Query Builder / Dialect
packages/orm/src/client/crud/dialects/base-dialect.ts
Added buildIsFilter handling in buildFilter: validates discriminator, maps camelCase keys to discriminator values, enforces discriminator equality, and, for nested filters, adds correlated EXISTS subqueries joined on id fields. Multiple sub-model entries are OR-combined.
Schema Validation (Zod)
packages/orm/src/client/zod/factory.ts
makeWhereSchema now conditionally inserts an optional top-level $is strict object for delegate models; keys are lowercased sub-model names and values are true or a lazily-evaluated nested where schema.
Tests
tests/e2e/orm/client-api/delegate.test.ts
Added e2e tests for $is on base models: true matches, nested sub-model predicates, combinations with base-model filters, OR across sub-models, and nested $is paths; additional seeded data included.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I'm a rabbit in the model wood,
I sniff the $is where sub-models stood,
I hop through predicates, small and grand,
Combining checks with a correlated strand,
Polymorphic hops — query fields in hand! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check✅ PassedThe PR fully addresses the primary objectives of issue #1740: enabling queries on delegate base models using sub-model-specific properties with type safety and developer-friendly API.
Out of Scope Changes check✅ PassedAll changes are directly related to implementing the $is sub-model filter feature; no unrelated modifications are present in the PR.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title 'feat(orm): implement $is sub-model filter for delegate base models' is directly related to the main change in the changeset, which introduces a new $is filter operator for delegate (polymorphic) base models across type definitions, dialect logic, validation, and tests.

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

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

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.

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 Hi, what about you think of this?

@sanny-io

Copy link
Copy Markdown
Contributor

This is very interesting. It may be better to use the lowercase-first name of the model (Video -> video).

Also, null matching the first query seems odd to me. Might it be better to use booleans, instead?

awaitclient.asset.findMany({where: {$is: {video: true}},});

@motopods

Copy link
Copy Markdown
ContributorAuthor

@sanny-io You make a lot of sense.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 367-382: The EXISTS subquery is built manually (using tmpAlias,
selectFrom and this.eb.exists) which bypasses delegate-aware helpers and causes
inherited-delegate filters from this.buildFilter to reference outer tables
incorrectly; replace the manual existsSubquery construction with the
dialect-aware helper (use the existing buildExistsExpression /
buildDelegateQueryHelpers pathway) so the subquery is created via the same
delegate-aware query builder, passing subModelName, subAlias, joinConditions and
subWhere (or subWhereFilter) into that helper instead of calling
this.eb.selectFrom directly; ensure the final condition still uses
discriminatorCheck and the helper-produced exists expression (instead of
this.eb.exists(existsSubquery)) so dialect-specific EXISTS workarounds and
proper alias joins are preserved.
In `@packages/orm/src/client/zod/factory.ts`:
- Around line 490-503: The $is filter currently uses z.object(...) which strips
unknown sub-model keys and hides typos; update the construction of
subModelSchema to use z.strictObject(...) instead (i.e. replace z.object(...)
with z.strictObject(...)) so unknown sub-model names will be rejected at
validation time; keep the same Object.fromEntries mapping of subModel names and
the existing union that references this.makeWhereSchema, and preserve the
.optional() on subModelSchema before assigning it to fields['$is'].
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7ffa087-26a0-464d-9e91-d1bac09d8a0b

📥 Commits

Reviewing files that changed from the base of the PR and between 39a0a28 and d62333d.

📒 Files selected for processing (4)
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/zod/factory.ts
  • tests/e2e/orm/client-api/delegate.test.ts

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts Outdated
Comment threadpackages/orm/src/client/zod/factory.ts Outdated
@motopodsmotopods changed the title implement $is sub-model filter for delegate base modelsfit(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@motopodsmotopods changed the title fit(orm): implement $is sub-model filter for delegate base modelsfeat(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@ymc9

ymc9 commented Apr 12, 2026

Copy link
Copy Markdown
Member

Hi @motopods , it's a very useful feature and I'm glad you're tackling it!

I'm not sure if we need to introduce the $is key. Do you think we can instead rely on the discriminator field for smart typing? Like:

awaitclient.asset.findMany({where: {type: 'Video',duration: {gt: 100}},});

The filter's typing is then a discriminated union with each component fixing the delegate type and allowing its concrete fields.

What do you think?

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 That makes a lot of sense. I’ll try this approach and see if it works.

@motopods
motopods marked this pull request as draft April 12, 2026 02:43
Sign up for freeto 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.

[Feature Request] Polymorphic Models: Query base model with concrete model's props

3 participants

@motopods@sanny-io@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(orm): implement $is sub-model filter for delegate base models - #2559

Closed
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev
Closed

feat(orm): implement $is sub-model filter for delegate base models#2559
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev

Conversation

@motopods

@motopodsmotopods commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Adds support for the $is filter operator on delegate (polymorphic) base model where clauses, enabling queries that filter by sub-model-specific fields.

What's new

When querying from a delegate base model, use $is to filter by sub-model type and/or sub-model-specific fields. Multiple sub-model entries are combined with OR semantics.

// Return only Video assets (includes RatedVideo since it extends Video)awaitclient.asset.findMany({where: {$is: {video: {}}},});// true value is equivalent to an empty filter — same as aboveawaitclient.asset.findMany({where: {$is: {video: true}},});// Filter on a sub-model-specific fieldawaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}}}},});// Combine a base field filter with a sub-model filter (AND)awaitclient.asset.findMany({where: {viewCount: {gt: 0},$is: {video: {duration: {gt: 100}}},},});// Multiple sub-models — OR semantics// Returns: (Videos with duration > 100) OR (Images with format 'png')awaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}},image: {format: 'png'},},},});// Nested $is — for multi-level delegate hierarchies// Asset.$is.video.$is.ratedVideoawaitclient.asset.findMany({where: {$is: {video: {$is: {ratedVideo: {rating: 5}}},},},});// $is on an intermediate delegateawaitclient.video.findMany({where: {$is: {ratedVideo: {rating: 5}}},});
  • Correlated EXISTS subqueries — Filters are translated into EXISTS (SELECT 1 FROM <submodel> WHERE id = <base>.id AND <discriminator> AND <sub-fields>) SQL, ensuring correct row-level scoping.
  • Type-safe WhereInput$SubModelWhereInput types are generated in crud-types.ts so TypeScript enforces valid sub-model fields at compile time.
  • Runtime Zod validationzod/factory.ts validates $is payloads at runtime, giving clear error messages for unknown sub-models or invalid field shapes.

Files changed

FileChange
packages/orm/src/client/crud-types.tsAdded $SubModelWhereInput and wired $is into WhereInput
packages/orm/src/client/crud/dialects/base-dialect.tsAdded buildIsFilter to emit correlated EXISTS subqueries
packages/orm/src/client/zod/factory.tsAdded runtime Zod validation for $is payloads
tests/e2e/orm/client-api/delegate.test.tsEnd-to-end tests covering basic and nested $is filtering

Close#1740

Summary by CodeRabbit

  • New Features

    • Added a top-level $is filter for delegate (polymorphic) base models to filter by sub-model type, including nested $is conditions, OR-combined multi-sub-model queries, and correlated-subquery semantics for inherited/base fields. Input validation now recognizes $is in where/filter schemas.
  • Tests

    • Added e2e coverage validating $is behavior across base models, nested $is paths, nested predicates, and multi-sub-model queries.

@coderabbitai

coderabbitaiBot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a $is polymorphic sub-model filter: type definitions, query-predicate construction, Zod where-schema generation, and end-to-end tests enabling base-model queries narrowed by concrete sub-model conditions (including OR semantics and nested $is).

Changes

Cohort / File(s)Summary
Type System
packages/orm/src/client/crud-types.ts
Added SubModelWhereInput and extended WhereInput for delegate models to optionally include $is?: SubModelWhereInput<...>, where each sub-model key maps to true or a nested WhereInput.
Query Builder / Dialect
packages/orm/src/client/crud/dialects/base-dialect.ts
Added buildIsFilter handling in buildFilter: validates discriminator, maps camelCase keys to discriminator values, enforces discriminator equality, and, for nested filters, adds correlated EXISTS subqueries joined on id fields. Multiple sub-model entries are OR-combined.
Schema Validation (Zod)
packages/orm/src/client/zod/factory.ts
makeWhereSchema now conditionally inserts an optional top-level $is strict object for delegate models; keys are lowercased sub-model names and values are true or a lazily-evaluated nested where schema.
Tests
tests/e2e/orm/client-api/delegate.test.ts
Added e2e tests for $is on base models: true matches, nested sub-model predicates, combinations with base-model filters, OR across sub-models, and nested $is paths; additional seeded data included.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I'm a rabbit in the model wood,
I sniff the $is where sub-models stood,
I hop through predicates, small and grand,
Combining checks with a correlated strand,
Polymorphic hops — query fields in hand! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check✅ PassedThe PR fully addresses the primary objectives of issue #1740: enabling queries on delegate base models using sub-model-specific properties with type safety and developer-friendly API.
Out of Scope Changes check✅ PassedAll changes are directly related to implementing the $is sub-model filter feature; no unrelated modifications are present in the PR.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title 'feat(orm): implement $is sub-model filter for delegate base models' is directly related to the main change in the changeset, which introduces a new $is filter operator for delegate (polymorphic) base models across type definitions, dialect logic, validation, and tests.

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

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

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.

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 Hi, what about you think of this?

@sanny-io

Copy link
Copy Markdown
Contributor

This is very interesting. It may be better to use the lowercase-first name of the model (Video -> video).

Also, null matching the first query seems odd to me. Might it be better to use booleans, instead?

awaitclient.asset.findMany({where: {$is: {video: true}},});

@motopods

Copy link
Copy Markdown
ContributorAuthor

@sanny-io You make a lot of sense.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 367-382: The EXISTS subquery is built manually (using tmpAlias,
selectFrom and this.eb.exists) which bypasses delegate-aware helpers and causes
inherited-delegate filters from this.buildFilter to reference outer tables
incorrectly; replace the manual existsSubquery construction with the
dialect-aware helper (use the existing buildExistsExpression /
buildDelegateQueryHelpers pathway) so the subquery is created via the same
delegate-aware query builder, passing subModelName, subAlias, joinConditions and
subWhere (or subWhereFilter) into that helper instead of calling
this.eb.selectFrom directly; ensure the final condition still uses
discriminatorCheck and the helper-produced exists expression (instead of
this.eb.exists(existsSubquery)) so dialect-specific EXISTS workarounds and
proper alias joins are preserved.
In `@packages/orm/src/client/zod/factory.ts`:
- Around line 490-503: The $is filter currently uses z.object(...) which strips
unknown sub-model keys and hides typos; update the construction of
subModelSchema to use z.strictObject(...) instead (i.e. replace z.object(...)
with z.strictObject(...)) so unknown sub-model names will be rejected at
validation time; keep the same Object.fromEntries mapping of subModel names and
the existing union that references this.makeWhereSchema, and preserve the
.optional() on subModelSchema before assigning it to fields['$is'].
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7ffa087-26a0-464d-9e91-d1bac09d8a0b

📥 Commits

Reviewing files that changed from the base of the PR and between 39a0a28 and d62333d.

📒 Files selected for processing (4)
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/zod/factory.ts
  • tests/e2e/orm/client-api/delegate.test.ts

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts Outdated
Comment threadpackages/orm/src/client/zod/factory.ts Outdated
@motopodsmotopods changed the title implement $is sub-model filter for delegate base modelsfit(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@motopodsmotopods changed the title fit(orm): implement $is sub-model filter for delegate base modelsfeat(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@ymc9

ymc9 commented Apr 12, 2026

Copy link
Copy Markdown
Member

Hi @motopods , it's a very useful feature and I'm glad you're tackling it!

I'm not sure if we need to introduce the $is key. Do you think we can instead rely on the discriminator field for smart typing? Like:

awaitclient.asset.findMany({where: {type: 'Video',duration: {gt: 100}},});

The filter's typing is then a discriminated union with each component fixing the delegate type and allowing its concrete fields.

What do you think?

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 That makes a lot of sense. I’ll try this approach and see if it works.

@motopods
motopods marked this pull request as draft April 12, 2026 02:43
Sign up for freeto 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.

[Feature Request] Polymorphic Models: Query base model with concrete model's props

3 participants

@motopods@sanny-io@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(orm): implement $is sub-model filter for delegate base models - #2559

Closed
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev
Closed

feat(orm): implement $is sub-model filter for delegate base models#2559
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev

Conversation

@motopods

@motopodsmotopods commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Adds support for the $is filter operator on delegate (polymorphic) base model where clauses, enabling queries that filter by sub-model-specific fields.

What's new

When querying from a delegate base model, use $is to filter by sub-model type and/or sub-model-specific fields. Multiple sub-model entries are combined with OR semantics.

// Return only Video assets (includes RatedVideo since it extends Video)awaitclient.asset.findMany({where: {$is: {video: {}}},});// true value is equivalent to an empty filter — same as aboveawaitclient.asset.findMany({where: {$is: {video: true}},});// Filter on a sub-model-specific fieldawaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}}}},});// Combine a base field filter with a sub-model filter (AND)awaitclient.asset.findMany({where: {viewCount: {gt: 0},$is: {video: {duration: {gt: 100}}},},});// Multiple sub-models — OR semantics// Returns: (Videos with duration > 100) OR (Images with format 'png')awaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}},image: {format: 'png'},},},});// Nested $is — for multi-level delegate hierarchies// Asset.$is.video.$is.ratedVideoawaitclient.asset.findMany({where: {$is: {video: {$is: {ratedVideo: {rating: 5}}},},},});// $is on an intermediate delegateawaitclient.video.findMany({where: {$is: {ratedVideo: {rating: 5}}},});
  • Correlated EXISTS subqueries — Filters are translated into EXISTS (SELECT 1 FROM <submodel> WHERE id = <base>.id AND <discriminator> AND <sub-fields>) SQL, ensuring correct row-level scoping.
  • Type-safe WhereInput$SubModelWhereInput types are generated in crud-types.ts so TypeScript enforces valid sub-model fields at compile time.
  • Runtime Zod validationzod/factory.ts validates $is payloads at runtime, giving clear error messages for unknown sub-models or invalid field shapes.

Files changed

FileChange
packages/orm/src/client/crud-types.tsAdded $SubModelWhereInput and wired $is into WhereInput
packages/orm/src/client/crud/dialects/base-dialect.tsAdded buildIsFilter to emit correlated EXISTS subqueries
packages/orm/src/client/zod/factory.tsAdded runtime Zod validation for $is payloads
tests/e2e/orm/client-api/delegate.test.tsEnd-to-end tests covering basic and nested $is filtering

Close#1740

Summary by CodeRabbit

  • New Features

    • Added a top-level $is filter for delegate (polymorphic) base models to filter by sub-model type, including nested $is conditions, OR-combined multi-sub-model queries, and correlated-subquery semantics for inherited/base fields. Input validation now recognizes $is in where/filter schemas.
  • Tests

    • Added e2e coverage validating $is behavior across base models, nested $is paths, nested predicates, and multi-sub-model queries.

@coderabbitai

coderabbitaiBot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a $is polymorphic sub-model filter: type definitions, query-predicate construction, Zod where-schema generation, and end-to-end tests enabling base-model queries narrowed by concrete sub-model conditions (including OR semantics and nested $is).

Changes

Cohort / File(s)Summary
Type System
packages/orm/src/client/crud-types.ts
Added SubModelWhereInput and extended WhereInput for delegate models to optionally include $is?: SubModelWhereInput<...>, where each sub-model key maps to true or a nested WhereInput.
Query Builder / Dialect
packages/orm/src/client/crud/dialects/base-dialect.ts
Added buildIsFilter handling in buildFilter: validates discriminator, maps camelCase keys to discriminator values, enforces discriminator equality, and, for nested filters, adds correlated EXISTS subqueries joined on id fields. Multiple sub-model entries are OR-combined.
Schema Validation (Zod)
packages/orm/src/client/zod/factory.ts
makeWhereSchema now conditionally inserts an optional top-level $is strict object for delegate models; keys are lowercased sub-model names and values are true or a lazily-evaluated nested where schema.
Tests
tests/e2e/orm/client-api/delegate.test.ts
Added e2e tests for $is on base models: true matches, nested sub-model predicates, combinations with base-model filters, OR across sub-models, and nested $is paths; additional seeded data included.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I'm a rabbit in the model wood,
I sniff the $is where sub-models stood,
I hop through predicates, small and grand,
Combining checks with a correlated strand,
Polymorphic hops — query fields in hand! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check✅ PassedThe PR fully addresses the primary objectives of issue #1740: enabling queries on delegate base models using sub-model-specific properties with type safety and developer-friendly API.
Out of Scope Changes check✅ PassedAll changes are directly related to implementing the $is sub-model filter feature; no unrelated modifications are present in the PR.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title 'feat(orm): implement $is sub-model filter for delegate base models' is directly related to the main change in the changeset, which introduces a new $is filter operator for delegate (polymorphic) base models across type definitions, dialect logic, validation, and tests.

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

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

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.

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 Hi, what about you think of this?

@sanny-io

Copy link
Copy Markdown
Contributor

This is very interesting. It may be better to use the lowercase-first name of the model (Video -> video).

Also, null matching the first query seems odd to me. Might it be better to use booleans, instead?

awaitclient.asset.findMany({where: {$is: {video: true}},});

@motopods

Copy link
Copy Markdown
ContributorAuthor

@sanny-io You make a lot of sense.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 367-382: The EXISTS subquery is built manually (using tmpAlias,
selectFrom and this.eb.exists) which bypasses delegate-aware helpers and causes
inherited-delegate filters from this.buildFilter to reference outer tables
incorrectly; replace the manual existsSubquery construction with the
dialect-aware helper (use the existing buildExistsExpression /
buildDelegateQueryHelpers pathway) so the subquery is created via the same
delegate-aware query builder, passing subModelName, subAlias, joinConditions and
subWhere (or subWhereFilter) into that helper instead of calling
this.eb.selectFrom directly; ensure the final condition still uses
discriminatorCheck and the helper-produced exists expression (instead of
this.eb.exists(existsSubquery)) so dialect-specific EXISTS workarounds and
proper alias joins are preserved.
In `@packages/orm/src/client/zod/factory.ts`:
- Around line 490-503: The $is filter currently uses z.object(...) which strips
unknown sub-model keys and hides typos; update the construction of
subModelSchema to use z.strictObject(...) instead (i.e. replace z.object(...)
with z.strictObject(...)) so unknown sub-model names will be rejected at
validation time; keep the same Object.fromEntries mapping of subModel names and
the existing union that references this.makeWhereSchema, and preserve the
.optional() on subModelSchema before assigning it to fields['$is'].
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7ffa087-26a0-464d-9e91-d1bac09d8a0b

📥 Commits

Reviewing files that changed from the base of the PR and between 39a0a28 and d62333d.

📒 Files selected for processing (4)
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/zod/factory.ts
  • tests/e2e/orm/client-api/delegate.test.ts

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts Outdated
Comment threadpackages/orm/src/client/zod/factory.ts Outdated
@motopodsmotopods changed the title implement $is sub-model filter for delegate base modelsfit(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@motopodsmotopods changed the title fit(orm): implement $is sub-model filter for delegate base modelsfeat(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@ymc9

ymc9 commented Apr 12, 2026

Copy link
Copy Markdown
Member

Hi @motopods , it's a very useful feature and I'm glad you're tackling it!

I'm not sure if we need to introduce the $is key. Do you think we can instead rely on the discriminator field for smart typing? Like:

awaitclient.asset.findMany({where: {type: 'Video',duration: {gt: 100}},});

The filter's typing is then a discriminated union with each component fixing the delegate type and allowing its concrete fields.

What do you think?

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 That makes a lot of sense. I’ll try this approach and see if it works.

@motopods
motopods marked this pull request as draft April 12, 2026 02:43
Sign up for freeto 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.

[Feature Request] Polymorphic Models: Query base model with concrete model's props

3 participants

@motopods@sanny-io@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(orm): implement $is sub-model filter for delegate base models - #2559

Closed
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev
Closed

feat(orm): implement $is sub-model filter for delegate base models#2559
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev

Conversation

@motopods

@motopodsmotopods commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Adds support for the $is filter operator on delegate (polymorphic) base model where clauses, enabling queries that filter by sub-model-specific fields.

What's new

When querying from a delegate base model, use $is to filter by sub-model type and/or sub-model-specific fields. Multiple sub-model entries are combined with OR semantics.

// Return only Video assets (includes RatedVideo since it extends Video)awaitclient.asset.findMany({where: {$is: {video: {}}},});// true value is equivalent to an empty filter — same as aboveawaitclient.asset.findMany({where: {$is: {video: true}},});// Filter on a sub-model-specific fieldawaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}}}},});// Combine a base field filter with a sub-model filter (AND)awaitclient.asset.findMany({where: {viewCount: {gt: 0},$is: {video: {duration: {gt: 100}}},},});// Multiple sub-models — OR semantics// Returns: (Videos with duration > 100) OR (Images with format 'png')awaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}},image: {format: 'png'},},},});// Nested $is — for multi-level delegate hierarchies// Asset.$is.video.$is.ratedVideoawaitclient.asset.findMany({where: {$is: {video: {$is: {ratedVideo: {rating: 5}}},},},});// $is on an intermediate delegateawaitclient.video.findMany({where: {$is: {ratedVideo: {rating: 5}}},});
  • Correlated EXISTS subqueries — Filters are translated into EXISTS (SELECT 1 FROM <submodel> WHERE id = <base>.id AND <discriminator> AND <sub-fields>) SQL, ensuring correct row-level scoping.
  • Type-safe WhereInput$SubModelWhereInput types are generated in crud-types.ts so TypeScript enforces valid sub-model fields at compile time.
  • Runtime Zod validationzod/factory.ts validates $is payloads at runtime, giving clear error messages for unknown sub-models or invalid field shapes.

Files changed

FileChange
packages/orm/src/client/crud-types.tsAdded $SubModelWhereInput and wired $is into WhereInput
packages/orm/src/client/crud/dialects/base-dialect.tsAdded buildIsFilter to emit correlated EXISTS subqueries
packages/orm/src/client/zod/factory.tsAdded runtime Zod validation for $is payloads
tests/e2e/orm/client-api/delegate.test.tsEnd-to-end tests covering basic and nested $is filtering

Close#1740

Summary by CodeRabbit

  • New Features

    • Added a top-level $is filter for delegate (polymorphic) base models to filter by sub-model type, including nested $is conditions, OR-combined multi-sub-model queries, and correlated-subquery semantics for inherited/base fields. Input validation now recognizes $is in where/filter schemas.
  • Tests

    • Added e2e coverage validating $is behavior across base models, nested $is paths, nested predicates, and multi-sub-model queries.

@coderabbitai

coderabbitaiBot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a $is polymorphic sub-model filter: type definitions, query-predicate construction, Zod where-schema generation, and end-to-end tests enabling base-model queries narrowed by concrete sub-model conditions (including OR semantics and nested $is).

Changes

Cohort / File(s)Summary
Type System
packages/orm/src/client/crud-types.ts
Added SubModelWhereInput and extended WhereInput for delegate models to optionally include $is?: SubModelWhereInput<...>, where each sub-model key maps to true or a nested WhereInput.
Query Builder / Dialect
packages/orm/src/client/crud/dialects/base-dialect.ts
Added buildIsFilter handling in buildFilter: validates discriminator, maps camelCase keys to discriminator values, enforces discriminator equality, and, for nested filters, adds correlated EXISTS subqueries joined on id fields. Multiple sub-model entries are OR-combined.
Schema Validation (Zod)
packages/orm/src/client/zod/factory.ts
makeWhereSchema now conditionally inserts an optional top-level $is strict object for delegate models; keys are lowercased sub-model names and values are true or a lazily-evaluated nested where schema.
Tests
tests/e2e/orm/client-api/delegate.test.ts
Added e2e tests for $is on base models: true matches, nested sub-model predicates, combinations with base-model filters, OR across sub-models, and nested $is paths; additional seeded data included.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I'm a rabbit in the model wood,
I sniff the $is where sub-models stood,
I hop through predicates, small and grand,
Combining checks with a correlated strand,
Polymorphic hops — query fields in hand! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check✅ PassedThe PR fully addresses the primary objectives of issue #1740: enabling queries on delegate base models using sub-model-specific properties with type safety and developer-friendly API.
Out of Scope Changes check✅ PassedAll changes are directly related to implementing the $is sub-model filter feature; no unrelated modifications are present in the PR.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title 'feat(orm): implement $is sub-model filter for delegate base models' is directly related to the main change in the changeset, which introduces a new $is filter operator for delegate (polymorphic) base models across type definitions, dialect logic, validation, and tests.

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

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

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.

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 Hi, what about you think of this?

@sanny-io

Copy link
Copy Markdown
Contributor

This is very interesting. It may be better to use the lowercase-first name of the model (Video -> video).

Also, null matching the first query seems odd to me. Might it be better to use booleans, instead?

awaitclient.asset.findMany({where: {$is: {video: true}},});

@motopods

Copy link
Copy Markdown
ContributorAuthor

@sanny-io You make a lot of sense.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 367-382: The EXISTS subquery is built manually (using tmpAlias,
selectFrom and this.eb.exists) which bypasses delegate-aware helpers and causes
inherited-delegate filters from this.buildFilter to reference outer tables
incorrectly; replace the manual existsSubquery construction with the
dialect-aware helper (use the existing buildExistsExpression /
buildDelegateQueryHelpers pathway) so the subquery is created via the same
delegate-aware query builder, passing subModelName, subAlias, joinConditions and
subWhere (or subWhereFilter) into that helper instead of calling
this.eb.selectFrom directly; ensure the final condition still uses
discriminatorCheck and the helper-produced exists expression (instead of
this.eb.exists(existsSubquery)) so dialect-specific EXISTS workarounds and
proper alias joins are preserved.
In `@packages/orm/src/client/zod/factory.ts`:
- Around line 490-503: The $is filter currently uses z.object(...) which strips
unknown sub-model keys and hides typos; update the construction of
subModelSchema to use z.strictObject(...) instead (i.e. replace z.object(...)
with z.strictObject(...)) so unknown sub-model names will be rejected at
validation time; keep the same Object.fromEntries mapping of subModel names and
the existing union that references this.makeWhereSchema, and preserve the
.optional() on subModelSchema before assigning it to fields['$is'].
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7ffa087-26a0-464d-9e91-d1bac09d8a0b

📥 Commits

Reviewing files that changed from the base of the PR and between 39a0a28 and d62333d.

📒 Files selected for processing (4)
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/zod/factory.ts
  • tests/e2e/orm/client-api/delegate.test.ts

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts Outdated
Comment threadpackages/orm/src/client/zod/factory.ts Outdated
@motopodsmotopods changed the title implement $is sub-model filter for delegate base modelsfit(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@motopodsmotopods changed the title fit(orm): implement $is sub-model filter for delegate base modelsfeat(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@ymc9

ymc9 commented Apr 12, 2026

Copy link
Copy Markdown
Member

Hi @motopods , it's a very useful feature and I'm glad you're tackling it!

I'm not sure if we need to introduce the $is key. Do you think we can instead rely on the discriminator field for smart typing? Like:

awaitclient.asset.findMany({where: {type: 'Video',duration: {gt: 100}},});

The filter's typing is then a discriminated union with each component fixing the delegate type and allowing its concrete fields.

What do you think?

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 That makes a lot of sense. I’ll try this approach and see if it works.

@motopods
motopods marked this pull request as draft April 12, 2026 02:43
Sign up for freeto 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.

[Feature Request] Polymorphic Models: Query base model with concrete model's props

3 participants

@motopods@sanny-io@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(orm): implement $is sub-model filter for delegate base models - #2559

Closed
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev
Closed

feat(orm): implement $is sub-model filter for delegate base models#2559
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev

Conversation

@motopods

@motopodsmotopods commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Adds support for the $is filter operator on delegate (polymorphic) base model where clauses, enabling queries that filter by sub-model-specific fields.

What's new

When querying from a delegate base model, use $is to filter by sub-model type and/or sub-model-specific fields. Multiple sub-model entries are combined with OR semantics.

// Return only Video assets (includes RatedVideo since it extends Video)awaitclient.asset.findMany({where: {$is: {video: {}}},});// true value is equivalent to an empty filter — same as aboveawaitclient.asset.findMany({where: {$is: {video: true}},});// Filter on a sub-model-specific fieldawaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}}}},});// Combine a base field filter with a sub-model filter (AND)awaitclient.asset.findMany({where: {viewCount: {gt: 0},$is: {video: {duration: {gt: 100}}},},});// Multiple sub-models — OR semantics// Returns: (Videos with duration > 100) OR (Images with format 'png')awaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}},image: {format: 'png'},},},});// Nested $is — for multi-level delegate hierarchies// Asset.$is.video.$is.ratedVideoawaitclient.asset.findMany({where: {$is: {video: {$is: {ratedVideo: {rating: 5}}},},},});// $is on an intermediate delegateawaitclient.video.findMany({where: {$is: {ratedVideo: {rating: 5}}},});
  • Correlated EXISTS subqueries — Filters are translated into EXISTS (SELECT 1 FROM <submodel> WHERE id = <base>.id AND <discriminator> AND <sub-fields>) SQL, ensuring correct row-level scoping.
  • Type-safe WhereInput$SubModelWhereInput types are generated in crud-types.ts so TypeScript enforces valid sub-model fields at compile time.
  • Runtime Zod validationzod/factory.ts validates $is payloads at runtime, giving clear error messages for unknown sub-models or invalid field shapes.

Files changed

FileChange
packages/orm/src/client/crud-types.tsAdded $SubModelWhereInput and wired $is into WhereInput
packages/orm/src/client/crud/dialects/base-dialect.tsAdded buildIsFilter to emit correlated EXISTS subqueries
packages/orm/src/client/zod/factory.tsAdded runtime Zod validation for $is payloads
tests/e2e/orm/client-api/delegate.test.tsEnd-to-end tests covering basic and nested $is filtering

Close#1740

Summary by CodeRabbit

  • New Features

    • Added a top-level $is filter for delegate (polymorphic) base models to filter by sub-model type, including nested $is conditions, OR-combined multi-sub-model queries, and correlated-subquery semantics for inherited/base fields. Input validation now recognizes $is in where/filter schemas.
  • Tests

    • Added e2e coverage validating $is behavior across base models, nested $is paths, nested predicates, and multi-sub-model queries.

@coderabbitai

coderabbitaiBot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a $is polymorphic sub-model filter: type definitions, query-predicate construction, Zod where-schema generation, and end-to-end tests enabling base-model queries narrowed by concrete sub-model conditions (including OR semantics and nested $is).

Changes

Cohort / File(s)Summary
Type System
packages/orm/src/client/crud-types.ts
Added SubModelWhereInput and extended WhereInput for delegate models to optionally include $is?: SubModelWhereInput<...>, where each sub-model key maps to true or a nested WhereInput.
Query Builder / Dialect
packages/orm/src/client/crud/dialects/base-dialect.ts
Added buildIsFilter handling in buildFilter: validates discriminator, maps camelCase keys to discriminator values, enforces discriminator equality, and, for nested filters, adds correlated EXISTS subqueries joined on id fields. Multiple sub-model entries are OR-combined.
Schema Validation (Zod)
packages/orm/src/client/zod/factory.ts
makeWhereSchema now conditionally inserts an optional top-level $is strict object for delegate models; keys are lowercased sub-model names and values are true or a lazily-evaluated nested where schema.
Tests
tests/e2e/orm/client-api/delegate.test.ts
Added e2e tests for $is on base models: true matches, nested sub-model predicates, combinations with base-model filters, OR across sub-models, and nested $is paths; additional seeded data included.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I'm a rabbit in the model wood,
I sniff the $is where sub-models stood,
I hop through predicates, small and grand,
Combining checks with a correlated strand,
Polymorphic hops — query fields in hand! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check✅ PassedThe PR fully addresses the primary objectives of issue #1740: enabling queries on delegate base models using sub-model-specific properties with type safety and developer-friendly API.
Out of Scope Changes check✅ PassedAll changes are directly related to implementing the $is sub-model filter feature; no unrelated modifications are present in the PR.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title 'feat(orm): implement $is sub-model filter for delegate base models' is directly related to the main change in the changeset, which introduces a new $is filter operator for delegate (polymorphic) base models across type definitions, dialect logic, validation, and tests.

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

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

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.

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 Hi, what about you think of this?

@sanny-io

Copy link
Copy Markdown
Contributor

This is very interesting. It may be better to use the lowercase-first name of the model (Video -> video).

Also, null matching the first query seems odd to me. Might it be better to use booleans, instead?

awaitclient.asset.findMany({where: {$is: {video: true}},});

@motopods

Copy link
Copy Markdown
ContributorAuthor

@sanny-io You make a lot of sense.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 367-382: The EXISTS subquery is built manually (using tmpAlias,
selectFrom and this.eb.exists) which bypasses delegate-aware helpers and causes
inherited-delegate filters from this.buildFilter to reference outer tables
incorrectly; replace the manual existsSubquery construction with the
dialect-aware helper (use the existing buildExistsExpression /
buildDelegateQueryHelpers pathway) so the subquery is created via the same
delegate-aware query builder, passing subModelName, subAlias, joinConditions and
subWhere (or subWhereFilter) into that helper instead of calling
this.eb.selectFrom directly; ensure the final condition still uses
discriminatorCheck and the helper-produced exists expression (instead of
this.eb.exists(existsSubquery)) so dialect-specific EXISTS workarounds and
proper alias joins are preserved.
In `@packages/orm/src/client/zod/factory.ts`:
- Around line 490-503: The $is filter currently uses z.object(...) which strips
unknown sub-model keys and hides typos; update the construction of
subModelSchema to use z.strictObject(...) instead (i.e. replace z.object(...)
with z.strictObject(...)) so unknown sub-model names will be rejected at
validation time; keep the same Object.fromEntries mapping of subModel names and
the existing union that references this.makeWhereSchema, and preserve the
.optional() on subModelSchema before assigning it to fields['$is'].
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7ffa087-26a0-464d-9e91-d1bac09d8a0b

📥 Commits

Reviewing files that changed from the base of the PR and between 39a0a28 and d62333d.

📒 Files selected for processing (4)
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/zod/factory.ts
  • tests/e2e/orm/client-api/delegate.test.ts

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts Outdated
Comment threadpackages/orm/src/client/zod/factory.ts Outdated
@motopodsmotopods changed the title implement $is sub-model filter for delegate base modelsfit(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@motopodsmotopods changed the title fit(orm): implement $is sub-model filter for delegate base modelsfeat(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@ymc9

ymc9 commented Apr 12, 2026

Copy link
Copy Markdown
Member

Hi @motopods , it's a very useful feature and I'm glad you're tackling it!

I'm not sure if we need to introduce the $is key. Do you think we can instead rely on the discriminator field for smart typing? Like:

awaitclient.asset.findMany({where: {type: 'Video',duration: {gt: 100}},});

The filter's typing is then a discriminated union with each component fixing the delegate type and allowing its concrete fields.

What do you think?

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 That makes a lot of sense. I’ll try this approach and see if it works.

@motopods
motopods marked this pull request as draft April 12, 2026 02:43
Sign up for freeto 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.

[Feature Request] Polymorphic Models: Query base model with concrete model's props

3 participants

@motopods@sanny-io@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(orm): implement $is sub-model filter for delegate base models - #2559

Closed
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev
Closed

feat(orm): implement $is sub-model filter for delegate base models#2559
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev

Conversation

@motopods

@motopodsmotopods commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Adds support for the $is filter operator on delegate (polymorphic) base model where clauses, enabling queries that filter by sub-model-specific fields.

What's new

When querying from a delegate base model, use $is to filter by sub-model type and/or sub-model-specific fields. Multiple sub-model entries are combined with OR semantics.

// Return only Video assets (includes RatedVideo since it extends Video)awaitclient.asset.findMany({where: {$is: {video: {}}},});// true value is equivalent to an empty filter — same as aboveawaitclient.asset.findMany({where: {$is: {video: true}},});// Filter on a sub-model-specific fieldawaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}}}},});// Combine a base field filter with a sub-model filter (AND)awaitclient.asset.findMany({where: {viewCount: {gt: 0},$is: {video: {duration: {gt: 100}}},},});// Multiple sub-models — OR semantics// Returns: (Videos with duration > 100) OR (Images with format 'png')awaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}},image: {format: 'png'},},},});// Nested $is — for multi-level delegate hierarchies// Asset.$is.video.$is.ratedVideoawaitclient.asset.findMany({where: {$is: {video: {$is: {ratedVideo: {rating: 5}}},},},});// $is on an intermediate delegateawaitclient.video.findMany({where: {$is: {ratedVideo: {rating: 5}}},});
  • Correlated EXISTS subqueries — Filters are translated into EXISTS (SELECT 1 FROM <submodel> WHERE id = <base>.id AND <discriminator> AND <sub-fields>) SQL, ensuring correct row-level scoping.
  • Type-safe WhereInput$SubModelWhereInput types are generated in crud-types.ts so TypeScript enforces valid sub-model fields at compile time.
  • Runtime Zod validationzod/factory.ts validates $is payloads at runtime, giving clear error messages for unknown sub-models or invalid field shapes.

Files changed

FileChange
packages/orm/src/client/crud-types.tsAdded $SubModelWhereInput and wired $is into WhereInput
packages/orm/src/client/crud/dialects/base-dialect.tsAdded buildIsFilter to emit correlated EXISTS subqueries
packages/orm/src/client/zod/factory.tsAdded runtime Zod validation for $is payloads
tests/e2e/orm/client-api/delegate.test.tsEnd-to-end tests covering basic and nested $is filtering

Close#1740

Summary by CodeRabbit

  • New Features

    • Added a top-level $is filter for delegate (polymorphic) base models to filter by sub-model type, including nested $is conditions, OR-combined multi-sub-model queries, and correlated-subquery semantics for inherited/base fields. Input validation now recognizes $is in where/filter schemas.
  • Tests

    • Added e2e coverage validating $is behavior across base models, nested $is paths, nested predicates, and multi-sub-model queries.

@coderabbitai

coderabbitaiBot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a $is polymorphic sub-model filter: type definitions, query-predicate construction, Zod where-schema generation, and end-to-end tests enabling base-model queries narrowed by concrete sub-model conditions (including OR semantics and nested $is).

Changes

Cohort / File(s)Summary
Type System
packages/orm/src/client/crud-types.ts
Added SubModelWhereInput and extended WhereInput for delegate models to optionally include $is?: SubModelWhereInput<...>, where each sub-model key maps to true or a nested WhereInput.
Query Builder / Dialect
packages/orm/src/client/crud/dialects/base-dialect.ts
Added buildIsFilter handling in buildFilter: validates discriminator, maps camelCase keys to discriminator values, enforces discriminator equality, and, for nested filters, adds correlated EXISTS subqueries joined on id fields. Multiple sub-model entries are OR-combined.
Schema Validation (Zod)
packages/orm/src/client/zod/factory.ts
makeWhereSchema now conditionally inserts an optional top-level $is strict object for delegate models; keys are lowercased sub-model names and values are true or a lazily-evaluated nested where schema.
Tests
tests/e2e/orm/client-api/delegate.test.ts
Added e2e tests for $is on base models: true matches, nested sub-model predicates, combinations with base-model filters, OR across sub-models, and nested $is paths; additional seeded data included.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I'm a rabbit in the model wood,
I sniff the $is where sub-models stood,
I hop through predicates, small and grand,
Combining checks with a correlated strand,
Polymorphic hops — query fields in hand! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check✅ PassedThe PR fully addresses the primary objectives of issue #1740: enabling queries on delegate base models using sub-model-specific properties with type safety and developer-friendly API.
Out of Scope Changes check✅ PassedAll changes are directly related to implementing the $is sub-model filter feature; no unrelated modifications are present in the PR.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title 'feat(orm): implement $is sub-model filter for delegate base models' is directly related to the main change in the changeset, which introduces a new $is filter operator for delegate (polymorphic) base models across type definitions, dialect logic, validation, and tests.

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

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

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.

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 Hi, what about you think of this?

@sanny-io

Copy link
Copy Markdown
Contributor

This is very interesting. It may be better to use the lowercase-first name of the model (Video -> video).

Also, null matching the first query seems odd to me. Might it be better to use booleans, instead?

awaitclient.asset.findMany({where: {$is: {video: true}},});

@motopods

Copy link
Copy Markdown
ContributorAuthor

@sanny-io You make a lot of sense.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 367-382: The EXISTS subquery is built manually (using tmpAlias,
selectFrom and this.eb.exists) which bypasses delegate-aware helpers and causes
inherited-delegate filters from this.buildFilter to reference outer tables
incorrectly; replace the manual existsSubquery construction with the
dialect-aware helper (use the existing buildExistsExpression /
buildDelegateQueryHelpers pathway) so the subquery is created via the same
delegate-aware query builder, passing subModelName, subAlias, joinConditions and
subWhere (or subWhereFilter) into that helper instead of calling
this.eb.selectFrom directly; ensure the final condition still uses
discriminatorCheck and the helper-produced exists expression (instead of
this.eb.exists(existsSubquery)) so dialect-specific EXISTS workarounds and
proper alias joins are preserved.
In `@packages/orm/src/client/zod/factory.ts`:
- Around line 490-503: The $is filter currently uses z.object(...) which strips
unknown sub-model keys and hides typos; update the construction of
subModelSchema to use z.strictObject(...) instead (i.e. replace z.object(...)
with z.strictObject(...)) so unknown sub-model names will be rejected at
validation time; keep the same Object.fromEntries mapping of subModel names and
the existing union that references this.makeWhereSchema, and preserve the
.optional() on subModelSchema before assigning it to fields['$is'].
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7ffa087-26a0-464d-9e91-d1bac09d8a0b

📥 Commits

Reviewing files that changed from the base of the PR and between 39a0a28 and d62333d.

📒 Files selected for processing (4)
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/zod/factory.ts
  • tests/e2e/orm/client-api/delegate.test.ts

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts Outdated
Comment threadpackages/orm/src/client/zod/factory.ts Outdated
@motopodsmotopods changed the title implement $is sub-model filter for delegate base modelsfit(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@motopodsmotopods changed the title fit(orm): implement $is sub-model filter for delegate base modelsfeat(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@ymc9

ymc9 commented Apr 12, 2026

Copy link
Copy Markdown
Member

Hi @motopods , it's a very useful feature and I'm glad you're tackling it!

I'm not sure if we need to introduce the $is key. Do you think we can instead rely on the discriminator field for smart typing? Like:

awaitclient.asset.findMany({where: {type: 'Video',duration: {gt: 100}},});

The filter's typing is then a discriminated union with each component fixing the delegate type and allowing its concrete fields.

What do you think?

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 That makes a lot of sense. I’ll try this approach and see if it works.

@motopods
motopods marked this pull request as draft April 12, 2026 02:43
Sign up for freeto 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.

[Feature Request] Polymorphic Models: Query base model with concrete model's props

3 participants

@motopods@sanny-io@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(orm): implement $is sub-model filter for delegate base models - #2559

Closed
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev
Closed

feat(orm): implement $is sub-model filter for delegate base models#2559
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev

Conversation

@motopods

@motopodsmotopods commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Adds support for the $is filter operator on delegate (polymorphic) base model where clauses, enabling queries that filter by sub-model-specific fields.

What's new

When querying from a delegate base model, use $is to filter by sub-model type and/or sub-model-specific fields. Multiple sub-model entries are combined with OR semantics.

// Return only Video assets (includes RatedVideo since it extends Video)awaitclient.asset.findMany({where: {$is: {video: {}}},});// true value is equivalent to an empty filter — same as aboveawaitclient.asset.findMany({where: {$is: {video: true}},});// Filter on a sub-model-specific fieldawaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}}}},});// Combine a base field filter with a sub-model filter (AND)awaitclient.asset.findMany({where: {viewCount: {gt: 0},$is: {video: {duration: {gt: 100}}},},});// Multiple sub-models — OR semantics// Returns: (Videos with duration > 100) OR (Images with format 'png')awaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}},image: {format: 'png'},},},});// Nested $is — for multi-level delegate hierarchies// Asset.$is.video.$is.ratedVideoawaitclient.asset.findMany({where: {$is: {video: {$is: {ratedVideo: {rating: 5}}},},},});// $is on an intermediate delegateawaitclient.video.findMany({where: {$is: {ratedVideo: {rating: 5}}},});
  • Correlated EXISTS subqueries — Filters are translated into EXISTS (SELECT 1 FROM <submodel> WHERE id = <base>.id AND <discriminator> AND <sub-fields>) SQL, ensuring correct row-level scoping.
  • Type-safe WhereInput$SubModelWhereInput types are generated in crud-types.ts so TypeScript enforces valid sub-model fields at compile time.
  • Runtime Zod validationzod/factory.ts validates $is payloads at runtime, giving clear error messages for unknown sub-models or invalid field shapes.

Files changed

FileChange
packages/orm/src/client/crud-types.tsAdded $SubModelWhereInput and wired $is into WhereInput
packages/orm/src/client/crud/dialects/base-dialect.tsAdded buildIsFilter to emit correlated EXISTS subqueries
packages/orm/src/client/zod/factory.tsAdded runtime Zod validation for $is payloads
tests/e2e/orm/client-api/delegate.test.tsEnd-to-end tests covering basic and nested $is filtering

Close#1740

Summary by CodeRabbit

  • New Features

    • Added a top-level $is filter for delegate (polymorphic) base models to filter by sub-model type, including nested $is conditions, OR-combined multi-sub-model queries, and correlated-subquery semantics for inherited/base fields. Input validation now recognizes $is in where/filter schemas.
  • Tests

    • Added e2e coverage validating $is behavior across base models, nested $is paths, nested predicates, and multi-sub-model queries.

@coderabbitai

coderabbitaiBot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a $is polymorphic sub-model filter: type definitions, query-predicate construction, Zod where-schema generation, and end-to-end tests enabling base-model queries narrowed by concrete sub-model conditions (including OR semantics and nested $is).

Changes

Cohort / File(s)Summary
Type System
packages/orm/src/client/crud-types.ts
Added SubModelWhereInput and extended WhereInput for delegate models to optionally include $is?: SubModelWhereInput<...>, where each sub-model key maps to true or a nested WhereInput.
Query Builder / Dialect
packages/orm/src/client/crud/dialects/base-dialect.ts
Added buildIsFilter handling in buildFilter: validates discriminator, maps camelCase keys to discriminator values, enforces discriminator equality, and, for nested filters, adds correlated EXISTS subqueries joined on id fields. Multiple sub-model entries are OR-combined.
Schema Validation (Zod)
packages/orm/src/client/zod/factory.ts
makeWhereSchema now conditionally inserts an optional top-level $is strict object for delegate models; keys are lowercased sub-model names and values are true or a lazily-evaluated nested where schema.
Tests
tests/e2e/orm/client-api/delegate.test.ts
Added e2e tests for $is on base models: true matches, nested sub-model predicates, combinations with base-model filters, OR across sub-models, and nested $is paths; additional seeded data included.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I'm a rabbit in the model wood,
I sniff the $is where sub-models stood,
I hop through predicates, small and grand,
Combining checks with a correlated strand,
Polymorphic hops — query fields in hand! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check✅ PassedThe PR fully addresses the primary objectives of issue #1740: enabling queries on delegate base models using sub-model-specific properties with type safety and developer-friendly API.
Out of Scope Changes check✅ PassedAll changes are directly related to implementing the $is sub-model filter feature; no unrelated modifications are present in the PR.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title 'feat(orm): implement $is sub-model filter for delegate base models' is directly related to the main change in the changeset, which introduces a new $is filter operator for delegate (polymorphic) base models across type definitions, dialect logic, validation, and tests.

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

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

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.

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 Hi, what about you think of this?

@sanny-io

Copy link
Copy Markdown
Contributor

This is very interesting. It may be better to use the lowercase-first name of the model (Video -> video).

Also, null matching the first query seems odd to me. Might it be better to use booleans, instead?

awaitclient.asset.findMany({where: {$is: {video: true}},});

@motopods

Copy link
Copy Markdown
ContributorAuthor

@sanny-io You make a lot of sense.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 367-382: The EXISTS subquery is built manually (using tmpAlias,
selectFrom and this.eb.exists) which bypasses delegate-aware helpers and causes
inherited-delegate filters from this.buildFilter to reference outer tables
incorrectly; replace the manual existsSubquery construction with the
dialect-aware helper (use the existing buildExistsExpression /
buildDelegateQueryHelpers pathway) so the subquery is created via the same
delegate-aware query builder, passing subModelName, subAlias, joinConditions and
subWhere (or subWhereFilter) into that helper instead of calling
this.eb.selectFrom directly; ensure the final condition still uses
discriminatorCheck and the helper-produced exists expression (instead of
this.eb.exists(existsSubquery)) so dialect-specific EXISTS workarounds and
proper alias joins are preserved.
In `@packages/orm/src/client/zod/factory.ts`:
- Around line 490-503: The $is filter currently uses z.object(...) which strips
unknown sub-model keys and hides typos; update the construction of
subModelSchema to use z.strictObject(...) instead (i.e. replace z.object(...)
with z.strictObject(...)) so unknown sub-model names will be rejected at
validation time; keep the same Object.fromEntries mapping of subModel names and
the existing union that references this.makeWhereSchema, and preserve the
.optional() on subModelSchema before assigning it to fields['$is'].
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7ffa087-26a0-464d-9e91-d1bac09d8a0b

📥 Commits

Reviewing files that changed from the base of the PR and between 39a0a28 and d62333d.

📒 Files selected for processing (4)
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/zod/factory.ts
  • tests/e2e/orm/client-api/delegate.test.ts

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts Outdated
Comment threadpackages/orm/src/client/zod/factory.ts Outdated
@motopodsmotopods changed the title implement $is sub-model filter for delegate base modelsfit(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@motopodsmotopods changed the title fit(orm): implement $is sub-model filter for delegate base modelsfeat(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@ymc9

ymc9 commented Apr 12, 2026

Copy link
Copy Markdown
Member

Hi @motopods , it's a very useful feature and I'm glad you're tackling it!

I'm not sure if we need to introduce the $is key. Do you think we can instead rely on the discriminator field for smart typing? Like:

awaitclient.asset.findMany({where: {type: 'Video',duration: {gt: 100}},});

The filter's typing is then a discriminated union with each component fixing the delegate type and allowing its concrete fields.

What do you think?

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 That makes a lot of sense. I’ll try this approach and see if it works.

@motopods
motopods marked this pull request as draft April 12, 2026 02:43
Sign up for freeto 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.

[Feature Request] Polymorphic Models: Query base model with concrete model's props

3 participants

@motopods@sanny-io@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(orm): implement $is sub-model filter for delegate base models - #2559

Closed
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev
Closed

feat(orm): implement $is sub-model filter for delegate base models#2559
motopods wants to merge 0 commit into
zenstackhq:devfrom
motopods:dev

Conversation

@motopods

@motopodsmotopods commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Adds support for the $is filter operator on delegate (polymorphic) base model where clauses, enabling queries that filter by sub-model-specific fields.

What's new

When querying from a delegate base model, use $is to filter by sub-model type and/or sub-model-specific fields. Multiple sub-model entries are combined with OR semantics.

// Return only Video assets (includes RatedVideo since it extends Video)awaitclient.asset.findMany({where: {$is: {video: {}}},});// true value is equivalent to an empty filter — same as aboveawaitclient.asset.findMany({where: {$is: {video: true}},});// Filter on a sub-model-specific fieldawaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}}}},});// Combine a base field filter with a sub-model filter (AND)awaitclient.asset.findMany({where: {viewCount: {gt: 0},$is: {video: {duration: {gt: 100}}},},});// Multiple sub-models — OR semantics// Returns: (Videos with duration > 100) OR (Images with format 'png')awaitclient.asset.findMany({where: {$is: {video: {duration: {gt: 100}},image: {format: 'png'},},},});// Nested $is — for multi-level delegate hierarchies// Asset.$is.video.$is.ratedVideoawaitclient.asset.findMany({where: {$is: {video: {$is: {ratedVideo: {rating: 5}}},},},});// $is on an intermediate delegateawaitclient.video.findMany({where: {$is: {ratedVideo: {rating: 5}}},});
  • Correlated EXISTS subqueries — Filters are translated into EXISTS (SELECT 1 FROM <submodel> WHERE id = <base>.id AND <discriminator> AND <sub-fields>) SQL, ensuring correct row-level scoping.
  • Type-safe WhereInput$SubModelWhereInput types are generated in crud-types.ts so TypeScript enforces valid sub-model fields at compile time.
  • Runtime Zod validationzod/factory.ts validates $is payloads at runtime, giving clear error messages for unknown sub-models or invalid field shapes.

Files changed

FileChange
packages/orm/src/client/crud-types.tsAdded $SubModelWhereInput and wired $is into WhereInput
packages/orm/src/client/crud/dialects/base-dialect.tsAdded buildIsFilter to emit correlated EXISTS subqueries
packages/orm/src/client/zod/factory.tsAdded runtime Zod validation for $is payloads
tests/e2e/orm/client-api/delegate.test.tsEnd-to-end tests covering basic and nested $is filtering

Close#1740

Summary by CodeRabbit

  • New Features

    • Added a top-level $is filter for delegate (polymorphic) base models to filter by sub-model type, including nested $is conditions, OR-combined multi-sub-model queries, and correlated-subquery semantics for inherited/base fields. Input validation now recognizes $is in where/filter schemas.
  • Tests

    • Added e2e coverage validating $is behavior across base models, nested $is paths, nested predicates, and multi-sub-model queries.

@coderabbitai

coderabbitaiBot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a $is polymorphic sub-model filter: type definitions, query-predicate construction, Zod where-schema generation, and end-to-end tests enabling base-model queries narrowed by concrete sub-model conditions (including OR semantics and nested $is).

Changes

Cohort / File(s)Summary
Type System
packages/orm/src/client/crud-types.ts
Added SubModelWhereInput and extended WhereInput for delegate models to optionally include $is?: SubModelWhereInput<...>, where each sub-model key maps to true or a nested WhereInput.
Query Builder / Dialect
packages/orm/src/client/crud/dialects/base-dialect.ts
Added buildIsFilter handling in buildFilter: validates discriminator, maps camelCase keys to discriminator values, enforces discriminator equality, and, for nested filters, adds correlated EXISTS subqueries joined on id fields. Multiple sub-model entries are OR-combined.
Schema Validation (Zod)
packages/orm/src/client/zod/factory.ts
makeWhereSchema now conditionally inserts an optional top-level $is strict object for delegate models; keys are lowercased sub-model names and values are true or a lazily-evaluated nested where schema.
Tests
tests/e2e/orm/client-api/delegate.test.ts
Added e2e tests for $is on base models: true matches, nested sub-model predicates, combinations with base-model filters, OR across sub-models, and nested $is paths; additional seeded data included.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I'm a rabbit in the model wood,
I sniff the $is where sub-models stood,
I hop through predicates, small and grand,
Combining checks with a correlated strand,
Polymorphic hops — query fields in hand! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check✅ PassedThe PR fully addresses the primary objectives of issue #1740: enabling queries on delegate base models using sub-model-specific properties with type safety and developer-friendly API.
Out of Scope Changes check✅ PassedAll changes are directly related to implementing the $is sub-model filter feature; no unrelated modifications are present in the PR.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title 'feat(orm): implement $is sub-model filter for delegate base models' is directly related to the main change in the changeset, which introduces a new $is filter operator for delegate (polymorphic) base models across type definitions, dialect logic, validation, and tests.

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

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

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.

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 Hi, what about you think of this?

@sanny-io

Copy link
Copy Markdown
Contributor

This is very interesting. It may be better to use the lowercase-first name of the model (Video -> video).

Also, null matching the first query seems odd to me. Might it be better to use booleans, instead?

awaitclient.asset.findMany({where: {$is: {video: true}},});

@motopods

Copy link
Copy Markdown
ContributorAuthor

@sanny-io You make a lot of sense.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 367-382: The EXISTS subquery is built manually (using tmpAlias,
selectFrom and this.eb.exists) which bypasses delegate-aware helpers and causes
inherited-delegate filters from this.buildFilter to reference outer tables
incorrectly; replace the manual existsSubquery construction with the
dialect-aware helper (use the existing buildExistsExpression /
buildDelegateQueryHelpers pathway) so the subquery is created via the same
delegate-aware query builder, passing subModelName, subAlias, joinConditions and
subWhere (or subWhereFilter) into that helper instead of calling
this.eb.selectFrom directly; ensure the final condition still uses
discriminatorCheck and the helper-produced exists expression (instead of
this.eb.exists(existsSubquery)) so dialect-specific EXISTS workarounds and
proper alias joins are preserved.
In `@packages/orm/src/client/zod/factory.ts`:
- Around line 490-503: The $is filter currently uses z.object(...) which strips
unknown sub-model keys and hides typos; update the construction of
subModelSchema to use z.strictObject(...) instead (i.e. replace z.object(...)
with z.strictObject(...)) so unknown sub-model names will be rejected at
validation time; keep the same Object.fromEntries mapping of subModel names and
the existing union that references this.makeWhereSchema, and preserve the
.optional() on subModelSchema before assigning it to fields['$is'].
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7ffa087-26a0-464d-9e91-d1bac09d8a0b

📥 Commits

Reviewing files that changed from the base of the PR and between 39a0a28 and d62333d.

📒 Files selected for processing (4)
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/zod/factory.ts
  • tests/e2e/orm/client-api/delegate.test.ts

Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts Outdated
Comment threadpackages/orm/src/client/zod/factory.ts Outdated
@motopodsmotopods changed the title implement $is sub-model filter for delegate base modelsfit(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@motopodsmotopods changed the title fit(orm): implement $is sub-model filter for delegate base modelsfeat(orm): implement $is sub-model filter for delegate base modelsApr 9, 2026
@ymc9

ymc9 commented Apr 12, 2026

Copy link
Copy Markdown
Member

Hi @motopods , it's a very useful feature and I'm glad you're tackling it!

I'm not sure if we need to introduce the $is key. Do you think we can instead rely on the discriminator field for smart typing? Like:

awaitclient.asset.findMany({where: {type: 'Video',duration: {gt: 100}},});

The filter's typing is then a discriminated union with each component fixing the delegate type and allowing its concrete fields.

What do you think?

@motopods

Copy link
Copy Markdown
ContributorAuthor

@ymc9 That makes a lot of sense. I’ll try this approach and see if it works.

@motopods
motopods marked this pull request as draft April 12, 2026 02:43
Sign up for freeto 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.

[Feature Request] Polymorphic Models: Query base model with concrete model's props

3 participants

@motopods@sanny-io@ymc9