Skip to content

fix(orm): fallback to compact temp aliases for overlong names#2425

Merged
ymc9 merged 7 commits intozenstackhq:devfrom
pkudinov:fix/issue-2424-temp-alias-safety-fallback
Mar 6, 2026
Merged

fix(orm): fallback to compact temp aliases for overlong names#2425
ymc9 merged 7 commits intozenstackhq:devfrom
pkudinov:fix/issue-2424-temp-alias-safety-fallback

Conversation

@pkudinov
Copy link
Contributor

@pkudinov pkudinov commented Mar 1, 2026

Fixes #2424

Summary

This PR fixes deep nested include failures caused by overlong temporary relation aliases when useCompactAliasNames: false.

  • Keeps existing behavior for useCompactAliasNames: false when aliases are short/safe.
  • Adds a safety fallback: if any temp alias exceeds a safe identifier length threshold, ZenStack applies the existing compact alias transformer for that query.
  • Leaves the default path (useCompactAliasNames: true) unchanged.

Root Cause

Deep relation paths generate long temp aliases (via $$_...).
When aliases exceed identifier limits, they can be truncated/collide and break reference resolution in nested joins, resulting in errors like 42703 (column ... does not exist).

Summary by CodeRabbit

  • Bug Fixes

    • Improved alias handling to ensure consistent, stable temporary aliases in all modes, preventing incorrect truncation or mismatched nested include results.
  • Documentation

    • Clarified alias examples and fallback behavior when compact aliasing is disabled and identifier length limits apply.
  • Tests

    • Added a regression test validating deep nested include behavior with non-compact aliasing.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Always construct and run TempAliasTransformer with an explicit mode based on useCompactAliasNames; add modes and a length checker to only compact when needed; implement stable short alias mapping; update JSDoc example; add regression test validating deep nested include behavior with non-compact alias mode.

Changes

Cohort / File(s) Summary
Query executor
packages/orm/src/client/executor/zenstack-query-executor.ts
processTempAlias now always instantiates TempAliasTransformer with a mode (alwaysCompact or compactLongNames) and invokes it instead of skipping transformation when compact aliases are disabled.
Temp alias transformer internals
packages/orm/src/client/executor/temp-alias-transformer.ts
Introduce TempAliasTransformerMode and TempAliasTransformerOptions; add TempAliasLengthChecker; TempAliasTransformer gains mode and maxIdentifierLength, a constructor accepting options, early-return optimization when compactLongNames finds no overlong aliases, and stable mapping of TEMP_ALIAS_PREFIX identifiers to compact $$tN aliases.
Options JSDoc
packages/orm/src/client/options.ts
Update JSDoc for ClientOptions.useCompactAliasNames example alias format to $$t1/$$t2 and note fallback behavior when false and identifier length limits apply.
Regression test
tests/regression/test/issue-2424.test.ts
Add a regression test that seeds related models and asserts a deep nested include query under PolicyPlugin with non-compact alias names returns correct nested results (verifies fix for #2424).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I nibble names that stretch and creep,
I count to sixty‑three before they sleep.
When long ones loom, I map and mend,
$$t1, $$t2 — neat tails at end.
Hopping through queries, I make aliases keep.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(orm): fallback to compact temp aliases for overlong names' directly and clearly describes the main fix—implementing a fallback to compact aliases when temp aliases exceed safe length limits.
Linked Issues check ✅ Passed The PR meets all coding requirements from issue #2424: aliases now respect database identifier byte-length limits by detecting overlong temp aliases and falling back to compact aliases, preventing truncation collisions and broken SQL references.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing issue #2424: TempAliasTransformer modes, length detection, alias mapping logic, JSDoc updates, executor refactoring, and the regression test are all necessary to implement the fallback mechanism.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

@ymc9
Copy link
Member

ymc9 commented Mar 2, 2026

Hi @pkudinov , with the name compaction, the useCompactAliasNames is more a debugging facility now. Any specific reason you set it to true?

@pkudinov
Copy link
Contributor Author

pkudinov commented Mar 2, 2026

Hi @ymc9, excited to get to contribute to Zenstack. We caught this issue while migrating from V2 to V3.

I did not flip useCompactAliasNames to true globally in this PR - false is still used by default. Compaction only kicks in as a safety fallback when temp aliases become overlong.

For #2424 specifically, alias collision is reproducible due to Postgres identifier truncation (63 bytes). In the failing SQL, these distinct aliases are generated:

  - $$_CustomerOrderPaymentSummary$customerOrderPaymentSummaryLine$sub (66)
  - $$_CustomerOrderPaymentSummary$customerOrderPaymentSummaryLine$paymentTransaction$paymentTransactionLineItem$sub (112)
  - $$_CustomerOrderPaymentSummary$customerOrderPaymentSummaryLine$paymentTransaction$paymentTransactionLineItem$productCatalogItem$sub (131)

All truncate to the same 63-byte prefix:

  - $$_CustomerOrderPaymentSummary$customerOrderPaymentSummaryLine$

So distinct qualifiers collapse to one identifier, and references like ...paymentTransactionLineItem$sub.productSku are resolved in the wrong scope / become unresolved, leading to 42703 error.

Is there some better way to fix this?

@ymc9
Copy link
Member

ymc9 commented Mar 3, 2026

Hi @ymc9, excited to get to contribute to Zenstack. We caught this issue while migrating from V2 to V3.

I did not flip useCompactAliasNames to true globally in this PR - false is still used by default. Compaction only kicks in as a safety fallback when temp aliases become overlong.

For #2424 specifically, alias collision is reproducible due to Postgres identifier truncation (63 bytes). In the failing SQL, these distinct aliases are generated:

  - $$_CustomerOrderPaymentSummary$customerOrderPaymentSummaryLine$sub (66)
  - $$_CustomerOrderPaymentSummary$customerOrderPaymentSummaryLine$paymentTransaction$paymentTransactionLineItem$sub (112)
  - $$_CustomerOrderPaymentSummary$customerOrderPaymentSummaryLine$paymentTransaction$paymentTransactionLineItem$productCatalogItem$sub (131)

All truncate to the same 63-byte prefix:

  - $$_CustomerOrderPaymentSummary$customerOrderPaymentSummaryLine$

So distinct qualifiers collapse to one identifier, and references like ...paymentTransactionLineItem$sub.productSku are resolved in the wrong scope / become unresolved, leading to 42703 error.

Is there some better way to fix this?

Thanks for the explanation @pkudinov . I was just wondering if the temp alias compaction logic had any issues that made you set the flag to false 😄.

For this PR, I feel it's cleaner to move the name length check into the TempAliasTransformer class so we consolidate all alias handling logic there. We can probably add mode to the class like "alwaysCompact" vs "compactLongNames". What do you think?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/orm/src/client/executor/temp-alias-transformer.ts (1)

41-46: Validate maxIdentifierLength at construction.

A non-integer or non-positive value can silently make fallback behavior unreliable. Guarding once in the constructor will harden this path.

Proposed diff
 constructor(options: TempAliasTransformerOptions = {}) {
     super();
     this.mode = options.mode ?? 'alwaysCompact';
     // PostgreSQL limits identifier length to 63 bytes and silently truncates overlong aliases.
-    this.maxIdentifierLength = options.maxIdentifierLength ?? 63;
+    const maxIdentifierLength = options.maxIdentifierLength ?? 63;
+    if (!Number.isInteger(maxIdentifierLength) || maxIdentifierLength <= 0) {
+        throw new Error('maxIdentifierLength must be a positive integer');
+    }
+    this.maxIdentifierLength = maxIdentifierLength;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/orm/src/client/executor/temp-alias-transformer.ts` around lines 41 -
46, Validate the maxIdentifierLength passed into the TempAliasTransformer
constructor: in constructor of class TempAliasTransformer, ensure
options.maxIdentifierLength is a finite integer > 0 (use Number.isInteger and
isFinite) before assigning to this.maxIdentifierLength; if invalid, throw a
clear RangeError/TypeError (e.g., "maxIdentifierLength must be a positive
integer") so downstream fallback logic that relies on maxIdentifierLength can't
silently misbehave.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/orm/src/client/executor/temp-alias-transformer.ts`:
- Around line 41-46: Validate the maxIdentifierLength passed into the
TempAliasTransformer constructor: in constructor of class TempAliasTransformer,
ensure options.maxIdentifierLength is a finite integer > 0 (use Number.isInteger
and isFinite) before assigning to this.maxIdentifierLength; if invalid, throw a
clear RangeError/TypeError (e.g., "maxIdentifierLength must be a positive
integer") so downstream fallback logic that relies on maxIdentifierLength can't
silently misbehave.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e6425275-091b-46eb-b931-7f7e2cce7c38

📥 Commits

Reviewing files that changed from the base of the PR and between c6a3d3a and ff685cd.

📒 Files selected for processing (2)
  • packages/orm/src/client/executor/temp-alias-transformer.ts
  • packages/orm/src/client/executor/zenstack-query-executor.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/orm/src/client/executor/zenstack-query-executor.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/orm/src/client/executor/temp-alias-transformer.ts`:
- Around line 24-26: The temp-alias length check in transformIdentifier
currently uses character count (node.name.length) which fails for multibyte
UTF-8 characters; change the check in transformIdentifier (and any related
comparisons to this.maxIdentifierLength) to use the byte length of the
identifier (e.g., Buffer.byteLength(node.name, 'utf8') or Buffer.from(node.name,
'utf8').length) when deciding to set hasOverlongTempAlias for names starting
with TEMP_ALIAS_PREFIX so the 63-byte PostgreSQL limit is enforced correctly.

ℹ️ Review info
Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1f959c56-ccdd-4d18-aa3a-cee251458a95

📥 Commits

Reviewing files that changed from the base of the PR and between ff685cd and 5ec19f3.

📒 Files selected for processing (1)
  • packages/orm/src/client/executor/temp-alias-transformer.ts

@pkudinov
Copy link
Contributor Author

pkudinov commented Mar 4, 2026

@ymc9 please take a look, is that what you had in mind? I refactored accordingly in ff685cd by moving the check into TempAliasTransformer with alwaysCompact/compactLongNames modes, and followed up in 560c557 to use UTF-8 byte length for the 63-byte PostgreSQL
limit.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/orm/src/client/executor/temp-alias-transformer.ts (1)

19-35: Consider early termination once an overlong alias is found.

Once hasOverlongTempAlias is set to true, the checker continues traversing the entire AST unnecessarily. For deeply nested queries (which is the target use case), this could be optimized.

♻️ Proposed optimization with early exit
+class TempAliasLengthChecker extends OperationNodeTransformer {
+    private hasOverlongTempAlias = false;
+    private readonly textEncoder = new TextEncoder();
+
+    constructor(private readonly maxIdentifierLength: number) {
+        super();
+    }
+
     run(node: OperationNode) {
         this.hasOverlongTempAlias = false;
-        this.transformNode(node);
+        try {
+            this.transformNode(node);
+        } catch (e) {
+            if (e !== 'early_exit') throw e;
+        }
         return this.hasOverlongTempAlias;
     }

     protected override transformIdentifier(node: IdentifierNode): IdentifierNode {
         if (!node.name.startsWith(TEMP_ALIAS_PREFIX)) {
             return node;
         }

         const aliasByteLength = this.textEncoder.encode(node.name).length;
         if (aliasByteLength > this.maxIdentifierLength) {
             this.hasOverlongTempAlias = true;
+            throw 'early_exit';
         }
         return node;
     }
 }

Alternatively, if Kysely's transformer supports a cleaner short-circuit mechanism, that would be preferable. This is a nice-to-have optimization given the target use case involves deep nesting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/orm/src/client/executor/temp-alias-transformer.ts` around lines 19 -
35, The traversal should short-circuit once an overlong temp alias is detected:
add a guard at the top of the AST traversal entry used by this transformer
(override/modify transformNode) so it immediately returns the incoming node when
this.hasOverlongTempAlias is true, and also add an early-return in
transformIdentifier (check this.hasOverlongTempAlias before calling
textEncoder.encode) to avoid doing byte-length checks after the flag is set;
this uses the existing symbols run, transformNode, transformIdentifier,
hasOverlongTempAlias, TEMP_ALIAS_PREFIX, textEncoder, and maxIdentifierLength so
the traversal stops as soon as an overlong alias is found.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/orm/src/client/executor/temp-alias-transformer.ts`:
- Around line 19-35: The traversal should short-circuit once an overlong temp
alias is detected: add a guard at the top of the AST traversal entry used by
this transformer (override/modify transformNode) so it immediately returns the
incoming node when this.hasOverlongTempAlias is true, and also add an
early-return in transformIdentifier (check this.hasOverlongTempAlias before
calling textEncoder.encode) to avoid doing byte-length checks after the flag is
set; this uses the existing symbols run, transformNode, transformIdentifier,
hasOverlongTempAlias, TEMP_ALIAS_PREFIX, textEncoder, and maxIdentifierLength so
the traversal stops as soon as an overlong alias is found.

ℹ️ Review info
Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: af85178c-daea-4064-b4a9-cafa2c527a15

📥 Commits

Reviewing files that changed from the base of the PR and between 5ec19f3 and 560c557.

📒 Files selected for processing (1)
  • packages/orm/src/client/executor/temp-alias-transformer.ts

@pkudinov
Copy link
Contributor Author

pkudinov commented Mar 4, 2026

Implemented in 6cb0088 — TempAliasLengthChecker now short-circuits traversal after detecting the first overlong temp alias (with an additional guard in transformIdentifier), so deep ASTs avoid unnecessary work.

@pkudinov pkudinov requested a review from ymc9 March 5, 2026 19:56
@ymc9 ymc9 merged commit 8208900 into zenstackhq:dev Mar 6, 2026
10 checks passed
@pkudinov pkudinov deleted the fix/issue-2424-temp-alias-safety-fallback branch March 6, 2026 23:14
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.

PolicyPlugin deep nested include generates malformed PostgreSQL alias reference (42703)

2 participants