package org.commcare.models.database.user.models;
import android.content.ContentValues;
import android.util.Pair;
import net.sqlcipher.Cursor;
import net.sqlcipher.database.SQLiteDatabase;
import org.commcare.CommCareApplication;
import org.commcare.android.database.user.models.ACase;
import org.commcare.cases.model.Case;
import org.commcare.cases.model.CaseIndex;
import org.commcare.cases.query.queryset.DualTableSingleMatchModelQuerySet;
import org.commcare.models.database.AndroidTableBuilder;
import org.commcare.models.database.DbUtil;
import org.commcare.models.database.SqlStorage;
import org.commcare.modern.database.DatabaseHelper;
import org.commcare.modern.database.DatabaseIndexingUtils;
import org.commcare.modern.engine.cases.CaseIndexTable;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Vector;
/**
* @author ctsims
*/
public class AndroidCaseIndexTable implements CaseIndexTable {
private static final String TABLE_NAME = "case_index_storage";
private static final String COL_CASE_RECORD_ID = "case_rec_id";
private static final String COL_INDEX_NAME = "name";
private static final String COL_INDEX_TYPE = "type";
private static final String COL_INDEX_TARGET = "target";
private final SQLiteDatabase db;
//TODO: We should do some synchronization to make it the case that nothing can hold
//an object for the same cache at once and let us manage the lifecycle
public AndroidCaseIndexTable() {
this.db = CommCareApplication.instance().getUserDbHandle();
}
public AndroidCaseIndexTable(SQLiteDatabase dbHandle) {
this.db = dbHandle;
}
public static String getTableDefinition() {
return "CREATE TABLE " + TABLE_NAME + "(" +
DatabaseHelper.ID_COL + " INTEGER PRIMARY KEY, " +
COL_CASE_RECORD_ID + ", " +
COL_INDEX_NAME + ", " +
COL_INDEX_TYPE + ", " +
COL_INDEX_TARGET +
")";
}
public static void createIndexes(SQLiteDatabase db) {
String recordFirstIndexId = "RECORD_NAME_ID_TARGET";
String recordFirstIndex = COL_CASE_RECORD_ID + ", " + COL_INDEX_NAME + ", " + COL_INDEX_TARGET;
db.execSQL(DatabaseIndexingUtils.indexOnTableCommand(recordFirstIndexId, TABLE_NAME, recordFirstIndex));
String typeFirstIndexId = "NAME_TARGET_RECORD";
String typeFirstIndex = COL_INDEX_NAME + ", " + COL_CASE_RECORD_ID + ", " + COL_INDEX_TARGET;
db.execSQL(DatabaseIndexingUtils.indexOnTableCommand(typeFirstIndexId, TABLE_NAME, typeFirstIndex));
}
/**
* Creates all indexes for this case.
* TODO: this doesn't ensure any sort of uniqueness, you should wipe constraints first
*/
public void indexCase(Case c) {
db.beginTransaction();
try {
for (CaseIndex ci : c.getIndices()) {
ContentValues cv = new ContentValues();
cv.put(COL_CASE_RECORD_ID, c.getID());
cv.put(COL_INDEX_NAME, ci.getName());
cv.put(COL_INDEX_TYPE, ci.getTargetType());
cv.put(COL_INDEX_TARGET, ci.getTarget());
db.insert(TABLE_NAME, null, cv);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void clearCaseIndices(Case c) {
clearCaseIndices(c.getID());
}
public void clearCaseIndices(int recordId) {
String recordIdString = String.valueOf(recordId);
db.beginTransaction();
try {
if (SqlStorage.STORAGE_OUTPUT_DEBUG) {
String sqlStatement = String.format("DELETE FROM %s WHERE %s = CAST(? as INT)", TABLE_NAME, COL_CASE_RECORD_ID);
DbUtil.explainSql(db, sqlStatement, new String[]{recordIdString});
}
//NOTE: The cast is very necessary, SQLite's type coercion has problems here because
//we can't provide arguments in any format other than a string
db.delete(TABLE_NAME, COL_CASE_RECORD_ID + "= CAST(? as INT)", new String[]{recordIdString});
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void clearCaseIndices(Collection<Integer> idsToClear) {
if (idsToClear.size() == 0) {
return;
}
db.beginTransaction();
try {
List<Pair<String, String[]>> whereParamList = AndroidTableBuilder.sqlList(idsToClear);
for (Pair<String, String[]> whereParams : whereParamList) {
db.delete(TABLE_NAME, COL_CASE_RECORD_ID + " IN " + whereParams.first, whereParams.second);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/**
* Get a list of Case Record id's for cases which index a provided value.
*
* @param indexName The name of the index
* @param targetValue The case targeted by the index
* @return An integer array of indexed case record ids
*/
public LinkedHashSet<Integer> getCasesMatchingIndex(String indexName, String targetValue) {
String[] args = new String[]{indexName, targetValue};
if (SqlStorage.STORAGE_OUTPUT_DEBUG) {
String query = String.format("SELECT %s FROM %s WHERE %s = ? AND %s = ?", COL_CASE_RECORD_ID, TABLE_NAME, COL_INDEX_NAME, COL_INDEX_TARGET);
DbUtil.explainSql(db, query, args);
}
Cursor c = db.query(TABLE_NAME, new String[]{COL_CASE_RECORD_ID}, COL_INDEX_NAME + " = ? AND " + COL_INDEX_TARGET + " = ?", args, null, null, null);
LinkedHashSet<Integer> ret = new LinkedHashSet<>();
SqlStorage.fillIdWindow(c, COL_CASE_RECORD_ID, ret);
return ret;
}
/**
* Get a list of Case Record id's for cases which index any of a set of provided values
*
* @param indexName The name of the index
* @param targetValueSet The set of cases targeted by the index
* @return An integer array of indexed case record ids
*/
public LinkedHashSet<Integer> getCasesMatchingValueSet(String indexName, String[] targetValueSet) {
String[] args = new String[1 + targetValueSet.length];
args[0] = indexName;
for (int i = 0; i < targetValueSet.length; ++i) {
args[i + 1] = targetValueSet[i];
}
String inSet = getArgumentBasedVariableSet(targetValueSet.length);
String whereExpr = String.format("%s = ? AND %s IN %s", COL_INDEX_NAME, COL_INDEX_TARGET, inSet);
if (SqlStorage.STORAGE_OUTPUT_DEBUG) {
String query = String.format("SELECT %s FROM %s WHERE %s", COL_CASE_RECORD_ID, TABLE_NAME, whereExpr);
DbUtil.explainSql(db, query, args);
}
Cursor c = db.query(TABLE_NAME, new String[]{COL_CASE_RECORD_ID}, whereExpr, args, null, null, null);
LinkedHashSet<Integer> ret = new LinkedHashSet<>();
SqlStorage.fillIdWindow(c, COL_CASE_RECORD_ID, ret);
return ret;
}
public int loadIntoIndexTable(HashMap<String, Vector<Integer>> indexCache, String indexName) {
int resultsReturned = 0;
String[] args = new String[]{indexName};
if (SqlStorage.STORAGE_OUTPUT_DEBUG) {
String query = String.format("SELECT %s,%s %s FROM %s where %s = '%s'", COL_CASE_RECORD_ID, COL_INDEX_NAME, COL_INDEX_TARGET, TABLE_NAME, COL_INDEX_NAME, indexName);
DbUtil.explainSql(db, query, null);
}
Cursor c = db.query(TABLE_NAME, new String[]{COL_CASE_RECORD_ID, COL_INDEX_NAME, COL_INDEX_TARGET},COL_INDEX_NAME + " = ?", args, null, null, null);
try {
if (c.moveToFirst()) {
while (!c.isAfterLast()) {
resultsReturned++;
int id = c.getInt(c.getColumnIndexOrThrow(COL_CASE_RECORD_ID));
String target = c.getString(c.getColumnIndexOrThrow(COL_INDEX_TARGET));
String cacheID = indexName + "|" + target;
Vector<Integer> cache;
if(indexCache.containsKey(cacheID)){
cache = indexCache.get(cacheID);
} else {
cache = new Vector<>();
}
cache.add(id);
indexCache.put(cacheID, cache);
c.moveToNext();
}
}
return resultsReturned;
} finally {
if (c != null) {
c.close();
}
}
}
/**
* Provided an index name and a list of case row ID's, provides a list of the row ID's of the
* cases which point to that ID
* @param cuedCases
* @return
*/
public DualTableSingleMatchModelQuerySet bulkReadIndexToCaseIdMatch(String indexName, Collection<Integer> cuedCases) {
DualTableSingleMatchModelQuerySet set = new DualTableSingleMatchModelQuerySet();
String caseIdIndex = AndroidTableBuilder.scrubName(Case.INDEX_CASE_ID);
List<Pair<String, String[]>> whereParamList = AndroidTableBuilder.sqlList(cuedCases, "CAST(? as INT)");
for(Pair<String, String[]> querySet : whereParamList) {
String query =String.format(
"SELECT %s,%s " +
"FROM %s " +
"INNER JOIN %s " +
"ON %s = %s " +
"WHERE %s = '%s' " +
"AND " +
"%s IN %s",
COL_CASE_RECORD_ID, ACase.STORAGE_KEY + "." + DatabaseHelper.ID_COL,
TABLE_NAME,
ACase.STORAGE_KEY,
COL_INDEX_TARGET, caseIdIndex,
COL_INDEX_NAME, indexName,
COL_CASE_RECORD_ID, querySet.first);
android.database.Cursor c = db.rawQuery(query, querySet.second);
try {
if (c.getCount() == 0) {
return set;
} else {
c.moveToFirst();
while (!c.isAfterLast()) {
int caseId = c.getInt(c.getColumnIndexOrThrow(COL_CASE_RECORD_ID));
int targetCase = c.getInt(c.getColumnIndex(DatabaseHelper.ID_COL));
set.loadResult(caseId, targetCase);
c.moveToNext();
}
}
} finally {
c.close();
}
}
return set;
}
public static String getArgumentBasedVariableSet(int number) {
StringBuffer sb = new StringBuffer();
sb.append("(");
for (int i = 0; i < number; i++) {
sb.append('?');
if (i < number - 1) {
sb.append(",");
}
}
sb.append(")");
return sb.toString();
}
}