Skip to content

Query Builder ​

The ScyllinX Query Builder provides a fluent, expressive interface for building database queries. It supports both SQL and ScyllaDB/Cassandra-specific features, allowing you to construct complex queries with ease while maintaining type safety.

Basic Usage ​

Getting Started ​

The Query Builder can be accessed through models or created directly:

typescript
import { QueryBuilder, User } from 'scyllinx';

// Through a model (recommended)
const query = User.query();

// Direct instantiation
const query = new QueryBuilder('users');

Basic Queries ​

typescript
// Select all users
const users = await User.query().get();

// Select specific columns
const users = await User.query()
  .select('id', 'name', 'email')
  .get();

// Get first result
const user = await User.query()
  .where('email', 'john@example.com')
  .first();

// Count records
const count = await User.query()
  .where('active', true)
  .count();

// Check if records exist
const exists = await User.query()
  .where('role', 'admin')
  .exists();

WHERE Clauses ​

Basic WHERE Conditions ​

typescript
// Simple equality
const users = await User.query()
  .where('active', true)
  .get();

// With operator
const users = await User.query()
  .where('age', '>', 18)
  .where('created_at', '<=', new Date())
  .get();

// Multiple conditions (AND)
const users = await User.query()
  .where('active', true)
  .where('role', 'user')
  .where('age', '>=', 21)
  .get();

OR Conditions ​

typescript
// OR WHERE
const users = await User.query()
  .where('role', 'admin')
  .orWhere('role', 'moderator')
  .get();

// Complex OR conditions
// Sub-queries NOT implemented
const users = await User.query()
  .where('active', true)
  .where(query => {
    query.where('role', 'admin')
         .orWhere('permissions', 'like', '%manage%');
  })
  .get();

WHERE IN / NOT IN ​

typescript
// WHERE IN
const users = await User.query()
  .whereIn('role', ['admin', 'moderator', 'editor'])
  .get();

// WHERE NOT IN
const users = await User.query()
  .whereNotIn('status', ['banned', 'suspended'])
  .get();

// With subquery
const activeUserIds = User.query()
  .where('active', true)
  .select('id');

const posts = await Post.query()
  .whereIn('user_id', activeUserIds)
  .get();

WHERE BETWEEN ​

typescript
// Date range
const users = await User.query()
  .whereBetween('created_at', [
    new Date('2024-01-01'),
    new Date('2024-12-31')
  ])
  .get();

// Numeric range
const users = await User.query()
  .whereBetween('age', [18, 65])
  .get();

NULL Checks ​

typescript
// WHERE NULL
const users = await User.query()
  .whereNull('deleted_at')
  .get();

// WHERE NOT NULL
const users = await User.query()
  .whereNotNull('email_verified_at')
  .get();

Advanced WHERE Conditions ​

typescript
// JSON column queries (if supported)
const users = await User.query()
  .where('settings->theme', 'dark')
  .get();

// Raw WHERE conditions NOT IMPLEMENTED
const users = await User.query()
  .whereRaw('LOWER(email) = ?', ['john@example.com'])
  .get();

// Conditional WHERE
const query = User.query();

if (filters.role) {
  query.where('role', filters.role);
}

if (filters.active !== undefined) {
  query.where('active', filters.active);
}

const users = await query.get();

Ordering and Limiting ​

ORDER BY ​

typescript
// Single column ordering
const users = await User.query()
  .orderBy('created_at', 'desc')
  .get();

// Multiple column ordering
const users = await User.query()
  .orderBy('role', 'asc')
  .orderBy('name', 'asc')
  .get();

// Random ordering (if supported)
// orderByRaw NOT implemented
const users = await User.query()
  .orderByRaw('RANDOM()')
  .limit(5)
  .get();

LIMIT and OFFSET ​

typescript
// Limit results
const users = await User.query()
  .limit(10)
  .get();

// Pagination
const users = await User.query()
  .offset(20)
  .limit(10)
  .get();

// Alternative pagination methods
const users = await User.query()
  .skip(20)
  .take(10)
  .get();

Grouping and Aggregation ​

GROUP BY ​

typescript
// Basic grouping
const usersByRole = await User.query()
  .select('role')
  .addSelect('COUNT(*) as count')
  .groupBy('role')
  .get();

// Multiple columns
const stats = await Post.query()
  .select('user_id', 'status')
  .addSelect('COUNT(*) as count')
  .groupBy('user_id', 'status')
  .get();

HAVING ​

typescript
// HAVING clause
const activeUsers = await User.query()
  .select('role')
  .addSelect('COUNT(*) as count')
  .groupBy('role')
  .having('count', '>', 5)
  .get();

// Complex HAVING
const stats = await Post.query()
  .select('user_id')
  .addSelect('COUNT(*) as post_count')
  .addSelect('AVG(view_count) as avg_views')
  .groupBy('user_id')
  .having('post_count', '>=', 10)
  .having('avg_views', '>', 1000)
  .get();

Aggregate Functions ​

typescript
// Count
const userCount = await User.query()
  .where('active', true)
  .count();

// Count with column
const emailCount = await User.query()
  .count('email');

// Other aggregates
// aggregate method NOT implemented
const stats = await Post.query()
  .where('published', true)
  .aggregate([
    'COUNT(*) as total_posts',
    'AVG(view_count) as avg_views',
    'MAX(view_count) as max_views',
    'MIN(created_at) as first_post',
    'SUM(view_count) as total_views'
  ]);

Joins ​

Basic Joins ​

typescript
// INNER JOIN
const usersWithPosts = await User.query()
  .join('posts', 'users.id', '=', 'posts.user_id')
  .select('users.*', 'posts.title')
  .get();

// LEFT JOIN
const usersWithOptionalPosts = await User.query()
  .leftJoin('posts', 'users.id', '=', 'posts.user_id')
  .select('users.*', 'posts.title')
  .get();

// RIGHT JOIN
const postsWithUsers = await Post.query()
  .rightJoin('users', 'posts.user_id', '=', 'users.id')
  .select('posts.*', 'users.name')
  .get();

Advanced Joins ​

typescript
// Join with additional conditions
// sub joins NOT implemented
const users = await User.query()
  .join('posts', (join) => {
    join.on('users.id', '=', 'posts.user_id')
        .where('posts.published', true);
  })
  .get();

// Multiple joins
const data = await User.query()
  .join('posts', 'users.id', '=', 'posts.user_id')
  .join('comments', 'posts.id', '=', 'comments.post_id')
  .select('users.name', 'posts.title', 'comments.content')
  .get();

// Self join
const userHierarchy = await User.query()
  .join('users as managers', 'users.manager_id', '=', 'managers.id')
  .select('users.name as employee', 'managers.name as manager')
  .get();

ScyllaDB-Specific Features ​

Token-Based Queries ​

typescript
// Token-based pagination for ScyllaDB
const users = await User.query()
  .whereToken(['user_id'], '>', [1000])
  .limit(100)
  .get();

// Token range queries
const users = await User.query()
  .whereToken(['partition_key'], '>=', [startToken])
  .whereToken(['partition_key'], '<', [endToken])
  .get();

ALLOW FILTERING ​

typescript
// Use ALLOW FILTERING for non-indexed columns
const users = await User.query()
  .where('non_indexed_column', 'value')
  .allowFiltering()
  .get();

// Note: Use sparingly as it can impact performance

TTL (Time To Live) ​

typescript
// Set TTL for INSERT operations
await User.query()
  .ttl(3600) // 1 hour
  .insert({
    id: 'temp-user',
    name: 'Temporary User',
    email: 'temp@example.com'
  });

// TTL for UPDATE operations
await User.query()
  .where('id', 'user-id')
  .ttl(7200) // 2 hours
  .update({
    last_activity: new Date()
  });

Lightweight Transactions ​

typescript
// IF NOT EXISTS
await User.query()
  .ifNotExists()
  .insert({
    id: 'unique-user',
    name: 'John Doe',
    email: 'john@example.com'
  });

// IF conditions
await User.query()
  .where('id', 'user-id')
  .if('version', '=', 1)
  .update({
    name: 'Updated Name',
    version: 2
  });

// Multiple IF conditions
await User.query()
  .where('id', 'user-id')
  .if('status', '=', 'active')
  .if('version', '=', currentVersion)
  .update({
    status: 'inactive',
    version: currentVersion + 1
  });

Data Modification ​

INSERT Operations ​

typescript
// Single insert
await User.query()
  .insert({
    id: 'user-1',
    name: 'John Doe',
    email: 'john@example.com'
  });

// Batch insert
await User.query()
  .insert([
    { id: 'user-1', name: 'John', email: 'john@example.com' },
    { id: 'user-2', name: 'Jane', email: 'jane@example.com' },
    { id: 'user-3', name: 'Bob', email: 'bob@example.com' }
  ]);

// Insert and get ID (for auto-increment)
const userId = await User.query()
  .insertGetId({
    name: 'New User',
    email: 'new@example.com'
  });

UPDATE Operations ​

typescript
// Basic update
const updatedCount = await User.query()
  .where('id', 'user-id')
  .update({
    name: 'Updated Name',
    updated_at: new Date()
  });

// Conditional update
const updatedCount = await User.query()
  .where('active', false)
  .where('last_login', '<', new Date('2023-01-01'))
  .update({
    status: 'inactive'
  });

// Update with increment/decrement
await User.query()
  .where('id', 'user-id')
  .increment('login_count', 1);

await User.query()
  .where('id', 'user-id')
  .decrement('credits', 10);

UPSERT Operations ​

typescript
// Update or insert
const success = await User.query()
  .updateOrInsert(
    { email: 'john@example.com' }, // Search criteria
    { name: 'John Doe', active: true } // Data to update/insert
  );

DELETE Operations ​

typescript
// Basic delete
const deletedCount = await User.query()
  .where('id', 'user-id')
  .delete();

// Conditional delete
const deletedCount = await User.query()
  .where('active', false)
  .where('created_at', '<', new Date('2023-01-01'))
  .delete();

// Truncate table (delete all records)
await User.query().truncate();

Query Optimization ​

Eager Loading ​

typescript
// Load relationships
const users = await User.query()
  .with('posts', 'profile')
  .get();

// Nested relationships
const users = await User.query()
  .with('posts.comments', 'posts.tags')
  .get();

// Conditional eager loading
// subqueries NOT implemented
const users = await User.query()
  .with('posts', (query) => {
    query.where('published', true)
         .orderBy('created_at', 'desc');
  })
  .get();

Query Debugging ​

SQL Generation ​

typescript
const query = User.query()
  .where('active', true)
  .orderBy('created_at', 'desc')
  .limit(10);

// Get SQL without executing
console.log(query.toSql());
// Output: SELECT * FROM users WHERE active = ? ORDER BY created_at DESC LIMIT ?

// Get SQL with parameters interpolated
console.log(query.toRawSql());
// Output: SELECT * FROM users WHERE active = true ORDER BY created_at DESC LIMIT 10

Advanced Query Patterns ​

Dynamic Query Building ​

typescript
class UserQueryBuilder {
  private query: QueryBuilder<User, UserAttributes>;

  constructor() {
    this.query = User.query();
  }

  filterByRole(role?: string): this {
    if (role) {
      this.query.where('role', role);
    }
    return this;
  }

  filterByStatus(active?: boolean): this {
    if (active !== undefined) {
      this.query.where('active', active);
    }
    return this;
  }

  filterByDateRange(startDate?: Date, endDate?: Date): this {
    if (startDate) {
      this.query.where('created_at', '>=', startDate);
    }
    if (endDate) {
      this.query.where('created_at', '<=', endDate);
    }
    return this;
  }

  search(term?: string): this {
    if (term) {
      this.query.where((query) => {
        query.where('name', 'like', `%${term}%`)
             .orWhere('email', 'like', `%${term}%`);
      });
    }
    return this;
  }

  paginate(page: number, perPage: number): this {
    this.query.offset((page - 1) * perPage).limit(perPage);
    return this;
  }

  async get(): Promise<User[]> {
    return await this.query.get();
  }

  async count(): Promise<number> {
    return await this.query.count();
  }
}

// Usage
const queryBuilder = new UserQueryBuilder();
const users = await queryBuilder
  .filterByRole('admin')
  .filterByStatus(true)
  .search('john')
  .paginate(1, 10)
  .get();

Repository Pattern ​

typescript
class UserRepository {
  async findActiveUsers(limit = 10): Promise<User[]> {
    return await User.query()
      .where('active', true)
      .orderBy('created_at', 'desc')
      .limit(limit)
      .get();
  }

  async findByEmailDomain(domain: string): Promise<User[]> {
    return await User.query()
      .where('email', 'like', `%@${domain}`)
      .get();
  }

  async getUserStats(): Promise<{
    total: number;
    active: number;
    inactive: number;
    byRole: Record<string, number>;
  }> {
    const [total, active, inactive, roleStats] = await Promise.all([
      User.query().count(),
      User.query().where('active', true).count(),
      User.query().where('active', false).count(),
      User.query()
        .select('role')
        .addSelect('COUNT(*) as count')
        .groupBy('role')
        .get()
    ]);

    const byRole = roleStats.reduce((acc, stat) => {
      acc[stat.role] = stat.count;
      return acc;
    }, {} as Record<string, number>);

    return { total, active, inactive, byRole };
  }
}

Best Practices ​

1. Use Type-Safe Queries ​

typescript
// Good: Type-safe column references
const users = await User.query()
  .select('id', 'name', 'email') // TypeScript knows these columns exist
  .where('active', true)
  .get();

// Avoid: Raw strings without type checking
const users = await User.query()
  .selectRaw('id, name, email') // No type checking
  .get();

2. Optimize for Your Database ​

typescript
// For ScyllaDB: Use partition keys in WHERE clauses
const userEvents = await UserEvent.query()
  .where('user_id', userId) // Partition key first
  .where('event_time', '>', startTime)
  .get();

// For SQL: Use indexes effectively
const users = await User.query()
  .where('email', email) // Assuming email is indexed
  .first();

3. Handle Large Result Sets ​

typescript
// Use pagination for large datasets
async function getAllUsers(callback: (users: User[]) => void) {
  let page = 1;
  const perPage = 1000;
  
  while (true) {
    const users = await User.query()
      .offset((page - 1) * perPage)
      .limit(perPage)
      .get();
    
    if (users.length === 0) break;
    
    await callback(users);
    page++;
  }
}

// Or use cursor-based pagination for ScyllaDB
async function getUsersWithCursor(lastToken?: string) {
  const query = User.query().limit(100);
  
  if (lastToken) {
    query.whereToken(['user_id'], '>', [lastToken]);
  }
  
  return await query.get();
}

4. Use Transactions When Needed ​

typescript
// Wrap related operations in transactions
await User.transaction(async (trx) => {
  const user = await User.query(trx)
    .where('id', userId)
    .first();
  
  if (!user) throw new Error('User not found');
  
  await user.update({ credits: user.credits - amount });
  
  await Transaction.query(trx)
    .insert({
      user_id: userId,
      amount: -amount,
      type: 'debit'
    });
});

The Query Builder is a powerful tool that provides the flexibility to construct complex queries while maintaining type safety and database compatibility. Use it wisely to build efficient and maintainable database interactions.

Released under the MIT License.