Skip to content
This repository was archived by the owner on Feb 11, 2022. It is now read-only.

Commit e9ef6fc

Browse files
committed
Merge pull request #47 from novoda/multi_column_constraints
Fix handling of multi-column constraints
2 parents 7ac278e + e36ab0c commit e9ef6fc

7 files changed

Lines changed: 209 additions & 27 deletions

File tree

core/src/androidTest/java/novoda/lib/sqliteprovider/util/DBUtilsTest.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import android.test.MoreAsserts;
66
import android.test.RenamingDelegatingContext;
77

8+
import java.util.Arrays;
89
import java.util.List;
910
import java.util.Map;
1011

@@ -16,7 +17,8 @@ public class DBUtilsTest extends AndroidTestCase {
1617
private static final String CREATE_TABLES = "CREATE TABLE t1(id INTEGER, name TEXT, r REAL);\n";
1718
private static final String CREATE_2_TABLES = "CREATE TABLE T(id INTEGER);\nCREATE TABLE T2(id INTEGER);\n";
1819
private static final String CREATE_2_TABLES_WITH_FOREIGN_KEY = "CREATE TABLE t(id INTEGER);\nCREATE TABLE t2(id INTEGER, t_id INTEGER);\n";
19-
private static final String CREATE_TABLE_WITH_CONSTRAIN = "CREATE TABLE t(id INTEGER, const TEXT UNIQUE NOT NULL);";
20+
private static final String CREATE_TABLE_WITH_CONSTRAINT = "CREATE TABLE t(id INTEGER, const TEXT UNIQUE NOT NULL);";
21+
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);";
2022

2123
@Override
2224
protected void setUp() throws Exception {
@@ -61,18 +63,26 @@ public void testGettingProjectionMap() throws Exception {
6163
assertEquals("t2.id AS t2_id", ft.get("t2_id"));
6264
}
6365

64-
public void testGettingUniqueConstrains() throws Exception {
65-
android.database.DatabaseUtils.createDbFromSqlStatements(getContext(), DB_NAME, 1, CREATE_TABLE_WITH_CONSTRAIN);
66+
public void testGettingUniqueConstraints() throws Exception {
67+
android.database.DatabaseUtils.createDbFromSqlStatements(getContext(), DB_NAME, 1, CREATE_TABLE_WITH_CONSTRAINT);
6668

6769
SQLiteDatabase db = getContext().openOrCreateDatabase(DB_NAME, 0, null);
68-
List<String> constrains = DBUtils.getUniqueConstrains(db, "t");
69-
MoreAsserts.assertContentsInAnyOrder(constrains, "const");
70+
List<Constraint> constrains = DBUtils.getUniqueConstraints(db, "t");
71+
MoreAsserts.assertContentsInAnyOrder(constrains, new Constraint(Arrays.asList("const")));
7072
}
7173

72-
public void testGettingUniqueConstrainsIsEmpty() throws Exception {
74+
public void testGettingMultiColumnUniqueConstraints() throws Exception {
75+
android.database.DatabaseUtils.createDbFromSqlStatements(getContext(), DB_NAME, 1, CREATE_TABLE_WITH_MULTI_COLUMN_CONSTRAINT);
76+
77+
SQLiteDatabase db = getContext().openOrCreateDatabase(DB_NAME, 0, null);
78+
List<Constraint> constrains = DBUtils.getUniqueConstraints(db, "t");
79+
MoreAsserts.assertContentsInAnyOrder(constrains, new Constraint(Arrays.asList("name", "desc")));
80+
}
81+
82+
public void testGettingUniqueConstraintsIsEmpty() throws Exception {
7383
android.database.DatabaseUtils.createDbFromSqlStatements(getContext(), DB_NAME, 1, CREATE_TABLES);
7484
SQLiteDatabase db = getContext().openOrCreateDatabase(DB_NAME, 0, null);
75-
List<String> constrains = DBUtils.getUniqueConstrains(db, "t");
85+
List<Constraint> constrains = DBUtils.getUniqueConstraints(db, "t");
7686
MoreAsserts.assertEmpty(constrains);
7787
}
7888
}

core/src/main/java/novoda/lib/sqliteprovider/provider/action/InsertHelper.java

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
import android.database.SQLException;
77
import android.net.Uri;
88

9+
import java.util.Arrays;
10+
import java.util.List;
11+
912
import novoda.lib.sqliteprovider.sqlite.ExtendedSQLiteOpenHelper;
13+
import novoda.lib.sqliteprovider.util.Constraint;
1014
import novoda.lib.sqliteprovider.util.Log;
1115
import novoda.lib.sqliteprovider.util.UriUtils;
1216

@@ -27,11 +31,11 @@ public InsertHelper(ExtendedSQLiteOpenHelper helper) {
2731
public long insert(Uri uri, ContentValues values) {
2832
ContentValues insertValues = (values != null) ? new ContentValues(values) : new ContentValues();
2933
final String table = UriUtils.getItemDirID(uri);
30-
final String firstConstrain = dbHelper.getFirstConstrain(table, insertValues);
34+
final Constraint constraint = dbHelper.getFirstConstraint(table, insertValues);
3135
appendParentReference(uri, insertValues);
3236
long rowId = -1;
33-
if (firstConstrain != null) {
34-
rowId = tryUpdateWithConstrain(table, firstConstrain, insertValues);
37+
if (constraint != null) {
38+
rowId = tryUpdateWithConstraint(table, constraint, insertValues);
3539
} else {
3640
if (Log.Provider.warningLoggingEnabled()) {
3741
Log.Provider.w("No constrain against URI: " + uri);
@@ -48,36 +52,77 @@ public long insert(Uri uri, ContentValues values) {
4852
throw new SQLException("Failed to insert row into " + uri);
4953
}
5054

55+
/**
56+
* Use {@link #tryUpdateWithConstraint(String, Constraint, ContentValues)}
57+
*/
58+
@Deprecated
5159
protected long tryUpdateWithConstrain(String table, String constrain, ContentValues values) {
5260
long rowId = -1;
5361
int update = dbHelper.getWritableDatabase().update(table, values, constrain + "=?",
54-
new String[] {
55-
values.getAsString(constrain)
56-
});
62+
new String[]{
63+
values.getAsString(constrain)
64+
});
5765

5866
if (Log.Provider.verboseLoggingEnabled()) {
5967
Log.Provider.v("Constrain " + constrain + " yield " + update);
6068
}
6169
if (update > 0) {
62-
rowId = getRowIdForUpdate(table, constrain, values);
70+
rowId = getRowIdForUpdate(table, new Constraint(Arrays.asList(constrain)), values);
6371
}
6472
return rowId;
6573
}
6674

75+
protected long tryUpdateWithConstraint(String table, Constraint constraint, ContentValues values) {
76+
long rowId = -1;
77+
String whereClause = getWhereClause(constraint);
78+
String[] whereArgs = getWhereArguments(constraint, values);
79+
int update = dbHelper.getWritableDatabase().update(table, values, whereClause, whereArgs);
80+
81+
if (Log.Provider.verboseLoggingEnabled()) {
82+
Log.Provider.v("Constrain " + constraint + " yield " + update);
83+
}
84+
if (update > 0) {
85+
rowId = getRowIdForUpdate(table, constraint, values);
86+
}
87+
return rowId;
88+
}
89+
90+
private String getWhereClause(Constraint constraint) {
91+
List<String> columns = constraint.getColumns();
92+
String whereClause = "";
93+
for (int i = 0; i < columns.size(); i++) {
94+
String column = columns.get(i);
95+
if (i > 0) {
96+
whereClause += " AND ";
97+
}
98+
whereClause += column + "=?";
99+
}
100+
return whereClause;
101+
}
102+
103+
private String[] getWhereArguments(Constraint constraint, ContentValues values) {
104+
List<String> columns = constraint.getColumns();
105+
int columnCount = columns.size();
106+
String[] whereArgs = new String[columnCount];
107+
for (int i = 0; i < columnCount; i++) {
108+
String column = columns.get(i);
109+
whereArgs[i] = values.getAsString(column);
110+
}
111+
return whereArgs;
112+
}
113+
67114
/**
68115
* Will get the Row id from the latest update.
69116
*
70117
* @param table
71-
* @param constrain
118+
* @param constraint
72119
* @param values
73120
* @return
74121
*/
75-
private long getRowIdForUpdate(String table, String constrain, ContentValues values) {
76-
final Cursor cur = dbHelper.getReadableDatabase().query(table, new String[] {
122+
private long getRowIdForUpdate(String table, Constraint constraint, ContentValues values) {
123+
final Cursor cur = dbHelper.getReadableDatabase().query(table, new String[]{
77124
"rowid"
78-
}, constrain + "=?", new String[] {
79-
values.getAsString(constrain)
80-
}, null, null, null);
125+
}, getWhereClause(constraint), getWhereArguments(constraint, values), null, null, null);
81126
if (!cur.moveToFirst()) {
82127
return -1;
83128
}

core/src/main/java/novoda/lib/sqliteprovider/sqlite/ExtendedSQLiteOpenHelper.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.Map;
1414

1515
import novoda.lib.sqliteprovider.migration.Migrations;
16+
import novoda.lib.sqliteprovider.util.Constraint;
1617
import novoda.lib.sqliteprovider.util.DBUtils;
1718
import novoda.lib.sqliteprovider.util.Log;
1819

@@ -26,8 +27,11 @@ public class ExtendedSQLiteOpenHelper extends SQLiteOpenHelper implements IDatab
2627
/*
2728
* We cache the constrains.
2829
*/
30+
@Deprecated
2931
private final Map<String, List<String>> constrains = new HashMap<String, List<String>>();
3032

33+
private final Map<String, List<Constraint>> constraints = new HashMap<String, List<Constraint>>();
34+
3135
public ExtendedSQLiteOpenHelper(Context context) throws IOException {
3236
this(context, null);
3337
}
@@ -85,6 +89,10 @@ public Map<String, String> getProjectionMap(String parent, String... foreignTabl
8589
return DBUtils.getProjectionMap(getReadableDatabase(), parent, foreignTables);
8690
}
8791

92+
/**
93+
* Use {@link #getUniqueConstraints(String)}
94+
*/
95+
@Deprecated
8896
@Override
8997
public List<String> getUniqueConstrains(String table) {
9098
if (!constrains.containsKey(table)) {
@@ -93,6 +101,18 @@ public List<String> getUniqueConstrains(String table) {
93101
return constrains.get(table);
94102
}
95103

104+
@Override
105+
public List<Constraint> getUniqueConstraints(String table) {
106+
if (!constraints.containsKey(table)) {
107+
constraints.put(table, DBUtils.getUniqueConstraints(getReadableDatabase(), table));
108+
}
109+
return constraints.get(table);
110+
}
111+
112+
/**
113+
* Use {@link #getFirstConstraint(String, ContentValues)}
114+
*/
115+
@Deprecated
96116
public String getFirstConstrain(String table, ContentValues values) {
97117
List<String> localConstrains = getUniqueConstrains(table);
98118
if (localConstrains == null) {
@@ -105,4 +125,23 @@ public String getFirstConstrain(String table, ContentValues values) {
105125
}
106126
return null;
107127
}
128+
129+
public Constraint getFirstConstraint(String table, ContentValues values) {
130+
List<Constraint> constraints = getUniqueConstraints(table);
131+
if (constraints == null) {
132+
return null;
133+
}
134+
for (Constraint constraint : constraints) {
135+
boolean isValidConstraint = true;
136+
for (String column : constraint.getColumns()) {
137+
if (!values.containsKey(column)) {
138+
isValidConstraint = false;
139+
}
140+
}
141+
if (isValidConstraint) {
142+
return constraint;
143+
}
144+
}
145+
return null;
146+
}
108147
}

core/src/main/java/novoda/lib/sqliteprovider/sqlite/IDatabaseMetaInfo.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,29 @@
44
import java.util.List;
55
import java.util.Map;
66

7+
import novoda.lib.sqliteprovider.util.Constraint;
8+
79
public interface IDatabaseMetaInfo {
810

11+
12+
913
public enum SQLiteType {
10-
NULL, INTEGER, REAL, TEXT, BLOB, NUMERIC
14+
NULL, INTEGER, REAL, TEXT, BLOB, NUMERIC;
1115
}
12-
1316
Map<String, SQLiteType> getColumns(String table);
1417

1518
List<String> getTables();
1619

1720
List<String> getForeignTables(String table);
1821

22+
/**
23+
* Use {@link #getUniqueConstraints(String)}
24+
*/
25+
@Deprecated
1926
List<String> getUniqueConstrains(String table);
2027

28+
List<Constraint> getUniqueConstraints(String table);
29+
2130
Map<String, String> getProjectionMap(String parent, String... foreignTables);
2231

2332
int getVersion();
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package novoda.lib.sqliteprovider.util;
2+
3+
import java.util.List;
4+
5+
public class Constraint {
6+
private final List<String> columns;
7+
8+
public Constraint(List<String> columns) {
9+
this.columns = columns;
10+
}
11+
12+
public List<String> getColumns() {
13+
return columns;
14+
}
15+
16+
@Override
17+
public boolean equals(Object o) {
18+
if (this == o) return true;
19+
if (o == null || getClass() != o.getClass()) return false;
20+
21+
Constraint that = (Constraint) o;
22+
23+
return columns.equals(that.columns);
24+
25+
}
26+
27+
@Override
28+
public int hashCode() {
29+
return columns.hashCode();
30+
}
31+
}

core/src/main/java/novoda/lib/sqliteprovider/util/DBUtils.java

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44
import android.database.Cursor;
55
import android.database.sqlite.SQLiteDatabase;
66

7-
import novoda.lib.sqliteprovider.sqlite.IDatabaseMetaInfo.SQLiteType;
8-
9-
import java.util.*;
7+
import java.util.ArrayList;
8+
import java.util.Arrays;
9+
import java.util.Collections;
10+
import java.util.HashMap;
11+
import java.util.List;
12+
import java.util.Map;
1013
import java.util.Map.Entry;
1114

15+
import novoda.lib.sqliteprovider.sqlite.IDatabaseMetaInfo.SQLiteType;
16+
1217
public final class DBUtils {
1318

1419
private static final String SELECT_TABLES_NAME = "SELECT name FROM sqlite_master WHERE type='table';";
@@ -19,7 +24,7 @@ public final class DBUtils {
1924

2025
private static final String PRGAMA_INDEX_INFO = "PRAGMA index_info('%1$s');";
2126

22-
private static List<String> defaultTables = Arrays.asList(new String[] {
27+
private static List<String> defaultTables = Arrays.asList(new String[]{
2328
"android_metadata"
2429
});
2530

@@ -113,6 +118,11 @@ public static String getSQLiteVersion() {
113118
return sqliteVersion.toString();
114119
}
115120

121+
122+
/**
123+
* Use {@link #getUniqueConstraints(SQLiteDatabase, String)}
124+
*/
125+
@Deprecated
116126
public static List<String> getUniqueConstrains(SQLiteDatabase db, String table) {
117127
List<String> constrains = new ArrayList<String>();
118128
final Cursor pragmas = db.rawQuery(String.format(PRGAMA_INDEX_LIST, table), null);
@@ -130,4 +140,26 @@ public static List<String> getUniqueConstrains(SQLiteDatabase db, String table)
130140
pragmas.close();
131141
return constrains;
132142
}
143+
144+
145+
public static List<Constraint> getUniqueConstraints(SQLiteDatabase db, String table) {
146+
List<Constraint> constraints = new ArrayList<Constraint>();
147+
final Cursor indexCursor = db.rawQuery(String.format(PRGAMA_INDEX_LIST, table), null);
148+
while (indexCursor.moveToNext()) {
149+
int isUnique = indexCursor.getInt(2);
150+
if (isUnique == 1) {
151+
String indexName = indexCursor.getString(1);
152+
final Cursor columnCursor = db.rawQuery(String.format(PRGAMA_INDEX_INFO, indexName), null);
153+
List<String> columns = new ArrayList<>(columnCursor.getCount());
154+
while (columnCursor.moveToNext()) {
155+
String columnName = columnCursor.getString(2);
156+
columns.add(columnName);
157+
}
158+
columnCursor.close();
159+
constraints.add(new Constraint(columns));
160+
}
161+
}
162+
indexCursor.close();
163+
return constraints;
164+
}
133165
}

0 commit comments

Comments
 (0)