mirror of
https://github.com/sqlite/sqlite.git
synced 2025-07-29 08:01:23 +03:00
JNI: add aggregate function support to the wrapper1 API.
FossilOrigin-Name: 15b28b340a5c5efdbfe3fbed16ee0b699561edaeebb77446addf2374bdf9357e
This commit is contained in:
@ -38,17 +38,6 @@ import java.util.concurrent.Future;
|
||||
@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 Tester1 implements Runnable {
|
||||
//! True when running in multi-threaded mode.
|
||||
private static boolean mtMode = false;
|
||||
|
25
ext/jni/src/org/sqlite/jni/capi/ValueHolder.java
Normal file
25
ext/jni/src/org/sqlite/jni/capi/ValueHolder.java
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
** 2023-10-16
|
||||
**
|
||||
** The author disclaims copyright to this source code. In place of
|
||||
** a legal notice, here is a blessing:
|
||||
**
|
||||
** May you do good and not evil.
|
||||
** May you find forgiveness for yourself and forgive others.
|
||||
** May you share freely, never taking more than you give.
|
||||
**
|
||||
*************************************************************************
|
||||
** This file contains a set of tests for the sqlite3 JNI bindings.
|
||||
*/
|
||||
package org.sqlite.jni.capi;
|
||||
|
||||
/**
|
||||
A helper class which simply holds a single value. Its primary use
|
||||
is for communicating values out of anonymous classes, as doing so
|
||||
requires a "final" reference.
|
||||
*/
|
||||
public class ValueHolder<T> {
|
||||
public T value;
|
||||
public ValueHolder(){}
|
||||
public ValueHolder(T v){value = v;}
|
||||
}
|
82
ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java
Normal file
82
ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
** 2023-10-16
|
||||
**
|
||||
** The author disclaims copyright to this source code. In place of
|
||||
** a legal notice, here is a blessing:
|
||||
**
|
||||
** May you do good and not evil.
|
||||
** May you find forgiveness for yourself and forgive others.
|
||||
** May you share freely, never taking more than you give.
|
||||
**
|
||||
*************************************************************************
|
||||
** This file is part of the wrapper1 interface for sqlite3.
|
||||
*/
|
||||
package org.sqlite.jni.wrapper1;
|
||||
import org.sqlite.jni.capi.CApi;
|
||||
import org.sqlite.jni.annotation.*;
|
||||
import org.sqlite.jni.capi.sqlite3_context;
|
||||
import org.sqlite.jni.capi.sqlite3_value;
|
||||
|
||||
/**
|
||||
EXPERIMENTAL/INCOMPLETE/UNTESTED
|
||||
|
||||
A SqlFunction implementation for aggregate functions. The T type
|
||||
represents the type of data accumulated by this aggregate while it
|
||||
works. e.g. a SUM()-like UDF might use Integer or Long and a
|
||||
CONCAT()-like UDF might use a StringBuilder or a List<String>.
|
||||
*/
|
||||
public abstract class AggregateFunction<T> implements SqlFunction {
|
||||
|
||||
/**
|
||||
As for the xStep() argument of the C API's
|
||||
sqlite3_create_function(). If this function throws, the
|
||||
exception is reported via sqlite3_result_error().
|
||||
*/
|
||||
public abstract void xStep(SqlFunction.Arguments args);
|
||||
|
||||
/**
|
||||
As for the xFinal() argument of the C API's
|
||||
sqlite3_create_function(). If this function throws, it is
|
||||
translated into sqlite3_result_error().
|
||||
|
||||
Note that the passed-in object will not actually contain any
|
||||
arguments for xFinal() but will contain the context object needed
|
||||
for setting the call's result or error state.
|
||||
*/
|
||||
public abstract void xFinal(SqlFunction.Arguments args);
|
||||
|
||||
/**
|
||||
Optionally override to be notified when the UDF is finalized by
|
||||
SQLite.
|
||||
*/
|
||||
public void xDestroy() {}
|
||||
|
||||
/** Per-invocation state for the UDF. */
|
||||
private final SqlFunction.PerContextState<T> map =
|
||||
new SqlFunction.PerContextState<>();
|
||||
|
||||
/**
|
||||
To be called from the implementation's xStep() method, as well
|
||||
as the xValue() and xInverse() methods of the {@link WindowFunction}
|
||||
subclass, to fetch the current per-call UDF state. On the
|
||||
first call to this method for any given sqlite3_context
|
||||
argument, the context is set to the given initial value. On all other
|
||||
calls, the 2nd argument is ignored.
|
||||
|
||||
@see SQLFunction.PerContextState#getAggregateState
|
||||
*/
|
||||
protected final ValueHolder<T> getAggregateState(SqlFunction.Arguments args, T initialValue){
|
||||
return map.getAggregateState(args, initialValue);
|
||||
}
|
||||
|
||||
/**
|
||||
To be called from the implementation's xFinal() method to fetch
|
||||
the final state of the UDF and remove its mapping.
|
||||
|
||||
see SQLFunction.PerContextState#takeAggregateState
|
||||
*/
|
||||
protected final T takeAggregateState(SqlFunction.Arguments args){
|
||||
return map.takeAggregateState(args);
|
||||
}
|
||||
|
||||
}
|
@ -13,22 +13,20 @@
|
||||
*/
|
||||
package org.sqlite.jni.wrapper1;
|
||||
import org.sqlite.jni.capi.CApi;
|
||||
import org.sqlite.jni.annotation.*;
|
||||
import org.sqlite.jni.capi.sqlite3_context;
|
||||
import org.sqlite.jni.capi.sqlite3_value;
|
||||
|
||||
/**
|
||||
EXPERIMENTAL/INCOMPLETE/UNTESTED
|
||||
Base marker interface for SQLite's three types of User-Defined SQL
|
||||
Functions (UDFs): Scalar, Aggregate, and Window functions.
|
||||
*/
|
||||
public interface SqlFunction {
|
||||
|
||||
/**
|
||||
EXPERIMENTAL/INCOMPLETE/UNTESTED. An attempt at hiding UDF-side
|
||||
uses of the sqlite3_context and sqlite3_value classes from a
|
||||
high-level wrapper. This level of indirection requires more than
|
||||
twice as much Java code (in this API, not client-side) as using
|
||||
the lower-level API. Client-side it's roughly the same amount of
|
||||
code.
|
||||
The Arguments type is an abstraction on top of the lower-level
|
||||
UDF function argument types. It provides _most_ of the functionality
|
||||
of the lower-level interface, insofar as possible without "leaking"
|
||||
those types into this API.
|
||||
*/
|
||||
public final static class Arguments implements Iterable<SqlFunction.Arguments.Arg>{
|
||||
private final sqlite3_context cx;
|
||||
@ -37,29 +35,34 @@ public interface SqlFunction {
|
||||
|
||||
/**
|
||||
Must be passed the context and arguments for the UDF call this
|
||||
object is wrapping.
|
||||
object is wrapping. Intended to be used by internal proxy
|
||||
classes which "convert" the lower-level interface into this
|
||||
package's higher-level interface, e.g. ScalarAdapter and
|
||||
AggregateAdapter.
|
||||
|
||||
Passing null for the args is equivalent to passing a length-0
|
||||
array.
|
||||
*/
|
||||
Arguments(@NotNull sqlite3_context cx, @NotNull sqlite3_value args[]){
|
||||
Arguments(sqlite3_context cx, sqlite3_value args[]){
|
||||
this.cx = cx;
|
||||
this.args = args;
|
||||
this.length = args.length;
|
||||
this.args = args==null ? new sqlite3_value[0] : args;;
|
||||
this.length = this.args.length;
|
||||
}
|
||||
|
||||
/**
|
||||
Wrapper for a single SqlFunction argument. Primarily intended
|
||||
for eventual use with the Arguments class's Iterable interface.
|
||||
for use with the Arguments class's Iterable interface.
|
||||
*/
|
||||
public final static class Arg {
|
||||
private final Arguments a;
|
||||
private final int ndx;
|
||||
/* Only for use by the Arguments class. */
|
||||
private Arg(@NotNull Arguments a, int ndx){
|
||||
private Arg(Arguments a, int ndx){
|
||||
this.a = a;
|
||||
this.ndx = ndx;
|
||||
}
|
||||
/** Returns this argument's index in its parent argument list. */
|
||||
public int getIndex(){return ndx;}
|
||||
|
||||
public int getInt(){return a.getInt(ndx);}
|
||||
public long getInt64(){return a.getInt64(ndx);}
|
||||
public double getDouble(){return a.getDouble(ndx);}
|
||||
@ -75,10 +78,9 @@ public interface SqlFunction {
|
||||
public void setAuxData(Object o){a.setAuxData(ndx, o);}
|
||||
}
|
||||
|
||||
//! Untested!
|
||||
@Override
|
||||
public java.util.Iterator<SqlFunction.Arguments.Arg> iterator(){
|
||||
Arg[] proxies = new Arg[args.length];
|
||||
final Arg[] proxies = new Arg[args.length];
|
||||
for( int i = 0; i < args.length; ++i ){
|
||||
proxies[i] = new Arg(this, i);
|
||||
}
|
||||
@ -98,6 +100,8 @@ public interface SqlFunction {
|
||||
return args[ndx];
|
||||
}
|
||||
|
||||
sqlite3_context getContext(){return cx;}
|
||||
|
||||
public int getArgCount(){ return args.length; }
|
||||
|
||||
public int getInt(int arg){return CApi.sqlite3_value_int(valueAt(arg));}
|
||||
@ -159,6 +163,73 @@ public interface SqlFunction {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
PerContextState assists aggregate and window functions in
|
||||
managing their accumulator state across calls to the UDF's
|
||||
callbacks.
|
||||
|
||||
<p>T must be of a type which can be legally stored as a value in
|
||||
java.util.HashMap<KeyType,T>.
|
||||
|
||||
<p>If a given aggregate or window function is called multiple times
|
||||
in a single SQL statement, e.g. SELECT MYFUNC(A), MYFUNC(B)...,
|
||||
then the clients need some way of knowing which call is which so
|
||||
that they can map their state between their various UDF callbacks
|
||||
and reset it via xFinal(). This class takes care of such
|
||||
mappings.
|
||||
|
||||
<p>This class works by mapping
|
||||
sqlite3_context.getAggregateContext() to a single piece of
|
||||
state, of a client-defined type (the T part of this class), which
|
||||
persists across a "matching set" of the UDF's callbacks.
|
||||
|
||||
<p>This class is a helper providing commonly-needed functionality
|
||||
- it is not required for use with aggregate or window functions.
|
||||
Client UDFs are free to perform such mappings using custom
|
||||
approaches. The provided {@link AggregateFunction} and {@link
|
||||
WindowFunction} classes use this.
|
||||
*/
|
||||
public static final class PerContextState<T> {
|
||||
private final java.util.Map<Long,ValueHolder<T>> map
|
||||
= new java.util.HashMap<>();
|
||||
|
||||
/**
|
||||
Should be called from a UDF's xStep(), xValue(), and xInverse()
|
||||
methods, passing it that method's first argument and an initial
|
||||
value for the persistent state. If there is currently no
|
||||
mapping for the given context within the map, one is created
|
||||
using the given initial value, else the existing one is used
|
||||
and the 2nd argument is ignored. It returns a ValueHolder<T>
|
||||
which can be used to modify that state directly without
|
||||
requiring that the client update the underlying map's entry.
|
||||
|
||||
<p>The caller is obligated to eventually call
|
||||
takeAggregateState() to clear the mapping.
|
||||
*/
|
||||
public ValueHolder<T> getAggregateState(SqlFunction.Arguments args, T initialValue){
|
||||
final Long key = args.getContext().getAggregateContext(true);
|
||||
ValueHolder<T> rc = null==key ? null : map.get(key);
|
||||
if( null==rc ){
|
||||
map.put(key, rc = new ValueHolder<>(initialValue));
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
/**
|
||||
Should be called from a UDF's xFinal() method and passed that
|
||||
method's first argument. This function removes the value
|
||||
associated with with the arguments' aggregate context from the
|
||||
map and returns it, returning null if no other UDF method has
|
||||
been called to set up such a mapping. The latter condition will
|
||||
be the case if a UDF is used in a statement which has no result
|
||||
rows.
|
||||
*/
|
||||
public T takeAggregateState(SqlFunction.Arguments args){
|
||||
final ValueHolder<T> h = map.remove(args.getContext().getAggregateContext(false));
|
||||
return null==h ? null : h.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Internal-use adapter for wrapping this package's ScalarFunction
|
||||
for use with the org.sqlite.jni.capi.ScalarFunction interface.
|
||||
@ -169,11 +240,57 @@ public interface SqlFunction {
|
||||
this.impl = impl;
|
||||
}
|
||||
/**
|
||||
Proxies this.f.xFunc(), adapting the call arguments to that
|
||||
function's signature.
|
||||
Proxies this.impl.xFunc(), adapting the call arguments to that
|
||||
function's signature. If the proxy throws, it's translated to
|
||||
sqlite_result_error() with the exception's message.
|
||||
*/
|
||||
public void xFunc(sqlite3_context cx, sqlite3_value[] args){
|
||||
impl.xFunc( new SqlFunction.Arguments(cx, args) );
|
||||
try{
|
||||
impl.xFunc( new SqlFunction.Arguments(cx, args) );
|
||||
}catch(Exception e){
|
||||
CApi.sqlite3_result_error(cx, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void xDestroy(){
|
||||
impl.xDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Internal-use adapter for wrapping this package's AggregateFunction
|
||||
for use with the org.sqlite.jni.capi.AggregateFunction interface.
|
||||
*/
|
||||
static final class AggregateAdapter extends org.sqlite.jni.capi.AggregateFunction {
|
||||
final AggregateFunction impl;
|
||||
AggregateAdapter(AggregateFunction impl){
|
||||
this.impl = impl;
|
||||
}
|
||||
|
||||
/**
|
||||
Proxies this.impl.xStep(), adapting the call arguments to that
|
||||
function's signature. If the proxied function throws, it is
|
||||
translated to sqlite_result_error() with the exception's
|
||||
message.
|
||||
*/
|
||||
public void xStep(sqlite3_context cx, sqlite3_value[] args){
|
||||
try{
|
||||
impl.xStep( new SqlFunction.Arguments(cx, args) );
|
||||
}catch(Exception e){
|
||||
CApi.sqlite3_result_error(cx, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
As for the xFinal() argument of the C API's sqlite3_create_function().
|
||||
If the proxied function throws, it is translated into a sqlite3_result_error().
|
||||
*/
|
||||
public void xFinal(sqlite3_context cx){
|
||||
try{
|
||||
impl.xFinal( new SqlFunction.Arguments(cx, null) );
|
||||
}catch(Exception e){
|
||||
CApi.sqlite3_result_error(cx, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void xDestroy(){
|
||||
|
@ -195,7 +195,6 @@ 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));
|
||||
@ -206,4 +205,14 @@ public final class Sqlite implements AutoCloseable {
|
||||
this.createFunction(name, nArg, CApi.SQLITE_UTF8, f);
|
||||
}
|
||||
|
||||
public void createFunction(String name, int nArg, int eTextRep, AggregateFunction f ){
|
||||
int rc = CApi.sqlite3_create_function(affirmOpen(), name, nArg, eTextRep,
|
||||
new SqlFunction.AggregateAdapter(f));
|
||||
if( 0!=rc ) throw new SqliteException(db);
|
||||
}
|
||||
|
||||
public void createFunction(String name, int nArg, AggregateFunction f){
|
||||
this.createFunction(name, nArg, CApi.SQLITE_UTF8, f);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -38,17 +38,6 @@ 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;
|
||||
@ -279,6 +268,36 @@ public class Tester2 implements Runnable {
|
||||
affirm( 1 == xDestroyCalled.value );
|
||||
}
|
||||
|
||||
void testUdfAggregate(){
|
||||
final ValueHolder<Integer> xDestroyCalled = new ValueHolder<>(0);
|
||||
final ValueHolder<Integer> vh = new ValueHolder<>(0);
|
||||
try (Sqlite db = openDb()) {
|
||||
execSql(db, "create table t(a); insert into t(a) values(1),(2),(3)");
|
||||
final AggregateFunction f = new AggregateFunction<Integer>(){
|
||||
public void xStep(SqlFunction.Arguments args){
|
||||
final ValueHolder<Integer> agg = this.getAggregateState(args, 0);
|
||||
for( SqlFunction.Arguments.Arg arg : args ){
|
||||
agg.value += arg.getInt();
|
||||
}
|
||||
}
|
||||
public void xFinal(SqlFunction.Arguments args){
|
||||
final Integer v = this.takeAggregateState(args);
|
||||
if( null==v ) args.resultNull();
|
||||
else args.resultInt(v);
|
||||
vh.value = v;
|
||||
}
|
||||
public void xDestroy(){
|
||||
++xDestroyCalled.value;
|
||||
}
|
||||
};
|
||||
db.createFunction("myagg", -1, f);
|
||||
execSql(db, "select myagg(a) from t");
|
||||
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 );
|
||||
|
25
ext/jni/src/org/sqlite/jni/wrapper1/ValueHolder.java
Normal file
25
ext/jni/src/org/sqlite/jni/wrapper1/ValueHolder.java
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
** 2023-10-16
|
||||
**
|
||||
** The author disclaims copyright to this source code. In place of
|
||||
** a legal notice, here is a blessing:
|
||||
**
|
||||
** May you do good and not evil.
|
||||
** May you find forgiveness for yourself and forgive others.
|
||||
** May you share freely, never taking more than you give.
|
||||
**
|
||||
*************************************************************************
|
||||
** This file contains a set of tests for the sqlite3 JNI bindings.
|
||||
*/
|
||||
package org.sqlite.jni.wrapper1;
|
||||
|
||||
/**
|
||||
A helper class which simply holds a single value. Its primary use
|
||||
is for communicating values out of anonymous classes, as doing so
|
||||
requires a "final" reference.
|
||||
*/
|
||||
public class ValueHolder<T> {
|
||||
public T value;
|
||||
public ValueHolder(){}
|
||||
public ValueHolder(T v){value = v;}
|
||||
}
|
Reference in New Issue
Block a user