Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
864b507
feat(format): schema evolution for the Java row codec
stevenschlansker Oct 23, 2026
b9dd1df
feat(format): dispatch nested versioned beans by recursive strict hash
stevenschlansker Oct 23, 2026
1e6fcf3
fix(format): enumerate versioned beans nested inside collection fields
stevenschlansker Oct 23, 2026
20ef687
fix(format): decode map struct keys at current schema during value pr…
stevenschlansker Oct 23, 2026
94cabc6
fix(format): evolve top-level array/map whose element/value wraps a v…
stevenschlansker Oct 23, 2026
a18e33e
fix(format): support interface beans as map values, discovered when n…
stevenschlansker Oct 23, 2026
44fbe0d
feat(format): evolve map keys via a combined key/value schema hash
stevenschlansker Oct 23, 2026
b43f124
feat(format): evolve versioned map keys nested in a row field
stevenschlansker Oct 23, 2026
5aefd22
fix(format): evolve every distinct bean reachable through a top-level…
stevenschlansker Oct 23, 2026
921c51a
fix(format): delimit struct children in row-codec strict schema hash
stevenschlansker Oct 23, 2026
9c16e3b
perf(format): generate row-codec schema projections lazily on first d…
stevenschlansker Oct 23, 2026
6f69991
fix(format): resolve bean-scoped custom codecs in schema-history fiel…
stevenschlansker Oct 23, 2026
2b8cc54
fix(format): reject removed-field schema-history cycles at build time
stevenschlansker Oct 23, 2026
dfcac07
fix(format): compile projection codecs in the build-time code generator
stevenschlansker Oct 23, 2026
f6ca0b2
feat(format): make row schema-evolution wire identity cross-language …
stevenschlansker Oct 23, 2026
e26b573
docs(format): document renames under row schema evolution
stevenschlansker Oct 23, 2026
550632c
fix(format): never introspect terminal custom-codec fields in the evo…
stevenschlansker Oct 23, 2026
0a7b99f
docs(format): state that row schema evolution has a shared design and…
stevenschlansker Oct 23, 2026
dca1244
fix(format): compile projections only for nested beans that live fiel…
stevenschlansker Oct 23, 2026
3bf347c
fix(format): hash compact evolution schemas in wire order at every depth
stevenschlansker Oct 23, 2026
7454d08
fix(format): hash every map position by its wire field
stevenschlansker Oct 23, 2026
9468f52
fix(format): give absent fields of plain beans the record defaults
stevenschlansker Oct 23, 2026
acf847b
fix(format): drop the payload-size checks from schema-hash decode
stevenschlansker Oct 23, 2026
6bdf64f
fix(format): tidy schema-evolution builder details
stevenschlansker Oct 23, 2026
faa6073
test(format): pin the row strict hash to known-answer vectors
stevenschlansker Oct 23, 2026
523eab7
docs(format): specify the row strict-hash framing and wire order exactly
stevenschlansker Oct 23, 2026
f576898
fix(format): skip version combinations that put one bean class at two…
stevenschlansker Oct 23, 2026
2ef548b
fix(format): prefix every evolution array and map payload with its st…
stevenschlansker Oct 23, 2026
d11a130
fix(format): resolve each projection codec once per codec, not per en…
stevenschlansker Oct 23, 2026
c04b90e
refactor(format): route nested projection codecs only through per-cla…
stevenschlansker Oct 23, 2026
ea59986
fix(format): skip map key and value pairs that put one bean class at …
stevenschlansker Oct 23, 2026
afffd0c
docs(format): document portable wire names and misnamed history methods
stevenschlansker Oct 23, 2026
f521b8e
refactor(format): write projection-suffix hashes as fixed-width hex
stevenschlansker Oct 23, 2026
e259832
fix(format): bind row projection codecs to a detached writer
stevenschlansker Oct 23, 2026
30fa7e6
fix(format): write map projection class suffixes as length-prefixed h…
stevenschlansker Oct 23, 2026
74d7b06
test(format): pin the array and map hash prefixes to spec framing vec…
stevenschlansker Oct 23, 2026
116253b
test(format): race first projected decodes and state test invariants
stevenschlansker Oct 23, 2026
e442cd7
fix(format): keep decimal hashes in the row schema-mismatch message
stevenschlansker Oct 23, 2026
8692022
test(format): race first projected decodes on classes no other test uses
stevenschlansker Oct 23, 2026
044f905
docs(format): correct the map prefix framing and annotation scope det…
stevenschlansker Oct 23, 2026
3357bd5
refactor(format): rename the evolution annotations to ForyRowVersion …
stevenschlansker Oct 23, 2026
9ab35e9
fix(format): read every live field of versions that collapse into one…
stevenschlansker Oct 23, 2026
328b576
refactor(format): size projection caches lazily and name projections …
stevenschlansker Oct 23, 2026
7135ca7
docs(format): document the evolution cutover, reader-first rollout, a…
stevenschlansker Oct 23, 2026
c870e39
refactor(format): route map projection bean codecs through one class-…
stevenschlansker Oct 23, 2026
f804167
fix(format): inherit removed fields and read re-added fields in every…
stevenschlansker Oct 23, 2026
419cafa
perf(format): add array and map cases to the schema-evolution benchmark
stevenschlansker Oct 23, 2026
1c4836b
fix(format): reject same-row-type renames and share projection plumbing
stevenschlansker Oct 23, 2026
14a92ea
fix(format): keep collapsed histories current and reject retyped vers…
stevenschlansker Oct 23, 2026
8ccbb32
fix(format): reject version annotations that schema evolution ignores
stevenschlansker Oct 23, 2026
14aae99
fix(format): key schema history layouts by strict hash
stevenschlansker Oct 23, 2026
0ac8cc6
fix(format): reject since = Integer.MAX_VALUE and verify the current …
stevenschlansker Oct 23, 2026
4ea6116
test(format): correct the wire name in an interface history comment
stevenschlansker Oct 23, 2026
0290568
docs(format): specify the exact compact field order
stevenschlansker Oct 23, 2026
b0cfcaa
refactor(format): find nested evolution sites through type inference
stevenschlansker Oct 23, 2026
0bc4a44
fix(format): tag every strict hash with the row format
stevenschlansker Oct 23, 2026
191a7e3
perf(format): keep evolution checks out of the plain array and map en…
stevenschlansker Oct 23, 2026
8a9643a
test(format): decode compact payloads whose nested bean changes sort …
stevenschlansker Oct 23, 2026
d71def3
docs(format): state what evolution changes on the wire and the map nu…
stevenschlansker Oct 23, 2026
8316233
fix(format): remember a failed projection code generation
stevenschlansker Oct 23, 2026
265be83
fix(format): bound the length of projection class names
stevenschlansker Oct 23, 2026
c4721a7
fix(format): name both members that share a wire name, and correct co…
stevenschlansker Oct 23, 2026
b6e1ff5
docs(format): point users at schema evolution and its hash mismatch
stevenschlansker Oct 23, 2026
5b17cff
test(format): stop asserting how an evolution-off reader fails on arr…
stevenschlansker Oct 23, 2026
d322677
fix(format): compare retyped fields in wire order
stevenschlansker Oct 23, 2026
09fd832
perf(format): move the row schema-mismatch branch out of decode
stevenschlansker Oct 23, 2026
ffb7e05
fix(format): reject a payload shorter than its hash and skip an unkno…
stevenschlansker Oct 23, 2026
b25de58
fix(format): throw a new exception for each cached projection failure
stevenschlansker Oct 23, 2026
b2316ae
fix(format): check the current layout in the array and map evolution …
stevenschlansker Oct 23, 2026
2df9a7f
fix(format): let a class bean implement a versioned interface bean
stevenschlansker Oct 23, 2026
50a09aa
docs(format): state that Java member names must be lowerCamelCase
stevenschlansker Oct 23, 2026
ccd58c0
docs(format): reflow builder comments and say why the version product…
stevenschlansker Oct 23, 2026
a042bf8
docs(format): name the GraalVM native image limit of schema evolution
stevenschlansker Oct 23, 2026
c331703
refactor(format): make SchemaHistory package-private in the encoder p…
stevenschlansker Oct 23, 2026
876813d
refactor(format): remove the public newTypeContext from TypeInference
stevenschlansker Oct 23, 2026
c83b155
fix(format): skip an unknown payload only when the buffer holds its body
stevenschlansker Oct 23, 2026
f972872
fix(format): reject member names with an underscore under schema evol…
stevenschlansker Oct 23, 2026
8286e74
fix(format): skip an unknown payload body without a bounds check
stevenschlansker Oct 23, 2026
29a6df6
refactor(format): name a row projection class with the inherited clas…
stevenschlansker Oct 23, 2026
6aed3cc
refactor(format): mark CompactBinaryRowWriter.sortSchemaDeep as internal
stevenschlansker Oct 23, 2026
3916086
docs(format): give the real reason for the projection suffix tokens
stevenschlansker Oct 23, 2026
3bf2712
docs(format): say that a nullability change also discards old values
stevenschlansker Oct 23, 2026
3820230
docs(format): state user-visible effects in place of codec internals
stevenschlansker Oct 23, 2026
f151ece
docs(format): put the hash prefix only on a top-level array or map
stevenschlansker Oct 23, 2026
5754ba1
test(format): do not pin the error of a size below the hash
stevenschlansker Oct 23, 2026
c79a3e9
fix(format): load an evolution array or map body before decoding from…
stevenschlansker Oct 23, 2026
91bdb67
refactor(format): move the other-schema decode of evolution arrays an…
stevenschlansker Oct 23, 2026
4387898
docs(format): correct the history method name rule in a SchemaHistory…
stevenschlansker Oct 23, 2026
666289a
docs(format): say that schema evolution rejects underscores, not that…
stevenschlansker Oct 23, 2026
934f980
docs(format): state when a reader decodes a peer's payload
stevenschlansker Oct 23, 2026
fa0c847
docs(format): name the member, not the descriptor, in the row format …
stevenschlansker Oct 23, 2026
d3ab837
docs(format): show how to build a schema evolution codec
stevenschlansker Oct 23, 2026
f3cdf2a
docs(format): give the real reason for the interface impl class suffix
stevenschlansker Oct 23, 2026
f9fdf6d
test(format): cover the strict hash of float and time types
stevenschlansker Oct 23, 2026
539852c
docs(format): say that equal-width struct children keep their order i…
stevenschlansker Oct 23, 2026
e799322
docs(format): say which fields a projection reads
stevenschlansker Oct 23, 2026
bbdc8cc
docs(format): say what the skip of an unknown row payload does on a s…
stevenschlansker Oct 23, 2026
57ccacc
docs(format): name the row format of the prefix vectors
stevenschlansker Oct 23, 2026
be8584c
test(format): detect a backward move in the size-below-hash test
stevenschlansker Oct 23, 2026
bca4ad4
test(format): decode a streamed row with an older layout
stevenschlansker Oct 23, 2026
754e2e9
refactor(format): make the codec class suffix fields package-private
stevenschlansker Oct 23, 2026
8be8b19
test(format): drop the float and time type hash separation test
stevenschlansker Oct 23, 2026
b660c87
chore(format): keep the codegen timing logs of apache/main
stevenschlansker Oct 23, 2026
99cdf77
fix(format): return the absent value from a default accessor of an ab…
stevenschlansker Oct 23, 2026
c8c3d1a
docs(format): use one meaning of body for the evolution framing
stevenschlansker Oct 23, 2026
df2efa6
docs(format): correct the suffix bound and wire name mapping comments
stevenschlansker Oct 23, 2026
c579417
test(format): correct stale statements in schema evolution test comments
stevenschlansker Oct 23, 2026
dc70e24
docs(format): count every field in the compact bitmap size and keep t…
stevenschlansker Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/*
* 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.fory.benchmark;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.fory.format.annotation.ForyRowVersion;
import org.apache.fory.format.encoder.ArrayEncoder;
import org.apache.fory.format.encoder.Encoders;
import org.apache.fory.format.encoder.MapEncoder;
import org.apache.fory.format.encoder.RowEncoder;
import org.apache.fory.logging.Logger;
import org.apache.fory.logging.LoggerFactory;
import org.apache.fory.reflect.TypeRef;
import org.openjdk.jmh.Main;
import org.openjdk.jmh.annotations.Benchmark;

/**
* Row-codec schema-evolution throughput and allocation. Pair with the JMH gc profiler ({@code -prof
* gc}) to read {@code gc.alloc.rate.norm} (bytes per op). Two comparisons matter: {@code
* currentDecode} vs {@code olderDecode} shows that decoding an older payload through a projection
* codec allocates no more than decoding the current schema, because each projection holds its
* historical schema's row layout (no per-decode rebuild); and the {@code *NoEvolution} benchmarks
* vs their evolution-on counterparts show the steady-state cost of enabling {@code
* withSchemaEvolution()} when reading and writing current-version data. The {@code array*} and
* {@code map*} benchmarks repeat both comparisons for a top-level {@code List<PersonV2>} and {@code
* Map<String, PersonV2>}, whose evolution-on payloads carry an 8-byte hash prefix.
*/
public class SchemaEvolutionSuite {
private static final Logger LOG = LoggerFactory.getLogger(SchemaEvolutionSuite.class);

public static class PersonV1 {
String name;
int age;
}

public static class PersonV2 {
String name;
int age;

@ForyRowVersion(since = 2)
String email;
}

// Evolution-enabled codecs for the current (V2) schema; the V1 codec only produces a payload
// whose hash routes the V2 reader onto its projection path. Both standard and compact formats
// are measured: compact is where a per-projection cached row layout matters, so olderDecode vs
// currentDecode there is the parity check.
private static final RowEncoder<PersonV1> v1Codec =
Encoders.buildBeanCodec(PersonV1.class).withSchemaEvolution().build().get();
private static final RowEncoder<PersonV2> v2Codec =
Encoders.buildBeanCodec(PersonV2.class).withSchemaEvolution().build().get();
private static final RowEncoder<PersonV1> v1CompactCodec =
Encoders.buildBeanCodec(PersonV1.class).compactEncoding().withSchemaEvolution().build().get();
private static final RowEncoder<PersonV2> v2CompactCodec =
Encoders.buildBeanCodec(PersonV2.class).compactEncoding().withSchemaEvolution().build().get();

// Evolution-disabled codecs for the same current (V2) schema. Comparing the *NoEvolution
// benchmarks against their evolution-on counterparts isolates the steady-state cost of the
// withSchemaEvolution() flag on the common path (reading and writing current-version data).
// Rows carry the 8-byte hash slot and decode compares it in both modes, so on current-version
// data the flag adds no work; only a hash mismatch takes the projection lookup.
private static final RowEncoder<PersonV2> v2PlainCodec =
Encoders.buildBeanCodec(PersonV2.class).build().get();
private static final RowEncoder<PersonV2> v2PlainCompactCodec =
Encoders.buildBeanCodec(PersonV2.class).compactEncoding().build().get();

private static final PersonV2 person = newPerson();
private static final byte[] currentBytes = v2Codec.encode(person);
private static final byte[] olderBytes = v1Codec.encode(newPersonV1());
private static final byte[] currentCompactBytes = v2CompactCodec.encode(person);
private static final byte[] olderCompactBytes = v1CompactCodec.encode(newPersonV1());
private static final byte[] plainBytes = v2PlainCodec.encode(person);
private static final byte[] plainCompactBytes = v2PlainCompactCodec.encode(person);

private static final TypeRef<List<PersonV1>> V1_LIST = new TypeRef<List<PersonV1>>() {};
private static final TypeRef<List<PersonV2>> V2_LIST = new TypeRef<List<PersonV2>>() {};
private static final TypeRef<Map<String, PersonV1>> V1_MAP =
new TypeRef<Map<String, PersonV1>>() {};
private static final TypeRef<Map<String, PersonV2>> V2_MAP =
new TypeRef<Map<String, PersonV2>>() {};

private static final ArrayEncoder<List<PersonV2>> v2ArrayCodec =
Encoders.buildArrayCodec(V2_LIST).withSchemaEvolution().build().get();
private static final ArrayEncoder<List<PersonV2>> v2PlainArrayCodec =
Encoders.buildArrayCodec(V2_LIST).build().get();
private static final MapEncoder<Map<String, PersonV2>> v2MapCodec =
Encoders.buildMapCodec(V2_MAP).withSchemaEvolution().build().get();
private static final MapEncoder<Map<String, PersonV2>> v2PlainMapCodec =
Encoders.buildMapCodec(V2_MAP).build().get();

private static final List<PersonV2> people = Arrays.asList(person, person, person, person);
private static final Map<String, PersonV2> personMap = newPersonMap(person);
private static final byte[] currentArrayBytes = v2ArrayCodec.encode(people);
private static final byte[] olderArrayBytes =
Encoders.buildArrayCodec(V1_LIST)
.withSchemaEvolution()
.build()
.get()
.encode(Arrays.asList(newPersonV1(), newPersonV1(), newPersonV1(), newPersonV1()));
private static final byte[] plainArrayBytes = v2PlainArrayCodec.encode(people);
private static final byte[] currentMapBytes = v2MapCodec.encode(personMap);
private static final byte[] olderMapBytes =
Encoders.buildMapCodec(V1_MAP)
.withSchemaEvolution()
.build()
.get()
.encode(newPersonMap(newPersonV1()));
private static final byte[] plainMapBytes = v2PlainMapCodec.encode(personMap);

private static <V> Map<String, V> newPersonMap(V value) {
Map<String, V> map = new LinkedHashMap<>();
for (int i = 0; i < 4; i++) {
map.put("person" + i, value);
}
return map;
}

private static PersonV2 newPerson() {
PersonV2 p = new PersonV2();
p.name = "Ada Lovelace";
p.age = 36;
p.email = "ada@example.com";
return p;
}

private static PersonV1 newPersonV1() {
PersonV1 p = new PersonV1();
p.name = "Ada Lovelace";
p.age = 36;
return p;
}

@Benchmark
public Object encode() {
return v2Codec.encode(person);
}

@Benchmark
public Object currentDecode() {
return v2Codec.decode(currentBytes);
}

@Benchmark
public Object olderDecode() {
return v2Codec.decode(olderBytes);
}

@Benchmark
public Object compactEncode() {
return v2CompactCodec.encode(person);
}

@Benchmark
public Object compactCurrentDecode() {
return v2CompactCodec.decode(currentCompactBytes);
}

@Benchmark
public Object compactOlderDecode() {
return v2CompactCodec.decode(olderCompactBytes);
}

// Evolution-off baselines for the current path. Pair each with its evolution-on counterpart
// (encode/currentDecode and the compact variants) to read the flag's overhead.
@Benchmark
public Object encodeNoEvolution() {
return v2PlainCodec.encode(person);
}

@Benchmark
public Object currentDecodeNoEvolution() {
return v2PlainCodec.decode(plainBytes);
}

@Benchmark
public Object compactEncodeNoEvolution() {
return v2PlainCompactCodec.encode(person);
}

@Benchmark
public Object compactCurrentDecodeNoEvolution() {
return v2PlainCompactCodec.decode(plainCompactBytes);
}

@Benchmark
public Object arrayEncode() {
return v2ArrayCodec.encode(people);
}

@Benchmark
public Object arrayCurrentDecode() {
return v2ArrayCodec.decode(currentArrayBytes);
}

@Benchmark
public Object arrayOlderDecode() {
return v2ArrayCodec.decode(olderArrayBytes);
}

@Benchmark
public Object arrayEncodeNoEvolution() {
return v2PlainArrayCodec.encode(people);
}

@Benchmark
public Object arrayCurrentDecodeNoEvolution() {
return v2PlainArrayCodec.decode(plainArrayBytes);
}

@Benchmark
public Object mapEncode() {
return v2MapCodec.encode(personMap);
}

@Benchmark
public Object mapCurrentDecode() {
return v2MapCodec.decode(currentMapBytes);
}

@Benchmark
public Object mapOlderDecode() {
return v2MapCodec.decode(olderMapBytes);
}

@Benchmark
public Object mapEncodeNoEvolution() {
return v2PlainMapCodec.encode(personMap);
}

@Benchmark
public Object mapCurrentDecodeNoEvolution() {
return v2PlainMapCodec.decode(plainMapBytes);
}

public static void main(String[] args) throws Exception {
if (args.length == 0) {
String commandLine =
"org.apache.fory.*SchemaEvolutionSuite.* -f 3 -wi 3 -i 3 -t 1 -w 2s -r 2s -prof gc -rf csv";
args = commandLine.split(" ");
}
LOG.info("command line: {}", Arrays.toString(args));
Main.main(args);
}
}
9 changes: 9 additions & 0 deletions docs/row-format/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,14 @@ Reuse the encoder within one thread. Create separate encoders for concurrent thr
Choose Compact Row only when every reader is Java and the space reduction justifies the
Java-specific layout. Use Standard Row for Java, Python, C++, Rust, and Go interchange.

## Schema Evolution

Java row, array, and map codecs built with `withSchemaEvolution()` read payloads that older
versions of a bean wrote, in Standard Row and in Compact Row. Each payload then carries a schema
hash, and a reader rejects a payload whose hash it does not know with
`ClassNotCompatibleException`. Producers and consumers must agree on the setting. See the
[Java guide](java.md#schema-evolution) for the annotations, the rollout procedure, and the
limits.

See the [Row Format specification](../specification/row_format_spec.md) for the exact Standard and
Compact binary layouts.
Loading
Loading