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