Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions packages/core/postgrest-js/docs/SELECT_BUILDER_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Plan: Add Array-Based Type-Safe select() API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this file


## Overview

Add a new array-based API for `select()` that provides IDE autocomplete while maintaining full backward compatibility with the existing string-based API.

## Design: Hybrid Array API

Simple cases use strings, complex features use objects:

```typescript
// Simple - just column names
.select(['id', 'name', 'email'])

// With alias
.select(['id', { column: 'username', as: 'display_name' }])

// With relation
.select(['id', { relation: 'posts', select: ['id', 'title'] }])

// Complex - all features
.select([
{ column: 'created_at', cast: 'text' },
{ column: 'data', json: ['settings', 'theme'] },
{ relation: 'posts', hint: 'fk_author', inner: true, select: ['id'] },
{ spread: true, relation: 'profile', select: ['status'] },
{ count: true, as: 'total' }
])
```

## Type Definitions

```typescript
// Aggregate functions
type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'

// Field/column selection
interface FieldSpec {
column: string
as?: string
cast?: string
json?: string[]
jsonText?: string[]
aggregate?: AggregateFunction
}

// Relation/join selection
interface RelationSpec {
relation: string
as?: string
hint?: string
inner?: boolean
left?: boolean
select: SelectSpec
}

// Spread operation
interface SpreadSpec {
spread: true
relation: string
hint?: string
select: SelectSpec
}

// Count shorthand
interface CountSpec {
count: true
as?: string
cast?: string
}

// Single item in select array
type SelectItem = string | FieldSpec | RelationSpec | SpreadSpec | CountSpec

// Full select specification
type SelectSpec = string | SelectItem[]
```

## Feature Coverage

| Feature | String Syntax | Array Syntax |
| ------------------ | ----------------------- | ----------------------------- |
| Simple columns | `'id, name'` | `['id', 'name']` |
| Column alias | `'display:username'` | `{ column, as }` |
| Type cast | `'col::text'` | `{ column, cast }` |
| JSON path (->) | `'data->foo'` | `{ column, json: [...] }` |
| JSON as text (->>) | `'data->>foo'` | `{ column, jsonText: [...] }` |
| Column aggregate | `'id.sum()'` | `{ column, aggregate }` |
| Top-level count | `'count()'` | `{ count: true }` |
| Relation (join) | `'posts(id)'` | `{ relation, select }` |
| Relation alias | `'author:users(id)'` | `{ relation, as, select }` |
| Inner join | `'posts!inner(id)'` | `{ relation, inner: true }` |
| Left join | `'posts!left(id)'` | `{ relation, left: true }` |
| FK hint | `'users!fk_id(id)'` | `{ relation, hint }` |
| Spread | `'...profile(status)'` | `{ spread: true, relation }` |
| Nested relations | `'posts(comments(id))'` | nested `select` arrays |

## Files Created/Modified

### New Files

- `src/select-query-parser/select-builder.ts` - Types and serialization
- `test/select-builder.test.ts` - Unit tests (56 tests)
- `test/select-builder-integration.test.ts` - Integration tests (24 tests)
- `test/select-builder.test-d.ts` - Type tests

### Modified Files

- `src/PostgrestQueryBuilder.ts` - Accept array in `select()`
- `src/PostgrestTransformBuilder.ts` - Accept array in `select()`
- `src/index.ts` - Export new types
33 changes: 30 additions & 3 deletions packages/core/postgrest-js/src/PostgrestQueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import PostgrestFilterBuilder from './PostgrestFilterBuilder'
import { GetResult } from './select-query-parser/result'
import { SelectItem, serializeSelectSpec } from './select-query-parser/select-builder'
import {
ClientServerOptions,
Fetch,
Expand Down Expand Up @@ -69,7 +70,9 @@ export default class PostgrestQueryBuilder<
/**
* Perform a SELECT query on the table or view.
*
* @param columns - The columns to retrieve, separated by commas. Columns can be renamed when returned with `customName:columnName`
* @param columns - The columns to retrieve. Can be:
* - A string with comma-separated column names (e.g., `'id, name, email'`)
* - An array of column names and/or field specs (e.g., `['id', { column: 'name', as: 'display_name' }]`)
*
* @param options - Named parameters
*
Expand All @@ -90,6 +93,19 @@ export default class PostgrestQueryBuilder<
* @remarks
* When using `count` with `.range()` or `.limit()`, the returned `count` is the total number of rows
* that match your filters, not the number of rows in the current page. Use this to build pagination UI.
*
* @example String-based select
* ```ts
* .select('id, name, email')
* .select('posts(id, title)')
* ```
*
* @example Array-based select (provides IDE autocomplete)
* ```ts
* .select(['id', 'name', 'email'])
* .select(['id', { relation: 'posts', select: ['id', 'title'] }])
* .select([{ column: 'username', as: 'display_name' }])
* ```
*/
select<
Query extends string = '*',
Expand All @@ -102,7 +118,7 @@ export default class PostgrestQueryBuilder<
ClientOptions
>,
>(
columns?: Query,
columns?: Query | SelectItem[],
options?: {
head?: boolean
count?: 'exact' | 'planned' | 'estimated'
Expand All @@ -119,9 +135,20 @@ export default class PostgrestQueryBuilder<
const { head = false, count } = options ?? {}

const method = head ? 'HEAD' : 'GET'

// Serialize array-based select specs to string
let columnsString: string
if (columns === undefined) {
columnsString = '*'
} else if (Array.isArray(columns)) {
columnsString = serializeSelectSpec(columns)
} else {
columnsString = columns
}

// Remove whitespaces except when quoted
let quoted = false
const cleanedColumns = (columns ?? '*')
const cleanedColumns = columnsString
.split('')
.map((c) => {
if (/\s/.test(c) && !quoted) {
Expand Down
30 changes: 27 additions & 3 deletions packages/core/postgrest-js/src/PostgrestTransformBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import PostgrestBuilder from './PostgrestBuilder'
import PostgrestFilterBuilder, { InvalidMethodError } from './PostgrestFilterBuilder'
import { GetResult } from './select-query-parser/result'
import { SelectItem, serializeSelectSpec } from './select-query-parser/select-builder'
import { CheckMatchingArrayTypes } from './types/types'
import { ClientServerOptions, GenericSchema } from './types/common/common'
import type { MaxAffectedEnabled } from './types/feature-flags'
Expand All @@ -21,13 +22,26 @@ export default class PostgrestTransformBuilder<
* return modified rows. By calling this method, modified rows are returned in
* `data`.
*
* @param columns - The columns to retrieve, separated by commas
* @param columns - The columns to retrieve. Can be:
* - A string with comma-separated column names (e.g., `'id, name, email'`)
* - An array of column names and/or field specs (e.g., `['id', { column: 'name', as: 'display_name' }]`)
*
* @example String-based select
* ```ts
* .select('id, name, email')
* ```
*
* @example Array-based select (provides IDE autocomplete)
* ```ts
* .select(['id', 'name', 'email'])
* .select([{ column: 'username', as: 'display_name' }])
* ```
*/
select<
Query extends string = '*',
NewResultOne = GetResult<Schema, Row, RelationName, Relationships, Query, ClientOptions>,
>(
columns?: Query
columns?: Query | SelectItem[]
): PostgrestFilterBuilder<
ClientOptions,
Schema,
Expand All @@ -41,9 +55,19 @@ export default class PostgrestTransformBuilder<
Relationships,
Method
> {
// Serialize array-based select specs to string
let columnsString: string
if (columns === undefined) {
columnsString = '*'
} else if (Array.isArray(columns)) {
columnsString = serializeSelectSpec(columns)
} else {
columnsString = columns
}

// Remove whitespaces except when quoted
let quoted = false
const cleanedColumns = (columns ?? '*')
const cleanedColumns = columnsString
.split('')
.map((c) => {
if (/\s/.test(c) && !quoted) {
Expand Down
11 changes: 11 additions & 0 deletions packages/core/postgrest-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,14 @@ export type { ClientServerOptions as PostgrestClientOptions } from './types/comm
// https://github.com/supabase/postgrest-js/issues/551
// To be replaced with a helper type that only uses public types
export type { GetResult as UnstableGetResult } from './select-query-parser/result'
// Array-based select builder types
export type {
SelectSpec,
SelectItem,
FieldSpec,
RelationSpec,
SpreadSpec,
CountSpec,
AggregateFunction,
} from './select-query-parser/select-builder'
export { serializeSelectSpec } from './select-query-parser/select-builder'
Loading
Loading