Repository: neo4j/neo4j-python-driver

Issue:
Crash when using spatial type as parameters
Hi folks!

I can't seem to get the spatial type working, just getting a crash (stack trace attached).

- Neo4j version: Enterprise 4.2.1
- Neo4j Mode: Single instance
- Python 3.9
- Driver version:  driver 4.2.1
- Operating system: Ubuntu 20.04

- **Steps to reproduce**: run the following piece of code:

```
import neo4j
print(neo4j.__version__)

from neo4j.spatial import WGS84Point
from neo4j import GraphDatabase, basic_auth
driver = GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("neo4j", ""), encrypted=False)
point = WGS84Point(dict(x=2, y=48))
cypher = "WITH $point as point RETURN [point.x, point.y]"
params = {"point": point}
with driver.session() as session:
    session.run(cypher, params)
```

- Expected behavior : query returns [2, 48] (or a meaningful error message in case of misuse)
- Actual behavior: `neo4j.exceptions.ServiceUnavailable: Failed to read from defunct connection IPv4Address(('localhost', 7687)) (IPv4Address(('127.0.0.1', 7687)))`

Full stack trace is attached.
[neo_spatial.log](https://github.com/neo4j/neo4j-python-driver/files/5968103/neo_spatial.log)

Also, only error in server logs is:

```
2021-02-11 18:00:06.353+0000 ERROR Client triggered an unexpected error [Neo.DatabaseError.General.UnknownError]: Unknown statement ID: -1. Existing IDs: [], reference 1c40d17c-1c03-4b40-b422-ceeef7fc49ec.
```

- Additional information:
    -  Same error with CartesianPoint  
    - Doing similar things with DateTime works well (and basically, anything else is working just fine :) )
    - I havn't found an example using spatial types in the doc, but the example is taken from https://github.com/neo4j/neo4j-python-driver/blob/4.3/tests/integration/test_spatial_types.py (I think)
    - Replacing the query with a `MATCH (n:Node) RETURN n.point` returns a `neo4j.spatial.WGS84Point` as expected
    - Same error as https://github.com/neo4j/neo4j-python-driver/issues/484 but, not sure the root cause is the same though.


Discussion:

Patch:
diff --git a/neo4j/spatial/__init__.py b/neo4j/spatial/__init__.py
index f7085fd4..36e8d66f 100644
--- a/neo4j/spatial/__init__.py
+++ b/neo4j/spatial/__init__.py
@@ -53,7 +53,7 @@ class Point(tuple):
     srid = None
 
     def __new__(cls, iterable):
-        return tuple.__new__(cls, iterable)
+        return tuple.__new__(cls, map(float, iterable))
 
     def __repr__(self):
         return "POINT(%s)" % " ".join(map(str, self))


Test patch:
diff --git a/tests/unit/spatial/__init__.py b/tests/unit/spatial/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/unit/spatial/test_cartesian_point.py b/tests/unit/spatial/test_cartesian_point.py
new file mode 100644
index 00000000..ee86e5b9
--- /dev/null
+++ b/tests/unit/spatial/test_cartesian_point.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# 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.
+
+import io
+import struct
+from unittest import TestCase
+
+from neo4j.data import DataDehydrator
+from neo4j.packstream import Packer
+from neo4j.spatial import CartesianPoint
+
+
+class CartesianPointTestCase(TestCase):
+
+    def test_alias(self):
+        x, y, z = 3.2, 4.0, -1.2
+        p = CartesianPoint((x, y, z))
+        self.assert_(hasattr(p, "x"))
+        self.assertEqual(p.x, x)
+        self.assert_(hasattr(p, "y"))
+        self.assertEqual(p.y, y)
+        self.assert_(hasattr(p, "z"))
+        self.assertEqual(p.z, z)
+
+    def test_dehydration_3d(self):
+        coordinates = (1, -2, 3.1)
+        p = CartesianPoint(coordinates)
+
+        dehydrator = DataDehydrator()
+        buffer = io.BytesIO()
+        packer = Packer(buffer)
+        packer.pack(dehydrator.dehydrate((p,))[0])
+        self.assertEqual(
+            buffer.getvalue(),
+            b"\xB4Y" +
+            b"\xC9" + struct.pack(">h", 9157) +
+            b"".join(map(lambda c: b"\xC1" + struct.pack(">d", c), coordinates))
+        )
+
+    def test_dehydration_2d(self):
+        coordinates = (.1, 0)
+        p = CartesianPoint(coordinates)
+
+        dehydrator = DataDehydrator()
+        buffer = io.BytesIO()
+        packer = Packer(buffer)
+        packer.pack(dehydrator.dehydrate((p,))[0])
+        self.assertEqual(
+            buffer.getvalue(),
+            b"\xB3X" +
+            b"\xC9" + struct.pack(">h", 7203) +
+            b"".join(map(lambda c: b"\xC1" + struct.pack(">d", c), coordinates))
+        )
diff --git a/tests/unit/spatial/test_point.py b/tests/unit/spatial/test_point.py
new file mode 100644
index 00000000..082f95c5
--- /dev/null
+++ b/tests/unit/spatial/test_point.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# 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.
+
+import io
+import struct
+from unittest import TestCase
+
+from neo4j.data import DataDehydrator
+from neo4j.packstream import Packer
+from neo4j.spatial import (
+    Point,
+    point_type,
+)
+
+
+class PointTestCase(TestCase):
+
+    def test_wrong_type_arguments(self):
+        for argument in (("a", "b"), ({"x": 1.0, "y": 2.0})):
+            with self.subTest():
+                with self.assertRaises(ValueError):
+                    Point(argument)
+
+    def test_number_arguments(self):
+        for argument in ((1, 2), (1.2, 2.1)):
+            with self.subTest():
+                p = Point(argument)
+                assert tuple(p) == argument
+
+    def test_dehydration(self):
+        MyPoint = point_type("MyPoint", ["x", "y"], {2: 1234})
+        coordinates = (.1, 0)
+        p = MyPoint(coordinates)
+
+        dehydrator = DataDehydrator()
+        buffer = io.BytesIO()
+        packer = Packer(buffer)
+        packer.pack(dehydrator.dehydrate((p,))[0])
+        self.assertEqual(
+            buffer.getvalue(),
+            b"\xB3X" +
+            b"\xC9" + struct.pack(">h", 1234) +
+            b"".join(map(lambda c: b"\xC1" + struct.pack(">d", c), coordinates))
+        )
+
+    def test_immutable_coordinates(self):
+        MyPoint = point_type("MyPoint", ["x", "y"], {2: 1234})
+        coordinates = (.1, 0)
+        p = MyPoint(coordinates)
+        with self.assertRaises(AttributeError):
+            p.x = 2.0
+        with self.assertRaises(AttributeError):
+            p.y = 2.0
+        with self.assertRaises(TypeError):
+            p[0] = 2.0
+        with self.assertRaises(TypeError):
+            p[1] = 2.0
diff --git a/tests/unit/spatial/test_wgs84_point.py b/tests/unit/spatial/test_wgs84_point.py
new file mode 100644
index 00000000..8f725a58
--- /dev/null
+++ b/tests/unit/spatial/test_wgs84_point.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# 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.
+
+import io
+import struct
+from unittest import TestCase
+
+from neo4j.data import DataDehydrator
+from neo4j.packstream import Packer
+from neo4j.spatial import WGS84Point
+
+
+class WGS84PointTestCase(TestCase):
+
+    def test_alias(self):
+        x, y, z = 3.2, 4.0, -1.2
+        p = WGS84Point((x, y, z))
+        self.assert_(hasattr(p, "longitude"))
+        self.assertEqual(p.longitude, x)
+        self.assert_(hasattr(p, "latitude"))
+        self.assertEqual(p.latitude, y)
+        self.assert_(hasattr(p, "height"))
+        self.assertEqual(p.height, z)
+
+    def test_dehydration_3d(self):
+        coordinates = (1, -2, 3.1)
+        p = WGS84Point(coordinates)
+
+        dehydrator = DataDehydrator()
+        buffer = io.BytesIO()
+        packer = Packer(buffer)
+        packer.pack(dehydrator.dehydrate((p,))[0])
+        self.assertEqual(
+            buffer.getvalue(),
+            b"\xB4Y" +
+            b"\xC9" + struct.pack(">h", 4979) +
+            b"".join(map(lambda c: b"\xC1" + struct.pack(">d", c), coordinates))
+        )
+
+    def test_dehydration_2d(self):
+        coordinates = (.1, 0)
+        p = WGS84Point(coordinates)
+
+        dehydrator = DataDehydrator()
+        buffer = io.BytesIO()
+        packer = Packer(buffer)
+        packer.pack(dehydrator.dehydrate((p,))[0])
+        self.assertEqual(
+            buffer.getvalue(),
+            b"\xB3X" +
+            b"\xC9" + struct.pack(">h", 4326) +
+            b"".join(map(lambda c: b"\xC1" + struct.pack(">d", c), coordinates))
+        )