Skip to content

Conversation

@ymc9
Copy link
Member

@ymc9 ymc9 commented Oct 21, 2025

fixes #284

Summary by CodeRabbit

  • Improvements

    • Simplified and more reliable computed-field resolution across delegate bases and sub-models, improving query correctness for computed and non-computed fields.
  • Tests

    • Added end-to-end coverage for computed fields with delegate relationships and updated tests to use isolated clients per test.
  • Documentation

    • Minor plugin policy comment updated.

Copilot AI review requested due to automatic review settings October 21, 2025 15:34
@coderabbitai
Copy link

coderabbitai bot commented Oct 21, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This PR replaces the exported buildFieldRef utility with a dialect-local fieldRef helper and updates buildSelectField to resolve computed fields using delegate/origin model resolution; it also removes the utility, updates tests to use local clients, and edits a sample comment.

Changes

Cohort / File(s) Summary
Core dialect refactor
packages/runtime/src/client/crud/dialects/base-dialect.ts
Rewrote buildSelectField to compute fieldModel/alias from originModel ?? model, introduced a public fieldRef helper that replaces buildFieldRef, and added explicit computed-field handling (inline vs handler lookup with clear QueryError when missing).
Utility removal
packages/runtime/src/client/query-utils.ts
Removed exported buildFieldRef and deleted related imports (ExpressionWrapper, ClientOptions), eliminating the previous shared field-ref implementation.
Tests
tests/e2e/orm/client-api/computed-fields.test.ts
Replaced shared db/afterEach cleanup with per-test const db = await createTestClient(...), added a test for delegate-base computed fields, and extended read-from-relation assertions for computed fields.
Sample comment
samples/blog/zenstack/schema.zmodel
Updated a documentation comment in the plugin policy block only; no behavioral changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰
I nibble at fields both near and far,
Found computed secrets on the base, not just the star.
With a new little helper I hop and I peek,
Each delegate model now shows the tweak.
Hooray — inheritance no longer hides the tweak! 🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "fix: make computed fields on base models work" clearly and concisely summarizes the primary change. It directly addresses the issue where computed fields defined on base models with @computed() annotation were not being recognized for derived models. The title is specific enough to indicate that this PR fixes the inherited computed fields functionality, and it aligns well with the main changes implemented across the codebase, particularly the refactoring in base-dialect.ts to handle delegate bases and the new test case validating this scenario.
Linked Issues Check ✅ Passed The pull request addresses the requirements specified in linked issue #284, which mandates that @computed() annotated properties on base models should be recognized and included for derived models. The code changes implement this fix through refactored logic in base-dialect.ts that properly resolves computed fields from delegate bases using originModel, rewritten fieldRef handling for computed fields that explicitly resolves them from options.computedFields, and a new end-to-end test case "allows sub models to use computed fields from delegate base" that directly validates the expected behavior. The changes appear to fulfill the requirement that inherited computed fields should appear in computedFields for derived models.
Out of Scope Changes Check ✅ Passed All code changes in this pull request are directly related to fixing the computed fields on base models issue. The refactoring in base-dialect.ts handles delegate bases and computed field resolution, the removal of buildFieldRef from query-utils.ts is part of consolidating the field reference logic, the documentation update in schema.zmodel is minimal, and the test changes add validation for the fix and refactor test structure for better isolation. No unrelated functionality, unrelated refactoring, or scope creep is evident in the changeset.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/issue-284

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2ce74c and d9b1abf.

📒 Files selected for processing (1)
  • packages/runtime/src/client/crud/dialects/base-dialect.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
{packages,samples,tests}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place packages only under packages/, samples/, or tests/

Files:

  • packages/runtime/src/client/crud/dialects/base-dialect.ts
🧬 Code graph analysis (1)
packages/runtime/src/client/crud/dialects/base-dialect.ts (3)
packages/runtime/src/client/query-utils.ts (1)
  • requireField (45-63)
packages/runtime/src/client/crud/operations/base.ts (1)
  • requireField (121-123)
packages/runtime/src/client/errors.ts (1)
  • QueryError (18-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-test (20.x, sqlite)
  • GitHub Check: build-test (20.x, postgresql)
🔇 Additional comments (2)
packages/runtime/src/client/crud/dialects/base-dialect.ts (2)

933-939: LGTM! Correct resolution of inherited fields.

The changes properly resolve computed fields from base models by deriving fieldModel and alias from fieldDef.originModel when present. This ensures that inherited computed fields reference the base model where they are defined, which aligns with the delegate join strategy (lines 58-64) where base models are joined using their model names as aliases.


1070-1091: LGTM! Elegant resolution of inherited computed fields.

The implementation correctly resolves computed field handlers for inherited fields using a double-resolution strategy:

  • When called from buildSelectField with a pre-resolved originModel, fieldDef.originModel is undefined and the method falls back to the model parameter (line 1084)
  • When called directly with a derived model, fieldDef.originModel points to the base model where the computed field is defined

This ensures computedFields[baseModel][field] is looked up correctly regardless of the call pattern, fixing issue #284. Error handling at line 1087 provides clear diagnostics when a computed field implementation is missing.


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.

@claude
Copy link

claude bot commented Oct 21, 2025

Claude finished @ymc9's task —— View job


Code Review Progress

Reviewing PR #321: Fix computed fields on base models

Review Tasks:

  • Analyze changed files and understand the fix
  • Review code quality and best practices
  • Check for potential bugs or issues
  • Evaluate performance considerations
  • Assess security concerns
  • Review test coverage

Review Summary

This PR successfully fixes computed fields functionality for delegate base models. The solution is clean, well-tested, and addresses the core issue effectively.

📋 Code Quality & Best Practices

✅ Excellent:

  • Clean refactoring of fieldRef() method in base-dialect.ts
  • Proper code deduplication by removing the buildFieldRef() function from query-utils.ts
  • Consistent error handling and validation
  • Good separation of concerns

✅ Follows TypeScript best practices:

  • Type safety maintained throughout
  • Proper use of optional parameters and default values
  • Consistent naming conventions

🐛 Bug Analysis

✅ No bugs identified:

  • The fix correctly uses fieldDef.originModel ?? model when looking up computed field implementations
  • Proper fallback logic for when originModel is undefined
  • Error handling is preserved for missing computed field implementations

The core fix (line 1086 in base-dialect.ts):

computer = computedFields?.[fieldDef.originModel ?? model]?.[field];

This correctly looks up computed fields in the base model when dealing with inherited fields.

⚡ Performance Considerations

✅ Performance neutral or positive:

  • Code simplification by removing buildFieldRef() utility reduces function call overhead
  • Inlined logic in fieldRef() method is more direct
  • No unnecessary object creation or extra database queries

🔒 Security Assessment

✅ No security concerns:

  • No user input validation changes
  • No new attack vectors introduced
  • Maintains existing validation patterns

🧪 Test Coverage

✅ Comprehensive test coverage:

  • New test specifically validates the delegate base model scenario
  • Tests both true/false cases for the computed field logic
  • Existing tests continue to pass, ensuring no regressions
  • Test uses realistic schema with proper delegate inheritance pattern

Test highlights:

  • Tests Content base model with isNews computed field
  • Tests Post model extending Content and using the inherited computed field
  • Validates the computed field logic works correctly (title like '%news%')

📚 Code Structure Improvements

✅ Better code organization:

  • Eliminated duplicate logic between buildFieldRef() and the dialect's fieldRef() method
  • Simplified the inheritance handling in buildSelectField()
  • More maintainable code structure

Minor Observations

  1. Documentation comment update in schema.zmodel: Simple clarification comment - good practice
  2. Test cleanup: Removed unnecessary afterEach and global variable - good cleanup
  3. Consistent pattern: The fix follows the same pattern used elsewhere for handling originModel vs model

Conclusion

✅ Recommended for approval

This is a well-executed fix that:

  • Solves the specific issue with computed fields on base models
  • Improves code maintainability by removing duplication
  • Includes comprehensive tests
  • Maintains backward compatibility
  • Follows existing patterns and conventions

The changes are minimal, focused, and address the root cause effectively. No security, performance, or code quality concerns identified.


Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes an issue where computed fields defined on base models were not working correctly in delegate model scenarios. The fix ensures that when a computed field is inherited from a delegate base model, the system correctly looks up the field implementation using the origin model name rather than the inheriting model name.

Key changes:

  • Refactored buildSelectField to handle computed fields from delegate bases
  • Inlined buildFieldRef logic into fieldRef method with support for originModel lookup
  • Updated tests to verify computed fields work with delegate inheritance

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/runtime/src/client/query-utils.ts Removed the buildFieldRef utility function (logic moved elsewhere)
packages/runtime/src/client/crud/dialects/base-dialect.ts Modified buildSelectField to handle computed fields from delegate bases and inlined buildFieldRef logic into fieldRef with origin model support
tests/e2e/orm/client-api/computed-fields.test.ts Added test case for computed fields on delegate base models and cleaned up test structure
samples/blog/zenstack/schema.zmodel Enhanced comment to clarify pnpm-specific workaround

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@claude
Copy link

claude bot commented Oct 21, 2025

Claude finished @ymc9's task —— View job


Code Review Progress

Reviewing PR #321: Fix computed fields on base models

Review Tasks:

  • Analyze changed files and understand the fix
  • Review code quality and best practices
  • Check for potential bugs or issues
  • Evaluate performance considerations
  • Assess security concerns
  • Review test coverage

Review Summary

This PR successfully fixes computed fields functionality for delegate base models. The solution is clean, well-tested, and addresses the core issue effectively.

📋 Code Quality & Best Practices

✅ Excellent:

  • Clean refactoring of fieldRef() method in base-dialect.ts
  • Proper code deduplication by removing the buildFieldRef() function from query-utils.ts
  • Consistent error handling and validation
  • Good separation of concerns

✅ Follows TypeScript best practices:

  • Type safety maintained throughout
  • Proper use of optional parameters and default values
  • Consistent naming conventions

🐛 Bug Analysis

✅ No bugs identified:

  • The fix correctly uses fieldDef.originModel ?? model when looking up computed field implementations
  • Proper fallback logic for when originModel is undefined
  • Error handling is preserved for missing computed field implementations

The core fix (line 1084 in base-dialect.ts):

computer = computedFields?.[fieldDef.originModel ?? model]?.[field];

This correctly looks up computed fields in the base model when dealing with inherited fields.

⚡ Performance Considerations

✅ Performance neutral or positive:

  • Code simplification by removing buildFieldRef() utility reduces function call overhead
  • Inlined logic in fieldRef() method is more direct
  • No unnecessary object creation or extra database queries

🔒 Security Assessment

✅ No security concerns:

  • No user input validation changes
  • No new attack vectors introduced
  • Maintains existing validation patterns

🧪 Test Coverage

✅ Comprehensive test coverage:

  • New test specifically validates the delegate base model scenario
  • Tests both true/false cases for the computed field logic
  • Existing tests continue to pass, ensuring no regressions
  • Test uses realistic schema with proper delegate inheritance pattern

Test highlights:

  • Tests Content base model with isNews computed field
  • Tests Post model extending Content and using the inherited computed field
  • Validates the computed field logic works correctly (title like '%news%')

📚 Code Structure Improvements

✅ Better code organization:

  • Eliminated duplicate logic between buildFieldRef() and the dialect's fieldRef() method
  • Simplified the inheritance handling in buildSelectField()
  • More maintainable code structure

Response to Copilot Review Comment

Regarding the concern about logic flow inversion: After reviewing the code, the new implementation is actually more correct. The key insight is that fieldDef.originModel ?? model ensures we always look up computed fields using the correct model name:

  • For inherited fields: Uses originModel (the base model where the field is defined)
  • For regular fields: Uses model (the current model)

This pattern is consistent with how buildSelectField() handles field references and ensures computed fields work correctly across delegate inheritance hierarchies.

Minor Observations

  1. Documentation comment update in schema.zmodel: Simple clarification comment - good practice
  2. Test cleanup: Removed unnecessary afterEach and global variable - good cleanup
  3. Consistent pattern: The fix follows the same pattern used elsewhere for handling originModel vs model

Conclusion

✅ Recommended for approval

This is a well-executed fix that:

  • Solves the specific issue with computed fields on base models
  • Improves code maintainability by removing duplication
  • Includes comprehensive tests
  • Maintains backward compatibility
  • Follows existing patterns and conventions

The changes are minimal, focused, and address the root cause effectively. No security, performance, or code quality concerns identified.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/runtime/src/client/crud/dialects/base-dialect.ts (2)

751-756: OrderBy helper picks origin alias but relation bases aren’t joined

When ordering by a field from a related model that originates from its delegate base, the alias for the base (e.g., Content) hasn’t been joined in the relation leftJoin branch below. This can produce invalid SQL (unknown table/alias) for inherited/computed fields on relations.

Add delegate-base joins for the relation model before recursing:

@@
                 } else {
                     // order by relation
                     const relationModel = fieldDef.type;
 
                     if (fieldDef.array) {
@@
                     } else {
                         // order by to-one relation
                         result = result.leftJoin(relationModel, (join) => {
                             const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, relationModel);
                             return join.on((eb) =>
                                 this.and(
                                     ...joinPairs.map(([left, right]) => eb(this.eb.ref(left), '=', this.eb.ref(right))),
                                 ),
                             );
                         });
+                        // ensure delegate bases of relation are joined for inherited/computed fields
+                        {
+                            let base = requireModel(this.schema, relationModel).baseModel;
+                            while (base) {
+                                result = this.buildDelegateJoin(relationModel, relationModel, base, result);
+                                base = requireModel(this.schema, base).baseModel;
+                            }
+                        }
                         result = this.buildOrderBy(result, fieldDef.type, relationModel, value, negated);
                     }
                 }

103-107: Add validation guard to prevent computed fields in cursor pagination

The buildCursorFilter method references raw columns (${modelAlias}.${field} at line 185), which fails for computed fields since they don't exist as database columns. Add an early validation guard:

        if (args.cursor) {
+            // guard: cursor pagination cannot use computed fields in orderBy
+            const ob = args.orderBy ?? makeDefaultOrderBy(this.schema, model);
+            for (const [f] of ensureArray(ob).flatMap((o) => Object.entries<SortOrder>(o as any))) {
+                const fd = requireField(this.schema, model, f);
+                if (fd.computed) {
+                    throw new QueryError('Cursor pagination by computed fields is not supported');
+                }
+            }
             result = this.buildCursorFilter(model, result, args.cursor, args.orderBy, negateOrderBy, modelAlias);
         }

All required utilities (makeDefaultOrderBy, ensureArray, requireField, QueryError) are already imported in this file.

🧹 Nitpick comments (2)
packages/runtime/src/client/crud/dialects/base-dialect.ts (1)

1073-1092: fieldRef computed handler: solid, with two small improvements

  • Type the handler for stronger guarantees and DX.
  • Enhance the error to mention the resolved origin model.
-            let computer: Function | undefined;
+            type ComputedHandler = (eb: ExpressionBuilder<any, any>, ctx: { modelAlias?: string }) => Expression<any>;
+            let computer: ComputedHandler | undefined;
@@
-            if (!computer) {
-                throw new QueryError(`Computed field "${field}" implementation not provided for model "${model}"`);
-            }
+            if (!computer) {
+                const resolvedModel = fieldDef.originModel ?? model;
+                throw new QueryError(
+                    `Computed field "${field}" implementation not provided for model "${resolvedModel}"`,
+                );
+            }
tests/e2e/orm/client-api/computed-fields.test.ts (1)

238-274: Add test assertions for filtering and orderBy on inherited computed fields from delegate base

The review comment's suggestions are valid and identify real coverage gaps. Current tests confirm:

  • orderBy on computed fields exists but only for base models (lines 56, 64 show upperName on User), not for inherited computed fields in delegate scenarios
  • No filtering (where clauses) on computed fields is tested in the computed-fields.test.ts suite
  • The new test (lines 238–274) validates the computed field's presence and correctness but lacks assertions for query operations (filtering, orderBy) on the inherited field

The suggested additions would harden coverage by testing:

  1. Filtering the sub model by inherited computed field (e.g., post.findMany({ where: { isNews: true } }))
  2. OrderBy on inherited computed field from delegate base
  3. OrderBy involving joins with related models' inherited computed fields

These scenarios exercise distinct code paths and are not currently covered.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14eaf36 and b2ce74c.

📒 Files selected for processing (4)
  • packages/runtime/src/client/crud/dialects/base-dialect.ts (2 hunks)
  • packages/runtime/src/client/query-utils.ts (0 hunks)
  • samples/blog/zenstack/schema.zmodel (1 hunks)
  • tests/e2e/orm/client-api/computed-fields.test.ts (6 hunks)
💤 Files with no reviewable changes (1)
  • packages/runtime/src/client/query-utils.ts
🧰 Additional context used
📓 Path-based instructions (3)
{packages,samples,tests}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place packages only under packages/, samples/, or tests/

Files:

  • packages/runtime/src/client/crud/dialects/base-dialect.ts
  • samples/blog/zenstack/schema.zmodel
  • tests/e2e/orm/client-api/computed-fields.test.ts
**/schema.zmodel

📄 CodeRabbit inference engine (CLAUDE.md)

Name ZModel schema files schema.zmodel

Files:

  • samples/blog/zenstack/schema.zmodel
tests/e2e/**

📄 CodeRabbit inference engine (CLAUDE.md)

End-to-end tests must live under tests/e2e/

Files:

  • tests/e2e/orm/client-api/computed-fields.test.ts
🧬 Code graph analysis (2)
packages/runtime/src/client/crud/dialects/base-dialect.ts (2)
packages/runtime/src/client/query-utils.ts (1)
  • requireField (45-63)
packages/runtime/src/client/errors.ts (1)
  • QueryError (18-22)
tests/e2e/orm/client-api/computed-fields.test.ts (1)
packages/testtools/src/client.ts (1)
  • createTestClient (52-169)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-test (20.x, postgresql)
  • GitHub Check: build-test (20.x, sqlite)
  • GitHub Check: claude-review
🔇 Additional comments (10)
samples/blog/zenstack/schema.zmodel (1)

13-16: Comment-only tweak is fine

The note clarifies local sample constraints without affecting behavior. LGTM.

tests/e2e/orm/client-api/computed-fields.test.ts (6)

2-2: Vitest import: OK

Using vitest’s describe/it/expect directly is correct.


6-21: Non-optional computed field flow: OK

Good end-to-end coverage for create/find/orderBy/aggregate/groupBy with a computed scalar.


94-130: Type-level check (non-optional): OK

The ts-expect-error guards assignment; compile-time intent is clear.


134-149: Optional computed field flow: OK

Happy path for null-returning computed field is covered.


161-197: Type-level check (optional): OK

Covers nullable assignment; consistent with runtime config.


200-236: Read-from-relation returns computed field: OK

Asserts computed on the related author; aligns with new resolution.

packages/runtime/src/client/crud/dialects/base-dialect.ts (3)

141-147: Correct: filters now use fieldRef honoring originModel

This fixes filtering by inherited/computed fields across delegate bases. LGTM.


934-941: Select field recursion: OK

Selecting via originModel for inherited fields is correct and avoids duplicating logic.


93-101: The proposed fix using fieldRef won't work for DISTINCT ON operations.

The review correctly identifies that computed fields will break (they're not database columns), but the suggested solution is architecturally incorrect:

  1. fieldRef incompatibility: fieldRef() inlines computed fields using callback functions (line 1085 shows computedFields?.[...][field] invocation). DISTINCT ON requires actual column references, not expressions. Inlining won't produce valid SQL.

  2. Inherited field logic flaw: The suggested code calls fieldRef(fd.originModel, f, fd.originModel) which attempts to resolve the field in the wrong model context. This doesn't match the current query's model and table alias.

Correct approach: Reject computed fields explicitly with a clear error message, allowing only regular/database columns for DISTINCT ON. The validator (makeDistinctSchema) already allows computed fields to pass through—a runtime check rejecting them is needed.

if ('distinct' in args && (args as any).distinct) {
    const distinct = ensureArray((args as any).distinct) as string[];
    for (const f of distinct) {
        const fieldDef = requireField(this.schema, model, f);
        if (fieldDef.computed) {
            throw new QueryError(`Computed field "${f}" cannot be used in distinct`);
        }
    }
    if (this.supportsDistinctOn) {
        result = result.distinctOn(distinct.map((f) => this.eb.ref(`${modelAlias}.${f}`)));
    } else {
        throw new QueryError(`"distinct" is not supported by "${this.schema.provider.type}" provider`);
    }
}

Likely an incorrect or invalid review comment.

@ymc9 ymc9 merged commit 23adf91 into dev Oct 21, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants