You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/09/27 10:57:52 UTC

[sis] 01/02: Support "export" foreigner keys on queries as we did for tables.

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

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 957d7b0832878b94d307a14d35d01a41169bbe0d
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Sep 27 10:50:44 2021 +0200

    Support "export" foreigner keys on queries as we did for tables.
---
 .../sis/internal/sql/feature/FeatureAnalyzer.java  | 30 +++++++-
 .../sis/internal/sql/feature/QueryAnalyzer.java    | 82 +++++++++++-----------
 .../sis/internal/sql/feature/TableAnalyzer.java    | 42 -----------
 .../org/apache/sis/storage/sql/SQLStoreTest.java   | 38 +++++-----
 4 files changed, 87 insertions(+), 105 deletions(-)

diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java
index 19b8e3b..5ecd631 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java
@@ -297,14 +297,38 @@ abstract class FeatureAnalyzer {
      * @throws DataStoreException if a logical error occurred while analyzing the relations.
      * @throws Exception for WKB parsing error or other kinds of errors.
      */
-    PrimaryKey createAssociations(Relation[] exportedKeys) throws Exception {
+    final PrimaryKey createAssociations(Relation[] exportedKeys) throws Exception {
         if (primaryKey.size() > 1) {
             if (!primaryKeyNullable) {
                 primaryKeyClass = Numbers.wrapperToPrimitive(primaryKeyClass);
             }
             primaryKeyClass = Classes.changeArrayDimension(primaryKeyClass, 1);
         }
-        return PrimaryKey.create(primaryKeyClass, primaryKey);
+        final PrimaryKey pk = PrimaryKey.create(primaryKeyClass, primaryKey);
+        int count = 0;
+        for (final Relation dependency : exportedKeys) {
+            if (dependency != null) {
+                final GenericName typeName = dependency.getName(analyzer);
+                String propertyName = toHeuristicLabel(typeName.tip().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, pk, 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);
+            }
+        }
+        return pk;
     }
 
     /**
@@ -330,7 +354,7 @@ abstract class FeatureAnalyzer {
      * @param  propertyName  name of the property to rewrite.
      * @return proposed human-readable property name.
      */
-    final String toHeuristicLabel(String propertyName) {
+    private String toHeuristicLabel(String propertyName) {
         if (countLowerCaseStarts > 0) {
             final CharSequence words = CharSequences.camelCaseToWords(propertyName, true);
             final int first = Character.codePointAt(words, 0);
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java
index edb3c1e..475a578 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java
@@ -108,53 +108,53 @@ final class QueryAnalyzer extends FeatureAnalyzer {
      */
     @Override
     Relation[] getForeignerKeys(final Relation.Direction direction) throws SQLException, DataStoreException {
-        if (Relation.Direction.IMPORT.equals(direction)) {
-            List<String> primaryKeyColumns = new ArrayList<>();
-            final List<Relation> relations = new ArrayList<>();
-            for (final Map.Entry<TableReference, Map<String,Column>> entry : columnsPerTable.entrySet()) {
-                final Set<String> columnNames = entry.getValue().keySet();
-                final TableReference src = entry.getKey();
-                /*
-                 * Search for foreigner keys in the table where the query columns come from.
-                 * Foreigner keys can be handled as such only if all required columns are in the query.
-                 * Otherwise we will handle those columns as ordinary attributes.
-                 */
-                try (ResultSet reflect = analyzer.metadata.getImportedKeys(src.catalog, src.schema, src.table)) {
-                    if (reflect.next()) do {
-                        final Relation relation = new Relation(analyzer, direction, reflect);
-                        if (columnNames.containsAll(relation.getForeignerKeys())) {
+        final boolean isImport = (direction == Relation.Direction.IMPORT);
+        List<String> primaryKeyColumns = isImport ? new ArrayList<>() : null;
+        final List<Relation> relations = new ArrayList<>();
+        for (final Map.Entry<TableReference, Map<String,Column>> entry : columnsPerTable.entrySet()) {
+            final Set<String> columnNames = entry.getValue().keySet();
+            final TableReference src = entry.getKey();
+            /*
+             * Search for foreigner keys in the table where the query columns come from.
+             * Foreigner keys can be handled as such only if all required columns are in the query.
+             * Otherwise we will handle those columns as ordinary attributes.
+             */
+            try (ResultSet reflect = isImport ? analyzer.metadata.getImportedKeys(src.catalog, src.schema, src.table)
+                                              : analyzer.metadata.getExportedKeys(src.catalog, src.schema, src.table))
+            {
+                if (reflect.next()) do {
+                    final Relation relation = new Relation(analyzer, direction, reflect);
+                    if (columnNames.containsAll(relation.getForeignerKeys())) {
+                        if (isImport) {
                             addForeignerKeys(relation);
-                            relations.add(relation);
-                        }
-                    } while (!reflect.isClosed());
-                }
-                /*
-                 * Opportunistically search for primary keys. They are not needed by this method,
-                 * but will be needed later by `createAttributes(…)` and other methods.
-                 * This is a "all or nothing" operations: if some primary key columns are missing
-                 * from the query, then we can not have primary key at all for this query.
-                 */
-                if (primaryKeyColumns != null) {
-                    try (ResultSet reflect = analyzer.metadata.getPrimaryKeys(src.catalog, src.schema, src.table)) {
-                        while (reflect.next()) {
-                            primaryKeyColumns.add(analyzer.getUniqueString(reflect, Reflection.COLUMN_NAME));
-                        }
-                        if (columnNames.containsAll(primaryKeyColumns)) {
-                            primaryKey.addAll(primaryKeyColumns);
-                            primaryKeyColumns.clear();
-                        } else {
-                            primaryKey.clear();
-                            primaryKeyColumns = null;       // Means to not search anymore.
                         }
+                        relations.add(relation);
                     }
-                }
+                } while (!reflect.isClosed());
             }
-            final int size = relations.size();
-            if (size != 0) {
-                return relations.toArray(new Relation[size]);
+            /*
+             * Opportunistically search for primary keys. They are not needed by this method,
+             * but will be needed later by `createAttributes(…)` and other methods.
+             * This is a "all or nothing" operations: if some primary key columns are missing
+             * from the query, then we can not have primary key at all for this query.
+             */
+            if (primaryKeyColumns != null) {
+                try (ResultSet reflect = analyzer.metadata.getPrimaryKeys(src.catalog, src.schema, src.table)) {
+                    while (reflect.next()) {
+                        primaryKeyColumns.add(analyzer.getUniqueString(reflect, Reflection.COLUMN_NAME));
+                    }
+                    if (columnNames.containsAll(primaryKeyColumns)) {
+                        primaryKey.addAll(primaryKeyColumns);
+                        primaryKeyColumns.clear();
+                    } else {
+                        primaryKey.clear();
+                        primaryKeyColumns = null;       // Means to not search anymore.
+                    }
+                }
             }
         }
-        return Relation.EMPTY;
+        final int size = relations.size();
+        return (size != 0) ? relations.toArray(new Relation[size]) : Relation.EMPTY;
     }
 
     /**
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java
index 38e7c56..736bb44 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java
@@ -22,9 +22,7 @@ import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.sql.SQLException;
 import java.sql.ResultSet;
-import org.opengis.util.GenericName;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.feature.builder.AssociationRoleBuilder;
 import org.apache.sis.internal.metadata.sql.Reflection;
 import org.apache.sis.internal.metadata.sql.SQLUtilities;
 import org.apache.sis.internal.util.Strings;
@@ -173,46 +171,6 @@ final class TableAnalyzer extends FeatureAnalyzer {
     }
 
     /**
-     * Adds 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.
-     *
-     * <h4>Side effects</h4>
-     * <p><b>Required by this method:</b> {@link #primaryKeyClass}.</p>
-     * <p><b>Computed by this method:</b> none.</p>
-     *
-     * @eturn the components of the primary key, or {@code null} if there is no primary key.
-     */
-    @Override
-    final PrimaryKey createAssociations(final Relation[] exportedKeys) throws Exception {
-        final PrimaryKey pk = super.createAssociations(exportedKeys);
-        int count = 0;
-        for (final Relation dependency : exportedKeys) {
-            if (dependency != null) {
-                final GenericName typeName = dependency.getName(analyzer);
-                String propertyName = toHeuristicLabel(typeName.tip().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, pk, 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);
-            }
-        }
-        return pk;
-    }
-
-    /**
      * Returns an optional description of the application schema.
      */
     @Override
diff --git a/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java b/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
index 7e84d04..4e113fd 100644
--- a/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
+++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
@@ -228,9 +228,9 @@ public final strictfp class SQLStoreTest extends TestCase {
      *
      * @param  feature       a feature returned by the stream.
      * @param  countryCount  number of time that the each country has been seen while iterating over the cities.
-     * @param  hasExport     whether the feature is expected to have associations for "export" foreigner keys.
+     * @param  isTable       {@code true} if the resource is from a table, or {@code false} if from a query.
      */
-    private void verifyContent(final Feature feature, final Map<String,Integer> countryCount, final boolean hasExport) {
+    private void verifyContent(final Feature feature, final Map<String,Integer> countryCount, final boolean isTable) {
         final String city = feature.getPropertyValue("native_name").toString();
         final City c;
         boolean isCanada = false;
@@ -266,23 +266,23 @@ public final strictfp class SQLStoreTest extends TestCase {
          * Associations using Relation.Direction.EXPORT.
          * Contrarily to the IMPORT case, those associations can contain many values.
          */
-        if (hasExport) {
-            final Collection<?> actualParks = (Collection<?>) feature.getPropertyValue("parks");
-            assertNotNull("parks", actualParks);
-            assertEquals("parks.length", c.parks.length, actualParks.size());
-            final Collection<String> expectedParks = new HashSet<>(Arrays.asList(c.parks));
-            for (final Object park : actualParks) {
-                final Feature pf = (Feature) park;
-                final String npn = (String) pf.getPropertyValue("native_name");
-                final String epn = (String) pf.getPropertyValue("english_name");
-                assertNotNull("park.native_name",  npn);
-                assertNotNull("park.english_name", epn);
-                assertNotEquals("park.names", npn, epn);
-                assertTrue("park.english_name", expectedParks.remove(epn));
-                /*
-                 * Verify the reverse association form Parks to Cities.
-                 * This create a cyclic graph, but SQLStore is capable to handle it.
-                 */
+        final Collection<?> actualParks = (Collection<?>) feature.getPropertyValue("parks");
+        assertNotNull("parks", actualParks);
+        assertEquals("parks.length", c.parks.length, actualParks.size());
+        final Collection<String> expectedParks = new HashSet<>(Arrays.asList(c.parks));
+        for (final Object park : actualParks) {
+            final Feature pf = (Feature) park;
+            final String npn = (String) pf.getPropertyValue("native_name");
+            final String epn = (String) pf.getPropertyValue("english_name");
+            assertNotNull("park.native_name",  npn);
+            assertNotNull("park.english_name", epn);
+            assertNotEquals("park.names", npn, epn);
+            assertTrue("park.english_name", expectedParks.remove(epn));
+            /*
+             * Verify the reverse association form Parks to Cities.
+             * This create a cyclic graph, but SQLStore is capable to handle it.
+             */
+            if (isTable) {
                 assertSame("City → Park → City", feature, pf.getPropertyValue("FK_City"));
             }
         }