sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Dispatch most of 'Database' implementation into specialized classes (Analyzer, Table, Relation, QueriedFeatureSet).
Date Mon, 09 Jul 2018 16:52:28 GMT
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 <martin.desruisseaux@geomatys.com>
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;


Mime
View raw message