diff --git a/grammar/TypeQL.g4 b/grammar/TypeQL.g4 index 37e578644..2cbf344d9 100644 --- a/grammar/TypeQL.g4 +++ b/grammar/TypeQL.g4 @@ -113,7 +113,7 @@ variable_concept : VAR_ IS VAR_ ; variable_type : type_any type_constraint ( ',' type_constraint )* ; type_constraint : ABSTRACT | SUB_ type_any - | OWNS type ( AS type )? ( IS_KEY )? + | OWNS type ( AS type )? annotations_owns | RELATES type ( AS type )? | PLAYS type_scoped ( AS type )? | VALUE value_type @@ -121,6 +121,8 @@ type_constraint : ABSTRACT | TYPE label_any ; +annotations_owns : ( ANNOTATION_KEY )? ( ANNOTATION_UNIQUE )? ; + // THING VARIABLES ============================================================= variable_things : ( variable_thing_any ';' )+ ; @@ -225,11 +227,16 @@ ASC : 'asc' ; DESC : 'desc' ; TYPE : 'type' ; ABSTRACT : 'abstract' ; SUB_ : SUB | SUBX ; SUB : 'sub' ; SUBX : 'sub!' ; -OWNS : 'owns' ; IS_KEY : '@key' ; +OWNS : 'owns' ; REGEX : 'regex' ; AS : 'as' ; PLAYS : 'plays' ; RELATES : 'relates' ; WHEN : 'when' ; THEN : 'then' ; +// TYPE ANNOTATIONS + +ANNOTATION_KEY : '@key'; +ANNOTATION_UNIQUE : '@unique'; + // THING VARIABLE CONSTRAINT KEYWORDS IID : 'iid' ; ISA_ : ISA | ISAX ; diff --git a/java/common/TypeQLToken.java b/java/common/TypeQLToken.java index f91282913..19e9f6c1f 100644 --- a/java/common/TypeQLToken.java +++ b/java/common/TypeQLToken.java @@ -125,6 +125,7 @@ public enum Char { SPACE(" "), COMMA(","), COMMA_SPACE(", "), + AT("@"), COMMA_NEW_LINE(",\n"), CURLY_OPEN("{"), CURLY_CLOSE("}"), @@ -316,7 +317,6 @@ public enum Constraint { HAS("has"), IID("iid"), IS("is"), - IS_KEY("@key"), ISA("isa"), ISAX("isa!"), OWNS("owns"), @@ -350,6 +350,31 @@ public static Constraint of(String value) { } } + public enum Annotation { + KEY("key"), + UNIQUE("unique"); + + private final String name; + + Annotation(String name) { + this.name = name; + } + + @Override + public String toString() { + return Char.AT + name; + } + + public static Annotation of(String value) { + for (Annotation annotation: Annotation.values()) { + if (annotation.name.equals(value)) { + return annotation; + } + } + return null; + } + } + public enum Literal { TRUE("true"), FALSE("false"); diff --git a/java/common/exception/ErrorMessage.java b/java/common/exception/ErrorMessage.java index f5256df90..47a730de2 100644 --- a/java/common/exception/ErrorMessage.java +++ b/java/common/exception/ErrorMessage.java @@ -101,7 +101,8 @@ public class ErrorMessage extends com.vaticle.typedb.common.exception.ErrorMessa new ErrorMessage(39, "Illegal grammar!"); public static final ErrorMessage ILLEGAL_CHAR_IN_LABEL = new ErrorMessage(40, "'%s' is not a valid Type label. Type labels must start with a letter, and may contain only letters, numbers, '-' and '_'."); - + public static final ErrorMessage INVALID_ANNOTATION = + new ErrorMessage(41, "Invalid annotation '%s' on '%s' constraint"); private static final String codePrefix = "TQL"; private static final String messagePrefix = "TypeQL Error"; diff --git a/java/parser/Parser.java b/java/parser/Parser.java index 16c2ada06..02eeae417 100644 --- a/java/parser/Parser.java +++ b/java/parser/Parser.java @@ -56,7 +56,6 @@ import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.DefaultErrorStrategy; import org.antlr.v4.runtime.ParserRuleContext; -import org.antlr.v4.runtime.RuleContext; import org.antlr.v4.runtime.atn.PredictionMode; import org.antlr.v4.runtime.misc.ParseCancellationException; import org.antlr.v4.runtime.tree.TerminalNode; @@ -491,7 +490,7 @@ public TypeVariable visitVariable_type(TypeQLParser.Variable_typeContext ctx) { type = type.constrain(new TypeConstraint.Sub(visitType_any(constraint.type_any()), sub == TypeQLToken.Constraint.SUBX)); } else if (constraint.OWNS() != null) { Either overridden = constraint.AS() == null ? null : visitType(constraint.type(1)); - type = type.constrain(new TypeConstraint.Owns(visitType(constraint.type(0)), overridden, constraint.IS_KEY() != null)); + type = type.constrain(new TypeConstraint.Owns(visitType(constraint.type(0)), overridden, visitAnnotations_owns(constraint.annotations_owns()))); } else if (constraint.PLAYS() != null) { Either overridden = constraint.AS() == null ? null : visitType(constraint.type(0)); type = type.constrain(new TypeConstraint.Plays(visitType_scoped(constraint.type_scoped()), overridden)); @@ -513,6 +512,31 @@ public TypeVariable visitVariable_type(TypeQLParser.Variable_typeContext ctx) { return type; } + @Override + public TypeQLToken.Annotation[] visitAnnotations_owns(TypeQLParser.Annotations_ownsContext ctx) { + // precompute array length to avoid double allocation of arrays + int count = 0; + if (ctx.ANNOTATION_KEY() != null) count++; + if (ctx.ANNOTATION_UNIQUE() != null) count++; + TypeQLToken.Annotation[] annotations = new TypeQLToken.Annotation[count]; + int index = 0; + if (ctx.ANNOTATION_KEY() != null) { + annotations[index] = parseAnnotation(ctx.ANNOTATION_KEY()); + index++; + } + if (ctx.ANNOTATION_UNIQUE() != null) { + annotations[index] = parseAnnotation(ctx.ANNOTATION_UNIQUE()); + index++; + } + assert index == count; + return annotations; + } + + private TypeQLToken.Annotation parseAnnotation(TerminalNode terminalNode) { + assert !terminalNode.getText().isEmpty() && terminalNode.getText().startsWith(TypeQLToken.Char.AT.toString()); + return TypeQLToken.Annotation.of(terminalNode.getText().substring(1)); + } + // THING VARIABLES ========================================================= @Override diff --git a/java/parser/test/ParserTest.java b/java/parser/test/ParserTest.java index b7ce0ab9c..e4c857e08 100644 --- a/java/parser/test/ParserTest.java +++ b/java/parser/test/ParserTest.java @@ -23,6 +23,7 @@ import com.vaticle.typeql.lang.TypeQL; import com.vaticle.typeql.lang.common.TypeQLArg; +import com.vaticle.typeql.lang.common.TypeQLToken; import com.vaticle.typeql.lang.common.exception.TypeQLException; import com.vaticle.typeql.lang.pattern.Conjunction; import com.vaticle.typeql.lang.pattern.Pattern; @@ -1143,8 +1144,15 @@ public void testTypeQLParseQuery() { } @Test - public void testParseKey() { - assertEquals("match\n$x owns name @key;\nget $x;", parseQuery("match\n$x owns name @key;\nget $x;").toString()); + public void testParseAnnotations() { + final String defineString = "define\n" + + "e1 owns a1 @key;\n" + + "e2 owns a2 @unique;"; + assertEquals( + TypeQL.define( + type("e1").owns("a1", TypeQLToken.Annotation.KEY), + type("e2").owns("a2", TypeQLToken.Annotation.UNIQUE)), + parseQuery(defineString)); } @Test diff --git a/java/pattern/constraint/TypeConstraint.java b/java/pattern/constraint/TypeConstraint.java index 2052c6d75..d61410fd3 100644 --- a/java/pattern/constraint/TypeConstraint.java +++ b/java/pattern/constraint/TypeConstraint.java @@ -25,12 +25,15 @@ import com.vaticle.typedb.common.collection.Pair; import com.vaticle.typeql.lang.common.TypeQLArg; import com.vaticle.typeql.lang.common.TypeQLToken; +import com.vaticle.typeql.lang.common.TypeQLToken.Annotation; import com.vaticle.typeql.lang.common.exception.TypeQLException; import com.vaticle.typeql.lang.pattern.variable.TypeVariable; import com.vaticle.typeql.lang.pattern.variable.UnboundVariable; import javax.annotation.Nullable; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -41,7 +44,6 @@ import static com.vaticle.typeql.lang.common.TypeQLToken.Char.COLON; import static com.vaticle.typeql.lang.common.TypeQLToken.Char.SPACE; import static com.vaticle.typeql.lang.common.TypeQLToken.Constraint.AS; -import static com.vaticle.typeql.lang.common.TypeQLToken.Constraint.IS_KEY; import static com.vaticle.typeql.lang.common.TypeQLToken.Constraint.OWNS; import static com.vaticle.typeql.lang.common.TypeQLToken.Constraint.PLAYS; import static com.vaticle.typeql.lang.common.TypeQLToken.Constraint.REGEX; @@ -51,6 +53,7 @@ import static com.vaticle.typeql.lang.common.TypeQLToken.Constraint.TYPE; import static com.vaticle.typeql.lang.common.TypeQLToken.Constraint.VALUE_TYPE; import static com.vaticle.typeql.lang.common.TypeQLToken.Type.RELATION; +import static com.vaticle.typeql.lang.common.exception.ErrorMessage.INVALID_ANNOTATION; import static com.vaticle.typeql.lang.common.exception.ErrorMessage.INVALID_ATTRIBUTE_TYPE_REGEX; import static com.vaticle.typeql.lang.common.exception.ErrorMessage.INVALID_CASTING; import static com.vaticle.typeql.lang.common.exception.ErrorMessage.MISSING_PATTERNS; @@ -107,6 +110,10 @@ public boolean isRelates() { return false; } + public List annotations() { + return Collections.emptyList(); + } + public TypeConstraint.Label asLabel() { throw TypeQLException.of(INVALID_CASTING.message(className(this.getClass()), className(Label.class))); } @@ -218,7 +225,7 @@ public Sub(UnboundVariable typeVar, boolean isExplicit) { public Sub(Either, UnboundVariable> typeArg, boolean isExplicit) { this(typeArg.apply(scoped -> hidden().constrain(new TypeConstraint.Label(scoped.first(), scoped.second())), - UnboundVariable::toType), isExplicit); + UnboundVariable::toType), isExplicit); } private Sub(TypeVariable type, boolean isExplicit) { @@ -399,46 +406,57 @@ public int hashCode() { public static class Owns extends TypeConstraint { + private static final Set VALID_ANNOTATIONS = set(Annotation.KEY, Annotation.UNIQUE); + private final TypeVariable attributeType; private final TypeVariable overriddenAttributeType; - private final boolean isKey; + private final List annotations; private final int hash; - public Owns(String attributeType, boolean isKey) { - this(hidden().type(attributeType), null, isKey); + public Owns(String attributeType, Annotation... annotations) { + this(hidden().type(attributeType), null, annotations); } - public Owns(UnboundVariable attributeTypeVar, boolean isKey) { - this(attributeTypeVar.toType(), null, isKey); + public Owns(UnboundVariable attributeTypeVar, Annotation... annotations) { + this(attributeTypeVar.toType(), null, annotations); } - public Owns(String attributeType, String overriddenAttributeType, boolean isKey) { - this(hidden().type(attributeType), overriddenAttributeType == null ? null : hidden().type(overriddenAttributeType), isKey); + public Owns(String attributeType, String overriddenAttributeType, Annotation... annotations) { + this(hidden().type(attributeType), overriddenAttributeType == null ? null : hidden().type(overriddenAttributeType), annotations); } - public Owns(UnboundVariable attributeTypeVar, String overriddenAttributeType, boolean isKey) { - this(attributeTypeVar.toType(), overriddenAttributeType == null ? null : hidden().type(overriddenAttributeType), isKey); + public Owns(UnboundVariable attributeTypeVar, String overriddenAttributeType, Annotation... annotations) { + this(attributeTypeVar.toType(), overriddenAttributeType == null ? null : hidden().type(overriddenAttributeType), annotations); } - public Owns(String attributeType, UnboundVariable overriddenAttributeTypeVar, boolean isKey) { - this(hidden().type(attributeType), overriddenAttributeTypeVar == null ? null : overriddenAttributeTypeVar.toType(), isKey); + public Owns(String attributeType, UnboundVariable overriddenAttributeTypeVar, Annotation... annotations) { + this(hidden().type(attributeType), overriddenAttributeTypeVar == null ? null : overriddenAttributeTypeVar.toType(), annotations); } - public Owns(UnboundVariable attributeTypeVar, UnboundVariable overriddenAttributeTypeVar, boolean isKey) { - this(attributeTypeVar.toType(), overriddenAttributeTypeVar == null ? null : overriddenAttributeTypeVar.toType(), isKey); + public Owns(UnboundVariable attributeTypeVar, UnboundVariable overriddenAttributeTypeVar, Annotation... annotations) { + this(attributeTypeVar.toType(), overriddenAttributeTypeVar == null ? null : overriddenAttributeTypeVar.toType(), annotations); } - public Owns(Either attributeTypeArg, Either overriddenAttributeTypeArg, boolean isKey) { + public Owns(Either attributeTypeArg, Either overriddenAttributeTypeArg, Annotation... annotations) { this(attributeTypeArg.apply(label -> hidden().type(label), UnboundVariable::toType), - overriddenAttributeTypeArg == null ? null : overriddenAttributeTypeArg.apply(label -> hidden().type(label), UnboundVariable::toType), - isKey); + overriddenAttributeTypeArg == null ? null : overriddenAttributeTypeArg.apply(label -> hidden().type(label), UnboundVariable::toType), + annotations); } - private Owns(TypeVariable attributeType, @Nullable TypeVariable overriddenAttributeType, boolean isKey) { + private Owns(TypeVariable attributeType, @Nullable TypeVariable overriddenAttributeType, Annotation... annotations) { this.attributeType = attributeType; this.overriddenAttributeType = overriddenAttributeType; - this.isKey = isKey; - this.hash = Objects.hash(Owns.class, this.attributeType, this.overriddenAttributeType, this.isKey); + validateAnnotations(annotations); + this.annotations = List.of(annotations); + this.hash = Objects.hash(Owns.class, this.attributeType, this.overriddenAttributeType, this.annotations); + } + + private static void validateAnnotations(Annotation[] annotations) { + for (Annotation annotation : annotations) { + if (!VALID_ANNOTATIONS.contains(annotation)) { + throw TypeQLException.of(INVALID_ANNOTATION.message(annotation, "owns")); + } + } } public TypeVariable attribute() { @@ -449,10 +467,6 @@ public Optional overridden() { return Optional.ofNullable(overriddenAttributeType); } - public boolean isKey() { - return isKey; - } - @Override public Set variables() { return overriddenAttributeType == null @@ -465,16 +479,20 @@ public boolean isOwns() { return true; } - @Override public TypeConstraint.Owns asOwns() { return this; } + @Override + public List annotations() { + return annotations; + } + @Override public String toString() { return "" + OWNS + SPACE + attributeType + (overriddenAttributeType != null ? "" + SPACE + AS + SPACE + overriddenAttributeType : "") + - (isKey ? "" + SPACE + IS_KEY : ""); + (!annotations.isEmpty() ? SPACE + annotations.stream().map(Annotation::toString).collect(SPACE.joiner()) : ""); } @Override @@ -484,7 +502,7 @@ public boolean equals(Object o) { Owns that = (Owns) o; return (this.attributeType.equals(that.attributeType) && Objects.equals(this.overriddenAttributeType, that.overriddenAttributeType) && - this.isKey == that.isKey); + this.annotations.equals(that.annotations)); } @Override @@ -526,7 +544,7 @@ public Plays(UnboundVariable roleTypeVar, UnboundVariable overriddenRoleTypeVar) public Plays(Either, UnboundVariable> roleTypeArg, Either overriddenRoleTypeArg) { this(roleTypeArg.apply(scoped -> hidden().constrain(new TypeConstraint.Label(scoped.first(), scoped.second())), UnboundVariable::toType), - overriddenRoleTypeArg == null ? null : overriddenRoleTypeArg.apply(Plays::scopedType, UnboundVariable::toType)); + overriddenRoleTypeArg == null ? null : overriddenRoleTypeArg.apply(Plays::scopedType, UnboundVariable::toType)); } private Plays(TypeVariable roleType, @Nullable TypeVariable overriddenRoleType) { @@ -631,7 +649,7 @@ public Relates(UnboundVariable roleTypeVar, UnboundVariable overriddenRoleTypeVa public Relates(Either roleTypeArg, Either overriddenRoleTypeArg) { this(roleTypeArg.apply(Relates::scopedType, UnboundVariable::toType), - overriddenRoleTypeArg == null ? null : overriddenRoleTypeArg.apply(Relates::scopedType, UnboundVariable::toType)); + overriddenRoleTypeArg == null ? null : overriddenRoleTypeArg.apply(Relates::scopedType, UnboundVariable::toType)); } private Relates(TypeVariable roleType, @Nullable TypeVariable overriddenRoleType) { diff --git a/java/pattern/variable/builder/TypeVariableBuilder.java b/java/pattern/variable/builder/TypeVariableBuilder.java index fc8d9e693..8f98ea439 100644 --- a/java/pattern/variable/builder/TypeVariableBuilder.java +++ b/java/pattern/variable/builder/TypeVariableBuilder.java @@ -77,52 +77,29 @@ default TypeVariable subX(UnboundVariable typeVar) { return constrain(new TypeConstraint.Sub(typeVar, true)); } - default TypeVariable owns(String attributeType) { - return constrain(new TypeConstraint.Owns(attributeType, false)); + default TypeVariable owns(String attributeType, TypeQLToken.Annotation... annotations) { + return constrain(new TypeConstraint.Owns(attributeType, annotations)); } - default TypeVariable owns(String attributeType, boolean isKey) { - return constrain(new TypeConstraint.Owns(attributeType, isKey)); + default TypeVariable owns(UnboundVariable attributeTypeVar, TypeQLToken.Annotation... annotations) { + return constrain(new TypeConstraint.Owns(attributeTypeVar, annotations)); } - default TypeVariable owns(UnboundVariable attributeTypeVar) { - return constrain(new TypeConstraint.Owns(attributeTypeVar, false)); - } - - default TypeVariable owns(UnboundVariable attributeTypeVar, boolean isKey) { - return constrain(new TypeConstraint.Owns(attributeTypeVar, isKey)); - } - - default TypeVariable owns(String attributeType, String overriddenAttributeType) { - return constrain(new TypeConstraint.Owns(attributeType, overriddenAttributeType, false)); - } - - default TypeVariable owns(String attributeType, String overriddenAttributeType, boolean isKey) { - return constrain(new TypeConstraint.Owns(attributeType, overriddenAttributeType, isKey)); - } - - default TypeVariable owns(String attributeType, UnboundVariable overriddenAttributeTypeVar) { - return constrain(new TypeConstraint.Owns(attributeType, overriddenAttributeTypeVar, false)); - } - - default TypeVariable owns(String attributeType, UnboundVariable overriddenAttributeTypeVar, boolean isKey) { - return constrain(new TypeConstraint.Owns(attributeType, overriddenAttributeTypeVar, isKey)); - } - default TypeVariable owns(UnboundVariable attributeTypeVar, String overriddenAttributeType) { - return constrain(new TypeConstraint.Owns(attributeTypeVar, overriddenAttributeType, false)); + default TypeVariable owns(String attributeType, String overriddenAttributeType, TypeQLToken.Annotation... annotations) { + return constrain(new TypeConstraint.Owns(attributeType, overriddenAttributeType, annotations)); } - default TypeVariable owns(UnboundVariable attributeTypeVar, String overriddenAttributeType, boolean isKey) { - return constrain(new TypeConstraint.Owns(attributeTypeVar, overriddenAttributeType, isKey)); + default TypeVariable owns(String attributeType, UnboundVariable overriddenAttributeTypeVar, TypeQLToken.Annotation... annotations) { + return constrain(new TypeConstraint.Owns(attributeType, overriddenAttributeTypeVar, annotations)); } - default TypeVariable owns(UnboundVariable attributeTypeVar, UnboundVariable overriddenAttributeTypeVar) { - return constrain(new TypeConstraint.Owns(attributeTypeVar, overriddenAttributeTypeVar, false)); + default TypeVariable owns(UnboundVariable attributeTypeVar, String overriddenAttributeType, TypeQLToken.Annotation... annotations) { + return constrain(new TypeConstraint.Owns(attributeTypeVar, overriddenAttributeType, annotations)); } - default TypeVariable owns(UnboundVariable attributeTypeVar, UnboundVariable overriddenAttributeTypeVar, boolean isKey) { - return constrain(new TypeConstraint.Owns(attributeTypeVar, overriddenAttributeTypeVar, isKey)); + default TypeVariable owns(UnboundVariable attributeTypeVar, UnboundVariable overriddenAttributeTypeVar, TypeQLToken.Annotation... annotations) { + return constrain(new TypeConstraint.Owns(attributeTypeVar, overriddenAttributeTypeVar, annotations)); } default TypeVariable plays(String relationType, String roleType) {