Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Have you ever heard the story of SQL the Delight?

Have you ever heard the story of SQL the Delight?

Talk about SQLDelight from DroidYangon 2019

Check source code at https://github.com/vincent-paing/DroidYangonSQL

Aung Kyaw Paing

July 09, 2019
Tweet

More Decks by Aung Kyaw Paing

Other Decks in Technology

Transcript

  1. Have you ever heard the story of SQL the Delight?

    Photo by Alexander Andrews on Unsplash
  2. History of SQLite in Android - Content Provider + Raw

    SQL - Object-Relational Mapper (ORMs) - Room
  3. Raw SQL class BallotMapper { fun toContentValues(ballot: Ballot): ContentValues {

    val contentValues = ContentValues() contentValues.put("id", ballot.id) contentValues.put("image", ballot.image) if (ballot.isValid) { contentValues.put("isValid", 1) } else { contentValues.put("isValid", 0) } return contentValues } fun fromCursor(cursor: Cursor): Ballot { val id = cursor.getLong(cursor.getColumnIndex("id")) val imageUrl = cursor.getString(cursor.getColumnIndex("image")) val isValid = cursor.getInt(cursor.getColumnIndex("isValid")) == 1 return Ballot( id = id, image = imageUrl, isValid = isValid ) } }
  4. ORM @DatabaseTable(tableName = "ballot") data class BallotOrmLiteEntity( @DatabaseField(generatedId = false,

    columnName = "id") var id: Long? = null, @DatabaseField(columnName = "image") var image: String? = null, @DatabaseField(columnName = "isValid") var isValid: Boolean? = null )
  5. ORM override fun getValidBallots(): List<Ballot> { return ballotDao .queryForEq("isValid'", true)

    .map { Ballot( id = it.id!!, image = it.image!!, isValid = it.isValid!! ) } }
  6. Object Database open class BallotRealmEntity : RealmObject() { @PrimaryKey var

    id: Long = 0 var image: String = "" var isValidBallot: Boolean = true }
  7. Object Database override fun getValidBallots(): List<Ballot> { return realm.where(BallotRealmEntity::class.java) .equalTo("isValidBallot",

    true) .findAll() .map { Ballot( id = it.id, image = it.image, isValid = it.isValidBallot ) } }
  8. Room - Announced in 2017 Google I/O - 2.0.0 in

    2018 - Officially recommended approach
  9. Room @Entity(tableName = "ballots") data class BallotRoomEntity( @PrimaryKey val id:

    Long, @ColumnInfo(name = "name") val image: String, @ColumnInfo(name = "is_valid") val isValid: Boolean ) @Dao interface BallotDao { @Query("SELECT * FROM ballots WHERE is_valid = :isValid") fun getValidBallots(isValid: Boolean): List<BallotRoomEntity> }
  10. SQLDelight - Write sql(.sq) file - Generate Kotlin code -

    Reverse of ORM - Two parts - Gradle plugin - Drivers
  11. SQLDelight buildscript { repositories { google() mavenCentral() } dependencies {

    classpath 'com.squareup.sqldelight:gradle-plugin:1.1.3' } } apply plugin: 'com.squareup.sqldelight'
  12. SQLDelight sqldelight { ExampleDb { packageName = "com.example.sqldelight" sourceFolders =

    ["sqldelight"] schemaOutputDirectory = file("build/sqldelight") } }
  13. SQLDelight sqldelight { ExampleDb { packageName = "com.example.sqldelight" sourceFolders =

    ["sqldelight"] schemaOutputDirectory = file("build/sqldelight") } }
  14. SQLDelight sqldelight { ExampleDb { packageName = "com.example.sqldelight" sourceFolders =

    ["sqldelight"] schemaOutputDirectory = file("build/sqldelight") } }
  15. SQLDelight sqldelight { ExampleDb { packageName = "com.example.sqldelight" sourceFolders =

    ["sqldelight"] schemaOutputDirectory = file("build/sqldelight") } }
  16. SQLDelight package com.example.sqldelight ... interface ExampleDb : Transacter { companion

    object { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver): ExampleDb = ExampleDb::class.newInstance(driver)} }
  17. SQLDelight package com.example.sqldelight ... interface ExampleDb : Transacter { companion

    object { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver): ExampleDb = ExampleDb::class.newInstance(driver)} }
  18. SQLDelight package com.example.sqldelight ... interface ExampleDb : Transacter { companion

    object { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver): ExampleDb = ExampleDb::class.newInstance(driver)} }
  19. SQLDelight package com.example.sqldelight ... interface ExampleDb : Transacter { companion

    object { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver): ExampleDb = ExampleDb::class.newInstance(driver)} }
  20. SQLDelight package com.example.sqldelight.sqldelight import ... internal val KClass<ExampleDb>.schema: SqlDriver.Schema get()

    = ExampleDbImpl.Schema internal fun KClass<ExampleDb>.newInstance(driver: SqlDriver): ExampleDb = ExampleDbImpl(driver) private class ExampleDbImpl(driver: SqlDriver) : TransacterImpl(driver), ExampleDb { override val ballotQueries: BallotQueriesImpl = BallotQueriesImpl(this, driver) object Schema : SqlDriver.Schema { override val version: Int get() = 1 override fun create(driver: SqlDriver) { } override fun migrate( driver: SqlDriver, oldVersion: Int, newVersion: Int ) { } } }
  21. SQLDelight package com.example.sqldelight.sqldelight import ... internal val KClass<ExampleDb>.schema: SqlDriver.Schema get()

    = ExampleDbImpl.Schema internal fun KClass<ExampleDb>.newInstance(driver: SqlDriver): ExampleDb = ExampleDbImpl(driver) private class ExampleDbImpl(driver: SqlDriver) : TransacterImpl(driver), ExampleDb { override val ballotQueries: BallotQueriesImpl = BallotQueriesImpl(this, driver) object Schema : SqlDriver.Schema { override val version: Int get() = 1 override fun create(driver: SqlDriver) { } override fun migrate( driver: SqlDriver, oldVersion: Int, newVersion: Int ) { } } }
  22. SQLDelight package com.example.sqldelight.sqldelight import ... internal val KClass<ExampleDb>.schema: SqlDriver.Schema get()

    = ExampleDbImpl.Schema internal fun KClass<ExampleDb>.newInstance(driver: SqlDriver): ExampleDb = ExampleDbImpl(driver) private class ExampleDbImpl(driver: SqlDriver) : TransacterImpl(driver), ExampleDb { object Schema : SqlDriver.Schema { override val version: Int get() = 1 override fun create(driver: SqlDriver) { } override fun migrate( driver: SqlDriver, oldVersion: Int, newVersion: Int ) { } } }
  23. CREATE TABLE Ballot( id INTEGER PRIMARY KEY NOT NULL, image

    TEXT NOT NULL, is_valid INTEGER AS Boolean NOT NULL DEFAULT 0 ); Schema Definition
  24. package com.aungkyawpaing.droidyangonsql.data.sqldelight import ... interface Ballot { val id: Long

    val name: String val is_valid: Boolean data class Impl( override val id: Long, override val image: String, override val is_valid: Boolean ) : Ballot { override fun toString(): String = """ |Ballot.Impl [ | id: $id | image: $image | is_valid: $is_valid |] """.trimMargin() } } Schema Definition
  25. private class BallotQueriesImpl( private val database: ExampleDbImpl, private val driver:

    SqlDriver ) : TransacterImpl(driver), BallotQueries { } Schema Definition
  26. interface ExampleDb : Transacter { companion object { val Schema:

    SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver): ExampleDb = ExampleDb::class.newInstance(driver)} } Schema Definition
  27. interface ExampleDb : Transacter { val ballotQueries: BallotQueries companion object

    { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver): ExampleDb = ExampleDb::class.newInstance(driver)} } Schema Definition
  28. interface ExampleDb : Transacter { val ballotQueries: BallotQueries companion object

    { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver): ExampleDb = ExampleDb::class.newInstance(driver)} } Schema Definition
  29. package com.example.sqldelight.sqldelight import ... internal val KClass<ExampleDb>.schema: SqlDriver.Schema get() =

    ExampleDbImpl.Schema internal fun KClass<ExampleDb>.newInstance(driver: SqlDriver): ExampleDb = ExampleDbImpl(driver) private class ExampleDbImpl(driver: SqlDriver) : TransacterImpl(driver), ExampleDb { object Schema : SqlDriver.Schema { override val version: Int get() = 1 override fun create(driver: SqlDriver) { } override fun migrate( driver: SqlDriver, oldVersion: Int, newVersion: Int ) { } } } Schema Definition
  30. package com.example.sqldelight.sqldelight import ... internal val KClass<ExampleDb>.schema: SqlDriver.Schema get() =

    ExampleDbImpl.Schema internal fun KClass<ExampleDb>.newInstance(driver: SqlDriver): ExampleDb = ExampleDbImpl(driver) private class ExampleDbImpl(driver: SqlDriver) : TransacterImpl(driver), ExampleDb { override val ballotQueries: BallotQueriesImpl = BallotQueriesImpl(this, driver) object Schema : SqlDriver.Schema { override val version: Int get() = 1 override fun create(driver: SqlDriver) { } override fun migrate( driver: SqlDriver, oldVersion: Int, newVersion: Int ) { } } } Schema Definition
  31. package com.example.sqldelight.sqldelight import ... internal val KClass<ExampleDb>.schema: SqlDriver.Schema get() =

    ExampleDbImpl.Schema internal fun KClass<ExampleDb>.newInstance(driver: SqlDriver): ExampleDb = ExampleDbImpl(driver) private class ExampleDbImpl(driver: SqlDriver) : TransacterImpl(driver), ExampleDb { override val ballotQueries: BallotQueriesImpl = BallotQueriesImpl(this, driver) object Schema : SqlDriver.Schema { override val version: Int get() = 1 override fun create(driver: SqlDriver) { } override fun migrate( driver: SqlDriver, oldVersion: Int, newVersion: Int ) { } } } Schema Definition
  32. override fun create(driver: SqlDriver) { driver.execute(null, """ |CREATE TABLE Ballot(

    | id INTEGER PRIMARY KEY NOT NULL, | image TEXT NOT NULL, | is_valid INTEGER NOT NULL DEFAULT 0 |) """.trimMargin(), 0) } Schema Definition
  33. interface BallotQueries : Transacter { fun <T : Any> select_valid_only(mapper:

    ( id: Long, image: String, is_valid: Boolean ) -> T): Query<T> fun select_valid_only(): Query<Ballot> } Query
  34. private class BallotQueriesImpl( private val database: ExampleDbImpl, private val driver:

    SqlDriver ) : TransacterImpl(driver), BallotQueries { } Query
  35. private class BallotQueriesImpl( private val database: ExampleDbImpl, private val driver:

    SqlDriver ) : TransacterImpl(driver), BallotQueries { internal val select_valid_only: MutableList<Query<*>> = copyOnWriteList() override fun <T : Any> select_valid_only(mapper: ( id: Long, image: String, is_valid: Boolean ) -> T): Query<T> = Query(0, select_valid_only, driver, "SELECT * FROM Ballot WHERE is_valid = 1") { cursor -> mapper( cursor.getLong(0)!!, cursor.getString(1)!!, cursor.getLong(2)!! == 1L ) } override fun select_valid_only(): Query<Ballot> = select_valid_only(Ballot::Impl) } Query
  36. private class BallotQueriesImpl( private val database: ExampleDbImpl, private val driver:

    SqlDriver ) : TransacterImpl(driver), BallotQueries { internal val select_valid_only: MutableList<Query<*>> = copyOnWriteList() override fun <T : Any> select_valid_only(mapper: ( id: Long, image: String, is_valid: Boolean ) -> T): Query<T> = Query(0, select_valid_only, driver, "SELECT * FROM Ballot WHERE is_valid = 1") { cursor -> mapper( cursor.getLong(0)!!, cursor.getString(1)!!, cursor.getLong(2)!! == 1L ) } override fun select_valid_only(): Query<Ballot> = select_valid_only(Ballot::Impl) } Query
  37. private class BallotQueriesImpl( private val database: ExampleDbImpl, private val driver:

    SqlDriver ) : TransacterImpl(driver), BallotQueries { internal val select_valid_only: MutableList<Query<*>> = copyOnWriteList() override fun <T : Any> select_valid_only(mapper: ( id: Long, image: String, is_valid: Boolean ) -> T): Query<T> = Query(0, select_valid_only, driver, "SELECT * FROM Ballot WHERE is_valid = 1") { cursor -> mapper( cursor.getLong(0)!!, cursor.getString(1)!!, cursor.getLong(2)!! == 1L ) } override fun select_valid_only(): Query<Ballot> = select_valid_only(Ballot::Impl) } Query
  38. //Create a driver val driver = AndroidSqliteDriver(ExampleDb.Schema, context, "example.db") //Create

    Database val exampleDb = ExampleDb(driver) //Get Queries from Database val ballotQueries = exampleDb.ballotQueries Query
  39. //Create a driver val driver = AndroidSqliteDriver(ExampleDb.Schema, context, "example.db") //Create

    Database val exampleDb = ExampleDb(driver) //Get Queries from Database val ballotQueries = exampleDb.ballotQueries Query
  40. //Create a driver val driver = AndroidSqliteDriver(ExampleDb.Schema, context, "example.db") //Create

    Database val exampleDb = ExampleDb(driver) //Get Queries from Database val ballotQueries = exampleDb.ballotQueries Query
  41. //Create Database val exampleDb = ExampleDb(driver) interface ExampleDb : Transacter

    { companion object { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver): ExampleDb = ExampleDb::class.newInstance(driver)} } Query
  42. //Create Database val exampleDb = ExampleDb(driver) interface ExampleDb : Transacter

    { companion object { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver): ExampleDb = ExampleDb::class.newInstance(driver)} } Query
  43. //Create Database val exampleDb = ExampleDb(driver) interface ExampleDb : Transacter

    { companion object { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver): ExampleDb = ExampleDb::class.newInstance(driver)} } internal fun KClass<ExampleDb>.newInstance(driver: SqlDriver): ExampleDb = ExampleDbImpl(driver) Query
  44. //Create Database val exampleDb = ExampleDb(driver) private class ExampleDbImpl(driver: SqlDriver)

    : TransacterImpl(driver), ExampleDb { override val ballotQueries: BallotQueriesImpl = BallotQueriesImpl(this, driver) object Schema : SqlDriver.Schema { ... override fun create(driver: SqlDriver) { driver.execute(null, """ |CREATE TABLE Ballot( | id INTEGER PRIMARY KEY NOT NULL, | name TEXT NOT NULL, | is_valid INTEGER NOT NULL DEFAULT 0 |) """.trimMargin(), 0) } ... } } Query
  45. //Create Database val exampleDb = ExampleDb(driver) private class ExampleDbImpl(driver: SqlDriver)

    : TransacterImpl(driver), ExampleDb { override val ballotQueries: BallotQueriesImpl = BallotQueriesImpl(this, driver) object Schema : SqlDriver.Schema { ... override fun create(driver: SqlDriver) { driver.execute(null, """ |CREATE TABLE Ballot( | id INTEGER PRIMARY KEY NOT NULL, | name TEXT NOT NULL, | is_valid INTEGER NOT NULL DEFAULT 0 |) """.trimMargin(), 0) } ... } } Query
  46. //Get Queries from Database val ballotQueries = exampleDb.ballotQueries internal fun

    KClass<ExampleDb>.newInstance(driver: SqlDriver): ExampleDb = ExampleDbImpl(driver) private class ExampleDbImpl(driver: SqlDriver) : TransacterImpl(driver), ExampleDb { override val ballotQueries: BallotQueriesImpl = BallotQueriesImpl(this, driver) object Schema : SqlDriver.Schema { ... override fun create(driver: SqlDriver) { driver.execute(null, """ |CREATE TABLE Ballot( | id INTEGER PRIMARY KEY NOT NULL, | name TEXT NOT NULL, | is_valid INTEGER NOT NULL DEFAULT 0 |) """.trimMargin(), 0) } ... } } Query
  47. private class ExampleDbImpl(driver: SqlDriver) : TransacterImpl(driver), ExampleDb { override val

    ballotQueries: BallotQueriesImpl = BallotQueriesImpl(this, driver) ... } //Get Queries from Database val ballotQueries = exampleDb.ballotQueries Query
  48. //Get Queries from Database val ballotQueries = exampleDb.ballotQueries private class

    ExampleDbImpl(driver: SqlDriver) : TransacterImpl(driver), ExampleDb { override val ballotQueries: BallotQueriesImpl = BallotQueriesImpl(this, driver) ... } private class BallotQueriesImpl( private val database: ExampleDbImpl, private val driver: SqlDriver ) : TransacterImpl(driver), BallotQueries { ... } Query
  49. override fun <T : Any> select_valid_only(mapper: ( id: Long, name:

    String, is_valid: Boolean ) -> T): Query<T> = Query(0, select_valid_only, driver, "SELECT * FROM Ballot WHERE is_valid = 1") { cursor -> mapper( cursor.getLong(0)!!, cursor.getString(1)!!, cursor.getLong(2)!! == 1L ) } Query
  50. Query override fun getValidBallots(): List<Ballot> { return ballotQueries.select_valid_only { id,

    image, is_valid -> Ballot( id, image, is_valid ) }.executeAsList() }
  51. Custom Type CREATE TABLE Ballot( id INTEGER PRIMARY KEY NOT

    NULL, image TEXT NOT NULL, is_valid INTEGER AS Boolean NOT NULL DEFAULT 0 );
  52. Custom Type CREATE TABLE Ballot( id INTEGER PRIMARY KEY NOT

    NULL, image TEXT NOT NULL, is_valid INTEGER AS Boolean NOT NULL DEFAULT 0 );
  53. Custom Type CREATE TABLE Ballot( id INTEGER PRIMARY KEY NOT

    NULL, image TEXT NOT NULL, is_valid INTEGER AS Boolean NOT NULL DEFAULT 0 );
  54. Custom Type import org.threeten.bp.Instant; CREATE TABLE Ballot( id INTEGER PRIMARY

    KEY NOT NULL, image TEXT NOT NULL, added_date INTEGER AS Instant NOT NULL, is_valid INTEGER AS Boolean NOT NULL DEFAULT 0 );
  55. Custom Type import org.threeten.bp.Instant; CREATE TABLE Ballot( id INTEGER PRIMARY

    KEY NOT NULL, image TEXT NOT NULL, added_date INTEGER AS Instant NOT NULL, is_valid INTEGER AS Boolean NOT NULL DEFAULT 0 );
  56. Custom Type interface Ballot { val id: Long val image:

    String val added_date: Instant val is_valid: Boolean data class Impl( override val id: Long, override val image: String, override val added_date: Instant, override val is_valid: Boolean ) : Ballot { override fun toString(): String = """ |Ballot.Impl [ | id: $id | image: $image | added_date: $added_date | is_valid: $is_valid |] """.trimMargin() } }
  57. Custom Type interface ExampleDb : Transacter { val ballotQueries: BallotQueries

    companion object { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver): ExampleDb = ExampleDb::class.newInstance(driver)} }
  58. Custom Type interface ExampleDb : Transacter { val ballotQueries: BallotQueries

    companion object { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver, BallotAdapter: Ballot.Adapter): ExampleDb = ExampleDb::class.newInstance(driver, BallotAdapter)} }
  59. Custom Type interface ExampleDb : Transacter { val ballotQueries: BallotQueries

    companion object { val Schema: SqlDriver.Schema get() = ExampleDb::class.schema operator fun invoke(driver: SqlDriver, BallotAdapter: Ballot.Adapter): ExampleDb = ExampleDb::class.newInstance(driver, BallotAdapter)} }
  60. Custom Type private val instantColumnAdapter = object : ColumnAdapter<Instant, Long>

    { override fun decode(databaseValue: Long): Instant { return Instant.ofEpochMilli(databaseValue) } override fun encode(value: Instant): Long { return value.toEpochMilli() } }
  61. Custom Type private val instantColumnAdapter = object : ColumnAdapter<Instant, Long>

    { override fun decode(databaseValue: Long): Instant { return Instant.ofEpochMilli(databaseValue) } override fun encode(value: Instant): Long { return value.toEpochMilli() } }
  62. Custom Type private val instantColumnAdapter = object : ColumnAdapter<Instant, Long>

    { override fun decode(databaseValue: Long): Instant { return Instant.ofEpochMilli(databaseValue) } override fun encode(value: Instant): Long { return value.toEpochMilli() } }
  63. Custom Type private val instantColumnAdapter = object : ColumnAdapter<Instant, Long>

    { override fun decode(databaseValue: Long): Instant { return Instant.ofEpochMilli(databaseValue) } override fun encode(value: Instant): Long { return value.toEpochMilli() } }
  64. Custom Type private val instantColumnAdapter = object : ColumnAdapter<Instant, Long>

    { override fun decode(databaseValue: Long): Instant { return Instant.ofEpochMilli(databaseValue) } override fun encode(value: Instant): Long { return value.toEpochMilli() } } private val ballotColumnAdapter = Ballot.Adapter( added_dateAdapter = instantColumnAdapter )
  65. Custom Type private val instantColumnAdapter = object : ColumnAdapter<Instant, Long>

    { override fun decode(databaseValue: Long): Instant { return Instant.ofEpochMilli(databaseValue) } override fun encode(value: Instant): Long { return value.toEpochMilli() } } private val ballotColumnAdapter = Ballot.Adapter( added_dateAdapter = instantColumnAdapter ) private val exampleDb = ExampleDb(driver, ballotColumnAdapter)
  66. Custom Type https://jakewharton.com/inline-classes-make-great-database-ids/ inline class BallotId(val value: Long) CREATE TABLE

    Ballot( ballot_id INTEGER as BallotId PRIMARY KEY NOT NULL, image TEXT NOT NULL, added_date INTEGER AS Instant NOT NULL, is_valid INTEGER AS Boolean NOT NULL DEFAULT 0 );
  67. Migration by script - Put into src/main/sqldelight - File naming:

    <version to upgrade from>.sqm - 1 to 2 => 1.sqm - 2 to 3 => 2.sqm - 7 to 8 => 7.sqm - .sq file will have to always contain latest structure
  68. Migration private class ExampleDbImpl(driver: SqlDriver) ...{ object Schema : SqlDriver.Schema

    { override val version: Int get() = 1 override fun create(driver: SqlDriver) { //... } override fun migrate( driver: SqlDriver, oldVersion: Int, newVersion: Int ) { } } }
  69. Migration private class ExampleDbImpl(driver: SqlDriver) ...{ object Schema : SqlDriver.Schema

    { override val version: Int get() = 1 override fun migrate( driver: SqlDriver, oldVersion: Int, newVersion: Int ) { } } }
  70. Migration private class ExampleDbImpl(driver: SqlDriver) ...{ object Schema : SqlDriver.Schema

    { override val version: Int get() = 2 override fun migrate( driver: SqlDriver, oldVersion: Int, newVersion: Int ) { } } }
  71. Migration private class ExampleDbImpl(driver: SqlDriver) ...{ object Schema : SqlDriver.Schema

    { override val version: Int get() = 2 override fun migrate( driver: SqlDriver, oldVersion: Int, newVersion: Int ) { if (oldVersion <= 1 && newVersion > 1) { driver.execute(null, "ALTER TABLE Ballot ADD COLUMN added_date INTEGER NOT NULL;”, 0) driver.execute(null, "UPDATE Ballot SET added_date = 0;", 0) } } } }
  72. - Not time efficient - Hard to understand - I/O

    controls - POJO not required - Flexibility - High level of abstraction SQLDelight ✅ Raw SQL ❌
  73. - Not time efficient - Hard to understand - I/O

    controls - POJO not required - Flexibility - High level of abstraction SQLDelight ✅ Raw SQL ❌
  74. - Not time efficient - Hard to understand - I/O

    controls - POJO not required - Flexibility - High level of abstraction SQLDelight ✅ Raw SQL ❌
  75. - Not time efficient - Hard to understand - I/O

    controls - POJO not required - Flexibility - High level of abstraction SQLDelight ✅ Raw SQL ❌