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.