package net.notdot.bdbdatastore.server; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import net.notdot.protorpc.RpcFailedError; import com.google.appengine.datastore_v3.DatastoreV3; import com.google.appengine.entity.Entity; import com.google.protobuf.ByteString; public class QuerySpec { public static final ByteString KEY_PROPERTY = ByteString.copyFromUtf8("__key__"); protected String app; protected ByteString kind; protected Entity.Reference ancestor = null; protected Map<ByteString, List<FilterSpec>> filters = new HashMap<ByteString, List<FilterSpec>>(); protected List<DatastoreV3.Query.Order> orders = new ArrayList<DatastoreV3.Query.Order>(); protected int offset = 0; protected int limit = -1; // Used for matching indexes to queries private Entity.Index index = null; private List<ByteString> unordered_properties = null; private List<DatastoreV3.Query.Order> ordered_properties = null; private boolean hasInequalities = false; public static QuerySpec build(DatastoreV3.Query query) { QuerySpec qs = new QuerySpec(); qs.app = query.getApp(); if(!query.hasKind()) throw new RpcFailedError("Queries must specify a kind", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); qs.kind = query.getKind(); if(query.hasAncestor()) qs.ancestor = query.getAncestor(); if(query.hasOffset()) qs.offset = query.getOffset(); if(query.hasLimit()) qs.limit = query.getLimit(); qs.filters = FilterSpec.FromQuery(query); qs.orders = new ArrayList<DatastoreV3.Query.Order>(query.getOrderList()); // Eliminate any orders that match equality filters for(Iterator<DatastoreV3.Query.Order> iter = qs.orders.iterator(); iter.hasNext();) { DatastoreV3.Query.Order order = iter.next(); List<FilterSpec> orderFilters = qs.filters.get(order.getProperty()); if(orderFilters != null) for(FilterSpec filter : orderFilters) if(filter.getOperator() == DatastoreV3.Query.Filter.Operator.EQUAL.getNumber()) iter.remove(); } return qs; } private QuerySpec() { } private void buildMatchData() { if(unordered_properties != null) return; // Build a list of properties used in equality filters. // We don't care what order they appear in the index, as long as it's // before any of the ordered properties. unordered_properties = new ArrayList<ByteString>(); ByteString inequality_property = null; for(Map.Entry<ByteString, List<FilterSpec>> entry : this.filters.entrySet()) { for(FilterSpec filter : entry.getValue()) { if(filter.operator == DatastoreV3.Query.Filter.Operator.EQUAL.getNumber()) { unordered_properties.add(filter.name); } else { inequality_property = filter.name; } } } Collections.sort(unordered_properties, ByteStringComparator.instance); // Build a list of properties used in sort orders and inequality filters. if(this.orders.size() == 0 && inequality_property != null) { ordered_properties = new ArrayList<DatastoreV3.Query.Order>(); ordered_properties.add(DatastoreV3.Query.Order.newBuilder() .setProperty(inequality_property) .setDirection(DatastoreV3.Query.Order.Direction.ASCENDING.getNumber()) .build()); } else { ordered_properties = this.orders; } } public boolean isValidIndex(Entity.Index index) { if(this.filters == null) return false; this.buildMatchData(); if(!index.getEntityType().equals(this.kind)) return false; if(unordered_properties.size() + ordered_properties.size() != index.getPropertyCount()) return false; // Check that properties match as expected int unordered_count = unordered_properties.size(); ByteString[] index_unordered_props = new ByteString[unordered_count]; for(int i = 0; i < index.getPropertyCount(); i++) { Entity.Index.Property prop = index.getProperty(i); if(i < unordered_count) { // Check the list of unordered properties matches later index_unordered_props[i] = prop.getName(); } else { // Ordered property must have the same name and sort order DatastoreV3.Query.Order orderprop = ordered_properties.get(i - unordered_count); if(!orderprop.getProperty().equals(prop.getName())) return false; if(i > unordered_count || this.orders.size() > 0) { // ... but we only care about order if there are sort orders if(orderprop.getDirection() != prop.getDirection().getNumber()) return false; } } } // Check that the unordered properties from index and query match Arrays.sort(index_unordered_props, ByteStringComparator.instance); for(int i = 0; i < unordered_count; i++) if(!unordered_properties.get(i).equals(index_unordered_props[i])) return false; return true; } public Entity.Index getIndex() { if(this.filters == null) return null; if(this.index == null) { Entity.Index.Builder builder = Entity.Index.newBuilder(); builder.setEntityType(this.kind); builder.setAncestor(this.ancestor != null); ByteString inequalityprop = null; // Add all equality filters for(List<FilterSpec> filterList : this.filters.values()) { for(FilterSpec filter : filterList) { if(filter.getOperator() == DatastoreV3.Query.Filter.Operator.EQUAL.getNumber()) { builder.addProperty(Entity.Index.Property.newBuilder() .setName(filter.getName()) .setDirection(Entity.Index.Property.Direction.ASCENDING)); } else { this.hasInequalities = true; if(inequalityprop != null && !filter.getName().equals(inequalityprop)) throw new RpcFailedError("Only one inequality property is permitted per query", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); inequalityprop = filter.getName(); } } } if(inequalityprop != null && this.orders.size() > 0 && !this.orders.get(0).getProperty().equals(inequalityprop)) throw new RpcFailedError("First sort order must match inequality property", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); // If there's no sort orders, add the inequality, ascending if(inequalityprop != null && this.orders.size() == 0) builder.addProperty(Entity.Index.Property.newBuilder().setName(inequalityprop)); // Add the sort orders for(DatastoreV3.Query.Order order : this.orders) { builder.addProperty(Entity.Index.Property.newBuilder() .setName(order.getProperty()) .setDirection(Entity.Index.Property.Direction.valueOf(order.getDirection()))); } this.index = builder.build(); } return this.index; } public boolean getBounds(Entity.Index idx, int direction, List<Entity.PropertyValue> bounds) { if(this.filters == null) return false; boolean exclusiveBound = false; Map<ByteString, Iterator<FilterSpec>> filters = new HashMap<ByteString, Iterator<FilterSpec>>(); // Create an iterator for each item in the list for(Map.Entry<ByteString, List<FilterSpec>> entry : this.filters.entrySet()) filters.put(entry.getKey(), entry.getValue().iterator()); // Iterate through each property in the index for(Entity.Index.Property prop : idx.getPropertyList()) { boolean filtered = false; // Find the effective sort direction int currentDirection = direction * (prop.getDirection()==Entity.Index.Property.Direction.ASCENDING?1:-1); // Get an iterator for all the filters for this property Iterator<FilterSpec> iter = filters.get(prop.getName()); FilterSpec filter = null; if(iter != null) { filters: // Iterate over the filters until we find a filter that fits the slot while(iter.hasNext()) { filter = iter.next(); switch(filter.getOperator()) { case 1: // Less than if(currentDirection == -1) { filtered = true; exclusiveBound = true; break filters; } break; case 2: // Less than or equal if(currentDirection == -1) { filtered = true; exclusiveBound = false; break filters; } break; case 3: // Greater than if(currentDirection == 1) { filtered = true; exclusiveBound = true; break filters; } break; case 4: // Greater than or equal if(currentDirection == 1) { filtered = true; exclusiveBound = false; break filters; } break; case 5: // Equal filtered = true; exclusiveBound = false; break filters; } } } if(filtered) { if(!iter.hasNext()) filters.remove(prop.getName()); bounds.add(filter.getValue()); } else { // First unfiltered property - add a sentinel if it's the upper bound if(currentDirection == -1) bounds.add(Entity.PropertyValue.getDefaultInstance()); return exclusiveBound; } } // TODO: Check for unused filters and error return exclusiveBound; } public String getApp() { return app; } public ByteString getKind() { return kind; } public Entity.Reference getAncestor() { return ancestor; } public boolean hasAncestor() { return ancestor != null; } public boolean hasInequalities() { this.getIndex(); return this.hasInequalities; } public Map<ByteString, List<FilterSpec>> getFilters() { return filters; } public List<DatastoreV3.Query.Order> getOrders() { return orders; } public int getOffset() { return offset; } public int getLimit() { return limit; } }