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 
21 import android.annotation.NonNull;
22 import android.health.connect.Constants;
23 import android.util.Pair;
24 import android.util.Slog;
25 
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.List;
29 import java.util.Objects;
30 
31 /**
32  * Creates a request and the table create statements for it.
33  *
34  * <p>Note: For every child table. This class automatically creates index statements for all the
35  * defined foreign keys.
36  *
37  * @hide
38  */
39 public final class CreateTableRequest {
40     public static final String TAG = "HealthConnectCreate";
41     public static final String FOREIGN_KEY_COMMAND = " FOREIGN KEY (";
42     private static final String CREATE_INDEX_COMMAND = "CREATE INDEX IF NOT EXISTS idx_";
43     private static final String CREATE_TABLE_COMMAND = "CREATE TABLE IF NOT EXISTS ";
44     private static final String UNIQUE_COMMAND = "UNIQUE ( ";
45     private final String mTableName;
46     private final List<Pair<String, String>> mColumnInfo;
47     private final List<String> mColumnsToIndex = new ArrayList<>();
48     private final List<List<String>> mUniqueColumns = new ArrayList<>();
49     private List<ForeignKey> mForeignKeys = new ArrayList<>();
50     private List<CreateTableRequest> mChildTableRequests = Collections.emptyList();
51     private List<GeneratedColumnInfo> mGeneratedColumnInfo = Collections.emptyList();
52 
CreateTableRequest(String tableName, List<Pair<String, String>> columnInfo)53     public CreateTableRequest(String tableName, List<Pair<String, String>> columnInfo) {
54         mTableName = tableName;
55         mColumnInfo = columnInfo;
56     }
57 
getTableName()58     public String getTableName() {
59         return mTableName;
60     }
61 
62     @NonNull
addForeignKey( String referencedTable, List<String> columnNames, List<String> referencedColumnNames)63     public CreateTableRequest addForeignKey(
64             String referencedTable, List<String> columnNames, List<String> referencedColumnNames) {
65         mForeignKeys = mForeignKeys == null ? new ArrayList<>() : mForeignKeys;
66         mForeignKeys.add(new ForeignKey(referencedTable, columnNames, referencedColumnNames));
67 
68         return this;
69     }
70 
71     @NonNull
createIndexOn(@onNull String columnName)72     public CreateTableRequest createIndexOn(@NonNull String columnName) {
73         Objects.requireNonNull(columnName);
74 
75         mColumnsToIndex.add(columnName);
76         return this;
77     }
78 
79     @NonNull
getChildTableRequests()80     public List<CreateTableRequest> getChildTableRequests() {
81         return mChildTableRequests;
82     }
83 
84     @NonNull
setChildTableRequests( @onNull List<CreateTableRequest> childCreateTableRequests)85     public CreateTableRequest setChildTableRequests(
86             @NonNull List<CreateTableRequest> childCreateTableRequests) {
87         Objects.requireNonNull(childCreateTableRequests);
88 
89         mChildTableRequests = childCreateTableRequests;
90         return this;
91     }
92 
93     @NonNull
getCreateCommand()94     public String getCreateCommand() {
95         final StringBuilder builder = new StringBuilder(CREATE_TABLE_COMMAND);
96         builder.append(mTableName);
97         builder.append(" (");
98         mColumnInfo.forEach(
99                 (columnInfo) ->
100                         builder.append(columnInfo.first)
101                                 .append(" ")
102                                 .append(columnInfo.second)
103                                 .append(", "));
104 
105         for (GeneratedColumnInfo generatedColumnInfo : mGeneratedColumnInfo) {
106             builder.append(generatedColumnInfo.getColumnName())
107                     .append(" ")
108                     .append(generatedColumnInfo.getColumnType())
109                     .append(" AS (")
110                     .append(generatedColumnInfo.getExpression())
111                     .append("), ");
112         }
113 
114         if (mForeignKeys != null) {
115             for (ForeignKey foreignKey : mForeignKeys) {
116                 builder.append(foreignKey.getFkConstraint()).append(", ");
117             }
118         }
119 
120         if (!mUniqueColumns.isEmpty()) {
121             mUniqueColumns.forEach(
122                     columns -> {
123                         builder.append(UNIQUE_COMMAND);
124                         for (String column : columns) {
125                             builder.append(column).append(", ");
126                         }
127                         builder.setLength(builder.length() - 2); // Remove the last 2 char i.e. ", "
128                         builder.append("), ");
129                     });
130         }
131         builder.setLength(builder.length() - 2); // Remove the last 2 char i.e. ", "
132 
133         builder.append(")");
134         if (Constants.DEBUG) {
135             Slog.d(TAG, "Create table: " + builder);
136         }
137 
138         return builder.toString();
139     }
140 
addUniqueConstraints(List<String> columnNames)141     public CreateTableRequest addUniqueConstraints(List<String> columnNames) {
142         mUniqueColumns.add(columnNames);
143         return this;
144     }
145 
146     @NonNull
getCreateIndexStatements()147     public List<String> getCreateIndexStatements() {
148         List<String> result = new ArrayList<>();
149         if (mForeignKeys != null) {
150             int index = 0;
151             for (ForeignKey foreignKey : mForeignKeys) {
152                 result.add(foreignKey.getFkIndexStatement(index++));
153             }
154         }
155 
156         if (!mColumnsToIndex.isEmpty()) {
157             for (String columnToIndex : mColumnsToIndex) {
158                 result.add(getCreateIndexCommand(columnToIndex));
159             }
160         }
161 
162         return result;
163     }
164 
setGeneratedColumnInfo( @onNull List<GeneratedColumnInfo> generatedColumnInfo)165     public CreateTableRequest setGeneratedColumnInfo(
166             @NonNull List<GeneratedColumnInfo> generatedColumnInfo) {
167         Objects.requireNonNull(generatedColumnInfo);
168 
169         mGeneratedColumnInfo = generatedColumnInfo;
170         return this;
171     }
172 
getCreateIndexCommand(String indexName, List<String> columnNames)173     private String getCreateIndexCommand(String indexName, List<String> columnNames) {
174         Objects.requireNonNull(columnNames);
175         Objects.requireNonNull(indexName);
176 
177         return CREATE_INDEX_COMMAND
178                 + indexName
179                 + " ON "
180                 + mTableName
181                 + "("
182                 + String.join(DELIMITER, columnNames)
183                 + ")";
184     }
185 
getCreateIndexCommand(String columnName)186     private String getCreateIndexCommand(String columnName) {
187         Objects.requireNonNull(columnName);
188 
189         return CREATE_INDEX_COMMAND
190                 + mTableName
191                 + "_"
192                 + columnName
193                 + " ON "
194                 + mTableName
195                 + "("
196                 + columnName
197                 + ")";
198     }
199 
200     /**
201      * Indicates whether some other object is "equal to" this one.
202      *
203      * @param obj the reference object with which to compare.
204      * @return {@code true} if this object is the same as the obj
205      */
206     @Override
equals(Object obj)207     public boolean equals(Object obj) {
208         if (this == obj) return true;
209         if (!(obj instanceof CreateTableRequest that)) return false;
210         return Objects.equals(mTableName, that.mTableName)
211                 && Objects.equals(mColumnInfo, that.mColumnInfo)
212                 && Objects.equals(mUniqueColumns, that.mUniqueColumns)
213                 && Objects.equals(mForeignKeys, that.mForeignKeys)
214                 && Objects.equals(mChildTableRequests, that.mChildTableRequests)
215                 && Objects.equals(mGeneratedColumnInfo, that.mGeneratedColumnInfo);
216     }
217 
218     /** Returns a hash code value for the object. */
219     @Override
hashCode()220     public int hashCode() {
221         return Objects.hash(
222                 mTableName,
223                 mColumnInfo,
224                 mUniqueColumns,
225                 mForeignKeys,
226                 mChildTableRequests,
227                 mGeneratedColumnInfo);
228     }
229 
230     public static final class GeneratedColumnInfo {
231         private final String columnName;
232         private final String type;
233         private final String expression;
234 
GeneratedColumnInfo(String columnName, String type, String expression)235         public GeneratedColumnInfo(String columnName, String type, String expression) {
236             this.columnName = columnName;
237             this.type = type;
238             this.expression = expression;
239         }
240 
getColumnName()241         public String getColumnName() {
242             return columnName;
243         }
244 
getColumnType()245         public String getColumnType() {
246             return type;
247         }
248 
getExpression()249         public String getExpression() {
250             return expression;
251         }
252 
253         /**
254          * Indicates whether some other object is "equal to" this one.
255          *
256          * @param obj the reference object with which to compare.
257          * @return {@code true} if this object is the same as the obj
258          */
259         @Override
equals(Object obj)260         public boolean equals(Object obj) {
261             if (this == obj) return true;
262             if (!(obj instanceof GeneratedColumnInfo that)) return false;
263             return (Objects.equals(columnName, that.columnName)
264                     && Objects.equals(type, that.type)
265                     && Objects.equals(expression, that.expression));
266         }
267 
268         /** Returns a hash code value for the object. */
269         @Override
hashCode()270         public int hashCode() {
271             return Objects.hash(columnName, type, expression);
272         }
273     }
274 
275     private final class ForeignKey {
276         private final List<String> mColumnNames;
277         private final String mReferencedTableName;
278         private final List<String> mReferencedColumnNames;
279 
ForeignKey( String referencedTable, List<String> columnNames, List<String> referencedColumnNames)280         ForeignKey(
281                 String referencedTable,
282                 List<String> columnNames,
283                 List<String> referencedColumnNames) {
284             mReferencedTableName = referencedTable;
285             mColumnNames = columnNames;
286             mReferencedColumnNames = referencedColumnNames;
287         }
288 
getFkConstraint()289         String getFkConstraint() {
290             return FOREIGN_KEY_COMMAND
291                     + String.join(DELIMITER, mColumnNames)
292                     + ")"
293                     + " REFERENCES "
294                     + mReferencedTableName
295                     + "("
296                     + String.join(DELIMITER, mReferencedColumnNames)
297                     + ") ON DELETE CASCADE";
298         }
299 
getFkIndexStatement(int fkNumber)300         String getFkIndexStatement(int fkNumber) {
301             return getCreateIndexCommand(mTableName + "_" + fkNumber, mColumnNames);
302         }
303 
304         /**
305          * Indicates whether some other object is "equal to" this one.
306          *
307          * @param obj the reference object with which to compare.
308          * @return {@code true} if this object is the same as the obj
309          */
310         @Override
equals(Object obj)311         public boolean equals(Object obj) {
312             if (this == obj) return true;
313             if (!(obj instanceof ForeignKey that)) return false;
314             return (Objects.equals(mColumnNames, that.mColumnNames)
315                     && Objects.equals(mReferencedTableName, that.mReferencedTableName)
316                     && Objects.equals(mReferencedColumnNames, that.mReferencedColumnNames));
317         }
318 
319         /** Returns a hash code value for the object. */
320         @Override
hashCode()321         public int hashCode() {
322             return Objects.hash(mColumnNames, mReferencedTableName, mReferencedColumnNames);
323         }
324     }
325 }
326