Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Introduce unique annotation #273

Merged
merged 13 commits into from
May 3, 2023
11 changes: 9 additions & 2 deletions grammar/TypeQL.g4
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,16 @@ 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
| REGEX STRING_
| TYPE label_any
;

annotations_owns : ( ANNOTATION_KEY )? ( ANNOTATION_UNIQUE )? ;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@krishnangovindraj raised good point about order indendence:

OWNS type ( AS type )? (annotation_owns*)

annotation_owns = ANNOTATION_KEY | ANNOTATION_UNIQUE | ANNOTATION_CARD ...

note that now each one is optional independently and the rule is written in singular form.


// THING VARIABLES =============================================================

variable_things : ( variable_thing_any ';' )+ ;
Expand Down Expand Up @@ -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 ;
Expand Down
27 changes: 26 additions & 1 deletion java/common/TypeQLToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public enum Char {
SPACE(" "),
COMMA(","),
COMMA_SPACE(", "),
AT("@"),
COMMA_NEW_LINE(",\n"),
CURLY_OPEN("{"),
CURLY_CLOSE("}"),
Expand Down Expand Up @@ -316,7 +317,6 @@ public enum Constraint {
HAS("has"),
IID("iid"),
IS("is"),
IS_KEY("@key"),
ISA("isa"),
ISAX("isa!"),
OWNS("owns"),
Expand Down Expand Up @@ -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");
Expand Down
3 changes: 2 additions & 1 deletion java/common/exception/ErrorMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
28 changes: 26 additions & 2 deletions java/parser/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, UnboundVariable> 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<String, UnboundVariable> overridden = constraint.AS() == null ? null : visitType(constraint.type(0));
type = type.constrain(new TypeConstraint.Plays(visitType_scoped(constraint.type_scoped()), overridden));
Expand All @@ -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
Expand Down
12 changes: 10 additions & 2 deletions java/parser/test/ParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
78 changes: 48 additions & 30 deletions java/pattern/constraint/TypeConstraint.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -107,6 +110,10 @@ public boolean isRelates() {
return false;
}

public List<Annotation> annotations() {
return Collections.emptyList();
}

public TypeConstraint.Label asLabel() {
throw TypeQLException.of(INVALID_CASTING.message(className(this.getClass()), className(Label.class)));
}
Expand Down Expand Up @@ -218,7 +225,7 @@ public Sub(UnboundVariable typeVar, boolean isExplicit) {

public Sub(Either<Pair<String, String>, 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) {
Expand Down Expand Up @@ -399,46 +406,57 @@ public int hashCode() {

public static class Owns extends TypeConstraint {

private static final Set<Annotation> VALID_ANNOTATIONS = set(Annotation.KEY, Annotation.UNIQUE);

private final TypeVariable attributeType;
private final TypeVariable overriddenAttributeType;
private final boolean isKey;
private final List<Annotation> 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<String, UnboundVariable> attributeTypeArg, Either<String, UnboundVariable> overriddenAttributeTypeArg, boolean isKey) {
public Owns(Either<String, UnboundVariable> attributeTypeArg, Either<String, UnboundVariable> 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() {
Expand All @@ -449,10 +467,6 @@ public Optional<TypeVariable> overridden() {
return Optional.ofNullable(overriddenAttributeType);
}

public boolean isKey() {
return isKey;
}

@Override
public Set<TypeVariable> variables() {
return overriddenAttributeType == null
Expand All @@ -465,16 +479,20 @@ public boolean isOwns() {
return true;
}

@Override
public TypeConstraint.Owns asOwns() {
return this;
}

@Override
public List<Annotation> 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
Expand All @@ -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
Expand Down Expand Up @@ -526,7 +544,7 @@ public Plays(UnboundVariable roleTypeVar, UnboundVariable overriddenRoleTypeVar)

public Plays(Either<Pair<String, String>, UnboundVariable> roleTypeArg, Either<String, UnboundVariable> 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) {
Expand Down Expand Up @@ -631,7 +649,7 @@ public Relates(UnboundVariable roleTypeVar, UnboundVariable overriddenRoleTypeVa

public Relates(Either<String, UnboundVariable> roleTypeArg, Either<String, UnboundVariable> 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) {
Expand Down
Loading