This is an automated email from the ASF dual-hosted git repository.

bchapuis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-baremaps.git


The following commit(s) were added to refs/heads/main by this push:
     new cd2018dc Add support for nested types, geoparquet groups, and postgres 
jsonb in data table (#860)
cd2018dc is described below

commit cd2018dcbc62a45f0c5fb830f082aa21feb41cc5
Author: Bertil Chapuis <[email protected]>
AuthorDate: Mon Jun 3 21:06:45 2024 +0200

    Add support for nested types, geoparquet groups, and postgres jsonb in data 
table (#860)
    
    * Add support for nested types in the DataTable
    
    * Add a JsonbHandler that serializes Objects
    
    * Add an EnvelopeField to the GeoParquet parser
    
    * Save the EnvelopeField as geometry in Postgis
    
    * Add a writeEnvelope method to the CopyWriter
    
    * BBox use float values in GeoParquet
    
    * Create Envelope from Double and Float values
    
    * Use the default CRS when the crs field is null in Geoparquet (#861)
    
    ---------
    
    Co-authored-by: Antoine Drabble <[email protected]>
---
 .../apache/baremaps/database/copy/CopyWriter.java  | 14 ++++
 ...ValueHandler.java => EnvelopeValueHandler.java} | 20 +++--
 .../database/copy/GeometryValueHandler.java        |  8 +-
 .../baremaps/database/copy/JsonbValueHandler.java  | 80 ++++++++++++++++++++
 .../database/metadata/DatabaseMetadata.java        | 36 ++++++---
 .../storage/flatgeobuf/FlatGeoBufDataTable.java    |  7 +-
 .../flatgeobuf/FlatGeoBufTypeConversion.java       |  6 +-
 .../storage/geopackage/GeoPackageDataTable.java    |  5 +-
 .../geoparquet/GeoParquetTypeConversion.java       | 75 ++++++++++++++-----
 .../storage/postgres/PostgresDataStore.java        | 22 +++++-
 .../storage/postgres/PostgresTypeConversion.java   |  5 +-
 .../shapefile/internal/ShapefileByteReader.java    |  7 +-
 .../org/apache/baremaps/calcite/CalciteTest.java   | 11 +--
 .../database/postgres/NodeRepositoryTest.java      |  4 +-
 .../org/apache/baremaps/storage/MockDataTable.java | 11 +--
 .../geoparquet/GeoParquetToPostgresTest.java       |  7 +-
 .../baremaps/data/calcite/SqlTypeConversion.java   | 14 ----
 .../apache/baremaps/data/storage/DataColumn.java   | 22 ++++--
 .../{DataColumnImpl.java => DataColumnFixed.java}  |  3 +-
 .../{DataColumnImpl.java => DataColumnNested.java} | 12 ++-
 .../baremaps/data/type/DataTypeProvider.java       | 35 ++++-----
 .../baremaps/geoparquet/data/GeoParquetGroup.java  | 26 +++++++
 .../geoparquet/data/GeoParquetGroupFactory.java    | 51 +++++++++----
 .../geoparquet/data/GeoParquetGroupImpl.java       | 85 +++++++++++++++++++---
 .../geoparquet/data/GeoParquetMetadata.java        | 21 +++---
 25 files changed, 446 insertions(+), 141 deletions(-)

diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/database/copy/CopyWriter.java 
b/baremaps-core/src/main/java/org/apache/baremaps/database/copy/CopyWriter.java
index 0e5b5de0..c71b752f 100644
--- 
a/baremaps-core/src/main/java/org/apache/baremaps/database/copy/CopyWriter.java
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/database/copy/CopyWriter.java
@@ -30,6 +30,7 @@ import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import org.locationtech.jts.geom.Envelope;
 import org.locationtech.jts.geom.Geometry;
 import org.postgresql.copy.PGCopyOutputStream;
 import org.postgresql.core.Oid;
@@ -106,6 +107,9 @@ public class CopyWriter implements AutoCloseable {
   public static final GeometryValueHandler GEOMETRY_HANDLER =
       new GeometryValueHandler();
 
+  public static final EnvelopeValueHandler ENVELOPE_HANDLER =
+      new EnvelopeValueHandler();
+
   private final DataOutputStream data;
 
   /**
@@ -397,6 +401,16 @@ public class CopyWriter implements AutoCloseable {
     GEOMETRY_HANDLER.handle(data, value);
   }
 
+  /**
+   * Writes an envelope value.
+   *
+   * @param value
+   * @throws IOException
+   */
+  public void writeEnvelope(Envelope value) throws IOException {
+    ENVELOPE_HANDLER.handle(data, value);
+  }
+
   /** Close the writer. */
   @Override
   public void close() throws IOException {
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/database/copy/GeometryValueHandler.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/database/copy/EnvelopeValueHandler.java
similarity index 65%
copy from 
baremaps-core/src/main/java/org/apache/baremaps/database/copy/GeometryValueHandler.java
copy to 
baremaps-core/src/main/java/org/apache/baremaps/database/copy/EnvelopeValueHandler.java
index 87d09264..804bae1f 100644
--- 
a/baremaps-core/src/main/java/org/apache/baremaps/database/copy/GeometryValueHandler.java
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/database/copy/EnvelopeValueHandler.java
@@ -21,21 +21,29 @@ import static org.locationtech.jts.io.WKBConstants.wkbNDR;
 
 import de.bytefish.pgbulkinsert.pgsql.handlers.BaseValueHandler;
 import java.io.DataOutputStream;
-import java.io.IOException;
+import org.locationtech.jts.geom.Envelope;
 import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
 import org.locationtech.jts.io.WKBWriter;
 
-public class GeometryValueHandler extends BaseValueHandler<Geometry> {
+public class EnvelopeValueHandler extends BaseValueHandler<Envelope> {
+
+  private static final GeometryFactory geometryFactory = new GeometryFactory();
+
+  private static byte[] asWKB(Envelope value) {
+    Geometry geometry = geometryFactory.toGeometry(value);
+    return new WKBWriter(2, wkbNDR, true).write(geometry);
+  }
 
   @Override
-  protected void internalHandle(DataOutputStream buffer, Geometry value) 
throws IOException {
-    byte[] wkb = new WKBWriter(2, wkbNDR, true).write(value);
+  protected void internalHandle(DataOutputStream buffer, Envelope value) 
throws Exception {
+    byte[] wkb = asWKB(value);
     buffer.writeInt(wkb.length);
     buffer.write(wkb, 0, wkb.length);
   }
 
   @Override
-  public int getLength(Geometry geometry) {
-    throw new UnsupportedOperationException();
+  public int getLength(Envelope value) {
+    return asWKB(value).length + 4;
   }
 }
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/database/copy/GeometryValueHandler.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/database/copy/GeometryValueHandler.java
index 87d09264..4d0a6614 100644
--- 
a/baremaps-core/src/main/java/org/apache/baremaps/database/copy/GeometryValueHandler.java
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/database/copy/GeometryValueHandler.java
@@ -27,15 +27,19 @@ import org.locationtech.jts.io.WKBWriter;
 
 public class GeometryValueHandler extends BaseValueHandler<Geometry> {
 
+  private static byte[] asWKB(Geometry geometry) {
+    return new WKBWriter(2, wkbNDR, true).write(geometry);
+  }
+
   @Override
   protected void internalHandle(DataOutputStream buffer, Geometry value) 
throws IOException {
-    byte[] wkb = new WKBWriter(2, wkbNDR, true).write(value);
+    byte[] wkb = asWKB(value);
     buffer.writeInt(wkb.length);
     buffer.write(wkb, 0, wkb.length);
   }
 
   @Override
   public int getLength(Geometry geometry) {
-    throw new UnsupportedOperationException();
+    return asWKB(geometry).length + 4;
   }
 }
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/database/copy/JsonbValueHandler.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/database/copy/JsonbValueHandler.java
new file mode 100644
index 00000000..7a101e95
--- /dev/null
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/database/copy/JsonbValueHandler.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+package org.apache.baremaps.database.copy;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import de.bytefish.pgbulkinsert.pgsql.handlers.BaseValueHandler;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+public class JsonbValueHandler extends BaseValueHandler<Object> {
+
+  private static final ObjectMapper objectMapper;
+
+  static {
+    objectMapper = new ObjectMapper();
+    SimpleModule module = new SimpleModule();
+    module.addSerializer(String.class, new NoQuotesStringSerializer());
+    objectMapper.registerModule(module);
+  }
+
+  static class NoQuotesStringSerializer extends JsonSerializer<String> {
+    @Override
+    public void serialize(String value, JsonGenerator gen, SerializerProvider 
serializers)
+        throws IOException {
+      gen.writeRawValue(value);
+    }
+  }
+
+  private final int jsonbProtocolVersion;
+
+  public JsonbValueHandler() {
+    this(1);
+  }
+
+  public JsonbValueHandler(int jsonbProtocolVersion) {
+    this.jsonbProtocolVersion = jsonbProtocolVersion;
+  }
+
+  private static byte[] asJson(Object object) {
+    try {
+      String value = objectMapper.writeValueAsString(object);
+      return value.getBytes("UTF-8");
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  protected void internalHandle(DataOutputStream buffer, Object value) throws 
Exception {
+    byte[] utf8Bytes = asJson(value);
+    buffer.writeInt(utf8Bytes.length + 1);
+    buffer.writeByte(jsonbProtocolVersion);
+    buffer.write(utf8Bytes);
+  }
+
+  @Override
+  public int getLength(Object value) {
+    byte[] utf8Bytes = asJson(value);
+    return utf8Bytes.length;
+  }
+}
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/database/metadata/DatabaseMetadata.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/database/metadata/DatabaseMetadata.java
index ff726ed3..c3b67575 100644
--- 
a/baremaps-core/src/main/java/org/apache/baremaps/database/metadata/DatabaseMetadata.java
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/database/metadata/DatabaseMetadata.java
@@ -82,18 +82,30 @@ public class DatabaseMetadata {
         var resultSet = connection.getMetaData().getColumns(catalog, 
schemaPattern,
             tableNamePattern, columnNamePattern)) {
       while (resultSet.next()) {
-        tableColumns.add(new ColumnResult(resultSet.getString("TABLE_CAT"),
-            resultSet.getString("TABLE_SCHEM"), 
resultSet.getString("TABLE_NAME"),
-            resultSet.getString("COLUMN_NAME"), resultSet.getInt("DATA_TYPE"),
-            resultSet.getString("TYPE_NAME"), resultSet.getInt("COLUMN_SIZE"),
-            resultSet.getInt("DECIMAL_DIGITS"), 
resultSet.getInt("NUM_PREC_RADIX"),
-            resultSet.getInt("NULLABLE"), resultSet.getString("REMARKS"),
-            resultSet.getString("COLUMN_DEF"), 
resultSet.getInt("SQL_DATA_TYPE"),
-            resultSet.getInt("SQL_DATETIME_SUB"), 
resultSet.getInt("CHAR_OCTET_LENGTH"),
-            resultSet.getInt("ORDINAL_POSITION"), 
resultSet.getString("IS_NULLABLE"),
-            resultSet.getString("SCOPE_CATALOG"), 
resultSet.getString("SCOPE_SCHEMA"),
-            resultSet.getString("SCOPE_TABLE"), 
resultSet.getShort("SOURCE_DATA_TYPE"),
-            resultSet.getString("IS_AUTOINCREMENT"), 
resultSet.getString("IS_GENERATEDCOLUMN")));
+        tableColumns.add(new ColumnResult(
+            resultSet.getString("TABLE_CAT"),
+            resultSet.getString("TABLE_SCHEM"),
+            resultSet.getString("TABLE_NAME"),
+            resultSet.getString("COLUMN_NAME"),
+            resultSet.getInt("DATA_TYPE"),
+            resultSet.getString("TYPE_NAME"),
+            resultSet.getInt("COLUMN_SIZE"),
+            resultSet.getInt("DECIMAL_DIGITS"),
+            resultSet.getInt("NUM_PREC_RADIX"),
+            resultSet.getInt("NULLABLE"),
+            resultSet.getString("REMARKS"),
+            resultSet.getString("COLUMN_DEF"),
+            resultSet.getInt("SQL_DATA_TYPE"),
+            resultSet.getInt("SQL_DATETIME_SUB"),
+            resultSet.getInt("CHAR_OCTET_LENGTH"),
+            resultSet.getInt("ORDINAL_POSITION"),
+            resultSet.getString("IS_NULLABLE"),
+            resultSet.getString("SCOPE_CATALOG"),
+            resultSet.getString("SCOPE_SCHEMA"),
+            resultSet.getString("SCOPE_TABLE"),
+            resultSet.getShort("SOURCE_DATA_TYPE"),
+            resultSet.getString("IS_AUTOINCREMENT"),
+            resultSet.getString("IS_GENERATEDCOLUMN")));
       }
     } catch (SQLException e) {
       throw new RuntimeException(e);
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/flatgeobuf/FlatGeoBufDataTable.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/flatgeobuf/FlatGeoBufDataTable.java
index a7c9c93c..f3598d28 100644
--- 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/flatgeobuf/FlatGeoBufDataTable.java
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/flatgeobuf/FlatGeoBufDataTable.java
@@ -57,7 +57,12 @@ public class FlatGeoBufDataTable implements DataTable {
     this.schema = readSchema(file);
   }
 
-
+  /**
+   * Reads the schema from a flatgeobuf file.
+   *
+   * @param file the path to the flatgeobuf file
+   * @return the schema of the table
+   */
   private static DataSchema readSchema(Path file) {
     try (var channel = FileChannel.open(file, StandardOpenOption.READ)) {
       // try to read the schema from the file
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/flatgeobuf/FlatGeoBufTypeConversion.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/flatgeobuf/FlatGeoBufTypeConversion.java
index 58c1074c..1a42148c 100644
--- 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/flatgeobuf/FlatGeoBufTypeConversion.java
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/flatgeobuf/FlatGeoBufTypeConversion.java
@@ -26,6 +26,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.*;
 import java.util.stream.Collectors;
 import org.apache.baremaps.data.storage.*;
+import org.apache.baremaps.data.storage.DataColumn.Cardinality;
 import org.apache.baremaps.data.storage.DataColumn.Type;
 import org.wololo.flatgeobuf.ColumnMeta;
 import org.wololo.flatgeobuf.GeometryConversions;
@@ -53,7 +54,10 @@ public class FlatGeoBufTypeConversion {
   public static DataSchema asSchema(HeaderMeta headerMeta) {
     var name = headerMeta.name;
     var columns = headerMeta.columns.stream()
-        .map(column -> new DataColumnImpl(column.name, 
Type.fromBinding(column.getBinding())))
+        .map(column -> new DataColumnFixed(
+            column.name,
+            column.nullable ? Cardinality.OPTIONAL : Cardinality.REQUIRED,
+            Type.fromBinding(column.getBinding())))
         .map(DataColumn.class::cast)
         .toList();
     return new DataSchemaImpl(name, columns);
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageDataTable.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageDataTable.java
index ed6bc89c..5518cb4b 100644
--- 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageDataTable.java
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageDataTable.java
@@ -24,6 +24,7 @@ import mil.nga.geopackage.features.user.FeatureDao;
 import mil.nga.geopackage.features.user.FeatureResultSet;
 import mil.nga.geopackage.geom.GeoPackageGeometryData;
 import org.apache.baremaps.data.storage.*;
+import org.apache.baremaps.data.storage.DataColumn.Cardinality;
 import org.apache.baremaps.data.storage.DataColumn.Type;
 import org.locationtech.jts.geom.*;
 
@@ -50,7 +51,9 @@ public class GeoPackageDataTable implements DataTable {
     for (FeatureColumn column : featureDao.getColumns()) {
       var propertyName = column.getName();
       var propertyType = classType(column);
-      columns.add(new DataColumnImpl(propertyName, propertyType));
+      var propertyCardinality = column.isNotNull() ? Cardinality.REQUIRED : 
Cardinality.OPTIONAL;
+      columns.add(new DataColumnFixed(
+          propertyName, propertyCardinality, propertyType));
     }
     schema = new DataSchemaImpl(name, columns);
     geometryFactory = new GeometryFactory(new PrecisionModel(), (int) 
featureDao.getSrs().getId());
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/geoparquet/GeoParquetTypeConversion.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/geoparquet/GeoParquetTypeConversion.java
index 19c09e41..78be7062 100644
--- 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/geoparquet/GeoParquetTypeConversion.java
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/geoparquet/GeoParquetTypeConversion.java
@@ -18,14 +18,15 @@
 package org.apache.baremaps.storage.geoparquet;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
-import org.apache.baremaps.data.storage.DataColumn;
+import java.util.Map;
+import org.apache.baremaps.data.storage.*;
+import org.apache.baremaps.data.storage.DataColumn.Cardinality;
 import org.apache.baremaps.data.storage.DataColumn.Type;
-import org.apache.baremaps.data.storage.DataColumnImpl;
-import org.apache.baremaps.data.storage.DataSchema;
-import org.apache.baremaps.data.storage.DataSchemaImpl;
 import org.apache.baremaps.geoparquet.data.GeoParquetGroup;
 import org.apache.baremaps.geoparquet.data.GeoParquetGroup.Field;
+import org.apache.baremaps.geoparquet.data.GeoParquetGroup.GroupField;
 import org.apache.baremaps.geoparquet.data.GeoParquetGroup.Schema;
 
 public class GeoParquetTypeConversion {
@@ -33,23 +34,34 @@ public class GeoParquetTypeConversion {
   private GeoParquetTypeConversion() {}
 
   public static DataSchema asSchema(String table, Schema schema) {
-    List<DataColumn> columns = schema.fields().stream()
-        .map(field -> (DataColumn) new DataColumnImpl(field.name(), 
asSchema(field.type())))
-        .toList();
+    List<DataColumn> columns = asDataColumns(schema);
     return new DataSchemaImpl(table, columns);
   }
 
-  public static Type asSchema(GeoParquetGroup.Type type) {
-    return switch (type) {
-      case BINARY -> Type.BYTE_ARRAY;
-      case BOOLEAN -> Type.BOOLEAN;
-      case INTEGER -> Type.INTEGER;
-      case INT96, LONG -> Type.LONG;
-      case FLOAT -> Type.FLOAT;
-      case DOUBLE -> Type.DOUBLE;
-      case STRING -> Type.STRING;
-      case GEOMETRY -> Type.GEOMETRY;
-      case GROUP -> null;
+  private static List<DataColumn> asDataColumns(Schema field) {
+    return field.fields().stream()
+        .map(GeoParquetTypeConversion::asDataColumn)
+        .toList();
+  }
+
+  private static DataColumn asDataColumn(Field field) {
+    Cardinality cardinality = switch (field.cardinality()) {
+      case REQUIRED -> Cardinality.REQUIRED;
+      case OPTIONAL -> Cardinality.OPTIONAL;
+      case REPEATED -> Cardinality.REPEATED;
+    };
+    return switch (field.type()) {
+      case BINARY -> new DataColumnFixed(field.name(), cardinality, 
Type.BINARY);
+      case BOOLEAN -> new DataColumnFixed(field.name(), cardinality, 
Type.BOOLEAN);
+      case INTEGER -> new DataColumnFixed(field.name(), cardinality, 
Type.INTEGER);
+      case INT96, LONG -> new DataColumnFixed(field.name(), cardinality, 
Type.LONG);
+      case FLOAT -> new DataColumnFixed(field.name(), cardinality, Type.FLOAT);
+      case DOUBLE -> new DataColumnFixed(field.name(), cardinality, 
Type.DOUBLE);
+      case STRING -> new DataColumnFixed(field.name(), cardinality, 
Type.STRING);
+      case GEOMETRY -> new DataColumnFixed(field.name(), cardinality, 
Type.GEOMETRY);
+      case ENVELOPE -> new DataColumnFixed(field.name(), cardinality, 
Type.ENVELOPE);
+      case GROUP -> new DataColumnNested(field.name(), cardinality,
+          asDataColumns(((GroupField) field).schema()));
     };
   }
 
@@ -59,7 +71,6 @@ public class GeoParquetTypeConversion {
     List<Field> fields = schema.fields();
     for (int i = 0; i < fields.size(); i++) {
       Field field = fields.get(i);
-      field.type();
       switch (field.type()) {
         case BINARY -> values.add(group.getBinaryValue(i).getBytes());
         case BOOLEAN -> values.add(group.getBooleanValue(i));
@@ -69,9 +80,33 @@ public class GeoParquetTypeConversion {
         case DOUBLE -> values.add(group.getDoubleValue(i));
         case STRING -> values.add(group.getStringValue(i));
         case GEOMETRY -> values.add(group.getGeometryValue(i));
-        case GROUP -> values.add(null); // TODO: 
values.add(asDataRow(group.getGroupValue(i)));
+        case ENVELOPE -> values.add(group.getEnvelopeValue(i));
+        case GROUP -> values.add(asNested(group.getGroupValue(i)));
       }
     }
     return values;
   }
+
+  public static Map<String, Object> asNested(GeoParquetGroup group) {
+    Map<String, Object> nested = new HashMap<>();
+    Schema schema = group.getSchema();
+    List<Field> fields = schema.fields();
+    for (int i = 0; i < fields.size(); i++) {
+      Field field = fields.get(i);
+      nested.put(field.name(), switch (field.type()) {
+        case BINARY -> group.getBinaryValue(i).getBytes();
+        case BOOLEAN -> group.getBooleanValue(i);
+        case INTEGER -> group.getIntegerValue(i);
+        case INT96, LONG -> group.getLongValue(i);
+        case FLOAT -> group.getFloatValue(i);
+        case DOUBLE -> group.getDoubleValue(i);
+        case STRING -> group.getStringValue(i);
+        case GEOMETRY -> group.getGeometryValue(i);
+        case ENVELOPE -> group.getEnvelopeValue(i);
+        case GROUP -> asNested(group.getGroupValue(i));
+      });
+    }
+    return nested;
+  }
+
 }
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresDataStore.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresDataStore.java
index eabd5c59..de05ebe6 100644
--- 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresDataStore.java
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresDataStore.java
@@ -28,7 +28,9 @@ import javax.sql.DataSource;
 import org.apache.baremaps.data.storage.*;
 import org.apache.baremaps.data.storage.DataColumn.Type;
 import org.apache.baremaps.database.copy.CopyWriter;
+import org.apache.baremaps.database.copy.EnvelopeValueHandler;
 import org.apache.baremaps.database.copy.GeometryValueHandler;
+import org.apache.baremaps.database.copy.JsonbValueHandler;
 import org.apache.baremaps.database.metadata.DatabaseMetadata;
 import org.apache.baremaps.database.metadata.TableMetadata;
 import org.postgresql.PGConnection;
@@ -106,7 +108,7 @@ public class PostgresDataStore implements DataStore {
         if (PostgresTypeConversion.typeToName.containsKey(column.type())) {
           var columnName = column.name().replaceAll(REGEX, "_").toLowerCase();
           mapping.put(columnName, column.name());
-          properties.add(new DataColumnImpl(columnName, column.type()));
+          properties.add(new DataColumnFixed(columnName, column.cardinality(), 
column.type()));
         }
       }
 
@@ -177,7 +179,10 @@ public class PostgresDataStore implements DataStore {
   protected static DataSchema createSchema(TableMetadata tableMetadata) {
     var name = tableMetadata.table().tableName();
     var columns = tableMetadata.columns().stream()
-        .map(column -> new DataColumnImpl(column.columnName(),
+        .map(column -> new DataColumnFixed(
+            column.columnName(),
+            column.isNullable().equals("NO") ? DataColumn.Cardinality.REQUIRED
+                : DataColumn.Cardinality.OPTIONAL,
             PostgresTypeConversion.nameToType.get(column.typeName())))
         .map(DataColumn.class::cast)
         .toList();
@@ -206,13 +211,20 @@ public class PostgresDataStore implements DataStore {
     builder.append(schema.name());
     builder.append("\" (");
     builder.append(schema.columns().stream()
-        .map(column -> "\"" + column.name()
-            + "\" " + PostgresTypeConversion.typeToName.get(column.type()))
+        .map(PostgresDataStore::getColumnType)
         .collect(Collectors.joining(", ")));
     builder.append(")");
     return builder.toString();
   }
 
+  private static String getColumnType(DataColumn column) {
+    String columnName = column.name();
+    String columnType = PostgresTypeConversion.typeToName.get(column.type());
+    String columnArray = column.cardinality() == 
DataColumn.Cardinality.REPEATED ? "[]" : "";
+    String columnNull = column.cardinality() == 
DataColumn.Cardinality.REQUIRED ? "NOT NULL" : "";
+    return String.format("\"%s\" %s%s %s", columnName, columnType, 
columnArray, columnNull).strip();
+  }
+
   /**
    * Generate a copy query.
    *
@@ -276,6 +288,8 @@ public class PostgresDataStore implements DataStore {
       case LOCAL_TIME -> new LocalTimeValueHandler();
       case LOCAL_DATE_TIME -> new LocalDateTimeValueHandler();
       case GEOMETRY, POINT, MULTIPOINT, LINESTRING, MULTILINESTRING, POLYGON, 
MULTIPOLYGON, GEOMETRYCOLLECTION -> new GeometryValueHandler();
+      case ENVELOPE -> new EnvelopeValueHandler();
+      case NESTED -> new JsonbValueHandler();
       default -> throw new IllegalArgumentException("Unsupported type: " + 
type);
     };
   }
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresTypeConversion.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresTypeConversion.java
index 4188ade5..9c14fd22 100644
--- 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresTypeConversion.java
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresTypeConversion.java
@@ -40,12 +40,14 @@ public class PostgresTypeConversion {
     typeToName.put(Type.POLYGON, "geometry");
     typeToName.put(Type.MULTIPOLYGON, "geometry");
     typeToName.put(Type.GEOMETRYCOLLECTION, "geometry");
+    typeToName.put(Type.ENVELOPE, "geometry");
     typeToName.put(Type.INET_ADDRESS, "inet");
     typeToName.put(Type.INET4_ADDRESS, "inet");
     typeToName.put(Type.INET6_ADDRESS, "inet");
     typeToName.put(Type.LOCAL_DATE, "date");
     typeToName.put(Type.LOCAL_TIME, "time");
     typeToName.put(Type.LOCAL_DATE_TIME, "timestamp");
+    typeToName.put(Type.NESTED, "jsonb");
   }
 
   protected static final Map<String, Type> nameToType = Map.ofEntries(
@@ -59,6 +61,7 @@ public class PostgresTypeConversion {
       Map.entry("inet", Type.INET6_ADDRESS),
       Map.entry("date", Type.LOCAL_DATE),
       Map.entry("time", Type.LOCAL_TIME),
-      Map.entry("timestamp", Type.LOCAL_DATE_TIME));
+      Map.entry("timestamp", Type.LOCAL_DATE_TIME),
+      Map.entry("jsonb", Type.NESTED));
 
 }
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileByteReader.java
 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileByteReader.java
index d733067b..841c2d3e 100644
--- 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileByteReader.java
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileByteReader.java
@@ -25,6 +25,7 @@ import java.nio.MappedByteBuffer;
 import java.nio.channels.FileChannel;
 import java.util.*;
 import org.apache.baremaps.data.storage.*;
+import org.apache.baremaps.data.storage.DataColumn.Cardinality;
 import org.apache.baremaps.data.storage.DataColumn.Type;
 import org.locationtech.jts.algorithm.Orientation;
 import org.locationtech.jts.geom.Coordinate;
@@ -90,7 +91,7 @@ public class ShapefileByteReader extends CommonByteReader {
   }
 
   /**
-   * Returns the DBase 3 fields descriptors.
+   * Returns the DBase 3 columns descriptors.
    *
    * @return Fields descriptors.
    */
@@ -148,11 +149,11 @@ public class ShapefileByteReader extends CommonByteReader 
{
         case TimeStamp -> Type.STRING;
         case DateTime -> Type.STRING;
       };
-      columns.add(new DataColumnImpl(columnName, columnType));
+      columns.add(new DataColumnFixed(columnName, Cardinality.OPTIONAL, 
columnType));
     }
 
     // Add geometry column.
-    columns.add(new DataColumnImpl(GEOMETRY_NAME, Type.GEOMETRY));
+    columns.add(new DataColumnFixed(GEOMETRY_NAME, Cardinality.OPTIONAL, 
Type.GEOMETRY));
 
     return new DataSchemaImpl(name, columns);
   }
diff --git 
a/baremaps-core/src/test/java/org/apache/baremaps/calcite/CalciteTest.java 
b/baremaps-core/src/test/java/org/apache/baremaps/calcite/CalciteTest.java
index d59123ae..13cf44d0 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/calcite/CalciteTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/calcite/CalciteTest.java
@@ -26,6 +26,7 @@ import org.apache.baremaps.data.calcite.SqlDataTable;
 import org.apache.baremaps.data.collection.AppendOnlyLog;
 import org.apache.baremaps.data.collection.IndexedDataList;
 import org.apache.baremaps.data.storage.*;
+import org.apache.baremaps.data.storage.DataColumn.Cardinality;
 import org.apache.baremaps.data.storage.DataColumn.Type;
 import org.apache.baremaps.data.type.RowDataType;
 import org.apache.baremaps.maplibre.vectortile.VectorTileFunctions;
@@ -74,9 +75,9 @@ public class CalciteTest {
 
       // Create the city table
       DataSchema cityRowType = new DataSchemaImpl("city", List.of(
-          new DataColumnImpl("id", Type.INTEGER),
-          new DataColumnImpl("name", Type.STRING),
-          new DataColumnImpl("geometry", Type.GEOMETRY)));
+          new DataColumnFixed("id", Cardinality.OPTIONAL, Type.INTEGER),
+          new DataColumnFixed("name", Cardinality.OPTIONAL, Type.STRING),
+          new DataColumnFixed("geometry", Cardinality.OPTIONAL, 
Type.GEOMETRY)));
       DataTable cityDataTable = new DataTableImpl(
           cityRowType,
           new IndexedDataList<>(new AppendOnlyLog<>(new 
RowDataType(cityRowType))));
@@ -89,8 +90,8 @@ public class CalciteTest {
 
       // Create the population table
       DataSchema populationRowType = new DataSchemaImpl("population", List.of(
-          new DataColumnImpl("city_id", Type.INTEGER),
-          new DataColumnImpl("population", Type.INTEGER)));
+          new DataColumnFixed("city_id", Cardinality.OPTIONAL, Type.INTEGER),
+          new DataColumnFixed("population", Cardinality.OPTIONAL, 
Type.INTEGER)));
       DataTable populationDataTable = new DataTableImpl(
           populationRowType,
           new IndexedDataList<>(new AppendOnlyLog<>(new 
RowDataType(populationRowType))));
diff --git 
a/baremaps-core/src/test/java/org/apache/baremaps/database/postgres/NodeRepositoryTest.java
 
b/baremaps-core/src/test/java/org/apache/baremaps/database/postgres/NodeRepositoryTest.java
index e2709fa5..ef42773d 100644
--- 
a/baremaps-core/src/test/java/org/apache/baremaps/database/postgres/NodeRepositoryTest.java
+++ 
b/baremaps-core/src/test/java/org/apache/baremaps/database/postgres/NodeRepositoryTest.java
@@ -21,8 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertIterableEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 
-import java.io.IOException;
-import java.sql.SQLException;
 import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -37,7 +35,7 @@ class NodeRepositoryTest extends PostgresRepositoryTest {
   NodeRepository nodeRepository;
 
   @BeforeEach
-  void beforeEach() throws SQLException, IOException {
+  void beforeEach() {
     nodeRepository = new NodeRepository(dataSource());
   }
 
diff --git 
a/baremaps-core/src/test/java/org/apache/baremaps/storage/MockDataTable.java 
b/baremaps-core/src/test/java/org/apache/baremaps/storage/MockDataTable.java
index f21bdd7f..86bcf176 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/storage/MockDataTable.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/storage/MockDataTable.java
@@ -22,6 +22,7 @@ import static 
org.apache.baremaps.database.repository.Constants.GEOMETRY_FACTORY
 import java.util.Iterator;
 import java.util.List;
 import org.apache.baremaps.data.storage.*;
+import org.apache.baremaps.data.storage.DataColumn.Cardinality;
 import org.apache.baremaps.data.storage.DataColumn.Type;
 import org.locationtech.jts.geom.Coordinate;
 
@@ -33,11 +34,11 @@ public class MockDataTable implements DataTable {
 
   public MockDataTable() {
     this.rowType = new DataSchemaImpl("mock", List.of(
-        new DataColumnImpl("string", Type.STRING),
-        new DataColumnImpl("integer", Type.INTEGER),
-        new DataColumnImpl("double", Type.DOUBLE),
-        new DataColumnImpl("float", Type.FLOAT),
-        new DataColumnImpl("geometry", Type.GEOMETRY)));
+        new DataColumnFixed("string", Cardinality.OPTIONAL, Type.STRING),
+        new DataColumnFixed("integer", Cardinality.OPTIONAL, Type.INTEGER),
+        new DataColumnFixed("double", Cardinality.OPTIONAL, Type.DOUBLE),
+        new DataColumnFixed("float", Cardinality.OPTIONAL, Type.FLOAT),
+        new DataColumnFixed("geometry", Cardinality.OPTIONAL, Type.GEOMETRY)));
     this.rows = List.of(
         new DataRowImpl(rowType,
             List.of("string", 1, 1.0, 1.0f, GEOMETRY_FACTORY.createPoint(new 
Coordinate(1, 1)))),
diff --git 
a/baremaps-core/src/test/java/org/apache/baremaps/storage/geoparquet/GeoParquetToPostgresTest.java
 
b/baremaps-core/src/test/java/org/apache/baremaps/storage/geoparquet/GeoParquetToPostgresTest.java
index 198a63d2..f96001da 100644
--- 
a/baremaps-core/src/test/java/org/apache/baremaps/storage/geoparquet/GeoParquetToPostgresTest.java
+++ 
b/baremaps-core/src/test/java/org/apache/baremaps/storage/geoparquet/GeoParquetToPostgresTest.java
@@ -42,8 +42,13 @@ class GeoParquetToPostgresTest extends PostgresContainerTest 
{
 
     // Check the table in Postgres
     var postgresTable = postgresStore.get("geoparquet");
+
+    for (var row : postgresTable) {
+      System.out.println(row);
+    }
+
     assertEquals("geoparquet", postgresTable.schema().name());
-    assertEquals(3, postgresTable.schema().columns().size());
+    assertEquals(4, postgresTable.schema().columns().size());
     assertEquals(5L, postgresTable.size());
     assertEquals(5L, postgresTable.stream().count());
   }
diff --git 
a/baremaps-data/src/main/java/org/apache/baremaps/data/calcite/SqlTypeConversion.java
 
b/baremaps-data/src/main/java/org/apache/baremaps/data/calcite/SqlTypeConversion.java
index e0116e59..78a6ee25 100644
--- 
a/baremaps-data/src/main/java/org/apache/baremaps/data/calcite/SqlTypeConversion.java
+++ 
b/baremaps-data/src/main/java/org/apache/baremaps/data/calcite/SqlTypeConversion.java
@@ -31,32 +31,18 @@ public class SqlTypeConversion {
   static {
     types.put(Type.BYTE, new JavaTypeFactoryImpl()
         .createSqlType(SqlTypeName.TINYINT));
-    types.put(Type.BYTE_ARRAY, new JavaTypeFactoryImpl()
-        .createArrayType(new 
JavaTypeFactoryImpl().createSqlType(SqlTypeName.TINYINT), -1));
     types.put(Type.BOOLEAN, new JavaTypeFactoryImpl()
         .createSqlType(SqlTypeName.BOOLEAN));
-    types.put(Type.BOOLEAN_ARRAY, new JavaTypeFactoryImpl()
-        .createArrayType(new 
JavaTypeFactoryImpl().createSqlType(SqlTypeName.BOOLEAN), -1));
     types.put(Type.SHORT, new JavaTypeFactoryImpl()
         .createSqlType(SqlTypeName.SMALLINT));
-    types.put(Type.SHORT_ARRAY, new JavaTypeFactoryImpl()
-        .createArrayType(new 
JavaTypeFactoryImpl().createSqlType(SqlTypeName.SMALLINT), -1));
     types.put(Type.INTEGER, new JavaTypeFactoryImpl()
         .createSqlType(SqlTypeName.INTEGER));
-    types.put(Type.INTEGER_ARRAY, new JavaTypeFactoryImpl()
-        .createArrayType(new 
JavaTypeFactoryImpl().createSqlType(SqlTypeName.INTEGER), -1));
     types.put(Type.LONG, new JavaTypeFactoryImpl()
         .createSqlType(SqlTypeName.BIGINT));
-    types.put(Type.LONG_ARRAY, new JavaTypeFactoryImpl()
-        .createArrayType(new 
JavaTypeFactoryImpl().createSqlType(SqlTypeName.BIGINT), -1));
     types.put(Type.FLOAT, new JavaTypeFactoryImpl()
         .createSqlType(SqlTypeName.FLOAT));
-    types.put(Type.FLOAT_ARRAY, new JavaTypeFactoryImpl()
-        .createArrayType(new 
JavaTypeFactoryImpl().createSqlType(SqlTypeName.FLOAT), -1));
     types.put(Type.DOUBLE, new JavaTypeFactoryImpl()
         .createSqlType(SqlTypeName.DOUBLE));
-    types.put(Type.DOUBLE_ARRAY, new JavaTypeFactoryImpl()
-        .createArrayType(new 
JavaTypeFactoryImpl().createSqlType(SqlTypeName.DOUBLE), -1));
     types.put(Type.STRING, new JavaTypeFactoryImpl()
         .createSqlType(SqlTypeName.VARCHAR));
     types.put(Type.GEOMETRY, new JavaTypeFactoryImpl()
diff --git 
a/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumn.java 
b/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumn.java
index 9c70bd34..1f86773c 100644
--- 
a/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumn.java
+++ 
b/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumn.java
@@ -23,6 +23,7 @@ import java.net.InetAddress;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.util.Map;
 import org.locationtech.jts.geom.*;
 
 /**
@@ -37,6 +38,15 @@ public interface DataColumn {
    */
   String name();
 
+
+  Cardinality cardinality();
+
+  enum Cardinality {
+    REQUIRED,
+    OPTIONAL,
+    REPEATED
+  }
+
   /**
    * Returns the type of the column.
    *
@@ -48,20 +58,14 @@ public interface DataColumn {
    * An enumeration of the supported data column types.
    */
   enum Type {
+    BINARY(byte[].class),
     BYTE(Byte.class),
-    BYTE_ARRAY(byte[].class),
     BOOLEAN(Boolean.class),
-    BOOLEAN_ARRAY(boolean[].class),
     SHORT(Short.class),
-    SHORT_ARRAY(short[].class),
     INTEGER(Integer.class),
-    INTEGER_ARRAY(int[].class),
     LONG(Long.class),
-    LONG_ARRAY(long[].class),
     FLOAT(Float.class),
-    FLOAT_ARRAY(float[].class),
     DOUBLE(Double.class),
-    DOUBLE_ARRAY(double[].class),
     STRING(String.class),
     COORDINATE(Coordinate.class),
     GEOMETRY(Geometry.class),
@@ -72,12 +76,14 @@ public interface DataColumn {
     MULTILINESTRING(MultiLineString.class),
     MULTIPOLYGON(MultiPolygon.class),
     GEOMETRYCOLLECTION(GeometryCollection.class),
+    ENVELOPE(Envelope.class),
     INET_ADDRESS(InetAddress.class),
     INET4_ADDRESS(Inet4Address.class),
     INET6_ADDRESS(Inet6Address.class),
     LOCAL_DATE(LocalDate.class),
     LOCAL_TIME(LocalTime.class),
-    LOCAL_DATE_TIME(LocalDateTime.class),;
+    LOCAL_DATE_TIME(LocalDateTime.class),
+    NESTED(Map.class);
 
     private final Class<?> binding;
 
diff --git 
a/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnImpl.java
 
b/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnFixed.java
similarity index 89%
copy from 
baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnImpl.java
copy to 
baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnFixed.java
index 400e6033..9b786e69 100644
--- 
a/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnImpl.java
+++ 
b/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnFixed.java
@@ -20,6 +20,7 @@ package org.apache.baremaps.data.storage;
 /**
  * A column in a table.
  */
-public record DataColumnImpl(String name, Type type) implements DataColumn {
+public record DataColumnFixed(String name, Cardinality cardinality,
+    Type type) implements DataColumn {
 
 }
diff --git 
a/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnImpl.java
 
b/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnNested.java
similarity index 80%
rename from 
baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnImpl.java
rename to 
baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnNested.java
index 400e6033..179b5c8f 100644
--- 
a/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnImpl.java
+++ 
b/baremaps-data/src/main/java/org/apache/baremaps/data/storage/DataColumnNested.java
@@ -17,9 +17,13 @@
 
 package org.apache.baremaps.data.storage;
 
-/**
- * A column in a table.
- */
-public record DataColumnImpl(String name, Type type) implements DataColumn {
+import java.util.List;
+
+public record DataColumnNested(String name, Cardinality cardinality,
+    List<DataColumn> columns) implements DataColumn {
 
+  @Override
+  public Type type() {
+    return Type.NESTED;
+  }
 }
diff --git 
a/baremaps-data/src/test/java/org/apache/baremaps/data/type/DataTypeProvider.java
 
b/baremaps-data/src/test/java/org/apache/baremaps/data/type/DataTypeProvider.java
index 0b2f98cf..eb0ec04d 100644
--- 
a/baremaps-data/src/test/java/org/apache/baremaps/data/type/DataTypeProvider.java
+++ 
b/baremaps-data/src/test/java/org/apache/baremaps/data/type/DataTypeProvider.java
@@ -22,6 +22,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 import org.apache.baremaps.data.storage.*;
+import org.apache.baremaps.data.storage.DataColumn.Cardinality;
 import org.apache.baremaps.data.storage.DataColumn.Type;
 import org.apache.baremaps.data.storage.DataSchema;
 import org.apache.baremaps.data.storage.DataSchemaImpl;
@@ -33,23 +34,23 @@ public class DataTypeProvider {
   private static final GeometryFactory geometryFactory = new GeometryFactory();
 
   private static final DataSchema DATA_SCHEMA = new DataSchemaImpl("row", 
List.of(
-      new DataColumnImpl("byte", Type.BYTE),
-      new DataColumnImpl("boolean", Type.BOOLEAN),
-      new DataColumnImpl("short", Type.SHORT),
-      new DataColumnImpl("integer", Type.INTEGER),
-      new DataColumnImpl("long", Type.LONG),
-      new DataColumnImpl("float", Type.FLOAT),
-      new DataColumnImpl("double", Type.DOUBLE),
-      new DataColumnImpl("string", Type.STRING),
-      new DataColumnImpl("geometry", Type.GEOMETRY),
-      new DataColumnImpl("point", Type.POINT),
-      new DataColumnImpl("linestring", Type.LINESTRING),
-      new DataColumnImpl("polygon", Type.POLYGON),
-      new DataColumnImpl("multipoint", Type.MULTIPOINT),
-      new DataColumnImpl("multilinestring", Type.MULTILINESTRING),
-      new DataColumnImpl("multipolygon", Type.MULTIPOLYGON),
-      new DataColumnImpl("geometrycollection", Type.GEOMETRYCOLLECTION),
-      new DataColumnImpl("coordinate", Type.COORDINATE)));
+      new DataColumnFixed("byte", Cardinality.OPTIONAL, Type.BYTE),
+      new DataColumnFixed("boolean", Cardinality.OPTIONAL, Type.BOOLEAN),
+      new DataColumnFixed("short", Cardinality.OPTIONAL, Type.SHORT),
+      new DataColumnFixed("integer", Cardinality.OPTIONAL, Type.INTEGER),
+      new DataColumnFixed("long", Cardinality.OPTIONAL, Type.LONG),
+      new DataColumnFixed("float", Cardinality.OPTIONAL, Type.FLOAT),
+      new DataColumnFixed("double", Cardinality.OPTIONAL, Type.DOUBLE),
+      new DataColumnFixed("string", Cardinality.OPTIONAL, Type.STRING),
+      new DataColumnFixed("geometry", Cardinality.OPTIONAL, Type.GEOMETRY),
+      new DataColumnFixed("point", Cardinality.OPTIONAL, Type.POINT),
+      new DataColumnFixed("linestring", Cardinality.OPTIONAL, Type.LINESTRING),
+      new DataColumnFixed("polygon", Cardinality.OPTIONAL, Type.POLYGON),
+      new DataColumnFixed("multipoint", Cardinality.OPTIONAL, Type.MULTIPOINT),
+      new DataColumnFixed("multilinestring", Cardinality.OPTIONAL, 
Type.MULTILINESTRING),
+      new DataColumnFixed("multipolygon", Cardinality.OPTIONAL, 
Type.MULTIPOLYGON),
+      new DataColumnFixed("geometrycollection", Cardinality.OPTIONAL, 
Type.GEOMETRYCOLLECTION),
+      new DataColumnFixed("coordinate", Cardinality.OPTIONAL, 
Type.COORDINATE)));
 
   private static final DataRow DATA_ROW = DATA_SCHEMA.createRow()
       .with("byte", Byte.MAX_VALUE)
diff --git 
a/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroup.java
 
b/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroup.java
index 755f582f..5a3b3709 100644
--- 
a/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroup.java
+++ 
b/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroup.java
@@ -20,6 +20,7 @@ package org.apache.baremaps.geoparquet.data;
 import java.util.List;
 import org.apache.parquet.io.api.Binary;
 import org.apache.parquet.schema.GroupType;
+import org.locationtech.jts.geom.Envelope;
 import org.locationtech.jts.geom.Geometry;
 
 /**
@@ -98,6 +99,10 @@ public interface GeoParquetGroup {
 
   List<Geometry> getGeometryValues(int fieldIndex);
 
+  Envelope getEnvelopeValue(int fieldIndex);
+
+  List<Envelope> getEnvelopeValues(int fieldIndex);
+
   GeoParquetGroup getGroupValue(int fieldIndex);
 
   List<GeoParquetGroup> getGroupValues(int fieldIndex);
@@ -142,6 +147,10 @@ public interface GeoParquetGroup {
 
   List<Geometry> getGeometryValues(String fieldName);
 
+  Envelope getEnvelopeValue(String fieldName);
+
+  List<Envelope> getEnvelopeValues(String fieldName);
+
   GeoParquetGroup getGroupValue(String fieldName);
 
   List<GeoParquetGroup> getGroupValues(String fieldName);
@@ -186,6 +195,10 @@ public interface GeoParquetGroup {
 
   void setGeometryValues(int fieldIndex, List<Geometry> geometryValues);
 
+  void setEnvelopeValue(int fieldIndex, Envelope envelopeValue);
+
+  void setEnvelopeValues(int fieldIndex, List<Envelope> envelopeValues);
+
   void setGroupValue(int fieldIndex, GeoParquetGroup groupValue);
 
   void setGroupValues(int fieldIndex, List<GeoParquetGroup> groupValues);
@@ -230,6 +243,10 @@ public interface GeoParquetGroup {
 
   void setGeometryValues(String fieldName, List<Geometry> geometryValues);
 
+  void setEnvelopeValue(String fieldName, Envelope envelopeValue);
+
+  void setEnvelopeValues(String fieldName, List<Envelope> envelopeValues);
+
   void setGroupValue(String fieldName, GeoParquetGroup groupValue);
 
   void setGroupValues(String fieldName, List<GeoParquetGroup> groupValues);
@@ -331,6 +348,14 @@ public interface GeoParquetGroup {
     }
   }
 
+  record EnvelopeField(String name, Cardinality cardinality, Schema schema) 
implements Field {
+
+    @Override
+        public Type type() {
+        return Type.ENVELOPE;
+        }
+  }
+
   record GroupField(String name, Cardinality cardinality, Schema schema) 
implements Field {
 
     @Override
@@ -352,6 +377,7 @@ public interface GeoParquetGroup {
     LONG,
     STRING,
     GEOMETRY,
+    ENVELOPE,
     GROUP
   }
 
diff --git 
a/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroupFactory.java
 
b/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroupFactory.java
index d89e0a03..f925df50 100644
--- 
a/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroupFactory.java
+++ 
b/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroupFactory.java
@@ -40,34 +40,57 @@ public class GeoParquetGroupFactory {
   public static GeoParquetGroup.Schema createGeoParquetSchema(
       GroupType schema,
       GeoParquetMetadata metadata) {
+
+    // Map the fields
     List<Field> fields = schema.getFields().stream().map(field -> {
+
+      // Map the column cardinality
       GeoParquetGroup.Cardinality cardinality = switch (field.getRepetition()) 
{
         case REQUIRED -> GeoParquetGroup.Cardinality.REQUIRED;
         case OPTIONAL -> GeoParquetGroup.Cardinality.OPTIONAL;
         case REPEATED -> GeoParquetGroup.Cardinality.REPEATED;
       };
+
+      // Handle geometry columns
       if (field.isPrimitive() && metadata.isGeometryColumn(field.getName())) {
         return new GeoParquetGroup.GeometryField(field.getName(), cardinality);
-      } else if (field.isPrimitive()) {
+      }
+
+      // Handle envelope columns
+      else if (!field.isPrimitive() && field.getName().equals("bbox")) {
+        GroupType groupType = field.asGroupType();
+        GeoParquetGroup.Schema geoParquetSchema = 
createGeoParquetSchema(groupType, metadata);
+        return new GeoParquetGroup.EnvelopeField(field.getName(), cardinality, 
geoParquetSchema);
+      }
+
+      // Handle group columns
+      else if (!field.isPrimitive()) {
+        GroupType groupType = field.asGroupType();
+        GeoParquetGroup.Schema geoParquetSchema = 
createGeoParquetSchema(groupType, metadata);
+        return (Field) new GeoParquetGroup.GroupField(
+            groupType.getName(),
+            GeoParquetGroup.Cardinality.REQUIRED,
+            geoParquetSchema);
+      }
+
+      // Handle primitive columns
+      else {
         PrimitiveType primitiveType = field.asPrimitiveType();
         PrimitiveTypeName primitiveTypeName = 
primitiveType.getPrimitiveTypeName();
-        String name = primitiveType.getName();
+        String columnName = primitiveType.getName();
         return switch (primitiveTypeName) {
-          case INT32 -> new GeoParquetGroup.IntegerField(name, cardinality);
-          case INT64 -> new GeoParquetGroup.LongField(name, cardinality);
-          case INT96 -> new GeoParquetGroup.Int96Field(name, cardinality);
-          case FLOAT -> new GeoParquetGroup.FloatField(name, cardinality);
-          case DOUBLE -> new GeoParquetGroup.DoubleField(name, cardinality);
-          case BOOLEAN -> new GeoParquetGroup.BooleanField(name, cardinality);
-          case BINARY -> new GeoParquetGroup.BinaryField(name, cardinality);
-          case FIXED_LEN_BYTE_ARRAY -> new GeoParquetGroup.BinaryField(name, 
cardinality);
+          case INT32 -> new GeoParquetGroup.IntegerField(columnName, 
cardinality);
+          case INT64 -> new GeoParquetGroup.LongField(columnName, cardinality);
+          case INT96 -> new GeoParquetGroup.Int96Field(columnName, 
cardinality);
+          case FLOAT -> new GeoParquetGroup.FloatField(columnName, 
cardinality);
+          case DOUBLE -> new GeoParquetGroup.DoubleField(columnName, 
cardinality);
+          case BOOLEAN -> new GeoParquetGroup.BooleanField(columnName, 
cardinality);
+          case BINARY -> new GeoParquetGroup.BinaryField(columnName, 
cardinality);
+          case FIXED_LEN_BYTE_ARRAY -> new 
GeoParquetGroup.BinaryField(columnName, cardinality);
         };
-      } else {
-        GroupType groupType = field.asGroupType();
-        return (Field) new GeoParquetGroup.GroupField(groupType.getName(),
-            GeoParquetGroup.Cardinality.REQUIRED, 
createGeoParquetSchema(groupType, metadata));
       }
     }).toList();
+
     return new GeoParquetGroup.Schema(schema.getName(), fields);
   }
 
diff --git 
a/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroupImpl.java
 
b/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroupImpl.java
index 2cd49058..9d959ca4 100644
--- 
a/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroupImpl.java
+++ 
b/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetGroupImpl.java
@@ -23,6 +23,7 @@ import org.apache.baremaps.geoparquet.GeoParquetException;
 import org.apache.parquet.io.api.Binary;
 import org.apache.parquet.io.api.RecordConsumer;
 import org.apache.parquet.schema.GroupType;
+import org.locationtech.jts.geom.Envelope;
 import org.locationtech.jts.geom.Geometry;
 import org.locationtech.jts.io.ParseException;
 import org.locationtech.jts.io.WKBReader;
@@ -38,7 +39,9 @@ public class GeoParquetGroupImpl implements GeoParquetGroup {
 
   private final List<?>[] data;
 
-  public GeoParquetGroupImpl(GroupType schema, GeoParquetMetadata metadata,
+  public GeoParquetGroupImpl(
+      GroupType schema,
+      GeoParquetMetadata metadata,
       Schema geoParquetSchema) {
     this.schema = schema;
     this.metadata = metadata;
@@ -50,11 +53,9 @@ public class GeoParquetGroupImpl implements GeoParquetGroup {
   }
 
   public GeoParquetGroupImpl addGroup(int fieldIndex) {
-    GeoParquetGroupImpl g =
-        new GeoParquetGroupImpl(schema.getType(fieldIndex).asGroupType(), 
metadata,
-            geoParquetSchema);
-    add(fieldIndex, g);
-    return g;
+    GeoParquetGroupImpl group = createGroup(fieldIndex);
+    add(fieldIndex, group);
+    return group;
   }
 
   public GeoParquetGroupImpl addGroup(String field) {
@@ -301,9 +302,21 @@ public class GeoParquetGroupImpl implements 
GeoParquetGroup {
   }
 
   @Override
-  public GeoParquetGroup createGroup(int fieldIndex) {
-    return new GeoParquetGroupImpl(schema.getType(fieldIndex).asGroupType(), 
metadata,
-        geoParquetSchema);
+  public GeoParquetGroupImpl createGroup(int fieldIndex) {
+    if (geoParquetSchema.fields().get(fieldIndex) instanceof EnvelopeField 
envelopeField) {
+      return new GeoParquetGroupImpl(schema.getType(fieldIndex).asGroupType(), 
metadata,
+          envelopeField.schema());
+    }
+
+    if (geoParquetSchema.fields().get(fieldIndex) instanceof GroupField 
groupField) {
+      return new GeoParquetGroupImpl(schema.getType(fieldIndex).asGroupType(), 
metadata,
+          groupField.schema());
+    }
+
+    GroupField field = ((GroupField) 
geoParquetSchema.fields().get(fieldIndex));
+    GeoParquetGroupImpl group =
+        new GeoParquetGroupImpl(schema.getType(fieldIndex).asGroupType(), 
metadata, field.schema());
+    return group;
   }
 
   @Override
@@ -414,6 +427,30 @@ public class GeoParquetGroupImpl implements 
GeoParquetGroup {
     return geometries;
   }
 
+  @Override
+  public Envelope getEnvelopeValue(int fieldIndex) {
+    return getEnvelopeValues(fieldIndex).get(0);
+  }
+
+  @Override
+  public List<Envelope> getEnvelopeValues(int fieldIndex) {
+    return getGroupValues(fieldIndex).stream().map(group -> {
+      double xMin = group.getSchema().fields().get(0).type().equals(Type.FLOAT)
+          ? group.getFloatValue(0)
+          : group.getDoubleValue(0);
+      double yMin = group.getSchema().fields().get(1).type().equals(Type.FLOAT)
+          ? group.getFloatValue(1)
+          : group.getDoubleValue(1);
+      double xMax = group.getSchema().fields().get(2).type().equals(Type.FLOAT)
+          ? group.getFloatValue(2)
+          : group.getDoubleValue(2);
+      double yMax = group.getSchema().fields().get(0).type().equals(Type.FLOAT)
+          ? group.getFloatValue(3)
+          : group.getDoubleValue(3);
+      return new Envelope(xMin, xMax, yMin, yMax);
+    }).toList();
+  }
+
   @Override
   public GeoParquetGroup getGroupValue(int fieldIndex) {
     return getGroupValues(fieldIndex).get(0);
@@ -524,6 +561,16 @@ public class GeoParquetGroupImpl implements 
GeoParquetGroup {
     return getGeometryValues(schema.getFieldIndex(fieldName));
   }
 
+  @Override
+  public Envelope getEnvelopeValue(String fieldName) {
+    return getEnvelopeValues(fieldName).get(0);
+  }
+
+  @Override
+  public List<Envelope> getEnvelopeValues(String fieldName) {
+    return getEnvelopeValues(schema.getFieldIndex(fieldName));
+  }
+
   @Override
   public GeoParquetGroup getGroupValue(String fieldName) {
     return getGroupValues(fieldName).get(0);
@@ -634,6 +681,16 @@ public class GeoParquetGroupImpl implements 
GeoParquetGroup {
     throw new UnsupportedOperationException();
   }
 
+  @Override
+  public void setEnvelopeValue(int fieldIndex, Envelope envelopeValue) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setEnvelopeValues(int fieldIndex, List<Envelope> envelopeValues) 
{
+    throw new UnsupportedOperationException();
+  }
+
   @Override
   public void setGroupValue(int fieldIndex, GeoParquetGroup groupValue) {
     throw new UnsupportedOperationException();
@@ -744,6 +801,16 @@ public class GeoParquetGroupImpl implements 
GeoParquetGroup {
     throw new UnsupportedOperationException();
   }
 
+  @Override
+  public void setEnvelopeValue(String fieldName, Envelope envelopeValue) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setEnvelopeValues(String fieldName, List<Envelope> 
envelopeValues) {
+    throw new UnsupportedOperationException();
+  }
+
   @Override
   public void setGroupValue(String fieldName, GeoParquetGroup groupValue) {
     throw new UnsupportedOperationException();
diff --git 
a/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetMetadata.java
 
b/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetMetadata.java
index 43c9f032..fe3955d7 100644
--- 
a/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetMetadata.java
+++ 
b/baremaps-geoparquet/src/main/java/org/apache/baremaps/geoparquet/data/GeoParquetMetadata.java
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.base.Objects;
 import java.util.Map;
+import java.util.Optional;
 
 public class GeoParquetMetadata {
 
@@ -58,15 +59,17 @@ public class GeoParquetMetadata {
   }
 
   public int getSrid(String column) {
-    JsonNode crsId = getColumns().get(column).getCrs().get("id");
-    return switch (crsId.get("authority").asText()) {
-      case "OGC" -> switch (crsId.get("code").asText()) {
-          case "CRS84" -> 4326;
-          default -> 0;
-        };
-      case "EPSG" -> crsId.get("code").asInt();
-      default -> 0;
-    };
+    return Optional.ofNullable(getColumns().get(column).getCrs()).map(crs -> {
+      JsonNode id = crs.get("id");
+      return switch (id.get("authority").asText()) {
+        case "OGC" -> switch (id.get("code").asText()) {
+            case "CRS84" -> 4326;
+            default -> 0;
+          };
+        case "EPSG" -> id.get("code").asInt();
+        default -> 0;
+      };
+    }).orElse(4326);
   }
 
   public boolean isGeometryColumn(String column) {

Reply via email to