Skip to content

Query Builder

PGRestify's query builder provides a fluent, type-safe API for constructing complex database queries. It supports all PostgREST operators and features while maintaining full TypeScript type safety.

For detailed API documentation, see our comprehensive Query Building Guide.

Basic Query Building

Simple Selection

typescript
// Select all columns
const response = await client.from<User>('users').execute();
const users = response.data || [];

// Select specific columns
const response = await client
  .from<User>('users')
  .select('id', 'name', 'email')
  .execute();

// Using getMany for cleaner syntax
const response = await client
  .from<User>('users')
  .select('id', 'name', 'email')
  .getMany();

Filtering

PGRestify supports all PostgREST filter operators:

Equality Operators

typescript
// Equal
const response = await client
  .from<User>('users')
  .eq('active', true)
  .execute();

// Not equal
const response = await client
  .from<User>('users')
  .neq('active', true)
  .getMany();

Comparison Operators

typescript
// Greater than
const response = await client
  .from<User>('users')
  .gt('created_at', '2023-01-01')
  .execute();

// Greater than or equal
const response = await client
  .from<User>('users')
  .gte('age', 18)
  .getMany();

// Less than
const response = await client
  .from<User>('users')
  .lt('age', 30)
  .execute();

// Less than or equal
const response = await client
  .from<User>('users')
  .lte('age', 65)
  .getMany();

Pattern Matching

typescript
// Like (case-sensitive)
const response = await client
  .from<User>('users')
  .like('name', 'John*')
  .execute();

// ILike (case-insensitive)
const response = await client
  .from<User>('users')
  .ilike('name', 'john*')
  .getMany();

// Match (regex, case-sensitive)
const response = await client
  .from<User>('users')
  .match('email', '.*@gmail\\.com$')
  .execute();

// IMatch (regex, case-insensitive)
const response = await client
  .from<User>('users')
  .imatch('email', '.*@GMAIL\\.COM$')
  .getMany();

Array and Range Operations

typescript
// In array
const response = await client
  .from<User>('users')
  .in('id', [1, 2, 3, 4, 5])
  .execute();

// Contains (for arrays and ranges)
const response = await client
  .from<Post>('posts')
  .contains('tags', ['javascript', 'typescript'])
  .getMany();

// Contained by
const response = await client
  .from<Post>('posts')
  .containedBy('tags', ['javascript', 'typescript', 'react', 'vue'])
  .execute();

// Overlaps (for ranges)
const response = await client
  .from<Event>('events')
  .overlaps('date_range', '[2023-01-01,2023-12-31)')
  .getMany();

Null Operations

typescript
// Is null
const response = await client
  .from<User>('users')
  .is('profile_id', null)
  .execute();

// Is not null
const response = await client
  .from<User>('users')
  .not('profile_id', 'is', null)
  .getMany();

Combining Filters

AND Conditions (Default)

By default, multiple filters are combined with AND:

typescript
const response = await client
  .from<User>('users')
  .eq('active', true)
  .gte('age', 18)
  .execute();
// WHERE active = true AND age >= 18

OR Conditions

typescript
const response = await client
  .from<User>('users')
  .or('age.lt.25,age.gt.65')
  .execute();
// WHERE age < 25 OR age > 65

// Complex OR with AND
const response = await client
  .from<User>('users')
  .or('and(name.ilike.*john*,active.eq.true),and(name.ilike.*jane*,active.eq.false)')
  .getMany();

NOT Conditions

typescript
const response = await client
  .from<User>('users')
  .not('name', 'ilike', '*john*')
  .execute();

Ordering

typescript
// Single column, ascending (default)
const response = await client
  .from<User>('users')
  .order('name')
  .execute();

// Single column, descending
const response = await client
  .from<User>('users')
  .order('created_at', { ascending: false })
  .getMany();

// Multiple columns
const response = await client
  .from<User>('users')
  .order('active', { ascending: false })
  .order('name', { ascending: true })
  .execute();

// Null handling
const response = await client
  .from<User>('users')
  .order('last_login', { ascending: false, nullsFirst: false })
  .getMany();

Pagination

Limit and Offset

typescript
// First 10 users
const response = await client
  .from<User>('users')
  .limit(10)
  .execute();

// Second page (skip first 10)
const response = await client
  .from<User>('users')
  .limit(10)
  .offset(10)
  .getMany();

Range-based Pagination

typescript
// Get rows 20-29 (zero-indexed)
const response = await client
  .from<User>('users')
  .range(20, 29)
  .execute();

Counting

typescript
// Get count only
const userCount = await client
  .from<User>('users')
  .getCount();

// Get data with count
const response = await client
  .from<User>('users')
  .select('*')
  .count('exact')
  .execute();
const { data, count } = response;

// Estimated count (faster for large tables)
const response = await client
  .from<User>('users')
  .count('estimated')
  .execute();
const { data, count } = response;

Single Row Queries

typescript
// Get single row (throws if not found or multiple found)
const response = await client
  .from<User>('users')
  .select('*')
  .eq('id', 1)
  .single()
  .execute();
const user = response.data;

// Maybe single (returns null if not found)
const response = await client
  .from<User>('users')
  .eq('id', 1)
  .maybeSingle()
  .execute();
const user = response.data;

// Get one or fail (cleaner syntax)
const user = await client
  .from<User>('users')
  .eq('id', 1)
  .getOneOrFail();

Advanced Selection

Column Aliasing

typescript
// Rename columns in response
const response = await client
  .from<User>('users')
  .selectAs({
    userId: 'id',
    fullName: 'name',
    emailAddress: 'email'
  })
  .execute();

Computed Fields

typescript
// Select with computed fields
const response = await client
  .from<Post>('posts')
  .select(`
    id,
    title,
    views,
    likes,
    engagement_score:(views + likes * 2),
    is_popular:(views > 1000)
  `)
  .execute();

Embedded Resources (Joins)

typescript
// Simple join
const response = await client
  .from<Post>('posts')
  .select(`
    id,
    title,
    content,
    author:users!posts_author_id_fkey(id, name, email)
  `)
  .execute();

// Complex nested joins
const postsWithEverything = await client
  .from<Post>('posts')
  .select(`
    id,
    title,
    content,
    author:users!posts_author_id_fkey(
      id,
      name,
      email,
      profile:profiles(bio, avatar_url)
    ),
    comments:comments(
      id,
      content,
      user:users(name)
    )
  `)
  .find();
typescript
// Simple full-text search
const response = await client
  .from<Post>('posts')
  .fts('content', 'javascript typescript')
  .execute();

// Search with configuration
const configuredSearch = await client
  .from<Post>('posts')
  .fts('content', 'javascript', { config: 'english' })
  .find();

// Phrase search
const phraseSearch = await client
  .from<Post>('posts')
  .plfts('content', 'web development')
  .find();

// WebSearch-style search
const webSearch = await client
  .from<Post>('posts')
  .wfts('content', 'javascript OR typescript')
  .find();

Aggregation Functions

typescript
// Basic aggregates
const stats = await client
  .from<Sale>('sales')
  .select(`
    count(*) as total_sales,
    sum(amount) as total_revenue,
    avg(amount) as avg_sale,
    min(amount) as min_sale,
    max(amount) as max_sale
  `)
  .single()
  .execute();

// Group by
const salesByRegion = await client
  .from<Sale>('sales')
  .select(`
    region,
    count(*) as total_sales,
    sum(amount) as revenue
  `)
  .groupBy('region')
  .execute();

// Having clause
const bigRegions = await client
  .from<Sale>('sales')
  .select(`
    region,
    sum(amount) as revenue
  `)
  .groupBy('region')
  .having('sum(amount) > 10000')
  .execute();

Method Chaining

All query builder methods can be chained for complex queries:

typescript
const response = await client
  .from<User>('users')
  .select('id, name, email, posts:posts(id, title)')
  .eq('active', true)
  .gte('created_at', '2023-01-01')
  .or('role.eq.admin,posts.gt.10')
  .order('created_at', { ascending: false })
  .order('name')
  .range(0, 19)
  .execute();

Query Execution

Execution Methods

typescript
// execute() - returns QueryResponse with data/error
const response = await client.from<User>('users').execute();
if (response.error) {
  console.error('Query failed:', response.error);
} else {
  console.log('Users:', response.data);
}

// getMany() - returns QueryResponse for multiple rows
const response = await client.from<User>('users').getMany();

// getOne() - returns QueryResponse for single row
const response = await client
  .from<User>('users')
  .eq('id', 1)
  .getOne();

// getOneOrFail() - throws if not found, returns the entity directly
const user = await client
  .from<User>('users')
  .eq('id', 1)
  .getOneOrFail();

// single() - ensure single row result (with execute)
const response = await client
  .from<User>('users')
  .eq('id', 1)
  .single()
  .execute();

// maybeSingle() - single row or null (with execute)
const response = await client
  .from<User>('users')
  .eq('email', 'user@example.com')
  .maybeSingle()
  .execute();

Error Handling

typescript
try {
  const response = await client.from<User>('users').execute();
  if (response.error) {
    throw response.error;
  }
  const users = response.data;
} catch (error) {
  if (error instanceof PostgRESTError) {
    console.log(`Database error ${error.statusCode}: ${error.message}`);
    console.log('Details:', error.details);
  }
}

Performance Tips

Use Specific Selects

typescript
// ❌ Don't select all columns if you don't need them
const response = await client.from<User>('users').execute();

// ✅ Select only what you need
const response = await client
  .from<User>('users')
  .select('id', 'name', 'email')
  .execute();

Use Limits

typescript
// ❌ Don't fetch unlimited rows
const response = await client.from<User>('users').execute();

// ✅ Always use limits for large datasets
const response = await client
  .from<User>('users')
  .limit(100)
  .execute();

Use Estimated Counts

typescript
// ❌ Exact counts are slow for large tables
const { count } = await client
  .from<User>('users')
  .count('exact')
  .execute();

// ✅ Use estimated counts when possible
const { count } = await client
  .from<User>('users')
  .count('estimated')
  .execute();

The query builder is the heart of PGRestify, providing a powerful yet intuitive way to construct any query your application needs! 🚀

Released under the MIT License.