• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.healthconnect.storage.request;
18 
19 import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER;
20 import static com.android.server.healthconnect.storage.utils.StorageUtils.DISTINCT;
21 import static com.android.server.healthconnect.storage.utils.StorageUtils.FROM;
22 import static com.android.server.healthconnect.storage.utils.StorageUtils.LIMIT_SIZE;
23 import static com.android.server.healthconnect.storage.utils.StorageUtils.SELECT;
24 import static com.android.server.healthconnect.storage.utils.StorageUtils.SELECT_ALL;
25 import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
26 
27 import android.annotation.Nullable;
28 import android.annotation.StringDef;
29 import android.health.connect.Constants;
30 import android.util.Slog;
31 
32 import com.android.server.healthconnect.storage.TransactionManager;
33 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper;
34 import com.android.server.healthconnect.storage.utils.OrderByClause;
35 import com.android.server.healthconnect.storage.utils.SqlJoin;
36 import com.android.server.healthconnect.storage.utils.WhereClauses;
37 
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Objects;
43 
44 /**
45  * A request for {@link TransactionManager} to read the DB
46  *
47  * @hide
48  */
49 public class ReadTableRequest {
50     private static final String TAG = "HealthConnectRead";
51     public static final String UNION_ALL = " UNION ALL ";
52     public static final String UNION = " UNION ";
53 
54     /** @hide */
55     @StringDef(
56             value = {
57                 UNION, UNION_ALL,
58             })
59     @Retention(RetentionPolicy.SOURCE)
60     public @interface UnionType {}
61 
62     private final String mTableName;
63     @Nullable private RecordHelper<?> mRecordHelper;
64     @Nullable private List<String> mColumnNames;
65     @Nullable private SqlJoin mJoinClause;
66     private WhereClauses mWhereClauses = new WhereClauses(AND);
67     private boolean mDistinct = false;
68     private OrderByClause mOrderByClause = new OrderByClause();
69     private OrderByClause mFinalOrderByClause = new OrderByClause();
70 
71     // Null means no limit.
72     @Nullable private Integer mLimit = null;
73     @Nullable private Integer mFinalLimit = null;
74     @Nullable private List<ReadTableRequest> mExtraReadRequests;
75     @Nullable private List<ReadTableRequest> mUnionReadRequests;
76     private String mUnionType = UNION_ALL;
77 
ReadTableRequest(String tableName)78     public ReadTableRequest(String tableName) {
79         Objects.requireNonNull(tableName);
80 
81         mTableName = tableName;
82     }
83 
84     /** Returns the record helper associated with this request if it has been set, or null. */
85     @Nullable
getRecordHelper()86     public RecordHelper<?> getRecordHelper() {
87         return mRecordHelper;
88     }
89 
90     /**
91      * Sets the record helper associated with this request. This is not used for creating the SQL,
92      * but is a side channel for other classes processing this request.
93      */
setRecordHelper(RecordHelper<?> recordHelper)94     public ReadTableRequest setRecordHelper(RecordHelper<?> recordHelper) {
95         mRecordHelper = Objects.requireNonNull(recordHelper);
96         return this;
97     }
98 
99     /** Sets the column names to select. */
setColumnNames(List<String> columnNames)100     public ReadTableRequest setColumnNames(List<String> columnNames) {
101         Objects.requireNonNull(columnNames);
102 
103         mColumnNames = columnNames;
104         return this;
105     }
106 
107     /** Sets the WHERE clause to use in this SELECT. */
setWhereClause(WhereClauses whereClauses)108     public ReadTableRequest setWhereClause(WhereClauses whereClauses) {
109         mWhereClauses = whereClauses;
110         return this;
111     }
112 
113     /** Used to set Join Clause for the read query */
setJoinClause(SqlJoin joinClause)114     public ReadTableRequest setJoinClause(SqlJoin joinClause) {
115         mJoinClause = joinClause;
116         return this;
117     }
118 
119     /**
120      * Use this method to enable the Distinct clause in the read command.
121      *
122      * <p><b>NOTE: make sure to use the {@link ReadTableRequest#setColumnNames(List)} to set the
123      * column names to be used as the selection args.</b>
124      */
setDistinctClause(boolean isDistinctValuesRequired)125     public ReadTableRequest setDistinctClause(boolean isDistinctValuesRequired) {
126         mDistinct = isDistinctValuesRequired;
127         return this;
128     }
129 
130     /**
131      * Returns this {@link ReadTableRequest} with union type set. If not set, the default uses
132      * {@link ReadTableRequest#UNION_ALL}.
133      */
setUnionType(@nionType String unionType)134     public ReadTableRequest setUnionType(@UnionType String unionType) {
135         Objects.requireNonNull(unionType);
136         mUnionType = unionType;
137         return this;
138     }
139 
140     /**
141      * Returns SQL statement to perform read operation.
142      *
143      * @throws IllegalArgumentException if the {@link ReadTableRequest} does not have a join clause
144      *     and has both a {@link #setOrderBy} and {@link #setFinalOrderBy}, or if it does not have a
145      *     join clause but has both a {@link #setLimit} and {@link #setFinalLimit} as this would
146      *     lead to an invalid query.
147      */
getReadCommand()148     public String getReadCommand() {
149         return getReadCommand(/* asCount= */ false);
150     }
151 
152     /**
153      * Returns an SQL statement that performs a count of the number of items that would be returned
154      * by the read operation.
155      *
156      * <p>The SQL result will have a single row, single column with the count of rows as the integer
157      * value in that first row, first column.
158      *
159      * @throws IllegalArgumentException if the {@link ReadTableRequest} does not have a join clause
160      *     and has both a {@link #setOrderBy} and {@link #setFinalOrderBy}, or if it does not have a
161      *     join clause but has both a {@link #setLimit} and {@link #setFinalLimit} as this would
162      *     lead to an invalid query.
163      */
getCountCommand()164     public String getCountCommand() {
165         return getReadCommand(/* asCount= */ true);
166     }
167 
168     /**
169      * Returns the SQL for this request.
170      *
171      * @param asCount if true, the SQL returns the count of the results, if false returns the
172      *     results
173      */
getReadCommand(boolean asCount)174     private String getReadCommand(boolean asCount) {
175         if (mUnionReadRequests != null && !mUnionReadRequests.isEmpty()) {
176             StringBuilder builder = new StringBuilder();
177             if (asCount) {
178                 builder.append("SELECT COUNT(*) FROM (");
179             }
180             for (ReadTableRequest unionReadRequest : mUnionReadRequests) {
181                 builder.append("SELECT * FROM (");
182                 builder.append(unionReadRequest.getReadCommand());
183                 builder.append(")");
184                 builder.append(mUnionType);
185             }
186             // For a union request we have to do the count outside the query.
187             builder.append(getReadQuery(/* asCount= */ false));
188             if (asCount) {
189                 builder.append(")");
190             }
191             return builder.toString();
192         } else {
193             return getReadQuery(asCount);
194         }
195     }
196 
197     /**
198      * Returns the SQL for this request, ignoring union read requests.
199      *
200      * @param asCount if true, the SQL returns the count of the results, if false returns the
201      *     results
202      */
getReadQuery(boolean asCount)203     private String getReadQuery(boolean asCount) {
204         String selectStatement = buildSelectStatement(asCount);
205 
206         String readQuery;
207         if (mJoinClause != null) {
208             String innerQuery = buildReadQuery(SELECT_ALL);
209             readQuery = mJoinClause.getJoinWithQueryCommand(selectStatement, innerQuery);
210         } else {
211             if (!mOrderByClause.getOrderBy().isEmpty()
212                     && !mFinalOrderByClause.getOrderBy().isEmpty()) {
213                 throw new IllegalArgumentException(
214                         "Without a join clause only one of orderByClause or finalOrderByClause may "
215                                 + "be set");
216             }
217             if (mLimit != null && mFinalLimit != null) {
218                 throw new IllegalArgumentException(
219                         "Without a join clause only one of the limit or finalLimit may be set");
220             }
221             readQuery = buildReadQuery(selectStatement);
222         }
223 
224         readQuery = appendFinalOrderByAndLimit(readQuery);
225 
226         if (Constants.DEBUG) {
227             Slog.d(TAG, "read query: " + readQuery);
228         }
229         return readQuery;
230     }
231 
buildSelectStatement(boolean asCount)232     private String buildSelectStatement(boolean asCount) {
233         StringBuilder selectStatement = new StringBuilder(SELECT);
234         if (asCount) {
235             selectStatement.append("COUNT(");
236         }
237         if (mDistinct) {
238             selectStatement.append(DISTINCT);
239         }
240         // If we have distinct over multiple columns, or no count we need the column names.
241         // COUNT(*) differs from COUNT(column) in that it only counts non-null values. However,
242         // a select query will return null values. Therefore, we need COUNT(*) for the count
243         // to match the number of rows in the result.
244         if (!mDistinct && asCount) {
245             selectStatement.append("*");
246         } else {
247             String columns = "*";
248             if (mColumnNames != null && !mColumnNames.isEmpty()) {
249                 columns = String.join(DELIMITER, mColumnNames);
250             }
251             selectStatement.append(columns);
252         }
253         if (asCount) {
254             selectStatement.append(")");
255         }
256 
257         selectStatement.append(FROM);
258         return selectStatement.toString();
259     }
260 
buildReadQuery(String selectStatement)261     private String buildReadQuery(String selectStatement) {
262         return selectStatement
263                 + mTableName
264                 + mWhereClauses.get(/* withWhereKeyword */ true)
265                 + mOrderByClause.getOrderBy()
266                 + (mLimit == null ? "" : LIMIT_SIZE + mLimit);
267     }
268 
appendFinalOrderByAndLimit(String query)269     private String appendFinalOrderByAndLimit(String query) {
270         return query
271                 + mFinalOrderByClause.getOrderBy()
272                 + (mFinalLimit == null ? "" : LIMIT_SIZE + mFinalLimit);
273     }
274 
275     /** Get requests for populating extra data */
276     @Nullable
getExtraReadRequests()277     public List<ReadTableRequest> getExtraReadRequests() {
278         return mExtraReadRequests;
279     }
280 
281     /** Sets requests to populate extra data */
setExtraReadRequests(List<ReadTableRequest> extraDataReadRequests)282     public ReadTableRequest setExtraReadRequests(List<ReadTableRequest> extraDataReadRequests) {
283         mExtraReadRequests = new ArrayList<>(extraDataReadRequests);
284         return this;
285     }
286 
287     /** Get table name of the request */
getTableName()288     public String getTableName() {
289         return mTableName;
290     }
291 
292     /**
293      * Sets order by clause for the read query
294      *
295      * <p>Note that if the query contains a join, the order by clause will be applied to the main
296      * table read sub query before joining. For setting the clause on the full query result use
297      * {@link #setFinalOrderBy}.
298      */
setOrderBy(OrderByClause orderBy)299     public ReadTableRequest setOrderBy(OrderByClause orderBy) {
300         mOrderByClause = orderBy;
301         return this;
302     }
303 
304     /**
305      * Sets order by clause for the read query, applied to the final query result
306      *
307      * <p>This means that if the query contains a join, the order by will be applied to the full
308      * query result instead of the main table read before joining, as is the case with {@link
309      * #setOrderBy}.
310      */
setFinalOrderBy(OrderByClause orderBy)311     public ReadTableRequest setFinalOrderBy(OrderByClause orderBy) {
312         mFinalOrderByClause = orderBy;
313         return this;
314     }
315 
316     /** Returns the current LIMIT size for the query, or null for no LIMIT. */
317     @Nullable
getLimit()318     public Integer getLimit() {
319         return mLimit;
320     }
321 
322     /**
323      * Sets LIMIT size for the read query, or null for no limit.
324      *
325      * <p>Note that if the query contains a join, the limit will be applied to the main table read
326      * before joining. For setting the limit on the full query result use {@link #setFinalLimit}.
327      *
328      * <p>If this ReadTableRequest has any unionReadRequests set, but no joinClause, the limit
329      * applies to the full query including the unionReadRequests.
330      */
setLimit(@ullable Integer limit)331     public ReadTableRequest setLimit(@Nullable Integer limit) {
332         mLimit = limit;
333         return this;
334     }
335 
336     /** Returns the current final LIMIT size for the query, or null for no LIMIT. */
337     @Nullable
getFinalLimit()338     public Integer getFinalLimit() {
339         return mFinalLimit;
340     }
341 
342     /**
343      * Sets final LIMIT size, or null for the read query, which will be applied to the final query
344      *
345      * <p>This means that if the query contains a join, the limit will apply to the full query
346      * result instead of the main table read before joining, as is the case with {@link #setLimit}.
347      */
setFinalLimit(@ullable Integer limit)348     public ReadTableRequest setFinalLimit(@Nullable Integer limit) {
349         mFinalLimit = limit;
350         return this;
351     }
352 
353     /** Sets union read requests. */
setUnionReadRequests( @ullable List<ReadTableRequest> unionReadRequests)354     public ReadTableRequest setUnionReadRequests(
355             @Nullable List<ReadTableRequest> unionReadRequests) {
356         mUnionReadRequests = unionReadRequests;
357 
358         return this;
359     }
360 }
361