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
commit 5797abfbc88b435163a858ef1aa6cac2ce1b6831 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Thu Mar 13 15:08:28 2025 +0100 The "raster_columns" table of PostGIS should be optional: do not throw `SQLException` if that table does not exist. --- .../apache/sis/storage/sql/feature/Analyzer.java | 24 ++++++-- .../apache/sis/storage/sql/feature/Database.java | 37 +++++++----- .../sis/storage/sql/feature/InfoStatements.java | 67 ++++++++++++++++------ .../sis/storage/sql/feature/QueryAnalyzer.java | 2 +- .../sis/storage/sql/feature/TableAnalyzer.java | 2 +- .../sis/storage/sql/feature/TableReference.java | 2 +- .../sis/storage/sql/postgis/ExtendedInfo.java | 31 ++++++---- .../apache/sis/storage/sql/postgis/Postgres.java | 2 +- 8 files changed, 113 insertions(+), 54 deletions(-) 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 44a06d9368..4e6cec6d49 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 @@ -110,10 +110,11 @@ public final class Analyzer { private final String[] tableTypes; /** - * Names of tables to ignore. This set includes at least the tables defined by the spatial - * schema standard from storing geometry columns, spatial reference systems, <i>etc.</i> + * Names of tables to ignore. This map includes at least the tables defined by the spatial + * schema standard for storing geometry columns, spatial reference systems, <i>etc</i>. + * The values tell whether the associated table exists in the database. */ - private final Set<String> ignoredTables; + private final Map<String,Boolean> ignoredTables; /** * All tables created by analysis of the database structure. A {@code null} value means that the table @@ -196,6 +197,21 @@ public final class Analyzer { ignoredTables = database.detectSpatialSchema(metadata, tableTypes); } + /** + * Returns whether the table of the given name does <em>not</em> exist in the database schema. + * This method recognizes only tables {@linkplain InfoStatements#completeIntrospection related + * to the spatial schema}. This method is not for checking the existence of feature tables. + * If there is no information about the given table, then this method returns {@code true}. + * It may result in a {@link SQLException} to be thrown later. This is intentional, + * in order to identify that a check is missing. + * + * @param table name of the information table to check. + * @return whether the table of the given name does <em>not</em> exist. + */ + final boolean skipInfoTable(final String table) { + return Boolean.FALSE.equals(ignoredTables.get(table)); // Null values are mapped to `false`. + } + /** * Collects the names of all tables specified by user, ignoring standard tables such as {@code SPATIAL_REF_SYS}. * This method requires a list of tables to include in the model, but this list should not include dependencies. @@ -218,7 +234,7 @@ public final class Analyzer { try (ResultSet reflect = metadata.getTables(names[2], names[1], names[0], tableTypes)) { while (reflect.next()) { final String table = getUniqueString(reflect, Reflection.TABLE_NAME); - if (ignoredTables.contains(table)) { + if (ignoredTables.containsKey(table)) { continue; } declared.add(new TableReference( 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 daa92f3695..dd98a4f350 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 @@ -16,7 +16,6 @@ */ package org.apache.sis.storage.sql.feature; -import java.util.Set; import java.util.Map; import java.util.List; import java.util.EnumSet; @@ -310,21 +309,25 @@ public class Database<G> extends Syntax { /** * Detects automatically which spatial schema is in use. Detects also the catalog name and schema name. * This method is invoked exactly once after construction and before the analysis of feature tables. + * Various fields such as {@link #spatialSchema} are initialized by this method. * * @param metadata metadata to use for verifying which tables are present. * @param tableTypes the "TABLE" and "VIEW" keywords for table types, with unsupported keywords omitted. - * @return names of the standard tables defined by the spatial schema. + * @return names of tables to ignore when searching for feature tables, together with whether the table exists. */ - final Set<String> detectSpatialSchema(final DatabaseMetaData metadata, final String[] tableTypes) throws SQLException { + final Map<String,Boolean> detectSpatialSchema(final DatabaseMetaData metadata, final String[] tableTypes) + throws SQLException + { /* - * The following tables are defined by ISO 19125 / OGC Simple feature access part 2. - * Note that the standard specified those names in upper-case letters, which is also - * the default case specified by the SQL standard. However, some databases use lower - * cases instead. + * The keys of `ignoredTables` are the tables defined by ISO 19125 / OGC Simple feature access part 2. + * Note that the standard specifies those names in upper-case letters, which is also the default case + * specified by the SQL standard. However, some databases use lower cases instead. */ String crsTable = null; final var ignoredTables = new HashMap<String,Boolean>(8); - for (SpatialSchema convention : getPossibleSpatialSchemas(ignoredTables)) { + final SpatialSchema[] candidates = getPossibleSpatialSchemas(ignoredTables); + for (int i=0; i<candidates.length; i++) { + final SpatialSchema convention = candidates[i]; String geomTable; crsTable = convention.crsTable; geomTable = convention.geometryColumns; @@ -335,8 +338,8 @@ public class Database<G> extends Syntax { crsTable = crsTable .toUpperCase(Locale.US).intern(); geomTable = geomTable.toUpperCase(Locale.US).intern(); } - ignoredTables.put(crsTable, Boolean.TRUE); - ignoredTables.put(geomTable, Boolean.TRUE); + ignoredTables.put(crsTable, null); // `null` means that we have not yet checked if the table exists. + ignoredTables.put(geomTable, null); /* * Check if the database contains at least one "ignored" tables associated to `Boolean.TRUE`. * If many tables are found, ensure that the catalog and schema names are the same. If this @@ -347,15 +350,19 @@ public class Database<G> extends Syntax { boolean consistent = true; String catalog = null, schema = null; for (final Map.Entry<String,Boolean> entry : ignoredTables.entrySet()) { - if (entry.getValue()) { + // Unconditionally check table existence during the first iteration. + if (i == 0 || entry.getValue() == null) { + boolean exists = false; String table = escapeWildcards(entry.getKey()); try (ResultSet reflect = metadata.getTables(null, null, table, tableTypes)) { while (reflect.next()) { consistent &= consistent(catalog, catalog = reflect.getString(Reflection.TABLE_CAT)); consistent &= consistent(schema, schema = reflect.getString(Reflection.TABLE_SCHEM)); - found = true; + found |= !Boolean.FALSE.equals(entry.getValue()); // Accept `true` and `null` values. + exists = true; } } + entry.setValue(exists); } } if (found) { @@ -390,7 +397,7 @@ public class Database<G> extends Syntax { } } } - return ignoredTables.keySet(); + return ignoredTables; } /** @@ -700,12 +707,12 @@ public class Database<G> extends Syntax { * <p>The values in the map tells whether the table can be used as a sentinel value for determining * that the {@link SpatialSchema} enumeration value can be accepted.</p> * - * @param tables where to add names of tables that describe the spatial schema. + * @param ignoredTables where to add names of tables to ignore, together with whether they are sentinel tables. * @return the spatial schema conventions that may be supported by this database. * * @see #getSpatialSchema() */ - protected SpatialSchema[] getPossibleSpatialSchemas(Map<String,Boolean> tables) { + protected SpatialSchema[] getPossibleSpatialSchemas(Map<String,Boolean> ignoredTables) { return SpatialSchema.values(); } 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 440e195d90..ce4c1dbfb1 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 @@ -72,6 +72,11 @@ import org.opengis.metadata.Identifier; * <li>Finding a SRID from a Coordinate Reference System (CRS).</li> * </ul> * + * Some statements are used only during the {@linkplain Analyzer analysis} of the database schema. + * This is the case of the search for geometry information. Other statements are used every times + * that a query is executed. This is the case of the statements for fetching the CRS. + * + * <h2>Thread safety</h2> * This class is <strong>not</strong> thread-safe. Each instance should be used in a single thread. * Instances are created by {@link Database#createInfoStatements(Connection)}. * @@ -108,9 +113,18 @@ 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}. */ 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. + */ + protected boolean isAnalysisPrepared; + /** * The statement for fetching CRS Well-Known Text (WKT) from a SRID code. * @@ -178,18 +192,23 @@ 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(TableReference, Map)} implementations. + * This method is for {@link #completeIntrospection(Analyzer, TableReference, Map)} implementations. * - * @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. - * @param geomColNameColumn column of geometry column name, or {@code null} for the standard value. - * @param geomTypeColumn column of geometry type, or {@code null} for the standard value, or "" for none. - * @return the prepared statement for querying the geometry table. + * @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. + * @param geomColNameColumn column of geometry column name, or {@code null} for the standard value. + * @param geomTypeColumn column of geometry type, or {@code null} for the standard value, or "" for none. + * @return the prepared statement for querying the geometry table, or {@code null} if the table does not exist. * @throws SQLException if the statement cannot be created. */ - protected final PreparedStatement prepareIntrospectionStatement(final String table, final boolean raster, + protected final PreparedStatement prepareIntrospectionStatement( + final Analyzer analyzer, final String table, final boolean raster, String geomColNameColumn, String geomTypeColumn) throws SQLException { + if (analyzer.skipInfoTable(table)) { + return null; + } final SpatialSchema schema = database.getSpatialSchema().orElseThrow(); final var sql = new SQLBuilder(database).append(SQLBuilder.SELECT); if (geomColNameColumn == null) { @@ -236,30 +255,37 @@ public class InfoStatements implements Localized, AutoCloseable { /** * Gets all geometry and raster columns for the given table and sets information on the corresponding columns. - * Column instances in the {@code columns} map are modified in-place (the map itself is not modified). - * This method should be invoked before the {@link Column#valueGetter} field is set. + * Column instances in the {@code columns} map are modified in-place (the map content is not directly modified). + * 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. * - * @param source the table for which to get all geometry columns. - * @param columns all columns for the specified table. Keys are column names. + * @param analyzer the opaque temporary object used for analyzing the database schema. + * @param source the table for which to get all geometry columns. + * @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. * @throws SQLException if a SQL error occurred. */ - public void completeIntrospection(final TableReference source, final Map<String,Column> columns) throws Exception { + public void completeIntrospection(final Analyzer analyzer, final TableReference source, final Map<String,Column> columns) + throws Exception + { final SpatialSchema schema = database.getSpatialSchema().orElseThrow(); - if (geometryColumns == null) { - geometryColumns = prepareIntrospectionStatement(schema.geometryColumns, false, null, null); + if (!isAnalysisPrepared) { + isAnalysisPrepared = true; + geometryColumns = prepareIntrospectionStatement(analyzer, schema.geometryColumns, false, null, null); } configureSpatialColumns(geometryColumns, source, columns, schema.typeEncoding); } /** - * Implementation of {@link #completeIntrospection(TableReference, Map)} for geometries, - * as a separated methods for allowing sub-classes to override above-cited method. - * May also be used for non-geometric columns such as rasters, in which case the - * {@code typeValueKind} argument shall be {@code null}. + * Sets information about the specified columns of the given table using the given query. + * This method is the implementation of {@link #completeIntrospection(Analyzer, TableReference, Map)}, + * provided as a separated methods for allowing sub-classes to override the above-cited public method. + * This method is used for both geometric and non-geometric columns such as rasters, in which case the + * {@code typeValueKind} argument shall be {@code null}. The given {@code columnQuery} argument is the + * value returned by {@link #prepareIntrospectionStatement(Analyzer, String, boolean, String, String)}. * - * @param columnQuery a statement prepared by {@link #prepareIntrospectionStatement(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 columns all columns for the specified table. Keys are column names. * @param typeValueKind {@code NUMERIC}, {@code TEXTUAL} or {@code null} if none. @@ -275,6 +301,9 @@ 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) { + return; + } int p = 0; if (database.supportsCatalogs) setCatalogOrSchema(columnQuery, ++p, source.catalog); if (database.supportsSchemas) setCatalogOrSchema(columnQuery, ++p, source.schema); 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 50448a641c..942d8bb9af 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 @@ -159,7 +159,7 @@ final class QueryAnalyzer extends FeatureAnalyzer { final InfoStatements spatialInformation = analyzer.spatialInformation; if (spatialInformation != null) { for (final Map.Entry<TableReference, Map<String,Column>> entry : columnsPerTable.entrySet()) { - spatialInformation.completeIntrospection(entry.getKey(), entry.getValue()); + spatialInformation.completeIntrospection(analyzer, entry.getKey(), entry.getValue()); } } /* 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 2571c18b95..043923ec66 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 @@ -163,7 +163,7 @@ final class TableAnalyzer extends FeatureAnalyzer { } final InfoStatements spatialInformation = analyzer.spatialInformation; if (spatialInformation != null) { - spatialInformation.completeIntrospection(id, columns); + spatialInformation.completeIntrospection(analyzer, id, columns); } /* * Analyze the type of each column, which may be geometric as a consequence of above call. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableReference.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableReference.java index b19f5a5130..5436ecb76c 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableReference.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableReference.java @@ -151,7 +151,7 @@ public class TableReference { * if the output device uses a monospaced font and supports Unicode. */ static String toString(final Object owner, final Consumer<TreeTable.Node> appender) { - final DefaultTreeTable table = new DefaultTreeTable(TableColumn.NAME); + final var table = new DefaultTreeTable(TableColumn.NAME); final TreeTable.Node root = table.getRoot(); root.setValue(TableColumn.NAME, owner.getClass().getSimpleName()); appender.accept(root); diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedInfo.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedInfo.java index 702a81ab09..3e0b602d73 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedInfo.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedInfo.java @@ -20,6 +20,7 @@ import java.util.Map; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import org.apache.sis.storage.sql.feature.Analyzer; import org.apache.sis.storage.sql.feature.Column; import org.apache.sis.storage.sql.feature.Database; import org.apache.sis.storage.sql.feature.TableReference; @@ -37,13 +38,17 @@ final class ExtendedInfo extends InfoStatements { /** * A statement for fetching geometric information for a specific column. * This statement is used for objects of type "Geography", which is a data type specific to PostGIS. + * 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}. */ private PreparedStatement geographyColumns; /** * A statement for fetching raster 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}. */ - protected PreparedStatement rasterColumns; + private PreparedStatement rasterColumns; /** * The object for reading a raster, or {@code null} if not yet created. @@ -61,19 +66,17 @@ final class ExtendedInfo extends InfoStatements { /** * Gets all geometry columns for the given table and sets the geometry information on the corresponding columns. * - * @param source the table for which to get all geometry columns. - * @param columns all columns for the specified table. Keys are column names. + * @param analyzer the opaque temporary object used for analyzing the database schema. + * @param source the table for which to get all geometry columns. + * @param columns all columns for the specified table. Keys are column names. */ @Override - public void completeIntrospection(final TableReference source, final Map<String,Column> columns) throws Exception { - if (geometryColumns == null) { - geometryColumns = prepareIntrospectionStatement("geometry_columns", false, "f_geometry_column", "type"); - } - if (geographyColumns == null) { - geographyColumns = prepareIntrospectionStatement("geography_columns", false, "f_geography_column", "type"); - } - if (rasterColumns == null) { - rasterColumns = prepareIntrospectionStatement("raster_columns", true, "r_raster_column", ""); + public void completeIntrospection(final Analyzer analyzer, final TableReference source, final Map<String,Column> columns) throws Exception { + if (!isAnalysisPrepared) { + isAnalysisPrepared = true; + geometryColumns = prepareIntrospectionStatement(analyzer, "geometry_columns", false, "f_geometry_column", "type"); + geographyColumns = prepareIntrospectionStatement(analyzer, "geography_columns", false, "f_geography_column", "type"); + rasterColumns = prepareIntrospectionStatement(analyzer, "raster_columns", true, "r_raster_column", ""); } configureSpatialColumns(geometryColumns, source, columns, GeometryTypeEncoding.TEXTUAL); configureSpatialColumns(geographyColumns, source, columns, GeometryTypeEncoding.TEXTUAL); @@ -100,6 +103,10 @@ final class ExtendedInfo extends InfoStatements { geographyColumns.close(); geographyColumns = null; } + if (rasterColumns != null) { + rasterColumns.close(); + rasterColumns = null; + } super.close(); } } 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 c4b0f97c23..356cf07238 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 @@ -197,7 +197,7 @@ public final class Postgres<G> extends Database<G> { * <p>The values in the map tells whether the table can be used as a sentinel value for * determining that the {@link SpatialSchema} enumeration value can be accepted.</p> * - * @param tables where to add names of tables that describe the spatial schema. + * @param ignoredTables where to add names of tables to ignore, together with whether they are sentinel tables. * @return the spatial schema convention supported by this database. */ @Override