diff --git a/src/binder/bind/bind_projection_clause.cpp b/src/binder/bind/bind_projection_clause.cpp index 3aeb5cf756..1f92fb5659 100644 --- a/src/binder/bind/bind_projection_clause.cpp +++ b/src/binder/bind/bind_projection_clause.cpp @@ -127,6 +127,8 @@ std::unique_ptr Binder::bindProjectionBody( } } if (!groupByExpressions.empty()) { + // TODO(Xiyang): we can remove augment group by. But make sure we test sufficient including + // edge case and bug before release. expression_vector augmentedGroupByExpressions = groupByExpressions; for (auto& expression : groupByExpressions) { if (ExpressionUtil::isNodeVariable(*expression)) { diff --git a/src/function/vector_hash_functions.cpp b/src/function/vector_hash_functions.cpp index 77d215d2d2..c92fedf4b4 100644 --- a/src/function/vector_hash_functions.cpp +++ b/src/function/vector_hash_functions.cpp @@ -53,6 +53,19 @@ void VectorHashFunction::computeHash(ValueVector* operand, ValueVector* result) case PhysicalTypeID::INTERVAL: { UnaryHashFunctionExecutor::execute(*operand, *result); } break; + case PhysicalTypeID::STRUCT: { + if (operand->dataType.getLogicalTypeID() == LogicalTypeID::NODE) { + assert(0 == common::StructType::getFieldIdx(&operand->dataType, InternalKeyword::ID)); + UnaryHashFunctionExecutor::execute( + *StructVector::getFieldVector(operand, 0), *result); + break; + } else if (operand->dataType.getLogicalTypeID() == LogicalTypeID::REL) { + assert(3 == StructType::getFieldIdx(&operand->dataType, InternalKeyword::ID)); + UnaryHashFunctionExecutor::execute( + *StructVector::getFieldVector(operand, 3), *result); + break; + } + } default: { throw RuntimeException( "Cannot hash data type " + diff --git a/src/include/processor/operator/aggregate/aggregate_hash_table.h b/src/include/processor/operator/aggregate/aggregate_hash_table.h index 8bd81572be..f051cd584d 100644 --- a/src/include/processor/operator/aggregate/aggregate_hash_table.h +++ b/src/include/processor/operator/aggregate/aggregate_hash_table.h @@ -32,7 +32,7 @@ struct HashSlot { * */ class AggregateHashTable; -using compare_function_t = std::function; +using compare_function_t = std::function; using update_agg_function_t = std::function&, const std::vector&, std::unique_ptr&, @@ -181,16 +181,8 @@ class AggregateHashTable : public BaseHashTable { void resizeHashTableIfNecessary(uint32_t maxNumDistinctHashKeys); - template - static bool compareEntryWithKeys(const uint8_t* keyValue, const uint8_t* entry) { - uint8_t result; - kuzu::function::Equals::operation(*(type*)keyValue, *(type*)entry, result, - nullptr /* leftVector */, nullptr /* rightVector */); - return result != 0; - } - static void getCompareEntryWithKeysFunc( - common::PhysicalTypeID physicalType, compare_function_t& func); + const common::LogicalType& logicalType, compare_function_t& func); void updateNullAggVectorState(const std::vector& flatKeyVectors, const std::vector& unFlatKeyVectors, diff --git a/src/optimizer/agg_key_dependency_optimizer.cpp b/src/optimizer/agg_key_dependency_optimizer.cpp index 64d1394868..f5e834472e 100644 --- a/src/optimizer/agg_key_dependency_optimizer.cpp +++ b/src/optimizer/agg_key_dependency_optimizer.cpp @@ -26,64 +26,65 @@ void AggKeyDependencyOptimizer::visitOperator(planner::LogicalOperator* op) { void AggKeyDependencyOptimizer::visitAggregate(planner::LogicalOperator* op) { auto agg = (LogicalAggregate*)op; - auto [keyExpressions, payloadExpressions] = - resolveKeysAndDependentKeys(agg->getKeyExpressions()); - agg->setKeyExpressions(keyExpressions); - agg->setDependentKeyExpressions(payloadExpressions); + auto [keys, dependentKeys] = resolveKeysAndDependentKeys(agg->getKeyExpressions()); + agg->setKeyExpressions(keys); + agg->setDependentKeyExpressions(dependentKeys); } void AggKeyDependencyOptimizer::visitDistinct(planner::LogicalOperator* op) { auto distinct = (LogicalDistinct*)op; - auto [keyExpressions, payloadExpressions] = - resolveKeysAndDependentKeys(distinct->getKeyExpressions()); - distinct->setKeyExpressions(keyExpressions); - distinct->setDependentKeyExpressions(payloadExpressions); + auto [keys, dependentKeys] = resolveKeysAndDependentKeys(distinct->getKeyExpressions()); + distinct->setKeyExpressions(keys); + distinct->setDependentKeyExpressions(dependentKeys); } std::pair -AggKeyDependencyOptimizer::resolveKeysAndDependentKeys(const binder::expression_vector& keys) { +AggKeyDependencyOptimizer::resolveKeysAndDependentKeys(const expression_vector& inputKeys) { // Consider example RETURN a.ID, a.age, COUNT(*). // We first collect a.ID into primaryKeys. Then collect "a" into primaryVarNames. // Finally, we loop through all group by keys to put non-primary key properties under name "a" // into dependentKeyExpressions. - // Collect primary keys from group keys. - std::vector primaryKeys; - for (auto& expression : keys) { - if (expression->expressionType == PROPERTY) { - auto propertyExpression = (binder::PropertyExpression*)expression.get(); - if (propertyExpression->isPrimaryKey() || propertyExpression->isInternalID()) { - primaryKeys.push_back(propertyExpression); + // Collect primary variables from keys. + std::unordered_set primaryVarNames; + for (auto& key : inputKeys) { + if (key->expressionType == PROPERTY) { + auto property = (PropertyExpression*)key.get(); + if (property->isPrimaryKey() || property->isInternalID()) { + primaryVarNames.insert(property->getVariableName()); } } } - // Collect variable names whose primary key is part of group keys. - std::unordered_set primaryVarNames; - for (auto& primaryKey : primaryKeys) { - primaryVarNames.insert(primaryKey->getVariableName()); - } - binder::expression_vector groupExpressions; - binder::expression_vector dependentExpressions; - for (auto& expression : keys) { - if (expression->expressionType == PROPERTY) { - auto propertyExpression = (binder::PropertyExpression*)expression.get(); - if (propertyExpression->isPrimaryKey() || - propertyExpression->isInternalID()) { // NOLINT(bugprone-branch-clone): Collapsing - // is a logical error. - groupExpressions.push_back(expression); - } else if (primaryVarNames.contains(propertyExpression->getVariableName())) { - dependentExpressions.push_back(expression); + // Resolve key dependency. + binder::expression_vector keys; + binder::expression_vector dependentKeys; + for (auto& key : inputKeys) { + if (key->expressionType == PROPERTY) { + auto property = (PropertyExpression*)key.get(); + if (property->isPrimaryKey() || + property->isInternalID()) { // NOLINT(bugprone-branch-clone): Collapsing + // is a logical error. + // Primary properties are always keys. + keys.push_back(key); + } else if (primaryVarNames.contains(property->getVariableName())) { + // Properties depend on any primary property are dependent keys. + // e.g. a.age depends on a._id + dependentKeys.push_back(key); + } else { + keys.push_back(key); + } + } else if (ExpressionUtil::isNodeVariable(*key) || ExpressionUtil::isRelVariable(*key)) { + if (primaryVarNames.contains(key->getUniqueName())) { + // e.g. a depends on a._id + dependentKeys.push_back(key); } else { - groupExpressions.push_back(expression); + keys.push_back(key); } - } else if (ExpressionUtil::isNodeVariable(*expression) || - ExpressionUtil::isRelVariable(*expression)) { - dependentExpressions.push_back(expression); } else { - groupExpressions.push_back(expression); + keys.push_back(key); } } - return std::make_pair(std::move(groupExpressions), std::move(dependentExpressions)); + return std::make_pair(std::move(keys), std::move(dependentKeys)); } } // namespace optimizer diff --git a/src/processor/operator/aggregate/aggregate_hash_table.cpp b/src/processor/operator/aggregate/aggregate_hash_table.cpp index ce4b62eb82..377c0da595 100644 --- a/src/processor/operator/aggregate/aggregate_hash_table.cpp +++ b/src/processor/operator/aggregate/aggregate_hash_table.cpp @@ -1,5 +1,6 @@ #include "processor/operator/aggregate/aggregate_hash_table.h" +#include "common/null_buffer.h" #include "common/utils.h" #include "function/aggregate/base_count.h" #include "function/hash/vector_hash_functions.h" @@ -138,7 +139,7 @@ void AggregateHashTable::initializeFT( for (auto& dataType : keyDataTypes) { auto size = LogicalTypeUtils::getRowLayoutSize(dataType); tableSchema->appendColumn(std::make_unique(isUnflat, dataChunkPos, size)); - getCompareEntryWithKeysFunc(dataType.getPhysicalType(), compareFuncs[colIdx]); + getCompareEntryWithKeysFunc(dataType, compareFuncs[colIdx]); numBytesForKeys += size; colIdx++; } @@ -466,7 +467,6 @@ bool AggregateHashTable::matchFlatGroupByKeys( auto keyVector = keyVectors[i]; assert(keyVector->state->isFlat()); auto pos = keyVector->state->selVector->selectedPositions[0]; - auto keyValue = keyVector->getData() + pos * keyVector->getNumBytesPerValue(); auto isKeyVectorNull = keyVector->isNull(pos); auto isEntryKeyNull = factorizedTable->isNonOverflowColNull( entry + factorizedTable->getTableSchema()->getNullMapOffset(), i); @@ -478,7 +478,7 @@ bool AggregateHashTable::matchFlatGroupByKeys( return false; } if (!compareFuncs[i]( - keyValue, entry + factorizedTable->getTableSchema()->getColOffset(i))) { + keyVector, pos, entry + factorizedTable->getTableSchema()->getColOffset(i))) { return false; } } @@ -494,8 +494,8 @@ uint64_t AggregateHashTable::matchUnFlatVecWithFTColumn( if (factorizedTable->hasNoNullGuarantee(colIdx)) { for (auto i = 0u; i < numMayMatches; i++) { auto idx = mayMatchIdxes[i]; - if (compareFuncs[colIdx](vector->getData() + idx * vector->getNumBytesPerValue(), - hashSlotsToUpdateAggState[idx]->entry + colOffset)) { + if (compareFuncs[colIdx]( + vector, idx, hashSlotsToUpdateAggState[idx]->entry + colOffset)) { mayMatchIdxes[mayMatchIdx++] = idx; } else { noMatchIdxes[numNoMatches++] = idx; @@ -504,7 +504,6 @@ uint64_t AggregateHashTable::matchUnFlatVecWithFTColumn( } else { for (auto i = 0u; i < numMayMatches; i++) { auto idx = mayMatchIdxes[i]; - auto value = vector->getData() + idx * vector->getNumBytesPerValue(); auto isEntryKeyNull = factorizedTable->isNonOverflowColNull( hashSlotsToUpdateAggState[idx]->entry + factorizedTable->getTableSchema()->getNullMapOffset(), @@ -514,7 +513,7 @@ uint64_t AggregateHashTable::matchUnFlatVecWithFTColumn( continue; } if (compareFuncs[colIdx]( - value, hashSlotsToUpdateAggState[idx]->entry + colOffset)) { + vector, idx, hashSlotsToUpdateAggState[idx]->entry + colOffset)) { mayMatchIdxes[mayMatchIdx++] = idx; } else { noMatchIdxes[numNoMatches++] = idx; @@ -524,7 +523,6 @@ uint64_t AggregateHashTable::matchUnFlatVecWithFTColumn( } else { for (auto i = 0u; i < numMayMatches; i++) { auto idx = mayMatchIdxes[i]; - auto value = vector->getData() + idx * vector->getNumBytesPerValue(); auto isKeyVectorNull = vector->isNull(idx); auto isEntryKeyNull = factorizedTable->isNonOverflowColNull( hashSlotsToUpdateAggState[idx]->entry + @@ -538,7 +536,8 @@ uint64_t AggregateHashTable::matchUnFlatVecWithFTColumn( continue; } - if (compareFuncs[colIdx](value, hashSlotsToUpdateAggState[idx]->entry + colOffset)) { + if (compareFuncs[colIdx]( + vector, idx, hashSlotsToUpdateAggState[idx]->entry + colOffset)) { mayMatchIdxes[mayMatchIdx++] = idx; } else { noMatchIdxes[numNoMatches++] = idx; @@ -555,7 +554,6 @@ uint64_t AggregateHashTable::matchFlatVecWithFTColumn( uint64_t mayMatchIdx = 0; auto pos = vector->state->selVector->selectedPositions[0]; auto isVectorNull = vector->isNull(pos); - auto value = vector->getData() + pos * vector->getNumBytesPerValue(); for (auto i = 0u; i < numMayMatches; i++) { auto idx = mayMatchIdxes[i]; auto isEntryKeyNull = factorizedTable->isNonOverflowColNull( @@ -569,7 +567,7 @@ uint64_t AggregateHashTable::matchFlatVecWithFTColumn( noMatchIdxes[numNoMatches++] = idx; continue; } - if (compareFuncs[colIdx](value, hashSlotsToUpdateAggState[idx]->entry + colOffset)) { + if (compareFuncs[colIdx](vector, pos, hashSlotsToUpdateAggState[idx]->entry + colOffset)) { mayMatchIdxes[mayMatchIdx++] = idx; } else { noMatchIdxes[numNoMatches++] = idx; @@ -632,68 +630,105 @@ void AggregateHashTable::resizeHashTableIfNecessary(uint32_t maxNumDistinctHashK } } +template +static bool compareEntry(common::ValueVector* vector, uint32_t vectorPos, const uint8_t* entry) { + uint8_t result; + auto key = vector->getData() + vectorPos * vector->getNumBytesPerValue(); + kuzu::function::Equals::operation( + *(T*)key, *(T*)entry, result, nullptr /* leftVector */, nullptr /* rightVector */); + return result != 0; +} + +static bool compareNodeEntry( + common::ValueVector* vector, uint32_t vectorPos, const uint8_t* entry) { + assert(0 == common::StructType::getFieldIdx(&vector->dataType, common::InternalKeyword::ID)); + auto idVector = common::StructVector::getFieldVector(vector, 0).get(); + return compareEntry(idVector, vectorPos, + entry + common::NullBuffer::getNumBytesForNullValues( + common::StructType::getNumFields(&vector->dataType))); +} + +static bool compareRelEntry(common::ValueVector* vector, uint32_t vectorPos, const uint8_t* entry) { + assert(3 == common::StructType::getFieldIdx(&vector->dataType, common::InternalKeyword::ID)); + auto idVector = common::StructVector::getFieldVector(vector, 3).get(); + return compareEntry(idVector, vectorPos, + entry + sizeof(common::internalID_t) * 2 + sizeof(common::ku_string_t) + + common::NullBuffer::getNumBytesForNullValues( + common::StructType::getNumFields(&vector->dataType))); +} + void AggregateHashTable::getCompareEntryWithKeysFunc( - PhysicalTypeID physicalType, compare_function_t& func) { - switch (physicalType) { + const LogicalType& logicalType, compare_function_t& func) { + switch (logicalType.getPhysicalType()) { case PhysicalTypeID::INTERNAL_ID: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::BOOL: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::INT64: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::INT32: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::INT16: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::INT8: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::UINT64: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::UINT32: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::UINT16: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::UINT8: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::DOUBLE: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::FLOAT: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::STRING: { - func = compareEntryWithKeys; + func = compareEntry; return; } case PhysicalTypeID::INTERVAL: { - func = compareEntryWithKeys; + func = compareEntry; return; } + case PhysicalTypeID::STRUCT: { + if (logicalType.getLogicalTypeID() == LogicalTypeID::NODE) { + func = compareNodeEntry; + return; + } else if (logicalType.getLogicalTypeID() == LogicalTypeID::REL) { + func = compareRelEntry; + return; + } + } default: { throw RuntimeException( - "Cannot compare data type " + PhysicalTypeUtils::physicalTypeToString(physicalType)); + "Cannot compare data type " + + PhysicalTypeUtils::physicalTypeToString(logicalType.getPhysicalType())); } } } diff --git a/test/test_files/issue/issue.test b/test/test_files/issue/issue.test index 7dfddf8ae1..6e881c9199 100644 --- a/test/test_files/issue/issue.test +++ b/test/test_files/issue/issue.test @@ -27,3 +27,38 @@ -STATEMENT MATCH (t:Test) RETURN t; ---- 1 {_ID: 0:0, _LABEL: Test, id: 0, content: mycontent} + +-CASE Kind-Of-Fruit +-STATEMENT CREATE NODE TABLE T(name STRING, PRIMARY KEY(name)); +---- ok +-STATEMENT CREATE NODE TABLE Fruit(name STRING, PRIMARY KEY(name)); +---- ok +-STATEMENT CREATE REL TABLE LIKES(FROM T TO Fruit); +---- ok +-STATEMENT CREATE REL TABLE KIND_OF(FROM T TO T); +---- ok +-STATEMENT CREATE + (t1:T {name: "T1"}), + (t2:T {name: "T2"}), + (t3:T {name: "T3"}), + (f1:Fruit {name: "Banana"}), + (f2:Fruit {name: "Orange"}), + (f3:Fruit {name: "Apple"}), + (t1)-[:LIKES]->(f1), + (t2)-[:LIKES]->(f2), + (t3)-[:LIKES]->(f3), + (t1)-[:KIND_OF]->(t2), + (t2)-[:KIND_OF]->(t3); +---- ok +-STATEMENT MATCH (t1:T)-[:KIND_OF*]->(t2:T), (t1)-[:LIKES]->(f1:Fruit), (t2)-[:LIKES]->(f2:Fruit) + WHERE f1.name = "Apple" and f2.name = "Banana" OR f1.name = "Banana" and f2.name = "Apple" return t1 as t +---- 1 +{_ID: 0:0, _LABEL: T, name: T1} +-STATEMENT MATCH (t:T)-[:LIKES]->(:Fruit {name: "Apple"}), (t)-[:LIKES]->(:Fruit {name: "Banana"}) return t; +---- 0 +-STATEMENT MATCH (t1:T)-[:KIND_OF*]->(t2:T), (t1)-[:LIKES]->(f1:Fruit), (t2)-[:LIKES]->(f2:Fruit) + WHERE f1.name = "Apple" and f2.name = "Banana" OR f1.name = "Banana" and f2.name = "Apple" return t1 as t + UNION + MATCH (t:T)-[:LIKES]->(:Fruit {name: "Apple"}), (t)-[:LIKES]->(:Fruit {name: "Banana"}) return t; +---- 1 +{_ID: 0:0, _LABEL: T, name: T1} diff --git a/test/test_files/tinysnb/agg/distinct_agg.test b/test/test_files/tinysnb/agg/distinct_agg.test index 231d0d8383..57cc237b5c 100644 --- a/test/test_files/tinysnb/agg/distinct_agg.test +++ b/test/test_files/tinysnb/agg/distinct_agg.test @@ -41,3 +41,25 @@ ---- 2 1|[True,False] 2|[True,False] + +-STATEMENT MATCH (p:person)-[:knows]->(b:person)-[:knows]->(c:person) RETURN DISTINCT c; +---- 4 +{_ID: 0:0, _LABEL: person, ID: 0, fName: Alice, gender: 1, isStudent: True, isWorker: False, age: 35, eyeSight: 5.000000, birthdate: 1900-01-01, registerTime: 2011-08-20 11:25:30, lastJobDuration: 3 years 2 days 13:02:00, workedHours: [10,5], usedNames: [Aida], courseScoresPerTerm: [[10,8],[6,7,8]], grades: [96,54,86,92], height: 1.731000} +{_ID: 0:1, _LABEL: person, ID: 2, fName: Bob, gender: 2, isStudent: True, isWorker: False, age: 30, eyeSight: 5.100000, birthdate: 1900-01-01, registerTime: 2008-11-03 15:25:30.000526, lastJobDuration: 10 years 5 months 13:00:00.000024, workedHours: [12,8], usedNames: [Bobby], courseScoresPerTerm: [[8,9],[9,10]], grades: [98,42,93,88], height: 0.990000} +{_ID: 0:2, _LABEL: person, ID: 3, fName: Carol, gender: 1, isStudent: False, isWorker: True, age: 45, eyeSight: 5.000000, birthdate: 1940-06-22, registerTime: 1911-08-20 02:32:21, lastJobDuration: 48:24:11, workedHours: [4,5], usedNames: [Carmen,Fred], courseScoresPerTerm: [[8,10]], grades: [91,75,21,95], height: 1.000000} +{_ID: 0:3, _LABEL: person, ID: 5, fName: Dan, gender: 2, isStudent: False, isWorker: True, age: 20, eyeSight: 4.800000, birthdate: 1950-07-23, registerTime: 2031-11-30 12:25:30, lastJobDuration: 10 years 5 months 13:00:00.000024, workedHours: [1,9], usedNames: [Wolfeschlegelstein,Daniel], courseScoresPerTerm: [[7,4],[8,8],[9]], grades: [76,88,99,89], height: 1.300000} + +-STATEMENT MATCH (p:person)-[:knows]->(b:person)-[e:knows]->(c:person)-[:knows]->(:person) RETURN DISTINCT e; +---- 12 +(0:0)-{_LABEL: knows, _ID: 3:0, date: 2021-06-30, meetTime: 1986-10-21 21:08:31.521, validInterval: 10 years 5 months 13:00:00.000024, comments: [rnme,m8sihsdnf2990nfiwf]}->(0:1) +(0:0)-{_LABEL: knows, _ID: 3:1, date: 2021-06-30, meetTime: 1946-08-25 19:07:22, validInterval: 20 years 30 days 48:00:00, comments: [njnojppo9u0jkmf,fjiojioh9h9h89hph]}->(0:2) +(0:0)-{_LABEL: knows, _ID: 3:2, date: 2021-06-30, meetTime: 2012-12-11 20:07:22, validInterval: 10 days, comments: [ioji232,jifhe8w99u43434]}->(0:3) +(0:1)-{_LABEL: knows, _ID: 3:3, date: 2021-06-30, meetTime: 1946-08-25 19:07:22, validInterval: 10 years 5 months 13:00:00.000024, comments: [2huh9y89fsfw23,23nsihufhw723]}->(0:0) +(0:1)-{_LABEL: knows, _ID: 3:4, date: 1950-05-14, meetTime: 1946-08-25 19:07:22, validInterval: 00:23:00, comments: [fwehu9h9832wewew,23u9h989sdfsss]}->(0:2) +(0:1)-{_LABEL: knows, _ID: 3:5, date: 1950-05-14, meetTime: 2012-12-11 20:07:22, validInterval: 20 years 30 days 48:00:00, comments: [fwh9y81232uisuiehuf,ewnuihxy8dyf232]}->(0:3) +(0:2)-{_LABEL: knows, _ID: 3:6, date: 2021-06-30, meetTime: 2002-07-31 11:42:53.12342, validInterval: 40 days 30:00:00, comments: [fnioh8323aeweae34d,osd89e2ejshuih12]}->(0:0) +(0:2)-{_LABEL: knows, _ID: 3:7, date: 1950-05-14, meetTime: 2007-02-12 12:11:42.123, validInterval: 00:28:00.03, comments: [fwh983-sdjisdfji,ioh89y32r2huir]}->(0:1) +(0:2)-{_LABEL: knows, _ID: 3:8, date: 2000-01-01, meetTime: 1998-10-02 13:09:22.423, validInterval: 00:00:00.3, comments: [psh989823oaaioe,nuiuah1nosndfisf]}->(0:3) +(0:3)-{_LABEL: knows, _ID: 3:10, date: 1950-05-14, meetTime: 1982-11-11 13:12:05.123, validInterval: 00:23:00, comments: [fewh9182912e3,h9y8y89soidfsf,nuhudf78w78efw,hioshe0f9023sdsd]}->(0:1) +(0:3)-{_LABEL: knows, _ID: 3:11, date: 2000-01-01, meetTime: 1999-04-21 15:12:11.42, validInterval: 48:00:00.052, comments: [23h9sdslnfowhu2932,shuhf98922323sf]}->(0:2) +(0:3)-{_LABEL: knows, _ID: 3:9, date: 2021-06-30, meetTime: 1936-11-02 11:02:01, validInterval: 00:00:00.00048, comments: [fwewe]}->(0:0)