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