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: Complete the construction of FeatureType from database structure (omitting geometric objects for now) and enable tests.
Date Fri, 13 Jul 2018 16:51:51 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 c643a85  Complete the construction of FeatureType from database structure (omitting
geometric objects for now) and enable tests.
c643a85 is described below

commit c643a85a6a1e43b4fd6e78b65219fb56812e23d1
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Fri Jul 13 18:51:23 2018 +0200

    Complete the construction of FeatureType from database structure (omitting geometric objects
for now) and enable tests.
---
 .../java/org/apache/sis/feature/FeatureFormat.java |   9 +-
 .../apache/sis/feature/StringJoinOperation.java    |   3 +-
 .../feature/builder/AssociationRoleBuilder.java    |   2 +-
 .../sis/feature/builder/AttributeTypeBuilder.java  |   4 +-
 .../sis/feature/builder/FeatureTypeBuilder.java    |  20 +++-
 .../sis/feature/builder/PropertyTypeBuilder.java   |   2 +-
 .../apache/sis/feature/builder/TypeBuilder.java    |   9 +-
 .../apache/sis/internal/sql/feature/Analyzer.java  |   2 +-
 .../apache/sis/internal/sql/feature/Database.java  |   7 +-
 .../apache/sis/internal/sql/feature/Relation.java  |  32 +++---
 .../org/apache/sis/internal/sql/feature/Table.java | 119 ++++++++++++++++-----
 .../sis/internal/sql/feature/TableReference.java   |  26 +++--
 .../org/apache/sis/storage/sql/SQLStoreTest.java   |  30 +++++-
 .../org/apache/sis/test/suite/SQLTestSuite.java    |  40 +++++++
 14 files changed, 229 insertions(+), 76 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java
index 17c9df6..f459ec2 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java
@@ -88,7 +88,7 @@ import org.opengis.feature.Operation;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.5
  * @module
  */
@@ -358,6 +358,13 @@ public class FeatureFormat extends TabularFormat<Object> {
                 toAppendTo.append(separator).append(toString(parent.getName()));
                 separator = SEPARATOR;
             }
+            final InternationalString definition = featureType.getDefinition();
+            if (definition != null) {
+                String text = definition.toString(displayLocale);
+                if (text != null && !(text = text.trim()).isEmpty()) {
+                    toAppendTo.append(getLineSeparator()).append(text);
+                }
+            }
         }
         toAppendTo.append(getLineSeparator());
         /*
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java
b/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java
index 6a4fa83..29bf49d 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java
@@ -524,10 +524,11 @@ final class StringJoinOperation extends AbstractOperation {
      */
     @Override
     void formatResultFormula(final Appendable buffer) throws IOException {
+        final String escape = ESCAPE + delimiter;
         if (prefix != null) buffer.append(prefix);
         for (int i=0; i<attributeNames.length; i++) {
             if (i != 0) buffer.append(delimiter);
-            buffer.append(attributeNames[i]);
+            buffer.append(attributeNames[i].replace(delimiter, escape));
         }
         if (suffix != null) buffer.append(suffix);
     }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java
b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java
index 164c31b..92b9cca 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java
@@ -197,7 +197,7 @@ public final class AssociationRoleBuilder extends PropertyTypeBuilder
{
     /**
      * Sets the maximum number of associations. If the given number is less than the
      * {@linkplain #getMinimumOccurs() minimal number} of associations, than the minimum
-     * is also set to that value.
+     * is also set to that value. {@link Integer#MAX_VALUE} means that there is no maximum.
      *
      * @param  occurs  the new maximum number of associations.
      * @return {@code this} for allowing method calls chaining.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
index 2b9eab2..54c3dda 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
@@ -252,7 +252,7 @@ public final class AttributeTypeBuilder<V> extends PropertyTypeBuilder
{
     /**
      * Sets the maximum number of attribute values. If the given number is less than the
      * {@linkplain #getMinimumOccurs() minimal number} of attribute values, than the minimum
-     * is also set to that value.
+     * is also set to that value. {@link Integer#MAX_VALUE} means that there is no maximum.
      *
      * @param  occurs  the new maximum number of attribute values.
      * @return {@code this} for allowing method calls chaining.
@@ -461,7 +461,7 @@ public final class AttributeTypeBuilder<V> extends PropertyTypeBuilder
{
      * @see #characteristics()
      */
     public CharacteristicTypeBuilder<?> getCharacteristic(final String name) {
-        return forName(characteristics, name);
+        return forName(characteristics, name, true);
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java
b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java
index 4c13a5d..04fb43b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java
@@ -101,7 +101,7 @@ import org.opengis.feature.Operation;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @see org.apache.sis.parameter.ParameterBuilder
  *
@@ -353,7 +353,7 @@ public class FeatureTypeBuilder extends TypeBuilder {
          */
         if (!propertyRoles.isEmpty()) {
             for (final Map.Entry<String,Set<AttributeRole>> entry : propertyRoles.entrySet())
{
-                final PropertyTypeBuilder property = forName(properties, entry.getKey());
+                final PropertyTypeBuilder property = forName(properties, entry.getKey(),
true);
                 if (property instanceof AttributeTypeBuilder<?>) {
                     ((AttributeTypeBuilder<?>) property).roles().addAll(entry.getValue());
                 }
@@ -627,6 +627,20 @@ public class FeatureTypeBuilder extends TypeBuilder {
     }
 
     /**
+     * Returns {@code true} if a property of the given name is defined or if the given name
is ambiguous.
+     * Invoking this method is equivalent to testing if {@code getProperty(name) != null}
except that this
+     * method does not throw exception if the given name is ambiguous.
+     *
+     * @param  name  the name to test.
+     * @return {@code true} if the given name is used by another property or is ambiguous.
+     *
+     * @since 1.0
+     */
+    public boolean isNameUsed(final String name) {
+        return forName(properties, name, false) != null;
+    }
+
+    /**
      * Returns the builder for the property of the given name. The given name does not need
to contains all elements
      * of a {@link org.opengis.util.ScopedName}; it is okay to specify only the tip (for
example {@code "myName"}
      * instead of {@code "myScope:myName"}) provided that ignoring the name head does not
create ambiguity.
@@ -638,7 +652,7 @@ public class FeatureTypeBuilder extends TypeBuilder {
      * @see #addProperty(PropertyType)
      */
     public PropertyTypeBuilder getProperty(final String name) {
-        return forName(properties, name);
+        return forName(properties, name, true);
     }
 
     /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/PropertyTypeBuilder.java
b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/PropertyTypeBuilder.java
index 9517cbd..40515b1 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/PropertyTypeBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/PropertyTypeBuilder.java
@@ -192,7 +192,7 @@ public abstract class PropertyTypeBuilder extends TypeBuilder {
     /**
      * Sets the maximum number of property values. If the given number is less than the
      * {@linkplain #getMinimumOccurs() minimal number} of property values, than the minimum
-     * is also set to that value.
+     * is also set to that value. {@link Integer#MAX_VALUE} means that there is no maximum.
      *
      * @param  occurs  the new maximum number of property values.
      * @return {@code this} for allowing method calls chaining.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/TypeBuilder.java
b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/TypeBuilder.java
index 3dffc1b..ae27b7d 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/TypeBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/TypeBuilder.java
@@ -428,13 +428,14 @@ public abstract class TypeBuilder implements Localized {
      * all elements of a {@link ScopedName}; it can be only the tip (for example {@code "myName"}
instead
      * of {@code "myScope:myName"}) provided that ignoring the name head does not create
ambiguity.
      *
-     * @param  types  the collection where to search for an element of the given name.
-     * @param  name   name of the element to search.
+     * @param  types         the collection where to search for an element of the given name.
+     * @param  name          name of the element to search.
+     * @param  nonAmbiguous  whether to throw an exception if the given name is ambiguous.
      * @return element of the given name, or {@code null} if none were found.
      * @throws IllegalArgumentException if the given name is ambiguous.
      */
     @SuppressWarnings("null")
-    final <E extends TypeBuilder> E forName(final List<E> types, final String
name) {
+    final <E extends TypeBuilder> E forName(final List<E> types, final String
name, final boolean nonAmbiguous) {
         E best      = null;                     // Best type found so far.
         E ambiguity = null;                     // If two types are found at the same depth,
the other type.
         int depth   = Integer.MAX_VALUE;        // Number of path elements that we had to
ignore in the GenericName.
@@ -457,7 +458,7 @@ public abstract class TypeBuilder implements Localized {
                 candidate = ((ScopedName) candidate).tail();
             }
         }
-        if (ambiguity != null) {
+        if (ambiguity != null && nonAmbiguous) {
             throw new PropertyNotFoundException(errors().getString(
                     Errors.Keys.AmbiguousName_3, best.getName(), ambiguity.getName(), name));
         }
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 154cfd5..b4ed102 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
@@ -231,7 +231,7 @@ final class Analyzer {
      * @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 Table analyze(final TableReference id, final GenericName name) throws SQLException,
DataStoreException {
+    final Table table(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.
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 8039938..cf03790 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
@@ -138,7 +138,7 @@ public final class Database {
         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)));
+            tableList.add(analyzer.table(reference, reference.getName(analyzer)));
         }
         /*
          * At this point we finished to create the table explicitly requested by the users.
@@ -216,8 +216,7 @@ public final class Database {
      * @param  parent  the parent node where to add the tree representation.
      */
     @Debug
-    final void appendTo(TreeTable.Node parent) {
-        parent = Relation.newChild(parent, "Database");
+    final void appendTo(final TreeTable.Node parent) {
         for (final Table child : tables) {
             child.appendTo(parent);
         }
@@ -232,6 +231,6 @@ public final class Database {
      */
     @Override
     public String toString() {
-        return TableReference.toString((n) -> appendTo(n));
+        return TableReference.toString(this, (n) -> appendTo(n));
     }
 }
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 b677771..d257b6f 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
@@ -62,8 +62,8 @@ final class Relation extends TableReference {
          *
          * @see DatabaseMetaData#getImportedKeys(String, String, String)
          */
-        IMPORT(Reflection.PK_NAME, Reflection.PKTABLE_CAT, Reflection.PKTABLE_SCHEM,
-               Reflection.PKTABLE_NAME, Reflection.PKCOLUMN_NAME, Reflection.FKCOLUMN_NAME),
+        IMPORT(Reflection.PKTABLE_CAT, Reflection.PKTABLE_SCHEM, Reflection.PKTABLE_NAME,
+               Reflection.PKCOLUMN_NAME, Reflection.FKCOLUMN_NAME),
 
         /**
          * Foreigner keys of other tables are referencing the primary keys of the table containing
the {@code Relation}.
@@ -71,23 +71,17 @@ final class Relation extends TableReference {
          *
          * @see DatabaseMetaData#getExportedKeys(String, String, String)
          */
-        EXPORT(Reflection.FK_NAME, Reflection.FKTABLE_CAT, Reflection.FKTABLE_SCHEM,
-               Reflection.FKTABLE_NAME, Reflection.FKCOLUMN_NAME, Reflection.PKCOLUMN_NAME);
+        EXPORT(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​(…).
+         * 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}.
-         */
-        final String name;
-
-        /**
          * The database {@link Reflection} key to use for fetching the name of other table
column.
          * That column is part of a primary key if the direction is {@link #IMPORT}, or part
of a
          * foreigner key if the direction is {@link #EXPORT}.
@@ -104,10 +98,9 @@ final class Relation extends TableReference {
         /**
          * Creates a new {@code Direction} enumeration value.
          */
-        private Direction(final String name, final String catalog, final String schema,
-                          final String table, final String column, final String containerColumn)
+        private Direction(final String catalog, final String schema, final String table,
+                          final String column, final String containerColumn)
         {
-            this.name            = name;
             this.catalog         = catalog;
             this.schema          = schema;
             this.table           = table;
@@ -150,7 +143,7 @@ final class Relation extends TableReference {
         super(reflect.getString(dir.catalog),
               reflect.getString(dir.schema),
               reflect.getString(dir.table),
-              reflect.getString(dir.name));
+              reflect.getString(Reflection.FK_NAME));
 
         final Map<String,String> m = new LinkedHashMap<>();
         boolean cascade = false;
@@ -199,12 +192,13 @@ final class Relation extends TableReference {
      * Creates a tree representation of this relation for debugging purpose.
      *
      * @param  parent  the parent node where to add the tree representation.
+     * @param  arrow   the symbol to use for relating the columns of two tables in a foreigner
key.
      */
     @Debug
-    void appendTo(final TreeTable.Node parent) {
-        final TreeTable.Node node = newChild(parent, remarks);
+    void appendTo(final TreeTable.Node parent, final String arrow) {
+        final TreeTable.Node node = newChild(parent, freeText);
         for (final Map.Entry<String,String> e : columns.entrySet()) {
-            newChild(node, e.getValue() + " → " + e.getKey());
+            newChild(node, e.getValue() + arrow + e.getKey());
         }
     }
 
@@ -215,6 +209,6 @@ final class Relation extends TableReference {
      */
     @Override
     public String toString() {
-        return toString((n) -> appendTo(n));
+        return toString(this, (n) -> appendTo(n, " — "));
     }
 }
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 0705e97..b147315 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
@@ -39,13 +39,15 @@ import org.apache.sis.internal.storage.AbstractFeatureSet;
 import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.Debug;
 
 // Branch-dependent imports
+import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
 import org.opengis.feature.AttributeType;
 import org.opengis.feature.FeatureAssociationRole;
-import org.opengis.feature.Feature;
 
 
 /**
@@ -144,7 +146,7 @@ final class Table extends AbstractFeatureSet {
         }
         try (ResultSet reflect = analyzer.metadata.getExportedKeys(id.catalog, id.schema,
id.table)) {
             if (reflect.next()) do {
-                exportedKeys.add(new Relation(Relation.Direction.IMPORT, reflect));
+                exportedKeys.add(new Relation(Relation.Direction.EXPORT, reflect));
             } while (!reflect.isClosed());
         }
         /*
@@ -153,6 +155,7 @@ final class Table extends AbstractFeatureSet {
          * nullability.
          */
         boolean hasGeometry = false;
+        int startWithLowerCase = 0;
         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()) {
@@ -160,19 +163,32 @@ final class Table extends AbstractFeatureSet {
                 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);
+                /*
+                 * Heuristic rule for determining if the column names starts with lower case
or upper case.
+                 * Words that are all upper-case are ignored on the assumption that they
are acronyms.
+                 */
+                if (!column.isEmpty()) {
+                    final int firstLetter = column.codePointAt(0);
+                    if (Character.isLowerCase(firstLetter)) {
+                        startWithLowerCase++;
+                    } else if (Character.isUpperCase(firstLetter) && !CharSequences.isUpperCase(column))
{
+                        startWithLowerCase--;
+                    }
+                }
+                /*
+                 * Add the column as an attribute. Foreign keys are excluded (they will be
replaced by associations),
+                 * except if the column is also a primary key. In the later case we need
to keep that column because
+                 * it is needed for building the feature identifier.
+                 */
+                AttributeTypeBuilder<?> attribute = null;
                 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
-                     * building the feature identifier.
-                     */
                     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<?> attribute = feature.addAttribute(type).setName(column);
+                    attribute = feature.addAttribute(type).setName(column);
                     if (CharSequence.class.isAssignableFrom(type)) {
                         final int size = reflect.getInt(Reflection.COLUMN_SIZE);
                         if (!reflect.wasNull()) {
@@ -211,32 +227,77 @@ final class Table extends AbstractFeatureSet {
                  * still be used in SQL queries). Note that columns may be used by more than
one relation.
                  */
                 if (dependencies != null) {
+                    int count = 0;
                     for (final Relation dependency : dependencies) {
                         if (dependency != null) {
+                            final GenericName typeName = dependency.getName(analyzer);
+                            final Table table = analyzer.table(dependency, typeName);
                             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 = feature.addAssociation(typeName);     // May
happen in case of cyclic dependency.
                             }
-                            association.setName(name);
                             if (!mandatory) {
                                 association.setMinimumOccurs(0);
                             }
+                            /*
+                             * If the column is also used in the primary key, then we have
a name clash.
+                             * Rename the primary key column with the addition of a "pk:"
scope. We rename
+                             * the primary key column instead than this association because
the primary key
+                             * column should rarely be used directly.
+                             */
+                            if (attribute != null) {
+                                attribute.setName(analyzer.nameFactory.createGenericName(null,
"pk", column));
+                                attribute = null;
+                            }
+                            association.setName((count == 0) ? column : column + '-' + count);
+                            count++;
                         }
                     }
                 }
             }
         }
         /*
+         * Add the associations created by other tables having foreigner keys to this table.
+         * We infer the column name from the target type. We may have a name clash with other
+         * columns, in which case an arbitrary name change is applied.
+         */
+        int count = 0;
+        for (final Relation dependency : exportedKeys) {
+            if (dependency != null) {
+                final GenericName typeName = dependency.getName(analyzer);
+                String name = typeName.tip().toString();
+                if (startWithLowerCase > 0) {
+                    final CharSequence words = CharSequences.camelCaseToWords(name, true);
+                    final int first = Character.codePointAt(words, 0);
+                    name = new StringBuilder(words.length())
+                            .appendCodePoint(Character.toLowerCase(first))
+                            .append(words, Character.charCount(first), words.length())
+                            .toString();
+                }
+                final String base = name;
+                while (feature.isNameUsed(name)) {
+                    name = base + '-' + ++count;
+                }
+                final Table table = analyzer.table(dependency, typeName);
+                final AssociationRoleBuilder association;
+                if (table != null) {
+                    association = feature.addAssociation(table.featureType);
+                } else {
+                    association = feature.addAssociation(typeName);     // May happen in
case of cyclic dependency.
+                }
+                association.setName(name)
+                           .setMinimumOccurs(0)
+                           .setMaximumOccurs(Integer.MAX_VALUE);
+            }
+        }
+        /*
          * Global information on the feature type (name, remarks).
-         * 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.
+         * The remarks are opportunistically stored in id.freeText if known by the caller.
          */
         feature.setName(id.getName(analyzer));
-        String remarks = id.remarks;
+        String remarks = id.freeText;
         if (id instanceof Relation) {
             try (ResultSet reflect = analyzer.metadata.getTables(id.catalog, schemaEsc, tableEsc,
null)) {
                 while (reflect.next()) {
@@ -250,8 +311,8 @@ final class Table extends AbstractFeatureSet {
                 }
             }
         }
-        if (remarks != null && !(remarks = remarks.trim()).isEmpty()) {
-            feature.setDescription(remarks);
+        if (remarks != null) {
+            feature.setDefinition(remarks);
         }
         this.featureType  = feature.build();
         this.primaryKeys  = CollectionsExt.compact(primaryKeys);
@@ -261,20 +322,17 @@ final class Table extends AbstractFeatureSet {
     }
 
     /**
-     * Appends all children to the given parent. The children are added under a node of the
given name.
+     * Appends all children to the given parent. The children are added under the given node.
      * If the children collection is empty, then this method does nothing.
      *
      * @param  parent    the node where to add children.
-     * @param  name      the name of a node to insert between the parent and the children.
      * @param  children  the children to add, or an empty collection if none.
+     * @param  arrow     the symbol to use for relating the columns of two tables in a foreigner
key.
      */
     @Debug
-    private static void appendAll(TreeTable.Node parent, final String name, final Collection<Relation>
children) {
-        if (!children.isEmpty()) {
-            parent = Relation.newChild(parent, name);
-            for (final Relation child : children) {
-                child.appendTo(parent);
-            }
+    private static void appendAll(final TreeTable.Node parent, final Collection<Relation>
children, final String arrow) {
+        for (final Relation child : children) {
+            child.appendTo(parent, arrow);
         }
     }
 
@@ -286,8 +344,13 @@ final class Table extends AbstractFeatureSet {
     @Debug
     final void appendTo(TreeTable.Node parent) {
         parent = Relation.newChild(parent, featureType.getName().toString());
-        appendAll(parent, "Imported Keys", importedKeys);
-        appendAll(parent, "Exported Keys", exportedKeys);
+        for (PropertyType p : featureType.getProperties(false)) {
+            if (p instanceof AttributeType) {
+                TableReference.newChild(parent, p.getName().tip().toString());
+            }
+        }
+        appendAll(parent, importedKeys,  " → ");
+        appendAll(parent, exportedKeys,  " ← ");
     }
 
     /**
@@ -297,7 +360,7 @@ final class Table extends AbstractFeatureSet {
      */
     @Override
     public String toString() {
-        return TableReference.toString((n) -> appendTo(n));
+        return TableReference.toString(this, (n) -> appendTo(n));
     }
 
     /**
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 6160d4e..67d05f6 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
@@ -47,16 +47,22 @@ class TableReference {
     /**
      * Ignored by this class; reserved for caller and subclasses usage.
      */
-    final String remarks;
+    final String freeText;
 
     /**
      * Creates a new tuple with the give names.
      */
-    TableReference(final String catalog, final String schema, final String table, final String
remarks) {
-        this.catalog = catalog;
-        this.schema  = schema;
-        this.table   = table;
-        this.remarks = remarks;
+    TableReference(final String catalog, final String schema, final String table, String
freeText) {
+        if (freeText != null) {
+            freeText = freeText.trim();
+            if (freeText.isEmpty()) {
+                freeText = null;
+            }
+        }
+        this.catalog  = catalog;
+        this.schema   = schema;
+        this.table    = table;
+        this.freeText = freeText;
     }
 
     /**
@@ -90,7 +96,7 @@ class TableReference {
         if (obj instanceof TableReference) {
             final TableReference other = (TableReference) obj;
             return table.equals(other.table) && Objects.equals(schema, other.schema)
&& Objects.equals(catalog, other.catalog);
-            // Other properties (remarks, columns, cascadeOnDelete) intentionally omitted.
+            // Other properties (freeText, columns, cascadeOnDelete) intentionally omitted.
         }
         return false;
     }
@@ -126,9 +132,11 @@ class TableReference {
      * can be printed to the {@linkplain System#out standard output stream} (for example)
      * if the output device uses a monospaced font and supports Unicode.
      */
-    static String toString(final Consumer<TreeTable.Node> appender) {
+    static String toString(final Object owner, final Consumer<TreeTable.Node> appender)
{
         final DefaultTreeTable table = new DefaultTreeTable(TableColumn.NAME);
-        appender.accept(table.getRoot());
+        final TreeTable.Node root = table.getRoot();
+        root.setValue(TableColumn.NAME, owner.getClass().getSimpleName());
+        appender.accept(root);
         return table.toString();
     }
 
diff --git a/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
b/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
index 834467b..8451362 100644
--- a/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
+++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
@@ -22,6 +22,13 @@ import org.apache.sis.test.sql.TestDatabase;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
+import static org.junit.Assert.*;
+
+// Branch-dependent imports
+import org.opengis.feature.PropertyType;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.FeatureAssociationRole;
+
 
 /**
  * Tests {@link SQLStore}.
@@ -44,9 +51,28 @@ public final strictfp class SQLStoreTest extends TestCase {
             try (SQLStore store = new SQLStore(new SQLStoreProvider(), new StorageConnector(tmp.source),
                     SQLStoreProvider.createTableName(null, "features", "Cities")))
             {
-                System.out.println(store.getMetadata());
                 final FeatureSet cities = (FeatureSet) store.findResource("Cities");
-                System.out.println(cities.getType());
+                final String[] expectedNames = {"sis:identifier", "pk:country", "country",
  "native_name", "translation", "population",  "parks"};
+                final Object[] expectedTypes = {null,             String.class, "Countries",
String.class,  String.class,  Integer.class, "Parks"};
+                int i = 0;
+                for (PropertyType pt : cities.getType().getProperties(false)) {
+                    assertEquals("name", expectedNames[i], pt.getName().toString());
+                    final Object expectedType = expectedTypes[i];
+                    if (expectedType != null) {
+                        final String label;
+                        final Object value;
+                        if (expectedType instanceof Class<?>) {
+                            label = "attribute type";
+                            value = ((AttributeType<?>) pt).getValueClass();
+                        } else {
+                            label = "association type";
+                            value = ((FeatureAssociationRole) pt).getValueType().getName().toString();
+                        }
+                        assertEquals(label, expectedType, value);
+                    }
+                    i++;
+                }
+                assertEquals("count", expectedNames.length, i);
             }
         }
     }
diff --git a/storage/sis-sqlstore/src/test/java/org/apache/sis/test/suite/SQLTestSuite.java
b/storage/sis-sqlstore/src/test/java/org/apache/sis/test/suite/SQLTestSuite.java
new file mode 100644
index 0000000..f84182a
--- /dev/null
+++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/test/suite/SQLTestSuite.java
@@ -0,0 +1,40 @@
+/*
+ * 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.test.suite;
+
+import org.apache.sis.test.TestSuite;
+import org.junit.runners.Suite;
+import org.junit.BeforeClass;
+
+
+/**
+ * All tests from the {@code sis-sqlstore} module, in approximative dependency order.
+ */
+@Suite.SuiteClasses({
+    org.apache.sis.storage.sql.SQLStoreTest.class
+})
+public final strictfp class SQLTestSuite extends TestSuite {
+    /**
+     * Verifies the list of tests before to run the suite.
+     * See {@link #verifyTestList(Class, Class[])} for more information.
+     */
+    @BeforeClass
+    public static void verifyTestList() {
+        assertNoMissingTest(SQLTestSuite.class);
+        verifyTestList(SQLTestSuite.class);
+    }
+}


Mime
View raw message