Skip to content

Commit

Permalink
Add initial support for db indexes.
Browse files Browse the repository at this point in the history
  • Loading branch information
zbsz committed Jul 3, 2015
1 parent 0aedf8a commit 72c1351
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 28 deletions.
15 changes: 9 additions & 6 deletions src/main/scala/com/geteit/cache/CacheEntryData.scala
Expand Up @@ -3,8 +3,8 @@ package com.geteit.cache
import java.lang.System.currentTimeMillis
import java.util.UUID

import android.database.sqlite.SQLiteDatabase
import com.geteit.db.{Dao, DbType, Id}
import android.database.sqlite.{SQLiteProgram, SQLiteDatabase}
import com.geteit.db.{Index, Dao, DbType, Id}
import com.geteit.json.{Json, JsonValue}
import com.geteit.util.Log

Expand All @@ -19,7 +19,10 @@ object Uid {
override def encode(v: Uid): String = v.str
}

implicit object UidDbType extends DbType[Uid] with DbType.Text
implicit object UidDbType extends DbType[Uid] with DbType.Text {
override def literal(v: Uid): String = s"'${v.str}'"
def bind(stmt: SQLiteProgram, pos: Int, v: Uid) = stmt.bindString(pos, v.str)
}
}

@Json
Expand All @@ -43,9 +46,9 @@ object CacheEntryData {
implicit object CacheEntryDao extends Dao[String, CacheEntryData] {

object Indexes {
val expires = new Index[Long]("lastUsed", { e => e.lastUsed + e.timeout })
val hasData = new Index[Boolean]("hasData", _.data.nonEmpty)
val fileId = new Index[Uid]("fileId", _.fileId)
val expires = new Index[CacheEntryData, Long]("lastUsed", { e => e.lastUsed + e.timeout })
val hasData = new Index[CacheEntryData, Boolean]("hasData", _.data.nonEmpty)
val fileId = new Index[CacheEntryData, Uid]("fileId", _.fileId)
}

override def getId(v: CacheEntryData): String = v.key
Expand Down
66 changes: 59 additions & 7 deletions src/main/scala/com/geteit/db/CachedStorage.scala
Expand Up @@ -4,7 +4,8 @@ import android.database.sqlite.SQLiteDatabase
import android.support.v4.util.LruCache
import com.geteit.concurrent.{LimitedExecutionContext, Threading}
import com.geteit.events
import com.geteit.events.{EventContext, Signal}
import com.geteit.events.{EventObserver, EventContext, Signal}
import com.geteit.util.Log._
import com.geteit.util.ThrottledProcessingQueue

import scala.collection.JavaConverters._
Expand Down Expand Up @@ -140,16 +141,67 @@ abstract class CachedStorage[K, V](implicit val dao: Dao[K, V]) {


trait CachedStorageSignal[K, V] { self: CachedStorage[K, V] =>
import Threading.global
private implicit val tag: LogTag = "CachedStorageSignal"

def signal(id: K)(implicit ev: EventContext): Signal[V] = new Signal[V]() {

def reload() = self.get(id).map { _.foreach(this ! _) } (Threading.global)
def reload() = self.get(id).map { _.foreach(this ! _) }

// TODO: implement autowiring
onAdded.filter(v => dao.getId(v) == id) { this ! _ }
onUpdated.filter(p => dao.getId(p._2) == id) { case (_, v) => this ! v }
onRemoved.filter(_ == id) { _ => reload() }
private var observers = Seq.empty[EventObserver[_]]

reload()
override protected def onWire(): Unit = {
observers = Seq(
onAdded.filter(v => dao.getId(v) == id) { this ! _ }, // FIXME: possible race condition with reload result
onUpdated.filter(p => dao.getId(p._2) == id) { case (_, v) => this ! v },
onRemoved.filter(_ == id) { _ => reload() }
)
reload()
}

override protected def onUnwire(): Unit = {
observers foreach (_.destroy())
value = None
}
}

def findSignal(matcher: Matcher[V])(implicit ev: EventContext): Signal[Set[K]] = new Signal[Set[K]] {

def reload() = {
verbose(s"reload ${matcher.whereSql}")
storage { dao.find(matcher.whereSql)(_) } onSuccess {
case ids =>
verbose(s"found: $ids")
this ! (value.getOrElse(Set.empty) ++ ids)
}
}

private var observers = Seq.empty[EventObserver[_]]

override protected def onWire(): Unit = {
value = None
observers = Seq(
// FIXME: possible race conditions - on every upate
onAdded { v =>
if (matcher(v)) this ! (value.getOrElse(Set.empty) + dao.getId(v))
},
onUpdated {
case (prev, up) =>
(matcher(prev), matcher(up)) match {
case (true, false) => value foreach { s => this ! (s - dao.getId(prev)) }
case (false, true) => this ! value.getOrElse(Set.empty) + dao.getId(up)
case _ =>
}
},
onRemoved { id => value foreach { s => this ! (s - id) } }
)
reload()
}

override protected def onUnwire(): Unit = {
verbose(s"onUnwire")
observers foreach (_.destroy())
value = None
}
}
}
87 changes: 75 additions & 12 deletions src/main/scala/com/geteit/db/Dao.scala
@@ -1,7 +1,7 @@
package com.geteit.db

import android.database.Cursor
import android.database.sqlite.{SQLiteDatabase, SQLiteStatement}
import android.database.sqlite.{SQLiteProgram, SQLiteDatabase, SQLiteStatement}
import com.geteit.json.{JsonDecoder, JsonEncoder}
import com.geteit.util.returning

Expand All @@ -22,6 +22,8 @@ object Id {

trait DbType[A] {
val sqlName: String
def literal(v: A): String
def bind(stmt: SQLiteProgram, pos: Int, v: A)
}
object DbType {
trait Text { self: DbType[_] =>
Expand All @@ -31,10 +33,51 @@ object DbType {
override val sqlName = "INTEGER"
}

implicit object StringDbType extends DbType[String] with Text
implicit object IntDbType extends DbType[Int] with Integer
implicit object LongDbType extends DbType[Long] with Integer
implicit object BoolDbType extends DbType[Boolean] with Integer
implicit object StringDbType extends DbType[String] with Text {
def literal(v: String) = s"'$v'"
def bind(stmt: SQLiteProgram, pos: Int, v: String) = stmt.bindString(pos, v)
}
implicit object IntDbType extends DbType[Int] with Integer {
def literal(v: Int) = v.toString
def bind(stmt: SQLiteProgram, pos: Int, v: Int) = stmt.bindLong(pos, v)
}
implicit object LongDbType extends DbType[Long] with Integer {
def literal(v: Long) = v.toString
def bind(stmt: SQLiteProgram, pos: Int, v: Long) = stmt.bindLong(pos, v)
}
implicit object BoolDbType extends DbType[Boolean] with Integer {
def literal(v: Boolean) = if (v) "1" else "0"
def bind(stmt: SQLiteProgram, pos: Int, v: Boolean) = stmt.bindLong(pos, if (v) 1 else 0)
}
}

case class Index[A, B](name: String, ext: A => B)(implicit val dbType: DbType[B]) {

def createTableSql = s"$name ${dbType.sqlName}"
def createIndexSql(table: String) = s"CREATE INDEX IF NOT EXISTS idx_${table}_$name on $table($name) "
def dropIndexSql(table: String) = s"DROP INDEX IF EXISTS idx_${table}_$name"

def bind(stmt: SQLiteProgram, pos: Int, v: A) = dbType.bind(stmt, pos, ext(v))

def apply(item: A) = ext(item)
}

trait Matcher[A] {
val whereSql: String
def apply(item: A): Boolean
}

object Matcher {

def like[A](index: Index[A, String])(query: String): Matcher[A] = new Matcher[A] {
override val whereSql: String = s"${index.name} LIKE '%$query%'"
override def apply(item: A): Boolean = index(item).contains(query)
}

def equal[A, B](index: Index[A, B])(v: B): Matcher[A] = new Matcher[A] {
override val whereSql: String = s"${index.name} = ${index.dbType.literal(v)}"
override def apply(item: A): Boolean = index(item) == v
}
}

abstract class Dao[I: Id, A: JsonDecoder : JsonEncoder] {
Expand All @@ -50,16 +93,22 @@ abstract class Dao[I: Id, A: JsonDecoder : JsonEncoder] {
val table: Table
def getId(v: A): I

case class Index[B: DbType](name: String, ext: A => B)

case class Table(name: String, indexes: Seq[Index[_]])
case class Table(name: String, indexes: Seq[Index[A, _]])

lazy val createTableSql = s"CREATE TABLE IF NOT EXISTS ${table.name} (_id TEXT PRIMARY KEY, _data TEXT)" // TODO: support indexes
lazy val createTableSql = {
val indexesSql = if (table.indexes.isEmpty) "" else table.indexes.map(_.createTableSql).mkString(", ", ", ", "")
s"CREATE TABLE IF NOT EXISTS ${table.name} (_id TEXT PRIMARY KEY, _data TEXT $indexesSql)"
}
lazy val dropTableSql = s"DROP TABLE IF EXISTS ${table.name}"

lazy val deleteSql = s"DELETE FROM ${table.name} WHERE _id = ?"
lazy val insertOrReplaceSql = s"INSERT OR REPLACE INTO ${table.name} (_id, _data) VALUES (?, ?)" // TODO: indexes
lazy val insertOrIgnoreSql = s"INSERT OR IGNORE INTO ${table.name} (_id, _data) VALUES (?, ?)"

private lazy val insertSql = {
if (table.indexes.isEmpty) s"INTO ${table.name} (_id, _data) VALUES (?, ?)"
else s"INTO ${table.name} (_id, _data ${table.indexes.map(_.name).mkString(", ", ", ", "")}) VALUES (${Seq.fill(table.indexes.size + 2)("?").mkString(", ")})"
}
lazy val insertOrReplaceSql = s"INSERT OR REPLACE $insertSql"
lazy val insertOrIgnoreSql = s"INSERT OR IGNORE $insertSql"

protected[db] def decode(c: Cursor) = decoder(c.getString(DataIndex))

Expand All @@ -78,6 +127,18 @@ abstract class Dao[I: Id, A: JsonDecoder : JsonEncoder] {

def list(implicit db: SQLiteDatabase): Seq[A] = list(db.query(table.name, null, null, null, null, null, null))

def query[K](ind: Index[A, K], value: K)(implicit db: SQLiteDatabase): Cursor =
db.query(table.name, null, s"${ind.name} = ${ind.dbType.literal(value)}", null, null, null, null)

def find[K](whereSql: String)(implicit db: SQLiteDatabase): Seq[I] = {
val c = db.query(table.name, Array("_id"), whereSql, null, null, null, null)
try {
val builder = Seq.newBuilder[I]
while (c.moveToNext()) builder += _id.decode(c.getString(0))
builder.result()
} finally c.close()
}

def insert(item: A)(implicit db: SQLiteDatabase): A = {
insert(Seq(item))
item
Expand All @@ -103,7 +164,9 @@ abstract class Dao[I: Id, A: JsonDecoder : JsonEncoder] {
items foreach { item =>
stmt.bindString(1, _id.encode(getId(item)))
stmt.bindString(2, encoder(item))
// TODO: indexes
table.indexes.zipWithIndex foreach { case (index, i) =>
index.bind(stmt, i + 3, item)
}
stmt.execute()
}
}
Expand Down
21 changes: 18 additions & 3 deletions src/main/scala/com/geteit/db/DaoDB.scala
@@ -1,20 +1,35 @@
package com.geteit.db

import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
import com.geteit.util.Log._

trait DaoDB { self: SQLiteOpenHelper =>
private implicit val tag: LogTag = "DaoDB"

val daos: Seq[Dao[_, _]]
val migrations: Seq[Migration]

override def onCreate(db: SQLiteDatabase): Unit =
override def onCreate(db: SQLiteDatabase): Unit = {
verbose(s"onCreate()")
daos.foreach { dao =>
verbose(s"creating table: ${dao.createTableSql}")
db.execSQL(dao.createTableSql)
dao.table.indexes foreach { index =>
db.execSQL(index.createIndexSql(dao.table.name))
}
}
}

override def onUpgrade(db: SQLiteDatabase, from: Int, to: Int): Unit =
override def onUpgrade(db: SQLiteDatabase, from: Int, to: Int): Unit = {
verbose(s"onUpgrade($from -> $to)")
new Migrations(migrations: _*).migrate(this, from, to)(db)
}

def dropAllTables(db: SQLiteDatabase): Unit =
daos.foreach { dao => db.execSQL(dao.dropTableSql) }
daos.foreach { dao =>
db.execSQL(dao.dropTableSql)
dao.table.indexes foreach { index =>
db.execSQL(index.dropIndexSql(dao.table.name))
}
}
}

0 comments on commit 72c1351

Please sign in to comment.