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: Resolve earlier the FeatureType in AssociationRole by creating Table dependencies earlier.
Date Fri, 13 Jul 2018 11:12:54 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 3b69577  Resolve earlier the FeatureType in AssociationRole by creating Table dependencies
earlier.
3b69577 is described below

commit 3b6957752392b1ce9074a4a09380fa971fa9115a
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Fri Jul 13 13:11:21 2018 +0200

    Resolve earlier the FeatureType in AssociationRole by creating Table dependencies earlier.
---
 .../org/apache/sis/util/iso/GlobalNameSpace.java   |  2 +-
 .../sis/util/resources/IndexedResourceBundle.java  |  2 +-
 .../resources/ResourceInternationalString.java     | 25 +++++-
 .../apache/sis/util/resources/package-info.java    |  2 +-
 .../apache/sis/internal/sql/feature/Analyzer.java  | 92 ++++++++++++----------
 .../apache/sis/internal/sql/feature/Database.java  | 45 +++++++----
 .../apache/sis/internal/sql/feature/Relation.java  | 22 ++++--
 .../apache/sis/internal/sql/feature/Resources.java |  2 +-
 .../org/apache/sis/internal/sql/feature/Table.java | 80 +++++++++++--------
 .../sis/internal/sql/feature/TableReference.java   | 21 +----
 .../apache/sis/storage/sql/SQLStoreProvider.java   |  8 +-
 11 files changed, 183 insertions(+), 118 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/GlobalNameSpace.java
b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/GlobalNameSpace.java
index cd717fa..4cb2046 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/GlobalNameSpace.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/GlobalNameSpace.java
@@ -20,7 +20,7 @@ import java.io.ObjectStreamException;
 
 
 /**
- * The global namespace. Only one instance of this class is allowed to exists. We do not
expose
+ * The global namespace. Only one instance of this class is allowed to exist. We do not expose
  * any global namespace in public API since ISO 19103 does not define them and users should
not
  * need to handle them explicitely.
  *
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
index 7890295..1ae2d76 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
@@ -395,7 +395,7 @@ public class IndexedResourceBundle extends ResourceBundle implements Localized
{
      * @param  arguments  the object to check.
      * @return {@code arguments} as an array, eventually with some elements replaced.
      */
-    private Object[] toArray(final Object arguments) {
+    final Object[] toArray(final Object arguments) {
         Object[] array;
         if (arguments instanceof Object[]) {
             array = (Object[]) arguments;
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 02ab357..e13d4b7 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
@@ -24,6 +24,8 @@ import java.io.IOException;
 import java.util.Locale;
 import java.util.MissingResourceException;
 import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.iso.AbstractInternationalString;
 
@@ -37,7 +39,7 @@ import org.apache.sis.util.iso.AbstractInternationalString;
  * This base class is immutable and thus inherently thread-safe.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -101,12 +103,31 @@ public abstract class ResourceInternationalString extends AbstractInternationalS
     /**
      * Returns the resource bundle for the given locale.
      *
-     * @param  locale  the locale for which to get the resource bundle.
+     * @param  locale  the locale for which to get the resource bundle, or {@code null} for
the default locale.
      * @return the resource bundle for the given locale.
      */
     protected abstract IndexedResourceBundle getBundle(final Locale locale);
 
     /**
+     * Converts this international string to a log record.
+     *
+     * @param  level  the logging level.
+     * @return a log record with the message of this international string.
+     *
+     * @since 1.0
+     */
+    public final LogRecord toLogRecord(final Level level) {
+        final LogRecord record = new LogRecord(level, getKeyConstants().getKeyName(key));
+        final IndexedResourceBundle resources = getBundle(null);
+        record.setResourceBundleName(resources.getClass().getName());
+        record.setResourceBundle(resources);
+        if (hasArguments) {
+            record.setParameters(resources.toArray(arguments));
+        }
+        return record;
+    }
+
+    /**
      * Returns a string in the specified locale.
      *
      * @param  locale  the desired locale for the string to be returned.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/package-info.java
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/package-info.java
index bd556a9..11bba0d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/package-info.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/package-info.java
@@ -83,7 +83,7 @@
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @see java.util.ResourceBundle
  * @see java.text.MessageFormat
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
index 28ea66e..154cfd5 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
@@ -18,22 +18,28 @@ package org.apache.sis.internal.sql.feature;
 
 import java.util.Set;
 import java.util.Map;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
-import java.util.LinkedHashMap;
-import java.util.Iterator;
+import java.util.Collection;
 import java.util.Locale;
 import java.util.Objects;
 import java.sql.SQLException;
 import java.sql.DatabaseMetaData;
+import java.util.Collections;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
 import org.opengis.util.NameSpace;
 import org.opengis.util.NameFactory;
 import org.opengis.util.GenericName;
-import org.opengis.util.InternationalString;
 import org.apache.sis.internal.metadata.sql.Dialect;
 import org.apache.sis.internal.system.DefaultFactories;
-import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.storage.sql.SQLStore;
 import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.util.resources.ResourceInternationalString;
+import org.apache.sis.util.resources.Errors;
 
 
 /**
@@ -78,23 +84,15 @@ final class Analyzer {
     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<TableReference,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.
+     * All tables created by analysis of the database structure. A {@code null} value means
that the table
+     * is in process of being created. This may happen if there is cyclic dependencies between
tables.
      */
-    private Iterator<Map.Entry<TableReference,Boolean>> depIter;
+    private final Map<GenericName,Table> tables;
 
     /**
      * Warnings found while analyzing a database structure. Duplicated warnings are omitted.
      */
-    private final Set<InternationalString> warnings;
+    private final Set<ResourceInternationalString> warnings;
 
     /**
      * Where to send warnings after we finished to collect them, or when reading the feature
instances.
@@ -153,7 +151,7 @@ final class Analyzer {
         /*
          * Information to be collected during table analysis.
          */
-        dependencies = new LinkedHashMap<>();
+        tables   = new HashMap<>();
         warnings = new LinkedHashSet<>();
     }
 
@@ -199,7 +197,8 @@ final class Analyzer {
     }
 
     /**
-     * Returns a namespace for the given catalog and schema names.
+     * Returns a namespace for the given catalog and schema names, or {@code null} if all
arguments are null.
+     * The namespace sets the name separator to {@code '.'} instead of {@code ':'}.
      */
     final NameSpace namespace(final String catalog, final String schema) {
         if (!Objects.equals(this.schema, schema) || !Objects.equals(this.catalog, catalog))
{
@@ -210,7 +209,7 @@ final class Analyzer {
                 } else {
                     name = nameFactory.createGenericName(null, catalog, schema);
                 }
-                namespace = nameFactory.createNameSpace(name, TableReference.NAMESPACE_PROPERTIES);
+                namespace = nameFactory.createNameSpace(name, Collections.singletonMap("separator",
"."));
             } else {
                 namespace = null;
             }
@@ -221,31 +220,28 @@ final class Analyzer {
     }
 
     /**
-     * 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 TableReference 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.
+     * Returns the feature of the given name if it exists, or creates it otherwise.
+     * This method may be invoked recursively if the table to create as a dependency
+     * to another table. If a cyclic dependency is detected, then this method return
+     * {@code null} for one of the tables.
+     *
+     * @param  id    identification of the table to create.
+     * @param  name  the value of {@code id.getName(analyzer)}
+     *               (as an argument for avoiding re-computation when already known by the
caller).
+     * @return the table, or {@code null} if there is a cyclic dependency and the table of
the given
+     *         name is already in process of being created.
      */
-    final TableReference nextDependency() {
-        if (depIter == null) {
-            depIter = dependencies.entrySet().iterator();
-        }
-        while (depIter.hasNext()) {
-            final Map.Entry<TableReference,Boolean> e = depIter.next();
-            if (!e.setValue(Boolean.TRUE)) {
-                return e.getKey();
+    final Table analyze(final TableReference id, final GenericName name) throws SQLException,
DataStoreException {
+        Table table = tables.get(name);
+        if (table == null && !tables.containsKey(name)) {
+            tables.put(name, null);                       // Mark the feature as in process
of being created.
+            table = new Table(this, id);
+            if (tables.put(name, table) != null) {
+                // Should never happen. If thrown, we have a bug (e.g. synchronization) in
this package.
+                throw new DataStoreException(Errors.format(Errors.Keys.UnexpectedChange_1,
name));
             }
         }
-        return null;
+        return table;
     }
 
     /**
@@ -257,4 +253,18 @@ final class Analyzer {
     final void warning(final short key, final Object argument) {
         warnings.add(Resources.formatInternational(key, argument));
     }
+
+    /**
+     * Invoked after we finished to create all tables. This method flush the warnings
+     * (omitting duplicated warnings), then returns all tables including dependencies.
+     */
+    final Collection<Table> finish() {
+        for (final ResourceInternationalString warning : warnings) {
+            final LogRecord record = warning.toLogRecord(Level.WARNING);
+            record.setSourceClassName(SQLStore.class.getName());
+            record.setSourceMethodName("components");                // Main public API trigging
the database analysis.
+            listeners.warning(record);
+        }
+        return tables.values();
+    }
 }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
index 9faadbd..8039938 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
@@ -19,6 +19,7 @@ package org.apache.sis.internal.sql.feature;
 import java.util.Set;
 import java.util.List;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.ArrayList;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
@@ -28,8 +29,8 @@ import org.opengis.util.GenericName;
 import org.apache.sis.internal.metadata.sql.Reflection;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.sql.SQLStore;
+import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.FeatureNaming;
@@ -39,6 +40,8 @@ import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.Debug;
 
+// Branch-dependent imports
+
 
 /**
  * Represent the structure of features in the database.
@@ -67,6 +70,7 @@ public final class Database {
 
     /**
      * All tables known to this {@code Database} in declaration order.
+     * This table contains only the tables specified at construction time, not the dependencies.
      */
     private final Table[] tables;
 
@@ -109,6 +113,7 @@ public final class Database {
     {
         final Analyzer analyzer = new Analyzer(connection.getMetaData(), listeners, store.getLocale());
         final String[] tableTypes = getTableTypes(analyzer.metadata);
+        final Set<TableReference> declared = new LinkedHashSet<>();
         for (final GenericName tableName : tableNames) {
             final String[] names = TableReference.splitName(tableName);
             try (ResultSet reflect = analyzer.metadata.getTables(names[2], names[1], names[0],
tableTypes)) {
@@ -117,26 +122,34 @@ public final class Database {
                     if (analyzer.isIgnoredTable(table)) {
                         continue;
                     }
-                    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 TableReference(
+                    declared.add(new TableReference(
                             reflect.getString(Reflection.TABLE_CAT),
                             reflect.getString(Reflection.TABLE_SCHEM),
-                            table, remarks));
+                            table, reflect.getString(Reflection.REMARKS)));
                 }
             }
         }
-        final List<Table> tableList = new ArrayList<>(tableNames.length);
-        tablesByNames = new FeatureNaming<>();
+        /*
+         * At this point we got the list of tables requested by the user. Now create the
Table objects for each
+         * specified name. During this iteration, we may discover new tables to analyze because
of dependencies
+         * (foreigner keys).
+         */
+        final List<Table> tableList;
+        tableList = new ArrayList<>(tableNames.length);
+        for (final TableReference reference : declared) {
+            // Adds only the table explicitly required by the user.
+            tableList.add(analyzer.analyze(reference, reference.getName(analyzer)));
+        }
+        /*
+         * At this point we finished to create the table explicitly requested by the users.
+         * Register all tables only at this point, because other tables (dependencies) may
+         * have been analyzed as a side-effect of above loop.
+         */
         boolean hasGeometry = false;
-        TableReference dependency;
-        while ((dependency = analyzer.nextDependency()) != null) {
-            final Table table = new Table(analyzer, dependency);
-            hasGeometry |= table.hasGeometry;
+        tablesByNames = new FeatureNaming<>();
+        for (final Table table : analyzer.finish()) {
             tablesByNames.add(store, table.getType().getName(), table);
-            if (!(dependency instanceof Relation)) {
-                tableList.add(table);                   // Adds only the table explicitly
required by the user.
-            }
+            hasGeometry |= table.hasGeometry;
         }
         this.tables = tableList.toArray(new Table[tableList.size()]);
         this.functions = analyzer.functions;
@@ -160,7 +173,8 @@ public final class Database {
     }
 
     /**
-     * Lists the tables in the given metadata.
+     * Stores information about tables in the given metadata.
+     * Only tables explicitely requested by the user are listed.
      *
      * @param  metadata  information about the database.
      * @param  builder   where to add information about the tables.
@@ -185,6 +199,7 @@ public final class Database {
 
     /**
      * Returns the table for the given name.
+     * The given name may be one of the tables specified at construction time, or one of
its dependencies.
      *
      * @param  store  the data store for which we are fetching a table. Used only in case
of error.
      * @param  name   name of the table to fetch.
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
index b5f22ba..b677771 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
@@ -16,9 +16,9 @@
  */
 package org.apache.sis.internal.sql.feature;
 
+import java.util.List;
 import java.util.Map;
 import java.util.LinkedHashMap;
-import java.util.Collection;
 import java.util.Objects;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -175,12 +175,24 @@ final class Relation extends TableReference {
     }
 
     /**
-     * Adds to the given collection the foreigner keys of the table that contains this relation.
+     * Adds to the given map 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.
+     * all the table foreigner keys. Some columns may be used in more than one relation.
+     *
+     * <p>This method puts {@code this} relation in the values of the map. However
if this relation describes a
+     * foreigner key using more than one column, then only one of the column will be associated
to {@code this}
+     * and all other columns will be associated to {@code null}. For example if a foreigner
key uses 3 columns,
+     * then we want to replace only one of those columns by an association, not create 3
identical associations.</p>
+     *
+     * @param  addTo  the map where to add the foreigner keys. After this method returns,
the set of map keys
+     *                will contain the column names and exactly one of the values will be
this relation.
      */
-    final void getForeignerKeys(final Collection<String> addTo) {
-        addTo.addAll(columns.values());
+    final void getForeignerKeys(final Map<String, List<Relation>> addTo) {
+        Relation rel = this;
+        for (final String column : columns.values()) {
+            CollectionsExt.addToMultiValuesMap(addTo, column, rel);
+            rel = null;     // Only the first column will be associated.
+        }
     }
 
     /**
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.java
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.java
index a56b2a9..c5b980f 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Resources.java
@@ -195,7 +195,7 @@ public final class Resources extends IndexedResourceBundle {
      * @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) {
+    public static ResourceInternationalString formatInternational(final short key, final
Object... args) {
         return new International(key, args);
     }
 }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
index 7e4332c..0705e97 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
@@ -18,18 +18,18 @@ 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.util.stream.Stream;
 import java.sql.DatabaseMetaData;
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.util.stream.Stream;
+import org.opengis.util.GenericName;
 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.AssociationRoleBuilder;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.internal.feature.Geometries;
@@ -96,10 +96,10 @@ final class Table extends AbstractFeatureSet {
      * 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.
      *
-     * @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.
+     * @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.
      */
-    Table(final Analyzer analyzer, final TableReference id) throws SQLException, DataStoreContentException
{
+    Table(final Analyzer analyzer, final TableReference id) throws SQLException, DataStoreException
{
         super(analyzer.listeners);
         final String tableEsc  = analyzer.escape(id.table);
         final String schemaEsc = analyzer.escape(id.schema);
@@ -134,20 +134,17 @@ final class Table extends AbstractFeatureSet {
          */
         final List<Relation> importedKeys = new ArrayList<>();
         final List<Relation> exportedKeys = new ArrayList<>();
-        final Set<String> foreignerKeys = new HashSet<>();
+        final Map<String, List<Relation>> foreignerKeys = new HashMap<>();
         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);
+                exportedKeys.add(new Relation(Relation.Direction.IMPORT, reflect));
             } while (!reflect.isClosed());
         }
         /*
@@ -156,13 +153,14 @@ final class Table extends AbstractFeatureSet {
          * nullability.
          */
         boolean hasGeometry = false;
-        final FeatureTypeBuilder ftb = new FeatureTypeBuilder(analyzer.nameFactory, analyzer.functions.library,
analyzer.locale);
+        final FeatureTypeBuilder feature = new FeatureTypeBuilder(analyzer.nameFactory, analyzer.functions.library,
analyzer.locale);
         try (ResultSet reflect = analyzer.metadata.getColumns(id.catalog, schemaEsc, tableEsc,
null)) {
             while (reflect.next()) {
-                final String column = reflect.getString(Reflection.COLUMN_NAME);
-                final boolean isPrimaryKey = primaryKeys.containsKey(column);
-                final boolean isForeignKey = foreignerKeys.contains(column);
-                if (isPrimaryKey | !isForeignKey) {
+                final String         column       = reflect.getString(Reflection.COLUMN_NAME);
+                final boolean        mandatory    = Boolean.FALSE.equals(SQLUtilities.parseBoolean(reflect.getString(Reflection.IS_NULLABLE)));
+                final boolean        isPrimaryKey = primaryKeys.containsKey(column);
+                final List<Relation> dependencies = foreignerKeys.get(column);
+                if (isPrimaryKey || dependencies == null) {
                     /*
                      * Foreign keys are excluded (they will be replaced by association),
except if the column is
                      * also a primary key. In the later case we need to keep that column
because it is needed for
@@ -174,16 +172,15 @@ final class Table extends AbstractFeatureSet {
                         analyzer.warning(Resources.Keys.UnknownType_1, typeName);
                         type = Object.class;
                     }
-                    final AttributeTypeBuilder<?> atb = ftb.addAttribute(type).setName(column);
+                    final AttributeTypeBuilder<?> attribute = feature.addAttribute(type).setName(column);
                     if (CharSequence.class.isAssignableFrom(type)) {
                         final int size = reflect.getInt(Reflection.COLUMN_SIZE);
                         if (!reflect.wasNull()) {
-                            atb.setMaximalLength(size);
+                            attribute.setMaximalLength(size);
                         }
                     }
-                    final Boolean nullable = SQLUtilities.parseBoolean(reflect.getString(Reflection.IS_NULLABLE));
-                    if (nullable == null || nullable) {
-                        atb.setMinimumOccurs(0);
+                    if (!mandatory) {
+                        attribute.setMinimumOccurs(0);
                     }
                     /*
                      * Some columns have special purposes: components of primary keys will
be used for creating
@@ -191,7 +188,7 @@ final class Table extends AbstractFeatureSet {
                      * may create synthetic columns, for example "sis:identifier".
                      */
                     if (isPrimaryKey) {
-                        atb.addRole(AttributeRole.IDENTIFIER_COMPONENT);
+                        attribute.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));
                         }
@@ -199,19 +196,37 @@ final class Table extends AbstractFeatureSet {
                     if (Geometries.isKnownType(type)) {
                         final CoordinateReferenceSystem crs = analyzer.functions.createGeometryCRS(reflect);
                         if (crs != null) {
-                            atb.setCRS(crs);
+                            attribute.setCRS(crs);
                         }
                         if (!hasGeometry) {
                             hasGeometry = true;
-                            atb.addRole(AttributeRole.DEFAULT_GEOMETRY);
+                            attribute.addRole(AttributeRole.DEFAULT_GEOMETRY);
                         }
                     }
                 }
                 /*
                  * If the column is a foreigner key, insert an association to another feature
instead.
+                 * If the foreigner key uses more than one column, only one of those columns
will become
+                 * an association and other columns will be omitted from the FeatureType
(but there will
+                 * still be used in SQL queries). Note that columns may be used by more than
one relation.
                  */
-                if (isForeignKey) {
-                    // TODO
+                if (dependencies != null) {
+                    for (final Relation dependency : dependencies) {
+                        if (dependency != null) {
+                            final AssociationRoleBuilder association;
+                            final GenericName name = dependency.getName(analyzer);
+                            final Table table = analyzer.analyze(dependency, name);
+                            if (table != null) {
+                                association = feature.addAssociation(table.featureType);
+                            } else {
+                                association = feature.addAssociation(name);        // May
happen in case of cyclic dependency.
+                            }
+                            association.setName(name);
+                            if (!mandatory) {
+                                association.setMinimumOccurs(0);
+                            }
+                        }
+                    }
                 }
             }
         }
@@ -220,9 +235,9 @@ final class Table extends AbstractFeatureSet {
          * The remarks are opportunistically stored in id.remarks if available by the caller.
          * An empty string means that the caller has checked for remarks and found none.
          */
-        ftb.setName(id.getName(analyzer));
+        feature.setName(id.getName(analyzer));
         String remarks = id.remarks;
-        if (remarks == null) {
+        if (id instanceof Relation) {
             try (ResultSet reflect = analyzer.metadata.getTables(id.catalog, schemaEsc, tableEsc,
null)) {
                 while (reflect.next()) {
                     remarks = reflect.getString(Reflection.REMARKS);
@@ -235,10 +250,10 @@ final class Table extends AbstractFeatureSet {
                 }
             }
         }
-        if (remarks != null && !remarks.isEmpty()) {
-            ftb.setDescription(remarks);
+        if (remarks != null && !(remarks = remarks.trim()).isEmpty()) {
+            feature.setDescription(remarks);
         }
-        this.featureType  = ftb.build();
+        this.featureType  = feature.build();
         this.primaryKeys  = CollectionsExt.compact(primaryKeys);
         this.importedKeys = CollectionsExt.compact(importedKeys);
         this.exportedKeys = CollectionsExt.compact(exportedKeys);
@@ -295,7 +310,8 @@ final class Table extends AbstractFeatureSet {
 
     /**
      * Returns the number of rows, or -1 if unknown. Note that some database drivers returns
0,
-     * so it is better to consider 0 as "unknown" too.
+     * so it is better to consider 0 as "unknown" too. We do not cache this count because
it may
+     * change at any time.
      *
      * @param  metadata     information about the database.
      * @param  approximate  whether approximative or outdated values are acceptable.
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableReference.java
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableReference.java
index 97dcac7..6160d4e 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableReference.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableReference.java
@@ -16,8 +16,6 @@
  */
 package org.apache.sis.internal.sql.feature;
 
-import java.util.Map;
-import java.util.HashMap;
 import java.util.Objects;
 import java.util.function.Consumer;
 import org.opengis.util.LocalName;
@@ -39,18 +37,7 @@ import org.apache.sis.util.Debug;
  * @since   1.0
  * @module
  */
-public class TableReference {
-    /**
-     * Properties to give to {@code NameFactory.createNameSpace(…)} for specifying the
separators.
-     */
-    public static final Map<String,String> NAMESPACE_PROPERTIES;
-    static {
-        final Map<String,String> properties = new HashMap<>(4);     // TODO:
use Map.of with JDK9.
-        properties.put("separator",      ".");
-        properties.put("separator.head", ":");
-        NAMESPACE_PROPERTIES = properties;
-    }
-
+class TableReference {
     /**
      * The catalog, schema and table name of a table.
      * The table name is mandatory, but the schema and catalog names may be null.
@@ -78,7 +65,7 @@ public class TableReference {
      */
     static String[] splitName(final GenericName name) {
         String[] parts = name.getParsedNames().stream().map(LocalName::toString).toArray(String[]::new);
-        ArraysExt.reverse(parts);               // Reorganize in (catalog, schemaPattern,
tablePattern) order.
+        ArraysExt.reverse(parts);               // Reorganize in (table, schema, catalog)
order.
         return ArraysExt.resize(parts, 3);      // Pad with null values if necessary.
     }
 
@@ -93,8 +80,8 @@ public class TableReference {
      * Returns {@code true} if the given object is a {@code TableReference} with equal table,
schema and catalog names.
      * All other properties that may be defined in subclasses (column names, action on delete,
etc.) are ignored; this
      * method is <strong>not</strong> for testing if two {@link Relation} are
fully equal. The purpose of this method
-     * is only to use {@code TableReference} as keys in {@link Analyzer#dependencies} map
for remembering full
-     * coordinates of tables that may need to be analyzed later.
+     * is only to use {@code TableReference} as keys in a {@code HashSet} for remembering
full coordinates of tables
+     * that may need to be analyzed later.
      *
      * @return whether the given object is another {@code TableReference} for the same table.
      */
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStoreProvider.java
b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStoreProvider.java
index 24ebe94..a03a1b2 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStoreProvider.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStoreProvider.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sis.storage.sql;
 
+import java.util.Map;
+import java.util.HashMap;
 import java.sql.Connection;
 import java.sql.SQLException;
 import javax.sql.DataSource;
@@ -28,7 +30,6 @@ import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterNotFoundException;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.sql.feature.Resources;
-import org.apache.sis.internal.sql.feature.TableReference;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.DataStoreException;
@@ -143,7 +144,10 @@ public class SQLStoreProvider extends DataStoreProvider {
         final NameFactory factory = DefaultFactories.forBuildin(NameFactory.class);
         NameSpace ns = tableNS;
         if (ns == null) {
-            tableNS = ns = factory.createNameSpace(factory.createLocalName(null, "JDBC"),
TableReference.NAMESPACE_PROPERTIES);
+            final Map<String,String> properties = new HashMap<>(4);     // TODO:
use Map.of with JDK9.
+            properties.put("separator",      ".");
+            properties.put("separator.head", ":");
+            tableNS = ns = factory.createNameSpace(factory.createLocalName(null, "database"),
properties);
         }
         return factory.createGenericName(ns, names);
     }


Mime
View raw message