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 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 public static final class GeneratedColumnInfo { 201 private final String columnName; 202 private final String type; 203 private final String expression; 204 GeneratedColumnInfo(String columnName, String type, String expression)205 public GeneratedColumnInfo(String columnName, String type, String expression) { 206 this.columnName = columnName; 207 this.type = type; 208 this.expression = expression; 209 } 210 getColumnName()211 public String getColumnName() { 212 return columnName; 213 } 214 getColumnType()215 public String getColumnType() { 216 return type; 217 } 218 getExpression()219 public String getExpression() { 220 return expression; 221 } 222 } 223 224 private final class ForeignKey { 225 private final List<String> mColumnNames; 226 private final String mReferencedTableName; 227 private final List<String> mReferencedColumnNames; 228 ForeignKey( String referencedTable, List<String> columnNames, List<String> referencedColumnNames)229 ForeignKey( 230 String referencedTable, 231 List<String> columnNames, 232 List<String> referencedColumnNames) { 233 mReferencedTableName = referencedTable; 234 mColumnNames = columnNames; 235 mReferencedColumnNames = referencedColumnNames; 236 } 237 getFkConstraint()238 String getFkConstraint() { 239 return FOREIGN_KEY_COMMAND 240 + String.join(DELIMITER, mColumnNames) 241 + ")" 242 + " REFERENCES " 243 + mReferencedTableName 244 + "(" 245 + String.join(DELIMITER, mReferencedColumnNames) 246 + ") ON DELETE CASCADE"; 247 } 248 getFkIndexStatement(int fkNumber)249 String getFkIndexStatement(int fkNumber) { 250 return getCreateIndexCommand(mTableName + "_" + fkNumber, mColumnNames); 251 } 252 } 253 } 254