refactor(core): switch from room to sqldelight
sqldelight provides far more control over the sql and allows me to make more optimisations such as removing generated rowid etc. sql also just looks better than the annotation hell from room.
This commit is contained in:
parent
ff2af310fb
commit
f1770744db
74 changed files with 601 additions and 5037 deletions
52
core/sqld/build.gradle.kts
Normal file
52
core/sqld/build.gradle.kts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
alias(libs.plugins.androidMultiplatformLibrary)
|
||||
alias(libs.plugins.sqldelight)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android {
|
||||
namespace = "moe.lava.banksia.core.sqld"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
}
|
||||
}
|
||||
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
|
||||
jvm()
|
||||
|
||||
sourceSets {
|
||||
androidMain.dependencies {
|
||||
implementation(libs.sqldelight.driver.android)
|
||||
}
|
||||
commonMain.dependencies {
|
||||
implementation(libs.okio)
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
implementation(projects.core)
|
||||
}
|
||||
nativeMain.dependencies {
|
||||
implementation(libs.sqldelight.driver.native)
|
||||
}
|
||||
jvmMain.dependencies {
|
||||
implementation(libs.sqldelight.driver.jvm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sqldelight {
|
||||
databases {
|
||||
register("BanksiaDatabase") {
|
||||
packageName.set("moe.lava.banksia.core.sqld")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package moe.lava.banksia.core.sqld
|
||||
|
||||
import android.content.Context
|
||||
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
actual class DatabaseManager actual constructor() : KoinComponent {
|
||||
actual val database by lazy {
|
||||
val ctx = get<Context>().applicationContext
|
||||
val driver = AndroidSqliteDriver(BanksiaDatabase.Schema, ctx, "timetable.db")
|
||||
BanksiaDatabase(driver)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package moe.lava.banksia.core.sqld
|
||||
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
expect class DatabaseManager() : KoinComponent {
|
||||
val database: BanksiaDatabase
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package moe.lava.banksia.core.sqld
|
||||
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val sqldDiModule = module {
|
||||
singleOf(::DatabaseManager)
|
||||
factory { get<DatabaseManager>().database }
|
||||
factory { get<BanksiaDatabase>().routeQueries }
|
||||
factory { get<BanksiaDatabase>().serviceQueries }
|
||||
factory { get<BanksiaDatabase>().serviceExceptionQueries }
|
||||
factory { get<BanksiaDatabase>().shapeQueries }
|
||||
factory { get<BanksiaDatabase>().stopQueries }
|
||||
factory { get<BanksiaDatabase>().stopTimeQueries }
|
||||
factory { get<BanksiaDatabase>().tripQueries }
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package moe.lava.banksia.core.sqld.mappers
|
||||
|
||||
import moe.lava.banksia.core.model.Route
|
||||
import moe.lava.banksia.core.model.RouteType
|
||||
import moe.lava.banksia.core.sqld.Route as DbRoute
|
||||
|
||||
fun DbRoute.asModel() = Route(
|
||||
id = id,
|
||||
type = RouteType.from(type.toInt()),
|
||||
number = number,
|
||||
name = name,
|
||||
)
|
||||
|
||||
fun Route.asDb() = DbRoute(id, type.value.toLong(), number, name)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package moe.lava.banksia.core.sqld.mappers
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import moe.lava.banksia.core.model.Service
|
||||
import moe.lava.banksia.core.util.deserialiseDaysBitflag
|
||||
import moe.lava.banksia.core.util.serialise
|
||||
import moe.lava.banksia.core.sqld.Service as DbService
|
||||
|
||||
fun DbService.asModel() = Service(
|
||||
id = id,
|
||||
days = days.toInt().deserialiseDaysBitflag(),
|
||||
start = LocalDate.fromEpochDays(start),
|
||||
end = LocalDate.fromEpochDays(end),
|
||||
)
|
||||
|
||||
fun Service.asDb() = DbService(
|
||||
id = id,
|
||||
days = days.serialise().toLong(),
|
||||
start = start.toEpochDays(),
|
||||
end = end.toEpochDays(),
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package moe.lava.banksia.core.sqld.mappers
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import moe.lava.banksia.core.model.ServiceException
|
||||
import moe.lava.banksia.core.sqld.ServiceException as DbServiceException
|
||||
|
||||
fun DbServiceException.asModel() = ServiceException(
|
||||
serviceId = serviceId,
|
||||
date = LocalDate.fromEpochDays(date),
|
||||
type = type.toInt(),
|
||||
)
|
||||
|
||||
fun ServiceException.asDb() = DbServiceException(
|
||||
serviceId = serviceId,
|
||||
type = date.toEpochDays(),
|
||||
date = type.toLong(),
|
||||
)
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package moe.lava.banksia.core.sqld.mappers
|
||||
|
||||
import moe.lava.banksia.core.model.Shape
|
||||
import moe.lava.banksia.core.model.ShapePath
|
||||
import moe.lava.banksia.core.util.Point
|
||||
import moe.lava.banksia.core.sqld.Shape as DbShape
|
||||
|
||||
fun DbShape.asModel() = Shape(
|
||||
id = id,
|
||||
path = bytesToPath(path),
|
||||
)
|
||||
|
||||
fun Shape.asDb() = DbShape(
|
||||
id = id,
|
||||
path = bytesFromPath(path),
|
||||
)
|
||||
|
||||
private fun bytesToPath(value: ByteArray): ShapePath {
|
||||
return value
|
||||
.asSequence()
|
||||
.asIterable()
|
||||
.chunked(8) {
|
||||
(it[0].toLong() and 0xFF) or
|
||||
(it[1].toLong() and 0xFF shl 8) or
|
||||
(it[2].toLong() and 0xFF shl 16) or
|
||||
(it[3].toLong() and 0xFF shl 24) or
|
||||
(it[4].toLong() and 0xFF shl 32) or
|
||||
(it[5].toLong() and 0xFF shl 40) or
|
||||
(it[6].toLong() and 0xFF shl 48) or
|
||||
(it[7].toLong() and 0xFF shl 56)
|
||||
}
|
||||
.map { Double.fromBits(it) }
|
||||
.chunked(2)
|
||||
.map { (lat, lng) -> Point(lat, lng) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun bytesFromPath(path: ShapePath): ByteArray {
|
||||
return path
|
||||
.flatMap { (lat, lng) -> listOf(lat.toBits(), lng.toBits()) }
|
||||
.flatMap { i -> listOf(
|
||||
i.toByte(),
|
||||
(i shr 8).toByte(),
|
||||
(i shr 16).toByte(),
|
||||
(i shr 24).toByte(),
|
||||
(i shr 32).toByte(),
|
||||
(i shr 40).toByte(),
|
||||
(i shr 48).toByte(),
|
||||
(i shr 56).toByte(),
|
||||
) }
|
||||
.toByteArray()
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package moe.lava.banksia.core.sqld.mappers
|
||||
|
||||
import moe.lava.banksia.core.model.Stop
|
||||
import moe.lava.banksia.core.util.Point
|
||||
import moe.lava.banksia.core.sqld.Stop as DbStop
|
||||
|
||||
fun DbStop.asModel() = Stop(
|
||||
id = id,
|
||||
name = name,
|
||||
pos = Point(lat, lng),
|
||||
parent = parent,
|
||||
hasWheelChairBoarding = hasWheelChairBoarding == 1L,
|
||||
level = level,
|
||||
platformCode = platformCode,
|
||||
)
|
||||
|
||||
fun Stop.asDb() = DbStop(
|
||||
id = id,
|
||||
name = name,
|
||||
lat = pos.lat,
|
||||
lng = pos.lng,
|
||||
parent = parent,
|
||||
hasWheelChairBoarding = if (hasWheelChairBoarding) 1L else 0L,
|
||||
level = level,
|
||||
platformCode = platformCode
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package moe.lava.banksia.core.sqld.mappers
|
||||
|
||||
import moe.lava.banksia.core.model.FutureTime
|
||||
import moe.lava.banksia.core.model.FutureTime.Companion.asInt
|
||||
import moe.lava.banksia.core.model.StopTime
|
||||
import moe.lava.banksia.core.sqld.StopTime as DbStopTime
|
||||
|
||||
fun DbStopTime.asModel() = StopTime(
|
||||
tripId = tripId,
|
||||
stopId = stopId,
|
||||
arrivalTime = FutureTime.fromInt(arrivalTime.toInt()),
|
||||
departureTime = FutureTime.fromInt(departureTime.toInt()),
|
||||
headsign = null,
|
||||
pickupType = pickupType.toInt(),
|
||||
dropOffType = dropOffType.toInt(),
|
||||
)
|
||||
|
||||
fun StopTime.asDb() = DbStopTime(
|
||||
tripId = tripId,
|
||||
stopId = stopId,
|
||||
arrivalTime = arrivalTime.asInt().toLong(),
|
||||
departureTime = departureTime.asInt().toLong(),
|
||||
pickupType = pickupType.toLong(),
|
||||
dropOffType = dropOffType.toLong(),
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package moe.lava.banksia.core.sqld.mappers
|
||||
|
||||
import moe.lava.banksia.core.model.Service
|
||||
import moe.lava.banksia.core.model.Trip
|
||||
import moe.lava.banksia.core.sqld.Trip as DbTrip
|
||||
|
||||
fun DbTrip.asModel(service: Service): Trip {
|
||||
if (serviceId != service.id) {
|
||||
throw IllegalArgumentException("trip and service id mismatch (${serviceId} != ${service.id})")
|
||||
}
|
||||
return Trip(
|
||||
id = id,
|
||||
routeId = routeId,
|
||||
service = service,
|
||||
shapeId = shapeId,
|
||||
tripHeadsign = tripHeadsign,
|
||||
directionId = directionId,
|
||||
blockId = blockId,
|
||||
wheelchairAccessible = wheelchairAccessible == 1L
|
||||
)
|
||||
}
|
||||
|
||||
fun Trip.asDb() = DbTrip(
|
||||
id = id,
|
||||
routeId = routeId,
|
||||
serviceId = service.id,
|
||||
shapeId = shapeId,
|
||||
tripHeadsign = tripHeadsign,
|
||||
directionId = directionId,
|
||||
blockId = blockId,
|
||||
wheelchairAccessible = if (wheelchairAccessible) 1L else 0L
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
CREATE TABLE Route (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
number TEXT,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
getAll:
|
||||
SELECT * FROM Route;
|
||||
|
||||
get:
|
||||
SELECT * FROM Route WHERE id == ?;
|
||||
|
||||
insert:
|
||||
INSERT INTO Route VALUES ?;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
CREATE TABLE Service (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
days INTEGER NOT NULL,
|
||||
start INTEGER NOT NULL,
|
||||
end INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_Service_days ON Service (days);
|
||||
|
||||
insert:
|
||||
INSERT INTO Service VALUES ?;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
CREATE TABLE ServiceException (
|
||||
serviceId TEXT NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
date INTEGER NOT NULL,
|
||||
PRIMARY KEY (serviceId, type)
|
||||
);
|
||||
|
||||
insert:
|
||||
INSERT INTO ServiceException VALUES ?;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE Shape (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
path BLOB NOT NULL
|
||||
);
|
||||
|
||||
insert:
|
||||
INSERT INTO Shape VALUES ?;
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
CREATE TABLE Stop (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
lat REAL NOT NULL,
|
||||
lng REAL NOT NULL,
|
||||
parent TEXT REFERENCES Stop(id),
|
||||
hasWheelChairBoarding INTEGER NOT NULL,
|
||||
level TEXT,
|
||||
platformCode TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_Stop_parent ON Stop (parent);
|
||||
|
||||
getAll:
|
||||
SELECT * FROM Stop;
|
||||
|
||||
getAllParentless:
|
||||
SELECT * FROM Stop WHERE platformCode IS NULL AND parent IS NULL;
|
||||
|
||||
get:
|
||||
SELECT * FROM Stop WHERE id == ?;
|
||||
|
||||
getMany:
|
||||
SELECT * FROM Stop WHERE id IN ?;
|
||||
|
||||
insert:
|
||||
INSERT INTO Stop VALUES ?;
|
||||
|
||||
updateParents:
|
||||
UPDATE Stop SET parent = ? WHERE id IN ?;
|
||||
|
||||
getByRoute:
|
||||
SELECT Stop.* FROM Stop
|
||||
INNER JOIN StopTime ON StopTime.stopId == Stop.id
|
||||
INNER JOIN Trip ON Trip.id == StopTime.tripId
|
||||
WHERE Trip.routeId == :id
|
||||
GROUP BY Stop.id;
|
||||
|
||||
-- I vibecoded this, sorry
|
||||
getParentsByRoute:
|
||||
WITH RECURSIVE Tree AS (
|
||||
SELECT Stop.* FROM Stop
|
||||
INNER JOIN StopTime ON StopTime.stopId == Stop.id
|
||||
INNER JOIN Trip ON Trip.id == StopTime.tripId
|
||||
WHERE Trip.routeId == :id
|
||||
GROUP BY Stop.id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT s.*
|
||||
FROM Stop s
|
||||
INNER JOIN Tree t ON s.id = t.parent
|
||||
)
|
||||
SELECT DISTINCT * FROM Tree WHERE parent IS NULL;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
CREATE TABLE StopTime (
|
||||
tripId TEXT NOT NULL REFERENCES Trip (id),
|
||||
stopId TEXT NOT NULL REFERENCES Stop (id),
|
||||
arrivalTime INTEGER NOT NULL,
|
||||
departureTime INTEGER NOT NULL,
|
||||
pickupType INTEGER NOT NULL,
|
||||
dropOffType INTEGER NOT NULL,
|
||||
PRIMARY KEY (tripId, stopId)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE INDEX idx_StopTime_stopId ON StopTime (stopId);
|
||||
|
||||
insert:
|
||||
INSERT OR REPLACE INTO StopTime VALUES ?;
|
||||
|
||||
getForStopDated:
|
||||
SELECT DISTINCT StopTime.* FROM StopTime
|
||||
INNER JOIN Service ON Service.days & :days = :days AND :date BETWEEN Service.start AND Service.`end`
|
||||
INNER JOIN Trip ON Trip.serviceId == Service.id
|
||||
LEFT JOIN ServiceException ON ServiceException.serviceId == Service.id AND ServiceException.date == :date
|
||||
WHERE StopTime.tripId == Trip.id
|
||||
AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
|
||||
AND ServiceException.type IS NULL;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
CREATE TABLE Trip (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
routeId TEXT NOT NULL REFERENCES Route (id),
|
||||
serviceId TEXT NOT NULL REFERENCES Service (id),
|
||||
shapeId TEXT NOT NULL REFERENCES Shape (id),
|
||||
tripHeadsign TEXT NOT NULL,
|
||||
directionId TEXT NOT NULL,
|
||||
blockId TEXT,
|
||||
wheelchairAccessible INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_Trip_serviceId ON Trip (serviceId);
|
||||
|
||||
insert:
|
||||
INSERT OR REPLACE INTO Trip VALUES ?;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package moe.lava.banksia.core.sqld
|
||||
|
||||
import app.cash.sqldelight.driver.native.NativeSqliteDriver
|
||||
|
||||
actual class DatabaseManager actual constructor() : org.koin.core.component.KoinComponent {
|
||||
actual val database by lazy {
|
||||
val driver = NativeSqliteDriver(BanksiaDatabase.Schema, "timetable.db")
|
||||
BanksiaDatabase(driver)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package moe.lava.banksia.core.sqld
|
||||
|
||||
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.lava.banksia.core.util.error
|
||||
import org.koin.core.component.KoinComponent
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
private const val DBNAME = "timetable"
|
||||
|
||||
actual class DatabaseManager actual constructor() : KoinComponent {
|
||||
private var driver = connect()
|
||||
actual val database get() = BanksiaDatabase(driver)
|
||||
|
||||
private fun connect(path: String = "./data/${DBNAME}.db") =
|
||||
JdbcSqliteDriver("jdbc:sqlite:${path}", Properties(), BanksiaDatabase.Schema)
|
||||
.apply { execute(null, "PRAGMA journal_mode = OFF;", 0) }
|
||||
|
||||
fun makeAlt() = run {
|
||||
File("./data/${DBNAME}_alt.db").takeIf { it.exists() }?.delete()
|
||||
val driver = connect("./data/${DBNAME}_alt.db")
|
||||
BanksiaDatabase(driver) to { driver.close() }
|
||||
}
|
||||
|
||||
fun swap(scope: CoroutineScope = CoroutineScope(Dispatchers.IO)) {
|
||||
val live = File("./data/${DBNAME}.db")
|
||||
val alt = File("./data/${DBNAME}_alt.db")
|
||||
val old = File("./data/${DBNAME}_old.db")
|
||||
|
||||
if (live.takeIf { it.exists() }?.renameTo(old) == false) {
|
||||
error("DatabaseManager", "Failed to rename database from live to old (${live.absolutePath} -> ${old.absolutePath})")
|
||||
return
|
||||
}
|
||||
if (alt.takeIf { it.exists() }?.renameTo(live) == false) {
|
||||
error("DatabaseManager", "Failed to rename database from alt to live, trying to undo.. (${alt.absolutePath} -> ${live.absolutePath})")
|
||||
if (!live.renameTo(old)) {
|
||||
error("DatabaseManager", "Failed to undo, critical failure, exiting..")
|
||||
exitProcess(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
val oldDriver = driver
|
||||
driver = connect()
|
||||
|
||||
scope.launch {
|
||||
delay(5000)
|
||||
if (old.takeIf { it.exists() }?.delete() == false) {
|
||||
error("DatabaseManager", "Failed to unlink old database, stray files! (${old.absolutePath})")
|
||||
}
|
||||
oldDriver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue