///|
pub(all) struct SchemaDifference {
direction : String
code : String
path : String
detail : String
} derive(Debug, ToJson)
///|
/// Conservative compatibility checks for this runtime's strict decoding
/// profile. An empty report is not a proof about arbitrary producer values.
pub(all) struct EvolutionReport {
old_version : Int32
new_version : Int32
old_reads_new : Bool
new_reads_old : Bool
differences : Array[SchemaDifference]
} derive(Debug, ToJson)
///|
fn difference(
issues : Array[SchemaDifference],
direction : String,
code : String,
path : String,
detail : String,
) -> Unit {
issues.push({ direction, code, path, detail, })
}
///|
fn find_field(block : BlockDef, id : Int32) -> FieldDef? {
for field in block.fields {
if field.id == id {
return Some(field)
}
}
None
}
///|
fn find_group(block : BlockDef, id : Int32) -> GroupDef? {
for group in block.groups {
if group.id == id {
return Some(group)
}
}
None
}
///|
fn find_data(block : BlockDef, id : Int32) -> DataDef? {
for data in block.data {
if data.id == id {
return Some(data)
}
}
None
}
///|
fn compare_type(
old : Schema,
newer : Schema,
old_name : String,
new_name : String,
path : String,
issues : Array[SchemaDifference],
depth : Int32,
) -> Unit raise SbeError {
if depth > 64 {
raise LimitExceeded("evolution comparison depth exceeds 64")
}
match (old.type_def(old_name), newer.type_def(new_name)) {
(Encoded(a), Encoded(b)) => {
if a.primitive != b.primitive ||
a.length != b.length ||
a.presence != b.presence ||
a.constant != b.constant ||
a.character_encoding != b.character_encoding {
difference(
issues, "both", "encoding-changed", path, "primitive, array, constant, presence or character encoding changed",
)
}
if a.min_value != b.min_value ||
a.max_value != b.max_value ||
a.null_value != b.null_value {
difference(
issues, "both", "value-domain-changed", path, "minValue, maxValue or nullValue changed; producer values require review",
)
}
}
(Enumeration(ap, ac, presence_a), Enumeration(bp, bc, presence_b)) => {
if ap != bp || presence_a != presence_b {
difference(
issues, "both", "enum-encoding-changed", path, "enum primitive or presence changed",
)
}
compare_cases(ac, bc, path, issues, "enum")
}
(BitSet(ap, ac), BitSet(bp, bc)) => {
if ap != bp {
difference(
issues, "both", "set-encoding-changed", path, "set primitive changed",
)
}
compare_cases(ac, bc, path, issues, "set")
}
(Composite(a, a_length), Composite(b, b_length)) => {
if a_length != b_length || a.length() != b.length() {
difference(
issues, "both", "composite-layout-changed", path, "composite encoded length or member count changed",
)
}
let length = if a.length() < b.length() { a.length() } else { b.length() }
for index in 0..
difference(
issues, "both", "type-kind-changed", path, "encoding kind changed between primitive, enum, set and composite",
)
}
}
///|
fn compare_cases(
old_cases : Array[EnumCase],
new_cases : Array[EnumCase],
path : String,
issues : Array[SchemaDifference],
kind : String,
) -> Unit {
for before in old_cases {
let mut found = false
for after in new_cases {
if before.name == after.name {
found = true
if before.value != after.value ||
before.since_version != after.since_version {
difference(
issues,
"both",
"\{kind}-case-changed",
"\{path}.\{before.name}",
"existing symbolic value or sinceVersion changed",
)
}
}
}
if !found {
difference(
issues,
"both",
"\{kind}-case-removed",
"\{path}.\{before.name}",
"existing enum/set choice was removed",
)
}
}
for after in new_cases {
let mut found = false
for before in old_cases {
if before.name == after.name {
found = true
}
}
if !found {
difference(
issues,
"old-reads-new",
"\{kind}-case-added",
"\{path}.\{after.name}",
"strict older decoders reject the new symbolic value or bit",
)
}
}
}
///|
fn compare_block(
old : Schema,
newer : Schema,
a : BlockDef,
b : BlockDef,
path : String,
issues : Array[SchemaDifference],
depth : Int32,
) -> Unit raise SbeError {
if depth > 32 {
raise LimitExceeded("evolution group depth exceeds 32")
}
if b.block_length < a.block_length {
difference(
issues, "old-reads-new", "block-shrank", path, "new fixed block is shorter than the old layout",
)
}
for before in a.fields {
let field_path = "\{path}.\{before.name}"
match find_field(b, before.id) {
None =>
difference(
issues, "both", "field-removed", field_path, "existing field ID no longer exists",
)
Some(after) => {
if before.offset != after.offset ||
before.presence != after.presence ||
before.since_version != after.since_version ||
before.value_ref != after.value_ref ||
before.name != after.name {
difference(
issues, "both", "field-contract-changed", field_path, "offset, presence, version, constant reference or symbolic name changed",
)
}
compare_type(
old,
newer,
before.type_name,
after.type_name,
field_path,
issues,
0,
)
}
}
}
for after in b.fields {
if find_field(a, after.id) is None {
if after.since_version <= old.version ||
(after.presence != Constant && after.offset < a.block_length) {
difference(
issues,
"both",
"unsafe-field-addition",
"\{path}.\{after.name}",
"new fixed fields must be versioned and appended beyond the previous block",
)
}
}
}
let old_group_order = a.groups.map(group => group.id)
let shared_group_order = b.groups
.filter(group => find_group(a, group.id) is Some(_))
.map(group => group.id)
if old_group_order != shared_group_order {
difference(
issues, "both", "group-order-changed", path, "existing repeating group order changed or a group was removed",
)
}
for before in a.groups {
match find_group(b, before.id) {
None =>
difference(
issues,
"both",
"group-removed",
"\{path}.\{before.name}",
"old repeating group no longer exists",
)
Some(after) => {
if before.name != after.name ||
before.since_version != after.since_version ||
before.count_primitive != after.count_primitive ||
before.max_count != after.max_count {
difference(
issues,
"both",
"group-dimensions-changed",
"\{path}.\{before.name}",
"group identity, version or count encoding changed",
)
}
compare_block(
old,
newer,
before.block,
after.block,
"\{path}.\{before.name}[]",
issues,
depth + 1,
)
}
}
}
for after in b.groups {
if find_group(a, after.id) is None {
let direction = if after.since_version > old.version {
"old-reads-new"
} else {
"both"
}
difference(
issues,
direction,
"group-added",
"\{path}.\{after.name}",
"older strict decoders cannot skip unknown variable group bodies",
)
}
}
let old_data_order = a.data.map(data => data.id)
let shared_data_order = b.data
.filter(data => find_data(a, data.id) is Some(_))
.map(data => data.id)
if old_data_order != shared_data_order {
difference(
issues, "both", "data-order-changed", path, "existing variable data order changed or data was removed",
)
}
for before in a.data {
match find_data(b, before.id) {
None =>
difference(
issues,
"both",
"data-removed",
"\{path}.\{before.name}",
"old variable data no longer exists",
)
Some(after) =>
if before.name != after.name ||
before.since_version != after.since_version ||
before.length_primitive != after.length_primitive ||
before.max_length != after.max_length ||
before.character_encoding != after.character_encoding {
difference(
issues,
"both",
"data-encoding-changed",
"\{path}.\{before.name}",
"variable data identity, length or text encoding changed",
)
}
}
}
for after in b.data {
if find_data(a, after.id) is None {
let direction = if after.since_version > old.version {
"old-reads-new"
} else {
"both"
}
difference(
issues,
direction,
"data-added",
"\{path}.\{after.name}",
"older strict decoders reject unknown trailing variable data",
)
}
}
}
///|
/// Compare two compiled schema versions. Results describe this library's
/// strict wire and symbolic-value profile, not general SBE conformance.
pub fn compare_schemas(
old : Schema,
newer : Schema,
) -> EvolutionReport raise SbeError {
let issues : Array[SchemaDifference] = []
if old.id != newer.id {
difference(
issues, "both", "schema-id-changed", "schema", "schemaId changed",
)
}
if old.byte_order != newer.byte_order {
difference(
issues, "both", "byte-order-changed", "schema", "byteOrder changed",
)
}
if newer.version < old.version {
difference(
issues, "both", "version-decreased", "schema", "new schema version is older",
)
}
for before in old.messages {
let mut found : MessageDef? = None
for after in newer.messages {
if before.id == after.id {
found = Some(after)
}
}
match found {
None =>
difference(
issues,
"both",
"message-removed",
before.name,
"templateId was removed",
)
Some(after) => {
if before.name != after.name ||
before.since_version != after.since_version {
difference(
issues,
"both",
"message-identity-changed",
before.name,
"message name or introduction version changed",
)
}
compare_block(
old,
newer,
before.block,
after.block,
before.name,
issues,
0,
)
}
}
}
for after in newer.messages {
let mut found = false
for before in old.messages {
if before.id == after.id {
found = true
}
}
if !found {
difference(
issues,
"old-reads-new",
"message-added",
after.name,
"old decoders do not recognize the new templateId",
)
}
}
let mut old_reads_new = true
let mut new_reads_old = true
for issue in issues {
if issue.direction == "both" || issue.direction == "old-reads-new" {
old_reads_new = false
}
if issue.direction == "both" || issue.direction == "new-reads-old" {
new_reads_old = false
}
}
{
old_version: old.version,
new_version: newer.version,
old_reads_new,
new_reads_old,
differences: issues,
}
}