feat(sql-to-rest): group by

This commit is contained in:
Greg Richardson committed 2024-05-08 15:53:06 -06:00
1 parent a5fb595e04
commit 8ca3cc1fa1
6 files changed
+140 -61

No files matched your search

+75
View File
@@ -226,12 +226,34 @@ function processSelectStmt(stmt: SelectStmt): Select {
throw new UnsupportedError('CTEs are not supported')
}
if (stmt.SelectStmt.havingClause) {
throw new UnsupportedError('The HAVING clause is not supported')
}
const [fromClause] = stmt.SelectStmt.fromClause
const { from, alias, embeddedTargets } = processFromClause(fromClause)
const targets = processTargetList(stmt.SelectStmt.targetList, alias ?? from, embeddedTargets)
const groupByColumns =
stmt.SelectStmt.groupClause?.map((columnRef) => renderFields(columnRef.ColumnRef.fields)) ?? []
if (
!groupByColumns.every((column) => someTarget(targets, (target) => target.column === column))
) {
throw new Error(`Every group by column must also exist as a select target`)
}
if (
groupByColumns.length > 0 &&
!someTarget(targets, (target) => target.type === 'aggregate-target')
) {
throw new Error(
`There must be at least one aggregate function in the select target list when using group by`
)
}
const filter = stmt.SelectStmt.whereClause
? processWhereClause(stmt.SelectStmt.whereClause)
: undefined
@@ -934,3 +956,56 @@ function processJsonTarget(expression: A_Expr): ColumnTarget {
cast,
}
}
/**
* Recursively iterates through PostgREST filters and checks if the predicate
* matches any of them (ie. `some()`).
*/
export function someFilter(filter: Filter, predicate: (filter: ColumnFilter) => boolean): boolean {
const { type } = filter
if (type === 'column') {
return predicate(filter)
} else if (type === 'logical') {
return filter.values.some((f) => someFilter(f, predicate))
} else {
throw new Error(`Unknown filter type '${type}'`)
}
}
/**
* Recursively iterates through a PostgREST target list and checks if the predicate
* matches any of them (ie. `some()`).
*/
export function someTarget(
targets: Target[],
predicate: (target: ColumnTarget | AggregateTarget) => boolean
): boolean {
return targets.some((target) => {
const { type } = target
if (type === 'column-target' || type === 'aggregate-target') {
return predicate(target)
} else if (type === 'embedded-target') {
return someTarget(target.targets, predicate)
} else {
throw new Error(`Unknown target type '${type}'`)
}
})
}
/**
* Recursively flattens PostgREST embedded targets.
*/
export function flattenTargets(targets: Target[]): Target[] {
return targets.flatMap((target) => {
const { type } = target
if (type === 'column-target' || type === 'aggregate-target') {
return target
} else if (type === 'embedded-target') {
return [target, ...flattenTargets(target.targets)]
} else {
throw new Error(`Unknown target type '${type}'`)
}
})
}
@@ -1149,4 +1149,63 @@ describe('select', () => {
expect(method).toBe('GET')
expect(fullPath).toBe('/orders?select=order_details->tax_amount::numeric.sum()')
})
test('group by aggregates', async () => {
const sql = stripIndents`
select
sum(amount),
category
from
orders
group by
category
`
const statement = await processSql(sql)
const { method, fullPath } = await renderHttp(statement)
expect(method).toBe('GET')
expect(fullPath).toBe('/orders?select=amount.sum(),category')
})
test('group by without select target fails', async () => {
const sql = stripIndents`
select
sum(amount)
from
orders
group by
category
`
await expect(processSql(sql)).rejects.toThrowError()
})
test('group by with having fails', async () => {
const sql = stripIndents`
select
sum(amount),
category
from
orders
group by
category
having sum(amount) > 1000
`
await expect(processSql(sql)).rejects.toThrowError()
})
test('group by without aggregate fails', async () => {
const sql = stripIndents`
select
category
from
orders
group by
category
`
await expect(processSql(sql)).rejects.toThrowError()
})
})
@@ -195,7 +195,7 @@ export type SelectStmt = {
distinctClause?: any[]
intoClause?: any[]
whereClause?: WhereExpression
groupClause?: any[]
groupClause?: ColumnRef[]
groupDistinct?: boolean
havingClause?: any[]
withClause?: any
@@ -1,5 +1,5 @@
import { EmbeddedTarget } from 'sql-to-rest'
import { ResultBundle, flattenTargets } from './util'
import { EmbeddedTarget, flattenTargets } from 'sql-to-rest'
import { ResultBundle } from './util'
export type Assumption = {
id: string
+2 -1
View File
@@ -1,5 +1,6 @@
import { stripIndent } from 'common-tags'
import { ResultBundle, someFilter } from './util'
import { someFilter } from 'sql-to-rest'
import { ResultBundle } from './util'
export type Faq = {
id: string
+1 -57
View File
@@ -1,12 +1,4 @@
import {
ColumnFilter,
ColumnTarget,
Filter,
HttpRequest,
Statement,
SupabaseJsQuery,
Target,
} from 'sql-to-rest'
import { HttpRequest, Statement, SupabaseJsQuery } from 'sql-to-rest'
export type BaseResult = {
statement: Statement
@@ -25,51 +17,3 @@ export type SupabaseJsResult = BaseResult &
}
export type ResultBundle = HttpResult | SupabaseJsResult
/**
* Recursively iterates through PostgREST filters and checks if the predicate
* matches any of them (ie. `some()`).
*/
export function someFilter(filter: Filter, predicate: (filter: ColumnFilter) => boolean): boolean {
const { type } = filter
if (type === 'column') {
return predicate(filter)
} else if (type === 'logical') {
return filter.values.some((f) => someFilter(f, predicate))
} else {
throw new Error(`Unknown filter type '${type}'`)
}
}
/**
* Recursively iterates through a PostgREST target list and checks if the predicate
* matches any of them (ie. `some()`).
*/
export function someTarget(target: Target, predicate: (target: ColumnTarget) => boolean): boolean {
const { type } = target
if (type === 'column-target') {
return predicate(target)
} else if (type === 'embedded-target') {
return target.targets.some((t) => someTarget(t, predicate))
} else {
throw new Error(`Unknown target type '${type}'`)
}
}
/**
* Recursively flattens PostgREST embedded targets.
*/
export function flattenTargets(targets: Target[]): Target[] {
return targets.flatMap((target) => {
const { type } = target
if (type === 'column-target') {
return target
} else if (type === 'embedded-target') {
return [target, ...flattenTargets(target.targets)]
} else {
throw new Error(`Unknown target type '${type}'`)
}
})
}