| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with this |
| * work for additional information regarding copyright ownership. The ASF |
| * licenses this file to You under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.hugegraph.backend.query; |
| |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| import org.apache.hugegraph.backend.BackendException; |
| import org.apache.hugegraph.backend.id.Id; |
| import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc; |
| import org.apache.hugegraph.exception.LimitExceedException; |
| import org.apache.hugegraph.structure.HugeElement; |
| import org.apache.hugegraph.type.HugeType; |
| import org.apache.hugegraph.type.define.CollectionType; |
| import org.apache.hugegraph.type.define.HugeKeys; |
| import org.apache.hugegraph.util.CollectionUtil; |
| import org.apache.hugegraph.util.E; |
| import org.apache.hugegraph.util.InsertionOrderUtil; |
| import org.apache.hugegraph.util.collection.IdSet; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| |
| public class Query implements Cloneable { |
| |
| // TODO: we should better not use Long.Max as the unify limit number |
| public static final long NO_LIMIT = Long.MAX_VALUE; |
| |
| public static final long COMMIT_BATCH = 500L; |
| public static final long QUERY_BATCH = 100L; |
| |
| public static final long NO_CAPACITY = -1L; |
| public static final long DEFAULT_CAPACITY = 800000L; // HugeGraph-777 |
| |
| private static final ThreadLocal<Long> CAPACITY_CONTEXT = new ThreadLocal<>(); |
| |
| protected static final Query NONE = new Query(HugeType.UNKNOWN); |
| |
| private static final Set<Id> EMPTY_OLAP_PKS = ImmutableSet.of(); |
| |
| private HugeType resultType; |
| private Map<HugeKeys, Order> orders; |
| private long offset; |
| private long actualOffset; |
| private long actualStoreOffset; |
| private long limit; |
| private String page; |
| private long capacity; |
| private boolean showHidden; |
| private boolean showDeleting; |
| private boolean showExpired; |
| private boolean olap; |
| private Set<Id> olapPks; |
| |
| private Aggregate aggregate; |
| |
| private Query originQuery; |
| |
| public Query(HugeType resultType) { |
| this(resultType, null); |
| } |
| |
| public Query(HugeType resultType, Query originQuery) { |
| this.resultType = resultType; |
| this.originQuery = originQuery; |
| |
| this.orders = null; |
| |
| this.offset = 0L; |
| this.actualOffset = 0L; |
| this.actualStoreOffset = 0L; |
| this.limit = NO_LIMIT; |
| this.page = null; |
| |
| this.capacity = defaultCapacity(); |
| |
| this.showHidden = false; |
| this.showDeleting = false; |
| |
| this.aggregate = null; |
| this.showExpired = false; |
| this.olap = false; |
| this.olapPks = EMPTY_OLAP_PKS; |
| } |
| |
| public void copyBasic(Query query) { |
| E.checkNotNull(query, "query"); |
| this.offset = query.offset(); |
| this.limit = query.limit(); |
| this.page = query.page(); |
| this.capacity = query.capacity(); |
| this.showHidden = query.showHidden(); |
| this.showDeleting = query.showDeleting(); |
| this.aggregate = query.aggregate(); |
| this.showExpired = query.showExpired(); |
| this.olap = query.olap(); |
| if (query.orders != null) { |
| this.orders(query.orders); |
| } |
| } |
| |
| public HugeType resultType() { |
| return this.resultType; |
| } |
| |
| public void resultType(HugeType resultType) { |
| this.resultType = resultType; |
| } |
| |
| public Query originQuery() { |
| return this.originQuery; |
| } |
| |
| public Query rootOriginQuery() { |
| Query root = this; |
| while (root.originQuery != null) { |
| root = root.originQuery; |
| } |
| return root; |
| } |
| |
| protected void originQuery(Query originQuery) { |
| this.originQuery = originQuery; |
| } |
| |
| public Map<HugeKeys, Order> orders() { |
| return Collections.unmodifiableMap(this.getOrNewOrders()); |
| } |
| |
| public void orders(Map<HugeKeys, Order> orders) { |
| this.orders = InsertionOrderUtil.newMap(orders); |
| } |
| |
| public void order(HugeKeys key, Order order) { |
| this.getOrNewOrders().put(key, order); |
| } |
| |
| protected Map<HugeKeys, Order> getOrNewOrders() { |
| if (this.orders != null) { |
| return this.orders; |
| } |
| this.orders = InsertionOrderUtil.newMap(); |
| return this.orders; |
| } |
| |
| public long offset() { |
| return this.offset; |
| } |
| |
| public void offset(long offset) { |
| E.checkArgument(offset >= 0L, "Invalid offset %s", offset); |
| this.offset = offset; |
| } |
| |
| public void copyOffset(Query parent) { |
| assert this.offset == 0L || this.offset == parent.offset; |
| assert this.actualOffset == 0L || |
| this.actualOffset == parent.actualOffset; |
| this.offset = parent.offset; |
| this.actualOffset = parent.actualOffset; |
| } |
| |
| public long actualOffset() { |
| return this.actualOffset; |
| } |
| |
| public void resetActualOffset() { |
| this.actualOffset = 0L; |
| this.actualStoreOffset = 0L; |
| } |
| |
| public long goOffset(long offset) { |
| E.checkArgument(offset >= 0L, "Invalid offset value: %s", offset); |
| if (this.originQuery != null) { |
| this.goParentOffset(offset); |
| } |
| return this.goSelfOffset(offset); |
| } |
| |
| private void goParentOffset(long offset) { |
| assert offset >= 0L; |
| Query parent = this.originQuery; |
| while (parent != null) { |
| parent.actualOffset += offset; |
| parent = parent.originQuery; |
| } |
| } |
| |
| private long goSelfOffset(long offset) { |
| assert offset >= 0L; |
| if (this.originQuery != null) { |
| this.originQuery.goStoreOffsetBySubQuery(offset); |
| } |
| this.actualOffset += offset; |
| return this.actualOffset; |
| } |
| |
| private long goStoreOffsetBySubQuery(long offset) { |
| Query parent = this.originQuery; |
| while (parent != null) { |
| parent.actualStoreOffset += offset; |
| parent = parent.originQuery; |
| } |
| this.actualStoreOffset += offset; |
| return this.actualStoreOffset; |
| } |
| |
| public <T> Set<T> skipOffsetIfNeeded(Set<T> elems) { |
| /* |
| * Skip index(index query with offset) for performance optimization. |
| * We assume one result is returned by each index, but if there are |
| * overridden index it will cause confusing offset and results. |
| */ |
| long fromIndex = this.offset() - this.actualOffset(); |
| if (fromIndex < 0L) { |
| // Skipping offset is overhead, no need to skip |
| fromIndex = 0L; |
| } else if (fromIndex > 0L) { |
| this.goOffset(fromIndex); |
| } |
| E.checkArgument(fromIndex <= Integer.MAX_VALUE, |
| "Offset must be <= 0x7fffffff, but got '%s'", |
| fromIndex); |
| |
| if (fromIndex >= elems.size()) { |
| return ImmutableSet.of(); |
| } |
| long toIndex = this.total(); |
| if (this.noLimit() || toIndex > elems.size()) { |
| toIndex = elems.size(); |
| } |
| if (fromIndex == 0L && toIndex == elems.size()) { |
| return elems; |
| } |
| assert fromIndex < elems.size(); |
| assert toIndex <= elems.size(); |
| return CollectionUtil.subSet(elems, (int) fromIndex, (int) toIndex); |
| } |
| |
| public long remaining() { |
| if (this.limit == NO_LIMIT) { |
| return NO_LIMIT; |
| } else { |
| return this.total() - this.actualOffset(); |
| } |
| } |
| |
| public long total() { |
| if (this.limit == NO_LIMIT) { |
| return NO_LIMIT; |
| } else { |
| return this.offset + this.limit; |
| } |
| } |
| |
| public long limit() { |
| if (this.capacity != NO_CAPACITY) { |
| E.checkArgument(this.limit == Query.NO_LIMIT || |
| this.limit <= this.capacity, |
| "Invalid limit %s, must be <= capacity(%s)", |
| this.limit, this.capacity); |
| } |
| return this.limit; |
| } |
| |
| public void limit(long limit) { |
| E.checkArgument(limit >= 0L || limit == NO_LIMIT, |
| "Invalid limit %s", limit); |
| this.limit = limit; |
| } |
| |
| public boolean noLimit() { |
| return this.limit() == NO_LIMIT; |
| } |
| |
| public boolean noLimitAndOffset() { |
| return this.limit() == NO_LIMIT && this.offset() == 0L; |
| } |
| |
| public boolean reachLimit(long count) { |
| long limit = this.limit(); |
| if (limit == NO_LIMIT) { |
| return false; |
| } |
| return count >= (limit + this.offset()); |
| } |
| |
| /** |
| * Set or update the offset and limit by a range [start, end) |
| * NOTE: it will use the min range one: max start and min end |
| * |
| * @param start the range start, include it |
| * @param end the range end, exclude it |
| */ |
| public long range(long start, long end) { |
| // Update offset |
| long offset = this.offset(); |
| start = Math.max(start, offset); |
| this.offset(start); |
| |
| // Update limit |
| if (end != -1L) { |
| if (!this.noLimit()) { |
| end = Math.min(end, offset + this.limit()); |
| } else { |
| assert end < Query.NO_LIMIT; |
| } |
| E.checkArgument(end >= start, |
| "Invalid range: [%s, %s)", start, end); |
| this.limit(end - start); |
| } else { |
| // Keep the origin limit |
| assert this.limit() <= Query.NO_LIMIT; |
| } |
| return this.limit; |
| } |
| |
| public String page() { |
| if (this.page != null) { |
| E.checkState(this.limit() != 0L, |
| "Can't set limit=0 when using paging"); |
| E.checkState(this.offset() == 0L, |
| "Can't set offset when using paging, but got '%s'", |
| this.offset()); |
| } |
| return this.page; |
| } |
| |
| public String pageWithoutCheck() { |
| return this.page; |
| } |
| |
| public void page(String page) { |
| this.page = page; |
| } |
| |
| public boolean paging() { |
| return this.page != null; |
| } |
| |
| public void olap(boolean olap) { |
| this.olap = olap; |
| } |
| |
| public boolean olap() { |
| return this.olap; |
| } |
| |
| public void olapPks(Set<Id> olapPks) { |
| for (Id olapPk : olapPks) { |
| this.olapPk(olapPk); |
| } |
| } |
| |
| public void olapPk(Id olapPk) { |
| if (this.olapPks == EMPTY_OLAP_PKS) { |
| this.olapPks = new IdSet(CollectionType.EC); |
| } |
| this.olapPks.add(olapPk); |
| } |
| |
| public Set<Id> olapPks() { |
| return this.olapPks; |
| } |
| |
| public long capacity() { |
| return this.capacity; |
| } |
| |
| public void capacity(long capacity) { |
| this.capacity = capacity; |
| } |
| |
| public boolean bigCapacity() { |
| return this.capacity == NO_CAPACITY || this.capacity > DEFAULT_CAPACITY; |
| } |
| |
| public void checkCapacity(long count) throws LimitExceedException { |
| // Throw LimitExceedException if reach capacity |
| if (this.capacity != Query.NO_CAPACITY && count > this.capacity) { |
| final int MAX_CHARS = 256; |
| String query = this.toString(); |
| if (query.length() > MAX_CHARS) { |
| query = query.substring(0, MAX_CHARS) + "..."; |
| } |
| throw new LimitExceedException( |
| "Too many records(must <= %s) for the query: %s", |
| this.capacity, query); |
| } |
| } |
| |
| public Aggregate aggregate() { |
| return this.aggregate; |
| } |
| |
| public Aggregate aggregateNotNull() { |
| E.checkArgument(this.aggregate != null, |
| "The aggregate must be set for number query"); |
| return this.aggregate; |
| } |
| |
| public void aggregate(AggregateFunc func, String property) { |
| this.aggregate = new Aggregate(func, property); |
| } |
| |
| public void aggregate(Aggregate aggregate) { |
| this.aggregate = aggregate; |
| } |
| |
| public boolean showHidden() { |
| return this.showHidden; |
| } |
| |
| public void showHidden(boolean showHidden) { |
| this.showHidden = showHidden; |
| } |
| |
| public boolean showDeleting() { |
| return this.showDeleting; |
| } |
| |
| public void showDeleting(boolean showDeleting) { |
| this.showDeleting = showDeleting; |
| } |
| |
| public boolean showExpired() { |
| return this.showExpired; |
| } |
| |
| public void showExpired(boolean showExpired) { |
| this.showExpired = showExpired; |
| } |
| |
| public Collection<Id> ids() { |
| return ImmutableList.of(); |
| } |
| |
| public Collection<Condition> conditions() { |
| return ImmutableList.of(); |
| } |
| |
| public int idsSize() { |
| return 0; |
| } |
| |
| public int conditionsSize() { |
| return 0; |
| } |
| |
| public boolean empty() { |
| return this.idsSize() == 0 && this.conditionsSize() == 0; |
| } |
| |
| public boolean test(HugeElement element) { |
| return true; |
| } |
| |
| public Query copy() { |
| try { |
| return (Query) this.clone(); |
| } catch (CloneNotSupportedException e) { |
| throw new BackendException(e); |
| } |
| } |
| |
| @Override |
| public boolean equals(Object object) { |
| if (!(object instanceof Query)) { |
| return false; |
| } |
| Query other = (Query) object; |
| return this.resultType.equals(other.resultType) && |
| this.orders().equals(other.orders()) && |
| this.offset == other.offset && |
| this.limit == other.limit && |
| Objects.equals(this.page, other.page) && |
| this.ids().equals(other.ids()) && |
| this.conditions().equals(other.conditions()); |
| } |
| |
| @Override |
| public int hashCode() { |
| return this.resultType.hashCode() ^ |
| this.orders().hashCode() ^ |
| Long.hashCode(this.offset) ^ |
| Long.hashCode(this.limit) ^ |
| Objects.hashCode(this.page) ^ |
| this.ids().hashCode() ^ |
| this.conditions().hashCode(); |
| } |
| |
| @Override |
| public String toString() { |
| Map<String, Object> pairs = InsertionOrderUtil.newMap(); |
| if (this.page != null) { |
| pairs.put("page", String.format("'%s'", this.page)); |
| } |
| if (this.offset != 0) { |
| pairs.put("offset", this.offset); |
| } |
| if (this.limit != NO_LIMIT) { |
| pairs.put("limit", this.limit); |
| } |
| if (!this.orders().isEmpty()) { |
| pairs.put("order by", this.orders()); |
| } |
| |
| StringBuilder sb = new StringBuilder(128); |
| sb.append("`Query "); |
| if (this.aggregate != null) { |
| sb.append(this.aggregate); |
| } else { |
| sb.append('*'); |
| } |
| sb.append(" from ").append(this.resultType); |
| for (Map.Entry<String, Object> entry : pairs.entrySet()) { |
| sb.append(' ').append(entry.getKey()) |
| .append(' ').append(entry.getValue()).append(','); |
| } |
| if (!pairs.isEmpty()) { |
| // Delete last comma |
| sb.deleteCharAt(sb.length() - 1); |
| } |
| |
| if (!this.empty()) { |
| sb.append(" where"); |
| } |
| |
| // Append ids |
| if (!this.ids().isEmpty()) { |
| sb.append(" id in ").append(this.ids()); |
| } |
| |
| // Append conditions |
| if (!this.conditions().isEmpty()) { |
| if (!this.ids().isEmpty()) { |
| sb.append(" and"); |
| } |
| sb.append(" ").append(this.conditions()); |
| } |
| |
| sb.append('`'); |
| return sb.toString(); |
| } |
| |
| public static long defaultCapacity(long capacity) { |
| Long old = CAPACITY_CONTEXT.get(); |
| CAPACITY_CONTEXT.set(capacity); |
| return old != null ? old : DEFAULT_CAPACITY; |
| } |
| |
| public static long defaultCapacity() { |
| Long capacity = CAPACITY_CONTEXT.get(); |
| return capacity != null ? capacity : DEFAULT_CAPACITY; |
| } |
| |
| public static void checkForceCapacity(long count) throws LimitExceedException { |
| if (count > Query.DEFAULT_CAPACITY) { |
| throw new LimitExceedException( |
| "Too many records(must <= %s) for one query", |
| Query.DEFAULT_CAPACITY); |
| } |
| } |
| |
| public enum Order { |
| ASC, |
| DESC |
| } |
| } |