JNI: add scalar UDF support to the wrapper1 API.

FossilOrigin-Name: a850535766d2243d9475e1523c753615875a2da9c9d82a41a9fb61b141c6334a
This commit is contained in:
stephan 2023-10-16 14:31:13 +00:00
parent f2d7e961d9
commit 626d0a9fda
6 changed files with 156 additions and 19 deletions

View 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() {}
}

View 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();
}
}
}

View 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);
}
}

View 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 );

View File

@ -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.

View File

@ -1 +1 @@
43b10a5cf9cb8be53d62914f340d533e60a70bf4caa8b9b91c0f867fa0f70493
a850535766d2243d9475e1523c753615875a2da9c9d82a41a9fb61b141c6334a