Remove old Database/Cursor/JdbcDatabase/MigrationHelper interface
This commit is contained in:
parent
b2f2e1f562
commit
0dbebecbfb
@ -19,28 +19,93 @@
|
||||
package org.isoron.uhabits.database
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.core.IsEqual.equalTo
|
||||
import org.isoron.platform.io.StepResult
|
||||
import org.isoron.platform.io.begin
|
||||
import org.isoron.platform.io.commit
|
||||
import org.isoron.platform.io.query
|
||||
import org.isoron.platform.io.queryInt
|
||||
import org.isoron.platform.io.querySingle
|
||||
import org.isoron.platform.io.run
|
||||
import org.isoron.uhabits.BaseAndroidTest
|
||||
import org.isoron.uhabits.core.database.Cursor
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class AndroidDatabaseTest : BaseAndroidTest() {
|
||||
private lateinit var db: AndroidDatabase
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
db = AndroidDatabase(SQLiteDatabase.create(null), null)
|
||||
db.execute("create table test(color int, name string)")
|
||||
db = AndroidDatabase(SQLiteDatabase.create(null))
|
||||
db.run("create table test(color int, name string)")
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testInsert() {
|
||||
val map = mapOf(Pair("name", "asd"), Pair("color", null))
|
||||
db.insert("test", map)
|
||||
val c: Cursor = db.query("select * from test")
|
||||
c.moveToNext()
|
||||
c.getInt(0)!!
|
||||
assertThat(c.getString(1), equalTo("asd"))
|
||||
fun testInsertAndQuery() {
|
||||
db.run("insert into test(color, name) values (?, ?)") {
|
||||
bindNull(1)
|
||||
bindText(2, "asd")
|
||||
}
|
||||
val stmt = db.prepareStatement("select color, name from test")
|
||||
assertEquals(StepResult.ROW, stmt.step())
|
||||
assertNull(stmt.getIntOrNull(0))
|
||||
assertEquals("asd", stmt.getText(1))
|
||||
assertEquals(StepResult.DONE, stmt.step())
|
||||
stmt.finalize()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTransactionsViaRawSQL() {
|
||||
db.run("create table t(v int)")
|
||||
db.begin()
|
||||
db.run("insert into t(v) values (1)")
|
||||
db.run("insert into t(v) values (2)")
|
||||
db.commit()
|
||||
assertEquals(2, db.queryInt("select count(*) from t"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testQueryHelpers() {
|
||||
db.run("create table t(id int, name text)")
|
||||
db.run("insert into t(id, name) values (1, 'Alice')")
|
||||
db.run("insert into t(id, name) values (2, 'Bob')")
|
||||
|
||||
val names = mutableListOf<String>()
|
||||
db.query("select name from t order by id") { stmt ->
|
||||
names.add(stmt.getText(0))
|
||||
}
|
||||
assertEquals(listOf("Alice", "Bob"), names)
|
||||
|
||||
val single = db.querySingle("select name from t where id = ?", "2") {
|
||||
it.getText(0)
|
||||
}
|
||||
assertEquals("Bob", single)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNestedQueriesOnAndroid() {
|
||||
db.run("create table groups_t(id int, name text)")
|
||||
db.run("create table items(group_id int, label text)")
|
||||
db.run("insert into groups_t(id, name) values (1, 'G1')")
|
||||
db.run("insert into groups_t(id, name) values (2, 'G2')")
|
||||
db.run("insert into items(group_id, label) values (1, 'A')")
|
||||
db.run("insert into items(group_id, label) values (1, 'B')")
|
||||
db.run("insert into items(group_id, label) values (2, 'C')")
|
||||
|
||||
val result = mutableMapOf<String, MutableList<String>>()
|
||||
db.query("select id, name from groups_t order by id") { g ->
|
||||
val gId = g.getInt(0)
|
||||
val gName = g.getText(1)
|
||||
val items = mutableListOf<String>()
|
||||
db.query(
|
||||
"select label from items where group_id = ? order by label",
|
||||
gId.toString()
|
||||
) { item ->
|
||||
items.add(item.getText(0))
|
||||
}
|
||||
result[gName] = items
|
||||
}
|
||||
assertEquals(listOf("A", "B"), result["G1"]!!.toList())
|
||||
assertEquals(listOf("C"), result["G2"]!!.toList())
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,6 @@ import org.isoron.platform.io.migrateTo
|
||||
import org.isoron.platform.io.setVersion
|
||||
import org.isoron.uhabits.core.database.UnsupportedDatabaseVersionException
|
||||
import org.isoron.uhabits.database.AndroidDatabase
|
||||
import java.io.File
|
||||
|
||||
class HabitsDatabaseOpener(
|
||||
private val context: Context,
|
||||
@ -52,7 +51,7 @@ class HabitsDatabaseOpener(
|
||||
) {
|
||||
db.disableWriteAheadLogging()
|
||||
if (db.version < 8) throw UnsupportedDatabaseVersionException()
|
||||
val wrappedDb = AndroidDatabase(db, File(databaseFilename))
|
||||
val wrappedDb = AndroidDatabase(db)
|
||||
wrappedDb.setVersion(db.version)
|
||||
wrappedDb.migrateTo(newVersion) { version ->
|
||||
val filename = "%02d.sql".format(version)
|
||||
|
||||
@ -19,12 +19,10 @@
|
||||
|
||||
package org.isoron.uhabits.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteStatement
|
||||
import org.isoron.platform.io.PreparedStatement
|
||||
import org.isoron.platform.io.StepResult
|
||||
import java.io.File
|
||||
|
||||
class AndroidPreparedStatement(
|
||||
private val db: SQLiteDatabase,
|
||||
@ -128,81 +126,12 @@ class AndroidPreparedStatement(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements both the new multiplatform Database interface (for repositories)
|
||||
* and the old JVM-only Database interface (for importers that still use Cursor).
|
||||
*/
|
||||
class AndroidDatabase(
|
||||
private val db: SQLiteDatabase,
|
||||
override val file: File? = null
|
||||
) : org.isoron.platform.io.Database, org.isoron.uhabits.core.database.Database {
|
||||
private val db: SQLiteDatabase
|
||||
) : org.isoron.platform.io.Database {
|
||||
|
||||
// New PreparedStatement-based interface
|
||||
override fun prepareStatement(sql: String): PreparedStatement =
|
||||
AndroidPreparedStatement(db, sql)
|
||||
|
||||
// Old Cursor-based interface
|
||||
override fun query(q: String, vararg params: String) = AndroidCursor(db.rawQuery(q, params))
|
||||
override fun execute(query: String, vararg params: Any) = db.execSQL(query, params)
|
||||
|
||||
override fun update(
|
||||
tableName: String,
|
||||
values: Map<String, Any?>,
|
||||
where: String,
|
||||
vararg params: String
|
||||
): Int {
|
||||
val contValues = mapToContentValues(values)
|
||||
return db.update(tableName, contValues, where, params)
|
||||
}
|
||||
|
||||
override fun insert(tableName: String, values: Map<String, Any?>): Long {
|
||||
val contValues = mapToContentValues(values)
|
||||
return db.insert(tableName, null, contValues)
|
||||
}
|
||||
|
||||
override fun delete(tableName: String, where: String, vararg params: String) {
|
||||
db.delete(tableName, where, params)
|
||||
}
|
||||
|
||||
override fun beginTransaction() = db.beginTransaction()
|
||||
override fun setTransactionSuccessful() = db.setTransactionSuccessful()
|
||||
override fun endTransaction() = db.endTransaction()
|
||||
|
||||
override fun close() = db.close()
|
||||
|
||||
override val version: Int
|
||||
get() = db.version
|
||||
|
||||
private fun mapToContentValues(map: Map<String, Any?>): ContentValues {
|
||||
val values = ContentValues()
|
||||
for ((key, value) in map) {
|
||||
when (value) {
|
||||
null -> values.putNull(key)
|
||||
is Int -> values.put(key, value)
|
||||
is Long -> values.put(key, value)
|
||||
is Double -> values.put(key, value)
|
||||
is String -> values.put(key, value)
|
||||
else -> throw IllegalStateException("unsupported type: $value")
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidCursor(private val cursor: android.database.Cursor) : org.isoron.uhabits.core.database.Cursor {
|
||||
|
||||
override fun close() = cursor.close()
|
||||
override fun moveToNext() = cursor.moveToNext()
|
||||
|
||||
override fun getInt(index: Int): Int? =
|
||||
if (cursor.isNull(index)) null else cursor.getInt(index)
|
||||
|
||||
override fun getLong(index: Int): Long? =
|
||||
if (cursor.isNull(index)) null else cursor.getLong(index)
|
||||
|
||||
override fun getDouble(index: Int): Double? =
|
||||
if (cursor.isNull(index)) null else cursor.getDouble(index)
|
||||
|
||||
override fun getString(index: Int): String? =
|
||||
if (cursor.isNull(index)) null else cursor.getString(index)
|
||||
}
|
||||
|
||||
@ -21,19 +21,17 @@ package org.isoron.uhabits.database
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.isoron.uhabits.core.database.DatabaseOpener
|
||||
import java.io.File
|
||||
import org.isoron.platform.io.DatabaseOpener
|
||||
|
||||
@Inject
|
||||
class AndroidDatabaseOpener() : DatabaseOpener {
|
||||
override fun open(file: File): AndroidDatabase {
|
||||
override fun open(path: String): AndroidDatabase {
|
||||
return AndroidDatabase(
|
||||
db = SQLiteDatabase.openDatabase(
|
||||
file.absolutePath,
|
||||
path,
|
||||
null,
|
||||
SQLiteDatabase.OPEN_READWRITE
|
||||
),
|
||||
file = file
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,10 +21,9 @@ package org.isoron.uhabits.inject
|
||||
import android.content.Context
|
||||
import me.tatarka.inject.annotations.Component
|
||||
import me.tatarka.inject.annotations.Provides
|
||||
import org.isoron.platform.io.DatabaseOpener
|
||||
import org.isoron.uhabits.core.AppScope
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.database.Database
|
||||
import org.isoron.uhabits.core.database.DatabaseOpener
|
||||
import org.isoron.uhabits.core.io.GenericImporter
|
||||
import org.isoron.uhabits.core.io.Logging
|
||||
import org.isoron.uhabits.core.models.HabitList
|
||||
@ -85,13 +84,9 @@ abstract class HabitsApplicationComponent(
|
||||
get() = providedDb
|
||||
|
||||
private val providedDb: AndroidDatabase by lazy {
|
||||
AndroidDatabase(DatabaseUtils.openDatabase(), dbFile)
|
||||
AndroidDatabase(DatabaseUtils.openDatabase())
|
||||
}
|
||||
|
||||
@AppScope
|
||||
@Provides
|
||||
open fun database(): Database = providedDb
|
||||
|
||||
@AppScope
|
||||
@Provides
|
||||
open fun preferences(storage: SharedPreferencesStorage): Preferences =
|
||||
|
||||
@ -77,14 +77,41 @@ fun Database.commit() {
|
||||
run("COMMIT")
|
||||
}
|
||||
|
||||
inline fun Database.query(
|
||||
sql: String,
|
||||
vararg params: String,
|
||||
block: (PreparedStatement) -> Unit
|
||||
) {
|
||||
val stmt = prepareStatement(sql)
|
||||
for (i in params.indices) {
|
||||
stmt.bindText(i + 1, params[i])
|
||||
}
|
||||
while (stmt.step() == StepResult.ROW) {
|
||||
block(stmt)
|
||||
}
|
||||
stmt.finalize()
|
||||
}
|
||||
|
||||
inline fun <T> Database.querySingle(
|
||||
sql: String,
|
||||
vararg params: String,
|
||||
block: (PreparedStatement) -> T
|
||||
): T? {
|
||||
val stmt = prepareStatement(sql)
|
||||
for (i in params.indices) {
|
||||
stmt.bindText(i + 1, params[i])
|
||||
}
|
||||
val result = if (stmt.step() == StepResult.ROW) block(stmt) else null
|
||||
stmt.finalize()
|
||||
return result
|
||||
}
|
||||
|
||||
fun Database.migrateTo(targetVersion: Int, loadMigrationSQL: (Int) -> String) {
|
||||
val currentVersion = getVersion()
|
||||
if (currentVersion >= targetVersion) return
|
||||
begin()
|
||||
for (v in (currentVersion + 1)..targetVersion) {
|
||||
val commands = SQLParser.parse(loadMigrationSQL(v))
|
||||
for (cmd in commands) run(cmd)
|
||||
setVersion(v)
|
||||
}
|
||||
commit()
|
||||
}
|
||||
|
||||
@ -0,0 +1,146 @@
|
||||
package org.isoron.platform.io
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DatabaseQueryHelpersTest {
|
||||
@Test
|
||||
fun testQueryIteratesAllRows() {
|
||||
val db = TestDatabaseHelper.createEmptyDatabase()
|
||||
db.run("create table t(v int)")
|
||||
db.run("insert into t(v) values (10)")
|
||||
db.run("insert into t(v) values (20)")
|
||||
db.run("insert into t(v) values (30)")
|
||||
|
||||
val values = mutableListOf<Int>()
|
||||
db.query("select v from t order by v") { stmt ->
|
||||
values.add(stmt.getInt(0))
|
||||
}
|
||||
assertEquals(listOf(10, 20, 30), values)
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testQueryWithParameters() {
|
||||
val db = TestDatabaseHelper.createEmptyDatabase()
|
||||
db.run("create table t(name text, age int)")
|
||||
db.run("insert into t(name, age) values ('Alice', 30)")
|
||||
db.run("insert into t(name, age) values ('Bob', 25)")
|
||||
db.run("insert into t(name, age) values ('Carol', 35)")
|
||||
|
||||
val names = mutableListOf<String>()
|
||||
db.query("select name from t where age > ?", "28") { stmt ->
|
||||
names.add(stmt.getText(0))
|
||||
}
|
||||
assertEquals(2, names.size)
|
||||
assertTrue(names.contains("Alice"))
|
||||
assertTrue(names.contains("Carol"))
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testQueryWithNoResults() {
|
||||
val db = TestDatabaseHelper.createEmptyDatabase()
|
||||
db.run("create table t(v int)")
|
||||
|
||||
var called = false
|
||||
db.query("select v from t") { called = true }
|
||||
assertFalse(called)
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testQuerySingleReturnsFirstRow() {
|
||||
val db = TestDatabaseHelper.createEmptyDatabase()
|
||||
db.run("create table t(v int)")
|
||||
db.run("insert into t(v) values (42)")
|
||||
db.run("insert into t(v) values (99)")
|
||||
|
||||
val result = db.querySingle("select v from t order by v") { it.getInt(0) }
|
||||
assertEquals(42, result)
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testQuerySingleReturnsNullForEmptyResult() {
|
||||
val db = TestDatabaseHelper.createEmptyDatabase()
|
||||
db.run("create table t(v int)")
|
||||
|
||||
val result = db.querySingle("select v from t") { it.getInt(0) }
|
||||
assertNull(result)
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testQuerySingleWithParameters() {
|
||||
val db = TestDatabaseHelper.createEmptyDatabase()
|
||||
db.run("create table t(name text, score int)")
|
||||
db.run("insert into t(name, score) values ('Alice', 90)")
|
||||
db.run("insert into t(name, score) values ('Bob', 80)")
|
||||
|
||||
val score = db.querySingle(
|
||||
"select score from t where name = ?",
|
||||
"Alice"
|
||||
) { it.getInt(0) }
|
||||
assertEquals(90, score)
|
||||
|
||||
val missing = db.querySingle(
|
||||
"select score from t where name = ?",
|
||||
"Nobody"
|
||||
) { it.getInt(0) }
|
||||
assertNull(missing)
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNestedQueries() {
|
||||
val db = TestDatabaseHelper.createEmptyDatabase()
|
||||
db.run("create table parents(id int, name text)")
|
||||
db.run("create table children(parent_id int, name text)")
|
||||
db.run("insert into parents(id, name) values (1, 'Alice')")
|
||||
db.run("insert into parents(id, name) values (2, 'Bob')")
|
||||
db.run("insert into children(parent_id, name) values (1, 'Charlie')")
|
||||
db.run("insert into children(parent_id, name) values (1, 'Diana')")
|
||||
db.run("insert into children(parent_id, name) values (2, 'Eve')")
|
||||
|
||||
val result = mutableMapOf<String, MutableList<String>>()
|
||||
db.query("select id, name from parents order by id") { parent ->
|
||||
val parentId = parent.getInt(0)
|
||||
val parentName = parent.getText(1)
|
||||
val kids = mutableListOf<String>()
|
||||
db.query(
|
||||
"select name from children where parent_id = ? order by name",
|
||||
parentId.toString()
|
||||
) { child ->
|
||||
kids.add(child.getText(0))
|
||||
}
|
||||
result[parentName] = kids
|
||||
}
|
||||
|
||||
assertEquals(listOf("Charlie", "Diana"), result["Alice"]!!.toList())
|
||||
assertEquals(listOf("Eve"), result["Bob"]!!.toList())
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testQueryHandlesNullableColumns() {
|
||||
val db = TestDatabaseHelper.createEmptyDatabase()
|
||||
db.run("create table t(a int, b text)")
|
||||
db.run("insert into t(a, b) values (1, 'hello')")
|
||||
db.run("insert into t(a, b) values (null, null)")
|
||||
|
||||
val results = mutableListOf<Pair<Int?, String?>>()
|
||||
db.query("select a, b from t order by rowid") { stmt ->
|
||||
results.add(Pair(stmt.getIntOrNull(0), stmt.getTextOrNull(1)))
|
||||
}
|
||||
assertEquals(2, results.size)
|
||||
assertEquals(1, results[0].first)
|
||||
assertEquals("hello", results[0].second)
|
||||
assertNull(results[1].first)
|
||||
assertNull(results[1].second)
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* 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
|
||||
|
||||
import java.io.Closeable
|
||||
|
||||
interface Cursor : Closeable {
|
||||
|
||||
override fun close()
|
||||
|
||||
/**
|
||||
* Moves the cursor forward one row from its current position. Returns
|
||||
* true if the current position is valid, or false if the cursor is already
|
||||
* past the last row. The cursor start at position -1, so this method must
|
||||
* be called first.
|
||||
*/
|
||||
fun moveToNext(): Boolean
|
||||
|
||||
/**
|
||||
* Retrieves the value of the designated column in the current row of this
|
||||
* Cursor as an Integer. If the value is null, returns null. The first
|
||||
* column has index zero.
|
||||
*/
|
||||
fun getInt(index: Int): Int?
|
||||
|
||||
/**
|
||||
* Retrieves the value of the designated column in the current row of this
|
||||
* Cursor as a Long. If the value is null, returns null. The first
|
||||
* column has index zero.
|
||||
*/
|
||||
fun getLong(index: Int): Long?
|
||||
|
||||
/**
|
||||
* Retrieves the value of the designated column in the current row of this
|
||||
* Cursor as a Double. If the value is null, returns null. The first
|
||||
* column has index zero.
|
||||
*/
|
||||
fun getDouble(index: Int): Double?
|
||||
|
||||
/**
|
||||
* Retrieves the value of the designated column in the current row of this
|
||||
* Cursor as a String. If the value is null, returns null. The first
|
||||
* column has index zero.
|
||||
*/
|
||||
fun getString(index: Int): String?
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* 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
|
||||
|
||||
import java.io.File
|
||||
|
||||
interface Database {
|
||||
|
||||
fun query(q: String, vararg params: String): Cursor
|
||||
|
||||
fun query(q: String, callback: ProcessCallback) {
|
||||
query(q).use { c ->
|
||||
c.moveToNext()
|
||||
callback.process(c)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(
|
||||
tableName: String,
|
||||
values: Map<String, Any?>,
|
||||
where: String,
|
||||
vararg params: String
|
||||
): Int
|
||||
|
||||
fun insert(tableName: String, values: Map<String, Any?>): Long?
|
||||
|
||||
fun delete(tableName: String, where: String, vararg params: String)
|
||||
|
||||
fun execute(query: String, vararg params: Any)
|
||||
|
||||
fun beginTransaction()
|
||||
|
||||
fun setTransactionSuccessful()
|
||||
|
||||
fun endTransaction()
|
||||
|
||||
fun close()
|
||||
|
||||
val version: Int
|
||||
|
||||
val file: File?
|
||||
|
||||
fun interface ProcessCallback {
|
||||
fun process(cursor: Cursor)
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* 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
|
||||
|
||||
import java.io.File
|
||||
|
||||
interface DatabaseOpener {
|
||||
fun open(file: File): Database
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* 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
|
||||
|
||||
import java.sql.ResultSet
|
||||
import java.sql.SQLException
|
||||
|
||||
class JdbcCursor(private val resultSet: ResultSet) : Cursor {
|
||||
override fun close() {
|
||||
try {
|
||||
resultSet.close()
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun moveToNext(): Boolean {
|
||||
return try {
|
||||
resultSet.next()
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInt(index: Int): Int? {
|
||||
return try {
|
||||
val value = resultSet.getInt(index + 1)
|
||||
if (resultSet.wasNull()) null else value
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLong(index: Int): Long? {
|
||||
return try {
|
||||
val value = resultSet.getLong(index + 1)
|
||||
if (resultSet.wasNull()) null else value
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDouble(index: Int): Double? {
|
||||
return try {
|
||||
val value = resultSet.getDouble(index + 1)
|
||||
if (resultSet.wasNull()) null else value
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getString(index: Int): String? {
|
||||
return try {
|
||||
val value = resultSet.getString(index + 1)
|
||||
if (resultSet.wasNull()) null else value
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,165 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* 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
|
||||
|
||||
import java.io.File
|
||||
import java.sql.Connection
|
||||
import java.sql.PreparedStatement
|
||||
import java.sql.SQLException
|
||||
import java.sql.Types
|
||||
import java.util.ArrayList
|
||||
|
||||
class JdbcDatabase(private val connection: Connection) : Database {
|
||||
private var transactionSuccessful = false
|
||||
override fun query(q: String, vararg params: String): Cursor {
|
||||
return try {
|
||||
val st = buildStatement(q, params)
|
||||
JdbcCursor(st.executeQuery())
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(
|
||||
tableName: String,
|
||||
values: Map<String, Any?>,
|
||||
where: String,
|
||||
vararg params: String
|
||||
): Int {
|
||||
return try {
|
||||
val fields = ArrayList<String?>()
|
||||
val valuesStr = ArrayList<String>()
|
||||
for ((key, value) in values) {
|
||||
fields.add("$key=?")
|
||||
valuesStr.add(value.toString())
|
||||
}
|
||||
valuesStr.addAll(listOf(*params))
|
||||
val query = String.format(
|
||||
"update %s set %s where %s",
|
||||
tableName,
|
||||
fields.joinToString(", "),
|
||||
where
|
||||
)
|
||||
val st = buildStatement(query, valuesStr.toTypedArray())
|
||||
st.executeUpdate()
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun insert(tableName: String, values: Map<String, Any?>): Long? {
|
||||
return try {
|
||||
val fields = ArrayList<String?>()
|
||||
val params = ArrayList<Any?>()
|
||||
val questionMarks = ArrayList<String?>()
|
||||
for ((key, value) in values) {
|
||||
fields.add(key)
|
||||
params.add(value)
|
||||
questionMarks.add("?")
|
||||
}
|
||||
val query = String.format(
|
||||
"insert into %s(%s) values(%s)",
|
||||
tableName,
|
||||
fields.joinToString(", "),
|
||||
questionMarks.joinToString(", ")
|
||||
)
|
||||
val st = buildStatement(query, params.toTypedArray())
|
||||
st.execute()
|
||||
var id: Long? = null
|
||||
val keys = st.generatedKeys
|
||||
if (keys.next()) id = keys.getLong(1)
|
||||
id
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(tableName: String, where: String, vararg params: String) {
|
||||
val query = String.format("delete from %s where %s", tableName, where)
|
||||
execute(query, *params)
|
||||
}
|
||||
|
||||
override fun execute(query: String, vararg params: Any) {
|
||||
try {
|
||||
buildStatement(query, params).execute()
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildStatement(query: String, params: Array<out Any?>): PreparedStatement {
|
||||
val st = connection.prepareStatement(query)
|
||||
var index = 1
|
||||
for (param in params) {
|
||||
when (param) {
|
||||
null -> st.setNull(index++, Types.INTEGER)
|
||||
is Int -> st.setInt(index++, param)
|
||||
is Double -> st.setDouble(index++, param)
|
||||
is String -> st.setString(index++, param)
|
||||
is Long -> st.setLong(index++, param)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun beginTransaction() {
|
||||
try {
|
||||
connection.autoCommit = false
|
||||
transactionSuccessful = false
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun setTransactionSuccessful() {
|
||||
transactionSuccessful = true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun endTransaction() {
|
||||
try {
|
||||
if (transactionSuccessful) connection.commit() else connection.rollback()
|
||||
connection.autoCommit = true
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
try {
|
||||
connection.close()
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override val version: Int
|
||||
get() {
|
||||
query("PRAGMA user_version").use { c ->
|
||||
c.moveToNext()
|
||||
return c.getInt(0)!!
|
||||
}
|
||||
}
|
||||
|
||||
override val file: File?
|
||||
get() = null
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* 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
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.util.Locale
|
||||
|
||||
class MigrationHelper(
|
||||
private val db: Database
|
||||
) {
|
||||
fun migrateTo(newVersion: Int) {
|
||||
try {
|
||||
for (v in db.version + 1..newVersion) {
|
||||
val fname = String.format(Locale.US, "/migrations/%02d.sql", v)
|
||||
val sql = open(fname)
|
||||
for (command in SQLParser.parse(sql)) db.execute(command)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun open(fname: String): String {
|
||||
val resource = javaClass.getResourceAsStream(fname)
|
||||
if (resource != null) return resource.bufferedReader().readText()
|
||||
|
||||
// Workaround for bug in Android Studio / IntelliJ. Removing this
|
||||
// causes unit tests to fail when run from within the IDE, although
|
||||
// everything works fine from the command line.
|
||||
val file = File("uhabits-core/src/main/resources/$fname")
|
||||
if (file.exists()) return FileInputStream(file).bufferedReader().readText()
|
||||
throw RuntimeException("resource not found: $fname")
|
||||
}
|
||||
}
|
||||
@ -19,17 +19,19 @@
|
||||
package org.isoron.uhabits.core.io
|
||||
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.isoron.platform.io.Database
|
||||
import org.isoron.platform.io.DatabaseOpener
|
||||
import org.isoron.platform.io.getVersion
|
||||
import org.isoron.platform.io.migrateTo
|
||||
import org.isoron.platform.io.query
|
||||
import org.isoron.platform.io.querySingle
|
||||
import org.isoron.platform.time.LocalDate
|
||||
import org.isoron.uhabits.core.AppScope
|
||||
import org.isoron.uhabits.core.DATABASE_VERSION
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.commands.CreateHabitCommand
|
||||
import org.isoron.uhabits.core.commands.EditHabitCommand
|
||||
import org.isoron.uhabits.core.database.Cursor
|
||||
import org.isoron.uhabits.core.database.Database
|
||||
import org.isoron.uhabits.core.database.DatabaseOpener
|
||||
import org.isoron.uhabits.core.database.HabitData
|
||||
import org.isoron.uhabits.core.database.MigrationHelper
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.HabitList
|
||||
import org.isoron.uhabits.core.models.ModelFactory
|
||||
@ -53,26 +55,30 @@ class LoopDBImporter(
|
||||
|
||||
override fun canHandle(file: File): Boolean {
|
||||
if (!file.isSQLite3File()) return false
|
||||
val db = opener.open(file)
|
||||
val db = opener.open(file.absolutePath)
|
||||
var canHandle = true
|
||||
val c = db.query("select count(*) from SQLITE_MASTER where name='Habits' or name='Repetitions'")
|
||||
if (!c.moveToNext() || c.getInt(0) != 2) {
|
||||
val count = db.querySingle(
|
||||
"select count(*) from SQLITE_MASTER where name='Habits' or name='Repetitions'"
|
||||
) { it.getInt(0) }
|
||||
if (count == null || count != 2) {
|
||||
logger.error("Cannot handle file: tables not found")
|
||||
canHandle = false
|
||||
}
|
||||
if (db.version > DATABASE_VERSION) {
|
||||
logger.error("Cannot handle file: incompatible version: ${db.version} > $DATABASE_VERSION")
|
||||
if (db.getVersion() > DATABASE_VERSION) {
|
||||
logger.error("Cannot handle file: incompatible version: ${db.getVersion()} > $DATABASE_VERSION")
|
||||
canHandle = false
|
||||
}
|
||||
c.close()
|
||||
db.close()
|
||||
return canHandle
|
||||
}
|
||||
|
||||
override fun importHabitsFromFile(file: File) {
|
||||
val db = opener.open(file)
|
||||
val helper = MigrationHelper(db)
|
||||
helper.migrateTo(DATABASE_VERSION)
|
||||
val db = opener.open(file.absolutePath)
|
||||
db.migrateTo(DATABASE_VERSION) { version ->
|
||||
val filename = "%02d.sql".format(version)
|
||||
javaClass.getResourceAsStream("/migrations/$filename")!!
|
||||
.bufferedReader().readText()
|
||||
}
|
||||
|
||||
val habitDataList = loadHabits(db)
|
||||
for (habitData in habitDataList) {
|
||||
@ -89,21 +95,20 @@ class LoopDBImporter(
|
||||
EditHabitCommand(habitList, habit.id!!, modified).run()
|
||||
}
|
||||
|
||||
// Reload saved version of the habit
|
||||
habit = habitList.getByUUID(habitData.uuid)!!
|
||||
val entries = habit.originalEntries
|
||||
|
||||
// Import entries
|
||||
loadEntries(db, habitData.id!!).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val timestamp = c.getLong(0) ?: continue
|
||||
val value = c.getInt(1) ?: continue
|
||||
val notes = c.getString(2) ?: ""
|
||||
val date = LocalDate.fromUnixTime(timestamp)
|
||||
val (_, existingValue, existingNotes) = entries.get(date)
|
||||
if (existingValue != value || existingNotes != notes) {
|
||||
entries.add(Entry(date, value, notes))
|
||||
}
|
||||
db.query(
|
||||
"SELECT timestamp, value, notes FROM Repetitions WHERE habit = ? ORDER BY timestamp DESC",
|
||||
habitData.id.toString()
|
||||
) { stmt ->
|
||||
val timestamp = stmt.getLongOrNull(0) ?: return@query
|
||||
val value = stmt.getIntOrNull(1) ?: return@query
|
||||
val notes = stmt.getTextOrNull(2) ?: ""
|
||||
val date = LocalDate.fromUnixTime(timestamp)
|
||||
val (_, existingValue, existingNotes) = entries.get(date)
|
||||
if (existingValue != value || existingNotes != notes) {
|
||||
entries.add(Entry(date, value, notes))
|
||||
}
|
||||
}
|
||||
habit.recompute()
|
||||
@ -119,41 +124,30 @@ class LoopDBImporter(
|
||||
"position, reminder_hour, reminder_min, reminder_days, highlight, " +
|
||||
"archived, type, target_value, target_type, unit, uuid " +
|
||||
"FROM Habits ORDER BY position"
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(cursorToHabitData(c))
|
||||
}
|
||||
) { stmt ->
|
||||
result.add(
|
||||
HabitData(
|
||||
id = stmt.getLongOrNull(0),
|
||||
name = stmt.getTextOrNull(1) ?: "",
|
||||
description = stmt.getTextOrNull(2) ?: "",
|
||||
question = stmt.getTextOrNull(3) ?: "",
|
||||
freqNum = stmt.getIntOrNull(4) ?: 1,
|
||||
freqDen = stmt.getIntOrNull(5) ?: 1,
|
||||
color = stmt.getIntOrNull(6) ?: 0,
|
||||
position = stmt.getIntOrNull(7) ?: 0,
|
||||
reminderHour = stmt.getIntOrNull(8),
|
||||
reminderMin = stmt.getIntOrNull(9),
|
||||
reminderDays = stmt.getIntOrNull(10) ?: 0,
|
||||
highlight = stmt.getIntOrNull(11) ?: 0,
|
||||
archived = stmt.getIntOrNull(12) ?: 0,
|
||||
type = stmt.getIntOrNull(13) ?: 0,
|
||||
targetValue = stmt.getRealOrNull(14) ?: 0.0,
|
||||
targetType = stmt.getIntOrNull(15) ?: 0,
|
||||
unit = stmt.getTextOrNull(16) ?: "",
|
||||
uuid = stmt.getTextOrNull(17)
|
||||
)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun loadEntries(db: Database, habitId: Long): Cursor {
|
||||
return db.query(
|
||||
"SELECT timestamp, value, notes FROM Repetitions WHERE habit = ? ORDER BY timestamp DESC",
|
||||
habitId.toString()
|
||||
)
|
||||
}
|
||||
|
||||
private fun cursorToHabitData(c: Cursor): HabitData {
|
||||
return HabitData(
|
||||
id = c.getLong(0),
|
||||
name = c.getString(1) ?: "",
|
||||
description = c.getString(2) ?: "",
|
||||
question = c.getString(3) ?: "",
|
||||
freqNum = c.getInt(4) ?: 1,
|
||||
freqDen = c.getInt(5) ?: 1,
|
||||
color = c.getInt(6) ?: 0,
|
||||
position = c.getInt(7) ?: 0,
|
||||
reminderHour = c.getInt(8),
|
||||
reminderMin = c.getInt(9),
|
||||
reminderDays = c.getInt(10) ?: 0,
|
||||
highlight = c.getInt(11) ?: 0,
|
||||
archived = c.getInt(12) ?: 0,
|
||||
type = c.getInt(13) ?: 0,
|
||||
targetValue = c.getDouble(14) ?: 0.0,
|
||||
targetType = c.getInt(15) ?: 0,
|
||||
unit = c.getString(16) ?: "",
|
||||
uuid = c.getString(17)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,10 +19,13 @@
|
||||
package org.isoron.uhabits.core.io
|
||||
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.isoron.platform.io.Database
|
||||
import org.isoron.platform.io.DatabaseOpener
|
||||
import org.isoron.platform.io.begin
|
||||
import org.isoron.platform.io.commit
|
||||
import org.isoron.platform.io.query
|
||||
import org.isoron.platform.io.querySingle
|
||||
import org.isoron.platform.time.LocalDate
|
||||
import org.isoron.uhabits.core.database.Cursor
|
||||
import org.isoron.uhabits.core.database.Database
|
||||
import org.isoron.uhabits.core.database.DatabaseOpener
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
@ -45,124 +48,98 @@ class RewireDBImporter(
|
||||
|
||||
override fun canHandle(file: File): Boolean {
|
||||
if (!file.isSQLite3File()) return false
|
||||
val db = opener.open(file)
|
||||
val c = db.query(
|
||||
"select count(*) from SQLITE_MASTER " +
|
||||
"where name='CHECKINS' or name='UNIT'"
|
||||
)
|
||||
val result = c.moveToNext() && c.getInt(0) == 2
|
||||
c.close()
|
||||
val db = opener.open(file.absolutePath)
|
||||
val count = db.querySingle(
|
||||
"select count(*) from SQLITE_MASTER where name='CHECKINS' or name='UNIT'"
|
||||
) { it.getInt(0) }
|
||||
db.close()
|
||||
return result
|
||||
return count == 2
|
||||
}
|
||||
|
||||
override fun importHabitsFromFile(file: File) {
|
||||
val db = opener.open(file)
|
||||
db.beginTransaction()
|
||||
val db = opener.open(file.absolutePath)
|
||||
db.begin()
|
||||
createHabits(db)
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
db.commit()
|
||||
db.close()
|
||||
}
|
||||
|
||||
private fun createHabits(db: Database) {
|
||||
var c: Cursor? = null
|
||||
try {
|
||||
c = db.query(
|
||||
"select _id, name, description, schedule, " +
|
||||
"active_days, repeating_count, days, period " +
|
||||
"from habits"
|
||||
)
|
||||
if (!c.moveToNext()) return
|
||||
do {
|
||||
val id = c.getInt(0)!!
|
||||
val name = c.getString(1)
|
||||
val description = c.getString(2)
|
||||
val schedule = c.getInt(3)!!
|
||||
val activeDays = c.getString(4)
|
||||
val repeatingCount = c.getInt(5)!!
|
||||
val days = c.getInt(6)!!
|
||||
val periodIndex = c.getInt(7)!!
|
||||
db.query(
|
||||
"select _id, name, description, schedule, " +
|
||||
"active_days, repeating_count, days, period " +
|
||||
"from habits"
|
||||
) { stmt ->
|
||||
val id = stmt.getInt(0)
|
||||
val name = stmt.getText(1)
|
||||
val description = stmt.getTextOrNull(2) ?: ""
|
||||
val schedule = stmt.getInt(3)
|
||||
val activeDays = stmt.getTextOrNull(4)
|
||||
val repeatingCount = stmt.getInt(5)
|
||||
val days = stmt.getInt(6)
|
||||
val periodIndex = stmt.getInt(7)
|
||||
|
||||
val habit = modelFactory.buildHabit()
|
||||
habit.name = name!!
|
||||
habit.description = description ?: ""
|
||||
val periods = intArrayOf(7, 31, 365)
|
||||
var numerator: Int
|
||||
var denominator: Int
|
||||
when (schedule) {
|
||||
0 -> {
|
||||
numerator = activeDays!!.split(",").toTypedArray().size
|
||||
denominator = 7
|
||||
}
|
||||
1 -> {
|
||||
numerator = days
|
||||
denominator = periods[periodIndex]
|
||||
}
|
||||
2 -> {
|
||||
numerator = 1
|
||||
denominator = repeatingCount
|
||||
}
|
||||
else -> throw IllegalStateException()
|
||||
val habit = modelFactory.buildHabit()
|
||||
habit.name = name
|
||||
habit.description = description
|
||||
val periods = intArrayOf(7, 31, 365)
|
||||
var numerator: Int
|
||||
var denominator: Int
|
||||
when (schedule) {
|
||||
0 -> {
|
||||
numerator = activeDays!!.split(",").toTypedArray().size
|
||||
denominator = 7
|
||||
}
|
||||
habit.frequency = Frequency(numerator, denominator)
|
||||
habitList.add(habit)
|
||||
createReminder(db, habit, id)
|
||||
createCheckmarks(db, habit, id)
|
||||
} while (c.moveToNext())
|
||||
} finally {
|
||||
c?.close()
|
||||
1 -> {
|
||||
numerator = days
|
||||
denominator = periods[periodIndex]
|
||||
}
|
||||
2 -> {
|
||||
numerator = 1
|
||||
denominator = repeatingCount
|
||||
}
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
habit.frequency = Frequency(numerator, denominator)
|
||||
habitList.add(habit)
|
||||
createReminder(db, habit, id)
|
||||
createCheckmarks(db, habit, id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCheckmarks(
|
||||
db: Database,
|
||||
habit: Habit,
|
||||
rewireHabitId: Int
|
||||
) {
|
||||
var c: Cursor? = null
|
||||
try {
|
||||
c = db.query(
|
||||
"select distinct date from checkins where habit_id=? and type=2",
|
||||
rewireHabitId.toString()
|
||||
)
|
||||
if (!c.moveToNext()) return
|
||||
do {
|
||||
val dateStr = c.getString(0)
|
||||
val year = dateStr!!.substring(0, 4).toInt()
|
||||
val month = dateStr.substring(4, 6).toInt()
|
||||
val day = dateStr.substring(6, 8).toInt()
|
||||
habit.originalEntries.add(Entry(LocalDate(year, month, day), Entry.YES_MANUAL))
|
||||
} while (c.moveToNext())
|
||||
} finally {
|
||||
c?.close()
|
||||
private fun createCheckmarks(db: Database, habit: Habit, rewireHabitId: Int) {
|
||||
db.query(
|
||||
"select distinct date from checkins where habit_id=? and type=2",
|
||||
rewireHabitId.toString()
|
||||
) { stmt ->
|
||||
val dateStr = stmt.getText(0)
|
||||
val year = dateStr.substring(0, 4).toInt()
|
||||
val month = dateStr.substring(4, 6).toInt()
|
||||
val day = dateStr.substring(6, 8).toInt()
|
||||
habit.originalEntries.add(Entry(LocalDate(year, month, day), Entry.YES_MANUAL))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createReminder(db: Database, habit: Habit, rewireHabitId: Int) {
|
||||
var c: Cursor? = null
|
||||
try {
|
||||
c = db.query(
|
||||
"select time, active_days from reminders where habit_id=? limit 1",
|
||||
rewireHabitId.toString()
|
||||
)
|
||||
if (!c.moveToNext()) return
|
||||
val rewireReminder = c.getInt(0)!!
|
||||
if (rewireReminder <= 0 || rewireReminder >= 1440) return
|
||||
val reminder = db.querySingle(
|
||||
"select time, active_days from reminders where habit_id=? limit 1",
|
||||
rewireHabitId.toString()
|
||||
) { stmt ->
|
||||
val rewireReminder = stmt.getInt(0)
|
||||
if (rewireReminder <= 0 || rewireReminder >= 1440) return@querySingle null
|
||||
val reminderDays = BooleanArray(7)
|
||||
val activeDays = c.getString(1)!!.split(",").toTypedArray()
|
||||
for (d in activeDays) {
|
||||
val activeDaysStr = stmt.getText(1).split(",").toTypedArray()
|
||||
for (d in activeDaysStr) {
|
||||
val idx = (d.toInt() + 1) % 7
|
||||
reminderDays[idx] = true
|
||||
}
|
||||
val hour = rewireReminder / 60
|
||||
val minute = rewireReminder % 60
|
||||
val days = WeekdayList(reminderDays)
|
||||
val reminder = Reminder(hour, minute, days)
|
||||
Reminder(hour, minute, WeekdayList(reminderDays))
|
||||
}
|
||||
if (reminder != null) {
|
||||
habit.reminder = reminder
|
||||
habitList.update(habit)
|
||||
} finally {
|
||||
c?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,10 +19,13 @@
|
||||
package org.isoron.uhabits.core.io
|
||||
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.isoron.platform.io.Database
|
||||
import org.isoron.platform.io.DatabaseOpener
|
||||
import org.isoron.platform.io.begin
|
||||
import org.isoron.platform.io.commit
|
||||
import org.isoron.platform.io.query
|
||||
import org.isoron.platform.io.querySingle
|
||||
import org.isoron.platform.time.LocalDate
|
||||
import org.isoron.uhabits.core.database.Cursor
|
||||
import org.isoron.uhabits.core.database.Database
|
||||
import org.isoron.uhabits.core.database.DatabaseOpener
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
@ -43,67 +46,45 @@ class TickmateDBImporter(
|
||||
|
||||
override fun canHandle(file: File): Boolean {
|
||||
if (!file.isSQLite3File()) return false
|
||||
val db = opener.open(file)
|
||||
val c = db.query(
|
||||
"select count(*) from SQLITE_MASTER " +
|
||||
"where name='tracks' or name='track2groups'"
|
||||
)
|
||||
val result = c.moveToNext() && c.getInt(0) == 2
|
||||
c.close()
|
||||
val db = opener.open(file.absolutePath)
|
||||
val count = db.querySingle(
|
||||
"select count(*) from SQLITE_MASTER where name='tracks' or name='track2groups'"
|
||||
) { it.getInt(0) }
|
||||
db.close()
|
||||
return result
|
||||
return count == 2
|
||||
}
|
||||
|
||||
override fun importHabitsFromFile(file: File) {
|
||||
val db = opener.open(file)
|
||||
db.beginTransaction()
|
||||
val db = opener.open(file.absolutePath)
|
||||
db.begin()
|
||||
createHabits(db)
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
db.commit()
|
||||
db.close()
|
||||
}
|
||||
|
||||
private fun createCheckmarks(
|
||||
db: Database,
|
||||
habit: Habit,
|
||||
tickmateTrackId: Int
|
||||
) {
|
||||
var c: Cursor? = null
|
||||
try {
|
||||
c = db.query(
|
||||
"select distinct year, month, day from ticks where _track_id=?",
|
||||
tickmateTrackId.toString()
|
||||
)
|
||||
if (!c.moveToNext()) return
|
||||
do {
|
||||
val year = c.getInt(0)!!
|
||||
val month = c.getInt(1)!!
|
||||
val day = c.getInt(2)!!
|
||||
habit.originalEntries.add(Entry(LocalDate(year, month + 1, day), Entry.YES_MANUAL))
|
||||
} while (c.moveToNext())
|
||||
} finally {
|
||||
c?.close()
|
||||
private fun createCheckmarks(db: Database, habit: Habit, tickmateTrackId: Int) {
|
||||
db.query(
|
||||
"select distinct year, month, day from ticks where _track_id=?",
|
||||
tickmateTrackId.toString()
|
||||
) { stmt ->
|
||||
val year = stmt.getInt(0)
|
||||
val month = stmt.getInt(1)
|
||||
val day = stmt.getInt(2)
|
||||
habit.originalEntries.add(Entry(LocalDate(year, month + 1, day), Entry.YES_MANUAL))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHabits(db: Database) {
|
||||
var c: Cursor? = null
|
||||
try {
|
||||
c = db.query("select _id, name, description from tracks")
|
||||
if (!c.moveToNext()) return
|
||||
do {
|
||||
val id = c.getInt(0)!!
|
||||
val name = c.getString(1)
|
||||
val description = c.getString(2)
|
||||
val habit = modelFactory.buildHabit()
|
||||
habit.name = name!!
|
||||
habit.description = description ?: ""
|
||||
habit.frequency = Frequency.DAILY
|
||||
habitList.add(habit)
|
||||
createCheckmarks(db, habit, id)
|
||||
} while (c.moveToNext())
|
||||
} finally {
|
||||
c?.close()
|
||||
db.query("select _id, name, description from tracks") { stmt ->
|
||||
val id = stmt.getInt(0)
|
||||
val name = stmt.getText(1)
|
||||
val description = stmt.getTextOrNull(2) ?: ""
|
||||
val habit = modelFactory.buildHabit()
|
||||
habit.name = name
|
||||
habit.description = description
|
||||
habit.frequency = Frequency.DAILY
|
||||
habitList.add(habit)
|
||||
createCheckmarks(db, habit, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,14 +19,11 @@
|
||||
package org.isoron.uhabits.core
|
||||
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.isoron.platform.io.JavaDatabaseOpener
|
||||
import org.isoron.platform.io.TestDatabaseHelper
|
||||
import org.isoron.platform.time.LocalDate
|
||||
import org.isoron.platform.time.setToday
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.database.Database
|
||||
import org.isoron.uhabits.core.database.DatabaseOpener
|
||||
import org.isoron.uhabits.core.database.JdbcDatabase
|
||||
import org.isoron.uhabits.core.database.MigrationHelper
|
||||
import org.isoron.uhabits.core.models.HabitList
|
||||
import org.isoron.uhabits.core.models.ModelFactory
|
||||
import org.isoron.uhabits.core.models.memory.MemoryModelFactory
|
||||
@ -45,8 +42,6 @@ import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Paths
|
||||
import java.sql.DriverManager
|
||||
import java.sql.SQLException
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.TimeZone
|
||||
|
||||
@ -57,22 +52,7 @@ open class BaseUnitTest {
|
||||
protected lateinit var modelFactory: ModelFactory
|
||||
protected lateinit var taskRunner: SingleThreadTaskRunner
|
||||
protected open lateinit var commandRunner: CommandRunner
|
||||
protected var databaseOpener: DatabaseOpener = object : DatabaseOpener {
|
||||
override fun open(file: File): Database {
|
||||
return try {
|
||||
JdbcDatabase(
|
||||
DriverManager.getConnection(
|
||||
String.format(
|
||||
"jdbc:sqlite:%s",
|
||||
file.absolutePath
|
||||
)
|
||||
)
|
||||
)
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
protected var databaseOpener: org.isoron.platform.io.DatabaseOpener = JavaDatabaseOpener()
|
||||
|
||||
@Before
|
||||
@Throws(Exception::class)
|
||||
@ -125,31 +105,17 @@ open class BaseUnitTest {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected fun openDatabaseResource(path: String): Database {
|
||||
protected fun openDatabaseResource(path: String): org.isoron.platform.io.Database {
|
||||
val original = openAsset(path)
|
||||
val tmpDbFile = File.createTempFile("database", ".db")
|
||||
tmpDbFile.deleteOnExit()
|
||||
IOUtils.copy(original, FileOutputStream(tmpDbFile))
|
||||
return databaseOpener.open(tmpDbFile)
|
||||
return databaseOpener.open(tmpDbFile.absolutePath)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun buildMemoryDatabase(): Database {
|
||||
return try {
|
||||
val db: Database = JdbcDatabase(
|
||||
DriverManager.getConnection("jdbc:sqlite::memory:")
|
||||
)
|
||||
db.execute("pragma user_version=8;")
|
||||
val helper = MigrationHelper(db)
|
||||
helper.migrateTo(DATABASE_VERSION)
|
||||
db
|
||||
} catch (e: SQLException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun buildNewMemoryDatabase(): org.isoron.platform.io.Database {
|
||||
return org.isoron.platform.io.TestDatabaseHelper.createEmptyDatabase()
|
||||
fun buildMemoryDatabase(): org.isoron.platform.io.Database {
|
||||
return TestDatabaseHelper.createEmptyDatabase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,132 +21,132 @@ package org.isoron.uhabits.core.database.migrations
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.isoron.platform.io.Database
|
||||
import org.isoron.platform.io.migrateTo
|
||||
import org.isoron.platform.io.querySingle
|
||||
import org.isoron.platform.io.run
|
||||
import org.isoron.uhabits.core.BaseUnitTest
|
||||
import org.isoron.uhabits.core.database.Cursor
|
||||
import org.isoron.uhabits.core.database.Database
|
||||
import org.isoron.uhabits.core.database.MigrationHelper
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
|
||||
class Version22Test : BaseUnitTest() {
|
||||
private lateinit var db: Database
|
||||
private lateinit var helper: MigrationHelper
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
db = openDatabaseResource("/databases/021.db")
|
||||
helper = MigrationHelper(db)
|
||||
}
|
||||
|
||||
private fun migrateTo(version: Int) {
|
||||
db.migrateTo(version) { v ->
|
||||
val filename = "%02d.sql".format(v)
|
||||
javaClass.getResourceAsStream("/migrations/$filename")!!
|
||||
.bufferedReader().readText()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testKeepValidReps() {
|
||||
db.query("select count(*) from repetitions") { c: Cursor ->
|
||||
assertThat(c.getInt(0), equalTo(3))
|
||||
}
|
||||
helper.migrateTo(22)
|
||||
db.query("select count(*) from repetitions") { c: Cursor ->
|
||||
assertThat(c.getInt(0), equalTo(3))
|
||||
}
|
||||
val before = db.querySingle("select count(*) from repetitions") { it.getInt(0) }
|
||||
assertThat(before, equalTo(3))
|
||||
migrateTo(22)
|
||||
val after = db.querySingle("select count(*) from repetitions") { it.getInt(0) }
|
||||
assertThat(after, equalTo(3))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testRemoveRepsWithInvalidId() {
|
||||
db.execute("insert into Repetitions(habit, timestamp, value) values (99999, 100, 2)")
|
||||
db.query("select count(*) from repetitions where habit = 99999") { c: Cursor ->
|
||||
assertThat(c.getInt(0), equalTo(1))
|
||||
}
|
||||
helper.migrateTo(22)
|
||||
db.query("select count(*) from repetitions where habit = 99999") { c: Cursor ->
|
||||
assertThat(c.getInt(0), equalTo(0))
|
||||
}
|
||||
db.run("insert into Repetitions(habit, timestamp, value) values (99999, 100, 2)")
|
||||
val before = db.querySingle(
|
||||
"select count(*) from repetitions where habit = 99999"
|
||||
) { it.getInt(0) }
|
||||
assertThat(before, equalTo(1))
|
||||
migrateTo(22)
|
||||
val after = db.querySingle(
|
||||
"select count(*) from repetitions where habit = 99999"
|
||||
) { it.getInt(0) }
|
||||
assertThat(after, equalTo(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testDisallowNewRepsWithInvalidRef() {
|
||||
helper.migrateTo(22)
|
||||
val exception = assertThrows(java.lang.RuntimeException::class.java) { db.execute("insert into Repetitions(habit, timestamp, value) values (99999, 100, 2)") }
|
||||
assertThat(exception.message, Matchers.containsString("SQLITE_CONSTRAINT"))
|
||||
migrateTo(22)
|
||||
val exception = assertThrows(Exception::class.java) {
|
||||
db.run("insert into Repetitions(habit, timestamp, value) values (99999, 100, 2)")
|
||||
}
|
||||
assertThat(exception.message, Matchers.containsString("constraint"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testRemoveRepetitionsWithNullTimestamp() {
|
||||
db.execute("insert into repetitions(habit, value) values (0, 2)")
|
||||
db.query("select count(*) from repetitions where timestamp is null") { c: Cursor ->
|
||||
assertThat(c.getInt(0), equalTo(1))
|
||||
}
|
||||
helper.migrateTo(22)
|
||||
db.query("select count(*) from repetitions where timestamp is null") { c: Cursor ->
|
||||
assertThat(c.getInt(0), equalTo(0))
|
||||
}
|
||||
db.run("insert into repetitions(habit, value) values (0, 2)")
|
||||
val before = db.querySingle(
|
||||
"select count(*) from repetitions where timestamp is null"
|
||||
) { it.getInt(0) }
|
||||
assertThat(before, equalTo(1))
|
||||
migrateTo(22)
|
||||
val after = db.querySingle(
|
||||
"select count(*) from repetitions where timestamp is null"
|
||||
) { it.getInt(0) }
|
||||
assertThat(after, equalTo(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testDisallowNullTimestamp() {
|
||||
helper.migrateTo(22)
|
||||
|
||||
val exception = assertThrows(java.lang.RuntimeException::class.java) {
|
||||
db.execute("insert into Repetitions(habit, value) " + "values (0, 2)")
|
||||
migrateTo(22)
|
||||
val exception = assertThrows(Exception::class.java) {
|
||||
db.run("insert into Repetitions(habit, value) values (0, 2)")
|
||||
}
|
||||
|
||||
assertThat(exception.message, Matchers.containsString("SQLITE_CONSTRAINT"))
|
||||
assertThat(exception.message, Matchers.containsString("constraint"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testRemoveRepetitionsWithNullHabit() {
|
||||
db.execute("insert into repetitions(timestamp, value) values (0, 2)")
|
||||
db.query("select count(*) from repetitions where habit is null") { c: Cursor ->
|
||||
assertThat(c.getInt(0), equalTo(1))
|
||||
}
|
||||
helper.migrateTo(22)
|
||||
db.query("select count(*) from repetitions where habit is null") { c: Cursor ->
|
||||
assertThat(c.getInt(0), equalTo(0))
|
||||
}
|
||||
db.run("insert into repetitions(timestamp, value) values (0, 2)")
|
||||
val before = db.querySingle(
|
||||
"select count(*) from repetitions where habit is null"
|
||||
) { it.getInt(0) }
|
||||
assertThat(before, equalTo(1))
|
||||
migrateTo(22)
|
||||
val after = db.querySingle(
|
||||
"select count(*) from repetitions where habit is null"
|
||||
) { it.getInt(0) }
|
||||
assertThat(after, equalTo(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testDisallowNullHabit() {
|
||||
helper.migrateTo(22)
|
||||
|
||||
val exception = assertThrows(java.lang.RuntimeException::class.java) {
|
||||
db.execute("insert into Repetitions(timestamp, value) " + "values (5, 2)")
|
||||
migrateTo(22)
|
||||
val exception = assertThrows(Exception::class.java) {
|
||||
db.run("insert into Repetitions(timestamp, value) values (5, 2)")
|
||||
}
|
||||
|
||||
assertThat(exception.message, Matchers.containsString("SQLITE_CONSTRAINT"))
|
||||
assertThat(exception.message, Matchers.containsString("constraint"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testRemoveDuplicateRepetitions() {
|
||||
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: Cursor ->
|
||||
assertThat(c.getInt(0), equalTo(3))
|
||||
}
|
||||
helper.migrateTo(22)
|
||||
db.query("select count(*) from repetitions where timestamp=100 and habit=0") { c: Cursor ->
|
||||
assertThat(c.getInt(0), equalTo(1))
|
||||
}
|
||||
db.run("insert into repetitions(habit, timestamp, value)values (0, 100, 2)")
|
||||
db.run("insert into repetitions(habit, timestamp, value)values (0, 100, 5)")
|
||||
db.run("insert into repetitions(habit, timestamp, value)values (0, 100, 10)")
|
||||
val before = db.querySingle(
|
||||
"select count(*) from repetitions where timestamp=100 and habit=0"
|
||||
) { it.getInt(0) }
|
||||
assertThat(before, equalTo(3))
|
||||
migrateTo(22)
|
||||
val after = db.querySingle(
|
||||
"select count(*) from repetitions where timestamp=100 and habit=0"
|
||||
) { it.getInt(0) }
|
||||
assertThat(after, equalTo(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testDisallowNewDuplicateTimestamps() {
|
||||
helper.migrateTo(22)
|
||||
db.execute("insert into repetitions(habit, timestamp, value)values (0, 100, 2)")
|
||||
|
||||
val exception = assertThrows(java.lang.RuntimeException::class.java) {
|
||||
db.execute("insert into repetitions(habit, timestamp, value)values (0, 100, 5)")
|
||||
migrateTo(22)
|
||||
db.run("insert into repetitions(habit, timestamp, value)values (0, 100, 2)")
|
||||
val exception = assertThrows(Exception::class.java) {
|
||||
db.run("insert into repetitions(habit, timestamp, value)values (0, 100, 5)")
|
||||
}
|
||||
|
||||
assertThat(exception.message, Matchers.containsString("SQLITE_CONSTRAINT"))
|
||||
assertThat(exception.message, Matchers.containsString("constraint"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,57 +21,59 @@ package org.isoron.uhabits.core.database.migrations
|
||||
|
||||
import org.hamcrest.CoreMatchers.equalTo
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.isoron.platform.io.Database
|
||||
import org.isoron.platform.io.migrateTo
|
||||
import org.isoron.platform.io.query
|
||||
import org.isoron.uhabits.core.BaseUnitTest
|
||||
import org.isoron.uhabits.core.database.Database
|
||||
import org.isoron.uhabits.core.database.MigrationHelper
|
||||
import org.junit.Test
|
||||
|
||||
class Version23Test : BaseUnitTest() {
|
||||
|
||||
private lateinit var db: Database
|
||||
|
||||
private lateinit var helper: MigrationHelper
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
db = openDatabaseResource("/databases/022.db")
|
||||
helper = MigrationHelper(db)
|
||||
}
|
||||
|
||||
private fun migrateTo23() = helper.migrateTo(23)
|
||||
private fun migrateTo(version: Int) {
|
||||
db.migrateTo(version) { v ->
|
||||
val filename = "%02d.sql".format(v)
|
||||
javaClass.getResourceAsStream("/migrations/$filename")!!
|
||||
.bufferedReader().readText()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test migrate to 23 creates question column`() {
|
||||
migrateTo23()
|
||||
val cursor = db.query("select question from Habits")
|
||||
cursor.moveToNext()
|
||||
migrateTo(23)
|
||||
db.query("select question from Habits") {}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test migrate to 23 moves description to question column`() {
|
||||
var cursor = db.query("select description from Habits")
|
||||
|
||||
val descriptions = mutableListOf<String?>()
|
||||
while (cursor.moveToNext()) {
|
||||
descriptions.add(cursor.getString(0))
|
||||
db.query("select description from Habits") { stmt ->
|
||||
descriptions.add(stmt.getTextOrNull(0))
|
||||
}
|
||||
|
||||
migrateTo23()
|
||||
cursor = db.query("select question from Habits")
|
||||
migrateTo(23)
|
||||
|
||||
for (i in 0 until descriptions.size) {
|
||||
cursor.moveToNext()
|
||||
assertThat(cursor.getString(0), equalTo(descriptions[i]))
|
||||
val questions = mutableListOf<String?>()
|
||||
db.query("select question from Habits") { stmt ->
|
||||
questions.add(stmt.getTextOrNull(0))
|
||||
}
|
||||
|
||||
for (i in descriptions.indices) {
|
||||
assertThat(questions[i], equalTo(descriptions[i]))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test migrate to 23 sets description to null`() {
|
||||
migrateTo23()
|
||||
val cursor = db.query("select description from Habits")
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
assertThat(cursor.getString(0), equalTo(""))
|
||||
migrateTo(23)
|
||||
db.query("select description from Habits") { stmt ->
|
||||
assertThat(stmt.getTextOrNull(0), equalTo(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
package org.isoron.uhabits.core.models.sqlite
|
||||
|
||||
import org.isoron.platform.time.LocalDate
|
||||
import org.isoron.uhabits.core.BaseUnitTest.Companion.buildNewMemoryDatabase
|
||||
import org.isoron.uhabits.core.BaseUnitTest.Companion.buildMemoryDatabase
|
||||
import org.isoron.uhabits.core.database.EntryData
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
||||
@ -30,7 +30,7 @@ import kotlin.test.assertEquals
|
||||
|
||||
class SQLiteEntryListTest {
|
||||
|
||||
private val database = buildNewMemoryDatabase()
|
||||
private val database = buildMemoryDatabase()
|
||||
private val factory = SQLModelFactory(database)
|
||||
private val entryRepository = factory.entryRepository
|
||||
private lateinit var entries: SQLiteEntryList
|
||||
|
||||
@ -46,7 +46,7 @@ class SQLiteHabitListTest : BaseUnitTest() {
|
||||
@Throws(Exception::class)
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
val db = buildNewMemoryDatabase()
|
||||
val db = buildMemoryDatabase()
|
||||
modelFactory = SQLModelFactory(db)
|
||||
habitList = SQLiteHabitList(modelFactory)
|
||||
fixtures = HabitFixtures(modelFactory, habitList)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user