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 androidx.room.util;
18
19import android.database.Cursor;
20import android.os.Build;
21
22import androidx.annotation.NonNull;
23import androidx.annotation.Nullable;
24import androidx.annotation.RestrictTo;
25import androidx.room.ColumnInfo;
26import androidx.sqlite.db.SupportSQLiteDatabase;
27
28import java.util.ArrayList;
29import java.util.Collections;
30import java.util.HashMap;
31import java.util.HashSet;
32import java.util.List;
33import java.util.Locale;
34import java.util.Map;
35import java.util.Set;
36import 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
53public 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