/* * Copyright 2006-2012 Amazon Technologies, Inc. or its affiliates. * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks * of Amazon Technologies, Inc. or its affiliates. All rights reserved. * * Licensed 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 com.amazon.carbonado; import java.io.Closeable; import java.io.IOException; import java.io.Serializable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.FilterValues; /** * Supports complex retrieval and deletion of {@link Storable} objects. * Queries are immutable representations of an action \u2013 they do not * contain any Storable instances. The apparent mutators (with, et al) do not * actually modify the Query. Instead, they return another Query instance which * has the requested modification. To obtain an initial Query instance, call * one of the {@link Storage} query methods. * *

Query objects are usually compiled and cached, and the same instance can * be re-used for future queries. This is possible because queries are * immutable and naturally thread-safe. * * @author Brian S O'Neill */ public interface Query { /** * Returns the specific type of Storable managed by this object. */ Class getStorableType(); /** * Returns the query's filter. */ Filter getFilter(); /** * Returns the query's filter values, which is null if filter has no * parameters. */ FilterValues getFilterValues(); /** * Returns the amount of blank parameters that need to be filled in. If * zero, then this query is ready to be used. */ int getBlankParameterCount(); /** * Returns a copy of this Query with the next blank parameter filled in. * * @param value parameter value to fill in * @throws IllegalStateException if no blank parameters * @throws IllegalArgumentException if type doesn't match */ Query with(int value); /** * Returns a copy of this Query with the next blank parameter filled in. * * @param value parameter value to fill in * @throws IllegalStateException if no blank parameters * @throws IllegalArgumentException if type doesn't match */ Query with(long value); /** * Returns a copy of this Query with the next blank parameter filled in. * * @param value parameter value to fill in * @throws IllegalStateException if no blank parameters * @throws IllegalArgumentException if type doesn't match */ Query with(float value); /** * Returns a copy of this Query with the next blank parameter filled in. * * @param value parameter value to fill in * @throws IllegalStateException if no blank parameters * @throws IllegalArgumentException if type doesn't match */ Query with(double value); /** * Returns a copy of this Query with the next blank parameter filled in. * * @param value parameter value to fill in * @throws IllegalStateException if no blank parameters * @throws IllegalArgumentException if type doesn't match */ Query with(boolean value); /** * Returns a copy of this Query with the next blank parameter filled in. * * @param value parameter value to fill in * @throws IllegalStateException if no blank parameters * @throws IllegalArgumentException if type doesn't match */ Query with(char value); /** * Returns a copy of this Query with the next blank parameter filled in. * * @param value parameter value to fill in * @throws IllegalStateException if no blank parameters * @throws IllegalArgumentException if type doesn't match */ Query with(byte value); /** * Returns a copy of this Query with the next blank parameter filled in. * * @param value parameter value to fill in * @throws IllegalStateException if no blank parameters * @throws IllegalArgumentException if type doesn't match */ Query with(short value); /** * Returns a copy of this Query with the next blank parameter filled in. * * @param value parameter value to fill in * @throws IllegalStateException if no blank parameters * @throws IllegalArgumentException if type doesn't match */ Query with(Object value); /** * Returns a copy of this Query with the next blank parameters filled in. * * @param values parameter values to fill in; if null or empty, this * Query instance is returned * @throws IllegalStateException if no blank parameters or if too many * parameter values supplied * @throws IllegalArgumentException if any type doesn't match */ Query withValues(Object... values); /** * Returns a new query which has another {@link Storage#query(String) * filter} logically "and"ed to this, potentially reducing the amount of * results. * * @param filter query filter expression * @throws FetchException if storage layer throws an exception * @throws IllegalStateException if any blank parameters in this query, or * if this query is already guaranteed to fetch nothing * @throws IllegalArgumentException if filter is null * @throws MalformedFilterException if expression is malformed * @throws UnsupportedOperationException if given filter is unsupported by repository */ Query and(String filter) throws FetchException; /** * Returns a new query which has another {@link Storage#query(String) * filter} logically "and"ed to this, potentially reducing the amount of * results. * * @param filter query filter * @throws FetchException if storage layer throws an exception * @throws IllegalStateException if any blank parameters in this query, or * if this query is already guaranteed to fetch nothing * @throws IllegalArgumentException if filter is null * @throws UnsupportedOperationException if given filter is unsupported by repository */ Query and(Filter filter) throws FetchException; /** * Returns a new query which has another {@link Storage#query(String) * filter} logically "or"ed to this, potentially increasing the amount of * results. * * @param filter query filter expression * @throws FetchException if storage layer throws an exception * @throws IllegalStateException if any blank parameters in this query, or * if this query is already guaranteed to fetch everything * @throws IllegalArgumentException if filter is null * @throws MalformedFilterException if expression is malformed * @throws UnsupportedOperationException if given filter is unsupported by repository */ Query or(String filter) throws FetchException; /** * Returns a new query which has another {@link Storage#query(String) * filter} logically "or"ed to this, potentially increasing the amount of * results. * * @param filter query filter * @throws FetchException if storage layer throws an exception * @throws IllegalStateException if any blank parameters in this query, or * if this query is already guaranteed to fetch everything * @throws IllegalArgumentException if filter is null * @throws UnsupportedOperationException if given filter is unsupported by repository */ Query or(Filter filter) throws FetchException; /** * Returns a new query which produces all the results not supplied in this * query. Any filled in parameters in this query are copied into the new * one. * * @throws FetchException if storage layer throws an exception * @throws UnsupportedOperationException if new query is unsupported by repository */ Query not() throws FetchException; /** * Returns a copy of this query ordered by a specific property value. The * property name may be prefixed with '+' or '-' to indicate ascending or * descending order. If the prefix is omitted, ascending order is assumed. * *

Note: Specification of ordering properties is not cumulative. Calling * this method will first remove any previous ordering properties. * * @param property name of property to order by * @throws FetchException if storage layer throws an exception * @throws IllegalArgumentException if property is null or is not a member * of type S * @throws UnsupportedOperationException if given ordering, combined with * query filter, is unsupported by repository */ Query orderBy(String property) throws FetchException; /** * Returns a copy of this query ordered by specific property values. The * property names may be prefixed with '+' or '-' to indicate ascending or * descending order. If the prefix is omitted, ascending order is assumed. * *

Note: Specification of ordering properties is not cumulative. Calling * this method will first remove any previous ordering properties. * * @param properties names of properties to order by * @throws FetchException if storage layer throws an exception * @throws IllegalArgumentException if any property is null or is not a * member of type S * @throws UnsupportedOperationException if given ordering, combined with * query filter, is unsupported by repository */ Query orderBy(String... properties) throws FetchException; /** * Returns a query which fetches results for this query after a given * starting point, which is useful for re-opening a cursor. This is only * effective when query has been given an explicit {@link #orderBy * ordering}. If not a total ordering, then query may start at an earlier * position. * *

Note: The returned query can be very expensive to fetch from * repeatedly, if the query needs to perform a sort operation. Ideally, the * query ordering should match the natural ordering of an index or key. * * @param start storable to attempt to start after; if null, this query is * returned * @throws IllegalStateException if any blank parameters in this query * @throws FetchException if storage layer throws an exception * @since 1.2 */ Query after(T start) throws FetchException; /** * Fetches results for this query. If any updates or deletes might be * performed on the results, consider enclosing the fetch in a * transaction. This allows the isolation level and "for update" mode to be * adjusted. Some repositories might otherwise deadlock. * * @return fetch results * @throws IllegalStateException if any blank parameters in this query * @throws FetchException if storage layer throws an exception * @see Repository#enterTransaction(IsolationLevel) */ Cursor fetch() throws FetchException; /** * Fetches results for this query. If any updates or deletes might be * performed on the results, consider enclosing the fetch in a * transaction. This allows the isolation level and "for update" mode to be * adjusted. Some repositories might otherwise deadlock. * * @param controller optional controller which can abort query operation * @return fetch results * @throws IllegalStateException if any blank parameters in this query * @throws FetchException if storage layer throws an exception * @see Repository#enterTransaction(IsolationLevel) */ Cursor fetch(Controller controller) throws FetchException; /** * Fetches a slice of results for this query, as defined by a numerical * range. A slice can be used to limit the number of results from a * query. It is strongly recommended that the query be given a total {@link * #orderBy ordering} in order for the slice results to be deterministic. * * @param from zero-based {@code from} record number, inclusive * @param to optional zero-based {@code to} record number, exclusive * @return fetch results * @throws IllegalStateException if any blank parameters in this query * @throws IllegalArgumentException if {@code from} is negative or if * {@code from} is more than {@code to} * @throws FetchException if storage layer throws an exception * @since 1.2 */ Cursor fetchSlice(long from, Long to) throws FetchException; /** * Fetches a slice of results for this query, as defined by a numerical * range. A slice can be used to limit the number of results from a * query. It is strongly recommended that the query be given a total {@link * #orderBy ordering} in order for the slice results to be deterministic. * * @param from zero-based {@code from} record number, inclusive * @param to optional zero-based {@code to} record number, exclusive * @param controller optional controller which can abort query operation * @return fetch results * @throws IllegalStateException if any blank parameters in this query * @throws IllegalArgumentException if {@code from} is negative or if * {@code from} is more than {@code to} * @throws FetchException if storage layer throws an exception * @since 1.2 */ Cursor fetchSlice(long from, Long to, Controller controller) throws FetchException; /** * Fetches results for this query after a given starting point, which is * useful for re-opening a cursor. This is only effective when query has * been given an explicit {@link #orderBy ordering}. If not a total * ordering, then cursor may start at an earlier position. * *

Note: This method can be very expensive to call repeatedly, if the * query needs to perform a sort operation. Ideally, the query ordering * should match the natural ordering of an index or key. * *

Calling {@code fetchAfter(s)} is equivalent to calling {@code * after(s).fetch()}. * * @param start storable to attempt to start after; if null, fetch all results * @return fetch results * @throws IllegalStateException if any blank parameters in this query * @throws FetchException if storage layer throws an exception * @see Repository#enterTransaction(IsolationLevel) * @see #after */ Cursor fetchAfter(T start) throws FetchException; /** * Fetches results for this query after a given starting point, which is * useful for re-opening a cursor. This is only effective when query has * been given an explicit {@link #orderBy ordering}. If not a total * ordering, then cursor may start at an earlier position. * *

Note: This method can be very expensive to call repeatedly, if the * query needs to perform a sort operation. Ideally, the query ordering * should match the natural ordering of an index or key. * *

Calling {@code fetchAfter(s)} is equivalent to calling {@code * after(s).fetch()}. * * @param start storable to attempt to start after; if null, fetch all results * @param controller optional controller which can abort query operation * @return fetch results * @throws IllegalStateException if any blank parameters in this query * @throws FetchException if storage layer throws an exception * @see Repository#enterTransaction(IsolationLevel) * @see #after */ Cursor fetchAfter(T start, Controller controller) throws FetchException; /** * Attempts to load exactly one matching object. If the number of matching * records is zero or exceeds one, then an exception is thrown instead. * * @return a single fetched object * @throws IllegalStateException if any blank parameters in this query * @throws FetchNoneException if no matching record found * @throws FetchMultipleException if more than one matching record found * @throws FetchException if storage layer throws an exception */ S loadOne() throws FetchException; /** * Attempts to load exactly one matching object. If the number of matching * records is zero or exceeds one, then an exception is thrown instead. * * @param controller optional controller which can abort query operation * @return a single fetched object * @throws IllegalStateException if any blank parameters in this query * @throws FetchNoneException if no matching record found * @throws FetchMultipleException if more than one matching record found * @throws FetchException if storage layer throws an exception */ S loadOne(Controller controller) throws FetchException; /** * Tries to load one record, but returns null if nothing was found. Throws * exception if record count is more than one. * * @return null or a single fetched object * @throws IllegalStateException if any blank parameters in this query * @throws FetchMultipleException if more than one matching record found * @throws FetchException if storage layer throws an exception */ S tryLoadOne() throws FetchException; /** * Tries to load one record, but returns null if nothing was found. Throws * exception if record count is more than one. * * @param controller optional controller which can abort query operation * @return null or a single fetched object * @throws IllegalStateException if any blank parameters in this query * @throws FetchMultipleException if more than one matching record found * @throws FetchException if storage layer throws an exception */ S tryLoadOne(Controller controller) throws FetchException; /** * Deletes one matching object. If the number of matching records is zero or * exceeds one, then no delete occurs, and an exception is thrown instead. * * @throws IllegalStateException if any blank parameters in this query * @throws PersistNoneException if no matching record found * @throws PersistMultipleException if more than one record matches * @throws PersistException if storage layer throws an exception */ void deleteOne() throws PersistException; /** * Deletes one matching object. If the number of matching records is zero or * exceeds one, then no delete occurs, and an exception is thrown instead. * * @param controller optional controller which can abort query operation * @throws IllegalStateException if any blank parameters in this query * @throws PersistNoneException if no matching record found * @throws PersistMultipleException if more than one record matches * @throws PersistException if storage layer throws an exception */ void deleteOne(Controller controller) throws PersistException; /** * Deletes zero or one matching objects. If the number of matching records * exceeds one, then no delete occurs, and an exception is thrown instead. * * @return true if record existed and was deleted, or false if no match * @throws IllegalStateException if any blank parameters in this query * @throws PersistMultipleException if more than one record matches * @throws PersistException if storage layer throws an exception */ boolean tryDeleteOne() throws PersistException; /** * Deletes zero or one matching objects. If the number of matching records * exceeds one, then no delete occurs, and an exception is thrown instead. * * @param controller optional controller which can abort query operation * @return true if record existed and was deleted, or false if no match * @throws IllegalStateException if any blank parameters in this query * @throws PersistMultipleException if more than one record matches * @throws PersistException if storage layer throws an exception */ boolean tryDeleteOne(Controller controller) throws PersistException; /** * Deletes zero or more matching objects. There is no guarantee that * deleteAll is an atomic operation. If atomic behavior is desired, wrap * the call in a transaction scope. * * @throws IllegalStateException if any blank parameters in this query * @throws PersistException if storage layer throws an exception */ void deleteAll() throws PersistException; /** * Deletes zero or more matching objects. There is no guarantee that * deleteAll is an atomic operation. If atomic behavior is desired, wrap * the call in a transaction scope. * * @param controller optional controller which can abort query operation * @throws IllegalStateException if any blank parameters in this query * @throws PersistException if storage layer throws an exception */ void deleteAll(Controller controller) throws PersistException; /** * Returns a count of all results matched by this query. Even though no * results are explicitly fetched, this method may still be expensive to * call. The actual performance will vary by repository and available indexes. * * @return count of matches * @throws IllegalStateException if any blank parameters in this query * @throws FetchException if storage layer throws an exception */ long count() throws FetchException; /** * Returns a count of all results matched by this query. Even though no * results are explicitly fetched, this method may still be expensive to * call. The actual performance will vary by repository and available indexes. * * @param controller optional controller which can abort query operation * @return count of matches * @throws IllegalStateException if any blank parameters in this query * @throws FetchException if storage layer throws an exception */ long count(Controller controller) throws FetchException; /** * Returns true if any results are matched by this query. * * @return true if any matches * @throws IllegalStateException if any blank parameters in this query * @throws FetchException if storage layer throws an exception * @since 1.2 */ boolean exists() throws FetchException; /** * Returns true if any results are matched by this query. * * @param controller optional controller which can abort query operation * @return true if any matches * @throws IllegalStateException if any blank parameters in this query * @throws FetchException if storage layer throws an exception * @since 1.2 */ boolean exists(Controller controller) throws FetchException; /** * Print the native query to standard out, which is useful for performance * analysis. Not all repositories have a native query format. An example * native format is SQL. * * @return false if not implemented */ boolean printNative(); /** * Prints the native query to any appendable, which is useful for * performance analysis. Not all repositories have a native query * format. An example native format is SQL. * * @param app append results here * @return false if not implemented */ boolean printNative(Appendable app) throws IOException; /** * Prints the native query to any appendable, which is useful for * performance analysis. Not all repositories have a native query * format. An example native format is SQL. * * @param app append results here * @param indentLevel amount to indent text, zero for none * @return false if not implemented */ boolean printNative(Appendable app, int indentLevel) throws IOException; /** * Prints the query excecution plan to standard out, which is useful for * performance analysis. There is no standard format for query plans, nor * is it a requirement that this method be implemented. * * @return false if not implemented */ boolean printPlan(); /** * Prints the query excecution plan to any appendable, which is useful for * performance analysis. There is no standard format for query plans, nor * is it a requirement that this method be implemented. * * @param app append results here * @return false if not implemented */ boolean printPlan(Appendable app) throws IOException; /** * Prints the query excecution plan to any appendable, which is useful for * performance analysis. There is no standard format for query plans, nor * is it a requirement that this method be implemented. * * @param app append results here * @param indentLevel amount to indent text, zero for none * @return false if not implemented */ boolean printPlan(Appendable app, int indentLevel) throws IOException; int hashCode(); boolean equals(Object obj); /** * Returns a description of the query filter and any other arguments. */ String toString(); /** * Controller instance can be used to abort query operations. * *

Example:

     * Storage<UserInfo> users = ...
     * long count = users.query("name = ?").count(Query.Timeout.seconds(10));
     * 
*/ public static interface Controller extends Serializable, Closeable { /** * Returns a non-negative value if controller imposes an absolute upper * bound on query execution time. */ public long getTimeout(); /** * Returns the unit for the timeout, if applicable. */ public TimeUnit getTimeoutUnit(); /** * Called by query when it begins, possibly multiple times. Implementation * is required to be idempotent and ignore multiple invocations. */ public void begin(); /** * Periodically called by query to determine if it should continue. */ public void continueCheck() throws FetchException; /** * Always called by query when finished, even when it fails. Implementation * is required to be idempotent and ignore multiple invocations. */ public void close(); } /** * Timeout controller, for aborting long running queries. One instance is * good for one timeout. The instance can be shared by multiple queries, if * they are part of a single logical operation. * *

The timeout applies to the entire duration of fetching results, not * just the time spent between individual fetches. A caller which is slowly * processing results can timeout. More sophisticated timeouts can be * implemented using custom Controller implementations. */ public static final class Timeout implements Controller { private static final long serialVersionUID = 1; private static final AtomicLongFieldUpdater endUpdater = AtomicLongFieldUpdater.newUpdater(Timeout.class, "mEndNanos"); /** * Return a new Timeout in nanoseconds. */ public static Timeout nanos(long timeout) { return new Timeout(timeout, TimeUnit.NANOSECONDS); } /** * Return a new Timeout in microseconds. */ public static Timeout micros(long timeout) { return new Timeout(timeout, TimeUnit.MICROSECONDS); } /** * Return a new Timeout in milliseconds. */ public static Timeout millis(long timeout) { return new Timeout(timeout, TimeUnit.MILLISECONDS); } /** * Return a new Timeout in seconds. */ public static Timeout seconds(long timeout) { return new Timeout(timeout, TimeUnit.SECONDS); } /** * Return a new Timeout in minutes. */ public static Timeout minutes(long timeout) { return new Timeout(timeout, TimeUnit.MINUTES); } /** * Return a new Timeout in hours. */ public static Timeout hours(long timeout) { return new Timeout(timeout, TimeUnit.HOURS); } private final long mTimeout; private final TimeUnit mUnit; private volatile transient long mEndNanos; public Timeout(long timeout, TimeUnit unit) { if (timeout < 0) { throw new IllegalArgumentException("Timeout cannot be negative: " + timeout); } if (unit == null && timeout != 0) { throw new IllegalArgumentException ("TimeUnit cannot be null if timeout is non-zero: " + timeout); } mTimeout = timeout; mUnit = unit; } public long getTimeout() { return mTimeout; } public TimeUnit getTimeoutUnit() { return mUnit; } @Override public void begin() { long end = System.nanoTime() + mUnit.toNanos(mTimeout); if (end == 0) { // Handle rare case to ensure atomic compare and set always // works the first time, supporting idempotent calls to this // method. end = 1; } endUpdater.compareAndSet(this, 0, end); } @Override public void continueCheck() throws FetchTimeoutException { long end = mEndNanos; if (end == 0) { // Begin was not called, in violation of how the Controller // must be used. Be lenient and begin now. begin(); end = mEndNanos; } // Subtract to support modulo comparison. if ((System.nanoTime() - end) >= 0) { throw new FetchTimeoutException("Timed out: " + mTimeout + ' ' + mUnit); } } @Override public void close() { // Nothing to do. } @Override public String toString() { return "Query.Timeout {timeout=" + mTimeout + ", unit=" + mUnit + '}'; } } }