jiayuasu commented on code in PR #1044:
URL: https://github.com/apache/sedona-db/pull/1044#discussion_r3565548206
##########
python/sedonadb/python/sedonadb/dataframe.py:
##########
@@ -1566,6 +1566,64 @@ def to_parquet(
single_file_output,
)
+ def to_csv(
+ self,
+ path: Union[str, Path],
+ *,
+ has_header: bool = True,
+ delimiter: str = ",",
+ ):
+ """Write this DataFrame to CSV
+
+ This is a plain tabular writer: every column is written using its CSV
+ text representation. It is not spatial-aware, so a geometry column is
+ written as hex-encoded WKB rather than as readable geometry (call
+ `ST_AsText()` first for WKT). To write geometry to a spatial format
such
+ as GeoJSON, FlatGeobuf, or GeoPackage, use `to_pyogrio()` instead.
+
+ A path ending in `.csv` is written as a single file; any other path is
+ treated as a directory and written as one CSV file per partition.
+
+ Args:
+ path: A filename or directory to which CSV output should be
written.
+ has_header: Whether to write the column names as the first row.
+ delimiter: The single-byte field delimiter to use between values.
+
+ Examples:
+
+ >>> import tempfile
+ >>> sd = sedona.db.connect()
+ >>> td = tempfile.TemporaryDirectory()
+ >>> sd.sql("SELECT 1 AS a, 'x' AS b").to_csv(f"{td.name}/tmp.csv")
+
+ """
+ self._impl.to_csv(str(Path(path)), has_header, delimiter)
Review Comment:
Good catch — fixed. Switched to `str(path)` so URI-style paths (`s3://`,
`gs://`) pass through unchanged; `Path()` was collapsing the `//`. This also
matches the reader side, where `read.py` uses `str(path)`.
##########
python/sedonadb/python/sedonadb/dataframe.py:
##########
@@ -1566,6 +1566,64 @@ def to_parquet(
single_file_output,
)
+ def to_csv(
+ self,
+ path: Union[str, Path],
+ *,
+ has_header: bool = True,
+ delimiter: str = ",",
+ ):
+ """Write this DataFrame to CSV
+
+ This is a plain tabular writer: every column is written using its CSV
+ text representation. It is not spatial-aware, so a geometry column is
+ written as hex-encoded WKB rather than as readable geometry (call
+ `ST_AsText()` first for WKT). To write geometry to a spatial format
such
+ as GeoJSON, FlatGeobuf, or GeoPackage, use `to_pyogrio()` instead.
+
+ A path ending in `.csv` is written as a single file; any other path is
+ treated as a directory and written as one CSV file per partition.
+
+ Args:
+ path: A filename or directory to which CSV output should be
written.
+ has_header: Whether to write the column names as the first row.
+ delimiter: The single-byte field delimiter to use between values.
+
+ Examples:
+
+ >>> import tempfile
+ >>> sd = sedona.db.connect()
+ >>> td = tempfile.TemporaryDirectory()
+ >>> sd.sql("SELECT 1 AS a, 'x' AS b").to_csv(f"{td.name}/tmp.csv")
+
+ """
+ self._impl.to_csv(str(Path(path)), has_header, delimiter)
+
+ def to_json(self, path: Union[str, Path]):
+ """Write this DataFrame to newline-delimited JSON
+
+ This is a plain tabular writer that emits one JSON object per row
+ (NDJSON). It is not spatial-aware and does not produce GeoJSON: a
+ geometry column is written as a hex-encoded WKB string rather than as
+ GeoJSON geometry. To write GeoJSON, use `to_pyogrio()` with a
+ `.geojson` path instead.
+
+ A path ending in `.json` is written as a single file; any other path is
+ treated as a directory and written as one JSON file per partition.
+
+ Args:
+ path: A filename or directory to which JSON output should be
written.
+
+ Examples:
+
+ >>> import tempfile
+ >>> sd = sedona.db.connect()
+ >>> td = tempfile.TemporaryDirectory()
+ >>> sd.sql("SELECT 1 AS a, 'x' AS
b").to_json(f"{td.name}/tmp.json")
+
+ """
+ self._impl.to_json(str(Path(path)))
Review Comment:
Fixed here too — `str(path)` instead of `str(Path(path))` to keep
`s3://`/`gs://` URLs intact.
##########
python/sedonadb/tests/io/test_write_csv_json.py:
##########
@@ -0,0 +1,97 @@
+# 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 tempfile
+from pathlib import Path
+
+import pandas as pd
+import pandas.testing as pdt
+import pytest
+
+
+def test_to_csv_round_trip(con):
+ with tempfile.TemporaryDirectory() as td:
+ p = Path(td) / "out.csv"
+ con.sql("SELECT 1 AS a, 'x' AS b UNION ALL SELECT 2, 'y'").to_csv(p)
+ # A single file is written when the path ends with ".csv".
+ assert p.is_file()
+ out = con.read.csv(p).sort("a").to_pandas()
+ pdt.assert_frame_equal(out, pd.DataFrame({"a": [1, 2], "b": ["x", "y"]}))
+
+
+def test_to_csv_no_header(con):
+ with tempfile.TemporaryDirectory() as td:
+ p = Path(td) / "out.csv"
+ con.sql("SELECT 1 AS a, 2 AS b").to_csv(p, has_header=False)
+ assert p.read_text() == "1,2\n"
+
+
+def test_to_csv_custom_delimiter(con):
+ with tempfile.TemporaryDirectory() as td:
+ p = Path(td) / "out.csv"
+ con.sql("SELECT 1 AS a, 'x' AS b").to_csv(p, delimiter=";")
+ assert p.read_text() == "a;b\n1;x\n"
+ # And it round-trips when read back with the same delimiter.
+ out = con.read.csv(p, delimiter=";").to_pandas()
+ pdt.assert_frame_equal(out, pd.DataFrame({"a": [1], "b": ["x"]}))
+
+
+def test_to_csv_bad_delimiter_raises(con):
+ from sedonadb._lib import SedonaError
+
+ with tempfile.TemporaryDirectory() as td:
+ p = Path(td) / "out.csv"
+ with pytest.raises(SedonaError, match="single byte"):
+ con.sql("SELECT 1 AS a").to_csv(p, delimiter=";;")
+
+
+def test_to_csv_directory_output(con):
+ # A path without a ".csv" suffix writes a directory of part file(s).
+ with tempfile.TemporaryDirectory() as td:
+ d = Path(td) / "parts"
+ con.sql("SELECT 1 AS a").to_csv(d)
+ assert d.is_dir()
+ assert list(d.glob("*.csv"))
+
+
+def test_to_csv_geometry_written_as_wkb(con):
+ # to_csv is not spatial-aware: a geometry column is written as hex-encoded
+ # WKB, not as WKT/GeoJSON. (Use ST_AsText() or to_pyogrio() for geometry.)
+ with tempfile.TemporaryDirectory() as td:
+ p = Path(td) / "geo.csv"
+ con.sql("SELECT ST_Point(1.0, 2.0) AS geometry, 'a' AS name").to_csv(p)
+ text = p.read_text()
+ assert "POINT" not in text
+ # WKB for a 2D point starts with byte order (01) + geometry type
(01000000).
+ assert "0101000000" in text
+
+
+def test_to_json_round_trip(con):
+ with tempfile.TemporaryDirectory() as td:
+ p = Path(td) / "out.json"
+ con.sql("SELECT 1 AS a, 'x' AS b UNION ALL SELECT 2, 'y'").to_json(p)
+ assert p.is_file()
+ out = con.read.json(p).sort("a").to_pandas()
+ pdt.assert_frame_equal(out, pd.DataFrame({"a": [1, 2], "b": ["x", "y"]}))
+
+
+def test_to_json_ndjson_format(con):
+ # to_json emits one JSON object per line (NDJSON).
+ with tempfile.TemporaryDirectory() as td:
+ p = Path(td) / "out.json"
+ con.sql("SELECT 1 AS a, 'x' AS b").to_json(p)
+ assert p.read_text() == '{"a":1,"b":"x"}\n'
Review Comment:
Done — the NDJSON test now parses each line with `json.loads` and asserts on
the resulting objects plus the line count, instead of matching the raw
serialized bytes.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]