/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.room.util; import android.database.Cursor; import android.os.Build; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.room.ColumnInfo; import androidx.sqlite.db.SupportSQLiteDatabase; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; /** * A data class that holds the information about a table. *
* It directly maps to the result of {@code PRAGMA table_info(
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
"SimplifiableIfStatement"})
// if you change this class, you must change TableInfoValidationWriter.kt
public final class TableInfo {
/**
* Identifies from where the info object was created.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE})
@interface CreatedFrom {
}
/**
* Identifier for when the info is created from an unknown source.
*/
public static final int CREATED_FROM_UNKNOWN = 0;
/**
* Identifier for when the info is created from an entity definition, such as generated code
* by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
*/
public static final int CREATED_FROM_ENTITY = 1;
/**
* Identifier for when the info is created from the database itself, reading information from a
* PRAGMA, such as table_info.
*/
public static final int CREATED_FROM_DATABASE = 2;
/**
* The table name.
*/
public final String name;
/**
* Unmodifiable map of columns keyed by column name.
*/
public final Map
* This is the value Room uses for equality check.
*/
@ColumnInfo.SQLiteTypeAffinity
public final int affinity;
/**
* Whether or not the column can be NULL.
*/
public final boolean notNull;
/**
* The position of the column in the list of primary keys, 0 if the column is not part
* of the primary key.
*
* This information is only available in API 20+.
* (SQLite version 3.7.16.2)
* On older platforms, it will be 1 if the column is part of the primary key and 0
* otherwise.
*
* The {@link #equals(Object)} implementation handles this inconsistency based on
* API levels os if you are using a custom SQLite deployment, it may return false
* positives.
*/
public final int primaryKeyPosition;
/**
* The default value of this column.
*/
public final String defaultValue;
@CreatedFrom
private final int mCreatedFrom;
/**
* @deprecated Use {@link Column#Column(String, String, boolean, int, String, int)} instead.
*/
@Deprecated
public Column(String name, String type, boolean notNull, int primaryKeyPosition) {
this(name, type, notNull, primaryKeyPosition, null, CREATED_FROM_UNKNOWN);
}
// if you change this constructor, you must change TableInfoWriter.kt
public Column(String name, String type, boolean notNull, int primaryKeyPosition,
String defaultValue, @CreatedFrom int createdFrom) {
this.name = name;
this.type = type;
this.notNull = notNull;
this.primaryKeyPosition = primaryKeyPosition;
this.affinity = findAffinity(type);
this.defaultValue = defaultValue;
this.mCreatedFrom = createdFrom;
}
/**
* Implements https://www.sqlite.org/datatype3.html section 3.1
*
* @param type The type that was given to the sqlite
* @return The normalized type which is one of the 5 known affinities
*/
@ColumnInfo.SQLiteTypeAffinity
private static int findAffinity(@Nullable String type) {
if (type == null) {
return ColumnInfo.BLOB;
}
String uppercaseType = type.toUpperCase(Locale.US);
if (uppercaseType.contains("INT")) {
return ColumnInfo.INTEGER;
}
if (uppercaseType.contains("CHAR")
|| uppercaseType.contains("CLOB")
|| uppercaseType.contains("TEXT")) {
return ColumnInfo.TEXT;
}
if (uppercaseType.contains("BLOB")) {
return ColumnInfo.BLOB;
}
if (uppercaseType.contains("REAL")
|| uppercaseType.contains("FLOA")
|| uppercaseType.contains("DOUB")) {
return ColumnInfo.REAL;
}
// sqlite returns NUMERIC here but it is like a catch all. We already
// have UNDEFINED so it is better to use UNDEFINED for consistency.
return ColumnInfo.UNDEFINED;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Column)) return false;
Column column = (Column) o;
if (Build.VERSION.SDK_INT >= 20) {
if (primaryKeyPosition != column.primaryKeyPosition) return false;
} else {
if (isPrimaryKey() != column.isPrimaryKey()) return false;
}
if (!name.equals(column.name)) return false;
//noinspection SimplifiableIfStatement
if (notNull != column.notNull) return false;
// Only validate default value if it was defined in an entity, i.e. if the info
// from the compiler itself has it. b/136019383
if (mCreatedFrom == CREATED_FROM_ENTITY
&& column.mCreatedFrom == CREATED_FROM_DATABASE
&& (defaultValue != null && !defaultValueEquals(defaultValue,
column.defaultValue))) {
return false;
} else if (mCreatedFrom == CREATED_FROM_DATABASE
&& column.mCreatedFrom == CREATED_FROM_ENTITY
&& (column.defaultValue != null && !defaultValueEquals(
column.defaultValue, defaultValue))) {
return false;
} else if (mCreatedFrom != CREATED_FROM_UNKNOWN
&& mCreatedFrom == column.mCreatedFrom
&& (defaultValue != null ? !defaultValueEquals(defaultValue,
column.defaultValue)
: column.defaultValue != null)) {
return false;
}
return affinity == column.affinity;
}
/**
* Checks if the default values provided match. Handles the special case in which the
* default value is surrounded by parenthesis (e.g. encountered in b/182284899).
*
* Surrounding parenthesis are removed by SQLite when reading from the database, hence
* this function will check if they are present in the actual value, if so, it will
* compare the two values by ignoring the surrounding parenthesis.
*
*/
public static boolean defaultValueEquals(@NonNull String actual, @Nullable String other) {
if (other == null) {
return false;
}
if (actual.equals(other)) {
return true;
} else if (containsSurroundingParenthesis(actual)) {
return actual.substring(1, actual.length() - 1).trim().equals(other);
}
return false;
}
/**
* Checks for potential surrounding parenthesis, if found, removes them and checks if
* remaining paranthesis are balanced. If so, the surrounding parenthesis are redundant,
* and returns true.
*/
private static boolean containsSurroundingParenthesis(@NonNull String actual) {
if (actual.length() == 0) {
return false;
}
int surroundingParenthesis = 0;
for (int i = 0; i < actual.length(); i++) {
char c = actual.charAt(i);
if (i == 0 && c != '(') {
return false;
}
if (c == '(') {
surroundingParenthesis++;
} else if (c == ')') {
surroundingParenthesis--;
if (surroundingParenthesis == 0 && i != actual.length() - 1) {
return false;
}
}
}
return surroundingParenthesis == 0;
}
/**
* Returns whether this column is part of the primary key or not.
*
* @return True if this column is part of the primary key, false otherwise.
*/
public boolean isPrimaryKey() {
return primaryKeyPosition > 0;
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + affinity;
result = 31 * result + (notNull ? 1231 : 1237);
result = 31 * result + primaryKeyPosition;
// Default value is not part of the hashcode since we conditionally check it for
// equality which would break the equals + hashcode contract.
// result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Column{"
+ "name='" + name + '\''
+ ", type='" + type + '\''
+ ", affinity='" + affinity + '\''
+ ", notNull=" + notNull
+ ", primaryKeyPosition=" + primaryKeyPosition
+ ", defaultValue='" + defaultValue + '\''
+ '}';
}
}
/**
* Holds the information about an SQLite foreign key
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static final class ForeignKey {
@NonNull
public final String referenceTable;
@NonNull
public final String onDelete;
@NonNull
public final String onUpdate;
@NonNull
public final List