From 890edb2fe0a46ce33a6aec929107328085ca4fc7 Mon Sep 17 00:00:00 2001 From: Matt Wonlaw Date: Thu, 4 Dec 2025 16:33:20 -0500 Subject: [PATCH 1/3] explain query plan --- src/objects/statement.cpp | 85 +++++++++++++++++++++++++++++++++++++++ src/objects/statement.hpp | 1 + 2 files changed, 86 insertions(+) diff --git a/src/objects/statement.cpp b/src/objects/statement.cpp index 02e43ce..37157c5 100644 --- a/src/objects/statement.cpp +++ b/src/objects/statement.cpp @@ -66,6 +66,7 @@ INIT(Statement::Init) { SetPrototypeMethod(isolate, data, t, "columns", JS_columns); SetPrototypeMethod(isolate, data, t, "scanStatusV2", JS_scanStatusV2); SetPrototypeMethod(isolate, data, t, "scanStatusReset", JS_scanStatusReset); + SetPrototypeMethod(isolate, data, t, "explainQueryPlan", JS_explainQueryPlan); SetPrototypeGetter(isolate, data, t, "busy", JS_busy); return t->GetFunction(OnlyContext).ToLocalChecked(); } @@ -227,6 +228,90 @@ NODE_METHOD(Statement::JS_all) { #endif } +NODE_METHOD(Statement::JS_explainQueryPlan) { + Statement* stmt = Unwrap(info.This()); + REQUIRE_STATEMENT_RETURNS_DATA(); + sqlite3_stmt* handle = stmt->handle; + Database* db = stmt->db; + REQUIRE_DATABASE_OPEN(db->GetState()); + REQUIRE_DATABASE_NOT_BUSY(db->GetState()); + REQUIRE_STATEMENT_NOT_LOCKED(stmt); + + // Binding is OPTIONAL: bind if params provided, skip if not + const bool pre_bound = stmt->bound; + bool did_bind = false; + if (!pre_bound && info.Length() > 0) { + STATEMENT_BIND(handle); + did_bind = true; + } else if (pre_bound && info.Length() > 0) { + return ThrowTypeError("This statement already has bound parameters"); + } + + db->GetState()->busy = true; + UseIsolate; + if (db->Log(isolate, handle)) { + db->GetState()->busy = false; + if (did_bind) { sqlite3_clear_bindings(handle); } + db->ThrowDatabaseError(); + return; + } + + UseContext; + const bool safe_ints = stmt->safe_ints; + const char mode = stmt->mode; + +#if !defined(NODE_MODULE_VERSION) || NODE_MODULE_VERSION < 127 + bool js_error = false; + uint32_t row_count = 0; + v8::Local result = v8::Array::New(isolate, 0); + + while (sqlite3_step(handle) == SQLITE_ROW) { + if (row_count == 0xffffffff) { ThrowRangeError("Array overflow (too many rows returned)"); js_error = true; break; } + result->Set(ctx, row_count++, Data::GetRowJS(isolate, ctx, handle, safe_ints, mode)).FromJust(); + } + + if (sqlite3_reset(handle) == SQLITE_OK && !js_error) { + db->GetState()->busy = false; + if (did_bind) { sqlite3_clear_bindings(handle); } + info.GetReturnValue().Set(result); + return; + } + if (js_error) db->GetState()->was_js_error = true; + db->GetState()->busy = false; + if (did_bind) { sqlite3_clear_bindings(handle); } + db->ThrowDatabaseError(); +#else + v8::LocalVector rows(isolate); + rows.reserve(8); + + if (mode == Data::FLAT) { + RowBuilder rowBuilder(isolate, handle, safe_ints); + while (sqlite3_step(handle) == SQLITE_ROW) { + rows.emplace_back(rowBuilder.GetRowJS()); + } + } else { + while (sqlite3_step(handle) == SQLITE_ROW) { + rows.emplace_back(Data::GetRowJS(isolate, ctx, handle, safe_ints, mode)); + } + } + + if (sqlite3_reset(handle) == SQLITE_OK) { + if (rows.size() > 0xffffffff) { + ThrowRangeError("Array overflow (too many rows returned)"); + db->GetState()->was_js_error = true; + } else { + db->GetState()->busy = false; + if (did_bind) { sqlite3_clear_bindings(handle); } + info.GetReturnValue().Set(v8::Array::New(isolate, rows.data(), rows.size())); + return; + } + } + db->GetState()->busy = false; + if (did_bind) { sqlite3_clear_bindings(handle); } + db->ThrowDatabaseError(); +#endif +} + NODE_METHOD(Statement::JS_iterate) { UseAddon; UseIsolate; diff --git a/src/objects/statement.hpp b/src/objects/statement.hpp index 8a37b69..d387ee0 100644 --- a/src/objects/statement.hpp +++ b/src/objects/statement.hpp @@ -45,6 +45,7 @@ class Statement : public node::ObjectWrap { friend class StatementIterator; static NODE_METHOD(JS_columns); static NODE_METHOD(JS_scanStatusV2); static NODE_METHOD(JS_scanStatusReset); + static NODE_METHOD(JS_explainQueryPlan); static NODE_GETTER(JS_busy); Database* const db; From ff957f2d409f5d0b14a3d93dfe7d47b42c658e74 Mon Sep 17 00:00:00 2001 From: Matt Wonlaw Date: Thu, 4 Dec 2025 16:37:18 -0500 Subject: [PATCH 2/3] types --- lib/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/index.d.ts b/lib/index.d.ts index d16d60e..c4949dd 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -44,6 +44,7 @@ declare namespace BetterSqlite3 { scanStatusV2(idx: number, opcode: ScanStatOpcode.SQLITE_SCANSTAT_NCYCLE, resetFlag: number): number | undefined; scanStatusV2(idx: number, opcode: ScanStatOpcode, resetFlag: number): number | string | undefined; scanStatusReset(): this; + explainQueryPlan(...params: BindParameters | []): Result[]; } interface ColumnDefinition { From 353b449dbdced4f920fb8cf382477246899f38fe Mon Sep 17 00:00:00 2001 From: Matt Wonlaw Date: Thu, 4 Dec 2025 16:39:51 -0500 Subject: [PATCH 3/3] bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6059012..d37c847 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rocicorp/zero-sqlite3", - "version": "1.0.12", + "version": "1.0.13", "description": "better-sqlite3 on bedrock", "homepage": "https://github.com/rocicorp/zero-sqlite3", "author": "Rocicorp",