Home | History | Annotate | Download | only in util
      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")
     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")
     85     public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) {
     86         this(name, columns, foreignKeys, Collections.<Index>emptySet());
     87     }
     88 
     89     @Override
     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
    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
    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")
    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 
    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 
    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 
    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
    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
    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
    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
    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
    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          */
    411         public boolean isPrimaryKey() {
    412             return primaryKeyPosition > 0;
    413         }
    414 
    415         @Override
    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
    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 
    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
    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
    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
    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 
    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
    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 
    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
    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
    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
    584         public String toString() {
    585             return "Index{"
    586                     + "name='" + name + '\''
    587                     + ", unique=" + unique
    588                     + ", columns=" + columns
    589                     + '}';
    590         }
    591     }
    592 }
    593