/*
Copyright (c) 2008 Health Market Science, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
You can contact Health Market Science at info@healthmarketscience.com
or at the following address:
Health Market Science
2700 Horizon Drive
Suite 200
King of Prussia, PA 19406
*/
package com.healthmarketscience.sqlbuilder;
import java.io.IOException;
import com.healthmarketscience.common.util.AppendableExt;
import com.healthmarketscience.sqlbuilder.dbspec.Column;
/**
* Base query for queries which generate a series of SELECT queries joined by
* one or more "set operations", such as UNION [ALL], EXCEPT [ALL], and
* INSERSECT [ALL].
*
* @author James Ahlborn
*/
public class SetOperationQuery<ThisType extends SetOperationQuery<ThisType>>
extends Query<ThisType>
{
/** Enumeration representing the type of union to use */
public enum Type
{
UNION(" UNION "),
UNION_ALL(" UNION ALL "),
EXCEPT(" EXCEPT "),
EXCEPT_ALL(" EXCEPT ALL "),
INTERSECT(" INTERSECT "),
INTERSECT_ALL(" INTERSECT ALL ");
private final String _typeStr;
private Type(String typeStr) {
_typeStr = typeStr;
}
@Override
public String toString() { return _typeStr; }
}
private Type _defaultType;
private SqlObjectList<RelateTo> _queries = SqlObjectList.create("");
private SqlObjectList<SqlObject> _ordering = SqlObjectList.create();
public SetOperationQuery(Type type) {
this(type, (Object[])null);
}
public SetOperationQuery(Type type, Object... queries) {
_defaultType = type;
addQueriesImpl(_defaultType, queries);
}
/** Actual implementation which adds the given queries to the list of
queries with the given type. */
private void addQueriesImpl(final Type type, Object[] queries)
{
_queries.addObjects(new Converter<Object,RelateTo>() {
@Override
public RelateTo convert(Object src) {
return (_queries.isEmpty() ?
new RelateTo(null, src) :
new RelateTo(type, src));
}
}, queries);
}
/** Adds the given queries to the list of queries with the default set
operation type (the one configured in the constructor). */
public ThisType addQueries(SelectQuery... queries) {
return addQueries((Object[])queries);
}
/** Adds the given queries to the list of queries with the given set
operation type. */
public ThisType addQueries(Type type, SelectQuery... queries) {
return addQueries(type, (Object[])queries);
}
/** Adds the given queries to the list of queries with the default set
operation type (the one configured in the constructor). */
public ThisType addQueries(Object... queries) {
return addQueries(_defaultType, queries);
}
/** Adds the given queries to the list of queries with the given set
operation type. */
public ThisType addQueries(Type type, Object... queries) {
addQueriesImpl(type, queries);
return getThisType();
}
/**
* Adds the given column with the given direction to the "ORDER BY"
* clause
* <p>
* {@code Object} -> {@code SqlObject} conversions handled by
* {@link Converter#toCustomColumnSqlObject(Object)}.
*/
public ThisType addCustomOrdering(Object columnStr,
OrderObject.Dir dir) {
return addCustomOrderings(
new OrderObject(dir, Converter.toCustomColumnSqlObject(columnStr)));
}
/**
* Adds the given columns to the "ORDER BY" clause
* <p>
* {@code Object} -> {@code SqlObject} conversions handled by
* {@link Converter#CUSTOM_COLUMN_TO_OBJ}.
*/
public ThisType addCustomOrderings(Object... columnStrs) {
_ordering.addObjects(Converter.CUSTOM_COLUMN_TO_OBJ, columnStrs);
return getThisType();
}
/** Adds the given column with the given direction to the "ORDER BY"
clause */
public ThisType addOrdering(Column column, OrderObject.Dir dir) {
return addCustomOrdering(column, dir);
}
/** Adds the given columns to the "ORDER BY" clause */
public ThisType addOrderings(Column... columns) {
return addCustomOrderings((Object[])columns);
}
/** Adds the given column index with the given direction to the "ORDER BY"
clause */
public ThisType addIndexedOrdering(Integer columnIdx,
OrderObject.Dir dir) {
return addCustomOrdering(columnIdx, dir);
}
/** Adds the given column index to the "ORDER BY" clause */
public ThisType addIndexedOrderings(Integer... columnIdxs) {
return addCustomOrderings((Object[])columnIdxs);
}
@Override
public void validate(ValidationContext vContext)
throws ValidationException
{
// check super
super.validate(vContext);
// now, validate each of our sub-query's select queries, and check numbers
// of args if possible
int currentCount = -1;
boolean ignoreColumnCount = false;
for(RelateTo relateTo : _queries) {
Object queryObj = relateTo.getQuery();
if(!(queryObj instanceof SelectQuery)) {
continue;
}
SelectQuery selectQuery = (SelectQuery)queryObj;
// check the column count against the other queries
if(!ignoreColumnCount) {
if(selectQuery.hasAllColumns()) {
// can't validate if using the "*" syntax
ignoreColumnCount = true;
} else {
if(currentCount < 0) {
// get expected column count
currentCount = selectQuery.getColumns().size();
} else {
// validate current query against expected count
if(currentCount != selectQuery.getColumns().size()) {
throw new ValidationException(
"mismatched number of columns in union statement");
}
}
}
}
// sub-selects may not have ordering clauses
if(!selectQuery.getOrdering().isEmpty()) {
throw new ValidationException(
"Union selects may not have ordering clause");
}
}
SelectQuery.validateOrdering(currentCount, _ordering, ignoreColumnCount);
}
@Override
protected void collectSchemaObjects(ValidationContext vContext) {
super.collectSchemaObjects(vContext);
_queries.collectSchemaObjects(vContext);
}
@Override
protected void appendTo(AppendableExt app, SqlContext newContext)
throws IOException
{
// this will only really apply to the ordering not to the sub-queries, as
// they may use a different value internally
newContext.setUseTableAliases(false);
app.append(_queries);
if(!_ordering.isEmpty()) {
// append ordering clause
app.append(" ORDER BY ").append(_ordering);
}
}
/**
* Convenience method to create a UNION query.
*/
public static UnionQuery union() {
return new UnionQuery(Type.UNION);
}
/**
* Convenience method to create a UNION query.
*/
public static UnionQuery union(SelectQuery... queries) {
return new UnionQuery(Type.UNION, queries);
}
/**
* Convenience method to create a UNION ALL query.
*/
public static UnionQuery unionAll() {
return new UnionQuery(Type.UNION_ALL);
}
/**
* Convenience method to create a UNION ALL query.
*/
public static UnionQuery unionAll(SelectQuery... queries) {
return new UnionQuery(Type.UNION_ALL, queries);
}
/**
* Convenience method to create a EXCEPT query.
*/
public static ExceptQuery except() {
return new ExceptQuery(Type.EXCEPT);
}
/**
* Convenience method to create a EXCEPT query.
*/
public static ExceptQuery except(SelectQuery... queries) {
return new ExceptQuery(Type.EXCEPT, queries);
}
/**
* Convenience method to create a EXCEPT ALL query.
*/
public static ExceptQuery exceptAll() {
return new ExceptQuery(Type.EXCEPT_ALL);
}
/**
* Convenience method to create a EXCEPT ALL query.
*/
public static ExceptQuery exceptAll(SelectQuery... queries) {
return new ExceptQuery(Type.EXCEPT_ALL, queries);
}
/**
* Convenience method to create a INTERSECT query.
*/
public static IntersectQuery intersect() {
return new IntersectQuery(Type.INTERSECT);
}
/**
* Convenience method to create a INTERSECT query.
*/
public static IntersectQuery intersect(SelectQuery... queries) {
return new IntersectQuery(Type.INTERSECT, queries);
}
/**
* Convenience method to create a INTERSECT ALL query.
*/
public static IntersectQuery intersectAll() {
return new IntersectQuery(Type.INTERSECT_ALL);
}
/**
* Convenience method to create a INTERSECT ALL query.
*/
public static IntersectQuery intersectAll(SelectQuery... queries) {
return new IntersectQuery(Type.INTERSECT_ALL, queries);
}
/**
* Outputs the set operator type (if non-{@code null}) and the query
* <code>"<type> <query>"</code>.
*/
private static class RelateTo extends Subquery
{
private Type _type;
private RelateTo(Type type, Object query)
{
super(query);
_type = type;
}
private Object getQuery() {
return _query;
}
@Override
public void appendTo(AppendableExt app) throws IOException {
// type is null for the first query
if(_type != null) {
app.append(_type);
}
app.append(getQuery());
}
}
}