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; + } + } +}
