diff --git a/docs/flex/interactive/data_model.md b/docs/flex/interactive/data_model.md index be3b3aebc35b..e44e67af02f9 100644 --- a/docs/flex/interactive/data_model.md +++ b/docs/flex/interactive/data_model.md @@ -46,7 +46,7 @@ vertex_type_pair_relations: Note: - A single edge type can have multiple `vertex_type_pair_relations`. For instance, a "knows" edge might connect one person to another, symbolizing their friendship. Alternatively, it could associate a person with a skill, indicating their proficiency in that skill. - The permissible relations include: `ONE_TO_ONE`, `ONE_TO_MANY`, `MANY_TO_ONE`, and `MANY_TO_MANY`. These relations can be utilized by the optimizer to generate more efficient execution plans. -- Currently we only support at most one property for each edge triplet. +- Multiple edge properties are supported. - We currently only support `var_char` and `long_text` as the edge property among all string types. - All implementation related configuration are put under `x_csr_params`. - `max_vertex_num` limit the number of vertices of this type: diff --git a/flex/engines/graph_db/runtime/common/columns/edge_columns.cc b/flex/engines/graph_db/runtime/common/columns/edge_columns.cc index 5e289ae5b11d..8c42cad299b5 100644 --- a/flex/engines/graph_db/runtime/common/columns/edge_columns.cc +++ b/flex/engines/graph_db/runtime/common/columns/edge_columns.cc @@ -21,12 +21,7 @@ namespace runtime { std::shared_ptr SDSLEdgeColumn::shuffle( const std::vector& offsets) const { - std::vector sub_types; - if (prop_type_ == PropertyType::kRecordView) { - sub_types = std::dynamic_pointer_cast>(prop_col_) - ->sub_types(); - } - SDSLEdgeColumnBuilder builder(dir_, label_, prop_type_, sub_types); + SDSLEdgeColumnBuilder builder(dir_, label_, prop_type_); size_t new_row_num = offsets.size(); builder.reserve(new_row_num); @@ -84,8 +79,7 @@ std::shared_ptr SDSLEdgeColumn::optional_shuffle( } std::shared_ptr SDSLEdgeColumnBuilder::finish() { - auto ret = - std::make_shared(dir_, label_, prop_type_, sub_types_); + auto ret = std::make_shared(dir_, label_, prop_type_); ret->edges_.swap(edges_); // shrink to fit prop_col_->resize(ret->edges_.size()); diff --git a/flex/engines/graph_db/runtime/common/columns/edge_columns.h b/flex/engines/graph_db/runtime/common/columns/edge_columns.h index ffc57b2106fd..4d4d30e4387d 100644 --- a/flex/engines/graph_db/runtime/common/columns/edge_columns.h +++ b/flex/engines/graph_db/runtime/common/columns/edge_columns.h @@ -58,7 +58,9 @@ static inline void get_edge_data(EdgePropVecBase* prop, size_t idx, edge_data.value.day_val = dynamic_cast*>(prop)->get_view(idx); } else if (prop->type() == PropertyType::kRecordView) { - // edge_data.type = RTAnyType::kRecordView; + edge_data.type = RTAnyType::kRecordView; + edge_data.value.record_view = + dynamic_cast*>(prop)->get_view(idx); } else { edge_data.type = RTAnyType::kUnknown; } @@ -84,6 +86,9 @@ static inline void set_edge_data(EdgePropVecBase* col, size_t idx, dynamic_cast*>(col)->set(idx, edge_data.value.date_val); } else if (edge_data.type == RTAnyType::kDate32) { dynamic_cast*>(col)->set(idx, edge_data.value.day_val); + } else if (edge_data.type == RTAnyType::kRecordView) { + dynamic_cast*>(col)->set( + idx, edge_data.value.record_view); } else { // LOG(FATAL) << "not support for " << edge_data.type; } @@ -650,13 +655,11 @@ class BDMLEdgeColumn : public IEdgeColumn { class SDSLEdgeColumnBuilder : public IContextColumnBuilder { public: SDSLEdgeColumnBuilder(Direction dir, const LabelTriplet& label, - PropertyType prop_type, - const std::vector& sub_types = {}) + PropertyType prop_type) : dir_(dir), label_(label), prop_type_(prop_type), - prop_col_(EdgePropVecBase::make_edge_prop_vec(prop_type)), - sub_types_(sub_types) {} + prop_col_(EdgePropVecBase::make_edge_prop_vec(prop_type)) {} ~SDSLEdgeColumnBuilder() = default; void reserve(size_t size) override { edges_.reserve(size); } @@ -684,7 +687,6 @@ class SDSLEdgeColumnBuilder : public IContextColumnBuilder { std::vector> edges_; PropertyType prop_type_; std::shared_ptr prop_col_; - std::vector sub_types_; }; template diff --git a/flex/engines/graph_db/runtime/common/operators/retrieve/edge_expand.cc b/flex/engines/graph_db/runtime/common/operators/retrieve/edge_expand.cc index e4204b6c0f2c..149f60b9a188 100644 --- a/flex/engines/graph_db/runtime/common/operators/retrieve/edge_expand.cc +++ b/flex/engines/graph_db/runtime/common/operators/retrieve/edge_expand.cc @@ -183,8 +183,7 @@ Context EdgeExpand::expand_edge_without_predicate( pt = props[0]; } - SDSLEdgeColumnBuilder builder(Direction::kIn, params.labels[0], pt, - props); + SDSLEdgeColumnBuilder builder(Direction::kIn, params.labels[0], pt); label_t dst_label = params.labels[0].dst_label; foreach_vertex(input_vertex_list, @@ -223,8 +222,7 @@ Context EdgeExpand::expand_edge_without_predicate( pt = PropertyType::kRecordView; } - SDSLEdgeColumnBuilder builder(Direction::kOut, params.labels[0], pt, - props); + SDSLEdgeColumnBuilder builder(Direction::kOut, params.labels[0], pt); label_t src_label = params.labels[0].src_label; foreach_vertex(input_vertex_list, [&](size_t index, label_t label, vid_t v) { @@ -312,7 +310,7 @@ Context EdgeExpand::expand_edge_without_predicate( if (params.dir == Direction::kOut) { auto& triplet = labels[0]; SDSLEdgeColumnBuilder builder(Direction::kOut, triplet, - label_props[0].second, props_vec[0]); + label_props[0].second); foreach_vertex( input_vertex_list, [&](size_t index, label_t label, vid_t v) { if (label == triplet.src_label) { @@ -332,7 +330,7 @@ Context EdgeExpand::expand_edge_without_predicate( } else if (params.dir == Direction::kIn) { auto& triplet = labels[0]; SDSLEdgeColumnBuilder builder(Direction::kIn, triplet, - label_props[0].second, props_vec[0]); + label_props[0].second); foreach_vertex( input_vertex_list, [&](size_t index, label_t label, vid_t v) { if (label == triplet.dst_label) { diff --git a/flex/engines/graph_db/runtime/common/operators/retrieve/edge_expand.h b/flex/engines/graph_db/runtime/common/operators/retrieve/edge_expand.h index 4466814bc477..29b0d63a4ba5 100644 --- a/flex/engines/graph_db/runtime/common/operators/retrieve/edge_expand.h +++ b/flex/engines/graph_db/runtime/common/operators/retrieve/edge_expand.h @@ -83,8 +83,7 @@ class EdgeExpand { pt = PropertyType::kRecordView; } - SDSLEdgeColumnBuilder builder(Direction::kIn, params.labels[0], pt, - props); + SDSLEdgeColumnBuilder builder(Direction::kIn, params.labels[0], pt); foreach_vertex(input_vertex_list, [&](size_t index, label_t label, vid_t v) { @@ -122,8 +121,7 @@ class EdgeExpand { pt = PropertyType::kRecordView; } - SDSLEdgeColumnBuilder builder(Direction::kOut, params.labels[0], pt, - props); + SDSLEdgeColumnBuilder builder(Direction::kOut, params.labels[0], pt); foreach_vertex(input_vertex_list, [&](size_t index, label_t label, vid_t v) { diff --git a/flex/engines/graph_db/runtime/common/rt_any.cc b/flex/engines/graph_db/runtime/common/rt_any.cc index b0f979e9b1d0..0c064e1fcd3e 100644 --- a/flex/engines/graph_db/runtime/common/rt_any.cc +++ b/flex/engines/graph_db/runtime/common/rt_any.cc @@ -693,6 +693,8 @@ static void sink_any(const Any& any, common::Value* value) { value->set_boolean(any.AsBool()); } else if (any.type == PropertyType::Double()) { value->set_f64(any.AsDouble()); + } else if (any.type == PropertyType::Empty()) { + value->mutable_none(); } else { LOG(FATAL) << "Any value: " << any.to_string() << ", type = " << any.type.type_enum; @@ -987,6 +989,8 @@ std::shared_ptr EdgePropVecBase::make_edge_prop_vec( return std::make_shared>(); } else if (type == PropertyType::Empty()) { return std::make_shared>(); + } else if (type == PropertyType::RecordView()) { + return std::make_shared>(); } else { LOG(FATAL) << "not support for " << type; return nullptr; diff --git a/flex/engines/graph_db/runtime/common/rt_any.h b/flex/engines/graph_db/runtime/common/rt_any.h index 94ea4f1da04d..2a618907f856 100644 --- a/flex/engines/graph_db/runtime/common/rt_any.h +++ b/flex/engines/graph_db/runtime/common/rt_any.h @@ -336,6 +336,8 @@ struct EdgeData { return grape::EmptyType(); } else if constexpr (std::is_same_v) { return Date(value.i64_val); + } else if constexpr (std::is_same_v) { + return value.record_view; } else { LOG(FATAL) << "not support for " << typeid(T).name(); } @@ -366,6 +368,9 @@ struct EdgeData { } else if constexpr (std::is_same_v) { type = RTAnyType::kTimestamp; value.date_val = val; + } else if constexpr (std::is_same_v) { + type = RTAnyType::kRecordView; + value.record_view = val; } else { LOG(FATAL) << "not support for " << typeid(T).name(); } @@ -385,14 +390,14 @@ struct EdgeData { return std::to_string(value.f64_val); } else if (type == RTAnyType::kBoolValue) { return value.b_val ? "true" : "false"; - } else if (type == RTAnyType::kEmpty) { - return ""; } else if (type == RTAnyType::kDate32) { return value.day_val.to_string(); } else if (type == RTAnyType::kTimestamp) { return std::to_string(value.date_val.milli_second); } else if (type == RTAnyType::kEmpty) { return ""; + } else if (type == RTAnyType::kRecordView) { + return value.record_view.to_string(); } else { LOG(FATAL) << "Unexpected property type: " << static_cast(type); return ""; @@ -430,6 +435,10 @@ struct EdgeData { type = RTAnyType::kTimestamp; value.date_val = any.value.d; break; + case impl::PropertyTypeImpl::kRecordView: + type = RTAnyType::kRecordView; + value.record_view = any.value.record_view; + break; default: LOG(FATAL) << "Unexpected property type: " << static_cast(any.type.type_enum); @@ -471,6 +480,8 @@ struct EdgeData { return value.day_val == e.value.day_val; } else if (type == RTAnyType::kTimestamp) { return value.date_val == e.value.date_val; + } else if (type == RTAnyType::kRecordView) { + return value.record_view == e.value.record_view; } else { return false; } @@ -486,6 +497,7 @@ struct EdgeData { pod_string_view str_val; Date date_val; Day day_val; + RecordView record_view; // todo: make recordview as a pod type // RecordView record; } value; diff --git a/flex/interactive/sdk/python/gs_interactive/tests/conftest.py b/flex/interactive/sdk/python/gs_interactive/tests/conftest.py index aa744a13b438..a516600b50dd 100644 --- a/flex/interactive/sdk/python/gs_interactive/tests/conftest.py +++ b/flex/interactive/sdk/python/gs_interactive/tests/conftest.py @@ -124,6 +124,91 @@ }, } +modern_graph_multiple_edge_properties = { + "name": "full_graph", + "description": "This is a test graph", + "schema": { + "vertex_types": [ + { + "type_name": "person", + "properties": [ + { + "property_name": "id", + "property_type": {"primitive_type": "DT_SIGNED_INT64"}, + }, + { + "property_name": "name", + "property_type": {"string": {"long_text": ""}}, + }, + { + "property_name": "age", + "property_type": {"primitive_type": "DT_SIGNED_INT32"}, + }, + ], + "primary_keys": ["id"], + }, + { + "type_name": "software", + "properties": [ + { + "property_name": "id", + "property_type": {"primitive_type": "DT_SIGNED_INT64"}, + }, + { + "property_name": "name", + "property_type": {"string": {"long_text": ""}}, + }, + { + "property_name": "lang", + "property_type": {"string": {"long_text": ""}}, + }, + ], + "primary_keys": ["id"], + }, + ], + "edge_types": [ + { + "type_name": "knows", + "vertex_type_pair_relations": [ + { + "source_vertex": "person", + "destination_vertex": "person", + "relation": "MANY_TO_MANY", + } + ], + "properties": [ + { + "property_name": "weight", + "property_type": {"primitive_type": "DT_DOUBLE"}, + } + ], + "primary_keys": [], + }, + { + "type_name": "created", + "vertex_type_pair_relations": [ + { + "source_vertex": "person", + "destination_vertex": "software", + "relation": "MANY_TO_MANY", + } + ], + "properties": [ + { + "property_name": "weight", + "property_type": {"primitive_type": "DT_DOUBLE"}, + }, + { + "property_name": "since", + "property_type": {"primitive_type": "DT_SIGNED_INT32"}, + }, + ], + "primary_keys": [], + }, + ], + }, +} + modern_graph_vertex_only = { "name": "vertex_only", "description": "This is a test graph, only contains vertex", @@ -895,6 +980,18 @@ def create_modern_graph(interactive_session): delete_running_graph(interactive_session, graph_id) +@pytest.fixture(scope="function") +def create_modern_graph_multiple_edge_property(interactive_session): + create_graph_request = CreateGraphRequest.from_dict( + modern_graph_multiple_edge_properties + ) + resp = interactive_session.create_graph(create_graph_request) + assert resp.is_ok() + graph_id = resp.get_value().graph_id + yield graph_id + delete_running_graph(interactive_session, graph_id) + + @pytest.fixture(scope="function") def create_graph_algo_graph(interactive_session): create_graph_request = CreateGraphRequest.from_dict(graph_algo_graph) diff --git a/flex/interactive/sdk/python/gs_interactive/tests/test_robustness.py b/flex/interactive/sdk/python/gs_interactive/tests/test_robustness.py index 8fb70a4abf95..05da5a299e0d 100644 --- a/flex/interactive/sdk/python/gs_interactive/tests/test_robustness.py +++ b/flex/interactive/sdk/python/gs_interactive/tests/test_robustness.py @@ -432,3 +432,42 @@ def test_var_char_property( for record in records: # all string property in this graph is var char with max_length 2 assert len(record["personName"]) == 2 + + +def test_multiple_edge_property( + interactive_session, neo4j_session, create_modern_graph_multiple_edge_property +): + print("[Test multiple edge property]") + import_data_to_full_modern_graph( + interactive_session, create_modern_graph_multiple_edge_property + ) + start_service_on_graph( + interactive_session, create_modern_graph_multiple_edge_property + ) + ensure_compiler_schema_ready( + interactive_session, neo4j_session, create_modern_graph_multiple_edge_property + ) + result = neo4j_session.run( + "MATCH (n: person)-[e]->(m: software) RETURN e.weight AS weight, e.since AS since ORDER BY weight ASC, since ASC;" + ) + records = result.fetch(10) + assert len(records) == 4 + expected_result = [ + {"weight": 0.2, "since": 2023}, + {"weight": 0.4, "since": 2020}, + {"weight": 0.4, "since": 2022}, + {"weight": 1.0, "since": 2021}, + ] + + for i in range(len(records)): + assert records[i]["weight"] == expected_result[i]["weight"] + assert records[i]["since"] == expected_result[i]["since"] + + result = neo4j_session.run( + "MATCH (n: person)-[e]->(m: software) RETURN e ORDER BY e.weight ASC, e.since ASC;" + ) + records = result.fetch(10) + assert len(records) == 4 + for i in range(len(records)): + assert records[i]["e"]["weight"] == expected_result[i]["weight"] + assert records[i]["e"]["since"] == expected_result[i]["since"] diff --git a/flex/utils/property/column.cc b/flex/utils/property/column.cc index 332d35f0c2cc..b35a0cd4cf23 100644 --- a/flex/utils/property/column.cc +++ b/flex/utils/property/column.cc @@ -178,6 +178,10 @@ std::shared_ptr CreateColumn( return std::make_shared>(strategy); } else if (type == PropertyType::kBool) { return std::make_shared(strategy); + } else if (type == PropertyType::kUInt8) { + return std::make_shared(strategy); + } else if (type == PropertyType::kUInt16) { + return std::make_shared(strategy); } else if (type == PropertyType::kInt32) { return std::make_shared(strategy); } else if (type == PropertyType::kInt64) { diff --git a/flex/utils/property/column.h b/flex/utils/property/column.h index d25c0fff1656..71a5bfd461a1 100644 --- a/flex/utils/property/column.h +++ b/flex/utils/property/column.h @@ -295,6 +295,8 @@ class TypedColumn : public ColumnBase { }; using BoolColumn = TypedColumn; +using UInt8Column = TypedColumn; +using UInt16Column = TypedColumn; using IntColumn = TypedColumn; using UIntColumn = TypedColumn; using LongColumn = TypedColumn; diff --git a/flex/utils/property/types.cc b/flex/utils/property/types.cc index 456524b07e3f..ea5de66151d8 100644 --- a/flex/utils/property/types.cc +++ b/flex/utils/property/types.cc @@ -90,6 +90,18 @@ Any RecordView::operator[](size_t col_id) const { return table->get_column_by_id(col_id)->get(offset); } +std::string RecordView::to_string() const { + std::string ret = "RecordView{"; + for (size_t i = 0; i < table->col_num(); ++i) { + if (i > 0) { + ret += ", "; + } + ret += table->get_column_by_id(i)->get(offset).to_string(); + } + ret += "}"; + return ret; +} + Record::Record(size_t len) : len(len) { props = new Any[len]; } Record::Record(const Record& record) { diff --git a/flex/utils/property/types.h b/flex/utils/property/types.h index dafa741b2842..6eeaa3be9a8a 100644 --- a/flex/utils/property/types.h +++ b/flex/utils/property/types.h @@ -286,7 +286,7 @@ struct LabelKey { class Table; struct Any; struct RecordView { - RecordView() : offset(0), table(nullptr) {} + RecordView() = default; RecordView(size_t offset, const Table* table) : offset(offset), table(table) {} size_t size() const; @@ -295,6 +295,15 @@ struct RecordView { template T get_field(int col_id) const; + inline bool operator==(const RecordView& other) const { + if (size() != other.size()) { + return false; + } + return table == other.table && offset == other.offset; + } + + std::string to_string() const; + size_t offset; const Table* table; }; @@ -1374,6 +1383,12 @@ inline ostream& operator<<(ostream& os, gs::PropertyType pt) { os << "varchar(" << pt.additional_type_info.max_length << ")"; } else if (pt == gs::PropertyType::VertexGlobalId()) { os << "vertex_global_id"; + } else if (pt == gs::PropertyType::Label()) { + os << "label"; + } else if (pt == gs::PropertyType::RecordView()) { + os << "record_view"; + } else if (pt == gs::PropertyType::Record()) { + os << "record"; } else { os << "unknown"; }