# EntityJoinTree.java

    package org.codefilarete.stalactite.engine.runtime.load;

    import javax.annotation.Nullable;
    import java.util.ArrayDeque;
    import java.util.ArrayList;
    import java.util.IdentityHashMap;
    import java.util.Iterator;
    import java.util.Map.Entry;
    import java.util.Queue;
    import java.util.Random;
    import java.util.Set;
    import java.util.TreeSet;
    import java.util.function.BiFunction;
    import java.util.function.Consumer;
    import java.util.function.Function;

    import org.apache.commons.collections4.BidiMap;
    import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
    import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
    import org.codefilarete.reflection.Accessor;
    import org.codefilarete.stalactite.engine.runtime.load.EntityTreeInflater.TreeInflationContext;
    import org.codefilarete.stalactite.engine.runtime.load.MergeJoinNode.MergeJoinRowConsumer;
    import org.codefilarete.stalactite.mapping.AbstractTransformer;
    import org.codefilarete.stalactite.mapping.EntityMapping;
    import org.codefilarete.stalactite.mapping.RowTransformer;
    import org.codefilarete.stalactite.query.api.Fromable;
    import org.codefilarete.stalactite.query.api.JoinLink;
    import org.codefilarete.stalactite.query.api.QueryStatement.PseudoColumn;
    import org.codefilarete.stalactite.query.api.QueryStatement.PseudoTable;
    import org.codefilarete.stalactite.query.api.Selectable;
    import org.codefilarete.stalactite.query.model.Union;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.ddl.structure.Key;
    import org.codefilarete.stalactite.sql.ddl.structure.Table;
    import org.codefilarete.stalactite.sql.result.BeanRelationFixer;
    import org.codefilarete.stalactite.sql.result.ColumnedRow;
    import org.codefilarete.tool.Duo;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.VisibleForTesting;
    import org.codefilarete.tool.bean.Randomizer;
    import org.codefilarete.tool.bean.Randomizer.LinearRandomGenerator;
    import org.codefilarete.tool.collection.Collections;
    import org.codefilarete.tool.collection.Iterables;
    import org.codefilarete.tool.collection.KeepOrderSet;
    import org.codefilarete.tool.collection.Maps;
    import org.codefilarete.tool.collection.ReadOnlyList;

    import static org.codefilarete.stalactite.sql.ddl.structure.Table.COMPARATOR_ON_SCHEMA_AND_NAME;

    /**
     * Tree representing joins of a from clause, nodes are {@link JoinNode}.
     * It maintains an index of its joins based on an unique name for each, so they can be referenced outside {@link EntityJoinTree} without
     * depending on classes of this package (since the reference is a {@link String}).
     *
     * @author Guillaume Mary
     */
    public class EntityJoinTree<C, I> {

    	/**
    	 * Key of the very first {@link EntityJoinTree} added to the join structure (the one generated by constructor), see {@link #getRoot()}
    	 */
    	public static final String ROOT_JOIN_NAME = "ROOT";

    	private final JoinRoot<C, I, ?> root;

    	/**
    	 * A mapping between a name and a join to find them when we want to join one with another new one
    	 *
    	 * @see #addRelationJoin(String, EntityInflater, Accessor, Key, Key, String, JoinType, BeanRelationFixer, Set)
    	 * @see #indexKeyGenerator
    	 */
    	// Implemented as a LinkedHashMap to keep order only for debugging purpose
    	private final BidiMap<String, JoinNode<?, ?>> joinIndex = new DualLinkedHashBidiMap<>();

    	/**
    	 * The objet that will help to give node names / keys into the index (no impact on the generated SQL)
    	 *
    	 * @see #joinIndex
    	 */
    	private final NodeKeyGenerator indexKeyGenerator = new NodeKeyGenerator();

    	// because Table doesn't implement an hashCode and because we may have clone in JoinNodes, we use a smart TreeSet to avoid duplicates
    	private final Set<Table<?>> tablesToExcludeFromDDL = new TreeSet<>(COMPARATOR_ON_SCHEMA_AND_NAME);

    	private final Set<Table<?>> tablesToIncludeToDDL = new TreeSet<>(COMPARATOR_ON_SCHEMA_AND_NAME);

    	public EntityJoinTree(EntityMapping<C, I, ?> entityMapping) {
    		this(new EntityInflater.EntityMappingAdapter<>(entityMapping), entityMapping.getTargetTable());
    	}

    	public EntityJoinTree(EntityInflater<C, I> rootEntityInflater, Fromable table) {
    		this.root = new JoinRoot<>(this, rootEntityInflater, table);
    		this.joinIndex.put(ROOT_JOIN_NAME, root);
    	}

    	public EntityJoinTree(Function<EntityJoinTree<C, I>, JoinRoot<C, I, ?>> joinRootCreator) {
    		this.root = joinRootCreator.apply(this);
    		this.joinIndex.put(ROOT_JOIN_NAME, root);
    	}

    	public JoinRoot<C, I, ?> getRoot() {
    		return root;
    	}

    	/**
    	 * Returns mapping between {@link JoinNode} and their internal name.
    	 *
    	 * @return an unmodifiable version of the internal mapping (because its maintenance responsibility falls to current class)
    	 */
    	@VisibleForTesting
    	public BidiMap<String, JoinNode> getJoinIndex() {
    		return UnmodifiableBidiMap.unmodifiableBidiMap(joinIndex);
    	}

    	/**
    	 * Declares a {@link Table} to be included in DDL generation.
    	 * To be used for particular use cases because {@link #giveTables()} collects this tree tables.
    	 *
    	 * @param table any {@link Table}
    	 */
    	public void addTableToIncludeToDDL(Table<?> table) {
    		this.tablesToIncludeToDDL.add(table);
    	}

    	/**
    	 * Adds a join to this select.
    	 * Use for one-to-one or one-to-many cases when join is used to describe a related bean.
    	 *
    	 * @param <U> type of bean mapped by the given strategy
    	 * @param <T1> joined left table
    	 * @param <T2> joined right table
    	 * @param <ID> type of joined values
    	 * @param leftStrategyName the name of a (previously) registered join. {@code leftJoinColumn} must be a {@link Column} of its left {@link Table}
    	 * @param inflater the strategy of the mapped bean. Used to give {@link Column}s and {@link RowTransformer}
    	 * @param propertyAccessor accessor to the property of this persister's entity from the source entity type
    	 * @param leftJoinColumn the {@link Column} (of a previously registered join) to be joined with {@code rightJoinColumn}
    	 * @param rightJoinColumn the {@link Column} to be joined with {@code leftJoinColumn}
    	 * @param rightTableAlias optional alias for right table, if null table name will be used
    	 * @param joinType says whether the join must be open
    	 * @param beanRelationFixer a function to fulfill relation between beans
    	 * @param additionalSelectableColumns columns to be added to SQL select clause out of ones took from given inflater, necessary for indexed relations
    	 * @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
    	 */
    	public <U, T1 extends Table<T1>, T2 extends Table<T2>, ID> String addRelationJoin(String leftStrategyName,
    																					  EntityInflater<U, ID> inflater,
    																					  Accessor<?, ?> propertyAccessor,
    																					  Key<T1, ID> leftJoinColumn,
    																					  Key<T2, ID> rightJoinColumn,
    																					  @Nullable String rightTableAlias,
    																					  JoinType joinType,
    																					  BeanRelationFixer<C, U> beanRelationFixer,
    																					  Set<? extends Column<T2, ?>> additionalSelectableColumns) {
    		return addRelationJoin(leftStrategyName, inflater, propertyAccessor, leftJoinColumn, rightJoinColumn, rightTableAlias, joinType, beanRelationFixer, additionalSelectableColumns, null);
    	}

    	/**
    	 * Adds a join to this select.
    	 * Use for one-to-one or one-to-many cases when join is used to describe a related bean.
    	 * Difference with {@link #addRelationJoin(String, EntityInflater, Accessor, Key, Key, String, JoinType, BeanRelationFixer, Set)} is last
    	 * parameter : an optional function which computes an identifier of a relation between 2 entities, this is required to prevent from fulfilling
    	 * twice the relation when SQL returns several times same identifier (when at least 2 collections are implied). By default this function is
    	 * made of parentEntityId + childEntityId and can be overwritten here (in particular when relation is a List, entity index is added to computation).
    	 * See {@link RelationJoinNode.RelationJoinRowConsumer#applyRelatedEntity(JoinRowConsumer.EntityReference, ColumnedRow, TreeInflationContext)} for usage.
    	 *
    	 * @param <U> type of bean mapped by the given strategy
    	 * @param <T1> joined left table
    	 * @param <T2> joined right table
    	 * @param <ID> type of joined values
    	 * @param leftStrategyName the name of a (previously) registered join. {@code leftJoinColumn} must be a {@link Column} of its left {@link Table}
    	 * @param inflater the strategy of the mapped bean. Used to give {@link Column}s and {@link RowTransformer}
    	 * @param propertyAccessor accessor to the property of this persister's entity from the source entity type
    	 * @param leftJoinColumn the {@link Column} (of a previously registered join) to be joined with {@code rightJoinColumn}
    	 * @param rightJoinColumn the {@link Column} to be joined with {@code leftJoinColumn}
    	 * @param rightTableAlias optional alias for right table, if null table name will be used
    	 * @param joinType says whether the join must be open
    	 * @param beanRelationFixer a function to fulfill relation between beans
    	 * @param additionalSelectableColumns columns to be added to SQL select clause out of ones took from given inflater, necessary for indexed relations
    	 * @param relationIdentifierProvider relation identifier provider, not null for List cases : necessary because List may contain duplicate
    	 * @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
    	 * @see RelationJoinNode.RelationJoinRowConsumer#applyRelatedEntity(JoinRowConsumer.EntityReference, ColumnedRow, TreeInflationContext)
    	 */
    	public <U, T1 extends Table<T1>, T2 extends Table<T2>, ID, JOINTYPE> String addRelationJoin(String leftStrategyName,
    																								EntityInflater<U, ID> inflater,
    																								Accessor<?, ?> propertyAccessor,
    																								Key<T1, JOINTYPE> leftJoinColumn,
    																								Key<T2, JOINTYPE> rightJoinColumn,
    																								@Nullable String rightTableAlias,
    																								JoinType joinType,
    																								BeanRelationFixer<?, U> beanRelationFixer,
    																								Set<? extends Column<T2, ?>> additionalSelectableColumns,
    																								@Nullable Function<ColumnedRow, Object> relationIdentifierProvider) {
    		return this.addJoin(leftStrategyName, parent -> {
    			Duo<T2, IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>>> tableClone = cloneTable(rightJoinColumn.getTable());
    			// Build a new Key using the cloned table and the corresponding cloned columns
    			Key.KeyBuilder<T2, JOINTYPE> rightJoinLinkBuilder = Key.from(tableClone.getLeft());
    			Set<? extends JoinLink<?, ?>> columns = rightJoinColumn.getColumns();
    			for (JoinLink<?, ?> column : columns) {
    				// Note that we can cast to JoinLink because we're already dealing with JoinLink since we are in JoinNode
    				JoinLink<T2, Object> clonedColumn = (JoinLink<T2, Object>) tableClone.getRight().get(column);
    				rightJoinLinkBuilder.addColumn(clonedColumn);
    			}

    			// We create the column mapping from the original node column to the cloned columns, not from the table clone ones.
    			// This allows keeping the original columns in the map (user's one), which is necessary for caller to decode the result set
    			IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> originalColumnsToClones = tableClone.getRight();

    			return new RelationJoinNode<U, T1, T2, JOINTYPE, ID>(
    					(JoinNode) parent,
    					propertyAccessor,
    					leftJoinColumn,
    					rightJoinLinkBuilder.build(),
    					joinType,
    					new KeepOrderSet<>(Collections.cat(inflater.getSelectableColumns(), additionalSelectableColumns)),
    					rightTableAlias,
    					inflater,
    					beanRelationFixer,
    					relationIdentifierProvider,
    					originalColumnsToClones);
    		});
    	}

    	/**
    	 * Adds a join to this select.
    	 * Use for inheritance cases when joined data are used to complete an existing bean.
    	 *
    	 * @param <U> type of bean mapped by the given strategy
    	 * @param <T1> left table type
    	 * @param <T2> right table type
    	 * @param <ID> type of joined values
    	 * @param leftStrategyName the name of a (previously) registered join. {@code leftJoinColumn} must be a {@link Column} of its left {@link Table}.
    	 * 						Right table data will be merged with this "root".
    	 * @param inflater the strategy of the mapped bean. Used to give {@link Column}s and {@link RowTransformer}
    	 * @param leftJoinColumn the {@link Column} (of previous strategy left table) to be joined with {@code rightJoinColumn}
    	 * @param rightJoinColumn the {@link Column} (of the strategy table) to be joined with {@code leftJoinColumn}
    	 * @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
    	 */
    	public <U, T1 extends Fromable, T2 extends Fromable, ID> String addMergeJoin(String leftStrategyName,
    																				 EntityMerger<U> inflater,
    																				 Key<T1, ID> leftJoinColumn,
    																				 Key<T2, ID> rightJoinColumn) {
    		return this.addJoin(leftStrategyName, parent -> new MergeJoinNode<>((JoinNode<?, T1>) parent,
    				leftJoinColumn, rightJoinColumn, JoinType.INNER,
    				null, inflater));
    	}

    	/**
    	 * Adds a merge join to this select : no bean will be created by given {@link EntityInflater}, only its
    	 * {@link AbstractTransformer#applyRowToBean(ColumnedRow, Object)} will be used during bean graph loading process.
    	 *
    	 * @param <T1> left table type
    	 * @param <T2> right table type
    	 * @param <ID> type of joined values
    	 * @param leftStrategyName join name on which join must be created
    	 * @param entityMerger strategy to be used to load bean
    	 * @param leftJoinColumn left join column, expected to be one of left strategy table
    	 * @param rightJoinColumn right join column
    	 * @param joinType type of join to create
    	 * @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
    	 */
    	public <U, T1 extends Fromable, T2 extends Fromable, ID> String addMergeJoin(String leftStrategyName,
    																				 EntityMerger<U> entityMerger,
    																				 Key<T1, ID> leftJoinColumn,
    																				 Key<T2, ID> rightJoinColumn,
    																				 JoinType joinType) {
    		return this.addJoin(leftStrategyName, parent -> new MergeJoinNode<>((JoinNode<?, T1>) parent,
    				leftJoinColumn, rightJoinColumn, joinType,
    				null, entityMerger));
    	}

    	public <U, T1 extends Fromable, T2 extends Fromable, ID> String addMergeJoin(String leftStrategyName,
    																				 EntityMerger<U> entityMerger,
    																				 Key<T1, ID> leftJoinColumn,
    																				 Key<T2, ID> rightJoinColumn,
    																				 JoinType joinType,
    																				 Function<JoinNode<U, T2>, MergeJoinRowConsumer<U>> columnedRowConsumer) {
    		return this.addJoin(leftStrategyName, parent -> new MergeJoinNode<U, T1, T2, ID>((JoinNode<?, T1>) parent,
    				leftJoinColumn, rightJoinColumn, joinType,
    				null, entityMerger) {
    			@Override
    			public MergeJoinRowConsumer<U> toConsumer(JoinNode<U, T2> joinNode) {
    				return columnedRowConsumer.apply(joinNode);
    			}
    		});
    	}

    	/**
    	 * Adds a passive join to this select : this kind of join doesn't take part to bean construction, it aims only at adding an SQL join to
    	 * bean graph loading.
    	 *
    	 * @param leftStrategyName join name on which join must be created
    	 * @param leftJoinColumn left join column, expected to be one of left strategy table
    	 * @param rightJoinColumn right join column
    	 * @param joinType type of join to create
    	 * @param columnsToSelect columns that must be added to final select
    	 * @param <T1> left table type
    	 * @param <T2> right table type
    	 * @param <JOINTYPE> type of joined values
    	 * @return the name of the created join, to be used as a key for other joins (through this method {@code leftStrategyName} argument)
    	 */
    	public <T1 extends Table<T1>, T2 extends Table<T2>, JOINTYPE> String addPassiveJoin(String leftStrategyName,
    																						Key<T1, JOINTYPE> leftJoinColumn,
    																						Key<T2, JOINTYPE> rightJoinColumn,
    																						JoinType joinType,
    																						Set<? extends JoinLink<T2, ?>> columnsToSelect) {
    		return this.addJoin(leftStrategyName, parent -> new PassiveJoinNode<C, T1, T2, JOINTYPE>((JoinNode<?, T1>) (JoinNode) parent,
    				leftJoinColumn, rightJoinColumn, joinType,
    				columnsToSelect, null));
    	}

    	public <T1 extends Table<T1>, T2 extends Table<T2>, JOINTYPE> String addPassiveJoin(String leftStrategyName,
    																						Key<T1, JOINTYPE> leftJoinColumn,
    																						Key<T2, JOINTYPE> rightJoinColumn,
    																						JoinType joinType,
    																						Set<? extends Selectable<?>> columnsToSelect,
    																						EntityTreeJoinNodeConsumptionListener<C> consumptionListener) {
    		return this.addJoin(leftStrategyName, parent -> new PassiveJoinNode<C, T1, T2, JOINTYPE>((JoinNode<?, T1>) (JoinNode) parent,
    				leftJoinColumn, rightJoinColumn, joinType,
    				columnsToSelect, null).setConsumptionListener(consumptionListener));
    	}

    	public <T1 extends Table<T1>, T2 extends Table<T2>, JOINTYPE> String addPassiveJoin(String leftStrategyName,
    																						Key<T1, JOINTYPE> leftJoinColumn,
    																						Key<T2, JOINTYPE> rightJoinColumn,
    																						String tableAlias,
    																						JoinType joinType,
    																						Set<? extends Selectable<?>> columnsToSelect,
    																						EntityTreeJoinNodeConsumptionListener<C> consumptionListener,
    																						boolean rightTableParticipatesToDDL) {
    		if (!rightTableParticipatesToDDL) {
    			tablesToExcludeFromDDL.add(rightJoinColumn.getTable());
    		}
    		return this.addJoin(leftStrategyName, parent -> new PassiveJoinNode<C, T1, T2, JOINTYPE>((JoinNode<?, T1>) (JoinNode) parent,
    				leftJoinColumn, rightJoinColumn, joinType,
    				columnsToSelect, tableAlias).setConsumptionListener(consumptionListener));
    	}

    	public String addJoin(String leftStrategyName, Function<? super JoinNode<?, Fromable> /* parent node */, ? extends AbstractJoinNode<?, ?, ?, ?>> joinNodeSupplier) {
    		JoinNode<?, Fromable> owningJoin = getJoin(leftStrategyName);
    		if (owningJoin == null) {
    			throw new IllegalArgumentException("No join named " + leftStrategyName + " exists to add a new join on");
    		}
    		AbstractJoinNode joinNode = joinNodeSupplier.apply(owningJoin);
    		String joinName = this.indexKeyGenerator.generateKey(joinNode);
    		this.joinIndex.put(joinName, joinNode);
    		return joinName;
    	}

    	/**
    	 * Gives a particular node of the joins graph by its name. Joins graph name are given in return of
    	 * {@link #addRelationJoin(String, EntityInflater, Accessor, Key, Key, String, JoinType, BeanRelationFixer, Set)}.
    	 * When {@link #ROOT_JOIN_NAME} is given, {@link #getRoot()} will be used, meanwhile, be aware that using this method to retreive root node
    	 * is not the recommended way : prefer usage of {@link #getRoot()} to prevent exposure of {@link #ROOT_JOIN_NAME}
    	 *
    	 * @param leftStrategyName join node name to be given
    	 * @return null if the node doesn't exist
    	 * @see #getRoot()
    	 */
    	@Nullable
    	public JoinNode<?, Fromable> getJoin(String leftStrategyName) {
    		return (JoinNode<?, Fromable>) this.joinIndex.get(leftStrategyName);
    	}

    	/**
    	 * Gives all tables used by this tree
    	 *
    	 * @return all joins tables of this tree
    	 */
    	public Set<Table<?>> giveTables() {
    		// because Table doesn't implement an hashCode and because we may have clone in JoinNodes, we use a smart TreeSet to avoid duplicates
    		Set<Table<?>> result = new TreeSet<>(COMPARATOR_ON_SCHEMA_AND_NAME);
    		extractTable(getRoot(), result);
    		foreachJoin(node -> {
    			// for now, we skip direct extraction of table-per-class nodes, because their originalColumnsToLocalOnes contains both join link values and
    			// columns to select, and the former is the one of the abstract entity that doesn't match a concrete table (because of table-per-class model);
    			// but sub-tables are addressed because the node contains the joins to the sub-tables.
    			// For now, we skip direct extraction of table-per-class nodes, because their originalColumnsToLocalOnes contains PseudoColumns
    			// to select which are not part of a concrete Table. Note that the sub-tables are still addressed because the node contains the joins
    			// to the sub-tables.
    			if (!(node instanceof TablePerClassPolymorphicRelationJoinNode)) {
    				extractTable(node, result);
    			}
    		});
    		tablesToIncludeToDDL.forEach(result::add);
    		return result;
    	}

    	private void extractTable(JoinNode<?, ?> node, Set<Table<?>> result) {
    		// we must take the original table because that's where user add the foreign keys, because the table cloning mechanism doesn't propagate them
    		node.getOriginalColumnsToLocalOnes().keySet().forEach(column -> {
    			if (column instanceof Column) {
    				Fromable table = ((Column) column).getTable();
    				if (!tablesToExcludeFromDDL.contains(table)) {
    					result.add((Table<?>) table);
    				}
    			}
    		});
    	}

    	/**
    	 * Shortcut for {@code joinIterator().forEachRemaining()}.
    	 * Goes down this tree by breadth first. Made to avoid everyone implements node iteration.
    	 * Consumer is invoked foreach node <strong>except root</strong> because it usually has a special treatment.
    	 * Traversal is made in pre-order : node is consumed first then its children.
    	 *
    	 * @param consumer a {@link AbstractJoinNode} consumer
    	 */
    	public void foreachJoin(Consumer<AbstractJoinNode<?, ?, ?, ?>> consumer) {
    		joinIterator().forEachRemaining(consumer);
    	}

    	/**
    	 * Copies this tree onto given one under given join node name
    	 *
    	 * @param target tree receiving copies of this tree nodes
    	 * @param joinNodeName node name under which this tree must be copied,
    	 * @param <E> main entity type of target tree
    	 * @param <ID> main entity identifier type of target tree
    	 */
    	public <E, ID> void projectTo(EntityJoinTree<E, ID> target, String joinNodeName) {
    		projectTo(target.getJoin(joinNodeName));
    	}

    	public void projectTo(JoinNode<?, Fromable> joinNode) {
    		EntityJoinTree<?, ?> tree = joinNode.getTree();
    		foreachJoinWithDepth(joinNode, (targetOwner, currentNode) -> {
    			// cloning each node, the only difference lays on left column : target gets its matching column
    			currentNode.getLeftJoinLink().getColumns().forEach(leftColumn -> {
    				Selectable<?> projectedLeftColumn = targetOwner.getTable().findColumn(leftColumn.getExpression());
    				if (projectedLeftColumn == null) {
    					throw new IllegalArgumentException("Expected column "
    							+ leftColumn.getExpression() + " to exist in target table " + targetOwner.getTable().getName()
    							+ " but couldn't be found among " + Iterables.collect(targetOwner.getTable().getColumns(), Selectable::getExpression, ArrayList::new));
    				}
    			});
    			AbstractJoinNode nodeClone = cloneNodeForParent(currentNode, targetOwner, currentNode.getLeftJoinLink());
    			// maintaining join names through trees : we add current node name to target one. Then nodes can be found across trees
    			Set<Entry<String, JoinNode<?, ?>>> set = this.joinIndex.entrySet();
    			Entry<String, JoinNode<?, ?>> nodeName = Iterables.find(set, entry -> entry.getValue() == currentNode);
    			tree.joinIndex.put(nodeName.getKey(), nodeClone);

    			return nodeClone;
    		});
    	}

    	/**
    	 * Creates an {@link Iterator} that goes down this tree by breadth first. Made to avoid everyone implements node iteration.
    	 * Consumer is invoked foreach node <strong>except root</strong> because it usually has a special treatment.
    	 * Traversal is made in pre-order : node is consumed first then its children.
    	 */
    	public Iterator<AbstractJoinNode<?, ?, ?, ?>> joinIterator() {
    		return new NodeIterator();
    	}

    	/**
    	 * Goes down this tree by breadth first.
    	 * Consumer is invoked foreach node <strong>except root</strong> because it usually has a special treatment.
    	 * Used to create an equivalent tree of this instance with another type of node. This generally requires knowing current parent to allow child
    	 * addition : consumer gets current parent as a first argument
    	 *
    	 * @param initialNode very first parent given as first argument to consumer
    	 * @param consumer producer of target tree node, gets previous created node (to add created node to it) and node of this tree
    	 * @param <S> type of node of the equivalent tree
    	 */
    	<S> void foreachJoinWithDepth(S initialNode, BiFunction<S, AbstractJoinNode<?, ?, ?, ?>, S> consumer) {
    		Queue<S> targetPath = new ArrayDeque<>();
    		targetPath.add(initialNode);
    		NodeIteratorWithDepth<S> nodeIterator = new NodeIteratorWithDepth<>(targetPath, consumer);
    		// We simply iterate all over the iterator to consume all elements
    		// Please note that forEachRemaining can't be used because it is unsupported by NodeIteratorWithDepth
    		while (nodeIterator.hasNext()) {
    			nodeIterator.next();
    		}

    	}

    	/**
    	 * Internal class that focuses on nodes. Iteration node is made breadth-first.
    	 */
    	private class NodeIterator implements Iterator<AbstractJoinNode<?, ?, ?, ?>> {

    		protected final Queue<AbstractJoinNode> joinStack;
    		protected AbstractJoinNode currentNode;
    		protected boolean nextDepth = false;

    		public NodeIterator() {
    			joinStack = new ArrayDeque<>(root.getJoins());
    		}

    		@Override
    		public boolean hasNext() {
    			return !joinStack.isEmpty();
    		}

    		@Override
    		@SuppressWarnings("java:S2272")    // NoSuchElementException is manged by Queue#remove()
    		public AbstractJoinNode next() {
    			// we prefer remove() to poll() because it manages NoSuchElementException which is also in next() contract
    			currentNode = joinStack.remove();
    			ReadOnlyList<AbstractJoinNode> nextJoins = currentNode.getJoins();
    			joinStack.addAll(nextJoins);
    			nextDepth = !nextJoins.isEmpty();
    			return currentNode;
    		}
    	}

    	private class NodeIteratorWithDepth<S> extends NodeIterator {

    		private final Queue<S> targetPath;
    		private final BiFunction<S, AbstractJoinNode<Object, Fromable, Fromable, Object>, S> consumer;

    		public NodeIteratorWithDepth(Queue<S> targetPath, BiFunction<S, AbstractJoinNode<?, ?, ?, ?>, S> consumer) {
    			this.targetPath = targetPath;
    			this.consumer = (BiFunction<S, AbstractJoinNode<Object, Fromable, Fromable, Object>, S>) (BiFunction) consumer;
    		}

    		@Override
    		public AbstractJoinNode next() {
    			super.next();
    			S targetOwner = targetPath.peek();
    			S nodeClone = (S) consumer.apply(targetOwner, currentNode);
    			if (nextDepth) {
    				targetPath.add(nodeClone);
    			}

    			// if depth changes, we must remove target depth
    			AbstractJoinNode nextIterationNode = joinStack.peek();
    			if (nextIterationNode != null && nextIterationNode.getParent() != currentNode.getParent()) {
    				targetPath.remove();
    			}

    			return currentNode;
    		}

    		@Override
    		public void forEachRemaining(Consumer<? super AbstractJoinNode<?, ?, ?, ?>> action) {
    			// this is not supported since a consumer is already given to constructor
    			throw new UnsupportedOperationException();
    		}
    	}

    	/**
    	 * Clones table of given join (only on its columns, no need for its foreign key clones nor indexes)
    	 *
    	 * @param fromable the table to clone
    	 * @return a copy (on name and columns) of given join table
    	 */
    	static <T extends Fromable> Duo<T, IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>>> cloneTable(T fromable) {
    		if (fromable instanceof Table) {
    			Table<?> table = (Table<?>) fromable;
    			Table tableClone = new Table(fromable.getName());
    			IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> columnClones = new IdentityHashMap<>(tableClone.getColumns().size());
    			(((Table<?>) fromable).getColumns()).forEach(column -> {
    				Column<?, ?> clone = tableClone.addColumn(column.getName(), column.getJavaType(), column.getSize(), column.isNullable());
    				columnClones.put(column, clone);
    			});

    			// Propagating primary key because right tables are used to generate schema, and at this stage they lack primary key.
    			// Note that foreign keys will be added through the tree building process when appending joins, so we don't need to clone them here
    			if (table.getPrimaryKey() != null) {
    				Key<?, ?> primaryKey = table.getPrimaryKey();
    				primaryKey.getColumns().forEach(column -> {
    					// we can cast to JoinLink because we're already dealing with JoinLink since we are in JoinNode
    					Column<?, ?> clonedColumn = (Column<?, ?>) columnClones.get(column);
    					clonedColumn.primaryKey();
    				});
    			}
    			return new Duo<>((T) tableClone, columnClones);
    		} else if (fromable instanceof PseudoTable) {
    			PseudoTable pseudoTable = new PseudoTable(((PseudoTable) fromable).getQueryStatement(), fromable.getName());
    			IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> columnClones = new IdentityHashMap<>(pseudoTable.getColumns().size());
    			(((PseudoTable) fromable).getColumns()).forEach(column -> {
    				// we can only have Union in From clause, no sub-query, because of table-per-class polymorphism, so we can cast to Union
    				PseudoColumn<?> clone = ((Union) pseudoTable.getQueryStatement()).registerColumn(column.getExpression(), column.getJavaType());
    				columnClones.put(column, clone);
    			});
    			return new Duo<>((T) pseudoTable, columnClones);
    		} else {
    			throw new UnsupportedOperationException("Cloning " + Reflections.toString(fromable.getClass()) + " is not implemented");
    		}
    	}

    	static <T extends Fromable> Key.KeyBuilder<T, ?> mimicKey(Key<?, ?> leftJoinColumn, T fromable) {
    		if (fromable instanceof Table) {
    			Table<?> table = (Table<?>) fromable;
    			Key.KeyBuilder<Table, ?> leftKeyBuilder = Key.from(table);
    			leftJoinColumn.getColumns().forEach(column -> {
    				Column column1 = table.addColumn(column.getExpression(), column.getJavaType());
    				leftKeyBuilder.addColumn(column1);
    			});
    			return (Key.KeyBuilder<T, ?>) leftKeyBuilder;
    		} else if (fromable instanceof PseudoTable) {
    			PseudoTable union = (PseudoTable) fromable;
    			Key.KeyBuilder<PseudoTable, ?> leftKeyBuilder = Key.from(union);
    			leftJoinColumn.getColumns().forEach(column -> {
    				Selectable<?> column1 = union.findColumn(column.getExpression());
    				leftKeyBuilder.addColumn((JoinLink<PseudoTable, ?>) column1);
    			});
    			return (Key.KeyBuilder<T, ?>) leftKeyBuilder;
    		} else {
    			throw new UnsupportedOperationException("Cloning " + Reflections.toString(fromable.getClass()) + " is not implemented");
    		}
    	}

    	/**
    	 * Copies given node and set it as a child of given parent.
    	 * Could have been implemented by each node class itself but since this behavior is required only by the tree
    	 * and a particular algorithm, decision was made to do it outside of them.
    	 *
    	 * @param node node to be cloned
    	 * @param parent parent node target of the clone
    	 * @param leftJoinColumn columns to be used as the left key of the new node
    	 * @return a copy of given node, put as child of parent, using leftColumn
    	 */
    	public static AbstractJoinNode<?, ?, ?, ?> cloneNodeForParent(AbstractJoinNode<?, ?, ?, ?> node, JoinNode parent, Key<?, ?> leftJoinColumn) {
    		Duo<Fromable, IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>>> tableClone = cloneTable(node.getTable());
    		// Build a new Key using the cloned table and the corresponding cloned columns
    		Key.KeyBuilder<Fromable, Object> rightJoinLinkBuilder = Key.from(tableClone.getLeft());
    		Set<? extends JoinLink<?, ?>> columns = node.getRightJoinLink().getColumns();
    		for (JoinLink<?, ?> column : columns) {
    			// Note that we can cast to JoinLink because we're already dealing with JoinLink since we are in JoinNode
    			JoinLink<Fromable, Object> clonedColumn = (JoinLink<Fromable, Object>) tableClone.getRight().get(column);
    			rightJoinLinkBuilder.addColumn(clonedColumn);
    		}
    		// We create the column mapping from the original node column to the cloned columns, not from the table clone ones.
    		// This allows keeping the original columns in the map (user's one), which is necessary for caller to decode the result set
    		IdentityHashMap<JoinLink<?, ?>, JoinLink<?, ?>> originalColumnsToClones = Maps.innerJoinOnValuesAndKeys(node.getOriginalColumnsToLocalOnes(), tableClone.getRight(), IdentityHashMap::new);

    		Key.KeyBuilder<Fromable, ?> leftJoinLinkBuilder = mimicKey(leftJoinColumn, parent.getTable());

    		AbstractJoinNode nodeCopy;
    		if (node instanceof RelationJoinNode) {
    			nodeCopy = new RelationJoinNode(
    					parent,
    					((RelationJoinNode<?, ?, ?, ?, ?>) node).getPropertyAccessor(),
    					leftJoinLinkBuilder.build(),
    					rightJoinLinkBuilder.build(),
    					node.getJoinType(),
    					node.getColumnsToSelect(),
    					node.getTableAlias(),
    					((RelationJoinNode) node).getEntityInflater(),
    					((RelationJoinNode) node).getBeanRelationFixer(),
    					((RelationJoinNode) node).getRelationIdentifierProvider(),
    					originalColumnsToClones);
    		} else if (node instanceof MergeJoinNode) {
    			nodeCopy = new MergeJoinNode(
    					parent,
    					leftJoinLinkBuilder.build(),
    					rightJoinLinkBuilder.build(),
    					node.getJoinType(),
    					node.getTableAlias(),
    					((MergeJoinNode) node).getMerger(),
    					node.getColumnsToSelect(),
    					originalColumnsToClones);
    		} else if (node instanceof PassiveJoinNode) {
    			nodeCopy = new PassiveJoinNode(
    					parent,
    					leftJoinLinkBuilder.build(),
    					rightJoinLinkBuilder.build(),
    					node.getJoinType(),
    					node.getColumnsToSelect(),
    					node.getTableAlias(),
    					originalColumnsToClones);
    		} else {
    			throw new UnsupportedOperationException("Unexpected type of join : some algorithm has changed, please implement it here or fix it : "
    					+ Reflections.toString(node.getClass()));
    		}
    		nodeCopy.setConsumptionListener(node.getConsumptionListener());

    		return nodeCopy;
    	}

    	private class NodeKeyGenerator {

    		/**
    		 * We don't use {@link java.security.SecureRandom} because it consumes too much time while computing random values (more than 500ms
    		 * for a 6-digit identifier ! ... for each Node !!), and there's no need for security here.
    		 */
    		@SuppressWarnings("java:S2245" /* no security issue about Random class : it's only used as identifier generator, using SecureRandom is less performant */)
    		private final Randomizer keyGenerator = new Randomizer(new LinearRandomGenerator(new Random()));

    		private String generateKey(JoinNode node) {
    			// We generate a name which is unique across trees so node clones can be found outside this class by their name on different trees.
    			// This is necessary for particular case of reading indexing column of indexed collection with an association table : the node that needs
    			// to use indexing column is not the owner of the association table clone hence it can't use it (see table clone mechanism at EntityTreeQueryBuilder).
    			// Said differently the "needer" is the official owner whereas the indexing column is on another node dedicated to the relation table maintenance.
    			// The found way for the official node to access data through the indexing column is to use the identifier of the relation table node,
    			// because it has it when it is created (see OneToManyWithIndexedAssociationTableEngine), and because the node is cloned through tree
    			// the identifier should be "universal".
    			// Note that this naming strategy could be more chaotic (totally random) since names are only here to give a unique identifier to joins
    			// but the hereafter algorithm can help for debug
    			return node.getTable().getAbsoluteName()
    					+ "-" + Integer.toHexString(System.identityHashCode(EntityJoinTree.this)) + "-" + keyGenerator.randomHexString(6);
    		}
    	}

    	public enum JoinType {
    		INNER,
    		OUTER
    	}

    }