bitflicker64 commented on code in PR #833:
URL: 
https://github.com/apache/hugegraph-toolchain/pull/833#discussion_r4349530201


##########
hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/GremlinUtil.java:
##########
@@ -464,56 +459,29 @@ private static boolean chainContinues(String gremlin, int 
newline) {
     }
 
     private static String optimizeStatement(String statement,
-                                            String matchable, int limit) {
+                                            String matchable, int limit,
+                                            boolean preview) {
         int codeEnd = matchable.length();
         while (codeEnd > 0 && Character.isWhitespace(
                                   matchable.charAt(codeEnd - 1))) {
             codeEnd--;
         }
         String code = matchable.substring(0, codeEnd);
-        boolean ignored = IGNORED_PATTERNS.stream().anyMatch(pattern -> {
-            return pattern.matcher(code).find();
-        });
-        if (ignored) {
+        ReadOnlyGremlin.Traversal traversal;
+        try {
+            // Parse once: validation, scopes, literal bounds and cardinality 
use
+            // the same traversal representation. Scripts and mutations retain
+            // their existing execution semantics.
+            traversal = ReadOnlyGremlin.parse(
+                    code.replace('\n', ' ').replace('\r', ' '));
+        } catch (RuntimeException ignored) {

Review Comment:
   Minor: When `ReadOnlyGremlin.parse` rejects a statement, it now runs with no 
default limit. Master's suffix rule gave these statements `.limit(250)`.
   
   Probe at this head (limit 300): scripts such as `x = g.V()` and `name = 
'marko'; g.V().has('name', name).out()`, and side-effect steps such as 
`g.V().store('x').out()`, `g.V().aggregate('a').out()` and 
`g.V().barrier().out()`, come back unchanged from both `optimizeLimit` and 
`optimizePreviewLimit`. The page still shows at most 1000 rows, but Hubble now 
receives the full result for them.
   
   The PR body says there is no suffix fallback, so this is intended. But 
"Complex scripts retain their existing path" reads as if nothing changed for 
them. Please say in the body or README that statements outside the read grammar 
run without the default limit.



##########
hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/ReadOnlyGremlin.java:
##########
@@ -65,13 +143,180 @@ public static void validate(String query) {
         if (!"V".equals(start) && !"E".equals(start)) {
             throw invalid();
         }
-        parser.arguments(0);
-        parser.chain(STEPS, 0);
+        List<Step> steps = new ArrayList<>();
+        steps.add(new Step(start, parser.arguments(0), 0));
+        parser.chain(STEPS.keySet(), 0, steps);
         parser.consume(';');
         parser.whitespace();
         if (parser.position != query.length()) {
             throw invalid();
         }
+        return new Traversal(steps, query.length());
+    }
+
+    private enum Output {
+        PRESERVE, EXPAND, REDUCE, COLLECT, LIMIT, CONSUME
+    }
+
+    static final class Traversal {
+
+        private final List<Step> steps;
+        private final int endPosition;
+
+        private Traversal(List<Step> steps, int endPosition) {
+            this.steps = List.copyOf(steps);
+            this.endPosition = endPosition;
+        }
+
+        int limitPosition(boolean preview, int limit) {
+            // Preview limits include the sentinel row used to report 
truncation.
+            BigInteger previewSize = BigInteger.valueOf((long) limit - 1);
+            Analysis result = this.analyze(previewSize);
+            if (preview && result.largeCollection) {
+                throw new 
ExternalException("gremlin.preview.collection_requires_async", previewSize);
+            }
+            return !result.singleResult && (preview || result.bound == null) ? 
result.position : -1;
+        }
+
+        private Analysis analyze(BigInteger previewSize) {
+            BigInteger bound = null;
+            boolean singleResult = false;
+            boolean largeCollection = false;
+            boolean nestedCollection = false;
+            int position = this.endPosition;
+            for (int i = 0; i < this.steps.size(); i++) {
+                Step step = this.steps.get(i);
+                switch (STEPS.get(step.name)) {
+                    case CONSUME:
+                        // Cap the traversal before its final consumer, 
preserving
+                        // the List/Set/value/boolean result type. Further 
methods
+                        // would operate on materialized results, outside this 
grammar.
+                        if (i != this.steps.size() - 1) {
+                            return new Analysis(-1, bound, true, false);
+                        }
+                        position = step.dotPosition;
+                        if ("hasNext".equals(step.name)) {
+                            largeCollection = false;
+                            nestedCollection = false;
+                        }
+                        break;
+                    case EXPAND:
+                        bound = null;
+                        singleResult = false;
+                        largeCollection = false;
+                        break;
+                    case REDUCE:
+                        largeCollection = false;
+                        nestedCollection = false;
+                        if (!step.localScope()) {
+                            bound = BigInteger.ONE;
+                            singleResult = true;
+                        }
+                        break;
+                    case COLLECT:
+                        // The keyed group overloads accumulate a side effect
+                        // while passing each input traverser through.
+                        if (("group".equals(step.name) || 
"groupCount".equals(step.name)) &&
+                            !step.arguments.isEmpty()) {
+                            break;
+                        }
+                        if (!step.localScope()) {
+                            // A single List/Map can still download an entire
+                            // graph. A later outer limit cannot shrink its 
input.

Review Comment:
   Important: In preview mode, which every interactive query from the analysis 
page uses, `g.V().groupCount().by(label)` and `g.E().label().groupCount()` now 
fail with `gremlin.preview.collection_requires_async` before execution. On 
master they ran. `QueryServiceTest` (lines 372-390) asserts this, so it is 
intended, and the error text points to async mode. But the PR body and README 
do not mention it, and a label histogram is a common query on this page.
   
   With a label key the map has one entry per label, so its size does not grow 
with the graph. That is not true of `g.V().groupCount()`, whose keys are the 
vertices.
   
   Consider letting `groupCount` through when its key is a label (`by(label)`, 
`by(T.label)`, or after `label()`), and keeping the rejection otherwise. At a 
minimum, add a README line saying that collection aggregations need async mode.



##########
hugegraph-hubble/hubble-fe/src/modules/analysis/Home/queryLimits.js:
##########
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+// A convenience preflight for common reads, not a Gremlin parser or server 
safety boundary.
+const maskLiterals = text => {
+    let output = '';
+    let quote = '';
+    let comment = '';
+    for (let i = 0; i < text.length; i++) {
+        const ch = text[i];
+        const next = text[i + 1];
+        if (comment) {
+            output += ' ';
+            if (comment === 'line' && ch === '\n') {
+                comment = '';
+            }
+            else if (comment === 'block' && ch === '*' && next === '/') {
+                output += ' ';
+                i++;
+                comment = '';
+            }
+        }
+        else if (quote) {
+            output += ' ';
+            if (ch === '\\') {
+                output += ' ';
+                i++;
+            }
+            else if (ch === quote) {
+                quote = '';
+            }
+        }
+        else if (ch === "'" || ch === '"' || ch === '`') {
+            quote = ch;
+            output += ' ';
+        }
+        else if (ch === '/' && (next === '/' || next === '*')) {
+            comment = next === '/' ? 'line' : 'block';
+            output += '  ';
+            i++;
+        }
+        else {
+            output += ch;
+        }
+    }
+    return quote || comment === 'block' ? '' : output.trim().replace(/;\s*$/, 
'');
+};
+
+// Decode integer literals without evaluating user code. Groovy accepts radix 
prefixes,
+// repeated interior underscores and I/L/G type suffixes; floating point is 
not a row bound.
+const integerBound = argument => {
+    const match = 
argument.trim().match(/^([+-]?)\s*(0[xX][\da-fA-F](?:[\da-fA-F_]*[\da-fA-F])?|0[bB][01](?:[01_]*[01])?|0(?:[0-7_]*[0-7])?|[1-9](?:[\d_]*\d)?)([iIlLgG]?)$/);
+    if (!match) {
+        return null;
+    }
+    let digits = match[2].replace(/_/g, '');
+    if (/^0\d/.test(digits)) {
+        digits = `0o${digits.slice(1)}`;
+    }
+    const value = BigInt(digits) * (match[1] === '-' ? -1n : 1n);
+    const suffix = match[3].toLowerCase();
+    if ((suffix === 'i' && (value < -2147483648n || value > 2147483647n))
+        || value < 0n || value > 9223372036854775807n) {
+        return null;
+    }
+    return value;
+};
+
+const hasLargeExplicitReadLimit = (text, language = 'gremlin', maximum = 1000) 
=> {
+    const code = maskLiterals(String(text ?? ''));
+    if (code.includes(';')) {
+        return false;
+    }
+    if (language.toLowerCase() === 'cypher') {
+        if (/\b(?:CREATE|MERGE|SET|DELETE|REMOVE|CALL)\b/i.test(code)) {
+            return false;
+        }
+        // A property named `limit` is not a result-limit clause, including
+        // whitespace or masked comments between the dot and property name.
+        const limitCode = code.replace(/\.\s*LIMIT\b/gi, '.');
+        const match = limitCode.match(/\bLIMIT\s+(\d+)\s*$/i);
+        // Keep expressions and parameters in async mode rather than evaluating
+        // user code to guess their result bound.
+        return match ? Number(match[1]) > maximum : 
/\bLIMIT\s+\S/i.test(limitCode);

Review Comment:
   Important: When there is no final numeric `LIMIT`, the fallback 
`/\bLIMIT\s+\S/` matches any `LIMIT` in the query. So a Cypher query whose only 
limit is in a `WITH` clause is blocked in the browser, however small the limit. 
This is the item I would fix before merge.
   
   Node probe: `MATCH (n) WITH n LIMIT 10 RETURN n` and `MATCH (n) WITH n ORDER 
BY n.age LIMIT 5 RETURN n.name` return `true`. No request is sent, and the user 
is told the explicit limit exceeds 1000. Both ran on master, and 
`ReadOnlyCypher.preview` already handles them (`... RETURN n LIMIT 300`). The 
code comment says the fallback is meant for parameters and expressions.
   
   Suggested fix: skip numeric limits that another clause follows, 
`/\bLIMIT\s+(?!\d+\s+[A-Z])\S/i.test(limitCode)`. All 21 existing Cypher cases 
still pass. Please add the two queries above to `queryLimits.test.js`.



##########
hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/QueryService.java:
##########
@@ -631,88 +678,92 @@ private Object getRealVertexId(HugeClient client, 
AdjacentQuery query) {
         return rawVertexId;
     }
 
-    private Map<String, Edge> edgesOfVertex(TypedResult result,
-                                            Map<Object, Vertex> vertices,
+    private record EdgePreview(Map<String, Edge> edges, boolean truncated) {
+    }
+
+    private EdgePreview edgesOfVertex(Map<Object, Vertex> vertices,
                                             HugeClient client) {
         final HugeConfig config = this.config;
         int batchSize = config.get(HubbleOptions.GREMLIN_BATCH_QUERY_IDS);
         int edgeLimit = config.get(HubbleOptions.GREMLIN_EDGES_TOTAL_LIMIT);
         int degreeLimit = 
config.get(HubbleOptions.GREMLIN_VERTEX_DEGREE_LIMIT);
 
         Set<Object> vertexIds = vertices.keySet();
-        Map<String, Edge> edges = new HashMap<>(vertexIds.size());
-        Iterables.partition(vertexIds, batchSize).forEach(batch -> {
+        Map<String, Edge> edges = new LinkedHashMap<>();
+        Map<Object, Integer> degrees = new HashMap<>();
+        boolean truncated = false;
+        for (List<Object> batch : Iterables.partition(vertexIds, batchSize)) {
+            if (edges.size() >= edgeLimit) {
+                // Do not query further batches just to prove completeness.
+                truncated = true;
+                break;
+            }
             List<String> escapedIds = batch.stream()
-                                           .map(GremlinUtil::escapeId)
-                                           .collect(Collectors.toList());
+                                          .map(GremlinUtil::escapeId)
+                                          .collect(Collectors.toList());
             String ids = StringUtils.join(escapedIds, ",");
-            // Any better way to find two vertices has linked?
-            String gremlin;
-            if (result.getType().isPath()) {
-                // If result type is path, the vertices count not too much in 
theory
-                gremlin = 
String.format("g.V(%s).bothE().local(limit(%s)).dedup()",
-                                        ids, degreeLimit);
-            } else {
-                gremlin = String.format("g.V(%s).bothE().dedup().limit(%s)",
-                                        ids, edgeLimit);
-            }
+            int batchLimit = edgeLimit - edges.size();
+            String gremlin = String.format("g.V(%s).bothE().dedup().limit(%s)",

Review Comment:
   Minor: For vertex-only path results such as `g.V().out().path()`, the graph 
view can drop edges between path vertices. Each batch of up to 100 vertices 
runs `bothE().dedup().limit(edgeLimit - found + 1)` (501 for the first batch). 
Only after that does it keep the edges whose two endpoints are both in the 
preview. At an average degree of 10, the edges of the first vertices in a batch 
fill the limit, mostly with edges that are then filtered out. Edges among the 
later vertices of each batch are never fetched. The view is marked truncated 
but can show far fewer than 500 edges.
   
   Plain vertex results already used this query on master. For paths, 
`bothE().local(limit(n))` did not bound edges per vertex, so bounding is right. 
Filtering on the server keeps the bound and the edges: 
`g.V(batch).bothE().where(otherV().hasId(<preview ids>)).dedup().limit(n)`.



##########
hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/QueryPropertyPreview.java:
##########
@@ -0,0 +1,305 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.hugegraph.service.query;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.hugegraph.entity.query.GraphView;
+import org.apache.hugegraph.entity.query.TypedResult;
+import org.apache.hugegraph.structure.GraphElement;
+import org.apache.hugegraph.structure.graph.Edge;
+import org.apache.hugegraph.structure.graph.Path;
+import org.apache.hugegraph.structure.graph.Vertex;
+
+final class QueryPropertyPreview {
+
+    static final int STRING_LIMIT = 256;
+    static final int VALUE_LIMIT = 10000;
+    static final int PATH_LIMIT = 1000;
+    private static final String VALUE_OMITTED = "[preview: value budget 
exhausted]";
+    private static final String NESTED_OMITTED = "[preview: nested value 
omitted]";
+    private static final int COLLECTION_LIMIT = 20;
+    private static final int DEPTH_LIMIT = 4;
+
+    private int truncationVersion;
+    private boolean structureTruncated;
+    private int remainingValues = VALUE_LIMIT;
+    // GraphView wraps vertices but retains their property map. Recognize only
+    // maps created for our detached copies, including through those wrappers.
+    // Caching original SDK objects/maps would let repeated rows evade the 
budget.
+    private final Map<Map<String, Object>, Boolean> detachedProperties = new 
IdentityHashMap<>();
+
+    boolean truncated() {
+        return this.truncationVersion > 0;
+    }
+
+    List<String> partialElementIds(Collection<? extends GraphElement> 
elements) {
+        List<String> ids = new ArrayList<>();
+        if (elements != null) {
+            for (GraphElement element : elements) {
+                if 
(Boolean.TRUE.equals(this.detachedProperties.get(element.properties()))) {
+                    // Match JacksonConfig graph element serializers on the 
wire.
+                    ids.add(String.valueOf(element.id()));
+                }
+            }
+        }
+        return ids;
+    }
+
+    private void markTruncated() {
+        // A boolean cannot identify later partial elements after the first
+        // truncation. Each element records changes within its own property 
tree.
+        this.truncationVersion++;
+    }
+
+    TypedResult project(TypedResult result) {
+        if (result.getData() == null) {
+            return result;
+        }
+        List<Object> data = new ArrayList<>(result.getData().size());
+        for (Object value : result.getData()) {
+            data.add(this.value(value, 0));
+        }
+        return new TypedResult(result.getType(), data,
+                               result.isTruncated() || 
this.structureTruncated);
+    }
+
+    GraphView project(GraphView graph) {
+        if (graph.getVertices() == null || graph.getEdges() == null) {
+            return graph;
+        }
+        List<Vertex> vertices = new ArrayList<>(graph.getVertices().size());
+        graph.getVertices().forEach(vertex -> vertices.add((Vertex) 
this.value(vertex, 0)));
+        List<Edge> edges = new ArrayList<>(graph.getEdges().size());
+        graph.getEdges().forEach(edge -> edges.add((Edge) this.value(edge, 
0)));
+        GraphView preview = new GraphView(vertices, edges);
+        preview.setTruncated(graph.isTruncated());
+        return preview;
+    }
+
+    private Object value(Object value, int depth) {
+        if (value instanceof GraphElement &&
+            this.detachedProperties.containsKey(((GraphElement) 
value).properties())) {
+            return value;
+        }
+        boolean available = this.takeValue();
+        if (value instanceof Vertex) {
+            Vertex original = (Vertex) value;
+            Vertex copy = new Vertex(original.label());
+            copy.id(original.id());
+            return this.element(original, copy, depth);
+        }
+        if (value instanceof Edge) {
+            Edge original = (Edge) value;
+            Edge copy = new Edge(original.label());
+            copy.id(original.id());
+            copy.sourceId(original.sourceId());
+            copy.targetId(original.targetId());
+            copy.sourceLabel(original.sourceLabel());
+            copy.targetLabel(original.targetLabel());
+            return this.element(original, copy, depth);
+        }
+        if (value instanceof Path) {
+            return this.path((Path) value, depth, available);
+        }
+        if (!available) {
+            return VALUE_OMITTED;
+        }
+        if (value instanceof byte[]) {
+            byte[] bytes = (byte[]) value;
+            if (bytes.length > STRING_LIMIT) {
+                this.markTruncated();
+                return "[binary preview: " + bytes.length + " bytes omitted]";
+            }
+            return bytes.clone();
+        }
+        if (value instanceof String) {
+            String text = (String) value;
+            if (text.length() > STRING_LIMIT) {
+                int end = STRING_LIMIT;
+                if (Character.isHighSurrogate(text.charAt(end - 1)) &&
+                    Character.isLowSurrogate(text.charAt(end))) {
+                    end--;
+                }
+                this.markTruncated();
+                return text.substring(0, end) + "… [preview " + end + "/" +
+                       text.length() + " chars]";
+            }
+        }
+        if (value instanceof Map || value instanceof List || value instanceof 
Set) {
+            if (depth >= DEPTH_LIMIT) {
+                this.markTruncated();
+                return NESTED_OMITTED;
+            }
+        }
+        if (value instanceof Map) {
+            Map<Object, Object> copy = new LinkedHashMap<>();
+            Map<?, ?> original = (Map<?, ?>) value;
+            int visited = 0;
+            for (Map.Entry<?, ?> entry : original.entrySet()) {
+                if (visited++ == COLLECTION_LIMIT) {
+                    this.markTruncated();
+                    break;
+                }
+                if (this.omitKey(entry.getKey())) {
+                    continue;
+                }
+                boolean exhausted = this.remainingValues == 0;
+                copy.put(entry.getKey(), this.value(entry.getValue(), depth + 
1));
+                if (exhausted) {
+                    break;
+                }
+            }
+            return copy;
+        }
+        if (value instanceof List) {
+            List<Object> copy = new ArrayList<>();
+            for (Object item : (List<?>) value) {
+                if (copy.size() == COLLECTION_LIMIT) {
+                    this.markTruncated();
+                    break;
+                }
+                boolean exhausted = this.remainingValues == 0;
+                copy.add(this.value(item, depth + 1));
+                if (exhausted) {
+                    break;
+                }
+            }
+            return copy;
+        }
+        if (value instanceof Set) {
+            Set<Object> copy = new LinkedHashSet<>();
+            int visited = 0;
+            for (Object item : (Set<?>) value) {
+                if (visited++ == COLLECTION_LIMIT) {
+                    this.markTruncated();
+                    break;
+                }
+                boolean exhausted = this.remainingValues == 0;
+                copy.add(this.value(item, depth + 1));
+                if (exhausted) {
+                    break;
+                }
+            }
+            return copy;
+        }
+        return value;
+    }
+
+    private boolean takeValue() {
+        if (this.remainingValues == 0) {
+            this.markTruncated();
+            return false;
+        }
+        this.remainingValues--;
+        return true;
+    }
+
+    private Path path(Path original, int depth, boolean available) {
+        Object crosspoint = original.crosspoint();
+        boolean nestedCrosspoint = crosspoint instanceof Path || crosspoint 
instanceof Map ||
+                                   crosspoint instanceof List ||
+                                   crosspoint instanceof Set || crosspoint 
instanceof byte[];
+        boolean descend = available && depth < DEPTH_LIMIT;
+        if (crosspoint instanceof GraphElement) {
+            crosspoint = this.value(crosspoint, depth + 1);
+        } else if (nestedCrosspoint) {
+            // Crosspoint IDs remain exact; arbitrary nested values share the
+            // same depth and work budget as the path objects and labels.
+            crosspoint = descend ? this.value(crosspoint, depth + 1) : 
NESTED_OMITTED;
+            if (!descend) {
+                this.markTruncated();
+            }
+        }
+        List<Object> objects = new ArrayList<>();
+        List<Object> labels = new ArrayList<>();
+        List<Object> originalObjects = original.objects();
+        List<Object> originalLabels = original.labels();
+        int slots = Math.min(PATH_LIMIT, Math.max(originalObjects.size(), 
originalLabels.size()));
+        for (int i = 0; descend && i < slots && this.remainingValues > 0; i++) 
{
+            if (i < originalObjects.size()) {
+                objects.add(this.value(originalObjects.get(i), depth + 1));
+            }
+            if (i < originalLabels.size() && this.remainingValues > 0) {
+                labels.add(this.value(originalLabels.get(i), depth + 1));
+            }
+        }
+        if (objects.size() < originalObjects.size() || labels.size() < 
originalLabels.size()) {
+            this.markTruncated();
+            this.structureTruncated = true;
+        }
+        // SDK Path stores CopyOnWriteArrayLists. Build each prefix in one 
batch
+        // rather than copying its growing backing array for every object.
+        Path copy = new Path(crosspoint, objects);
+        copy.labels(labels.toArray());
+        return copy;
+    }
+
+    private boolean omitKey(Object key) {
+        if (key instanceof String && ((String) key).length() <= STRING_LIMIT) {
+            return false;
+        }
+        if (key instanceof Boolean || key instanceof Character || key 
instanceof Byte ||
+            key instanceof Short || key instanceof Integer || key instanceof 
Long ||
+            key instanceof Float || key instanceof Double) {
+            return false;
+        }
+        // Jackson stringifies object keys outside the value projection budget.
+        // Only retain short text and fixed scalar wrappers; never stringify or
+        // rename arbitrary keys, which can contain entire property trees.
+        this.markTruncated();
+        return true;
+    }
+
+    private GraphElement element(GraphElement original, GraphElement copy, int 
depth) {
+        // SDK query elements have an attached manager: property() on them can
+        // write to Server. Only populate the detached copy's property map.
+        int before = this.truncationVersion;
+        if (depth >= DEPTH_LIMIT) {
+            if (!original.properties().isEmpty()) {
+                this.markTruncated();
+            }
+            this.detachedProperties.put(copy.properties(), 
this.truncationVersion != before);
+            return copy;
+        }
+        int visited = 0;
+        for (Map.Entry<String, Object> entry : 
original.properties().entrySet()) {
+            if (visited++ == COLLECTION_LIMIT) {

Review Comment:
   Important: `COLLECTION_LIMIT` (20) also caps an element's top-level property 
map. A vertex with 21 or more short properties loses the extra ones in the 
Graph, Table and JSON views and becomes read-only, although no value is large. 
The README describes clipping of large property values. It does not say that 
wide elements lose fields or editing.
   
   Probe of this class at the head: one vertex with 21 one-character properties 
keeps 20 and is listed in the partial IDs. 300 vertices with 20 properties each 
stay complete.
   
   Consider skipping `COLLECTION_LIMIT` for the top-level map in `element()`. 
`STRING_LIMIT` still bounds each value. The shared `VALUE_LIMIT` (10,000) still 
bounds the response, so at 300 rows elements with about 33 or more properties 
would still be clipped.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to