This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 38bf69764d Add a `Dialect.DUCKDB` case together with specific code in SQL store. Contains modifications to SQL store internal for accommodating DuckDB. 38bf69764d is described below commit 38bf69764dc1f8d52ad996344ea280b40c9551aa Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue Mar 18 19:46:34 2025 +0100 Add a `Dialect.DUCKDB` case together with specific code in SQL store. Contains modifications to SQL store internal for accommodating DuckDB. --- .../org/apache/sis/metadata/sql/privy/Dialect.java | 52 ++++++++++- .../main/module-info.java | 1 + .../org/apache/sis/storage/sql/duckdb/DuckDB.java | 88 ++++++++++++++++++ .../storage/sql/duckdb/ExtendedClauseWriter.java | 61 ++++++++++++ .../sis/storage/sql/duckdb/package-info.java | 60 ++++++++++++ .../apache/sis/storage/sql/feature/Analyzer.java | 2 + .../apache/sis/storage/sql/feature/Database.java | 23 ++++- .../sis/storage/sql/feature/FeatureIterator.java | 11 ++- .../sis/storage/sql/feature/GeometryEncoding.java | 39 ++++++++ .../sis/storage/sql/feature/GeometryGetter.java | 103 +++++++++++++-------- .../storage/sql/feature/SelectionClauseWriter.java | 3 +- .../storage/sql/feature/GeometryGetterTest.java | 2 +- .../sis/storage/sql/postgis/RasterReaderTest.java | 5 +- .../sis/storage/sql/postgis/RasterWriterTest.java | 4 +- 14 files changed, 405 insertions(+), 49 deletions(-) diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java index c1f2b16716..0cccbb1cd2 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java @@ -19,6 +19,7 @@ package org.apache.sis.metadata.sql.privy; import java.sql.SQLException; import java.sql.DatabaseMetaData; import org.apache.sis.util.CharSequences; +import org.apache.sis.util.Workaround; import org.apache.sis.util.privy.Constants; @@ -83,7 +84,40 @@ public enum Dialect { * * @see <a href="https://www.sqlite.org/omitted.html">SQL Features That SQLite Does Not Implement</a> */ - SQLITE("sqlite", 0); + SQLITE("sqlite", 0), + + /** + * The database uses DuckDB syntax. This is subset of SQL. DuckDB is not designed for transactional + * applications, but rather for analytical processing. It runs on the local machine without server. + * + * <h4>Spatial extension</h4> + * The following <abbr>SQL</abbr> statement needs to be executed at least once when DuckDB + * is used for the first time. It can be executed with a {@link java.sql.Statement}. + * + * {@snippet lang="sql" : + * INSTALL spatial + * } + * + * Then, the following <abbr>SQL</abbr> statement should be executed on every new connection. + * Actually, in our tests, it appears sometime necessary, sometime not. + * + * {@snippet lang="sql" : + * LOAD spatial + * } + * + * @see <a href="https://github.com/duckdb/duckdb-java/issues/165">DuckDB-Java issue #165</a> + */ + DUCKDB("duckdb", 0) { + @Override + @Workaround(library = "DuckDB", version = "1.2.1") + public String toCompatibleMetadataPattern(String pattern, final int argument) { + switch (argument) { + case 1: if (pattern == null) pattern = "%"; break; + case 2: pattern = pattern.replace("\\", ""); break; + } + return pattern; + } + }; /** * The protocol in JDBC URL, or {@code null} if unknown. @@ -159,6 +193,22 @@ public enum Dialect { return (flags & Supports.CONCURRENCY) != 0; } + /** + * Converts the pattern to something that can be used for requesting metadata. + * This is a workaround for a DuckDB bug and may be removed in a future version. + * + * @param pattern the schema pattern to apply. + * @param argument 1 for the {@code schemaPattern}, 2 for {@code functionNamePattern}. + * @return the schema pattern to use. + * + * @see DatabaseMetaData#getFunctions(String, String, String) + * @see <a href="https://github.com/duckdb/duckdb-java/issues/165">DuckDB-Java issue #165</a> + */ + @Workaround(library = "DuckDB", version = "1.2.1") + public String toCompatibleMetadataPattern(String pattern, int argument) { + return pattern; + } + /** * Returns the presumed SQL dialect. * If this method cannot guess the dialect, than {@link #ANSI} is presumed. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/module-info.java b/endorsed/src/org.apache.sis.storage.sql/main/module-info.java index aa3eab4fe6..c34df3d4f4 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/module-info.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/module-info.java @@ -41,6 +41,7 @@ * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) * @author Alexis Manin (Geomatys) + * @author Guilhem Legal (Geomatys) * @version 1.5 * @since 1.0 */ diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/DuckDB.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/DuckDB.java new file mode 100644 index 0000000000..58f6ada7da --- /dev/null +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/DuckDB.java @@ -0,0 +1,88 @@ +/* + * 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.sis.storage.sql.duckdb; + +import java.util.Locale; +import java.util.concurrent.locks.ReadWriteLock; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import javax.sql.DataSource; +import org.apache.sis.geometry.wrapper.Geometries; +import org.apache.sis.metadata.sql.privy.Dialect; +import org.apache.sis.storage.event.StoreListeners; +import org.apache.sis.storage.sql.feature.Column; +import org.apache.sis.storage.sql.feature.Database; +import org.apache.sis.storage.sql.feature.GeometryEncoding; +import org.apache.sis.storage.sql.feature.SelectionClauseWriter; + + +/** + * Information about a connection to a DuckDB database. + * This class specializes some of the functions for converting DuckDB spatial extension objects to Java objects. + * See the package Javadoc for recommendation about how to connect to a DuckDB database. + * + * @param <G> the type of geometry objects. Depends on the backing implementation (ESRI, JTS, Java2D…). + * + * @author Guilhem Legal (Geomatys) + * @author Martin Desruisseaux (Geomatys) + */ +public final class DuckDB<G> extends Database<G> { + /** + * Creates a new session for a DuckDB database. + * + * @param source provider of (pooled) connections to the database. + * @param metadata metadata about the database for which a session is created. + * @param dialect additional information not provided by {@code metadata}. + * @param geomLibrary the factory to use for creating geometric objects. + * @param contentLocale the locale to use for international texts to write in the database, or {@code null} for default. + * @param listeners where to send warnings. + * @param locks the read/write locks, or {@code null} if none. + * @throws SQLException if an error occurred while reading database metadata. + */ + public DuckDB(final DataSource source, final DatabaseMetaData metadata, final Dialect dialect, + final Geometries<G> geomLibrary, final Locale contentLocale, final StoreListeners listeners, + final ReadWriteLock locks) + throws SQLException + { + super(source, metadata, dialect, geomLibrary, contentLocale, listeners, locks); + } + + /** + * Whether to decode the geometry from <abbr>WKB</abbr> instead of <abbr>WKT</abbr>. + * In theory, the use of binary format should be more efficient. But the DuckDB driver + * has some issues with extracting bytes from geometry columns at the time or writing. + * The current version extracts the geometries through <abbr>WKT</abbr> representation. + * The reasons for not using <abbr>WKB</abbr> at this stage are: + * + * <ul> + * <li>It requires to build the query like this: {@code CAST(ST_AsWKB(geom_column) AS BLOB)}.</li> + * <li>It seems that for large dataset, reading from WKB is a lot slower than reading from WKT.</li> + * </ul> + */ + @Override + protected GeometryEncoding getGeometryEncoding(final Column columnDefinition) { + return GeometryEncoding.WKT; + } + + /** + * Returns the converter from filters/expressions to the {@code WHERE} part of SQL statement. + */ + @Override + protected SelectionClauseWriter getFilterToSQL() { + return ExtendedClauseWriter.INSTANCE; + } +} diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/ExtendedClauseWriter.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/ExtendedClauseWriter.java new file mode 100644 index 0000000000..a6232bda80 --- /dev/null +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/ExtendedClauseWriter.java @@ -0,0 +1,61 @@ +/* + * 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.sis.storage.sql.duckdb; + +import org.apache.sis.storage.sql.feature.SelectionClauseWriter; +import org.opengis.filter.SpatialOperatorName; + + +/** + * Converter from filters/expressions to the {@code WHERE} part of SQL statement. + * This class adds support for {@code BBOX} as a synonymous of {@code ST_Intersects}. + * + * @author Guilhem Legal (Geomatys) + */ +final class ExtendedClauseWriter extends SelectionClauseWriter { + /** + * The unique instance. + */ + static final ExtendedClauseWriter INSTANCE = new ExtendedClauseWriter(); + + /** + * Creates a new converter from filters/expressions to SQL. + */ + private ExtendedClauseWriter() { + super(DEFAULT); + setFilterHandler(SpatialOperatorName.BBOX, getFilterHandler(SpatialOperatorName.INTERSECTS)); + } + + /** + * Creates a new converter initialized to the same handlers as the specified converter. + * + * @param source the converter from which to copy the handlers. + */ + private ExtendedClauseWriter(ExtendedClauseWriter source) { + super(source); + } + + /** + * Creates a new converter of the same class as {@code this} and initialized with the same data. + * + * @return a converter initialized to a copy of {@code this}. + */ + @Override + protected SelectionClauseWriter duplicate() { + return new ExtendedClauseWriter(this); + } +} diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/package-info.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/package-info.java new file mode 100644 index 0000000000..5c100e6246 --- /dev/null +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/package-info.java @@ -0,0 +1,60 @@ +/* + * 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. + */ + +/** + * Specialization of {@code org.apache.sis.storage.sql.feature} for the DuckDB database. + * Since DuckDB 1.2.1 does not provide a {@link javax.sql.DataSource} implementation, + * users need to provide their own. The user's {@code DataSource} should load the spatial + * extension if desired and enable the streaming. The following snippet is a suggestion: + * + * {@snippet lang="java" : + * import java.util.Properties; + * import java.sql.Connection; + * import java.sql.DriverManager; + * import java.sql.SQLException; + * import java.sql.Statement; + * import javax.sql.DataSource; + * import org.duckdb.DuckDBDriver; + * + * class DuckDataSource implements DataSource { + * private final String url; + * private boolean initialized; + * + * DuckDataSource(final String url) { + * this.url = url; + * } + * + * @Override + * public Connection getConnection() throws SQLException { + * var info = new Properties(); + * info.setProperty(DuckDBDriver.JDBC_STREAM_RESULTS, "true"); + * Connection c = DriverManager.getConnection(url, info); + * try (Statement s = c.createStatement()) { + * if (!initialized) { + * initialized = true; + * s.execute("INSTALL spatial"); + * } + * s.execute("LOAD spatial"); + * } + * return c; + * } + * } + * + * @author Guilhem Legal (Geomatys) + * @author Martin Desruisseaux (Geomatys) + */ +package org.apache.sis.storage.sql.duckdb; diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java index 2f42b0d4bf..a2333db673 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java @@ -45,6 +45,7 @@ import org.apache.sis.storage.InternalDataStoreException; import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.storage.sql.ResourceDefinition; import org.apache.sis.storage.sql.postgis.Postgres; +import org.apache.sis.storage.sql.duckdb.DuckDB; import org.apache.sis.metadata.sql.privy.Dialect; import org.apache.sis.metadata.sql.privy.Reflection; import org.apache.sis.util.ArraysExt; @@ -191,6 +192,7 @@ public final class Analyzer { final Dialect dialect = Dialect.guess(metadata); switch (dialect) { case POSTGRESQL: database = new Postgres<>(source, metadata, dialect, g, contentLocale, listeners, locks); break; + case DUCKDB: database = new DuckDB<> (source, metadata, dialect, g, contentLocale, listeners, locks); break; default: database = new Database<>(source, metadata, dialect, g, contentLocale, listeners, locks); break; } ignoredTables = database.detectSpatialSchema(metadata, tableTypes); diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java index b00d58183b..fbb62c850b 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java @@ -630,6 +630,9 @@ public class Database<G> extends Syntax { * * @param columnDefinition information about the column to extract values from and expose through Java API. * @return converter to the corresponding java type, or {@code null} if this class cannot find a mapping, + * + * @see #getBinaryEncoding(Column) + * @see #getGeometryEncoding(Column) */ protected final ValueGetter<?> forGeometry(final Column columnDefinition) { /* @@ -640,7 +643,7 @@ public class Database<G> extends Syntax { final GeometryType type = columnDefinition.getGeometryType().orElse(GeometryType.GEOMETRY); final Class<? extends G> geometryClass = geomLibrary.getGeometryClass(type).asSubclass(geomLibrary.rootClass); return new GeometryGetter<>(geomLibrary, geometryClass, columnDefinition.getDefaultCRS().orElse(null), - getBinaryEncoding(columnDefinition)); + getBinaryEncoding(columnDefinition), getGeometryEncoding(columnDefinition)); } /** @@ -736,15 +739,31 @@ public class Database<G> extends Syntax { } /** - * Returns an identifier of the way binary data are encoded by the JDBC driver. + * Returns an identifier of the way binary data are encoded by the <abbr>JDBC</abbr> driver. + * The default implementation returns {@link BinaryEncoding#RAW}. * * @param columnDefinition information about the column to extract binary values from. * @return how the binary data are returned by the JDBC driver. + * + * @see #forGeometry(Column) */ protected BinaryEncoding getBinaryEncoding(final Column columnDefinition) { return BinaryEncoding.RAW; } + /** + * Returns an identifier of the way geometries should be read and written. + * The default implementation returns {@link GeometryEncoding#WKB}. + * + * @param columnDefinition information about the column to extract geometry values from. + * @return how the geometry should be read or written (as text or as binary). + * + * @see #forGeometry(Column) + */ + protected GeometryEncoding getGeometryEncoding(final Column columnDefinition) { + return GeometryEncoding.WKB; + } + /** * Computes an estimation of the envelope of all geometry columns in the given table. * The returned envelope shall contain at least the two-dimensional spatial components. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java index 13dea0b1fd..13be593b51 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java @@ -144,14 +144,19 @@ final class FeatureIterator implements Spliterator<Feature>, AutoCloseable { } sql = builder.appendFetchPage(offset, count).toString(); } - result = connection.createStatement().executeQuery(sql); - dependencies = new FeatureIterator[adapter.dependencies.length]; - statement = null; + /* + * Create the statement for the SQL query. The call to `createStatement()` should be at the end, + * after the call to `countRows(…)`, because some JDBC drivers close the statement when we ask + * for metadata (probably a bug, but not all JDBC drivers are mature). + */ if (filter == null) { estimatedSize = Math.min(table.countRows(connection.getMetaData(), distinct, true), offset + count) - offset; } else { estimatedSize = 0; // Cannot estimate the size if there is filtering conditions. } + result = connection.createStatement().executeQuery(sql); + dependencies = new FeatureIterator[adapter.dependencies.length]; + statement = null; } /** diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryEncoding.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryEncoding.java new file mode 100644 index 0000000000..544d0596ed --- /dev/null +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryEncoding.java @@ -0,0 +1,39 @@ +/* + * 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.sis.storage.sql.feature; + + +/** + * The encoding to use for reading or writing geometries from a {@code ResultSet}, in preference order. + * In theory, the use of a binary format should be more efficient. But some <abbr>JDBC</abbr> drivers + * have issues with extracting bytes from geometry columns. It also happens sometime that, surprisingly + * the use of <abbr>WKT</abbr> appear to be faster than <abbr>WKB</abbr> with some databases. + * + * @author Martin Desruisseaux (Geomatys) + */ +public enum GeometryEncoding { + /** + * Use Well-Known Binary (<abbr>WKB</abbr>) format. + * Includes the Geopackage geometry encoding extension, which is identified by the "GP" prefix. + */ + WKB, + + /** + * Use Well-Known Text (<abbr>WKT</abbr>) format. + */ + WKT +} diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java index fefebec715..38aadc6e33 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java @@ -71,6 +71,13 @@ final class GeometryGetter<G, V extends G> extends ValueGetter<V> { */ private final BinaryEncoding encoding; + /** + * Whether to use binary (<abbr>WKB</abbr>) or textual (<abbr>WKT</abbr>). + * In theory, the binary format should be more efficient. + * But this is not always well supported. + */ + private final GeometryEncoding format; + /** * Creates a new reader. The same instance can be reused for parsing an arbitrary * number of geometries sharing the same default CRS. @@ -79,14 +86,17 @@ final class GeometryGetter<G, V extends G> extends ValueGetter<V> { * @param geometryClass the type of geometry to be returned by this {@code ValueGetter}. * @param defaultCRS the CRS to use if none can be mapped from the SRID, or {@code null} if none. * @param encoding the way binary data are encoded in the geometry column. + * @param format whether to use <abbr>WKB</abbr> or <abbr>WKT</abbr>. */ GeometryGetter(final Geometries<G> geometryFactory, final Class<V> geometryClass, - final CoordinateReferenceSystem defaultCRS, final BinaryEncoding encoding) + final CoordinateReferenceSystem defaultCRS, final BinaryEncoding encoding, + final GeometryEncoding format) { super(geometryClass); this.geometryFactory = geometryFactory; this.defaultCRS = defaultCRS; this.encoding = encoding; + this.format = format; } /** @@ -105,45 +115,64 @@ final class GeometryGetter<G, V extends G> extends ValueGetter<V> { */ @Override public V getValue(final InfoStatements stmts, final ResultSet source, final int columnIndex) throws Exception { - final byte[] wkb = encoding.getBytes(source, columnIndex); - if (wkb == null) return null; - final ByteBuffer buffer = ByteBuffer.wrap(wkb); - /* - * The bytes should describe a geometry encoded in Well Known Binary (WKB) format, - * but this implementation accepts also the Geopackage geometry encoding: - * - * https://www.geopackage.org/spec140/index.html#gpb_spec - * - * This is still a geometry in WKB format, but preceded by a header of at least two 32-bits integers. - */ - int gpkgSrid = 0; // ≤0 means "no CRS" as of `stmts.fetchCRS(int)` contract. - if (wkb.length > 2*Integer.BYTES && wkb[0] == 'G' && wkb[1] == 'P') { - final int version = Byte.toUnsignedInt(wkb[2]); // 8-bit unsigned integer, 0 = version 1 - if (version != 0) { - throw new DataStoreContentException(Errors.forLocale(stmts.getLocale()) - .getString(Errors.Keys.UnsupportedFormatVersion_2, "Geopackage", version)); + final GeometryWrapper geom; + int gpkgSrid = 0; // A value ≤ 0 means "no CRS" as of `stmts.fetchCRS(int)` contract. + switch (format) { + default: { + return null; + } + case WKT: { + final String wkt = source.getString(columnIndex); + if (wkt == null) return null; + geom = geometryFactory.parseWKT(wkt); + break; } - final int flags = wkb[3]; - final boolean bigEndian = (flags & 0b000001) == 0; - final int envelopeType = (flags & 0b001110) >> 1; - // final boolean isEmpty = (flags & 0b010000) != 0; - // final boolean extendedType = (flags & 0b100000) != 0; - buffer.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); - gpkgSrid = buffer.getInt(Integer.BYTES); - // Skip header and envelope. - final int offset; - switch (envelopeType) { - case 0: offset = 2*Integer.BYTES; break; // No envelope. - case 1: offset = 2*Integer.BYTES + 4*Double.BYTES; break; // 2D envelope. - case 2: // 3D envelope with Z. - case 3: offset = 2*Integer.BYTES + 6*Double.BYTES; break; // 3D envelope with M. - case 4: offset = 2*Integer.BYTES + 8*Double.BYTES; break; // 4D envelope. - default: throw new DataStoreContentException(Errors.forLocale(stmts.getLocale()) - .getString(Errors.Keys.UnexpectedValueInElement_2, "envelope contents indicator")); + case WKB: { + final byte[] wkb = encoding.getBytes(source, columnIndex); + if (wkb == null) return null; + final ByteBuffer buffer = ByteBuffer.wrap(wkb); + /* + * The bytes should describe a geometry encoded in Well Known Binary (WKB) format, + * but this implementation accepts also the Geopackage geometry encoding: + * + * https://www.geopackage.org/spec140/index.html#gpb_spec + * + * This is still a geometry in WKB format, but preceded by a header of at least two 32-bits integers. + */ + if (wkb.length > 2*Integer.BYTES && wkb[0] == 'G' && wkb[1] == 'P') { + final int version = Byte.toUnsignedInt(wkb[2]); // 8-bit unsigned integer, 0 = version 1 + if (version != 0) { + throw new DataStoreContentException(Errors.forLocale(stmts.getLocale()) + .getString(Errors.Keys.UnsupportedFormatVersion_2, "Geopackage", version)); + } + final int flags = wkb[3]; + final boolean bigEndian = (flags & 0b000001) == 0; + final int envelopeType = (flags & 0b001110) >> 1; + // final boolean isEmpty = (flags & 0b010000) != 0; + // final boolean extendedType = (flags & 0b100000) != 0; + buffer.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + gpkgSrid = buffer.getInt(Integer.BYTES); + // Skip header and envelope. + final int offset; + switch (envelopeType) { + case 0: offset = 2*Integer.BYTES; break; // No envelope. + case 1: offset = 2*Integer.BYTES + 4*Double.BYTES; break; // 2D envelope. + case 2: // 3D envelope with Z. + case 3: offset = 2*Integer.BYTES + 6*Double.BYTES; break; // 3D envelope with M. + case 4: offset = 2*Integer.BYTES + 8*Double.BYTES; break; // 4D envelope. + default: throw new DataStoreContentException(Errors.forLocale(stmts.getLocale()) + .getString(Errors.Keys.UnexpectedValueInElement_2, "envelope contents indicator")); + } + buffer.position(offset).order(ByteOrder.BIG_ENDIAN); + } + geom = geometryFactory.parseWKB(buffer); + break; } - buffer.position(offset).order(ByteOrder.BIG_ENDIAN); } - final GeometryWrapper geom = geometryFactory.parseWKB(buffer); + /* + * Set the CRS. This is often a constant value defined for the whole column. + * But some formats allow to specify a SRID individually on the geometry. + */ CoordinateReferenceSystem crs = defaultCRS; if (stmts != null) { crs = stmts.fetchCRS(geom.getSRID().orElse(gpkgSrid)); diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java index a47317c3ca..208fb33c89 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java @@ -174,7 +174,8 @@ public class SelectionClauseWriter extends Visitor<Feature, SelectionClause> { */ final String prefix = database.escapeWildcards(lowerCase ? "st_" : "ST_"); try (ResultSet r = metadata.getFunctions(database.catalogOfSpatialTables, - database.schemaOfSpatialTables, prefix + '%')) + database.dialect.toCompatibleMetadataPattern(database.schemaOfSpatialTables, 1), + database.dialect.toCompatibleMetadataPattern(prefix + '%', 2))) { while (r.next()) { unsupported.remove(r.getString("FUNCTION_NAME")); diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java index 7fe09610ef..3158ada9a8 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java @@ -65,7 +65,7 @@ public final class GeometryGetterTest extends TestCase { @SuppressWarnings("unchecked") private GeometryGetter<?,?> createReader(final GeometryLibrary library, final BinaryEncoding encoding) { GF = Geometries.factory(library); - return new GeometryGetter<>(GF, (Class) GF.rootClass, HardCodedCRS.WGS84, encoding); + return new GeometryGetter<>(GF, (Class) GF.rootClass, HardCodedCRS.WGS84, encoding, GeometryEncoding.WKB); } /** diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/RasterReaderTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/RasterReaderTest.java index 763efc905c..4c4490eed6 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/RasterReaderTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/RasterReaderTest.java @@ -64,6 +64,7 @@ public final class RasterReaderTest extends TestCase { * Reads the file for the given test enumeration and compares with the expected raster. * The given reader and input are used for reading the raster. The input will be closed. */ + @SuppressWarnings("ConvertToTryWithResources") // Because testing on a byte array, closing is not very important. static void compareReadResult(final TestRaster test, final RasterReader reader, final ChannelDataInput input) throws Exception { final GridCoverage coverage = reader.readAsCoverage(input); input.channel.close(); @@ -77,8 +78,8 @@ public final class RasterReaderTest extends TestCase { */ static void compareReadResult(final TestRaster test, final GridCoverage coverage) { final RenderedImage image = coverage.render(null); - final DataBufferUShort expected = (DataBufferUShort) test.createRaster().getDataBuffer(); - final DataBufferUShort actual = (DataBufferUShort) image.getTile(0, 0).getDataBuffer(); + final var expected = (DataBufferUShort) test.createRaster().getDataBuffer(); + final var actual = (DataBufferUShort) image.getTile(0, 0).getDataBuffer(); assertTrue(Arrays.deepEquals(expected.getBankData(), actual.getBankData())); } } diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/RasterWriterTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/RasterWriterTest.java index 838f11891b..12b92c6c8a 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/RasterWriterTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/RasterWriterTest.java @@ -59,8 +59,8 @@ public final class RasterWriterTest extends TestCase { */ private static void compareWriteResult(final TestRaster test) throws Exception { final Raster raster = test.createRaster(); - final RasterWriter writer = new RasterWriter(null); - final ByteArrayOutputStream buffer = new ByteArrayOutputStream(test.length); + final var writer = new RasterWriter(null); + final var buffer = new ByteArrayOutputStream(test.length); final ChannelDataOutput output = test.output(buffer); writer.setGridToCRS(TestRaster.getGridGeometry()); writer.write(raster, output);