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 bce9be08df Add a fallback for trying to guess the SRID when the table
of a column is unknown. This is a workaround for incomplete JDBC drivers (in
this case, DuckDB 1.2.2.0).
bce9be08df is described below
commit bce9be08dfb17ed33c571e37374dbdecfb1ec0ee
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Apr 22 16:08:25 2025 +0200
Add a fallback for trying to guess the SRID when the table of a column is
unknown.
This is a workaround for incomplete JDBC drivers (in this case, DuckDB
1.2.2.0).
---
.../org/apache/sis/metadata/sql/privy/Dialect.java | 2 +
.../apache/sis/metadata/sql/privy/Supports.java | 3 +
.../org/apache/sis/storage/sql/feature/Column.java | 15 ++-
.../apache/sis/storage/sql/feature/Database.java | 2 +-
.../storage/sql/feature/GeometryTypeEncoding.java | 2 +
.../sis/storage/sql/feature/InfoStatements.java | 118 ++++++++++++++++++---
.../sis/storage/sql/feature/QueryAnalyzer.java | 2 +-
.../sis/storage/sql/feature/SpatialSchema.java | 9 +-
.../sis/storage/sql/feature/TableAnalyzer.java | 4 +-
9 files changed, 134 insertions(+), 23 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 83af6d9816..ddec3b61ab 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
@@ -18,6 +18,7 @@ package org.apache.sis.metadata.sql.privy;
import java.sql.SQLException;
import java.sql.DatabaseMetaData;
+import org.apache.sis.util.Workaround;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.privy.Constants;
@@ -197,6 +198,7 @@ public enum Dialect {
* This flag should be {@code false} when the JDBC driver returns a
non-null catalog name
* (for example, the database name) but doesn't accept the use of that
catalog in SQL.
*/
+ @Workaround(library = "DuckDB", version = "1.2.2.0")
public final boolean supportsCatalog() {
return (flags & Supports.CATALOG) != 0;
}
diff --git
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java
index b4f2086e1b..0820e1ea8f 100644
---
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java
+++
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java
@@ -16,6 +16,8 @@
*/
package org.apache.sis.metadata.sql.privy;
+import org.apache.sis.util.Workaround;
+
/**
* Enumeration of features that may be supported by a database.
@@ -71,6 +73,7 @@ final class Supports {
* This flag should be {@code false} when the JDBC driver returns a
non-null catalog name
* (for example, the database name) but doesn't accept the use of that
catalog in SQL.
*/
+ @Workaround(library = "DuckDB", version = "1.2.2.0")
static final int CATALOG = 64;
/**
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 1d23b834c0..9b6dc9444c 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
@@ -22,6 +22,7 @@ import java.sql.ResultSetMetaData;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.sql.SQLDataException;
+import java.util.Map;
import java.util.Optional;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.metadata.sql.privy.Reflection;
@@ -250,14 +251,20 @@ public final class Column implements Cloneable {
* 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.
+ * @param analyzer the object used for analyzing the database schema.
+ * @throws Exception if an error occurred while fetching the
<abbr>CRS</abbr>.
*/
- final void tryMakeSpatial(final Database<?> database) {
+ final void tryMakeSpatial(final Analyzer analyzer) throws Exception {
try {
geometryType = GeometryType.forName(typeName);
- geometryAsText = (database.getGeometryEncoding(this) ==
GeometryEncoding.WKT);
+ geometryAsText = (analyzer.database.getGeometryEncoding(this) ==
GeometryEncoding.WKT);
+ final InfoStatements spatialInformation =
analyzer.spatialInformation;
+ if (spatialInformation != null) {
+ spatialInformation.completeIntrospection(analyzer, null,
Map.of());
+ defaultCRS = spatialInformation.guessCRS(name);
+ }
} catch (IllegalArgumentException e) {
- // Ignore.
+ // The `typeName` value is not the name of a geometry type. Ignore.
}
}
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 515e6a0737..2fd5f41a7b 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
@@ -319,7 +319,7 @@ public class Database<G> extends Syntax {
this.cacheOfCRS = new Cache<>(7, 2, false);
this.cacheOfSRID = new WeakHashMap<>();
this.tablesByNames = new FeatureNaming<>();
- supportsCatalogs = metadata.supportsCatalogsInDataManipulation();
+ supportsCatalogs = dialect.supportsCatalog() &&
metadata.supportsCatalogsInDataManipulation();
supportsSchemas = metadata.supportsSchemasInDataManipulation();
supportsJavaTime = dialect.supportsJavaTime();
crsEncodings = EnumSet.noneOf(CRSEncoding.class);
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryTypeEncoding.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryTypeEncoding.java
index 6b8c7859f2..ef4d28dd6e 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryTypeEncoding.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryTypeEncoding.java
@@ -48,6 +48,8 @@ public enum GeometryTypeEncoding {
/**
* Decodes the geometry type encoded in the specified column of the given
result set.
* If there is no type information, then this method returns {@code null}.
+ *
+ * @throws IllegalArgumentException if the type cannot be decoded.
*/
GeometryType parse(final ResultSet result, final int columnIndex) throws
SQLException {
final int code = result.getInt(columnIndex);
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 294cd19b51..f2b5275f52 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
@@ -57,6 +57,7 @@ import org.apache.sis.util.privy.Constants;
import org.apache.sis.io.wkt.Convention;
import org.apache.sis.io.wkt.WKTFormat;
import org.apache.sis.io.wkt.Warnings;
+import org.apache.sis.util.Workaround;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.metadata.Identifier;
@@ -115,35 +116,69 @@ public class InfoStatements implements Localized,
AutoCloseable {
* A statement for fetching geometric information for a specific column.
* May be {@code null} if not yet prepared or if the table does not exist.
* This field is valid if {@link #isAnalysisPrepared} is {@code true}.
+ *
+ * @see #isAnalysisPrepared
+ * @see #completeIntrospection(Analyzer, TableReference, Map)
*/
protected PreparedStatement geometryColumns;
/**
* Whether the statements for schema analysis have been prepared.
- * Includes {@link #geometryColumns}, but not fetching the CRS.
- * A statement may still be null if the table has not been found.
+ * This flag tells whether the statements for the following information
are valid even if {@code null}:
+ *
+ * <ul>
+ * <li>{@linkplain #geometryColumns Geometry columns}</li>
+ * <li>Geography columns (specific to PostGIS)</li>
+ * <li>Raster columns (specific to PostGIS)</li>
+ * </ul>
+ *
+ * This flag does not apply to the statements working on the {@code
SPATIAL_REF_SYS} table,
+ * which is assumed to always exist.
*/
protected boolean isAnalysisPrepared;
/**
- * The statement for fetching CRS Well-Known Text (WKT) from a SRID code.
+ * The statement for fetching a SRID from a geometry column table when the
column's table is unknown.
+ * This is a workaround for <abbr>JDBC</abbr> drivers that do not provide
this information.
+ * It is created only if requested.
*
- * @see <a
href="http://postgis.refractions.net/documentation/manual-1.3/ch04.html#id2571265">PostGIS
documentation</a>
+ * @see #guessCRS(String)
*/
- private PreparedStatement wktFromSrid;
+ @Workaround(library = "DuckDB", version = "1.2.2.0")
+ private PreparedStatement sridForUnknownTable;
/**
* The statement for fetching a SRID from a CRS and its set of authority
codes.
+ * Created when first needed.
+ *
+ * @see #findOrAddCRS(CoordinateReferenceSystem)
*/
private PreparedStatement sridFromCRS;
/**
- * The object to use for parsing or formatting Well-Known Text (WKT),
created when first needed.
+ * The statement for fetching CRS Well-Known Text (<abbr>WKT</abbr>) from
a <abbr>SRID</abbr> code.
+ * Created when first needed.
+ *
+ * @see #parseCRS(int)
+ * @see <a
href="http://postgis.refractions.net/documentation/manual-1.3/ch04.html#id2571265">PostGIS
documentation</a>
+ */
+ private PreparedStatement wktFromSrid;
+
+ /**
+ * The object to use for parsing or formatting Well-Known Text
(<abbr>WKT</abbr>).
+ * Created when first needed.
*
* @see #wktFormat()
*/
private WKTFormat wktFormat;
+ /**
+ * Whether an error occurred while reading the geometry type.
+ * In such case, the type default to {@link GeometryType#GEOMETRY}.
+ * This flag is used for reporting the warning only once.
+ */
+ private boolean cannotReadGeometryType;
+
/**
* Creates an initially empty {@code CachedStatements} which will use
* the given connection for creating {@link PreparedStatement}s.
@@ -194,6 +229,11 @@ public class InfoStatements implements Localized,
AutoCloseable {
* Prepares the statement for fetching information about all geometry or
raster columns in a specified table.
* This method is for {@link #completeIntrospection(Analyzer,
TableReference, Map)} implementations.
*
+ * <h4>PostGIS special case</h4>
+ * By default, the {@code geomColNameColumn} and {@code geomTypeColumn}
argument values are fetched from the
+ * {@link SpatialSchema}. However, PostGIS uses a non-standard {@code
geomTypeColumn} value. It also has many
+ * "geometry columns"-like tables. This is handled by overriding {@code
completeIntrospection(…)}.
+ *
* @param analyzer the opaque temporary object used for
analyzing the database schema.
* @param table name of the geometry table. Standard value
is {@code "GEOMETRY_COLUMNS"}.
* @param raster whether the statement is for raster table
instead of geometry table.
@@ -214,7 +254,7 @@ public class InfoStatements implements Localized,
AutoCloseable {
if (geomColNameColumn == null) {
geomColNameColumn = schema.geomColNameColumn;
}
- appendColumn(sql, raster, geomColNameColumn).append(",
").append(schema.crsIdentifierColumn).append(' ');
+ appendColumn(sql, raster, geomColNameColumn).append(",
").append(schema.crsIdentifierColumn);
if (geomTypeColumn == null) {
geomTypeColumn = schema.geomTypeColumn;
}
@@ -259,8 +299,11 @@ public class InfoStatements implements Localized,
AutoCloseable {
* This method should be invoked at least once before the {@link
Column#valueGetter} field is set.
* It is invoked again for each table or query to analyze.
*
+ * <p>This method may be invoked with a null {@code source} and empty
{@code columns}
+ * for ensuring that {@link #geometryColumns} is initialized but without
executing it.</p>
+ *
* @param analyzer the opaque temporary object used for analyzing the
database schema.
- * @param source the table for which to get all geometry columns.
+ * @param source the table for which to get all geometry columns. May
be null if {@code columns} is empty.
* @param columns all columns for the specified table. Keys are column
names.
* @throws DataStoreContentException if a logical error occurred in
processing data.
* @throws ParseException if the WKT cannot be parsed.
@@ -273,6 +316,7 @@ public class InfoStatements implements Localized,
AutoCloseable {
if (!isAnalysisPrepared) {
isAnalysisPrepared = true;
geometryColumns = prepareIntrospectionStatement(analyzer,
schema.geometryColumns, false, null, null);
+ // The `geometryColumns` field may still be null.
}
configureSpatialColumns(geometryColumns, source, columns,
schema.typeEncoding);
}
@@ -286,7 +330,7 @@ public class InfoStatements implements Localized,
AutoCloseable {
* value returned by {@link #prepareIntrospectionStatement(Analyzer,
String, boolean, String, String)}.
*
* @param columnQuery the statement for fetching information, or
{@code null} if none.
- * @param source the table for which to get all geometry columns.
+ * @param source the table for which to get all geometry columns.
May be null if {@code columns} is empty.
* @param columns all columns for the specified table. Keys are
column names.
* @param typeValueKind {@code NUMERIC}, {@code TEXTUAL} or {@code null}
if none.
* @throws DataStoreContentException if a logical error occurred in
processing data.
@@ -301,7 +345,7 @@ public class InfoStatements implements Localized,
AutoCloseable {
protected final void configureSpatialColumns(final PreparedStatement
columnQuery, final TableReference source,
final Map<String,Column> columns, final GeometryTypeEncoding
typeValueKind) throws Exception
{
- if (columnQuery == null) {
+ if (columnQuery == null || columns.isEmpty()) {
return;
}
int p = 0;
@@ -312,18 +356,66 @@ public class InfoStatements implements Localized,
AutoCloseable {
while (result.next()) {
final Column target = columns.get(result.getString(1));
if (target != null) {
- final CoordinateReferenceSystem crs =
fetchCRS(result.getInt(2));
GeometryType type = null;
if (typeValueKind != null) {
- type = typeValueKind.parse(result, 3);
+ try {
+ type = typeValueKind.parse(result, 3);
+ } catch (IllegalArgumentException e) {
+ if (!cannotReadGeometryType) {
+ cannotReadGeometryType = true;
+
database.warning(Resources.Keys.CanNotAnalyzeFully, e);
+ }
+ }
if (type == null) {
type = GeometryType.GEOMETRY;
}
}
- target.makeSpatial(database, type, crs);
+ target.makeSpatial(database, type,
fetchCRS(result.getInt(2)));
+ }
+ }
+ }
+ }
+
+ /**
+ * Tries to guess the <abbr>CRS</abbr> for the specified column in an
unknown table.
+ * This is used for queries when the <abbr>JDBC</abbr> driver is
incomplete.
+ *
+ * @param column name of the column in unknown table.
+ * @return the <abbr>CRS</abbr>, or {@code null} if none or ambiguous.
+ * @throws Exception if an error occurred while fetching the
<abbr>CRS</abbr>.
+ */
+ @Workaround(library = "DuckDB", version = "1.2.2.0")
+ final CoordinateReferenceSystem guessCRS(final String column) throws
Exception {
+ if (sridForUnknownTable == null) {
+ if (geometryColumns == null) {
+ return null;
+ }
+ final SpatialSchema schema =
database.getSpatialSchema().orElseThrow();
+ final var sql = new SQLBuilder(database)
+ .append(SQLBuilder.SELECT).append("DISTINCT ")
+ .appendIdentifier(schema.crsIdentifierColumn)
+ .append(" FROM ").appendIdentifier(schema.geometryColumns)
+ .append(" WHERE
").appendIdentifier(schema.geomColNameColumn).append("=?");
+ sridForUnknownTable = connection.prepareStatement(sql.toString());
+ }
+ sridForUnknownTable.setString(1, column);
+ CoordinateReferenceSystem first = null;
+ try (ResultSet result = sridForUnknownTable.executeQuery()) {
+ while (result.next()) {
+ final int srid = result.getInt(1);
+ if (!result.wasNull()) {
+ CoordinateReferenceSystem crs = fetchCRS(srid);
+ if (crs != null) {
+ if (first == null) {
+ first = crs;
+ } else if (!Utilities.equalsIgnoreMetadata(first,
crs)) {
+ return null;
+ }
+ }
}
}
}
+ return first;
}
/**
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 b474ece77a..6ebbd52955 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
@@ -171,7 +171,7 @@ final class QueryAnalyzer extends FeatureAnalyzer {
final var attributes = new ArrayList<Column>();
for (final Column column : columns) {
if (fallback) {
- column.tryMakeSpatial(analyzer.database);
+ column.tryMakeSpatial(analyzer);
}
if (createAttribute(column)) {
attributes.add(column);
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java
index d4f2b34b9a..c187f0b4d8 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java
@@ -59,8 +59,8 @@ public enum SpatialSchema {
GeometryTypeEncoding.TEXTUAL), // How geometry types are
encoded in the above-cited type column.
/**
- * Table and column names as specified by ISO-13249 SQL/MM. This is the
same thing as {@link #SIMPLE_FEATURE}
- * with only different names. The table definition for CRS is:
+ * Table and column names as specified by ISO-13249 SQL/MM. This is
similar to {@link #SIMPLE_FEATURE}
+ * with different names and no {@code GEOMETRY_TYPE} column. The table
definition for CRS is:
*
* {@snippet lang="sql" :
* CREATE TABLE ST_SPATIAL_REFERENCE_SYSTEMS(
@@ -104,6 +104,11 @@ public enum SpatialSchema {
* AUTH_SRID INTEGER,
* SRTEXT CHARACTER VARYING(2048))
* }
+ *
+ * <h4>PostGIS special case</h4>
+ * PostGIS uses these table and column names (in lower cases), except the
{@code GEOMETRY_TYPE} column
+ * which is named only {@code TYPE} in PostGIS. There is no enumeration
value for PostGIS special case.
+ * Instead, it is handled by {@code
InfoStatements.completeIntrospection(…)} method overriding.
*/
SIMPLE_FEATURE(
"ISO 19125 / OGC Simple feature", // Human-readable name of
this spaial schema.
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 291a1797b5..28d53c2180 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,9 +187,9 @@ final class TableAnalyzer extends FeatureAnalyzer {
*/
final var attributes = new ArrayList<Column>();
for (final Column column : columns.values()) {
- if (spatialInformation == null) {
+ if (spatialInformation == null ||
spatialInformation.geometryColumns == null) {
// Fallback for databases without "geometry columns" table.
- column.tryMakeSpatial(analyzer.database);
+ column.tryMakeSpatial(analyzer);
}
if (createAttribute(column)) {
attributes.add(column);