Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions compiler/extension/fory_options.proto
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ message ForyFileOptions {
// namespace and type name. When false, types without explicit IDs are
// registered by namespace and name.
optional bool enable_auto_type_id = 3;

// Enable schema evolution for all messages in this file by default.
// When false, messages default to STRUCT/NAMED_STRUCT unless overridden.
// Default: true.
optional bool evolving = 4;
}

extend google.protobuf.FileOptions { optional ForyFileOptions fory = 50001; }
Expand Down
1 change: 1 addition & 0 deletions compiler/fory_compiler/frontend/fdl/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"polymorphism",
"enable_auto_type_id",
"go_nested_type_style",
"evolving",
}

# Known field-level options
Expand Down
11 changes: 11 additions & 0 deletions compiler/fory_compiler/generators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,17 @@ def should_register_by_id(self, type_def) -> bool:
type_id = getattr(type_def, "type_id", None)
return type_id is not None

def get_effective_evolving(self, message) -> bool:
"""Return effective evolving flag for a message."""
if message is None:
return True
if "evolving" in message.options:
return bool(message.options.get("evolving"))
file_default = self.schema.get_option("evolving")
if file_default is None:
return True
return bool(file_default)

def get_license_header(self, comment_prefix: str = "//") -> str:
"""Get the Apache license header."""
lines = [
Expand Down
12 changes: 12 additions & 0 deletions compiler/fory_compiler/generators/cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ def generate_header(self) -> GeneratedFile:
enum_macros: List[str] = []
union_macros: List[str] = []
field_config_macros: List[str] = []
evolving_macros: List[str] = []
definition_items = self.get_definition_order()

# Collect includes (including from nested types)
Expand Down Expand Up @@ -341,6 +342,7 @@ def generate_header(self) -> GeneratedFile:
enum_macros,
union_macros,
field_config_macros,
evolving_macros,
"",
)
)
Expand All @@ -366,6 +368,10 @@ def generate_header(self) -> GeneratedFile:
lines.append(f"}} // namespace {namespace}")
lines.append("")

if evolving_macros:
lines.extend(evolving_macros)
lines.append("")

# End header guard
lines.append(f"#endif // {guard_name}")
lines.append("")
Expand Down Expand Up @@ -872,6 +878,7 @@ def generate_message_definition(
enum_macros: List[str],
union_macros: List[str],
field_config_macros: List[str],
evolving_macros: List[str],
indent: str,
) -> List[str]:
"""Generate a C++ class definition with nested types."""
Expand Down Expand Up @@ -901,6 +908,7 @@ def generate_message_definition(
enum_macros,
union_macros,
field_config_macros,
evolving_macros,
body_indent,
)
)
Expand Down Expand Up @@ -964,6 +972,10 @@ def generate_message_definition(
else:
lines.append(f"{body_indent}FORY_STRUCT({struct_type_name});")

if not self.get_effective_evolving(message):
qualified_name = self.get_namespaced_type_name(message.name, parent_stack)
evolving_macros.append(f"FORY_STRUCT_EVOLVING({qualified_name}, false);")

lines.append(f"{indent}}};")

return lines
Expand Down
7 changes: 6 additions & 1 deletion compiler/fory_compiler/generators/go.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ def get_union_case_type_id_expr(
return "fory.NAMED_UNION"
return "fory.UNION"
if isinstance(type_def, Message):
evolving = bool(type_def.options.get("evolving"))
evolving = self.get_effective_evolving(type_def)
if type_def.type_id is None:
if evolving:
return "fory.NAMED_COMPATIBLE_STRUCT"
Expand Down Expand Up @@ -761,6 +761,11 @@ def generate_message(

lines.append("}")
lines.append("")
if not self.get_effective_evolving(message):
lines.append(f"func (*{type_name}) ForyEvolving() bool {{")
lines.append("\treturn false")
lines.append("}")
lines.append("")
lines.append(f"func (m *{type_name}) ToBytes() ([]byte, error) {{")
lines.append("\treturn getFory().Serialize(m)")
lines.append("}")
Expand Down
7 changes: 7 additions & 0 deletions compiler/fory_compiler/generators/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,8 @@ def generate_message_file(self, message: Message) -> GeneratedFile:
comment = self.format_type_id_comment(message, "//")
if comment:
lines.append(comment)
if not self.get_effective_evolving(message):
lines.append("@ForyObject(evolving = false)")
lines.append(f"public class {message.name} {{")

# Generate nested enums as static inner classes
Expand Down Expand Up @@ -587,6 +589,9 @@ def collect_message_imports(self, message: Message, imports: Set[str]):
for field in message.fields:
self.collect_field_imports(field, imports)

if not self.get_effective_evolving(message):
imports.add("org.apache.fory.annotation.ForyObject")

# Add imports for equals/hashCode
imports.add("java.util.Objects")
if self.has_array_field_recursive(message):
Expand Down Expand Up @@ -961,6 +966,8 @@ def generate_nested_message(
comment = self.format_type_id_comment(message, " " * indent + "//")
if comment:
lines.append(comment)
if not self.get_effective_evolving(message):
lines.append("@ForyObject(evolving = false)")
lines.append(f"public static class {message.name} {{")

# Generate nested enums
Expand Down
7 changes: 5 additions & 2 deletions compiler/fory_compiler/generators/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ def generate_module(self) -> GeneratedFile:
imports: Set[str] = set()

# Collect all imports
imports.add("from dataclasses import dataclass, field")
imports.add("from dataclasses import field")
imports.add("from enum import Enum, IntEnum")
imports.add("from typing import Dict, List, Optional, cast")
imports.add("import pyfory")
Expand Down Expand Up @@ -370,7 +370,10 @@ def generate_message(
comment = self.format_type_id_comment(message, f"{ind}#")
if comment:
lines.append(comment)
lines.append(f"{ind}@dataclass")
if not self.get_effective_evolving(message):
lines.append(f"{ind}@pyfory.dataclass(evolving=False)")
else:
lines.append(f"{ind}@pyfory.dataclass")
lines.append(f"{ind}class {message.name}:")

# Generate nested enums first (they need to be defined before fields reference them)
Expand Down
2 changes: 2 additions & 0 deletions compiler/fory_compiler/generators/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@ def generate_message(
if not self.message_has_any(message):
derives.extend(["Clone", "PartialEq", "Default"])
lines.append(f"#[derive({', '.join(derives)})]")
if not self.get_effective_evolving(message):
lines.append("#[fory(evolving = false)]")

lines.append(f"pub struct {type_name} {{")

Expand Down
4 changes: 4 additions & 0 deletions cpp/fory/meta/field_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,7 @@ constexpr auto concat_tuples_from_tuple(const Tuple &tuple) {
(type, unique_id, __VA_ARGS__)

#define FORY_STRUCT(type, ...) FORY_STRUCT_IMPL(type, __LINE__, __VA_ARGS__)

#define FORY_STRUCT_EVOLVING(type, value) \
template <> \
struct fory::meta::StructEvolving<type> : std::bool_constant<value> {}
2 changes: 2 additions & 0 deletions cpp/fory/meta/type_traits.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ template <typename T>
constexpr inline bool IsPairIterable =
decltype(details::is_pair_iterable_impl<T>(0))::value;

template <typename T> struct StructEvolving : std::true_type {};

} // namespace meta

} // namespace fory
31 changes: 31 additions & 0 deletions cpp/fory/serialization/struct_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
*/

#include "fory/serialization/fory.h"
#include "fory/type/type.h"
#include "gtest/gtest.h"
#include <cfloat>
#include <climits>
Expand Down Expand Up @@ -188,6 +189,20 @@ struct Scene {
FORY_STRUCT(Scene, camera, light, viewport);
};

struct EvolvingStruct {
int32_t id;

FORY_STRUCT(EvolvingStruct, id);
};

struct FixedStruct {
int32_t id;

FORY_STRUCT(FixedStruct, id);
};

FORY_STRUCT_EVOLVING(FixedStruct, false);

// Containers
struct VectorStruct {
std::vector<int32_t> numbers;
Expand Down Expand Up @@ -647,6 +662,22 @@ TEST(StructComprehensiveTest, ExternalEmptyStruct) {
test_roundtrip(external_test::ExternalEmpty{});
}

TEST(StructComprehensiveTest, StructEvolvingOverride) {
auto fory =
Fory::builder().xlang(true).compatible(true).track_ref(false).build();
ASSERT_TRUE(fory.register_struct<EvolvingStruct>(1).ok());
ASSERT_TRUE(fory.register_struct<FixedStruct>(2).ok());

auto evolving_info = fory.type_resolver().get_type_info<EvolvingStruct>();
ASSERT_TRUE(evolving_info.ok());
EXPECT_EQ(evolving_info.value()->type_id,
static_cast<uint32_t>(TypeId::COMPATIBLE_STRUCT));

auto fixed_info = fory.type_resolver().get_type_info<FixedStruct>();
ASSERT_TRUE(fixed_info.ok());
EXPECT_EQ(fixed_info.value()->type_id, static_cast<uint32_t>(TypeId::STRUCT));
}

} // namespace test
} // namespace serialization
} // namespace fory
10 changes: 6 additions & 4 deletions cpp/fory/serialization/type_resolver.h
Original file line number Diff line number Diff line change
Expand Up @@ -1344,8 +1344,9 @@ Result<void, Error> TypeResolver::register_by_id(uint32_t type_id) {

if constexpr (is_fory_serializable_v<T>) {
uint32_t actual_type_id =
compatible_ ? static_cast<uint32_t>(TypeId::COMPATIBLE_STRUCT)
: static_cast<uint32_t>(TypeId::STRUCT);
compatible_ && meta::StructEvolving<T>::value
? static_cast<uint32_t>(TypeId::COMPATIBLE_STRUCT)
: static_cast<uint32_t>(TypeId::STRUCT);
uint32_t user_type_id = type_id;

FORY_TRY(info, build_struct_type_info<T>(actual_type_id, user_type_id, "",
Expand Down Expand Up @@ -1398,8 +1399,9 @@ TypeResolver::register_by_name(const std::string &ns,

if constexpr (is_fory_serializable_v<T>) {
uint32_t actual_type_id =
compatible_ ? static_cast<uint32_t>(TypeId::NAMED_COMPATIBLE_STRUCT)
: static_cast<uint32_t>(TypeId::NAMED_STRUCT);
compatible_ && meta::StructEvolving<T>::value
? static_cast<uint32_t>(TypeId::NAMED_COMPATIBLE_STRUCT)
: static_cast<uint32_t>(TypeId::NAMED_STRUCT);

FORY_TRY(info, build_struct_type_info<T>(actual_type_id, kInvalidUserTypeId,
ns, type_name, true));
Expand Down
31 changes: 17 additions & 14 deletions docs/compiler/schema-idl.md
Original file line number Diff line number Diff line change
Expand Up @@ -1374,14 +1374,16 @@ FDL supports protobuf-style extension options for Fory-specific configuration. T
option (fory).use_record_for_java_message = true;
option (fory).polymorphism = true;
option (fory).enable_auto_type_id = true;
option (fory).evolving = true;
```

| Option | Type | Description |
| ----------------------------- | ------ | ------------------------------------------------------------ |
| `use_record_for_java_message` | bool | Generate Java records instead of classes |
| `polymorphism` | bool | Enable polymorphism for all types |
| `enable_auto_type_id` | bool | Auto-generate numeric type IDs when omitted (default: true) |
| `go_nested_type_style` | string | Go nested type naming: `underscore` (default) or `camelcase` |
| Option | Type | Description |
| ----------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `use_record_for_java_message` | bool | Generate Java records instead of classes |
| `polymorphism` | bool | Enable polymorphism for all types |
| `enable_auto_type_id` | bool | Auto-generate numeric type IDs when omitted (default: true) |
| `evolving` | bool | Default schema evolution for messages in this file (default: true). Set false to reduce payload size for messages that never change |
| `go_nested_type_style` | string | Go nested type naming: `underscore` (default) or `camelcase` |

### Message-Level Fory Options

Expand All @@ -1396,14 +1398,14 @@ message MyMessage {
}
```

| Option | Type | Description |
| --------------------- | ------ | ------------------------------------------------------------------------------------ |
| `id` | int | Type ID for serialization (auto-generated if omitted and enable_auto_type_id = true) |
| `alias` | string | Alternate name used as hash source for auto-generated IDs |
| `evolving` | bool | Schema evolution support (default: true). When false, schema is fixed like a struct |
| `use_record_for_java` | bool | Generate Java record for this message |
| `deprecated` | bool | Mark this message as deprecated |
| `namespace` | string | Custom namespace for type registration |
| Option | Type | Description |
| --------------------- | ------ | ------------------------------------------------------------------------------------------------------------------ |
| `id` | int | Type ID for serialization (auto-generated if omitted and enable_auto_type_id = true) |
| `alias` | string | Alternate name used as hash source for auto-generated IDs |
| `evolving` | bool | Schema evolution support (default: true). When false, schema is fixed like a struct and avoids compatible metadata |
| `use_record_for_java` | bool | Generate Java record for this message |
| `deprecated` | bool | Mark this message as deprecated |
| `namespace` | string | Custom namespace for type registration |

**Note:** `option (fory).id = 100` is equivalent to the inline syntax `message MyMessage [id=100]`.

Expand Down Expand Up @@ -1502,6 +1504,7 @@ message ForyFileOptions {
optional bool use_record_for_java_message = 1;
optional bool polymorphism = 2;
optional bool enable_auto_type_id = 3;
optional bool evolving = 4;
}

// Message-level options
Expand Down
25 changes: 0 additions & 25 deletions docs/compiler/type-system.md

This file was deleted.

12 changes: 12 additions & 0 deletions docs/guide/cpp/schema-evolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ int main() {
}
```

### Disable Evolution for Stable Structs

If a struct schema is stable and will not change, you can disable evolution for that struct to avoid compatible metadata overhead. Use `FORY_STRUCT_EVOLVING` after `FORY_STRUCT`:

```cpp
struct StableMessage {
int32_t id;
};
FORY_STRUCT(StableMessage, id);
FORY_STRUCT_EVOLVING(StableMessage, false);
```

## Schema Evolution Features

Compatible mode supports the following schema changes:
Expand Down
14 changes: 14 additions & 0 deletions docs/guide/go/schema-evolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ f := fory.New(fory.WithCompatible(true))
- Supports adding, removing, and reordering fields
- Enables forward and backward compatibility

### Disable Evolution for Stable Structs

If a struct schema is stable and will not change, you can disable evolution for that struct to avoid compatible metadata overhead. Implement the `ForyEvolving` interface and return `false`:

```go
type StableMessage struct {
ID int64
}

func (StableMessage) ForyEvolving() bool {
return false
}
```

## Supported Schema Changes

### Adding Fields
Expand Down
Loading
Loading