Skip to main content

Usage Examples

Real-world examples demonstrating common patterns and best practices.

Basic Query

Simple query execution with auto-connect.
import { DucklingClient } from '@chittihq/duckling';

const client = new DucklingClient({
  url: 'ws://localhost:3001/ws',
  apiKey: process.env.DUCKLING_API_KEY!,
  enableLogging: true,
  logLevel: 'info'
  // autoConnect: true (default)
  // autoPing: true (default - keeps connection alive every 30s)
});

try {
  // Execute query - client will auto-connect on first query
  console.log('Executing query: SELECT * FROM User LIMIT 5');
  const startTime = Date.now();
  const users = await client.query('SELECT * FROM User LIMIT 5');
  const duration = Date.now() - startTime;

  console.log(`✓ Query completed in ${duration}ms`);
  console.log(`✓ Rows returned: ${users.length}`);
  console.table(users);

  // Get table count
  const countResult = await client.query<{ count: number }>(
    'SELECT COUNT(*) as count FROM User'
  );
  console.log(`✓ Total users: ${countResult[0].count}`);

  // Connection stats
  console.log('Connection stats:', client.getStats());

} catch (error) {
  console.error('Error:', error instanceof Error ? error.message : error);
} finally {
  client.close();
}
Run:
DUCKLING_API_KEY=your-key tsx basic-query.ts

Parallel Queries

Execute multiple queries concurrently for maximum throughput.
import { DucklingClient } from '@chittihq/duckling';

const client = new DucklingClient({
  url: 'ws://localhost:3001/ws',
  apiKey: process.env.DUCKLING_API_KEY!
});

try {
  // Define multiple queries
  const queries = [
    'SELECT COUNT(*) as count FROM User',
    'SELECT COUNT(*) as count FROM Product',
    'SELECT COUNT(*) as count FROM SubProduct',
    'SELECT COUNT(*) as count FROM ActivityLog',
    'SELECT COUNT(*) as count FROM IndianPincode'
  ];

  // Sequential execution benchmark
  console.log('--- Sequential Execution Benchmark ---');
  const sequentialStart = Date.now();
  const sequentialResults = [];
  for (const query of queries) {
    const result = await client.query(query);
    sequentialResults.push(result);
  }
  const sequentialDuration = Date.now() - sequentialStart;
  console.log(`✓ Sequential execution: ${sequentialDuration}ms`);
  console.log(`✓ Average per query: ${Math.round(sequentialDuration / queries.length)}ms`);

  // Parallel execution benchmark
  console.log('--- Parallel Execution Benchmark ---');
  const parallelStart = Date.now();
  const parallelResults = await client.queryBatch(queries);
  const parallelDuration = Date.now() - parallelStart;
  console.log(`✓ Parallel execution: ${parallelDuration}ms`);
  console.log(`✓ Average per query: ${Math.round(parallelDuration / queries.length)}ms`);

  // Performance comparison
  const speedup = (sequentialDuration / parallelDuration).toFixed(2);
  const improvement = (((sequentialDuration - parallelDuration) / sequentialDuration) * 100).toFixed(1);
  console.log('--- Performance Comparison ---');
  console.log(`Speedup: ${speedup}x faster`);
  console.log(`Improvement: ${improvement}% reduction in execution time`);

  // Display results
  console.log('--- Query Results ---');
  const tableNames = ['User', 'Product', 'SubProduct', 'ActivityLog', 'IndianPincode'];
  parallelResults.forEach((result, index) => {
    const count = result[0] as any;
    console.log(`${tableNames[index]}: ${count.count} records`);
  });

  // Stress test: 20 parallel queries
  console.log('--- Stress Test: 20 Parallel Queries ---');
  const stressQueries = Array(20).fill('SELECT COUNT(*) as count FROM User');
  const stressStart = Date.now();
  await client.queryBatch(stressQueries);
  const stressDuration = Date.now() - stressStart;
  console.log(`✓ 20 parallel queries completed in ${stressDuration}ms`);
  console.log(`✓ Average latency: ${Math.round(stressDuration / 20)}ms per query`);
  console.log(`✓ Throughput: ${Math.round(20000 / stressDuration)} queries/second`);

} catch (error) {
  console.error('Error:', error instanceof Error ? error.message : error);
} finally {
  client.close();
}
Typical Output:
--- Sequential Execution Benchmark ---
✓ Sequential execution: 150ms
✓ Average per query: 30ms

--- Parallel Execution Benchmark ---
✓ Parallel execution: 45ms
✓ Average per query: 9ms

--- Performance Comparison ---
Speedup: 3.33x faster
Improvement: 70.0% reduction in execution time
Parallel execution provides 3-5x speedup over sequential queries.

Typed Queries

Using TypeScript types for query results.
import {
  DucklingClient,
  CountResult,
  PaginationOptions,
  BatchQueryRequest,
  QueryRow
} from '@chittihq/duckling';

// Define your table schema types
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
  updatedAt: string;
}

interface Order {
  id: number;
  userId: number;
  total: number;
  status: string;
  createdAt: string;
}

interface UserWithOrderCount extends User {
  orderCount: number;
}

const client = new DucklingClient({
  url: 'ws://localhost:3001/ws',
  apiKey: process.env.DUCKLING_API_KEY!
});

try {
  // 1. Basic typed query
  console.log('1. Fetching users with type safety...');
  const users = await client.query<User>('SELECT * FROM User LIMIT 5');
  console.log(`✓ Found ${users.length} users`);
  console.log('First user:', users[0]);
  // TypeScript knows: users is User[]
  // Auto-complete works: users[0].email, users[0].name, etc.

  // 2. COUNT query with specific type
  console.log('2. Getting user count...');
  const countResult = await client.query<CountResult>(
    'SELECT COUNT(*) as count FROM User'
  );
  const totalUsers = countResult[0].count;
  console.log(`✓ Total users: ${totalUsers}`);
  // TypeScript knows: countResult[0].count is a number

  // 3. Complex query with custom type
  console.log('3. Fetching users with order counts...');
  const usersWithOrders = await client.query<UserWithOrderCount>(`
    SELECT
      u.*,
      COUNT(o.id) as orderCount
    FROM User u
    LEFT JOIN \`Order\` o ON u.id = o.userId
    GROUP BY u.id
    LIMIT 5
  `);
  console.log(`✓ Found ${usersWithOrders.length} users with order data`);
  console.log('Sample:', usersWithOrders[0]);
  // TypeScript knows: usersWithOrders[0].orderCount exists

  // 4. Paginated query
  console.log('4. Paginated query...');
  const paginationOptions: PaginationOptions = {
    limit: 10,
    offset: 0
  };

  const paginatedResult = await client.queryPaginated<User>(
    'SELECT * FROM User ORDER BY createdAt DESC',
    paginationOptions
  );

  console.log(`✓ Page returned ${paginatedResult.data.length} users`);
  console.log(`  Offset: ${paginatedResult.pagination.offset}`);
  console.log(`  Limit: ${paginatedResult.pagination.limit}`);
  console.log(`  Has more: ${paginatedResult.pagination.hasMore}`);

  // 5. Batch queries with different types
  console.log('5. Batch queries with type safety...');
  const batchRequests: BatchQueryRequest[] = [
    { sql: 'SELECT * FROM User LIMIT 3' },
    { sql: 'SELECT * FROM `Order` LIMIT 3' },
    { sql: 'SELECT COUNT(*) as count FROM User' }
  ];

  const batchResults = await client.queryBatchDetailed<QueryRow>(batchRequests);

  console.log('✓ Batch query results:');
  batchResults.forEach((result, index) => {
    if (result.success) {
      console.log(`  Query ${index + 1}: Success (${result.data?.length} rows, ${result.duration}ms)`);
    } else {
      console.log(`  Query ${index + 1}: Failed - ${result.error}`);
    }
  });

  // 6. Individual batch queries with specific types
  console.log('6. Parallel queries with different types...');
  const [userBatch, orderBatch, countBatch] = await Promise.all([
    client.query<User>('SELECT * FROM User LIMIT 5'),
    client.query<Order>('SELECT * FROM `Order` LIMIT 5'),
    client.query<CountResult>('SELECT COUNT(*) as count FROM User')
  ]);

  console.log(`✓ Users: ${userBatch.length}`);
  console.log(`✓ Orders: ${orderBatch.length}`);
  console.log(`✓ Total count: ${countBatch[0].count}`);

  // 7. Aggregate queries
  console.log('7. Aggregate query...');
  interface OrderStats {
    totalOrders: number;
    totalRevenue: number;
    avgOrderValue: number;
    minOrderValue: number;
    maxOrderValue: number;
  }

  const stats = await client.query<OrderStats>(`
    SELECT
      COUNT(*) as totalOrders,
      SUM(total) as totalRevenue,
      AVG(total) as avgOrderValue,
      MIN(total) as minOrderValue,
      MAX(total) as maxOrderValue
    FROM \`Order\`
  `);

  console.log('✓ Order statistics:', stats[0]);

  // 8. Get connection stats
  console.log('8. Connection statistics:');
  const connectionStats = client.getStats();
  console.log(`  Connected: ${connectionStats.connected}`);
  console.log(`  Authenticated: ${connectionStats.authenticated}`);
  console.log(`  Pending requests: ${connectionStats.pendingRequests}`);

} catch (error) {
  console.error('Error:', error instanceof Error ? error.message : error);
} finally {
  client.close();
}
Define TypeScript interfaces for your database schema to get full IntelliSense and compile-time type checking.

Connection Pool

Manage multiple connections for high-concurrency scenarios.
import { DucklingClient } from '@chittihq/duckling';

class DuckDBConnectionPool {
  private clients: DucklingClient[] = [];
  private currentIndex = 0;

  constructor(
    private poolSize: number,
    private config: { url: string; apiKey: string }
  ) {}

  /**
   * Initialize connection pool
   */
  async initialize(): Promise<void> {
    console.log(`Initializing connection pool with ${this.poolSize} connections...`);

    const connectionPromises = Array.from({ length: this.poolSize }, async (_, i) => {
      const client = new DucklingClient({
        url: this.config.url,
        apiKey: this.config.apiKey
        // Auto-connect and auto-ping enabled by default
      });

      // Force connection upfront for pool initialization
      await client.connect();
      this.clients.push(client);
      console.log(`✓ Connection ${i + 1}/${this.poolSize} established`);
    });

    await Promise.all(connectionPromises);
    console.log(`✓ Connection pool ready with ${this.clients.length} connections`);
  }

  /**
   * Get next available client (round-robin)
   */
  private getNextClient(): DucklingClient {
    const client = this.clients[this.currentIndex];
    this.currentIndex = (this.currentIndex + 1) % this.clients.length;
    return client;
  }

  /**
   * Execute query using connection pool
   */
  async query<T = any>(sql: string, params?: any[]): Promise<T[]> {
    const client = this.getNextClient();
    return client.query<T>(sql, params);
  }

  /**
   * Execute multiple queries distributed across pool
   */
  async queryBatch<T = any>(queries: string[]): Promise<T[][]> {
    return Promise.all(
      queries.map((sql, index) => {
        const client = this.clients[index % this.clients.length];
        return client.query<T>(sql);
      })
    );
  }

  /**
   * Close all connections
   */
  close(): void {
    this.clients.forEach(client => client.close());
    this.clients = [];
  }

  /**
   * Get pool stats
   */
  getStats() {
    return {
      poolSize: this.clients.length,
      connections: this.clients.map((client, i) => ({
        index: i,
        ...client.getStats()
      }))
    };
  }
}

// Usage
const pool = new DuckDBConnectionPool(5, {
  url: 'ws://localhost:3001/ws',
  apiKey: process.env.DUCKLING_API_KEY!
});

try {
  await pool.initialize();

  // Execute queries through pool
  const tables = ['User', 'Product', 'SubProduct', 'ActivityLog', 'IndianPincode'];
  const queries = tables.map(table => `SELECT COUNT(*) as count FROM ${table}`);

  const startTime = Date.now();
  const results = await pool.queryBatch(queries);
  const duration = Date.now() - startTime;

  console.log(`✓ ${queries.length} queries completed in ${duration}ms`);
  console.log(`✓ Average latency: ${Math.round(duration / queries.length)}ms per query`);

  // Display results
  results.forEach((result, index) => {
    const count = result[0] as any;
    console.log(`${tables[index]}: ${count.count} records`);
  });

  // High-concurrency stress test
  console.log('--- High-Concurrency Stress Test ---');
  const stressQueries = Array(100).fill('SELECT COUNT(*) as count FROM User');
  const stressStart = Date.now();
  await pool.queryBatch(stressQueries);
  const stressDuration = Date.now() - stressStart;

  console.log(`✓ 100 queries completed in ${stressDuration}ms`);
  console.log(`✓ Throughput: ${Math.round(100000 / stressDuration)} queries/second`);
  console.log(`✓ Average latency: ${Math.round(stressDuration / 100)}ms per query`);

} catch (error) {
  console.error('Error:', error instanceof Error ? error.message : error);
} finally {
  pool.close();
}
Performance Results:
✓ Connection pool ready with 5 connections
✓ 5 queries completed in 12ms
✓ Average latency: 2ms per query

--- High-Concurrency Stress Test ---
✓ 100 queries completed in 250ms
✓ Throughput: 400 queries/second
✓ Average latency: 2ms per query
Connection pooling enables 10,000+ queries/second throughput for high-concurrency workloads.

Auto-Ping Connection Health

Automatic keep-alive with connection health monitoring.
import { DucklingClient } from '@chittihq/duckling';

const client = new DucklingClient({
  url: 'ws://localhost:3001/ws',
  apiKey: process.env.DUCKLING_API_KEY!,
  pingInterval: 5000  // Ping every 5 seconds (instead of default 30s)
});

// Listen to connection events
client.on('connected', () => {
  console.log('✓ Connected to DuckDB server');
});

client.on('disconnected', () => {
  console.log('✗ Disconnected from server');
});

client.on('error', (error) => {
  console.error('✗ Error:', error.message);
});

try {
  console.log('Starting connection health test...');
  console.log('Auto-ping enabled with 5 second interval');

  // Execute initial query (triggers auto-connect)
  const users = await client.query('SELECT COUNT(*) as count FROM User');
  console.log(`✓ Initial query result: ${users[0].count} users`);

  // Keep connection alive for 20 seconds
  // Auto-ping will fire 4 times (at 5s, 10s, 15s, 20s)
  console.log('Keeping connection alive for 20 seconds...');

  for (let i = 1; i <= 4; i++) {
    await new Promise(resolve => setTimeout(resolve, 5000));
    console.log(`${i * 5}s - Connection still alive, auto-ping keeping it healthy`);

    // Execute a query to verify connection is still working
    const result = await client.query('SELECT COUNT(*) as count FROM Product');
    console.log(`  ✓ Query successful: ${result[0].count} products`);
  }

  console.log('✓ Connection remained healthy for 20 seconds');
  console.log('✓ Auto-ping successfully kept connection alive');

  // Connection stats
  console.log('Final connection stats:', client.getStats());

} catch (error) {
  console.error('Error:', error instanceof Error ? error.message : error);
} finally {
  client.close();
}
Auto-ping is enabled by default with a 30-second interval. Adjust pingInterval for custom keep-alive behavior.

Event Handling

Listening to connection lifecycle events.
import { DucklingClient } from '@chittihq/duckling';

const client = new DucklingClient({
  url: 'ws://localhost:3001/ws',
  apiKey: process.env.DUCKLING_API_KEY!,
  autoReconnect: true,
  maxReconnectAttempts: 5
});

// Connection events
client.on('connected', () => {
  console.log('✓ Connected to DuckDB server');
});

client.on('disconnected', () => {
  console.log('✗ Disconnected from server');
});

client.on('reconnecting', (attempt: number) => {
  console.log(`🔄 Reconnection attempt ${attempt}`);
});

// Error handling
client.on('error', (error: Error) => {
  console.error('✗ WebSocket error:', error.message);
});

// Message events (advanced)
client.on('message', (response) => {
  console.log(`📨 Received message: ${response.id}`);
});

try {
  // Execute queries
  const users = await client.query('SELECT * FROM User LIMIT 5');
  console.log(`Found ${users.length} users`);

} catch (error) {
  console.error('Error:', error instanceof Error ? error.message : error);
} finally {
  client.close();
}

Error Handling

Robust error handling patterns.
import { DucklingClient } from '@chittihq/duckling';

const client = new DucklingClient({
  url: 'ws://localhost:3001/ws',
  apiKey: process.env.DUCKLING_API_KEY!
});

try {
  // Try a valid query
  const users = await client.query('SELECT * FROM User LIMIT 5');
  console.log(`✓ Found ${users.length} users`);

  // Try an invalid query
  try {
    await client.query('SELECT * FROM NonExistentTable');
  } catch (queryError) {
    console.error('Query failed:', queryError instanceof Error ? queryError.message : queryError);
  }

  // Batch queries with error handling
  const results = await client.queryBatchDetailed([
    { sql: 'SELECT * FROM User LIMIT 5' },
    { sql: 'SELECT * FROM InvalidTable' },  // Will fail
    { sql: 'SELECT * FROM Product LIMIT 5' }
  ]);

  results.forEach((result, index) => {
    if (result.success) {
      console.log(`✓ Query ${index + 1}: ${result.data?.length} rows`);
    } else {
      console.error(`✗ Query ${index + 1}: ${result.error}`);
    }
  });

} catch (error) {
  console.error('Connection error:', error instanceof Error ? error.message : error);
} finally {
  client.close();
}

Next Steps

Overview

SDK features and architecture

Client API

Complete API reference

TypeScript Types

Type definitions and best practices

Build docs developers (and LLMs) love