diff --git a/src/e2e/resources/simple-temple-expected/simple-temple-test-user-db/init.sql b/src/e2e/resources/simple-temple-expected/simple-temple-test-user-db/init.sql index f09b5c4d4..71bf0c4a1 100644 --- a/src/e2e/resources/simple-temple-expected/simple-temple-test-user-db/init.sql +++ b/src/e2e/resources/simple-temple-expected/simple-temple-test-user-db/init.sql @@ -15,7 +15,7 @@ CREATE TABLE simple_temple_test_user ( CREATE TABLE fred ( id UUID NOT NULL PRIMARY KEY, created_by UUID NOT NULL, - field TEXT NOT NULL, + field TEXT, friend UUID NOT NULL, image BYTEA CHECK (octet_length(image) <= 10000000) NOT NULL ); diff --git a/src/e2e/scala/temple/DSL/ParserE2ETest.scala b/src/e2e/scala/temple/DSL/ParserE2ETest.scala index 5f3c96536..5af04741e 100644 --- a/src/e2e/scala/temple/DSL/ParserE2ETest.scala +++ b/src/e2e/scala/temple/DSL/ParserE2ETest.scala @@ -5,7 +5,7 @@ import temple.DSL.parser.DSLParserMatchers import temple.DSL.semantics.Analyzer.parseAndValidate import temple.ast.AbstractAttribute.{Attribute, CreatedByAttribute, IDAttribute} import temple.ast.AbstractServiceBlock.ServiceBlock -import temple.ast.Annotation.{Server, Unique} +import temple.ast.Annotation.{Nullable, Server, Unique} import temple.ast.AttributeType._ import temple.ast.Metadata._ import temple.ast._ @@ -57,7 +57,7 @@ class ParserE2ETest extends FlatSpec with Matchers with DSLParserMatchers { Map( "id" -> IDAttribute, "createdBy" -> CreatedByAttribute, - "field" -> Attribute(StringType()), + "field" -> Attribute(StringType(), valueAnnotations = Set(Nullable)), "friend" -> Attribute(ForeignKey("SimpleTempleTestUser")), "image" -> Attribute(BlobType(size = Some(10_000_000))), ), diff --git a/src/it/scala/temple/generate/docker/DockerGeneratorIntegrationTestData.scala b/src/it/scala/temple/generate/docker/DockerGeneratorIntegrationTestData.scala index 6af99e7d4..744d20a14 100644 --- a/src/it/scala/temple/generate/docker/DockerGeneratorIntegrationTestData.scala +++ b/src/it/scala/temple/generate/docker/DockerGeneratorIntegrationTestData.scala @@ -28,7 +28,7 @@ object DockerGeneratorIntegrationTestData { "Test" -> StructBlock( ListMap( "favouriteColour" -> Attribute(StringType(), valueAnnotations = Set(Annotation.Unique)), - "bedTime" -> Attribute(TimeType), + "bedTime" -> Attribute(TimeType, valueAnnotations = Set(Annotation.Nullable)), "favouriteNumber" -> Attribute(IntType(max = Some(10), min = Some(0))), ), ), diff --git a/src/main/scala/temple/DSL/semantics/Analyzer.scala b/src/main/scala/temple/DSL/semantics/Analyzer.scala index 89cdaf0f0..6f3050333 100644 --- a/src/main/scala/temple/DSL/semantics/Analyzer.scala +++ b/src/main/scala/temple/DSL/semantics/Analyzer.scala @@ -144,6 +144,7 @@ object Analyzer { val valueAnnotations = mutable.HashSet[Annotation.ValueAnnotation]() annotations.iterator.map(_.key) foreach { case "unique" => valueAnnotations += Annotation.Unique + case "nullable" => valueAnnotations += Annotation.Nullable case "server" => setAccessAnnotation(Annotation.Server) case "client" => setAccessAnnotation(Annotation.Client) case "serverSet" => setAccessAnnotation(Annotation.ServerSet) diff --git a/src/main/scala/temple/DSL/semantics/Validator.scala b/src/main/scala/temple/DSL/semantics/Validator.scala index 7854e4ac1..c9781de2b 100644 --- a/src/main/scala/temple/DSL/semantics/Validator.scala +++ b/src/main/scala/temple/DSL/semantics/Validator.scala @@ -3,6 +3,7 @@ package temple.DSL.semantics import temple.DSL.semantics.NameClashes._ import temple.ast.AbstractAttribute.{Attribute, CreatedByAttribute, IDAttribute} import temple.ast.AbstractServiceBlock._ +import temple.ast.Annotation.Nullable import temple.ast.AttributeType._ import temple.ast.Metadata.ServiceAuth import temple.ast.{Metadata, _} @@ -172,7 +173,8 @@ private class Validator private (templefile: Templefile) { case None => // nothing to validate } attribute.valueAnnotations foreach { - case Annotation.Unique => // nothing to validate + case Annotation.Unique => // nothing to validate + case Annotation.Nullable => // nothing to validate } } @@ -215,7 +217,7 @@ private class Validator private (templefile: Templefile) { private val referenceCycles: Set[Set[String]] = { val graph = templefile.providedBlockNames.map { blockName => blockName -> templefile.getBlock(blockName).attributes.values.collect { - case Attribute(ForeignKey(references), _, annotations) => references + case Attribute(ForeignKey(references), _, annotations) if !annotations.contains(Nullable) => references } }.toMap Tarjan(graph).filter(_.sizeIs > 1) diff --git a/src/main/scala/temple/ast/Annotation.scala b/src/main/scala/temple/ast/Annotation.scala index c8bb02285..a4f879e94 100644 --- a/src/main/scala/temple/ast/Annotation.scala +++ b/src/main/scala/temple/ast/Annotation.scala @@ -7,6 +7,7 @@ object Annotation { sealed abstract class ValueAnnotation(render: String) extends Annotation(render) case object Unique extends ValueAnnotation("@unique") + case object Nullable extends ValueAnnotation("@nullable") case object Server extends AccessAnnotation("@server") case object ServerSet extends AccessAnnotation("@serverSet") case object Client extends AccessAnnotation("@client") diff --git a/src/main/scala/temple/builder/DatabaseBuilder.scala b/src/main/scala/temple/builder/DatabaseBuilder.scala index 64ab270a8..171fd610d 100644 --- a/src/main/scala/temple/builder/DatabaseBuilder.scala +++ b/src/main/scala/temple/builder/DatabaseBuilder.scala @@ -1,6 +1,7 @@ package temple.builder import temple.ast.AbstractAttribute.{CreatedByAttribute, IDAttribute} +import temple.ast.Annotation.Nullable import temple.ast._ import temple.generate.CRUD._ import temple.generate.database.ast.ColumnConstraint.Check @@ -22,12 +23,13 @@ object DatabaseBuilder { } private def toColDef(name: String, attribute: AbstractAttribute): ColumnDef = { - val nonNullConstraint = Some(ColumnConstraint.NonNull) + val nonNullConstraint = when(!attribute.valueAnnotations.contains(Nullable)) { ColumnConstraint.NonNull } val primaryKeyConstraint = when(attribute == IDAttribute) { ColumnConstraint.PrimaryKey } val valueConstraints = attribute.valueAnnotations.flatMap { - case Annotation.Unique => Some(ColumnConstraint.Unique) + case Annotation.Unique => Some(ColumnConstraint.Unique) + case Annotation.Nullable => None } ++ nonNullConstraint ++ primaryKeyConstraint val (colType, typeConstraints) = attribute.attributeType match { diff --git a/src/test/scala/temple/DSL/parser/DSLParserTest.scala b/src/test/scala/temple/DSL/parser/DSLParserTest.scala index 7cb297a35..091c806fd 100644 --- a/src/test/scala/temple/DSL/parser/DSLParserTest.scala +++ b/src/test/scala/temple/DSL/parser/DSLParserTest.scala @@ -83,7 +83,7 @@ class DSLParserTest extends FlatSpec with DSLParserMatchers { "Fred", "struct", Seq( - Attribute("field", AttributeType.Primitive("string")), + Attribute("field", AttributeType.Primitive("string"), Seq(Annotation("nullable"))), Attribute("friend", AttributeType.Foreign("User")), Attribute("image", AttributeType.Primitive("data", Args(Seq(Arg.IntArg(10_000_000))))), Metadata("enumerable"), diff --git a/src/test/scala/temple/DSL/semantics/SemanticAnalyzerTest.scala b/src/test/scala/temple/DSL/semantics/SemanticAnalyzerTest.scala index e44ccc03d..8786c2484 100644 --- a/src/test/scala/temple/DSL/semantics/SemanticAnalyzerTest.scala +++ b/src/test/scala/temple/DSL/semantics/SemanticAnalyzerTest.scala @@ -248,7 +248,7 @@ class SemanticAnalyzerTest extends FlatSpec with Matchers { parseSemantics( mkTemplefileASTWithUserService( Entry.Attribute("a", syntax.AttributeType.Primitive("int"), Seq(syntax.Annotation("unique"))), - Entry.Attribute("b", syntax.AttributeType.Primitive("int")), + Entry.Attribute("b", syntax.AttributeType.Primitive("int"), Seq(syntax.Annotation("nullable"))), Entry.Attribute("c", syntax.AttributeType.Primitive("float"), Seq(syntax.Annotation("serverSet"))), Entry.Attribute("d", syntax.AttributeType.Primitive("float"), Seq(syntax.Annotation("client"))), Entry.Attribute("e", syntax.AttributeType.Primitive("float"), Seq(syntax.Annotation("server"))), diff --git a/src/test/scala/temple/DSL/semantics/ValidatorTest.scala b/src/test/scala/temple/DSL/semantics/ValidatorTest.scala index 525a873c0..b556404c3 100644 --- a/src/test/scala/temple/DSL/semantics/ValidatorTest.scala +++ b/src/test/scala/temple/DSL/semantics/ValidatorTest.scala @@ -5,7 +5,7 @@ import temple.DSL.semantics.Validator._ import temple.DSL.semantics.ValidatorTest._ import temple.ast.AbstractAttribute.{Attribute, IDAttribute} import temple.ast.AbstractServiceBlock._ -import temple.ast.Annotation.Unique +import temple.ast.Annotation.{Nullable, Unique} import temple.ast.AttributeType._ import temple.ast.Metadata.Endpoint.Delete import temple.ast.Metadata._ @@ -25,7 +25,7 @@ class ValidatorTest extends FlatSpec with Matchers { attributes = Map( "a" -> Attribute(IntType(), accessAnnotation = Some(Annotation.Server)), "b" -> Attribute(BoolType, accessAnnotation = Some(Annotation.Client)), - "c" -> Attribute(BlobType(), valueAnnotations = Set(Unique)), + "c" -> Attribute(BlobType(), valueAnnotations = Set(Nullable, Unique)), "d" -> Attribute(FloatType(), accessAnnotation = Some(Annotation.ServerSet)), "e" -> Attribute(StringType()), "f" -> Attribute(ForeignKey("Box")), @@ -312,7 +312,18 @@ class ValidatorTest extends FlatSpec with Matchers { ) shouldBe empty } - it should "find cycles in dependencies" in { + it should "find cycles in dependencies, but only if they are not nullable" in { + validationErrors( + Templefile( + "MyProject", + services = Map( + "User" -> ServiceBlock(Map("box" -> Attribute(ForeignKey("Box")))), + "Box" -> ServiceBlock(Map("fred" -> Attribute(ForeignKey("Fred"), None, Set(Nullable)))), + "Fred" -> ServiceBlock(Map("user" -> Attribute(ForeignKey("User")))), + ), + ), + ) shouldBe Set() + validationErrors( Templefile( "MyProject", diff --git a/src/test/scala/temple/builder/BuilderTestData.scala b/src/test/scala/temple/builder/BuilderTestData.scala index 809de6313..b5081da47 100644 --- a/src/test/scala/temple/builder/BuilderTestData.scala +++ b/src/test/scala/temple/builder/BuilderTestData.scala @@ -52,7 +52,7 @@ object BuilderTestData { "Test" -> StructBlock( ListMap( "favouriteColour" -> Attribute(StringType(), valueAnnotations = Set(Annotation.Unique)), - "bedTime" -> Attribute(TimeType), + "bedTime" -> Attribute(TimeType, valueAnnotations = Set(Annotation.Nullable)), "favouriteNumber" -> Attribute(IntType(max = Some(10), min = Some(0))), ), ), diff --git a/src/test/scala/temple/builder/DatabaseBuilderTestData.scala b/src/test/scala/temple/builder/DatabaseBuilderTestData.scala index 8e91acde0..c9a82d2bf 100644 --- a/src/test/scala/temple/builder/DatabaseBuilderTestData.scala +++ b/src/test/scala/temple/builder/DatabaseBuilderTestData.scala @@ -68,7 +68,7 @@ object DatabaseBuilderTestData { "test", Seq( ColumnDef("favourite_colour", StringCol, Seq(Unique, NonNull)), - ColumnDef("bed_time", TimeCol, Seq(NonNull)), + ColumnDef("bed_time", TimeCol), ColumnDef( "favourite_number", IntCol(4), diff --git a/src/test/scala/temple/testfiles/simple-dc.temple b/src/test/scala/temple/testfiles/simple-dc.temple index c8f9e04ab..0052923e2 100644 --- a/src/test/scala/temple/testfiles/simple-dc.temple +++ b/src/test/scala/temple/testfiles/simple-dc.temple @@ -17,7 +17,7 @@ User: service { breakfastTime: time; Fred: struct { - field: string; + field: string @nullable; friend: User; image: data(10M); #enumerable; diff --git a/src/test/scala/temple/testfiles/simple.temple b/src/test/scala/temple/testfiles/simple.temple index 2ab4c4097..5b476c8b6 100644 --- a/src/test/scala/temple/testfiles/simple.temple +++ b/src/test/scala/temple/testfiles/simple.temple @@ -17,7 +17,7 @@ User: service { breakfastTime: time; Fred: struct { - field: string; + field: string @nullable; friend: User; image: data(10M); #enumerable;