Query Resultsets

      +

      Description — Couchbase Lite JavaScript — Working with Query Results
      Related Content — SQL++ for Mobile | Live Queries | Indexing

      Overview

      After executing a query, you work with the results through query resultsets. This page shows you how to iterate through results, access result values, and work with result data in different formats.

      Executing Queries

      Couchbase Lite JavaScript provides two ways to execute queries:

      Execute and Return Array

      The execute() method without arguments returns an array of all results:

      Example 1. Execute and return array
      const query = database.createQuery('SELECT * FROM tasks WHERE completed = false');
      const results = await query.execute();
      
      console.log(`Found ${results.length} incomplete tasks`);
      
      for (const row of results) {
          console.log(`Task: ${(row.tasks! as JSONObject).title}`);
      }

      Execute with Callback

      The execute(callback) method calls your callback function for each result row, which is more memory-efficient for large result sets:

      Example 2. Execute with callback
      const query = database.createQuery('SELECT * FROM tasks WHERE completed = false');
      
      let count = 0;
      await query.execute(row => {
          console.log(`Task ${++count}: ${(row.tasks! as JSONObject).title}`);
          // Process each row without storing all in memory
      });
      
      console.log(`Processed ${count} tasks total`);
      Use the callback pattern when processing large result sets to avoid loading all results into memory at once.

      Result Structure

      Each result row is a JavaScript object. The structure depends on your SELECT clause.

      Getting Column Names

      Use the columnNames property to get the names of result columns:

      Example 3. Access column names
      const query = database.createQuery(`
          SELECT title, completed, createdAt
          FROM tasks
      `);
      
      const columnNames = query.columnNames;
      console.log('Columns:', columnNames);  // ['title', 'completed', 'createdAt']

      SELECT * Results

      When you use SELECT *, results are nested under the collection name:

      Example 4. SELECT * structure
      const starQuery = database.createQuery('SELECT * FROM tasks');
      const starResults = await starQuery.execute();
      
      for (const row of starResults) {
          // Results nested under collection name
          const task = row.tasks! as JSONObject;  // Access via collection name
          console.log(task.title, task.completed);
      }

      SELECT Properties Results

      When you select specific properties, they appear as top-level properties in the result:

      Example 5. SELECT properties structure
      const propsQuery = database.createQuery(`
          SELECT title, completed
          FROM tasks
      `);
      const propsResults = await propsQuery.execute();
      
      for (const row of propsResults) {
          // Properties at top level
          console.log(row.title, row.completed);
      }

      Accessing Result Values

      Result values are accessed as regular JavaScript object properties:

      Example 6. Accessing values
      const query = database.createQuery('SELECT title, completed, priority FROM tasks');
      const results = await query.execute();
      
      for (const row of results) {
          const title = row.title;
          const completed = row.completed;
          const priority = row.priority;
          console.log(`${title}: ${completed ? 'Done' : 'Pending'} (Priority: ${priority})`);
      }

      Using Result Aliases

      Use the AS keyword to create aliases for result columns:

      Example 7. Result aliases
      const aliasQuery = database.createQuery(`
          SELECT title     AS taskName,
                 completed AS isDone,
                 createdAt AS created
          FROM tasks
      `);
      
      const results = await aliasQuery.execute();
      
      for (const row of results) {
          console.log(`${row.taskName} - Done: ${row.isDone}`);
      }

      Accessing Nested Properties

      When querying nested objects, the result structure reflects the query:

      Example 8. Nested properties
      const nestedQuery = database.createQuery(`
          SELECT title,
                 assignee.name,
                 assignee.email,
                 metadata.tags
          FROM tasks
      `);
      
      const results = await nestedQuery.execute();
      
      for (const row of results) {
          const tags = row.tags! as JSONArray;
          console.log(`Task: ${row.title}`);
          console.log(`Assigned to: ${row.name} (${row.email})`);
          console.log(`Tags: ${tags.join(', ')}`);
      }

      Collecting Results

      The execute() method without arguments returns all results as an array:

      Example 9. Collect all results
      const query = database.createQuery('SELECT * FROM tasks');
      const allResults = await query.execute();
      
      // Now you have an array of all results
      console.log(`Total tasks: ${allResults.length}`);
      
      // Can use array methods
      const completedTasks = allResults.filter(row => {
          const task = row.tasks! as JSONObject;
          return task.completed;
      });
      console.log(`Completed: ${completedTasks.length}`);

      For memory efficiency with large result sets, use the callback pattern instead:

      Example 10. Process results with callback
      const query = database.createQuery('SELECT * FROM tasks');
      
      const completed = [];
      await query.execute(row => {
          const task = row.tasks! as JSONObject;
          if (task.completed) {
              completed.push(task);
          }
          // Row is processed and can be garbage collected
      });
      
      console.log(`Found ${completed.length} completed tasks`);

      Counting Results

      To count results without collecting them all:

      Example 11. Count results
      const query = database.createQuery('SELECT COUNT(*) AS count FROM tasks WHERE completed = false');
      const results = await query.execute();
      
      const count = results[0].count;
      console.log(`Incomplete tasks: ${count}`);

      Working with Aggregate Results

      When using aggregate functions, results contain the aggregated values:

      Example 12. Aggregate results
      const aggQuery = database.createQuery(`
          SELECT COUNT(*)            AS total,
                 SUM(estimatedHours) AS totalHours,
                 AVG(priority)       AS avgPriority,
                 MIN(createdAt)      AS earliest,
                 MAX(createdAt)      AS latest
          FROM tasks
      `);
      
      const results = await aggQuery.execute();
      const stats = results[0];
      
      console.log(`Total: ${stats.total}`);
      console.log(`Total Hours: ${stats.totalHours}`);
      console.log(`Avg Priority: ${stats.avgPriority}`);

      GROUP BY Results

      GROUP BY queries return one result per group:

      Example 13. GROUP BY results
      const groupQuery = database.createQuery(`
          SELECT status,
                 COUNT(*) AS count,
          AVG(priority) AS avgPriority
          FROM tasks
          GROUP BY status
      `);
      
      const results = await groupQuery.execute();
      
      for (const row of results) {
          console.log(`${row.status}: ${row.count} tasks (Avg Priority: ${row.avgPriority})`);
      }

      JOIN Results

      JOIN queries combine data from multiple collections:

      Example 14. JOIN results
      const joinQuery = database.createQuery(`
          SELECT tasks.title,
                 tasks.status,
                 users.username,
                 users.email
          FROM tasks
                   LEFT OUTER JOIN users ON tasks.assignedTo = users._id
      `);
      
      const results = await joinQuery.execute();
      
      for (const row of results) {
          console.log(`Task: ${row.title}`);
          if (row.username) {
              console.log(`Assigned to: ${row.username} (${row.email})`);
          } else {
              console.log('Unassigned');
          }
      }

      Handling NULL and MISSING Values

      SQL++ distinguishes between NULL and MISSING values:

      • NULL - Explicitly set to null

      • MISSING - Property doesn’t exist

      Example 15. NULL and MISSING handling
      const query = database.createQuery(`
          SELECT title,
                 assignedTo,
                 dueDate,
                 IFNULL(assignedTo, 'unassigned')  AS assigned,
                 IFMISSING(dueDate, 'no due date') AS due
          FROM tasks
      `);
      
      const results = await query.execute();
      
      for (const row of results) {
          // NULL values appear as null
          if (row.assignedTo === null) {
              console.log('Task not assigned');
          }
      
          // MISSING values don't exist in the object
          if (!('dueDate' in row)) {
              console.log('No due date set');
          }
      }

      Converting Results to JSON

      You can convert result objects to JSON strings:

      Example 16. JSON conversion
      const query = database.createQuery('SELECT * FROM tasks LIMIT 10');
      const results = await query.execute();
      
      // Convert to JSON string
      const jsonString = JSON.stringify(results, null, 2);
      console.log(jsonString);
      
      // Or save to file/localStorage
      localStorage.setItem('taskResults', jsonString);

      TypeScript Result Types

      When using TypeScript, specify the result type using generics for type safety:

      Example 17. Typed results
      interface TaskResult {
          title: string;
          completed: boolean;
          priority: number;
          createdAt: string;
      }
      
      const query = database.createQuery(`
          SELECT title, completed, priority, createdAt
          FROM tasks
          WHERE completed = false
      `);
      
      const results = await query.execute<TaskResult>();
      
      // TypeScript knows the result type
      for (const row of results) {
          const title: string = row.title;  // Type-safe access
          const priority: number = row.priority;
      }

      The Query class is generic: Query<Schema> where Schema is inherited from its Database. You can also specify the result type when calling execute<T>():

      Example 18. Specify result type
      interface TaskSummary {
          taskName: string;
          isDone: boolean;
      }
      
      const query = database.createQuery(`
          SELECT title     AS taskName,
                 completed AS isDone
          FROM tasks
      `);
      
      const results = await query.execute<TaskSummary>();
      
      // Results are typed as TaskSummary[]
      results.forEach(task => {
          console.log(`${task.taskName}: ${task.isDone ? 'Done' : 'Pending'}`);
      });
      TypeScript result types are not runtime-checked. It’s your responsibility to ensure the type matches the actual query results.

      Result Processing Patterns

      Streaming Processing

      Process results as they arrive without storing them all in memory:

      Example 19. Streaming pattern
      const query = database.createQuery('SELECT * FROM tasks');
      
      let processedCount = 0;
      await query.execute(row => {
          // Process each row immediately
          processRow(row.tasks);
          processedCount++;
      
          // Row is not stored, can be garbage collected
      });
      
      console.log(`Streamed ${processedCount} results`);

      Filtering Results

      Filter results in your callback:

      Example 20. Filtering pattern
      const query = database.createQuery('SELECT * FROM tasks');
      
      const urgentTasks = [];
      await query.execute(row => {
          const task = row.tasks as JSONObject;
      
          // Only keep urgent tasks
          if (task.priority === 'high' && !task.completed) {
              urgentTasks.push(task);
          }
      });
      
      console.log(`Found ${urgentTasks.length} urgent tasks`);

      Transforming Results

      Transform result data during iteration:

      Example 21. Transformation pattern
      const query = database.createQuery(
          'SELECT * FROM tasks'
      );
      
      const taskSummaries = [];
      await query.execute(row => {
          const task = row.tasks as JSONObject;
      
          // Transform to summary format
          taskSummaries.push({
              id: task._id,
              title: task.title,
              status: task.completed ? 'Done' : 'Pending',
              daysOld: calculateDaysOld(task.createdAt as string)
          });
      });

      Asynchronous Processing

      The execute() method itself is asynchronous. If you use the callback pattern, your callback can also be asynchronous:

      Example 22. Async callback processing
      const query = database.createQuery(
          'SELECT * FROM tasks'
      );
      
      await query.execute(async (row) => {
          const task = row.tasks;
      
          // Can perform async operations
          await saveToCache(task);
          await updateUI(task);
      
          // Query waits for callback to complete
      });
      
      console.log('All results processed asynchronously');
      With async callbacks, the execute() method waits for each callback to complete before processing the next result.

      Error Handling

      Handle errors during query execution:

      Example 23. Error handling
      try {
          const query = database.createQuery(
              'SELECT * FROM tasks WHERE priority > $minPriority'
          );
          query.parameters = { minPriority: 5 };
          const results = await query.execute();
      
          console.log(`Found ${results.length} high priority tasks`);
      } catch (error) {
          if (error instanceof N1QLParseError) {
              console.error('Invalid query syntax:', error.message);
          } else if (error instanceof InterruptedQueryError) {
              console.error('Query was interrupted');
          } else {
              console.error('Query failed:', error);
          }
      }

      Performance Tips

      Process Results Efficiently:

      • Use the callback pattern to process results one at a time

      • Avoid collecting all results if you only need to process them

      • Use LIMIT to restrict result count when appropriate

      Memory Management:

      • Don’t store large result sets in memory unnecessarily

      • Process and release results as you go

      • Consider pagination for large datasets

      Query Optimization:

      • Select only the properties you need

      • Use indexes to speed up queries (see Indexing)

      • Check query explanations to verify index usage

      Controlling Result Iteration

      When using the callback pattern, execute() returns a boolean indicating whether the query completed:

      Example 24. Check if query completed
      const query = database.createQuery(
          'SELECT * FROM tasks'
      );
      
      let count = 0;
      const completed = await query.execute(row => {
          count++;
          // Process row
      });
      
      if (completed) {
          console.log(`Query completed successfully, processed ${count} rows`);
      } else {
          console.log('Query was interrupted');
      }

      Interrupting Queries

      You can stop a running query using the interrupt() method:

      Example 25. Interrupt a query
      const query = database.createQuery(
          'SELECT * FROM tasks'
      );
      
      // Start query execution
      const executePromise = query.execute(row => {
          // Long-running processing
          processRow(row);
      });
      
      // Interrupt after timeout
      setTimeout(() => {
          query.interrupt();
          console.log('Query interrupted');
      }, 5000);
      
      try {
          await executePromise;
      } catch (error) {
          if (error instanceof InterruptedQueryError) {
              console.log('Query was interrupted as expected');
          }
      }

      When interrupted, execute() throws an InterruptedQueryError.

      How to . . .

      .

      Dive Deeper . . .

      Mobile Forum | Blog | Tutorials

      .