Author: gk
Date: Wed Jul 22 14:03:02 2026
New Revision: 1936487
Log:
TORQUE-369 patches (TORQUE-369-1-fix), TORQUE-369-2-tests), thanks to Max
Philipp Wriedt.
Resolves issue "LoadExternalSchemaTransformer recursively includes when
cross-referencing" using an InclusionTracker object or a set to differentiate
between references and inline schemata (to include or to skip). No support for
nested references from subdirectories; contains also tests with example
schemata.
Added:
db/torque/trunk/torque-templates/src/test/cyclic-external-schema/
db/torque/trunk/torque-templates/src/test/cyclic-external-schema/a-schema.xml
db/torque/trunk/torque-templates/src/test/cyclic-external-schema/b-schema.xml
db/torque/trunk/torque-templates/src/test/cyclic-include-schema/
db/torque/trunk/torque-templates/src/test/cyclic-include-schema/a-schema.xml
db/torque/trunk/torque-templates/src/test/cyclic-include-schema/b-schema.xml
db/torque/trunk/torque-templates/src/test/diamond-include-schema/
db/torque/trunk/torque-templates/src/test/diamond-include-schema/left-schema.xml
db/torque/trunk/torque-templates/src/test/diamond-include-schema/right-schema.xml
db/torque/trunk/torque-templates/src/test/diamond-include-schema/root-schema.xml
db/torque/trunk/torque-templates/src/test/diamond-include-schema/shared-schema.xml
db/torque/trunk/torque-templates/src/test/java/org/apache/torque/templates/CyclicSchemaReferenceTest.java
Modified:
db/torque/trunk/torque-templates/src/main/java/org/apache/torque/templates/transformer/IncludeSchemaTransformer.java
db/torque/trunk/torque-templates/src/main/java/org/apache/torque/templates/transformer/LoadExternalSchemaTransformer.java
Modified:
db/torque/trunk/torque-templates/src/main/java/org/apache/torque/templates/transformer/IncludeSchemaTransformer.java
==============================================================================
---
db/torque/trunk/torque-templates/src/main/java/org/apache/torque/templates/transformer/IncludeSchemaTransformer.java
Wed Jul 22 13:34:40 2026 (r1936486)
+++
db/torque/trunk/torque-templates/src/main/java/org/apache/torque/templates/transformer/IncludeSchemaTransformer.java
Wed Jul 22 14:03:02 2026 (r1936487)
@@ -20,8 +20,12 @@ package org.apache.torque.templates.tran
*/
import java.io.File;
+import java.io.IOException;
import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -92,7 +96,29 @@ public class IncludeSchemaTransformer im
final ControllerState controllerState)
throws SourceTransformerException
{
- final SourceElement root = (SourceElement) modelRoot;
+ return transform(
+ (SourceElement) modelRoot,
+ controllerState,
+ new InclusionTracker(controllerState.getSourceFile()));
+ }
+
+ /**
+ * Loads the included schema objects into the current source graph.
+ *
+ * @param root the database root element of the source tree, not null.
+ * @param controllerState the controller state, not null.
+ * @param tracker keeps track of the schema files already included in this
+ * transformation, not null.
+ * @return SourceElement
+ * @throws SourceTransformerException if the transformation fails,
+ * or if the schema files include each other cyclically.
+ */
+ private SourceElement transform(
+ final SourceElement root,
+ final ControllerState controllerState,
+ final InclusionTracker tracker)
+ throws SourceTransformerException
+ {
final List<SourceElement> includeSchemaList
= root.getChildren(TorqueSchemaElementName.INCLUDE_SCHEMA);
final List<SourceElement> childrenList = root.getChildren();
@@ -114,6 +140,12 @@ public class IncludeSchemaTransformer im
.toString();
final File includeSchemaPath
= new File(includeSchemaBaseDir, relativePath);
+ if (!tracker.enter(includeSchemaPath))
+ {
+ log.debug("Skipping included file " + includeSchemaPath
+ + ", it was already included in this transformation.");
+ continue;
+ }
log.trace("Trying to read included file " + includeSchemaPath);
try
{
@@ -126,7 +158,10 @@ public class IncludeSchemaTransformer im
log.trace("successfully read included file "
+ includeSchemaPath);
- this.transform(includeSchemaRootElement, controllerState);
+ this.transform(
+ includeSchemaRootElement,
+ controllerState,
+ tracker);
// disattach children from their current parent
// so that the new parent is the primary parent
@@ -150,6 +185,10 @@ public class IncludeSchemaTransformer im
e);
throw new SourceTransformerException(e);
}
+ finally
+ {
+ tracker.leave(includeSchemaPath);
+ }
}
return root;
}
@@ -167,6 +206,29 @@ public class IncludeSchemaTransformer im
final ControllerState controllerState)
throws SourceTransformerException
{
+ return transform(
+ database,
+ controllerState,
+ new InclusionTracker(controllerState.getSourceFile()));
+ }
+
+ /**
+ * Loads the included schema content into the current model.
+ *
+ * @param database the database root element of the source tree, not null.
+ * @param controllerState the controller state, not null.
+ * @param tracker keeps track of the schema files already included in this
+ * transformation, not null.
+ * @return Database object
+ * @throws SourceTransformerException if the transformation fails,
+ * or if the schema files include each other cyclically.
+ */
+ private Database transform(
+ final Database database,
+ final ControllerState controllerState,
+ final InclusionTracker tracker)
+ throws SourceTransformerException
+ {
// copy original list to iterate so we can modify the model list
// during iteration
final List<IncludeSchema> originalIncludeSchemaList
@@ -185,6 +247,12 @@ public class IncludeSchemaTransformer im
}
final File includeSchemaPath
= new File(includeSchemaBaseDir, includeSchema.filename);
+ if (!tracker.enter(includeSchemaPath))
+ {
+ log.debug("Skipping included file " + includeSchemaPath
+ + ", it was already included in this transformation.");
+ continue;
+ }
log.trace("Trying to read included file " + includeSchemaPath);
try
{
@@ -201,7 +269,7 @@ public class IncludeSchemaTransformer im
includeSchemaRootElement,
controllerState);
- this.transform(includedDatabase, controllerState);
+ this.transform(includedDatabase, controllerState, tracker);
// add child elements from loaded schema
database.optionList.addAll(includedDatabase.optionList);
@@ -223,7 +291,121 @@ public class IncludeSchemaTransformer im
e);
throw new SourceTransformerException(e);
}
+ finally
+ {
+ tracker.leave(includeSchemaPath);
+ }
}
return database;
}
+
+ /**
+ * Keeps track of the schema files included during a single transformation.
+ *
+ * <p>Two cases must be told apart. A schema file which is reached again
+ * while it is still being processed further up the recursion forms a
+ * cycle; as include-schema inlines the content of the included file, the
+ * result would depend on the schema file the generation was started from,
+ * so this is treated as an error. A schema file which is reached again
+ * after it has been completely processed is simply included by more than
+ * one schema file; it is skipped, because inlining it twice would
+ * duplicate its tables.</p>
+ *
+ * <p>The tracker is created per transformation and handed down the
+ * recursion, because the transformer is held in static fields by its
+ * callers and must not keep transformation state of its own.</p>
+ */
+ private static final class InclusionTracker
+ {
+ /** The canonical paths of all schema files included so far. */
+ private final Set<String> included = new HashSet<>();
+
+ /**
+ * The canonical paths of the schema files currently being processed,
+ * in the order in which they include each other.
+ */
+ private final LinkedHashSet<String> inProgress = new LinkedHashSet<>();
+
+ /**
+ * Constructor.
+ *
+ * @param startFile the schema file the transformation starts from,
+ * may be null.
+ */
+ private InclusionTracker(final File startFile)
+ {
+ if (startFile != null)
+ {
+ final String startPath = canonicalPath(startFile);
+ included.add(startPath);
+ inProgress.add(startPath);
+ }
+ }
+
+ /**
+ * Registers that a schema file is about to be included.
+ * Every call which returns true must be matched by a call to
+ * {@link #leave(File)}.
+ *
+ * @param schemaFile the schema file to include, not null.
+ * @return true if the file should be included, false if it was
+ * already included and should be skipped.
+ * @throws SourceTransformerException if the file is already being
+ * processed further up the recursion, i.e. the schema files
+ * include each other cyclically.
+ */
+ private boolean enter(final File schemaFile)
+ throws SourceTransformerException
+ {
+ final String path = canonicalPath(schemaFile);
+ if (inProgress.contains(path))
+ {
+ throw new SourceTransformerException(
+ "Cyclic include-schema reference: "
+ + String.join(" -> ", inProgress) + " -> " + path
+ + ". A schema file must not include itself,"
+ + " directly or indirectly.");
+ }
+ if (!included.add(path))
+ {
+ return false;
+ }
+ inProgress.add(path);
+ return true;
+ }
+
+ /**
+ * Registers that a schema file has been processed completely.
+ *
+ * @param schemaFile the schema file, not null.
+ */
+ private void leave(final File schemaFile)
+ {
+ inProgress.remove(canonicalPath(schemaFile));
+ }
+
+ /**
+ * Returns the canonical path of a schema file, which is used to
+ * recognize a schema file which was already included. The database
+ * name cannot serve as identity here because it is optional in the
+ * schema and need not be unique across schema files.
+ *
+ * @param file the schema file, not null.
+ * @return the canonical path, or the absolute path if the canonical
+ * path cannot be determined.
+ */
+ private static String canonicalPath(final File file)
+ {
+ try
+ {
+ return file.getCanonicalPath();
+ }
+ catch (final IOException e)
+ {
+ log.debug("Could not determine canonical path of " + file
+ + ", falling back to the absolute path.", e);
+ return file.getAbsolutePath();
+ }
+ }
+ }
}
Modified:
db/torque/trunk/torque-templates/src/main/java/org/apache/torque/templates/transformer/LoadExternalSchemaTransformer.java
==============================================================================
---
db/torque/trunk/torque-templates/src/main/java/org/apache/torque/templates/transformer/LoadExternalSchemaTransformer.java
Wed Jul 22 13:34:40 2026 (r1936486)
+++
db/torque/trunk/torque-templates/src/main/java/org/apache/torque/templates/transformer/LoadExternalSchemaTransformer.java
Wed Jul 22 14:03:02 2026 (r1936487)
@@ -20,7 +20,10 @@ package org.apache.torque.templates.tran
*/
import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -100,7 +103,28 @@ public class LoadExternalSchemaTransform
final ControllerState controllerState)
throws SourceTransformerException
{
- final SourceElement root = (SourceElement) modelRoot;
+ return transform(
+ (SourceElement) modelRoot,
+ controllerState,
+ newVisitedSchemaSet(controllerState));
+ }
+
+ /**
+ * Loads the external schemata tables into the current graph.
+ *
+ * @param root the database root element of the source tree, not null.
+ * @param controllerState the controller state, not null.
+ * @param visitedSchemaPaths the canonical paths of all schema files
+ * already loaded in this transformation, not null.
+ * @return SourceElement
+ * @throws SourceTransformerException if the transformation fails.
+ */
+ private SourceElement transform(
+ final SourceElement root,
+ final ControllerState controllerState,
+ final Set<String> visitedSchemaPaths)
+ throws SourceTransformerException
+ {
final List<SourceElement> externalSchemaElementList
= root.getChildren(TorqueSchemaElementName.EXTERNAL_SCHEMA);
SourceElement allTables = root.getChild(
@@ -137,6 +161,14 @@ public class LoadExternalSchemaTransform
.toString();
final File externalSchemaPath
= new File(externalSchemaBaseDir, relativePath);
+ if (!visitedSchemaPaths.add(canonicalPath(externalSchemaPath)))
+ {
+ log.info("Skipping external schema " + externalSchemaPath
+ + " because it was already loaded in this"
+ + " transformation (cyclic external-schema"
+ + " reference).");
+ continue;
+ }
try
{
final FileSource fileSource = new FileSource(
@@ -153,8 +185,10 @@ public class LoadExternalSchemaTransform
rootDatabaseName,
controllerState);
- // todo infinite loop
- this.transform(externalSchemaRootElement, controllerState);
+ this.transform(
+ externalSchemaRootElement,
+ controllerState,
+ visitedSchemaPaths);
externalSchemaElement.getChildren().add(
externalSchemaRootElement);
@@ -219,6 +253,28 @@ public class LoadExternalSchemaTransform
final ControllerState controllerState)
throws SourceTransformerException
{
+ return transform(
+ database,
+ controllerState,
+ newVisitedSchemaSet(controllerState));
+ }
+
+ /**
+ * Loads the external schemata tables into the current model.
+ *
+ * @param database the database root element of the source tree, not null.
+ * @param controllerState the controller state, not null.
+ * @param visitedSchemaPaths the canonical paths of all schema files
+ * already loaded in this transformation, not null.
+ * @return Database object
+ * @throws SourceTransformerException if the transformation fails.
+ */
+ private Database transform(
+ final Database database,
+ final ControllerState controllerState,
+ final Set<String> visitedSchemaPaths)
+ throws SourceTransformerException
+ {
for (final ExternalSchema externalSchema : database.externalSchemaList)
{
File externalSchemaBaseDir;
@@ -233,6 +289,14 @@ public class LoadExternalSchemaTransform
}
final File externalSchemaPath
= new File(externalSchemaBaseDir, externalSchema.filename);
+ if (!visitedSchemaPaths.add(canonicalPath(externalSchemaPath)))
+ {
+ log.info("Skipping external schema " + externalSchemaPath
+ + " because it was already loaded in this"
+ + " transformation (cyclic external-schema"
+ + " reference).");
+ continue;
+ }
try
{
final FileSource fileSource = new FileSource(
@@ -245,31 +309,18 @@ public class LoadExternalSchemaTransform
= (Database) toModelTransformer.transform(
externalSchemaRootElement,
controllerState);
-
-// log.warn( "get value of database.rootDatabaseName and set "
+ database.rootDatabaseName );
externalDatabase.rootDatabaseName = database.rootDatabaseName;
- // TODO continue if
-// if (database.externalSchemaList.contains( externalSchema)) {
-// log.warn( "checked root has externalShema included: " +
externalSchema );
-// }
-// log.warn( "database.name " + database.name );
-// log.warn( "externalDatabase.externalSchemaList " +
externalDatabase.externalSchemaList.toString() );
-// log.warn( "externalDatabase.name " + externalDatabase.name );
-// boolean identicalInclude =
externalDatabase.externalSchemaList.stream().anyMatch(
-// extSchema -> extSchema != null &&
extSchema.database.name.equals( externalDatabase.name ));
-// //.parent.externalSchemaList.contains(
externalDatabase ) );
-// log.warn( "identicalInclude " + identicalInclude );
-// if (identicalInclude) {
-// return database;
-// }
// TODO fix static access
DatabaseMapInitTransformer.setDatabaseMapInitClassNameAttributes(
externalSchemaRootElement,
database.rootDatabaseName,
controllerState);
- this.transform(externalDatabase, controllerState);
+ this.transform(
+ externalDatabase,
+ controllerState,
+ visitedSchemaPaths);
externalSchema.database = externalDatabase;
@@ -291,4 +342,51 @@ public class LoadExternalSchemaTransform
database.allViews.addAll(database.viewList);
return database;
}
+
+ /**
+ * Creates the set of schema files already loaded in a transformation.
+ * The set is seeded with the schema file the transformation starts from,
+ * so that an external schema referencing its own origin is detected.
+ * The set is created per invocation and handed down the recursion because
+ * this transformer is held in static fields by its callers and must not
+ * keep transformation state of its own.
+ *
+ * @param controllerState the controller state, not null.
+ * @return a new mutable set of canonical schema file paths, not null.
+ */
+ private Set<String> newVisitedSchemaSet(
+ final ControllerState controllerState)
+ {
+ final Set<String> visitedSchemaPaths = new HashSet<>();
+ final File sourceFile = controllerState.getSourceFile();
+ if (sourceFile != null)
+ {
+ visitedSchemaPaths.add(canonicalPath(sourceFile));
+ }
+ return visitedSchemaPaths;
+ }
+
+ /**
+ * Returns the canonical path of a schema file, which is used to recognize
+ * a schema file which was already loaded. The database name cannot serve
+ * as identity here because it is optional in the schema and need not be
+ * unique across schema files.
+ *
+ * @param file the schema file, not null.
+ * @return the canonical path, or the absolute path if the canonical path
+ * cannot be determined.
+ */
+ private static String canonicalPath(final File file)
+ {
+ try
+ {
+ return file.getCanonicalPath();
+ }
+ catch (final IOException e)
+ {
+ log.debug("Could not determine canonical path of " + file
+ + ", falling back to the absolute path.", e);
+ return file.getAbsolutePath();
+ }
+ }
}
Added:
db/torque/trunk/torque-templates/src/test/cyclic-external-schema/a-schema.xml
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
db/torque/trunk/torque-templates/src/test/cyclic-external-schema/a-schema.xml
Wed Jul 22 14:03:02 2026 (r1936487)
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="ISO-8859-1" standalone="no" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<!--
+ TORQUE-369: a-schema references b-schema and b-schema references back to
+ a-schema. Both schemata must be loadable without running into an endless
+ recursion.
+-->
+<database
+ xmlns="http://db.apache.org/torque/5.0/templates/database"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://db.apache.org/torque/5.0/templates/database
+
https://db.apache.org/torque/torque-5.0/documentation/orm-reference/database-5-0.xsd"
+ name="a"
+ defaultIdMethod="native">
+
+ <external-schema filename="b-schema.xml" />
+
+ <table name="a">
+ <column
+ name="a_id"
+ required="true"
+ primaryKey="true"
+ type="INTEGER"
+ />
+ <column
+ name="b_id"
+ required="true"
+ type="INTEGER"
+ />
+ <foreign-key foreignTable="b">
+ <reference local="b_id" foreign="b_id"/>
+ </foreign-key>
+ </table>
+</database>
Added:
db/torque/trunk/torque-templates/src/test/cyclic-external-schema/b-schema.xml
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
db/torque/trunk/torque-templates/src/test/cyclic-external-schema/b-schema.xml
Wed Jul 22 14:03:02 2026 (r1936487)
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="ISO-8859-1" standalone="no" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<!--
+ TORQUE-369: back reference to a-schema, which references this schema.
+-->
+<database
+ xmlns="http://db.apache.org/torque/5.0/templates/database"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://db.apache.org/torque/5.0/templates/database
+
https://db.apache.org/torque/torque-5.0/documentation/orm-reference/database-5-0.xsd"
+ name="b"
+ defaultIdMethod="native">
+
+ <external-schema filename="a-schema.xml" />
+
+ <table name="b">
+ <column
+ name="b_id"
+ required="true"
+ primaryKey="true"
+ type="INTEGER"
+ />
+ <column
+ name="a_id"
+ required="true"
+ type="INTEGER"
+ />
+ <foreign-key foreignTable="a">
+ <reference local="a_id" foreign="a_id"/>
+ </foreign-key>
+ </table>
+</database>
Added:
db/torque/trunk/torque-templates/src/test/cyclic-include-schema/a-schema.xml
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
db/torque/trunk/torque-templates/src/test/cyclic-include-schema/a-schema.xml
Wed Jul 22 14:03:02 2026 (r1936487)
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="ISO-8859-1" standalone="no" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<!--
+ TORQUE-369: a-schema includes b-schema, which includes a-schema again.
+ As include-schema inlines the content of the included file, this cycle is
+ an error and must be reported as such.
+-->
+<database
+ xmlns="http://db.apache.org/torque/5.0/templates/database"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://db.apache.org/torque/5.0/templates/database
+
https://db.apache.org/torque/torque-5.0/documentation/orm-reference/database-5-0.xsd"
+ name="cyclicinclude"
+ defaultIdMethod="native">
+
+ <include-schema filename="b-schema.xml" />
+
+ <table name="a">
+ <column
+ name="a_id"
+ required="true"
+ primaryKey="true"
+ type="INTEGER"
+ />
+ </table>
+</database>
Added:
db/torque/trunk/torque-templates/src/test/cyclic-include-schema/b-schema.xml
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
db/torque/trunk/torque-templates/src/test/cyclic-include-schema/b-schema.xml
Wed Jul 22 14:03:02 2026 (r1936487)
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1" standalone="no" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<!--
+ TORQUE-369: back reference to a-schema, which includes this schema.
+-->
+<database
+ xmlns="http://db.apache.org/torque/5.0/templates/database"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://db.apache.org/torque/5.0/templates/database
+
https://db.apache.org/torque/torque-5.0/documentation/orm-reference/database-5-0.xsd"
+ name="cyclicinclude"
+ defaultIdMethod="native">
+
+ <include-schema filename="a-schema.xml" />
+
+ <table name="b">
+ <column
+ name="b_id"
+ required="true"
+ primaryKey="true"
+ type="INTEGER"
+ />
+ </table>
+</database>
Added:
db/torque/trunk/torque-templates/src/test/diamond-include-schema/left-schema.xml
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
db/torque/trunk/torque-templates/src/test/diamond-include-schema/left-schema.xml
Wed Jul 22 14:03:02 2026 (r1936487)
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1" standalone="no" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<!--
+ TORQUE-369: includes shared-schema, as does the sibling schema.
+-->
+<database
+ xmlns="http://db.apache.org/torque/5.0/templates/database"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://db.apache.org/torque/5.0/templates/database
+
https://db.apache.org/torque/torque-5.0/documentation/orm-reference/database-5-0.xsd"
+ name="diamond"
+ defaultIdMethod="native">
+
+ <include-schema filename="shared-schema.xml" />
+
+ <table name="left">
+ <column
+ name="left_id"
+ required="true"
+ primaryKey="true"
+ type="INTEGER"
+ />
+ </table>
+</database>
Added:
db/torque/trunk/torque-templates/src/test/diamond-include-schema/right-schema.xml
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
db/torque/trunk/torque-templates/src/test/diamond-include-schema/right-schema.xml
Wed Jul 22 14:03:02 2026 (r1936487)
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1" standalone="no" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<!--
+ TORQUE-369: includes shared-schema, as does the sibling schema.
+-->
+<database
+ xmlns="http://db.apache.org/torque/5.0/templates/database"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://db.apache.org/torque/5.0/templates/database
+
https://db.apache.org/torque/torque-5.0/documentation/orm-reference/database-5-0.xsd"
+ name="diamond"
+ defaultIdMethod="native">
+
+ <include-schema filename="shared-schema.xml" />
+
+ <table name="right">
+ <column
+ name="right_id"
+ required="true"
+ primaryKey="true"
+ type="INTEGER"
+ />
+ </table>
+</database>
Added:
db/torque/trunk/torque-templates/src/test/diamond-include-schema/root-schema.xml
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
db/torque/trunk/torque-templates/src/test/diamond-include-schema/root-schema.xml
Wed Jul 22 14:03:02 2026 (r1936487)
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="ISO-8859-1" standalone="no" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<!--
+ TORQUE-369: root includes left and right, both of which include shared.
+ This is not a cycle, but shared must be inlined only once, otherwise its
+ tables are generated twice.
+-->
+<database
+ xmlns="http://db.apache.org/torque/5.0/templates/database"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://db.apache.org/torque/5.0/templates/database
+
https://db.apache.org/torque/torque-5.0/documentation/orm-reference/database-5-0.xsd"
+ name="diamond"
+ defaultIdMethod="native">
+
+ <include-schema filename="left-schema.xml" />
+ <include-schema filename="right-schema.xml" />
+
+ <table name="root">
+ <column
+ name="root_id"
+ required="true"
+ primaryKey="true"
+ type="INTEGER"
+ />
+ </table>
+</database>
Added:
db/torque/trunk/torque-templates/src/test/diamond-include-schema/shared-schema.xml
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
db/torque/trunk/torque-templates/src/test/diamond-include-schema/shared-schema.xml
Wed Jul 22 14:03:02 2026 (r1936487)
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="ISO-8859-1" standalone="no" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<!--
+ TORQUE-369: included by both left-schema and right-schema.
+-->
+<database
+ xmlns="http://db.apache.org/torque/5.0/templates/database"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://db.apache.org/torque/5.0/templates/database
+
https://db.apache.org/torque/torque-5.0/documentation/orm-reference/database-5-0.xsd"
+ name="diamond"
+ defaultIdMethod="native">
+
+ <table name="shared">
+ <column
+ name="shared_id"
+ required="true"
+ primaryKey="true"
+ type="INTEGER"
+ />
+ </table>
+</database>
Added:
db/torque/trunk/torque-templates/src/test/java/org/apache/torque/templates/CyclicSchemaReferenceTest.java
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
db/torque/trunk/torque-templates/src/test/java/org/apache/torque/templates/CyclicSchemaReferenceTest.java
Wed Jul 22 14:03:02 2026 (r1936487)
@@ -0,0 +1,286 @@
+package org.apache.torque.templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.torque.generator.configuration.UnitDescriptor;
+import
org.apache.torque.generator.configuration.option.MapOptionsConfiguration;
+import org.apache.torque.generator.configuration.paths.CustomProjectPaths;
+import
org.apache.torque.generator.configuration.paths.DefaultTorqueGeneratorPaths;
+import
org.apache.torque.generator.configuration.paths.Maven2DirectoryProjectPaths;
+import org.apache.torque.generator.configuration.paths.Maven2ProjectPaths;
+import org.apache.torque.generator.control.Controller;
+import org.apache.torque.generator.file.Fileset;
+import org.apache.torque.generator.source.SourceProvider;
+import org.apache.torque.generator.source.stream.FileSourceProvider;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests that schema files referencing each other cyclically are handled
+ * without running into an endless recursion.
+ *
+ * <p>See TORQUE-369. An external-schema reference is a reference to another
+ * database, so a back reference is a valid, if unusual, way to model foreign
+ * keys across schemata and the schema is only loaded once. An include-schema
+ * reference inlines the content of the included file, so a cycle would make
+ * the result depend on the schema file the generation was started from and is
+ * reported as an error. Including the same file from two different schema
+ * files is not a cycle and the file is inlined only once.</p>
+ *
+ * @version $Id$
+ */
+public class CyclicSchemaReferenceTest
+{
+ /** The configuration directory of the sql ddl templates. */
+ private static final File SQL_CONFIGURATION_DIR = new File(
+ "src/main/resources/org/apache/torque/templates/sql");
+
+ /**
+ * Two schemata referencing each other as external schema must be
+ * generated without an endless recursion, and each of them must yield
+ * its own create script.
+ *
+ * @throws Exception if the test fails.
+ */
+ @Test
+ public void testCyclicExternalSchemaIsLoadedOnce() throws Exception
+ {
+ final File generationDir
+ = new File("target/generated-sql-cyclic-external");
+ FileUtils.deleteDirectory(generationDir);
+
+ runGeneration(
+ new File("src/test/cyclic-external-schema"),
+ null,
+ generationDir);
+
+ final File aDdlScript = new File(generationDir, "a-schema.sql");
+ final File bDdlScript = new File(generationDir, "b-schema.sql");
+ assertTrue(aDdlScript.exists(),
+ "The ddl script for a-schema should have been generated");
+ assertTrue(bDdlScript.exists(),
+ "The ddl script for b-schema should have been generated");
+
+ // A ddl script contains the tables of its own schema only, the
+ // tables of the external schema are generated into their own script.
+ assertEquals(1, countCreateTable(aDdlScript, "a"));
+ assertEquals(0, countCreateTable(aDdlScript, "b"));
+ assertEquals(0, countCreateTable(bDdlScript, "a"));
+ assertEquals(1, countCreateTable(bDdlScript, "b"));
+
+ // The foreign keys into the other schema must still be resolved,
+ // which is only possible if the external schema was loaded despite
+ // the back reference to the schema it is loaded from.
+ assertTrue(contentOf(aDdlScript).contains("REFERENCES b"),
+ "The foreign key of a into the external schema b should have"
+ + " been resolved");
+ assertTrue(contentOf(bDdlScript).contains("REFERENCES a"),
+ "The foreign key of b into the external schema a should have"
+ + " been resolved");
+ }
+
+ /**
+ * Two schemata including each other must be rejected, because inlining
+ * them would make the result depend on the schema file the generation
+ * was started from.
+ */
+ @Test
+ public void testCyclicIncludeSchemaIsRejected()
+ {
+ final File generationDir
+ = new File("target/generated-sql-cyclic-include");
+
+ final Exception exception = assertThrows(
+ Exception.class,
+ () -> runGeneration(
+ new File("src/test/cyclic-include-schema"),
+ "a-schema.xml",
+ generationDir));
+
+ assertTrue(
+ messageChainOf(exception).contains(
+ "Cyclic include-schema reference"),
+ "The generation should have failed because of the cyclic"
+ + " include-schema reference, but failed with: "
+ + messageChainOf(exception));
+ }
+
+ /**
+ * A schema file which is included by two different schema files must be
+ * inlined only once, otherwise its tables are generated twice.
+ *
+ * @throws Exception if the test fails.
+ */
+ @Test
+ public void testSchemaIncludedTwiceIsInlinedOnce() throws Exception
+ {
+ final File generationDir
+ = new File("target/generated-sql-diamond-include");
+ FileUtils.deleteDirectory(generationDir);
+
+ runGeneration(
+ new File("src/test/diamond-include-schema"),
+ "root-schema.xml",
+ generationDir);
+
+ final File createScript
+ = new File(generationDir, "root-schema.sql");
+ assertTrue(createScript.exists(),
+ "The ddl script should have been generated");
+
+ assertEquals(1, countCreateTable(createScript, "shared"),
+ "The schema included by both left-schema and right-schema"
+ + " should have been inlined only once");
+ assertEquals(1, countCreateTable(createScript, "root"));
+ assertEquals(1, countCreateTable(createScript, "left"));
+ assertEquals(1, countCreateTable(createScript, "right"));
+ }
+
+ /**
+ * Runs the createdb sql generation on a schema directory.
+ *
+ * @param schemaDir the directory containing the schema files, not null.
+ * @param sourceInclude the only schema file to use as a source,
+ * or null to use all schema files in the directory.
+ * @param generationDir the directory to generate into, not null.
+ *
+ * @throws Exception if the generation fails.
+ */
+ private void runGeneration(
+ final File schemaDir,
+ final String sourceInclude,
+ final File generationDir)
+ throws Exception
+ {
+ final Map<String, String> overrideOptions = new HashMap<>();
+ overrideOptions.put("torque.database", "mysql");
+
+ final CustomProjectPaths projectPaths = new CustomProjectPaths(
+ new Maven2DirectoryProjectPaths(new File(".")));
+ projectPaths.setConfigurationDir(SQL_CONFIGURATION_DIR);
+ projectPaths.setSourceDir(schemaDir);
+ projectPaths.setOutputDirectory(null, generationDir);
+ projectPaths.setOutputDirectory(
+ Maven2ProjectPaths.MODIFIABLE_OUTPUT_DIR_KEY,
+ new File(generationDir.getPath() + "-modifiable"));
+
+ final UnitDescriptor unitDescriptor = new UnitDescriptor(
+ UnitDescriptor.Packaging.DIRECTORY,
+ projectPaths,
+ new DefaultTorqueGeneratorPaths());
+ unitDescriptor.setOverrideOptions(
+ new MapOptionsConfiguration(overrideOptions));
+ if (sourceInclude != null)
+ {
+ final Fileset sourceFileset = new Fileset();
+ final Set<String> sourceIncludes = new HashSet<>();
+ sourceIncludes.add(sourceInclude);
+ sourceFileset.setIncludes(sourceIncludes);
+ sourceFileset.setBasedir(projectPaths.getDefaultSourcePath());
+ final SourceProvider sourceProvider = new FileSourceProvider(
+ null,
+ sourceFileset,
+ null);
+ unitDescriptor.setOverrideSourceProvider(sourceProvider);
+ }
+
+ final List<UnitDescriptor> unitDescriptors = new ArrayList<>();
+ unitDescriptors.add(unitDescriptor);
+ new Controller().run(unitDescriptors);
+ }
+
+ /**
+ * Counts the create statements for a table in a generated sql script.
+ *
+ * @param sqlScript the generated sql script, not null.
+ * @param tableName the name of the table, not null.
+ *
+ * @return the number of create statements for the table.
+ *
+ * @throws Exception if the script cannot be read.
+ */
+ private int countCreateTable(final File sqlScript, final String tableName)
+ throws Exception
+ {
+ final String content = contentOf(sqlScript);
+ final Matcher matcher = Pattern.compile(
+ "CREATE TABLE\\s+`?" + Pattern.quote(tableName) + "`?\\s",
+ Pattern.CASE_INSENSITIVE).matcher(content);
+ int count = 0;
+ while (matcher.find())
+ {
+ count++;
+ }
+ return count;
+ }
+
+ /**
+ * Reads a generated sql script.
+ *
+ * @param sqlScript the generated sql script, not null.
+ *
+ * @return the content of the script, not null.
+ *
+ * @throws Exception if the script cannot be read.
+ */
+ private String contentOf(final File sqlScript) throws Exception
+ {
+ return new String(
+ Files.readAllBytes(sqlScript.toPath()),
+ StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Concatenates the messages of an exception and all its causes, so that
+ * an assertion does not depend on how deeply the generator wraps an
+ * exception thrown by a transformer.
+ *
+ * @param exception the exception, not null.
+ *
+ * @return the concatenated messages, not null.
+ */
+ private String messageChainOf(final Throwable exception)
+ {
+ final StringBuilder result = new StringBuilder();
+ Throwable current = exception;
+ while (current != null)
+ {
+ result.append(current.getMessage()).append('\n');
+ current = current.getCause();
+ }
+ return result.toString();
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]