xFuncCount = new ValueHolder<>(0);
- final fts5_extension_function func = new fts5_extension_function(){
- public void xFunction(Fts5ExtensionApi ext, Fts5Context fCx,
- sqlite3_context pCx, sqlite3_value argv[]){
- int nCols = ext.xColumnCount(fCx);
- affirm( 2 == nCols );
- affirm( nCols == argv.length );
- affirm( ext.xUserData(fCx) == pUserData );
- if(true){
- OutputPointer.String op = new OutputPointer.String();
- for(int i = 0; i < nCols; ++i ){
- int rc = ext.xColumnText(fCx, i, op);
- affirm( 0 == rc );
- final String val = op.value;
- affirm( val.equals(sqlite3_value_text(argv[i])) );
- //outln("xFunction col "+i+": "+val);
- }
- }
- ++xFuncCount.value;
- }
- public void xDestroy(){
- xDestroyCalled.value = true;
- }
- };
-
- int rc = fApi.xCreateFunction("myaux", pUserData, func);
- affirm( 0==rc );
-
- affirm( 0==xFuncCount.value );
- execSql(db, "select myaux(ft,a,b) from ft;");
- affirm( 2==xFuncCount.value );
- affirm( !xDestroyCalled.value );
- sqlite3_close_v2(db);
- affirm( xDestroyCalled.value );
- }
-
- public TesterFts5(boolean verbose){
- if(verbose){
- final long timeStart = System.currentTimeMillis();
- final int oldAffirmCount = Tester1.affirmCount;
- test1();
- final int affirmCount = Tester1.affirmCount - oldAffirmCount;
- final long timeEnd = System.currentTimeMillis();
- outln("FTS5 Tests done. Assertions checked = ",affirmCount,
- ", Total time = ",(timeEnd - timeStart),"ms");
- }else{
- test1();
- }
- }
- public TesterFts5(){ this(false); }
-}
diff --git a/ext/jni/src/org/sqlite/jni/annotation/Canonical.java b/ext/jni/src/org/sqlite/jni/annotation/Canonical.java
deleted file mode 100644
index f329aee137..0000000000
--- a/ext/jni/src/org/sqlite/jni/annotation/Canonical.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.sqlite.jni.annotation;
-
-/**
- This annotation is for marking functions as "canonical", meaning
- that they map directly to a function in the core sqlite3 C API. The
- intent is to distinguish them from functions added specifically to
- the Java API.
-
- Canonical functions, unless specifically documented, have the
- same semantics as their counterparts in
- the C API documentation,
- despite their signatures perhaps differing. The Java API adds a
- number of overloads to simplify use, as well as a few Java-specific
- functions, and those are never flagged as @Canonical.
-
-
In some cases, the canonical version of a function is private
- and exposed to Java via public overloads.
-
-
In rare cases, the Java interface for a canonical function has a
- different name than its C counterpart. For such cases,
- (cname=the-C-side-name) is passed to this annotation and a
- Java-side implementation with a slightly different signature is
- added to with the canonical name. As of this writing, that applies
- only to {@link org.sqlite.jni.SQLite3Jni#sqlite3_value_text_utf8}
- and {@link org.sqlite.jni.SQLite3Jni#sqlite3_column_text_utf8}.
-
-
The comment property can be used to add a comment.
-*/
-@java.lang.annotation.Documented
-@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
-@java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD)
-public @interface Canonical{
- /**
- Java functions which directly map to a canonical function but
- change its name for some reason should not the original name
- in this property.
- */
- String cname() default ""/*doesn't allow null*/;
- /**
- Brief comments about the binding, e.g. noting any major
- semantic differences.
- */
- String comment() default "";
-}
diff --git a/ext/jni/src/org/sqlite/jni/annotation/Experimental.java b/ext/jni/src/org/sqlite/jni/annotation/Experimental.java
new file mode 100644
index 0000000000..190435c858
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/annotation/Experimental.java
@@ -0,0 +1,30 @@
+/*
+** 2023-09-27
+**
+** 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 houses the Experimental annotation for the sqlite3 C API.
+*/
+package org.sqlite.jni.annotation;
+import java.lang.annotation.*;
+
+/**
+ This annotation is for flagging methods, constructors, and types
+ which are expressly experimental and subject to any amount of
+ change or outright removal. Client code should not rely on such
+ features.
+*/
+@Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target({
+ ElementType.METHOD,
+ ElementType.CONSTRUCTOR,
+ ElementType.TYPE
+})
+public @interface Experimental{}
diff --git a/ext/jni/src/org/sqlite/jni/annotation/NotNull.java b/ext/jni/src/org/sqlite/jni/annotation/NotNull.java
index 99eae7370a..0c31782f23 100644
--- a/ext/jni/src/org/sqlite/jni/annotation/NotNull.java
+++ b/ext/jni/src/org/sqlite/jni/annotation/NotNull.java
@@ -1,26 +1,71 @@
+/*
+** 2023-09-27
+**
+** 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 houses the NotNull annotation for the sqlite3 C API.
+*/
package org.sqlite.jni.annotation;
+import java.lang.annotation.*;
/**
This annotation is for flagging parameters which may not legally be
- null. When used in the context of callback methods which are
- called into from the C APIs, this annotation communicates that the
- C API will never pass a null value to the callback.
+ null or point to closed/finalized C-side resources.
+
+
In the case of Java types which map directly to C struct types
+ (e.g. {@link org.sqlite.jni.capi.sqlite3}, {@link
+ org.sqlite.jni.capi.sqlite3_stmt}, and {@link
+ org.sqlite.jni.capi.sqlite3_context}), a closed/finalized resource
+ is also considered to be null for purposes this annotation because
+ the C-side effect of passing such a handle is the same as if null
+ is passed.
+
+ When used in the context of Java interfaces which are called
+ from the C APIs, this annotation communicates that the C API will
+ never pass a null value to the callback for that parameter.
+
+ Passing a null, for this annotation's definition of null, for
+ any parameter marked with this annoation specifically invokes
+ undefined behavior (see below).
+
+ Passing 0 (i.e. C NULL) or a negative value for any long-type
+ parameter marked with this annoation specifically invokes undefined
+ behavior (see below). Such values are treated as C pointers in the
+ JNI layer.
+
+ Undefined behaviour: the JNI build uses the {@code
+ SQLITE_ENABLE_API_ARMOR} build flag, meaning that the C code
+ invoked with invalid NULL pointers and the like will not invoke
+ undefined behavior in the conventional C sense, but may, for
+ example, return result codes which are not documented for the
+ affected APIs or may otherwise behave unpredictably. In no known
+ cases will such arguments result in C-level code dereferencing a
+ NULL pointer or accessing out-of-bounds (or otherwise invalid)
+ memory. In other words, they may cause unexpected behavior but
+ should never cause an outright crash or security issue.
Note that the C-style API does not throw any exceptions on its
own because it has a no-throw policy in order to retain its C-style
semantics, but it may trigger NullPointerExceptions (or similar) if
- passed a null for a parameter flagged with this annotation.
+ passed a null for a parameter flagged with this annotation.
This annotation is informational only. No policy is in place to
programmatically ensure that NotNull is conformed to in client
- code.
+ code.
- This annotation is solely for the use by the classes in this
- package but is made public so that javadoc will link to it from the
- annotated functions. It is not part of the public API and
- client-level code must not rely on it.
+
This annotation is solely for the use by the classes in the
+ org.sqlite.jni package and subpackages, but is made public so that
+ javadoc will link to it from the annotated functions. It is not
+ part of the public API and client-level code must not rely on
+ it.
*/
-@java.lang.annotation.Documented
-@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
-@java.lang.annotation.Target(java.lang.annotation.ElementType.PARAMETER)
+@Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.PARAMETER)
public @interface NotNull{}
diff --git a/ext/jni/src/org/sqlite/jni/annotation/Nullable.java b/ext/jni/src/org/sqlite/jni/annotation/Nullable.java
index 7a011e33b1..e3fa30efc9 100644
--- a/ext/jni/src/org/sqlite/jni/annotation/Nullable.java
+++ b/ext/jni/src/org/sqlite/jni/annotation/Nullable.java
@@ -1,4 +1,18 @@
+/*
+** 2023-09-27
+**
+** 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 houses the Nullable annotation for the sqlite3 C API.
+*/
package org.sqlite.jni.annotation;
+import java.lang.annotation.*;
/**
This annotation is for flagging parameters which may legally be
@@ -13,7 +27,7 @@ package org.sqlite.jni.annotation;
annotated functions. It is not part of the public API and
client-level code must not rely on it.
*/
-@java.lang.annotation.Documented
-@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
-@java.lang.annotation.Target(java.lang.annotation.ElementType.PARAMETER)
+@Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.PARAMETER)
public @interface Nullable{}
diff --git a/ext/jni/src/org/sqlite/jni/annotation/package-info.java b/ext/jni/src/org/sqlite/jni/annotation/package-info.java
index 50db2a32bd..20ac7a3017 100644
--- a/ext/jni/src/org/sqlite/jni/annotation/package-info.java
+++ b/ext/jni/src/org/sqlite/jni/annotation/package-info.java
@@ -1,4 +1,17 @@
+/*
+** 2023-09-27
+**
+** 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 package houses annotations specific a JNI binding to the SQLite3 C API.
+ This package houses annotations specific to the JNI bindings of the
+ SQLite3 C API.
*/
package org.sqlite.jni.annotation;
diff --git a/ext/jni/src/org/sqlite/jni/AbstractCollationCallback.java b/ext/jni/src/org/sqlite/jni/capi/AbstractCollationCallback.java
similarity index 97%
rename from ext/jni/src/org/sqlite/jni/AbstractCollationCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/AbstractCollationCallback.java
index 63cac66a52..925536636e 100644
--- a/ext/jni/src/org/sqlite/jni/AbstractCollationCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/AbstractCollationCallback.java
@@ -11,7 +11,7 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
import org.sqlite.jni.annotation.NotNull;
/**
diff --git a/ext/jni/src/org/sqlite/jni/SQLFunction.java b/ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java
similarity index 62%
rename from ext/jni/src/org/sqlite/jni/SQLFunction.java
rename to ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java
index 66119ebe55..1fa6c6b805 100644
--- a/ext/jni/src/org/sqlite/jni/SQLFunction.java
+++ b/ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java
@@ -1,5 +1,5 @@
/*
-** 2023-07-22
+** 2023-08-25
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
@@ -11,27 +11,36 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
+
/**
- SQLFunction is used in conjunction with the
- sqlite3_create_function() JNI-bound API to give that native code
- access to the callback functions needed in order to implement SQL
- functions in Java.
-
-
-
- This class is not used by itself, but is a marker base class. The
- three UDF types are modelled by the inner classes Scalar,
- Aggregate, and Window. Most simply, clients may subclass
- those, or create anonymous classes from them, to implement
- UDFs. Clients are free to create their own classes for use with
- UDFs, so long as they conform to the public interfaces defined by
- those three classes. The JNI layer only actively relies on the
- SQLFunction base class and the method names and signatures used by
- the UDF callback interfaces.
+ A SQLFunction implementation for aggregate functions. Its T is the
+ data type of its "accumulator" state, an instance of which is
+ intended to be be managed using the getAggregateState() and
+ takeAggregateState() methods.
*/
-public interface SQLFunction {
+public abstract class AggregateFunction implements SQLFunction {
+
+ /**
+ As for the xStep() argument of the C API's
+ sqlite3_create_function(). If this function throws, the
+ exception is not propagated and a warning might be emitted to a
+ debugging channel.
+ */
+ public abstract void xStep(sqlite3_context cx, sqlite3_value[] args);
+
+ /**
+ As for the xFinal() argument of the C API's sqlite3_create_function().
+ If this function throws, it is translated into an sqlite3_result_error().
+ */
+ public abstract void xFinal(sqlite3_context cx);
+
+ /**
+ Optionally override to be notified when the UDF is finalized by
+ SQLite.
+ */
+ public void xDestroy() {}
/**
PerContextState assists aggregate and window functions in
@@ -100,4 +109,30 @@ public interface SQLFunction {
}
}
+ /** Per-invocation state for the UDF. */
+ private final PerContextState map = new 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 getAggregateState(sqlite3_context cx, T initialValue){
+ return map.getAggregateState(cx, 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(sqlite3_context cx){
+ return map.takeAggregateState(cx);
+ }
}
diff --git a/ext/jni/src/org/sqlite/jni/AuthorizerCallback.java b/ext/jni/src/org/sqlite/jni/capi/AuthorizerCallback.java
similarity index 69%
rename from ext/jni/src/org/sqlite/jni/AuthorizerCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/AuthorizerCallback.java
index a9f15fc6c2..298e3a5900 100644
--- a/ext/jni/src/org/sqlite/jni/AuthorizerCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/AuthorizerCallback.java
@@ -11,16 +11,17 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
import org.sqlite.jni.annotation.*;
/**
- Callback for use with {@link SQLite3Jni#sqlite3_set_authorizer}.
+ Callback for use with {@link CApi#sqlite3_set_authorizer}.
*/
-public interface AuthorizerCallback extends SQLite3CallbackProxy {
+public interface AuthorizerCallback extends CallbackProxy {
/**
Must function as described for the C-level
- sqlite3_set_authorizer() callback.
+ sqlite3_set_authorizer() callback. If it throws, the error is
+ converted to a db-level error and the exception is suppressed.
*/
int call(int opId, @Nullable String s1, @Nullable String s2,
@Nullable String s3, @Nullable String s4);
diff --git a/ext/jni/src/org/sqlite/jni/AutoExtensionCallback.java b/ext/jni/src/org/sqlite/jni/capi/AutoExtensionCallback.java
similarity index 86%
rename from ext/jni/src/org/sqlite/jni/AutoExtensionCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/AutoExtensionCallback.java
index 1f8ace2fb8..7a54132d29 100644
--- a/ext/jni/src/org/sqlite/jni/AutoExtensionCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/AutoExtensionCallback.java
@@ -11,13 +11,13 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
- Callback for use with the {@link SQLite3Jni#sqlite3_auto_extension}
+ Callback for use with the {@link CApi#sqlite3_auto_extension}
family of APIs.
*/
-public interface AutoExtensionCallback extends SQLite3CallbackProxy {
+public interface AutoExtensionCallback extends CallbackProxy {
/**
Must function as described for a C-level
sqlite3_auto_extension() callback.
diff --git a/ext/jni/src/org/sqlite/jni/BusyHandlerCallback.java b/ext/jni/src/org/sqlite/jni/capi/BusyHandlerCallback.java
similarity index 80%
rename from ext/jni/src/org/sqlite/jni/BusyHandlerCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/BusyHandlerCallback.java
index db9295bb61..00223f0b66 100644
--- a/ext/jni/src/org/sqlite/jni/BusyHandlerCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/BusyHandlerCallback.java
@@ -11,12 +11,12 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
- Callback for use with {@link SQLite3Jni#sqlite3_busy_handler}.
+ Callback for use with {@link CApi#sqlite3_busy_handler}.
*/
-public interface BusyHandlerCallback extends SQLite3CallbackProxy {
+public interface BusyHandlerCallback extends CallbackProxy {
/**
Must function as documented for the C-level
sqlite3_busy_handler() callback argument, minus the (void*)
diff --git a/ext/jni/src/org/sqlite/jni/SQLite3Jni.java b/ext/jni/src/org/sqlite/jni/capi/CApi.java
similarity index 51%
rename from ext/jni/src/org/sqlite/jni/SQLite3Jni.java
rename to ext/jni/src/org/sqlite/jni/capi/CApi.java
index d32320b26f..b5d08306e2 100644
--- a/ext/jni/src/org/sqlite/jni/SQLite3Jni.java
+++ b/ext/jni/src/org/sqlite/jni/capi/CApi.java
@@ -9,9 +9,9 @@
** May you share freely, never taking more than you give.
**
*************************************************************************
-** This file declares JNI bindings for the sqlite3 C API.
+** This file declares the main JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
import java.nio.charset.StandardCharsets;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -19,6 +19,7 @@ import java.lang.annotation.Target;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import org.sqlite.jni.annotation.*;
+import java.util.Arrays;
/**
This class contains the entire C-style sqlite3 JNI API binding,
@@ -26,12 +27,11 @@ import org.sqlite.jni.annotation.*;
use, a static import is recommended:
{@code
- import static org.sqlite.jni.SQLite3Jni.*;
+ import static org.sqlite.jni.capi.CApi.*;
}
The C-side part can be found in sqlite3-jni.c.
-
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
@@ -47,12 +47,6 @@ import org.sqlite.jni.annotation.*;
#sqlite3_result_int}, and sqlite3_result_set() has many
type-specific overloads.
-
Though most of the {@code SQLITE_abc...} C macros represented by
- this class are defined as final, a few are necessarily non-final
- because they cannot be set until static class-level initialization
- is run. Modifying them at runtime has no effect on the library but
- may confuse any client-level code which uses them.
-
Notes regarding Java's Modified UTF-8 vs standard UTF-8:
SQLite internally uses UTF-8 encoding, whereas Java natively uses
@@ -74,8 +68,8 @@ import org.sqlite.jni.annotation.*;
Java strings converted to byte arrays for encoding purposes are not
NUL-terminated, and conversion to a Java byte array must sometimes
be careful to add one. Functions which take a length do not require
- this so long as the length is provided. Search the SQLite3Jni class
- for "\0" for many examples.
+ this so long as the length is provided. Search the CApi class
+ for "\0" for examples.
@@ -88,15 +82,23 @@ import org.sqlite.jni.annotation.*;
https://docs.oracle.com/javase/8/docs/api/java/io/DataInput.html#modified-utf-8
*/
-public final class SQLite3Jni {
+public final class CApi {
static {
System.loadLibrary("sqlite3-jni");
}
//! Not used
- private SQLite3Jni(){}
+ private CApi(){}
//! Called from static init code.
private static native void init();
+ /**
+ Returns a nul-terminated copy of s as a UTF-8-encoded byte array,
+ or null if s is null.
+ */
+ private static byte[] nulTerminateUtf8(String s){
+ return null==s ? null : (s+"\0").getBytes(StandardCharsets.UTF_8);
+ }
+
/**
Each thread which uses the SQLite3 JNI APIs should call
sqlite3_jni_uncache_thread() when it is done with the library -
@@ -117,11 +119,38 @@ public final class SQLite3Jni {
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
- decisions.
+ which client-level code can use to make any informed
+ decisions. Its return type and semantics are not considered
+ stable and may change at any time.
*/
public static native boolean sqlite3_java_uncache_thread();
+ /**
+ Returns true if this JVM has JNI-level support for C-level direct
+ memory access using java.nio.ByteBuffer, else returns false.
+ */
+ @Experimental
+ public static native boolean sqlite3_jni_supports_nio();
+
+ /**
+ For internal use only. Sets the given db's error code and
+ (optionally) string. If rc is 0, it defaults to SQLITE_ERROR.
+
+ On success it returns rc. On error it may return a more serious
+ code, such as SQLITE_NOMEM. Returns SQLITE_MISUSE if db is null.
+ */
+ static native int sqlite3_jni_db_error(@NotNull sqlite3 db,
+ int rc, @Nullable String msg);
+
+ /**
+ Convenience overload which uses e.toString() as the error
+ message.
+ */
+ static int sqlite3_jni_db_error(@NotNull sqlite3 db,
+ int rc, @NotNull Exception e){
+ return sqlite3_jni_db_error(db, rc, e.toString());
+ }
+
//////////////////////////////////////////////////////////////////////
// Maintenance reminder: please keep the sqlite3_.... functions
// alphabetized. The SQLITE_... values. on the other hand, are
@@ -143,107 +172,268 @@ public final class SQLite3Jni {
allocation error. In all casses, 0 is considered the sentinel
"not a key" value.
*/
- @Canonical
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
+
- The callback interface 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, 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.
+ execute for the current database. That is, doing so will result
+ in unpredictable, but not undefined, behavior.
See the AutoExtension class docs for more information.
*/
- @Canonical
public static native int sqlite3_auto_extension(@NotNull AutoExtensionCallback callback);
- /**
- Results are undefined if data is not null and n<0 || n>=data.length.
- */
- @Canonical
- public static native int sqlite3_bind_blob(
- @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int n
+ private static native int sqlite3_backup_finish(@NotNull long ptrToBackup);
+
+ public static int sqlite3_backup_finish(@NotNull sqlite3_backup b){
+ return null==b ? 0 : sqlite3_backup_finish(b.clearNativePointer());
+ }
+
+ private static native sqlite3_backup sqlite3_backup_init(
+ @NotNull long ptrToDbDest, @NotNull String destTableName,
+ @NotNull long ptrToDbSrc, @NotNull String srcTableName
);
+ public static sqlite3_backup sqlite3_backup_init(
+ @NotNull sqlite3 dbDest, @NotNull String destTableName,
+ @NotNull sqlite3 dbSrc, @NotNull String srcTableName
+ ){
+ return sqlite3_backup_init( dbDest.getNativePointer(), destTableName,
+ dbSrc.getNativePointer(), srcTableName );
+ }
+
+ private static native int sqlite3_backup_pagecount(@NotNull long ptrToBackup);
+
+ public static int sqlite3_backup_pagecount(@NotNull sqlite3_backup b){
+ return sqlite3_backup_pagecount(b.getNativePointer());
+ }
+
+ private static native int sqlite3_backup_remaining(@NotNull long ptrToBackup);
+
+ public static int sqlite3_backup_remaining(@NotNull sqlite3_backup b){
+ return sqlite3_backup_remaining(b.getNativePointer());
+ }
+
+ private static native int sqlite3_backup_step(@NotNull long ptrToBackup, int nPage);
+
+ public static int sqlite3_backup_step(@NotNull sqlite3_backup b, int nPage){
+ return sqlite3_backup_step(b.getNativePointer(), nPage);
+ }
+
+ private static native int sqlite3_bind_blob(
+ @NotNull long ptrToStmt, int ndx, @Nullable byte[] data, int n
+ );
+
+ /**
+ If n is negative, SQLITE_MISUSE is returned. If n>data.length
+ then n is silently truncated to data.length.
+ */
+ public static int sqlite3_bind_blob(
+ @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int n
+ ){
+ return sqlite3_bind_blob(stmt.getNativePointer(), ndx, data, n);
+ }
+
public static int sqlite3_bind_blob(
@NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data
){
return (null==data)
- ? sqlite3_bind_null(stmt, ndx)
- : sqlite3_bind_blob(stmt, ndx, data, data.length);
+ ? sqlite3_bind_null(stmt.getNativePointer(), ndx)
+ : sqlite3_bind_blob(stmt.getNativePointer(), ndx, data, data.length);
}
- @Canonical
- public static native int sqlite3_bind_double(
+ /**
+ Convenience overload which is a simple proxy for
+ sqlite3_bind_nio_buffer().
+ */
+ @Experimental
+ /*public*/ static int sqlite3_bind_blob(
+ @NotNull sqlite3_stmt stmt, int ndx, @Nullable java.nio.ByteBuffer data,
+ int begin, int n
+ ){
+ return sqlite3_bind_nio_buffer(stmt, ndx, data, begin, n);
+ }
+
+ /**
+ Convenience overload which is equivalant to passing its arguments
+ to sqlite3_bind_nio_buffer() with the values 0 and -1 for the
+ final two arguments.
+ */
+ @Experimental
+ /*public*/ static int sqlite3_bind_blob(
+ @NotNull sqlite3_stmt stmt, int ndx, @Nullable java.nio.ByteBuffer data
+ ){
+ return sqlite3_bind_nio_buffer(stmt, ndx, data, 0, -1);
+ }
+
+ private static native int sqlite3_bind_double(
+ @NotNull long ptrToStmt, int ndx, double v
+ );
+
+ public static int sqlite3_bind_double(
@NotNull sqlite3_stmt stmt, int ndx, double v
+ ){
+ return sqlite3_bind_double(stmt.getNativePointer(), ndx, v);
+ }
+
+ private static native int sqlite3_bind_int(
+ @NotNull long ptrToStmt, int ndx, int v
);
- @Canonical
- public static native int sqlite3_bind_int(
+ public static int sqlite3_bind_int(
@NotNull sqlite3_stmt stmt, int ndx, int v
+ ){
+ return sqlite3_bind_int(stmt.getNativePointer(), ndx, v);
+ }
+
+ private static native int sqlite3_bind_int64(
+ @NotNull long ptrToStmt, int ndx, long v
);
- @Canonical
- public static native int sqlite3_bind_int64(
- @NotNull sqlite3_stmt stmt, int ndx, long v
+ public static int sqlite3_bind_int64(@NotNull sqlite3_stmt stmt, int ndx, long v){
+ return sqlite3_bind_int64( stmt.getNativePointer(), ndx, v );
+ }
+
+ private static native int sqlite3_bind_java_object(
+ @NotNull long ptrToStmt, int ndx, @Nullable Object o
);
/**
- Binds the given object at the given index.
+ Binds the contents of the given buffer object as a blob.
+
+ The byte range of the buffer may be restricted by providing a
+ start index and a number of bytes. beginPos may not be negative.
+ Negative howMany is interpretated as the remainder of the buffer
+ past the given start position, up to the buffer's limit() (as
+ opposed its capacity()).
+
+ If beginPos+howMany would extend past the limit() of the buffer
+ then SQLITE_ERROR is returned.
+
+ If any of the following are true, this function behaves like
+ sqlite3_bind_null(): the buffer is null, beginPos is at or past
+ the end of the buffer, howMany is 0, or the calculated slice of
+ the blob has a length of 0.
+
+ If ndx is out of range, it returns SQLITE_RANGE, as documented
+ for sqlite3_bind_blob(). If beginPos is negative or if
+ sqlite3_jni_supports_nio() returns false then SQLITE_MISUSE is
+ returned. Note that this function is bound (as it were) by the
+ SQLITE_LIMIT_LENGTH constraint and SQLITE_TOOBIG is returned if
+ the resulting slice of the buffer exceeds that limit.
+
+ This function does not modify the buffer's streaming-related
+ cursors.
+
+ If the buffer is modified in a separate thread while this
+ operation is running, results are undefined and will likely
+ result in corruption of the bound data or a segmentation fault.
+
+ Design note: this function should arguably take a java.nio.Buffer
+ instead of ByteBuffer, but it can only operate on "direct"
+ buffers and the only such class offered by Java is (apparently)
+ ByteBuffer.
+
+ @see https://docs.oracle.com/javase/8/docs/api/java/nio/Buffer.html
+ */
+ @Experimental
+ /*public*/ static native int sqlite3_bind_nio_buffer(
+ @NotNull sqlite3_stmt stmt, int ndx, @Nullable java.nio.ByteBuffer data,
+ int beginPos, int howMany
+ );
+
+ /**
+ Convenience overload which binds the given buffer's entire
+ contents, up to its limit() (as opposed to its capacity()).
+ */
+ @Experimental
+ /*public*/ static int sqlite3_bind_nio_buffer(
+ @NotNull sqlite3_stmt stmt, int ndx, @Nullable java.nio.ByteBuffer data
+ ){
+ return sqlite3_bind_nio_buffer(stmt, ndx, data, 0, -1);
+ }
+
+ /**
+ Binds the given object at the given index. If o is null then this behaves like
+ sqlite3_bind_null().
@see #sqlite3_result_java_object
*/
- public static native int sqlite3_bind_java_object(
- @NotNull sqlite3_stmt cx, int ndx, @Nullable Object o
- );
+ public static int sqlite3_bind_java_object(
+ @NotNull sqlite3_stmt stmt, int ndx, @Nullable Object o
+ ){
+ return sqlite3_bind_java_object(stmt.getNativePointer(), ndx, o);
+ }
- @Canonical
- public static native int sqlite3_bind_null(
- @NotNull sqlite3_stmt stmt, int ndx
- );
+ private static native int sqlite3_bind_null(@NotNull long ptrToStmt, int ndx);
- @Canonical
- public static native int sqlite3_bind_parameter_count(
- @NotNull sqlite3_stmt stmt
- );
+ public static int sqlite3_bind_null(@NotNull sqlite3_stmt stmt, int ndx){
+ return sqlite3_bind_null(stmt.getNativePointer(), ndx);
+ }
+
+ private static native int sqlite3_bind_parameter_count(@NotNull long ptrToStmt);
+
+ public static int sqlite3_bind_parameter_count(@NotNull sqlite3_stmt stmt){
+ return sqlite3_bind_parameter_count(stmt.getNativePointer());
+ }
/**
Requires that paramName be a NUL-terminated UTF-8 string.
+
+ This overload is private because: (A) to keep users from
+ inadvertently passing non-NUL-terminated byte arrays (an easy
+ thing to do). (B) it is cheaper to NUL-terminate the
+ String-to-byte-array conversion in the public-facing Java-side
+ overload than to do that in C, so that signature is the
+ public-facing one.
*/
- @Canonical
- public static native int sqlite3_bind_parameter_index(
- @NotNull sqlite3_stmt stmt, byte[] paramName
+ private static native int sqlite3_bind_parameter_index(
+ @NotNull long ptrToStmt, @NotNull byte[] paramName
);
- @Canonical
public static int sqlite3_bind_parameter_index(
@NotNull sqlite3_stmt stmt, @NotNull String paramName
){
- final byte[] utf8 = (paramName+"\0").getBytes(StandardCharsets.UTF_8);
- return sqlite3_bind_parameter_index(stmt, utf8);
+ final byte[] utf8 = nulTerminateUtf8(paramName);
+ return null==utf8 ? 0 : sqlite3_bind_parameter_index(stmt.getNativePointer(), utf8);
}
+ private static native String sqlite3_bind_parameter_name(
+ @NotNull long ptrToStmt, int index
+ );
+
+ public static String sqlite3_bind_parameter_name(@NotNull sqlite3_stmt stmt, int index){
+ return sqlite3_bind_parameter_name(stmt.getNativePointer(), index);
+ }
+
+ private static native int sqlite3_bind_text(
+ @NotNull long ptrToStmt, int ndx, @Nullable byte[] utf8, int maxBytes
+ );
+
/**
Works like the C-level sqlite3_bind_text() but assumes
SQLITE_TRANSIENT for the final C API parameter. The byte array is
assumed to be in UTF-8 encoding.
-
Results are undefined if data is not null and
- maxBytes>=utf8.length. If maxBytes is negative then results are
- undefined if data is not null and does not contain a NUL byte.
+
If data is not null and maxBytes>utf8.length then maxBytes is
+ silently truncated to utf8.length. If maxBytes is negative then
+ results are undefined if data is not null and does not contain a
+ NUL byte.
*/
- @Canonical
- public static native int sqlite3_bind_text(
+ static int sqlite3_bind_text(
@NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] utf8, int maxBytes
- );
+ ){
+ return sqlite3_bind_text(stmt.getNativePointer(), ndx, utf8, maxBytes);
+ }
/**
Converts data, if not null, to a UTF-8-encoded byte array and
@@ -253,9 +443,9 @@ public final class SQLite3Jni {
public static int sqlite3_bind_text(
@NotNull sqlite3_stmt stmt, int ndx, @Nullable String data
){
- if(null == data) return sqlite3_bind_null(stmt, ndx);
+ if( null==data ) return sqlite3_bind_null(stmt.getNativePointer(), ndx);
final byte[] utf8 = data.getBytes(StandardCharsets.UTF_8);
- return sqlite3_bind_text(stmt, ndx, utf8, utf8.length);
+ return sqlite3_bind_text(stmt.getNativePointer(), ndx, utf8, utf8.length);
}
/**
@@ -264,20 +454,25 @@ public final class SQLite3Jni {
public static int sqlite3_bind_text(
@NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] utf8
){
- return (null == utf8)
- ? sqlite3_bind_null(stmt, ndx)
- : sqlite3_bind_text(stmt, ndx, utf8, utf8.length);
+ return ( null==utf8 )
+ ? sqlite3_bind_null(stmt.getNativePointer(), ndx)
+ : sqlite3_bind_text(stmt.getNativePointer(), ndx, utf8, utf8.length);
}
+ private static native int sqlite3_bind_text16(
+ @NotNull long ptrToStmt, int ndx, @Nullable byte[] data, int maxBytes
+ );
+
/**
Identical to the sqlite3_bind_text() overload with the same
signature but requires that its input be encoded in UTF-16 in
platform byte order.
*/
- @Canonical
- public static native int sqlite3_bind_text16(
+ static int sqlite3_bind_text16(
@NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int maxBytes
- );
+ ){
+ return sqlite3_bind_text16(stmt.getNativePointer(), ndx, data, maxBytes);
+ }
/**
Converts its string argument to UTF-16 and binds it as such, returning
@@ -289,7 +484,7 @@ public final class SQLite3Jni {
){
if(null == data) return sqlite3_bind_null(stmt, ndx);
final byte[] bytes = data.getBytes(StandardCharsets.UTF_16);
- return sqlite3_bind_text16(stmt, ndx, bytes, bytes.length);
+ return sqlite3_bind_text16(stmt.getNativePointer(), ndx, bytes, bytes.length);
}
/**
@@ -301,143 +496,470 @@ public final class SQLite3Jni {
@NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data
){
return (null == data)
- ? sqlite3_bind_null(stmt, ndx)
- : sqlite3_bind_text16(stmt, ndx, data, data.length);
+ ? sqlite3_bind_null(stmt.getNativePointer(), ndx)
+ : sqlite3_bind_text16(stmt.getNativePointer(), ndx, data, data.length);
}
- @Canonical
- public static native int sqlite3_bind_zeroblob(
- @NotNull sqlite3_stmt stmt, int ndx, int n
+ private static native int sqlite3_bind_value(@NotNull long ptrToStmt, int ndx, long ptrToValue);
+
+ /**
+ Functions like the C-level sqlite3_bind_value(), or
+ sqlite3_bind_null() if val is null.
+ */
+ public static int sqlite3_bind_value(@NotNull sqlite3_stmt stmt, int ndx, sqlite3_value val){
+ return sqlite3_bind_value(stmt.getNativePointer(), ndx,
+ null==val ? 0L : val.getNativePointer());
+ }
+
+ private static native int sqlite3_bind_zeroblob(@NotNull long ptrToStmt, int ndx, int n);
+
+ public static int sqlite3_bind_zeroblob(@NotNull sqlite3_stmt stmt, int ndx, int n){
+ return sqlite3_bind_zeroblob(stmt.getNativePointer(), ndx, n);
+ }
+
+ private static native int sqlite3_bind_zeroblob64(
+ @NotNull long ptrToStmt, int ndx, long n
);
- @Canonical
- public static native int sqlite3_bind_zeroblob64(
- @NotNull sqlite3_stmt stmt, int ndx, long n
+ public static int sqlite3_bind_zeroblob64(@NotNull sqlite3_stmt stmt, int ndx, long n){
+ return sqlite3_bind_zeroblob64(stmt.getNativePointer(), ndx, n);
+ }
+
+ private static native int sqlite3_blob_bytes(@NotNull long ptrToBlob);
+
+ public static int sqlite3_blob_bytes(@NotNull sqlite3_blob blob){
+ return sqlite3_blob_bytes(blob.getNativePointer());
+ }
+
+ private static native int sqlite3_blob_close(@Nullable long ptrToBlob);
+
+ public static int sqlite3_blob_close(@Nullable sqlite3_blob blob){
+ return null==blob ? 0 : sqlite3_blob_close(blob.clearNativePointer());
+ }
+
+ private static native int sqlite3_blob_open(
+ @NotNull long ptrToDb, @NotNull String dbName,
+ @NotNull String tableName, @NotNull String columnName,
+ long iRow, int flags, @NotNull OutputPointer.sqlite3_blob out
+ );
+
+ public static int sqlite3_blob_open(
+ @NotNull sqlite3 db, @NotNull String dbName,
+ @NotNull String tableName, @NotNull String columnName,
+ long iRow, int flags, @NotNull OutputPointer.sqlite3_blob out
+ ){
+ return sqlite3_blob_open(db.getNativePointer(), dbName, tableName,
+ columnName, iRow, flags, out);
+ }
+
+ /**
+ Convenience overload.
+ */
+ public static sqlite3_blob sqlite3_blob_open(
+ @NotNull sqlite3 db, @NotNull String dbName,
+ @NotNull String tableName, @NotNull String columnName,
+ long iRow, int flags ){
+ final OutputPointer.sqlite3_blob out = new OutputPointer.sqlite3_blob();
+ sqlite3_blob_open(db.getNativePointer(), dbName, tableName, columnName,
+ iRow, flags, out);
+ return out.take();
+ };
+
+ private static native int sqlite3_blob_read(
+ @NotNull long ptrToBlob, @NotNull byte[] target, int srcOffset
);
/**
- As for the C-level function of the same name, with a BusyHandlerCallback
- instance in place of a callback function. Pass it a null handler
- to clear the busy handler.
+ As per C's sqlite3_blob_read(), but writes its output to the
+ given byte array. Note that the final argument is the offset of
+ the source buffer, not the target array.
+ */
+ public static int sqlite3_blob_read(
+ @NotNull sqlite3_blob src, @NotNull byte[] target, int srcOffset
+ ){
+ return sqlite3_blob_read(src.getNativePointer(), target, srcOffset);
+ }
+
+ /**
+ An internal level of indirection.
*/
- @Canonical
- public static native int sqlite3_busy_handler(
+ @Experimental
+ private static native int sqlite3_blob_read_nio_buffer(
+ @NotNull long ptrToBlob, int srcOffset,
+ @NotNull java.nio.ByteBuffer tgt, int tgtOffset, int howMany
+ );
+
+ /**
+ Reads howMany bytes from offset srcOffset of src into position
+ tgtOffset of tgt.
+
+ Returns SQLITE_MISUSE if src is null, tgt is null, or
+ sqlite3_jni_supports_nio() returns false. Returns SQLITE_ERROR if
+ howMany or either offset are negative. If argument validation
+ succeeds, it returns the result of the underlying call to
+ sqlite3_blob_read() (0 on success).
+ */
+ @Experimental
+ /*public*/ static int sqlite3_blob_read_nio_buffer(
+ @NotNull sqlite3_blob src, int srcOffset,
+ @NotNull java.nio.ByteBuffer tgt, int tgtOffset, int howMany
+ ){
+ return (JNI_SUPPORTS_NIO && src!=null && tgt!=null)
+ ? sqlite3_blob_read_nio_buffer(
+ src.getNativePointer(), srcOffset, tgt, tgtOffset, howMany
+ )
+ : SQLITE_MISUSE;
+ }
+
+ /**
+ Convenience overload which reads howMany bytes from position
+ srcOffset of src and returns the result as a new ByteBuffer.
+
+ srcOffset may not be negative. If howMany is negative, it is
+ treated as all bytes following srcOffset.
+
+ Returns null if sqlite3_jni_supports_nio(), any arguments are
+ invalid, if the number of bytes to read is 0 or is larger than
+ the src blob, or the underlying call to sqlite3_blob_read() fails
+ for any reason.
+ */
+ @Experimental
+ /*public*/ static java.nio.ByteBuffer sqlite3_blob_read_nio_buffer(
+ @NotNull sqlite3_blob src, int srcOffset, int howMany
+ ){
+ if( !JNI_SUPPORTS_NIO || src==null ) return null;
+ else if( srcOffset<0 ) return null;
+ final int nB = sqlite3_blob_bytes(src);
+ if( srcOffset>=nB ) return null;
+ else if( howMany<0 ) howMany = nB - srcOffset;
+ if( srcOffset + howMany > nB ) return null;
+ final java.nio.ByteBuffer tgt =
+ java.nio.ByteBuffer.allocateDirect(howMany);
+ final int rc = sqlite3_blob_read_nio_buffer(
+ src.getNativePointer(), srcOffset, tgt, 0, howMany
+ );
+ return 0==rc ? tgt : null;
+ }
+
+ /**
+ Overload alias for sqlite3_blob_read_nio_buffer().
+ */
+ @Experimental
+ /*public*/ static int sqlite3_blob_read(
+ @NotNull sqlite3_blob src, int srcOffset,
+ @NotNull java.nio.ByteBuffer tgt,
+ int tgtOffset, int howMany
+ ){
+ return sqlite3_blob_read_nio_buffer(
+ src, srcOffset, tgt, tgtOffset, howMany
+ );
+ }
+
+ /**
+ Convenience overload which uses 0 for both src and tgt offsets
+ and reads a number of bytes equal to the smaller of
+ sqlite3_blob_bytes(src) and tgt.limit().
+
+ On success it sets tgt.limit() to the number of bytes read. On
+ error, tgt.limit() is not modified.
+
+ Returns 0 on success. Returns SQLITE_MISUSE is either argument is
+ null or sqlite3_jni_supports_nio() returns false. Else it returns
+ the result of the underlying call to sqlite3_blob_read().
+ */
+ @Experimental
+ /*public*/ static int sqlite3_blob_read(
+ @NotNull sqlite3_blob src,
+ @NotNull java.nio.ByteBuffer tgt
+ ){
+ if(!JNI_SUPPORTS_NIO || src==null || tgt==null) return SQLITE_MISUSE;
+ final int nSrc = sqlite3_blob_bytes(src);
+ final int nTgt = tgt.limit();
+ final int nRead = nTgt T sqlite3_column_java_object(
+ @NotNull sqlite3_stmt stmt, int ndx, @NotNull Class type
+ ){
+ final Object o = sqlite3_column_java_object(stmt, ndx);
+ return type.isInstance(o) ? (T)o : null;
+ }
+
+ private static native String sqlite3_column_name(@NotNull long ptrToStmt, int ndx);
+
+ public static String sqlite3_column_name(@NotNull sqlite3_stmt stmt, int ndx){
+ return sqlite3_column_name(stmt.getNativePointer(), ndx);
+ }
+
+ /**
+ A variant of sqlite3_column_blob() which returns the blob as a
+ ByteBuffer object. Returns null if its argument is null, if
+ sqlite3_jni_supports_nio() is false, or if sqlite3_column_blob()
+ would return null for the same inputs.
+ */
+ @Experimental
+ /*public*/ static native java.nio.ByteBuffer sqlite3_column_nio_buffer(
+ @NotNull sqlite3_stmt stmt, int ndx
+ );
+
+ private static native String sqlite3_column_origin_name(@NotNull long ptrToStmt, int ndx);
+
+ /**
+ Only available if built with SQLITE_ENABLE_COLUMN_METADATA.
+ */
+ public static String sqlite3_column_origin_name(@NotNull sqlite3_stmt stmt, int ndx){
+ return sqlite3_column_origin_name(stmt.getNativePointer(), ndx);
+ }
+
+ private static native String sqlite3_column_table_name(@NotNull long ptrToStmt, int ndx);
+
+ /**
+ Only available if built with SQLITE_ENABLE_COLUMN_METADATA.
+ */
+ public static String sqlite3_column_table_name(@NotNull sqlite3_stmt stmt, int ndx){
+ return sqlite3_column_table_name(stmt.getNativePointer(), ndx);
+ }
+
+ /**
+ Functions identially to the C API, and this note is just to
+ stress that the returned bytes are encoded as UTF-8. It returns
+ null if the underlying C-level sqlite3_column_text() returns NULL
+ or on allocation error.
+
+ @see #sqlite3_column_text16(sqlite3_stmt,int)
+ */
+ public static native byte[] sqlite3_column_text(
@NotNull sqlite3_stmt stmt, int ndx
);
- @Canonical
public static native String sqlite3_column_text16(
@NotNull sqlite3_stmt stmt, int ndx
);
@@ -471,7 +993,7 @@ public final class SQLite3Jni {
// }
// case SQLITE_FLOAT: rv = new Double(sqlite3_value_double(v)); break;
// case SQLITE_BLOB: rv = sqlite3_value_blob(v); break;
- // case SQLITE_TEXT: rv = sqlite3_value_text(v); break;
+ // case SQLITE_TEXT: rv = sqlite3_value_text16(v); break;
// default: break;
// }
// }
@@ -479,43 +1001,77 @@ public final class SQLite3Jni {
// return rv;
// }
- @Canonical
- public static native int sqlite3_column_type(
- @NotNull sqlite3_stmt stmt, int ndx
- );
+ private static native int sqlite3_column_type(@NotNull long ptrToStmt, int ndx);
+
+ public static int sqlite3_column_type(@NotNull sqlite3_stmt stmt, int ndx){
+ return sqlite3_column_type(stmt.getNativePointer(), ndx);
+ }
- @Canonical
public static native sqlite3_value sqlite3_column_value(
@NotNull sqlite3_stmt stmt, int ndx
);
+ private static native int sqlite3_collation_needed(
+ @NotNull long ptrToDb, @Nullable CollationNeededCallback callback
+ );
+
/**
This functions like C's sqlite3_collation_needed16() because
- Java's string type is compatible with that interface.
+ Java's string type is inherently compatible with that interface.
*/
- @Canonical
- public static native int sqlite3_collation_needed(
+ public static int sqlite3_collation_needed(
@NotNull sqlite3 db, @Nullable CollationNeededCallback callback
+ ){
+ return sqlite3_collation_needed(db.getNativePointer(), callback);
+ }
+
+ private static native CommitHookCallback sqlite3_commit_hook(
+ @NotNull long ptrToDb, @Nullable CommitHookCallback hook
);
- @Canonical
- public static native sqlite3 sqlite3_context_db_handle(
- @NotNull sqlite3_context cx
- );
-
- @Canonical
- public static native CommitHookCallback sqlite3_commit_hook(
+ public static CommitHookCallback sqlite3_commit_hook(
@NotNull sqlite3 db, @Nullable CommitHookCallback hook
+ ){
+ return sqlite3_commit_hook(db.getNativePointer(), hook);
+ }
+
+ public static native String sqlite3_compileoption_get(int n);
+
+ public static native boolean sqlite3_compileoption_used(String optName);
+
+ /**
+ This implementation is private because it's too easy to pass it
+ non-NUL-terminated byte arrays from client code.
+ */
+ private static native int sqlite3_complete(
+ @NotNull byte[] nulTerminatedUtf8Sql
);
- @Canonical
- public static native String sqlite3_compileoption_get(
- int n
+ /**
+ Unlike the C API, this returns SQLITE_MISUSE if its argument is
+ null (as opposed to invoking UB).
+ */
+ public static int sqlite3_complete(@NotNull String sql){
+ return sqlite3_complete( nulTerminateUtf8(sql) );
+ }
+
+ /**
+ Internal level of indirection for sqlite3_config(int).
+ */
+ private static native int sqlite3_config__enable(int op);
+
+ /**
+ Internal level of indirection for sqlite3_config(ConfigLogCallback).
+ */
+ private static native int sqlite3_config__CONFIG_LOG(
+ @Nullable ConfigLogCallback logger
);
- @Canonical
- public static native boolean sqlite3_compileoption_used(
- @NotNull String optName
+ /**
+ Internal level of indirection for sqlite3_config(ConfigSqlLogCallback).
+ */
+ private static native int sqlite3_config__SQLLOG(
+ @Nullable ConfigSqlLogCallback logger
);
/**
@@ -532,17 +1088,15 @@ public final class SQLite3Jni {
Note that sqlite3_config() is not threadsafe with regards to
the rest of the library. This must not be called when any other
library APIs are being called.
-
*/
- @Canonical(comment="Option subset: "+
- "SQLITE_CONFIG_SINGLETHREAD, SQLITE_CONFIG_MULTITHREAD, "+
- "SQLITE_CONFIG_SERIALIZED")
- public static native int sqlite3_config(int op);
+ public static int sqlite3_config(int op){
+ return sqlite3_config__enable(op);
+ }
/**
If the native library was built with SQLITE_ENABLE_SQLLOG defined
then this acts as a proxy for C's
- sqlite3_config(SQLITE_ENABLE_SQLLOG,...). This sets or clears the
+ sqlite3_config(SQLITE_CONFIG_SQLLOG,...). This sets or clears the
logger. If installation of a logger fails, any previous logger is
retained.
@@ -552,12 +1106,27 @@ public final class SQLite3Jni {
Note that sqlite3_config() is not threadsafe with regards to
the rest of the library. This must not be called when any other
library APIs are being called.
-
*/
- @Canonical(comment="Option subset: SQLITE_CONFIG_SQLLOG")
- public static native int sqlite3_config( @Nullable ConfigSqllogCallback logger );
+ public static int sqlite3_config( @Nullable ConfigSqlLogCallback logger ){
+ return sqlite3_config__SQLLOG(logger);
+ }
+
+ /**
+ The sqlite3_config() overload for handling the SQLITE_CONFIG_LOG
+ option.
+ */
+ public static int sqlite3_config( @Nullable ConfigLogCallback logger ){
+ return sqlite3_config__CONFIG_LOG(logger);
+ }
+
+ /**
+ Unlike the C API, this returns null if its argument is
+ null (as opposed to invoking UB).
+ */
+ public static native sqlite3 sqlite3_context_db_handle(
+ @NotNull sqlite3_context cx
+ );
- @Canonical
public static native int sqlite3_create_collation(
@NotNull sqlite3 db, @NotNull String name, int eTextRep,
@NotNull CollationCallback col
@@ -570,24 +1139,29 @@ public final class SQLite3Jni {
depends on which methods the final argument implements. See
SQLFunction's subclasses (ScalarFunction, AggregateFunction,
and WindowFunction) for details.
+
+ Unlike the C API, this returns SQLITE_MISUSE null if its db or
+ functionName arguments are null (as opposed to invoking UB).
*/
- @Canonical
public static native int sqlite3_create_function(
@NotNull sqlite3 db, @NotNull String functionName,
int nArg, int eTextRep, @NotNull SQLFunction func
);
- @Canonical
- public static native int sqlite3_data_count(
- @NotNull sqlite3_stmt stmt
- );
+ private static native int sqlite3_data_count(@NotNull long ptrToStmt);
+
+ public static int sqlite3_data_count(@NotNull sqlite3_stmt stmt){
+ return sqlite3_data_count(stmt.getNativePointer());
+ }
/**
Overload for sqlite3_db_config() calls which take (int,int*)
variadic arguments. Returns SQLITE_MISUSE if op is not one of the
SQLITE_DBCONFIG_... options which uses this call form.
+
+
Unlike the C API, this returns SQLITE_MISUSE if its db argument
+ is null (as opposed to invoking UB).
*/
- @Canonical
public static native int sqlite3_db_config(
@NotNull sqlite3 db, int op, int onOff, @Nullable OutputPointer.Int32 out
);
@@ -599,73 +1173,102 @@ public final class SQLite3Jni {
SQLITE_DBCONFIG_MAINDBNAME, but that set of options may be
extended in future versions.
*/
- @Canonical(comment="Supports only a subset of options.")
public static native int sqlite3_db_config(
@NotNull sqlite3 db, int op, @NotNull String val
);
- @Canonical
+ private static native String sqlite3_db_name(@NotNull long ptrToDb, int ndx);
+
+ public static String sqlite3_db_name(@NotNull sqlite3 db, int ndx){
+ return null==db ? null : sqlite3_db_name(db.getNativePointer(), ndx);
+ }
+
public static native String sqlite3_db_filename(
@NotNull sqlite3 db, @NotNull String dbName
);
- @Canonical
- public static native sqlite3 sqlite3_db_handle( @NotNull sqlite3_stmt stmt );
+ public static native sqlite3 sqlite3_db_handle(@NotNull sqlite3_stmt stmt);
+
+ public static native int sqlite3_db_readonly(@NotNull sqlite3 db, String dbName);
+
+ public static native int sqlite3_db_release_memory(sqlite3 db);
- @Canonical
public static native int sqlite3_db_status(
@NotNull sqlite3 db, int op, @NotNull OutputPointer.Int32 pCurrent,
@NotNull OutputPointer.Int32 pHighwater, boolean reset
);
- @Canonical
public static native int sqlite3_errcode(@NotNull sqlite3 db);
- @Canonical
- public static native String sqlite3_expanded_sql(@NotNull sqlite3_stmt stmt);
-
- @Canonical
- public static native int sqlite3_extended_errcode(@NotNull sqlite3 db);
-
- @Canonical
- public static native boolean sqlite3_extended_result_codes(
- @NotNull sqlite3 db, boolean onoff
- );
-
- @Canonical
public static native String sqlite3_errmsg(@NotNull sqlite3 db);
- @Canonical
- public static native String sqlite3_errstr(int resultCode);
+ private static native int sqlite3_error_offset(@NotNull long ptrToDb);
/**
Note that the returned byte offset values assume UTF-8-encoded
inputs, so won't always match character offsets in Java Strings.
*/
- @Canonical
- public static native int sqlite3_error_offset(@NotNull sqlite3 db);
+ public static int sqlite3_error_offset(@NotNull sqlite3 db){
+ return sqlite3_error_offset(db.getNativePointer());
+ }
- @Canonical
- public static native int sqlite3_finalize(@NotNull sqlite3_stmt stmt);
+ public static native String sqlite3_errstr(int resultCode);
+
+ public static native String sqlite3_expanded_sql(@NotNull sqlite3_stmt stmt);
+
+ private static native int sqlite3_extended_errcode(@NotNull long ptrToDb);
+
+ public static int sqlite3_extended_errcode(@NotNull sqlite3 db){
+ return sqlite3_extended_errcode(db.getNativePointer());
+ }
+
+ public static native int sqlite3_extended_result_codes(
+ @NotNull sqlite3 db, boolean on
+ );
+
+ private static native boolean sqlite3_get_autocommit(@NotNull long ptrToDb);
+
+ public static boolean sqlite3_get_autocommit(@NotNull sqlite3 db){
+ return sqlite3_get_autocommit(db.getNativePointer());
+ }
+
+ public static native Object sqlite3_get_auxdata(
+ @NotNull sqlite3_context cx, int n
+ );
+
+ private static native int sqlite3_finalize(long ptrToStmt);
+
+ public static int sqlite3_finalize(@NotNull sqlite3_stmt stmt){
+ return null==stmt ? 0 : sqlite3_finalize(stmt.clearNativePointer());
+ }
- @Canonical
public static native int sqlite3_initialize();
- @Canonical
public static native void sqlite3_interrupt(@NotNull sqlite3 db);
- @Canonical
public static native boolean sqlite3_is_interrupted(@NotNull sqlite3 db);
- @Canonical
+ public static native boolean sqlite3_keyword_check(@NotNull String word);
+
+ public static native int sqlite3_keyword_count();
+
+ public static native String sqlite3_keyword_name(int index);
+
+
public static native long sqlite3_last_insert_rowid(@NotNull sqlite3 db);
- @Canonical
public static native String sqlite3_libversion();
- @Canonical
public static native int sqlite3_libversion_number();
+ public static native int sqlite3_limit(@NotNull sqlite3 db, int id, int newVal);
+
+ /**
+ Only available if built with SQLITE_ENABLE_NORMALIZE. If not, it always
+ returns null.
+ */
+ public static native String sqlite3_normalized_sql(@NotNull sqlite3_stmt stmt);
+
/**
Works like its C counterpart and makes the native pointer of the
underling (sqlite3*) object available via
@@ -679,7 +1282,6 @@ public final class SQLite3Jni {
object and it is up to the caller to sqlite3_close() that
db handle.
*/
- @Canonical
public static native int sqlite3_open(
@Nullable String filename, @NotNull OutputPointer.sqlite3 ppDb
);
@@ -698,7 +1300,6 @@ public final class SQLite3Jni {
return out.take();
};
- @Canonical
public static native int sqlite3_open_v2(
@Nullable String filename, @NotNull OutputPointer.sqlite3 ppDb,
int flags, @Nullable String zVfs
@@ -735,7 +1336,7 @@ public final class SQLite3Jni {
necessary, however, and overloads are provided which gloss over
that.
-
Results are undefined if maxBytes>=sqlUtf8.length.
+
Results are undefined if maxBytes>sqlUtf8.length.
This routine is private because its maxBytes value is not
strictly necessary in the Java interface, as sqlUtf8.length tells
@@ -743,9 +1344,8 @@ public final class SQLite3Jni {
more ways to shoot themselves in the foot without providing any
real utility.
*/
- @Canonical
private static native int sqlite3_prepare(
- @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int maxBytes,
+ @NotNull long ptrToDb, @NotNull byte[] sqlUtf8, int maxBytes,
@NotNull OutputPointer.sqlite3_stmt outStmt,
@Nullable OutputPointer.Int32 pTailOffset
);
@@ -763,14 +1363,16 @@ public final class SQLite3Jni {
@NotNull OutputPointer.sqlite3_stmt outStmt,
@Nullable OutputPointer.Int32 pTailOffset
){
- return sqlite3_prepare(db, sqlUtf8, sqlUtf8.length, outStmt, pTailOffset);
+ return sqlite3_prepare(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+ outStmt, pTailOffset);
}
public static int sqlite3_prepare(
@NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
@NotNull OutputPointer.sqlite3_stmt outStmt
){
- return sqlite3_prepare(db, sqlUtf8, sqlUtf8.length, outStmt, null);
+ return sqlite3_prepare(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+ outStmt, null);
}
public static int sqlite3_prepare(
@@ -778,7 +1380,8 @@ public final class SQLite3Jni {
@NotNull OutputPointer.sqlite3_stmt outStmt
){
final byte[] utf8 = sql.getBytes(StandardCharsets.UTF_8);
- return sqlite3_prepare(db, utf8, utf8.length, outStmt, null);
+ return sqlite3_prepare(db.getNativePointer(), utf8, utf8.length,
+ outStmt, null);
}
/**
@@ -796,13 +1399,11 @@ public final class SQLite3Jni {
sqlite3_prepare(db, sql, out);
return out.take();
}
-
/**
@see #sqlite3_prepare
*/
- @Canonical
private static native int sqlite3_prepare_v2(
- @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int maxBytes,
+ @NotNull long ptrToDb, @NotNull byte[] sqlUtf8, int maxBytes,
@NotNull OutputPointer.sqlite3_stmt outStmt,
@Nullable OutputPointer.Int32 pTailOffset
);
@@ -817,14 +1418,16 @@ public final class SQLite3Jni {
@NotNull OutputPointer.sqlite3_stmt outStmt,
@Nullable OutputPointer.Int32 pTailOffset
){
- return sqlite3_prepare_v2(db, sqlUtf8, sqlUtf8.length, outStmt, pTailOffset);
+ return sqlite3_prepare_v2(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+ outStmt, pTailOffset);
}
public static int sqlite3_prepare_v2(
@NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
@NotNull OutputPointer.sqlite3_stmt outStmt
){
- return sqlite3_prepare_v2(db, sqlUtf8, sqlUtf8.length, outStmt, null);
+ return sqlite3_prepare_v2(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+ outStmt, null);
}
public static int sqlite3_prepare_v2(
@@ -832,7 +1435,8 @@ public final class SQLite3Jni {
@NotNull OutputPointer.sqlite3_stmt outStmt
){
final byte[] utf8 = sql.getBytes(StandardCharsets.UTF_8);
- return sqlite3_prepare_v2(db, utf8, utf8.length, outStmt, null);
+ return sqlite3_prepare_v2(db.getNativePointer(), utf8, utf8.length,
+ outStmt, null);
}
/**
@@ -850,9 +1454,8 @@ public final class SQLite3Jni {
/**
@see #sqlite3_prepare
*/
- @Canonical
private static native int sqlite3_prepare_v3(
- @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int maxBytes,
+ @NotNull long ptrToDb, @NotNull byte[] sqlUtf8, int maxBytes,
int prepFlags, @NotNull OutputPointer.sqlite3_stmt outStmt,
@Nullable OutputPointer.Int32 pTailOffset
);
@@ -867,22 +1470,34 @@ public final class SQLite3Jni {
@NotNull OutputPointer.sqlite3_stmt outStmt,
@Nullable OutputPointer.Int32 pTailOffset
){
- return sqlite3_prepare_v3(db, sqlUtf8, sqlUtf8.length, prepFlags, outStmt, pTailOffset);
+ return sqlite3_prepare_v3(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+ prepFlags, outStmt, pTailOffset);
}
+ /**
+ Convenience overload which elides the seldom-used pTailOffset
+ parameter.
+ */
public static int sqlite3_prepare_v3(
@NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int prepFlags,
@NotNull OutputPointer.sqlite3_stmt outStmt
){
- return sqlite3_prepare_v3(db, sqlUtf8, sqlUtf8.length, prepFlags, outStmt, null);
+ return sqlite3_prepare_v3(db.getNativePointer(), sqlUtf8, sqlUtf8.length,
+ prepFlags, outStmt, null);
}
+ /**
+ Convenience overload which elides the seldom-used pTailOffset
+ parameter and converts the given string to UTF-8 before passing
+ it on.
+ */
public static int sqlite3_prepare_v3(
@NotNull sqlite3 db, @NotNull String sql, int prepFlags,
@NotNull OutputPointer.sqlite3_stmt outStmt
){
final byte[] utf8 = sql.getBytes(StandardCharsets.UTF_8);
- return sqlite3_prepare_v3(db, utf8, utf8.length, prepFlags, outStmt, null);
+ return sqlite3_prepare_v3(db.getNativePointer(), utf8, utf8.length,
+ prepFlags, outStmt, null);
}
/**
@@ -897,46 +1512,179 @@ public final class SQLite3Jni {
return out.take();
}
+ /**
+ A convenience wrapper around sqlite3_prepare_v3() which accepts
+ an arbitrary amount of input provided as a UTF-8-encoded byte
+ array. It loops over the input bytes looking for
+ statements. Each one it finds is passed to p.call(), passing
+ ownership of it to that function. If p.call() returns 0, looping
+ continues, else the loop stops and p.call()'s result code is
+ returned. If preparation of any given segment fails, looping
+ stops and that result code is returned.
+
+
If p.call() throws, the exception is converted to a db-level
+ error and a non-0 code is returned, in order to retain the
+ C-style error semantics of the API.
+
+
How each statement is handled, including whether it is finalized
+ or not, is up to the callback object. e.g. the callback might
+ collect them for later use. If it does not collect them then it
+ must finalize them. See PrepareMultiCallback.Finalize for a
+ simple proxy which does that.
+ */
+ public static int sqlite3_prepare_multi(
+ @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+ int prepFlags,
+ @NotNull PrepareMultiCallback p){
+ final OutputPointer.Int32 oTail = new OutputPointer.Int32();
+ int pos = 0, n = 1;
+ byte[] sqlChunk = sqlUtf8;
+ int rc = 0;
+ final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
+ while( 0==rc && pos0 ){
+ sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
+ sqlChunk.length);
+ }
+ if( 0==sqlChunk.length ) break;
+ rc = sqlite3_prepare_v3(db, sqlChunk, prepFlags, outStmt, oTail);
+ if( 0!=rc ) break;
+ pos = oTail.value;
+ stmt = outStmt.take();
+ if( null==stmt ){
+ // empty statement (whitespace/comments)
+ continue;
+ }
+ try{
+ rc = p.call(stmt);
+ }catch(Exception e){
+ rc = sqlite3_jni_db_error( db, SQLITE_ERROR, e );
+ }
+ }
+ return rc;
+ }
+
+ /**
+ Convenience overload which accepts its SQL as a String and uses
+ no statement-preparation flags.
+ */
+ public static int sqlite3_prepare_multi(
+ @NotNull sqlite3 db, @NotNull byte[] sqlUtf8,
+ @NotNull PrepareMultiCallback p){
+ return sqlite3_prepare_multi(db, sqlUtf8, 0, p);
+ }
+
+ /**
+ Convenience overload which accepts its SQL as a String.
+ */
+ public static int sqlite3_prepare_multi(
+ @NotNull sqlite3 db, @NotNull String sql, int prepFlags,
+ @NotNull PrepareMultiCallback p){
+ return sqlite3_prepare_multi(
+ db, sql.getBytes(StandardCharsets.UTF_8), prepFlags, p
+ );
+ }
+
+ /**
+ Convenience overload which accepts its SQL as a String and uses
+ no statement-preparation flags.
+ */
+ public static int sqlite3_prepare_multi(
+ @NotNull sqlite3 db, @NotNull String sql,
+ @NotNull PrepareMultiCallback p){
+ return sqlite3_prepare_multi(db, sql, 0, p);
+ }
+
+ /**
+ Convenience overload which accepts its SQL as a String
+ array. They will be concatenated together as-is, with no
+ separator, and passed on to one of the other overloads.
+ */
+ public static int sqlite3_prepare_multi(
+ @NotNull sqlite3 db, @NotNull String[] sql, int prepFlags,
+ @NotNull PrepareMultiCallback p){
+ return sqlite3_prepare_multi(db, String.join("",sql), prepFlags, p);
+ }
+
+ /**
+ Convenience overload which uses no statement-preparation flags.
+ */
+ public static int sqlite3_prepare_multi(
+ @NotNull sqlite3 db, @NotNull String[] sql,
+ @NotNull PrepareMultiCallback p){
+ return sqlite3_prepare_multi(db, sql, 0, p);
+ }
+
+ private static native int sqlite3_preupdate_blobwrite(@NotNull long ptrToDb);
+
/**
If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this
acts as a proxy for C's sqlite3_preupdate_blobwrite(), else it returns
SQLITE_MISUSE with no side effects.
*/
- @Canonical
- public static native int sqlite3_preupdate_blobwrite(@NotNull sqlite3 db);
+ public static int sqlite3_preupdate_blobwrite(@NotNull sqlite3 db){
+ return sqlite3_preupdate_blobwrite(db.getNativePointer());
+ }
+
+ private static native int sqlite3_preupdate_count(@NotNull long ptrToDb);
+
/**
If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this
acts as a proxy for C's sqlite3_preupdate_count(), else it returns
SQLITE_MISUSE with no side effects.
*/
- @Canonical
- public static native int sqlite3_preupdate_count(@NotNull sqlite3 db);
+ public static int sqlite3_preupdate_count(@NotNull sqlite3 db){
+ return sqlite3_preupdate_count(db.getNativePointer());
+ }
+
+ private static native int sqlite3_preupdate_depth(@NotNull long ptrToDb);
+
/**
If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this
acts as a proxy for C's sqlite3_preupdate_depth(), else it returns
SQLITE_MISUSE with no side effects.
*/
- @Canonical
- public static native int sqlite3_preupdate_depth(@NotNull sqlite3 db);
+ public static int sqlite3_preupdate_depth(@NotNull sqlite3 db){
+ return sqlite3_preupdate_depth(db.getNativePointer());
+ }
+
+ private static native PreupdateHookCallback sqlite3_preupdate_hook(
+ @NotNull long ptrToDb, @Nullable PreupdateHookCallback hook
+ );
/**
If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this
acts as a proxy for C's sqlite3_preupdate_hook(), else it returns null
with no side effects.
*/
- @Canonical
- public static native PreupdateHookCallback sqlite3_preupdate_hook(
+ public static PreupdateHookCallback sqlite3_preupdate_hook(
@NotNull sqlite3 db, @Nullable PreupdateHookCallback hook
- );
+ ){
+ return sqlite3_preupdate_hook(db.getNativePointer(), hook);
+ }
+
+ private static native int sqlite3_preupdate_new(@NotNull long ptrToDb, int col,
+ @NotNull OutputPointer.sqlite3_value out);
/**
If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined,
this acts as a proxy for C's sqlite3_preupdate_new(), else it
returns SQLITE_MISUSE with no side effects.
+
+ WARNING: client code _must not_ hold a reference to the returned
+ sqlite3_value object beyond the scope of the preupdate hook in
+ which this function is called. Doing so will leave the client
+ holding a stale pointer, the address of which could point to
+ anything at all after the pre-update hook is complete. This API
+ has no way to record such objects and clear/invalidate them at
+ the end of a pre-update hook. We "could" add infrastructure to do
+ so, but would require significant levels of bookkeeping.
*/
- @Canonical
- public static native int sqlite3_preupdate_new(@NotNull sqlite3 db, int col,
- @NotNull OutputPointer.sqlite3_value out);
+ public static int sqlite3_preupdate_new(@NotNull sqlite3 db, int col,
+ @NotNull OutputPointer.sqlite3_value out){
+ return sqlite3_preupdate_new(db.getNativePointer(), col, out);
+ }
/**
Convenience wrapper for the 3-arg sqlite3_preupdate_new() which returns
@@ -944,18 +1692,25 @@ public final class SQLite3Jni {
*/
public static sqlite3_value sqlite3_preupdate_new(@NotNull sqlite3 db, int col){
final OutputPointer.sqlite3_value out = new OutputPointer.sqlite3_value();
- sqlite3_preupdate_new(db, col, out);
+ sqlite3_preupdate_new(db.getNativePointer(), col, out);
return out.take();
}
+ private static native int sqlite3_preupdate_old(@NotNull long ptrToDb, int col,
+ @NotNull OutputPointer.sqlite3_value out);
+
/**
If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined,
this acts as a proxy for C's sqlite3_preupdate_old(), else it
returns SQLITE_MISUSE with no side effects.
+
+ WARNING: see warning in sqlite3_preupdate_new() regarding the
+ potential for stale sqlite3_value handles.
*/
- @Canonical
- public static native int sqlite3_preupdate_old(@NotNull sqlite3 db, int col,
- @NotNull OutputPointer.sqlite3_value out);
+ public static int sqlite3_preupdate_old(@NotNull sqlite3 db, int col,
+ @NotNull OutputPointer.sqlite3_value out){
+ return sqlite3_preupdate_old(db.getNativePointer(), col, out);
+ }
/**
Convenience wrapper for the 3-arg sqlite3_preupdate_old() which returns
@@ -963,16 +1718,18 @@ public final class SQLite3Jni {
*/
public static sqlite3_value sqlite3_preupdate_old(@NotNull sqlite3 db, int col){
final OutputPointer.sqlite3_value out = new OutputPointer.sqlite3_value();
- sqlite3_preupdate_old(db, col, out);
+ sqlite3_preupdate_old(db.getNativePointer(), col, out);
return out.take();
}
- @Canonical
public static native void sqlite3_progress_handler(
@NotNull sqlite3 db, int n, @Nullable ProgressHandlerCallback h
);
- @Canonical
+ public static native void sqlite3_randomness(byte[] target);
+
+ public static native int sqlite3_release_memory(int n);
+
public static native int sqlite3_reset(@NotNull sqlite3_stmt stmt);
/**
@@ -980,10 +1737,8 @@ public final class SQLite3Jni {
extensions are currently running. (The JNI-level list of
extensions cannot be manipulated while it is being traversed.)
*/
- @Canonical
public static native void sqlite3_reset_auto_extension();
- @Canonical
public static native void sqlite3_result_double(
@NotNull sqlite3_context cx, double v
);
@@ -992,10 +1747,9 @@ public final class SQLite3Jni {
The main sqlite3_result_error() impl of which all others are
proxies. eTextRep must be one of SQLITE_UTF8 or SQLITE_UTF16 and
msg must be encoded correspondingly. Any other eTextRep value
- results in the C-level sqlite3_result_error() being called with
- a complaint about the invalid argument.
+ results in the C-level sqlite3_result_error() being called with a
+ complaint about the invalid argument.
*/
- @Canonical
private static native void sqlite3_result_error(
@NotNull sqlite3_context cx, @NotNull byte[] msg, int eTextRep
);
@@ -1038,32 +1792,22 @@ public final class SQLite3Jni {
sqlite3_result_error(cx, e.toString());
}
- @Canonical
public static native void sqlite3_result_error_toobig(
@NotNull sqlite3_context cx
);
- @Canonical
public static native void sqlite3_result_error_nomem(
@NotNull sqlite3_context cx
);
- @Canonical
public static native void sqlite3_result_error_code(
@NotNull sqlite3_context cx, int c
);
- @Canonical
- public static native void sqlite3_result_null(
- @NotNull sqlite3_context cx
- );
-
- @Canonical
public static native void sqlite3_result_int(
@NotNull sqlite3_context cx, int v
);
- @Canonical
public static native void sqlite3_result_int64(
@NotNull sqlite3_context cx, long v
);
@@ -1078,9 +1822,6 @@ public final class SQLite3Jni {
cross-language semantic mismatch and (B) Java doesn't need that
argument for its intended purpose (type safety).
- Note that there is no sqlite3_column_java_object(), as the
- C-level API has no sqlite3_column_pointer() to proxy.
-
@see #sqlite3_value_java_object
@see #sqlite3_bind_java_object
*/
@@ -1088,6 +1829,45 @@ public final class SQLite3Jni {
@NotNull sqlite3_context cx, @NotNull Object o
);
+ /**
+ Similar to sqlite3_bind_nio_buffer(), this works like
+ sqlite3_result_blob() but accepts a java.nio.ByteBuffer as its
+ input source. See sqlite3_bind_nio_buffer() for the semantics of
+ the second and subsequent arguments.
+
+ If cx is null then this function will silently fail. If
+ sqlite3_jni_supports_nio() returns false or iBegin is negative,
+ an error result is set. If (begin+n) extends beyond the end of
+ the buffer, it is silently truncated to fit.
+
+ If any of the following apply, this function behaves like
+ sqlite3_result_null(): the blob is null, the resulting slice of
+ the blob is empty.
+
+ If the resulting slice of the buffer exceeds SQLITE_LIMIT_LENGTH
+ then this function behaves like sqlite3_result_error_toobig().
+ */
+ @Experimental
+ /*public*/ static native void sqlite3_result_nio_buffer(
+ @NotNull sqlite3_context cx, @Nullable java.nio.ByteBuffer blob,
+ int begin, int n
+ );
+
+ /**
+ Convenience overload which uses the whole input object
+ as the result blob content.
+ */
+ @Experimental
+ /*public*/ static void sqlite3_result_nio_buffer(
+ @NotNull sqlite3_context cx, @Nullable java.nio.ByteBuffer blob
+ ){
+ sqlite3_result_nio_buffer(cx, blob, 0, -1);
+ }
+
+ public static native void sqlite3_result_null(
+ @NotNull sqlite3_context cx
+ );
+
public static void sqlite3_result_set(
@NotNull sqlite3_context cx, @NotNull Boolean v
){
@@ -1148,22 +1928,26 @@ public final class SQLite3Jni {
else sqlite3_result_blob(cx, blob, blob.length);
}
- @Canonical
+ 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
);
- @Canonical
public static native void sqlite3_result_zeroblob(
@NotNull sqlite3_context cx, int n
);
- @Canonical
public static native int sqlite3_result_zeroblob64(
@NotNull sqlite3_context cx, long n
);
- @Canonical
+ /**
+ This overload is private because its final parameter is arguably
+ unnecessary in Java.
+ */
private static native void sqlite3_result_blob(
@NotNull sqlite3_context cx, @Nullable byte[] blob, int maxLen
);
@@ -1174,6 +1958,29 @@ public final class SQLite3Jni {
sqlite3_result_blob(cx, blob, (int)(null==blob ? 0 : blob.length));
}
+ /**
+ Convenience overload which behaves like
+ sqlite3_result_nio_buffer().
+ */
+ @Experimental
+ /*public*/ static void sqlite3_result_blob(
+ @NotNull sqlite3_context cx, @Nullable java.nio.ByteBuffer blob,
+ int begin, int n
+ ){
+ sqlite3_result_nio_buffer(cx, blob, begin, n);
+ }
+
+ /**
+ Convenience overload which behaves like the two-argument overload of
+ sqlite3_result_nio_buffer().
+ */
+ @Experimental
+ /*public*/ static void sqlite3_result_blob(
+ @NotNull sqlite3_context cx, @Nullable java.nio.ByteBuffer blob
+ ){
+ sqlite3_result_nio_buffer(cx, blob);
+ }
+
/**
Binds the given text using C's sqlite3_result_blob64() unless:
@@ -1186,10 +1993,12 @@ public final class SQLite3Jni {
- If @param maxLen is larger than blob.length, it is truncated to
- that value. If it is negative, results are undefined.
+
If @param maxLen is larger than blob.length, it is truncated
+ to that value. If it is negative, results are undefined.
+
+ This overload is private because its final parameter is
+ arguably unnecessary in Java.
*/
- @Canonical
private static native void sqlite3_result_blob64(
@NotNull sqlite3_context cx, @Nullable byte[] blob, long maxLen
);
@@ -1200,7 +2009,10 @@ public final class SQLite3Jni {
sqlite3_result_blob64(cx, blob, (long)(null==blob ? 0 : blob.length));
}
- @Canonical
+ /**
+ This overload is private because its final parameter is
+ arguably unnecessary in Java.
+ */
private static native void sqlite3_result_text(
@NotNull sqlite3_context cx, @Nullable byte[] utf8, int maxLen
);
@@ -1226,7 +2038,8 @@ public final class SQLite3Jni {
- - text is null: translates to a call to sqlite3_result_null()
+ - text is null: translates to a call to {@link
+ #sqlite3_result_null}
- text is too large: translates to a call to
{@link #sqlite3_result_error_toobig}
@@ -1240,8 +2053,10 @@ public final class SQLite3Jni {
text.length, it is silently truncated to text.length. If it is
negative, results are undefined. If text is null, the subsequent
arguments are ignored.
+
+ This overload is private because its maxLength parameter is
+ arguably unnecessary in Java.
*/
- @Canonical
private static native void sqlite3_result_text64(
@NotNull sqlite3_context cx, @Nullable byte[] text,
long maxLength, int encoding
@@ -1254,7 +2069,8 @@ public final class SQLite3Jni {
public static void sqlite3_result_text16(
@NotNull sqlite3_context cx, @Nullable byte[] utf16
){
- sqlite3_result_text64(cx, utf16, utf16.length, SQLITE_UTF16);
+ if(null == utf16) sqlite3_result_null(cx);
+ else sqlite3_result_text64(cx, utf16, utf16.length, SQLITE_UTF16);
}
public static void sqlite3_result_text16(
@@ -1267,17 +2083,24 @@ public final class SQLite3Jni {
}
}
- @Canonical
- public static native RollbackHookCallback sqlite3_rollback_hook(
- @NotNull sqlite3 db, @Nullable RollbackHookCallback hook
+ private static native RollbackHookCallback sqlite3_rollback_hook(
+ @NotNull long ptrToDb, @Nullable RollbackHookCallback hook
);
- @Canonical
+ public static RollbackHookCallback sqlite3_rollback_hook(
+ @NotNull sqlite3 db, @Nullable RollbackHookCallback hook
+ ){
+ return sqlite3_rollback_hook(db.getNativePointer(), hook);
+ }
+
public static native int sqlite3_set_authorizer(
@NotNull sqlite3 db, @Nullable AuthorizerCallback auth
);
- @Canonical
+ public static native void sqlite3_set_auxdata(
+ @NotNull sqlite3_context cx, int n, @Nullable Object data
+ );
+
public static native void sqlite3_set_last_insert_rowid(
@NotNull sqlite3 db, long rowid
);
@@ -1292,127 +2115,246 @@ public final class SQLite3Jni {
to use those objects after this routine is called invoked
undefined behavior.
*/
- @Canonical
public static synchronized native int sqlite3_shutdown();
- @Canonical
public static native int sqlite3_sleep(int ms);
- @Canonical
public static native String sqlite3_sourceid();
- @Canonical
public static native String sqlite3_sql(@NotNull sqlite3_stmt stmt);
-
- @Canonical
+ //! Consider removing this. We can use sqlite3_status64() instead,
+ // or use that one's impl with this one's name.
public static native int sqlite3_status(
int op, @NotNull OutputPointer.Int32 pCurrent,
@NotNull OutputPointer.Int32 pHighwater, boolean reset
);
- @Canonical
public static native int sqlite3_status64(
int op, @NotNull OutputPointer.Int64 pCurrent,
@NotNull OutputPointer.Int64 pHighwater, boolean reset
);
- @Canonical
- public static native int sqlite3_step(@NotNull sqlite3_stmt stmt);
+ private static native int sqlite3_step(@NotNull long ptrToStmt);
+
+ public static int sqlite3_step(@NotNull sqlite3_stmt stmt){
+ return null==stmt ? SQLITE_MISUSE : sqlite3_step(stmt.getNativePointer());
+ }
+
+ public static native boolean sqlite3_stmt_busy(@NotNull sqlite3_stmt stmt);
+
+ private static native int sqlite3_stmt_explain(@NotNull long ptrToStmt, int op);
+
+ public static int sqlite3_stmt_explain(@NotNull sqlite3_stmt stmt, int op){
+ return null==stmt ? SQLITE_MISUSE : sqlite3_stmt_explain(stmt.getNativePointer(), op);
+ }
+
+ private static native int sqlite3_stmt_isexplain(@NotNull long ptrToStmt);
+
+ public static int sqlite3_stmt_isexplain(@NotNull sqlite3_stmt stmt){
+ return null==stmt ? 0 : sqlite3_stmt_isexplain(stmt.getNativePointer());
+ }
+
+ public static native boolean sqlite3_stmt_readonly(@NotNull sqlite3_stmt stmt);
+
+ public static native int sqlite3_stmt_status(
+ @NotNull sqlite3_stmt stmt, int op, boolean reset
+ );
/**
Internal impl of the public sqlite3_strglob() method. Neither
- argument may be NULL and both MUST be NUL-terminated UTF-8.
+ argument may be null and both must be NUL-terminated UTF-8.
+
+ This overload is private because: (A) to keep users from
+ inadvertently passing non-NUL-terminated byte arrays (an easy
+ thing to do). (B) it is cheaper to NUL-terminate the
+ String-to-byte-array conversion in the Java implementation
+ (sqlite3_strglob(String,String)) than to do that in C, so that
+ signature is the public-facing one.
*/
- @Canonical
private static native int sqlite3_strglob(
- @NotNull byte[] glob, @NotNull byte[] txt
+ @NotNull byte[] glob, @NotNull byte[] nulTerminatedUtf8
);
public static int sqlite3_strglob(
@NotNull String glob, @NotNull String txt
){
- return sqlite3_strglob(
- (glob+"\0").getBytes(StandardCharsets.UTF_8),
- (txt+"\0").getBytes(StandardCharsets.UTF_8)
- );
+ return sqlite3_strglob(nulTerminateUtf8(glob),
+ nulTerminateUtf8(txt));
}
/**
- Internal impl of the public sqlite3_strlike() method. Neither
- argument may be NULL and both MUST be NUL-terminated UTF-8.
+ The LIKE counterpart of the private sqlite3_strglob() method.
*/
- @Canonical
private static native int sqlite3_strlike(
- @NotNull byte[] glob, @NotNull byte[] txt, int escChar
+ @NotNull byte[] glob, @NotNull byte[] nulTerminatedUtf8,
+ int escChar
);
public static int sqlite3_strlike(
@NotNull String glob, @NotNull String txt, char escChar
){
- return sqlite3_strlike(
- (glob+"\0").getBytes(StandardCharsets.UTF_8),
- (txt+"\0").getBytes(StandardCharsets.UTF_8),
- (int)escChar
- );
+ return sqlite3_strlike(nulTerminateUtf8(glob),
+ nulTerminateUtf8(txt),
+ (int)escChar);
+ }
+
+ private static native int sqlite3_system_errno(@NotNull long ptrToDb);
+
+ public static int sqlite3_system_errno(@NotNull sqlite3 db){
+ return sqlite3_system_errno(db.getNativePointer());
+ }
+
+ public static native int sqlite3_table_column_metadata(
+ @NotNull sqlite3 db, @NotNull String zDbName,
+ @NotNull String zTableName, @NotNull String zColumnName,
+ @Nullable OutputPointer.String pzDataType,
+ @Nullable OutputPointer.String pzCollSeq,
+ @Nullable OutputPointer.Bool pNotNull,
+ @Nullable OutputPointer.Bool pPrimaryKey,
+ @Nullable OutputPointer.Bool pAutoinc
+ );
+
+ /**
+ Convenience overload which returns its results via a single
+ output object. If this function returns non-0 (error), the the
+ contents of the output object are not modified.
+ */
+ public static int sqlite3_table_column_metadata(
+ @NotNull sqlite3 db, @NotNull String zDbName,
+ @NotNull String zTableName, @NotNull String zColumnName,
+ @NotNull TableColumnMetadata out){
+ return sqlite3_table_column_metadata(
+ db, zDbName, zTableName, zColumnName,
+ out.pzDataType, out.pzCollSeq, out.pNotNull,
+ out.pPrimaryKey, out.pAutoinc);
+ }
+
+ /**
+ Convenience overload which returns the column metadata object on
+ success and null on error.
+ */
+ public static TableColumnMetadata sqlite3_table_column_metadata(
+ @NotNull sqlite3 db, @NotNull String zDbName,
+ @NotNull String zTableName, @NotNull String zColumnName){
+ final TableColumnMetadata out = new TableColumnMetadata();
+ return 0==sqlite3_table_column_metadata(
+ db, zDbName, zTableName, zColumnName, out
+ ) ? out : null;
}
- @Canonical
public static native int sqlite3_threadsafe();
- @Canonical
- public static native int sqlite3_total_changes(@NotNull sqlite3 db);
+ private static native int sqlite3_total_changes(@NotNull long ptrToDb);
- @Canonical
- public static native long sqlite3_total_changes64(@NotNull sqlite3 db);
+ public static int sqlite3_total_changes(@NotNull sqlite3 db){
+ return sqlite3_total_changes(db.getNativePointer());
+ }
+
+ private static native long sqlite3_total_changes64(@NotNull long ptrToDb);
+
+ public static long sqlite3_total_changes64(@NotNull sqlite3 db){
+ return sqlite3_total_changes64(db.getNativePointer());
+ }
/**
Works like C's sqlite3_trace_v2() except that the 3rd argument to that
function is elided here because the roles of that functions' 3rd and 4th
arguments are encapsulated in the final argument to this function.
- Unlike the C API, which is documented as always returning 0, this
- implementation returns non-0 if initialization of the tracer
- mapping state fails.
+
Unlike the C API, which is documented as always returning 0,
+ this implementation returns non-0 if initialization of the tracer
+ mapping state fails (e.g. on OOM).
*/
- @Canonical
public static native int sqlite3_trace_v2(
@NotNull sqlite3 db, int traceMask, @Nullable TraceV2Callback tracer
);
- @Canonical
- public static native UpdateHookCallback sqlite3_update_hook(
- sqlite3 db, UpdateHookCallback hook
+ public static native int sqlite3_txn_state(
+ @NotNull sqlite3 db, @Nullable String zSchema
);
- @Canonical
- public static native byte[] sqlite3_value_blob(@NotNull sqlite3_value v);
-
- @Canonical
- public static native int sqlite3_value_bytes(@NotNull sqlite3_value v);
-
- @Canonical
- public static native int sqlite3_value_bytes16(@NotNull sqlite3_value v);
-
- @Canonical
- public static native double sqlite3_value_double(@NotNull sqlite3_value v);
-
- @Canonical
- public static native sqlite3_value sqlite3_value_dup(
- @NotNull sqlite3_value v
+ private static native UpdateHookCallback sqlite3_update_hook(
+ @NotNull long ptrToDb, @Nullable UpdateHookCallback hook
);
- @Canonical
- public static native int sqlite3_value_encoding(@NotNull sqlite3_value v);
+ public static UpdateHookCallback sqlite3_update_hook(
+ @NotNull sqlite3 db, @Nullable UpdateHookCallback hook
+ ){
+ return sqlite3_update_hook(db.getNativePointer(), hook);
+ }
- @Canonical
- public static native void sqlite3_value_free(@Nullable sqlite3_value v);
+ /*
+ Note that:
- @Canonical
- public static native int sqlite3_value_int(@NotNull sqlite3_value v);
+ void * sqlite3_user_data(sqlite3_context*)
- @Canonical
- public static native long sqlite3_value_int64(@NotNull sqlite3_value v);
+ Is not relevant in the JNI binding, as its feature is replaced by
+ the ability to pass an object, including any relevant state, to
+ sqlite3_create_function().
+ */
+
+ private static native byte[] sqlite3_value_blob(@NotNull long ptrToValue);
+
+ public static byte[] sqlite3_value_blob(@NotNull sqlite3_value v){
+ return sqlite3_value_blob(v.getNativePointer());
+ }
+
+ private static native int sqlite3_value_bytes(@NotNull long ptrToValue);
+
+ public static int sqlite3_value_bytes(@NotNull sqlite3_value v){
+ return sqlite3_value_bytes(v.getNativePointer());
+ }
+
+ private static native int sqlite3_value_bytes16(@NotNull long ptrToValue);
+
+ public static int sqlite3_value_bytes16(@NotNull sqlite3_value v){
+ return sqlite3_value_bytes16(v.getNativePointer());
+ }
+
+ private static native double sqlite3_value_double(@NotNull long ptrToValue);
+
+ public static double sqlite3_value_double(@NotNull sqlite3_value v){
+ return sqlite3_value_double(v.getNativePointer());
+ }
+
+ private static native sqlite3_value sqlite3_value_dup(@NotNull long ptrToValue);
+
+ public static sqlite3_value sqlite3_value_dup(@NotNull sqlite3_value v){
+ return sqlite3_value_dup(v.getNativePointer());
+ }
+
+ private static native int sqlite3_value_encoding(@NotNull long ptrToValue);
+
+ public static int sqlite3_value_encoding(@NotNull sqlite3_value v){
+ return sqlite3_value_encoding(v.getNativePointer());
+ }
+
+ private static native void sqlite3_value_free(@Nullable long ptrToValue);
+
+ public static void sqlite3_value_free(@Nullable sqlite3_value v){
+ if( null!=v ) sqlite3_value_free(v.clearNativePointer());
+ }
+
+ private static native boolean sqlite3_value_frombind(@NotNull long ptrToValue);
+
+ public static boolean sqlite3_value_frombind(@NotNull sqlite3_value v){
+ return sqlite3_value_frombind(v.getNativePointer());
+ }
+
+ private static native int sqlite3_value_int(@NotNull long ptrToValue);
+
+ public static int sqlite3_value_int(@NotNull sqlite3_value v){
+ return sqlite3_value_int(v.getNativePointer());
+ }
+
+ private static native long sqlite3_value_int64(@NotNull long ptrToValue);
+
+ public static long sqlite3_value_int64(@NotNull sqlite3_value v){
+ return sqlite3_value_int64(v.getNativePointer());
+ }
+
+ private static native Object sqlite3_value_java_object(@NotNull long ptrToValue);
/**
If the given value was set using {@link
@@ -1422,9 +2364,9 @@ public final class SQLite3Jni {
It is up to the caller to inspect the object to determine its
type, and cast it if necessary.
*/
- public static native Object sqlite3_value_java_object(
- @NotNull sqlite3_value v
- );
+ public static Object sqlite3_value_java_object(@NotNull sqlite3_value v){
+ return sqlite3_value_java_object(v.getNativePointer());
+ }
/**
A variant of sqlite3_value_java_object() which returns the
@@ -1432,54 +2374,64 @@ public final class SQLite3Jni {
given Class, else it returns null.
*/
@SuppressWarnings("unchecked")
- public static T sqlite3_value_java_casted(@NotNull sqlite3_value v,
+ public static T sqlite3_value_java_object(@NotNull sqlite3_value v,
@NotNull Class type){
final Object o = sqlite3_value_java_object(v);
return type.isInstance(o) ? (T)o : null;
}
/**
- Returns the given value as UTF-8-encoded bytes, or null if the
- underlying C-level sqlite3_value_text() returns NULL.
+ A variant of sqlite3_column_blob() which returns the blob as a
+ ByteBuffer object. Returns null if its argument is null, if
+ sqlite3_jni_supports_nio() is false, or if sqlite3_value_blob()
+ would return null for the same input.
*/
- @Canonical(cname="sqlite3_value_text",
- comment="Renamed because its String-returning overload would "+
- "otherwise be ambiguous.")
- public static native byte[] sqlite3_value_text_utf8(@NotNull sqlite3_value v);
+ @Experimental
+ /*public*/ static native java.nio.ByteBuffer sqlite3_value_nio_buffer(
+ @NotNull sqlite3_value v
+ );
+
+ private static native int sqlite3_value_nochange(@NotNull long ptrToValue);
+
+ public static int sqlite3_value_nochange(@NotNull sqlite3_value v){
+ return sqlite3_value_nochange(v.getNativePointer());
+ }
+
+ private static native int sqlite3_value_numeric_type(@NotNull long ptrToValue);
+
+ public static int sqlite3_value_numeric_type(@NotNull sqlite3_value v){
+ return sqlite3_value_numeric_type(v.getNativePointer());
+ }
+
+ private static native int sqlite3_value_subtype(@NotNull long ptrToValue);
+
+ public static int sqlite3_value_subtype(@NotNull sqlite3_value v){
+ return sqlite3_value_subtype(v.getNativePointer());
+ }
+
+ private static native byte[] sqlite3_value_text(@NotNull long ptrToValue);
/**
- Provides the same feature as the same-named C API but returns the
- text in Java-native encoding rather than the C API's UTF-8.
-
- @see #sqlite3_value_text16
+ Functions identially to the C API, and this note is just to
+ stress that the returned bytes are encoded as UTF-8. It returns
+ null if the underlying C-level sqlite3_value_text() returns NULL
+ or on allocation error.
*/
- public static native String sqlite3_value_text(@NotNull sqlite3_value v);
+ public static byte[] sqlite3_value_text(@NotNull sqlite3_value v){
+ return sqlite3_value_text(v.getNativePointer());
+ }
- /**
- In the Java layer, sqlite3_value_text() and
- sqlite3_value_text16() are functionally equivalent, the
- difference being only where the encoding to UTF-16 (if necessary)
- takes place. This function does it via SQLite and
- sqlite3_value_text() fetches UTF-8 (SQLite's default encoding)
- and converts it to UTF-16 in Java.
- */
- @Canonical
- public static native String sqlite3_value_text16(@NotNull sqlite3_value v);
+ private static native String sqlite3_value_text16(@NotNull long ptrToValue);
- @Canonical
- public static native int sqlite3_value_type(@NotNull sqlite3_value v);
+ public static String sqlite3_value_text16(@NotNull sqlite3_value v){
+ return sqlite3_value_text16(v.getNativePointer());
+ }
- @Canonical
- public static native int sqlite3_value_numeric_type(@NotNull sqlite3_value v);
+ private static native int sqlite3_value_type(@NotNull long ptrToValue);
- @Canonical
- public static native int sqlite3_value_nochange(@NotNull sqlite3_value v);
-
- @Canonical
- public static native int sqlite3_value_frombind(@NotNull sqlite3_value v);
-
- @Canonical
- public static native int sqlite3_value_subtype(@NotNull sqlite3_value v);
+ public static int sqlite3_value_type(@NotNull sqlite3_value v){
+ return sqlite3_value_type(v.getNativePointer());
+ }
/**
This is NOT part of the public API. It exists solely as a place
@@ -1497,10 +2449,6 @@ public final class SQLite3Jni {
public static final String SQLITE_VERSION = sqlite3_libversion();
public static final String SQLITE_SOURCE_ID = sqlite3_sourceid();
- // Initialized at static init time to the build-time value of
- // SQLITE_THREADSAFE.
- public static int SQLITE_THREADSAFE = -1;
-
// access
public static final int SQLITE_ACCESS_EXISTS = 0;
public static final int SQLITE_ACCESS_READWRITE = 1;
@@ -1713,63 +2661,47 @@ public final class SQLite3Jni {
public static final int SQLITE_IOCAP_IMMUTABLE = 8192;
public static final int SQLITE_IOCAP_BATCH_ATOMIC = 16384;
- // limits. These get injected at init-time so that they stay in sync
- // with the compile-time options. This unfortunately means they are
- // not final, but keeping them in sync with their C values seems
- // more important than protecting users from assigning to these
- // (with unpredictable results).
- public static int SQLITE_MAX_ALLOCATION_SIZE = -1;
- public static int SQLITE_LIMIT_LENGTH = -1;
- public static int SQLITE_MAX_LENGTH = -1;
- public static int SQLITE_LIMIT_SQL_LENGTH = -1;
- public static int SQLITE_MAX_SQL_LENGTH = -1;
- public static int SQLITE_LIMIT_COLUMN = -1;
- public static int SQLITE_MAX_COLUMN = -1;
- public static int SQLITE_LIMIT_EXPR_DEPTH = -1;
- public static int SQLITE_MAX_EXPR_DEPTH = -1;
- public static int SQLITE_LIMIT_COMPOUND_SELECT = -1;
- public static int SQLITE_MAX_COMPOUND_SELECT = -1;
- public static int SQLITE_LIMIT_VDBE_OP = -1;
- public static int SQLITE_MAX_VDBE_OP = -1;
- public static int SQLITE_LIMIT_FUNCTION_ARG = -1;
- public static int SQLITE_MAX_FUNCTION_ARG = -1;
- public static int SQLITE_LIMIT_ATTACHED = -1;
- public static int SQLITE_MAX_ATTACHED = -1;
- public static int SQLITE_LIMIT_LIKE_PATTERN_LENGTH = -1;
- public static int SQLITE_MAX_LIKE_PATTERN_LENGTH = -1;
- public static int SQLITE_LIMIT_VARIABLE_NUMBER = -1;
- public static int SQLITE_MAX_VARIABLE_NUMBER = -1;
- public static int SQLITE_LIMIT_TRIGGER_DEPTH = -1;
- public static int SQLITE_MAX_TRIGGER_DEPTH = -1;
- public static int SQLITE_LIMIT_WORKER_THREADS = -1;
- public static int SQLITE_MAX_WORKER_THREADS = -1;
+ // limits
+ public static final int SQLITE_LIMIT_LENGTH = 0;
+ public static final int SQLITE_LIMIT_SQL_LENGTH = 1;
+ public static final int SQLITE_LIMIT_COLUMN = 2;
+ public static final int SQLITE_LIMIT_EXPR_DEPTH = 3;
+ public static final int SQLITE_LIMIT_COMPOUND_SELECT = 4;
+ public static final int SQLITE_LIMIT_VDBE_OP = 5;
+ public static final int SQLITE_LIMIT_FUNCTION_ARG = 6;
+ public static final int SQLITE_LIMIT_ATTACHED = 7;
+ public static final int SQLITE_LIMIT_LIKE_PATTERN_LENGTH = 8;
+ public static final int SQLITE_LIMIT_VARIABLE_NUMBER = 9;
+ public static final int SQLITE_LIMIT_TRIGGER_DEPTH = 10;
+ public static final int SQLITE_LIMIT_WORKER_THREADS = 11;
// open flags
- public static final int SQLITE_OPEN_READONLY = 1;
- public static final int SQLITE_OPEN_READWRITE = 2;
- public static final int SQLITE_OPEN_CREATE = 4;
- public static final int SQLITE_OPEN_URI = 64;
- public static final int SQLITE_OPEN_MEMORY = 128;
- public static final int SQLITE_OPEN_NOMUTEX = 32768;
- public static final int SQLITE_OPEN_FULLMUTEX = 65536;
- public static final int SQLITE_OPEN_SHAREDCACHE = 131072;
- public static final int SQLITE_OPEN_PRIVATECACHE = 262144;
- public static final int SQLITE_OPEN_EXRESCODE = 33554432;
- public static final int SQLITE_OPEN_NOFOLLOW = 16777216;
- public static final int SQLITE_OPEN_MAIN_DB = 256;
- public static final int SQLITE_OPEN_MAIN_JOURNAL = 2048;
- public static final int SQLITE_OPEN_TEMP_DB = 512;
- public static final int SQLITE_OPEN_TEMP_JOURNAL = 4096;
- public static final int SQLITE_OPEN_TRANSIENT_DB = 1024;
- public static final int SQLITE_OPEN_SUBJOURNAL = 8192;
- public static final int SQLITE_OPEN_SUPER_JOURNAL = 16384;
- public static final int SQLITE_OPEN_WAL = 524288;
- public static final int SQLITE_OPEN_DELETEONCLOSE = 8;
- public static final int SQLITE_OPEN_EXCLUSIVE = 16;
+
+ public static final int SQLITE_OPEN_READONLY = 0x00000001 /* Ok for sqlite3_open_v2() */;
+ public static final int SQLITE_OPEN_READWRITE = 0x00000002 /* Ok for sqlite3_open_v2() */;
+ public static final int SQLITE_OPEN_CREATE = 0x00000004 /* Ok for sqlite3_open_v2() */;
+ //public static final int SQLITE_OPEN_DELETEONCLOSE = 0x00000008 /* VFS only */;
+ //public static final int SQLITE_OPEN_EXCLUSIVE = 0x00000010 /* VFS only */;
+ //public static final int SQLITE_OPEN_AUTOPROXY = 0x00000020 /* VFS only */;
+ public static final int SQLITE_OPEN_URI = 0x00000040 /* Ok for sqlite3_open_v2() */;
+ public static final int SQLITE_OPEN_MEMORY = 0x00000080 /* Ok for sqlite3_open_v2() */;
+ //public static final int SQLITE_OPEN_MAIN_DB = 0x00000100 /* VFS only */;
+ //public static final int SQLITE_OPEN_TEMP_DB = 0x00000200 /* VFS only */;
+ //public static final int SQLITE_OPEN_TRANSIENT_DB = 0x00000400 /* VFS only */;
+ //public static final int SQLITE_OPEN_MAIN_JOURNAL = 0x00000800 /* VFS only */;
+ //public static final int SQLITE_OPEN_TEMP_JOURNAL = 0x00001000 /* VFS only */;
+ //public static final int SQLITE_OPEN_SUBJOURNAL = 0x00002000 /* VFS only */;
+ //public static final int SQLITE_OPEN_SUPER_JOURNAL = 0x00004000 /* VFS only */;
+ public static final int SQLITE_OPEN_NOMUTEX = 0x00008000 /* Ok for sqlite3_open_v2() */;
+ public static final int SQLITE_OPEN_FULLMUTEX = 0x00010000 /* Ok for sqlite3_open_v2() */;
+ public static final int SQLITE_OPEN_SHAREDCACHE = 0x00020000 /* Ok for sqlite3_open_v2() */;
+ public static final int SQLITE_OPEN_PRIVATECACHE = 0x00040000 /* Ok for sqlite3_open_v2() */;
+ //public static final int SQLITE_OPEN_WAL = 0x00080000 /* VFS only */;
+ public static final int SQLITE_OPEN_NOFOLLOW = 0x01000000 /* Ok for sqlite3_open_v2() */;
+ public static final int SQLITE_OPEN_EXRESCODE = 0x02000000 /* Extended result codes */;
// prepare flags
public static final int SQLITE_PREPARE_PERSISTENT = 1;
- public static final int SQLITE_PREPARE_NORMALIZE = 2;
public static final int SQLITE_PREPARE_NO_VTAB = 4;
// result codes
@@ -1925,9 +2857,11 @@ public final class SQLite3Jni {
public static final int SQLITE_TXN_WRITE = 2;
// udf flags
- public static final int SQLITE_DETERMINISTIC = 2048;
- public static final int SQLITE_DIRECTONLY = 524288;
- public static final int SQLITE_INNOCUOUS = 2097152;
+ public static final int SQLITE_DETERMINISTIC = 0x000000800;
+ public static final int SQLITE_DIRECTONLY = 0x000080000;
+ public static final int SQLITE_SUBTYPE = 0x000100000;
+ public static final int SQLITE_INNOCUOUS = 0x000200000;
+ public static final int SQLITE_RESULT_SUBTYPE = 0x001000000;
// virtual tables
public static final int SQLITE_INDEX_SCAN_UNIQUE = 1;
@@ -1956,8 +2890,8 @@ public final class SQLite3Jni {
public static final int SQLITE_FAIL = 3;
public static final int SQLITE_REPLACE = 5;
static {
- // This MUST come after the SQLITE_MAX_... values or else
- // attempting to modify them silently fails.
init();
}
+ /* Must come after static init(). */
+ private static final boolean JNI_SUPPORTS_NIO = sqlite3_jni_supports_nio();
}
diff --git a/ext/jni/src/org/sqlite/jni/SQLite3CallbackProxy.java b/ext/jni/src/org/sqlite/jni/capi/CallbackProxy.java
similarity index 87%
rename from ext/jni/src/org/sqlite/jni/SQLite3CallbackProxy.java
rename to ext/jni/src/org/sqlite/jni/capi/CallbackProxy.java
index 5052664937..7df748e8d8 100644
--- a/ext/jni/src/org/sqlite/jni/SQLite3CallbackProxy.java
+++ b/ext/jni/src/org/sqlite/jni/capi/CallbackProxy.java
@@ -11,7 +11,7 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
This marker interface exists soley for use as a documentation and
class-grouping tool. It should be applied to interfaces or
@@ -24,8 +24,9 @@ package org.sqlite.jni;
propagated. For callback interfaces which support returning error
info to the core, the JNI binding will convert any exceptions to
C-level error information. For callback interfaces which do not
- support, all exceptions will necessarily be suppressed in order to
- retain the C-style no-throw semantics.
+ support returning error information, all exceptions will
+ necessarily be suppressed in order to retain the C-style no-throw
+ semantics and avoid invoking undefined behavior in the C layer.
Callbacks of this style follow a common naming convention:
@@ -41,4 +42,4 @@ package org.sqlite.jni;
2) They all have a {@code call()} method but its signature is
callback-specific.
*/
-public interface SQLite3CallbackProxy {}
+public interface CallbackProxy {}
diff --git a/ext/jni/src/org/sqlite/jni/CollationCallback.java b/ext/jni/src/org/sqlite/jni/capi/CollationCallback.java
similarity index 85%
rename from ext/jni/src/org/sqlite/jni/CollationCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/CollationCallback.java
index 481c6cd956..ed8bd09475 100644
--- a/ext/jni/src/org/sqlite/jni/CollationCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/CollationCallback.java
@@ -11,16 +11,16 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
import org.sqlite.jni.annotation.NotNull;
/**
- Callback for use with {@link SQLite3Jni#sqlite3_create_collation}.
+ Callback for use with {@link CApi#sqlite3_create_collation}.
@see AbstractCollationCallback
*/
public interface CollationCallback
- extends SQLite3CallbackProxy, XDestroyCallback {
+ extends CallbackProxy, XDestroyCallback {
/**
Must compare the given byte arrays and return the result using
{@code memcmp()} semantics.
diff --git a/ext/jni/src/org/sqlite/jni/CollationNeededCallback.java b/ext/jni/src/org/sqlite/jni/capi/CollationNeededCallback.java
similarity index 59%
rename from ext/jni/src/org/sqlite/jni/CollationNeededCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/CollationNeededCallback.java
index e6c917a2c2..ffd7fa94ab 100644
--- a/ext/jni/src/org/sqlite/jni/CollationNeededCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/CollationNeededCallback.java
@@ -11,18 +11,19 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
- Callback for use with {@link SQLite3Jni#sqlite3_collation_needed}.
+ Callback for use with {@link CApi#sqlite3_collation_needed}.
*/
-public interface CollationNeededCallback extends SQLite3CallbackProxy {
+public interface CollationNeededCallback extends CallbackProxy {
/**
Has the same semantics as the C-level sqlite3_create_collation()
callback.
-
If it throws, the exception message is passed on to the db and
- the exception is suppressed.
+
Because the C API has no mechanism for reporting errors
+ from this callbacks, any exceptions thrown by this callback
+ are suppressed.
*/
- int call(sqlite3 db, int eTextRep, String collationName);
+ void call(sqlite3 db, int eTextRep, String collationName);
}
diff --git a/ext/jni/src/org/sqlite/jni/CommitHookCallback.java b/ext/jni/src/org/sqlite/jni/capi/CommitHookCallback.java
similarity index 69%
rename from ext/jni/src/org/sqlite/jni/CommitHookCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/CommitHookCallback.java
index 253d0b8cfa..e1e55c78d2 100644
--- a/ext/jni/src/org/sqlite/jni/CommitHookCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/CommitHookCallback.java
@@ -11,15 +11,16 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
- Callback for use with {@link SQLite3Jni#sqlite3_commit_hook}.
+ Callback for use with {@link CApi#sqlite3_commit_hook}.
*/
-public interface CommitHookCallback extends SQLite3CallbackProxy {
+public interface CommitHookCallback extends CallbackProxy {
/**
Works as documented for the C-level sqlite3_commit_hook()
- callback. Must not throw.
+ callback. If it throws, the exception is translated into
+ a db-level error.
*/
int call();
}
diff --git a/ext/jni/src/org/sqlite/jni/capi/ConfigLogCallback.java b/ext/jni/src/org/sqlite/jni/capi/ConfigLogCallback.java
new file mode 100644
index 0000000000..6513b0730d
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/capi/ConfigLogCallback.java
@@ -0,0 +1,25 @@
+/*
+** 2023-08-23
+**
+** 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 JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+ A callback for use with sqlite3_config().
+*/
+public interface ConfigLogCallback {
+ /**
+ Must function as described for a C-level callback for
+ {@link CApi#sqlite3_config(ConfigLogCallback)}, with the slight signature change.
+ */
+ void call(int errCode, String msg);
+}
diff --git a/ext/jni/src/org/sqlite/jni/ConfigSqllogCallback.java b/ext/jni/src/org/sqlite/jni/capi/ConfigSqlLogCallback.java
similarity index 79%
rename from ext/jni/src/org/sqlite/jni/ConfigSqllogCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/ConfigSqlLogCallback.java
index 9bdd209a7a..a5530b49a4 100644
--- a/ext/jni/src/org/sqlite/jni/ConfigSqllogCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/ConfigSqlLogCallback.java
@@ -11,15 +11,15 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
A callback for use with sqlite3_config().
*/
-public interface ConfigSqllogCallback {
+public interface ConfigSqlLogCallback {
/**
Must function as described for a C-level callback for
- {@link SQLite3Jni#sqlite3_config(ConfigSqllogCallback)}, with the slight signature change.
+ {@link CApi#sqlite3_config(ConfigSqlLogCallback)}, with the slight signature change.
*/
void call(sqlite3 db, String msg, int msgType );
}
diff --git a/ext/jni/src/org/sqlite/jni/NativePointerHolder.java b/ext/jni/src/org/sqlite/jni/capi/NativePointerHolder.java
similarity index 74%
rename from ext/jni/src/org/sqlite/jni/NativePointerHolder.java
rename to ext/jni/src/org/sqlite/jni/capi/NativePointerHolder.java
index 251eb7faad..e82909e424 100644
--- a/ext/jni/src/org/sqlite/jni/NativePointerHolder.java
+++ b/ext/jni/src/org/sqlite/jni/capi/NativePointerHolder.java
@@ -11,7 +11,7 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
A helper for passing pointers between JNI C code and Java, in
@@ -29,5 +29,18 @@ package org.sqlite.jni;
public class NativePointerHolder {
//! Only set from JNI, where access permissions don't matter.
private volatile long nativePointer = 0;
+ /**
+ For use ONLY by package-level APIs which act as proxies for
+ close/finalize operations. Such ops must call this to zero out
+ the pointer so that this object is not carrying a stale
+ pointer. This function returns the prior value of the pointer and
+ sets it to 0.
+ */
+ final long clearNativePointer() {
+ final long rv = nativePointer;
+ nativePointer= 0;
+ return rv;
+ }
+
public final long getNativePointer(){ return nativePointer; }
}
diff --git a/ext/jni/src/org/sqlite/jni/OutputPointer.java b/ext/jni/src/org/sqlite/jni/capi/OutputPointer.java
similarity index 66%
rename from ext/jni/src/org/sqlite/jni/OutputPointer.java
rename to ext/jni/src/org/sqlite/jni/capi/OutputPointer.java
index 8a59de574b..7bf7529da1 100644
--- a/ext/jni/src/org/sqlite/jni/OutputPointer.java
+++ b/ext/jni/src/org/sqlite/jni/capi/OutputPointer.java
@@ -11,7 +11,7 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
Helper classes for handling JNI output pointers.
@@ -49,16 +49,37 @@ public final class OutputPointer {
code.
*/
public static final class sqlite3 {
- private org.sqlite.jni.sqlite3 value;
+ private org.sqlite.jni.capi.sqlite3 value;
/** Initializes with a null value. */
public sqlite3(){value = null;}
/** Sets the current value to null. */
public void clear(){value = null;}
/** Returns the current value. */
- public final org.sqlite.jni.sqlite3 get(){return value;}
+ public final org.sqlite.jni.capi.sqlite3 get(){return value;}
/** Equivalent to calling get() then clear(). */
- public final org.sqlite.jni.sqlite3 take(){
- final org.sqlite.jni.sqlite3 v = value;
+ public final org.sqlite.jni.capi.sqlite3 take(){
+ final org.sqlite.jni.capi.sqlite3 v = value;
+ value = null;
+ return v;
+ }
+ }
+
+ /**
+ Output pointer for sqlite3_blob_open(). These
+ pointers can only be set by the JNI layer, not by client-level
+ code.
+ */
+ public static final class sqlite3_blob {
+ private org.sqlite.jni.capi.sqlite3_blob value;
+ /** Initializes with a null value. */
+ public sqlite3_blob(){value = null;}
+ /** Sets the current value to null. */
+ public void clear(){value = null;}
+ /** Returns the current value. */
+ public final org.sqlite.jni.capi.sqlite3_blob get(){return value;}
+ /** Equivalent to calling get() then clear(). */
+ public final org.sqlite.jni.capi.sqlite3_blob take(){
+ final org.sqlite.jni.capi.sqlite3_blob v = value;
value = null;
return v;
}
@@ -71,16 +92,16 @@ public final class OutputPointer {
code.
*/
public static final class sqlite3_stmt {
- private org.sqlite.jni.sqlite3_stmt value;
+ private org.sqlite.jni.capi.sqlite3_stmt value;
/** Initializes with a null value. */
public sqlite3_stmt(){value = null;}
/** Sets the current value to null. */
public void clear(){value = null;}
/** Returns the current value. */
- public final org.sqlite.jni.sqlite3_stmt get(){return value;}
+ public final org.sqlite.jni.capi.sqlite3_stmt get(){return value;}
/** Equivalent to calling get() then clear(). */
- public final org.sqlite.jni.sqlite3_stmt take(){
- final org.sqlite.jni.sqlite3_stmt v = value;
+ public final org.sqlite.jni.capi.sqlite3_stmt take(){
+ final org.sqlite.jni.capi.sqlite3_stmt v = value;
value = null;
return v;
}
@@ -93,21 +114,41 @@ public final class OutputPointer {
code.
*/
public static final class sqlite3_value {
- private org.sqlite.jni.sqlite3_value value;
+ private org.sqlite.jni.capi.sqlite3_value value;
/** Initializes with a null value. */
public sqlite3_value(){value = null;}
/** Sets the current value to null. */
public void clear(){value = null;}
/** Returns the current value. */
- public final org.sqlite.jni.sqlite3_value get(){return value;}
+ public final org.sqlite.jni.capi.sqlite3_value get(){return value;}
/** Equivalent to calling get() then clear(). */
- public final org.sqlite.jni.sqlite3_value take(){
- final org.sqlite.jni.sqlite3_value v = value;
+ public final org.sqlite.jni.capi.sqlite3_value take(){
+ final org.sqlite.jni.capi.sqlite3_value v = value;
value = null;
return v;
}
}
+ /**
+ Output pointer for use with native routines which return booleans
+ via integer output pointers.
+ */
+ public static final class Bool {
+ /**
+ This is public for ease of use. Accessors are provided for
+ consistency with the higher-level types.
+ */
+ public boolean value;
+ /** Initializes with the value 0. */
+ public Bool(){this(false);}
+ /** Initializes with the value v. */
+ public Bool(boolean v){value = v;}
+ /** Returns the current value. */
+ public final boolean get(){return value;}
+ /** Sets the current value to v. */
+ public final void set(boolean v){value = v;}
+ }
+
/**
Output pointer for use with native routines which return integers via
output pointers.
@@ -187,4 +228,26 @@ public final class OutputPointer {
/** Sets the current value. */
public final void set(byte[] v){value = v;}
}
+
+ /**
+ Output pointer for use with native routines which return
+ blobs via java.nio.ByteBuffer.
+
+ See {@link org.sqlite.jni.capi.CApi#sqlite3_jni_supports_nio}
+ */
+ public static final class ByteBuffer {
+ /**
+ This is public for ease of use. Accessors are provided for
+ consistency with the higher-level types.
+ */
+ public java.nio.ByteBuffer value;
+ /** Initializes with the value null. */
+ public ByteBuffer(){this(null);}
+ /** Initializes with the value v. */
+ public ByteBuffer(java.nio.ByteBuffer v){value = v;}
+ /** Returns the current value. */
+ public final java.nio.ByteBuffer get(){return value;}
+ /** Sets the current value. */
+ public final void set(java.nio.ByteBuffer v){value = v;}
+ }
}
diff --git a/ext/jni/src/org/sqlite/jni/capi/PrepareMultiCallback.java b/ext/jni/src/org/sqlite/jni/capi/PrepareMultiCallback.java
new file mode 100644
index 0000000000..9f6dd478ce
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/capi/PrepareMultiCallback.java
@@ -0,0 +1,81 @@
+/*
+** 2023-09-13
+**
+** 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 JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+ Callback for use with {@link CApi#sqlite3_prepare_multi}.
+*/
+public interface PrepareMultiCallback extends CallbackProxy {
+
+ /**
+ Gets passed a sqlite3_stmt which it may handle in arbitrary ways,
+ transfering ownership of it to this function.
+
+ sqlite3_prepare_multi() will _not_ finalize st - it is up
+ to the call() implementation how st is handled.
+
+ Must return 0 on success or an SQLITE_... code on error. If it
+ throws, sqlite3_prepare_multi() will transform the exception into
+ a db-level error in order to retain the C-style error semantics
+ of the API.
+
+ See the {@link Finalize} class for a wrapper which finalizes the
+ statement after calling a proxy PrepareMultiCallback.
+ */
+ int call(sqlite3_stmt st);
+
+ /**
+ A PrepareMultiCallback impl which wraps a separate impl and finalizes
+ any sqlite3_stmt passed to its callback.
+ */
+ public static final class Finalize implements PrepareMultiCallback {
+ private final PrepareMultiCallback p;
+ /**
+ p is the proxy to call() when this.call() is called.
+ */
+ public Finalize( PrepareMultiCallback p ){
+ this.p = p;
+ }
+ /**
+ Calls the call() method of the proxied callback and either returns its
+ result or propagates an exception. Either way, it passes its argument to
+ sqlite3_finalize() before returning.
+ */
+ @Override public int call(sqlite3_stmt st){
+ try {
+ return this.p.call(st);
+ }finally{
+ CApi.sqlite3_finalize(st);
+ }
+ }
+ }
+
+ /**
+ A PrepareMultiCallback impl which steps entirely through a result set,
+ ignoring all non-error results.
+ */
+ public static final class StepAll implements PrepareMultiCallback {
+ public StepAll(){}
+ /**
+ Calls sqlite3_step() on st until it returns something other than
+ SQLITE_ROW. If the final result is SQLITE_DONE then 0 is returned,
+ else the result of the final step is returned.
+ */
+ @Override public int call(sqlite3_stmt st){
+ int rc = CApi.SQLITE_DONE;
+ while( CApi.SQLITE_ROW == (rc = CApi.sqlite3_step(st)) ){}
+ return CApi.SQLITE_DONE==rc ? 0 : rc;
+ }
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/PreupdateHookCallback.java b/ext/jni/src/org/sqlite/jni/capi/PreupdateHookCallback.java
similarity index 70%
rename from ext/jni/src/org/sqlite/jni/PreupdateHookCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/PreupdateHookCallback.java
index b68dd4b6d4..38f7c5613e 100644
--- a/ext/jni/src/org/sqlite/jni/PreupdateHookCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/PreupdateHookCallback.java
@@ -11,15 +11,16 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
- Callback for use with {@link SQLite3Jni#sqlite3_preupdate_hook}.
+ Callback for use with {@link CApi#sqlite3_preupdate_hook}.
*/
-public interface PreupdateHookCallback extends SQLite3CallbackProxy {
+public interface PreupdateHookCallback extends CallbackProxy {
/**
Must function as described for the C-level sqlite3_preupdate_hook()
- callback.
+ callback. If it throws, the exception is translated to a
+ db-level error and the exception is suppressed.
*/
void call(sqlite3 db, int op, String dbName, String dbTable,
long iKey1, long iKey2 );
diff --git a/ext/jni/src/org/sqlite/jni/ProgressHandlerCallback.java b/ext/jni/src/org/sqlite/jni/capi/ProgressHandlerCallback.java
similarity index 79%
rename from ext/jni/src/org/sqlite/jni/ProgressHandlerCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/ProgressHandlerCallback.java
index d15bf31a11..464baa2e3d 100644
--- a/ext/jni/src/org/sqlite/jni/ProgressHandlerCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/ProgressHandlerCallback.java
@@ -11,12 +11,12 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
- Callback for use with {@link SQLite3Jni#sqlite3_progress_handler}.
+ Callback for use with {@link CApi#sqlite3_progress_handler}.
*/
-public interface ProgressHandlerCallback extends SQLite3CallbackProxy {
+public interface ProgressHandlerCallback extends CallbackProxy {
/**
Works as documented for the C-level sqlite3_progress_handler() callback.
diff --git a/ext/jni/src/org/sqlite/jni/capi/ResultCode.java b/ext/jni/src/org/sqlite/jni/capi/ResultCode.java
new file mode 100644
index 0000000000..5a8b2e6a18
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/capi/ResultCode.java
@@ -0,0 +1,155 @@
+/*
+** 2023-07-21
+**
+** 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 JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+ This enum contains all of the core and "extended" result codes used
+ by the sqlite3 library. It is provided not for use with the C-style
+ API (with which it won't work) but for higher-level code which may
+ find it useful to map SQLite result codes to human-readable names.
+*/
+public enum ResultCode {
+ SQLITE_OK(CApi.SQLITE_OK),
+ SQLITE_ERROR(CApi.SQLITE_ERROR),
+ SQLITE_INTERNAL(CApi.SQLITE_INTERNAL),
+ SQLITE_PERM(CApi.SQLITE_PERM),
+ SQLITE_ABORT(CApi.SQLITE_ABORT),
+ SQLITE_BUSY(CApi.SQLITE_BUSY),
+ SQLITE_LOCKED(CApi.SQLITE_LOCKED),
+ SQLITE_NOMEM(CApi.SQLITE_NOMEM),
+ SQLITE_READONLY(CApi.SQLITE_READONLY),
+ SQLITE_INTERRUPT(CApi.SQLITE_INTERRUPT),
+ SQLITE_IOERR(CApi.SQLITE_IOERR),
+ SQLITE_CORRUPT(CApi.SQLITE_CORRUPT),
+ SQLITE_NOTFOUND(CApi.SQLITE_NOTFOUND),
+ SQLITE_FULL(CApi.SQLITE_FULL),
+ SQLITE_CANTOPEN(CApi.SQLITE_CANTOPEN),
+ SQLITE_PROTOCOL(CApi.SQLITE_PROTOCOL),
+ SQLITE_EMPTY(CApi.SQLITE_EMPTY),
+ SQLITE_SCHEMA(CApi.SQLITE_SCHEMA),
+ SQLITE_TOOBIG(CApi.SQLITE_TOOBIG),
+ SQLITE_CONSTRAINT(CApi.SQLITE_CONSTRAINT),
+ SQLITE_MISMATCH(CApi.SQLITE_MISMATCH),
+ SQLITE_MISUSE(CApi.SQLITE_MISUSE),
+ SQLITE_NOLFS(CApi.SQLITE_NOLFS),
+ SQLITE_AUTH(CApi.SQLITE_AUTH),
+ SQLITE_FORMAT(CApi.SQLITE_FORMAT),
+ SQLITE_RANGE(CApi.SQLITE_RANGE),
+ SQLITE_NOTADB(CApi.SQLITE_NOTADB),
+ SQLITE_NOTICE(CApi.SQLITE_NOTICE),
+ SQLITE_WARNING(CApi.SQLITE_WARNING),
+ SQLITE_ROW(CApi.SQLITE_ROW),
+ SQLITE_DONE(CApi.SQLITE_DONE),
+ SQLITE_ERROR_MISSING_COLLSEQ(CApi.SQLITE_ERROR_MISSING_COLLSEQ),
+ SQLITE_ERROR_RETRY(CApi.SQLITE_ERROR_RETRY),
+ SQLITE_ERROR_SNAPSHOT(CApi.SQLITE_ERROR_SNAPSHOT),
+ SQLITE_IOERR_READ(CApi.SQLITE_IOERR_READ),
+ SQLITE_IOERR_SHORT_READ(CApi.SQLITE_IOERR_SHORT_READ),
+ SQLITE_IOERR_WRITE(CApi.SQLITE_IOERR_WRITE),
+ SQLITE_IOERR_FSYNC(CApi.SQLITE_IOERR_FSYNC),
+ SQLITE_IOERR_DIR_FSYNC(CApi.SQLITE_IOERR_DIR_FSYNC),
+ SQLITE_IOERR_TRUNCATE(CApi.SQLITE_IOERR_TRUNCATE),
+ SQLITE_IOERR_FSTAT(CApi.SQLITE_IOERR_FSTAT),
+ SQLITE_IOERR_UNLOCK(CApi.SQLITE_IOERR_UNLOCK),
+ SQLITE_IOERR_RDLOCK(CApi.SQLITE_IOERR_RDLOCK),
+ SQLITE_IOERR_DELETE(CApi.SQLITE_IOERR_DELETE),
+ SQLITE_IOERR_BLOCKED(CApi.SQLITE_IOERR_BLOCKED),
+ SQLITE_IOERR_NOMEM(CApi.SQLITE_IOERR_NOMEM),
+ SQLITE_IOERR_ACCESS(CApi.SQLITE_IOERR_ACCESS),
+ SQLITE_IOERR_CHECKRESERVEDLOCK(CApi.SQLITE_IOERR_CHECKRESERVEDLOCK),
+ SQLITE_IOERR_LOCK(CApi.SQLITE_IOERR_LOCK),
+ SQLITE_IOERR_CLOSE(CApi.SQLITE_IOERR_CLOSE),
+ SQLITE_IOERR_DIR_CLOSE(CApi.SQLITE_IOERR_DIR_CLOSE),
+ SQLITE_IOERR_SHMOPEN(CApi.SQLITE_IOERR_SHMOPEN),
+ SQLITE_IOERR_SHMSIZE(CApi.SQLITE_IOERR_SHMSIZE),
+ SQLITE_IOERR_SHMLOCK(CApi.SQLITE_IOERR_SHMLOCK),
+ SQLITE_IOERR_SHMMAP(CApi.SQLITE_IOERR_SHMMAP),
+ SQLITE_IOERR_SEEK(CApi.SQLITE_IOERR_SEEK),
+ SQLITE_IOERR_DELETE_NOENT(CApi.SQLITE_IOERR_DELETE_NOENT),
+ SQLITE_IOERR_MMAP(CApi.SQLITE_IOERR_MMAP),
+ SQLITE_IOERR_GETTEMPPATH(CApi.SQLITE_IOERR_GETTEMPPATH),
+ SQLITE_IOERR_CONVPATH(CApi.SQLITE_IOERR_CONVPATH),
+ SQLITE_IOERR_VNODE(CApi.SQLITE_IOERR_VNODE),
+ SQLITE_IOERR_AUTH(CApi.SQLITE_IOERR_AUTH),
+ SQLITE_IOERR_BEGIN_ATOMIC(CApi.SQLITE_IOERR_BEGIN_ATOMIC),
+ SQLITE_IOERR_COMMIT_ATOMIC(CApi.SQLITE_IOERR_COMMIT_ATOMIC),
+ SQLITE_IOERR_ROLLBACK_ATOMIC(CApi.SQLITE_IOERR_ROLLBACK_ATOMIC),
+ SQLITE_IOERR_DATA(CApi.SQLITE_IOERR_DATA),
+ SQLITE_IOERR_CORRUPTFS(CApi.SQLITE_IOERR_CORRUPTFS),
+ SQLITE_LOCKED_SHAREDCACHE(CApi.SQLITE_LOCKED_SHAREDCACHE),
+ SQLITE_LOCKED_VTAB(CApi.SQLITE_LOCKED_VTAB),
+ SQLITE_BUSY_RECOVERY(CApi.SQLITE_BUSY_RECOVERY),
+ SQLITE_BUSY_SNAPSHOT(CApi.SQLITE_BUSY_SNAPSHOT),
+ SQLITE_BUSY_TIMEOUT(CApi.SQLITE_BUSY_TIMEOUT),
+ SQLITE_CANTOPEN_NOTEMPDIR(CApi.SQLITE_CANTOPEN_NOTEMPDIR),
+ SQLITE_CANTOPEN_ISDIR(CApi.SQLITE_CANTOPEN_ISDIR),
+ SQLITE_CANTOPEN_FULLPATH(CApi.SQLITE_CANTOPEN_FULLPATH),
+ SQLITE_CANTOPEN_CONVPATH(CApi.SQLITE_CANTOPEN_CONVPATH),
+ SQLITE_CANTOPEN_SYMLINK(CApi.SQLITE_CANTOPEN_SYMLINK),
+ SQLITE_CORRUPT_VTAB(CApi.SQLITE_CORRUPT_VTAB),
+ SQLITE_CORRUPT_SEQUENCE(CApi.SQLITE_CORRUPT_SEQUENCE),
+ SQLITE_CORRUPT_INDEX(CApi.SQLITE_CORRUPT_INDEX),
+ SQLITE_READONLY_RECOVERY(CApi.SQLITE_READONLY_RECOVERY),
+ SQLITE_READONLY_CANTLOCK(CApi.SQLITE_READONLY_CANTLOCK),
+ SQLITE_READONLY_ROLLBACK(CApi.SQLITE_READONLY_ROLLBACK),
+ SQLITE_READONLY_DBMOVED(CApi.SQLITE_READONLY_DBMOVED),
+ SQLITE_READONLY_CANTINIT(CApi.SQLITE_READONLY_CANTINIT),
+ SQLITE_READONLY_DIRECTORY(CApi.SQLITE_READONLY_DIRECTORY),
+ SQLITE_ABORT_ROLLBACK(CApi.SQLITE_ABORT_ROLLBACK),
+ SQLITE_CONSTRAINT_CHECK(CApi.SQLITE_CONSTRAINT_CHECK),
+ SQLITE_CONSTRAINT_COMMITHOOK(CApi.SQLITE_CONSTRAINT_COMMITHOOK),
+ SQLITE_CONSTRAINT_FOREIGNKEY(CApi.SQLITE_CONSTRAINT_FOREIGNKEY),
+ SQLITE_CONSTRAINT_FUNCTION(CApi.SQLITE_CONSTRAINT_FUNCTION),
+ SQLITE_CONSTRAINT_NOTNULL(CApi.SQLITE_CONSTRAINT_NOTNULL),
+ SQLITE_CONSTRAINT_PRIMARYKEY(CApi.SQLITE_CONSTRAINT_PRIMARYKEY),
+ SQLITE_CONSTRAINT_TRIGGER(CApi.SQLITE_CONSTRAINT_TRIGGER),
+ SQLITE_CONSTRAINT_UNIQUE(CApi.SQLITE_CONSTRAINT_UNIQUE),
+ SQLITE_CONSTRAINT_VTAB(CApi.SQLITE_CONSTRAINT_VTAB),
+ SQLITE_CONSTRAINT_ROWID(CApi.SQLITE_CONSTRAINT_ROWID),
+ SQLITE_CONSTRAINT_PINNED(CApi.SQLITE_CONSTRAINT_PINNED),
+ SQLITE_CONSTRAINT_DATATYPE(CApi.SQLITE_CONSTRAINT_DATATYPE),
+ SQLITE_NOTICE_RECOVER_WAL(CApi.SQLITE_NOTICE_RECOVER_WAL),
+ SQLITE_NOTICE_RECOVER_ROLLBACK(CApi.SQLITE_NOTICE_RECOVER_ROLLBACK),
+ SQLITE_WARNING_AUTOINDEX(CApi.SQLITE_WARNING_AUTOINDEX),
+ SQLITE_AUTH_USER(CApi.SQLITE_AUTH_USER),
+ SQLITE_OK_LOAD_PERMANENTLY(CApi.SQLITE_OK_LOAD_PERMANENTLY);
+
+ public final int value;
+
+ ResultCode(int rc){
+ value = rc;
+ ResultCodeMap.set(rc, this);
+ }
+
+ /**
+ Returns the entry from this enum for the given result code, or
+ null if no match is found.
+ */
+ public static ResultCode getEntryForInt(int rc){
+ return ResultCodeMap.get(rc);
+ }
+
+ /**
+ Internal level of indirection required because we cannot initialize
+ static enum members in an enum before the enum constructor is
+ invoked.
+ */
+ private static final class ResultCodeMap {
+ private static final java.util.Map i2e
+ = new java.util.HashMap<>();
+ private static void set(int rc, ResultCode e){ i2e.put(rc, e); }
+ private static ResultCode get(int rc){ return i2e.get(rc); }
+ }
+
+}
diff --git a/ext/jni/src/org/sqlite/jni/capi/RollbackHookCallback.java b/ext/jni/src/org/sqlite/jni/capi/RollbackHookCallback.java
new file mode 100644
index 0000000000..cf9c4b6e7a
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/capi/RollbackHookCallback.java
@@ -0,0 +1,26 @@
+/*
+** 2023-08-25
+**
+** 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 JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+ Callback for use with {@link CApi#sqlite3_rollback_hook}.
+*/
+public interface RollbackHookCallback extends CallbackProxy {
+ /**
+ Must function as documented for the C-level sqlite3_rollback_hook()
+ callback. If it throws, the exception is translated into
+ a db-level error.
+ */
+ void call();
+}
diff --git a/ext/jni/src/org/sqlite/jni/capi/SQLFunction.java b/ext/jni/src/org/sqlite/jni/capi/SQLFunction.java
new file mode 100644
index 0000000000..7ad1381a7a
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/capi/SQLFunction.java
@@ -0,0 +1,36 @@
+/*
+** 2023-07-22
+**
+** 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 JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+ SQLFunction is used in conjunction with the
+ sqlite3_create_function() JNI-bound API to give that native code
+ access to the callback functions needed in order to implement SQL
+ functions in Java.
+
+
+
+ This class is not used by itself, but is a marker base class. The
+ three UDF types are modelled by the inner classes Scalar,
+ Aggregate, and Window. Most simply, clients may subclass
+ those, or create anonymous classes from them, to implement
+ UDFs. Clients are free to create their own classes for use with
+ UDFs, so long as they conform to the public interfaces defined by
+ those three classes. The JNI layer only actively relies on the
+ SQLFunction base class and the method names and signatures used by
+ the UDF callback interfaces.
+*/
+public interface SQLFunction {
+
+}
diff --git a/ext/jni/src/org/sqlite/jni/tester/SQLTester.java b/ext/jni/src/org/sqlite/jni/capi/SQLTester.java
similarity index 98%
rename from ext/jni/src/org/sqlite/jni/tester/SQLTester.java
rename to ext/jni/src/org/sqlite/jni/capi/SQLTester.java
index 517d8c30a5..81d6106be7 100644
--- a/ext/jni/src/org/sqlite/jni/tester/SQLTester.java
+++ b/ext/jni/src/org/sqlite/jni/capi/SQLTester.java
@@ -12,16 +12,13 @@
** This file contains the main application entry pointer for the
** SQLTester framework.
*/
-package org.sqlite.jni.tester;
+package org.sqlite.jni.capi;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.nio.charset.StandardCharsets;
import java.util.regex.*;
-import org.sqlite.jni.*;
-import static org.sqlite.jni.SQLite3Jni.*;
-import org.sqlite.jni.sqlite3;
-
+import static org.sqlite.jni.capi.CApi.*;
/**
Modes for how to escape (or not) column values and names from
@@ -150,12 +147,15 @@ class Outer {
}
/**
- This class provides an application which aims to implement the
+ This class provides an application which aims to implement the
rudimentary SQL-driven test tool described in the accompanying
{@code test-script-interpreter.md}.
-
This is a work in progress.
-
+
This class is an internal testing tool, not part of the public
+ interface but is (A) in the same package as the library because
+ access permissions require it to be so and (B) the JDK8 javadoc
+ offers no way to filter individual classes out of the doc
+ generation process (it can only exclude packages, but see (A)).
An instance of this application provides a core set of services
which TestScript instances use for processing testing logic.
@@ -457,7 +457,7 @@ public class SQLTester {
}
private void appendDbErr(sqlite3 db, StringBuilder sb, int rc){
- sb.append(org.sqlite.jni.ResultCode.getEntryForInt(rc)).append(' ');
+ sb.append(org.sqlite.jni.capi.ResultCode.getEntryForInt(rc)).append(' ');
final String msg = escapeSqlValue(sqlite3_errmsg(db));
if( '{' == msg.charAt(0) ){
sb.append(msg);
@@ -668,7 +668,7 @@ public class SQLTester {
static {
System.loadLibrary("sqlite3-jni")
/* Interestingly, when SQLTester is the main app, we have to
- load that lib from here. The same load from SQLite3Jni does
+ load that lib from here. The same load from CApi does
not happen early enough. Without this,
installCustomExtensions() is an unresolved symbol. */;
}
diff --git a/ext/jni/src/org/sqlite/jni/ScalarFunction.java b/ext/jni/src/org/sqlite/jni/capi/ScalarFunction.java
similarity index 91%
rename from ext/jni/src/org/sqlite/jni/ScalarFunction.java
rename to ext/jni/src/org/sqlite/jni/capi/ScalarFunction.java
index 73fb58cda2..95541bdcba 100644
--- a/ext/jni/src/org/sqlite/jni/ScalarFunction.java
+++ b/ext/jni/src/org/sqlite/jni/capi/ScalarFunction.java
@@ -11,7 +11,7 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
@@ -27,7 +27,7 @@ public abstract class ScalarFunction implements SQLFunction {
/**
Optionally override to be notified when the UDF is finalized by
- SQLite. This implementation does nothing.
+ SQLite. This default implementation does nothing.
*/
public void xDestroy() {}
}
diff --git a/ext/jni/src/org/sqlite/jni/capi/TableColumnMetadata.java b/ext/jni/src/org/sqlite/jni/capi/TableColumnMetadata.java
new file mode 100644
index 0000000000..d8b6226ac9
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/capi/TableColumnMetadata.java
@@ -0,0 +1,35 @@
+/*
+** 2023-07-21
+**
+** 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 JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+ A wrapper object for use with sqlite3_table_column_metadata().
+ They are populated only via that interface.
+*/
+public final class TableColumnMetadata {
+ OutputPointer.Bool pNotNull = new OutputPointer.Bool();
+ OutputPointer.Bool pPrimaryKey = new OutputPointer.Bool();
+ OutputPointer.Bool pAutoinc = new OutputPointer.Bool();
+ OutputPointer.String pzCollSeq = new OutputPointer.String();
+ OutputPointer.String pzDataType = new OutputPointer.String();
+
+ public TableColumnMetadata(){
+ }
+
+ public String getDataType(){ return pzDataType.value; }
+ public String getCollation(){ return pzCollSeq.value; }
+ public boolean isNotNull(){ return pNotNull.value; }
+ public boolean isPrimaryKey(){ return pPrimaryKey.value; }
+ public boolean isAutoincrement(){ return pAutoinc.value; }
+}
diff --git a/ext/jni/src/org/sqlite/jni/Tester1.java b/ext/jni/src/org/sqlite/jni/capi/Tester1.java
similarity index 66%
rename from ext/jni/src/org/sqlite/jni/Tester1.java
rename to ext/jni/src/org/sqlite/jni/capi/Tester1.java
index e418de6096..05b1cfeaed 100644
--- a/ext/jni/src/org/sqlite/jni/Tester1.java
+++ b/ext/jni/src/org/sqlite/jni/capi/Tester1.java
@@ -11,8 +11,8 @@
*************************************************************************
** This file contains a set of tests for the sqlite3 JNI bindings.
*/
-package org.sqlite.jni;
-import static org.sqlite.jni.SQLite3Jni.*;
+package org.sqlite.jni.capi;
+import static org.sqlite.jni.capi.CApi.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.ArrayList;
@@ -38,6 +38,14 @@ import java.util.concurrent.Future;
@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
@interface SingleThreadOnly{}
+/**
+ Annotation for Tester1 tests which must only be run if
+ sqlite3_jni_supports_nio() is true.
+*/
+@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
+@interface RequiresJniNio{}
+
public class Tester1 implements Runnable {
//! True when running in multi-threaded mode.
private static boolean mtMode = false;
@@ -46,7 +54,7 @@ public class Tester1 implements Runnable {
//! True to shuffle the order of the tests.
private static boolean shuffle = false;
//! True to dump the list of to-run tests to stdout.
- private static boolean listRunTests = false;
+ private static int listRunTests = 0;
//! True to squelch all out() and outln() output.
private static boolean quietMode = false;
//! Total number of runTests() calls.
@@ -68,62 +76,67 @@ public class Tester1 implements Runnable {
static final Metrics metrics = new Metrics();
- public synchronized static void outln(){
+ public static synchronized void outln(){
if( !quietMode ){
System.out.println("");
}
}
- public synchronized static void outln(Object val){
+ public static synchronized void outPrefix(){
if( !quietMode ){
System.out.print(Thread.currentThread().getName()+": ");
+ }
+ }
+
+ public static synchronized void outln(Object val){
+ if( !quietMode ){
+ outPrefix();
System.out.println(val);
}
}
- public synchronized static void out(Object val){
+ public static synchronized void out(Object val){
if( !quietMode ){
System.out.print(val);
}
}
@SuppressWarnings("unchecked")
- public synchronized static void out(Object... vals){
+ public static synchronized void out(Object... vals){
if( !quietMode ){
- System.out.print(Thread.currentThread().getName()+": ");
+ outPrefix();
for(Object v : vals) out(v);
}
}
@SuppressWarnings("unchecked")
- public synchronized static void outln(Object... vals){
+ public static synchronized void outln(Object... vals){
if( !quietMode ){
out(vals); out("\n");
}
}
static volatile int affirmCount = 0;
- public synchronized static void affirm(Boolean v, String comment){
+ public static synchronized int affirm(Boolean v, String comment){
++affirmCount;
if( false ) assert( v /* prefer assert over exception if it's enabled because
the JNI layer sometimes has to suppress exceptions,
so they might be squelched on their way back to the
top. */);
if( !v ) throw new RuntimeException(comment);
+ return affirmCount;
}
public static void affirm(Boolean v){
affirm(v, "Affirmation failed.");
}
- @ManualTest /* because testing this for threading is pointless */
+ @SingleThreadOnly /* because it's thread-agnostic */
private void test1(){
affirm(sqlite3_libversion_number() == SQLITE_VERSION_NUMBER);
- affirm(SQLITE_MAX_LENGTH > 0);
- affirm(SQLITE_MAX_TRIGGER_DEPTH>0);
}
- static sqlite3 createNewDb(){
+ public static sqlite3 createNewDb(){
final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
int rc = sqlite3_open(":memory:", out);
++metrics.dbOpen;
@@ -141,11 +154,11 @@ public class Tester1 implements Runnable {
return db;
}
- static void execSql(sqlite3 db, String[] sql){
+ public static void execSql(sqlite3 db, String[] sql){
execSql(db, String.join("", sql));
}
- static int execSql(sqlite3 db, boolean throwOnError, String sql){
+ public static int execSql(sqlite3 db, boolean throwOnError, String sql){
OutputPointer.Int32 oTail = new OutputPointer.Int32();
final byte[] sqlUtf8 = sql.getBytes(StandardCharsets.UTF_8);
int pos = 0, n = 1;
@@ -186,13 +199,13 @@ public class Tester1 implements Runnable {
return rc;
}
- static void execSql(sqlite3 db, String sql){
+ public static void execSql(sqlite3 db, String sql){
execSql(db, true, sql);
}
- static sqlite3_stmt prepare(sqlite3 db, boolean throwOnError, String sql){
+ public static sqlite3_stmt prepare(sqlite3 db, boolean throwOnError, String sql){
final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
- int rc = sqlite3_prepare(db, sql, outStmt);
+ int rc = sqlite3_prepare_v2(db, sql, outStmt);
if( throwOnError ){
affirm( 0 == rc );
}
@@ -203,9 +216,11 @@ public class Tester1 implements Runnable {
}
return rv;
}
- static sqlite3_stmt prepare(sqlite3 db, String sql){
+
+ public static sqlite3_stmt prepare(sqlite3 db, String sql){
return prepare(db, true, sql);
}
+
private void showCompileOption(){
int i = 0;
String optName;
@@ -214,7 +229,15 @@ public class Tester1 implements Runnable {
outln("\t"+optName+"\t (used="+
sqlite3_compileoption_used(optName)+")");
}
+ }
+ private void testCompileOption(){
+ int i = 0;
+ String optName;
+ for( ; null != (optName = sqlite3_compileoption_get(i)); ++i){
+ }
+ affirm( i > 10 );
+ affirm( null==sqlite3_compileoption_get(-1) );
}
private void testOpenDb1(){
@@ -223,12 +246,18 @@ public class Tester1 implements Runnable {
++metrics.dbOpen;
sqlite3 db = out.get();
affirm(0 == rc);
- affirm(0 < db.getNativePointer());
+ affirm(db.getNativePointer()!=0);
sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, null)
/* This function has different mangled names in jdk8 vs jdk19,
and this call is here to ensure that the build fails
if it cannot find both names. */;
+ affirm( 0==sqlite3_db_readonly(db,"main") );
+ affirm( 0==sqlite3_db_readonly(db,null) );
+ affirm( 0>sqlite3_db_readonly(db,"nope") );
+ affirm( 0>sqlite3_db_readonly(null,null) );
+ affirm( 0==sqlite3_last_insert_rowid(null) );
+
// These interrupt checks are only to make sure that the JNI binding
// has the proper exported symbol names. They don't actually test
// anything useful.
@@ -247,7 +276,7 @@ public class Tester1 implements Runnable {
++metrics.dbOpen;
affirm(0 == rc);
sqlite3 db = out.get();
- affirm(0 < db.getNativePointer());
+ affirm(0 != db.getNativePointer());
sqlite3_close_v2(db);
affirm(0 == db.getNativePointer());
}
@@ -260,11 +289,9 @@ public class Tester1 implements Runnable {
affirm(0 == rc);
sqlite3_stmt stmt = outStmt.take();
affirm(0 != stmt.getNativePointer());
+ affirm( !sqlite3_stmt_readonly(stmt) );
affirm( db == sqlite3_db_handle(stmt) );
rc = sqlite3_step(stmt);
- if( SQLITE_DONE != rc ){
- outln("step failed ??? ",rc, " ",sqlite3_errmsg(db));
- }
affirm(SQLITE_DONE == rc);
sqlite3_finalize(stmt);
affirm( null == sqlite3_db_handle(stmt) );
@@ -308,7 +335,7 @@ public class Tester1 implements Runnable {
rc = sqlite3_prepare_v3(db, "INSERT INTO t2(a) VALUES(1),(2),(3)",
- SQLITE_PREPARE_NORMALIZE, outStmt);
+ 0, outStmt);
affirm(0 == rc);
stmt = outStmt.get();
affirm(0 != stmt.getNativePointer());
@@ -335,6 +362,7 @@ public class Tester1 implements Runnable {
affirm(1 == sqlite3_bind_parameter_count(stmt));
final int paramNdx = sqlite3_bind_parameter_index(stmt, ":a");
affirm(1 == paramNdx);
+ affirm( ":a".equals(sqlite3_bind_parameter_name(stmt, paramNdx)));
int total1 = 0;
long rowid = -1;
int changes = sqlite3_changes(db);
@@ -360,72 +388,97 @@ public class Tester1 implements Runnable {
affirm(sqlite3_changes64(db) > changes64);
affirm(sqlite3_total_changes64(db) > changesT64);
stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+ affirm( sqlite3_stmt_readonly(stmt) );
+ affirm( !sqlite3_stmt_busy(stmt) );
+ if( sqlite3_compileoption_used("ENABLE_COLUMN_METADATA") ){
+ /* Unlike in native C code, JNI won't trigger an
+ UnsatisfiedLinkError until these are called (on Linux, at
+ least). */
+ affirm("t".equals(sqlite3_column_table_name(stmt,0)));
+ affirm("main".equals(sqlite3_column_database_name(stmt,0)));
+ affirm("a".equals(sqlite3_column_origin_name(stmt,0)));
+ }
+
int total2 = 0;
while( SQLITE_ROW == sqlite3_step(stmt) ){
+ affirm( sqlite3_stmt_busy(stmt) );
total2 += sqlite3_column_int(stmt, 0);
sqlite3_value sv = sqlite3_column_value(stmt, 0);
affirm( null != sv );
affirm( 0 != sv.getNativePointer() );
affirm( SQLITE_INTEGER == sqlite3_value_type(sv) );
}
+ affirm( !sqlite3_stmt_busy(stmt) );
sqlite3_finalize(stmt);
affirm(total1 == total2);
+
+ // sqlite3_value_frombind() checks...
+ stmt = prepare(db, "SELECT 1, ?");
+ sqlite3_bind_int(stmt, 1, 2);
+ rc = sqlite3_step(stmt);
+ affirm( SQLITE_ROW==rc );
+ affirm( !sqlite3_value_frombind(sqlite3_column_value(stmt, 0)) );
+ affirm( sqlite3_value_frombind(sqlite3_column_value(stmt, 1)) );
+ sqlite3_finalize(stmt);
+
sqlite3_close_v2(db);
affirm(0 == db.getNativePointer());
}
private void testBindFetchInt64(){
- sqlite3 db = createNewDb();
- execSql(db, "CREATE TABLE t(a)");
- sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
- long total1 = 0;
- for(long i = 0xffffffff; i < 0xffffffff + 3; ++i ){
- total1 += i;
- sqlite3_bind_int64(stmt, 1, i);
- sqlite3_step(stmt);
- sqlite3_reset(stmt);
+ try (sqlite3 db = createNewDb()){
+ execSql(db, "CREATE TABLE t(a)");
+ sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+ long total1 = 0;
+ for(long i = 0xffffffff; i < 0xffffffff + 3; ++i ){
+ total1 += i;
+ sqlite3_bind_int64(stmt, 1, i);
+ sqlite3_step(stmt);
+ sqlite3_reset(stmt);
+ }
+ sqlite3_finalize(stmt);
+ stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+ long total2 = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ total2 += sqlite3_column_int64(stmt, 0);
+ }
+ sqlite3_finalize(stmt);
+ affirm(total1 == total2);
+ //sqlite3_close_v2(db);
}
- sqlite3_finalize(stmt);
- stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
- long total2 = 0;
- while( SQLITE_ROW == sqlite3_step(stmt) ){
- total2 += sqlite3_column_int64(stmt, 0);
- }
- sqlite3_finalize(stmt);
- affirm(total1 == total2);
- sqlite3_close_v2(db);
}
private void testBindFetchDouble(){
- sqlite3 db = createNewDb();
- execSql(db, "CREATE TABLE t(a)");
- sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
- double total1 = 0;
- for(double i = 1.5; i < 5.0; i = i + 1.0 ){
- total1 += i;
- sqlite3_bind_double(stmt, 1, i);
- sqlite3_step(stmt);
- sqlite3_reset(stmt);
+ try (sqlite3 db = createNewDb()){
+ execSql(db, "CREATE TABLE t(a)");
+ sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+ double total1 = 0;
+ for(double i = 1.5; i < 5.0; i = i + 1.0 ){
+ total1 += i;
+ sqlite3_bind_double(stmt, 1, i);
+ sqlite3_step(stmt);
+ sqlite3_reset(stmt);
+ }
+ sqlite3_finalize(stmt);
+ stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+ double total2 = 0;
+ int counter = 0;
+ while( SQLITE_ROW == sqlite3_step(stmt) ){
+ ++counter;
+ total2 += sqlite3_column_double(stmt, 0);
+ }
+ affirm(4 == counter);
+ sqlite3_finalize(stmt);
+ affirm(total2<=total1+0.01 && total2>=total1-0.01);
+ //sqlite3_close_v2(db);
}
- sqlite3_finalize(stmt);
- stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
- double total2 = 0;
- int counter = 0;
- while( SQLITE_ROW == sqlite3_step(stmt) ){
- ++counter;
- total2 += sqlite3_column_double(stmt, 0);
- }
- affirm(4 == counter);
- sqlite3_finalize(stmt);
- affirm(total2<=total1+0.01 && total2>=total1-0.01);
- sqlite3_close_v2(db);
}
private void testBindFetchText(){
sqlite3 db = createNewDb();
execSql(db, "CREATE TABLE t(a)");
sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
- String[] list1 = { "hell🤩", "w😃rld", "!" };
+ String[] list1 = { "hell🤩", "w😃rld", "!🤩" };
int rc;
int n = 0;
for( String e : list1 ){
@@ -441,20 +494,56 @@ public class Tester1 implements Runnable {
stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
StringBuilder sbuf = new StringBuilder();
n = 0;
+ final boolean tryNio = sqlite3_jni_supports_nio();
while( SQLITE_ROW == sqlite3_step(stmt) ){
final sqlite3_value sv = sqlite3_value_dup(sqlite3_column_value(stmt,0));
final String txt = sqlite3_column_text16(stmt, 0);
sbuf.append( txt );
- affirm( txt.equals(sqlite3_column_text(stmt, 0)) );
- affirm( txt.equals(sqlite3_value_text(sv)) );
+ affirm( txt.equals(new String(
+ sqlite3_column_text(stmt, 0),
+ StandardCharsets.UTF_8
+ )) );
+ affirm( txt.length() < sqlite3_value_bytes(sv) );
+ affirm( txt.equals(new String(
+ sqlite3_value_text(sv),
+ StandardCharsets.UTF_8)) );
+ affirm( txt.length() == sqlite3_value_bytes16(sv)/2 );
affirm( txt.equals(sqlite3_value_text16(sv)) );
+ if( tryNio ){
+ java.nio.ByteBuffer bu = sqlite3_value_nio_buffer(sv);
+ byte ba[] = sqlite3_value_blob(sv);
+ affirm( ba.length == bu.capacity() );
+ int i = 0;
+ for( byte b : ba ){
+ affirm( b == bu.get(i++) );
+ }
+ }
sqlite3_value_free(sv);
++n;
}
sqlite3_finalize(stmt);
affirm(3 == n);
- affirm("w😃rldhell🤩!".equals(sbuf.toString()));
- sqlite3_close_v2(db);
+ affirm("w😃rldhell🤩!🤩".equals(sbuf.toString()));
+
+ try( sqlite3_stmt stmt2 = prepare(db, "SELECT ?, ?") ){
+ rc = sqlite3_bind_text(stmt2, 1, "");
+ affirm( 0==rc );
+ rc = sqlite3_bind_text(stmt2, 2, (String)null);
+ affirm( 0==rc );
+ rc = sqlite3_step(stmt2);
+ affirm( SQLITE_ROW==rc );
+ byte[] colBa = sqlite3_column_text(stmt2, 0);
+ affirm( 0==colBa.length );
+ colBa = sqlite3_column_text(stmt2, 1);
+ affirm( null==colBa );
+ //sqlite3_finalize(stmt);
+ }
+
+ if(true){
+ sqlite3_close_v2(db);
+ }else{
+ // Let the Object.finalize() override deal with it.
+ }
}
private void testBindFetchBlob(){
@@ -486,6 +575,79 @@ public class Tester1 implements Runnable {
sqlite3_close_v2(db);
}
+ @RequiresJniNio
+ private void testBindByteBuffer(){
+ /* TODO: these tests need to be much more extensive to check the
+ begin/end range handling. */
+
+ java.nio.ByteBuffer zeroCheck =
+ java.nio.ByteBuffer.allocateDirect(0);
+ affirm( null != zeroCheck );
+ zeroCheck = null;
+ sqlite3 db = createNewDb();
+ execSql(db, "CREATE TABLE t(a)");
+
+ final java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocateDirect(10);
+ buf.put((byte)0x31)/*note that we'll skip this one*/
+ .put((byte)0x32)
+ .put((byte)0x33)
+ .put((byte)0x34)
+ .put((byte)0x35)/*we'll skip this one too*/;
+
+ final int expectTotal = buf.get(1) + buf.get(2) + buf.get(3);
+ sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+ affirm( SQLITE_ERROR == sqlite3_bind_blob(stmt, 1, buf, -1, 0),
+ "Buffer offset may not be negative." );
+ affirm( 0 == sqlite3_bind_blob(stmt, 1, buf, 1, 3) );
+ affirm( SQLITE_DONE == sqlite3_step(stmt) );
+ sqlite3_finalize(stmt);
+ stmt = prepare(db, "SELECT a FROM t;");
+ int total = 0;
+ affirm( SQLITE_ROW == sqlite3_step(stmt) );
+ byte blob[] = sqlite3_column_blob(stmt, 0);
+ java.nio.ByteBuffer nioBlob =
+ sqlite3_column_nio_buffer(stmt, 0);
+ affirm(3 == blob.length);
+ affirm(blob.length == nioBlob.capacity());
+ affirm(blob.length == nioBlob.limit());
+ int i = 0;
+ for(byte b : blob){
+ affirm( i<=3 );
+ affirm(b == buf.get(1 + i));
+ affirm(b == nioBlob.get(i));
+ ++i;
+ total += b;
+ }
+ affirm( SQLITE_DONE == sqlite3_step(stmt) );
+ sqlite3_finalize(stmt);
+ affirm(total == expectTotal);
+
+ SQLFunction func =
+ new ScalarFunction(){
+ public void xFunc(sqlite3_context cx, sqlite3_value[] args){
+ sqlite3_result_blob(cx, buf, 1, 3);
+ }
+ };
+
+ affirm( 0 == sqlite3_create_function(db, "myfunc", -1, SQLITE_UTF8, func) );
+ stmt = prepare(db, "SELECT myfunc()");
+ affirm( SQLITE_ROW == sqlite3_step(stmt) );
+ blob = sqlite3_column_blob(stmt, 0);
+ affirm(3 == blob.length);
+ i = 0;
+ total = 0;
+ for(byte b : blob){
+ affirm( i<=3 );
+ affirm(b == buf.get(1 + i++));
+ total += b;
+ }
+ affirm( SQLITE_DONE == sqlite3_step(stmt) );
+ sqlite3_finalize(stmt);
+ affirm(total == expectTotal);
+
+ sqlite3_close_v2(db);
+ }
+
private void testSql(){
sqlite3 db = createNewDb();
sqlite3_stmt stmt = prepare(db, "SELECT 1");
@@ -493,7 +655,10 @@ public class Tester1 implements Runnable {
sqlite3_finalize(stmt);
stmt = prepare(db, "SELECT ?");
sqlite3_bind_text(stmt, 1, "hell😃");
- affirm( "SELECT 'hell😃'".equals(sqlite3_expanded_sql(stmt)) );
+ final String expect = "SELECT 'hell😃'";
+ affirm( expect.equals(sqlite3_expanded_sql(stmt)) );
+ String n = sqlite3_normalized_sql(stmt);
+ affirm( null==n || "SELECT?;".equals(n) );
sqlite3_finalize(stmt);
sqlite3_close(db);
}
@@ -528,9 +693,9 @@ public class Tester1 implements Runnable {
};
final CollationNeededCallback collLoader = new CollationNeededCallback(){
@Override
- public int call(sqlite3 dbArg, int eTextRep, String collationName){
+ public void call(sqlite3 dbArg, int eTextRep, String collationName){
affirm(dbArg == db/* as opposed to a temporary object*/);
- return sqlite3_create_collation(dbArg, "reversi", eTextRep, myCollation);
+ sqlite3_create_collation(dbArg, "reversi", eTextRep, myCollation);
}
};
int rc = sqlite3_collation_needed(db, collLoader);
@@ -573,7 +738,7 @@ public class Tester1 implements Runnable {
affirm( 1 == xDestroyCalled.value );
}
- @ManualTest /* because threading is meaningless here */
+ @SingleThreadOnly /* because it's thread-agnostic */
private void testToUtf8(){
/**
https://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html
@@ -618,6 +783,8 @@ public class Tester1 implements Runnable {
// These ValueHolders are just to confirm that the func did what we want...
final ValueHolder xDestroyCalled = new ValueHolder<>(false);
final ValueHolder xFuncAccum = new ValueHolder<>(0);
+ final ValueHolder neverEverDoThisInClientCode = new ValueHolder<>(null);
+ final ValueHolder neverEverDoThisInClientCode2 = new ValueHolder<>(null);
// Create an SQLFunction instance using one of its 3 subclasses:
// Scalar, Aggregate, or Window:
@@ -628,6 +795,15 @@ public class Tester1 implements Runnable {
new ScalarFunction(){
public void xFunc(sqlite3_context cx, sqlite3_value[] args){
affirm(db == sqlite3_context_db_handle(cx));
+ if( null==neverEverDoThisInClientCode.value ){
+ /* !!!NEVER!!! hold a reference to an sqlite3_value or
+ sqlite3_context object like this in client code! They
+ are ONLY legal for the duration of their single
+ call. We do it here ONLY to test that the defenses
+ against clients doing this are working. */
+ neverEverDoThisInClientCode2.value = cx;
+ neverEverDoThisInClientCode.value = args;
+ }
int result = 0;
for( sqlite3_value v : args ) result += sqlite3_value_int(v);
xFuncAccum.value += result;// just for post-run testing
@@ -653,6 +829,13 @@ public class Tester1 implements Runnable {
affirm(1 == n);
affirm(6 == xFuncAccum.value);
affirm( !xDestroyCalled.value );
+ affirm( null!=neverEverDoThisInClientCode.value );
+ affirm( null!=neverEverDoThisInClientCode2.value );
+ affirm( 0(false);
+ final ValueHolder neverEverDoThisInClientCode = new ValueHolder<>(null);
+ final ValueHolder neverEverDoThisInClientCode2 = new ValueHolder<>(null);
SQLFunction func = new AggregateFunction(){
@Override
public void xStep(sqlite3_context cx, sqlite3_value[] args){
+ if( null==neverEverDoThisInClientCode.value ){
+ /* !!!NEVER!!! hold a reference to an sqlite3_value or
+ sqlite3_context object like this in client code! They
+ are ONLY legal for the duration of their single
+ call. We do it here ONLY to test that the defenses
+ against clients doing this are working. */
+ neverEverDoThisInClientCode.value = args;
+ }
final ValueHolder 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){
+ if( null==neverEverDoThisInClientCode2.value ){
+ neverEverDoThisInClientCode2.value = cx;
+ }
final Integer v = this.takeAggregateState(cx);
if(null == v){
xFinalNull.value = true;
@@ -762,7 +963,7 @@ 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 );
+ affirm( 0==sqlite3_stmt_status(stmt, SQLITE_STMTSTATUS_RUN, false) );
int n = 0;
if( SQLITE_ROW == sqlite3_step(stmt) ){
int v = sqlite3_column_int(stmt, 0);
@@ -773,7 +974,12 @@ public class Tester1 implements Runnable {
}
affirm( 1==n );
affirm(!xFinalNull.value);
+ affirm( null!=neverEverDoThisInClientCode.value );
+ affirm( null!=neverEverDoThisInClientCode2.value );
+ affirm( 0 funcList = new java.util.ArrayList<>();
for(java.lang.reflect.Method m : declaredMethods){
if((m.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0){
@@ -940,48 +1145,51 @@ public class Tester1 implements Runnable {
affirm( 7 == counter.value );
}
- @ManualTest /* because threads inherently break this test */
- private void testBusy(){
+ @SingleThreadOnly /* because threads inherently break this test */
+ private static void testBusy(){
final String dbName = "_busy-handler.db";
- final OutputPointer.sqlite3 outDb = new OutputPointer.sqlite3();
- final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
-
- int rc = sqlite3_open(dbName, outDb);
- ++metrics.dbOpen;
- affirm( 0 == rc );
- final sqlite3 db1 = outDb.get();
- execSql(db1, "CREATE TABLE IF NOT EXISTS t(a)");
- rc = sqlite3_open(dbName, outDb);
- ++metrics.dbOpen;
- affirm( 0 == rc );
- affirm( outDb.get() != db1 );
- final sqlite3 db2 = outDb.get();
- rc = sqlite3_db_config(db1, SQLITE_DBCONFIG_MAINDBNAME, "foo");
- affirm( sqlite3_db_filename(db1, "foo").endsWith(dbName) );
-
- final ValueHolder xBusyCalled = new ValueHolder<>(0);
- BusyHandlerCallback handler = new BusyHandlerCallback(){
- @Override public int call(int n){
- //outln("busy handler #"+n);
- return n > 2 ? 0 : ++xBusyCalled.value;
- }
- };
- rc = sqlite3_busy_handler(db2, handler);
- affirm(0 == rc);
-
- // Force a locked condition...
- execSql(db1, "BEGIN EXCLUSIVE");
- rc = sqlite3_prepare_v2(db2, "SELECT * from t", outStmt);
- affirm( SQLITE_BUSY == rc);
- assert( null == outStmt.get() );
- affirm( 3 == xBusyCalled.value );
- sqlite3_close_v2(db1);
- sqlite3_close_v2(db2);
try{
- final java.io.File f = new java.io.File(dbName);
- f.delete();
- }catch(Exception e){
- /* ignore */
+ final OutputPointer.sqlite3 outDb = new OutputPointer.sqlite3();
+ final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
+
+ int rc = sqlite3_open(dbName, outDb);
+ ++metrics.dbOpen;
+ affirm( 0 == rc );
+ final sqlite3 db1 = outDb.get();
+ execSql(db1, "CREATE TABLE IF NOT EXISTS t(a)");
+ rc = sqlite3_open(dbName, outDb);
+ ++metrics.dbOpen;
+ affirm( 0 == rc );
+ affirm( outDb.get() != db1 );
+ final sqlite3 db2 = outDb.get();
+
+ affirm( "main".equals( sqlite3_db_name(db1, 0) ) );
+ rc = sqlite3_db_config(db1, SQLITE_DBCONFIG_MAINDBNAME, "foo");
+ affirm( sqlite3_db_filename(db1, "foo").endsWith(dbName) );
+ affirm( "foo".equals( sqlite3_db_name(db1, 0) ) );
+ affirm( SQLITE_MISUSE == sqlite3_db_config(db1, 0, 0, null) );
+
+ final ValueHolder xBusyCalled = new ValueHolder<>(0);
+ BusyHandlerCallback handler = new BusyHandlerCallback(){
+ @Override public int call(int n){
+ //outln("busy handler #"+n);
+ return n > 2 ? 0 : ++xBusyCalled.value;
+ }
+ };
+ rc = sqlite3_busy_handler(db2, handler);
+ affirm(0 == rc);
+
+ // Force a locked condition...
+ execSql(db1, "BEGIN EXCLUSIVE");
+ rc = sqlite3_prepare_v2(db2, "SELECT * from t", outStmt);
+ affirm( SQLITE_BUSY == rc);
+ affirm( null == outStmt.get() );
+ affirm( 3 == xBusyCalled.value );
+ sqlite3_close_v2(db1);
+ sqlite3_close_v2(db2);
+ }finally{
+ try{(new java.io.File(dbName)).delete();}
+ catch(Exception e){/* ignore */}
}
}
@@ -1005,6 +1213,7 @@ public class Tester1 implements Runnable {
private void testCommitHook(){
final sqlite3 db = createNewDb();
+ sqlite3_extended_result_codes(db, true);
final ValueHolder counter = new ValueHolder<>(0);
final ValueHolder hookResult = new ValueHolder<>(0);
final CommitHookCallback theHook = new CommitHookCallback(){
@@ -1047,7 +1256,7 @@ public class Tester1 implements Runnable {
affirm( 5 == counter.value );
hookResult.value = SQLITE_ERROR;
int rc = execSql(db, false, "BEGIN; update t set a='j' where a='i'; COMMIT;");
- affirm( SQLITE_CONSTRAINT == rc );
+ affirm( SQLITE_CONSTRAINT_COMMITHOOK == rc );
affirm( 6 == counter.value );
sqlite3_close_v2(db);
}
@@ -1224,10 +1433,13 @@ public class Tester1 implements Runnable {
}
Exception err = null;
try {
- Class t = Class.forName("org.sqlite.jni.TesterFts5");
+ Class t = Class.forName("org.sqlite.jni.fts5.TesterFts5");
java.lang.reflect.Constructor ctor = t.getConstructor();
ctor.setAccessible(true);
+ final long timeStart = System.currentTimeMillis();
ctor.newInstance() /* will run all tests */;
+ final long timeEnd = System.currentTimeMillis();
+ outln("FTS5 Tests done in ",(timeEnd - timeStart),"ms");
}catch(ClassNotFoundException e){
outln("FTS5 classes not loaded.");
err = e;
@@ -1263,6 +1475,9 @@ public class Tester1 implements Runnable {
authRc.value = SQLITE_DENY;
int rc = execSql(db, false, "UPDATE t SET a=2");
affirm( SQLITE_AUTH==rc );
+ sqlite3_set_authorizer(db, null);
+ rc = execSql(db, false, "UPDATE t SET a=2");
+ affirm( 0==rc );
// TODO: expand these tests considerably
sqlite3_close(db);
}
@@ -1273,7 +1488,7 @@ public class Tester1 implements Runnable {
final ValueHolder val = new ValueHolder<>(0);
final ValueHolder toss = new ValueHolder<>(null);
final AutoExtensionCallback ax = new AutoExtensionCallback(){
- @Override public synchronized int call(sqlite3 db){
+ @Override public int call(sqlite3 db){
++val.value;
if( null!=toss.value ){
throw new RuntimeException(toss.value);
@@ -1300,7 +1515,7 @@ public class Tester1 implements Runnable {
sqlite3 db = createNewDb();
affirm( 4==val.value );
- execSql(db, "ATTACH ':memory' as foo");
+ execSql(db, "ATTACH ':memory:' as foo");
affirm( 4==val.value, "ATTACH uses the same connection, not sub-connections." );
sqlite3_close(db);
db = null;
@@ -1324,7 +1539,7 @@ public class Tester1 implements Runnable {
val.value = 0;
final AutoExtensionCallback ax2 = new AutoExtensionCallback(){
- @Override public synchronized int call(sqlite3 db){
+ @Override public int call(sqlite3 db){
++val.value;
return 0;
}
@@ -1359,7 +1574,317 @@ public class Tester1 implements Runnable {
affirm( 8 == val.value );
}
- @ManualTest /* we really only want to run this test manually. */
+
+ private void testColumnMetadata(){
+ final sqlite3 db = createNewDb();
+ execSql(db, new String[] {
+ "CREATE TABLE t(a duck primary key not null collate noCase); ",
+ "INSERT INTO t(a) VALUES(1),(2),(3);"
+ });
+ OutputPointer.Bool bNotNull = new OutputPointer.Bool();
+ OutputPointer.Bool bPrimaryKey = new OutputPointer.Bool();
+ OutputPointer.Bool bAutoinc = new OutputPointer.Bool();
+ OutputPointer.String zCollSeq = new OutputPointer.String();
+ OutputPointer.String zDataType = new OutputPointer.String();
+ int rc = sqlite3_table_column_metadata(
+ db, "main", "t", "a", zDataType, zCollSeq,
+ bNotNull, bPrimaryKey, bAutoinc);
+ affirm( 0==rc );
+ affirm( bPrimaryKey.value );
+ affirm( !bAutoinc.value );
+ affirm( bNotNull.value );
+ affirm( "noCase".equals(zCollSeq.value) );
+ affirm( "duck".equals(zDataType.value) );
+
+ TableColumnMetadata m =
+ sqlite3_table_column_metadata(db, "main", "t", "a");
+ affirm( null != m );
+ affirm( bPrimaryKey.value == m.isPrimaryKey() );
+ affirm( bAutoinc.value == m.isAutoincrement() );
+ affirm( bNotNull.value == m.isNotNull() );
+ affirm( zCollSeq.value.equals(m.getCollation()) );
+ affirm( zDataType.value.equals(m.getDataType()) );
+
+ affirm( null == sqlite3_table_column_metadata(db, "nope", "t", "a") );
+ affirm( null == sqlite3_table_column_metadata(db, "main", "nope", "a") );
+
+ m = sqlite3_table_column_metadata(db, "main", "t", null)
+ /* Check only for existence of table */;
+ affirm( null != m );
+ affirm( m.isPrimaryKey() );
+ affirm( !m.isAutoincrement() );
+ affirm( !m.isNotNull() );
+ affirm( "BINARY".equalsIgnoreCase(m.getCollation()) );
+ affirm( "INTEGER".equalsIgnoreCase(m.getDataType()) );
+
+ sqlite3_close_v2(db);
+ }
+
+ private void testTxnState(){
+ final sqlite3 db = createNewDb();
+ affirm( SQLITE_TXN_NONE == sqlite3_txn_state(db, null) );
+ affirm( sqlite3_get_autocommit(db) );
+ execSql(db, "BEGIN;");
+ affirm( !sqlite3_get_autocommit(db) );
+ affirm( SQLITE_TXN_NONE == sqlite3_txn_state(db, null) );
+ execSql(db, "SELECT * FROM sqlite_schema;");
+ affirm( SQLITE_TXN_READ == sqlite3_txn_state(db, "main") );
+ execSql(db, "CREATE TABLE t(a);");
+ affirm( SQLITE_TXN_WRITE == sqlite3_txn_state(db, null) );
+ execSql(db, "ROLLBACK;");
+ affirm( SQLITE_TXN_NONE == sqlite3_txn_state(db, null) );
+ sqlite3_close_v2(db);
+ }
+
+
+ private void testExplain(){
+ final sqlite3 db = createNewDb();
+ sqlite3_stmt stmt = prepare(db,"SELECT 1");
+
+ affirm( 0 == sqlite3_stmt_isexplain(stmt) );
+ int rc = sqlite3_stmt_explain(stmt, 1);
+ affirm( 1 == sqlite3_stmt_isexplain(stmt) );
+ rc = sqlite3_stmt_explain(stmt, 2);
+ affirm( 2 == sqlite3_stmt_isexplain(stmt) );
+ sqlite3_finalize(stmt);
+ sqlite3_close_v2(db);
+ }
+
+ private void testLimit(){
+ final sqlite3 db = createNewDb();
+ int v;
+
+ v = sqlite3_limit(db, SQLITE_LIMIT_LENGTH, -1);
+ affirm( v > 0 );
+ affirm( v == sqlite3_limit(db, SQLITE_LIMIT_LENGTH, v-1) );
+ affirm( v-1 == sqlite3_limit(db, SQLITE_LIMIT_LENGTH, -1) );
+ sqlite3_close_v2(db);
+ }
+
+ private void testComplete(){
+ affirm( 0==sqlite3_complete("select 1") );
+ affirm( 0!=sqlite3_complete("select 1;") );
+ affirm( 0!=sqlite3_complete("nope 'nope' 'nope' 1;"), "Yup" );
+ }
+
+ private void testKeyword(){
+ final int n = sqlite3_keyword_count();
+ affirm( n>0 );
+ affirm( !sqlite3_keyword_check("_nope_") );
+ affirm( sqlite3_keyword_check("seLect") );
+ affirm( null!=sqlite3_keyword_name(0) );
+ affirm( null!=sqlite3_keyword_name(n-1) );
+ affirm( null==sqlite3_keyword_name(n) );
+ }
+
+ private void testBackup(){
+ final sqlite3 dbDest = createNewDb();
+
+ try (sqlite3 dbSrc = createNewDb()) {
+ execSql(dbSrc, new String[]{
+ "pragma page_size=512; VACUUM;",
+ "create table t(a);",
+ "insert into t(a) values(1),(2),(3);"
+ });
+ affirm( null==sqlite3_backup_init(dbSrc,"main",dbSrc,"main") );
+ try (sqlite3_backup b = sqlite3_backup_init(dbDest,"main",dbSrc,"main")) {
+ affirm( null!=b );
+ affirm( b.getNativePointer()!=0 );
+ int rc;
+ while( SQLITE_DONE!=(rc = sqlite3_backup_step(b, 1)) ){
+ affirm( 0==rc );
+ }
+ affirm( sqlite3_backup_pagecount(b) > 0 );
+ rc = sqlite3_backup_finish(b);
+ affirm( 0==rc );
+ affirm( b.getNativePointer()==0 );
+ }
+ }
+
+ try (sqlite3_stmt stmt = prepare(dbDest,"SELECT sum(a) from t")) {
+ sqlite3_step(stmt);
+ affirm( sqlite3_column_int(stmt,0) == 6 );
+ }
+ sqlite3_close_v2(dbDest);
+ }
+
+ private void testRandomness(){
+ byte[] foo = new byte[20];
+ int i = 0;
+ for( byte b : foo ){
+ i += b;
+ }
+ affirm( i==0 );
+ sqlite3_randomness(foo);
+ for( byte b : foo ){
+ if(b!=0) ++i;
+ }
+ affirm( i!=0, "There's a very slight chance that 0 is actually correct." );
+ }
+
+ private void testBlobOpen(){
+ final sqlite3 db = createNewDb();
+
+ execSql(db, "CREATE TABLE T(a BLOB);"
+ +"INSERT INTO t(rowid,a) VALUES(1, 'def'),(2, 'XYZ');"
+ );
+ final OutputPointer.sqlite3_blob pOut = new OutputPointer.sqlite3_blob();
+ int rc = sqlite3_blob_open(db, "main", "t", "a",
+ sqlite3_last_insert_rowid(db), 1, pOut);
+ affirm( 0==rc );
+ sqlite3_blob b = pOut.take();
+ affirm( null!=b );
+ affirm( 0!=b.getNativePointer() );
+ affirm( 3==sqlite3_blob_bytes(b) );
+ rc = sqlite3_blob_write( b, new byte[] {100, 101, 102 /*"DEF"*/}, 0);
+ affirm( 0==rc );
+ rc = sqlite3_blob_close(b);
+ affirm( 0==rc );
+ rc = sqlite3_blob_close(b);
+ affirm( 0!=rc );
+ affirm( 0==b.getNativePointer() );
+ sqlite3_stmt stmt = prepare(db,"SELECT length(a), a FROM t ORDER BY a");
+ affirm( SQLITE_ROW == sqlite3_step(stmt) );
+ affirm( 3 == sqlite3_column_int(stmt,0) );
+ affirm( "def".equals(sqlite3_column_text16(stmt,1)) );
+ sqlite3_finalize(stmt);
+
+ b = sqlite3_blob_open(db, "main", "t", "a",
+ sqlite3_last_insert_rowid(db), 0);
+ affirm( null!=b );
+ rc = sqlite3_blob_reopen(b, 2);
+ affirm( 0==rc );
+ final byte[] tgt = new byte[3];
+ rc = sqlite3_blob_read(b, tgt, 0);
+ affirm( 0==rc );
+ affirm( 100==tgt[0] && 101==tgt[1] && 102==tgt[2], "DEF" );
+ rc = sqlite3_blob_close(b);
+ affirm( 0==rc );
+
+ if( !sqlite3_jni_supports_nio() ){
+ outln("WARNING: skipping tests for ByteBuffer-using sqlite3_blob APIs ",
+ "because this platform lacks that support.");
+ sqlite3_close_v2(db);
+ return;
+ }
+ /* Sanity checks for the java.nio.ByteBuffer-taking overloads of
+ sqlite3_blob_read/write(). */
+ execSql(db, "UPDATE t SET a=zeroblob(10)");
+ b = sqlite3_blob_open(db, "main", "t", "a", 1, 1);
+ affirm( null!=b );
+ java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocateDirect(10);
+ for( byte i = 0; i < 10; ++i ){
+ bb.put((int)i, (byte)(48+i & 0xff));
+ }
+ rc = sqlite3_blob_write(b, 1, bb, 1, 10);
+ affirm( rc==SQLITE_ERROR, "b length < (srcOffset + bb length)" );
+ rc = sqlite3_blob_write(b, -1, bb);
+ affirm( rc==SQLITE_ERROR, "Target offset may not be negative" );
+ rc = sqlite3_blob_write(b, 0, bb, -1, -1);
+ affirm( rc==SQLITE_ERROR, "Source offset may not be negative" );
+ rc = sqlite3_blob_write(b, 1, bb, 1, 8);
+ affirm( rc==0 );
+ // b's contents: 0 49 50 51 52 53 54 55 56 0
+ // ascii: 0 '1' '2' '3' '4' '5' '6' '7' '8' 0
+ byte br[] = new byte[10];
+ java.nio.ByteBuffer bbr =
+ java.nio.ByteBuffer.allocateDirect(bb.limit());
+ rc = sqlite3_blob_read( b, br, 0 );
+ affirm( rc==0 );
+ rc = sqlite3_blob_read( b, bbr );
+ affirm( rc==0 );
+ java.nio.ByteBuffer bbr2 = sqlite3_blob_read_nio_buffer(b, 0, 12);
+ affirm( null==bbr2, "Read size is too big");
+ bbr2 = sqlite3_blob_read_nio_buffer(b, -1, 3);
+ affirm( null==bbr2, "Source offset is negative");
+ bbr2 = sqlite3_blob_read_nio_buffer(b, 5, 6);
+ affirm( null==bbr2, "Read pos+size is too big");
+ bbr2 = sqlite3_blob_read_nio_buffer(b, 4, 7);
+ affirm( null==bbr2, "Read pos+size is too big");
+ bbr2 = sqlite3_blob_read_nio_buffer(b, 4, 6);
+ affirm( null!=bbr2 );
+ java.nio.ByteBuffer bbr3 =
+ java.nio.ByteBuffer.allocateDirect(2 * bb.limit());
+ java.nio.ByteBuffer bbr4 =
+ java.nio.ByteBuffer.allocateDirect(5);
+ rc = sqlite3_blob_read( b, bbr3 );
+ affirm( rc==0 );
+ rc = sqlite3_blob_read( b, bbr4 );
+ affirm( rc==0 );
+ affirm( sqlite3_blob_bytes(b)==bbr3.limit() );
+ affirm( 5==bbr4.limit() );
+ sqlite3_blob_close(b);
+ affirm( 0==br[0] );
+ affirm( 0==br[9] );
+ affirm( 0==bbr.get(0) );
+ affirm( 0==bbr.get(9) );
+ affirm( bbr2.limit() == 6 );
+ affirm( 0==bbr3.get(0) );
+ {
+ Exception ex = null;
+ try{ bbr3.get(11); }
+ catch(Exception e){ex = e;}
+ affirm( ex instanceof IndexOutOfBoundsException,
+ "bbr3.limit() was reset by read()" );
+ ex = null;
+ }
+ affirm( 0==bbr4.get(0) );
+ for( int i = 1; i < 9; ++i ){
+ affirm( br[i] == 48 + i );
+ affirm( br[i] == bbr.get(i) );
+ affirm( br[i] == bbr3.get(i) );
+ if( i>3 ){
+ affirm( br[i] == bbr2.get(i-4) );
+ }
+ if( i < bbr4.limit() ){
+ affirm( br[i] == bbr4.get(i) );
+ }
+ }
+ sqlite3_close_v2(db);
+ }
+
+ private void testPrepareMulti(){
+ final sqlite3 db = createNewDb();
+ final String[] sql = {
+ "create table t(","a)",
+ "; insert into t(a) values(1),(2),(3);",
+ "select a from t;"
+ };
+ final List liStmt = new ArrayList();
+ final PrepareMultiCallback proxy = new PrepareMultiCallback.StepAll();
+ final ValueHolder toss = new ValueHolder<>(null);
+ PrepareMultiCallback m = new PrepareMultiCallback() {
+ @Override public int call(sqlite3_stmt st){
+ liStmt.add(st);
+ if( null!=toss.value ){
+ throw new RuntimeException(toss.value);
+ }
+ return proxy.call(st);
+ }
+ };
+ int rc = sqlite3_prepare_multi(db, sql, m);
+ affirm( 0==rc );
+ affirm( liStmt.size() == 3 );
+ for( sqlite3_stmt st : liStmt ){
+ sqlite3_finalize(st);
+ }
+ toss.value = "This is an exception.";
+ rc = sqlite3_prepare_multi(db, "SELECT 1", m);
+ affirm( SQLITE_ERROR==rc );
+ affirm( sqlite3_errmsg(db).indexOf(toss.value)>0 );
+ sqlite3_close_v2(db);
+ }
+
+ /* Copy/paste/rename this to add new tests. */
+ private void _testTemplate(){
+ final sqlite3 db = createNewDb();
+ sqlite3_stmt stmt = prepare(db,"SELECT 1");
+ sqlite3_finalize(stmt);
+ sqlite3_close_v2(db);
+ }
+
+
+ @ManualTest /* we really only want to run this test manually */
private void testSleep(){
out("Sleeping briefly... ");
sqlite3_sleep(600);
@@ -1385,7 +1910,7 @@ public class Tester1 implements Runnable {
mlist = new ArrayList<>( testMethods.subList(0, testMethods.size()) );
java.util.Collections.shuffle(mlist);
}
- if( listRunTests ){
+ if( (!fromThread && listRunTests>0) || listRunTests>1 ){
synchronized(this.getClass()){
if( !fromThread ){
out("Initial test"," list: ");
@@ -1402,8 +1927,6 @@ public class Tester1 implements Runnable {
outln();
}
}
- testToUtf8();
- test1();
for(java.lang.reflect.Method m : mlist){
nap();
try{
@@ -1413,9 +1936,6 @@ public class Tester1 implements Runnable {
throw e;
}
}
- if( !fromThread ){
- testBusy();
- }
synchronized( this.getClass() ){
++nTestRuns;
}
@@ -1452,8 +1972,11 @@ public class Tester1 implements Runnable {
-naps: sleep small random intervals between tests in order to add
some chaos for cross-thread contention.
+
-list-tests: outputs the list of tests being run, minus some
- which are hard-coded. This is noisy in multi-threaded mode.
+ which are hard-coded. In multi-threaded mode, use this twice to
+ to emit the list run by each thread (which may differ from the initial
+ list, in particular if -shuffle is used).
-fail: forces an exception to be thrown during the test run. Use
with -shuffle to make its appearance unpredictable.
@@ -1466,6 +1989,7 @@ public class Tester1 implements Runnable {
Integer nRepeat = 1;
boolean forceFail = false;
boolean sqlLog = false;
+ boolean configLog = false;
boolean squelchTestOutput = false;
for( int i = 0; i < args.length; ){
String arg = args[i++];
@@ -1481,11 +2005,13 @@ public class Tester1 implements Runnable {
}else if(arg.equals("shuffle")){
shuffle = true;
}else if(arg.equals("list-tests")){
- listRunTests = true;
+ ++listRunTests;
}else if(arg.equals("fail")){
forceFail = true;
}else if(arg.equals("sqllog")){
sqlLog = true;
+ }else if(arg.equals("configlog")){
+ configLog = true;
}else if(arg.equals("naps")){
takeNaps = true;
}else if(arg.equals("q") || arg.equals("quiet")){
@@ -1498,7 +2024,7 @@ public class Tester1 implements Runnable {
if( sqlLog ){
if( sqlite3_compileoption_used("ENABLE_SQLLOG") ){
- final ConfigSqllogCallback log = new ConfigSqllogCallback() {
+ final ConfigSqlLogCallback log = new ConfigSqlLogCallback() {
@Override public void call(sqlite3 db, String msg, int op){
switch(op){
case 0: outln("Opening db: ",db); break;
@@ -1509,7 +2035,7 @@ public class Tester1 implements Runnable {
};
int rc = sqlite3_config( log );
affirm( 0==rc );
- rc = sqlite3_config( null );
+ rc = sqlite3_config( (ConfigSqlLogCallback)null );
affirm( 0==rc );
rc = sqlite3_config( log );
affirm( 0==rc );
@@ -1518,6 +2044,19 @@ public class Tester1 implements Runnable {
"without SQLITE_ENABLE_SQLLOG.");
}
}
+ if( configLog ){
+ final ConfigLogCallback log = new ConfigLogCallback() {
+ @Override public void call(int code, String msg){
+ outln("ConfigLogCallback: ",ResultCode.getEntryForInt(code),": ", msg);
+ };
+ };
+ int rc = sqlite3_config( log );
+ affirm( 0==rc );
+ rc = sqlite3_config( (ConfigLogCallback)null );
+ affirm( 0==rc );
+ rc = sqlite3_config( log );
+ affirm( 0==rc );
+ }
quietMode = squelchTestOutput;
outln("If you just saw warning messages regarding CallStaticObjectMethod, ",
@@ -1527,27 +2066,32 @@ public class Tester1 implements Runnable {
{
// Build list of tests to run from the methods named test*().
testMethods = new ArrayList<>();
- out("Skipping tests in multi-thread mode:");
+ int nSkipped = 0;
for(final java.lang.reflect.Method m : Tester1.class.getDeclaredMethods()){
final String name = m.getName();
if( name.equals("testFail") ){
if( forceFail ){
testMethods.add(m);
}
+ }else if( m.isAnnotationPresent( RequiresJniNio.class )
+ && !sqlite3_jni_supports_nio() ){
+ outln("Skipping test for lack of JNI java.nio.ByteBuffer support: ",
+ name,"()\n");
+ ++nSkipped;
}else if( !m.isAnnotationPresent( ManualTest.class ) ){
if( nThread>1 && m.isAnnotationPresent( SingleThreadOnly.class ) ){
- out(" "+name+"()");
+ out("Skipping test in multi-thread mode: ",name,"()\n");
+ ++nSkipped;
}else if( name.startsWith("test") ){
testMethods.add(m);
}
}
}
- out("\n");
}
final long timeStart = System.currentTimeMillis();
int nLoop = 0;
- switch( SQLITE_THREADSAFE ){ /* Sanity checking */
+ switch( sqlite3_threadsafe() ){ /* Sanity checking */
case 0:
affirm( SQLITE_ERROR==sqlite3_config( SQLITE_CONFIG_SINGLETHREAD ),
"Could not switch to single-thread mode." );
@@ -1573,19 +2117,22 @@ public class Tester1 implements Runnable {
outln("libversion_number: ",
sqlite3_libversion_number(),"\n",
sqlite3_libversion(),"\n",SQLITE_SOURCE_ID,"\n",
- "SQLITE_THREADSAFE=",SQLITE_THREADSAFE);
- outln("Running ",nRepeat," loop(s) with ",nThread," thread(s) each.");
+ "SQLITE_THREADSAFE=",sqlite3_threadsafe());
+ outln("JVM NIO support? ",sqlite3_jni_supports_nio() ? "YES" : "NO");
+ final boolean showLoopCount = (nRepeat>1 && nThread>1);
+ if( showLoopCount ){
+ outln("Running ",nRepeat," loop(s) with ",nThread," thread(s) each.");
+ }
if( takeNaps ) outln("Napping between tests is enabled.");
for( int n = 0; n < nRepeat; ++n ){
++nLoop;
- out((1==nLoop ? "" : " ")+nLoop);
+ if( showLoopCount ) out((1==nLoop ? "" : " ")+nLoop);
if( nThread<=1 ){
new Tester1(0).runTests(false);
continue;
}
Tester1.mtMode = true;
final ExecutorService ex = Executors.newFixedThreadPool( nThread );
- //final List> futures = new ArrayList<>();
for( int i = 0; i < nThread; ++i ){
ex.submit( new Tester1(i), i );
}
@@ -1608,7 +2155,7 @@ public class Tester1 implements Runnable {
if( null!=err ) throw err;
}
}
- outln();
+ if( showLoopCount ) outln();
quietMode = false;
final long timeEnd = System.currentTimeMillis();
@@ -1618,13 +2165,14 @@ public class Tester1 implements Runnable {
if( doSomethingForDev ){
sqlite3_jni_internal_details();
}
+ affirm( 0==sqlite3_release_memory(1) );
sqlite3_shutdown();
int nMethods = 0;
int nNatives = 0;
final java.lang.reflect.Method[] declaredMethods =
- SQLite3Jni.class.getDeclaredMethods();
+ CApi.class.getDeclaredMethods();
for(java.lang.reflect.Method m : declaredMethods){
- int mod = m.getModifiers();
+ final int mod = m.getModifiers();
if( 0!=(mod & java.lang.reflect.Modifier.STATIC) ){
final String name = m.getName();
if(name.startsWith("sqlite3_")){
@@ -1635,9 +2183,11 @@ public class Tester1 implements Runnable {
}
}
}
- outln("\tSQLite3Jni.sqlite3_*() methods: "+
- nNatives+" native methods and "+
- (nMethods - nNatives)+" Java impls");
+ outln("\tCApi.sqlite3_*() methods: "+
+ nMethods+" total, with "+
+ nNatives+" native, "+
+ (nMethods - nNatives)+" Java"
+ );
outln("\tTotal test time = "
+(timeEnd - timeStart)+"ms");
}
diff --git a/ext/jni/src/org/sqlite/jni/TraceV2Callback.java b/ext/jni/src/org/sqlite/jni/capi/TraceV2Callback.java
similarity index 91%
rename from ext/jni/src/org/sqlite/jni/TraceV2Callback.java
rename to ext/jni/src/org/sqlite/jni/capi/TraceV2Callback.java
index 897aeefa9f..56465a2c0a 100644
--- a/ext/jni/src/org/sqlite/jni/TraceV2Callback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/TraceV2Callback.java
@@ -11,13 +11,13 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
import org.sqlite.jni.annotation.Nullable;
/**
- Callback for use with {@link SQLite3Jni#sqlite3_trace_v2}.
+ Callback for use with {@link CApi#sqlite3_trace_v2}.
*/
-public interface TraceV2Callback extends SQLite3CallbackProxy {
+public interface TraceV2Callback extends CallbackProxy {
/**
Called by sqlite3 for various tracing operations, as per
sqlite3_trace_v2(). Note that this interface elides the 2nd
diff --git a/ext/jni/src/org/sqlite/jni/UpdateHookCallback.java b/ext/jni/src/org/sqlite/jni/capi/UpdateHookCallback.java
similarity index 71%
rename from ext/jni/src/org/sqlite/jni/UpdateHookCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/UpdateHookCallback.java
index 4fd0a63240..e3d491f67e 100644
--- a/ext/jni/src/org/sqlite/jni/UpdateHookCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/UpdateHookCallback.java
@@ -11,15 +11,16 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
- Callback for use with {@link SQLite3Jni#sqlite3_update_hook}.
+ Callback for use with {@link CApi#sqlite3_update_hook}.
*/
-public interface UpdateHookCallback extends SQLite3CallbackProxy {
+public interface UpdateHookCallback extends CallbackProxy {
/**
Must function as described for the C-level sqlite3_update_hook()
- callback.
+ callback. If it throws, the exception is translated into
+ a db-level error.
*/
void call(int opId, String dbName, String tableName, long rowId);
}
diff --git a/ext/jni/src/org/sqlite/jni/capi/ValueHolder.java b/ext/jni/src/org/sqlite/jni/capi/ValueHolder.java
new file mode 100644
index 0000000000..0a469fea9a
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/capi/ValueHolder.java
@@ -0,0 +1,27 @@
+/*
+** 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 the ValueHolder utility class 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, as well as communicating aggregate
+ SQL function state across calls to such functions.
+*/
+public class ValueHolder {
+ public T value;
+ public ValueHolder(){}
+ public ValueHolder(T v){value = v;}
+}
diff --git a/ext/jni/src/org/sqlite/jni/WindowFunction.java b/ext/jni/src/org/sqlite/jni/capi/WindowFunction.java
similarity index 97%
rename from ext/jni/src/org/sqlite/jni/WindowFunction.java
rename to ext/jni/src/org/sqlite/jni/capi/WindowFunction.java
index 7f70177ac0..eaf1bb9a35 100644
--- a/ext/jni/src/org/sqlite/jni/WindowFunction.java
+++ b/ext/jni/src/org/sqlite/jni/capi/WindowFunction.java
@@ -11,7 +11,7 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
diff --git a/ext/jni/src/org/sqlite/jni/XDestroyCallback.java b/ext/jni/src/org/sqlite/jni/capi/XDestroyCallback.java
similarity index 97%
rename from ext/jni/src/org/sqlite/jni/XDestroyCallback.java
rename to ext/jni/src/org/sqlite/jni/capi/XDestroyCallback.java
index 4b547e6bc9..372e4ec8d0 100644
--- a/ext/jni/src/org/sqlite/jni/XDestroyCallback.java
+++ b/ext/jni/src/org/sqlite/jni/capi/XDestroyCallback.java
@@ -11,7 +11,7 @@
*************************************************************************
** This file declares JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
Callback for a hook called by SQLite when certain client-provided
diff --git a/ext/jni/src/org/sqlite/jni/capi/package-info.java b/ext/jni/src/org/sqlite/jni/capi/package-info.java
new file mode 100644
index 0000000000..127f380675
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/capi/package-info.java
@@ -0,0 +1,89 @@
+/**
+ This package houses a JNI binding to the SQLite3 C API.
+
+ The primary interfaces are in {@link
+ org.sqlite.jni.capi.CApi}.
+
+ API Goals and Requirements
+
+
+
+ - A 1-to-1(-ish) mapping of the C API to Java via JNI, insofar
+ as cross-language semantics allow for. A closely-related goal is
+ that the C
+ documentation should be usable as-is, insofar as possible,
+ for most of the JNI binding. As a rule, undocumented symbols in
+ the Java interface behave as documented for their C API
+ counterpart. Only semantic differences and Java-specific features
+ are documented here.
+
+ - Support Java as far back as version 8 (2014).
+
+ - Environment-independent. Should work everywhere both Java and
+ SQLite3 do.
+
+ - No 3rd-party dependencies beyond the JDK. That includes no
+ build-level dependencies for specific IDEs and toolchains. We
+ welcome the addition of build files for arbitrary environments
+ insofar as they neither interfere with each other nor become a
+ maintenance burden for the sqlite developers.
+
+
+
+ Non-Goals
+
+
+
+ - Creation of high-level OO wrapper APIs. Clients are free to
+ create them off of the C-style API.
+
+ - Support for mixed-mode operation, where client code accesses
+ SQLite both via the Java-side API and the C API via their own
+ native code. In such cases, proxy functionalities (primarily
+ callback handler wrappers of all sorts) may fail because the
+ C-side use of the SQLite APIs will bypass those proxies.
+
+
+
+ State of this API
+
+ As of version 3.43, this software is in "tech preview" form. We
+ tentatively plan to stamp it as stable with the 3.44 release.
+
+ Threading Considerations
+
+ This API is, if built with SQLITE_THREADSAFE set to 1 or 2,
+ thread-safe, insofar as the C API guarantees, with some addenda:
+
+
+
+ - It is not legal to use Java-facing SQLite3 resource handles
+ (sqlite3, sqlite3_stmt, etc) from multiple threads concurrently,
+ nor to use any database-specific resources concurrently in a
+ thread separate from the one the database is currently in use
+ in. i.e. do not use a sqlite3_stmt in thread #2 when thread #1 is
+ using the database which prepared that handle.
+
+
Violating this will eventually corrupt the JNI-level bindings
+ between Java's and C's view of the database. This is a limitation
+ of the JNI bindings, not the lower-level library.
+
+
+ - It is legal to use a given handle, and database-specific
+ resources, across threads, so long as no two threads pass
+ resources owned by the same database into the library
+ concurrently.
+
+
+
+
+ Any number of threads may, of course, create and use any number
+ of database handles they wish. Care only needs to be taken when
+ those handles or their associated resources cross threads, or...
+
+ When built with SQLITE_THREADSAFE=0 then no threading guarantees
+ are provided and multi-threaded use of the library will provoke
+ undefined behavior.
+
+*/
+package org.sqlite.jni.capi;
diff --git a/ext/jni/src/org/sqlite/jni/sqlite3.java b/ext/jni/src/org/sqlite/jni/capi/sqlite3.java
similarity index 76%
rename from ext/jni/src/org/sqlite/jni/sqlite3.java
rename to ext/jni/src/org/sqlite/jni/capi/sqlite3.java
index cfc6c08d47..cc6f2e6e8d 100644
--- a/ext/jni/src/org/sqlite/jni/sqlite3.java
+++ b/ext/jni/src/org/sqlite/jni/capi/sqlite3.java
@@ -11,7 +11,7 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
A wrapper for communicating C-level (sqlite3*) instances with
@@ -19,19 +19,25 @@ package org.sqlite.jni;
simply provide a type-safe way to communicate it between Java
and C via JNI.
*/
-public final class sqlite3 extends NativePointerHolder {
+public final class sqlite3 extends NativePointerHolder
+ implements AutoCloseable {
+
// Only invoked from JNI
private sqlite3(){}
public String toString(){
- long ptr = getNativePointer();
+ final long ptr = getNativePointer();
if( 0==ptr ){
return sqlite3.class.getSimpleName()+"@null";
}
- String fn = SQLite3Jni.sqlite3_db_filename(this, "main");
+ final String fn = CApi.sqlite3_db_filename(this, "main");
return sqlite3.class.getSimpleName()
+"@"+String.format("0x%08x",ptr)
+"["+((null == fn) ? "" : fn)+"]"
;
}
+
+ @Override public void close(){
+ CApi.sqlite3_close_v2(this);
+ }
}
diff --git a/ext/jni/src/org/sqlite/jni/capi/sqlite3_backup.java b/ext/jni/src/org/sqlite/jni/capi/sqlite3_backup.java
new file mode 100644
index 0000000000..0ef75c17eb
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/capi/sqlite3_backup.java
@@ -0,0 +1,31 @@
+/*
+** 2023-09-03
+**
+** 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 JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+ A wrapper for passing C-level (sqlite3_backup*) instances around in
+ Java. These wrappers do not own their associated pointer, they
+ simply provide a type-safe way to communicate it between Java and C
+ via JNI.
+*/
+public final class sqlite3_backup extends NativePointerHolder
+ implements AutoCloseable {
+ // Only invoked from JNI.
+ private sqlite3_backup(){}
+
+ @Override public void close(){
+ CApi.sqlite3_backup_finish(this);
+ }
+
+}
diff --git a/ext/jni/src/org/sqlite/jni/capi/sqlite3_blob.java b/ext/jni/src/org/sqlite/jni/capi/sqlite3_blob.java
new file mode 100644
index 0000000000..bdc0200af4
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/capi/sqlite3_blob.java
@@ -0,0 +1,30 @@
+/*
+** 2023-09-03
+**
+** 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 JNI bindings for the sqlite3 C API.
+*/
+package org.sqlite.jni.capi;
+
+/**
+ A wrapper for passing C-level (sqlite3_blob*) instances around in
+ Java. These wrappers do not own their associated pointer, they
+ simply provide a type-safe way to communicate it between Java and C
+ via JNI.
+*/
+public final class sqlite3_blob extends NativePointerHolder
+ implements AutoCloseable {
+ // Only invoked from JNI.
+ private sqlite3_blob(){}
+
+ @Override public void close(){
+ CApi.sqlite3_blob_close(this);
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/sqlite3_context.java b/ext/jni/src/org/sqlite/jni/capi/sqlite3_context.java
similarity index 96%
rename from ext/jni/src/org/sqlite/jni/sqlite3_context.java
rename to ext/jni/src/org/sqlite/jni/capi/sqlite3_context.java
index b9f11d7336..82ec49af16 100644
--- a/ext/jni/src/org/sqlite/jni/sqlite3_context.java
+++ b/ext/jni/src/org/sqlite/jni/capi/sqlite3_context.java
@@ -11,7 +11,7 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
sqlite3_context instances are used in conjunction with user-defined
@@ -71,7 +71,7 @@ public final class sqlite3_context extends NativePointerHolder
*/
public synchronized Long getAggregateContext(boolean initIfNeeded){
if( aggregateContext==null ){
- aggregateContext = SQLite3Jni.sqlite3_aggregate_context(this, initIfNeeded);
+ aggregateContext = CApi.sqlite3_aggregate_context(this, initIfNeeded);
if( !initIfNeeded && null==aggregateContext ) aggregateContext = 0L;
}
return (null==aggregateContext || 0!=aggregateContext) ? aggregateContext : null;
diff --git a/ext/jni/src/org/sqlite/jni/sqlite3_stmt.java b/ext/jni/src/org/sqlite/jni/capi/sqlite3_stmt.java
similarity index 84%
rename from ext/jni/src/org/sqlite/jni/sqlite3_stmt.java
rename to ext/jni/src/org/sqlite/jni/capi/sqlite3_stmt.java
index d672301378..564891c727 100644
--- a/ext/jni/src/org/sqlite/jni/sqlite3_stmt.java
+++ b/ext/jni/src/org/sqlite/jni/capi/sqlite3_stmt.java
@@ -11,7 +11,7 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
/**
A wrapper for communicating C-level (sqlite3_stmt*) instances with
@@ -19,7 +19,12 @@ package org.sqlite.jni;
simply provide a type-safe way to communicate it between Java and C
via JNI.
*/
-public final class sqlite3_stmt extends NativePointerHolder {
+public final class sqlite3_stmt extends NativePointerHolder
+ implements AutoCloseable {
// Only invoked from JNI.
private sqlite3_stmt(){}
+
+ @Override public void close(){
+ CApi.sqlite3_finalize(this);
+ }
}
diff --git a/ext/jni/src/org/sqlite/jni/sqlite3_value.java b/ext/jni/src/org/sqlite/jni/capi/sqlite3_value.java
similarity index 95%
rename from ext/jni/src/org/sqlite/jni/sqlite3_value.java
rename to ext/jni/src/org/sqlite/jni/capi/sqlite3_value.java
index 2cfb32ff1a..a4772f0f63 100644
--- a/ext/jni/src/org/sqlite/jni/sqlite3_value.java
+++ b/ext/jni/src/org/sqlite/jni/capi/sqlite3_value.java
@@ -11,7 +11,7 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.capi;
public final class sqlite3_value extends NativePointerHolder {
//! Invoked only from JNI.
diff --git a/ext/jni/src/org/sqlite/jni/Fts5.java b/ext/jni/src/org/sqlite/jni/fts5/Fts5.java
similarity index 67%
rename from ext/jni/src/org/sqlite/jni/Fts5.java
rename to ext/jni/src/org/sqlite/jni/fts5/Fts5.java
index 3135db96d3..0dceeafd2e 100644
--- a/ext/jni/src/org/sqlite/jni/Fts5.java
+++ b/ext/jni/src/org/sqlite/jni/fts5/Fts5.java
@@ -11,26 +11,18 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.fts5;
/**
INCOMPLETE AND COMPLETELY UNTESTED.
- A wrapper for communicating C-level (fts5_api*) instances with
- Java. These wrappers do not own their associated pointer, they
- simply provide a type-safe way to communicate it between Java and C
- via JNI.
+ A utility object for holding FTS5-specific types and constants
+ which are used by multiple FTS5 classes.
*/
public final class Fts5 {
/* Not used */
private Fts5(){}
- /**
- Callback type for use with xTokenize() variants
- */
- public static interface xTokenize_callback {
- int call(int tFlags, byte[] txt, int iStart, int iEnd);
- }
public static final int FTS5_TOKENIZE_QUERY = 0x0001;
public static final int FTS5_TOKENIZE_PREFIX = 0x0002;
diff --git a/ext/jni/src/org/sqlite/jni/Fts5Context.java b/ext/jni/src/org/sqlite/jni/fts5/Fts5Context.java
similarity index 92%
rename from ext/jni/src/org/sqlite/jni/Fts5Context.java
rename to ext/jni/src/org/sqlite/jni/fts5/Fts5Context.java
index e78f67d556..439b477910 100644
--- a/ext/jni/src/org/sqlite/jni/Fts5Context.java
+++ b/ext/jni/src/org/sqlite/jni/fts5/Fts5Context.java
@@ -11,7 +11,8 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.fts5;
+import org.sqlite.jni.capi.*;
/**
A wrapper for communicating C-level (Fts5Context*) instances with
diff --git a/ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java b/ext/jni/src/org/sqlite/jni/fts5/Fts5ExtensionApi.java
similarity index 90%
rename from ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java
rename to ext/jni/src/org/sqlite/jni/fts5/Fts5ExtensionApi.java
index ab2995f378..594f3eaad6 100644
--- a/ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java
+++ b/ext/jni/src/org/sqlite/jni/fts5/Fts5ExtensionApi.java
@@ -11,15 +11,12 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.fts5;
import java.nio.charset.StandardCharsets;
+import org.sqlite.jni.capi.*;
import org.sqlite.jni.annotation.*;
/**
- ALMOST COMPLETELY UNTESTED.
-
- FAR FROM COMPLETE and the feasibility of binding this to Java
- is still undetermined. This might be removed.
*/
public final class Fts5ExtensionApi extends NativePointerHolder {
//! Only called from JNI
@@ -27,8 +24,8 @@ public final class Fts5ExtensionApi extends NativePointerHolder aOut = new ArrayList();
+
+ /* Iterate through the list of SQL statements. For each, step through
+ ** it and add any results to the aOut[] array. */
+ int rc = sqlite3_prepare_multi(db, sql, new PrepareMultiCallback() {
+ @Override public int call(sqlite3_stmt pStmt){
+ while( SQLITE_ROW==sqlite3_step(pStmt) ){
+ int ii;
+ for(ii=0; ii, );
+ */
+ class fts5_aux implements fts5_extension_function {
+ @Override public void call(
+ Fts5ExtensionApi ext,
+ Fts5Context fCx,
+ sqlite3_context pCx,
+ sqlite3_value argv[]
+ ){
+ if( argv.length>1 ){
+ throw new RuntimeException("fts5_aux: wrong number of args");
+ }
+
+ boolean bClear = (argv.length==1);
+ Object obj = ext.xGetAuxdata(fCx, bClear);
+ if( obj instanceof String ){
+ sqlite3_result_text16(pCx, (String)obj);
+ }
+
+ if( argv.length==1 ){
+ String val = sqlite3_value_text16(argv[0]);
+ if( !val.equals("") ){
+ ext.xSetAuxdata(fCx, val);
+ }
+ }
+ }
+ public void xDestroy(){ }
+ };
+
+ /*
+ ** fts5_inst();
+ **
+ ** This is used to test the xInstCount() and xInst() APIs. It returns a
+ ** text value containing a Tcl list with xInstCount() elements. Each
+ ** element is itself a list of 3 integers - the phrase number, column
+ ** number and token offset returned by each call to xInst().
+ */
+ fts5_extension_function fts5_inst = new fts5_extension_function(){
+ @Override public void call(
+ Fts5ExtensionApi ext,
+ Fts5Context fCx,
+ sqlite3_context pCx,
+ sqlite3_value argv[]
+ ){
+ if( argv.length!=0 ){
+ throw new RuntimeException("fts5_inst: wrong number of args");
+ }
+
+ OutputPointer.Int32 pnInst = new OutputPointer.Int32();
+ OutputPointer.Int32 piPhrase = new OutputPointer.Int32();
+ OutputPointer.Int32 piCol = new OutputPointer.Int32();
+ OutputPointer.Int32 piOff = new OutputPointer.Int32();
+ String ret = new String();
+
+ int rc = ext.xInstCount(fCx, pnInst);
+ int nInst = pnInst.get();
+ int ii;
+
+ for(ii=0; rc==SQLITE_OK && ii0 ) ret += " ";
+ ret += "{"+piPhrase.get()+" "+piCol.get()+" "+piOff.get()+"}";
+ }
+
+ sqlite3_result_text(pCx, ret);
+ }
+ public void xDestroy(){ }
+ };
+
+ /*
+ ** fts5_pinst();
+ **
+ ** Like SQL function fts5_inst(), except using the following
+ **
+ ** xPhraseCount
+ ** xPhraseFirst
+ ** xPhraseNext
+ */
+ fts5_extension_function fts5_pinst = new fts5_extension_function(){
+ @Override public void call(
+ Fts5ExtensionApi ext,
+ Fts5Context fCx,
+ sqlite3_context pCx,
+ sqlite3_value argv[]
+ ){
+ if( argv.length!=0 ){
+ throw new RuntimeException("fts5_pinst: wrong number of args");
+ }
+
+ OutputPointer.Int32 piCol = new OutputPointer.Int32();
+ OutputPointer.Int32 piOff = new OutputPointer.Int32();
+ String ret = new String();
+ int rc = SQLITE_OK;
+
+ int nPhrase = ext.xPhraseCount(fCx);
+ int ii;
+
+ for(ii=0; rc==SQLITE_OK && ii=0;
+ ext.xPhraseNext(fCx, pIter, piCol, piOff)
+ ){
+ if( !ret.equals("") ) ret += " ";
+ ret += "{"+ii+" "+piCol.get()+" "+piOff.get()+"}";
+ }
+ }
+
+ if( rc!=SQLITE_OK ){
+ throw new RuntimeException("fts5_pinst: rc=" + rc);
+ }else{
+ sqlite3_result_text(pCx, ret);
+ }
+ }
+ public void xDestroy(){ }
+ };
+
+ /*
+ ** fts5_pcolinst();
+ **
+ ** Like SQL function fts5_pinst(), except using the following
+ **
+ ** xPhraseFirstColumn
+ ** xPhraseNextColumn
+ */
+ fts5_extension_function fts5_pcolinst = new fts5_extension_function(){
+ @Override public void call(
+ Fts5ExtensionApi ext,
+ Fts5Context fCx,
+ sqlite3_context pCx,
+ sqlite3_value argv[]
+ ){
+ if( argv.length!=0 ){
+ throw new RuntimeException("fts5_pcolinst: wrong number of args");
+ }
+
+ OutputPointer.Int32 piCol = new OutputPointer.Int32();
+ String ret = new String();
+ int rc = SQLITE_OK;
+
+ int nPhrase = ext.xPhraseCount(fCx);
+ int ii;
+
+ for(ii=0; rc==SQLITE_OK && ii=0;
+ ext.xPhraseNextColumn(fCx, pIter, piCol)
+ ){
+ if( !ret.equals("") ) ret += " ";
+ ret += "{"+ii+" "+piCol.get()+"}";
+ }
+ }
+
+ if( rc!=SQLITE_OK ){
+ throw new RuntimeException("fts5_pcolinst: rc=" + rc);
+ }else{
+ sqlite3_result_text(pCx, ret);
+ }
+ }
+ public void xDestroy(){ }
+ };
+
+ /*
+ ** fts5_rowcount();
+ */
+ fts5_extension_function fts5_rowcount = new fts5_extension_function(){
+ @Override public void call(
+ Fts5ExtensionApi ext,
+ Fts5Context fCx,
+ sqlite3_context pCx,
+ sqlite3_value argv[]
+ ){
+ if( argv.length!=0 ){
+ throw new RuntimeException("fts5_rowcount: wrong number of args");
+ }
+ OutputPointer.Int64 pnRow = new OutputPointer.Int64();
+
+ int rc = ext.xRowCount(fCx, pnRow);
+ if( rc==SQLITE_OK ){
+ sqlite3_result_int64(pCx, pnRow.get());
+ }else{
+ throw new RuntimeException("fts5_rowcount: rc=" + rc);
+ }
+ }
+ public void xDestroy(){ }
+ };
+
+ /*
+ ** fts5_phrasesize();
+ */
+ fts5_extension_function fts5_phrasesize = new fts5_extension_function(){
+ @Override public void call(
+ Fts5ExtensionApi ext,
+ Fts5Context fCx,
+ sqlite3_context pCx,
+ sqlite3_value argv[]
+ ){
+ if( argv.length!=1 ){
+ throw new RuntimeException("fts5_phrasesize: wrong number of args");
+ }
+ int iPhrase = sqlite3_value_int(argv[0]);
+
+ int sz = ext.xPhraseSize(fCx, iPhrase);
+ sqlite3_result_int(pCx, sz);
+ }
+ public void xDestroy(){ }
+ };
+
+ /*
+ ** fts5_phrasehits(, );
+ **
+ ** Use the xQueryPhrase() API to determine how many hits, in total,
+ ** there are for phrase in the database.
+ */
+ fts5_extension_function fts5_phrasehits = new fts5_extension_function(){
+ @Override public void call(
+ Fts5ExtensionApi ext,
+ Fts5Context fCx,
+ sqlite3_context pCx,
+ sqlite3_value argv[]
+ ){
+ if( argv.length!=1 ){
+ throw new RuntimeException("fts5_phrasesize: wrong number of args");
+ }
+ int iPhrase = sqlite3_value_int(argv[0]);
+ int rc = SQLITE_OK;
+
+ class MyCallback implements Fts5ExtensionApi.XQueryPhraseCallback {
+ public int nRet = 0;
+ public int getRet() { return nRet; }
+
+ @Override
+ public int call(Fts5ExtensionApi fapi, Fts5Context cx){
+ OutputPointer.Int32 pnInst = new OutputPointer.Int32();
+ int rc = fapi.xInstCount(cx, pnInst);
+ nRet += pnInst.get();
+ return rc;
+ }
+ };
+
+ MyCallback xCall = new MyCallback();
+ rc = ext.xQueryPhrase(fCx, iPhrase, xCall);
+ if( rc!=SQLITE_OK ){
+ throw new RuntimeException("fts5_phrasehits: rc=" + rc);
+ }
+ sqlite3_result_int(pCx, xCall.getRet());
+ }
+ public void xDestroy(){ }
+ };
+
+ /*
+ ** fts5_tokenize(, )
+ */
+ fts5_extension_function fts5_tokenize = new fts5_extension_function(){
+ @Override public void call(
+ Fts5ExtensionApi ext,
+ Fts5Context fCx,
+ sqlite3_context pCx,
+ sqlite3_value argv[]
+ ){
+ if( argv.length!=1 ){
+ throw new RuntimeException("fts5_tokenize: wrong number of args");
+ }
+ byte[] utf8 = sqlite3_value_text(argv[0]);
+ int rc = SQLITE_OK;
+
+ class MyCallback implements XTokenizeCallback {
+ private List myList = new ArrayList();
+
+ public String getval() {
+ return String.join("+", myList);
+ }
+
+ @Override
+ public int call(int tFlags, byte[] txt, int iStart, int iEnd){
+ try {
+ String str = new String(txt, "UTF-8");
+ myList.add(str);
+ } catch (Exception e) {
+ }
+ return SQLITE_OK;
+ }
+ };
+
+ MyCallback xCall = new MyCallback();
+ ext.xTokenize(fCx, utf8, xCall);
+ sqlite3_result_text16(pCx, xCall.getval());
+
+ if( rc!=SQLITE_OK ){
+ throw new RuntimeException("fts5_tokenize: rc=" + rc);
+ }
+ }
+ public void xDestroy(){ }
+ };
+
+ fts5_api api = fts5_api.getInstanceForDb(db);
+ api.xCreateFunction("fts5_rowid", fts5_rowid);
+ api.xCreateFunction("fts5_columncount", fts5_columncount);
+ api.xCreateFunction("fts5_columnsize", fts5_columnsize);
+ api.xCreateFunction("fts5_columntext", fts5_columntext);
+ api.xCreateFunction("fts5_columntotalsize", fts5_columntsize);
+
+ api.xCreateFunction("fts5_aux1", new fts5_aux());
+ api.xCreateFunction("fts5_aux2", new fts5_aux());
+
+ api.xCreateFunction("fts5_inst", fts5_inst);
+ api.xCreateFunction("fts5_pinst", fts5_pinst);
+ api.xCreateFunction("fts5_pcolinst", fts5_pcolinst);
+ api.xCreateFunction("fts5_rowcount", fts5_rowcount);
+ api.xCreateFunction("fts5_phrasesize", fts5_phrasesize);
+ api.xCreateFunction("fts5_phrasehits", fts5_phrasehits);
+ api.xCreateFunction("fts5_tokenize", fts5_tokenize);
+ }
+ /*
+ ** Test of various Fts5ExtensionApi methods
+ */
+ private static void test2(){
+
+ /* Open db and populate an fts5 table */
+ sqlite3 db = createNewDb();
+ do_execsql_test(db,
+ "CREATE VIRTUAL TABLE ft USING fts5(a, b);" +
+ "INSERT INTO ft(rowid, a, b) VALUES(-9223372036854775808, 'x', 'x');" +
+ "INSERT INTO ft(rowid, a, b) VALUES(0, 'x', 'x');" +
+ "INSERT INTO ft(rowid, a, b) VALUES(1, 'x y z', 'x y z');" +
+ "INSERT INTO ft(rowid, a, b) VALUES(2, 'x y z', 'x z');" +
+ "INSERT INTO ft(rowid, a, b) VALUES(3, 'x y z', 'x y z');" +
+ "INSERT INTO ft(rowid, a, b) VALUES(9223372036854775807, 'x', 'x');"
+ );
+
+ create_test_functions(db);
+
+ /* Test that fts5_rowid() seems to work */
+ do_execsql_test(db,
+ "SELECT rowid==fts5_rowid(ft) FROM ft('x')",
+ "[1, 1, 1, 1, 1, 1]"
+ );
+
+ /* Test fts5_columncount() */
+ do_execsql_test(db,
+ "SELECT fts5_columncount(ft) FROM ft('x')",
+ "[2, 2, 2, 2, 2, 2]"
+ );
+
+ /* Test fts5_columnsize() */
+ do_execsql_test(db,
+ "SELECT fts5_columnsize(ft, 0) FROM ft('x') ORDER BY rowid",
+ "[1, 1, 3, 3, 3, 1]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_columnsize(ft, 1) FROM ft('x') ORDER BY rowid",
+ "[1, 1, 3, 2, 3, 1]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_columnsize(ft, -1) FROM ft('x') ORDER BY rowid",
+ "[2, 2, 6, 5, 6, 2]"
+ );
+
+ /* Test that xColumnSize() returns SQLITE_RANGE if the column number
+ ** is out-of range */
+ try {
+ do_execsql_test(db,
+ "SELECT fts5_columnsize(ft, 2) FROM ft('x') ORDER BY rowid"
+ );
+ } catch( RuntimeException e ){
+ affirm( e.getMessage().matches(".*column index out of range") );
+ }
+
+ /* Test fts5_columntext() */
+ do_execsql_test(db,
+ "SELECT fts5_columntext(ft, 0) FROM ft('x') ORDER BY rowid",
+ "[x, x, x y z, x y z, x y z, x]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_columntext(ft, 1) FROM ft('x') ORDER BY rowid",
+ "[x, x, x y z, x z, x y z, x]"
+ );
+ boolean threw = false;
+ try{
+ /* columntext() used to return NULLs when given an out-of bounds column
+ but now results in a range error. */
+ do_execsql_test(db,
+ "SELECT fts5_columntext(ft, 2) FROM ft('x') ORDER BY rowid",
+ "[null, null, null, null, null, null]"
+ );
+ }catch(Exception e){
+ threw = true;
+ affirm( e.getMessage().matches(".*column index out of range") );
+ }
+ affirm( threw );
+ threw = false;
+
+ /* Test fts5_columntotalsize() */
+ do_execsql_test(db,
+ "SELECT fts5_columntotalsize(ft, 0) FROM ft('x') ORDER BY rowid",
+ "[12, 12, 12, 12, 12, 12]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_columntotalsize(ft, 1) FROM ft('x') ORDER BY rowid",
+ "[11, 11, 11, 11, 11, 11]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_columntotalsize(ft, -1) FROM ft('x') ORDER BY rowid",
+ "[23, 23, 23, 23, 23, 23]"
+ );
+
+ /* Test that xColumnTotalSize() returns SQLITE_RANGE if the column
+ ** number is out-of range */
+ try {
+ do_execsql_test(db,
+ "SELECT fts5_columntotalsize(ft, 2) FROM ft('x') ORDER BY rowid"
+ );
+ } catch( RuntimeException e ){
+ affirm( e.getMessage().matches(".*column index out of range") );
+ }
+
+ do_execsql_test(db,
+ "SELECT rowid, fts5_rowcount(ft) FROM ft('z')",
+ "[1, 6, 2, 6, 3, 6]"
+ );
+
+ sqlite3_close_v2(db);
+ }
+
+ /*
+ ** Test of various Fts5ExtensionApi methods
+ */
+ private static void test3(){
+
+ /* Open db and populate an fts5 table */
+ sqlite3 db = createNewDb();
+ do_execsql_test(db,
+ "CREATE VIRTUAL TABLE ft USING fts5(a, b);" +
+ "INSERT INTO ft(a, b) VALUES('the one', 1);" +
+ "INSERT INTO ft(a, b) VALUES('the two', 2);" +
+ "INSERT INTO ft(a, b) VALUES('the three', 3);" +
+ "INSERT INTO ft(a, b) VALUES('the four', '');"
+ );
+ create_test_functions(db);
+
+ /* Test fts5_aux1() + fts5_aux2() - users of xGetAuxdata and xSetAuxdata */
+ do_execsql_test(db,
+ "SELECT fts5_aux1(ft, a) FROM ft('the')",
+ "[null, the one, the two, the three]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_aux2(ft, b) FROM ft('the')",
+ "[null, 1, 2, 3]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_aux1(ft, a), fts5_aux2(ft, b) FROM ft('the')",
+ "[null, null, the one, 1, the two, 2, the three, 3]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_aux1(ft, b), fts5_aux1(ft) FROM ft('the')",
+ "[null, 1, 1, 2, 2, 3, 3, null]"
+ );
+ }
+
+ /*
+ ** Test of various Fts5ExtensionApi methods
+ */
+ private static void test4(){
+
+ /* Open db and populate an fts5 table */
+ sqlite3 db = createNewDb();
+ create_test_functions(db);
+ do_execsql_test(db,
+ "CREATE VIRTUAL TABLE ft USING fts5(a, b);" +
+ "INSERT INTO ft(a, b) VALUES('one two three', 'two three four');" +
+ "INSERT INTO ft(a, b) VALUES('two three four', 'three four five');" +
+ "INSERT INTO ft(a, b) VALUES('three four five', 'four five six');"
+ );
+
+
+ do_execsql_test(db,
+ "SELECT fts5_inst(ft) FROM ft('two')",
+ "[{0 0 1} {0 1 0}, {0 0 0}]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_inst(ft) FROM ft('four')",
+ "[{0 1 2}, {0 0 2} {0 1 1}, {0 0 1} {0 1 0}]"
+ );
+
+ do_execsql_test(db,
+ "SELECT fts5_inst(ft) FROM ft('a OR b OR four')",
+ "[{2 1 2}, {2 0 2} {2 1 1}, {2 0 1} {2 1 0}]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_inst(ft) FROM ft('two four')",
+ "[{0 0 1} {0 1 0} {1 1 2}, {0 0 0} {1 0 2} {1 1 1}]"
+ );
+
+ do_execsql_test(db,
+ "SELECT fts5_pinst(ft) FROM ft('two')",
+ "[{0 0 1} {0 1 0}, {0 0 0}]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_pinst(ft) FROM ft('four')",
+ "[{0 1 2}, {0 0 2} {0 1 1}, {0 0 1} {0 1 0}]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_pinst(ft) FROM ft('a OR b OR four')",
+ "[{2 1 2}, {2 0 2} {2 1 1}, {2 0 1} {2 1 0}]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_pinst(ft) FROM ft('two four')",
+ "[{0 0 1} {0 1 0} {1 1 2}, {0 0 0} {1 0 2} {1 1 1}]"
+ );
+
+ do_execsql_test(db,
+ "SELECT fts5_pcolinst(ft) FROM ft('two')",
+ "[{0 0} {0 1}, {0 0}]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_pcolinst(ft) FROM ft('four')",
+ "[{0 1}, {0 0} {0 1}, {0 0} {0 1}]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_pcolinst(ft) FROM ft('a OR b OR four')",
+ "[{2 1}, {2 0} {2 1}, {2 0} {2 1}]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_pcolinst(ft) FROM ft('two four')",
+ "[{0 0} {0 1} {1 1}, {0 0} {1 0} {1 1}]"
+ );
+
+ do_execsql_test(db,
+ "SELECT fts5_phrasesize(ft, 0) FROM ft('four five six') LIMIT 1;",
+ "[1]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_phrasesize(ft, 0) FROM ft('four + five + six') LIMIT 1;",
+ "[3]"
+ );
+
+
+ sqlite3_close_v2(db);
+ }
+
+ private static void test5(){
+ /* Open db and populate an fts5 table */
+ sqlite3 db = createNewDb();
+ create_test_functions(db);
+ do_execsql_test(db,
+ "CREATE VIRTUAL TABLE ft USING fts5(x, b);" +
+ "INSERT INTO ft(x) VALUES('one two three four five six seven eight');" +
+ "INSERT INTO ft(x) VALUES('one two one four one six one eight');" +
+ "INSERT INTO ft(x) VALUES('one two three four five six seven eight');"
+ );
+
+ do_execsql_test(db,
+ "SELECT fts5_phrasehits(ft, 0) FROM ft('one') LIMIT 1",
+ "[6]"
+ );
+
+ sqlite3_close_v2(db);
+ }
+
+ private static void test6(){
+ sqlite3 db = createNewDb();
+ create_test_functions(db);
+ do_execsql_test(db,
+ "CREATE VIRTUAL TABLE ft USING fts5(x, b);" +
+ "INSERT INTO ft(x) VALUES('one two three four five six seven eight');"
+ );
+
+ do_execsql_test(db,
+ "SELECT fts5_tokenize(ft, 'abc def ghi') FROM ft('one')",
+ "[abc+def+ghi]"
+ );
+ do_execsql_test(db,
+ "SELECT fts5_tokenize(ft, 'it''s BEEN a...') FROM ft('one')",
+ "[it+s+been+a]"
+ );
+
+ sqlite3_close_v2(db);
+ }
+
+ private static synchronized void runTests(){
+ test1();
+ test2();
+ test3();
+ test4();
+ test5();
+ test6();
+ }
+
+ public TesterFts5(){
+ runTests();
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/RollbackHookCallback.java b/ext/jni/src/org/sqlite/jni/fts5/XTokenizeCallback.java
similarity index 60%
rename from ext/jni/src/org/sqlite/jni/RollbackHookCallback.java
rename to ext/jni/src/org/sqlite/jni/fts5/XTokenizeCallback.java
index 3bf9f79a1a..3aa514f314 100644
--- a/ext/jni/src/org/sqlite/jni/RollbackHookCallback.java
+++ b/ext/jni/src/org/sqlite/jni/fts5/XTokenizeCallback.java
@@ -1,5 +1,5 @@
/*
-** 2023-08-25
+** 2023-08-04
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
@@ -11,15 +11,12 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.fts5;
+
/**
- Callback for use with {@link SQLite3Jni#sqlite3_rollback_hook}.
+ Callback type for use with xTokenize() variants.
*/
-public interface RollbackHookCallback extends SQLite3CallbackProxy {
- /**
- Works as documented for the C-level sqlite3_rollback_hook()
- callback.
- */
- void call();
+public interface XTokenizeCallback {
+ int call(int tFlags, byte[] txt, int iStart, int iEnd);
}
diff --git a/ext/jni/src/org/sqlite/jni/fts5_api.java b/ext/jni/src/org/sqlite/jni/fts5/fts5_api.java
similarity index 82%
rename from ext/jni/src/org/sqlite/jni/fts5_api.java
rename to ext/jni/src/org/sqlite/jni/fts5/fts5_api.java
index 92ca7c669a..d7d2da430d 100644
--- a/ext/jni/src/org/sqlite/jni/fts5_api.java
+++ b/ext/jni/src/org/sqlite/jni/fts5/fts5_api.java
@@ -11,12 +11,11 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.fts5;
import org.sqlite.jni.annotation.*;
+import org.sqlite.jni.capi.*;
/**
- INCOMPLETE AND COMPLETELY UNTESTED.
-
A wrapper for communicating C-level (fts5_api*) instances with
Java. These wrappers do not own their associated pointer, they
simply provide a type-safe way to communicate it between Java and C
@@ -25,7 +24,8 @@ import org.sqlite.jni.annotation.*;
public final class fts5_api extends NativePointerHolder {
/* Only invoked from JNI */
private fts5_api(){}
- public final int iVersion = 2;
+
+ public static final int iVersion = 2;
/**
Returns the fts5_api instance associated with the given db, or
@@ -33,6 +33,30 @@ public final class fts5_api extends NativePointerHolder {
*/
public static synchronized native fts5_api getInstanceForDb(@NotNull sqlite3 db);
+ public synchronized native int xCreateFunction(@NotNull String name,
+ @Nullable Object userData,
+ @NotNull fts5_extension_function xFunction);
+
+ /**
+ Convenience overload which passes null as the 2nd argument to the
+ 3-parameter form.
+ */
+ public int xCreateFunction(@NotNull String name,
+ @NotNull fts5_extension_function xFunction){
+ return xCreateFunction(name, null, xFunction);
+ }
+
+ // /* Create a new auxiliary function */
+ // int (*xCreateFunction)(
+ // fts5_api *pApi,
+ // const char *zName,
+ // void *pContext,
+ // fts5_extension_function xFunction,
+ // void (*xDestroy)(void*)
+ // );
+
+ // Still potentially todo:
+
// int (*xCreateTokenizer)(
// fts5_api *pApi,
// const char *zName,
@@ -49,22 +73,4 @@ public final class fts5_api extends NativePointerHolder {
// fts5_tokenizer *pTokenizer
// );
- // /* Create a new auxiliary function */
- // int (*xCreateFunction)(
- // fts5_api *pApi,
- // const char *zName,
- // void *pContext,
- // fts5_extension_function xFunction,
- // void (*xDestroy)(void*)
- // );
-
- public synchronized native int xCreateFunction(@NotNull String name,
- @Nullable Object userData,
- @NotNull fts5_extension_function xFunction);
-
- public int xCreateFunction(@NotNull String name,
- @NotNull fts5_extension_function xFunction){
- return xCreateFunction(name, null, xFunction);
- }
-
}
diff --git a/ext/jni/src/org/sqlite/jni/fts5_extension_function.java b/ext/jni/src/org/sqlite/jni/fts5/fts5_extension_function.java
similarity index 61%
rename from ext/jni/src/org/sqlite/jni/fts5_extension_function.java
rename to ext/jni/src/org/sqlite/jni/fts5/fts5_extension_function.java
index 0e273119f5..5e47633baa 100644
--- a/ext/jni/src/org/sqlite/jni/fts5_extension_function.java
+++ b/ext/jni/src/org/sqlite/jni/fts5/fts5_extension_function.java
@@ -11,13 +11,14 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.fts5;
+import org.sqlite.jni.capi.sqlite3_context;
+import org.sqlite.jni.capi.sqlite3_value;
/**
JNI-level wrapper for C's fts5_extension_function type.
-
*/
-public abstract class fts5_extension_function {
+public interface fts5_extension_function {
// typedef void (*fts5_extension_function)(
// const Fts5ExtensionApi *pApi, /* API offered by current FTS version */
// Fts5Context *pFts, /* First arg to pass to pApi functions */
@@ -30,8 +31,17 @@ public abstract class fts5_extension_function {
The callback implementation, corresponding to the xFunction
argument of C's fts5_api::xCreateFunction().
*/
- public abstract void xFunction(Fts5ExtensionApi ext, Fts5Context fCx,
- sqlite3_context pCx, sqlite3_value argv[]);
- //! Optionally override
- public void xDestroy(){}
+ void call(Fts5ExtensionApi ext, Fts5Context fCx,
+ sqlite3_context pCx, sqlite3_value argv[]);
+ /**
+ Is called when this function is destroyed by sqlite3. Typically
+ this function will be empty.
+ */
+ void xDestroy();
+
+ public static abstract class Abstract implements fts5_extension_function {
+ @Override public abstract void call(Fts5ExtensionApi ext, Fts5Context fCx,
+ sqlite3_context pCx, sqlite3_value argv[]);
+ @Override public void xDestroy(){}
+ }
}
diff --git a/ext/jni/src/org/sqlite/jni/fts5_tokenizer.java b/ext/jni/src/org/sqlite/jni/fts5/fts5_tokenizer.java
similarity index 90%
rename from ext/jni/src/org/sqlite/jni/fts5_tokenizer.java
rename to ext/jni/src/org/sqlite/jni/fts5/fts5_tokenizer.java
index 053434e266..f4ada4dc30 100644
--- a/ext/jni/src/org/sqlite/jni/fts5_tokenizer.java
+++ b/ext/jni/src/org/sqlite/jni/fts5/fts5_tokenizer.java
@@ -11,12 +11,11 @@
*************************************************************************
** This file is part of the JNI bindings for the sqlite3 C API.
*/
-package org.sqlite.jni;
-import org.sqlite.jni.annotation.*;
+package org.sqlite.jni.fts5;
+import org.sqlite.jni.capi.NativePointerHolder;
+import org.sqlite.jni.annotation.NotNull;
/**
- INCOMPLETE AND COMPLETELY UNTESTED.
-
A wrapper for communicating C-level (fts5_tokenizer*) instances with
Java. These wrappers do not own their associated pointer, they
simply provide a type-safe way to communicate it between Java and C
@@ -31,7 +30,7 @@ public final class fts5_tokenizer extends NativePointerHolder {
public native int xTokenize(@NotNull Fts5Tokenizer t, int tokFlags,
@NotNull byte pText[],
- @NotNull Fts5.xTokenize_callback callback);
+ @NotNull XTokenizeCallback callback);
// int (*xTokenize)(Fts5Tokenizer*,
diff --git a/ext/jni/src/org/sqlite/jni/package-info.java b/ext/jni/src/org/sqlite/jni/package-info.java
deleted file mode 100644
index 2ca997955a..0000000000
--- a/ext/jni/src/org/sqlite/jni/package-info.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- This package houses a JNI binding to the SQLite3 C API.
-
- The docs are in progress.
-
-
The primary interfaces are in {@link org.sqlite.jni.SQLite3Jni}.
-
-
State of this API
-
- As of version 3.43, this software is in "tech preview" form. We
- tentatively plan to stamp it as stable with the 3.44 release.
-
-
Threading Considerations
-
- This API is, if built with SQLITE_THREADSAFE set to 1 or 2,
- thread-safe, insofar as the C API guarantees, with some addenda:
-
-
-
- - It is not legal to use Java-facing SQLite3 resource handles
- (sqlite3, sqlite3_stmt, etc) from multiple threads concurrently,
- nor to use any database-specific resources concurrently in a
- thread separate from the one the database is currently in use
- in. i.e. do not use a sqlite3_stmt in thread #2 when thread #1 is
- using the database which prepared that handle.
-
-
Violating this will eventually corrupt the JNI-level bindings
- between Java's and C's view of the database. This is a limitation
- of the JNI bindings, not the lower-level library.
-
-
- - It is legal to use a given handle, and database-specific
- resources, across threads, so long as no two threads pass
- resources owned by the same database into the library
- concurrently.
-
-
-
-
- Any number of threads may, of course, create and use any number
- of database handles they wish. Care only needs to be taken when
- those handles or their associated resources cross threads, or...
-
-
When built with SQLITE_THREADSAFE=0 then no threading guarantees
- are provided and multi-threaded use of the library will provoke
- undefined behavior.
-
-*/
-package org.sqlite.jni;
diff --git a/ext/jni/src/org/sqlite/jni/tester/test-script-interpreter.md b/ext/jni/src/org/sqlite/jni/test-script-interpreter.md
similarity index 100%
rename from ext/jni/src/org/sqlite/jni/tester/test-script-interpreter.md
rename to ext/jni/src/org/sqlite/jni/test-script-interpreter.md
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java b/ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java
new file mode 100644
index 0000000000..fc63b53542
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java
@@ -0,0 +1,144 @@
+/*
+** 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;
+
+/**
+ 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.
+*/
+public abstract class AggregateFunction 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() {}
+
+ /**
+ PerContextState assists aggregate and window functions in
+ managing their accumulator state across calls to the UDF's
+ callbacks.
+
+ T must be of a type which can be legally stored as a value in
+ java.util.HashMap.
+
+ 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.
+
+
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.
+
+
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 {
+ private final java.util.Map> 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
+ which can be used to modify that state directly without
+ requiring that the client update the underlying map's entry.
+
+ The caller is obligated to eventually call
+ takeAggregateState() to clear the mapping.
+ */
+ public ValueHolder getAggregateState(SqlFunction.Arguments args, T initialValue){
+ final Long key = args.getContext().getAggregateContext(true);
+ ValueHolder 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 h = map.remove(args.getContext().getAggregateContext(false));
+ return null==h ? null : h.value;
+ }
+ }
+
+ /** Per-invocation state for the UDF. */
+ private final PerContextState map = new 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 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);
+ }
+
+}
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/ScalarFunction.java b/ext/jni/src/org/sqlite/jni/wrapper1/ScalarFunction.java
new file mode 100644
index 0000000000..067a6983eb
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/ScalarFunction.java
@@ -0,0 +1,37 @@
+/*
+** 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;
+
+/**
+ The SqlFunction type for scalar SQL functions.
+*/
+public abstract class ScalarFunction implements SqlFunction {
+ /**
+ As for the xFunc() argument of the C API's
+ sqlite3_create_function(). If this function throws, it is
+ translated into an sqlite3_result_error().
+ */
+ public abstract void xFunc(SqlFunction.Arguments args);
+
+ /**
+ Optionally override to be notified when the UDF is finalized by
+ SQLite. This default implementation does nothing.
+ */
+ public void xDestroy() {}
+
+}
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java b/ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java
new file mode 100644
index 0000000000..dcfc2ebebd
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java
@@ -0,0 +1,318 @@
+/*
+** 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.capi.sqlite3_context;
+import org.sqlite.jni.capi.sqlite3_value;
+
+/**
+ Base marker interface for SQLite's three types of User-Defined SQL
+ Functions (UDFs): Scalar, Aggregate, and Window functions.
+*/
+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 SUBTYPE = CApi.SQLITE_SUBTYPE;
+ public static final int RESULT_SUBTYPE = CApi.SQLITE_RESULT_SUBTYPE;
+ public static final int UTF8 = CApi.SQLITE_UTF8;
+ public static final int UTF16 = CApi.SQLITE_UTF16;
+
+ /**
+ 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{
+ private final sqlite3_context cx;
+ private final sqlite3_value args[];
+ public final int length;
+
+ /**
+ Must be passed the context and arguments for the UDF call this
+ 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(sqlite3_context cx, sqlite3_value args[]){
+ this.cx = cx;
+ this.args = args==null ? new sqlite3_value[0] : args;
+ this.length = this.args.length;
+ }
+
+ /**
+ Returns the sqlite3_value at the given argument index or throws
+ an IllegalArgumentException exception if ndx is out of range.
+ */
+ private sqlite3_value valueAt(int ndx){
+ if(ndx<0 || ndx>=args.length){
+ throw new IllegalArgumentException(
+ "SQL function argument index "+ndx+" is out of range."
+ );
+ }
+ return args[ndx];
+ }
+
+ //! Returns the underlying sqlite3_context for these arguments.
+ sqlite3_context getContext(){return cx;}
+
+ /**
+ Returns the Sqlite (db) object associated with this UDF call,
+ or null if the UDF is somehow called without such an object or
+ the db has been closed in an untimely manner (e.g. closed by a
+ UDF call).
+ */
+ public Sqlite getDb(){
+ return Sqlite.fromNative( CApi.sqlite3_context_db_handle(cx) );
+ }
+
+ public int getArgCount(){ return args.length; }
+
+ 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 getObject(int argNdx, Class type){
+ return CApi.sqlite3_value_java_object(valueAt(argNdx), type);
+ }
+
+ 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); }
+ public void resultDouble(double v){ CApi.sqlite3_result_double(cx, v); }
+ public void resultError(String msg){CApi.sqlite3_result_error(cx, msg);}
+ public void resultError(Exception e){CApi.sqlite3_result_error(cx, e);}
+ public void resultErrorTooBig(){CApi.sqlite3_result_error_toobig(cx);}
+ public void resultErrorCode(int rc){CApi.sqlite3_result_error_code(cx, rc);}
+ public void resultObject(Object o){CApi.sqlite3_result_java_object(cx, o);}
+ public void resultNull(){CApi.sqlite3_result_null(cx);}
+ /**
+ Analog to sqlite3_result_value(), using the Value object at the
+ given argument index.
+ */
+ 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.
+ CApi.sqlite3_result_zeroblob64(cx, n);
+ }
+
+ public void resultBlob(byte[] blob){CApi.sqlite3_result_blob(cx, blob);}
+ public void resultText(byte[] utf8){CApi.sqlite3_result_text(cx, utf8);}
+ public void resultText(String txt){CApi.sqlite3_result_text(cx, txt);}
+ public void resultText16(byte[] utf16){CApi.sqlite3_result_text16(cx, utf16);}
+ public void resultText16(String txt){CApi.sqlite3_result_text16(cx, txt);}
+
+ /**
+ Callbacks should invoke this on OOM errors, instead of throwing
+ OutOfMemoryError, because the latter cannot be propagated
+ through the C API.
+ */
+ public void resultNoMem(){CApi.sqlite3_result_error_nomem(cx);}
+
+ /**
+ Analog to sqlite3_set_auxdata() but throws if argNdx is out of
+ range.
+ */
+ 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(argNdx);
+ CApi.sqlite3_set_auxdata(cx, argNdx, o);
+ }
+
+ /**
+ Analog to sqlite3_get_auxdata() but throws if argNdx is out of
+ range.
+ */
+ public Object getAuxData(int argNdx){
+ valueAt(argNdx);
+ return CApi.sqlite3_get_auxdata(cx, argNdx);
+ }
+
+ /**
+ Represents 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 getObject(Class 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);}
+ }
+
+ @Override
+ public java.util.Iterator 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();
+ }
+
+ }
+
+ /**
+ Internal-use adapter for wrapping this package's ScalarFunction
+ for use with the org.sqlite.jni.capi.ScalarFunction interface.
+ */
+ static final class ScalarAdapter extends org.sqlite.jni.capi.ScalarFunction {
+ private final ScalarFunction impl;
+ ScalarAdapter(ScalarFunction impl){
+ this.impl = impl;
+ }
+ /**
+ 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){
+ 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 /*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;
+ }
+
+ /**
+ 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(){
+ impl.xDestroy();
+ }
+ }
+
+ /**
+ 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
new file mode 100644
index 0000000000..de131e8542
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java
@@ -0,0 +1,1991 @@
+/*
+** 2023-10-09
+**
+** 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 java.nio.charset.StandardCharsets;
+import org.sqlite.jni.capi.CApi;
+import org.sqlite.jni.capi.sqlite3;
+import org.sqlite.jni.capi.sqlite3_stmt;
+import org.sqlite.jni.capi.sqlite3_backup;
+import org.sqlite.jni.capi.sqlite3_blob;
+import org.sqlite.jni.capi.OutputPointer;
+import java.nio.ByteBuffer;
+
+/**
+ This class represents a database connection, analog to the C-side
+ sqlite3 class but with added argument validation, exceptions, and
+ similar "smoothing of sharp edges" to make the API safe to use from
+ Java. It also acts as a namespace for other types for which
+ individual instances are tied to a specific database connection.
+*/
+public final class Sqlite implements AutoCloseable {
+ private sqlite3 db;
+ private static final boolean JNI_SUPPORTS_NIO =
+ CApi.sqlite3_jni_supports_nio();
+
+ // Result codes
+ public static final int OK = CApi.SQLITE_OK;
+ public static final int ERROR = CApi.SQLITE_ERROR;
+ public static final int INTERNAL = CApi.SQLITE_INTERNAL;
+ public static final int PERM = CApi.SQLITE_PERM;
+ public static final int ABORT = CApi.SQLITE_ABORT;
+ public static final int BUSY = CApi.SQLITE_BUSY;
+ public static final int LOCKED = CApi.SQLITE_LOCKED;
+ public static final int NOMEM = CApi.SQLITE_NOMEM;
+ public static final int READONLY = CApi.SQLITE_READONLY;
+ public static final int INTERRUPT = CApi.SQLITE_INTERRUPT;
+ public static final int IOERR = CApi.SQLITE_IOERR;
+ public static final int CORRUPT = CApi.SQLITE_CORRUPT;
+ public static final int NOTFOUND = CApi.SQLITE_NOTFOUND;
+ public static final int FULL = CApi.SQLITE_FULL;
+ public static final int CANTOPEN = CApi.SQLITE_CANTOPEN;
+ public static final int PROTOCOL = CApi.SQLITE_PROTOCOL;
+ public static final int EMPTY = CApi.SQLITE_EMPTY;
+ public static final int SCHEMA = CApi.SQLITE_SCHEMA;
+ public static final int TOOBIG = CApi.SQLITE_TOOBIG;
+ public static final int CONSTRAINT = CApi. SQLITE_CONSTRAINT;
+ public static final int MISMATCH = CApi.SQLITE_MISMATCH;
+ public static final int MISUSE = CApi.SQLITE_MISUSE;
+ public static final int NOLFS = CApi.SQLITE_NOLFS;
+ public static final int AUTH = CApi.SQLITE_AUTH;
+ public static final int FORMAT = CApi.SQLITE_FORMAT;
+ public static final int RANGE = CApi.SQLITE_RANGE;
+ public static final int NOTADB = CApi.SQLITE_NOTADB;
+ public static final int NOTICE = CApi.SQLITE_NOTICE;
+ public static final int WARNING = CApi.SQLITE_WARNING;
+ public static final int ROW = CApi.SQLITE_ROW;
+ public static final int DONE = CApi.SQLITE_DONE;
+ public static final int ERROR_MISSING_COLLSEQ = CApi.SQLITE_ERROR_MISSING_COLLSEQ;
+ public static final int ERROR_RETRY = CApi.SQLITE_ERROR_RETRY;
+ public static final int ERROR_SNAPSHOT = CApi.SQLITE_ERROR_SNAPSHOT;
+ public static final int IOERR_READ = CApi.SQLITE_IOERR_READ;
+ public static final int IOERR_SHORT_READ = CApi.SQLITE_IOERR_SHORT_READ;
+ public static final int IOERR_WRITE = CApi.SQLITE_IOERR_WRITE;
+ public static final int IOERR_FSYNC = CApi.SQLITE_IOERR_FSYNC;
+ public static final int IOERR_DIR_FSYNC = CApi.SQLITE_IOERR_DIR_FSYNC;
+ public static final int IOERR_TRUNCATE = CApi.SQLITE_IOERR_TRUNCATE;
+ public static final int IOERR_FSTAT = CApi.SQLITE_IOERR_FSTAT;
+ public static final int IOERR_UNLOCK = CApi.SQLITE_IOERR_UNLOCK;
+ public static final int IOERR_RDLOCK = CApi.SQLITE_IOERR_RDLOCK;
+ public static final int IOERR_DELETE = CApi.SQLITE_IOERR_DELETE;
+ public static final int IOERR_BLOCKED = CApi.SQLITE_IOERR_BLOCKED;
+ public static final int IOERR_NOMEM = CApi.SQLITE_IOERR_NOMEM;
+ public static final int IOERR_ACCESS = CApi.SQLITE_IOERR_ACCESS;
+ public static final int IOERR_CHECKRESERVEDLOCK = CApi.SQLITE_IOERR_CHECKRESERVEDLOCK;
+ public static final int IOERR_LOCK = CApi.SQLITE_IOERR_LOCK;
+ public static final int IOERR_CLOSE = CApi.SQLITE_IOERR_CLOSE;
+ public static final int IOERR_DIR_CLOSE = CApi.SQLITE_IOERR_DIR_CLOSE;
+ public static final int IOERR_SHMOPEN = CApi.SQLITE_IOERR_SHMOPEN;
+ public static final int IOERR_SHMSIZE = CApi.SQLITE_IOERR_SHMSIZE;
+ public static final int IOERR_SHMLOCK = CApi.SQLITE_IOERR_SHMLOCK;
+ public static final int IOERR_SHMMAP = CApi.SQLITE_IOERR_SHMMAP;
+ public static final int IOERR_SEEK = CApi.SQLITE_IOERR_SEEK;
+ public static final int IOERR_DELETE_NOENT = CApi.SQLITE_IOERR_DELETE_NOENT;
+ public static final int IOERR_MMAP = CApi.SQLITE_IOERR_MMAP;
+ public static final int IOERR_GETTEMPPATH = CApi.SQLITE_IOERR_GETTEMPPATH;
+ public static final int IOERR_CONVPATH = CApi.SQLITE_IOERR_CONVPATH;
+ public static final int IOERR_VNODE = CApi.SQLITE_IOERR_VNODE;
+ public static final int IOERR_AUTH = CApi.SQLITE_IOERR_AUTH;
+ public static final int IOERR_BEGIN_ATOMIC = CApi.SQLITE_IOERR_BEGIN_ATOMIC;
+ public static final int IOERR_COMMIT_ATOMIC = CApi.SQLITE_IOERR_COMMIT_ATOMIC;
+ public static final int IOERR_ROLLBACK_ATOMIC = CApi.SQLITE_IOERR_ROLLBACK_ATOMIC;
+ public static final int IOERR_DATA = CApi.SQLITE_IOERR_DATA;
+ public static final int IOERR_CORRUPTFS = CApi.SQLITE_IOERR_CORRUPTFS;
+ public static final int LOCKED_SHAREDCACHE = CApi.SQLITE_LOCKED_SHAREDCACHE;
+ public static final int LOCKED_VTAB = CApi.SQLITE_LOCKED_VTAB;
+ public static final int BUSY_RECOVERY = CApi.SQLITE_BUSY_RECOVERY;
+ public static final int BUSY_SNAPSHOT = CApi.SQLITE_BUSY_SNAPSHOT;
+ public static final int BUSY_TIMEOUT = CApi.SQLITE_BUSY_TIMEOUT;
+ public static final int CANTOPEN_NOTEMPDIR = CApi.SQLITE_CANTOPEN_NOTEMPDIR;
+ public static final int CANTOPEN_ISDIR = CApi.SQLITE_CANTOPEN_ISDIR;
+ public static final int CANTOPEN_FULLPATH = CApi.SQLITE_CANTOPEN_FULLPATH;
+ public static final int CANTOPEN_CONVPATH = CApi.SQLITE_CANTOPEN_CONVPATH;
+ public static final int CANTOPEN_SYMLINK = CApi.SQLITE_CANTOPEN_SYMLINK;
+ public static final int CORRUPT_VTAB = CApi.SQLITE_CORRUPT_VTAB;
+ public static final int CORRUPT_SEQUENCE = CApi.SQLITE_CORRUPT_SEQUENCE;
+ public static final int CORRUPT_INDEX = CApi.SQLITE_CORRUPT_INDEX;
+ public static final int READONLY_RECOVERY = CApi.SQLITE_READONLY_RECOVERY;
+ public static final int READONLY_CANTLOCK = CApi.SQLITE_READONLY_CANTLOCK;
+ public static final int READONLY_ROLLBACK = CApi.SQLITE_READONLY_ROLLBACK;
+ public static final int READONLY_DBMOVED = CApi.SQLITE_READONLY_DBMOVED;
+ public static final int READONLY_CANTINIT = CApi.SQLITE_READONLY_CANTINIT;
+ public static final int READONLY_DIRECTORY = CApi.SQLITE_READONLY_DIRECTORY;
+ public static final int ABORT_ROLLBACK = CApi.SQLITE_ABORT_ROLLBACK;
+ public static final int CONSTRAINT_CHECK = CApi.SQLITE_CONSTRAINT_CHECK;
+ public static final int CONSTRAINT_COMMITHOOK = CApi.SQLITE_CONSTRAINT_COMMITHOOK;
+ public static final int CONSTRAINT_FOREIGNKEY = CApi.SQLITE_CONSTRAINT_FOREIGNKEY;
+ public static final int CONSTRAINT_FUNCTION = CApi.SQLITE_CONSTRAINT_FUNCTION;
+ public static final int CONSTRAINT_NOTNULL = CApi.SQLITE_CONSTRAINT_NOTNULL;
+ public static final int CONSTRAINT_PRIMARYKEY = CApi.SQLITE_CONSTRAINT_PRIMARYKEY;
+ public static final int CONSTRAINT_TRIGGER = CApi.SQLITE_CONSTRAINT_TRIGGER;
+ public static final int CONSTRAINT_UNIQUE = CApi.SQLITE_CONSTRAINT_UNIQUE;
+ public static final int CONSTRAINT_VTAB = CApi.SQLITE_CONSTRAINT_VTAB;
+ public static final int CONSTRAINT_ROWID = CApi.SQLITE_CONSTRAINT_ROWID;
+ public static final int CONSTRAINT_PINNED = CApi.SQLITE_CONSTRAINT_PINNED;
+ public static final int CONSTRAINT_DATATYPE = CApi.SQLITE_CONSTRAINT_DATATYPE;
+ public static final int NOTICE_RECOVER_WAL = CApi.SQLITE_NOTICE_RECOVER_WAL;
+ public static final int NOTICE_RECOVER_ROLLBACK = CApi.SQLITE_NOTICE_RECOVER_ROLLBACK;
+ public static final int WARNING_AUTOINDEX = CApi.SQLITE_WARNING_AUTOINDEX;
+ public static final int AUTH_USER = CApi.SQLITE_AUTH_USER;
+ public static final int OK_LOAD_PERMANENTLY = CApi.SQLITE_OK_LOAD_PERMANENTLY;
+
+ // sqlite3_open() flags
+ 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;
+
+ // transaction state
+ 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;
+
+ // sqlite3_status() ops
+ 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;
+
+ // sqlite3_db_status() ops
+ public static final int DBSTATUS_LOOKASIDE_USED = CApi.SQLITE_DBSTATUS_LOOKASIDE_USED;
+ public static final int DBSTATUS_CACHE_USED = CApi.SQLITE_DBSTATUS_CACHE_USED;
+ public static final int DBSTATUS_SCHEMA_USED = CApi.SQLITE_DBSTATUS_SCHEMA_USED;
+ public static final int DBSTATUS_STMT_USED = CApi.SQLITE_DBSTATUS_STMT_USED;
+ public static final int DBSTATUS_LOOKASIDE_HIT = CApi.SQLITE_DBSTATUS_LOOKASIDE_HIT;
+ public static final int DBSTATUS_LOOKASIDE_MISS_SIZE = CApi.SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE;
+ public static final int DBSTATUS_LOOKASIDE_MISS_FULL = CApi.SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL;
+ public static final int DBSTATUS_CACHE_HIT = CApi.SQLITE_DBSTATUS_CACHE_HIT;
+ public static final int DBSTATUS_CACHE_MISS = CApi.SQLITE_DBSTATUS_CACHE_MISS;
+ public static final int DBSTATUS_CACHE_WRITE = CApi.SQLITE_DBSTATUS_CACHE_WRITE;
+ public static final int DBSTATUS_DEFERRED_FKS = CApi.SQLITE_DBSTATUS_DEFERRED_FKS;
+ public static final int DBSTATUS_CACHE_USED_SHARED = CApi.SQLITE_DBSTATUS_CACHE_USED_SHARED;
+ public static final int DBSTATUS_CACHE_SPILL = CApi.SQLITE_DBSTATUS_CACHE_SPILL;
+
+ // Limits
+ 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;
+
+ // sqlite3_prepare_v3() flags
+ public static final int PREPARE_PERSISTENT = CApi.SQLITE_PREPARE_PERSISTENT;
+ public static final int PREPARE_NO_VTAB = CApi.SQLITE_PREPARE_NO_VTAB;
+
+ // sqlite3_trace_v2() flags
+ public static final int TRACE_STMT = CApi.SQLITE_TRACE_STMT;
+ public static final int TRACE_PROFILE = CApi.SQLITE_TRACE_PROFILE;
+ public static final int TRACE_ROW = CApi.SQLITE_TRACE_ROW;
+ public static final int TRACE_CLOSE = CApi.SQLITE_TRACE_CLOSE;
+ public static final int TRACE_ALL = TRACE_STMT | TRACE_PROFILE | TRACE_ROW | TRACE_CLOSE;
+
+ // sqlite3_db_config() ops
+ public static final int DBCONFIG_ENABLE_FKEY = CApi.SQLITE_DBCONFIG_ENABLE_FKEY;
+ public static final int DBCONFIG_ENABLE_TRIGGER = CApi.SQLITE_DBCONFIG_ENABLE_TRIGGER;
+ public static final int DBCONFIG_ENABLE_FTS3_TOKENIZER = CApi.SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER;
+ public static final int DBCONFIG_ENABLE_LOAD_EXTENSION = CApi.SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION;
+ public static final int DBCONFIG_NO_CKPT_ON_CLOSE = CApi.SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE;
+ public static final int DBCONFIG_ENABLE_QPSG = CApi.SQLITE_DBCONFIG_ENABLE_QPSG;
+ public static final int DBCONFIG_TRIGGER_EQP = CApi.SQLITE_DBCONFIG_TRIGGER_EQP;
+ public static final int DBCONFIG_RESET_DATABASE = CApi.SQLITE_DBCONFIG_RESET_DATABASE;
+ public static final int DBCONFIG_DEFENSIVE = CApi.SQLITE_DBCONFIG_DEFENSIVE;
+ public static final int DBCONFIG_WRITABLE_SCHEMA = CApi.SQLITE_DBCONFIG_WRITABLE_SCHEMA;
+ public static final int DBCONFIG_LEGACY_ALTER_TABLE = CApi.SQLITE_DBCONFIG_LEGACY_ALTER_TABLE;
+ public static final int DBCONFIG_DQS_DML = CApi.SQLITE_DBCONFIG_DQS_DML;
+ public static final int DBCONFIG_DQS_DDL = CApi.SQLITE_DBCONFIG_DQS_DDL;
+ public static final int DBCONFIG_ENABLE_VIEW = CApi.SQLITE_DBCONFIG_ENABLE_VIEW;
+ public static final int DBCONFIG_LEGACY_FILE_FORMAT = CApi.SQLITE_DBCONFIG_LEGACY_FILE_FORMAT;
+ public static final int DBCONFIG_TRUSTED_SCHEMA = CApi.SQLITE_DBCONFIG_TRUSTED_SCHEMA;
+ public static final int DBCONFIG_STMT_SCANSTATUS = CApi.SQLITE_DBCONFIG_STMT_SCANSTATUS;
+ public static final int DBCONFIG_REVERSE_SCANORDER = CApi.SQLITE_DBCONFIG_REVERSE_SCANORDER;
+
+ // sqlite3_config() ops
+ public static final int CONFIG_SINGLETHREAD = CApi.SQLITE_CONFIG_SINGLETHREAD;
+ public static final int CONFIG_MULTITHREAD = CApi.SQLITE_CONFIG_MULTITHREAD;
+ public static final int CONFIG_SERIALIZED = CApi.SQLITE_CONFIG_SERIALIZED;
+
+ // Encodings
+ public static final int UTF8 = CApi.SQLITE_UTF8;
+ public static final int UTF16 = CApi.SQLITE_UTF16;
+ public static final int UTF16LE = CApi.SQLITE_UTF16LE;
+ public static final int UTF16BE = CApi.SQLITE_UTF16BE;
+ /* We elide the UTF16_ALIGNED from this interface because it
+ is irrelevant for the Java interface. */
+
+ // SQL data type IDs
+ public static final int INTEGER = CApi.SQLITE_INTEGER;
+ public static final int FLOAT = CApi.SQLITE_FLOAT;
+ public static final int TEXT = CApi.SQLITE_TEXT;
+ public static final int BLOB = CApi.SQLITE_BLOB;
+ public static final int NULL = CApi.SQLITE_NULL;
+
+ // Authorizer codes.
+ public static final int DENY = CApi.SQLITE_DENY;
+ public static final int IGNORE = CApi.SQLITE_IGNORE;
+ public static final int CREATE_INDEX = CApi.SQLITE_CREATE_INDEX;
+ public static final int CREATE_TABLE = CApi.SQLITE_CREATE_TABLE;
+ public static final int CREATE_TEMP_INDEX = CApi.SQLITE_CREATE_TEMP_INDEX;
+ public static final int CREATE_TEMP_TABLE = CApi.SQLITE_CREATE_TEMP_TABLE;
+ public static final int CREATE_TEMP_TRIGGER = CApi.SQLITE_CREATE_TEMP_TRIGGER;
+ public static final int CREATE_TEMP_VIEW = CApi.SQLITE_CREATE_TEMP_VIEW;
+ public static final int CREATE_TRIGGER = CApi.SQLITE_CREATE_TRIGGER;
+ public static final int CREATE_VIEW = CApi.SQLITE_CREATE_VIEW;
+ public static final int DELETE = CApi.SQLITE_DELETE;
+ public static final int DROP_INDEX = CApi.SQLITE_DROP_INDEX;
+ public static final int DROP_TABLE = CApi.SQLITE_DROP_TABLE;
+ public static final int DROP_TEMP_INDEX = CApi.SQLITE_DROP_TEMP_INDEX;
+ public static final int DROP_TEMP_TABLE = CApi.SQLITE_DROP_TEMP_TABLE;
+ public static final int DROP_TEMP_TRIGGER = CApi.SQLITE_DROP_TEMP_TRIGGER;
+ public static final int DROP_TEMP_VIEW = CApi.SQLITE_DROP_TEMP_VIEW;
+ public static final int DROP_TRIGGER = CApi.SQLITE_DROP_TRIGGER;
+ public static final int DROP_VIEW = CApi.SQLITE_DROP_VIEW;
+ public static final int INSERT = CApi.SQLITE_INSERT;
+ public static final int PRAGMA = CApi.SQLITE_PRAGMA;
+ public static final int READ = CApi.SQLITE_READ;
+ public static final int SELECT = CApi.SQLITE_SELECT;
+ public static final int TRANSACTION = CApi.SQLITE_TRANSACTION;
+ public static final int UPDATE = CApi.SQLITE_UPDATE;
+ public static final int ATTACH = CApi.SQLITE_ATTACH;
+ public static final int DETACH = CApi.SQLITE_DETACH;
+ public static final int ALTER_TABLE = CApi.SQLITE_ALTER_TABLE;
+ public static final int REINDEX = CApi.SQLITE_REINDEX;
+ public static final int ANALYZE = CApi.SQLITE_ANALYZE;
+ public static final int CREATE_VTABLE = CApi.SQLITE_CREATE_VTABLE;
+ public static final int DROP_VTABLE = CApi.SQLITE_DROP_VTABLE;
+ public static final int FUNCTION = CApi.SQLITE_FUNCTION;
+ public static final int SAVEPOINT = CApi.SQLITE_SAVEPOINT;
+ public static final int RECURSIVE = CApi.SQLITE_RECURSIVE;
+
+ //! Used only by the open() factory functions.
+ private Sqlite(sqlite3 db){
+ this.db = db;
+ }
+
+ /** Maps org.sqlite.jni.capi.sqlite3 to Sqlite instances. */
+ private static final java.util.Map nativeToWrapper
+ = new java.util.HashMap<>();
+
+
+ /**
+ When any given thread is done using the SQLite library, calling
+ this will free up any native-side resources which may be
+ associated specifically with that thread. This is not strictly
+ necessary, in particular in applications which only use SQLite
+ from a single thread, but may help free some otherwise errant
+ resources.
+
+ Calling into SQLite from a given thread after this has been
+ called in that thread is harmless. The library will simply start
+ to re-cache certain state for that thread.
+
+ Contrariwise, failing to call this will effectively leak a small
+ amount of cached state for the thread, which may add up to
+ significant amounts if the application uses SQLite from many
+ threads.
+
+ This must never be called while actively using SQLite from this
+ thread, e.g. from within a query loop or a callback which is
+ operating on behalf of the library.
+ */
+ static void uncacheThread(){
+ CApi.sqlite3_java_uncache_thread();
+ }
+
+ /**
+ Returns the Sqlite object associated with the given sqlite3
+ object, or null if there is no such mapping.
+ */
+ static Sqlite fromNative(sqlite3 low){
+ synchronized(nativeToWrapper){
+ return nativeToWrapper.get(low);
+ }
+ }
+
+ /**
+ Returns a newly-opened db connection or throws SqliteException if
+ opening fails. All arguments are as documented for
+ sqlite3_open_v2().
+
+ Design question: do we want static factory functions or should
+ this be reformulated as a constructor?
+ */
+ public static Sqlite open(String filename, int flags, String vfsName){
+ final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
+ final int rc = CApi.sqlite3_open_v2(filename, out, flags, vfsName);
+ final sqlite3 n = out.take();
+ if( 0!=rc ){
+ if( null==n ) throw new SqliteException(rc);
+ final SqliteException ex = new SqliteException(n);
+ n.close();
+ throw ex;
+ }
+ final Sqlite rv = new Sqlite(n);
+ synchronized(nativeToWrapper){
+ nativeToWrapper.put(n, rv);
+ }
+ runAutoExtensions(rv);
+ return rv;
+ }
+
+ public static Sqlite open(String filename, int flags){
+ return open(filename, flags, null);
+ }
+
+ public static Sqlite open(String filename){
+ return open(filename, OPEN_READWRITE|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();
+ }
+
+ /**
+ 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();
+ }
+
+ /**
+ Analog to sqlite3_compileoption_get().
+ */
+ public static String compileOptionGet(int n){
+ return CApi.sqlite3_compileoption_get(n);
+ }
+
+ /**
+ Analog to sqlite3_compileoption_used().
+ */
+ public static boolean compileOptionUsed(String optName){
+ return CApi.sqlite3_compileoption_used(optName);
+ }
+
+ private static boolean hasNormalizeSql =
+ compileOptionUsed("ENABLE_NORMALIZE");
+
+ private static boolean hasSqlLog =
+ compileOptionUsed("ENABLE_SQLLOG");
+
+ /**
+ Throws UnsupportedOperationException if check is false.
+ flag is expected to be the name of an SQLITE_ENABLE_...
+ build flag.
+ */
+ private static void checkSupported(boolean check, String flag){
+ if( !check ){
+ throw new UnsupportedOperationException(
+ "Library was built without "+flag
+ );
+ }
+ }
+
+ /**
+ Analog to sqlite3_complete().
+ */
+ public static boolean isCompleteStatement(String sql){
+ switch(CApi.sqlite3_complete(sql)){
+ case 0: return false;
+ case CApi.SQLITE_MISUSE:
+ throw new IllegalArgumentException("Input may not be null.");
+ case CApi.SQLITE_NOMEM:
+ throw new OutOfMemoryError();
+ default:
+ return true;
+ }
+ }
+
+ public static int keywordCount(){
+ return CApi.sqlite3_keyword_count();
+ }
+
+ public static boolean keywordCheck(String word){
+ return CApi.sqlite3_keyword_check(word);
+ }
+
+ public static String keywordName(int index){
+ return CApi.sqlite3_keyword_name(index);
+ }
+
+ 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);
+ }
+
+ /**
+ Output object for use with status() and libStatus().
+ */
+ public static final class Status {
+ /** The current value for the requested status() or libStatus() metric. */
+ long current;
+ /** The peak value for the requested status() or libStatus() metric. */
+ long peak;
+ };
+
+ /**
+ As per sqlite3_status64(), but returns its current and high-water
+ results as a Status object. Throws if the first argument is
+ not one of the STATUS_... constants.
+ */
+ public static Status 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();
+ checkRcStatic( CApi.sqlite3_status64(op, pCurrent, pHighwater, resetStats) );
+ final Status s = new Status();
+ s.current = pCurrent.value;
+ s.peak = pHighwater.value;
+ return s;
+ }
+
+ /**
+ As per sqlite3_db_status(), but returns its current and
+ high-water results as a Status object. Throws if the first
+ argument is not one of the DBSTATUS_... constants or on any other
+ misuse.
+ */
+ public Status status(int op, boolean resetStats){
+ org.sqlite.jni.capi.OutputPointer.Int32 pCurrent =
+ new org.sqlite.jni.capi.OutputPointer.Int32();
+ org.sqlite.jni.capi.OutputPointer.Int32 pHighwater =
+ new org.sqlite.jni.capi.OutputPointer.Int32();
+ checkRc( CApi.sqlite3_db_status(thisDb(), op, pCurrent, pHighwater, resetStats) );
+ final Status s = new Status();
+ s.current = pCurrent.value;
+ s.peak = pHighwater.value;
+ return s;
+ }
+
+ @Override public void close(){
+ if(null!=this.db){
+ synchronized(nativeToWrapper){
+ nativeToWrapper.remove(this.db);
+ }
+ this.db.close();
+ this.db = null;
+ }
+ }
+
+ /**
+ 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 thisDb(){
+ if( null==db || 0==db.getNativePointer() ){
+ throw new IllegalArgumentException("This database instance is closed.");
+ }
+ return this.db;
+ }
+
+ // private byte[] stringToUtf8(String s){
+ // return s==null ? null : s.getBytes(StandardCharsets.UTF_8);
+ // }
+
+ /**
+ 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.
+
+ As a special case, if rc is SQLITE_NOMEM, an OutOfMemoryError is
+ thrown.
+ */
+ private void checkRc(int rc){
+ if( 0!=rc ){
+ if( CApi.SQLITE_NOMEM==rc ){
+ throw new OutOfMemoryError();
+ }else if( null==db || 0==CApi.sqlite3_errcode(db) ){
+ throw new SqliteException(rc);
+ }else{
+ throw new SqliteException(db);
+ }
+ }
+ }
+
+ /**
+ Like checkRc() but behaves as if that function were
+ called with a null db object.
+ */
+ private static void checkRcStatic(int rc){
+ if( 0!=rc ){
+ if( CApi.SQLITE_NOMEM==rc ){
+ throw new OutOfMemoryError();
+ }else{
+ throw new SqliteException(rc);
+ }
+ }
+ }
+
+ /**
+ Toggles the use of extended result codes on or off. By default
+ they are turned off, but they can be enabled by default by
+ including the OPEN_EXRESCODE flag when opening a database.
+
+ Because this API reports db-side errors using exceptions,
+ enabling this may change the values returned by
+ SqliteException.errcode().
+ */
+ public void useExtendedResultCodes(boolean on){
+ checkRc( CApi.sqlite3_extended_result_codes(thisDb(), on) );
+ }
+
+ /**
+ Analog to sqlite3_prepare_v3(), this prepares the first SQL
+ statement from the given input string and returns it as a
+ Stmt. It throws an SqliteException if preparation fails or an
+ IllegalArgumentException if the input is empty (e.g. contains
+ only comments or whitespace).
+
+ The first argument must be SQL input in UTF-8 encoding.
+
+ prepFlags must be 0 or a bitmask of the PREPARE_... constants.
+
+ For processing multiple statements from a single input, use
+ prepareMulti().
+
+ Design note: though the C-level API succeeds with a null
+ statement object for empty inputs, that approach is cumbersome to
+ use in higher-level APIs because every prepared statement has to
+ be checked for null before using it.
+ */
+ public Stmt prepare(byte utf8Sql[], int prepFlags){
+ final OutputPointer.sqlite3_stmt out = new OutputPointer.sqlite3_stmt();
+ final int rc = CApi.sqlite3_prepare_v3(thisDb(), utf8Sql, 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);
+ }
+
+ /**
+ Equivalent to prepare(X, prepFlags), where X is
+ sql.getBytes(StandardCharsets.UTF_8).
+ */
+ public Stmt prepare(String sql, int prepFlags){
+ return prepare( sql.getBytes(StandardCharsets.UTF_8), prepFlags );
+ }
+
+ /**
+ Equivalent to prepare(sql, 0).
+ */
+ public Stmt prepare(String sql){
+ return prepare(sql, 0);
+ }
+
+
+ /**
+ Callback type for use with prepareMulti().
+ */
+ public interface PrepareMulti {
+ /**
+ Gets passed a Stmt which it may handle in arbitrary ways.
+ Ownership of st is passed to this function. It must throw on
+ error.
+ */
+ void call(Sqlite.Stmt st);
+ }
+
+ /**
+ A PrepareMulti implementation which calls another PrepareMulti
+ object and then finalizes its statement.
+ */
+ public static class PrepareMultiFinalize implements PrepareMulti {
+ private final PrepareMulti pm;
+ /**
+ Proxies the given PrepareMulti via this object's call() method.
+ */
+ public PrepareMultiFinalize(PrepareMulti proxy){
+ this.pm = proxy;
+ }
+ /**
+ Passes st to the call() method of the object this one proxies,
+ then finalizes st, propagating any exceptions from call() after
+ finalizing st.
+ */
+ @Override public void call(Stmt st){
+ try{ pm.call(st); }
+ finally{ st.finalizeStmt(); }
+ }
+ }
+
+ /**
+ Equivalent to prepareMulti(sql,0,visitor).
+ */
+ public void prepareMulti(String sql, PrepareMulti visitor){
+ prepareMulti( sql, 0, visitor );
+ }
+
+ /**
+ Equivallent to prepareMulti(X,prepFlags,visitor), where X is
+ sql.getBytes(StandardCharsets.UTF_8).
+ */
+ public void prepareMulti(String sql, int prepFlags, PrepareMulti visitor){
+ prepareMulti(sql.getBytes(StandardCharsets.UTF_8), prepFlags, visitor);
+ }
+
+ /**
+ A variant of prepare() which can handle multiple SQL statements
+ in a single input string. For each statement in the given string,
+ the statement is passed to visitor.call() a single time, passing
+ ownership of the statement to that function. This function does
+ not step() or close() statements - those operations are left to
+ caller or the visitor function.
+
+ Unlike prepare(), this function does not fail if the input
+ contains only whitespace or SQL comments. In that case it is up
+ to the caller to arrange for that to be an error (if desired).
+
+ PrepareMultiFinalize offers a proxy which finalizes each
+ statement after it is passed to another client-defined visitor.
+
+ Be aware that certain legal SQL constructs may fail in the
+ preparation phase, before the corresponding statement can be
+ stepped. Most notably, authorizer checks which disallow access to
+ something in a statement behave that way.
+ */
+ public void prepareMulti(byte sqlUtf8[], int prepFlags, PrepareMulti visitor){
+ int pos = 0, n = 1;
+ byte[] sqlChunk = sqlUtf8;
+ final org.sqlite.jni.capi.OutputPointer.sqlite3_stmt outStmt =
+ new org.sqlite.jni.capi.OutputPointer.sqlite3_stmt();
+ final org.sqlite.jni.capi.OutputPointer.Int32 oTail =
+ new org.sqlite.jni.capi.OutputPointer.Int32();
+ while( pos < sqlChunk.length ){
+ sqlite3_stmt stmt = null;
+ if( pos>0 ){
+ sqlChunk = java.util.Arrays.copyOfRange(sqlChunk, pos, sqlChunk.length);
+ }
+ if( 0==sqlChunk.length ) break;
+ checkRc(
+ CApi.sqlite3_prepare_v3(db, sqlChunk, prepFlags, outStmt, oTail)
+ );
+ pos = oTail.value;
+ stmt = outStmt.take();
+ if( null==stmt ){
+ /* empty statement, e.g. only comments or whitespace, was parsed. */
+ continue;
+ }
+ visitor.call(new Stmt(this, stmt));
+ }
+ }
+
+ 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());
+ }
+
+ /**
+ 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 sqlite3_db_config() for the call forms which take one
+ of the boolean-type db configuration flags (namely the
+ DBCONFIG_... constants defined in this class). On success it
+ returns the result of that underlying call. Throws on error.
+ */
+ public boolean dbConfig(int op, boolean on){
+ org.sqlite.jni.capi.OutputPointer.Int32 pOut =
+ new org.sqlite.jni.capi.OutputPointer.Int32();
+ checkRc( CApi.sqlite3_db_config(thisDb(), op, on ? 1 : 0, pOut) );
+ return pOut.get()!=0;
+ }
+
+ /**
+ 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 libReleaseMemory(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;
+ }
+
+ public interface TraceCallback {
+ /**
+ Called by sqlite3 for various tracing operations, as per
+ sqlite3_trace_v2(). Note that this interface elides the 2nd
+ argument to the native trace callback, as that role is better
+ filled by instance-local state.
+
+ These callbacks may throw, in which case their exceptions are
+ converted to C-level error information.
+
+
The 2nd argument to this function, if non-null, will be a an
+ Sqlite or Sqlite.Stmt object, depending on the first argument
+ (see below).
+
+
The final argument to this function is the "X" argument
+ documented for sqlite3_trace() and sqlite3_trace_v2(). Its type
+ depends on value of the first argument:
+
+
- SQLITE_TRACE_STMT: pNative is a Sqlite.Stmt. pX is a String
+ containing the prepared SQL.
+
+
- SQLITE_TRACE_PROFILE: pNative is a sqlite3_stmt. pX is a Long
+ holding an approximate number of nanoseconds the statement took
+ to run.
+
+
- SQLITE_TRACE_ROW: pNative is a sqlite3_stmt. pX is null.
+
+
- SQLITE_TRACE_CLOSE: pNative is a sqlite3. pX is null.
+ */
+ void call(int traceFlag, Object pNative, Object pX);
+ }
+
+ /**
+ Analog to sqlite3_trace_v2(). traceMask must be a mask of the
+ TRACE_... constants. Pass a null callback to remove tracing.
+
+ Throws on error.
+ */
+ public void trace(int traceMask, TraceCallback callback){
+ final Sqlite self = this;
+ final org.sqlite.jni.capi.TraceV2Callback tc =
+ (null==callback) ? null : new org.sqlite.jni.capi.TraceV2Callback(){
+ @SuppressWarnings("unchecked")
+ @Override public int call(int flag, Object pNative, Object pX){
+ switch(flag){
+ case TRACE_ROW:
+ case TRACE_PROFILE:
+ case TRACE_STMT:
+ callback.call(flag, Sqlite.Stmt.fromNative((sqlite3_stmt)pNative), pX);
+ break;
+ case TRACE_CLOSE:
+ callback.call(flag, self, pX);
+ break;
+ }
+ return 0;
+ }
+ };
+ checkRc( CApi.sqlite3_trace_v2(thisDb(), traceMask, tc) );
+ };
+
+ /**
+ Corresponds to the sqlite3_stmt class. Use Sqlite.prepare() to
+ create new instances.
+ */
+ public static final class Stmt implements AutoCloseable {
+ private Sqlite _db = null;
+ private sqlite3_stmt stmt = null;
+
+ /** Only called by the prepare() factory functions. */
+ Stmt(Sqlite db, sqlite3_stmt stmt){
+ this._db = db;
+ this.stmt = stmt;
+ synchronized(nativeToWrapper){
+ nativeToWrapper.put(this.stmt, this);
+ }
+ }
+
+ sqlite3_stmt nativeHandle(){
+ return stmt;
+ }
+
+ /** Maps org.sqlite.jni.capi.sqlite3_stmt to Stmt instances. */
+ private static final java.util.Map nativeToWrapper
+ = new java.util.HashMap<>();
+
+ /**
+ Returns the Stmt object associated with the given sqlite3_stmt
+ object, or null if there is no such mapping.
+ */
+ static Stmt fromNative(sqlite3_stmt low){
+ synchronized(nativeToWrapper){
+ return nativeToWrapper.get(low);
+ }
+ }
+
+ /**
+ If this statement is still opened, its low-level handle is
+ returned, else 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 statement's result column
+ count. Intended to be used by the columnXyz() methods. */
+ private sqlite3_stmt checkColIndex(int n){
+ if(n<0 || n>=columnCount()){
+ 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
+ signature. It does not throw on error here because "destructors
+ do not throw." If it returns non-0, the object is still
+ finalized, but the result code is an indication that something
+ went wrong in a prior call into the statement's API, as
+ documented for sqlite3_finalize().
+ */
+ public int finalizeStmt(){
+ int rc = 0;
+ if( null!=stmt ){
+ synchronized(nativeToWrapper){
+ nativeToWrapper.remove(this.stmt);
+ }
+ CApi.sqlite3_finalize(stmt);
+ stmt = null;
+ _db = null;
+ }
+ return rc;
+ }
+
+ @Override public void close(){
+ finalizeStmt();
+ }
+
+ /**
+ Throws if rc is any value other than 0, SQLITE_ROW, or
+ 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){
+ case 0:
+ case CApi.SQLITE_ROW:
+ case CApi.SQLITE_DONE: return rc;
+ default:
+ if( null==stmt ) throw new SqliteException(rc);
+ else throw new SqliteException(this);
+ }
+ }
+
+ /**
+ Works like sqlite3_step() but returns true for SQLITE_ROW,
+ false for SQLITE_DONE, and throws SqliteException for any other
+ result.
+ */
+ public boolean step(){
+ switch(checkRc(CApi.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."
+ );
+ }
+ }
+
+ /**
+ Works like sqlite3_step(), returning the same result codes as
+ that function unless throwOnError is true, in which case it
+ will throw an SqliteException for any result codes other than
+ Sqlite.ROW or Sqlite.DONE.
+
+ The utility of this overload over the no-argument one is the
+ ability to handle BUSY and LOCKED errors more easily.
+ */
+ public int step(boolean throwOnError){
+ final int rc = (null==stmt)
+ ? Sqlite.MISUSE
+ : CApi.sqlite3_step(stmt);
+ return throwOnError ? checkRc(rc) : rc;
+ }
+
+ /**
+ Returns the Sqlite which prepared this statement, or null if
+ this statement has been finalized.
+ */
+ public Sqlite getDb(){ return this._db; }
+
+ /**
+ Works like sqlite3_reset() but throws on error.
+ */
+ public void reset(){
+ checkRc(CApi.sqlite3_reset(thisStmt()));
+ }
+
+ public boolean isBusy(){
+ return CApi.sqlite3_stmt_busy(thisStmt());
+ }
+
+ public boolean isReadOnly(){
+ return CApi.sqlite3_stmt_readonly(thisStmt());
+ }
+
+ public String sql(){
+ return CApi.sqlite3_sql(thisStmt());
+ }
+
+ public String expandedSql(){
+ return CApi.sqlite3_expanded_sql(thisStmt());
+ }
+
+ /**
+ Analog to sqlite3_stmt_explain() but throws if op is invalid.
+ */
+ public void explain(int op){
+ checkRc(CApi.sqlite3_stmt_explain(thisStmt(), op));
+ }
+
+ /**
+ Analog to sqlite3_stmt_isexplain().
+ */
+ public int isExplain(){
+ return CApi.sqlite3_stmt_isexplain(thisStmt());
+ }
+
+ /**
+ Analog to sqlite3_normalized_sql(), but throws
+ UnsupportedOperationException if the library was built without
+ the SQLITE_ENABLE_NORMALIZE flag.
+ */
+ public String normalizedSql(){
+ Sqlite.checkSupported(hasNormalizeSql, "SQLITE_ENABLE_NORMALIZE");
+ return CApi.sqlite3_normalized_sql(thisStmt());
+ }
+
+ public void clearBindings(){
+ 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 asUtf16){
+ checkRc(CApi.sqlite3_bind_text16(thisStmt(), ndx, asUtf16));
+ }
+ 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));
+ }
+
+ 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 );
+ }
+ /**
+ Analog to sqlite3_column_count() but throws if this statement
+ has been finalized.
+ */
+ public int columnCount(){
+ /* We cannot reliably cache the column count in a class
+ member because an ALTER TABLE from a separate statement
+ can invalidate that count and we have no way, short of
+ installing a COMMIT handler or the like, of knowing when
+ to re-read it. We cannot install such a handler without
+ interfering with a client's ability to do so. */
+ return CApi.sqlite3_column_count(thisStmt());
+ }
+ 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 columnObject(int ndx, Class 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 */
+
+ /**
+ Interface for auto-extensions, as per the
+ sqlite3_auto_extension() API.
+
+ Design note: the chicken/egg timing of auto-extension execution
+ requires that this feature be entirely re-implemented in Java
+ because the C-level API has no access to the Sqlite type so
+ cannot pass on an object of that type while the database is being
+ opened. One side effect of this reimplementation is that this
+ class's list of auto-extensions is 100% independent of the
+ C-level list so, e.g., clearAutoExtensions() will have no effect
+ on auto-extensions added via the C-level API and databases opened
+ from that level of API will not be passed to this level's
+ AutoExtension instances.
+ */
+ public interface AutoExtension {
+ public void call(Sqlite db);
+ }
+
+ private static final java.util.Set autoExtensions =
+ new java.util.LinkedHashSet<>();
+
+ /**
+ Passes db to all auto-extensions. If any one of them throws,
+ db.close() is called before the exception is propagated.
+ */
+ private static void runAutoExtensions(Sqlite db){
+ AutoExtension list[];
+ synchronized(autoExtensions){
+ /* Avoid that modifications to the AutoExtension list from within
+ auto-extensions affect this execution of this list. */
+ list = autoExtensions.toArray(new AutoExtension[0]);
+ }
+ try {
+ for( AutoExtension ax : list ) ax.call(db);
+ }catch(Exception e){
+ db.close();
+ throw e;
+ }
+ }
+
+ /**
+ Analog to sqlite3_auto_extension(), adds the given object to the
+ list of auto-extensions if it is not already in that list. The
+ given object will be run as part of Sqlite.open(), and passed the
+ being-opened database. If the extension throws then open() will
+ fail.
+
+ This API does not guaranty whether or not manipulations made to
+ the auto-extension list from within auto-extension callbacks will
+ affect the current traversal of the auto-extension list. Whether
+ or not they do is unspecified and subject to change between
+ versions. e.g. if an AutoExtension calls addAutoExtension(),
+ whether or not the new extension will be run on the being-opened
+ database is undefined.
+
+ Note that calling Sqlite.open() from an auto-extension will
+ necessarily result in recursion loop and (eventually) a stack
+ overflow.
+ */
+ public static void addAutoExtension( AutoExtension e ){
+ if( null==e ){
+ throw new IllegalArgumentException("AutoExtension may not be null.");
+ }
+ synchronized(autoExtensions){
+ autoExtensions.add(e);
+ }
+ }
+
+ /**
+ Removes the given object from the auto-extension list if it is in
+ that list, otherwise this has no side-effects beyond briefly
+ locking that list.
+ */
+ public static void removeAutoExtension( AutoExtension e ){
+ synchronized(autoExtensions){
+ autoExtensions.remove(e);
+ }
+ }
+
+ /**
+ Removes all auto-extensions which were added via addAutoExtension().
+ */
+ public static void clearAutoExtensions(){
+ synchronized(autoExtensions){
+ autoExtensions.clear();
+ }
+ }
+
+ /**
+ Encapsulates state related to the sqlite3 backup API. Use
+ Sqlite.initBackup() to create new instances.
+ */
+ public static final class Backup implements AutoCloseable {
+ private sqlite3_backup b = null;
+ private Sqlite dbTo = null;
+ private Sqlite dbFrom = null;
+
+ Backup(Sqlite dbDest, String schemaDest,Sqlite dbSrc, String schemaSrc){
+ this.dbTo = dbDest;
+ this.dbFrom = dbSrc;
+ b = CApi.sqlite3_backup_init(dbDest.nativeHandle(), schemaDest,
+ dbSrc.nativeHandle(), schemaSrc);
+ if(null==b) toss();
+ }
+
+ private void toss(){
+ int rc = CApi.sqlite3_errcode(dbTo.nativeHandle());
+ if(0!=rc) throw new SqliteException(dbTo);
+ rc = CApi.sqlite3_errcode(dbFrom.nativeHandle());
+ if(0!=rc) throw new SqliteException(dbFrom);
+ throw new SqliteException(CApi.SQLITE_ERROR);
+ }
+
+ private sqlite3_backup getNative(){
+ if( null==b ) throw new IllegalStateException("This Backup is already closed.");
+ return b;
+ }
+ /**
+ If this backup is still active, this completes the backup and
+ frees its native resources, otherwise it this is a no-op.
+ */
+ public void finish(){
+ if( null!=b ){
+ CApi.sqlite3_backup_finish(b);
+ b = null;
+ dbTo = null;
+ dbFrom = null;
+ }
+ }
+
+ /** Equivalent to finish(). */
+ @Override public void close(){
+ this.finish();
+ }
+
+ /**
+ Analog to sqlite3_backup_step(). Returns 0 if stepping succeeds
+ or, Sqlite.DONE if the end is reached, Sqlite.BUSY if one of
+ the databases is busy, Sqlite.LOCKED if one of the databases is
+ locked, and throws for any other result code or if this object
+ has been closed. Note that BUSY and LOCKED are not necessarily
+ permanent errors, so do not trigger an exception.
+ */
+ public int step(int pageCount){
+ final int rc = CApi.sqlite3_backup_step(getNative(), pageCount);
+ switch(rc){
+ case 0:
+ case Sqlite.DONE:
+ case Sqlite.BUSY:
+ case Sqlite.LOCKED:
+ return rc;
+ default:
+ toss();
+ return CApi.SQLITE_ERROR/*not reached*/;
+ }
+ }
+
+ /**
+ Analog to sqlite3_backup_pagecount().
+ */
+ public int pageCount(){
+ return CApi.sqlite3_backup_pagecount(getNative());
+ }
+
+ /**
+ Analog to sqlite3_backup_remaining().
+ */
+ public int remaining(){
+ return CApi.sqlite3_backup_remaining(getNative());
+ }
+ }
+
+ /**
+ Analog to sqlite3_backup_init(). If schemaSrc is null, "main" is
+ assumed. Throws if either this db or dbSrc (the source db) are
+ not opened, if either of schemaDest or schemaSrc are null, or if
+ the underlying call to sqlite3_backup_init() fails.
+
+ The returned object must eventually be cleaned up by either
+ arranging for it to be auto-closed (e.g. using
+ try-with-resources) or by calling its finish() method.
+ */
+ public Backup initBackup(String schemaDest, Sqlite dbSrc, String schemaSrc){
+ thisDb();
+ dbSrc.thisDb();
+ if( null==schemaSrc || null==schemaDest ){
+ throw new IllegalArgumentException(
+ "Neither the source nor destination schema name may be null."
+ );
+ }
+ return new Backup(this, schemaDest, dbSrc, schemaSrc);
+ }
+
+
+ /**
+ Callback type for use with createCollation().
+ */
+ public interface Collation {
+ /**
+ Called by the SQLite core to compare inputs. Implementations
+ must compare its two arguments using memcmp(3) semantics.
+
+ Warning: the SQLite core has no mechanism for reporting errors
+ from custom collations and its workflow does not accommodate
+ propagation of exceptions from callbacks. Any exceptions thrown
+ from collations will be silently supressed and sorting results
+ will be unpredictable.
+ */
+ int call(byte[] lhs, byte[] rhs);
+ }
+
+ /**
+ Analog to sqlite3_create_collation().
+
+ Throws if name is null or empty, c is null, or the encoding flag
+ is invalid. The encoding must be one of the UTF8, UTF16, UTF16LE,
+ or UTF16BE constants.
+ */
+ public void createCollation(String name, int encoding, Collation c){
+ thisDb();
+ if( null==name || 0==name.length()){
+ throw new IllegalArgumentException("Collation name may not be null or empty.");
+ }
+ if( null==c ){
+ throw new IllegalArgumentException("Collation may not be null.");
+ }
+ switch(encoding){
+ case UTF8:
+ case UTF16:
+ case UTF16LE:
+ case UTF16BE:
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid Collation encoding.");
+ }
+ checkRc(
+ CApi.sqlite3_create_collation(
+ thisDb(), name, encoding, new org.sqlite.jni.capi.CollationCallback(){
+ @Override public int call(byte[] lhs, byte[] rhs){
+ try{return c.call(lhs, rhs);}
+ catch(Exception e){return 0;}
+ }
+ @Override public void xDestroy(){}
+ }
+ )
+ );
+ }
+
+ /**
+ Callback for use with onCollationNeeded().
+ */
+ public interface CollationNeeded {
+ /**
+ Must behave as documented for the callback for
+ sqlite3_collation_needed().
+
+ Warning: the C API has no mechanism for reporting or
+ propagating errors from this callback, so any exceptions it
+ throws are suppressed.
+ */
+ void call(Sqlite db, int encoding, String collationName);
+ }
+
+ /**
+ Sets up the given object to be called by the SQLite core when it
+ encounters a collation name which it does not know. Pass a null
+ object to disconnect the object from the core. This replaces any
+ existing collation-needed loader, or is a no-op if the given
+ object is already registered. Throws if registering the loader
+ fails.
+ */
+ public void onCollationNeeded( CollationNeeded cn ){
+ org.sqlite.jni.capi.CollationNeededCallback cnc = null;
+ if( null!=cn ){
+ cnc = new org.sqlite.jni.capi.CollationNeededCallback(){
+ @Override public void call(sqlite3 db, int encoding, String collationName){
+ final Sqlite xdb = Sqlite.fromNative(db);
+ if(null!=xdb) cn.call(xdb, encoding, collationName);
+ }
+ };
+ }
+ checkRc( CApi.sqlite3_collation_needed(thisDb(), cnc) );
+ }
+
+ /**
+ Callback for use with busyHandler().
+ */
+ public interface BusyHandler {
+ /**
+ Must function as documented for the C-level
+ sqlite3_busy_handler() callback argument, minus the (void*)
+ argument the C-level function requires.
+
+ If this function throws, it is translated to a database-level
+ error.
+ */
+ int call(int n);
+ }
+
+ /**
+ Analog to sqlite3_busy_timeout().
+ */
+ public void setBusyTimeout(int ms){
+ checkRc(CApi.sqlite3_busy_timeout(thisDb(), ms));
+ }
+
+ /**
+ Analog to sqlite3_busy_handler(). If b is null then any
+ current handler is cleared.
+ */
+ public void setBusyHandler( BusyHandler b ){
+ org.sqlite.jni.capi.BusyHandlerCallback bhc = null;
+ if( null!=b ){
+ bhc = new org.sqlite.jni.capi.BusyHandlerCallback(){
+ @Override public int call(int n){
+ return b.call(n);
+ }
+ };
+ }
+ checkRc( CApi.sqlite3_busy_handler(thisDb(), bhc) );
+ }
+
+ public interface CommitHook {
+ /**
+ Must behave as documented for the C-level sqlite3_commit_hook()
+ callback. If it throws, the exception is translated into
+ a db-level error.
+ */
+ int call();
+ }
+
+ /**
+ A level of indirection to permit setCommitHook() to have similar
+ semantics as the C API, returning the previous hook. The caveat
+ is that if the low-level API is used to install a hook, it will
+ have a different hook type than Sqlite.CommitHook so
+ setCommitHook() will return null instead of that object.
+ */
+ private static class CommitHookProxy
+ implements org.sqlite.jni.capi.CommitHookCallback {
+ final CommitHook commitHook;
+ CommitHookProxy(CommitHook ch){
+ this.commitHook = ch;
+ }
+ @Override public int call(){
+ return commitHook.call();
+ }
+ }
+
+ /**
+ Analog to sqlite3_commit_hook(). Returns the previous hook, if
+ any (else null). Throws if this db is closed.
+
+ Minor caveat: if a commit hook is set on this object's underlying
+ db handle using the lower-level SQLite API, this function may
+ return null when replacing it, despite there being a hook,
+ because it will have a different callback type. So long as the
+ handle is only manipulated via the high-level API, this caveat
+ does not apply.
+ */
+ public CommitHook setCommitHook( CommitHook c ){
+ CommitHookProxy chp = null;
+ if( null!=c ){
+ chp = new CommitHookProxy(c);
+ }
+ final org.sqlite.jni.capi.CommitHookCallback rv =
+ CApi.sqlite3_commit_hook(thisDb(), chp);
+ return (rv instanceof CommitHookProxy)
+ ? ((CommitHookProxy)rv).commitHook
+ : null;
+ }
+
+
+ public interface RollbackHook {
+ /**
+ Must behave as documented for the C-level sqlite3_rollback_hook()
+ callback. If it throws, the exception is translated into
+ a db-level error.
+ */
+ void call();
+ }
+
+ /**
+ A level of indirection to permit setRollbackHook() to have similar
+ semantics as the C API, returning the previous hook. The caveat
+ is that if the low-level API is used to install a hook, it will
+ have a different hook type than Sqlite.RollbackHook so
+ setRollbackHook() will return null instead of that object.
+ */
+ private static class RollbackHookProxy
+ implements org.sqlite.jni.capi.RollbackHookCallback {
+ final RollbackHook rollbackHook;
+ RollbackHookProxy(RollbackHook ch){
+ this.rollbackHook = ch;
+ }
+ @Override public void call(){rollbackHook.call();}
+ }
+
+ /**
+ Analog to sqlite3_rollback_hook(). Returns the previous hook, if
+ any (else null). Throws if this db is closed.
+
+ Minor caveat: if a rollback hook is set on this object's underlying
+ db handle using the lower-level SQLite API, this function may
+ return null when replacing it, despite there being a hook,
+ because it will have a different callback type. So long as the
+ handle is only manipulated via the high-level API, this caveat
+ does not apply.
+ */
+ public RollbackHook setRollbackHook( RollbackHook c ){
+ RollbackHookProxy chp = null;
+ if( null!=c ){
+ chp = new RollbackHookProxy(c);
+ }
+ final org.sqlite.jni.capi.RollbackHookCallback rv =
+ CApi.sqlite3_rollback_hook(thisDb(), chp);
+ return (rv instanceof RollbackHookProxy)
+ ? ((RollbackHookProxy)rv).rollbackHook
+ : null;
+ }
+
+ public interface UpdateHook {
+ /**
+ Must function as described for the C-level sqlite3_update_hook()
+ callback.
+ */
+ void call(int opId, String dbName, String tableName, long rowId);
+ }
+
+ /**
+ A level of indirection to permit setUpdateHook() to have similar
+ semantics as the C API, returning the previous hook. The caveat
+ is that if the low-level API is used to install a hook, it will
+ have a different hook type than Sqlite.UpdateHook so
+ setUpdateHook() will return null instead of that object.
+ */
+ private static class UpdateHookProxy
+ implements org.sqlite.jni.capi.UpdateHookCallback {
+ final UpdateHook updateHook;
+ UpdateHookProxy(UpdateHook ch){
+ this.updateHook = ch;
+ }
+ @Override public void call(int opId, String dbName, String tableName, long rowId){
+ updateHook.call(opId, dbName, tableName, rowId);
+ }
+ }
+
+ /**
+ Analog to sqlite3_update_hook(). Returns the previous hook, if
+ any (else null). Throws if this db is closed.
+
+ Minor caveat: if a update hook is set on this object's underlying
+ db handle using the lower-level SQLite API, this function may
+ return null when replacing it, despite there being a hook,
+ because it will have a different callback type. So long as the
+ handle is only manipulated via the high-level API, this caveat
+ does not apply.
+ */
+ public UpdateHook setUpdateHook( UpdateHook c ){
+ UpdateHookProxy chp = null;
+ if( null!=c ){
+ chp = new UpdateHookProxy(c);
+ }
+ final org.sqlite.jni.capi.UpdateHookCallback rv =
+ CApi.sqlite3_update_hook(thisDb(), chp);
+ return (rv instanceof UpdateHookProxy)
+ ? ((UpdateHookProxy)rv).updateHook
+ : null;
+ }
+
+
+ /**
+ Callback interface for use with setProgressHandler().
+ */
+ public interface ProgressHandler {
+ /**
+ Must behave as documented for the C-level sqlite3_progress_handler()
+ callback. If it throws, the exception is translated into
+ a db-level error.
+ */
+ int call();
+ }
+
+ /**
+ Analog to sqlite3_progress_handler(), sets the current progress
+ handler or clears it if p is null.
+
+ Note that this API, in contrast to setUpdateHook(),
+ setRollbackHook(), and setCommitHook(), cannot return the
+ previous handler. That inconsistency is part of the lower-level C
+ API.
+ */
+ public void setProgressHandler( int n, ProgressHandler p ){
+ org.sqlite.jni.capi.ProgressHandlerCallback phc = null;
+ if( null!=p ){
+ phc = new org.sqlite.jni.capi.ProgressHandlerCallback(){
+ @Override public int call(){ return p.call(); }
+ };
+ }
+ CApi.sqlite3_progress_handler( thisDb(), n, phc );
+ }
+
+
+ /**
+ Callback for use with setAuthorizer().
+ */
+ public interface Authorizer {
+ /**
+ Must function as described for the C-level
+ sqlite3_set_authorizer() callback. If it throws, the error is
+ converted to a db-level error and the exception is suppressed.
+ */
+ int call(int opId, String s1, String s2, String s3, String s4);
+ }
+
+ /**
+ Analog to sqlite3_set_authorizer(), this sets the current
+ authorizer callback, or clears if it passed null.
+ */
+ public void setAuthorizer( Authorizer a ) {
+ org.sqlite.jni.capi.AuthorizerCallback ac = null;
+ if( null!=a ){
+ ac = new org.sqlite.jni.capi.AuthorizerCallback(){
+ @Override public int call(int opId, String s1, String s2, String s3, String s4){
+ return a.call(opId, s1, s2, s3, s4);
+ }
+ };
+ }
+ checkRc( CApi.sqlite3_set_authorizer( thisDb(), ac ) );
+ }
+
+ /**
+ Object type for use with blobOpen()
+ */
+ public final class Blob implements AutoCloseable {
+ private Sqlite db;
+ private sqlite3_blob b;
+ Blob(Sqlite db, sqlite3_blob b){
+ this.db = db;
+ this.b = b;
+ }
+
+ /**
+ If this blob is still opened, its low-level handle is
+ returned, else an IllegalArgumentException is thrown.
+ */
+ private sqlite3_blob thisBlob(){
+ if( null==b || 0==b.getNativePointer() ){
+ throw new IllegalArgumentException("This Blob has been finalized.");
+ }
+ return b;
+ }
+
+ /**
+ Analog to sqlite3_blob_close().
+ */
+ @Override public void close(){
+ if( null!=b ){
+ CApi.sqlite3_blob_close(b);
+ b = null;
+ db = null;
+ }
+ }
+
+ /**
+ Throws if the JVM does not have JNI-level support for
+ ByteBuffer.
+ */
+ private void checkNio(){
+ if( !Sqlite.JNI_SUPPORTS_NIO ){
+ throw new UnsupportedOperationException(
+ "This JVM does not support JNI access to ByteBuffer."
+ );
+ }
+ }
+ /**
+ Analog to sqlite3_blob_reopen() but throws on error.
+ */
+ public void reopen(long newRowId){
+ db.checkRc( CApi.sqlite3_blob_reopen(thisBlob(), newRowId) );
+ }
+
+ /**
+ Analog to sqlite3_blob_write() but throws on error.
+ */
+ public void write( byte[] bytes, int atOffset ){
+ db.checkRc( CApi.sqlite3_blob_write(thisBlob(), bytes, atOffset) );
+ }
+
+ /**
+ Analog to sqlite3_blob_read() but throws on error.
+ */
+ public void read( byte[] dest, int atOffset ){
+ db.checkRc( CApi.sqlite3_blob_read(thisBlob(), dest, atOffset) );
+ }
+
+ /**
+ Analog to sqlite3_blob_bytes().
+ */
+ public int bytes(){
+ return CApi.sqlite3_blob_bytes(thisBlob());
+ }
+ }
+
+ /**
+ Analog to sqlite3_blob_open(). Returns a Blob object for the
+ given database, table, column, and rowid. The blob is opened for
+ read-write mode if writeable is true, else it is read-only.
+
+ The returned object must eventually be freed, before this
+ database is closed, by either arranging for it to be auto-closed
+ or calling its close() method.
+
+ Throws on error.
+ */
+ public Blob blobOpen(String dbName, String tableName, String columnName,
+ long iRow, boolean writeable){
+ final OutputPointer.sqlite3_blob out = new OutputPointer.sqlite3_blob();
+ checkRc(
+ CApi.sqlite3_blob_open(thisDb(), dbName, tableName, columnName,
+ iRow, writeable ? 1 : 0, out)
+ );
+ return new Blob(this, out.take());
+ }
+
+ /**
+ Callback for use with libConfigLog().
+ */
+ public interface ConfigLog {
+ /**
+ Must function as described for a C-level callback for
+ sqlite3_config()'s SQLITE_CONFIG_LOG callback, with the slight
+ signature change. Any exceptions thrown from this callback are
+ necessarily suppressed.
+ */
+ void call(int errCode, String msg);
+ }
+
+ /**
+ Analog to sqlite3_config() with the SQLITE_CONFIG_LOG option,
+ this sets or (if log is null) clears the current logger.
+ */
+ public static void libConfigLog(ConfigLog log){
+ final org.sqlite.jni.capi.ConfigLogCallback l =
+ null==log
+ ? null
+ : new org.sqlite.jni.capi.ConfigLogCallback() {
+ @Override public void call(int errCode, String msg){
+ log.call(errCode, msg);
+ }
+ };
+ checkRcStatic(CApi.sqlite3_config(l));
+ }
+
+ /**
+ Callback for use with libConfigSqlLog().
+ */
+ public interface ConfigSqlLog {
+ /**
+ Must function as described for a C-level callback for
+ sqlite3_config()'s SQLITE_CONFIG_SQLLOG callback, with the
+ slight signature change. Any exceptions thrown from this
+ callback are necessarily suppressed.
+ */
+ void call(Sqlite db, String msg, int msgType);
+ }
+
+ /**
+ Analog to sqlite3_config() with the SQLITE_CONFIG_SQLLOG option,
+ this sets or (if log is null) clears the current logger.
+
+ If SQLite is built without SQLITE_ENABLE_SQLLOG defined then this
+ will throw an UnsupportedOperationException.
+ */
+ public static void libConfigSqlLog(ConfigSqlLog log){
+ Sqlite.checkSupported(hasNormalizeSql, "SQLITE_ENABLE_SQLLOG");
+ final org.sqlite.jni.capi.ConfigSqlLogCallback l =
+ null==log
+ ? null
+ : new org.sqlite.jni.capi.ConfigSqlLogCallback() {
+ @Override public void call(sqlite3 db, String msg, int msgType){
+ try{
+ log.call(fromNative(db), msg, msgType);
+ }catch(Exception e){
+ /* Suppressed */
+ }
+ }
+ };
+ checkRcStatic(CApi.sqlite3_config(l));
+ }
+
+ /**
+ Analog to the C-level sqlite3_config() with one of the
+ SQLITE_CONFIG_... constants defined as CONFIG_... in this
+ class. Throws on error, including passing of an unknown option or
+ if a specified option is not supported by the underlying build of
+ the SQLite library.
+ */
+ public static void libConfigOp( int op ){
+ checkRcStatic(CApi.sqlite3_config(op));
+ }
+
+}
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java b/ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java
new file mode 100644
index 0000000000..9b4440f190
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java
@@ -0,0 +1,85 @@
+/*
+** 2023-10-09
+**
+** 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.capi.sqlite3;
+
+/**
+ A wrapper for communicating C-level (sqlite3*) instances with
+ Java. These wrappers do not own their associated pointer, they
+ simply provide a type-safe way to communicate it between Java
+ and C via JNI.
+*/
+public final class SqliteException extends java.lang.RuntimeException {
+ 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
+ error code and extended error code.
+ */
+ public SqliteException(String msg){
+ super(msg);
+ }
+
+ /**
+ Uses sqlite3_errstr(sqlite3ResultCode) for the error string and
+ sets both the error code and extended error code to the given
+ 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(CApi.sqlite3_errstr(sqlite3ResultCode));
+ errCode = xerrCode = sqlite3ResultCode;
+ }
+
+ /**
+ Records the current error state of db (which must not be null and
+ must refer to an opened db object). Note that this does not close
+ the db.
+
+ 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(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);
+ }
+
+ /**
+ Records the current error state of db (which must not be null and
+ must refer to an open database).
+ */
+ public SqliteException(Sqlite db){
+ this(db.nativeHandle());
+ }
+
+ public SqliteException(Sqlite.Stmt stmt){
+ this(stmt.getDb());
+ }
+
+ public int errcode(){ return errCode; }
+ public int extendedErrcode(){ return xerrCode; }
+ public int errorOffset(){ return errOffset; }
+ public int systemErrno(){ return sysErrno; }
+
+}
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java b/ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java
new file mode 100644
index 0000000000..5ac41323cb
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java
@@ -0,0 +1,1213 @@
+/*
+** 2023-10-09
+**
+** 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;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.sqlite.jni.capi.CApi;
+
+/**
+ An annotation for Tester2 tests which we do not want to run in
+ reflection-driven test mode because either they are not suitable
+ for multi-threaded threaded mode or we have to control their execution
+ order.
+*/
+@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
+@interface ManualTest{}
+/**
+ Annotation for Tester2 tests which mark those which must be skipped
+ in multi-threaded mode.
+*/
+@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
+@interface SingleThreadOnly{}
+
+public class Tester2 implements Runnable {
+ //! True when running in multi-threaded mode.
+ private static boolean mtMode = false;
+ //! True to sleep briefly between tests.
+ private static boolean takeNaps = false;
+ //! True to shuffle the order of the tests.
+ private static boolean shuffle = false;
+ //! True to dump the list of to-run tests to stdout.
+ private static int listRunTests = 0;
+ //! True to squelch all out() and outln() output.
+ private static boolean quietMode = false;
+ //! Total number of runTests() calls.
+ private static int nTestRuns = 0;
+ //! List of test*() methods to run.
+ private static List testMethods = null;
+ //! List of exceptions collected by run()
+ private static List listErrors = new ArrayList<>();
+ private static final class Metrics {
+ //! Number of times createNewDb() (or equivalent) is invoked.
+ volatile int dbOpen = 0;
+ }
+
+ //! Instance ID.
+ private Integer tId;
+
+ Tester2(Integer id){
+ tId = id;
+ }
+
+ static final Metrics metrics = new Metrics();
+
+ public static synchronized void outln(){
+ if( !quietMode ){
+ System.out.println("");
+ }
+ }
+
+ public static synchronized void outPrefix(){
+ if( !quietMode ){
+ System.out.print(Thread.currentThread().getName()+": ");
+ }
+ }
+
+ public static synchronized void outln(Object val){
+ if( !quietMode ){
+ outPrefix();
+ System.out.println(val);
+ }
+ }
+
+ public static synchronized void out(Object val){
+ if( !quietMode ){
+ System.out.print(val);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static synchronized void out(Object... vals){
+ if( !quietMode ){
+ outPrefix();
+ for(Object v : vals) out(v);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static synchronized void outln(Object... vals){
+ if( !quietMode ){
+ out(vals); out("\n");
+ }
+ }
+
+ static volatile int affirmCount = 0;
+ public static synchronized int affirm(Boolean v, String comment){
+ ++affirmCount;
+ if( false ) assert( v /* prefer assert over exception if it's enabled because
+ the JNI layer sometimes has to suppress exceptions,
+ so they might be squelched on their way back to the
+ top. */);
+ if( !v ) throw new RuntimeException(comment);
+ return affirmCount;
+ }
+
+ public static void affirm(Boolean v){
+ affirm(v, "Affirmation failed.");
+ }
+
+
+ public static void execSql(Sqlite db, String sql[]){
+ 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 ValueHolder rv = new ValueHolder<>(0);
+ final Sqlite.PrepareMulti pm = new Sqlite.PrepareMulti(){
+ @Override public void call(Sqlite.Stmt stmt){
+ try{
+ while( Sqlite.ROW == (rv.value = stmt.step(throwOnError)) ){}
+ }
+ finally{ stmt.finalizeStmt(); }
+ }
+ };
+ try {
+ dbw.prepareMulti(sql, pm);
+ }catch(SqliteException se){
+ if( throwOnError ){
+ throw se;
+ }else{
+ /* This error (likely) happened in the prepare() phase and we
+ need to preempt it. */
+ rv.value = se.errcode();
+ }
+ }
+ return (rv.value==Sqlite.DONE) ? 0 : rv.value;
+ }
+
+ static void execSql(Sqlite db, String sql){
+ execSql(db, true, sql);
+ }
+
+ @SingleThreadOnly /* because it's thread-agnostic */
+ private void test1(){
+ affirm(Sqlite.libVersionNumber() == CApi.SQLITE_VERSION_NUMBER);
+ }
+
+ private void nap() throws InterruptedException {
+ if( takeNaps ){
+ Thread.sleep(java.util.concurrent.ThreadLocalRandom.current().nextInt(3, 17), 0);
+ }
+ }
+
+ Sqlite openDb(String name){
+ final Sqlite db = Sqlite.open(name, Sqlite.OPEN_READWRITE|
+ Sqlite.OPEN_CREATE|
+ Sqlite.OPEN_EXRESCODE);
+ ++metrics.dbOpen;
+ return db;
+ }
+
+ Sqlite openDb(){ return openDb(":memory:"); }
+
+ void testOpenDb1(){
+ Sqlite db = openDb();
+ affirm( 0!=db.nativeHandle().getNativePointer() );
+ affirm( "main".equals( db.dbName(0) ) );
+ db.setMainDbName("foo");
+ affirm( "foo".equals( db.dbName(0) ) );
+ affirm( db.dbConfig(Sqlite.DBCONFIG_DEFENSIVE, true)
+ /* The underlying function has different mangled names in jdk8
+ vs jdk19, and this call is here to ensure that the build
+ fails if it cannot find both names. */ );
+ affirm( !db.dbConfig(Sqlite.DBCONFIG_DEFENSIVE, false) );
+ SqliteException ex = null;
+ try{ db.dbConfig(0, false); }
+ catch(SqliteException e){ ex = e; }
+ affirm( null!=ex );
+ ex = null;
+ db.close();
+ affirm( null==db.nativeHandle() );
+
+ try{ db = openDb("/no/such/dir/.../probably"); }
+ catch(SqliteException e){ ex = e; }
+ affirm( ex!=null );
+ affirm( ex.errcode() != 0 );
+ affirm( ex.extendedErrcode() != 0 );
+ affirm( ex.errorOffset() < 0 );
+ // there's no reliable way to predict what ex.systemErrno() might be
+ }
+
+ void testPrepare1(){
+ try (Sqlite db = openDb()) {
+ Sqlite.Stmt stmt = db.prepare("SELECT ?1");
+ Exception e = null;
+ affirm( null!=stmt.nativeHandle() );
+ affirm( db == stmt.getDb() );
+ 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( Sqlite.ROW==stmt.step(false) );
+ affirm( !stmt.step() );
+ affirm( 0 == stmt.finalizeStmt() );
+ affirm( null==stmt.nativeHandle() );
+
+ stmt = db.prepare("SELECT ?");
+ stmt.bindObject(1, db);
+ affirm( Sqlite.ROW == stmt.step(false) );
+ 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() );
+ }
+ }
+
+ void testUdfScalar(){
+ final ValueHolder xDestroyCalled = new ValueHolder<>(0);
+ try (Sqlite db = openDb()) {
+ execSql(db, "create table t(a); insert into t(a) values(1),(2),(3)");
+ final ValueHolder vh = new ValueHolder<>(0);
+ final ScalarFunction f = new ScalarFunction(){
+ public void xFunc(SqlFunction.Arguments args){
+ affirm( db == args.getDb() );
+ for( SqlFunction.Arguments.Arg arg : args ){
+ vh.value += arg.getInt();
+ }
+ args.resultInt(vh.value);
+ }
+ public void xDestroy(){
+ ++xDestroyCalled.value;
+ }
+ };
+ db.createFunction("myfunc", -1, f);
+ Sqlite.Stmt q = db.prepare("select myfunc(1,2,3)");
+ affirm( q.step() );
+ affirm( 6 == vh.value );
+ affirm( 6 == q.columnInt(0) );
+ q.finalizeStmt();
+ affirm( 0 == xDestroyCalled.value );
+ vh.value = 0;
+ q = db.prepare("select myfunc(-1,-2,-3)");
+ affirm( q.step() );
+ affirm( -6 == vh.value );
+ affirm( -6 == q.columnInt(0) );
+ affirm( 0 == xDestroyCalled.value );
+ q.finalizeStmt();
+ }
+ affirm( 1 == xDestroyCalled.value );
+ }
+
+ void testUdfAggregate(){
+ final ValueHolder xDestroyCalled = 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(){
+ public void xStep(SqlFunction.Arguments args){
+ final ValueHolder 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);
+ }
+ public void xDestroy(){
+ ++xDestroyCalled.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( 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(){
+ //! 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 testKeyword(){
+ final int n = Sqlite.keywordCount();
+ affirm( n>0 );
+ affirm( !Sqlite.keywordCheck("_nope_") );
+ affirm( Sqlite.keywordCheck("seLect") );
+ affirm( null!=Sqlite.keywordName(0) );
+ affirm( null!=Sqlite.keywordName(n-1) );
+ affirm( null==Sqlite.keywordName(n) );
+ }
+
+
+ private void testExplain(){
+ final Sqlite db = openDb();
+ Sqlite.Stmt q = db.prepare("SELECT 1");
+ affirm( 0 == q.isExplain() );
+ q.explain(0);
+ affirm( 0 == q.isExplain() );
+ q.explain(1);
+ affirm( 1 == q.isExplain() );
+ q.explain(2);
+ affirm( 2 == q.isExplain() );
+ Exception ex = null;
+ try{
+ q.explain(-1);
+ }catch(Exception e){
+ ex = e;
+ }
+ affirm( ex instanceof SqliteException );
+ q.finalizeStmt();
+ db.close();
+ }
+
+
+ private void testTrace(){
+ final Sqlite db = openDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ /* Ensure that characters outside of the UTF BMP survive the trip
+ from Java to sqlite3 and back to Java. (At no small efficiency
+ penalty.) */
+ final String nonBmpChar = "😃";
+ db.trace(
+ Sqlite.TRACE_ALL,
+ new Sqlite.TraceCallback(){
+ @Override public void call(int traceFlag, Object pNative, Object x){
+ ++counter.value;
+ //outln("TRACE "+traceFlag+" pNative = "+pNative.getClass().getName());
+ switch(traceFlag){
+ case Sqlite.TRACE_STMT:
+ affirm(pNative instanceof Sqlite.Stmt);
+ //outln("TRACE_STMT sql = "+x);
+ affirm(x instanceof String);
+ affirm( ((String)x).indexOf(nonBmpChar) > 0 );
+ break;
+ case Sqlite.TRACE_PROFILE:
+ affirm(pNative instanceof Sqlite.Stmt);
+ affirm(x instanceof Long);
+ //outln("TRACE_PROFILE time = "+x);
+ break;
+ case Sqlite.TRACE_ROW:
+ affirm(pNative instanceof Sqlite.Stmt);
+ affirm(null == x);
+ //outln("TRACE_ROW = "+sqlite3_column_text16((sqlite3_stmt)pNative, 0));
+ break;
+ case Sqlite.TRACE_CLOSE:
+ affirm(pNative instanceof Sqlite);
+ affirm(null == x);
+ break;
+ default:
+ affirm(false /*cannot happen*/);
+ break;
+ }
+ }
+ });
+ execSql(db, "SELECT coalesce(null,null,'"+nonBmpChar+"'); "+
+ "SELECT 'w"+nonBmpChar+"orld'");
+ affirm( 6 == counter.value );
+ db.close();
+ affirm( 7 == counter.value );
+ }
+
+ private void testStatus(){
+ final Sqlite db = openDb();
+ execSql(db, "create table t(a); insert into t values(1),(2),(3)");
+
+ Sqlite.Status s = Sqlite.libStatus(Sqlite.STATUS_MEMORY_USED, false);
+ affirm( s.current > 0 );
+ affirm( s.peak >= s.current );
+
+ s = db.status(Sqlite.DBSTATUS_SCHEMA_USED, false);
+ affirm( s.current > 0 );
+ affirm( s.peak == 0 /* always 0 for SCHEMA_USED */ );
+
+ db.close();
+ }
+
+ @SingleThreadOnly /* because multiple threads legitimately make these
+ results unpredictable */
+ private synchronized void testAutoExtension(){
+ final ValueHolder val = new ValueHolder<>(0);
+ final ValueHolder toss = new ValueHolder<>(null);
+ final Sqlite.AutoExtension ax = new Sqlite.AutoExtension(){
+ @Override public void call(Sqlite db){
+ ++val.value;
+ if( null!=toss.value ){
+ throw new RuntimeException(toss.value);
+ }
+ }
+ };
+ Sqlite.addAutoExtension(ax);
+ openDb().close();
+ affirm( 1==val.value );
+ openDb().close();
+ affirm( 2==val.value );
+ Sqlite.clearAutoExtensions();
+ openDb().close();
+ affirm( 2==val.value );
+
+ Sqlite.addAutoExtension( ax );
+ Sqlite.addAutoExtension( ax ); // Must not add a second entry
+ Sqlite.addAutoExtension( ax ); // or a third one
+ openDb().close();
+ affirm( 3==val.value );
+
+ Sqlite db = openDb();
+ affirm( 4==val.value );
+ execSql(db, "ATTACH ':memory:' as foo");
+ affirm( 4==val.value, "ATTACH uses the same connection, not sub-connections." );
+ db.close();
+ db = null;
+
+ Sqlite.removeAutoExtension(ax);
+ openDb().close();
+ affirm( 4==val.value );
+ Sqlite.addAutoExtension(ax);
+ Exception err = null;
+ toss.value = "Throwing from auto_extension.";
+ try{
+ openDb();
+ }catch(Exception e){
+ err = e;
+ }
+ affirm( err!=null );
+ affirm( err.getMessage().indexOf(toss.value)>=0 );
+ toss.value = null;
+
+ val.value = 0;
+ final Sqlite.AutoExtension ax2 = new Sqlite.AutoExtension(){
+ @Override public void call(Sqlite db){
+ ++val.value;
+ }
+ };
+ Sqlite.addAutoExtension(ax2);
+ openDb().close();
+ affirm( 2 == val.value );
+ Sqlite.removeAutoExtension(ax);
+ openDb().close();
+ affirm( 3 == val.value );
+ Sqlite.addAutoExtension(ax);
+ openDb().close();
+ affirm( 5 == val.value );
+ Sqlite.removeAutoExtension(ax2);
+ openDb().close();
+ affirm( 6 == val.value );
+ Sqlite.addAutoExtension(ax2);
+ openDb().close();
+ affirm( 8 == val.value );
+
+ Sqlite.clearAutoExtensions();
+ openDb().close();
+ affirm( 8 == val.value );
+ }
+
+ private void testBackup(){
+ final Sqlite dbDest = openDb();
+
+ try (Sqlite dbSrc = openDb()) {
+ execSql(dbSrc, new String[]{
+ "pragma page_size=512; VACUUM;",
+ "create table t(a);",
+ "insert into t(a) values(1),(2),(3);"
+ });
+ Exception e = null;
+ try {
+ dbSrc.initBackup("main",dbSrc,"main");
+ }catch(Exception x){
+ e = x;
+ }
+ affirm( e instanceof SqliteException );
+ e = null;
+ try (Sqlite.Backup b = dbDest.initBackup("main",dbSrc,"main")) {
+ affirm( null!=b );
+ int rc;
+ while( Sqlite.DONE!=(rc = b.step(1)) ){
+ affirm( 0==rc );
+ }
+ affirm( b.pageCount() > 0 );
+ b.finish();
+ }
+ }
+
+ try (Sqlite.Stmt q = dbDest.prepare("SELECT sum(a) from t")) {
+ q.step();
+ affirm( q.columnInt(0) == 6 );
+ }
+ dbDest.close();
+ }
+
+ private void testCollation(){
+ final Sqlite db = openDb();
+ execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+ final Sqlite.Collation myCollation = new Sqlite.Collation() {
+ private String myState =
+ "this is local state. There is much like it, but this is mine.";
+ @Override
+ // Reverse-sorts its inputs...
+ public int call(byte[] lhs, byte[] rhs){
+ int len = lhs.length > rhs.length ? rhs.length : lhs.length;
+ int c = 0, i = 0;
+ for(i = 0; i < len; ++i){
+ c = lhs[i] - rhs[i];
+ if(0 != c) break;
+ }
+ if(0==c){
+ if(i < lhs.length) c = 1;
+ else if(i < rhs.length) c = -1;
+ }
+ return -c;
+ }
+ };
+ final Sqlite.CollationNeeded collLoader = new Sqlite.CollationNeeded(){
+ @Override
+ public void call(Sqlite dbArg, int eTextRep, String collationName){
+ affirm(dbArg == db);
+ db.createCollation("reversi", eTextRep, myCollation);
+ }
+ };
+ db.onCollationNeeded(collLoader);
+ Sqlite.Stmt stmt = db.prepare("SELECT a FROM t ORDER BY a COLLATE reversi");
+ int counter = 0;
+ while( stmt.step() ){
+ final String val = stmt.columnText16(0);
+ ++counter;
+ switch(counter){
+ case 1: affirm("c".equals(val)); break;
+ case 2: affirm("b".equals(val)); break;
+ case 3: affirm("a".equals(val)); break;
+ }
+ }
+ affirm(3 == counter);
+ stmt.finalizeStmt();
+ stmt = db.prepare("SELECT a FROM t ORDER BY a");
+ counter = 0;
+ while( stmt.step() ){
+ final String val = stmt.columnText16(0);
+ ++counter;
+ //outln("Non-REVERSI'd row#"+counter+": "+val);
+ switch(counter){
+ case 3: affirm("c".equals(val)); break;
+ case 2: affirm("b".equals(val)); break;
+ case 1: affirm("a".equals(val)); break;
+ }
+ }
+ affirm(3 == counter);
+ stmt.finalizeStmt();
+ db.onCollationNeeded(null);
+ db.close();
+ }
+
+ @SingleThreadOnly /* because threads inherently break this test */
+ private void testBusy(){
+ final String dbName = "_busy-handler.db";
+ try{
+ Sqlite db1 = openDb(dbName);
+ ++metrics.dbOpen;
+ execSql(db1, "CREATE TABLE IF NOT EXISTS t(a)");
+ Sqlite db2 = openDb(dbName);
+ ++metrics.dbOpen;
+
+ final ValueHolder xBusyCalled = new ValueHolder<>(0);
+ Sqlite.BusyHandler handler = new Sqlite.BusyHandler(){
+ @Override public int call(int n){
+ return n > 2 ? 0 : ++xBusyCalled.value;
+ }
+ };
+ db2.setBusyHandler(handler);
+
+ // Force a locked condition...
+ execSql(db1, "BEGIN EXCLUSIVE");
+ int rc = 0;
+ SqliteException ex = null;
+ try{
+ db2.prepare("SELECT * from t");
+ }catch(SqliteException x){
+ ex = x;
+ }
+ affirm( null!=ex );
+ affirm( Sqlite.BUSY == ex.errcode() );
+ affirm( 3 == xBusyCalled.value );
+ db1.close();
+ db2.close();
+ }finally{
+ try{(new java.io.File(dbName)).delete();}
+ catch(Exception e){/* ignore */}
+ }
+ }
+
+ private void testCommitHook(){
+ final Sqlite db = openDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ final ValueHolder hookResult = new ValueHolder<>(0);
+ final Sqlite.CommitHook theHook = new Sqlite.CommitHook(){
+ @Override public int call(){
+ ++counter.value;
+ return hookResult.value;
+ }
+ };
+ Sqlite.CommitHook oldHook = db.setCommitHook(theHook);
+ affirm( null == oldHook );
+ execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+ affirm( 2 == counter.value );
+ execSql(db, "BEGIN; SELECT 1; SELECT 2; COMMIT;");
+ affirm( 2 == counter.value /* NOT invoked if no changes are made */ );
+ execSql(db, "BEGIN; update t set a='d' where a='c'; COMMIT;");
+ affirm( 3 == counter.value );
+ oldHook = db.setCommitHook(theHook);
+ affirm( theHook == oldHook );
+ execSql(db, "BEGIN; update t set a='e' where a='d'; COMMIT;");
+ affirm( 4 == counter.value );
+ oldHook = db.setCommitHook(null);
+ affirm( theHook == oldHook );
+ execSql(db, "BEGIN; update t set a='f' where a='e'; COMMIT;");
+ affirm( 4 == counter.value );
+ oldHook = db.setCommitHook(null);
+ affirm( null == oldHook );
+ execSql(db, "BEGIN; update t set a='g' where a='f'; COMMIT;");
+ affirm( 4 == counter.value );
+
+ final Sqlite.CommitHook newHook = new Sqlite.CommitHook(){
+ @Override public int call(){return 0;}
+ };
+ oldHook = db.setCommitHook(newHook);
+ affirm( null == oldHook );
+ execSql(db, "BEGIN; update t set a='h' where a='g'; COMMIT;");
+ affirm( 4 == counter.value );
+ oldHook = db.setCommitHook(theHook);
+ affirm( newHook == oldHook );
+ execSql(db, "BEGIN; update t set a='i' where a='h'; COMMIT;");
+ affirm( 5 == counter.value );
+ hookResult.value = Sqlite.ERROR;
+ int rc = execSql(db, false, "BEGIN; update t set a='j' where a='i'; COMMIT;");
+ affirm( Sqlite.CONSTRAINT_COMMITHOOK == rc );
+ affirm( 6 == counter.value );
+ db.close();
+ }
+
+ private void testRollbackHook(){
+ final Sqlite db = openDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ final Sqlite.RollbackHook theHook = new Sqlite.RollbackHook(){
+ @Override public void call(){
+ ++counter.value;
+ }
+ };
+ Sqlite.RollbackHook oldHook = db.setRollbackHook(theHook);
+ affirm( null == oldHook );
+ execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+ affirm( 0 == counter.value );
+ execSql(db, false, "BEGIN; SELECT 1; SELECT 2; ROLLBACK;");
+ affirm( 1 == counter.value /* contra to commit hook, is invoked if no changes are made */ );
+
+ final Sqlite.RollbackHook newHook = new Sqlite.RollbackHook(){
+ @Override public void call(){}
+ };
+ oldHook = db.setRollbackHook(newHook);
+ affirm( theHook == oldHook );
+ execSql(db, false, "BEGIN; SELECT 1; ROLLBACK;");
+ affirm( 1 == counter.value );
+ oldHook = db.setRollbackHook(theHook);
+ affirm( newHook == oldHook );
+ execSql(db, false, "BEGIN; SELECT 1; ROLLBACK;");
+ affirm( 2 == counter.value );
+ int rc = execSql(db, false, "BEGIN; SELECT 1; ROLLBACK;");
+ affirm( 0 == rc );
+ affirm( 3 == counter.value );
+ db.close();
+ }
+
+ private void testUpdateHook(){
+ final Sqlite db = openDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ final ValueHolder expectedOp = new ValueHolder<>(0);
+ final Sqlite.UpdateHook theHook = new Sqlite.UpdateHook(){
+ @Override
+ public void call(int opId, String dbName, String tableName, long rowId){
+ ++counter.value;
+ if( 0!=expectedOp.value ){
+ affirm( expectedOp.value == opId );
+ }
+ }
+ };
+ Sqlite.UpdateHook oldHook = db.setUpdateHook(theHook);
+ affirm( null == oldHook );
+ expectedOp.value = Sqlite.INSERT;
+ execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+ affirm( 3 == counter.value );
+ expectedOp.value = Sqlite.UPDATE;
+ execSql(db, "update t set a='d' where a='c';");
+ affirm( 4 == counter.value );
+ oldHook = db.setUpdateHook(theHook);
+ affirm( theHook == oldHook );
+ expectedOp.value = Sqlite.DELETE;
+ execSql(db, "DELETE FROM t where a='d'");
+ affirm( 5 == counter.value );
+ oldHook = db.setUpdateHook(null);
+ affirm( theHook == oldHook );
+ execSql(db, "update t set a='e' where a='b';");
+ affirm( 5 == counter.value );
+ oldHook = db.setUpdateHook(null);
+ affirm( null == oldHook );
+
+ final Sqlite.UpdateHook newHook = new Sqlite.UpdateHook(){
+ @Override public void call(int opId, String dbName, String tableName, long rowId){
+ }
+ };
+ oldHook = db.setUpdateHook(newHook);
+ affirm( null == oldHook );
+ execSql(db, "update t set a='h' where a='a'");
+ affirm( 5 == counter.value );
+ oldHook = db.setUpdateHook(theHook);
+ affirm( newHook == oldHook );
+ expectedOp.value = Sqlite.UPDATE;
+ execSql(db, "update t set a='i' where a='h'");
+ affirm( 6 == counter.value );
+ db.close();
+ }
+
+ private void testProgress(){
+ final Sqlite db = openDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ db.setProgressHandler(1, new Sqlite.ProgressHandler(){
+ @Override public int call(){
+ ++counter.value;
+ return 0;
+ }
+ });
+ execSql(db, "SELECT 1; SELECT 2;");
+ affirm( counter.value > 0 );
+ int nOld = counter.value;
+ db.setProgressHandler(0, null);
+ execSql(db, "SELECT 1; SELECT 2;");
+ affirm( nOld == counter.value );
+ db.close();
+ }
+
+ private void testAuthorizer(){
+ final Sqlite db = openDb();
+ final ValueHolder counter = new ValueHolder<>(0);
+ final ValueHolder authRc = new ValueHolder<>(0);
+ final Sqlite.Authorizer auth = new Sqlite.Authorizer(){
+ public int call(int op, String s0, String s1, String s2, String s3){
+ ++counter.value;
+ //outln("xAuth(): "+s0+" "+s1+" "+s2+" "+s3);
+ return authRc.value;
+ }
+ };
+ execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
+ db.setAuthorizer(auth);
+ execSql(db, "UPDATE t SET a=1");
+ affirm( 1 == counter.value );
+ authRc.value = Sqlite.DENY;
+ int rc = execSql(db, false, "UPDATE t SET a=2");
+ affirm( Sqlite.AUTH==rc );
+ db.setAuthorizer(null);
+ rc = execSql(db, false, "UPDATE t SET a=2");
+ affirm( 0==rc );
+ db.close();
+ }
+
+ private void testBlobOpen(){
+ final Sqlite db = openDb();
+
+ execSql(db, "CREATE TABLE T(a BLOB);"
+ +"INSERT INTO t(rowid,a) VALUES(1, 'def'),(2, 'XYZ');"
+ );
+ Sqlite.Blob b = db.blobOpen("main", "t", "a",
+ db.lastInsertRowId(), true);
+ affirm( 3==b.bytes() );
+ b.write(new byte[] {100, 101, 102 /*"DEF"*/}, 0);
+ b.close();
+ Sqlite.Stmt stmt = db.prepare("SELECT length(a), a FROM t ORDER BY a");
+ affirm( stmt.step() );
+ affirm( 3 == stmt.columnInt(0) );
+ affirm( "def".equals(stmt.columnText16(1)) );
+ stmt.finalizeStmt();
+
+ b = db.blobOpen("main", "t", "a", db.lastInsertRowId(), false);
+ final byte[] tgt = new byte[3];
+ b.read( tgt, 0 );
+ affirm( 100==tgt[0] && 101==tgt[1] && 102==tgt[2], "DEF" );
+ execSql(db,"UPDATE t SET a=zeroblob(10) WHERE rowid=2");
+ b.close();
+ b = db.blobOpen("main", "t", "a", db.lastInsertRowId(), true);
+ byte[] bw = new byte[]{
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
+ };
+ b.write(bw, 0);
+ byte[] br = new byte[10];
+ b.read(br, 0);
+ for( int i = 0; i < br.length; ++i ){
+ affirm(bw[i] == br[i]);
+ }
+ b.close();
+ db.close();
+ }
+
+ void testPrepareMulti(){
+ final ValueHolder fCount = new ValueHolder<>(0);
+ final ValueHolder mCount = new ValueHolder<>(0);
+ try (Sqlite db = openDb()) {
+ execSql(db, "create table t(a); insert into t(a) values(1),(2),(3)");
+ db.createFunction("counter", -1, new ScalarFunction(){
+ @Override public void xFunc(SqlFunction.Arguments args){
+ ++fCount.value;
+ args.resultNull();
+ }
+ }
+ );
+ final Sqlite.PrepareMulti pm = new Sqlite.PrepareMultiFinalize(
+ new Sqlite.PrepareMulti() {
+ @Override public void call(Sqlite.Stmt q){
+ ++mCount.value;
+ while(q.step()){}
+ }
+ }
+ );
+ final String sql = "select counter(*) from t;"+
+ "select counter(*) from t; /* comment */"+
+ "select counter(*) from t; -- comment\n"
+ ;
+ db.prepareMulti(sql, pm);
+ }
+ affirm( 3 == mCount.value );
+ affirm( 9 == fCount.value );
+ }
+
+
+ /* Copy/paste/rename this to add new tests. */
+ private void _testTemplate(){
+ try (Sqlite db = openDb()) {
+ Sqlite.Stmt stmt = db.prepare("SELECT 1");
+ stmt.finalizeStmt();
+ }
+ }
+
+ private void runTests(boolean fromThread) throws Exception {
+ List mlist = testMethods;
+ affirm( null!=mlist );
+ if( shuffle ){
+ mlist = new ArrayList<>( testMethods.subList(0, testMethods.size()) );
+ java.util.Collections.shuffle(mlist);
+ }
+ if( (!fromThread && listRunTests>0) || listRunTests>1 ){
+ synchronized(this.getClass()){
+ if( !fromThread ){
+ out("Initial test"," list: ");
+ for(java.lang.reflect.Method m : testMethods){
+ out(m.getName()+" ");
+ }
+ outln();
+ outln("(That list excludes some which are hard-coded to run.)");
+ }
+ out("Running"," tests: ");
+ for(java.lang.reflect.Method m : mlist){
+ out(m.getName()+" ");
+ }
+ outln();
+ }
+ }
+ for(java.lang.reflect.Method m : mlist){
+ nap();
+ try{
+ m.invoke(this);
+ }catch(java.lang.reflect.InvocationTargetException e){
+ outln("FAILURE: ",m.getName(),"(): ", e.getCause());
+ throw e;
+ }
+ }
+ synchronized( this.getClass() ){
+ ++nTestRuns;
+ }
+ }
+
+ public void run() {
+ try {
+ runTests(0!=this.tId);
+ }catch(Exception e){
+ synchronized( listErrors ){
+ listErrors.add(e);
+ }
+ }finally{
+ Sqlite.uncacheThread();
+ }
+ }
+
+ /**
+ Runs the basic sqlite3 JNI binding sanity-check suite.
+
+ CLI flags:
+
+ -q|-quiet: disables most test output.
+
+ -t|-thread N: runs the tests in N threads
+ concurrently. Default=1.
+
+ -r|-repeat N: repeats the tests in a loop N times, each one
+ consisting of the -thread value's threads.
+
+ -shuffle: randomizes the order of most of the test functions.
+
+ -naps: sleep small random intervals between tests in order to add
+ some chaos for cross-thread contention.
+
+ -list-tests: outputs the list of tests being run, minus some
+ which are hard-coded. In multi-threaded mode, use this twice to
+ to emit the list run by each thread (which may differ from the initial
+ list, in particular if -shuffle is used).
+
+ -fail: forces an exception to be thrown during the test run. Use
+ with -shuffle to make its appearance unpredictable.
+
+ -v: emit some developer-mode info at the end.
+ */
+ public static void main(String[] args) throws Exception {
+ Integer nThread = 1;
+ boolean doSomethingForDev = false;
+ Integer nRepeat = 1;
+ boolean forceFail = false;
+ boolean sqlLog = false;
+ boolean configLog = false;
+ boolean squelchTestOutput = false;
+ for( int i = 0; i < args.length; ){
+ String arg = args[i++];
+ if(arg.startsWith("-")){
+ arg = arg.replaceFirst("-+","");
+ if(arg.equals("v")){
+ doSomethingForDev = true;
+ //listBoundMethods();
+ }else if(arg.equals("t") || arg.equals("thread")){
+ nThread = Integer.parseInt(args[i++]);
+ }else if(arg.equals("r") || arg.equals("repeat")){
+ nRepeat = Integer.parseInt(args[i++]);
+ }else if(arg.equals("shuffle")){
+ shuffle = true;
+ }else if(arg.equals("list-tests")){
+ ++listRunTests;
+ }else if(arg.equals("fail")){
+ forceFail = true;
+ }else if(arg.equals("sqllog")){
+ sqlLog = true;
+ }else if(arg.equals("configlog")){
+ configLog = true;
+ }else if(arg.equals("naps")){
+ takeNaps = true;
+ }else if(arg.equals("q") || arg.equals("quiet")){
+ squelchTestOutput = true;
+ }else{
+ throw new IllegalArgumentException("Unhandled flag:"+arg);
+ }
+ }
+ }
+
+ if( sqlLog ){
+ if( Sqlite.compileOptionUsed("ENABLE_SQLLOG") ){
+ Sqlite.libConfigSqlLog( new Sqlite.ConfigSqlLog() {
+ @Override public void call(Sqlite db, String msg, int op){
+ switch(op){
+ case 0: outln("Opening db: ",db); break;
+ case 1: outln("SQL ",db,": ",msg); break;
+ case 2: outln("Closing db: ",db); break;
+ }
+ }
+ }
+ );
+ }else{
+ outln("WARNING: -sqllog is not active because library was built ",
+ "without SQLITE_ENABLE_SQLLOG.");
+ }
+ }
+ if( configLog ){
+ Sqlite.libConfigLog( new Sqlite.ConfigLog() {
+ @Override public void call(int code, String msg){
+ outln("ConfigLog: ",Sqlite.errstr(code),": ", msg);
+ };
+ }
+ );
+ }
+
+ quietMode = squelchTestOutput;
+ outln("If you just saw warning messages regarding CallStaticObjectMethod, ",
+ "you are very likely seeing the side effects of a known openjdk8 ",
+ "bug. It is unsightly but does not affect the library.");
+
+ {
+ // Build list of tests to run from the methods named test*().
+ testMethods = new ArrayList<>();
+ int nSkipped = 0;
+ for(final java.lang.reflect.Method m : Tester2.class.getDeclaredMethods()){
+ final String name = m.getName();
+ if( name.equals("testFail") ){
+ if( forceFail ){
+ testMethods.add(m);
+ }
+ }else if( !m.isAnnotationPresent( ManualTest.class ) ){
+ if( nThread>1 && m.isAnnotationPresent( SingleThreadOnly.class ) ){
+ if( 0==nSkipped++ ){
+ out("Skipping tests in multi-thread mode:");
+ }
+ out(" "+name+"()");
+ }else if( name.startsWith("test") ){
+ testMethods.add(m);
+ }
+ }
+ }
+ if( nSkipped>0 ) out("\n");
+ }
+
+ final long timeStart = System.currentTimeMillis();
+ outln("libversion_number: ",
+ Sqlite.libVersionNumber(),"\n",
+ Sqlite.libVersion(),"\n",Sqlite.libSourceId(),"\n",
+ "SQLITE_THREADSAFE=",CApi.sqlite3_threadsafe());
+ final boolean showLoopCount = (nRepeat>1 && nThread>1);
+ if( showLoopCount ){
+ outln("Running ",nRepeat," loop(s) with ",nThread," thread(s) each.");
+ }
+ if( takeNaps ) outln("Napping between tests is enabled.");
+ int nLoop = 0;
+ for( int n = 0; n < nRepeat; ++n ){
+ ++nLoop;
+ if( showLoopCount ) out((1==nLoop ? "" : " ")+nLoop);
+ if( nThread<=1 ){
+ new Tester2(0).runTests(false);
+ continue;
+ }
+ Tester2.mtMode = true;
+ final ExecutorService ex = Executors.newFixedThreadPool( nThread );
+ for( int i = 0; i < nThread; ++i ){
+ ex.submit( new Tester2(i), i );
+ }
+ ex.shutdown();
+ try{
+ ex.awaitTermination(nThread*200, java.util.concurrent.TimeUnit.MILLISECONDS);
+ ex.shutdownNow();
+ }catch (InterruptedException ie){
+ ex.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ if( !listErrors.isEmpty() ){
+ quietMode = false;
+ outln("TEST ERRORS:");
+ Exception err = null;
+ for( Exception e : listErrors ){
+ e.printStackTrace();
+ if( null==err ) err = e;
+ }
+ if( null!=err ) throw err;
+ }
+ }
+ if( showLoopCount ) outln();
+ quietMode = false;
+
+ final long timeEnd = System.currentTimeMillis();
+ outln("Tests done. Metrics across ",nTestRuns," total iteration(s):");
+ outln("\tAssertions checked: ",affirmCount);
+ outln("\tDatabases opened: ",metrics.dbOpen);
+ if( doSomethingForDev ){
+ CApi.sqlite3_jni_internal_details();
+ }
+ affirm( 0==Sqlite.libReleaseMemory(1) );
+ CApi.sqlite3_shutdown();
+ int nMethods = 0;
+ int nNatives = 0;
+ int nCanonical = 0;
+ final java.lang.reflect.Method[] declaredMethods =
+ CApi.class.getDeclaredMethods();
+ for(java.lang.reflect.Method m : declaredMethods){
+ final int mod = m.getModifiers();
+ if( 0!=(mod & java.lang.reflect.Modifier.STATIC) ){
+ final String name = m.getName();
+ if(name.startsWith("sqlite3_")){
+ ++nMethods;
+ if( 0!=(mod & java.lang.reflect.Modifier.NATIVE) ){
+ ++nNatives;
+ }
+ }
+ }
+ }
+ outln("\tCApi.sqlite3_*() methods: "+
+ nMethods+" total, with "+
+ nNatives+" native, "+
+ (nMethods - nNatives)+" Java"
+ );
+ outln("\tTotal test time = "
+ +(timeEnd - timeStart)+"ms");
+ }
+}
diff --git a/ext/jni/src/org/sqlite/jni/ValueHolder.java b/ext/jni/src/org/sqlite/jni/wrapper1/ValueHolder.java
similarity index 68%
rename from ext/jni/src/org/sqlite/jni/ValueHolder.java
rename to ext/jni/src/org/sqlite/jni/wrapper1/ValueHolder.java
index 7f6a463ba5..7549bb97b2 100644
--- a/ext/jni/src/org/sqlite/jni/ValueHolder.java
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/ValueHolder.java
@@ -1,5 +1,5 @@
/*
-** 2023-07-21
+** 2023-10-16
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
@@ -9,13 +9,13 @@
** May you share freely, never taking more than you give.
**
*************************************************************************
-** This file is part of the JNI bindings for the sqlite3 C API.
+** This file contains the ValueHolder utility class.
*/
-package org.sqlite.jni;
+package org.sqlite.jni.wrapper1;
/**
- A helper class which simply holds a single value. Its current use
- is for communicating values out of anonymous classes, as doing so
+ A helper class which simply holds a single value. Its primary use
+ is for communicating values out of anonymous callbacks, as doing so
requires a "final" reference.
*/
public class ValueHolder {
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 0000000000..a3905567d4
--- /dev/null
+++ b/ext/jni/src/org/sqlite/jni/wrapper1/WindowFunction.java
@@ -0,0 +1,42 @@
+/*
+** 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;
+
+/**
+ 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.
+*/
+public abstract class WindowFunction extends AggregateFunction {
+
+ /**
+ 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);
+
+}
diff --git a/ext/lsm1/lsm_vtab.c b/ext/lsm1/lsm_vtab.c
index bb1460297d..8c21923e1a 100644
--- a/ext/lsm1/lsm_vtab.c
+++ b/ext/lsm1/lsm_vtab.c
@@ -1061,6 +1061,11 @@ static sqlite3_module lsm1Module = {
lsm1Rollback, /* xRollback */
0, /* xFindMethod */
0, /* xRename */
+ 0, /* xSavepoint */
+ 0, /* xRelease */
+ 0, /* xRollbackTo */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
diff --git a/ext/misc/amatch.c b/ext/misc/amatch.c
index bafa43283a..dd9bee53d7 100644
--- a/ext/misc/amatch.c
+++ b/ext/misc/amatch.c
@@ -1475,7 +1475,8 @@ static sqlite3_module amatchModule = {
0, /* xSavepoint */
0, /* xRelease */
0, /* xRollbackTo */
- 0 /* xShadowName */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
#endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/misc/btreeinfo.c b/ext/misc/btreeinfo.c
index 22f8268139..02f8c0319c 100644
--- a/ext/misc/btreeinfo.c
+++ b/ext/misc/btreeinfo.c
@@ -411,7 +411,8 @@ int sqlite3BinfoRegister(sqlite3 *db){
0, /* xSavepoint */
0, /* xRelease */
0, /* xRollbackTo */
- 0 /* xShadowName */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
return sqlite3_create_module(db, "sqlite_btreeinfo", &binfo_module, 0);
}
diff --git a/ext/misc/carray.c b/ext/misc/carray.c
index 709c894f27..b1caa98c3f 100644
--- a/ext/misc/carray.c
+++ b/ext/misc/carray.c
@@ -409,6 +409,11 @@ static sqlite3_module carrayModule = {
0, /* xRollback */
0, /* xFindMethod */
0, /* xRename */
+ 0, /* xSavepoint */
+ 0, /* xRelease */
+ 0, /* xRollbackTo */
+ 0, /* xShadow */
+ 0 /* xIntegrity */
};
/*
diff --git a/ext/misc/closure.c b/ext/misc/closure.c
index db9b2b7394..79a5a21d1e 100644
--- a/ext/misc/closure.c
+++ b/ext/misc/closure.c
@@ -939,7 +939,8 @@ static sqlite3_module closureModule = {
0, /* xSavepoint */
0, /* xRelease */
0, /* xRollbackTo */
- 0 /* xShadowName */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
#endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/misc/completion.c b/ext/misc/completion.c
index d9e7b85972..987595a3d6 100644
--- a/ext/misc/completion.c
+++ b/ext/misc/completion.c
@@ -470,7 +470,8 @@ static sqlite3_module completionModule = {
0, /* xSavepoint */
0, /* xRelease */
0, /* xRollbackTo */
- 0 /* xShadowName */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
#endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/misc/csv.c b/ext/misc/csv.c
index 870a0cf60e..b38500f4b9 100644
--- a/ext/misc/csv.c
+++ b/ext/misc/csv.c
@@ -897,6 +897,11 @@ static sqlite3_module CsvModule = {
0, /* xRollback */
0, /* xFindMethod */
0, /* xRename */
+ 0, /* xSavepoint */
+ 0, /* xRelease */
+ 0, /* xRollbackTo */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
#ifdef SQLITE_TEST
@@ -929,6 +934,11 @@ static sqlite3_module CsvModuleFauxWrite = {
0, /* xRollback */
0, /* xFindMethod */
0, /* xRename */
+ 0, /* xSavepoint */
+ 0, /* xRelease */
+ 0, /* xRollbackTo */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
#endif /* SQLITE_TEST */
diff --git a/ext/misc/explain.c b/ext/misc/explain.c
index 0095194570..726af76b96 100644
--- a/ext/misc/explain.c
+++ b/ext/misc/explain.c
@@ -293,6 +293,7 @@ static sqlite3_module explainModule = {
0, /* xRelease */
0, /* xRollbackTo */
0, /* xShadowName */
+ 0 /* xIntegrity */
};
#endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/misc/fileio.c b/ext/misc/fileio.c
index 7cdbd5968f..70546adfca 100644
--- a/ext/misc/fileio.c
+++ b/ext/misc/fileio.c
@@ -983,6 +983,7 @@ static int fsdirRegister(sqlite3 *db){
0, /* xRelease */
0, /* xRollbackTo */
0, /* xShadowName */
+ 0 /* xIntegrity */
};
int rc = sqlite3_create_module(db, "fsdir", &fsdirModule, 0);
diff --git a/ext/misc/fossildelta.c b/ext/misc/fossildelta.c
index 6a597e0d7d..e638737d2b 100644
--- a/ext/misc/fossildelta.c
+++ b/ext/misc/fossildelta.c
@@ -1058,7 +1058,8 @@ static sqlite3_module deltaparsevtabModule = {
/* xSavepoint */ 0,
/* xRelease */ 0,
/* xRollbackTo */ 0,
- /* xShadowName */ 0
+ /* xShadowName */ 0,
+ /* xIntegrity */ 0
};
diff --git a/ext/misc/fuzzer.c b/ext/misc/fuzzer.c
index 65d9d8df69..92b7c0dae0 100644
--- a/ext/misc/fuzzer.c
+++ b/ext/misc/fuzzer.c
@@ -1165,6 +1165,11 @@ static sqlite3_module fuzzerModule = {
0, /* xRollback */
0, /* xFindMethod */
0, /* xRename */
+ 0, /* xSavepoint */
+ 0, /* xRelease */
+ 0, /* xRollbackTo */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
#endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/misc/memstat.c b/ext/misc/memstat.c
index 800a86e7a4..c56af9f297 100644
--- a/ext/misc/memstat.c
+++ b/ext/misc/memstat.c
@@ -396,6 +396,7 @@ static sqlite3_module memstatModule = {
0, /* xRelease */
0, /* xRollbackTo */
0, /* xShadowName */
+ 0 /* xIntegrity */
};
#endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/misc/mmapwarm.c b/ext/misc/mmapwarm.c
index 5afa47bf7a..851f8b0eb7 100644
--- a/ext/misc/mmapwarm.c
+++ b/ext/misc/mmapwarm.c
@@ -38,7 +38,7 @@ int sqlite3_mmap_warm(sqlite3 *db, const char *zDb){
int rc = SQLITE_OK;
char *zSql = 0;
int pgsz = 0;
- int nTotal = 0;
+ unsigned int nTotal = 0;
if( 0==sqlite3_get_autocommit(db) ) return SQLITE_MISUSE;
@@ -86,8 +86,8 @@ int sqlite3_mmap_warm(sqlite3 *db, const char *zDb){
rc = p->xFetch(pFd, pgsz*iPg, pgsz, (void**)&pMap);
if( rc!=SQLITE_OK || pMap==0 ) break;
- nTotal += pMap[0];
- nTotal += pMap[pgsz-1];
+ nTotal += (unsigned int)pMap[0];
+ nTotal += (unsigned int)pMap[pgsz-1];
rc = p->xUnfetch(pFd, pgsz*iPg, (void*)pMap);
if( rc!=SQLITE_OK ) break;
@@ -103,5 +103,6 @@ int sqlite3_mmap_warm(sqlite3 *db, const char *zDb){
if( rc==SQLITE_OK ) rc = rc2;
}
+ (void)nTotal;
return rc;
}
diff --git a/ext/misc/noop.c b/ext/misc/noop.c
index d3a58670c4..18c25e10f7 100644
--- a/ext/misc/noop.c
+++ b/ext/misc/noop.c
@@ -38,6 +38,24 @@ static void noopfunc(
sqlite3_result_value(context, argv[0]);
}
+/*
+** Implementation of the multitype_text() function.
+**
+** The function returns its argument. The result will always have a
+** TEXT value. But if the original input is numeric, it will also
+** have that numeric value.
+*/
+static void multitypeTextFunc(
+ sqlite3_context *context,
+ int argc,
+ sqlite3_value **argv
+){
+ assert( argc==1 );
+ (void)argc;
+ (void)sqlite3_value_text(argv[0]);
+ sqlite3_result_value(context, argv[0]);
+}
+
#ifdef _WIN32
__declspec(dllexport)
#endif
@@ -64,5 +82,9 @@ int sqlite3_noop_init(
rc = sqlite3_create_function(db, "noop_nd", 1,
SQLITE_UTF8,
0, noopfunc, 0, 0);
+ if( rc ) return rc;
+ rc = sqlite3_create_function(db, "multitype_text", 1,
+ SQLITE_UTF8,
+ 0, multitypeTextFunc, 0, 0);
return rc;
}
diff --git a/ext/misc/prefixes.c b/ext/misc/prefixes.c
index 3f053b7f1c..e6517e7195 100644
--- a/ext/misc/prefixes.c
+++ b/ext/misc/prefixes.c
@@ -248,7 +248,8 @@ static sqlite3_module prefixesModule = {
/* xSavepoint */ 0,
/* xRelease */ 0,
/* xRollbackTo */ 0,
- /* xShadowName */ 0
+ /* xShadowName */ 0,
+ /* xIntegrity */ 0
};
/*
diff --git a/ext/misc/qpvtab.c b/ext/misc/qpvtab.c
index fb0c155a27..b7c2a05126 100644
--- a/ext/misc/qpvtab.c
+++ b/ext/misc/qpvtab.c
@@ -439,7 +439,8 @@ static sqlite3_module qpvtabModule = {
/* xSavepoint */ 0,
/* xRelease */ 0,
/* xRollbackTo */ 0,
- /* xShadowName */ 0
+ /* xShadowName */ 0,
+ /* xIntegrity */ 0
};
#endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/misc/randomjson.c b/ext/misc/randomjson.c
index 3a6f545fe6..cacc4feb68 100644
--- a/ext/misc/randomjson.c
+++ b/ext/misc/randomjson.c
@@ -26,9 +26,14 @@
**
** .load ./randomjson
** SELECT random_json(1);
+** SELECT random_json5(1);
*/
-#include "sqlite3ext.h"
-SQLITE_EXTENSION_INIT1
+#ifdef SQLITE_STATIC_RANDOMJSON
+# include "sqlite3.h"
+#else
+# include "sqlite3ext.h"
+ SQLITE_EXTENSION_INIT1
+#endif
#include
#include
#include
@@ -51,17 +56,18 @@ static unsigned int prngInt(Prng *p){
return p->x ^ p->y;
}
-static const char *azJsonAtoms[] = {
- /* JSON /* JSON-5 */
+static char *azJsonAtoms[] = {
+ /* JSON JSON-5 */
"0", "0",
"1", "1",
"-1", "-1",
"2", "+2",
- "3", "3",
- "2.5", "2.5",
+ "3DDDD", "3DDDD",
+ "2.5DD", "2.5DD",
"0.75", ".75",
"-4.0e2", "-4.e2",
"5.0e-3", "+5e-3",
+ "6.DDe+0DD", "6.DDe+0DD",
"0", "0x0",
"512", "0x200",
"256", "+0x100",
@@ -73,12 +79,14 @@ static const char *azJsonAtoms[] = {
"-9.0e999", "-Infinity",
"9.0e999", "+Infinity",
"null", "NaN",
- "-0.0005123", "-0.0005123",
+ "-0.0005DD", "-0.0005DD",
"4.35e-3", "+4.35e-3",
"\"gem\\\"hay\"", "\"gem\\\"hay\"",
"\"icy'joy\"", "'icy\\'joy\'",
"\"keylog\"", "\"key\\\nlog\"",
"\"mix\\\\\\tnet\"", "\"mix\\\\\\tnet\"",
+ "\"oat\\r\\n\"", "\"oat\\r\\n\"",
+ "\"\\fpan\\b\"", "\"\\fpan\\b\"",
"{}", "{}",
"[]", "[]",
"[]", "[/*empty*/]",
@@ -89,19 +97,20 @@ static const char *azJsonAtoms[] = {
"\"day\"", "\"day\"",
"\"end\"", "'end'",
"\"fly\"", "\"fly\"",
+ "\"\\u00XX\\u00XX\"", "\"\\xXX\\xXX\"",
+ "\"y\\uXXXXz\"", "\"y\\uXXXXz\"",
"\"\"", "\"\"",
};
-static const char *azJsonTemplate[] = {
+static char *azJsonTemplate[] = {
/* JSON JSON-5 */
- "{\"a\":%,\"b\":%,\"c\":%}", "{a:%,b:%,c:%}",
+ "{\"a\":%,\"b\":%,\"cDD\":%}", "{a:%,b:%,cDD:%}",
"{\"a\":%,\"b\":%,\"c\":%,\"d\":%,\"e\":%}", "{a:%,b:%,c:%,d:%,e:%}",
- "{\"a\":%,\"b\":%,\"c\":%,\"d\":%,\"\":%}", "{a:%,b:%,c:%,d:%,\"\":%}",
+ "{\"a\":%,\"b\":%,\"c\":%,\"d\":%,\"\":%}", "{a:%,b:%,c:%,d:%,'':%}",
"{\"d\":%}", "{d:%}",
"{\"eeee\":%, \"ffff\":%}", "{eeee:% /*and*/, ffff:%}",
- "{\"$g\":%,\"_h_\":%}", "{$g:%,_h_:%,}",
+ "{\"$g\":%,\"_h_\":%,\"a b c d\":%}", "{$g:%,_h_:%,\"a b c d\":%}",
"{\"x\":%,\n \"y\":%}", "{\"x\":%,\n \"y\":%}",
- "{\"a b c d\":%,\"e\":%,\"f\":%,\"x\":%,\"y\":%}",
- "{\"a b c d\":%,e:%,f:%,x:%,y:%}",
+ "{\"\\u00XX\":%,\"\\uXXXX\":%}", "{\"\\xXX\":%,\"\\uXXXX\":%}",
"{\"Z\":%}", "{Z:%,}",
"[%]", "[%,]",
"[%,%]", "[%,%]",
@@ -122,15 +131,13 @@ static void jsonExpand(
unsigned int r /* Growth probability 0..1000. 0 means no growth */
){
unsigned int i, j, k;
- const char *z;
+ char *z;
+ char *zX;
size_t n;
+ char zBuf[200];
j = 0;
- if( zSrc==0 ){
- k = prngInt(p)%(count(azJsonTemplate)/2);
- k = k*2 + eType;
- zSrc = azJsonTemplate[k];
- }
+ if( zSrc==0 ) zSrc = "%";
if( strlen(zSrc)>=STRSZ/10 ) r = 0;
for(i=0; zSrc[i]; i++){
if( zSrc[i]!='%' ){
@@ -149,9 +156,36 @@ static void jsonExpand(
z = azJsonTemplate[k];
}
n = strlen(z);
+ if( (zX = strstr(z,"XX"))!=0 ){
+ unsigned int y = prngInt(p);
+ if( (y&0xff)==((y>>8)&0xff) ) y += 0x100;
+ while( (y&0xff)==((y>>16)&0xff) || ((y>>8)&0xff)==((y>>16)&0xff) ){
+ y += 0x10000;
+ }
+ memcpy(zBuf, z, n+1);
+ z = zBuf;
+ zX = strstr(z,"XX");
+ while( zX!=0 ){
+ zX[0] = "0123456789abcdef"[y%16]; y /= 16;
+ zX[1] = "0123456789abcdef"[y%16]; y /= 16;
+ zX = strstr(zX, "XX");
+ }
+ }else if( (zX = strstr(z,"DD"))!=0 ){
+ unsigned int y = prngInt(p);
+ memcpy(zBuf, z, n+1);
+ z = zBuf;
+ zX = strstr(z,"DD");
+ while( zX!=0 ){
+ zX[0] = "0123456789"[y%10]; y /= 10;
+ zX[1] = "0123456789"[y%10]; y /= 10;
+ zX = strstr(zX, "DD");
+ }
+ }
+ assert( strstr(z, "XX")==0 );
+ assert( strstr(z, "DD")==0 );
if( j+n=(sqlite3_uint64)LLONG_MAX ){
+static sqlite3_int64 genSeqMember(
+ sqlite3_int64 smBase,
+ sqlite3_int64 smStep,
+ sqlite3_uint64 ix
+){
+ static const sqlite3_uint64 mxI64 =
+ ((sqlite3_uint64)0x7fffffff)<<32 | 0xffffffff;
+ if( ix>=mxI64 ){
/* Get ix into signed i64 range. */
- ix -= (sqlite3_uint64)LLONG_MAX;
+ ix -= mxI64;
/* With 2's complement ALU, this next can be 1 step, but is split into
* 2 for UBSAN's satisfaction (and hypothetical 1's complement ALUs.) */
- smBase += (LLONG_MAX/2) * smStep;
- smBase += (LLONG_MAX - LLONG_MAX/2) * smStep;
+ smBase += (mxI64/2) * smStep;
+ smBase += (mxI64 - mxI64/2) * smStep;
}
/* Under UBSAN (or on 1's complement machines), must do this last term
* in steps to avoid the dreaded (and harmless) signed multiply overlow. */
@@ -557,7 +561,8 @@ static sqlite3_module seriesModule = {
0, /* xSavepoint */
0, /* xRelease */
0, /* xRollbackTo */
- 0 /* xShadowName */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
#endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/misc/spellfix.c b/ext/misc/spellfix.c
index 3b78d9f07d..a0c5aafd10 100644
--- a/ext/misc/spellfix.c
+++ b/ext/misc/spellfix.c
@@ -3009,6 +3009,11 @@ static sqlite3_module spellfix1Module = {
0, /* xRollback */
0, /* xFindMethod */
spellfix1Rename, /* xRename */
+ 0, /* xSavepoint */
+ 0, /* xRelease */
+ 0, /* xRollbackTo */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
/*
diff --git a/ext/misc/stmt.c b/ext/misc/stmt.c
index 4819cbe673..cc1aaa8493 100644
--- a/ext/misc/stmt.c
+++ b/ext/misc/stmt.c
@@ -314,6 +314,7 @@ static sqlite3_module stmtModule = {
0, /* xRelease */
0, /* xRollbackTo */
0, /* xShadowName */
+ 0 /* xIntegrity */
};
#endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/misc/templatevtab.c b/ext/misc/templatevtab.c
index d7efa2b40e..5865f5214b 100644
--- a/ext/misc/templatevtab.c
+++ b/ext/misc/templatevtab.c
@@ -249,7 +249,8 @@ static sqlite3_module templatevtabModule = {
/* xSavepoint */ 0,
/* xRelease */ 0,
/* xRollbackTo */ 0,
- /* xShadowName */ 0
+ /* xShadowName */ 0,
+ /* xIntegrity */ 0
};
diff --git a/ext/misc/totype.c b/ext/misc/totype.c
index 50d7f05d90..31c497a567 100644
--- a/ext/misc/totype.c
+++ b/ext/misc/totype.c
@@ -350,6 +350,20 @@ totype_atof_calc:
return z>=zEnd && nDigits>0 && eValid && nonNum==0;
}
+/*
+** Convert a floating point value to an integer. Or, if this cannot be
+** done in a way that avoids 'outside the range of representable values'
+** warnings from UBSAN, return 0.
+**
+** This function is a modified copy of internal SQLite function
+** sqlite3RealToI64().
+*/
+static sqlite3_int64 totypeDoubleToInt(double r){
+ if( r<-9223372036854774784.0 ) return 0;
+ if( r>+9223372036854774784.0 ) return 0;
+ return (sqlite3_int64)r;
+}
+
/*
** tointeger(X): If X is any value (integer, double, blob, or string) that
** can be losslessly converted into an integer, then make the conversion and
@@ -365,7 +379,7 @@ static void tointegerFunc(
switch( sqlite3_value_type(argv[0]) ){
case SQLITE_FLOAT: {
double rVal = sqlite3_value_double(argv[0]);
- sqlite3_int64 iVal = (sqlite3_int64)rVal;
+ sqlite3_int64 iVal = totypeDoubleToInt(rVal);
if( rVal==(double)iVal ){
sqlite3_result_int64(context, iVal);
}
@@ -440,7 +454,7 @@ static void torealFunc(
case SQLITE_INTEGER: {
sqlite3_int64 iVal = sqlite3_value_int64(argv[0]);
double rVal = (double)iVal;
- if( iVal==(sqlite3_int64)rVal ){
+ if( iVal==totypeDoubleToInt(rVal) ){
sqlite3_result_double(context, rVal);
}
break;
diff --git a/ext/misc/unionvtab.c b/ext/misc/unionvtab.c
index 6ac7ca6e95..506ad5ddba 100644
--- a/ext/misc/unionvtab.c
+++ b/ext/misc/unionvtab.c
@@ -1351,7 +1351,8 @@ static int createUnionVtab(sqlite3 *db){
0, /* xSavepoint */
0, /* xRelease */
0, /* xRollbackTo */
- 0 /* xShadowName */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
int rc;
diff --git a/ext/misc/vfsstat.c b/ext/misc/vfsstat.c
index 83a7a3df75..ba22115ab8 100644
--- a/ext/misc/vfsstat.c
+++ b/ext/misc/vfsstat.c
@@ -775,6 +775,11 @@ static sqlite3_module VfsStatModule = {
0, /* xRollback */
0, /* xFindMethod */
0, /* xRename */
+ 0, /* xSavepoint */
+ 0, /* xRelease */
+ 0, /* xRollbackTo */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
/*
diff --git a/ext/misc/vtablog.c b/ext/misc/vtablog.c
index 424b3457ff..e414e7afa7 100644
--- a/ext/misc/vtablog.c
+++ b/ext/misc/vtablog.c
@@ -493,6 +493,7 @@ static sqlite3_module vtablogModule = {
0, /* xRelease */
0, /* xRollbackTo */
0, /* xShadowName */
+ 0 /* xIntegrity */
};
#ifdef _WIN32
diff --git a/ext/misc/wholenumber.c b/ext/misc/wholenumber.c
index 03d6e6902e..4c955925da 100644
--- a/ext/misc/wholenumber.c
+++ b/ext/misc/wholenumber.c
@@ -254,6 +254,11 @@ static sqlite3_module wholenumberModule = {
0, /* xRollback */
0, /* xFindMethod */
0, /* xRename */
+ 0, /* xSavepoint */
+ 0, /* xRelease */
+ 0, /* xRollbackTo */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
#endif /* SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/misc/zipfile.c b/ext/misc/zipfile.c
index 4dbf80b197..db327809f8 100644
--- a/ext/misc/zipfile.c
+++ b/ext/misc/zipfile.c
@@ -29,6 +29,7 @@ SQLITE_EXTENSION_INIT1
#include
#include
#include
+#include
#include
@@ -2200,7 +2201,8 @@ static int zipfileRegister(sqlite3 *db){
0, /* xSavepoint */
0, /* xRelease */
0, /* xRollback */
- 0 /* xShadowName */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
int rc = sqlite3_create_module(db, "zipfile" , &zipfileModule, 0);
diff --git a/ext/recover/dbdata.c b/ext/recover/dbdata.c
index e3bec33d8d..b6cb26ecd3 100644
--- a/ext/recover/dbdata.c
+++ b/ext/recover/dbdata.c
@@ -582,6 +582,7 @@ static int dbdataNext(sqlite3_vtab_cursor *pCursor){
bNextPage = 1;
}else{
iOff += dbdataGetVarintU32(&pCsr->aPage[iOff], &nPayload);
+ if( nPayload>0x7fffff00 ) nPayload &= 0x3fff;
}
/* If this is a leaf intkey cell, load the rowid */
@@ -933,7 +934,8 @@ static int sqlite3DbdataRegister(sqlite3 *db){
0, /* xSavepoint */
0, /* xRelease */
0, /* xRollbackTo */
- 0 /* xShadowName */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
int rc = sqlite3_create_module(db, "sqlite_dbdata", &dbdata_module, 0);
diff --git a/ext/recover/test_recover.c b/ext/recover/test_recover.c
index 1c333df8e0..ba8ef6da11 100644
--- a/ext/recover/test_recover.c
+++ b/ext/recover/test_recover.c
@@ -236,7 +236,7 @@ static int test_sqlite3_recover_init(
zDb = Tcl_GetString(objv[2]);
if( zDb[0]=='\0' ) zDb = 0;
- pNew = ckalloc(sizeof(TestRecover));
+ pNew = (TestRecover*)ckalloc(sizeof(TestRecover));
if( bSql==0 ){
zUri = Tcl_GetString(objv[3]);
pNew->p = sqlite3_recover_init(db, zDb, zUri);
diff --git a/ext/repair/checkindex.c b/ext/repair/checkindex.c
index 080a51530d..5f6e646e44 100644
--- a/ext/repair/checkindex.c
+++ b/ext/repair/checkindex.c
@@ -907,6 +907,8 @@ static int ciInit(sqlite3 *db){
0, /* xSavepoint */
0, /* xRelease */
0, /* xRollbackTo */
+ 0, /* xShadowName */
+ 0 /* xIntegrity */
};
return sqlite3_create_module(db, "incremental_index_check", &cidx_module, 0);
}
diff --git a/ext/rtree/geopoly.c b/ext/rtree/geopoly.c
index 640cb86b20..3e9c2a2713 100644
--- a/ext/rtree/geopoly.c
+++ b/ext/rtree/geopoly.c
@@ -1252,24 +1252,28 @@ static int geopolyInit(
(void)pAux;
sqlite3_vtab_config(db, SQLITE_VTAB_CONSTRAINT_SUPPORT, 1);
+ sqlite3_vtab_config(db, SQLITE_VTAB_INNOCUOUS);
/* Allocate the sqlite3_vtab structure */
nDb = strlen(argv[1]);
nName = strlen(argv[2]);
- pRtree = (Rtree *)sqlite3_malloc64(sizeof(Rtree)+nDb+nName+2);
+ pRtree = (Rtree *)sqlite3_malloc64(sizeof(Rtree)+nDb+nName*2+8);
if( !pRtree ){
return SQLITE_NOMEM;
}
- memset(pRtree, 0, sizeof(Rtree)+nDb+nName+2);
+ memset(pRtree, 0, sizeof(Rtree)+nDb+nName*2+8);
pRtree->nBusy = 1;
pRtree->base.pModule = &rtreeModule;
pRtree->zDb = (char *)&pRtree[1];
pRtree->zName = &pRtree->zDb[nDb+1];
+ pRtree->zNodeName = &pRtree->zName[nName+1];
pRtree->eCoordType = RTREE_COORD_REAL32;
pRtree->nDim = 2;
pRtree->nDim2 = 4;
memcpy(pRtree->zDb, argv[1], nDb);
memcpy(pRtree->zName, argv[2], nName);
+ memcpy(pRtree->zNodeName, argv[2], nName);
+ memcpy(&pRtree->zNodeName[nName], "_node", 6);
/* Create/Connect to the underlying relational database schema. If
@@ -1683,7 +1687,6 @@ static int geopolyUpdate(
}
if( rc==SQLITE_OK ){
int rc2;
- pRtree->iReinsertHeight = -1;
rc = rtreeInsertCell(pRtree, pLeaf, &cell, 0);
rc2 = nodeRelease(pRtree, pLeaf);
if( rc==SQLITE_OK ){
@@ -1780,7 +1783,8 @@ static sqlite3_module geopolyModule = {
rtreeSavepoint, /* xSavepoint */
0, /* xRelease */
0, /* xRollbackTo */
- rtreeShadowName /* xShadowName */
+ rtreeShadowName, /* xShadowName */
+ rtreeIntegrity /* xIntegrity */
};
static int sqlite3_geopoly_init(sqlite3 *db){
diff --git a/ext/rtree/rtree.c b/ext/rtree/rtree.c
index 4e85cc8aec..11996d110c 100644
--- a/ext/rtree/rtree.c
+++ b/ext/rtree/rtree.c
@@ -166,6 +166,7 @@ struct Rtree {
int iDepth; /* Current depth of the r-tree structure */
char *zDb; /* Name of database containing r-tree table */
char *zName; /* Name of r-tree table */
+ char *zNodeName; /* Name of the %_node table */
u32 nBusy; /* Current number of users of this structure */
i64 nRowEst; /* Estimated number of rows in this table */
u32 nCursor; /* Number of open cursors */
@@ -178,7 +179,6 @@ struct Rtree {
** headed by the node (leaf nodes have RtreeNode.iNode==0).
*/
RtreeNode *pDeleted;
- int iReinsertHeight; /* Height of sub-trees Reinsert() has run on */
/* Blob I/O on xxx_node */
sqlite3_blob *pNodeBlob;
@@ -200,7 +200,7 @@ struct Rtree {
/* Statement for writing to the "aux:" fields, if there are any */
sqlite3_stmt *pWriteAux;
- RtreeNode *aHash[HASHSIZE]; /* Hash table of in-memory nodes. */
+ RtreeNode *aHash[HASHSIZE]; /* Hash table of in-memory nodes. */
};
/* Possible values for Rtree.eCoordType: */
@@ -475,15 +475,20 @@ struct RtreeMatchArg {
** -DSQLITE_RUNTIME_BYTEORDER=1 is set, then byte-order is determined
** at run-time.
*/
-#ifndef SQLITE_BYTEORDER
-# if defined(i386) || defined(__i386__) || defined(_M_IX86) || \
+#ifndef SQLITE_BYTEORDER /* Replicate changes at tag-20230904a */
+# if defined(__BYTE_ORDER__) && __BYTE_ORDER__==__ORDER_BIG_ENDIAN__
+# define SQLITE_BYTEORDER 4321
+# elif defined(__BYTE_ORDER__) && __BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__
+# define SQLITE_BYTEORDER 1234
+# elif defined(__BIG_ENDIAN__) && __BIG_ENDIAN__==1
+# define SQLITE_BYTEORDER 4321
+# elif defined(i386) || defined(__i386__) || defined(_M_IX86) || \
defined(__x86_64) || defined(__x86_64__) || defined(_M_X64) || \
defined(_M_AMD64) || defined(_M_ARM) || defined(__x86) || \
defined(__ARMEL__) || defined(__AARCH64EL__) || defined(_M_ARM64)
-# define SQLITE_BYTEORDER 1234
-# elif defined(sparc) || defined(__ppc__) || \
- defined(__ARMEB__) || defined(__AARCH64EB__)
-# define SQLITE_BYTEORDER 4321
+# define SQLITE_BYTEORDER 1234
+# elif defined(sparc) || defined(__ARMEB__) || defined(__AARCH64EB__)
+# define SQLITE_BYTEORDER 4321
# else
# define SQLITE_BYTEORDER 0
# endif
@@ -689,11 +694,9 @@ static RtreeNode *nodeNew(Rtree *pRtree, RtreeNode *pParent){
** Clear the Rtree.pNodeBlob object
*/
static void nodeBlobReset(Rtree *pRtree){
- if( pRtree->pNodeBlob && pRtree->inWrTrans==0 && pRtree->nCursor==0 ){
- sqlite3_blob *pBlob = pRtree->pNodeBlob;
- pRtree->pNodeBlob = 0;
- sqlite3_blob_close(pBlob);
- }
+ sqlite3_blob *pBlob = pRtree->pNodeBlob;
+ pRtree->pNodeBlob = 0;
+ sqlite3_blob_close(pBlob);
}
/*
@@ -712,7 +715,7 @@ static int nodeAcquire(
** increase its reference count and return it.
*/
if( (pNode = nodeHashLookup(pRtree, iNode))!=0 ){
- if( pParent && pParent!=pNode->pParent ){
+ if( pParent && ALWAYS(pParent!=pNode->pParent) ){
RTREE_IS_CORRUPT(pRtree);
return SQLITE_CORRUPT_VTAB;
}
@@ -732,14 +735,11 @@ static int nodeAcquire(
}
}
if( pRtree->pNodeBlob==0 ){
- char *zTab = sqlite3_mprintf("%s_node", pRtree->zName);
- if( zTab==0 ) return SQLITE_NOMEM;
- rc = sqlite3_blob_open(pRtree->db, pRtree->zDb, zTab, "data", iNode, 0,
+ rc = sqlite3_blob_open(pRtree->db, pRtree->zDb, pRtree->zNodeName,
+ "data", iNode, 0,
&pRtree->pNodeBlob);
- sqlite3_free(zTab);
}
if( rc ){
- nodeBlobReset(pRtree);
*ppNode = 0;
/* If unable to open an sqlite3_blob on the desired row, that can only
** be because the shadow tables hold erroneous data. */
@@ -799,6 +799,7 @@ static int nodeAcquire(
}
*ppNode = pNode;
}else{
+ nodeBlobReset(pRtree);
if( pNode ){
pRtree->nNodeRef--;
sqlite3_free(pNode);
@@ -943,6 +944,7 @@ static void nodeGetCoord(
int iCoord, /* Which coordinate to extract */
RtreeCoord *pCoord /* OUT: Space to write result to */
){
+ assert( iCellzData[12 + pRtree->nBytesPerCell*iCell + 4*iCoord], pCoord);
}
@@ -1132,7 +1134,9 @@ static int rtreeClose(sqlite3_vtab_cursor *cur){
sqlite3_finalize(pCsr->pReadAux);
sqlite3_free(pCsr);
pRtree->nCursor--;
- nodeBlobReset(pRtree);
+ if( pRtree->nCursor==0 && pRtree->inWrTrans==0 ){
+ nodeBlobReset(pRtree);
+ }
return SQLITE_OK;
}
@@ -1717,7 +1721,11 @@ static int rtreeRowid(sqlite3_vtab_cursor *pVtabCursor, sqlite_int64 *pRowid){
int rc = SQLITE_OK;
RtreeNode *pNode = rtreeNodeOfFirstSearchPoint(pCsr, &rc);
if( rc==SQLITE_OK && ALWAYS(p) ){
- *pRowid = nodeGetRowid(RTREE_OF_CURSOR(pCsr), pNode, p->iCell);
+ if( p->iCell>=NCELL(pNode) ){
+ rc = SQLITE_ABORT;
+ }else{
+ *pRowid = nodeGetRowid(RTREE_OF_CURSOR(pCsr), pNode, p->iCell);
+ }
}
return rc;
}
@@ -1735,6 +1743,7 @@ static int rtreeColumn(sqlite3_vtab_cursor *cur, sqlite3_context *ctx, int i){
if( rc ) return rc;
if( NEVER(p==0) ) return SQLITE_OK;
+ if( p->iCell>=NCELL(pNode) ) return SQLITE_ABORT;
if( i==0 ){
sqlite3_result_int64(ctx, nodeGetRowid(pRtree, pNode, p->iCell));
}else if( i<=pRtree->nDim2 ){
@@ -2077,8 +2086,12 @@ static int rtreeBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pIdxInfo){
pIdxInfo->idxNum = 2;
pIdxInfo->needToFreeIdxStr = 1;
- if( iIdx>0 && 0==(pIdxInfo->idxStr = sqlite3_mprintf("%s", zIdxStr)) ){
- return SQLITE_NOMEM;
+ if( iIdx>0 ){
+ pIdxInfo->idxStr = sqlite3_malloc( iIdx+1 );
+ if( pIdxInfo->idxStr==0 ){
+ return SQLITE_NOMEM;
+ }
+ memcpy(pIdxInfo->idxStr, zIdxStr, iIdx+1);
}
nRow = pRtree->nRowEst >> (iIdx/2);
@@ -2157,31 +2170,22 @@ static void cellUnion(Rtree *pRtree, RtreeCell *p1, RtreeCell *p2){
*/
static int cellContains(Rtree *pRtree, RtreeCell *p1, RtreeCell *p2){
int ii;
- int isInt = (pRtree->eCoordType==RTREE_COORD_INT32);
- for(ii=0; iinDim2; ii+=2){
- RtreeCoord *a1 = &p1->aCoord[ii];
- RtreeCoord *a2 = &p2->aCoord[ii];
- if( (!isInt && (a2[0].fa1[1].f))
- || ( isInt && (a2[0].ia1[1].i))
- ){
- return 0;
+ if( pRtree->eCoordType==RTREE_COORD_INT32 ){
+ for(ii=0; iinDim2; ii+=2){
+ RtreeCoord *a1 = &p1->aCoord[ii];
+ RtreeCoord *a2 = &p2->aCoord[ii];
+ if( a2[0].ia1[1].i ) return 0;
+ }
+ }else{
+ for(ii=0; iinDim2; ii+=2){
+ RtreeCoord *a1 = &p1->aCoord[ii];
+ RtreeCoord *a2 = &p2->aCoord[ii];
+ if( a2[0].fa1[1].f ) return 0;
}
}
return 1;
}
-/*
-** Return the amount cell p would grow by if it were unioned with pCell.
-*/
-static RtreeDValue cellGrowth(Rtree *pRtree, RtreeCell *p, RtreeCell *pCell){
- RtreeDValue area;
- RtreeCell cell;
- memcpy(&cell, p, sizeof(RtreeCell));
- area = cellArea(pRtree, &cell);
- cellUnion(pRtree, &cell, pCell);
- return (cellArea(pRtree, &cell)-area);
-}
-
static RtreeDValue cellOverlap(
Rtree *pRtree,
RtreeCell *p,
@@ -2228,38 +2232,52 @@ static int ChooseLeaf(
for(ii=0; rc==SQLITE_OK && ii<(pRtree->iDepth-iHeight); ii++){
int iCell;
sqlite3_int64 iBest = 0;
-
+ int bFound = 0;
RtreeDValue fMinGrowth = RTREE_ZERO;
RtreeDValue fMinArea = RTREE_ZERO;
-
int nCell = NCELL(pNode);
- RtreeCell cell;
RtreeNode *pChild = 0;
- RtreeCell *aCell = 0;
-
- /* Select the child node which will be enlarged the least if pCell
- ** is inserted into it. Resolve ties by choosing the entry with
- ** the smallest area.
+ /* First check to see if there is are any cells in pNode that completely
+ ** contains pCell. If two or more cells in pNode completely contain pCell
+ ** then pick the smallest.
*/
for(iCell=0; iCell1 ){
- int iLeft = 0;
- int iRight = 0;
-
- int nLeft = nIdx/2;
- int nRight = nIdx-nLeft;
- int *aLeft = aIdx;
- int *aRight = &aIdx[nLeft];
-
- SortByDistance(aLeft, nLeft, aDistance, aSpare);
- SortByDistance(aRight, nRight, aDistance, aSpare);
-
- memcpy(aSpare, aLeft, sizeof(int)*nLeft);
- aLeft = aSpare;
-
- while( iLeftnDim; iDim++){
- aCenterCoord[iDim] += DCOORD(aCell[ii].aCoord[iDim*2]);
- aCenterCoord[iDim] += DCOORD(aCell[ii].aCoord[iDim*2+1]);
- }
- }
- for(iDim=0; iDimnDim; iDim++){
- aCenterCoord[iDim] = (aCenterCoord[iDim]/(nCell*(RtreeDValue)2));
- }
-
- for(ii=0; iinDim; iDim++){
- RtreeDValue coord = (DCOORD(aCell[ii].aCoord[iDim*2+1]) -
- DCOORD(aCell[ii].aCoord[iDim*2]));
- aDistance[ii] += (coord-aCenterCoord[iDim])*(coord-aCenterCoord[iDim]);
- }
- }
-
- SortByDistance(aOrder, nCell, aDistance, aSpare);
- nodeZero(pRtree, pNode);
-
- for(ii=0; rc==SQLITE_OK && ii<(nCell-(RTREE_MINCELLS(pRtree)+1)); ii++){
- RtreeCell *p = &aCell[aOrder[ii]];
- nodeInsertCell(pRtree, pNode, p);
- if( p->iRowid==pCell->iRowid ){
- if( iHeight==0 ){
- rc = rowidWrite(pRtree, p->iRowid, pNode->iNode);
- }else{
- rc = parentWrite(pRtree, p->iRowid, pNode->iNode);
- }
- }
- }
- if( rc==SQLITE_OK ){
- rc = fixBoundingBox(pRtree, pNode);
- }
- for(; rc==SQLITE_OK && iiiNode currently contains
- ** the height of the sub-tree headed by the cell.
- */
- RtreeNode *pInsert;
- RtreeCell *p = &aCell[aOrder[ii]];
- rc = ChooseLeaf(pRtree, p, iHeight, &pInsert);
- if( rc==SQLITE_OK ){
- int rc2;
- rc = rtreeInsertCell(pRtree, pInsert, p, iHeight);
- rc2 = nodeRelease(pRtree, pInsert);
- if( rc==SQLITE_OK ){
- rc = rc2;
- }
- }
- }
-
- sqlite3_free(aCell);
- return rc;
-}
-
+
/*
** Insert cell pCell into node pNode. Node pNode is the head of a
** subtree iHeight high (leaf nodes have iHeight==0).
@@ -3008,12 +2854,7 @@ static int rtreeInsertCell(
}
}
if( nodeInsertCell(pRtree, pNode, pCell) ){
- if( iHeight<=pRtree->iReinsertHeight || pNode->iNode==1){
- rc = SplitNode(pRtree, pNode, pCell, iHeight);
- }else{
- pRtree->iReinsertHeight = iHeight;
- rc = Reinsert(pRtree, pNode, pCell, iHeight);
- }
+ rc = SplitNode(pRtree, pNode, pCell, iHeight);
}else{
rc = AdjustTree(pRtree, pNode, pCell);
if( ALWAYS(rc==SQLITE_OK) ){
@@ -3356,7 +3197,6 @@ static int rtreeUpdate(
}
if( rc==SQLITE_OK ){
int rc2;
- pRtree->iReinsertHeight = -1;
rc = rtreeInsertCell(pRtree, pLeaf, &cell, 0);
rc2 = nodeRelease(pRtree, pLeaf);
if( rc==SQLITE_OK ){
@@ -3386,7 +3226,7 @@ constraint:
static int rtreeBeginTransaction(sqlite3_vtab *pVtab){
Rtree *pRtree = (Rtree *)pVtab;
assert( pRtree->inWrTrans==0 );
- pRtree->inWrTrans++;
+ pRtree->inWrTrans = 1;
return SQLITE_OK;
}
@@ -3400,6 +3240,9 @@ static int rtreeEndTransaction(sqlite3_vtab *pVtab){
nodeBlobReset(pRtree);
return SQLITE_OK;
}
+static int rtreeRollback(sqlite3_vtab *pVtab){
+ return rtreeEndTransaction(pVtab);
+}
/*
** The xRename method for rtree module virtual tables.
@@ -3497,8 +3340,11 @@ static int rtreeShadowName(const char *zName){
return 0;
}
+/* Forward declaration */
+static int rtreeIntegrity(sqlite3_vtab*, const char*, const char*, int, char**);
+
static sqlite3_module rtreeModule = {
- 3, /* iVersion */
+ 4, /* iVersion */
rtreeCreate, /* xCreate - create a table */
rtreeConnect, /* xConnect - connect to an existing table */
rtreeBestIndex, /* xBestIndex - Determine search strategy */
@@ -3515,13 +3361,14 @@ static sqlite3_module rtreeModule = {
rtreeBeginTransaction, /* xBegin - begin transaction */
rtreeEndTransaction, /* xSync - sync transaction */
rtreeEndTransaction, /* xCommit - commit transaction */
- rtreeEndTransaction, /* xRollback - rollback transaction */
+ rtreeRollback, /* xRollback - rollback transaction */
0, /* xFindFunction - function overloading */
rtreeRename, /* xRename - rename the table */
rtreeSavepoint, /* xSavepoint */
0, /* xRelease */
0, /* xRollbackTo */
- rtreeShadowName /* xShadowName */
+ rtreeShadowName, /* xShadowName */
+ rtreeIntegrity /* xIntegrity */
};
static int rtreeSqlInit(
@@ -3614,7 +3461,7 @@ static int rtreeSqlInit(
}
sqlite3_free(zSql);
}
- if( pRtree->nAux ){
+ if( pRtree->nAux && rc!=SQLITE_NOMEM ){
pRtree->zReadAuxSql = sqlite3_mprintf(
"SELECT * FROM \"%w\".\"%w_rowid\" WHERE rowid=?1",
zDb, zPrefix);
@@ -3777,22 +3624,27 @@ static int rtreeInit(
}
sqlite3_vtab_config(db, SQLITE_VTAB_CONSTRAINT_SUPPORT, 1);
+ sqlite3_vtab_config(db, SQLITE_VTAB_INNOCUOUS);
+
/* Allocate the sqlite3_vtab structure */
nDb = (int)strlen(argv[1]);
nName = (int)strlen(argv[2]);
- pRtree = (Rtree *)sqlite3_malloc64(sizeof(Rtree)+nDb+nName+2);
+ pRtree = (Rtree *)sqlite3_malloc64(sizeof(Rtree)+nDb+nName*2+8);
if( !pRtree ){
return SQLITE_NOMEM;
}
- memset(pRtree, 0, sizeof(Rtree)+nDb+nName+2);
+ memset(pRtree, 0, sizeof(Rtree)+nDb+nName*2+8);
pRtree->nBusy = 1;
pRtree->base.pModule = &rtreeModule;
pRtree->zDb = (char *)&pRtree[1];
pRtree->zName = &pRtree->zDb[nDb+1];
+ pRtree->zNodeName = &pRtree->zName[nName+1];
pRtree->eCoordType = (u8)eCoordType;
memcpy(pRtree->zDb, argv[1], nDb);
memcpy(pRtree->zName, argv[2], nName);
+ memcpy(pRtree->zNodeName, argv[2], nName);
+ memcpy(&pRtree->zNodeName[nName], "_node", 6);
/* Create/Connect to the underlying relational database schema. If
@@ -4289,7 +4141,6 @@ static int rtreeCheckTable(
){
RtreeCheck check; /* Common context for various routines */
sqlite3_stmt *pStmt = 0; /* Used to find column count of rtree table */
- int bEnd = 0; /* True if transaction should be closed */
int nAux = 0; /* Number of extra columns. */
/* Initialize the context object */
@@ -4298,24 +4149,14 @@ static int rtreeCheckTable(
check.zDb = zDb;
check.zTab = zTab;
- /* If there is not already an open transaction, open one now. This is
- ** to ensure that the queries run as part of this integrity-check operate
- ** on a consistent snapshot. */
- if( sqlite3_get_autocommit(db) ){
- check.rc = sqlite3_exec(db, "BEGIN", 0, 0, 0);
- bEnd = 1;
- }
-
/* Find the number of auxiliary columns */
- if( check.rc==SQLITE_OK ){
- pStmt = rtreeCheckPrepare(&check, "SELECT * FROM %Q.'%q_rowid'", zDb, zTab);
- if( pStmt ){
- nAux = sqlite3_column_count(pStmt) - 2;
- sqlite3_finalize(pStmt);
- }else
- if( check.rc!=SQLITE_NOMEM ){
- check.rc = SQLITE_OK;
- }
+ pStmt = rtreeCheckPrepare(&check, "SELECT * FROM %Q.'%q_rowid'", zDb, zTab);
+ if( pStmt ){
+ nAux = sqlite3_column_count(pStmt) - 2;
+ sqlite3_finalize(pStmt);
+ }else
+ if( check.rc!=SQLITE_NOMEM ){
+ check.rc = SQLITE_OK;
}
/* Find number of dimensions in the rtree table. */
@@ -4346,15 +4187,35 @@ static int rtreeCheckTable(
sqlite3_finalize(check.aCheckMapping[0]);
sqlite3_finalize(check.aCheckMapping[1]);
- /* If one was opened, close the transaction */
- if( bEnd ){
- int rc = sqlite3_exec(db, "END", 0, 0, 0);
- if( check.rc==SQLITE_OK ) check.rc = rc;
- }
*pzReport = check.zReport;
return check.rc;
}
+/*
+** Implementation of the xIntegrity method for Rtree.
+*/
+static int rtreeIntegrity(
+ sqlite3_vtab *pVtab, /* The virtual table to check */
+ const char *zSchema, /* Schema in which the virtual table lives */
+ const char *zName, /* Name of the virtual table */
+ int isQuick, /* True for a quick_check */
+ char **pzErr /* Write results here */
+){
+ Rtree *pRtree = (Rtree*)pVtab;
+ int rc;
+ assert( pzErr!=0 && *pzErr==0 );
+ UNUSED_PARAMETER(zSchema);
+ UNUSED_PARAMETER(zName);
+ UNUSED_PARAMETER(isQuick);
+ rc = rtreeCheckTable(pRtree->db, pRtree->zDb, pRtree->zName, pzErr);
+ if( rc==SQLITE_OK && *pzErr ){
+ *pzErr = sqlite3_mprintf("In RTree %s.%s:\n%z",
+ pRtree->zDb, pRtree->zName, *pzErr);
+ if( (*pzErr)==0 ) rc = SQLITE_NOMEM;
+ }
+ return rc;
+}
+
/*
** Usage:
**
diff --git a/ext/rtree/rtree1.test b/ext/rtree/rtree1.test
index 633d0a5d5f..61664e1529 100644
--- a/ext/rtree/rtree1.test
+++ b/ext/rtree/rtree1.test
@@ -774,14 +774,27 @@ do_execsql_test 21.1 {
# Round-off error associated with using large integer constraints on
# a rtree search.
#
+if {$tcl_platform(machine)!="i686" || $tcl_platform(os)!="Linux"} {
+ reset_db
+ do_execsql_test 22.0 {
+ CREATE VIRTUAL TABLE t1 USING rtree ( id, x0, x1 );
+ INSERT INTO t1 VALUES (123, 9223372036854775799, 9223372036854775800);
+ SELECT id FROM t1 WHERE x0 > 9223372036854775807;
+ } {123}
+ do_execsql_test 22.1 {
+ SELECT id, x0 > 9223372036854775807 AS 'a0' FROM t1;
+ } {123 1}
+}
+
+# 2023-10-14 dbsqlfuzz --sql-fuzz find. rtreecheck() should not call
+# BEGIN/COMMIT because that causes problems with statement transactions,
+# and it is unnecessary.
+#
reset_db
-do_execsql_test 22.0 {
- CREATE VIRTUAL TABLE t1 USING rtree ( id, x0, x1 );
- INSERT INTO t1 VALUES (123, 9223372036854775799, 9223372036854775800);
- SELECT id FROM t1 WHERE x0 > 9223372036854775807;
-} {123}
-do_execsql_test 22.1 {
- SELECT id, x0 > 9223372036854775807 AS 'a0' FROM t1;
-} {123 1}
+do_test 23.0 {
+ db eval {CREATE TABLE t1(a,b,c);}
+ catch {db eval {CREATE TABLE t2 AS SELECT rtreecheck('t1') AS y;}}
+ db eval {PRAGMA integrity_check;}
+} {ok}
finish_test
diff --git a/ext/rtree/rtree8.test b/ext/rtree/rtree8.test
index 12e75a6850..51bd404164 100644
--- a/ext/rtree/rtree8.test
+++ b/ext/rtree/rtree8.test
@@ -75,7 +75,7 @@ do_test rtree8-1.2.2 { nested_select 1 } {51}
#
populate_t1 1500
do_rtree_integrity_test rtree8-1.3.0 t1
-do_execsql_test rtree8-1.3.1 { SELECT max(nodeno) FROM t1_node } {164}
+do_execsql_test rtree8-1.3.1 { SELECT max(nodeno) FROM t1_node } {183}
do_test rtree8-1.3.2 {
set rowids [execsql {SELECT min(rowid) FROM t1_rowid GROUP BY nodeno}]
set stmt_list [list]
diff --git a/ext/rtree/rtreeA.test b/ext/rtree/rtreeA.test
index 301cd4fc62..0b52070c42 100644
--- a/ext/rtree/rtreeA.test
+++ b/ext/rtree/rtreeA.test
@@ -113,7 +113,7 @@ do_execsql_test rtreeA-1.1.1 {
SELECT rtreecheck('main', 't1')
} {{Node 1 missing from database
Wrong number of entries in %_rowid table - expected 0, actual 500
-Wrong number of entries in %_parent table - expected 0, actual 23}}
+Wrong number of entries in %_parent table - expected 0, actual 25}}
do_execsql_test rtreeA-1.2.0 { DROP TABLE t1_node } {}
do_corruption_tests rtreeA-1.2 -error "database disk image is malformed" {
@@ -191,7 +191,7 @@ do_execsql_test rtreeA-3.3.3.4 {
SELECT rtreecheck('main', 't1')
} {{Rtree depth out of range (65535)
Wrong number of entries in %_rowid table - expected 0, actual 499
-Wrong number of entries in %_parent table - expected 0, actual 23}}
+Wrong number of entries in %_parent table - expected 0, actual 25}}
#-------------------------------------------------------------------------
# Set the "number of entries" field on some nodes incorrectly.
diff --git a/ext/rtree/rtreeJ.test b/ext/rtree/rtreeJ.test
new file mode 100644
index 0000000000..b091d2c686
--- /dev/null
+++ b/ext/rtree/rtreeJ.test
@@ -0,0 +1,273 @@
+# 2024-02-03
+#
+# 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.
+#
+#***********************************************************************
+#
+# ROLLBACK in the middle of an RTREE query
+#
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source $testdir/tester.tcl
+set testprefix rtreeJ
+ifcapable !rtree { finish_test ; return }
+
+do_execsql_test 1.0 {
+ CREATE VIRTUAL TABLE t1 USING rtree(id, x1, x2);
+ INSERT INTO t1 VALUES(1, 1, 1), (2, 2, 2);
+} {}
+
+do_execsql_test 1.1 {
+ SELECT * FROM t1
+} {1 1.0 1.0 2 2.0 2.0}
+
+# If a ROLLBACK occurs that backs out changes to the RTREE, then
+# all pending queries to the RTREE are aborted.
+#
+do_test 1.2 {
+ db eval {
+ BEGIN;
+ INSERT INTO t1 VALUES(3, 3, 3);
+ INSERT INTO t1 VALUES(4, 4, 4);
+ }
+ set rc [catch {
+ db eval { SELECT * FROM t1 } {
+ if {$id==1} {
+ db eval { ROLLBACK }
+ }
+ lappend res $id $x1 $x2
+ }
+ } msg]
+ list $rc $msg
+} {1 {query aborted}}
+
+do_execsql_test 1.3 {
+ SELECT * FROM t1;
+} {1 1.0 1.0 2 2.0 2.0}
+
+# A COMMIT of changes to the RTREE does not affect pending queries
+#
+do_test 1.4 {
+ set res {}
+ db eval {
+ BEGIN;
+ INSERT INTO t1 VALUES(5, 5, 5);
+ INSERT INTO t1 VALUES(6, 6, 6);
+ }
+ db eval { SELECT * FROM t1 } {
+ if {$id==1} {
+ db eval { COMMIT }
+ }
+ lappend res $id $x1 $x2
+ }
+ set res
+} {1 1.0 1.0 2 2.0 2.0 5 5.0 5.0 6 6.0 6.0}
+
+do_execsql_test 1.5 {
+ SELECT * FROM t1;
+} {1 1.0 1.0 2 2.0 2.0 5 5.0 5.0 6 6.0 6.0}
+
+do_execsql_test 1.6 {
+ DELETE FROM t1;
+ INSERT INTO t1 VALUES(1,1,1),(2,2,2),(3,3,3),(4,4,4);
+ CREATE TABLE t2(x);
+ SELECT * FROM t1;
+} {1 1.0 1.0 2 2.0 2.0 3 3.0 3.0 4 4.0 4.0}
+
+# A rollback that does not affect the rtree table because
+# the rtree table has not been written to does not cause
+# a query abort.
+#
+do_test 1.7 {
+ set res {}
+ db eval {
+ BEGIN;
+ INSERT INTO t2(x) VALUES(12345);
+ }
+ db eval { SELECT * FROM t1 } {
+ if {$id==1} {
+ db eval { ROLLBACK }
+ }
+ lappend res $id $x1 $x2
+ }
+ set res
+} {1 1.0 1.0 2 2.0 2.0 3 3.0 3.0 4 4.0 4.0}
+
+# ROLLBACK TO that affects the RTREE does cause a query abort.
+#
+do_test 1.8 {
+ db eval {
+ DELETE FROM t1 WHERE rowid>1;
+ BEGIN;
+ DELETE FROM t2;
+ INSERT INTO t2(x) VALUES(23456);
+ SAVEPOINT 'one';
+ INSERT INTO t1 VALUES(2,2,2),(3,3,3);
+ }
+ set rc [catch {
+ db eval { SELECT * FROM t1 } {
+ if {$id==1} {
+ db eval { ROLLBACK TO 'one'; }
+ }
+ lappend res $id $x1 $x2
+ }
+ } msg]
+ list $rc $msg
+} {1 {query aborted}}
+
+do_execsql_test 1.9 {
+ COMMIT;
+ SELECT * FROM t1;
+} {1 1.0 1.0}
+
+# ROLLBACK TO that does not affect the RTREE does not cause a query abort.
+#
+do_execsql_test 1.10 {
+ DELETE FROM t1;
+ INSERT INTO t1 VALUES(1,1,1),(2,2,2),(3,3,3);
+ BEGIN;
+ DELETE FROM t2;
+ INSERT INTO t2(x) VALUES(34567);
+ SAVEPOINT 'one';
+ INSERT INTO t2(x) VALUES('a string');
+ SELECT * FROM t1;
+} {1 1.0 1.0 2 2.0 2.0 3 3.0 3.0}
+do_test 1.11 {
+ set rc [catch {
+ set res {}
+ db eval { SELECT * FROM t1 } {
+ if {$id==2} {
+ # db eval { ROLLBACK TO 'one'; }
+ }
+ lappend res $id $x1 $x2
+ }
+ set res
+ } msg]
+ list $rc $msg
+} {0 {1 1.0 1.0 2 2.0 2.0 3 3.0 3.0}}
+
+do_execsql_test 1.12 {
+ COMMIT;
+ SELECT * FROM t1;
+} {1 1.0 1.0 2 2.0 2.0 3 3.0 3.0}
+
+#----------------------------------------------------------------------
+
+reset_db
+do_execsql_test 2.0 {
+ CREATE VIRTUAL TABLE t1 USING rtree(id, x1, x2);
+ INSERT INTO t1 VALUES(1, 1, 1), (2, 2, 2);
+ CREATE TABLE t2(x);
+} {}
+
+do_test 2.1 {
+ db eval {
+ BEGIN;
+ INSERT INTO t1 VALUES(3, 3, 3);
+ PRAGMA writable_schema = RESET;
+ }
+
+ set rc [catch {
+ db eval { SELECT x1, x2 FROM t1 } {
+ if {$x1==1} {
+ db eval { ROLLBACK }
+ }
+ lappend res $x1 $x2
+ }
+ } msg]
+ list $rc $msg
+} {1 {query aborted}}
+
+do_execsql_test 2.1 {
+ CREATE TABLE bak_node(nodeno, data);
+ CREATE TABLE bak_parent(nodeno, parentnode);
+ CREATE TABLE bak_rowid(rowid, nodeno);
+}
+proc save_t1 {} {
+ db eval {
+ DELETE FROM bak_node;
+ DELETE FROM bak_parent;
+ DELETE FROM bak_rowid;
+ INSERT INTO bak_node SELECT * FROM t1_node;
+ INSERT INTO bak_parent SELECT * FROM t1_parent;
+ INSERT INTO bak_rowid SELECT * FROM t1_rowid;
+ }
+}
+proc restore_t1 {} {
+ db eval {
+ DELETE FROM t1_node;
+ DELETE FROM t1_parent;
+ DELETE FROM t1_rowid;
+ INSERT INTO t1_node SELECT * FROM bak_node;
+ INSERT INTO t1_parent SELECT * FROM bak_parent;
+ INSERT INTO t1_rowid SELECT * FROM bak_rowid;
+ }
+}
+
+do_test 2.3 {
+ save_t1
+ db eval {
+ INSERT INTO t1 VALUES(3, 3, 3);
+ }
+ set rc [catch {
+ db eval { SELECT rowid, x1, x2 FROM t1 } {
+ if {$x1==1} {
+ restore_t1
+ }
+ lappend res $x1 $x2
+ }
+ } msg]
+ list $rc $msg
+} {1 {query aborted}}
+do_execsql_test 2.4 {
+ SELECT * FROM t1
+} {1 1.0 1.0 2 2.0 2.0}
+
+do_test 2.5 {
+ save_t1
+ db eval {
+ INSERT INTO t1 VALUES(3, 3, 3);
+ }
+ set rc [catch {
+ db eval { SELECT x1 FROM t1 } {
+ if {$x1==1} {
+ restore_t1
+ }
+ lappend res $x1 $x2
+ }
+ } msg]
+ list $rc $msg
+} {1 {query aborted}}
+do_execsql_test 2.6 {
+ SELECT * FROM t1
+} {1 1.0 1.0 2 2.0 2.0}
+
+do_test 2.7 {
+ save_t1
+ db eval {
+ INSERT INTO t1 VALUES(3, 3, 3);
+ }
+ set ::res [list]
+ set rc [catch {
+ db eval { SELECT 'abc' FROM t1 } {
+ if {$::res==[list]} {
+ restore_t1
+ set ::bDone 1
+ }
+ lappend res abc
+ }
+ } msg]
+ set res
+} {abc abc abc}
+do_execsql_test 2.6 {
+ SELECT * FROM t1
+} {1 1.0 1.0 2 2.0 2.0}
+
+
+finish_test
diff --git a/ext/rtree/rtree_util.tcl b/ext/rtree/rtree_util.tcl
index afa588e4e9..5640baf4e0 100644
--- a/ext/rtree/rtree_util.tcl
+++ b/ext/rtree/rtree_util.tcl
@@ -192,6 +192,6 @@ proc rtree_treedump {db zTab} {
}
proc do_rtree_integrity_test {tn tbl} {
- uplevel [list do_execsql_test $tn "SELECT rtreecheck('$tbl')" ok]
+ uplevel [list do_execsql_test $tn.1 "SELECT rtreecheck('$tbl')" ok]
+ uplevel [list do_execsql_test $tn.2 "PRAGMA integrity_check" ok]
}
-
diff --git a/ext/rtree/rtreecheck.test b/ext/rtree/rtreecheck.test
index ff5397e1e1..7a98f9bf4e 100644
--- a/ext/rtree/rtreecheck.test
+++ b/ext/rtree/rtreecheck.test
@@ -78,6 +78,11 @@ do_execsql_test 2.3 {
SELECT rtreecheck('r1')
} {{Dimension 0 of cell 0 on node 1 is corrupt
Dimension 1 of cell 3 on node 1 is corrupt}}
+do_execsql_test 2.3b {
+ PRAGMA integrity_check;
+} {{In RTree main.r1:
+Dimension 0 of cell 0 on node 1 is corrupt
+Dimension 1 of cell 3 on node 1 is corrupt}}
setup_simple_db
do_execsql_test 2.4 {
@@ -85,12 +90,21 @@ do_execsql_test 2.4 {
SELECT rtreecheck('r1')
} {{Mapping (3 -> 1) missing from %_rowid table
Wrong number of entries in %_rowid table - expected 5, actual 4}}
+do_execsql_test 2.4b {
+ PRAGMA integrity_check
+} {{In RTree main.r1:
+Mapping (3 -> 1) missing from %_rowid table
+Wrong number of entries in %_rowid table - expected 5, actual 4}}
setup_simple_db
do_execsql_test 2.5 {
UPDATE r1_rowid SET nodeno=2 WHERE rowid=3;
SELECT rtreecheck('r1')
} {{Found (3 -> 2) in %_rowid table, expected (3 -> 1)}}
+do_execsql_test 2.5b {
+ PRAGMA integrity_check
+} {{In RTree main.r1:
+Found (3 -> 2) in %_rowid table, expected (3 -> 1)}}
reset_db
do_execsql_test 3.0 {
@@ -104,14 +118,16 @@ do_execsql_test 3.0 {
INSERT INTO r1 VALUES(7, 5, 0x00000080);
INSERT INTO r1 VALUES(8, 5, 0x40490fdb);
INSERT INTO r1 VALUES(9, 0x7f800000, 0x7f900000);
- SELECT rtreecheck('r1')
-} {ok}
+ SELECT rtreecheck('r1');
+ PRAGMA integrity_check;
+} {ok ok}
do_execsql_test 3.1 {
CREATE VIRTUAL TABLE r2 USING rtree_i32(id, x1, x2);
INSERT INTO r2 VALUES(2, -1*(1<<31), -1*(1<<31)+5);
- SELECT rtreecheck('r2')
-} {ok}
+ SELECT rtreecheck('r2');
+ PRAGMA integrity_check;
+} {ok ok}
sqlite3_db_config db DEFENSIVE 0
do_execsql_test 3.2 {
@@ -125,6 +141,11 @@ do_execsql_test 3.3 {
UPDATE r2_node SET data = X'00001234';
SELECT rtreecheck('r2')!='ok';
} {1}
+do_execsql_test 3.4 {
+ PRAGMA integrity_check;
+} {{In RTree main.r2:
+Node 1 is too small for cell count of 4660 (4 bytes)
+Wrong number of entries in %_rowid table - expected 0, actual 1}}
do_execsql_test 4.0 {
CREATE TABLE notanrtree(i);
@@ -181,4 +202,3 @@ if {[permutation]=="inmemory_journal"} {
}
finish_test
-
diff --git a/ext/rtree/rtreedoc.test b/ext/rtree/rtreedoc.test
index b64faa2e99..4e610db8a2 100644
--- a/ext/rtree/rtreedoc.test
+++ b/ext/rtree/rtreedoc.test
@@ -601,11 +601,21 @@ do_execsql_test 2.5.2 {
SELECT A.id FROM demo_index AS A, demo_index AS B
WHERE A.maxX>=B.minX AND A.minX<=B.maxX
AND A.maxY>=B.minY AND A.minY<=B.maxY
- AND B.id=28269;
+ AND B.id=28269 ORDER BY +A.id;
} {
- 28293 28216 28322 28286 28269
- 28215 28336 28262 28291 28320
- 28313 28298 28287
+ 28215
+ 28216
+ 28262
+ 28269
+ 28286
+ 28287
+ 28291
+ 28293
+ 28298
+ 28313
+ 28320
+ 28322
+ 28336
}
# EVIDENCE-OF: R-02723-34107 Note that it is not necessary for all
@@ -1575,7 +1585,7 @@ execsql BEGIN
do_test 3.6 {
execsql { INSERT INTO rt2_parent VALUES(1000, 1000) }
execsql { SELECT rtreecheck('rt2') }
-} {{Wrong number of entries in %_parent table - expected 9, actual 10}}
+} {{Wrong number of entries in %_parent table - expected 10, actual 11}}
execsql ROLLBACK
diff --git a/ext/rtree/rtreefuzz001.test b/ext/rtree/rtreefuzz001.test
index 58fd179ab9..c81c41da30 100644
--- a/ext/rtree/rtreefuzz001.test
+++ b/ext/rtree/rtreefuzz001.test
@@ -1043,7 +1043,7 @@ do_test rtreefuzz001-500 {
| end crash-2e81f5dce5cbd4.db}]
execsql { PRAGMA writable_schema = 1;}
catchsql {UPDATE t1 SET ex= ex ISNULL}
-} {1 {database disk image is malformed}}
+} {0 {}}
do_test rtreefuzz001-600 {
sqlite3 db {}
diff --git a/ext/session/session3.test b/ext/session/session3.test
index ba316348ef..ee955f1376 100644
--- a/ext/session/session3.test
+++ b/ext/session/session3.test
@@ -135,8 +135,8 @@ do_test 2.2.2 {
DROP TABLE t2;
CREATE TABLE t2(a, b PRIMARY KEY, c, d);
}
- list [catch { S changeset } msg] $msg
-} {1 SQLITE_SCHEMA}
+ catch { S changeset }
+} {0}
do_test 2.2.3 {
S delete
sqlite3session S db main
@@ -167,8 +167,8 @@ do_test 2.2.4 {
CREATE TABLE t2(a, b PRIMARY KEY, c, d);
INSERT INTO t2 VALUES(4, 5, 6, 7);
}
- list [catch { S changeset } msg] $msg
-} {1 SQLITE_SCHEMA}
+ catch { S changeset }
+} {0}
do_test 2.3 {
S delete
diff --git a/ext/session/sessionalter.test b/ext/session/sessionalter.test
new file mode 100644
index 0000000000..34424cf275
--- /dev/null
+++ b/ext/session/sessionalter.test
@@ -0,0 +1,238 @@
+# 2023 October 02
+#
+# 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 implements that the sessions module interacts well with
+# the ALTER TABLE ADD COLUMN command.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+
+ifcapable !session {finish_test; return}
+set testprefix sessionalter
+
+
+forcedelete test.db2
+sqlite3 db2 test.db2
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+}
+
+do_execsql_test -db db2 1.1 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c DEFAULT 1234);
+}
+
+do_then_apply_sql {
+ INSERT INTO t1 VALUES(1, 'one');
+ INSERT INTO t1 VALUES(2, 'two');
+}
+
+do_execsql_test -db db2 1.2 {
+ SELECT * FROM t1
+} {
+ 1 one 1234
+ 2 two 1234
+}
+
+do_then_apply_sql {
+ UPDATE t1 SET b='four' WHERE a=2;
+}
+
+do_execsql_test -db db2 1.3 {
+ SELECT * FROM t1
+} {
+ 1 one 1234
+ 2 four 1234
+}
+
+do_then_apply_sql {
+ DELETE FROM t1 WHERE a=1;
+}
+
+do_execsql_test -db db2 1.4 {
+ SELECT * FROM t1
+} {
+ 2 four 1234
+}
+
+db2 close
+
+#--------------------------------------------------------------------------
+reset_db
+
+do_execsql_test 2.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+}
+
+do_test 2.1 {
+ sqlite3session S db main
+ S attach t1
+ set {} {}
+} {}
+do_execsql_test 2.2 {
+ INSERT INTO t1 VALUES(1, 2);
+ ALTER TABLE t1 ADD COLUMN c DEFAULT 'abcd';
+ INSERT INTO t1 VALUES(2, 3, 4);
+}
+do_changeset_test 2.3 S {
+ {INSERT t1 0 X.. {} {i 1 i 2 t abcd}}
+ {INSERT t1 0 X.. {} {i 2 i 3 i 4}}
+}
+
+do_iterator_test 2.4 {} {
+ DELETE FROM t1 WHERE a=2;
+ ALTER TABLE t1 ADD COLUMN d DEFAULT 'abcd';
+ ALTER TABLE t1 ADD COLUMN e DEFAULT 5;
+ ALTER TABLE t1 ADD COLUMN f DEFAULT 7.2;
+ -- INSERT INTO t1 VALUES(9, 9, 9, 9);
+} {
+ {DELETE t1 0 X..... {i 2 i 3 i 4 t abcd i 5 f 7.2} {}}
+}
+
+#-------------------------------------------------------------------------
+# Tests of the sqlite3changegroup_xxx() APIs.
+#
+reset_db
+do_execsql_test 3.0 {
+ CREATE TABLE t1(x INTEGER PRIMARY KEY, y);
+ CREATE TABLE t2(x PRIMARY KEY, y);
+ CREATE TABLE t3(x, y);
+ CREATE TABLE t4(y PRIMARY KEY, x) WITHOUT ROWID;
+
+ INSERT INTO t1 VALUES(1, 2), (3, 4), (5, 6);
+ INSERT INTO t2 VALUES('one', 'two'), ('three', 'four'), ('five', 'six');
+ INSERT INTO t3 VALUES(1, 2), (3, 4), (5, 6);
+
+ INSERT INTO t4(x, y) VALUES(1, 2), (3, 4), (5, 6);
+}
+
+db_save_and_close
+foreach {tn sql1 at sql2} {
+ 1 {
+ INSERT INTO t1(x, y) VALUES(7, 8);
+ } {
+ ALTER TABLE t1 ADD COLUMN z DEFAULT 10;
+ } {
+ UPDATE t1 SET y=11 WHERE x=7;
+ }
+
+ 2 {
+ UPDATE t2 SET y='two.two' WHERE x='one';
+ DELETE FROM t2 WHERE x='five';
+ INSERT INTO t2(x, y) VALUES('seven', 'eight');
+ } {
+ ALTER TABLE t2 ADD COLUMN z;
+ ALTER TABLE t2 ADD COLUMN zz;
+ } {
+ }
+
+ 3 {
+ DELETE FROM t2 WHERE x='five';
+ } {
+ ALTER TABLE t2 ADD COLUMN z DEFAULT 'xyz';
+ } {
+ }
+
+ 4 {
+ UPDATE t2 SET y='two.two' WHERE x='three';
+ } {
+ ALTER TABLE t2 ADD COLUMN z;
+ } {
+ UPDATE t2 SET z='abc' WHERE x='one';
+ }
+
+ 5* {
+ UPDATE t2 SET y='two.two' WHERE x='three';
+ } {
+ ALTER TABLE t2 ADD COLUMN z DEFAULT 'defu1';
+ } {
+ }
+
+ 6* {
+ INSERT INTO t2(x, y) VALUES('nine', 'ten');
+ } {
+ ALTER TABLE t2 ADD COLUMN z;
+ ALTER TABLE t2 ADD COLUMN a DEFAULT 'eelve';
+ ALTER TABLE t2 ADD COLUMN b DEFAULT x'1234abcd';
+ ALTER TABLE t2 ADD COLUMN c DEFAULT 4.2;
+ ALTER TABLE t2 ADD COLUMN d DEFAULT NULL;
+ } {
+ }
+
+ 7 {
+ INSERT INTO t3(x, y) VALUES(7, 8);
+ UPDATE t3 SET y='fourteen' WHERE x=1;
+ DELETE FROM t3 WHERE x=3;
+ } {
+ ALTER TABLE t3 ADD COLUMN c;
+ } {
+ INSERT INTO t3(x, y, c) VALUES(9, 10, 11);
+ }
+
+ 8 {
+ INSERT INTO t4(x, y) VALUES(7, 8);
+ UPDATE t4 SET y='fourteen' WHERE x=1;
+ DELETE FROM t4 WHERE x=3;
+ } {
+ ALTER TABLE t4 ADD COLUMN c;
+ } {
+ INSERT INTO t4(x, y, c) VALUES(9, 10, 11);
+ }
+} {
+ foreach {tn2 cmd} {
+ 1 changeset_from_sql
+ 2 patchset_from_sql
+ } {
+ db_restore_and_reopen
+
+ set C1 [$cmd $sql1]
+ execsql $at
+ set C2 [$cmd $sql2]
+
+ sqlite3changegroup grp
+ grp schema db main
+ grp add $C1
+ grp add $C2
+ set T1 [grp output]
+ grp delete
+
+ db_restore_and_reopen
+ execsql $at
+ set T2 [$cmd "$sql1 ; $sql2"]
+
+ if {[string range $tn end end]!="*"} {
+ do_test 3.1.$tn.$tn2.1 { changeset_to_list $T1 } [changeset_to_list $T2]
+ set testname "$tn.$tn2"
+ } else {
+ set testname "[string range $tn 0 end-1].$tn2"
+ }
+
+ db_restore_and_reopen
+ proc xConflict {args} { return "REPLACE" }
+ sqlite3changeset_apply_v2 db $T1 xConflict
+ set S1 [scksum db main]
+
+ db_restore_and_reopen
+ sqlite3changeset_apply_v2 db $T2 xConflict
+ set S2 [scksum db main]
+
+ # if { $tn==7 } { puts [changeset_to_list $T1] }
+
+ do_test 3.1.$tn.2 { set S1 } $S2
+ }
+}
+
+
+finish_test
+
diff --git a/ext/session/sessionfault3.test b/ext/session/sessionfault3.test
new file mode 100644
index 0000000000..af5a4cdb43
--- /dev/null
+++ b/ext/session/sessionfault3.test
@@ -0,0 +1,59 @@
+# 2016 October 6
+#
+# 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.
+#
+#***********************************************************************
+#
+# The focus of this file is testing the session module.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+set testprefix sessionfault3
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a, b, PRIMARY KEY(a));
+ INSERT INTO t1 VALUES(1, 2);
+ INSERT INTO t1 VALUES(3, 4);
+ INSERT INTO t1 VALUES('five', 'six');
+}
+
+set C1 [changeset_from_sql {
+ INSERT INTO t1 VALUES('seven', 'eight');
+ UPDATE t1 SET b=6 WHERE a='five';
+ DELETE FROM t1 WHERE a=1;
+}]
+
+do_execsql_test 1.1 {
+ ALTER TABLE t1 ADD COLUMN d DEFAULT 123;
+ ALTER TABLE t1 ADD COLUMN e DEFAULT 'string';
+}
+
+set C2 [changeset_from_sql {
+ UPDATE t1 SET e='new value' WHERE a='seven';
+ INSERT INTO t1 VALUES(0, 0, 0, 0);
+}]
+
+do_faultsim_test 1 -faults oom* -prep {
+ sqlite3changegroup G
+} -body {
+ G schema db main
+ G add $::C1
+ G add $::C2
+ G output
+ set {} {}
+} -test {
+ catch { G delete }
+ faultsim_test_result {0 {}} {1 SQLITE_NOMEM}
+}
+
+finish_test
diff --git a/ext/session/sessionnoact.test b/ext/session/sessionnoact.test
new file mode 100644
index 0000000000..1274ecb146
--- /dev/null
+++ b/ext/session/sessionnoact.test
@@ -0,0 +1,110 @@
+# 2023 October 20
+#
+# 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 implements regression tests for SQLite library.
+#
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix sessionnoact
+
+do_execsql_test 1.0 {
+ CREATE TABLE p1(a INTEGER PRIMARY KEY, b, c UNIQUE);
+ INSERT INTO p1 VALUES(1, 1, 'one');
+ INSERT INTO p1 VALUES(2, 2, 'two');
+ INSERT INTO p1 VALUES(3, 3, 'three');
+ INSERT INTO p1 VALUES(4, 4, 'four');
+}
+
+db_save
+
+set C [changeset_from_sql {
+ DELETE FROM p1 WHERE a=2;
+ UPDATE p1 SET c='six' WHERE a=3;
+ INSERT INTO p1 VALUES(5, 5, 'two');
+ INSERT INTO p1 VALUES(6, 6, 'three');
+}]
+
+db_restore_and_reopen
+
+do_execsql_test 1.1 {
+ CREATE TABLE c1(x INTEGER PRIMARY KEY, y,
+ FOREIGN KEY(y) REFERENCES p1(c) ON DELETE CASCADE ON UPDATE SET NULL
+ );
+
+ INSERT INTO c1 VALUES(10, 'one');
+ INSERT INTO c1 VALUES(20, 'two');
+ INSERT INTO c1 VALUES(30, 'three');
+ INSERT INTO c1 VALUES(40, 'four');
+}
+
+db_save
+
+do_execsql_test 1.2 {
+ PRAGMA foreign_keys = 1;
+}
+
+set ::nConflict 0
+proc conflict {args} {
+ incr ::nConflict
+ return "OMIT"
+}
+
+sqlite3changeset_apply_v2 db $C conflict
+
+do_execsql_test 1.3 {
+ SELECT * FROM c1
+} {
+ 10 one
+ 30 {}
+ 40 four
+}
+
+db_restore_and_reopen
+
+do_execsql_test 1.4 {
+ PRAGMA foreign_keys = 1;
+}
+
+do_execsql_test 1.5 {
+ UPDATE p1 SET c=12345 WHERE a = 45;
+}
+
+sqlite3changeset_apply_v2 -noaction db $C conflict
+do_execsql_test 1.6 {
+ SELECT * FROM c1
+} {
+ 10 one
+ 20 two
+ 30 three
+ 40 four
+}
+
+do_execsql_test 1.7 {
+ PRAGMA foreign_keys = 1;
+ UPDATE p1 SET c = 'ten' WHERE c='two';
+ SELECT * FROM c1;
+} {
+ 10 one
+ 20 {}
+ 30 three
+ 40 four
+}
+
+do_execsql_test 1.8 {
+ PRAGMA foreign_key_check
+}
+
+finish_test
diff --git a/ext/session/sessionstat1.test b/ext/session/sessionstat1.test
index 2757d60440..16dc4e2727 100644
--- a/ext/session/sessionstat1.test
+++ b/ext/session/sessionstat1.test
@@ -92,7 +92,7 @@ do_test 2.1 {
} {}
do_execsql_test -db db2 2.2 {
- SELECT * FROM sqlite_stat1
+ SELECT * FROM sqlite_stat1 ORDER BY tbl, idx
} {
t1 sqlite_autoindex_t1_1 {32 1}
t1 t1b {32 4}
@@ -104,7 +104,7 @@ do_test 2.3 {
} {}
do_execsql_test -db db2 2.4 {
- SELECT * FROM sqlite_stat1
+ SELECT * FROM sqlite_stat1 ORDER BY tbl, idx;
} {
t1 sqlite_autoindex_t1_1 {32 1}
t1 t1b {32 4}
diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c
index 9f862f2465..acb945194d 100644
--- a/ext/session/sqlite3session.c
+++ b/ext/session/sqlite3session.c
@@ -119,6 +119,18 @@ struct sqlite3_changeset_iter {
** The data associated with each hash-table entry is a structure containing
** a subset of the initial values that the modified row contained at the
** start of the session. Or no initial values if the row was inserted.
+**
+** pDfltStmt:
+** This is only used by the sqlite3changegroup_xxx() APIs, not by
+** regular sqlite3_session objects. It is a SELECT statement that
+** selects the default value for each table column. For example,
+** if the table is
+**
+** CREATE TABLE xx(a DEFAULT 1, b, c DEFAULT 'abc')
+**
+** then this variable is the compiled version of:
+**
+** SELECT 1, NULL, 'abc'
*/
struct SessionTable {
SessionTable *pNext;
@@ -127,10 +139,12 @@ struct SessionTable {
int bStat1; /* True if this is sqlite_stat1 */
int bRowid; /* True if this table uses rowid for PK */
const char **azCol; /* Column names */
+ const char **azDflt; /* Default value expressions */
u8 *abPK; /* Array of primary key flags */
int nEntry; /* Total number of entries in hash table */
int nChange; /* Size of apChange[] array */
SessionChange **apChange; /* Hash table buckets */
+ sqlite3_stmt *pDfltStmt;
};
/*
@@ -299,6 +313,7 @@ struct SessionTable {
struct SessionChange {
u8 op; /* One of UPDATE, DELETE, INSERT */
u8 bIndirect; /* True if this change is "indirect" */
+ u16 nRecordField; /* Number of fields in aRecord[] */
int nMaxSize; /* Max size of eventual changeset record */
int nRecord; /* Number of bytes in buffer aRecord[] */
u8 *aRecord; /* Buffer containing old.* record */
@@ -324,7 +339,7 @@ static int sessionVarintLen(int iVal){
** Read a varint value from aBuf[] into *piVal. Return the number of
** bytes read.
*/
-static int sessionVarintGet(u8 *aBuf, int *piVal){
+static int sessionVarintGet(const u8 *aBuf, int *piVal){
return getVarint32(aBuf, *piVal);
}
@@ -587,9 +602,11 @@ static int sessionPreupdateHash(
** Return the number of bytes of space occupied by the value (including
** the type byte).
*/
-static int sessionSerialLen(u8 *a){
- int e = *a;
+static int sessionSerialLen(const u8 *a){
+ int e;
int n;
+ assert( a!=0 );
+ e = *a;
if( e==0 || e==0xFF ) return 1;
if( e==SQLITE_NULL ) return 1;
if( e==SQLITE_INTEGER || e==SQLITE_FLOAT ) return 9;
@@ -994,13 +1011,14 @@ static int sessionGrowHash(
**
** For example, if the table is declared as:
**
-** CREATE TABLE tbl1(w, x, y, z, PRIMARY KEY(w, z));
+** CREATE TABLE tbl1(w, x DEFAULT 'abc', y, z, PRIMARY KEY(w, z));
**
-** Then the four output variables are populated as follows:
+** Then the five output variables are populated as follows:
**
** *pnCol = 4
** *pzTab = "tbl1"
** *pazCol = {"w", "x", "y", "z"}
+** *pazDflt = {NULL, 'abc', NULL, NULL}
** *pabPK = {1, 0, 0, 1}
**
** All returned buffers are part of the same single allocation, which must
@@ -1014,6 +1032,7 @@ static int sessionTableInfo(
int *pnCol, /* OUT: number of columns */
const char **pzTab, /* OUT: Copy of zThis */
const char ***pazCol, /* OUT: Array of column names for table */
+ const char ***pazDflt, /* OUT: Array of default value expressions */
u8 **pabPK, /* OUT: Array of booleans - true for PK col */
int *pbRowid /* OUT: True if only PK is a rowid */
){
@@ -1026,11 +1045,18 @@ static int sessionTableInfo(
int i;
u8 *pAlloc = 0;
char **azCol = 0;
+ char **azDflt = 0;
u8 *abPK = 0;
int bRowid = 0; /* Set to true to use rowid as PK */
assert( pazCol && pabPK );
+ *pazCol = 0;
+ *pabPK = 0;
+ *pnCol = 0;
+ if( pzTab ) *pzTab = 0;
+ if( pazDflt ) *pazDflt = 0;
+
nThis = sqlite3Strlen30(zThis);
if( nThis==12 && 0==sqlite3_stricmp("sqlite_stat1", zThis) ){
rc = sqlite3_table_column_metadata(db, zDb, zThis, 0, 0, 0, 0, 0, 0);
@@ -1044,39 +1070,28 @@ static int sessionTableInfo(
}else if( rc==SQLITE_ERROR ){
zPragma = sqlite3_mprintf("");
}else{
- *pazCol = 0;
- *pabPK = 0;
- *pnCol = 0;
- if( pzTab ) *pzTab = 0;
return rc;
}
}else{
zPragma = sqlite3_mprintf("PRAGMA '%q'.table_info('%q')", zDb, zThis);
}
if( !zPragma ){
- *pazCol = 0;
- *pabPK = 0;
- *pnCol = 0;
- if( pzTab ) *pzTab = 0;
return SQLITE_NOMEM;
}
rc = sqlite3_prepare_v2(db, zPragma, -1, &pStmt, 0);
sqlite3_free(zPragma);
if( rc!=SQLITE_OK ){
- *pazCol = 0;
- *pabPK = 0;
- *pnCol = 0;
- if( pzTab ) *pzTab = 0;
return rc;
}
nByte = nThis + 1;
bRowid = (pbRowid!=0);
while( SQLITE_ROW==sqlite3_step(pStmt) ){
- nByte += sqlite3_column_bytes(pStmt, 1);
+ nByte += sqlite3_column_bytes(pStmt, 1); /* name */
+ nByte += sqlite3_column_bytes(pStmt, 4); /* dflt_value */
nDbCol++;
- if( sqlite3_column_int(pStmt, 5) ) bRowid = 0;
+ if( sqlite3_column_int(pStmt, 5) ) bRowid = 0; /* pk */
}
if( nDbCol==0 ) bRowid = 0;
nDbCol += bRowid;
@@ -1084,15 +1099,18 @@ static int sessionTableInfo(
rc = sqlite3_reset(pStmt);
if( rc==SQLITE_OK ){
- nByte += nDbCol * (sizeof(const char *) + sizeof(u8) + 1);
+ nByte += nDbCol * (sizeof(const char *)*2 + sizeof(u8) + 1 + 1);
pAlloc = sessionMalloc64(pSession, nByte);
if( pAlloc==0 ){
rc = SQLITE_NOMEM;
+ }else{
+ memset(pAlloc, 0, nByte);
}
}
if( rc==SQLITE_OK ){
azCol = (char **)pAlloc;
- pAlloc = (u8 *)&azCol[nDbCol];
+ azDflt = (char**)&azCol[nDbCol];
+ pAlloc = (u8 *)&azDflt[nDbCol];
abPK = (u8 *)pAlloc;
pAlloc = &abPK[nDbCol];
if( pzTab ){
@@ -1112,11 +1130,21 @@ static int sessionTableInfo(
}
while( SQLITE_ROW==sqlite3_step(pStmt) ){
int nName = sqlite3_column_bytes(pStmt, 1);
+ int nDflt = sqlite3_column_bytes(pStmt, 4);
const unsigned char *zName = sqlite3_column_text(pStmt, 1);
+ const unsigned char *zDflt = sqlite3_column_text(pStmt, 4);
+
if( zName==0 ) break;
memcpy(pAlloc, zName, nName+1);
azCol[i] = (char *)pAlloc;
pAlloc += nName+1;
+ if( zDflt ){
+ memcpy(pAlloc, zDflt, nDflt+1);
+ azDflt[i] = (char *)pAlloc;
+ pAlloc += nDflt+1;
+ }else{
+ azDflt[i] = 0;
+ }
abPK[i] = sqlite3_column_int(pStmt, 5);
i++;
}
@@ -1127,14 +1155,11 @@ static int sessionTableInfo(
** free any allocation made. An error code will be returned in this case.
*/
if( rc==SQLITE_OK ){
- *pazCol = (const char **)azCol;
+ *pazCol = (const char**)azCol;
+ if( pazDflt ) *pazDflt = (const char**)azDflt;
*pabPK = abPK;
*pnCol = nDbCol;
}else{
- *pazCol = 0;
- *pabPK = 0;
- *pnCol = 0;
- if( pzTab ) *pzTab = 0;
sessionFree(pSession, azCol);
}
if( pbRowid ) *pbRowid = bRowid;
@@ -1143,10 +1168,9 @@ static int sessionTableInfo(
}
/*
-** This function is only called from within a pre-update handler for a
-** write to table pTab, part of session pSession. If this is the first
-** write to this table, initalize the SessionTable.nCol, azCol[] and
-** abPK[] arrays accordingly.
+** This function is called to initialize the SessionTable.nCol, azCol[]
+** abPK[] and azDflt[] members of SessionTable object pTab. If these
+** fields are already initilialized, this function is a no-op.
**
** If an error occurs, an error code is stored in sqlite3_session.rc and
** non-zero returned. Or, if no error occurs but the table has no primary
@@ -1154,15 +1178,22 @@ static int sessionTableInfo(
** indicate that updates on this table should be ignored. SessionTable.abPK
** is set to NULL in this case.
*/
-static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){
+static int sessionInitTable(
+ sqlite3_session *pSession, /* Optional session handle */
+ SessionTable *pTab, /* Table object to initialize */
+ sqlite3 *db, /* Database handle to read schema from */
+ const char *zDb /* Name of db - "main", "temp" etc. */
+){
+ int rc = SQLITE_OK;
+
if( pTab->nCol==0 ){
u8 *abPK;
assert( pTab->azCol==0 || pTab->abPK==0 );
- pSession->rc = sessionTableInfo(pSession, pSession->db, pSession->zDb,
- pTab->zName, &pTab->nCol, 0, &pTab->azCol, &abPK,
- (pSession->bImplicitPK ? &pTab->bRowid : 0)
+ rc = sessionTableInfo(pSession, db, zDb,
+ pTab->zName, &pTab->nCol, 0, &pTab->azCol, &pTab->azDflt, &abPK,
+ ((pSession==0 || pSession->bImplicitPK) ? &pTab->bRowid : 0)
);
- if( pSession->rc==SQLITE_OK ){
+ if( rc==SQLITE_OK ){
int i;
for(i=0; inCol; i++){
if( abPK[i] ){
@@ -1174,14 +1205,321 @@ static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){
pTab->bStat1 = 1;
}
- if( pSession->bEnableSize ){
+ if( pSession && pSession->bEnableSize ){
pSession->nMaxChangesetSize += (
1 + sessionVarintLen(pTab->nCol) + pTab->nCol + strlen(pTab->zName)+1
);
}
}
}
- return (pSession->rc || pTab->abPK==0);
+
+ if( pSession ){
+ pSession->rc = rc;
+ return (rc || pTab->abPK==0);
+ }
+ return rc;
+}
+
+/*
+** Re-initialize table object pTab.
+*/
+static int sessionReinitTable(sqlite3_session *pSession, SessionTable *pTab){
+ int nCol = 0;
+ const char **azCol = 0;
+ const char **azDflt = 0;
+ u8 *abPK = 0;
+ int bRowid = 0;
+
+ assert( pSession->rc==SQLITE_OK );
+
+ pSession->rc = sessionTableInfo(pSession, pSession->db, pSession->zDb,
+ pTab->zName, &nCol, 0, &azCol, &azDflt, &abPK,
+ (pSession->bImplicitPK ? &bRowid : 0)
+ );
+ if( pSession->rc==SQLITE_OK ){
+ if( pTab->nCol>nCol || pTab->bRowid!=bRowid ){
+ pSession->rc = SQLITE_SCHEMA;
+ }else{
+ int ii;
+ int nOldCol = pTab->nCol;
+ for(ii=0; iinCol ){
+ if( pTab->abPK[ii]!=abPK[ii] ){
+ pSession->rc = SQLITE_SCHEMA;
+ }
+ }else if( abPK[ii] ){
+ pSession->rc = SQLITE_SCHEMA;
+ }
+ }
+
+ if( pSession->rc==SQLITE_OK ){
+ const char **a = pTab->azCol;
+ pTab->azCol = azCol;
+ pTab->nCol = nCol;
+ pTab->azDflt = azDflt;
+ pTab->abPK = abPK;
+ azCol = a;
+ }
+ if( pSession->bEnableSize ){
+ pSession->nMaxChangesetSize += (nCol - nOldCol);
+ pSession->nMaxChangesetSize += sessionVarintLen(nCol);
+ pSession->nMaxChangesetSize -= sessionVarintLen(nOldCol);
+ }
+ }
+ }
+
+ sqlite3_free((char*)azCol);
+ return pSession->rc;
+}
+
+/*
+** Session-change object (*pp) contains an old.* record with fewer than
+** nCol fields. This function updates it with the default values for
+** the missing fields.
+*/
+static void sessionUpdateOneChange(
+ sqlite3_session *pSession, /* For memory accounting */
+ int *pRc, /* IN/OUT: Error code */
+ SessionChange **pp, /* IN/OUT: Change object to update */
+ int nCol, /* Number of columns now in table */
+ sqlite3_stmt *pDflt /* SELECT */
+){
+ SessionChange *pOld = *pp;
+
+ while( pOld->nRecordField