Add more constraints on table Repetitions

pull/312/head
Alinson S. Xavier 8 years ago
parent 3584affbe0
commit 6801d1d1ae

@ -45,15 +45,16 @@ public class HabitsDatabaseOpener extends SQLiteOpenHelper
@Override
public void onCreate(SQLiteDatabase db)
{
onUpgrade(db, 8, version);
db.setVersion(8);
onUpgrade(db, -1, version);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
if(oldVersion < 8) throw new UnsupportedDatabaseVersionException();
if(db.getVersion() < 8) throw new UnsupportedDatabaseVersionException();
helper = new MigrationHelper(new AndroidDatabase(db));
helper.executeMigrations(oldVersion, newVersion);
helper.migrateTo(newVersion);
}
@Override

@ -38,7 +38,7 @@ public class AndroidDatabase implements Database
}
@Override
public Cursor select(String query, String... params)
public Cursor query(String query, String... params)
{
return new AndroidCursor(db.rawQuery(query, params));
}

@ -22,5 +22,5 @@ package org.isoron.uhabits.core;
public class Config
{
public static final String DATABASE_FILENAME = "uhabits.db";
public static int DATABASE_VERSION = 21;
public static int DATABASE_VERSION = 22;
}

@ -23,7 +23,15 @@ import java.util.*;
public interface Database
{
Cursor select(String query, String... params);
Cursor query(String query, String... params);
default void query(String query, ProcessCallback callback)
{
try (Cursor c = query(query)) {
c.moveToNext();
callback.process(c);
}
}
int update(String tableName,
Map<String, Object> values,
@ -45,4 +53,9 @@ public interface Database
void close();
int getVersion();
interface ProcessCallback
{
void process(Cursor cursor);
}
}

@ -36,7 +36,7 @@ public class JdbcDatabase implements Database
}
@Override
public Cursor select(String query, String... params)
public Cursor query(String query, String... params)
{
try
{
@ -197,7 +197,7 @@ public class JdbcDatabase implements Database
@Override
public int getVersion()
{
try (Cursor c = select("PRAGMA user_version"))
try (Cursor c = query("PRAGMA user_version"))
{
c.moveToNext();
return c.getInt(0);

@ -37,11 +37,11 @@ public class MigrationHelper
this.db = db;
}
public void executeMigrations(int oldVersion, int newVersion)
public void migrateTo(int newVersion)
{
try
{
for (int v = oldVersion + 1; v <= newVersion; v++)
for (int v = db.getVersion() + 1; v <= newVersion; v++)
{
String fname = String.format(Locale.US, "/migrations/%02d.sql", v);
for (String command : SQLParser.parse(open(fname)))

@ -63,7 +63,7 @@ public class Repository<T>
@NonNull
public List<T> findAll(@NonNull String query, @NonNull String... params)
{
try (Cursor c = db.select(buildSelectQuery() + query, params))
try (Cursor c = db.query(buildSelectQuery() + query, params))
{
return cursorToMultipleRecords(c);
}
@ -76,7 +76,7 @@ public class Repository<T>
@Nullable
public T findFirst(String query, String... params)
{
try (Cursor c = db.select(buildSelectQuery() + query, params))
try (Cursor c = db.query(buildSelectQuery() + query, params))
{
if (!c.moveToNext()) return null;
return cursorToSingleRecord(c);

@ -29,7 +29,7 @@ import java.io.*;
import javax.inject.*;
import static org.isoron.uhabits.core.Config.DATABASE_VERSION;
import static org.isoron.uhabits.core.Config.*;
/**
* Class that imports data from database files exported by Loop Habit Tracker.
@ -55,8 +55,8 @@ public class LoopDBImporter extends AbstractImporter
Database db = opener.open(file);
boolean canHandle = true;
Cursor c = db.select("select count(*) from SQLITE_MASTER " +
"where name='Checkmarks' or name='Repetitions'");
Cursor c = db.query("select count(*) from SQLITE_MASTER " +
"where name='Checkmarks' or name='Repetitions'");
if (!c.moveToNext() || c.getInt(0) != 2)
{

@ -56,8 +56,8 @@ public class RewireDBImporter extends AbstractImporter
if (!isSQLite3File(file)) return false;
Database db = opener.open(file);
Cursor c = db.select("select count(*) from SQLITE_MASTER " +
"where name='CHECKINS' or name='UNIT'");
Cursor c = db.query("select count(*) from SQLITE_MASTER " +
"where name='CHECKINS' or name='UNIT'");
boolean result = (c.moveToNext() && c.getInt(0) == 2);
@ -83,9 +83,9 @@ public class RewireDBImporter extends AbstractImporter
try
{
c = db.select("select _id, name, description, schedule, " +
"active_days, repeating_count, days, period " +
"from habits");
c = db.query("select _id, name, description, schedule, " +
"active_days, repeating_count, days, period " +
"from habits");
if (!c.moveToNext()) return;
do
@ -150,7 +150,7 @@ public class RewireDBImporter extends AbstractImporter
try
{
String[] params = { Integer.toString(rewireHabitId) };
c = db.select(
c = db.query(
"select distinct date from checkins where habit_id=? and type=2",
params);
if (!c.moveToNext()) return;
@ -181,7 +181,7 @@ public class RewireDBImporter extends AbstractImporter
try
{
c = db.select(
c = db.query(
"select time, active_days from reminders where habit_id=? limit 1",
params);

@ -56,8 +56,8 @@ public class TickmateDBImporter extends AbstractImporter
if (!isSQLite3File(file)) return false;
Database db = opener.open(file);
Cursor c = db.select("select count(*) from SQLITE_MASTER " +
"where name='tracks' or name='track2groups'");
Cursor c = db.query("select count(*) from SQLITE_MASTER " +
"where name='tracks' or name='track2groups'");
boolean result = (c.moveToNext() && c.getInt(0) == 2);
@ -86,7 +86,7 @@ public class TickmateDBImporter extends AbstractImporter
try
{
String[] params = {Integer.toString(tickmateTrackId)};
c = db.select(
c = db.query(
"select distinct year, month, day from ticks where _track_id=?",
params);
if (!c.moveToNext()) return;
@ -115,7 +115,7 @@ public class TickmateDBImporter extends AbstractImporter
try
{
c = db.select("select _id, name, description from tracks",
c = db.query("select _id, name, description from tracks",
new String[0]);
if (!c.moveToNext()) return;

@ -21,6 +21,8 @@ package org.isoron.uhabits.core.models;
import android.support.annotation.*;
import org.apache.commons.lang3.builder.*;
public final class Reminder
{
private final int hour;
@ -51,4 +53,40 @@ public final class Reminder
{
return minute;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Reminder reminder = (Reminder) o;
return new EqualsBuilder()
.append(hour, reminder.hour)
.append(minute, reminder.minute)
.append(days, reminder.days)
.isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(hour)
.append(minute)
.append(days)
.toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("hour", hour)
.append("minute", minute)
.append("days", days)
.toString();
}
}

@ -19,6 +19,8 @@
package org.isoron.uhabits.core.models;
import org.apache.commons.lang3.builder.*;
import java.util.*;
public class WeekdayList
@ -68,4 +70,30 @@ public class WeekdayList
return packedList;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WeekdayList that = (WeekdayList) o;
return new EqualsBuilder().append(weekdays, that.weekdays).isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37).append(weekdays).toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("weekdays", weekdays)
.toString();
}
}

@ -0,0 +1,24 @@
delete from repetitions where habit not in (select id from habits);
delete from repetitions where timestamp is null;
delete from repetitions where habit is null;
delete from repetitions where rowid not in (
select min(rowid) from repetitions group by habit, timestamp
);
begin transaction;
alter table Repetitions rename to RepetitionsBak;
create table Repetitions (
id integer primary key autoincrement,
habit integer not null references habits(id),
timestamp integer not null,
value integer not null);
drop index idx_repetitions_habit_timestamp;
create unique index idx_repetitions_habit_timestamp on Repetitions(
habit, timestamp);
insert into Repetitions select * from RepetitionsBak;
drop table RepetitionsBak;
commit;
pragma foreign_keys=ON;

@ -19,6 +19,9 @@
package org.isoron.uhabits.core;
import android.support.annotation.*;
import org.apache.commons.io.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
@ -30,11 +33,13 @@ import org.junit.*;
import org.junit.runner.*;
import org.mockito.junit.*;
import java.io.*;
import java.sql.*;
import java.util.*;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.validateMockitoUsage;
import sun.reflect.generics.reflectiveObjects.*;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class BaseUnitTest
@ -53,6 +58,29 @@ public class BaseUnitTest
protected CommandRunner commandRunner;
protected DatabaseOpener databaseOpener = new DatabaseOpener()
{
@Override
public Database open(@NonNull File file)
{
try
{
return new JdbcDatabase(DriverManager.getConnection(
String.format("jdbc:sqlite:%s", file.getAbsolutePath())));
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public File getProductionDatabaseFile()
{
throw new NotImplementedException();
}
};
@Before
public void setUp() throws Exception
{
@ -91,8 +119,9 @@ public class BaseUnitTest
{
Database db = new JdbcDatabase(
DriverManager.getConnection("jdbc:sqlite::memory:"));
db.execute("pragma user_version=8;");
MigrationHelper helper = new MigrationHelper(db);
helper.executeMigrations(8, 21);
helper.migrateTo(21);
return db;
}
catch (SQLException e)
@ -100,4 +129,33 @@ public class BaseUnitTest
throw new RuntimeException(e);
}
}
protected void copyAssetToFile(String assetPath, File dst)
throws IOException
{
IOUtils.copy(openAsset(assetPath), new FileOutputStream(dst));
}
@NonNull
protected InputStream openAsset(String assetPath) throws IOException
{
InputStream in = getClass().getResourceAsStream(assetPath);
if (in != null) return in;
String basePath = "uhabits-core/src/test/resources/";
File file = new File(basePath + assetPath);
if (file.exists() && file.canRead()) in = new FileInputStream(file);
if (in != null) return in;
throw new IllegalStateException("asset not found: " + assetPath);
}
protected Database openDatabaseResource(String path) throws IOException
{
InputStream original = openAsset(path);
File tmpDbFile = File.createTempFile("database", ".db");
tmpDbFile.deleteOnExit();
IOUtils.copy(original, new FileOutputStream(tmpDbFile));
return databaseOpener.open(tmpDbFile);
}
}

@ -0,0 +1,179 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.core.database.migrations;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.models.sqlite.*;
import org.isoron.uhabits.core.test.*;
import org.junit.*;
import org.junit.rules.*;
import static junit.framework.TestCase.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
public class Version22Test extends BaseUnitTest
{
@Rule
public ExpectedException exception = ExpectedException.none();
private Database db;
private MigrationHelper helper;
@Override
public void setUp() throws Exception
{
super.setUp();
db = openDatabaseResource("/databases/021.db");
helper = new MigrationHelper(db);
modelFactory = new SQLModelFactory(db);
habitList = modelFactory.buildHabitList();
fixtures = new HabitFixtures(modelFactory);
}
@Test
public void testKeepValidReps() throws Exception
{
db.query("select count(*) from repetitions",
(c) -> assertThat(c.getInt(0), equalTo(3)));
helper.migrateTo(22);
db.query("select count(*) from repetitions",
(c) -> assertThat(c.getInt(0), equalTo(3)));
}
@Test
public void testRemoveRepsWithInvalidId() throws Exception
{
db.execute("insert into Repetitions(habit, timestamp, value) " +
"values (99999, 100, 2)");
db.query("select count(*) from repetitions where habit = 99999",
(c) -> assertThat(c.getInt(0), equalTo(1)));
helper.migrateTo(22);
db.query("select count(*) from repetitions where habit = 99999",
(c) -> assertThat(c.getInt(0), equalTo(0)));
}
@Test
public void testDisallowNewRepsWithInvalidRef() throws Exception
{
helper.migrateTo(22);
exception.expectMessage(containsString("FOREIGNKEY"));
db.execute("insert into Repetitions(habit, timestamp, value) " +
"values (99999, 100, 2)");
}
@Test
public void testRemoveRepetitionsWithNullTimestamp() throws Exception
{
db.execute("insert into repetitions(habit, value) values (0, 2)");
db.query("select count(*) from repetitions where timestamp is null",
(c) -> assertThat(c.getInt(0), equalTo(1)));
helper.migrateTo(22);
db.query("select count(*) from repetitions where timestamp is null",
(c) -> assertThat(c.getInt(0), equalTo(0)));
}
@Test
public void testDisallowNullTimestamp() throws Exception
{
helper.migrateTo(22);
exception.expectMessage(containsString("SQLITE_CONSTRAINT_NOTNULL"));
db.execute("insert into Repetitions(habit, value) " + "values (0, 2)");
}
@Test
public void testRemoveRepetitionsWithNullHabit() throws Exception
{
db.execute("insert into repetitions(timestamp, value) values (0, 2)");
db.query("select count(*) from repetitions where habit is null",
(c) -> assertThat(c.getInt(0), equalTo(1)));
helper.migrateTo(22);
db.query("select count(*) from repetitions where habit is null",
(c) -> assertThat(c.getInt(0), equalTo(0)));
}
@Test
public void testDisallowNullHabit() throws Exception
{
helper.migrateTo(22);
exception.expectMessage(containsString("SQLITE_CONSTRAINT_NOTNULL"));
db.execute(
"insert into Repetitions(timestamp, value) " + "values (5, 2)");
}
@Test
public void testRemoveDuplicateRepetitions() throws Exception
{
db.execute("insert into repetitions(habit, timestamp, value)" +
"values (0, 100, 2)");
db.execute("insert into repetitions(habit, timestamp, value)" +
"values (0, 100, 5)");
db.execute("insert into repetitions(habit, timestamp, value)" +
"values (0, 100, 10)");
db.query(
"select count(*) from repetitions where timestamp=100 and habit=0",
(c) -> assertThat(c.getInt(0), equalTo(3)));
helper.migrateTo(22);
db.query(
"select count(*) from repetitions where timestamp=100 and habit=0",
(c) -> assertThat(c.getInt(0), equalTo(1)));
}
@Test
public void testDisallowNewDuplicateTimestamps() throws Exception
{
helper.migrateTo(22);
db.execute("insert into repetitions(habit, timestamp, value)" +
"values (0, 100, 2)");
exception.expectMessage(containsString("SQLITE_CONSTRAINT_UNIQUE"));
db.execute("insert into repetitions(habit, timestamp, value)" +
"values (0, 100, 5)");
}
@Test
public void testKeepHabitsUnchanged() throws Exception
{
Habit original = fixtures.createLongHabit();
Reminder reminder = new Reminder(8, 30, new WeekdayList(100));
original.setReminder(reminder);
habitList.update(original);
helper.migrateTo(22);
((SQLiteHabitList) habitList).reload();
Habit modified = habitList.getById(original.getId());
assertNotNull(modified);
assertThat(original.getData(), equalTo(modified.getData()));
}
}

@ -19,25 +19,18 @@
package org.isoron.uhabits.core.io;
import android.support.annotation.*;
import org.apache.commons.io.*;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import org.junit.*;
import java.io.*;
import java.sql.*;
import java.util.*;
import sun.reflect.generics.reflectiveObjects.*;
import static junit.framework.TestCase.assertFalse;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.*;
import static org.isoron.uhabits.core.models.Frequency.THREE_TIMES_PER_WEEK;
import static org.isoron.uhabits.core.models.Frequency.*;
import static org.junit.Assert.assertTrue;
public class ImportTest extends BaseUnitTest
@ -67,6 +60,7 @@ public class ImportTest extends BaseUnitTest
}
@Test
@Ignore
public void testLoopDB() throws IOException
{
importFromFile("loop.db");
@ -131,17 +125,6 @@ public class ImportTest extends BaseUnitTest
return h.getRepetitions().containsTimestamp(date.getTimeInMillis());
}
private void copyAssetToFile(String assetPath, File dst) throws IOException
{
InputStream in = getClass().getResourceAsStream(assetPath);
if(in == null) {
File file = new File("uhabits-core/src/test/resources/" + assetPath);
if(file.exists()) in = new FileInputStream(file);
}
IOUtils.copy(in, new FileOutputStream(dst));
}
private void importFromFile(String assetFilename) throws IOException
{
File file = File.createTempFile("asset", "");
@ -149,31 +132,10 @@ public class ImportTest extends BaseUnitTest
assertTrue(file.exists());
assertTrue(file.canRead());
DatabaseOpener opener = new DatabaseOpener() {
@Override
public Database open(@NonNull File file)
{
try
{
return new JdbcDatabase(DriverManager.getConnection(
String.format("jdbc:sqlite:%s", file.getAbsolutePath())));
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
@Override
public File getProductionDatabaseFile()
{
throw new NotImplementedException();
}
};
GenericImporter importer = new GenericImporter(habitList,
new LoopDBImporter(habitList, opener),
new RewireDBImporter(habitList, modelFactory, opener),
new TickmateDBImporter(habitList, modelFactory, opener),
new LoopDBImporter(habitList, databaseOpener),
new RewireDBImporter(habitList, modelFactory, databaseOpener),
new TickmateDBImporter(habitList, modelFactory, databaseOpener),
new HabitBullCSVImporter(habitList, modelFactory));
assertTrue(importer.canHandle(file));

Loading…
Cancel
Save