diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/Columns.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/Columns.kt
index 4b7564c35..c04175b2d 100644
--- a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/Columns.kt
+++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/Columns.kt
@@ -1,5 +1,6 @@
 package io.github.jan.supabase.postgrest.query
 
+import io.github.jan.supabase.annotations.SupabaseExperimental
 import io.github.jan.supabase.postgrest.classPropertyNames
 import kotlin.jvm.JvmInline
 
@@ -17,6 +18,15 @@ value class Columns @PublishedApi internal constructor(val value: String) {
          */
         val ALL = Columns("*")
 
+        /**
+         * Select all columns given in the [builder] parameter
+         * @param builder The columns to select
+         */
+        @SupabaseExperimental
+        inline operator fun invoke(builder: BasicColumnsBuilder.() -> Unit): Columns {
+            return Columns(BasicColumnsBuilder().apply(builder).build())
+        }
+
         /**
          * Select all columns given in the [value] parameter
          * @param value The columns to select, separated by a comma
@@ -41,21 +51,6 @@ value class Columns @PublishedApi internal constructor(val value: String) {
          */
         inline fun <reified T> type() = list(classPropertyNames<T>())
 
-        private fun String.clean(): String {
-            var quoted = false
-            val regex = Regex("\\s")
-            return this.map {
-                if (it == '"') {
-                    quoted = !quoted
-                }
-                if (regex.matches(it.toString()) && !quoted) {
-                    ""
-                } else {
-                    it
-                }
-            }.joinToString("")
-        }
-
     }
 
 }
diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/ColumnsBuilder.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/ColumnsBuilder.kt
new file mode 100644
index 000000000..9e5cde47c
--- /dev/null
+++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/query/ColumnsBuilder.kt
@@ -0,0 +1,160 @@
+package io.github.jan.supabase.postgrest.query
+
+internal object Aggregates {
+    const val AVG = "avg()"
+    const val COUNT = "count()"
+    const val MAX = "max()"
+    const val MIN = "min()"
+    const val SUM = "sum()"
+}
+
+/**
+ * Type-safe builder for selecting columns
+ */
+open class BasicColumnsBuilder {
+
+    internal val columns: MutableList<String> = mutableListOf<String>()
+
+    /**
+     * Selects the given [columns].
+     * - To rename a column, use the [withAlias] infix function.
+     * - To use a function on a column, use the [withFunction] infix function.
+     * - To specify a type for a column/cast a column value, use the [withType] infix function.
+     *
+     * Example:
+     * ```kotlin
+     * named("name" withAlias "my_name", "id" withType "text") // name AS my_name, id::text
+     * ```
+     * @param columns The columns to select
+     */
+    fun named(vararg columns: String) {
+        this.columns.addAll(columns)
+    }
+
+    /**
+     * Selects all/the remaining columns
+     */
+    fun all() {
+        columns.add("*")
+    }
+
+    /**
+     * Selects a JSON column
+     *
+     * For example to select the key `key` from the JSON column in `json_data`:
+     *
+     * ```json
+     * {
+     *    "key": "value",
+     *    "array": [{
+     *        "key": "value"
+     *    }]
+     * }
+     * ```
+     *
+     * ```kotlin
+     * json("json_data", "array", "0", "key", returnAsText = true) // jsonData->array->0->>key
+     * ```
+     *
+     * @param column The column to select
+     * @param path The path to the JSON key
+     * @param returnAsText Whether to return the JSON key as text
+     */
+    fun json(column: String, vararg path: String, returnAsText: Boolean = false) {
+        val operator = if(returnAsText) "->>" else "->"
+        val formattedPath = if(path.size > 1) path.dropLast(1).joinToString("->", prefix = "->") else ""
+        val key = path.last()
+        columns.add("$column$formattedPath$operator$key")
+    }
+
+    /**
+     * Selects a foreign column
+     * @param name The name of the foreign column or the table name
+     * @param columnsBuilder The columns to select from the foreign column
+     */
+    fun foreign(name: String, columnsBuilder: ForeignColumnsBuilder.() -> Unit = {}) {
+        val foreignColumns = ForeignColumnsBuilder().apply(columnsBuilder)
+        val spread = if(foreignColumns.spread) "..." else ""
+        val key = if(foreignColumns.key != null) "!${foreignColumns.key}" else ""
+        columns.add("$spread$name$key(${foreignColumns.build()})")
+    }
+
+    /**
+     * Renames a column to the given [alias]
+     * @param alias The alias to rename the column to
+     */
+    infix fun String.withAlias(alias: String) = "$alias:$this"
+
+    /**
+     * Applies a function to the column
+     * @param name The name of the function
+     */
+    infix fun String.withFunction(name: String) = "$this.$name"
+
+    /**
+     * Casts a column to the given [type]
+     * @param type The type to cast the column to
+     */
+    infix fun String.withType(type: String) = "$this::$type"
+
+    /**
+     * Applies the `avg()` function to the column
+     */
+    fun avg() = Aggregates.AVG
+
+    /**
+     * Applies the `count()` function to the column
+     */
+    fun count() = Aggregates.COUNT
+
+    /**
+     * Applies the `max()` function to the column
+     */
+    fun max() = Aggregates.MAX
+
+    /**
+     * Applies the `min()` function to the column
+     */
+    fun min() = Aggregates.MIN
+
+    /**
+     * Applies the `sum()` function to the column
+     */
+    fun sum() = Aggregates.SUM
+
+    @PublishedApi
+    internal fun build() = columns.joinToString(",")
+
+}
+
+/**
+ * Type-safe builder for selecting columns
+ */
+class ForeignColumnsBuilder: BasicColumnsBuilder() {
+
+    /**
+     * Whether to spread the foreign columns in the response
+     */
+    var spread = false
+
+    /**
+     * The key to use for the foreign column when having multiple foreign columns
+     */
+    var key: String? = null
+
+}
+
+internal fun String.clean(): String {
+    var quoted = false
+    val regex = Regex("\\s")
+    return this.map {
+        if (it == '"') {
+            quoted = !quoted
+        }
+        if (regex.matches(it.toString()) && !quoted) {
+            ""
+        } else {
+            it
+        }
+    }.joinToString("")
+}
\ No newline at end of file