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.migration; 18 19 20 import static org.hamcrest.CoreMatchers.equalTo; 21 import static org.hamcrest.CoreMatchers.is; 22 import static org.hamcrest.MatcherAssert.assertThat; 23 24 import static java.util.Arrays.asList; 25 import static java.util.Collections.singletonList; 26 27 import android.support.test.InstrumentationRegistry; 28 import android.support.test.filters.SmallTest; 29 import android.support.test.runner.AndroidJUnit4; 30 import android.util.Pair; 31 32 import androidx.room.util.TableInfo; 33 import androidx.sqlite.db.SupportSQLiteDatabase; 34 import androidx.sqlite.db.SupportSQLiteOpenHelper; 35 import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; 36 37 import org.junit.After; 38 import org.junit.Test; 39 import org.junit.runner.RunWith; 40 41 import java.io.IOException; 42 import java.util.Arrays; 43 import java.util.Collections; 44 import java.util.HashMap; 45 import java.util.HashSet; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Set; 49 50 @SuppressWarnings("ArraysAsListWithZeroOrOneArgument") 51 @RunWith(AndroidJUnit4.class) 52 @SmallTest 53 public class TableInfoTest { 54 private SupportSQLiteDatabase mDb; 55 56 @Test 57 public void readSimple() { 58 mDb = createDatabase( 59 "CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT," 60 + "name TEXT)"); 61 TableInfo info = TableInfo.read(mDb, "foo"); 62 assertThat(info, is(new TableInfo("foo", 63 toMap(new TableInfo.Column("id", "INTEGER", false, 1), 64 new TableInfo.Column("name", "TEXT", false, 0)), 65 Collections.<TableInfo.ForeignKey>emptySet()))); 66 } 67 68 @Test 69 public void multiplePrimaryKeys() { 70 mDb = createDatabase( 71 "CREATE TABLE foo (id INTEGER," 72 + "name TEXT, PRIMARY KEY(name, id))"); 73 TableInfo info = TableInfo.read(mDb, "foo"); 74 assertThat(info, is(new TableInfo("foo", 75 toMap(new TableInfo.Column("id", "INTEGER", false, 2), 76 new TableInfo.Column("name", "TEXT", false, 1)), 77 Collections.<TableInfo.ForeignKey>emptySet()))); 78 } 79 80 @Test 81 public void alteredTable() { 82 mDb = createDatabase( 83 "CREATE TABLE foo (id INTEGER," 84 + "name TEXT, PRIMARY KEY(name))"); 85 mDb.execSQL("ALTER TABLE foo ADD COLUMN added REAL;"); 86 TableInfo info = TableInfo.read(mDb, "foo"); 87 assertThat(info, is(new TableInfo("foo", 88 toMap(new TableInfo.Column("id", "INTEGER", false, 0), 89 new TableInfo.Column("name", "TEXT", false, 1), 90 new TableInfo.Column("added", "REAL", false, 0)), 91 Collections.<TableInfo.ForeignKey>emptySet()))); 92 } 93 94 @Test 95 public void nonNull() { 96 mDb = createDatabase( 97 "CREATE TABLE foo (name TEXT NOT NULL)"); 98 TableInfo info = TableInfo.read(mDb, "foo"); 99 assertThat(info, is(new TableInfo("foo", 100 toMap(new TableInfo.Column("name", "TEXT", true, 0)), 101 Collections.<TableInfo.ForeignKey>emptySet()))); 102 } 103 104 @Test 105 public void defaultValue() { 106 mDb = createDatabase( 107 "CREATE TABLE foo (name TEXT DEFAULT blah)"); 108 TableInfo info = TableInfo.read(mDb, "foo"); 109 assertThat(info, is(new TableInfo( 110 "foo", 111 toMap(new TableInfo.Column("name", "TEXT", false, 0)), 112 Collections.<TableInfo.ForeignKey>emptySet()))); 113 } 114 115 @Test 116 public void foreignKey() { 117 mDb = createDatabase( 118 "CREATE TABLE foo (name TEXT)", 119 "CREATE TABLE bar(barName TEXT, FOREIGN KEY(barName) REFERENCES foo(name))" 120 ); 121 TableInfo info = TableInfo.read(mDb, "bar"); 122 assertThat(info.foreignKeys.size(), is(1)); 123 final TableInfo.ForeignKey foreignKey = info.foreignKeys.iterator().next(); 124 assertThat(foreignKey.columnNames, is(singletonList("barName"))); 125 assertThat(foreignKey.referenceColumnNames, is(singletonList("name"))); 126 assertThat(foreignKey.onDelete, is("NO ACTION")); 127 assertThat(foreignKey.onUpdate, is("NO ACTION")); 128 assertThat(foreignKey.referenceTable, is("foo")); 129 } 130 131 @Test 132 public void multipleForeignKeys() { 133 mDb = createDatabase( 134 "CREATE TABLE foo (name TEXT, lastName TEXT)", 135 "CREATE TABLE foo2 (name TEXT, lastName TEXT)", 136 "CREATE TABLE bar(barName TEXT, barLastName TEXT, " 137 + " FOREIGN KEY(barName) REFERENCES foo(name) ON UPDATE SET NULL," 138 + " FOREIGN KEY(barLastName) REFERENCES foo2(lastName) ON DELETE CASCADE)"); 139 TableInfo info = TableInfo.read(mDb, "bar"); 140 assertThat(info.foreignKeys.size(), is(2)); 141 Set<TableInfo.ForeignKey> expected = new HashSet<>(); 142 expected.add(new TableInfo.ForeignKey("foo2", // table 143 "CASCADE", // on delete 144 "NO ACTION", // on update 145 singletonList("barLastName"), // my 146 singletonList("lastName")) // ref 147 ); 148 expected.add(new TableInfo.ForeignKey("foo", // table 149 "NO ACTION", // on delete 150 "SET NULL", // on update 151 singletonList("barName"), // mine 152 singletonList("name")/*ref*/)); 153 assertThat(info.foreignKeys, equalTo(expected)); 154 } 155 156 @Test 157 public void compositeForeignKey() { 158 mDb = createDatabase( 159 "CREATE TABLE foo (name TEXT, lastName TEXT)", 160 "CREATE TABLE bar(barName TEXT, barLastName TEXT, " 161 + " FOREIGN KEY(barName, barLastName) REFERENCES foo(name, lastName)" 162 + " ON UPDATE cascade ON DELETE RESTRICT)"); 163 TableInfo info = TableInfo.read(mDb, "bar"); 164 assertThat(info.foreignKeys.size(), is(1)); 165 TableInfo.ForeignKey expected = new TableInfo.ForeignKey( 166 "foo", // table 167 "RESTRICT", // on delete 168 "CASCADE", // on update 169 asList("barName", "barLastName"), // my columns 170 asList("name", "lastName") // ref columns 171 ); 172 assertThat(info.foreignKeys.iterator().next(), is(expected)); 173 } 174 175 @Test 176 public void caseInsensitiveTypeName() { 177 mDb = createDatabase( 178 "CREATE TABLE foo (n integer)"); 179 TableInfo info = TableInfo.read(mDb, "foo"); 180 assertThat(info, is(new TableInfo( 181 "foo", 182 toMap(new TableInfo.Column("n", "INTEGER", false, 0)), 183 Collections.<TableInfo.ForeignKey>emptySet()))); 184 } 185 186 @Test 187 public void readIndices() { 188 mDb = createDatabase( 189 "CREATE TABLE foo (n INTEGER, indexed TEXT, unique_indexed TEXT," 190 + "a INTEGER, b INTEGER);", 191 "CREATE INDEX foo_indexed ON foo(indexed);", 192 "CREATE UNIQUE INDEX foo_unique_indexed ON foo(unique_indexed COLLATE NOCASE" 193 + " DESC);", 194 "CREATE INDEX " + TableInfo.Index.DEFAULT_PREFIX + "foo_composite_indexed" 195 + " ON foo(a, b);" 196 ); 197 TableInfo info = TableInfo.read(mDb, "foo"); 198 assertThat(info, is(new TableInfo( 199 "foo", 200 toMap(new TableInfo.Column("n", "INTEGER", false, 0), 201 new TableInfo.Column("indexed", "TEXT", false, 0), 202 new TableInfo.Column("unique_indexed", "TEXT", false, 0), 203 new TableInfo.Column("a", "INTEGER", false, 0), 204 new TableInfo.Column("b", "INTEGER", false, 0)), 205 Collections.<TableInfo.ForeignKey>emptySet(), 206 toSet(new TableInfo.Index("index_foo_blahblah", false, 207 Arrays.asList("a", "b")), 208 new TableInfo.Index("foo_unique_indexed", true, 209 Arrays.asList("unique_indexed")), 210 new TableInfo.Index("foo_indexed", false, 211 Arrays.asList("indexed")))) 212 )); 213 } 214 215 @Test 216 public void compatColumnTypes() { 217 // see:https://www.sqlite.org/datatype3.html 3.1 218 List<Pair<String, String>> testCases = Arrays.asList( 219 new Pair<>("TINYINT", "integer"), 220 new Pair<>("VARCHAR", "text"), 221 new Pair<>("DOUBLE", "real"), 222 new Pair<>("BOOLEAN", "numeric"), 223 new Pair<>("FLOATING POINT", "integer") 224 ); 225 for (Pair<String, String> testCase : testCases) { 226 mDb = createDatabase( 227 "CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT," 228 + "name " + testCase.first + ")"); 229 TableInfo info = TableInfo.read(mDb, "foo"); 230 assertThat(info, is(new TableInfo("foo", 231 toMap(new TableInfo.Column("id", "INTEGER", false, 1), 232 new TableInfo.Column("name", testCase.second, false, 0)), 233 Collections.<TableInfo.ForeignKey>emptySet()))); 234 } 235 } 236 237 private static Map<String, TableInfo.Column> toMap(TableInfo.Column... columns) { 238 Map<String, TableInfo.Column> result = new HashMap<>(); 239 for (TableInfo.Column column : columns) { 240 result.put(column.name, column); 241 } 242 return result; 243 } 244 245 private static <T> Set<T> toSet(T... ts) { 246 final HashSet<T> result = new HashSet<T>(); 247 for (T t : ts) { 248 result.add(t); 249 } 250 return result; 251 } 252 253 @After 254 public void closeDb() throws IOException { 255 if (mDb != null && mDb.isOpen()) { 256 mDb.close(); 257 } 258 } 259 260 private static SupportSQLiteDatabase createDatabase(final String... queries) { 261 return new FrameworkSQLiteOpenHelperFactory().create( 262 SupportSQLiteOpenHelper.Configuration 263 .builder(InstrumentationRegistry.getTargetContext()) 264 .name(null) 265 .callback(new SupportSQLiteOpenHelper.Callback(1) { 266 @Override 267 public void onCreate(SupportSQLiteDatabase db) { 268 for (String query : queries) { 269 db.execSQL(query); 270 } 271 } 272 273 @Override 274 public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, 275 int newVersion) { 276 throw new IllegalStateException("should not be upgrading"); 277 } 278 }).build() 279 ).getWritableDatabase(); 280 } 281 } 282