blob: 32f416b3712f093dd35f3240abff37a3c8eb5c8b [file] [log] [blame]
/*
* 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
}
}