1/* 2 * Copyright (C) 2017 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 17package android.arch.persistence.room.util; 18 19import android.arch.persistence.db.SupportSQLiteDatabase; 20import android.database.Cursor; 21import android.os.Build; 22import android.support.annotation.NonNull; 23import android.support.annotation.RestrictTo; 24 25import java.util.ArrayList; 26import java.util.Collections; 27import java.util.HashMap; 28import java.util.HashSet; 29import java.util.List; 30import java.util.Map; 31import java.util.Set; 32 33/** 34 * A data class that holds the information about a table. 35 * <p> 36 * It directly maps to the result of {@code PRAGMA table_info(<table_name>)}. Check the 37 * <a href="http://www.sqlite.org/pragma.html#pragma_table_info">PRAGMA table_info</a> 38 * documentation for more details. 39 * <p> 40 * Even though SQLite column names are case insensitive, this class uses case sensitive matching. 41 * 42 * @hide 43 */ 44@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 45@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources"}) 46// if you change this class, you must change TableInfoWriter.kt 47public class TableInfo { 48 /** 49 * The table name. 50 */ 51 public final String name; 52 /** 53 * Unmodifiable map of columns keyed by column name. 54 */ 55 public final Map<String, Column> columns; 56 57 public final Set<ForeignKey> foreignKeys; 58 59 @SuppressWarnings("unused") 60 public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) { 61 this.name = name; 62 this.columns = Collections.unmodifiableMap(columns); 63 this.foreignKeys = Collections.unmodifiableSet(foreignKeys); 64 } 65 66 /** 67 * Reads the table information from the given database. 68 * 69 * @param database The database to read the information from. 70 * @param tableName The table name. 71 * @return A TableInfo containing the schema information for the provided table name. 72 */ 73 @SuppressWarnings("SameParameterValue") 74 public static TableInfo read(SupportSQLiteDatabase database, String tableName) { 75 Map<String, Column> columns = readColumns(database, tableName); 76 Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName); 77 return new TableInfo(tableName, columns, foreignKeys); 78 } 79 80 private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database, 81 String tableName) { 82 Set<ForeignKey> foreignKeys = new HashSet<>(); 83 // this seems to return everything in order but it is not documented so better be safe 84 Cursor cursor = database.query("PRAGMA foreign_key_list(`" + tableName + "`)"); 85 try { 86 final int idColumnIndex = cursor.getColumnIndex("id"); 87 final int seqColumnIndex = cursor.getColumnIndex("seq"); 88 final int tableColumnIndex = cursor.getColumnIndex("table"); 89 final int onDeleteColumnIndex = cursor.getColumnIndex("on_delete"); 90 final int onUpdateColumnIndex = cursor.getColumnIndex("on_update"); 91 92 final List<ForeignKeyWithSequence> ordered = readForeignKeyFieldMappings(cursor); 93 final int count = cursor.getCount(); 94 for (int position = 0; position < count; position++) { 95 cursor.moveToPosition(position); 96 final int seq = cursor.getInt(seqColumnIndex); 97 if (seq != 0) { 98 continue; 99 } 100 final int id = cursor.getInt(idColumnIndex); 101 List<String> myColumns = new ArrayList<>(); 102 List<String> refColumns = new ArrayList<>(); 103 for (ForeignKeyWithSequence key : ordered) { 104 if (key.mId == id) { 105 myColumns.add(key.mFrom); 106 refColumns.add(key.mTo); 107 } 108 } 109 foreignKeys.add(new ForeignKey( 110 cursor.getString(tableColumnIndex), 111 cursor.getString(onDeleteColumnIndex), 112 cursor.getString(onUpdateColumnIndex), 113 myColumns, 114 refColumns 115 )); 116 } 117 } finally { 118 cursor.close(); 119 } 120 return foreignKeys; 121 } 122 123 private static List<ForeignKeyWithSequence> readForeignKeyFieldMappings(Cursor cursor) { 124 final int idColumnIndex = cursor.getColumnIndex("id"); 125 final int seqColumnIndex = cursor.getColumnIndex("seq"); 126 final int fromColumnIndex = cursor.getColumnIndex("from"); 127 final int toColumnIndex = cursor.getColumnIndex("to"); 128 final int count = cursor.getCount(); 129 List<ForeignKeyWithSequence> result = new ArrayList<>(); 130 for (int i = 0; i < count; i++) { 131 cursor.moveToPosition(i); 132 result.add(new ForeignKeyWithSequence( 133 cursor.getInt(idColumnIndex), 134 cursor.getInt(seqColumnIndex), 135 cursor.getString(fromColumnIndex), 136 cursor.getString(toColumnIndex) 137 )); 138 } 139 Collections.sort(result); 140 return result; 141 } 142 143 private static Map<String, Column> readColumns(SupportSQLiteDatabase database, 144 String tableName) { 145 Cursor cursor = database 146 .query("PRAGMA table_info(`" + tableName + "`)"); 147 //noinspection TryFinallyCanBeTryWithResources 148 Map<String, Column> columns = new HashMap<>(); 149 try { 150 if (cursor.getColumnCount() > 0) { 151 int nameIndex = cursor.getColumnIndex("name"); 152 int typeIndex = cursor.getColumnIndex("type"); 153 int notNullIndex = cursor.getColumnIndex("notnull"); 154 int pkIndex = cursor.getColumnIndex("pk"); 155 156 while (cursor.moveToNext()) { 157 final String name = cursor.getString(nameIndex); 158 final String type = cursor.getString(typeIndex); 159 final boolean notNull = 0 != cursor.getInt(notNullIndex); 160 final int primaryKeyPosition = cursor.getInt(pkIndex); 161 columns.put(name, new Column(name, type, notNull, primaryKeyPosition)); 162 } 163 } 164 } finally { 165 cursor.close(); 166 } 167 return columns; 168 } 169 170 @Override 171 public boolean equals(Object o) { 172 if (this == o) return true; 173 if (o == null || getClass() != o.getClass()) return false; 174 175 TableInfo tableInfo = (TableInfo) o; 176 177 if (!name.equals(tableInfo.name)) return false; 178 //noinspection SimplifiableIfStatement 179 if (!columns.equals(tableInfo.columns)) return false; 180 return foreignKeys.equals(tableInfo.foreignKeys); 181 } 182 183 @Override 184 public int hashCode() { 185 int result = name.hashCode(); 186 result = 31 * result + columns.hashCode(); 187 result = 31 * result + foreignKeys.hashCode(); 188 return result; 189 } 190 191 @Override 192 public String toString() { 193 return "TableInfo{" 194 + "name='" + name + '\'' 195 + ", columns=" + columns 196 + ", foreignKeys=" + foreignKeys 197 + '}'; 198 } 199 200 /** 201 * Holds the information about a database column. 202 */ 203 @SuppressWarnings("WeakerAccess") 204 public static class Column { 205 /** 206 * The column name. 207 */ 208 public final String name; 209 /** 210 * The column type affinity. 211 */ 212 public final String type; 213 /** 214 * Whether or not the column can be NULL. 215 */ 216 public final boolean notNull; 217 /** 218 * The position of the column in the list of primary keys, 0 if the column is not part 219 * of the primary key. 220 * <p> 221 * This information is only available in API 20+. 222 * <a href="https://www.sqlite.org/releaselog/3_7_16_2.html">(SQLite version 3.7.16.2)</a> 223 * On older platforms, it will be 1 if the column is part of the primary key and 0 224 * otherwise. 225 * <p> 226 * The {@link #equals(Object)} implementation handles this inconsistency based on 227 * API levels os if you are using a custom SQLite deployment, it may return false 228 * positives. 229 */ 230 public final int primaryKeyPosition; 231 232 // if you change this constructor, you must change TableInfoWriter.kt 233 public Column(String name, String type, boolean notNull, int primaryKeyPosition) { 234 this.name = name; 235 this.type = type; 236 this.notNull = notNull; 237 this.primaryKeyPosition = primaryKeyPosition; 238 } 239 240 @Override 241 public boolean equals(Object o) { 242 if (this == o) return true; 243 if (o == null || getClass() != o.getClass()) return false; 244 245 Column column = (Column) o; 246 if (Build.VERSION.SDK_INT >= 20) { 247 if (primaryKeyPosition != column.primaryKeyPosition) return false; 248 } else { 249 if (isPrimaryKey() != column.isPrimaryKey()) return false; 250 } 251 252 if (!name.equals(column.name)) return false; 253 //noinspection SimplifiableIfStatement 254 if (notNull != column.notNull) return false; 255 return type != null ? type.equalsIgnoreCase(column.type) : column.type == null; 256 } 257 258 /** 259 * Returns whether this column is part of the primary key or not. 260 * 261 * @return True if this column is part of the primary key, false otherwise. 262 */ 263 public boolean isPrimaryKey() { 264 return primaryKeyPosition > 0; 265 } 266 267 @Override 268 public int hashCode() { 269 int result = name.hashCode(); 270 result = 31 * result + (type != null ? type.hashCode() : 0); 271 result = 31 * result + (notNull ? 1231 : 1237); 272 result = 31 * result + primaryKeyPosition; 273 return result; 274 } 275 276 @Override 277 public String toString() { 278 return "Column{" 279 + "name='" + name + '\'' 280 + ", type='" + type + '\'' 281 + ", notNull=" + notNull 282 + ", primaryKeyPosition=" + primaryKeyPosition 283 + '}'; 284 } 285 } 286 287 /** 288 * Holds the information about an SQLite foreign key 289 * 290 * @hide 291 */ 292 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 293 public static class ForeignKey { 294 @NonNull 295 public final String referenceTable; 296 @NonNull 297 public final String onDelete; 298 @NonNull 299 public final String onUpdate; 300 @NonNull 301 public final List<String> columnNames; 302 @NonNull 303 public final List<String> referenceColumnNames; 304 305 public ForeignKey(@NonNull String referenceTable, @NonNull String onDelete, 306 @NonNull String onUpdate, 307 @NonNull List<String> columnNames, @NonNull List<String> referenceColumnNames) { 308 this.referenceTable = referenceTable; 309 this.onDelete = onDelete; 310 this.onUpdate = onUpdate; 311 this.columnNames = Collections.unmodifiableList(columnNames); 312 this.referenceColumnNames = Collections.unmodifiableList(referenceColumnNames); 313 } 314 315 @Override 316 public boolean equals(Object o) { 317 if (this == o) return true; 318 if (o == null || getClass() != o.getClass()) return false; 319 320 ForeignKey that = (ForeignKey) o; 321 322 if (!referenceTable.equals(that.referenceTable)) return false; 323 if (!onDelete.equals(that.onDelete)) return false; 324 if (!onUpdate.equals(that.onUpdate)) return false; 325 //noinspection SimplifiableIfStatement 326 if (!columnNames.equals(that.columnNames)) return false; 327 return referenceColumnNames.equals(that.referenceColumnNames); 328 } 329 330 @Override 331 public int hashCode() { 332 int result = referenceTable.hashCode(); 333 result = 31 * result + onDelete.hashCode(); 334 result = 31 * result + onUpdate.hashCode(); 335 result = 31 * result + columnNames.hashCode(); 336 result = 31 * result + referenceColumnNames.hashCode(); 337 return result; 338 } 339 340 @Override 341 public String toString() { 342 return "ForeignKey{" 343 + "referenceTable='" + referenceTable + '\'' 344 + ", onDelete='" + onDelete + '\'' 345 + ", onUpdate='" + onUpdate + '\'' 346 + ", columnNames=" + columnNames 347 + ", referenceColumnNames=" + referenceColumnNames 348 + '}'; 349 } 350 } 351 352 /** 353 * Temporary data holder for a foreign key row in the pragma result. We need this to ensure 354 * sorting in the generated foreign key object. 355 * 356 * @hide 357 */ 358 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 359 static class ForeignKeyWithSequence implements Comparable<ForeignKeyWithSequence> { 360 final int mId; 361 final int mSequence; 362 final String mFrom; 363 final String mTo; 364 365 ForeignKeyWithSequence(int id, int sequence, String from, String to) { 366 mId = id; 367 mSequence = sequence; 368 mFrom = from; 369 mTo = to; 370 } 371 372 @Override 373 public int compareTo(ForeignKeyWithSequence o) { 374 final int idCmp = mId - o.mId; 375 if (idCmp == 0) { 376 return mSequence - o.mSequence; 377 } else { 378 return idCmp; 379 } 380 } 381 } 382} 383