diff --git a/src/codec/RowReaderV2.cpp b/src/codec/RowReaderV2.cpp index 9c9ef343edc..47101dd9239 100644 --- a/src/codec/RowReaderV2.cpp +++ b/src/codec/RowReaderV2.cpp @@ -2,13 +2,44 @@ * * This source code is licensed under Apache 2.0 License. */ - #include "codec/RowReaderV2.h" namespace nebula { using nebula::cpp2::PropertyType; +template +Value extractIntOrFloat(const folly::StringPiece& data, size_t& offset) { + int32_t containerOffset; + memcpy(reinterpret_cast(&containerOffset), data.data() + offset, sizeof(int32_t)); + if (static_cast(containerOffset) >= data.size()) { + LOG(ERROR) << "Container offset out of bounds. Offset: " << containerOffset + << ", Data size: " << data.size(); + return Value::kNullValue; + } + int32_t containerSize; + memcpy(reinterpret_cast(&containerSize), data.data() + containerOffset, sizeof(int32_t)); + containerOffset += sizeof(int32_t); + Container container; + for (int32_t i = 0; i < containerSize; ++i) { + T value; + if (static_cast(containerOffset + sizeof(T)) > data.size()) { + LOG(ERROR) << "Reading beyond data bounds. Attempting to read at offset: " << containerOffset + << ", Data size: " << data.size(); + return Value::kNullValue; + } + memcpy(reinterpret_cast(&value), data.data() + containerOffset, sizeof(T)); + containerOffset += sizeof(T); + + if constexpr (std::is_same_v) { + container.values.emplace_back(Value(value)); + } else if constexpr (std::is_same_v) { + container.values.insert(Value(value)); + } + } + return Value(std::move(container)); +} + bool RowReaderV2::resetImpl(meta::NebulaSchemaProvider const* schema, folly::StringPiece row) { schema_ = schema; data_ = row; @@ -206,6 +237,70 @@ Value RowReaderV2::getValueByIndex(const int64_t index) const { } return std::move(geogRet).value(); } + case PropertyType::LIST_STRING: { + int32_t listOffset; + memcpy(reinterpret_cast(&listOffset), &data_[offset], sizeof(int32_t)); + if (static_cast(listOffset) >= data_.size()) { + LOG(ERROR) << "List offset out of bounds for LIST_STRING."; + return Value::kNullValue; + } + int32_t listSize; + memcpy(reinterpret_cast(&listSize), &data_[listOffset], sizeof(int32_t)); + listOffset += sizeof(int32_t); + + List list; + for (int32_t i = 0; i < listSize; ++i) { + int32_t strLen; + memcpy(reinterpret_cast(&strLen), &data_[listOffset], sizeof(int32_t)); + listOffset += sizeof(int32_t); + if (static_cast(listOffset + strLen) > data_.size()) { + LOG(ERROR) << "String length out of bounds for LIST_STRING."; + return Value::kNullValue; + } + std::string str(&data_[listOffset], strLen); + listOffset += strLen; + list.values.emplace_back(str); + } + return Value(std::move(list)); + } + case PropertyType::LIST_INT: + return nebula::extractIntOrFloat(data_, offset); + case PropertyType::LIST_FLOAT: + return nebula::extractIntOrFloat(data_, offset); + case PropertyType::SET_STRING: { + int32_t setOffset; + memcpy(reinterpret_cast(&setOffset), &data_[offset], sizeof(int32_t)); + if (static_cast(setOffset) >= data_.size()) { + LOG(ERROR) << "Set offset out of bounds for SET_STRING."; + return Value::kNullValue; + } + int32_t setSize; + memcpy(reinterpret_cast(&setSize), &data_[setOffset], sizeof(int32_t)); + setOffset += sizeof(int32_t); + + Set set; + std::unordered_set uniqueStrings; + for (int32_t i = 0; i < setSize; ++i) { + int32_t strLen; + memcpy(reinterpret_cast(&strLen), &data_[setOffset], sizeof(int32_t)); + setOffset += sizeof(int32_t); + if (static_cast(setOffset + strLen) > data_.size()) { + LOG(ERROR) << "String length out of bounds for SET_STRING."; + return Value::kNullValue; + } + std::string str(&data_[setOffset], strLen); + setOffset += strLen; + uniqueStrings.insert(std::move(str)); + } + for (const auto& str : uniqueStrings) { + set.values.insert(Value(str)); + } + return Value(std::move(set)); + } + case PropertyType::SET_INT: + return nebula::extractIntOrFloat(data_, offset); + case PropertyType::SET_FLOAT: + return nebula::extractIntOrFloat(data_, offset); case PropertyType::UNKNOWN: break; } diff --git a/src/codec/RowWriterV2.cpp b/src/codec/RowWriterV2.cpp index 1e4245559fa..b2ca647b0fa 100644 --- a/src/codec/RowWriterV2.cpp +++ b/src/codec/RowWriterV2.cpp @@ -16,6 +16,80 @@ namespace nebula { using nebula::cpp2::PropertyType; +// Function used to identify the data type written +WriteResult writeItem(const Value& item, Value::Type valueType, std::string& buffer) { + switch (valueType) { + case Value::Type::STRING: { + std::string str = item.getStr(); + int32_t strLen = str.size(); + buffer.append(reinterpret_cast(&strLen), sizeof(int32_t)); + buffer.append(str.data(), strLen); + break; + } + case Value::Type::INT: { + int32_t intVal = item.getInt(); + buffer.append(reinterpret_cast(&intVal), sizeof(int32_t)); + break; + } + case Value::Type::FLOAT: { + float floatVal = item.getFloat(); + buffer.append(reinterpret_cast(&floatVal), sizeof(float)); + break; + } + default: + LOG(ERROR) << "Unsupported value type: " << static_cast(valueType); + return WriteResult::TYPE_MISMATCH; + } + return WriteResult::SUCCEEDED; +} + +// Function used to identify List data types (List, List, List) +template +WriteResult writeList(const List& container, Value::Type valueType, std::string& buffer) { + int32_t listSize = container.values.size(); + if (listSize == 0) { + return WriteResult::SUCCEEDED; + } + for (const auto& item : container.values) { + if (item.type() != valueType) { + LOG(ERROR) << "Type mismatch: Expected " << static_cast(valueType) << " but got " + << static_cast(item.type()); + return WriteResult::TYPE_MISMATCH; + } + } + for (const auto& item : container.values) { + auto result = writeItem(item, valueType, buffer); + if (result != WriteResult::SUCCEEDED) { + return result; + } + } + return WriteResult::SUCCEEDED; +} + +// Function used to identify Set data types (Set, Set, Set) +template +WriteResult writeSet(const Set& container, Value::Type valueType, std::string& buffer) { + for (const auto& item : container.values) { + if (item.type() != valueType) { + LOG(ERROR) << "Type mismatch: Expected " << static_cast(valueType) << " but got " + << static_cast(item.type()); + return WriteResult::TYPE_MISMATCH; + } + } + std::unordered_set serialized; + for (const auto& item : container.values) { + if (serialized.find(item) != serialized.end()) { + continue; + } + auto result = writeItem(item, valueType, buffer); + if (result != WriteResult::SUCCEEDED) { + return result; + } + serialized.insert(item); + } + return WriteResult::SUCCEEDED; +} + RowWriterV2::RowWriterV2(const meta::NebulaSchemaProvider* schema) : schema_(schema), numNullBytes_(0), approxStrLen_(0), finished_(false), outOfSpaceStr_(false) { CHECK(!!schema_); @@ -138,6 +212,12 @@ RowWriterV2::RowWriterV2(RowReaderWrapper& reader) : RowWriterV2(reader.getSchem case Value::Type::DURATION: set(i, v.moveDuration()); break; + case Value::Type::LIST: + set(i, v.moveList()); + break; + case Value::Type::SET: + set(i, v.moveSet()); + break; default: LOG(FATAL) << "Invalid data: " << v << ", type: " << v.typeName(); isSet_[i] = false; @@ -226,11 +306,14 @@ WriteResult RowWriterV2::setValue(ssize_t index, const Value& val) { return write(index, val.getGeography()); case Value::Type::DURATION: return write(index, val.getDuration()); + case Value::Type::LIST: + return write(index, val.getList()); + case Value::Type::SET: + return write(index, val.getSet()); default: return WriteResult::TYPE_MISMATCH; } } - WriteResult RowWriterV2::setValue(const std::string& name, const Value& val) { CHECK(!finished_) << "You have called finish()"; int64_t index = schema_->getFieldIndex(name); @@ -821,6 +904,78 @@ WriteResult RowWriterV2::write(ssize_t index, const Geography& v) { return write(index, folly::StringPiece(wkb), true); } +WriteResult RowWriterV2::write(ssize_t index, const List& list) { + auto field = schema_->field(index); + auto offset = headerLen_ + numNullBytes_ + field->offset(); + int32_t listSize = list.size(); + int32_t listOffset = buf_.size(); + if (listSize > kMaxArraySize) { + LOG(ERROR) << "List size exceeds the maximum allowed length of " << kMaxArraySize; + return WriteResult::OUT_OF_RANGE; + } + if (isSet_[index]) { + outOfSpaceStr_ = true; + } + buf_.append(reinterpret_cast(&listSize), sizeof(int32_t)); + Value::Type valueType; + if (field->type() == PropertyType::LIST_STRING) { + valueType = Value::Type::STRING; + } else if (field->type() == PropertyType::LIST_INT) { + valueType = Value::Type::INT; + } else if (field->type() == PropertyType::LIST_FLOAT) { + valueType = Value::Type::FLOAT; + } else { + LOG(ERROR) << "Unsupported list type: " << static_cast(field->type()); + return WriteResult::TYPE_MISMATCH; + } + auto result = writeList(list, valueType, buf_); + if (result != WriteResult::SUCCEEDED) { + return result; + } + memcpy(&buf_[offset], reinterpret_cast(&listOffset), sizeof(int32_t)); + if (field->nullable()) { + clearNullBit(field->nullFlagPos()); + } + isSet_[index] = true; + return WriteResult::SUCCEEDED; +} + +WriteResult RowWriterV2::write(ssize_t index, const Set& set) { + auto field = schema_->field(index); + auto offset = headerLen_ + numNullBytes_ + field->offset(); + int32_t setSize = set.size(); + int32_t setOffset = buf_.size(); + if (setSize > kMaxArraySize) { + LOG(ERROR) << "Set size exceeds the maximum allowed length of " << kMaxArraySize; + return WriteResult::OUT_OF_RANGE; + } + if (isSet_[index]) { + outOfSpaceStr_ = true; + } + buf_.append(reinterpret_cast(&setSize), sizeof(int32_t)); + Value::Type valueType; + if (field->type() == PropertyType::SET_STRING) { + valueType = Value::Type::STRING; + } else if (field->type() == PropertyType::SET_INT) { + valueType = Value::Type::INT; + } else if (field->type() == PropertyType::SET_FLOAT) { + valueType = Value::Type::FLOAT; + } else { + LOG(ERROR) << "Unsupported set type: " << static_cast(field->type()); + return WriteResult::TYPE_MISMATCH; + } + auto result = writeSet(set, valueType, buf_); + if (result != WriteResult::SUCCEEDED) { + return result; + } + memcpy(&buf_[offset], reinterpret_cast(&setOffset), sizeof(int32_t)); + if (field->nullable()) { + clearNullBit(field->nullFlagPos()); + } + isSet_[index] = true; + return WriteResult::SUCCEEDED; +} + WriteResult RowWriterV2::checkUnsetFields() { DefaultValueContext expCtx; for (size_t i = 0; i < schema_->getNumFields(); i++) { @@ -868,6 +1023,12 @@ WriteResult RowWriterV2::checkUnsetFields() { case Value::Type::DURATION: r = write(i, defVal.getDuration()); break; + case Value::Type::LIST: + r = write(i, defVal.getList()); + break; + case Value::Type::SET: + r = write(i, defVal.getSet()); + break; default: LOG(FATAL) << "Unsupported default value type: " << defVal.typeName() << ", default value: " << defVal diff --git a/src/codec/RowWriterV2.h b/src/codec/RowWriterV2.h index 6c544d0c392..c1560e62a56 100644 --- a/src/codec/RowWriterV2.h +++ b/src/codec/RowWriterV2.h @@ -221,6 +221,8 @@ class RowWriterV2 { size_t numNullBytes_; size_t approxStrLen_; bool finished_; + // Limit array length + static constexpr int32_t kMaxArraySize = 65535; // When outOfSpaceStr_ is true, variant length string fields // could hold an index, referring to the strings in the strList_ @@ -263,6 +265,12 @@ class RowWriterV2 { WriteResult write(ssize_t index, const Duration& v); WriteResult write(ssize_t index, const Geography& v); + // Supports storing ordered lists of strings, integers, and floats, + // including LIST_STRING, LIST_INT, and LIST_FLOAT. + WriteResult write(ssize_t index, const List& list); + // Supports storing unordered sets of strings, integers, and floats, + // including SET_STRING, SET_INT, and SET_FLOAT + WriteResult write(ssize_t index, const Set& set); }; } // namespace nebula diff --git a/src/common/datatypes/List.h b/src/common/datatypes/List.h index a2694c52ca8..1d801d4fc43 100644 --- a/src/common/datatypes/List.h +++ b/src/common/datatypes/List.h @@ -24,6 +24,10 @@ struct List { } explicit List(const std::vector& l) : values(l) {} + // Template static factory method to create a list with specific types + template + static List createFromVector(const std::vector& items); + bool empty() const { return values.empty(); } @@ -102,6 +106,16 @@ inline std::ostream& operator<<(std::ostream& os, const List& l) { return os << l.toString(); } +// Define using template static factory method +template +inline List List::createFromVector(const std::vector& items) { + std::vector values; + values.reserve(items.size()); + for (const auto& item : items) { + values.emplace_back(Value(item)); + } + return List(std::move(values)); +} } // namespace nebula namespace std { diff --git a/src/common/datatypes/Set.h b/src/common/datatypes/Set.h index b7194a16f6b..c1387a8f8bf 100644 --- a/src/common/datatypes/Set.h +++ b/src/common/datatypes/Set.h @@ -22,6 +22,10 @@ struct Set { values = std::move(value); } + // Template Static Factory Method Declaration + template + static Set createFromVector(const std::vector& items); + void clear() { values.clear(); } @@ -68,6 +72,16 @@ struct Set { inline std::ostream& operator<<(std::ostream& os, const Set& s) { return os << s.toString(); } + +// define using template static factory method +template +inline Set Set::createFromVector(const std::vector& items) { + std::unordered_set values; + for (const auto& item : items) { + values.emplace(Value(item)); + } + return Set(std::move(values)); +} } // namespace nebula namespace std { diff --git a/src/common/expression/test/AggregateExpressionTest.cpp b/src/common/expression/test/AggregateExpressionTest.cpp index 9a7281549e1..f306615c785 100644 --- a/src/common/expression/test/AggregateExpressionTest.cpp +++ b/src/common/expression/test/AggregateExpressionTest.cpp @@ -223,26 +223,34 @@ TEST_F(AggregateExpressionTest, AggregateExpression) { TEST_AGG(MAX, true, isConst, vals2_, expected2); } { - const std::unordered_map expected1 = { - {"a", Value(List({1, 3}))}, {"b", Value(List({4}))}, {"c", Value(List({3, 8, 5, 8}))}}; + // Modify the testing of List type + const std::unordered_map expected1 = { + {"a", nebula::Value(nebula::List::createFromVector(std::vector{1, 3}))}, + {"b", nebula::Value(nebula::List::createFromVector(std::vector{4}))}, + {"c", nebula::Value(nebula::List::createFromVector(std::vector{3, 8, 5, 8}))}}; TEST_AGG(COLLECT, false, abs, vals1_, expected1); TEST_AGG(COLLECT, false, isConst, vals2_, expected1); - const std::unordered_map expected2 = { - {"a", List({1, 3})}, {"b", List({4})}, {"c", List({3, 8, 5})}}; + const std::unordered_map expected2 = { + {"a", nebula::List::createFromVector(std::vector{1, 3})}, + {"b", nebula::List::createFromVector(std::vector{4})}, + {"c", nebula::List::createFromVector(std::vector{3, 8, 5})}}; TEST_AGG(COLLECT, true, abs, vals1_, expected2); TEST_AGG(COLLECT, true, isConst, vals2_, expected2); } { - const std::unordered_map expected1 = { - {"b", Set({4})}, {"a", Set({1, 3})}, {"c", Set({3, 8, 5})}}; - TEST_AGG(COLLECT_SET, false, abs, vals1_, expected1); - TEST_AGG(COLLECT_SET, false, isConst, vals2_, expected1); - - const std::unordered_map expected2 = { - {"b", Set({4})}, {"a", Set({1, 3})}, {"c", Set({3, 8, 5})}}; - TEST_AGG(COLLECT_SET, true, abs, vals1_, expected2); - TEST_AGG(COLLECT_SET, true, isConst, vals2_, expected2); + // Modify the testing of Set type + const std::unordered_map expected1_set = { + {"a", nebula::Value(nebula::Set::createFromVector(std::vector{1, 3}))}, + {"b", nebula::Value(nebula::Set::createFromVector(std::vector{4}))}, + {"c", nebula::Value(nebula::Set::createFromVector(std::vector{3, 8, 5}))}}; + + const std::unordered_map expected2_set = { + {"a", nebula::Set::createFromVector(std::vector{1, 3})}, + {"b", nebula::Set::createFromVector(std::vector{4})}, + {"c", nebula::Set::createFromVector(std::vector{3, 8, 5})}}; + TEST_AGG(COLLECT_SET, true, abs, vals1_, expected2_set); + TEST_AGG(COLLECT_SET, true, isConst, vals2_, expected2_set); } { const std::unordered_map expected1 = { diff --git a/src/common/function/FunctionManager.cpp b/src/common/function/FunctionManager.cpp index f654ba64dd0..ca159d75dee 100644 --- a/src/common/function/FunctionManager.cpp +++ b/src/common/function/FunctionManager.cpp @@ -161,7 +161,25 @@ std::unordered_map> FunctionManager::typ {"right", {TypeSignature({Value::Type::STRING, Value::Type::INT}, Value::Type::STRING)}}, {"replace", {TypeSignature({Value::Type::STRING, Value::Type::STRING, Value::Type::STRING}, - Value::Type::STRING)}}, + Value::Type::STRING), + TypeSignature({Value::Type::LIST, Value::Type::STRING, Value::Type::STRING}, + Value::Type::LIST), + TypeSignature({Value::Type::LIST, Value::Type::INT, Value::Type::INT}, Value::Type::LIST), + TypeSignature({Value::Type::LIST, Value::Type::FLOAT, Value::Type::FLOAT}, Value::Type::LIST), + TypeSignature({Value::Type::SET, Value::Type::STRING, Value::Type::STRING}, Value::Type::SET), + TypeSignature({Value::Type::SET, Value::Type::INT, Value::Type::INT}, Value::Type::SET), + TypeSignature({Value::Type::SET, Value::Type::FLOAT, Value::Type::FLOAT}, Value::Type::SET)}}, + {"erase", + {TypeSignature({Value::Type::LIST, Value::Type::STRING}, Value::Type::LIST), + TypeSignature({Value::Type::LIST, Value::Type::INT}, Value::Type::LIST), + TypeSignature({Value::Type::LIST, Value::Type::FLOAT}, Value::Type::LIST), + TypeSignature({Value::Type::SET, Value::Type::STRING}, Value::Type::SET), + TypeSignature({Value::Type::SET, Value::Type::INT}, Value::Type::SET), + TypeSignature({Value::Type::SET, Value::Type::FLOAT}, Value::Type::SET)}}, + {"setadd", + {TypeSignature({Value::Type::SET, Value::Type::STRING}, Value::Type::SET), + TypeSignature({Value::Type::SET, Value::Type::INT}, Value::Type::SET), + TypeSignature({Value::Type::SET, Value::Type::FLOAT}, Value::Type::SET)}}, {"reverse", {TypeSignature({Value::Type::STRING}, Value::Type::STRING), TypeSignature({Value::Type::LIST}, Value::Type::LIST)}}, @@ -1377,6 +1395,65 @@ FunctionManager::FunctionManager() { std::string newStr(args[2].get().getStr()); return boost::replace_all_copy(origStr, search, newStr); } + if (args[0].get().isList()) { + List list = args[0].get().getList(); + auto search = args[1].get(); + auto newValue = args[2].get(); + for (auto &item : list.values) { + if (item == search) { + item = newValue; + } + } + return list; + } + if (args[0].get().isSet()) { + Set set = args[0].get().getSet(); + auto search = args[1].get(); + auto newValue = args[2].get(); + if (set.values.erase(search)) { + set.values.insert(newValue); + } + return set; + } + return Value::kNullBadType; + }; + } + { + auto &attr = functions_["erase"]; + attr.minArity_ = 2; + attr.maxArity_ = 2; + attr.isAlwaysPure_ = true; + attr.body_ = [](const auto &args) -> Value { + if (args[0].get().isList()) { + auto list = args[0].get().getList(); + auto target = args[1].get(); + list.values.erase(std::remove(list.values.begin(), list.values.end(), target), + list.values.end()); + return list; + } + if (args[0].get().isSet()) { + auto set = args[0].get().getSet(); + auto target = args[1].get(); + set.values.erase(target); + return set; + } + return Value::kNullBadType; + }; + } + { + auto &attr = functions_["setadd"]; + attr.minArity_ = 2; + attr.maxArity_ = 2; + attr.isAlwaysPure_ = true; + attr.body_ = [](const auto &args) -> Value { + if (args[0].get().isSet()) { + auto set = args[0].get().getSet(); + auto newValue = args[1].get(); + if (set.values.find(newValue) == set.values.end()) { + set.values.insert(newValue); + } + return set; + } return Value::kNullBadType; }; } diff --git a/src/common/meta/NebulaSchemaProvider.cpp b/src/common/meta/NebulaSchemaProvider.cpp index 570d931bc6e..40475a1f672 100644 --- a/src/common/meta/NebulaSchemaProvider.cpp +++ b/src/common/meta/NebulaSchemaProvider.cpp @@ -167,6 +167,14 @@ std::size_t NebulaSchemaProvider::fieldSize(PropertyType type, std::size_t fixed return 8; // as same as STRING case PropertyType::DURATION: return sizeof(int64_t) + sizeof(int32_t) + sizeof(int32_t); + case PropertyType::LIST_STRING: + case PropertyType::LIST_INT: + case PropertyType::LIST_FLOAT: + return 8; + case PropertyType::SET_STRING: + case PropertyType::SET_INT: + case PropertyType::SET_FLOAT: + return 8; case PropertyType::UNKNOWN: break; } diff --git a/src/common/utils/IndexKeyUtils.h b/src/common/utils/IndexKeyUtils.h index 0af8c904393..ff4d058a82e 100644 --- a/src/common/utils/IndexKeyUtils.h +++ b/src/common/utils/IndexKeyUtils.h @@ -54,6 +54,14 @@ class IndexKeyUtils final { return Value::Type::GEOGRAPHY; case PropertyType::DURATION: return Value::Type::DURATION; + case PropertyType::LIST_STRING: + case PropertyType::LIST_INT: + case PropertyType::LIST_FLOAT: + return Value::Type::LIST; + case PropertyType::SET_STRING: + case PropertyType::SET_INT: + case PropertyType::SET_FLOAT: + return Value::Type::SET; case PropertyType::UNKNOWN: return Value::Type::__EMPTY__; } diff --git a/src/graph/executor/algo/AllPathsExecutor.cpp b/src/graph/executor/algo/AllPathsExecutor.cpp index 351719a306d..e79e02c8813 100644 --- a/src/graph/executor/algo/AllPathsExecutor.cpp +++ b/src/graph/executor/algo/AllPathsExecutor.cpp @@ -562,38 +562,38 @@ folly::Future AllPathsExecutor::conjunctPath(std::vector& leftPa } return folly::collectAll(futures) .via(runner()) - .thenValue([this, - path = std::move(oneWayPath)](std::vector>>&& resps) { - memory::MemoryCheckGuard guard; - result_.rows = std::move(path); - for (auto& respVal : resps) { - if (respVal.hasException()) { - auto ex = respVal.exception().get_exception(); - if (ex) { - throw std::bad_alloc(); - } else { - throw std::runtime_error(respVal.exception().what().c_str()); + .thenValue( + [this, path = std::move(oneWayPath)](std::vector>>&& resps) { + memory::MemoryCheckGuard guard; + result_.rows = std::move(path); + for (auto& respVal : resps) { + if (respVal.hasException()) { + auto ex = respVal.exception().get_exception(); + if (ex) { + throw std::bad_alloc(); + } else { + throw std::runtime_error(respVal.exception().what().c_str()); + } + } + auto rows = std::move(respVal).value(); + if (rows.empty()) { + continue; + } + auto& emptyPropVerticesRow = rows.back(); + for (auto& emptyPropVertex : emptyPropVerticesRow.values) { + auto iter = emptyPropVertices_.find(emptyPropVertex); + if (iter == emptyPropVertices_.end()) { + emptyPropVids_.emplace_back(emptyPropVertex.getVertex().vid); + } + emptyPropVertices_.emplace(emptyPropVertex); + } + rows.pop_back(); + result_.rows.insert(result_.rows.end(), + std::make_move_iterator(rows.begin()), + std::make_move_iterator(rows.end())); } - } - auto rows = std::move(respVal).value(); - if (rows.empty()) { - continue; - } - auto& emptyPropVerticesRow = rows.back(); - for (auto& emptyPropVertex : emptyPropVerticesRow.values) { - auto iter = emptyPropVertices_.find(emptyPropVertex); - if (iter == emptyPropVertices_.end()) { - emptyPropVids_.emplace_back(emptyPropVertex.getVertex().vid); - } - emptyPropVertices_.emplace(emptyPropVertex); - } - rows.pop_back(); - result_.rows.insert(result_.rows.end(), - std::make_move_iterator(rows.begin()), - std::make_move_iterator(rows.end())); - } - return Status::OK(); - }) + return Status::OK(); + }) .thenError(folly::tag_t{}, [this](const std::bad_alloc&) { memoryExceeded_ = true; diff --git a/src/graph/util/SchemaUtil.cpp b/src/graph/util/SchemaUtil.cpp index 4cae786348f..f6a62feaaf0 100644 --- a/src/graph/util/SchemaUtil.cpp +++ b/src/graph/util/SchemaUtil.cpp @@ -305,6 +305,14 @@ Value::Type SchemaUtil::propTypeToValueType(nebula::cpp2::PropertyType propType) return Value::Type::GEOGRAPHY; case nebula::cpp2::PropertyType::DURATION: return Value::Type::DURATION; + case nebula::cpp2::PropertyType::LIST_STRING: + case nebula::cpp2::PropertyType::LIST_INT: + case nebula::cpp2::PropertyType::LIST_FLOAT: + return Value::Type::LIST; + case nebula::cpp2::PropertyType::SET_STRING: + case nebula::cpp2::PropertyType::SET_INT: + case nebula::cpp2::PropertyType::SET_FLOAT: + return Value::Type::SET; case nebula::cpp2::PropertyType::UNKNOWN: return Value::Type::__EMPTY__; } diff --git a/src/graph/validator/SetValidator.cpp b/src/graph/validator/SetValidator.cpp index f808f248c63..7e7c90753e0 100644 --- a/src/graph/validator/SetValidator.cpp +++ b/src/graph/validator/SetValidator.cpp @@ -37,11 +37,11 @@ Status SetValidator::validateImpl() { // Combine two sub-plans by parallel dependencies. Status SetValidator::toPlan() { - auto setSentence = static_cast(sentence_); + auto setSentence = static_cast(sentence_); auto lRoot = DCHECK_NOTNULL(lValidator_->root()); auto rRoot = DCHECK_NOTNULL(rValidator_->root()); auto colNames = lRoot->colNames(); - BinaryInputNode *bNode = nullptr; + BinaryInputNode* bNode = nullptr; switch (setSentence->op()) { case SetSentence::Operator::UNION: { bNode = Union::make(qctx_, lRoot, rRoot); diff --git a/src/interface/common.thrift b/src/interface/common.thrift index 7a87b1fb807..2acbb1ff615 100644 --- a/src/interface/common.thrift +++ b/src/interface/common.thrift @@ -291,6 +291,17 @@ enum PropertyType { // Geo spatial GEOGRAPHY = 31, + + //NEW LIST + LIST_STRING = 32, + LIST_INT = 33, + LIST_FLOAT = 34, + + // NEW SET + SET_STRING = 35, + SET_INT = 36, + SET_FLOAT = 37, + } (cpp.enum_strict) /* diff --git a/src/meta/processors/index/CreateEdgeIndexProcessor.cpp b/src/meta/processors/index/CreateEdgeIndexProcessor.cpp index 5d97097d876..09816dfe6c4 100644 --- a/src/meta/processors/index/CreateEdgeIndexProcessor.cpp +++ b/src/meta/processors/index/CreateEdgeIndexProcessor.cpp @@ -145,6 +145,18 @@ void CreateEdgeIndexProcessor::process(const cpp2::CreateEdgeIndexReq& req) { onFinished(); return; } + if (col.type.get_type() == nebula::cpp2::PropertyType::LIST_STRING || + col.type.get_type() == nebula::cpp2::PropertyType::LIST_INT || + col.type.get_type() == nebula::cpp2::PropertyType::LIST_FLOAT || + col.type.get_type() == nebula::cpp2::PropertyType::SET_STRING || + col.type.get_type() == nebula::cpp2::PropertyType::SET_INT || + col.type.get_type() == nebula::cpp2::PropertyType::SET_FLOAT) { + LOG(INFO) << "Field " << field.get_name() << " in Edge " << edgeName + << " is of a list or set type that cannot be indexed."; + handleErrorCode(nebula::cpp2::ErrorCode::E_INVALID_PARM); + onFinished(); + return; + } if (col.type.get_type() == nebula::cpp2::PropertyType::FIXED_STRING) { if (field.get_type_length() != nullptr) { LOG(INFO) << "Length should not be specified of fixed_string index :" << field.get_name(); diff --git a/src/meta/processors/index/CreateTagIndexProcessor.cpp b/src/meta/processors/index/CreateTagIndexProcessor.cpp index 1efef4410b5..0064b469785 100644 --- a/src/meta/processors/index/CreateTagIndexProcessor.cpp +++ b/src/meta/processors/index/CreateTagIndexProcessor.cpp @@ -144,6 +144,19 @@ void CreateTagIndexProcessor::process(const cpp2::CreateTagIndexReq& req) { onFinished(); return; } + // Add checks for list and set types that cannot be indexed + if (col.type.get_type() == nebula::cpp2::PropertyType::LIST_STRING || + col.type.get_type() == nebula::cpp2::PropertyType::LIST_INT || + col.type.get_type() == nebula::cpp2::PropertyType::LIST_FLOAT || + col.type.get_type() == nebula::cpp2::PropertyType::SET_STRING || + col.type.get_type() == nebula::cpp2::PropertyType::SET_INT || + col.type.get_type() == nebula::cpp2::PropertyType::SET_FLOAT) { + LOG(INFO) << "Field " << field.get_name() << " in Tag " << tagName + << " is of a list or set type that cannot be indexed."; + handleErrorCode(nebula::cpp2::ErrorCode::E_INVALID_PARM); + onFinished(); + return; + } if (col.type.get_type() == nebula::cpp2::PropertyType::FIXED_STRING) { if (field.get_type_length() != nullptr) { LOG(INFO) << "Length should not be specified of fixed_string index :" << field.get_name(); diff --git a/src/meta/processors/schema/SchemaUtil.cpp b/src/meta/processors/schema/SchemaUtil.cpp index 526efc1fe34..0bdcc38e7f1 100644 --- a/src/meta/processors/schema/SchemaUtil.cpp +++ b/src/meta/processors/schema/SchemaUtil.cpp @@ -14,6 +14,60 @@ namespace meta { using nebula::cpp2::PropertyType; +template +bool extractIntOrFloat(const Value& value, const std::string& name) { + std::vector elements; + + if constexpr (std::is_same_v) { + if (!value.isList()) { + LOG(ERROR) << "Invalid default value for `" << name << "`, expected a List but got " + << value.type(); + return false; + } + elements = value.getList().values; + } + + if constexpr (std::is_same_v) { + if (!value.isSet()) { + LOG(ERROR) << "Invalid default value for `" << name << "`, expected a Set but got " + << value.type(); + return false; + } + elements = std::vector(value.getSet().values.begin(), value.getSet().values.end()); + } + + for (const auto& elem : elements) { + if constexpr (!std::is_same_v) { + if constexpr (std::is_same_v) { + if (!elem.isList()) { + LOG(ERROR) << "Invalid default value for `" << name + << "`, elements must be Lists but got " << elem.type(); + return false; + } + } + + if constexpr (std::is_same_v) { + if (!elem.isSet()) { + LOG(ERROR) << "Invalid default value for `" << name << "`, elements must be Sets but got " + << elem.type(); + return false; + } + } + + if (!extractIntOrFloat(elem, name)) { + return false; + } + } else { + if (!elem.isInt() && !elem.isFloat()) { + LOG(ERROR) << "Invalid default value for `" << name + << "`, elements must be integers or floats but got " << elem.type(); + return false; + } + } + } + return true; +} + bool SchemaUtil::checkType(std::vector& columns) { DefaultValueContext mContext; ObjectPool Objpool; @@ -180,6 +234,48 @@ bool SchemaUtil::checkType(std::vector& columns) { } return true; } + case PropertyType::LIST_STRING: { + if (!value.isList()) { + LOG(INFO) << "Invalid default value for `" << name << "`, expected a List but got " + << value.type(); + return false; + } + for (const auto& elem : value.getList().values) { + if (!elem.isStr()) { + LOG(INFO) << "Invalid default value for `" << name + << "`, list elements must be strings but got " << elem.type(); + return false; + } + } + return true; + } + case PropertyType::LIST_INT: { + return extractIntOrFloat(value, name); + } + case PropertyType::LIST_FLOAT: { + return extractIntOrFloat(value, name); + } + case PropertyType::SET_STRING: { + if (!value.isSet()) { + LOG(INFO) << "Invalid default value for `" << name << "`, expected a Set but got " + << value.type(); + return false; + } + for (const auto& elem : value.getSet().values) { + if (!elem.isStr()) { + LOG(INFO) << "Invalid default value for `" << name + << "`, set elements must be strings but got " << elem.type(); + return false; + } + } + return true; + } + case PropertyType::SET_INT: { + return extractIntOrFloat(value, name); + } + case PropertyType::SET_FLOAT: { + return extractIntOrFloat(value, name); + } case PropertyType::UNKNOWN: case PropertyType::VID: DLOG(INFO) << "Don't supported type " diff --git a/src/meta/upgrade/v2/meta.thrift b/src/meta/upgrade/v2/meta.thrift index 8dbe888140f..49a0416c32a 100644 --- a/src/meta/upgrade/v2/meta.thrift +++ b/src/meta/upgrade/v2/meta.thrift @@ -57,6 +57,17 @@ enum PropertyType { // Geo spatial GEOGRAPHY = 31, + + // NEW List + LIST_STRING = 32, + LIST_INT = 33, + LIST_FLOAT = 34, + + // NEW Set + SET_STRING = 35, + SET_INT = 36, + SET_FLOAT = 37, + } (cpp.enum_strict) // Geo shape type diff --git a/src/parser/parser.yy b/src/parser/parser.yy index 5a902606095..c54370886a3 100644 --- a/src/parser/parser.yy +++ b/src/parser/parser.yy @@ -216,12 +216,17 @@ using namespace nebula; %token KW_MERGE KW_DIVIDE KW_RENAME %token KW_JOIN KW_LEFT KW_RIGHT KW_OUTER KW_INNER KW_SEMI KW_ANTI + + /* symbols */ %token L_PAREN R_PAREN L_BRACKET R_BRACKET L_BRACE R_BRACE COMMA MINUS_L_BRACKET R_BRACKET_MINUS L_ARROW_L_BRACKET R_BRACKET_R_ARROW PIPE MINUS_MINUS MINUS_R_ARROW L_ARROW_MINUS L_ARROW_R_ARROW ASSIGN DOT DOT_DOT COLON QM SEMICOLON L_ARROW R_ARROW AT ID_PROP TYPE_PROP SRC_ID_PROP DST_ID_PROP RANK_PROP INPUT_REF DST_REF SRC_REF +//NEW CREATE +%token LT GT + /* token type specification */ %token BOOL @@ -1290,6 +1295,32 @@ type_spec $$ = new meta::cpp2::ColumnTypeDef(); $$->type_ref() = nebula::cpp2::PropertyType::DURATION; } + //NEW CREATE LIST + | KW_LIST LT KW_STRING GT { + $$ = new meta::cpp2::ColumnTypeDef(); + $$->type_ref() = nebula::cpp2::PropertyType::LIST_STRING; + } + | KW_LIST LT KW_INT GT { + $$ = new meta::cpp2::ColumnTypeDef(); + $$->type_ref() = nebula::cpp2::PropertyType::LIST_INT; + } + | KW_LIST LT KW_FLOAT GT { + $$ = new meta::cpp2::ColumnTypeDef(); + $$->type_ref() = nebula::cpp2::PropertyType::LIST_FLOAT; + } + //NEW CREATE SET + | KW_SET LT KW_STRING GT { + $$ = new meta::cpp2::ColumnTypeDef(); + $$->type_ref() = nebula::cpp2::PropertyType::SET_STRING; + } + | KW_SET LT KW_INT GT { + $$ = new meta::cpp2::ColumnTypeDef(); + $$->type_ref() = nebula::cpp2::PropertyType::SET_INT; + } + | KW_SET LT KW_FLOAT GT { + $$ = new meta::cpp2::ColumnTypeDef(); + $$->type_ref() = nebula::cpp2::PropertyType::SET_FLOAT; + } ; @@ -1306,7 +1337,7 @@ container_expression ; list_expression - : L_BRACKET expression_list R_BRACKET { + : L_BRACKET opt_expression_list R_BRACKET { $$ = ListExpression::make(qctx->objPool(), $2); } | KW_LIST L_BRACKET opt_expression_list R_BRACKET { @@ -3171,6 +3202,9 @@ update_item delete $1; delete $3; } + | subscript_expression ASSIGN expression { + $$ = new UpdateItem($1, $3); + } ; update_vertex_sentence diff --git a/src/parser/test/ParserTest.cpp b/src/parser/test/ParserTest.cpp index 7572fd09b2d..e90055ffd3b 100644 --- a/src/parser/test/ParserTest.cpp +++ b/src/parser/test/ParserTest.cpp @@ -511,6 +511,12 @@ TEST_F(ParserTest, ColumnSpacesTest) { auto result = parse(query); ASSERT_FALSE(result.ok()); } + { + std::string query = + "CREATE TAG player(name string, age int, hobby List, ids List);"; + auto result = parse(query); + ASSERT_FALSE(result.ok()); + } { std::string query = "CREATE TAG man(name string, age)" @@ -982,6 +988,15 @@ TEST_F(ParserTest, UpdateVertex) { auto result = parse(query); ASSERT_TRUE(result.ok()) << result.status(); } + { + std::string query = + "UPDATE VERTEX ON player \"12345\" " + "SET hobby = REPLACE(hobby,\"Basketball\", \"Football\") " + "WHEN name == \"Tom\" " + "YIELD name AS Name, age AS Age, hobby AS hobby;"; + auto result = parse(query); + ASSERT_TRUE(result.ok()) << result.status(); + } } TEST_F(ParserTest, InsertEdge) { diff --git a/tests/tck/features/ddl/Ddl.feature b/tests/tck/features/ddl/Ddl.feature index 70083f4c864..782c02debdd 100644 --- a/tests/tck/features/ddl/Ddl.feature +++ b/tests/tck/features/ddl/Ddl.feature @@ -1,3 +1,4 @@ +@mintest # Copyright (c) 2023 vesoft inc. All rights reserved. # # This source code is licensed under Apache 2.0 License. @@ -30,7 +31,13 @@ Feature: DDL test name string NOT NULL, createDate DATETIME, location geography(polygon), isVisited bool COMMENT "kHop search flag", - nickName TIME DEFAULT time() + nickName TIME DEFAULT time(), + listString List< string >, + listInt List< int >, + listFloat List< float >, + setString Set< string >, + setInt Set< int >, + setFloat Set< float > ) TTL_DURATION = 100, TTL_COL = "id", COMMENT = "TAG B"; """ @@ -53,6 +60,12 @@ Feature: DDL test | "location" | "geography(polygon)" | "YES" | | | | "isVisited" | "bool" | "YES" | | "kHop search flag" | | "nickName" | "time" | "YES" | "time()" | | + | "listString" | "list_string" | "YES" | | | + | "listInt" | "list_int" | "YES" | | | + | "listFloat" | "list_float" | "YES" | | | + | "setString" | "set_string" | "YES" | | | + | "setInt" | "set_int" | "YES" | | | + | "setFloat" | "set_float" | "YES" | | | When executing query: """ ALTER TAG B DROP (name) @@ -154,6 +167,66 @@ Feature: DDL test CREATE TAG INDEX idx_B_1 on B(isVisited, id, nickName, namex(1), createDate); """ Then the execution should be successful + When executing query: + """ + CREATE TAG INDEX idx_listString ON B(listString); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + CREATE TAG INDEX idx_listInt ON B(listInt); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + CREATE TAG INDEX idx_listFloat ON B(listFloat); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + CREATE TAG INDEX idx_setString ON B(setString); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + CREATE TAG INDEX idx_setInt ON B(setInt); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + CREATE TAG INDEX idx_setFloat ON B(setFloat); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + ALTER TAG B CHANGE (listString string) + """ + Then a ExecutionError should be raised at runtime: Unsupported! + When executing query: + """ + ALTER TAG B CHANGE (listString Set< string >) + """ + Then a ExecutionError should be raised at runtime: Unsupported! + When executing query: + """ + ALTER TAG B CHANGE (listString List< int >) + """ + Then a ExecutionError should be raised at runtime: Unsupported! + When executing query: + """ + ALTER TAG B CHANGE (setString string) + """ + Then a ExecutionError should be raised at runtime: Unsupported! + When executing query: + """ + ALTER TAG B CHANGE (setString List< string >) + """ + Then a ExecutionError should be raised at runtime: Unsupported! + When executing query: + """ + ALTER TAG B CHANGE (setString Set< int >) + """ + Then a ExecutionError should be raised at runtime: Unsupported! Scenario: Edge DDL When executing query: @@ -173,7 +246,13 @@ Feature: DDL test name string NOT NULL, createDate DATETIME, location geography(polygon), isVisited bool COMMENT "kHop search flag", - nickName TIME DEFAULT time() + nickName TIME DEFAULT time(), + listString List< string >, + listInt List< int >, + listFloat List< float >, + setString Set< string >, + setInt Set< int >, + setFloat Set< float > ) TTL_DURATION = 100, TTL_COL = "id", COMMENT = "EDGE E2"; """ @@ -196,6 +275,12 @@ Feature: DDL test | "location" | "geography(polygon)" | "YES" | | | | "isVisited" | "bool" | "YES" | | "kHop search flag" | | "nickName" | "time" | "YES" | "time()" | | + | "listString" | "list_string" | "YES" | | | + | "listInt" | "list_int" | "YES" | | | + | "listFloat" | "list_float" | "YES" | | | + | "setString" | "set_string" | "YES" | | | + | "setInt" | "set_int" | "YES" | | | + | "setFloat" | "set_float" | "YES" | | | When executing query: """ ALTER EDGE E2 DROP (name) @@ -296,3 +381,63 @@ Feature: DDL test CREATE EDGE INDEX idx_E2_1 on E2(isVisited, id, nickName, namex(1), createDate); """ Then the execution should be successful + When executing query: + """ + CREATE EDGE INDEX idx_listString ON E2(listString); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + CREATE EDGE INDEX idx_listInt ON E2(listInt); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + CREATE EDGE INDEX idx_listFloat ON E2(listFloat); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + CREATE EDGE INDEX idx_setString ON E2(setString); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + CREATE EDGE INDEX idx_setInt ON E2(setInt); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + CREATE EDGE INDEX idx_setFloat ON E2(setFloat); + """ + Then a ExecutionError should be raised at runtime: Invalid param! + When executing query: + """ + ALTER EDGE E2 CHANGE (listString string) + """ + Then a ExecutionError should be raised at runtime: Unsupported! + When executing query: + """ + ALTER EDGE E2 CHANGE (listString Set< string >) + """ + Then a ExecutionError should be raised at runtime: Unsupported! + When executing query: + """ + ALTER EDGE E2 CHANGE (listString List< int >) + """ + Then a ExecutionError should be raised at runtime: Unsupported! + When executing query: + """ + ALTER EDGE E2 CHANGE (setString string) + """ + Then a ExecutionError should be raised at runtime: Unsupported! + When executing query: + """ + ALTER EDGE E2 CHANGE (setString List< string >) + """ + Then a ExecutionError should be raised at runtime: Unsupported! + When executing query: + """ + ALTER EDGE E2 CHANGE (setString Set< int >) + """ + Then a ExecutionError should be raised at runtime: Unsupported! diff --git a/tests/tck/features/fetch/FetchEmpty.feature b/tests/tck/features/fetch/FetchEmpty.feature index 6827f3f4d2f..9d91fa2fdd3 100644 --- a/tests/tck/features/fetch/FetchEmpty.feature +++ b/tests/tck/features/fetch/FetchEmpty.feature @@ -172,3 +172,55 @@ Feature: Fetch prop on empty tag/edge | col1 | | "🐏" | And drop the used space + + Scenario: Fetch hobby property from player tag + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + When executing query: + """ + CREATE TAG player(name string, age int, hobby List< string >, ids List< int >, score List< float >); + """ + Then the execution should be successful + # Ensure the tag is successfully created + And wait 3 seconds + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, ["Basketball", "Swimming"], [1, 2], [9.0, 8.0]); + """ + Then the execution should be successful + When executing query: + """ + FETCH PROP ON player "player100" YIELD properties(vertex).hobby AS hobby; + """ + Then the result should be, in any order, with relax comparison: + | hobby | + | ["Basketball", "Swimming"] | + + Scenario: Fetch hobby property from player tag + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + When executing query: + """ + CREATE TAG player(name string, age int, hobby Set< string >, ids Set< int >, score Set< float >); + """ + Then the execution should be successful + # Ensure the tag is successfully created + And wait 3 seconds + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, {"Basketball", "Swimming", "Basketball"}, {1, 2, 1}, {9.0, 8.0, 9.0}); + """ + Then the execution should be successful + When executing query: + """ + FETCH PROP ON player "player100" YIELD properties(vertex).hobby AS hobby, properties(vertex).ids AS ids, properties(vertex).score AS score; + """ + Then the result should be, in any order, with relax comparison: + | hobby | ids | score | + | {"Basketball", "Swimming"} | {1, 2} | {8.0, 9.0} | diff --git a/tests/tck/features/fetch/FetchVertices.strVid.feature b/tests/tck/features/fetch/FetchVertices.strVid.feature index 59d9516430e..4c051049b55 100644 --- a/tests/tck/features/fetch/FetchVertices.strVid.feature +++ b/tests/tck/features/fetch/FetchVertices.strVid.feature @@ -527,3 +527,101 @@ Feature: Fetch String Vertices Then the result should be, in any order: | id(VERTEX) | keys | tags_ | props | | "Tim Duncan" | ["age", "name", "speciality"] | ["bachelor", "player"] | {age: 42, name: "Tim Duncan", speciality: "psychology"} | + + Scenario: Fetch properties from player tag with Lists + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + When executing query: + """ + CREATE TAG player(name string, age int, hobby List< string >, ids List< int >, score List< float >); + """ + Then the execution should be successful + # Ensure the tag is successfully created + And wait 3 seconds + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, ["Basketball", "Swimming"], [1, 2], [9.0, 8.0]); + """ + Then the execution should be successful + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.name, player.age, player.hobby, player.ids, player.score; + """ + Then the result should be, in any order, with relax comparison: + | player.name | player.age | player.hobby | player.ids | player.score | + | "Tim Duncan" | 42 | ["Basketball", "Swimming"] | [1, 2] | [9.0, 8.0] | + When executing query: + """ + FETCH PROP ON * "player100" YIELD player.name, player.age, player.hobby, player.ids, player.score, vertex as node; + """ + Then the result should be, in any order: + | player.name | player.age | player.hobby | player.ids | player.score | node | + | "Tim Duncan" | 42 | ["Basketball", "Swimming"] | [1, 2] | [9.0, 8.0] | ("player100" :player{age: 42, hobby: ["Basketball", "Swimming"], ids: [1, 2], score: [9.0, 8.0], name: "Tim Duncan"}) | + # Fetch vertex ID and properties on player100 + When executing query: + """ + FETCH PROP ON player "player100" YIELD id(vertex) as id, properties(vertex).name as name, properties(vertex); + """ + Then the result should be, in any order: + | id | name | properties(VERTEX) | + | "player100" | "Tim Duncan" | {age: 42, hobby: ["Basketball", "Swimming"], ids: [1, 2], score: [9.0, 8.0], name: "Tim Duncan"} | + # Fetch vertex ID, keys, tags, and properties on player100 + When executing query: + """ + FETCH PROP ON player "player100" YIELD id(vertex), keys(vertex) as keys, tags(vertex) as tags_, properties(vertex) as props; + """ + Then the result should be, in any order: + | id(VERTEX) | keys | tags_ | props | + | "player100" | ["age", "hobby", "ids", "score", "name"] | ["player"] | {age: 42, hobby: ["Basketball", "Swimming"], ids: [1, 2], score: [9.0, 8.0], name: "Tim Duncan"} | + + Scenario: Fetch properties from player tag with Sets + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + When executing query: + """ + CREATE TAG player(name string, age int, hobby Set< string >, ids Set< int >, score Set< float >); + """ + Then the execution should be successful + # Ensure the tag is successfully created + And wait 3 seconds + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, {"Basketball", "Swimming", "Basketball"}, {1, 2, 2}, {9.0, 8.0, 8.0}); + """ + Then the execution should be successful + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.name, player.age, player.hobby, player.ids, player.score; + """ + Then the result should be, in any order, with relax comparison: + | player.name | player.age | player.hobby | player.ids | player.score | + | "Tim Duncan" | 42 | {"Basketball", "Swimming"} | {1, 2} | {9.0, 8.0} | + When executing query: + """ + FETCH PROP ON * "player100" YIELD player.name, player.age, player.hobby, player.ids, player.score, vertex as node; + """ + Then the result should be, in any order: + | player.name | player.age | player.hobby | player.ids | player.score | node | + | "Tim Duncan" | 42 | {"Basketball", "Swimming"} | {1, 2} | {9.0, 8.0} | ("player100" :player{age: 42, hobby: {"Basketball", "Swimming"}, ids: {1, 2}, score: {9.0, 8.0}, name: "Tim Duncan"}) | + # Fetch vertex ID and properties on player100 + When executing query: + """ + FETCH PROP ON player "player100" YIELD id(vertex) as id, properties(vertex).name as name, properties(vertex); + """ + Then the result should be, in any order: + | id | name | properties(VERTEX) | + | "player100" | "Tim Duncan" | {age: 42, hobby: {"Basketball", "Swimming"}, ids: {1, 2}, score: {9.0, 8.0}, name: "Tim Duncan"} | + # Fetch vertex ID, keys, tags, and properties on player100 + When executing query: + """ + FETCH PROP ON player "player100" YIELD id(vertex), keys(vertex) as keys, tags(vertex) as tags_, properties(vertex) as props; + """ + Then the result should be, in any order: + | id(VERTEX) | keys | tags_ | props | + | "player100" | ["age", "hobby", "ids", "score", "name"] | ["player"] | {age: 42, hobby: {"Basketball", "Swimming"}, ids: {1, 2}, score: {9.0, 8.0}, name: "Tim Duncan"} | diff --git a/tests/tck/features/insert/Insert.feature b/tests/tck/features/insert/Insert.feature index 2a3785cec9f..adcac9cda94 100644 --- a/tests/tck/features/insert/Insert.feature +++ b/tests/tck/features/insert/Insert.feature @@ -687,3 +687,202 @@ Feature: Insert string vid of vertex and edge INSERT VERTEX invalid_vertex VALUES "non_existed_tag":() """ Then a SemanticError should be raised at runtime: No schema found + + Scenario: insert player(name string, age int, hobby List< string >, ids List< int >, score List< float >) + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + When executing query: + """ + CREATE TAG player(name string, age int, hobby List< string >, ids List< int >, score List< float >); + CREATE TAG playerWithDefault( + name string DEFAULT "", + age int DEFAULT 18, isMarried bool DEFAULT false, + BMI double DEFAULT 18.5, department string DEFAULT "engineering", + birthday timestamp DEFAULT timestamp("2020-01-10T10:00:00"), + number int DEFAULT 0 + ); + CREATE TAG school(name string, create_time timestamp); + CREATE EDGE schoolmate(likeness int, nickname string); + CREATE EDGE schoolmateWithDefault(likeness int DEFAULT 80); + CREATE EDGE study(start_time timestamp, end_time timestamp); + """ + Then the execution should be successful + And wait 3 seconds + When try to execute query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, [], [], []); + """ + Then the execution should be successful + When executing query: + """ + FETCH PROP ON * "player100" YIELD vertex as node; + """ + Then the result should be, in any order, with relax comparison: + | node | + | ("player100":player{name:"Tim Duncan", age:42, hobby:[], ids:[], score:[]}) | + When try to execute query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, ["Basketball", "Swimming", "Reading"], [1, 2, 3], [10.5, 20.5, 30.5]); + """ + Then the execution should be successful + # Insert and update the player vertex to modify only the lists + When try to execute query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, ["Basketball", "Gaming"], [4, 5, 6], [40.5, 50.5, 60.5]); + """ + Then the execution should be successful + # Verify that the latest data matches the expected state + When executing query: + """ + FETCH PROP ON * "player100" YIELD vertex as node; + """ + Then the result should be, in any order, with relax comparison: + | node | + | ("player100":player{name:"Tim Duncan", age:42, hobby:["Basketball", "Gaming"], ids:[4, 5, 6], score:[40.5, 50.5, 60.5]}) | + # Handle the edge cases by inserting incorrect types and handling errors + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 2, [10, 11, 12], ["Swimming", "Painting"], {100.5, 110.5, 120.5}); + """ + Then a ExecutionError should be raised at runtime: Storage Error: The data type does not meet the requirements. Use the correct type of data. + # Insert edges and fetch results + When try to execute query: + """ + INSERT EDGE schoolmate(likeness, nickname) VALUES "player100"->"player200":(85, "Lily"); + INSERT EDGE schoolmate(likeness, nickname) VALUES "player200"->"player300":("87", ""); + INSERT EDGE schoolmate(likeness) VALUES "player200"->"player300":("hello", "87"); + INSERT EDGE schoolmate(likeness, HH) VALUES "player200"->"player300":(88, ""); + INSERT EDGE study(start_time, end_time) VALUES "player200"->"school1":(timestamp("2019-01-01T10:00:00"), now()+3600*24*365*3); + """ + Then the execution should be successful + # Insert vertices with default properties + When try to execute query: + """ + INSERT VERTEX playerWithDefault() VALUES "player400":(); + INSERT VERTEX playerWithDefault(age, isMarried, BMI) VALUES "player100":(18, false, 19.5); + INSERT VERTEX playerWithDefault(name) VALUES "player100":("Tim Duncan"); + INSERT VERTEX playerWithDefault(name, age) VALUES "player100":("Tim Duncan", 20); + INSERT VERTEX playerWithDefault(name, BMI, number) VALUES "player200":("Laura", 21.5, 20190901008), "player300":("Amber", 22.5, 20180901003); + """ + Then the execution should be successful + # Fetch properties and validate results + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.name, player.age, player.hobby, player.ids, player.score; + """ + Then the result should be, in any order, with relax comparison: + | player.name | player.age | player.hobby | player.ids | player.score | + | "Tim Duncan" | 42 | ["Basketball", "Gaming"] | [4, 5, 6] | [40.5, 50.5, 60.5] | + When executing query: + """ + FETCH PROP ON playerWithDefault "player200" YIELD playerWithDefault.name, playerWithDefault.birthday, playerWithDefault.department; + """ + Then the execution should be successful + # Insert and query multi-tagged vertices and edges + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player200":("Laura", 8, ["Basketball", "Reading"], [7, 8, 9], [70.5, 80.5, 90.5]); + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player300":("Amber", 9, ["Swimming", "Painting"], [10, 11, 12], [100.5, 110.5, 120.5]); + INSERT EDGE schoolmateWithDefault() VALUES "player100"->"player200":(), "player100"->"player300":(); + GO FROM "player100" OVER schoolmateWithDefault YIELD $^.player.name, schoolmateWithDefault.likeness, $$.player.name; + """ + Then the execution should be successful + + Scenario: insert player(name string, age int, hobby Set< string >, ids Set< int >, score Set< float >) + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + When executing query: + """ + CREATE TAG player(name string, age int, hobby Set< string >, ids Set< int >, score Set< float >); + CREATE TAG playerWithDefault( + name string DEFAULT "", + age int DEFAULT 18, isMarried bool DEFAULT false, + BMI double DEFAULT 18.5, department string DEFAULT "engineering", + birthday timestamp DEFAULT timestamp("2020-01-10T10:00:00"), + number int DEFAULT 0 + ); + CREATE TAG school(name string, create_time timestamp); + CREATE EDGE schoolmate(likeness int, nickname string); + CREATE EDGE schoolmateWithDefault(likeness int DEFAULT 80); + CREATE EDGE study(start_time timestamp, end_time timestamp); + """ + Then the execution should be successful + And wait 3 seconds + When try to execute query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, set{}, set{}, set{}); + """ + Then the execution should be successful + When try to execute query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, {"Basketball", "Swimming", "Basketball"}, {1, 2, 2, 3}, {10.5, 20.5, 10.5, 30.5}); + """ + Then the execution should be successful + # Insert and update the player vertex to modify only the sets + When try to execute query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, {"Basketball", "Gaming"}, {4, 5, 6, 6}, {40.5, 50.5, 60.5}); + """ + Then the execution should be successful + # Verify that the latest data matches the expected state + When executing query: + """ + FETCH PROP ON * "player100" YIELD vertex as node; + """ + Then the result should be, in any order, with relax comparison: + | node | + | ("player100":player{name:"Tim Duncan", age:42, hobby:{"Basketball", "Gaming"}, ids:{4, 5, 6}, score:{40.5, 50.5, 60.5}}) | + # Handle the edge cases by inserting incorrect types and handling errors + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 2, ["Basketball", "Gaming"], {40.5, 50.5, 60.5}, {4, 5, 6, 6}); + """ + Then a ExecutionError should be raised at runtime: Storage Error: The data type does not meet the requirements. Use the correct type of data. + # Insert edges and fetch results + When try to execute query: + """ + INSERT EDGE schoolmate(likeness, nickname) VALUES "player100"->"player200":(85, "Lily"); + INSERT EDGE schoolmate(likeness, nickname) VALUES "player200"->"player300":("87", ""); + INSERT EDGE schoolmate(likeness) VALUES "player200"->"player300":("hello", "87"); + INSERT EDGE schoolmate(likeness) VALUES "player200"->"player300":(88, ""); + INSERT EDGE study(start_time, end_time) VALUES "player200"->"school1":(timestamp("2019-01-01T10:00:00"), now()+3600*24*365*3); + """ + Then the execution should be successful + # Insert vertices with default properties + When try to execute query: + """ + INSERT VERTEX playerWithDefault() VALUES "player400":(); + INSERT VERTEX playerWithDefault(age, isMarried, BMI) VALUES "player100":(18, false, 19.5); + INSERT VERTEX playerWithDefault(name) VALUES "player100":("Tim Duncan"); + INSERT VERTEX playerWithDefault(name, age) VALUES "player100":("Tim Duncan", 20); + INSERT VERTEX playerWithDefault(name, BMI, number) VALUES "player200":("Laura", 21.5, 20190901008), "player300":("Amber", 22.5, 20180901003); + """ + Then the execution should be successful + # Fetch properties and validate results + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.name, player.age, player.hobby, player.ids, player.score; + """ + Then the result should be, in any order, with relax comparison: + | player.name | player.age | player.hobby | player.ids | player.score | + | "Tim Duncan" | 42 | {"Basketball", "Gaming"} | {4, 5, 6} | {40.5, 50.5, 60.5} | + When executing query: + """ + FETCH PROP ON playerWithDefault "player200" YIELD playerWithDefault.name, playerWithDefault.birthday, playerWithDefault.department; + """ + Then the execution should be successful + # Insert and query multi-tagged vertices and edges + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player200":("Laura", 8, {"Basketball", "Reading"}, {7, 8, 9}, {70.5, 80.5, 90.5}); + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player300":("Amber", 9, {"Swimming", "Painting"}, {10, 11, 12}, {100.5, 110.5, 120.5}); + INSERT EDGE schoolmateWithDefault() VALUES "player100"->"player200":(), "player100"->"player300":(); + GO FROM "player100" OVER schoolmateWithDefault YIELD $^.player.name, schoolmateWithDefault.likeness, $$.player.name; + """ + Then the execution should be successful diff --git a/tests/tck/features/insert/InsertIfNotExists.feature b/tests/tck/features/insert/InsertIfNotExists.feature index cf76d4871b6..fc31a1c4268 100644 --- a/tests/tck/features/insert/InsertIfNotExists.feature +++ b/tests/tck/features/insert/InsertIfNotExists.feature @@ -540,3 +540,439 @@ Feature: Insert vertex and edge with if not exists | "wang" | "li" | 18 | | "wang" | "zhang" | 41 | And drop the used space + + Scenario: insert player(name string, age int, hobby List< string >, ids List< int >, score List< float >) + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + And having executed: + """ + CREATE TAG IF NOT EXISTS player(name string, age int, hobby List< string >, ids List< int >, score List< float >); + CREATE EDGE IF NOT EXISTS like(likeness int); + CREATE TAG INDEX IF NOT EXISTS index_player_age ON player(age); + """ + And wait 6 seconds # Wait for the index to be created + # Insert vertex data for player100 + When try to execute query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, ["Basketball", "Swimming"], [1, 2, 3], [10.0, 20.0]); + """ + Then the execution should be successful + # Insert multiple player vertices, using IF NOT EXISTS to avoid duplicate entries + When executing query: + """ + INSERT VERTEX IF NOT EXISTS + player(name, age, hobby, ids, score) + VALUES + "player101":("Michael Jordan", 35, ["Basketball", "Baseball"], [4, 5, 6], [30.0, 40.0]), + "player102":("LeBron James", 36, ["Basketball", "Football"], [7, 8, 9], [50.0, 60.0]), + "player103":("Kobe Bryant", 34, ["Basketball", "Soccer"], [10, 11, 12], [70.0, 80.0]), + "player100":("Tim Duncan", 40, ["Basketball", "Golf"], [13, 14, 15], [90.0, 100.0]); + """ + Then the execution should be successful + # Fetch the age property of player100 + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | age | + | 42 | + # Insert an existing player vertex using IF NOT EXISTS + When executing query: + """ + INSERT VERTEX IF NOT EXISTS player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 45, ["Basketball", "Tennis"], [16, 17, 18], [110.0, 120.0]); + """ + Then the execution should be successful + # Fetch the age property of player100 again to verify data + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | age | + | 42 | + # Directly insert the existing player100 vertex and update its data + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 50, ["Basketball", "Table Tennis"], [19, 20, 21], [130.0, 140.0]); + """ + Then the execution should be successful + # Fetch the age property of player100 again to verify the updated data + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | age | + | 50 | + # Insert an edge 'like', indicating that player100 likes player101 + When try to execute query: + """ + INSERT EDGE like(likeness) VALUES "player100"->"player101":(95); + """ + Then the execution should be successful + # Fetch the edge property + When executing query: + """ + FETCH PROP ON like "player100"->"player101" YIELD like.likeness; + """ + Then the result should be, in any order: + | like.likeness | + | 95 | + # Insert an edge using IF NOT EXISTS + When executing query: + """ + INSERT EDGE IF NOT EXISTS like(likeness) VALUES "player100"->"player101":(50); + """ + Then the execution should be successful + # Fetch the edge property again to verify data + When executing query: + """ + FETCH PROP ON like "player100"->"player101" YIELD like.likeness; + """ + Then the result should be, in any order: + | like.likeness | + | 95 | + # Directly insert an existing edge and update its property + When executing query: + """ + INSERT EDGE like(likeness) VALUES "player100"->"player101":(100); + """ + Then the execution should be successful + # Fetch the edge property again to verify the updated data + When executing query: + """ + FETCH PROP ON like "player100"->"player101" YIELD like.likeness; + """ + Then the result should be, in any order: + | like.likeness | + | 100 | + # Query destination vertices starting from player100 using the like edge + When executing query: + """ + GO FROM "player100" over like YIELD like.likeness as like, like._src as src, like._dst as dst; + """ + Then the result should be, in any order: + | like | src | dst | + | 100 | "player100" | "player101" | + # Insert multiple player vertices and use multiple tags + When try to execute query: + """ + INSERT VERTEX IF NOT EXISTS + player(name, age, hobby, ids, score) + VALUES + "player104":("Neal", 49, ["Basketball", "Acting"], [22, 23, 24], [150.0, 160.0]), + "player105":("Magic Johnson", 60, ["Basketball", "Business"], [25, 26, 27], [170.0, 180.0]); + """ + Then the execution should be successful + # Fetch the age property of player104 + When executing query: + """ + FETCH PROP ON player "player104" YIELD player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | age | + | 49 | + # Fetch the hobby property of player105 + When executing query: + """ + FETCH PROP ON player "player105" YIELD player.hobby as hobby; + """ + Then the result should be, in any order, with relax comparison: + | hobby | + | ["Basketball", "Business"] | + # Insert multiple edges, indicating associations between player100 and other players + When try to execute query: + """ + INSERT EDGE IF NOT EXISTS + like(likeness) + VALUES + "player100"->"player102":(85), + "player100"->"player103":(88); + """ + Then the execution should be successful + # Fetch the edge property + When executing query: + """ + FETCH PROP ON like "player100"->"player102" YIELD like.likeness; + """ + Then the result should be, in any order: + | like.likeness | + | 85 | + When executing query: + """ + FETCH PROP ON like "player100"->"player103" YIELD like.likeness; + """ + Then the result should be, in any order: + | like.likeness | + | 88 | + # Delete player vertices + When try to execute query: + """ + DELETE TAG player FROM "player100", "player101", "player102", "player103"; + """ + Then the execution should be successful + # Reinsert previously deleted player vertices + When try to execute query: + """ + INSERT VERTEX IF NOT EXISTS + player(name, age, hobby, ids, score) + VALUES + "player106":("Stephen Curry", 32, ["Basketball", "Golf"], [28, 29, 30], [190.0, 200.0]), + "player107":("Kevin Durant", 31, ["Basketball", "Video Games"], [31, 32, 33], [210.0, 220.0]); + """ + Then the execution should be successful + # Lookup player vertices with age less than 35 + When executing query: + """ + LOOKUP ON player WHERE player.age < 35 YIELD player.name as name, player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | name | age | + | "Stephen Curry" | 32 | + | "Kevin Durant" | 31 | + # Lookup player vertices with age greater than 35 + When executing query: + """ + LOOKUP ON player WHERE player.age > 35 YIELD player.name as name, player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | name | age | + | "Neal" | 49 | + | "Magic Johnson" | 60 | + # Fetch properties for multiple player vertices + When executing query: + """ + FETCH PROP ON player "player106", "player107" YIELD player.name as name, player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | name | age | + | "Stephen Curry" | 32 | + | "Kevin Durant" | 31 | + # Delete player106 and player107 vertices + When try to execute query: + """ + DELETE TAG player FROM "player106", "player107"; + """ + Then the execution should be successful + + Scenario: insert player(name string, age int, hobby Set< string >, ids Set< int >, score Set< float >) + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + And having executed: + """ + CREATE TAG IF NOT EXISTS player(name string, age int, hobby Set< string >, ids Set< int >, score Set< float >); + CREATE EDGE IF NOT EXISTS like(likeness int); + CREATE TAG INDEX IF NOT EXISTS index_player_age ON player(age); + """ + And wait 6 seconds # Wait for the index to be created + # Insert vertex data for player100 with duplicate items + When try to execute query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, {"Basketball", "Swimming", "Basketball"}, {1, 2, 2, 3}, {10.0, 10.0, 20.0}); + """ + Then the execution should be successful + # Insert multiple player vertices, using IF NOT EXISTS to avoid duplicate entries + When executing query: + """ + INSERT VERTEX IF NOT EXISTS + player(name, age, hobby, ids, score) + VALUES + "player101":("Michael Jordan", 35, {"Basketball", "Baseball"}, {4, 5, 5, 6}, {30.0, 40.0}), + "player102":("LeBron James", 36, {"Basketball", "Football"}, {7, 8, 8, 9}, {50.0, 60.0}), + "player103":("Kobe Bryant", 34, {"Basketball", "Soccer"}, {10, 11, 11, 12}, {70.0, 80.0}), + "player100":("Tim Duncan", 40, {"Basketball", "Golf"}, {13, 14, 14, 15}, {90.0, 100.0}); + """ + Then the execution should be successful + # Fetch the age property of player100 + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | age | + | 42 | + # Insert an existing player vertex using IF NOT EXISTS + When executing query: + """ + INSERT VERTEX IF NOT EXISTS player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 45, {"Basketball", "Tennis"}, {16, 17, 17, 18}, {110.0, 120.0}); + """ + Then the execution should be successful + # Fetch the age property of player100 again to verify data + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | age | + | 42 | + # Directly insert the existing player100 vertex and update its data + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 50, {"Basketball", "Table Tennis"}, {19, 20, 20, 21}, {130.0, 140.0}); + """ + Then the execution should be successful + # Fetch the age property of player100 again to verify the updated data + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | age | + | 50 | + # Insert an edge 'like', indicating that player100 likes player101 + When try to execute query: + """ + INSERT EDGE like(likeness) VALUES "player100"->"player101":(95); + """ + Then the execution should be successful + # Fetch the edge property + When executing query: + """ + FETCH PROP ON like "player100"->"player101" YIELD like.likeness; + """ + Then the result should be, in any order: + | like.likeness | + | 95 | + # Insert an edge using IF NOT EXISTS + When executing query: + """ + INSERT EDGE IF NOT EXISTS like(likeness) VALUES "player100"->"player101":(50); + """ + Then the execution should be successful + # Fetch the edge property again to verify data + When executing query: + """ + FETCH PROP ON like "player100"->"player101" YIELD like.likeness; + """ + Then the result should be, in any order: + | like.likeness | + | 95 | + # Directly insert an existing edge and update its property + When executing query: + """ + INSERT EDGE like(likeness) VALUES "player100"->"player101":(100); + """ + Then the execution should be successful + # Fetch the edge property again to verify the updated data + When executing query: + """ + FETCH PROP ON like "player100"->"player101" YIELD like.likeness; + """ + Then the result should be, in any order: + | like.likeness | + | 100 | + # Query destination vertices starting from player100 using the like edge + When executing query: + """ + GO FROM "player100" over like YIELD like.likeness as like, like._src as src, like._dst as dst; + """ + Then the result should be, in any order: + | like | src | dst | + | 100 | "player100" | "player101" | + # Insert multiple player vertices and use multiple tags + When try to execute query: + """ + INSERT VERTEX IF NOT EXISTS + player(name, age, hobby, ids, score) + VALUES + "player104":("Neal", 49, {"Basketball", "Acting"}, {22, 23, 23, 24}, {150.0, 160.0}), + "player105":("Magic Johnson", 60, {"Basketball", "Business"}, {25, 26, 26, 27}, {170.0, 180.0}); + """ + Then the execution should be successful + # Fetch the age property of player104 + When executing query: + """ + FETCH PROP ON player "player104" YIELD player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | age | + | 49 | + # Fetch the hobby property of player105 + When executing query: + """ + FETCH PROP ON player "player105" YIELD player.hobby as hobby; + """ + Then the result should be, in any order, with relax comparison: + | hobby | + | {"Basketball", "Business"} | + # Insert multiple edges, indicating associations between player100 and other players + When try to execute query: + """ + INSERT EDGE IF NOT EXISTS + like(likeness) + VALUES + "player100"->"player102":(85), + "player100"->"player103":(88); + """ + Then the execution should be successful + # Fetch the edge property + When executing query: + """ + FETCH PROP ON like "player100"->"player102" YIELD like.likeness; + """ + Then the result should be, in any order: + | like.likeness | + | 85 | + When executing query: + """ + FETCH PROP ON like "player100"->"player103" YIELD like.likeness; + """ + Then the result should be, in any order: + | like.likeness | + | 88 | + # Delete player vertices + When try to execute query: + """ + DELETE TAG player FROM "player100", "player101", "player102", "player103"; + """ + Then the execution should be successful + # Reinsert previously deleted player vertices + When try to execute query: + """ + INSERT VERTEX IF NOT EXISTS + player(name, age, hobby, ids, score) + VALUES + "player106":("Stephen Curry", 32, {"Basketball", "Golf"}, {28, 29, 29, 30}, {190.0, 200.0}), + "player107":("Kevin Durant", 31, {"Basketball", "Video Games"}, {31, 32, 32, 33}, {210.0, 220.0}); + """ + Then the execution should be successful + # Lookup player vertices with age less than 35 + When executing query: + """ + LOOKUP ON player WHERE player.age < 35 YIELD player.name as name, player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | name | age | + | "Stephen Curry" | 32 | + | "Kevin Durant" | 31 | + # Lookup player vertices with age greater than 35 + When executing query: + """ + LOOKUP ON player WHERE player.age > 35 YIELD player.name as name, player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | name | age | + | "Neal" | 49 | + | "Magic Johnson" | 60 | + # Fetch properties for multiple player vertices + When executing query: + """ + FETCH PROP ON player "player106", "player107" YIELD player.name as name, player.age as age; + """ + Then the result should be, in any order, with relax comparison: + | name | age | + | "Stephen Curry" | 32 | + | "Kevin Durant" | 31 | + # Delete player106 and player107 vertices + When try to execute query: + """ + DELETE TAG player FROM "player106", "player107"; + """ + Then the execution should be successful diff --git a/tests/tck/features/match/Base.feature b/tests/tck/features/match/Base.feature index 5ebdcef3dd8..a1e76c38778 100644 --- a/tests/tck/features/match/Base.feature +++ b/tests/tck/features/match/Base.feature @@ -2376,16 +2376,179 @@ Feature: Basic match | cnt | | 243 | -# Then the result should be, in any order: -# | cnt | -# | 0 | -# When executing query: -# """ -# MATCH (v:player{name: 'Tim Duncan'})-[e:like*0..2]-(v2)-[i]-(v3) -# WHERE size([i in e WHERE (v)-[i]-(v2) | i])>1 -# RETURN count(*) AS cnt -# """ -# FIXME(czp): Fix this case after https://github.com/vesoft-inc/nebula/issues/5289 closed -# Then the result should be, in any order: -# | cnt | -# | 0 | + # Then the result should be, in any order: + # | cnt | + # | 0 | + # When executing query: + # """ + # MATCH (v:player{name: 'Tim Duncan'})-[e:like*0..2]-(v2)-[i]-(v3) + # WHERE size([i in e WHERE (v)-[i]-(v2) | i])>1 + # RETURN count(*) AS cnt + # """ + # FIXME(czp): Fix this case after https://github.com/vesoft-inc/nebula/issues/5289 closed + # Then the result should be, in any order: + # | cnt | + # | 0 | + Scenario: Test MATCH queries on tag with List< string >, List< int >, List< float > data types + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + When executing query: + """ + CREATE TAG player(name string, age int, hobby List< string >, ids List< int >, score List< float >); + """ + Then the execution should be successful + # Ensure the tag is successfully created + And wait 3 seconds + When executing query: + """ + DESCRIBE TAG player; + """ + Then the result should be, in any order: + | Field | Type | Null | Default | Comment | + | "name" | "string" | "YES" | | | + | "age" | "int64" | "YES" | | | + | "hobby" | "list_string" | "YES" | | | + | "ids" | "list_int" | "YES" | | | + | "score" | "list_float" | "YES" | | | + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, ["Basketball", "Swimming"], [1, 2, 3], [9.0, 8.5, 7.5]); + """ + Then the execution should be successful + # Test matching vertices with specific hobby in the List< string > + When executing query: + """ + MATCH (v:player) WHERE ANY(x IN v.player.hobby WHERE x == "Basketball") RETURN v; + """ + Then the result should be, in any order, with relax comparison: + | v | + | ("player100" :player{age: 42, hobby: ["Basketball", "Swimming"], ids: [1, 2, 3], score: [9.0, 8.5, 7.5], name: "Tim Duncan"}) | + # Test returning the list of hobbies for a player + When executing query: + """ + MATCH (v:player) RETURN v.player.hobby; + """ + Then the result should be, in any order, with relax comparison: + | v.player.hobby | + | ["Basketball", "Swimming"] | + # Test returning the hobbies of a specific player by ID + When executing query: + """ + MATCH (v:player) WHERE id(v) == 'player100' RETURN v.player.hobby; + """ + Then the result should be, in any order, with relax comparison: + | v.player.hobby | + | ["Basketball", "Swimming"] | + # Test accessing a specific hobby by index in the List + When executing query: + """ + MATCH (v:player) WHERE id(v) == 'player100' RETURN v.player.hobby[1]; + """ + Then the result should be, in any order, with relax comparison: + | v.player.hobby[1] | + | "Swimming" | + # Test returning the list of ids for a player + When executing query: + """ + MATCH (v:player) RETURN v.player.ids; + """ + Then the result should be, in any order, with relax comparison: + | v.player.ids | + | [1, 2, 3] | + # Test accessing a specific id by index in the List + When executing query: + """ + MATCH (v:player) WHERE id(v) == 'player100' RETURN v.player.ids[2]; + """ + Then the result should be, in any order, with relax comparison: + | v.player.ids[2] | + | 3 | + # Test returning the list of scores for a player + When executing query: + """ + MATCH (v:player) RETURN v.player.score; + """ + Then the result should be, in any order, with relax comparison: + | v.player.score | + | [9.0, 8.5, 7.5] | + # Test accessing a specific score by index in the List + When executing query: + """ + MATCH (v:player) WHERE id(v) == 'player100' RETURN v.player.score[1]; + """ + Then the result should be, in any order, with relax comparison: + | v.player.score[1] | + | 8.5 | + + Scenario: Test MATCH queries on tag with Set< string >, Set< int >, Set< float > data types + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + When executing query: + """ + CREATE TAG player(name string, age int, hobby Set< string >, ids Set< int >, score Set< float >); + """ + Then the execution should be successful + # Ensure the tag is successfully created + And wait 3 seconds + When executing query: + """ + DESCRIBE TAG player; + """ + Then the result should be, in any order: + | Field | Type | Null | Default | Comment | + | "name" | "string" | "YES" | | | + | "age" | "int64" | "YES" | | | + | "hobby" | "set_string" | "YES" | | | + | "ids" | "set_int" | "YES" | | | + | "score" | "set_float" | "YES" | | | + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, {"Basketball", "Swimming", "Basketball"}, {1, 2, 3, 2}, {9.0, 8.5, 7.5, 8.5}); + """ + Then the execution should be successful + # Test matching vertices with specific hobby in the Set< string > + When executing query: + """ + MATCH (v:player) WHERE "Basketball" IN v.player.hobby RETURN v; + """ + Then the result should be, in any order, with relax comparison: + | v | + | ("player100" :player{age: 42, hobby: {"Basketball", "Swimming"}, ids: {1, 2, 3}, score: {9.0, 8.5, 7.5}, name: "Tim Duncan"}) | + # Test returning the set of hobbies for a player + When executing query: + """ + MATCH (v:player) RETURN v.player.hobby; + """ + Then the result should be, in any order, with relax comparison: + | v.player.hobby | + | {"Basketball", "Swimming"} | + # Test returning the hobbies of a specific player by ID + When executing query: + """ + MATCH (v:player) WHERE id(v) == 'player100' RETURN v.player.hobby; + """ + Then the result should be, in any order, with relax comparison: + | v.player.hobby | + | {"Basketball", "Swimming"} | + # Test accessing a specific id in the Set (Note: Sets are unordered, this just tests existence) + When executing query: + """ + MATCH (v:player) WHERE id(v) == 'player100' RETURN v.player.ids; + """ + Then the result should be, in any order, with relax comparison: + | v.player.ids | + | {1, 2, 3} | + # Test returning the set of scores for a player + When executing query: + """ + MATCH (v:player) RETURN v.player.score; + """ + Then the result should be, in any order, with relax comparison: + | v.player.score | + | {9.0, 8.5, 7.5} | diff --git a/tests/tck/features/schema/Schema.feature b/tests/tck/features/schema/Schema.feature index fc386184eaf..155ae7d049b 100644 --- a/tests/tck/features/schema/Schema.feature +++ b/tests/tck/features/schema/Schema.feature @@ -987,3 +987,92 @@ Feature: Insert string vid of vertex and edge ALTER TAG person ADD (age int); """ Then a ExecutionError should be raised at runtime: Schema exisited before! + + Scenario: Create and test a tag with List< string >, List< int >, and List< float > data types + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + When executing query: + """ + CREATE TAG player(name string, age int, hobby List< string >, ids List< int >, score List< float >); + """ + Then the execution should be successful + # Ensure the tag is successfully created + And wait 3 seconds + When executing query: + """ + DESCRIBE TAG player; + """ + Then the result should be, in any order: + | Field | Type | Null | Default | Comment | + | "name" | "string" | "YES" | | | + | "age" | "int64" | "YES" | | | + | "hobby" | "list_string" | "YES" | | | + | "ids" | "list_int" | "YES" | | | + | "score" | "list_float" | "YES" | | | + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":("Tim Duncan", 42, ["Basketball", "Swimming"], [100, 528, 37564], [50.0, 22.0, 85.0]); + """ + Then the execution should be successful + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.name, player.age, player.hobby, player.ids, player.score; + """ + Then the result should be, in any order: + | player.name | player.age | player.hobby | player.ids | player.score | + | "Tim Duncan" | 42 | ["Basketball", "Swimming"] | [100, 528, 37564] | [50.0, 22.0, 85.0] | + When executing query: + """ + DELETE VERTEX "player100"; + """ + Then the execution should be successful + + Scenario: Validate that Set, Set, and Set types are unordered and remove duplicates + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + When executing query: + """ + CREATE TAG player(name string, age int, hobby Set< string >, ids Set< int >, score Set< float >); + """ + Then the execution should be successful + And wait 4 seconds + When executing query: + """ + DESCRIBE TAG player; + """ + Then the result should be, in any order: + | Field | Type | Null | Default | Comment | + | "name" | "string" | "YES" | | | + | "age" | "int64" | "YES" | | | + | "hobby" | "set_string" | "YES" | | | + | "ids" | "set_int" | "YES" | | | + | "score" | "set_float" | "YES" | | | + When executing query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":( + "Tim Duncan", 42, + {"Basketball", "Swimming", "Basketball", "Football", "Basketball", "Swimming"}, + {100, 528, 100, 37564, 528, 37564}, + {50.0, 22.0, 22.0, 85.0, 85.0, 50.0} + ); + """ + Then the execution should be successful + # Fetch the properties and validate unordered and deduplicated results + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.name, player.age, player.hobby, player.ids, player.score; + """ + Then the result should contain: + | player.name | player.age | player.hobby | player.ids | player.score | + | "Tim Duncan" | 42 | {"Basketball", "Swimming", "Football"} | {100, 528, 37564} | {50.0, 22.0, 85.0} | + When executing query: + """ + DELETE VERTEX "player100"; + """ + Then the execution should be successful diff --git a/tests/tck/features/update/Update.feature b/tests/tck/features/update/Update.feature index acca6947375..5995985752e 100644 --- a/tests/tck/features/update/Update.feature +++ b/tests/tck/features/update/Update.feature @@ -1207,3 +1207,166 @@ Feature: Update string vid of vertex and edge | like.likeness | like.new_field | | 1.0 | "111" | Then drop the used space + + Scenario: Update player(name string, age int, hobby List< string >, ids List< int >, score List< float >) + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + And having executed: + """ + CREATE TAG player(name string, age int, hobby List< string >, ids List< int >, score List< float >); + """ + And wait 3 seconds # Wait for the index to be created + # Insert vertex data for player100 + When try to execute query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":( + "Tim Duncan", 42, ["Basketball", "Swimming"], [100, 528], [50.0, 22.0] + ); + """ + Then the execution should be successful + When executing query: + """ + UPDATE VERTEX ON player "player100" + SET hobby = hobby + [3525], + ids = ids + ["Coding"], + score = score + ["85.0"] + WHEN name == "Tim Duncan" + YIELD name AS Name, age AS Age, hobby AS Hobby, ids AS Ids, score AS Score; + """ + Then a ExecutionError should be raised at runtime: Storage Error: Invalid data, may be wrong value type. + When executing query: + """ + UPDATE VERTEX ON player "player100" + SET hobby = hobby + [], + ids = ids + [], + score = score + [] + WHEN name == "Tim Duncan" + YIELD name AS Name, age AS Age, hobby AS Hobby, ids AS Ids, score AS Score; + """ + Then the result should be, in any order: + | Name | Age | Hobby | Ids | Score | + | "Tim Duncan" | 42 | ["Basketball", "Swimming"] | [100, 528] | [50.0, 22.0] | + When executing query: + """ + UPDATE VERTEX ON player "player100" + SET hobby = hobby + ["Coding"], + ids = ids + [37564], + score = score + [85.0] + WHEN name == "Tim Duncan" + YIELD name AS Name, age AS Age, hobby AS Hobby, ids AS Ids, score AS Score; + """ + Then the result should be, in any order: + | Name | Age | Hobby | Ids | Score | + | "Tim Duncan" | 42 | ["Basketball", "Swimming", "Coding"] | [100, 528, 37564] | [50.0, 22.0, 85.0] | + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.name, player.age, player.hobby, player.ids, player.score; + """ + Then the result should be, in any order: + | player.name | player.age | player.hobby | player.ids | player.score | + | "Tim Duncan" | 42 | ["Basketball", "Swimming", "Coding"] | [100, 528, 37564] | [50.0, 22.0, 85.0] | + When executing query: + """ + UPDATE VERTEX ON player "player100" + SET hobby = REPLACE(hobby, "Basketball", 9528), + ids = REPLACE(ids, 37564, "12345") + WHEN name == "Tim Duncan" + YIELD name AS Name, age AS Age, hobby AS Hobby, ids AS Ids; + """ + Then a ExecutionError should be raised at runtime: Storage Error: Invalid data, may be wrong value type. + When executing query: + """ + UPDATE VERTEX ON player "player100" + SET hobby = REPLACE(hobby, "Basketball", "Football"), + ids = REPLACE(ids, 37564, 12345) + WHEN name == "Tim Duncan" + YIELD name AS Name, age AS Age, hobby AS Hobby, ids AS Ids; + """ + Then the result should be, in any order: + | Name | Age | Hobby | Ids | + | "Tim Duncan" | 42 | ["Football", "Swimming", "Coding"] | [100, 528, 12345] | + When executing query: + """ + UPDATE VERTEX ON player "player100" + SET hobby = ERASE(hobby, "Swimming"), + ids = ERASE(ids, 100) + WHEN name == "Tim Duncan" + YIELD name AS Name, age AS Age, hobby AS Hobby, ids AS Ids; + """ + Then the result should be, in any order: + | Name | Age | Hobby | Ids | + | "Tim Duncan" | 42 | ["Football", "Coding"] | [528, 12345] | + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.name, player.age, player.hobby, player.ids, player.score; + """ + Then the result should be, in any order: + | player.name | player.age | player.hobby | player.ids | player.score | + | "Tim Duncan" | 42 | ["Football", "Coding"] | [528, 12345] | [50.0, 22.0, 85.0] | + + Scenario: Update player(name string, age int, hobby Set< string >, ids Set< int >, score Set< float >) + Given an empty graph + And create a space with following options: + | partition_num | 1 | + | replica_factor | 1 | + | vid_type | FIXED_STRING(20) | + And having executed: + """ + CREATE TAG player(name string, age int, hobby Set< string >, ids Set< int >, score Set< float >); + """ + And wait 3 seconds # Wait for the index to be created + # Insert vertex data for player100 + When try to execute query: + """ + INSERT VERTEX player(name, age, hobby, ids, score) VALUES "player100":( + "Tim Duncan", 42, {"Basketball", "Swimming", "Swimming"}, {100, 528, 100}, {50.0, 22.0, 50.0} + ); + """ + Then the execution should be successful + # Test SETADD operation on SET + When executing query: + """ + UPDATE VERTEX ON player "player100" + SET hobby = SETADD(hobby, "Coding"), + ids = SETADD(ids, 37564), + score = SETADD(score, 85.0) + WHEN name == "Tim Duncan" + YIELD name AS Name, age AS Age, hobby AS Hobby, ids AS Ids, score AS Score; + """ + Then the result should be, in any order: + | Name | Age | Hobby | Ids | Score | + | "Tim Duncan" | 42 | {"Basketball", "Swimming", "Coding"} | {100, 528, 37564} | {50.0, 22.0, 85.0} | + When executing query: + """ + UPDATE VERTEX ON player "player100" + SET hobby = REPLACE(hobby, "Basketball", "Football"), + ids = REPLACE(ids, 37564, 12345), + score = REPLACE(score, 85.0, 90.0) + WHEN name == "Tim Duncan" + YIELD name AS Name, age AS Age, hobby AS Hobby, ids AS Ids, score AS Score; + """ + Then the result should be, in any order: + | Name | Age | Hobby | Ids | Score | + | "Tim Duncan" | 42 | {"Football", "Swimming", "Coding"} | {100, 528, 12345} | {50.0, 22.0, 90.0} | + When executing query: + """ + UPDATE VERTEX ON player "player100" + SET hobby = ERASE(hobby, "Coding"), + ids = ERASE(ids, 100), + score = ERASE(score, 22.0) + WHEN name == "Tim Duncan" + YIELD name AS Name, age AS Age, hobby AS Hobby, ids AS Ids, score AS Score; + """ + Then the result should be, in any order: + | Name | Age | Hobby | Ids | Score | + | "Tim Duncan" | 42 | {"Football", "Swimming"} | {528, 12345} | {50.0, 90.0} | + When executing query: + """ + FETCH PROP ON player "player100" YIELD player.name, player.age, player.hobby, player.ids, player.score; + """ + Then the result should be, in any order: + | player.name | player.age | player.hobby | player.ids | player.score | + | "Tim Duncan" | 42 | {"Football", "Swimming"} | {528, 12345} | {50.0, 90.0} |