#417 - ENH: Add JSON expressions ... path exists and value at path equals value

This commit is contained in:
Robin Bygrave committed 2015-09-21 16:20:14 +12:00
1 parent 054f7dc86f
commit 5726410f6f
18 files changed
+584 -33

No files matched your search

@@ -36,6 +36,46 @@ import java.util.Map;
*/
public interface ExpressionFactory {
/**
* Path exists - for the given path in a JSON document.
*/
Expression jsonExists(String propertyName, String path);
/**
* Path does not exist - for the given path in a JSON document.
*/
Expression jsonNotExists(String propertyName, String path);
/**
* Equal to - for the given path in a JSON document.
*/
Expression jsonEqualTo(String propertyName, String path, Object val);
/**
* Not Equal to - for the given path in a JSON document.
*/
Expression jsonNotEqualTo(String propertyName, String path, Object val);
/**
* Greater than - for the given path in a JSON document.
*/
Expression jsonGreaterThan(String propertyName, String path, Object val);
/**
* Greater than or equal to - for the given path in a JSON document.
*/
Expression jsonGreaterOrEqual(String propertyName, String path, Object val);
/**
* Less than - for the given path in a JSON document.
*/
Expression jsonLessThan(String propertyName, String path, Object val);
/**
* Less than or equal to - for the given path in a JSON document.
*/
Expression jsonLessOrEqualTo(String propertyName, String path, Object val);
/**
* Equal To - property equal to the given value.
*/
@@ -345,12 +345,66 @@ public interface ExpressionList<T> extends Serializable {
*/
ExpressionList<T> where();
/**
* Path exists - for the given path in a JSON document.
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
*/
ExpressionList<T> jsonExists(String propertyName, String path);
/**
* Path does not exist - for the given path in a JSON document.
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
*/
ExpressionList<T> jsonNotExists(String propertyName, String path);
/**
* Equal to expression for the value at the given path in the JSON document.
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
* @param value the value used to test equality against the document path's value
*/
ExpressionList<T> jsonEqualTo(String propertyName, String path, Object value);
/**
* Not Equal to - for the given path in a JSON document.
*
* @param propertyName the property that holds a JSON document
* @param path the nested path in the JSON document in dot notation
* @param value the value used to test equality against the document path's value
*/
ExpressionList<T> jsonNotEqualTo(String propertyName, String path, Object value);
/**
* Greater than - for the given path in a JSON document.
*/
ExpressionList<T> jsonGreaterThan(String propertyName, String path, Object val);
/**
* Greater than or equal to - for the given path in a JSON document.
*/
ExpressionList<T> jsonGreaterOrEqual(String propertyName, String path, Object val);
/**
* Less than - for the given path in a JSON document.
*/
ExpressionList<T> jsonLessThan(String propertyName, String path, Object val);
/**
* Less than or equal to - for the given path in a JSON document.
*/
ExpressionList<T> jsonLessOrEqualTo(String propertyName, String path, Object val);
/**
* Add an Expression to the list.
* <p>
* This returns the list so that add() can be chained.
* </p>
*
*
* <pre>{@code
*
* Query<Customer> query = Ebean.find(Customer.class);
@@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.api;
import java.util.ArrayList;
import com.avaje.ebeaninternal.server.core.JsonExpressionHandler;
import com.avaje.ebeaninternal.server.core.SpiOrmQueryRequest;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
@@ -10,6 +11,11 @@ import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
*/
public interface SpiExpressionRequest {
/**
* Return the DB specific JSON expression handler.
*/
JsonExpressionHandler getJsonHander();
/**
* Parse the logical property name to the deployment name.
*/
@@ -209,11 +209,29 @@ public class InternalConfiguration {
* For 'As Of' queries return the number of bind variables per predicate.
*/
private Binder getBinder(TypeManager typeManager, DatabasePlatform databasePlatform) {
JsonExpressionHandler jsonHandler = getJsonExpressionHandler(databasePlatform);
DbHistorySupport historySupport = databasePlatform.getHistorySupport();
if (historySupport == null) {
return new Binder(typeManager, 0, false);
return new Binder(typeManager, 0, false, jsonHandler);
}
return new Binder(typeManager, historySupport.getBindCount(), historySupport.isBindWithFromClause());
return new Binder(typeManager, historySupport.getBindCount(), historySupport.isBindWithFromClause(), jsonHandler);
}
/**
* Return the JSON expression handler for the given database platform.
*/
private JsonExpressionHandler getJsonExpressionHandler(DatabasePlatform databasePlatform) {
String name = databasePlatform.getName();
if ("postgres".equalsIgnoreCase(name)) {
return new PostgresJsonExpression();
}
if ("oracle".equalsIgnoreCase(name)) {
return new OracleJsonExpression();
}
return new NotSupportedJsonExpression();
}
/**
@@ -0,0 +1,17 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Adds the db platform specific json expression.
*/
public interface JsonExpressionHandler {
/**
* Write the db platform specific json expression.
*/
void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value);
}
@@ -0,0 +1,15 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Not supported JSON expression handler.
*/
public class NotSupportedJsonExpression implements JsonExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
throw new RuntimeException("JSON expressions only supported on Postgres and Oracle");
}
}
@@ -0,0 +1,24 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Postgres JSON expression handler
*/
public class OracleJsonExpression implements JsonExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
if (operator == Op.EXISTS) {
request.append("json_exists(").append(propName).append(", '$.").append(path).append("')");
} else if (operator == Op.NOT_EXISTS) {
request.append("not json_exists(").append(propName).append(", '$.").append(path).append("')");
} else {
request.append("json_value(").append(propName).append(", '$.").append(path).append("')");
request.append(operator.bind());
}
}
}
@@ -0,0 +1,71 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.expression.Op;
/**
* Postgres JSON expression handler
*/
public class PostgresJsonExpression implements JsonExpressionHandler {
@Override
public void addSql(SpiExpressionRequest request, String propName, String path, Op operator, Object value) {
StringBuilder sb = new StringBuilder(50);
String[] paths = path.split("\\.");
if (paths.length == 1) {
// (t0.content ->> 'title') = 'Some value'
sb.append("(").append(propName).append(" ->> '").append(path).append("')");
} else {
// (t0.content #>> '{path,inner}') = 'Some value'
sb.append("(").append(propName).append(" #>> '{");
for (int i = 0; i < paths.length; i++) {
if (i > 0) {
sb.append(",");
}
sb.append(paths[i]);
}
sb.append("}')");
}
request.append(castType(sb.toString(), value));
request.append(operator.bind());
}
/**
* Postgres CAST the type if necessary as text values always returned from the json operators used.
*/
private String castType(String expression, Object value) {
if (value == null) {
// for exists and not-exists expressions
return expression;
}
// Postgres cast of returned text value
if (isIntegerType(value)) {
return expression+"::INTEGER";
}
if (isNumberType(value)) {
return expression+"::DECIMAL";
}
if (isBooleanType(value)) {
return expression+"::BOOLEAN";
}
return expression;
}
private boolean isBooleanType(Object value) {
return (value instanceof Boolean);
}
private boolean isIntegerType(Object value) {
return (value instanceof Integer) || (value instanceof Long);
}
private boolean isNumberType(Object value) {
return (value instanceof Number);
}
}
@@ -211,7 +211,7 @@ public class DefaultExampleExpression implements SpiExpression, ExampleExpressio
// exclude the zero values typically to weed out
// primitive int and long that initialise to 0
if (includeZeros || !isZero(value)) {
list.add(new SimpleExpression(propName, SimpleExpression.Op.EQ, value));
list.add(new SimpleExpression(propName, Op.EQ, value));
}
}
}
@@ -1,9 +1,5 @@
package com.avaje.ebeaninternal.server.expression;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.avaje.ebean.ExampleExpression;
import com.avaje.ebean.Expression;
import com.avaje.ebean.ExpressionFactory;
@@ -15,6 +11,10 @@ import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebeaninternal.api.SpiExpressionFactory;
import com.avaje.ebeaninternal.api.SpiQuery;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Default Expression factory for creating standard expressions.
*/
@@ -36,6 +36,38 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
return "sql";
}
public Expression jsonExists(String propertyName, String path) {
return new JsonPathExpression(propertyName, path, Op.EXISTS, null);
}
public Expression jsonNotExists(String propertyName, String path) {
return new JsonPathExpression(propertyName, path, Op.NOT_EXISTS, null);
}
public Expression jsonEqualTo(String propertyName, String path, Object val) {
return new JsonPathExpression(propertyName, path, Op.EQ, val);
}
public Expression jsonNotEqualTo(String propertyName, String path, Object val) {
return new JsonPathExpression(propertyName, path, Op.NOT_EQ, val);
}
public Expression jsonGreaterThan(String propertyName, String path, Object val) {
return new JsonPathExpression(propertyName, path, Op.GT, val);
}
public Expression jsonGreaterOrEqual(String propertyName, String path, Object val) {
return new JsonPathExpression(propertyName, path, Op.GT_EQ, val);
}
public Expression jsonLessThan(String propertyName, String path, Object val) {
return new JsonPathExpression(propertyName, path, Op.LT, val);
}
public Expression jsonLessOrEqualTo(String propertyName, String path, Object val) {
return new JsonPathExpression(propertyName, path, Op.LT_EQ, val);
}
/**
* Equal To - property equal to the given value.
*/
@@ -43,7 +75,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
if (value == null) {
return equalsWithNullAsNoop ? NoopExpression.INSTANCE : isNull(propertyName);
}
return new SimpleExpression(propertyName, SimpleExpression.Op.EQ, value);
return new SimpleExpression(propertyName, Op.EQ, value);
}
/**
@@ -53,7 +85,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
if (value == null) {
return equalsWithNullAsNoop ? NoopExpression.INSTANCE : isNotNull(propertyName);
}
return new SimpleExpression(propertyName, SimpleExpression.Op.NOT_EQ, value);
return new SimpleExpression(propertyName, Op.NOT_EQ, value);
}
/**
@@ -88,7 +120,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression gt(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.GT, value);
return new SimpleExpression(propertyName, Op.GT, value);
}
/**
@@ -97,7 +129,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression ge(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.GT_EQ, value);
return new SimpleExpression(propertyName, Op.GT_EQ, value);
}
/**
@@ -105,7 +137,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression lt(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.LT, value);
return new SimpleExpression(propertyName, Op.LT, value);
}
/**
@@ -113,7 +145,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
*/
public Expression le(String propertyName, Object value) {
return new SimpleExpression(propertyName, SimpleExpression.Op.LT_EQ, value);
return new SimpleExpression(propertyName, Op.LT_EQ, value);
}
/**
@@ -0,0 +1,69 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebean.event.BeanQueryRequest;
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
/**
* Generally speaking tests the value at a given path in the JSON document.
* <p>
* Supports the usual operators (equal to, greater than etc).
* </p>
* <p>
* The value passed in is expected to be a valid JSON type so string, number, boolean.
* </p>
*/
class JsonPathExpression extends AbstractExpression {
/**
* The path in the JSON document in dot notation form.
*/
protected final String path;
/**
* The expression operator.
*/
protected final Op operator;
/**
* The bind value used to compare against the document path value.
*/
protected final Object value;
JsonPathExpression(String propertyName, String path, Op operator, Object value) {
super(propertyName);
this.path = path;
this.operator = operator;
this.value = value;
}
@Override
public void queryAutoTuneHash(HashQueryPlanBuilder builder) {
builder.add(JsonPathExpression.class).add(propName).add(path).add(operator);
}
@Override
public void queryPlanHash(BeanQueryRequest<?> request, HashQueryPlanBuilder builder) {
queryAutoTuneHash(builder);
}
@Override
public int queryBindHash() {
return (value == null) ? 0 : value.hashCode();
}
@Override
public void addSql(SpiExpressionRequest request) {
// Use DB specific expression handling (Postgres and Oracle supported)
request.getJsonHander().addSql(request, propName, path, operator, value);
}
@Override
public void addBindValues(SpiExpressionRequest request) {
if (value != null) {
request.addBindValue(value);
}
}
}
@@ -313,6 +313,70 @@ abstract class JunctionExpression<T> implements Junction<T>, SpiExpression, Expr
return exprList.findUnique();
}
/**
* Path exists - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonExists(String propertyName, String path) {
return exprList.jsonExists(propertyName, path);
}
/**
* Path does not exist - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonNotExists(String propertyName, String path){
return exprList.jsonNotExists(propertyName, path);
}
/**
* Equal to - for the value at the given path in the JSON document.
*/
@Override
public ExpressionList<T> jsonEqualTo(String propertyName, String path, Object value){
return exprList.jsonEqualTo(propertyName, path, value);
}
/**
* Not Equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonNotEqualTo(String propertyName, String path, Object val){
return exprList.jsonNotEqualTo(propertyName, path, val);
}
/**
* Greater than - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonGreaterThan(String propertyName, String path, Object val){
return exprList.jsonGreaterThan(propertyName, path, val);
}
/**
* Greater than or equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonGreaterOrEqual(String propertyName, String path, Object val){
return exprList.jsonGreaterOrEqual(propertyName, path, val);
}
/**
* Less than - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonLessThan(String propertyName, String path, Object val){
return exprList.jsonLessThan(propertyName, path, val);
}
/**
* Less than or equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonLessOrEqualTo(String propertyName, String path, Object val){
return exprList.jsonLessOrEqualTo(propertyName, path, val);
}
@Override
public ExpressionList<T> ge(String propertyName, Object value) {
return exprList.ge(propertyName, value);
@@ -0,0 +1,62 @@
package com.avaje.ebeaninternal.server.expression;
/**
* Simple operators - equals, greater than, less than etc.
*/
public enum Op {
/**
* Exists (JSON).
*/
EXISTS(" is not null "),
/**
* Not Exists (JSON).
*/
NOT_EXISTS(" is null "),
/**
* Equal to
*/
EQ(" = ? "),
/**
* Not equal to.
*/
NOT_EQ(" <> ? "),
/**
*
* Less than.
*/
LT(" < ? "),
/**
* Less than or equal to.
*/
LT_EQ(" <= ? "),
/**
* Greater than.
*/
GT(" > ? "),
/**
* Greater than or equal to.
*/
GT_EQ(" >= ? ");
final String exp;
Op(String exp) {
this.exp = exp;
}
/**
* Return the bind expression include JDBC ? bind placeholder.
*/
public String bind() {
return exp;
}
}
@@ -10,20 +10,6 @@ public class SimpleExpression extends AbstractExpression {
private static final long serialVersionUID = -382881395755603790L;
enum Op {
EQ(" = ? "), NOT_EQ(" <> ? "), LT(" < ? "), LT_EQ(" <= ? "), GT(" > ? "), GT_EQ(" >= ? ");
final String exp;
Op(String exp) {
this.exp = exp;
}
public String bind() {
return exp;
}
}
private final Op type;
private final Object value;
@@ -10,6 +10,7 @@ import java.util.List;
import javax.persistence.PersistenceException;
import com.avaje.ebeaninternal.api.BindParams;
import com.avaje.ebeaninternal.server.core.JsonExpressionHandler;
import com.avaje.ebeaninternal.server.core.Message;
import com.avaje.ebeaninternal.server.type.DataBind;
import com.avaje.ebeaninternal.server.type.ScalarType;
@@ -30,13 +31,16 @@ public class Binder {
private final boolean bindAsOfWithFromClause;
private final JsonExpressionHandler jsonExpressionHandler;
/**
* Set the PreparedStatement with which to bind variables to.
*/
public Binder(TypeManager typeManager, int asOfBindCount, boolean bindAsOfWithFromClause) {
public Binder(TypeManager typeManager, int asOfBindCount, boolean bindAsOfWithFromClause, JsonExpressionHandler jsonExpressionHandler) {
this.typeManager = typeManager;
this.asOfBindCount = asOfBindCount;
this.bindAsOfWithFromClause = bindAsOfWithFromClause;
this.jsonExpressionHandler = jsonExpressionHandler;
}
/**
@@ -375,4 +379,11 @@ public class Binder {
return false;
}
}
/**
* Return the JsonExpressionHandler specific to the database.
*/
public JsonExpressionHandler getJsonExpressionHandler() {
return jsonExpressionHandler;
}
}
@@ -302,7 +302,7 @@ public class CQueryPredicates {
SpiExpressionList<?> whereExp = query.getWhereExpressions();
if (whereExp != null) {
DefaultExpressionRequest whereReq = new DefaultExpressionRequest(request, deployParser);
DefaultExpressionRequest whereReq = new DefaultExpressionRequest(request, deployParser, binder);
whereExprBindValues = whereExp.buildBindValues(whereReq);
if (buildSql) {
whereExprSql = whereExp.buildSql(whereReq);
@@ -314,7 +314,7 @@ public class CQueryPredicates {
OrmQueryProperties chunk = query.getDetail().getChunk(manyProperty.getName(), false);
SpiExpressionList<?> filterMany = chunk.getFilterMany();
if (filterMany != null) {
DefaultExpressionRequest filterReq = new DefaultExpressionRequest(request, deployParser);
DefaultExpressionRequest filterReq = new DefaultExpressionRequest(request, deployParser, binder);
filterManyExprBindValues = filterMany.buildBindValues(filterReq);
if (buildSql) {
filterManyExprSql = filterMany.buildSql(filterReq);
@@ -325,7 +325,7 @@ public class CQueryPredicates {
// having expression
SpiExpressionList<?> havingExpr = query.getHavingExpressions();
if (havingExpr != null) {
DefaultExpressionRequest havingReq = new DefaultExpressionRequest(request, deployParser);
DefaultExpressionRequest havingReq = new DefaultExpressionRequest(request, deployParser, binder);
havingExprBindValues = havingExpr.buildBindValues(havingReq);
if (buildSql) {
havingExprSql = havingExpr.buildSql(havingReq);
@@ -363,6 +363,78 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
return hash;
}
/**
* Path exists - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonExists(String propertyName, String path) {
add(expr.jsonExists(propertyName, path));
return this;
}
/**
* Path does not exist - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonNotExists(String propertyName, String path){
add(expr.jsonNotExists(propertyName, path));
return this;
}
/**
* Equal to expression for the value at the given path in the JSON document.
*/
@Override
public ExpressionList<T> jsonEqualTo(String propertyName, String path, Object value){
add(expr.jsonEqualTo(propertyName, path, value));
return this;
}
/**
* Not Equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonNotEqualTo(String propertyName, String path, Object val){
add(expr.jsonNotEqualTo(propertyName, path, val));
return this;
}
/**
* Greater than - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonGreaterThan(String propertyName, String path, Object val){
add(expr.jsonGreaterThan(propertyName, path, val));
return this;
}
/**
* Greater than or equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonGreaterOrEqual(String propertyName, String path, Object val){
add(expr.jsonGreaterOrEqual(propertyName, path, val));
return this;
}
/**
* Less than - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonLessThan(String propertyName, String path, Object val){
add(expr.jsonLessThan(propertyName, path, val));
return this;
}
/**
* Less than or equal to - for the given path in a JSON document.
*/
@Override
public ExpressionList<T> jsonLessOrEqualTo(String propertyName, String path, Object val){
add(expr.jsonLessOrEqualTo(propertyName, path, val));
return this;
}
@Override
public ExpressionList<T> eq(String propertyName, Object value) {
add(expr.eq(propertyName, value));
@@ -3,9 +3,11 @@ package com.avaje.ebeaninternal.util;
import java.util.ArrayList;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.server.core.JsonExpressionHandler;
import com.avaje.ebeaninternal.server.core.SpiOrmQueryRequest;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.DeployParser;
import com.avaje.ebeaninternal.server.persist.Binder;
public class DefaultExpressionRequest implements SpiExpressionRequest {
@@ -19,18 +21,26 @@ public class DefaultExpressionRequest implements SpiExpressionRequest {
private final DeployParser deployParser;
private final Binder binder;
private int paramIndex;
public DefaultExpressionRequest(SpiOrmQueryRequest<?> queryRequest, DeployParser deployParser) {
public DefaultExpressionRequest(SpiOrmQueryRequest<?> queryRequest, DeployParser deployParser, Binder binder) {
this.queryRequest = queryRequest;
this.beanDescriptor = queryRequest.getBeanDescriptor();
this.deployParser = deployParser;
this.binder = binder;
}
public DefaultExpressionRequest(BeanDescriptor<?> beanDescriptor) {
this.beanDescriptor = beanDescriptor;
this.queryRequest = null;
this.deployParser = null;
this.binder = null;
}
public JsonExpressionHandler getJsonHander() {
return binder.getJsonExpressionHandler();
}
public String parseDeploy(String logicalProp) {