package org.molgenis.data.postgresql;
import com.google.common.collect.Lists;
import org.molgenis.data.*;
import org.molgenis.data.QueryRule.Operator;
import org.molgenis.data.meta.AttributeType;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.EntityType;
import org.molgenis.data.support.QueryImpl;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import static java.lang.String.format;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.IntStream.range;
import static java.util.stream.StreamSupport.stream;
import static org.molgenis.data.QueryRule.Operator.NESTED;
import static org.molgenis.data.meta.AttributeType.*;
import static org.molgenis.data.postgresql.PostgreSqlQueryUtils.*;
import static org.molgenis.data.support.EntityTypeUtils.*;
/**
* Utility class that generates the SQL used by {@link PostgreSqlRepository} and {@link PostgreSqlRepositoryCollection}
*/
class PostgreSqlQueryGenerator
{
final static String ERR_CODE_READONLY_VIOLATION = "23506";
private PostgreSqlQueryGenerator()
{
}
private static String getSqlConstraintPrimaryKey(EntityType entityType, Attribute attr)
{
return "CONSTRAINT " + getPrimaryKeyName(entityType, attr) + " PRIMARY KEY (" + getColumnName(attr) + ')';
}
private static String getSqlForeignKey(EntityType entityType, Attribute attr)
{
StringBuilder strBuilder = new StringBuilder("CONSTRAINT ").append(getForeignKeyName(entityType, attr))
.append(" FOREIGN KEY (").append(getColumnName(attr)).append(") REFERENCES ")
.append(getTableName(attr.getRefEntity())).append('(')
.append(getColumnName(attr.getRefEntity().getIdAttribute())).append(')');
// for self-referencing data or inversed attributes defer checking constraints until the end of the transaction
if (attr.getRefEntity().getName().equals(entityType.getName()))
{
strBuilder.append(" DEFERRABLE INITIALLY DEFERRED");
}
return strBuilder.toString();
}
private static String getSqlUniqueKey(EntityType entityType, Attribute attr)
{
return "CONSTRAINT " + getUniqueKeyName(entityType, attr) + " UNIQUE (" + getColumnName(attr) + ')';
}
private static String getSqlCheckConstraint(EntityType entityType, Attribute attr)
{
if (attr.getDataType() != ENUM)
{
throw new MolgenisDataException(
format("Check constraint only allowed for attribute type [%s]", ENUM.toString()));
}
return "CONSTRAINT " + getCheckConstraintName(entityType, attr) + " CHECK (" + getColumnName(attr) + " IN ("
+ attr.getEnumOptions().stream().map(enumOption -> '\'' + enumOption + '\'').collect(joining(","))
+ "))";
}
static String getSqlCreateForeignKey(EntityType entityType, Attribute attr)
{
return "ALTER TABLE " + getTableName(entityType) + " ADD " + getSqlForeignKey(entityType, attr);
}
static String getSqlDropForeignKey(EntityType entityType, Attribute attr)
{
return "ALTER TABLE " + getTableName(entityType) + " DROP CONSTRAINT " + getForeignKeyName(entityType, attr);
}
static String getSqlCreateUniqueKey(EntityType entityType, Attribute attr)
{
return "ALTER TABLE " + getTableName(entityType) + " ADD " + getSqlUniqueKey(entityType, attr);
}
static String getSqlDropUniqueKey(EntityType entityType, Attribute attr)
{
return "ALTER TABLE " + getTableName(entityType) + " DROP CONSTRAINT " + getUniqueKeyName(entityType, attr);
}
static String getSqlCreateCheckConstraint(EntityType entityType, Attribute attr)
{
return "ALTER TABLE " + getTableName(entityType) + " ADD " + getSqlCheckConstraint(entityType, attr);
}
static String getSqlDropCheckConstraint(EntityType entityType, Attribute attr)
{
if (attr.getDataType() != ENUM)
{
throw new MolgenisDataException(
format("Check constraint only allowed for attribute type [%s]", ENUM.toString()));
}
return "ALTER TABLE " + getTableName(entityType) + " DROP CONSTRAINT " + getCheckConstraintName(entityType,
attr);
}
static String getSqlSetNotNull(EntityType entityType, Attribute attr)
{
return "ALTER TABLE " + getTableName(entityType) + " ALTER COLUMN " + getColumnName(attr) + " SET NOT NULL";
}
static String getSqlDropNotNull(EntityType entityType, Attribute attr)
{
return "ALTER TABLE " + getTableName(entityType) + " ALTER COLUMN " + getColumnName(attr) + " DROP NOT NULL";
}
static String getSqlSetDataType(EntityType entityType, Attribute attr)
{
return "ALTER TABLE " + getTableName(entityType) + " ALTER COLUMN " + getColumnName(attr) + " SET DATA TYPE "
+ getPostgreSqlType(attr) + " USING " + getColumnName(attr) + "::" + getPostgreSqlType(attr);
}
/**
* Returns SQL string to add a column to an existing table.
*
* @param entityType entity meta data
* @param attr attribute
* @return SQL string
*/
static String getSqlAddColumn(EntityType entityType, Attribute attr)
{
StringBuilder sql = new StringBuilder("ALTER TABLE ");
String columnSql = getSqlColumn(entityType, attr);
sql.append(getTableName(entityType)).append(" ADD ").append(columnSql);
List<String> sqlTableConstraints = getSqlTableConstraints(entityType, attr);
if (!sqlTableConstraints.isEmpty())
{
sqlTableConstraints.forEach(sqlTableConstraint -> sql.append(",ADD ").append(sqlTableConstraint));
}
return sql.toString();
}
static String getSqlCreateTable(EntityType entityType)
{
List<Attribute> persistedTableAttrs = getTableAttributes(entityType).collect(toList());
StringBuilder sql = new StringBuilder("CREATE TABLE ").append(getTableName(entityType)).append('(');
// add columns
for (Iterator<Attribute> it = persistedTableAttrs.iterator(); it.hasNext(); )
{
Attribute attr = it.next();
sql.append(getSqlColumn(entityType, attr));
if (it.hasNext())
{
sql.append(',');
}
}
// add table constraints
for (Attribute persistedTableAttr : persistedTableAttrs)
{
List<String> sqlTableConstraints = getSqlTableConstraints(entityType, persistedTableAttr);
if (!sqlTableConstraints.isEmpty())
{
sqlTableConstraints.forEach(sqlTableConstraint -> sql.append(',').append(sqlTableConstraint));
}
}
sql.append(')');
return sql.toString();
}
private static String getSqlFunctionValidateUpdateName(EntityType entityType)
{
return '"' + "validate_update_" + getTableName(entityType, false) + '"';
}
static String getSqlCreateFunctionValidateUpdate(EntityType entityType, Collection<Attribute> readonlyTableAttrs)
{
StringBuilder strBuilder = new StringBuilder(512).append("CREATE FUNCTION ")
.append(getSqlFunctionValidateUpdateName(entityType)).append("() RETURNS TRIGGER AS $$\nBEGIN\n");
String tableName = getTableName(entityType);
String idColName = getColumnName(entityType.getIdAttribute());
readonlyTableAttrs.forEach(attr ->
{
String colName = getColumnName(attr);
strBuilder.append(" IF OLD.").append(colName).append(" <> NEW.").append(colName).append(" THEN\n");
strBuilder.append(" RAISE EXCEPTION 'Updating read-only column ").append(colName).append(" of table ")
.append(tableName).append(" with id [%] is not allowed', OLD.").append(idColName)
.append(" USING ERRCODE = '").append(ERR_CODE_READONLY_VIOLATION).append("';\n");
strBuilder.append(" END IF;\n");
});
strBuilder.append(" RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;");
return strBuilder.toString();
}
static String getSqlDropFunctionValidateUpdate(EntityType entityType)
{
return "DROP FUNCTION " + getSqlFunctionValidateUpdateName(entityType) + "();";
}
private static String getSqlUpdateTriggerName(EntityType entityType)
{
return '"' + "update_trigger_" + getTableName(entityType, false) + '"';
}
static String getSqlCreateUpdateTrigger(EntityType entityType, Collection<Attribute> readonlyTableAttrs)
{
StringBuilder strBuilder = new StringBuilder(512).append("CREATE TRIGGER ")
.append(getSqlUpdateTriggerName(entityType)).append(" AFTER UPDATE ON ")
.append(getTableName(entityType)).append(" FOR EACH ROW WHEN (");
strBuilder.append(readonlyTableAttrs.stream()
.map(attr -> "OLD." + getColumnName(attr) + " IS DISTINCT FROM NEW." + getColumnName(attr))
.collect(joining(" OR ")));
strBuilder.append(") EXECUTE PROCEDURE ").append(getSqlFunctionValidateUpdateName(entityType)).append("();");
return strBuilder.toString();
}
static String getSqlDropUpdateTrigger(EntityType entityType)
{
return "DROP TRIGGER " + getSqlUpdateTriggerName(entityType) + " ON " + getTableName(entityType);
}
static String getSqlCreateJunctionTable(EntityType entityType, Attribute attr)
{
Attribute idAttr = entityType.getIdAttribute();
StringBuilder sql = new StringBuilder("CREATE TABLE ").append(getJunctionTableName(entityType, attr))
.append(" (").append(getColumnName(JUNCTION_TABLE_ORDER_ATTR_NAME)).append(" INT,")
.append(getColumnName(idAttr)).append(' ').append(getPostgreSqlType(idAttr)).append(" NOT NULL, ")
.append(getColumnName(attr)).append(' ').append(getPostgreSqlType(attr.getRefEntity().getIdAttribute()))
.append(" NOT NULL").append(", FOREIGN KEY (").append(getColumnName(idAttr)).append(") REFERENCES ")
.append(getTableName(entityType)).append('(').append(getColumnName(idAttr))
.append(") ON DELETE CASCADE");
// for self-referencing data defer checking constraints until the end of the transaction
if (attr.getRefEntity().getName().equals(entityType.getName()))
{
sql.append(" DEFERRABLE INITIALLY DEFERRED");
}
if (isPersistedInPostgreSql(attr.getRefEntity()))
{
sql.append(", FOREIGN KEY (").append(getColumnName(attr)).append(") REFERENCES ")
.append(getTableName(attr.getRefEntity())).append('(')
.append(getColumnName(attr.getRefEntity().getIdAttribute())).append(") ON DELETE CASCADE");
// for self-referencing data defer checking constraints until the end of the transaction
if (attr.getRefEntity().getName().equals(entityType.getName()))
{
sql.append(" DEFERRABLE INITIALLY DEFERRED");
}
}
AttributeType attrType = attr.getDataType();
switch (attrType)
{
case CATEGORICAL_MREF:
case MREF:
sql.append(", UNIQUE (").append(getColumnName(idAttr)).append(',').append(getColumnName(attr))
.append(')');
break;
default:
throw new RuntimeException(format("Illegal attribute type [%s]", attrType.toString()));
}
sql.append(", UNIQUE (").append(getColumnName(JUNCTION_TABLE_ORDER_ATTR_NAME)).append(',')
.append(getColumnName(idAttr)).append(')');
sql.append(')');
return sql.toString();
}
static String getSqlCreateJunctionTableIndex(EntityType entityType, Attribute attr)
{
Attribute idAttr = entityType.getIdAttribute();
String junctionTableName = getJunctionTableName(entityType, attr);
String junctionTableIndexName = getJunctionTableIndexName(entityType, attr, idAttr);
String idxColumnName = getColumnName(idAttr);
return "CREATE INDEX " + junctionTableIndexName + " ON " + junctionTableName + " (" + idxColumnName + ')';
}
static String getSqlDropJunctionTable(EntityType entityType, Attribute attr)
{
return getSqlDropTable(getJunctionTableName(entityType, attr));
}
static String getSqlDropTable(EntityType entityType)
{
return getSqlDropTable(getTableName(entityType));
}
static String getSqlDropColumn(EntityType entityType, Attribute attr)
{
return "ALTER TABLE " + getTableName(entityType) + " DROP COLUMN " + getColumnName(attr);
}
static String getSqlInsert(EntityType entityType)
{
StringBuilder sql = new StringBuilder("INSERT INTO ").append(getTableName(entityType)).append(" (");
StringBuilder params = new StringBuilder();
getTableAttributes(entityType).forEach(attr ->
{
sql.append(getColumnName(attr)).append(", ");
params.append("?, ");
});
if (sql.charAt(sql.length() - 1) == ' ' && sql.charAt(sql.length() - 2) == ',')
{
sql.setLength(sql.length() - 2);
params.setLength(params.length() - 2);
}
sql.append(") VALUES (").append(params).append(')');
return sql.toString();
}
static String getSqlInsertJunction(EntityType entityType, Attribute attr)
{
String junctionTableName = getJunctionTableName(entityType, attr);
return "INSERT INTO " + junctionTableName + " (" + getColumnName(JUNCTION_TABLE_ORDER_ATTR_NAME) + ','
+ getColumnName(entityType.getIdAttribute()) + ',' + getColumnName(attr) + ") VALUES (?,?,?)";
}
static String getSqlDeleteAll(EntityType entityType)
{
return "DELETE FROM " + getTableName(entityType);
}
static String getSqlDelete(EntityType entityType)
{
return getSqlDelete(getTableName(entityType), entityType.getIdAttribute());
}
static String getSqlDelete(String tableName, Attribute attr)
{
return "DELETE FROM " + tableName + " WHERE " + getColumnName(attr) + " = ?";
}
/**
* Returns whether this attribute is stored in the entity table or another table such as a junction table or
* referenced entity table.
*
* @param attr attribute
* @return whether this attribute is stored in another table than the entity table
*/
private static boolean isPersistedInOtherTable(Attribute attr)
{
boolean bidirectionalOneToMany = attr.getDataType() == ONE_TO_MANY && attr.isMappedBy();
return isMultipleReferenceType(attr) || bidirectionalOneToMany;
}
static String getSqlJunctionTableSelect(EntityType entityType, Attribute attr, int numOfIds)
{
String idColName = getColumnName(entityType.getIdAttribute());
String refIdColName = getColumnName(attr);
return "SELECT " + idColName + ", \"" + JUNCTION_TABLE_ORDER_ATTR_NAME + "\"," + refIdColName + " FROM "
+ getJunctionTableName(entityType, attr) + " WHERE " + idColName + " in (" + range(0, numOfIds)
.mapToObj((x) -> "?").collect(joining(", ")) + ") ORDER BY " + idColName + ", \""
+ JUNCTION_TABLE_ORDER_ATTR_NAME + '"';
}
/**
* Determines whether a distinct select is required based on a given query.
*
* @param entityType entity meta data
* @param q query
* @param <E> entity type
* @return <code>true</code> if a distinct select is required for SQL queries based on the given query
* @throws UnknownAttributeException if query field refers to an attribute that does not exist in entity meta
*/
private static <E extends Entity> boolean isDistinctSelectRequired(EntityType entityType, Query<E> q)
{
return isDistinctSelectRequiredRec(entityType, q.getRules());
}
private static boolean isDistinctSelectRequiredRec(EntityType entityType, List<QueryRule> queryRules)
{
if (queryRules.isEmpty())
{
return false;
}
for (QueryRule queryRule : queryRules)
{
if (queryRule.getOperator() == NESTED)
{
if (isDistinctSelectRequiredRec(entityType, queryRule.getNestedRules()))
{
return true;
}
}
else
{
String attrName = queryRule.getField();
if (attrName != null)
{
Attribute attr = entityType.getAttribute(attrName);
if (attr == null)
{
throw new UnknownAttributeException(
format("Unknown attribute [%s] in entity [%s]", attrName, entityType.getName()));
}
if (isPersistedInOtherTable(attr))
{
return true;
}
}
}
}
return false;
}
static <E extends Entity> String getSqlSelect(EntityType entityType, Query<E> q, List<Object> parameters,
boolean includeMrefs)
{
final StringBuilder select = new StringBuilder("SELECT ");
if (isDistinctSelectRequired(entityType, q))
{
select.append("DISTINCT ");
}
final StringBuilder group = new StringBuilder();
final AtomicInteger count = new AtomicInteger();
final Attribute idAttribute = entityType.getIdAttribute();
getPersistedAttributes(entityType).forEach(attr ->
{
if (q.getFetch() == null || q.getFetch().hasField(attr.getName()) || (q.getSort() != null && q.getSort()
.hasField(attr.getName())))
{
if (count.get() > 0)
{
select.append(", ");
}
if (isPersistedInOtherTable(attr))
{
if (includeMrefs || (attr.getDataType() == ONE_TO_MANY && attr.isMappedBy()))
{
if (attr.getDataType() == ONE_TO_MANY && attr.isMappedBy())
{
Attribute refIdAttr = attr.getRefEntity().getIdAttribute();
String mrefSelect = "(SELECT array_agg(" + getColumnName(refIdAttr);
Sort orderBy = attr.getOrderBy();
if (orderBy == null)
{
orderBy = new Sort(refIdAttr.getName());
}
mrefSelect +=
' ' + getSqlSort(attr.getRefEntity(), new QueryImpl<>().sort(orderBy)) + ") FROM "
+ getTableName(attr.getRefEntity()) + " WHERE this." + getColumnName(
idAttribute) + " = " + getTableName(attr.getRefEntity()) + '.'
+ getColumnName(attr.getMappedBy()) + ") AS " + getColumnName(attr);
select.append(mrefSelect);
}
else
{
// TODO retrieve mref values in separate queries to allow specifying limit and offset after nested MOLGENIS queries are implemented as sub-queries instead of query rules
String mrefSelect = MessageFormat
.format("(SELECT array_agg(DISTINCT ARRAY[{0}.{1}::TEXT,{0}.{0}::TEXT]) "
+ "FROM {2} AS {0} WHERE this.{3} = {0}.{3}) AS {0}", getColumnName(attr),
getColumnName(JUNCTION_TABLE_ORDER_ATTR_NAME),
getJunctionTableName(entityType, attr), getColumnName(idAttribute));
select.append(mrefSelect);
}
}
else
{
select.append("NULL AS ").append(getColumnName(attr));
}
}
else
{
select.append("this.").append(getColumnName(attr));
if (group.length() > 0)
{
group.append(", this.").append(getColumnName(attr));
}
else
{
group.append("this.").append(getColumnName(attr));
}
}
count.incrementAndGet();
}
});
// from
StringBuilder result = new StringBuilder().append(select).append(getSqlFrom(entityType, q));
// where
String where = getSqlWhere(entityType, q, parameters, 0);
if (where.length() > 0)
{
result.append(" WHERE ").append(where);
}
// order by
result.append(' ').append(getSqlSort(entityType, q));
// limit
if (q.getPageSize() > 0)
{
result.append(" LIMIT ").append(q.getPageSize());
}
if (q.getOffset() > 0)
{
result.append(" OFFSET ").append(q.getOffset());
}
return result.toString().trim();
}
static String getSqlUpdate(EntityType entityType)
{
// use (readonly) identifier
Attribute idAttribute = entityType.getIdAttribute();
// create sql
StringBuilder sql = new StringBuilder("UPDATE ").append(getTableName(entityType)).append(" SET ");
getTableAttributes(entityType).forEach(attr -> sql.append(getColumnName(attr)).append(" = ?, "));
if (sql.charAt(sql.length() - 1) == ' ' && sql.charAt(sql.length() - 2) == ',')
{
sql.setLength(sql.length() - 2);
}
sql.append(" WHERE ").append(getColumnName(idAttribute)).append("= ?");
return sql.toString();
}
/**
* Produces SQL to count the number of entities that match the given query. Ignores query offset and pagesize.
*
* @param q query
* @param parameters prepared statement parameters
* @return SQL string
*/
static <E extends Entity> String getSqlCount(EntityType entityType, Query<E> q, List<Object> parameters)
{
StringBuilder sqlBuilder = new StringBuilder("SELECT COUNT");
String idAttribute = getColumnName(entityType.getIdAttribute());
List<QueryRule> queryRules = q.getRules();
if (queryRules == null || queryRules.isEmpty())
{
sqlBuilder.append("(*) FROM ").append(getTableName(entityType));
}
else
{
boolean distinctSelectRequired = isDistinctSelectRequired(entityType, q);
if (distinctSelectRequired)
{
// distinct count in case query contains one or more rules referring to MREF attributes.
sqlBuilder.append("(DISTINCT this.").append(idAttribute).append(')');
}
else
{
sqlBuilder.append("(*)");
}
String from = getSqlFrom(entityType, q);
String where = getSqlWhere(entityType, q, parameters, 0);
sqlBuilder.append(from).append(" WHERE ").append(where);
}
return sqlBuilder.toString();
}
private static String getSqlColumn(EntityType entityType, Attribute attr)
{
StringBuilder sqlBuilder = new StringBuilder(getColumnName(attr)).append(' ');
AttributeType attrType = attr.getDataType();
switch (attrType)
{
case BOOL:
case DATE:
case DATE_TIME:
case DECIMAL:
case EMAIL:
case ENUM:
case HTML:
case HYPERLINK:
case INT:
case LONG:
case SCRIPT:
case STRING:
case TEXT:
sqlBuilder.append(getPostgreSqlType(attr));
break;
case CATEGORICAL:
case FILE:
case XREF:
sqlBuilder.append(getPostgreSqlType(attr.getRefEntity().getIdAttribute()));
break;
case ONE_TO_MANY:
case COMPOUND:
case CATEGORICAL_MREF:
case MREF:
throw new RuntimeException(format("Illegal attribute type [%s]", attrType.toString()));
default:
throw new RuntimeException(format("Unknown attribute type [%s]", attrType.toString()));
}
String sqlColumnConstraints = getSqlColumnConstraints(entityType, attr);
if (!sqlColumnConstraints.isEmpty())
{
sqlBuilder.append(' ').append(sqlColumnConstraints);
}
return sqlBuilder.toString();
}
private static String getSqlColumnConstraints(EntityType entityType, Attribute attr)
{
StringBuilder sqlBuilder = new StringBuilder();
if (!attr.getName().equals(entityType.getIdAttribute().getName()))
{
if (!attr.isNillable())
{
sqlBuilder.append("NOT NULL");
}
}
return sqlBuilder.toString();
}
private static List<String> getSqlTableConstraints(EntityType entityType, Attribute attr)
{
List<String> tableConstraints = Lists.newArrayList();
if (attr.getName().equals(entityType.getIdAttribute().getName()))
{
tableConstraints.add(getSqlConstraintPrimaryKey(entityType, attr));
}
else
{
if (isSingleReferenceType(attr) && isPersistedInPostgreSql(attr.getRefEntity()))
{
tableConstraints.add(getSqlForeignKey(entityType, attr));
}
if (attr.isUnique())
{
tableConstraints.add(getSqlUniqueKey(entityType, attr));
}
if (attr.getDataType() == ENUM)
{
tableConstraints.add(getSqlCheckConstraint(entityType, attr));
}
}
return tableConstraints;
}
private static String getSqlDropTable(String tableName)
{
return "DROP TABLE " + tableName;
}
private static <E extends Entity> String getSqlWhere(EntityType entityType, Query<E> q, List<Object> parameters,
int mrefFilterIndex)
{
StringBuilder result = new StringBuilder();
for (QueryRule r : q.getRules())
{
Attribute attr = null;
if (r.getField() != null)
{
attr = entityType.getAttribute(r.getField());
if (attr == null)
{
throw new MolgenisDataException(format("Unknown attribute [%s]", r.getField()));
}
if (isPersistedInOtherTable(attr))
{
mrefFilterIndex++;
}
}
StringBuilder predicate = new StringBuilder();
Operator operator = r.getOperator();
switch (operator)
{
case AND:
result.append(" AND ");
break;
case NESTED:
QueryImpl<Entity> nestedQ = new QueryImpl<>(r.getNestedRules());
result.append('(').append(getSqlWhere(entityType, nestedQ, parameters, mrefFilterIndex))
.append(')');
break;
case OR:
result.append(" OR ");
break;
case LIKE:
String columnName;
if (isPersistedInOtherTable(attr))
{
columnName = getFilterColumnName(attr, mrefFilterIndex);
}
else
{
columnName = "this." + getColumnName(attr);
}
if (isStringType(attr) || isTextType(attr))
{
result.append(' ').append(columnName);
}
else
{
result.append(" CAST(").append(columnName).append(" as TEXT)");
}
result.append(" LIKE ?");
parameters.add("%" + PostgreSqlUtils.getPostgreSqlQueryValue(r.getValue(), attr) + '%');
break;
case IN:
Object inValue = r.getValue();
if (inValue == null)
{
throw new MolgenisDataException("Missing value for IN query");
}
if (!(inValue instanceof Iterable<?>))
{
throw new MolgenisDataException(format("IN value is of type [%s] instead of [Iterable]",
inValue.getClass().getSimpleName()));
}
StringBuilder in = new StringBuilder();
Attribute inAttr = attr;
Stream<Object> postgreSqlIds = stream(((Iterable<?>) inValue).spliterator(), false)
.map(idValue -> PostgreSqlUtils.getPostgreSqlQueryValue(idValue, inAttr));
for (Iterator<Object> it = postgreSqlIds.iterator(); it.hasNext(); )
{
Object postgreSqlId = it.next();
in.append('?');
if (it.hasNext())
{
in.append(',');
}
parameters.add(postgreSqlId);
}
if (isPersistedInOtherTable(attr))
{
result.append(getFilterColumnName(attr, mrefFilterIndex));
}
else
{
result.append("this");
}
result.append('.').append(getColumnName(r.getField())).append(" IN (").append(in).append(')');
break;
case NOT:
result.append(" NOT ");
break;
case RANGE:
Object range = r.getValue();
if (range == null)
{
throw new MolgenisDataException("Missing value for RANGE query");
}
if (!(range instanceof Iterable<?>))
{
throw new MolgenisDataException(format("RANGE value is of type [%s] instead of [Iterable]",
range.getClass().getSimpleName()));
}
Iterator<?> rangeValues = ((Iterable<?>) range).iterator();
parameters.add(rangeValues.next()); // from
parameters.add(rangeValues.next()); // to
StringBuilder column = new StringBuilder();
if (isPersistedInOtherTable(attr))
{
column.append(getFilterColumnName(attr, mrefFilterIndex));
}
else
{
column.append("this");
}
column.append('.').append(getColumnName(r.getField()));
predicate.append(column).append(" >= ? AND ").append(column).append(" <= ?");
result.append(predicate);
break;
case EQUALS:
if (attr == null)
{
throw new MolgenisDataException("Missing attribute field in EQUALS query rule");
}
if (isPersistedInOtherTable(attr))
{
predicate.append(getFilterColumnName(attr, mrefFilterIndex));
}
else
{
predicate.append("this");
}
String attrName;
if (attr.isMappedBy())
{
attrName = attr.getRefEntity().getIdAttribute().getName();
}
else
{
attrName = r.getField();
}
predicate.append('.').append(getColumnName(attrName));
if (r.getValue() == null)
{
// expression = null is not valid, use IS NULL
predicate.append(" IS NULL ");
}
else
{
Object postgreSqlVal = PostgreSqlUtils.getPostgreSqlQueryValue(r.getValue(), attr);
//Postgres does not return the rows with an empty value in a boolean field when queried with for example "... NOT abstract = TRUE"
//It does however return those rows when queried with "... NOT abstract IS TRUE"
if (attr.getDataType() == BOOL)
{
Boolean bool = (Boolean) postgreSqlVal;
//noinspection ConstantConditions (getPostgreSqlQueryValue() != null if r.getValue() != null)
if (bool) predicate.append(" IS TRUE");
else predicate.append(" IS FALSE");
}
else
{
predicate.append(" =");
predicate.append(" ? ");
parameters.add(postgreSqlVal);
}
}
if (result.length() > 0 && !result.toString().endsWith(" OR ") && !result.toString()
.endsWith(" AND ") && !result.toString().endsWith(" NOT "))
{
result.append(" AND ");
}
result.append(predicate);
break;
case GREATER:
case GREATER_EQUAL:
case LESS:
case LESS_EQUAL:
if (isPersistedInOtherTable(attr))
{
predicate.append(getFilterColumnName(attr, mrefFilterIndex));
}
else
{
predicate.append("this");
}
predicate.append('.').append(getColumnName(r.getField()));
switch (operator)
{
case GREATER:
predicate.append(" >");
break;
case GREATER_EQUAL:
predicate.append(" >=");
break;
case LESS:
predicate.append(" <");
break;
case LESS_EQUAL:
predicate.append(" <=");
break;
// $CASES-OMITTED$
default:
throw new RuntimeException(format("Unexpected query operator [%s]", operator));
}
predicate.append(" ? ");
parameters.add(PostgreSqlUtils.getPostgreSqlQueryValue(r.getValue(), attr));
if (result.length() > 0 && !result.toString().endsWith(" OR ") && !result.toString()
.endsWith(" AND ") && !result.toString().endsWith(" NOT "))
{
result.append(" AND ");
}
result.append(predicate);
break;
case DIS_MAX:
case FUZZY_MATCH:
case FUZZY_MATCH_NGRAM:
case SEARCH:
case SHOULD:
// PostgreSQL does not support semantic searching and sorting matching rows on relevance.
throw new UnsupportedOperationException(
format("Query operator [%s] not supported by PostgreSQL repository", operator.toString()));
default:
throw new RuntimeException(format("Unknown query operator [%s]", operator.toString()));
}
}
return result.toString().trim();
}
private static <E extends Entity> String getSqlSort(EntityType entityType, Query<E> q)
{
StringBuilder sortSql = new StringBuilder();
if (q.getSort() != null)
{
for (Sort.Order o : q.getSort())
{
Attribute attr = entityType.getAttribute(o.getAttr());
if (isPersistedInOtherTable(attr))
{
sortSql.append(", ").append(getColumnName(attr));
}
else
{
sortSql.append(", ").append(getColumnName(attr));
}
if (o.getDirection().equals(Sort.Direction.DESC))
{
sortSql.append(" DESC");
}
else
{
sortSql.append(" ASC");
}
}
if (sortSql.length() > 0)
{
sortSql = new StringBuilder("ORDER BY ").append(sortSql.substring(2));
}
}
return sortSql.toString();
}
private static <E extends Entity> String getSqlFrom(EntityType entityType, Query<E> q)
{
List<Attribute> mrefAttrsInQuery = getJoinQueryAttrs(entityType, q);
StringBuilder from = new StringBuilder(" FROM ").append(getTableName(entityType)).append(" AS this");
Attribute idAttribute = entityType.getIdAttribute();
for (int i = 0; i < mrefAttrsInQuery.size(); i++)
{
// extra join so we can filter on the mrefs
Attribute mrefAttr = mrefAttrsInQuery.get(i);
if (mrefAttr.getDataType() == ONE_TO_MANY && mrefAttr.isMappedBy())
{
// query table of referenced entity
from.append(" LEFT JOIN ").append(getTableName(mrefAttr.getRefEntity())).append(" AS ")
.append(getFilterColumnName(mrefAttr, i + 1)).append(" ON (this.")
.append(getColumnName(idAttribute)).append(" = ").append(getFilterColumnName(mrefAttr, i + 1))
.append('.').append(getColumnName(mrefAttr.getMappedBy())).append(')');
}
else
{
// query junction table
from.append(" LEFT JOIN ").append(getJunctionTableName(entityType, mrefAttr)).append(" AS ")
.append(getFilterColumnName(mrefAttr, i + 1)).append(" ON (this.")
.append(getColumnName(idAttribute)).append(" = ").append(getFilterColumnName(mrefAttr, i + 1))
.append('.').append(getColumnName(idAttribute)).append(')');
}
}
return from.toString();
}
private static String getColumnName(Attribute attr)
{
return getColumnName(attr.getName());
}
private static String getColumnName(String attrName)
{
return '"' + attrName + '"';
}
private static String getFilterColumnName(Attribute attr, int filterIndex)
{
return '"' + attr.getName() + "_filter" + filterIndex + '"';
}
private static String getPrimaryKeyName(EntityType entityType, Attribute attr)
{
return getConstraintName(entityType, attr, "pkey");
}
private static String getForeignKeyName(EntityType entityType, Attribute attr)
{
return getConstraintName(entityType, attr, "fkey");
}
private static String getUniqueKeyName(EntityType entityType, Attribute attr)
{
return getConstraintName(entityType, attr, "key");
}
private static String getCheckConstraintName(EntityType entityType, Attribute attr)
{
return getConstraintName(entityType, attr, "chk");
}
private static String getConstraintName(EntityType entityType, Attribute attr, String constraintPostfix)
{
return '"' + entityType.getName() + '_' + attr.getName() + '_' + constraintPostfix + '"';
}
private static <E extends Entity> List<Attribute> getJoinQueryAttrs(EntityType entityType, Query<E> q)
{
List<Attribute> joinAttrs = Lists.newArrayList();
getJoinQueryAttrsRec(entityType, q.getRules(), joinAttrs);
return joinAttrs;
}
private static void getJoinQueryAttrsRec(EntityType entityType, List<QueryRule> rules, List<Attribute> joinAttrs)
{
for (QueryRule rule : rules)
{
if (rule.getField() != null)
{
Attribute attr = entityType.getAttribute(rule.getField());
if (attr != null && isPersistedInOtherTable(attr))
{
joinAttrs.add(attr);
}
}
if (rule.getNestedRules() != null && !rule.getNestedRules().isEmpty())
{
getJoinQueryAttrsRec(entityType, rule.getNestedRules(), joinAttrs);
}
}
}
private static String getPostgreSqlType(Attribute attr)
{
while (true)
{
AttributeType attrType = attr.getDataType();
switch (attrType)
{
case BOOL:
return "boolean";
case CATEGORICAL:
case XREF:
case FILE:
attr = attr.getRefEntity().getIdAttribute();
continue;
case DATE:
return "date";
case DATE_TIME:
return "timestamp";
case DECIMAL:
return "double precision"; // alias: float8
case EMAIL:
case ENUM:
case HYPERLINK:
case STRING:
return "character varying(255)"; // alias: varchar(255)
case HTML:
case SCRIPT:
case TEXT:
return "text";
case INT:
return "integer"; // alias: int, int4
case LONG:
return "bigint"; // alias: int8
case CATEGORICAL_MREF:
case MREF:
case ONE_TO_MANY:
case COMPOUND:
throw new RuntimeException(format("Illegal attribute type [%s]", attrType.toString()));
default:
throw new RuntimeException(format("Unknown attribute type [%s]", attrType.toString()));
}
}
}
}