Skip to content

Load SQLite extensions via SQLite C-API #319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,14 @@ jni-header: $(TARGET)/common-lib/NativeDB.h
$(TARGET)/common-lib/NativeDB.h: $(TARGET)/common-lib/org/sqlite/core/NativeDB.class
$(JAVAH) -classpath $(TARGET)/common-lib -jni -o $@ org.sqlite.core.NativeDB

test:
test: test-natives
mvn test

test-natives:
mkdir -p target/test-classes/
$(CC) -I$(SQLITE_SOURCE) -fPIC -shared -o target/test-classes/libtest.so src/test/c/test.c
$(CC) -I$(SQLITE_SOURCE) -fPIC -shared -o target/test-classes/libtest2.so src/test/c/test2.c

clean: clean-native clean-java clean-tests


Expand Down Expand Up @@ -110,44 +115,44 @@ $(NATIVE_DLL): $(SQLITE_OUT)/$(LIBNAME)

DOCKER_RUN_OPTS=--rm

win32: $(SQLITE_UNPACKED) jni-header
win32: $(SQLITE_UNPACKED) jni-header test-natives
./docker/dockcross-windows-x86 -a $(DOCKER_RUN_OPTS) bash -c 'make clean-native native CROSS_PREFIX=i686-w64-mingw32.static- OS_NAME=Windows OS_ARCH=x86'

win64: $(SQLITE_UNPACKED) jni-header
win64: $(SQLITE_UNPACKED) jni-header test-natives
./docker/dockcross-windows-x64 -a $(DOCKER_RUN_OPTS) bash -c 'make clean-native native CROSS_PREFIX=x86_64-w64-mingw32.static- OS_NAME=Windows OS_ARCH=x86_64'

linux32: $(SQLITE_UNPACKED) jni-header
linux32: $(SQLITE_UNPACKED) jni-header test-natives
docker run $(DOCKER_RUN_OPTS) -ti -v $$PWD:/work xerial/centos5-linux-x86 bash -c 'make clean-native native OS_NAME=Linux OS_ARCH=x86'

linux64: $(SQLITE_UNPACKED) jni-header
linux64: $(SQLITE_UNPACKED) jni-header test-natives
docker run $(DOCKER_RUN_OPTS) -ti -v $$PWD:/work xerial/centos5-linux-x86_64 bash -c 'make clean-native native OS_NAME=Linux OS_ARCH=x86_64'

alpine-linux64: $(SQLITE_UNPACKED) jni-header
alpine-linux64: $(SQLITE_UNPACKED) jni-header test-natives
docker run $(DOCKER_RUN_OPTS) -ti -v $$PWD:/work xerial/alpine-linux-x86_64 bash -c 'make clean-native native OS_NAME=Linux OS_ARCH=x86_64'

linux-arm: $(SQLITE_UNPACKED) jni-header
linux-arm: $(SQLITE_UNPACKED) jni-header test-natives
./docker/dockcross-armv5 -a $(DOCKER_RUN_OPTS) bash -c 'make clean-native native CROSS_PREFIX=arm-linux-gnueabi- OS_NAME=Linux OS_ARCH=arm'

linux-armv6: $(SQLITE_UNPACKED) jni-header
linux-armv6: $(SQLITE_UNPACKED) jni-header test-natives
./docker/dockcross-armv6 -a $(DOCKER_RUN_OPTS) bash -c 'make clean-native native CROSS_PREFIX=arm-linux-gnueabihf- OS_NAME=Linux OS_ARCH=armv6'

linux-armv7: $(SQLITE_UNPACKED) jni-header
linux-armv7: $(SQLITE_UNPACKED) jni-header test-natives
./docker/dockcross-armv7 -a $(DOCKER_RUN_OPTS) bash -c 'make clean-native native CROSS_PREFIX=arm-linux-gnueabihf- OS_NAME=Linux OS_ARCH=armv7'

linux-arm64: $(SQLITE_UNPACKED) jni-header
linux-arm64: $(SQLITE_UNPACKED) jni-header test-natives
./docker/dockcross-arm64 -a $(DOCKER_RUN_OPTS) bash -c 'make clean-native native CROSS_PREFIX=/usr/bin/aarch64-unknown-linux-gnueabi/bin/aarch64-unknown-linux-gnueabi- OS_NAME=Linux OS_ARCH=aarch64'

linux-android-arm: $(SQLITE_UNPACKED) jni-header
linux-android-arm: $(SQLITE_UNPACKED) jni-header test-natives
./docker/dockcross-android-arm -a $(DOCKER_RUN_OPTS) bash -c 'make clean-native native CROSS_PREFIX=/usr/arm-linux-androideabi/bin/arm-linux-androideabi- OS_NAME=Linux OS_ARCH=android-arm'

linux-ppc64: $(SQLITE_UNPACKED) jni-header
linux-ppc64: $(SQLITE_UNPACKED) jni-header test-natives
./docker/dockcross-ppc64 -a $(DOCKER_RUN_OPTS) bash -c 'make clean-native native CROSS_PREFIX=powerpc64le-linux-gnu- OS_NAME=Linux OS_ARCH=ppc64'

mac64: $(SQLITE_UNPACKED) jni-header
mac64: $(SQLITE_UNPACKED) jni-header test-natives
docker run -it $(DOCKER_RUN_OPTS) -v $$PWD:/workdir -e CROSS_TRIPLE=x86_64-apple-darwin multiarch/crossbuild make clean-native native OS_NAME=Mac OS_ARCH=x86_64

# deprecated
mac32: $(SQLITE_UNPACKED) jni-header
mac32: $(SQLITE_UNPACKED) jni-header test-natives
docker run -it $(DOCKER_RUN_OPTS) -v $$PWD:/workdir -e CROSS_TRIPLE=i386-apple-darwin multiarch/crossbuild make clean-native native OS_NAME=Mac OS_ARCH=x86

sparcv9:
Expand All @@ -166,6 +171,7 @@ clean-java:

clean-tests:
rm -rf $(TARGET)/{surefire*,testdb.jar*}
rm -rf target/test-classes

docker-linux64:
docker build -f docker/Dockerfile.linux_x86_64 -t xerial/centos5-linux-x86_64 .
Expand Down
178 changes: 178 additions & 0 deletions src/main/java/org/sqlite/ExtensionInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package org.sqlite;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import org.sqlite.util.StringUtils;

/**
* File and entry point needed to load a SQLite extension.
*
* Also provides static methods to serialize and deserialize a
* collection of <code>ExtensionInfo</code>s into a <code>String</code>.
*
* @see <a href="https://sqlite.org/loadext.html#loading_an_extension">https://sqlite.org/loadext.html#loading_an_extension</a>
* @see <a href="https://sqlite.org/lang_corefunc.html#load_extension">https://sqlite.org/lang_corefunc.html#load_extension</a>
* @see <a href="https://sqlite.org/c3ref/load_extension.html">https://sqlite.org/c3ref/load_extension.html</a>
* @author Andy-2639
*/
public class ExtensionInfo {

private static final char SERIALIZE_ESCAPE_CHAR = '|';
private static final char SERIALIZE_EXTENSION_INFO_SEPARATOR = '*';
private static final char SERIALIZE_FILE_ENTRY_SEPARATOR = '"';

/** Must not be <code>null</code>. */
private final String file;
/** May be <code>null</null>. */
private final String entry;

/*
* <serialized> ::= ( <file_entry> SERIALIZE_EXTENSION_INFO_SEPARATOR )*
* <file_entry> ::= <file> [ SERIALIZE_FILE_ENTRY_SEPARATOR <entry> ]
*/
/**
* @param extensionInfos must not be <code>null</code>.
* @return Serialized {@link ExtensionInfo}s. Never <code>null</code>.
*/
static String serialize(Collection<ExtensionInfo> extensionInfos) {
final char[] serializeSpecialChars = new char[] {
SERIALIZE_EXTENSION_INFO_SEPARATOR,
SERIALIZE_FILE_ENTRY_SEPARATOR
};
StringBuilder builder = new StringBuilder();
for (ExtensionInfo ei : extensionInfos) {
builder.append(StringUtils.escape(ei.getFile(), SERIALIZE_ESCAPE_CHAR, serializeSpecialChars));
if (ei.getEntry() != null) {
builder.append(SERIALIZE_FILE_ENTRY_SEPARATOR);
builder.append(StringUtils.escape(ei.getEntry(), SERIALIZE_ESCAPE_CHAR, serializeSpecialChars));
}
builder.append(SERIALIZE_EXTENSION_INFO_SEPARATOR);
}
return builder.toString();
}

/**
* @param extensionInfosSerialized must not be null.
* @return Deserialized {@link EntensionInfo}s. Never <code>null</code>.
*/
static Set<ExtensionInfo> deserialize(String extensionInfosSerialized) {
Set<ExtensionInfo> eis = new HashSet<ExtensionInfo>();
StringBuilder file = new StringBuilder();
StringBuilder entry = new StringBuilder();
boolean parseFile = true;
boolean esc = false;
for (int i = 0; i < extensionInfosSerialized.length(); i++) {
char ch = extensionInfosSerialized.charAt(i);
if (esc) {
if (parseFile) {
file.append(ch);
} else {
entry.append(ch);
}
esc = false;
} else if (parseFile) {
assert (entry.length() == 0);
switch (ch) {
case SERIALIZE_ESCAPE_CHAR:
esc = true;
break;
case SERIALIZE_EXTENSION_INFO_SEPARATOR:
eis.add(new ExtensionInfo(file.toString(), null));
file.setLength(0);
break;
case SERIALIZE_FILE_ENTRY_SEPARATOR:
parseFile = false;
break;
default:
file.append(ch);
break;
}
} else {
switch (ch) {
case SERIALIZE_ESCAPE_CHAR:
esc = true;
break;
case SERIALIZE_EXTENSION_INFO_SEPARATOR:
eis.add(new ExtensionInfo(file.toString(), entry.toString()));
file.setLength(0);
entry.setLength(0);
parseFile = true;
break;
case SERIALIZE_FILE_ENTRY_SEPARATOR:
throw new IllegalStateException("Unexpected SERIALIZE_FILE_ENTRY_SEPARATOR: " + extensionInfosSerialized);
default:
entry.append(ch);
break;
}
}
}
// assert last char was unescaped SERIALIZE_EXTENSION_INFO_SEPARATOR
assert (!esc && parseFile && (file.length() == 0));
//System.err.println(extensionInfosSerialized);
//for (ExtensionInfo ei : eis) {
// System.err.println("file: " + ei.getFile() + " - entry: " + ei.getEntry());
//}
return eis;
}

/**
* @param file native library containing the SQLite extension. Must not be null.
* @param entry if <code>null</code>, SQLite determines the entry point.
*
* @see <a href="https://sqlite.org/lang_corefunc.html#load_extension">https://sqlite.org/lang_corefunc.html#load_extension</a>
* @see <a href="https://sqlite.org/c3ref/load_extension.html">https://sqlite.org/c3ref/load_extension.html</a>
*/
ExtensionInfo(String file, String entry) {
if (file == null) {
throw new NullPointerException("file must not be null");
}
this.file = file;
this.entry = entry;
}

/**
* @return file native library containing the SQLite extension. Never <code>null</code>.
*/
public String getFile() {
return file;
}

/**
* Entry point of the extension.
*
* @return may be <code>null</code>.
*/
public String getEntry() {
return entry;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + file.hashCode();
result = prime * result + ((entry == null) ? 0 : entry.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if ((obj == null) || (getClass() != obj.getClass())) {
return false;
}
ExtensionInfo other = (ExtensionInfo)obj;
if (!file.equals(other.file)) {
return false;
}
if ((entry == null) != (other.entry == null)) {
return false;
}
if ((entry != null) && !(entry.equals(other.entry))) {
return false;
}
return true;
}

}
34 changes: 33 additions & 1 deletion src/main/java/org/sqlite/SQLiteConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ public class SQLiteConfig

private final int busyTimeout;

/** @author Andy-2639 */
private final Set<ExtensionInfo> loadExtensions = new HashSet<ExtensionInfo>();

private final SQLiteConnectionConfig defaultConnectionConfig;

/**
Expand Down Expand Up @@ -87,13 +90,30 @@ public SQLiteConfig(Properties prop) {

this.busyTimeout = Integer.parseInt(pragmaTable.getProperty(Pragma.BUSY_TIMEOUT.pragmaName, "3000"));
this.defaultConnectionConfig = SQLiteConnectionConfig.fromPragmaTable(pragmaTable);

String loadExtsString = pragmaTable.getProperty(Pragma.LOAD_EXTENSIONS.pragmaName);
if (loadExtsString != null) {
loadExtensions.addAll(ExtensionInfo.deserialize(loadExtsString));
}
}

public SQLiteConnectionConfig newConnectionConfig()
{
return defaultConnectionConfig.copyConfig();
}

public void loadExtension(String file, String entry) {
loadExtensions.add(new ExtensionInfo(file, entry));
}

public void loadExtension(String file) {
this.loadExtension(file, null);
}

public Set<ExtensionInfo> getLoadExtensions() {
return loadExtensions;
}

/**
* Create a new JDBC connection using the current configuration
* @return The connection.
Expand Down Expand Up @@ -123,6 +143,7 @@ public void apply(Connection conn) throws SQLException {
pragmaParams.remove(Pragma.DATE_STRING_FORMAT.pragmaName);
pragmaParams.remove(Pragma.PASSWORD.pragmaName);
pragmaParams.remove(Pragma.HEXKEY_MODE.pragmaName);
pragmaParams.remove(Pragma.LOAD_EXTENSIONS.pragmaName);

Statement stat = conn.createStatement();
try {
Expand Down Expand Up @@ -235,6 +256,14 @@ public Properties toProperties() {
pragmaTable.setProperty(Pragma.DATE_PRECISION.pragmaName, defaultConnectionConfig.getDatePrecision().getValue());
pragmaTable.setProperty(Pragma.DATE_STRING_FORMAT.pragmaName, defaultConnectionConfig.getDateStringFormat());

if (loadExtensions.isEmpty()) {
pragmaTable.remove(Pragma.LOAD_EXTENSIONS.pragmaName);
} else {
pragmaTable.setProperty(
Pragma.LOAD_EXTENSIONS.pragmaName,
ExtensionInfo.serialize(loadExtensions));
}

return pragmaTable;
}

Expand Down Expand Up @@ -309,7 +338,10 @@ public static enum Pragma {
DATE_STRING_FORMAT("date_string_format", "Format to store and retrieve dates stored as text. Defaults to \"yyyy-MM-dd HH:mm:ss.SSS\"", null),
BUSY_TIMEOUT("busy_timeout", null),
HEXKEY_MODE("hexkey_mode", toStringArray(HexKeyMode.values())),
PASSWORD("password", null);
PASSWORD("password", null),

LOAD_EXTENSIONS("load_extensions"),
;

public final String pragmaName;
public final String[] choices;
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/org/sqlite/SQLiteConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Executor;

/**
Expand Down Expand Up @@ -241,6 +242,17 @@ private static DB open(String url, String origFileName, Properties props) throws
throw err;
}
db.open(fileName, config.getOpenModeFlags());

Set<ExtensionInfo> loadExtensions = config.getLoadExtensions();
if (!loadExtensions.isEmpty()) {
db.dbconfig_enable_load_extension(true);
for (ExtensionInfo ei : loadExtensions) {
db.load_extension(ei.getFile(), ei.getEntry());
}
db.dbconfig_enable_load_extension(false);
}
db.enable_load_extension(config.isEnabledLoadExtension());

return db;
}

Expand Down
Loading