Skip to content

Commit

Permalink
DB: Add NSW stops and product class tables (#627)
Browse files Browse the repository at this point in the history
### TL;DR
Added NSW stops database tables and related queries to support stop location functionality.

### What changed?
- Created new tables `NswStops` and `NswStopProductClass` to store stop information and their associated product classes
- Added SQL queries for inserting and selecting stops based on name and product class filters
- Implemented new Sandook interface methods for managing NSW stops data
- Added migration script for version 2 to create the new tables
- Added documentation for generating Kotlin files from SQL tables

### How to test?
1. Run `./gradlew generateCommonMainKrailSandookInterface` to generate updated Kotlin files
2. Verify the new tables are created by:
   - Inserting a stop using `insertNswStop`
   - Adding product classes with `insertNswStopProductClass`
   - Querying stops using the new select methods
3. Test different stop search scenarios:
   - Partial name matching
   - Filtering by included product classes
   - Filtering by excluded product classes

### Why make this change?
To support the storage and retrieval of NSW transport stop locations and their associated transport modes (product classes), enabling location-based features and stop searching functionality in the KRAIL app.
  • Loading branch information
ksharma-xyz authored Feb 27, 2025
1 parent a39051f commit a318e1d
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package xyz.ksharma.core.test.fakes

import xyz.ksharma.krail.sandook.NswStops
import xyz.ksharma.krail.sandook.Sandook
import xyz.ksharma.krail.sandook.SavedTrip
import xyz.ksharma.krail.sandook.SelectServiceAlertsByJourneyId
Expand All @@ -9,6 +10,8 @@ class FakeSandook : Sandook {
private var productClass: Long? = null
private val trips = mutableListOf<SavedTrip>()
private val alerts = mutableMapOf<String, List<SelectServiceAlertsByJourneyId>>()
private val stops = mutableListOf<NswStops>()
private val stopProductClasses = mutableMapOf<String, MutableList<Int>>()

override fun insertOrReplaceTheme(productClass: Long) {
this.productClass = productClass
Expand Down Expand Up @@ -72,4 +75,48 @@ class FakeSandook : Sandook {
override fun insertAlerts(journeyId: String, alerts: List<SelectServiceAlertsByJourneyId>) {
this.alerts[journeyId] = alerts
}

override fun insertNswStop(stopId: String, stopName: String, stopLat: Double, stopLon: Double) {
stops.add(NswStops(stopId, stopName, stopLat, stopLon))
}

override fun insertNswStopProductClass(stopId: String, productClass: Int) {
val productClasses = stopProductClasses.getOrPut(stopId) { mutableListOf() }
productClasses.add(productClass)
}

override fun selectStopsByPartialName(stopName: String): List<NswStops> {
return stops.filter { it.stopName.contains(stopName, ignoreCase = true) }
}

override fun selectStopsByNameAndProductClass(
stopName: String,
includeProductClassList: List<Int>,
): List<NswStops> {
return stops.filter { stop ->
stop.stopName.contains(stopName, ignoreCase = true) &&
stopProductClasses[stop.stopId]?.any { it in includeProductClassList } == true
}
}

override fun selectStopsByNameExcludingProductClass(
stopName: String,
excludeProductClassList: List<Int>,
): List<NswStops> {
return stops.filter { stop ->
stop.stopName.contains(stopName, ignoreCase = true) &&
stopProductClasses[stop.stopId]?.none { it in excludeProductClassList } == true
}
}

override fun selectStopsByNameExcludingProductClassOrExactId(
stopName: String,
excludeProductClassList: List<Int>,
): List<NswStops> {
return stops.filter { stop ->
(stop.stopName.contains(stopName, ignoreCase = true) ||
stop.stopId == stopName) &&
stopProductClasses[stop.stopId]?.none { it in excludeProductClassList } == true
}
}
}
8 changes: 5 additions & 3 deletions sandook/Migrations.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
## Sandook

Sandook is the db for KRAIL app.
Sandook is the db for KRAIL app.

## Migrations

Add migration in .sqm files in `migrations` folder.

### Android
### Android

- Update in SandookCallback

### iOS
### iOS

- Create a new class for after version migration for `SandookMigration`
- Update in `IosSandookDriverFactory` and add `SandookMigrationAfterX` to `getMigrationCallbacks()`
method.
7 changes: 7 additions & 0 deletions sandook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Sandook

### Generate Kotlin files for SQL tables

Run `./gradlew generateCommonMainKrailSandookInterface` to generate the
Kotlin files for the sql tables created in common module. The Kotlin files will be generated in the
following dir: `build/generated/sqldelight/code/KrailSandook/commonMain`
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ internal class RealSandook(factory: SandookDriverFactory) : Sandook {
private val sandook = KrailSandook(factory.createDriver())
private val query = sandook.krailSandookQueries

private val nswStopsQueries = sandook.nswStopsQueries

// region Theme
override fun insertOrReplaceTheme(productClass: Long) {
query.insertOrReplaceProductClass(productClass)
Expand Down Expand Up @@ -79,4 +81,66 @@ internal class RealSandook(factory: SandookDriverFactory) : Sandook {
}

// endregion

// region NswStops

override fun insertNswStop(
stopId: String,
stopName: String,
stopLat: Double,
stopLon: Double,
) {
nswStopsQueries.insertStop(
stopId = stopId,
stopName = stopName,
stopLat = stopLat,
stopLon = stopLon,
)
}

override fun insertNswStopProductClass(stopId: String, productClass: Int) {
nswStopsQueries.insertStopProductClass(stopId, productClass.toLong())
}

override fun selectStopsByPartialName(stopName: String): List<NswStops> {
return nswStopsQueries.selectStopsByPartialName(stopName).executeAsList()
}

override fun selectStopsByNameAndProductClass(
stopName: String,
includeProductClassList: List<Int>,
): List<NswStops> {
return nswStopsQueries.selectStopsByNameAndProductClass(
stopName,
includeProductClassList.map { it.toLong() }
).executeAsList()
}

override fun selectStopsByNameExcludingProductClass(
stopName: String,
excludeProductClassList: List<Int>
): List<NswStops> {
return nswStopsQueries.selectStopsByNameExcludingProductClass(
stopName,
excludeProductClassList.map { it.toLong() }
).executeAsList()
}

/**
* Combines exact stopId and partial [stopName] search logic while excluding stops
* based on the given list of product classes.
*/
override fun selectStopsByNameExcludingProductClassOrExactId(
stopName: String,
excludeProductClassList: List<Int>
): List<NswStops> {
val stopId = stopName
return nswStopsQueries.selectStopsByNameExcludingProductClassOrExactStopId(
stopId,
stopName,
excludeProductClassList.map { it.toLong() }
).executeAsList()
}

// endregion NswStops
}
41 changes: 41 additions & 0 deletions sandook/src/commonMain/kotlin/xyz/ksharma/krail/sandook/Sandook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,45 @@ interface Sandook {
fun insertAlerts(journeyId: String, alerts: List<SelectServiceAlertsByJourneyId>)

// endregion

// region NswStops
fun insertNswStop(stopId: String, stopName: String, stopLat: Double, stopLon: Double)

fun insertNswStopProductClass(stopId: String, productClass: Int)

fun selectStopsByPartialName(stopName: String): List<NswStops>

/**
* Select stops by name and product class. This is useful for selecting stops that are of a certain product class.
* Use with care, because it may also include those stops which are of multiple product classes,
* that are not included in the include list.
*/
fun selectStopsByNameAndProductClass(
stopName: String,
includeProductClassList: List<Int>,
): List<NswStops>

/**
* Select stops by name excluding product classes. This is useful for selecting stops that are
* not of a certain product class.
*/
fun selectStopsByNameExcludingProductClass(
stopName: String,
excludeProductClassList: List<Int> = emptyList(),
): List<NswStops>

/**
* Retrieves stops by matching an exact stop \id\ or partially matching a stop \name\.
* Excludes stops having product classes in the given \excludeProductClassList\.
* \param stopId Exact stop \id\ to match.
* \param stopName Partial stop \name\ to match.
* \param excludeProductClassList Product class IDs to exclude.
* \return List of matching NswStops.
*/
fun selectStopsByNameExcludingProductClassOrExactId(
stopName: String,
excludeProductClassList: List<Int> = emptyList(),
): List<NswStops>

// endregion
}
15 changes: 15 additions & 0 deletions sandook/src/commonMain/sqldelight/migrations/2.sqm
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- Create NSW Stops Table --
CREATE TABLE IF NOT EXISTS NswStops (
stopId TEXT PRIMARY KEY,
stopName TEXT NOT NULL,
stopLat REAL NOT NULL,
stopLon REAL NOT NULL
);

-- Create NSW Stop Product Class Table --
CREATE TABLE IF NOT EXISTS NswStopProductClass (
id INTEGER PRIMARY KEY AUTOINCREMENT,
stopId TEXT NOT NULL,
productClass INTEGER NOT NULL,
FOREIGN KEY (stopId) REFERENCES NswStops(stopId)
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
-- Create NSW Stops Table --
CREATE TABLE IF NOT EXISTS NswStops (
stopId TEXT PRIMARY KEY,
stopName TEXT NOT NULL,
stopLat REAL NOT NULL,
stopLon REAL NOT NULL
);

-- Create NSW Stop Product Class Table --
-- Foregin key ensures that any stopId value inserted into NswStopProductClass must exist in
-- NswStops, helping maintain data integrity between the two tables.
CREATE TABLE IF NOT EXISTS NswStopProductClass (
id INTEGER PRIMARY KEY AUTOINCREMENT,
stopId TEXT NOT NULL,
productClass INTEGER NOT NULL,
FOREIGN KEY (stopId) REFERENCES NswStops(stopId)
);

-- Insert data into NswStops table (without productClass) --
insertStop:
INSERT OR IGNORE INTO NswStops(stopId, stopName, stopLat, stopLon)
VALUES (?, ?, ?, ?);

-- Insert each productClass value for a given stop into NswStopProductClass --
insertStopProductClass:
INSERT INTO NswStopProductClass(stopId, productClass)
VALUES (?, ?);

-- Select stops with partial match on stopName --
selectStopsByPartialName:
SELECT * FROM NswStops
WHERE stopName LIKE '%' || ? || '%';

-- Select stops with partial match on stopName and specific productClass values --
selectStopsByNameAndProductClass:
SELECT DISTINCT s.*
FROM NswStops AS s
JOIN NswStopProductClass AS p ON s.stopId = p.stopId
WHERE s.stopName LIKE '%' || ? || '%'
AND p.productClass IN ?;

selectStopsByNameExcludingProductClass:
SELECT DISTINCT s.*
FROM NswStops AS s
WHERE s.stopName LIKE '%' || ? || '%'
AND s.stopId NOT IN (
SELECT p.stopId
FROM NswStopProductClass AS p
WHERE p.productClass IN ?
);

selectStopsByNameExcludingProductClassOrExactStopId:
SELECT DISTINCT s.*
FROM NswStops AS s
WHERE (
-- Exact match scenario: returns a stop if its stopId matches the given parameter
s.stopId = ?
-- Partial match scenario: returns stops whose stopName contains the given parameter
OR s.stopName LIKE '%' || ? || '%')
AND s.stopId NOT IN (
-- Exclusion scenario: filters out any stopIds linked to product classes in the specified list
SELECT p.stopId
FROM NswStopProductClass AS p
WHERE p.productClass IN ?
);
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import app.cash.sqldelight.db.AfterVersion
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.native.NativeSqliteDriver
import xyz.ksharma.krail.sandook.migrations.SandookMigrationAfter1
import xyz.ksharma.krail.sandook.migrations.SandookMigrationAfter2

class IosSandookDriverFactory : SandookDriverFactory {
override fun createDriver(): SqlDriver {
Expand All @@ -16,5 +17,6 @@ class IosSandookDriverFactory : SandookDriverFactory {

private fun getMigrationCallbacks(): Array<AfterVersion> = arrayOf(
AfterVersion(1) { SandookMigrationAfter1.migrate(it) },
AfterVersion(2) { SandookMigrationAfter2.migrate(it) },
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package xyz.ksharma.krail.sandook.migrations

import app.cash.sqldelight.db.SqlDriver
import xyz.ksharma.krail.core.log.log

internal object SandookMigrationAfter2 : SandookMigration {

override fun migrate(sqlDriver: SqlDriver) {
log("Upgrading database from version 2 to 3")
sqlDriver.execute(
identifier = null,
sql = """
CREATE TABLE IF NOT EXISTS NswStops (
stopId TEXT PRIMARY KEY,
stopName TEXT NOT NULL,
stopLat REAL NOT NULL,
stopLon REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS NswStopProductClass (
id INTEGER PRIMARY KEY AUTOINCREMENT,
stopId TEXT NOT NULL,
productClass INTEGER NOT NULL,
FOREIGN KEY (stopId) REFERENCES NswStops(stopId)
);
""".trimIndent(),
parameters = 0,
)
}
}

0 comments on commit a318e1d

Please sign in to comment.