diff --git a/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGeneratorTest.java b/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGeneratorTest.java index c5428bd21..b9b8f580f 100644 --- a/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGeneratorTest.java +++ b/core/esmf-aspect-model-aas-generator/src/test/java/org/eclipse/esmf/aspectmodel/aas/AspectModelAasGeneratorTest.java @@ -21,7 +21,6 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Set; - import javax.xml.XMLConstants; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AbstractGenerator.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AbstractGenerator.java index 62b92d88d..76b36b67f 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AbstractGenerator.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AbstractGenerator.java @@ -30,4 +30,6 @@ protected void writeCharSequenceToOutputStream( final CharSequence charSequence, writer.flush(); } } + + public static final String SAMM_EXTENSION = "x-samm-aspect-model-urn"; } diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AbstractSchemaArtifact.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AbstractSchemaArtifact.java index ee8281fd0..38966aecf 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AbstractSchemaArtifact.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/AbstractSchemaArtifact.java @@ -20,7 +20,6 @@ import java.util.function.Function; import java.util.stream.Collectors; -import org.eclipse.esmf.aspectmodel.generator.jsonschema.AspectModelJsonSchemaVisitor; import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; import com.fasterxml.jackson.core.JsonProcessingException; @@ -125,14 +124,14 @@ private JsonNode updateRefValues( final JsonNode node, final Map protected Map getContentWithSeparateSchemasAsJson( final Optional mainSpec ) { final JsonNode jsonContent = getContent(); final String aspectName = AspectModelUrn.fromUrn( - jsonContent.get( "info" ).get( AspectModelJsonSchemaVisitor.SAMM_EXTENSION ).asText() ).getName(); + jsonContent.get( "info" ).get( AbstractGenerator.SAMM_EXTENSION ).asText() ).getName(); return getSeparateSchemas( aspectName, "json", mainSpec ); } protected Map getContentWithSeparateSchemasAsYaml( final Optional mainSpec ) { final JsonNode jsonContent = getContent(); final String aspectName = AspectModelUrn.fromUrn( - jsonContent.get( "info" ).get( AspectModelJsonSchemaVisitor.SAMM_EXTENSION ).asText() ).getName(); + jsonContent.get( "info" ).get( AbstractGenerator.SAMM_EXTENSION ).asText() ).getName(); return getSeparateSchemas( aspectName, "yaml", mainSpec ).entrySet().stream().collect( Collectors.toMap( Map.Entry::getKey, entry -> jsonToYaml( entry.getValue() ) ) ); } diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/asyncapi/AspectModelAsyncApiGenerator.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/asyncapi/AspectModelAsyncApiGenerator.java index 2b71a4b1b..f545d3de8 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/asyncapi/AspectModelAsyncApiGenerator.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/asyncapi/AspectModelAsyncApiGenerator.java @@ -6,9 +6,9 @@ import java.util.Locale; import org.eclipse.esmf.aspectmodel.VersionNumber; +import org.eclipse.esmf.aspectmodel.generator.AbstractGenerator; import org.eclipse.esmf.aspectmodel.generator.ArtifactGenerator; import org.eclipse.esmf.aspectmodel.generator.XsdToJsonTypeMapping; -import org.eclipse.esmf.aspectmodel.generator.jsonschema.AspectModelJsonSchemaVisitor; import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn; import org.eclipse.esmf.metamodel.Aspect; import org.eclipse.esmf.metamodel.Event; @@ -59,7 +59,7 @@ public AsyncApiSchemaArtifact apply( final Aspect aspect, final AsyncApiSchemaGe info.put( TITLE_FIELD, aspect.getPreferredName( config.locale() ) + " MQTT API" ); info.put( "version", apiVersion ); info.put( DESCRIPTION_FIELD, getDescription( aspect.getDescription( config.locale() ) ) ); - info.put( AspectModelJsonSchemaVisitor.SAMM_EXTENSION, + info.put( AbstractGenerator.SAMM_EXTENSION, aspect.getAspectModelUrn().map( AspectModelUrn::toString ).orElseThrow() ); rootNode.set( "channels", getChannelNode( aspect, config ) ); diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/jsonschema/AspectModelJsonSchemaVisitor.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/jsonschema/AspectModelJsonSchemaVisitor.java index 4807a92d4..4c75e8083 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/jsonschema/AspectModelJsonSchemaVisitor.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/jsonschema/AspectModelJsonSchemaVisitor.java @@ -25,6 +25,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import org.eclipse.esmf.aspectmodel.generator.AbstractGenerator; import org.eclipse.esmf.aspectmodel.generator.DocumentGenerationException; import org.eclipse.esmf.aspectmodel.generator.XsdToJsonTypeMapping; import org.eclipse.esmf.aspectmodel.resolver.services.SammDataType; @@ -77,7 +78,6 @@ import org.apache.jena.vocabulary.XSD; public class AspectModelJsonSchemaVisitor implements AspectVisitor { - public static final String SAMM_EXTENSION = "x-samm-aspect-model-urn"; private static final JsonNodeFactory FACTORY = JsonNodeFactory.instance; private final List processedProperties = new LinkedList<>(); private final BiMap schemaNameForElement = HashBiMap.create(); @@ -667,6 +667,6 @@ private ObjectNode addDescription( final ObjectNode node, final NamedElement des } private void addSammExtensionAttribute( final ObjectNode node, final NamedElement describedElement ) { - describedElement.getAspectModelUrn().ifPresent( urn -> node.put( SAMM_EXTENSION, urn.toString() ) ); + describedElement.getAspectModelUrn().ifPresent( urn -> node.put( AbstractGenerator.SAMM_EXTENSION, urn.toString() ) ); } } diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGenerator.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGenerator.java index 06333bf4a..65f2083f3 100644 --- a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGenerator.java +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGenerator.java @@ -28,6 +28,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.eclipse.esmf.aspectmodel.generator.AbstractGenerator; import org.eclipse.esmf.aspectmodel.generator.ArtifactGenerator; import org.eclipse.esmf.aspectmodel.generator.jsonschema.AspectModelJsonSchemaGenerator; import org.eclipse.esmf.aspectmodel.generator.jsonschema.AspectModelJsonSchemaVisitor; @@ -123,7 +124,7 @@ public OpenApiSchemaArtifact apply( final Aspect aspect, final OpenApiSchemaGene ((ObjectNode) rootNode.get( "info" )).put( "title", aspect.getPreferredName( config.locale() ) ); ((ObjectNode) rootNode.get( "info" )).put( "version", apiVersion ); - ((ObjectNode) rootNode.get( "info" )).put( AspectModelJsonSchemaVisitor.SAMM_EXTENSION, + ((ObjectNode) rootNode.get( "info" )).put( AbstractGenerator.SAMM_EXTENSION, aspect.getAspectModelUrn().map( Object::toString ).orElse( "" ) ); setServers( rootNode, config.baseUrl(), apiVersion, READ_SERVER_PATH ); final boolean includePaging = includePaging( aspect, config.pagingOption() ); diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/AspectModelSqlGenerator.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/AspectModelSqlGenerator.java new file mode 100644 index 000000000..2107dc448 --- /dev/null +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/AspectModelSqlGenerator.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel.generator.sql; + +import org.eclipse.esmf.aspectmodel.generator.ArtifactGenerator; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.AspectModelDatabricksDenormalizedSqlVisitor; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.AspectModelDatabricksDenormalizedSqlVisitorContextBuilder; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfig; +import org.eclipse.esmf.metamodel.Aspect; + +/** + * Generates SQL scripts from an Aspect Model that set up tables to contain the data of the Aspect. + */ +public class AspectModelSqlGenerator implements ArtifactGenerator { + public static final AspectModelSqlGenerator INSTANCE = new AspectModelSqlGenerator(); + + private AspectModelSqlGenerator() { + } + + @Override + public SqlArtifact apply( final Aspect aspect, final SqlGenerationConfig sqlGenerationConfig ) { + final String content = switch ( sqlGenerationConfig.dialect() ) { + case DATABRICKS -> switch ( sqlGenerationConfig.mappingStrategy() ) { + case DENORMALIZED -> { + final DatabricksSqlGenerationConfig config = + sqlGenerationConfig.dialectSpecificConfig() instanceof final DatabricksSqlGenerationConfig databricksConfig + ? databricksConfig + : new DatabricksSqlGenerationConfig(); + yield aspect.accept( new AspectModelDatabricksDenormalizedSqlVisitor( config ), + AspectModelDatabricksDenormalizedSqlVisitorContextBuilder.builder().build() ); + } + }; + }; + + return new SqlArtifact( aspect.getName() + ".sql", content ); + } +} diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/SqlArtifact.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/SqlArtifact.java new file mode 100644 index 000000000..c3f05d75f --- /dev/null +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/SqlArtifact.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel.generator.sql; + +import org.eclipse.esmf.aspectmodel.generator.Artifact; + +/** + * Represents a generated SQL script. + */ +public class SqlArtifact implements Artifact { + private final String id; + private final String content; + + public SqlArtifact( final String id, final String content ) { + this.id = id; + this.content = content; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getContent() { + return content; + } +} diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/SqlGenerationConfig.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/SqlGenerationConfig.java new file mode 100644 index 000000000..b13f542a8 --- /dev/null +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/SqlGenerationConfig.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel.generator.sql; + +import org.eclipse.esmf.aspectmodel.generator.GenerationConfig; + +import io.soabase.recordbuilder.core.RecordBuilder; + +/** + * Configuration for generating SQL scripts from an Aspect Model. + * + * @param dialect the SQL dialect to generate for + * @param mappingStrategy the mapping strategy to use + * @param dialectSpecificConfig the dialect-specific configuration + */ +@RecordBuilder +public record SqlGenerationConfig( + Dialect dialect, + MappingStrategy mappingStrategy, + DialectSpecificConfig dialectSpecificConfig +) implements GenerationConfig { + public enum Dialect { + DATABRICKS + } + + public enum MappingStrategy { + DENORMALIZED + } + + public interface DialectSpecificConfig extends GenerationConfig { + } + + public static class DefaultDialectSpecificConfig implements DialectSpecificConfig { + } + + public SqlGenerationConfig { + if ( dialect == null ) { + dialect = Dialect.DATABRICKS; + } + + if ( mappingStrategy == null ) { + mappingStrategy = MappingStrategy.DENORMALIZED; + } + + if ( dialectSpecificConfig == null ) { + dialectSpecificConfig = new DefaultDialectSpecificConfig(); + } + } +} diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/AspectModelDatabricksDenormalizedSqlVisitor.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/AspectModelDatabricksDenormalizedSqlVisitor.java new file mode 100644 index 000000000..b8e34d6e5 --- /dev/null +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/AspectModelDatabricksDenormalizedSqlVisitor.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel.generator.sql.databricks; + +import static org.eclipse.esmf.aspectmodel.generator.sql.databricks.AspectModelDatabricksDenormalizedSqlVisitorContextBuilder.builder; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import org.eclipse.esmf.aspectmodel.generator.AbstractGenerator; +import org.eclipse.esmf.aspectmodel.vocabulary.SAMM; +import org.eclipse.esmf.characteristic.Collection; +import org.eclipse.esmf.characteristic.Either; +import org.eclipse.esmf.characteristic.Trait; +import org.eclipse.esmf.metamodel.Aspect; +import org.eclipse.esmf.metamodel.Characteristic; +import org.eclipse.esmf.metamodel.ComplexType; +import org.eclipse.esmf.metamodel.Entity; +import org.eclipse.esmf.metamodel.ModelElement; +import org.eclipse.esmf.metamodel.NamedElement; +import org.eclipse.esmf.metamodel.Property; +import org.eclipse.esmf.metamodel.Scalar; +import org.eclipse.esmf.metamodel.StructureElement; +import org.eclipse.esmf.metamodel.Type; +import org.eclipse.esmf.metamodel.visitor.AspectVisitor; +import org.eclipse.esmf.samm.KnownVersion; + +import com.google.common.base.CaseFormat; +import com.google.common.collect.ImmutableMap; +import io.soabase.recordbuilder.core.RecordBuilder; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.XSD; + +/** + * Generates Databricks SQL with a denormalized schema from an Aspect Model. + */ +public class AspectModelDatabricksDenormalizedSqlVisitor + implements AspectVisitor { + private static final String LEVEL_DELIMITER = "__"; + private static final int MAX_RECURSION_DEPTH = 20; + private final DatabricksSqlGenerationConfig config; + private final Map databricksTypeMap; + + @RecordBuilder + public record Context( + String prefix, + Property currentProperty, + boolean forceOptional, + NamedElement forceDescriptionFromElement, + Map recursionDepth + ) { + public Context { + if ( prefix == null ) { + prefix = ""; + } + if ( recursionDepth == null ) { + recursionDepth = new HashMap<>(); + } + } + + public AspectModelDatabricksDenormalizedSqlVisitorContextBuilder copy() { + return builder() + .prefix( prefix() ) + .currentProperty( currentProperty() ) + .forceOptional( forceOptional() ) + .forceDescriptionFromElement( forceDescriptionFromElement() ) + .recursionDepth( recursionDepth() ); + } + } + + public AspectModelDatabricksDenormalizedSqlVisitor( final DatabricksSqlGenerationConfig config ) { + this.config = config; + databricksTypeMap = ImmutableMap. builder() + .put( XSD.xstring.getURI(), DatabricksType.STRING ) + .put( XSD.xboolean.getURI(), DatabricksType.BOOLEAN ) + .put( XSD.decimal.getURI(), new DatabricksType.DatabricksDecimal( Optional.of( config.decimalPrecision() ) ) ) + .put( XSD.integer.getURI(), new DatabricksType.DatabricksDecimal() ) + .put( XSD.xdouble.getURI(), DatabricksType.DOUBLE ) + .put( XSD.xfloat.getURI(), DatabricksType.FLOAT ) + .put( XSD.date.getURI(), DatabricksType.STRING ) + .put( XSD.time.getURI(), DatabricksType.STRING ) + .put( XSD.dateTime.getURI(), DatabricksType.STRING ) + .put( XSD.dateTimeStamp.getURI(), DatabricksType.TIMESTAMP ) + .put( XSD.gYear.getURI(), DatabricksType.STRING ) + .put( XSD.gMonth.getURI(), DatabricksType.STRING ) + .put( XSD.gDay.getURI(), DatabricksType.STRING ) + .put( XSD.gYearMonth.getURI(), DatabricksType.STRING ) + .put( XSD.gMonthDay.getURI(), DatabricksType.STRING ) + .put( XSD.duration.getURI(), DatabricksType.STRING ) + .put( XSD.yearMonthDuration.getURI(), DatabricksType.STRING ) + .put( XSD.dayTimeDuration.getURI(), DatabricksType.STRING ) + .put( XSD.xbyte.getURI(), DatabricksType.TINYINT ) + .put( XSD.xshort.getURI(), DatabricksType.SMALLINT ) + .put( XSD.xint.getURI(), DatabricksType.INT ) + .put( XSD.xlong.getURI(), DatabricksType.BIGINT ) + .put( XSD.unsignedByte.getURI(), DatabricksType.SMALLINT ) + .put( XSD.unsignedShort.getURI(), DatabricksType.INT ) + .put( XSD.unsignedInt.getURI(), DatabricksType.BIGINT ) + .put( XSD.unsignedLong.getURI(), new DatabricksType.DatabricksDecimal() ) + .put( XSD.positiveInteger.getURI(), new DatabricksType.DatabricksDecimal( Optional.of( config.decimalPrecision() ) ) ) + .put( XSD.nonNegativeInteger.getURI(), new DatabricksType.DatabricksDecimal( Optional.of( config.decimalPrecision() ) ) ) + .put( XSD.negativeInteger.getURI(), new DatabricksType.DatabricksDecimal( Optional.of( config.decimalPrecision() ) ) ) + .put( XSD.nonPositiveInteger.getURI(), new DatabricksType.DatabricksDecimal( Optional.of( config.decimalPrecision() ) ) ) + .put( XSD.hexBinary.getURI(), DatabricksType.BINARY ) + .put( XSD.base64Binary.getURI(), DatabricksType.BINARY ) + .put( XSD.anyURI.getURI(), DatabricksType.STRING ) + .put( new SAMM( KnownVersion.getLatest() ).resource( "curie" ).getURI(), DatabricksType.STRING ) + .put( RDF.langString.getURI(), DatabricksType.STRING ) + .build(); + } + + @Override + public String visitBase( final ModelElement modelElement, final Context context ) { + return ""; + } + + private String tableName( final Aspect aspect ) { + return CaseFormat.UPPER_CAMEL.to( CaseFormat.LOWER_UNDERSCORE, aspect.getName() ); + } + + private String columnName( final Property property ) { + return CaseFormat.LOWER_CAMEL.to( CaseFormat.LOWER_UNDERSCORE, property.getPayloadName() ); + } + + @Override + public String visitStructureElement( final StructureElement structureElement, final Context context ) { + final StringBuilder result = new StringBuilder(); + for ( final Property property : structureElement.getProperties() ) { + if ( property.isNotInPayload() ) { + continue; + } + final String propertyResult = property.accept( this, context ); + if ( !propertyResult.isBlank() ) { + if ( !result.isEmpty() ) { + result.append( ",\n" ); + } + if ( !propertyResult.startsWith( " " ) ) { + result.append( " " ); + } + result.append( propertyResult ); + } + } + return result.toString(); + } + + private String escapeComment( final String comment ) { + return comment.replace( "'", "\\'" ); + } + + @Override + public String visitAspect( final Aspect aspect, final Context context ) { + final String columnDeclarations = visitStructureElement( aspect, context ); + final String comment = config.includeTableComment() + ? Optional.ofNullable( aspect.getDescription( config.commentLanguage() ) ).map( description -> + "COMMENT '" + escapeComment( description ) + "'\n" ).orElse( "" ) + : ""; + return "%s %s (\n%s%s)\n%sTBLPROPERTIES ('%s'='%s');\n".formatted( + config.createTableCommandPrefix(), + tableName( aspect ), + columnDeclarations, + columnDeclarations.isEmpty() ? "" : "\n", + comment, + AbstractGenerator.SAMM_EXTENSION, + aspect.getAspectModelUrn().orElseThrow() + ); + } + + @Override + public String visitProperty( final Property property, final Context context ) { + context.recursionDepth().put( property, context.recursionDepth().getOrDefault( property, 0 ) + 1 ); + if ( property.getCharacteristic().isEmpty() ) { + return ""; + } + + return property.getCharacteristic().get().accept( this, context.copy() + .prefix( (context.prefix().isEmpty() ? "" : context.prefix() + LEVEL_DELIMITER) + columnName( property ) ) + .currentProperty( property ) + .build() ); + } + + @Override + public String visitEither( final Either either, final Context context ) { + final String leftResult = either.getLeft().accept( this, context.copy() + .prefix( context.prefix() + LEVEL_DELIMITER + "left" ) + .forceOptional( true ) + .forceDescriptionFromElement( either.getLeft() ) + .build() ); + final String rightResult = either.getRight().accept( this, context.copy() + .prefix( context.prefix() + LEVEL_DELIMITER + "right" ) + .forceOptional( true ) + .forceDescriptionFromElement( either.getRight() ) + .build() ); + return leftResult + "\n" + (rightResult.startsWith( " " ) ? "" : " ") + rightResult; + } + + @Override + public String visitCharacteristic( final Characteristic characteristic, final Context context ) { + final Property property = context.currentProperty(); + final Type type = characteristic.getDataType().orElseThrow(); + if ( type.isComplexType() ) { + // Break endless recursion + if ( context.recursionDepth().getOrDefault( property, 0 ) >= MAX_RECURSION_DEPTH ) { + return ""; + } + // If the property is optional but points to an Entity with mandatory properties, the columns for those + // properties still need to be optional (i.e. nullable), so we force optionality here. + final Context contextForComplexType = property.isOptional() + ? context.copy().forceOptional( true ).build() + : context; + return type.accept( this, contextForComplexType ); + } + + final Optional comment = config.includeColumnComments() + ? Optional.ofNullable( Optional.ofNullable( context.forceDescriptionFromElement() ).orElse( property ) + .getDescription( config.commentLanguage() ) ) + : Optional.empty(); + return column( context.prefix(), type.accept( this, context ), property.isOptional() || context.forceOptional(), comment ); + } + + private String column( final String columnName, final String columnType, final boolean isNullable, final Optional comment ) { + return "%s %s%s".formatted( columnName, columnType, isNullable ? "" : " NOT NULL" ) + + comment.map( args -> " COMMENT '%s'".formatted( escapeComment( args ) ) ).orElse( "" ); + } + + @Override + public String visitTrait( final Trait trait, final Context context ) { + return trait.getBaseCharacteristic().accept( this, context ); + } + + @Override + public String visitCollection( final Collection collection, final Context context ) { + final Property property = context.currentProperty(); + final Type type = collection.getDataType().orElseThrow(); + final Optional comment = config.includeColumnComments() + ? Optional.ofNullable( Optional.ofNullable( context.forceDescriptionFromElement() ).orElse( property ) + .getDescription( config.commentLanguage() ) ) + : Optional.empty(); + final String typeDef = type.isComplexType() + ? entityToStruct( type.as( ComplexType.class ) ).toString() + : type.accept( this, context ); + return column( context.prefix(), "ARRAY<" + typeDef + ">", property.isOptional() || context.forceOptional(), + comment ); + } + + private DatabricksType.DatabricksStruct entityToStruct( final ComplexType entity ) { + return new DatabricksType.DatabricksStruct( entity.getAllProperties().stream() + .flatMap( property -> { + if ( property.getDataType().isEmpty() || property.isNotInPayload() ) { + return Stream.empty(); + } + final Type type = property.getDataType().get(); + final DatabricksType databricksType; + if ( type instanceof final Scalar scalar ) { + databricksType = databricksTypeMap.get( scalar.getUrn() ); + } else if ( type instanceof final Entity entityType ) { + databricksType = entityToStruct( entityType ); + } else { + return Stream.empty(); + } + return Stream.of( new DatabricksType.DatabricksStructEntry( columnName( property ), databricksType, + !property.isOptional(), Optional.ofNullable( property.getDescription( config.commentLanguage() ) ) ) ); + } ) + .toList() ); + } + + @Override + public String visitScalar( final Scalar scalar, final Context context ) { + return databricksTypeMap.get( scalar.getUrn() ).toString(); + } + + @Override + public String visitEntity( final Entity entity, final Context context ) { + return visitStructureElement( entity, context ); + } +} diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksSqlGenerationConfig.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksSqlGenerationConfig.java new file mode 100644 index 000000000..3edf81340 --- /dev/null +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksSqlGenerationConfig.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel.generator.sql.databricks; + +import java.util.Locale; + +import org.eclipse.esmf.aspectmodel.generator.sql.SqlGenerationConfig; + +import io.soabase.recordbuilder.core.RecordBuilder; + +/** + * Configuration specific to the Databricks denormalized SQL generation. + * + * @param createTableCommandPrefix the command for table creation, default is "CREATE TABLE IF NOT EXISTS" + * @param includeTableComment whether to include a comment for the table + * @param includeColumnComments whether to include comments for the columns + * @param commentLanguage the language to use for comments + * @param decimalPrecision the precision to use for decimal columns, see DECIMAL type for more info. + */ +@RecordBuilder +public record DatabricksSqlGenerationConfig( + String createTableCommandPrefix, + boolean includeTableComment, + boolean includeColumnComments, + Locale commentLanguage, + int decimalPrecision +) implements SqlGenerationConfig.DialectSpecificConfig { + public static final String DEFAULT_TABLE_COMMAND_PREFIX = "CREATE TABLE IF NOT EXISTS"; + // As defined in https://docs.databricks.com/en/sql/language-manual/data-types/decimal-type.html + public static final int DECIMAL_DEFAULT_PRECISION = 10; + // As defined in https://docs.databricks.com/en/sql/language-manual/data-types/decimal-type.html + public static final int DECIMAL_MAX_PRECISION = 38; + public static final boolean DEFAULT_INCLUDE_TABLE_COMMENT = true; + public static final boolean DEFAULT_INCLUDE_COLUMN_COMMENTS = true; + public static final Locale DEFAULT_COMMENT_LANGUAGE = Locale.ENGLISH; + + public DatabricksSqlGenerationConfig() { + this( DEFAULT_TABLE_COMMAND_PREFIX, DEFAULT_INCLUDE_TABLE_COMMENT, DEFAULT_INCLUDE_COLUMN_COMMENTS, DEFAULT_COMMENT_LANGUAGE, + DECIMAL_DEFAULT_PRECISION ); + } + + public DatabricksSqlGenerationConfig { + if ( createTableCommandPrefix == null ) { + createTableCommandPrefix = DEFAULT_TABLE_COMMAND_PREFIX; + } + if ( decimalPrecision <= 0 ) { + decimalPrecision = DECIMAL_DEFAULT_PRECISION; + } + if ( decimalPrecision > DECIMAL_MAX_PRECISION ) { + decimalPrecision = DECIMAL_MAX_PRECISION; + } + if ( commentLanguage == null ) { + commentLanguage = DEFAULT_COMMENT_LANGUAGE; + } + } +} diff --git a/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksType.java b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksType.java new file mode 100644 index 000000000..8b4c63fc0 --- /dev/null +++ b/core/esmf-aspect-model-document-generators/src/main/java/org/eclipse/esmf/aspectmodel/generator/sql/databricks/DatabricksType.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel.generator.sql.databricks; + +import java.util.List; +import java.util.Optional; + +/** + * Databricks SQL types. + */ +public sealed interface DatabricksType { + DatabricksType BIGINT = new DatabricksBigint(); + DatabricksType BINARY = new DatabricksBinary(); + DatabricksType BOOLEAN = new DatabricksBoolean(); + DatabricksType DATE = new DatabricksDate(); + DatabricksType DOUBLE = new DatabricksDouble(); + DatabricksType FLOAT = new DatabricksFloat(); + DatabricksType INT = new DatabricksInt(); + DatabricksType SMALLINT = new DatabricksSmallint(); + DatabricksType STRING = new DatabricksString(); + DatabricksType TIMESTAMP = new DatabricksTimestamp(); + DatabricksType TIMESTAMP_NTZ = new DatabricksTimestampNtz(); + DatabricksType TINYINT = new DatabricksTinyint(); + + record DatabricksDecimal( Optional precision, Optional scale ) implements DatabricksType { + public DatabricksDecimal( final Optional precision ) { + this( precision, Optional.empty() ); + } + + public DatabricksDecimal() { + this( Optional.empty(), Optional.empty() ); + } + + @Override + public String toString() { + if ( precision.isPresent() ) { + if ( scale().isPresent() ) { + //noinspection OptionalGetWithoutIsPresent + return "DECIMAL(" + precision().get() + "," + scale().get() + ")"; + } + //noinspection OptionalGetWithoutIsPresent + return "DECIMAL(" + precision().get() + ")"; + } + return "DECIMAL"; + } + } + + record DatabricksArray( DatabricksType elementType ) implements DatabricksType { + @Override + public String toString() { + return "ARRAY<" + elementType() + ">"; + } + } + + record DatabricksMap( DatabricksType keyType, DatabricksType valueType ) implements DatabricksType { + public DatabricksMap { + if ( keyType instanceof DatabricksMap ) { + throw new RuntimeException( "Key type cannot be MAP" ); + } + } + + @Override + public String toString() { + return "MAP<" + keyType() + "," + valueType() + ">"; + } + } + + record DatabricksStructEntry( String name, DatabricksType type, boolean notNull, Optional comment ) { + @Override + public String toString() { + return name + ": " + type + (notNull ? " NOT NULL" : "") + comment.map( c -> " COMMENT '" + c + "'" ).orElse( "" ); + } + } + + record DatabricksStruct( List entries ) implements DatabricksType { + @Override + public String toString() { + return "STRUCT<" + String.join( ", ", entries.stream().map( DatabricksStructEntry::toString ).toList() ) + ">"; + } + } + + final class DatabricksBigint implements DatabricksType { + @Override + public String toString() { + return "BIGINT"; + } + } + + final class DatabricksBinary implements DatabricksType { + @Override + public String toString() { + return "BINARY"; + } + } + + final class DatabricksBoolean implements DatabricksType { + @Override + public String toString() { + return "BOOLEAN"; + } + } + + final class DatabricksDate implements DatabricksType { + @Override + public String toString() { + return "DATE"; + } + } + + final class DatabricksDouble implements DatabricksType { + @Override + public String toString() { + return "DOUBLE"; + } + } + + final class DatabricksFloat implements DatabricksType { + @Override + public String toString() { + return "FLOAT"; + } + } + + final class DatabricksInt implements DatabricksType { + @Override + public String toString() { + return "INT"; + } + } + + final class DatabricksSmallint implements DatabricksType { + @Override + public String toString() { + return "SMALLINT"; + } + } + + final class DatabricksString implements DatabricksType { + @Override + public String toString() { + return "STRING"; + } + } + + final class DatabricksTimestamp implements DatabricksType { + @Override + public String toString() { + return "TIMESTAMP"; + } + } + + final class DatabricksTimestampNtz implements DatabricksType { + @Override + public String toString() { + return "TIMESTAMP_NTZ"; + } + } + + final class DatabricksTinyint implements DatabricksType { + @Override + public String toString() { + return "TINYINT"; + } + } +} diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/jsonschema/AspectModelJsonSchemaGeneratorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/jsonschema/AspectModelJsonSchemaGeneratorTest.java index cd5969f66..d5c18b0dc 100644 --- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/jsonschema/AspectModelJsonSchemaGeneratorTest.java +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/jsonschema/AspectModelJsonSchemaGeneratorTest.java @@ -22,6 +22,7 @@ import java.util.Locale; import java.util.Map; +import org.eclipse.esmf.aspectmodel.generator.AbstractGenerator; import org.eclipse.esmf.aspectmodel.generator.json.AspectModelJsonPayloadGenerator; import org.eclipse.esmf.aspectmodel.resolver.services.VersionedModel; import org.eclipse.esmf.aspectmodel.vocabulary.SAMMC; @@ -216,7 +217,7 @@ public void testTypeMapping( final KnownVersion metaModelVersion ) { assertThat( context. read( "$['components']['schemas']['" + unitReference + "']['type']" ) ) .isEqualTo( "string" ); assertThat( context. read( - "$['components']['schemas']['" + unitReference + "']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['" + unitReference + "']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( sammc.UnitReference().getURI() ); characteristicReference = context. read( "$['properties']['dateProperty']['$ref']" ); @@ -231,7 +232,7 @@ public void testTypeMapping( final KnownVersion metaModelVersion ) { assertThat( context. read( "$['components']['schemas']['" + timestamp + "']['type']" ) ) .isEqualTo( "string" ); assertThat( context. read( - "$['components']['schemas']['" + timestamp + "']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['" + timestamp + "']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( sammc.Timestamp().getURI() ); characteristicReference = context. read( "$['properties']['dateTimeStampProperty']['$ref']" ); @@ -309,7 +310,7 @@ public void testTypeMapping( final KnownVersion metaModelVersion ) { assertThat( context. read( "$['components']['schemas']" + "['" + multiLanguageText + "']['type']" ) ).isEqualTo( "object" ); assertThat( context. read( - "$['components']['schemas']['" + multiLanguageText + "']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['" + multiLanguageText + "']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( sammc.MultiLanguageText().getURI() ); characteristicReference = context. read( "$['properties']['longProperty']['$ref']" ); @@ -347,7 +348,7 @@ public void testTypeMapping( final KnownVersion metaModelVersion ) { .isEqualTo( "#/components/schemas/" + text ); assertThat( context. read( "$['components']['schemas']['" + text + "']['type']" ) ).isEqualTo( "string" ); assertThat( context. read( - "$['components']['schemas']['" + text + "']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['" + text + "']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( sammc.Text().getURI() ); characteristicReference = context. read( "$['properties']['timeProperty']['$ref']" ); @@ -393,7 +394,7 @@ public void testOptionalPropertyMapping( final KnownVersion metaModelVersion ) { assertThat( context. read( "$['components']['schemas']['" + text + "']['type']" ) ).isEqualTo( "string" ); assertThat( context. read( - "$['components']['schemas']['" + text + "']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['" + text + "']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( sammc.Text().getURI() ); assertThat( context. read( "$['properties']['testProperty']['$ref']" ) ) .isEqualTo( "#/components/schemas/" + text ); @@ -411,7 +412,7 @@ public void testAspectWithRecursivePropertyWithOptional( final KnownVersion meta .isEqualTo( "#/components/schemas/TestItemCharacteristic" ); assertThat( context. read( - "$['components']['schemas']['TestItemCharacteristic']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['TestItemCharacteristic']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestItemCharacteristic" ); assertThat( context.> read( "$['components']['schemas']['TestItemCharacteristic']['required']" ) ).isNull(); assertThat( context.> read( "$['required']" ).stream().findFirst().get() ).isEqualTo( "testProperty" ); @@ -442,7 +443,7 @@ public void testCollectionWithElementConstraint( final KnownVersion metaModelVer assertThat( context. read( "$['components']['schemas']['TestCollection']['items']['type']" ) ) .isEqualTo( "number" ); assertThat( - context. read( "$['components']['schemas']['TestCollection']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + context. read( "$['components']['schemas']['TestCollection']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestCollection" ); assertThat( context. read( "$['components']['schemas']['TestCollection']['items']['minimum']" ) ) .isCloseTo( 2.3d, Percentage.withPercentage( 1.0d ) ); @@ -476,7 +477,7 @@ public void testEntityMapping( final KnownVersion metaModelVersion ) { assertThat( context. read( "$['components']['schemas']['TestEntity']['properties']['entityProperty']['$ref']" ) ) .isEqualTo( "#/components/schemas/" + text ); assertThat( context. read( - "$['components']['schemas']['" + text + "']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['" + text + "']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( sammc.Text().getURI() ); assertThat( context.> read( "$['components']['schemas']['TestEntity']['required']" ) ) .isEqualTo( List.of( "entityProperty" ) ); @@ -496,7 +497,7 @@ public void testLengthConstraintForStringMapping( final KnownVersion metaModelVe assertThat( context. read( "$['components']['schemas']['TestLengthConstraint']['type']" ) ).isEqualTo( "string" ); assertThat( context. read( - "$['components']['schemas']['TestLengthConstraint']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['TestLengthConstraint']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestLengthConstraint" ); assertThat( context. read( "$['components']['schemas']['TestLengthConstraint']['maxLength']" ) ).isEqualTo( 10 ); assertThat( context. read( "$['components']['schemas']['TestLengthConstraint']['minLength']" ) ).isEqualTo( 5 ); @@ -540,7 +541,7 @@ public void testLengthConstraintForListMapping( final KnownVersion metaModelVers .isEqualTo( "Test Length Constraint with collection" ); assertThat( context. read( - "$['components']['schemas']['TestLengthConstraintWithCollection']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "$['components']['schemas']['TestLengthConstraintWithCollection']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestLengthConstraintWithCollection" ); assertThat( context. read( "$['components']['schemas']['TestLengthConstraintWithCollection']['type']" ) ) @@ -572,7 +573,7 @@ public void testRangeConstraintMapping( final KnownVersion metaModelVersion ) { .isEqualTo( "This is a test range constraint." ); assertThat( context. read( "$['components']['schemas']['TestRangeConstraint']['type']" ) ).isEqualTo( "number" ); assertThat( context. read( - "$['components']['schemas']['TestRangeConstraint']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['TestRangeConstraint']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestRangeConstraint" ); assertThat( context. read( "$['components']['schemas']['TestRangeConstraint']['minimum']" ) ) .isCloseTo( 2.3d, Percentage.withPercentage( 1.0d ) ); @@ -595,7 +596,7 @@ public void testRangeConstraintOnConstrainedNumericType( final KnownVersion meta assertThat( context. read( "$['components']['schemas']['TestRangeConstraint']['type']" ) ).isEqualTo( "number" ); assertThat( context. read( - "$['components']['schemas']['TestRangeConstraint']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['TestRangeConstraint']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestRangeConstraint" ); assertThat( context. read( "$['components']['schemas']['TestRangeConstraint']['minimum']" ) ).isEqualTo( 5 ); assertThat( context. read( "$['components']['schemas']['TestRangeConstraint']['maximum']" ) ) @@ -614,7 +615,7 @@ public void testRangeConstraintWithBoundsMapping( final KnownVersion metaModelVe assertThat( context. read( "$['components']['schemas']['FloatRange']['description']" ) ) .isEqualTo( "This is a floating range constraint" ); assertThat( - context. read( "$['components']['schemas']['FloatRange']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + context. read( "$['components']['schemas']['FloatRange']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "FloatRange" ); assertThat( context. read( "$['components']['schemas']['FloatRange']['type']" ) ).isEqualTo( "number" ); assertThat( context. read( "$['components']['schemas']['FloatRange']['minimum']" ) ) @@ -629,7 +630,7 @@ context. read( "$['components']['schemas']['FloatRange']['" + AspectMode assertThat( context. read( "$['components']['schemas']['DoubleRange']['description']" ) ) .isEqualTo( "This is a double range constraint" ); assertThat( - context. read( "$['components']['schemas']['DoubleRange']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + context. read( "$['components']['schemas']['DoubleRange']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "DoubleRange" ); assertThat( context. read( "$['components']['schemas']['DoubleRange']['type']" ) ).isEqualTo( "number" ); assertThat( context. read( "$['components']['schemas']['DoubleRange']['minimum']" ) ) @@ -644,7 +645,7 @@ context. read( "$['components']['schemas']['DoubleRange']['" + AspectMod assertThat( context. read( "$['components']['schemas']['DecimalRange']['description']" ) ) .isEqualTo( "This is a decimal range constraint" ); assertThat( - context. read( "$['components']['schemas']['DecimalRange']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + context. read( "$['components']['schemas']['DecimalRange']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "DecimalRange" ); assertThat( context. read( "$['components']['schemas']['DecimalRange']['type']" ) ).isEqualTo( "number" ); assertThat( context. read( "$['components']['schemas']['DecimalRange']['minimum']" ) ) @@ -659,7 +660,7 @@ context. read( "$['components']['schemas']['DecimalRange']['" + AspectMo assertThat( context. read( "$['components']['schemas']['IntegerRange']['description']" ) ) .isEqualTo( "This is a integer range constraint" ); assertThat( - context. read( "$['components']['schemas']['IntegerRange']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + context. read( "$['components']['schemas']['IntegerRange']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "IntegerRange" ); assertThat( context. read( "$['components']['schemas']['IntegerRange']['type']" ) ).isEqualTo( "number" ); assertThat( context. read( "$['components']['schemas']['IntegerRange']['minimum']" ) ).isEqualTo( 12 ); @@ -669,7 +670,7 @@ context. read( "$['components']['schemas']['IntegerRange']['" + AspectMo assertThat( context. read( "$['properties']['intProp']['$ref']" ) ) .isEqualTo( "#/components/schemas/IntRange" ); - assertThat( context. read( "$['components']['schemas']['IntRange']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + assertThat( context. read( "$['components']['schemas']['IntRange']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "IntRange" ); assertThat( context. read( "$['components']['schemas']['IntRange']['type']" ) ).isEqualTo( "number" ); assertThat( context. read( "$['components']['schemas']['IntRange']['minimum']" ) ).isEqualTo( 12 ); @@ -706,7 +707,7 @@ public void testSetMapping() { assertThat( context. read( "$['properties']['testProperty']['description']" ) ).isEqualTo( "This is a test property." ); assertThat( context. read( "$['properties']['testProperty']['$ref']" ) ).isEqualTo( "#/components/schemas/TestSet" ); assertThat( context. read( "$['components']['schemas']['TestSet']['type']" ) ).isEqualTo( "array" ); - assertThat( context. read( "$['components']['schemas']['TestSet']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + assertThat( context. read( "$['components']['schemas']['TestSet']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestSet" ); assertThat( context. read( "$['components']['schemas']['TestSet']['description']" ) ).isEqualTo( "This is a test set." ); assertThat( context. read( "$['components']['schemas']['TestSet']['uniqueItems']" ) ).isTrue(); @@ -726,7 +727,7 @@ public void testSortedSetSetMapping() { .isEqualTo( "#/components/schemas/TestSortedSet" ); assertThat( context. read( "$['components']['schemas']['TestSortedSet']['type']" ) ).isEqualTo( "array" ); assertThat( - context. read( "$['components']['schemas']['TestSortedSet']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + context. read( "$['components']['schemas']['TestSortedSet']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestSortedSet" ); assertThat( context. read( "$['components']['schemas']['TestSortedSet']['description']" ) ) .isEqualTo( "This is a test sorted set." ); @@ -749,14 +750,14 @@ public void testLangStringMapping( final KnownVersion metaModelVersion ) { assertThat( context. read( "$['components']['schemas']['" + multiLanguageText + "']['type']" ) ) .isEqualTo( "object" ); assertThat( context. read( - "$['components']['schemas']['" + multiLanguageText + "']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['" + multiLanguageText + "']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( sammc.MultiLanguageText().getURI() ); assertThat( context. read( "$['components']['schemas']['" + multiLanguageText + "']['description']" ) ) .isEqualTo( "Describes a Property which contains plain text in multiple " + "languages. This is intended exclusively for human readable strings, not for " + "identifiers, measurement values, etc." ); assertThat( context. read( - "$['components']['schemas']['" + multiLanguageText + "']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['" + multiLanguageText + "']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( sammc.MultiLanguageText().getURI() ); assertThat( context. read( "$['components']['schemas']['" + multiLanguageText + "']['patternProperties']" + "['^.*$']['type']" ) ).isEqualTo( "string" ); @@ -782,7 +783,7 @@ public void testEitherMapping( final KnownVersion metaModelVersion ) { assertThat( context. read( "$['components']['schemas']['TestEither']['description']" ) ) .isEqualTo( "This is a test Either." ); assertThat( - context. read( "$['components']['schemas']['TestEither']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + context. read( "$['components']['schemas']['TestEither']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestEither" ); assertThat( context. read( "$['components']['schemas']['TestEither']['properties']['left']['type']" ) ) .isEqualTo( "string" ); @@ -818,7 +819,7 @@ public void testEnumScalarMapping( final KnownVersion metaModelVersion ) { assertThat( context. read( "$['components']['schemas']['TestEnumeration']['type']" ) ).isEqualTo( "number" ); assertThat( context. read( - "$['components']['schemas']['TestEnumeration']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['TestEnumeration']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestEnumeration" ); assertThat( context.> read( "$['components']['schemas']['TestEnumeration']['enum']" ) ) .containsExactly( 1, 2, 3 ); @@ -838,7 +839,7 @@ public void testEnumComplexMapping( final KnownVersion metaModelVersion ) { .isEqualTo( "Possible values for the evaluation of a process" ); assertThat( context. read( "$['components']['schemas']['EvaluationResults']['type']" ) ).isEqualTo( "object" ); assertThat( context. read( - "$['components']['schemas']['EvaluationResults']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['EvaluationResults']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "EvaluationResults" ); assertThat( context. read( "$['components']['schemas']['EvaluationResults']['oneOf'][0]['$ref']" ) ) .isEqualTo( "#/components/schemas/ResultGood" ); @@ -868,7 +869,7 @@ public void testEnumComplexWithNotInPayloadMapping( final KnownVersion metaModel .isEqualTo( "#/components/schemas/EvaluationResults" ); assertThat( context. read( - "$['components']['schemas']['EvaluationResults']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['EvaluationResults']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "EvaluationResults" ); assertThat( context. read( "$['components']['schemas']['ResultNoStatus']['properties']['average']" @@ -898,7 +899,7 @@ public void testEnumWithLangStringMapping( final KnownVersion metaModelVersion ) assertThat( context. read( "$['components']['schemas']['TestEnumeration']['description']" ) ) .isEqualTo( "This is a test for enumeration." ); assertThat( context. read( - "$['components']['schemas']['TestEnumeration']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['TestEnumeration']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestEnumeration" ); assertThat( context. read( "$['components']['schemas']['entityInstance']['type']" ) ) @@ -920,7 +921,7 @@ public void testRegularExpressionConstraintMapping( final KnownVersion metaModel .isEqualTo( "#/components/schemas/TestRegularExpressionConstraint" ); assertThat( context. read( - "$['components']['schemas']['TestRegularExpressionConstraint']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['TestRegularExpressionConstraint']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "TestRegularExpressionConstraint" ); assertThat( context. read( "$['components']['schemas']['TestRegularExpressionConstraint']['description']" ) ) .isEqualTo( "This is a test regular expression constraint." ); @@ -946,7 +947,7 @@ public void testComplexEntityCollectionEnum( final KnownVersion metaModelVersion assertThat( context. read( "$['components']['schemas']['MyEnumerationOne']['type']" ) ) .isEqualTo( "object" ); assertThat( context. read( - "$['components']['schemas']['MyEnumerationOne']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['MyEnumerationOne']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "MyEnumerationOne" ); assertThat( context. read( "$['components']['schemas']['MyEnumerationOne']['oneOf'][0]['$ref']" ) ) .isEqualTo( "#/components/schemas/entityInstanceOne" ); @@ -981,10 +982,10 @@ public void testAspectWithAbstractSingleEntity( final KnownVersion metaModelVers assertThat( context. read( "$['components']['schemas']['ExtendingTestEntity']['properties']['entityProperty']['$ref']" ) ) .isEqualTo( "#/components/schemas/" + text ); assertThat( context. read( - "$['components']['schemas']['ExtendingTestEntity']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['ExtendingTestEntity']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "ExtendingTestEntity" ); assertThat( context. read( - "$['components']['schemas']['" + text + "']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['" + text + "']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( sammc.Text().getURI() ); assertThat( context. read( "$['components']['schemas']['AbstractTestEntity']['description']" ) ) .isEqualTo( "This is an abstract test entity" ); @@ -1009,14 +1010,14 @@ public void testAspectWithAbstractEntity( final KnownVersion metaModelVersion ) assertThat( context. read( "$['components']['schemas']['ExtendingTestEntity']['description']" ) ) .isEqualTo( "This is a test entity" ); assertThat( context. read( - "$['components']['schemas']['ExtendingTestEntity']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['ExtendingTestEntity']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "ExtendingTestEntity" ); assertThat( context. read( "$['components']['schemas']['ExtendingTestEntity']['allOf'][0]['$ref']" ) ) .isEqualTo( "#/components/schemas/AbstractTestEntity" ); assertThat( context. read( "$['components']['schemas']['ExtendingTestEntity']['properties']['entityProperty']['$ref']" ) ) .isEqualTo( "#/components/schemas/" + text ); assertThat( context. read( - "$['components']['schemas']['" + text + "']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['" + text + "']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( sammc.Text().getURI() ); assertThat( context. read( "$['components']['schemas']['AbstractTestEntity']['description']" ) ) .isEqualTo( "This is a abstract test entity" ); @@ -1061,7 +1062,7 @@ public void testAspectWithCollectionWithAbstractEntity( final KnownVersion metaM assertThat( context. read( "$['components']['schemas']['AbstractTestEntity']['description']" ) ) .isEqualTo( "This is an abstract test entity" ); assertThat( context. read( - "$['components']['schemas']['AbstractTestEntity']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['AbstractTestEntity']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "AbstractTestEntity" ); assertThat( context. read( "$['components']['schemas']['ExtendingTestEntity']['properties']['entityProperty']['$ref']" ) ) .isEqualTo( "#/components/schemas/Text" ); @@ -1074,7 +1075,7 @@ context. read( "$['components']['schemas']['AbstractTestEntity']['proper assertThat( context. read( "$['components']['schemas']['EntityCollectionCharacteristic']['description']" ) ) .isEqualTo( "This is an entity collection characteristic" ); assertThat( context. read( - "$['components']['schemas']['EntityCollectionCharacteristic']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ) + "$['components']['schemas']['EntityCollectionCharacteristic']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ) .isEqualTo( TestModel.TEST_NAMESPACE + "EntityCollectionCharacteristic" ); assertThat( context. read( "$['components']['schemas']['EntityCollectionCharacteristic']['items']['$ref']" ) ) .isEqualTo( "#/components/schemas/AbstractTestEntity" ); diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java index 1b8c46009..ecd7fdc01 100644 --- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java @@ -28,7 +28,7 @@ import java.util.Map; import java.util.stream.Collectors; -import org.eclipse.esmf.aspectmodel.generator.jsonschema.AspectModelJsonSchemaVisitor; +import org.eclipse.esmf.aspectmodel.generator.AbstractGenerator; import org.eclipse.esmf.aspectmodel.resolver.services.VersionedModel; import org.eclipse.esmf.metamodel.Aspect; import org.eclipse.esmf.metamodel.Property; @@ -97,8 +97,8 @@ void testGeneration( final TestAspect testAspect ) throws IOException { final OpenApiSchemaArtifact result = apiJsonGenerator.apply( aspect, config ); final JsonNode json = result.getContent(); assertSpecificationIsValid( json, json.toString(), aspect ); - assertThat( json.get( "info" ).get( AspectModelJsonSchemaVisitor.SAMM_EXTENSION ) ).isNotNull(); - assertThat( json.get( "info" ).get( AspectModelJsonSchemaVisitor.SAMM_EXTENSION ).asText() ).isEqualTo( + assertThat( json.get( "info" ).get( AbstractGenerator.SAMM_EXTENSION ) ).isNotNull(); + assertThat( json.get( "info" ).get( AbstractGenerator.SAMM_EXTENSION ).asText() ).isEqualTo( aspect.getAspectModelUrn().map( Object::toString ).orElse( "" ) ); // Check that the map containing separate schema files contains the same information as the @@ -148,8 +148,8 @@ void testUseSemanticVersion( final KnownVersion metaModelVersion ) { final OpenAPI openApi = result.getOpenAPI(); assertThat( openApi.getInfo().getVersion() ).isEqualTo( "v1.0.0" ); - assertThat( json.get( "info" ).get( AspectModelJsonSchemaVisitor.SAMM_EXTENSION ) ).isNotNull(); - assertThat( json.get( "info" ).get( AspectModelJsonSchemaVisitor.SAMM_EXTENSION ).asText() ).isEqualTo( + assertThat( json.get( "info" ).get( AbstractGenerator.SAMM_EXTENSION ) ).isNotNull(); + assertThat( json.get( "info" ).get( AbstractGenerator.SAMM_EXTENSION ).asText() ).isEqualTo( aspect.getAspectModelUrn().map( Object::toString ).orElse( "" ) ); openApi.getServers().forEach( server -> assertThat( server.getUrl() ).contains( "v1.0.0" ) ); @@ -675,13 +675,13 @@ private void assertSpecificationIsValid( final JsonNode jsonNode, final String j final DocumentContext context = JsonPath.parse( json ); assertThat( context. read( "$['components']['schemas']['" + aspect.getName() + "']" ) ).isNotNull(); assertThat( context. read( - "$['components']['schemas']['" + aspect.getName() + "']['" + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + "']" ) ).isEqualTo( + "$['components']['schemas']['" + aspect.getName() + "']['" + AbstractGenerator.SAMM_EXTENSION + "']" ) ).isEqualTo( aspect.getAspectModelUrn().get().toString() ); for ( final Property property : aspect.getProperties() ) { assertThat( context. read( "$['components']['schemas']" + "['" + aspect.getName() + "']['properties']['" + property.getPayloadName() + "']['" - + AspectModelJsonSchemaVisitor.SAMM_EXTENSION + + AbstractGenerator.SAMM_EXTENSION + "']" ) ).isEqualTo( property.getAspectModelUrn().get().toString() ); } @@ -707,8 +707,8 @@ private void validateOpenApiSpec( final JsonNode node, final OpenAPI openApi, fi final String expectedApiVersion = getExpectedApiVersion( aspect ); assertThat( openApi.getInfo().getVersion() ).isEqualTo( expectedApiVersion ); - assertThat( node.get( "info" ).get( AspectModelJsonSchemaVisitor.SAMM_EXTENSION ) ).isNotNull(); - assertThat( node.get( "info" ).get( AspectModelJsonSchemaVisitor.SAMM_EXTENSION ).asText() ).isEqualTo( + assertThat( node.get( "info" ).get( AbstractGenerator.SAMM_EXTENSION ) ).isNotNull(); + assertThat( node.get( "info" ).get( AbstractGenerator.SAMM_EXTENSION ).asText() ).isEqualTo( aspect.getAspectModelUrn().map( Object::toString ).orElse( "" ) ); assertThat( openApi.getServers() ).hasSize( 1 ); @@ -726,9 +726,9 @@ private void validateOpenApiSpec( final JsonNode node, final OpenAPI openApi, fi assertThat( openApi.getComponents().getResponses() ).containsKey( aspect.getName() ); assertThat( openApi.getComponents().getRequestBodies() ).containsKey( aspect.getName() ); assertThat( openApi.getComponents().getSchemas().get( aspect.getName() ).getExtensions() - .get( AspectModelJsonSchemaVisitor.SAMM_EXTENSION ) ).isNotNull(); + .get( AbstractGenerator.SAMM_EXTENSION ) ).isNotNull(); assertThat( - openApi.getComponents().getSchemas().get( aspect.getName() ).getExtensions().get( AspectModelJsonSchemaVisitor.SAMM_EXTENSION ) + openApi.getComponents().getSchemas().get( aspect.getName() ).getExtensions().get( AbstractGenerator.SAMM_EXTENSION ) .toString() ).contains( aspect.getAspectModelUrn().map( Object::toString ).orElse( "" ) ); diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/AspectModelDatabricksDenormalizedSqlVisitorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/AspectModelDatabricksDenormalizedSqlVisitorTest.java new file mode 100644 index 000000000..652bc6181 --- /dev/null +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/AspectModelDatabricksDenormalizedSqlVisitorTest.java @@ -0,0 +1,510 @@ +/* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel.generator.sql; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Locale; + +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.AspectModelDatabricksDenormalizedSqlVisitor; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.AspectModelDatabricksDenormalizedSqlVisitorContextBuilder; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfig; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfigBuilder; +import org.eclipse.esmf.aspectmodel.resolver.services.VersionedModel; +import org.eclipse.esmf.metamodel.Aspect; +import org.eclipse.esmf.metamodel.loader.AspectModelLoader; +import org.eclipse.esmf.samm.KnownVersion; +import org.eclipse.esmf.test.TestAspect; +import org.eclipse.esmf.test.TestResources; + +import org.junit.jupiter.api.Test; + +@SuppressWarnings( "checkstyle:LineLength" ) +public class AspectModelDatabricksDenormalizedSqlVisitorTest { + private String sql( final TestAspect testAspect, final DatabricksSqlGenerationConfig config ) { + final VersionedModel versionedModel = TestResources.getModel( testAspect, KnownVersion.getLatest() ).get(); + final Aspect aspect = AspectModelLoader.getSingleAspect( versionedModel ).getOrElseThrow( () -> new RuntimeException() ); + return aspect.accept( new AspectModelDatabricksDenormalizedSqlVisitor( config ), + AspectModelDatabricksDenormalizedSqlVisitorContextBuilder.builder().build() ); + } + + private String sql( final TestAspect testAspect ) { + final DatabricksSqlGenerationConfig config = new DatabricksSqlGenerationConfig(); + return sql( testAspect, config ); + } + + @Test + void testAspectWithAbstractEntity() { + assertThat( sql( TestAspect.ASPECT_WITH_ABSTRACT_ENTITY ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_abstract_entity ( + test_property__entity_property STRING NOT NULL COMMENT 'This is a property for the test entity.' + ) + COMMENT 'This is a test description' + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithAbstractEntity'); + """ ); + } + + @Test + void testAspectWithAllBaseAttributes() { + assertThat( sql( TestAspect.ASPECT_WITH_ALL_BASE_ATTRIBUTES ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_all_base_attributes ( + test_boolean BOOLEAN NOT NULL + ) + COMMENT 'Test Description' + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithAllBaseAttributes'); + """ ); + } + + @Test + void testAspectWithCollection() { + assertThat( sql( TestAspect.ASPECT_WITH_COLLECTION ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_collection ( + test_property ARRAY NOT NULL COMMENT 'This is a test property.' + ) + COMMENT 'This is a test description' + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithCollection'); + """ ); + } + + @Test + void testAspectWithCollectionsWithElementCharacteristicAndSimpleDataType() { + assertThat( sql( TestAspect.ASPECT_WITH_COLLECTIONS_WITH_ELEMENT_CHARACTERISTIC_AND_SIMPLE_DATA_TYPE ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_collections_with_element_characteristic_and_simple_data_type ( + test_property ARRAY NOT NULL, + test_property_two ARRAY NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithCollectionsWithElementCharacteristicAndSimpleDataType'); + """ ); + } + + @Test + void testAspectWithCollectionAndElementCharacteristic() { + assertThat( sql( TestAspect.ASPECT_WITH_COLLECTION_AND_ELEMENT_CHARACTERISTIC ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_collection_and_element_characteristic ( + items ARRAY> NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithCollectionAndElementCharacteristic'); + """ ); + } + + @Test + void testAspectWithCollectionOfSimpleType() { + assertThat( sql( TestAspect.ASPECT_WITH_COLLECTION_OF_SIMPLE_TYPE ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_collection_of_simple_type ( + test_list ARRAY NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithCollectionOfSimpleType'); + """ ); + } + + @Test + void testAspectWithComplexCollectionEnum() { + assertThat( sql( TestAspect.ASPECT_WITH_COMPLEX_COLLECTION_ENUM ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_complex_collection_enum ( + my_property_one__entity_property_one ARRAY NOT NULL, + my_property_two__entity_property_two ARRAY NOT NULL, + my_property_three__entity_property_three ARRAY NOT NULL, + my_property_four__entity_property_four ARRAY NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithComplexCollectionEnum'); + """ ); + } + + @Test + void testAspectWithComplexEntityCollectionEnum() { + assertThat( sql( TestAspect.ASPECT_WITH_COMPLEX_ENTITY_COLLECTION_ENUM ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_complex_entity_collection_enum ( + my_property_one__entity_property_one ARRAY> NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithComplexEntityCollectionEnum'); + """ ); + } + + @Test + void testAspectWithComplexEnumInclOptional() { + assertThat( sql( TestAspect.ASPECT_WITH_COMPLEX_ENUM_INCL_OPTIONAL ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_complex_enum_incl_optional ( + result__numeric_code SMALLINT COMMENT 'Numeric code for the evaluation result', + result__description STRING COMMENT 'Human-readable description of the process result code', + simple_result STRING NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithComplexEnumInclOptional'); + """ ); + } + + @Test + void testAspectWithCurie() { + assertThat( sql( TestAspect.ASPECT_WITH_CURIE ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_curie ( + test_curie STRING NOT NULL, + test_curie_without_example_value STRING NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithCurie'); + """ ); + } + + @Test + void testAspectWithEither() { + assertThat( sql( TestAspect.ASPECT_WITH_EITHER ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_either ( + test_property__left STRING COMMENT 'Describes a Property which contains plain text. This is intended exclusively for human readable strings, not for identifiers, measurement values, etc.' + test_property__right BOOLEAN COMMENT 'Represents a boolean value (i.e. a "flag").' + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEither'); + """ ); + } + + @Test + void testAspectWithEitherWithComplexTypes() { + assertThat( sql( TestAspect.ASPECT_WITH_EITHER_WITH_COMPLEX_TYPES ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_either_with_complex_types ( + test_property__left__result STRING COMMENT 'Left Type Characteristic' + test_property__right__error STRING COMMENT 'Right Type Characteristic' + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEitherWithComplexTypes'); + """ ); + } + + @Test + void testAspectWithEnglishAndGermanDescription() { + final DatabricksSqlGenerationConfig config = DatabricksSqlGenerationConfigBuilder.builder() + .includeTableComment( true ) + .includeColumnComments( true ) + .commentLanguage( Locale.GERMAN ).build(); + assertThat( sql( TestAspect.ASPECT_WITH_ENGLISH_AND_GERMAN_DESCRIPTION, config ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_english_and_german_description ( + test_string STRING NOT NULL COMMENT 'Es ist ein Test-String' + ) + COMMENT 'Aspekt mit mehrsprachigen Beschreibungen' + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEnglishAndGermanDescription'); + """ ); + } + + @Test + void testAspectWithEntity() { + assertThat( sql( TestAspect.ASPECT_WITH_ENTITY ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_entity ( + test_property__entity_property STRING NOT NULL COMMENT 'This is a property for the test entity.' + ) + COMMENT 'This is a test description' + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntity'); + """ ); + } + + @Test + void testAspectWithEntityInstanceWithScalarListProperty() { + assertThat( sql( TestAspect.ASPECT_WITH_ENTITY_INSTANCE_WITH_SCALAR_LIST_PROPERTY ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_entity_instance_with_scalar_list_property ( + test_property__code SMALLINT NOT NULL, + test_property__test_list ARRAY NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntityInstanceWithScalarListProperty'); + """ ); + } + + @Test + void testAspectWithEntityList() { + assertThat( sql( TestAspect.ASPECT_WITH_ENTITY_LIST ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_entity_list ( + test_list ARRAY> NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntityList'); + """ ); + } + + @Test + void testAspectWithEntityWithNestedEntityListProperty() { + assertThat( sql( TestAspect.ASPECT_WITH_ENTITY_WITH_NESTED_ENTITY_LIST_PROPERTY ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_entity_with_nested_entity_list_property ( + test_property__code SMALLINT NOT NULL, + test_property__test_list ARRAY> NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithEntityWithNestedEntityListProperty'); + """ ); + } + + @Test + void testAspectWithExtendedEntity() { + assertThat( sql( TestAspect.ASPECT_WITH_EXTENDED_ENTITY ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_extended_entity ( + test_property ARRAY> NOT NULL COMMENT 'This is a test property.' + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithExtendedEntity'); + """ ); + } + + @Test + void testAspectWithList() { + assertThat( sql( TestAspect.ASPECT_WITH_LIST ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_list ( + test_property ARRAY NOT NULL COMMENT 'This is a test property.' + ) + COMMENT 'This is a test description' + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithList'); + """ ); + } + + @Test + void testAspectWithMultipleEntities() { + assertThat( sql( TestAspect.ASPECT_WITH_MULTIPLE_ENTITIES ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_multiple_entities ( + test_entity_one__test_string STRING NOT NULL, + test_entity_one__test_int INT NOT NULL, + test_entity_one__test_float FLOAT NOT NULL, + test_entity_one__test_local_date_time TIMESTAMP NOT NULL, + test_entity_one__random_value STRING NOT NULL, + test_entity_two__test_string STRING NOT NULL, + test_entity_two__test_int INT NOT NULL, + test_entity_two__test_float FLOAT NOT NULL, + test_entity_two__test_local_date_time TIMESTAMP NOT NULL, + test_entity_two__random_value STRING NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithMultipleEntities'); + """ ); + } + + @Test + void testAspectWithMultipleEntitiesAndEither() { + assertThat( sql( TestAspect.ASPECT_WITH_MULTIPLE_ENTITIES_AND_EITHER ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_multiple_entities_and_either ( + test_entity_one__test_string STRING NOT NULL, + test_entity_one__test_int INT NOT NULL, + test_entity_one__test_float FLOAT NOT NULL, + test_entity_one__test_local_date_time TIMESTAMP NOT NULL, + test_entity_one__random_value STRING NOT NULL, + test_entity_two__test_string STRING NOT NULL, + test_entity_two__test_int INT NOT NULL, + test_entity_two__test_float FLOAT NOT NULL, + test_entity_two__test_local_date_time TIMESTAMP NOT NULL, + test_entity_two__random_value STRING NOT NULL, + test_either_property__left__test_string STRING COMMENT 'Left type Characteristic', + test_either_property__left__test_int INT COMMENT 'Left type Characteristic', + test_either_property__left__test_float FLOAT COMMENT 'Left type Characteristic', + test_either_property__left__test_local_date_time TIMESTAMP COMMENT 'Left type Characteristic', + test_either_property__left__random_value STRING COMMENT 'Left type Characteristic' + test_either_property__right__test_string STRING COMMENT 'Right type Characteristic', + test_either_property__right__test_int INT COMMENT 'Right type Characteristic', + test_either_property__right__test_float FLOAT COMMENT 'Right type Characteristic', + test_either_property__right__test_local_date_time TIMESTAMP COMMENT 'Right type Characteristic', + test_either_property__right__random_value STRING COMMENT 'Right type Characteristic' + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithMultipleEntitiesAndEither'); + """ ); + } + + @Test + void testAspectWithMultipleEntitiesOnMultipleLevels() { + assertThat( sql( TestAspect.ASPECT_WITH_MULTIPLE_ENTITIES_ON_MULTIPLE_LEVELS ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_multiple_entities_on_multiple_levels ( + test_entity_one__test_local_date_time STRING NOT NULL, + test_entity_one__random_value STRING NOT NULL, + test_entity_one__test_third_entity__test_string STRING NOT NULL, + test_entity_one__test_third_entity__test_float FLOAT NOT NULL, + test_entity_two__test_local_date_time STRING NOT NULL, + test_entity_two__random_value STRING NOT NULL, + test_entity_two__test_third_entity__test_string STRING NOT NULL, + test_entity_two__test_third_entity__test_float FLOAT NOT NULL, + test_string STRING NOT NULL, + test_second_entity__test_int INT NOT NULL, + test_second_entity__test_float FLOAT NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithMultipleEntitiesOnMultipleLevels'); + """ ); + } + + @Test + void testAspectWithMultipleEntityCollections() { + assertThat( sql( TestAspect.ASPECT_WITH_MULTIPLE_ENTITY_COLLECTIONS ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_multiple_entity_collections ( + test_list_one ARRAY> NOT NULL, + test_list_two ARRAY> NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithMultipleEntityCollections'); + """ ); + } + + @Test + void testAspectWithNestedEntityList() { + assertThat( sql( TestAspect.ASPECT_WITH_NESTED_ENTITY_LIST ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_nested_entity_list ( + test_list ARRAY NOT NULL>> NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithNestedEntityList'); + """ ); + } + + @Test + void testAspectWithNestedEntityEnumerationWithNotInPayload() { + assertThat( sql( TestAspect.ASPECT_WITH_NESTED_ENTITY_ENUMERATION_WITH_NOT_IN_PAYLOAD ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_nested_entity_enumeration_with_not_in_payload ( + test_property__entity_property STRING NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithNestedEntityEnumerationWithNotInPayload'); + """ ); + } + + @Test + void testAspectWithOperation() { + assertThat( sql( TestAspect.ASPECT_WITH_OPERATION ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_operation ( + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithOperation'); + """ ); + } + + @Test + void testAspectWithOptionalProperties() { + assertThat( sql( TestAspect.ASPECT_WITH_OPTIONAL_PROPERTIES ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_optional_properties ( + number_property DECIMAL, + timestamp_property STRING NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithOptionalProperties'); + """ ); + } + + @Test + void testAspectWithOptionalPropertiesWithEntity() { + assertThat( sql( TestAspect.ASPECT_WITH_OPTIONAL_PROPERTIES_WITH_ENTITY ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_optional_properties_with_entity ( + test_string STRING NOT NULL, + test_optional_string STRING, + test_optional_entity__code_property INT, + test_optional_entity__test_second_string STRING, + test_optional_entity__test_int_list ARRAY + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithOptionalPropertiesWithEntity'); + """ ); + } + + @Test + void testAspectWithPropertyWithPayloadName() { + assertThat( sql( TestAspect.ASPECT_WITH_PROPERTY_WITH_PAYLOAD_NAME ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_property_with_payload_name ( + test STRING NOT NULL COMMENT 'This is a test property.' + ) + COMMENT 'This is a test description' + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithPropertyWithPayloadName'); + """ ); + } + + @Test + void testAspectWithRecursivePropertyWithOptional() { + assertThat( sql( TestAspect.ASPECT_WITH_RECURSIVE_PROPERTY_WITH_OPTIONAL_AND_ENTITY_PROPERTY ) ) + .contains( "test_property__test_property__test_property__test_property" ) + .contains( "test_property__test_property2" ); + } + + @Test + void testAspectWithScriptTags() { + assertThat( sql( TestAspect.ASPECT_WITH_SCRIPT_TAGS ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_script_tags ( + test_entity__test_string STRING NOT NULL COMMENT 'Test description with script: ', + test_entity__test_int INT NOT NULL COMMENT 'Test description with script: ', + test_entity__test_float FLOAT NOT NULL COMMENT 'Test description with script: ', + test_entity__test_local_date_time TIMESTAMP NOT NULL, + test_entity__random_value STRING NOT NULL COMMENT 'Test description with script: ' + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithScriptTags'); + """ ); + } + + @Test + void testAspectWithSet() { + assertThat( sql( TestAspect.ASPECT_WITH_SET ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_set ( + test_property ARRAY NOT NULL COMMENT 'This is a test property.' + ) + COMMENT 'This is a test description' + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithSet'); + """ ); + } + + @Test + void testAspectWithSimpleTypes() { + assertThat( sql( TestAspect.ASPECT_WITH_SIMPLE_TYPES ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_simple_types ( + any_uri_property STRING NOT NULL, + base64_binary_property BINARY NOT NULL, + boolean_property BOOLEAN NOT NULL, + byte_property TINYINT NOT NULL, + curie_property STRING NOT NULL, + date_property STRING NOT NULL, + date_time_property STRING NOT NULL, + date_time_stamp_property TIMESTAMP NOT NULL, + day_time_duration STRING NOT NULL, + decimal_property DECIMAL(10) NOT NULL, + double_property DOUBLE NOT NULL, + duration_property STRING NOT NULL, + float_property FLOAT NOT NULL, + g_day_property STRING NOT NULL, + g_month_day_property STRING NOT NULL, + g_month_property STRING NOT NULL, + g_year_month_property STRING NOT NULL, + g_year_property STRING NOT NULL, + hex_binary_property BINARY NOT NULL, + int_property INT NOT NULL, + integer_property DECIMAL NOT NULL, + lang_string_property STRING NOT NULL, + long_property BIGINT NOT NULL, + negative_integer_property DECIMAL(10) NOT NULL, + non_negative_integer_property DECIMAL(10) NOT NULL, + non_positive_integer DECIMAL(10) NOT NULL, + positive_integer_property DECIMAL(10) NOT NULL, + short_property SMALLINT NOT NULL, + string_property STRING NOT NULL, + time_property STRING NOT NULL, + unsigned_byte_property SMALLINT NOT NULL, + unsigned_int_property BIGINT NOT NULL, + unsigned_long_property DECIMAL NOT NULL, + unsigned_short_property INT NOT NULL, + year_month_duration_property STRING NOT NULL + ) + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithSimpleTypes'); + """ ); + } + + @Test + void testAspectWithSortedSet() { + assertThat( sql( TestAspect.ASPECT_WITH_SORTED_SET ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_sorted_set ( + test_property ARRAY NOT NULL COMMENT 'This is a test property.' + ) + COMMENT 'This is a test description' + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithSortedSet'); + """ ); + } + + @Test + void testAspectWithTimeSeries() { + assertThat( sql( TestAspect.ASPECT_WITH_TIME_SERIES ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_time_series ( + test_property ARRAY> NOT NULL COMMENT 'This is a test property.' + ) + COMMENT 'This is a test description' + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithTimeSeries'); + """ ); + } + + @Test + void testAspectWithComplexSet() { + assertThat( sql( TestAspect.ASPECT_WITH_COMPLEX_SET ) ).isEqualTo( """ + CREATE TABLE IF NOT EXISTS aspect_with_complex_set ( + test_property ARRAY> NOT NULL COMMENT 'This is a test property.' + ) + COMMENT 'This is a test description' + TBLPROPERTIES ('x-samm-aspect-model-urn'='urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithComplexSet'); + """ ); + } +} diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/AspectModelSqlGeneratorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/AspectModelSqlGeneratorTest.java new file mode 100644 index 000000000..8876b8f96 --- /dev/null +++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/sql/AspectModelSqlGeneratorTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel.generator.sql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.util.Locale; + +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfig; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfigBuilder; +import org.eclipse.esmf.aspectmodel.resolver.services.VersionedModel; +import org.eclipse.esmf.metamodel.Aspect; +import org.eclipse.esmf.metamodel.loader.AspectModelLoader; +import org.eclipse.esmf.samm.KnownVersion; +import org.eclipse.esmf.test.TestAspect; +import org.eclipse.esmf.test.TestResources; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class AspectModelSqlGeneratorTest { + @ParameterizedTest + @EnumSource( value = TestAspect.class ) + void testDatabricksGeneration( final TestAspect testAspect ) { + final VersionedModel versionedModel = TestResources.getModel( testAspect, KnownVersion.getLatest() ).get(); + final Aspect aspect = AspectModelLoader.getSingleAspect( versionedModel ).getOrElseThrow( () -> new RuntimeException() ); + assertThatCode( () -> { + final DatabricksSqlGenerationConfig dialectSpecificConfig = DatabricksSqlGenerationConfigBuilder.builder() + .includeTableComment( true ) + .includeColumnComments( true ) + .commentLanguage( Locale.ENGLISH ) + .build(); + final SqlArtifact sqlArtifact = AspectModelSqlGenerator.INSTANCE.apply( aspect, SqlGenerationConfigBuilder.builder() + .dialect( SqlGenerationConfig.Dialect.DATABRICKS ) + .dialectSpecificConfig( dialectSpecificConfig ) + .build() ); + final String result = sqlArtifact.getContent(); + + assertThat( result ).contains( "TBLPROPERTIES ('x-samm-aspect-model-urn'='" ); + assertThat( result ).doesNotContain( "ARRAY . +@prefix samm: . +@prefix unit: . +@prefix samm-c: . +@prefix samm-e: . +@prefix : . + +:AspectWithRecursivePropertyWithOptionalAndEntityProperty a samm:Aspect; + samm:properties ( :testProperty ) ; + samm:operations ( ) . + +:testProperty a samm:Property ; + samm:characteristic :TestItemCharacteristic . + +:TestItemCharacteristic a samm-c:SingleEntity ; + samm:dataType :TestEntity . + +:TestEntity a samm:Entity ; + samm:properties ( [ + samm:property :testProperty ; + samm:optional "true"^^xsd:boolean ; + ] + :testProperty2 ). + +:testProperty2 a samm:Property ; + samm:characteristic samm-c:Text . diff --git a/core/esmf-test-aspect-models/src/main/resources/valid/samm_2_1_0/org.eclipse.esmf.test/1.0.0/AspectWithCollectionWithAbstractEntity.ttl b/core/esmf-test-aspect-models/src/main/resources/valid/samm_2_1_0/org.eclipse.esmf.test/1.0.0/AspectWithCollectionWithAbstractEntity.ttl index d26dc9488..c530912c6 100644 --- a/core/esmf-test-aspect-models/src/main/resources/valid/samm_2_1_0/org.eclipse.esmf.test/1.0.0/AspectWithCollectionWithAbstractEntity.ttl +++ b/core/esmf-test-aspect-models/src/main/resources/valid/samm_2_1_0/org.eclipse.esmf.test/1.0.0/AspectWithCollectionWithAbstractEntity.ttl @@ -21,7 +21,7 @@ :testProperty a samm:Property ; samm:characteristic :EntityCollectionCharacteristic ; - samm:description "This is an test property"@en . + samm:description "This is a test property"@en . :EntityCollectionCharacteristic a samm-c:Collection ; samm:dataType :AbstractTestEntity ; diff --git a/core/esmf-test-aspect-models/src/main/resources/valid/samm_2_1_0/org.eclipse.esmf.test/1.0.0/AspectWithRecursivePropertyWithOptionalAndEntityProperty.ttl b/core/esmf-test-aspect-models/src/main/resources/valid/samm_2_1_0/org.eclipse.esmf.test/1.0.0/AspectWithRecursivePropertyWithOptionalAndEntityProperty.ttl new file mode 100644 index 000000000..0d1e44bae --- /dev/null +++ b/core/esmf-test-aspect-models/src/main/resources/valid/samm_2_1_0/org.eclipse.esmf.test/1.0.0/AspectWithRecursivePropertyWithOptionalAndEntityProperty.ttl @@ -0,0 +1,37 @@ +# Copyright (c) 2023 Robert Bosch Manufacturing Solutions GmbH +# +# See the AUTHORS file(s) distributed with this work for additional +# information regarding authorship. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 + +@prefix xsd: . +@prefix samm: . +@prefix unit: . +@prefix samm-c: . +@prefix samm-e: . +@prefix : . + +:AspectWithRecursivePropertyWithOptionalAndEntityProperty a samm:Aspect; + samm:properties ( :testProperty ) ; + samm:operations ( ) . + +:testProperty a samm:Property ; + samm:characteristic :TestItemCharacteristic . + +:TestItemCharacteristic a samm-c:SingleEntity ; + samm:dataType :TestEntity . + +:TestEntity a samm:Entity ; + samm:properties ( [ + samm:property :testProperty ; + samm:optional "true"^^xsd:boolean ; + ] + :testProperty2 ). + +:testProperty2 a samm:Property ; + samm:characteristic samm-c:Text . diff --git a/documentation/developer-guide/modules/tooling-guide/examples/GenerateHtml.java b/documentation/developer-guide/modules/tooling-guide/examples/GenerateHtml.java index 118e93985..6623407bc 100644 --- a/documentation/developer-guide/modules/tooling-guide/examples/GenerateHtml.java +++ b/documentation/developer-guide/modules/tooling-guide/examples/GenerateHtml.java @@ -15,6 +15,7 @@ // tag::imports[] import java.nio.file.Paths; +import java.util.Collection; import java.util.Map; import org.eclipse.esmf.aspectmodel.generator.docu.AspectModelDocumentationGenerator; import org.eclipse.esmf.aspectmodel.generator.docu.AspectModelDocumentationGenerator.HtmlGenerationOption; @@ -43,7 +44,7 @@ public void generate() throws IOException { new AspectModelResolver().resolveAspectModel( strategy, targetAspect ).get(); // tag::generate[] final Aspect aspect = AspectModelLoader.getAspects( model ).toJavaStream() // <1> - .flatMap( aspects -> aspects.stream() ) + .flatMap( Collection::stream ) .filter( theAspect -> theAspect.getAspectModelUrn().map( urn -> urn.equals( targetAspect ) ).orElse( false ) ) .findFirst().orElseThrow(); final AspectModelDocumentationGenerator generator = // <2> diff --git a/documentation/developer-guide/modules/tooling-guide/examples/GenerateSql.java b/documentation/developer-guide/modules/tooling-guide/examples/GenerateSql.java new file mode 100644 index 000000000..fbde8e896 --- /dev/null +++ b/documentation/developer-guide/modules/tooling-guide/examples/GenerateSql.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package examples; + +// tag::imports[] +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +import org.eclipse.esmf.aspectmodel.generator.sql.AspectModelSqlGenerator; +import org.eclipse.esmf.aspectmodel.generator.sql.SqlGenerationConfig; +import org.eclipse.esmf.aspectmodel.generator.sql.SqlGenerationConfigBuilder; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfig; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfigBuilder; +import org.eclipse.esmf.aspectmodel.resolver.AspectModelResolver; +import org.eclipse.esmf.metamodel.Aspect; +import org.eclipse.esmf.metamodel.loader.AspectModelLoader; +// end::imports[] +import org.junit.jupiter.api.Test; + +public class GenerateSql extends AbstractGenerator { + @Test + public void generate() throws IOException { + final File modelFile = new File( "aspect-models/org.eclipse.esmf.examples.movement/1.0.0/Movement.ttl" ); + + // tag::generate[] + // Aspect as created by the AspectModelLoader + final Aspect aspect = // ... + // end::generate[] + // exclude the actual loading from the example to reduce noise + AspectModelResolver.loadAndResolveModel( modelFile ).flatMap( AspectModelLoader::getSingleAspect ).get(); + // tag::generate[] + + final DatabricksSqlGenerationConfig databricksSqlGenerationConfig = + DatabricksSqlGenerationConfigBuilder.builder() + .commentLanguage( Locale.ENGLISH ) // optional + .includeTableComment( true ) // optional + .includeColumnComments( true ) // optional + .decimalPrecision( 10 ) // optional + .build(); + final SqlGenerationConfig sqlGenerationConfig = + SqlGenerationConfigBuilder.builder() + .dialect( SqlGenerationConfig.Dialect.DATABRICKS ) + .mappingStrategy( SqlGenerationConfig.MappingStrategy.DENORMALIZED ) + .dialectSpecificConfig( databricksSqlGenerationConfig ) + .build(); + final String result = AspectModelSqlGenerator.INSTANCE.apply( aspect, sqlGenerationConfig ).getContent(); + // end::generate[] + } +} diff --git a/documentation/developer-guide/modules/tooling-guide/pages/java-aspect-tooling.adoc b/documentation/developer-guide/modules/tooling-guide/pages/java-aspect-tooling.adoc index b18bee6a0..ad2fcd229 100644 --- a/documentation/developer-guide/modules/tooling-guide/pages/java-aspect-tooling.adoc +++ b/documentation/developer-guide/modules/tooling-guide/pages/java-aspect-tooling.adoc @@ -512,6 +512,78 @@ include::example$GenerateAsyncApi.java[tags=generateYaml] include::example$GenerateAsyncApi.java[tags=generateJson] ---- +[[generating-sql]] +=== Generating SQL for Aspect Models + +Using the Aspect Model SQL generator, an SQL script can be generated that sets up a table for data +corresponding to the Aspect. The current implementation provides support for the +https://docs.databricks.com/en/sql/language-manual/index.html[Databricks SQL] dialect and a mapping +strategy that uses a denormalized table, i.e., the table contains one column for each Property used +in the Aspect Model (or any of its transitively referenced Entities). + +++++ +
+Show used imports +++++ +[source,java,indent=0,subs="+macros,+quotes"] +---- +include::example$GenerateSql.java[tags=imports] +---- +++++ +
+++++ + +[source,java,indent=0,subs="+macros,+quotes"] +---- +include::example$GenerateSql.java[tags=generate] +---- + +[[databricks-type-mapping]] +==== Databricks Type Mapping + +xref:samm-specification:ROOT:datatypes.adoc#data-types[Data types] in the Aspect Model are mapped to Databricks types using the following correspondences: + +[width="100%", options="header", cols="33,33,33"] +|=== +| Aspect model type | Databricks SQL type | Note +| `xsd:string` | `STRING` | +| `xsd:boolean` | `BOOLEAN` | +| `xsd:decimal` | `DECIMAL` | While xsd:decimal is by definition unbounded, DECIMAL's default + precision is 10 digits and can be up to 38. if we assume values larger than that can appear +in the data, the Aspect Models using xsd:decimal should also use a xref:samm-specification:ROOT:characteristics.adoc#fixed-point-constraint[samm-c:FixedPointConstraint] accordingly. +| `xsd:integer` | `DECIMAL` | As opposed to xsd:int, xsd:integer has arbitrary precision, i.e. DECIMAL is needed. +| `xsd:double` | `DOUBLE` | +| `xsd:float` | `FLOAT` | +| `xsd:date` | `STRING` | `DATE` can not be used, because it does not retain timezone information. +| `xsd:time` | `STRING` | +| `xsd:dateTime` | `STRING` | +| `xsd:dateTimeStamp` | `TIMESTAMP` | +| `xsd:gYear` | `STRING` | +| `xsd:gMonth` | `STRING` | +| `xsd:gDay` | `STRING` | +| `xsd:gYearMonth` | `STRING` | +| `xsd:gMonthDay` | `STRING` | +| `xsd:duration` | `STRING` | +| `xsd:yearMonthDuration` | `STRING` | +| `xsd:dayTimeDuration` | `STRING` | +| `xsd:byte` | `TINYINT` | +| `xsd:short` | `SMALLINT` | +| `xsd:int` | `INT` | +| `xsd:long` | `BIGINT` | +| `xsd:unsignedByte` | `SMALLINT` | +| `xsd:unsignedShort` | `INT` | +| `xsd:unsignedInt` | `BIGINT` | +| `xsd:unsignedLong` | `DECIMAL` | +| `xsd:positiveInteger` | `DECIMAL` | +| `xsd:nonNegativeInteger` | `DECIMAL` | +| `xsd:negativeInteger` | `DECIMAL` | +| `xsd:nonPositiveInteger` | `DECIMAL` | +| `xsd:hexBinary` | `BINARY` | +| `xsd:base64Binary` | `BINARY` | +| `xsd:anyURI` | `STRING` | +| `samm:curie` | `STRING` | +|=== + [[generating-java-code]] == Generating Java Code for Aspect Models diff --git a/documentation/developer-guide/modules/tooling-guide/pages/maven-plugin.adoc b/documentation/developer-guide/modules/tooling-guide/pages/maven-plugin.adoc index 3bd9cd110..083d808aa 100644 --- a/documentation/developer-guide/modules/tooling-guide/pages/maven-plugin.adoc +++ b/documentation/developer-guide/modules/tooling-guide/pages/maven-plugin.adoc @@ -329,6 +329,56 @@ Configuration Properties: | `language` | The language from the model for which an AsyncAPI specification should be generated. | `String` | en | {nok} |=== +== Generate an SQL script from an Aspect Model + +The `generateSql` goal generates an SQL table creation script for a given Aspect Model. The default +life cycle phase for the goal is `generate-resources`. + +Usage: + +[source,xml,subs=attributes+] +---- + + + + esmf-aspect-model-maven-plugin + + + generate-sql + + generateSql + + + + + $\{path-to-models-root} + + $\{urn-of-aspect-model-to-be-included} + + $\{directory-for-generated-source-files} + + + + +---- + +Configuration Properties: + +[width="100%", options="header", cols="20,50,10,10,10"] +|=== +| Property | Description | Type | Default Value | Required +| `modelsRootDirectory` | The path to the root directory containing the Aspect Model file(s). | `String` | `$\{basedir}/src/main/resources/aspects` | {nok} +| `outputDirectory` | The path to the directory where the generated SQL script will be written to. | `String` | none | {ok} +| `dialect` | The SQL dialect to generate for. | `String` | `databricks` | {nok} +| `strategy` | The mapping strategy to use. | `String` | `denormalized` | {nok} +| `language` | The language from the model to use for generated comments. | `String` | en | {nok} +| `includeTableComment` | Include table comment in the generated SQL script. | `Boolean` | `true` | {nok} +| `includeColumnComments` | Include column comments in the generated SQL script. | `Boolean` | `true` | {nok} +| `tableCommandPrefix` | The prefix to use for Databricks table creation commands. | `String` | `CREATE TABLE IF NOT EXISTS` | {nok} +| `decimalPrecision` | The precision to use for Databricks decimal columns, between 1 and 38. See also notes in + the xref:java-aspect-tooling.adoc#databricks-type-mapping[Databricks type mapping]. | `Integer` | 10 | {nok} +|=== + == Generate Documentation for an Aspect Model === Generating HTML Documentation diff --git a/documentation/developer-guide/modules/tooling-guide/pages/samm-cli.adoc b/documentation/developer-guide/modules/tooling-guide/pages/samm-cli.adoc index 051a84f7a..10180d230 100644 --- a/documentation/developer-guide/modules/tooling-guide/pages/samm-cli.adoc +++ b/documentation/developer-guide/modules/tooling-guide/pages/samm-cli.adoc @@ -59,32 +59,32 @@ The available options and their meaning can also be seen in the help text of the | help | Get overview of all commands | `samm help` | help | Get help for a specific subcommand | `samm help aspect` | aspect help | Get help for `aspect` subcommands | `samm aspect help validate` -.2+| aspect validate | Validate Aspect Model | `samm aspect AspectModel.ttl validate` +.2+| [[aspect-validate]] aspect validate | Validate Aspect Model | `samm aspect AspectModel.ttl validate` | _--custom-resolver_ : use an external resolver for the resolution of the model elements | `samm aspect AspectModel.ttl validate --custom-resolver myresolver.sh` -.2+| aspect prettyprint | Pretty-print Aspect Model | `samm aspect AspectModel.ttl prettyprint` +.2+| [[aspect-prettyprint]] aspect prettyprint | Pretty-print Aspect Model | `samm aspect AspectModel.ttl prettyprint` | _--output, -o_ : the output will be saved to the given file | `samm aspect AspectModel.ttl prettyprint -o c:\Results\PrettyPrinted.ttl` -.2+| aspect migrate | Migrate Aspect Model to the latest SAMM version | `samm aspect AspectModel.ttl migrate AspectModel.ttl` +.2+| [[aspect-migrate]] aspect migrate | Migrate Aspect Model to the latest SAMM version | `samm aspect AspectModel.ttl migrate AspectModel.ttl` | _--output, -o_ : the output will be saved to the given file | `samm aspect AspectModel.ttl migrate AspectModel.ttl -o c:\Results\MigratedModel.ttl` -.5+| aspect to html | Generate HTML documentation for an Aspect Model | `samm aspect AspectModel.ttl to html` +.5+| [[aspect-to-html]] aspect to html | Generate HTML documentation for an Aspect Model | `samm aspect AspectModel.ttl to html` | _--output, -o_ : the output will be saved to the given file | `samm aspect AspectModel.ttl to html -o c:\Model.html` | _--css, -c_ : CSS file with custom styles to be included in the generated HTML documentation | `samm aspect AspectModel.ttl to html -c c:\styles.css` | _--language, -l_ : The language from the model for which the HTML should be generated (default: en) | `samm aspect AspectModel.ttl to html -l de` | _--custom-resolver_ : use an external resolver for the resolution of the model elements | `samm aspect AspectModel.ttl to html --custom-resolver myresolver.bat` -.4+| aspect to png | Generate PNG diagram for Aspect Model | `samm aspect AspectModel.ttl to png` +.4+| [[aspect-to-png]] aspect to png | Generate PNG diagram for Aspect Model | `samm aspect AspectModel.ttl to png` | _--output, -o_ : output file path (default: stdout); as PNG is a binary format, it is strongly recommended to output the result to a file by using the -o option or the console redirection operator '>')| | _--language, -l_ : the language from the model for which the diagram should be generated (default: en) | | _--custom-resolver_ : use an external resolver for the resolution of the model elements | `samm aspect AspectModel.ttl to png --custom-resolver resolver.jar` -.4+| aspect to svg | Generate SVG diagram for Aspect Model | `samm aspect AspectModel.ttl to svg` +.4+| [[aspect-to-svg]] aspect to svg | Generate SVG diagram for Aspect Model | `samm aspect AspectModel.ttl to svg` | _--output, -o_ : the output will be saved to the given file | | _--language, -l_ : the language from the model for which the diagram should be generated (default: en) | | _--custom-resolver_ : use an external resolver for the resolution of the model elements | `samm aspect AspectModel.ttl to svg --custom-resolver "java -jar resolver.jar"` -.8+| aspect to java | Generate Java classes from an Aspect Model | `samm aspect AspectModel.ttl to java` +.8+| [[asepct-to-java]] aspect to java | Generate Java classes from an Aspect Model | `samm aspect AspectModel.ttl to java` | _--output-directory, -d_ : output directory to write files to (default: current directory) | | _--package-name, -pn_ : package to use for generated Java classes | `samm aspect AspectModel.ttl to java -pn org.company.product` @@ -96,10 +96,11 @@ The available options and their meaning can also be seen in the help text of the https://velocity.apache.org/[Velocity] macro library | | _--static, -s_ : generate Java domain classes for a Static Meta Model | | _--custom-resolver_ : use an external resolver for the resolution of the model elements | -.20+| aspect to openapi | Generate https://spec.openapis.org/oas/v3.0.3[OpenAPI] specification for an Aspect Model| `samm aspect AspectModel.ttl to openapi -j` +.20+| [[aspect-to-openapi]] aspect to openapi | Generate https://spec.openapis.org/oas/v3.0.3[OpenAPI] specification + for an Aspect Model | `samm aspect AspectModel.ttl to openapi -j` | _--output, -o_ : output file path (default: stdout) | | _--api-base-url, -b_ : the base url for the Aspect API used in the - https://spec.openapis.org/oas/v3.0.3[OpenAPI] specification | `samm aspect AspectModel.ttl to openapi -j -b \http://mysite.de` + https://spec.openapis.org/oas/v3.0.3[OpenAPI] specification | `samm aspect AspectModel.ttl to openapi -j -b \http://example.org` | _--json, -j_ : generate a JSON specification for an Aspect Model (default format is YAML) | | _--comment, -c_ : only in combination with --json; generates `$comment` @@ -127,36 +128,53 @@ The available options and their meaning can also be seen in the help text of the be generated (default: en) | `samm aspect AspectModel.ttl to openapi -l de` | _--separate-files, -sf_ : Create separate files for each schema | | _--custom-resolver_ : use an external resolver for the resolution of the model elements | -.8+| aspect to asyncapi | Generate https://www.asyncapi.com/docs/reference/specification/v3.0.0[AsyncAPI] specification for an Aspect Model| `samm aspect AspectModel.ttl to asyncapi` +.8+| [[aspect-to-asyncapi]] aspect to asyncapi | Generate https://www.asyncapi.com/docs/reference/specification/v3.0.0[AsyncAPI] specification for an Aspect Model| `samm aspect AspectModel.ttl to asyncapi` | _--output, -o_ : output file path (default: stdout) | | _--channel-address, -ca_ : Sets the channel address (i.e., for MQTT, the topic's name). https://spec.openapis.org/oas/v3.0.3[OpenAPI] specification | `samm aspect AspectModel.ttl to asyncapi -ca 123-456/789-012/namespace/1.0.0/Aspect` - | _--application-id, -ai_ : Sets the application id, e.g. an identifying URL. | + | _--application-id, -ai_ : Sets the application id, e.g. an identifying URL. | | _--semantic-version, -sv_ : use the full semantic version from the Aspect Model as the version for the Aspect API | | _--language, -l_ : The language from the model for which an AsyncAPI specification should be generated (default: en) | `samm aspect AspectModel.ttl to asyncapi -l de` | _--separate-files, -sf_ : Create separate files for each schema | | _--custom-resolver_ : use an external resolver for the resolution of the model elements | -.3+| aspect to json | Generate example JSON payload data for an Aspect Model | `samm aspect AspectModel.ttl to json` +.3+| [[aspect-to-json]] aspect to json | Generate example JSON payload data for an Aspect Model | `samm aspect AspectModel.ttl to json` | _--output, -o_ : output file path (default: stdout) | | _--custom-resolver_ : use an external resolver for the resolution of the model elements | -.4+| aspect to schema | Generate JSON schema for an Aspect Model | `samm aspect AspectModel.ttl to schema` +.4+| [[aspect-to-schema]] aspect to schema | Generate JSON schema for an Aspect Model | `samm aspect AspectModel.ttl to schema` | _--output, -o_ : output file path (default: stdout) | | _--language, -l_ : The language from the model for which a JSON schema should be generated (default: en) | `samm aspect AspectModel.ttl to schema -l de` | _--custom-resolver_ : use an external resolver for the resolution of the model elements | -.5+| aspect to aas | Generate an Asset Administration Shell (AAS) submodel template from an Aspect Model | `samm aspect AspectModel.ttl to aas` +.9+| [[aspect-to-sql]] aspect to sql | Generate SQL script that sets up a table for data for this Aspect | `samm aspect AspectModel.ttl to sql` + | _--output, -o_ : output file path (default: stdout) | + | _--language, -l_ : The language from the model to use for generated comments | + | _--dialect, -d_ : The SQL dialect to generate for (default: `databricks`) | + | _--mapping-strategy, -s_ : The mapping strategy to use (default: `denormalized`) | + | _--include-table-comment, -tc_ : Include table comment in the generated SQL script + (default: `true`) | + | _--include-column-comments, -cc_ : Include column comments in the generated SQL + script (default: `true`) | + | _--table-command-prefix, -tcp_ : The prefix to use for Databricks table creation + commands (default: `CREATE TABLE IF NOT EXISTS`) | + | _--decimal-precision, -dp_ : The precision to use for Databricks decimal columns + (default: 10). See also notes in + the xref:java-aspect-tooling.adoc#databricks-type-mapping[Databricks type mapping]. | +.5+| [[aspect-to-aas]] aspect to aas | Generate an Asset Administration Shell (AAS) submodel template from an + Aspect Model | `samm aspect AspectModel.ttl to aas` | _--output, -o_ : output file path (default: stdout) | | _--format, -f_ : output file format (xml, json, or aasx, default: xml) | | _--custom-resolver_ : use an external resolver for the resolution of the model elements | - | _--aspect-data, -a_ : path to a JSON file containing aspect data corresponding to the Aspect Model | -.3+| aas to aspect | Translate Asset Administration Shell (AAS) Submodel Templates to Aspect Models | `samm aas AssetAdminShell.aasx to aspect` + | _--aspect-data, -a_ : path to a JSON file containing aspect data corresponding to the + Aspect Model | +.3+| [[aas-to-aspect]] aas to aspect | Translate Asset Administration Shell (AAS) Submodel Templates to + Aspect Models | `samm aas AssetAdminShell.aasx to aspect` | _--output-directory, -d_ : output directory to write files to (default: current directory) | | _--submodel-template, -s_ : selected submodel template for generating; run `samm aas list` to list them. | `samm aas AssetAdminShell.aasx to aspect -s 1 -s 2` -.1+| aas list | Retrieve a list of submodel templates contained within the provided +.1+| [[aas-list]] aas list | Retrieve a list of submodel templates contained within the provided Asset Administration Shell (AAS) file. | `samm aas AssetAdminShell.aasx list` |=== @@ -258,7 +276,7 @@ The version information as described above is also used in the URL definitions o ---- { "servers" : [ { - "url" : "http://mysite/api/v2", // <1> + "url" : "http://example.com/api/v2", // <1> "variables" : { "api-version" : { "default" : "v2" // <1> diff --git a/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/CodeGenerationMojo.java b/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/CodeGenerationMojo.java index f96c9772a..4bd3cc420 100644 --- a/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/CodeGenerationMojo.java +++ b/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/CodeGenerationMojo.java @@ -48,9 +48,9 @@ protected void validateParameters( final File templateLibFile ) throws MojoExecu protected final Function nameMapper = artifact -> { final String path = artifact.getPackageName(); final String fileName = artifact.getClassName(); - final String outputDirectoryForArtefact = outputDirectory + File.separator + path.replace( '.', File.separatorChar ); - final String artefactName = fileName + ".java"; - return getOutputStreamForFile( artefactName, outputDirectoryForArtefact ); + final String outputDirectoryForArtifact = outputDirectory + File.separator + path.replace( '.', File.separatorChar ); + final String artifactName = fileName + ".java"; + return getOutputStreamForFile( artifactName, outputDirectoryForArtifact ); }; protected String determinePackageName( final Aspect aspect ) { diff --git a/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/GenerateSql.java b/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/GenerateSql.java new file mode 100644 index 000000000..0090f95fe --- /dev/null +++ b/tools/esmf-aspect-model-maven-plugin/src/main/java/org/eclipse/esmf/aspectmodel/GenerateSql.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Locale; +import java.util.Set; + +import org.eclipse.esmf.aspectmodel.generator.sql.AspectModelSqlGenerator; +import org.eclipse.esmf.aspectmodel.generator.sql.SqlArtifact; +import org.eclipse.esmf.aspectmodel.generator.sql.SqlGenerationConfig; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfig; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfigBuilder; +import org.eclipse.esmf.metamodel.AspectContext; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Mojo( name = "generateSql", defaultPhase = LifecyclePhase.GENERATE_RESOURCES ) +public class GenerateSql extends AspectModelMojo { + private final Logger logger = LoggerFactory.getLogger( GenerateSql.class ); + + @Parameter( defaultValue = "" + DatabricksSqlGenerationConfig.DEFAULT_INCLUDE_TABLE_COMMENT ) + private boolean includeTableComment; + + @Parameter( defaultValue = "" + DatabricksSqlGenerationConfig.DEFAULT_INCLUDE_COLUMN_COMMENTS ) + private boolean includeColumnComments; + + @Parameter( defaultValue = DatabricksSqlGenerationConfig.DEFAULT_TABLE_COMMAND_PREFIX ) + private String tableCommandPrefix = DatabricksSqlGenerationConfig.DEFAULT_TABLE_COMMAND_PREFIX; + + @Parameter( defaultValue = "" + DatabricksSqlGenerationConfig.DECIMAL_DEFAULT_PRECISION ) + private int decimalPrecision = DatabricksSqlGenerationConfig.DECIMAL_DEFAULT_PRECISION; + + @Parameter( defaultValue = "en" ) + private String language = DatabricksSqlGenerationConfig.DEFAULT_COMMENT_LANGUAGE.getLanguage(); + + @Parameter( defaultValue = "databricks" ) + private String dialect = SqlGenerationConfig.Dialect.DATABRICKS.toString().toLowerCase(); + + @Parameter( defaultValue = "denormalized" ) + private String strategy = SqlGenerationConfig.MappingStrategy.DENORMALIZED.toString().toLowerCase(); + + @Override + public void execute() throws MojoExecutionException { + validateParameters(); + + final Set aspectModels = loadModelsOrFail(); + for ( final AspectContext context : aspectModels ) { + final DatabricksSqlGenerationConfig generatorConfig = + DatabricksSqlGenerationConfigBuilder.builder() + .commentLanguage( Locale.forLanguageTag( language ) ) + .includeTableComment( includeTableComment ) + .includeColumnComments( includeColumnComments ) + .createTableCommandPrefix( tableCommandPrefix ) + .decimalPrecision( decimalPrecision ) + .build(); + final SqlGenerationConfig sqlConfig = new SqlGenerationConfig( SqlGenerationConfig.Dialect.valueOf( dialect.toUpperCase() ), + SqlGenerationConfig.MappingStrategy.valueOf( strategy.toUpperCase() ), generatorConfig ); + final SqlArtifact result = AspectModelSqlGenerator.INSTANCE.apply( context.aspect(), sqlConfig ); + + try ( final OutputStream out = getOutputStreamForFile( context.aspect().getName() + ".sql", outputDirectory ) ) { + out.write( result.getContent().getBytes() ); + } catch ( final IOException exception ) { + throw new MojoExecutionException( "Could not write SQL file.", exception ); + } + } + logger.info( "Successfully generated SQL script." ); + } +} diff --git a/tools/esmf-aspect-model-maven-plugin/src/test/java/org/eclipse/esmf/aspectmodel/GenerateSqlTest.java b/tools/esmf-aspect-model-maven-plugin/src/test/java/org/eclipse/esmf/aspectmodel/GenerateSqlTest.java new file mode 100644 index 000000000..8d35d15c9 --- /dev/null +++ b/tools/esmf-aspect-model-maven-plugin/src/test/java/org/eclipse/esmf/aspectmodel/GenerateSqlTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspectmodel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.maven.plugin.Mojo; +import org.junit.Test; + +public class GenerateSqlTest extends AspectModelMojoTest { + @Test + public void testGenerateSqlWithDefaultSettings() throws Exception { + final File testPom = getTestFile( "src/test/resources/generate-sql-pom-valid-aspect-model-default-settings.xml" ); + final Mojo generateSql = lookupMojo( "generateSql", testPom ); + assertThatCode( generateSql::execute ).doesNotThrowAnyException(); + final Path generatedFile = generatedFilePath( "AspectWithSimpleTypes.sql" ); + assertThat( generatedFile ).exists(); + final String sqlContent = new String( Files.readAllBytes( generatedFile ) ); + + assertThat( sqlContent ).contains( "CREATE TABLE IF NOT EXISTS aspect_with_simple_types" ); + } + + @Test + public void testGenerateSqlWithAdjustedSettings() throws Exception { + final File testPom = getTestFile( "src/test/resources/generate-sql-pom-valid-aspect-model-adjusted-settings.xml" ); + final Mojo generateSql = lookupMojo( "generateSql", testPom ); + assertThatCode( generateSql::execute ).doesNotThrowAnyException(); + final Path generatedFile = generatedFilePath( "AspectWithSimpleTypes.sql" ); + assertThat( generatedFile ).exists(); + final String sqlContent = new String( Files.readAllBytes( generatedFile ) ); + + assertThat( sqlContent ).contains( "CREATE TABLE aspect_with_simple_types" ); + assertThat( sqlContent ).contains( "DECIMAL(23)" ); + } +} diff --git a/tools/esmf-aspect-model-maven-plugin/src/test/resources/generate-sql-pom-valid-aspect-model-adjusted-settings.xml b/tools/esmf-aspect-model-maven-plugin/src/test/resources/generate-sql-pom-valid-aspect-model-adjusted-settings.xml new file mode 100644 index 000000000..2050344e4 --- /dev/null +++ b/tools/esmf-aspect-model-maven-plugin/src/test/resources/generate-sql-pom-valid-aspect-model-adjusted-settings.xml @@ -0,0 +1,41 @@ + + + + + 4.0.0 + + org.eclipse.esmf + test-generate-sql-mojo + 1.0 + jar + Test Generate SQL Mojo + + + + + esmf-aspect-model-maven-plugin + + ${basedir}/../../core/esmf-test-aspect-models/src/main/resources/valid/samm_2_0_0 + + urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithSimpleTypes + + CREATE TABLE + 23 + ${basedir}/target/test-artifacts + + + + + diff --git a/tools/esmf-aspect-model-maven-plugin/src/test/resources/generate-sql-pom-valid-aspect-model-default-settings.xml b/tools/esmf-aspect-model-maven-plugin/src/test/resources/generate-sql-pom-valid-aspect-model-default-settings.xml new file mode 100644 index 000000000..874c3f398 --- /dev/null +++ b/tools/esmf-aspect-model-maven-plugin/src/test/resources/generate-sql-pom-valid-aspect-model-default-settings.xml @@ -0,0 +1,39 @@ + + + + + 4.0.0 + + org.eclipse.esmf + test-generate-sql-mojo + 1.0 + jar + Test Generate SQL Mojo + + + + + esmf-aspect-model-maven-plugin + + ${basedir}/../../core/esmf-test-aspect-models/src/main/resources/valid/samm_2_0_0 + + urn:samm:org.eclipse.esmf.test:1.0.0#AspectWithSimpleTypes + + ${basedir}/target/test-artifacts + + + + + diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectToCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectToCommand.java index 43404fdc2..6bcc10462 100644 --- a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectToCommand.java +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/AspectToCommand.java @@ -23,6 +23,7 @@ import org.eclipse.esmf.aspect.to.AspectToJsonSchemaCommand; import org.eclipse.esmf.aspect.to.AspectToOpenapiCommand; import org.eclipse.esmf.aspect.to.AspectToPngCommand; +import org.eclipse.esmf.aspect.to.AspectToSqlCommand; import org.eclipse.esmf.aspect.to.AspectToSvgCommand; import org.eclipse.esmf.exception.SubCommandException; @@ -39,7 +40,8 @@ AspectToPngCommand.class, AspectToJsonSchemaCommand.class, AspectToSvgCommand.class, - AspectToAasCommand.class + AspectToAasCommand.class, + AspectToSqlCommand.class }, descriptionHeading = "%n@|bold Description|@:%n%n", parameterListHeading = "%n@|bold Parameters|@:%n", diff --git a/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToSqlCommand.java b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToSqlCommand.java new file mode 100644 index 000000000..cbc11db25 --- /dev/null +++ b/tools/samm-cli/src/main/java/org/eclipse/esmf/aspect/to/AspectToSqlCommand.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH + * + * See the AUTHORS file(s) distributed with this work for additional + * information regarding authorship. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.eclipse.esmf.aspect.to; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Locale; + +import org.eclipse.esmf.AbstractCommand; +import org.eclipse.esmf.ExternalResolverMixin; +import org.eclipse.esmf.LoggingMixin; +import org.eclipse.esmf.aspect.AspectToCommand; +import org.eclipse.esmf.aspectmodel.generator.sql.AspectModelSqlGenerator; +import org.eclipse.esmf.aspectmodel.generator.sql.SqlArtifact; +import org.eclipse.esmf.aspectmodel.generator.sql.SqlGenerationConfig; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfig; +import org.eclipse.esmf.aspectmodel.generator.sql.databricks.DatabricksSqlGenerationConfigBuilder; +import org.eclipse.esmf.exception.CommandException; +import org.eclipse.esmf.metamodel.AspectContext; + +import picocli.CommandLine; + +@CommandLine.Command( name = AspectToSqlCommand.COMMAND_NAME, + description = "Generate SQL table creation script for an Aspect Model", + descriptionHeading = "%n@|bold Description|@:%n%n", + parameterListHeading = "%n@|bold Parameters|@:%n", + optionListHeading = "%n@|bold Options|@:%n", + mixinStandardHelpOptions = true +) +public class AspectToSqlCommand extends AbstractCommand { + public static final String COMMAND_NAME = "sql"; + + @CommandLine.Option( names = { "--output", "-o" }, description = "Output file path" ) + private String outputFilePath = "-"; + + @CommandLine.Option( names = { "--dialect", "-d" }, + description = "The SQL dialect to generate for (default: ${DEFAULT-VALUE}" ) + private SqlGenerationConfig.Dialect dialect = SqlGenerationConfig.Dialect.DATABRICKS; + + @CommandLine.Option( names = { "--mapping-strategy", "-s" }, + description = "The mapping strategy to use (default: ${DEFAULT-VALUE}" ) + private SqlGenerationConfig.MappingStrategy strategy = SqlGenerationConfig.MappingStrategy.DENORMALIZED; + + @CommandLine.Option( names = { "--language", "-l" }, + description = "The language from the model for which comments should be generated (default: ${DEFAULT-VALUE})" ) + private String language = DatabricksSqlGenerationConfig.DEFAULT_COMMENT_LANGUAGE.getLanguage(); + + @CommandLine.Option( names = { "--include-table-comment", "-tc" }, + description = "Include table comment in the generated SQL script (default: ${DEFAULT-VALUE})" ) + private boolean includeTableComment = DatabricksSqlGenerationConfig.DEFAULT_INCLUDE_TABLE_COMMENT; + + @CommandLine.Option( names = { "--include-column-comments", "-cc" }, + description = "Include column comments in the generated SQL script (default: ${DEFAULT-VALUE})" ) + private boolean includeColumnComments = DatabricksSqlGenerationConfig.DEFAULT_INCLUDE_COLUMN_COMMENTS; + + @CommandLine.Option( names = { "--table-command-prefix", "-tcp" }, + description = "The prefix to use for Databricks table creation commands (default: ${DEFAULT-VALUE})" ) + private String tableCommandPrefix = DatabricksSqlGenerationConfig.DEFAULT_TABLE_COMMAND_PREFIX; + + @CommandLine.Option( names = { "--decimal-precision", "-dp" }, + description = "The precision to use for Databricks decimal columns, between 1 and 38. (default: ${DEFAULT-VALUE})" ) + private int decimalPrecision = DatabricksSqlGenerationConfig.DECIMAL_DEFAULT_PRECISION; + + @CommandLine.ParentCommand + private AspectToCommand parentCommand; + + @CommandLine.Mixin + private ExternalResolverMixin customResolver; + + @CommandLine.Mixin + private LoggingMixin loggingMixin; + + @Override + public void run() { + final AspectContext context = loadModelOrFail( parentCommand.parentCommand.getInput(), customResolver ); + final DatabricksSqlGenerationConfig generatorConfig = + DatabricksSqlGenerationConfigBuilder.builder() + .commentLanguage( Locale.forLanguageTag( language ) ) + .includeTableComment( includeTableComment ) + .includeColumnComments( includeColumnComments ) + .createTableCommandPrefix( tableCommandPrefix ) + .decimalPrecision( decimalPrecision ) + .build(); + final SqlGenerationConfig sqlConfig = new SqlGenerationConfig( dialect, strategy, generatorConfig ); + final SqlArtifact result = AspectModelSqlGenerator.INSTANCE.apply( context.aspect(), sqlConfig ); + + try ( final OutputStream out = getStreamForFile( outputFilePath ) ) { + out.write( result.getContent().getBytes() ); + } catch ( final IOException e ) { + throw new CommandException( e ); + } + } +} diff --git a/tools/samm-cli/src/test/java/org/eclipse/esmf/SammCliTest.java b/tools/samm-cli/src/test/java/org/eclipse/esmf/SammCliTest.java index 2da21d621..8169d89d6 100644 --- a/tools/samm-cli/src/test/java/org/eclipse/esmf/SammCliTest.java +++ b/tools/samm-cli/src/test/java/org/eclipse/esmf/SammCliTest.java @@ -1052,6 +1052,13 @@ void testAspectToSvgWithCustomResolver() { assertThat( result.stderr() ).isEmpty(); } + @Test + void testAspectToSqlToStdout() { + final ExecutionResult result = sammCli.runAndExpectSuccess( "--disable-color", "aspect", defaultInputFile, "to", "sql" ); + assertThat( result.stdout() ).contains( "CREATE TABLE" ); + assertThat( result.stderr() ).isEmpty(); + } + /** * Returns the File object for a test model file */