This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch tinkergraph-storage
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit dc72ba579a1245ee774a81924e9a9e792d7aa79a
Author: Stephen Mallette <[email protected]>
AuthorDate: Wed Aug 19 14:31:18 2026 +0000

    Encode TinkerStorageGraph elements as dictionary-referenced components
    
    Replace whole-DetachedVertex/DetachedEdge GraphBinary serialization with a
    component codec: element ids and property values go through GraphBinary's
    scalar serializers (a one-byte DataType tag + raw value), while labels,
    property keys, and meta-property keys are dictionary-encoded to small 
integer
    refs. New keys are emitted as OP_DICT_APPEND records within the same frame,
    before their first use. Vertex-property ids are auto-generated and no longer
    persisted (regenerated on load); element and edge ids are preserved.
    
    Compaction preserves dictionary numbering and writes the whole dictionary 
as a
    self-contained snapshot header, and dict-append decode is idempotent, so a 
log
    surviving the compaction crash window still resolves its refs. This removes 
the
    old whole-object codec (no dead code) and drops per-element key/label
    repetition and the per-property envelope, shrinking the on-disk footprint.
    
    Assisted-by: Claude Code:claude-opus-4-8
---
 .../structure/storage/GraphBinaryStorage.java      | 371 ++++++++++++++++++---
 .../structure/storage/GraphBinaryStorageTest.java  |  14 +-
 .../storage/StorageCrashConsistencyTest.java       |  15 +-
 3 files changed, 334 insertions(+), 66 deletions(-)

diff --git 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorage.java
 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorage.java
index a5f3fd1c5c..1266dee705 100644
--- 
a/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorage.java
+++ 
b/tinkergraph-gremlin/src/main/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorage.java
@@ -19,34 +19,51 @@
 package org.apache.tinkerpop.gremlin.tinkergraph.structure.storage;
 
 import org.apache.tinkerpop.gremlin.structure.Edge;
+import org.apache.tinkerpop.gremlin.structure.Property;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.structure.VertexProperty;
+import org.apache.tinkerpop.gremlin.structure.io.binary.DataType;
 import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
 import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
+import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializer;
 import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry;
 import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge;
-import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedFactory;
+import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedProperty;
 import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex;
+import 
org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertexProperty;
 import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph;
 import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerEdge;
 import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerVertex;
 
 import java.io.DataOutputStream;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * The GraphBinary {@link TinkerStorage} codec on top of {@link 
AbstractLogStorage}. The base owns the durable
  * log-structured machinery (file layout, {@code VERSION} marker, CRC framing, 
replay fold, {@link SyncMode}
- * durability, crash-safe and threshold compaction); this class supplies only 
how an element is encoded and decoded,
- * using the GraphBinary serializers ({@link GraphBinaryWriter}/{@link 
GraphBinaryReader}).
+ * durability, crash-safe and threshold compaction); this class supplies only 
how an element is encoded and decoded.
  * <p/>
- * Known limitation (write amplification): a commit records each changed 
element in full — a single property change on
- * a large element rewrites the whole element to the log. Elements are 
typically small and automatic compaction bounds
- * the resulting log growth, so this is accepted rather than mitigated with 
per-property deltas, which would complicate
- * the {@link TinkerStorageMutation} contract and the replay fold. The 
snapshot, by contrast, is streamed one element
- * at a time so compaction never holds a second full copy of the graph in heap.
+ * Rather than serialize a whole {@code DetachedVertex}/{@code DetachedEdge} 
(which repeats every property key and
+ * label as a full string on every element and wraps each property in a {@code 
VertexProperty} envelope), this codec
+ * writes element <em>components</em> directly: element ids and property 
values go through GraphBinary's scalar
+ * serializers (a one-byte {@link DataType} tag plus the raw value), while 
labels, property keys, and meta-property
+ * keys are dictionary-encoded to small integer refs. The dictionary is a 
single dense namespace built by first
+ * appearance; new entries are emitted as {@code OP_DICT_APPEND} records 
inside the same frame, before the entries
+ * that reference them, so a torn trailing frame drops a ref and its user 
atomically.
+ * <p/>
+ * Vertex-property ids are auto-generated and, by default, not persisted (they 
are regenerated on load); element and
+ * edge ids are always preserved. The snapshot streams one element per frame, 
so compaction never holds a second full
+ * copy of the graph in heap.
  */
 public final class GraphBinaryStorage extends AbstractLogStorage {
 
@@ -54,71 +71,230 @@ public final class GraphBinaryStorage extends 
AbstractLogStorage {
     private static final byte OP_DEL_VERTEX = 2;
     private static final byte OP_PUT_EDGE = 3;
     private static final byte OP_DEL_EDGE = 4;
+    private static final byte OP_DICT_APPEND = 5;
 
-    private final GraphBinaryWriter writer = new 
GraphBinaryWriter(TypeSerializerRegistry.INSTANCE);
-    private final GraphBinaryReader reader = new 
GraphBinaryReader(TypeSerializerRegistry.INSTANCE);
+    private final TypeSerializerRegistry registry = 
TypeSerializerRegistry.INSTANCE;
+    private final GraphBinaryWriter writer = new GraphBinaryWriter(registry);
+    private final GraphBinaryReader reader = new GraphBinaryReader(registry);
 
     /**
-     * Serialize a commit record: txVersion, entry count, then each entry as 
an op byte followed by either the
-     * serialized element (put) or the serialized id (delete).
+     * Shared string dictionary for labels, property keys, and meta-property 
keys. Dense ids assigned by first
+     * appearance; grows monotonically within a write session and is rebuilt 
fresh on compaction and on replay.
      */
+    private final Map<String, Integer> keyToId = new HashMap<>();
+    private final List<String> idToKey = new ArrayList<>();
+
+    @Override
+    protected void beginReplay() {
+        keyToId.clear();
+        idToKey.clear();
+    }
+
+    // 
---------------------------------------------------------------------------------------------
 encode
+
     @Override
     protected byte[] encodeCommit(final long txVersion,
                                   final 
Collection<TinkerStorageMutation<TinkerVertex>> changedVertices,
                                   final 
Collection<TinkerStorageMutation<TinkerEdge>> changedEdges) throws IOException {
-        final ByteBufferBuffer buffer = new ByteBufferBuffer();
-        buffer.writeLong(txVersion);
-        buffer.writeInt(changedVertices.size() + changedEdges.size());
+        final ByteBufferBuffer buf = new ByteBufferBuffer();
+        // register the strings introduced by this commit (deletes carry only 
an id, no strings)
+        final List<String> appends = new ArrayList<>();
+        for (final TinkerStorageMutation<TinkerVertex> m : changedVertices)
+            if (!m.isDeleted()) registerVertexStrings(m.element(), appends);
+        for (final TinkerStorageMutation<TinkerEdge> m : changedEdges)
+            if (!m.isDeleted()) registerEdgeStrings(m.element(), appends);
+
+        writeVarInt(buf, appends.size() + changedVertices.size() + 
changedEdges.size());
+        // dictionary appends first, so every ref below resolves during the 
fold
+        for (final String s : appends) {
+            buf.writeByte(OP_DICT_APPEND);
+            writeVarInt(buf, keyToId.get(s));
+            writeString(buf, s);
+        }
         for (final TinkerStorageMutation<TinkerVertex> m : changedVertices) {
             if (m.isDeleted()) {
-                buffer.writeByte(OP_DEL_VERTEX);
-                writer.write(m.id(), buffer);
+                buf.writeByte(OP_DEL_VERTEX);
+                writeScalar(buf, m.id());
             } else {
-                buffer.writeByte(OP_PUT_VERTEX);
-                // detach to a stable form independent of the transactional 
element
-                writer.write(DetachedFactory.detach(m.element(), true), 
buffer);
+                buf.writeByte(OP_PUT_VERTEX);
+                writeVertexRecord(buf, m.element());
             }
         }
         for (final TinkerStorageMutation<TinkerEdge> m : changedEdges) {
             if (m.isDeleted()) {
-                buffer.writeByte(OP_DEL_EDGE);
-                writer.write(m.id(), buffer);
+                buf.writeByte(OP_DEL_EDGE);
+                writeScalar(buf, m.id());
             } else {
-                buffer.writeByte(OP_PUT_EDGE);
-                writer.write(DetachedFactory.detach(m.element(), true), 
buffer);
+                buf.writeByte(OP_PUT_EDGE);
+                writeEdgeRecord(buf, m.element());
+            }
+        }
+        return buf.toWrittenArray();
+    }
+
+    @Override
+    protected void writeSnapshot(final AbstractTinkerGraph graph, final 
DataOutputStream out) throws IOException {
+        // Preserve the existing dictionary numbering rather than renumbering: 
the compaction crash window can leave
+        // the new snapshot in place with the old log not yet truncated, and 
that log's refs use the current
+        // numbering. Register any not-yet-seen live strings (this only 
extends the dictionary, never renumbers), then
+        // emit the whole dictionary as a self-contained header frame so a 
snapshot-only replay resolves every ref.
+        final List<String> ignored = new ArrayList<>();
+        Iterator<Vertex> vertices = graph.vertices();
+        while (vertices.hasNext())
+            registerVertexStrings(vertices.next(), ignored);
+        Iterator<Edge> edges = graph.edges();
+        while (edges.hasNext())
+            registerEdgeStrings(edges.next(), ignored);
+
+        final ByteBufferBuffer dictBuf = new ByteBufferBuffer();
+        writeVarInt(dictBuf, idToKey.size());
+        for (int id = 0; id < idToKey.size(); id++) {
+            dictBuf.writeByte(OP_DICT_APPEND);
+            writeVarInt(dictBuf, id);
+            writeString(dictBuf, idToKey.get(id));
+        }
+        writeFrame(out, dictBuf.toWrittenArray());
+
+        // one element per frame; all keys are already in the dictionary 
header, so no per-frame appends
+        vertices = graph.vertices();
+        while (vertices.hasNext())
+            writeElementFrame(out, OP_PUT_VERTEX, vertices.next());
+        edges = graph.edges();
+        while (edges.hasNext())
+            writeElementFrame(out, OP_PUT_EDGE, edges.next());
+    }
+
+    /**
+     * Write one element as its own single-entry put frame. One element is 
held in memory at a time, so a large graph
+     * is never buffered whole.
+     */
+    private void writeElementFrame(final DataOutputStream out, final byte op, 
final Object element) throws IOException {
+        final ByteBufferBuffer buf = new ByteBufferBuffer();
+        writeVarInt(buf, 1);
+        buf.writeByte(op);
+        if (op == OP_PUT_VERTEX) writeVertexRecord(buf, (Vertex) element);
+        else writeEdgeRecord(buf, (Edge) element);
+        writeFrame(out, buf.toWrittenArray());
+    }
+
+    private void registerVertexStrings(final Vertex v, final List<String> 
appends) {
+        for (final String label : v.labels())
+            register(label, appends);
+        final Iterator<VertexProperty<Object>> vps = v.properties();
+        while (vps.hasNext()) {
+            final VertexProperty<Object> vp = vps.next();
+            register(vp.key(), appends);
+            final Iterator<Property<Object>> metas = vp.properties();
+            while (metas.hasNext())
+                register(metas.next().key(), appends);
+        }
+    }
+
+    private void registerEdgeStrings(final Edge e, final List<String> appends) 
{
+        register(e.label(), appends);
+        final Iterator<Property<Object>> props = e.properties();
+        while (props.hasNext())
+            register(props.next().key(), appends);
+    }
+
+    private void register(final String s, final List<String> appends) {
+        if (!keyToId.containsKey(s)) {
+            final int id = idToKey.size();
+            keyToId.put(s, id);
+            idToKey.add(s);
+            appends.add(s);
+        }
+    }
+
+    private void writeVertexRecord(final ByteBufferBuffer buf, final Vertex v) 
throws IOException {
+        writeScalar(buf, v.id());
+        final Set<String> labels = v.labels();
+        writeVarInt(buf, labels.size());
+        for (final String label : labels)
+            writeVarInt(buf, keyToId.get(label));
+
+        // group vertex properties by key so multi-properties (list/set) 
round-trip
+        final Map<String, List<VertexProperty<Object>>> groups = new 
LinkedHashMap<>();
+        final Iterator<VertexProperty<Object>> vps = v.properties();
+        while (vps.hasNext()) {
+            final VertexProperty<Object> vp = vps.next();
+            groups.computeIfAbsent(vp.key(), k -> new ArrayList<>()).add(vp);
+        }
+        writeVarInt(buf, groups.size());
+        for (final Map.Entry<String, List<VertexProperty<Object>>> group : 
groups.entrySet()) {
+            writeVarInt(buf, keyToId.get(group.getKey()));
+            final List<VertexProperty<Object>> values = group.getValue();
+            writeVarInt(buf, values.size());
+            for (final VertexProperty<Object> vp : values) {
+                writeScalar(buf, vp.value());
+                final List<Property<Object>> metas = new ArrayList<>();
+                vp.properties().forEachRemaining(metas::add);
+                writeVarInt(buf, metas.size());
+                for (final Property<Object> meta : metas) {
+                    writeVarInt(buf, keyToId.get(meta.key()));
+                    writeScalar(buf, meta.value());
+                }
             }
         }
-        return buffer.toWrittenArray();
     }
 
+    private void writeEdgeRecord(final ByteBufferBuffer buf, final Edge e) 
throws IOException {
+        writeScalar(buf, e.id());
+        writeVarInt(buf, keyToId.get(e.label()));
+        writeScalar(buf, e.outVertex().id());
+        writeScalar(buf, e.inVertex().id());
+        final List<Property<Object>> props = new ArrayList<>();
+        e.properties().forEachRemaining(props::add);
+        writeVarInt(buf, props.size());
+        for (final Property<Object> p : props) {
+            writeVarInt(buf, keyToId.get(p.key()));
+            writeScalar(buf, p.value());
+        }
+    }
+
+    // 
---------------------------------------------------------------------------------------------
 decode
+
     @Override
     protected void decodeFrame(final byte[] record,
                                final Map<Object, DetachedVertex> vertices,
                                final Map<Object, DetachedEdge> edges) throws 
IOException {
-        final ByteBufferBuffer buffer = new ByteBufferBuffer(record);
-        buffer.readLong(); // txVersion, retained for diagnostics/future use
-        final int entryCount = buffer.readInt();
+        final ByteBufferBuffer buf = new ByteBufferBuffer(record);
+        final int entryCount = readVarInt(buf);
         for (int i = 0; i < entryCount; i++) {
-            final byte op = buffer.readByte();
+            final byte op = buf.readByte();
             switch (op) {
+                case OP_DICT_APPEND: {
+                    final int id = readVarInt(buf);
+                    final String s = readString(buf);
+                    // idempotent: a snapshot header defines the whole 
dictionary, and a log surviving the compaction
+                    // crash window may re-append entries the snapshot already 
established. Re-appending an existing
+                    // id with the same string is a no-op; a mismatch or a gap 
is corruption.
+                    if (id < idToKey.size()) {
+                        if (!idToKey.get(id).equals(s))
+                            throw new IOException(String.format("Corrupt 
storage: dictionary id %d redefined ('%s' vs '%s')", id, idToKey.get(id), s));
+                    } else if (id == idToKey.size()) {
+                        idToKey.add(s);
+                    } else {
+                        throw new IOException(String.format("Corrupt storage: 
dictionary append gap (got %d, expected <= %d)", id, idToKey.size()));
+                    }
+                    break;
+                }
                 case OP_PUT_VERTEX: {
-                    final Vertex v = reader.read(buffer);
-                    vertices.put(v.id(), (DetachedVertex) v);
+                    final DetachedVertex v = readVertexRecord(buf);
+                    vertices.put(v.id(), v);
                     break;
                 }
                 case OP_DEL_VERTEX: {
-                    final Object id = reader.read(buffer);
-                    vertices.remove(id);
+                    vertices.remove(readScalar(buf));
                     break;
                 }
                 case OP_PUT_EDGE: {
-                    final Edge e = reader.read(buffer);
-                    edges.put(e.id(), (DetachedEdge) e);
+                    final DetachedEdge e = readEdgeRecord(buf);
+                    edges.put(e.id(), e);
                     break;
                 }
                 case OP_DEL_EDGE: {
-                    final Object id = reader.read(buffer);
-                    edges.remove(id);
+                    edges.remove(readScalar(buf));
                     break;
                 }
                 default:
@@ -127,29 +303,114 @@ public final class GraphBinaryStorage extends 
AbstractLogStorage {
         }
     }
 
+    private DetachedVertex readVertexRecord(final ByteBufferBuffer buf) throws 
IOException {
+        final Object id = readScalar(buf);
+        final DetachedVertex.Builder b = DetachedVertex.build().setId(id);
+        final int labelCount = readVarInt(buf);
+        if (labelCount == 1) {
+            b.setLabel(idToKey.get(readVarInt(buf)));
+        } else if (labelCount > 1) {
+            final Set<String> labels = new LinkedHashSet<>();
+            for (int i = 0; i < labelCount; i++)
+                labels.add(idToKey.get(readVarInt(buf)));
+            b.setLabels(labels);
+        }
+        final int keyGroupCount = readVarInt(buf);
+        for (int g = 0; g < keyGroupCount; g++) {
+            final String key = idToKey.get(readVarInt(buf));
+            final int valueCount = readVarInt(buf);
+            for (int j = 0; j < valueCount; j++) {
+                final Object value = readScalar(buf);
+                final DetachedVertexProperty.Builder vpb = 
DetachedVertexProperty.build().setLabel(key).setValue(value);
+                final int metaCount = readVarInt(buf);
+                for (int m = 0; m < metaCount; m++) {
+                    final String metaKey = idToKey.get(readVarInt(buf));
+                    final Object metaValue = readScalar(buf);
+                    vpb.addProperty(new DetachedProperty<>(metaKey, 
metaValue));
+                }
+                b.addProperty(vpb.create());
+            }
+        }
+        return b.create();
+    }
+
+    private DetachedEdge readEdgeRecord(final ByteBufferBuffer buf) throws 
IOException {
+        final Object id = readScalar(buf);
+        final String label = idToKey.get(readVarInt(buf));
+        final Object outVId = readScalar(buf);
+        final Object inVId = readScalar(buf);
+        final DetachedEdge.Builder b = 
DetachedEdge.build().setId(id).setLabel(label)
+                .setOutV(DetachedVertex.build().setId(outVId).create())
+                .setInV(DetachedVertex.build().setId(inVId).create());
+        final int propCount = readVarInt(buf);
+        for (int i = 0; i < propCount; i++) {
+            final String key = idToKey.get(readVarInt(buf));
+            final Object value = readScalar(buf);
+            b.addProperty(new DetachedProperty<>(key, value));
+        }
+        return b.create();
+    }
+
+    // 
---------------------------------------------------------------------------------------------
 primitives
+
     /**
-     * Stream the current committed state as one framed put record per vertex 
and per edge, so peak memory is bounded
-     * to a single element rather than materializing the whole graph as one 
byte array.
+     * Write a value as a one-byte {@link DataType} tag followed by the raw 
value (no value-flag byte). A {@code null}
+     * is a single {@link DataType#UNSPECIFIED_NULL} tag.
      */
-    @Override
-    protected void writeSnapshot(final AbstractTinkerGraph graph, final 
DataOutputStream out) throws IOException {
-        final Iterator<Vertex> vertexIterator = graph.vertices();
-        while (vertexIterator.hasNext())
-            writeElementFrame(out, OP_PUT_VERTEX, vertexIterator.next());
-        final Iterator<Edge> edgeIterator = graph.edges();
-        while (edgeIterator.hasNext())
-            writeElementFrame(out, OP_PUT_EDGE, edgeIterator.next());
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private void writeScalar(final ByteBufferBuffer buf, final Object value) 
throws IOException {
+        if (value == null) {
+            buf.writeByte(DataType.UNSPECIFIED_NULL.getCodeByte());
+            return;
+        }
+        final TypeSerializer serializer = 
registry.getSerializer(value.getClass());
+        buf.writeByte(serializer.getDataType().getCodeByte());
+        serializer.writeValue(value, buf, writer, false);
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private Object readScalar(final ByteBufferBuffer buf) throws IOException {
+        final DataType dataType = 
DataType.get(Byte.toUnsignedInt(buf.readByte()));
+        if (dataType == DataType.UNSPECIFIED_NULL)
+            return null;
+        final TypeSerializer serializer = registry.getSerializer(dataType);
+        return serializer.readValue(buf, reader, false);
+    }
+
+    private static void writeString(final ByteBufferBuffer buf, final String 
s) {
+        final byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
+        writeVarInt(buf, bytes.length);
+        buf.writeBytes(bytes);
+    }
+
+    private static String readString(final ByteBufferBuffer buf) {
+        final byte[] bytes = new byte[readVarInt(buf)];
+        buf.readBytes(bytes);
+        return new String(bytes, StandardCharsets.UTF_8);
     }
 
     /**
-     * Encode a single element as a one-entry put record and write it as a 
framed record to {@code out}.
+     * Unsigned LEB128 varint. Counts and dictionary refs are small and 
non-negative, so they cost one byte in the
+     * common case.
      */
-    private void writeElementFrame(final DataOutputStream out, final byte op, 
final Object element) throws IOException {
-        final ByteBufferBuffer buffer = new ByteBufferBuffer();
-        buffer.writeLong(0L); // snapshot records have no single tx version
-        buffer.writeInt(1);
-        buffer.writeByte(op);
-        writer.write(DetachedFactory.detach(element, true), buffer);
-        writeFrame(out, buffer.toWrittenArray());
+    private static void writeVarInt(final ByteBufferBuffer buf, final int 
value) {
+        int v = value;
+        while ((v & ~0x7F) != 0) {
+            buf.writeByte((v & 0x7F) | 0x80);
+            v >>>= 7;
+        }
+        buf.writeByte(v & 0x7F);
+    }
+
+    private static int readVarInt(final ByteBufferBuffer buf) {
+        int result = 0;
+        int shift = 0;
+        byte b;
+        do {
+            b = buf.readByte();
+            result |= (b & 0x7F) << shift;
+            shift += 7;
+        } while ((b & 0x80) != 0);
+        return result;
     }
 }
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorageTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorageTest.java
index c113511176..cbde701a22 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorageTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/GraphBinaryStorageTest.java
@@ -127,10 +127,10 @@ public class GraphBinaryStorageTest extends 
AbstractTinkerStorageConformanceTest
         graph.tx().commit();
         graph.compact();
 
-        // the snapshot must be written as one framed record per element (3 
vertices + 2 edges = 5), rather than a
-        // single whole-graph frame, so compaction never buffers the entire 
graph in one array
+        // the snapshot must be streamed: a dictionary header frame plus one 
framed record per element (3 vertices +
+        // 2 edges = 5), rather than a single whole-graph frame, so compaction 
never buffers the entire graph at once
         final File snapshotFile = new File(location, 
GraphBinaryStorage.SNAPSHOT_FILE);
-        assertEquals(5, countFrames(snapshotFile));
+        assertEquals(1 + 5, countFrames(snapshotFile));
         graph.close();
 
         // and the streamed snapshot must reopen to exactly the same graph
@@ -145,9 +145,9 @@ public class GraphBinaryStorageTest extends 
AbstractTinkerStorageConformanceTest
     @Test
     public void shouldStreamSnapshotFrameByFrameAtScale() {
         // Bounded-memory proxy for the streaming snapshot path: rather than 
measure heap (flaky, JVM-dependent), assert
-        // the observable streaming property holds at scale — a large graph is 
written as exactly one frame per element,
-        // never one whole-graph frame — and round-trips intact. This is the 
property that keeps compaction from
-        // materializing a second full copy of the graph in memory; it is not 
a hard OOM assertion.
+        // the observable streaming property holds at scale — a large graph is 
written as a dictionary header frame plus
+        // one frame per element, never one whole-graph frame — and 
round-trips intact. This is the property that keeps
+        // compaction from materializing a second full copy of the graph in 
memory; it is not a hard OOM assertion.
         final int vertexCount = 500;
         final int edgeCount = 499;
         final TinkerStorageGraph graph = open();
@@ -161,7 +161,7 @@ public class GraphBinaryStorageTest extends 
AbstractTinkerStorageConformanceTest
             graph.compact();
 
             try {
-                assertEquals(vertexCount + edgeCount, countFrames(new 
File(location, GraphBinaryStorage.SNAPSHOT_FILE)));
+                assertEquals(1 + vertexCount + edgeCount, countFrames(new 
File(location, GraphBinaryStorage.SNAPSHOT_FILE)));
             } catch (Exception ex) {
                 throw new RuntimeException(ex);
             }
diff --git 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/StorageCrashConsistencyTest.java
 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/StorageCrashConsistencyTest.java
index 614cfdb0ba..8f3c475f13 100644
--- 
a/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/StorageCrashConsistencyTest.java
+++ 
b/tinkergraph-gremlin/src/test/java/org/apache/tinkerpop/gremlin/tinkergraph/structure/storage/StorageCrashConsistencyTest.java
@@ -123,10 +123,17 @@ public class StorageCrashConsistencyTest {
 
     @Test
     public void shouldRecoverDurableCommitWithNoSnapshot() throws Exception {
-        // WAL guarantee: a commit whose frame was durably written to the log, 
with no compaction having run, must
-        // recover on reopen even though no snapshot exists.
-        captureBuildingBlocks();
-        Files.write(logFile.toPath(), logV2);
+        // WAL guarantee: a commit durably written to the log with no 
compaction (no snapshot) recovers on reopen. On
+        // a fresh store the dictionary starts empty, so the commit frame is 
self-contained (it carries its own
+        // dictionary appends), which is exactly the real "log only, never 
compacted" case.
+        final TinkerStorageGraph g = open();
+        g.addVertex(T.id, 2, "value", 2);
+        g.tx().commit();
+        g.tx().close();
+        final byte[] selfContainedLog = Files.readAllBytes(logFile.toPath());
+        g.close(); // compaction on close would fold the log away; the raw log 
was captured above
+        resetFiles();
+        Files.write(logFile.toPath(), selfContainedLog);
         assertReopensTo(2);
     }
 

Reply via email to