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

Merge the jni-threading branch into trunk, eliminating the JNI API's prior threading limitations.

FossilOrigin-Name: 3739c8aa7080d8e1044ca51ab7b699b50da4d29620b35acfcea2745059b65bac
This commit is contained in:
stephan
2023-08-24 14:49:29 +00:00
20 changed files with 3204 additions and 2076 deletions

View File

@ -68,6 +68,7 @@ JAVA_FILES.main := $(patsubst %,$(dir.src.jni)/%,\
ResultCode.java \
RollbackHook.java \
SQLFunction.java \
SQLLog.java \
sqlite3_context.java \
sqlite3.java \
SQLite3Jni.java \
@ -160,17 +161,16 @@ SQLITE_OPT = \
-DSQLITE_ENABLE_DBSTAT_VTAB \
-DSQLITE_ENABLE_BYTECODE_VTAB \
-DSQLITE_ENABLE_OFFSET_SQL_FUNC \
-DSQLITE_ENABLE_PREUPDATE_HOOK \
-DSQLITE_ENABLE_SQLLOG \
-DSQLITE_OMIT_LOAD_EXTENSION \
-DSQLITE_OMIT_DEPRECATED \
-DSQLITE_OMIT_SHARED_CACHE \
-DSQLITE_THREADSAFE=0 \
-DSQLITE_THREADSAFE=1 \
-DSQLITE_TEMP_STORE=2 \
-DSQLITE_USE_URI=1 \
-DSQLITE_C=$(sqlite3.c)
# -DSQLITE_DEBUG
# -DSQLITE_DEBUG is just to work around a -Wall warning
# for a var which gets set in all builds but only read
# via assert().
-DSQLITE_C=$(sqlite3.c) \
-DSQLITE_DEBUG
SQLITE_OPT += -g -DDEBUG -UNDEBUG
@ -226,12 +226,16 @@ $(sqlite3-jni.dll): $(dir.bld.c) $(sqlite3-jni.c) $(SQLite3Jni.java) $(MAKEFILE)
$(sqlite3-jni.c) -shared -o $@
all: $(sqlite3-jni.dll)
.PHONY: test
test.flags ?= -v
test: $(SQLite3Jni.class) $(sqlite3-jni.dll)
$(bin.java) -ea -Djava.library.path=$(dir.bld.c) \
$(java.flags) -cp $(classpath) \
org.sqlite.jni.Tester1 $(if $(test.flags),-- $(test.flags),)
.PHONY: test test-one
test.flags ?=
test.main.flags = -ea -Djava.library.path=$(dir.bld.c) \
$(java.flags) -cp $(classpath) \
org.sqlite.jni.Tester1
test-one: $(SQLite3Jni.class) $(sqlite3-jni.dll)
$(bin.java) $(test.main.flags) $(test.flags)
test: test-one
@echo "Again in multi-threaded mode:";
$(bin.java) $(test.main.flags) -t 5 -r 20 -shuffle $(test.flags)
tester.scripts := $(sort $(wildcard $(dir.src)/tests/*.test))
tester.flags ?= # --verbose
@ -272,8 +276,7 @@ package.jar.in := $(abspath $(dir.src)/jar.in)
CLEAN_FILES += $(package.jar.in)
$(package.jar.in): $(MAKEFILE) $(CLASS_FILES.main)
cd $(dir.src); ls -1 org/sqlite/jni/*.java org/sqlite/jni/*.class > $@
@ls -la $@
@echo "To use this jar you will need the -Djava.library.path=DIR/WITH/libsqlite3-jni.so flag."
@echo "To use this jar you will need the -Djava.library.path=DIR/CONTAINING/libsqlite3-jni.so flag."
@echo "e.g. java -jar $@ -Djava.library.path=bld"
$(package.jar): $(CLASS_FILES) $(MAKEFILE) $(package.jar.in)
@ -317,6 +320,10 @@ dist: \
$(bin.version-info) $(sqlite3.canonical.c) \
$(package.jar) $(MAKEFILE)
@echo "Making end-user deliverables..."
@echo "****************************************************************************"; \
echo "*** WARNING: be sure to build this with JDK8 (javac 1.8) for compatibility."; \
echo "*** reasons!"; $$($(bin.javac) -version); \
echo "****************************************************************************"
@rm -fr $(dist-dir.top)
@mkdir -p $(dist-dir.src)
@cp -p $(dist.top.extras) $(dist-dir.top)/.

View File

@ -15,7 +15,10 @@ Technical support is available in the forum:
> **FOREWARNING:** this subproject is very much in development and
subject to any number of changes. Please do not rely on any
information about its API until this disclaimer is removed.
information about its API until this disclaimer is removed. The JNI
bindings released with version 3.43 are a "tech preview" and 3.44
will be "final," at which point strong backward compatibility
guarantees will apply.
Project goals/requirements:
@ -41,13 +44,34 @@ Non-goals:
them off of the C-style API.
Significant TODOs
========================================================================
Hello World
-----------------------------------------------------------------------
- The initial beta release with version 3.43 has severe threading
limitations. Namely, two threads cannot call into the JNI-bound API
at once. This limitation will be remove in a subsequent release.
```java
import org.sqlite.jni.*;
import static SQLite3Jni.*;
...
final sqlite3 db = sqlite3_open(":memory:");
try {
final int rc = sqlite3_errcode(db);
if( 0 != rc ){
if( null != db ){
System.out.print("Error opening db: "+sqlite3_errmsg(db));
}else{
System.out.print("Error opening db: rc="+rc);
}
... handle error ...
}
// ... else use the db ...
}finally{
// ALWAYS close databases using sqlite3_close() or sqlite3_close_v2()
// when done with them. All of their active statement handles must
// first have been passed to sqlite3_finalize().
sqlite3_close_v2(db);
}
```
Building
========================================================================
@ -60,55 +84,108 @@ The canonical builds assumes a Linux-like environment and requires:
Put simply:
```
```console
$ export JAVA_HOME=/path/to/jdk/root
$ make
$ make test
$ make clean
```
The jar distribution can be created with `make jar`, but note that it
does not contain the binary DLL file. A different DLL is needed for
each target platform.
<a id='1to1ish'></a>
One-to-One(-ish) Mapping to C
========================================================================
This JNI binding aims to provide as close to a 1-to-1 experience with
the C API as cross-language semantics allow. Exceptions are
the C API as cross-language semantics allow. Interface changes are
necessarily made where cross-language semantics do not allow a 1-to-1,
and judiciously made where a 1-to-1 mapping would be unduly cumbersome
to use in Java.
to use in Java. In all cases, this binding makes every effort to
provide semantics compatible with the C API documentation even if the
interface to those semantics is slightly different. Any cases which
deviate from those semantics (either removing or adding semantics) are
clearly documented.
Golden Rule: _Never_ Throw from Callbacks
Where it makes sense to do so for usability, Java-side overloads are
provided which accept or return data in alternative forms or provide
sensible default argument values. In all such cases they are thin
proxies around the corresponding C APIs and do not introduce new
semantics.
In some very few cases, Java-specific capabilities have been added in
new APIs, all of which have "_java" somewhere in their names.
Examples include:
- `sqlite3_result_java_object()`
- `sqlite3_column_java_object()`
- `sqlite3_column_java_casted()`
- `sqlite3_value_java_object()`
- `sqlite3_value_java_casted()`
which, as one might surmise, collectively enable the passing of
arbitrary Java objects from user-defined SQL functions through to the
caller.
Golden Rule: Garbage Collection Cannot Free SQLite Resources
------------------------------------------------------------------------
JNI bindings which accept client-defined functions _must never throw
exceptions_ unless _very explicitly documented_ as being
throw-safe. Exceptions are generally reserved for higher-level
bindings which are constructed to specifically deal with them and
ensure that they do not leak C-level resources. Some of the JNI
bindings are provided as Java functions which expect this rule to
always hold.
It is important that all databases and prepared statement handles get
cleaned up by client code. A database cannot be closed if it has open
statement handles. `sqlite3_close()` fails if the db cannot be closed
whereas `sqlite3_close_v2()` recognizes that case and marks the db as
a "zombie," pending finalization when the library detects that all
pending statements have been closed. Be aware that Java garbage
collection _cannot_ close a database or finalize a prepared statement.
Those things require explicit API calls.
UTF-8(-ish)
Golden Rule #2: _Never_ Throw from Callbacks (Unless...)
------------------------------------------------------------------------
SQLite internally uses UTF-8 encoding, whereas Java natively uses
UTF-16. Java JNI has routines for converting to and from UTF-8, _but_
Java uses what its docs call "[modified UTF-8][modutf8]." Care must be
taken when converting Java strings to UTF-8 to ensure that the proper
conversion is performed. In short,
`String.getBytes(StandardCharsets.UTF_8)` performs the proper
conversion in Java, and there is no JNI C API for that conversion
(JNI's `NewStringUTF()` returns MUTF-8).
All routines in this API, barring explicitly documented exceptions,
retain C-like semantics. For example, they are not permitted to throw
or propagate exceptions and must return error information (if any) via
result codes or `null`. The only cases where the C-style APIs may
throw is through client-side misuse, e.g. passing in a null where it
shouldn't be used. The APIs clearly mark function parameters which
should not be null, but does not actively defend itself against such
misuse. Some C-style APIs explicitly accept `null` as a no-op for
usability's sake, and some of the JNI APIs deliberately return an
error code, instead of segfaulting, when passed a `null`.
Known consequences and limitations of this discrepancy include:
Client-defined callbacks _must never throw exceptions_ unless _very
explicitly documented_ as being throw-safe. Exceptions are generally
reserved for higher-level bindings which are constructed to
specifically deal with them and ensure that they do not leak C-level
resources. In some cases, callback handlers are permitted to throw, in
which cases they get translated to C-level result codes and/or
messages. If a callback which is not permitted to throw throws, its
exception may trigger debug output but will otherwise be suppressed.
- Names of databases, tables, and collations must not contain
characters which differ in MUTF-8 and UTF-8, or certain APIs will
mis-translate them on their way between languages. APIs which
transfer other client-side data to Java take extra care to
convert the data at the cost of performance.
The reason some callbacks are permitted to throw and others not is
because all such callbacks act as proxies for C function callback
interfaces and some of those interfaces have no error-reporting
mechanism. Those which are capable of propagating errors back through
the library convert exceptions from callbacks into corresponding
C-level error information. Those which cannot propagate errors
necessarily suppress any exceptions in order to maintain the C-style
semantics of the APIs.
[modutf8]: https://docs.oracle.com/javase/8/docs/api/java/io/DataInput.html#modified-utf-8
Awkward Callback Names
------------------------------------------------------------------------
In places where the Java interface uses callbacks (see below), those
callbacks often have what might fairly be labeled as awkward names,
e.g. `sqlScalarFunction.xFunc()` and `preupdateHook.xPreUpdate()`.
Those names were chosen because they match the corresponding arguments
in the C-level API docs. If they were renamed to be more Java-esque,
documentation transparency would suffer.
Unwieldy Constructs are Re-mapped
@ -126,7 +203,7 @@ A prime example of where interface changes for Java are necessary for
usability is [registration of a custom
collation](https://sqlite.org/c3ref/create_collation.html):
```
```c
// C:
int sqlite3_create_collation(sqlite3 * db, const char * name, int eTextRep,
void *pUserData,
@ -145,7 +222,7 @@ passed that object as their first argument. That data is passed around
bind that part as-is to Java, the result would be awkward to use (^Yes,
we tried this.):
```
```java
// Java:
int sqlite3_create_collation(sqlite3 db, String name, int eTextRep,
Object pUserData, xCompareType xCompare);
@ -160,7 +237,7 @@ for callbacks and (B) having their internal state provided separately,
which is ill-fitting in Java. For the sake of usability, C APIs which
follow that pattern use a slightly different Java interface:
```
```java
int sqlite3_create_collation(sqlite3 db, String name, int eTextRep,
Collation collation);
```
@ -169,7 +246,7 @@ Where the `Collation` class has an abstract `xCompare()` method and
no-op `xDestroy()` method which can be overridden if needed, leading to
a much more Java-esque usage:
```
```java
int rc = sqlite3_create_collation(db, "mycollation", SQLITE_UTF8, new Collation(){
// Required comparison function:
@ -188,8 +265,8 @@ int rc = sqlite3_create_collation(db, "mycollation", SQLITE_UTF8, new Collation(
Noting that:
- It is still possible to bind in call-scope-local state via closures,
if desired.
- It is possible to bind in call-scope-local state via closures, if
desired, as opposed to packing it into the Collation object.
- No capabilities of the C API are lost or unduly obscured via the
above API reshaping, so power users need not make any compromises.
@ -200,6 +277,7 @@ Noting that:
overriding the `xDestroy()` method effectively gives it v2
semantics.
### User-defined SQL Functions (a.k.a. UDFs)
The [`sqlite3_create_function()`](https://sqlite.org/c3ref/create_function.html)
@ -207,12 +285,13 @@ family of APIs make heavy use of function pointers to provide
client-defined callbacks, necessitating interface changes in the JNI
binding. The Java API has only one core function-registration function:
```
```java
int sqlite3_create_function(sqlite3 db, String funcName, int nArgs,
int encoding, SQLFunction func);
```
> Design question: does the encoding argument serve any purpose in JS?
> Design question: does the encoding argument serve any purpose in
Java? That's as-yet undetermined. If not, it will be removed.
`SQLFunction` is not used directly, but is instead instantiated via
one of its three subclasses:
@ -230,5 +309,9 @@ Search [`Tester1.java`](/file/ext/jni/src/org/sqlite/jni/Tester1.java) for
Reminder: see the disclaimer at the top of this document regarding the
in-flux nature of this API.
[jsrc]: /file/
[www]: https://sqlite.org
### And so on...
Various APIs which accept callbacks, e.g. `sqlite3_trace_v2()` and
`sqlite3_update_hook()`, use interfaces similar to those shown above.
Despite the changes in signature, the JNI layer makes every effort to
provide the same semantics as the C API documentation suggests.

View File

@ -6,7 +6,9 @@
# proper top-level JDK directory and, depending on the platform, add a
# platform-specific -I directory. It should build as-is with any
# 2020s-era version of gcc or clang. It requires JDK version 8 or
# higher.
# higher and that JAVA_HOME points to the top-most installation
# directory of that JDK. On Ubuntu-style systems the JDK is typically
# installed under /usr/lib/jvm/java-VERSION-PLATFORM.
default: all
@ -31,10 +33,11 @@ SQLITE_OPT = \
-DSQLITE_OMIT_LOAD_EXTENSION \
-DSQLITE_OMIT_DEPRECATED \
-DSQLITE_OMIT_SHARED_CACHE \
-DSQLITE_THREADSAFE=0 \
-DSQLITE_THREADSAFE=1 \
-DSQLITE_TEMP_STORE=2 \
-DSQLITE_USE_URI=1 \
-DSQLITE_ENABLE_FTS5
-DSQLITE_ENABLE_FTS5 \
-DSQLITE_DEBUG
sqlite3-jni.dll = libsqlite3-jni.so
$(sqlite3-jni.dll):
@ -46,8 +49,10 @@ $(sqlite3-jni.dll):
src/sqlite3-jni.c -shared -o $@
@echo "Now try running it with: make test"
test.flags = -Djava.library.path=. sqlite3-jni-*.jar
test: $(sqlite3-jni.dll)
java -jar -Djava.library.path=. sqlite3-jni-*.jar
java -jar $(test.flags)
java -jar $(test.flags) -t 7 -r 10 -shuffle
clean:
-rm -f $(sqlite3-jni.dll)

File diff suppressed because it is too large Load Diff

View File

@ -843,6 +843,14 @@ JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1parameter_1
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1text
(JNIEnv *, jclass, jobject, jint, jbyteArray, jint);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_bind_text16
* Signature: (Lorg/sqlite/jni/sqlite3_stmt;I[BI)I
*/
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1text16
(JNIEnv *, jclass, jobject, jint, jbyteArray, jint);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_bind_zeroblob
@ -1013,18 +1021,18 @@ JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1table_
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_column_text16
* Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)Ljava/lang/String;
* Method: sqlite3_column_text_utf8
* Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)[B
*/
JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1text16
JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1text_1utf8
(JNIEnv *, jclass, jobject, jint);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_column_text
* Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)[B
* Method: sqlite3_column_text16
* Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)Ljava/lang/String;
*/
JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1text
JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1text16
(JNIEnv *, jclass, jobject, jint);
/*
@ -1083,6 +1091,22 @@ JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1compileoption_
JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1compileoption_1used
(JNIEnv *, jclass, jstring);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_config
* Signature: (I)I
*/
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1config__I
(JNIEnv *, jclass, jint);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_config
* Signature: (Lorg/sqlite/jni/SQLLog;)I
*/
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1config__Lorg_sqlite_jni_SQLLog_2
(JNIEnv *, jclass, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_create_collation
@ -1211,6 +1235,22 @@ JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1finalize
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1initialize
(JNIEnv *, jclass);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_interrupt
* Signature: (Lorg/sqlite/jni/sqlite3;)V
*/
JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1interrupt
(JNIEnv *, jclass, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_is_interrupted
* Signature: (Lorg/sqlite/jni/sqlite3;)Z
*/
JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1is_1interrupted
(JNIEnv *, jclass, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_last_insert_rowid
@ -1275,6 +1315,54 @@ JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1prepare_1v2
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1prepare_1v3
(JNIEnv *, jclass, jobject, jbyteArray, jint, jint, jobject, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_preupdate_blobwrite
* Signature: (Lorg/sqlite/jni/sqlite3;)I
*/
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1blobwrite
(JNIEnv *, jclass, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_preupdate_count
* Signature: (Lorg/sqlite/jni/sqlite3;)I
*/
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1count
(JNIEnv *, jclass, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_preupdate_depth
* Signature: (Lorg/sqlite/jni/sqlite3;)I
*/
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1depth
(JNIEnv *, jclass, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_preupdate_hook
* Signature: (Lorg/sqlite/jni/sqlite3;Lorg/sqlite/jni/PreUpdateHook;)Lorg/sqlite/jni/PreUpdateHook;
*/
JNIEXPORT jobject JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1hook
(JNIEnv *, jclass, jobject, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_preupdate_new
* Signature: (Lorg/sqlite/jni/sqlite3;ILorg/sqlite/jni/OutputPointer/sqlite3_value;)I
*/
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1new
(JNIEnv *, jclass, jobject, jint, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_preupdate_old
* Signature: (Lorg/sqlite/jni/sqlite3;ILorg/sqlite/jni/OutputPointer/sqlite3_value;)I
*/
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1old
(JNIEnv *, jclass, jobject, jint, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_progress_handler
@ -1427,6 +1515,14 @@ JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1text
JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1text64
(JNIEnv *, jclass, jobject, jbyteArray, jlong, jint);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_shutdown
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1shutdown
(JNIEnv *, jclass);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_status
@ -1635,14 +1731,6 @@ JNIEXPORT jlong JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1int64
JNIEXPORT jobject JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1java_1object
(JNIEnv *, jclass, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_value_text
* Signature: (Lorg/sqlite/jni/sqlite3_value;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1text
(JNIEnv *, jclass, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_value_text_utf8
@ -1715,14 +1803,6 @@ JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1frombind
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1subtype
(JNIEnv *, jclass, jobject);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_shutdown
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1shutdown
(JNIEnv *, jclass);
/*
* Class: org_sqlite_jni_SQLite3Jni
* Method: sqlite3_do_something_for_developer

View File

@ -14,18 +14,27 @@
package org.sqlite.jni;
/**
A callback for use with sqlite3_auto_extension().
A callback for use with the sqlite3_auto_extension() family of
APIs.
*/
public interface AutoExtension {
/**
Must function as described for the sqlite3_auto_extension(),
with the caveat that the signature is more limited.
Must function as described for a sqlite3_auto_extension()
callback, with the caveat that the signature is shorter.
As an exception (as it were) to the callbacks-must-not-throw
rule, AutoExtensions may do so and the exception's error message
AutoExtensions may throw and the exception's error message
will be set as the db's error string.
Results are undefined if db is closed by an auto-extension.
Tips for implementations:
- Opening a database from an auto-extension handler will lead to
an endless recursion of the auto-handler triggering itself
indirectly for each newly-opened database.
- If this routine is stateful, it may be useful to make the
overridden method synchronized.
- Results are undefined if db is closed by an auto-extension.
*/
int xEntryPoint(sqlite3 db);
}

View File

@ -27,7 +27,7 @@ public final class Fts5 {
//! Callback type for use with xTokenize() variants
public static interface xTokenizeCallback {
int xToken(int tFlags, byte txt[], int iStart, int iEnd);
int xToken(int tFlags, byte[] txt, int iStart, int iEnd);
}
public static final int FTS5_TOKENIZE_QUERY = 0x0001;

View File

@ -23,7 +23,7 @@ import java.nio.charset.StandardCharsets;
public final class Fts5ExtensionApi extends NativePointerHolder<Fts5ExtensionApi> {
//! Only called from JNI
private Fts5ExtensionApi(){}
private int iVersion = 2;
private final int iVersion = 2;
/* Callback type for used by xQueryPhrase(). */
public static interface xQueryPhraseCallback {
@ -33,54 +33,54 @@ public final class Fts5ExtensionApi extends NativePointerHolder<Fts5ExtensionApi
/**
Returns the singleton instance of this class.
*/
public static synchronized native Fts5ExtensionApi getInstance();
public static native Fts5ExtensionApi getInstance();
public synchronized native int xColumnCount(@NotNull Fts5Context fcx);
public synchronized native int xColumnSize(@NotNull Fts5Context cx, int iCol,
public native int xColumnCount(@NotNull Fts5Context fcx);
public native int xColumnSize(@NotNull Fts5Context cx, int iCol,
@NotNull OutputPointer.Int32 pnToken);
public synchronized native int xColumnText(@NotNull Fts5Context cx, int iCol,
public native int xColumnText(@NotNull Fts5Context cx, int iCol,
@NotNull OutputPointer.String txt);
public synchronized native int xColumnTotalSize(@NotNull Fts5Context fcx, int iCol,
public native int xColumnTotalSize(@NotNull Fts5Context fcx, int iCol,
@NotNull OutputPointer.Int64 pnToken);
public synchronized native Object xGetAuxdata(@NotNull Fts5Context cx, boolean clearIt);
public synchronized native int xInst(@NotNull Fts5Context cx, int iIdx,
public native Object xGetAuxdata(@NotNull Fts5Context cx, boolean clearIt);
public native int xInst(@NotNull Fts5Context cx, int iIdx,
@NotNull OutputPointer.Int32 piPhrase,
@NotNull OutputPointer.Int32 piCol,
@NotNull OutputPointer.Int32 piOff);
public synchronized native int xInstCount(@NotNull Fts5Context fcx,
public native int xInstCount(@NotNull Fts5Context fcx,
@NotNull OutputPointer.Int32 pnInst);
public synchronized native int xPhraseCount(@NotNull Fts5Context fcx);
public synchronized native int xPhraseFirst(@NotNull Fts5Context cx, int iPhrase,
public native int xPhraseCount(@NotNull Fts5Context fcx);
public native int xPhraseFirst(@NotNull Fts5Context cx, int iPhrase,
@NotNull Fts5PhraseIter iter,
@NotNull OutputPointer.Int32 iCol,
@NotNull OutputPointer.Int32 iOff);
public synchronized native int xPhraseFirstColumn(@NotNull Fts5Context cx, int iPhrase,
public native int xPhraseFirstColumn(@NotNull Fts5Context cx, int iPhrase,
@NotNull Fts5PhraseIter iter,
@NotNull OutputPointer.Int32 iCol);
public synchronized native void xPhraseNext(@NotNull Fts5Context cx,
public native void xPhraseNext(@NotNull Fts5Context cx,
@NotNull Fts5PhraseIter iter,
@NotNull OutputPointer.Int32 iCol,
@NotNull OutputPointer.Int32 iOff);
public synchronized native void xPhraseNextColumn(@NotNull Fts5Context cx,
public native void xPhraseNextColumn(@NotNull Fts5Context cx,
@NotNull Fts5PhraseIter iter,
@NotNull OutputPointer.Int32 iCol);
public synchronized native int xPhraseSize(@NotNull Fts5Context fcx, int iPhrase);
public synchronized native int xQueryPhrase(@NotNull Fts5Context cx, int iPhrase,
public native int xPhraseSize(@NotNull Fts5Context fcx, int iPhrase);
public native int xQueryPhrase(@NotNull Fts5Context cx, int iPhrase,
@NotNull xQueryPhraseCallback callback);
public synchronized native int xRowCount(@NotNull Fts5Context fcx,
public native int xRowCount(@NotNull Fts5Context fcx,
@NotNull OutputPointer.Int64 nRow);
public synchronized native long xRowid(@NotNull Fts5Context cx);
public native long xRowid(@NotNull Fts5Context cx);
/* Note that the JNI binding lacks the C version's xDelete()
callback argument. Instead, if pAux has an xDestroy() method, it
is called if the FTS5 API finalizes the aux state (including if
allocation of storage for the auxdata fails). Any reference to
pAux held by the JNI layer will be relinquished regardless of
whether pAux has an xDestroy() method. */
public synchronized native int xSetAuxdata(@NotNull Fts5Context cx, @Nullable Object pAux);
public synchronized native int xTokenize(@NotNull Fts5Context cx, @NotNull byte pText[],
public native int xSetAuxdata(@NotNull Fts5Context cx, @Nullable Object pAux);
public native int xTokenize(@NotNull Fts5Context cx, @NotNull byte[] pText,
@NotNull Fts5.xTokenizeCallback callback);
public synchronized native Object xUserData(Fts5Context cx);
public native Object xUserData(Fts5Context cx);
//^^^ returns the pointer passed as the 3rd arg to the C-level
// fts5_api::xCreateFunction.
// fts5_api::xCreateFunction().
}

View File

@ -28,6 +28,6 @@ package org.sqlite.jni;
*/
public class NativePointerHolder<ContextType> {
//! Only set from JNI, where access permissions don't matter.
private long nativePointer = 0;
private volatile long nativePointer = 0;
public final long getNativePointer(){ return nativePointer; }
}

View File

@ -36,6 +36,9 @@ package org.sqlite.jni;
access to the object's value via the `value` property, whereas the
JNI-level opaque types do not permit client-level code to set that
property.
Warning: do not share instances of these classes across
threads. Doing so may lead to corrupting sqlite3-internal state.
*/
public final class OutputPointer {
@ -83,6 +86,28 @@ public final class OutputPointer {
}
}
/**
Output pointer for use with routines, such as sqlite3_prepupdate_new(),
which return a sqlite3_value handle via an output pointer. These
pointers can only be set by the JNI layer, not by client-level
code.
*/
public static final class sqlite3_value {
private org.sqlite.jni.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;}
//! Equivalent to calling get() then clear().
public final org.sqlite.jni.sqlite3_value take(){
final org.sqlite.jni.sqlite3_value v = value;
value = null;
return v;
}
}
/**
Output pointer for use with native routines which return integers via
output pointers.

View File

@ -0,0 +1,29 @@
/*
** 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;
/**
A callback for use with sqlite3_preupdate_hook().
*/
public interface PreUpdateHook {
/**
Must function as described for the sqlite3_preupdate_hook().
callback, with the slight signature change.
Must not throw. Any exceptions may emit debugging messages and
will be suppressed.
*/
void xPreUpdate(sqlite3 db, int op, String dbName, String dbTable,
long iKey1, long iKey2 );
}

View File

@ -21,17 +21,19 @@ package org.sqlite.jni;
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<T>, and Window<T>. Most simply, clients may create
anonymous classes from those 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.
Aggregate<T>, and Window<T>. 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 abstract class SQLFunction {
/**
PerContextState assists aggregate and window functions in
managinga their accumulator state across calls to the UDF's
managing their accumulator state across calls to the UDF's
callbacks.
If a given aggregate or window function is called multiple times
@ -96,7 +98,11 @@ public abstract class SQLFunction {
//! Subclass for creating scalar functions.
public static abstract class Scalar extends SQLFunction {
//! As for the xFunc() argument of the C API's sqlite3_create_function()
/**
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(sqlite3_context cx, sqlite3_value[] args);
/**
@ -114,10 +120,18 @@ public abstract class SQLFunction {
*/
public static abstract class Aggregate<T> extends SQLFunction {
//! As for the xStep() argument of the C API's sqlite3_create_function()
/**
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()
/**
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);
/**
@ -163,10 +177,18 @@ public abstract class SQLFunction {
*/
public static abstract class Window<T> extends Aggregate<T> {
//! As for the xInverse() argument of the C API's sqlite3_create_window_function()
/**
As for the xInverse() argument of the C API's
sqlite3_create_window_function(). If this function throws, the
exception is not propagated and a warning might be emitted
to a debugging channel.
*/
public abstract void xInverse(sqlite3_context cx, sqlite3_value[] args);
//! As for the xValue() argument of the C API's sqlite3_create_window_function()
/**
As for the xValue() argument of the C API's sqlite3_create_window_function().
See xInverse() for the fate of any exceptions this throws.
*/
public abstract void xValue(sqlite3_context cx);
}
}

View File

@ -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;
/**
A callback for use with sqlite3_config(SQLLog).
*/
public interface SQLLog {
/**
Must function as described for sqlite3_config(SQLITE_CONFIG_SQLLOG)
callback, with the slight signature change.
*/
void xSqllog(sqlite3 db, String msg, int msgType );
}

File diff suppressed because it is too large Load Diff

View File

@ -15,65 +15,116 @@ package org.sqlite.jni;
import static org.sqlite.jni.SQLite3Jni.*;
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 java.util.concurrent.Future;
public class Tester1 {
/**
An annotation for Tester1 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{}
public class Tester1 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 boolean listRunTests = false;
//! 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<java.lang.reflect.Method> testMethods = null;
//! List of exceptions collected by run()
private static List<Exception> listErrors = new ArrayList<>();
private static final class Metrics {
int dbOpen;
//! Number of times createNewDb() (or equivalent) is invoked.
volatile int dbOpen = 0;
}
private Integer tId;
Tester1(Integer id){
tId = id;
}
static final Metrics metrics = new Metrics();
private static final OutputPointer.sqlite3_stmt outStmt
= new OutputPointer.sqlite3_stmt();
public static void out(Object val){
System.out.print(val);
public synchronized static void outln(){
if( !quietMode ){
System.out.println("");
}
}
public static void outln(Object val){
System.out.println(val);
public synchronized static void outln(Object val){
if( !quietMode ){
System.out.print(Thread.currentThread().getName()+": ");
System.out.println(val);
}
}
public synchronized static void out(Object val){
if( !quietMode ){
System.out.print(val);
}
}
@SuppressWarnings("unchecked")
public static void out(Object... vals){
int n = 0;
for(Object v : vals) out((n++>0 ? " " : "")+v);
public synchronized static void out(Object... vals){
if( !quietMode ){
System.out.print(Thread.currentThread().getName()+": ");
for(Object v : vals) out(v);
}
}
@SuppressWarnings("unchecked")
public static void outln(Object... vals){
out(vals); out("\n");
public synchronized static void outln(Object... vals){
if( !quietMode ){
out(vals); out("\n");
}
}
static int affirmCount = 0;
public static void affirm(Boolean v){
static volatile int affirmCount = 0;
public synchronized static void affirm(Boolean v, String comment){
++affirmCount;
assert( v /* prefer assert over exception if it's enabled because
the JNI layer sometimes has to suppress exceptions. */);
if( !v ) throw new RuntimeException("Assertion failed.");
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);
}
private static void test1(){
outln("libversion_number:",
sqlite3_libversion_number()
+ "\n"
+ sqlite3_libversion()
+ "\n"
+ SQLITE_SOURCE_ID);
public static void affirm(Boolean v){
affirm(v, "Affirmation failed.");
}
@ManualTest /* because testing this for threading is pointless */
private void test1(){
affirm(sqlite3_libversion_number() == SQLITE_VERSION_NUMBER);
//outln("threadsafe = "+sqlite3_threadsafe());
affirm(SQLITE_MAX_LENGTH > 0);
affirm(SQLITE_MAX_TRIGGER_DEPTH>0);
}
public static sqlite3 createNewDb(){
static sqlite3 createNewDb(){
final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
int rc = sqlite3_open(":memory:", out);
++metrics.dbOpen;
sqlite3 db = out.take();
if( 0!=rc ){
final String msg = db.getNativePointer()==0
? sqlite3_errstr(rc)
: sqlite3_errmsg(db);
final String msg =
null==db ? sqlite3_errstr(rc) : sqlite3_errmsg(db);
sqlite3_close(db);
throw new RuntimeException("Opening db failed: "+msg);
}
affirm( null == out.get() );
@ -83,17 +134,18 @@ public class Tester1 {
return db;
}
public static void execSql(sqlite3 db, String[] sql){
static void execSql(sqlite3 db, String[] sql){
execSql(db, String.join("", sql));
}
public static int execSql(sqlite3 db, boolean throwOnError, String sql){
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;
byte[] sqlChunk = sqlUtf8;
int rc = 0;
sqlite3_stmt stmt = null;
final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
while(pos < sqlChunk.length){
if(pos > 0){
sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
@ -127,21 +179,27 @@ public class Tester1 {
return rc;
}
public static void execSql(sqlite3 db, String sql){
static void execSql(sqlite3 db, String sql){
execSql(db, true, sql);
}
public static sqlite3_stmt prepare(sqlite3 db, String sql){
outStmt.clear();
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);
affirm( 0 == rc );
if( throwOnError ){
affirm( 0 == rc );
}
final sqlite3_stmt rv = outStmt.take();
affirm( null == outStmt.get() );
affirm( 0 != rv.getNativePointer() );
if( throwOnError ){
affirm( 0 != rv.getNativePointer() );
}
return rv;
}
private static void testCompileOption(){
static sqlite3_stmt prepare(sqlite3 db, String sql){
return prepare(db, true, sql);
}
private void showCompileOption(){
int i = 0;
String optName;
outln("compile options:");
@ -152,7 +210,7 @@ public class Tester1 {
}
private static void testOpenDb1(){
private void testOpenDb1(){
final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
int rc = sqlite3_open(":memory:", out);
++metrics.dbOpen;
@ -163,11 +221,18 @@ public class Tester1 {
/* 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. */;
// 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.
affirm( !sqlite3_is_interrupted(db) );
sqlite3_interrupt(db);
affirm( sqlite3_is_interrupted(db) );
sqlite3_close_v2(db);
affirm(0 == db.getNativePointer());
}
private static void testOpenDb2(){
private void testOpenDb2(){
final OutputPointer.sqlite3 out = new OutputPointer.sqlite3();
int rc = sqlite3_open_v2(":memory:", out,
SQLITE_OPEN_READWRITE
@ -180,14 +245,18 @@ public class Tester1 {
affirm(0 == db.getNativePointer());
}
private static void testPrepare123(){
private void testPrepare123(){
sqlite3 db = createNewDb();
int rc;
final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt();
rc = sqlite3_prepare(db, "CREATE TABLE t1(a);", outStmt);
affirm(0 == rc);
sqlite3_stmt stmt = outStmt.get();
sqlite3_stmt stmt = outStmt.take();
affirm(0 != stmt.getNativePointer());
rc = sqlite3_step(stmt);
if( SQLITE_DONE != rc ){
outln("step failed ??? ",rc, " ",sqlite3_errmsg(db));
}
affirm(SQLITE_DONE == rc);
sqlite3_finalize(stmt);
affirm(0 == stmt.getNativePointer());
@ -236,10 +305,20 @@ public class Tester1 {
affirm(0 != stmt.getNativePointer());
sqlite3_finalize(stmt);
affirm(0 == stmt.getNativePointer() );
affirm( 0==sqlite3_errcode(db) );
stmt = sqlite3_prepare(db, "intentional error");
affirm( null==stmt );
affirm( 0!=sqlite3_errcode(db) );
affirm( 0==sqlite3_errmsg(db).indexOf("near \"intentional\"") );
sqlite3_finalize(stmt);
stmt = sqlite3_prepare(db, "/* empty input*/\n-- comments only");
affirm( null==stmt );
affirm( 0==sqlite3_errcode(db) );
sqlite3_close_v2(db);
}
private static void testBindFetchInt(){
private void testBindFetchInt(){
sqlite3 db = createNewDb();
execSql(db, "CREATE TABLE t(a)");
@ -286,7 +365,7 @@ public class Tester1 {
affirm(0 == db.getNativePointer());
}
private static void testBindFetchInt64(){
private void testBindFetchInt64(){
sqlite3 db = createNewDb();
execSql(db, "CREATE TABLE t(a)");
sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
@ -308,7 +387,7 @@ public class Tester1 {
sqlite3_close_v2(db);
}
private static void testBindFetchDouble(){
private void testBindFetchDouble(){
sqlite3 db = createNewDb();
execSql(db, "CREATE TABLE t(a)");
sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
@ -333,14 +412,17 @@ public class Tester1 {
sqlite3_close_v2(db);
}
private static void testBindFetchText(){
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", "!" };
int rc;
int n = 0;
for( String e : list1 ){
rc = sqlite3_bind_text(stmt, 1, e);
rc = (0==n)
? sqlite3_bind_text(stmt, 1, e)
: sqlite3_bind_text16(stmt, 1, e);
affirm(0 == rc);
rc = sqlite3_step(stmt);
affirm(SQLITE_DONE==rc);
@ -349,7 +431,7 @@ public class Tester1 {
sqlite3_finalize(stmt);
stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
StringBuilder sbuf = new StringBuilder();
int n = 0;
n = 0;
while( SQLITE_ROW == sqlite3_step(stmt) ){
String txt = sqlite3_column_text16(stmt, 0);
//outln("txt = "+txt);
@ -362,7 +444,7 @@ public class Tester1 {
sqlite3_close_v2(db);
}
private static void testBindFetchBlob(){
private void testBindFetchBlob(){
sqlite3 db = createNewDb();
execSql(db, "CREATE TABLE t(a)");
sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
@ -391,7 +473,7 @@ public class Tester1 {
sqlite3_close_v2(db);
}
private static void testSql(){
private void testSql(){
sqlite3 db = createNewDb();
sqlite3_stmt stmt = prepare(db, "SELECT 1");
affirm( "SELECT 1".equals(sqlite3_sql(stmt)) );
@ -400,9 +482,10 @@ public class Tester1 {
sqlite3_bind_text(stmt, 1, "hell😃");
affirm( "SELECT 'hell😃'".equals(sqlite3_expanded_sql(stmt)) );
sqlite3_finalize(stmt);
sqlite3_close(db);
}
private static void testCollation(){
private void testCollation(){
final sqlite3 db = createNewDb();
execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')");
final ValueHolder<Boolean> xDestroyCalled = new ValueHolder<>(false);
@ -472,14 +555,13 @@ public class Tester1 {
rc = sqlite3_collation_needed(db, null);
affirm( 0 == rc );
sqlite3_close_v2(db);
affirm( 0 == db.getNativePointer() );
affirm(xDestroyCalled.value);
}
private static void testToUtf8(){
@ManualTest /* because threading is meaningless here */
private void testToUtf8(){
/**
Java docs seem contradictory, claiming to use "modified UTF-8"
encoding while also claiming to export using RFC 2279:
https://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html
Let's ensure that we can convert to standard UTF-8 in Java code
@ -489,7 +571,7 @@ public class Tester1 {
affirm( 5 == ba.length /* as opposed to 6 in modified utf-8 */);
}
private static void testStatus(){
private void testStatus(){
final OutputPointer.Int64 cur64 = new OutputPointer.Int64();
final OutputPointer.Int64 high64 = new OutputPointer.Int64();
final OutputPointer.Int32 cur32 = new OutputPointer.Int32();
@ -517,7 +599,7 @@ public class Tester1 {
sqlite3_close_v2(db);
}
private static void testUdf1(){
private void testUdf1(){
final sqlite3 db = createNewDb();
// These ValueHolders are just to confirm that the func did what we want...
final ValueHolder<Boolean> xDestroyCalled = new ValueHolder<>(false);
@ -561,7 +643,49 @@ public class Tester1 {
affirm( xDestroyCalled.value );
}
private static void testUdfJavaObject(){
private void testUdfThrows(){
final sqlite3 db = createNewDb();
final ValueHolder<Integer> xFuncAccum = new ValueHolder<>(0);
SQLFunction funcAgg = new SQLFunction.Aggregate<Integer>(){
@Override public void xStep(sqlite3_context cx, sqlite3_value[] args){
/** Throwing from here should emit loud noise on stdout or stderr
but the exception is supressed because we have no way to inform
sqlite about it from these callbacks. */
//throw new RuntimeException("Throwing from an xStep");
}
@Override public void xFinal(sqlite3_context cx){
throw new RuntimeException("Throwing from an xFinal");
}
};
int rc = sqlite3_create_function(db, "myagg", 1, SQLITE_UTF8, funcAgg);
affirm(0 == rc);
affirm(0 == xFuncAccum.value);
sqlite3_stmt stmt = prepare(db, "SELECT myagg(1)");
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
affirm( 0 != rc );
affirm( sqlite3_errmsg(db).indexOf("an xFinal") > 0 );
SQLFunction funcSc = new SQLFunction.Scalar(){
@Override public void xFunc(sqlite3_context cx, sqlite3_value[] args){
throw new RuntimeException("Throwing from an xFunc");
}
};
rc = sqlite3_create_function(db, "mysca", 0, SQLITE_UTF8, funcSc);
affirm(0 == rc);
affirm(0 == xFuncAccum.value);
stmt = prepare(db, "SELECT mysca()");
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
affirm( 0 != rc );
affirm( sqlite3_errmsg(db).indexOf("an xFunc") > 0 );
rc = sqlite3_create_function(db, "mysca", 1, -1, funcSc);
affirm( SQLITE_FORMAT==rc, "invalid encoding value." );
sqlite3_close_v2(db);
}
private void testUdfJavaObject(){
final sqlite3 db = createNewDb();
final ValueHolder<sqlite3> testResult = new ValueHolder<>(db);
final SQLFunction func = new SQLFunction.Scalar(){
@ -590,7 +714,7 @@ public class Tester1 {
sqlite3_close_v2(db);
}
private static void testUdfAggregate(){
private void testUdfAggregate(){
final sqlite3 db = createNewDb();
final ValueHolder<Boolean> xFinalNull =
// To confirm that xFinal() is called with no aggregate state
@ -652,7 +776,7 @@ public class Tester1 {
sqlite3_close_v2(db);
}
private static void testUdfWindow(){
private void testUdfWindow(){
final sqlite3 db = createNewDb();
/* Example window function, table, and results taken from:
https://sqlite.org/windowfunctions.html#udfwinfunc */
@ -709,14 +833,14 @@ public class Tester1 {
sqlite3_close_v2(db);
}
private static void listBoundMethods(){
private void listBoundMethods(){
if(false){
final java.lang.reflect.Field[] declaredFields =
SQLite3Jni.class.getDeclaredFields();
outln("Bound constants:\n");
for(java.lang.reflect.Field field : declaredFields) {
if(java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
outln("\t"+field.getName());
outln("\t",field.getName());
}
}
}
@ -735,12 +859,12 @@ public class Tester1 {
java.util.Collections.sort(funcList);
for(String n : funcList){
++count;
outln("\t"+n+"()");
outln("\t",n,"()");
}
outln(count+" functions named sqlite3_*.");
outln(count," functions named sqlite3_*.");
}
private static void testTrace(){
private void testTrace(){
final sqlite3 db = createNewDb();
final ValueHolder<Integer> counter = new ValueHolder<>(0);
/* Ensure that characters outside of the UTF BMP survive the trip
@ -789,9 +913,11 @@ public class Tester1 {
affirm( 7 == counter.value );
}
private static void testBusy(){
@ManualTest /* because threads inherently break this test */
private 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;
@ -839,7 +965,7 @@ public class Tester1 {
}
}
private static void testProgress(){
private void testProgress(){
final sqlite3 db = createNewDb();
final ValueHolder<Integer> counter = new ValueHolder<>(0);
sqlite3_progress_handler(db, 1, new ProgressHandler(){
@ -857,7 +983,7 @@ public class Tester1 {
sqlite3_close_v2(db);
}
private static void testCommitHook(){
private void testCommitHook(){
final sqlite3 db = createNewDb();
final ValueHolder<Integer> counter = new ValueHolder<>(0);
final ValueHolder<Integer> hookResult = new ValueHolder<>(0);
@ -906,12 +1032,13 @@ public class Tester1 {
sqlite3_close_v2(db);
}
private static void testUpdateHook(){
private void testUpdateHook(){
final sqlite3 db = createNewDb();
final ValueHolder<Integer> counter = new ValueHolder<>(0);
final ValueHolder<Integer> expectedOp = new ValueHolder<>(0);
final UpdateHook theHook = new UpdateHook(){
@SuppressWarnings("unchecked")
@Override
public void xUpdateHook(int opId, String dbName, String tableName, long rowId){
++counter.value;
if( 0!=expectedOp.value ){
@ -955,7 +1082,84 @@ public class Tester1 {
sqlite3_close_v2(db);
}
private static void testRollbackHook(){
/**
This test is functionally identical to testUpdateHook(), only with a
different callback type.
*/
private synchronized void testPreUpdateHook(){
if( !sqlite3_compileoption_used("ENABLE_PREUPDATE_HOOK") ){
//outln("Skipping testPreUpdateHook(): no pre-update hook support.");
return;
}
final sqlite3 db = createNewDb();
final ValueHolder<Integer> counter = new ValueHolder<>(0);
final ValueHolder<Integer> expectedOp = new ValueHolder<>(0);
final PreUpdateHook theHook = new PreUpdateHook(){
@SuppressWarnings("unchecked")
@Override
public void xPreUpdate(sqlite3 db, int opId, String dbName, String dbTable,
long iKey1, long iKey2 ){
++counter.value;
switch( opId ){
case SQLITE_UPDATE:
affirm( 0 < sqlite3_preupdate_count(db) );
affirm( null != sqlite3_preupdate_new(db, 0) );
affirm( null != sqlite3_preupdate_old(db, 0) );
break;
case SQLITE_INSERT:
affirm( null != sqlite3_preupdate_new(db, 0) );
break;
case SQLITE_DELETE:
affirm( null != sqlite3_preupdate_old(db, 0) );
break;
default:
break;
}
if( 0!=expectedOp.value ){
affirm( expectedOp.value == opId );
}
}
};
PreUpdateHook oldHook = sqlite3_preupdate_hook(db, 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 = sqlite3_preupdate_hook(db, theHook);
affirm( theHook == oldHook );
expectedOp.value = SQLITE_DELETE;
execSql(db, "DELETE FROM t where a='d'");
affirm( 5 == counter.value );
oldHook = sqlite3_preupdate_hook(db, null);
affirm( theHook == oldHook );
execSql(db, "update t set a='e' where a='b';");
affirm( 5 == counter.value );
oldHook = sqlite3_preupdate_hook(db, null);
affirm( null == oldHook );
final PreUpdateHook newHook = new PreUpdateHook(){
@Override
public void xPreUpdate(sqlite3 db, int opId, String dbName,
String tableName, long iKey1, long iKey2){
}
};
oldHook = sqlite3_preupdate_hook(db, newHook);
affirm( null == oldHook );
execSql(db, "update t set a='h' where a='a'");
affirm( 5 == counter.value );
oldHook = sqlite3_preupdate_hook(db, theHook);
affirm( newHook == oldHook );
expectedOp.value = SQLITE_UPDATE;
execSql(db, "update t set a='i' where a='h'");
affirm( 6 == counter.value );
sqlite3_close_v2(db);
}
private void testRollbackHook(){
final sqlite3 db = createNewDb();
final ValueHolder<Integer> counter = new ValueHolder<>(0);
final RollbackHook theHook = new RollbackHook(){
@ -993,9 +1197,11 @@ public class Tester1 {
it throws.
*/
@SuppressWarnings("unchecked")
private static void testFts5() throws Exception {
if( !SQLITE_ENABLE_FTS5 ){
outln("SQLITE_ENABLE_FTS5 is not set. Skipping FTS5 tests.");
@ManualTest /* because the Fts5 parts are not yet known to be
thread-safe */
private void testFts5() throws Exception {
if( !sqlite3_compileoption_used("ENABLE_FTS5") ){
//outln("SQLITE_ENABLE_FTS5 is not set. Skipping FTS5 tests.");
return;
}
Exception err = null;
@ -1021,7 +1227,7 @@ public class Tester1 {
}
}
private static void testAuthorizer(){
private void testAuthorizer(){
final sqlite3 db = createNewDb();
final ValueHolder<Integer> counter = new ValueHolder<>(0);
final ValueHolder<Integer> authRc = new ValueHolder<>(0);
@ -1043,7 +1249,9 @@ public class Tester1 {
sqlite3_close(db);
}
private static void testAutoExtension(){
@ManualTest/* because multiple threads legitimately make these
results unpredictable */
private synchronized void testAutoExtension(){
final ValueHolder<Integer> val = new ValueHolder<>(0);
final ValueHolder<String> toss = new ValueHolder<>(null);
final AutoExtension ax = new AutoExtension(){
@ -1071,73 +1279,305 @@ public class Tester1 {
affirm( 0==rc );
sqlite3_close( createNewDb() );
affirm( 3==val.value );
sqlite3 db = createNewDb();
affirm( 4==val.value );
execSql(db, "ATTACH ':memory' as foo");
affirm( 4==val.value, "ATTACH uses the same connection, not sub-connections." );
sqlite3_close(db);
db = null;
affirm( sqlite3_cancel_auto_extension(ax) );
affirm( !sqlite3_cancel_auto_extension(ax) );
sqlite3_close(createNewDb());
affirm( 3==val.value );
affirm( 4==val.value );
rc = sqlite3_auto_extension( ax );
affirm( 0==rc );
Exception err = null;
toss.value = "Throwing from AutoExtension.";
try{
createNewDb();
sqlite3_close(createNewDb());
}catch(Exception e){
err = e;
}
affirm( err!=null );
affirm( err.getMessage().indexOf(toss.value)>0 );
toss.value = null;
val.value = 0;
final AutoExtension ax2 = new AutoExtension(){
public synchronized int xEntryPoint(sqlite3 db){
++val.value;
return 0;
}
};
rc = sqlite3_auto_extension( ax2 );
affirm( 0 == rc );
sqlite3_close(createNewDb());
affirm( 2 == val.value );
affirm( sqlite3_cancel_auto_extension(ax) );
affirm( !sqlite3_cancel_auto_extension(ax) );
sqlite3_close(createNewDb());
affirm( 3 == val.value );
rc = sqlite3_auto_extension( ax );
affirm( 0 == rc );
sqlite3_close(createNewDb());
affirm( 5 == val.value );
affirm( sqlite3_cancel_auto_extension(ax2) );
affirm( !sqlite3_cancel_auto_extension(ax2) );
sqlite3_close(createNewDb());
affirm( 6 == val.value );
rc = sqlite3_auto_extension( ax2 );
affirm( 0 == rc );
sqlite3_close(createNewDb());
affirm( 8 == val.value );
sqlite3_reset_auto_extension();
sqlite3_close(createNewDb());
affirm( 8 == val.value );
affirm( !sqlite3_cancel_auto_extension(ax) );
affirm( !sqlite3_cancel_auto_extension(ax2) );
sqlite3_close(createNewDb());
affirm( 8 == val.value );
}
private static void testSleep(){
@ManualTest /* because we only want to run this test manually */
private void testSleep(){
out("Sleeping briefly... ");
sqlite3_sleep(600);
outln("Woke up.");
}
public static void main(String[] args) throws Exception {
final long timeStart = System.nanoTime();
test1();
if(false) testCompileOption();
final java.util.List<String> liArgs =
java.util.Arrays.asList(args);
testOpenDb1();
testOpenDb2();
testPrepare123();
testBindFetchInt();
testBindFetchInt64();
testBindFetchDouble();
testBindFetchText();
testBindFetchBlob();
testSql();
testCollation();
testToUtf8();
testStatus();
testUdf1();
testUdfJavaObject();
testUdfAggregate();
testUdfWindow();
testTrace();
testBusy();
testProgress();
testCommitHook();
testRollbackHook();
testUpdateHook();
testAuthorizer();
testFts5();
testAutoExtension();
//testSleep();
if(liArgs.indexOf("-v")>0){
sqlite3_do_something_for_developer();
//listBoundMethods();
private void nap() throws InterruptedException {
if( takeNaps ){
Thread.sleep(java.util.concurrent.ThreadLocalRandom.current().nextInt(3, 17), 0);
}
final long timeEnd = System.nanoTime();
affirm( SQLite3Jni.uncacheJniEnv() );
affirm( !SQLite3Jni.uncacheJniEnv() );
outln("Tests done. Metrics:");
outln("\tAssertions checked: "+affirmCount);
outln("\tDatabases opened: "+metrics.dbOpen);
}
@ManualTest /* because we only want to run this test on demand */
private void testFail(){
affirm( false, "Intentional failure." );
}
private void runTests(boolean fromThread) throws Exception {
if(false) showCompileOption();
List<java.lang.reflect.Method> mlist = testMethods;
affirm( null!=mlist );
if( shuffle ){
mlist = new ArrayList<>( testMethods.subList(0, testMethods.size()) );
java.util.Collections.shuffle(mlist);
}
if( listRunTests ){
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();
}
}
testToUtf8();
test1();
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;
}
}
if( !fromThread ){
testBusy();
if( !mtMode ){
testAutoExtension() /* threads rightfully muck up these results */;
testFts5();
}
}
synchronized( this.getClass() ){
++nTestRuns;
}
}
public void run() {
try {
runTests(0!=this.tId);
}catch(Exception e){
synchronized( listErrors ){
listErrors.add(e);
}
}finally{
affirm( SQLite3Jni.uncacheJniEnv() );
affirm( !SQLite3Jni.uncacheJniEnv() );
}
}
/**
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. This is noisy in multi-threaded mode.
-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 = null;
boolean doSomethingForDev = false;
Integer nRepeat = 1;
boolean forceFail = false;
boolean sqlLog = 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 = true;
}else if(arg.equals("fail")){
forceFail = true;
}else if(arg.equals("sqllog")){
sqlLog = 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);
}
}
}
{
// Build list of tests to run from the methods named test*().
testMethods = new ArrayList<>();
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( ManualTest.class ) ){
if( name.startsWith("test") ){
testMethods.add(m);
}
}
}
}
if( sqlLog ){
if( sqlite3_compileoption_used("ENABLE_SQLLOG") ){
int rc = sqlite3_config( new SQLLog() {
@Override public void xSqllog(sqlite3 db, String msg, int op){
switch(op){
case 0: outln("Opening db: ",db); break;
case 1: outln(db,": ",msg); break;
case 2: outln("Closing db: ",db); break;
}
}
});
affirm( 0==rc );
}else{
outln("WARNING: -sqllog is not active because library was built ",
"without SQLITE_ENABLE_SQLLOG.");
}
}
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.");
final long timeStart = System.currentTimeMillis();
int nLoop = 0;
affirm( 0==sqlite3_config( SQLITE_CONFIG_SINGLETHREAD ),
"Could not switch to single-thread mode." );
affirm( 0==sqlite3_config( SQLITE_CONFIG_MULTITHREAD ),
"Could not switch to multithread mode." );
affirm( 0==sqlite3_config( SQLITE_CONFIG_SERIALIZED ),
"Could not switch to serialized threading mode." );
outln("libversion_number: ",
sqlite3_libversion_number(),"\n",
sqlite3_libversion(),"\n",SQLITE_SOURCE_ID);
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 ){
if( nThread==null || nThread<=1 ){
new Tester1(0).runTests(false);
continue;
}
Tester1.mtMode = true;
final ExecutorService ex = Executors.newFixedThreadPool( nThread );
//final List<Future<?>> futures = new ArrayList<>();
++nLoop;
out((1==nLoop ? "" : " ")+nLoop);
for( int i = 0; i < nThread; ++i ){
ex.submit( new Tester1(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;
}
}
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 ){
sqlite3_do_something_for_developer();
}
sqlite3_shutdown();
int nMethods = 0;
int nNatives = 0;
final java.lang.reflect.Method[] declaredMethods =
@ -1154,10 +1594,10 @@ public class Tester1 {
}
}
}
outln("\tSQLite3Jni sqlite3_*() methods: "+
outln("\tSQLite3Jni.sqlite3_*() methods: "+
nNatives+" native methods and "+
(nMethods - nNatives)+" Java impls");
outln("\tTotal time = "
+((timeEnd - timeStart)/1000000.0)+"ms");
outln("\tTotal test time = "
+(timeEnd - timeStart)+"ms");
}
}

View File

@ -72,16 +72,18 @@ public class TesterFts5 {
affirm( xDestroyCalled.value );
}
public TesterFts5(){
int oldAffirmCount = Tester1.affirmCount;
Tester1.affirmCount = 0;
final long timeStart = System.nanoTime();
test1();
final long timeEnd = System.nanoTime();
outln("FTS5 Tests done. Metrics:");
outln("\tAssertions checked: "+Tester1.affirmCount);
outln("\tTotal time = "
+((timeEnd - timeStart)/1000000.0)+"ms");
Tester1.affirmCount = oldAffirmCount;
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); }
}

View File

@ -19,8 +19,7 @@ package org.sqlite.jni;
*/
public final class sqlite3_context extends NativePointerHolder<sqlite3_context> {
/**
For use only by the JNI layer. It's permitted to set this even
though it's private.
Only set by the JNI layer.
*/
private long aggregateContext = 0;

View File

@ -250,14 +250,14 @@ public class SQLTester {
}
public void runTests() throws Exception {
final long tStart = System.nanoTime();
final long tStart = System.currentTimeMillis();
for(String f : listInFiles){
reset();
++nTestFile;
final TestScript ts = new TestScript(f);
outln(nextStartEmoji(), " starting [",f,"]");
boolean threw = false;
final long timeStart = System.nanoTime();
final long timeStart = System.currentTimeMillis();
try{
ts.run(this);
}catch(SQLTesterException e){
@ -267,14 +267,14 @@ public class SQLTester {
if( keepGoing ) outln("Continuing anyway becaure of the keep-going option.");
else if( e.isFatal() ) throw e;
}finally{
final long timeEnd = System.nanoTime();
final long timeEnd = System.currentTimeMillis();
outln("🏁",(threw ? "" : "")," ",nTest," test(s) in ",
((timeEnd-timeStart)/1000000.0),"ms.");
(timeEnd-timeStart),"ms.");
//ts.getFilename());
}
}
final long tEnd = System.nanoTime();
outln("Total run-time: ",((tEnd-tStart)/1000000.0),"ms");
final long tEnd = System.currentTimeMillis();
outln("Total run-time: ",(tEnd-tStart),"ms");
Util.unlink(initialDbName);
}