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 8abf1fdeeb Add explicit `ST_AsBinary` or `ST_AsText` functions when requesting a geometry. 8abf1fdeeb is described below commit 8abf1fdeeb7749124c01f7b447b9fa4fa4f70e2a Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue Apr 15 15:31:45 2025 +0200 Add explicit `ST_AsBinary` or `ST_AsText` functions when requesting a geometry. --- .../org/apache/sis/storage/sql/feature/Column.java | 43 ++++++++-- .../apache/sis/storage/sql/feature/Database.java | 48 +++++++++-- .../sis/storage/sql/feature/FeatureAdapter.java | 20 ++++- .../sis/storage/sql/feature/GeometryEncoding.java | 93 +++++++++++++++++++++- .../sis/storage/sql/feature/InfoStatements.java | 2 +- .../sis/storage/sql/feature/QueryAnalyzer.java | 6 ++ .../storage/sql/feature/SelectionClauseWriter.java | 6 +- .../sis/storage/sql/feature/TableAnalyzer.java | 4 + .../sis/storage/sql/feature/ValueGetter.java | 2 +- .../apache/sis/storage/sql/postgis/Postgres.java | 4 +- 10 files changed, 207 insertions(+), 21 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java index e4a2393e17..1d23b834c0 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java @@ -27,7 +27,6 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.metadata.sql.privy.Reflection; import org.apache.sis.metadata.sql.privy.SQLUtilities; import org.apache.sis.geometry.wrapper.GeometryType; -import org.apache.sis.util.Localized; import org.apache.sis.util.privy.Strings; import org.apache.sis.storage.DataStoreContentException; import org.apache.sis.feature.builder.AttributeTypeBuilder; @@ -116,6 +115,13 @@ public final class Column implements Cloneable { */ private GeometryType geometryType; + /** + * Whether the geometries are encoded in <abbr>WKT</abbr> rather than <abbr>WKB</abbr>. + * + * @see #getGeometryEncoding() + */ + private boolean geometryAsText; + /** * If this column is a geometry or raster column, the Coordinate Reference System (CRS). Otherwise {@code null}. * This is determined from the geometry Spatial Reference Identifier (SRID). @@ -218,11 +224,11 @@ public final class Column implements Cloneable { * It can also be invoked during the inspection of {@code "GEOGRAPHY_COLUMNS"} or {@code "RASTER_COLUMNS"} * tables, which are PostGIS extensions. In the raster case, the geometry {@code type} argument shall be null. * - * @param caller provider of the locale for error message, if any. - * @param type the type of values in the column, or {@code null} if not geometric. - * @param crs the Coordinate Reference System (CRS), or {@code null} if unknown. + * @param database the database for which to analyze the tables. + * @param type the type of values in the column, or {@code null} if not geometric. + * @param crs the Coordinate Reference System (CRS), or {@code null} if unknown. */ - final void makeSpatial(final Localized caller, final GeometryType type, final CoordinateReferenceSystem crs) + final void makeSpatial(final Database<?> database, final GeometryType type, final CoordinateReferenceSystem crs) throws DataStoreContentException { final String property; @@ -233,12 +239,28 @@ public final class Column implements Cloneable { } else { geometryType = type; defaultCRS = crs; + geometryAsText = (database.getGeometryEncoding(this) == GeometryEncoding.WKT); return; } - throw new DataStoreContentException(Errors.forLocale(caller.getLocale()) + throw new DataStoreContentException(Errors.forLocale(database.listeners.getLocale()) .getString(Errors.Keys.ValueAlreadyDefined_1, property)); } + /** + * Tries to parses the geometry type from the field type. + * This is used as a fallback when no geometry column is found or can be used. + * + * @param database the database for which to analyze the tables. + */ + final void tryMakeSpatial(final Database<?> database) { + try { + geometryType = GeometryType.forName(typeName); + geometryAsText = (database.getGeometryEncoding(this) == GeometryEncoding.WKT); + } catch (IllegalArgumentException e) { + // Ignore. + } + } + /** * Returns a column identical to this column except for the property name. * This method does not modify this column, but may return {@code this} if @@ -287,6 +309,15 @@ public final class Column implements Cloneable { return Optional.ofNullable(geometryType); } + /** + * Returns whether the geometries are encoded in <abbr>WKT</abbr> rather than <abbr>WKB</abbr>. + * + * @return the encoding used for geometries. + */ + public final GeometryEncoding getGeometryEncoding() { + return geometryAsText ? GeometryEncoding.WKT : GeometryEncoding.WKB; + } + /** * If this column is a geometry or raster column, returns the default coordinate reference system. * Otherwise returns empty. The CRS may also be empty even for a geometry column if it is unspecified. 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 752c2da858..515e6a0737 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 @@ -19,6 +19,7 @@ package org.apache.sis.storage.sql.feature; import java.util.Map; import java.util.List; import java.util.EnumSet; +import java.util.EnumMap; import java.util.HashMap; import java.util.WeakHashMap; import java.util.LinkedHashMap; @@ -129,6 +130,15 @@ public class Database<G> extends Syntax { */ final Geometries<G> geomLibrary; + /** + * The functions to use for fetching a geometry from a column. Initialized (indirectly) the first time + * that {@link #getFilterToSupportedSQL()} is invoked and should not be modified after that point. + * + * @see #setGeometryEncodingFunctions(String[][]) + * @see #getGeometryEncodingFunction(Column) + */ + private final EnumMap<GeometryEncoding, String> geometryReaders; + /** * Whether {@link Types#TINYINT} is a signed integer. Both conventions (-128 … 127 range and 0 … 255 range) * are found on the web. If unspecified, we conservatively assume unsigned bytes. @@ -313,6 +323,7 @@ public class Database<G> extends Syntax { supportsSchemas = metadata.supportsSchemasInDataManipulation(); supportsJavaTime = dialect.supportsJavaTime(); crsEncodings = EnumSet.noneOf(CRSEncoding.class); + geometryReaders = new EnumMap<>(GeometryEncoding.class); transactionLocks = dialect.supportsConcurrency() ? null : locks; softwareVersions = new LinkedHashMap<>(4); final String product = Strings.trimOrNull(metadata.getDatabaseProductName()); @@ -437,6 +448,14 @@ public class Database<G> extends Syntax { return ignoredTables; } + /** + * Helper method for checking if catalog or schema names are consistent. + * If an previous (old) name existed, the new name should be the same. + */ + private static boolean consistent(final String oldName, final String newName) { + return (oldName == null) || oldName.equals(newName); + } + /** * Creates a model about the specified tables in the database. * This method shall be invoked exactly once after {@code Database} construction. @@ -486,11 +505,15 @@ public class Database<G> extends Syntax { } /** - * Helper method for checking if catalog or schema names are consistent. - * If an previous (old) name existed, the new name should be the same. + * Sets the preferred functions for fetching or storing geometries. + * This method is invoked indirectly by {@link #analyze analyze(…)}. + * + * @param accessors the array created by {@link GeometryEncoding#initial()}. + * + * @see #getGeometryEncodingFunction(Column) */ - private static boolean consistent(final String oldName, final String newName) { - return (oldName == null) || oldName.equals(newName); + final void setGeometryEncodingFunctions(final String[][] accessors) { + GeometryEncoding.store(accessors, geometryReaders); } /** @@ -645,7 +668,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), getGeometryEncoding(columnDefinition)); + getBinaryEncoding(columnDefinition), columnDefinition.getGeometryEncoding()); } /** @@ -828,6 +851,8 @@ public class Database<G> extends Syntax { /** * Returns the converter from filters/expressions to the {@code WHERE} part of SQL statement * without the functions that are unsupported by the database software. + * + * A side effect of this method is to initialize {@link #geometryReaders}. */ final synchronized SelectionClauseWriter getFilterToSupportedSQL() { if (filterToSQL == null) { @@ -836,6 +861,19 @@ public class Database<G> extends Syntax { return filterToSQL; } + /** + * Returns the function to use fo reading or writing a geometry in the database. + * + * @todo Add a parameter for specifying whether this is for a read or write operation. + * + * @param column the column of the geometry to read or write. + * @return the function to use, or {@code null} if none. + */ + final String getGeometryEncodingFunction(final Column column) { + getFilterToSupportedSQL(); // Force initialization of `geometryReaders` if not already done. + return geometryReaders.get(column.getGeometryEncoding()); + } + /** * Prepares a cache of statements about spatial information using the given connection. * Each statement in the returned object will be created only when first needed. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAdapter.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAdapter.java index 7cb4b6e8f7..5aedba02ed 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAdapter.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAdapter.java @@ -170,7 +170,11 @@ final class FeatureAdapter { */ final var sql = new SQLBuilder(table.database).append(SQLBuilder.SELECT); for (final Column column : attributes) { - appendColumn(sql, column.label, columnIndices); + String function = null; + if (column.getGeometryType().isPresent()) { + function = table.database.getGeometryEncodingFunction(column); + } + appendColumn(sql, table.database, function, column.label, columnIndices); } /* * Collect information about associations in local arrays before to assign @@ -270,15 +274,23 @@ final class FeatureAdapter { * An exception is thrown if the column has already been added (should never happen). * * @param sql the SQL statement where to add column identifiers after the {@code SELECT} clause. + * @param database the database. May be {@code null} if {@code function} is null. + * @param function a function for which the column is an argument, or {@code null} if none. * @param column name of the column to add. * @param columnIndices map where to add the mapping from column name to 1-based column index. */ - private static int appendColumn(final SQLBuilder sql, final String column, - final Map<String,Integer> columnIndices) throws InternalDataStoreException + private static int appendColumn(final SQLBuilder sql, final Database<?> database, final String function, + final String column, final Map<String,Integer> columnIndices) throws InternalDataStoreException { int columnCount = columnIndices.size(); if (columnCount != 0) sql.append(", "); + if (function != null) { + sql.appendIdentifier(database.catalogOfSpatialTables, database.schemaOfSpatialTables, function, false).append('('); + } sql.appendIdentifier(column); + if (function != null) { + sql.append(')'); + } if (columnIndices.put(column, ++columnCount) == null) return columnCount; throw new InternalDataStoreException(Resources.format(Resources.Keys.DuplicatedColumn_1, column)); } @@ -301,7 +313,7 @@ final class FeatureAdapter { final int[] indices = new int[columns.size()]; for (final String column : columns) { final Integer pos = columnIndices.get(column); - indices[i++] = (pos != null) ? pos : appendColumn(sql, column, columnIndices); + indices[i++] = (pos != null) ? pos : appendColumn(sql, null, null, column, columnIndices); } return indices; } 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 index 544d0596ed..73eb41fc0a 100644 --- 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 @@ -16,6 +16,8 @@ */ package org.apache.sis.storage.sql.feature; +import java.util.EnumMap; + /** * The encoding to use for reading or writing geometries from a {@code ResultSet}, in preference order. @@ -29,11 +31,98 @@ public enum GeometryEncoding { /** * Use Well-Known Binary (<abbr>WKB</abbr>) format. * Includes the Geopackage geometry encoding extension, which is identified by the "GP" prefix. + * + * <p>If the extended <abbr>WKB</abbr> format is supported, then {@code SQLStore} will use that function + * despite the fact that it is non-standard, in order to get the coordinate reference system associated + * with the geometry. Otherwise, the <abbr>SQLMM</abbr> standard function for fetching this value from + * a database is {@code "ST_AsBinary"}. However, some databases expect {@code "ST_AsWKB"} instead.</p> */ - WKB, + WKB(new String[] {"ST_AsEWKB", "ST_AsBinary", "ST_AsWKB"}), /** * Use Well-Known Text (<abbr>WKT</abbr>) format. + * + * <p>The <abbr>SQLMM</abbr> standard function for fetching this value from a database is {@code "ST_AsText"}. + * However, some databases expect {@code "ST_AsWKT"} instead.</p> + */ + WKT(new String[] {"ST_AsText", "ST_AsWKT"}); + + /** + * The functions to use, in preference order, for getting the value from the database. + */ + private final String[] readers; + + /** + * Creates a new enumeration value. + * + * @param readers the functions to use, in preference order, for getting the value from the database. + */ + private GeometryEncoding(final String[] readers) { + this.readers = readers; + } + + /** + * All enumeration values, fetching once for avoiding multiple array creations. + */ + private static final GeometryEncoding[] VALUES = values(); + + /** + * Creates an initially empty array to use as argument in the calls to {@code checkSupport(…)} method. + * Should be considered as an opaque storage mechanism used by this class only. + * + * @see #checkSupport(String[][], String) + */ + static String[][] initial() { + return new String[VALUES.length][]; + } + + /** + * Invoked in a loop over for identifying which functions are supported for fetching or storing geometries. + * + * @param accessors the array created by {@link #initial()}. + * @param function a function of the database. + * + * @todo Add a loop over {@code writers} after we implemented write support. + */ + static void checkSupport(final String[][] accessors, final String function) { + for (int j=0; j < VALUES.length; j++) { + final GeometryEncoding encoding = VALUES[j]; + final String[] readers = encoding.readers; + for (int i=0; i < readers.length; i++) { + if (readers[i].equalsIgnoreCase(function)) { + String[] functions = accessors[j]; + if (functions == null) { + functions = new String[readers.length]; + accessors[j] = functions; + } + functions[i] = function; // Keep the case used by the database. + } + } + } + } + + /** + * Puts in the given map the preferred functions for fetching or storing geometries. + * If many functions are supported, the standard one is preferred. + * + * @param accessors the array created by {@link #initial()}. + * @param target where to store the preferred functions for fetching or storing geometries. + * + * @todo Add an argument for specifying whether read or write operation is desired. */ - WKT + static void store(final String[][] accessors, final EnumMap<GeometryEncoding, String> target) { +next: for (int j=0; j < accessors.length; j++) { + final GeometryEncoding encoding = VALUES[j]; + final String[] functions = accessors[j]; + if (functions != null) { + for (String function : functions) { + if (function != null) { + target.put(encoding, function); // We want the function at the lowest index. + continue next; + } + } + } + target.remove(encoding); + } + } } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java index 34d4a25d5c..294cd19b51 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java @@ -320,7 +320,7 @@ public class InfoStatements implements Localized, AutoCloseable { type = GeometryType.GEOMETRY; } } - target.makeSpatial(this, type, crs); + target.makeSpatial(database, type, crs); } } } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java index aa2f7b8ce3..b474ece77a 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java @@ -155,9 +155,12 @@ final class QueryAnalyzer extends FeatureAnalyzer { Column[] createAttributes() throws Exception { /* * Identify geometry columns. Must be done before the calls to `Analyzer.setValueGetterOf(column)`. + * If the database does not have a "geometry columns" table, parse field type names as a fallback. */ + boolean fallback = true; final InfoStatements spatialInformation = analyzer.spatialInformation; if (spatialInformation != null) { + fallback = columnsPerTable.isEmpty(); for (final Map.Entry<TableReference, Map<String,Column>> entry : columnsPerTable.entrySet()) { spatialInformation.completeIntrospection(analyzer, entry.getKey(), entry.getValue()); } @@ -167,6 +170,9 @@ final class QueryAnalyzer extends FeatureAnalyzer { */ final var attributes = new ArrayList<Column>(); for (final Column column : columns) { + if (fallback) { + column.tryMakeSpatial(analyzer.database); + } if (createAttribute(column)) { attributes.add(column); } 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 5468a84c63..2f4cb10fd3 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 @@ -150,6 +150,7 @@ public class SelectionClauseWriter extends Visitor<Feature, SelectionClause> { */ final SelectionClauseWriter removeUnsupportedFunctions(final Database<?> database) { final var unsupported = new HashMap<String, SpatialOperatorName>(); + final var accessors = GeometryEncoding.initial(); try (Connection c = database.source.getConnection()) { final DatabaseMetaData metadata = c.getMetaData(); /* @@ -178,7 +179,9 @@ public class SelectionClauseWriter extends Visitor<Feature, SelectionClause> { prefix + '%')) { while (r.next()) { - unsupported.remove(r.getString("FUNCTION_NAME")); + final String function = r.getString("FUNCTION_NAME"); + GeometryEncoding.checkSupport(accessors, function); + unsupported.remove(function); } } } catch (SQLException e) { @@ -188,6 +191,7 @@ public class SelectionClauseWriter extends Visitor<Feature, SelectionClause> { */ database.listeners.warning(e); } + database.setGeometryEncodingFunctions(accessors); /* * Remaining functions are unsupported functions. */ diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java index 728e8e1d9c..291a1797b5 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java @@ -187,6 +187,10 @@ final class TableAnalyzer extends FeatureAnalyzer { */ final var attributes = new ArrayList<Column>(); for (final Column column : columns.values()) { + if (spatialInformation == null) { + // Fallback for databases without "geometry columns" table. + column.tryMakeSpatial(analyzer.database); + } if (createAttribute(column)) { attributes.add(column); } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java index dd1253b117..24e250c21f 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java @@ -472,7 +472,7 @@ public class ValueGetter<T> { } /** - * Converts the given SQL array to a Java array and free the SQL array. + * Converts the given SQL array to a Java array and frees the SQL array. * The returned array may be a primitive array or an array of objects. * * @param stmts information about the statement being executed, or {@code null} if none. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java index a68494cd8f..519257998f 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java @@ -158,10 +158,12 @@ public final class Postgres<G> extends Database<G> { /** * Returns an identifier of the way binary data are encoded by the JDBC driver. * Data stored as PostgreSQL {@code BYTEA} type are encoded in hexadecimal. + * Geometry type are handled as binary because of the insertion of the SQL + * function {@code ST_AsBinary(column)}. */ @Override protected BinaryEncoding getBinaryEncoding(final Column columnDefinition) { - if (columnDefinition.type == Types.BLOB) { + if (columnDefinition.type == Types.BLOB || columnDefinition.getGeometryType().isPresent()) { return super.getBinaryEncoding(columnDefinition); } else { return BinaryEncoding.HEXADECIMAL;