diff --git a/src/main/mondrian/rolap/RolapNativeSet.java b/src/main/mondrian/rolap/RolapNativeSet.java index f9a82ce1de..fe0d41b568 100644 --- a/src/main/mondrian/rolap/RolapNativeSet.java +++ b/src/main/mondrian/rolap/RolapNativeSet.java @@ -157,6 +157,7 @@ protected class SetEvaluator implements NativeEvaluator { private final SchemaReaderWithMemberReaderAvailable schemaReader; private final TupleConstraint constraint; private int maxRows = 0; + private boolean nonEmpty = false; public SetEvaluator( CrossJoinArg[] args, @@ -185,12 +186,11 @@ public Object execute(ResultStyle desiredResultStyle) { new HighCardSqlTupleReader(constraint)); } // Use the regular tuple reader. - return executeList( - new SqlTupleReader(constraint)); + return executeList(constraint); } case MUTABLE_LIST: case LIST: - return executeList(new SqlTupleReader(constraint)); + return executeList(constraint); default: throw ResultStyleException.generate( ResultStyle.ITERABLE_MUTABLELIST_LIST, @@ -198,6 +198,10 @@ public Object execute(ResultStyle desiredResultStyle) { } } + protected TupleList executeList(TupleConstraint constraint) { + return executeList(new SqlTupleReader(constraint)); + } + protected TupleList executeList(final SqlTupleReader tr) { tr.setMaxRows(maxRows); for (CrossJoinArg arg : args) { @@ -219,6 +223,7 @@ protected TupleList executeList(final SqlTupleReader tr) { key.add(tr.getCacheKey()); key.addAll(Arrays.asList(args)); key.add(maxRows); + key.add(nonEmpty); TupleList result = cache.get(key); boolean hasEnumTargets = (tr.getEnumTargetCount() > 0); @@ -389,6 +394,14 @@ int getMaxRows() { void setMaxRows(int maxRows) { this.maxRows = maxRows; } + + public boolean isNonEmpty() { + return nonEmpty; + } + + public void setNonEmpty(boolean nonEmpty) { + this.nonEmpty = nonEmpty; + } } /** diff --git a/src/main/mondrian/rolap/RolapNativeTopCount.java b/src/main/mondrian/rolap/RolapNativeTopCount.java index b4dc94bd84..dc41ec0007 100644 --- a/src/main/mondrian/rolap/RolapNativeTopCount.java +++ b/src/main/mondrian/rolap/RolapNativeTopCount.java @@ -11,16 +11,24 @@ */ package mondrian.rolap; +import mondrian.calc.TupleList; import mondrian.mdx.MemberExpr; import mondrian.olap.*; import mondrian.rolap.aggmatcher.AggStar; import mondrian.rolap.sql.*; +import mondrian.rolap.sql.query.*; +import mondrian.spi.Dialect; +import mondrian.spi.DialectManager; +import mondrian.util.Pair; import java.util.ArrayList; import java.util.List; import javax.sql.DataSource; +import static mondrian.rolap.sql.query.RightJoinBuilder.BASE_QUERY_ALIAS; +import static mondrian.rolap.sql.query.RightJoinBuilder.JOIN_QUERY_ALIAS; + /** * Computes a TopCount in SQL. * @@ -31,7 +39,7 @@ public class RolapNativeTopCount extends RolapNativeSet { public RolapNativeTopCount() { super.setEnabled( - MondrianProperties.instance().EnableNativeTopCount.get()); + MondrianProperties.instance().EnableNativeTopCount.get()); } static class TopCountConstraint extends SetConstraint { @@ -47,7 +55,7 @@ public TopCountConstraint( super(args, evaluator, true); this.orderByExpr = orderByExpr; this.ascending = ascending; - this.topCount = new Integer(count); + this.topCount = count; } /** @@ -127,11 +135,10 @@ NativeEvaluator createEvaluator( FunDef fun, Exp[] args) { - boolean ascending; - if (!isEnabled()) { return null; } + if (!TopCountConstraint.isValidContext( evaluator, restrictMemberTypes())) { @@ -140,6 +147,7 @@ evaluator, restrictMemberTypes())) // is this "TopCount(, , [])" String funName = fun.getName(); + final boolean ascending; if ("TopCount".equalsIgnoreCase(funName)) { ascending = false; } else if ("BottomCount".equalsIgnoreCase(funName)) { @@ -216,14 +224,107 @@ evaluator, restrictMemberTypes())) TupleConstraint constraint = new TopCountConstraint( count, combinedArgs, evaluator, orderByExpr, ascending); - SetEvaluator sev = - new SetEvaluator(cjArgs, schemaReader, constraint); + + SetEvaluator sev; + if (evaluator.isNonEmpty() && args.length == 3) { + sev = new SetEvaluator(cjArgs, schemaReader, constraint); + } else { + sev = new NativeTopCountSetEvaluator(cjArgs, schemaReader, constraint); + } sev.setMaxRows(count); + sev.setNonEmpty(evaluator.isNonEmpty()); return sev; } finally { evaluator.restore(savepoint); } } + + private class NativeTopCountSetEvaluator extends SetEvaluator { + public NativeTopCountSetEvaluator(CrossJoinArg[] args, SchemaReader schemaReader, TupleConstraint constraint) { + super(args, schemaReader, constraint); + } + + @Override + protected TupleList executeList(TupleConstraint constraint) { + return executeList(new TopCountSqlTupleReader(constraint)); + } + } + + private static class TopCountSqlTupleReader extends SqlTupleReader { + public TopCountSqlTupleReader(TupleConstraint constraint) { + super(constraint); + } + + @Override + protected Pair> generateSelectForLevels(DataSource dataSource, RolapCube baseCube, WhichSelect whichSelect) { + SqlQueryBuilder subselect = new SqlQueryBuilder(DialectManager.createDialect(dataSource, null)); + subselect.setAllowHints(true); + super.generateSelectForLevels(baseCube, whichSelect, subselect, constraint); + + Evaluator evaluator = getEvaluator(constraint); + AggStar aggStar = chooseAggStar(constraint, evaluator, baseCube); + + CrossJoinBuilder builder = CrossJoinBuilder.builder(subselect.getDialect()); + + for (TargetBase target : targets) { + if (target.getSrcMembers() == null) { + SqlQueryBuilder crossJoinQuery = new SqlQueryBuilder(subselect.getDialect()); + addLevelMemberSql( + crossJoinQuery, + target.getLevel(), + baseCube, + whichSelect, + aggStar, + DefaultTupleConstraint.instance()); + builder.append(crossJoinQuery); + } + } + + SqlSelect crossJoin = builder.build(); + if (crossJoin == null) { + return subselect.toSqlAndTypes(); + } + + SqlSelect result = RightJoinBuilder.builder(subselect.getDialect()) + .query(subselect.toSelect()) + .rightJoin(crossJoin) + .build( + new RightJoinBuilder.Populator() { + @Override + public void populateOutput(SqlQuery query, SqlSelect baseQuery, SqlSelect joinQuery) { + SqlUtils.addSelectFields(query, joinQuery.getOutputFields(), JOIN_QUERY_ALIAS); + int populated = joinQuery.getOutputFields().size(); + int subQuerySize = baseQuery.getOutputFields().size(); + if (populated < subQuerySize) { + List restFromBase = baseQuery.getOutputFields().subList(populated, subQuerySize); + SqlUtils.addSelectFields(query, restFromBase, BASE_QUERY_ALIAS); + } + } + }, + new RightJoinBuilder.Mapper() { + @Override + public String map(List baseQueryOutput, List joinQueryOutput, Dialect dialect) { + StringBuilder sb = new StringBuilder(); + addEqualsCondition(sb, baseQueryOutput.get(0), joinQueryOutput.get(0), dialect); + for (int i = 1, len = joinQueryOutput.size(); i < len; i++) { + sb.append(" and "); + addEqualsCondition(sb, baseQueryOutput.get(i), joinQueryOutput.get(i), dialect); + } + return sb.toString(); + } + + private void addEqualsCondition(StringBuilder sb, SelectElement baseQueryOutput, SelectElement joinQueryOutput, Dialect dialect) { + sb.append('(') + .append(dialect.quoteIdentifier(BASE_QUERY_ALIAS, baseQueryOutput.getAlias())) + .append('=') + .append(dialect.quoteIdentifier(JOIN_QUERY_ALIAS, joinQueryOutput.getAlias())) + .append(')'); + } + }); + + return Pair.of(result.getSql(), result.getOutputTypes()); + } + } } // End RolapNativeTopCount.java diff --git a/src/main/mondrian/rolap/SqlTupleReader.java b/src/main/mondrian/rolap/SqlTupleReader.java index 49f1f8fcc0..1fbc07fc5b 100644 --- a/src/main/mondrian/rolap/SqlTupleReader.java +++ b/src/main/mondrian/rolap/SqlTupleReader.java @@ -904,19 +904,28 @@ Pair> sqlForEmptyTuple( * @param whichSelect Position of this select statement in a union * @return SQL statement string and types */ - Pair> generateSelectForLevels( + protected Pair> generateSelectForLevels( DataSource dataSource, RolapCube baseCube, WhichSelect whichSelect) { String s = - "while generating query to retrieve members of level(s) " + targets; + "while generating query to retrieve members of level(s) " + targets; // Allow query to use optimization hints from the table definition SqlQuery sqlQuery = SqlQuery.newQuery(dataSource, s); sqlQuery.setAllowHints(true); + generateSelectForLevels(baseCube, whichSelect, sqlQuery, constraint); + return sqlQuery.toSqlAndTypes(); + } + protected void generateSelectForLevels( + RolapCube baseCube, + WhichSelect whichSelect, + SqlQuery sqlQuery, + TupleConstraint constraint) + { Evaluator evaluator = getEvaluator(constraint); AggStar aggStar = chooseAggStar(constraint, evaluator, baseCube); @@ -930,13 +939,11 @@ Pair> generateSelectForLevels( target.getLevel(), baseCube, whichSelect, - aggStar); + aggStar, + constraint); } } - constraint.addConstraint(sqlQuery, baseCube, aggStar); - - return sqlQuery.toSqlAndTypes(); } boolean targetIsOnBaseCube(TargetBase target, RolapCube baseCube) { @@ -1039,7 +1046,17 @@ protected void addLevelMemberSql( RolapLevel level, RolapCube baseCube, WhichSelect whichSelect, - AggStar aggStar) + AggStar aggStar) { + addLevelMemberSql(sqlQuery, level, baseCube, whichSelect, aggStar, constraint); + } + + protected void addLevelMemberSql( + SqlQuery sqlQuery, + RolapLevel level, + RolapCube baseCube, + WhichSelect whichSelect, + AggStar aggStar, + TupleConstraint constraint) { RolapHierarchy hierarchy = level.getHierarchy(); diff --git a/src/main/mondrian/rolap/sql/SqlQuery.java b/src/main/mondrian/rolap/sql/SqlQuery.java index 55ad8654c7..171e381e90 100644 --- a/src/main/mondrian/rolap/sql/SqlQuery.java +++ b/src/main/mondrian/rolap/sql/SqlQuery.java @@ -76,12 +76,12 @@ public class SqlQuery { private boolean distinct; - private final ClauseList select; - private final FromClauseList from; - private final ClauseList where; - private final ClauseList groupBy; - private final ClauseList having; - private final ClauseList orderBy; + protected final ClauseList select; + protected final FromClauseList from; + protected final ClauseList where; + protected final ClauseList groupBy; + protected final ClauseList having; + protected final ClauseList orderBy; private final List groupingSets; private final ClauseList groupingFunctions; @@ -101,7 +101,7 @@ public class SqlQuery { private final List fromAliases; /** The SQL dialect this query is to be generated in. */ - private final Dialect dialect; + protected final Dialect dialect; /** Scratch buffer. Clear it before use. */ private final StringBuilder buf; @@ -117,7 +117,7 @@ public class SqlQuery { mapRootToRelations = new HashMap>(); - private final Map columnAliases = + protected final Map columnAliases = new HashMap(); private static final String INDENT = " "; @@ -785,6 +785,10 @@ private void flatten( } } + public boolean isGenerateFormattedSql() { + return generateFormattedSql; + } + private static class JoinOnClause { private final String condition; private final String left; @@ -881,7 +885,7 @@ void appendJoin( } } - static class ClauseList extends ArrayList { + protected static class ClauseList extends ArrayList { protected final boolean allowDups; ClauseList(final boolean allowDups) { diff --git a/src/main/mondrian/rolap/sql/query/AliasGenerator.java b/src/main/mondrian/rolap/sql/query/AliasGenerator.java new file mode 100644 index 0000000000..03431a7524 --- /dev/null +++ b/src/main/mondrian/rolap/sql/query/AliasGenerator.java @@ -0,0 +1,24 @@ +package mondrian.rolap.sql.query; + +/** + * @author Andrey Khayrutdinov + */ +// todo Khayrutdinov: docs +class AliasGenerator { + private final StringBuilder sb; + private final int seedLength; + private int counter; + + AliasGenerator(String seed) { + this.seedLength = seed.length(); + this.counter = 0; + + sb = new StringBuilder(seedLength + 2); + sb.append(seed); + } + + String nextAlias() { + sb.setLength(seedLength); + return sb.append(counter++).toString(); + } +} diff --git a/src/main/mondrian/rolap/sql/query/CrossJoinBuilder.java b/src/main/mondrian/rolap/sql/query/CrossJoinBuilder.java new file mode 100644 index 0000000000..245ec5e56a --- /dev/null +++ b/src/main/mondrian/rolap/sql/query/CrossJoinBuilder.java @@ -0,0 +1,94 @@ +package mondrian.rolap.sql.query; + +import mondrian.olap.Util; +import mondrian.rolap.sql.SqlQuery; +import mondrian.spi.Dialect; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Andrey Khayrutdinov + */ +public class CrossJoinBuilder { + + public static CrossJoinBuilder builder(Dialect dialect) { + return new CrossJoinBuilder(dialect); + } + + + private final List queries; + private final Dialect dialect; + + public CrossJoinBuilder(Dialect dialect) { + this.queries = new ArrayList(8); + this.dialect = dialect; + } + + public CrossJoinBuilder append(SqlQueryBuilder sqlQuery) { + return append(sqlQuery.toSelect()); + } + + public CrossJoinBuilder append(SqlSelect select) { + queries.add(select); + return this; + } + + public SqlSelect build() { + int size = queries.size(); + if (size == 0) { + return null; + } + + if (size == 1) { + return queries.get(0); + } + + AliasGenerator aliasGenerator = new AliasGenerator("t"); + SqlQueryBuilder query = new SqlQueryBuilder(dialect); + + SqlSelect select = queries.get(0); + String alias = aliasGenerator.nextAlias(); + query.addFromQuery(select.getSql(), alias, true); + + SqlUtils.addSelectFields(query, select.getOutputFields(), alias); + + StringBuilder sb = new StringBuilder(); + for (int i = 1, len = queries.size(); i < len; i++) { + select = queries.get(i); + alias = aliasGenerator.nextAlias(); + SqlUtils.addSelectFields(query, select.getOutputFields(), alias); + appendCrossJoin(sb, query, select.getSql(), alias); + } + + SqlSelect selectQuery = query.toSelect(); + + String crossJoins = sb.toString(); + String selectQuerySql = selectQuery.getSql(); + + String result = + new StringBuilder(selectQuerySql.length() + crossJoins.length()) + .append(selectQuerySql) + .append(crossJoins) + .toString(); + + return StringSelect.of(result) + .returning(selectQuery.getOutputFields()) + .build(); + } + + private void appendCrossJoin(StringBuilder sb, SqlQuery query, String sql, String tableAlias) { + sb.append(Util.nl).append("cross join ("); + + if (query.isGenerateFormattedSql()) { + sb.append(Util.nl).append(sql).append(Util.nl).append(')'); + } else { + sb.append(sql).append(')'); + } + + if (dialect.allowsAs()) { + sb.append(" as"); + } + sb.append(' ').append(dialect.quoteIdentifier(tableAlias)); + } +} diff --git a/src/main/mondrian/rolap/sql/query/OrderElement.java b/src/main/mondrian/rolap/sql/query/OrderElement.java new file mode 100644 index 0000000000..a73a01f5cd --- /dev/null +++ b/src/main/mondrian/rolap/sql/query/OrderElement.java @@ -0,0 +1,34 @@ +package mondrian.rolap.sql.query; + +/** + * @author Andrey Khayrutdinov + */ +public class OrderElement { + private final String expression; + private final String alias; + private final boolean ascending; + private final boolean nullable; + + public OrderElement(String expression, String alias, boolean ascending, boolean nullable) { + this.expression = expression; + this.alias = alias; + this.ascending = ascending; + this.nullable = nullable; + } + + public String getExpression() { + return expression; + } + + public String getAlias() { + return alias; + } + + public boolean isAscending() { + return ascending; + } + + public boolean isNullable() { + return nullable; + } +} diff --git a/src/main/mondrian/rolap/sql/query/RightJoinBuilder.java b/src/main/mondrian/rolap/sql/query/RightJoinBuilder.java new file mode 100644 index 0000000000..8ed6f34dc8 --- /dev/null +++ b/src/main/mondrian/rolap/sql/query/RightJoinBuilder.java @@ -0,0 +1,108 @@ +package mondrian.rolap.sql.query; + +import mondrian.olap.Util; +import mondrian.rolap.sql.SqlQuery; +import mondrian.spi.Dialect; +import java.util.List; + +/** + * @author Andrey Khayrutdinov + */ +public class RightJoinBuilder { + + public static final String BASE_QUERY_ALIAS = "subsel"; + public static final String JOIN_QUERY_ALIAS = "jndq"; + + public static RightJoinBuilder builder(Dialect dialect) { + return new RightJoinBuilder(dialect); + } + + private final Dialect dialect; + private SqlSelect baseQuery; + private SqlSelect joinQuery; + + public RightJoinBuilder(Dialect dialect) { + this.dialect = dialect; + } + + public RightJoinBuilder query(SqlSelect baseQuery) { + this.baseQuery = baseQuery; + return this; + } + + public RightJoinBuilder rightJoin(SqlSelect joinQuery) { + this.joinQuery = joinQuery; + return this; + } + + public SqlSelect build(Populator populator, Mapper mapper) { + SqlQueryBuilder queryBuilder = new SqlQueryBuilder(dialect); + populator.populateOutput(queryBuilder, baseQuery, joinQuery); + + queryBuilder.addFromQuery(baseQuery.getSql(), BASE_QUERY_ALIAS, true); + + SqlSelect select = queryBuilder.toSelect(); + + String selectSql = select.getSql(); + String joinQuerySql = joinQuery.getSql(); + + StringBuilder sb = new StringBuilder(selectSql.length() * 2 + joinQuerySql.length()); + sb.append(selectSql); + sb.append(Util.nl).append("right join (").append(joinQuerySql).append(") "); + if (dialect.allowsAs()) { + sb.append("as "); + } + sb.append(dialect.quoteIdentifier(JOIN_QUERY_ALIAS)); + sb.append(" on ").append(Util.nl).append(mapper.map(baseQuery.getOutputFields(), joinQuery.getOutputFields(), dialect)); + + // todo technical debt here + List orderByStatements = null; + + if (!baseQuery.getOrderElements().isEmpty()) { + List baseQueryOutputFields = baseQuery.getOutputFields(); + int joinQueryOutputSize = joinQuery.getOutputFields().size(); + + SqlQuery sqlGenerator = new SqlQuery(dialect, false); + for (OrderElement element : baseQuery.getOrderElements()) { + String table; + int index = findExpression(baseQueryOutputFields, element.getExpression()); + if (index != -1 && index < joinQueryOutputSize) { + table = JOIN_QUERY_ALIAS; + } else { + table = BASE_QUERY_ALIAS; + } + String quoted = dialect.quoteIdentifier(table, element.getAlias()); + sqlGenerator.addOrderBy(quoted, element.isAscending(), false, element.isNullable()); + } + + String generated = sqlGenerator.toString(); + int index = generated.indexOf("order by"); + if (index != -1) { + sb.append(Util.nl).append(generated.substring(index)); + } + } + + return StringSelect.of(sb.toString()) + .returning(select.getOutputFields()) + .orderedBy(orderByStatements) + .build(); + } + + private int findExpression(List elements, String expression) { + for (int i = 0; i < elements.size(); i++) { + SelectElement element = elements.get(i); + if (expression.equals(element.getExpression()) || expression.equals(element.getAlias())) { + return i; + } + } + return -1; + } + + public interface Populator { + void populateOutput(SqlQuery query, SqlSelect baseQuery, SqlSelect joinQuery); + } + + public interface Mapper { + String map(List baseQueryOutput, List joinQueryOutput, Dialect dialect); + } +} diff --git a/src/main/mondrian/rolap/sql/query/SelectElement.java b/src/main/mondrian/rolap/sql/query/SelectElement.java new file mode 100644 index 0000000000..d806990866 --- /dev/null +++ b/src/main/mondrian/rolap/sql/query/SelectElement.java @@ -0,0 +1,30 @@ +package mondrian.rolap.sql.query; + +import mondrian.rolap.SqlStatement; + +/** + * @author Andrey Khayrutdinov + */ +public class SelectElement { + private final String expression; + private final String alias; + private final SqlStatement.Type type; + + public SelectElement(String expression, String alias, SqlStatement.Type type) { + this.expression = expression; + this.alias = (alias == null) ? expression : alias; + this.type = type; + } + + public String getExpression() { + return expression; + } + + public String getAlias() { + return alias; + } + + public SqlStatement.Type getType() { + return type; + } +} diff --git a/src/main/mondrian/rolap/sql/query/SqlQueryBuilder.java b/src/main/mondrian/rolap/sql/query/SqlQueryBuilder.java new file mode 100644 index 0000000000..63b60446de --- /dev/null +++ b/src/main/mondrian/rolap/sql/query/SqlQueryBuilder.java @@ -0,0 +1,51 @@ +package mondrian.rolap.sql.query; + +import mondrian.rolap.SqlStatement; +import mondrian.rolap.sql.SqlQuery; +import mondrian.spi.Dialect; +import mondrian.util.Pair; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Andrey Khayrutdinov + */ +public class SqlQueryBuilder extends SqlQuery { + + private final List outputFields = new ArrayList(); + private final List orderByExpr = new ArrayList(); + + public SqlQueryBuilder(Dialect dialect) { + super(dialect); + } + + @Override + public String addSelect(String expression, SqlStatement.Type type, String alias) { + if (alias == null) { + outputFields.add(new SelectElement(expression, null, type)); + } else { + outputFields.add(new SelectElement(expression, dialect.quoteIdentifier(alias), type)); + } + return super.addSelect(expression, type, alias); + } + + @Override + public void addOrderBy(String expr, String alias, boolean ascending, boolean prepend, boolean nullable, boolean collateNullsLast) { + super.addOrderBy(expr, alias, ascending, prepend, nullable, collateNullsLast); + String matchingOutputAlias = columnAliases.get(expr); + if (matchingOutputAlias != null) { + OrderElement element = new OrderElement(expr, matchingOutputAlias, ascending, nullable); + if (prepend) { + orderByExpr.add(0, element); + } else { + orderByExpr.add(element); + } + } + } + + public SqlSelect toSelect() { + Pair> pair = toSqlAndTypes(); + return StringSelect.of(pair.left).returning(outputFields).orderedBy(orderByExpr).build(); + } +} diff --git a/src/main/mondrian/rolap/sql/query/SqlSelect.java b/src/main/mondrian/rolap/sql/query/SqlSelect.java new file mode 100644 index 0000000000..4789342a46 --- /dev/null +++ b/src/main/mondrian/rolap/sql/query/SqlSelect.java @@ -0,0 +1,19 @@ +package mondrian.rolap.sql.query; + +import mondrian.rolap.SqlStatement; + +import java.util.List; + +/** + * @author Andrey Khayrutdinov + */ +// todo Khayrutdinov : java docs +public interface SqlSelect { + + List getOutputFields(); + List getOutputTypes(); + + List getOrderElements(); + + String getSql(); +} diff --git a/src/main/mondrian/rolap/sql/query/SqlUtils.java b/src/main/mondrian/rolap/sql/query/SqlUtils.java new file mode 100644 index 0000000000..5a01badd40 --- /dev/null +++ b/src/main/mondrian/rolap/sql/query/SqlUtils.java @@ -0,0 +1,28 @@ +package mondrian.rolap.sql.query; + +import mondrian.rolap.SqlStatement; +import mondrian.rolap.sql.SqlQuery; +import mondrian.spi.Dialect; + +import java.util.List; + +/** + * @author Andrey Khayrutdinov + */ +public class SqlUtils { + + public static void addSelectFields(SqlQuery query, List fields, String tableAlias) { + StringBuilder sb = new StringBuilder(); + Dialect dialect = query.getDialect(); + String[] pair = new String[]{tableAlias, null}; + for (SelectElement field : fields) { + sb.setLength(0); + pair[1] = field.getAlias(); + dialect.quoteIdentifier(sb, pair); + String expression = sb.toString(); + + SqlStatement.Type type = field.getType(); + query.addSelect(expression, type); + } + } +} diff --git a/src/main/mondrian/rolap/sql/query/StringSelect.java b/src/main/mondrian/rolap/sql/query/StringSelect.java new file mode 100644 index 0000000000..e959f07f49 --- /dev/null +++ b/src/main/mondrian/rolap/sql/query/StringSelect.java @@ -0,0 +1,86 @@ +package mondrian.rolap.sql.query; + +import mondrian.rolap.SqlStatement; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static java.util.Collections.unmodifiableList; + +/** + * @author Andrey Khayrutdinov + */ +public class StringSelect implements SqlSelect { + + private final String sql; + private final List outputFields; + private final List orderByElements; + + StringSelect(String sql, List outputFields, List orderByElements) { + this.sql = sql; + this.outputFields = outputFields; + this.orderByElements = orderByElements; + } + + @Override + public List getOutputFields() { + return outputFields; + } + + @Override + public List getOutputTypes() { + List types = new ArrayList(outputFields.size()); + for (SelectElement field : outputFields) { + types.add(field.getType()); + } + return types; + } + + @Override + public List getOrderElements() { + return orderByElements; + } + + @Override + public String getSql() { + return sql; + } + + + public static Builder of(String sql) { + return new Builder(sql); + } + + public static class Builder { + private String sql; + private List outputFields; + private List orderElements; + + public Builder(String sql) { + this.sql = sql; + } + + public Builder returning(List fields) { + this.outputFields = fields; + return this; + } + + public Builder orderedBy(List orderByStatements) { + this.orderElements = orderByStatements; + return this; + } + + public StringSelect build() { + return new StringSelect(sql, protectList(outputFields), protectList(orderElements)); + } + + private List protectList(List list) { + if (list == null || list.isEmpty()) { + return Collections.emptyList(); + } else { + return unmodifiableList(new ArrayList(list)); + } + } + } +} diff --git a/testsrc/mondrian/rolap/sql/query/AliasGeneratorTest.java b/testsrc/mondrian/rolap/sql/query/AliasGeneratorTest.java new file mode 100644 index 0000000000..59d26887be --- /dev/null +++ b/testsrc/mondrian/rolap/sql/query/AliasGeneratorTest.java @@ -0,0 +1,26 @@ +package mondrian.rolap.sql.query; + +import junit.framework.TestCase; + +/** + * @author Andrey Khayrutdinov + */ +// todo Khayrutdinov: include into Main +public class AliasGeneratorTest extends TestCase { + + public void testNextAlias_OneCharSeed() { + AliasGenerator generator = new AliasGenerator("a"); + for (int i = 0; i < 10; i++) { + String expected = "a" + i; + assertEquals(expected, generator.nextAlias()); + } + } + + public void testNextAlias_EmptySeed() { + AliasGenerator generator = new AliasGenerator(""); + for (int i = 0; i < 10; i++) { + String expected = Integer.toString(i); + assertEquals(expected, generator.nextAlias()); + } + } +} \ No newline at end of file diff --git a/testsrc/mondrian/rolap/sql/query/CrossJoinBuilderTest.java b/testsrc/mondrian/rolap/sql/query/CrossJoinBuilderTest.java new file mode 100644 index 0000000000..84b6789503 --- /dev/null +++ b/testsrc/mondrian/rolap/sql/query/CrossJoinBuilderTest.java @@ -0,0 +1,154 @@ +package mondrian.rolap.sql.query; + +import mondrian.rolap.SqlStatement; +import mondrian.spi.Dialect; +import mondrian.spi.DialectManager; +import mondrian.test.FoodMartTestCase; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static mondrian.rolap.SqlStatement.Type.DOUBLE; +import static mondrian.rolap.SqlStatement.Type.INT; + +/** + * @author Andrey Khayrutdinov + */ +// todo Khayrutdinov: Main.java +public class CrossJoinBuilderTest extends FoodMartTestCase { + + private CrossJoinBuilder builder; + + @Override + public void setUp() throws Exception { + super.setUp(); + DataSource ds = getTestContext().getConnection().getDataSource(); + Dialect dialect = DialectManager.createDialect(ds, null); + builder = CrossJoinBuilder.builder(dialect); + } + + public void testNoQueries() { + assertNull(builder.build()); + } + + private StringSelect select(String sql, List fields, List types) { + List elements = new ArrayList(fields.size()); + for (int i = 0; i < fields.size(); i++) { + String field = fields.get(i); + elements.add(new SelectElement(null, field, types.get(i))); + } + return StringSelect.of(sql).returning(elements).build(); + } + + public void testOneQuery() { + StringSelect select = select( + "select `col` from `tbl`", + singletonList("`col`"), singletonList(INT)); + SqlSelect crossJoin = builder.append(select).build(); + assertQuery(crossJoin, select.getSql(), singletonList("`col`"), singletonList(INT)); + } + + public void testTwoQueries() { + StringSelect subQuery1 = select( + "select " + + "`a`.`a` as `c0`, " + + "`a`.`b` as `c1` " + + "from `a`", + asList("`c0`", "`c1`"), + asList(INT, null)); + StringSelect subQuery2 = select( + "select " + + "`b`.`a` as `c0`, " + + "`b`.`b` as `c1` " + + "from `b`", + asList("`c0`", "`c1`"), + asList(DOUBLE, null)); + + SqlSelect crossJoin = builder + .append(subQuery1) + .append(subQuery2) + .build(); + + String expectedSql = "" + + "select " + + "`t0`.`c0` as `c0`, " + + "`t0`.`c1` as `c1`, " + + "`t1`.`c0` as `c2`, " + + "`t1`.`c1` as `c3` " + + "from (" + subQuery1.getSql() + ") as `t0` " + + "cross join (" + subQuery2.getSql() + ") as `t1`"; + + assertQuery(crossJoin, expectedSql, asList("`c0`", "`c1`", "`c2`", "`c3`"), asList(INT, null, DOUBLE, null)); + } + + public void testThreeQueries() { + List nullTypes = singletonList(null); + + String sql1 = "select a from a"; + SqlSelect select1 = select(sql1, singletonList("a"), nullTypes); + + String sql2 = "select b from b"; + SqlSelect select2 = select(sql2, singletonList("b"), nullTypes); + + String sql3 = "select c from c"; + SqlSelect select3 = select(sql3, singletonList("c"), nullTypes); + + SqlSelect crossJoin = builder + .append(select1) + .append(select2) + .append(select3) + .build(); + + String expectedSql = "" + + "select " + + "`t0`.`a` as `c0`, " + + "`t1`.`b` as `c1`, " + + "`t2`.`c` as `c2` " + + "from (" + sql1 + ") as `t0` " + + "cross join (" + sql2 + ") as `t1` " + + "cross join (" + sql3 + ") as `t2`"; + + assertQuery(crossJoin, expectedSql, asList("`c0`", "`c1`", "`c2`"), Arrays.asList(null, null, null) ); + } + + + + private void assertQuery(SqlSelect crossJoin, String expectedSql, List expectedOutput, List types) { + String crossJoinSql = stripWhiteSpace(crossJoin.getSql()); + expectedSql = stripWhiteSpace(expectedSql); + assertEquals(expectedSql, crossJoinSql); + + assertListsAreEqual(expectedOutput, extractElementsAliases(crossJoin.getOutputFields())); + + assertListsAreEqual(types, crossJoin.getOutputTypes()); + } + + private List extractElementsAliases(List elements) { + List result = new ArrayList(elements.size()); + for (SelectElement element : elements) { + result.add(element.getAlias()); + } + return result; + } + + private String stripWhiteSpace(String string) { + string = string.replaceAll("\n", " ").replaceAll("\r", " "); + while (string.contains(" ")) { + string = string.replaceAll(" ", " "); + } + return string; + } + + private void assertListsAreEqual(List expected, List actual) { + assertEquals(expected.size(), actual.size()); + for (int i = 0; i < expected.size(); i++) { + T e = expected.get(i); + T a = actual.get(i); + assertEquals(Integer.toString(i), e, a); + } + } +} \ No newline at end of file