1
0
mirror of https://github.com/sqlite/sqlite.git synced 2025-07-30 19:03:16 +03:00

Do not pre-allocate sqlite3_aggregate_context() for Java UDFs, as it unduly complicates UDF initialization.

FossilOrigin-Name: e8308f0c6ec2d8999c8a2502fb130cb3501ba326f23f71f2cd8d452debae79b5
This commit is contained in:
stephan
2023-08-24 21:31:56 +00:00
parent 36018803d6
commit 0f0bf3ff9e
9 changed files with 118 additions and 107 deletions

View File

@ -1164,49 +1164,6 @@ static int S3JniAutoExtension_init(JNIEnv *const env,
return 0;
}
/*
** Requires that jCx be a Java-side sqlite3_context wrapper for pCx.
** This function calls sqlite3_aggregate_context() to allocate a tiny
** sliver of memory, the address of which is set in
** jCx->aggregateContext. The memory is only used as a key for
** mapping client-side results of aggregate result sets across
** calls to the UDF's callbacks.
**
** isFinal must be 1 for xFinal() calls and 0 for all others, the
** difference being that the xFinal() invocation will not allocate
** new memory if it was not already, resulting in a value of 0
** for jCx->aggregateContext.
**
** Returns 0 on success. Returns SQLITE_NOMEM on allocation error,
** noting that it will not allocate when isFinal is true. It returns
** SQLITE_ERROR if there's a serious internal error in dealing with
** the JNI state.
*/
static int udf_setAggregateContext(JNIEnv * env, jobject jCx,
sqlite3_context * pCx,
int isFinal){
void * pAgg;
int rc = 0;
S3JniNphClass * const pNC =
S3JniGlobal_nph_cache(env, &S3NphRefs.sqlite3_context);
if( !pNC->fidAggCtx ){
S3JniMutex_Nph_enter;
if( !pNC->fidAggCtx ){
pNC->fidAggCtx = (*env)->GetFieldID(env, pNC->klazz, "aggregateContext", "J");
EXCEPTION_IS_FATAL("Cannot get sqlite3_contex.aggregateContext member.");
}
S3JniMutex_Nph_leave;
}
pAgg = sqlite3_aggregate_context(pCx, isFinal ? 0 : sizeof(void*));
if( pAgg || isFinal ){
(*env)->SetLongField(env, jCx, pNC->fidAggCtx, (jlong)pAgg);
}else{
assert(!pAgg);
rc = SQLITE_NOMEM;
}
return rc;
}
/*
** Common init for OutputPointer_set_Int32() and friends. pRef must be
** a pointer from S3NphRefs. jOut must be an instance of that
@ -1628,6 +1585,7 @@ static int udf_report_exception(JNIEnv * const env, int translateToErr,
(*env)->ExceptionDescribe( env );
S3JniExceptionClear;
}
UNREF_L(ex);
return rc;
}
@ -1645,10 +1603,6 @@ static int udf_xFSI(sqlite3_context* const pCx, int argc,
int rc = udf_args(env, pCx, argc, argv, &args.jcx, &args.jargv);
//MARKER(("UDF::%s.%s()\n", s->zFuncName, zFuncType));
if( rc ) return rc;
if( UDF_SCALAR != s->type ){
rc = udf_setAggregateContext(env, args.jcx, pCx, 0);
}
if( 0 == rc ){
(*env)->CallVoidMethod(env, s->jObj, xMethodID, args.jcx, args.jargv);
S3JniIfThrew{
@ -1678,15 +1632,10 @@ static int udf_xFV(sqlite3_context* cx, S3JniUdf * s,
return SQLITE_NOMEM;
}
//MARKER(("UDF::%s.%s()\n", s->zFuncName, zFuncType));
if( UDF_SCALAR != s->type ){
rc = udf_setAggregateContext(env, jcx, cx, isFinal);
}
if( 0 == rc ){
(*env)->CallVoidMethod(env, s->jObj, xMethodID, jcx);
S3JniIfThrew{
rc = udf_report_exception(env, isFinal, cx, s->zFuncName,
zFuncType);
}
(*env)->CallVoidMethod(env, s->jObj, xMethodID, jcx);
S3JniIfThrew{
rc = udf_report_exception(env, isFinal, cx, s->zFuncName,
zFuncType);
}
UNREF_L(jcx);
return rc;
@ -1834,6 +1783,20 @@ WRAP_INT_SVALUE(1value_1type, sqlite3_value_type)
#undef WRAP_MUTF8_VOID
#undef WRAP_STR_STMT_INT
S3JniApi(sqlite3_aggregate_context(),jlong,1aggregate_1context)(
JniArgsEnvClass, jobject jCx, jboolean initialize
){
sqlite3_context * const pCx = PtrGet_sqlite3_context(jCx);
void * const p = pCx
? sqlite3_aggregate_context(pCx, (int)(initialize
? (int)sizeof(void*)
: 0))
: 0;
return (jlong)p / sizeof(void*);
}
/* Central auto-extension handler. */
static int s3jni_run_java_auto_extensions(sqlite3 *pDb, const char **pzErr,
const struct sqlite3_api_routines *ignored){

View File

@ -771,6 +771,14 @@ JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_init
JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_SQLite3Jni_uncacheJniEnv
(JNIEnv *, jclass);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_aggregate_context
* Signature: (Lorg/sqlite/jni/sqlite3_context;Z)J
*/
JNIEXPORT jlong JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1aggregate_1context
(JNIEnv *, jclass, jobject, jboolean);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_auto_extension

View File

@ -55,6 +55,9 @@ public abstract class SQLFunction {
Client UDFs are free to perform such mappings using custom
approaches. The provided Aggregate<T> and Window<T> classes
use this.
<p>T must be of a type which can be legally stored as a value in
java.util.HashMap<KeyType,T>.
*/
public static final class PerContextState<T> {
private final java.util.Map<Long,ValueHolder<T>> map
@ -64,20 +67,20 @@ public abstract class SQLFunction {
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 cx.getAggregateContext() 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.
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>T must be of a type which can be legally stored as a value in
java.util.HashMap<KeyType,T>.
<p>The caller is obligated to eventually call
takeAggregateState() to clear the mapping.
*/
public ValueHolder<T> getAggregateState(sqlite3_context cx, T initialValue){
ValueHolder<T> rc = map.get(cx.getAggregateContext());
if(null == rc){
map.put(cx.getAggregateContext(), rc = new ValueHolder<>(initialValue));
final Long key = cx.getAggregateContext(true);
ValueHolder<T> rc = null==key ? null : map.get(key);
if( null==rc ){
map.put(key, rc = new ValueHolder<>(initialValue));
}
return rc;
}
@ -92,7 +95,7 @@ public abstract class SQLFunction {
rows.
*/
public T takeAggregateState(sqlite3_context cx){
final ValueHolder<T> h = map.remove(cx.getAggregateContext());
final ValueHolder<T> h = map.remove(cx.getAggregateContext(false));
return null==h ? null : h.value;
}
}

View File

@ -126,19 +126,19 @@ public final class SQLite3Jni {
This will clean up any cached per-JNIEnv info. Calling into the
library will re-initialize the cache on demand.
This process does not close any databases or finalize
<p>This process does not close any databases or finalize
any prepared statements because their ownership does not depend on
a given thread. For proper library behavior, and to
avoid C-side leaks, be sure to finalize all statements and close
all databases before calling this function.
Calling this from the main application thread is not strictly
<p>Calling this from the main application thread is not strictly
required but is "polite." Additional threads must call this
before ending or they will leak cache entries in the C heap,
which in turn may keep numerous Java-side global references
active.
This routine returns false without side effects if the current
<p>This routine returns false without side effects if the current
JNIEnv is not cached, else returns true, but this information is
primarily for testing of the JNI bindings and is not information
which client-level code should use to make any informed
@ -151,22 +151,42 @@ public final class SQLite3Jni {
// alphabetized. The SQLITE_... values. on the other hand, are
// grouped by category.
/**
Functions exactly like the native form except that (A) the
returned value is only intended for use as a lookup key in a
higher-level data structure and (B) the 2nd argument is a boolean
instead of an int. If passed true, it will attempt to allocate
enough memory to use as a UDF-call-local context key. If passed
false it will not allocate any memory.
<p>It is only valid for the life of the current UDF method call
and must not be retained for later use. The return value 0
indicates an allocation error unless initialize is false, in
which case it means that the given context was never passed to
this function with a true second argument so never had to
allocate.
<p>For the JNI wrapping, the value of sz is provided for API
consistency but it is ignored unless it's 0. Results are
undefined if the value is negative.
*/
public static native long sqlite3_aggregate_context(sqlite3_context cx, boolean initialize);
/**
Functions almost as documented for the C API, with these
exceptions:
- The callback interface is is shorter because of cross-language
differences. Specifically, 3rd argument to the C auto-extension
callback interface is unnecessary here.
<p>- The callback interface is is shorter because of
cross-language differences. Specifically, 3rd argument to the C
auto-extension callback interface is unnecessary here.
The C API docs do not specifically say so, if the list of
<p>The C API docs do not specifically say so, but if the list of
auto-extensions is manipulated from an auto-extension, it is
undefined which, if any, auto-extensions will subsequently
execute for the current database.
See the AutoExtension class docs for more information.
<p>See the AutoExtension class docs for more information.
*/
public static native int sqlite3_auto_extension(@NotNull AutoExtension callback);

View File

@ -723,7 +723,9 @@ public class Tester1 implements Runnable {
SQLFunction func = new SQLFunction.Aggregate<Integer>(){
@Override
public void xStep(sqlite3_context cx, sqlite3_value[] args){
this.getAggregateState(cx, 0).value += sqlite3_value_int(args[0]);
final ValueHolder<Integer> agg = this.getAggregateState(cx, 0);
agg.value += sqlite3_value_int(args[0]);
affirm( agg == this.getAggregateState(cx, 0) );
}
@Override
public void xFinal(sqlite3_context cx){
@ -740,15 +742,19 @@ public class Tester1 implements Runnable {
int rc = sqlite3_create_function(db, "myfunc", 1, SQLITE_UTF8, func);
affirm(0 == rc);
sqlite3_stmt stmt = prepare(db, "select myfunc(a), myfunc(a+10) from t");
affirm( null != stmt );
int n = 0;
if( SQLITE_ROW == sqlite3_step(stmt) ){
final int v = sqlite3_column_int(stmt, 0);
int v = sqlite3_column_int(stmt, 0);
affirm( 6 == v );
int v2 = sqlite3_column_int(stmt, 1);
affirm( 30+v == v2 );
++n;
}
affirm( 1==n );
affirm(!xFinalNull.value);
sqlite3_reset(stmt);
// Ensure that the accumulator is reset...
// Ensure that the accumulator is reset on subsequent calls...
n = 0;
if( SQLITE_ROW == sqlite3_step(stmt) ){
final int v = sqlite3_column_int(stmt, 0);
@ -767,9 +773,9 @@ public class Tester1 implements Runnable {
affirm( 6 == c0 );
affirm( 12 == c1 );
}
sqlite3_finalize(stmt);
affirm( 1 == n );
affirm(!xFinalNull.value);
sqlite3_finalize(stmt);
execSql(db, "SELECT myfunc(1) WHERE 0");
affirm(xFinalNull.value);

View File

@ -18,10 +18,7 @@ package org.sqlite.jni;
SQL functions (a.k.a. UDFs).
*/
public final class sqlite3_context extends NativePointerHolder<sqlite3_context> {
/**
Only set by the JNI layer.
*/
private long aggregateContext = 0;
private Long aggregateContext = null;
/**
getAggregateContext() corresponds to C's
@ -32,19 +29,29 @@ public final class sqlite3_context extends NativePointerHolder<sqlite3_context>
such that all calls into those callbacks can determine which "set"
of those calls they belong to.
If this object is being used in the context of an aggregate or
<p>If the argument is true and the aggregate context has not yet
been set up, it will be initialized fetched on demand, else it
won't. The intent is that xStep(), xValue(), and xInverse()
methods pass true and xFinal() methods pass false.
<p>This function treats numeric 0 as null, always returning null instead
of 0.
<p>If this object is being used in the context of an aggregate or
window UDF, this function returns a non-0 value which is distinct
for each set of UDF callbacks from a single invocation of the
UDF, otherwise it returns 0. The returned value is only only
valid within the context of execution of a single SQL statement,
and may be re-used by future invocations of the UDF in different
SQL statements.
and must not be re-used by future invocations of the UDF in
different SQL statements.
Consider this SQL, where MYFUNC is a user-defined aggregate function:
<p>Consider this SQL, where MYFUNC is a user-defined aggregate function:
{code
SELECT MYFUNC(A), MYFUNC(B) FROM T;
}
The xStep() and xFinal() methods of the callback need to be able
<p>The xStep() and xFinal() methods of the callback need to be able
to differentiate between those two invocations in order to
perform their work properly. The value returned by
getAggregateContext() will be distinct for each of those
@ -52,14 +59,18 @@ public final class sqlite3_context extends NativePointerHolder<sqlite3_context>
key for mapping callback invocations to whatever client-defined
state is needed by the UDF.
There is one case where this will return 0 in the context of an
<p>There is one case where this will return 0 in the context of an
aggregate or window function: if the result set has no rows,
the UDF's xFinal() will be called without any other x...() members
having been called. In that one case, no aggregate context key will
have been generated. xFinal() implementations need to be prepared to
accept that condition as legal.
*/
public long getAggregateContext(){
return aggregateContext;
public synchronized Long getAggregateContext(boolean initIfNeeded){
if( aggregateContext==null ){
aggregateContext = SQLite3Jni.sqlite3_aggregate_context(this, initIfNeeded);
if( !initIfNeeded && null==aggregateContext ) aggregateContext = 0L;
}
return (null==aggregateContext || 0!=aggregateContext) ? aggregateContext : null;
}
}