#1333 - ENH: Support "dynamic formula" with findSingleAttributeList() ... e.g. .select("concat(lastName,', ',firstName)")

This commit is contained in:
Rob Bygrave committed 2018-03-06 17:29:45 +13:00
1 parent 05b8adbf6e
commit c1c85b67c2
12 files changed
+266 -100

No files matched your search

@@ -222,7 +222,7 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
if (query.isRawSql()) {
return new DeployPropertyParserMap(query.getRawSql().getColumnMapping().getMapping());
} else {
return beanDescriptor.createDeployPropertyParser();
return beanDescriptor.parser();
}
}
@@ -1582,7 +1582,7 @@ public class BeanDescriptor<T> implements BeanType<T> {
}
}
public DeployPropertyParser createDeployPropertyParser() {
public DeployPropertyParser parser() {
return new DeployPropertyParser(this);
}
@@ -2453,18 +2453,7 @@ public class BeanDescriptor<T> implements BeanType<T> {
*/
private SqlTreeProperty findSqlTreeFormula(String formulaExpression) {
return dynamicProperty.computeIfAbsent(formulaExpression, (formula) -> {
FormulaPropertyPath propertyFormula = new FormulaPropertyPath(formula);
if (!propertyFormula.isFormula()) {
throw new IllegalStateException("unable to parse formula [" + formula + "} on bean type " + fullName);
}
String baseName = propertyFormula.basePropertyName();
BeanProperty base = _findBeanProperty(baseName);
if (base == null) {
throw new IllegalStateException("unable to find property [" + baseName + "] from formula [" + formula + "} on bean type " + fullName);
}
return propertyFormula.formulaProperty(base);
});
return dynamicProperty.computeIfAbsent(formulaExpression, (formula) -> new FormulaPropertyPath(this, formula).build());
}
/**
@@ -2497,7 +2486,7 @@ public class BeanDescriptor<T> implements BeanType<T> {
return _findBeanProperty(propName);
}
private BeanProperty _findBeanProperty(String propName) {
BeanProperty _findBeanProperty(String propName) {
BeanProperty prop = propMap.get(propName);
if (prop == null && inheritInfo != null) {
// search in sub types...
@@ -22,10 +22,29 @@ public final class DeployPropertyParser extends DeployParser {
private final Set<String> includes = new HashSet<>();
private boolean catchFirst;
private ElPropertyDeploy firstProp;
DeployPropertyParser(BeanDescriptor<?> beanDescriptor) {
this.beanDescriptor = beanDescriptor;
}
/**
* Set to true to catch the first property.
*/
public DeployPropertyParser setCatchFirst(boolean catchFirst) {
this.catchFirst = catchFirst;
return this;
}
/**
* Return the first property found by the parser.
*/
public ElPropertyDeploy getFirstProp() {
return firstProp;
}
@Override
public Set<String> getIncludes() {
return includes;
@@ -45,6 +64,9 @@ public final class DeployPropertyParser extends DeployParser {
if (elProp == null) {
return null;
} else {
if (catchFirst && firstProp == null) {
firstProp = elProp;
}
addIncludes(elProp.getElPrefix());
return elProp.getElPlaceholder(encrypted);
}
@@ -10,24 +10,27 @@ import javax.persistence.PersistenceException;
*/
class DynamicPropertyAggregationFormula extends DynamicPropertyBase {
private final String parsedAggregation;
private final String parsedFormula;
private final boolean aggregate;
private final BeanProperty asTarget;
DynamicPropertyAggregationFormula(String name, ScalarType<?> scalarType, String parsedAggregation, BeanProperty asTarget) {
DynamicPropertyAggregationFormula(String name, ScalarType<?> scalarType, String parsedFormula, boolean aggregate, BeanProperty asTarget) {
super(name, name, null, scalarType);
this.parsedAggregation = parsedAggregation;
this.parsedFormula = parsedFormula;
this.aggregate = aggregate;
this.asTarget = asTarget;
}
@Override
public String toString() {
return "DynamicPropertyFormula[" + parsedAggregation + "]";
return "DynamicPropertyFormula[" + parsedFormula + "]";
}
@Override
public boolean isAggregation() {
return true;
return aggregate;
}
@Override
@@ -46,7 +49,7 @@ class DynamicPropertyAggregationFormula extends DynamicPropertyBase {
@Override
public void appendSelect(DbSqlContext ctx, boolean subQuery) {
ctx.appendParseSelect(parsedAggregation);
ctx.appendParseSelect(parsedFormula);
}
}
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.deploy;
import io.ebeaninternal.server.el.ElPropertyDeploy;
import io.ebeaninternal.server.query.SqlTreeProperty;
import io.ebeaninternal.server.type.ScalarType;
@@ -9,27 +10,34 @@ import java.util.regex.Pattern;
class FormulaPropertyPath {
private static final Pattern pattern = Pattern.compile("(max|min|avg|count)\\((.*)\\)");
private static final String[] AGG_FUNCTIONS = {"count","max","min","avg"};
private static final Pattern pattern = Pattern.compile("([a-zA-Z]*)\\((.*)\\)");
private static final String DISTINCT_ = "distinct ";
private final String aggType;
private final BeanDescriptor<?> descriptor;
private final String baseName;
private final String formula;
private final String outerFunction;
private final String internalExpression;
private boolean countDistinct;
FormulaPropertyPath(String propName) {
FormulaPropertyPath(BeanDescriptor<?> descriptor, String formula) {
Matcher matcher = pattern.matcher(propName);
if (matcher.find()) {
aggType = matcher.group(1);
baseName = trimDistinct(matcher.group(2));
this.descriptor = descriptor;
this.formula = formula;
} else {
aggType = null;
baseName = null;
Matcher matcher = pattern.matcher(formula);
if (!matcher.find()) {
throw new IllegalStateException("Unable to parse formula [" + formula + "]");
}
//int groupCount = matcher.groupCount();
outerFunction = matcher.group(1);
internalExpression = trimDistinct(matcher.group(2));
}
private String trimDistinct(String propertyName) {
@@ -41,49 +49,58 @@ class FormulaPropertyPath {
}
}
boolean isFormula() {
return aggType != null;
}
String aggType() {
return aggType;
return outerFunction;
}
String basePropertyName() {
return baseName;
return internalExpression;
}
/**
* Create a bean property dynamically for the formula in the select clause.
*/
SqlTreeProperty formulaProperty(BeanProperty base) {
String parsedAggregation = buildFormula(base);
String name = logicalName();
SqlTreeProperty build() {
ScalarType<?> scalarType = base.getScalarType();
DeployPropertyParser parser = descriptor.parser().setCatchFirst(true);
String parsed = parser.parse(internalExpression);
ElPropertyDeploy firstProp = parser.getFirstProp();
ScalarType<?> scalarType;
if (isCount()) {
// count maps to Long / BIGINT
scalarType = base.getBeanDescriptor().getScalarType(Types.BIGINT);
scalarType = descriptor.getScalarType(Types.BIGINT);
} else {
// determine scalarType based on first property found by parser
if (firstProp != null) {
scalarType = firstProp.getBeanProperty().getScalarType();
} else {
throw new IllegalStateException("unable to determine scalarType of formula [" + formula + "] for type " + descriptor+" - maybe use a cast like ::String ?");
}
}
return new DynamicPropertyAggregationFormula(name, scalarType, parsedAggregation, null);
String parsedAggregation = buildFormula(parsed);
return new DynamicPropertyAggregationFormula(formula, scalarType, parsedAggregation, isAggregate(),null);
}
private String buildFormula(BeanProperty base) {
private boolean isAggregate() {
for (String aggFunction : AGG_FUNCTIONS) {
if (aggFunction.equals(outerFunction)) {
return true;
}
}
return false;
}
private String buildFormula(String parsed) {
if (countDistinct) {
return "count(distinct ${}"+base.getDbColumn()+")";
return "count(distinct "+parsed+")";
} else {
return aggType+"(${}"+base.getDbColumn()+")";
return outerFunction +"("+parsed+")";
}
}
private boolean isCount() {
return aggType.equals("count");
}
private String logicalName() {
return aggType+Character.toUpperCase(baseName.charAt(0))+baseName.substring(1);
return outerFunction.equals("count");
}
}
@@ -2,8 +2,10 @@ package io.ebeaninternal.server.querydefn;
import io.ebean.FetchConfig;
import io.ebean.util.StringHelper;
import io.ebeaninternal.server.util.DSelectColumnsParser;
import java.util.LinkedHashSet;
import java.util.List;
/**
* Parses the path properties string.
@@ -113,10 +115,10 @@ class OrmQueryPropertiesParser {
return null;
}
String[] res = inputProperties.split(",");
List<String> res = splitRawSelect(inputProperties);
StringBuilder sb = new StringBuilder(70);
LinkedHashSet<String> propertySet = new LinkedHashSet<>(res.length * 2);
LinkedHashSet<String> propertySet = new LinkedHashSet<>(res.size() * 2);
int count = 0;
String temp;
@@ -148,6 +150,13 @@ class OrmQueryPropertiesParser {
return propertySet;
}
/**
* Split allowing 'dynamic function based properties'.
*/
private List<String> splitRawSelect(String inputProperties) {
return DSelectColumnsParser.parse(inputProperties);
}
private int parseBatchHint(int pos, String option) {
int startPos = pos + option.length();
@@ -1,10 +1,12 @@
package io.ebeaninternal.server.rawsql;
import io.ebeaninternal.server.rawsql.SpiRawSql.ColumnMapping;
import io.ebeaninternal.server.util.DSelectColumnsParser;
import java.util.regex.Pattern;
import javax.persistence.PersistenceException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* Parses columnMapping (select clause) mapping columns to bean properties.
@@ -13,12 +15,8 @@ final class DRawSqlColumnsParser {
private static final Pattern COLINFO_SPLIT = Pattern.compile("\\s(?=[^\\)]*(?:\\(|$))");
private final int end;
private final String sqlSelect;
private int pos;
private int indexPos;
public static ColumnMapping parse(String sqlSelect) {
@@ -27,25 +25,21 @@ final class DRawSqlColumnsParser {
private DRawSqlColumnsParser(String sqlSelect) {
this.sqlSelect = sqlSelect;
this.end = sqlSelect.length();
}
private ColumnMapping parse() {
ArrayList<ColumnMapping.Column> columns = new ArrayList<>();
while (pos <= end) {
ColumnMapping.Column c = nextColumnInfo();
columns.add(c);
}
List<String> columnList = DSelectColumnsParser.parse(sqlSelect);
List<ColumnMapping.Column> columns = new ArrayList<>(columnList.size());
for (String rawColumn : columnList) {
columns.add(parseColumn(rawColumn));
}
return new ColumnMapping(columns);
}
private ColumnMapping.Column nextColumnInfo() {
int start = pos;
nextComma();
String colInfo = sqlSelect.substring(start, pos++);
colInfo = colInfo.trim();
private ColumnMapping.Column parseColumn(String colInfo) {
String[] split = COLINFO_SPLIT.split(colInfo);
if (split.length > 1) {
@@ -82,18 +76,4 @@ final class DRawSqlColumnsParser {
return new ColumnMapping.Column(indexPos++, sb.toString(), split[split.length - 1]);
}
private void nextComma() {
boolean inQuote = false;
int inbrackets = 0;
while (pos < end) {
char c = sqlSelect.charAt(pos);
if (c == '\'') inQuote = !inQuote;
else if (c == '(') inbrackets++;
else if (c == ')') inbrackets--;
else if (!inQuote && inbrackets == 0 && c == ',') {
return;
}
pos++;
}
}
}
@@ -0,0 +1,55 @@
package io.ebeaninternal.server.util;
import java.util.ArrayList;
import java.util.List;
/**
* Splits a select clause into 'logical columns' taking into account functions and quotes.
*/
public final class DSelectColumnsParser {
private final int end;
private final String selectClause;
private int pos;
public static List<String> parse(String sqlSelect) {
return new DSelectColumnsParser(sqlSelect).parse();
}
private DSelectColumnsParser(String selectClause) {
this.selectClause = selectClause;
this.end = selectClause.length();
}
private List<String> parse() {
ArrayList<String> columns = new ArrayList<>();
while (pos <= end) {
columns.add(nextColumnInfo());
}
return columns;
}
private String nextColumnInfo() {
int start = pos;
nextComma();
return selectClause.substring(start, pos++).trim();
}
private void nextComma() {
boolean inQuote = false;
int inBrackets = 0;
while (pos < end) {
char c = selectClause.charAt(pos);
if (c == '\'') inQuote = !inQuote;
else if (c == '(') inBrackets++;
else if (c == ')') inBrackets--;
else if (!inQuote && inBrackets == 0 && c == ',') {
return;
}
pos++;
}
}
}