Bug / Integer primary key conflict should upsert #60
Changes from 7 commits
08e95ad
da3079f
2b67486
288d2e6
3a894c6
bebfd47
495339f
cd8f32c
d31bf49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
CREATE TABLE 'parents' (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT, latitude REAL, longitude REAL, createdAt INTEGER); | ||
CREATE TABLE 'childs' (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, parent_id INTEGER NOT NULL, FOREIGN KEY(parent_id) REFERENCES parent(_id)); | ||
-- This table intentionally has no Foreign Keys, no UNIQUE fields, no indexes, apart from the implicit index from the INTEGER PRIMARY KEY. | ||
CREATE TABLE 'integer_primary_key_table' (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ public class DBUtilsTest extends AndroidTestCase { | |
private static final String CREATE_2_TABLES_WITH_FOREIGN_KEY = "CREATE TABLE t(id INTEGER);\nCREATE TABLE t2(id INTEGER, t_id INTEGER);\n"; | ||
private static final String CREATE_TABLE_WITH_CONSTRAINT = "CREATE TABLE t(id INTEGER, const TEXT UNIQUE NOT NULL);"; | ||
private static final String CREATE_TABLE_WITH_MULTI_COLUMN_CONSTRAINT = "CREATE TABLE t(id INTEGER, name TEXT, desc TEXT NOT NULL, UNIQUE(name, desc) ON CONFLICT REPLACE);"; | ||
private static final String CREATE_TABLE_WITH_INTEGER_PRIMARY_KEY = "CREATE TABLE t(_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);"; | ||
|
||
@Override | ||
protected void setUp() throws Exception { | ||
|
@@ -71,6 +72,14 @@ public void testGettingUniqueConstraints() throws Exception { | |
MoreAsserts.assertContentsInAnyOrder(constrains, new Constraint(Arrays.asList("const"))); | ||
} | ||
|
||
public void testGettingUniqueConstraintForIntegerPrimaryKey() throws Exception { | ||
android.database.DatabaseUtils.createDbFromSqlStatements(getContext(), DB_NAME, 1, CREATE_TABLE_WITH_INTEGER_PRIMARY_KEY); | ||
|
||
SQLiteDatabase db = getContext().openOrCreateDatabase(DB_NAME, 0, null); | ||
List<Constraint> constrains = DBUtils.getUniqueConstraints(db, "t"); | ||
MoreAsserts.assertContentsInAnyOrder(constrains, new Constraint(Arrays.asList("_id"))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. contraints? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. D'oh. Cheers! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Heh. C+P'd and adapted one of the other tests, that used to use a method called "getUniqueConstrains". I've fixed them all :) |
||
} | ||
|
||
public void testGettingMultiColumnUniqueConstraints() throws Exception { | ||
android.database.DatabaseUtils.createDbFromSqlStatements(getContext(), DB_NAME, 1, CREATE_TABLE_WITH_MULTI_COLUMN_CONSTRAINT); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,12 +18,18 @@ public final class DBUtils { | |
|
||
private static final String SELECT_TABLES_NAME = "SELECT name FROM sqlite_master WHERE type='table';"; | ||
|
||
private static final String PRAGMA_TABLE = "PRAGMA table_info(\"%1$s\");"; | ||
private static final String PRAGMA_TABLE_INFO = "PRAGMA table_info('%1$s');"; | ||
|
||
private static final String PRGAMA_INDEX_LIST = "PRAGMA index_list('%1$s');"; | ||
|
||
private static final String PRGAMA_INDEX_INFO = "PRAGMA index_info('%1$s');"; | ||
|
||
private static final String COLUMN_PRIMARY_KEY = "pk"; | ||
|
||
private static final String COLUMN_NAME = "name"; | ||
|
||
private static final String COLUMN_TYPE = "type"; | ||
|
||
private static List<String> defaultTables = Arrays.asList(new String[]{ | ||
"android_metadata" | ||
}); | ||
|
@@ -33,13 +39,13 @@ private DBUtils() { | |
} | ||
|
||
public static List<String> getForeignTables(SQLiteDatabase db, String table) { | ||
final Cursor cur = db.rawQuery(String.format(PRAGMA_TABLE, table), null); | ||
final Cursor cur = queryTableColumnsFor(table, db); | ||
List<String> tables = getTables(db); | ||
List<String> foreignTables = new ArrayList<String>(5); | ||
String name; | ||
String tableName; | ||
while (cur.moveToNext()) { | ||
name = cur.getString(cur.getColumnIndexOrThrow("name")); | ||
name = cur.getString(cur.getColumnIndexOrThrow(COLUMN_NAME)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
if (name.endsWith("_id")) { | ||
tableName = name.substring(0, name.lastIndexOf('_')); | ||
if (tables.contains(tableName + "s")) { | ||
|
@@ -89,16 +95,16 @@ public static Map<String, String> getProjectionMap(SQLiteDatabase db, String par | |
} | ||
|
||
public static Map<String, SQLiteType> getFields(SQLiteDatabase db, String table) { | ||
final Cursor cur = db.rawQuery(String.format(PRAGMA_TABLE, table), null); | ||
Map<String, SQLiteType> fields = new HashMap<String, SQLiteType>(cur.getCount()); | ||
final Cursor cursor = queryTableColumnsFor(table, db); | ||
Map<String, SQLiteType> fields = new HashMap<String, SQLiteType>(cursor.getCount()); | ||
String name; | ||
String type; | ||
while (cur.moveToNext()) { | ||
name = cur.getString(cur.getColumnIndexOrThrow("name")); | ||
type = cur.getString(cur.getColumnIndexOrThrow("type")); | ||
fields.put(name, SQLiteType.valueOf(type.toUpperCase())); | ||
while (cursor.moveToNext()) { | ||
name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)); | ||
type = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_TYPE)); | ||
fields.put(name, SQLiteType.fromName(type)); | ||
} | ||
cur.close(); | ||
cursor.close(); | ||
return Collections.unmodifiableMap(fields); | ||
} | ||
|
||
|
@@ -125,31 +131,38 @@ public static String getSQLiteVersion() { | |
@Deprecated | ||
public static List<String> getUniqueConstrains(SQLiteDatabase db, String table) { | ||
List<String> constrains = new ArrayList<String>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. contraints here too (maybe cmd-r would be a good idea) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this one is "correct" i.e. shouldn't be changed. It's a deprecated method and it used to refer to "constrains". Yeah... even though that should never have been a thing. Anyway, the new stuff uses "constraints", and the old stuff will die. |
||
final Cursor pragmas = db.rawQuery(String.format(PRGAMA_INDEX_LIST, table), null); | ||
while (pragmas.moveToNext()) { | ||
int isUnique = pragmas.getInt(2); | ||
final Cursor indices = queryIndexListForTable(table, db); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't want to fix it in the deprecated version? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's deprecated... so, no? If someone updated the lib to get the fix, they could switch to using the non-deprecated version. |
||
while (indices.moveToNext()) { | ||
int isUnique = indices.getInt(2); | ||
if (isUnique == 1) { | ||
String name = pragmas.getString(1); | ||
final Cursor pragmaInfo = db.rawQuery(String.format(PRGAMA_INDEX_INFO, name), null); | ||
String indexName = indices.getString(1); | ||
final Cursor pragmaInfo = queryIndexInfo(indexName, db); | ||
if (pragmaInfo.moveToFirst()) { | ||
constrains.add(pragmaInfo.getString(2)); | ||
} | ||
pragmaInfo.close(); | ||
} | ||
} | ||
pragmas.close(); | ||
indices.close(); | ||
return constrains; | ||
} | ||
|
||
|
||
public static List<Constraint> getUniqueConstraints(SQLiteDatabase db, String table) { | ||
List<Constraint> constraints = new ArrayList<Constraint>(); | ||
final Cursor indexCursor = db.rawQuery(String.format(PRGAMA_INDEX_LIST, table), null); | ||
|
||
// This is an implicit unique index, and won't show up querying the other indexes | ||
Constraint integerPrimaryKeyConstraint = findIntegerPrimaryKeyConstraint(db, table); | ||
if (integerPrimaryKeyConstraint != null) { | ||
constraints.add(integerPrimaryKeyConstraint); | ||
} | ||
|
||
final Cursor indexCursor = queryIndexListForTable(table, db); | ||
while (indexCursor.moveToNext()) { | ||
int isUnique = indexCursor.getInt(2); | ||
if (isUnique == 1) { | ||
String indexName = indexCursor.getString(1); | ||
final Cursor columnCursor = db.rawQuery(String.format(PRGAMA_INDEX_INFO, indexName), null); | ||
final Cursor columnCursor = queryIndexInfo(indexName, db); | ||
List<String> columns = new ArrayList<>(columnCursor.getCount()); | ||
while (columnCursor.moveToNext()) { | ||
String columnName = columnCursor.getString(2); | ||
|
@@ -162,4 +175,42 @@ public static List<Constraint> getUniqueConstraints(SQLiteDatabase db, String ta | |
indexCursor.close(); | ||
return constraints; | ||
} | ||
|
||
private static Constraint findIntegerPrimaryKeyConstraint(SQLiteDatabase database, String table) { | ||
final Cursor cursor = queryTableColumnsFor(table, database); | ||
try { | ||
while (cursor.moveToNext()) { | ||
if (isTableInfoItemAnIntegerPrimaryKey(cursor)) { | ||
String columnName = cursor.getString(cursor.getColumnIndex(COLUMN_NAME)); | ||
return new Constraint(Collections.singletonList(columnName)); | ||
} | ||
} | ||
} finally { | ||
cursor.close(); | ||
} | ||
return null; | ||
} | ||
|
||
private static Cursor queryTableColumnsFor(String table, SQLiteDatabase database) { | ||
return database.rawQuery(String.format(PRAGMA_TABLE_INFO, table), null); | ||
} | ||
|
||
private static Cursor queryIndexListForTable(String table, SQLiteDatabase database) { | ||
return database.rawQuery(String.format(PRGAMA_INDEX_LIST, table), null); | ||
} | ||
|
||
private static Cursor queryIndexInfo(String index, SQLiteDatabase database) { | ||
return database.rawQuery(String.format(PRGAMA_INDEX_INFO, index), null); | ||
} | ||
|
||
private static boolean isTableInfoItemAnIntegerPrimaryKey(Cursor cursor) { | ||
int pkInt = cursor.getInt(cursor.getColumnIndex(COLUMN_PRIMARY_KEY)); | ||
String columnType = cursor.getString(cursor.getColumnIndex(COLUMN_TYPE)); | ||
|
||
boolean isPrimaryKey = pkInt == 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. according to https://www.sqlite.org/pragma.html#pragma_table_info the check should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooh, nice catch, thanks. I'll update that |
||
boolean isInteger = SQLiteType.fromName(columnType) == SQLiteType.INTEGER; | ||
|
||
return isPrimaryKey && isInteger; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍