Skip to content

Commit e58b5cd

Browse files
Feature / Model versioning semantics (#467)
* Provide a model version validator * Provide a flow version validator (no-op) * Add models and flows to the list of objects that support versioning * Functional test case for model version validation * Add new version validation test at the metadata API level
1 parent 221c6d4 commit e58b5cd

File tree

7 files changed

+566
-7
lines changed

7 files changed

+566
-7
lines changed

tracdap-libs/tracdap-lib-common/src/main/java/org/finos/tracdap/common/metadata/MetadataConstants.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ public class MetadataConstants {
4747
ObjectType.SCHEMA,
4848
ObjectType.FILE,
4949
ObjectType.STORAGE,
50-
ObjectType.CUSTOM);
50+
ObjectType.CUSTOM,
51+
ObjectType.MODEL,
52+
ObjectType.FLOW);
5153

5254
// Valid identifiers are made up of alphanumeric characters, numbers and the underscore, not starting with a number
5355
// Use \\A - \\Z to match the whole input

tracdap-libs/tracdap-lib-test/src/main/java/org/finos/tracdap/test/meta/TestData.java

+119-2
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,37 @@ public static ObjectDefinition dummyVersionForType(ObjectDefinition definition)
8484
case CUSTOM: return nextCustomDef(definition);
8585
case STORAGE: return nextStorageDef(definition);
8686
case SCHEMA: return nextSchemaDef(definition);
87+
case FILE: return nextFileDef(definition);
88+
89+
case FLOW:
90+
case JOB:
91+
92+
return definition;
93+
94+
default:
95+
throw new RuntimeException("No second version available in dummy data for object type " + objectType.name());
96+
}
97+
}
98+
99+
public static ObjectDefinition dummyBadVersionForType(ObjectDefinition definition) {
100+
101+
// Not all object types have semantics defined for versioning
102+
// It is sometimes helpful to create versions anyway for testing
103+
// E.g. to test that version increments are rejected for objects that don't support versioning!
104+
105+
var objectType = definition.getObjectType();
106+
107+
switch (objectType) {
108+
109+
case DATA: return nextBadDataDef(definition);
110+
case MODEL: return nextBadModelDef(definition);
111+
case CUSTOM: return nextBadCustomDef(definition);
112+
case STORAGE: return nextBadStorageDef(definition);
113+
case SCHEMA: return nextBadSchemaDef(definition);
114+
case FILE: return nextBadFileDef(definition);
87115

88116
case FLOW:
89117
case JOB:
90-
case FILE:
91118

92119
return definition;
93120

@@ -185,6 +212,16 @@ public static ObjectDefinition nextDataDef(ObjectDefinition origDef) {
185212
.build();
186213
}
187214

215+
public static ObjectDefinition nextBadDataDef(ObjectDefinition origDef) {
216+
217+
var newSchema = changeBadSchemaField(origDef.getData().getSchema());
218+
219+
return origDef.toBuilder()
220+
.setData(origDef.getData().toBuilder()
221+
.setSchema(newSchema))
222+
.build();
223+
}
224+
188225
public static ObjectDefinition dummySchemaDef() {
189226

190227
var dataDef = dummyDataDef();
@@ -202,6 +239,13 @@ public static ObjectDefinition nextSchemaDef(ObjectDefinition origDef) {
202239
.build();
203240
}
204241

242+
public static ObjectDefinition nextBadSchemaDef(ObjectDefinition origDef) {
243+
244+
return origDef.toBuilder()
245+
.setSchema(changeBadSchemaField(origDef.getSchema()))
246+
.build();
247+
}
248+
205249
private static SchemaDefinition addFieldToSchema(SchemaDefinition origSchema) {
206250

207251
var fieldName = "extra_field_" + (origSchema.getTable().getFieldsCount() + 1);
@@ -219,6 +263,19 @@ private static SchemaDefinition addFieldToSchema(SchemaDefinition origSchema) {
219263
.build();
220264
}
221265

266+
private static SchemaDefinition changeBadSchemaField(SchemaDefinition origSchema) {
267+
268+
var originalField = origSchema.getTable().getFields(0);
269+
var newField = originalField.toBuilder()
270+
.setFieldType(BasicType.DATE)
271+
.build();
272+
273+
return origSchema.toBuilder()
274+
.setTable(origSchema.getTable().toBuilder()
275+
.setFields(0, newField))
276+
.build();
277+
}
278+
222279
public static ObjectDefinition dummyStorageDef() {
223280

224281
var storageTimestamp = OffsetDateTime.now();
@@ -265,6 +322,26 @@ public static ObjectDefinition nextStorageDef(ObjectDefinition definition) {
265322
.build();
266323
}
267324

325+
public static ObjectDefinition nextBadStorageDef(ObjectDefinition definition) {
326+
327+
var originalItem = definition.getStorage().getDataItemsOrThrow("dummy_item");
328+
var originalCopy = originalItem.getIncarnations(0).getCopies(0);
329+
330+
var newCopy = originalCopy.toBuilder()
331+
.setStorageFormat("PARQUET")
332+
.build();
333+
334+
var newItem = originalItem.toBuilder()
335+
.setIncarnations(0, originalItem.getIncarnations(0).toBuilder()
336+
.setCopies(0, newCopy))
337+
.build();
338+
339+
return definition.toBuilder()
340+
.setStorage(definition.getStorage().toBuilder()
341+
.putDataItems("dummy_item", newItem))
342+
.build();
343+
}
344+
268345
public static ObjectDefinition dummyModelDef() {
269346

270347
return ObjectDefinition.newBuilder()
@@ -315,6 +392,15 @@ public static ObjectDefinition nextModelDef(ObjectDefinition origDef) {
315392
.build();
316393
}
317394

395+
public static ObjectDefinition nextBadModelDef(ObjectDefinition origDef) {
396+
397+
return origDef.toBuilder()
398+
.setModel(origDef.getModel()
399+
.toBuilder()
400+
.setPath("altered/layout/src"))
401+
.build();
402+
}
403+
318404
public static ObjectDefinition dummyFlowDef() {
319405

320406
return ObjectDefinition.newBuilder()
@@ -374,6 +460,27 @@ public static ObjectDefinition dummyFileDef() {
374460
.build();
375461
}
376462

463+
public static ObjectDefinition nextFileDef(ObjectDefinition origDef) {
464+
465+
return origDef.toBuilder()
466+
.setFile(origDef.getFile().toBuilder()
467+
.setName("magic_template_v2_updated") // File names are likely to be changed with suffixes etc
468+
.setSize(87533)
469+
.setDataItem("file/FILE_ID/version-2"))
470+
.build();
471+
}
472+
473+
public static ObjectDefinition nextBadFileDef(ObjectDefinition origDef) {
474+
475+
return origDef.toBuilder()
476+
.setFile(origDef.getFile().toBuilder()
477+
.setExtension("txt")
478+
.setMimeType("text/plain")
479+
.setSize(87533)
480+
.setDataItem("file/FILE_ID/version-2"))
481+
.build();
482+
}
483+
377484
public static ObjectDefinition dummyCustomDef() {
378485

379486
var jsonReportDef = "{ reportType: 'magic', mainGraph: { content: 'more_magic' } }";
@@ -382,7 +489,7 @@ public static ObjectDefinition dummyCustomDef() {
382489
.setObjectType(ObjectType.CUSTOM)
383490
.setCustom(CustomDefinition.newBuilder()
384491
.setCustomSchemaType("REPORT")
385-
.setCustomSchemaVersion(2)
492+
.setCustomSchemaVersion(1)
386493
.setCustomData(ByteString.copyFromUtf8(jsonReportDef)))
387494
.build();
388495
}
@@ -395,10 +502,20 @@ public static ObjectDefinition nextCustomDef(ObjectDefinition origDef) {
395502
return origDef.toBuilder()
396503
.setCustom(origDef.getCustom()
397504
.toBuilder()
505+
.setCustomSchemaVersion(2)
398506
.setCustomData(ByteString.copyFromUtf8(jsonReportDef)))
399507
.build();
400508
}
401509

510+
public static ObjectDefinition nextBadCustomDef(ObjectDefinition origDef) {
511+
512+
return origDef.toBuilder()
513+
.setCustom(origDef.getCustom()
514+
.toBuilder()
515+
.setCustomSchemaType("DASHBOARD"))
516+
.build();
517+
}
518+
402519
public static Tag dummyTag(ObjectDefinition definition, boolean includeHeader) {
403520

404521
var tag = Tag.newBuilder()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2024 finTRAC Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.finos.tracdap.common.validation.version;
18+
19+
import org.finos.tracdap.common.validation.core.ValidationContext;
20+
import org.finos.tracdap.common.validation.core.ValidationType;
21+
import org.finos.tracdap.common.validation.core.Validator;
22+
import org.finos.tracdap.metadata.FlowDefinition;
23+
24+
@Validator(type = ValidationType.VERSION)
25+
public class FlowVersionValidator {
26+
27+
@Validator
28+
public static ValidationContext flowVersion(FlowDefinition current, FlowDefinition prior, ValidationContext ctx) {
29+
30+
// There are no structural constraints on what constitutes a flow version
31+
// This is to stay in line with model version validation, since flows have no "coordinates"
32+
// What constitutes a flow version vs a new flow is left upto the client application to decide
33+
34+
return ctx;
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2024 finTRAC Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.finos.tracdap.common.validation.version;
18+
19+
import org.finos.tracdap.metadata.ModelDefinition;
20+
import org.finos.tracdap.common.validation.core.ValidationContext;
21+
import org.finos.tracdap.common.validation.core.ValidationType;
22+
import org.finos.tracdap.common.validation.core.Validator;
23+
24+
import com.google.protobuf.Descriptors;
25+
26+
import static org.finos.tracdap.common.validation.core.ValidatorUtils.field;
27+
28+
29+
@Validator(type = ValidationType.VERSION)
30+
public class ModelVersionValidator {
31+
32+
private static final Descriptors.Descriptor MODEL_DEFINITION;
33+
private static final Descriptors.FieldDescriptor MD_MODEL_TYPE;
34+
private static final Descriptors.FieldDescriptor MD_LANGUAGE;
35+
private static final Descriptors.FieldDescriptor MD_REPOSITORY;
36+
private static final Descriptors.FieldDescriptor MD_PACKAGE_GROUP;
37+
private static final Descriptors.FieldDescriptor MD_PACKAGE;
38+
private static final Descriptors.FieldDescriptor MD_VERSION;
39+
private static final Descriptors.FieldDescriptor MD_PATH;
40+
private static final Descriptors.FieldDescriptor MD_ENTRY_POINT;
41+
42+
static {
43+
44+
MODEL_DEFINITION = ModelDefinition.getDescriptor();
45+
MD_MODEL_TYPE = field(MODEL_DEFINITION, ModelDefinition.MODELTYPE_FIELD_NUMBER);
46+
MD_LANGUAGE = field(MODEL_DEFINITION, ModelDefinition.LANGUAGE_FIELD_NUMBER);
47+
MD_REPOSITORY = field(MODEL_DEFINITION, ModelDefinition.REPOSITORY_FIELD_NUMBER);
48+
MD_PACKAGE_GROUP = field(MODEL_DEFINITION, ModelDefinition.PACKAGEGROUP_FIELD_NUMBER);
49+
MD_PACKAGE = field(MODEL_DEFINITION, ModelDefinition.PACKAGE_FIELD_NUMBER);
50+
MD_VERSION = field(MODEL_DEFINITION, ModelDefinition.VERSION_FIELD_NUMBER);
51+
MD_PATH = field(MODEL_DEFINITION, ModelDefinition.PATH_FIELD_NUMBER);
52+
MD_ENTRY_POINT = field(MODEL_DEFINITION, ModelDefinition.ENTRYPOINT_FIELD_NUMBER);
53+
}
54+
55+
@Validator
56+
public static ValidationContext modelVersion(ModelDefinition current, ModelDefinition prior, ValidationContext ctx) {
57+
58+
// Two models are considered versions of the same model if all coordinates match between versions
59+
// I.e. model compatibility is not based on functional compatibility
60+
// If a model is moved to a different repo or namespace, it is considered a new model
61+
// So, the evolution of one model in code can be tracked by a single model object in TRAC
62+
63+
ctx = ctx.push(MD_MODEL_TYPE)
64+
.apply(CommonValidators::exactMatch)
65+
.pop();
66+
67+
ctx = ctx.push(MD_LANGUAGE)
68+
.apply(CommonValidators::exactMatch)
69+
.pop();
70+
71+
ctx = ctx.push(MD_REPOSITORY)
72+
.apply(CommonValidators::exactMatch)
73+
.pop();
74+
75+
ctx = ctx.push(MD_PACKAGE_GROUP)
76+
.apply(CommonValidators::exactMatch)
77+
.pop();
78+
79+
ctx = ctx.push(MD_PACKAGE)
80+
.apply(CommonValidators::exactMatch)
81+
.pop();
82+
83+
ctx = ctx.push(MD_VERSION)
84+
.apply(CommonValidators::exactMatch)
85+
.pop();
86+
87+
ctx = ctx.push(MD_PATH)
88+
.apply(CommonValidators::exactMatch)
89+
.pop();
90+
91+
ctx = ctx.push(MD_ENTRY_POINT)
92+
.apply(CommonValidators::exactMatch)
93+
.pop();
94+
95+
return ctx;
96+
}
97+
}

tracdap-libs/tracdap-lib-validation/src/test/java/org/finos/tracdap/common/validation/test/BaseValidatorTest.java

+17
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import com.google.protobuf.Message;
2121
import org.finos.tracdap.common.exception.EInputValidation;
22+
import org.finos.tracdap.common.exception.EVersionValidation;
2223
import org.finos.tracdap.common.validation.Validator;
2324
import org.junit.jupiter.api.Assertions;
2425
import org.junit.jupiter.api.BeforeAll;
@@ -46,4 +47,20 @@ protected static <TMsg extends Message> void expectInvalid(TMsg msg) {
4647
() -> validator.validateFixedObject(msg),
4748
"Validation passed for an invalid message");
4849
}
50+
51+
protected static <TMsg extends Message>
52+
void expectValidVersion(TMsg current, TMsg prior) {
53+
54+
Assertions.assertDoesNotThrow(
55+
() -> validator.validateVersion(current, prior),
56+
"Versioning validation failed for a valid version update");
57+
}
58+
59+
protected static <TMsg extends Message>
60+
void expectInvalidVersion(TMsg current, TMsg prior) {
61+
62+
Assertions.assertThrows(EVersionValidation.class,
63+
() -> validator.validateVersion(current, prior),
64+
"Versioning validation passed for an invalid version update");
65+
}
4966
}

0 commit comments

Comments
 (0)