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
|
package org.isoron.uhabits.database
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.isoron.platform.io.StepResult
|
||||||
import org.hamcrest.core.IsEqual.equalTo
|
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.BaseAndroidTest
|
||||||
import org.isoron.uhabits.core.database.Cursor
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class AndroidDatabaseTest : BaseAndroidTest() {
|
class AndroidDatabaseTest : BaseAndroidTest() {
|
||||||
private lateinit var db: AndroidDatabase
|
private lateinit var db: AndroidDatabase
|
||||||
|
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
db = AndroidDatabase(SQLiteDatabase.create(null), null)
|
db = AndroidDatabase(SQLiteDatabase.create(null))
|
||||||
db.execute("create table test(color int, name string)")
|
db.run("create table test(color int, name string)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
fun testInsertAndQuery() {
|
||||||
fun testInsert() {
|
db.run("insert into test(color, name) values (?, ?)") {
|
||||||
val map = mapOf(Pair("name", "asd"), Pair("color", null))
|
bindNull(1)
|
||||||
db.insert("test", map)
|
bindText(2, "asd")
|
||||||
val c: Cursor = db.query("select * from test")
|
}
|
||||||
c.moveToNext()
|
val stmt = db.prepareStatement("select color, name from test")
|
||||||
c.getInt(0)!!
|
assertEquals(StepResult.ROW, stmt.step())
|
||||||
assertThat(c.getString(1), equalTo("asd"))
|
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.platform.io.setVersion
|
||||||
import org.isoron.uhabits.core.database.UnsupportedDatabaseVersionException
|
import org.isoron.uhabits.core.database.UnsupportedDatabaseVersionException
|
||||||
import org.isoron.uhabits.database.AndroidDatabase
|
import org.isoron.uhabits.database.AndroidDatabase
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class HabitsDatabaseOpener(
|
class HabitsDatabaseOpener(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -52,7 +51,7 @@ class HabitsDatabaseOpener(
|
|||||||
) {
|
) {
|
||||||
db.disableWriteAheadLogging()
|
db.disableWriteAheadLogging()
|
||||||
if (db.version < 8) throw UnsupportedDatabaseVersionException()
|
if (db.version < 8) throw UnsupportedDatabaseVersionException()
|
||||||
val wrappedDb = AndroidDatabase(db, File(databaseFilename))
|
val wrappedDb = AndroidDatabase(db)
|
||||||
wrappedDb.setVersion(db.version)
|
wrappedDb.setVersion(db.version)
|
||||||
wrappedDb.migrateTo(newVersion) { version ->
|
wrappedDb.migrateTo(newVersion) { version ->
|
||||||
val filename = "%02d.sql".format(version)
|
val filename = "%02d.sql".format(version)
|
||||||
|
|||||||
@ -19,12 +19,10 @@
|
|||||||
|
|
||||||
package org.isoron.uhabits.database
|
package org.isoron.uhabits.database
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteStatement
|
import android.database.sqlite.SQLiteStatement
|
||||||
import org.isoron.platform.io.PreparedStatement
|
import org.isoron.platform.io.PreparedStatement
|
||||||
import org.isoron.platform.io.StepResult
|
import org.isoron.platform.io.StepResult
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class AndroidPreparedStatement(
|
class AndroidPreparedStatement(
|
||||||
private val db: SQLiteDatabase,
|
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(
|
class AndroidDatabase(
|
||||||
private val db: SQLiteDatabase,
|
private val db: SQLiteDatabase
|
||||||
override val file: File? = null
|
) : org.isoron.platform.io.Database {
|
||||||
) : org.isoron.platform.io.Database, org.isoron.uhabits.core.database.Database {
|
|
||||||
|
|
||||||
// New PreparedStatement-based interface
|
|
||||||
override fun prepareStatement(sql: String): PreparedStatement =
|
override fun prepareStatement(sql: String): PreparedStatement =
|
||||||
AndroidPreparedStatement(db, sql)
|
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 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 android.database.sqlite.SQLiteDatabase
|
||||||
import me.tatarka.inject.annotations.Inject
|
import me.tatarka.inject.annotations.Inject
|
||||||
import org.isoron.uhabits.core.database.DatabaseOpener
|
import org.isoron.platform.io.DatabaseOpener
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class AndroidDatabaseOpener() : DatabaseOpener {
|
class AndroidDatabaseOpener() : DatabaseOpener {
|
||||||
override fun open(file: File): AndroidDatabase {
|
override fun open(path: String): AndroidDatabase {
|
||||||
return AndroidDatabase(
|
return AndroidDatabase(
|
||||||
db = SQLiteDatabase.openDatabase(
|
db = SQLiteDatabase.openDatabase(
|
||||||
file.absolutePath,
|
path,
|
||||||
null,
|
null,
|
||||||
SQLiteDatabase.OPEN_READWRITE
|
SQLiteDatabase.OPEN_READWRITE
|
||||||
),
|
)
|
||||||
file = file
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,10 +21,9 @@ package org.isoron.uhabits.inject
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import me.tatarka.inject.annotations.Component
|
import me.tatarka.inject.annotations.Component
|
||||||
import me.tatarka.inject.annotations.Provides
|
import me.tatarka.inject.annotations.Provides
|
||||||
|
import org.isoron.platform.io.DatabaseOpener
|
||||||
import org.isoron.uhabits.core.AppScope
|
import org.isoron.uhabits.core.AppScope
|
||||||
import org.isoron.uhabits.core.commands.CommandRunner
|
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.GenericImporter
|
||||||
import org.isoron.uhabits.core.io.Logging
|
import org.isoron.uhabits.core.io.Logging
|
||||||
import org.isoron.uhabits.core.models.HabitList
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
@ -85,13 +84,9 @@ abstract class HabitsApplicationComponent(
|
|||||||
get() = providedDb
|
get() = providedDb
|
||||||
|
|
||||||
private val providedDb: AndroidDatabase by lazy {
|
private val providedDb: AndroidDatabase by lazy {
|
||||||
AndroidDatabase(DatabaseUtils.openDatabase(), dbFile)
|
AndroidDatabase(DatabaseUtils.openDatabase())
|
||||||
}
|
}
|
||||||
|
|
||||||
@AppScope
|
|
||||||
@Provides
|
|
||||||
open fun database(): Database = providedDb
|
|
||||||
|
|
||||||
@AppScope
|
@AppScope
|
||||||
@Provides
|
@Provides
|
||||||
open fun preferences(storage: SharedPreferencesStorage): Preferences =
|
open fun preferences(storage: SharedPreferencesStorage): Preferences =
|
||||||
|
|||||||
@ -77,14 +77,41 @@ fun Database.commit() {
|
|||||||
run("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) {
|
fun Database.migrateTo(targetVersion: Int, loadMigrationSQL: (Int) -> String) {
|
||||||
val currentVersion = getVersion()
|
val currentVersion = getVersion()
|
||||||
if (currentVersion >= targetVersion) return
|
if (currentVersion >= targetVersion) return
|
||||||
begin()
|
|
||||||
for (v in (currentVersion + 1)..targetVersion) {
|
for (v in (currentVersion + 1)..targetVersion) {
|
||||||
val commands = SQLParser.parse(loadMigrationSQL(v))
|
val commands = SQLParser.parse(loadMigrationSQL(v))
|
||||||
for (cmd in commands) run(cmd)
|
for (cmd in commands) run(cmd)
|
||||||
setVersion(v)
|
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
|
package org.isoron.uhabits.core.io
|
||||||
|
|
||||||
import me.tatarka.inject.annotations.Inject
|
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.platform.time.LocalDate
|
||||||
import org.isoron.uhabits.core.AppScope
|
import org.isoron.uhabits.core.AppScope
|
||||||
import org.isoron.uhabits.core.DATABASE_VERSION
|
import org.isoron.uhabits.core.DATABASE_VERSION
|
||||||
import org.isoron.uhabits.core.commands.CommandRunner
|
import org.isoron.uhabits.core.commands.CommandRunner
|
||||||
import org.isoron.uhabits.core.commands.CreateHabitCommand
|
import org.isoron.uhabits.core.commands.CreateHabitCommand
|
||||||
import org.isoron.uhabits.core.commands.EditHabitCommand
|
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.HabitData
|
||||||
import org.isoron.uhabits.core.database.MigrationHelper
|
|
||||||
import org.isoron.uhabits.core.models.Entry
|
import org.isoron.uhabits.core.models.Entry
|
||||||
import org.isoron.uhabits.core.models.HabitList
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
import org.isoron.uhabits.core.models.ModelFactory
|
import org.isoron.uhabits.core.models.ModelFactory
|
||||||
@ -53,26 +55,30 @@ class LoopDBImporter(
|
|||||||
|
|
||||||
override fun canHandle(file: File): Boolean {
|
override fun canHandle(file: File): Boolean {
|
||||||
if (!file.isSQLite3File()) return false
|
if (!file.isSQLite3File()) return false
|
||||||
val db = opener.open(file)
|
val db = opener.open(file.absolutePath)
|
||||||
var canHandle = true
|
var canHandle = true
|
||||||
val c = db.query("select count(*) from SQLITE_MASTER where name='Habits' or name='Repetitions'")
|
val count = db.querySingle(
|
||||||
if (!c.moveToNext() || c.getInt(0) != 2) {
|
"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")
|
logger.error("Cannot handle file: tables not found")
|
||||||
canHandle = false
|
canHandle = false
|
||||||
}
|
}
|
||||||
if (db.version > DATABASE_VERSION) {
|
if (db.getVersion() > DATABASE_VERSION) {
|
||||||
logger.error("Cannot handle file: incompatible version: ${db.version} > $DATABASE_VERSION")
|
logger.error("Cannot handle file: incompatible version: ${db.getVersion()} > $DATABASE_VERSION")
|
||||||
canHandle = false
|
canHandle = false
|
||||||
}
|
}
|
||||||
c.close()
|
|
||||||
db.close()
|
db.close()
|
||||||
return canHandle
|
return canHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun importHabitsFromFile(file: File) {
|
override fun importHabitsFromFile(file: File) {
|
||||||
val db = opener.open(file)
|
val db = opener.open(file.absolutePath)
|
||||||
val helper = MigrationHelper(db)
|
db.migrateTo(DATABASE_VERSION) { version ->
|
||||||
helper.migrateTo(DATABASE_VERSION)
|
val filename = "%02d.sql".format(version)
|
||||||
|
javaClass.getResourceAsStream("/migrations/$filename")!!
|
||||||
|
.bufferedReader().readText()
|
||||||
|
}
|
||||||
|
|
||||||
val habitDataList = loadHabits(db)
|
val habitDataList = loadHabits(db)
|
||||||
for (habitData in habitDataList) {
|
for (habitData in habitDataList) {
|
||||||
@ -89,21 +95,20 @@ class LoopDBImporter(
|
|||||||
EditHabitCommand(habitList, habit.id!!, modified).run()
|
EditHabitCommand(habitList, habit.id!!, modified).run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload saved version of the habit
|
|
||||||
habit = habitList.getByUUID(habitData.uuid)!!
|
habit = habitList.getByUUID(habitData.uuid)!!
|
||||||
val entries = habit.originalEntries
|
val entries = habit.originalEntries
|
||||||
|
|
||||||
// Import entries
|
db.query(
|
||||||
loadEntries(db, habitData.id!!).use { c ->
|
"SELECT timestamp, value, notes FROM Repetitions WHERE habit = ? ORDER BY timestamp DESC",
|
||||||
while (c.moveToNext()) {
|
habitData.id.toString()
|
||||||
val timestamp = c.getLong(0) ?: continue
|
) { stmt ->
|
||||||
val value = c.getInt(1) ?: continue
|
val timestamp = stmt.getLongOrNull(0) ?: return@query
|
||||||
val notes = c.getString(2) ?: ""
|
val value = stmt.getIntOrNull(1) ?: return@query
|
||||||
val date = LocalDate.fromUnixTime(timestamp)
|
val notes = stmt.getTextOrNull(2) ?: ""
|
||||||
val (_, existingValue, existingNotes) = entries.get(date)
|
val date = LocalDate.fromUnixTime(timestamp)
|
||||||
if (existingValue != value || existingNotes != notes) {
|
val (_, existingValue, existingNotes) = entries.get(date)
|
||||||
entries.add(Entry(date, value, notes))
|
if (existingValue != value || existingNotes != notes) {
|
||||||
}
|
entries.add(Entry(date, value, notes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
habit.recompute()
|
habit.recompute()
|
||||||
@ -119,41 +124,30 @@ class LoopDBImporter(
|
|||||||
"position, reminder_hour, reminder_min, reminder_days, highlight, " +
|
"position, reminder_hour, reminder_min, reminder_days, highlight, " +
|
||||||
"archived, type, target_value, target_type, unit, uuid " +
|
"archived, type, target_value, target_type, unit, uuid " +
|
||||||
"FROM Habits ORDER BY position"
|
"FROM Habits ORDER BY position"
|
||||||
).use { c ->
|
) { stmt ->
|
||||||
while (c.moveToNext()) {
|
result.add(
|
||||||
result.add(cursorToHabitData(c))
|
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
|
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
|
package org.isoron.uhabits.core.io
|
||||||
|
|
||||||
import me.tatarka.inject.annotations.Inject
|
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.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.Entry
|
||||||
import org.isoron.uhabits.core.models.Frequency
|
import org.isoron.uhabits.core.models.Frequency
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
@ -45,124 +48,98 @@ class RewireDBImporter(
|
|||||||
|
|
||||||
override fun canHandle(file: File): Boolean {
|
override fun canHandle(file: File): Boolean {
|
||||||
if (!file.isSQLite3File()) return false
|
if (!file.isSQLite3File()) return false
|
||||||
val db = opener.open(file)
|
val db = opener.open(file.absolutePath)
|
||||||
val c = db.query(
|
val count = db.querySingle(
|
||||||
"select count(*) from SQLITE_MASTER " +
|
"select count(*) from SQLITE_MASTER where name='CHECKINS' or name='UNIT'"
|
||||||
"where name='CHECKINS' or name='UNIT'"
|
) { it.getInt(0) }
|
||||||
)
|
|
||||||
val result = c.moveToNext() && c.getInt(0) == 2
|
|
||||||
c.close()
|
|
||||||
db.close()
|
db.close()
|
||||||
return result
|
return count == 2
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun importHabitsFromFile(file: File) {
|
override fun importHabitsFromFile(file: File) {
|
||||||
val db = opener.open(file)
|
val db = opener.open(file.absolutePath)
|
||||||
db.beginTransaction()
|
db.begin()
|
||||||
createHabits(db)
|
createHabits(db)
|
||||||
db.setTransactionSuccessful()
|
db.commit()
|
||||||
db.endTransaction()
|
|
||||||
db.close()
|
db.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createHabits(db: Database) {
|
private fun createHabits(db: Database) {
|
||||||
var c: Cursor? = null
|
db.query(
|
||||||
try {
|
"select _id, name, description, schedule, " +
|
||||||
c = db.query(
|
"active_days, repeating_count, days, period " +
|
||||||
"select _id, name, description, schedule, " +
|
"from habits"
|
||||||
"active_days, repeating_count, days, period " +
|
) { stmt ->
|
||||||
"from habits"
|
val id = stmt.getInt(0)
|
||||||
)
|
val name = stmt.getText(1)
|
||||||
if (!c.moveToNext()) return
|
val description = stmt.getTextOrNull(2) ?: ""
|
||||||
do {
|
val schedule = stmt.getInt(3)
|
||||||
val id = c.getInt(0)!!
|
val activeDays = stmt.getTextOrNull(4)
|
||||||
val name = c.getString(1)
|
val repeatingCount = stmt.getInt(5)
|
||||||
val description = c.getString(2)
|
val days = stmt.getInt(6)
|
||||||
val schedule = c.getInt(3)!!
|
val periodIndex = stmt.getInt(7)
|
||||||
val activeDays = c.getString(4)
|
|
||||||
val repeatingCount = c.getInt(5)!!
|
|
||||||
val days = c.getInt(6)!!
|
|
||||||
val periodIndex = c.getInt(7)!!
|
|
||||||
|
|
||||||
val habit = modelFactory.buildHabit()
|
val habit = modelFactory.buildHabit()
|
||||||
habit.name = name!!
|
habit.name = name
|
||||||
habit.description = description ?: ""
|
habit.description = description
|
||||||
val periods = intArrayOf(7, 31, 365)
|
val periods = intArrayOf(7, 31, 365)
|
||||||
var numerator: Int
|
var numerator: Int
|
||||||
var denominator: Int
|
var denominator: Int
|
||||||
when (schedule) {
|
when (schedule) {
|
||||||
0 -> {
|
0 -> {
|
||||||
numerator = activeDays!!.split(",").toTypedArray().size
|
numerator = activeDays!!.split(",").toTypedArray().size
|
||||||
denominator = 7
|
denominator = 7
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
numerator = days
|
|
||||||
denominator = periods[periodIndex]
|
|
||||||
}
|
|
||||||
2 -> {
|
|
||||||
numerator = 1
|
|
||||||
denominator = repeatingCount
|
|
||||||
}
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
}
|
||||||
habit.frequency = Frequency(numerator, denominator)
|
1 -> {
|
||||||
habitList.add(habit)
|
numerator = days
|
||||||
createReminder(db, habit, id)
|
denominator = periods[periodIndex]
|
||||||
createCheckmarks(db, habit, id)
|
}
|
||||||
} while (c.moveToNext())
|
2 -> {
|
||||||
} finally {
|
numerator = 1
|
||||||
c?.close()
|
denominator = repeatingCount
|
||||||
|
}
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
habit.frequency = Frequency(numerator, denominator)
|
||||||
|
habitList.add(habit)
|
||||||
|
createReminder(db, habit, id)
|
||||||
|
createCheckmarks(db, habit, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createCheckmarks(
|
private fun createCheckmarks(db: Database, habit: Habit, rewireHabitId: Int) {
|
||||||
db: Database,
|
db.query(
|
||||||
habit: Habit,
|
"select distinct date from checkins where habit_id=? and type=2",
|
||||||
rewireHabitId: Int
|
rewireHabitId.toString()
|
||||||
) {
|
) { stmt ->
|
||||||
var c: Cursor? = null
|
val dateStr = stmt.getText(0)
|
||||||
try {
|
val year = dateStr.substring(0, 4).toInt()
|
||||||
c = db.query(
|
val month = dateStr.substring(4, 6).toInt()
|
||||||
"select distinct date from checkins where habit_id=? and type=2",
|
val day = dateStr.substring(6, 8).toInt()
|
||||||
rewireHabitId.toString()
|
habit.originalEntries.add(Entry(LocalDate(year, month, day), Entry.YES_MANUAL))
|
||||||
)
|
|
||||||
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 createReminder(db: Database, habit: Habit, rewireHabitId: Int) {
|
private fun createReminder(db: Database, habit: Habit, rewireHabitId: Int) {
|
||||||
var c: Cursor? = null
|
val reminder = db.querySingle(
|
||||||
try {
|
"select time, active_days from reminders where habit_id=? limit 1",
|
||||||
c = db.query(
|
rewireHabitId.toString()
|
||||||
"select time, active_days from reminders where habit_id=? limit 1",
|
) { stmt ->
|
||||||
rewireHabitId.toString()
|
val rewireReminder = stmt.getInt(0)
|
||||||
)
|
if (rewireReminder <= 0 || rewireReminder >= 1440) return@querySingle null
|
||||||
if (!c.moveToNext()) return
|
|
||||||
val rewireReminder = c.getInt(0)!!
|
|
||||||
if (rewireReminder <= 0 || rewireReminder >= 1440) return
|
|
||||||
val reminderDays = BooleanArray(7)
|
val reminderDays = BooleanArray(7)
|
||||||
val activeDays = c.getString(1)!!.split(",").toTypedArray()
|
val activeDaysStr = stmt.getText(1).split(",").toTypedArray()
|
||||||
for (d in activeDays) {
|
for (d in activeDaysStr) {
|
||||||
val idx = (d.toInt() + 1) % 7
|
val idx = (d.toInt() + 1) % 7
|
||||||
reminderDays[idx] = true
|
reminderDays[idx] = true
|
||||||
}
|
}
|
||||||
val hour = rewireReminder / 60
|
val hour = rewireReminder / 60
|
||||||
val minute = rewireReminder % 60
|
val minute = rewireReminder % 60
|
||||||
val days = WeekdayList(reminderDays)
|
Reminder(hour, minute, WeekdayList(reminderDays))
|
||||||
val reminder = Reminder(hour, minute, days)
|
}
|
||||||
|
if (reminder != null) {
|
||||||
habit.reminder = reminder
|
habit.reminder = reminder
|
||||||
habitList.update(habit)
|
habitList.update(habit)
|
||||||
} finally {
|
|
||||||
c?.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,10 +19,13 @@
|
|||||||
package org.isoron.uhabits.core.io
|
package org.isoron.uhabits.core.io
|
||||||
|
|
||||||
import me.tatarka.inject.annotations.Inject
|
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.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.Entry
|
||||||
import org.isoron.uhabits.core.models.Frequency
|
import org.isoron.uhabits.core.models.Frequency
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
@ -43,67 +46,45 @@ class TickmateDBImporter(
|
|||||||
|
|
||||||
override fun canHandle(file: File): Boolean {
|
override fun canHandle(file: File): Boolean {
|
||||||
if (!file.isSQLite3File()) return false
|
if (!file.isSQLite3File()) return false
|
||||||
val db = opener.open(file)
|
val db = opener.open(file.absolutePath)
|
||||||
val c = db.query(
|
val count = db.querySingle(
|
||||||
"select count(*) from SQLITE_MASTER " +
|
"select count(*) from SQLITE_MASTER where name='tracks' or name='track2groups'"
|
||||||
"where name='tracks' or name='track2groups'"
|
) { it.getInt(0) }
|
||||||
)
|
|
||||||
val result = c.moveToNext() && c.getInt(0) == 2
|
|
||||||
c.close()
|
|
||||||
db.close()
|
db.close()
|
||||||
return result
|
return count == 2
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun importHabitsFromFile(file: File) {
|
override fun importHabitsFromFile(file: File) {
|
||||||
val db = opener.open(file)
|
val db = opener.open(file.absolutePath)
|
||||||
db.beginTransaction()
|
db.begin()
|
||||||
createHabits(db)
|
createHabits(db)
|
||||||
db.setTransactionSuccessful()
|
db.commit()
|
||||||
db.endTransaction()
|
|
||||||
db.close()
|
db.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createCheckmarks(
|
private fun createCheckmarks(db: Database, habit: Habit, tickmateTrackId: Int) {
|
||||||
db: Database,
|
db.query(
|
||||||
habit: Habit,
|
"select distinct year, month, day from ticks where _track_id=?",
|
||||||
tickmateTrackId: Int
|
tickmateTrackId.toString()
|
||||||
) {
|
) { stmt ->
|
||||||
var c: Cursor? = null
|
val year = stmt.getInt(0)
|
||||||
try {
|
val month = stmt.getInt(1)
|
||||||
c = db.query(
|
val day = stmt.getInt(2)
|
||||||
"select distinct year, month, day from ticks where _track_id=?",
|
habit.originalEntries.add(Entry(LocalDate(year, month + 1, day), Entry.YES_MANUAL))
|
||||||
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 createHabits(db: Database) {
|
private fun createHabits(db: Database) {
|
||||||
var c: Cursor? = null
|
db.query("select _id, name, description from tracks") { stmt ->
|
||||||
try {
|
val id = stmt.getInt(0)
|
||||||
c = db.query("select _id, name, description from tracks")
|
val name = stmt.getText(1)
|
||||||
if (!c.moveToNext()) return
|
val description = stmt.getTextOrNull(2) ?: ""
|
||||||
do {
|
val habit = modelFactory.buildHabit()
|
||||||
val id = c.getInt(0)!!
|
habit.name = name
|
||||||
val name = c.getString(1)
|
habit.description = description
|
||||||
val description = c.getString(2)
|
habit.frequency = Frequency.DAILY
|
||||||
val habit = modelFactory.buildHabit()
|
habitList.add(habit)
|
||||||
habit.name = name!!
|
createCheckmarks(db, habit, id)
|
||||||
habit.description = description ?: ""
|
|
||||||
habit.frequency = Frequency.DAILY
|
|
||||||
habitList.add(habit)
|
|
||||||
createCheckmarks(db, habit, id)
|
|
||||||
} while (c.moveToNext())
|
|
||||||
} finally {
|
|
||||||
c?.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,14 +19,11 @@
|
|||||||
package org.isoron.uhabits.core
|
package org.isoron.uhabits.core
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.isoron.platform.io.JavaDatabaseOpener
|
||||||
import org.isoron.platform.io.TestDatabaseHelper
|
import org.isoron.platform.io.TestDatabaseHelper
|
||||||
import org.isoron.platform.time.LocalDate
|
import org.isoron.platform.time.LocalDate
|
||||||
import org.isoron.platform.time.setToday
|
import org.isoron.platform.time.setToday
|
||||||
import org.isoron.uhabits.core.commands.CommandRunner
|
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.HabitList
|
||||||
import org.isoron.uhabits.core.models.ModelFactory
|
import org.isoron.uhabits.core.models.ModelFactory
|
||||||
import org.isoron.uhabits.core.models.memory.MemoryModelFactory
|
import org.isoron.uhabits.core.models.memory.MemoryModelFactory
|
||||||
@ -45,8 +42,6 @@ import java.io.FileOutputStream
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.sql.DriverManager
|
|
||||||
import java.sql.SQLException
|
|
||||||
import java.util.GregorianCalendar
|
import java.util.GregorianCalendar
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
|
||||||
@ -57,22 +52,7 @@ open class BaseUnitTest {
|
|||||||
protected lateinit var modelFactory: ModelFactory
|
protected lateinit var modelFactory: ModelFactory
|
||||||
protected lateinit var taskRunner: SingleThreadTaskRunner
|
protected lateinit var taskRunner: SingleThreadTaskRunner
|
||||||
protected open lateinit var commandRunner: CommandRunner
|
protected open lateinit var commandRunner: CommandRunner
|
||||||
protected var databaseOpener: DatabaseOpener = object : DatabaseOpener {
|
protected var databaseOpener: org.isoron.platform.io.DatabaseOpener = JavaDatabaseOpener()
|
||||||
override fun open(file: File): Database {
|
|
||||||
return try {
|
|
||||||
JdbcDatabase(
|
|
||||||
DriverManager.getConnection(
|
|
||||||
String.format(
|
|
||||||
"jdbc:sqlite:%s",
|
|
||||||
file.absolutePath
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: SQLException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -125,31 +105,17 @@ open class BaseUnitTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected fun openDatabaseResource(path: String): Database {
|
protected fun openDatabaseResource(path: String): org.isoron.platform.io.Database {
|
||||||
val original = openAsset(path)
|
val original = openAsset(path)
|
||||||
val tmpDbFile = File.createTempFile("database", ".db")
|
val tmpDbFile = File.createTempFile("database", ".db")
|
||||||
tmpDbFile.deleteOnExit()
|
tmpDbFile.deleteOnExit()
|
||||||
IOUtils.copy(original, FileOutputStream(tmpDbFile))
|
IOUtils.copy(original, FileOutputStream(tmpDbFile))
|
||||||
return databaseOpener.open(tmpDbFile)
|
return databaseOpener.open(tmpDbFile.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun buildMemoryDatabase(): Database {
|
fun buildMemoryDatabase(): org.isoron.platform.io.Database {
|
||||||
return try {
|
return TestDatabaseHelper.createEmptyDatabase()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,132 +21,132 @@ package org.isoron.uhabits.core.database.migrations
|
|||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
import org.hamcrest.Matchers
|
import org.hamcrest.Matchers
|
||||||
import org.hamcrest.Matchers.equalTo
|
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.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.Test
|
||||||
import org.junit.jupiter.api.Assertions.assertThrows
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
|
||||||
class Version22Test : BaseUnitTest() {
|
class Version22Test : BaseUnitTest() {
|
||||||
private lateinit var db: Database
|
private lateinit var db: Database
|
||||||
private lateinit var helper: MigrationHelper
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
db = openDatabaseResource("/databases/021.db")
|
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
|
@Test
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testKeepValidReps() {
|
fun testKeepValidReps() {
|
||||||
db.query("select count(*) from repetitions") { c: Cursor ->
|
val before = db.querySingle("select count(*) from repetitions") { it.getInt(0) }
|
||||||
assertThat(c.getInt(0), equalTo(3))
|
assertThat(before, equalTo(3))
|
||||||
}
|
migrateTo(22)
|
||||||
helper.migrateTo(22)
|
val after = db.querySingle("select count(*) from repetitions") { it.getInt(0) }
|
||||||
db.query("select count(*) from repetitions") { c: Cursor ->
|
assertThat(after, equalTo(3))
|
||||||
assertThat(c.getInt(0), equalTo(3))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testRemoveRepsWithInvalidId() {
|
fun testRemoveRepsWithInvalidId() {
|
||||||
db.execute("insert into Repetitions(habit, timestamp, value) values (99999, 100, 2)")
|
db.run("insert into Repetitions(habit, timestamp, value) values (99999, 100, 2)")
|
||||||
db.query("select count(*) from repetitions where habit = 99999") { c: Cursor ->
|
val before = db.querySingle(
|
||||||
assertThat(c.getInt(0), equalTo(1))
|
"select count(*) from repetitions where habit = 99999"
|
||||||
}
|
) { it.getInt(0) }
|
||||||
helper.migrateTo(22)
|
assertThat(before, equalTo(1))
|
||||||
db.query("select count(*) from repetitions where habit = 99999") { c: Cursor ->
|
migrateTo(22)
|
||||||
assertThat(c.getInt(0), equalTo(0))
|
val after = db.querySingle(
|
||||||
}
|
"select count(*) from repetitions where habit = 99999"
|
||||||
|
) { it.getInt(0) }
|
||||||
|
assertThat(after, equalTo(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testDisallowNewRepsWithInvalidRef() {
|
fun testDisallowNewRepsWithInvalidRef() {
|
||||||
helper.migrateTo(22)
|
migrateTo(22)
|
||||||
val exception = assertThrows(java.lang.RuntimeException::class.java) { db.execute("insert into Repetitions(habit, timestamp, value) values (99999, 100, 2)") }
|
val exception = assertThrows(Exception::class.java) {
|
||||||
assertThat(exception.message, Matchers.containsString("SQLITE_CONSTRAINT"))
|
db.run("insert into Repetitions(habit, timestamp, value) values (99999, 100, 2)")
|
||||||
|
}
|
||||||
|
assertThat(exception.message, Matchers.containsString("constraint"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testRemoveRepetitionsWithNullTimestamp() {
|
fun testRemoveRepetitionsWithNullTimestamp() {
|
||||||
db.execute("insert into repetitions(habit, value) values (0, 2)")
|
db.run("insert into repetitions(habit, value) values (0, 2)")
|
||||||
db.query("select count(*) from repetitions where timestamp is null") { c: Cursor ->
|
val before = db.querySingle(
|
||||||
assertThat(c.getInt(0), equalTo(1))
|
"select count(*) from repetitions where timestamp is null"
|
||||||
}
|
) { it.getInt(0) }
|
||||||
helper.migrateTo(22)
|
assertThat(before, equalTo(1))
|
||||||
db.query("select count(*) from repetitions where timestamp is null") { c: Cursor ->
|
migrateTo(22)
|
||||||
assertThat(c.getInt(0), equalTo(0))
|
val after = db.querySingle(
|
||||||
}
|
"select count(*) from repetitions where timestamp is null"
|
||||||
|
) { it.getInt(0) }
|
||||||
|
assertThat(after, equalTo(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testDisallowNullTimestamp() {
|
fun testDisallowNullTimestamp() {
|
||||||
helper.migrateTo(22)
|
migrateTo(22)
|
||||||
|
val exception = assertThrows(Exception::class.java) {
|
||||||
val exception = assertThrows(java.lang.RuntimeException::class.java) {
|
db.run("insert into Repetitions(habit, value) values (0, 2)")
|
||||||
db.execute("insert into Repetitions(habit, value) " + "values (0, 2)")
|
|
||||||
}
|
}
|
||||||
|
assertThat(exception.message, Matchers.containsString("constraint"))
|
||||||
assertThat(exception.message, Matchers.containsString("SQLITE_CONSTRAINT"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testRemoveRepetitionsWithNullHabit() {
|
fun testRemoveRepetitionsWithNullHabit() {
|
||||||
db.execute("insert into repetitions(timestamp, value) values (0, 2)")
|
db.run("insert into repetitions(timestamp, value) values (0, 2)")
|
||||||
db.query("select count(*) from repetitions where habit is null") { c: Cursor ->
|
val before = db.querySingle(
|
||||||
assertThat(c.getInt(0), equalTo(1))
|
"select count(*) from repetitions where habit is null"
|
||||||
}
|
) { it.getInt(0) }
|
||||||
helper.migrateTo(22)
|
assertThat(before, equalTo(1))
|
||||||
db.query("select count(*) from repetitions where habit is null") { c: Cursor ->
|
migrateTo(22)
|
||||||
assertThat(c.getInt(0), equalTo(0))
|
val after = db.querySingle(
|
||||||
}
|
"select count(*) from repetitions where habit is null"
|
||||||
|
) { it.getInt(0) }
|
||||||
|
assertThat(after, equalTo(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testDisallowNullHabit() {
|
fun testDisallowNullHabit() {
|
||||||
helper.migrateTo(22)
|
migrateTo(22)
|
||||||
|
val exception = assertThrows(Exception::class.java) {
|
||||||
val exception = assertThrows(java.lang.RuntimeException::class.java) {
|
db.run("insert into Repetitions(timestamp, value) values (5, 2)")
|
||||||
db.execute("insert into Repetitions(timestamp, value) " + "values (5, 2)")
|
|
||||||
}
|
}
|
||||||
|
assertThat(exception.message, Matchers.containsString("constraint"))
|
||||||
assertThat(exception.message, Matchers.containsString("SQLITE_CONSTRAINT"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testRemoveDuplicateRepetitions() {
|
fun testRemoveDuplicateRepetitions() {
|
||||||
db.execute("insert into repetitions(habit, timestamp, value)values (0, 100, 2)")
|
db.run("insert into repetitions(habit, timestamp, value)values (0, 100, 2)")
|
||||||
db.execute("insert into repetitions(habit, timestamp, value)values (0, 100, 5)")
|
db.run("insert into repetitions(habit, timestamp, value)values (0, 100, 5)")
|
||||||
db.execute("insert into repetitions(habit, timestamp, value)values (0, 100, 10)")
|
db.run("insert into repetitions(habit, timestamp, value)values (0, 100, 10)")
|
||||||
db.query("select count(*) from repetitions where timestamp=100 and habit=0") { c: Cursor ->
|
val before = db.querySingle(
|
||||||
assertThat(c.getInt(0), equalTo(3))
|
"select count(*) from repetitions where timestamp=100 and habit=0"
|
||||||
}
|
) { it.getInt(0) }
|
||||||
helper.migrateTo(22)
|
assertThat(before, equalTo(3))
|
||||||
db.query("select count(*) from repetitions where timestamp=100 and habit=0") { c: Cursor ->
|
migrateTo(22)
|
||||||
assertThat(c.getInt(0), equalTo(1))
|
val after = db.querySingle(
|
||||||
}
|
"select count(*) from repetitions where timestamp=100 and habit=0"
|
||||||
|
) { it.getInt(0) }
|
||||||
|
assertThat(after, equalTo(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testDisallowNewDuplicateTimestamps() {
|
fun testDisallowNewDuplicateTimestamps() {
|
||||||
helper.migrateTo(22)
|
migrateTo(22)
|
||||||
db.execute("insert into repetitions(habit, timestamp, value)values (0, 100, 2)")
|
db.run("insert into repetitions(habit, timestamp, value)values (0, 100, 2)")
|
||||||
|
val exception = assertThrows(Exception::class.java) {
|
||||||
val exception = assertThrows(java.lang.RuntimeException::class.java) {
|
db.run("insert into repetitions(habit, timestamp, value)values (0, 100, 5)")
|
||||||
db.execute("insert into repetitions(habit, timestamp, value)values (0, 100, 5)")
|
|
||||||
}
|
}
|
||||||
|
assertThat(exception.message, Matchers.containsString("constraint"))
|
||||||
assertThat(exception.message, Matchers.containsString("SQLITE_CONSTRAINT"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,57 +21,59 @@ package org.isoron.uhabits.core.database.migrations
|
|||||||
|
|
||||||
import org.hamcrest.CoreMatchers.equalTo
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
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.BaseUnitTest
|
||||||
import org.isoron.uhabits.core.database.Database
|
|
||||||
import org.isoron.uhabits.core.database.MigrationHelper
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class Version23Test : BaseUnitTest() {
|
class Version23Test : BaseUnitTest() {
|
||||||
|
|
||||||
private lateinit var db: Database
|
private lateinit var db: Database
|
||||||
|
|
||||||
private lateinit var helper: MigrationHelper
|
|
||||||
|
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
db = openDatabaseResource("/databases/022.db")
|
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
|
@Test
|
||||||
fun `test migrate to 23 creates question column`() {
|
fun `test migrate to 23 creates question column`() {
|
||||||
migrateTo23()
|
migrateTo(23)
|
||||||
val cursor = db.query("select question from Habits")
|
db.query("select question from Habits") {}
|
||||||
cursor.moveToNext()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test migrate to 23 moves description to question column`() {
|
fun `test migrate to 23 moves description to question column`() {
|
||||||
var cursor = db.query("select description from Habits")
|
|
||||||
|
|
||||||
val descriptions = mutableListOf<String?>()
|
val descriptions = mutableListOf<String?>()
|
||||||
while (cursor.moveToNext()) {
|
db.query("select description from Habits") { stmt ->
|
||||||
descriptions.add(cursor.getString(0))
|
descriptions.add(stmt.getTextOrNull(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
migrateTo23()
|
migrateTo(23)
|
||||||
cursor = db.query("select question from Habits")
|
|
||||||
|
|
||||||
for (i in 0 until descriptions.size) {
|
val questions = mutableListOf<String?>()
|
||||||
cursor.moveToNext()
|
db.query("select question from Habits") { stmt ->
|
||||||
assertThat(cursor.getString(0), equalTo(descriptions[i]))
|
questions.add(stmt.getTextOrNull(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in descriptions.indices) {
|
||||||
|
assertThat(questions[i], equalTo(descriptions[i]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test migrate to 23 sets description to null`() {
|
fun `test migrate to 23 sets description to null`() {
|
||||||
migrateTo23()
|
migrateTo(23)
|
||||||
val cursor = db.query("select description from Habits")
|
db.query("select description from Habits") { stmt ->
|
||||||
|
assertThat(stmt.getTextOrNull(0), equalTo(""))
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
assertThat(cursor.getString(0), equalTo(""))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
package org.isoron.uhabits.core.models.sqlite
|
package org.isoron.uhabits.core.models.sqlite
|
||||||
|
|
||||||
import org.isoron.platform.time.LocalDate
|
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.database.EntryData
|
||||||
import org.isoron.uhabits.core.models.Entry
|
import org.isoron.uhabits.core.models.Entry
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
||||||
@ -30,7 +30,7 @@ import kotlin.test.assertEquals
|
|||||||
|
|
||||||
class SQLiteEntryListTest {
|
class SQLiteEntryListTest {
|
||||||
|
|
||||||
private val database = buildNewMemoryDatabase()
|
private val database = buildMemoryDatabase()
|
||||||
private val factory = SQLModelFactory(database)
|
private val factory = SQLModelFactory(database)
|
||||||
private val entryRepository = factory.entryRepository
|
private val entryRepository = factory.entryRepository
|
||||||
private lateinit var entries: SQLiteEntryList
|
private lateinit var entries: SQLiteEntryList
|
||||||
|
|||||||
@ -46,7 +46,7 @@ class SQLiteHabitListTest : BaseUnitTest() {
|
|||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
val db = buildNewMemoryDatabase()
|
val db = buildMemoryDatabase()
|
||||||
modelFactory = SQLModelFactory(db)
|
modelFactory = SQLModelFactory(db)
|
||||||
habitList = SQLiteHabitList(modelFactory)
|
habitList = SQLiteHabitList(modelFactory)
|
||||||
fixtures = HabitFixtures(modelFactory, habitList)
|
fixtures = HabitFixtures(modelFactory, habitList)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user