1 /* 2 * Copyright (C) 2023 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.utils; 18 19 import static com.android.server.healthconnect.storage.utils.StorageUtils.SELECT_ALL; 20 21 import android.annotation.StringDef; 22 23 import java.lang.annotation.Retention; 24 import java.lang.annotation.RetentionPolicy; 25 import java.util.ArrayList; 26 import java.util.List; 27 import java.util.Objects; 28 29 /** 30 * Represents SQL join. Default join type is INNER join. 31 * 32 * @hide 33 */ 34 public final class SqlJoin { 35 public static final String SQL_JOIN_INNER = "INNER"; 36 public static final String SQL_JOIN_LEFT = "LEFT"; 37 38 public static final String INNER_QUERY_ALIAS = "inner_query_result"; 39 40 /** @hide */ 41 @StringDef( 42 value = { 43 SQL_JOIN_INNER, 44 SQL_JOIN_LEFT, 45 }) 46 @Retention(RetentionPolicy.SOURCE) 47 public @interface JoinType {} 48 49 private final String mSelfTableName; 50 private final String mTableNameToJoinOn; 51 private final String mSelfColumnNameToMatch; 52 private final String mJoiningColumnNameToMatch; 53 54 private List<SqlJoin> mAttachedJoins; 55 private String mJoinType = SQL_JOIN_INNER; 56 57 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression 58 private WhereClauses mTableToJoinWhereClause = null; 59 60 @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression SqlJoin( String selfTableName, String tableNameToJoinOn, String selfColumnNameToMatch, String joiningColumnNameToMatch)61 public SqlJoin( 62 String selfTableName, 63 String tableNameToJoinOn, 64 String selfColumnNameToMatch, 65 String joiningColumnNameToMatch) { 66 mSelfTableName = selfTableName; 67 mTableNameToJoinOn = tableNameToJoinOn; 68 mSelfColumnNameToMatch = selfColumnNameToMatch; 69 mJoiningColumnNameToMatch = joiningColumnNameToMatch; 70 } 71 72 /** 73 * Sets join type to the current joint, default value is inner join. Returns class with join 74 * type set. 75 */ setJoinType(@oinType String joinType)76 public SqlJoin setJoinType(@JoinType String joinType) { 77 Objects.requireNonNull(joinType); 78 mJoinType = joinType; 79 return this; 80 } 81 82 /** 83 * Returns query by applying JOIN condition on the innerQuery 84 * 85 * @param innerQuery An inner query to be used for the JOIN 86 * @param selectStatement the outer select statement where you can specify column names, 87 * DISTINCT, etc. 88 * @return Final query with JOIN condition 89 */ getJoinWithQueryCommand(String selectStatement, String innerQuery)90 public String getJoinWithQueryCommand(String selectStatement, String innerQuery) { 91 if (innerQuery == null) { 92 throw new IllegalArgumentException("Inner query cannot be null"); 93 } 94 return selectStatement 95 + "( " 96 + innerQuery 97 + " ) AS " 98 + INNER_QUERY_ALIAS 99 + " " 100 + getJoinCommand(/* withInnerQuery= */ true); 101 } 102 103 /** Returns join command. */ getJoinCommand()104 public String getJoinCommand() { 105 return getJoinCommand(/* withInnerQuery= */ false); 106 } 107 108 /** Attaches another join to this join. Returns this class with another join attached. */ attachJoin(SqlJoin join)109 public SqlJoin attachJoin(SqlJoin join) { 110 Objects.requireNonNull(join); 111 112 if (mAttachedJoins == null) { 113 mAttachedJoins = new ArrayList<>(); 114 } 115 116 mAttachedJoins.add(join); 117 return this; 118 } 119 120 /** Sets the {@link WhereClauses} for the second table and returns this {@link SqlJoin}. */ setSecondTableWhereClause(WhereClauses whereClause)121 public SqlJoin setSecondTableWhereClause(WhereClauses whereClause) { 122 mTableToJoinWhereClause = whereClause; 123 return this; 124 } 125 getJoinCommand(boolean withInnerQuery)126 private String getJoinCommand(boolean withInnerQuery) { 127 String selfColumnPrefix = withInnerQuery ? INNER_QUERY_ALIAS + "." : mSelfTableName + "."; 128 return " " 129 + mJoinType 130 + " JOIN " 131 + (mTableToJoinWhereClause == null ? "" : "( " + buildFilterQuery() + ") ") 132 + mTableNameToJoinOn 133 + " ON " 134 + selfColumnPrefix 135 + mSelfColumnNameToMatch 136 + " = " 137 + mTableNameToJoinOn 138 + "." 139 + mJoiningColumnNameToMatch 140 + buildAttachedJoinsCommand(withInnerQuery); 141 } 142 buildFilterQuery()143 private String buildFilterQuery() { 144 return SELECT_ALL + mTableNameToJoinOn + mTableToJoinWhereClause.get(true); 145 } 146 buildAttachedJoinsCommand(boolean withInnerQuery)147 private String buildAttachedJoinsCommand(boolean withInnerQuery) { 148 if (mAttachedJoins == null) { 149 return ""; 150 } 151 152 StringBuilder command = new StringBuilder(); 153 for (SqlJoin join : mAttachedJoins) { 154 if (withInnerQuery && join.mSelfTableName.equals(mSelfTableName)) { 155 // When we're joining from the top level table, and there is an inner query, use the 156 // inner query prefix. 157 command.append(" ").append(join.getJoinCommand(true)); 158 } else { 159 // Otherwise use the table name itself. 160 command.append(" ").append(join.getJoinCommand(false)); 161 } 162 } 163 164 return command.toString(); 165 } 166 } 167