From d4e59499ea2d5358b8cbfe597e458f8f82eb69da Mon Sep 17 00:00:00 2001 From: Victor Petrovykh Date: Fri, 2 Aug 2024 05:41:49 -0400 Subject: [PATCH] Add geometry, geography, box2d, and box3d PostGIS types. --- edgedb/protocol/codecs/codecs.pyx | 51 ++- tests/test_postgis.py | 553 ++++++++++++++++++++++++++++++ 2 files changed, 603 insertions(+), 1 deletion(-) create mode 100644 tests/test_postgis.py diff --git a/edgedb/protocol/codecs/codecs.pyx b/edgedb/protocol/codecs/codecs.pyx index 1218d3e6..97d764d3 100644 --- a/edgedb/protocol/codecs/codecs.pyx +++ b/edgedb/protocol/codecs/codecs.pyx @@ -26,7 +26,7 @@ from edgedb import enums from edgedb.datatypes import datatypes from libc.string cimport memcpy - +from cpython.bytes cimport PyBytes_FromStringAndSize include "./edb_types.pxi" @@ -894,6 +894,27 @@ cdef bigint_decode(pgproto.CodecContext settings, FRBuffer *buf): return int(result) +cdef geometry_encode(pgproto.CodecContext settings, WriteBuffer buf, obj): + buf.write_int32(len(obj.wkb)) + buf.write_bytes(obj.wkb) + + +cdef geometry_decode(pgproto.CodecContext settings, FRBuffer *buf): + cdef: + object result + # Just wrap the bytes into a named tuple with a single field `wkb` + descriptor = datatypes.record_desc_new( + ('wkb',), NULL, NULL) + result = datatypes.namedtuple_new( + datatypes.namedtuple_type_new(descriptor)) + + elem = PyBytes_FromStringAndSize(frb_read_all(buf), buf.len) + cpython.Py_INCREF(elem) + cpython.PyTuple_SET_ITEM(result, 0, elem) + + return result + + cdef register_base_scalar_codecs(): register_base_scalar_codec( 'std::uuid', @@ -1007,5 +1028,33 @@ cdef register_base_scalar_codecs(): uuid.UUID('9565dd88-04f5-11ee-a691-0b6ebe179825'), ) + register_base_scalar_codec( + 'ext::postgis::geometry', + geometry_encode, + geometry_decode, + uuid.UUID('44c901c0-d922-4894-83c8-061bd05e4840'), + ) + + register_base_scalar_codec( + 'ext::postgis::geography', + geometry_encode, + geometry_decode, + uuid.UUID('4d738878-3a5f-4821-ab76-9d8e7d6b32c4'), + ) + + register_base_scalar_codec( + 'ext::postgis::box2d', + geometry_encode, + geometry_decode, + uuid.UUID('7fae5536-6311-4f60-8eb9-096a5d972f48'), + ) + + register_base_scalar_codec( + 'ext::postgis::box3d', + geometry_encode, + geometry_decode, + uuid.UUID('c1a50ff8-fded-48b0-85c2-4905a8481433'), + ) + register_base_scalar_codecs() diff --git a/tests/test_postgis.py b/tests/test_postgis.py new file mode 100644 index 00000000..9d6583ef --- /dev/null +++ b/tests/test_postgis.py @@ -0,0 +1,553 @@ +# +# This source file is part of the EdgeDB open source project. +# +# Copyright 2024-present MagicStack Inc. and the EdgeDB authors. +# +# 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. +# + +from collections import namedtuple + +from edgedb import _testbase as tb + + +Geo = namedtuple('Geo', ['wkb']) + + +class TestPostgis(tb.SyncQueryTestCase): + '''Test PostGIS extension. + + The GEO data gets sent as WKB (or EWKB) binary format. Thus we wrap the + bytes into a simple object that has `wkb` field avaialable. Any library + that can work with GEOS data should be able to read this format. + + Raw bytes used as sample GEOS data in (E)WKB format. + ''' + + def setUp(self): + super().setUp() + + if not self.client.query_required_single(''' + select exists ( + select sys::ExtensionPackage filter .name = 'postgis' + ) + '''): + self.skipTest("feature not implemented") + + self.client.execute(''' + create extension postgis; + ''') + + def tearDown(self): + try: + self.client.execute(''' + drop extension postgis; + ''') + finally: + super().tearDown() + + async def _test_postgis_geometry(self, wkt, wkb): + val = self.client.query_single(f''' + with module ext::postgis + select {wkt!r} + ''') + self.assertEqual(val.wkb, wkb) + self.assertEqual(val, (wkb,)) + self.assertEqual(val, Geo(wkb=wkb)) + + val = self.client.query_single(f''' + with module ext::postgis + select {wkt!r} = $0 + ''', Geo(wkb=wkb)) + self.assertTrue(val) + + async def _test_postgis_geography(self, wkt, wkb): + val = self.client.query_single(f''' + with module ext::postgis + select {wkt!r} + ''') + self.assertEqual(val.wkb, wkb) + self.assertEqual(val, (wkb,)) + self.assertEqual(val, Geo(wkb=wkb)) + + val = self.client.query_single(f''' + with module ext::postgis + select {wkt!r} = $0 + ''', Geo(wkb=wkb)) + self.assertTrue(val) + + async def test_postgis_01(self): + await self._test_postgis_geometry( + 'point(1 2)', + b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00' + b'\x00\x00\x00\x00\x00@', + ) + + await self._test_postgis_geometry( + 'point z (1 2 3)', + b'\x01\x01\x00\x00\x80\x00\x00\x00\x00\x00\x00\xf0?\x00\x00' + b'\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@', + ) + + await self._test_postgis_geometry( + 'point m (1 2 3)', + b'\x01\x01\x00\x00@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00' + b'\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@', + ) + + await self._test_postgis_geometry( + 'point zm (1 2 3 4)', + b'\x01\x01\x00\x00\xc0\x00\x00\x00\x00\x00\x00\xf0?\x00\x00' + b'\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@\x00\x00' + b'\x00\x00\x00\x00\x10@', + ) + + async def test_postgis_02(self): + await self._test_postgis_geometry( + 'multipoint ((1 2), (4 5))', + b'\x01\x04\x00\x00\x00\x02\x00\x00\x00\x01\x01\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@\x01' + b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00' + b'\x00\x00\x14@', + ) + + await self._test_postgis_geometry( + 'multipoint z ((1 2 3), (4 5 6))', + b'\x01\x04\x00\x00\x80\x02\x00\x00\x00\x01\x01\x00\x00\x80\x00' + b'\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@\x00' + b'\x00\x00\x00\x00\x00\x08@\x01\x01\x00\x00\x80\x00\x00\x00\x00' + b'\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00' + b'\x00\x18@', + ) + + async def test_postgis_03(self): + await self._test_postgis_geometry( + 'linestring (1 2, 3 4, 5 6)', + b'\x01\x02\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\xf0?\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08' + b'@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x14@' + b'\x00\x00\x00\x00\x00\x00\x18@', + ) + + await self._test_postgis_geometry( + '''polygon ( + (0 0 0, 4 0 0, 4 4 0, 0 4 0, 0 0 0), + (1 1 0, 2 1 0, 2 2 0, 1 2 0, 1 1 0) + )''', + b'\x01\x03\x00\x00\x80\x02\x00\x00\x00\x05\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10@' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00' + b'\x10@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00' + b'\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00' + b'\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0' + b'?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00' + b'\x00\x00', + ) + + await self._test_postgis_geometry( + 'multilinestring ((0 0, 1 1, 1 2), (2 3, 3 2, 5 4))', + b'\x01\x05\x00\x00\x00\x02\x00\x00\x00\x01\x02\x00\x00\x00\x03' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00' + b'\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00' + b'\x00\x00\x00\x00\x00@\x01\x02\x00\x00\x00\x03\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08' + b'@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00' + b'\x00@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00' + b'\x00\x10@', + ) + + await self._test_postgis_geometry( + '''multipolygon ( + ((1 5, 5 5, 5 1, 1 1, 1 5)), ((6 5, 9 1, 6 1, 6 5)) + )''', + b'\x01\x06\x00\x00\x00\x02\x00\x00\x00\x01\x03\x00\x00\x00\x01' + b'\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?' + b'\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x14' + b'@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00' + b'\x14@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00' + b'\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00' + b'\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x14@\x01\x03\x00' + b'\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x18@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00' + b'\x00\x00"@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00' + b'\x00\x00\x00\x18@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00' + b'\x00\x00\x00\x00\x18@\x00\x00\x00\x00\x00\x00\x14@', + ) + + await self._test_postgis_geometry( + 'geometrycollection ( point(2 3), linestring(2 3, 3 4))', + b'\x01\x07\x00\x00\x00\x02\x00\x00\x00\x01\x01\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@' + b'\x01\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00' + b'\x00\x08@\x00\x00\x00\x00\x00\x00\x10@', + ) + + async def test_postgis_04(self): + # Extended WKB + await self._test_postgis_geometry( + '''polyhedralsurface z ( + ((0 0 0, 0 0 1, 0 1 1, 0 1 0, 0 0 0)), + ((0 0 0, 0 1 0, 1 1 0, 1 0 0, 0 0 0)), + ((0 0 0, 1 0 0, 1 0 1, 0 0 1, 0 0 0)), + ((1 1 0, 1 1 1, 1 0 1, 1 0 0, 1 1 0)), + ((0 1 0, 0 1 1, 1 1 1, 1 1 0, 0 1 0)), + ((0 0 1, 1 0 1, 1 1 1, 0 1 1, 0 0 1)) + )''', + b'\x01\x0f\x00\x00\x80\x06\x00\x00\x00\x01\x03\x00\x00\x80\x01' + b'\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00' + b'\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x80\x01\x00\x00' + b'\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0' + b'?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x01\x03\x00\x00\x80\x01\x00\x00\x00\x05' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x01\x03\x00\x00\x80\x01\x00\x00\x00\x05\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00' + b'\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00' + b'\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00' + b'\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?' + b'\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x01\x03\x00\x00\x80\x01\x00\x00\x00\x05\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00' + b'\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00' + b'\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00' + b'\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x01' + b'\x03\x00\x00\x80\x01\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?' + b'\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0' + b'?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00' + b'\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?', + ) + + await self._test_postgis_geometry( + 'triangle ((0 0, 0 9, 9 0, 0 0))', + b'\x01\x11\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"@' + b'\x00\x00\x00\x00\x00\x00"@\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00', + ) + + await self._test_postgis_geometry( + '''tin z ( + ((0 0 0, 0 0 1, 0 1 0, 0 0 0)), + ((0 0 0, 0 1 0, 1 1 0, 0 0 0)) + )''', + b'\x01\x10\x00\x00\x80\x02\x00\x00\x00\x01\x11\x00\x00\x80\x01' + b'\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x11' + b'\x00\x00\x80\x01\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00', + ) + + async def test_postgis_05(self): + # Curved geometry: extended WKB + await self._test_postgis_geometry( + 'circularstring(0 0, 1 1, 1 0)', + b'\x01\x08\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00' + b'\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00', + ) + + await self._test_postgis_geometry( + 'compoundcurve( circularstring(0 0, 1 1, 1 0),(1 0, 0 1))', + b'\x01\t\x00\x00\x00\x02\x00\x00\x00\x01\x08\x00\x00\x00\x03' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00' + b'\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x00\x02\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\xf0?', + ) + + await self._test_postgis_geometry( + '''curvepolygon( + compoundcurve( + circularstring(0 0, 2 0, 2 1, 2 3, 4 3), + (4 3, 4 5, 1 4, 0 0) + ), + circularstring(1.7 1, 1.4 0.4, 1.6 0.4, 1.6 0.5, 1.7 1) + )''', + b'\x01\n\x00\x00\x00\x02\x00\x00\x00\x01\t\x00\x00\x00\x02' + b'\x00\x00\x00\x01\x08\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\xf0?' + b'\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08' + b'@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00' + b'\x08@\x01\x02\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00' + b'\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x14@\x00\x00' + b'\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x10@\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x01\x08\x00\x00\x00\x05\x00\x00\x00333333' + b'\xfb?\x00\x00\x00\x00\x00\x00\xf0?fffff' + b'f\xf6?\x9a\x99\x99\x99\x99\x99\xd9?\x9a\x99\x99\x99' + b'\x99\x99\xf9?\x9a\x99\x99\x99\x99\x99\xd9?\x9a\x99\x99' + b'\x99\x99\x99\xf9?\x00\x00\x00\x00\x00\x00\xe0?33' + b'3333\xfb?\x00\x00\x00\x00\x00\x00\xf0?', + ) + + await self._test_postgis_geometry( + 'multicurve( (0 0, 5 5), circularstring(4 0, 4 4, 8 4))', + b'\x01\x0b\x00\x00\x00\x02\x00\x00\x00\x01\x02\x00\x00\x00\x02' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00' + b'\x00\x00\x00\x14@\x01\x08\x00\x00\x00\x03\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x10' + b'@\x00\x00\x00\x00\x00\x00 @\x00\x00\x00\x00\x00\x00' + b'\x10@', + ) + + await self._test_postgis_geometry( + '''multisurface( + curvepolygon( + circularstring(0 0, 4 0, 4 4, 0 4, 0 0), + (1 1, 3 3, 3 1, 1 1) + ), + ( + (10 10, 14 12, 11 10, 10 10), + (11 11, 11.5 11, 11 11.5, 11 11) + ) + )''', + b'\x01\x0c\x00\x00\x00\x02\x00\x00\x00\x01\n\x00\x00\x00\x02' + b'\x00\x00\x00\x01\x08\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x10@' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10' + b'@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x01\x02\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00' + b'\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x08@\x00\x00' + b'\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\xf0?\x00' + b'\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?' + b'\x01\x03\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x00$@\x00' + b'\x00\x00\x00\x00\x00,@\x00\x00\x00\x00\x00\x00(@' + b'\x00\x00\x00\x00\x00\x00&@\x00\x00\x00\x00\x00\x00$' + b'@\x00\x00\x00\x00\x00\x00$@\x00\x00\x00\x00\x00\x00' + b'$@\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00&@\x00' + b"\x00\x00\x00\x00\x00&@\x00\x00\x00\x00\x00\x00'@" + b'\x00\x00\x00\x00\x00\x00&@\x00\x00\x00\x00\x00\x00&' + b"@\x00\x00\x00\x00\x00\x00'@\x00\x00\x00\x00\x00\x00" + b'&@\x00\x00\x00\x00\x00\x00&@', + ) + + async def test_postgis_06(self): + # Geometry with SRID + await self._test_postgis_geometry( + 'srid=4; point(1 2)', + b'\x01\x01\x00\x00 \x04\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\xf0?\x00\x00\x00\x00\x00\x00\x00@', + ) + + await self._test_postgis_geometry( + 'srid=4267; point(1 2)', + b'\x01\x01\x00\x00 \xab\x10\x00\x00\x00\x00\x00\x00\x00\x00' + b'\xf0?\x00\x00\x00\x00\x00\x00\x00@', + ) + + async def test_postgis_07(self): + # Geography with SRID + await self._test_postgis_geography( + 'point(1 2)', + b'\x01\x01\x00\x00 \xe6\x10\x00\x00\x00\x00\x00\x00\x00\x00' + b'\xf0?\x00\x00\x00\x00\x00\x00\x00@', + ) + + await self._test_postgis_geography( + 'srid=4267; point(1 2)', + b'\x01\x01\x00\x00 \xab\x10\x00\x00\x00\x00\x00\x00\x00\x00' + b'\xf0?\x00\x00\x00\x00\x00\x00\x00@', + ) + + async def test_postgis_08(self): + text = 'box(0 1, 2 3)' + data = ( + b'\x01\x03\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08@' + b'\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08' + b'@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00' + b'\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\xf0?' + ) + + val = self.client.query_single(f''' + with module ext::postgis + select {text!r} + ''') + self.assertEqual(val.wkb, data) + self.assertEqual(val, (data,)) + self.assertEqual(val, Geo(wkb=data)) + + val = self.client.query_single(f''' + with module ext::postgis + select {text!r} = $0 + ''', Geo(wkb=data)) + self.assertTrue(val) + + val = self.client.query_single(f''' + with module ext::postgis + select {text!r} = (>$0)[0] + ''', [Geo(wkb=data)]) + self.assertTrue(val) + + val = self.client.query_single(f''' + with module ext::postgis + select {text!r} = (>$0).0 + ''', (Geo(wkb=data), 'ok')) + self.assertTrue(val) + + async def test_postgis_09(self): + text = 'BOX3D(0 1 5, 2 3 9)' + data = ( + b'\x01\x0f\x00\x00\x80\x06\x00\x00\x00\x01\x03\x00\x00\x80\x01' + b'\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x14' + b'@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x08@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00' + b'\x00\x00@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00' + b'\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00' + b'\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x14@\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00' + b'\x00\x00\x00\x00\x00\x14@\x01\x03\x00\x00\x80\x01\x00\x00' + b'\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00"@\x00' + b'\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\xf0?' + b'\x00\x00\x00\x00\x00\x00"@\x00\x00\x00\x00\x00\x00\x00' + b'@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00' + b'"@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x08@\x00\x00\x00\x00\x00\x00"@\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00' + b'\x00\x00\x00"@\x01\x03\x00\x00\x80\x01\x00\x00\x00\x05' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00' + b'\x00\x00\x00\x00"@\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00"@' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08' + b'@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00' + b'\x00\x14@\x01\x03\x00\x00\x80\x01\x00\x00\x00\x05\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00' + b'\xf0?\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00' + b'\x00\x00@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00' + b'\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00' + b'\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00"@\x00\x00' + b'\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\xf0?\x00' + b'\x00\x00\x00\x00\x00"@\x00\x00\x00\x00\x00\x00\x00@' + b'\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x14' + b'@\x01\x03\x00\x00\x80\x01\x00\x00\x00\x05\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?' + b'\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x00' + b'@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00' + b'\x14@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00' + b'\x00\xf0?\x00\x00\x00\x00\x00\x00"@\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00' + b'\x00\x00\x00"@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x14@\x01' + b'\x03\x00\x00\x80\x01\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08@\x00\x00' + b'\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00"@' + b'\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08' + b'@\x00\x00\x00\x00\x00\x00"@\x00\x00\x00\x00\x00\x00' + b'\x00@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00' + b'\x00\x14@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x14@' + ) + + val = self.client.query_single(f''' + with module ext::postgis + select {text!r} + ''') + self.assertEqual(val.wkb, data) + self.assertEqual(val, (data,)) + self.assertEqual(val, Geo(wkb=data)) + + val = self.client.query_single(f''' + with module ext::postgis + select {text!r} = $0 + ''', Geo(wkb=data)) + self.assertTrue(val) + + val = self.client.query_single(f''' + with module ext::postgis + select {text!r} = (>$0)[0] + ''', [Geo(wkb=data)]) + self.assertTrue(val) + + val = self.client.query_single(f''' + with module ext::postgis + select {text!r} = (>$0).0 + ''', (Geo(wkb=data), 'ok')) + self.assertTrue(val)