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.request; 18 19 import android.database.SQLException; 20 import android.util.Pair; 21 import android.util.Slog; 22 23 import java.util.ArrayList; 24 import java.util.HashMap; 25 import java.util.List; 26 import java.util.Locale; 27 import java.util.Map; 28 29 /** 30 * Creates a alter table request and alter statements for it. 31 * 32 * @hide 33 */ 34 public final class AlterTableRequest { 35 public static final String TAG = "HealthConnectAlter"; 36 private static final String NOT_NULL = "NOT NULL"; 37 private static final String ALTER_TABLE_COMMAND = "ALTER TABLE "; 38 private static final String ADD_COLUMN_COMMAND = " ADD COLUMN "; 39 private final String mTableName; 40 private final List<Pair<String, String>> mColumnInfo; 41 42 private final Map<String, Pair<String, String>> mForeignKeyConstraints = new HashMap<>(); 43 AlterTableRequest(String tableName, List<Pair<String, String>> columnInfo)44 public AlterTableRequest(String tableName, List<Pair<String, String>> columnInfo) { 45 mTableName = tableName; 46 mColumnInfo = columnInfo; 47 } 48 49 /** 50 * Adds a foreign key constraint between one column and another. Deletion behavior is to set 51 * dangling references to null. 52 */ addForeignKeyConstraint( String column, String referencedTable, String referencedColumn)53 public AlterTableRequest addForeignKeyConstraint( 54 String column, String referencedTable, String referencedColumn) { 55 mForeignKeyConstraints.put(column, new Pair<>(referencedTable, referencedColumn)); 56 return this; 57 } 58 59 /** Returns a list of alter table SQL statements to add new columns */ getAlterTableAddColumnsCommands()60 public List<String> getAlterTableAddColumnsCommands() { 61 List<String> statements = new ArrayList<>(); 62 for (int i = 0; i < mColumnInfo.size(); i++) { 63 StringBuilder statement = new StringBuilder(ALTER_TABLE_COMMAND); 64 statement.append(mTableName); 65 String columnName = mColumnInfo.get(i).first; 66 String columnType = mColumnInfo.get(i).second; 67 statement.append(ADD_COLUMN_COMMAND).append(columnName).append(" ").append(columnType); 68 if (mForeignKeyConstraints.containsKey(columnName)) { 69 statement.append(" "); 70 statement.append("REFERENCES "); 71 statement.append(mForeignKeyConstraints.get(columnName).first); 72 statement.append("("); 73 statement.append(mForeignKeyConstraints.get(columnName).second); 74 statement.append(")"); 75 statement.append(" ON DELETE SET NULL"); 76 } 77 statement.append(";"); 78 statements.add(statement.toString()); 79 } 80 Slog.d(TAG, "Alter table: " + statements); 81 82 // Check on the final commands for now as it's more broad. Should this become a problem 83 // later, we can change/move the check to narrow scope such as only check on the 84 // `columnInfo` list passed to the constructor 85 checkNoNotNullColumns(statements); 86 return statements; 87 } 88 getAlterTableCommandToAddGeneratedColumn( String tableName, CreateTableRequest.GeneratedColumnInfo generatedColumnInfo)89 public static String getAlterTableCommandToAddGeneratedColumn( 90 String tableName, CreateTableRequest.GeneratedColumnInfo generatedColumnInfo) { 91 String request = 92 ALTER_TABLE_COMMAND 93 + tableName 94 + ADD_COLUMN_COMMAND 95 + generatedColumnInfo.getColumnName() 96 + " " 97 + generatedColumnInfo.getColumnType() 98 + " GENERATED ALWAYS AS (" 99 + generatedColumnInfo.getExpression() 100 + ")"; 101 102 Slog.d(TAG, "Alter table generated: " + request); 103 return request; 104 } 105 checkNoNotNullColumns(List<String> alterTableCommands)106 private static void checkNoNotNullColumns(List<String> alterTableCommands) { 107 for (String command : alterTableCommands) { 108 if (command == null) { 109 continue; 110 } 111 String upperCased = command.toUpperCase(Locale.ROOT); 112 if (upperCased.contains(NOT_NULL)) { 113 throw new SQLException( 114 String.format( 115 "Alter table command \"%s\" must not contain \"%s\"", 116 upperCased, NOT_NULL)); 117 } 118 } 119 } 120 } 121