Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.jetbrains.youtrackdb.internal.core.record.impl;

import com.jetbrains.youtrackdb.internal.core.db.DatabaseSessionEmbedded;
import com.jetbrains.youtrackdb.internal.core.db.record.record.Identifiable;
import com.jetbrains.youtrackdb.internal.core.db.record.record.RID;
import com.jetbrains.youtrackdb.internal.core.db.record.record.Vertex;
import com.jetbrains.youtrackdb.internal.core.db.record.ridbag.LinkBag;
import com.jetbrains.youtrackdb.internal.core.storage.ridbag.RidPair;
import it.unimi.dsi.fastutil.ints.IntSet;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -86,6 +89,21 @@ public Iterator<Vertex> iterator() {
linkBag.iterator(), session, linkBag.size(), acceptedCollectionIds, acceptedRids);
}

/**
* Returns an iterator that yields {@link Identifiable} (RecordId) objects
* directly from the LinkBag's secondary RIDs without calling
* {@code loadEntity()}. Class filter and RID filter are applied on the
* RID (no disk I/O) — only matching RIDs are yielded.
*
* <p>Used by the MATCH engine to defer entity loading to first property
* access via {@code ResultInternal}'s built-in lazy loading.
*/
@Nonnull
public Iterator<Identifiable> ridIterator() {
return new RidOnlyIterator(
linkBag.iterator(), acceptedCollectionIds, acceptedRids);
}

@Override
public int size() {
return linkBag.size();
Expand All @@ -95,4 +113,57 @@ public int size() {
public boolean isSizeable() {
return linkBag.isSizeable();
}

/**
* Iterator that yields RecordId objects from LinkBag secondary RIDs,
* applying class and RID filters without loading entities from storage.
*/
private static final class RidOnlyIterator implements Iterator<Identifiable> {

private final Iterator<RidPair> ridPairIterator;
@Nullable private final IntSet acceptedCollectionIds;
@Nullable private final Set<RID> acceptedRids;
@Nullable private Identifiable nextRid;

RidOnlyIterator(
Iterator<RidPair> ridPairIterator,
@Nullable IntSet acceptedCollectionIds,
@Nullable Set<RID> acceptedRids) {
this.ridPairIterator = ridPairIterator;
this.acceptedCollectionIds = acceptedCollectionIds;
this.acceptedRids = acceptedRids;
}

@Override
public boolean hasNext() {
while (nextRid == null && ridPairIterator.hasNext()) {
nextRid = filterRid(ridPairIterator.next());
}
return nextRid != null;
}

@Override
public Identifiable next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
var current = nextRid;
nextRid = null;
return current;
}

@Nullable private Identifiable filterRid(RidPair ridPair) {
ridPair.validateEdgePair();
var rid = ridPair.secondaryRid();

if (acceptedCollectionIds != null
&& !acceptedCollectionIds.contains(rid.getCollectionId())) {
return null;
}
if (acceptedRids != null && !acceptedRids.contains(rid)) {
return null;
}
return rid;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -466,9 +466,24 @@ public <T> T getProperty(@Nonnull String name) {
if (content != null && content.containsKey(name)) {
//noinspection unchecked
result = (T) content.get(name);
} else {
if (isEntity()) {
result = asEntity().getProperty(name);
} else if (identifiable != null) {
// Fast path: resolve entity directly without going through asEntity()
// which would call isEntity() → isBlob() a second time. For bare RIDs
// (lazy MATCH path) this avoids 2 redundant schema snapshot lookups
// per property access.
Entity entity;
if (identifiable instanceof Entity e) {
entity = e;
} else if (!isBlob()) {
var transaction = session.getActiveTransaction();
entity = transaction.loadEntity(identifiable);
this.identifiable = entity;
} else {
entity = null;
}
if (entity != null) {
//noinspection unchecked
result = (T) entity.getProperty(name);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.jetbrains.youtrackdb.internal.core.query.Result;
import com.jetbrains.youtrackdb.internal.core.record.impl.EntityImpl;
import com.jetbrains.youtrackdb.internal.core.record.impl.PreFilterableLinkBagIterable;
import com.jetbrains.youtrackdb.internal.core.record.impl.VertexFromLinkBagIterable;
import com.jetbrains.youtrackdb.internal.core.sql.executor.ResultInternal;
import com.jetbrains.youtrackdb.internal.core.sql.executor.RidFilterDescriptor;
import com.jetbrains.youtrackdb.internal.core.sql.executor.RidSet;
Expand Down Expand Up @@ -684,6 +685,12 @@ static ExecutionStream toExecutionStream(
case ResultInternal resultInternal -> ExecutionStream.singleton(resultInternal);
case Identifiable identifiable -> ExecutionStream.singleton(
new ResultInternal(session, identifiable));
// RID-only path: yield RecordIds without loading entities from storage.
// ResultInternal wraps each RecordId and defers loadEntity() to first
// property access — intermediate MATCH steps that only traverse by RID
// skip disk I/O entirely.
case VertexFromLinkBagIterable vfli ->
ExecutionStream.iterator(vfli.ridIterator());
case Iterable<?> iterable -> ExecutionStream.iterator(iterable.iterator());
default -> ExecutionStream.empty();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,25 +76,12 @@ public boolean evaluate(Result currentRecord, CommandContext ctx) {
return evaluateAllFunction(currentRecord, ctx);
}

// In-place comparison fast path: avoid deserialization for simple
// "property <op> constant" patterns. Collation is checked inside
// EntityImpl (both serialized and deserialized paths return FALLBACK
// for non-default collation), so no getCollate guard needed here.
if (left.isBaseIdentifier()
&& left.mathExpression instanceof SQLBaseExpression baseExpr
&& right.isEarlyCalculated(ctx)
&& currentRecord instanceof ResultInternal ri
&& ri.asEntityOrNull() instanceof EntityImpl entityImpl) {
var propName = baseExpr.getIdentifier().getSuffix()
.getIdentifier().getStringValue();
var rightVal = right.execute(currentRecord, ctx);

var inPlaceResult = tryInPlaceComparison(entityImpl, propName, rightVal);
if (inPlaceResult != null) {
return inPlaceResult;
}
// FALLBACK: fall through to standard path which handles collation
}
// In-place comparison is intentionally NOT used for the Result overload.
// When the Result wraps a lazy entity, asEntityOrNull() triggers a full
// entity load, and the subsequent page-frame comparison re-deserializes
// the field — double work that is slower than the standard path.
// The Identifiable overload above keeps the in-place optimization because
// the entity is already materialized there.

var leftVal = left.execute(currentRecord, ctx);
var rightVal = right.execute(currentRecord, ctx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,201 @@ public void combinedFilter_requiresBothToPass() {
assertFalse(iterator.hasNext());
}

// =========================================================================
// ridIterator() tests — RID-only iteration without entity loading
// =========================================================================

/**
* ridIterator() yields RecordId objects from the LinkBag without calling
* loadEntity(). Verifies that the returned identifiable has the correct
* RID and that no entity loading occurs.
*/
@Test
public void ridIterator_yieldsRecordIdsWithoutLoading() {
var rid1 = new RecordId(10, 1);
var rid2 = new RecordId(10, 2);
var linkBag = mockLinkBag(
RidPair.ofPair(new RecordId(30, 1), rid1),
RidPair.ofPair(new RecordId(30, 2), rid2));

var iterable = new VertexFromLinkBagIterable(linkBag, session);
var iter = iterable.ridIterator();

assertTrue(iter.hasNext());
assertEquals(rid1, iter.next().getIdentity());
assertTrue(iter.hasNext());
assertEquals(rid2, iter.next().getIdentity());
assertFalse(iter.hasNext());

// Verify loadEntity was never called — entities were not loaded
org.mockito.Mockito.verifyNoInteractions(transaction);
}

/**
* ridIterator() applies class filter by collection ID without loading.
*/
@Test
public void ridIterator_classFilter_skipsNonMatchingCollectionId() {
var matchingRid = new RecordId(10, 1); // collection 10 — accepted
var nonMatchingRid = new RecordId(20, 1); // collection 20 — rejected
var linkBag = mockLinkBag(
RidPair.ofPair(new RecordId(30, 1), nonMatchingRid),
RidPair.ofPair(new RecordId(30, 2), matchingRid));

var iterable = new VertexFromLinkBagIterable(linkBag, session)
.withClassFilter(it.unimi.dsi.fastutil.ints.IntOpenHashSet.of(10));
var iter = iterable.ridIterator();

assertTrue(iter.hasNext());
assertEquals(matchingRid, iter.next().getIdentity());
assertFalse(iter.hasNext());
}

/**
* ridIterator() applies RID filter without loading.
*/
@Test
public void ridIterator_ridFilter_skipsNonMatchingRid() {
var matchingRid = new RecordId(10, 1);
var nonMatchingRid = new RecordId(10, 2);
var linkBag = mockLinkBag(
RidPair.ofPair(new RecordId(30, 1), nonMatchingRid),
RidPair.ofPair(new RecordId(30, 2), matchingRid));

var acceptedRids = new java.util.HashSet<RID>();
acceptedRids.add(matchingRid);

var iterable = new VertexFromLinkBagIterable(linkBag, session)
.withRidFilter(acceptedRids);
var iter = iterable.ridIterator();

assertTrue(iter.hasNext());
assertEquals(matchingRid, iter.next().getIdentity());
assertFalse(iter.hasNext());
}

/**
* ridIterator() on an empty LinkBag returns an empty iterator.
*/
@Test
public void ridIterator_emptyLinkBag_yieldsNothing() {
var linkBag = mockLinkBag();
var iterable = new VertexFromLinkBagIterable(linkBag, session);
assertFalse(iterable.ridIterator().hasNext());
}

/**
* ridIterator() throws NoSuchElementException when exhausted.
*/
@Test(expected = NoSuchElementException.class)
public void ridIterator_throwsWhenExhausted() {
var linkBag = mockLinkBag();
var iterable = new VertexFromLinkBagIterable(linkBag, session);
iterable.ridIterator().next();
}

/**
* ridIterator() hasNext() is idempotent — calling it multiple times
* before next() does not advance past elements.
*/
@Test
public void ridIterator_hasNextIsIdempotent() {
var rid = new RecordId(10, 1);
var linkBag = mockLinkBag(RidPair.ofPair(new RecordId(30, 1), rid));

var iter = new VertexFromLinkBagIterable(linkBag, session).ridIterator();

assertTrue(iter.hasNext());
assertTrue("Second hasNext() should still return true", iter.hasNext());
assertTrue("Third hasNext() should still return true", iter.hasNext());
assertEquals(rid, iter.next().getIdentity());
assertFalse(iter.hasNext());
}

/**
* ridIterator() applies both class and RID filters simultaneously.
* A vertex must pass both to be yielded.
*/
@Test
public void ridIterator_combinedFilter_requiresBothToPass() {
// rid1: collection 10 (accepted) AND in ridSet → yielded
var rid1 = new RecordId(10, 1);
// rid2: collection 10 (accepted) but NOT in ridSet → skipped
var rid2 = new RecordId(10, 2);
// rid3: collection 20 (rejected by class filter) → skipped
var rid3 = new RecordId(20, 1);

var acceptedRids = new java.util.HashSet<RID>();
acceptedRids.add(rid1);

var linkBag = mockLinkBag(
RidPair.ofPair(new RecordId(30, 1), rid3),
RidPair.ofPair(new RecordId(30, 2), rid2),
RidPair.ofPair(new RecordId(30, 3), rid1));

var iter = new VertexFromLinkBagIterable(linkBag, session)
.withClassFilter(it.unimi.dsi.fastutil.ints.IntOpenHashSet.of(10))
.withRidFilter(acceptedRids)
.ridIterator();

assertTrue(iter.hasNext());
assertEquals(rid1, iter.next().getIdentity());
assertFalse(iter.hasNext());

// No entity loading should have occurred
org.mockito.Mockito.verifyNoInteractions(transaction);
}

/**
* ridIterator() yields all matching RIDs when multiple pass the filters,
* preserving LinkBag iteration order.
*/
@Test
public void ridIterator_multipleMatches_preservesOrder() {
var rid1 = new RecordId(10, 1);
var rid2 = new RecordId(10, 2);
var rid3 = new RecordId(10, 3);
var linkBag = mockLinkBag(
RidPair.ofPair(new RecordId(30, 1), rid1),
RidPair.ofPair(new RecordId(30, 2), rid2),
RidPair.ofPair(new RecordId(30, 3), rid3));

var iter = new VertexFromLinkBagIterable(linkBag, session).ridIterator();

assertEquals(rid1, iter.next().getIdentity());
assertEquals(rid2, iter.next().getIdentity());
assertEquals(rid3, iter.next().getIdentity());
assertFalse(iter.hasNext());

org.mockito.Mockito.verifyNoInteractions(transaction);
}

/**
* ridIterator() returns RID objects (not loaded entities). Verifies the
* returned Identifiable is a RecordId instance, not a Vertex or Entity.
*/
@Test
public void ridIterator_returnsRecordIdNotEntity() {
var rid = new RecordId(10, 1);
var linkBag = mockLinkBag(RidPair.ofPair(new RecordId(30, 1), rid));

var result = new VertexFromLinkBagIterable(linkBag, session)
.ridIterator().next();

assertTrue(
"ridIterator should yield RecordId, not a loaded entity",
result instanceof RecordId);
assertEquals(rid, result.getIdentity());
}

private com.jetbrains.youtrackdb.internal.core.db.record.ridbag.LinkBag mockLinkBag(
RidPair... pairs) {
var linkBag = mock(com.jetbrains.youtrackdb.internal.core.db.record.ridbag.LinkBag.class);
when(linkBag.iterator()).thenReturn(List.of(pairs).iterator());
when(linkBag.size()).thenReturn(pairs.length);
return linkBag;
}

/**
* Configures the mock transaction to return a vertex entity when loadEntity
* is called with the given RID.
Expand Down
Loading