From 3bfe6f2a48d5f1e3b72e813691a91bab277e06a0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 28 Apr 2025 15:45:21 +0200 Subject: [PATCH 1/5] Add new schema options --- packages/powersync_core/lib/src/crud.dart | 34 ++++++- packages/powersync_core/lib/src/schema.dart | 84 +++++++++++++++-- packages/powersync_core/test/crud_test.dart | 92 +++++++++++++++++++ packages/powersync_core/test/schema_test.dart | 70 ++++++++++++-- 4 files changed, 261 insertions(+), 19 deletions(-) diff --git a/packages/powersync_core/lib/src/crud.dart b/packages/powersync_core/lib/src/crud.dart index 3cf6afaf..1be8c677 100644 --- a/packages/powersync_core/lib/src/crud.dart +++ b/packages/powersync_core/lib/src/crud.dart @@ -3,6 +3,8 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:powersync_core/sqlite3_common.dart' as sqlite; +import 'schema.dart'; + /// A batch of client-side changes. class CrudBatch { /// List of client-side changes. @@ -68,6 +70,14 @@ class CrudEntry { /// ID of the changed row. final String id; + /// An optional metadata string attached to this entry at the time the write + /// has been issued. + /// + /// For tables where [Table.includeMetadata] is enabled, a hidden `_metadata` + /// column is added to this table that can be used during updates to attach + /// a hint to the update thas is preserved here. + final String? metadata; + /// Data associated with the change. /// /// For PUT, this is contains all non-null columns of the row. @@ -77,8 +87,22 @@ class CrudEntry { /// For DELETE, this is null. final Map? opData; - CrudEntry(this.clientId, this.op, this.table, this.id, this.transactionId, - this.opData); + /// Old values before an update. + /// + /// This is only tracked for tables for which this has been enabled by setting + /// the [Table.includeOld]. + final Map? oldData; + + CrudEntry( + this.clientId, + this.op, + this.table, + this.id, + this.transactionId, + this.opData, { + this.oldData, + this.metadata, + }); factory CrudEntry.fromRow(sqlite.Row row) { final data = jsonDecode(row['data'] as String); @@ -89,6 +113,8 @@ class CrudEntry { data['id'] as String, row['tx_id'] as int, data['data'] as Map?, + oldData: data['old'] as Map?, + metadata: data['metadata'] as String?, ); } @@ -100,7 +126,9 @@ class CrudEntry { 'type': table, 'id': id, 'tx_id': transactionId, - 'data': opData + 'data': opData, + 'metadata': metadata, + 'old': oldData, }; } diff --git a/packages/powersync_core/lib/src/schema.dart b/packages/powersync_core/lib/src/schema.dart index d4802025..b9be6f85 100644 --- a/packages/powersync_core/lib/src/schema.dart +++ b/packages/powersync_core/lib/src/schema.dart @@ -1,3 +1,4 @@ +import 'crud.dart'; import 'schema_logic.dart'; /// The schema used by the database. @@ -26,8 +27,27 @@ class Schema { } } +/// Options to include old values in [CrudEntry] for update statements. +/// +/// This options are enabled by passing it to a non-local [Table] constructor. +final class IncludeOldOptions { + /// A filter of column names for which updates should be tracked. + /// + /// When set to a non-null value, olumns not included in this list will not + /// appear in [CrudEntry.oldData]. By default, all columns are included. + final List? columnFilter; + + /// Whether to only include old values when they were changed by an update, + /// instead of always including all old values. + final bool onlyWhenChanged; + + const IncludeOldOptions({this.columnFilter, this.onlyWhenChanged = false}); +} + /// A single table in the schema. class Table { + static const _maxNumberOfColumns = 1999; + /// The synced table name, matching sync rules. final String name; @@ -37,12 +57,26 @@ class Table { /// List of indexes. final List indexes; - /// Whether the table only exists only. + /// Whether to add a hidden `_metadata` column that will be enabled for + /// updates to attach custom information about writes that will be reported + /// through [CrudEntry.metadata]. + final bool includeMetadata; + + /// Whether to track old values of columns for [CrudEntry.oldData]. + /// + /// See [IncludeOldOptions] for details. + final IncludeOldOptions? includeOld; + + /// Whether the table only exists locally. final bool localOnly; /// Whether this is an insert-only table. final bool insertOnly; + /// Whether an `UPDATE` statement that doesn't change any values should be + /// ignored when creating CRUD entries. + final bool ignoreEmptyUpdate; + /// Override the name for the view final String? _viewNameOverride; @@ -50,7 +84,7 @@ class Table { /// per table to 1999, due to internal SQLite limits. /// /// In earlier versions this was limited to 63. - final int maxNumberOfColumns = 1999; + final int maxNumberOfColumns = _maxNumberOfColumns; /// Internal use only. /// @@ -66,9 +100,16 @@ class Table { /// Create a synced table. /// /// Local changes are recorded, and remote changes are synced to the local table. - const Table(this.name, this.columns, - {this.indexes = const [], String? viewName, this.localOnly = false}) - : insertOnly = false, + const Table( + this.name, + this.columns, { + this.indexes = const [], + String? viewName, + this.localOnly = false, + this.ignoreEmptyUpdate = false, + this.includeMetadata = false, + this.includeOld, + }) : insertOnly = false, _viewNameOverride = viewName; /// Create a table that only exists locally. @@ -78,6 +119,9 @@ class Table { {this.indexes = const [], String? viewName}) : localOnly = true, insertOnly = false, + includeMetadata = false, + includeOld = null, + ignoreEmptyUpdate = false, _viewNameOverride = viewName; /// Create a table that only supports inserts. @@ -88,8 +132,14 @@ class Table { /// /// SELECT queries on the table will always return 0 rows. /// - const Table.insertOnly(this.name, this.columns, {String? viewName}) - : localOnly = false, + const Table.insertOnly( + this.name, + this.columns, { + String? viewName, + this.ignoreEmptyUpdate = false, + this.includeMetadata = false, + this.includeOld, + }) : localOnly = false, insertOnly = true, indexes = const [], _viewNameOverride = viewName; @@ -106,9 +156,9 @@ class Table { /// Check that there are no issues in the table definition. void validate() { - if (columns.length > maxNumberOfColumns) { + if (columns.length > _maxNumberOfColumns) { throw AssertionError( - "Table $name has more than $maxNumberOfColumns columns, which is not supported"); + "Table $name has more than $_maxNumberOfColumns columns, which is not supported"); } if (invalidSqliteCharacters.hasMatch(name)) { @@ -121,6 +171,14 @@ class Table { "Invalid characters in view name: $_viewNameOverride"); } + if (includeMetadata && localOnly) { + throw AssertionError("Local-only tables can't track metadata"); + } + + if (includeOld != null && localOnly) { + throw AssertionError("Local-only tables can't track old values"); + } + Set columnNames = {"id"}; for (var column in columns) { if (column.name == 'id') { @@ -168,7 +226,13 @@ class Table { 'local_only': localOnly, 'insert_only': insertOnly, 'columns': columns, - 'indexes': indexes.map((e) => e.toJson(this)).toList(growable: false) + 'indexes': indexes.map((e) => e.toJson(this)).toList(growable: false), + 'ignore_empty_update': ignoreEmptyUpdate, + 'include_metadata': includeMetadata, + if (includeOld case final includeOld?) ...{ + 'include_old': includeOld.columnFilter ?? true, + 'include_old_only_when_changed': includeOld.onlyWhenChanged, + }, }; } diff --git a/packages/powersync_core/test/crud_test.dart b/packages/powersync_core/test/crud_test.dart index e73a961b..9555e485 100644 --- a/packages/powersync_core/test/crud_test.dart +++ b/packages/powersync_core/test/crud_test.dart @@ -139,6 +139,7 @@ void main() { test('INSERT-only tables', () async { await powersync.disconnectAndClear(); + await powersync.close(); powersync = await testUtils.setupPowerSync( path: path, schema: const Schema([ @@ -269,5 +270,96 @@ void main() { await tx2.complete(); expect(await powersync.getNextCrudTransaction(), equals(null)); }); + + test('include metadata', () async { + await powersync.updateSchema(Schema([ + Table( + 'lists', + [Column.text('name')], + includeMetadata: true, + ) + ])); + + await powersync.execute( + 'INSERT INTO lists (id, name, _metadata) VALUES (uuid(), ?, ?)', + ['entry', 'so meta']); + + final batch = await powersync.getNextCrudTransaction(); + expect(batch!.crud[0].metadata, 'so meta'); + }); + + test('include old values', () async { + await powersync.updateSchema(Schema([ + Table( + 'lists', + [Column.text('name'), Column.text('content')], + includeOld: IncludeOldOptions(), + ) + ])); + + await powersync.execute( + 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', + ['entry', 'content']); + await powersync.execute('DELETE FROM ps_crud;'); + await powersync.execute('UPDATE lists SET name = ?;', ['new name']); + + final batch = await powersync.getNextCrudTransaction(); + expect(batch!.crud[0].oldData, {'name': 'entry', 'content': 'content'}); + }); + + test('include old values with column filter', () async { + await powersync.updateSchema(Schema([ + Table( + 'lists', + [Column.text('name'), Column.text('content')], + includeOld: IncludeOldOptions(columnFilter: ['name']), + ) + ])); + + await powersync.execute( + 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', + ['name', 'content']); + await powersync.execute('DELETE FROM ps_crud;'); + await powersync.execute('UPDATE lists SET name = ?, content = ?', + ['new name', 'new content']); + + final batch = await powersync.getNextCrudTransaction(); + expect(batch!.crud[0].oldData, {'name': 'name'}); + }); + + test('include old values when changed', () async { + await powersync.updateSchema(Schema([ + Table( + 'lists', + [Column.text('name'), Column.text('content')], + includeOld: IncludeOldOptions(onlyWhenChanged: true), + ) + ])); + + await powersync.execute( + 'INSERT INTO lists (id, name, content) VALUES (uuid(), ?, ?)', + ['name', 'content']); + await powersync.execute('DELETE FROM ps_crud;'); + await powersync.execute('UPDATE lists SET name = ?', ['new name']); + + final batch = await powersync.getNextCrudTransaction(); + expect(batch!.crud[0].oldData, {'name': 'name'}); + }); + + test('ignore empty update', () async { + await powersync.updateSchema(Schema([ + Table( + 'lists', + [Column.text('name')], + ignoreEmptyUpdate: true, + ) + ])); + + await powersync + .execute('INSERT INTO lists (id, name) VALUES (uuid(), ?)', ['name']); + await powersync.execute('DELETE FROM ps_crud;'); + await powersync.execute('UPDATE lists SET name = ?;', ['name']); + expect(await powersync.getNextCrudTransaction(), isNull); + }); }); } diff --git a/packages/powersync_core/test/schema_test.dart b/packages/powersync_core/test/schema_test.dart index 1a3df3e0..47459835 100644 --- a/packages/powersync_core/test/schema_test.dart +++ b/packages/powersync_core/test/schema_test.dart @@ -318,6 +318,26 @@ void main() { ); }); + test('local-only with metadata', () { + final table = Table('foo', [Column.text('bar')], + localOnly: true, includeMetadata: true); + + expect( + table.validate, + throwsA(isA().having((e) => e.message, 'emssage', + "Local-only tables can't track metadata"))); + }); + + test('local-only with includeOld', () { + final table = Table('foo', [Column.text('bar')], + localOnly: true, includeOld: IncludeOldOptions()); + + expect( + table.validate, + throwsA(isA().having((e) => e.message, 'emssage', + "Local-only tables can't track old values"))); + }); + test('Schema without duplicate table names', () { final schema = Schema([ Table('duplicate', [ @@ -362,13 +382,51 @@ void main() { ]); final json = table.toJson(); + expect(json, { + 'name': 'users', + 'view_name': null, + 'local_only': false, + 'insert_only': false, + 'columns': hasLength(2), + 'indexes': hasLength(1), + 'ignore_empty_update': false, + 'include_metadata': false, + }); + }); + + test('handles options', () { + expect(Table('foo', [], includeMetadata: true).toJson(), + containsPair('include_metadata', isTrue)); + + expect(Table('foo', [], ignoreEmptyUpdate: true).toJson(), + containsPair('ignore_empty_update', isTrue)); - expect(json['name'], equals('users')); - expect(json['view_name'], isNull); - expect(json['local_only'], isFalse); - expect(json['insert_only'], isFalse); - expect(json['columns'].length, equals(2)); - expect(json['indexes'].length, equals(1)); + expect( + Table('foo', [], includeOld: IncludeOldOptions()).toJson(), + allOf( + containsPair('include_old', isTrue), + containsPair('include_old_only_when_changed', isFalse), + ), + ); + + expect( + Table('foo', [], + includeOld: IncludeOldOptions(columnFilter: ['foo', 'bar'])) + .toJson(), + allOf( + containsPair('include_old', ['foo', 'bar']), + containsPair('include_old_only_when_changed', isFalse), + ), + ); + + expect( + Table('foo', [], includeOld: IncludeOldOptions(onlyWhenChanged: true)) + .toJson(), + allOf( + containsPair('include_old', isTrue), + containsPair('include_old_only_when_changed', isTrue), + ), + ); }); }); } From 54c8d3679b38c9329dd445a7f70235aa11a33ef6 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 28 Apr 2025 15:55:33 +0200 Subject: [PATCH 2/5] Typo --- packages/powersync_core/lib/src/schema.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/powersync_core/lib/src/schema.dart b/packages/powersync_core/lib/src/schema.dart index b9be6f85..9b5a2950 100644 --- a/packages/powersync_core/lib/src/schema.dart +++ b/packages/powersync_core/lib/src/schema.dart @@ -29,7 +29,7 @@ class Schema { /// Options to include old values in [CrudEntry] for update statements. /// -/// This options are enabled by passing it to a non-local [Table] constructor. +/// These options are enabled by passing it to a non-local [Table] constructor. final class IncludeOldOptions { /// A filter of column names for which updates should be tracked. /// From 4adbc13429fdd7ddc0d95fc247b899ec08558578 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 28 Apr 2025 15:55:55 +0200 Subject: [PATCH 3/5] typo again --- packages/powersync_core/lib/src/schema.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/powersync_core/lib/src/schema.dart b/packages/powersync_core/lib/src/schema.dart index 9b5a2950..0ac20ac2 100644 --- a/packages/powersync_core/lib/src/schema.dart +++ b/packages/powersync_core/lib/src/schema.dart @@ -29,7 +29,8 @@ class Schema { /// Options to include old values in [CrudEntry] for update statements. /// -/// These options are enabled by passing it to a non-local [Table] constructor. +/// These options are enabled by passing them to a non-local [Table] +/// constructor. final class IncludeOldOptions { /// A filter of column names for which updates should be tracked. /// From 783dc5652cb7c5079218245465e09120beb7c5c4 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 28 Apr 2025 16:51:10 +0200 Subject: [PATCH 4/5] Even more typos --- packages/powersync_core/lib/src/schema.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/powersync_core/lib/src/schema.dart b/packages/powersync_core/lib/src/schema.dart index 0ac20ac2..53a30158 100644 --- a/packages/powersync_core/lib/src/schema.dart +++ b/packages/powersync_core/lib/src/schema.dart @@ -34,7 +34,7 @@ class Schema { final class IncludeOldOptions { /// A filter of column names for which updates should be tracked. /// - /// When set to a non-null value, olumns not included in this list will not + /// When set to a non-null value, columns not included in this list will not /// appear in [CrudEntry.oldData]. By default, all columns are included. final List? columnFilter; From 8654b08a530978ef2dbebc21e1f2c3bebc175155 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 30 Apr 2025 11:58:16 +0200 Subject: [PATCH 5/5] Rename options --- packages/powersync_core/lib/src/crud.dart | 4 +- packages/powersync_core/lib/src/schema.dart | 45 ++++++++++--------- packages/powersync_core/test/crud_test.dart | 12 ++--- packages/powersync_core/test/schema_test.dart | 20 +++++---- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/packages/powersync_core/lib/src/crud.dart b/packages/powersync_core/lib/src/crud.dart index 1be8c677..0593fb01 100644 --- a/packages/powersync_core/lib/src/crud.dart +++ b/packages/powersync_core/lib/src/crud.dart @@ -73,7 +73,7 @@ class CrudEntry { /// An optional metadata string attached to this entry at the time the write /// has been issued. /// - /// For tables where [Table.includeMetadata] is enabled, a hidden `_metadata` + /// For tables where [Table.trackMetadata] is enabled, a hidden `_metadata` /// column is added to this table that can be used during updates to attach /// a hint to the update thas is preserved here. final String? metadata; @@ -90,7 +90,7 @@ class CrudEntry { /// Old values before an update. /// /// This is only tracked for tables for which this has been enabled by setting - /// the [Table.includeOld]. + /// the [Table.trackPreviousValues]. final Map? oldData; CrudEntry( diff --git a/packages/powersync_core/lib/src/schema.dart b/packages/powersync_core/lib/src/schema.dart index 53a30158..5dfe0d1e 100644 --- a/packages/powersync_core/lib/src/schema.dart +++ b/packages/powersync_core/lib/src/schema.dart @@ -31,7 +31,7 @@ class Schema { /// /// These options are enabled by passing them to a non-local [Table] /// constructor. -final class IncludeOldOptions { +final class TrackPreviousValuesOptions { /// A filter of column names for which updates should be tracked. /// /// When set to a non-null value, columns not included in this list will not @@ -42,7 +42,8 @@ final class IncludeOldOptions { /// instead of always including all old values. final bool onlyWhenChanged; - const IncludeOldOptions({this.columnFilter, this.onlyWhenChanged = false}); + const TrackPreviousValuesOptions( + {this.columnFilter, this.onlyWhenChanged = false}); } /// A single table in the schema. @@ -61,12 +62,12 @@ class Table { /// Whether to add a hidden `_metadata` column that will be enabled for /// updates to attach custom information about writes that will be reported /// through [CrudEntry.metadata]. - final bool includeMetadata; + final bool trackMetadata; /// Whether to track old values of columns for [CrudEntry.oldData]. /// - /// See [IncludeOldOptions] for details. - final IncludeOldOptions? includeOld; + /// See [TrackPreviousValuesOptions] for details. + final TrackPreviousValuesOptions? trackPreviousValues; /// Whether the table only exists locally. final bool localOnly; @@ -76,7 +77,7 @@ class Table { /// Whether an `UPDATE` statement that doesn't change any values should be /// ignored when creating CRUD entries. - final bool ignoreEmptyUpdate; + final bool ignoreEmptyUpdates; /// Override the name for the view final String? _viewNameOverride; @@ -107,9 +108,9 @@ class Table { this.indexes = const [], String? viewName, this.localOnly = false, - this.ignoreEmptyUpdate = false, - this.includeMetadata = false, - this.includeOld, + this.ignoreEmptyUpdates = false, + this.trackMetadata = false, + this.trackPreviousValues, }) : insertOnly = false, _viewNameOverride = viewName; @@ -120,9 +121,9 @@ class Table { {this.indexes = const [], String? viewName}) : localOnly = true, insertOnly = false, - includeMetadata = false, - includeOld = null, - ignoreEmptyUpdate = false, + trackMetadata = false, + trackPreviousValues = null, + ignoreEmptyUpdates = false, _viewNameOverride = viewName; /// Create a table that only supports inserts. @@ -137,9 +138,9 @@ class Table { this.name, this.columns, { String? viewName, - this.ignoreEmptyUpdate = false, - this.includeMetadata = false, - this.includeOld, + this.ignoreEmptyUpdates = false, + this.trackMetadata = false, + this.trackPreviousValues, }) : localOnly = false, insertOnly = true, indexes = const [], @@ -172,11 +173,11 @@ class Table { "Invalid characters in view name: $_viewNameOverride"); } - if (includeMetadata && localOnly) { + if (trackMetadata && localOnly) { throw AssertionError("Local-only tables can't track metadata"); } - if (includeOld != null && localOnly) { + if (trackPreviousValues != null && localOnly) { throw AssertionError("Local-only tables can't track old values"); } @@ -228,11 +229,11 @@ class Table { 'insert_only': insertOnly, 'columns': columns, 'indexes': indexes.map((e) => e.toJson(this)).toList(growable: false), - 'ignore_empty_update': ignoreEmptyUpdate, - 'include_metadata': includeMetadata, - if (includeOld case final includeOld?) ...{ - 'include_old': includeOld.columnFilter ?? true, - 'include_old_only_when_changed': includeOld.onlyWhenChanged, + 'ignore_empty_update': ignoreEmptyUpdates, + 'include_metadata': trackMetadata, + if (trackPreviousValues case final trackPreviousValues?) ...{ + 'include_old': trackPreviousValues.columnFilter ?? true, + 'include_old_only_when_changed': trackPreviousValues.onlyWhenChanged, }, }; } diff --git a/packages/powersync_core/test/crud_test.dart b/packages/powersync_core/test/crud_test.dart index 9555e485..4f5fb886 100644 --- a/packages/powersync_core/test/crud_test.dart +++ b/packages/powersync_core/test/crud_test.dart @@ -276,7 +276,7 @@ void main() { Table( 'lists', [Column.text('name')], - includeMetadata: true, + trackMetadata: true, ) ])); @@ -293,7 +293,7 @@ void main() { Table( 'lists', [Column.text('name'), Column.text('content')], - includeOld: IncludeOldOptions(), + trackPreviousValues: TrackPreviousValuesOptions(), ) ])); @@ -312,7 +312,8 @@ void main() { Table( 'lists', [Column.text('name'), Column.text('content')], - includeOld: IncludeOldOptions(columnFilter: ['name']), + trackPreviousValues: + TrackPreviousValuesOptions(columnFilter: ['name']), ) ])); @@ -332,7 +333,8 @@ void main() { Table( 'lists', [Column.text('name'), Column.text('content')], - includeOld: IncludeOldOptions(onlyWhenChanged: true), + trackPreviousValues: + TrackPreviousValuesOptions(onlyWhenChanged: true), ) ])); @@ -351,7 +353,7 @@ void main() { Table( 'lists', [Column.text('name')], - ignoreEmptyUpdate: true, + ignoreEmptyUpdates: true, ) ])); diff --git a/packages/powersync_core/test/schema_test.dart b/packages/powersync_core/test/schema_test.dart index 47459835..f397d5bb 100644 --- a/packages/powersync_core/test/schema_test.dart +++ b/packages/powersync_core/test/schema_test.dart @@ -320,7 +320,7 @@ void main() { test('local-only with metadata', () { final table = Table('foo', [Column.text('bar')], - localOnly: true, includeMetadata: true); + localOnly: true, trackMetadata: true); expect( table.validate, @@ -328,9 +328,9 @@ void main() { "Local-only tables can't track metadata"))); }); - test('local-only with includeOld', () { + test('local-only with trackPreviousValues', () { final table = Table('foo', [Column.text('bar')], - localOnly: true, includeOld: IncludeOldOptions()); + localOnly: true, trackPreviousValues: TrackPreviousValuesOptions()); expect( table.validate, @@ -395,14 +395,15 @@ void main() { }); test('handles options', () { - expect(Table('foo', [], includeMetadata: true).toJson(), + expect(Table('foo', [], trackMetadata: true).toJson(), containsPair('include_metadata', isTrue)); - expect(Table('foo', [], ignoreEmptyUpdate: true).toJson(), + expect(Table('foo', [], ignoreEmptyUpdates: true).toJson(), containsPair('ignore_empty_update', isTrue)); expect( - Table('foo', [], includeOld: IncludeOldOptions()).toJson(), + Table('foo', [], trackPreviousValues: TrackPreviousValuesOptions()) + .toJson(), allOf( containsPair('include_old', isTrue), containsPair('include_old_only_when_changed', isFalse), @@ -411,7 +412,8 @@ void main() { expect( Table('foo', [], - includeOld: IncludeOldOptions(columnFilter: ['foo', 'bar'])) + trackPreviousValues: + TrackPreviousValuesOptions(columnFilter: ['foo', 'bar'])) .toJson(), allOf( containsPair('include_old', ['foo', 'bar']), @@ -420,7 +422,9 @@ void main() { ); expect( - Table('foo', [], includeOld: IncludeOldOptions(onlyWhenChanged: true)) + Table('foo', [], + trackPreviousValues: + TrackPreviousValuesOptions(onlyWhenChanged: true)) .toJson(), allOf( containsPair('include_old', isTrue),