applyChangeset method

Future<int> applyChangeset(
  1. Uint8List bytes, {
  2. ChangesetConflictHandler? onConflict,
})

Apply a previously-recorded changeset blob to this database. Returns the number of changes applied (skipped/aborted changes are not counted). Pass onConflict to decide what to do when a row is missing for UPDATE/DELETE or already present for INSERT — by default conflicts are silently skipped.

Implementation

Future<int> applyChangeset(
  Uint8List bytes, {
  ChangesetConflictHandler? onConflict,
}) async {
  final changes = Session.decode(bytes);
  final handler = onConflict ?? (_, __) => ConflictResolution.skip;
  var applied = 0;
  return _lock.write(() async {
    for (final c in changes) {
      final t = _tables[c.table] ??
          _tables[c.table.toLowerCase()] ??
          _tables[c.table.toUpperCase()];
      if (t == null) {
        final res = handler(c, ConflictKind.notFound);
        if (res == ConflictResolution.abort) return applied;
        continue;
      }
      // Map recorded column order -> current column order.
      int? colIdxIn(List<String> cols, String name) {
        for (var i = 0; i < cols.length; i++) {
          if (cols[i].toLowerCase() == name.toLowerCase()) return i;
        }
        return null;
      }

      List<Object?> projectToCurrent(List<Object?> recorded) {
        final out = List<Object?>.filled(t.columns.length, null);
        for (var i = 0; i < t.columns.length; i++) {
          final src = colIdxIn(c.columns, t.columns[i].name);
          if (src != null && src < recorded.length) out[i] = recorded[src];
        }
        return out;
      }

      int? findRow(List<Object?> recorded) {
        if (c.pkColumns.isEmpty) {
          // Fall back to whole-row equality on the recorded columns.
          for (var ri = 0; ri < t.rows.length; ri++) {
            var match = true;
            for (var k = 0; k < c.columns.length; k++) {
              final ti = colIdxIn([
                for (final col in t.columns) col.name,
              ], c.columns[k]);
              if (ti == null) {
                match = false;
                break;
              }
              if (t.rows[ri][ti] != recorded[k]) {
                match = false;
                break;
              }
            }
            if (match) return ri;
          }
          return null;
        }
        // Locate by primary-key columns.
        final pkIdxRecorded = [
          for (final p in c.pkColumns) colIdxIn(c.columns, p),
        ];
        if (pkIdxRecorded.contains(null)) return null;
        final pkIdxTable = [for (final p in c.pkColumns) t.columnIndex(p)];
        for (var ri = 0; ri < t.rows.length; ri++) {
          var match = true;
          for (var k = 0; k < c.pkColumns.length; k++) {
            if (t.rows[ri][pkIdxTable[k]] != recorded[pkIdxRecorded[k]!]) {
              match = false;
              break;
            }
          }
          if (match) return ri;
        }
        return null;
      }

      switch (c.op) {
        case 'INSERT':
          final candidate = projectToCurrent(c.newValues!);
          if (findRow(c.newValues!) != null) {
            final res = handler(c, ConflictKind.notUnique);
            if (res == ConflictResolution.abort) return applied;
            if (res == ConflictResolution.replace) {
              final ri = findRow(c.newValues!)!;
              t.rows[ri] = candidate;
              _rebuildIndexes(t);
              applied++;
            }
            continue;
          }
          t.insertRow(candidate);
          _rebuildIndexes(t);
          applied++;
          break;
        case 'DELETE':
          final ri = findRow(c.oldValues!);
          if (ri == null) {
            final res = handler(c, ConflictKind.notFound);
            if (res == ConflictResolution.abort) return applied;
            continue;
          }
          t.rows.removeAt(ri);
          _rebuildIndexes(t);
          applied++;
          break;
        case 'UPDATE':
          final ri = findRow(c.oldValues!);
          if (ri == null) {
            final res = handler(c, ConflictKind.notFound);
            if (res == ConflictResolution.abort) return applied;
            continue;
          }
          t.rows[ri] = projectToCurrent(c.newValues!);
          _rebuildIndexes(t);
          applied++;
          break;
        default:
          // unknown op — treat as conflict
          final res = handler(c, ConflictKind.data);
          if (res == ConflictResolution.abort) return applied;
      }
    }
    await _persist();
    return applied;
  });
}