feat(sql-to-rest): in operator

This commit is contained in:
Greg Richardson committed 2024-05-09 13:58:14 -06:00
1 parent 560006a9cd
commit ee005f4a7a
5 files changed
+212 -57

No files matched your search

+133 -50
View File
@@ -1,6 +1,7 @@
import { parseQuery } from 'libpg-query'
import { ParsingError, UnimplementedError, UnsupportedError } from './errors'
import {
A_Const,
A_Expr,
ColumnRef,
Field,
@@ -55,22 +56,22 @@ export type NeqColumnFilter = BaseColumnFilter & {
export type GtColumnFilter = BaseColumnFilter & {
operator: 'gt'
value: number
value: string | number
}
export type GteColumnFilter = BaseColumnFilter & {
operator: 'gte'
value: number
value: string | number
}
export type LtColumnFilter = BaseColumnFilter & {
operator: 'lt'
value: number
value: string | number
}
export type LteColumnFilter = BaseColumnFilter & {
operator: 'lte'
value: number
value: string | number
}
export type LikeColumnFilter = BaseColumnFilter & {
@@ -98,6 +99,11 @@ export type IsColumnFilter = BaseColumnFilter & {
value: null
}
export type InColumnFilter = BaseColumnFilter & {
operator: 'in'
value: (string | number)[]
}
export type ColumnFilter =
| EqColumnFilter
| NeqColumnFilter
@@ -110,6 +116,7 @@ export type ColumnFilter =
| MatchColumnFilter
| ImatchColumnFilter
| IsColumnFilter
| InColumnFilter
export type LogicalFilter = BaseFilter & {
type: 'logical'
@@ -655,34 +662,75 @@ function processWhereClause(expression: WhereExpression): Filter {
throw new UnsupportedError(`Non column values not supported in WHERE clause`)
}
const kind = expression.A_Expr.kind
const [name] = expression.A_Expr.name
const operatorSymbol = name.String.sval
const operator = mapOperatorSymbol(operatorSymbol)
const operator = mapOperatorSymbol(kind, operatorSymbol)
if (!('A_Const' in expression.A_Expr.rexpr)) {
throw new UnsupportedError(`Expected right side of WHERE clause expression to be a constant`)
}
if (
operator === 'eq' ||
operator === 'neq' ||
operator === 'gt' ||
operator === 'gte' ||
operator === 'lt' ||
operator === 'lte'
) {
if (!('A_Const' in expression.A_Expr.rexpr)) {
throw new UnsupportedError(
`Expected right side of WHERE clause '${operatorSymbol}' expression to be a constant`
)
}
let value: any
const value = parseConstant(expression.A_Expr.rexpr)
return {
type: 'column',
column,
operator,
negate: false,
value,
}
} else if (
operator === 'like' ||
operator === 'ilike' ||
operator === 'match' ||
operator === 'imatch'
) {
if (!('A_Const' in expression.A_Expr.rexpr) || !('sval' in expression.A_Expr.rexpr.A_Const)) {
throw new UnsupportedError(
`Expected right side of WHERE clause '${operator}' expression to be a string constant`
)
}
if ('sval' in expression.A_Expr.rexpr.A_Const) {
value = expression.A_Expr.rexpr.A_Const.sval.sval
} else if ('ival' in expression.A_Expr.rexpr.A_Const) {
value = expression.A_Expr.rexpr.A_Const.ival.ival
} else if ('fval' in expression.A_Expr.rexpr.A_Const) {
value = parseFloat(expression.A_Expr.rexpr.A_Const.fval.fval)
const value = expression.A_Expr.rexpr.A_Const.sval.sval
return {
type: 'column',
column,
operator,
negate: false,
value,
}
} else if (operator === 'in') {
if (
!('List' in expression.A_Expr.rexpr) ||
!expression.A_Expr.rexpr.List.items.every((item) => 'A_Const' in item)
) {
throw new UnsupportedError(
`Expected right side of WHERE clause '${operator}' expression to be a list of constants`
)
}
const value = expression.A_Expr.rexpr.List.items.map((item) => parseConstant(item))
return {
type: 'column',
column,
operator,
negate: false,
value,
}
} else {
throw new UnsupportedError(
`WHERE clause values must be a string (sval), integer (ival), or float (fval)`
)
}
return {
type: 'column',
column,
operator,
negate: false,
value,
throw new UnsupportedError(`Unsupported operator '${operator}'`)
}
} else if ('NullTest' in expression) {
const { fields } = expression.NullTest.arg.ColumnRef
@@ -849,30 +897,54 @@ function mapJoinType(joinType: string) {
}
}
function mapOperatorSymbol(operatorSymbol: string) {
switch (operatorSymbol) {
case '=':
return 'eq'
case '<>':
return 'neq'
case '>':
return 'gt'
case '>=':
return 'gte'
case '<':
return 'lt'
case '<=':
return 'lte'
case '~~':
return 'like'
case '~~*':
return 'ilike'
case '~':
return 'match'
case '~*':
return 'imatch'
default:
throw new UnsupportedError(`Unsupported operator symbol '${operatorSymbol}'`)
function mapOperatorSymbol(kind: A_Expr['A_Expr']['kind'], operatorSymbol: string) {
switch (kind) {
case 'AEXPR_OP': {
switch (operatorSymbol) {
case '=':
return 'eq'
case '<>':
return 'neq'
case '>':
return 'gt'
case '>=':
return 'gte'
case '<':
return 'lt'
case '<=':
return 'lte'
case '~':
return 'match'
case '~*':
return 'imatch'
default:
throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)
}
}
case 'AEXPR_LIKE': {
switch (operatorSymbol) {
case '~~':
return 'like'
default:
throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)
}
}
case 'AEXPR_ILIKE': {
switch (operatorSymbol) {
case '~~*':
return 'ilike'
default:
throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)
}
}
case 'AEXPR_IN': {
switch (operatorSymbol) {
case '=':
return 'in'
default:
throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)
}
}
}
}
@@ -1117,3 +1189,14 @@ function validateGroupClause(groupClause: ColumnRef[], targets: Target[], from:
)
}
}
function parseConstant(constant: A_Const) {
if ('sval' in constant.A_Const) {
return constant.A_Const.sval.sval
} else if ('ival' in constant.A_Const) {
return constant.A_Const.ival.ival
} else if ('fval' in constant.A_Const) {
return parseFloat(constant.A_Const.fval.fval)
} else {
throw new UnsupportedError(`Constant values must be a string, integer, or float`)
}
}
@@ -309,6 +309,40 @@ describe('select', () => {
expect(fullPath).toBe('/books?description=imatch.%5E[a-z]%2B')
})
test('in operator', async () => {
const sql = stripIndents`
select
*
from
books
where
category in ('fiction', 'sci-fi')
`
const statement = await processSql(sql)
const { method, fullPath } = await renderHttp(statement)
expect(method).toBe('GET')
expect(fullPath).toBe('/books?category=in.(fiction,sci-fi)')
})
test('in operator with comma', async () => {
const sql = stripIndents`
select
*
from
books
where
category in ('a,b,c', 'd,e,f')
`
const statement = await processSql(sql)
const { method, fullPath } = await renderHttp(statement)
expect(method).toBe('GET')
expect(fullPath).toBe('/books?category=in.(%22a,b,c%22,%22d,e,f%22)')
})
test('"and" expression', async () => {
const sql = stripIndents`
select
+15 -4
View File
@@ -94,13 +94,24 @@ function formatSelectFilterRoot(params: URLSearchParams, filter: Filter) {
// Column filter, eg. "title=eq.Cheese"
if (type === 'column') {
let value = filter.value
// Convert '%' to URL-safe '*'
if (filter.operator === 'like' || filter.operator === 'ilike') {
value = filter.value.replaceAll('%', '*')
const value = filter.value.replaceAll('%', '*')
params.append(filter.column, `${maybeNot}${filter.operator}.${value}`)
} else if (filter.operator === 'in') {
const value = filter.value
.map((value) => {
// If an 'in' value contains a comma, wrap in double quotes
if (value.toString().includes(',')) {
return `"${value}"`
}
return value
})
.join(',')
params.append(filter.column, `${maybeNot}${filter.operator}.(${value})`)
} else {
params.append(filter.column, `${maybeNot}${filter.operator}.${filter.value}`)
}
params.append(filter.column, `${maybeNot}${filter.operator}.${value}`)
}
// Logical operator filter, eg. "or=(title.eq.Cheese,title.eq.Salsa)""
else if (type === 'logical') {
@@ -322,7 +322,7 @@ describe('select', () => {
from
books
where
description ~ '^[a-zA-Z]+'
description ~ '^[a-zA-Z]+'
`
const statement = await processSql(sql)
@@ -343,7 +343,7 @@ describe('select', () => {
from
books
where
description ~* '^[a-z]+'
description ~* '^[a-z]+'
`
const statement = await processSql(sql)
@@ -357,6 +357,27 @@ describe('select', () => {
`)
})
test('in operator', async () => {
const sql = stripIndents`
select
*
from
books
where
category in ('fiction', 'sci-fi')
`
const statement = await processSql(sql)
const { code } = await renderSupabaseJs(statement)
expect(code).toBe(stripIndent`
const { data, error } = await supabase
.from('books')
.select()
.in('category', ['fiction', 'sci-fi'])
`)
})
test('"and" expression', async () => {
const sql = stripIndents`
select
@@ -58,7 +58,7 @@ export type A_Expr = {
| 'AEXPR_NOT_BETWEEN_SYM'
name: PgString[]
lexpr: A_Const | A_Expr | ColumnRef | TypeCast
rexpr: A_Const | A_Expr | ColumnRef | TypeCast
rexpr: A_Const | A_Expr | ColumnRef | TypeCast | List
location: number
}
}
@@ -105,6 +105,12 @@ export type TypeCast = {
}
}
export type List = {
List: {
items: A_Const[]
}
}
export type FuncCall = {
FuncCall: {
funcname: PgString[]