Skip to content

Commit 6f5d570

Browse files
bnjmnt4nsteve-chavez
authored andcommitted
Parser: Add support for aggregate functions
1 parent f30fcc2 commit 6f5d570

File tree

2 files changed

+104
-11
lines changed

2 files changed

+104
-11
lines changed

src/select-query-parser.ts

+85-11
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ type TypeScriptSingleValueTypes<T extends SingleValuePostgreSQLTypes> = T extend
9797
? Record<string, unknown>
9898
: unknown
9999

100+
type AggregateFunctions = 'count' | 'sum' | 'avg' | 'min' | 'max'
101+
100102
type StripUnderscore<T extends string> = T extends `_${infer U}` ? U : T
101103

102104
type TypeScriptTypes<T extends PostgreSQLTypes> = T extends ArrayPostgreSQLTypes
@@ -392,21 +394,74 @@ type ParseField<Input extends string> = Input extends ''
392394
* Parses a field excluding embedded resources, without preceding field renaming.
393395
* This is one of the following:
394396
* - `field`
397+
* - `field.aggregate()`
398+
* - `field.aggregate()::type`
399+
* - `field::type`
400+
* - `field::type.aggregate()`
401+
* - `field::type.aggregate()::type`
402+
* - `field->json...`
403+
* - `field->json.aggregate()`
404+
* - `field->json.aggregate()::type`
405+
* - `field->json::type`
406+
* - `field->json::type.aggregate()`
407+
* - `field->json::type.aggregate()::type`
408+
*/
409+
type ParseFieldWithoutEmbeddedResource<Input extends string> =
410+
ParseFieldWithoutEmbeddedResourceAndAggregation<Input> extends [infer Field, `${infer Remainder}`]
411+
? ParseFieldAggregation<EatWhitespace<Remainder>> extends [
412+
`${infer AggregateFunction}`,
413+
`${infer Remainder}`
414+
]
415+
? ParseFieldTypeCast<EatWhitespace<Remainder>> extends [infer Type, `${infer Remainder}`]
416+
? // `field.aggregate()::type`
417+
[
418+
Omit<Field, 'name' | 'original' | 'type'> & {
419+
name: AggregateFunction
420+
original: AggregateFunction
421+
type: Type
422+
},
423+
EatWhitespace<Remainder>
424+
]
425+
: ParseFieldTypeCast<EatWhitespace<Remainder>> extends ParserError<string>
426+
? ParseFieldTypeCast<EatWhitespace<Remainder>>
427+
: // `field.aggregate()`
428+
[
429+
Omit<Field, 'name' | 'original'> & {
430+
name: AggregateFunction
431+
original: AggregateFunction
432+
},
433+
EatWhitespace<Remainder>
434+
]
435+
: ParseFieldAggregation<EatWhitespace<Remainder>> extends ParserError<string>
436+
? ParseFieldAggregation<EatWhitespace<Remainder>>
437+
: // `field`
438+
[Field, EatWhitespace<Remainder>]
439+
: CreateParserErrorIfRequired<
440+
ParseFieldWithoutEmbeddedResourceAndAggregation<Input>,
441+
`Expected field at \`${Input}\``
442+
>
443+
444+
/**
445+
* Parses a field excluding embedded resources or aggregation, without preceding field renaming.
446+
* This is one of the following:
447+
* - `field`
395448
* - `field::type`
396449
* - `field->json...`
397450
* - `field->json...::type`
398451
*/
399-
type ParseFieldWithoutEmbeddedResource<Input extends string> = Input extends ''
400-
? ParserError<'Empty string'>
401-
: ParseFieldWithoutEmbeddedResourceAndTypeCast<Input> extends [infer Field, `${infer Remainder}`]
402-
? ParseFieldTypeCast<EatWhitespace<Remainder>> extends [infer Type, `${infer Remainder}`]
403-
? // `field::type`
404-
[Field & { type: Type }, EatWhitespace<Remainder>]
405-
: ParseFieldTypeCast<EatWhitespace<Remainder>> extends ParserError<string>
406-
? ParseFieldTypeCast<EatWhitespace<Remainder>>
407-
: // `field`
408-
[Field, EatWhitespace<Remainder>]
409-
: ParserError<`Expected identifier at \`${Input}\``>
452+
type ParseFieldWithoutEmbeddedResourceAndAggregation<Input extends string> =
453+
ParseFieldWithoutEmbeddedResourceAndTypeCast<Input> extends [infer Field, `${infer Remainder}`]
454+
? ParseFieldTypeCast<EatWhitespace<Remainder>> extends [infer Type, `${infer Remainder}`]
455+
? // `field::type` or `field->json...::type`
456+
[Omit<Field, 'type'> & { type: Type }, EatWhitespace<Remainder>]
457+
: ParseFieldTypeCast<EatWhitespace<Remainder>> extends ParserError<string>
458+
? ParseFieldTypeCast<EatWhitespace<Remainder>>
459+
: // `field` or `field->json...`
460+
[Field, EatWhitespace<Remainder>]
461+
: CreateParserErrorIfRequired<
462+
ParseFieldWithoutEmbeddedResourceAndTypeCast<Input>,
463+
`Expected field at \`${Input}\``
464+
>
410465

411466
/**
412467
* Parses a field excluding embedded resources or typecasting, without preceding field renaming.
@@ -443,6 +498,25 @@ type ParseFieldTypeCast<Input extends string> = EatWhitespace<Input> extends `::
443498
: ParserError<`Invalid type for \`::\` operator at \`${Remainder}\``>
444499
: Input
445500

501+
/**
502+
* Parses a field aggregation (`.max()`), returning a tuple of ["Aggregate function", "Remainder of text"]
503+
* or the original string input indicating that no aggregation was found.
504+
*/
505+
type ParseFieldAggregation<Input extends string> =
506+
EatWhitespace<Input> extends `.${infer Remainder}`
507+
? ParseIdentifier<EatWhitespace<Remainder>> extends [
508+
`${infer FunctionName}`,
509+
`${infer Remainder}`
510+
]
511+
? // Ensure that aggregation function is valid.
512+
FunctionName extends AggregateFunctions
513+
? EatWhitespace<Remainder> extends `()${infer Remainder}`
514+
? [FunctionName, EatWhitespace<Remainder>]
515+
: ParserError<`Expected \`()\` after \`.\` operator \`${FunctionName}\``>
516+
: ParserError<`Invalid type for \`.\` operator \`${FunctionName}\``>
517+
: ParserError<`Invalid type for \`.\` operator at \`${Remainder}\``>
518+
: Input
519+
446520
/**
447521
* Parses a node.
448522
* A node is one of the following:

test/index.test-d.ts

+19
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,25 @@ const postgrest = new PostgrestClient<Database>(REST_URL)
112112
expectType<string>(data.baz)
113113
}
114114

115+
// typecasting and aggregate functions
116+
{
117+
const { data, error } = await postgrest
118+
.from('messages')
119+
.select(
120+
'message, users.count(), casted_message:message::int4, casted_count:users.count()::text'
121+
)
122+
.single()
123+
if (error) {
124+
throw new Error(error.message)
125+
}
126+
expectType<{
127+
message: string | null
128+
count: number
129+
casted_message: number
130+
casted_count: string
131+
}>(data)
132+
}
133+
115134
// rpc return type
116135
{
117136
const { data, error } = await postgrest.rpc('get_status')

0 commit comments

Comments
 (0)