diff --git a/python/sedonadb/pyproject.toml b/python/sedonadb/pyproject.toml index 8e10481c2..4705f6427 100644 --- a/python/sedonadb/pyproject.toml +++ b/python/sedonadb/pyproject.toml @@ -41,6 +41,7 @@ test = [ "pandas", "polars", "pytest", + "pyyaml", ] geopandas = [ "adbc-driver-manager[dbapi]", diff --git a/python/sedonadb/python/sedonadb/testing.py b/python/sedonadb/python/sedonadb/testing.py index 86712c1c0..3bda13629 100644 --- a/python/sedonadb/python/sedonadb/testing.py +++ b/python/sedonadb/python/sedonadb/testing.py @@ -18,7 +18,7 @@ import os import warnings from pathlib import Path -from typing import TYPE_CHECKING, List, Tuple, Any +from typing import TYPE_CHECKING, Any, List, Tuple import geoarrow.pyarrow as ga import pyarrow as pa @@ -657,6 +657,216 @@ def __init__(self, uri=None): cur.execute("SET max_parallel_workers_per_gather TO 0") +class BigQuery(DBEngine): + """A BigQuery implementation of the DBEngine using ADBC + + Uses the ADBC BigQuery driver. Authentication uses Application Default + Credentials (ADC) by default — run ``gcloud auth application-default login`` + once to set that up. Set the following environment variables to configure + the connection: + + - SEDONADB_BIGQUERY_TEST_PROJECT_ID: GCP project identifier. Defaults to + "sedonadb-testing". + - SEDONADB_BIGQUERY_TEST_DATASET_ID: Dataset identifier. In general data is + not scanned for these tests because doing so would incur cost. + - SEDONADB_BIGQUERY_TEST_CREDENTIALS_FILE: (optional) Path to a service + account JSON key file. When omitted, ADC is used instead. + + Unless modifying these tests, the cached results should allow these tests + to run without an active connection (and should allow tests to run locally + much faster as opening a connection to BigQuery is slow). + """ + + _CACHE_DIR = Path(__file__).resolve().parent.parent.parent / "tests" / "geography" + _shared_cache: "ArrowSQLCache | None" = None + + def __init__(self, cache_path: "Path | None" = None): + self._cache_path = cache_path or self._CACHE_DIR / "bigquery_cache.yml" + if cache_path is not None or BigQuery._shared_cache is None: + BigQuery._shared_cache = ArrowSQLCache("bigquery", self._cache_path) + self._file_cache = BigQuery._shared_cache + self.con = None + self._con_attempted = False + + def _ensure_con(self): + import adbc_driver_bigquery + import adbc_driver_bigquery.dbapi + + if self.con is not None or self._con_attempted: + return + self._con_attempted = True + + project_id = os.environ.get( + "SEDONADB_BIGQUERY_TEST_PROJECT_ID", "sedonadb-testing" + ) + dataset_id = os.environ.get( + "SEDONADB_BIGQUERY_TEST_DATASET_ID", "sedonadb_test" + ) + credentials_file = os.environ.get("SEDONADB_BIGQUERY_TEST_CREDENTIALS_FILE") + + db_kwargs = { + adbc_driver_bigquery.DatabaseOptions.PROJECT_ID.value: project_id, + adbc_driver_bigquery.DatabaseOptions.DATASET_ID.value: dataset_id, + } + + if credentials_file: + db_kwargs[adbc_driver_bigquery.DatabaseOptions.AUTH_TYPE.value] = ( + adbc_driver_bigquery.DatabaseOptions.AUTH_VALUE_JSON_CREDENTIAL_FILE.value + ) + db_kwargs[adbc_driver_bigquery.DatabaseOptions.AUTH_CREDENTIALS.value] = ( + credentials_file + ) + + self.con = adbc_driver_bigquery.dbapi.connect(db_kwargs=db_kwargs) + + def close(self): + """Close the connection and flush any new cache entries to disk""" + self._file_cache.flush() + if self.con: + self.con.close() + + def __del__(self): + try: + self.close() + except Exception: + pass + + @classmethod + def name(cls): + return "bigquery" + + @classmethod + def install_hint(cls): + return ( + "- Run `pip install adbc-driver-bigquery` to install the required driver\n" + "- Run `gcloud auth application-default login` to authenticate\n" + "- Set SEDONADB_BIGQUERY_TEST_PROJECT_ID to a valid BigQuery project identifier" + ) + + def val_or_null(self, arg): + if isinstance(arg, bytes): + return f"FROM_HEX('{arg.hex()}')" + else: + return super().val_or_null(arg) + + def create_table_parquet(self, name, paths) -> "BigQuery": + raise NotImplementedError("Create table from Parquet not implemented") + + def create_table_arrow(self, name, obj, *, geometry_cols=None) -> "BigQuery": + raise NotImplementedError("Create table from Arrow not implemented") + + def execute_and_collect(self, query) -> pa.Table: + cached = self._file_cache.get(query) + if cached is not None: + return cached + + try: + self._ensure_con() + except Exception as e: + raise RuntimeError( + "Query not in cache and BigQuery connection unavailable:\n" + + BigQuery.install_hint() + ) from e + + with self.con.cursor() as cur: + cur.execute(query) + result = cur.fetch_arrow_table() + self._file_cache.put(query, result) + return result + + def result_to_table(self, result: pa.Table) -> pa.Table: + # BigQuery only has a GEOGRAPHY type (always WGS84 with spherical edges). + # The ADBC driver returns geography columns as WKT strings with + # Arrow extension metadata: ARROW:extension:name = 'google:sqlType:geography'. + cols = {} + for name, col in zip(result.schema.names, result.columns): + field = result.schema.field(name) + ext_name = (field.metadata or {}).get(b"ARROW:extension:name", b"") + if ext_name == b"google:sqlType:geography": + col_wkb = ga.as_wkb(col.cast(pa.string())) + cols[name] = ga.with_crs( + ga.wkb().with_edge_type(ga.EdgeType.SPHERICAL).wrap_array(col_wkb), + ga.OGC_CRS84, + ) + else: + cols[name] = col + + return pa.table(cols) + + +class ArrowSQLCache: + """A YAML-file-backed cache for Arrow-based query results. + + Each entry stores a pa.Table as base64-encoded Arrow IPC. Queries are + sorted alphabetically when written for stable git diffs. Results are + nested under ``results.`` in the YAML output. + + Leading comment lines (e.g., a license header) are preserved across + rewrites. + """ + + def __init__(self, engine_name: str, path: Path): + self._engine_name = engine_name + self._path = path + self._header_lines: list[str] = [] + self._entries: dict = {} + self._dirty = False + if self._path.exists(): + self._load() + + def _load(self): + import yaml + + with open(self._path) as f: + lines = f.readlines() + + # Split leading comment lines from the YAML body + body_start = 0 + for i, line in enumerate(lines): + if line.startswith("#") or line.strip() == "": + body_start = i + 1 + else: + break + self._header_lines = lines[:body_start] + + body = "".join(lines[body_start:]) + doc = yaml.safe_load(body) if body.strip() else {} + if doc and "results" in doc: + self._entries = doc["results"].get(self._engine_name, {}) + else: + self._entries = doc or {} + + def get(self, query: str) -> "pa.Table | None": + entry = self._entries.get(query) + if entry is None: + return None + import base64 + + buf = base64.b64decode(entry) + return pa.ipc.open_stream(buf).read_all() + + def put(self, query: str, table: pa.Table): + import base64 + + sink = pa.BufferOutputStream() + with pa.ipc.new_stream(sink, table.schema) as writer: + writer.write_table(table) + self._entries[query] = base64.b64encode(sink.getvalue().to_pybytes()).decode() + self._dirty = True + + def flush(self): + if not self._dirty: + return + self._path.parent.mkdir(parents=True, exist_ok=True) + doc = {"results": {self._engine_name: self._entries}} + with open(self._path, "w") as f: + import yaml + + f.writelines(self._header_lines) + yaml.dump(doc, f, default_flow_style=False, sort_keys=True) + self._dirty = False + + def geom_or_null(arg, srid=None): """Format SQL expression for a geometry object or NULL""" if arg is None: diff --git a/python/sedonadb/tests/functions/test_functions.py b/python/sedonadb/tests/functions/test_functions.py index fbbda82cd..181325eb0 100644 --- a/python/sedonadb/tests/functions/test_functions.py +++ b/python/sedonadb/tests/functions/test_functions.py @@ -2392,31 +2392,6 @@ def test_st_reverse(eng, geom, expected): eng.assert_query_result(f"SELECT ST_Reverse({geom_or_null(geom)})", expected) -@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) -@pytest.mark.parametrize( - ("x", "y", "expected"), - [ - (None, None, None), - (1, None, None), - (None, 1, None), - (1, 1, "POINT (1 1)"), - (1.0, 1.0, "POINT (1 1)"), - (10, -1.5, "POINT (10 -1.5)"), - ], -) -def test_st_geogpoint(eng, x, y, expected): - eng = eng.create_or_skip() - if eng == SedonaDB: - eng.assert_query_result( - f"SELECT ST_GeogPoint({val_or_null(x)}, {val_or_null(y)})", expected - ) - else: - eng.assert_query_result( - f"SELECT ST_Point({val_or_null(x)}, {val_or_null(y)}) as geography", - expected, - ) - - @pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) @pytest.mark.parametrize( ("x", "y", "expected"), diff --git a/python/sedonadb/tests/geography/bigquery_cache.yml b/python/sedonadb/tests/geography/bigquery_cache.yml new file mode 100644 index 000000000..ca9808666 --- /dev/null +++ b/python/sedonadb/tests/geography/bigquery_cache.yml @@ -0,0 +1,60 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +results: + bigquery: + SELECT ST_Area(NULL): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAEAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA= + SELECT ST_Area(ST_GeogFromText('GEOMETRYCOLLECTION EMPTY')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + SELECT ST_Area(ST_GeogFromText('LINESTRING (0 0, 1 1)')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + SELECT ST_Area(ST_GeogFromText('LINESTRING EMPTY')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + SELECT ST_Area(ST_GeogFromText('MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + SELECT ST_Area(ST_GeogFromText('MULTILINESTRING EMPTY')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + SELECT ST_Area(ST_GeogFromText('MULTIPOINT ((0 0), (1 1))')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + SELECT ST_Area(ST_GeogFromText('MULTIPOINT EMPTY')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + SELECT ST_Area(ST_GeogFromText('MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((10 10, 11 10, 11 11, 10 11, 10 10)))')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAACixmuoX9YWQv////8AAAAA + SELECT ST_Area(ST_GeogFromText('MULTIPOLYGON EMPTY')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + SELECT ST_Area(ST_GeogFromText('POINT (5 2)')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + SELECT ST_Area(ST_GeogFromText('POINT EMPTY')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + SELECT ST_Area(ST_GeogFromText('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAACBnLjOoQcHQv////8AAAAA + SELECT ST_Area(ST_GeogFromText('POLYGON EMPTY')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + SELECT ST_Centroid(ST_GeogFromText('LINESTRING (0 0, 0 1)')): /////xgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABBRQAAAC8AAAACAAAABAAAAAAAAAAAwAAAGYwXwACAAAAVAAAAAQAAAC8////IAAAAAQAAAATAAAAeyJlbmNvZGluZyI6ICJXS1QifQAYAAAAQVJST1c6ZXh0ZW5zaW9uOm1ldGFkYXRhAAAAAAgADAAEAAgACAAAACgAAAAEAAAAGAAAAGdvb2dsZTpzcWxUeXBlOmdlb2dyYXBoeQAAAAAUAAAAQVJST1c6ZXh0ZW5zaW9uOm5hbWUAAAAABAAEAAQAAAAAAAAA/////5gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAABgAAAAAAAAAAAAKABgADAAEAAgACgAAAEwAAAAQAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAMAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAUE9JTlQoMCAwLjUpAAAAAP////8AAAAA + SELECT ST_Centroid(ST_GeogFromText('POINT (0 0)')): /////xgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABBRQAAAC8AAAACAAAABAAAAAAAAAAAwAAAGYwXwACAAAAVAAAAAQAAAC8////IAAAAAQAAAATAAAAeyJlbmNvZGluZyI6ICJXS1QifQAYAAAAQVJST1c6ZXh0ZW5zaW9uOm1ldGFkYXRhAAAAAAgADAAEAAgACAAAACgAAAAEAAAAGAAAAGdvb2dsZTpzcWxUeXBlOmdlb2dyYXBoeQAAAAAUAAAAQVJST1c6ZXh0ZW5zaW9uOm5hbWUAAAAABAAEAAQAAAAAAAAA/////5gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAABgAAAAAAAAAAAAKABgADAAEAAgACgAAAEwAAAAQAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAKAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAUE9JTlQoMCAwKQAAAAAAAP////8AAAAA + SELECT ST_Centroid(ST_GeogFromText('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))')): /////xgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABBRQAAAC8AAAACAAAABAAAAAAAAAAAwAAAGYwXwACAAAAVAAAAAQAAAC8////IAAAAAQAAAATAAAAeyJlbmNvZGluZyI6ICJXS1QifQAYAAAAQVJST1c6ZXh0ZW5zaW9uOm1ldGFkYXRhAAAAAAgADAAEAAgACAAAACgAAAAEAAAAGAAAAGdvb2dsZTpzcWxUeXBlOmdlb2dyYXBoeQAAAAAUAAAAQVJST1c6ZXh0ZW5zaW9uOm5hbWUAAAAABAAEAAQAAAAAAAAA/////5gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAADgAAAAAAAAAAAAKABgADAAEAAgACgAAAEwAAAAQAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAqAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAqAAAAUE9JTlQoMC40OTk5OTk5OTk5OTk4ODMgMC41MDAwMDYzNDIzMjE4MjIpAAAAAAAA/////wAAAAA= + SELECT ST_Distance(NULL, NULL): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAEAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAA= + SELECT ST_Distance(ST_GeogFromText('POINT (0 0)'), ST_GeogFromText('POINT (0 0)')): /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA + ? SELECT ST_Distance(ST_GeogFromText('POINT(-72.1235 42.3521)'), ST_GeogFromText('LINESTRING(-72.1260 + 42.45, -72.123 42.1546)')) + : /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAgCR/dct5eQP////8AAAAA + ? SELECT ST_Distance(ST_GeogFromText('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))'), ST_GeogFromText('POLYGON + ((5 5, 6 5, 6 6, 5 6, 5 5))')) + : /////3gAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEDEAAAABwAAAAEAAAAAAAAAAMAAABmMF8AAAAGAAgABgAGAAAAAAACAAAAAAD/////iAAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAACAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAPAAAABAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAABa/lpPTi4jQf////8AAAAA + SELECT ST_GeogPoint(1, 1): /////xgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABBRQAAAC8AAAACAAAABAAAAAAAAAAAwAAAGYwXwACAAAAVAAAAAQAAAC8////IAAAAAQAAAATAAAAeyJlbmNvZGluZyI6ICJXS1QifQAYAAAAQVJST1c6ZXh0ZW5zaW9uOm1ldGFkYXRhAAAAAAgADAAEAAgACAAAACgAAAAEAAAAGAAAAGdvb2dsZTpzcWxUeXBlOmdlb2dyYXBoeQAAAAAUAAAAQVJST1c6ZXh0ZW5zaW9uOm5hbWUAAAAABAAEAAQAAAAAAAAA/////5gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAABgAAAAAAAAAAAAKABgADAAEAAgACgAAAEwAAAAQAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAKAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAUE9JTlQoMSAxKQAAAAAAAP////8AAAAA + SELECT ST_GeogPoint(1, NULL): /////xgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABBRQAAAC8AAAACAAAABAAAAAAAAAAAwAAAGYwXwACAAAAVAAAAAQAAAC8////IAAAAAQAAAATAAAAeyJlbmNvZGluZyI6ICJXS1QifQAYAAAAQVJST1c6ZXh0ZW5zaW9uOm1ldGFkYXRhAAAAAAgADAAEAAgACAAAACgAAAAEAAAAGAAAAGdvb2dsZTpzcWxUeXBlOmdlb2dyYXBoeQAAAAAUAAAAQVJST1c6ZXh0ZW5zaW9uOm5hbWUAAAAABAAEAAQAAAAAAAAA/////5gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAABAAAAAAAAAAAAAKABgADAAEAAgACgAAAEwAAAAQAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA== + SELECT ST_GeogPoint(1.0, 1.0): /////xgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABBRQAAAC8AAAACAAAABAAAAAAAAAAAwAAAGYwXwACAAAAVAAAAAQAAAC8////IAAAAAQAAAATAAAAeyJlbmNvZGluZyI6ICJXS1QifQAYAAAAQVJST1c6ZXh0ZW5zaW9uOm1ldGFkYXRhAAAAAAgADAAEAAgACAAAACgAAAAEAAAAGAAAAGdvb2dsZTpzcWxUeXBlOmdlb2dyYXBoeQAAAAAUAAAAQVJST1c6ZXh0ZW5zaW9uOm5hbWUAAAAABAAEAAQAAAAAAAAA/////5gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAABgAAAAAAAAAAAAKABgADAAEAAgACgAAAEwAAAAQAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAKAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAUE9JTlQoMSAxKQAAAAAAAP////8AAAAA + SELECT ST_GeogPoint(10, -1.5): /////xgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABBRQAAAC8AAAACAAAABAAAAAAAAAAAwAAAGYwXwACAAAAVAAAAAQAAAC8////IAAAAAQAAAATAAAAeyJlbmNvZGluZyI6ICJXS1QifQAYAAAAQVJST1c6ZXh0ZW5zaW9uOm1ldGFkYXRhAAAAAAgADAAEAAgACAAAACgAAAAEAAAAGAAAAGdvb2dsZTpzcWxUeXBlOmdlb2dyYXBoeQAAAAAUAAAAQVJST1c6ZXh0ZW5zaW9uOm5hbWUAAAAABAAEAAQAAAAAAAAA/////5gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAABgAAAAAAAAAAAAKABgADAAEAAgACgAAAEwAAAAQAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAOAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAUE9JTlQoMTAgLTEuNSkAAP////8AAAAA + SELECT ST_GeogPoint(NULL, 1): /////xgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABBRQAAAC8AAAACAAAABAAAAAAAAAAAwAAAGYwXwACAAAAVAAAAAQAAAC8////IAAAAAQAAAATAAAAeyJlbmNvZGluZyI6ICJXS1QifQAYAAAAQVJST1c6ZXh0ZW5zaW9uOm1ldGFkYXRhAAAAAAgADAAEAAgACAAAACgAAAAEAAAAGAAAAGdvb2dsZTpzcWxUeXBlOmdlb2dyYXBoeQAAAAAUAAAAQVJST1c6ZXh0ZW5zaW9uOm5hbWUAAAAABAAEAAQAAAAAAAAA/////5gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAABAAAAAAAAAAAAAKABgADAAEAAgACgAAAEwAAAAQAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA== + SELECT ST_GeogPoint(NULL, NULL): /////xgBAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAYAAAAAAASABgACAAGAAcADAAAABAAFAASAAAAAAABBRQAAAC8AAAACAAAABAAAAAAAAAAAwAAAGYwXwACAAAAVAAAAAQAAAC8////IAAAAAQAAAATAAAAeyJlbmNvZGluZyI6ICJXS1QifQAYAAAAQVJST1c6ZXh0ZW5zaW9uOm1ldGFkYXRhAAAAAAgADAAEAAgACAAAACgAAAAEAAAAGAAAAGdvb2dsZTpzcWxUeXBlOmdlb2dyYXBoeQAAAAAUAAAAQVJST1c6ZXh0ZW5zaW9uOm5hbWUAAAAABAAEAAQAAAAAAAAA/////5gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAABAAAAAAAAAAAAAKABgADAAEAAgACgAAAEwAAAAQAAAAAQAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA== + SELECT ST_Intersects(ST_GeogFromText('POINT (0 0)'), ST_GeogFromText('LINESTRING (0 0, 1 1)')): /////3AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEGEAAAABgAAAAEAAAAAAAAAAMAAABmMF8ABAAEAAQAAAAAAAAA/////4gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAAgAAAAAAAAAAAAKABgADAAEAAgACgAAADwAAAAQAAAAAQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAD/////AAAAAA== + SELECT ST_Intersects(ST_GeogFromText('POINT (0 0)'), ST_GeogFromText('POINT (0 0)')): /////3AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEGEAAAABgAAAAEAAAAAAAAAAMAAABmMF8ABAAEAAQAAAAAAAAA/////4gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAAgAAAAAAAAAAAAKABgADAAEAAgACgAAADwAAAAQAAAAAQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAD/////AAAAAA== + ? SELECT ST_Intersects(ST_GeogFromText('POINT (1 1)'), ST_GeogFromText('GEOMETRYCOLLECTION + (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1))')) + : /////3AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEGEAAAABgAAAAEAAAAAAAAAAMAAABmMF8ABAAEAAQAAAAAAAAA/////4gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAAgAAAAAAAAAAAAKABgADAAEAAgACgAAADwAAAAQAAAAAQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAD/////AAAAAA== + SELECT ST_Intersects(ST_GeogFromText('POINT EMPTY'), ST_GeogFromText('POINT (0 0)')): /////3AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEGEAAAABgAAAAEAAAAAAAAAAMAAABmMF8ABAAEAAQAAAAAAAAA/////4gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAAgAAAAAAAAAAAAKABgADAAEAAgACgAAADwAAAAQAAAAAQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA== + SELECT ST_Intersects(ST_GeogFromText('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))'), ST_GeogFromText('LINESTRING (0 0, 1 1)')): /////3AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEGEAAAABgAAAAEAAAAAAAAAAMAAABmMF8ABAAEAAQAAAAAAAAA/////4gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAAgAAAAAAAAAAAAKABgADAAEAAgACgAAADwAAAAQAAAAAQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAD/////AAAAAA== + ? SELECT ST_Intersects(ST_GeogFromText('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))'), + ST_GeogFromText('POLYGON ((5 5, 6 5, 6 6, 5 6, 5 5))')) + : /////3AAAAAQAAAAAAAKAAwABgAFAAgACgAAAAABBAAMAAAACAAIAAAABAAIAAAABAAAAAEAAAAUAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAEGEAAAABgAAAAEAAAAAAAAAAMAAABmMF8ABAAEAAQAAAAAAAAA/////4gAAAAUAAAAAAAAAAwAFgAGAAUACAAMAAwAAAAAAwQAGAAAAAgAAAAAAAAAAAAKABgADAAEAAgACgAAADwAAAAQAAAAAQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAA== diff --git a/python/sedonadb/tests/geography/test_constructors_parsers_formatters.py b/python/sedonadb/tests/geography/test_constructors_parsers_formatters.py new file mode 100644 index 000000000..84fe194c2 --- /dev/null +++ b/python/sedonadb/tests/geography/test_constructors_parsers_formatters.py @@ -0,0 +1,83 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 pytest +import sedonadb +from sedonadb.testing import BigQuery, PostGIS, SedonaDB, geog_or_null, val_or_null + +if "s2geography" not in sedonadb.__features__: + pytest.skip("Python package built without s2geography", allow_module_level=True) + + +@pytest.mark.parametrize("eng", [SedonaDB, BigQuery, PostGIS]) +@pytest.mark.parametrize( + ("x", "y", "expected"), + [ + (None, None, None), + (1, None, None), + (None, 1, None), + (1, 1, "POINT (1 1)"), + (1.0, 1.0, "POINT (1 1)"), + (10, -1.5, "POINT (10 -1.5)"), + ], +) +def test_st_geogpoint(eng, x, y, expected): + eng = eng.create_or_skip() + if eng.name() != "postgis": + eng.assert_query_result( + f"SELECT ST_GeogPoint({val_or_null(x)}, {val_or_null(y)})", expected + ) + else: + eng.assert_query_result( + f"SELECT ST_Point({val_or_null(x)}, {val_or_null(y)}) as geography", + expected, + ) + + +# Note that we can't test WKB output for BigQuery because it snaps vertices such that +# the exact output bytes are not identical. +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS]) +@pytest.mark.parametrize( + ("geom", "expected"), + [ + ( + "POINT (1 1)", + b"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\x3f\x00\x00\x00\x00\x00\x00\xf0\x3f", + ), + ( + "POINT EMPTY", + b"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf8\x7f\x00\x00\x00\x00\x00\x00\xf8\x7f", + ), + ( + "LINESTRING (0 0, 1 2, 3 4)", + b"\x01\x02\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\x3f\x00\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00\x00\x08\x40\x00\x00\x00\x00\x00\x00\x10\x40", + ), + ("LINESTRING EMPTY", b"\x01\x02\x00\x00\x00\x00\x00\x00\x00"), + ( + "POINT ZM (0 0 0 0)", + b"\x01\xb9\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ), + ( + "GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))", + b"\x01\x07\x00\x00\x00\x02\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ), + ], +) +def test_st_asbinary(eng, geom, expected): + eng = eng.create_or_skip() + eng.assert_query_result(f"SELECT ST_AsBinary({geog_or_null(geom)})", expected) diff --git a/python/sedonadb/tests/geography/test_geog_accessors.py b/python/sedonadb/tests/geography/test_geog_accessors.py new file mode 100644 index 000000000..360097f5c --- /dev/null +++ b/python/sedonadb/tests/geography/test_geog_accessors.py @@ -0,0 +1,51 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 pytest +import sedonadb +from sedonadb.testing import BigQuery, SedonaDB, geog_or_null + +if "s2geography" not in sedonadb.__features__: + pytest.skip("Python package built without s2geography", allow_module_level=True) + + +@pytest.mark.parametrize("eng", [SedonaDB, BigQuery]) +@pytest.mark.parametrize( + ("geog", "expected"), + [ + (None, None), + ("POINT EMPTY", 0.0), + ("LINESTRING EMPTY", 0.0), + ("POLYGON EMPTY", 0.0), + ("MULTIPOINT EMPTY", 0.0), + ("MULTILINESTRING EMPTY", 0.0), + ("MULTIPOLYGON EMPTY", 0.0), + ("GEOMETRYCOLLECTION EMPTY", 0.0), + ("POINT (5 2)", 0.0), + ("MULTIPOINT ((0 0), (1 1))", 0.0), + ("LINESTRING (0 0, 1 1)", 0.0), + ("MULTILINESTRING ((0 0, 1 1), (1 1, 2 2))", 0.0), + ("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 12364036567.076418), + ( + "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((10 10, 11 10, 11 11, 10 11, 10 10)))", + 24521468442.943977, + ), + ], +) +def test_st_area(eng, geog, expected): + eng = eng.create_or_skip() + eng.assert_query_result(f"SELECT ST_Area({geog_or_null(geog)})", expected) diff --git a/python/sedonadb/tests/geography/test_geog_measures.py b/python/sedonadb/tests/geography/test_geog_measures.py new file mode 100644 index 000000000..bcf232295 --- /dev/null +++ b/python/sedonadb/tests/geography/test_geog_measures.py @@ -0,0 +1,53 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 pytest +from sedonadb.testing import BigQuery, PostGIS, SedonaDB, geog_or_null +import sedonadb + +if "s2geography" not in sedonadb.__features__: + pytest.skip("Python package built without s2geography", allow_module_level=True) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS, BigQuery]) +@pytest.mark.parametrize( + ("geom1", "geom2", "expected"), + [ + (None, None, None), + # Single arg nulls are not handled by SedonaDB until upgrade + # ("POINT (0 0)", None, None), + # (None, "POINT (0 0)", None), + ("POINT (0 0)", "POINT (0 0)", 0), + ( + "POINT(-72.1235 42.3521)", + "LINESTRING(-72.1260 42.45, -72.123 42.1546)", + 123.47576072749062, # 123.80207675 + ), + ( + "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", + "POLYGON ((5 5, 6 5, 6 6, 5 6, 5 5))", + 628519.1549911008, # 627129.50261075 + ), + ], +) +def test_st_distance(eng, geom1, geom2, expected): + eng = eng.create_or_skip() + eng.assert_query_result( + f"SELECT ST_Distance({geog_or_null(geom1)}, {geog_or_null(geom2)})", + expected, + numeric_epsilon=1e-2, + ) diff --git a/python/sedonadb/tests/geography/test_geog_predicates.py b/python/sedonadb/tests/geography/test_geog_predicates.py new file mode 100644 index 000000000..a08e4d382 --- /dev/null +++ b/python/sedonadb/tests/geography/test_geog_predicates.py @@ -0,0 +1,51 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 pytest +import sedonadb +from sedonadb.testing import BigQuery, PostGIS, SedonaDB, geog_or_null + +if "s2geography" not in sedonadb.__features__: + pytest.skip("Python package built without s2geography", allow_module_level=True) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS, BigQuery]) +@pytest.mark.parametrize( + ("geom1", "geom2", "expected"), + [ + ("POINT (0 0)", "POINT (0 0)", True), + ("POINT EMPTY", "POINT (0 0)", False), + ("POINT (0 0)", "LINESTRING (0 0, 1 1)", True), + ("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "LINESTRING (0 0, 1 1)", True), + ( + "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", + "POLYGON ((5 5, 6 5, 6 6, 5 6, 5 5))", + False, + ), + ( + "POINT (1 1)", + "GEOMETRYCOLLECTION (POINT (0 0), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1))", + True, + ), + ], +) +def test_st_intersects(eng, geom1, geom2, expected): + eng = eng.create_or_skip() + eng.assert_query_result( + f"SELECT ST_Intersects({geog_or_null(geom1)}, {geog_or_null(geom2)})", + expected, + ) diff --git a/python/sedonadb/tests/geography/test_geog_transformations.py b/python/sedonadb/tests/geography/test_geog_transformations.py new file mode 100644 index 000000000..741c9bde7 --- /dev/null +++ b/python/sedonadb/tests/geography/test_geog_transformations.py @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 pytest +from sedonadb.testing import BigQuery, PostGIS, SedonaDB, geog_or_null +import sedonadb + +if "s2geography" not in sedonadb.__features__: + pytest.skip("Python package built without s2geography", allow_module_level=True) + + +@pytest.mark.parametrize("eng", [SedonaDB, PostGIS, BigQuery]) +@pytest.mark.parametrize( + ("geom", "expected"), + [ + ("POINT (0 0)", "POINT (0 0)"), + ("LINESTRING (0 0, 0 1)", "POINT (0 0.5)"), + ("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", "POINT (0.5 0.5)"), + ], +) +def test_st_centroid(eng, geom, expected): + eng = eng.create_or_skip() + eng.assert_query_result( + f"SELECT ST_Centroid({geog_or_null(geom)})", expected, wkt_precision=4 + ) diff --git a/rust/sedona-functions/src/st_xyzm.rs b/rust/sedona-functions/src/st_xyzm.rs index 66013f6b6..4621b904c 100644 --- a/rust/sedona-functions/src/st_xyzm.rs +++ b/rust/sedona-functions/src/st_xyzm.rs @@ -134,15 +134,13 @@ fn invoke_scalar(item: &Wkb, dim_index: usize) -> Result> { let coord = PointTrait::coord(point); return get_coord(coord_dim, coord, dim_index); } - geo_traits::GeometryType::LineString(linestring) => { - if LineStringTrait::num_coords(linestring) == 0 { - return Ok(None); - } + geo_traits::GeometryType::LineString(linestring) + if LineStringTrait::num_coords(linestring) == 0 => + { + return Ok(None); } - geo_traits::GeometryType::Polygon(polygon) => { - if PolygonTrait::exterior(polygon).is_none() { - return Ok(None); - } + geo_traits::GeometryType::Polygon(polygon) if PolygonTrait::exterior(polygon).is_none() => { + return Ok(None); } geo_traits::GeometryType::MultiPoint(multipoint) => { match MultiPointTrait::num_points(multipoint) { @@ -157,20 +155,20 @@ fn invoke_scalar(item: &Wkb, dim_index: usize) -> Result> { _ => {} } } - geo_traits::GeometryType::MultiLineString(multilinestring) => { - if MultiLineStringTrait::num_line_strings(multilinestring) == 0 { - return Ok(None); - } + geo_traits::GeometryType::MultiLineString(multilinestring) + if MultiLineStringTrait::num_line_strings(multilinestring) == 0 => + { + return Ok(None); } - geo_traits::GeometryType::MultiPolygon(multipolygon) => { - if MultiPolygonTrait::num_polygons(multipolygon) == 0 { - return Ok(None); - } + geo_traits::GeometryType::MultiPolygon(multipolygon) + if MultiPolygonTrait::num_polygons(multipolygon) == 0 => + { + return Ok(None); } - geo_traits::GeometryType::GeometryCollection(geometrycollection) => { - if geometrycollection.num_geometries() == 0 { - return Ok(None); - } + geo_traits::GeometryType::GeometryCollection(geometrycollection) + if geometrycollection.num_geometries() == 0 => + { + return Ok(None); } _ => {} }; diff --git a/rust/sedona-geo/src/st_asgeojson.rs b/rust/sedona-geo/src/st_asgeojson.rs index 7475bc7ad..8e0391b37 100644 --- a/rust/sedona-geo/src/st_asgeojson.rs +++ b/rust/sedona-geo/src/st_asgeojson.rs @@ -84,17 +84,13 @@ impl SedonaScalarKernel for STAsGeoJSON { fn geom_to_geojson(geom: &Wkb) -> Result { // Special case handling for geometries that geo_types::Geometry cannot represent match geom.as_type() { - geo_traits::GeometryType::Point(pt) => { - if pt.coord().is_none() { - // Empty point - geo_types cannot represent this - return Ok(r#"{"type":"Point","coordinates":[]}"#.to_string()); - } + geo_traits::GeometryType::Point(pt) if pt.coord().is_none() => { + // Empty point - geo_types cannot represent this + return Ok(r#"{"type":"Point","coordinates":[]}"#.to_string()); } - geo_traits::GeometryType::Polygon(poly) => { - if poly.exterior().is_none() { - // Empty polygon - to match PostGIS behavior - return Ok(r#"{"type":"Polygon","coordinates":[]}"#.to_string()); - } + geo_traits::GeometryType::Polygon(poly) if poly.exterior().is_none() => { + // Empty polygon - to match PostGIS behavior + return Ok(r#"{"type":"Polygon","coordinates":[]}"#.to_string()); } _ => {} } diff --git a/rust/sedona-query-planner/src/spatial_expr_utils.rs b/rust/sedona-query-planner/src/spatial_expr_utils.rs index 2c1a18d2b..0aa581791 100644 --- a/rust/sedona-query-planner/src/spatial_expr_utils.rs +++ b/rust/sedona-query-planner/src/spatial_expr_utils.rs @@ -47,15 +47,11 @@ pub(crate) fn collect_spatial_predicate_names(expr: &Expr) -> HashSet { collect(left, acc); collect(right, acc); } - Operator::Lt | Operator::LtEq => { - if is_distance_expr(left) { - acc.insert("st_dwithin".to_string()); - } + Operator::Lt | Operator::LtEq if is_distance_expr(left) => { + acc.insert("st_dwithin".to_string()); } - Operator::Gt | Operator::GtEq => { - if is_distance_expr(right) { - acc.insert("st_dwithin".to_string()); - } + Operator::Gt | Operator::GtEq if is_distance_expr(right) => { + acc.insert("st_dwithin".to_string()); } _ => (), }, diff --git a/rust/sedona-spatial-join/src/partitioning/stream_repartitioner.rs b/rust/sedona-spatial-join/src/partitioning/stream_repartitioner.rs index 8d86d40b8..46ed50833 100644 --- a/rust/sedona-spatial-join/src/partitioning/stream_repartitioner.rs +++ b/rust/sedona-spatial-join/src/partitioning/stream_repartitioner.rs @@ -546,8 +546,8 @@ impl StreamRepartitioner { for ((writer_opt, accumulator), num_rows) in self .spill_registry .into_iter() - .zip(self.geo_stats_accumulators.into_iter()) - .zip(self.num_rows.into_iter()) + .zip(self.geo_stats_accumulators) + .zip(self.num_rows) { let spilled_partition = if let Some(writer) = writer_opt { let spill_files = vec![Arc::new(writer.finish()?)]; diff --git a/rust/sedona-spatial-join/src/utils/join_utils.rs b/rust/sedona-spatial-join/src/utils/join_utils.rs index 9c6565e91..c97ee1796 100644 --- a/rust/sedona-spatial-join/src/utils/join_utils.rs +++ b/rust/sedona-spatial-join/src/utils/join_utils.rs @@ -669,7 +669,7 @@ fn append_probe_indices_in_order( for (build_index, probe_index) in build_indices .values() .into_iter() - .zip(probe_indices.values().into_iter()) + .zip(probe_indices.values()) { // Append values between previous and current probe index with null build index: for value in prev_index..*probe_index { diff --git a/rust/sedona-testing/src/compare.rs b/rust/sedona-testing/src/compare.rs index 780b2ff96..0f04df252 100644 --- a/rust/sedona-testing/src/compare.rs +++ b/rust/sedona-testing/src/compare.rs @@ -267,16 +267,14 @@ fn compare_wkb_topologically( let expected = wkb::reader::read_wkb(expected_wkb); let actual = wkb::reader::read_wkb(actual_wkb); match (expected, actual) { - (Ok(expected_geom), Ok(actual_geom)) => { - if expected_geom.dim() == Dimensions::Xy && actual_geom.dim() == Dimensions::Xy { - let expected_geom = expected_geom.to_geometry(); - let actual_geom = actual_geom.to_geometry(); - expected_geom.relate(&actual_geom).is_equal_topo() - } else { - // geo crate does not support 3D/4D geometry operations, so we fall back to using the result - // of byte-wise comparison - false - } + // geo crate does not support 3D/4D geometry operations, so we fall back to using the result + // of byte-wise comparison + (Ok(expected_geom), Ok(actual_geom)) + if expected_geom.dim() == Dimensions::Xy && actual_geom.dim() == Dimensions::Xy => + { + let expected_geom = expected_geom.to_geometry(); + let actual_geom = actual_geom.to_geometry(); + expected_geom.relate(&actual_geom).is_equal_topo() } _ => false, }