package com.alvazan.orm.layer5.query;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import com.alvazan.orm.api.z5api.IndexColumnInfo;
import com.alvazan.orm.api.z5api.NoSqlSession;
import com.alvazan.orm.api.z5api.SpiQueryAdapter;
import com.alvazan.orm.api.z8spi.Key;
import com.alvazan.orm.api.z8spi.KeyValue;
import com.alvazan.orm.api.z8spi.Row;
import com.alvazan.orm.api.z8spi.ScanInfo;
import com.alvazan.orm.api.z8spi.action.IndexColumn;
import com.alvazan.orm.api.z8spi.conv.ByteArray;
import com.alvazan.orm.api.z8spi.conv.StandardConverters;
import com.alvazan.orm.api.z8spi.iter.AbstractCursor;
import com.alvazan.orm.api.z8spi.iter.DirectCursor;
import com.alvazan.orm.api.z8spi.iter.IterableWrappingCursor;
import com.alvazan.orm.api.z8spi.meta.DboColumnMeta;
import com.alvazan.orm.api.z8spi.meta.DboTableMeta;
import com.alvazan.orm.api.z8spi.meta.ViewInfo;
import com.alvazan.orm.parser.antlr.ChildSide;
import com.alvazan.orm.parser.antlr.ExpressionNode;
import com.alvazan.orm.parser.antlr.JoinInfo;
import com.alvazan.orm.parser.antlr.JoinMeta;
import com.alvazan.orm.parser.antlr.JoinType;
import com.alvazan.orm.parser.antlr.NoSqlLexer;
import com.alvazan.orm.parser.antlr.ParsedNode;
import com.alvazan.orm.parser.antlr.PartitionMeta;
import com.alvazan.orm.parser.antlr.StateAttribute;
import com.alvazan.orm.parser.antlr.ViewInfoImpl;
public class SpiIndexQueryImpl implements SpiQueryAdapter {
private SpiMetaQueryImpl spiMeta;
private NoSqlSession session;
private Map<String, ByteArray> parameters = new HashMap<String, ByteArray>();
private Integer batchSize = null;
public void setup(SpiMetaQueryImpl spiMetaQueryImpl, NoSqlSession session) {
this.spiMeta = spiMetaQueryImpl;
this.session = session;
}
@Override
public void setParameter(String parameterName, byte[] value) {
ByteArray val = new ByteArray(value);
parameters.put(parameterName, val);
}
private ByteArray getParameter(String parameterName) {
ByteArray result = parameters.get(parameterName);
if(result == null)
throw new IllegalStateException("You did not call query.setParameter(\""+parameterName+"\", <yourvalue>) and that parameter is required");
return result;
}
@Override
public DirectCursor<IndexColumnInfo> getResultList(Set<ViewInfo> alreadyJoinedViews, String indexedColumn) {
if(alreadyJoinedViews == null || alreadyJoinedViews.size() != 0)
throw new IllegalArgumentException("You must pass us a non-null Set that is EMPTY and not null");
DirectCursor<IndexColumnInfo> cursor = getResultListImpl(alreadyJoinedViews, indexedColumn);
return cursor;
}
public DirectCursor<IndexColumnInfo> getResultListImpl(Set<ViewInfo> alreadyJoinedViews, String indexedColumn) {
ExpressionNode root = spiMeta.getASTTree();
if(root == null) {
ViewInfoImpl tableInfo = (ViewInfoImpl) spiMeta.getTargetViews().get(0);
DboTableMeta tableMeta = tableInfo.getTableMeta();
PartitionMeta partitionMeta = tableInfo.getPartition();
DboColumnMeta partColMeta = null;
if (partitionMeta != null)
partColMeta = partitionMeta.getPartitionColumn();
DboColumnMeta metaCol = tableMeta.getAnyIndex(indexedColumn, partColMeta);
ScanInfo scanInfo = createScanInfo(tableInfo, metaCol);
alreadyJoinedViews.add(tableInfo);
AbstractCursor<IndexColumn> scan = session.scanIndex(scanInfo, null, null, batchSize);
return processKeys(tableInfo, null, scan);
}
return processExpressionTree(root, alreadyJoinedViews);
}
private ScanInfo createScanInfo(ViewInfoImpl tableInfo, DboColumnMeta metaCol) {
PartitionMeta partitionMeta = tableInfo.getPartition();
String partitionBy = null;
String partitionId = null;
if(partitionMeta != null) {
DboColumnMeta colMeta = partitionMeta.getPartitionColumn();
partitionBy = colMeta.getColumnName();
byte[] partId = retrieveValue(colMeta, (ExpressionNode) partitionMeta.getNode());
Object partIdObj = colMeta.convertFromStorage2(partId);
partitionId = colMeta.convertTypeToString(partIdObj);
}
ScanInfo scanInfo = ScanInfo.createScanInfo(metaCol, partitionBy, partitionId);
return scanInfo;
}
private DirectCursor<IndexColumnInfo> processExpressionTree(ExpressionNode parent, Set<ViewInfo> alreadyJoinedViews) {
int type = parent.getType();
switch (type) {
case NoSqlLexer.AND:
case NoSqlLexer.OR:
return processAndOr(parent, alreadyJoinedViews);
case NoSqlLexer.EQ:
case NoSqlLexer.NE:
case NoSqlLexer.GT:
case NoSqlLexer.LT:
case NoSqlLexer.GE:
case NoSqlLexer.LE:
case NoSqlLexer.BETWEEN:
case NoSqlLexer.IN:
return processRangeExpression(parent, alreadyJoinedViews);
default:
throw new UnsupportedOperationException("bug, unsupported type="+type);
}
}
private DirectCursor<IndexColumnInfo> processAndOr(ExpressionNode root, Set<ViewInfo> alreadyJoinedViews) {
ExpressionNode left = root.getChild(ChildSide.LEFT);
ExpressionNode right = root.getChild(ChildSide.RIGHT);
DirectCursor<IndexColumnInfo> leftResults = processExpressionTree(left, alreadyJoinedViews);
DirectCursor<IndexColumnInfo> rightResults = processExpressionTree(right, alreadyJoinedViews);
JoinMeta joinMeta = left.getJoinMeta();
ViewInfo leftView = joinMeta.getPrimaryJoinInfo().getPrimaryTable();
JoinMeta joinMeta2 = right.getJoinMeta();
ViewInfo rightView = joinMeta2.getPrimaryJoinInfo().getPrimaryTable();
JoinType joinType = root.getJoinMeta().getJoinType();
if(joinType == JoinType.INNER || joinType == JoinType.LEFT_OUTER) {
//We need to proxy the right results to translate to the same primary key as the
//left results and our And and Or Cursor can then take care of the rest
JoinInfo joinInfo = root.getJoinMeta().getPrimaryJoinInfo();
ViewInfoImpl newView = joinInfo.getPrimaryTable();
DboColumnMeta col = joinInfo.getPrimaryCol();
ScanInfo scanInfo = createScanInfo(newView, col);
//FROM an ORM perspective, we join to smaller tables in general as we don't want to blow out memory so do the
//join first(ie. we process left sides first in and and or cursors)
CursorForJoin temp = new CursorForJoin(newView, leftView, leftResults, joinType);
temp.setColMeta(col);
temp.setScanInfo(scanInfo);
temp.setSession(session);
temp.setBatchSize(batchSize);
leftResults = temp;
leftView = newView;
}
if(root.getType() == NoSqlLexer.AND) {
CursorForAnd cursor = new CursorForAnd(leftView, leftResults, rightView, rightResults);
//AND always returns LESS results(or same) than the left or right sides,
//sooooo, we cache results if there is less than 500 results
return new CachingCursor<IndexColumnInfo>(cursor);
} else {
//Since OR always returns MORE results(or the same) as the left or right views
//There is no need to use a caching cursor as the people below us have a caching
//cursor AND there would be no performance benefit
return new CursorForOr(leftView, leftResults, rightView, rightResults);
}
}
private DirectCursor<IndexColumnInfo> processRangeExpression(ExpressionNode root, Set<ViewInfo> alreadyJoinedViews) {
StateAttribute attr;
if(root.getType() == NoSqlLexer.BETWEEN) {
ExpressionNode grandChild = root.getChild(ChildSide.LEFT).getChild(ChildSide.LEFT);
attr = (StateAttribute) grandChild.getState();
} else if (root.getType() == NoSqlLexer.IN) {
ExpressionNode grandChild = root.getChild(ChildSide.LEFT);
attr = (StateAttribute) grandChild.getState();
} else {
attr = (StateAttribute) root.getChild(ChildSide.LEFT).getState();
}
DboColumnMeta info = attr.getColumnInfo();
ViewInfoImpl viewInfo = attr.getViewInfo();
ScanInfo scanInfo = createScanInfo(viewInfo, info);
alreadyJoinedViews.add(viewInfo);
if(info.isIndexed()) {
//its an indexed column
return processIndexColumn(root, scanInfo, viewInfo, info);
} else if (info.getOwner().getIdColumnMeta().getColumnName().equals(info.getColumnName())) {
//its a non-indexed primary key
return processPrimaryKey(root, scanInfo, viewInfo, info);
} else
throw new IllegalArgumentException("You cannot have '"+info.getColumnName() + "' in your sql query since "+info.getColumnName()+" is neither a Primary Key nor a column with @Index annotation on the field in the entity");
}
private DirectCursor<IndexColumnInfo> processIndexColumn(ExpressionNode root, ScanInfo scanInfo, ViewInfoImpl viewInfo, DboColumnMeta info) {
AbstractCursor<IndexColumn> scan;
if(root.getType() == NoSqlLexer.EQ) {
byte[] data = retrieveValue(info, root.getChild(ChildSide.RIGHT));
Key key = new Key(data, true);
scan = session.scanIndex(scanInfo, key, key, batchSize);
} else if(root.getType() == NoSqlLexer.GT
|| root.getType() == NoSqlLexer.GE
|| root.getType() == NoSqlLexer.LT
|| root.getType() == NoSqlLexer.LE
|| root.isBetweenExpression()) {
Key from = null;
Key to = null;
if(root.isBetweenExpression()) {
ExpressionNode node = root.getGreaterThan();
ExpressionNode node2 = root.getLessThan();
from = createLeftKey(node, info);
to = createRightKey(node2, info);
} else if(root.getType() == NoSqlLexer.GT
|| root.getType() == NoSqlLexer.GE) {
from = createLeftKey(root, info);
} else if(root.getType() == NoSqlLexer.LT) {
to = createRightKey(root, info);
} else
throw new UnsupportedOperationException("not done yet here");
scan = session.scanIndex(scanInfo, from, to, batchSize);
} else if(root.getType() == NoSqlLexer.IN) {
List<byte[]> values = new ArrayList<byte[]>();
List<ParsedNode> keys = root.getChildrenForIn();
for (ParsedNode keyNode : keys) {
byte[] data = retrieveValue(info, (ExpressionNode) keyNode);
byte[] virtualkey = info.getOwner().getIdColumnMeta().formVirtRowKey(data);
values.add(virtualkey);
}
scan = session.scanIndex(scanInfo, values);
} else
throw new UnsupportedOperationException("not supported yet. type="+root.getType());
DirectCursor<IndexColumnInfo> processKeys = processKeys(viewInfo, info, scan);
return processKeys;
}
@SuppressWarnings("unchecked")
private DirectCursor<IndexColumnInfo> processPrimaryKey(ExpressionNode root, ScanInfo scanInfo, ViewInfoImpl viewInfo, DboColumnMeta info) {
AbstractCursor<KeyValue<Row>> scan;
if(root.getType() == NoSqlLexer.EQ) {
byte[] data = retrieveValue(info, root.getChild(ChildSide.RIGHT));
if (data == null)
throw new UnsupportedOperationException("Primary key "+ info.getColumnName() + " cannot be null");
byte[] virtualkey = info.getOwner().getIdColumnMeta().formVirtRowKey(data);
List<byte[]> keyList= new ArrayList<byte[]>();
keyList.add(virtualkey);
scan = session.find(info.getOwner(), new IterableWrappingCursor<byte[]>(keyList), false, true, batchSize);
} else if (root.getType() == NoSqlLexer.IN) {
List<byte[]> keyList = new ArrayList<byte[]>();
List<ParsedNode> keys = root.getChildrenForIn();
for (ParsedNode keyNode : keys) {
byte[] data = retrieveValue(info, (ExpressionNode) keyNode);
byte[] virtualkey = info.getOwner().getIdColumnMeta().formVirtRowKey(data);
keyList.add(virtualkey);
}
scan = session.find(info.getOwner(), new IterableWrappingCursor<byte[]>(keyList), false, true, batchSize);
} else
throw new UnsupportedOperationException("Other operations not supported yet for Primary Key. Use @NoSQLIndexed for Primary Key.type="+root.getType());
DirectCursor<IndexColumnInfo> processKeys = processKeysforPK(viewInfo, info, scan);
return processKeys;
}
private Key createRightKey(ExpressionNode node, DboColumnMeta info) {
byte[] data = retrieveValue(info, node.getChild(ChildSide.RIGHT));
if(node.getType() == NoSqlLexer.LT)
return new Key(data, false);
else if(node.getType() == NoSqlLexer.LE)
return new Key(data, true);
else
throw new RuntimeException("bug, should never happen, but should be easy to fix this one");
}
private Key createLeftKey(ExpressionNode node, DboColumnMeta info) {
byte[] data = retrieveValue(info, node.getChild(ChildSide.RIGHT));
if(node.getType() == NoSqlLexer.GT)
return new Key(data, false);
else if(node.getType() == NoSqlLexer.GE)
return new Key(data, true);
else
throw new RuntimeException("bug, should never happen, but should be easy to fix this one. type="+node.getType());
}
private DirectCursor<IndexColumnInfo> processKeys(ViewInfo viewInfo, DboColumnMeta info, AbstractCursor<IndexColumn> scan) {
DirectCursor<IndexColumnInfo> cursor = new CursorSimpleTranslator(viewInfo, info, scan);
return new CachingCursor<IndexColumnInfo>(cursor);
}
private DirectCursor<IndexColumnInfo> processKeysforPK(ViewInfo viewInfo, DboColumnMeta info, AbstractCursor<KeyValue<Row>> scan) {
DirectCursor<IndexColumnInfo> cursor = new CursorForPrimaryKey(viewInfo, info, scan);
return new CachingCursor<IndexColumnInfo>(cursor);
}
private byte[] retrieveValue(DboColumnMeta info, ExpressionNode node) {
if(node.isParameter()) {
return processParam(info, node);
} else if(node.isConstant()) {
return processConstant(info, node);
} else
throw new UnsupportedOperationException("type not supported="+node.getType());
}
private byte[] processConstant(DboColumnMeta info, ExpressionNode node) {
//constant is either BigDecimal, BigInteger or a String
Object constant = node.getState();
if (info.isJodaType()) {
DateTimeFormatter fmt;
if (info.getClassType().getName().equals("org.joda.time.DateTime")) {
fmt = ISODateTimeFormat.dateTime();
DateTime dateTime = fmt.parseDateTime(constant.toString());
return StandardConverters.convertToBytes(dateTime);
} else if (info.getClassType().getName().equals("org.joda.time.LocalDateTime")) {
fmt = ISODateTimeFormat.dateTime();
LocalDateTime localDateTime = fmt.parseLocalDateTime(constant.toString());
return StandardConverters.convertToBytes(localDateTime);
} else if (info.getClassType().getName().equals("org.joda.time.LocalDate")) {
fmt = ISODateTimeFormat.date();
LocalDate localDate = fmt.parseLocalDate(constant.toString());
return StandardConverters.convertToBytes(localDate);
} else if (info.getClassType().getName().equals("org.joda.time.LocalTime")) {
fmt = ISODateTimeFormat.time();
LocalTime localTime = fmt.parseLocalTime(constant.toString());
return StandardConverters.convertToBytes(localTime);
}
}
return info.convertToStorage2(constant);
}
private byte[] processParam(DboColumnMeta info, ExpressionNode node) {
String paramName = (String) node.getState();
ByteArray val = getParameter(paramName);
return val.getKey();
}
@Override
public void setBatchSize(int batchSize) {
if(batchSize <= 0)
throw new IllegalArgumentException("batchSize must be 1 or greater, but really, please don't use 1, use something like 500(the default anyways)");
this.batchSize = batchSize;
}
}