refactor: optimisation around stoptimes

- moved stoptime related functionality into new core:data:stoptime module
  - will feature all the different realtime stoptime sources to be
      integrated later
- create proper database schema for future migrations
- deduplicate trips into stoppingpatterns, since many trips share the
  exact same stopping pattern
  - stoptimes are now linked to stoppingpatterns instead
  - stoppingpattern ids are generated from a hash composed of all stoptimes
- stoptimes now use deltas for arrival time to save space
This commit is contained in:
Cilly Leang 2026-05-05 03:23:11 +10:00
parent f1770744db
commit 102c028407
Signed by: cilly
GPG key ID: 6500251E087653C9
39 changed files with 396 additions and 223 deletions

View file

@ -47,6 +47,7 @@ sqldelight {
databases {
register("BanksiaDatabase") {
packageName.set("moe.lava.banksia.core.sqld")
schemaOutputDirectory.set(file("src/commonMain/sqldelight/schema"))
}
}
}

View file

@ -5,10 +5,10 @@ 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 class DatabaseManager : KoinComponent {
actual val database by lazy {
val ctx = get<Context>().applicationContext
val driver = AndroidSqliteDriver(BanksiaDatabase.Schema, ctx, "timetable.db")
val driver = AndroidSqliteDriver(BanksiaDatabase.Schema, ctx, "${DBNAME}.db")
BanksiaDatabase(driver)
}
}

View file

@ -1,7 +1,7 @@
package moe.lava.banksia.core.sqld
import org.koin.core.component.KoinComponent
internal const val DBNAME = "timetable"
expect class DatabaseManager() : KoinComponent {
expect class DatabaseManager() {
val database: BanksiaDatabase
}

View file

@ -11,6 +11,7 @@ val sqldDiModule = module {
factory { get<BanksiaDatabase>().serviceExceptionQueries }
factory { get<BanksiaDatabase>().shapeQueries }
factory { get<BanksiaDatabase>().stopQueries }
factory { get<BanksiaDatabase>().stoppingPatternQueries }
factory { get<BanksiaDatabase>().stopTimeQueries }
factory { get<BanksiaDatabase>().tripQueries }
}

View file

@ -3,23 +3,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.model.TimeType
import moe.lava.banksia.core.sqld.StopTime as DbStopTime
fun DbStopTime.asModel() = StopTime(
tripId = tripId,
patternId = patternId,
stopId = stopId,
arrivalTime = FutureTime.fromInt(arrivalTime.toInt()),
departureTime = FutureTime.fromInt(departureTime.toInt()),
headsign = null,
time = TimeType.Undated(
arrival = FutureTime.fromInt((departureTime + arrivalDelta).toInt()),
departure = FutureTime.fromInt(departureTime.toInt()),
),
pickupType = pickupType.toInt(),
dropOffType = dropOffType.toInt(),
)
fun StopTime.asDb() = DbStopTime(
tripId = tripId,
fun StopTime.Undated.asDb() = DbStopTime(
patternId = patternId,
stopId = stopId,
arrivalTime = arrivalTime.asInt().toLong(),
departureTime = departureTime.asInt().toLong(),
arrivalDelta = (time.arrival.asInt() - time.departure.asInt()).toLong(),
departureTime = time.departure.asInt().toLong(),
pickupType = pickupType.toLong(),
dropOffType = dropOffType.toLong(),
)

View file

@ -0,0 +1,22 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.StopTime
import moe.lava.banksia.core.model.StoppingPattern
import moe.lava.banksia.core.sqld.StoppingPattern as DbStoppingPattern
fun DbStoppingPattern.asModel(stoptimes: List<StopTime.Undated>) = StoppingPattern.Undated(
id = id,
routeId = routeId,
shapeId = shapeId,
headsign = headsign,
wheelchairAccessible = wheelchairAccessible == 1L,
stoptimes = stoptimes,
)
fun StoppingPattern.Undated.asDb() = DbStoppingPattern(
id = id,
routeId = routeId,
shapeId = shapeId,
headsign = headsign,
wheelchairAccessible = if (wheelchairAccessible) 1L else 0L,
)

View file

@ -1,32 +1,27 @@
package moe.lava.banksia.core.sqld.mappers
import moe.lava.banksia.core.model.Service
import moe.lava.banksia.core.model.StoppingPattern
import moe.lava.banksia.core.model.Trip
import moe.lava.banksia.core.sqld.Trip as DbTrip
fun DbTrip.asModel(service: Service): Trip {
fun DbTrip.asModel(pattern: StoppingPattern.Undated, service: Service): Trip.Undated {
if (serviceId != service.id) {
throw IllegalArgumentException("trip and service id mismatch (${serviceId} != ${service.id})")
}
return Trip(
id = id,
routeId = routeId,
id = gtfsId,
pattern = pattern,
service = service,
shapeId = shapeId,
tripHeadsign = tripHeadsign,
directionId = directionId,
blockId = blockId,
wheelchairAccessible = wheelchairAccessible == 1L
directionId = directionId.toInt(),
blockId = blockId.toString(),
)
}
fun Trip.asDb() = DbTrip(
id = id,
routeId = routeId,
fun Trip.Undated.asDb() = DbTrip(
gtfsId = id,
patternId = pattern.id,
serviceId = service.id,
shapeId = shapeId,
tripHeadsign = tripHeadsign,
directionId = directionId,
blockId = blockId,
wheelchairAccessible = if (wheelchairAccessible) 1L else 0L
directionId = directionId.toLong(),
blockId = blockId?.toLong(),
)

View file

@ -15,7 +15,7 @@ getAll:
SELECT * FROM Stop;
getAllParentless:
SELECT * FROM Stop WHERE platformCode IS NULL AND parent IS NULL;
SELECT * FROM Stop WHERE platformCode IS NOT NULL AND parent IS NULL;
get:
SELECT * FROM Stop WHERE id == ?;
@ -32,8 +32,8 @@ 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
INNER JOIN StoppingPattern ON StoppingPattern.id == StopTime.patternId
WHERE StoppingPattern.routeId == :id
GROUP BY Stop.id;
-- I vibecoded this, sorry
@ -41,8 +41,8 @@ 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
INNER JOIN StoppingPattern ON StoppingPattern.id == StopTime.patternId
WHERE StoppingPattern.routeId == :id
GROUP BY Stop.id
UNION ALL

View file

@ -1,11 +1,11 @@
CREATE TABLE StopTime (
tripId TEXT NOT NULL REFERENCES Trip (id),
patternId INTEGER NOT NULL REFERENCES StoppingPattern (id),
stopId TEXT NOT NULL REFERENCES Stop (id),
arrivalTime INTEGER NOT NULL,
arrivalDelta INTEGER NOT NULL,
departureTime INTEGER NOT NULL,
pickupType INTEGER NOT NULL,
dropOffType INTEGER NOT NULL,
PRIMARY KEY (tripId, stopId)
PRIMARY KEY (patternId, stopId)
) WITHOUT ROWID;
CREATE INDEX idx_StopTime_stopId ON StopTime (stopId);
@ -16,8 +16,9 @@ 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
INNER JOIN Trip ON Trip.serviceId == Service.id
INNER JOIN StoppingPattern ON StoppingPattern.id == Trip.patternId
WHERE StopTime.patternId == StoppingPattern.id
AND StopTime.stopId IN (SELECT Stop.id FROM Stop WHERE Stop.parent == :stopId OR Stop.id == :stopId)
AND ServiceException.type IS NULL;

View file

@ -0,0 +1,10 @@
CREATE TABLE StoppingPattern (
id INTEGER PRIMARY KEY NOT NULL,
routeId TEXT NOT NULL REFERENCES Route (id),
shapeId TEXT NOT NULL REFERENCES Shape (id),
headsign TEXT NOT NULL,
wheelchairAccessible INTEGER NOT NULL
);
insert:
INSERT OR REPLACE INTO StoppingPattern VALUES ?;

View file

@ -1,14 +1,12 @@
CREATE TABLE Trip (
id TEXT PRIMARY KEY NOT NULL,
routeId TEXT NOT NULL REFERENCES Route (id),
gtfsId TEXT PRIMARY KEY NOT NULL,
patternId INTEGER NOT NULL REFERENCES StoppingPattern (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
blockId INTEGER,
directionId INTEGER NOT NULL
);
CREATE INDEX idx_Trip_patternId ON Trip (patternId);
CREATE INDEX idx_Trip_serviceId ON Trip (serviceId);
insert:

Binary file not shown.

View file

@ -1,10 +1,11 @@
package moe.lava.banksia.core.sqld
import app.cash.sqldelight.driver.native.NativeSqliteDriver
import org.koin.core.component.KoinComponent
actual class DatabaseManager actual constructor() : org.koin.core.component.KoinComponent {
actual class DatabaseManager : KoinComponent {
actual val database by lazy {
val driver = NativeSqliteDriver(BanksiaDatabase.Schema, "timetable.db")
val driver = NativeSqliteDriver(BanksiaDatabase.Schema, "${DBNAME}.db")
BanksiaDatabase(driver)
}
}

View file

@ -11,9 +11,7 @@ import java.io.File
import java.util.Properties
import kotlin.system.exitProcess
private const val DBNAME = "timetable"
actual class DatabaseManager actual constructor() : KoinComponent {
actual class DatabaseManager : KoinComponent {
private var driver = connect()
actual val database get() = BanksiaDatabase(driver)