Twelve: Refactor Query
diff --git a/app/src/main/java/org/lineageos/twelve/datasources/LocalDataSource.kt b/app/src/main/java/org/lineageos/twelve/datasources/LocalDataSource.kt
index 9e22001..32d668c 100644
--- a/app/src/main/java/org/lineageos/twelve/datasources/LocalDataSource.kt
+++ b/app/src/main/java/org/lineageos/twelve/datasources/LocalDataSource.kt
@@ -34,9 +34,9 @@
 import org.lineageos.twelve.models.RequestStatus
 import org.lineageos.twelve.query.Query
 import org.lineageos.twelve.query.and
+import org.lineageos.twelve.query.query
 import org.lineageos.twelve.query.eq
 import org.lineageos.twelve.query.`in`
-import org.lineageos.twelve.query.join
 import org.lineageos.twelve.query.like
 import org.lineageos.twelve.query.neq
 
@@ -201,8 +201,9 @@
             albumsUri,
             albumsProjection,
             bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to
-                        (MediaStore.Audio.AlbumColumns.ALBUM like Query.ARG).build(),
+                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                    MediaStore.Audio.AlbumColumns.ALBUM like Query.ARG
+                },
                 ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(query),
             )
         ).mapEachRow(albumsProjection, mapAlbum),
@@ -210,8 +211,9 @@
             artistsUri,
             artistsProjection,
             bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to
-                        (MediaStore.Audio.ArtistColumns.ARTIST like Query.ARG).build(),
+                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                    MediaStore.Audio.ArtistColumns.ARTIST like Query.ARG
+                },
                 ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(query),
             )
         ).mapEachRow(artistsProjection, mapArtist),
@@ -219,8 +221,9 @@
             audiosUri,
             audiosProjection,
             bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to
-                        (MediaStore.Audio.AudioColumns.TITLE like Query.ARG).build(),
+                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                    MediaStore.Audio.AudioColumns.TITLE like Query.ARG
+                },
                 ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(query),
             )
         ).mapEachRow(audiosProjection, mapAudio),
@@ -228,8 +231,9 @@
             genresUri,
             genresProjection,
             bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to
-                        (MediaStore.Audio.GenresColumns.NAME like Query.ARG).build(),
+                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                    MediaStore.Audio.GenresColumns.NAME like Query.ARG
+                },
                 ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(query),
             )
         ).mapEachRow(genresProjection, mapGenre),
@@ -241,8 +245,9 @@
         audiosUri,
         audiosProjection,
         bundleOf(
-            ContentResolver.QUERY_ARG_SQL_SELECTION to
-                    (MediaStore.Audio.AudioColumns._ID eq Query.ARG).build(),
+            ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                MediaStore.Audio.AudioColumns._ID eq Query.ARG
+            },
             ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
                 ContentUris.parseId(audioUri).toString(),
             ),
@@ -258,8 +263,9 @@
             albumsUri,
             albumsProjection,
             bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to
-                        (MediaStore.Audio.AudioColumns._ID eq Query.ARG).build(),
+                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                    MediaStore.Audio.AudioColumns._ID eq Query.ARG
+                },
                 ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
                     ContentUris.parseId(albumUri).toString(),
                 ),
@@ -269,8 +275,9 @@
             audiosUri,
             audiosProjection,
             bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to
-                        (MediaStore.Audio.AudioColumns.ALBUM_ID eq Query.ARG).build(),
+                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                    MediaStore.Audio.AudioColumns.ALBUM_ID eq Query.ARG
+                },
                 ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
                     ContentUris.parseId(albumUri).toString(),
                 ),
@@ -287,8 +294,9 @@
             artistsUri,
             artistsProjection,
             bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to
-                        (MediaStore.Audio.AudioColumns._ID eq Query.ARG).build(),
+                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                    MediaStore.Audio.AudioColumns._ID eq Query.ARG
+                },
                 ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
                     ContentUris.parseId(artistUri).toString(),
                 ),
@@ -298,8 +306,9 @@
             albumsUri,
             albumsProjection,
             bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to
-                        (MediaStore.Audio.AlbumColumns.ARTIST_ID eq Query.ARG).build(),
+                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                    MediaStore.Audio.AlbumColumns.ARTIST_ID eq Query.ARG
+                },
                 ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
                     ContentUris.parseId(artistUri).toString(),
                 ),
@@ -309,8 +318,9 @@
             audiosUri,
             audioAlbumIdsProjection,
             bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to
-                        (MediaStore.Audio.AudioColumns.ARTIST_ID eq Query.ARG).build(),
+                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                    MediaStore.Audio.AudioColumns.ARTIST_ID eq Query.ARG
+                },
                 ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
                     ContentUris.parseId(artistUri).toString(),
                 ),
@@ -324,12 +334,12 @@
                 albumsUri,
                 albumsProjection,
                 bundleOf(
-                    ContentResolver.QUERY_ARG_SQL_SELECTION to listOf(
-                        MediaStore.Audio.AudioColumns.ARTIST_ID neq Query.ARG,
-                        MediaStore.Audio.AudioColumns._ID `in` List(albumIds.size) {
-                            Query.ARG
-                        },
-                    ).join(Query::and).build(),
+                    ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                        (MediaStore.Audio.AudioColumns.ARTIST_ID neq Query.ARG) and
+                                (MediaStore.Audio.AudioColumns._ID `in` List(albumIds.size) {
+                                    Query.ARG
+                                })
+                    },
                     ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
                         ContentUris.parseId(artistUri).toString(),
                         *albumIds
@@ -356,8 +366,9 @@
             genresUri,
             genresProjection,
             bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to
-                        (MediaStore.Audio.AudioColumns._ID eq Query.ARG).build(),
+                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                    MediaStore.Audio.AudioColumns._ID eq Query.ARG
+                },
                 ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
                     ContentUris.parseId(genreUri).toString(),
                 ),
@@ -367,8 +378,9 @@
             audiosUri,
             audiosProjection,
             bundleOf(
-                ContentResolver.QUERY_ARG_SQL_SELECTION to
-                        (MediaStore.Audio.AudioColumns.GENRE_ID eq Query.ARG).build(),
+                ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                    MediaStore.Audio.AudioColumns.GENRE_ID eq Query.ARG
+                },
                 ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
                     ContentUris.parseId(genreUri).toString(),
                 ),
@@ -455,10 +467,11 @@
         audiosUri,
         audiosProjection,
         bundleOf(
-            ContentResolver.QUERY_ARG_SQL_SELECTION to
-                    (MediaStore.Audio.AudioColumns._ID `in` List(audioUris.size) {
-                        Query.ARG
-                    }).build(),
+            ContentResolver.QUERY_ARG_SQL_SELECTION to query {
+                MediaStore.Audio.AudioColumns._ID `in` List(audioUris.size) {
+                    Query.ARG
+                }
+            },
             ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to audioUris.map {
                 ContentUris.parseId(it).toString()
             }.toTypedArray(),
diff --git a/app/src/main/java/org/lineageos/twelve/query/Query.kt b/app/src/main/java/org/lineageos/twelve/query/Query.kt
index e9f7fa6..99ad2c7 100644
--- a/app/src/main/java/org/lineageos/twelve/query/Query.kt
+++ b/app/src/main/java/org/lineageos/twelve/query/Query.kt
@@ -7,49 +7,36 @@
 
 typealias Column = String
 
-sealed interface Node {
-    fun build(): String = when (this) {
-        is And -> "(${lhs.build()}) AND (${rhs.build()})"
-        is Eq -> "${lhs.build()} = ${rhs.build()}"
-        is Neq -> "${lhs.build()} != ${rhs.build()}"
-        is In<*> -> "$value IN (${values.joinToString(", ")})"
-        is Like -> "${lhs.build()} LIKE ${rhs.build()}"
-        is Literal<*> -> "$`val`"
-        is Or -> "(${lhs.build()}) OR (${rhs.build()})"
-    }
-}
-
-private class And(val lhs: Node, val rhs: Node) : Node
-private class Eq(val lhs: Node, val rhs: Node) : Node
-private class Neq(val lhs: Node, val rhs: Node) : Node
-private class In<T>(val value: T, val values: Collection<T>) : Node
-private class Like(val lhs: Node, val rhs: Node) : Node
-private class Literal<T>(val `val`: T) : Node
-private class Or(val lhs: Node, val rhs: Node) : Node
-
-class Query(val root: Node) {
-    fun build() = root.build()
+sealed interface Query {
+    fun build(): String
 
     companion object {
         const val ARG = "?"
     }
 }
 
-infix fun Query.and(other: Query) = Query(And(this.root, other.root))
-infix fun Query.eq(other: Query) = Query(Eq(this.root, other.root))
-infix fun Query.neq(other: Query) = Query(Neq(this.root, other.root))
-infix fun Query.like(other: Query) = Query(Like(this.root, other.root))
-infix fun Query.or(other: Query) = Query(Or(this.root, other.root))
+enum class Operator(val symbol: String) {
+    AND("AND"), OR("OR"), EQUALS("="), NOT_EQUALS("!="), LIKE("LIKE"),
+}
 
-infix fun <T> Column.eq(other: T) = Query(Literal(this)) eq Query(Literal(other))
-infix fun <T> Column.neq(other: T) = Query(Literal(this)) neq Query(Literal(other))
-infix fun <T> Column.`in`(values: Collection<T>) = Query(In(this, values))
-infix fun <T> Column.like(other: T) = Query(Literal(this)) like Query(Literal(other))
+class LogicalOp(private val lhs: Query, private val op: Operator, private val rhs: Query) : Query {
+    override fun build() = "(${lhs.build()}) ${op.symbol} (${rhs.build()})"
+}
 
-fun Iterable<Query>.join(
-    func: Query.(other: Query) -> Query,
-) = reduce(func)
+class StringOp<T>(private val lhs: Column, private val op: Operator, private val rhs: T) : Query {
+    override fun build() = "$lhs ${op.symbol} $rhs"
+}
 
-fun Iterable<Query>.joinNullable(
-    func: Query.(other: Query) -> Query,
-) = reduceOrNull(func)
+class In<T>(private val value: T, private val values: Collection<T>) : Query {
+    override fun build() = "$value IN (${values.joinToString(", ")})"
+}
+
+infix fun Query.and(other: Query) = LogicalOp(this, Operator.AND, other)
+infix fun Query.or(other: Query) = LogicalOp(this, Operator.OR, other)
+
+infix fun Column.eq(other: String) = StringOp(this, Operator.EQUALS, other)
+infix fun Column.neq(other: String) = StringOp(this, Operator.NOT_EQUALS, other)
+infix fun Column.like(other: String) = StringOp(this, Operator.LIKE, other)
+infix fun <T> Column.`in`(values: Collection<T>) = In(this, values)
+
+inline fun query(block: () -> Query) = block().build()