diff --git a/libsql/src/androidTest/java/tech/turso/libsql/LibsqlTest.java b/libsql/src/androidTest/java/tech/turso/libsql/LibsqlTest.java
index 1c519e2..82347e0 100644
--- a/libsql/src/androidTest/java/tech/turso/libsql/LibsqlTest.java
+++ b/libsql/src/androidTest/java/tech/turso/libsql/LibsqlTest.java
@@ -2,6 +2,7 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
@@ -40,6 +41,46 @@ public void failNestedTransaction() {
         }
     }
 
+    @Test
+    public void queryColumnCount() {
+        try (var db = Libsql.open(":memory:");
+             var conn = db.connect()) {
+            try (var rows = conn.query("select 1 as foo, 2 as bar")) {
+                assertEquals(2, rows.getColumnCount());
+
+            }
+        }
+    }
+
+
+    @Test
+    public void queryColumnName() {
+        try (var db = Libsql.open(":memory:");
+             var conn = db.connect()) {
+            try (var rows = conn.query("select 1 as foo, 2 as bar")) {
+                assertEquals("foo", rows.columnNames((0)));
+                assertEquals("bar", rows.columnNames((1)));
+                assertNull(rows.columnNames((2)));
+
+            }
+        }
+    }
+
+    @Test
+    public void queryColumnType() {
+        try (var db = Libsql.open(":memory:");
+             var conn = db.connect()) {
+            try (var rows = conn.query("select 1 as integer, 3.14 as real, 'text' as text, X'68656C6C6F' as blob, null as nullValue")) {
+                assertEquals(ValueType.Integer, rows.columnType((0)));
+                assertEquals(ValueType.Real, rows.columnType((1)));
+                assertEquals(ValueType.Text, rows.columnType((2)));
+                assertEquals(ValueType.Blob, rows.columnType((3)));
+                assertEquals(ValueType.Null, rows.columnType((4)));
+                assertNull(rows.columnNames((5)));
+            }
+        }
+    }
+
     @Test
     public void queryEmptyParameters() {
         try (var db = Libsql.open(":memory:");
diff --git a/libsql/src/main/kotlin/tech/turso/libsql/Rows.kt b/libsql/src/main/kotlin/tech/turso/libsql/Rows.kt
index 82bd503..a5279b9 100644
--- a/libsql/src/main/kotlin/tech/turso/libsql/Rows.kt
+++ b/libsql/src/main/kotlin/tech/turso/libsql/Rows.kt
@@ -10,6 +10,17 @@ class Rows internal constructor(private var inner: Long) : AutoCloseable, Iterab
         require(this.inner != 0L) { "Attempted to construct a Rows with a null pointer" }
     }
 
+    val columnCount: Int
+        get() = nativeColumnCount(inner)
+
+    fun columnNames(idx: Int): String? {
+        return nativeColumnName(this.inner, idx)
+    }
+
+    fun columnType(idx: Int): ValueType? {
+        return ValueType.fromInt(nativeColumnType(this.inner ,idx))
+    }
+
     fun next(): Row {
         val buf: ByteArray = nativeNext(this.inner)
         return ProtoRow.parseFrom(buf).valuesList.map {
@@ -32,6 +43,12 @@ class Rows internal constructor(private var inner: Long) : AutoCloseable, Iterab
 
     override fun iterator(): Iterator<Row> = RowsIterator(this)
 
+    private external fun nativeColumnCount(rows: Long): Int
+
+    private external fun nativeColumnName(rows: Long, idx: Int): String?
+
+    private external fun nativeColumnType(rows: Long, idx: Int): Int
+
     private external fun nativeNext(rows: Long): ByteArray
 
     private external fun nativeClose(rows: Long)
diff --git a/libsql/src/main/kotlin/tech/turso/libsql/ValueType.kt b/libsql/src/main/kotlin/tech/turso/libsql/ValueType.kt
new file mode 100644
index 0000000..b8902bf
--- /dev/null
+++ b/libsql/src/main/kotlin/tech/turso/libsql/ValueType.kt
@@ -0,0 +1,15 @@
+package tech.turso.libsql
+
+enum class ValueType(val value: Int) {
+    Integer(1),
+    Real(2),
+    Text(3),
+    Blob(4),
+    Null(5);
+
+    companion object {
+        fun fromInt(value: Int): ValueType? {
+            return entries.find { it.value == value }
+        }
+    }
+}
\ No newline at end of file
diff --git a/libsql/src/main/rust/src/lib.rs b/libsql/src/main/rust/src/lib.rs
index 5725508..ab28688 100644
--- a/libsql/src/main/rust/src/lib.rs
+++ b/libsql/src/main/rust/src/lib.rs
@@ -2,7 +2,7 @@
 
 use jni::{
     objects::{JByteArray, JClass, JString},
-    sys::{jboolean, jbyteArray, jlong},
+    sys::{jboolean, jbyteArray, jint, jlong, jstring},
     JNIEnv,
 };
 use jni_fn::jni_fn;
@@ -283,6 +283,44 @@ pub fn nativeClose(_: JNIEnv, _: JClass, conn: jlong) {
     drop(unsafe { Box::from_raw(conn as *mut Connection) });
 }
 
+#[jni_fn("tech.turso.libsql.Rows")]
+pub fn nativeColumnCount(_: JNIEnv, _: JClass, rows: jlong) -> jint {
+    let rows = ManuallyDrop::new(unsafe { Box::from_raw(rows as *mut Rows) });
+    return rows.column_count();
+}
+
+#[jni_fn("tech.turso.libsql.Rows")]
+pub fn nativeColumnName(mut env: JNIEnv, _: JClass, rows: jlong, idx: jint) -> jstring {
+    let result = (|| -> anyhow::Result<Option<JString>> {
+        let rows = ManuallyDrop::new(unsafe { Box::from_raw(rows as *mut Rows) });
+        if let Some(name) = rows.column_name(idx) {
+            Ok(Some(env.new_string(name)?))
+        } else {
+            Ok(None)
+        }
+    })();
+
+    match result {
+        Ok(Some(jstr)) => jstr.into_raw(),
+        Ok(None) => ptr::null_mut(),
+        Err(err) => {
+            env.throw(err.to_string()).unwrap();
+            ptr::null_mut()
+        }
+    }
+}
+
+#[jni_fn("tech.turso.libsql.Rows")]
+pub fn nativeColumnType(mut env: JNIEnv, _: JClass, rows: jlong, idx: jint) -> jint {
+    let rows = ManuallyDrop::new(unsafe { Box::from_raw(rows as *mut Rows) });
+    match rows.column_type(idx) {
+        Ok(v) => v as jint,
+        Err(err) => {
+            env.throw(err.to_string()).unwrap();
+            -1
+        }
+    }
+}
 #[jni_fn("tech.turso.libsql.Rows")]
 pub fn nativeNext(mut env: JNIEnv, _: JClass, rows: jlong) -> jbyteArray {
     match (|| -> anyhow::Result<JByteArray> {