Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ac25ec5
improve(query): clarify condition resolution semantics
contrueCT Apr 12, 2026
c4cf3d5
test(core): avoid unnecessary data setup in label index regression
contrueCT Apr 13, 2026
ed3b788
improve(query): consolidate unique label resolution
contrueCT Apr 23, 2026
a326f44
test(core): cover edge label query semantics
contrueCT Jun 4, 2026
b10e3c2
fix(core): tolerate missing related index labels
contrueCT Jun 5, 2026
a183571
fix(core): skip stale index entries
contrueCT Jun 5, 2026
ebc31c8
fix(core): stabilize hstore range index ordering
contrueCT Jun 5, 2026
2df4802
fix(core): reset hstore range scan offset
contrueCT Jun 7, 2026
939cc12
fix(core): preserve sorted hstore range ordering
contrueCT Jun 8, 2026
9300691
fix(core): handle non-eq label-only traversals
contrueCT Jun 11, 2026
7c85cca
fix(hstore): preserve range index paging order
contrueCT Jun 13, 2026
9e24432
fix(hstore): scope ordered range scan to indexes
contrueCT Jun 14, 2026
3646a14
fix(hstore): route order-sensitive range scans through ordered merge
contrueCT Jun 15, 2026
9eb75e8
fix(hstore): stream ordered range scans lazily
contrueCT Jun 15, 2026
212e89a
fix(hstore): keep range ordering in index layer
contrueCT Jun 15, 2026
b16f5f1
fix(hstore): defer ordered range paging follow-up
contrueCT Jun 16, 2026
18fe87f
Merge branch 'master' into task/improve-condition-query-semantics
imbajin Jun 18, 2026
057b390
Merge branch 'master' into task/improve-condition-query-semantics
imbajin Jun 23, 2026
2627903
fix(hstore): restore backend range page state
contrueCT Jun 24, 2026
c35b389
fix(core): remove hstore range paging workaround
contrueCT Jun 24, 2026
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
Expand Up @@ -258,7 +258,7 @@ private QueryResults<R> each(IdHolder holder) {
return null;
}

return this.queryByIndexIds(ids);
return this.queryByIndexIds(ids, holder.keepOrder());
});
}

Expand All @@ -275,7 +275,8 @@ public PageResults<R> iterator(int index, String page, long pageSize) {
return PageResults.emptyIterator();
}

QueryResults<R> results = this.queryByIndexIds(pageIds.ids());
QueryResults<R> results = this.queryByIndexIds(pageIds.ids(),
holder.keepOrder());

return new PageResults<>(results, pageIds.pageState());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,24 +256,27 @@ public boolean containsLabelOrUserpropRelation() {
return false;
}

/**
* Returns the legacy condition value of the specified key.
*
* This method keeps the historical behavior for existing callers:
* <ul>
* <li>returns {@code null} if no top-level EQ/IN relation exists</li>
* <li>returns {@code null} if top-level EQ/IN relations resolve to empty</li>
* <li>returns the single value if only one value is resolved</li>
* <li>returns the raw IN list if there is exactly one top-level IN relation</li>
* <li>throws if multiple values remain after resolving several relations</li>
* </ul>
*
* Prefer {@link #conditionValues(Object)}, {@link #uniqueConditionValue(Object)}
* or {@link #conditionValue(Object)} for new code that needs explicit
* semantics.
*/
@Watched
public <T> T condition(Object key) {
List<Object> valuesEQ = InsertionOrderUtil.newList();
List<Object> valuesIN = InsertionOrderUtil.newList();
for (Condition c : this.conditions) {
if (c.isRelation()) {
Condition.Relation r = (Condition.Relation) c;
if (r.key().equals(key)) {
if (r.relation() == RelationType.EQ) {
valuesEQ.add(r.value());
} else if (r.relation() == RelationType.IN) {
Object value = r.value();
assert value instanceof List;
valuesIN.add(value);
}
}
}
}
this.collectConditionValues(key, valuesEQ, valuesIN);
if (valuesEQ.isEmpty() && valuesIN.isEmpty()) {
return null;
}
Expand All @@ -288,29 +291,8 @@ public <T> T condition(Object key) {
return value;
}

boolean initialized = false;
Set<Object> intersectValues = InsertionOrderUtil.newSet();
for (Object value : valuesEQ) {
List<Object> valueAsList = ImmutableList.of(value);
if (!initialized) {
intersectValues.addAll(valueAsList);
initialized = true;
} else {
CollectionUtil.intersectWithModify(intersectValues,
valueAsList);
}
}
for (Object value : valuesIN) {
@SuppressWarnings("unchecked")
List<Object> valueAsList = (List<Object>) value;
if (!initialized) {
intersectValues.addAll(valueAsList);
initialized = true;
} else {
CollectionUtil.intersectWithModify(intersectValues,
valueAsList);
}
}
Set<Object> intersectValues = this.resolveConditionValues(valuesEQ,
valuesIN);

if (intersectValues.isEmpty()) {
return null;
Expand All @@ -323,20 +305,151 @@ public <T> T condition(Object key) {
return value;
}

/**
* Returns whether there is any top-level relation for the specified key.
*/
public boolean containsCondition(Object key) {
for (Condition c : this.conditions) {
if (c.isRelation()) {
Condition.Relation r = (Condition.Relation) c;
if (r.key().equals(key)) {
return true;
}
}
}
return false;
}

/**
* Returns the resolved candidate values of the specified key from
* top-level EQ/IN relations.
*
* Use {@link #containsConditionValues(Object)} to distinguish "no EQ/IN
* condition" from "EQ/IN conditions exist but resolve to an empty
* intersection".
*/
public Set<Object> conditionValues(Object key) {
List<Object> valuesEQ = InsertionOrderUtil.newList();
List<Object> valuesIN = InsertionOrderUtil.newList();
this.collectConditionValues(key, valuesEQ, valuesIN);
if (valuesEQ.isEmpty() && valuesIN.isEmpty()) {
return InsertionOrderUtil.newSet();
}
return this.resolveConditionValues(valuesEQ, valuesIN);
}

/**
* Returns whether there is any top-level EQ/IN relation for the specified
* key.
*/
public boolean containsConditionValues(Object key) {
for (Condition c : this.conditions) {
if (c.isRelation()) {
Condition.Relation r = (Condition.Relation) c;
if (r.key().equals(key) &&
(r.relation() == RelationType.EQ ||
r.relation() == RelationType.IN)) {
return true;
}
}
}
return false;
}

/**
* Returns the unique resolved value of the specified key from top-level
* EQ/IN relations.
*
* Returns {@code null} when the resolved candidate set is empty. Throws
* if multiple values remain after resolution.
*/
public <T> T conditionValue(Object key) {
Set<Object> values = this.conditionValues(key);
if (values.isEmpty()) {
return null;
}
E.checkState(values.size() == 1,
"Illegal key '%s' with more than one value: %s",
key, values);
@SuppressWarnings("unchecked")
T value = (T) values.iterator().next();
return value;
}

/**
* Returns the unique resolved value of the specified key from top-level
* EQ/IN relations, or {@code null} if the resolved candidate set doesn't
* contain exactly one value.
*
* Use this method when callers want "single-or-null" semantics instead of
* treating multiple remaining values as an error.
*/
public <T> T uniqueConditionValue(Object key) {
Set<Object> values = this.conditionValues(key);
if (values.size() != 1) {
return null;
}
@SuppressWarnings("unchecked")
T value = (T) values.iterator().next();
return value;
}

public void unsetCondition(Object key) {
this.conditions.removeIf(c -> c.isRelation() && ((Relation) c).key().equals(key));
}

public boolean containsCondition(HugeKeys key) {
return this.containsCondition((Object) key);
}

public boolean containsConditionValues(HugeKeys key) {
return this.containsConditionValues((Object) key);
}

private void collectConditionValues(Object key, List<Object> valuesEQ,
List<Object> valuesIN) {
for (Condition c : this.conditions) {
if (c.isRelation()) {
Condition.Relation r = (Condition.Relation) c;
if (r.key().equals(key)) {
return true;
if (r.relation() == RelationType.EQ) {
valuesEQ.add(r.value());
} else if (r.relation() == RelationType.IN) {
Object value = r.value();
assert value instanceof List;
valuesIN.add(value);
}
}
}
}
return false;
}

private Set<Object> resolveConditionValues(List<Object> valuesEQ,
List<Object> valuesIN) {
boolean initialized = false;
Comment thread
contrueCT marked this conversation as resolved.
Set<Object> intersectValues = InsertionOrderUtil.newSet();
for (Object value : valuesEQ) {
List<Object> valueAsList = ImmutableList.of(value);
if (!initialized) {
intersectValues.addAll(valueAsList);
initialized = true;
} else {
CollectionUtil.intersectWithModify(intersectValues,
valueAsList);
}
}
for (Object value : valuesIN) {
@SuppressWarnings("unchecked")
List<Object> valueAsList = (List<Object>) value;
if (!initialized) {
intersectValues.addAll(valueAsList);
initialized = true;
} else {
CollectionUtil.intersectWithModify(intersectValues,
valueAsList);
}
}
return intersectValues;
}

public boolean containsCondition(Condition.RelationType type) {
Expand Down Expand Up @@ -566,6 +679,15 @@ public boolean hasNeqCondition() {
return false;
}

public boolean hasUserpropNeqCondition() {
for (Condition.Relation r : this.userpropRelations()) {
if (r.relation() == RelationType.NEQ) {
return true;
}
}
return false;
}

public boolean matchUserpropKeys(List<Id> keys) {
Set<Id> conditionKeys = this.userpropKeys();
return !keys.isEmpty() && conditionKeys.containsAll(keys);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ public <T extends Idfiable> Iterator<T> keepInputOrderIfNeeded(
return origin;
}
Collection<Id> ids;
if (!this.mustSortByInputIds() || this.paging() ||
if (!this.mustSortByInputIds() ||
(ids = this.queryIds()).size() <= 1) {
/*
* Return the original iterator if it's paging query or if the
* query input is less than one id, or don't have to do sort.
* NOTE: queryIds() only return the first batch of index query
* Return the original iterator if the query input is less than one
* id, or don't have to do sort.
* NOTE: queryIds() only return the first batch of index query.
*/
return origin;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ private Query writeQueryEdgeRangeCondition(ConditionQuery cq) {
if (direction == null) {
direction = Directions.OUT;
}
Id label = cq.condition(HugeKeys.LABEL);
Id label = (Id) this.edgeIdConditionValue(cq, HugeKeys.LABEL);
Comment thread
contrueCT marked this conversation as resolved.

BytesBuffer start = BytesBuffer.allocate(BytesBuffer.BUF_EDGE_ID);
writePartitionedId(HugeType.EDGE, vertex, start);
Expand Down Expand Up @@ -722,7 +722,7 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) {
int count = 0;
BytesBuffer buffer = BytesBuffer.allocate(BytesBuffer.BUF_EDGE_ID);
for (HugeKeys key : EdgeId.KEYS) {
Object value = cq.condition(key);
Object value = this.edgeIdConditionValue(cq, key);

if (value != null) {
count++;
Expand Down Expand Up @@ -763,6 +763,17 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) {
return null;
}

private Object edgeIdConditionValue(ConditionQuery cq, HugeKeys key) {
if (key == HugeKeys.LABEL) {
/*
* LABEL may still be represented by multiple top-level EQ/IN
* relations before strict edge-id serialization.
*/
return cq.conditionValue(key);
}
return cq.condition(key);
}

@Override
protected Query writeQueryCondition(Query query) {
HugeType type = query.resultType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ private Query writeQueryEdgeRangeCondition(ConditionQuery cq) {
if (direction == null) {
direction = Directions.OUT;
}
Object label = cq.condition(HugeKeys.LABEL);
Object label = this.edgeIdConditionValue(cq, HugeKeys.LABEL);

List<String> start = new ArrayList<>(cq.conditionsSize());
start.add(writeEntryId((Id) vertex));
Expand Down Expand Up @@ -491,7 +491,7 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) {
List<String> condParts = new ArrayList<>(cq.conditionsSize());

for (HugeKeys key : EdgeId.KEYS) {
Object value = cq.condition(key);
Object value = this.edgeIdConditionValue(cq, key);
if (value == null) {
break;
}
Expand All @@ -516,6 +516,17 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) {
return null;
}

private Object edgeIdConditionValue(ConditionQuery cq, HugeKeys key) {
if (key == HugeKeys.LABEL) {
/*
* LABEL may still be represented by multiple top-level EQ/IN
* relations before strict edge-id serialization.
*/
return cq.conditionValue(key);
}
return cq.condition(key);
}

@Override
protected Query writeQueryCondition(Query query) {
ConditionQuery result = (ConditionQuery) query;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ public boolean matched(Query query) {
int conditionsSize = cq.conditionsSize();
Object owner = cq.condition(HugeKeys.OWNER_VERTEX);
Directions direction = cq.condition(HugeKeys.DIRECTION);
Id label = cq.condition(HugeKeys.LABEL);
Id label = cq.uniqueConditionValue(HugeKeys.LABEL);

if (direction == null && conditionsSize > 1) {
for (Condition cond : cq.conditions()) {
Expand Down Expand Up @@ -316,7 +316,7 @@ private Iterator<HugeEdge> query(ConditionQuery query) {
if (dir == null) {
dir = Directions.BOTH;
}
Id label = query.condition(HugeKeys.LABEL);
Id label = query.uniqueConditionValue(HugeKeys.LABEL);
if (label == null) {
label = IdGenerator.ZERO;
}
Expand Down
Loading
Loading