diff --git a/.gitignore b/.gitignore index 683a57a..c2aa9b0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ pymgclient.egg-info/ .venv *.pyc *.pyo -__pycache__/ \ No newline at end of file +__pycache__/ +.pytest_cache/ diff --git a/mgclient b/mgclient index 6f59c8a..df0aeb3 160000 --- a/mgclient +++ b/mgclient @@ -1 +1 @@ -Subproject commit 6f59c8a4216d20e42e0943cf506d0ddb6241c29b +Subproject commit df0aeb3439813eb08d541c3855b312a90f54cb25 diff --git a/src/glue.c b/src/glue.c index 40999f8..166fc61 100644 --- a/src/glue.c +++ b/src/glue.c @@ -342,6 +342,20 @@ PyObject *mg_duration_to_py_delta(const mg_duration *dur) { return make_py_delta(days, seconds, (nanoseconds / 1000)); } +PyObject *mg_point_2d_to_py_point2d(const mg_point_2d *point2d) { + PyObject *ret = PyObject_CallFunction( + (PyObject *)&Point2DType, "Hdd", mg_point_2d_srid(point2d), + mg_point_2d_x(point2d), mg_point_2d_y(point2d)); + return ret; +} + +PyObject *mg_point_3d_to_py_point3d(const mg_point_3d *point3d) { + PyObject *ret = PyObject_CallFunction( + (PyObject *)&Point3DType, "Hddd", mg_point_3d_srid(point3d), + mg_point_3d_x(point3d), mg_point_3d_y(point3d), mg_point_3d_z(point3d)); + return ret; +} + PyObject *mg_value_to_py_object(const mg_value *value) { switch (mg_value_get_type(value)) { case MG_VALUE_TYPE_NULL: @@ -379,6 +393,10 @@ PyObject *mg_value_to_py_object(const mg_value *value) { return mg_local_date_time_to_py_datetime(mg_value_local_date_time(value)); case MG_VALUE_TYPE_DURATION: return mg_duration_to_py_delta(mg_value_duration(value)); + case MG_VALUE_TYPE_POINT_2D: + return mg_point_2d_to_py_point2d(mg_value_point_2d(value)); + case MG_VALUE_TYPE_POINT_3D: + return mg_point_3d_to_py_point3d(mg_value_point_3d(value)); default: PyErr_SetString(PyExc_RuntimeError, "encountered a mg_value of unknown type"); @@ -587,6 +605,20 @@ mg_duration *py_delta_to_mg_duration(PyObject *obj) { return mg_duration_make(0, days, seconds, microseconds * 1000); } +mg_point_2d *py_point2d_to_mg_point_2d(PyObject *point_object) { + assert(Py_TYPE(point_object) == &Point2DType); + Point2DObject *py_point2d = (Point2DObject *)point_object; + return mg_point_2d_make(py_point2d->srid, py_point2d->x_longitude, + py_point2d->y_latitude); +} + +mg_point_3d *py_point3d_to_mg_point_3d(PyObject *point_object) { + assert(Py_TYPE(point_object) == &Point3DType); + Point3DObject *py_point3d = (Point3DObject *)point_object; + return mg_point_3d_make(py_point3d->srid, py_point3d->x_longitude, + py_point3d->y_latitude, py_point3d->z_height); +} + mg_value *py_object_to_mg_value(PyObject *object) { mg_value *ret = NULL; @@ -648,6 +680,18 @@ mg_value *py_object_to_mg_value(PyObject *object) { return NULL; } ret = mg_value_make_duration(dur); + } else if (Py_TYPE(object) == &Point2DType) { + mg_point_2d *point = py_point2d_to_mg_point_2d(object); + if (!point) { + return NULL; + } + ret = mg_value_make_point_2d(point); + } else if (Py_TYPE(object) == &Point3DType) { + mg_point_3d *point = py_point3d_to_mg_point_3d(object); + if (!point) { + return NULL; + } + ret = mg_value_make_point_3d(point); } else { PyErr_Format(PyExc_ValueError, "value of type '%s' can't be used as query parameter", diff --git a/src/mgclientmodule.c b/src/mgclientmodule.c index 4c97106..080a049 100644 --- a/src/mgclientmodule.c +++ b/src/mgclientmodule.c @@ -171,6 +171,8 @@ static struct { {"Node", &NodeType}, {"Relationship", &RelationshipType}, {"Path", &PathType}, + {"Point2D", &Point2DType}, + {"Point3D", &Point3DType}, {NULL, NULL}}; static int add_module_types(PyObject *module) { diff --git a/src/types.c b/src/types.c index b51e87f..c7d515f 100644 --- a/src/types.c +++ b/src/types.c @@ -571,6 +571,282 @@ PyTypeObject PathType = { .tp_init = (initproc)path_init, .tp_new = PyType_GenericNew }; +// clang-format on -#undef CHECK_ATTRIBUTE +static void point2d_dealloc(Point2DObject *point2d) { + Py_TYPE(point2d)->tp_free(point2d); +} + +static PyObject *point2d_repr(Point2DObject *point2d) { + char buffer[256]; + snprintf(buffer, sizeof(buffer), + "<%s(srid=%u, x_longitude=%f, y_latitude=%f) at %p>", + Py_TYPE(point2d)->tp_name, point2d->srid, point2d->x_longitude, + point2d->y_latitude, point2d); + return PyUnicode_FromFormat("%s", buffer); +} + +static PyObject *point2d_str(Point2DObject *point2d) { + // NOTE: Somehow, PyUnicode_FromFormat doesn't suppord formatting double + // values. + // https://stackoverflow.com/questions/1701055/what-is-the-maximum-length-in-chars-needed-to-represent-any-double-value + char buffer[256]; + snprintf(buffer, sizeof(buffer), + "Point2D({ srid=%u, x_longitude=%f, y_latitude=%f })", point2d->srid, + point2d->x_longitude, point2d->y_latitude); + return PyUnicode_FromFormat("%s", buffer); +} + +// Helper function for implementing richcompare. +static PyObject *point2d_astuple(Point2DObject *point2d) { + PyObject *tuple = NULL; + PyObject *srid = NULL; + PyObject *x_longitude = NULL; + PyObject *y_latitude = NULL; + + if (!(srid = PyLong_FromUnsignedLong(point2d->srid))) { + goto cleanup; + } + if (!(x_longitude = PyFloat_FromDouble(point2d->x_longitude))) { + goto cleanup; + } + if (!(y_latitude = PyFloat_FromDouble(point2d->y_latitude))) { + goto cleanup; + } + if (!(tuple = PyTuple_New(3))) { + goto cleanup; + } + + PyTuple_SET_ITEM(tuple, 0, srid); + PyTuple_SET_ITEM(tuple, 1, x_longitude); + PyTuple_SET_ITEM(tuple, 2, y_latitude); + return tuple; + +cleanup: + Py_XDECREF(tuple); + Py_XDECREF(srid); + Py_XDECREF(x_longitude); + Py_XDECREF(y_latitude); + return NULL; +} + +static PyObject *point2d_richcompare(Point2DObject *lhs, PyObject *rhs, + int op) { + PyObject *tlhs = NULL; + PyObject *trhs = NULL; + PyObject *ret = NULL; + + if (Py_TYPE(rhs) == &Point2DType) { + if (!(tlhs = point2d_astuple(lhs))) { + goto exit; + } + if (!(trhs = point2d_astuple((Point2DObject *)rhs))) { + goto exit; + } + ret = PyObject_RichCompare(tlhs, trhs, op); + } else { + Py_INCREF(Py_False); + ret = Py_False; + } + +exit: + Py_XDECREF(tlhs); + Py_XDECREF(trhs); + return ret; +} + +int point2d_init(Point2DObject *point2d, PyObject *args, PyObject *kwargs) { + uint16_t srid = 0; + double x_longitude = 0; + double y_latitude = 0; + static char *kwlist[] = {"", "", "", NULL}; + // https://docs.python.org/3/c-api/arg.html#numbers + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Hdd", kwlist, &srid, + &x_longitude, &y_latitude)) { + return -1; + } + + point2d->srid = srid; + point2d->x_longitude = x_longitude; + point2d->y_latitude = y_latitude; + return 0; +} + +PyDoc_STRVAR(Point2DType_srid_doc, + "Point2D srid (a unique identifier associated with a specific " + "coordinate system, tolerance, and resolution)."); +PyDoc_STRVAR(Point2DType_x_longitude_doc, "Point2D x or longitude value."); +PyDoc_STRVAR(Point2DType_y_latitude_doc, "Point2D y or latitude value."); +static PyMemberDef point2d_members[] = { + {"srid", T_USHORT, offsetof(Point2DObject, srid), READONLY, + Point2DType_srid_doc}, + {"x_longitude", T_DOUBLE, offsetof(Point2DObject, x_longitude), READONLY, + Point2DType_x_longitude_doc}, + {"y_latitude", T_DOUBLE, offsetof(Point2DObject, y_latitude), READONLY, + Point2DType_y_latitude_doc}, + {NULL}}; + +PyDoc_STRVAR(Point2DType_doc, "A Point2D object."); +// clang-format off +PyTypeObject Point2DType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "mgclient.Point2D", + .tp_basicsize = sizeof(Point2DObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)point2d_dealloc, + .tp_repr = (reprfunc)point2d_repr, + .tp_str = (reprfunc)point2d_str, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = Point2DType_doc, + .tp_richcompare = (richcmpfunc)point2d_richcompare, + .tp_members = point2d_members, + .tp_init = (initproc)point2d_init, + .tp_new = PyType_GenericNew +}; // clang-format on + +static void point3d_dealloc(Point3DObject *point3d) { + Py_TYPE(point3d)->tp_free(point3d); +} + +static PyObject *point3d_repr(Point3DObject *point3d) { + char buffer[256]; + snprintf(buffer, sizeof(buffer), + "<%s(srid=%u, x_longitude=%f, y_latitude=%f, z_height=%f) at %p>", + Py_TYPE(point3d)->tp_name, point3d->srid, point3d->x_longitude, + point3d->y_latitude, point3d->z_height, point3d); + return PyUnicode_FromFormat("%s", buffer); +} + +static PyObject *point3d_str(Point3DObject *point3d) { + // NOTE: Somehow, PyUnicode_FromFormat doesn't suppord formatting double + // values. + // https://stackoverflow.com/questions/1701055/what-is-the-maximum-length-in-chars-needed-to-represent-any-double-value + char buffer[256]; + snprintf(buffer, sizeof(buffer), + "Point3D({ srid=%u, x_longitude=%f, y_latitude=%f, z_height=%f })", + point3d->srid, point3d->x_longitude, point3d->y_latitude, + point3d->z_height); + return PyUnicode_FromFormat("%s", buffer); +} + +// Helper function for implementing richcompare. +static PyObject *point3d_astuple(Point3DObject *point3d) { + PyObject *tuple = NULL; + PyObject *srid = NULL; + PyObject *x_longitude = NULL; + PyObject *y_latitude = NULL; + PyObject *z_height = NULL; + + if (!(srid = PyLong_FromUnsignedLong(point3d->srid))) { + goto cleanup; + } + if (!(x_longitude = PyFloat_FromDouble(point3d->x_longitude))) { + goto cleanup; + } + if (!(y_latitude = PyFloat_FromDouble(point3d->y_latitude))) { + goto cleanup; + } + if (!(z_height = PyFloat_FromDouble(point3d->z_height))) { + goto cleanup; + } + if (!(tuple = PyTuple_New(4))) { + goto cleanup; + } + + PyTuple_SET_ITEM(tuple, 0, srid); + PyTuple_SET_ITEM(tuple, 1, x_longitude); + PyTuple_SET_ITEM(tuple, 2, y_latitude); + PyTuple_SET_ITEM(tuple, 3, z_height); + return tuple; + +cleanup: + Py_XDECREF(tuple); + Py_XDECREF(srid); + Py_XDECREF(x_longitude); + Py_XDECREF(y_latitude); + Py_XDECREF(z_height); + return NULL; +} + +static PyObject *point3d_richcompare(Point3DObject *lhs, PyObject *rhs, + int op) { + PyObject *tlhs = NULL; + PyObject *trhs = NULL; + PyObject *ret = NULL; + + if (Py_TYPE(rhs) == &Point3DType) { + if (!(tlhs = point3d_astuple(lhs))) { + goto exit; + } + if (!(trhs = point3d_astuple((Point3DObject *)rhs))) { + goto exit; + } + ret = PyObject_RichCompare(tlhs, trhs, op); + } else { + Py_INCREF(Py_False); + ret = Py_False; + } + +exit: + Py_XDECREF(tlhs); + Py_XDECREF(trhs); + return ret; +} + +int point3d_init(Point3DObject *point3d, PyObject *args, PyObject *kwargs) { + uint16_t srid = 0; + double x_longitude = 0; + double y_latitude = 0; + double z_height = 0; + static char *kwlist[] = {"", "", "", "", NULL}; + // https://docs.python.org/3/c-api/arg.html#numbers + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Hddd", kwlist, &srid, + &x_longitude, &y_latitude, &z_height)) { + return -1; + } + + point3d->srid = srid; + point3d->x_longitude = x_longitude; + point3d->y_latitude = y_latitude; + point3d->z_height = z_height; + return 0; +} + +PyDoc_STRVAR(Point3DType_srid_doc, + "Point3D srid (a unique identifier associated with a specific " + "coordinate system, tolerance, and resolution)."); +PyDoc_STRVAR(Point3DType_x_longitude_doc, "Point3D x or longitude value."); +PyDoc_STRVAR(Point3DType_y_latitude_doc, "Point3D y or latitude value."); +PyDoc_STRVAR(Point3DType_z_height_doc, "Point3D z or height value."); +static PyMemberDef point3d_members[] = { + {"srid", T_USHORT, offsetof(Point3DObject, srid), READONLY, + Point3DType_srid_doc}, + {"x_longitude", T_DOUBLE, offsetof(Point3DObject, x_longitude), READONLY, + Point3DType_x_longitude_doc}, + {"y_latitude", T_DOUBLE, offsetof(Point3DObject, y_latitude), READONLY, + Point3DType_y_latitude_doc}, + {"z_height", T_DOUBLE, offsetof(Point3DObject, z_height), READONLY, + Point3DType_z_height_doc}, + {NULL}}; + +PyDoc_STRVAR(Point3DType_doc, "A Point3D object."); +// clang-format off +PyTypeObject Point3DType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "mgclient.Point3D", + .tp_basicsize = sizeof(Point3DObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)point3d_dealloc, + .tp_repr = (reprfunc)point3d_repr, + .tp_str = (reprfunc)point3d_str, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = Point3DType_doc, + .tp_richcompare = (richcmpfunc)point3d_richcompare, + .tp_members = point3d_members, + .tp_init = (initproc)point3d_init, + .tp_new = PyType_GenericNew +}; +// clang-format on + +#undef CHECK_ATTRIBUTE diff --git a/src/types.h b/src/types.h index 74863bc..a20adbf 100644 --- a/src/types.h +++ b/src/types.h @@ -42,10 +42,29 @@ typedef struct { PyObject *nodes; PyObject *relationships; } PathObject; + +typedef struct { + PyObject_HEAD + + uint16_t srid; + double x_longitude; + double y_latitude; +} Point2DObject; + +typedef struct { + PyObject_HEAD + + uint16_t srid; + double x_longitude; + double y_latitude; + double z_height; +} Point3DObject; // clang-format on extern PyTypeObject NodeType; extern PyTypeObject RelationshipType; extern PyTypeObject PathType; +extern PyTypeObject Point2DType; +extern PyTypeObject Point3DType; #endif diff --git a/test/common.py b/test/common.py index f3c481c..e67590c 100644 --- a/test/common.py +++ b/test/common.py @@ -88,7 +88,7 @@ def start_memgraph(cert_file="", key_file=""): "--storage-properties-on-edges=true", "--storage-snapshot-interval-sec=0", "--storage-wal-enabled=false", - "--storage-recover-on-startup=false", + "--data-recovery-on-startup=false", "--storage-snapshot-on-exit=false", "--telemetry-enabled=false", "--log-file", diff --git a/test/test_glue.py b/test/test_glue.py index eafa2e1..d7b8951 100644 --- a/test/test_glue.py +++ b/test/test_glue.py @@ -263,3 +263,35 @@ def test_duration(memgraph_connection): cursor.execute("RETURN $value", {"value": datetime.timedelta(64, 7, 11, 1)}) result = cursor.fetchall() assert result == [(datetime.timedelta(64, 7, 1011),)] + + +def test_point2d_receive(memgraph_connection): + conn = memgraph_connection + cursor = conn.cursor() + cursor.execute("RETURN point({x:0, y:1}) AS point;") + result = cursor.fetchall() + assert result == [(mgclient.Point2D(7203, 0, 1),)] + + +def test_point2d_send(memgraph_connection): + conn = memgraph_connection + cursor = conn.cursor() + cursor.execute("RETURN $value", {"value": mgclient.Point2D(7203, 0, 1)}) + result = cursor.fetchall() + assert result == [(mgclient.Point2D(7203, 0, 1),)] + + +def test_point3d_receive(memgraph_connection): + conn = memgraph_connection + cursor = conn.cursor() + cursor.execute("RETURN point({x:0, y:1, z:2}) AS point;") + result = cursor.fetchall() + assert result == [(mgclient.Point3D(9757, 0, 1, 2),)] + + +def test_point3d_send(memgraph_connection): + conn = memgraph_connection + cursor = conn.cursor() + cursor.execute("RETURN $value", {"value": mgclient.Point3D(9757, 0, 1, 2)}) + result = cursor.fetchall() + assert result == [(mgclient.Point3D(9757, 0, 1, 2),)] diff --git a/test/test_types.py b/test/test_types.py index e1da3d4..6d94a2c 100644 --- a/test/test_types.py +++ b/test/test_types.py @@ -47,3 +47,41 @@ def test_path(): path = mgclient.Path([n1, n2, n3], [e1, e2]) assert str(path) == "(:Label1)-[:Edge1]->(:Label2)<-[:Edge2]-(:Label3)" + + +def test_point2d(): + p1 = mgclient.Point2D(0, 1, 2); + assert p1.srid == 0 + assert p1.x_longitude == 1 + assert p1.y_latitude == 2 + assert str(p1) == "Point2D({ srid=0, x_longitude=1.000000, y_latitude=2.000000 })" + assert repr(p1).startswith("