This is an automated email from the ASF dual-hosted git repository. bchapuis pushed a commit to branch calcite-schema-ddl in repository https://gitbox.apache.org/repos/asf/incubator-baremaps.git
commit dbbeea8d75449272e1bb008bfe9bc096c7930d4d Author: Bertil Chapuis <[email protected]> AuthorDate: Mon Apr 14 17:09:04 2025 +0200 Add schema for shapefiles --- .../calcite/shapefile/ShapefileSchema.java | 155 +++++++++++++++++++++ .../calcite/shapefile/ShapefileSchemaTest.java | 154 ++++++++++++++++++++ 2 files changed, 309 insertions(+) diff --git a/baremaps-calcite/src/main/java/org/apache/baremaps/calcite/shapefile/ShapefileSchema.java b/baremaps-calcite/src/main/java/org/apache/baremaps/calcite/shapefile/ShapefileSchema.java new file mode 100644 index 000000000..e66887b74 --- /dev/null +++ b/baremaps-calcite/src/main/java/org/apache/baremaps/calcite/shapefile/ShapefileSchema.java @@ -0,0 +1,155 @@ +/* + * 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 + * 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.calcite.shapefile; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.schema.Table; +import org.apache.calcite.schema.impl.AbstractSchema; + +/** + * A Calcite schema implementation for shapefile data. This schema provides access to shapefiles + * through the Apache Calcite framework for SQL querying. + */ +public class ShapefileSchema extends AbstractSchema { + + private final File directory; + private final Map<String, Table> tableMap; + private final RelDataTypeFactory typeFactory; + + /** + * Constructs a ShapefileSchema with the specified directory. + * + * @param directory the directory containing shapefiles + * @param typeFactory the type factory to use for creating tables + * @throws IOException if an I/O error occurs + */ + public ShapefileSchema(File directory, RelDataTypeFactory typeFactory) throws IOException { + this.directory = Objects.requireNonNull(directory, "Directory cannot be null"); + this.typeFactory = Objects.requireNonNull(typeFactory, "Type factory cannot be null"); + this.tableMap = new HashMap<>(); + + // Process files in the directory + File[] files = directory.listFiles((dir, name) -> + name.toLowerCase().endsWith(".shp")); + + if (files != null) { + for (File file : files) { + // Extract the base name without extension (e.g., "countries" from "countries.shp") + String fileName = file.getName(); + String tableName = fileName; + + // Remove all extensions (e.g., "countries.shp" -> "countries") + while (tableName.contains(".")) { + int lastDotIndex = tableName.lastIndexOf('.'); + if (lastDotIndex > 0) { + tableName = tableName.substring(0, lastDotIndex); + } else { + break; + } + } + + // Create the table with the file reference + tableMap.put(tableName, createTable(file)); + } + } + } + + /** + * Constructs a ShapefileSchema with a single file. + * + * @param file the shapefile + * @param typeFactory the type factory to use for creating tables + * @throws IOException if an I/O error occurs + */ + public ShapefileSchema(File file, RelDataTypeFactory typeFactory, boolean isDirectory) throws IOException { + if (isDirectory) { + // If isDirectory is true, treat the file as a directory + this.directory = Objects.requireNonNull(file, "Directory cannot be null"); + this.typeFactory = Objects.requireNonNull(typeFactory, "Type factory cannot be null"); + this.tableMap = new HashMap<>(); + + // Process files in the directory + File[] files = file.listFiles((dir, name) -> + name.toLowerCase().endsWith(".shp")); + + if (files != null) { + for (File shapeFile : files) { + // Extract the base name without extension (e.g., "countries" from "countries.shp") + String fileName = shapeFile.getName(); + String tableName = fileName; + + // Remove all extensions (e.g., "countries.shp" -> "countries") + while (tableName.contains(".")) { + int lastDotIndex = tableName.lastIndexOf('.'); + if (lastDotIndex > 0) { + tableName = tableName.substring(0, lastDotIndex); + } else { + break; + } + } + + // Create the table with the file reference + tableMap.put(tableName, createTable(shapeFile)); + } + } + } else { + // If isDirectory is false, treat the file as a single file + this.directory = Objects.requireNonNull(file, "File cannot be null"); + this.typeFactory = Objects.requireNonNull(typeFactory, "Type factory cannot be null"); + this.tableMap = new HashMap<>(); + + // Extract the base name without extension (e.g., "countries" from "countries.shp") + String fileName = file.getName(); + String tableName = fileName; + + // Remove all extensions (e.g., "countries.shp" -> "countries") + while (tableName.contains(".")) { + int lastDotIndex = tableName.lastIndexOf('.'); + if (lastDotIndex > 0) { + tableName = tableName.substring(0, lastDotIndex); + } else { + break; + } + } + + // Create the table with the file reference + tableMap.put(tableName, createTable(file)); + } + } + + /** + * Creates a table for the given file. + * + * @param file the shapefile + * @return the created table + * @throws IOException if an I/O error occurs + */ + private Table createTable(File file) throws IOException { + return new ShapefileTable(file); + } + + @Override + protected Map<String, Table> getTableMap() { + return tableMap; + } +} \ No newline at end of file diff --git a/baremaps-calcite/src/test/java/org/apache/baremaps/calcite/shapefile/ShapefileSchemaTest.java b/baremaps-calcite/src/test/java/org/apache/baremaps/calcite/shapefile/ShapefileSchemaTest.java new file mode 100644 index 000000000..fb534dd0e --- /dev/null +++ b/baremaps-calcite/src/test/java/org/apache/baremaps/calcite/shapefile/ShapefileSchemaTest.java @@ -0,0 +1,154 @@ +/* + * 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 + * 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.calcite.shapefile; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import org.apache.baremaps.testing.TestFiles; +import org.apache.calcite.config.CalciteConnectionConfig; +import org.apache.calcite.config.CalciteConnectionConfigImpl; +import org.apache.calcite.config.CalciteConnectionProperty; +import org.apache.calcite.jdbc.CalciteConnection; +import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.schema.SchemaPlus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Test class for the ShapefileSchema implementation. + */ +public class ShapefileSchemaTest { + + @TempDir + Path tempDir; + + @BeforeEach + public void setup() throws SQLException, IOException { + // Create a temporary directory for test files + Path testDir = tempDir.resolve("shapefile-test"); + Files.createDirectories(testDir); + + // Copy the sample shapefile to the test directory + Path sourceFile = TestFiles.POINT_SHP; + Path targetFile = testDir.resolve("point.shp"); + Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); + + // Copy the associated .dbf file + Path sourceDbf = sourceFile.resolveSibling("point.dbf"); + Path targetDbf = targetFile.resolveSibling("point.dbf"); + Files.copy(sourceDbf, targetDbf, StandardCopyOption.REPLACE_EXISTING); + + // Copy the associated .shx file + Path sourceShx = sourceFile.resolveSibling("point.shx"); + Path targetShx = targetFile.resolveSibling("point.shx"); + Files.copy(sourceShx, targetShx, StandardCopyOption.REPLACE_EXISTING); + } + + @Test + public void testSchemaCreation() throws SQLException, IOException { + // Create a connection to Calcite + Connection connection = DriverManager.getConnection("jdbc:calcite:"); + CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class); + SchemaPlus rootSchema = calciteConnection.getRootSchema(); + + // Create and register the shapefile schema + ShapefileSchema schema = new ShapefileSchema(tempDir.resolve("shapefile-test").toFile(), calciteConnection.getTypeFactory()); + rootSchema.add("shapefile", schema); + + // Verify that the schema contains the expected table + assertTrue(rootSchema.getSubSchemaNames().contains("shapefile")); + assertTrue(rootSchema.getSubSchema("shapefile").getTableNames().contains("point")); + + connection.close(); + } + + @Test + public void testSqlQuery() throws SQLException, IOException { + // Create a connection to Calcite + Connection connection = DriverManager.getConnection("jdbc:calcite:"); + CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class); + SchemaPlus rootSchema = calciteConnection.getRootSchema(); + + // Create and register the shapefile schema + ShapefileSchema schema = new ShapefileSchema(tempDir.resolve("shapefile-test").toFile(), calciteConnection.getTypeFactory()); + rootSchema.add("shapefile", schema); + + // Execute a simple SQL query - use lowercase for schema and table names + try (Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery( + "SELECT * FROM \"shapefile\".\"point\""); + + // Verify that we get results + assertTrue(resultSet.next()); + + // Verify that the result set has the expected columns + // Note: The actual column names will depend on the sample shapefile + assertNotNull(resultSet.getMetaData()); + assertTrue(resultSet.getMetaData().getColumnCount() > 0); + } + + connection.close(); + } + + @Test + public void testSingleFileSchema() throws SQLException, IOException { + // Create a connection to Calcite + Connection connection = DriverManager.getConnection("jdbc:calcite:"); + CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class); + SchemaPlus rootSchema = calciteConnection.getRootSchema(); + + // Get the sample shapefile + File sampleFile = tempDir.resolve("shapefile-test").resolve("point.shp").toFile(); + + // Create and register the shapefile schema with a single file + ShapefileSchema schema = new ShapefileSchema(sampleFile, calciteConnection.getTypeFactory(), false); + rootSchema.add("single", schema); + + // Verify that the schema contains the expected table + assertTrue(rootSchema.getSubSchemaNames().contains("single")); + assertTrue(rootSchema.getSubSchema("single").getTableNames().contains("point")); + + // Execute a simple SQL query - use lowercase for schema and table names + try (Statement statement = connection.createStatement()) { + ResultSet resultSet = statement.executeQuery( + "SELECT * FROM \"single\".\"point\""); + + // Verify that we get results + assertTrue(resultSet.next()); + + // Verify that the result set has the expected columns + assertNotNull(resultSet.getMetaData()); + assertTrue(resultSet.getMetaData().getColumnCount() > 0); + } + + connection.close(); + } +} \ No newline at end of file
