mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 08:09:11 +03:00
feat(sql-to-rest): group by
This commit is contained in:
1 parent
a5fb595e04
commit
8ca3cc1fa1
6 files changed
+140
-61
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,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}'`)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user