]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
JNI: add scalar UDF support to the wrapper1 API.
authorstephan <stephan@noemail.net>
Mon, 16 Oct 2023 14:31:13 +0000 (14:31 +0000)
committerstephan <stephan@noemail.net>
Mon, 16 Oct 2023 14:31:13 +0000 (14:31 +0000)
FossilOrigin-Name: a850535766d2243d9475e1523c753615875a2da9c9d82a41a9fb61b141c6334a

ext/jni/src/org/sqlite/jni/capi/ScalarFunction.java
ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java
ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java
ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java
manifest
manifest.uuid

index f517b3610c74adda72fbee8c94bc7e1aa447db71..95541bdcbaf86b79ac34089a8840b5b5164dce65 100644 (file)
@@ -27,7 +27,7 @@ public abstract class ScalarFunction implements SQLFunction {
 
   /**
      Optionally override to be notified when the UDF is finalized by
-     SQLite. This implementation does nothing.
+     SQLite. This default implementation does nothing.
   */
   public void xDestroy() {}
 }
index 4be6dcbe23e9ff287f18be6bab8e5bdedcf64b50..6851bf83793f0f6674ce0fe05e791705f55596a6 100644 (file)
@@ -33,6 +33,7 @@ public interface SqlFunction  {
   public final static class Arguments implements Iterable<SqlFunction.Arguments.Arg>{
     private final sqlite3_context cx;
     private final sqlite3_value args[];
+    public final int length;
 
     /**
        Must be passed the context and arguments for the UDF call this
@@ -41,6 +42,7 @@ public interface SqlFunction  {
     Arguments(@NotNull sqlite3_context cx, @NotNull sqlite3_value args[]){
       this.cx = cx;
       this.args = args;
+      this.length = args.length;
     }
 
     /**
@@ -112,6 +114,11 @@ public interface SqlFunction  {
     }
 
     public int getType(int arg){return CApi.sqlite3_value_type(valueAt(arg));}
+    public int getSubtype(int arg){return CApi.sqlite3_value_subtype(valueAt(arg));}
+    public int getNumericType(int arg){return CApi.sqlite3_value_numeric_type(valueAt(arg));}
+    public int getNoChange(int arg){return CApi.sqlite3_value_nochange(valueAt(arg));}
+    public boolean getFromBind(int arg){return CApi.sqlite3_value_frombind(valueAt(arg));}
+    public int getEncoding(int arg){return CApi.sqlite3_value_encoding(valueAt(arg));}
 
     public void resultInt(int v){ CApi.sqlite3_result_int(cx, v); }
     public void resultInt64(long v){ CApi.sqlite3_result_int64(cx, v); }
@@ -121,6 +128,7 @@ public interface SqlFunction  {
     public void resultErrorTooBig(){CApi.sqlite3_result_error_toobig(cx);}
     public void resultErrorCode(int rc){CApi.sqlite3_result_error_code(cx, rc);}
     public void resultObject(Object o){CApi.sqlite3_result_java_object(cx, o);}
+    public void resultNull(){CApi.sqlite3_result_null(cx);}
     public void resultArg(int argNdx){CApi.sqlite3_result_value(cx, valueAt(argNdx));}
     public void resultZeroBlob(long n){
       // Throw on error? If n is too big,
@@ -150,4 +158,27 @@ public interface SqlFunction  {
       return CApi.sqlite3_get_auxdata(cx, arg);
     }
   }
+
+  /**
+     Internal-use adapter for wrapping this package's ScalarFunction
+     for use with the org.sqlite.jni.capi.ScalarFunction interface.
+  */
+  static final class ScalarAdapter extends org.sqlite.jni.capi.ScalarFunction {
+    final ScalarFunction impl;
+    ScalarAdapter(ScalarFunction impl){
+      this.impl = impl;
+    }
+    /**
+       Proxies this.f.xFunc(), adapting the call arguments to that
+       function's signature.
+    */
+    public void xFunc(sqlite3_context cx, sqlite3_value[] args){
+      impl.xFunc( new SqlFunction.Arguments(cx, args) );
+    }
+
+    public void xDestroy(){
+      impl.xDestroy();
+    }
+  }
+
 }
index b6a4bda108e2aa843166f5a99da42c975d43c191..7321d7bb36a27b1f3a0411870901d6b8df503f34 100644 (file)
@@ -14,6 +14,7 @@
 package org.sqlite.jni.wrapper1;
 import java.nio.charset.StandardCharsets;
 import static org.sqlite.jni.capi.CApi.*;
+import org.sqlite.jni.capi.CApi;
 import org.sqlite.jni.capi.sqlite3;
 import org.sqlite.jni.capi.sqlite3_stmt;
 import org.sqlite.jni.capi.OutputPointer;
@@ -71,14 +72,16 @@ public final class Sqlite implements AutoCloseable  {
 
   /**
      Returns this object's underlying native db handle, or null if
-     this instance has been closed.
+     this instance has been closed. This is very specifically not
+     public.
   */
   sqlite3 nativeHandle(){ return this.db; }
 
-  private void affirmOpen(){
+  private sqlite3 affirmOpen(){
     if( null==db || 0==db.getNativePointer() ){
       throw new IllegalArgumentException("This database instance is closed.");
     }
+    return this.db;
   }
 
   // private byte[] stringToUtf8(String s){
@@ -91,6 +94,10 @@ public final class Sqlite implements AutoCloseable  {
     }
   }
 
+  /**
+     Corresponds to the sqlite3_stmt class. Use Sqlite.prepare() to
+     create new instances.
+  */
   public final class Stmt implements AutoCloseable {
     private Sqlite _db = null;
     private sqlite3_stmt stmt = null;
@@ -114,7 +121,7 @@ public final class Sqlite implements AutoCloseable  {
     /**
        Corresponds to sqlite3_finalize(), but we cannot override the
        name finalize() here because this one requires a different
-       signature. We do not throw on error here because "destructors
+       signature. It does not throw on error here because "destructors
        do not throw." If it returns non-0, the object is still
        finalized.
     */
@@ -178,9 +185,8 @@ public final class Sqlite implements AutoCloseable  {
      rather than the sqlite3_stmt class.
   */
   public Stmt prepare(String sql, int prepFlags){
-    affirmOpen();
     final OutputPointer.sqlite3_stmt out = new OutputPointer.sqlite3_stmt();
-    final int rc = sqlite3_prepare_v3(this.db, sql, prepFlags, out);
+    final int rc = sqlite3_prepare_v3(affirmOpen(), sql, prepFlags, out);
     affirmRcOk(rc);
     return new Stmt(this, out.take());
   }
@@ -189,4 +195,15 @@ public final class Sqlite implements AutoCloseable  {
     return prepare(sql, 0);
   }
 
+
+  public void createFunction(String name, int nArg, int eTextRep, ScalarFunction f ){
+    int rc = CApi.sqlite3_create_function(affirmOpen(), name, nArg, eTextRep,
+                                           new SqlFunction.ScalarAdapter(f));
+    if( 0!=rc ) throw new SqliteException(db);
+  }
+
+  public void createFunction(String name, int nArg, ScalarFunction f){
+    this.createFunction(name, nArg, CApi.SQLITE_UTF8, f);
+  }
+
 }
index 73f07b1561fe56947e3c65a3087171f418d4d11d..07a43b63dd01feda329a25fea584f55abd3ba651 100644 (file)
@@ -38,6 +38,17 @@ import org.sqlite.jni.capi.*;
 @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
 @interface SingleThreadOnly{}
 
+/**
+   A helper class which simply holds a single value. Its current use
+   is for communicating values out of anonymous classes, as doing so
+   requires a "final" reference.
+*/
+class ValueHolder<T> {
+  public T value;
+  public ValueHolder(){}
+  public ValueHolder(T v){value = v;}
+}
+
 public class Tester2 implements Runnable {
   //! True when running in multi-threaded mode.
   private static boolean mtMode = false;
@@ -124,6 +135,56 @@ public class Tester2 implements Runnable {
     affirm(v, "Affirmation failed.");
   }
 
+
+  public static void execSql(Sqlite db, String[] sql){
+    execSql(db, String.join("", sql));
+  }
+
+  public static int execSql(Sqlite dbw, boolean throwOnError, String sql){
+    final sqlite3 db = dbw.nativeHandle();
+    OutputPointer.Int32 oTail = new OutputPointer.Int32();
+    final byte[] sqlUtf8 = sql.getBytes(StandardCharsets.UTF_8);
+    int pos = 0, n = 1;
+    byte[] sqlChunk = sqlUtf8;
+    int rc = 0;
+    sqlite3_stmt stmt = null;
+    final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
+    while(pos < sqlChunk.length){
+      if(pos > 0){
+        sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
+                                      sqlChunk.length);
+      }
+      if( 0==sqlChunk.length ) break;
+      rc = CApi.sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
+      if(throwOnError) affirm(0 == rc);
+      else if( 0!=rc ) break;
+      pos = oTail.value;
+      stmt = outStmt.take();
+      if( null == stmt ){
+        // empty statement was parsed.
+        continue;
+      }
+      affirm(0 != stmt.getNativePointer());
+      while( CApi.SQLITE_ROW == (rc = CApi.sqlite3_step(stmt)) ){
+      }
+      CApi.sqlite3_finalize(stmt);
+      affirm(0 == stmt.getNativePointer());
+      if(0!=rc && CApi.SQLITE_ROW!=rc && CApi.SQLITE_DONE!=rc){
+        break;
+      }
+    }
+    CApi.sqlite3_finalize(stmt);
+    if(CApi.SQLITE_ROW==rc || CApi.SQLITE_DONE==rc) rc = 0;
+    if( 0!=rc && throwOnError){
+      throw new SqliteException(db);
+    }
+    return rc;
+  }
+
+  static void execSql(Sqlite db, String sql){
+    execSql(db, true, sql);
+  }
+
   @SingleThreadOnly /* because it's thread-agnostic */
   private void test1(){
     affirm(CApi.sqlite3_libversion_number() == CApi.SQLITE_VERSION_NUMBER);
@@ -144,9 +205,11 @@ public class Tester2 implements Runnable {
   }
 
   Sqlite openDb(String name){
-    return Sqlite.open(name, CApi.SQLITE_OPEN_READWRITE|
-                       CApi.SQLITE_OPEN_CREATE|
-                       CApi.SQLITE_OPEN_EXRESCODE);
+    final Sqlite db = Sqlite.open(name, CApi.SQLITE_OPEN_READWRITE|
+                                  CApi.SQLITE_OPEN_CREATE|
+                                  CApi.SQLITE_OPEN_EXRESCODE);
+    ++metrics.dbOpen;
+    return db;
   }
 
   Sqlite openDb(){ return openDb(":memory:"); }
@@ -190,6 +253,32 @@ public class Tester2 implements Runnable {
     }
   }
 
+  void testUdfScalar(){
+    final ValueHolder<Integer> xDestroyCalled = new ValueHolder<>(0);
+    try (Sqlite db = openDb()) {
+      execSql(db, "create table t(a); insert into t(a) values(1),(2),(3)");
+      final ValueHolder<Integer> vh = new ValueHolder<>(0);
+      final ScalarFunction f = new ScalarFunction(){
+          public void xFunc(SqlFunction.Arguments args){
+            for( SqlFunction.Arguments.Arg arg : args ){
+              vh.value += arg.getInt();
+            }
+          }
+          public void xDestroy(){
+            ++xDestroyCalled.value;
+          }
+        };
+      db.createFunction("myfunc", -1, f);
+      execSql(db, "select myfunc(1,2,3)");
+      affirm( 6 == vh.value );
+      vh.value = 0;
+      execSql(db, "select myfunc(-1,-2,-3)");
+      affirm( -6 == vh.value );
+      affirm( 0 == xDestroyCalled.value );
+    }
+    affirm( 1 == xDestroyCalled.value );
+  }
+
   private void runTests(boolean fromThread) throws Exception {
     List<java.lang.reflect.Method> mlist = testMethods;
     affirm( null!=mlist );
index 978dd1cded6ec208731f1c092cf39a30c59987aa..1fb9230659f8b4e7a67196caef77803d07daadb4 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C JNI:\sinitial\sdraft\s(untested\s-\srequires\smore\sinfrastructure\sfirst)\sof\sa\sUDF\sargument/result-handling\sinterface\swhich\scompletely\shides\sthe\sC-style\sAPI\sfrom\sthe\sclient.
-D 2023-10-16T13:04:42.394
+C JNI:\sadd\sscalar\sUDF\ssupport\sto\sthe\swrapper1\sAPI.
+D 2023-10-16T14:31:13.824
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -264,7 +264,7 @@ F ext/jni/src/org/sqlite/jni/capi/ResultCode.java 8141171f1bcf9f46eef303b9d3c5dc
 F ext/jni/src/org/sqlite/jni/capi/RollbackHookCallback.java 105e324d09c207100485e7667ad172e64322c62426bb49b547e9b0dc9c33f5f0
 F ext/jni/src/org/sqlite/jni/capi/SQLFunction.java fef556adbc3624292423083a648bdf97fa8a4f6b3b6577c9660dd7bd6a6d3c4a
 F ext/jni/src/org/sqlite/jni/capi/SQLTester.java 09bee15aa0eedac68d767ae21d9a6a62a31ade59182a3ccbf036d6463d9e30b1
-F ext/jni/src/org/sqlite/jni/capi/ScalarFunction.java dee85ef2650a9c95067f5d55bd6e290e0404e6643a5d115d1a1533df21f9b5c8
+F ext/jni/src/org/sqlite/jni/capi/ScalarFunction.java 93b9700fca4c68075ccab12fe0fbbc76c91cafc9f368e835b9bd7cd7732c8615
 F ext/jni/src/org/sqlite/jni/capi/TableColumnMetadata.java addf120e0e76e5be1ff2260daa7ce305ff9b5fafd64153a7a28e9d8f000a815f
 F ext/jni/src/org/sqlite/jni/capi/Tester1.java 8aacea90b0eed6e4e801cfba2515a66b5d602e124f1ba68fe3d2f0aa98f0f443
 F ext/jni/src/org/sqlite/jni/capi/TraceV2Callback.java 0a25e117a0daae3394a77f24713e36d7b44c67d6e6d30e9e1d56a63442eef723
@@ -289,10 +289,10 @@ F ext/jni/src/org/sqlite/jni/fts5/fts5_api.java a8e88c3783d21cec51b0748568a96653
 F ext/jni/src/org/sqlite/jni/fts5/fts5_extension_function.java 9e2b954d210d572552b28aca523b272fae14bd41e318921b22f65b728d5bf978
 F ext/jni/src/org/sqlite/jni/fts5/fts5_tokenizer.java 92bdaa3893bd684533004d64ade23d329843f809cd0d0f4f1a2856da6e6b4d90
 F ext/jni/src/org/sqlite/jni/test-script-interpreter.md f9f25126127045d051e918fe59004a1485311c50a13edbf18c79a6ff9160030e
-F ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java 40a9f4f8a7a72b90b12baa82d26ba16376a5758009739b069c1863201770e816
-F ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java 2bc90edc4c25225e018ed600b5eff43ba0485be85db08f8b6b35246372fdac20
+F ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java 8b422ec8a2e922c1c21db549e68e0eb93078d2c4d341354043975e111a43b10d
+F ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java 7826de9bea3102d8a2ecaef3cc84480d8d6f6bc617c531d2078b419913c866fd
 F ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java 1386f7b753134fc12253ce2fbbc448ba8c970567fac01a3356cb672e14408d73
-F ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java aee8301f92256ab8572043cf5de2a35afda057d2a6ff09970a2f84a90305471e
+F ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java 3dccdb259bd1d737c6d104bdf488fb489063b40a113c03b311284e0287d0d5b7
 F ext/jni/src/tests/000-000-sanity.test c3427a0e0ac84d7cbe4c95fdc1cd4b61f9ddcf43443408f3000139478c4dc745
 F ext/jni/src/tests/000-001-ignored.test e17e874c6ab3c437f1293d88093cf06286083b65bf162317f91bbfd92f961b70
 F ext/jni/src/tests/900-001-fts.test bf0ce17a8d082773450e91f2388f5bbb2dfa316d0b676c313c637a91198090f0
@@ -2129,8 +2129,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
 F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P abc82bf4b800dde1b6e6172c7be816edb391fdbed5dbd2749f54623fdf3bf8e6
-R 287c21329f3772a974832101be3bffee
+P 43b10a5cf9cb8be53d62914f340d533e60a70bf4caa8b9b91c0f867fa0f70493
+R b476ec63940a46ee7b1e2c0e07bbcf7b
 U stephan
-Z a4e77b564ffa0b3970c5e65a1253c53d
+Z a39099e216c3c59927f5c5a18a7e93ef
 # Remove this line to create a well-formed Fossil manifest.
index 2eac8e0f45e19c80cec8a423c1f7cf3799b83f22..0ec7f3375d42053fe61028327768eab1647b8990 100644 (file)
@@ -1 +1 @@
-43b10a5cf9cb8be53d62914f340d533e60a70bf4caa8b9b91c0f867fa0f70493
\ No newline at end of file
+a850535766d2243d9475e1523c753615875a2da9c9d82a41a9fb61b141c6334a
\ No newline at end of file