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/10/05 18:09:00 UTC
[sis] 03/03: Avoid recursivity with "exported" foreigner keys in
addition of "imported" ones.
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 911a413919d06cd4a549a4365c461e02adb622d3
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue Oct 5 12:07:17 2021 +0200
Avoid recursivity with "exported" foreigner keys in addition of "imported" ones.
---
.../apache/sis/internal/sql/feature/Analyzer.java | 16 +--
.../sis/internal/sql/feature/FeatureAdapter.java | 6 +-
.../sis/internal/sql/feature/FeatureAnalyzer.java | 12 +-
.../sis/internal/sql/feature/QueryAnalyzer.java | 2 +-
.../apache/sis/internal/sql/feature/Relation.java | 28 +++--
.../sis/internal/sql/feature/SchemaModifier.java | 14 ++-
.../org/apache/sis/internal/sql/feature/Table.java | 8 +-
.../sis/internal/sql/feature/TableAnalyzer.java | 35 ++++--
.../sis/internal/sql/feature/package-info.java | 2 +-
.../org/apache/sis/storage/sql/package-info.java | 2 +-
.../org/apache/sis/storage/sql/SQLStoreTest.java | 129 ++++++++++++++-------
11 files changed, 168 insertions(+), 86 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 5b46252..11ae100 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
@@ -46,7 +46,7 @@ import org.apache.sis.util.resources.ResourceInternationalString;
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.0
* @module
*/
@@ -191,19 +191,19 @@ final class Analyzer {
* of another table. If a cyclic dependency is detected, then this method returns
* {@code null} for one of the tables.
*
- * @param id identification of the table to create.
- * @param name the value of {@code id.getName(analyzer)}
- * (as an argument for avoiding re-computation when already known by the caller).
- * @param importedBy if this table is imported by the foreigner keys of another table, the parent table.
- * Otherwise {@code null}.
+ * @param id identification of the table to create.
+ * @param name the value of {@code id.getName(analyzer)}
+ * (as an argument for avoiding re-computation when already known by the caller).
+ * @param dependencyOf if the analyzed table is imported/exported by foreigner keys,
+ * the table that "contains" this table. Otherwise {@code null}.
* @return the table, or {@code null} if there is a cyclic dependency and
* the table of the given name is already in process of being created.
*/
- public final Table table(final TableReference id, final GenericName name, final TableReference importedBy) throws Exception {
+ public final Table table(final TableReference id, final GenericName name, final TableReference dependencyOf) throws Exception {
Table table = tables.get(name);
if (table == null && !tables.containsKey(name)) {
tables.put(name, null); // Mark the feature as in process of being created.
- table = new Table(database, new TableAnalyzer(this, id, importedBy), null);
+ table = new Table(database, new TableAnalyzer(this, id, dependencyOf), null);
if (tables.put(name, table) != null) {
// Should never happen. If thrown, we have a bug (e.g. synchronization) in this package.
throw new InternalDataStoreException(internalError());
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java
index d0a3613..ac85330 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java
@@ -49,7 +49,7 @@ import org.opengis.feature.FeatureType;
*
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.1
* @module
*/
@@ -197,6 +197,7 @@ final class FeatureAdapter {
*/
count = 0; // We will recount.
for (final Relation dependency : table.importedKeys) {
+ if (dependency.excluded) continue;
if (dependency != noFollow) {
dependency.startFollowing(following); // Safety against never-ending recursivity.
associationNames [count] = dependency.propertyName;
@@ -216,6 +217,7 @@ final class FeatureAdapter {
* associations we need to iterate over all "Parks" rows referencing the city.
*/
for (final Relation dependency : table.exportedKeys) {
+ if (dependency.excluded) continue;
dependency.startFollowing(following); // Safety against never-ending recursivity.
final Table foreigner = dependency.getSearchTable();
final Relation inverse = foreigner.getInverseOf(dependency, table.name);
@@ -296,7 +298,7 @@ final class FeatureAdapter {
private static int[] getColumnIndices(final SQLBuilder sql, final Relation dependency,
final Map<String,Integer> columnIndices) throws InternalDataStoreException
{
- final Collection<String> columns = dependency.getForeignerKeys();
+ final Collection<String> columns = dependency.getOwnerColumns();
int i = 0;
final int[] indices = new int[columns.size()];
for (final String column : columns) {
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 e806e92..7afcc4a 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
@@ -61,7 +61,7 @@ import org.opengis.feature.FeatureType;
*
* @author Alexis Manin (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.1
* @module
*/
@@ -165,7 +165,7 @@ abstract class FeatureAnalyzer {
* This is a helper method for {@link #getForeignerKeys(Relation.Direction)} implementations.
*/
final void addForeignerKeys(Relation relation) {
- for (final String column : relation.getForeignerKeys()) {
+ for (final String column : relation.getOwnerColumns()) {
CollectionsExt.addToMultiValuesMap(foreignerKeys, column, relation);
relation = null; // Only the first column will be associated.
}
@@ -244,7 +244,7 @@ abstract class FeatureAnalyzer {
if (dependencies != null) {
int count = 0;
for (final Relation dependency : dependencies) {
- if (dependency != null) {
+ if (dependency != null && !dependency.excluded) {
final GenericName typeName = dependency.getName(analyzer);
final Table table = analyzer.table(dependency, typeName, id);
/*
@@ -297,7 +297,7 @@ 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.
*/
- final PrimaryKey createAssociations(Relation[] exportedKeys) throws Exception {
+ final PrimaryKey createAssociations(final Relation[] exportedKeys) throws Exception {
if (primaryKey.size() > 1) {
if (!primaryKeyNullable) {
primaryKeyClass = Numbers.wrapperToPrimitive(primaryKeyClass);
@@ -307,7 +307,7 @@ abstract class FeatureAnalyzer {
final PrimaryKey pk = PrimaryKey.create(primaryKeyClass, primaryKey);
int count = 0;
for (final Relation dependency : exportedKeys) {
- if (dependency != null) {
+ if (dependency != null && !dependency.excluded) {
final GenericName typeName = dependency.getName(analyzer);
String propertyName = toHeuristicLabel(typeName.tip().toString());
final String base = propertyName;
@@ -315,7 +315,7 @@ abstract class FeatureAnalyzer {
propertyName = base + '-' + ++count;
}
dependency.propertyName = propertyName;
- final Table table = analyzer.table(dependency, typeName, null); // `null` because exported, not imported.
+ final Table table = analyzer.table(dependency, typeName, id);
final AssociationRoleBuilder association;
if (table != null) {
dependency.setSearchTable(analyzer, table, pk, Relation.Direction.EXPORT);
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 475a578..164acd4 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
@@ -124,7 +124,7 @@ final class QueryAnalyzer extends FeatureAnalyzer {
{
if (reflect.next()) do {
final Relation relation = new Relation(analyzer, direction, reflect);
- if (columnNames.containsAll(relation.getForeignerKeys())) {
+ if (columnNames.containsAll(relation.getOwnerColumns())) {
if (isImport) {
addForeignerKeys(relation);
}
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
index 4bfae80..af3fb28 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Relation.java
@@ -53,7 +53,7 @@ import org.apache.sis.util.Debug;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.0
* @module
*/
@@ -170,6 +170,16 @@ final class Relation extends TableReference {
private boolean useFullKey;
/**
+ * Whether this relation should be omitted from the list of feature type properties.
+ * This is a temporary information used only at {@code FeatureType} construction time.
+ * A relation is excluded if table <var>A</var> is a dependency of table <var>B</var>
+ * (defined by foreigner keys) and the relation is pointing back to <var>A</var>.
+ *
+ * @since 1.2
+ */
+ boolean excluded;
+
+ /**
* Creates a new relation for an imported key. The given {@code ResultSet} must be positioned
* on the first row of {@code DatabaseMetaData.getImportedKeys(catalog, schema, table)} result,
* and the result must be sorted in the order of the given keys:
@@ -351,10 +361,10 @@ final class Relation extends TableReference {
}
/**
- * Returns whether {@link #setSearchTable setSearchTable(…)} has been invoked.
+ * Returns whether {@link #setSearchTable setSearchTable(…)} needs to be invoked.
*/
- final boolean isSearchTableDefined() {
- return searchTable != null;
+ final boolean isSearchTableDeferred() {
+ return !excluded && searchTable == null;
}
/**
@@ -369,11 +379,12 @@ final class Relation extends TableReference {
/**
* Returns the foreigner key columns of the table that contains this relation.
- * This method should be invoked only for {@link Direction#IMPORT} (otherwise the method name is wrong).
- * This method returns only the foreigner key columns known to this relation; this is not necessarily
+ * For {@link Direction#IMPORT}, this is the foreigner keys in the enclosing table.
+ * For {@link Direction#EXPORT}, this is the primary keys in the dependency table.
+ * This method returns only the columns known to this relation; this is not necessarily
* all the enclosing table foreigner keys. Some columns may be used in more than one relation.
*/
- final Collection<String> getForeignerKeys() {
+ final Collection<String> getOwnerColumns() {
return columns.values();
}
@@ -440,6 +451,9 @@ final class Relation extends TableReference {
if (freeText != null) {
label = freeText + " ⟶ " + label;
}
+ if (excluded) {
+ label += " (excluded)";
+ }
final TreeTable.Node node = newChild(parent, label);
for (final Map.Entry<String,String> e : columns.entrySet()) {
newChild(node, e.getValue() + arrow + e.getKey());
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SchemaModifier.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SchemaModifier.java
index f648721..8a960ba 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SchemaModifier.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SchemaModifier.java
@@ -32,7 +32,7 @@ import org.opengis.feature.FeatureType;
* in which case the {@link TableReference} argument is no longer necessary.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.1
* @module
*/
@@ -56,6 +56,18 @@ public interface SchemaModifier {
}
/**
+ * Returns {@code true} if the given dependency is allowed to have an association to its dependent feature.
+ * A value of {@code true} creates a cyclic dependency, which {@code SQLStore} can manage but may surprise users.
+ * The default value is {@code false}.
+ *
+ * @param dependency the dependency table.
+ * @return whether the dependency is allowed to have an association to its parent.
+ */
+ default boolean isCyclicAssociationAllowed(TableReference dependency) {
+ return false;
+ }
+
+ /**
* The option for declaring a schema modifier at {@link org.apache.sis.storage.sql.SQLStore} creation time.
*
* @todo if we move this key in public API in the future, then it would be a
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 0d72ab8..a0f71a5 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
@@ -56,7 +56,7 @@ import org.opengis.feature.FeatureAssociationRole;
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.0
* @module
*/
@@ -195,8 +195,8 @@ final class Table extends AbstractFeatureSet {
/**
* Sets the search tables on all {@link Relation} instances for which this operation has been deferred.
- * This happen when a table could not be obtained because of circular dependency. This method is invoked
- * after all tables have been created in order to fill such holes.
+ * This happen when a table could not be obtained because of circular dependency.
+ * This method is invoked after all tables have been created in order to fill such holes.
*
* @param tables all tables created.
*/
@@ -209,7 +209,7 @@ final class Table extends AbstractFeatureSet {
default: continue;
}
for (final Relation relation : relations) {
- if (!relation.isSearchTableDefined()) {
+ if (relation.isSearchTableDeferred()) {
/*
* `ClassCastException` should never occur below because `relation.propertyName` fields
* have been set to association names. If `ClassCastException` occurs here, it is a bug
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 736bb44..9f76123 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
@@ -35,16 +35,16 @@ import org.apache.sis.internal.util.Strings;
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.1
* @module
*/
final class TableAnalyzer extends FeatureAnalyzer {
/**
- * If the analyzed table is imported by the foreigner keys of another table, the parent table.
- * Otherwise {@code null}. This is relevant only for {@link Relation.Direction#EXPORT}.
+ * If the analyzed table is imported/exported by foreigner keys,
+ * the table that "contains" this table. Otherwise {@code null}.
*/
- private final TableReference importedBy;
+ private final TableReference dependencyOf;
/**
* The table/schema name width {@code '_'} and {@code '%'} characters escaped.
@@ -57,16 +57,16 @@ final class TableAnalyzer extends FeatureAnalyzer {
* The table is identified by {@code id}, which contains a (catalog, schema, name) tuple.
* The catalog and schema parts are optional and can be null, but the table is mandatory.
*
- * @param id the catalog, schema and table name of the table to analyze.
- * @param importedBy if the analyzed table is imported by the foreigner keys of another table,
- * the parent table. Otherwise {@code null}.
+ * @param id the catalog, schema and table name of the table to analyze.
+ * @param dependencyOf if the analyzed table is imported/exported by foreigner keys,
+ * the table that "contains" this table. Otherwise {@code null}.
* @throws SQLException if an error occurred while fetching information from the database.
*/
- TableAnalyzer(final Analyzer analyzer, final TableReference id, final TableReference importedBy) throws SQLException {
+ TableAnalyzer(final Analyzer analyzer, final TableReference id, final TableReference dependencyOf) throws SQLException {
super(analyzer, id);
- this.importedBy = importedBy;
- this.tableEsc = escape(id.table);
- this.schemaEsc = escape(id.schema);
+ this.dependencyOf = dependencyOf;
+ this.tableEsc = escape(id.table);
+ this.schemaEsc = escape(id.schema);
try (ResultSet reflect = analyzer.metadata.getPrimaryKeys(id.catalog, id.schema, id.table)) {
while (reflect.next()) {
primaryKey.add(analyzer.getUniqueString(reflect, Reflection.COLUMN_NAME));
@@ -115,9 +115,20 @@ final class TableAnalyzer extends FeatureAnalyzer {
{
if (reflect.next()) do {
final Relation relation = new Relation(analyzer, direction, reflect);
+ if (relation.equals(dependencyOf)) {
+ if (!isImport) {
+ /*
+ * For export keys we can just forget the relation because there is no column in the
+ * analyzed table for the relation (i.e. the foreigner keys are not in this table).
+ */
+ continue;
+ }
+ final SchemaModifier customizer = analyzer.customizer;
+ relation.excluded = (customizer == null) || !customizer.isCyclicAssociationAllowed(id);
+ }
if (isImport) {
addForeignerKeys(relation);
- } else if (relation.equals(importedBy)) {
+ } else if (relation.excluded) {
continue;
}
relations.add(relation);
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/package-info.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/package-info.java
index cb0c948..145b81c 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/package-info.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/package-info.java
@@ -31,7 +31,7 @@
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.0
* @module
*/
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java
index 85e98ec..5fab066 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java
@@ -56,7 +56,7 @@
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.0
* @module
*/
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 3dcf87b..baf9f19 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
@@ -24,11 +24,13 @@ import java.util.Collection;
import java.util.stream.Stream;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.FeatureQuery;
import org.apache.sis.storage.StorageConnector;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.filter.DefaultFilterFactory;
import org.apache.sis.internal.feature.AttributeConvention;
-import org.apache.sis.storage.FeatureQuery;
+import org.apache.sis.internal.sql.feature.SchemaModifier;
+import org.apache.sis.internal.sql.feature.TableReference;
import org.apache.sis.test.sql.TestDatabase;
import org.apache.sis.test.TestUtilities;
import org.apache.sis.test.TestCase;
@@ -51,7 +53,7 @@ import org.opengis.filter.SortOrder;
*
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.1
* @module
*/
@@ -87,6 +89,13 @@ public final strictfp class SQLStoreTest extends TestCase {
};
/**
+ * Whether dependencies are allowed to have an association to their dependent feature.
+ *
+ * @see SchemaModifier#isCyclicAssociationAllowed(TableReference)
+ */
+ private boolean isCyclicAssociationAllowed;
+
+ /**
* The {@code Country} value for Canada, or {@code null} if not yet visited.
* This feature should appear twice, and all those occurrences should use the exact same instance.
* We use that for verifying the {@code Table.instanceForPrimaryKeys} caching.
@@ -143,7 +152,9 @@ public final strictfp class SQLStoreTest extends TestCase {
* and will be the only schema to exist (ignoring system schema); i.e. we assume that there
* is no ambiguity if we do not specify the schema in {@link SQLStore} constructor.
*/
- private void test(final TestDatabase database, final boolean inMemory) throws Exception {
+ private void test(final TestDatabase database, final boolean inMemory)
+ throws Exception
+ {
final String[] scripts = {
"CREATE SCHEMA " + SCHEMA + ';',
"file:Features.sql"
@@ -154,42 +165,8 @@ public final strictfp class SQLStoreTest extends TestCase {
try (TestDatabase tmp = database) { // TODO: omit `tmp` with JDK16.
tmp.executeSQL(SQLStoreTest.class, scripts);
final StorageConnector connector = new StorageConnector(tmp.source);
- try (SQLStore store = new SQLStore(new SQLStoreProvider(), connector,
- ResourceDefinition.table(null, inMemory ? null : SCHEMA, "Cities")))
- {
- final FeatureSet cities = store.findResource("Cities");
- /*
- * Feature properties should be in same order than columns in the database table, except for
- * the generated identifier. Note that the country is an association to another feature.
- */
- verifyFeatureType(cities.getType(),
- new String[] {"sis:identifier", "pk:country", "country", "native_name", "english_name", "population", "parks"},
- new Object[] {null, String.class, "Countries", String.class, String.class, Integer.class, "Parks"});
-
- verifyFeatureType(store.findResource("Countries").getType(),
- new String[] {"sis:identifier", "code", "native_name"},
- new Object[] {null, String.class, String.class});
-
- verifyFeatureType(store.findResource("Parks").getType(),
- new String[] {"sis:identifier", "pk:country", "FK_City", "city", "native_name", "english_name"},
- new Object[] {null, String.class, "Cities", String.class, String.class, String.class});
-
- final Map<String,Integer> countryCount = new HashMap<>();
- try (Stream<Feature> features = cities.features(false)) {
- features.forEach((f) -> verifyContent(f, countryCount, true));
- }
- assertEquals(Integer.valueOf(2), countryCount.remove("CAN"));
- assertEquals(Integer.valueOf(1), countryCount.remove("FRA"));
- assertEquals(Integer.valueOf(1), countryCount.remove("JPN"));
- assertTrue(countryCount.isEmpty());
- /*
- * Verify overloaded stream operations (sorting, etc.).
- */
- verifySimpleQuerySorting(store);
- verifySimpleWhere(store);
- verifyStreamOperations(store.findResource("Cities"));
- canada = null;
- }
+ final ResourceDefinition table = ResourceDefinition.table(null, inMemory ? null : SCHEMA, "Cities");
+ testTableQuery(connector, table);
/*
* Verify using SQL statements instead of tables.
*/
@@ -197,7 +174,73 @@ public final strictfp class SQLStoreTest extends TestCase {
verifyNestedSQLQuery(connector);
verifyLimitOffsetAndColumnSelectionFromQuery(connector);
verifyDistinctQuery(connector);
+ /*
+ * Test on the table again, but with cyclic associations enabled.
+ */
+ connector.setOption(SchemaModifier.OPTION, new SchemaModifier() {
+ @Override public boolean isCyclicAssociationAllowed(TableReference dependency) {
+ return true;
+ }
+ });
+ isCyclicAssociationAllowed = true;
+ testTableQuery(connector, table);
+ }
+ }
+
+ /**
+ * Creates a {@link SQLStore} instance with the specified table as a resource, then tests some queries.
+ */
+ private void testTableQuery(final StorageConnector connector, final ResourceDefinition table) throws Exception {
+ try (SQLStore store = new SQLStore(new SQLStoreProvider(), connector, table)) {
+ verifyFeatureTypes(store);
+ final Map<String,Integer> countryCount = new HashMap<>();
+ try (Stream<Feature> features = store.findResource("Cities").features(false)) {
+ features.forEach((f) -> verifyContent(f, countryCount));
+ }
+ assertEquals(Integer.valueOf(2), countryCount.remove("CAN"));
+ assertEquals(Integer.valueOf(1), countryCount.remove("FRA"));
+ assertEquals(Integer.valueOf(1), countryCount.remove("JPN"));
+ assertTrue(countryCount.isEmpty());
+ /*
+ * Verify overloaded stream operations (sorting, etc.).
+ */
+ verifySimpleQuerySorting(store);
+ verifySimpleWhere(store);
+ verifyStreamOperations(store.findResource("Cities"));
+ }
+ canada = null;
+ }
+
+ /**
+ * Verifies the feature types of the "Cities" resource and its dependencies.
+ * Feature properties should be in same order than columns in the database table, except for
+ * the generated identifier. Note that the country is an association to another feature.
+ *
+ * @param isCyclicAssociationAllowed whether dependencies are allowed to have an association
+ * to their dependent feature, which create a cyclic dependency.
+ */
+ private void verifyFeatureTypes(final SQLStore store) throws DataStoreException {
+ verifyFeatureType(store.findResource("Cities").getType(),
+ new String[] {"sis:identifier", "pk:country", "country", "native_name", "english_name", "population", "parks"},
+ new Object[] {null, String.class, "Countries", String.class, String.class, Integer.class, "Parks"});
+
+ verifyFeatureType(store.findResource("Countries").getType(),
+ new String[] {"sis:identifier", "code", "native_name"},
+ new Object[] {null, String.class, String.class});
+ /*
+ * If cyclic dependencies are allowed, an additional properties "FK_City" is present
+ * compared to the case where cyclic dependencies are avoided.
+ */
+ final String[] expectedNames;
+ final Object[] expectedTypes;
+ if (isCyclicAssociationAllowed) {
+ expectedNames = new String[] {"sis:identifier", "pk:country", "FK_City", "city", "native_name", "english_name"};
+ expectedTypes = new Object[] {null, String.class, "Cities", String.class, String.class, String.class};
+ } else {
+ expectedNames = new String[] {"sis:identifier", "country", "city", "native_name", "english_name"};
+ expectedTypes = new Object[] {null, String.class, String.class, String.class, String.class};
}
+ verifyFeatureType(store.findResource("Parks").getType(), expectedNames, expectedTypes);
}
/**
@@ -234,9 +277,8 @@ 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 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 isTable) {
+ private void verifyContent(final Feature feature, final Map<String,Integer> countryCount) {
final String city = feature.getPropertyValue("native_name").toString();
final City c;
boolean isCanada = false;
@@ -288,7 +330,7 @@ public final strictfp class SQLStoreTest extends TestCase {
* Verify the reverse association form Parks to Cities.
* This create a cyclic graph, but SQLStore is capable to handle it.
*/
- if (isTable) {
+ if (isCyclicAssociationAllowed) {
assertSame("City → Park → City", feature, pf.getPropertyValue("FK_City"));
}
}
@@ -420,13 +462,14 @@ public final strictfp class SQLStoreTest extends TestCase {
final FeatureSet cities = store.findResource("LargeCities");
final Map<String,Integer> countryCount = new HashMap<>();
try (Stream<Feature> features = cities.features(false)) {
- features.forEach((f) -> verifyContent(f, countryCount, false));
+ features.forEach((f) -> verifyContent(f, countryCount));
}
assertEquals(Integer.valueOf(1), countryCount.remove("CAN"));
assertEquals(Integer.valueOf(1), countryCount.remove("FRA"));
assertEquals(Integer.valueOf(1), countryCount.remove("JPN"));
assertTrue(countryCount.isEmpty());
}
+ canada = null;
}
/**