mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-11-14 14:46:25 +00:00
#1333 - ENH: Support "dynamic formula" with findSingleAttributeList() ... e.g. .select("concat(lastName,', ',firstName)")
This commit is contained in:
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user