diff --git a/deps/defines.gypi b/deps/defines.gypi index 58bbe36a..59765f44 100644 --- a/deps/defines.gypi +++ b/deps/defines.gypi @@ -27,6 +27,7 @@ 'SQLITE_ENABLE_MATH_FUNCTIONS', 'SQLITE_ENABLE_RTREE', 'SQLITE_ENABLE_STAT4', + 'SQLITE_ENABLE_STMT_SCANSTATUS', 'SQLITE_ENABLE_UPDATE_DELETE_LIMIT', 'SQLITE_LIKE_DOESNT_MATCH_BLOBS', 'SQLITE_OMIT_DEPRECATED', diff --git a/lib/database.js b/lib/database.js index 4bf6c56d..8b3ea9dc 100644 --- a/lib/database.js +++ b/lib/database.js @@ -86,4 +86,16 @@ Database.prototype.defaultSafeIntegers = wrappers.defaultSafeIntegers; Database.prototype.unsafeMode = wrappers.unsafeMode; Database.prototype[util.inspect] = require('./methods/inspect'); +// Export SQLITE_SCANSTAT_* constants from native addon +const nativeAddon = DEFAULT_ADDON || require('bindings')('better_sqlite3.node'); +Database.SQLITE_SCANSTAT_NLOOP = nativeAddon.SQLITE_SCANSTAT_NLOOP; +Database.SQLITE_SCANSTAT_NVISIT = nativeAddon.SQLITE_SCANSTAT_NVISIT; +Database.SQLITE_SCANSTAT_EST = nativeAddon.SQLITE_SCANSTAT_EST; +Database.SQLITE_SCANSTAT_NAME = nativeAddon.SQLITE_SCANSTAT_NAME; +Database.SQLITE_SCANSTAT_EXPLAIN = nativeAddon.SQLITE_SCANSTAT_EXPLAIN; +Database.SQLITE_SCANSTAT_SELECTID = nativeAddon.SQLITE_SCANSTAT_SELECTID; +Database.SQLITE_SCANSTAT_PARENTID = nativeAddon.SQLITE_SCANSTAT_PARENTID; +Database.SQLITE_SCANSTAT_NCYCLE = nativeAddon.SQLITE_SCANSTAT_NCYCLE; +Database.SQLITE_SCANSTAT_COMPLEX = nativeAddon.SQLITE_SCANSTAT_COMPLEX; + module.exports = Database; diff --git a/lib/index.d.ts b/lib/index.d.ts index dc6eff86..50ea011f 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -5,6 +5,17 @@ type VariableArgFunction = (...params: any[]) => unknown; type ArgumentTypes = F extends (...args: infer A) => unknown ? A : never; type ElementOf = T extends Array ? E : T; +declare const enum ScanStatOpcode { + SQLITE_SCANSTAT_NLOOP = 0, + SQLITE_SCANSTAT_NVISIT = 1, + SQLITE_SCANSTAT_EST = 2, + SQLITE_SCANSTAT_NAME = 3, + SQLITE_SCANSTAT_EXPLAIN = 4, + SQLITE_SCANSTAT_SELECTID = 5, + SQLITE_SCANSTAT_PARENTID = 6, + SQLITE_SCANSTAT_NCYCLE = 7, +} + declare namespace BetterSqlite3 { interface Statement { database: Database; @@ -23,6 +34,15 @@ declare namespace BetterSqlite3 { bind(...params: BindParameters): this; columns(): ColumnDefinition[]; safeIntegers(toggleState?: boolean): this; + scanStatusV2(idx: number, opcode: ScanStatOpcode.SQLITE_SCANSTAT_NAME, resetFlag: number): string | undefined; + scanStatusV2(idx: number, opcode: ScanStatOpcode.SQLITE_SCANSTAT_EXPLAIN, resetFlag: number): string | undefined; + scanStatusV2(idx: number, opcode: ScanStatOpcode.SQLITE_SCANSTAT_NLOOP, resetFlag: number): number | undefined; + scanStatusV2(idx: number, opcode: ScanStatOpcode.SQLITE_SCANSTAT_NVISIT, resetFlag: number): number | undefined; + scanStatusV2(idx: number, opcode: ScanStatOpcode.SQLITE_SCANSTAT_EST, resetFlag: number): number | undefined; + scanStatusV2(idx: number, opcode: ScanStatOpcode.SQLITE_SCANSTAT_SELECTID, resetFlag: number): number | undefined; + scanStatusV2(idx: number, opcode: ScanStatOpcode.SQLITE_SCANSTAT_PARENTID, resetFlag: number): number | undefined; + scanStatusV2(idx: number, opcode: ScanStatOpcode.SQLITE_SCANSTAT_NCYCLE, resetFlag: number): number | undefined; + scanStatusV2(idx: number, opcode: ScanStatOpcode, resetFlag: number): number | string | undefined; } interface ColumnDefinition { @@ -89,6 +109,16 @@ declare namespace BetterSqlite3 { prototype: Database; SqliteError: typeof SqliteError; + + // scanstatus constants + SQLITE_SCANSTAT_NLOOP: ScanStatOpcode.SQLITE_SCANSTAT_NLOOP; + SQLITE_SCANSTAT_NVISIT: ScanStatOpcode.SQLITE_SCANSTAT_NVISIT; + SQLITE_SCANSTAT_EST: ScanStatOpcode.SQLITE_SCANSTAT_EST; + SQLITE_SCANSTAT_NAME: ScanStatOpcode.SQLITE_SCANSTAT_NAME; + SQLITE_SCANSTAT_EXPLAIN: ScanStatOpcode.SQLITE_SCANSTAT_EXPLAIN; + SQLITE_SCANSTAT_SELECTID: ScanStatOpcode.SQLITE_SCANSTAT_SELECTID; + SQLITE_SCANSTAT_PARENTID: ScanStatOpcode.SQLITE_SCANSTAT_PARENTID; + SQLITE_SCANSTAT_NCYCLE: ScanStatOpcode.SQLITE_SCANSTAT_NCYCLE; } } diff --git a/package.json b/package.json index dc88780b..d462e9ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rocicorp/zero-sqlite3", - "version": "1.0.8", + "version": "1.0.9", "description": "better-sqlite3 on bedrock", "homepage": "https://github.com/rocicorp/zero-sqlite3", "author": "Rocicorp", diff --git a/src/better_sqlite3.cpp b/src/better_sqlite3.cpp index 6e596b59..2e9c097d 100644 --- a/src/better_sqlite3.cpp +++ b/src/better_sqlite3.cpp @@ -73,6 +73,17 @@ NODE_MODULE_INIT(/* exports, context */) { exports->Set(context, InternalizedFromLatin1(isolate, "Backup"), Backup::Init(isolate, data)).FromJust(); exports->Set(context, InternalizedFromLatin1(isolate, "setErrorConstructor"), v8::FunctionTemplate::New(isolate, Addon::JS_setErrorConstructor, data)->GetFunction(context).ToLocalChecked()).FromJust(); + // Export SQLITE_SCANSTAT_* constants + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_NLOOP"), v8::Int32::New(isolate, SQLITE_SCANSTAT_NLOOP)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_NVISIT"), v8::Int32::New(isolate, SQLITE_SCANSTAT_NVISIT)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_EST"), v8::Int32::New(isolate, SQLITE_SCANSTAT_EST)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_NAME"), v8::Int32::New(isolate, SQLITE_SCANSTAT_NAME)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_EXPLAIN"), v8::Int32::New(isolate, SQLITE_SCANSTAT_EXPLAIN)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_SELECTID"), v8::Int32::New(isolate, SQLITE_SCANSTAT_SELECTID)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_PARENTID"), v8::Int32::New(isolate, SQLITE_SCANSTAT_PARENTID)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_NCYCLE"), v8::Int32::New(isolate, SQLITE_SCANSTAT_NCYCLE)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_COMPLEX"), v8::Int32::New(isolate, SQLITE_SCANSTAT_COMPLEX)).FromJust(); + // Store addon instance data. addon->Statement.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "Statement")).ToLocalChecked().As()); addon->StatementIterator.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "StatementIterator")).ToLocalChecked().As()); @@ -773,6 +784,7 @@ v8::Local Statement::Init (v8::Isolate * isolate, v8::Local GetFunction( isolate -> GetCurrentContext ( ) ).ToLocalChecked(); } @@ -1083,9 +1095,59 @@ void Statement::JS_columns (v8::FunctionCallbackInfo const & info) info.GetReturnValue().Set(columns); } -#line 321 "./src/objects/statement.lzz" +#line 322 "./src/objects/statement.lzz" +void Statement::JS_scanStatusV2 (v8::FunctionCallbackInfo const & info) +#line 322 "./src/objects/statement.lzz" + { + Statement* stmt = node :: ObjectWrap :: Unwrap (info.This()); + if (!stmt->db->GetState()->open) return ThrowTypeError("The database connection is not open"); + if (!stmt->alive) return ThrowTypeError("The statement has been finalized and can no longer be used"); + + int idx; if (info.Length() < (0 + 1) || !info[0]->IsInt32()) return ThrowTypeError("Expected " "first" " argument to be " "a 32-bit signed integer"); idx = (info[0].As()->Value());; + int iScanStatusOp; if (info.Length() < (1 + 1) || !info[1]->IsInt32()) return ThrowTypeError("Expected " "second" " argument to be " "a 32-bit signed integer"); iScanStatusOp = (info[1].As()->Value());; + int flags; if (info.Length() < (2 + 1) || !info[2]->IsInt32()) return ThrowTypeError("Expected " "third" " argument to be " "a 32-bit signed integer"); flags = (info[2].As()->Value());; + + v8::Isolate* isolate = info.GetIsolate();; + + // Based on iScanStatusOp, we know what type of output to expect + int rc; + if (iScanStatusOp == 0 || iScanStatusOp == 1 || iScanStatusOp == 5 || iScanStatusOp == 6 || iScanStatusOp == 7) { + // NLOOP, NVISIT, SELECTID, PARENTID, NCYCLE - return sqlite3_int64 + sqlite3_int64 iOut; + rc = sqlite3_stmt_scanstatus_v2(stmt->handle, idx, iScanStatusOp, flags, (void*)&iOut); + if (rc == 0) { + info.GetReturnValue().Set(stmt->safe_ints + ? v8::BigInt::New(isolate, iOut).As() + : v8::Number::New(isolate, (double)iOut).As()); + return; + } + } else if (iScanStatusOp == 2) { + // EST - return double + double dOut; + rc = sqlite3_stmt_scanstatus_v2(stmt->handle, idx, iScanStatusOp, flags, (void*)&dOut); + if (rc == 0) { + info.GetReturnValue().Set(v8::Number::New(isolate, dOut)); + return; + } + } else if (iScanStatusOp == 3 || iScanStatusOp == 4) { + // NAME, EXPLAIN - return const char* + const char* zOut; + rc = sqlite3_stmt_scanstatus_v2(stmt->handle, idx, iScanStatusOp, flags, (void*)&zOut); + if (rc == 0 && zOut != NULL) { + info.GetReturnValue().Set(StringFromUtf8(isolate, zOut, -1)); + return; + } else if (rc == 0) { + info.GetReturnValue().Set(v8::Null(isolate)); + return; + } + } + + // Error or invalid operation - return undefined + info.GetReturnValue().Set(v8::Undefined(isolate)); +} +#line 370 "./src/objects/statement.lzz" void Statement::JS_busy (v8::Local _, v8::PropertyCallbackInfo const & info) -#line 321 "./src/objects/statement.lzz" +#line 370 "./src/objects/statement.lzz" { Statement* stmt = node :: ObjectWrap :: Unwrap (info.This()); info.GetReturnValue().Set(stmt->alive && stmt->locked); diff --git a/src/better_sqlite3.hpp b/src/better_sqlite3.hpp index eb887f28..9f52c41f 100644 --- a/src/better_sqlite3.hpp +++ b/src/better_sqlite3.hpp @@ -362,7 +362,9 @@ class Statement : public node::ObjectWrap static void JS_safeIntegers (v8::FunctionCallbackInfo const & info); #line 278 "./src/objects/statement.lzz" static void JS_columns (v8::FunctionCallbackInfo const & info); -#line 321 "./src/objects/statement.lzz" +#line 322 "./src/objects/statement.lzz" + static void JS_scanStatusV2 (v8::FunctionCallbackInfo const & info); +#line 370 "./src/objects/statement.lzz" static void JS_busy (v8::Local _, v8::PropertyCallbackInfo const & info); #line 326 "./src/objects/statement.lzz" Database * const db; diff --git a/src/better_sqlite3.lzz b/src/better_sqlite3.lzz index 430991fc..1e27091d 100644 --- a/src/better_sqlite3.lzz +++ b/src/better_sqlite3.lzz @@ -80,6 +80,17 @@ NODE_MODULE_INIT(/* exports, context */) { exports->Set(context, InternalizedFromLatin1(isolate, "Backup"), Backup::Init(isolate, data)).FromJust(); exports->Set(context, InternalizedFromLatin1(isolate, "setErrorConstructor"), v8::FunctionTemplate::New(isolate, Addon::JS_setErrorConstructor, data)->GetFunction(context).ToLocalChecked()).FromJust(); + // Export SQLITE_SCANSTAT_* constants + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_NLOOP"), v8::Int32::New(isolate, SQLITE_SCANSTAT_NLOOP)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_NVISIT"), v8::Int32::New(isolate, SQLITE_SCANSTAT_NVISIT)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_EST"), v8::Int32::New(isolate, SQLITE_SCANSTAT_EST)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_NAME"), v8::Int32::New(isolate, SQLITE_SCANSTAT_NAME)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_EXPLAIN"), v8::Int32::New(isolate, SQLITE_SCANSTAT_EXPLAIN)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_SELECTID"), v8::Int32::New(isolate, SQLITE_SCANSTAT_SELECTID)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_PARENTID"), v8::Int32::New(isolate, SQLITE_SCANSTAT_PARENTID)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_NCYCLE"), v8::Int32::New(isolate, SQLITE_SCANSTAT_NCYCLE)).FromJust(); + exports->Set(context, InternalizedFromLatin1(isolate, "SQLITE_SCANSTAT_COMPLEX"), v8::Int32::New(isolate, SQLITE_SCANSTAT_COMPLEX)).FromJust(); + // Store addon instance data. addon->Statement.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "Statement")).ToLocalChecked().As()); addon->StatementIterator.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "StatementIterator")).ToLocalChecked().As()); diff --git a/src/objects/statement.lzz b/src/objects/statement.lzz index 5e2c8057..4ee54693 100644 --- a/src/objects/statement.lzz +++ b/src/objects/statement.lzz @@ -13,6 +13,7 @@ public: SetPrototypeMethod(isolate, data, t, "raw", JS_raw); SetPrototypeMethod(isolate, data, t, "safeIntegers", JS_safeIntegers); SetPrototypeMethod(isolate, data, t, "columns", JS_columns); + SetPrototypeMethod(isolate, data, t, "scanStatusV2", JS_scanStatusV2); SetPrototypeGetter(isolate, data, t, "busy", JS_busy); return t->GetFunction(OnlyContext).ToLocalChecked(); } @@ -318,6 +319,54 @@ private: info.GetReturnValue().Set(columns); } + NODE_METHOD(JS_scanStatusV2) { + Statement* stmt = Unwrap(info.This()); + REQUIRE_DATABASE_OPEN(stmt->db->GetState()); + if (!stmt->alive) return ThrowTypeError("The statement has been finalized and can no longer be used"); + + REQUIRE_ARGUMENT_INT32(first, int idx); + REQUIRE_ARGUMENT_INT32(second, int iScanStatusOp); + REQUIRE_ARGUMENT_INT32(third, int flags); + + UseIsolate; + + // Based on iScanStatusOp, we know what type of output to expect + int rc; + if (iScanStatusOp == 0 || iScanStatusOp == 1 || iScanStatusOp == 5 || iScanStatusOp == 6 || iScanStatusOp == 7) { + // NLOOP, NVISIT, SELECTID, PARENTID, NCYCLE - return sqlite3_int64 + sqlite3_int64 iOut; + rc = sqlite3_stmt_scanstatus_v2(stmt->handle, idx, iScanStatusOp, flags, (void*)&iOut); + if (rc == 0) { + info.GetReturnValue().Set(stmt->safe_ints + ? v8::BigInt::New(isolate, iOut).As() + : v8::Number::New(isolate, (double)iOut).As()); + return; + } + } else if (iScanStatusOp == 2) { + // EST - return double + double dOut; + rc = sqlite3_stmt_scanstatus_v2(stmt->handle, idx, iScanStatusOp, flags, (void*)&dOut); + if (rc == 0) { + info.GetReturnValue().Set(v8::Number::New(isolate, dOut)); + return; + } + } else if (iScanStatusOp == 3 || iScanStatusOp == 4) { + // NAME, EXPLAIN - return const char* + const char* zOut; + rc = sqlite3_stmt_scanstatus_v2(stmt->handle, idx, iScanStatusOp, flags, (void*)&zOut); + if (rc == 0 && zOut != NULL) { + info.GetReturnValue().Set(StringFromUtf8(isolate, zOut, -1)); + return; + } else if (rc == 0) { + info.GetReturnValue().Set(v8::Null(isolate)); + return; + } + } + + // Error or invalid operation - return undefined + info.GetReturnValue().Set(v8::Undefined(isolate)); + } + NODE_GETTER(JS_busy) { Statement* stmt = Unwrap(info.This()); info.GetReturnValue().Set(stmt->alive && stmt->locked); diff --git a/test/26.statement.scanstatus.js b/test/26.statement.scanstatus.js new file mode 100644 index 00000000..0ce1aa30 --- /dev/null +++ b/test/26.statement.scanstatus.js @@ -0,0 +1,100 @@ +'use strict'; +const Database = require('../.'); + +describe('Statement#scanStatusV2()', function () { + beforeEach(function () { + this.db = new Database(util.next()); + this.db.prepare('CREATE TABLE entries (id INTEGER PRIMARY KEY, name TEXT, value INTEGER)').run(); + this.db.prepare('INSERT INTO entries (name, value) VALUES (?, ?)').run('foo', 1); + this.db.prepare('INSERT INTO entries (name, value) VALUES (?, ?)').run('bar', 2); + this.db.prepare('INSERT INTO entries (name, value) VALUES (?, ?)').run('baz', 3); + }); + afterEach(function () { + this.db.close(); + }); + + it('should return scan status information for a query', function () { + const stmt = this.db.prepare('SELECT * FROM entries WHERE value > ?'); + + // Execute the statement to populate scan status + const rows = stmt.all(1); + expect(rows).to.have.lengthOf(2); + + // Get scan status for the first loop (idx=0) + const explain = stmt.scanStatusV2(0, Database.SQLITE_SCANSTAT_EXPLAIN, 0); + expect(explain).to.be.a('string'); + + const nLoop = stmt.scanStatusV2(0, Database.SQLITE_SCANSTAT_NLOOP, 0); + expect(nLoop).to.be.a('number'); + + const nVisit = stmt.scanStatusV2(0, Database.SQLITE_SCANSTAT_NVISIT, 0); + expect(nVisit).to.be.a('number'); + }); + + it('should return undefined for invalid index', function () { + const stmt = this.db.prepare('SELECT * FROM entries'); + stmt.all(); + + // Try to get scan status for a non-existent loop + const result = stmt.scanStatusV2(999, Database.SQLITE_SCANSTAT_EXPLAIN, 0); + expect(result).to.be.undefined; + }); + + it('should work with SQLITE_SCANSTAT_COMPLEX flag', function () { + const stmt = this.db.prepare('SELECT * FROM entries WHERE value > ?'); + stmt.all(1); + + // Use COMPLEX flag to get more detailed information + const explain = stmt.scanStatusV2(0, Database.SQLITE_SCANSTAT_EXPLAIN, Database.SQLITE_SCANSTAT_COMPLEX); + expect(explain).to.be.a('string'); + + const selectId = stmt.scanStatusV2(0, Database.SQLITE_SCANSTAT_SELECTID, Database.SQLITE_SCANSTAT_COMPLEX); + expect(selectId).to.be.a('number'); + }); + + it('should respect safeIntegers setting', function () { + const stmt = this.db.prepare('SELECT * FROM entries').safeIntegers(true); + stmt.all(); + + const nLoop = stmt.scanStatusV2(0, Database.SQLITE_SCANSTAT_NLOOP, 0); + expect(nLoop).to.be.a('bigint'); + }); + + it('should return EST as a double', function () { + const stmt = this.db.prepare('SELECT * FROM entries WHERE value > ?'); + stmt.all(1); + + const est = stmt.scanStatusV2(0, Database.SQLITE_SCANSTAT_EST, 0); + expect(est).to.be.a('number'); + expect(est).to.be.greaterThan(0); + }); + + it('should return NAME for table/index name', function () { + const stmt = this.db.prepare('SELECT * FROM entries'); + stmt.all(); + + const name = stmt.scanStatusV2(0, Database.SQLITE_SCANSTAT_NAME, 0); + // Name could be null for some query plans or a string for table/index name + expect(name === null || typeof name === 'string').to.be.true; + }); + + it('should throw when database is closed', function () { + const stmt = this.db.prepare('SELECT * FROM entries'); + this.db.close(); + + expect(() => stmt.scanStatusV2(0, Database.SQLITE_SCANSTAT_EXPLAIN, 0)) + .to.throw(TypeError); + }); + + it('should verify constants are exported', function () { + expect(Database.SQLITE_SCANSTAT_NLOOP).to.equal(0); + expect(Database.SQLITE_SCANSTAT_NVISIT).to.equal(1); + expect(Database.SQLITE_SCANSTAT_EST).to.equal(2); + expect(Database.SQLITE_SCANSTAT_NAME).to.equal(3); + expect(Database.SQLITE_SCANSTAT_EXPLAIN).to.equal(4); + expect(Database.SQLITE_SCANSTAT_SELECTID).to.equal(5); + expect(Database.SQLITE_SCANSTAT_PARENTID).to.equal(6); + expect(Database.SQLITE_SCANSTAT_NCYCLE).to.equal(7); + expect(Database.SQLITE_SCANSTAT_COMPLEX).to.equal(0x0001); + }); +});