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 a20b329fb8 Allow FeatureSQL to understand different standards about the table of CRS definitions. Supported standards are: a20b329fb8 is described below commit a20b329fb80d438e449113acba9200dc8114224f Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Aug 7 14:10:57 2024 +0200 Allow FeatureSQL to understand different standards about the table of CRS definitions. Supported standards are: - Geopackage - ISO-13249 SQL/MM - ISO 19125 / OGC Simple feature access part 2 They have basically the same content, but with different table and column names. --- .../apache/sis/metadata/sql/privy/SQLBuilder.java | 13 ++ .../org/apache/sis/metadata/sql/TestDatabase.java | 2 +- .../apache/sis/storage/sql/feature/Analyzer.java | 2 +- .../apache/sis/storage/sql/feature/Database.java | 93 +++++++---- .../sis/storage/sql/feature/FeatureIterator.java | 5 +- .../sis/storage/sql/feature/InfoStatements.java | 98 ++++++----- .../sis/storage/sql/feature/SpatialSchema.java | 180 +++++++++++++++++++++ .../sis/storage/sql/postgis/ExtendedInfo.java | 6 +- .../apache/sis/storage/sql/postgis/Postgres.java | 15 +- .../org/apache/sis/storage/sql/SQLStoreTest.java | 2 +- 10 files changed, 327 insertions(+), 89 deletions(-) diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java index 11fa406bac..50bc4fa242 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java @@ -113,6 +113,19 @@ public class SQLBuilder extends Syntax { return this; } + /** + * Appends verbatim a sub-sequence of the given text. + * + * @param text the text to append verbatim. + * @param start starting index of the text append, inclusive. + * @param end end index of the text to append, exclusive. + * @return this builder, for method call chaining. + */ + public final SQLBuilder append(final String text, final int start, final int end) { + buffer.append(text, start, end); + return this; + } + /** * Appends the given text verbatim. * The text should be SQL keywords like {@code "SELECT * FROM"}. diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/TestDatabase.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/TestDatabase.java index 31f69a43f8..6568d54fee 100644 --- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/TestDatabase.java +++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/TestDatabase.java @@ -285,7 +285,7 @@ public class TestDatabase implements AutoCloseable { } return new TestDatabase(ds, Dialect.POSTGRESQL) { @Override public void close() throws SQLException { - final PGSimpleDataSource ds = (PGSimpleDataSource) source; + final var ds = (PGSimpleDataSource) source; try (Connection c = ds.getConnection()) { try (Statement s = c.createStatement()) { /* 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 44a534e9b5..a6c61b1c59 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 @@ -134,7 +134,7 @@ final class Analyzer { this.metadata = metadata; this.escape = metadata.getSearchStringEscape(); this.nameFactory = DefaultNameFactory.provider(); - spatialInformation = database.isSpatial() ? database.createInfoStatements(connection) : null; + spatialInformation = database.getSpatialSchema().isPresent() ? database.createInfoStatements(connection) : null; } /** 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 be81eba736..71503d3fd3 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 @@ -25,6 +25,7 @@ import java.util.LinkedHashSet; import java.util.WeakHashMap; import java.util.ArrayList; import java.util.Locale; +import java.util.Optional; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.logging.LogRecord; import java.sql.Array; @@ -79,9 +80,9 @@ import org.apache.sis.util.collection.Cache; * <h2>Specializations</h2> * Subclasses may be defined for some database engines. Methods that can be overridden are: * <ul> + * <li>{@link #getPossibleSpatialSchemas(Map)} for enumerating the spatial schema conventions that may be used.</li> * <li>{@link #getMapping(Column)} for adding column types to recognize.</li> * <li>{@link #createInfoStatements(Connection)} for more info about spatial information.</li> - * <li>{@link #addIgnoredTables(Map)} for specifying more tables to ignore.</li> * </ul> * * <h2>Multi-threading</h2> @@ -138,14 +139,17 @@ public class Database<G> extends Syntax { private Table[] tables; /** - * Whether the database contains "GEOMETRY_COLUMNS" and/or "SPATIAL_REF_SYS" tables. - * May also be set to {@code true} if some database-specific tables are found such as - * {@code "geography_columns"} and {@code "raster_columns"} in PostGIS. + * Information about table names and column names used for the spatial schema, or {@code null}. + * This is non-null if the database contains "GEOMETRY_COLUMNS" and/or "SPATIAL_REF_SYS" tables, + * possibly with different name depending on the conventions of the spatial schema. May also be + * non-null if some database-specific tables are found such as {@code "geography_columns"} and + * {@code "raster_columns"} in PostGIS. + * * This field is initialized by {@link #analyze analyze(…)} and shall not be modified after that point. * - * @see #isSpatial() + * @see #getSpatialSchema() */ - private boolean isSpatial; + private SpatialSchema spatialSchema; /** * {@code true} if this database contains at least one geometry column. @@ -164,8 +168,8 @@ public class Database<G> extends Syntax { private boolean hasRaster; /** - * Catalog and schema of the {@value InfoStatements#GEOMETRY_COLUMNS} and - * {@value InfoStatements#SPATIAL_REF_SYS} tables, or null or empty string if none. + * Catalog and schema of the {@code "GEOMETRY_COLUMNS"} and {@code "SPATIAL_REF_SYS"} tables, + * or null or empty string if none. The actual table names depend on {@link #spatialSchema}. */ String catalogOfSpatialTables, schemaOfSpatialTables; @@ -201,10 +205,10 @@ public class Database<G> extends Syntax { /** * Cache of Coordinate Reference Systems created for a given SRID. - * SRID are primary keys in the {@value InfoStatements#SPATIAL_REF_SYS} table. + * SRID are primary keys in the {@code "SPATIAL_REF_SYS"} (or equivalent) table. * They are not EPSG codes, even if the numerical values are often the same. * - * <p>This mapping depend on the content of {@value InfoStatements#SPATIAL_REF_SYS} table. + * <p>This mapping depends on the content of {@code "SPATIAL_REF_SYS"} (or equivalent) table. * For that reason, a distinct cache exists for each database.</p> */ final Cache<Integer, CoordinateReferenceSystem> cacheOfCRS; @@ -342,17 +346,26 @@ public class Database<G> extends Syntax { * the default case specified by the SQL standard. However, some databases use lower * cases instead. */ - String tableCRS = InfoStatements.SPATIAL_REF_SYS; - String tableGeom = InfoStatements.GEOMETRY_COLUMNS; - if (metadata.storesLowerCaseIdentifiers()) { - tableCRS = tableCRS .toLowerCase(Locale.US).intern(); - tableGeom = tableGeom.toLowerCase(Locale.US).intern(); + final var ignoredTables = new HashMap<String,Boolean>(8); + for (SpatialSchema schema : getPossibleSpatialSchemas(ignoredTables)) { + String tableCRS = schema.crsTable;; + String tableGeom = schema.geometryColumns; + if (metadata.storesLowerCaseIdentifiers()) { + tableCRS = tableCRS .toLowerCase(Locale.US).intern(); + tableGeom = tableGeom.toLowerCase(Locale.US).intern(); + } else if (metadata.storesUpperCaseIdentifiers()) { + tableCRS = tableCRS .toUpperCase(Locale.US).intern(); + tableGeom = tableGeom.toUpperCase(Locale.US).intern(); + } + ignoredTables.put(tableCRS, Boolean.TRUE); + ignoredTables.put(tableGeom, Boolean.TRUE); + if (hasTable(metadata, tableTypes, ignoredTables)) { + spatialSchema = schema; + break; + } + ignoredTables.remove(tableCRS); + ignoredTables.remove(tableGeom); } - final Map<String,Boolean> ignoredTables = new HashMap<>(8); - ignoredTables.put(tableCRS, Boolean.TRUE); - ignoredTables.put(tableGeom, Boolean.TRUE); - addIgnoredTables(ignoredTables); - isSpatial = hasTable(metadata, tableTypes, ignoredTables); /* * Collect the names of all tables specified by user, ignoring the tables * used for database internal working (for example by PostGIS). @@ -434,14 +447,14 @@ public class Database<G> extends Syntax { * * @param metadata value of {@code connection.getMetaData()}. * @param tableTypes value of {@link #getTableTypes(DatabaseMetaData)}. - * @param tables name of the table to search. + * @param tables name of the table to search. Will not be modified. * @return whether the given table has been found. */ private boolean hasTable(final DatabaseMetaData metadata, final String[] tableTypes, final Map<String,Boolean> tables) throws SQLException { // `SimpleImmutableEntry` used as a way to store a (catalog,schema) pair of strings. - final FrequencySortedSet<SimpleImmutableEntry<String,String>> schemas = new FrequencySortedSet<>(true); + final var schemas = new FrequencySortedSet<SimpleImmutableEntry<String,String>>(true); int count = 0; for (final Map.Entry<String,Boolean> entry : tables.entrySet()) { if (entry.getValue()) { @@ -527,13 +540,13 @@ public class Database<G> extends Syntax { } /** - * Returns {@code true} if this database is a spatial database. - * Tables such as "SPATIAL_REF_SYS" are used as sentinel values. + * Returns an identification of the table and column naming conventions. + * This is absent if the database is not spatial. * - * @return whether this database is a spatial database. + * @return an identification of the table and column naming conventions. */ - public final boolean isSpatial() { - return isSpatial; + public final Optional<SpatialSchema> getSpatialSchema() { + return Optional.ofNullable(spatialSchema); } /** @@ -706,16 +719,28 @@ public class Database<G> extends Syntax { } /** - * Adds to the given map a list of tables to ignore when searching for feature tables. - * The given map already contains the {@code "SPATIAL_REF_SYS"} and {@code "GEOMETRY_COLUMNS"} - * entries when this method is invoked. The default implementation adds nothing. + * Returns the spatial schema conventions that may possibly be supported by this database. + * The default implementation returns all {@link SpatialSchema} enumeration values. + * Subclasses may restrict to a smaller set of possibilities. + * + * <p>In addition, this method can declare in the supplied map which tables are used for describing + * the spatial schema. The default implementation does nothing because the entries to add depend on + * the {@link SpatialSchema}. For example, if Simple Features conventions are used, then the tables + * are {@code "SPATIAL_REF_SYS"} and {@code "GEOMETRY_COLUMNS"}. Subclasses can add other entries + * if they know in advance that they support only one convention, or that all the conventions that + * they support use the same table names. The table added to the map will be ignored when searching + * for feature tables.</p> + * + * <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> * - * <p>Values tells whether the table can be used as a sentinel value for determining - * that this database {@linkplain #isSpatial is a spatial database}.</p> + * @param tables where to add names of tables that describe the spatial schema. + * @return the spatial schema conventions that may be supported by this database. * - * @param ignoredTables where to add names of tables to ignore. + * @see #getSpatialSchema() */ - protected void addIgnoredTables(final Map<String,Boolean> ignoredTables) { + protected SpatialSchema[] getPossibleSpatialSchemas(Map<String,Boolean> tables) { + return SpatialSchema.values(); } /** 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 eb60040003..01b605c8da 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 @@ -85,7 +85,7 @@ final class FeatureIterator implements Spliterator<Feature>, AutoCloseable { /** * A cache of statements for fetching spatial information such as geometry columns or SRID. - * This is non-null only if the {@linkplain Database#isSpatial() database is spatial}. + * This is non-null only if the {@linkplain Database#getSpatialSchema() database is spatial}. * The same instance is shared by all dependencies of this {@code FeatureIterator}. */ private final InfoStatements spatialInformation; @@ -118,7 +118,8 @@ final class FeatureIterator implements Spliterator<Feature>, AutoCloseable { throws SQLException, InternalDataStoreException { adapter = table.adapter(connection); - spatialInformation = table.database.isSpatial() ? table.database.createInfoStatements(connection) : null; + spatialInformation = table.database.getSpatialSchema().isPresent() + ? table.database.createInfoStatements(connection) : null; String sql = adapter.sql; if (distinct || filter != null || sort != null || offset > 0 || count > 0) { final SQLBuilder builder = new SQLBuilder(table.database).append(sql); 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 0010191315..45b087f698 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 @@ -74,19 +74,6 @@ import org.opengis.metadata.Identifier; * @see <a href="https://www.ogc.org/standards/sfs">OGC Simple feature access — Part 2: SQL option</a> */ public class InfoStatements implements Localized, AutoCloseable { - /** - * The table containing CRS definitions, as specified by ISO 19125 / OGC Simple feature access part 2. - * Note that the standard specifies table names in upper-case letters, which is also the default case - * specified by the SQL standard. However, some databases use lower cases instead. This table name can - * be used unquoted for letting the database engine converts the case. - */ - static final String SPATIAL_REF_SYS = "SPATIAL_REF_SYS"; - - /** - * The table containing the list of geometry columns, as specified by ISO 19125 / OGC Simple feature access part 2. - */ - static final String GEOMETRY_COLUMNS = "GEOMETRY_COLUMNS"; - /** * Specifies how the geometry type is encoded in the {@code "GEOMETRY_TYPE"} column. * The OGC standard defines numeric values, but PostGIS uses textual values. @@ -202,38 +189,51 @@ public class InfoStatements implements Localized, AutoCloseable { } /** - * Appends a statement after {@code "WHERE"} such as {@code ""F_TABLE_NAME = ?"}. + * Appends the name of a geometry column or raster column. * - * @param sql the builder where to add the SQL statement. - * @param prefix the column name prefix: {@code 'F'} for features or {@code 'R'} for rasters. - * @param column the column name (e.g. {@code "TABLE_NAME"}. + * @param sql the builder where to add the column name. + * @param raster whether the statement is for raster table instead of geometry table. + * @param column the column name (i.e., {@code "F_TABLE_NAME"}. * @return the given SQL builder. */ - private static SQLBuilder appendCondition(final SQLBuilder sql, final char prefix, final String column) { - return sql.append(prefix).append('_').append(column).append(" = ?"); + private static SQLBuilder appendColumn(final SQLBuilder sql, final boolean raster, final String column) { + if (raster && column.startsWith("F_")) { + return sql.append('R').append(column, 1, column.length()); + } else { + return sql.append(column); + } } /** * 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. * - * @param table name of the geometry table. Standard value is {@code "GEOMETRY_COLUMNS"}. - * @param prefix column name prefix: {@code 'F'} for features or {@code 'R'} for rasters. - * @param column name of the geometry column without prefix. Standard value is {@code "GEOMETRY_COLUMN"}. - * @param otherColumn additional columns or {@code null} if none. Standard value is {@code "GEOMETRY_TYPE"}. + * @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. * @throws SQLException if the statement cannot be created. */ - protected final PreparedStatement prepareIntrospectionStatement(final String table, - final char prefix, final String column, final String otherColumn) throws SQLException + protected final PreparedStatement prepareIntrospectionStatement(final String table, final boolean raster, + String geomColNameColumn, String geomTypeColumn) throws SQLException { - final SQLBuilder sql = new SQLBuilder(database).append(SQLBuilder.SELECT) - .append(prefix).append('_').append(column).append(", SRID "); - if (otherColumn != null) sql.append(", ").append(otherColumn); + final SpatialSchema schema = database.getSpatialSchema().orElseThrow(); + final var sql = new SQLBuilder(database).append(SQLBuilder.SELECT); + if (geomColNameColumn == null) { + geomColNameColumn = schema.geomColNameColumn; + } + appendColumn(sql, raster, geomColNameColumn).append(", ").append(schema.crsIdentifierColumn).append(' '); + if (geomTypeColumn == null) { + geomTypeColumn = schema.geomTypeColumn; + } + if (geomTypeColumn != null && !geomTypeColumn.isEmpty()) { + sql.append(", ").append(geomTypeColumn); + } appendFrom(sql, table); - if (database.supportsCatalogs) appendCondition(sql, prefix, "TABLE_CATALOG").append(" AND "); - if (database.supportsSchemas) appendCondition(sql, prefix, "TABLE_SCHEMA" ).append(" AND "); - appendCondition(sql, prefix, "TABLE_NAME"); + if (database.supportsCatalogs) appendColumn(sql, raster, schema.geomCatalogColumn).append("=? AND "); + if (database.supportsSchemas) appendColumn(sql, raster, schema.geomSchemaColumn) .append("=? AND "); + appendColumn(sql, raster, schema.geomTableColumn).append("=?"); return connection.prepareStatement(sql.toString()); } @@ -250,7 +250,8 @@ public class InfoStatements implements Localized, AutoCloseable { */ public void completeIntrospection(final TableReference source, final Map<String,Column> columns) throws Exception { if (geometryColumns == null) { - geometryColumns = prepareIntrospectionStatement(GEOMETRY_COLUMNS, 'F', "GEOMETRY_COLUMN", "GEOMETRY_TYPE"); + final SpatialSchema schema = database.getSpatialSchema().orElseThrow(); + geometryColumns = prepareIntrospectionStatement(schema.geometryColumns, false, null, null); } configureSpatialColumns(geometryColumns, source, columns, GeometryTypeEncoding.NUMERIC); } @@ -261,7 +262,7 @@ public class InfoStatements implements Localized, AutoCloseable { * May also be used for non-geometric columns such as rasters, in which case the * {@code typeValueKind} argument shall be {@code null}. * - * @param columnQuery a statement prepared by {@link #prepareIntrospectionStatement(String, char, String, String)}. + * @param columnQuery a statement prepared by {@link #prepareIntrospectionStatement(String, boolean, String, String)}. * @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. @@ -302,12 +303,13 @@ public class InfoStatements implements Localized, AutoCloseable { /** * Gets a Coordinate Reference System for to given SRID. * If the given SRID is zero or negative, then this method returns {@code null}. - * Otherwise the CRS is decoded from the database {@value #SPATIAL_REF_SYS} table. + * Otherwise the CRS is decoded from the database {@code "SPATIAL_REF_SYS"} table + * or equivalent (depending on the {@link SpatialSchema}). * * @param srid the Spatial Reference Identifier (SRID) to resolve as a CRS object. * @return the CRS associated to the given SRID, or {@code null} if the SRID is zero. * @throws DataStoreContentException if the CRS cannot be fetched. Possible reasons are: - * no entry found in the {@value #SPATIAL_REF_SYS} table, or more than one entry is found, + * no entry found in the {@code "SPATIAL_REF_SYS"} table, or more than one entry is found, * or a single entry exists but has no WKT definition and its authority code is unsupported by SIS. * @throws ParseException if the WKT cannot be parsed. * @throws SQLException if a SQL error occurred. @@ -323,7 +325,7 @@ public class InfoStatements implements Localized, AutoCloseable { /** * Invoked when the requested CRS is not in the cache. This method gets the entry from the - * {@value #SPATIAL_REF_SYS} table then gets the CRS from its authority code if possible, + * {@link SpatialSchema#refSysTable} then gets the CRS from its authority code if possible, * or fallback on the WKT otherwise. * * @param srid the Spatial Reference Identifier (SRID) of the CRS to create from the database content. @@ -332,10 +334,13 @@ public class InfoStatements implements Localized, AutoCloseable { */ private CoordinateReferenceSystem parseCRS(final int srid) throws Exception { if (wktFromSrid == null) { + final SpatialSchema schema = database.getSpatialSchema().orElseThrow(); final SQLBuilder sql = new SQLBuilder(database); - sql.append("SELECT auth_name, auth_srid, srtext"); - appendFrom(sql, SPATIAL_REF_SYS); - sql.append("srid=?"); + sql.append(SQLBuilder.SELECT).append(schema.crsAuthorityNameColumn) + .append(", ").append(schema.crsAuthorityCodeColumn) + .append(", ").append(schema.crsDefinitionColumn); + appendFrom(sql, schema.crsTable); + sql.append(schema.crsIdentifierColumn).append("=?"); wktFromSrid = connection.prepareStatement(sql.toString()); } wktFromSrid.setInt(1, srid); @@ -393,7 +398,8 @@ public class InfoStatements implements Localized, AutoCloseable { if (crs == null) { crs = v.recommendation; } else if (!crs.equals(v.recommendation)) { - throw invalidSRID(Resources.Keys.DuplicatedSRID_2, SPATIAL_REF_SYS, srid, authorityError); + final SpatialSchema schema = database.getSpatialSchema().orElseThrow(); + throw invalidSRID(Resources.Keys.DuplicatedSRID_2, schema.crsTable, srid, authorityError); } warning = v.warning(false); if (warning == null && fromWKT != null) { @@ -417,7 +423,8 @@ public class InfoStatements implements Localized, AutoCloseable { if (authorityError != null) { throw authorityError; } - throw invalidSRID(Resources.Keys.UnknownSRID_2, SPATIAL_REF_SYS, srid, null); + final SpatialSchema schema = database.getSpatialSchema().orElseThrow(); + throw invalidSRID(Resources.Keys.UnknownSRID_2, schema.crsTable, srid, null); } if (warning != null) { warning.setLoggerName(Modules.SQL); @@ -497,10 +504,13 @@ public class InfoStatements implements Localized, AutoCloseable { * Get the WKT and verifies if the CRS is approximately equal. */ if (sridFromCRS == null) { + final SpatialSchema schema = database.getSpatialSchema().orElseThrow(); final SQLBuilder sql = new SQLBuilder(database); - sql.append("SELECT srtext, srid"); - appendFrom(sql, SPATIAL_REF_SYS); - sql.append("auth_name=? AND auth_srid=?"); + sql.append(SQLBuilder.SELECT).append(schema.crsDefinitionColumn) + .append(", ").append(schema.crsIdentifierColumn); + appendFrom(sql, schema.crsTable); + sql.append(schema.crsAuthorityNameColumn).append("=? AND ") + .append(schema.crsAuthorityCodeColumn).append("=?"); sridFromCRS = connection.prepareStatement(sql.toString()); } sridFromCRS.setString(1, authority); 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 new file mode 100644 index 0000000000..857530ca9b --- /dev/null +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java @@ -0,0 +1,180 @@ +/* + * 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; + + +/** + * Information about table names and column names used for the spatial schema. + * There is many standards, with nearly identical content but different names. + * + * <ul> + * <li>Geopackage</li> + * <li>ISO-13249 SQL/MM</li> + * <li>ISO 19125 / OGC Simple feature access part 2</li> + * </ul> + * + * The presence of tables for each standard will be tested in enumeration order. + * + * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) + */ +public enum SpatialSchema { + /** + * Table and column names as specified by Geopackage. This is the same thing as {@link #SQL_MM} + * except for table names and for the case (Geopackage uses lower case). + */ + GEOPACKAGE("gpkg_spatial_ref_sys", "srs_id", "organization", "organization_coordsys_id", "definition", + "gpkg_geometry_columns", "table_catalog", "table_schema", "table_name", "column_name", null), + + /** + * 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: + * + * {@snippet lang="sql" : + * CREATE TABLE ST_SPATIAL_REFERENCE_SYSTEMS( + * SRS_NAME CHARACTER VARYING(ST_MaxSRSNameLength) NOT NULL, + * SRS_ID INTEGER NOT NULL, + * ORGANIZATION CHARACTER VARYING(ST_MaxOrganizationNameLength), + * ORGANIZATION_COORDSYS_ID INTEGER, + * DEFINITION CHARACTER VARYING(ST_MaxSRSDefinitionLength) NOT NULL, + * DESCRIPTION CHARACTER VARYING(ST_MaxDescriptionLength)) + * } + * + * In Geopackage, this table is named {@code "gpkg_spatial_ref_sys"} but otherwise has identical content + * except for the case (Geopackage uses lower case). + */ + SQL_MM("ST_SPATIAL_REFERENCE_SYSTEMS", "SRS_ID", "ORGANIZATION", "ORGANIZATION_COORDSYS_ID", "DEFINITION", + "ST_GEOMETRY_COLUMNS", "TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME", null), + + /** + * Table and column names as specified by ISO 19125 / OGC Simple feature access part 2. + * Note that the standard specifies table names in upper-case letters, which is also the default case + * specified by the SQL standard. However, some databases use lower cases instead. This table name can + * be used unquoted for letting the database engine converts the case. The table definition for CRS is: + * + * {@snippet lang="sql" : + * CREATE TABLE SPATIAL_REF_SYS ( + * SRID INTEGER NOT NULL PRIMARY KEY, + * AUTH_NAME CHARACTER VARYING, + * AUTH_SRID INTEGER, + * SRTEXT CHARACTER VARYING(2048)) + * } + */ + SIMPLE_FEATURE("SPATIAL_REF_SYS", "SRID", "AUTH_NAME", "AUTH_SRID", "SRTEXT", + "GEOMETRY_COLUMNS", "F_TABLE_CATALOG", "F_TABLE_SCHEMA", "F_TABLE_NAME", "F_GEOMETRY_COLUMN", "GEOMETRY_TYPE"); + + /** + * Name of the table for Spatial Reference System definitions. + * Example: {@code "SPATIAL_REF_SYS"}, {@code "ST_SPATIAL_REFERENCE_SYSTEMS"}. + */ + final String crsTable; + + /** + * Name of the column for CRS identifiers. + * Example: {@code "SRID"}, {@code "SRS_ID"}. + * Also used in the geometry columns table. + */ + final String crsIdentifierColumn; + + /** + * Name of the column for CRS authority names. + * Example: {@code "AUTH_NAME"}, {@code "ORGANIZATION"}. + */ + final String crsAuthorityNameColumn; + + /** + * Name of the column for CRS authority codes. + * Example: {@code "AUTH_SRID"}, {@code "ORGANIZATION_COORDSYS_ID"}. + */ + final String crsAuthorityCodeColumn; + + /** + * Name of the column for CRS definitions in Well-Known Text (<abbr>WKT</abbr>) format. + * Example: {@code "SRTEXT"}, {@code "DEFINITION"}. + */ + final String crsDefinitionColumn; + + /** + * Name of the table enumerating the geometry columns. + */ + final String geometryColumns; + + /** + * Name of the column where the catalog of each geometry column is stored. + * Example: {@code "F_TABLE_CATALOG"}, {@code "TABLE_CATALOG"}. + */ + final String geomCatalogColumn; + + /** + * Name of the column where the schema of each geometry column is stored. + * Example: {@code "F_TABLE_SCHEMA"}, {@code "TABLE_SCHEMA"}. + */ + final String geomSchemaColumn; + + /** + * Name of the column where the table of each geometry column is stored. + * Example: {@code "F_TABLE_NAME"}, {@code "TABLE_NAME"}. + */ + final String geomTableColumn; + + /** + * Name of the column where the column of each geometry column is stored. + * Example: {@code "F_GEOMETRY_COLUMN"}, {@code "COLUMN_NAME"}. + */ + final String geomColNameColumn; + + /** + * Name of the column where the type of each geometry column is stored, or {@code null} if none. + * Example: {@code "GEOMETRY_TYPE"}. + * + * @see InfoStatements.GeometryTypeEncoding + */ + final String geomTypeColumn; + + /** + * Creates a new enumeration value. + * + * @param crsTable name of the table for Spatial Reference System definitions. + * @param crsIdentifierColumn name of the column for CRS identifiers. + * @param crsAuthorityNameColumn name of the column for CRS authority names. + * @param crsAuthorityCodeColumn name of the column for CRS authority codes. + * @param crsDefinitionColumn name of the column for CRS definitions in <abbr>WKT</abbr> format. + * @param geometryColumns name of the table enumerating the geometry columns. + * @param geomCatalogColumn name of the column where the catalog of each geometry column is stored. + * @param geomSchemaColumn name of the column where the schema of each geometry column is stored. + * @param geomTableColumn name of the column where the table of each geometry column is stored. + * @param geomColNameColumn name of the column where the column of each geometry column is stored. + * @param geomTypeColumn name of the column where the type of each geometry column is stored, or null if none. + */ + private SpatialSchema(String crsTable, String crsIdentifierColumn, String crsAuthorityNameColumn, + String crsAuthorityCodeColumn, String crsDefinitionColumn, String geometryColumns, + String geomCatalogColumn, String geomSchemaColumn, String geomTableColumn, + String geomColNameColumn, String geomTypeColumn) + { + this.crsTable = crsTable; + this.crsIdentifierColumn = crsIdentifierColumn; + this.crsAuthorityNameColumn = crsAuthorityNameColumn; + this.crsAuthorityCodeColumn = crsAuthorityCodeColumn; + this.crsDefinitionColumn = crsDefinitionColumn; + this.geometryColumns = geometryColumns; + this.geomCatalogColumn = geomCatalogColumn; + this.geomSchemaColumn = geomSchemaColumn; + this.geomTableColumn = geomTableColumn; + this.geomColNameColumn = geomColNameColumn; + this.geomTypeColumn = geomTypeColumn; + } +} 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 2dc0f863e8..843804a7ed 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 @@ -66,13 +66,13 @@ final class ExtendedInfo extends InfoStatements { @Override public void completeIntrospection(final TableReference source, final Map<String,Column> columns) throws Exception { if (geometryColumns == null) { - geometryColumns = prepareIntrospectionStatement("geometry_columns", 'f', "geometry_column", "type"); + geometryColumns = prepareIntrospectionStatement("geometry_columns", false, "f_geometry_column", "type"); } if (geographyColumns == null) { - geographyColumns = prepareIntrospectionStatement("geography_columns", 'f', "geography_column", "type"); + geographyColumns = prepareIntrospectionStatement("geography_columns", false, "f_geography_column", "type"); } if (rasterColumns == null) { - rasterColumns = prepareIntrospectionStatement("raster_columns", 'r', "raster_column", null); + rasterColumns = prepareIntrospectionStatement("raster_columns", true, "r_raster_column", ""); } configureSpatialColumns(geometryColumns, source, columns, GeometryTypeEncoding.TEXTUAL); configureSpatialColumns(geographyColumns, source, columns, GeometryTypeEncoding.TEXTUAL); 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 054f36b253..20a4a43d1a 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 @@ -37,6 +37,7 @@ import org.apache.sis.storage.sql.feature.Database; import org.apache.sis.storage.sql.feature.ValueGetter; import org.apache.sis.storage.sql.feature.Resources; import org.apache.sis.storage.sql.feature.SelectionClauseWriter; +import org.apache.sis.storage.sql.feature.SpatialSchema; import org.apache.sis.metadata.sql.privy.Dialect; import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.util.Version; @@ -185,15 +186,23 @@ public final class Postgres<G> extends Database<G> { } /** - * Adds to the given set a list of tables to ignore when searching for feature tables. + * Returns the spatial schema conventions that may possibly be supported by this database. + * The only value expected by PostGIS databases is {@link SpatialSchema#SIMPLE_FEATURE}. + * This method also completes the given map with additional tables describing the schema. + * Those tables shall be ignored when searching for feature tables. * - * @param ignoredTables where to add names of tables to ignore. + * <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. + * @return the spatial schema convention supported by this database. */ @Override - protected void addIgnoredTables(final Map<String,Boolean> ignoredTables) { + protected SpatialSchema[] getPossibleSpatialSchemas(final Map<String,Boolean> ignoredTables) { ignoredTables.put("geography_columns", Boolean.TRUE); // Postgis 1+ ignoredTables.put("raster_columns", Boolean.TRUE); // Postgis 2 ignoredTables.put("raster_overviews", Boolean.FALSE); + return new SpatialSchema[] {SpatialSchema.SIMPLE_FEATURE}; } /** diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java index a797cfe2ce..fcf690c9b6 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java @@ -132,7 +132,7 @@ public final class SQLStoreTest extends TestOnAllDatabases { */ @Override protected void test(final TestDatabase database, final boolean noschema) throws Exception { - final var scripts = new ArrayList<>(2); + final var scripts = new ArrayList<Object>(2); if (noschema) { scripts.add("CREATE SCHEMA " + SCHEMA + ';'); // Omit the "CREATE SCHEMA" statement if the schema already exists.