package com.getbase.android.db.fluentsqlite; import com.getbase.android.db.fluentsqlite.Query.QueryBuilder; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.AbstractIterator; import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; public final class Expressions { public static final Function<Query, Iterable<String>> GET_TABLES = new Function<Query, Iterable<String>>() { @Override public Iterable<String> apply(Query subquery) { return subquery.getTables(); } }; private Expressions() { } static void addExpressionArgs(List<Object> args, Expression expression, Object... boundArgs) { if (boundArgs == null) { Preconditions.checkArgument( expression.getArgsCount() == expression.getBoundArgs().size(), "Expression contains args placeholders, but bound args list is null" ); args.addAll(expression.getBoundArgs().values()); } else { Preconditions.checkArgument( expression.getArgsCount() == boundArgs.length + expression.getBoundArgs().size(), "Invalid number of arguments: expression has %s arg placeholders and %s bound args, so I need %s additional args specified, but there was %s args", expression.getArgsCount(), expression.getBoundArgs().size(), (expression.getArgsCount() - expression.getBoundArgs().size()), boundArgs.length ); int boundArgsIndex = 0; for (int i = 0; i < expression.getArgsCount(); i++) { final Object arg; if (expression.getBoundArgs().containsKey(i)) { arg = expression.getBoundArgs().get(i); } else { arg = boundArgs[boundArgsIndex++]; } args.add(arg); } } } public interface UnaryOperator { ExpressionCore not(); } public interface UnaryPostfixOperator { ExpressionCombiner collate(CollatingSequence collatingSequence); } public enum CollatingSequence { BINARY, NOCASE, RTRIM, UNICODE, LOCALIZED } public static abstract class Expression { Expression() { } public String toRawSql() { Preconditions.checkState(getBoundArgs().isEmpty(), "Cannot get raw sql for Expression with bound args."); return getSql(); } abstract String getSql(); abstract int getArgsCount(); abstract Map<Integer, Object> getBoundArgs(); abstract Set<String> getTables(); @SuppressWarnings("unchecked") abstract <T> Object[] getMergedArgs(T... boundArgs); } public interface ExpressionCore { // basic stuff ExpressionCombiner column(String col); ExpressionCombiner column(String table, String col); ExpressionCombiner arg(); ExpressionCombiner nul(); ExpressionCombiner literal(Number number); ExpressionCombiner literal(Object object); // aggregate functions ExpressionCombiner sum(Expression e); ExpressionCombiner count(Expression e); ExpressionCombiner count(); ExpressionCombiner max(Expression e); ExpressionCombiner min(Expression e); // coalescing functions ExpressionCombiner ifNull(Expression left, Expression right); ExpressionCombiner nullIf(Expression left, Expression right); ExpressionCombiner coalesce(Expression... expressions); // strings operations ExpressionCombiner length(Expression e); ExpressionCombiner concat(Expression... e); ExpressionCombiner join(String on, Expression... e); // generic expression ExpressionCombiner expr(String expression); ExpressionCombiner expr(Expression expression); } public interface CaseExpressions { CaseCondition cases(); CaseCondition cases(Expression e); } public interface CaseCondition { CaseValue when(Expression e); } public interface CaseValue { CaseExpressionBuilder then(Expression e); } public interface CaseExpressionBuilder extends CaseCondition, CaseExpressionEndStep { ExpressionCombiner otherwise(Expression e); } public interface CaseExpressionEndStep { ExpressionCombiner end(); } public interface BinaryOperator { ExpressionBuilder eq(); ExpressionCombiner eq(Expression e); ExpressionBuilder ne(); ExpressionCombiner ne(Expression e); ExpressionBuilder gt(); ExpressionCombiner gt(Expression e); ExpressionBuilder ge(); ExpressionCombiner ge(Expression e); ExpressionBuilder lt(); ExpressionCombiner lt(Expression e); ExpressionBuilder le(); ExpressionCombiner le(Expression e); ExpressionBuilder is(); ExpressionCombiner is(Expression e); ExpressionCombiner in(Query subquery); ExpressionCombiner in(QueryBuilder subqueryBuilder); ExpressionCombiner in(Expression... e); ExpressionCombiner notIn(Query subquery); ExpressionCombiner notIn(QueryBuilder subqueryBuilder); ExpressionCombiner notIn(Expression... e); ExpressionBuilder or(); ExpressionCombiner or(Expression e); ExpressionBuilder and(); ExpressionCombiner and(Expression e); } public interface ExpressionBuilder extends UnaryOperator, ExpressionCore, CaseExpressions { } public static abstract class ExpressionCombiner extends Expression implements BinaryOperator, UnaryPostfixOperator { } // mirror all method from ExpressionBuilder interface public static ExpressionCore not() { return new Builder().not(); } public static ExpressionCombiner column(String col) { return new Builder().column(col); } public static ExpressionCombiner column(String table, String col) { return new Builder().column(table, col); } public static ExpressionCombiner arg() { return new Builder().arg(); } public static ExpressionCombiner nul() { return new Builder().nul(); } public static ExpressionCombiner literal(Number number) { return new Builder().literal(number); } public static ExpressionCombiner literal(Object object) { return new Builder().literal(object); } @SafeVarargs public static <T> Expression[] literals(T... objects) { Preconditions.checkNotNull(objects); Expression[] result = new Expression[objects.length]; for (int i = 0; i < objects.length; i++) { result[i] = literal(objects[i]); } return result; } public static Expression[] literals(Number... numbers) { Preconditions.checkNotNull(numbers); Expression[] result = new Expression[numbers.length]; for (int i = 0; i < numbers.length; i++) { result[i] = literal(numbers[i]); } return result; } public static ExpressionCombiner sum(Expression e) { return new Builder().sum(e); } public static ExpressionCombiner count(Expression e) { return new Builder().count(e); } public static ExpressionCombiner count() { return new Builder().count(); } public static ExpressionCombiner max(Expression e) { return new Builder().max(e); } public static ExpressionCombiner min(Expression e) { return new Builder().min(e); } public static ExpressionCombiner ifNull(Expression left, Expression right) { return new Builder().ifNull(left, right); } public static ExpressionCombiner nullIf(Expression left, Expression right) { return new Builder().nullIf(left, right); } public static ExpressionCombiner coalesce(Expression... expressions) { return new Builder().coalesce(expressions); } public static ExpressionCombiner length(Expression e) { return new Builder().length(e); } public static ExpressionCombiner concat(Expression... e) { return new Builder().concat(e); } public static ExpressionCombiner expr(String expression) { return new Builder().expr(expression); } public static ExpressionCombiner expr(Expression expression) { return new Builder().expr(expression); } public static ExpressionCombiner join(String on, Expression... e) { return new Builder().join(on, e); } public static CaseCondition cases() { return new Builder().cases(); } public static CaseCondition cases(Expression e) { return new Builder().cases(e); } private static class Builder extends ExpressionCombiner implements ExpressionBuilder, CaseExpressionBuilder, CaseValue { private StringBuilder mBuilder = new StringBuilder(); private Map<Integer, Object> mArgs = Maps.newHashMap(); private List<Query> mSubqueries = Lists.newArrayList(); private int mArgsCount; private static final Joiner ARGS_JOINER = Joiner.on(", "); private static final Joiner CONCAT_JOINER = Joiner.on(" || "); private static final Function<Expression, String> GET_EXPR_SQL = new Function<Expression, String>() { @Override public String apply(Expression e) { return e.getSql(); } }; private void expressions(Expression e) { addArgs(e); mBuilder .append("(") .append(e.getSql()) .append(")"); } private void expressions(Expression... e) { for (Expression expression : e) { addArgs(expression); } mBuilder .append("(") .append(ARGS_JOINER.join(getSQLs(e))) .append(")"); } private void addArgs(Expression expression) { for (Entry<Integer, Object> boundArg : expression.getBoundArgs().entrySet()) { mArgs.put(mArgsCount + boundArg.getKey(), boundArg.getValue()); } mArgsCount += expression.getArgsCount(); } private ExpressionBuilder binaryOperator(String operator) { mBuilder.append(" "); mBuilder.append(operator); mBuilder.append(" "); return this; } @Override public ExpressionBuilder eq() { return binaryOperator("=="); } @Override public ExpressionCombiner eq(Expression e) { eq(); expressions(e); return this; } @Override public ExpressionBuilder ne() { return binaryOperator("!="); } @Override public ExpressionCombiner ne(Expression e) { ne(); expressions(e); return this; } @Override public ExpressionBuilder gt() { return binaryOperator(">"); } @Override public ExpressionCombiner gt(Expression e) { gt(); expressions(e); return this; } @Override public ExpressionBuilder ge() { return binaryOperator(">="); } @Override public ExpressionCombiner ge(Expression e) { ge(); expressions(e); return this; } @Override public ExpressionBuilder lt() { return binaryOperator("<"); } @Override public ExpressionCombiner lt(Expression e) { lt(); expressions(e); return this; } @Override public ExpressionBuilder le() { return binaryOperator("<="); } @Override public ExpressionCombiner le(Expression e) { le(); expressions(e); return this; } @Override public ExpressionBuilder is() { return binaryOperator("IS"); } @Override public ExpressionCombiner is(Expression e) { is(); expressions(e); return this; } @Override public ExpressionCombiner in(Query subquery) { RawQuery rawQuery = subquery.toRawQuery(); for (String rawQueryArg : rawQuery.mRawQueryArgs) { mArgs.put(mArgsCount++, rawQueryArg); } mSubqueries.add(subquery); binaryOperator("IN"); mBuilder .append("(") .append(rawQuery.mRawQuery) .append(")"); return this; } @Override public ExpressionCombiner in(QueryBuilder subqueryBuilder) { return in(subqueryBuilder.build()); } @Override public ExpressionCombiner in(Expression... e) { binaryOperator("IN"); expressions(e); return this; } @Override public ExpressionCombiner notIn(Query subquery) { mBuilder.append(" NOT"); return in(subquery); } @Override public ExpressionCombiner notIn(QueryBuilder subqueryBuilder) { return notIn(subqueryBuilder.build()); } @Override public ExpressionCombiner notIn(Expression... e) { mBuilder.append(" NOT"); return in(e); } @Override public ExpressionBuilder or() { return binaryOperator("OR"); } @Override public ExpressionCombiner or(Expression e) { or(); expressions(e); return this; } @Override public ExpressionBuilder and() { return binaryOperator("AND"); } @Override public ExpressionCombiner and(Expression e) { and(); expressions(e); return this; } @Override public String getSql() { return mBuilder.toString().trim(); } @Override public int getArgsCount() { return mArgsCount; } @Override public Map<Integer, Object> getBoundArgs() { return mArgs; } @Override public Set<String> getTables() { return FluentIterable .from(mSubqueries) .transformAndConcat(GET_TABLES) .toSet(); } @SafeVarargs @Override public final <T> Object[] getMergedArgs(T... boundArgs) { ArrayList<Object> args = Lists.newArrayList(); addExpressionArgs(args, this, boundArgs); return args.toArray(); } @Override public ExpressionCombiner column(String col) { mBuilder.append(col); return this; } @Override public ExpressionCombiner column(String table, String col) { mBuilder.append(table); mBuilder.append("."); mBuilder.append(col); return this; } @Override public ExpressionCombiner arg() { mBuilder.append("?"); ++mArgsCount; return this; } @Override public ExpressionCombiner nul() { mBuilder.append("NULL"); return this; } @Override public ExpressionCombiner literal(Number number) { mBuilder.append(number.toString()); return this; } @Override public ExpressionCombiner literal(Object object) { mBuilder .append('\'') .append(object.toString().replaceAll("'", "''")) .append('\''); return this; } @Override public ExpressionCombiner sum(Expression e) { return function("SUM", e); } @Override public ExpressionCombiner count(Expression e) { return function("COUNT", e); } @Override public ExpressionCombiner count() { mBuilder.append("COUNT(*)"); return this; } @Override public ExpressionCombiner max(Expression e) { return function("MAX", e); } @Override public ExpressionCombiner min(Expression e) { return function("MIN", e); } @Override public ExpressionCombiner ifNull(Expression left, Expression right) { return function("ifnull", left, right); } @Override public ExpressionCombiner nullIf(Expression left, Expression right) { return function("nullif", left, right); } @Override public ExpressionCombiner coalesce(Expression... expressions) { Preconditions.checkArgument(expressions.length >= 2); return function("coalesce", expressions); } @Override public ExpressionCombiner length(Expression e) { return function("length", e); } @Override public ExpressionCombiner concat(Expression... e) { mBuilder.append(CONCAT_JOINER.join(getSQLs(e))); return this; } private Iterable<String> getSQLs(Expression[] e) { return Iterables.transform(Arrays.asList(e), GET_EXPR_SQL); } private static <T> Iterable<T> intersperse(final T element, final Iterable<T> iterable) { return new Iterable<T>() { @Override public Iterator<T> iterator() { final Iterator<T> iterator = iterable.iterator(); return new AbstractIterator<T>() { boolean intersperse = false; @Override protected T computeNext() { if (iterator.hasNext()) { final T result; if (intersperse) { result = element; } else { result = iterator.next(); } intersperse = !intersperse; return result; } return endOfData(); } }; } }; } @Override public ExpressionCombiner join(String on, Expression... e) { return concat(FluentIterable .from( intersperse( Expressions.literal(on), Arrays.asList(e) ) ) .toArray(Expression.class)); } private ExpressionCombiner function(String func, Expression... e) { mBuilder.append(func); expressions(e); return this; } @Override public ExpressionCombiner expr(String expr) { mBuilder.append(expr); return this; } @Override public ExpressionCombiner expr(Expression expression) { expressions(expression); return this; } @Override public ExpressionCore not() { mBuilder.append("NOT "); return this; } @Override public ExpressionCombiner otherwise(Expression e) { mBuilder.append(" ELSE "); expressions(e); return end(); } @Override public CaseValue when(Expression e) { mBuilder.append(" WHEN "); expressions(e); return this; } @Override public ExpressionCombiner end() { mBuilder.append(" END"); return this; } @Override public CaseCondition cases() { mBuilder.append("CASE"); return this; } @Override public CaseCondition cases(Expression e) { mBuilder.append("CASE "); expressions(e); return this; } @Override public CaseExpressionBuilder then(Expression e) { mBuilder.append(" THEN "); expressions(e); return this; } @Override public ExpressionCombiner collate(CollatingSequence collatingSequence) { mBuilder.append(" COLLATE ").append(collatingSequence.name()); return this; } } }