Skip to content

supabase-community/sql-to-rest

Repository files navigation

SQL to PostgREST translator

Tests Package License: MIT

TypeScript library that translates SQL queries to the equivalent PostgREST-compitable HTTP requests and client library code. Works on both browser and server.

What is PostgREST?

PostgREST is a REST API that auto-generates endpoints based on relations in your database (public schema only by default). It uses JWTs and RLS policies to handle authorization.

How can SQL be converted to REST?

The PostgREST API supports a lot of SQL-like features including:

  • Vertical filtering (select only the columns you care about)
  • Horizontal filtering (filter rows by comparing data in columns: =, >, <, in, like, etc)
  • Sorting
  • Limit and offset
  • Resource embeddings (joins to other relations using foreign keys)
  • Aggregate operations (count(), sum(), avg(), min(), max())
  • Nested AND/OR expressions
  • Aliasing and casting
  • JSON columns (selecting, filtering, and sorting)

This library takes the SQL input and translates it to 1-to-1 to the equivalent PostgREST syntax. Any unsupported SQL will throw an error.

Example

The following SQL:

select
  title,
  description
from
  books
where
  description ilike '%cheese%'
order by
  title desc
limit
  5
offset
  10

Will get translated to:

cURL

curl -G http://localhost:54321/rest/v1/books \
  -d "select=title,description" \
  -d "description=ilike.*cheese*" \
  -d "order=title.desc" \
  -d "limit=5" \
  -d "offset=10"

Raw HTTP

GET /rest/v1/books?select=title,description&description=ilike.*cheese*&order=title.desc&limit=5&offset=10 HTTP/1.1
Host: localhost:54321

supabase-js

const { data, error } = await supabase
  .from('books')
  .select(
    `
    title,
    description
    `
  )
  .ilike('description', '%cheese%')
  .order('title', { ascending: false })
  .range(10, 15)

Install

npm i @supabase/sql-to-rest
yarn add @supabase/sql-to-rest

Usage

Note: This library is pre-1.0, so expect slight API changes over time.

import { processSql, renderHttp, formatCurl } from '@supabase/sql-to-rest'

// Process SQL into intermediate PostgREST AST
const statement = await processSql(`
  select
    *
  from
    books
`)

// Render the AST into an HTTP request
const httpRequest = await renderHttp(statement)

// Format the HTTP request as a cURL command (requires base URL)
const curlCommand = formatCurl('http://localhost:54321/rest/v1', httpRequest)

console.log(curlCommand)
// curl http://localhost:54321/rest/v1/books

// Or use it directly
const response = await fetch(`http://localhost:54321/rest/v1${httpRequest.fullPath}`, {
  method: httpRequest.method,
})

processSql()

Takes a SQL string and converts it into a PostgREST abstract syntax tree (AST) called a Statement. This is an intermediate object that can later be rendered to your language/protocol of choice.

import { processSql } from '@supabase/sql-to-rest'

const statement = await processSql(`
  select
    *
  from
    books
`)

Outputs a Promise<Statement>:

{
  type: 'select',
  from: 'books',
  targets: [
    {
      type: 'column-target',
      column: '*',
      alias: undefined,
    },
  ],
  filter: undefined,
  sorts: [],
  limit: undefined
}

renderHttp()

Takes the intermediate Statement and renders it as an HTTP request.

import { processSql, renderHttp } from '@supabase/sql-to-rest'

const statement = await processSql(`
  select
    *
  from
    books
`)

const httpRequest = await renderHttp(statement)

Outputs a Promise<HttpRequest>:

{
  method: 'GET',
  path: '/books',
  params: URLSearchParams {},
  fullPath: [Getter] // combines path with the query params
}

An HttpRequest can also be formatted as a cURL command or as raw HTTP.

cURL command

import {
  // ...
  formatCurl,
} from '@supabase/sql-to-rest'

// ...

const curlCommand = formatCurl('http://localhost:54321/rest/v1', httpRequest)

Outputs:

curl http://localhost:54321/rest/v1/books

Raw HTTP

import {
  // ...
  formatHttp,
} from '@supabase/sql-to-rest'

// ...

const rawHttp = formatHttp('http://localhost:54321/rest/v1', httpRequest)

Outputs:

GET /rest/v1/books HTTP/1.1
Host: localhost:54321

renderSupabaseJs()

Takes the intermediate Statement and renders it as supabase-js client code.

import { processSql, renderSupabaseJs } from '@supabase/sql-to-rest'

const statement = await processSql(`
  select
    *
  from
    books
`)

const { code } = await renderSupabaseJs(statement)

Outputs a Promise<SupabaseJsQuery>, where code contains:

const { data, error } = await supabase.from('books').select()

The rendered JS code is automatically formatted using prettier.

How does it work?

SQL to REST diagram
  1. The SQL string is parsed into a PostgreSQL abstract syntax tree (AST) using libpg-query-node, a JavaScript SQL parser that uses C code from the official PostgreSQL codebase (compiled to WASM for browser targets, NAPI for Node targets). Supports Postgres 15 syntax.
  2. The PostgreSQL AST is translated into a much smaller and simpler PostgREST AST. Since PostgREST supports a subset of SQL syntax, any unsupported SQL operation will throw an UnsupportedError with a description of exactly what wasn't supported.
  3. The intermediate PostgREST AST can be rendered to your language/protocol of choice. Currently supports HTTP (with cURL and raw HTTP formatters), and supabase-js code (which wraps PostgREST). Other languages are on the roadmap (PR's welcome!)

Roadmap

SQL features

Statements

Column operators
  • = (eq)
  • > (gt)
  • >= (gte)
  • < (lt)
  • <= (lte)
  • <> or != (neq)
  • like (like)
  • ilike (ilike)
  • ~ (match)
  • ~* (imatch)
  • in (in)
  • is (is): partial support, only is null for now
  • is distinct from (isdistinct)
  • @@ (fts, plfts, phfts, wfts)
  • @> (cs)
  • <@ (cd)
  • && (ov)
  • << (sl)
  • >> (sr)
  • &< (nxr)
  • &> (nxl)
  • -|- (adj)
Logical operators
  • not (not)
  • or (or)
  • and (and)
  • all (all)
  • any (any)
  • asc (asc)
  • desc (desc)
  • nulls first (nullsfirst)
  • nulls last (nullslast)
  • limit (limit)
  • offset (offset)
  • HTTP range headers
Functions
  • count()
  • sum()
  • avg()
  • max()
  • min()
Features
  • aggregate over entire table
  • aggregate on joined table column
  • aggregate with group by
  • aggregate with group by on joined table column

Joins (Resource Embedding)

SQL joins are supported using PostgREST resource embeddings with the spread ... syntax (flattens joined table into primary table).

  • column aliases
  • table aliases
  • column casts (in select target only)
  • aggregate function casts (in select target only)

JSON columns (eg. select metadata->'person'->>'name') are supported in the following places:

  • select targets
  • filters
  • sorts

Renderers

FAQs

Are you parsing SQL from scratch?

Thankfully no. We use libpg-query-node which takes source code from the real PostgreSQL parser and wraps it in JavaScript bindings. It compiles the C code into WASM for browser environments and uses native NAPI bindings for server environments.

This means we never have to worry about the SQL itself getting parsed incorrectly - it uses the exact same code as the actual PostgreSQL database. This library uses code from PostgreSQL 15.

SQL is a very open language - how can it all translate to REST?

It can't. PostgREST only supports a subset of SQL-like features (by design), so this library only translates features that can be mapped 1-to-1.

When it detects SQL that doesn't translate (eg. sub-queries), it will throw an UnsupportedError with a description of exactly what couldn't be translated.

How can I be confident that my SQL is translating correctly?

We've built unit tests for every feature supported. The vast majority of PostgREST features have been implemented, but it doesn't cover 100% yet (see Roadmap). If you discover an error in the translation, please submit an issue.

License

MIT