fix: Allow only one releasable column per unique index (#91)
This commit is contained in:
parent
8acfa7f269
commit
af139ea800
7 changed files with 353 additions and 112 deletions
|
|
@ -24,6 +24,7 @@ final demoSyncTables = <Table>[
|
|||
Types.t,
|
||||
Unique.t,
|
||||
UniqueComposite.t,
|
||||
UniqueDiscriminator.t,
|
||||
UniqueSetNullChild.t,
|
||||
UniqueUuid.t,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -295,6 +295,7 @@ class DemoDebugEndpoint extends Endpoint {
|
|||
await add<Unique>();
|
||||
await add<UniqueUuid>();
|
||||
await add<UniqueComposite>();
|
||||
await add<UniqueDiscriminator>();
|
||||
await add<UniqueSetNullChild>();
|
||||
await add<Types>();
|
||||
await add<FkChainRoot>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
class: UniqueDiscriminator
|
||||
table: unique_discriminator
|
||||
database: all
|
||||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
categoryId: int
|
||||
name: String, unique(per=[scopeId, categoryId])
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
class: UniqueNoRelease
|
||||
table: unique_no_release
|
||||
database: all
|
||||
fields:
|
||||
id: UuidValue?, defaultPersist=random_v7
|
||||
### Owner scope of this row. Maintained by the CRDT sync layer.
|
||||
scopeId: int?, relation(parent=crdt_scopes, onDelete=Cascade)
|
||||
categoryId: int, unique(per=[scopeId])
|
||||
|
|
@ -421,6 +421,94 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'Given a remote update that loses a composite unique conflict with a stable discriminator, ',
|
||||
() {
|
||||
late UniqueDiscriminator loser;
|
||||
late CrdtMergeUpdate remoteUpdate;
|
||||
|
||||
setUp(() async {
|
||||
await session.db.transactionForUser(testCrdtUserId, (tx) async {
|
||||
await UniqueDiscriminator.db.insertRow(
|
||||
session,
|
||||
UniqueDiscriminator(
|
||||
id: const Uuid().v7obj(),
|
||||
categoryId: 7,
|
||||
name: 'shared-name',
|
||||
),
|
||||
transaction: tx,
|
||||
);
|
||||
loser = await UniqueDiscriminator.db.insertRow(
|
||||
session,
|
||||
UniqueDiscriminator(
|
||||
id: const Uuid().v7obj(),
|
||||
categoryId: 7,
|
||||
name: 'original-name',
|
||||
),
|
||||
transaction: tx,
|
||||
);
|
||||
});
|
||||
|
||||
final loserCrdtRow = await CrdtDataRow.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.uuidRowId.equals(loser.id),
|
||||
include: CrdtDataRow.include(node: CrdtNode.include()),
|
||||
);
|
||||
|
||||
remoteUpdate = CrdtMergeUpdate(
|
||||
uuidScopeId: testCrdtUserId,
|
||||
tableName: UniqueDiscriminator.t.tableName,
|
||||
uuidRowId: loser.id!,
|
||||
uuidNodeId: const Uuid().v7obj(),
|
||||
hlcDatetime: loserCrdtRow!.hlc.datetime.advance(),
|
||||
hlcCounter: loserCrdtRow.hlc.counter,
|
||||
columnName: UniqueDiscriminator.t.name.columnName,
|
||||
value: 'shared-name',
|
||||
);
|
||||
|
||||
mergeSet = [remoteUpdate];
|
||||
});
|
||||
|
||||
group('when merging, ', () {
|
||||
setUp(() async {
|
||||
await session.db.mergeChanges(
|
||||
mergeSet,
|
||||
scopeId: testCrdtUserId,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'then the losing row keeps the discriminator and receives a conflict-free name.',
|
||||
() async {
|
||||
final row = await UniqueDiscriminator.db.findById(session, loser.id!);
|
||||
|
||||
expect(row, isNotNull);
|
||||
expect(row!.categoryId, 7);
|
||||
expect(row.name, 'shared-name__conflict__${loser.id!.uuid}');
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'then the stable discriminator does not receive CRDT field metadata.',
|
||||
() async {
|
||||
final fields = await CrdtDataField.db.find(
|
||||
session,
|
||||
where: (t) => t.row.uuidRowId.equals(loser.id),
|
||||
include: CrdtDataField.include(
|
||||
column: CrdtSchemaColumn.include(),
|
||||
node: CrdtNode.include(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(fields, hasLength(1));
|
||||
expect(fields.first.column!.name, UniqueDiscriminator.t.name.columnName);
|
||||
expect(fields.first.hlc, remoteUpdate.hlc);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'Given a remote update that loses a UUID unique conflict, ',
|
||||
() {
|
||||
|
|
|
|||
|
|
@ -297,6 +297,70 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'Given a CRDT database with a scoped composite unique index that has a stable discriminator and a releasable column, '
|
||||
'when the database is initialized, '
|
||||
'then the unique index metadata is accepted.',
|
||||
() async {
|
||||
final crdtSession = CrdtDatabaseSession.wraps(
|
||||
testSession,
|
||||
syncTables: [UniqueDiscriminator.t],
|
||||
);
|
||||
|
||||
await expectLater(crdtSession.db.initialize(), completes);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'Given a CRDT schema registry with a scoped unique index that has no releasable non-scope column, '
|
||||
'when the registry is created, '
|
||||
'then an error is thrown.',
|
||||
() {
|
||||
expect(
|
||||
() => CrdtSchemaRegistry(
|
||||
session,
|
||||
syncTables: [UniqueNoRelease.t],
|
||||
),
|
||||
throwsA(
|
||||
isA<StateError>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
contains(
|
||||
'CRDT unique conflict resolution requires at least one '
|
||||
'releasable non-scope column',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'Given a CRDT database with a scoped unique index that has no releasable non-scope column, '
|
||||
'when the database is initialized, '
|
||||
'then an error is thrown.',
|
||||
() async {
|
||||
final crdtSession = CrdtDatabaseSession.wraps(
|
||||
testSession,
|
||||
syncTables: [UniqueNoRelease.t],
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
crdtSession.db.initialize(),
|
||||
throwsA(
|
||||
isA<StateError>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
contains(
|
||||
'CRDT unique conflict resolution requires at least one '
|
||||
'releasable non-scope column',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'Given a CRDT schema registry with a synced table that has the scopeId cascade relation to crdt_scopes, '
|
||||
'when the registry is created, '
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ Future<void> _initialize() async {
|
|||
Town.t,
|
||||
Unique.t,
|
||||
UniqueComposite.t,
|
||||
UniqueDiscriminator.t,
|
||||
UniqueSetNullChild.t,
|
||||
UniqueUuid.t,
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue