From c62900f325059178a8a1449f87f9d2d78853b467 Mon Sep 17 00:00:00 2001 From: Matt Wonlaw Date: Wed, 8 Apr 2026 12:50:51 -0400 Subject: [PATCH 1/3] expose loadExtension --- lib/database.js | 1 + lib/methods/wrappers.js | 5 +++++ src/objects/database.cpp | 27 ++++++++++++++++++++++++++- src/objects/database.hpp | 1 + 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/database.js b/lib/database.js index fef430bd..f1e269eb 100644 --- a/lib/database.js +++ b/lib/database.js @@ -84,6 +84,7 @@ Database.prototype.exec = wrappers.exec; Database.prototype.close = wrappers.close; Database.prototype.defaultSafeIntegers = wrappers.defaultSafeIntegers; Database.prototype.unsafeMode = wrappers.unsafeMode; +Database.prototype.loadExtension = wrappers.loadExtension; Database.prototype[util.inspect] = require('./methods/inspect'); // Export SQLITE_SCANSTAT_* constants from native addon diff --git a/lib/methods/wrappers.js b/lib/methods/wrappers.js index 0914ed3e..67d80a4f 100644 --- a/lib/methods/wrappers.js +++ b/lib/methods/wrappers.js @@ -25,6 +25,11 @@ exports.unsafeMode = function unsafeMode(...args) { return this; }; +exports.loadExtension = function loadExtension(...args) { + this[cppdb].loadExtension(...args); + return this; +}; + exports.getters = { name: { get: function name() { return this[cppdb].name; }, diff --git a/src/objects/database.cpp b/src/objects/database.cpp index 1fcacc0d..19e615f6 100644 --- a/src/objects/database.cpp +++ b/src/objects/database.cpp @@ -132,6 +132,7 @@ INIT(Database::Init) { SetPrototypeMethod(isolate, data, t, "function", JS_function); SetPrototypeMethod(isolate, data, t, "aggregate", JS_aggregate); SetPrototypeMethod(isolate, data, t, "table", JS_table); + SetPrototypeMethod(isolate, data, t, "loadExtension", JS_loadExtension); SetPrototypeMethod(isolate, data, t, "close", JS_close); SetPrototypeMethod(isolate, data, t, "defaultSafeIntegers", JS_defaultSafeIntegers); SetPrototypeMethod(isolate, data, t, "unsafeMode", JS_unsafeMode); @@ -171,7 +172,9 @@ NODE_METHOD(Database::JS_new) { sqlite3_busy_timeout(db_handle, timeout); sqlite3_limit(db_handle, SQLITE_LIMIT_LENGTH, MAX_BUFFER_SIZE < MAX_STRING_SIZE ? MAX_BUFFER_SIZE : MAX_STRING_SIZE); sqlite3_limit(db_handle, SQLITE_LIMIT_SQL_LENGTH, MAX_STRING_SIZE); - int status = sqlite3_db_config(db_handle, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL); + int status = sqlite3_db_config(db_handle, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL); + assert(status == SQLITE_OK); + status = sqlite3_db_config(db_handle, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL); assert(status == SQLITE_OK); ((void)status); if (node::Buffer::HasInstance(buffer) && !Deserialize(buffer.As(), addon, db_handle, readonly)) { @@ -206,6 +209,28 @@ NODE_METHOD(Database::JS_prepare) { if (!maybeStatement.IsEmpty()) info.GetReturnValue().Set(maybeStatement.ToLocalChecked()); } +NODE_METHOD(Database::JS_loadExtension) { + Database* db = Unwrap(info.This()); + REQUIRE_ARGUMENT_STRING(first, v8::Local filename); + v8::Local entryPoint; + if (info.Length() > 1) { REQUIRE_ARGUMENT_STRING(second, entryPoint); } + REQUIRE_DATABASE_OPEN(db); + REQUIRE_DATABASE_NOT_BUSY(db); + REQUIRE_DATABASE_NO_ITERATORS(db); + UseIsolate; + char* error; + int status = sqlite3_load_extension( + db->db_handle, + *v8::String::Utf8Value(isolate, filename), + entryPoint.IsEmpty() ? NULL : *v8::String::Utf8Value(isolate, entryPoint), + &error + ); + if (status != SQLITE_OK) { + ThrowSqliteError(db->addon, error, status); + } + sqlite3_free(error); +} + NODE_METHOD(Database::JS_exec) { Database* db = Unwrap(info.This()); REQUIRE_ARGUMENT_STRING(first, v8::Local source); diff --git a/src/objects/database.hpp b/src/objects/database.hpp index 87fb2c37..bd8889d2 100644 --- a/src/objects/database.hpp +++ b/src/objects/database.hpp @@ -75,6 +75,7 @@ class Database : public node::ObjectWrap { static NODE_METHOD(JS_function); static NODE_METHOD(JS_aggregate); static NODE_METHOD(JS_table); + static NODE_METHOD(JS_loadExtension); static NODE_METHOD(JS_close); static NODE_METHOD(JS_defaultSafeIntegers); static NODE_METHOD(JS_unsafeMode); From 436f454927a754bfb1aa8e87963991da359bb524 Mon Sep 17 00:00:00 2001 From: Matt Wonlaw Date: Wed, 8 Apr 2026 12:56:50 -0400 Subject: [PATCH 2/3] test loadExtension --- lib/index.d.ts | 2 +- test/15.database.load-extension.js | 53 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 test/15.database.load-extension.js diff --git a/lib/index.d.ts b/lib/index.d.ts index c4949ddd..e57c14c2 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -96,7 +96,7 @@ declare namespace BetterSqlite3 { result?: ((total: T) => unknown) | undefined; }, ): this; - loadExtension(path: string): this; + loadExtension(path: string, entryPoint?: string): this; close(): this; defaultSafeIntegers(toggleState?: boolean): this; backup(destinationFile: string, options?: Database.BackupOptions): Promise; diff --git a/test/15.database.load-extension.js b/test/15.database.load-extension.js new file mode 100644 index 00000000..d5cf6c42 --- /dev/null +++ b/test/15.database.load-extension.js @@ -0,0 +1,53 @@ +'use strict'; +const { execSync } = require('child_process'); +const path = require('path'); +const Database = require('../.'); + +const extensionSrc = path.join(__dirname, '..', 'deps', 'test_extension.c'); +const sqliteInclude = path.join(__dirname, '..', 'deps', 'sqlite3'); +const extensionPath = path.join(__dirname, '..', 'temp', 'test_extension'); + +describe('Database#loadExtension()', function () { + before(function () { + const ext = process.platform === 'win32' ? '.dll' : process.platform === 'darwin' ? '.dylib' : '.so'; + this.extensionFile = extensionPath + ext; + if (process.platform === 'win32') { + execSync(`cl /LD /I"${sqliteInclude}" "${extensionSrc}" /Fe:"${this.extensionFile}"`); + } else { + execSync(`cc -shared -fPIC -I "${sqliteInclude}" -o "${this.extensionFile}" "${extensionSrc}"`); + } + }); + beforeEach(function () { + this.db = new Database(util.next()); + }); + afterEach(function () { + this.db.close(); + }); + + it('should throw an exception if a string is not provided', function () { + expect(() => this.db.loadExtension(123)).to.throw(TypeError); + expect(() => this.db.loadExtension(null)).to.throw(TypeError); + expect(() => this.db.loadExtension()).to.throw(TypeError); + }); + it('should throw an exception if the extension is not found', function () { + expect(() => this.db.loadExtension('/tmp/nonexistent_extension')).to.throw(Database.SqliteError); + }); + it('should load the extension and make its functions available', function () { + const r = this.db.loadExtension(extensionPath); + expect(r).to.equal(this.db); + const result = this.db.prepare('SELECT testExtensionFunction(1, 2, 3) AS val').get(); + expect(result.val).to.equal(3); + }); + it('should not allow loading extensions while the database is busy', function () { + this.db.exec('CREATE TABLE data (x)'); + this.db.exec('INSERT INTO data VALUES (1)'); + const iter = this.db.prepare('SELECT * FROM data').iterate(); + iter.next(); + expect(() => this.db.loadExtension(extensionPath)).to.throw(TypeError); + iter.return(); + }); + it('should not allow loading extensions after the database is closed', function () { + this.db.close(); + expect(() => this.db.loadExtension(extensionPath)).to.throw(TypeError); + }); +}); From 9893278a46a57e3c250ef1d983122772199ea1f0 Mon Sep 17 00:00:00 2001 From: Matt Wonlaw Date: Wed, 8 Apr 2026 13:48:47 -0400 Subject: [PATCH 3/3] skip test on windows --- test/15.database.load-extension.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test/15.database.load-extension.js b/test/15.database.load-extension.js index d5cf6c42..6ee725a7 100644 --- a/test/15.database.load-extension.js +++ b/test/15.database.load-extension.js @@ -3,19 +3,16 @@ const { execSync } = require('child_process'); const path = require('path'); const Database = require('../.'); +const isWindows = process.platform === 'win32'; const extensionSrc = path.join(__dirname, '..', 'deps', 'test_extension.c'); const sqliteInclude = path.join(__dirname, '..', 'deps', 'sqlite3'); const extensionPath = path.join(__dirname, '..', 'temp', 'test_extension'); -describe('Database#loadExtension()', function () { +(isWindows ? describe.skip : describe)('Database#loadExtension()', function () { before(function () { - const ext = process.platform === 'win32' ? '.dll' : process.platform === 'darwin' ? '.dylib' : '.so'; + const ext = process.platform === 'darwin' ? '.dylib' : '.so'; this.extensionFile = extensionPath + ext; - if (process.platform === 'win32') { - execSync(`cl /LD /I"${sqliteInclude}" "${extensionSrc}" /Fe:"${this.extensionFile}"`); - } else { - execSync(`cc -shared -fPIC -I "${sqliteInclude}" -o "${this.extensionFile}" "${extensionSrc}"`); - } + execSync(`cc -shared -fPIC -I "${sqliteInclude}" -o "${this.extensionFile}" "${extensionSrc}"`); }); beforeEach(function () { this.db = new Database(util.next());