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 eb5b678 Dispatch most of 'Database' implementation into specialized
classes (Analyzer, Table, Relation, QueriedFeatureSet).
eb5b678 is described below
commit eb5b6788d9ce29eb4838966156b39ef4370496f4
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Jul 9 18:51:21 2018 +0200
Dispatch most of 'Database' implementation into specialized classes
(Analyzer, Table, Relation, QueriedFeatureSet).
---
.../sis/internal/metadata/sql/Reflection.java | 6 +
.../apache/sis/internal/util/CollectionsExt.java | 22 +
.../resources/ResourceInternationalString.java | 2 +
storage/sis-sql/pom.xml | 6 +-
.../apache/sis/internal/sql/feature/Analyzer.java | 200 +++++++++
.../apache/sis/internal/sql/feature/Column.java | 195 ---------
.../apache/sis/internal/sql/feature/Database.java | 469 +++------------------
.../sis/internal/sql/feature/PrimaryKey.java | 103 -----
...QueryFeatureSet.java => QueriedFeatureSet.java} | 71 ++--
.../apache/sis/internal/sql/feature/Relation.java | 40 +-
.../apache/sis/internal/sql/feature/Resources.java | 46 ++
.../sis/internal/sql/feature/Resources.properties | 1 +
.../internal/sql/feature/Resources_fr.properties | 1 +
.../sis/internal/sql/feature/SpatialFunctions.java | 176 ++++----
.../org/apache/sis/internal/sql/feature/Table.java | 207 +++++++--
.../apache/sis/internal/sql/feature/TableName.java | 74 ++++
.../java/org/apache/sis/storage/sql/SQLStore.java | 4 +-
.../sis/internal/storage/AbstractFeatureSet.java | 2 +-
18 files changed, 733 insertions(+), 892 deletions(-)
diff --git
a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Reflection.java
b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Reflection.java
index cfd27ca..ffdee2e 100644
---
a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Reflection.java
+++
b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Reflection.java
@@ -93,6 +93,12 @@ public final class Reflection {
public static final String COLUMN_SIZE = "COLUMN_SIZE";
/**
+ * Whether an integer type is unsigned.
+ * Values in this column are integers ({@code boolean}) rather than {@code
String}.
+ */
+ public static final String UNSIGNED_ATTRIBUTE = "UNSIGNED_ATTRIBUTE";
+
+ /**
* The {@value} key for the nullability of a column. Possible values are
{@code "YES"} if
* the parameter can include NULLs, {@code "NO"} if the parameter cannot
include NULLs,
* and empty string if the nullability for the parameter is unknown.
diff --git
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/CollectionsExt.java
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/CollectionsExt.java
index f40905a..0a60b8e 100644
---
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/CollectionsExt.java
+++
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/CollectionsExt.java
@@ -635,6 +635,28 @@ public final class CollectionsExt extends Static {
}
/**
+ * Returns a more compact representation of the given list. This method is
similar to
+ * {@link #unmodifiableOrCopy(Collection)} except that it does not wrap
the list in an unmodifiable view.
+ * The intend is to avoid one level of indirection for performance and
memory reasons.
+ * This is okay only if the list is kept in a private field and never
escape outside that class.
+ *
+ * @param <E> the type of elements in the list.
+ * @param list the list to compact, or {@code null}.
+ * @return a unmodifiable version of the given list, or {@code null} if
the given list was null.
+ *
+ * @see #unmodifiableOrCopy(Collection)
+ */
+ public static <E> List<E> compact(final List<E> list) {
+ if (list != null) {
+ switch (list.size()) {
+ case 0: return Collections.emptyList();
+ case 1: return Collections.singletonList(list.get(0));
+ }
+ }
+ return list;
+ }
+
+ /**
* Returns a snapshot of the given list. The returned list will not be
affected by changes
* in the given list after this method call. This method makes no
guaranteed about whether
* the returned list is modifiable or not.
diff --git
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/ResourceInternationalString.java
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/ResourceInternationalString.java
index 83ebff6..02ab357 100644
---
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/ResourceInternationalString.java
+++
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/ResourceInternationalString.java
@@ -121,6 +121,8 @@ public abstract class ResourceInternationalString extends
AbstractInternationalS
/**
* Compares this international string with the specified object for
equality.
+ * Two {@code ResourceInternationalString} are considered equal if they are
+ * of the same class and have been constructed with equal arguments.
*
* @param object the object to compare with this international string.
* @return {@code true} if the given object is equal to this string.
diff --git a/storage/sis-sql/pom.xml b/storage/sis-sql/pom.xml
index 01c8bb9..e7e1cb1 100644
--- a/storage/sis-sql/pom.xml
+++ b/storage/sis-sql/pom.xml
@@ -111,9 +111,9 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>com.esri.geometry</groupId>
- <artifactId>esri-geometry-api</artifactId>
- <optional>false</optional>
+ <groupId>org.postgresql</groupId>
+ <artifactId>postgresql</artifactId>
+ <scope>test</scope>
</dependency>
</dependencies>
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
new file mode 100644
index 0000000..2a86ff0
--- /dev/null
+++
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
@@ -0,0 +1,200 @@
+/*
+ * 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.internal.sql.feature;
+
+import java.util.Set;
+import java.util.Map;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedHashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.sql.SQLException;
+import java.sql.DatabaseMetaData;
+import org.opengis.util.InternationalString;
+import org.apache.sis.internal.metadata.sql.Dialect;
+
+
+/**
+ * Helper methods for creating {@code FeatureType}s from database structure.
+ * An instance of this class is created temporarily when starting the analysis
+ * of a database structure, and discarded once the analysis is finished.
+ *
+ * @author Johann Sorel (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since 1.0
+ * @module
+ */
+final class Analyzer {
+ /**
+ * Information about the database as a whole.
+ * Used for fetching tables, columns, primary keys <i>etc.</i>
+ */
+ final DatabaseMetaData metadata;
+
+ /**
+ * Functions that may be specific to the geospatial database in use.
+ */
+ final SpatialFunctions functions;
+
+ /**
+ * The string to insert before wildcard characters ({@code '_'} or {@code
'%'}) to escape.
+ * This is used by {@link #escape(String)} before to pass argument values
(e.g. table name)
+ * to {@link DatabaseMetaData} methods expecting a pattern.
+ */
+ private final String escape;
+
+ /**
+ * Names of tables to ignore when inspecting a database schema.
+ * Those tables are used for database internal working (for example by
PostGIS).
+ */
+ private final Set<String> ignoredTables;
+
+ /**
+ * Relations to other tables found while doing introspection on a table.
+ * The value tells whether the table in the relation has already been
analyzed.
+ * Only the catalog, schema and table names are taken in account for the
keys in this map.
+ */
+ private final Map<TableName,Boolean> dependencies;
+
+ /**
+ * Iterator over {@link #dependencies} entries, or {@code null} if none.
+ * This field may be set to {@code null} in the middle of an iteration if
+ * the {@link #dependencies} map is modified concurrently.
+ */
+ private Iterator<Map.Entry<TableName,Boolean>> depIter;
+
+ /**
+ * Warnings found while analyzing a database structure. Duplicated
warnings are omitted.
+ */
+ private final Set<InternationalString> warnings;
+
+ /**
+ * Creates a new analyzer for the database described by given metadata.
+ */
+ Analyzer(final DatabaseMetaData metadata) throws SQLException {
+ this.metadata = metadata;
+ this.escape = metadata.getSearchStringEscape();
+ this.functions = new SpatialFunctions(metadata);
+ /*
+ * 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.
+ */
+ String crs = "SPATIAL_REF_SYS";
+ String geom = "GEOMETRY_COLUMNS";
+ if (metadata.storesLowerCaseIdentifiers()) {
+ crs = crs .toLowerCase(Locale.US).intern();
+ geom = geom.toLowerCase(Locale.US).intern();
+ }
+ ignoredTables = new HashSet<>(4);
+ ignoredTables.add(crs);
+ ignoredTables.add(geom);
+ final Dialect dialect = Dialect.guess(metadata);
+ if (dialect == Dialect.POSTGRESQL) {
+ ignoredTables.add("geography_columns"); // Postgis 1+
+ ignoredTables.add("raster_columns"); // Postgis 2
+ ignoredTables.add("raster_overviews");
+ }
+ /*
+ * Information to be collected during table analysis.
+ */
+ dependencies = new LinkedHashMap<>();
+ warnings = new LinkedHashSet<>();
+ }
+
+ /**
+ * Returns the given pattern with {@code '_'} and {@code '%'} characters
escaped by the database-specific
+ * escape characters. This method should be invoked for escaping the
values of all {@link DatabaseMetaData}
+ * method arguments with a name ending by {@code "Pattern"}. Note that not
all arguments are pattern; please
+ * checks carefully {@link DatabaseMetaData} javadoc for each method.
+ *
+ * <div class="note"><b>Example:</b> if a method expects an argument named
{@code tableNamePattern},
+ * then that argument value should be escaped. But if the argument name is
only {@code tableName},
+ * then the value should not be escaped.</div>
+ */
+ final String escape(final String pattern) {
+ if (pattern != null) {
+ StringBuilder buffer = null;
+ for (int i = pattern.length(); --i >= 0;) {
+ final char c = pattern.charAt(i);
+ if (c == '_' || c == '%') {
+ if (buffer == null) {
+ buffer = new StringBuilder(pattern);
+ }
+ buffer.insert(i, escape);
+ }
+ }
+ if (buffer != null) {
+ return buffer.toString();
+ }
+ }
+ return pattern;
+ }
+
+ /**
+ * Returns whether a table is reserved for database internal working.
+ * If this method returns {@code false}, then the given table is a
candidate
+ * for use as a {@code FeatureType}.
+ *
+ * @param name database table name to test (case sensitive).
+ * @return {@code true} if the named table should be ignored when looking
for feature types.
+ */
+ final boolean isIgnoredTable(final String name) {
+ return ignoredTables.contains(name);
+ }
+
+ /**
+ * Declares that a relation to a foreigner table has been found. Only the
catalog, schema and table names
+ * are taken in account. If a dependency for the same table has already
been declared before or if that
+ * table has already been analyzed, then this method does nothing.
Otherwise if the table has not yet
+ * been analyzed, then this method remembers that the foreigner table will
need to be analyzed later.
+ */
+ final void addDependency(final TableName foreigner) {
+ if (dependencies.putIfAbsent(foreigner, Boolean.FALSE) == null) {
+ depIter = null; // Will need to fetch a new iterator.
+ }
+ }
+
+ /**
+ * Returns the next table to visit, or {@code null} if there is no more.
+ */
+ final TableName nextDependency() {
+ if (depIter == null) {
+ depIter = dependencies.entrySet().iterator();
+ }
+ while (depIter.hasNext()) {
+ final Map.Entry<TableName,Boolean> e = depIter.next();
+ if (!e.setValue(Boolean.TRUE)) {
+ return e.getKey();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Reports a warning. Duplicated warnings will be ignored.
+ *
+ * @param key one of {@link Resources.Keys} values.
+ * @param argument the value to substitute to {0} tag in the warning
message.
+ */
+ final void warning(final short key, final Object argument) {
+ warnings.add(Resources.formatInternational(key, argument));
+ }
+}
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Column.java
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Column.java
deleted file mode 100644
index 6916bde..0000000
---
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Column.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * 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.internal.sql.feature;
-
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Collections;
-import java.util.UUID;
-import org.opengis.feature.AttributeType;
-import org.apache.sis.feature.DefaultAttributeType;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.internal.metadata.sql.SQLBuilder;
-
-
-/**
- * Description of a table column.
- *
- * @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-final class Column {
- /**
- * Description of the attribute telling whether a field is unique in the
database.
- */
- static final AttributeType<Boolean> JDBC_PROPERTY_UNIQUE = new
DefaultAttributeType<>(
- Collections.singletonMap(DefaultAttributeType.NAME_KEY, "unique"),
- Boolean.class, 1, 1, null);
-
- /**
- * Whether values in a column are generated by the database, computed from
a sequence of supplied.
- */
- enum Type {
- /**
- * Indicate this field value is generated by the database.
- */
- AUTO,
-
- /**
- * Indicate a sequence is used to generate field values.
- */
- SEQUENCED,
-
- /**
- * Indicate field value must be provided.
- */
- PROVIDED
- }
-
- /**
- * Database scheme where this column is found.
- */
- final String schema;
-
- /**
- * Database table where this column is found
- */
- final String table;
-
- /**
- * Name of the column.
- */
- final String name;
-
- /**
- * Column SQL type (integer, characters, …) as one of {@link
java.sql.Types} constants.
- */
- final int sqlType;
-
- /**
- * Name of {@link #sqlType}.
- */
- final String sqlTypeName;
-
- /**
- * Java class for the {@link #sqlType}.
- */
- final Class<?> clazz;
-
- /**
- * If the column is a primary key, specifies how the value is generated.
- */
- final Type type;
-
- /**
- * If the column is a primary key, the optional sequence name.
- */
- final String sequenceName;
-
- /**
- * Stores information about a table column.
- *
- * @param schema database scheme where this column is found.
- * @param table database table where this column is found.
- * @param name name of this column.
- * @param sqlType column SQL type as one of {@link java.sql.Types}
constants.
- * @param sqlTypeName name of {@code sqlType}.
- * @param clazz Java class for {@code sqlType}.
- * @param type if the column is a primary key, specify how the
value is generated.
- * @param sequenceName if the column is a primary key, optional sequence
name.
- */
- Column(final String schema, final String table, final String name,
- final int sqlType, final String sqlTypeName, final Class<?> clazz,
- final Type type, final String sequenceName)
- {
- this.schema = schema;
- this.table = table;
- this.name = name;
- this.sqlType = sqlType;
- this.sqlTypeName = sqlTypeName;
- this.clazz = clazz;
- this.type = type;
- this.sequenceName = sequenceName;
- }
-
- /**
- * Tries to compute next column value.
- *
- * @param dialect handler for syntax elements specific to the database.
- * @param cx connection to the database.
- * @return next field value.
- * @throws SQLException if a JDBC error occurred while executing a
statement.
- * @throws DataStoreException if another error occurred while fetching the
next value.
- */
- public Object nextValue(final SpatialFunctions dialect, final Connection
cx) throws SQLException, DataStoreException {
- Object next = null;
- if (type == Type.AUTO || type == Type.SEQUENCED) {
- // Delegate to the database for next value.
- next = dialect.nextValue(this, cx);
- } else {
- // Generate value if possible.
- if (Number.class.isAssignableFrom(clazz)) {
- // Get the maximum value in the database and increment it
- final String sql = new SQLBuilder(cx.getMetaData(), true)
- .append("SELECT 1 + MAX(")
- .appendIdentifier(name)
- .append(") FROM ")
- .appendIdentifier(schema, table)
- .toString();
- try (Statement st = cx.createStatement();
- ResultSet rs = st.executeQuery(sql)) {
- rs.next();
- next = rs.getObject(1);
- }
- if (next == null) {
- // Can be the result of an empty table.
- next = 1;
- }
- } else if (CharSequence.class.isAssignableFrom(clazz)) {
- // Use an UUID to reduce risk of conflicts.
- next = UUID.randomUUID().toString();
- }
- if (next == null) {
- throw new DataStoreException("Failed to generate a value for
column " + toString());
- }
- }
- return next;
- }
-
- /**
- * Returns a string representation of this column description for
debugging purpose.
- * The string returned by this method may change in any future SIS version.
- *
- * @return a string representation for debugging purpose.
- */
- @Override
- public String toString() {
- return new StringBuilder(name)
- .append('[')
- .append(sqlType)
- .append(", ")
- .append(sqlTypeName)
- .append(", ")
- .append(type)
- .append(']')
- .toString();
- }
-}
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Database.java
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Database.java
index 4ed2102..46dacb8 100644
---
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Database.java
+++
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Database.java
@@ -16,37 +16,20 @@
*/
package org.apache.sis.internal.sql.feature;
+import java.util.Set;
+import java.util.HashSet;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import org.opengis.util.GenericName;
-import org.opengis.coverage.Coverage;
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.PropertyNotFoundException;
-import org.opengis.feature.PropertyType;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.feature.builder.AttributeRole;
-import org.apache.sis.feature.builder.AttributeTypeBuilder;
-import org.apache.sis.feature.builder.FeatureTypeBuilder;
-import org.apache.sis.feature.builder.PropertyTypeBuilder;
-import org.apache.sis.internal.metadata.sql.SQLUtilities;
import org.apache.sis.internal.metadata.sql.Reflection;
-import org.apache.sis.internal.feature.Geometries;
import org.apache.sis.storage.sql.SQLStore;
-import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.FeatureNaming;
+import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.IllegalNameException;
-import org.apache.sis.util.ArgumentChecks;
+
+// Branch-dependent imports
+import org.opengis.feature.FeatureType;
/**
@@ -54,426 +37,88 @@ import org.apache.sis.util.ArgumentChecks;
* The work done here is similar to reverse engineering.
*
* @author Johann Sorel (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 1.0
* @module
*/
public final class Database {
/**
- * Possible value for the {@value Reflection#TABLE_TYPE} column in the
{@link ResultSet}
- * returned by {@link DatabaseMetaData#getTables(String, String, String,
String[])}.
- * Also a possible value for the last argument of above-cited method.
+ * All tables known to this {@code Database}. Each table contains a {@link
Table#featureType}.
*/
- private static final String TABLE = "TABLE", VIEW = "VIEW";
+ private final FeatureNaming<Table> tables;
/**
- * Abstract type used to mark features that are components of other
features.
- *
- * @deprecated replace by scoped name (TODO).
+ * Functions that may be specific to the geospatial database in use.
*/
- @Deprecated
- private static final FeatureType COMPONENT = new
FeatureTypeBuilder().setName("Component").setAbstract(true).build();
-
- private final SpatialFunctions functions;
- private final FeatureNaming<PrimaryKey> pkIndex;
- private final FeatureNaming<FeatureType> typeIndex;
- private final Map<String,Schema> schemas;
-
- public Database(final SQLStore store, final SpatialFunctions functions,
final String catalog,
- final String schema, final String table, final List<String>
addWarningsTo)
- throws SQLException, DataStoreException
- {
- if (table != null) {
- ArgumentChecks.ensureNonEmpty("table", table);
- }
- this.functions = functions;
- pkIndex = new FeatureNaming<>();
- typeIndex = new FeatureNaming<>();
- schemas = new HashMap<>();
- analyze(store, catalog, schema, table, addWarningsTo);
- }
-
- public FeatureType getFeatureType(final SQLStore store, final String
typeName) throws IllegalNameException {
- return typeIndex.get(store, typeName);
- }
+ final SpatialFunctions functions;
/**
- * Explores all tables and views then recreate a complex feature model
from relations.
+ * Creates a new model about the specified tables in a database.
+ * This constructor requires a list of tables to include in the model,
+ * but this list should not include the dependencies; this constructor
+ * will follow foreigner keys automatically.
+ *
+ * @param store the data store for which we are creating a
model. Used only in case of error.
+ * @param connection connection to the database.
+ * @param catalog name of a catalog as it is stored in the
database, or {@code null} for any catalog.
+ * @param schemaPattern pattern (with {@code '_'} and {@code '%'}
wildcards) of a schema, or {@code null} for any.
+ * @param tablePatterns pattern (with {@code '_'} and {@code '%'}
wildcards) of tables to include in the model.
+ * @throws SQLException if a database error occurred while reading
metadata.
+ * @throws DataStoreException if a logical error occurred while analyzing
the database structure.
*/
- private synchronized void analyze(final SQLStore store, final String
catalog, final String schemaName,
- final String tableName, final List<String> addWarningsTo)
+ public Database(final SQLStore store, final Connection connection, final
String catalog,
+ final String schemaPattern, final String[] tablePatterns)
throws SQLException, DataStoreException
{
- try (Connection cx = store.getDataSource().getConnection()) {
- final DatabaseMetaData metadata = cx.getMetaData();
- /*
- * Keep trace of the schemas that we need to visit, and the schema
already visited.
- * The boolean value tells whether the schema has already been
visited or not.
- * New schemas to visit may be added when following the relation
established by foreigner keys.
- */
- final Map<String,Boolean> requiredSchemas = new HashMap<>();
- /*
- * Schema names available in the database:
- * 1. TABLE_SCHEM : String => schema name
- * 2. TABLE_CATALOG : String => catalog name (may be null)
- */
- if (schemaName != null) {
- requiredSchemas.put(schemaName, Boolean.FALSE);
- } else try (ResultSet reflect = metadata.getSchemas()) {
- // TODO: use schemas in getTables instead.
+ tables = new FeatureNaming<>();
+ final Analyzer analyzer = new Analyzer(connection.getMetaData());
+ final String[] tableTypes = getTableTypes(analyzer.metadata);
+ for (final String tablePattern : tablePatterns) {
+ try (ResultSet reflect = analyzer.metadata.getTables(catalog,
schemaPattern, tablePattern, tableTypes)) {
while (reflect.next()) {
-
requiredSchemas.put(reflect.getString(Reflection.TABLE_SCHEM), Boolean.FALSE);
- }
- }
- /*
- * Iterate over all schemas that we need to process. We may need
to stop iteration and recreate
- * a new iterator because the methods invoked in this loop may
alter the map content.
- *
- * TODO: use a boolean return value telling us if we need to
recreate the iterator.
- */
- Iterator<Map.Entry<String,Boolean>> it;
- while ((it = requiredSchemas.entrySet().iterator()).hasNext()) {
- final Map.Entry<String,Boolean> sn = it.next();
- if (!sn.setValue(Boolean.TRUE)) {
- // TODO: escape with metadata.getSearchStringEscape().
- final Schema schema = analyzeSchema(metadata, catalog,
sn.getKey(), tableName, requiredSchemas, addWarningsTo);
- schemas.put(schema.name, schema);
+ String remarks = reflect.getString(Reflection.REMARKS);
+ remarks = (remarks != null) ? remarks.trim() : ""; //
Empty string means that we verified that there is no remarks.
+ analyzer.addDependency(new TableName(remarks, //
Opportunistically use the 'name' field for storing remarks.
+ reflect.getString(Reflection.TABLE_CAT),
+ reflect.getString(Reflection.TABLE_SCHEM),
+ reflect.getString(Reflection.TABLE_NAME)));
}
}
- reverseSimpleFeatureTypes(metadata);
}
- /*
- * Build indexes.
- */
- final Collection<Schema> candidates;
- if (schemaName == null) {
- candidates = schemas.values(); // Take all schemas.
- } else {
- candidates = Collections.singleton(schemas.get(schemaName));
- }
- for (Schema schema : candidates) {
- if (schema == null) {
- throw new SQLException("Specifed schema " + schemaName + "
does not exist.");
- }
- for (Table table : schema.getTables()) {
- final FeatureTypeBuilder ft = table.featureType;
- final GenericName name = ft.getName();
- pkIndex.add(store, name, table.key);
- if (table.isComponent()) {
- // We don't show subtype, they are part of other feature
types, add a flag to identify then
- ft.setSuperTypes(COMPONENT);
- }
- typeIndex.add(store, name, ft.build());
- }
+ TableName dependency;
+ while ((dependency = analyzer.nextDependency()) != null) {
+ final Table table = new Table(analyzer, dependency);
+ tables.add(store, table.featureType.getName(), table);
}
+ functions = analyzer.functions;
}
/**
- * @param schemaPattern schema name with "%" and "_" interpreted as
wildcards, or {@code null} for all schemas.
+ * Returns the "TABLE" and "VIEW" keywords for table type, with
unsupported keywords omitted.
*/
- private Schema analyzeSchema(final DatabaseMetaData metadata, final String
catalog, final String schemaPattern,
- final String tableNamePattern, final Map<String,Boolean>
requiredSchemas,
- final List<String> addWarningsTo) throws SQLException,
DataStoreException
- {
- final Schema schema = new Schema(schemaPattern);
- /*
- * Description of the tables available:
- * 1. TABLE_SCHEM : String => table schema (may be null)
- * 2. TABLE_NAME : String => table name
- * 3. TABLE_TYPE : String => table type (typically "TABLE" or
"VIEW").
- */
- try (ResultSet reflect = metadata.getTables(catalog, schemaPattern,
tableNamePattern, new String[] {TABLE, VIEW})) { // TODO: use
metadata.getTableTypes()
- while (reflect.next()) {
- schema.addTable(analyzeTable(metadata, reflect,
requiredSchemas, addWarningsTo));
- }
- }
- return schema;
- }
-
- private Table analyzeTable(final DatabaseMetaData metadata, final
ResultSet tableSet,
- final Map<String,Boolean> requiredSchemas, final List<String>
addWarningsTo)
- throws SQLException, DataStoreException
- {
- final String catalog = tableSet.getString(Reflection.TABLE_CAT);
- final String schemaName = tableSet.getString(Reflection.TABLE_SCHEM);
- final String tableName = tableSet.getString(Reflection.TABLE_NAME);
- final String tableType = tableSet.getString(Reflection.TABLE_TYPE);
- final Table table = new Table(tableName);
- final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
- /*
- * Explore all columns.
- */
- try (ResultSet reflect = metadata.getColumns(catalog, schemaName,
tableName, null)) {
- while (reflect.next()) {
- analyzeColumn(metadata, reflect,
ftb.addAttribute(Object.class));
- }
- }
- /*
- * Find primary keys.
- */
- final List<Column> cols = new ArrayList<>();
- try (ResultSet rp = metadata.getPrimaryKeys(catalog, schemaName,
tableName)) {
- while (rp.next()) {
- final String columnNamePattern =
rp.getString(Reflection.COLUMN_NAME);
- // TODO: escape columnNamePattern with
metadata.getSearchStringEscape().
- try (ResultSet reflect = metadata.getColumns(catalog,
schemaName, tableName, columnNamePattern)) {
- while (reflect.next()) {
// Should loop exactly once.
- final int sqlType =
reflect.getInt(Reflection.DATA_TYPE);
- final String sqlTypeName =
reflect.getString(Reflection.TYPE_NAME);
- Class<?> columnType = functions.getJavaType(sqlType,
sqlTypeName);
- if (columnType == null) {
- addWarningsTo.add("No class for SQL type " +
sqlType);
- columnType = Object.class;
- }
- Column col;
- final Boolean b =
SQLUtilities.parseBoolean(reflect.getString(Reflection.IS_AUTOINCREMENT));
- if (b != null && b) {
- col = new Column(schemaName, tableName,
columnNamePattern, sqlType, sqlTypeName, columnType, Column.Type.AUTO, null);
- } else {
- // TODO: need to distinguish "NO" and empty string.
- final String sequenceName =
functions.getColumnSequence(metadata.getConnection(), schemaName, tableName,
columnNamePattern);
- if (sequenceName != null) {
- col = new Column(schemaName, tableName,
columnNamePattern, sqlType,
- sqlTypeName, columnType,
Column.Type.SEQUENCED,sequenceName);
- } else {
- col = new Column(schemaName, tableName,
columnNamePattern, sqlType,
- sqlTypeName, columnType,
Column.Type.PROVIDED, null);
- }
- }
- cols.add(col);
- }
- }
- }
- }
- /*
- * Search indexes, they provide informations such as:
- * - Unique indexes may indicate 1:1 relations in complexe features
- * - Unique indexes can be used as primary key if no primary key are
defined
- */
- final boolean pkEmpty = cols.isEmpty();
- final List<String> names = new ArrayList<>();
- final Map<String,List<String>> uniqueIndexes = new HashMap<>();
- String indexname = null;
- // We can't cache this one, seems to be a bug in the driver, it won't
find anything for table name like '%'
- try (ResultSet reflect = metadata.getIndexInfo(catalog, schemaName,
tableName, true, false)) {
+ private static String[] getTableTypes(final DatabaseMetaData metadata)
throws SQLException {
+ final Set<String> types = new HashSet<>(4);
+ try (ResultSet reflect = metadata.getTableTypes()) {
while (reflect.next()) {
- final String columnName =
reflect.getString(Reflection.COLUMN_NAME);
- final String idxName =
reflect.getString(Reflection.INDEX_NAME);
- List<String> lst = uniqueIndexes.get(idxName);
- if (lst == null) {
- lst = new ArrayList<>();
- uniqueIndexes.put(idxName, lst);
- }
- lst.add(columnName);
- if (pkEmpty) {
- /*
- * We use a single index columns set as primary key
- * We must not mix with other potential indexes.
- */
- if (indexname == null) {
- indexname = idxName;
- } else if (!indexname.equals(idxName)) {
- continue;
- }
- names.add(columnName);
- }
- }
- }
- /*
- * For each unique index composed of one column add a flag on the
property descriptor.
- */
- for (Map.Entry<String,List<String>> entry : uniqueIndexes.entrySet()) {
- final List<String> columns = entry.getValue();
- if (columns.size() == 1) {
- String columnName = columns.get(0);
- for (PropertyTypeBuilder desc : ftb.properties()) {
- if (desc.getName().tip().toString().equals(columnName)) {
- final AttributeTypeBuilder<?> atb =
(AttributeTypeBuilder) desc;
-
atb.addCharacteristic(Column.JDBC_PROPERTY_UNIQUE).setDefaultValue(Boolean.TRUE);
- }
- }
- }
- }
- if (pkEmpty && !names.isEmpty()) {
- /*
- * Build a primary key from unique index.
- */
- try (ResultSet reflect = metadata.getColumns(catalog, schemaName,
tableName, null)) {
- while (reflect.next()) {
- final String columnName =
reflect.getString(Reflection.COLUMN_NAME);
- if (names.contains(columnName)) {
- final int sqlType =
reflect.getInt(Reflection.DATA_TYPE);
- final String sqlTypeName =
reflect.getString(Reflection.TYPE_NAME);
- final Class<?> columnType =
functions.getJavaType(sqlType, sqlTypeName);
- final Column col = new Column(schemaName, tableName,
columnName,
- sqlType, sqlTypeName, columnType,
Column.Type.PROVIDED, null);
- cols.add(col);
- /*
- * Set as identifier
- */
- for (PropertyTypeBuilder desc : ftb.properties()) {
- if
(desc.getName().tip().toString().equals(columnName)) {
- final AttributeTypeBuilder<?> atb =
(AttributeTypeBuilder) desc;
-
atb.addRole(AttributeRole.IDENTIFIER_COMPONENT);
- break;
- }
- }
- }
- }
- }
- }
- if (cols.isEmpty()) {
- if (TABLE.equals(tableType)) {
- addWarningsTo.add("No primary key found for " + tableName);
- }
- }
- table.key = new PrimaryKey(tableName, cols);
- /*
- * Mark primary key columns.
- */
- for (PropertyTypeBuilder desc : ftb.properties()) {
- for (Column col : cols) {
- if (desc.getName().tip().toString().equals(col.name)) {
- final AttributeTypeBuilder<?> atb = (AttributeTypeBuilder)
desc;
- atb.addRole(AttributeRole.IDENTIFIER_COMPONENT);
- break;
- }
- }
- }
- /*
- * Creates a list of associations between the table read by this
method and other tables.
- * The associations are defined by the foreigner keys referencing
primary keys. Note that
- * the table relations can be defined in both ways: the foreigner
keys of this table may
- * be referencing the primary keys of other tables (Direction.IMPORT)
or the primary keys
- * of this table may be referenced by the foreigner keys of other
tables (Direction.EXPORT).
- * However in both case, we will translate that into associations from
this table to the
- * other tables. We can not rely on IMPORT versus EXPORT for
determining the association
- * navigability because the database designer's choice may be driven
by the need to support
- * multi-occurrences.
- */
- try (ResultSet reflect = metadata.getImportedKeys(catalog, schemaName,
tableName)) {
- while (!reflect.isClosed()) {
- final Relation relation = new
Relation(Relation.Direction.IMPORT, reflect);
- table.importedKeys.add(relation);
- if (relation.schema != null) {
- requiredSchemas.putIfAbsent(relation.schema,
Boolean.FALSE);
- }
- }
- }
- try (ResultSet reflect = metadata.getExportedKeys(catalog, schemaName,
tableName)) {
- while (!reflect.isClosed()) {
- final Relation relation = new
Relation(Relation.Direction.IMPORT, reflect);
- table.exportedKeys.add(relation);
- if (relation.schema != null) {
- requiredSchemas.putIfAbsent(relation.schema,
Boolean.FALSE);
+ final String type = reflect.getString(Reflection.TABLE_TYPE);
+ if ("TABLE".equalsIgnoreCase(type) ||
"VIEW".equalsIgnoreCase(type)) {
+ types.add(type);
}
}
}
- ftb.setName(tableName);
- table.tableType = ftb;
- return table;
- }
-
- private AttributeType<?> analyzeColumn(final DatabaseMetaData metadata,
final ResultSet columnSet, final AttributeTypeBuilder<?> atb) throws
SQLException {
- final String schemaName =
columnSet.getString(Reflection.TABLE_SCHEM);
- final String tableName =
columnSet.getString(Reflection.TABLE_NAME);
- final String columnName =
columnSet.getString(Reflection.COLUMN_NAME);
- final int columnSize = columnSet.getInt
(Reflection.COLUMN_SIZE);
- final int columnDataType = columnSet.getInt
(Reflection.DATA_TYPE);
- final String columnTypeName =
columnSet.getString(Reflection.TYPE_NAME);
- final String columnNullable =
columnSet.getString(Reflection.IS_NULLABLE);
- atb.setName(columnName);
- atb.setMaximalLength(columnSize);
- functions.decodeColumnType(atb, metadata.getConnection(),
columnTypeName, columnDataType, schemaName, tableName, columnName);
- // TODO: need to distinguish "YES" and empty string?
- final Boolean b = SQLUtilities.parseBoolean(columnNullable);
- atb.setMinimumOccurs(b != null && !b ? 1 : 0);
- atb.setMaximumOccurs(1);
- return atb.build();
+ return types.toArray(new String[types.size()]);
}
/**
- * Analyze the metadata of the ResultSet to rebuild a feature type.
- */
- final FeatureType analyzeResult(final ResultSet result, final String name)
throws SQLException, DataStoreException {
- final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
- ftb.setName(name);
- final ResultSetMetaData metadata = result.getMetaData();
- final int nbcol = metadata.getColumnCount();
- for (int i=1; i <= nbcol; i++) {
- final String columnName = metadata.getColumnName(i);
- final String columnLabel = metadata.getColumnLabel(i);
- final String schemaName = metadata.getSchemaName(i);
- final String tableName = metadata.getTableName(i);
- final int sqlType = metadata.getColumnType(i);
- final String sqlTypeName = metadata.getColumnTypeName(i);
-
- // Search if we already have this property
- PropertyType desc = null;
- final Schema schema = schemas.get(schemaName);
- if (schema != null) {
- Table table = schema.getTable(tableName);
- if (table != null) {
- try {
- desc =
table.featureType.build().getProperty(columnName);
- } catch (PropertyNotFoundException ex) {
- // ok
- }
- }
- }
- if (desc != null) {
- ftb.addProperty(desc);
- } else {
- // could not find the original type
- // this column must be calculated
- final AttributeTypeBuilder<?> atb =
ftb.addAttribute(Object.class);
- final int nullable = metadata.isNullable(i);
- atb.setName(columnLabel);
- atb.setMinimumOccurs(nullable ==
ResultSetMetaData.columnNullable ? 0 : 1);
- atb.setMaximumOccurs(1);
- atb.setName(columnLabel);
- atb.setValueClass(functions.getJavaType(sqlType, sqlTypeName));
- }
- }
- return ftb.build();
- }
-
- /**
- * Rebuild simple feature types for each table.
+ * Returns the feature type of the given name.
+ *
+ * @param store the data store for which we are created the model. Used
only in case of error.
+ * @param name name of the feature type to fetch.
+ * @return the feature type of the given name.
+ * @throws IllegalNameException if the given name is unknown or ambiguous.
*/
- private void reverseSimpleFeatureTypes(final DatabaseMetaData metadata)
throws SQLException {
- for (final Schema schema : schemas.values()) {
- for (final Table table : schema.getTables()) {
- final FeatureTypeBuilder ftb = new
FeatureTypeBuilder(table.tableType.build());
- final String featureName = ftb.getName().tip().toString();
- ftb.setName(featureName);
- final List<PropertyTypeBuilder> descs = ftb.properties();
- boolean defaultGeomSet = false;
- for (int i=0,n=descs.size(); i<n; i++) {
- final AttributeTypeBuilder<?> atb = (AttributeTypeBuilder)
descs.get(i);
- final String name = atb.getName().tip().toString();
- atb.setName(name);
- /*
- * Configure CRS if the column contains a geometry or a
raster.
- */
- final Class<?> binding = atb.getValueClass();
- final boolean isGeometry = Geometries.isKnownType(binding);
- if (isGeometry ||
Coverage.class.isAssignableFrom(binding)) {
- // TODO: escape columnNamePattern with
metadata.getSearchStringEscape().
- try (ResultSet reflect = metadata.getColumns(null,
schema.name, table.name, name)) {
- while (reflect.next()) { // Should loop
exactly once.
- CoordinateReferenceSystem crs =
functions.createGeometryCRS(reflect);
- atb.setCRS(crs);
- if (isGeometry & !defaultGeomSet) {
-
atb.addRole(AttributeRole.DEFAULT_GEOMETRY);
- defaultGeomSet = true;
- }
- }
- }
- }
- }
- table.featureType = ftb;
- }
- }
+ public FeatureType getFeatureType(final SQLStore store, final String name)
throws IllegalNameException {
+ return tables.get(store, name).featureType;
}
}
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/PrimaryKey.java
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/PrimaryKey.java
deleted file mode 100644
index a4e9139..0000000
---
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/PrimaryKey.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * 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.internal.sql.feature;
-
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
-import org.apache.sis.storage.DataStoreException;
-
-
-/**
- * Description of a table primary key.
- *
- * @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-final class PrimaryKey {
-
- final String table;
- final List<Column> columns;
-
- PrimaryKey(final String table, List<Column> columns) {
- this.table = table;
- if (columns == null) {
- columns = Collections.emptyList();
- }
- this.columns = columns;
- }
-
- /**
- * Creates a feature identifier from primary key column values.
- * This method uses only the current row of the given result set.
- *
- * @param rs the result set positioned on a row.
- * @return the feature identifier for current row of the given result set.
- */
- String buildIdentifier(final ResultSet rs) throws SQLException {
- final int size = columns.size();
- switch (size) {
- case 0: {
- // No primary key columns, generate a random id
- return UUID.randomUUID().toString();
- }
- case 1: {
- // Unique column value
- return rs.getString(columns.get(0).name);
- }
- default: {
- // Aggregate column values
- final Object[] values = new Object[size];
- for (int i=0; i<size; i++) {
- values[i] = rs.getString(columns.get(i).name);
- }
- return buildIdentifier(values);
- }
- }
- }
-
- private static String buildIdentifier(final Object[] values) {
- final StringBuilder sb = new StringBuilder();
- for (int i=0; i<values.length; i++) {
- if (i > 0) sb.append('.');
- sb.append(values[i]);
- }
- return sb.toString();
- }
-
- /**
- * Creates the field values for all columns of a the primary key.
- *
- * @param dialect handler for syntax elements specific to the database.
- * @param cx connection to the database.
- * @return primary key values.
- * @throws SQLException if a JDBC error occurred while executing a
statement.
- * @throws DataStoreException if another error occurred while fetching the
next value.
- */
- Object[] nextValues(final SpatialFunctions dialect, final Connection cx)
throws SQLException, DataStoreException {
- final Object[] parts = new Object[columns.size()];
- for (int i=0; i<parts.length; i++) {
- parts[i] = columns.get(i).nextValue(dialect, cx);
- }
- return parts;
- }
-}
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/QueriedFeatureSet.java
similarity index 50%
rename from
storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java
rename to
storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/QueriedFeatureSet.java
index 15dd6c9..bcb4172 100644
---
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java
+++
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/QueriedFeatureSet.java
@@ -18,19 +18,21 @@ package org.apache.sis.internal.sql.feature;
import java.sql.Connection;
import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.stream.Stream;
import org.apache.sis.storage.sql.SQLQuery;
import org.apache.sis.storage.sql.SQLStore;
import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.event.ChangeEvent;
-import org.apache.sis.storage.event.ChangeListener;
+import org.apache.sis.internal.storage.AbstractFeatureSet;
+import org.apache.sis.feature.builder.AttributeTypeBuilder;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+
+// Branch-dependent imports
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
-import org.opengis.geometry.Envelope;
-import org.opengis.metadata.Metadata;
+import org.opengis.feature.PropertyType;
/**
@@ -41,14 +43,15 @@ import org.opengis.metadata.Metadata;
* @since 1.0
* @module
*/
-public final class QueryFeatureSet implements FeatureSet {
+public final class QueriedFeatureSet extends AbstractFeatureSet {
private final Database model;
private final SQLStore store;
private final SQLQuery query;
private FeatureType type;
- public QueryFeatureSet(final SQLStore store, final Database model, final
SQLQuery query) {
+ public QueriedFeatureSet(final SQLStore store, final Database model, final
SQLQuery query) {
+ super((AbstractFeatureSet) null);
this.store = store;
this.model = model;
this.query = query;
@@ -59,9 +62,9 @@ public final class QueryFeatureSet implements FeatureSet {
if (type == null) {
final String sql = query.getStatement();
try (Connection cnx = store.getDataSource().getConnection();
- Statement stmt = cnx.createStatement();
- ResultSet rs = stmt.executeQuery(sql)) {
- type = model.analyzeResult(rs, query.getName());
+ Statement stmt = cnx.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ type = analyzeResult(rs, query.getName());
} catch (SQLException ex) {
throw new DataStoreException(ex);
}
@@ -69,26 +72,40 @@ public final class QueryFeatureSet implements FeatureSet {
return type;
}
- @Override
- public Stream<Feature> features(boolean parallel) throws
DataStoreException {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
- @Override
- public Envelope getEnvelope() throws DataStoreException {
- return null;
+ /**
+ * Analyze the metadata of the ResultSet to rebuild a feature type.
+ */
+ final FeatureType analyzeResult(final ResultSet result, final String name)
throws SQLException {
+ final FeatureTypeBuilder ftb = new FeatureTypeBuilder().setName(name);
+ final ResultSetMetaData metadata = result.getMetaData();
+ final int nbcol = metadata.getColumnCount();
+ for (int i=1; i <= nbcol; i++) {
+ /*
+ * Search if we already have this property.
+ */
+ PropertyType desc = null; // TODO
+// model.getProperty(metadata.getCatalogName(i),
+// metadata.getSchemaName(i),
+// metadata.getTableName(i),
+// metadata.getColumnName(i));
+ if (desc != null) {
+ ftb.addProperty(desc);
+ } else {
+ /*
+ * Could not find the type. This column may be a calculation
result.
+ */
+ final Class<?> type =
model.functions.toJavaType(metadata.getColumnType(i),
metadata.getColumnTypeName(i));
+ final AttributeTypeBuilder<?> atb =
ftb.addAttribute(type).setName(metadata.getColumnLabel(i));
+ if (metadata.isNullable(i) ==
ResultSetMetaData.columnNullable) {
+ atb.setMinimumOccurs(0);
+ }
+ }
+ }
+ return ftb.build();
}
@Override
- public Metadata getMetadata() throws DataStoreException {
+ public Stream<Feature> features(boolean parallel) throws
DataStoreException {
throw new UnsupportedOperationException("Not supported yet.");
}
-
- @Override
- public <T extends ChangeEvent> void addListener(ChangeListener<? super T>
listener, Class<T> eventType) {
- }
-
- @Override
- public <T extends ChangeEvent> void removeListener(ChangeListener<? super
T> listener, Class<T> eventType) {
- }
}
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
index 8076ef0..8f45ef7 100644
---
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
+++
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
@@ -18,6 +18,8 @@ package org.apache.sis.internal.sql.feature;
import java.util.Map;
import java.util.LinkedHashMap;
+import java.util.Collection;
+import java.util.Objects;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.DatabaseMetaData;
@@ -49,7 +51,7 @@ import org.apache.sis.storage.DataStoreContentException;
* @since 1.0
* @module
*/
-final class Relation extends MetaModel {
+final class Relation extends TableName {
/**
* Whether another table is <em>using</em> or is <em>used by</em> the
table containing the {@link Relation}.
*/
@@ -72,6 +74,13 @@ final class Relation extends MetaModel {
EXPORT(Reflection.FK_NAME, Reflection.FKTABLE_CAT,
Reflection.FKTABLE_SCHEM,
Reflection.FKTABLE_NAME, Reflection.FKCOLUMN_NAME,
Reflection.PKCOLUMN_NAME);
+ /*
+ * Note: another possible type of relation is the one provided by
getCrossReference(…).
+ * Inconvenient is that it requires to know the tables on both side of
the relation.
+ * But advantage is that it work with any set of columns having unique
values
+ * (not necessarily the primary key).
+ */
+
/**
* The database {@link Reflection} key to use for fetching the name of
a relation.
* The name is used only for informative purpose and may be {@code
null}.
@@ -108,11 +117,6 @@ final class Relation extends MetaModel {
}
/**
- * The catalog, schema and table name of the other table.
- */
- final String catalog, schema, table;
-
- /**
* The columns of the other table that constitute a primary or foreigner
key. Keys are the columns of the
* other table and values are columns of the table containing this {@code
Relation}.
*/
@@ -143,10 +147,11 @@ final class Relation extends MetaModel {
* least one row, unless an exception occurs.</p>
*/
Relation(final Direction dir, final ResultSet reflect) throws
SQLException, DataStoreContentException {
- super(reflect.getString(dir.name));
- catalog = reflect.getString(dir.catalog);
- schema = reflect.getString(dir.schema);
- table = reflect.getString(dir.table);
+ super(reflect.getString(dir.name),
+ reflect.getString(dir.catalog),
+ reflect.getString(dir.schema),
+ reflect.getString(dir.table));
+
final Map<String,String> m = new LinkedHashMap<>();
boolean cascade = false;
do {
@@ -161,15 +166,24 @@ final class Relation extends MetaModel {
reflect.close();
break;
}
- } while (reflect.getString(dir.table) == table &&
- reflect.getString(dir.schema) == schema &&
- reflect.getString(dir.catalog) == catalog);
+ } while (table.equals(reflect.getString(dir.table)) &&
// Table name is mandatory.
+ Objects.equals(schema, reflect.getString(dir.schema)) &&
// Schema and catalog may be null.
+ Objects.equals(catalog, reflect.getString(dir.catalog)));
columns = CollectionsExt.compact(m);
cascadeOnDelete = cascade;
}
/**
+ * Adds to the given collection the foreigner keys of the table that
contains this relation.
+ * This method adds only the foreigner keys known to this relation; this
is not necessarily
+ * all the table foreigner keys.
+ */
+ final void getForeignerKeys(final Collection<String> addTo) {
+ addTo.addAll(columns.values());
+ }
+
+ /**
* Creates a tree representation of this relation for debugging purpose.
*/
@Debug
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.java
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.java
index 5c4a910..cf11959 100644
---
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.java
+++
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.java
@@ -20,8 +20,10 @@ import java.net.URL;
import java.util.Locale;
import java.util.MissingResourceException;
import javax.annotation.Generated;
+import org.opengis.util.InternationalString;
import org.apache.sis.util.resources.KeyConstants;
import org.apache.sis.util.resources.IndexedResourceBundle;
+import org.apache.sis.util.resources.ResourceInternationalString;
/**
@@ -62,6 +64,11 @@ public final class Resources extends IndexedResourceBundle {
* Unexpected duplication of “{0}” entity named “{1}”.
*/
public static final short DuplicatedEntity_2 = 1;
+
+ /**
+ * No mapping from SQL type “{0}” to a Java class.
+ */
+ public static final short UnknownType_1 = 2;
}
/**
@@ -137,4 +144,43 @@ public final class Resources extends IndexedResourceBundle
{
{
return forLocale(null).getString(key, arg0, arg1);
}
+
+ /**
+ * The international string to be returned by {@link formatInternational}.
+ */
+ private static final class International extends
ResourceInternationalString {
+ private static final long serialVersionUID = 7325356372249131588L;
+
+ International(short key) {super(key);}
+ International(short key, Object args) {super(key, args);}
+ @Override protected KeyConstants getKeyConstants() {return
Keys.INSTANCE;}
+ @Override protected IndexedResourceBundle getBundle(final Locale
locale) {
+ return forLocale(locale);
+ }
+ }
+
+ /**
+ * Gets an international string for the given key. This method does not
check for the key
+ * validity. If the key is invalid, then a {@link
MissingResourceException} may be thrown
+ * when a {@link InternationalString#toString(Locale)} method is invoked.
+ *
+ * @param key the key for the desired string.
+ * @return an international string for the given key.
+ */
+ public static InternationalString formatInternational(final short key) {
+ return new International(key);
+ }
+
+ /**
+ * Gets an international string for the given key. This method does not
check for the key
+ * validity. If the key is invalid, then a {@link
MissingResourceException} may be thrown
+ * when a {@link InternationalString#toString(Locale)} method is invoked.
+ *
+ * @param key the key for the desired string.
+ * @param args values to substitute to "{0}", "{1}", <i>etc</i>.
+ * @return an international string for the given key.
+ */
+ public static InternationalString formatInternational(final short key,
final Object... args) {
+ return new International(key, args);
+ }
}
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.properties
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.properties
index 3e2e217..db1b778 100644
---
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.properties
+++
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources.properties
@@ -20,3 +20,4 @@
# For resources shared by all modules in the Apache SIS project, see
"org.apache.sis.util.resources" package.
#
DuplicatedEntity_2 = Unexpected duplication of \u201c{0}\u201d
entity named \u201c{1}\u201d.
+UnknownType_1 = No mapping from SQL type \u201c{0}\u201d
to a Java class.
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources_fr.properties
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources_fr.properties
index cd0f2b9..7b627c3 100644
---
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources_fr.properties
+++
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Resources_fr.properties
@@ -25,3 +25,4 @@
# U+00A0 NO-BREAK SPACE before :
#
DuplicatedEntity_2 = Doublon inattendu d\u2019une entit\u00e9
\u00ab\u202f{0}\u202f\u00bb nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb.
+UnknownType_1 = Pas de correspondance entre le type SQL
\u00ab\u202f{0}\u202f\u00bb et une classe Java.
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java
index af3fbb5..ee4ad7f 100644
---
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java
+++
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java
@@ -16,22 +16,25 @@
*/
package org.apache.sis.internal.sql.feature;
-import java.util.Set;
-import java.util.HashSet;
-import java.util.Locale;
-import java.sql.Connection;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.LocalDateTime;
+import java.time.OffsetTime;
+import java.time.OffsetDateTime;
+import java.sql.Types;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.DatabaseMetaData;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.feature.builder.AttributeTypeBuilder;
-import org.apache.sis.internal.metadata.sql.Dialect;
-import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.internal.metadata.sql.Reflection;
/**
* Access to functions provided by geospatial databases.
* Those functions may depend on the actual database product (PostGIS, etc).
+ * Protected methods in this class can be overridden in subclasses
+ * for handling database-specific features.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
@@ -39,123 +42,92 @@ import org.apache.sis.storage.DataStoreException;
* @since 1.0
* @module
*/
-abstract class SpatialFunctions {
+class SpatialFunctions {
/**
- * Names of tables to ignore when inspecting a database schema.
- * Those tables are used for database internal working (for example by
PostGIS).
+ * Whether {@link Types#TINYINT} is an unsigned integer. Both conventions
(-128 … 127 range and
+ * 0 … 255 range) are found on the web. If unspecified, we conservatively
assume unsigned bytes.
+ * All other integer types are presumed signed.
*/
- private final Set<String> ignoredTables;
+ private final boolean isByteUnsigned;
/**
* Creates a new accessor to geospatial functions for the database
described by given metadata.
*/
SpatialFunctions(final DatabaseMetaData metadata) throws SQLException {
- ignoredTables = new HashSet<>(4);
/*
- * 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.
+ * Get information about whether byte are unsigned.
+ * According JDBC specification, the rows shall be ordered by
DATA_TYPE.
+ * But the PostgreSQL driver 42.2.2 still provides rows in random
order.
*/
- String crs = "SPATIAL_REF_SYS";
- String geom = "GEOMETRY_COLUMNS";
- if (metadata.storesLowerCaseIdentifiers()) {
- crs = crs .toLowerCase(Locale.US).intern();
- geom = geom.toLowerCase(Locale.US).intern();
- }
- ignoredTables.add(crs);
- ignoredTables.add(geom);
- final Dialect dialect = Dialect.guess(metadata);
- if (dialect == Dialect.POSTGRESQL) {
- ignoredTables.add("geography_columns"); // Postgis 1+
- ignoredTables.add("raster_columns"); // Postgis 2
- ignoredTables.add("raster_overviews");
+ boolean unsigned = true;
+ try (ResultSet reflect = metadata.getTypeInfo()) {
+ while (reflect.next()) {
+ if (reflect.getInt(Reflection.DATA_TYPE) == Types.TINYINT) {
+ unsigned =
reflect.getBoolean(Reflection.UNSIGNED_ATTRIBUTE);
+ if (unsigned) break; // Give precedence to "true"
value.
+ }
+ }
}
+ isByteUnsigned = unsigned;
}
/**
- * Returns whether a table is reserved for database internal working.
- * If this method returns {@code false}, then the given table is a
candidate
- * for use as a {@code FeatureType}.
+ * Maps a given SQL type to a Java class.
+ * This method shall not return primitive types; their wrappers shall be
used instead.
+ * It may return array of primitive types however.
+ * If no match is found, then this method returns {@code null}.
*
- * @param name database table name to test.
- * @return {@code true} if the named table should be ignored when looking
for feature types.
- */
- final boolean isIgnoredTable(final String name) {
- return ignoredTables.contains(name);
- }
-
- /**
- * Gets the Java class mapped to a given SQL type.
+ * <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 name of {@code sqlType}.
- * @return corresponding java type.
- *
- * @todo What happen if there is no match?
- */
- public abstract Class<?> getJavaType(int sqlType, String sqlTypeName);
-
- /**
- * If a column is an auto-increment or has a sequence, tries to extract
next value.
- *
- * @param column description of the database column for which to get the
next value.
- * @param cx connection to the database.
- * @return column value or null if none.
- * @throws SQLException if a JDBC error occurred while executing a
statement.
- * @throws DataStoreException if another error occurred while fetching the
next value.
- */
- public abstract Object nextValue(Column column, Connection cx) throws
SQLException, DataStoreException;
-
- /**
- * Gets the value sequence name used by a column.
- *
- * @param cx connection to the database.
- * @param schema name of the database schema.
- * @param table name of the database table.
- * @param column name of the database column.
- * @return sequence name or null if none.
- * @throws SQLException if a JDBC error occurred while executing a
statement.
- */
- public abstract String getColumnSequence(Connection cx, String schema,
String table, String column) throws SQLException;
-
- /**
- * Builds column attribute type.
- *
- * @param atb builder for the attribute being created.
- * @param cx connection to the database.
- * @param typeName column data type name.
- * @param datatype column data type code.
- * @param schema name of the database schema.
- * @param table name of the database table.
- * @param column name of the database column.
- * @throws SQLException if a JDBC error occurred while executing a
statement.
+ * @param sqlTypeName data source dependent type name. For User Defined
Type (UDT) the name is fully qualified.
+ * @return corresponding java type, or {@code null} if unknown.
*/
- public abstract void decodeColumnType(final AttributeTypeBuilder<?> atb,
final Connection cx,
- final String typeName, final int datatype, final String schema,
- final String table, final String column) throws SQLException;
+ @SuppressWarnings("fallthrough")
+ protected Class<?> toJavaType(final int sqlType, final String sqlTypeName)
{
+ switch (sqlType) {
+ case Types.BIT:
+ case Types.BOOLEAN: return Boolean.class;
+ case Types.TINYINT: if (!isByteUnsigned) return
Byte.class; // else fallthrough.
+ case Types.SMALLINT: return Short.class;
+ case Types.INTEGER: return Integer.class;
+ case Types.BIGINT: return Long.class;
+ case Types.REAL: return Float.class;
+ case Types.FLOAT: // Despite the name, this is
implemented as DOUBLE in major databases.
+ case Types.DOUBLE: return Double.class;
+ case Types.NUMERIC: // Similar to DECIMAL except
that it uses exactly the specified precision.
+ case Types.DECIMAL: return BigDecimal.class;
+ case Types.CHAR:
+ case Types.VARCHAR:
+ case Types.LONGVARCHAR: return String.class;
+ case Types.DATE: return LocalDate.class;
+ case Types.TIME: return LocalTime.class;
+ case Types.TIMESTAMP: return LocalDateTime.class;
+ case Types.TIME_WITH_TIMEZONE: return OffsetTime.class;
+ case Types.TIMESTAMP_WITH_TIMEZONE: return OffsetDateTime.class;
+ case Types.BINARY:
+ case Types.VARBINARY:
+ case Types.LONGVARBINARY: return byte[].class;
+ case Types.ARRAY: return Object[].class;
+ case Types.OTHER: // Database-specific accessed
via getObject and setObject.
+ case Types.JAVA_OBJECT: return Object.class;
+ default: return null;
+ }
+ }
/**
- * Builds geometry column attribute type.
+ * Creates the Coordinate Reference System associated to the the geometry
SRID of a given column.
+ * The {@code reflect} argument is the result of a call to {@link
DatabaseMetaData#getColumns
+ * DatabaseMetaData.getColumns(…)} with the cursor positioned on the row
describing the column.
*
- * @param atb builder for the attribute being created.
- * @param cx connection to the database.
- * @param rs connection result set.
- * @param columnIndex geometric column index.
- * @param customquery {@code true} if the request is a custom query.
- * @throws SQLException if a JDBC error occurred while executing a
statement.
- */
- public abstract void decodeGeometryColumnType(final
AttributeTypeBuilder<?> atb, final Connection cx,
- final ResultSet rs, final int columnIndex, boolean customquery)
throws SQLException;
-
- /**
- * Creates the CRS associated to the the geometry SRID of a given column.
The {@code reflect} argument
- * is the result of a call to {@link DatabaseMetaData#getColumns(String,
String, String, String)
- * DatabaseMetaData.getColumns(…)} with the cursor positioned on the row
to process.
+ * <p>The default implementation returns {@code null}. Subclasses may
override.</p>
*
* @param reflect the result of {@link DatabaseMetaData#getColumns
DatabaseMetaData.getColumns(…)}.
- * @return CoordinateReferenceSystem ID in the database
+ * @return Coordinate Reference System in the database for the given
column, or {@code null} if unknown.
* @throws SQLException if a JDBC error occurred while executing a
statement.
*/
- public abstract CoordinateReferenceSystem createGeometryCRS(ResultSet
reflect) throws SQLException;
+ protected CoordinateReferenceSystem createGeometryCRS(ResultSet reflect)
throws SQLException {
+ return null;
+ }
}
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Table.java
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Table.java
index d451f48..8a66477 100644
---
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Table.java
+++
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/Table.java
@@ -16,75 +16,214 @@
*/
package org.apache.sis.internal.sql.feature;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.List;
import java.util.ArrayList;
-import java.util.Collection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.util.Debug;
import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.AttributeTypeBuilder;
import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.internal.feature.Geometries;
+import org.apache.sis.internal.metadata.sql.Reflection;
+import org.apache.sis.internal.metadata.sql.SQLUtilities;
+import org.apache.sis.internal.util.CollectionsExt;
+
+// Branch-dependent imports
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.FeatureAssociationRole;
/**
- * Description of a table in the database. The description is provided as a
{@code FeatureType}.
+ * Description of a table in the database, including columns, primary keys and
foreigner keys.
+ * This class contains a {@link FeatureType} inferred from the table
structure. The {@link FeatureType}
+ * contains an {@link AttributeType} for each table column, except foreigner
keys which are represented
+ * by {@link FeatureAssociationRole}s.
*
* @author Johann Sorel (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 1.0
* @module
*/
final class Table extends MetaModel {
/**
- * @deprecated to be replaced by {@link #featureType} only (TODO).
+ * The structure of this table represented as a feature. Each feature
attribute is a table column,
+ * except synthetic attributes like "sis:identifier". The feature may also
contain associations
+ * inferred from foreigner keys that are not immediately apparent in the
table.
*/
- @Deprecated
- FeatureTypeBuilder tableType;
+ final FeatureType featureType;
/**
- * A temporary object used for building the {@code FeatureType}.
+ * The primary key of this table. The boolean values tells whether the
column
+ * uses auto-increment, with null value meaning that we don't know.
*/
- FeatureTypeBuilder featureType;
-
- /**
- * The primary key of this table.
- */
- PrimaryKey key;
+ private final Map<String,Boolean> primaryKeys;
/**
* The primary keys of other tables that are referenced by this table
foreign key columns.
* They are 0:1 relations.
*/
- final Collection<Relation> importedKeys;
+ private final List<Relation> importedKeys;
/**
* The foreign keys of other tables that reference this table primary key
columns.
* They are 0:N relations
*/
- final Collection<Relation> exportedKeys;
+ private final List<Relation> exportedKeys;
/**
- * Creates a new table of the given name.
- */
- Table(final String name) {
- super(name);
- importedKeys = new ArrayList<>();
- exportedKeys = new ArrayList<>();
- }
-
- /**
- * Determines if this table is a component of another table. Conditions
are:
- * <ul>
- * <li>having a relation toward another type</li>
- * <li>relation must be cascading.</li>
- * </ul>
+ * Creates a description of the table of the given name.
+ * The table is identified by {@code id}, which contains a (catalog,
schema, name) tuple.
+ * The catalog and schema parts are optional and can be null, but the
table is mandatory.
+ *
+ * <p>The {@link TableName#name} field is opportunistically used for
storing optional remarks
+ * (this may change in any future version).</p>
*
- * @return whether this table is a component of another table.
+ * @param analyzer helper functions, e.g. for converting SQL types to
Java types.
+ * @param id the catalog, schema and table name of the table to
analyze.
*/
- boolean isComponent() {
- for (Relation relation : importedKeys) {
- if (relation.cascadeOnDelete) {
- return true;
+ Table(final Analyzer analyzer, final TableName id) throws SQLException,
DataStoreContentException {
+ super(id.table);
+ final String tableEsc = analyzer.escape(id.table);
+ final String schemaEsc = analyzer.escape(id.schema);
+ /*
+ * Get a list of primary keys. We need to know them before to create
the attributes,
+ * in order to detect which attributes are used as components of
Feature identifiers.
+ * In the 'primaryKeys' map, the boolean tells whether the column uses
auto-increment,
+ * with null value meaning that we don't know.
+ *
+ * Note: when a table contains no primary keys, we could still look
for index columns
+ * with unique constraint using metadata.getIndexInfo(catalog, schema,
table, true).
+ * We don't do that for now because of uncertainties (which index to
use if there is
+ * many? If they are suitable as identifiers why they are not primary
keys?).
+ */
+ final Map<String,Boolean> primaryKeys = new HashMap<>();
+ try (ResultSet reflect = analyzer.metadata.getPrimaryKeys(id.catalog,
id.schema, id.table)) {
+ while (reflect.next()) {
+ primaryKeys.put(reflect.getString(Reflection.COLUMN_NAME),
null);
+ // The actual Boolean value will be fetched in the loop on
columns later.
}
}
- return false;
+ /*
+ * Creates a list of associations between the table read by this
method and other tables.
+ * The associations are defined by the foreigner keys referencing
primary keys. Note that
+ * the table relations can be defined in both ways: the foreigner
keys of this table may
+ * be referencing the primary keys of other tables (Direction.IMPORT)
or the primary keys
+ * of this table may be referenced by the foreigner keys of other
tables (Direction.EXPORT).
+ * However in both case, we will translate that into associations from
this table to the
+ * other tables. We can not rely on IMPORT versus EXPORT for
determining the association
+ * navigability because the database designer's choice may be driven
by the need to support
+ * multi-occurrences.
+ */
+ final List<Relation> importedKeys = new ArrayList<>();
+ final List<Relation> exportedKeys = new ArrayList<>();
+ final Set<String> foreignerKeys = new HashSet<>();
+ try (ResultSet reflect = analyzer.metadata.getImportedKeys(id.catalog,
id.schema, id.table)) {
+ if (reflect.next()) do {
+ final Relation relation = new
Relation(Relation.Direction.IMPORT, reflect);
+ relation.getForeignerKeys(foreignerKeys);
+ analyzer.addDependency(relation);
+ importedKeys.add(relation);
+ } while (!reflect.isClosed());
+ }
+ try (ResultSet reflect = analyzer.metadata.getExportedKeys(id.catalog,
id.schema, id.table)) {
+ if (reflect.next()) do {
+ final Relation relation = new
Relation(Relation.Direction.IMPORT, reflect);
+ analyzer.addDependency(relation);
+ exportedKeys.add(relation);
+ } while (!reflect.isClosed());
+ }
+ /*
+ * For each column in the table that is not a foreigner key, create an
AttributeType of the same name.
+ * The Java type is inferred from the SQL type, and the attribute
cardinality in inferred from the SQL
+ * nullability.
+ */
+ boolean hasGeometry = false;
+ final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+ try (ResultSet reflect = analyzer.metadata.getColumns(id.catalog,
schemaEsc, tableEsc, null)) {
+ while (reflect.next()) {
+ final String column =
reflect.getString(Reflection.COLUMN_NAME);
+ if (foreignerKeys.contains(column)) {
+ // TODO: create association.
+ continue;
+ }
+ final String typeName =
reflect.getString(Reflection.TYPE_NAME);
+ Class<?> type =
analyzer.functions.toJavaType(reflect.getInt(Reflection.DATA_TYPE), typeName);
+ if (type == null) {
+ analyzer.warning(Resources.Keys.UnknownType_1, typeName);
+ type = Object.class;
+ }
+ final AttributeTypeBuilder<?> atb =
ftb.addAttribute(type).setName(column);
+ final int size = reflect.getInt(Reflection.COLUMN_SIZE);
+ if (!reflect.wasNull()) {
+ atb.setMaximalLength(size);
+ }
+ final Boolean nullable =
SQLUtilities.parseBoolean(reflect.getString(Reflection.IS_NULLABLE));
+ if (nullable == null || nullable) {
+ atb.setMinimumOccurs(0);
+ }
+ /*
+ * Some columns have special purposes: components of primary
keys will be used for creating
+ * identifiers, some columns may contain a geometric object.
Adding a role on those columns
+ * may create synthetic columns, for example "sis:identifier".
+ */
+ if (primaryKeys.containsKey(column)) {
+ atb.addRole(AttributeRole.IDENTIFIER_COMPONENT);
+ if (primaryKeys.put(column,
SQLUtilities.parseBoolean(reflect.getString(Reflection.IS_AUTOINCREMENT))) !=
null) {
+ throw new
DataStoreContentException(Resources.format(Resources.Keys.DuplicatedEntity_2,
"Column", column));
+ }
+ }
+ if (Geometries.isKnownType(type)) {
+ final CoordinateReferenceSystem crs =
analyzer.functions.createGeometryCRS(reflect);
+ if (crs != null) {
+ atb.setCRS(crs);
+ }
+ if (!hasGeometry) {
+ hasGeometry = true;
+ atb.addRole(AttributeRole.DEFAULT_GEOMETRY);
+ }
+ }
+ }
+ }
+ /*
+ * Global information on the feature type (name, remarks).
+ * The remarks are opportunistically stored in id.name if available by
the caller.
+ * An empty string means that the caller has checked for remarks and
found none.
+ */
+ if (id.schema != null) {
+ ftb.setNameSpace(id.schema);
+ }
+ ftb.setName(id.table);
+ String remarks = id.name;
+ if (remarks == null) {
+ try (ResultSet reflect = analyzer.metadata.getTables(id.catalog,
schemaEsc, tableEsc, null)) {
+ while (reflect.next()) {
+ remarks = reflect.getString(Reflection.REMARKS);
+ if (remarks != null) {
+ remarks = remarks.trim();
+ if (remarks.isEmpty()) {
+ remarks = null;
+ } else break;
+ }
+ }
+ }
+ }
+ if (remarks != null && !remarks.isEmpty()) {
+ ftb.setDescription(remarks);
+ }
+ this.featureType = ftb.build();
+ this.primaryKeys = CollectionsExt.compact(primaryKeys);
+ this.importedKeys = CollectionsExt.compact(importedKeys);
+ this.exportedKeys = CollectionsExt.compact(exportedKeys);
}
/**
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/TableName.java
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/TableName.java
new file mode 100644
index 0000000..41dc99a
--- /dev/null
+++
b/storage/sis-sql/src/main/java/org/apache/sis/internal/sql/feature/TableName.java
@@ -0,0 +1,74 @@
+/*
+ * 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.internal.sql.feature;
+
+import java.util.Objects;
+
+
+/**
+ * A (catalog, schema, table) name tuple, which can be used as keys in hash
map.
+ * The {@link #name} field is for informative purpose only and ignored by this
class.
+ *
+ * @author Johann Sorel (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since 1.0
+ * @module
+ */
+class TableName extends MetaModel {
+ /**
+ * The catalog, schema and table name of a table.
+ * The table name is mandatory, but the schema and catalog names may be
null.
+ */
+ final String catalog, schema, table;
+
+ /**
+ * Creates a new tuple with the give names.
+ */
+ TableName(final String name, final String catalog, final String schema,
final String table) {
+ super(name);
+ this.catalog = catalog;
+ this.schema = schema;
+ this.table = table;
+ }
+
+ /**
+ * Returns {@code true} if the given object is a {@code Relation} with
equal table, schema and catalog names.
+ * All other properties (column names, action on delete…) are ignored;
this method is <strong>not</strong> for
+ * testing if two {@code Relation} are fully equal. The purpose of this
method is only to use {@code Relation}
+ * as keys in {@link Analyzer#dependencies} map for remembering full
coordinates of tables that may need to be
+ * analyzed later.
+ */
+ @Override
+ public final boolean equals(final Object obj) {
+ if (obj instanceof TableName) {
+ final TableName other = (TableName) obj;
+ return table.equals(other.table) && Objects.equals(schema,
other.schema) && Objects.equals(catalog, other.catalog);
+ // Other properties (columns, cascadeOnDelete) intentionally
omitted.
+ }
+ return false;
+ }
+
+ /**
+ * Computes a hash code from the catalog, schema and table names.
+ * See {@link #equals(Object)} for information about the purpose.
+ */
+ @Override
+ public final int hashCode() {
+ return table.hashCode() + 31*Objects.hashCode(schema) +
37*Objects.hashCode(catalog);
+ }
+}
diff --git
a/storage/sis-sql/src/main/java/org/apache/sis/storage/sql/SQLStore.java
b/storage/sis-sql/src/main/java/org/apache/sis/storage/sql/SQLStore.java
index 9eaaae7..540728f 100644
--- a/storage/sis-sql/src/main/java/org/apache/sis/storage/sql/SQLStore.java
+++ b/storage/sis-sql/src/main/java/org/apache/sis/storage/sql/SQLStore.java
@@ -18,7 +18,7 @@ package org.apache.sis.storage.sql;
import javax.sql.DataSource;
import org.apache.sis.internal.sql.feature.Database;
-import org.apache.sis.internal.sql.feature.QueryFeatureSet;
+import org.apache.sis.internal.sql.feature.QueriedFeatureSet;
import org.apache.sis.storage.DataStore;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.FeatureSet;
@@ -78,6 +78,6 @@ public abstract class SQLStore extends DataStore {
* @return the features obtained by the given given query.
*/
public FeatureSet query(final SQLQuery query) {
- return new QueryFeatureSet(this, model, query);
+ return new QueriedFeatureSet(this, model, query);
}
}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
index c2ae387..afecc91 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
@@ -92,7 +92,7 @@ public abstract class AbstractFeatureSet extends
AbstractResource implements Fea
}
}
// No geographic extent - see above javadoc.
- metadata.freeze();
+ metadata.apply(DefaultMetadata.State.FINAL);
this.metadata = metadata;
}
return metadata;