From 28741e21b0449e3ee30404d7e8cf9a35b1be2853 Mon Sep 17 00:00:00 2001 From: grant lodge <6323995+thelonelyvulpes@users.noreply.github.com> Date: Mon, 14 Mar 2022 14:12:43 +0000 Subject: [PATCH] Implement ElementID for Nodes & Relationships (#591) - Implement string ElementId for Nodes and Relationships. - Deprecate Id - 5.0 Handshake --- .../IO/Connection.cs | 4 +- .../SupportedFeatures.cs | 1 + .../TestBlackList.cs | 3 +- .../Transaction/TransactionManager.cs | 82 +++---- .../Types/NativeToCypher.cs | 40 +++- .../Neo4j.Driver.Tests/EntityTests.cs | 104 --------- .../ElementNodeSerializerTests.cs | 97 +++++++++ .../ElementPathSerializerTests.cs | 206 ++++++++++++++++++ .../ElementRelationshipSerializerTests.cs | 105 +++++++++ ...ementUnboundRelationshipSerializerTests.cs | 97 +++++++++ Neo4j.Driver/Neo4j.Driver.Tests/NodeTests.cs | 57 +++++ Neo4j.Driver/Neo4j.Driver.Tests/PathTests.cs | 48 ++++ .../Neo4j.Driver.Tests/RelationshipTests.cs | 50 +++++ .../Internal/Connector/SocketClient.cs | 3 +- .../Internal/IO/PackStreamReader.cs | 3 - .../Internal/IO/ReadOnlySerializer.cs | 7 +- .../ValueSerializers/ElementNodeSerializer.cs | 56 +++++ .../ElementRelationshipSerializer.cs | 47 ++++ .../ElementUnboundRelationshipSerializer.cs | 44 ++++ .../IO/ValueSerializers/PathSerializer.cs | 4 +- .../Internal/Protocol/BoltProtocolFactory.cs | 112 ++++------ .../Protocol/BoltProtocolMessageFormat.cs | 2 + .../Internal/Protocol/BoltProtocolV5_0.cs | 2 + .../Protocol/BoltProtocolV5_0MessageFormat.cs | 36 +++ .../Internal/Protocol/BoltProtocolVersion.cs | 3 - .../Neo4j.Driver/Internal/Types/Node.cs | 42 +++- .../Internal/Types/Relationship.cs | 74 +++++-- Neo4j.Driver/Neo4j.Driver/Types/IEntity.cs | 6 + .../Neo4j.Driver/Types/IRelationship.cs | 12 + 29 files changed, 1084 insertions(+), 263 deletions(-) delete mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/EntityTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementNodeSerializerTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementPathSerializerTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementRelationshipSerializerTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementUnboundRelationshipSerializerTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/NodeTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/PathTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/RelationshipTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementNodeSerializer.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementRelationshipSerializer.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementUnboundRelationshipSerializer.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolV5_0MessageFormat.cs diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/IO/Connection.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/IO/Connection.cs index d61522727..952f7d1e1 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/IO/Connection.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/IO/Connection.cs @@ -88,10 +88,10 @@ public void Close() } public void Dispose() - { + { Dispose(true); GC.SuppressFinalize(this); - } + } protected virtual void Dispose(bool disposing) { diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs index 2919d90bc..8d5ec6ab0 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs @@ -30,6 +30,7 @@ static SupportedFeatures() "Feature:Bolt:4.2", "Feature:Bolt:4.3", "Feature:Bolt:4.4", + "Feature:Bolt:5.0", "Feature:Impersonation", //"Feature:TLS:1.1", "Feature:TLS:1.2", diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/TestBlackList.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/TestBlackList.cs index 69cf09380..63c3a50a1 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/TestBlackList.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/TestBlackList.cs @@ -108,8 +108,9 @@ static class TestBlackList "Backend does not yet support serializing paths"), ("stub.tx_lifetime.test_tx_lifetime.TestTxLifetime.test_managed_tx_raises_tx_managed_exec", - "Driver (still) allows explicit managing of managed transaction") + "Driver (still) allows explicit managing of managed transaction"), + ("test_summary.TestSummary.test_protocol_version_information", "Server not responding with 5.0") }; public static bool FindTest(string testName, out string reason) diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Transaction/TransactionManager.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Transaction/TransactionManager.cs index 07a8c702b..717581e21 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Transaction/TransactionManager.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Transaction/TransactionManager.cs @@ -6,45 +6,45 @@ namespace Neo4j.Driver.Tests.TestBackend { - internal class TransactionWrapper - { - public IAsyncTransaction Transaction { get; private set; } - private Func> ResultHandler; - - public TransactionWrapper(IAsyncTransaction transaction, Func>resultHandler) - { - Transaction = transaction; - ResultHandler = resultHandler; - } - - public async Task ProcessResults(IResultCursor cursor) - { - return await ResultHandler(cursor); - } - - } - - - internal class TransactionManager - { - private Dictionary Transactions { get; set; } = new Dictionary(); - - public string AddTransaction(TransactionWrapper transation) - { - var key = ProtocolObjectManager.GenerateUniqueIdString(); - Transactions.Add(key, transation); - return key; - } - - public void RemoveTransaction(string key) - { - Transactions.Remove(key); - } - - public TransactionWrapper FindTransaction(string key) - { - return Transactions[key]; - } - - } + internal class TransactionWrapper + { + public IAsyncTransaction Transaction { get; private set; } + private Func> ResultHandler; + + public TransactionWrapper(IAsyncTransaction transaction, Func>resultHandler) + { + Transaction = transaction; + ResultHandler = resultHandler; + } + + public async Task ProcessResults(IResultCursor cursor) + { + return await ResultHandler(cursor); + } + + } + + + internal class TransactionManager + { + private Dictionary Transactions { get; set; } = new Dictionary(); + + public string AddTransaction(TransactionWrapper transation) + { + var key = ProtocolObjectManager.GenerateUniqueIdString(); + Transactions.Add(key, transation); + return key; + } + + public void RemoveTransaction(string key) + { + Transactions.Remove(key); + } + + public TransactionWrapper FindTransaction(string key) + { + return Transactions[key]; + } + + } } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs index 9da78c3c0..7ae8874b1 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; namespace Neo4j.Driver.Tests.TestBackend { @@ -38,17 +39,15 @@ internal static class NativeToCypher { typeof(Duration), CypherTODO }, { typeof(Point), CypherTODO }, - { typeof(INode), CypherNode }, - { typeof(IRelationship), CypherTODO }, - { typeof(IPath), CypherTODO } + { typeof(INode), CypherNode }, + { typeof(IRelationship), CypherRelationship }, + { typeof(IPath), CypherPath } }; public static object Convert(object sourceObject) { if (sourceObject is null) - { return new NativeToCypherObject { name = "CypherNull", data = { } }; - } if (sourceObject as List != null) return FunctionMap[typeof(List)]("CypherList", sourceObject); @@ -146,12 +145,43 @@ public static NativeToCypherObject CypherNode(string cypherType, object obj) var cypherNode = new Dictionary { ["id"] = Convert(node.Id), + ["elementId"] = Convert(node.ElementId), ["labels"] = Convert(new List(node.Labels)), ["props"] = Convert(new Dictionary(node.Properties)) }; return new NativeToCypherObject() { name = "Node", data = cypherNode }; } + + public static NativeToCypherObject CypherRelationship(string cypherType, object obj) + { + var rel = (IRelationship)obj; + var cypherRel = new Dictionary + { + ["id"] = Convert(rel.Id), + ["startNodeId"] = Convert(rel.StartNodeId), + ["type"] = Convert(rel.Type), + ["endNodeId"] = Convert(rel.EndNodeId), + ["props"] = Convert(new Dictionary(rel.Properties)), + ["elementId"] = Convert(rel.ElementId), + ["startNodeElementId"] = Convert(rel.StartNodeElementId), + ["endNodeElementId"] = Convert(rel.EndNodeElementId), + }; + + return new NativeToCypherObject() { name = "Relationship", data = cypherRel }; + } + + public static NativeToCypherObject CypherPath(string cypherType, object obj) + { + var path = (IPath)obj; + var cypherPath = new Dictionary + { + ["nodes"] = Convert(path.Nodes.OfType().ToList()), + ["relationships"] = Convert(path.Relationships.OfType().ToList()) + }; + + return new NativeToCypherObject() { name = "Path", data = cypherPath }; + } } } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/EntityTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/EntityTests.cs deleted file mode 100644 index 278eabaa5..000000000 --- a/Neo4j.Driver/Neo4j.Driver.Tests/EntityTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) "Neo4j" -// Neo4j Sweden AB [http://neo4j.com] -// -// This file is part of Neo4j. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -using FluentAssertions; -using Moq; -using Neo4j.Driver.Internal; -using Neo4j.Driver.Internal.Types; -using Neo4j.Driver; -using Xunit; - -namespace Neo4j.Driver.Tests -{ - public class EntityTests - { - public class NodeTests - { - [Fact] - public void ShouldEqualIfIdEquals() - { - var node1 = new Node(123, new []{"buibui"}, null); - var node2 = new Node(123, new []{"lala"}, null); - node1.Equals(node2).Should().BeTrue(); - Equals(node1, node2).Should().BeTrue(); - node1.GetHashCode().Should().Be(node2.GetHashCode()); - - var node3Mock = new Mock(); - node3Mock.Setup(f => f.Id).Returns(123); - node3Mock.Setup(f => f.Labels).Returns(new[] { "same interface, different implementation" }); - node3Mock.Setup(f => f.GetHashCode()).Returns(123); - var node3 = node3Mock.Object; - node1.Equals(node3).Should().BeTrue(); - Equals(node1, node3).Should().BeTrue(); - // TODO: The following test is currently not supported by Moq - //node1.GetHashCode().Should().Be(node3.GetHashCode()); - } - } - - public class RelationshipTests - { - [Fact] - public void ShouldEqualIfIdEquals() - { - var rel1 = new Relationship(123, 000, 111, "buibui", null); - var rel2 = new Relationship(123, 222, 333, "lala", null); - rel1.Equals(rel2).Should().BeTrue(); - Equals(rel1, rel2).Should().BeTrue(); - rel1.GetHashCode().Should().Be(rel2.GetHashCode()); - - var rel3Mock = new Mock(); - rel3Mock.Setup(f => f.Id).Returns(123); - rel3Mock.Setup(f => f.StartNodeId).Returns(444); - rel3Mock.Setup(f => f.EndNodeId).Returns(555); - rel3Mock.Setup(f => f.Type).Returns("same interface, different implementation"); - rel3Mock.Setup(f => f.GetHashCode()).Returns(123); - var rel3 = rel3Mock.Object; - - rel1.Equals(rel3).Should().BeTrue(); - Equals(rel1, rel3).Should().BeTrue(); - // TODO: The following test is currently not supported by Moq - //rel1.GetHashCode().Should().Be(rel3.GetHashCode()); - } - } - - public class PathTests - { - [Fact] - public void ShouldEqualIfIdEquals() - { - var path1 = new Path(null, - new []{ new Node(123, new []{"buibui"}, null) }, - new []{ new Relationship(1, 000, 111, "buibui", null)}); - var path2 = new Path(null, - new[] { new Node(123, new[] { "lala" }, null) }, - new []{ new Relationship(1, 222, 333, "lala", null)}); - path1.Equals(path2).Should().BeTrue(); - Equals(path1, path2).Should().BeTrue(); - path1.GetHashCode().Should().Be(path2.GetHashCode()); - - var path3Mock = new Mock(); - path3Mock.Setup(f => f.Start).Returns(new Node(123, new[] { "same interface, different implementation" }, null)); - path3Mock.Setup(f => f.Relationships).Returns(new[] { new Relationship(1, 222, 333, "same interface --- different implementation", null) }); - var path3 = path3Mock.Object; - path1.Equals(path3).Should().BeTrue(); - Equals(path1, path3).Should().BeTrue(); - - // TODO: The following test is currently not supported by Moq - //path1.GetHashCode().Should().Be(path2.GetHashCode()); - } - } - } -} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementNodeSerializerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementNodeSerializerTests.cs new file mode 100644 index 000000000..3cd5b8136 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementNodeSerializerTests.cs @@ -0,0 +1,97 @@ +// Copyright (c) 2002-2022 "Neo4j," +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using FluentAssertions; +using Neo4j.Driver.Internal.Types; +using Xunit; + +namespace Neo4j.Driver.Internal.IO.ValueSerializers +{ + public class ElementNodeSerializerTests : PackStreamSerializerTests + { + internal override IPackStreamSerializer SerializerUnderTest => new ElementNodeSerializer(); + + [Fact] + public void ShouldDeserialize() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer(); + + writer.WriteStructHeader(3, ElementNodeSerializer.Node); + writer.Write(1); + writer.Write(new List { "Label1", "Label2" }); + writer.Write(new Dictionary + { + {"prop1", "something"}, + {"prop2", 15}, + {"prop3", true} + }); + writer.Write("1"); + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var value = readerMachine.Reader().Read(); + + value.Should().NotBeNull(); + value.Should().BeOfType().Which.Id.Should().Be(1L); + value.Should().BeOfType().Which.Labels.Should().Equal(new[] { "Label1", "Label2" }); + value.Should().BeOfType().Which.Properties.Should().HaveCount(3).And.Contain(new[] + { + new KeyValuePair("prop1", "something"), + new KeyValuePair("prop2", 15L), + new KeyValuePair("prop3", true), + }); + value.Should().BeOfType().Which.ElementId.Should().Be("1"); + } + + [Fact] + public void ShouldDeserializeWithNulls() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer(); + + writer.WriteStructHeader(3, ElementNodeSerializer.Node); + writer.WriteNull(); + writer.Write(new List { "Label1", "Label2" }); + writer.Write(new Dictionary + { + {"prop1", "something"}, + {"prop2", 15}, + {"prop3", true} + }); + writer.Write("1"); + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var value = readerMachine.Reader().Read(); + + value.Should().NotBeNull(); + value.Should().BeOfType(); + var node = value.As(); + + node.ElementId.Should().Be("1"); + node.Id.Should().Be(-1L); + + node.Labels.Should().Equal(new[] { "Label1", "Label2" }); + node.Properties.Should().HaveCount(3).And.Contain(new[] + { + new KeyValuePair("prop1", "something"), + new KeyValuePair("prop2", 15L), + new KeyValuePair("prop3", true), + }); + } + } +} \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementPathSerializerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementPathSerializerTests.cs new file mode 100644 index 000000000..ac14ad142 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementPathSerializerTests.cs @@ -0,0 +1,206 @@ +// Copyright (c) 2002-2022 "Neo4j," +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using FluentAssertions; +using Neo4j.Driver.Internal.Types; +using Xunit; + +namespace Neo4j.Driver.Internal.IO.ValueSerializers +{ + public class ElementPathSerializerTests : PackStreamSerializerTests + { + internal override IPackStreamSerializer SerializerUnderTest => new PathSerializer(); + + internal override IEnumerable SerializersNeeded => + new IPackStreamSerializer[] { new ElementNodeSerializer(), new ElementUnboundRelationshipSerializer() }; + + + [Fact] + public void ShouldDeserializeAddingElementIds() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer(); + + SerializeElementPath(writer, + new List + { + new Node(1, new List{"a"}, new Dictionary()), + new Node(2, new List{"a"}, new Dictionary()), + }, + new List + { + new Relationship(1, -1, -1, "LIKES", new Dictionary()) + }, + new List + { + 1, 1 + }); + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var value = readerMachine.Reader().Read(); + + var path = value.Should().BeOfType(); + + path.Which.Nodes.Should().AllBeOfType(); + path.Which.Relationships.Should().AllBeOfType(); + + var nodes = path.Which.Nodes; + var relationships = path.Which.Relationships; + + + nodes[0].Id.Should().Be(1L); + nodes[0].ElementId.Should().Be("1"); + nodes[1].Id.Should().Be(2L); + nodes[1].ElementId.Should().Be("2"); + + relationships[0].Id.Should().Be(1); + relationships[0].ElementId.Should().Be("1"); + relationships[0].StartNodeId.Should().Be(1L); + relationships[0].StartNodeElementId.Should().Be("1"); + relationships[0].EndNodeId.Should().Be(2L); + relationships[0].EndNodeElementId.Should().Be("2"); + } + + [Fact] + public void ShouldDeserializeWithElementIds() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer(); + + SerializeElementPath(writer, + new List + { + new Node(1, "n1", new List{"a"}, new Dictionary()), + new Node(2, "n2",new List{"a"}, new Dictionary()), + }, + new List + { + new Relationship(1, "r1", -1, -1, "-1", "-1", "LIKES", new Dictionary()) + }, + new List + { + 1, 1 + }); + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var value = readerMachine.Reader().Read(); + + var path = value.Should().BeOfType(); + + path.Which.Nodes.Should().AllBeOfType(); + path.Which.Relationships.Should().AllBeOfType(); + + var nodes = path.Which.Nodes; + var relationships = path.Which.Relationships; + + nodes[0].Id.Should().Be(1L); + nodes[0].ElementId.Should().Be("n1"); + nodes[1].Id.Should().Be(2L); + nodes[1].ElementId.Should().Be("n2"); + + relationships[0].Id.Should().Be(1); + relationships[0].ElementId.Should().Be("r1"); + relationships[0].StartNodeId.Should().Be(1L); + relationships[0].StartNodeElementId.Should().Be("n1"); + relationships[0].EndNodeId.Should().Be(2L); + relationships[0].EndNodeElementId.Should().Be("n2"); + } + + [Fact] + public void ShouldDeserializeWithOnlyElementIds() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer(); + + SerializeElementPath(writer, + new List + { + new Node(-1, "n1", new List{"a"}, new Dictionary()), + new Node(-1, "n2",new List{"a"}, new Dictionary()), + }, + new List + { + new Relationship(-1, "r1", -1, -1, "-1", "-1", "LIKES", new Dictionary()) + }, + new List + { + 1, 1 + }); + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var value = readerMachine.Reader().Read(); + + var path = value.Should().BeOfType(); + + path.Which.Nodes.Should().AllBeOfType(); + path.Which.Relationships.Should().AllBeOfType(); + + var nodes = path.Which.Nodes; + var relationships = path.Which.Relationships; + + nodes[0].Id.Should().Be(-1L); + nodes[0].ElementId.Should().Be("n1"); + nodes[1].Id.Should().Be(-1L); + nodes[1].ElementId.Should().Be("n2"); + + relationships[0].Id.Should().Be(-1L); + relationships[0].ElementId.Should().Be("r1"); + relationships[0].StartNodeId.Should().Be(-1L); + relationships[0].StartNodeElementId.Should().Be("n1"); + relationships[0].EndNodeId.Should().Be(-1L); + relationships[0].EndNodeElementId.Should().Be("n2"); + } + + private static void SerializeElementPath(IPackStreamWriter writer, List nodes, List rels, + List indicies) + { + writer.WriteStructHeader(3, PathSerializer.Path); + writer.WriteListHeader(nodes.Count); + + foreach (var node in nodes) + { + writer.WriteStructHeader(3, ElementNodeSerializer.Node); + + if (node.Id == -1) + writer.WriteNull(); + else + writer.Write(node.Id); + writer.Write(node.Labels); + writer.Write(node.Properties); + writer.Write(node.ElementId); + } + + writer.WriteListHeader(rels.Count); + + foreach (var rel in rels) + { + writer.WriteStructHeader(3, UnboundRelationshipSerializer.UnboundRelationship); + + if (rel.Id == -1) + writer.WriteNull(); + else + writer.Write(rel.Id); + writer.Write(rel.Type); + writer.Write(rel.Properties); + writer.Write(rel.ElementId); + } + + writer.Write(indicies); + } + } +} \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementRelationshipSerializerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementRelationshipSerializerTests.cs new file mode 100644 index 000000000..389abc70e --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementRelationshipSerializerTests.cs @@ -0,0 +1,105 @@ +// Copyright (c) 2002-2022 "Neo4j," +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using FluentAssertions; +using Neo4j.Driver.Internal.Types; +using Xunit; + +namespace Neo4j.Driver.Internal.IO.ValueSerializers +{ + public class ElementRelationshipSerializerTests : PackStreamSerializerTests + { + internal override IPackStreamSerializer SerializerUnderTest => new ElementRelationshipSerializer(); + + [Fact] + public void ShouldDeserialize() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer(); + + writer.WriteStructHeader(3, ElementRelationshipSerializer.Relationship); + writer.Write(1); + writer.Write(2); + writer.Write(3); + writer.Write("RELATES_TO"); + writer.Write(new Dictionary + { + {"prop3", true} + }); + writer.Write("r1"); + writer.Write("n1"); + writer.Write("n2"); + + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var value = readerMachine.Reader().Read(); + + value.Should().NotBeNull(); + value.Should().BeOfType().Which.Id.Should().Be(1L); + value.Should().BeOfType().Which.StartNodeId.Should().Be(2L); + value.Should().BeOfType().Which.EndNodeId.Should().Be(3L); + value.Should().BeOfType().Which.Type.Should().Be("RELATES_TO"); + value.Should().BeOfType().Which.Properties.Should() + .HaveCount(1).And.Contain(new[] + { + new KeyValuePair("prop3", true), + }); + value.Should().BeOfType().Which.ElementId.Should().Be("r1"); + value.Should().BeOfType().Which.StartNodeElementId.Should().Be("n1"); + value.Should().BeOfType().Which.EndNodeElementId.Should().Be("n2"); + } + + [Fact] + public void ShouldDeserializeWithNulls() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer(); + + writer.WriteStructHeader(3, ElementRelationshipSerializer.Relationship); + writer.WriteNull(); + writer.WriteNull(); + writer.WriteNull(); + writer.Write("RELATES_TO"); + writer.Write(new Dictionary + { + {"prop3", true} + }); + writer.Write("r1"); + writer.Write("n1"); + writer.Write("n2"); + + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var value = readerMachine.Reader().Read(); + + value.Should().NotBeNull(); + value.Should().BeOfType().Which.Id.Should().Be(-1L); + value.Should().BeOfType().Which.StartNodeId.Should().Be(-1L); + value.Should().BeOfType().Which.EndNodeId.Should().Be(-1L); + value.Should().BeOfType().Which.Type.Should().Be("RELATES_TO"); + value.Should().BeOfType().Which.Properties.Should() + .HaveCount(1).And.Contain(new[] + { + new KeyValuePair("prop3", true), + }); + value.Should().BeOfType().Which.ElementId.Should().Be("r1"); + value.Should().BeOfType().Which.StartNodeElementId.Should().Be("n1"); + value.Should().BeOfType().Which.EndNodeElementId.Should().Be("n2"); + } + } +} \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementUnboundRelationshipSerializerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementUnboundRelationshipSerializerTests.cs new file mode 100644 index 000000000..c11d5174d --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/ElementUnboundRelationshipSerializerTests.cs @@ -0,0 +1,97 @@ +// Copyright (c) 2002-2022 "Neo4j," +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using FluentAssertions; +using Neo4j.Driver.Internal.Types; +using Xunit; + +namespace Neo4j.Driver.Internal.IO.ValueSerializers +{ + public class ElementUnboundRelationshipSerializerTests : PackStreamSerializerTests + { + internal override IPackStreamSerializer SerializerUnderTest => new ElementUnboundRelationshipSerializer(); + + [Fact] + public void ShouldDeserialize() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer(); + + writer.WriteStructHeader(3, UnboundRelationshipSerializer.UnboundRelationship); + writer.Write(1); + writer.Write("RELATES_TO"); + writer.Write(new Dictionary + { + {"prop3", true} + }); + writer.Write("r1"); + + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var value = readerMachine.Reader().Read(); + + value.Should().NotBeNull(); + value.Should().BeOfType().Which.Id.Should().Be(1L); + value.Should().BeOfType().Which.StartNodeId.Should().Be(-1L); + value.Should().BeOfType().Which.EndNodeId.Should().Be(-1L); + value.Should().BeOfType().Which.Type.Should().Be("RELATES_TO"); + value.Should().BeOfType().Which.Properties.Should() + .HaveCount(1).And.Contain(new[] + { + new KeyValuePair("prop3", true), + }); + value.Should().BeOfType().Which.ElementId.Should().Be("r1"); + value.Should().BeOfType().Which.StartNodeElementId.Should().Be("-1"); + value.Should().BeOfType().Which.EndNodeElementId.Should().Be("-1"); + } + + [Fact] + public void ShouldDeserializeWithNulls() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer(); + + writer.WriteStructHeader(3, UnboundRelationshipSerializer.UnboundRelationship); + writer.WriteNull(); + writer.Write("RELATES_TO"); + writer.Write(new Dictionary + { + {"prop3", true} + }); + writer.Write("r1"); + + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var value = readerMachine.Reader().Read(); + + value.Should().NotBeNull(); + value.Should().BeOfType().Which.Id.Should().Be(-1L); + value.Should().BeOfType().Which.StartNodeId.Should().Be(-1L); + value.Should().BeOfType().Which.EndNodeId.Should().Be(-1L); + value.Should().BeOfType().Which.Type.Should().Be("RELATES_TO"); + value.Should().BeOfType().Which.Properties.Should() + .HaveCount(1).And.Contain(new[] + { + new KeyValuePair("prop3", true), + }); + value.Should().BeOfType().Which.ElementId.Should().Be("r1"); + value.Should().BeOfType().Which.StartNodeElementId.Should().Be("-1"); + value.Should().BeOfType().Which.EndNodeElementId.Should().Be("-1"); + } + } +} \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/NodeTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/NodeTests.cs new file mode 100644 index 000000000..f002b71e6 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/NodeTests.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2002-2022 "Neo4j," +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using FluentAssertions; +using Moq; +using Neo4j.Driver.Internal.Types; +using Xunit; + +namespace Neo4j.Driver.Tests +{ + public class NodeTests + { + [Fact] + public void ShouldEqualIfIdEquals() + { + var node1 = new Node(123, new[] { "buibui" }, null); + var node2 = new Node(123, new[] { "lala" }, null); + + node1.Equals(node2).Should().BeTrue(); + Equals(node1, node2).Should().BeTrue(); + + Dictionary nodes = new Dictionary(); + nodes.Add(node1, 123); + + nodes.TryGetValue(node2, out var value).Should().BeTrue(); + value.Should().Be(123); + + var node3Mock = new Mock(); + node3Mock.Setup(f => f.Id).Returns(123); + node3Mock.Setup(f => f.ElementId).Returns("123"); + node3Mock.Setup(f => f.Labels) + .Returns(new[] + { + "same interface, different implementation" + }); + var node3 = node3Mock.Object; + + node1.Equals(node3).Should().BeTrue(); + Equals(node1, node3).Should().BeTrue(); + } + } +} \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/PathTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/PathTests.cs new file mode 100644 index 000000000..83c0e0494 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/PathTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using FluentAssertions; +using Moq; +using Neo4j.Driver.Internal.Types; +using Xunit; + +namespace Neo4j.Driver.Tests +{ + public class PathTests + { + [Fact] + public void ShouldEqualIfIdEquals() + { + var path1 = new Path(null, + new[] { new Node(123, new[] { "buibui" }, null) }, + new[] { new Relationship(1, 000, 111, "buibui", null) }); + var path2 = new Path(null, + new[] { new Node(123, new[] { "lala" }, null) }, + new[] { new Relationship(1, 222, 333, "lala", null) }); + path1.Equals(path2).Should().BeTrue(); + Equals(path1, path2).Should().BeTrue(); + path1.GetHashCode().Should().Be(path2.GetHashCode()); + + var path3Mock = new Mock(); + path3Mock.Setup(f => f.Start).Returns(new Node(123, new[] { "same interface, different implementation" }, null)); + path3Mock.Setup(f => f.Relationships).Returns(new[] { new Relationship(1, 222, 333, "same interface --- different implementation", null) }); + var path3 = path3Mock.Object; + path1.Equals(path3).Should().BeTrue(); + Equals(path1, path3).Should().BeTrue(); + } + } +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/RelationshipTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/RelationshipTests.cs new file mode 100644 index 000000000..aaf59a4ac --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/RelationshipTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2002-2022 "Neo4j," +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using FluentAssertions; +using Moq; +using Neo4j.Driver.Internal.Types; +using Xunit; + +namespace Neo4j.Driver.Tests +{ + public class RelationshipTests + { + [Fact] + public void ShouldEqualIfIdEquals() + { + var rel1 = new Relationship(123, 000, 111, "buibui", null); + var rel2 = new Relationship(123, 222, 333, "lala", null); + rel1.Equals(rel2).Should().BeTrue(); + Equals(rel1, rel2).Should().BeTrue(); + rel1.GetHashCode().Should().Be(rel2.GetHashCode()); + + var rel3Mock = new Mock(); + rel3Mock.Setup(f => f.ElementId).Returns("123"); + rel3Mock.Setup(f => f.StartNodeElementId).Returns("444"); + rel3Mock.Setup(f => f.EndNodeElementId).Returns("555"); + rel3Mock.Setup(f => f.Type).Returns("same interface, different implementation"); + rel3Mock.Setup(f => f.GetHashCode()).Returns(123); + var rel3 = rel3Mock.Object; + + rel1.Equals(rel3).Should().BeTrue(); + Equals(rel1, rel3).Should().BeTrue(); + // TODO: The following test is currently not supported by Moq + //rel1.GetHashCode().Should().Be(rel3.GetHashCode()); + } + } +} \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Connector/SocketClient.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Connector/SocketClient.cs index e198042bd..ba6a1fee2 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Connector/SocketClient.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Connector/SocketClient.cs @@ -31,7 +31,6 @@ namespace Neo4j.Driver.Internal.Connector { internal class SocketClient : ISocketClient { - private const int NumSupportedVersions = 4; private const string MessagePattern = "C: {0}"; private readonly Uri _uri; private readonly BufferSettings _bufferSettings; @@ -150,7 +149,7 @@ public Task StopAsync() private async Task DoHandshakeAsync(CancellationToken cancellationToken = default) { - var data = BoltProtocolFactory.PackSupportedVersions(NumSupportedVersions); + var data = BoltProtocolFactory.PackSupportedVersions(); await _tcpSocketClient.WriteStream.WriteAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false); await _tcpSocketClient.WriteStream.FlushAsync(cancellationToken).ConfigureAwait(false); _logger?.Debug("C: [HANDSHAKE] {0}", data.ToHexString()); diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/PackStreamReader.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/PackStreamReader.cs index 845de6234..4dba2c689 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/IO/PackStreamReader.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/PackStreamReader.cs @@ -17,9 +17,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; -using Neo4j.Driver; -using Neo4j.Driver.Internal; namespace Neo4j.Driver.Internal.IO { diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ReadOnlySerializer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ReadOnlySerializer.cs index fdc2a8a77..830d7171f 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ReadOnlySerializer.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ReadOnlySerializer.cs @@ -18,7 +18,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Neo4j.Driver; namespace Neo4j.Driver.Internal.IO { @@ -35,5 +34,11 @@ public void Serialize(IPackStreamWriter writer, object value) public abstract IEnumerable ReadableStructs { get; } public abstract object Deserialize(IPackStreamReader reader, byte signature, long size); + + protected static T? ReadNullAndReturnNull(IPackStreamReader reader) where T : struct + { + reader.ReadNull(); + return null; + } } } \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementNodeSerializer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementNodeSerializer.cs new file mode 100644 index 000000000..668f47d4c --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementNodeSerializer.cs @@ -0,0 +1,56 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using Neo4j.Driver.Internal.Types; + +namespace Neo4j.Driver.Internal.IO.ValueSerializers +{ + internal class ElementNodeSerializer : ReadOnlySerializer + { + public const byte Node = (byte)'N'; + public override IEnumerable ReadableStructs => new[] {Node}; + + public override object Deserialize(IPackStreamReader reader, byte signature, long size) + { + var includingLongs = reader.PeekNextType() != PackStream.PackType.Null; + + var nodeId = includingLongs ? reader.ReadLong() : ReadNullAndReturnNull(reader); + + var numLabels = (int) reader.ReadListHeader(); + var labels = new List(numLabels); + for (var i = 0; i < numLabels; i++) + { + labels.Add(reader.ReadString()); + } + + var numProps = (int) reader.ReadMapHeader(); + var props = new Dictionary(numProps); + for (var j = 0; j < numProps; j++) + { + var key = reader.ReadString(); + props.Add(key, reader.Read()); + } + + var stringId = reader.ReadString(); + + return includingLongs + ? new Node(nodeId.Value, stringId, labels, props) + : new Node(stringId, labels, props); + } + } +} \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementRelationshipSerializer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementRelationshipSerializer.cs new file mode 100644 index 000000000..6b7b81d3e --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementRelationshipSerializer.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2002-2022 "Neo4j," +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using Neo4j.Driver.Internal.Types; + +namespace Neo4j.Driver.Internal.IO.ValueSerializers +{ + internal class ElementRelationshipSerializer : ReadOnlySerializer + { + public const byte Relationship = (byte)'R'; + public override IEnumerable ReadableStructs => new[] { Relationship }; + + public override object Deserialize(IPackStreamReader reader, byte signature, long size) + { + var includingLongs = reader.PeekNextType() != PackStream.PackType.Null; + var relId = includingLongs ? reader.ReadLong() : ReadNullAndReturnNull(reader); + var relStartId = includingLongs ? reader.ReadLong() : ReadNullAndReturnNull(reader); + var relEndId = includingLongs ? reader.ReadLong() : ReadNullAndReturnNull(reader); + + var relType = reader.ReadString(); + var props = reader.ReadMap(); + + var urn = reader.ReadString(); + var startUrn = reader.ReadString(); + var endUrn = reader.ReadString(); + + return includingLongs + ? new Relationship(relId.Value, urn, relStartId.Value, relEndId.Value, startUrn, endUrn, relType, props) + : new Relationship(urn, startUrn, endUrn, relType, props); + } + } +} \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementUnboundRelationshipSerializer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementUnboundRelationshipSerializer.cs new file mode 100644 index 000000000..1799190f5 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/ElementUnboundRelationshipSerializer.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2002-2022 "Neo4j," +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using Neo4j.Driver.Internal.Types; + +namespace Neo4j.Driver.Internal.IO.ValueSerializers +{ + internal class ElementUnboundRelationshipSerializer : ReadOnlySerializer + { + public const byte UnboundRelationship = (byte)'r'; + public override IEnumerable ReadableStructs => new[] { UnboundRelationship }; + + public override object Deserialize(IPackStreamReader reader, byte signature, long size) + { + var includingLongs = reader.PeekNextType() != PackStream.PackType.Null; + + var relId = includingLongs ? reader.ReadLong() : ReadNullAndReturnNull(reader); + + var relType = reader.ReadString(); + var props = reader.ReadMap(); + + var urn = reader.ReadString(); + + return includingLongs + ? new Relationship(relId.Value, urn, -1, -1, "-1", "-1", relType, props) + : new Relationship(urn, "-1", "-1", relType, props); + } + } +} \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/PathSerializer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/PathSerializer.cs index cf706febe..12ee52c80 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/PathSerializer.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/PathSerializer.cs @@ -72,12 +72,12 @@ public override object Deserialize(IPackStreamReader reader, byte signature, lon if (relIdx < 0) { rel = uniqRels[(-relIdx) - 1]; // -1 because rel idx are 1-indexed - rel.SetStartAndEnd(nextNode.Id, prevNode.Id); + rel.SetStartAndEnd(nextNode, prevNode); } else { rel = uniqRels[relIdx - 1]; - rel.SetStartAndEnd(prevNode.Id, nextNode.Id); + rel.SetStartAndEnd(prevNode, nextNode); } nodes[i + 1] = nextNode; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolFactory.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolFactory.cs index a135102eb..d06609b03 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolFactory.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolFactory.cs @@ -17,73 +17,57 @@ using System; using System.Collections.Generic; -using Neo4j.Driver.Internal.IO; using System.Linq; +using System.Threading; +using Neo4j.Driver.Internal.IO; namespace Neo4j.Driver.Internal.Protocol { internal static class BoltProtocolFactory { - //This is a 'magic' handshake identifier to indicate we're using 'BOLT' ('GOGOBOLT') - private const int BoltIdentifier = 0x6060B017; - private const int BoltHTTPIdentifier = 1213486160; //0x‭48 54 54 50 - or HTTP ascii codes... + private const int BoltHttpIdentifier = 1213486160; //0x‭48 54 54 50 - or HTTP ascii codes... + static readonly string HttpErrorMessage = "Server responded HTTP. Make sure you are not trying to connect to the http endpoint " + + $"(HTTP defaults to port 7474 whereas BOLT defaults to port {GraphDatabase.DefaultBoltPort})"; + private static readonly string NoAgreedVersion = + "The Neo4j server does not support any of the protocol versions supported by this client. " + + "Ensure that you are using driver and server versions that are compatible with one another."; - private static readonly int[] SupportedVersions = - { - BoltProtocolVersion.V4_4.PackToIntRange(BoltProtocolVersion.V4_2), - BoltProtocolVersion.V4_1.PackToInt(), - BoltProtocolVersion.V4_0.PackToInt(), - BoltProtocolVersion.V3_0.PackToInt() - }; + private static readonly Lazy HandshakeBytesLazy = + new Lazy(() => + { + const int goGoBolt = 0x6060B017; + var versions = new int[] + { + //This is a 'magic' handshake identifier to indicate we're using 'BOLT' ('GOGOBOLT') + goGoBolt, + // 4 versions max. + BoltProtocolVersion.V5_0.PackToInt(), + BoltProtocolVersion.V4_4.PackToIntRange(BoltProtocolVersion.V4_2), + BoltProtocolVersion.V4_1.PackToInt(), + BoltProtocolVersion.V3_0.PackToInt() + }; + return versions.SelectMany(PackStreamBitConverter.GetBytes).ToArray(); + }, LazyThreadSafetyMode.PublicationOnly); public static IBoltProtocol ForVersion(BoltProtocolVersion version, IDictionary routingContext = null) { - if (version.Equals(3, 0)) - { - return new BoltProtocolV3(); - } - else if (version.Equals(4, 0)) - { - return new BoltProtocolV4_0(); - } - else if (version.Equals(4, 1) ) - { - return new BoltProtocolV4_1(routingContext); - } - else if (version.Equals(4, 2)) - { - return new BoltProtocolV4_2(routingContext); - } - else if (version.Equals(4, 3)) - { - return new BoltProtocolV4_3(routingContext); - } - else if (version.Equals(4, 4)) - { - return new BoltProtocolV4_4(routingContext); - } - else if (version.Equals(5, 0)) - { - return new BoltProtocolV5_0(routingContext); - } - else if(version.Equals(0, 0)) - { - throw new NotSupportedException( - "The Neo4j server does not support any of the protocol versions supported by this client. " + - "Ensure that you are using driver and server versions that are compatible with one another."); - } - else if (version == new BoltProtocolVersion(BoltHTTPIdentifier)) + return version switch { - throw new NotSupportedException( - "Server responded HTTP. Make sure you are not trying to connect to the http endpoint " + - $"(HTTP defaults to port 7474 whereas BOLT defaults to port {GraphDatabase.DefaultBoltPort})"); - } - else - { - throw new NotSupportedException( - "Protocol error, server suggested unexpected protocol version: " + version.MajorVersion + "." + version.MinorVersion); - } + {MajorVersion: 3, MinorVersion: 0} => new BoltProtocolV3(), + {MajorVersion: 4, MinorVersion: 0} => new BoltProtocolV4_0(), + {MajorVersion: 4, MinorVersion: 1} => new BoltProtocolV4_1(routingContext), + {MajorVersion: 4, MinorVersion: 2} => new BoltProtocolV4_2(routingContext), + {MajorVersion: 4, MinorVersion: 3} => new BoltProtocolV4_3(routingContext), + {MajorVersion: 4, MinorVersion: 4} => new BoltProtocolV4_4(routingContext), + {MajorVersion: 5, MinorVersion: 0} => new BoltProtocolV5_0(routingContext), + // no matching versions + {MajorVersion: 0, MinorVersion: 0} => throw new NotSupportedException(NoAgreedVersion), + // http response + _ when version == new BoltProtocolVersion(BoltHttpIdentifier) => throw new NotSupportedException(HttpErrorMessage), + // undefined + _ => throw new NotSupportedException($"Protocol error, server suggested unexpected protocol version: {version}") + }; } public static BoltProtocolVersion UnpackAgreedVersion(byte[] data) @@ -91,22 +75,6 @@ public static BoltProtocolVersion UnpackAgreedVersion(byte[] data) return BoltProtocolVersion.FromPackedInt(PackStreamBitConverter.ToInt32(data)); } - public static byte[] PackSupportedVersions(int numVersionsToPack) - { - return PackVersions(SupportedVersions.Take(numVersionsToPack)); - } - - private static byte[] PackVersions(IEnumerable versions) - { - var aLittleBitOfMagic = PackStreamBitConverter.GetBytes(BoltIdentifier); - - var bytes = new List(aLittleBitOfMagic); - foreach (var version in versions) - { - bytes.AddRange(PackStreamBitConverter.GetBytes(version)); - } - - return bytes.ToArray(); - } + public static byte[] PackSupportedVersions() => HandshakeBytesLazy.Value; } } \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolMessageFormat.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolMessageFormat.cs index ddfb31565..b70cb6945 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolMessageFormat.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolMessageFormat.cs @@ -32,5 +32,7 @@ internal static class BoltProtocolMessageFormat public static readonly IMessageFormat V4_3 = new BoltProtocolV4_3MessageFormat(); public static readonly IMessageFormat V4_4 = new BoltProtocolV4_4MessageFormat(); + + public static readonly IMessageFormat V5_0 = new BoltProtocolV5_0MessageFormat(); } } \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolV5_0.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolV5_0.cs index f45483483..9c04c8fb2 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolV5_0.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolV5_0.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using Neo4j.Driver.Internal.Connector; +using Neo4j.Driver.Internal.IO; using Neo4j.Driver.Internal.MessageHandling; using Neo4j.Driver.Internal.MessageHandling.V5_0; @@ -25,6 +26,7 @@ namespace Neo4j.Driver.Internal.Protocol internal class BoltProtocolV5_0 : BoltProtocolV4_4 { public override BoltProtocolVersion Version => BoltProtocolVersion.V5_0; + protected override IMessageFormat MessageFormat => BoltProtocolMessageFormat.V5_0; public BoltProtocolV5_0(IDictionary routingContext) : base(routingContext) { diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolV5_0MessageFormat.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolV5_0MessageFormat.cs new file mode 100644 index 000000000..ec5cafcb9 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolV5_0MessageFormat.cs @@ -0,0 +1,36 @@ +// Copyright (c) 2002-2022 "Neo4j," +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neo4j.Driver.Internal.IO.ValueSerializers; + +namespace Neo4j.Driver.Internal.Protocol +{ + class BoltProtocolV5_0MessageFormat : BoltProtocolV4_4MessageFormat + { + public BoltProtocolV5_0MessageFormat() + { + RemoveHandler(); + AddHandler(); + + RemoveHandler(); + AddHandler(); + + RemoveHandler(); + AddHandler(); + } + } +} \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolVersion.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolVersion.cs index 77126b4dc..0c9031464 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolVersion.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolVersion.cs @@ -112,8 +112,6 @@ public byte PackToByte() { return (byte)((MinorVersion << 4) | MajorVersion); } - - public override bool Equals(object obj) { @@ -230,5 +228,4 @@ public override string ToString() return $"{MajorVersion}.{MinorVersion}"; } } - } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Types/Node.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Types/Node.cs index 9d99f6e6c..1474805af 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Types/Node.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Types/Node.cs @@ -14,34 +14,54 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Neo4j.Driver; namespace Neo4j.Driver.Internal.Types { internal class Node : INode { - public long Id { get; } + [Obsolete("Replaced with ElementId, Will be removed in 6.0")] + public long Id { get; } = -1; + + public string ElementId { get; } public IReadOnlyList Labels { get; } public IReadOnlyDictionary Properties { get; } public object this[string key] => Properties[key]; - public Node(long id, IReadOnlyList lables, IReadOnlyDictionary prop) + public Node(long id, IReadOnlyList labels, IReadOnlyDictionary prop) + { + Id = id; + ElementId = id.ToString(); + Labels = labels; + Properties = prop; + } + + public Node(string elementId, IReadOnlyList labels, IReadOnlyDictionary prop) + { + ElementId = elementId; + Labels = labels; + Properties = prop; + } + + public Node(long id, string elementId, IReadOnlyList labels, IReadOnlyDictionary prop) { Id = id; - Labels = lables; + ElementId = elementId; + Labels = labels; Properties = prop; } public bool Equals(INode other) { - if (other == null) return false; - if (ReferenceEquals(this, other)) return true; - return Equals(Id, other.Id); + if (other == null) + return false; + + if (ReferenceEquals(this, other)) + return true; + + return Equals(ElementId, other.ElementId); } public override bool Equals(object obj) @@ -51,7 +71,7 @@ public override bool Equals(object obj) public override int GetHashCode() { - return Id.GetHashCode(); + return ElementId.GetHashCode(); } } } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Types/Relationship.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Types/Relationship.cs index a103b6773..8ff087fc8 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Types/Relationship.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Types/Relationship.cs @@ -14,39 +14,80 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Neo4j.Driver; namespace Neo4j.Driver.Internal.Types { internal class Relationship : IRelationship { - public long Id { get; } + [Obsolete("Replaced by ElementId, Will be removed in 6.0")] + public long Id { get; } = -1; + [Obsolete("Replaced by StartNodeElementId, Will be removed in 6.0")] + public long StartNodeId { get; internal set; } = -1; + [Obsolete("Replaced by EndNodeElementId, Will be removed in 6.0")] + public long EndNodeId { get; internal set; } = -1; + public string Type { get; } - public long StartNodeId { get; internal set; } - public long EndNodeId { get; internal set; } + + public string ElementId { get; } + public string StartNodeElementId { get; internal set; } + public string EndNodeElementId { get; internal set; } public IReadOnlyDictionary Properties { get; } public object this[string key] => Properties[key]; - + public Relationship(long id, long startId, long endId, string relType, IReadOnlyDictionary props) { Id = id; StartNodeId = startId; EndNodeId = endId; + + ElementId = id.ToString(); + StartNodeElementId = startId.ToString(); + EndNodeElementId = endId.ToString(); + Type = relType; + Properties = props; + } + + public Relationship(string id, string startId, string endId, string relType, + IReadOnlyDictionary props) + { + ElementId = id; + StartNodeElementId = startId; + EndNodeElementId = endId; + Type = relType; Properties = props; } + public Relationship(long id, string elementId, long startId, long endId, string startElementId, string endElementId, + string relType, + IReadOnlyDictionary props) + { + Id = id; + StartNodeId = startId; + EndNodeId = endId; + + ElementId = elementId; + StartNodeElementId = startElementId; + EndNodeElementId = endElementId; + + Type = relType; + Properties = props; + } + + public bool Equals(IRelationship other) { - if (other == null) return false; - if (ReferenceEquals(this, other)) return true; - return Equals(Id, other.Id); + if (other == null) + return false; + + if (ReferenceEquals(this, other)) + return true; + + return Equals(ElementId, other.ElementId); } public override bool Equals(object obj) @@ -56,14 +97,15 @@ public override bool Equals(object obj) public override int GetHashCode() { - return Id.GetHashCode(); + return ElementId.GetHashCode(); } - internal void SetStartAndEnd(long start, long end) + internal void SetStartAndEnd(INode start, INode end) { - StartNodeId = start; - EndNodeId = end; + StartNodeId = start.Id; + EndNodeId = end.Id; + StartNodeElementId = start.ElementId; + EndNodeElementId = end.ElementId; } } - } diff --git a/Neo4j.Driver/Neo4j.Driver/Types/IEntity.cs b/Neo4j.Driver/Neo4j.Driver/Types/IEntity.cs index b99b8b1a7..7081cfe41 100644 --- a/Neo4j.Driver/Neo4j.Driver/Types/IEntity.cs +++ b/Neo4j.Driver/Neo4j.Driver/Types/IEntity.cs @@ -40,6 +40,12 @@ public interface IEntity /// /// Get the identity as a number. /// + [Obsolete("Replaced with ElementId, will be removed in 6.0")] long Id { get; } + + /// + /// Get the identity as a . + /// + string ElementId { get; } } } \ No newline at end of file diff --git a/Neo4j.Driver/Neo4j.Driver/Types/IRelationship.cs b/Neo4j.Driver/Neo4j.Driver/Types/IRelationship.cs index 9756a0776..501af5f66 100644 --- a/Neo4j.Driver/Neo4j.Driver/Types/IRelationship.cs +++ b/Neo4j.Driver/Neo4j.Driver/Types/IRelationship.cs @@ -33,11 +33,23 @@ public interface IRelationship : IEntity, IEquatable /// /// Gets the id of the start node of the relationship. /// + [Obsolete("Replaced with StartNodeElementId, will be removed in 6.0")] long StartNodeId { get; } /// /// Gets the id of the end node of the relationship. /// + [Obsolete("Replaced with EndNodeElementId, will be removed in 6.0")] long EndNodeId { get; } + + /// + /// Gets the ElementId of the start node of the relationship. + /// + string StartNodeElementId { get; } + + /// + /// Gets the ElementId of the end node of the relationship. + /// + string EndNodeElementId { get; } } } \ No newline at end of file