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