diff --git a/packages/graalvm/api/graalvm.api b/packages/graalvm/api/graalvm.api index 0d20e627d7..a9606f314e 100644 --- a/packages/graalvm/api/graalvm.api +++ b/packages/graalvm/api/graalvm.api @@ -7773,6 +7773,12 @@ public synthetic class elide/runtime/intrinsics/sqlite/$SQLiteAPI$ReflectConfig public fun getAnnotationMetadata ()Lio/micronaut/core/annotation/AnnotationMetadata; } +public synthetic class elide/runtime/intrinsics/sqlite/$SQLiteChanges$ReflectConfig : io/micronaut/core/graal/GraalReflectionConfigurer { + public static final field $ANNOTATION_METADATA Lio/micronaut/core/annotation/AnnotationMetadata; + public fun ()V + public fun getAnnotationMetadata ()Lio/micronaut/core/annotation/AnnotationMetadata; +} + public synthetic class elide/runtime/intrinsics/sqlite/$SQLiteDatabase$Defaults$ReflectConfig : io/micronaut/core/graal/GraalReflectionConfigurer { public static final field $ANNOTATION_METADATA Lio/micronaut/core/annotation/AnnotationMetadata; public fun ()V @@ -7836,6 +7842,11 @@ public synthetic class elide/runtime/intrinsics/sqlite/$SQLiteTransactor$Reflect public abstract interface class elide/runtime/intrinsics/sqlite/SQLiteAPI : org/graalvm/polyglot/proxy/ProxyObject { } +public abstract interface class elide/runtime/intrinsics/sqlite/SQLiteChanges { + public abstract fun getChanges ()J + public abstract fun getLastInsertRowid ()J +} + public abstract interface class elide/runtime/intrinsics/sqlite/SQLiteDatabase : elide/runtime/interop/ReadOnlyProxyObject, elide/runtime/intrinsics/js/Disposable, java/io/Closeable, java/lang/AutoCloseable { public static final field DEFAULT_CREATE Z public static final field DEFAULT_READONLY Z @@ -7847,8 +7858,8 @@ public abstract interface class elide/runtime/intrinsics/sqlite/SQLiteDatabase : public abstract fun connection ()Lorg/sqlite/SQLiteConnection; public abstract fun deserialize ([BLjava/lang/String;)V public static synthetic fun deserialize$default (Lelide/runtime/intrinsics/sqlite/SQLiteDatabase;[BLjava/lang/String;ILjava/lang/Object;)V - public abstract fun exec (Lelide/runtime/intrinsics/sqlite/SQLiteStatement;[Ljava/lang/Object;)Lcom/oracle/truffle/js/runtime/objects/JSDynamicObject; - public abstract fun exec (Ljava/lang/String;[Ljava/lang/Object;)Lcom/oracle/truffle/js/runtime/objects/JSDynamicObject; + public abstract fun exec (Lelide/runtime/intrinsics/sqlite/SQLiteStatement;[Ljava/lang/Object;)Lelide/runtime/intrinsics/sqlite/SQLiteChanges; + public abstract fun exec (Ljava/lang/String;[Ljava/lang/Object;)Lelide/runtime/intrinsics/sqlite/SQLiteChanges; public abstract fun getActive ()Z public synthetic fun getMemberKeys ()Ljava/lang/Object; public fun getMemberKeys ()[Ljava/lang/String; @@ -7902,7 +7913,7 @@ public abstract interface class elide/runtime/intrinsics/sqlite/SQLiteStatement public abstract fun get ([Ljava/lang/Object;)Lelide/runtime/intrinsics/sqlite/SQLiteObject; public abstract fun prepare ([Ljava/lang/Object;)Ljava/sql/PreparedStatement; public static synthetic fun prepare$default (Lelide/runtime/intrinsics/sqlite/SQLiteStatement;[Ljava/lang/Object;ILjava/lang/Object;)Ljava/sql/PreparedStatement; - public abstract fun run ([Ljava/lang/Object;)V + public abstract fun run ([Ljava/lang/Object;)Lelide/runtime/intrinsics/sqlite/SQLiteChanges; public abstract fun unwrap ()Ljava/sql/Statement; public abstract fun values ([Ljava/lang/Object;)Ljava/util/List; } diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/sqlite/SqliteModule.kt b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/sqlite/SqliteModule.kt index 064c956233..5537a940ed 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/sqlite/SqliteModule.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/sqlite/SqliteModule.kt @@ -266,6 +266,27 @@ internal class SqliteDatabaseProxy private constructor ( } } + // Internal SQLiteChanges implementation + private data class SQLiteChangesImpl( + override val changes: Long, + override val lastInsertRowid: Long, + ) : SQLiteChanges, ProxyObject { + override fun getMemberKeys(): Array = arrayOf("changes", "lastInsertRowid") + override fun hasMember(key: String): Boolean = key == "changes" || key == "lastInsertRowid" + override fun getMember(key: String): Any? = when (key) { + "changes" -> changes + "lastInsertRowid" -> lastInsertRowid + else -> null + } + override fun putMember(key: String?, value: Value?) { + throw UnsupportedOperationException("SQLiteChanges is immutable") + } + + companion object { + val EMPTY = SQLiteChangesImpl(0, 0) + } + } + // Internal SQLite object implementation, backed by a de-serialized map. private data class SQLiteObjectImpl private constructor ( private val schema: SQLiteObjectSchema, @@ -430,8 +451,8 @@ internal class SqliteDatabaseProxy private constructor ( resultSet.firstOrNull() } - @Polyglot override fun run(vararg args: Any?) { - requireNotNull(db.get()) { "Database is closed" }.exec(this, *args) + @Polyglot override fun run(vararg args: Any?): SQLiteChanges { + return requireNotNull(db.get()) { "Database is closed" }.exec(this, *args) } @Polyglot @JvmSynthetic override fun finalize() { @@ -579,10 +600,11 @@ internal class SqliteDatabaseProxy private constructor ( override fun connection(): SQLiteConnection = withOpen { this } override fun unwrap(): DB = withOpen { database } - @Polyglot override fun loadExtension(extension: String) = withOpen { + @Polyglot override fun loadExtension(extension: String): JSDynamicObject = withOpen { unwrap().enable_load_extension(true) require(';' !in extension && ' ' !in extension) { "Invalid extension name" } // sanity check exec("SELECT load_extension('$extension')") + Undefined.instance } @Polyglot override fun prepare(statement: String, vararg args: Any?): Statement = withOpen { @@ -603,15 +625,23 @@ internal class SqliteDatabaseProxy private constructor ( } } - @Polyglot override fun exec(statement: String, vararg args: Any?): JSDynamicObject = withOpen { + @Polyglot override fun exec(statement: String, vararg args: Any?): SQLiteChanges = withOpen { exec(oneShotStatement(statement, args)) } - @Polyglot override fun exec(statement: Statement, vararg args: Any?): JSDynamicObject = withOpen { - statement.prepare(args).use { - it.execute() + @Polyglot override fun exec(statement: Statement, vararg args: Any?): SQLiteChanges = withOpen { + val prepared = statement.prepare(args) + prepared.use { stmt -> + stmt.execute() + val updateCount = stmt.updateCount.toLong() + // Get last insert rowid via SQLite function + val lastRowId = connection().prepareStatement("SELECT last_insert_rowid()").use { rowIdStmt -> + rowIdStmt.executeQuery().use { rs -> + if (rs.next()) rs.getLong(1) else 0L + } + } + SQLiteChangesImpl(updateCount, lastRowId) } - Undefined.instance } @Polyglot override fun transaction(runnable: SQLiteTransactor): SQLiteTransaction = withOpen { diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/sqlite/SQLiteChanges.kt b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/sqlite/SQLiteChanges.kt new file mode 100644 index 0000000000..382751cc72 --- /dev/null +++ b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/sqlite/SQLiteChanges.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ +package elide.runtime.intrinsics.sqlite + +import io.micronaut.core.annotation.ReflectiveAccess +import elide.annotations.API +import elide.vm.annotations.Polyglot + +/** + * # SQLite Changes + * + * Represents the result of a write operation (INSERT, UPDATE, DELETE) on a SQLite database. + * This matches the Bun SQLite API's `Changes` interface. + * + * @see SQLiteStatement.run + * @see SQLiteDatabase.exec + */ +@API @ReflectiveAccess public interface SQLiteChanges { + /** + * The number of rows changed by the last `run` or `exec` call. + */ + @get:Polyglot public val changes: Long + + /** + * The rowid of the last inserted row, or 0 if no row was inserted. + * If `safeIntegers` is enabled, this should be treated as a bigint. + */ + @get:Polyglot public val lastInsertRowid: Long +} diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/sqlite/SQLiteDatabase.kt b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/sqlite/SQLiteDatabase.kt index 6ac72bb299..bc06222027 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/sqlite/SQLiteDatabase.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/sqlite/SQLiteDatabase.kt @@ -196,12 +196,11 @@ private val SQLITE_DATABASE_PROPS_AND_METHODS = arrayOf( * Parse, prepare, and then execute an SQL query [statement] with the provided [args] (if any), against the current * SQLite database. * - * This method does not return a value. - * * @param statement SQL query to execute. * @param args Arguments to bind to the statement. + * @return Changes object containing the number of affected rows and last insert rowid. */ - @Polyglot public fun exec(@Language("sql") statement: String, vararg args: Any?): JSDynamicObject + @Polyglot public fun exec(@Language("sql") statement: String, vararg args: Any?): SQLiteChanges /** * ## Execute (Statement) @@ -209,12 +208,11 @@ private val SQLITE_DATABASE_PROPS_AND_METHODS = arrayOf( * Execute the provided [statement], preparing it if necessary, with the provided [args] (if any), against the current * SQLite database. * - * This method does not return a value. - * * @param statement Prepared statement to execute. * @param args Arguments to bind to the statement. + * @return Changes object containing the number of affected rows and last insert rowid. */ - @Polyglot public fun exec(statement: SQLiteStatement, vararg args: Any?): JSDynamicObject + @Polyglot public fun exec(statement: SQLiteStatement, vararg args: Any?): SQLiteChanges /** * ## Execute Transaction diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/sqlite/SQLiteStatement.kt b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/sqlite/SQLiteStatement.kt index 2f3695c0f3..e338317e23 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/sqlite/SQLiteStatement.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/sqlite/SQLiteStatement.kt @@ -119,8 +119,11 @@ import elide.vm.annotations.Polyglot * * Repeated calls to this method with unchanging [args] will cache the underlying rendered query, but will not cache * execution of the query (in other words, the query is executed each time [run] is called). + * + * @param args Arguments to render into the query. + * @return Changes object containing the number of affected rows and last insert rowid. */ - @Polyglot public fun run(vararg args: Any?) + @Polyglot public fun run(vararg args: Any?): SQLiteChanges /** * ## Finalize Statement