> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wherobots.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Geometry and Geography Tables

> Create, write, and query Apache Iceberg tables with GEOMETRY and GEOGRAPHY columns in WherobotsDB.

WherobotsDB stores tables in Apache Iceberg and supports Iceberg format-version 3 (V3). New tables in Wherobots-managed catalogs, such as `org_catalog`, are V3 tables. Tables created before September 15, 2026 were created at format-version 2 (V2), and Wherobots doesn't raise them to V3. To check a table, run `SHOW TBLPROPERTIES <table> ('format-version')`. Geometry and geography columns declared with a CRS, such as `GEOMETRY(4326)`, use the V3 `geometry` and `geography` types. Wherobots adds a `raster` column type and the `CREATE SPATIAL INDEX` statement, which rewrites a table's data files clustered by a spatial column.

On V3 tables, merge-on-read `DELETE`, `UPDATE`, and `MERGE` write deletion vectors, the row lineage columns `_row_id` and `_last_updated_sequence_number` can be queried, and `variant` columns can be written and read. Column default values can't be set, and a table with a `timestamp_ns` column can't be read.

This document describes how to create, write, and query tables with geometry and geography columns.

> [!NOTE]
>
> Tables created before September 15, 2026, and tables created from the result of an `ST_` function, have geometry columns with no declared CRS. To check a table and upgrade it, see [Upgrade Existing Geometry Tables](https://docs.wherobots.com/data-engineering/iceberg-tables/upgrade-geometry-tables.md).

> **Readable as geometry by other engines**
>
> The geometry type is recorded in the table schema and in the Parquet files, so an Apache Iceberg engine outside Wherobots that supports geospatial types sees a geometry column instead of a binary column it has to decode itself.

> **One CRS per column, enforced**
>
> A column carries exactly one Coordinate Reference System (CRS). A value whose Spatial Reference Identifier (SRID) disagrees with it is rejected instead of being stored under the wrong CRS.

> [!NOTE]
>
> `org_catalog` throughout this page is an example catalog name. Replace it with the name of the catalog you are writing to.

## Creating Table with Geometry Column

Apache Iceberg tables have a `GEOMETRY` data type to represent geospatial data. Declare the column with its CRS in parentheses, as an SRID:

**Python**

```python theme={"system"}
sedona.sql("""
CREATE TABLE IF NOT EXISTS org_catalog.test_db.test_table (
    id bigint,
    data string,
    geom geometry(4326)
) USING iceberg
PARTITIONED BY (bucket(4, id))""")
```

**Scala**

```scala theme={"system"}
sedona.sql("""
CREATE TABLE IF NOT EXISTS org_catalog.test_db.test_table (
    id bigint,
    data string,
    geom geometry(4326)
) USING iceberg
PARTITIONED BY (bucket(4, id))""")
```

**Java**

```java theme={"system"}
sedona.sql(
    "CREATE TABLE IF NOT EXISTS org_catalog.test_db.test_table (" +
    "id bigint," +
    "data string," +
    "geom geometry(4326)" +
    ") USING iceberg " +
    "PARTITIONED BY (bucket(4, id))"
);
```

This will create an empty table partitioned by the bucketed value of `id`, with a geometry column in SRID 4326, which is World Geodetic System 1984 (WGS84) longitude and latitude. A geometry column requires a declared CRS: write `geometry(4326)`, not `geometry`.

Currently, a `geometry(<srid>)` column can declare SRID 0, 3857, or 4326, and a `geography(<srid>)` column can declare 4326. Declaring another SRID, such as 4269 or 32633, fails with `ST_INVALID_SRID_VALUE`. For data in another CRS, see [Data in another CRS](#data-in-another-crs).

`USING iceberg` names the Apache Iceberg data source. `havasu.iceberg` is an accepted alias for it, in `USING` and in `format(...)`.

A table with a `geometry(<srid>)` or `geography(<srid>)` column is Apache Iceberg format-version 3, which is the default for new tables, so leave `'format-version'` out of `TBLPROPERTIES`.

> **Error: ... is not supported until v3**
>
> The table was created with `TBLPROPERTIES ('format-version'='2')`. Remove the property:
>
> ```
> Invalid schema for v2: - Invalid type for geom: geometry(OGC:CRS84) is not supported until v3
> ```

> **Error: bare GEOMETRY in a column definition does not declare a CRS**
>
> The column was declared as `geometry` with no CRS in parentheses. Add the SRID:
>
> ```
> Operation not allowed: bare GEOMETRY in a column definition does not declare a CRS:
> with spark.sql.geospatial.enabled=true it would silently create a Havasu-legacy column
> with no CRS enforcement. Declare a CRS (e.g. GEOMETRY(4326)) to create an Iceberg V3
> native column, or write the table from a Sedona GeometryUDT DataFrame if a Havasu-legacy
> column is intended.
> ```

We can inspect the table schema using `DESCRIBE` command:

**Python**

```python theme={"system"}
sedona.sql("DESCRIBE TABLE org_catalog.test_db.test_table").show()
```

**Scala**

```scala theme={"system"}
sedona.sql("DESCRIBE TABLE org_catalog.test_db.test_table").show()
```

**Java**

```java theme={"system"}
sedona.sql("DESCRIBE TABLE org_catalog.test_db.test_table").show();
```

```
+--------------+-------------+-------+
|      col_name|    data_type|comment|
+--------------+-------------+-------+
|            id|       bigint|   NULL|
|          data|       string|   NULL|
|          geom|     geometry|   NULL|
|              |             |       |
|# Partitioning|             |       |
|        Part 0|bucket(4, id)|       |
+--------------+-------------+-------+
```

`DESCRIBE TABLE` shows the type as `geometry` without the CRS. To see the CRS, run `SHOW CREATE TABLE`, which renders the column as `geom GEOMETRY(4326)`:

**Python**

```python theme={"system"}
print(sedona.sql("SHOW CREATE TABLE org_catalog.test_db.test_table").collect()[0][0])
```

**Scala**

```scala theme={"system"}
println(sedona.sql("SHOW CREATE TABLE org_catalog.test_db.test_table").collect()(0).getString(0))
```

**Java**

```java theme={"system"}
System.out.println(sedona.sql("SHOW CREATE TABLE org_catalog.test_db.test_table").collectAsList().get(0).getString(0));
```

```
CREATE TABLE org_catalog.test_db.test_table (
id BIGINT,
data STRING,
geom GEOMETRY(4326))
USING iceberg
PARTITIONED BY (bucket(4, id))
...
```

> [!TIP]
>
> The statement `SHOW CREATE TABLE` returns recreates an equivalent table, so it is also the way to clone the schema of an existing table.

> [!NOTE]
>
> A geometry column created without a CRS, which is how tables were created before `geometry(<srid>)` was available, renders as `geom GEOMETRY` with no parentheses in `SHOW CREATE TABLE`. `DESCRIBE TABLE` shows `geometry` for both kinds of column. To see a table's Apache Iceberg format version, run `SHOW TBLPROPERTIES org_catalog.test_db.test_table ('format-version')`.
>
> Tables created without a CRS keep working: reads, writes, and every `ST_` function behave the same. Spatial filters on such a column skip data files only in a format-version 1 or 2 table; in a format-version 3 table, they read every data file. These columns do not check the SRID of written values, and other Iceberg engines read them as binary. Wherobots will upgrade the read-only public tables in the `wherobots_open_data` and `wherobots_pro_data` catalogs. To upgrade your own tables, see [Upgrade Existing Geometry Tables](https://docs.wherobots.com/data-engineering/iceberg-tables/upgrade-geometry-tables.md).

### Data in another CRS

Until more SRIDs can be declared, there are three ways to store data in another CRS, such as a Universal Transverse Mercator (UTM) zone. Spatial filters skip data files with all three:

| Option | Keeps the coordinates | Records the CRS | Format version |
| :- | :- | :- | :- |
| Reproject with `ST_Transform`, and declare `geometry(4326)` or `geometry(3857)` | No | Yes, in the column | 3 |
| Declare `geometry(0)`, and write the values with SRID 0 | Yes | No | 3 |
| Keep a column with no declared CRS, in a table created at format-version 2 | Yes | Yes, in each value | 2 |

> [!WARNING]
>
> Don't keep a column with no declared CRS in a format-version 3 table. That is what `CREATE TABLE ... AS SELECT` and `writeTo(...).create()` give you by default when the geometry comes from an `ST_` function, and spatial filters on that column read every data file. Nothing reports it: queries return correct results, only more slowly.

To use `geometry(0)`, create the table, then set each value's SRID to 0 as you write it:

```sql theme={"system"}
CREATE TABLE org_catalog.test_db.parcels_utm (
    id bigint,
    geom geometry(0)
) USING iceberg;

INSERT INTO org_catalog.test_db.parcels_utm
SELECT id, ST_SetSRID(geom, 0) FROM org_catalog.test_db.parcels_source;
```

Writing a value that still carries its SRID fails with `Cannot write geometry with SRID 32633 into a column with SRID 0`. Build query shapes with SRID 0 too, for example with `ST_GeomFromText` and no SRID argument. Distances and areas come out in the CRS's units, meters for UTM. The table doesn't record the CRS, so keep track of it yourself, and label the values before you reproject them: `ST_Transform(ST_SetSRID(geom, 32633), 'EPSG:4326')`.

To use a format-version 2 table, create it from a query or a DataFrame with the format version set. `CREATE TABLE` with a `geometry` column and no SRID is rejected, even at format-version 2.

```sql theme={"system"}
CREATE TABLE org_catalog.test_db.parcels_utm
USING iceberg
TBLPROPERTIES ('format-version'='2')
AS SELECT id, ST_Transform(geom, 'EPSG:32633') AS geom FROM org_catalog.test_db.parcels_source;
```

From a DataFrame, use `df.writeTo("org_catalog.test_db.parcels_utm").tableProperty("format-version", "2").create()`. Later writes with `INSERT INTO` or `writeTo(...).append()` keep the table at format-version 2. Each value keeps its own SRID, which the column doesn't check. A format-version 2 table can't use format-version 3 features or take a `geometry(<srid>)` column. When your SRID can be declared, copy the table as described in [Upgrade Existing Geometry Tables](https://docs.wherobots.com/data-engineering/iceberg-tables/upgrade-geometry-tables.md).

## Writing Data

### INSERT INTO

User can insert data into a table using `INSERT INTO table_name VALUES`. Every value has to carry the SRID of the column:

**Python**

```python theme={"system"}
sedona.sql("""
INSERT INTO org_catalog.test_db.test_table
VALUES (1, 'a', ST_SetSRID(ST_GeomFromText('POINT (1 2)'), 4326)),
       (2, 'b', ST_SetSRID(ST_Point(2, 3), 4326))
""")
```

**Scala**

```scala theme={"system"}
sedona.sql("""
INSERT INTO org_catalog.test_db.test_table
VALUES (1, 'a', ST_SetSRID(ST_GeomFromText('POINT (1 2)'), 4326)),
       (2, 'b', ST_SetSRID(ST_Point(2, 3), 4326))
""")
```

**Java**

```java theme={"system"}
sedona.sql(
        "INSERT INTO org_catalog.test_db.test_table " +
        "VALUES (1, 'a', ST_SetSRID(ST_GeomFromText('POINT (1 2)'), 4326)), " +
        "(2, 'b', ST_SetSRID(ST_Point(2, 3), 4326))"
);
```

Or using `INSERT INTO table_name SELECT ...` to insert result set of a query into the table:

**Python**

```python theme={"system"}
sedona.sql("""
INSERT INTO org_catalog.test_db.test_table
SELECT 3, 'c', ST_SetSRID(ST_Point(3, 4), 4326)
""")
```

**Scala**

```scala theme={"system"}
sedona.sql("""
INSERT INTO org_catalog.test_db.test_table
SELECT 3, 'c', ST_SetSRID(ST_Point(3, 4), 4326)
""")
```

**Java**

```java theme={"system"}
sedona.sql(
        "INSERT INTO org_catalog.test_db.test_table " +
        "SELECT 3, 'c', ST_SetSRID(ST_Point(3, 4), 4326)"
);
```

### Matching the column CRS

Geometry constructors such as `ST_GeomFromText` and `ST_Point` return SRID 0, which is a distinct CRS rather than "unset", so a `geometry(4326)` column rejects it. Which function you need depends on the SRID your values already carry:

| Your values | Apply | Effect |
| :- | :- | :- |
| SRID 0, from a constructor, from shapely, or from a file with no CRS metadata | `ST_SetSRID(geom, 4326)` | Labels the values. Coordinates are unchanged. |
| A different CRS, such as SRID 3857 | `ST_Transform(geom, 'EPSG:3857', 'EPSG:4326')` | Reprojects the coordinates. |
| Already SRID 4326 | Nothing | Insert as-is. |
| A `geometry(4326)` column on another table | Nothing | The CRS carries through the read. |

`ST_SetSRID` relabels without touching the coordinates. Applying it to metre coordinates declares them as longitude and latitude, which puts the data in the wrong place. Use `ST_Transform` when the coordinates are in another CRS.

> **Error: Cannot write geometry with SRID ...**
>
> A value disagrees with the column CRS. The message names the fix:
>
> ```
> java.lang.IllegalArgumentException: Cannot write geometry with SRID 0 into a column
> with SRID 4326: an Iceberg geometry column stores one CRS for every value. Use
> ST_SetSRID to label an unset SRID, or ST_Transform to reproject the values to SRID 4326.
> ```

### Writing a DataFrame to a table

User can write a DataFrame containing geometry data to a table. The same SRID rule applies to the DataFrame's values:

**Python**

```python theme={"system"}
from pyspark.sql.functions import expr

# Create a DataFrame with geometry data
df = sedona.createDataFrame([
    (1, "a", "POINT (1 2)"),
    (2, "b", "POINT (2 3)")
], ["id", "data", "geom"]) \
    .withColumn("geom", expr("ST_SetSRID(ST_GeomFromText(geom), 4326)"))

# Write the DataFrame to the table
df.writeTo("org_catalog.test_db.test_table").append()
```

**Scala**

```scala theme={"system"}
// Create a DataFrame with geometry data
val df = Seq(
    (1, "a", "POINT (1 2)"),
    (2, "b", "POINT (2 3)")
).toDF("id", "data", "geom")
.withColumn("geom", expr("ST_SetSRID(ST_GeomFromText(geom), 4326)"))

// Write the DataFrame to the table
df.writeTo("org_catalog.test_db.test_table").append()
```

**Java**

```java theme={"system"}
List<Row> data = Arrays.asList(
    RowFactory.create(1, "a", "POINT (1 2)"),
    RowFactory.create(2, "b", "POINT (2 3)")
);

List<StructField> fields = Arrays.asList(
    DataTypes.createStructField("id", DataTypes.IntegerType, true),
    DataTypes.createStructField("data", DataTypes.StringType, true),
    DataTypes.createStructField("geom", DataTypes.StringType, true)
);

StructType schema = DataTypes.createStructType(fields);

Dataset<Row> df = sedona.createDataFrame(data, schema)
    .withColumn("geom", expr("ST_SetSRID(ST_GeomFromText(geom), 4326)"));

df.writeTo("org_catalog.test_db.test_table").append();
```

This is semantically equivalent to the `INSERT INTO table_name SELECT ...` statement.

### Creating a table from a query or a DataFrame

`CREATE TABLE ... AS SELECT`, `writeTo(...).create()`, and `writeTo(...).createOrReplace()` create a table from the result of a query or a DataFrame. When the geometry comes from a `geometry(<srid>)` column read from a table, the new column has the same declared CRS. When the geometry is the result of an `ST_` function, such as `ST_Point`, `ST_SetSRID`, or `ST_Buffer`, the new column has no declared CRS. In a Wherobots-managed catalog, such as `org_catalog`, the new table is format-version 3 unless you set `'format-version'`, and in a format-version 3 table, spatial filters on a column with no declared CRS read every data file. To declare the CRS of the new column, create the table with `CREATE TABLE` first and then write to it with `INSERT INTO ... SELECT` or `writeTo(...).append()`, as shown in [Working with Geometry Data](#working-with-geometry-data).

### Updating data

Iceberg tables support UPDATE queries that update matching rows. Update queries accept a filter to match rows to update, including spatial filters.

**Python**

```python theme={"system"}
sedona.sql("UPDATE org_catalog.test_db.test_table SET data = 'd' WHERE id = 1")

# Use ST_Intersects to update rows that intersect with a polygon
sedona.sql("""
UPDATE org_catalog.test_db.test_table
SET data = 'd'
WHERE ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))
""")
```

**Scala**

```scala theme={"system"}
sedona.sql("UPDATE org_catalog.test_db.test_table SET data = 'd' WHERE id = 1")

// Use ST_Intersects to update rows that intersect with a polygon
sedona.sql("""
UPDATE org_catalog.test_db.test_table
SET data = 'd'
WHERE ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))
""")
```

**Java**

```java theme={"system"}
sedona.sql("UPDATE org_catalog.test_db.test_table SET data = 'd' WHERE id = 1");

// Use ST_Intersects to update rows that intersect with a polygon
sedona.sql(
        "UPDATE org_catalog.test_db.test_table " +
        "SET data = 'd' " +
        "WHERE ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))"
);
```

### Deleting data

Iceberg tables support DELETE FROM queries to remove rows. Delete queries accept a filter to match rows to delete, including spatial filters.

**Python**

```python theme={"system"}
sedona.sql("DELETE FROM org_catalog.test_db.test_table WHERE id = 1")

# Use ST_Intersects to delete rows that intersect with a polygon
sedona.sql("""
DELETE FROM org_catalog.test_db.test_table
WHERE ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))
""")
```

**Scala**

```scala theme={"system"}
sedona.sql("DELETE FROM org_catalog.test_db.test_table WHERE id = 1")

// Use ST_Intersects to delete rows that intersect with a polygon
sedona.sql("""
DELETE FROM org_catalog.test_db.test_table
WHERE ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))
""")
```

**Java**

```java theme={"system"}
sedona.sql("DELETE FROM org_catalog.test_db.test_table WHERE id = 1");

// Use ST_Intersects to delete rows that intersect with a polygon
sedona.sql(
        "DELETE FROM org_catalog.test_db.test_table " +
        "WHERE ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))"
);
```

### Merging data using `MERGE INTO`

Iceberg tables support MERGE INTO.
The syntax is identical to the open source Apache Iceberg, please refer to [Apache Iceberg - MERGE INTO](https://iceberg.apache.org/docs/latest/spark-writes/#merge-into) for more information.

`UPDATE`, `DELETE`, and `MERGE INTO` work in both copy-on-write and merge-on-read mode, which the `write.update.mode`, `write.delete.mode`, and `write.merge.mode` table properties select.

## Querying Data

User can load data from a table using `sedona.table(...)`:

**Python**

```python theme={"system"}
df = sedona.table("org_catalog.test_db.test_table")
```

**Scala**

```scala theme={"system"}
val df = sedona.table("org_catalog.test_db.test_table")
```

**Java**

```java theme={"system"}
Dataset<Row> df = sedona.table("org_catalog.test_db.test_table");
```

You can apply some configurations for reading the table, such as the split size if you want to read the table into a DataFrame with more partitions:

**Python**

```python theme={"system"}
df = sedona.read.option('split-size', '1000').table("org_catalog.test_db.test_table")
```

**Scala**

```scala theme={"system"}
val df = sedona.read.option("split-size", "1000").table("org_catalog.test_db.test_table")
```

**Java**

```java theme={"system"}
Dataset<Row> df = sedona.read().option("split-size", "1000").table("org_catalog.test_db.test_table");
```

You can run spatial range query on the table using Spatial SQL:

**Python**

```python theme={"system"}
df = sedona.sql("""
SELECT * FROM org_catalog.test_db.test_table
WHERE ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))
""")
```

**Scala**

```scala theme={"system"}
val df = sedona.sql("""
SELECT * FROM org_catalog.test_db.test_table
WHERE ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))
""")
```

**Java**

```java theme={"system"}
Dataset<Row> df = sedona.sql(
        "SELECT * FROM org_catalog.test_db.test_table " +
        "WHERE ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))"
);
```

Or using the DataFrame API:

**Python**

```python theme={"system"}
df = sedona.table("org_catalog.test_db.test_table")\
    .where("ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))")
```

**Scala**

```scala theme={"system"}
val df = sedona.table("org_catalog.test_db.test_table")
    .where("ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))")
```

**Java**

```java theme={"system"}
Dataset<Row> df = sedona.table("org_catalog.test_db.test_table")
    .where("ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))");
```

Users can also load a table by specifying the name of the data source explicitly using `.format("iceberg")`, this will load an isolated table reference that will not automatically refresh tables used by queries.

**Python**

```python theme={"system"}
df = sedona.read.format("iceberg").load("org_catalog.test_db.test_table")\
    .where("ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))")
```

**Scala**

```scala theme={"system"}
val df = sedona.read.format("iceberg").load("org_catalog.test_db.test_table")
    .where("ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))")
```

**Java**

```java theme={"system"}
Dataset<Row> df = sedona.read().format("iceberg").load("org_catalog.test_db.test_table")
    .where("ST_Intersects(geom, ST_GeomFromText('POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))'))");
```

WherobotsDB skips reading data files that don't contain data that satisfy a spatial filter, which makes selective spatial range queries fast.
Please refer to [Cluster Tables with a Spatial Index](https://docs.wherobots.com/data-engineering/iceberg-tables/spatial-index.md) for more information.

## Working with Geometry Data

User can use any `ST_` functions provided by WherobotsDB to manipulate the data in a geometry column. For example, user can use `ST_Buffer` to create a buffer around the geometry column:

**Python**

```python theme={"system"}
sedona.sql("SELECT ST_Buffer(geom, 0.001) FROM org_catalog.test_db.test_table").show()
```

**Scala**

```scala theme={"system"}
sedona.sql("SELECT ST_Buffer(geom, 0.001) FROM org_catalog.test_db.test_table").show()
```

**Java**

```java theme={"system"}
sedona.sql("SELECT ST_Buffer(geom, 0.001) FROM org_catalog.test_db.test_table").show();
```

```
+----------------------+
|st_buffer(geom, 0.001)|
+----------------------+
|  POLYGON ((3.001 4...|
|  POLYGON ((1.001 2...|
|  POLYGON ((2.001 3...|
|  POLYGON ((1.001 2...|
|  POLYGON ((2.001 3...|
+----------------------+
```

To write the result back to a table, create the table with its geometry column first, then insert the result of the query. `ST_Buffer` keeps the SRID of its input, so the buffered values match a `geometry(4326)` column:

**Python**

```python theme={"system"}
sedona.sql("""
CREATE TABLE org_catalog.test_db.test_table_buf (
    geom geometry(4326)
) USING iceberg""")

sedona.sql("""
INSERT INTO org_catalog.test_db.test_table_buf
SELECT ST_Buffer(geom, 0.001) FROM org_catalog.test_db.test_table
""")
```

**Scala**

```scala theme={"system"}
sedona.sql("""
CREATE TABLE org_catalog.test_db.test_table_buf (
    geom geometry(4326)
) USING iceberg""")

sedona.sql("""
INSERT INTO org_catalog.test_db.test_table_buf
SELECT ST_Buffer(geom, 0.001) FROM org_catalog.test_db.test_table
""")
```

**Java**

```java theme={"system"}
sedona.sql(
        "CREATE TABLE org_catalog.test_db.test_table_buf (" +
        "geom geometry(4326)" +
        ") USING iceberg"
);

sedona.sql(
        "INSERT INTO org_catalog.test_db.test_table_buf " +
        "SELECT ST_Buffer(geom, 0.001) FROM org_catalog.test_db.test_table"
);
```

Or call `writeTo(...).append()` on the resulting DataFrame of the query:

**Python**

```python theme={"system"}
sedona.sql("SELECT ST_Buffer(geom, 0.001) AS geom FROM org_catalog.test_db.test_table")\
    .writeTo("org_catalog.test_db.test_table_buf").append()
```

**Scala**

```scala theme={"system"}
sedona.sql("SELECT ST_Buffer(geom, 0.001) AS geom FROM org_catalog.test_db.test_table")
    .writeTo("org_catalog.test_db.test_table_buf").append()
```

**Java**

```java theme={"system"}
sedona.sql("SELECT ST_Buffer(geom, 0.001) AS geom FROM org_catalog.test_db.test_table")
    .writeTo("org_catalog.test_db.test_table_buf").append();
```

## Geography Columns

`GEOGRAPHY(<srid>)` interprets coordinates on a sphere rather than a plane, so `ST_Distance`, `ST_Area`, and `ST_Length` return spherical results in meters without projecting first.

```python theme={"system"}
sedona.sql("""
CREATE TABLE org_catalog.test_db.stations (
    id bigint,
    geog geography(4326)
) USING iceberg
""")

sedona.sql("""
INSERT INTO org_catalog.test_db.stations
SELECT 1L, ST_GeogFromWKT('POINT (-122.3937 37.7955)', 4326)
""")
```

A geography column rejects a value whose SRID is a different CRS, the same as a geometry column. Unlike a geometry column, it accepts a value with no SRID, such as the result of `ST_GeogFromWKT` without the SRID argument, and stores it under the column CRS. There is no cast from `GEOMETRY` to `GEOGRAPHY`; use `ST_GeomToGeography` to convert, and `ST_GeogToGeometry` in the other direction. See the [geography function reference](https://docs.wherobots.com/reference/wherobots-db/geometry-data/geography/geography-functions.md) for the full list.

## Further Reading

WherobotsDB tables are Apache Iceberg tables. Please refer to [Apache Iceberg documentation for Spark](https://iceberg.apache.org/docs/latest/getting-started/) for more information.

If you have spatial data stored in an Iceberg table or Parquet files as WKT or WKB, copy it into a table with a `GEOMETRY(<srid>)` column as described in [Upgrade Existing Geometry Tables](https://docs.wherobots.com/data-engineering/iceberg-tables/upgrade-geometry-tables.md).

WherobotsDB pushes spatial filters down to skip data files and can optimize spatial
range queries.

Please refer to [Cluster Tables with a Spatial Index](https://docs.wherobots.com/data-engineering/iceberg-tables/spatial-index.md) for how to organize your spatial data for better performance.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.