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 39a8abf6b Add a csv data store (#901)
39a8abf6b is described below

commit 39a8abf6b9ca2371a9dc5d48bef2bfae28370b9e
Author: Bertil Chapuis <[email protected]>
AuthorDate: Thu Oct 31 15:43:29 2024 +0100

    Add a csv data store (#901)
    
    * Add a csv datastore
    
    * Add tests that parses csv files and a sample geonames file
    
    * Format the source code
    
    * Fix issues identified by codeql and sonar
---
 .../apache/baremaps/storage/csv/CsvDataStore.java  |  98 ++++++++++
 .../apache/baremaps/storage/csv/CsvDataTable.java  | 201 +++++++++++++++++++++
 .../storage/csv/CsvDataTableGeonamesTest.java      | 126 +++++++++++++
 .../baremaps/storage/csv/CsvDataTableTest.java     | 195 ++++++++++++++++++++
 .../org/apache/baremaps/testing/TestFiles.java     |   3 +
 5 files changed, 623 insertions(+)

diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataStore.java 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataStore.java
new file mode 100644
index 000000000..93b0624a9
--- /dev/null
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataStore.java
@@ -0,0 +1,98 @@
+/*
+ * 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.storage.csv;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import org.apache.baremaps.data.storage.DataSchema;
+import org.apache.baremaps.data.storage.DataStore;
+import org.apache.baremaps.data.storage.DataStoreException;
+import org.apache.baremaps.data.storage.DataTable;
+
+/**
+ * A DataStore implementation that manages a single CSV file.
+ */
+public class CsvDataStore implements DataStore {
+
+  private final String tableName;
+  private final DataSchema schema;
+  private final CsvDataTable dataTable;
+
+  /**
+   * Constructs a CsvDataStore with the specified table name, schema, and CSV 
file.
+   *
+   * @param tableName the name of the table
+   * @param schema the data schema defining the structure
+   * @param csvFile the CSV file to read data from
+   * @param hasHeader whether the CSV file includes a header row
+   * @param separator the character used to separate columns in the CSV file
+   * @throws IOException if an I/O error occurs
+   */
+  public CsvDataStore(String tableName, DataSchema schema, File csvFile, 
boolean hasHeader,
+      char separator) throws IOException {
+    this.tableName = tableName;
+    this.schema = schema;
+    this.dataTable = new CsvDataTable(schema, csvFile, hasHeader, separator);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<String> list() throws DataStoreException {
+    return Collections.singletonList(tableName);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public DataTable get(String name) throws DataStoreException {
+    if (this.tableName.equals(name)) {
+      return dataTable;
+    } else {
+      throw new DataStoreException("Table '" + name + "' not found.");
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void add(DataTable table) throws DataStoreException {
+    throw new UnsupportedOperationException("Adding tables is not supported in 
CsvDataStore.");
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void add(String name, DataTable table) throws DataStoreException {
+    throw new UnsupportedOperationException("Adding tables is not supported in 
CsvDataStore.");
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void remove(String name) throws DataStoreException {
+    throw new UnsupportedOperationException("Removing tables is not supported 
in CsvDataStore.");
+  }
+}
diff --git 
a/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataTable.java 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataTable.java
new file mode 100644
index 000000000..033578a69
--- /dev/null
+++ 
b/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataTable.java
@@ -0,0 +1,201 @@
+/*
+ * 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.storage.csv;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.dataformat.csv.CsvMapper;
+import com.fasterxml.jackson.dataformat.csv.CsvSchema;
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import org.apache.baremaps.data.storage.*;
+import org.locationtech.jts.io.WKTReader;
+
+/**
+ * A DataTable implementation that reads data from a CSV file using Jackson.
+ */
+public class CsvDataTable implements DataTable {
+
+  private final DataSchema schema;
+  private final File csvFile;
+  private final CsvSchema csvSchema;
+  private final long size;
+
+  /**
+   * Constructs a CsvDataTable with the specified schema, CSV file, header 
presence, and separator.
+   *
+   * @param schema the data schema defining the structure
+   * @param csvFile the CSV file to read data from
+   * @param hasHeader whether the CSV file includes a header row
+   * @param separator the character used to separate columns in the CSV file
+   * @throws IOException if an I/O error occurs
+   */
+  public CsvDataTable(DataSchema schema, File csvFile, boolean hasHeader, char 
separator)
+      throws IOException {
+    this.schema = schema;
+    this.csvFile = csvFile;
+    this.csvSchema = buildCsvSchema(schema, hasHeader, separator);
+    this.size = calculateSize();
+  }
+
+  /**
+   * Builds the CsvSchema for Jackson based on the provided DataSchema, header 
presence, and
+   * separator.
+   *
+   * @param dataSchema the data schema
+   * @param hasHeader whether the CSV file includes a header row
+   * @param separator the character used to separate columns
+   * @return the CsvSchema for Jackson
+   */
+  private CsvSchema buildCsvSchema(DataSchema dataSchema, boolean hasHeader, 
char separator) {
+    CsvSchema.Builder builder = CsvSchema.builder();
+    for (DataColumn column : dataSchema.columns()) {
+      builder.addColumn(column.name());
+    }
+    return 
builder.setUseHeader(hasHeader).setColumnSeparator(separator).build();
+  }
+
+  /**
+   * Calculates the number of rows in the CSV file.
+   *
+   * @return the number of rows
+   * @throws IOException if an I/O error occurs
+   */
+  private long calculateSize() throws IOException {
+    try (var parser = new CsvMapper().readerFor(Map.class)
+        .with(csvSchema)
+        .createParser(csvFile)) {
+      long rowCount = 0;
+      while (parser.nextToken() != null) {
+        if (parser.currentToken() == JsonToken.START_OBJECT) {
+          rowCount++;
+        }
+      }
+      return rowCount;
+    }
+  }
+
+  @Override
+  public DataSchema schema() {
+    return schema;
+  }
+
+  @Override
+  public boolean add(DataRow row) {
+    throw new UnsupportedOperationException("Adding rows is not supported.");
+  }
+
+  @Override
+  public void clear() {
+    throw new UnsupportedOperationException("Clearing rows is not supported.");
+  }
+
+  @Override
+  public long size() {
+    return size;
+  }
+
+  @Override
+  public Iterator<DataRow> iterator() {
+    try {
+      CsvMapper csvMapper = new CsvMapper();
+      JsonParser parser = csvMapper.readerFor(Map.class)
+          .with(csvSchema)
+          .createParser(csvFile);
+
+      Iterator<Map<String, String>> csvIterator = 
csvMapper.readerFor(Map.class)
+          .with(csvSchema)
+          .readValues(parser);
+
+      return new Iterator<>() {
+        @Override
+        public boolean hasNext() {
+          return csvIterator.hasNext();
+        }
+
+        @Override
+        public DataRow next() {
+          Map<String, String> csvRow = csvIterator.next();
+          DataRow dataRow = schema.createRow();
+
+          for (int i = 0; i < schema.columns().size(); i++) {
+            DataColumn column = schema.columns().get(i);
+            String columnName = column.name();
+            String value = csvRow.get(columnName);
+
+            if (value != null) {
+              Object parsedValue = parseValue(column, value);
+              dataRow.set(i, parsedValue);
+            } else {
+              dataRow.set(i, null);
+            }
+          }
+          return dataRow;
+        }
+      };
+
+    } catch (IOException e) {
+      throw new DataStoreException("Error reading CSV file", e);
+    }
+  }
+
+  /**
+   * Parses the string value from the CSV according to the column type.
+   *
+   * @param column the data column
+   * @param value the string value from the CSV
+   * @return the parsed value
+   */
+  private Object parseValue(DataColumn column, String value) {
+    DataColumn.Type type = column.type();
+    try {
+      if (value == null || value.isEmpty()) {
+        return null;
+      }
+        return switch (type) {
+            case STRING -> value;
+            case INTEGER -> Integer.parseInt(value);
+            case LONG -> Long.parseLong(value);
+            case FLOAT -> Float.parseFloat(value);
+            case DOUBLE -> Double.parseDouble(value);
+            case BOOLEAN -> Boolean.parseBoolean(value);
+            case GEOMETRY, POINT, LINESTRING, POLYGON, MULTIPOINT, 
MULTILINESTRING, MULTIPOLYGON,
+                 GEOMETRYCOLLECTION -> {
+                WKTReader reader = new WKTReader();
+                yield reader.read(value);
+            }
+            default -> throw new IllegalArgumentException("Unsupported column 
type: " + type);
+        };
+    } catch (Exception e) {
+      throw new DataStoreException("Error parsing value for column " + 
column.name(), e);
+    }
+  }
+
+  @Override
+  public Spliterator<DataRow> spliterator() {
+    return Spliterators.spliteratorUnknownSize(iterator(), 
Spliterator.ORDERED);
+  }
+
+  @Override
+  public Stream<DataRow> stream() {
+    return StreamSupport.stream(spliterator(), false);
+  }
+}
diff --git 
a/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableGeonamesTest.java
 
b/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableGeonamesTest.java
new file mode 100644
index 000000000..5fa787b3e
--- /dev/null
+++ 
b/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableGeonamesTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.storage.csv;
+
+import static org.apache.baremaps.testing.TestFiles.GEONAMES_CSV;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.util.List;
+import org.apache.baremaps.data.storage.*;
+import org.junit.jupiter.api.*;
+import org.locationtech.jts.geom.Point;
+
+class CsvDataTableGeonamesTest {
+
+  @Test
+  void testGeonamesCsvDataTable() throws IOException {
+    List<DataColumn> columns = List.of(
+        new DataColumnFixed("id", DataColumn.Cardinality.REQUIRED, 
DataColumn.Type.INTEGER),
+        new DataColumnFixed("name", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING),
+        new DataColumnFixed("asciiname", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING),
+        new DataColumnFixed("alternatenames", DataColumn.Cardinality.OPTIONAL,
+            DataColumn.Type.STRING),
+        new DataColumnFixed("latitude", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.DOUBLE),
+        new DataColumnFixed("longitude", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.DOUBLE),
+        new DataColumnFixed("feature_class", DataColumn.Cardinality.OPTIONAL,
+            DataColumn.Type.STRING),
+        new DataColumnFixed("feature_code", DataColumn.Cardinality.OPTIONAL,
+            DataColumn.Type.STRING),
+        new DataColumnFixed("country_code", DataColumn.Cardinality.OPTIONAL,
+            DataColumn.Type.STRING),
+        new DataColumnFixed("cc2", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING),
+        new DataColumnFixed("admin1_code", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING),
+        new DataColumnFixed("admin2_code", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING),
+        new DataColumnFixed("admin3_code", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING),
+        new DataColumnFixed("admin4_code", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING),
+        new DataColumnFixed("population", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.LONG),
+        new DataColumnFixed("elevation", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.INTEGER),
+        new DataColumnFixed("dem", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.INTEGER),
+        new DataColumnFixed("timezone", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING),
+        new DataColumnFixed("modification_date", 
DataColumn.Cardinality.OPTIONAL,
+            DataColumn.Type.STRING));
+    DataSchema schema = new DataSchemaImpl("geonames", columns);
+
+    boolean hasHeader = false;
+    char separator = '\t';
+    DataTable dataTable = new CsvDataTable(schema, GEONAMES_CSV.toFile(), 
hasHeader, separator);
+
+    assertEquals(5, dataTable.size(), "DataTable should have 5 rows.");
+
+    int rowCount = 0;
+    for (DataRow row : dataTable) {
+      rowCount++;
+
+      // Extract values
+      Integer id = (Integer) row.get("id");
+      String name = (String) row.get("name");
+      Double latitude = (Double) row.get("latitude");
+      Double longitude = (Double) row.get("longitude");
+
+      // Perform assertions for each row
+      assertNotNull(id, "ID should not be null.");
+      assertNotNull(name, "Name should not be null.");
+      assertNotNull(latitude, "Latitude should not be null.");
+      assertNotNull(longitude, "Longitude should not be null.");
+
+      switch (id) {
+        case 1:
+          assertEquals("HEIG", name);
+          assertEquals(1.111, latitude);
+          assertEquals(1.111, longitude);
+          break;
+        case 2:
+          assertEquals("Yverdon-les-bains", name);
+          assertEquals(2.222, latitude);
+          assertEquals(2.222, longitude);
+          break;
+        case 3:
+          assertEquals("Route de Cheseaux 1", name);
+          assertEquals(3.333, latitude);
+          assertEquals(3.333, longitude);
+          break;
+        case 4:
+          assertEquals("Switzerland", name);
+          assertEquals(4.444, latitude);
+          assertEquals(4.444, longitude);
+          break;
+        case 5:
+          assertEquals("Switzerland", name);
+          assertEquals(47.00016, latitude);
+          assertEquals(8.01427, longitude);
+          break;
+        default:
+          fail("Unexpected ID: " + id);
+      }
+
+      Point point = createPoint(longitude, latitude);
+      assertNotNull(point, "Point geometry should not be null.");
+    }
+    assertEquals(5, rowCount, "Row count should be 5.");
+  }
+
+  private Point createPoint(Double longitude, Double latitude) {
+    if (longitude != null && latitude != null) {
+      return new org.locationtech.jts.geom.GeometryFactory()
+          .createPoint(new org.locationtech.jts.geom.Coordinate(longitude, 
latitude));
+    } else {
+      return null;
+    }
+  }
+}
diff --git 
a/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableTest.java
 
b/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableTest.java
new file mode 100644
index 000000000..ede62effb
--- /dev/null
+++ 
b/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.storage.csv;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import org.apache.baremaps.data.storage.*;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.locationtech.jts.geom.Geometry;
+
+class CsvDataTableTest {
+
+  private File tempCsvFile;
+
+  @BeforeEach
+  void setUp() throws IOException {
+    Path tempCsvFilePath = Files.createTempFile("test", ".csv");
+    tempCsvFile = tempCsvFilePath.toFile();
+    tempCsvFile.deleteOnExit();
+  }
+
+  @AfterEach
+  void tearDown() {
+    if (tempCsvFile.exists()) {
+      tempCsvFile.delete();
+    }
+  }
+
+  @Test
+  void testCsvWithHeaderAndCommaSeparator() throws IOException {
+    String csvContent = """
+        id,name,geom
+        1,PointA,"POINT(1 1)"
+        2,PointB,"POINT(2 2)"
+        """;
+    Files.writeString(tempCsvFile.toPath(), csvContent);
+    List<DataColumn> columns = List.of(
+        new DataColumnFixed("id", DataColumn.Cardinality.REQUIRED, 
DataColumn.Type.INTEGER),
+        new DataColumnFixed("name", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING),
+        new DataColumnFixed("geom", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.GEOMETRY));
+    DataSchema schema = new DataSchemaImpl("test_table", columns);
+    DataTable dataTable = new CsvDataTable(schema, tempCsvFile, true, ',');
+    assertEquals(2, dataTable.size());
+    int rowCount = 0;
+    for (DataRow row : dataTable) {
+      rowCount++;
+      Integer id = (Integer) row.get("id");
+      String name = (String) row.get("name");
+      Geometry geometry = (Geometry) row.get("geom");
+      assertNotNull(id);
+      assertNotNull(name);
+      assertNotNull(geometry);
+      assertEquals("Point" + (rowCount == 1 ? "A" : "B"), name);
+      assertEquals("POINT (" + rowCount + " " + rowCount + ")", 
geometry.toText());
+    }
+    assertEquals(2, rowCount);
+  }
+
+  @Test
+  void testCsvWithoutHeaderAndSemicolonSeparator() throws IOException {
+    String csvContent = """
+        1;PointA;"POINT(1 1)"
+        2;PointB;"POINT(2 2)"
+        """;
+    Files.writeString(tempCsvFile.toPath(), csvContent);
+    List<DataColumn> columns = List.of(
+        new DataColumnFixed("column1", DataColumn.Cardinality.REQUIRED, 
DataColumn.Type.INTEGER),
+        new DataColumnFixed("column2", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING),
+        new DataColumnFixed("column3", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.GEOMETRY));
+    DataSchema schema = new DataSchemaImpl("test_table", columns);
+    DataTable dataTable = new CsvDataTable(schema, tempCsvFile, false, ';');
+    assertEquals(2, dataTable.size());
+    int rowCount = 0;
+    for (DataRow row : dataTable) {
+      rowCount++;
+      Integer id = (Integer) row.get("column1");
+      String name = (String) row.get("column2");
+      Geometry geometry = (Geometry) row.get("column3");
+
+      // Verify data
+      assertNotNull(id);
+      assertNotNull(name);
+      assertNotNull(geometry);
+
+      assertEquals("Point" + (rowCount == 1 ? "A" : "B"), name);
+      assertEquals("POINT (" + rowCount + " " + rowCount + ")", 
geometry.toText());
+    }
+    assertEquals(2, rowCount);
+  }
+
+  @Test
+  void testCsvWithDifferentDataTypes() throws IOException {
+    String csvContent = """
+        int_col,double_col,bool_col,string_col
+        1,1.1,true,Hello
+        2,2.2,false,World
+        """;
+    Files.writeString(tempCsvFile.toPath(), csvContent);
+    List<DataColumn> columns = List.of(
+        new DataColumnFixed("int_col", DataColumn.Cardinality.REQUIRED, 
DataColumn.Type.INTEGER),
+        new DataColumnFixed("double_col", DataColumn.Cardinality.REQUIRED, 
DataColumn.Type.DOUBLE),
+        new DataColumnFixed("bool_col", DataColumn.Cardinality.REQUIRED, 
DataColumn.Type.BOOLEAN),
+        new DataColumnFixed("string_col", DataColumn.Cardinality.REQUIRED, 
DataColumn.Type.STRING));
+    DataSchema schema = new DataSchemaImpl("test_table", columns);
+    DataTable dataTable = new CsvDataTable(schema, tempCsvFile, true, ',');
+    assertEquals(2, dataTable.size());
+    int rowCount = 0;
+    for (DataRow row : dataTable) {
+      rowCount++;
+      Integer intCol = (Integer) row.get("int_col");
+      Double doubleCol = (Double) row.get("double_col");
+      Boolean boolCol = (Boolean) row.get("bool_col");
+      String stringCol = (String) row.get("string_col");
+
+      // Verify data
+      assertEquals(rowCount, intCol);
+      assertEquals(rowCount * 1.1, doubleCol);
+      assertEquals(rowCount == 1, boolCol);
+      assertEquals(rowCount == 1 ? "Hello" : "World", stringCol);
+    }
+    assertEquals(2, rowCount);
+  }
+
+  @Test
+  void testCsvWithInvalidData() throws IOException {
+    String csvContent = """
+        id,name
+        abc,TestName
+        """;
+    Files.writeString(tempCsvFile.toPath(), csvContent);
+    List<DataColumn> columns = List.of(
+        new DataColumnFixed("id", DataColumn.Cardinality.REQUIRED, 
DataColumn.Type.INTEGER),
+        new DataColumnFixed("name", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING));
+    DataSchema schema = new DataSchemaImpl("test_table", columns);
+    DataTable dataTable = new CsvDataTable(schema, tempCsvFile, true, ',');
+    assertThrows(RuntimeException.class, () -> {
+      for (DataRow row : dataTable) {
+        // This line should throw an exception because abc is not a valid 
integer
+        row.values();
+      }
+    });
+  }
+
+  @Test
+  void testAddAndClearUnsupportedOperations() throws IOException {
+    String csvContent = "";
+    Files.writeString(tempCsvFile.toPath(), csvContent);
+    List<DataColumn> columns = List.of(
+        new DataColumnFixed("id", DataColumn.Cardinality.REQUIRED, 
DataColumn.Type.INTEGER),
+        new DataColumnFixed("name", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING));
+    DataSchema schema = new DataSchemaImpl("test_table", columns);
+    DataTable dataTable = new CsvDataTable(schema, tempCsvFile, true, ',');
+    assertThrows(UnsupportedOperationException.class, () -> 
dataTable.add(null));
+    assertThrows(UnsupportedOperationException.class, dataTable::clear);
+  }
+
+  @Test
+  void testSizeCalculation() throws IOException {
+    String csvContent = """
+        id,name
+        1,Name1
+        2,Name2
+        3,Name3
+        """;
+    Files.writeString(tempCsvFile.toPath(), csvContent);
+    List<DataColumn> columns = List.of(
+        new DataColumnFixed("id", DataColumn.Cardinality.REQUIRED, 
DataColumn.Type.INTEGER),
+        new DataColumnFixed("name", DataColumn.Cardinality.OPTIONAL, 
DataColumn.Type.STRING));
+    DataSchema schema = new DataSchemaImpl("test_table", columns);
+    DataTable dataTable = new CsvDataTable(schema, tempCsvFile, true, ',');
+    assertEquals(3, dataTable.size());
+  }
+}
diff --git 
a/baremaps-testing/src/main/java/org/apache/baremaps/testing/TestFiles.java 
b/baremaps-testing/src/main/java/org/apache/baremaps/testing/TestFiles.java
index 4e7e2e2a9..2b284166e 100644
--- a/baremaps-testing/src/main/java/org/apache/baremaps/testing/TestFiles.java
+++ b/baremaps-testing/src/main/java/org/apache/baremaps/testing/TestFiles.java
@@ -76,6 +76,9 @@ public class TestFiles {
   public static final Path TILEJSON_JSON =
       resolve("baremaps-testing/data/tilesets/tilejson.json");
 
+  public static final Path GEONAMES_CSV =
+      resolve("baremaps-testing/data/geonames/sample.txt");
+
   /* The geometries of the osm-sample/sample.osm.xml file */
 
   private static final GeometryFactory GEOMETRY_FACTORY = new 
GeometryFactory();

Reply via email to