From 9107b880192b31ffc78409e37615199ca0a0686c Mon Sep 17 00:00:00 2001 From: Igor Bubelov Date: Sat, 18 May 2024 17:38:35 +0700 Subject: [PATCH] Protect db connection by mutex --- app/src/main/kotlin/app/AppModule.kt | 4 +- app/src/main/kotlin/area/AreaQueries.kt | 157 +++--- app/src/main/kotlin/area/AreasRepo.kt | 2 +- app/src/main/kotlin/conf/ConfQueries.kt | 100 ++-- app/src/main/kotlin/db/Database.kt | 67 ++- app/src/main/kotlin/element/ElementQueries.kt | 505 +++++++++--------- app/src/main/kotlin/element/ElementsRepo.kt | 2 +- app/src/main/kotlin/event/EventQueries.kt | 270 +++++----- app/src/main/kotlin/event/EventsRepo.kt | 2 +- app/src/main/kotlin/reports/ReportQueries.kt | 188 +++---- app/src/main/kotlin/reports/ReportsRepo.kt | 2 +- app/src/main/kotlin/user/UserQueries.kt | 176 +++--- app/src/main/kotlin/user/UsersRepo.kt | 2 +- 13 files changed, 756 insertions(+), 721 deletions(-) diff --git a/app/src/main/kotlin/app/AppModule.kt b/app/src/main/kotlin/app/AppModule.kt index 6a164f99..71c48ffc 100644 --- a/app/src/main/kotlin/app/AppModule.kt +++ b/app/src/main/kotlin/app/AppModule.kt @@ -9,7 +9,7 @@ import area.AreasModel import area.AreasRepo import conf.ConfQueries import conf.ConfRepo -import db.openDbConnection +import db.Database import delivery.DeliveryModel import element.ElementQueries import element.ElementsRepo @@ -36,7 +36,7 @@ import user.UsersModel import user.UsersRepo val appModule = module { - single { openDbConnection(get()) } + single { Database(get()) } single { ApiImpl() }.bind(Api::class) diff --git a/app/src/main/kotlin/area/AreaQueries.kt b/app/src/main/kotlin/area/AreaQueries.kt index ba54eb3a..3da9f77b 100644 --- a/app/src/main/kotlin/area/AreaQueries.kt +++ b/app/src/main/kotlin/area/AreaQueries.kt @@ -1,13 +1,12 @@ package area -import androidx.sqlite.SQLiteConnection -import androidx.sqlite.execSQL import androidx.sqlite.use +import db.Database import db.getJsonObject import db.getZonedDateTime import java.time.ZonedDateTime -class AreaQueries(private val conn: SQLiteConnection) { +class AreaQueries(private val db: Database) { companion object { const val CREATE_TABLE = """ @@ -20,113 +19,119 @@ class AreaQueries(private val conn: SQLiteConnection) { } fun insertOrReplace(areas: List) { - conn.execSQL("BEGIN IMMEDIATE TRANSACTION") - - try { - areas.forEach { insertOrReplace(it) } - conn.execSQL("END TRANSACTION") - } catch (t: Throwable) { - conn.execSQL("ROLLBACK TRANSACTION") - } - } - - fun insertOrReplace(area: Area) { - conn.prepare("INSERT OR REPLACE INTO area(id, tags, updated_at) VALUES(?1, ?2, ?3)").use { - it.bindLong(1, area.id) - it.bindText(2, area.tags.toString()) - it.bindText(3, area.updatedAt.toString()) - it.step() + db.transaction { conn -> + areas.forEach { area -> + conn.prepare("INSERT OR REPLACE INTO area(id, tags, updated_at) VALUES(?1, ?2, ?3)") + .use { + it.bindLong(1, area.id) + it.bindText(2, area.tags.toString()) + it.bindText(3, area.updatedAt.toString()) + it.step() + } + } } } fun selectById(id: Long): Area? { - return conn.prepare("SELECT id, tags, updated_at FROM area WHERE id = ?1").use { - it.bindLong(1, id) + return db.withConn { conn -> + conn.prepare("SELECT id, tags, updated_at FROM area WHERE id = ?1").use { + it.bindLong(1, id) - if (it.step()) { - Area( - id = it.getLong(0), - tags = it.getJsonObject(1), - updatedAt = it.getZonedDateTime(2), - ) - } else { - null + if (it.step()) { + Area( + id = it.getLong(0), + tags = it.getJsonObject(1), + updatedAt = it.getZonedDateTime(2), + ) + } else { + null + } } } } fun selectByType(type: String): List { - return conn.prepare( - """ - SELECT id, tags, updated_at - FROM area - WHERE json_extract(tags, '$.type') = ?1 - """ - ).use { - it.bindText(1, type) + return db.withConn { conn -> + conn.prepare( + """ + SELECT id, tags, updated_at + FROM area + WHERE json_extract(tags, '$.type') = ?1 + """ + ).use { + it.bindText(1, type) - buildList { - while (it.step()) { - add( - Area( - id = it.getLong(0), - tags = it.getJsonObject(1), - updatedAt = it.getZonedDateTime(2), + buildList { + while (it.step()) { + add( + Area( + id = it.getLong(0), + tags = it.getJsonObject(1), + updatedAt = it.getZonedDateTime(2), + ) ) - ) + } } } } } fun selectMaxUpdatedAt(): ZonedDateTime? { - return conn.prepare("SELECT max(updated_at) FROM area").use { - if (it.step()) { - it.getZonedDateTime(0) - } else { - null + return db.withConn { conn -> + conn.prepare("SELECT max(updated_at) FROM area").use { + if (it.step()) { + it.getZonedDateTime(0) + } else { + null + } } } } fun selectMeetups(): List { - return conn.prepare( - """ - SELECT - json_extract(tags, '$.meetup_lat') AS lat, - json_extract(tags, '$.meetup_lon') AS lon, - id - FROM area - WHERE - lat IS NOT NULL - AND lon IS NOT NULL - """ - ).use { - buildList { - while (it.step()) { - add( - Meetup( - lat = it.getDouble(0), - lon = it.getDouble(1), - areaId = it.getLong(2), + return db.withConn { conn -> + conn.prepare( + """ + SELECT + json_extract(tags, '$.meetup_lat') AS lat, + json_extract(tags, '$.meetup_lon') AS lon, + id + FROM area + WHERE + lat IS NOT NULL + AND lon IS NOT NULL + """ + ).use { + buildList { + while (it.step()) { + add( + Meetup( + lat = it.getDouble(0), + lon = it.getDouble(1), + areaId = it.getLong(2), + ) ) - ) + } } } } } fun selectCount(): Long { - return conn.prepare("SELECT count(*) FROM area").use { - it.step() - it.getLong(0) + return db.withConn { conn -> + conn.prepare("SELECT count(*) FROM area").use { + it.step() + it.getLong(0) + } } } fun deleteById(id: Long) { - conn.prepare("DELETE FROM area WHERE id = ?1").use { - it.bindLong(1, id) - it.step() + db.withConn { conn -> + conn.prepare("DELETE FROM area WHERE id = ?1").use { + it.bindLong(1, id) + it.step() + } } } } \ No newline at end of file diff --git a/app/src/main/kotlin/area/AreasRepo.kt b/app/src/main/kotlin/area/AreasRepo.kt index bec83ad3..d92f9f68 100644 --- a/app/src/main/kotlin/area/AreasRepo.kt +++ b/app/src/main/kotlin/area/AreasRepo.kt @@ -84,7 +84,7 @@ class AreasRepo( updatedItems++ } - queries.insertOrReplace(it.toArea()) + queries.insertOrReplace(listOf(it.toArea())) } else { if (cached == null) { // Already evicted from cache, nothing to do here diff --git a/app/src/main/kotlin/conf/ConfQueries.kt b/app/src/main/kotlin/conf/ConfQueries.kt index 137f1b75..f7238299 100644 --- a/app/src/main/kotlin/conf/ConfQueries.kt +++ b/app/src/main/kotlin/conf/ConfQueries.kt @@ -1,11 +1,11 @@ package conf -import androidx.sqlite.SQLiteConnection import androidx.sqlite.execSQL import androidx.sqlite.use +import db.Database import db.getZonedDateTime -class ConfQueries(private val conn: SQLiteConnection) { +class ConfQueries(private val db: Database) { companion object { const val CREATE_TABLE = """ @@ -24,27 +24,25 @@ class ConfQueries(private val conn: SQLiteConnection) { } fun insertOrReplace(conf: Conf) { - conn.execSQL("BEGIN IMMEDIATE TRANSACTION") - - try { + db.transaction { conn -> conn.execSQL("DELETE FROM conf") conn.prepare( + """ + INSERT + INTO conf ( + last_sync_date, + viewport_north_lat, + viewport_east_lon, + viewport_south_lat, + viewport_west_lon, + show_atms, + show_osm_attribution, + show_sync_summary, + show_all_new_elements + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9); """ - INSERT - INTO conf ( - last_sync_date, - viewport_north_lat, - viewport_east_lon, - viewport_south_lat, - viewport_west_lon, - show_atms, - show_osm_attribution, - show_sync_summary, - show_all_new_elements - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) - """ ).use { it.bindText(1, conf.lastSyncDate?.toString() ?: "") it.bindDouble(2, conf.viewportNorthLat) @@ -56,43 +54,41 @@ class ConfQueries(private val conn: SQLiteConnection) { it.bindLong(8, if (conf.showSyncSummary) 1 else 0) it.bindLong(9, if (conf.showAllNewElements) 1 else 0) } - - conn.execSQL("END TRANSACTION") - } catch (t: Throwable) { - conn.execSQL("ROLLBACK TRANSACTION") } } fun select(): Conf? { - return conn.prepare( - """ - SELECT - last_sync_date, - viewport_north_lat, - viewport_east_lon, - viewport_south_lat, - viewport_west_lon, - show_atms, - show_osm_attribution, - show_sync_summary, - show_all_new_elements - FROM conf - """ - ).use { - if (it.step()) { - Conf( - lastSyncDate = it.getZonedDateTime(0), - viewportNorthLat = it.getDouble(1), - viewportEastLon = it.getDouble(2), - viewportSouthLat = it.getDouble(3), - viewportWestLon = it.getDouble(4), - showAtms = it.getBoolean(5), - showOsmAttribution = it.getBoolean(6), - showSyncSummary = it.getBoolean(7), - showAllNewElements = it.getBoolean(8), - ) - } else { - null + return db.withConn { conn -> + conn.prepare( + """ + SELECT + last_sync_date, + viewport_north_lat, + viewport_east_lon, + viewport_south_lat, + viewport_west_lon, + show_atms, + show_osm_attribution, + show_sync_summary, + show_all_new_elements + FROM conf + """ + ).use { + if (it.step()) { + Conf( + lastSyncDate = it.getZonedDateTime(0), + viewportNorthLat = it.getDouble(1), + viewportEastLon = it.getDouble(2), + viewportSouthLat = it.getDouble(3), + viewportWestLon = it.getDouble(4), + showAtms = it.getBoolean(5), + showOsmAttribution = it.getBoolean(6), + showSyncSummary = it.getBoolean(7), + showAllNewElements = it.getBoolean(8), + ) + } else { + null + } } } } diff --git a/app/src/main/kotlin/db/Database.kt b/app/src/main/kotlin/db/Database.kt index 139532c4..a36c3a6a 100644 --- a/app/src/main/kotlin/db/Database.kt +++ b/app/src/main/kotlin/db/Database.kt @@ -13,33 +13,58 @@ import kotlinx.coroutines.flow.MutableStateFlow import reports.ReportQueries import user.UserQueries import java.time.LocalDateTime +import java.util.concurrent.locks.ReentrantLock -val elementsUpdatedAt = MutableStateFlow(LocalDateTime.now()) +class Database(context: Context) { -private const val DB_FILE_NAME = "btcmap-2024-05-15.db" + private val conn: SQLiteConnection = + BundledSQLiteDriver().open(context.getDatabasePath("btcmap-2024-05-15.db").absolutePath) + .apply { + execSQL("PRAGMA journal_mode=WAL") + execSQL("PRAGMA synchronous=NORMAL") -fun openDbConnection(context: Context): SQLiteConnection { - return BundledSQLiteDriver().open(context.getDatabasePath(DB_FILE_NAME).absolutePath) - .apply { - execSQL("PRAGMA journal_mode=WAL") - execSQL("PRAGMA synchronous=NORMAL") + val version = prepare("SELECT user_version FROM pragma_user_version").use { + if (it.step()) { + it.getInt(0) + } else { + 0 + } + } - val version = prepare("SELECT user_version FROM pragma_user_version").use { - if (it.step()) { - it.getInt(0) - } else { - 0 + if (version == 0) { + execSQL(ElementQueries.CREATE_TABLE) + execSQL(EventQueries.CREATE_TABLE) + execSQL(ReportQueries.CREATE_TABLE) + execSQL(UserQueries.CREATE_TABLE) + execSQL(AreaQueries.CREATE_TABLE) + execSQL(ConfQueries.CREATE_TABLE) + execSQL("PRAGMA user_version=1") } } - if (version == 0) { - execSQL(ElementQueries.CREATE_TABLE) - execSQL(EventQueries.CREATE_TABLE) - execSQL(ReportQueries.CREATE_TABLE) - execSQL(UserQueries.CREATE_TABLE) - execSQL(AreaQueries.CREATE_TABLE) - execSQL(ConfQueries.CREATE_TABLE) - execSQL("PRAGMA user_version=1") + private val connLock = ReentrantLock() + + fun withConn(action: (conn: SQLiteConnection) -> T): T { + connLock.lock() + return try { + action(conn) + } finally { + connLock.unlock() + } + } + + fun transaction(action: (conn: SQLiteConnection) -> T) { + return withConn { conn -> + conn.execSQL("BEGIN TRANSACTION") + + try { + action(conn) + conn.execSQL("END TRANSACTION") + } catch (t: Throwable) { + conn.execSQL("ROLLBACK TRANSACTION") } } -} \ No newline at end of file + } +} + +val elementsUpdatedAt = MutableStateFlow(LocalDateTime.now()) \ No newline at end of file diff --git a/app/src/main/kotlin/element/ElementQueries.kt b/app/src/main/kotlin/element/ElementQueries.kt index 37e9bdfb..7b9b798f 100644 --- a/app/src/main/kotlin/element/ElementQueries.kt +++ b/app/src/main/kotlin/element/ElementQueries.kt @@ -1,9 +1,7 @@ package element -import android.util.Log -import androidx.sqlite.SQLiteConnection -import androidx.sqlite.execSQL import androidx.sqlite.use +import db.Database import db.getJsonArray import db.getJsonObject import db.getText @@ -11,7 +9,7 @@ import db.getZonedDateTime import db.getZonedDateTimeOrNull import java.time.ZonedDateTime -class ElementQueries(private val conn: SQLiteConnection) { +class ElementQueries(private val db: Database) { companion object { const val CREATE_TABLE = """ @@ -27,169 +25,166 @@ class ElementQueries(private val conn: SQLiteConnection) { } fun insertOrReplace(elements: List) { - conn.execSQL("BEGIN IMMEDIATE TRANSACTION") - - try { - elements.forEach { insertOrReplace(it) } - conn.execSQL("END TRANSACTION") - } catch (t: Throwable) { - conn.execSQL("ROLLBACK TRANSACTION") - } - } - - fun insertOrReplace(element: Element) { - conn.prepare( - """ - INSERT OR REPLACE - INTO element ( - id, - overpass_data, - tags, - updated_at, - ext_lat, - ext_lon - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6) - """ - ).use { - it.bindLong(1, element.id) - it.bindText(2, element.overpassData.toString()) - it.bindText(3, element.tags.toString()) - it.bindText(4, element.updatedAt) - it.bindDouble(5, element.lat) - it.bindDouble(6, element.lon) - it.step() + db.transaction { conn -> + elements.forEach { element -> + conn.prepare( + """ + INSERT OR REPLACE + INTO element ( + id, + overpass_data, + tags, + updated_at, + ext_lat, + ext_lon + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6) + """ + ).use { + it.bindLong(1, element.id) + it.bindText(2, element.overpassData.toString()) + it.bindText(3, element.tags.toString()) + it.bindText(4, element.updatedAt) + it.bindDouble(5, element.lat) + it.bindDouble(6, element.lon) + it.step() + } + } } } fun selectById(id: Long): Element? { - Log.d("sync", "selectById") - return conn.prepare( - """ - SELECT - id, - overpass_data, - tags, - updated_at, - ext_lat, - ext_lon - FROM element - WHERE id = ?1 - """ - ).use { - it.bindLong(1, id) + return db.withConn { conn -> + conn.prepare( + """ + SELECT + id, + overpass_data, + tags, + updated_at, + ext_lat, + ext_lon + FROM element + WHERE id = ?1 + """ + ).use { + it.bindLong(1, id) - if (it.step()) { - Element( - id = it.getLong(0), - overpassData = it.getJsonObject(1), - tags = it.getJsonObject(2), - updatedAt = it.getText(3), - lat = it.getDouble(4), - lon = it.getDouble(5), - ) - } else { - null + if (it.step()) { + Element( + id = it.getLong(0), + overpassData = it.getJsonObject(1), + tags = it.getJsonObject(2), + updatedAt = it.getText(3), + lat = it.getDouble(4), + lon = it.getDouble(5), + ) + } else { + null + } } } } fun selectBySearchString(searchString: String): List { - Log.d("sync", "selectBySearchString") - return conn.prepare( - """ - SELECT - id, - overpass_data, - tags, - updated_at, - ext_lat, - ext_lon - FROM element - WHERE UPPER(overpass_data) LIKE '%' || UPPER(?1) || '%' - """ - ).use { - it.bindText(1, searchString) + return db.withConn { conn -> + conn.prepare( + """ + SELECT + id, + overpass_data, + tags, + updated_at, + ext_lat, + ext_lon + FROM element + WHERE UPPER(overpass_data) LIKE '%' || UPPER(?1) || '%' + """ + ).use { + it.bindText(1, searchString) - buildList { - while (it.step()) { - add( - Element( - id = it.getLong(0), - overpassData = it.getJsonObject(1), - tags = it.getJsonObject(2), - updatedAt = it.getText(3), - lat = it.getDouble(4), - lon = it.getDouble(5), + buildList { + while (it.step()) { + add( + Element( + id = it.getLong(0), + overpassData = it.getJsonObject(1), + tags = it.getJsonObject(2), + updatedAt = it.getText(3), + lat = it.getDouble(4), + lon = it.getDouble(5), + ) ) - ) + } } } } } fun selectByOsmTagValue(tagName: String, tagValue: String): List { - Log.d("sync", "selectByOsmTagValue") - return conn.prepare( - """ - SELECT - id, - overpass_data, - tags, - updated_at, - ext_lat, - ext_lon - FROM element - WHERE json_extract(overpass_data, '$.tags.$tagName') = ?1 - """ - ).use { - it.bindText(1, tagValue) + return db.withConn { conn -> + conn.prepare( + """ + SELECT + id, + overpass_data, + tags, + updated_at, + ext_lat, + ext_lon + FROM element + WHERE json_extract(overpass_data, '$.tags.$tagName') = ?1 + """ + ).use { + it.bindText(1, tagValue) - buildList { - while (it.step()) { - add( - Element( - id = it.getLong(0), - overpassData = it.getJsonObject(1), - tags = it.getJsonObject(2), - updatedAt = it.getText(3), - lat = it.getDouble(4), - lon = it.getDouble(5), + buildList { + while (it.step()) { + add( + Element( + id = it.getLong(0), + overpassData = it.getJsonObject(1), + tags = it.getJsonObject(2), + updatedAt = it.getText(3), + lat = it.getDouble(4), + lon = it.getDouble(5), + ) ) - ) + } } } } } fun selectByBtcMapTagValue(tagName: String, tagValue: String): List { - Log.d("sync", "selectByBtcMapTagValue") - return conn.prepare( - """ - SELECT - id, - overpass_data, - tags, - updated_at, - ext_lat, - ext_lon - FROM element - WHERE json_extract(tags, '$.$tagName') = ?1 - """ - ).use { - it.bindText(1, tagValue) + return db.withConn { conn -> + conn.prepare( + """ + SELECT + id, + overpass_data, + tags, + updated_at, + ext_lat, + ext_lon + FROM element + WHERE json_extract(tags, '$.$tagName') = ?1 + """ + ).use { + it.bindText(1, tagValue) - buildList { - while (it.step()) { - add( - Element( - id = it.getLong(0), - overpassData = it.getJsonObject(1), - tags = it.getJsonObject(2), - updatedAt = it.getText(3), - lat = it.getDouble(4), - lon = it.getDouble(5), + buildList { + while (it.step()) { + add( + Element( + id = it.getLong(0), + overpassData = it.getJsonObject(1), + tags = it.getJsonObject(2), + updatedAt = it.getText(3), + lat = it.getDouble(4), + lon = it.getDouble(5), + ) ) - ) + } } } } @@ -202,44 +197,45 @@ class ElementQueries(private val conn: SQLiteConnection) { maxLon: Double, excludedCategories: List, ): List { - Log.d("sync", "selectWithoutClustering") - return conn.prepare( - """ - SELECT - id, - ext_lat, - ext_lon, - json_extract(tags, '$.icon:android') AS icon_id, - json_extract(tags, '$.boost:expires') AS boost_expires, - json_extract(overpass_data, '$.tags.payment:lightning:requires_companion_app') AS requires_companion_app - FROM element - WHERE - json_extract(tags, '$.category') NOT IN (${excludedCategories.joinToString { "'$it'" }}) - AND ext_lat > ?1 - AND ext_lat < ?2 - AND ext_lon > ?3 - AND ext_lon < ?4 - ORDER BY ext_lat DESC - """ - ).use { - it.bindDouble(1, minLat) - it.bindDouble(2, maxLat) - it.bindDouble(3, minLon) - it.bindDouble(4, maxLon) + return db.withConn { conn -> + conn.prepare( + """ + SELECT + id, + ext_lat, + ext_lon, + json_extract(tags, '$.icon:android') AS icon_id, + json_extract(tags, '$.boost:expires') AS boost_expires, + json_extract(overpass_data, '$.tags.payment:lightning:requires_companion_app') AS requires_companion_app + FROM element + WHERE + json_extract(tags, '$.category') NOT IN (${excludedCategories.joinToString { "'$it'" }}) + AND ext_lat > ?1 + AND ext_lat < ?2 + AND ext_lon > ?3 + AND ext_lon < ?4 + ORDER BY ext_lat DESC + """ + ).use { + it.bindDouble(1, minLat) + it.bindDouble(2, maxLat) + it.bindDouble(3, minLon) + it.bindDouble(4, maxLon) - buildList { - while (it.step()) { - add( - ElementsCluster( - count = 1, - id = it.getLong(0), - lat = it.getDouble(1), - lon = it.getDouble(2), - iconId = it.getText(3), - boostExpires = it.getZonedDateTimeOrNull(4), - requiresCompanionApp = it.getText(5, defaultValue = "no") == "yes", + buildList { + while (it.step()) { + add( + ElementsCluster( + count = 1, + id = it.getLong(0), + lat = it.getDouble(1), + lon = it.getDouble(2), + iconId = it.getText(3), + boostExpires = it.getZonedDateTimeOrNull(4), + requiresCompanionApp = it.getText(5, defaultValue = "no") == "yes", + ) ) - ) + } } } } @@ -249,38 +245,39 @@ class ElementQueries(private val conn: SQLiteConnection) { step: Double, excludedCategories: List, ): List { - Log.d("sync", "selectClusters") - return conn.prepare( - """ - SELECT - count(*), - e.id, - avg(e.ext_lat) AS lat, - avg(e.ext_lon) AS lon, - json_extract(e.tags, '$.icon:android') AS icon_id, - json_extract(e.tags, '$.boost:expires') AS boost_expires, - json_extract(e.overpass_data, '$.tags.payment:lightning:requires_companion_app') AS requires_companion_app - FROM element e - WHERE json_extract(e.tags, '$.category') NOT IN (${excludedCategories.joinToString { "'$it'" }}) - GROUP BY round(ext_lat / ?1) * ?1, round(ext_lon / ?1) * ?1 - ORDER BY e.ext_lat DESC - """ - ).use { - it.bindDouble(1, step) + return db.withConn { conn -> + conn.prepare( + """ + SELECT + count(*), + e.id, + avg(e.ext_lat) AS lat, + avg(e.ext_lon) AS lon, + json_extract(e.tags, '$.icon:android') AS icon_id, + json_extract(e.tags, '$.boost:expires') AS boost_expires, + json_extract(e.overpass_data, '$.tags.payment:lightning:requires_companion_app') AS requires_companion_app + FROM element e + WHERE json_extract(e.tags, '$.category') NOT IN (${excludedCategories.joinToString { "'$it'" }}) + GROUP BY round(ext_lat / ?1) * ?1, round(ext_lon / ?1) * ?1 + ORDER BY e.ext_lat DESC + """ + ).use { + it.bindDouble(1, step) - buildList { - while (it.step()) { - add( - ElementsCluster( - count = it.getLong(0), - id = it.getLong(1), - lat = it.getDouble(2), - lon = it.getDouble(3), - iconId = it.getText(4), - boostExpires = it.getZonedDateTimeOrNull(5), - requiresCompanionApp = it.getText(6, defaultValue = "no") == "yes", + buildList { + while (it.step()) { + add( + ElementsCluster( + count = it.getLong(0), + id = it.getLong(1), + lat = it.getDouble(2), + lon = it.getDouble(3), + iconId = it.getText(4), + boostExpires = it.getZonedDateTimeOrNull(5), + requiresCompanionApp = it.getText(6, defaultValue = "no") == "yes", + ) ) - ) + } } } } @@ -292,70 +289,74 @@ class ElementQueries(private val conn: SQLiteConnection) { minLon: Double, maxLon: Double, ): List { - Log.d("sync", "selectByBoundingBox") - return conn.prepare( - """ - SELECT - id, - ext_lat, - ext_lon, - json_extract(tags, '$.icon:android') AS icon_id, - json_extract(overpass_data, '$.tags') AS osm_tags, - json_extract(tags, '$.issues') AS issues, - json_extract(overpass_data, '$.type') AS osm_type, - json_extract(overpass_data, '$.id') AS osm_id - FROM element - WHERE ext_lat > ?1 AND ext_lat < ?2 AND ext_lon > ?3 AND ext_lon < ?4 - """ - ).use { - it.bindDouble(1, minLat) - it.bindDouble(2, maxLat) - it.bindDouble(3, minLon) - it.bindDouble(4, maxLon) + return db.withConn { conn -> + conn.prepare( + """ + SELECT + id, + ext_lat, + ext_lon, + json_extract(tags, '$.icon:android') AS icon_id, + json_extract(overpass_data, '$.tags') AS osm_tags, + json_extract(tags, '$.issues') AS issues, + json_extract(overpass_data, '$.type') AS osm_type, + json_extract(overpass_data, '$.id') AS osm_id + FROM element + WHERE ext_lat > ?1 AND ext_lat < ?2 AND ext_lon > ?3 AND ext_lon < ?4 + """ + ).use { + it.bindDouble(1, minLat) + it.bindDouble(2, maxLat) + it.bindDouble(3, minLon) + it.bindDouble(4, maxLon) - buildList { - while (it.step()) { - add( - AreaElement( - id = it.getLong(0), - lat = it.getDouble(1), - lon = it.getDouble(2), - icon = it.getText(3), - osmTags = it.getJsonObject(4), - issues = it.getJsonArray(5), - osmType = it.getText(6), - osmId = it.getLong(7), + buildList { + while (it.step()) { + add( + AreaElement( + id = it.getLong(0), + lat = it.getDouble(1), + lon = it.getDouble(2), + icon = it.getText(3), + osmTags = it.getJsonObject(4), + issues = it.getJsonArray(5), + osmType = it.getText(6), + osmId = it.getLong(7), + ) ) - ) + } } } } } fun selectMaxUpdatedAt(): ZonedDateTime? { - Log.d("sync", "selectMaxUpdatedAt") - return conn.prepare("SELECT max(updated_at) FROM element").use { - if (it.step()) { - it.getZonedDateTime(0) - } else { - null + return db.withConn { conn -> + conn.prepare("SELECT max(updated_at) FROM element").use { + if (it.step()) { + it.getZonedDateTime(0) + } else { + null + } } } } fun selectCount(): Long { - Log.d("sync", "selectCount") - return conn.prepare("SELECT count(*) FROM element").use { - it.step() - it.getLong(0) + return db.withConn { conn -> + conn.prepare("SELECT count(*) FROM element").use { + it.step() + it.getLong(0) + } } } fun deleteById(id: Long) { - Log.d("sync", "deleteById") - conn.prepare("DELETE FROM element WHERE id = ?1").use { - it.bindLong(1, id) - it.step() + db.withConn { conn -> + conn.prepare("DELETE FROM element WHERE id = ?1").use { + it.bindLong(1, id) + it.step() + } } } } \ No newline at end of file diff --git a/app/src/main/kotlin/element/ElementsRepo.kt b/app/src/main/kotlin/element/ElementsRepo.kt index 8e0cb664..d8fefad4 100644 --- a/app/src/main/kotlin/element/ElementsRepo.kt +++ b/app/src/main/kotlin/element/ElementsRepo.kt @@ -210,7 +210,7 @@ class ElementsRepo( updatedItems++ } - queries.insertOrReplace(it.toElement()) + queries.insertOrReplace(listOf(it.toElement())) } else { if (cached == null) { // Already evicted from cache, nothing to do here diff --git a/app/src/main/kotlin/event/EventQueries.kt b/app/src/main/kotlin/event/EventQueries.kt index 416abfca..a2662d5b 100644 --- a/app/src/main/kotlin/event/EventQueries.kt +++ b/app/src/main/kotlin/event/EventQueries.kt @@ -1,15 +1,14 @@ package event -import androidx.sqlite.SQLiteConnection -import androidx.sqlite.execSQL import androidx.sqlite.use +import db.Database import db.getJsonObject import db.getText import db.getZonedDateTime import java.time.ZonedDateTime import java.util.regex.Pattern -class EventQueries(private val conn: SQLiteConnection) { +class EventQueries(private val db: Database) { companion object { const val CREATE_TABLE = """ @@ -26,169 +25,174 @@ class EventQueries(private val conn: SQLiteConnection) { } fun insertOrReplace(events: List) { - conn.execSQL("BEGIN IMMEDIATE TRANSACTION") - - try { - events.forEach { insertOrReplace(it) } - conn.execSQL("END TRANSACTION") - } catch (t: Throwable) { - conn.execSQL("ROLLBACK TRANSACTION") - } - } - - fun insertOrReplace(event: Event) { - conn.prepare( - """ - INSERT OR REPLACE - INTO event( - id, - user_id, - element_id, - type, - tags, - created_at, - updated_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) - """ - ).use { - event.apply { - it.bindLong(1, id) - it.bindLong(2, userId) - it.bindLong(3, elementId) - it.bindLong(4, type) - it.bindText(5, tags.toString()) - it.bindText(6, createdAt.toString()) - it.bindText(7, updatedAt.toString()) + db.transaction { conn -> + events.forEach { event -> + conn.prepare( + """ + INSERT OR REPLACE + INTO event( + id, + user_id, + element_id, + type, + tags, + created_at, + updated_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + """ + ).use { + event.apply { + it.bindLong(1, id) + it.bindLong(2, userId) + it.bindLong(3, elementId) + it.bindLong(4, type) + it.bindText(5, tags.toString()) + it.bindText(6, createdAt.toString()) + it.bindText(7, updatedAt.toString()) + } + + it.step() + } } - - it.step() } } fun selectById(id: Long): Event? { - return conn.prepare( - """ - SELECT - id, - user_id, - element_id, - type, - tags, - created_at, - updated_at - FROM event - WHERE id = ?1 - """ - ).use { - it.bindLong(1, id) - - if (it.step()) { - Event( - id = it.getLong(0), - userId = it.getLong(1), - elementId = it.getLong(2), - type = it.getLong(3), - tags = it.getJsonObject(4), - createdAt = it.getZonedDateTime(5), - updatedAt = it.getZonedDateTime(6), - ) - } else { - null + return db.withConn { conn -> + conn.prepare( + """ + SELECT + id, + user_id, + element_id, + type, + tags, + created_at, + updated_at + FROM event + WHERE id = ?1 + """ + ).use { + it.bindLong(1, id) + + if (it.step()) { + Event( + id = it.getLong(0), + userId = it.getLong(1), + elementId = it.getLong(2), + type = it.getLong(3), + tags = it.getJsonObject(4), + createdAt = it.getZonedDateTime(5), + updatedAt = it.getZonedDateTime(6), + ) + } else { + null + } } } } fun selectAll(limit: Long): List { - return conn.prepare( - """ - SELECT - ev.type AS event_type, - el.id AS element_id, - json_extract(el.overpass_data, '$.tags.name') AS element_name, - ev.created_at AS event_date, - json_extract(u.osm_data, '$.display_name') AS user_name, - json_extract(u.osm_data, '$.description') AS user_description - FROM event ev - LEFT JOIN element el ON el.id = ev.element_id - JOIN user u ON u.id = ev.user_id - ORDER BY ev.created_at DESC - LIMIT ?1 - """ - ).use { - it.bindLong(1, limit) - - buildList { - while (it.step()) { - add( - EventListItem( - eventType = it.getLong(0), - elementId = it.getLong(1), - elementName = it.getText(2, ""), - eventDate = it.getZonedDateTime(3), - userName = it.getText(4), - userTips = getLnUrl(it.getText(5)), + return db.withConn { conn -> + conn.prepare( + """ + SELECT + ev.type AS event_type, + el.id AS element_id, + json_extract(el.overpass_data, '$.tags.name') AS element_name, + ev.created_at AS event_date, + json_extract(u.osm_data, '$.display_name') AS user_name, + json_extract(u.osm_data, '$.description') AS user_description + FROM event ev + LEFT JOIN element el ON el.id = ev.element_id + JOIN user u ON u.id = ev.user_id + ORDER BY ev.created_at DESC + LIMIT ?1 + """ + ).use { + it.bindLong(1, limit) + + buildList { + while (it.step()) { + add( + EventListItem( + eventType = it.getLong(0), + elementId = it.getLong(1), + elementName = it.getText(2, ""), + eventDate = it.getZonedDateTime(3), + userName = it.getText(4), + userTips = getLnUrl(it.getText(5)), + ) ) - ) + } } } } } fun selectByUserId(userId: Long): List { - return conn.prepare( - """ - SELECT - ev.type AS event_type, - el.id AS element_id, - json_extract(el.overpass_data, '$.tags.name') AS element_name, - ev.created_at AS event_date - FROM event ev - LEFT JOIN element el ON el.id = ev.element_id - JOIN user u ON u.id = ev.user_id - WHERE ev.user_id = ?1 - ORDER BY ev.created_at DESC - """ - ).use { - it.bindLong(1, userId) - - buildList { - while (it.step()) { - add( - EventListItem( - eventType = it.getLong(0), - elementId = it.getLong(1), - elementName = it.getText(2, ""), - eventDate = it.getZonedDateTime(3), - userName = "", - userTips = "", + return db.withConn { conn -> + conn.prepare( + """ + SELECT + ev.type AS event_type, + el.id AS element_id, + json_extract(el.overpass_data, '$.tags.name') AS element_name, + ev.created_at AS event_date + FROM event ev + LEFT JOIN element el ON el.id = ev.element_id + JOIN user u ON u.id = ev.user_id + WHERE ev.user_id = ?1 + ORDER BY ev.created_at DESC + """ + ).use { + it.bindLong(1, userId) + + buildList { + while (it.step()) { + add( + EventListItem( + eventType = it.getLong(0), + elementId = it.getLong(1), + elementName = it.getText(2, ""), + eventDate = it.getZonedDateTime(3), + userName = "", + userTips = "", + ) ) - ) + } } } } } fun selectMaxUpdatedAt(): ZonedDateTime? { - return conn.prepare("SELECT max(updated_at) FROM event").use { - if (it.step()) { - it.getZonedDateTime(0) - } else { - null + return db.withConn { conn -> + conn.prepare("SELECT max(updated_at) FROM event").use { + if (it.step()) { + it.getZonedDateTime(0) + } else { + null + } } } } fun selectCount(): Long { - return conn.prepare("SELECT count(*) FROM event").use { - it.step() - it.getLong(0) + return db.withConn { conn -> + conn.prepare("SELECT count(*) FROM event").use { + it.step() + it.getLong(0) + } } } fun deleteById(id: Long) { - conn.prepare("DELETE FROM event WHERE id = ?1").use { - it.bindLong(1, id) - it.step() + db.withConn { conn -> + conn.prepare("DELETE FROM event WHERE id = ?1").use { + it.bindLong(1, id) + it.step() + } } } diff --git a/app/src/main/kotlin/event/EventsRepo.kt b/app/src/main/kotlin/event/EventsRepo.kt index f4b7b10d..b900593a 100644 --- a/app/src/main/kotlin/event/EventsRepo.kt +++ b/app/src/main/kotlin/event/EventsRepo.kt @@ -78,7 +78,7 @@ class EventsRepo( updatedItems++ } - queries.insertOrReplace(it.toEvent()) + queries.insertOrReplace(listOf(it.toEvent())) } else { if (cached == null) { // Already evicted from cache, nothing to do here diff --git a/app/src/main/kotlin/reports/ReportQueries.kt b/app/src/main/kotlin/reports/ReportQueries.kt index 69a60f3a..ec3c84ad 100644 --- a/app/src/main/kotlin/reports/ReportQueries.kt +++ b/app/src/main/kotlin/reports/ReportQueries.kt @@ -1,14 +1,13 @@ package reports -import androidx.sqlite.SQLiteConnection -import androidx.sqlite.execSQL import androidx.sqlite.use +import db.Database import db.getDate import db.getJsonObject import db.getZonedDateTime import java.time.ZonedDateTime -class ReportQueries(private val conn: SQLiteConnection) { +class ReportQueries(private val db: Database) { companion object { const val CREATE_TABLE = """ @@ -23,125 +22,128 @@ class ReportQueries(private val conn: SQLiteConnection) { } fun insertOrReplace(reports: List) { - conn.execSQL("BEGIN IMMEDIATE TRANSACTION") - - try { - reports.forEach { insertOrReplace(it) } - conn.execSQL("END TRANSACTION") - } catch (t: Throwable) { - conn.execSQL("ROLLBACK TRANSACTION") - } - } + db.transaction { conn -> + reports.forEach { report -> + conn.prepare( + """ + INSERT OR REPLACE + INTO report( + id, + area_id, + date, + tags, + updated_at + ) + VALUES (?1, ?2, ?3, ?4, ?5) + """ + ).use { + report.apply { + it.bindLong(1, id) + it.bindLong(2, areaId) + it.bindText(3, date.toString()) + it.bindText(4, tags.toString()) + it.bindText(5, updatedAt.toString()) + } - fun insertOrReplace(report: Report) { - conn.prepare( - """ - INSERT OR REPLACE - INTO report( - id, - area_id, - date, - tags, - updated_at - ) - VALUES (?1, ?2, ?3, ?4, ?5) - """ - ).use { - report.apply { - it.bindLong(1, id) - it.bindLong(2, areaId) - it.bindText(3, date.toString()) - it.bindText(4, tags.toString()) - it.bindText(5, updatedAt.toString()) + it.step() + } } - - it.step() } } fun selectById(id: Long): Report? { - return conn.prepare( - """ - SELECT - id, - area_id, - date, - tags, - updated_at - FROM report - WHERE id = ?1 - ORDER BY date - """ - ).use { - it.bindLong(1, id) + return db.withConn { conn -> + conn.prepare( + """ + SELECT + id, + area_id, + date, + tags, + updated_at + FROM report + WHERE id = ?1 + ORDER BY date + """ + ).use { + it.bindLong(1, id) - if (it.step()) { - Report( - id = it.getLong(0), - areaId = it.getLong(1), - date = it.getDate(2), - tags = it.getJsonObject(3), - updatedAt = it.getZonedDateTime(4), - ) - } else { - null + if (it.step()) { + Report( + id = it.getLong(0), + areaId = it.getLong(1), + date = it.getDate(2), + tags = it.getJsonObject(3), + updatedAt = it.getZonedDateTime(4), + ) + } else { + null + } } } } fun selectByAreaId(areaId: Long): List { - return conn.prepare( - """ - SELECT - id, - area_id, - date, - tags, - updated_at - FROM report - WHERE area_id = ?1 - ORDER BY date - """ - ).use { - it.bindLong(1, areaId) + return db.withConn { conn -> + conn.prepare( + """ + SELECT + id, + area_id, + date, + tags, + updated_at + FROM report + WHERE area_id = ?1 + ORDER BY date + """ + ).use { + it.bindLong(1, areaId) - buildList { - while (it.step()) { - add( - Report( - id = it.getLong(0), - areaId = it.getLong(1), - date = it.getDate(2), - tags = it.getJsonObject(3), - updatedAt = it.getZonedDateTime(4), + buildList { + while (it.step()) { + add( + Report( + id = it.getLong(0), + areaId = it.getLong(1), + date = it.getDate(2), + tags = it.getJsonObject(3), + updatedAt = it.getZonedDateTime(4), + ) ) - ) + } } } } } fun selectMaxUpdatedAt(): ZonedDateTime? { - return conn.prepare("SELECT max(updated_at) FROM report").use { - if (it.step()) { - it.getZonedDateTime(0) - } else { - null + return db.withConn { conn -> + conn.prepare("SELECT max(updated_at) FROM report").use { + if (it.step()) { + it.getZonedDateTime(0) + } else { + null + } } } } fun selectCount(): Long { - return conn.prepare("SELECT count(*) FROM report").use { - it.step() - it.getLong(0) + return db.withConn { conn -> + conn.prepare("SELECT count(*) FROM report").use { + it.step() + it.getLong(0) + } } } fun deleteById(id: Long) { - conn.prepare("DELETE FROM report WHERE id = ?1").use { - it.bindLong(1, id) - it.step() + db.withConn { conn -> + conn.prepare("DELETE FROM report WHERE id = ?1").use { + it.bindLong(1, id) + it.step() + } } } } \ No newline at end of file diff --git a/app/src/main/kotlin/reports/ReportsRepo.kt b/app/src/main/kotlin/reports/ReportsRepo.kt index 8e7a6047..c80afbdc 100644 --- a/app/src/main/kotlin/reports/ReportsRepo.kt +++ b/app/src/main/kotlin/reports/ReportsRepo.kt @@ -72,7 +72,7 @@ class ReportsRepo( updatedItems++ } - queries.insertOrReplace(it.toReport()) + queries.insertOrReplace(listOf(it.toReport())) } else { if (cached == null) { // Already evicted from cache, nothing to do here diff --git a/app/src/main/kotlin/user/UserQueries.kt b/app/src/main/kotlin/user/UserQueries.kt index 85edd799..4a63954c 100644 --- a/app/src/main/kotlin/user/UserQueries.kt +++ b/app/src/main/kotlin/user/UserQueries.kt @@ -1,15 +1,14 @@ package user -import androidx.sqlite.SQLiteConnection -import androidx.sqlite.execSQL import androidx.sqlite.use +import db.Database import db.getHttpUrlOrNull import db.getJsonObject import db.getZonedDateTime import java.time.ZonedDateTime import java.util.regex.Pattern -data class UserQueries(private val conn: SQLiteConnection) { +data class UserQueries(private val db: Database) { companion object { const val CREATE_TABLE = """ @@ -23,118 +22,121 @@ data class UserQueries(private val conn: SQLiteConnection) { } fun insertOrReplace(users: List) { - conn.execSQL("BEGIN IMMEDIATE TRANSACTION") + db.transaction { conn -> + users.forEach { user -> + conn.prepare( + """ + INSERT OR REPLACE + INTO user ( + id, + osm_data, + tags, + updated_at + ) VALUES (?1, ?2, ?3, ?4) + """ + ).use { + user.apply { + it.bindLong(1, id) + it.bindText(2, osmData.toString()) + it.bindText(3, tags.toString()) + it.bindText(4, updatedAt.toString()) + } - try { - users.forEach { insertOrReplace(it) } - conn.execSQL("END TRANSACTION") - } catch (t: Throwable) { - conn.execSQL("ROLLBACK TRANSACTION") - } - } - - fun insertOrReplace(user: User) { - conn.prepare( - """ - INSERT OR REPLACE - INTO user ( - id, - osm_data, - tags, - updated_at - ) VALUES (?1, ?2, ?3, ?4) - """ - ).use { - user.apply { - it.bindLong(1, id) - it.bindText(2, osmData.toString()) - it.bindText(3, tags.toString()) - it.bindText(4, updatedAt.toString()) + it.step() + } } - - it.step() } } fun selectAll(): List { - return conn.prepare( - """ - SELECT - u.id AS id, - json_extract(u.osm_data, '$.img.href') AS image, - json_extract(u.osm_data, '$.display_name') AS name, - json_extract(u.osm_data, '$.description') AS description, - count(e.user_id) AS changes - FROM user u - LEFT JOIN event e ON e.user_id = u.id AND json_extract(e.tags, '$.automated') IS NULL - GROUP BY u.id - ORDER BY changes DESC - """ - ).use { - buildList { - while (it.step()) { - add( - UserListItem( - id = it.getLong(0), - image = it.getHttpUrlOrNull(1), - name = it.getText(2), - tips = getLnUrl(it.getText(3)), - changes = it.getLong(4), + return db.withConn { conn -> + conn.prepare( + """ + SELECT + u.id AS id, + json_extract(u.osm_data, '$.img.href') AS image, + json_extract(u.osm_data, '$.display_name') AS name, + json_extract(u.osm_data, '$.description') AS description, + count(e.user_id) AS changes + FROM user u + LEFT JOIN event e ON e.user_id = u.id AND json_extract(e.tags, '$.automated') IS NULL + GROUP BY u.id + ORDER BY changes DESC + """ + ).use { + buildList { + while (it.step()) { + add( + UserListItem( + id = it.getLong(0), + image = it.getHttpUrlOrNull(1), + name = it.getText(2), + tips = getLnUrl(it.getText(3)), + changes = it.getLong(4), + ) ) - ) + } } } } } fun selectById(id: Long): User? { - return conn.prepare( - """ - SELECT - id, - osm_data, - tags, - updated_at - FROM user - WHERE id = ?1 - """ - ).use { - it.bindLong(1, id) + return db.withConn { conn -> + conn.prepare( + """ + SELECT + id, + osm_data, + tags, + updated_at + FROM user + WHERE id = ?1 + """ + ).use { + it.bindLong(1, id) - if (it.step()) { - User( - id = it.getLong(0), - osmData = it.getJsonObject(1), - tags = it.getJsonObject(2), - updatedAt = it.getZonedDateTime(3), - ) - } else { - null + if (it.step()) { + User( + id = it.getLong(0), + osmData = it.getJsonObject(1), + tags = it.getJsonObject(2), + updatedAt = it.getZonedDateTime(3), + ) + } else { + null + } } } } fun selectMaxUpdatedAt(): ZonedDateTime? { - return conn.prepare("SELECT max(updated_at) FROM user").use { - if (it.step()) { - it.getZonedDateTime(0) - } else { - null + return db.withConn { conn -> + conn.prepare("SELECT max(updated_at) FROM user").use { + if (it.step()) { + it.getZonedDateTime(0) + } else { + null + } } } } fun selectCount(): Long { - return conn.prepare("SELECT count(*) FROM user").use { - it.step() - it.getLong(0) + return db.withConn { conn -> + conn.prepare("SELECT count(*) FROM user").use { + it.step() + it.getLong(0) + } } } fun deleteById(id: Long) { - conn.prepare("DELETE FROM user WHERE id = ?1").use { - it.bindLong(1, id) - it.step() + db.withConn { conn -> + conn.prepare("DELETE FROM user WHERE id = ?1").use { + it.bindLong(1, id) + it.step() + } } } diff --git a/app/src/main/kotlin/user/UsersRepo.kt b/app/src/main/kotlin/user/UsersRepo.kt index fd15d5c4..0b3a96c8 100644 --- a/app/src/main/kotlin/user/UsersRepo.kt +++ b/app/src/main/kotlin/user/UsersRepo.kt @@ -78,7 +78,7 @@ class UsersRepo( updatedItems++ } - queries.insertOrReplace(it.toUser()) + queries.insertOrReplace(listOf(it.toUser())) } else { if (cached == null) { // Already evicted from cache, nothing to do here