Skip to content

Commit 0b94f78

Browse files
pubkeyclaude
andauthored
Fix CRDT plugin dropping composite primary key during conflict resolution (#8356)
The bulkInsert hook in the CRDT plugin captured the document fields for the initial CRDT $set operation before RxDB computed the composite primary key. As a result, rebuildFromCRDT (used during conflict resolution) produced a document without the primary key, which could corrupt replicated state. Also fill the composite primary key inside the CRDT bulkInsert hook so that the computed key is captured in the initial CRDT operation body. https://claude.ai/code/session_01V9mbvpPUfyWCtR2yfsUv4H Co-authored-by: Claude <noreply@anthropic.com>
1 parent 575e69b commit 0b94f78

3 files changed

Lines changed: 87 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- FIX CRDT plugin `bulkInsert` hook not including the composite primary key in CRDT operations, causing the primary key field to be lost during conflict resolution rebuild for schemas that use a composite primary key

src/plugins/crdt/index.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
RxError
3434
} from '../../index.ts';
3535
import { mingoUpdater } from '../update/mingo-updater.ts';
36-
import { fillObjectWithDefaults } from '../../rx-schema-helper.ts';
36+
import { fillObjectWithDefaults, fillPrimaryKey } from '../../rx-schema-helper.ts';
3737

3838

3939

@@ -523,6 +523,23 @@ export const RxDBcrdtPlugin: RxPlugin = {
523523
*/
524524
fillObjectWithDefaults(collection.schema, docData);
525525

526+
/**
527+
* Fill composite primary key before building the CRDT $set body.
528+
* When a schema uses a composite primary key, the value of the
529+
* primary key field is computed from other fields by RxDB and
530+
* is not provided by the user. Without this, the CRDT operation
531+
* would miss the primary key field and rebuildFromCRDT (used
532+
* during conflict resolution) would produce a document without
533+
* the primary key.
534+
*/
535+
if (typeof collection.schema.jsonSchema.primaryKey !== 'string') {
536+
fillPrimaryKey(
537+
collection.schema.primaryPath,
538+
collection.schema.jsonSchema,
539+
docData
540+
);
541+
}
542+
526543
const setMe: Partial<RxDocumentData<any>> = {};
527544
Object.entries(docData).forEach(([key, value]) => {
528545
if (

test/unit/crdt.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,74 @@ describeParallel('crdt.test.ts', () => {
475475
doc1.collection.database.close();
476476
doc2.collection.database.close();
477477
});
478+
it('should preserve the composite primary key during conflict resolution', async () => {
479+
/**
480+
* When a schema uses a composite primary key, the value of the
481+
* primary key field is computed from the other fields by RxDB
482+
* and is not provided by the user during insert.
483+
* The CRDT operation must include the computed primary key,
484+
* otherwise rebuildFromCRDT (used during conflict resolution)
485+
* will produce a document with a missing primary key field.
486+
*/
487+
type CompositeDocType = {
488+
id: string;
489+
firstName: string;
490+
lastName: string;
491+
age: number;
492+
};
493+
const compositeSchema: RxJsonSchema<CompositeDocType> = {
494+
version: 0,
495+
primaryKey: {
496+
key: 'id',
497+
fields: ['firstName', 'age'],
498+
separator: '|'
499+
},
500+
type: 'object',
501+
properties: {
502+
id: { type: 'string', maxLength: 100 },
503+
firstName: { type: 'string', maxLength: 100 },
504+
lastName: { type: 'string' },
505+
age: { type: 'integer', minimum: 0, maximum: 150 }
506+
},
507+
required: ['id', 'firstName', 'lastName', 'age']
508+
};
509+
510+
async function getDocFromNewDb() {
511+
const c = await getCRDTCollection<CompositeDocType>(compositeSchema);
512+
const doc = await c.insert({
513+
firstName: 'Alice',
514+
lastName: 'Smith',
515+
age: 25
516+
// id is NOT provided, must be auto-computed by RxDB
517+
} as CompositeDocType);
518+
return doc;
519+
}
520+
const doc1 = await getDocFromNewDb();
521+
const doc2 = await getDocFromNewDb();
522+
523+
// Both must have the auto-computed composite primary key
524+
assert.strictEqual(doc1.getLatest().id, 'Alice|25');
525+
assert.strictEqual(doc2.getLatest().id, 'Alice|25');
526+
527+
// Resolve a conflict between the two versions.
528+
const schemaFilled = enableCRDTinSchema(fillWithDefaultSettings(compositeSchema));
529+
const handler = getCRDTConflictHandler<WithCRDTs<CompositeDocType>>(
530+
defaultHashSha256,
531+
schemaFilled
532+
);
533+
534+
const resolved = await handler.resolve({
535+
newDocumentState: doc1.toMutableJSON(true) as any,
536+
realMasterState: doc2.toMutableJSON(true) as any
537+
}, 'test-composite-primary');
538+
539+
// After conflict resolution, the composite primary key must be preserved.
540+
assert.strictEqual((resolved as any).id, 'Alice|25',
541+
'Composite primary key "id" was lost during conflict resolution rebuild');
542+
543+
doc1.collection.database.close();
544+
doc2.collection.database.close();
545+
});
478546
});
479547
describe('conflicts during replication', () => {
480548
if (!config.storage.hasReplication) {

0 commit comments

Comments
 (0)