This is an automated email from the ASF dual-hosted git repository. amanin pushed a commit to branch refactor/sql-store in repository https://gitbox.apache.org/repos/asf/sis.git
commit 19dde60f603dab3e8e246978a4d3db778b6394f7 Author: Alexis Manin <[email protected]> AuthorDate: Fri Nov 8 17:17:42 2019 +0100 WIP(SQLSotre): first functional draft for PostGIS geometry decoding. --- .../sis/internal/sql/feature/ANSIInterpreter.java | 2 +- .../sis/internal/sql/feature/ANSIMapping.java | 17 +- .../apache/sis/internal/sql/feature/Analyzer.java | 61 +++--- .../sis/internal/sql/feature/ColumnAdapter.java | 22 ++- .../apache/sis/internal/sql/feature/Database.java | 30 ++- .../sis/internal/sql/feature/DialectMapping.java | 21 +- .../sis/internal/sql/feature/EWKBReader.java | 68 +++++-- .../sql/feature/GeometryIdentification.java | 212 +++++++++++++++++++++ .../internal/sql/feature/GeometryIdentifier.java | 97 ---------- .../sis/internal/sql/feature/PostGISMapping.java | 147 +++++++++++++- .../sis/internal/sql/feature/QueryFeatureSet.java | 30 +-- .../sis/internal/sql/feature/SQLCloseable.java | 8 + .../apache/sis/internal/sql/feature/SQLColumn.java | 82 ++++---- .../sis/internal/sql/feature/SpatialFunctions.java | 18 +- .../apache/sis/internal/sql/feature/StreamSQL.java | 5 +- .../org/apache/sis/internal/sql/feature/Table.java | 2 +- 16 files changed, 583 insertions(+), 239 deletions(-) diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java index 943df9e..76bfec0 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java @@ -184,7 +184,7 @@ public class ANSIInterpreter implements FilterVisitor, ExpressionVisitor { public Object visit(BBOX filter, Object extraData) { // TODO: This is a wrong interpretation, but sqlmm has no equivalent of filter encoding bbox, so we'll // fallback on a standard intersection. However, PostGIS, H2, etc. have their own versions of such filters. - return function("ST_Intersects(", filter, extraData); + return function("ST_Intersects", filter, extraData); } @Override diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java index f430e40..ef6403b 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java @@ -14,8 +14,6 @@ import java.time.OffsetTime; import java.time.ZoneOffset; import java.util.Optional; -import org.apache.sis.internal.metadata.sql.Dialect; - public class ANSIMapping implements DialectMapping { /** @@ -30,17 +28,20 @@ public class ANSIMapping implements DialectMapping { } @Override - public Dialect getDialect() { - return Dialect.ANSI; + public Spi getSpi() { + return null; } @Override - public Optional<ColumnAdapter<?>> getMapping(int sqlType, String sqlTypeName) { - return Optional.ofNullable(getMappingImpl(sqlType, sqlTypeName)); + public void close() throws SQLException {} + + @Override + public Optional<ColumnAdapter<?>> getMapping(SQLColumn columnDefinition) { + return Optional.ofNullable(getMappingImpl(columnDefinition)); } - public ColumnAdapter<?> getMappingImpl(int sqlType, String sqlTypeName) { - switch (sqlType) { + public ColumnAdapter<?> getMappingImpl(SQLColumn columnDefinition) { + switch (columnDefinition.type) { case Types.BIT: case Types.BOOLEAN: return forceCast(Boolean.class); case Types.TINYINT: if (!isByteUnsigned) return forceCast(Byte.class); // else fallthrough. diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java index 4a9cdd1..9878a5e 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java @@ -16,6 +16,7 @@ */ package org.apache.sis.internal.sql.feature; +import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -27,8 +28,6 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import javax.sql.DataSource; -import org.opengis.feature.Feature; -import org.opengis.feature.PropertyType; import org.opengis.util.GenericName; import org.opengis.util.NameFactory; import org.opengis.util.NameSpace; @@ -52,6 +51,8 @@ import org.apache.sis.util.iso.Names; import org.apache.sis.util.logging.WarningListeners; import org.apache.sis.util.resources.ResourceInternationalString; +import static org.apache.sis.util.ArgumentChecks.ensureNonNull; + /** * Helper methods for creating {@code FeatureType}s from database structure. @@ -60,6 +61,7 @@ import org.apache.sis.util.resources.ResourceInternationalString; * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) + * @author Alexis Manin (Geomatys) * @version 1.0 * @since 1.0 * @module @@ -73,6 +75,11 @@ final class Analyzer { final DataSource source; /** + * A connection used all along this component life to query database. + */ + final Connection connection; + + /** * Information about the database as a whole. * Used for fetching tables, columns, primary keys <i>etc.</i> */ @@ -147,20 +154,24 @@ final class Analyzer { * Creates a new analyzer for the database described by given metadata. * * @param source the data source, usually given by user at {@code SQLStore} creation time. - * @param metadata Value of {@code source.getConnection().getMetaData()}. + * @param databaseConnection Database entrypoint. It's the caller responsability to handle connection lifecycle, + * and ensure this object life span is shorter than the connection one. * @param listeners Value of {@code SQLStore.listeners}. * @param locale Value of {@code SQLStore.getLocale()}. */ - Analyzer(final DataSource source, final DatabaseMetaData metadata, final WarningListeners<DataStore> listeners, + Analyzer(final DataSource source, final Connection databaseConnection, final WarningListeners<DataStore> listeners, final Locale locale) throws SQLException { + ensureNonNull("Database connection provider", source); + ensureNonNull("Database connection", databaseConnection); this.source = source; - this.metadata = metadata; + this.connection = databaseConnection; + this.metadata = databaseConnection.getMetaData(); this.listeners = listeners; this.locale = locale; this.strings = new HashMap<>(); this.escape = metadata.getSearchStringEscape(); - this.functions = new SpatialFunctions(metadata); + this.functions = new SpatialFunctions(databaseConnection, metadata); this.nameFactory = DefaultFactories.forBuildin(NameFactory.class); /* * The following tables are defined by ISO 19125 / OGC Simple feature access part 2. @@ -305,10 +316,6 @@ final class Analyzer { warnings.add(Resources.formatInternational(key, argument)); } - private PropertyAdapter analyze(SQLColumn target) { - throw new UnsupportedOperationException(); - } - /** * Invoked after we finished to create all tables. This method flush the warnings * (omitting duplicated warnings), then returns all tables including dependencies. @@ -345,10 +352,10 @@ final class Analyzer { int i = 0; for (SQLColumn col : spec.getColumns()) { i++; - final ColumnAdapter<?> colAdapter = functions.toJavaType(col.getType(), col.getTypeName()); + final ColumnAdapter<?> colAdapter = functions.toJavaType(col); Class<?> type = colAdapter.javaType; - final String colName = col.getName().getColumnName(); - final String attrName = col.getName().getAttributeName(); + final String colName = col.naming.getColumnName(); + final String attrName = col.naming.getAttributeName(); if (type == null) { warning(Resources.Keys.UnknownType_1, colName); type = Object.class; @@ -357,14 +364,13 @@ final class Analyzer { final AttributeTypeBuilder<?> attribute = builder .addAttribute(type) .setName(attrName); - if (col.isNullable()) attribute.setMinimumOccurs(0); - final int precision = col.getPrecision(); - /* TODO: we should check column type. Precision for numbers or blobs is meaningfull, but the convention + if (col.isNullable) attribute.setMinimumOccurs(0); + /* TODO: we should check column type. Precision for numbers or blobs is meaningful, but the convention * exposed by SIS does not allow to distinguish such cases. */ - if (precision > 0) attribute.setMaximalLength(precision); + if (col.precision > 0) attribute.setMaximalLength(col.precision); - col.getCrs().ifPresent(attribute::setCRS); + colAdapter.getCrs().ifPresent(attribute::setCRS); if (geomCol.equals(attrName)) attribute.addRole(AttributeRole.DEFAULT_GEOMETRY); if (pkCols.contains(colName)) attribute.addRole(AttributeRole.IDENTIFIER_COMPONENT); @@ -422,11 +428,10 @@ final class Analyzer { } } - private interface PropertyAdapter { - PropertyType getType(); - void fill(ResultSet source, final Feature target); - } - + /** + * TODO: this object needs a live connection. Check if we should parse all information at built, to avoid requiring + * keeping a connection all along. + */ private final class TableMetadata implements SQLTypeSpecification { final TableReference id; private final String tableEsc; @@ -448,7 +453,6 @@ final class Analyzer { final List<String> cols = new ArrayList<>(); while (reflect.next()) { cols.add(getUniqueString(reflect, Reflection.COLUMN_NAME)); - // The actual Boolean value will be fetched in the loop on columns later. } pk = PrimaryKey.create(cols); } @@ -577,12 +581,18 @@ final class Analyzer { final ArrayList<SQLColumn> tmpCols = new ArrayList<>(total); for (int i = 1 ; i <= total ; i++) { + final TableReference optTable; + final String table = meta.getTableName(i); + if (table != null) { + optTable = new TableReference(meta.getCatalogName(i), meta.getSchemaName(i), table, null); + } else optTable = null; tmpCols.add(new SQLColumn( meta.getColumnType(i), meta.getColumnTypeName(i), meta.isNullable(i) == ResultSetMetaData.columnNullable, new ColumnRef(meta.getColumnName(i)).as(meta.getColumnLabel(i)), - meta.getPrecision(i) + meta.getPrecision(i), + optTable )); } @@ -619,5 +629,4 @@ final class Analyzer { return Collections.EMPTY_LIST; } } - } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java index 8fdd3a5..da13ae4 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java @@ -2,8 +2,11 @@ package org.apache.sis.internal.sql.feature; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Optional; import java.util.function.Function; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + import static org.apache.sis.util.ArgumentChecks.ensureNonNull; /** @@ -12,15 +15,21 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull; * * @param <T> Type of object decoded from cell. */ -class ColumnAdapter<T> implements SQLBiFunction<ResultSet, Integer, T> { +public class ColumnAdapter<T> implements SQLBiFunction<ResultSet, Integer, T> { final Class<T> javaType; private final SQLBiFunction<ResultSet, Integer, T> fetchValue; + private final CoordinateReferenceSystem crs; + + public ColumnAdapter(Class<T> javaType, SQLBiFunction<ResultSet, Integer, T> fetchValue) { + this(javaType, fetchValue, null); + } - protected ColumnAdapter(Class<T> javaType, SQLBiFunction<ResultSet, Integer, T> fetchValue) { + public ColumnAdapter(Class<T> javaType, SQLBiFunction<ResultSet, Integer, T> fetchValue, final CoordinateReferenceSystem crs) { ensureNonNull("Result java type", javaType); ensureNonNull("Function for value retrieval", fetchValue); this.javaType = javaType; this.fetchValue = fetchValue; + this.crs = crs; } @Override @@ -32,4 +41,13 @@ class ColumnAdapter<T> implements SQLBiFunction<ResultSet, Integer, T> { public <V> SQLBiFunction<ResultSet, Integer, V> andThen(Function<? super T, ? extends V> after) { return fetchValue.andThen(after); } + + /** + * Note : This method could be used not only for geometric fields, but also on numeric ones representing 1D systems. + * + * @return Potentially an empty shell, or the default coordinate reference system for this column values. + */ + public Optional<CoordinateReferenceSystem> getCrs() { + return Optional.ofNullable(crs); + } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java index 3f04da4..0985cfb 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java @@ -112,7 +112,7 @@ public final class Database { final GenericName[] tableNames, final WarningListeners<DataStore> listeners) throws SQLException, DataStoreException { - final Analyzer analyzer = new Analyzer(source, connection.getMetaData(), listeners, store.getLocale()); + final Analyzer analyzer = new Analyzer(source, connection, listeners, store.getLocale()); final String[] tableTypes = getTableTypes(analyzer.metadata); final Set<TableReference> declared = new LinkedHashSet<>(); for (final GenericName tableName : tableNames) { @@ -233,4 +233,32 @@ public final class Database { public String toString() { return TableReference.toString(this, (n) -> appendTo(n)); } + + /** + * Acquire a connection over parent database, forcing a few parameters to ensure optimal read performance and + * limiting user rights : + * <ul> + * <li>{@link Connection#setAutoCommit(boolean) auto-commit} to false</li> + * <li>{@link Connection#setReadOnly(boolean) querying read-only}</li> + * </ul> + * + * @param source Database pointer to create connection from. + * @return A new connection to database, with deactivated auto-commit. + * @throws SQLException If we cannot create a new connection. See {@link DataSource#getConnection()} for details. + */ + public static Connection connectReadOnly(final DataSource source) throws SQLException { + final Connection c = source.getConnection(); + try { + c.setAutoCommit(false); + c.setReadOnly(true); + } catch (SQLException e) { + try { + c.close(); + } catch (RuntimeException | SQLException bis) { + e.addSuppressed(bis); + } + throw e; + } + return c; + } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/DialectMapping.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/DialectMapping.java index ce6e403..33d0266 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/DialectMapping.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/DialectMapping.java @@ -1,12 +1,27 @@ package org.apache.sis.internal.sql.feature; +import java.sql.Connection; +import java.sql.SQLException; import java.util.Optional; import org.apache.sis.internal.metadata.sql.Dialect; -public interface DialectMapping { +public interface DialectMapping extends SQLCloseable { - Dialect getDialect(); + Spi getSpi(); - Optional<ColumnAdapter<?>> getMapping(final int sqlType, final String sqlTypeName); + Optional<ColumnAdapter<?>> getMapping(final SQLColumn columnDefinition); + + interface Spi { + /** + * + * @param c The connection to use to connect to the database. It will be read-only. + * @return A component compatible with database of given connection, or nothing if the database is not supported + * by this component. + * @throws SQLException If an error occurs while fetching information from database. + */ + Optional<DialectMapping> create(final Connection c) throws SQLException; + + Dialect getDialect(); + } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java index 1681034..926dbc5 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java @@ -4,18 +4,24 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + import org.apache.sis.internal.feature.Geometries; import org.apache.sis.math.Vector; import org.apache.sis.setup.GeometryLibrary; +import static java.lang.Character.digit; +import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty; + /** - * PostGIS Hexa-EWKB Geometry reader/write classes. + * PostGIS EWKB Geometry reader/write classes. * http://postgis.net/docs/using_postgis_dbmanagement.html#EWKB_EWKT * * This format is the natural form returned by a query selection a geometry field * whithout using any ST_X method. * * @author Johann Sorel (Geomatys) + * @author Alexis Manin (Geomatys) */ class EWKBReader { @@ -35,19 +41,34 @@ class EWKBReader { private final Geometries factory; + private CoordinateReferenceSystem crs; + EWKBReader() { - this(null); + this((GeometryLibrary) null); } EWKBReader(GeometryLibrary library) { - this.factory = Geometries.implementation(library); + this(Geometries.implementation(library)); + } + + EWKBReader(Geometries geometryFactory) { + this.factory = geometryFactory; + } + + public EWKBReader setCrs(CoordinateReferenceSystem defaultCrs) { + this.crs = defaultCrs; + return this; + } + + Object readHexa(final String hexaEWkb) { + return read(decodeHex(hexaEWkb)); } Object read(final byte[] eWkb) { return new Reader(ByteBuffer.wrap(eWkb)).read(); } - private class Reader { + private final class Reader { final ByteBuffer buffer; final int geomType; final int dimension; @@ -55,23 +76,26 @@ class EWKBReader { private Reader(ByteBuffer buffer) { final byte endianess = buffer.get(); - if (isBigEndian(endianess)) { - this.buffer = buffer.order(ByteOrder.BIG_ENDIAN); + if (isLittleEndian(endianess)) { + this.buffer = buffer.order(ByteOrder.LITTLE_ENDIAN); } else this.buffer = buffer; final int flags = buffer.getInt(); final boolean flagZ = (flags & MASK_Z) != 0; final boolean flagM = (flags & MASK_M) != 0; final boolean flagSRID = (flags & MASK_SRID) != 0; geomType = (flags & MASK_GEOMTYPE); - dimension = 2 + ((flagZ)?1:0) + ((flagM)?1:0); + dimension = 2 + ((flagZ) ? 1 : 0) + ((flagM) ? 1 : 0); srid = flagSRID ? buffer.getInt() : 0; } Object read() { final Object geom = decodeGeometry(); - if (srid > 0) { - // TODO: set CRS + + // TODO: set CRS + if (crs != null) { + } else if (srid > 0) { } + return geom; } @@ -145,7 +169,29 @@ class EWKBReader { } - private static boolean isBigEndian(byte endianess) { - return endianess == 0; // org.postgis.binary.ValueGetter.XDR.NUMBER + private static boolean isLittleEndian(byte endianess) { + return endianess == 1; // org.postgis.binary.ValueGetter.NDR.NUMBER + } + + /** + * Convert a text representing an hexadecimal set of values (no separator, each 2 characters form a value). + * + * @param hexa The hexadecimal values to decode. Should neither be null nor empty. + * @return Real values encoded by input hexadecimal text. Never null, never empty. + */ + static byte[] decodeHex(String hexa) { + ensureNonEmpty("Hexadecimal text", hexa); + int len = hexa.length(); + // Handle odd length by considering last character as a lone value + byte[] data = new byte[(len+1) / 2]; + int limit = (len % 2 == 0) ? len : len - 1; + for (int i = 0, j=0 ; i < limit ; ) { + data[j++] = (byte) ((digit(hexa.charAt(i++), 16) << 4) + + digit(hexa.charAt(i++), 16)); + } + + if (limit < len) data[data.length - 1] = (byte) digit(hexa.charAt(limit), 16); + + return data; } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentification.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentification.java new file mode 100644 index 0000000..a10932c --- /dev/null +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentification.java @@ -0,0 +1,212 @@ +package org.apache.sis.internal.sql.feature; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.text.ParseException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.util.FactoryException; + +import org.apache.sis.io.wkt.Convention; +import org.apache.sis.io.wkt.WKTFormat; +import org.apache.sis.referencing.CRS; + +import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty; + +/** + * Not THREAD-SAFE ! + * + * @implNote <a href="https://www.jooq.org/doc/3.12/manual/sql-execution/fetching/pojos/#N5EFC1">I miss JOOQ...</a> + */ +class GeometryIdentification implements SQLCloseable { + + private final Connection c; + final PreparedStatement identifySchemaQuery; + /** + * A statement serving two purposes: + * <ol> + * <li>Searching all available geometric columns of a specified table</li> + * <li>Fetching geometric information for a specific column</li> + * </ol> + * + * @implNote The statement definition is able to serve both purposes by changing geometry column filter to no-op. + */ + final PreparedStatement columnQuery; + final PreparedStatement wktFromSrid; + + private final WKTFormat wktReader; + + /** + * Describes if geometry column registry include a column for geometry types, according that one can apparently omit + * it (see Simple_feature_access_-_Part_2_SQL_option_v1.2.1, section 6.2: Architecture - SQL implementation using + * Geometry Types). + */ + final boolean typeIncluded; + + public GeometryIdentification(Connection c) throws SQLException { + this(c, "geometry_columns", "f_geometry_column", "geometry_type"); + } + public GeometryIdentification(Connection c, String identificationTable, String geometryColumnName, String typeColumnName) throws SQLException { + this.c = c; + typeIncluded = typeColumnName != null && !(typeColumnName=typeColumnName.trim()).isEmpty(); + identifySchemaQuery = c.prepareStatement("SELECT DISTINCT(f_table_schema) FROM "+identificationTable+" WHERE f_table_name = ?"); + columnQuery = c.prepareStatement( + "SELECT "+geometryColumnName+", coord_dimension, srid" + (typeIncluded ? ", "+typeColumnName : "") + ' ' + + "FROM "+identificationTable+" " + + "WHERE f_table_schema LIKE ? " + + "AND f_table_name = ? " + + "AND "+geometryColumnName+" LIKE ?" + ); + wktFromSrid = c.prepareStatement("SELECT auth_name, auth_srid, srtext FROM spatial_ref_sys WHERE srid=?"); + wktReader = new WKTFormat(null, null); + wktReader.setConvention(Convention.WKT1_COMMON_UNITS); + } + + Set<GeometryColumn> fetchGeometricColumns(String schema, final String table) throws SQLException, ParseException { + ensureNonEmpty("Table name", table); + if (schema == null || (schema = schema.trim()).isEmpty()) { + // To avoid ambiguity, we have to restrict search to a single schema + identifySchemaQuery.setString(1, table); + try (ResultSet result = identifySchemaQuery.executeQuery()) { + if (!result.next()) return Collections.EMPTY_SET; + schema = result.getString(1); + if (result.next()) throw new IllegalArgumentException("Multiple tables match given name. Please specify schema to remove all ambiguities"); + } + } + + columnQuery.setString(1, schema); + columnQuery.setString(2, table); + columnQuery.setString(3, "%"); + try (ResultSet result = columnQuery.executeQuery()) { + final HashSet<GeometryColumn> cols = new HashSet<>(); + while (result.next()) { + cols.add(create(result)); + } + return cols; + } finally { + columnQuery.clearParameters(); + } + } + + Optional<GeometryColumn> fetch(SQLColumn target) throws SQLException, ParseException { + if (target == null || target.origin == null) return Optional.empty(); + + String schema = target.origin.schema; + if (schema == null || (schema = schema.trim()).isEmpty()) schema = "%"; + columnQuery.setString(1, schema); + columnQuery.setString(2, target.origin.table); + columnQuery.setString(3, target.naming.getColumnName()); + + try (ResultSet result = columnQuery.executeQuery()) { + if (result.next()) return Optional.of(create(result)); + } finally { + columnQuery.clearParameters(); + } + return Optional.empty(); + } + + private GeometryColumn create(final ResultSet cursor) throws SQLException, ParseException { + final String name = cursor.getString(1); + final int dimension = cursor.getInt(2); + final int pgSrid = cursor.getInt(3); + final String type = cursor.getString(4); + // Note: we make a query per entry, which could impact performance. However, 99% of defined tables + // will have only one geometry column. Moreover, even with more than one, with prepared statement, the + // performance impact should stay low. + final CoordinateReferenceSystem crs = fetchCrs(pgSrid); + return new GeometryColumn(name, dimension, pgSrid, type, crs); + } + + /** + * Try to fetch spatial system relative to given SRID. + * @param pgSrid The SRID as defined by the database (see + * <a href="http://postgis.refractions.net/documentation/manual-1.3/ch04.html#id2571265">Official PostGIS documentation</a> for details). + * @return If input was 0 or less, a null value is returned. Otherwise, return the CRS decoded from database WKT. + * @throws IllegalArgumentException If given SRID is above 0, but no coordinate system definition can be found for + * it in the database, or found object is not a database, or no WKT is available, but authority code is not + * supported by SIS. + * @throws IllegalStateException If more than one match is found for given SRID. + */ + private CoordinateReferenceSystem fetchCrs(int pgSrid) throws SQLException, IllegalArgumentException, ParseException { + if (pgSrid <= 0) return null; + + wktFromSrid.setInt(1, pgSrid); + try (ResultSet result = wktFromSrid.executeQuery()) { + if (!result.next()) throw new IllegalArgumentException("No entry found for SRID "+pgSrid); + final String authority = result.getString(1); + final int authorityCode = result.getInt(2); + final String pgWkt = result.getString(3); + + // That should never happen, but if it does, there's a serious problem ! + if (result.next()) throw new IllegalStateException("More than one definition available for SRID "+pgSrid); + + if (pgWkt == null || pgWkt.trim().isEmpty()) { + try { + return CRS.getAuthorityFactory(authority).createCoordinateReferenceSystem(Integer.toString(authorityCode)); + } catch (FactoryException e) { + throw new IllegalArgumentException(String.format( + "Input SRID (%d) does not provide any WKT, but its authority code (%s:%d) is not supported by SIS", + pgSrid, authority, authorityCode + ), e); + } + } + final Object parsedWkt = wktReader.parseObject(pgWkt); + if (parsedWkt instanceof CoordinateReferenceSystem) { + return (CoordinateReferenceSystem) parsedWkt; + } else throw new ParseException(String.format( + "WKT of given SRID cannot be interprated as a CRS.%nInput SRID: %d%nOutput type: %s", + pgSrid, parsedWkt.getClass().getCanonicalName() + ), 0); + } finally { + wktFromSrid.clearParameters(); + } + } + + @Override + public void close() throws SQLException { + try ( + SQLCloseable c1 = columnQuery::close; + SQLCloseable c2 = identifySchemaQuery::close; + SQLCloseable c3 = wktFromSrid::close; + ) {} + } + + static final class GeometryColumn { + final String name; + final int dimension; + final int pgSrid; + final String type; + + final CoordinateReferenceSystem crs; + + private GeometryColumn(String name, int dimension, int srid, final String type, CoordinateReferenceSystem crs) { + this.name = name; + this.dimension = dimension; + this.pgSrid = srid; + this.crs = crs; + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GeometryColumn that = (GeometryColumn) o; + return dimension == that.dimension && + pgSrid == that.pgSrid && + name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentifier.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentifier.java deleted file mode 100644 index cd0cdc0..0000000 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentifier.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.apache.sis.internal.sql.feature; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -import org.apache.sis.internal.metadata.sql.SQLBuilder; - -import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty; - -/** - * Not THREAD-SAFE ! - */ -class GeometryIdentifier implements AutoCloseable { - - private final Connection c; - final PreparedStatement identifySchemaQuery; - final PreparedStatement columnQuery; - - /** - * Prefetch an SQL builder, so subsequent calls will use a copy constructor to avoid re-analyzing database metadata. - */ - private final SQLBuilder template; - - public GeometryIdentifier(Connection c, boolean quoteSchema) throws SQLException { - this.c = c; - template = new SQLBuilder(c.getMetaData(), quoteSchema); - identifySchemaQuery = c.prepareStatement("SELECT DISTINCT(f_table_schema) FROM GEOMETRY_COLUMNS WHERE f_table_name = ?"); - columnQuery = c.prepareStatement("SELECT f_geometry_column, coord_dimension, srid FROM GEOMETRY_COLUMNS WHERE f_schema_name = ? AND table = ?"); - } - - Set<GeometryColumn> fetchGeometricColumns(String schema, final String table) throws SQLException { - final SQLBuilder builder = new SQLBuilder(template); - ensureNonEmpty("Table name", table); - if (schema == null || (schema = schema.trim()).isEmpty()) { - // To avoid ambiguity, we have to restrict search to a single schema - identifySchemaQuery.setString(1, table); - try (ResultSet result = identifySchemaQuery.executeQuery()) { - if (!result.next()) return Collections.EMPTY_SET; - schema = result.getString(1); - if (result.next()) throw new IllegalArgumentException("Multiple tables match given name. Please specify schema to remove all ambiguities"); - } - } - - columnQuery.setString(1, schema); - columnQuery.setString(2, table); - try (ResultSet result = columnQuery.executeQuery()) { - final HashSet<GeometryColumn> cols = new HashSet<>(); - while (result.next()) { - cols.add(new GeometryColumn(result.getString(1), result.getInt(2), result.getInt(3))); - } - return cols; - } - } - - @Override - public void close() throws SQLException { - try (SQLCloseable c1 = columnQuery::close; SQLCloseable c2 = identifySchemaQuery::close;) {} - } - - private interface SQLCloseable extends AutoCloseable { - @Override - void close() throws SQLException; - } - - static final class GeometryColumn { - final String name; - final int dimension; - final int srid; - - GeometryColumn(String name, int dimension, int srid) { - this.name = name; - this.dimension = dimension; - this.srid = srid; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - GeometryColumn that = (GeometryColumn) o; - return dimension == that.dimension && - srid == that.srid && - name.equals(that.name); - } - - @Override - public int hashCode() { - return Objects.hash(name); - } - } -} diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java index 9c9303d..84144c8 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java @@ -1,29 +1,160 @@ package org.apache.sis.internal.sql.feature; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.Types; +import java.text.ParseException; import java.util.Optional; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +import org.apache.sis.internal.feature.Geometries; import org.apache.sis.internal.metadata.sql.Dialect; +import org.apache.sis.util.collection.BackingStoreException; + +public final class PostGISMapping implements DialectMapping { + + final PostGISMapping.Spi spi; + final GeometryIdentification identifyGeometries; + final GeometryIdentification identifyGeographies; + + final Connection connection; + + final Geometries<?> library; + + private PostGISMapping(final PostGISMapping.Spi spi, Connection c) throws SQLException { + connection = c; + this.spi = spi; + this.identifyGeometries = new GeometryIdentification(c, "geometry_columns", "f_geometry_column", "type"); + this.identifyGeographies = new GeometryIdentification(c, "geography_columns", "f_geography_column", "type"); + + this.library = Geometries.implementation(null); + } -public class PostGISMapping implements DialectMapping { @Override - public Dialect getDialect() { - return Dialect.POSTGRESQL; + public Spi getSpi() { + return spi; } @Override - public Optional<ColumnAdapter<?>> getMapping(int sqlType, String sqlTypeName) { - switch (sqlType) { - case (Types.OTHER): + public Optional<ColumnAdapter<?>> getMapping(SQLColumn definition) { + switch (definition.type) { + case (Types.OTHER): return Optional.ofNullable(forOther(definition)); } return Optional.empty(); } - private ColumnAdapter<?> forOther(String sqlTypeName) { - switch (sqlTypeName.toLowerCase()) { + private ColumnAdapter<?> forOther(SQLColumn definition) { + switch (definition.typeName.trim().toLowerCase()) { case "geometry": + return forGeometry(definition, identifyGeometries); case "geography": + return forGeometry(definition, identifyGeographies); default: return null; } } + + private ColumnAdapter<?> forGeometry(SQLColumn definition, GeometryIdentification ident) { + // In case of a computed column, geometric definition could be null. + final GeometryIdentification.GeometryColumn geomDef; + try { + geomDef = ident.fetch(definition).orElse(null); + } catch (SQLException | ParseException e) { + throw new BackingStoreException(e); + } + String geometryType = geomDef == null ? null : geomDef.type; + final Class geomClass = getGeometricClass(geometryType); + SQLBiFunction<ResultSet, Integer, Object> geometryDecoder; + // TODO: activate optimisation : WKB is lighter, but we need to modify user query, and to know CRS in advance. +// if (geomDef.crs == null) { + geometryDecoder = new HexEWKBReader(); +// } else { +// // For optimisation purpose, we'll return directly a WKB reader if CRS is known in advance. +// geometryDecoder = new WKBReader(geomDef.crs); +// } + + return new ColumnAdapter<>(geomClass, geometryDecoder, geomDef == null ? null : geomDef.crs); + } + + private Class getGeometricClass(String geometryType) { + if (geometryType == null) return library.rootClass; + + // remove Z, M or ZM suffix + if (geometryType.endsWith("M")) geometryType = geometryType.substring(0, geometryType.length()-1); + if (geometryType.endsWith("Z")) geometryType = geometryType.substring(0, geometryType.length()-1); + + final Class geomClass; + switch (geometryType) { + case "POINT": + geomClass = library.pointClass; + break; + case "LINESTRING": + geomClass = library.polylineClass; + break; + case "POLYGON": + geomClass = library.polygonClass; + break; + default: geomClass = library.rootClass; + } + return geomClass; + } + + @Override + public void close() throws SQLException { + identifyGeometries.close(); + } + + public static final class Spi implements DialectMapping.Spi { + + @Override + public Optional<DialectMapping> create(Connection c) throws SQLException { + return Optional.of(new PostGISMapping(this, c)); + } + + @Override + public Dialect getDialect() { + return Dialect.POSTGRESQL; + } + } + + private final class WKBReader implements SQLBiFunction<ResultSet, Integer, Object> { + + final CoordinateReferenceSystem crsToApply; + + private WKBReader(CoordinateReferenceSystem crsToApply) { + this.crsToApply = crsToApply; + } + + @Override + public Object apply(ResultSet resultSet, Integer integer) throws SQLException { + final byte[] bytes = resultSet.getBytes(integer); + if (bytes == null) return null; + final Object value = library.parseWKB(bytes); + if (value != null) { + // TODO: set CRS + } + + return value; + } + } + + private final class HexEWKBReader implements SQLBiFunction<ResultSet, Integer, Object> { + + final EWKBReader reader; + + private HexEWKBReader() { + this(null); + } + + private HexEWKBReader(CoordinateReferenceSystem crsToApply) { + reader = new EWKBReader(library).setCrs(crsToApply); + } + + @Override + public Object apply(ResultSet resultSet, Integer integer) throws SQLException { + final String hexa = resultSet.getString(integer); + return hexa == null ? null : reader.readHexa(hexa); + } + } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java index a117b13..7b94f2f 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java @@ -123,7 +123,7 @@ public class QueryFeatureSet extends AbstractFeatureSet { * @throws SQLException If input query compiling or analysis of its metadata fails. */ public QueryFeatureSet(SQLBuilder queryBuilder, DataSource source, Connection conn) throws SQLException { - this(queryBuilder, new Analyzer(source, conn.getMetaData(), null, null), source, conn); + this(queryBuilder, new Analyzer(source, conn, null, null), source, conn); } @@ -195,34 +195,6 @@ public class QueryFeatureSet extends AbstractFeatureSet { return super.subset(query); } - /** - * Acquire a connection over parent database, forcing a few parameters to ensure optimal read performance and - * limiting user rights : - * <ul> - * <li>{@link Connection#setAutoCommit(boolean) auto-commit} to false</li> - * <li>{@link Connection#setReadOnly(boolean) querying read-only}</li> - * </ul> - * - * @param source Database pointer to create connection from. - * @return A new connection to database, with deactivated auto-commit. - * @throws SQLException If we cannot create a new connection. See {@link DataSource#getConnection()} for details. - */ - public static Connection connectReadOnly(final DataSource source) throws SQLException { - final Connection c = source.getConnection(); - try { - c.setAutoCommit(false); - c.setReadOnly(true); - } catch (SQLException e) { - try { - c.close(); - } catch (RuntimeException | SQLException bis) { - e.addSuppressed(bis); - } - throw e; - } - return c; - } - class SubsetAdapter extends SQLQueryAdapter { SubsetAdapter() { diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLCloseable.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLCloseable.java new file mode 100644 index 0000000..439b4f1 --- /dev/null +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLCloseable.java @@ -0,0 +1,8 @@ +package org.apache.sis.internal.sql.feature; + +import java.sql.SQLException; + +public interface SQLCloseable extends AutoCloseable { + @Override + void close() throws SQLException; +} diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLColumn.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLColumn.java index d92fe8c..68e6382 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLColumn.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLColumn.java @@ -1,58 +1,58 @@ package org.apache.sis.internal.sql.feature; +import java.sql.DatabaseMetaData; import java.sql.ResultSetMetaData; -import java.util.Optional; +import java.sql.Types; -import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.internal.metadata.sql.Reflection; +/** + * A simple POJO to hold information about an SQL column. This mainly represents information extracted from + * {@link DatabaseMetaData#getColumns(String, String, String, String) database metadata}. + * Note that for now, only a few selected information are represented. If needed, new fields could be added if needed. + * The aim is to describe as well as possible all SQL related information about a column, to allow mapping to feature + * model as accurate as possible. + */ class SQLColumn { + + /** + * Value type as specified in {@link Types} + */ final int type; + /** + * A name for the value type, free-text from the database engine. For more information about this, please see + * {@link DatabaseMetaData#getColumns(String, String, String, String)} and {@link Reflection#TYPE_NAME}. + */ final String typeName; - private final boolean isNullable; - private final ColumnRef naming; - private final int precision; - - SQLColumn(int type, String typeName, boolean isNullable, ColumnRef naming, int precision) { - this.type = type; - this.typeName = typeName; - this.isNullable = isNullable; - this.naming = naming; - this.precision = precision; - } - - public ColumnRef getName() { - return naming; - } - - public int getType() { - return type; - } - - public String getTypeName() { - return typeName; - } + final boolean isNullable; - public boolean isNullable() { - return isNullable; - } + /** + * Name of the column, optionally with an alias, in case of a query analysis. + */ + final ColumnRef naming; /** - * Same as {@link ResultSetMetaData#getPrecision(int)}. - * @return 0 if unknown. For texts, maximum number of characters allowed. For numerics, max precision. For blobs, - * number of bytes allowed. + * Same as {@link ResultSetMetaData#getPrecision(int)}. It will be 0 if unknown. For texts, it represents maximum + * number of characters allowed. For numbers, its maximum precision. For blobs, a limit in allowed number of bytes. */ - public int getPrecision() { - return precision; - } + final int precision; /** - * TODO: implement. - * Note : This method could be used not only for geometric fields, but also on numeric ones representing 1D - * systems. - * - * @return null for now, implementation needed. + * Optional. The table that contains this column. It could be null in case this column specification is done from + * query analysis. */ - public Optional<CoordinateReferenceSystem> getCrs() { - return Optional.empty(); + final TableReference origin; + + SQLColumn(int type, String typeName, boolean isNullable, ColumnRef naming, int precision) { + this(type, typeName, isNullable, naming, precision, null); + } + + SQLColumn(int type, String typeName, boolean isNullable, ColumnRef naming, int precision, TableReference origin) { + this.type = type; + this.typeName = typeName; + this.isNullable = isNullable; + this.naming = naming; + this.precision = precision; + this.origin = origin; } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java index ca5ff05..01b59b8 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java @@ -16,6 +16,7 @@ */ package org.apache.sis.internal.sql.feature; +import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; @@ -60,7 +61,7 @@ class SpatialFunctions { /** * Creates a new accessor to geospatial functions for the database described by given metadata. */ - SpatialFunctions(final DatabaseMetaData metadata) throws SQLException { + SpatialFunctions(final Connection c, final DatabaseMetaData metadata) throws SQLException { /* * Get information about whether byte are unsigned. * According JDBC specification, the rows shall be ordered by DATA_TYPE. @@ -83,7 +84,7 @@ class SpatialFunctions { library = null; final Dialect dialect = Dialect.guess(metadata); - specificMapping = forDialect(dialect); + specificMapping = forDialect(dialect, c); defaultMapping = new ANSIMapping(isByteUnsigned); } @@ -96,14 +97,13 @@ class SpatialFunctions { * <p>The default implementation handles the types declared in {@link Types} class. * Subclasses should handle the geometry types declared by spatial extensions.</p> * - * @param sqlType SQL type code as one of {@link java.sql.Types} constants. - * @param sqlTypeName data source dependent type name. For User Defined Type (UDT) the name is fully qualified. + * @param columnDefinition Definition of source database column, including its SQL type and type name. * @return corresponding java type, or {@code null} if unknown. */ @SuppressWarnings("fallthrough") - protected ColumnAdapter<?> toJavaType(final int sqlType, final String sqlTypeName) { - return specificMapping.flatMap(dialect -> dialect.getMapping(sqlType, sqlTypeName)) - .orElseGet(() -> defaultMapping.getMappingImpl(sqlType, sqlTypeName)); + protected ColumnAdapter<?> toJavaType(final SQLColumn columnDefinition) { + return specificMapping.flatMap(dialect -> dialect.getMapping(columnDefinition)) + .orElseGet(() -> defaultMapping.getMappingImpl(columnDefinition)); } /** @@ -121,9 +121,9 @@ class SpatialFunctions { return null; } - static Optional<DialectMapping> forDialect(final Dialect dialect) { + static Optional<DialectMapping> forDialect(final Dialect dialect, Connection c) throws SQLException { switch (dialect) { - case POSTGRESQL: return Optional.of(new PostGISMapping()); + case POSTGRESQL: return new PostGISMapping.Spi().create(c); default: return Optional.empty(); } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/StreamSQL.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/StreamSQL.java index 7291a04..ff72c02 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/StreamSQL.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/StreamSQL.java @@ -48,6 +48,7 @@ import org.apache.sis.storage.DataStoreException; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.util.logging.Logging; +import static org.apache.sis.internal.sql.feature.Database.connectReadOnly; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; import static org.apache.sis.util.ArgumentChecks.ensurePositive; @@ -179,7 +180,7 @@ class StreamSQL extends StreamDecoration<Feature> { // If underlying connector does not support query estimation, we will fallback on brut-force counting. return super.count(); } - try (Connection conn = QueryFeatureSet.connectReadOnly(source)) { + try (Connection conn = connectReadOnly(source)) { try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) { if (rs.next()) { @@ -194,7 +195,7 @@ class StreamSQL extends StreamDecoration<Feature> { @Override protected synchronized Stream<Feature> createDecoratedStream() { final AtomicReference<Connection> connectionRef = new AtomicReference<>(); - Stream<Feature> featureStream = Stream.of(uncheck(() -> QueryFeatureSet.connectReadOnly(source))) + Stream<Feature> featureStream = Stream.of(uncheck(() -> connectReadOnly(source))) .map(Supplier::get) .peek(connectionRef::set) .flatMap(conn -> { diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java index 7205f70..22c9aba 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java @@ -200,7 +200,7 @@ final class Table extends AbstractFeatureSet { this.hasGeometry = specification.getPrimaryGeometryColumn().isPresent(); this.attributes = Collections.unmodifiableList( specification.getColumns().stream() - .map(SQLColumn::getName) + .map(column -> column.naming) .collect(Collectors.toList()) ); }
