Found while covering the inheritance path for #5637 (PR #5644). Unrelated to that issue and to the whole partitioned-strategy series - the line predates all of it - so it is filed rather than folded in.
TypeIndex.getIndexesByKeys prunes to one bucket by asking the type's strategy for a bucket index, then applies that same index to every subtype:
final int bucketIndex = type.getBucketIndexByKeys(propNames, keys, ...);
if (bucketIndex > -1) {
List<IndexInternal> polymorphicIndexesOnKeys = type.getPolymorphicBucketIndexByBucketId(
type.getBuckets(false).get(bucketIndex).getFileId(), propNames);
for (DocumentType s : type.getSubTypes()) {
final List<IndexInternal> subIndexes = s.getPolymorphicBucketIndexByBucketId(
s.getBuckets(false).get(bucketIndex).getFileId(), propNames); // <-- here
...
The index is only meaningful modulo the bucket count it was computed against. A subtype is free to declare a different one, and then s.getBuckets(false).get(bucketIndex) is out of range.
Reproduce
CREATE DOCUMENT TYPE Base BUCKETS 3;
CREATE PROPERTY Base.k STRING;
CREATE INDEX ON Base(k) UNIQUE;
ALTER TYPE Base BucketSelectionStrategy `partitioned('k')`;
CREATE DOCUMENT TYPE Derived EXTENDS Base; -- inherits the strategy, but gets the DEFAULT bucket count
INSERT INTO Derived SET k = 'acme';
java.lang.IndexOutOfBoundsException: Index 1 out of bounds for length 1
at java.base/java.util.Collections$UnmodifiableList.get(Collections.java:1502)
at com.arcadedb.index.TypeIndex.getIndexesByKeys(TypeIndex.java:627)
at com.arcadedb.database.TransactionIndexContext.addIndexKeyLock(TransactionIndexContext.java:456)
at com.arcadedb.database.TransactionContext.addIndexOperation(TransactionContext.java:1848)
at com.arcadedb.index.lsm.LSMTreeIndex.put(LSMTreeIndex.java:637)
at com.arcadedb.database.DocumentIndexer.addToIndex(DocumentIndexer.java:103)
at com.arcadedb.database.MutableDocument.save(MutableDocument.java:367)
Giving Derived the same BUCKETS 3 makes it pass, which is what PartitionedStrategyLifecycleTest.aSubtypeInheritsAPartitionedStrategyAndItStillPrunes does, with a comment pointing here.
Note the crash needs no INSERT on the subtype specifically - it is on the read side of the index lookup, so a SELECT through the index reaches it too. The direction of the mismatch decides which: a subtype with FEWER buckets than its parent throws, and one with MORE silently prunes to a bucket the record was never placed in, which is the #5589 failure mode again (wrong bucket, record silently missed) rather than an exception.
Where the fix belongs
bucketIndex is the wrong currency to pass across a type boundary. Each subtype has to re-derive its own placement from the key, through its own strategy and its own modulus - s.getBucketIndexByKeys(propNames, keys, async) rather than reusing the parent's answer - and decline to prune that subtype (fall back to its full bucket list) whenever it cannot, e.g. when the subtype is not partitioned at all or its strategy declines. Worth checking the symmetric assumption elsewhere: SelectExecutionPlanner and the Cypher PartitionPruning rule both turn a bucket index into a bucket name and would have the same question to answer for polymorphic queries.
Alternatively, refuse the mismatch at schema time - a subtype of a partitioned type must inherit its parent's bucket count - but that is a real restriction on existing databases and does nothing for the ones already in that shape, so the read-side fix looks like the one worth having.
Found while covering the inheritance path for #5637 (PR #5644). Unrelated to that issue and to the whole partitioned-strategy series - the line predates all of it - so it is filed rather than folded in.
TypeIndex.getIndexesByKeysprunes to one bucket by asking the type's strategy for a bucket index, then applies that same index to every subtype:The index is only meaningful modulo the bucket count it was computed against. A subtype is free to declare a different one, and then
s.getBuckets(false).get(bucketIndex)is out of range.Reproduce
Giving
Derivedthe sameBUCKETS 3makes it pass, which is whatPartitionedStrategyLifecycleTest.aSubtypeInheritsAPartitionedStrategyAndItStillPrunesdoes, with a comment pointing here.Note the crash needs no
INSERTon the subtype specifically - it is on the read side of the index lookup, so aSELECTthrough the index reaches it too. The direction of the mismatch decides which: a subtype with FEWER buckets than its parent throws, and one with MORE silently prunes to a bucket the record was never placed in, which is the #5589 failure mode again (wrong bucket, record silently missed) rather than an exception.Where the fix belongs
bucketIndexis the wrong currency to pass across a type boundary. Each subtype has to re-derive its own placement from the key, through its own strategy and its own modulus -s.getBucketIndexByKeys(propNames, keys, async)rather than reusing the parent's answer - and decline to prune that subtype (fall back to its full bucket list) whenever it cannot, e.g. when the subtype is not partitioned at all or its strategy declines. Worth checking the symmetric assumption elsewhere:SelectExecutionPlannerand the CypherPartitionPruningrule both turn a bucket index into a bucket name and would have the same question to answer for polymorphic queries.Alternatively, refuse the mismatch at schema time - a subtype of a partitioned type must inherit its parent's bucket count - but that is a real restriction on existing databases and does nothing for the ones already in that shape, so the read-side fix looks like the one worth having.