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