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 17 package androidx.room.util; 18 19 import android.database.Cursor; 20 import android.os.Build; 21 22 import androidx.annotation.NonNull; 23 import androidx.annotation.Nullable; 24 import androidx.annotation.RestrictTo; 25 import androidx.room.ColumnInfo; 26 import androidx.sqlite.db.SupportSQLiteDatabase; 27 28 import java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.HashMap; 31 import java.util.HashSet; 32 import java.util.List; 33 import java.util.Locale; 34 import java.util.Map; 35 import java.util.Set; 36 import java.util.TreeMap; 37 38 /** 39 * A data class that holds the information about a table. 40 * <p> 41 * It directly maps to the result of {@code PRAGMA table_info(<table_name>)}. Check the 42 * <a href="http://www.sqlite.org/pragma.html#pragma_table_info">PRAGMA table_info</a> 43 * documentation for more details. 44 * <p> 45 * Even though SQLite column names are case insensitive, this class uses case sensitive matching. 46 * 47 * @hide 48 */ 49 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 50 @SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources", 51 "SimplifiableIfStatement"}) 52 // if you change this class, you must change TableInfoWriter.kt 53 public class TableInfo { 54 /** 55 * The table name. 56 */ 57 public final String name; 58 /** 59 * Unmodifiable map of columns keyed by column name. 60 */ 61 public final Map<String, Column> columns; 62 63 public final Set<ForeignKey> foreignKeys; 64 65 /** 66 * Sometimes, Index information is not available (older versions). If so, we skip their 67 * verification. 68 */ 69 @Nullable 70 public final Set<Index> indices; 71 72 @SuppressWarnings("unused") TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys, Set<Index> indices)73 public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys, 74 Set<Index> indices) { 75 this.name = name; 76 this.columns = Collections.unmodifiableMap(columns); 77 this.foreignKeys = Collections.unmodifiableSet(foreignKeys); 78 this.indices = indices == null ? null : Collections.unmodifiableSet(indices); 79 } 80 81 /** 82 * For backward compatibility with dbs created with older versions. 83 */ 84 @SuppressWarnings("unused") TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys)85 public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) { 86 this(name, columns, foreignKeys, Collections.<Index>emptySet()); 87 } 88 89 @Override equals(Object o)90 public boolean equals(Object o) { 91 if (this == o) return true; 92 if (o == null || getClass() != o.getClass()) return false; 93 94 TableInfo tableInfo = (TableInfo) o; 95 96 if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false; 97 if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) { 98 return false; 99 } 100 if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys) 101 : tableInfo.foreignKeys != null) { 102 return false; 103 } 104 if (indices == null || tableInfo.indices == null) { 105 // if one us is missing index information, seems like we couldn't acquire the 106 // information so we better skip. 107 return true; 108 } 109 return indices.equals(tableInfo.indices); 110 } 111 112 @Override hashCode()113 public int hashCode() { 114 int result = name != null ? name.hashCode() : 0; 115 result = 31 * result + (columns != null ? columns.hashCode() : 0); 116 result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0); 117 // skip index, it is not reliable for comparison. 118 return result; 119 } 120 121 @Override toString()122 public String toString() { 123 return "TableInfo{" 124 + "name='" + name + '\'' 125 + ", columns=" + columns 126 + ", foreignKeys=" + foreignKeys 127 + ", indices=" + indices 128 + '}'; 129 } 130 131 /** 132 * Reads the table information from the given database. 133 * 134 * @param database The database to read the information from. 135 * @param tableName The table name. 136 * @return A TableInfo containing the schema information for the provided table name. 137 */ 138 @SuppressWarnings("SameParameterValue") read(SupportSQLiteDatabase database, String tableName)139 public static TableInfo read(SupportSQLiteDatabase database, String tableName) { 140 Map<String, Column> columns = readColumns(database, tableName); 141 Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName); 142 Set<Index> indices = readIndices(database, tableName); 143 return new TableInfo(tableName, columns, foreignKeys, indices); 144 } 145 readForeignKeys(SupportSQLiteDatabase database, String tableName)146 private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database, 147 String tableName) { 148 Set<ForeignKey> foreignKeys = new HashSet<>(); 149 // this seems to return everything in order but it is not documented so better be safe 150 Cursor cursor = database.query("PRAGMA foreign_key_list(`" + tableName + "`)"); 151 try { 152 final int idColumnIndex = cursor.getColumnIndex("id"); 153 final int seqColumnIndex = cursor.getColumnIndex("seq"); 154 final int tableColumnIndex = cursor.getColumnIndex("table"); 155 final int onDeleteColumnIndex = cursor.getColumnIndex("on_delete"); 156 final int onUpdateColumnIndex = cursor.getColumnIndex("on_update"); 157 158 final List<ForeignKeyWithSequence> ordered = readForeignKeyFieldMappings(cursor); 159 final int count = cursor.getCount(); 160 for (int position = 0; position < count; position++) { 161 cursor.moveToPosition(position); 162 final int seq = cursor.getInt(seqColumnIndex); 163 if (seq != 0) { 164 continue; 165 } 166 final int id = cursor.getInt(idColumnIndex); 167 List<String> myColumns = new ArrayList<>(); 168 List<String> refColumns = new ArrayList<>(); 169 for (ForeignKeyWithSequence key : ordered) { 170 if (key.mId == id) { 171 myColumns.add(key.mFrom); 172 refColumns.add(key.mTo); 173 } 174 } 175 foreignKeys.add(new ForeignKey( 176 cursor.getString(tableColumnIndex), 177 cursor.getString(onDeleteColumnIndex), 178 cursor.getString(onUpdateColumnIndex), 179 myColumns, 180 refColumns 181 )); 182 } 183 } finally { 184 cursor.close(); 185 } 186 return foreignKeys; 187 } 188 readForeignKeyFieldMappings(Cursor cursor)189 private static List<ForeignKeyWithSequence> readForeignKeyFieldMappings(Cursor cursor) { 190 final int idColumnIndex = cursor.getColumnIndex("id"); 191 final int seqColumnIndex = cursor.getColumnIndex("seq"); 192 final int fromColumnIndex = cursor.getColumnIndex("from"); 193 final int toColumnIndex = cursor.getColumnIndex("to"); 194 final int count = cursor.getCount(); 195 List<ForeignKeyWithSequence> result = new ArrayList<>(); 196 for (int i = 0; i < count; i++) { 197 cursor.moveToPosition(i); 198 result.add(new ForeignKeyWithSequence( 199 cursor.getInt(idColumnIndex), 200 cursor.getInt(seqColumnIndex), 201 cursor.getString(fromColumnIndex), 202 cursor.getString(toColumnIndex) 203 )); 204 } 205 Collections.sort(result); 206 return result; 207 } 208 readColumns(SupportSQLiteDatabase database, String tableName)209 private static Map<String, Column> readColumns(SupportSQLiteDatabase database, 210 String tableName) { 211 Cursor cursor = database 212 .query("PRAGMA table_info(`" + tableName + "`)"); 213 //noinspection TryFinallyCanBeTryWithResources 214 Map<String, Column> columns = new HashMap<>(); 215 try { 216 if (cursor.getColumnCount() > 0) { 217 int nameIndex = cursor.getColumnIndex("name"); 218 int typeIndex = cursor.getColumnIndex("type"); 219 int notNullIndex = cursor.getColumnIndex("notnull"); 220 int pkIndex = cursor.getColumnIndex("pk"); 221 222 while (cursor.moveToNext()) { 223 final String name = cursor.getString(nameIndex); 224 final String type = cursor.getString(typeIndex); 225 final boolean notNull = 0 != cursor.getInt(notNullIndex); 226 final int primaryKeyPosition = cursor.getInt(pkIndex); 227 columns.put(name, new Column(name, type, notNull, primaryKeyPosition)); 228 } 229 } 230 } finally { 231 cursor.close(); 232 } 233 return columns; 234 } 235 236 /** 237 * @return null if we cannot read the indices due to older sqlite implementations. 238 */ 239 @Nullable readIndices(SupportSQLiteDatabase database, String tableName)240 private static Set<Index> readIndices(SupportSQLiteDatabase database, String tableName) { 241 Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)"); 242 try { 243 final int nameColumnIndex = cursor.getColumnIndex("name"); 244 final int originColumnIndex = cursor.getColumnIndex("origin"); 245 final int uniqueIndex = cursor.getColumnIndex("unique"); 246 if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) { 247 // we cannot read them so better not validate any index. 248 return null; 249 } 250 HashSet<Index> indices = new HashSet<>(); 251 while (cursor.moveToNext()) { 252 String origin = cursor.getString(originColumnIndex); 253 if (!"c".equals(origin)) { 254 // Ignore auto-created indices 255 continue; 256 } 257 String name = cursor.getString(nameColumnIndex); 258 boolean unique = cursor.getInt(uniqueIndex) == 1; 259 Index index = readIndex(database, name, unique); 260 if (index == null) { 261 // we cannot read it properly so better not read it 262 return null; 263 } 264 indices.add(index); 265 } 266 return indices; 267 } finally { 268 cursor.close(); 269 } 270 } 271 272 /** 273 * @return null if we cannot read the index due to older sqlite implementations. 274 */ 275 @Nullable readIndex(SupportSQLiteDatabase database, String name, boolean unique)276 private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) { 277 Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)"); 278 try { 279 final int seqnoColumnIndex = cursor.getColumnIndex("seqno"); 280 final int cidColumnIndex = cursor.getColumnIndex("cid"); 281 final int nameColumnIndex = cursor.getColumnIndex("name"); 282 if (seqnoColumnIndex == -1 || cidColumnIndex == -1 || nameColumnIndex == -1) { 283 // we cannot read them so better not validate any index. 284 return null; 285 } 286 final TreeMap<Integer, String> results = new TreeMap<>(); 287 288 while (cursor.moveToNext()) { 289 int cid = cursor.getInt(cidColumnIndex); 290 if (cid < 0) { 291 // Ignore SQLite row ID 292 continue; 293 } 294 int seq = cursor.getInt(seqnoColumnIndex); 295 String columnName = cursor.getString(nameColumnIndex); 296 results.put(seq, columnName); 297 } 298 final List<String> columns = new ArrayList<>(results.size()); 299 columns.addAll(results.values()); 300 return new Index(name, unique, columns); 301 } finally { 302 cursor.close(); 303 } 304 } 305 306 /** 307 * Holds the information about a database column. 308 */ 309 @SuppressWarnings("WeakerAccess") 310 public static class Column { 311 /** 312 * The column name. 313 */ 314 public final String name; 315 /** 316 * The column type affinity. 317 */ 318 public final String type; 319 /** 320 * The column type after it is normalized to one of the basic types according to 321 * https://www.sqlite.org/datatype3.html Section 3.1. 322 * <p> 323 * This is the value Room uses for equality check. 324 */ 325 @ColumnInfo.SQLiteTypeAffinity 326 public final int affinity; 327 /** 328 * Whether or not the column can be NULL. 329 */ 330 public final boolean notNull; 331 /** 332 * The position of the column in the list of primary keys, 0 if the column is not part 333 * of the primary key. 334 * <p> 335 * This information is only available in API 20+. 336 * <a href="https://www.sqlite.org/releaselog/3_7_16_2.html">(SQLite version 3.7.16.2)</a> 337 * On older platforms, it will be 1 if the column is part of the primary key and 0 338 * otherwise. 339 * <p> 340 * The {@link #equals(Object)} implementation handles this inconsistency based on 341 * API levels os if you are using a custom SQLite deployment, it may return false 342 * positives. 343 */ 344 public final int primaryKeyPosition; 345 346 // if you change this constructor, you must change TableInfoWriter.kt Column(String name, String type, boolean notNull, int primaryKeyPosition)347 public Column(String name, String type, boolean notNull, int primaryKeyPosition) { 348 this.name = name; 349 this.type = type; 350 this.notNull = notNull; 351 this.primaryKeyPosition = primaryKeyPosition; 352 this.affinity = findAffinity(type); 353 } 354 355 /** 356 * Implements https://www.sqlite.org/datatype3.html section 3.1 357 * 358 * @param type The type that was given to the sqlite 359 * @return The normalized type which is one of the 5 known affinities 360 */ 361 @ColumnInfo.SQLiteTypeAffinity findAffinity(@ullable String type)362 private static int findAffinity(@Nullable String type) { 363 if (type == null) { 364 return ColumnInfo.BLOB; 365 } 366 String uppercaseType = type.toUpperCase(Locale.US); 367 if (uppercaseType.contains("INT")) { 368 return ColumnInfo.INTEGER; 369 } 370 if (uppercaseType.contains("CHAR") 371 || uppercaseType.contains("CLOB") 372 || uppercaseType.contains("TEXT")) { 373 return ColumnInfo.TEXT; 374 } 375 if (uppercaseType.contains("BLOB")) { 376 return ColumnInfo.BLOB; 377 } 378 if (uppercaseType.contains("REAL") 379 || uppercaseType.contains("FLOA") 380 || uppercaseType.contains("DOUB")) { 381 return ColumnInfo.REAL; 382 } 383 // sqlite returns NUMERIC here but it is like a catch all. We already 384 // have UNDEFINED so it is better to use UNDEFINED for consistency. 385 return ColumnInfo.UNDEFINED; 386 } 387 388 @Override equals(Object o)389 public boolean equals(Object o) { 390 if (this == o) return true; 391 if (o == null || getClass() != o.getClass()) return false; 392 393 Column column = (Column) o; 394 if (Build.VERSION.SDK_INT >= 20) { 395 if (primaryKeyPosition != column.primaryKeyPosition) return false; 396 } else { 397 if (isPrimaryKey() != column.isPrimaryKey()) return false; 398 } 399 400 if (!name.equals(column.name)) return false; 401 //noinspection SimplifiableIfStatement 402 if (notNull != column.notNull) return false; 403 return affinity == column.affinity; 404 } 405 406 /** 407 * Returns whether this column is part of the primary key or not. 408 * 409 * @return True if this column is part of the primary key, false otherwise. 410 */ isPrimaryKey()411 public boolean isPrimaryKey() { 412 return primaryKeyPosition > 0; 413 } 414 415 @Override hashCode()416 public int hashCode() { 417 int result = name.hashCode(); 418 result = 31 * result + affinity; 419 result = 31 * result + (notNull ? 1231 : 1237); 420 result = 31 * result + primaryKeyPosition; 421 return result; 422 } 423 424 @Override toString()425 public String toString() { 426 return "Column{" 427 + "name='" + name + '\'' 428 + ", type='" + type + '\'' 429 + ", affinity='" + affinity + '\'' 430 + ", notNull=" + notNull 431 + ", primaryKeyPosition=" + primaryKeyPosition 432 + '}'; 433 } 434 } 435 436 /** 437 * Holds the information about an SQLite foreign key 438 * 439 * @hide 440 */ 441 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 442 public static class ForeignKey { 443 @NonNull 444 public final String referenceTable; 445 @NonNull 446 public final String onDelete; 447 @NonNull 448 public final String onUpdate; 449 @NonNull 450 public final List<String> columnNames; 451 @NonNull 452 public final List<String> referenceColumnNames; 453 ForeignKey(@onNull String referenceTable, @NonNull String onDelete, @NonNull String onUpdate, @NonNull List<String> columnNames, @NonNull List<String> referenceColumnNames)454 public ForeignKey(@NonNull String referenceTable, @NonNull String onDelete, 455 @NonNull String onUpdate, 456 @NonNull List<String> columnNames, @NonNull List<String> referenceColumnNames) { 457 this.referenceTable = referenceTable; 458 this.onDelete = onDelete; 459 this.onUpdate = onUpdate; 460 this.columnNames = Collections.unmodifiableList(columnNames); 461 this.referenceColumnNames = Collections.unmodifiableList(referenceColumnNames); 462 } 463 464 @Override equals(Object o)465 public boolean equals(Object o) { 466 if (this == o) return true; 467 if (o == null || getClass() != o.getClass()) return false; 468 469 ForeignKey that = (ForeignKey) o; 470 471 if (!referenceTable.equals(that.referenceTable)) return false; 472 if (!onDelete.equals(that.onDelete)) return false; 473 if (!onUpdate.equals(that.onUpdate)) return false; 474 //noinspection SimplifiableIfStatement 475 if (!columnNames.equals(that.columnNames)) return false; 476 return referenceColumnNames.equals(that.referenceColumnNames); 477 } 478 479 @Override hashCode()480 public int hashCode() { 481 int result = referenceTable.hashCode(); 482 result = 31 * result + onDelete.hashCode(); 483 result = 31 * result + onUpdate.hashCode(); 484 result = 31 * result + columnNames.hashCode(); 485 result = 31 * result + referenceColumnNames.hashCode(); 486 return result; 487 } 488 489 @Override toString()490 public String toString() { 491 return "ForeignKey{" 492 + "referenceTable='" + referenceTable + '\'' 493 + ", onDelete='" + onDelete + '\'' 494 + ", onUpdate='" + onUpdate + '\'' 495 + ", columnNames=" + columnNames 496 + ", referenceColumnNames=" + referenceColumnNames 497 + '}'; 498 } 499 } 500 501 /** 502 * Temporary data holder for a foreign key row in the pragma result. We need this to ensure 503 * sorting in the generated foreign key object. 504 * 505 * @hide 506 */ 507 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 508 static class ForeignKeyWithSequence implements Comparable<ForeignKeyWithSequence> { 509 final int mId; 510 final int mSequence; 511 final String mFrom; 512 final String mTo; 513 ForeignKeyWithSequence(int id, int sequence, String from, String to)514 ForeignKeyWithSequence(int id, int sequence, String from, String to) { 515 mId = id; 516 mSequence = sequence; 517 mFrom = from; 518 mTo = to; 519 } 520 521 @Override compareTo(@onNull ForeignKeyWithSequence o)522 public int compareTo(@NonNull ForeignKeyWithSequence o) { 523 final int idCmp = mId - o.mId; 524 if (idCmp == 0) { 525 return mSequence - o.mSequence; 526 } else { 527 return idCmp; 528 } 529 } 530 } 531 532 /** 533 * Holds the information about an SQLite index 534 * 535 * @hide 536 */ 537 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 538 public static class Index { 539 // should match the value in Index.kt 540 public static final String DEFAULT_PREFIX = "index_"; 541 public final String name; 542 public final boolean unique; 543 public final List<String> columns; 544 Index(String name, boolean unique, List<String> columns)545 public Index(String name, boolean unique, List<String> columns) { 546 this.name = name; 547 this.unique = unique; 548 this.columns = columns; 549 } 550 551 @Override equals(Object o)552 public boolean equals(Object o) { 553 if (this == o) return true; 554 if (o == null || getClass() != o.getClass()) return false; 555 556 Index index = (Index) o; 557 if (unique != index.unique) { 558 return false; 559 } 560 if (!columns.equals(index.columns)) { 561 return false; 562 } 563 if (name.startsWith(Index.DEFAULT_PREFIX)) { 564 return index.name.startsWith(Index.DEFAULT_PREFIX); 565 } else { 566 return name.equals(index.name); 567 } 568 } 569 570 @Override hashCode()571 public int hashCode() { 572 int result; 573 if (name.startsWith(DEFAULT_PREFIX)) { 574 result = DEFAULT_PREFIX.hashCode(); 575 } else { 576 result = name.hashCode(); 577 } 578 result = 31 * result + (unique ? 1 : 0); 579 result = 31 * result + columns.hashCode(); 580 return result; 581 } 582 583 @Override toString()584 public String toString() { 585 return "Index{" 586 + "name='" + name + '\'' 587 + ", unique=" + unique 588 + ", columns=" + columns 589 + '}'; 590 } 591 } 592 } 593