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> {