aboutsummaryrefslogtreecommitdiff
path: root/ext/jni/src
diff options
context:
space:
mode:
Diffstat (limited to 'ext/jni/src')
-rw-r--r--ext/jni/src/c/sqlite3-jni.c63
-rw-r--r--ext/jni/src/c/sqlite3-jni.h32
-rw-r--r--ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java70
-rw-r--r--ext/jni/src/org/sqlite/jni/capi/CApi.java53
-rw-r--r--ext/jni/src/org/sqlite/jni/capi/SQLFunction.java67
-rw-r--r--ext/jni/src/org/sqlite/jni/capi/Tester1.java13
-rw-r--r--ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java70
-rw-r--r--ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java243
-rw-r--r--ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java526
-rw-r--r--ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java31
-rw-r--r--ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java152
-rw-r--r--ext/jni/src/org/sqlite/jni/wrapper1/WindowFunction.java46
12 files changed, 1040 insertions, 326 deletions
diff --git a/ext/jni/src/c/sqlite3-jni.c b/ext/jni/src/c/sqlite3-jni.c
index 245ce4f9e..6d54391e1 100644
--- a/ext/jni/src/c/sqlite3-jni.c
+++ b/ext/jni/src/c/sqlite3-jni.c
@@ -91,12 +91,6 @@
#endif
/**********************************************************************/
-/* SQLITE_M... */
-#ifndef SQLITE_MAX_ALLOCATION_SIZE
-# define SQLITE_MAX_ALLOCATION_SIZE 0x1fffffff
-#endif
-
-/**********************************************************************/
/* SQLITE_O... */
#ifndef SQLITE_OMIT_DEPRECATED
# define SQLITE_OMIT_DEPRECATED 1
@@ -1670,8 +1664,9 @@ static int encodingTypeIsValid(int eTextRep){
}
}
-/* For use with sqlite3_result/value_pointer() */
-static const char * const ResultJavaValuePtrStr = "org.sqlite.jni.capi.ResultJavaVal";
+/* For use with sqlite3_result_pointer(), sqlite3_value_pointer(),
+ sqlite3_bind_java_object(), and sqlite3_column_java_object(). */
+static const char * const s3jni__value_jref_key = "org.sqlite.jni.capi.ResultJavaVal";
/*
** If v is not NULL, it must be a jobject global reference. Its
@@ -2181,7 +2176,9 @@ S3JniApi(sqlite3_aggregate_context(),jlong,1aggregate_1context)(
return S3JniCast_P2L(p);
}
-/* Central auto-extension handler. */
+/*
+** Central auto-extension runner for auto-extensions created in Java.
+*/
static int s3jni_run_java_auto_extensions(sqlite3 *pDb, const char **pzErr,
const struct sqlite3_api_routines *ignored){
int rc = 0;
@@ -2418,7 +2415,7 @@ S3JniApi(sqlite3_bind_java_object(),jint,1bind_1java_1object)(
if(pStmt){
jobject const rv = S3JniRefGlobal(val);
if( rv ){
- rc = sqlite3_bind_pointer(pStmt, ndx, rv, ResultJavaValuePtrStr,
+ rc = sqlite3_bind_pointer(pStmt, ndx, rv, s3jni__value_jref_key,
S3Jni_jobject_finalizer);
}else if(val){
rc = SQLITE_NOMEM;
@@ -2870,6 +2867,26 @@ S3JniApi(sqlite3_column_int64(),jlong,1column_1int64)(
return (jlong)sqlite3_column_int64(PtrGet_sqlite3_stmt(jpStmt), (int)ndx);
}
+S3JniApi(sqlite3_column_java_object(),jobject,1column_1java_1object)(
+ JniArgsEnvClass, jlong jpStmt, jint ndx
+){
+ sqlite3_stmt * const stmt = S3JniLongPtr_sqlite3_stmt(jpStmt);
+ jobject rv = 0;
+ if( stmt ){
+ sqlite3 * const db = sqlite3_db_handle(stmt);
+ sqlite3_value * sv;
+ sqlite3_mutex_enter(sqlite3_db_mutex(db));
+ sv = sqlite3_column_value(stmt, (int)ndx);
+ if( sv ){
+ rv = S3JniRefLocal(
+ sqlite3_value_pointer(sv, s3jni__value_jref_key)
+ );
+ }
+ sqlite3_mutex_leave(sqlite3_db_mutex(db));
+ }
+ return rv;
+}
+
S3JniApi(sqlite3_column_text(),jbyteArray,1column_1text)(
JniArgsEnvClass, jobject jpStmt, jint ndx
){
@@ -3516,9 +3533,16 @@ S3JniApi(sqlite3_errmsg(),jstring,1errmsg)(
S3JniApi(sqlite3_errstr(),jstring,1errstr)(
JniArgsEnvClass, jint rcCode
){
- jstring const rv = (*env)->NewStringUTF(env, sqlite3_errstr((int)rcCode))
- /* We know these values to be plain ASCII, so pose no MUTF-8
- ** incompatibility */;
+ jstring rv;
+ const char * z = sqlite3_errstr((int)rcCode);
+ if( !z ){
+ /* This hypothetically cannot happen, but we'll behave like the
+ low-level library would in such a case... */
+ z = "unknown error";
+ }
+ rv = (*env)->NewStringUTF(env, z)
+ /* We know these values to be plain ASCII, so pose no MUTF-8
+ ** incompatibility */;
s3jni_oom_check( rv );
return rv;
}
@@ -4351,7 +4375,7 @@ S3JniApi(sqlite3_result_java_object(),void,1result_1java_1object)(
jobject const rjv = S3JniRefGlobal(v);
if( rjv ){
sqlite3_result_pointer(pCx, rjv,
- ResultJavaValuePtrStr, S3Jni_jobject_finalizer);
+ s3jni__value_jref_key, S3Jni_jobject_finalizer);
}else{
sqlite3_result_error_nomem(PtrGet_sqlite3_context(jpCx));
}
@@ -4366,6 +4390,13 @@ S3JniApi(sqlite3_result_null(),void,1result_1null)(
sqlite3_result_null(PtrGet_sqlite3_context(jpCx));
}
+S3JniApi(sqlite3_result_subtype(),void,1result_1subtype)(
+ JniArgsEnvClass, jobject jpCx, jint v
+){
+ sqlite3_result_subtype(PtrGet_sqlite3_context(jpCx), (unsigned int)v);
+}
+
+
S3JniApi(sqlite3_result_text(),void,1result_1text)(
JniArgsEnvClass, jobject jpCx, jbyteArray jBa, jint nMax
){
@@ -4594,7 +4625,7 @@ static int s3jni_strlike_glob(int isLike, JNIEnv *const env,
jbyteArray baG, jbyteArray baT, jint escLike){
int rc = 0;
jbyte * const pG = s3jni_jbyteArray_bytes(baG);
- jbyte * const pT = pG ? s3jni_jbyteArray_bytes(baT) : 0;
+ jbyte * const pT = s3jni_jbyteArray_bytes(baT);
/* Note that we're relying on the byte arrays having been
NUL-terminated on the Java side. */
@@ -4889,7 +4920,7 @@ S3JniApi(sqlite3_value_java_object(),jobject,1value_1java_1object)(
){
sqlite3_value * const sv = S3JniLongPtr_sqlite3_value(jpSVal);
return sv
- ? sqlite3_value_pointer(sv, ResultJavaValuePtrStr)
+ ? sqlite3_value_pointer(sv, s3jni__value_jref_key)
: 0;
}
diff --git a/ext/jni/src/c/sqlite3-jni.h b/ext/jni/src/c/sqlite3-jni.h
index bf6df7ac9..e4393ddd8 100644
--- a/ext/jni/src/c/sqlite3-jni.h
+++ b/ext/jni/src/c/sqlite3-jni.h
@@ -1121,6 +1121,14 @@ JNIEXPORT jlong JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1int64
/*
* Class: org_sqlite_jni_capi_CApi
+ * Method: sqlite3_column_java_object
+ * Signature: (JI)Ljava/lang/Object;
+ */
+JNIEXPORT jobject JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1column_1java_1object
+ (JNIEnv *, jclass, jlong, jint);
+
+/*
+ * Class: org_sqlite_jni_capi_CApi
* Method: sqlite3_column_name
* Signature: (JI)Ljava/lang/String;
*/
@@ -1681,14 +1689,6 @@ JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1error_1cod
/*
* Class: org_sqlite_jni_capi_CApi
- * Method: sqlite3_result_null
- * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;)V
- */
-JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1null
- (JNIEnv *, jclass, jobject);
-
-/*
- * Class: org_sqlite_jni_capi_CApi
* Method: sqlite3_result_int
* Signature: (Lorg/sqlite/jni/capi/sqlite3_context;I)V
*/
@@ -1713,6 +1713,22 @@ JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1java_1obje
/*
* Class: org_sqlite_jni_capi_CApi
+ * Method: sqlite3_result_null
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1null
+ (JNIEnv *, jclass, jobject);
+
+/*
+ * Class: org_sqlite_jni_capi_CApi
+ * Method: sqlite3_result_subtype
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_context;I)V
+ */
+JNIEXPORT void JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1result_1subtype
+ (JNIEnv *, jclass, jobject, jint);
+
+/*
+ * Class: org_sqlite_jni_capi_CApi
* Method: sqlite3_result_value
* Signature: (Lorg/sqlite/jni/capi/sqlite3_context;Lorg/sqlite/jni/capi/sqlite3_value;)V
*/
diff --git a/ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java b/ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java
index 89c4f2742..1fa6c6b80 100644
--- a/ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java
+++ b/ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java
@@ -42,9 +42,75 @@ public abstract class AggregateFunction<T> implements SQLFunction {
*/
public void xDestroy() {}
+ /**
+ 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(sqlite3_context cx, T 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;
+ }
+
+ /**
+ Should be called from a UDF's xFinal() method and passed that
+ method's first argument. This function removes the value
+ associated with cx.getAggregateContext() 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(sqlite3_context cx){
+ final ValueHolder<T> h = map.remove(cx.getAggregateContext(false));
+ return null==h ? null : h.value;
+ }
+ }
+
/** Per-invocation state for the UDF. */
- private final SQLFunction.PerContextState<T> map =
- new SQLFunction.PerContextState<>();
+ private final PerContextState<T> map = new PerContextState<>();
/**
To be called from the implementation's xStep() method, as well
diff --git a/ext/jni/src/org/sqlite/jni/capi/CApi.java b/ext/jni/src/org/sqlite/jni/capi/CApi.java
index 302cdb760..6eeeb29a2 100644
--- a/ext/jni/src/org/sqlite/jni/capi/CApi.java
+++ b/ext/jni/src/org/sqlite/jni/capi/CApi.java
@@ -32,15 +32,6 @@ import java.util.Arrays;
<p>The C-side part can be found in sqlite3-jni.c.
- <p>This class is package-private in order to keep Java clients from
- having direct access to the low-level C-style APIs, a design
- decision made by Java developers based on the C-style API being
- riddled with opportunities for Java developers to proverbially shoot
- themselves in the foot with. Third-party copies of this code may
- eliminate that guard by simply changing this class from
- package-private to public. Its methods which are intended to be
- exposed that way are all public.
-
<p>Only functions which materially differ from their C counterparts
are documented here, and only those material differences are
documented. The C documentation is otherwise applicable for these
@@ -595,6 +586,36 @@ public final class CApi {
@NotNull sqlite3_stmt stmt, int ndx
);
+ static native Object sqlite3_column_java_object(
+ @NotNull long ptrToStmt, int ndx
+ );
+
+ /**
+ If the given result column was bound with
+ sqlite3_bind_java_object() or sqlite3_result_java_object() then
+ that object is returned, else null is returned. This routine
+ requires locking the owning database's mutex briefly in order to
+ extract the object in a thread-safe way.
+ */
+ public static Object sqlite3_column_java_object(
+ @NotNull sqlite3_stmt stmt, int ndx
+ ){
+ return sqlite3_column_java_object( stmt.getNativePointer(), ndx );
+ }
+
+ /**
+ If the two-parameter overload of sqlite3_column_java_object()
+ returns non-null and the returned value is an instance of T then
+ that object is returned, else null is returned.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T sqlite3_column_java_object(
+ @NotNull sqlite3_stmt stmt, int ndx, @NotNull Class<T> type
+ ){
+ final Object o = sqlite3_column_java_object(stmt, ndx);
+ return type.isInstance(o) ? (T)o : null;
+ }
+
static native String sqlite3_column_name(@NotNull long ptrToStmt, int ndx);
public static String sqlite3_column_name(@NotNull sqlite3_stmt stmt, int ndx){
@@ -1432,10 +1453,6 @@ public final class CApi {
@NotNull sqlite3_context cx, int c
);
- public static native void sqlite3_result_null(
- @NotNull sqlite3_context cx
- );
-
public static native void sqlite3_result_int(
@NotNull sqlite3_context cx, int v
);
@@ -1464,6 +1481,10 @@ public final class CApi {
@NotNull sqlite3_context cx, @NotNull Object o
);
+ public static native void sqlite3_result_null(
+ @NotNull sqlite3_context cx
+ );
+
public static void sqlite3_result_set(
@NotNull sqlite3_context cx, @NotNull Boolean v
){
@@ -1524,6 +1545,10 @@ public final class CApi {
else sqlite3_result_blob(cx, blob, blob.length);
}
+ public static native void sqlite3_result_subtype(
+ @NotNull sqlite3_context cx, int val
+ );
+
public static native void sqlite3_result_value(
@NotNull sqlite3_context cx, @NotNull sqlite3_value v
);
@@ -1938,7 +1963,7 @@ public final class CApi {
given Class, else it returns null.
*/
@SuppressWarnings("unchecked")
- public static <T> T sqlite3_value_java_casted(@NotNull sqlite3_value v,
+ public static <T> T sqlite3_value_java_object(@NotNull sqlite3_value v,
@NotNull Class<T> type){
final Object o = sqlite3_value_java_object(v);
return type.isInstance(o) ? (T)o : null;
diff --git a/ext/jni/src/org/sqlite/jni/capi/SQLFunction.java b/ext/jni/src/org/sqlite/jni/capi/SQLFunction.java
index 4806e2fc0..7ad1381a7 100644
--- a/ext/jni/src/org/sqlite/jni/capi/SQLFunction.java
+++ b/ext/jni/src/org/sqlite/jni/capi/SQLFunction.java
@@ -33,71 +33,4 @@ package org.sqlite.jni.capi;
*/
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(sqlite3_context cx, T 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;
- }
-
- /**
- Should be called from a UDF's xFinal() method and passed that
- method's first argument. This function removes the value
- associated with cx.getAggregateContext() 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(sqlite3_context cx){
- final ValueHolder<T> h = map.remove(cx.getAggregateContext(false));
- return null==h ? null : h.value;
- }
- }
-
}
diff --git a/ext/jni/src/org/sqlite/jni/capi/Tester1.java b/ext/jni/src/org/sqlite/jni/capi/Tester1.java
index 6fb28e65b..465718565 100644
--- a/ext/jni/src/org/sqlite/jni/capi/Tester1.java
+++ b/ext/jni/src/org/sqlite/jni/capi/Tester1.java
@@ -803,13 +803,17 @@ public class Tester1 implements Runnable {
affirm( 0==rc );
int n = 0;
if( SQLITE_ROW == sqlite3_step(stmt) ){
+ affirm( testResult.value == sqlite3_column_java_object(stmt, 0) );
+ affirm( testResult.value == sqlite3_column_java_object(stmt, 0, sqlite3.class) );
+ affirm( null == sqlite3_column_java_object(stmt, 0, sqlite3_stmt.class) );
+ affirm( null == sqlite3_column_java_object(stmt,1) );
final sqlite3_value v = sqlite3_column_value(stmt, 0);
affirm( testResult.value == sqlite3_value_java_object(v) );
- affirm( testResult.value == sqlite3_value_java_casted(v, sqlite3.class) );
+ affirm( testResult.value == sqlite3_value_java_object(v, sqlite3.class) );
affirm( testResult.value ==
- sqlite3_value_java_casted(v, testResult.value.getClass()) );
- affirm( testResult.value == sqlite3_value_java_casted(v, Object.class) );
- affirm( null == sqlite3_value_java_casted(v, String.class) );
+ sqlite3_value_java_object(v, testResult.value.getClass()) );
+ affirm( testResult.value == sqlite3_value_java_object(v, Object.class) );
+ affirm( null == sqlite3_value_java_object(v, String.class) );
++n;
}
sqlite3_finalize(stmt);
@@ -925,7 +929,6 @@ public class Tester1 implements Runnable {
"ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING"+
") AS sum_y "+
"FROM twin ORDER BY x;");
- affirm( 0 == rc );
int n = 0;
while( SQLITE_ROW == sqlite3_step(stmt) ){
final String s = sqlite3_column_text16(stmt, 0);
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java b/ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java
index 173d775e6..6a38d4b53 100644
--- a/ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java
@@ -51,9 +51,75 @@ public abstract class AggregateFunction<T> implements SqlFunction {
*/
public void xDestroy() {}
+ /**
+ 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;
+ }
+ }
+
/** Per-invocation state for the UDF. */
- private final SqlFunction.PerContextState<T> map =
- new SqlFunction.PerContextState<>();
+ private final PerContextState<T> map = new PerContextState<>();
/**
To be called from the implementation's xStep() method, as well
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java b/ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java
index d6acda5aa..2d0ebfaf3 100644
--- a/ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java
@@ -22,6 +22,17 @@ import org.sqlite.jni.capi.sqlite3_value;
*/
public interface SqlFunction {
+ public static final int DETERMINISTIC = CApi.SQLITE_DETERMINISTIC;
+ public static final int INNOCUOUS = CApi.SQLITE_INNOCUOUS;
+ public static final int DIRECTONLY = CApi.SQLITE_DIRECTONLY;
+ public static final int UTF8 = CApi.SQLITE_UTF8;
+ public static final int UTF16 = CApi.SQLITE_UTF16;
+ // /**
+ // For Window functions only and is not currently bound because
+ // doing so may require exposing sqlite3_value for effective use.
+ // */
+ // public static final int SUBTYPE = CApi.SQLITE_SUBTYPE;
+
/**
The Arguments type is an abstraction on top of the lower-level
UDF function argument types. It provides _most_ of the functionality
@@ -50,44 +61,6 @@ public interface SqlFunction {
}
/**
- Wrapper for a single SqlFunction argument. Primarily intended
- 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(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);}
- public byte[] getBlob(){return a.getBlob(ndx);}
- public byte[] getText(){return a.getText(ndx);}
- public String getText16(){return a.getText16(ndx);}
- public int getBytes(){return a.getBytes(ndx);}
- public int getBytes16(){return a.getBytes16(ndx);}
- public Object getObject(){return a.getObject(ndx);}
- public <T> T getObjectCasted(Class<T> type){ return a.getObjectCasted(ndx, type); }
- public int getType(){return a.getType(ndx);}
- public Object getAuxData(){return a.getAuxData(ndx);}
- public void setAuxData(Object o){a.setAuxData(ndx, o);}
- }
-
- @Override
- public java.util.Iterator<SqlFunction.Arguments.Arg> iterator(){
- final Arg[] proxies = new Arg[args.length];
- for( int i = 0; i < args.length; ++i ){
- proxies[i] = new Arg(this, i);
- }
- return java.util.Arrays.stream(proxies).iterator();
- }
-
- /**
Returns the sqlite3_value at the given argument index or throws
an IllegalArgumentException exception if ndx is out of range.
*/
@@ -100,29 +73,30 @@ public interface SqlFunction {
return args[ndx];
}
+ //! Returns the underlying sqlite3_context for these arguments.
sqlite3_context getContext(){return cx;}
public int getArgCount(){ return args.length; }
- public int getInt(int arg){return CApi.sqlite3_value_int(valueAt(arg));}
- public long getInt64(int arg){return CApi.sqlite3_value_int64(valueAt(arg));}
- public double getDouble(int arg){return CApi.sqlite3_value_double(valueAt(arg));}
- public byte[] getBlob(int arg){return CApi.sqlite3_value_blob(valueAt(arg));}
- public byte[] getText(int arg){return CApi.sqlite3_value_text(valueAt(arg));}
- public String getText16(int arg){return CApi.sqlite3_value_text16(valueAt(arg));}
- public int getBytes(int arg){return CApi.sqlite3_value_bytes(valueAt(arg));}
- public int getBytes16(int arg){return CApi.sqlite3_value_bytes16(valueAt(arg));}
- public Object getObject(int arg){return CApi.sqlite3_value_java_object(valueAt(arg));}
- public <T> T getObjectCasted(int arg, Class<T> type){
- return CApi.sqlite3_value_java_casted(valueAt(arg), type);
+ public int getInt(int argNdx){return CApi.sqlite3_value_int(valueAt(argNdx));}
+ public long getInt64(int argNdx){return CApi.sqlite3_value_int64(valueAt(argNdx));}
+ public double getDouble(int argNdx){return CApi.sqlite3_value_double(valueAt(argNdx));}
+ public byte[] getBlob(int argNdx){return CApi.sqlite3_value_blob(valueAt(argNdx));}
+ public byte[] getText(int argNdx){return CApi.sqlite3_value_text(valueAt(argNdx));}
+ public String getText16(int argNdx){return CApi.sqlite3_value_text16(valueAt(argNdx));}
+ public int getBytes(int argNdx){return CApi.sqlite3_value_bytes(valueAt(argNdx));}
+ public int getBytes16(int argNdx){return CApi.sqlite3_value_bytes16(valueAt(argNdx));}
+ public Object getObject(int argNdx){return CApi.sqlite3_value_java_object(valueAt(argNdx));}
+ public <T> T getObject(int argNdx, Class<T> type){
+ return CApi.sqlite3_value_java_object(valueAt(argNdx), type);
}
- 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 int getType(int argNdx){return CApi.sqlite3_value_type(valueAt(argNdx));}
+ public int getSubtype(int argNdx){return CApi.sqlite3_value_subtype(valueAt(argNdx));}
+ public int getNumericType(int argNdx){return CApi.sqlite3_value_numeric_type(valueAt(argNdx));}
+ public int getNoChange(int argNdx){return CApi.sqlite3_value_nochange(valueAt(argNdx));}
+ public boolean getFromBind(int argNdx){return CApi.sqlite3_value_frombind(valueAt(argNdx));}
+ public int getEncoding(int argNdx){return CApi.sqlite3_value_encoding(valueAt(argNdx));}
public void resultInt(int v){ CApi.sqlite3_result_int(cx, v); }
public void resultInt64(long v){ CApi.sqlite3_result_int64(cx, v); }
@@ -134,6 +108,7 @@ public interface SqlFunction {
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 resultSubtype(int subtype){CApi.sqlite3_result_subtype(cx, subtype);}
public void resultZeroBlob(long n){
// Throw on error? If n is too big,
// sqlite3_result_error_toobig() is automatically called.
@@ -146,88 +121,60 @@ public interface SqlFunction {
public void resultText16(byte[] utf16){CApi.sqlite3_result_text16(cx, utf16);}
public void resultText16(String txt){CApi.sqlite3_result_text16(cx, txt);}
- public void setAuxData(int arg, Object o){
+ public void setAuxData(int argNdx, Object o){
/* From the API docs: https://www.sqlite.org/c3ref/get_auxdata.html
The value of the N parameter to these interfaces should be
non-negative. Future enhancements may make use of negative N
values to define new kinds of function caching behavior.
*/
- valueAt(arg);
- CApi.sqlite3_set_auxdata(cx, arg, o);
+ valueAt(argNdx);
+ CApi.sqlite3_set_auxdata(cx, argNdx, o);
}
- public Object getAuxData(int arg){
- valueAt(arg);
- return CApi.sqlite3_get_auxdata(cx, arg);
+ public Object getAuxData(int argNdx){
+ valueAt(argNdx);
+ return CApi.sqlite3_get_auxdata(cx, argNdx);
}
- }
-
- /**
- 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.
+ Wrapper for a single SqlFunction argument. Primarily intended
+ for use with the Arguments class's Iterable interface.
*/
- 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));
+ public final static class Arg {
+ private final Arguments a;
+ private final int ndx;
+ /* Only for use by the Arguments class. */
+ private Arg(Arguments a, int ndx){
+ this.a = a;
+ this.ndx = ndx;
}
- return rc;
+ /** 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);}
+ public byte[] getBlob(){return a.getBlob(ndx);}
+ public byte[] getText(){return a.getText(ndx);}
+ public String getText16(){return a.getText16(ndx);}
+ public int getBytes(){return a.getBytes(ndx);}
+ public int getBytes16(){return a.getBytes16(ndx);}
+ public Object getObject(){return a.getObject(ndx);}
+ public <T> T getObject(Class<T> type){ return a.getObject(ndx, type); }
+ public int getType(){return a.getType(ndx);}
+ public Object getAuxData(){return a.getAuxData(ndx);}
+ public void setAuxData(Object o){a.setAuxData(ndx, o);}
}
- /**
- 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;
+ @Override
+ public java.util.Iterator<SqlFunction.Arguments.Arg> iterator(){
+ final Arg[] proxies = new Arg[args.length];
+ for( int i = 0; i < args.length; ++i ){
+ proxies[i] = new Arg(this, i);
+ }
+ return java.util.Arrays.stream(proxies).iterator();
}
+
}
/**
@@ -235,7 +182,7 @@ public interface SqlFunction {
for use with the org.sqlite.jni.capi.ScalarFunction interface.
*/
static final class ScalarAdapter extends org.sqlite.jni.capi.ScalarFunction {
- final ScalarFunction impl;
+ private final ScalarFunction impl;
ScalarAdapter(ScalarFunction impl){
this.impl = impl;
}
@@ -261,8 +208,9 @@ public interface SqlFunction {
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;
+ static /*cannot be final without duplicating the whole body in WindowAdapter*/
+ class AggregateAdapter extends org.sqlite.jni.capi.AggregateFunction {
+ private final AggregateFunction impl;
AggregateAdapter(AggregateFunction impl){
this.impl = impl;
}
@@ -282,8 +230,9 @@ public interface SqlFunction {
}
/**
- 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().
+ 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{
@@ -298,4 +247,46 @@ public interface SqlFunction {
}
}
+ /**
+ Internal-use adapter for wrapping this package's WindowFunction
+ for use with the org.sqlite.jni.capi.WindowFunction interface.
+ */
+ static final class WindowAdapter extends AggregateAdapter {
+ private final WindowFunction impl;
+ WindowAdapter(WindowFunction impl){
+ super(impl);
+ this.impl = impl;
+ }
+
+ /**
+ Proxies this.impl.xInverse(), 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 xInverse(sqlite3_context cx, sqlite3_value[] args){
+ try{
+ impl.xInverse( new SqlFunction.Arguments(cx, args) );
+ }catch(Exception e){
+ CApi.sqlite3_result_error(cx, e);
+ }
+ }
+
+ /**
+ As for the xValue() argument of the C API's sqlite3_create_window_function().
+ If the proxied function throws, it is translated into a sqlite3_result_error().
+ */
+ public void xValue(sqlite3_context cx){
+ try{
+ impl.xValue( new SqlFunction.Arguments(cx, null) );
+ }catch(Exception e){
+ CApi.sqlite3_result_error(cx, e);
+ }
+ }
+
+ public void xDestroy(){
+ impl.xDestroy();
+ }
+ }
+
}
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java b/ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java
index bcf97b239..b464bd7d5 100644
--- a/ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java
@@ -29,6 +29,38 @@ import org.sqlite.jni.capi.OutputPointer;
public final class Sqlite implements AutoCloseable {
private sqlite3 db;
+ public static final int OPEN_READWRITE = CApi.SQLITE_OPEN_READWRITE;
+ public static final int OPEN_CREATE = CApi.SQLITE_OPEN_CREATE;
+ public static final int OPEN_EXRESCODE = CApi.SQLITE_OPEN_EXRESCODE;
+ public static final int TXN_NONE = CApi.SQLITE_TXN_NONE;
+ public static final int TXN_READ = CApi.SQLITE_TXN_READ;
+ public static final int TXN_WRITE = CApi.SQLITE_TXN_WRITE;
+
+ public static final int STATUS_MEMORY_USED = CApi.SQLITE_STATUS_MEMORY_USED;
+ public static final int STATUS_PAGECACHE_USED = CApi.SQLITE_STATUS_PAGECACHE_USED;
+ public static final int STATUS_PAGECACHE_OVERFLOW = CApi.SQLITE_STATUS_PAGECACHE_OVERFLOW;
+ public static final int STATUS_MALLOC_SIZE = CApi.SQLITE_STATUS_MALLOC_SIZE;
+ public static final int STATUS_PARSER_STACK = CApi.SQLITE_STATUS_PARSER_STACK;
+ public static final int STATUS_PAGECACHE_SIZE = CApi.SQLITE_STATUS_PAGECACHE_SIZE;
+ public static final int STATUS_MALLOC_COUNT = CApi.SQLITE_STATUS_MALLOC_COUNT;
+
+ public static final int LIMIT_LENGTH = CApi.SQLITE_LIMIT_LENGTH;
+ public static final int LIMIT_SQL_LENGTH = CApi.SQLITE_LIMIT_SQL_LENGTH;
+ public static final int LIMIT_COLUMN = CApi.SQLITE_LIMIT_COLUMN;
+ public static final int LIMIT_EXPR_DEPTH = CApi.SQLITE_LIMIT_EXPR_DEPTH;
+ public static final int LIMIT_COMPOUND_SELECT = CApi.SQLITE_LIMIT_COMPOUND_SELECT;
+ public static final int LIMIT_VDBE_OP = CApi.SQLITE_LIMIT_VDBE_OP;
+ public static final int LIMIT_FUNCTION_ARG = CApi.SQLITE_LIMIT_FUNCTION_ARG;
+ public static final int LIMIT_ATTACHED = CApi.SQLITE_LIMIT_ATTACHED;
+ public static final int LIMIT_LIKE_PATTERN_LENGTH = CApi.SQLITE_LIMIT_LIKE_PATTERN_LENGTH;
+ public static final int LIMIT_VARIABLE_NUMBER = CApi.SQLITE_LIMIT_VARIABLE_NUMBER;
+ public static final int LIMIT_TRIGGER_DEPTH = CApi.SQLITE_LIMIT_TRIGGER_DEPTH;
+ public static final int LIMIT_WORKER_THREADS = CApi.SQLITE_LIMIT_WORKER_THREADS;
+
+ public static final int PREPARE_PERSISTENT = CApi.SQLITE_PREPARE_PERSISTENT;
+ public static final int PREPARE_NORMALIZE = CApi.SQLITE_PREPARE_NORMALIZE;
+ public static final int PREPARE_NO_VTAB = CApi.SQLITE_PREPARE_NO_VTAB;
+
//! Used only by the open() factory functions.
private Sqlite(sqlite3 db){
this.db = db;
@@ -63,6 +95,33 @@ public final class Sqlite implements AutoCloseable {
return open(filename, SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, null);
}
+ public static String libVersion(){
+ return CApi.sqlite3_libversion();
+ }
+
+ public static int libVersionNumber(){
+ return CApi.sqlite3_libversion_number();
+ }
+
+ public static String libSourceId(){
+ return CApi.sqlite3_sourceid();
+ }
+
+ /**
+ As per sqlite3_status64(), but returns its current and high-water
+ results as a two-element array. Throws if the first argument is
+ not one of the STATUS_... constants.
+ */
+ public long[] libStatus(int op, boolean resetStats){
+ org.sqlite.jni.capi.OutputPointer.Int64 pCurrent =
+ new org.sqlite.jni.capi.OutputPointer.Int64();
+ org.sqlite.jni.capi.OutputPointer.Int64 pHighwater =
+ new org.sqlite.jni.capi.OutputPointer.Int64();
+ final int rc = CApi.sqlite3_status64(op, pCurrent, pHighwater, resetStats);
+ checkRc(rc);
+ return new long[] {pCurrent.value, pHighwater.value};
+ }
+
@Override public void close(){
if(null!=this.db){
this.db.close();
@@ -71,13 +130,29 @@ public final class Sqlite implements AutoCloseable {
}
/**
+ Returns the value of the native library's build-time value of the
+ SQLITE_THREADSAFE build option.
+ */
+ public static int libThreadsafe(){
+ return CApi.sqlite3_threadsafe();
+ }
+
+ public static boolean strglob(String glob, String txt){
+ return 0==CApi.sqlite3_strglob(glob, txt);
+ }
+
+ public static boolean strlike(String glob, String txt, char escChar){
+ return 0==CApi.sqlite3_strlike(glob, txt, escChar);
+ }
+
+ /**
Returns this object's underlying native db handle, or null if
this instance has been closed. This is very specifically not
public.
*/
sqlite3 nativeHandle(){ return this.db; }
- private sqlite3 affirmOpen(){
+ private sqlite3 thisDb(){
if( null==db || 0==db.getNativePointer() ){
throw new IllegalArgumentException("This database instance is closed.");
}
@@ -88,10 +163,259 @@ public final class Sqlite implements AutoCloseable {
// return s==null ? null : s.getBytes(StandardCharsets.UTF_8);
// }
- private void affirmRcOk(int rc){
+ /**
+ If rc!=0, throws an SqliteException. If this db is currently
+ opened and has non-0 sqlite3_errcode(), the error state is
+ extracted from it, else only the string form of rc is used. It is
+ the caller's responsibility to filter out non-error codes such as
+ SQLITE_ROW and SQLITE_DONE before calling this.
+ */
+ private void checkRc(int rc){
if( 0!=rc ){
- throw new SqliteException(db);
+ if( null==db || 0==sqlite3_errcode(db)) throw new SqliteException(rc);
+ else throw new SqliteException(db);
+ }
+ }
+
+ /**
+ prepFlags must be 0 or a bitmask of the PREPARE_... constants.
+
+ prepare() TODOs include:
+
+ - overloads taking byte[] and ByteBuffer.
+
+ - multi-statement processing, like CApi.sqlite3_prepare_multi()
+ but using a callback specific to the higher-level Stmt class
+ rather than the sqlite3_stmt class.
+ */
+ public Stmt prepare(String sql, int prepFlags){
+ final OutputPointer.sqlite3_stmt out = new OutputPointer.sqlite3_stmt();
+ final int rc = sqlite3_prepare_v3(thisDb(), sql, prepFlags, out);
+ checkRc(rc);
+ final sqlite3_stmt q = out.take();
+ if( null==q ){
+ /* The C-level API treats input which is devoid of SQL
+ statements (e.g. all comments or an empty string) as success
+ but returns a NULL sqlite3_stmt object. In higher-level APIs,
+ wrapping a "successful NULL" object that way is tedious to
+ use because it forces clients and/or wrapper-level code to
+ check for that unusual case. In practice, higher-level
+ bindings are generally better-served by treating empty SQL
+ input as an error. */
+ throw new IllegalArgumentException("Input contains no SQL statements.");
+ }
+ return new Stmt(this, q);
+ }
+
+ public Stmt prepare(String sql){
+ return prepare(sql, 0);
+ }
+
+ public void createFunction(String name, int nArg, int eTextRep, ScalarFunction f){
+ int rc = CApi.sqlite3_create_function(thisDb(), 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);
+ }
+
+ public void createFunction(String name, int nArg, int eTextRep, AggregateFunction f){
+ int rc = CApi.sqlite3_create_function(thisDb(), 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);
+ }
+
+ public void createFunction(String name, int nArg, int eTextRep, WindowFunction f){
+ int rc = CApi.sqlite3_create_function(thisDb(), name, nArg, eTextRep,
+ new SqlFunction.WindowAdapter(f));
+ if( 0!=rc ) throw new SqliteException(db);
+ }
+
+ public void createFunction(String name, int nArg, WindowFunction f){
+ this.createFunction(name, nArg, CApi.SQLITE_UTF8, f);
+ }
+
+ public long changes(){
+ return CApi.sqlite3_changes64(thisDb());
+ }
+
+ public long totalChanges(){
+ return CApi.sqlite3_total_changes64(thisDb());
+ }
+
+ public long lastInsertRowId(){
+ return CApi.sqlite3_last_insert_rowid(thisDb());
+ }
+
+ public void setLastInsertRowId(long rowId){
+ CApi.sqlite3_set_last_insert_rowid(thisDb(), rowId);
+ }
+
+ public void interrupt(){
+ CApi.sqlite3_interrupt(thisDb());
+ }
+
+ public boolean isInterrupted(){
+ return CApi.sqlite3_is_interrupted(thisDb());
+ }
+
+ public boolean isAutoCommit(){
+ return CApi.sqlite3_get_autocommit(thisDb());
+ }
+
+ public void setBusyTimeout(int ms){
+ checkRc(CApi.sqlite3_busy_timeout(thisDb(), ms));
+ }
+
+ /**
+ Analog to sqlite3_txn_state(). Returns one of TXN_NONE, TXN_READ,
+ or TXN_WRITE to denote this database's current transaction state
+ for the given schema name (or the most restrictive state of any
+ schema if zSchema is null).
+ */
+ public int transactionState(String zSchema){
+ return CApi.sqlite3_txn_state(thisDb(), zSchema);
+ }
+
+ /**
+ Analog to sqlite3_db_name(). Returns null if passed an unknown
+ index.
+ */
+ public String dbName(int dbNdx){
+ return CApi.sqlite3_db_name(thisDb(), dbNdx);
+ }
+
+ /**
+ Analog to sqlite3_db_filename(). Returns null if passed an
+ unknown db name.
+ */
+ public String dbFileName(String dbName){
+ return CApi.sqlite3_db_filename(thisDb(), dbName);
+ }
+
+ /**
+ Analog to the variant of sqlite3_db_config() for configuring the
+ SQLITE_DBCONFIG_MAINDBNAME option. Throws on error.
+ */
+ public void setMainDbName(String name){
+ checkRc(
+ CApi.sqlite3_db_config(thisDb(), CApi.SQLITE_DBCONFIG_MAINDBNAME,
+ name)
+ );
+ }
+
+ /**
+ Analog to sqlite3_db_readonly() but throws an SqliteException
+ with result code SQLITE_NOTFOUND if given an unknown database
+ name.
+ */
+ public boolean readOnly(String dbName){
+ final int rc = CApi.sqlite3_db_readonly(thisDb(), dbName);
+ if( 0==rc ) return false;
+ else if( rc>0 ) return true;
+ throw new SqliteException(CApi.SQLITE_NOTFOUND);
+ }
+
+ /**
+ Analog to sqlite3_db_release_memory().
+ */
+ public void releaseMemory(){
+ CApi.sqlite3_db_release_memory(thisDb());
+ }
+
+ /**
+ Analog to sqlite3_release_memory().
+ */
+ public static int releaseMemory(int n){
+ return CApi.sqlite3_release_memory(n);
+ }
+
+ /**
+ Analog to sqlite3_limit(). limitId must be one of the
+ LIMIT_... constants.
+
+ Returns the old limit for the given option. If newLimit is
+ negative, it returns the old limit without modifying the limit.
+
+ If sqlite3_limit() returns a negative value, this function throws
+ an SqliteException with the SQLITE_RANGE result code but no
+ further error info (because that case does not qualify as a
+ db-level error). Such errors may indicate an invalid argument
+ value or an invalid range for newLimit (the underlying function
+ does not differentiate between those).
+ */
+ public int limit(int limitId, int newLimit){
+ final int rc = CApi.sqlite3_limit(thisDb(), limitId, newLimit);
+ if( rc<0 ){
+ throw new SqliteException(CApi.SQLITE_RANGE);
}
+ return rc;
+ }
+
+ /**
+ Analog to sqlite3_errstr().
+ */
+ static String errstr(int resultCode){
+ return CApi.sqlite3_errstr(resultCode);
+ }
+
+ /**
+ A wrapper object for use with tableColumnMetadata(). They are
+ created and populated only via that interface.
+ */
+ public final class TableColumnMetadata {
+ Boolean pNotNull = null;
+ Boolean pPrimaryKey = null;
+ Boolean pAutoinc = null;
+ String pzCollSeq = null;
+ String pzDataType = null;
+
+ private TableColumnMetadata(){}
+
+ public String getDataType(){ return pzDataType; }
+ public String getCollation(){ return pzCollSeq; }
+ public boolean isNotNull(){ return pNotNull; }
+ public boolean isPrimaryKey(){ return pPrimaryKey; }
+ public boolean isAutoincrement(){ return pAutoinc; }
+ }
+
+ /**
+ Returns data about a database, table, and (optionally) column
+ (which may be null), as per sqlite3_table_column_metadata().
+ Throws if passed invalid arguments, else returns the result as a
+ new TableColumnMetadata object.
+ */
+ TableColumnMetadata tableColumnMetadata(
+ String zDbName, String zTableName, String zColumnName
+ ){
+ org.sqlite.jni.capi.OutputPointer.String pzDataType
+ = new org.sqlite.jni.capi.OutputPointer.String();
+ org.sqlite.jni.capi.OutputPointer.String pzCollSeq
+ = new org.sqlite.jni.capi.OutputPointer.String();
+ org.sqlite.jni.capi.OutputPointer.Bool pNotNull
+ = new org.sqlite.jni.capi.OutputPointer.Bool();
+ org.sqlite.jni.capi.OutputPointer.Bool pPrimaryKey
+ = new org.sqlite.jni.capi.OutputPointer.Bool();
+ org.sqlite.jni.capi.OutputPointer.Bool pAutoinc
+ = new org.sqlite.jni.capi.OutputPointer.Bool();
+ final int rc = CApi.sqlite3_table_column_metadata(
+ thisDb(), zDbName, zTableName, zColumnName,
+ pzDataType, pzCollSeq, pNotNull, pPrimaryKey, pAutoinc
+ );
+ checkRc(rc);
+ TableColumnMetadata rv = new TableColumnMetadata();
+ rv.pzDataType = pzDataType.value;
+ rv.pzCollSeq = pzCollSeq.value;
+ rv.pNotNull = pNotNull.value;
+ rv.pPrimaryKey = pPrimaryKey.value;
+ rv.pAutoinc = pAutoinc.value;
+ return rv;
}
/**
@@ -101,23 +425,44 @@ public final class Sqlite implements AutoCloseable {
public final class Stmt implements AutoCloseable {
private Sqlite _db = null;
private sqlite3_stmt stmt = null;
+ /**
+ We save the result column count in order to prevent having to
+ call into C to fetch that value every time we need to check
+ that value for the columnXyz() methods.
+ */
+ private final int resultColCount;
+
/** Only called by the prepare() factory functions. */
Stmt(Sqlite db, sqlite3_stmt stmt){
this._db = db;
this.stmt = stmt;
+ this.resultColCount = CApi.sqlite3_column_count(stmt);
}
sqlite3_stmt nativeHandle(){
return stmt;
}
- private sqlite3_stmt affirmOpen(){
+ /**
+ If this statement is still opened, its low-level handle is
+ returned, eelse an IllegalArgumentException is thrown.
+ */
+ private sqlite3_stmt thisStmt(){
if( null==stmt || 0==stmt.getNativePointer() ){
throw new IllegalArgumentException("This Stmt has been finalized.");
}
return stmt;
}
+ /** Throws if n is out of range of this.resultColCount. Intended
+ to be used by the columnXyz() methods. */
+ private sqlite3_stmt checkColIndex(int n){
+ if(n<0 || n>=this.resultColCount){
+ throw new IllegalArgumentException("Column index "+n+" is out of range.");
+ }
+ return thisStmt();
+ }
+
/**
Corresponds to sqlite3_finalize(), but we cannot override the
name finalize() here because this one requires a different
@@ -140,7 +485,9 @@ public final class Sqlite implements AutoCloseable {
/**
Throws if rc is any value other than 0, SQLITE_ROW, or
- SQLITE_DONE, else returns rc.
+ SQLITE_DONE, else returns rc. Error state for the exception is
+ extracted from this statement object (if it's opened) or the
+ string form of rc.
*/
private int checkRc(int rc){
switch(rc){
@@ -148,16 +495,33 @@ public final class Sqlite implements AutoCloseable {
case SQLITE_ROW:
case SQLITE_DONE: return rc;
default:
- throw new SqliteException(this);
+ if( null==stmt ) throw new SqliteException(rc);
+ else throw new SqliteException(this);
}
}
/**
- Works like sqlite3_step() but throws SqliteException for any
- result other than 0, SQLITE_ROW, or SQLITE_DONE.
+ Works like sqlite3_step() but returns true for SQLITE_ROW,
+ false for SQLITE_DONE, and throws SqliteException for any other
+ result.
*/
- public int step(){
- return checkRc(sqlite3_step(affirmOpen()));
+ public boolean step(){
+ switch(checkRc(sqlite3_step(thisStmt()))){
+ case CApi.SQLITE_ROW: return true;
+ case CApi.SQLITE_DONE: return false;
+ default:
+ throw new IllegalStateException(
+ "This \"cannot happen\": all possible result codes were checked already."
+ );
+ }
+ /*
+ Potential signature change TODO:
+
+ boolean step()
+
+ Returning true for SQLITE_ROW and false for anything else.
+ Those semantics have proven useful in the WASM/JS bindings.
+ */
}
public Sqlite db(){ return this._db; }
@@ -166,53 +530,109 @@ public final class Sqlite implements AutoCloseable {
Works like sqlite3_reset() but throws on error.
*/
public void reset(){
- checkRc(sqlite3_reset(affirmOpen()));
+ checkRc(CApi.sqlite3_reset(thisStmt()));
}
public void clearBindings(){
- sqlite3_clear_bindings( affirmOpen() );
+ CApi.sqlite3_clear_bindings( thisStmt() );
+ }
+ public void bindInt(int ndx, int val){
+ checkRc(CApi.sqlite3_bind_int(thisStmt(), ndx, val));
+ }
+ public void bindInt64(int ndx, long val){
+ checkRc(CApi.sqlite3_bind_int64(thisStmt(), ndx, val));
+ }
+ public void bindDouble(int ndx, double val){
+ checkRc(CApi.sqlite3_bind_double(thisStmt(), ndx, val));
+ }
+ public void bindObject(int ndx, Object o){
+ checkRc(CApi.sqlite3_bind_java_object(thisStmt(), ndx, o));
+ }
+ public void bindNull(int ndx){
+ checkRc(CApi.sqlite3_bind_null(thisStmt(), ndx));
+ }
+ public int bindParameterCount(){
+ return CApi.sqlite3_bind_parameter_count(thisStmt());
+ }
+ public int bindParameterIndex(String paramName){
+ return CApi.sqlite3_bind_parameter_index(thisStmt(), paramName);
+ }
+ public String bindParameterName(int ndx){
+ return CApi.sqlite3_bind_parameter_name(thisStmt(), ndx);
+ }
+ public void bindText(int ndx, byte[] utf8){
+ checkRc(CApi.sqlite3_bind_text(thisStmt(), ndx, utf8));
+ }
+ public void bindText(int ndx, String asUtf8){
+ checkRc(CApi.sqlite3_bind_text(thisStmt(), ndx, asUtf8));
+ }
+ public void bindText16(int ndx, byte[] utf16){
+ checkRc(CApi.sqlite3_bind_text16(thisStmt(), ndx, utf16));
+ }
+ public void bindText16(int ndx, String txt){
+ checkRc(CApi.sqlite3_bind_text16(thisStmt(), ndx, txt));
+ }
+ public void bindZeroBlob(int ndx, int n){
+ checkRc(CApi.sqlite3_bind_zeroblob(thisStmt(), ndx, n));
+ }
+ public void bindBlob(int ndx, byte[] bytes){
+ checkRc(CApi.sqlite3_bind_blob(thisStmt(), ndx, bytes));
}
- }
-
-
- /**
- prepare() TODOs include:
-
- - overloads taking byte[] and ByteBuffer.
-
- - multi-statement processing, like CApi.sqlite3_prepare_multi()
- but using a callback specific to the higher-level Stmt class
- rather than the sqlite3_stmt class.
- */
- public Stmt prepare(String sql, int prepFlags){
- final OutputPointer.sqlite3_stmt out = new OutputPointer.sqlite3_stmt();
- final int rc = sqlite3_prepare_v3(affirmOpen(), sql, prepFlags, out);
- affirmRcOk(rc);
- return new Stmt(this, out.take());
- }
-
- public Stmt prepare(String sql){
- 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);
- }
-
- 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);
- }
+ public byte[] columnBlob(int ndx){
+ return CApi.sqlite3_column_blob( checkColIndex(ndx), ndx );
+ }
+ public byte[] columnText(int ndx){
+ return CApi.sqlite3_column_text( checkColIndex(ndx), ndx );
+ }
+ public String columnText16(int ndx){
+ return CApi.sqlite3_column_text16( checkColIndex(ndx), ndx );
+ }
+ public int columnBytes(int ndx){
+ return CApi.sqlite3_column_bytes( checkColIndex(ndx), ndx );
+ }
+ public int columnBytes16(int ndx){
+ return CApi.sqlite3_column_bytes16( checkColIndex(ndx), ndx );
+ }
+ public int columnInt(int ndx){
+ return CApi.sqlite3_column_int( checkColIndex(ndx), ndx );
+ }
+ public long columnInt64(int ndx){
+ return CApi.sqlite3_column_int64( checkColIndex(ndx), ndx );
+ }
+ public double columnDouble(int ndx){
+ return CApi.sqlite3_column_double( checkColIndex(ndx), ndx );
+ }
+ public int columnType(int ndx){
+ return CApi.sqlite3_column_type( checkColIndex(ndx), ndx );
+ }
+ public String columnDeclType(int ndx){
+ return CApi.sqlite3_column_decltype( checkColIndex(ndx), ndx );
+ }
+ public int columnCount(){
+ return resultColCount;
+ }
+ public int columnDataCount(){
+ return CApi.sqlite3_data_count( thisStmt() );
+ }
+ public Object columnObject(int ndx){
+ return CApi.sqlite3_column_java_object( checkColIndex(ndx), ndx );
+ }
+ public <T> T columnObject(int ndx, Class<T> type){
+ return CApi.sqlite3_column_java_object( checkColIndex(ndx), ndx, type );
+ }
+ public String columnName(int ndx){
+ return CApi.sqlite3_column_name( checkColIndex(ndx), ndx );
+ }
+ public String columnDatabaseName(int ndx){
+ return CApi.sqlite3_column_database_name( checkColIndex(ndx), ndx );
+ }
+ public String columnOriginName(int ndx){
+ return CApi.sqlite3_column_origin_name( checkColIndex(ndx), ndx );
+ }
+ public String columnTableName(int ndx){
+ return CApi.sqlite3_column_table_name( checkColIndex(ndx), ndx );
+ }
+ } /* Stmt class */
}
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java b/ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java
index 111f004db..27cfc0e6b 100644
--- a/ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java
@@ -12,7 +12,7 @@
** This file is part of the wrapper1 interface for sqlite3.
*/
package org.sqlite.jni.wrapper1;
-import static org.sqlite.jni.capi.CApi.*;
+import org.sqlite.jni.capi.CApi;
import org.sqlite.jni.capi.sqlite3;
/**
@@ -22,10 +22,10 @@ import org.sqlite.jni.capi.sqlite3;
and C via JNI.
*/
public final class SqliteException extends java.lang.RuntimeException {
- int errCode = SQLITE_ERROR;
- int xerrCode = SQLITE_ERROR;
- int errOffset = -1;
- int sysErrno = 0;
+ private int errCode = CApi.SQLITE_ERROR;
+ private int xerrCode = CApi.SQLITE_ERROR;
+ private int errOffset = -1;
+ private int sysErrno = 0;
/**
Records the given error string and uses SQLITE_ERROR for both the
@@ -38,10 +38,13 @@ public final class SqliteException extends java.lang.RuntimeException {
/**
Uses sqlite3_errstr(sqlite3ResultCode) for the error string and
sets both the error code and extended error code to the given
- value.
+ value. This approach includes no database-level information and
+ systemErrno() will be 0, so is intended only for use with sqlite3
+ APIs for which a result code is not an error but which the
+ higher-level wrapper should treat as one.
*/
public SqliteException(int sqlite3ResultCode){
- super(sqlite3_errstr(sqlite3ResultCode));
+ super(CApi.sqlite3_errstr(sqlite3ResultCode));
errCode = xerrCode = sqlite3ResultCode;
}
@@ -50,16 +53,16 @@ public final class SqliteException extends java.lang.RuntimeException {
must refer to an opened db object). Note that this does NOT close
the db.
- Design note: closing the db on error is likely only useful during
+ Design note: closing the db on error is really only useful during
a failed db-open operation, and the place(s) where that can
happen are inside this library, not client-level code.
*/
SqliteException(sqlite3 db){
- super(sqlite3_errmsg(db));
- errCode = sqlite3_errcode(db);
- xerrCode = sqlite3_extended_errcode(db);
- errOffset = sqlite3_error_offset(db);
- sysErrno = sqlite3_system_errno(db);
+ super(CApi.sqlite3_errmsg(db));
+ errCode = CApi.sqlite3_errcode(db);
+ xerrCode = CApi.sqlite3_extended_errcode(db);
+ errOffset = CApi.sqlite3_error_offset(db);
+ sysErrno = CApi.sqlite3_system_errno(db);
}
/**
@@ -71,7 +74,7 @@ public final class SqliteException extends java.lang.RuntimeException {
}
public SqliteException(Sqlite.Stmt stmt){
- this( stmt.db() );
+ this(stmt.db());
}
public int errcode(){ return errCode; }
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java b/ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java
index f5fd5f84e..ca4e6d052 100644
--- a/ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java
@@ -129,6 +129,11 @@ public class Tester2 implements Runnable {
execSql(db, String.join("", sql));
}
+ /**
+ Executes all SQL statements in the given string. If throwOnError
+ is true then it will throw for any prepare/step errors, else it
+ will return the corresponding non-0 result code.
+ */
public static int execSql(Sqlite dbw, boolean throwOnError, String sql){
final sqlite3 db = dbw.nativeHandle();
OutputPointer.Int32 oTail = new OutputPointer.Int32();
@@ -145,7 +150,7 @@ public class Tester2 implements Runnable {
}
if( 0==sqlChunk.length ) break;
rc = CApi.sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
- if(throwOnError) affirm(0 == rc);
+ if( throwOnError ) affirm(0 == rc);
else if( 0!=rc ) break;
pos = oTail.value;
stmt = outStmt.take();
@@ -158,7 +163,7 @@ public class Tester2 implements Runnable {
}
CApi.sqlite3_finalize(stmt);
affirm(0 == stmt.getNativePointer());
- if(0!=rc && CApi.SQLITE_ROW!=rc && CApi.SQLITE_DONE!=rc){
+ if(CApi.SQLITE_DONE!=rc){
break;
}
}
@@ -194,9 +199,9 @@ public class Tester2 implements Runnable {
}
Sqlite openDb(String name){
- final Sqlite db = Sqlite.open(name, CApi.SQLITE_OPEN_READWRITE|
- CApi.SQLITE_OPEN_CREATE|
- CApi.SQLITE_OPEN_EXRESCODE);
+ final Sqlite db = Sqlite.open(name, Sqlite.OPEN_READWRITE|
+ Sqlite.OPEN_CREATE|
+ Sqlite.OPEN_EXRESCODE);
++metrics.dbOpen;
return db;
}
@@ -224,19 +229,50 @@ public class Tester2 implements Runnable {
void testPrepare1(){
try (Sqlite db = openDb()) {
- Sqlite.Stmt stmt = db.prepare("SELECT 1");
+ Sqlite.Stmt stmt = db.prepare("SELECT ?1");
+ Exception e = null;
affirm( null!=stmt.nativeHandle() );
- affirm( CApi.SQLITE_ROW == stmt.step() );
- affirm( CApi.SQLITE_DONE == stmt.step() );
+ affirm( 1==stmt.bindParameterCount() );
+ affirm( "?1".equals(stmt.bindParameterName(1)) );
+ affirm( null==stmt.bindParameterName(2) );
+ stmt.bindInt64(1, 1);
+ stmt.bindDouble(1, 1.1);
+ stmt.bindObject(1, db);
+ stmt.bindNull(1);
+ stmt.bindText(1, new byte[] {32,32,32});
+ stmt.bindText(1, "123");
+ stmt.bindText16(1, "123".getBytes(StandardCharsets.UTF_16));
+ stmt.bindText16(1, "123");
+ stmt.bindZeroBlob(1, 8);
+ stmt.bindBlob(1, new byte[] {1,2,3,4});
+ stmt.bindInt(1, 17);
+ try{ stmt.bindInt(2,1); }
+ catch(Exception ex){ e = ex; }
+ affirm( null!=e );
+ e = null;
+ affirm( stmt.step() );
+ try{ stmt.columnInt(1); }
+ catch(Exception ex){ e = ex; }
+ affirm( null!=e );
+ e = null;
+ affirm( 17 == stmt.columnInt(0) );
+ affirm( 17L == stmt.columnInt64(0) );
+ affirm( 17.0 == stmt.columnDouble(0) );
+ affirm( "17".equals(stmt.columnText16(0)) );
+ affirm( !stmt.step() );
stmt.reset();
- affirm( CApi.SQLITE_ROW == stmt.step() );
- affirm( CApi.SQLITE_DONE == stmt.step() );
+ affirm( stmt.step() );
+ affirm( !stmt.step() );
affirm( 0 == stmt.finalizeStmt() );
affirm( null==stmt.nativeHandle() );
- stmt = db.prepare("SELECT 1");
- affirm( CApi.SQLITE_ROW == stmt.step() );
- affirm( 0 == stmt.finalizeStmt() )
+ stmt = db.prepare("SELECT ?");
+ stmt.bindObject(1, db);
+ affirm( stmt.step() );
+ affirm( db==stmt.columnObject(0) );
+ affirm( db==stmt.columnObject(0, Sqlite.class ) );
+ affirm( null==stmt.columnObject(0, Sqlite.Stmt.class ) );
+ affirm( 0==stmt.finalizeStmt() )
/* getting a non-0 out of sqlite3_finalize() is tricky */;
affirm( null==stmt.nativeHandle() );
}
@@ -270,7 +306,7 @@ public class Tester2 implements Runnable {
void testUdfAggregate(){
final ValueHolder<Integer> xDestroyCalled = new ValueHolder<>(0);
- final ValueHolder<Integer> vh = new ValueHolder<>(0);
+ Sqlite.Stmt q = null;
try (Sqlite db = openDb()) {
execSql(db, "create table t(a); insert into t(a) values(1),(2),(3)");
final AggregateFunction f = new AggregateFunction<Integer>(){
@@ -284,18 +320,96 @@ public class Tester2 implements Runnable {
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 );
+ db.createFunction("summer", 1, f);
+ q = db.prepare(
+ "with cte(v) as ("+
+ "select 3 union all select 5 union all select 7"+
+ ") select summer(v), summer(v+1) from cte"
+ /* ------------------^^^^^^^^^^^ ensures that we're handling
+ sqlite3_aggregate_context() properly. */
+ );
+ affirm( q.step() );
+ affirm( 15==q.columnInt(0) );
+ q.finalizeStmt();
+ q = null;
affirm( 0 == xDestroyCalled.value );
+ db.createFunction("summerN", -1, f);
+
+ q = db.prepare("select summerN(1,8,9), summerN(2,3,4)");
+ affirm( q.step() );
+ affirm( 18==q.columnInt(0) );
+ affirm( 9==q.columnInt(1) );
+ q.finalizeStmt();
+ q = null;
+
+ }/*db*/
+ finally{
+ if( null!=q ) q.finalizeStmt();
}
- affirm( 1 == xDestroyCalled.value );
+ affirm( 2 == xDestroyCalled.value
+ /* because we've bound the same instance twice */ );
+ }
+
+ private void testUdfWindow(){
+ final Sqlite db = openDb();
+ /* Example window function, table, and results taken from:
+ https://sqlite.org/windowfunctions.html#udfwinfunc */
+ final WindowFunction func = new WindowFunction<Integer>(){
+ //! Impl of xStep() and xInverse()
+ private void xStepInverse(SqlFunction.Arguments args, int v){
+ this.getAggregateState(args,0).value += v;
+ }
+ @Override public void xStep(SqlFunction.Arguments args){
+ this.xStepInverse(args, args.getInt(0));
+ }
+ @Override public void xInverse(SqlFunction.Arguments args){
+ this.xStepInverse(args, -args.getInt(0));
+ }
+ //! Impl of xFinal() and xValue()
+ private void xFinalValue(SqlFunction.Arguments args, Integer v){
+ if(null == v) args.resultNull();
+ else args.resultInt(v);
+ }
+ @Override public void xFinal(SqlFunction.Arguments args){
+ xFinalValue(args, this.takeAggregateState(args));
+ affirm( null == this.getAggregateState(args,null).value );
+ }
+ @Override public void xValue(SqlFunction.Arguments args){
+ xFinalValue(args, this.getAggregateState(args,null).value);
+ }
+ };
+ db.createFunction("winsumint", 1, func);
+ execSql(db, new String[] {
+ "CREATE TEMP TABLE twin(x, y); INSERT INTO twin VALUES",
+ "('a', 4),('b', 5),('c', 3),('d', 8),('e', 1)"
+ });
+ final Sqlite.Stmt stmt = db.prepare(
+ "SELECT x, winsumint(y) OVER ("+
+ "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING"+
+ ") AS sum_y "+
+ "FROM twin ORDER BY x;"
+ );
+ int n = 0;
+ while( stmt.step() ){
+ final String s = stmt.columnText16(0);
+ final int i = stmt.columnInt(1);
+ switch(++n){
+ case 1: affirm( "a".equals(s) && 9==i ); break;
+ case 2: affirm( "b".equals(s) && 12==i ); break;
+ case 3: affirm( "c".equals(s) && 16==i ); break;
+ case 4: affirm( "d".equals(s) && 12==i ); break;
+ case 5: affirm( "e".equals(s) && 9==i ); break;
+ default: affirm( false /* cannot happen */ );
+ }
+ }
+ stmt.close();
+ affirm( 5 == n );
+ db.close();
}
private void runTests(boolean fromThread) throws Exception {
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/WindowFunction.java b/ext/jni/src/org/sqlite/jni/wrapper1/WindowFunction.java
new file mode 100644
index 000000000..479fc74d7
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/WindowFunction.java
@@ -0,0 +1,46 @@
+/*
+** 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;
+
+/**
+ A SqlFunction implementation for window functions. The T type
+ represents the type of data accumulated by this function 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 WindowFunction<T> extends AggregateFunction<T> {
+
+ /**
+ As for the xInverse() argument of the C API's
+ sqlite3_create_window_function(). If this function throws, the
+ exception is reported via sqlite3_result_error().
+ */
+ public abstract void xInverse(SqlFunction.Arguments args);
+
+ /**
+ As for the xValue() argument of the C API's
+ sqlite3_create_window_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 xValue() but will contain the context object needed
+ for setting the call's result or error state.
+ */
+ public abstract void xValue(SqlFunction.Arguments args);
+
+}