Skip to content

feat(sql-orm-client): expand and simplify output types#283

Open
aqrln wants to merge 1 commit intomainfrom
tml-2139-prettify-query-result-types-with-recursive-mapped-type
Open

feat(sql-orm-client): expand and simplify output types#283
aqrln wants to merge 1 commit intomainfrom
tml-2139-prettify-query-result-types-with-recursive-mapped-type

Conversation

@aqrln
Copy link
Copy Markdown
Member

@aqrln aqrln commented Apr 2, 2026

Closes: https://linear.app/prisma-company/issue/TML-2139/prettify-query-result-types-with-recursive-mapped-type

Query result types in sql-orm-client are currently unexpanded intersections full of internal utility types, making them hard to read in IDE tooltips and error messages.

This change applies a recursive mapped type at the top-level return positions so that IDE tooltips and hover types display fully-evaluated object literals.

Before:

image

After:

image

Summary by CodeRabbit

  • Type System Improvements

    • Enhanced type inference for SQL collection queries with improved type simplification for better IDE support and clearer result shapes.
  • Style

    • Updated CSS quote formatting for consistency.
  • Tests

    • Added type validation tests for query result shape handling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ea953b5e-3b44-407b-9c53-47c72edc3f7f

📥 Commits

Reviewing files that changed from the base of the PR and between 131787e and c25c1dd.

📒 Files selected for processing (5)
  • examples/mongo-demo/src/app/styles.css
  • packages/3-extensions/sql-orm-client/src/collection.ts
  • packages/3-extensions/sql-orm-client/src/grouped-collection.ts
  • packages/3-extensions/sql-orm-client/src/types.ts
  • packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts
✅ Files skipped from review due to trivial changes (2)
  • examples/mongo-demo/src/app/styles.css
  • packages/3-extensions/sql-orm-client/src/grouped-collection.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/3-extensions/sql-orm-client/src/types.ts
  • packages/3-extensions/sql-orm-client/src/collection.ts

📝 Walkthrough

Walkthrough

The PR introduces a new SimplifyDeep TypeScript utility type that recursively flattens nested and intersected types, applies it to Collection and GroupedCollection method return types to improve type inference, adds comprehensive tests validating the utility, and updates CSS quote formatting.

Changes

Cohort / File(s) Summary
Type utility definition
packages/3-extensions/sql-orm-client/src/types.ts
Introduced new exported SimplifyDeep<T> utility type that recursively simplifies nested types, unwraps readonly arrays, treats primitives and built-ins as terminal, and flattens object intersections.
Collection type signatures
packages/3-extensions/sql-orm-client/src/collection.ts, packages/3-extensions/sql-orm-client/src/grouped-collection.ts
Updated generic default and method return types (include, select, aggregate) to wrap row type expressions with SimplifyDeep for improved type inference and flattened object shapes; no runtime logic changes.
Type utility tests
packages/3-extensions/sql-orm-client/test/simplify-deep.test.ts
Added comprehensive test suite validating SimplifyDeep behavior on primitives, branded types, built-ins, intersections, arrays, and nullable unions; includes runtime integration tests for Collection query result type shapes.
Style formatting
examples/mongo-demo/src/app/styles.css
Updated CSS custom property and content values to use double quotes consistently instead of single quotes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 With SimplifyDeep, the types now gleam,
No nested intersections mar our dream,
Flattened objects, arrays unwound,
Clean inference where chaos was found!
Hop along, the types align,
TypeScript's magic, now refined. ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(sql-orm-client): expand and simplify output types' accurately describes the main change—introducing SimplifyDeep to flatten and improve query result type display in IDE tooltips.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2139-prettify-query-result-types-with-recursive-mapped-type

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 2, 2026

Open in StackBlitz

@prisma-next/runtime-executor

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/runtime-executor@283

@prisma-next/mongo-core

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-core@283

@prisma-next/mongo-orm

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-orm@283

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-runtime@283

@prisma-next/sql-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-runtime@283

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-paradedb@283

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-pgvector@283

@prisma-next/postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/postgres@283

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-orm-client@283

@prisma-next/target-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-mongo@283

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-mongo@283

@prisma-next/driver-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-mongo@283

@prisma-next/contract-authoring

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract-authoring@283

@prisma-next/contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract-ts@283

@prisma-next/ids

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ids@283

@prisma-next/psl-parser

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-parser@283

@prisma-next/psl-printer

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-printer@283

@prisma-next/cli

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli@283

@prisma-next/emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/emitter@283

@prisma-next/migration-tools

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/migration-tools@283

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/vite-plugin-contract-emit@283

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-emitter@283

@prisma-next/sql-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract@283

@prisma-next/sql-errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-errors@283

@prisma-next/sql-operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-operations@283

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-schema-ir@283

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-psl@283

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-ts@283

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-emitter@283

@prisma-next/family-sql

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-sql@283

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-lane-query-builder@283

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-relational-core@283

@prisma-next/sql-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-builder@283

@prisma-next/target-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-postgres@283

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-postgres@283

@prisma-next/driver-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-postgres@283

@prisma-next/core-control-plane

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/core-control-plane@283

@prisma-next/core-execution-plane

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/core-execution-plane@283

@prisma-next/config

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/config@283

@prisma-next/contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract@283

@prisma-next/operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/operations@283

@prisma-next/plan

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/plan@283

@prisma-next/utils

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/utils@283

commit: c25c1dd

Copy link
Copy Markdown

@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 (2)
packages/3-extensions/sql-orm-client/test/simplify-deep.test-d.ts (1)

74-75: Use double cast for test mocks.

Per coding guidelines, test mocks should use double casts (as unknown as X) to make the unsafe boundary explicit.

♻️ Suggested fix
   const runtime = createMockRuntime();
-  const context = {} as ExecutionContext<TestContract>;
+  const context = {} as unknown as ExecutionContext<TestContract>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-extensions/sql-orm-client/test/simplify-deep.test-d.ts` around
lines 74 - 75, The test mock `context` is using a single cast which hides the
unsafe conversion; change the declaration where `context` is assigned (`const
context = {} as ExecutionContext<TestContract>`) to use a double cast (`as
unknown as ExecutionContext<TestContract>`) so the unsafe boundary is explicit;
leave `createMockRuntime()` as-is and update only the `context` variable's
casting.
packages/3-extensions/sql-orm-client/src/types.ts (1)

28-35: Readonly arrays lose their readonly modifier.

The array branch returns SimplifyDeep<Element>[] (mutable), which strips readonly from input types like readonly T[]. If any row fields are typed as readonly arrays, the simplified type will become mutable.

If preserving readonly is desired:

♻️ Suggested fix to preserve readonly
 export type SimplifyDeep<T> =
   T extends readonly (infer Element)[]
-    ? SimplifyDeep<Element>[]
+    ? readonly SimplifyDeep<Element>[]
     : T extends string | number | boolean | bigint | symbol | Date | Uint8Array
       ? T
       : T extends object
         ? { [K in keyof T]: SimplifyDeep<T[K]> }
         : T;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-extensions/sql-orm-client/src/types.ts` around lines 28 - 35, The
SimplifyDeep utility is stripping readonly from array types because the array
branch returns SimplifyDeep<Element>[] (mutable); update the array branch in
SimplifyDeep so it preserves readonly (e.g., when T extends readonly (infer
Element)[] return a readonly array of SimplifyDeep<Element>), ensuring readonly
T[] inputs remain readonly in the output; modify the SimplifyDeep definition
(the branch matching readonly (infer Element)[]) to produce a readonly
SimplifyDeep<Element>[] rather than a mutable array.
🤖 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/3-extensions/sql-orm-client/src/types.ts`:
- Around line 28-35: The SimplifyDeep utility is stripping readonly from array
types because the array branch returns SimplifyDeep<Element>[] (mutable); update
the array branch in SimplifyDeep so it preserves readonly (e.g., when T extends
readonly (infer Element)[] return a readonly array of SimplifyDeep<Element>),
ensuring readonly T[] inputs remain readonly in the output; modify the
SimplifyDeep definition (the branch matching readonly (infer Element)[]) to
produce a readonly SimplifyDeep<Element>[] rather than a mutable array.

In `@packages/3-extensions/sql-orm-client/test/simplify-deep.test-d.ts`:
- Around line 74-75: The test mock `context` is using a single cast which hides
the unsafe conversion; change the declaration where `context` is assigned
(`const context = {} as ExecutionContext<TestContract>`) to use a double cast
(`as unknown as ExecutionContext<TestContract>`) so the unsafe boundary is
explicit; leave `createMockRuntime()` as-is and update only the `context`
variable's casting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 5f49d154-0964-462d-a77d-f8515e353d75

📥 Commits

Reviewing files that changed from the base of the PR and between 39fd54b and 131787e.

📒 Files selected for processing (4)
  • packages/3-extensions/sql-orm-client/src/collection.ts
  • packages/3-extensions/sql-orm-client/src/grouped-collection.ts
  • packages/3-extensions/sql-orm-client/src/types.ts
  • packages/3-extensions/sql-orm-client/test/simplify-deep.test-d.ts

@aqrln aqrln marked this pull request as draft April 2, 2026 10:19
Closes: https://linear.app/prisma-company/issue/TML-2139/prettify-query-result-types-with-recursive-mapped-type

Query result types in `sql-orm-client` are currently unexpanded
intersections full of internal utility types, making them hard to read
in IDE tooltips and error messages.

This change applies a recursive mapped type at the top-level return
positions so that IDE tooltips and hover types display fully-evaluated
object literals.
@aqrln aqrln force-pushed the tml-2139-prettify-query-result-types-with-recursive-mapped-type branch from 131787e to c25c1dd Compare April 2, 2026 10:21
@aqrln aqrln marked this pull request as ready for review April 2, 2026 10:30
Copy link
Copy Markdown
Contributor

@wmadden wmadden left a comment

Choose a reason for hiding this comment

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

Preemptively approving — the approach is sound and the implementation is clean. Please address or respond to the inline comments before merging; no re-review needed.

Blocking (2): F01 (double-wrapping at each chaining step — apply at terminal methods instead), F02 (add test for deeply chained operations).

Non-blocking (3): F03 (readonly array erasure), F04 (raise to @prisma-next/utils), F05 (polymorphic model forward risk).

Comment on lines 130 to 134
export class Collection<
TContract extends SqlContract<SqlStorage>,
ModelName extends string,
Row = DefaultModelRow<TContract, ModelName>,
Row = SimplifyDeep<DefaultModelRow<TContract, ModelName>>,
State extends CollectionTypeState = DefaultCollectionTypeState,
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.

F01 — Blocking: SimplifyDeep applied at every chaining step instead of terminal methods

The default Row is already SimplifyDeep<DefaultModelRow<...>>. When include() returns SimplifyDeep<Row & { posts: ... }>, this becomes SimplifyDeep<SimplifyDeep<...> & { ... }>. Each chaining step adds another wrapper. For users.select('name').include('posts'), that's three nested SimplifyDeep evaluations — O(N×K) work for N chained ops with K total keys.

Suggestion: Apply SimplifyDeep only at the terminal methods (first(), all(), toArray()) instead of at each builder step. Builder methods accumulate raw intersections internally; only the final result type gets simplified. This reduces overhead to exactly one pass regardless of chain length and is architecturally cleaner — simplification is a presentation concern, not a builder concern.

Comment on lines +72 to +149
describe('Collection result types are simplified', () => {
const runtime = createMockRuntime();
const context = getTestContext();

test('default Row is a plain object', () => {
const users = new Collection({ runtime, context }, 'User');
type UserRow = Awaited<ReturnType<typeof users.first>>;
expectTypeOf<NonNullable<UserRow>>().toEqualTypeOf<{
id: number;
name: string;
email: string;
invitedById: number | null;
}>();
});

test('select() produces a plain object', () => {
const users = new Collection({ runtime, context }, 'User');
const selected = users.select('id', 'email');
type SelectedRow = Awaited<ReturnType<typeof selected.first>>;
expectTypeOf<NonNullable<SelectedRow>>().toEqualTypeOf<{
id: number;
email: string;
}>();
});

test('include() produces a plain object with nested relation', () => {
const users = new Collection({ runtime, context }, 'User');
const withPosts = users.include('posts');
type WithPostsRow = Awaited<ReturnType<typeof withPosts.first>>;
expectTypeOf<NonNullable<WithPostsRow>>().toEqualTypeOf<{
id: number;
name: string;
email: string;
invitedById: number | null;
posts: {
id: number;
title: string;
userId: number;
views: number;
}[];
}>();
});

test('select().include() produces a plain object', () => {
const users = new Collection({ runtime, context }, 'User');
const selected = users.select('name').include('posts');
type Row = Awaited<ReturnType<typeof selected.first>>;
expectTypeOf<NonNullable<Row>>().toEqualTypeOf<{
name: string;
posts: {
id: number;
title: string;
userId: number;
views: number;
}[];
}>();
});

test('include() with non-nullable to-one relation', () => {
const posts = new Collection({ runtime, context }, 'Post');
const withAuthor = posts.include('author');
type Row = Awaited<ReturnType<typeof withAuthor.first>>;
type AuthorField = NonNullable<Row>['author'];
expectTypeOf<AuthorField>().toEqualTypeOf<{
id: number;
name: string;
email: string;
invitedById: number | null;
}>();
});

test('include() with count refinement', () => {
const users = new Collection({ runtime, context }, 'User');
const withPostCount = users.include('posts', (posts) => posts.count());
type Row = Awaited<ReturnType<typeof withPostCount.first>>;
expectTypeOf<NonNullable<Row>['posts']>().toEqualTypeOf<number>();
});
});
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.

F02 — Blocking: No test coverage for multi-step chaining (the primary use case)

Deeply nested chains are the primary use case that motivated this change — they produce the most unreadable intersections in IDE tooltips. The tests here verify individual operations and one two-method chain (select().include()), but don't test deeper chains like include('posts').include('invitedBy').

Since each chaining step currently wraps the previous Row in a new SimplifyDeep<Row & ...>, these deeper chains exercise the idempotency property that unit-level tests don't cover.

Suggestion: Add at least one multi-include chain test:

test('chained include() produces a plain object', () => {
  const users = new Collection({ runtime, context }, 'User');
  const withPostsAndInviter = users.include('posts').include('invitedBy');
  type Row = Awaited<ReturnType<typeof withPostsAndInviter.first>>;
  expectTypeOf<NonNullable<Row>>().toEqualTypeOf<{
    id: number;
    name: string;
    email: string;
    invitedById: number | null;
    posts: { id: number; title: string; userId: number; views: number }[];
    invitedBy: { id: number; name: string; email: string; invitedById: number | null } | null;
  }>();
});

Comment on lines +28 to +29
export type SimplifyDeep<T> = T extends readonly (infer Element)[]
? SimplifyDeep<Element>[]
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.

F03 — Non-blocking: readonly arrays are silently converted to mutable arrays

The array branch matches readonly (infer Element)[] but returns SimplifyDeep<Element>[] (mutable). A readonly T[] input becomes T[]. Acceptable for current query result types (which are always mutable arrays), but subtly changes the type contract — relevant if SimplifyDeep is applied more broadly (see F04).

Suggestion: Either preserve readonly with an additional branch, or add a test documenting this intentional behavior:

test('readonly arrays become mutable (intentional)', () => {
  type Input = readonly ({ a: number } & { b: string })[];
  type Result = SimplifyDeep<Input>;
  expectTypeOf<Result>().toEqualTypeOf<{ a: number; b: string }[]>();
});

Comment on lines +24 to +26
// ---------------------------------------------------------------------------
// SimplifyDeep — recursive type prettifier for IDE tooltips
// ---------------------------------------------------------------------------
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.

F04 — Non-blocking: SimplifyDeep should be raised to @prisma-next/utils for cross-lane reuse

SimplifyDeep is a general-purpose type utility with no dependency on sql-orm-client internals. The @prisma-next/utils package (packages/1-framework/1-core/shared/utils) is the canonical home for shared utilities. Moving it there enables reuse by the Mongo family and other lanes.

Not blocking this PR, but should be done as a follow-up before other packages duplicate the type.

Comment on lines +28 to +34
export type SimplifyDeep<T> = T extends readonly (infer Element)[]
? SimplifyDeep<Element>[]
: T extends string | number | boolean | bigint | symbol | Date | Uint8Array
? T
: T extends object
? { [K in keyof T]: SimplifyDeep<T[K]> }
: T;
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.

F05 — Non-blocking: Forward risk — polymorphic model compatibility

Polymorphic model support (ADR 173 — discriminator + variants) will be ported to the SQL domain and will impact this ORM client. Experience implementing a similar helper in the Mongo domain surfaced issues with polymorphic model type shapes.

SimplifyDeep distributes over simple unions correctly (SimplifyDeep<A | B> = SimplifyDeep<A> | SimplifyDeep<B>), but more complex polymorphic type algebra (variant-specific computed properties, conditional types) may not be preserved correctly by the { [K in keyof T]: ... } mapping.

Not a blocker — just flagging that when polymorphic models land in SQL ORM, this helper will need verification and may need extension or removal.

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