Skip to content

Commit e129315

Browse files
authored
Fix backwards compatibility issues with Obj.referenced (#10218)
While this is a real bug fix, the underlying issue does not affect data correctness. `Obj.referenced()` was introduced to track when a particular `Obj` was (last) written. This information is crucial when purging unreferenced data in Nessie's persistence backend database. The current code uses the sentinel value `0` for `Obj.reference()` for accidentally _two_ scenarios: to indicate that the value is not set and needs to be written with the actual timestamp and it is also `0` when old rows that do not have the `referenced` column set (aka `NULL`). This change updates the code to introduce `-1` for `referenced` as a sentinel for "absent" (NULL). There is also a bug in the implementations that prevents the "purge unreferenced data" to actually work for rows that do not have a value in the `referenced` column. This change fixes this. Unfortunately not all databases properly (DynamoDB and BigTable) support checking for the absence of a value.
1 parent f8da3d6 commit e129315

File tree

16 files changed

+272
-70
lines changed

16 files changed

+272
-70
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ as necessary. Empty sections will not end in the release notes.
3232

3333
### Fixes
3434

35+
- Fix an issue that prevents the Nessie Server Admin tool to purge unreferenced data in the backend
36+
database, for data being written before Nessie version 0.101.0.
37+
3538
### Commits
3639

3740
## [0.101.3] Release (2024-12-18)

Diff for: versioned/storage/bigtable/src/main/java/org/projectnessie/versioned/storage/bigtable/BigTablePersist.java

+22-12
Original file line numberDiff line numberDiff line change
@@ -568,11 +568,18 @@ public void upsertObjs(@Nonnull Obj[] objs) throws ObjTooLargeException {
568568

569569
@Override
570570
public boolean deleteWithReferenced(@Nonnull Obj obj) {
571-
Filter condition =
572-
FILTERS
573-
.chain()
574-
.filter(FILTERS.qualifier().exactMatch(QUALIFIER_OBJ_REFERENCED))
575-
.filter(FILTERS.value().exactMatch(copyFromUtf8(Long.toString(obj.referenced()))));
571+
Filter condition;
572+
if (obj.referenced() != -1L) {
573+
condition =
574+
FILTERS
575+
.chain()
576+
.filter(FILTERS.qualifier().exactMatch(QUALIFIER_OBJ_REFERENCED))
577+
.filter(FILTERS.value().exactMatch(copyFromUtf8(Long.toString(obj.referenced()))));
578+
} else {
579+
// We take a risk here in case the given object does _not_ have a referenced() value (old
580+
// object). It's sadly not possible to check for the _absence_ of a cell.
581+
condition = FILTERS.pass();
582+
}
576583

577584
ConditionalRowMutation conditionalRowMutation =
578585
conditionalRowMutation(obj, condition, Mutation.create().deleteRow());
@@ -653,12 +660,15 @@ static <M extends MutationApi<M>> M objToMutation(
653660
}
654661
mutation
655662
.setCell(FAMILY_OBJS, QUALIFIER_OBJS, CELL_TIMESTAMP, unsafeWrap(serialized))
656-
.setCell(FAMILY_OBJS, QUALIFIER_OBJ_TYPE, CELL_TIMESTAMP, objTypeValue)
657-
.setCell(
658-
FAMILY_OBJS,
659-
QUALIFIER_OBJ_REFERENCED,
660-
CELL_TIMESTAMP,
661-
copyFromUtf8(Long.toString(referenced)));
663+
.setCell(FAMILY_OBJS, QUALIFIER_OBJ_TYPE, CELL_TIMESTAMP, objTypeValue);
664+
if (obj.referenced() != -1L) {
665+
// -1 is a sentinel for AbstractBasePersistTests.deleteWithReferenced()
666+
mutation.setCell(
667+
FAMILY_OBJS,
668+
QUALIFIER_OBJ_REFERENCED,
669+
CELL_TIMESTAMP,
670+
copyFromUtf8(Long.toString(referenced)));
671+
}
662672
UpdateableObj.extractVersionToken(obj)
663673
.map(ByteString::copyFromUtf8)
664674
.ifPresent(bs -> mutation.setCell(FAMILY_OBJS, QUALIFIER_OBJ_VERS, CELL_TIMESTAMP, bs));
@@ -756,7 +766,7 @@ private Obj objFromRow(Row row) {
756766
List<RowCell> objReferenced = row.getCells(FAMILY_OBJS, QUALIFIER_OBJ_REFERENCED);
757767
long referenced =
758768
objReferenced.isEmpty()
759-
? 0L
769+
? -1L
760770
: Long.parseLong(objReferenced.get(0).getValue().toStringUtf8());
761771
List<RowCell> objCells = row.getCells(FAMILY_OBJS, QUALIFIER_OBJS);
762772
ByteBuffer obj = objCells.get(0).getValue().asReadOnlyByteBuffer();

Diff for: versioned/storage/cassandra/src/main/java/org/projectnessie/versioned/storage/cassandra/CassandraPersist.java

+29-10
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ public <T extends Obj> T[] fetchTypedObjsIfExist(
285285
}
286286
ObjId id = deserializeObjId(row.getString(COL_OBJ_ID.name()));
287287
String versionToken = row.getString(COL_OBJ_VERS.name());
288-
long referenced = row.getLong(COL_OBJ_REFERENCED.name());
288+
String colReferenced = COL_OBJ_REFERENCED.name();
289+
long referenced = row.isNull(colReferenced) ? -1 : row.getLong(colReferenced);
289290
@SuppressWarnings("unchecked")
290291
T typed =
291292
(T)
@@ -345,13 +346,21 @@ public void upsertObjs(@Nonnull Obj[] objs) throws ObjTooLargeException {
345346

346347
@Override
347348
public boolean deleteWithReferenced(@Nonnull Obj obj) {
349+
long referenced = obj.referenced();
348350
BoundStatement stmt =
349-
backend.buildStatement(
350-
DELETE_OBJ_REFERENCED,
351-
false,
352-
config.repositoryId(),
353-
serializeObjId(obj.id()),
354-
obj.referenced());
351+
referenced != -1L
352+
? backend.buildStatement(
353+
DELETE_OBJ_REFERENCED,
354+
false,
355+
config.repositoryId(),
356+
serializeObjId(obj.id()),
357+
referenced)
358+
// We take a risk here in case the given object does _not_ have a referenced() value
359+
// (old object).
360+
// Cassandra's conditional DELETE ... IF doesn't allow us to use "IF col IS NULL" or "IF
361+
// (col = 0 OR col IS NULL)".
362+
: backend.buildStatement(
363+
DELETE_OBJ, false, config.repositoryId(), serializeObjId(obj.id()));
355364
return backend.executeCas(stmt);
356365
}
357366

@@ -391,8 +400,13 @@ public boolean updateConditional(@Nonnull UpdateableObj expected, @Nonnull Updat
391400
.setString(COL_OBJ_ID.name(), serializeObjId(id))
392401
.setString(COL_OBJ_TYPE.name() + EXPECTED_SUFFIX, type.name())
393402
.setString(COL_OBJ_VERS.name() + EXPECTED_SUFFIX, expectedVersion)
394-
.setString(COL_OBJ_VERS.name(), newVersion)
395-
.setLong(COL_OBJ_REFERENCED.name(), referenced);
403+
.setString(COL_OBJ_VERS.name(), newVersion);
404+
if (newValue.referenced() != -1L) {
405+
// -1 is a sentinel for AbstractBasePersistTests.deleteWithReferenced()
406+
stmt = stmt.setLong(COL_OBJ_REFERENCED.name(), referenced);
407+
} else {
408+
stmt = stmt.setToNull(COL_OBJ_REFERENCED.name());
409+
}
396410

397411
serializer.serialize(
398412
newValue.withReferenced(referenced),
@@ -531,9 +545,14 @@ private <R> R writeSingleObj(
531545
.newBoundStatementBuilder(serializer.insertCql(upsert), upsert)
532546
.setString(COL_REPO_ID.name(), config.repositoryId())
533547
.setString(COL_OBJ_ID.name(), serializeObjId(id))
534-
.setLong(COL_OBJ_REFERENCED.name(), referenced)
535548
.setString(COL_OBJ_TYPE.name(), type.name())
536549
.setString(COL_OBJ_VERS.name(), versionToken);
550+
if (obj.referenced() != -1L) {
551+
// -1 is a sentinel for AbstractBasePersistTests.deleteWithReferenced()
552+
stmt = stmt.setLong(COL_OBJ_REFERENCED.name(), referenced);
553+
} else {
554+
stmt = stmt.setToNull(COL_OBJ_REFERENCED.name());
555+
}
537556

538557
serializer.serialize(
539558
obj,

Diff for: versioned/storage/cassandra2/src/main/java/org/projectnessie/versioned/storage/cassandra2/Cassandra2Persist.java

+29-10
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,8 @@ public <T extends Obj> T[] fetchTypedObjsIfExist(
290290
ObjId id = deserializeObjId(row.getByteBuffer(COL_OBJ_ID.name()));
291291
String versionToken = row.getString(COL_OBJ_VERS.name());
292292
ByteBuffer serialized = row.getByteBuffer(COL_OBJ_VALUE.name());
293-
long referenced = row.getLong(COL_OBJ_REFERENCED.name());
293+
String colReferenced = COL_OBJ_REFERENCED.name();
294+
long referenced = row.isNull(colReferenced) ? -1 : row.getLong(colReferenced);
294295
return typeClass.cast(deserializeObj(id, referenced, serialized, versionToken));
295296
};
296297

@@ -345,13 +346,21 @@ public void upsertObjs(@Nonnull Obj[] objs) throws ObjTooLargeException {
345346

346347
@Override
347348
public boolean deleteWithReferenced(@Nonnull Obj obj) {
349+
var referenced = obj.referenced();
348350
BoundStatement stmt =
349-
backend.buildStatement(
350-
DELETE_OBJ_REFERENCED,
351-
false,
352-
config.repositoryId(),
353-
serializeObjId(obj.id()),
354-
obj.referenced());
351+
referenced != -1L
352+
? backend.buildStatement(
353+
DELETE_OBJ_REFERENCED,
354+
false,
355+
config.repositoryId(),
356+
serializeObjId(obj.id()),
357+
referenced)
358+
// We take a risk here in case the given object does _not_ have a referenced() value
359+
// (old object).
360+
// Cassandra's conditional DELETE ... IF doesn't allow us to use "IF col IS NULL" or "IF
361+
// (col = 0 OR col IS NULL)".
362+
: backend.buildStatement(
363+
DELETE_OBJ, false, config.repositoryId(), serializeObjId(obj.id()));
355364
return backend.executeCas(stmt);
356365
}
357366

@@ -397,8 +406,13 @@ public boolean updateConditional(@Nonnull UpdateableObj expected, @Nonnull Updat
397406
.setString(COL_OBJ_TYPE.name() + EXPECTED_SUFFIX, type.shortName())
398407
.setString(COL_OBJ_VERS.name() + EXPECTED_SUFFIX, expectedVersion)
399408
.setString(COL_OBJ_VERS.name(), newVersion)
400-
.setByteBuffer(COL_OBJ_VALUE.name(), ByteBuffer.wrap(serialized))
401-
.setLong(COL_OBJ_REFERENCED.name(), referenced);
409+
.setByteBuffer(COL_OBJ_VALUE.name(), ByteBuffer.wrap(serialized));
410+
if (newValue.referenced() != -1L) {
411+
// -1 is a sentinel for AbstractBasePersistTests.deleteWithReferenced()
412+
stmt = stmt.setLong(COL_OBJ_REFERENCED.name(), referenced);
413+
} else {
414+
stmt = stmt.setToNull(COL_OBJ_REFERENCED.name());
415+
}
402416

403417
return backend.executeCas(stmt.build());
404418
}
@@ -538,10 +552,15 @@ private <R> R writeSingleObj(
538552
.newBoundStatementBuilder(upsert ? UPSERT_OBJ : STORE_OBJ, upsert)
539553
.setString(COL_REPO_ID.name(), config.repositoryId())
540554
.setByteBuffer(COL_OBJ_ID.name(), serializeObjId(id))
541-
.setLong(COL_OBJ_REFERENCED.name(), referenced)
542555
.setString(COL_OBJ_TYPE.name(), type.shortName())
543556
.setString(COL_OBJ_VERS.name(), versionToken)
544557
.setByteBuffer(COL_OBJ_VALUE.name(), ByteBuffer.wrap(serialized));
558+
if (obj.referenced() != -1L) {
559+
// -1 is a sentinel for AbstractBasePersistTests.deleteWithReferenced()
560+
stmt = stmt.setLong(COL_OBJ_REFERENCED.name(), referenced);
561+
} else {
562+
stmt = stmt.setToNull(COL_OBJ_REFERENCED.name());
563+
}
545564

546565
return consumer.apply(stmt.build());
547566
}

Diff for: versioned/storage/common-tests/src/main/java/org/projectnessie/versioned/storage/commontests/AbstractBasePersistTests.java

+41
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static java.util.Objects.requireNonNull;
2626
import static java.util.UUID.randomUUID;
2727
import static org.assertj.core.api.Assumptions.assumeThat;
28+
import static org.assertj.core.api.InstanceOfAssertFactories.LONG;
2829
import static org.assertj.core.api.InstanceOfAssertFactories.list;
2930
import static org.assertj.core.api.InstanceOfAssertFactories.type;
3031
import static org.junit.jupiter.params.provider.Arguments.arguments;
@@ -140,6 +141,46 @@ public class AbstractBasePersistTests {
140141

141142
@NessiePersist protected Persist persist;
142143

144+
@SuppressWarnings("DataFlowIssue")
145+
@Test
146+
public void deleteWithReferenced() throws Exception {
147+
assumeThat(persist.isCaching()).isFalse();
148+
149+
// Do NOT use any batch store operation here - implementations are only adopted to "respect" the
150+
// test-sentinel value -1 for exactly this .storeObj() signature!
151+
152+
var objWithReferenced = SimpleTestObj.builder().id(randomObjId()).text("foo").build();
153+
persist.storeObj(objWithReferenced, true);
154+
var readWithReferenced = persist.fetchObj(objWithReferenced.id());
155+
soft.assertThat(readWithReferenced).extracting(Obj::referenced, LONG).isGreaterThan(0);
156+
soft.assertThat(persist.deleteWithReferenced(objWithReferenced)).isFalse();
157+
soft.assertThatCode(() -> persist.fetchObj(objWithReferenced.id())).doesNotThrowAnyException();
158+
soft.assertThat(persist.deleteWithReferenced(objWithReferenced.withReferenced(Long.MAX_VALUE)))
159+
.isFalse();
160+
soft.assertThatCode(() -> persist.fetchObj(objWithReferenced.id())).doesNotThrowAnyException();
161+
soft.assertThat(persist.deleteWithReferenced(readWithReferenced)).isTrue();
162+
soft.assertThatCode(() -> persist.fetchObj(objWithReferenced.id()))
163+
.isInstanceOf(ObjNotFoundException.class);
164+
165+
var objWithoutReferenced1 =
166+
SimpleTestObj.builder().referenced(-1L).id(randomObjId()).text("foo").build();
167+
persist.storeObj(objWithoutReferenced1, true);
168+
var readWithoutReferenced1 = persist.fetchObj(objWithoutReferenced1.id());
169+
soft.assertThat(readWithoutReferenced1).extracting(Obj::referenced, LONG).isEqualTo(-1L);
170+
soft.assertThat(persist.deleteWithReferenced(objWithoutReferenced1)).isTrue();
171+
soft.assertThatCode(() -> persist.fetchObj(objWithoutReferenced1.id()))
172+
.isInstanceOf(ObjNotFoundException.class);
173+
174+
var objWithoutReferenced2 =
175+
SimpleTestObj.builder().referenced(-1L).id(randomObjId()).text("foo").build();
176+
persist.storeObj(objWithoutReferenced2, true);
177+
var readWithoutReferenced2 = persist.fetchObj(objWithoutReferenced2.id());
178+
soft.assertThat(readWithoutReferenced2).extracting(Obj::referenced, LONG).isEqualTo(-1L);
179+
soft.assertThat(persist.deleteWithReferenced(objWithoutReferenced2)).isTrue();
180+
soft.assertThatCode(() -> persist.fetchObj(objWithoutReferenced2.id()))
181+
.isInstanceOf(ObjNotFoundException.class);
182+
}
183+
143184
@ParameterizedTest
144185
@MethodSource
145186
public void genericObj(

Diff for: versioned/storage/common/src/main/java/org/projectnessie/versioned/storage/common/persist/Obj.java

+12
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ public interface Obj {
3939
* <p>The value of this attribute is generated exclusively by the {@link Persist} implementations.
4040
*
4141
* <p>This attribute is <em>not</em> consistent when using a caching {@link Persist}.
42+
*
43+
* <p>When <em>reading</em> an object, this value is either {@code 0}, which means that the object
44+
* was written using a Nessie version that did not have this attribute, or a (positive) timestamp
45+
* when the object was written.
46+
*
47+
* <p>When <em>storing</em> an object, this value <em>must</em> be {@code 0}. The only one
48+
* exception is for tests that exercise the relevant code paths - those tests do also use {@code
49+
* -1} as a sentinel to write "NULL".
50+
*
51+
* <p>In any case it is <em>illegal</em> to refer to and/or interpret this attribute from code
52+
* that does not have to deal explicitly with this value, only code that runs maintenance
53+
* operations shall use this value.
4254
*/
4355
@JsonIgnore
4456
@JacksonInject(OBJ_REFERENCED_KEY)

Diff for: versioned/storage/dynamodb/src/main/java/org/projectnessie/versioned/storage/dynamodb/DynamoDBPersist.java

+19-10
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse;
9292
import software.amazon.awssdk.services.dynamodb.model.Condition;
9393
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
94+
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
9495
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
9596
import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue;
9697
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
@@ -549,16 +550,21 @@ public void upsertObjs(@Nonnull Obj[] objs) throws ObjTooLargeException {
549550
public boolean deleteWithReferenced(@Nonnull Obj obj) {
550551
ObjId id = obj.id();
551552

552-
Map<String, ExpectedAttributeValue> expectedValues =
553-
Map.of(
554-
COL_OBJ_REFERENCED,
555-
ExpectedAttributeValue.builder().value(fromS(Long.toString(obj.referenced()))).build());
553+
var deleteItemRequest =
554+
DeleteItemRequest.builder().tableName(backend.tableObjs).key(objKeyMap(id));
555+
if (obj.referenced() != -1L) {
556+
// We take a risk here in case the given object does _not_ have a referenced() value
557+
// (old object). It's not possible in DynamoDB to check for '== 0 OR IS ABSENT/NULL'.
558+
deleteItemRequest.expected(
559+
Map.of(
560+
COL_OBJ_REFERENCED,
561+
ExpectedAttributeValue.builder()
562+
.value(fromS(Long.toString(obj.referenced())))
563+
.build()));
564+
}
556565

557566
try {
558-
backend
559-
.client()
560-
.deleteItem(
561-
b -> b.tableName(backend.tableObjs).key(objKeyMap(id)).expected(expectedValues));
567+
backend.client().deleteItem(deleteItemRequest.build());
562568
return true;
563569
} catch (ConditionalCheckFailedException checkFailedException) {
564570
return false;
@@ -663,7 +669,7 @@ private <T extends Obj> T itemToObj(
663669
Map<String, AttributeValue> inner = item.get(serializer.attributeName()).m();
664670
String versionToken = attributeToString(item, COL_OBJ_VERS);
665671
String referencedString = attributeToString(item, COL_OBJ_REFERENCED);
666-
long referenced = referencedString != null ? Long.parseLong(referencedString) : 0L;
672+
long referenced = referencedString != null ? Long.parseLong(referencedString) : -1L;
667673
@SuppressWarnings("unchecked")
668674
T typed = (T) serializer.fromMap(id, type, referenced, inner, versionToken);
669675
return typed;
@@ -680,7 +686,10 @@ private Map<String, AttributeValue> objToItem(
680686
Map<String, AttributeValue> inner = new HashMap<>();
681687
item.put(KEY_NAME, objKey(id));
682688
item.put(COL_OBJ_TYPE, fromS(type.shortName()));
683-
item.put(COL_OBJ_REFERENCED, fromS(Long.toString(referenced)));
689+
if (obj.referenced() != -1) {
690+
// -1 is a sentinel for AbstractBasePersistTests.deleteWithReferenced()
691+
item.put(COL_OBJ_REFERENCED, fromS(Long.toString(referenced)));
692+
}
684693
UpdateableObj.extractVersionToken(obj).ifPresent(token -> item.put(COL_OBJ_VERS, fromS(token)));
685694
int incrementalIndexSizeLimit =
686695
ignoreSoftSizeRestrictions ? Integer.MAX_VALUE : effectiveIncrementalIndexSizeLimit();

Diff for: versioned/storage/dynamodb2/src/main/java/org/projectnessie/versioned/storage/dynamodb2/DynamoDB2Persist.java

+19-10
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse;
9393
import software.amazon.awssdk.services.dynamodb.model.Condition;
9494
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
95+
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
9596
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
9697
import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue;
9798
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
@@ -550,16 +551,21 @@ public void upsertObjs(@Nonnull Obj[] objs) throws ObjTooLargeException {
550551
public boolean deleteWithReferenced(@Nonnull Obj obj) {
551552
ObjId id = obj.id();
552553

553-
Map<String, ExpectedAttributeValue> expectedValues =
554-
Map.of(
555-
COL_OBJ_REFERENCED,
556-
ExpectedAttributeValue.builder().value(fromS(Long.toString(obj.referenced()))).build());
554+
var deleteItemRequest =
555+
DeleteItemRequest.builder().tableName(backend.tableObjs).key(objKeyMap(id));
556+
if (obj.referenced() != -1L) {
557+
// We take a risk here in case the given object does _not_ have a referenced() value
558+
// (old object). It's not possible in DynamoDB to check for '== 0 OR IS ABSENT/NULL'.
559+
deleteItemRequest.expected(
560+
Map.of(
561+
COL_OBJ_REFERENCED,
562+
ExpectedAttributeValue.builder()
563+
.value(fromS(Long.toString(obj.referenced())))
564+
.build()));
565+
}
557566

558567
try {
559-
backend
560-
.client()
561-
.deleteItem(
562-
b -> b.tableName(backend.tableObjs).key(objKeyMap(id)).expected(expectedValues));
568+
backend.client().deleteItem(deleteItemRequest.build());
563569
return true;
564570
} catch (ConditionalCheckFailedException checkFailedException) {
565571
return false;
@@ -663,7 +669,7 @@ private <T extends Obj> T itemToObj(
663669
ByteBuffer bin = item.get(COL_OBJ_VALUE).b().asByteBuffer();
664670
String versionToken = attributeToString(item, COL_OBJ_VERS);
665671
String referencedString = attributeToString(item, COL_OBJ_REFERENCED);
666-
long referenced = referencedString != null ? Long.parseLong(referencedString) : 0L;
672+
long referenced = referencedString != null ? Long.parseLong(referencedString) : -1L;
667673
Obj obj = deserializeObj(id, referenced, bin, versionToken);
668674
return typeClass.cast(obj);
669675
}
@@ -677,7 +683,10 @@ private Map<String, AttributeValue> objToItem(
677683
Map<String, AttributeValue> item = new HashMap<>();
678684
item.put(KEY_NAME, objKey(id));
679685
item.put(COL_OBJ_TYPE, fromS(type.shortName()));
680-
item.put(COL_OBJ_REFERENCED, fromS(Long.toString(referenced)));
686+
if (obj.referenced() != -1) {
687+
// -1 is a sentinel for AbstractBasePersistTests.deleteWithReferenced()
688+
item.put(COL_OBJ_REFERENCED, fromS(Long.toString(referenced)));
689+
}
681690
UpdateableObj.extractVersionToken(obj).ifPresent(token -> item.put(COL_OBJ_VERS, fromS(token)));
682691
int incrementalIndexSizeLimit =
683692
ignoreSoftSizeRestrictions ? Integer.MAX_VALUE : effectiveIncrementalIndexSizeLimit();

0 commit comments

Comments
 (0)