Skip to content

Commit a43f781

Browse files
committed
Updated datamodel grammar so edition is a single number (major version)
Updated version util to only update on major version update Added stability checks and refactored logging in datamodel manager Moved datamodel version check to validations. Fixed issue in serializer to properly handle 0 numeric values
1 parent c6d7c0f commit a43f781

File tree

14 files changed

+117
-53
lines changed

14 files changed

+117
-53
lines changed

examples/mapping-example/ExampleDWH/datamodel.cm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ datamodel:
44
description: "Example Data Warehouse Logical Data Model"
55
type: logical
66
version: 1.0.0
7+
crossmodel:
8+
edition: core
9+
version: 0
710
dependencies:
811
- datamodel: ExampleCRM
912
version: 1.0.0
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
datamodel:
22
id: ExampleOtherModel
33
name: "Example Other Model"
4-
type: logical
4+
type: logical
5+
crossmodel:
6+
edition: core
7+
version: 0

examples/mapping-example/Sources/ExampleCRM/datamodel.cm

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ datamodel:
22
id: ExampleCRM
33
name: "Example CRM"
44
type: logical
5-
version: 1.0.0
5+
version: 1.0.0
6+
crossmodel:
7+
edition: core
8+
version: 0

examples/mapping-example/Sources/ExampleMasterdata/datamodel.cm

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ datamodel:
22
id: ExampleMasterdata
33
name: "Example Masterdata"
44
type: logical
5-
version: 1.0.0
5+
version: 1.0.0
6+
crossmodel:
7+
edition: core
8+
version: 0

packages/core/src/browser/new-element-contribution.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
TargetObjectType,
1717
findNextUnique,
1818
getCrossModelEdition,
19-
getCrossModelVersion,
19+
getCrossModelMajorVersion,
2020
isMemberPermittedInModel,
2121
quote,
2222
toId,
@@ -64,14 +64,20 @@ interface NewElementTemplate<T extends readonly InputOptions[] = readonly InputO
6464
* The version is read from CROSSMODEL_VERSION constant which is sourced from package.json.
6565
*/
6666
function getInitialDataModelContent(): string {
67+
const edition = getCrossModelEdition();
68+
const majorVersion = getCrossModelMajorVersion();
69+
const crossmodelBlock =
70+
edition !== undefined && majorVersion !== undefined
71+
? `
72+
crossmodel:
73+
edition: ${edition}
74+
version: ${majorVersion}`
75+
: '';
6776
return `datamodel:
6877
id: _
6978
name: ""
7079
type: ${DataModelTypeInfos.logical.value}
71-
version: 1.0.0
72-
crossmodel:
73-
edition: ${getCrossModelEdition()}
74-
version: ${getCrossModelVersion()}
80+
version: 1.0.0${crossmodelBlock}
7581
`;
7682
}
7783

packages/protocol/src/model-service/protocol.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,20 @@ export interface RelationshipEdge extends CrossModelElement, IdentifiedObject, W
272272
targetNode: Reference<EntityNode>;
273273
}
274274

275+
export const CrossModelEditionInfoType = 'CrossModelEditionInfo';
276+
export interface CrossModelEditionInfo extends CrossModelElement {
277+
readonly $type: typeof CrossModelEditionInfoType;
278+
edition: string;
279+
version: number;
280+
}
281+
275282
export const DataModelType = 'DataModel';
276283
export type DataModelType = 'conceptual' | 'logical' | 'relational';
277284
export interface DataModel extends CrossModelElement, NamedObject, WithCustomProperties {
278285
readonly $type: typeof DataModelType;
279286
type: DataModelType;
280287
version?: string;
288+
crossmodel?: CrossModelEditionInfo;
281289
dependencies: Array<DataModelDependency>;
282290
}
283291

packages/protocol/src/version.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,15 @@ export function getCrossModelVersion(): string {
6363
export function getCrossModelEdition(): string {
6464
return getEditionFromPackage();
6565
}
66+
67+
/**
68+
* Returns the major version component of the CrossModel version as a number.
69+
* Minor and patch versions don't cause breaking grammar changes, so only the major version
70+
* is stored in datamodel.cm files for migration detection.
71+
*
72+
* @returns the major version number, or undefined if it cannot be parsed
73+
*/
74+
export function getCrossModelMajorVersion(): number | undefined {
75+
const version = getVersionFromPackage();
76+
return parseInt(version.split('.')[0], 10);
77+
}

packages/server/src/language-server/cross-model-datamodel-manager.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { Disposable, DocumentState, LangiumDocument, MultiMap, UriUtils } from '
77
import * as protocol from '@crossmodel/protocol';
88
import { CancellationToken, WorkspaceFolder } from 'vscode-languageserver';
99
import { URI } from 'vscode-uri';
10-
import { needsCrossModelVersionUpdate, updateCrossModelVersion } from './util/crossmodel-version-util.js';
1110
import { CrossModelLangiumDocuments } from './cross-model-langium-documents.js';
1211
import { CrossModelSharedServices } from './cross-model-module.js';
1312
import { QUALIFIED_ID_SEPARATOR } from './cross-model-naming.js';
@@ -126,10 +125,15 @@ export class CrossModelDataModelManager {
126125
const content = await this.fileSystemProvider.readDirectory(folderPath);
127126
await Promise.all(
128127
content.map(async entry => {
129-
if (entry.isDirectory) {
130-
await this.initializeDataModels(entry.uri);
131-
} else if (entry.isFile && isDataModelUri(entry.uri)) {
132-
await this.updateDataModel(entry.uri);
128+
try {
129+
if (entry.isDirectory) {
130+
await this.initializeDataModels(entry.uri);
131+
} else if (entry.isFile && isDataModelUri(entry.uri)) {
132+
await this.updateDataModel(entry.uri);
133+
}
134+
} catch (error) {
135+
const path = this.shared.workspace.WorkspaceManager.wsRelativePath(entry.uri);
136+
this.logger.error(`[DataModel] Failed to initialize data model at ${path}: ${error}`);
133137
}
134138
})
135139
);
@@ -290,23 +294,13 @@ export class CrossModelDataModelManager {
290294

291295
// Updates (or adds) the data model for the given URI. Returns the IDs of data models that were added/updated.
292296
protected async updateDataModel(uri: URI): Promise<string[]> {
293-
let newDataModel = await parseDataModelFile(uri, this.langiumDocuments);
297+
const wsRelPath = this.shared.workspace.WorkspaceManager.wsRelativePath(uri);
298+
const newDataModel = await parseDataModelFile(uri, this.langiumDocuments, this.logger);
294299
if (!newDataModel) {
300+
this.logger.error(`[DataModel] Failed to parse data model at ${wsRelPath}`);
295301
return [];
296302
}
297303

298-
// Ensure CrossModel version/edition is current on load
299-
if (needsCrossModelVersionUpdate(newDataModel)) {
300-
updateCrossModelVersion(newDataModel);
301-
// Serialize the updated AST and persist to disk
302-
const document = await this.langiumDocuments.getOrCreateDocument(uri);
303-
const serializer = this.shared.ServiceRegistry.getServices(uri).serializer.Serializer;
304-
const text = serializer.serialize(document.parseResult.value);
305-
await this.shared.workspace.TextDocumentManager.save(uri.toString(), text, 'datamodel-manager');
306-
// Re-parse so in-memory document matches updated disk content
307-
newDataModel = (await parseDataModelFile(uri, this.langiumDocuments)) ?? newDataModel;
308-
}
309-
310304
const toUpdate = [];
311305
const existingDataModelInfo = this.uriToDataModel.get(uri.toString());
312306
const newDataModelId = createDataModelId(newDataModel.id, newDataModel.version);
@@ -368,19 +362,25 @@ function getAndRemoveDataModelUris(uris: URI[]): URI[] {
368362
return dataModels;
369363
}
370364

371-
async function parseDataModelFile(uri?: URI, langiumDocuments?: CrossModelLangiumDocuments): Promise<DataModel | undefined> {
365+
async function parseDataModelFile(
366+
uri?: URI,
367+
langiumDocuments?: CrossModelLangiumDocuments,
368+
logger?: { error(message: string): void }
369+
): Promise<DataModel | undefined> {
372370
if (!uri || !langiumDocuments) {
373371
return undefined;
374372
}
375373
try {
376374
const document = await langiumDocuments.updateOrCreateDocument(uri);
377375
if (document.parseResult.lexerErrors.length > 0 || document.parseResult.parserErrors.length > 0) {
378-
console.error('Parse errors in datamodel file:', document.parseResult.lexerErrors, document.parseResult.parserErrors);
376+
const msg = `[DataModel] Parse errors in ${uri.fsPath}`;
377+
logger ? logger.error(msg) : console.error(msg);
379378
return undefined;
380379
}
381380
return findDataModel(document);
382381
} catch (error) {
383-
console.error('Failed to parse datamodel file:', error);
382+
const msg = `[DataModel] Failed to parse ${uri.fsPath}: ${error}`;
383+
logger ? logger.error(msg) : console.error(msg);
384384
return undefined;
385385
}
386386
}

packages/server/src/language-server/cross-model-serializer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export class CrossModelSerializer implements Serializer<CrossModelRoot> {
109109
// arrays and objects start on a new line -- skip some objects that we do not actually serialize in object structure
110110
const onNewLine = Array.isArray(propValue) || (isAstNode(propValue) && !isInlineSerializedType(propValue.$type));
111111
const serializedPropValue = this.toYaml(value, prop, propValue, onNewLine ? indentationLevel + 1 : 0);
112-
if (!serializedPropValue) {
112+
if (serializedPropValue === undefined || serializedPropValue === '') {
113113
return undefined;
114114
}
115115
const separator = onNewLine ? CrossModelSerializer.CHAR_NEWLINE : ' ';

packages/server/src/language-server/cross-model-validator.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
AttributeMappingExpression,
2020
BinaryExpression,
2121
CrossModelAstType,
22+
DataModel,
2223
IdentifiedObject,
2324
InheritanceEdge,
2425
isCrossModelRoot,
@@ -40,6 +41,7 @@ import {
4041
TargetObjectAttribute
4142
} from './generated/ast.js';
4243
import { findDocument, getOwner, isSemanticRoot } from './util/ast-util.js';
44+
import { isMissingCrossModelVersion, needsCrossModelMigration } from './util/crossmodel-version-util.js';
4345
import { getAttributeMappingExpressionRefRange } from './util/expression-range.js';
4446

4547
export interface FilenameNotMatchingDiagnostic extends Diagnostic {
@@ -63,6 +65,7 @@ export function registerValidationChecks(services: CrossModelServices): void {
6365

6466
const checks: ValidationChecks<CrossModelAstType> = {
6567
AstNode: validator.checkNode,
68+
DataModel: validator.checkDataModel,
6669
IdentifiedObject: validator.checkIdentifiedObject,
6770
AttributeMapping: validator.checkAttributeMapping,
6871
LogicalAttribute: validator.checkLogicalAttribute,
@@ -87,6 +90,14 @@ export function registerValidationChecks(services: CrossModelServices): void {
8790
export class CrossModelValidator {
8891
constructor(protected services: CrossModelServices) {}
8992

93+
checkDataModel(dataModel: DataModel, accept: ValidationAcceptor): void {
94+
if (isMissingCrossModelVersion(dataModel)) {
95+
accept('warning', 'This data model is missing a CrossModel version and needs migration.', { node: dataModel });
96+
} else if (needsCrossModelMigration(dataModel)) {
97+
accept('error', 'This data model was created with a different CrossModel version and needs migration.', { node: dataModel });
98+
}
99+
}
100+
90101
checkNamedObject(namedObject: NamedObject, accept: ValidationAcceptor): void {
91102
if (namedObject.name === undefined || namedObject.name.length === 0) {
92103
accept('error', 'The name cannot be empty', {

0 commit comments

Comments
 (0)