Handling Data Conflicts

      +

      Description — Couchbase Lite JavaScript — Handling conflict between data changes
      Related Content — Remote Sync Gateway | CORS Configuration

      Causes of Conflicts

      Document conflicts can occur if multiple changes are made to the same version of a document by multiple peers in a distributed system. For Couchbase Mobile, this can be a Couchbase Lite or Sync Gateway database instance.

      Such conflicts can occur after either of the following events:

      Deletes always win. So, in either of the above cases, if one of the changes was a Delete then that change wins.

      The following sections discuss each scenario in more detail.

      Conflicts when Replicating

      There’s no practical way to prevent a conflict when incompatible changes to a document are made in multiple instances of an app. The conflict is realized only when replication propagates the incompatible changes to each other.

      Example 1. A typical cause of replication conflicts:
      1. Alice uses her device to create DocumentA.

      2. Replication syncs DocumentA to Bob’s device.

      3. Alice uses her device to apply ChangeX to DocumentA.

      4. Bob uses his device to make a different change, ChangeY, to DocumentA.

      5. Replication syncs ChangeY to Alice’s device.

        This device already has ChangeX putting the local document in conflict.

      6. Replication syncs ChangeX to Bob’s device.

        This device already has ChangeY and now Bob’s local document is in conflict.

      Automatic Conflict Resolution

      The rules only apply to conflicts caused by replication. Conflict resolution takes place exclusively during pull replication, while push replication remains unaffected.

      Couchbase Lite uses the following rules to handle conflicts such as those described in Example 1:

      • If one of the changes is a deletion:

        A deleted document (that is, a tombstone) always wins over a document update.

      • If both changes are document changes:

        The change with the most revisions wins. If both have the same number of revisions, a deterministic algorithm is used to pick a winner.

      The result is saved internally by the Couchbase Lite replicator. Those rules describe the internal behavior of the replicator. For additional control over the handling of conflicts, including when a replication is in progress, see Custom Conflict Resolution.

      Custom Conflict Resolution

      Application developers who want more control over how document conflicts are handled can use custom logic to select the winner between conflicting revisions of a document.

      If a custom conflict resolver is not provided, the system will automatically resolve conflicts as discussed in Automatic Conflict Resolution.

      Custom conflict handlers should be optimized and fast. Time-consuming conflict resolution can slow down the replication process significantly.

      To implement custom conflict resolution during replication, you create a conflict resolver function and configure it on the replicator.

      Conflict Resolver

      Apps have the following strategies for resolving conflicts:

      • Local Wins: The current revision in the database wins.

      • Remote Wins: The revision pulled from the remote endpoint through replication wins.

      • Merge: Merge the content of the conflicting revisions.

      Example 2. Conflict Resolution Strategies
      • Local Wins

      • Remote Wins

      • Merge

      • Delete

      const localWinsResolver: PullConflictResolver = async (local, remote) => {
          return local;
      };
      const remoteWinsResolver: PullConflictResolver = async (local, remote) => {
          return remote;
      };
          const mergeResolver: PullConflictResolver = async (local, remote) => {
              if (local && remote) {
                  return { ...local, ...remote } as CBLDocument;
              } else {
                  return local ?? remote;
              }
          };
      
          const deleteResolver: PullConflictResolver = async (local, remote) => {
              return null;
          };
      
          // tag::conflict-resolver-config
          const config: ReplicatorConfig = {
              database: database as unknown as Database,
              url: 'wss://sync-gateway.example.com:4984/myapp',
              collections: {
                  tasks: {
                      pull: { conflictResolver: remoteWinsResolver, continuous: true },
                      push: { continuous: true}
                  }
              }
          };
          // end::conflict-resolver-config
      }
      
      {
          // Configure logging
          await logtape.configure({
              sinks: {
                  console: logtape.getConsoleSink(),
              },
              loggers: [
                  {
                      category: LogCategory,
                      lowestLevel: 'info',
                      sinks: ['console'],
                  }
              ],
          });
      }
      
      {
          // Configure logging for database operations
          await logtape.configure({
              sinks: {
                  console: logtape.getConsoleSink(),
              },
              loggers: [
                  {
                      category: [LogCategory, 'DB'],
                      lowestLevel: 'debug',
                      sinks: ['console'],
                  }
              ],
          });
      }
      
      {
          // Configure logging for queries
          await logtape.configure({
              sinks: {
                  console: logtape.getConsoleSink(),
              },
              loggers: [
                  {
                      category: [LogCategory, 'Query'],
                      lowestLevel: 'debug',
                      sinks: ['console'],
                  }
              ],
          });
      }
      
      {
          // Declare indexes when opening database
          const config: DatabaseConfig = {
              name: 'myapp',
              version: 1,
              collections: {
                  products: {
                      indexes: ['name', 'price', 'category']
                  }
              }
          };
      
          const db = await Database.open(config);
      }
      
      {
          // Index individual properties
          const productConfig: DatabaseConfig = {
              name: 'store',
              version: 1,
              collections: {
                  products: {
                      indexes: [
                          'sku',        // Product SKU
                          'price',      // Product price
                          'inStock'     // Stock status
                      ]
                  }
              }
          };
      }
      
      {
          // Index array properties
          const articleConfig: DatabaseConfig = {
              name: 'blog',
              version: 1,
              collections: {
                  articles: {
                      indexes: [
                          'tags',       // Array of tags
                          'categories'  // Array of categories
                      ]
                  }
              }
          };
      }
      
      {
          // Index nested object properties using dot notation
          const userConfig: DatabaseConfig = {
              name: 'app',
              version: 1,
              collections: {
                  users: {
                      indexes: [
                          'profile.name',
                          'profile.email',
                          'address.city',
                          'address.country'
                      ]
                  }
              }
          };
      }
      
      {
          // Create multiple indexes for complex queries
          const orderConfig: DatabaseConfig = {
              name: 'orders',
              version: 1,
              collections: {
                  orders: {
                      indexes: [
                          'customerId',     // Filter by customer
                          'status',         // Filter by status
                          'orderDate',      // Sort by date
                          'totalAmount'     // Sort/filter by amount
                      ]
                  }
              }
          };
      }
      
      {
          // Define schema with TypeScript
          interface Product {
              sku: string;
              name: string;
              price: number;
              category: string;
              tags: string[];
          }
      
          interface StoreSchema {
              products: Product;
          }
      
          // Configure with type-safe indexes
          const storeConfig: DatabaseConfig<StoreSchema> = {
              name: 'store',
              version: 1,
              collections: {
                  products: {
                      indexes: ['sku', 'price', 'category', 'tags']
                  }
              }
          };
      
          const storeDb = await Database.open(storeConfig);
      }
      
      {
          // Close database first
          await database.close();
      
          // Reopen with new indexes
          const updatedConfig: DatabaseConfig = {
              name: 'myapp',
              version: 2,  // Increment version
              collections: {
                  products: {
                      indexes: [
                          'name',
                          'price',
                          'category',
                          'manufacturer'  // New index added
                      ]
                  }
              }
          };
      
          const updatedDb = await Database.open(updatedConfig);
      }
      
      {
          // Store dates as ISO strings or timestamps
          interface Event {
              title: string;
              startDate: string;      // ISO date string: "2024-01-15T10:00:00Z"
              timestamp: number;      // Unix timestamp: 1705315200000
          }
      
          const eventConfig: DatabaseConfig = {
              name: 'events',
              version: 1,
              collections: {
                  events: {
                      indexes: [
                          'startDate',    // Index ISO date string
                          'timestamp'     // Index numeric timestamp
                      ]
                  }
              }
          };
      
          const database = await Database.open(eventConfig);
      }
      
      {
          // High selectivity: many unique values
          const emailConfig: DatabaseConfig = {
              name: 'users',
              version: 1,
              collections: {
                  users: {
                      indexes: [
                          'email',      // Unique per user - excellent selectivity
                          'userId',     // Unique - excellent selectivity
                          'ssn'         // Unique - excellent selectivity
                      ]
                  }
              }
          };
      
          const database = await Database.open(emailConfig);
      }
      
      {
          // Low selectivity: few unique values
          const taskConfig: DatabaseConfig = {
              name: 'tasks',
              version: 1,
              collections: {
                  tasks: {
                      // Not recommended: boolean has only 2 possible values
                      indexes: ['completed']  // Low selectivity
                  }
              }
          };
      
          const database = await Database.open(taskConfig);
      
          // Better: combine with high-selectivity queries
          const query = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE completed = false AND assignedTo = $userId -- High selectivity filter
          `);
      }
      
      {
          // Check IndexedDB storage usage
          if (navigator.storage && navigator.storage.estimate) {
              const {quota = 0, usage = 0} = await navigator.storage.estimate();
              const quotaGB = (quota / (1024 ** 3)).toFixed(2);
              const usageGB = (usage / (1024 ** 3)).toFixed(2);
              const percentUsed = ((usage / quota) * 100).toFixed(1);
      
              console.log(`Storage: ${usageGB} GB / ${quotaGB} GB (${percentUsed}%)`);
          }
      }
      
      {
          // Query that uses indexes efficiently
          const indexedQuery = database.createQuery(`
              SELECT *
              FROM products
              WHERE category = 'electronics' -- Uses category index
              ORDER BY price -- Uses price index
              LIMIT 50
          `);
      
          const results = await indexedQuery.execute();
      }
      
      {
          // Query without index - scans entire collection
          const unindexedQuery = database.createQuery(`
              SELECT *
              FROM products
              WHERE description LIKE '%wireless%' -- No index on description
          `);
      
          // This will be slow on large datasets
          const slowResults = await unindexedQuery.execute();
      }
      
      {
          // Use LIMIT for pagination and performance
          const limitedQuery = database.createQuery(`
              SELECT *
              FROM products
              WHERE category = $category
              ORDER BY price DESC LIMIT 20
              OFFSET $offset
          `);
      
          // Fetch first page
          limitedQuery.parameters = {
              category: 'electronics',
              offset: 0
          };
          const page1 = await limitedQuery.execute();
      
          // Fetch next page
          limitedQuery.parameters = {
              category: 'electronics',
              offset: 20
          };
          const page2 = await limitedQuery.execute();
      }
      
      {
          // Email/username lookup pattern
          const authConfig: DatabaseConfig = {
              name: 'auth',
              version: 1,
              collections: {
                  users: {
                      indexes: ['email', 'username']
                  }
              }
          };
      
          // Fast lookup by email
          const userQuery = database.createQuery(`
              SELECT *
              FROM users
              WHERE email = $email
          `);
      
          userQuery.parameters = {email: 'user@example.com'};
          const user = await userQuery.execute();
      }
      
      {
          // Status filtering pattern
          const workflowConfig: DatabaseConfig = {
              name: 'workflow',
              version: 1,
              collections: {
                  tasks: {
                      indexes: ['status', 'priority', 'assignedTo']
                  }
              }
          };
      
          // Query by status
          const activeTasksQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE status IN ('pending', 'in-progress')
                AND assignedTo = $userId
              ORDER BY priority DESC
          `);
      }
      
      {
          // Date range query pattern
          const analyticsConfig: DatabaseConfig = {
              name: 'analytics',
              version: 1,
              collections: {
                  events: {
                      indexes: ['timestamp', 'eventType']
                  }
              }
          };
      
          // Query date range
          const dateRangeQuery = database.createQuery(`
              SELECT *
              FROM events
              WHERE timestamp >= $startDate
                AND timestamp <= $endDate
              ORDER BY timestamp DESC
          `);
      
          dateRangeQuery.parameters = {
              startDate: Date.now() - (7 * 24 * 60 * 60 * 1000),  // 7 days ago
              endDate: Date.now()
          };
          const recentEvents = await dateRangeQuery.execute();
      }
      
      {
          // Category filtering pattern
          const catalogConfig: DatabaseConfig = {
              name: 'catalog',
              version: 1,
              collections: {
                  products: {
                      indexes: ['category', 'subcategory', 'brand']
                  }
              }
          };
      
          // Hierarchical category query
          const categoryQuery = database.createQuery(`
              SELECT *
              FROM products
              WHERE category = $category
                AND subcategory = $subcategory
              ORDER BY name
          `);
      
          categoryQuery.parameters = {
              category: 'electronics',
              subcategory: 'laptops'
          };
          const laptops = await categoryQuery.execute();
      }
      
      {
          // Performance testing pattern
          async function testQueryPerformance() {
              // Test without index
              const startUnindexed = performance.now();
              const unindexedResults = await database.createQuery(`
                  SELECT *
                  FROM products
                  WHERE description LIKE '%test%'
              `).execute();
              const timeUnindexed = performance.now() - startUnindexed;
      
              console.log(`Unindexed query: ${timeUnindexed.toFixed(2)}ms`);
              console.log(`Results: ${unindexedResults.length}`);
      
              // Test with index
              const startIndexed = performance.now();
              const indexedResults = await database.createQuery(`
                  SELECT *
                  FROM products
                  WHERE category = 'electronics'
              `).execute();
              const timeIndexed = performance.now() - startIndexed;
      
              console.log(`Indexed query: ${timeIndexed.toFixed(2)}ms`);
              console.log(`Results: ${indexedResults.length}`);
              console.log(`Speedup: ${(timeUnindexed / timeIndexed).toFixed(1)}x`);
          }
      }
      
      {
          // Migration strategy for adding indexes
          async function migrateIndexes() {
              // Step 1: Check current version
              const currentDb = await Database.open({
                  name: 'myapp',
                  version: 1,
                  collections: {
                      products: {
                          indexes: ['name']
                      }
                  }
              });
      
              console.log('Current version:', currentDb.config.version);
      
              // Step 2: Close database
              await currentDb.close();
      
              // Step 3: Reopen with new indexes and incremented version
              const migratedDb = await Database.open({
                  name: 'myapp',
                  version: 2,
                  collections: {
                      products: {
                          indexes: [
                              'name',
                              'price',      // New index
                              'category'    // New index
                          ]
                      }
                  }
              });
      
              console.log('Migrated to version:', migratedDb.config.version);
      
              return migratedDb;
          }
      
          await migrateIndexes();
      }
      
      {
          const query = database.createQuery('SELECT * FROM tasks');
          const results = await query.execute();
      
          const allQuery = database.createQuery('SELECT * FROM tasks');
          const allResults = await allQuery.execute();
      
          const propsQuery = database.createQuery(`
              SELECT title, completed, createdAt
              FROM tasks
          `);
          const propResults = await propsQuery.execute();
      
          // Simple WHERE clause
          const whereQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE completed = false
          `);
      
          // Multiple conditions
          const multiWhereQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE completed = false
                AND type = 'urgent'
          `);
      
          // Using AND
          const andQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE completed = false
                AND priority = 'high'
          `);
      
          // Using OR
          const orQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE priority = 'high'
                 OR priority = 'critical'
          `);
      
          // Using NOT
          const notQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE NOT completed
          `);
      
          // LIKE with wildcards
          const likeQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE title LIKE 'Learn%'
          `);
      
          // Contains pattern
          const containsQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE title LIKE '%Couchbase%'
          `);
      
          // Single character wildcard
          const singleQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE title LIKE 'Task_'
          `);
      
          // Regular expression matching
          const regexQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE REGEXP_LIKE(title, '^[A-Z].*')
          `);
      
          // Check if contains pattern
          const regexContainsQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE REGEXP_CONTAINS(description, 'important|urgent')
          `);
      
          // Order ascending
          const ascQuery = database.createQuery(`
              SELECT *
              FROM tasks
              ORDER BY createdAt ASC
          `);
      
          // Order descending
          const descQuery = database.createQuery(`
              SELECT *
              FROM tasks
              ORDER BY createdAt DESC
          `);
      
          // Multiple order columns
          const multiOrderQuery = database.createQuery(`
              SELECT *
              FROM tasks
              ORDER BY priority DESC, createdAt ASC
          `);
      
          // Limit results
          const limitQuery = database.createQuery(`
              SELECT *
              FROM tasks LIMIT 10
          `);
      
          // Pagination with LIMIT and OFFSET
          const pageQuery = database.createQuery(`
              SELECT *
              FROM tasks
              ORDER BY createdAt DESC LIMIT 20
              OFFSET 40
          `);
      
          // LEFT OUTER JOIN
          const joinQuery = database.createQuery(`
              SELECT tasks.title, users.username
              FROM tasks
                       LEFT OUTER JOIN users ON tasks.assignedTo = users._id
              WHERE tasks.completed = false
          `);
      
          const joinResults = await joinQuery.execute();
      
          // GROUP BY with aggregate function
          const groupQuery = database.createQuery(`
              SELECT status, COUNT(*) AS count
              FROM tasks
              GROUP BY status
          `);
      
          // GROUP BY with HAVING clause
          const havingQuery = database.createQuery(`
              SELECT assignedTo, COUNT(*) AS taskCount
              FROM tasks
              GROUP BY assignedTo
              HAVING COUNT(*) > 5
          `);
      
          // Array functions
          const arrayQuery = database.createQuery(`
              SELECT title,
                     ARRAY_LENGTH(tags)             AS tagCount,
                     ARRAY_CONTAINS(tags, 'urgent') AS isUrgent
              FROM tasks
              WHERE ARRAY_LENGTH(tags) > 0
          `);
      
          // Array aggregation
          const arrayAggQuery = database.createQuery(`
              SELECT category,
                     ARRAY_AGG(title) AS titles
              FROM tasks
              GROUP BY category
          `);
      
          // String manipulation
          const stringQuery = database.createQuery(`
              SELECT UPPER(title)  AS upperTitle,
                     LOWER(title)  AS lowerTitle,
                     LENGTH(title) AS titleLength,
                     TRIM(title)   AS trimmedTitle
              FROM tasks
          `);
      
          // String concatenation
          const concatQuery = database.createQuery(`
              SELECT title || ' - ' || status AS fullTitle
              FROM tasks
          `);
      
          // Date functions
          const dateQuery = database.createQuery(`
              SELECT title,
                     STR_TO_MILLIS(createdAt) AS timestamp,
              MILLIS_TO_STR(STR_TO_MILLIS(createdAt)) AS formattedDate
              FROM tasks
          `);
      
          // Date arithmetic
          const dateRangeQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE STR_TO_MILLIS(createdAt) > STR_TO_MILLIS($startDate)
                AND STR_TO_MILLIS(createdAt) < STR_TO_MILLIS($endDate)
          `);
      
          // Mathematical functions
          const mathQuery = database.createQuery(`
              SELECT title,
                     ROUND(progress * 100) AS percentage,
                     ABS(budget - spent)   AS difference,
                     CEIL(duration / 24)   AS days,
                     FLOOR(duration / 24)  AS wholeDays
              FROM tasks
          `);
      
          // Using POWER and SQRT
          const advMathQuery = database.createQuery(`
              SELECT title,
                     POWER(priority, 2)   AS prioritySquared,
                     SQRT(estimatedHours) AS sqrtHours
              FROM tasks
          `);
      
          // Type checking
          const typeCheckQuery = database.createQuery(`
              SELECT title,
                     TYPE(metadata)     AS metadataType,
                     ISSTRING(title)    AS isTitleString,
                     ISNUMBER(priority) AS isPriorityNumber,
                     ISARRAY(tags)      AS isTagsArray
              FROM tasks
          `);
      
          // Type conversion
          const typeConvertQuery = database.createQuery(`
              SELECT TOSTRING(priority)       AS priorityString,
                     TONUMBER(estimatedHours) AS hours,
                     TOBOOLEAN(completed)     AS isDone
              FROM tasks
          `);
      
          // Handling NULL and MISSING
          const conditionalQuery = database.createQuery(`
              SELECT title,
                     IFNULL(assignedTo, 'unassigned')              AS assigned,
                     IFMISSING(dueDate, 'no due date')             AS due,
                     IFMISSINGORNULL(completedAt, 'not completed') AS completion
              FROM tasks
          `);
      
          // NULLIF and MISSINGIF
          const nullifQuery = database.createQuery(`
              SELECT title,
                     NULLIF(status, 'unknown') AS validStatus,
                     MISSINGIF(progress, 0)    AS nonZeroProgress
              FROM tasks
          `);
      
          // Simple CASE expression
          const caseQuery = database.createQuery(`
              SELECT title,
                     CASE priority
                         WHEN 1 THEN 'Low'
                         WHEN 2 THEN 'Medium'
                         WHEN 3 THEN 'High'
                         ELSE 'Unknown'
                         END AS priorityLabel
              FROM tasks
          `);
      
          // Searched CASE expression
          const searchedCaseQuery = database.createQuery(`
              SELECT title,
                     CASE
                         WHEN completed THEN 'Done'
                         WHEN dueDate < $now THEN 'Overdue'
                         WHEN dueDate < $soon THEN 'Due Soon'
                         ELSE 'On Track'
                         END AS taskStatus
              FROM tasks
          `);
      
          // Access document metadata
          const metaQuery = database.createQuery(`
              SELECT META().id       AS docId,
                     META().sequence AS seq,
                     title,
                     completed
              FROM tasks
          `);
      
          // Filter by document ID
          const idQuery = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE META().id = $docId
          `);
      }
      
      {
          // Execute and return array of all results
          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 for memory efficiency
          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`);
      }
      
      {
          // Get column names from query
          const query = database.createQuery(`
              SELECT title, completed, createdAt
              FROM tasks
          `);
      
          const columnNames = query.columnNames;
          console.log('Columns:', columnNames);  // ['title', 'completed', 'createdAt']
      }
      
      {
          // 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 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
          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 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
          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(', ')}`);
          }
      }
      
      {
          // Collect all results as array
          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}`);
      }
      
      {
          // Process with callback for memory efficiency
          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`);
      }
      
      {
          // Count results efficiently
          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}`);
      }
      
      {
          // Aggregate functions
          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
          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
          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
          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');
              }
          }
      }
      
      {
          // Convert results to JSON
          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);
      }
      
      {
          // Type-safe query results with TypeScript
          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;
          }
      }
      
      {
          // Specify result type on execute
          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'}`);
          });
      }
      
      {
          // Process results as stream without storing all
          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`);
      }
      
      {
          // Filter results in callback
          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`);
      }
      
      {
          // Transform results during iteration
          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)
              });
          });
      }
      
      {
          // Async callback for 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');
      }
      
      {
          // Error handling for queries
          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);
              }
          }
      }
      
      {
          // 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');
          }
      }
      
      {
          // Interrupt a running 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');
              }
          }
      }
      
      // Working with blobs in results
      // This is not supported yet.
      /*
      const query = database.createQuery(`
          SELECT title, attachment
          FROM documents
          WHERE attachment IS NOT NULL
      `);
      
      const results = await query.execute();
      
      for (const row of results) {
          if (row.attachment) {
              // Get blob data
              const blob = row.attachment;
              const url = blob.getURL();  // Get URL for display
              const data = await blob.getArrayBuffer();  // Get binary data
      
              console.log(`Document: ${row.title}`);
              console.log(`Attachment size: ${blob.length} bytes`);
          }
      }
      */
      
      {
          // Type-safe query with TypeScript
          interface TaskResult {
              title: string;
              completed: boolean;
              priority: number;
          }
      
          // Create parameterized query
          let query = database.createQuery(`
              SELECT *
              FROM tasks
              WHERE completed = $completed
                AND priority >= $minPriority
              ORDER BY createdAt DESC
          `);
      
          // Set parameters using the parameters property
          query.parameters = {
              completed: false,
              minPriority: 3
          };
      
          // Execute with parameters
          const results = await query.execute();
          console.log(`Found ${results.length} high priority incomplete tasks`);
      
          const taskResults = await query.execute<TaskResult>();
      
          // TypeScript provides full type checking
          taskResults.forEach((task: TaskResult) => {
              console.log(`${task.title} - Priority: ${task.priority}`);
          });
      }
      
      function processRow(task: any) {
          // Process task
      }
      
      function calculateDaysOld(date: string): number {
          const created = new Date(date);
          const now = new Date();
          return Math.floor((now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24));
      }
      
      async function saveToCache(task: any) {
          // Save to cache
      }
      
      async function updateUI(task: any) {
          // Update UI
      }
      
      {
          // Working with blobs
          // Create the blob from a File object
          const fileInput = document.getElementById('avatarFile') as HTMLInputElement;
          if (!fileInput?.files || fileInput.files.length === 0) {
              throw new Error('Please choose an image file first.');
          }
          const file = fileInput.files[0];
          const buffer = new Uint8Array(await file.arrayBuffer());
          const blob = new NewBlob(buffer, 'image/jpeg'); (1)
      
          // Add the blob to a document
          const user: CBLDictionary = {
              type: 'user',
              username: 'jane',
              email: 'jane@email.com',
              role: 'engineer',
              avatar: blob
          };
      
          // Create a document and save
          const users = database.getCollection('users');
          const doc = users.createDocument(DocID("profile1"), user);
          const saved = await users.save(doc);
      }
      
      {
          // Create blob from file input
          const fileInput = document.getElementById('avatarFile') as HTMLInputElement;
          if (!fileInput?.files || fileInput.files.length === 0) {
              throw new Error('Please choose an image file first.');
          }
          const file = fileInput.files[0];
          const buffer = new Uint8Array(await file.arrayBuffer());
          const blob = new NewBlob(buffer, 'image/jpeg');
      }
      
      {
          // Fetch remote binary data
          const response = await fetch('https://couchbase-example.com/images/avatar.jpg');
          if (!response.ok) {
              throw new Error(`Failed to fetch image: ${response.status}`);
          }
      
          // Convert the response into binary data
          const data = await response.arrayBuffer();
      
          // Create a blob from the downloaded data
          const buffer = new Uint8Array(await response.arrayBuffer());
          const blob = new NewBlob(buffer, 'image/jpeg');
      }
      
      {
          const users = database.getCollection('users');
          const doc = await users.getDocument(DocID("profile1"));
          if (doc) {
              // Note: Import as { Blob as CBLBlob } from ‘@couchbase/lite-js’ to
              // avoid conflict with the standard Blob type.
              const blob = doc.avatar as CBLBlob;
              if (blob instanceof CBLBlob) {
                  const contentType = blob.content_type ?? "None";
                  const length = blob.length ?? 0;
                  const digest = blob.digest;
                  console.log(`Blob info:
                      Content-Type: ${contentType}
                      Length: ${length} bytes
                      Digest: ${digest}`
                  );
                  const content = await blob.getContents();
                  // Use content as needed
              }
          } else {
              console.log('User not found');
          }
      }
      
      {
          // Note: Import as { Blob as CBLBlob } from ‘@couchbase/lite-js’
          // to avoid conflict with the standard Blob type.
          interface User {
              type: 'user';
              username: string;
              email: string;
              role: string;
              avatar: CBLBlob | null;
          }
      
          const users = database.collections.users;
          const doc = await users.getDocument(DocID("profile1"));
          if (doc) {
              const blob = doc.avatar
              if (blob) {
                  const content = await blob.getContents();
                  // Use content as needed
              }
          }
      }
      const deleteResolver: PullConflictResolver = async (local, remote) => {
          return null;
      };

      When null is returned by the resolver, the conflict is resolved as a document deletion.

      Conflicts when Saving

      When updating a document, you need to consider the possibility of update conflicts. Update conflicts can occur when you try to update a document that’s been updated since you read it.

      Example 3. How Updating May Cause Conflicts

      Here’s a typical sequence of events that would create an update conflict:

      1. Your code reads the document’s current properties, and constructs a modified copy to save.

      2. Another thread (perhaps the replicator) updates the document, creating a new revision with different properties.

      3. Your code updates the document with its modified properties using the save operation.

      Automatic Conflict Resolution

      In Couchbase Lite, by default, the conflict is automatically resolved and only one document update is stored in the database. The Last-Write-Win (LWW) algorithm is used to pick the winning update. So in effect, the changes from step 2 would be overwritten and lost.

      If the probability of update conflicts is high in your app and you wish to avoid the possibility of overwritten data, you can use a custom save handler with concurrency control.

      Save with Conflict Handler

      Implement a conflict handler when saving documents to handle conflicts during save operations:

      Example 4. Save with Conflict Handler
      // Use my changes
      await tasks.save(doc, (_mine, _theirs /* conflicting */) => {
          return 'replace';
      });
      
      // Discard my change
      await tasks.save(doc, (_mine, _theirs /* conflicting */) => {
          return 'revert';
      });
      
      // Abort with an error
      await tasks.save(doc, (_mine, _theirs /* conflicting */) => {
          return 'fail';
      });

      The conflict handler receives:

      • document - The document being saved

      • conflicting - The current document in the database (that conflicts)

      The handler should return:

      • The resolved document to save

      • null to cancel the save operation

      Custom Merge on Save

      Implement property-level merging when saving:

      Example 5. Custom merge on save
      // Use my changes
      await tasks.save(doc, (mine, theirs /* conflicting */) => {
          // Delete always wins
          if (!theirs) return 'revert';
      
          // Custom merge
          mine.title = `${mine.title} and ${theirs.title}`;
          mine.completed = !!(mine.completed && theirs.completed);
          mine.priority = Math.min(mine.priority, theirs.priority);
          mine.createdAt = new Date().toISOString();
          return 'replace';
      });

      How to . . .

      .

      Dive Deeper . . .

      Mobile Forum | Blog | Tutorials

      .