Skip to content

Commit e8e6577

Browse files
committed
fix(typegen): infinite recursion error
Fixes regression introduced in #627 While fixing the invalid intersection for conflicting keys case the Omit did lead to a huge increase in the recursive type complexity Removing it bring back the corner case, but allow much more longer queries. A test is now in place to ensure minimal coverage over the query complexities we can handle before reaching infinite recursion errors.
1 parent b9cd2b1 commit e8e6577

File tree

6 files changed

+69
-91
lines changed

6 files changed

+69
-91
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
"test": "run-s format:check test:types db:clean db:run test:run db:clean && node test/smoke.cjs && node test/smoke.mjs",
4141
"test:run": "jest --runInBand --coverage",
4242
"test:update": "run-s db:clean db:run db:generate-test-types && jest --runInBand --updateSnapshot && run-s db:clean",
43-
"test:types": "tsd --files 'test/**/*.test*.ts'",
44-
"test:types:watch": "tsd --files 'test/**/*.test*.ts' --watch",
43+
"test:types": "run-s build && tsd --files 'test/**/*.test-d.ts'",
44+
"test:types:watch": "run-s build && tsd --files 'test/**/*.test-d.ts' --watch",
4545
"type-check": "tsc --noEmit --project tsconfig.json",
4646
"type-check:test": "tsc --noEmit --project tsconfig.test.json",
4747
"db:clean": "cd test/db && docker compose down --volumes",

src/select-query-parser/result.ts

Lines changed: 41 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -167,89 +167,6 @@ export type RPCCallNodes<
167167
: SelectQueryError<'Invalid first node in RPC call'>
168168
: Prettify<Acc>
169169

170-
type SplitArray<
171-
T extends readonly unknown[],
172-
N extends number,
173-
Acc extends readonly unknown[] = []
174-
> = Acc['length'] extends N
175-
? [Acc, T]
176-
: T extends readonly [infer Head, ...infer Tail]
177-
? SplitArray<Tail, N, [...Acc, Head]>
178-
: [Acc, T]
179-
180-
type ProcessBatch<
181-
ClientOptions extends ClientServerOptions,
182-
Schema extends GenericSchema,
183-
Row extends Record<string, unknown>,
184-
RelationName extends string,
185-
Relationships extends GenericRelationship[],
186-
Batch extends Ast.Node[],
187-
Acc extends Record<string, unknown>
188-
> = Batch extends [infer Head, ...infer Tail]
189-
? Head extends Ast.Node
190-
? Tail extends Ast.Node[]
191-
? ProcessNode<
192-
ClientOptions,
193-
Schema,
194-
Row,
195-
RelationName,
196-
Relationships,
197-
Head
198-
> extends infer Result
199-
? Result extends Record<string, unknown>
200-
? ProcessBatch<
201-
ClientOptions,
202-
Schema,
203-
Row,
204-
RelationName,
205-
Relationships,
206-
Tail,
207-
Omit<Acc, keyof Result> & Result
208-
>
209-
: Result
210-
: SelectQueryError<'Node processing failed'>
211-
: Acc
212-
: Acc
213-
: Acc
214-
215-
type ProcessNodesBatched<
216-
ClientOptions extends ClientServerOptions,
217-
Schema extends GenericSchema,
218-
Row extends Record<string, unknown>,
219-
RelationName extends string,
220-
Relationships extends GenericRelationship[],
221-
Nodes extends Ast.Node[],
222-
Acc extends Record<string, unknown> = {}
223-
> = Nodes extends []
224-
? Prettify<Acc>
225-
: SplitArray<Nodes, 5> extends [infer Batch, infer Remaining]
226-
? Batch extends Ast.Node[]
227-
? Remaining extends Ast.Node[]
228-
? ProcessBatch<
229-
ClientOptions,
230-
Schema,
231-
Row,
232-
RelationName,
233-
Relationships,
234-
Batch,
235-
Acc
236-
> extends infer BatchResult
237-
? BatchResult extends Record<string, unknown>
238-
? ProcessNodesBatched<
239-
ClientOptions,
240-
Schema,
241-
Row,
242-
RelationName,
243-
Relationships,
244-
Remaining,
245-
BatchResult
246-
>
247-
: BatchResult
248-
: SelectQueryError<'Batch processing failed'>
249-
: SelectQueryError<'Invalid remaining nodes'>
250-
: SelectQueryError<'Invalid batch nodes'>
251-
: SelectQueryError<'Array splitting failed'>
252-
253170
/**
254171
* Recursively processes an array of Nodes and accumulates the resulting TypeScript type.
255172
*
@@ -266,9 +183,48 @@ export type ProcessNodes<
266183
Row extends Record<string, unknown>,
267184
RelationName extends string,
268185
Relationships extends GenericRelationship[],
269-
Nodes extends Ast.Node[]
186+
Nodes extends Ast.Node[],
187+
Acc extends Record<string, unknown> = {} // Acc is now an object
270188
> = CheckDuplicateEmbededReference<Schema, RelationName, Relationships, Nodes> extends false
271-
? ProcessNodesBatched<ClientOptions, Schema, Row, RelationName, Relationships, Nodes>
189+
? Nodes extends [infer FirstNode, ...infer RestNodes]
190+
? FirstNode extends Ast.Node
191+
? RestNodes extends Ast.Node[]
192+
? ProcessNode<
193+
ClientOptions,
194+
Schema,
195+
Row,
196+
RelationName,
197+
Relationships,
198+
FirstNode
199+
> extends infer FieldResult
200+
? FieldResult extends Record<string, unknown>
201+
? ProcessNodes<
202+
ClientOptions,
203+
Schema,
204+
Row,
205+
RelationName,
206+
Relationships,
207+
RestNodes,
208+
// TODO:
209+
// This SHOULD be `Omit<Acc, keyof FieldResult> & FieldResult` since in the case where the key
210+
// is present in the Acc already, the intersection will create bad intersection types
211+
// (eg: `{ a: number } & { a: { property } }` will become `{ a: number & { property } }`)
212+
// but using Omit here explode the inference complexity resulting in "infinite recursion error" from typescript
213+
// very early (see: 'Check that selecting many fields doesn't yield an possibly infinite recursion error') test
214+
// in this case we can't get above ~10 fields before reaching the recursion error
215+
// If someone find a better way to do this, please do it !
216+
// It'll also allow to fix those two tests:
217+
// - `'join over a 1-M relation with both nullables and non-nullables fields using column name hinting on nested relation'`
218+
// - `'self reference relation via column''`
219+
Acc & FieldResult
220+
>
221+
: FieldResult extends SelectQueryError<infer E>
222+
? SelectQueryError<E>
223+
: SelectQueryError<'Could not retrieve a valid record or error value'>
224+
: SelectQueryError<'Processing node failed.'>
225+
: SelectQueryError<'Invalid rest nodes array type in ProcessNodes'>
226+
: SelectQueryError<'Invalid first node type in ProcessNodes'>
227+
: Prettify<Acc>
272228
: Prettify<CheckDuplicateEmbededReference<Schema, RelationName, Relationships, Nodes>>
273229

274230
/**

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ type MergeExplicit<New, Row> = {
194194
: never
195195
}
196196

197-
export type MergeDeep<New, Row> = Simplify<
197+
type MergeDeep<New, Row> = Simplify<
198198
MergeExplicit<New, Row> &
199199
// Intersection here is to restore dynamic keys into the merging result
200200
// eg:

test/relationships-join-operations.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,20 @@ test('join over a 1-M relation with both nullables and non-nullables fields usin
625625
// TODO: older versions of zod require this trick for non optional unknown data type
626626
// newer version of zod don't have this issue but require an upgrade of typescript minimal version
627627
let expected: RequiredDeep<z.infer<typeof ExpectedSchema>>
628-
expectType<TypeEqual<typeof result, typeof expected>>(true)
628+
// TODO: fix this type, property merging override give invalid intersection types
629+
// See: result.ts#ProcessNodes comment for more details
630+
let crippledExpected: {
631+
first_friend_of: {
632+
id: (typeof expected)['first_friend_of'][number]['id']
633+
second_user: (typeof expected)['first_friend_of'][number]['second_user']
634+
third_wheel: (typeof expected)['first_friend_of'][number]['third_wheel']
635+
// This intersection shouldn't exist
636+
first_user: string & (typeof expected)['first_friend_of'][number]['first_user']
637+
}[]
638+
second_friend_of: (typeof expected)['second_friend_of']
639+
third_wheel_of: (typeof expected)['third_wheel_of']
640+
}
641+
expectType<TypeEqual<typeof result, typeof crippledExpected>>(true)
629642
ExpectedSchema.parse(res.data)
630643
})
631644

test/relationships.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,14 @@ test('self reference relation via column', async () => {
493493
.nullable(),
494494
})
495495
let expected: z.infer<typeof ExpectedSchema>
496-
expectType<TypeEqual<typeof result, typeof expected>>(true)
496+
// TODO: fix this type, property merging override give invalid intersection types
497+
// See: result.ts#ProcessNodes comment for more details
498+
let crippledExpected: {
499+
description: (typeof expected)['description']
500+
id: (typeof expected)['id']
501+
parent_id: (number & (typeof expected)['parent_id']) | null
502+
}
503+
expectType<TypeEqual<typeof result, typeof crippledExpected>>(true)
497504
ExpectedSchema.parse(res.data)
498505
})
499506

test/returns.test-d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,9 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
194194
{
195195
const result = await postgrest
196196
.from('users')
197-
// Maximum reached was one hundred seventy six fields before the recursion too deep error
197+
// Maximum reached was one hundred seventy six fields
198+
// Without the Omit<Acc, keyof FieldResult> & FieldResult
199+
// With it, it raise as soon as after 12 fields only
198200
.select(
199201
`username,
200202
catchphrase,

0 commit comments

Comments
 (0)