sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ama...@apache.org
Subject [sis] 11/45: WIP(SQL-Store): refactor feature-type building to factorize query and table discovery.
Date Tue, 12 Nov 2019 16:44:38 GMT
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 <amanin@apache.org>
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;
+        }
+    }
+}


Mime
View raw message