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