Package org.jpox.store.rdbms.query

Source Code of org.jpox.store.rdbms.query.QueryStatement

/**********************************************************************
Copyright (c) 2002 Kelly Grizzle (TJDO) and others. All rights reserved.
Licensed 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.

Contributors:
2002 Mike Martin (TJDO)
2003 Erik Bengtson - added UNION operator
2003 Erik Bengtson - fixed andCondition to queries in UNION
2003 Erik Bengtson - fixed number of column positions in sql returned
                     on select method
2004 Erik Bengtson - removed ununsed imports
2004 Erik Bengtson - block duplicate joins
2004 Erik Bengtson - fixed bug [833915] QueryResult passed as parameter
                     for another Query
2004 Erik Bengtson - fixed bug [832484] Query - boolean expressions                 
2004 Andy Jefferson - commented
2004 Andy Jefferson - added selectAlias methods and support for DatabaseAdapters
                      that need the ORDER BY columns aliased.
2004 Erik Bengtson - added DELETE queries
2008 Andy Jefferson - restructured to have single main table and no "default" table
    ...
**********************************************************************/
package org.jpox.store.rdbms.query;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jpox.ClassLoaderResolver;
import org.jpox.exceptions.JPOXException;
import org.jpox.store.mapped.DatastoreClass;
import org.jpox.store.mapped.DatastoreContainerObject;
import org.jpox.store.mapped.DatastoreField;
import org.jpox.store.mapped.DatastoreIdentifier;
import org.jpox.store.mapped.IdentifierFactory;
import org.jpox.store.mapped.MappedStoreManager;
import org.jpox.store.mapped.expression.AggregateExpression;
import org.jpox.store.mapped.expression.BooleanExpression;
import org.jpox.store.mapped.expression.BooleanLiteral;
import org.jpox.store.mapped.expression.LogicSetExpression;
import org.jpox.store.mapped.expression.MetaDataStringLiteral;
import org.jpox.store.mapped.expression.QueryExpression;
import org.jpox.store.mapped.expression.ScalarExpression;
import org.jpox.store.mapped.mapping.JavaTypeMapping;
import org.jpox.store.mapped.query.StatementText;
import org.jpox.store.rdbms.RDBMSManager;
import org.jpox.store.rdbms.adapter.RDBMSAdapter;
import org.jpox.util.JPOXLogger;
import org.jpox.util.Localiser;

/**
* Representation of a statement for a Query.
* Provides methods to specify joins to other tables and to select columns from these tables.
* Provides methods to apply restrictions on the result.
* The resultant statement consists of a SELECT clause, a FROM clause and a WHERE clause.
* Provides output as either a SELECT statement or as a DELETE statement. The DELETE form of
* the statement uses just the FROM and WHERE clauses.
* This statement may represent a subquery.
**/
public class QueryStatement implements QueryExpression
{
    /** Localisation for messages. */
    protected static final Localiser LOCALISER = Localiser.getInstance("org.jpox.store.rdbms.Localisation",
        RDBMSManager.class.getClassLoader());

    /** Store Manager. */
    protected final MappedStoreManager storeMgr;

    /** Resolver for class loading issues. */
    private final ClassLoaderResolver clr;

    /** Candidate class for the query. */
    protected Class candidateClass = null;

    /** Candidate alias used by the query (defaults to "this"). */
    protected String candidateAlias = "this";

    /** Alias for the main table. */
    protected final DatastoreIdentifier mainTableAlias;

    /** Table expression for the main table of this statement. */
    protected final LogicSetExpression mainTableExpr;

    protected Map tableExprsByAlias = new HashMap();

    /** Parent query expression for this query (where this is a subquery). **/
    private QueryExpression parentQueryExpr;

    /** List of unioned query expressions. */
    protected List union = new ArrayList();

    /** Whether this query is to be used as a as set for the Exists function. <PRE>e.g WHERE EXISTS(QUERY)</PRE>. */
    protected boolean isExistsSubQuery = false;

    /** Whether to make results distinct. */
    protected boolean distinctResults = false;

    /** Expression(s) for selected fields/columns. */
    protected List selected = new ArrayList();

    /** whether there is an aggregate expression present in the select **/
    protected boolean hasAggregateExpression = false;

    /** inner/left/right joins */
    protected List joins = new ArrayList();

    /** Internal List of table aliases, to ensure that we don't have duplicate JOINs From vs To */
    protected List joinsToTableAliases = new ArrayList();

    /** Cross joins. Will be output as (FROM) TABLE1,TABLE2,TABLE3 */
    protected List crossJoins = new ArrayList();

    /**
     * Separator for joins. Can be overridden by subclasses.
     * e.g. <pre>FROM T1 JOIN T2 ON T1.ID=T2.ID JOIN T3 ON T3.ID=T2.ID, T4...</pre>
     * This represents the space between "JOIN T2 ON T1.ID=T2.ID" AND "JOIN T3 ON T3.ID=T2.ID" in the above example
     */
    protected char stmtJoinsSeparator = ' ';

    /** Expression for the WHERE clause. */
    protected BooleanExpression whereExpr = null;

    /** Expression(s) for the GROUP BY clause. */
    protected List groupingExpressions = null;

    /** Expression for any HAVING clause. */
    protected BooleanExpression havingExpr = null;

    /** Expressions for any ORDER BY clause. */
    protected ScalarExpression[] orderingExpressions = null;

    /** Directions for any ORDER BY expressions (1 for each orderingExpressions entry). */
    protected boolean[] orderingDirections = null;

    /** Update condition */
    protected ScalarExpression[] updateExprs = null;

    /** The offset for any range restriction. */
    protected long rangeOffset = -1;

    /** The number of records to be retrieved in any range restriction. */
    protected long rangeCount = -1;

    /** Map of extensions, keyed by the extension name. */
    protected HashMap extensions;

    /** Statement text for this Query Statement. Created by toXXXStatementText() */
    protected StatementText stmtText = null;

    /**
     * Constructor. The main table will be aliased as "THIS".
     * @param mainTable The main table for this statement.
     * @param clr ClassLoader resolver
     */
    public QueryStatement(DatastoreContainerObject mainTable, ClassLoaderResolver clr)
    {
        this(mainTable, null, clr);
    }

    /**
     * Constructor, allowing the specification of the alias of the main table.
     * @param mainTable The main table for this statement.
     * @param alias The alias for the main table
     * @param clr ClassLoader resolver.
     */
    public QueryStatement(DatastoreContainerObject mainTable, DatastoreIdentifier alias, ClassLoaderResolver clr)
    {
        this.storeMgr = mainTable.getStoreManager();
        this.clr = clr;

        if (alias == null)
        {
            mainTableAlias = storeMgr.getIdentifierFactory().newIdentifier(IdentifierFactory.TABLE, "this");
        }
        else
        {
            mainTableAlias = alias;
        }
        mainTableExpr = newTableExpression(mainTable, mainTableAlias);
        tableExprsByAlias.put(mainTableAlias, mainTableExpr);
    }

    /**
     * Method to set the candidate class and alias in use by the query.
     * The expression is created with a candidate table, yet this could store more than 1 class.
     * Additionally the "alias" of the candidate table expression is a DatastoreIdentifier whereas
     * this alias here is a String form.
     * @param cls The candidate class
     * @param alias The alias
     */
    public void setCandidateInformation(Class cls, String alias)
    {
        this.candidateClass = cls;
        this.candidateAlias = alias;
    }

    /**
     * Accessor for the candidate class of the query expression.
     * @return Candidate class
     */
    public Class getCandidateClass()
    {
        return candidateClass;
    }

    /**
     * Accessor for the candidate alias in use by the query.
     * @return Candidate alias
     */
    public String getCandidateAlias()
    {
        return candidateAlias;
    }

    /**
     * Resets the compiled expression statement.
     */
    public void reset()
    {
        if (stmtText == null)
        {
            return;
        }

        stmtText = null;
        for( int i=0; i<union.size(); i++)
        {
            ((QueryExpression)union.get(i)).reset();
        }
    }

    /**
     * Sets the parent QueryExpression of this query. In SQL it can be exemplified as
     * <code>
     * SELECT 1 FROM PARENT WHERE EXISTS (SELECT 1 FROM THIS)
     * </code>
     * The parent QueryExpression is the outer SELECT, and this QueryExpression is the inner SELECT.
     * @param parentQueryExpr the parent of this query
     */
    public void setParent(QueryExpression parentQueryExpr)
    {
        this.parentQueryExpr = parentQueryExpr;
        if (parentQueryExpr != null)
        {
            // Copy extensions from parent
            HashMap parentExts = parentQueryExpr.getExtensions();
            if (parentExts != null)
            {
                Set entries = parentExts.entrySet();
                Iterator entryIter = entries.iterator();
                while (entryIter.hasNext())
                {
                    Map.Entry entry = (Map.Entry)entryIter.next();
                    addExtension((String)entry.getKey(), entry.getValue());
                }
            }
        }
    }

    /**
     * Accessor for the parent QueryExpression if this is a nested expression.
     * @return Parent expression
     */
    public QueryExpression getParent()
    {
        return parentQueryExpr;
    }

    /**
     * Accessor for the class-loader resolver.
     * @return The ClassLoader resolver.
     */
    public ClassLoaderResolver getClassLoaderResolver()
    {
        return clr;
    }

    /**
     * Accessor for the Store Manager.
     * @return The Store Manager.
     */
    public MappedStoreManager getStoreManager()
    {
        return storeMgr;
    }

    /**
     * Accessor for whether this query will return distinct results.
     * @return Whether it returns distinct
     */
    public boolean getDistinctResults()
    {
        return distinctResults;
    }

    /**
     * Mutator for whether the query returns distinct results.
     * @param distinctResults Whether to return distinct
     */
    public void setDistinctResults(boolean distinctResults)
    {
        assertNotFrozen();

        this.distinctResults = distinctResults;
    }

    /**
     * Set this query is to be used as a set for the Exists function.
     * example WHERE EXISTS (QUERY)
     * @param isExistsSubQuery The isExistsSubQuery to set.
     */
    public void setExistsSubQuery(boolean isExistsSubQuery)
    {
        this.isExistsSubQuery = isExistsSubQuery;

        if (isExistsSubQuery && !((RDBMSAdapter)storeMgr.getDatastoreAdapter()).supportsExistsSyntax())
        {
            throw new JPOXException(LOCALISER.msg("052504", "EXISTS")).setFatal();
        }
    }

    /**
     * Method to union this query statement with another query statement.
     * @param expr The other query statement to union
     */
    public void union(QueryExpression expr)
    {
        assertNotFrozen();

        union.add(expr);
    }

    /**
     * Accessor for the unioned expressions.
     * @return The unions
     */
    public List getUnionedExpressions()
    {
        return union;
    }

    /**
     * Method to define an extension for this query statement allowing control over its behaviour
     * in generating a query.
     * @param key Extension key
     * @param value Value for the key
     */
    public void addExtension(String key, Object value)
    {
        if (extensions == null)
        {
            extensions = new HashMap();
        }
        extensions.put(key, value);
    }

    /**
     * Accessor for the value for an extension.
     * @param key Key for the extension
     * @return Value for the extension (if any)
     */
    public Object getValueForExtension(String key)
    {
        if (extensions == null)
        {
            return extensions;
        }
        return extensions.get(key);
    }

    /**
     * Accessor for the extensions for this expression.
     * @return Extensions
     */
    public HashMap getExtensions()
    {
        return extensions;
    }

    // ------------------------- SELECT clause methods -------------------------------

    /**
     * Select the datastore identity of the primary table of the query.
     * @param alias Alias to use for the selected datastore identity column (if any)
     * @param unionQueries Whether to apply to the primary table of any unioned tables
     * @return Index of the column in the select (or null if not datastore mapping)
     */
    public synchronized int[] selectDatastoreIdentity(String alias, boolean unionQueries)
    {
        int[] index = selectDatastoreIdentity(alias);
        if (unionQueries)
        {
            for (int i=0; i<union.size(); i++)
            {
                QueryStatement qs = ((QueryStatement)union.get(i));
                qs.selectDatastoreIdentity(alias, unionQueries);
            }
        }
        return index;
    }

    /**
     * Select the datastore identity of the primary table of the query.
     * @param alias Alias to use for the selected datastore identity column (if any)
     * @return Index of the column in the select (or null if no datastore mapping)
     */
    private synchronized int[] selectDatastoreIdentity(String alias)
    {
        if (!(mainTableExpr.getMainTable() instanceof DatastoreClass))
        {
            return null;
        }
        DatastoreClass mainTable = (DatastoreClass)mainTableExpr.getMainTable();
        if (mainTable.getDataStoreObjectIdMapping() == null)
        {
            return null;
        }

        return select(mainTableAlias, mainTable.getDataStoreObjectIdMapping(), alias);
    }

    /**
     * Select the version column of the primary table of the query.
     * @param alias Alias to use for the selected version column (if any)
     * @param unionQueries Whether to also select the version column of the primary table of any unioned queries
     * @return Index of the column in the select (or null if no version mapping)
     */
    public synchronized int[] selectVersion(String alias, boolean unionQueries)
    {
        int[] index = selectVersion(alias);
        if (unionQueries)
        {
            for (int i=0; i<union.size(); i++)
            {
                QueryStatement qs = ((QueryStatement)union.get(i));
                qs.selectVersion(alias, unionQueries);
            }
        }
        return index;
    }

    /**
     * Select the version column of the primary table of the query.
     * @param alias Alias to use for the selected version column (if any)
     * @return Index of the column in the select (or null if no version mapping)
     */
    private synchronized int[] selectVersion(String alias)
    {
        if (!(mainTableExpr.getMainTable() instanceof DatastoreClass))
        {
            return null;
        }
        DatastoreClass mainTable = (DatastoreClass)mainTableExpr.getMainTable();
        if (mainTable.getVersionMapping(false) == null)
        {
            return null;
        }

        return select(mainTableAlias, mainTable.getVersionMapping(false), alias);
    }

    /**
     * Select the column(s) for the specified field of the primary table of the query.
     * @param fieldName Name of the field to select.
     * @param alias Alias to use for the selected field column(s) (if any).
     * @param unionQueries Whether to also select the field column(s) of the primary table of any unioned queries
     * @return Index of the column(s) in the select (or null of no field of this name present).
     */
    public synchronized int[] selectField(String fieldName, String alias, boolean unionQueries)
    {
        int[] index = selectField(fieldName, alias);
        if (unionQueries)
        {
            for (int i=0; i<union.size(); i++)
            {
                QueryStatement qs = ((QueryStatement)union.get(i));
                qs.selectField(fieldName, alias, unionQueries);
            }
        }
        return index;
    }

    /**
     * Select the column(s) for the specified field of the primary table of the query.
     * @param fieldName Name of the field to select.
     * @param alias Alias to use for the selected field column(s) (if any).
     * @return Index of the column(s) in the select (or null of no field of this name present).
     */
    private synchronized int[] selectField(String fieldName, String alias)
    {
        if (!(mainTableExpr.getMainTable() instanceof DatastoreClass))
        {
            return null;
        }
        DatastoreClass mainTable = (DatastoreClass)mainTableExpr.getMainTable();
        JavaTypeMapping fieldMapping = mainTable.getFieldMapping(fieldName);
        if (fieldMapping == null)
        {
            return null;
        }

        return select(mainTableAlias, fieldMapping, alias);
    }

    /**
     * Select the columns for a mapping.
     * @param mapping The mapping
     * @return The index of the columns in the select
     **/
    public synchronized int[] select(JavaTypeMapping mapping)
    {
        int[] index = new int[mapping.getNumberOfDatastoreFields()];
        for (int i=0; i<index.length; i++)
        {
            FieldQueryExpression qe = getQueryExpression(mainTableAlias, mapping.getDataStoreMapping(i).getDatastoreField(), null);
            index[i] = selectQueryExpression(qe);
        }

        return index;
    }

    /**
     * select a new column, add to union queries, if unionQueries is true
     * @param mapping The mapping
     * @param unionQueries Whether to add to any union
     * @return The index of the columns in the select
     */
    public synchronized int[] select(JavaTypeMapping mapping, boolean unionQueries)
    {
        int[] index = select(mapping);
        if (unionQueries)
        {
            for (int i=0; i<union.size(); i++)
            {
                QueryStatement qs = ((QueryStatement)union.get(i));
                qs.select(mapping, unionQueries);
            }
        }
        return index;
    }

    /**
     * Select columns of the specified mapping for the table identifier.
     * @param alias The alias
     * @param mapping The mapping
     * @return The index of the columns in the select
     */
    public synchronized int[] select(DatastoreIdentifier alias, JavaTypeMapping mapping)
    {
        return select(alias, mapping, null);
    }

    /**
     * Select columns of the specified mapping for the table identifier.
     * If an alias is supplied the column(s) will have "AS alias" added.
     * If there is more than 1 column in the mapping then the alias will be "alias_[i]".
     * @param alias The alias
     * @param mapping The mapping
     * @param columnAlias Alias to use for the selected column (if any)
     * @return The index of the columns in the select
     */
    private synchronized int[] select(DatastoreIdentifier alias,
                                      JavaTypeMapping mapping,
                                      String columnAlias)
    {
        assertNotFrozen();

        int index[] = new int[mapping.getNumberOfDatastoreFields()];
        for (int i=0; i<index.length; i++)
        {
            String colAlias = columnAlias;
            if (index.length > 1)
            {
                colAlias += "_" + i;
            }
            index[i] = selectQueryExpression(getQueryExpression(alias, mapping.getDataStoreMapping(i).getDatastoreField(), colAlias));
        }
        return index;
    }

    /**
     * select columns, add to union queries, if unionQueries is true
     * @param alias The alias
     * @param mapping The mapping
     * @param unionQueries Whether to add to any union
     * @return The index of the column in the select
     */
    public synchronized int[] select(DatastoreIdentifier alias,
                                     JavaTypeMapping mapping,
                                     boolean unionQueries)
    {
        int[] index = select(alias, mapping);
        if (unionQueries)
        {
            for (int i=0; i<union.size(); i++)
            {
                QueryStatement qs = ((QueryStatement)union.get(i));
                qs.select(alias, mapping, null);
            }
        }
        return index;  
    }

    /**
     * Select an expression.
     * @param expr The expression to add to the select statement
     * @return The index of the expression in the select
     */
    public int selectScalarExpression(ScalarExpression expr)
    {
        assertNotFrozen();

        if (expr instanceof AggregateExpression)
        {
            hasAggregateExpression = true;
        }

        String exprStr = expr.toStatementText(ScalarExpression.PROJECTION).toStatementString(ScalarExpression.PROJECTION);
        return selectItem(exprStr);
    }

    /**
     * Select an expression.
     * @param expr The expression to add to the select statement
     * @param unionQueries whether to apply the select in all queries unified by the union clause
     * @return The index of the expression in the select
     */
    public int selectScalarExpression(ScalarExpression expr, boolean unionQueries)
    {
        assertNotFrozen();

        if (unionQueries)
        {
            for (int i=0; i<union.size(); i++)
            {
                QueryStatement qs = ((QueryStatement)union.get(i));
                qs.selectScalarExpression(expr);
            }
        }

        return selectScalarExpression(expr);
    }

    /**
     * Select a column specified by the query expression
     * @param queryExpr The query expression
     * @return The index of the column in the select
     */
    private synchronized int selectQueryExpression(FieldQueryExpression queryExpr)
    {
        assertNotFrozen();

        String exprStr = queryExpr.toString();
        return selectItem(exprStr);
    }

    /**
     * Convenience method to find the position of an item in the select list
     * and return its position (first item is position 1). If the item is not
     * selected it is added and its new position returned.
     * @param item The item
     * @return Its position in the select list (first position is 1)
     */
    private int selectItem(String item)
    {
        if (selected.contains(item))
        {
            // Already have a select item with this exact name so just return with that
            return selected.indexOf(item) + 1;
        }

        int numberSelected = selected.size();
        for (int i=0;i<numberSelected;i++)
        {
            String selectedItem = (String)selected.get(i);
            if (selectedItem.startsWith(item + " "))
            {
                // We already have the same column but with an alias
                return (i+1);
            }
            else if (item.startsWith(selectedItem + " "))
            {
                // We are trying to add an aliased form of something that already exists
                // so swap what is there already for our aliased variant
                selected.set(i, item);
                return (i+1);
            }
        }

        // The item doesnt exist so add it and return its new position
        selected.add(selected.size(), item);
        return selected.indexOf(item) + 1;
    }

    /**
     * Accessor for the number of ScalarExpression projected.
     * @return The number of columns in the SELECT
     **/
    public int getNumberOfScalarExpressions()
    {
        return selected.size();
    }

    /**
     * Whether this query will return a meta data expression (containing JPOXMETADATA).
     * @return true if a meta data expression is returned by this query
     */
    public boolean hasMetaDataExpression()
    {
        for (int i=0; i<selected.size(); i++)
        {
            if (selected.get(i).toString().endsWith(MetaDataStringLiteral.QUERY_META_DATA))           
            {
                return true;
            }
        }
        return false;
    }

    /**
     * An expression in a query. In RBDMS datastores, an expression might be a column.
     **/
    private class FieldQueryExpression
    {
        private final LogicSetExpression te;
        private final DatastoreField datastoreField;
        private final String toString;
        private final int hashCode;

        protected FieldQueryExpression(LogicSetExpression te, DatastoreField datastoreField, String alias)
        {
            this.te = te;
            this.datastoreField = datastoreField;
            if (alias != null)
            {
                this.toString = te.referenceColumn(datastoreField) + " AS " + alias;
            }
            else
            {
                this.toString = te.referenceColumn(datastoreField);
            }
            this.hashCode = this.toString.hashCode();
        }

        public int hashCode()
        {
            return hashCode;
        }
       
        public boolean equals(Object o)
        {
            if (o == this)
            {
                return true;
            }

            if (!(o instanceof FieldQueryExpression))
            {
                return false;
            }

            FieldQueryExpression qsc = (FieldQueryExpression)o;
            return te.equals(qsc.te) && datastoreField.equals(qsc.datastoreField);
        }

        public String toString()
        {
            return toString;
        }
    }

    private FieldQueryExpression getQueryExpression(DatastoreIdentifier alias, DatastoreField col, String colAlias)
    {
        LogicSetExpression te = (LogicSetExpression)tableExprsByAlias.get(alias);
        if (te == null)
        {
            throw new JPOXException(LOCALISER.msg("052501", alias)).setFatal();
        }

        return getQueryExpression(te, col, colAlias);
    }

    private FieldQueryExpression getQueryExpression(LogicSetExpression te, DatastoreField field, String alias)
    {
        return new FieldQueryExpression(te, field, alias);
    }

    // -------------------------------- FROM clause methods --------------------------------------

    /**
     * Accessor for the main table of this statement.
     * @return Returns the main table expression
     */
    public LogicSetExpression getMainTableExpression()
    {
        return mainTableExpr;
    }

    /**
     * Accessor for the main table identifier alias.
     * @return Alias for the main table
     */
    public DatastoreIdentifier getMainTableAlias()
    {
        return mainTableAlias;
    }

    /**
     * Accessor for the table with the specified "alias".
     * Returns null if the table is not utilised in this query expression.
     * @param alias Alias for the table required
     * @return The table expression
     */
    public LogicSetExpression getTableExpression(DatastoreIdentifier alias)
    {
        return (LogicSetExpression)tableExprsByAlias.get(alias);
    }

    /**
     * Method to return the table expression for a new table, and add to the managed table expressions
     * for this query.
     * @param table The table
     * @param alias Alias for the table
     * @return The table expression
     */
    public LogicSetExpression newTableExpression(DatastoreContainerObject table, DatastoreIdentifier alias)
    {
        assertNotFrozen();

        LogicSetExpression te = (LogicSetExpression)tableExprsByAlias.get(alias);
        if (te == null)
        {
            te = ((RDBMSAdapter)storeMgr.getDatastoreAdapter()).newTableExpression(this, table, alias);
            tableExprsByAlias.put(alias, te);
        }
        else
        {
            if (!te.getMainTable().equals(table))
            {
                throw new JPOXException(LOCALISER.msg("052500", alias,
                    this.toStatementText(false))).setFatal();
            }
        }

        return te;
    }

    /**
     * Method to return the table expression for a new table, and add to the managed table expressions
     * for this query.
     * @param table The table
     * @param alias Alias for the table
     * @param unionQueries Whether to apply to all unioned query statements.
     * @return The table expression
     */
    public LogicSetExpression[] newTableExpression(DatastoreContainerObject table, DatastoreIdentifier alias,
            boolean unionQueries)
    {
        LogicSetExpression[] expr;
        if (unionQueries)
        {
            expr = new LogicSetExpression[union.size()+1];
        }
        else
        {
            expr = new LogicSetExpression[1];
        }
        if (unionQueries)
        {
            for (int i=0; i<union.size(); i++)
            {
                QueryStatement qs = ((QueryStatement)union.get(i));
                expr[i+1] = qs.newTableExpression(table, alias);
            }
        }
        expr[0] = newTableExpression(table, alias);
        return expr;
    }

    /**
     * A join in a query.
     * Supports ANSI-92, or ANSI-86 style joins.
     * Provides the FROM clause only.
     **/
    public static class Join
    {
        /** INNER JOIN **/
        public static final int INNER_JOIN = 1;
        /** LEFT OUTER JOIN **/
        public static final int LEFT_OUTER_JOIN = 2;
        /** RIGHT OUTER JOIN **/
        public static final int RIGHT_OUTER_JOIN = 3;

        private final int type;
        private final LogicSetExpression tblExpr;
        private final ScalarExpression expr1;
        private final ScalarExpression expr2;

        /**
         * Constructor generating ANSI-92 style join.
         * This creates a FROM clause like "INNER JOIN TABLE1 ON THIS.COL1 = TABLE1.COL2"
         * @param type Type of Join
         * @param expr1 left side expression
         * @param expr2 right side expression
         * @param tblExpr Table expression for the table to apply the join
         */
        public Join(int type, ScalarExpression expr1, ScalarExpression expr2, LogicSetExpression tblExpr)
        {
            this.type = type;
            this.expr1 = expr1;
            this.expr2 = expr2;
            this.tblExpr = tblExpr;
        }

        /**
         * Constructor generating ANSI-86 style join.
         * @param expr1 left side expression
         * @param expr2 right side expression
         * @param tblExpr Table expression
         */
        public Join(ScalarExpression expr1, ScalarExpression expr2, LogicSetExpression tblExpr)
        {
            this.type = -1;
            this.expr1 = expr1;
            this.expr2 = expr2;
            this.tblExpr = tblExpr;
        }

        /**
         * Method to return the FROM clause to add.
         * This outputs the ANSI-92 style FROM clause.
         * @param rdbmsAdapter The datastore adapter
         * @param lock Whether to lock the query statement
         * @return The FROM clause of this join
         */
        public String toString(RDBMSAdapter rdbmsAdapter, boolean lock)
        {
            if (type > 0)
            {
                StringBuffer result = new StringBuffer();
                if (type == INNER_JOIN)
                {
                    result.append("INNER JOIN");
                }
                else if (type == LEFT_OUTER_JOIN)
                {
                    result.append("LEFT OUTER JOIN");
                }
                else if (type == RIGHT_OUTER_JOIN)
                {
                    result.append("RIGHT OUTER JOIN");
                }
                result.append(" " + tblExpr);

                if (lock && rdbmsAdapter.getPlaceWithOptionWithinJoinClauses())
                {
                    result.append(" WITH ").append(rdbmsAdapter.getSelectWithLockOption());
                }
                return result.append(" ON " + expr1.eq(expr2).toStatementText(ScalarExpression.FILTER)).toString();
            }
            else
            {
                return "" + tblExpr;
            }
        }

        /**
         * Accessor for the join statement FROM clause (ANSI-92).
         * @return The join statement
         */
        public String toString()
        {
            return toString(null, false);
        }
    }

    /**
     * Method to add a join to another table.
     * @param expr1 the left hand expression
     * @param expr2 the right hand expression
     * @param tblExpr The table expression for the table to apply the join
     * @param joinType Type of join
     * @param equals if the join is applied as filter, if use equals or not equals
     * @param unionQueries Whether to apply to all unioned queries as well
     **/
    public synchronized void join(ScalarExpression expr1,
                                  ScalarExpression expr2,
                                  LogicSetExpression tblExpr,
                                  int joinType,
                                  boolean equals,
                                  boolean unionQueries)
    {
        if (unionQueries)
        {
            Iterator i = union.iterator();
            while (i.hasNext())
            {
                QueryStatement qs = ((QueryStatement)i.next());
                qs.join(expr1, expr2, tblExpr, joinType, equals, unionQueries);
            }
        }
        join(expr1, expr2, tblExpr, joinType, equals);
    }

    /**
     * Method to do a join to another table.
     * All join methods relay to this one.
     * @param expr1 the left hand expression
     * @param expr2 the right hand expression
     * @param tblExpr The table expression for the table to apply the join
     * @param joinType Type of join
     * @param equals if the join is applied as filter, if use equals or not equals
     **/
    public void join(ScalarExpression expr1, ScalarExpression expr2, LogicSetExpression tblExpr, int joinType, boolean equals)
    {
        assertNotFrozen();

        if (extensions != null)
        {
            // Apply any extensions
            String value = (String)extensions.get("org.jpox.rdbms.jdoql.joinType");
            if (value != null)
            {
                // JPOX extension to specify the join type to use
                joinType = (value.equals("INNER") ? Join.INNER_JOIN : Join.LEFT_OUTER_JOIN);
            }
        }

        Join join = new Join(joinType, expr1, expr2, tblExpr);
        if (joinType == Join.INNER_JOIN)
        {
            if (tblExpr.equals(getMainTableExpression()))
            {
                if (equals)
                {
                    andCondition(expr1.eq(expr2));
                }
                else
                {
                    andCondition(expr1.noteq(expr2));
                }
                return;
            }
            if (crossJoins.contains(tblExpr))
            {
                if (equals)
                {
                    andCondition(expr1.eq(expr2));
                }
                else
                {
                    andCondition(expr1.noteq(expr2));
                }
                return;
            }
        }
        if (crossJoins.contains(tblExpr))
        {
            if (equals)
            {
                andCondition(expr1.eq(expr2));
            }
            else
            {
                andCondition(expr1.noteq(expr2));
            }
            return;
        }
        if (mainTableExpr.equals(tblExpr))
        {
            if (equals)
            {
                andCondition(expr1.eq(expr2));
            }
            else
            {
                andCondition(expr1.noteq(expr2));
            }
            return;
        }

        // if we already have this join, return
        if (joinsToTableAliases.contains(tblExpr.getAlias()))
        {
            return;
        }

        joinsToTableAliases.add(tblExpr.getAlias());
        joins.add(join);
    }

    /**
     * Method to do an inner join to another table, and optionally apply it to
     * any unions for this query.
     * @param expr the left hand expression
     * @param expr2 the right hand expression
     * @param tblExpr the
     * @param equals if the join is applied as filter, if use equals or not equals
     * @param unionQueries whether to apply the inner join in all queries unified by the union clause
     **/
    public synchronized void innerJoin(ScalarExpression expr,
                                       ScalarExpression expr2,
                           LogicSetExpression tblExpr,
                                       boolean equals,
                                       boolean unionQueries)
    {
        join(expr, expr2, tblExpr, Join.INNER_JOIN, equals, unionQueries);
    }

    /**
     * Method to do an inner join to another table.
     * @param expr the left hand expression
     * @param expr2 the right hand expression
     * @param tblExpr The table expression for the table to apply the join
     * @param equals if the join is applied as filter, if use equals or not equals
     **/
    public void innerJoin(ScalarExpression expr, ScalarExpression expr2, LogicSetExpression tblExpr, boolean equals)
    {
        join(expr, expr2, tblExpr, Join.INNER_JOIN, equals);
    }

    /**
     * Method to do a left outer join to another table, and optionally apply it
     * to any unions for this query.
     * @param expr the left hand expression
     * @param expr2 the right hand expression
     * @param tblExpr The table expression for the table to apply the join
     * @param equals if the join is applied as filter, if use equals or not equals
     * @param unionQueries Whether to apply to unions of this query.
     **/
    public synchronized void leftOuterJoin(ScalarExpression expr,
                                           ScalarExpression expr2,
                             LogicSetExpression tblExpr,
                                           boolean equals,
                                           boolean unionQueries)
    {
        join(expr, expr2, tblExpr, Join.LEFT_OUTER_JOIN, equals, unionQueries);
    }

    /**
     * Method to do a left outer join to another table.
     * @param expr the left hand expression
     * @param expr2 the right hand expression
     * @param tblExpr The table expression
     * @param equals if the join is applied as filter, if use equals or not equals
     **/
    public void leftOuterJoin(ScalarExpression expr, ScalarExpression expr2, LogicSetExpression tblExpr, boolean equals)
    {
        join(expr, expr2, tblExpr, Join.LEFT_OUTER_JOIN, equals);
    }

    /**
     * Method to do a right outer join to another table, and optionally apply it
     * to any unions for this query.
     * @param expr the left hand expression
     * @param expr2 the right hand expression
     * @param tblExpr The table expression for the table to apply the join
     * @param equals if the join is applied as filter, if use equals or not equals
     * @param unionQueries Whether to apply to unions of this query.
     **/
    public synchronized void rightOuterJoin(ScalarExpression expr,
                                            ScalarExpression expr2,
                            LogicSetExpression tblExpr,
                                            boolean equals,
                                            boolean unionQueries)
    {
        join(expr, expr2, tblExpr, Join.RIGHT_OUTER_JOIN, equals, unionQueries);
    }

    /**
     * Method to do a right outer join to another table.
     * @param expr the left hand expression
     * @param expr2 the right hand expression
     * @param tblExpr The table expression for the table to apply the join
     * @param equals if the join is applied as filter, if use equals or not equals
     **/
    public void rightOuterJoin(ScalarExpression expr, ScalarExpression expr2, LogicSetExpression tblExpr, boolean equals)
    {
        join(expr, expr2, tblExpr, Join.RIGHT_OUTER_JOIN, equals);
    }

    /**
     * Method to return if the query statement has a cross join involving the supplied table expression.
     * Navigates up through any parent query expression if necessary.
     * @param tableExpr table expression
     * @return whether there is a cross join with this table
     */
    public boolean hasCrossJoin(LogicSetExpression tableExpr)
    {
        if (crossJoins.contains(tableExpr))
        {
            return true;
        }
        for (int i=0; i<joins.size(); i++)
        {
            if (((Join)joins.get(i)).tblExpr.equals(tableExpr))
            {
                return true;
            }
        }
        if (this.parentQueryExpr != null)
        {
            return this.parentQueryExpr.hasCrossJoin(tableExpr);
        }
        return false;
    }

    /**
     * Method to add a cross-join to the supplied table.
     * Cross joins are output in the statement as <pre>FROM TABLE1,TABLE2,TABLE3</pre>
     * @param tableExpr table expression
     * @param unionQueries Whether to apply the alias to unions of this query.
     */
    public void crossJoin(LogicSetExpression tableExpr, boolean unionQueries)
    {
        if (unionQueries)
        {
            Iterator i = union.iterator();
            while (i.hasNext())
            {
                QueryStatement qs = ((QueryStatement)i.next());
                qs.crossJoin(tableExpr);
            }
        }
        crossJoin(tableExpr);
    }

    /**
     * Internal method to add a cross join to the supplied table.
     * If a cross-join is already present for the table, or if the table is the candidate table, does nothing.
     * @param tableExpr table expression
     */
    private void crossJoin(LogicSetExpression tableExpr)
    {
        assertNotFrozen();
        if (hasCrossJoin(tableExpr) || mainTableExpr.equals(tableExpr))
        {
            // if we already have this alias, return
            return;
        }
        crossJoins.add(tableExpr);
    }

    /**
     * Sort Join instances making sure that joining tables where previously
     * declared before the search condition ON.
     * @param toSort list of Join to sort
     * @return a list of sorted Join instances
     */
    private List sortJoins(List toSort)
    {
        //sort it twice to make sure the last joins are correctly placed
        //it seems no sense, but the logic on internalSortJoins needs a second pass
        List temp = internalSortJoins(toSort);
        return internalSortJoins(temp);
    }

    /**
     * Sort Join instances making sure that joining tables where previously
     * declared before the search condition ON.
     * @param toSort list of Join to sort
     * @return a list of sorted Join instances
     */
    private List internalSortJoins(List toSort)
    {
        //Join joins[] = (Join[]) toSort.toArray(new Join[toSort.size()]); 
        List temp = toSort;
        for (int i = 0; i < temp.size(); i++)
        {
            Join join0 = (Join) temp.get(i);
            int index = i;
            boolean changed = false;

            //if THIS != join expression side 1
            DatastoreIdentifier joinExpr1TableAlias = join0.expr1.getLogicSetExpression().getAlias();
            if (!getMainTableExpression().getAlias().equals(joinExpr1TableAlias))
            {
                for (int j = 0; j < temp.size(); j++)
                {
                    Join join1 = (Join) temp.get(j);
                    if (join1 != join0 && join1.tblExpr.getAlias().equals(joinExpr1TableAlias))
                    {
                        if (!changed)
                        {
                            index = j;
                            changed = true;
                        }
                    }
                }
            }

            //if THIS != join expression side 2
            DatastoreIdentifier joinExpr2TableAlias = join0.expr2.getLogicSetExpression().getAlias();
            if (!getMainTableExpression().getAlias().equals(joinExpr2TableAlias))
            {
                for (int j = 0; j < temp.size(); j++)
                {
                    Join join1 = (Join) temp.get(j);
                    if (join1 != join0 && join1.tblExpr.getAlias().equals(joinExpr2TableAlias))
                    {
                        if (!changed || index < j)
                        {
                            index = j;
                            changed = true;
                        }
                    }
                }
            }

            if (index != i)
            {
                temp.remove(i);
                temp.add((i < index) ? index : index + 1, join0);
            }
        }
        return temp;
    }

    // --------------------------------- WHERE clause methods ------------------------------------

    /**
     * add an condition to the query and queries involved in the union if unionQuery is true
     * @param condition the Boolean expression
     * @param unionQueries whether to apply the condition in all queries unified by the union clause
     */
    public void andCondition(BooleanExpression condition, boolean unionQueries)
    {
        assertNotFrozen();

        andCondition(condition);
        if (!unionQueries)
        {
            return;
        }

        // add condition to union
        Iterator i = union.iterator();
        while (i.hasNext())
        {
            QueryStatement qs = ((QueryStatement)i.next());
            qs.andCondition(condition);
        }
    }

    /**
     * Method to add an additional WHERE clause to the query.
     * @param condition The where clause expression
     */
    public void andCondition(BooleanExpression condition)
    {
        assertNotFrozen();

        // if condition.toString() == "TRUE" or "FALSE", doesnt make sense or doesnt work pass it to the sql query
        if (condition instanceof BooleanLiteral)
        {
            if (((Boolean)((BooleanLiteral)condition).getValue()).booleanValue())
            {
                return;
            }
            JavaTypeMapping m = storeMgr.getDatastoreAdapter().getMapping(Integer.class, storeMgr);
            condition = m.newLiteral(this, new Integer("1")).eq(m.newLiteral(this, new Integer("0")));
        }

        if (whereExpr == null)
        {
            whereExpr = condition;
        }
        else
        {
            whereExpr = whereExpr.and(condition);
        }
    }

    /**
     * add an condition to the query and queries involved in the union if unionQuery is true
     * @param condition the Boolean expression
     * @param unionQueries whether to apply the condition in all queries unified by the union clause
     */
    public void iorCondition(BooleanExpression condition, boolean unionQueries)
    {
        assertNotFrozen();

        iorCondition(condition);
        if (!unionQueries)
        {
            return;
        }

        // add condition to union
        Iterator i = union.iterator();
        while (i.hasNext())
        {
            QueryStatement qs = ((QueryStatement)i.next());
            qs.iorCondition(condition);
        }
    }

    /**
     * Method to add an IOR condition to the WHERE clause of the statement.
     * @param condition The WHERE clause expression
     */
    public void iorCondition(BooleanExpression condition)
    {
        assertNotFrozen();

        // if condition.toString() == "TRUE" or "FALSE", doesnt make sense or doesnt work pass it to the sql query
        if (condition instanceof BooleanLiteral)
        {
            if (condition.toStatementText(ScalarExpression.FILTER).toStatementString(ScalarExpression.FILTER).equals("TRUE"))
            {
                return;
            }      
            JavaTypeMapping m = storeMgr.getDatastoreAdapter().getMapping(Integer.class, storeMgr);
            condition = m.newLiteral(this, new Integer("1")).eq(m.newLiteral(this, new Integer("0")));
        }

        if (whereExpr == null)
        {
            whereExpr = condition;
        }
        else
        {
            whereExpr = whereExpr.ior(condition);
        }
    }

    /**
     * Method to add a grouping expression to the query.
     * @param expr The expression
     */
    public void addGroupingExpression(ScalarExpression expr)
    {
        if (groupingExpressions == null)
        {
            groupingExpressions = new ArrayList();
        }
        groupingExpressions.add(expr);
        hasAggregateExpression = true;
    }

    /**
     * Accessor for the grouping expressions (if any).
     * @return The grouping expressions
     */
    public ScalarExpression[] getGroupingExpressions()
    {
        if (groupingExpressions == null)
        {
            return null;
        }
        return (ScalarExpression[])groupingExpressions.toArray(new ScalarExpression[groupingExpressions.size()]);
    }

    /**
     * Mutator for the "having" expression.
     * @param expr Boolean expression for the having clause
     */
    public void setHaving(BooleanExpression expr)
    {
        havingExpr = expr;
        hasAggregateExpression = true;
    }

    /**
     * Accessor for the having expression (if any).
     * @return The having expression
     */
    public BooleanExpression getHavingExpression()
    {
        return havingExpr;
    }

    /**
     * Mutator for the ordering criteria.
     * @param exprs The expressions to order by
     * @param descending Whether each expression is ascending/descending
     **/
    public void setOrdering(ScalarExpression[] exprs, boolean[] descending)
    {
        assertNotFrozen();
        if (exprs.length != descending.length)
        {
            throw new JPOXException(LOCALISER.msg("052503", "" + exprs.length, "" + descending.length)).setFatal();
        }

        orderingExpressions = exprs;
        orderingDirections = descending;
    }

    /**
     * Accessor for the ordering expressions (if any).
     * @return The ordering expressions
     */
    public ScalarExpression[] getOrderingExpressions()
    {
        return orderingExpressions;
    }

    /**
     * Method to add a range constraint on any SELECT.
     * This typically will use LIMIT/OFFSET where they are supported by
     * the underlying RDBMS.
     * @param offset The offset to start from
     * @param count The number of records to return
     */
    public void setRangeConstraint(long offset, long count)
    {
        this.rangeOffset = offset;
        this.rangeCount = count;
    }

    /**
     * Convenience method to add any necessary columns to the SELECT that are needed
     * by the ordering constraint.
     */
    protected void addOrderingColumnsToSelect()
    {
        if (orderingExpressions != null)
        {
            // Add the ordering columns to the selected list if required
            boolean needsSelect = ((RDBMSAdapter)storeMgr.getDatastoreAdapter()).includeOrderByColumnsInSelect();
            if (needsSelect)
            {
                for (int i=0; i<orderingExpressions.length; ++i)
                {
                    String orderExpr = "JPOXORDER" + i;
                    Iterator iterator = union.iterator();
                    while (iterator.hasNext())
                    {
                        QueryStatement qs = (QueryStatement)iterator.next();
                        if (hasAggregateExpression)
                        {
                            qs.selectScalarExpression(orderingExpressions[i]);
                        }
                        else
                        {
                            qs.selectScalarExpression(orderingExpressions[i].as(orderExpr));
                        }
                    }
                    if (hasAggregateExpression)
                    {
                        selectScalarExpression(orderingExpressions[i]);
                    }
                    else
                    {
                        selectScalarExpression(orderingExpressions[i].as(orderExpr));
                    }
                }
            }
        }
    }

    /**
     * Convenience method to generate the ordering statement to add to the overall
     * query statement.
     * @return The ordering statement
     */
    protected StatementText generateOrderingStatement()
    {
        StatementText orderByStmt = null;
        if (orderingExpressions != null && orderingExpressions.length > 0)
        {
            orderByStmt = new StatementText();
            boolean needsSelect = ((RDBMSAdapter)storeMgr.getDatastoreAdapter()).includeOrderByColumnsInSelect();
            for (int i=0; i<orderingExpressions.length; ++i)
            {
                String orderExpr = "JPOXORDER" + i;
                if (i > 0)
                {
                    orderByStmt.append(',');
                }

                if (needsSelect && !hasAggregateExpression)
                {
                    // Order by the "JPOXORDER?" if we need them to be selected and it isn't an aggregate
                    orderByStmt.append(orderExpr);
                }
                else
                {
                    // Order by the "THIS.COLUMN" otherwise
                    orderByStmt.append(orderingExpressions[i].toStatementText(ScalarExpression.PROJECTION).toStatementString(ScalarExpression.PROJECTION));
                }

                if (orderingDirections[i])
                {
                    orderByStmt.append(" DESC");
                }
            }
        }
        return orderByStmt;
    }

    // --------------------------------- UPDATE clause methods ------------------------------------

    /**
     * Method to set the UPDATE clause of the statement.
     * @param exprs The update clause expression
     */
    public void setUpdates(ScalarExpression[] exprs)
    {
        assertNotFrozen();

        updateExprs = exprs;
    }

    // ----------------------------------- Output methods ------------------------------------------

    /**
     * Method to convert the criteria into a delete statement text.
     * <pre>
     * DELETE FROM TBL THIS WHERE THIS.COL3 = VAL3
     * </pre>
     * @return The StatementText for the DELETE
     **/
    public StatementText toDeleteStatementText()
    {
        StatementText stmtText = new StatementText("DELETE FROM ");
        stmtText.append(mainTableExpr.toString());

        // TODO This only works where the base query statement has no joins

        if (whereExpr != null)
        {
            stmtText.append(" WHERE ").append(
                whereExpr.toStatementText(ScalarExpression.FILTER), ScalarExpression.FILTER);
        }

        return stmtText;
    }

    /**
     * Method to convert the criteria into an update statement text.
     * TODO This could generate more than 1 statement if the updated fields are stored in
     * more than 1 table
     * <pre>
     * UPDATE TBL THIS SET THIS.COL1 = VAL1, THIS.COL2 = VAL2 WHERE THIS.COL3 = VAL3
     * </pre>
     * @return The StatementText for the UPDATE
     */
    public StatementText toUpdateStatementText()
    {
        StatementText stmtText = new StatementText("UPDATE ");
        stmtText.append(mainTableExpr.toString());

        stmtText.append(" SET ");
        if (updateExprs != null && updateExprs.length > 0)
        {
            for (int i=0;i<updateExprs.length;i++)
            {
                if (i != 0)
                {
                    stmtText.append(",");
                }
                stmtText.append(
                    updateExprs[i].toStatementText(ScalarExpression.PROJECTION), ScalarExpression.PROJECTION);
            }
        }

        if (whereExpr != null)
        {
            stmtText.append(" WHERE ").append(
                whereExpr.toStatementText(ScalarExpression.FILTER), ScalarExpression.FILTER);
        }

        return stmtText;
    }

    /**
     * Method to convert the criteria into the statement text.
     * @param lock whether to lock the instances using this statement
     * @return The StatementText
     */
    public StatementText toStatementText(boolean lock)
    {
        if (this.stmtText == null)
        {
            // Make sure we have any necessary ordering columns in the SELECT (where required)
            addOrderingColumnsToSelect();

            final RDBMSAdapter rdbmsAdapter = (RDBMSAdapter)storeMgr.getDatastoreAdapter();
            StatementText stmtText = null;

      stmtText = new StatementText("SELECT ");

            if (rangeOffset > -1 || rangeCount > -1)
            {
                if (rdbmsAdapter.getRangeByRowNumberColumn().length()>0)
                {
                    // When doing range queries using ROWNUM, we must wrap the original query
                    // in an outer query that does selection based on an aliased ROWNUM column
                    // of the inner (original) query (CORE-2721)
                    //
                    // see also http://72.14.207.104/search?q=cache:IZZXCUsqdIIJ:www.arrowsent.com/oratip/tip41.htm+oracle+rownum+range&hl=en&ct=clnk&cd=1
                    // SELECT the ROWNUM column and alias it in what will be the inner query
                    stmtText.append(rdbmsAdapter.getRangeByRowNumberColumn() + " rn, ");
                }
                else
                {
                    // Add a LIMIT clause to SELECT if it is supported
                    stmtText.append(rdbmsAdapter.getRangeByLimitSelectClause(rangeOffset, rangeCount));
                }
            }

            boolean usingDistinct = false;
            if (!isExistsSubQuery)
            {
              if (distinctResults)
              {
                  stmtText.append("DISTINCT ");
                    usingDistinct = true;
              }

              Iterator iterator = selected.iterator();
              while (iterator.hasNext())
              {
                    Object selectExpr = iterator.next();
                  stmtText.append(selectExpr.toString());
                  if (iterator.hasNext())
                  {
                      stmtText.append(',');
                  }
              }
                if ((rangeOffset > -1 || rangeCount > -1) && rdbmsAdapter.getRangeByRowNumberColumn().length() > 0)
                {
                    // Add a ROW NUMBER column if supported as the means of handling ranges by the RDBMS
                    stmtText.append(',').append(rdbmsAdapter.getRangeByRowNumberColumn());
                }
            }
            else
            {
                JavaTypeMapping m = rdbmsAdapter.getMapping(Integer.class, storeMgr);
                stmtText.append(m.newLiteral(this, new Integer("1")).toStatementText(ScalarExpression.PROJECTION).toStatementString(ScalarExpression.PROJECTION));
            }

            /*
             * 1. Push joins down
             * 2. try to make sure joins refer to previous table in stack
             *
             *  A
             *  JOIN B = A
             *  JOIN C = C
             *  JOIN D = D
             */
            //SORT JOINS
            List sorted = sortJoins(joins);
            final Join[] sortedJoins = (Join[]) sorted.toArray(new Join[sorted.size()]);

            // FROM condition(s)
            stmtText.append(" FROM ");

            List crossJoinss = new ArrayList();
            crossJoinss.add(mainTableExpr);
            crossJoinss.addAll(crossJoins);
            if (crossJoinss.size()==1)
            {
                stmtText.append(crossJoinss.get(0).toString());
                if (lock && rdbmsAdapter.getSelectWithLockOption() != null && rdbmsAdapter.getPlaceWithOptionAfterFromClause())
                {
                    // Add any locking of the rows for datastores that require WITH option to be placed after the FROM clause
                    stmtText.append(" WITH " + rdbmsAdapter.getSelectWithLockOption());
                }
                crossJoinss.remove(0);
            }
           
            for (int i=0; i<sortedJoins.length; i++)
            {
                final Join join = sortedJoins[i];
                for (int j=crossJoinss.size()-1; j>=0; j--)
                {
                    if ((sortedJoins[i].expr1.getLogicSetExpression().equals(crossJoinss.get(j)) ||
                         sortedJoins[i].expr2.getLogicSetExpression().equals(crossJoinss.get(j))))
                    {
                        if (i > 0)
                        {
                            stmtText.append(rdbmsAdapter.cartersianProduct((LogicSetExpression)crossJoinss.get(j)));
                        }
                        else
                        {
                            stmtText.append(crossJoinss.get(j).toString());
                        }
                        if (lock && rdbmsAdapter.getSelectWithLockOption() != null && rdbmsAdapter.getPlaceWithOptionAfterFromClause())
                        {
                            // Add any locking of the rows for datastores that require WITH option to be placed after the FROM clause
                            stmtText.append(" WITH " + rdbmsAdapter.getSelectWithLockOption());
                        }
                        crossJoinss.remove(j);
                        break;
                    }
                }
                stmtText.append(stmtJoinsSeparator).append(join.toString(rdbmsAdapter, lock));
            }
            crossJoinss.remove(null);
            for (int i=0; i<crossJoinss.size(); i++)
            {
                if (sortedJoins.length > 0 || i > 0)
                {
                    stmtText.append(rdbmsAdapter.cartersianProduct((LogicSetExpression)crossJoinss.get(i)));
                }
                else
                {
                    stmtText.append(crossJoinss.get(i).toString());
                }
                if (lock && rdbmsAdapter.getSelectWithLockOption() != null && rdbmsAdapter.getPlaceWithOptionAfterFromClause())
                {
                    // Add any locking of the rows for datastores that require WITH option to be placed after the FROM clause
                    stmtText.append(" WITH " + rdbmsAdapter.getSelectWithLockOption());
                }
            }

            // WHERE condition(s)
            if (whereExpr != null)
            {
                stmtText.append(" WHERE ").append(whereExpr.toStatementText(ScalarExpression.FILTER),ScalarExpression.FILTER);
            }

            // GROUP BY clause(s)
            if (groupingExpressions != null)
            {
                List groupBy = new ArrayList();
                for (int i=0;i<groupingExpressions.size();i++)
                {
                    // Add on the user-specified groupings
                    String exprText = ((ScalarExpression)groupingExpressions.get(i)).toStatementText(ScalarExpression.PROJECTION).toString();
                    if (!groupBy.contains(exprText))
                    {
                        groupBy.add(exprText);
                    }
                }
                if (groupBy.size() > 0 && hasAggregateExpression)
                {
                    stmtText.append(" GROUP BY ");
                    boolean first = true;
                    for (int i=0; i<groupBy.size(); i++)
                    {
                        if (!first)
                        {
                            stmtText.append(',');
                        }
                        stmtText.append((String)groupBy.get(i));
                        first = false;
                    }
                }
            }

            // HAVING clause
            if (havingExpr != null)
            {
                stmtText.append(" HAVING ").append(havingExpr.toStatementText(ScalarExpression.FILTER),ScalarExpression.FILTER);
            }

            // UNION to other query statements
            Iterator iterator = union.iterator();
            while (iterator.hasNext())
            {
                if (!rdbmsAdapter.supportsUnionSyntax())
                {
                    throw new JPOXException(LOCALISER.msg("052504", "UNION")).setFatal();
                }

                if (rdbmsAdapter.useUnionAll())
                {
                    stmtText.append(" UNION ALL ");
                }
                else
                {
                    stmtText.append(" UNION ");
                }
                QueryStatement qs = ((QueryStatement)iterator.next());
                //do not use for update in other union queries. is this correct for all databases?
                stmtText.append(qs.toStatementText(false),ScalarExpression.FILTER);
            }

            // ORDER clause
            if (!isExistsSubQuery)
            {
                if (orderingExpressions != null && orderingExpressions.length > 0)
                {
                    StatementText orderByStmt = generateOrderingStatement();
                  stmtText.append(" ORDER BY ").append(orderByStmt,ScalarExpression.PROJECTION);
              }
            }

            if (rangeOffset > -1 || rangeCount > -1)
            {
                // Add a LIMIT clause to WHERE if it is supported
                stmtText.append(rdbmsAdapter.getRangeByLimitWhereClause(rangeOffset, rangeCount));
            }

            // Add any required locking based on the RDBMS capability
            if (lock && rdbmsAdapter.supportsLockWithSelectForUpdate())
            {
                if (usingDistinct && !rdbmsAdapter.supportsDistinctWithSelectForUpdate())
                {
                    JPOXLogger.QUERY.warn(LOCALISER.msg("052502"));
                }
                else
                {
                    stmtText.append(" FOR UPDATE");
                }
            }
           
            if ((rangeOffset > -1 || rangeCount > -1) && rdbmsAdapter.getRangeByRowNumberColumn().length() > 0)
            {
                // range query using ROWNUM (CORE-2721): wrap in outer query,
                // which must select all columns of the inner query,
                // except for the ROWNUM column (which is not contained in
                // selected list)
                StatementText innerQuery = stmtText;
                stmtText = new StatementText("SELECT ");
                iterator = selected.iterator();
                while (iterator.hasNext())
                {
                    Object selectExpr = iterator.next();
                    stmtText.append("subq.");
                    String selectedCol = selectExpr.toString();
                    final int dotIndex = selectedCol.indexOf(".");
                    if (dotIndex != -1)
                    {
                        // must remove prepending "THIS.bla" from referenced
                        // column bla
                        selectedCol = selectedCol.substring(dotIndex + 1);
                    }
                    stmtText.append(selectedCol);
                    if (iterator.hasNext())
                    {
                        stmtText.append(',');
                    }
                }
                stmtText.append(" FROM (");
                stmtText.append(innerQuery, ScalarExpression.FILTER);
                stmtText.append(") subq");
                // Add conditions on the row number
                stmtText.append(" WHERE ");
                if (rangeOffset > -1)
                {
                    stmtText.append("subq.rn").append(">=").append("" + rangeOffset);
                }
                if (rangeCount > -1)
                {
                    if (rangeOffset > -1)
                    {
                        stmtText.append(" AND ");
                    }
                    stmtText.append("subq.rn").append("<").append("" + (rangeCount + rangeOffset));
                }
            }

      this.stmtText = stmtText;
        }

        return this.stmtText;
    }

    /**
     * Assert if the statement text is already generated.
     * @throws JPOXException Thrown if the statement is already generated
     */
    protected void assertNotFrozen()
    {
        if (stmtText != null)
        {
            // TODO Remove this so that we allow a statement be generated then further modified later
            // Would help a lot in debugging. This method should simply call reset()
            throw new JPOXException("A query statement cannot be modified after being output").setFatal();
        }
    }
}
TOP

Related Classes of org.jpox.store.rdbms.query.QueryStatement

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.