From 6155300deed4c1eeea83c5f98fce5f291b928865 Mon Sep 17 00:00:00 2001 From: Joshua Send Date: Fri, 26 May 2023 19:53:36 +0100 Subject: [PATCH] Introduce Values to support expressions (#296) ## What is the goal of this PR? Introduce the 'Value' type, which is returned as the result of an expression's computation. This change follows from https://github.com/vaticle/typeql/pull/260, which outlines the capabilities of the new expression syntax. Values (representing any of Long, Double, Boolean, String, or DateTime) are returned as part of `ConceptMap` answers and are subtypes of `Concept` for the time being. Their main API is made of the `.get_value()` method and `.get_value_vype()` method, along with all the standard safe downcasting methods to convert a `Concept` into a `Value`, using `Concept.is_value()` and `Concept.as_value()`. We also move the import location of `ValueType` from `attribute_type.py` to `concept.py`. ## What are the changes implemented in this PR? * Introduces the `Value` concept and the required `ValueImpl` that implements it * Refactor `ValueType` to no longer live within `AttributeType` - now it exists in `Concept.ValueType` * Updates the test framework for tests involving values, including the new `ExpressionTest` behaviour scenarios, which we also add to CI --- .factory/automation.yml | 2 + dependencies/vaticle/artifacts.bzl | 4 +- dependencies/vaticle/repositories.bzl | 6 +- requirements.txt | 2 +- .../thing/attribute/attribute_steps.py | 2 +- .../attributetype/attribute_type_steps.py | 74 ++++---- tests/behaviour/config/parameters.py | 12 +- .../typeql/language/expression/BUILD | 55 ++++++ tests/behaviour/typeql/typeql_steps.py | 159 +++++++++++++----- tests/integration/test_typedb.py | 6 +- typedb/api/concept/concept.py | 24 +++ typedb/api/concept/concept_manager.py | 3 +- typedb/api/concept/type/attribute_type.py | 44 ++--- typedb/api/concept/type/thing_type.py | 5 +- typedb/api/concept/value/value.py | 122 ++++++++++++++ typedb/common/exception.py | 7 +- typedb/common/rpc/request_builder.py | 43 +---- typedb/concept/concept_manager.py | 7 +- typedb/concept/proto/concept_proto_builder.py | 20 +-- typedb/concept/proto/concept_proto_reader.py | 45 +++-- typedb/concept/type/attribute_type.py | 28 +-- typedb/concept/type/thing_type.py | 5 +- typedb/concept/value/value.py | 120 +++++++++++++ 23 files changed, 588 insertions(+), 207 deletions(-) create mode 100644 tests/behaviour/typeql/language/expression/BUILD create mode 100644 typedb/api/concept/value/value.py create mode 100644 typedb/concept/value/value.py diff --git a/.factory/automation.yml b/.factory/automation.yml index 07de8a4a..305b1c4a 100644 --- a/.factory/automation.yml +++ b/.factory/automation.yml @@ -121,6 +121,7 @@ build: bazel run @vaticle_dependencies//distribution/artifact:create-netrc .factory/test-core.sh //tests/behaviour/typeql/language/match/... --test_output=errors .factory/test-core.sh //tests/behaviour/typeql/language/get/... --test_output=errors + .factory/test-core.sh //tests/behaviour/typeql/language/expression/... --test_output=errors test-behaviour-match-cluster: image: vaticle-ubuntu-22.04 type: foreground @@ -135,6 +136,7 @@ build: bazel run @vaticle_dependencies//distribution/artifact:create-netrc .factory/test-cluster.sh //tests/behaviour/typeql/language/match/... --test_output=errors .factory/test-cluster.sh //tests/behaviour/typeql/language/get/... --test_output=errors + .factory/test-cluster.sh //tests/behaviour/typeql/language/expression/... --test_output=errors test-behaviour-writable-core: image: vaticle-ubuntu-22.04 type: foreground diff --git a/dependencies/vaticle/artifacts.bzl b/dependencies/vaticle/artifacts.bzl index d8f2ac59..2ca1f579 100644 --- a/dependencies/vaticle/artifacts.bzl +++ b/dependencies/vaticle/artifacts.bzl @@ -29,7 +29,7 @@ def vaticle_typedb_artifacts(): artifact_name = "typedb-server-{platform}-{version}.{ext}", tag_source = deployment["artifact.release"], commit_source = deployment["artifact.snapshot"], - commit = "0f43f6654dff206d168711d90785d12df4c98b5e", + commit = "cb119c536c44275f58df7b54da033f8fe076cac5", ) def vaticle_typedb_cluster_artifacts(): @@ -39,5 +39,5 @@ def vaticle_typedb_cluster_artifacts(): artifact_name = "typedb-cluster-all-{platform}-{version}.{ext}", tag_source = deployment_private["artifact.release"], commit_source = deployment_private["artifact.snapshot"], - commit = "9b70d31b54ed0162ce5df6ac4ab6180229bd7cb9", + commit = "8044753056dd20b8e6bec6f62036eb0003d4ba06", ) diff --git a/dependencies/vaticle/repositories.bzl b/dependencies/vaticle/repositories.bzl index 27653f2d..11b253dc 100644 --- a/dependencies/vaticle/repositories.bzl +++ b/dependencies/vaticle/repositories.bzl @@ -25,19 +25,19 @@ def vaticle_dependencies(): git_repository( name = "vaticle_dependencies", remote = "https://github.com/vaticle/dependencies", - commit = "e0446ffa70cacca89cb1faa2cd6f299c3784d845", # sync-marker: do not remove this comment, this is used for sync-dependencies by @vaticle_dependencies + commit = "385716283e1e64245c3679a06054e271a0608ac1", # sync-marker: do not remove this comment, this is used for sync-dependencies by @vaticle_dependencies ) def vaticle_typedb_common(): git_repository( name = "vaticle_typedb_common", remote = "https://github.com/vaticle/typedb-common", - tag = "2.17.0" # sync-marker: do not remove this comment, this is used for sync-dependencies by @vaticle_typedb_common + commit = "9372dfb227d54c6eb631eed02e67f250e55e657e" # sync-marker: do not remove this comment, this is used for sync-dependencies by @vaticle_typedb_common ) def vaticle_typedb_behaviour(): git_repository( name = "vaticle_typedb_behaviour", remote = "https://github.com/vaticle/typedb-behaviour", - commit = "aa675d9052046b1a4ffd45f444854d8735028702" # sync-marker: do not remove this comment, this is used for sync-dependencies by @vaticle_typedb_behaviour + commit = "767bf98fef7383addf42a1ae6e97a44874bb4f0b" # sync-marker: do not remove this comment, this is used for sync-dependencies by @vaticle_typedb_behaviour ) diff --git a/requirements.txt b/requirements.txt index 169e97a2..076ed51b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ ## Dependencies -typedb-protocol==2.18.0.dev0 +typedb-protocol==2.18.0.dev2 grpcio>=1.43.0,<2 protobuf>=3.15.6,<4 parse==1.18.0 diff --git a/tests/behaviour/concept/thing/attribute/attribute_steps.py b/tests/behaviour/concept/thing/attribute/attribute_steps.py index 75696c33..5ca524a3 100644 --- a/tests/behaviour/concept/thing/attribute/attribute_steps.py +++ b/tests/behaviour/concept/thing/attribute/attribute_steps.py @@ -47,7 +47,7 @@ def step_impl(context: Context, var1: str, var2: str): @step("attribute {var:Var} has value type: {value_type:ValueType}") -def step_impl(context: Context, var: str, value_type: AttributeType.ValueType): +def step_impl(context: Context, var: str, value_type: ValueType): assert_that(context.get(var).as_attribute().get_type().get_value_type(), is_(value_type)) diff --git a/tests/behaviour/concept/type/attributetype/attribute_type_steps.py b/tests/behaviour/concept/type/attributetype/attribute_type_steps.py index 3127cbc5..5e687cbe 100644 --- a/tests/behaviour/concept/type/attributetype/attribute_type_steps.py +++ b/tests/behaviour/concept/type/attributetype/attribute_type_steps.py @@ -21,94 +21,90 @@ from behave import * from hamcrest import * -from typedb.client import * from tests.behaviour.config.parameters import parse_value_type, parse_list, parse_label from tests.behaviour.context import Context +from typedb.client import * -@step("put attribute type: {type_label}, with value type: {value_type}") -def step_impl(context: Context, type_label: str, value_type: str): - context.tx().concepts().put_attribute_type(type_label, parse_value_type(value_type)) +@step("put attribute type: {type_label}, with value type: {value_type:ValueType}") +def step_impl(context: Context, type_label: str, value_type: ValueType): + context.tx().concepts().put_attribute_type(type_label, value_type) -@step("attribute({type_label}) get value type: {value_type}") -def step_impl(context: Context, type_label: str, value_type: str): +@step("attribute({type_label}) get value type: {value_type:ValueType}") +def step_impl(context: Context, type_label: str, value_type: ValueType): assert_that(context.tx().concepts().get_attribute_type(type_label).get_value_type(), - is_(parse_value_type(value_type))) + is_(value_type)) -@step("attribute({type_label}) get supertype value type: {value_type}") -def step_impl(context: Context, type_label: str, value_type: str): +@step("attribute({type_label}) get supertype value type: {value_type:ValueType}") +def step_impl(context: Context, type_label: str, value_type: ValueType): supertype = context.tx().concepts().get_attribute_type(type_label).as_remote( context.tx()).get_supertype().as_attribute_type() - assert_that(supertype.get_value_type(), is_(parse_value_type(value_type))) + assert_that(supertype.get_value_type(), is_(value_type)) -def attribute_type_as_value_type(context: Context, type_label: str, value_type: AttributeType.ValueType): +def attribute_type_as_value_type(context: Context, type_label: str, value_type: ValueType): attribute_type = context.tx().concepts().get_attribute_type(type_label) - if value_type is AttributeType.ValueType.OBJECT: + if value_type is ValueType.OBJECT: return attribute_type - elif value_type is AttributeType.ValueType.BOOLEAN: + elif value_type is ValueType.BOOLEAN: return attribute_type.as_boolean() - elif value_type is AttributeType.ValueType.LONG: + elif value_type is ValueType.LONG: return attribute_type.as_long() - elif value_type is AttributeType.ValueType.DOUBLE: + elif value_type is ValueType.DOUBLE: return attribute_type.as_double() - elif value_type is AttributeType.ValueType.STRING: + elif value_type is ValueType.STRING: return attribute_type.as_string() - elif value_type is AttributeType.ValueType.DATETIME: + elif value_type is ValueType.DATETIME: return attribute_type.as_datetime() else: raise ValueError("Unrecognised value type: " + str(value_type)) -@step("attribute({type_label}) as({value_type}) get subtypes contain") -def step_impl(context: Context, type_label: str, value_type: str): +@step("attribute({type_label}) as({value_type:ValueType}) get subtypes contain") +def step_impl(context: Context, type_label: str, value_type: ValueType): sub_labels = [parse_label(s) for s in parse_list(context.table)] - attribute_type = attribute_type_as_value_type(context, type_label, parse_value_type(value_type)) + attribute_type = attribute_type_as_value_type(context, type_label, value_type) actuals = list(map(lambda tt: tt.get_label(), attribute_type.as_remote(context.tx()).get_subtypes())) for sub_label in sub_labels: assert_that(sub_label, is_in(actuals)) -@step("attribute({type_label}) as({value_type}) get subtypes do not contain") -def step_impl(context: Context, type_label: str, value_type: str): +@step("attribute({type_label}) as({value_type:ValueType}) get subtypes do not contain") +def step_impl(context: Context, type_label: str, value_type: ValueType): sub_labels = [parse_label(s) for s in parse_list(context.table)] - attribute_type = attribute_type_as_value_type(context, type_label, parse_value_type(value_type)) + attribute_type = attribute_type_as_value_type(context, type_label, value_type) actuals = list(map(lambda tt: tt.get_label(), attribute_type.as_remote(context.tx()).get_subtypes())) for sub_label in sub_labels: assert_that(sub_label, not_(is_in(actuals))) -@step("attribute({type_label}) as({value_type}) set regex: {regex}") -def step_impl(context: Context, type_label: str, value_type, regex: str): - value_type = parse_value_type(value_type) - assert_that(value_type, is_(AttributeType.ValueType.STRING)) +@step("attribute({type_label}) as({value_type:ValueType}) set regex: {regex}") +def step_impl(context: Context, type_label: str, value_type: ValueType, regex: str): + assert_that(value_type, is_(ValueType.STRING)) attribute_type = attribute_type_as_value_type(context, type_label, value_type) attribute_type.as_remote(context.tx()).set_regex(regex) -@step("attribute({type_label}) as({value_type}) unset regex") -def step_impl(context: Context, type_label: str, value_type): - value_type = parse_value_type(value_type) - assert_that(value_type, is_(AttributeType.ValueType.STRING)) +@step("attribute({type_label}) as({value_type:ValueType}) unset regex") +def step_impl(context: Context, type_label: str, value_type: ValueType): + assert_that(value_type, is_(ValueType.STRING)) attribute_type = attribute_type_as_value_type(context, type_label, value_type) attribute_type.as_remote(context.tx()).set_regex(None) -@step("attribute({type_label}) as({value_type}) get regex: {regex}") -def step_impl(context: Context, type_label: str, value_type, regex: str): - value_type = parse_value_type(value_type) - assert_that(value_type, is_(AttributeType.ValueType.STRING)) +@step("attribute({type_label}) as({value_type:ValueType}) get regex: {regex}") +def step_impl(context: Context, type_label: str, value_type: ValueType, regex: str): + assert_that(value_type, is_(ValueType.STRING)) attribute_type = attribute_type_as_value_type(context, type_label, value_type) assert_that(attribute_type.as_remote(context.tx()).get_regex(), is_(regex)) -@step("attribute({type_label}) as({value_type}) does not have any regex") -def step_impl(context: Context, type_label: str, value_type): - value_type = parse_value_type(value_type) - assert_that(value_type, is_(AttributeType.ValueType.STRING)) +@step("attribute({type_label}) as({value_type:ValueType}) does not have any regex") +def step_impl(context: Context, type_label: str, value_type: ValueType): + assert_that(value_type, is_(ValueType.STRING)) attribute_type = attribute_type_as_value_type(context, type_label, value_type) assert_that(attribute_type.as_remote(context.tx()).get_regex(), is_(None)) diff --git a/tests/behaviour/config/parameters.py b/tests/behaviour/config/parameters.py index aca03783..399ef81e 100644 --- a/tests/behaviour/config/parameters.py +++ b/tests/behaviour/config/parameters.py @@ -129,13 +129,13 @@ def parse_var(text: str): @parse.with_pattern(r"long|double|string|boolean|datetime") -def parse_value_type(value: str) -> AttributeType.ValueType: +def parse_value_type(value: str) -> ValueType: mapping = { - "long": AttributeType.ValueType.LONG, - "double": AttributeType.ValueType.DOUBLE, - "string": AttributeType.ValueType.STRING, - "boolean": AttributeType.ValueType.BOOLEAN, - "datetime": AttributeType.ValueType.DATETIME + "long": ValueType.LONG, + "double": ValueType.DOUBLE, + "string": ValueType.STRING, + "boolean": ValueType.BOOLEAN, + "datetime": ValueType.DATETIME } return mapping[value] diff --git a/tests/behaviour/typeql/language/expression/BUILD b/tests/behaviour/typeql/language/expression/BUILD new file mode 100644 index 00000000..6fe0406a --- /dev/null +++ b/tests/behaviour/typeql/language/expression/BUILD @@ -0,0 +1,55 @@ +# +# Copyright (C) 2022 Vaticle +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +package(default_visibility = ["//tests/behaviour:__subpackages__"]) +load("//tools:behave_rule.bzl", "typedb_behaviour_py_test") +load("@vaticle_dependencies//tool/checkstyle:rules.bzl", "checkstyle_test") + +typedb_behaviour_py_test( + name = "test", + feats = ["@vaticle_typedb_behaviour//typeql/language:expression.feature"], + background_core = ["//tests/behaviour/background:core"], + background_cluster = ["//tests/behaviour/background:cluster"], + steps = [ + "//tests/behaviour/connection:steps", + "//tests/behaviour/connection/database:steps", + "//tests/behaviour/connection/session:steps", + "//tests/behaviour/connection/transaction:steps", + "//tests/behaviour/typeql:steps", + ], + deps = [ + "//:client_python", + "//tests/behaviour:context", + "//tests/behaviour/util:util", + "//tests/behaviour/config:parameters", + "//tests/behaviour/background", + ], + native_typedb_artifact = "//tests:native-typedb-artifact", + native_typedb_cluster_artifact = "//tests:native-typedb-cluster-artifact", + size = "medium", +) + +checkstyle_test( + name = "checkstyle", + include = glob(["*"]), + license_type = "apache-header", + size = "small", +) diff --git a/tests/behaviour/typeql/typeql_steps.py b/tests/behaviour/typeql/typeql_steps.py index 84d8a13f..30b74858 100644 --- a/tests/behaviour/typeql/typeql_steps.py +++ b/tests/behaviour/typeql/typeql_steps.py @@ -23,11 +23,12 @@ from behave import * from hamcrest import * -from typedb.client import * from tests.behaviour.config.parameters import parse_bool, parse_int, parse_float, parse_datetime, parse_table, \ parse_label from tests.behaviour.context import Context +from typedb.api.concept.value.value import Value +from typedb.client import * @step("typeql define") @@ -42,7 +43,8 @@ def step_impl(context: Context): @step("typeql define; throws exception containing \"{pattern}\"") def step_impl(context: Context, pattern: str): - assert_that(calling(context.tx().query().define(query=context.text).get), raises(TypeDBClientException, pattern)) + assert_that(calling(context.tx().query().define(query=context.text).get), + raises(TypeDBClientException, re.escape(pattern))) @step("typeql undefine") @@ -57,7 +59,8 @@ def step_impl(context: Context): @step("typeql undefine; throws exception containing \"{pattern}\"") def step_impl(context: Context, pattern: str): - assert_that(calling(context.tx().query().undefine(query=context.text).get), raises(TypeDBClientException, pattern)) + assert_that(calling(context.tx().query().undefine(query=context.text).get), + raises(TypeDBClientException, re.escape(pattern))) @step("typeql insert") @@ -72,7 +75,8 @@ def step_impl(context: Context): @step("typeql insert; throws exception containing \"{pattern}\"") def step_impl(context: Context, pattern: str): - assert_that(calling(next).with_args(context.tx().query().insert(query=context.text)), raises(TypeDBClientException, pattern)) + assert_that(calling(next).with_args(context.tx().query().insert(query=context.text)), + raises(TypeDBClientException, re.escape(pattern))) @step("typeql delete") @@ -87,7 +91,8 @@ def step_impl(context: Context): @step("typeql delete; throws exception containing \"{pattern}\"") def step_impl(context: Context, pattern: str): - assert_that(calling(context.tx().query().delete(query=context.text).get), raises(TypeDBClientException, pattern)) + assert_that(calling(context.tx().query().delete(query=context.text).get), + raises(TypeDBClientException, re.escape(pattern))) @step("typeql update") @@ -102,7 +107,8 @@ def step_impl(context: Context): @step("typeql update; throws exception containing \"{pattern}\"") def step_impl(context: Context, pattern: str): - assert_that(calling(next).with_args(context.tx().query().update(query=context.text)), raises(TypeDBClientException, pattern)) + assert_that(calling(next).with_args(context.tx().query().update(query=context.text)), + raises(TypeDBClientException, re.escape(pattern))) @step("get answers of typeql insert") @@ -122,6 +128,12 @@ def step_impl(context: Context): assert_that(calling(next).with_args(context.tx().query().match(query=context.text)), raises(TypeDBClientException)) +@step("typeql match; throws exception containing \"{pattern}\"") +def step_impl(context: Context, pattern: str): + assert_that(calling(next).with_args(context.tx().query().match(query=context.text)), + raises(TypeDBClientException, re.escape(pattern))) + + @step("get answer of typeql match aggregate") def step_impl(context: Context): context.clear_answers() @@ -141,7 +153,8 @@ def step_impl(context: Context): @step("typeql match group; throws exception") def step_impl(context: Context): - assert_that(calling(next).with_args(context.tx().query().match_group(query=context.text)), raises(TypeDBClientException)) + assert_that(calling(next).with_args(context.tx().query().match_group(query=context.text)), + raises(TypeDBClientException)) @step("get answers of typeql match group aggregate") @@ -152,7 +165,8 @@ def step_impl(context: Context): @step("answer size is: {expected_size:Int}") def step_impl(context: Context, expected_size: int): - assert_that(context.answers, has_length(expected_size), "Expected [%d] answers, but got [%d]" % (expected_size, len(context.answers))) + assert_that(context.answers, has_length(expected_size), + "Expected [%d] answers, but got [%d]" % (expected_size, len(context.answers))) @step("rules contain: {rule_label}") @@ -212,20 +226,21 @@ class AttributeMatcher(ConceptMatcher, ABC): def __init__(self, type_and_value: str): self.type_and_value = type_and_value s = type_and_value.split(":") - assert_that(s, has_length(2), "[%s] is not a valid attribute identifier. It should have format \"type_label:value\"." % type_and_value) - self.type_label, self.value = s + assert_that(s, has_length(2), + "[%s] is not a valid attribute identifier. It should have format \"type_label:value\"." % type_and_value) + self.type_label, self.value_string = s def check(self, attribute: Attribute): if attribute.is_boolean(): - return ConceptMatchResult.of(parse_bool(self.value), attribute.get_value()) + return ConceptMatchResult.of(parse_bool(self.value_string), attribute.get_value()) elif attribute.is_long(): - return ConceptMatchResult.of(parse_int(self.value), attribute.get_value()) + return ConceptMatchResult.of(parse_int(self.value_string), attribute.get_value()) elif attribute.is_double(): - return ConceptMatchResult.of(parse_float(self.value), attribute.get_value()) + return ConceptMatchResult.of(parse_float(self.value_string), attribute.get_value()) elif attribute.is_string(): - return ConceptMatchResult.of(self.value, attribute.get_value()) + return ConceptMatchResult.of(self.value_string, attribute.get_value()) elif attribute.is_datetime(): - return ConceptMatchResult.of(parse_datetime(self.value), attribute.get_value()) + return ConceptMatchResult.of(parse_datetime(self.value_string), attribute.get_value()) else: raise ValueError("Unrecognised value type " + str(type(attribute))) @@ -234,12 +249,15 @@ class AttributeValueMatcher(AttributeMatcher): def match(self, context: Context, concept: Concept): if not concept.is_attribute(): - return ConceptMatchResult.of_error(self.type_and_value, "%s was matched by Attribute Value, but it is not an Attribute." % concept) + return ConceptMatchResult.of_error(self.type_and_value, + "%s was matched by Attribute Value, but it is not an Attribute." % concept) attribute = concept.as_attribute() if self.type_label != attribute.get_type().get_label().name(): - return ConceptMatchResult.of_error(self.type_and_value, "%s was matched by Attribute Value expecting type label [%s], but its actual type is %s." % (attribute, self.type_label, attribute.get_type())) + return ConceptMatchResult.of_error(self.type_and_value, + "%s was matched by Attribute Value expecting type label [%s], but its actual type is %s." % ( + attribute, self.type_label, attribute.get_type())) return self.check(attribute) @@ -248,7 +266,8 @@ class ThingKeyMatcher(AttributeMatcher): def match(self, context: Context, concept: Concept): if not concept.is_thing(): - return ConceptMatchResult.of_error(self.type_and_value, "%s was matched by Key, but it is not a Thing." % concept) + return ConceptMatchResult.of_error(self.type_and_value, + "%s was matched by Key, but it is not a Thing." % concept) keys = [key for key in concept.as_thing().as_remote(context.tx()).get_has(annotations=set([Annotations.KEY]))] @@ -256,7 +275,47 @@ def match(self, context: Context, concept: Concept): if key.get_type().get_label().name() == self.type_label: return self.check(key) - return ConceptMatchResult.of_error(self.type_and_value, "%s was matched by Key expecting key type [%s], but it doesn't own any key of that type." % (concept, self.type_label)) + return ConceptMatchResult.of_error(self.type_and_value, + "%s was matched by Key expecting key type [%s], but it doesn't own any key of that type." % ( + concept, self.type_label)) + + +class ValueMatcher(ConceptMatcher): + + def __init__(self, value_type_and_value: str): + self.value_type_and_value = value_type_and_value + s = value_type_and_value.split(":") + assert_that(s, has_length(2), + "[%s] is not a valid identifier. It should have format \"value_type:value\"." % value_type_and_value) + self.value_type_name, self.value_string = s + + def match(self, context: Context, concept: Concept): + if not concept.is_value(): + return ConceptMatchResult.of_error(self.value_type_and_value, + "%s was matched by Value, but it is not Value." % concept) + + value = concept.as_value() + + if self.value_type_name != str(value.get_value_type()): + return ConceptMatchResult.of_error(self.value_type_and_value, + "%s was matched by Value expecting value type [%s], but its actual value type is %s." % ( + value, self.value_type_name, value.get_value_type())) + + return self.check(value) + + def check(self, value: Value): + if value.is_boolean(): + return ConceptMatchResult.of(parse_bool(self.value_string), value.get_value()) + elif value.is_long(): + return ConceptMatchResult.of(parse_int(self.value_string), value.get_value()) + elif value.is_double(): + return ConceptMatchResult.of(parse_float(self.value_string), value.get_value()) + elif value.is_string(): + return ConceptMatchResult.of(self.value_string, value.get_value()) + elif value.is_datetime(): + return ConceptMatchResult.of(parse_datetime(self.value_string), value.get_value()) + else: + raise ValueError("Unrecognised value type " + str(type(value))) def parse_concept_identifier(value: str): @@ -265,8 +324,10 @@ def parse_concept_identifier(value: str): return TypeLabelMatcher(label=identifier_body) elif identifier_type == "key": return ThingKeyMatcher(type_and_value=identifier_body) - elif identifier_type == "value": + elif identifier_type == "attr": return AttributeValueMatcher(type_and_value=identifier_body) + elif identifier_type == "value": + return ValueMatcher(value_type_and_value=identifier_body) else: raise ValueError("Failed to parse concept identifier: " + value) @@ -283,10 +344,12 @@ def matches(self): return True def __str__(self): - return "[matches: %s, concept_match_results: %s]" % (self.matches(), [str(x) for x in self.concept_match_results]) + return "[matches: %s, concept_match_results: %s]" % ( + self.matches(), [str(x) for x in self.concept_match_results]) -def match_answer_concepts(context: Context, answer_identifier: List[Tuple[str, str]], answer: ConceptMap) -> AnswerMatchResult: +def match_answer_concepts(context: Context, answer_identifier: List[Tuple[str, str]], + answer: ConceptMap) -> AnswerMatchResult: results = [] for var, concept_identifier in answer_identifier: matcher = parse_concept_identifier(concept_identifier) @@ -299,7 +362,8 @@ def match_answer_concepts(context: Context, answer_identifier: List[Tuple[str, s def step_impl(context: Context): answer_identifiers = parse_table(context.table) assert_that(context.answers, has_length(len(answer_identifiers)), - "The number of answers [%d] should match the number of answer identifiers [%d]." % (len(context.answers), len(answer_identifiers))) + "The number of answers [%d] should match the number of answer identifiers [%d]." % ( + len(context.answers), len(answer_identifiers))) result_set = [(ai, [], []) for ai in answer_identifiers] for answer_identifier, matched_answers, match_attempts in result_set: @@ -308,7 +372,9 @@ def step_impl(context: Context): match_attempts.append(result) if result.matches(): matched_answers.append(answer) - assert_that(matched_answers, has_length(1), "Each answer identifier should match precisely 1 answer, but [%d] answers matched the identifier [%s].\nThe match results were: %s" % (len(matched_answers), answer_identifier, [str(x) for x in match_attempts])) + assert_that(matched_answers, has_length(1), + "Each answer identifier should match precisely 1 answer, but [%d] answers matched the identifier [%s].\nThe match results were: %s" % ( + len(matched_answers), answer_identifier, [str(x) for x in match_attempts])) for answer in context.answers: matches = 0 @@ -320,19 +386,24 @@ def step_impl(context: Context): match_attempts = [] for answer_identifier in answer_identifiers: match_attempts.append(match_answer_concepts(context, answer_identifier, answer)) - assert_that(matches, is_(1), "Each answer should match precisely 1 answer identifier, but [%d] answer identifiers matched the answer [%s].\nThe match results were: %s" % (matches, answer, [str(x) for x in match_attempts])) + assert_that(matches, is_(1), + "Each answer should match precisely 1 answer identifier, but [%d] answer identifiers matched the answer [%s].\nThe match results were: %s" % ( + matches, answer, [str(x) for x in match_attempts])) @step("order of answer concepts is") def step_impl(context: Context): answer_identifiers = parse_table(context.table) assert_that(context.answers, has_length(len(answer_identifiers)), - "The number of answers [%d] should match the number of answer identifiers [%d]." % (len(context.answers), len(answer_identifiers))) + "The number of answers [%d] should match the number of answer identifiers [%d]." % ( + len(context.answers), len(answer_identifiers))) for i in range(len(context.answers)): answer = context.answers[i] answer_identifier = answer_identifiers[i] result = match_answer_concepts(context, answer_identifier, answer) - assert_that(result.matches(), is_(True), "The answer at index [%d] does not match the identifier [%s].\nThe match results were: %s" % (i, answer_identifier, result)) + assert_that(result.matches(), is_(True), + "The answer at index [%d] does not match the identifier [%s].\nThe match results were: %s" % ( + i, answer_identifier, result)) def get_numeric_value(numeric: Numeric): @@ -365,12 +436,13 @@ def step_impl(context: Context): class AnswerIdentifierGroup: - GROUP_COLUMN_NAME = "owner" def __init__(self, raw_answer_identifiers: List[List[Tuple[str, str]]]): - self.owner_identifier = next(entry[1] for entry in raw_answer_identifiers[0] if entry[0] == self.GROUP_COLUMN_NAME) - self.answer_identifiers = [[(var, concept_identifier) for (var, concept_identifier) in raw_answer_identifier if var != self.GROUP_COLUMN_NAME] + self.owner_identifier = next( + entry[1] for entry in raw_answer_identifiers[0] if entry[0] == self.GROUP_COLUMN_NAME) + self.answer_identifiers = [[(var, concept_identifier) for (var, concept_identifier) in raw_answer_identifier if + var != self.GROUP_COLUMN_NAME] for raw_answer_identifier in raw_answer_identifiers] @@ -381,14 +453,17 @@ def step_impl(context: Context): for raw_answer_identifier in raw_answer_identifiers: owner = next(entry[1] for entry in raw_answer_identifier if entry[0] == AnswerIdentifierGroup.GROUP_COLUMN_NAME) grouped_answer_identifiers[owner].append(raw_answer_identifier) - answer_identifier_groups = [AnswerIdentifierGroup(raw_identifiers) for raw_identifiers in grouped_answer_identifiers.values()] + answer_identifier_groups = [AnswerIdentifierGroup(raw_identifiers) for raw_identifiers in + grouped_answer_identifiers.values()] assert_that(context.answer_groups, has_length(len(answer_identifier_groups)), - "Expected [%d] answer groups, but found [%d]." % (len(answer_identifier_groups), len(context.answer_groups))) + "Expected [%d] answer groups, but found [%d]." % ( + len(answer_identifier_groups), len(context.answer_groups))) for answer_identifier_group in answer_identifier_groups: identifier = parse_concept_identifier(answer_identifier_group.owner_identifier) - answer_group = next((group for group in context.answer_groups if identifier.match(context, group.owner()).matches), None) + answer_group = next( + (group for group in context.answer_groups if identifier.match(context, group.owner()).matches), None) assert_that(answer_group is not None, reason="The group identifier [%s] does not match any of the answer group owners." % answer_identifier_group.owner_identifier) @@ -399,7 +474,9 @@ def step_impl(context: Context): match_attempts.append(result) if result.matches(): matched_answers.append(answer) - assert_that(matched_answers, has_length(1), "Each answer identifier should match precisely 1 answer, but [%d] answers matched the identifier [%s].\nThe match results were: %s" % (len(matched_answers), answer_identifier, [str(x) for x in match_attempts])) + assert_that(matched_answers, has_length(1), + "Each answer identifier should match precisely 1 answer, but [%d] answers matched the identifier [%s].\nThe match results were: %s" % ( + len(matched_answers), answer_identifier, [str(x) for x in match_attempts])) for answer in answer_group.concept_maps(): matches = 0 @@ -411,7 +488,9 @@ def step_impl(context: Context): match_attempts = [] for answer_identifier in answer_identifier_group.answer_identifiers: match_attempts.append(match_answer_concepts(context, answer_identifier, answer)) - assert_that(matches, is_(1), "Each answer should match precisely 1 answer identifier, but [%d] answer identifiers matched the answer [%s].\nThe match results were: %s" % (matches, answer, [str(x) for x in match_attempts])) + assert_that(matches, is_(1), + "Each answer should match precisely 1 answer identifier, but [%d] answer identifiers matched the answer [%s].\nThe match results were: %s" % ( + matches, answer, [str(x) for x in match_attempts])) @step("group aggregate values are") @@ -424,17 +503,21 @@ def step_impl(context: Context): expectations[owner] = expected_answer assert_that(context.numeric_answer_groups, has_length(len(expectations)), - reason="Expected [%d] answer groups, but found [%d]." % (len(expectations), len(context.numeric_answer_groups))) + reason="Expected [%d] answer groups, but found [%d]." % ( + len(expectations), len(context.numeric_answer_groups))) for (owner_identifier, expected_answer) in expectations.items(): identifier = parse_concept_identifier(owner_identifier) - numeric_group = next((group for group in context.numeric_answer_groups if identifier.match(context, group.owner()).matches), None) + numeric_group = next( + (group for group in context.numeric_answer_groups if identifier.match(context, group.owner()).matches), + None) assert_that(numeric_group is not None, reason="The group identifier [%s] does not match any of the answer group owners." % owner_identifier) actual_answer = get_numeric_value(numeric_group.numeric()) assert_numeric_value(numeric_group.numeric(), expected_answer, - reason="Expected answer [%f] for group [%s], but got [%f]" % (expected_answer, owner_identifier, actual_answer)) + reason="Expected answer [%f] for group [%s], but got [%f]" % ( + expected_answer, owner_identifier, actual_answer)) def variable_from_template_placeholder(placeholder: str): diff --git a/tests/integration/test_typedb.py b/tests/integration/test_typedb.py index 7b27111b..c3259582 100644 --- a/tests/integration/test_typedb.py +++ b/tests/integration/test_typedb.py @@ -22,10 +22,12 @@ import unittest import uuid -import typedb from tests.integration.base import test_base, TypeDBServer + +import typedb from typedb import TypeDBError -from typedb.connection import TypeDBClient, ValueType, Transaction +from typedb.api.concept.concept import ValueType +from typedb.connection import TypeDBClient, Transaction # TODO: we should ensure that all these tests are migrated to BDD diff --git a/typedb/api/concept/concept.py b/typedb/api/concept/concept.py index 2bdbf734..813ba81b 100644 --- a/typedb/api/concept/concept.py +++ b/typedb/api/concept/concept.py @@ -18,9 +18,12 @@ # specific language governing permissions and limitations # under the License. # +import enum from abc import ABC, abstractmethod from typing import Mapping, Union, TYPE_CHECKING +import typedb_protocol.common.concept_pb2 as concept_proto + from typedb.common.exception import TypeDBClientException, INVALID_CONCEPT_CASTING if TYPE_CHECKING: @@ -69,6 +72,9 @@ def is_attribute(self) -> bool: def is_relation(self) -> bool: return False + def is_value(self) -> bool: + return False + def as_type(self) -> "Type": raise TypeDBClientException.of(INVALID_CONCEPT_CASTING, (self.__class__.__name__, "Type")) @@ -99,6 +105,9 @@ def as_attribute(self) -> "Attribute": def as_relation(self) -> "Relation": raise TypeDBClientException.of(INVALID_CONCEPT_CASTING, (self.__class__.__name__, "Relation")) + def as_value(self) -> "Value": + raise TypeDBClientException.of(INVALID_CONCEPT_CASTING, (self.__class__.__name__, "Value")) + @abstractmethod def as_remote(self, transaction: "TypeDBTransaction") -> "RemoteConcept": pass @@ -112,6 +121,21 @@ def to_json(self) -> Mapping[str, Union[str, int, float, bool]]: pass +class ValueType(enum.Enum): + OBJECT = 0 + BOOLEAN = 1 + LONG = 2 + DOUBLE = 3 + STRING = 4 + DATETIME = 5 + + def proto(self) -> concept_proto.ValueType: + return concept_proto.ValueType.Value(self.name) + + def __str__(self): + return self.name.lower() + + class RemoteConcept(Concept, ABC): @abstractmethod diff --git a/typedb/api/concept/concept_manager.py b/typedb/api/concept/concept_manager.py index 999b1be6..e441c04e 100644 --- a/typedb/api/concept/concept_manager.py +++ b/typedb/api/concept/concept_manager.py @@ -20,6 +20,7 @@ # from abc import ABC, abstractmethod +from typedb.api.concept.concept import ValueType from typedb.api.concept.thing.thing import Thing from typedb.api.concept.type.attribute_type import AttributeType from typedb.api.concept.type.entity_type import EntityType @@ -74,5 +75,5 @@ def get_attribute_type(self, label: str) -> AttributeType: pass @abstractmethod - def put_attribute_type(self, label: str, value_type: AttributeType.ValueType) -> AttributeType: + def put_attribute_type(self, label: str, value_type: ValueType) -> AttributeType: pass diff --git a/typedb/api/concept/type/attribute_type.py b/typedb/api/concept/type/attribute_type.py index b326db1a..128b1423 100644 --- a/typedb/api/concept/type/attribute_type.py +++ b/typedb/api/concept/type/attribute_type.py @@ -23,7 +23,7 @@ from datetime import datetime from typing import Optional, TYPE_CHECKING, Iterator, Set -import typedb_protocol.common.concept_pb2 as concept_proto +from typedb.api.concept.concept import ValueType from typedb.api.concept.thing.attribute import BooleanAttribute, LongAttribute, DoubleAttribute, StringAttribute, \ DateTimeAttribute, Attribute from typedb.api.concept.type.thing_type import ThingType, RemoteThingType, Annotation @@ -35,7 +35,7 @@ class AttributeType(ThingType, ABC): def get_value_type(self) -> "ValueType": - return AttributeType.ValueType.OBJECT + return ValueType.OBJECT def is_attribute_type(self) -> bool: return True @@ -79,26 +79,6 @@ def as_string(self) -> "StringAttributeType": def as_datetime(self) -> "DateTimeAttributeType": pass - class ValueType(enum.Enum): - OBJECT = 0 - BOOLEAN = 1 - LONG = 2 - DOUBLE = 3 - STRING = 4 - DATETIME = 5 - - def is_writable(self) -> bool: - return self is not AttributeType.ValueType.OBJECT - - def is_keyable(self) -> bool: - return self in [AttributeType.ValueType.LONG, AttributeType.ValueType.STRING, AttributeType.ValueType.DATETIME] - - def proto(self) -> concept_proto.AttributeType.ValueType: - return concept_proto.AttributeType.ValueType.Value(self.name) - - def __str__(self): - return self.name.lower() - class RemoteAttributeType(RemoteThingType, AttributeType, ABC): @@ -145,8 +125,8 @@ def as_datetime(self) -> "RemoteDateTimeAttributeType": class BooleanAttributeType(AttributeType, ABC): - def get_value_type(self) -> AttributeType.ValueType: - return AttributeType.ValueType.BOOLEAN + def get_value_type(self) -> ValueType: + return ValueType.BOOLEAN def is_boolean(self) -> bool: return True @@ -181,8 +161,8 @@ def set_supertype(self, attribute_type: BooleanAttributeType) -> None: class LongAttributeType(AttributeType, ABC): - def get_value_type(self) -> AttributeType.ValueType: - return AttributeType.ValueType.LONG + def get_value_type(self) -> ValueType: + return ValueType.LONG def is_long(self) -> bool: return True @@ -217,8 +197,8 @@ def set_supertype(self, attribute_type: LongAttributeType) -> None: class DoubleAttributeType(AttributeType, ABC): - def get_value_type(self) -> AttributeType.ValueType: - return AttributeType.ValueType.DOUBLE + def get_value_type(self) -> ValueType: + return ValueType.DOUBLE def is_double(self) -> bool: return True @@ -253,8 +233,8 @@ def set_supertype(self, attribute_type: DoubleAttributeType) -> None: class StringAttributeType(AttributeType, ABC): - def get_value_type(self) -> AttributeType.ValueType: - return AttributeType.ValueType.STRING + def get_value_type(self) -> ValueType: + return ValueType.STRING def is_string(self) -> bool: return True @@ -297,8 +277,8 @@ def set_supertype(self, attribute_type: StringAttributeType) -> None: class DateTimeAttributeType(AttributeType, ABC): - def get_value_type(self) -> AttributeType.ValueType: - return AttributeType.ValueType.DATETIME + def get_value_type(self) -> ValueType: + return ValueType.DATETIME def is_datetime(self) -> bool: return True diff --git a/typedb/api/concept/type/thing_type.py b/typedb/api/concept/type/thing_type.py index 20c86c94..7072c205 100644 --- a/typedb/api/concept/type/thing_type.py +++ b/typedb/api/concept/type/thing_type.py @@ -22,6 +22,7 @@ from enum import Enum from typing import TYPE_CHECKING, Iterator, Optional, Set +from typedb.api.concept.concept import ValueType from typedb.api.concept.thing.thing import Thing from typedb.api.concept.type.role_type import RoleType from typedb.api.concept.type.type import Type, RemoteType @@ -117,12 +118,12 @@ def get_plays_overridden(self, role_type: "RoleType") -> Optional["RoleType"]: pass @abstractmethod - def get_owns(self, value_type: "AttributeType.ValueType" = None, + def get_owns(self, value_type: "ValueType" = None, annotations: Set["Annotation"] = frozenset()) -> Iterator["AttributeType"]: pass @abstractmethod - def get_owns_explicit(self, value_type: "AttributeType.ValueType" = None, + def get_owns_explicit(self, value_type: "ValueType" = None, annotations: Set["Annotation"] = frozenset()) -> Iterator["AttributeType"]: pass diff --git a/typedb/api/concept/value/value.py b/typedb/api/concept/value/value.py new file mode 100644 index 00000000..d44a57c8 --- /dev/null +++ b/typedb/api/concept/value/value.py @@ -0,0 +1,122 @@ +# +# Copyright (C) 2022 Vaticle +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +from abc import ABC, abstractmethod +from datetime import datetime +from typing import TYPE_CHECKING, Mapping, Union + +from typedb.api.concept.concept import Concept, ValueType +from typedb.api.connection.transaction import TypeDBTransaction +from typedb.common.exception import TypeDBClientException, VALUE_HAS_NO_REMOTE + + + +class Value(Concept, ABC): + + @abstractmethod + def get_value_type(self) -> "ValueType": + pass + + @abstractmethod + def get_value(self) -> Union[bool, int, float, str, datetime]: + pass + + def is_value(self): + return True + + def is_boolean(self): + return False + + def is_long(self): + return False + + def is_double(self): + return False + + def is_string(self): + return False + + def is_datetime(self): + return False + + def as_remote(self, transaction: "TypeDBTransaction"): + raise TypeDBClientException.of(VALUE_HAS_NO_REMOTE) + + def to_json(self) -> Mapping[str, Union[str, int, float, bool]]: + return { + "value_type": str(self.get_value_type()), + "value": self.get_value(), + } + + +class BooleanValue(Value, ABC): + + def is_boolean(self) -> bool: + return True + + @abstractmethod + def get_value(self) -> bool: + pass + + +class LongValue(Value, ABC): + + def is_long(self) -> bool: + return True + + @abstractmethod + def get_value(self) -> int: + pass + + +class DoubleValue(Value, ABC): + + def is_double(self) -> bool: + return True + + @abstractmethod + def get_value(self) -> float: + pass + + +class StringValue(Value, ABC): + + def is_string(self) -> bool: + return True + + @abstractmethod + def get_value(self) -> str: + pass + + +class DateTimeValue(Value, ABC): + + def is_datetime(self) -> bool: + return True + + @abstractmethod + def get_value(self) -> datetime: + pass + + def to_json(self) -> Mapping[str, Union[str, int, float, bool]]: + return { + "value_type": str(self.get_value_type()), + "value": self.get_value().isoformat(timespec='milliseconds') + } diff --git a/typedb/common/exception.py b/typedb/common/exception.py index e54c6751..e67cddf6 100644 --- a/typedb/common/exception.py +++ b/typedb/common/exception.py @@ -109,9 +109,10 @@ def __init__(self, code: int, message: str): BAD_ENCODING = ConceptErrorMessage(6, "The encoding '%s' was not recognised.") BAD_VALUE_TYPE = ConceptErrorMessage(7, "The value type '%s' was not recognised.") BAD_ATTRIBUTE_VALUE = ConceptErrorMessage(8, "The attribute value '%s' was not recognised.") -NONEXISTENT_EXPLAINABLE_CONCEPT = ConceptErrorMessage(9, "The concept identified by '%s' is not explainable.") -NONEXISTENT_EXPLAINABLE_OWNERSHIP = ConceptErrorMessage(10, "The ownership by owner '%s' of attribute '%s' is not explainable.") -GET_HAS_WITH_MULTIPLE_FILTERS = ConceptErrorMessage(11, "Only one filter can be applied at a time to get_has. The possible filters are: [attribute_type, attribute_types, annotations]") +VALUE_HAS_NO_REMOTE = ConceptErrorMessage(9, "A 'value' has no remote concept.") +NONEXISTENT_EXPLAINABLE_CONCEPT = ConceptErrorMessage(10, "The concept identified by '%s' is not explainable.") +NONEXISTENT_EXPLAINABLE_OWNERSHIP = ConceptErrorMessage(11, "The ownership by owner '%s' of attribute '%s' is not explainable.") +GET_HAS_WITH_MULTIPLE_FILTERS = ConceptErrorMessage(12, "Only one filter can be applied at a time to get_has. The possible filters are: [attribute_type, attribute_types, annotations]") class QueryErrorMessage(ErrorMessage): diff --git a/typedb/common/rpc/request_builder.py b/typedb/common/rpc/request_builder.py index 0a084b0d..e77016d1 100644 --- a/typedb/common/rpc/request_builder.py +++ b/typedb/common/rpc/request_builder.py @@ -18,8 +18,7 @@ # specific language governing permissions and limitations # under the License. # -from datetime import datetime -from typing import List, Set +from typing import List from uuid import UUID import typedb_protocol.cluster.cluster_database_pb2 as cluster_database_proto @@ -318,7 +317,7 @@ def concept_manager_put_relation_type_req(label: str): return concept_manager_req(req) -def concept_manager_put_attribute_type_req(label: str, value_type: concept_proto.AttributeType.ValueType): +def concept_manager_put_attribute_type_req(label: str, value_type: concept_proto.ValueType): req = concept_proto.ConceptManager.Req() put_attribute_type_req = concept_proto.ConceptManager.PutAttributeType.Req() put_attribute_type_req.label = label @@ -539,7 +538,7 @@ def thing_type_unset_plays_req(label: Label, role_type: concept_proto.Type): return type_req(req, label) -def thing_type_get_owns_req(label: Label, value_type: concept_proto.AttributeType.ValueType = None, +def thing_type_get_owns_req(label: Label, value_type: concept_proto.ValueType = None, annotations: List[concept_proto.Type.Annotation] = None): req = concept_proto.Type.Req() get_owns_req = concept_proto.ThingType.GetOwns.Req() @@ -551,7 +550,7 @@ def thing_type_get_owns_req(label: Label, value_type: concept_proto.AttributeTyp return type_req(req, label) -def thing_type_get_owns_explicit_req(label: Label, value_type: concept_proto.AttributeType.ValueType = None, +def thing_type_get_owns_explicit_req(label: Label, value_type: concept_proto.ValueType = None, annotations: List[concept_proto.Type.Annotation] = None): req = concept_proto.Type.Req() get_owns_explicit_req = concept_proto.ThingType.GetOwnsExplicit.Req() @@ -684,7 +683,7 @@ def attribute_type_get_owners_explicit_req(label: Label, annotations: List[conce return type_req(req, label) -def attribute_type_put_req(label: Label, value: concept_proto.Attribute.Value): +def attribute_type_put_req(label: Label, value: concept_proto.ConceptValue): req = concept_proto.Type.Req() put_req = concept_proto.AttributeType.Put.Req() put_req.value.CopyFrom(value) @@ -692,7 +691,7 @@ def attribute_type_put_req(label: Label, value: concept_proto.Attribute.Value): return type_req(req, label) -def attribute_type_get_req(label: Label, value: concept_proto.Attribute.Value): +def attribute_type_get_req(label: Label, value: concept_proto.ConceptValue): req = concept_proto.Type.Req() get_req = concept_proto.AttributeType.Get.Req() get_req.value.CopyFrom(value) @@ -838,36 +837,6 @@ def attribute_get_owners_req(iid: str, owner_type: concept_proto.Type = None): return thing_req(req, iid) -def proto_boolean_attribute_value(value: bool): - value_proto = concept_proto.Attribute.Value() - value_proto.boolean = value - return value_proto - - -def proto_long_attribute_value(value: int): - value_proto = concept_proto.Attribute.Value() - value_proto.long = value - return value_proto - - -def proto_double_attribute_value(value: float): - value_proto = concept_proto.Attribute.Value() - value_proto.double = value - return value_proto - - -def proto_string_attribute_value(value: str): - value_proto = concept_proto.Attribute.Value() - value_proto.string = value - return value_proto - - -def proto_datetime_attribute_value(value: datetime): - value_proto = concept_proto.Attribute.Value() - value_proto.date_time = int((value - datetime(1970, 1, 1)).total_seconds() * 1000) - return value_proto - - # Rule def rule_req(label: str, req: logic_proto.Rule.Req): diff --git a/typedb/concept/concept_manager.py b/typedb/concept/concept_manager.py index ebf6a040..d70904d9 100644 --- a/typedb/concept/concept_manager.py +++ b/typedb/concept/concept_manager.py @@ -21,10 +21,11 @@ import typedb_protocol.common.transaction_pb2 as transaction_proto +from typedb.api.concept.concept import ValueType from typedb.api.concept.concept_manager import ConceptManager -from typedb.api.concept.type.attribute_type import AttributeType from typedb.api.connection.transaction import _TypeDBTransactionExtended -from typedb.common.rpc.request_builder import concept_manager_put_entity_type_req, concept_manager_put_relation_type_req, \ +from typedb.common.rpc.request_builder import concept_manager_put_entity_type_req, \ + concept_manager_put_relation_type_req, \ concept_manager_put_attribute_type_req, concept_manager_get_thing_type_req, concept_manager_get_thing_req from typedb.concept.proto import concept_proto_reader from typedb.concept.type.entity_type import _EntityType @@ -63,7 +64,7 @@ def get_relation_type(self, label: str): _type = self.get_thing_type(label) return _type if _type and _type.is_relation_type() else None - def put_attribute_type(self, label: str, value_type: AttributeType.ValueType): + def put_attribute_type(self, label: str, value_type: ValueType): res = self.execute(concept_manager_put_attribute_type_req(label, value_type.proto())) return concept_proto_reader.attribute_type(res.put_attribute_type_res.attribute_type) diff --git a/typedb/concept/proto/concept_proto_builder.py b/typedb/concept/proto/concept_proto_builder.py index 4fdf5741..e31a4871 100644 --- a/typedb/concept/proto/concept_proto_builder.py +++ b/typedb/concept/proto/concept_proto_builder.py @@ -58,32 +58,32 @@ def types(ts: Optional[List[Type]]): return map(lambda t: thing_type(t) if t.is_thing_type() else role_type(t), ts) if ts else None -def boolean_attribute_value(value: bool): - value_proto = concept_proto.Attribute.Value() +def boolean_value(value: bool): + value_proto = concept_proto.ConceptValue() value_proto.boolean = value return value_proto -def long_attribute_value(value: int): - value_proto = concept_proto.Attribute.Value() +def long_value(value: int): + value_proto = concept_proto.ConceptValue() value_proto.long = value return value_proto -def double_attribute_value(value: float): - value_proto = concept_proto.Attribute.Value() +def double_value(value: float): + value_proto = concept_proto.ConceptValue() value_proto.double = value return value_proto -def string_attribute_value(value: str): - value_proto = concept_proto.Attribute.Value() +def string_value(value: str): + value_proto = concept_proto.ConceptValue() value_proto.string = value return value_proto -def datetime_attribute_value(value: datetime): - value_proto = concept_proto.Attribute.Value() +def datetime_value(value: datetime): + value_proto = concept_proto.ConceptValue() value_proto.date_time = int((value - datetime(1970, 1, 1)).total_seconds() * 1000) return value_proto diff --git a/typedb/concept/proto/concept_proto_reader.py b/typedb/concept/proto/concept_proto_reader.py index 7ce47648..4c9177d2 100644 --- a/typedb/concept/proto/concept_proto_reader.py +++ b/typedb/concept/proto/concept_proto_reader.py @@ -33,6 +33,7 @@ from typedb.concept.type.relation_type import _RelationType from typedb.concept.type.role_type import _RoleType from typedb.concept.type.thing_type import _ThingType +from typedb.concept.value.value import _BooleanValue, _LongValue, _DoubleValue, _StringValue, _DateTimeValue def iid(proto_iid: bytes): @@ -40,7 +41,12 @@ def iid(proto_iid: bytes): def concept(proto_concept: concept_proto.Concept): - return thing(proto_concept.thing) if proto_concept.HasField("thing") else type_(proto_concept.type) + if proto_concept.HasField("thing"): + return thing(proto_concept.thing) + elif proto_concept.HasField("type"): + return type_(proto_concept.type) + else: + return value(proto_concept.value) def thing(proto_thing: concept_proto.Thing): @@ -55,20 +61,35 @@ def thing(proto_thing: concept_proto.Thing): def attribute(proto_thing: concept_proto.Thing): - if proto_thing.type.value_type == concept_proto.AttributeType.ValueType.Value("BOOLEAN"): + if proto_thing.type.value_type == concept_proto.ValueType.Value("BOOLEAN"): return _BooleanAttribute.of(proto_thing) - elif proto_thing.type.value_type == concept_proto.AttributeType.ValueType.Value("LONG"): + elif proto_thing.type.value_type == concept_proto.ValueType.Value("LONG"): return _LongAttribute.of(proto_thing) - elif proto_thing.type.value_type == concept_proto.AttributeType.ValueType.Value("DOUBLE"): + elif proto_thing.type.value_type == concept_proto.ValueType.Value("DOUBLE"): return _DoubleAttribute.of(proto_thing) - elif proto_thing.type.value_type == concept_proto.AttributeType.ValueType.Value("STRING"): + elif proto_thing.type.value_type == concept_proto.ValueType.Value("STRING"): return _StringAttribute.of(proto_thing) - elif proto_thing.type.value_type == concept_proto.AttributeType.ValueType.Value("DATETIME"): + elif proto_thing.type.value_type == concept_proto.ValueType.Value("DATETIME"): return _DateTimeAttribute.of(proto_thing) else: raise TypeDBClientException.of(BAD_VALUE_TYPE, proto_thing.type.value_type) +def value(proto_value: concept_proto.Value): + if proto_value.value_type == concept_proto.ValueType.Value("BOOLEAN"): + return _BooleanValue.of(proto_value) + elif proto_value.value_type == concept_proto.ValueType.Value("LONG"): + return _LongValue.of(proto_value) + elif proto_value.value_type == concept_proto.ValueType.Value("DOUBLE"): + return _DoubleValue.of(proto_value) + elif proto_value.value_type == concept_proto.ValueType.Value("STRING"): + return _StringValue.of(proto_value) + elif proto_value.value_type == concept_proto.ValueType.Value("DATETIME"): + return _DateTimeValue.of(proto_value) + else: + raise TypeDBClientException.of(BAD_VALUE_TYPE, proto_value.type.value_type) + + def type_(proto_type: concept_proto.Type): if proto_type.encoding == concept_proto.Type.Encoding.Value("ROLE_TYPE"): return _RoleType.of(proto_type) @@ -90,17 +111,17 @@ def thing_type(proto_type: concept_proto.Type): def attribute_type(proto_type: concept_proto.Type): - if proto_type.value_type == concept_proto.AttributeType.ValueType.Value("BOOLEAN"): + if proto_type.value_type == concept_proto.ValueType.Value("BOOLEAN"): return _BooleanAttributeType.of(proto_type) - elif proto_type.value_type == concept_proto.AttributeType.ValueType.Value("LONG"): + elif proto_type.value_type == concept_proto.ValueType.Value("LONG"): return _LongAttributeType.of(proto_type) - elif proto_type.value_type == concept_proto.AttributeType.ValueType.Value("DOUBLE"): + elif proto_type.value_type == concept_proto.ValueType.Value("DOUBLE"): return _DoubleAttributeType.of(proto_type) - elif proto_type.value_type == concept_proto.AttributeType.ValueType.Value("STRING"): + elif proto_type.value_type == concept_proto.ValueType.Value("STRING"): return _StringAttributeType.of(proto_type) - elif proto_type.value_type == concept_proto.AttributeType.ValueType.Value("DATETIME"): + elif proto_type.value_type == concept_proto.ValueType.Value("DATETIME"): return _DateTimeAttributeType.of(proto_type) - elif proto_type.value_type == concept_proto.AttributeType.ValueType.Value("OBJECT"): + elif proto_type.value_type == concept_proto.ValueType.Value("OBJECT"): return _AttributeType(Label.of(proto_type.label), proto_type.is_root, proto_type.is_abstract) else: raise TypeDBClientException.of(BAD_VALUE_TYPE, proto_type.value_type) diff --git a/typedb/concept/type/attribute_type.py b/typedb/concept/type/attribute_type.py index 67443c88..5f4cf365 100644 --- a/typedb/concept/type/attribute_type.py +++ b/typedb/concept/type/attribute_type.py @@ -23,6 +23,8 @@ from typing import Optional, Iterator, Set import typedb_protocol.common.concept_pb2 as concept_proto + +from typedb.api.concept.concept import ValueType from typedb.api.concept.type.attribute_type import AttributeType, RemoteAttributeType, BooleanAttributeType, \ RemoteBooleanAttributeType, LongAttributeType, RemoteLongAttributeType, DoubleAttributeType, \ RemoteDoubleAttributeType, StringAttributeType, RemoteStringAttributeType, DateTimeAttributeType, \ @@ -96,7 +98,7 @@ def as_attribute_type(self) -> "RemoteAttributeType": def get_subtypes(self) -> Iterator[AttributeType]: stream = super(_RemoteAttributeType, self).get_subtypes() - if self.is_root() and self.get_value_type() is not AttributeType.ValueType.OBJECT: + if self.is_root() and self.get_value_type() is not ValueType.OBJECT: return (subtype for subtype in stream if subtype.get_value_type() is self.get_value_type() or subtype.get_label() == self.get_label()) else: return stream @@ -111,11 +113,11 @@ def get_owners_explicit(self, annotations: Set["Annotation"] = frozenset()): for rp in self.stream(attribute_type_get_owners_explicit_req(self.get_label(), [concept_proto_builder.annotation(a) for a in annotations])) for tt in rp.attribute_type_get_owners_explicit_res_part.thing_types) - def put_internal(self, proto_value: concept_proto.Attribute.Value): + def put_internal(self, proto_value: concept_proto.ConceptValue): res = self.execute(attribute_type_put_req(self.get_label(), proto_value)).attribute_type_put_res return concept_proto_reader.attribute(res.attribute) - def get_internal(self, proto_value: concept_proto.Attribute.Value): + def get_internal(self, proto_value: concept_proto.ConceptValue): res = self.execute(attribute_type_get_req(self.get_label(), proto_value)).attribute_type_get_res return concept_proto_reader.attribute(res.attribute) if res.WhichOneof("res") == "attribute" else None @@ -177,10 +179,10 @@ def as_remote(self, transaction): return _RemoteBooleanAttributeType(transaction, self.get_label(), self.is_root(), self.is_abstract()) def put(self, value: bool): - return self.put_internal(concept_proto_builder.boolean_attribute_value(value)) + return self.put_internal(concept_proto_builder.boolean_value(value)) def get(self, value: bool): - return self.get_internal(concept_proto_builder.boolean_attribute_value(value)) + return self.get_internal(concept_proto_builder.boolean_value(value)) def as_boolean(self): return self @@ -205,10 +207,10 @@ def as_remote(self, transaction): return _RemoteLongAttributeType(transaction, self.get_label(), self.is_root(), self.is_abstract()) def put(self, value: int): - return self.put_internal(concept_proto_builder.long_attribute_value(value)) + return self.put_internal(concept_proto_builder.long_value(value)) def get(self, value: int): - return self.get_internal(concept_proto_builder.long_attribute_value(value)) + return self.get_internal(concept_proto_builder.long_value(value)) def as_long(self): return self @@ -233,10 +235,10 @@ def as_remote(self, transaction): return _RemoteDoubleAttributeType(transaction, self.get_label(), self.is_root(), self.is_abstract()) def put(self, value: float): - return self.put_internal(concept_proto_builder.double_attribute_value(value)) + return self.put_internal(concept_proto_builder.double_value(value)) def get(self, value: float): - return self.get_internal(concept_proto_builder.double_attribute_value(value)) + return self.get_internal(concept_proto_builder.double_value(value)) def as_double(self): return self @@ -261,10 +263,10 @@ def as_remote(self, transaction): return _RemoteStringAttributeType(transaction, self.get_label(), self.is_root(), self.is_abstract()) def put(self, value: str): - return self.put_internal(concept_proto_builder.string_attribute_value(value)) + return self.put_internal(concept_proto_builder.string_value(value)) def get(self, value: str): - return self.get_internal(concept_proto_builder.string_attribute_value(value)) + return self.get_internal(concept_proto_builder.string_value(value)) def get_regex(self): res = self.execute(attribute_type_get_regex_req(self.get_label())) @@ -299,10 +301,10 @@ def as_remote(self, transaction): return _RemoteDateTimeAttributeType(transaction, self.get_label(), self.is_root(), self.is_abstract()) def put(self, value: datetime): - return self.put_internal(concept_proto_builder.datetime_attribute_value(value)) + return self.put_internal(concept_proto_builder.datetime_value(value)) def get(self, value: datetime): - return self.get_internal(concept_proto_builder.datetime_attribute_value(value)) + return self.get_internal(concept_proto_builder.datetime_value(value)) def as_datetime(self): return self diff --git a/typedb/concept/type/thing_type.py b/typedb/concept/type/thing_type.py index 98ad6760..cf882b5c 100644 --- a/typedb/concept/type/thing_type.py +++ b/typedb/concept/type/thing_type.py @@ -20,6 +20,7 @@ # from typing import Set +from typedb.api.concept.concept import ValueType from typedb.api.concept.type.attribute_type import AttributeType from typedb.api.concept.type.role_type import RoleType from typedb.api.concept.type.thing_type import ThingType, RemoteThingType, Annotation @@ -95,14 +96,14 @@ def set_owns(self, attribute_type: AttributeType, overridden_type: AttributeType def unset_owns(self, attribute_type: AttributeType): self.execute(thing_type_unset_owns_req(self.get_label(), concept_proto_builder.thing_type(attribute_type))) - def get_owns(self, value_type: AttributeType.ValueType = None, annotations: Set["Annotation"] = frozenset()): + def get_owns(self, value_type: ValueType = None, annotations: Set["Annotation"] = frozenset()): return (concept_proto_reader.type_(t) for rp in self.stream(thing_type_get_owns_req(self.get_label(), value_type.proto() if value_type else None, [concept_proto_builder.annotation(a) for a in annotations])) for t in rp.thing_type_get_owns_res_part.attribute_types) - def get_owns_explicit(self, value_type: AttributeType.ValueType = None, annotations: Set["Annotation"] = frozenset()): + def get_owns_explicit(self, value_type: ValueType = None, annotations: Set["Annotation"] = frozenset()): return (concept_proto_reader.type_(t) for rp in self.stream(thing_type_get_owns_explicit_req(self.get_label(), value_type.proto() if value_type else None, diff --git a/typedb/concept/value/value.py b/typedb/concept/value/value.py new file mode 100644 index 00000000..4de6ea63 --- /dev/null +++ b/typedb/concept/value/value.py @@ -0,0 +1,120 @@ +# +# Copyright (C) 2022 Vaticle +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +from abc import ABC +from datetime import datetime + +import typedb_protocol.common.concept_pb2 as concept_proto + +from typedb.api.concept.concept import ValueType +from typedb.api.concept.value.value import Value, LongValue, BooleanValue, DoubleValue, StringValue, DateTimeValue +from typedb.concept.concept import _Concept +from typedb.concept.proto import concept_proto_reader + + +class _Value(Value, _Concept, ABC): + + def as_value(self) -> "Value": + return self + + +class _BooleanValue(BooleanValue, _Value): + + def __init__(self, value: bool): + super(_BooleanValue, self).__init__() + self._value = value + + @staticmethod + def of(value_proto: concept_proto.Value): + return _BooleanValue(value_proto.value.boolean) + + def get_value(self): + return self._value + + def get_value_type(self) -> "ValueType": + return ValueType.BOOLEAN + + +class _LongValue(LongValue, _Value): + + def __init__(self, value: int): + super(_LongValue, self).__init__() + self._value = value + + @staticmethod + def of(value_proto: concept_proto.Value): + return _LongValue(value_proto.value.long) + + def get_value(self): + return self._value + + def get_value_type(self) -> "ValueType": + return ValueType.LONG + + +class _DoubleValue(DoubleValue, _Value): + + def __init__(self, value: float): + super(_DoubleValue, self).__init__() + self._value = value + + @staticmethod + def of(value_proto: concept_proto.Value): + return _DoubleValue(value_proto.value.double) + + def get_value(self): + return self._value + + def get_value_type(self) -> "ValueType": + return ValueType.DOUBLE + + +class _StringValue(StringValue, _Value): + + def __init__(self, value: str): + super(_StringValue, self).__init__() + self._value = value + + @staticmethod + def of(value_proto: concept_proto.Value): + return _StringValue(value_proto.value.string) + + def get_value(self): + return self._value + + def get_value_type(self) -> "ValueType": + return ValueType.STRING + + +class _DateTimeValue(DateTimeValue, _Value): + + def __init__(self, value: datetime): + super(_DateTimeValue, self).__init__() + self._value = value + + @staticmethod + def of(value_proto: concept_proto.Value): + return _DateTimeValue(datetime.fromtimestamp(float(value_proto.value.date_time) / 1000.0)) + + def get_value(self): + return self._value + + def get_value_type(self) -> "ValueType": + return ValueType.DATETIME