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