Skip to content

fix(hstore): don't push sysprop-only range queries down to the store - #3184

Merged
imbajin merged 5 commits into
apache:masterfrom
SebastianGruza:fix-hstore-range-sinking-guard
Sep 16, 2026
Merged

imbajin merged 5 commits into
apache:masterfrom
SebastianGruza:fix-hstore-range-sinking-guard

Conversation

@SebastianGruza

@SebastianGruza SebastianGruza commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Purpose of the PR

On 1.7.x with HStore, every sort-key prefix-equality or range traversal fails, e.g. g.V('a').outE('flow').has('asset','ETC')Can't construct Cardinality from code 0, or with a range on the second sort key → Unsupported data type UNKNOWN. Minimal REST+Gremlin reproducer: https://gist.github.com/SebastianGruza/616f81e915f00f08c4be3dc447ca77c7 (analysis in #3090 (comment) — see the September comment).

Root cause of the crash path: such a query reaches HstoreTable.queryByRange() with sysprop conditions only (owner vertex, direction, label, sort values) — all already enforced by the scan key range — but queryByRange() pushed the serialized ConditionQuery to the store unconditionally. The store-side row decoder then tries to parse property values it cannot parse (server writes raw values, store reader expects a self-describing (cardinality<<6)|dataType byte) and crashes on the first row in range.

Main Changes

  1. queryByRange() now applies the same prepareConditionQuery() guard that queryByPrefix() already uses: the query is pushed down only when user-prop conditions remain. No on-disk format change, no store-side change; the proper versioned sinking codec remains tracked in Track HStore core-test exclusions and property codec mismatch #3090.
  2. Both prepareConditionQuery() / prepareConditionQueryList() now operate on a copy of the origin query instead of mutating it via resetConditions() — core still uses the origin query for its own result filtering after the scan returns.

Verifying these changes

  • New unit tests in HstoreTableTest (a scan-recording Session fake):
    • sysprop-only range query → scan() receives null query bytes, origin query untouched;
    • range query with a user-prop condition → query bytes are pushed and the origin query keeps all its conditions (guards the copy-not-mutate change).
  • mvn -pl hugegraph-server/hugegraph-hstore -am test -Dtest=HstoreTableTest: 7/7 pass.
  • Live A/B on a local PD + HStore + Server cluster (single-node, fresh graph, reproducer above):
    • master (98477f0): sort-key prefix query fails with Can't construct Cardinality from code 0, range on the second sort key fails with Unsupported data type UNKNOWN — exactly as reported;
    • this branch: all 6 reproducer queries return correct results (3 / 1 / 2 / 2 / 1 / 0 edges), verified with a jar swap in both directions on the same running cluster.

Does this PR potentially affect the following parts?

  • Nope
  • Logic (the HStore range-scan pushdown path)

🤖 Generated with Claude Code

Note on codecov/patch: the red patch check is an artifact — the surefire run of the hugegraph-hstore module is not part of the coverage upload, so the report shows 0 % for the changed lines although HstoreTableTest (8 tests) covers them. Scale measurement (13 sort-key shapes on 5 M edges, HStore vs RocksDB id sets): https://github.com/SebastianGruza/hugegraph-validation/tree/master/results/pr-3184-sortkeys

A sort-key prefix/range traversal reaches HstoreTable.queryByRange() with
sysprop conditions only (owner vertex, direction, label, sort values) --
all already enforced by the scan key range. queryByRange() still pushed
the serialized ConditionQuery down unconditionally, so the store tried to
decode row property values it cannot parse (the server writes raw values,
the store-side reader expects a self-describing (cardinality<<6)|dataType
byte) and every such query failed with errors like:
'Can't construct Cardinality from code 0' /
'Unsupported data type UNKNOWN'.

Apply the same prepareConditionQuery() guard that queryByPrefix() already
uses: push the query down only when user-prop conditions remain.

Also make both prepare methods operate on a copy of the origin query
instead of mutating it via resetConditions() -- core still uses the
origin query for its own result filtering after the scan returns.

Part of apache#3090 (interim mitigation; the versioned sinking codec is
tracked separately there).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SebastianGruza

Copy link
Copy Markdown
Contributor Author

Live A/B validation — single-node PD + Store + Server cluster built from source (Temurin 17), fresh graph, schema/data/queries from the reproducer linked in the PR description. Same running cluster, hugegraph-hstore jar swapped in both directions between runs.

Before — master (98477f0): both sort-key queries crash in store-side row decoding, reached from HstoreTable.queryByRange:653:

master: full responses with stack traces (2 queries, 2 crashes)
=== master (98477f0), hugegraph-hstore jar without the patch ===
### GET /graph/edges?vertex_id=a&direction=OUT&label=flow&properties={"asset":"ETC"}
{"exception":"class org.apache.hugegraph.store.client.type.HgStoreClientException","message":"io.grpc.StatusRuntimeException: INTERNAL: an exception occurred while scanning data:Can't construct Cardinality from code 0,java.lang.IllegalArgumentException: Can't construct Cardinality from code 0\n\tat com.google.common.base.Preconditions.checkArgument(Preconditions.java:165)\n\tat org.apache.hugegraph.util.E.checkArgument(E.java:52)\n\tat org.apache.hugegraph.type.define.SerialEnum.fromCode(SerialEnum.java:50)\n\tat org.apache.hugegraph.serializer.BytesBuffer.readProperty(BytesBuffer.java:589)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseProperty(BinaryElementSerializer.java:116)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseProperties(BinaryElementSerializer.java:135)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseEdge(BinaryElementSerializer.java:296)\n\tat org.apache.hugegraph.store.business.FilterIterator.hasNext(FilterIterator.java:67)\n\tat org.apache.hugegraph.store.business.SelectIterator.hasNext(SelectIterator.java:74)\n\tat org.apache.hugegraph.store.node.grpc.ScanStreamResponse.lambda$initIterator$0(ScanStreamResponse.java:146)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)\n\tat java.base/java.lang.Thread.run(Thread.java:840)\n","cause":"io.grpc.StatusRuntimeException: INTERNAL: an exception occurred while scanning data:Can't construct Cardinality from code 0,java.lang.IllegalArgumentException: Can't construct Cardinality from code 0\n\tat com.google.common.base.Preconditions.checkArgument(Preconditions.java:165)\n\tat org.apache.hugegraph.util.E.checkArgument(E.java:52)\n\tat org.apache.hugegraph.type.define.SerialEnum.fromCode(SerialEnum.java:50)\n\tat org.apache.hugegraph.serializer.BytesBuffer.readProperty(BytesBuffer.java:589)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseProperty(BinaryElementSerializer.java:116)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseProperties(BinaryElementSerializer.java:135)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseEdge(BinaryElementSerializer.java:296)\n\tat org.apache.hugegraph.store.business.FilterIterator.hasNext(FilterIterator.java:67)\n\tat org.apache.hugegraph.store.business.SelectIterator.hasNext(SelectIterator.java:74)\n\tat org.apache.hugegraph.store.node.grpc.ScanStreamResponse.lambda$initIterator$0(ScanStreamResponse.java:146)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)\n\tat java.base/java.lang.Thread.run(Thread.java:840)\n","trace":["org.apache.hugegraph.store.client.type.HgStoreClientException.of(HgStoreClientException.java:46)","org.apache.hugegraph.store.client.util.HgBufferProxy.checkErr(HgBufferProxy.java:159)","org.apache.hugegraph.store.client.util.HgBufferProxy.receive(HgBufferProxy.java:129)","org.apache.hugegraph.store.client.grpc.KvPageScanner.hasNext(KvPageScanner.java:222)","org.apache.hugegraph.store.client.grpc.GrpcKvIteratorImpl.hasNext(GrpcKvIteratorImpl.java:77)","org.apache.hugegraph.store.client.SequencedIterator.getIterator(SequencedIterator.java:64)","org.apache.hugegraph.store.client.SequencedIterator.hasNext(SequencedIterator.java:116)","org.apache.hugegraph.backend.store.hstore.HstoreSessionsImpl$ColumnIterator.<init>(HstoreSessionsImpl.java:245)","org.apache.hugegraph.backend.store.hstore.HstoreSessionsImpl$HstoreSession.scan(HstoreSessionsImpl.java:727)","org.apache.hugegraph.backend.store.hstore.HstoreTable.queryByRange(HstoreTable.java:653)","org.apache.hugegraph.backend.store.hstore.HstoreTable.queryBy(HstoreTable.java:422)","org.apache.hugegraph.backend.store.hstore.HstoreTable.query(HstoreTable.java:319)","org.apache.hugegraph.backend.store.hstore.HstoreStore.query(HstoreStore.java:342)","org.apache.hugegraph.backend.tx.AbstractTransaction.query(AbstractTransaction.java:161)","org.apache.hugegraph.backend.tx.GraphTransaction.lambda$query$0(GraphTransaction.java:542)","org.apache.hugegraph.backend.page.QueryList$OptimizedQuery.iterator(QueryList.java:162)","org.apache.hugegraph.backend.query.QueryResults.lambda$flatMap$0(QueryResults.java:196)","org.apache.hugegraph.iterator.FlatMapperIterator.fetch(FlatMapperIterator.java:60)","org.apache.hugegraph.iterator.WrappedIterator.hasNext(WrappedIterator.java:38)","org.apache.hugegraph.iterator.FlatMapperIterator.fetch(FlatMapperIterator.java:56)","org.apache.hugegraph.iterator.WrappedIterator.hasNext(WrappedIterator.java:38)","org.apache.hugegraph.iterator.FilterIterator.fetch(FilterIterator.java:40)","org.apache.hugegraph.iterator.WrappedIterator.hasNext(WrappedIterator.java:38)","org.apache.hugegraph.backend.query.QueryResults.keepInputOrderIfNeeded(QueryResults.java:109)","org.apache.hugegraph.backend.tx.GraphTransaction.queryEdgesFromBackendInternal(GraphTransaction.java:1106)","org.apache.hugegraph.backend.tx.GraphTransaction.lambda$queryEdgesFromBackend$7(GraphTransaction.java:1071)","java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)","java.base/java.util.AbstractList$RandomAccessSpliterator.forEachRemaining(AbstractList.java:720)","java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)","java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)","java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)","java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)","java.base/java.util.stream.ReferencePipeline.reduce(ReferencePipeline.java:662)","org.apache.hugegraph.backend.tx.GraphTransaction.queryEdgesFromBackend(GraphTransaction.java:1075)","org.apache.hugegraph.backend.cache.CachedGraphTransaction.queryEdgesFromBackend(CachedGraphTransaction.java:422)","org.apache.hugegraph.backend.tx.GraphTransaction.queryEdges(GraphTransaction.java:1013)","org.apache.hugegraph.StandardHugeGraph.edges(StandardHugeGraph.java:865)","org.apache.hugegraph.traversal.optimize.HugeVertexStep.queryEdges(HugeVertexStep.java:116)","org.apache.hugegraph.traversal.optimize.HugeVertexStep.edges(HugeVertexStep.java:97)","org.apache.hugegraph.traversal.optimize.HugeVertexStep.flatMap(HugeVertexStep.java:76)","org.apache.tinkerpop.gremlin.process.traversal.step.map.FlatMapStep.processNextStart(FlatMapStep.java:49)","org.apache.tinkerpop.gremlin.process.traversal.step.util.AbstractStep.hasNext(AbstractStep.java:150)","org.apache.tinkerpop.gremlin.process.traversal.step.util.ExpandableStepIterator.next(ExpandableStepIterator.java:55)","org.apache.tinkerpop.gremlin.process.traversal.step.filter.FilterStep.processNextStart(FilterStep.java:37)","org.apache.tinkerpop.gremlin.process.traversal.step.util.AbstractStep.hasNext(AbstractStep.java:150)","org.apache.tinkerpop.gremlin.process.traversal.util.DefaultTraversal.hasNext(DefaultTraversal.java:222)","org.apache.hugegraph.serializer.JsonSerializer.writeIterator(JsonSerializer.java:107)","org.apache.hugegraph.serializer.JsonSerializer.writeEdges(JsonSerializer.java:253)","org.apache.hugegraph.api.graph.EdgeAPI.list(EdgeAPI.java:356)","java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)","java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)","java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)","java.base/java.lang.reflect.Method.invoke(Method.java:569)","org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory.lambda$static$0(ResourceMethodInvocationHandlerFactory.java:52)","org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher$1.run(AbstractJavaResourceMethodDispatcher.java:124)","org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.invoke(AbstractJavaResourceMethodDispatcher.java:167)","org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$TypeOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:219)","org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:79)","org.glassfish.jersey.server.model.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:475)","org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:397)","org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:81)","org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:255)","org.glassfish.jersey.internal.Errors$1.call(Errors.java:248)","org.glassfish.jersey.internal.Errors$1.call(Errors.java:244)","org.glassfish.jersey.internal.Errors.process(Errors.java:292)","org.glassfish.jersey.internal.Errors.process(Errors.java:274)","org.glassfish.jersey.internal.Errors.process(Errors.java:244)","org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:265)","org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:234)","org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:684)","org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer.service(GrizzlyHttpContainer.java:356)","org.glassfish.grizzly.http.server.HttpHandler$1.run(HttpHandler.java:190)","org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:535)","org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.run(AbstractThreadPool.java:515)","java.base/java.lang.Thread.run(Thread.java:840)"]}

### GET /graph/edges?vertex_id=a&direction=OUT&label=flow&properties={"asset":"ETC","epoch":"P.gte(150)"}
{"exception":"class org.apache.hugegraph.store.client.type.HgStoreClientException","message":"io.grpc.StatusRuntimeException: INTERNAL: an exception occurred while scanning data:Unsupported data type UNKNOWN,java.lang.IllegalArgumentException: Unsupported data type UNKNOWN\n\tat org.apache.hugegraph.serializer.BytesBuffer.readProperty(BytesBuffer.java:674)\n\tat org.apache.hugegraph.serializer.BytesBuffer.readProperty(BytesBuffer.java:596)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseProperty(BinaryElementSerializer.java:116)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseProperties(BinaryElementSerializer.java:135)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseEdge(BinaryElementSerializer.java:296)\n\tat org.apache.hugegraph.store.business.FilterIterator.hasNext(FilterIterator.java:67)\n\tat org.apache.hugegraph.store.business.SelectIterator.hasNext(SelectIterator.java:74)\n\tat org.apache.hugegraph.store.node.grpc.ScanStreamResponse.lambda$initIterator$0(ScanStreamResponse.java:146)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)\n\tat java.base/java.lang.Thread.run(Thread.java:840)\n","cause":"io.grpc.StatusRuntimeException: INTERNAL: an exception occurred while scanning data:Unsupported data type UNKNOWN,java.lang.IllegalArgumentException: Unsupported data type UNKNOWN\n\tat org.apache.hugegraph.serializer.BytesBuffer.readProperty(BytesBuffer.java:674)\n\tat org.apache.hugegraph.serializer.BytesBuffer.readProperty(BytesBuffer.java:596)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseProperty(BinaryElementSerializer.java:116)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseProperties(BinaryElementSerializer.java:135)\n\tat org.apache.hugegraph.serializer.BinaryElementSerializer.parseEdge(BinaryElementSerializer.java:296)\n\tat org.apache.hugegraph.store.business.FilterIterator.hasNext(FilterIterator.java:67)\n\tat org.apache.hugegraph.store.business.SelectIterator.hasNext(SelectIterator.java:74)\n\tat org.apache.hugegraph.store.node.grpc.ScanStreamResponse.lambda$initIterator$0(ScanStreamResponse.java:146)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)\n\tat java.base/java.lang.Thread.run(Thread.java:840)\n","trace":["org.apache.hugegraph.store.client.type.HgStoreClientException.of(HgStoreClientException.java:46)","org.apache.hugegraph.store.client.util.HgBufferProxy.checkErr(HgBufferProxy.java:159)","org.apache.hugegraph.store.client.util.HgBufferProxy.receive(HgBufferProxy.java:129)","org.apache.hugegraph.store.client.grpc.KvPageScanner.hasNext(KvPageScanner.java:222)","org.apache.hugegraph.store.client.grpc.GrpcKvIteratorImpl.hasNext(GrpcKvIteratorImpl.java:77)","org.apache.hugegraph.store.client.SequencedIterator.getIterator(SequencedIterator.java:64)","org.apache.hugegraph.store.client.SequencedIterator.hasNext(SequencedIterator.java:116)","org.apache.hugegraph.backend.store.hstore.HstoreSessionsImpl$ColumnIterator.<init>(HstoreSessionsImpl.java:245)","org.apache.hugegraph.backend.store.hstore.HstoreSessionsImpl$HstoreSession.scan(HstoreSessionsImpl.java:727)","org.apache.hugegraph.backend.store.hstore.HstoreTable.queryByRange(HstoreTable.java:653)","org.apache.hugegraph.backend.store.hstore.HstoreTable.queryBy(HstoreTable.java:422)","org.apache.hugegraph.backend.store.hstore.HstoreTable.query(HstoreTable.java:319)","org.apache.hugegraph.backend.store.hstore.HstoreStore.query(HstoreStore.java:342)","org.apache.hugegraph.backend.tx.AbstractTransaction.query(AbstractTransaction.java:161)","org.apache.hugegraph.backend.tx.GraphTransaction.lambda$query$0(GraphTransaction.java:542)","org.apache.hugegraph.backend.page.QueryList$OptimizedQuery.iterator(QueryList.java:162)","org.apache.hugegraph.backend.query.QueryResults.lambda$flatMap$0(QueryResults.java:196)","org.apache.hugegraph.iterator.FlatMapperIterator.fetch(FlatMapperIterator.java:60)","org.apache.hugegraph.iterator.WrappedIterator.hasNext(WrappedIterator.java:38)","org.apache.hugegraph.iterator.FlatMapperIterator.fetch(FlatMapperIterator.java:56)","org.apache.hugegraph.iterator.WrappedIterator.hasNext(WrappedIterator.java:38)","org.apache.hugegraph.iterator.FilterIterator.fetch(FilterIterator.java:40)","org.apache.hugegraph.iterator.WrappedIterator.hasNext(WrappedIterator.java:38)","org.apache.hugegraph.backend.query.QueryResults.keepInputOrderIfNeeded(QueryResults.java:109)","org.apache.hugegraph.backend.tx.GraphTransaction.queryEdgesFromBackendInternal(GraphTransaction.java:1106)","org.apache.hugegraph.backend.tx.GraphTransaction.lambda$queryEdgesFromBackend$7(GraphTransaction.java:1071)","java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)","java.base/java.util.AbstractList$RandomAccessSpliterator.forEachRemaining(AbstractList.java:720)","java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)","java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)","java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)","java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)","java.base/java.util.stream.ReferencePipeline.reduce(ReferencePipeline.java:662)","org.apache.hugegraph.backend.tx.GraphTransaction.queryEdgesFromBackend(GraphTransaction.java:1075)","org.apache.hugegraph.backend.cache.CachedGraphTransaction.queryEdgesFromBackend(CachedGraphTransaction.java:422)","org.apache.hugegraph.backend.tx.GraphTransaction.queryEdges(GraphTransaction.java:1013)","org.apache.hugegraph.StandardHugeGraph.edges(StandardHugeGraph.java:865)","org.apache.hugegraph.traversal.optimize.HugeVertexStep.queryEdges(HugeVertexStep.java:116)","org.apache.hugegraph.traversal.optimize.HugeVertexStep.edges(HugeVertexStep.java:97)","org.apache.hugegraph.traversal.optimize.HugeVertexStep.flatMap(HugeVertexStep.java:76)","org.apache.tinkerpop.gremlin.process.traversal.step.map.FlatMapStep.processNextStart(FlatMapStep.java:49)","org.apache.tinkerpop.gremlin.process.traversal.step.util.AbstractStep.hasNext(AbstractStep.java:150)","org.apache.tinkerpop.gremlin.process.traversal.step.util.ExpandableStepIterator.next(ExpandableStepIterator.java:55)","org.apache.tinkerpop.gremlin.process.traversal.step.filter.FilterStep.processNextStart(FilterStep.java:37)","org.apache.tinkerpop.gremlin.process.traversal.step.util.AbstractStep.hasNext(AbstractStep.java:150)","org.apache.tinkerpop.gremlin.process.traversal.util.DefaultTraversal.hasNext(DefaultTraversal.java:222)","org.apache.hugegraph.serializer.JsonSerializer.writeIterator(JsonSerializer.java:107)","org.apache.hugegraph.serializer.JsonSerializer.writeEdges(JsonSerializer.java:253)","org.apache.hugegraph.api.graph.EdgeAPI.list(EdgeAPI.java:356)","java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)","java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)","java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)","java.base/java.lang.reflect.Method.invoke(Method.java:569)","org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory.lambda$static$0(ResourceMethodInvocationHandlerFactory.java:52)","org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher$1.run(AbstractJavaResourceMethodDispatcher.java:124)","org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.invoke(AbstractJavaResourceMethodDispatcher.java:167)","org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$TypeOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:219)","org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:79)","org.glassfish.jersey.server.model.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:475)","org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:397)","org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:81)","org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:255)","org.glassfish.jersey.internal.Errors$1.call(Errors.java:248)","org.glassfish.jersey.internal.Errors$1.call(Errors.java:244)","org.glassfish.jersey.internal.Errors.process(Errors.java:292)","org.glassfish.jersey.internal.Errors.process(Errors.java:274)","org.glassfish.jersey.internal.Errors.process(Errors.java:244)","org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:265)","org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:234)","org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:684)","org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer.service(GrizzlyHttpContainer.java:356)","org.glassfish.grizzly.http.server.HttpHandler$1.run(HttpHandler.java:190)","org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:535)","org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.run(AbstractThreadPool.java:515)","java.base/java.lang.Thread.run(Thread.java:840)"]}

After — this branch (847fab9): the same queries return the correct edges (the remaining 4 reproducer queries also pass — 3/1/2/0 edges as expected):

=== branch fix-hstore-range-sinking-guard (847fab9) ===
### GET /graph/edges?vertex_id=a&direction=OUT&label=flow&properties={"asset":"ETC"}
{"edges":[{"id":"Sa>1>1>ETC!21_>Sb","label":"flow","type":"edge","outV":"a","outVLabel":"node","inV":"b","inVLabel":"node","properties":{"amount":1.5,"asset":"ETC","epoch":100}},{"id":"Sa>1>1>ETC!238>Sb","label":"flow","type":"edge","outV":"a","outVLabel":"node","inV":"b","inVLabel":"node","properties":{"amount":2.5,"asset":"ETC","epoch":200}}]}

### GET /graph/edges?vertex_id=a&direction=OUT&label=flow&properties={"asset":"ETC","epoch":"P.gte(150)"}
{"edges":[{"id":"Sa>1>1>ETC!238>Sb","label":"flow","type":"edge","outV":"a","outVLabel":"node","inV":"b","inVLabel":"node","properties":{"amount":2.5,"asset":"ETC","epoch":200}}]}

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: no. Summary: The guard is the right shape and I did not find a correctness regression. For sort-key edge queries core already resets user props on the copy it sends down (GraphTransaction.java:1591-1602), the scan key range encodes owner vertex, direction, label, sub-label and sort values (BinarySerializer.java:667-717), and core still filters with cq.test() because the query is marked OptimizedType.SORT_KEYS (GraphTransaction.java:1590, 1933), so dropping the pushdown does not widen the result set. Three notes inline: the sibling guard in prepareConditionQueryList() still lets label-only queries through, the pushdown copy adds a nesting level to the serialized payload, and the new user-prop test does not check what was pushed. Evidence: mvn -o -pl hugegraph-server/hugegraph-hstore -am test -Dtest=HstoreTableTest on JDK 11 at 847fab9 gives BUILD SUCCESS with 7/7 passing, matching the PR description (the same command on JDK 17 fails in hugegraph-commons on Lombok, in files this PR does not touch). Payload sizes come from probes compiled against this head. gh -R apache/hugegraph pr checks 3184 reports no checks on the branch, so the HStore integration lanes are unverified here, and I did not start a cluster, so the store-side decode failure itself is unreproduced.

…tent in test

Review follow-up (thanks @bitflicker64):
- setOriginQuery(null) on the pushdown copy in both prepare methods -- the
  copy() back reference nested the origin query into the serialized payload
  (measured by reviewer: 2275 -> 657 bytes); nothing store-side reads it
- test now decodes the pushed bytes via ConditionQuery.fromBytes and asserts
  the user-prop condition survives, OWNER_VERTEX is dropped and the back
  reference is cleared

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: no. Summary: The guard is sound: the pushed-down query is filter-only on the store side, every sysprop that can reach the edge range path is already encoded in the scan key, and core re-tests the results regardless. The copy-not-mutate change additionally stops the backend mutating a query its caller still owns. Evidence: built this head with JDK 11, mvn -o -pl hugegraph-server/hugegraph-hstore -am test -Dtest=HstoreTableTest gives 7/7. Store side is filter-only: hg-store-node/.../grpc/HgStoreWrapperEx.java hands the query bytes straight to FilterIterator.of, which returns the raw iterator when they are empty (hg-store-core/.../business/FilterIterator.java:45-47) and otherwise runs parseEdge/parseVertex then query.test() (lines 60-72). Core re-tests: the sort-keys branch sets optimized(OptimizedType.SORT_KEYS) (GraphTransaction.java:1590), and queryEdges(Query) feeds filterUnmatchedRecords (line 1014) into rightResultFromIndexQuery, which calls cq.test(elem) (line 1933). Key coverage: BinarySerializer.writeQueryEdgeRangeCondition() (line 667) writes owner vertex, direction, label and sub-label into the key, and the only server-code producer of an OTHER_VERTEX condition (EdgeExistenceTraverser.java:48) uses an EQ sort value, so hasRangeCondition() (line 660) is false for it and it never reaches that method. Payload shape: ConditionQuery.copy() sets originQuery(this) and resets optimizedType/resultsFilter (ConditionQuery.java:575-585), which is why setOriginQuery(null) is needed before bytes() (line 980).

…QueryList via queryByPrefixList

Review follow-ups for apache#3184:
- queryByRange(): compute the pushed query bytes in one place and end with a
  single session.scan(...) call; the ConditionQuery is block-local now.
- HstoreTableTest: ScanRecordingSession records the owner-key list scan
  instead of throwing, and testPrefixListQueryPushesCopyAndKeepsOrigin drives
  prepareConditionQueryList() through queryByPrefixList() with a shared
  origin query: the origin keeps all conditions (including OWNER_VERTEX), the
  pushed payload drops OWNER_VERTEX, keeps LABEL and the user-prop condition,
  and has no back reference.

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: no. Summary: Both cleanups asked for on 1072872 landed without changing behaviour and I found no correctness defect at this head; the single note withdraws the reachability half of my own earlier comments.

Evidence: on JDK 11 at this head, mvn -o -pl hugegraph-server/hugegraph-hstore -am test is BUILD SUCCESS with 11 tests (HstoreTableTest 8/8) and mvn -o -pl hugegraph-server/hugegraph-hstore checkstyle:check is clean; git diff 1072872 0ecc10a is the queryBytes hoist plus the new test, with both former exits of queryByRange() collapsing into the same session.scan(...) call.

On the second behaviour change, which the title does not advertise: with user props present the range path now pushes a copy stripped of the OWNER_VERTEX conditions instead of the untrimmed origin. onlyOwnerVertex() (HstoreTable.java:612) drops only conditions whose relations are all on OWNER_VERTEX, and writeQueryEdgeRangeCondition() already pins that vertex in the scan key (writePartitionedId(HugeType.EDGE, vertex, start), BinarySerializer.java:680), so the trimmed filter cannot admit a row the key range would not return anyway. Suppressing a sysprop-only pushdown also puts hstore where RocksDBTable.queryByRange() (RocksDBTable.java:258-269) already sits: it pushes no query at all.

@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.90%. Comparing base (98477f0) to head (89bb0ec).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
...he/hugegraph/backend/store/hstore/HstoreTable.java 0.00% 12 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3184      +/-   ##
============================================
+ Coverage     37.78%   37.90%   +0.12%     
- Complexity     6556     6604      +48     
============================================
  Files           800      800              
  Lines         68929    69033     +104     
  Branches       9157     9185      +28     
============================================
+ Hits          26046    26170     +124     
+ Misses        39824    39787      -37     
- Partials       3059     3076      +17     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@SebastianGruza

Copy link
Copy Markdown
Contributor Author

@imbajin @contrueCT, a request for a decision on this PR. It is a 12-line guard in HstoreTable.queryByRange(), the same one queryByPrefix() has had for a long time; CI has been green since Sep 7 and @bitflicker64 went through three rounds without finding a regression ("blocking: no" three times, unit tests 8/8, checkstyle clean). There has been no reaction since, so I am asking directly: can it go in ahead of #2994?

The reason I ask about the order: without this guard #2994 cannot be verified on HStore. Our backend axis for #2994 (the same queries on hstore and rocksdb, compared as id sets) gave 38 TARGET-ERR failures from #3090 before #3184 was on the branch, and OK=170 with it. Anyone reviewing #2994 on hstore will hit the same wall; @contrueCT noted this in #3090 on Sep 3.

Codecov

The only red check is codecov/patch. It is an artifact: the surefire run of the hugegraph-hstore module is not part of the coverage upload, so the report shows 0 % for the changed lines even though HstoreTableTest (8 tests) covers them; @bitflicker64's review ran that module locally. I have added this sentence to the PR description so nobody has to work it out.

What this bug blocks on 1.7.0 with HStore

The reproducer in the description has 3 edges. Below is the same failure on 5 M edges, on a schema with four sort keys (xfer[btc_addr, seq, height, asset], vertex ids are BTC/ETH-style addresses), 13 query shapes from one vertex, master 36811483 vs PR head 0ecc10a3, hstore vs rocksdb on the same data. The servers were swapped in place without reloading the data.

shape, from g.V(v) master hstore master rocksdb #3184 hstore id sets #3184 hstore = rocksdb
outE('xfer'), no condition 57 57 57 same
.has('btc_addr',A) prefix 1 500 Can't construct Cardinality from code 0 2 2 same
.has('btc_addr',A).has('seq',S) prefix 2 500 1 1 same
prefix 1 + has('seq',gte(1)) 500 2 2 same
prefix 2 + has('height',between(..)) 500 1 1 same
all four keys eq 1 1 1 same
prefix 3 + has('asset',gte('a')) 1 1 1 same
.has('seq',S) alone (not a prefix, filter) 1 1 1 same
.has('amount',gte(0)) (not a sort key) 57 57 57 same
prefix 1 + has('amount',gte(0)) 500 2 2 same
bothE('xfer').has('btc_addr',A) 500 2 2 same
...inV().inE('xfer').has('btc_addr',A) 500 1 1 same
prefix 1 + valueMap() 500 2 2 same

On master with HStore every partial sort-key prefix and every range on a key other than the last one fails, including bothE, inE and reading the properties; only "no condition", "all keys equal" and non-prefix filters work. So the whole use case sort keys exist for (the vertex-centric index) is unavailable on 1.7.0 with HStore. With #3184 all 13 shapes pass and return the same ids as RocksDB.

Script, raw results and log: results/pr-3184-sortkeys/.

@contrueCT

Copy link
Copy Markdown
Contributor

@SebastianGruza @imbajin I think it makes sense to merge #3184 before #2994.

The change here is relatively isolated: it avoids pushing sysprop-only range conditions to HStore when those constraints are already covered by the key range, while user-property residual conditions are still pushed down. Also, copying the ConditionQuery before trimming it avoids mutating the query still used by the core layer.

More importantly, #3184 removes an existing HStore failure unrelated to #2994. Without it, the HStore regression for #2994 is partially blocked by #3090, which makes failures harder to attribute.

So I suggest merging #3184 first, then rebasing #2994 on the latest master and rerunning the HStore/RocksDB regression matrix.

#3090 should still remain open since #3184 is only a mitigation for the underlying codec issue.

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: no. Summary: The range-path guard is sound and I found no correctness regression at this head: suppressing the pushdown only removes store-side filtering, every sysprop reaching the edge range path is already encoded in the scan key, and core re-tests the results; the copy-not-mutate change stops the backend stripping conditions from a query its caller still owns. One note: the same unguarded pushdown is still live in queryAll(). Evidence: built this head with JDK 17, mvn -o -pl hugegraph-server/hugegraph-hstore -am test -Dtest=HstoreTableTest gives 8/8. Store side is filter-only: FilterIterator.of returns the raw iterator when the query bytes are empty (hg-store-core/.../business/FilterIterator.java:44-50) and otherwise runs parseEdge/parseVertex then query.test() (lines 52-84). Key coverage: BinarySerializer.writeQueryEdgeRangeCondition() (line 667-717) writes owner vertex, direction, label and sub-label into the key. Core re-tests: optimizeQuery() sets optimized(OptimizedType.SORT_KEYS) (GraphTransaction.java:1589) and queryEdges(Query) feeds filterUnmatchedRecords (line 1014) into rightResultFromIndexQuery, which calls cq.test(elem) (line 1933). setOriginQuery(null) is payload-only: the store deserializes into org.apache.hugegraph.query.ConditionQuery (hugegraph-struct 1.7.0), whose test(BaseElement) reads ids, resultsFilter, conditions and element2IndexValueMap and never originQuery or optimizedType (verified with javap -c). manual_review_needed: no check runs are reported for eb5e05ab (combined status pending, no contexts), and no live PD + HStore cluster was available, so the author's A/B result was not reproduced.

@SebastianGruza

Copy link
Copy Markdown
Contributor Author

@contrueCT thanks, agreed on the order. Once #2994 is rebased on a master that contains this PR, I will run the same HStore/RocksDB matrix (the same queries on both backends, compared as id sets, 170+ shapes) on the new head and post the result under #2994.

@bitflicker64 the queryAll() point is handled in 89bb0ec (a comment line) and recorded in #3090 (#3090 (comment)); details and the measurement are in the inline thread.

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: no. Summary: The range-path guard is sound. Sort-key edge queries already have their sysprops encoded in the scan key and core still re-tests results with cq.test(), so dropping the sysprop-only pushdown cannot widen results. Building the pushdown from a copy also stops the backend stripping conditions from a query core still uses. Earlier review points are addressed at this head, and the remaining queryAll() and shard-scan gap is tracked in #3090. Evidence: on this head with JDK 11, mvn -o -pl hugegraph-server/hugegraph-hstore -am test -Dtest=HstoreTableTest passes 8/8. All required checks and workflow runs are green. codecov/patch is red but not a required check, and the changed lines are covered by HstoreTableTest.

@imbajin

imbajin commented Sep 16, 2026

Copy link
Copy Markdown
Member

+1, agreed with the scope and merge order. @SebastianGruza @contrueCT @bitflicker64 this PR can go in ahead of #2994. The range guard is appropriately scoped, and the review threads are now resolved.

After merging, please rebase #2994 and post the HStore/RocksDB regression results for the new head there, as planned. Keep #3090 open for the codec and remaining scan-path follow-ups, and update both threads to distinguish what #3184 fixes from what remains outstanding.

@imbajin imbajin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up context and merge order are noted in #3184 (comment).

@imbajin
imbajin merged commit 1a15e76 into apache:master Sep 16, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants