UNPKG

@supabase/sql-to-rest

Version:

[![Tests](https://github.com/supabase-community/sql-to-rest/actions/workflows/tests.yml/badge.svg)](https://github.com/supabase-community/sql-to-rest/actions?query=branch%3Amain) [![Package](https://img.shields.io/npm/v/@supabase/sql-to-rest)](https://www

1 lines 133 kB
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/processor/index.ts","../src/processor/util.ts","../src/processor/aggregate.ts","../src/processor/filter.ts","../src/processor/limit.ts","../src/processor/sort.ts","../src/processor/select.ts","../node_modules/common-tags/src/TemplateTag/TemplateTag.js","../node_modules/common-tags/src/trimResultTransformer/trimResultTransformer.js","../node_modules/common-tags/src/stripIndentTransformer/stripIndentTransformer.js","../node_modules/common-tags/src/replaceResultTransformer/replaceResultTransformer.js","../node_modules/common-tags/src/replaceSubstitutionTransformer/replaceSubstitutionTransformer.js","../node_modules/common-tags/src/inlineArrayTransformer/inlineArrayTransformer.js","../node_modules/common-tags/src/splitStringTransformer/splitStringTransformer.js","../node_modules/common-tags/src/removeNonPrintingValuesTransformer/removeNonPrintingValuesTransformer.js","../node_modules/common-tags/src/commaLists/commaLists.js","../node_modules/common-tags/src/commaListsAnd/commaListsAnd.js","../node_modules/common-tags/src/commaListsOr/commaListsOr.js","../node_modules/common-tags/src/html/html.js","../node_modules/common-tags/src/safeHtml/safeHtml.js","../node_modules/common-tags/src/oneLine/oneLine.js","../node_modules/common-tags/src/oneLineTrim/oneLineTrim.js","../node_modules/common-tags/src/oneLineCommaLists/oneLineCommaLists.js","../node_modules/common-tags/src/oneLineCommaListsOr/oneLineCommaListsOr.js","../node_modules/common-tags/src/oneLineCommaListsAnd/oneLineCommaListsAnd.js","../node_modules/common-tags/src/inlineLists/inlineLists.js","../node_modules/common-tags/src/oneLineInlineLists/oneLineInlineLists.js","../node_modules/common-tags/src/stripIndent/stripIndent.js","../node_modules/common-tags/src/stripIndents/stripIndents.js","../src/renderers/util.ts","../src/renderers/http.ts","../src/renderers/supabase-js.ts"],"sourcesContent":["export * from './errors.js'\nexport * from './processor/index.js'\nexport * from './renderers/http.js'\nexport * from './renderers/supabase-js.js'\n","export class ParsingError extends Error {\n override name = 'ParsingError'\n\n constructor(\n message: string,\n public hint?: string\n ) {\n super(sentenceCase(message))\n }\n}\n\nexport class UnimplementedError extends Error {\n override name = 'UnimplementedError'\n}\n\nexport class UnsupportedError extends Error {\n override name = 'UnsupportedError'\n\n constructor(\n message: string,\n public hint?: string\n ) {\n super(message)\n }\n}\n\nexport class RenderError extends Error {\n override name = 'RenderError'\n\n constructor(\n message: string,\n public renderer: 'http' | 'supabase-js'\n ) {\n super(message)\n }\n}\n\nexport function sentenceCase(value: string) {\n if (typeof value !== 'string') {\n throw new TypeError('Expected a string')\n }\n\n if (value.length === 0) {\n return value\n }\n\n return value[0]!.toUpperCase() + value.slice(1)\n}\n\n/**\n * Returns hints for common parsing errors.\n */\nexport function getParsingErrorHint(message: string) {\n switch (message) {\n case 'syntax error at or near \"from\"':\n return 'Did you leave a trailing comma in the select target list?'\n case 'syntax error at or near \"where\"':\n return 'Do you have an incomplete join in the FROM clause?'\n default:\n undefined\n }\n}\n","import { PgParser, unwrapParseResult } from '@supabase/pg-parser'\nimport type { RawStmt } from '@supabase/pg-parser/17/types'\nimport {\n ParsingError,\n UnimplementedError,\n UnsupportedError,\n getParsingErrorHint,\n} from '../errors.js'\nimport { processSelectStatement } from './select.js'\nimport type { Statement } from './types.js'\n\nexport { supportedAggregateFunctions } from './select.js'\nexport * from './types.js'\nexport { everyTarget, flattenTargets, someFilter, someTarget } from './util.js'\n\nconst parser = new PgParser()\n\n/**\n * Coverts SQL into a PostgREST-compatible `Statement`.\n *\n * Expects SQL to contain only one statement.\n *\n * @returns An intermediate `Statement` object that\n * can be rendered to various targets (HTTP, supabase-js, etc).\n */\nexport async function processSql(sql: string): Promise<Statement> {\n try {\n const result = await unwrapParseResult(parser.parse(sql))\n\n if (!result.stmts || result.stmts.length === 0) {\n throw new UnsupportedError('Expected a statement, but received none')\n }\n\n if (result.stmts.length > 1) {\n throw new UnsupportedError('Expected a single statement, but received multiple')\n }\n\n const [statement] = result.stmts.map((stmt) => {\n if (!stmt) {\n throw new UnsupportedError('Expected a statement, but received an empty one')\n }\n\n return processStatement(stmt)\n })\n\n return statement!\n } catch (err) {\n if (err instanceof Error && 'cursorPosition' in err) {\n const hint = getParsingErrorHint(err.message)\n const parsingError = new ParsingError(err.message, hint)\n\n Object.assign(parsingError, err)\n throw parsingError\n } else {\n throw err\n }\n }\n}\n\n/**\n * Converts a pg-query `Stmt` into a PostgREST-compatible `Statement`.\n */\nfunction processStatement({ stmt }: RawStmt): Statement {\n if (!stmt) {\n throw new UnsupportedError('Expected a statement, but received an empty one')\n }\n\n if ('SelectStmt' in stmt) {\n return processSelectStatement(stmt.SelectStmt)\n } else if ('InsertStmt' in stmt) {\n throw new UnimplementedError(`Insert statements are not yet implemented by the translator`)\n } else if ('UpdateStmt' in stmt) {\n throw new UnimplementedError(`Update statements are not yet implemented by the translator`)\n } else if ('DeleteStmt' in stmt) {\n throw new UnimplementedError(`Delete statements are not yet implemented by the translator`)\n } else if ('ExplainStmt' in stmt) {\n throw new UnimplementedError(`Explain statements are not yet implemented by the translator`)\n } else {\n const [stmtType] = Object.keys(stmt)\n if (!stmtType) {\n throw new UnsupportedError('Expected a statement, but received an empty one')\n }\n const statementType = stmtType.replace(/Stmt$/, '')\n throw new UnsupportedError(`${statementType} statements are not supported`)\n }\n}\n","import type { A_Const, A_Expr, Node, String } from '@supabase/pg-parser/17/types'\nimport { UnsupportedError } from '../errors.js'\nimport type {\n AggregateTarget,\n ColumnFilter,\n ColumnTarget,\n EmbeddedTarget,\n Filter,\n Relations,\n Target,\n} from './types.js'\n\nexport function processJsonTarget(expression: A_Expr, relations: Relations): ColumnTarget {\n if (!expression.name || expression.name.length === 0) {\n throw new UnsupportedError('JSON operator must have a name')\n }\n\n if (expression.name.length > 1) {\n throw new UnsupportedError('Only one operator name supported per expression')\n }\n\n const [name] = expression.name\n\n if (!('String' in name!)) {\n throw new UnsupportedError('JSON operator name must be a string')\n }\n\n const operator = name.String.sval\n\n if (!operator) {\n throw new UnsupportedError('JSON operator name cannot be empty')\n }\n\n if (!['->', '->>'].includes(operator)) {\n throw new UnsupportedError(`Invalid JSON operator`)\n }\n\n let cast: string | undefined = undefined\n let left: string | number\n let right: string | number\n\n if (!expression.lexpr) {\n throw new UnsupportedError('JSON path must have a left expression')\n }\n\n if ('A_Const' in expression.lexpr) {\n // JSON path cannot contain a float\n if ('fval' in expression.lexpr.A_Const) {\n throw new UnsupportedError('Invalid JSON path')\n }\n left = parseConstant(expression.lexpr.A_Const)\n } else if ('A_Expr' in expression.lexpr) {\n const { column } = processJsonTarget(expression.lexpr.A_Expr, relations)\n left = column\n } else if ('ColumnRef' in expression.lexpr) {\n if (!expression.lexpr.ColumnRef.fields) {\n throw new UnsupportedError('JSON path must have a column reference')\n }\n left = renderFields(expression.lexpr.ColumnRef.fields, relations)\n } else {\n throw new UnsupportedError('Invalid JSON path')\n }\n\n if (!expression.rexpr || !expression.rexpr) {\n throw new UnsupportedError('JSON path must have a right expression')\n }\n\n if ('A_Const' in expression.rexpr) {\n // JSON path cannot contain a float\n if ('fval' in expression.rexpr.A_Const) {\n throw new UnsupportedError('Invalid JSON path')\n }\n right = parseConstant(expression.rexpr.A_Const)\n } else if ('TypeCast' in expression.rexpr) {\n if (!expression.rexpr.TypeCast.typeName?.names) {\n throw new UnsupportedError('Type cast must have a name')\n }\n cast = renderDataType(\n expression.rexpr.TypeCast.typeName.names.map((n) => {\n if (!('String' in n)) {\n throw new UnsupportedError('Type cast name must be a string')\n }\n return n.String\n })\n )\n\n if (!expression.rexpr.TypeCast.arg) {\n throw new UnsupportedError('Type cast must have an argument')\n }\n\n if ('A_Const' in expression.rexpr.TypeCast.arg) {\n if ('sval' in expression.rexpr.TypeCast.arg.A_Const) {\n if (!expression.rexpr.TypeCast.arg.A_Const.sval?.sval) {\n throw new UnsupportedError('Type cast argument cannot be empty')\n }\n right = expression.rexpr.TypeCast.arg.A_Const.sval.sval\n } else {\n throw new UnsupportedError('Invalid JSON path')\n }\n } else {\n throw new UnsupportedError('Invalid JSON path')\n }\n } else {\n throw new UnsupportedError('Invalid JSON path')\n }\n\n return {\n type: 'column-target',\n column: `${left}${operator}${right}`,\n cast,\n }\n}\n\nexport function renderFields(\n fields: Node[],\n relations: Relations,\n syntax: 'dot' | 'parenthesis' = 'dot'\n): string {\n // Get qualified column name segments, eg. `author.name` -> ['author', 'name']\n const nameSegments = fields.map((field) => {\n if ('String' in field) {\n return field.String.sval\n } else if ('A_Star' in field) {\n return '*'\n } else {\n const [internalType] = Object.keys(field)\n throw new UnsupportedError(`Unsupported internal type '${internalType}' for data type names`)\n }\n })\n\n // Relation and column names are last two parts of the qualified name\n const [relationOrAliasName] = nameSegments.slice(-2, -1)\n const [columnName] = nameSegments.slice(-1)\n\n const joinedRelation = relations.joined.find(\n (t) => (t.alias ?? t.relation) === relationOrAliasName\n )\n\n // If the column is prefixed with the primary relation, strip the prefix\n if (!relationOrAliasName || relationOrAliasName === relations.primary.reference) {\n if (!columnName) {\n throw new UnsupportedError('Column name cannot be empty')\n }\n return columnName\n }\n // If it's prefixed with a joined relation in the FROM clause, keep the relation prefix\n else if (joinedRelation) {\n // Joined relations that are spread don't support aliases, so we will\n // convert the alias back to the original relation name in this case\n const joinedRelationName = joinedRelation.flatten\n ? joinedRelation.relation\n : relationOrAliasName\n\n if (syntax === 'dot') {\n return [joinedRelationName, columnName].join('.')\n } else if (syntax === 'parenthesis') {\n return `${joinedRelationName}(${columnName})`\n } else {\n throw new Error(`Unknown render syntax '${syntax}'`)\n }\n }\n // If it's prefixed with an unknown relation, throw an error\n else {\n const qualifiedName = [relationOrAliasName, columnName].join('.')\n\n throw new UnsupportedError(\n `Found foreign column '${qualifiedName}' without a join to that relation`,\n 'Did you forget to join that relation or alias it to something else?'\n )\n }\n}\n\nexport function renderDataType(names: String[]) {\n const [first, ...rest] = names\n\n if (!first) {\n throw new UnsupportedError('Data type must have a name')\n }\n\n if (first.sval === 'pg_catalog' && rest.length === 1) {\n const [name] = rest\n\n if (!name) {\n throw new UnsupportedError('Data type must have a name')\n }\n\n // The PG parser converts some data types, eg. int -> pg_catalog.int4\n // so we'll map those back\n switch (name.sval) {\n case 'int2':\n return 'smallint'\n case 'int4':\n return 'int'\n case 'int8':\n return 'bigint'\n case 'float8':\n return 'float'\n default:\n return name.sval\n }\n } else if (rest.length > 0) {\n throw new UnsupportedError(\n `Casts can only reference data types by their unqualified name (not schema-qualified)`\n )\n } else {\n return first.sval\n }\n}\n\nexport function parseConstant(constant: A_Const) {\n if ('sval' in constant) {\n if (constant.sval?.sval === undefined) {\n throw new UnsupportedError('Constant value cannot be empty')\n }\n return constant.sval.sval\n } else if ('ival' in constant) {\n if (constant.ival === undefined) {\n throw new UnsupportedError('Constant value cannot be undefined')\n }\n // The PG parser turns 0 into undefined, so convert it back here\n return constant.ival.ival ?? 0\n } else if ('fval' in constant) {\n if (constant.fval?.fval === undefined) {\n throw new UnsupportedError('Constant value cannot be undefined')\n }\n return parseFloat(constant.fval.fval)\n } else {\n throw new UnsupportedError(`Constant values must be a string, integer, or float`)\n }\n}\n\n/**\n * Recursively flattens PostgREST embedded targets.\n */\nexport function flattenTargets(targets: Target[]): Target[] {\n return targets.flatMap((target) => {\n const { type } = target\n if (type === 'column-target' || type === 'aggregate-target') {\n return target\n } else if (type === 'embedded-target') {\n return [target, ...flattenTargets(target.targets)]\n } else {\n throw new UnsupportedError(`Unknown target type '${type}'`)\n }\n })\n}\n\n/**\n * Recursively iterates through PostgREST filters and checks if the predicate\n * matches any of them (ie. `some()`).\n */\nexport function someFilter(filter: Filter, predicate: (filter: ColumnFilter) => boolean): boolean {\n const { type } = filter\n\n if (type === 'column') {\n return predicate(filter)\n } else if (type === 'logical') {\n return filter.values.some((f) => someFilter(f, predicate))\n } else {\n throw new UnsupportedError(`Unknown filter type '${type}'`)\n }\n}\n\n/**\n * Recursively iterates through a PostgREST target list and checks if the predicate\n * matches every one of them (ie. `some()`).\n */\nexport function everyTarget(\n targets: Target[],\n predicate: (target: ColumnTarget | AggregateTarget, parent?: EmbeddedTarget) => boolean,\n parent?: EmbeddedTarget\n): boolean {\n return targets.every((target) => {\n const { type } = target\n\n if (type === 'column-target' || type === 'aggregate-target') {\n return predicate(target, parent)\n } else if (type === 'embedded-target') {\n return everyTarget(target.targets, predicate, target)\n } else {\n throw new UnsupportedError(`Unknown target type '${type}'`)\n }\n })\n}\n\n/**\n * Recursively iterates through a PostgREST target list and checks if the predicate\n * matches any of them (ie. `some()`).\n */\nexport function someTarget(\n targets: Target[],\n predicate: (target: ColumnTarget | AggregateTarget, parent?: EmbeddedTarget) => boolean,\n parent?: EmbeddedTarget\n): boolean {\n return targets.some((target) => {\n const { type } = target\n\n if (type === 'column-target' || type === 'aggregate-target') {\n return predicate(target, parent)\n } else if (type === 'embedded-target') {\n return someTarget(target.targets, predicate, target)\n } else {\n throw new UnsupportedError(`Unknown target type '${type}'`)\n }\n })\n}\n","import type { ColumnRef } from '@supabase/pg-parser/17/types'\nimport { UnsupportedError } from '../errors.js'\nimport type { Relations, Target } from './types.js'\nimport { everyTarget, renderFields, someTarget } from './util.js'\n\nexport function validateGroupClause(\n groupClause: ColumnRef[],\n targets: Target[],\n relations: Relations\n) {\n const groupByColumns = groupClause.map((columnRef) => {\n if (!columnRef.fields) {\n throw new UnsupportedError('Group by clause must contain at least one column')\n }\n return renderFields(columnRef.fields, relations) ?? []\n })\n\n if (\n !groupByColumns.every((column) =>\n someTarget(targets, (target, parent) => {\n // The `count()` special case aggregate has no column attached\n if (!('column' in target)) {\n return false\n }\n\n const path = parent\n ? // joined columns have to be prefixed with their relation\n [parent.alias && !parent.flatten ? parent.alias : parent.relation, target.column]\n : // top-level columns will have no prefix\n [target.column]\n\n const qualifiedName = path.join('.')\n return qualifiedName === column\n })\n )\n ) {\n throw new UnsupportedError(`Every group by column must also exist as a select target`)\n }\n\n if (\n someTarget(targets, (target) => target.type === 'aggregate-target') &&\n !everyTarget(targets, (target, parent) => {\n if (target.type === 'aggregate-target') {\n return true\n }\n\n const path = parent\n ? // joined columns have to be prefixed with their relation\n [parent.alias && !parent.flatten ? parent.alias : parent.relation, target.column]\n : // top-level columns will have no prefix\n [target.column]\n\n const qualifiedName = path.join('.')\n\n return groupByColumns.some((column) => qualifiedName === column)\n })\n ) {\n throw new UnsupportedError(\n `Every non-aggregate select target must also exist in a group by clause`\n )\n }\n\n if (\n groupByColumns.length > 0 &&\n !someTarget(targets, (target) => target.type === 'aggregate-target')\n ) {\n throw new UnsupportedError(\n `There must be at least one aggregate function in the select target list when using group by`\n )\n }\n}\n","import type { A_Expr_Kind, Node } from '@supabase/pg-parser/17/types'\nimport { UnsupportedError } from '../errors.js'\nimport type { ColumnFilter, Filter, Relations } from './types.js'\nimport { parseConstant, processJsonTarget, renderFields } from './util.js'\n\nexport function processWhereClause(expression: Node, relations: Relations): Filter {\n if ('A_Expr' in expression) {\n let column: string\n\n if (!expression.A_Expr.name || expression.A_Expr.name.length > 1) {\n throw new UnsupportedError('Only one operator name supported per expression')\n }\n\n const kind = expression.A_Expr.kind\n\n if (!kind) {\n throw new UnsupportedError('WHERE clause must have an operator kind')\n }\n\n const [name] = expression.A_Expr.name\n\n if (!name) {\n throw new UnsupportedError('WHERE clause must have an operator name')\n }\n\n if (!('String' in name)) {\n throw new UnsupportedError('WHERE clause operator name must be a string')\n }\n\n if (!name.String.sval) {\n throw new UnsupportedError('WHERE clause operator name cannot be empty')\n }\n\n const operatorSymbol = name.String.sval.toLowerCase()\n const operator = mapOperatorSymbol(kind, operatorSymbol)\n\n if (!expression.A_Expr.lexpr) {\n throw new UnsupportedError('Left side of WHERE clause must be a column or expression')\n }\n\n if ('A_Expr' in expression.A_Expr.lexpr) {\n try {\n const target = processJsonTarget(expression.A_Expr.lexpr.A_Expr, relations)\n column = target.column\n } catch (err) {\n throw new UnsupportedError(`Left side of WHERE clause must be a column`)\n }\n } else if ('ColumnRef' in expression.A_Expr.lexpr) {\n const { fields } = expression.A_Expr.lexpr.ColumnRef\n if (!fields || fields.length === 0) {\n throw new UnsupportedError(`Left side of WHERE clause must reference a column`)\n }\n column = renderFields(fields, relations)\n } else if ('TypeCast' in expression.A_Expr.lexpr) {\n throw new UnsupportedError('Casting is not supported in the WHERE clause')\n } else if ('FuncCall' in expression.A_Expr.lexpr) {\n if (!expression.A_Expr.lexpr.FuncCall.funcname) {\n throw new UnsupportedError(`Left side of WHERE clause must reference a column`)\n }\n const functionName = renderFields(expression.A_Expr.lexpr.FuncCall.funcname, relations)\n\n // Only 'to_tsvector' function is supported on left side of WHERE clause (when using FTS `@@` operator))\n if (operator === 'fts') {\n if (functionName === 'to_tsvector') {\n if (\n !expression.A_Expr.lexpr.FuncCall.args ||\n expression.A_Expr.lexpr.FuncCall.args.length !== 1\n ) {\n throw new UnsupportedError(`${functionName} requires 1 column argument`)\n }\n\n // We grab the column passed to `to_tsvector` and discard the `to_tsvector` function\n // We can do this because Postgres will implicitly wrap text columns in `to_tsvector` at query time\n const [arg] = expression.A_Expr.lexpr.FuncCall.args\n\n if (!arg) {\n throw new UnsupportedError(`${functionName} requires a column argument`)\n }\n\n if ('A_Expr' in arg) {\n try {\n const target = processJsonTarget(arg.A_Expr, relations)\n column = target.column\n } catch (err) {\n throw new UnsupportedError(`${functionName} requires a column argument`)\n }\n } else if ('ColumnRef' in arg) {\n const { fields } = arg.ColumnRef\n if (!fields) {\n throw new UnsupportedError(`${functionName} requires a column argument`)\n }\n column = renderFields(fields, relations)\n } else if ('TypeCast' in arg) {\n throw new UnsupportedError('Casting is not supported in the WHERE clause')\n } else {\n throw new UnsupportedError(`${functionName} requires a column argument`)\n }\n } else {\n throw new UnsupportedError(\n `Only 'to_tsvector' function allowed on left side of text search operator`\n )\n }\n } else {\n throw new UnsupportedError(`Left side of WHERE clause must be a column`)\n }\n } else {\n throw new UnsupportedError(`Left side of WHERE clause must be a column`)\n }\n\n if (\n operator === 'eq' ||\n operator === 'neq' ||\n operator === 'gt' ||\n operator === 'gte' ||\n operator === 'lt' ||\n operator === 'lte'\n ) {\n if (!expression.A_Expr.rexpr) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be present`\n )\n }\n\n if (!('A_Const' in expression.A_Expr.rexpr)) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be a constant`,\n `Did you forget to wrap your value in single quotes?`\n )\n }\n\n const value = parseConstant(expression.A_Expr.rexpr.A_Const)\n return {\n type: 'column',\n column,\n operator,\n negate: false,\n value,\n }\n }\n // Between is not supported by PostgREST, but we can generate the equivalent using '>=' and '<='\n else if (\n operator === 'between' ||\n operator === 'between symmetric' ||\n operator === 'not between' ||\n operator === 'not between symmetric'\n ) {\n if (!expression.A_Expr.rexpr) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be present`\n )\n }\n\n if (\n !('List' in expression.A_Expr.rexpr) ||\n expression.A_Expr.rexpr.List.items?.length !== 2\n ) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must contain two constants`\n )\n }\n\n let [leftValue, rightValue] = expression.A_Expr.rexpr.List.items.map((item) => {\n if (!('A_Const' in item)) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must contain two constants`\n )\n }\n return parseConstant(item.A_Const)\n })\n\n // 'between symmetric' doesn't care which argument comes first order-wise,\n // ie. it auto swaps the arguments if the left value is greater than the right value\n if (operator.includes('symmetric')) {\n // We can only implement the symmetric logic if the values are numbers\n // If they're strings, they could be dates, text columns, etc which we can't sort here\n if (typeof leftValue !== 'number' || typeof rightValue !== 'number') {\n throw new UnsupportedError(`BETWEEN SYMMETRIC is only supported with number values`)\n }\n\n // If the left value is greater than the right, swap them\n if (leftValue > rightValue) {\n const temp = rightValue\n rightValue = leftValue\n leftValue = temp\n }\n }\n\n if (!leftValue) {\n throw new UnsupportedError(\n `Left side of WHERE clause '${operatorSymbol}' expression must be a constant`\n )\n }\n\n const leftFilter: ColumnFilter = {\n type: 'column',\n column,\n operator: 'gte',\n negate: false,\n value: leftValue,\n }\n\n if (!rightValue) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be a constant`\n )\n }\n\n const rightFilter: ColumnFilter = {\n type: 'column',\n column,\n operator: 'lte',\n negate: false,\n value: rightValue,\n }\n\n return {\n type: 'logical',\n operator: 'and',\n negate: operator.includes('not'),\n values: [leftFilter, rightFilter],\n }\n } else if (\n operator === 'like' ||\n operator === 'ilike' ||\n operator === 'match' ||\n operator === 'imatch'\n ) {\n if (!expression.A_Expr.rexpr) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be present`\n )\n }\n\n if (\n !('A_Const' in expression.A_Expr.rexpr) ||\n !('sval' in expression.A_Expr.rexpr.A_Const) ||\n !expression.A_Expr.rexpr.A_Const.sval?.sval\n ) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operator}' expression must be a string constant`\n )\n }\n\n const value = expression.A_Expr.rexpr.A_Const.sval.sval\n\n return {\n type: 'column',\n column,\n operator,\n negate: false,\n value,\n }\n } else if (operator === 'in') {\n if (!expression.A_Expr.rexpr) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be present`\n )\n }\n\n if (\n !('List' in expression.A_Expr.rexpr) ||\n !expression.A_Expr.rexpr.List.items?.every((item) => 'A_Const' in item)\n ) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operator}' expression must be a list of constants`\n )\n }\n\n const value = expression.A_Expr.rexpr.List.items.map((item) => parseConstant(item.A_Const))\n\n return {\n type: 'column',\n column,\n operator,\n negate: false,\n value,\n }\n } else if (operator === 'fts') {\n const supportedTextSearchFunctions = [\n 'to_tsquery',\n 'plainto_tsquery',\n 'phraseto_tsquery',\n 'websearch_to_tsquery',\n ]\n\n if (!expression.A_Expr.rexpr) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be present`\n )\n }\n\n if (!('FuncCall' in expression.A_Expr.rexpr) || !expression.A_Expr.rexpr.FuncCall.funcname) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be one of these functions: ${supportedTextSearchFunctions.join(', ')}`\n )\n }\n\n const functionName = renderFields(expression.A_Expr.rexpr.FuncCall.funcname, relations)\n\n if (!supportedTextSearchFunctions.includes(functionName)) {\n throw new UnsupportedError(\n `Right side of WHERE clause '${operatorSymbol}' expression must be one of these functions: ${supportedTextSearchFunctions.join(', ')}`\n )\n }\n\n if (\n !expression.A_Expr.rexpr.FuncCall.args ||\n expression.A_Expr.rexpr.FuncCall.args.length === 0 ||\n expression.A_Expr.rexpr.FuncCall.args.length > 2\n ) {\n throw new UnsupportedError(`${functionName} requires 1 or 2 arguments`)\n }\n\n const args = expression.A_Expr.rexpr.FuncCall.args.map((arg) => {\n if (!('A_Const' in arg) || !arg.A_Const.sval?.sval) {\n throw new UnsupportedError(`${functionName} only accepts text arguments`)\n }\n\n return arg.A_Const.sval.sval\n })\n\n // config (eg. 'english') is the first argument if passed\n const [config] = args.slice(-2, -1)\n\n // query is always the last argument\n const [query] = args.slice(-1)\n\n if (!query) {\n throw new UnsupportedError(`${functionName} requires a query argument`)\n }\n\n // Adjust operator based on FTS function\n const operator = mapTextSearchFunction(functionName)\n\n return {\n type: 'column',\n column,\n operator,\n config,\n value: query,\n negate: false,\n }\n } else {\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n } else if ('NullTest' in expression) {\n if (!expression.NullTest.arg || !('ColumnRef' in expression.NullTest.arg)) {\n throw new UnsupportedError(`NullTest expression must have an argument of type ColumnRef`)\n }\n\n const { fields } = expression.NullTest.arg.ColumnRef\n\n if (!fields) {\n throw new UnsupportedError(`NullTest expression must reference a column`)\n }\n\n const column = renderFields(fields, relations)\n const negate = expression.NullTest.nulltesttype === 'IS_NOT_NULL'\n const operator = 'is'\n const value = null\n\n return {\n type: 'column',\n column,\n operator,\n negate,\n value,\n }\n } else if ('BoolExpr' in expression) {\n let operator: 'and' | 'or' | 'not'\n\n if (expression.BoolExpr.boolop === 'AND_EXPR') {\n operator = 'and'\n } else if (expression.BoolExpr.boolop === 'OR_EXPR') {\n operator = 'or'\n } else if (expression.BoolExpr.boolop === 'NOT_EXPR') {\n operator = 'not'\n } else {\n throw new UnsupportedError(`Unknown boolop '${expression.BoolExpr.boolop}'`)\n }\n\n if (!expression.BoolExpr.args) {\n throw new UnsupportedError(`BoolExpr must have arguments`)\n }\n\n const values = expression.BoolExpr.args.map((arg) => processWhereClause(arg, relations))\n\n // The 'not' operator is special - instead of wrapping its child,\n // we just return the child directly and set negate=true on it.\n if (operator === 'not') {\n if (values.length > 1) {\n throw new UnsupportedError(\n `NOT expressions must have only 1 child, but received ${values.length} children`\n )\n }\n\n const [filter] = values\n if (!filter) {\n throw new UnsupportedError(`NOT expression must have a child filter`)\n }\n\n filter.negate = true\n return filter\n }\n\n return {\n type: 'logical',\n operator,\n negate: false,\n values,\n }\n } else {\n throw new UnsupportedError(`The WHERE clause must contain an expression`)\n }\n}\n\nfunction mapOperatorSymbol(kind: A_Expr_Kind, operatorSymbol: string) {\n switch (kind) {\n case 'AEXPR_OP': {\n switch (operatorSymbol) {\n case '=':\n return 'eq'\n case '<>':\n return 'neq'\n case '>':\n return 'gt'\n case '>=':\n return 'gte'\n case '<':\n return 'lt'\n case '<=':\n return 'lte'\n case '~':\n return 'match'\n case '~*':\n return 'imatch'\n case '@@':\n // 'fts' isn't necessarily the final operator (there is also plfts, phfts, wfts)\n // we adjust this downstream based on the tsquery function used\n return 'fts'\n default:\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n }\n case 'AEXPR_BETWEEN':\n case 'AEXPR_BETWEEN_SYM':\n case 'AEXPR_NOT_BETWEEN':\n case 'AEXPR_NOT_BETWEEN_SYM': {\n switch (operatorSymbol) {\n case 'between':\n return 'between'\n case 'between symmetric':\n return 'between symmetric'\n case 'not between':\n return 'not between'\n case 'not between symmetric':\n return 'not between symmetric'\n default:\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n }\n case 'AEXPR_LIKE': {\n switch (operatorSymbol) {\n case '~~':\n return 'like'\n default:\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n }\n case 'AEXPR_ILIKE': {\n switch (operatorSymbol) {\n case '~~*':\n return 'ilike'\n default:\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n }\n case 'AEXPR_IN': {\n switch (operatorSymbol) {\n case '=':\n return 'in'\n default:\n throw new UnsupportedError(`Unsupported operator '${operatorSymbol}'`)\n }\n }\n }\n}\n\n/**\n * Maps text search query functions to the respective PostgREST operator.\n */\nfunction mapTextSearchFunction(functionName: string) {\n switch (functionName) {\n case 'to_tsquery':\n return 'fts'\n case 'plainto_tsquery':\n return 'plfts'\n case 'phraseto_tsquery':\n return 'phfts'\n case 'websearch_to_tsquery':\n return 'wfts'\n default:\n throw new UnsupportedError(`Function '${functionName}' not supported for full-text search`)\n }\n}\n","import type { SelectStmt } from '@supabase/pg-parser/17/types'\nimport { UnsupportedError } from '../errors.js'\nimport type { Limit } from './types.js'\n\nexport function processLimit(selectStmt: SelectStmt): Limit | undefined {\n let count: number | undefined = undefined\n let offset: number | undefined = undefined\n\n if (selectStmt.limitCount) {\n if (!('A_Const' in selectStmt.limitCount)) {\n throw new UnsupportedError(`Limit count must be an A_Const`)\n }\n\n if (!('ival' in selectStmt.limitCount.A_Const)) {\n throw new UnsupportedError(`Limit count must be an integer`)\n }\n\n if (!selectStmt.limitCount.A_Const.ival) {\n throw new UnsupportedError(`Limit count must have an integer value`)\n }\n\n count = selectStmt.limitCount.A_Const.ival.ival\n }\n\n if (selectStmt.limitOffset) {\n if (!('A_Const' in selectStmt.limitOffset)) {\n throw new UnsupportedError(`Limit offset must be an A_Const`)\n }\n\n if (!('ival' in selectStmt.limitOffset.A_Const)) {\n throw new UnsupportedError(`Limit offset must be an integer`)\n }\n\n if (!selectStmt.limitOffset.A_Const.ival) {\n throw new UnsupportedError(`Limit offset must have an integer value`)\n }\n\n offset = selectStmt.limitOffset.A_Const.ival.ival\n }\n\n if (count === undefined && offset === undefined) {\n return undefined\n }\n\n return {\n count,\n offset,\n }\n}\n","import type { SortBy } from '@supabase/pg-parser/17/types'\nimport { UnsupportedError } from '../errors.js'\nimport type { Relations, Sort } from './types.js'\nimport { processJsonTarget, renderFields } from './util.js'\n\nexport function processSortClause(sorts: SortBy[], relations: Relations): Sort[] {\n return sorts.map((sortBy) => {\n let column: string\n\n if (!sortBy.node) {\n throw new UnsupportedError(`ORDER BY clause must reference a column`)\n }\n\n if ('A_Expr' in sortBy.node) {\n try {\n const target = processJsonTarget(sortBy.node.A_Expr, relations)\n column = target.column\n } catch (err) {\n throw new UnsupportedError(`ORDER BY clause must reference a column`)\n }\n } else if ('ColumnRef' in sortBy.node) {\n const { fields } = sortBy.node.ColumnRef\n if (!fields) {\n throw new UnsupportedError(`ORDER BY clause must reference a column`)\n }\n column = renderFields(fields, relations, 'parenthesis')\n } else if ('TypeCast' in sortBy.node) {\n throw new UnsupportedError('Casting is not supported in the ORDER BY clause')\n } else {\n throw new UnsupportedError(`ORDER BY clause must reference a column`)\n }\n\n if (!sortBy.sortby_dir) {\n throw new UnsupportedError(`ORDER BY clause must specify a direction`)\n }\n\n const direction = mapSortByDirection(sortBy.sortby_dir)\n\n if (!sortBy.sortby_nulls) {\n throw new UnsupportedError(`ORDER BY clause must specify nulls handling`)\n }\n\n const nulls = mapSortByNulls(sortBy.sortby_nulls)\n\n return {\n column,\n direction,\n nulls,\n }\n })\n}\n\nfunction mapSortByDirection(direction: string) {\n switch (direction) {\n case 'SORTBY_ASC':\n return 'asc'\n case 'SORTBY_DESC':\n return 'desc'\n case 'SORTBY_DEFAULT':\n return undefined\n default:\n throw new UnsupportedError(`Unknown sort by direction '${direction}'`)\n }\n}\n\nfunction mapSortByNulls(nulls: string) {\n switch (nulls) {\n case 'SORTBY_NULLS_FIRST':\n return 'first'\n case 'SORTBY_NULLS_LAST':\n return 'last'\n case 'SORTBY_NULLS_DEFAULT':\n return undefined\n default:\n throw new UnsupportedError(`Unknown sort by nulls '${nulls}'`)\n }\n}\n","import type {\n A_Expr,\n ColumnRef,\n FuncCall,\n Node,\n ResTarget,\n SelectStmt,\n String,\n TypeCast,\n} from '@supabase/pg-parser/17/types'\nimport { UnsupportedError } from '../errors.js'\nimport { validateGroupClause } from './aggregate.js'\nimport { processWhereClause } from './filter.js'\nimport { processLimit } from './limit.js'\nimport { processSortClause } from './sort.js'\nimport type {\n AggregateTarget,\n ColumnTarget,\n EmbeddedTarget,\n JoinedColumn,\n Relations,\n Select,\n Target,\n} from './types.js'\nimport { processJsonTarget, renderDataType, renderFields } from './util.js'\n\nexport const supportedAggregateFunctions = ['avg', 'count', 'max', 'min', 'sum']\n\nexport function processSelectStatement(stmt: SelectStmt): Select {\n if (!stmt) {\n throw new UnsupportedError('Expected a statement, but received an empty one')\n }\n\n if (!stmt.fromClause) {\n throw new UnsupportedError('The query must have a from clause')\n }\n\n if (!stmt.targetList) {\n throw new UnsupportedError('The query must have a target list')\n }\n\n if (stmt.fromClause.length > 1) {\n throw new UnsupportedError('Only one FROM source is supported')\n }\n\n if (stmt.withClause) {\n throw new UnsupportedError('CTEs are not supported')\n }\n\n if (stmt.distinctClause) {\n throw new UnsupportedError('SELECT DISTINCT is not supported')\n }\n\n if (stmt.havingClause) {\n throw new UnsupportedError('The HAVING clause is not supported')\n }\n\n const [fromClause] = stmt.fromClause\n\n if (!fromClause) {\n throw new UnsupportedError('The FROM clause must have a relation')\n }\n\n const relations = processFromClause(fromClause)\n\n const from = relations.primary.name\n\n const targetList = stmt.targetList.map((node) => {\n if (!('ResTarget' in node)) {\n throw new UnsupportedError('Target list must contain ResTarget nodes')\n }\n return node.ResTarget\n })\n\n const targets = processTargetList(targetList, relations)\n\n const groupByColumns =\n stmt.groupClause?.map((node) => {\n if (!('ColumnRef' in node)) {\n throw new UnsupportedError('Group by clause must contain column references')\n }\n return node.ColumnRef\n }) ?? []\n\n validateGroupClause(groupByColumns, targets, relations)\n\n const filter = stmt.whereClause ? processWhereClause(stmt.whereClause, relations) : undefined\n\n const sortByColumns =\n stmt.sortClause?.map((sortBy) => {\n if (!('SortBy' in sortBy)) {\n throw new UnsupportedError('Sort clause must contain SortBy nodes')\n }\n return sortBy.SortBy\n }) ?? []\n\n const sorts = processSortClause(sortByColumns, relations)\n\n const limit = processLimit(stmt)\n\n return {\n type: 'select',\n from,\n targets,\n filter,\n sorts,\n limit,\n }\n}\n\nfunction processFromClause(fromClause: Node): Relations {\n if ('RangeVar' in fromClause) {\n if (!fromClause.RangeVar.relname) {\n throw new UnsupportedError('The FROM clause must have a relation name')\n }\n\n return {\n primary: {\n name: fromClause.RangeVar.relname,\n alias: fromClause.RangeVar.alias?.aliasname,\n get reference() {\n return this.alias ?? this.name\n },\n },\n joined: [],\n }\n } else if ('JoinExpr' in fromClause) {\n if (!fromClause.JoinExpr.jointype) {\n throw new UnsupportedError('Join expression must have a join type')\n }\n\n if (!fromClause.JoinExpr.larg || !fromClause.JoinExpr.rarg) {\n throw new UnsupportedError('Join expression must have both left and right relations')\n }\n const joinType = mapJoinType(fromClause.JoinExpr.jointype)\n const { primary, joined } = processFromClause(fromClause.JoinExpr.larg)\n\n if (!('RangeVar' in fromClause.JoinExpr.rarg)) {\n throw new UnsupportedError('Join expression must have a right relation of type RangeVar')\n }\n\n const joinedRelationAlias = fromClause.JoinExpr.rarg.RangeVar.alias?.aliasname\n const joinedRelation = joinedRelationAlias ?? fromClause.JoinExpr.rarg.RangeVar.relname\n\n const existingRelations = [\n primary.reference,\n ...joined.map((t) => t.alias ?? t.relation),\n joinedRelation,\n ]\n\n if (!fromClause.JoinExpr.quals || !('A_Expr' in fromClause.JoinExpr.quals)) {\n throw new UnsupportedError(`Join qualifier must be an expression comparing columns`)\n }\n\n let leftQualifierRelation\n let rightQualifierRelation\n\n const joinQualifierExpression = fromClause.JoinExpr.quals.A_Expr\n\n if (!joinQualifierExpression.lexpr || !('ColumnRef' in joinQualifierExpression.lexpr)) {\n throw new UnsupportedError(`Left side of join qualifier must be a column`)\n }\n\n if (\n !joinQualifierExpression.lexpr.ColumnRef.fields ||\n !joinQualifierExpression.lexpr.ColumnRef.fields.every(\n (field): field is { String: String } => 'String' in field\n )\n ) {\n throw new UnsupportedError(`Left side column of join qualifier must contain String fields`)\n }\n\n const leftColumnFields = joinQualifierExpression.lexpr.ColumnRef.fields.map(\n (field) => field.String.sval\n )\n\n // Relation and column names are last two parts of the qualified name\n const [leftRelationName] = leftColumnFields.slice(-2, -1)\n const [leftColumnName] = leftColumnFields.slice(-1)\n\n if (!leftColumnName) {\n throw new UnsupportedError(`Left side of join qualifier must have a column name`)\n }\n\n if (!leftRelationName) {\n leftQualifierRelation = primary.reference\n } else if (existingRelations.includes(leftRelationName)) {\n leftQualifierRelation = leftRelationName\n } else if (leftRelationName === joinedRelation) {\n leftQualifierRelation = joinedRelation\n } else {\n throw new UnsupportedError(\n `Left side of join qualifier references a different relation (${leftRelationName}) than the join (${existingRelations.join(', ')})`\n )\n }\n\n if (!joinQualifierExpression.rexpr) {\n throw new UnsupportedError(`Join qualifier must have a right side expression`)\n }\n\n if (!('ColumnRef' in joinQualifierExpression.rexpr)) {\n throw new UnsupportedError(`Right side of join qualifier must be a column`)\n }\n\n if (\n !joinQualifierExpression.rexpr.ColumnRef.fields?.every(\n (field): field is { String: String } => 'String' in field\n )\n ) {\n throw new UnsupportedError(`Right side column of join qualifier must contain String fields`)\n }\n\n const rightColumnFields = joinQualifierExpression.rexpr.ColumnRef.fields.map(\n (field) => field.String.sval\n )\n\n // Relation and column names are last two parts of the qualified name\n const [rightRelationName] = rightColumnFields.slice(-2, -1)\n const [rightColumnName] = rightColumnFields.slice(-1)\n\n if (!rightColumnName) {\n throw new UnsupportedError(`Right side of join qualifier must have a column name`)\n }\n\n if (!rightRelationName) {\n rightQualifierRelation = primary.reference\n } else if (existingRelations.includes(rightRelationName)) {\n rightQualifierRelation = rightRelationName\n } else if (rightRelationName === joinedRelation) {\n rightQualifierRelation = joinedRelation\n } else {\n throw new UnsupportedError(\n `Right side of join qualifier references a different relation (${rightRelationName}) than the join (${existingRelations.join(', ')})`\n )\n }\n\n if (rightQualifierRelation === leftQualifierRelation) {\n // TODO: support for recursive relationships\n throw new UnsupportedError(`Join qualifier cannot compare columns from same relation`)\n }\n\n if (rightQualifierRelation !== joinedRelation && leftQualifierRelation !== joinedRelation) {\n throw new UnsupportedError(`Join qualifier must reference a column from the joined table`)\n }\n\n if (!joinQualifierExpression.name) {\n throw new UnsupportedError(`Join qualifier must have an operator`)\n }\n\n const [qualifierOperatorString] = joinQualifierExpression.name\n\n if (!qualifierOperatorString || !('String' in qualifierOperatorString)) {\n throw new UnsupportedError(`Join qualifier operator must be a string`)\n }\n\n if (qualifierOperatorString.String.sval !== '=') {\n throw new UnsupportedError(`Join qualifier operator must be '='`)\n }\n\n let left: JoinedColumn\n let right: JoinedColumn\n\n // If left qualifier referenced the joined relation, swap left and right\n if (rightQualifierRelation === joinedRelation) {\n left = {\n relation: leftQualifierRelation,\n column: leftColumnName,\n }\n right = {\n relation: rightQualifierRelation,\n column: rightColumnName,\n }\n } else {\n right = {\n relation: leftQualifierRelation,\n column: leftColumnName,\n }\n left = {\n relation: rightQualifierRelation,\n column: rightColumnName,\n }\n }\n\n if (!fromClause.JoinExpr.rarg.RangeVar.relname) {\n throw new UnsupportedError('Join expression must have a right relation name')\n }\n\n const embeddedTarget: EmbeddedTarget = {\n type: 'embedded-target',\n relation: fromClause.JoinExpr.rarg.RangeVar.relname,\n alias: fromClause.JoinExpr.rarg.RangeVar.alias?.aliasname,\n joinType,\n targets: [], // these will be filled in later when processing the select target list\n flatten: true,\n joinedColumns: {\n left,\n right,\n },\n }\n\n return {\n primary,\n joined: [...joined, embeddedTarget],\n }\n } else {\n const [fieldType] = Object.keys(fromClause)\n throw new UnsupportedError(`Unsupported FROM clause type '${fieldType}'`)\n }\n}\n\nfunction processTargetList(targetList: ResTarget[], relations: Relations): Target[] {\n // First pass: map each SQL target column to a PostgREST target 1-to-1\n const flattenedColumnTargets: (ColumnTarget | AggregateTarget)[] = targetList.map((resTarget) => {\n if (!resTarget.val) {\n throw new UnsupportedError(`Target list item must have a value`)\n }\n\n const target = processTarget(resTarget.val, relations)\n target.alias = resTarget.name\n\n return target\n })\n\n // Second pass: transfer joined columns to `embeddedTargets`\n const columnTargets = flattenedColumnTargets.filter((target) => {\n // Account for the special case when the aggregate doesn't have a column attached\n // ie. `count()`: should always be applied to the top level relation\n if (target.type === 'aggregate-target' && !('column' in target)) {\n return true\n }\n\n const qualifiedName = target.column.split('.')\n\n // Relation and column names are last two parts of the qualified name\n const [relationName] = qualifiedName.slice(-2, -1)\n const [columnName] = qualifiedName.slice(-1)\n\n // If there is no prefix, this column belongs to the primary relation a