This is an automated email from the ASF dual-hosted git repository.

amanin pushed a commit to branch refactor/sql-store
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 92b4b0ec384ca4305025ea651bb86eb3764a6e32
Author: Alexis Manin <[email protected]>
AuthorDate: Fri Sep 20 19:01:03 2019 +0200

    WIP(SQL-Store): refactor feature-type building to factorize query and table 
discovery.
---
 .../apache/sis/internal/sql/feature/Analyzer.java  | 348 ++++++++++++++++++---
 .../apache/sis/internal/sql/feature/Features.java  |   9 +-
 .../sis/internal/sql/feature/QueryFeatureSet.java  |   7 +-
 .../org/apache/sis/internal/sql/feature/Table.java | 209 +------------
 .../sql/feature/{ => metamodel}/ColumnRef.java     |  15 +-
 .../internal/sql/feature/metamodel/PrimaryKey.java |  48 +++
 6 files changed, 372 insertions(+), 264 deletions(-)

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 b221086..c91ddbb 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
@@ -17,37 +17,39 @@
 package org.apache.sis.internal.sql.feature;
 
 import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
+import java.util.*;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import javax.sql.DataSource;
 
+import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.util.GenericName;
 import org.opengis.util.NameFactory;
 import org.opengis.util.NameSpace;
 
+import org.apache.sis.feature.builder.AssociationRoleBuilder;
+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.internal.metadata.sql.Dialect;
 import org.apache.sis.internal.metadata.sql.Reflection;
 import org.apache.sis.internal.metadata.sql.SQLUtilities;
+import org.apache.sis.internal.sql.feature.metamodel.ColumnRef;
+import org.apache.sis.internal.sql.feature.metamodel.PrimaryKey;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.InternalDataStoreException;
 import org.apache.sis.storage.sql.SQLStore;
+import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.util.resources.ResourceInternationalString;
 
@@ -303,6 +305,10 @@ final class Analyzer {
         warnings.add(Resources.formatInternational(key, argument));
     }
 
+    private PropertyAdapter analyze(SQLColumn target) {
+        throw new UnsupportedOperationException();
+    }
+
     /**
      * Invoked after we finished to create all tables. This method flush the 
warnings
      * (omitting duplicated warnings), then returns all tables including 
dependencies.
@@ -320,31 +326,141 @@ final class Analyzer {
         return tables.values();
     }
 
-    public FeatureType buildFeatureType(final ResultSetMetaData target) {
-        throw new UnsupportedOperationException("");
+    public FeatureType buildFeatureType(final TableReference table, final 
TableReference importedBy) throws SQLException {
+        try (TableMetadata metadata = new TableMetadata(table, importedBy)) {
+            return build(metadata);
+        }
+    }
+
+    public FeatureType buildFeatureType(final PreparedStatement target, final 
String sourceQuery, final GenericName optName) throws SQLException {
+        return build(new QuerySpecification(target, sourceQuery, optName));
     }
 
     private FeatureType build(final SQLTypeSpecification spec) throws 
SQLException {
         final FeatureTypeBuilder builder = new FeatureTypeBuilder(nameFactory, 
functions.library, locale);
         builder.setName(spec.getName());
         builder.setDefinition(spec.getDefinition());
+        final String geomCol = spec.getPrimaryGeometryColumn().orElse("");
+        final List pkCols = 
spec.getPK().map(PrimaryKey::getColumns).orElse(Collections.EMPTY_LIST);
         while  (spec.hasNext()) {
-            final SQLColumnSpecification col = spec.next();
-            functions.toJavaType(col.getType(), col.getName());
+            final SQLColumn col = spec.next();
+            Class<?> type = functions.toJavaType(col.getType(), 
col.getTypeName());
+            final String colName = col.getName().getColumnName();
+            final String attrName = col.getName().getAttributeName();
+            if (type == null) {
+                warning(Resources.Keys.UnknownType_1, colName);
+                type = Object.class;
+            }
+
+            final AttributeTypeBuilder<?> attribute = builder
+                    .addAttribute(type)
+                    .setName(attrName);
+            if (col.isNullable) attribute.setMinimumOccurs(0);
+            final int precision = col.getPrecision();
+            /* TODO: we should check column type. Precision for numbers or 
blobs is meaningfull, but the convention
+             * exposed by SIS does not allow to distinguish such cases.
+             */
+            if (precision > 0) attribute.setMaximalLength(precision);
+
+            col.getCrs().ifPresent(attribute::setCRS);
+            if (geomCol.equals(attrName)) 
attribute.addRole(AttributeRole.DEFAULT_GEOMETRY);
 
+            if (pkCols.contains(colName)) 
attribute.addRole(AttributeRole.IDENTIFIER_COMPONENT);
         }
 
-        throw new UnsupportedOperationException();
+        addImports(spec, builder);
+
+        addExports(spec, builder);
+
+        return builder.build();
     }
 
-    private interface SQLTypeSpecification extends 
Iterator<SQLColumnSpecification> {
+    private void addExports(SQLTypeSpecification spec, FeatureTypeBuilder 
builder) throws SQLException {
+        final List<Relation> exports;
+        try {
+            exports = spec.getExports();
+        } catch (DataStoreContentException e) {
+            throw new BackingStoreException(e);
+        }
+
+        for (final Relation r : exports) {
+            final GenericName foreignTypeName = r.getName(Analyzer.this);
+            String propertyName = foreignTypeName.tip().toString();
+            final String base = propertyName;
+            int count = 0;
+            while (builder.isNameUsed("sis:"+base)) {
+                propertyName = base + '-' + ++count;
+            }
+            r.propertyName = propertyName;
+            try {
+                final Table foreignTable = table(r, foreignTypeName, null); // 
'null' because exported, not imported.
+                final AssociationRoleBuilder association;
+                if (foreignTable != null) {
+                    r.setSearchTable(Analyzer.this, foreignTable, 
spec.getPK().map(PrimaryKey::getColumns).map(l -> l.toArray(new 
String[0])).orElse(null), Relation.Direction.EXPORT);
+                    association = 
builder.addAssociation(foreignTable.featureType);
+                } else {
+                    association = builder.addAssociation(foreignTypeName);     
// May happen in case of cyclic dependency.
+                }
+                association.setName("sis", r.propertyName)
+                        .setMinimumOccurs(0)
+                        .setMaximumOccurs(Integer.MAX_VALUE);
+            } catch (DataStoreException e) {
+                throw new BackingStoreException(e);
+            }
+        }
+    }
+
+    private void addImports(SQLTypeSpecification spec, FeatureTypeBuilder 
target) throws SQLException {
+        final List<Relation> imports;
+        try {
+            imports = spec.getImports();
+        } catch (DataStoreContentException e) {
+            throw new BackingStoreException(e);
+        }
+
+        // TODO: add an abstraction here, so we can specify source table when 
origin is one.
+        for (Relation r : imports) {
+            final GenericName foreignTypeName = r.getName(Analyzer.this);
+            final Table foreignTable;
+            try {
+                foreignTable = table(r, foreignTypeName, null);
+            } catch (DataStoreException e) {
+                throw new BackingStoreException(e);
+            }
+            final AssociationRoleBuilder association = foreignTable == null?
+                    target.addAssociation(foreignTypeName) : 
target.addAssociation(foreignTable.featureType);
+            r.propertyName = foreignTypeName.tip().toString();
+            association.setName("sis", r.propertyName);
+        }
+    }
+
+    private interface PropertyAdapter {
+        PropertyType getType();
+        void fill(ResultSet source, final Feature target);
+    }
+
+    private interface SQLTypeSpecification extends Iterator<SQLColumn> {
+        /**
+         *
+         * @return Name for the feature type to build. Nullable.
+         * @throws SQLException If an error occurs while retrieving 
information from database.
+         */
         GenericName getName() throws SQLException;
+
+        /**
+         *
+         * @return A succint description of the data source. Nullable.
+         * @throws SQLException If an error occurs while retrieving 
information from database.
+         */
         String getDefinition() throws SQLException;
-    }
 
-    private interface SQLColumnSpecification {
-        int getType() throws SQLException;
-        String getName() throws SQLException;
+        Optional<PrimaryKey<?>> getPK() throws SQLException;
+
+        List<Relation> getImports() throws SQLException, 
DataStoreContentException;
+
+        List<Relation> getExports() throws SQLException, 
DataStoreContentException;
+
+        default Optional<String> getPrimaryGeometryColumn() {return 
Optional.empty();}
     }
 
     private class TableMetadata implements SQLTypeSpecification, AutoCloseable 
{
@@ -354,18 +470,27 @@ final class Analyzer {
         private final String tableEsc;
         private final String schemaEsc;
 
-        private TableMetadata(TableReference source) throws SQLException {
+        private boolean hasNext;
+
+        private final TableReference importedBy;
+
+        private TableMetadata(TableReference source, TableReference 
importedBy) throws SQLException {
             this.id = source;
             tableEsc = escape(source.table);
             schemaEsc = escape(source.schema);
             reflect = metadata.getColumns(source.catalog, schemaEsc, tableEsc, 
null);
+            hasNext = reflect.next();
+            this.importedBy = importedBy;
         }
 
         @Override
-        public GenericName getName() throws SQLException {
+        public GenericName getName() {
             return id.getName(Analyzer.this);
         }
 
+        /**
+         * The remarks are opportunistically stored in id.freeText if known by 
the caller.
+         */
         @Override
         public String getDefinition() throws SQLException {
             String remarks = id.freeText;
@@ -386,13 +511,70 @@ final class Analyzer {
         }
 
         @Override
+        public Optional<PrimaryKey<?>> getPK() throws SQLException {
+            try (ResultSet reflect = metadata.getPrimaryKeys(id.catalog, 
id.schema, id.table)) {
+                final List<String> cols = new ArrayList<>();
+                while (reflect.next()) {
+                    cols.add(getUniqueString(reflect, Reflection.COLUMN_NAME));
+                    // The actual Boolean value will be fetched in the loop on 
columns later.
+                }
+                return PrimaryKey.create(cols);
+            }
+        }
+
+        @Override
+        public List<Relation> getImports() throws SQLException, 
DataStoreContentException {
+            try (ResultSet reflect = metadata.getImportedKeys(id.catalog, 
id.schema, id.table)) {
+                if (!reflect.next()) return Collections.EMPTY_LIST;
+                final List<Relation> fks = new ArrayList<>(2);
+                do {
+                    Relation relation = new Relation(Analyzer.this, 
Relation.Direction.IMPORT, reflect);
+                    fks.add(relation);
+                } while (!reflect.isClosed());
+                return fks;
+            }
+        }
+
+        @Override
+        public List<Relation> getExports() throws SQLException, 
DataStoreContentException {
+            try (ResultSet reflect = metadata.getExportedKeys(id.catalog, 
id.schema, id.table)) {
+                if (!reflect.next()) return Collections.EMPTY_LIST;
+                final List<Relation> exports = new ArrayList<>(2);
+                do {
+                    final Relation export = new Relation(Analyzer.this, 
Relation.Direction.EXPORT, reflect);
+                    if (!export.equals(importedBy)) {
+                        exports.add(export);
+                    }
+                } while (!reflect.isClosed());
+                return exports;
+            }
+        }
+
+        @Override
+        public Optional<String> getPrimaryGeometryColumn() {
+            return Optional.empty();
+            //throw new UnsupportedOperationException("Not supported yet"); // 
"Alexis Manin (Geomatys)" on 20/09/2019
+        }
+
+        @Override
         public boolean hasNext() {
-            throw new UnsupportedOperationException("Not supported yet"); // 
"Alexis Manin (Geomatys)" on 19/09/2019
+            return hasNext;
         }
 
         @Override
-        public SQLColumnSpecification next() {
-            throw new UnsupportedOperationException("Not supported yet"); // 
"Alexis Manin (Geomatys)" on 19/09/2019
+        public SQLColumn next() {
+            try {
+                final int type = reflect.getInt(Reflection.DATA_TYPE);
+                final String typeName = 
reflect.getString(Reflection.TYPE_NAME);
+                final boolean isNullable = 
Boolean.TRUE.equals(SQLUtilities.parseBoolean(reflect.getString(Reflection.IS_NULLABLE)));
+                final ColumnRef name = new ColumnRef(getUniqueString(reflect, 
Reflection.COLUMN_NAME));
+                final int precision = reflect.getInt(Reflection.COLUMN_SIZE);
+                final SQLColumn col = new SQLColumn(type, typeName, 
isNullable, name, precision);
+                hasNext = reflect.next();
+                return col;
+            } catch (SQLException e) {
+                throw new BackingStoreException(e);
+            }
         }
 
         @Override
@@ -401,42 +583,120 @@ final class Analyzer {
         }
     }
 
-    private class TableColumn implements SQLColumnSpecification {
+    private class QuerySpecification implements SQLTypeSpecification {
 
-        final ResultSet reflect;
+        int idx = 0;
+        final int total;
+        final PreparedStatement source;
+        private final ResultSetMetaData meta;
+        private final String query;
+        private final GenericName name;
 
-        private TableColumn(ResultSet reflect) {
-            this.reflect = reflect;
+        public QuerySpecification(PreparedStatement source, String 
sourceQuery, GenericName optName) throws SQLException {
+            this.source = source;
+            meta = source.getMetaData();
+            total = meta.getColumnCount();
+            query = sourceQuery;
+            name = optName;
         }
 
         @Override
-        public int getType() throws SQLException {
-            return reflect.getInt(Reflection.DATA_TYPE);
+        public GenericName getName() throws SQLException {
+            return name;
         }
 
         @Override
-        public String getName() throws SQLException {
-            return reflect.getString(Reflection.TYPE_NAME);
+        public String getDefinition() throws SQLException {
+            return query;
         }
-    }
 
-    private class QueryColumn implements SQLColumnSpecification {
-        final int idx;
-        final ResultSetMetaData source;
+        @Override
+        public Optional<PrimaryKey<?>> getPK() throws SQLException {
+            return Optional.empty();
+        }
 
-        private QueryColumn(int idx, ResultSetMetaData source) {
-            this.idx = idx;
-            this.source = source;
+        @Override
+        public List<Relation> getImports() throws SQLException {
+            return Collections.EMPTY_LIST;
         }
 
         @Override
-        public int getType() throws SQLException {
-            return source.getColumnType(idx);
+        public List<Relation> getExports() throws SQLException, 
DataStoreContentException {
+            return Collections.EMPTY_LIST;
         }
 
         @Override
-        public String getName() throws SQLException {
-            return source.getColumnName(idx);
+        public boolean hasNext() {
+            return idx < total;
+        }
+
+        @Override
+        public SQLColumn next() {
+            try {
+                final SQLColumn col = new SQLColumn(
+                        meta.getColumnType(idx),
+                        meta.getColumnTypeName(idx),
+                        meta.isNullable(idx) == 
ResultSetMetaData.columnNullable,
+                        new 
ColumnRef(meta.getColumnName(idx)).as(meta.getColumnLabel(idx)),
+                        meta.getPrecision(idx)
+                );
+                idx++;
+                return col;
+            } catch (SQLException e) {
+                throw new BackingStoreException(e);
+            }
+        }
+    }
+
+    private class SQLColumn {
+        final int type;
+        final String typeName;
+        private final boolean isNullable;
+        private final ColumnRef naming;
+        private final int precision;
+
+        public SQLColumn(int type, String typeName, boolean isNullable, 
ColumnRef naming, int precision) {
+            this.type = type;
+            this.typeName = typeName;
+            this.isNullable = isNullable;
+            this.naming = naming;
+            this.precision = precision;
+        }
+
+        public ColumnRef getName() {
+            return naming;
+        }
+
+        public int getType() {
+            return type;
+        }
+
+        public String getTypeName() {
+            return typeName;
+        }
+
+        public boolean isNullable() {
+            return isNullable;
+        }
+
+        /**
+         * Same as {@link ResultSetMetaData#getPrecision(int)}.
+         * @return 0 if unknown. For texts, maximum number of characters 
allowed. For numerics, max precision. For blobs,
+         * number of bytes allowed.
+         */
+        public int getPrecision() {
+            return precision;
+        }
+
+        /**
+         * TODO: implement.
+         * Note : This method could be used not only for geometric fields, but 
also on numeric ones representing 1D
+         * systems.
+         *
+         * @return null for now, implementation needed.
+         */
+        public Optional<CoordinateReferenceSystem> getCrs() {
+            return Optional.empty();
         }
     }
 }
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java
index c343e70..c47f65e 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java
@@ -40,6 +40,7 @@ import org.opengis.filter.Filter;
 import org.opengis.filter.sort.SortBy;
 
 import org.apache.sis.internal.metadata.sql.SQLBuilder;
+import org.apache.sis.internal.sql.feature.metamodel.ColumnRef;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.InternalDataStoreException;
 import org.apache.sis.util.ArraysExt;
@@ -174,7 +175,7 @@ final class Features implements Spliterator<Feature> {
         attributeNames = new String[attributeColumns.length];
         int i = 0;
         for (ColumnRef column : columns) {
-            attributeColumns[i] = column.name;
+            attributeColumns[i] = column.getColumnName();
             attributeNames[i++] = column.getAttributeName();
         }
         this.featureType = table.featureType;
@@ -625,12 +626,12 @@ final class Features implements Spliterator<Feature> {
             if (count) sql.append("COUNT(");
             if (distinct) sql.append("DISTINCT ");
             // If we want a count and no distinct clause is specified, we can 
query it for a single column.
-            if (count && !distinct) 
sql.appendIdentifier(source.parent.attributes.get(0).name);
+            if (count && !distinct) 
source.parent.attributes.get(0).append(sql);
             else {
                 final Iterator<ColumnRef> it = 
source.parent.attributes.iterator();
-                sql.appendIdentifier(it.next().name);
+                it.next().append(sql);
                 while (it.hasNext()) {
-                    sql.append(", ").appendIdentifier(it.next().name);
+                    it.next().append(sql.append(", "));
                 }
             }
 
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java
index 7c1679a..b814271 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java
@@ -2,7 +2,6 @@ package org.apache.sis.internal.sql.feature;
 
 import java.sql.Connection;
 import java.sql.PreparedStatement;
-import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
 import java.util.stream.Stream;
 import javax.sql.DataSource;
@@ -33,9 +32,9 @@ public class QueryFeatureSet extends AbstractFeatureSet {
         this.source = source;
 
         try (Connection conn = connectReadOnly(source)) {
-            final PreparedStatement statement = 
conn.prepareStatement(queryBuilder.toString());
-            final ResultSetMetaData rmd = statement.getMetaData();
-            resultType = analyzer.buildFeatureType(rmd);
+            final String sql = queryBuilder.toString();
+            final PreparedStatement statement = conn.prepareStatement(sql);
+            resultType = analyzer.buildFeatureType(statement, sql, null); // 
TODO: allow user to give a name ?
         } catch (SQLException e) {
             throw new DataStoreException("Cannot analyze query metadata 
(feature type determination)", e);
         }
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 3bffd90..a66add7 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
@@ -23,7 +23,6 @@ import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -36,27 +35,18 @@ import org.opengis.feature.AttributeType;
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureAssociationRole;
 import org.opengis.feature.FeatureType;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.util.GenericName;
 
-import org.apache.sis.feature.builder.AssociationRoleBuilder;
-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.internal.feature.Geometries;
 import org.apache.sis.internal.metadata.sql.Reflection;
 import org.apache.sis.internal.metadata.sql.SQLBuilder;
-import org.apache.sis.internal.metadata.sql.SQLUtilities;
+import org.apache.sis.internal.sql.feature.metamodel.ColumnRef;
 import org.apache.sis.internal.storage.AbstractFeatureSet;
 import org.apache.sis.internal.storage.query.SimpleQuery;
-import org.apache.sis.internal.util.CollectionsExt;
-import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.InternalDataStoreException;
 import org.apache.sis.storage.Query;
 import org.apache.sis.storage.UnsupportedQueryException;
-import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.Numbers;
@@ -205,26 +195,7 @@ final class Table extends AbstractFeatureSet {
          * multi-occurrences.
          */
         final List<Relation> importedKeys = new ArrayList<>();
-        final Map<String, List<Relation>> foreignerKeys = new HashMap<>();
-        try (ResultSet reflect = analyzer.metadata.getImportedKeys(id.catalog, 
id.schema, id.table)) {
-            if (reflect.next()) do {
-                Relation relation = new Relation(analyzer, 
Relation.Direction.IMPORT, reflect);
-                importedKeys.add(relation);
-                for (final String column : relation.getForeignerKeys()) {
-                    CollectionsExt.addToMultiValuesMap(foreignerKeys, column, 
relation);
-                    relation = null;     // Only the first column will be 
associated.
-                }
-            } while (!reflect.isClosed());
-        }
         final List<Relation> exportedKeys = new ArrayList<>();
-        try (ResultSet reflect = analyzer.metadata.getExportedKeys(id.catalog, 
id.schema, id.table)) {
-            if (reflect.next()) do {
-                final Relation export = new Relation(analyzer, 
Relation.Direction.EXPORT, reflect);
-                if (!export.equals(importedBy)) {
-                    exportedKeys.add(export);
-                }
-            } 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 
multiplicity in inferred from the SQL
@@ -234,161 +205,8 @@ final class Table extends AbstractFeatureSet {
         Class<?> primaryKeyClass   = null;
         boolean  primaryKeyNonNull = true;
         boolean  hasGeometry       = false;
-        int startWithLowerCase     = 0;
         final List<ColumnRef> attributes = new ArrayList<>();
-        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       = 
analyzer.getUniqueString(reflect, 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);
-                /*
-                 * 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--;
-                    }
-                }
-
-                ColumnRef colRef = new ColumnRef(column);
-                /*
-                 * 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) {
-                    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;
-                    }
-                    attribute = feature.addAttribute(type).setName(column);
-                    if (CharSequence.class.isAssignableFrom(type)) {
-                        final int size = 
reflect.getInt(Reflection.COLUMN_SIZE);
-                        if (!reflect.wasNull()) {
-                            attribute.setMaximalLength(size);
-                        }
-                    }
-                    if (!mandatory) {
-                        attribute.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 (isPrimaryKey) {
-                        attribute.addRole(AttributeRole.IDENTIFIER_COMPONENT);
-                        primaryKeyNonNull &= mandatory;
-                        primaryKeyClass = 
Classes.findCommonClass(primaryKeyClass, type);
-                        if (primaryKeys.put(column, 
SQLUtilities.parseBoolean(reflect.getString(Reflection.IS_AUTOINCREMENT))) != 
null) {
-                            throw new 
DataStoreContentException(Resources.forLocale(analyzer.locale)
-                                    
.getString(Resources.Keys.DuplicatedColumn_1, column));
-                        }
-                    }
-                    if (Geometries.isKnownType(type)) {
-                        final CoordinateReferenceSystem crs = 
analyzer.functions.createGeometryCRS(reflect);
-                        if (crs != null) {
-                            attribute.setCRS(crs);
-                        }
-                        if (!hasGeometry) {
-                            hasGeometry = true;
-                            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 (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, id);
-                            /*
-                             * Use the column name as the association name, 
provided that the foreigner key
-                             * use only that column. If the foreigner key use 
more than one column, then we
-                             * do not know which column describes better the 
association (often there is none).
-                             * In such case we use the foreigner key name as a 
fallback.
-                             */
-                            dependency.setPropertyName(column, count++);
-                            final AssociationRoleBuilder association;
-                            if (table != null) {
-                                dependency.setSearchTable(analyzer, table, 
table.primaryKeys, Relation.Direction.IMPORT);
-                                association = 
feature.addAssociation(table.featureType);
-                            } else {
-                                association = 
feature.addAssociation(typeName);     // May happen in case of cyclic 
dependency.
-                            }
-                            association.setName(dependency.propertyName);
-                            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));
-                                colRef = 
colRef.as(attribute.getName().toString());
-                                attribute = null;
-                            }
-                        }
-                    }
-                }
 
-                attributes.add(colRef);
-            }
-        }
-        /*
-         * 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 propertyName = typeName.tip().toString();
-                if (startWithLowerCase > 0) {
-                    final CharSequence words = 
CharSequences.camelCaseToWords(propertyName, true);
-                    final int first = Character.codePointAt(words, 0);
-                    propertyName = new StringBuilder(words.length())
-                            .appendCodePoint(Character.toLowerCase(first))
-                            .append(words, Character.charCount(first), 
words.length())
-                            .toString();
-                }
-                final String base = propertyName;
-                while (feature.isNameUsed(propertyName)) {
-                    propertyName = base + '-' + ++count;
-                }
-                dependency.propertyName = propertyName;
-                final Table table = analyzer.table(dependency, typeName, 
null);   // 'null' because exported, not imported.
-                final AssociationRoleBuilder association;
-                if (table != null) {
-                    dependency.setSearchTable(analyzer, table, 
this.primaryKeys, Relation.Direction.EXPORT);
-                    association = feature.addAssociation(table.featureType);
-                } else {
-                    association = feature.addAssociation(typeName);     // May 
happen in case of cyclic dependency.
-                }
-                association.setName(propertyName)
-                           .setMinimumOccurs(0)
-                           .setMaximumOccurs(Integer.MAX_VALUE);
-            }
-        }
         /*
          * If the primary keys uses more than one column, we will need an 
array to store it.
          * If all columns are non-null numbers, use primitive arrays instead 
than array of wrappers.
@@ -399,29 +217,8 @@ final class Table extends AbstractFeatureSet {
             }
             primaryKeyClass = Classes.changeArrayDimension(primaryKeyClass, 1);
         }
-        /*
-         * Global information on the feature type (name, remarks).
-         * The remarks are opportunistically stored in id.freeText if known by 
the caller.
-         */
-        feature.setName(id.getName(analyzer));
-        String remarks = id.freeText;
-        if (id instanceof Relation) {
-            try (ResultSet reflect = analyzer.metadata.getTables(id.catalog, 
schemaEsc, tableEsc, null)) {
-                while (reflect.next()) {
-                    remarks = analyzer.getUniqueString(reflect, 
Reflection.REMARKS);
-                    if (remarks != null) {
-                        remarks = remarks.trim();
-                        if (remarks.isEmpty()) {
-                            remarks = null;
-                        } else break;
-                    }
-                }
-            }
-        }
-        if (remarks != null) {
-            feature.setDefinition(remarks);
-        }
-        this.featureType      = feature.build();
+
+        this.featureType      = analyzer.buildFeatureType(id, importedBy);
         this.importedKeys     = toArray(importedKeys);
         this.exportedKeys     = toArray(exportedKeys);
         this.primaryKeyClass  = primaryKeyClass;
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnRef.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/metamodel/ColumnRef.java
similarity index 76%
rename from 
storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnRef.java
rename to 
storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/metamodel/ColumnRef.java
index f2d229d..a2ff8c2 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnRef.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/metamodel/ColumnRef.java
@@ -1,4 +1,4 @@
-package org.apache.sis.internal.sql.feature;
+package org.apache.sis.internal.sql.feature.metamodel;
 
 import java.util.Objects;
 
@@ -11,9 +11,9 @@ import static 
org.apache.sis.util.ArgumentChecks.ensureNonNull;
  * By default, column has no alias. To create a column with an alias, use 
{@code ColumnRef myCol = new ColumnRef("colName).as("myAlias");}
  */
 public final class ColumnRef {
-    final String name;
-    final String alias;
-    final String attrName;
+    private final String name;
+    private final String alias;
+    private final String attrName;
 
     public ColumnRef(String name) {
         ensureNonNull("Column name", name);
@@ -27,11 +27,13 @@ public final class ColumnRef {
         this.alias = this.attrName = alias;
     }
 
-    ColumnRef as(final String alias) {
+    public ColumnRef as(final String alias) {
+        if (Objects.equals(alias, this.alias)) return this;
+        else if (alias == null || alias.equals(name)) return new 
ColumnRef(name);
         return new ColumnRef(name, alias);
     }
 
-    SQLBuilder append(final SQLBuilder target) {
+    public SQLBuilder append(final SQLBuilder target) {
         target.appendIdentifier(name);
         if (alias != null) {
             target.append(" AS ").appendIdentifier(alias);
@@ -40,6 +42,7 @@ public final class ColumnRef {
         return target;
     }
 
+    public String getColumnName() { return name; }
     public String getAttributeName() {
         return attrName;
     }
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/metamodel/PrimaryKey.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/metamodel/PrimaryKey.java
new file mode 100644
index 0000000..2633960
--- /dev/null
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/metamodel/PrimaryKey.java
@@ -0,0 +1,48 @@
+package org.apache.sis.internal.sql.feature.metamodel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.sis.util.ArgumentChecks;
+
+public interface PrimaryKey<T> {
+
+    static Optional<PrimaryKey<?>> create(List<String> cols) {
+        if (cols == null || cols.isEmpty()) return Optional.empty();
+        if (cols.size() == 1) return Optional.of(new Simple(cols.get(0)));
+        return Optional.of(new Composite(cols));
+    }
+
+    //Class<T> getViewType();
+    List<String> getColumns();
+
+    class Simple implements PrimaryKey {
+        final String column;
+
+        public Simple(String column) {
+            this.column = column;
+        }
+
+        @Override
+        public List<String> getColumns() { return 
Collections.singletonList(column); }
+    }
+
+    class Composite implements PrimaryKey {
+        /**
+         * Name of columns composing primary keys.
+         */
+        private final List<String> columns;
+
+        public Composite(List<String> columns) {
+            ArgumentChecks.ensureNonEmpty("Primary key column names", columns);
+            this.columns = Collections.unmodifiableList(new 
ArrayList<>(columns));
+        }
+
+        @Override
+        public List<String> getColumns() {
+            return columns;
+        }
+    }
+}

Reply via email to