Skip to content

Commit d4b19c1

Browse files
moxie0moxie-signal
andauthored
Add support for update/commit/rollback event notifications (#350)
Using sqlite3_commit_hook, sqlite3_rollback_hook, and sqlite3_update_hook Co-authored-by: Moxie Marlinspike <[email protected]>
1 parent d28a8aa commit d4b19c1

File tree

7 files changed

+412
-1
lines changed

7 files changed

+412
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.sqlite;
2+
3+
/**
4+
* https://www.sqlite.org/c3ref/commit_hook.html
5+
*/
6+
7+
public interface SQLiteCommitListener {
8+
9+
void onCommit();
10+
void onRollback();
11+
12+
}

src/main/java/org/sqlite/SQLiteConnection.java

+36
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,42 @@ public void rollback() throws SQLException {
417417
db.exec(connectionConfig.transactionPrefix(), getAutoCommit());
418418
}
419419

420+
/**
421+
* Add a listener for DB update events, see https://www.sqlite.org/c3ref/update_hook.html
422+
*
423+
* @param listener The listener to receive update events
424+
*/
425+
public void addUpdateListener(SQLiteUpdateListener listener) {
426+
db.addUpdateListener(listener);
427+
}
428+
429+
/**
430+
* Remove a listener registered for DB update events.
431+
*
432+
* @param listener The listener to no longer receive update events
433+
*/
434+
public void removeUpdateListener(SQLiteUpdateListener listener) {
435+
db.removeUpdateListener(listener);
436+
}
437+
438+
439+
/**
440+
* Add a listener for DB commit/rollback events, see https://www.sqlite.org/c3ref/commit_hook.html
441+
*
442+
* @param listener The listener to receive commit events
443+
*/
444+
public void addCommitListener(SQLiteCommitListener listener) {
445+
db.addCommitListener(listener);
446+
}
447+
448+
/**
449+
* Remove a listener registered for DB commit/rollback events.
450+
*
451+
* @param listener The listener to no longer receive commit/rollback events.
452+
*/
453+
public void removeCommitListener(SQLiteCommitListener listener) {
454+
db.removeCommitListener(listener);
455+
}
420456

421457
/**
422458
* Extracts PRAGMA values from the filename and sets them into the Properties
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.sqlite;
2+
3+
/**
4+
* https://www.sqlite.org/c3ref/update_hook.html
5+
*/
6+
public interface SQLiteUpdateListener {
7+
8+
public enum Type {
9+
INSERT, DELETE, UPDATE
10+
}
11+
12+
void onUpdate(Type type, String database, String table, long rowId);
13+
14+
}

src/main/java/org/sqlite/core/DB.java

+69
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@
1818
import org.sqlite.BusyHandler;
1919
import org.sqlite.Function;
2020
import org.sqlite.ProgressHandler;
21+
import org.sqlite.SQLiteCommitListener;
2122
import org.sqlite.SQLiteConfig;
2223
import org.sqlite.SQLiteErrorCode;
2324
import org.sqlite.SQLiteException;
25+
import org.sqlite.SQLiteUpdateListener;
2426

2527
import java.sql.BatchUpdateException;
2628
import java.sql.SQLException;
2729
import java.util.HashMap;
30+
import java.util.HashSet;
2831
import java.util.Iterator;
2932
import java.util.Map;
33+
import java.util.Set;
3034
import java.util.concurrent.atomic.AtomicBoolean;
3135

3236
/*
@@ -55,6 +59,9 @@ public abstract class DB implements Codes
5559
/** Tracer for statements to avoid unfinalized statements on db close. */
5660
private final Map<Long, CoreStatement> stmts = new HashMap<Long, CoreStatement>();
5761

62+
private final Set<SQLiteUpdateListener> updateListeners = new HashSet<SQLiteUpdateListener>();
63+
private final Set<SQLiteCommitListener> commitListeners = new HashSet<SQLiteCommitListener>();
64+
5865
public DB(String url, String fileName, SQLiteConfig config)
5966
throws SQLException
6067
{
@@ -901,6 +908,68 @@ public final synchronized int executeUpdate(CoreStatement stmt, Object[] vals) t
901908
return changes();
902909
}
903910

911+
abstract void set_commit_listener(boolean enabled);
912+
abstract void set_update_listener(boolean enabled);
913+
914+
public synchronized void addUpdateListener(SQLiteUpdateListener listener) {
915+
if (updateListeners.add(listener) && updateListeners.size() == 1) {
916+
set_update_listener(true);
917+
}
918+
}
919+
920+
public synchronized void addCommitListener(SQLiteCommitListener listener) {
921+
if (commitListeners.add(listener) && commitListeners.size() == 1) {
922+
set_commit_listener(true);
923+
}
924+
}
925+
926+
public synchronized void removeUpdateListener(SQLiteUpdateListener listener) {
927+
if (updateListeners.remove(listener) && updateListeners.isEmpty()) {
928+
set_update_listener(false);
929+
}
930+
}
931+
932+
public synchronized void removeCommitListener(SQLiteCommitListener listener) {
933+
if (commitListeners.remove(listener) && commitListeners.isEmpty()) {
934+
set_commit_listener(false);
935+
}
936+
}
937+
938+
void onUpdate(int type, String database, String table, long rowId) {
939+
Set<SQLiteUpdateListener> listeners;
940+
941+
synchronized (this) {
942+
listeners = new HashSet<SQLiteUpdateListener>(updateListeners);
943+
}
944+
945+
for (SQLiteUpdateListener listener : listeners) {
946+
SQLiteUpdateListener.Type operationType;
947+
948+
switch (type) {
949+
case 18: operationType = SQLiteUpdateListener.Type.INSERT; break;
950+
case 9: operationType = SQLiteUpdateListener.Type.DELETE; break;
951+
case 23: operationType = SQLiteUpdateListener.Type.UPDATE; break;
952+
default: throw new AssertionError("Unknown type: " + type);
953+
}
954+
955+
956+
listener.onUpdate(operationType, database, table, rowId);
957+
}
958+
}
959+
960+
void onCommit(boolean commit) {
961+
Set<SQLiteCommitListener> listeners;
962+
963+
synchronized (this) {
964+
listeners = new HashSet<SQLiteCommitListener>(commitListeners);
965+
}
966+
967+
for (SQLiteCommitListener listener : listeners) {
968+
if (commit) listener.onCommit();
969+
else listener.onRollback();
970+
}
971+
}
972+
904973
/**
905974
* Throws SQLException with error message.
906975
* @throws SQLException

src/main/java/org/sqlite/core/NativeDB.c

+73-1
Original file line numberDiff line numberDiff line change
@@ -1328,7 +1328,6 @@ JNIEXPORT void JNICALL Java_org_sqlite_core_NativeDB_free_1functions(
13281328
}
13291329
}
13301330

1331-
13321331
// COMPOUND FUNCTIONS ///////////////////////////////////////////////
13331332

13341333
JNIEXPORT jobjectArray JNICALL Java_org_sqlite_core_NativeDB_column_1metadata(
@@ -1607,3 +1606,76 @@ JNIEXPORT void JNICALL Java_org_sqlite_core_NativeDB_clear_1progress_1handler(
16071606
sqlite3_progress_handler(gethandle(env, this), 0, NULL, NULL);
16081607
(*env)->DeleteGlobalRef(env, progress_handler_context.phandler);
16091608
}
1609+
1610+
// Update hook
1611+
1612+
struct UpdateHandlerContext {
1613+
JavaVM *vm;
1614+
jmethodID method;
1615+
jobject handler;
1616+
};
1617+
1618+
static struct UpdateHandlerContext update_handler_context;
1619+
1620+
1621+
void update_hook(void *context, int type, char const *database, char const *table, sqlite3_int64 row) {
1622+
JNIEnv *env = 0;
1623+
(*update_handler_context.vm)->AttachCurrentThread(update_handler_context.vm, (void **)&env, 0);
1624+
1625+
jstring databaseString = (*env)->NewStringUTF(env, database);
1626+
jstring tableString = (*env)->NewStringUTF(env, table);
1627+
1628+
(*env)->CallVoidMethod(env, update_handler_context.handler, update_handler_context.method, type, databaseString, tableString, row);
1629+
1630+
(*env)->DeleteLocalRef(env, databaseString);
1631+
(*env)->DeleteLocalRef(env, tableString);
1632+
}
1633+
1634+
JNIEXPORT void JNICALL Java_org_sqlite_core_NativeDB_set_1update_1listener(JNIEnv *env, jobject this, jboolean enabled) {
1635+
if (enabled) {
1636+
update_handler_context.method = (*env)->GetMethodID(env, dbclass, "onUpdate", "(ILjava/lang/String;Ljava/lang/String;J)V");
1637+
update_handler_context.handler = (*env)->NewGlobalRef(env, this);
1638+
(*env)->GetJavaVM(env, &update_handler_context.vm);
1639+
sqlite3_update_hook(gethandle(env, this), &update_hook, NULL);
1640+
} else {
1641+
sqlite3_update_hook(gethandle(env, this), NULL, NULL);
1642+
(*env)->DeleteGlobalRef(env, update_handler_context.handler);
1643+
}
1644+
}
1645+
1646+
// Commit hook
1647+
1648+
struct CommitHandlerContext {
1649+
JavaVM *vm;
1650+
jmethodID method;
1651+
jobject handler;
1652+
};
1653+
1654+
static struct CommitHandlerContext commit_handler_context;
1655+
1656+
int commit_hook(void *context) {
1657+
JNIEnv *env = 0;
1658+
(*commit_handler_context.vm)->AttachCurrentThread(commit_handler_context.vm, (void **)&env, 0);
1659+
(*env)->CallVoidMethod(env, commit_handler_context.handler, commit_handler_context.method, 1);
1660+
return 0;
1661+
}
1662+
1663+
void rollback_hook(void *context) {
1664+
JNIEnv *env = 0;
1665+
(*commit_handler_context.vm)->AttachCurrentThread(commit_handler_context.vm, (void **)&env, 0);
1666+
(*env)->CallVoidMethod(env, commit_handler_context.handler, commit_handler_context.method, 0);
1667+
}
1668+
1669+
JNIEXPORT void JNICALL Java_org_sqlite_core_NativeDB_set_1commit_1listener(JNIEnv *env, jobject this, jboolean enabled) {
1670+
if (enabled) {
1671+
commit_handler_context.method = (*env)->GetMethodID(env, dbclass, "onCommit", "(Z)V");
1672+
commit_handler_context.handler = (*env)->NewGlobalRef(env, this);
1673+
(*env)->GetJavaVM(env, &commit_handler_context.vm);
1674+
sqlite3_commit_hook(gethandle(env, this), &commit_hook, NULL);
1675+
sqlite3_rollback_hook(gethandle(env, this), &rollback_hook, NULL);
1676+
} else {
1677+
sqlite3_commit_hook(gethandle(env, this), NULL, NULL);
1678+
sqlite3_update_hook(gethandle(env, this), NULL, NULL);
1679+
(*env)->DeleteGlobalRef(env, commit_handler_context.handler);
1680+
}
1681+
}

src/main/java/org/sqlite/core/NativeDB.java

+6
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,12 @@ native synchronized int restore(byte[] dbNameUtf8, byte[] sourceFileName,
469469
@Override
470470
native synchronized boolean[][] column_metadata(long stmt);
471471

472+
@Override
473+
native synchronized void set_commit_listener(boolean enabled);
474+
475+
@Override
476+
native synchronized void set_update_listener(boolean enabled);
477+
472478
/**
473479
* Throws an SQLException
474480
* @param msg Message for the SQLException.

0 commit comments

Comments
 (0)