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