implemented Database#explain()

This commit is contained in:
Joshua Wise
2026-07-21 01:06:49 -05:00
parent f999e55af0
commit df5a17def4
8 changed files with 93 additions and 2 deletions
+14
View File
@@ -11,6 +11,7 @@
- [Database#prepare()](#preparestring---statement) (see [`Statement`](#class-statement))
- [Database#transaction()](#transactionfunction---function)
- [Database#pragma()](#pragmastring-options---results)
- [Database#explain()](#explainstring---array-of-rows)
- [Database#backup()](#backupdestination-options---promise)
- [Database#serialize()](#serializeoptions---buffer)
- [Database#function()](#functionname-options-function---this)
@@ -126,6 +127,19 @@ If execution of the PRAGMA fails, an `Error` is thrown.
It's better to use this method instead of normal [prepared statements](#preparestring---statement) when executing PRAGMA, because this method normalizes some odd behavior that may otherwise be experienced. The documentation on SQLite PRAGMA can be found [here](https://www.sqlite.org/pragma.html).
### .explain(*string*) -> *array of rows*
Runs [`EXPLAIN`](https://www.sqlite.org/eqp.html) on the given SQL string and returns the result. By default this returns the low-level [bytecode](https://www.sqlite.org/opcode.html) program that SQLite would use to execute the statement (one row per virtual machine instruction). To get the higher-level [query plan](https://www.sqlite.org/eqp.html) instead, prefix your SQL with `QUERY PLAN`.
```js
db.explain('SELECT * FROM cats WHERE name = ?');
db.explain('QUERY PLAN SELECT * FROM cats WHERE name = ?');
```
Unlike normal [prepared statements](#preparestring---statement), the SQL passed to this method may contain [parameters](#binding-parameters) that are left unbound. Since `EXPLAIN` only inspects the statement rather than executing it, no parameter values are needed.
If the given SQL is invalid, an `Error` is thrown.
### .backup(*destination*, [*options*]) -> *promise*
Initiates a [backup](https://www.sqlite.org/backup.html) of the database, returning a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) for when the backup is complete. If the backup fails, the promise will be rejected with an `Error`. You can optionally backup an attached database instead by setting the `attached` option to the name of the desired attached database. A backup file is just a regular SQLite database file. It can be opened by [`new Database()`](#new-databasepath-options) just like any SQLite database.
+1
View File
@@ -64,6 +64,7 @@ module.exports = function createDatabase(getAddon, allowNativeBinding) {
Database.prototype.prepare = wrappers.prepare;
Database.prototype.transaction = require('./methods/transaction');
Database.prototype.pragma = require('./methods/pragma');
Database.prototype.explain = require('./methods/explain');
Database.prototype.backup = require('./methods/backup');
Database.prototype.serialize = require('./methods/serialize');
Database.prototype.function = require('./methods/function');
+9
View File
@@ -0,0 +1,9 @@
'use strict';
const { cppdb } = require('../util');
module.exports = function explain(source) {
if (typeof source !== 'string') throw new TypeError('Expected first argument to be a string');
const stmt = this[cppdb].prepare(`EXPLAIN ${source}`, this, false, true);
return stmt.all();
};
+1 -1
View File
@@ -7,6 +7,6 @@ module.exports = function pragma(source, options) {
if (typeof options !== 'object') throw new TypeError('Expected second argument to be an options object');
const simple = getBooleanOption(options, 'simple');
const stmt = this[cppdb].prepare(`PRAGMA ${source}`, this, true);
const stmt = this[cppdb].prepare(`PRAGMA ${source}`, this, true, false);
return simple ? stmt.pluck().get() : stmt.all();
};
+1 -1
View File
@@ -2,7 +2,7 @@
const { cppdb } = require('../util');
exports.prepare = function prepare(sql) {
return this[cppdb].prepare(sql, this, false);
return this[cppdb].prepare(sql, this, false, false);
};
exports.exec = function exec(sql) {
+2
View File
@@ -199,9 +199,11 @@ NODE_METHOD(Database::JS_prepare) {
REQUIRE_ARGUMENT_STRING(first, Napi::String source);
REQUIRE_ARGUMENT_OBJECT(second, Napi::Object database);
REQUIRE_ARGUMENT_BOOLEAN(third, bool pragmaMode);
REQUIRE_ARGUMENT_BOOLEAN(fourth, bool explainMode);
(void)source;
(void)database;
(void)pragmaMode;
(void)explainMode;
UseAddon;
UseIsolate;
Napi::Function c = addon->Statement.Value();
+5
View File
@@ -89,12 +89,16 @@ NODE_METHOD(Statement::JS_new) {
Napi::String source = pinfo[0].As<Napi::String>();
Napi::Object database = pinfo[1].As<Napi::Object>();
bool pragmaMode = pinfo[2].As<Napi::Boolean>().Value();
bool explainMode = pinfo[3].As<Napi::Boolean>().Value();
int flags = SQLITE_PREPARE_PERSISTENT;
if (pragmaMode) {
REQUIRE_DATABASE_NO_ITERATORS_UNLESS_UNSAFE(db->GetState());
flags = 0;
}
if (explainMode) {
flags = 0;
}
UseIsolate;
std::string utf8 = source.Utf8Value();
@@ -140,6 +144,7 @@ NODE_METHOD(Statement::JS_new) {
this->db = db;
this->handle = handle;
this->extras = new Extras(env, addon->RowFactory.Value(), addon->ArrayFactory.Value(), addon->NextId());
this->bound = explainMode;
this->safe_ints = db->GetState()->safe_ints;
this->returns_data = returns_data;
this->alive = true;
+60
View File
@@ -0,0 +1,60 @@
'use strict';
const Database = require('../.');
describe('Database#explain()', function () {
beforeEach(function () {
this.db = new Database(util.next());
this.db.prepare('CREATE TABLE entries (a TEXT, b INTEGER)').run();
});
afterEach(function () {
this.db.close();
});
it('should throw an exception if a string is not provided', function () {
expect(() => this.db.explain(123)).to.throw(TypeError);
expect(() => this.db.explain(0)).to.throw(TypeError);
expect(() => this.db.explain(null)).to.throw(TypeError);
expect(() => this.db.explain()).to.throw(TypeError);
expect(() => this.db.explain(new String('SELECT * FROM entries'))).to.throw(TypeError);
});
it('should throw an exception if invalid SQL is provided', function () {
expect(() => this.db.explain('SELECT * FROM nonexistent')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
expect(() => this.db.explain('CREATE TABLE entries (a TEXT')).to.throw(Database.SqliteError).with.property('code', 'SQLITE_ERROR');
});
it('should return the bytecode program as an array of rows', function () {
const program = this.db.explain('SELECT * FROM entries');
expect(program).to.be.an('array');
expect(program.length).to.be.above(0);
for (const row of program) {
expect(row).to.be.an('object');
expect(row.addr).to.be.a('number');
expect(row.opcode).to.be.a('string');
expect(row).to.have.keys(['addr', 'opcode', 'p1', 'p2', 'p3', 'p4', 'p5', 'comment']);
}
expect(program.some(row => row.opcode === 'Halt')).to.be.true;
});
it('should explain statements with unbound parameters', function () {
// A normal prepared statement cannot be executed without its parameters.
expect(() => this.db.prepare('SELECT * FROM entries WHERE a = ?').all()).to.throw(RangeError);
const program = this.db.explain('SELECT * FROM entries WHERE a = ?');
expect(program).to.be.an('array');
expect(program.length).to.be.above(0);
});
it('should explain statements with multiple unbound parameters', function () {
const program = this.db.explain('SELECT * FROM entries WHERE a = ? AND b = @b');
expect(program).to.be.an('array');
expect(program.length).to.be.above(0);
});
it('should return the query plan when prefixed with QUERY PLAN', function () {
const plan = this.db.explain('QUERY PLAN SELECT * FROM entries');
expect(plan).to.be.an('array');
expect(plan.length).to.be.above(0);
for (const row of plan) {
expect(row).to.be.an('object');
expect(row).to.have.keys(['id', 'parent', 'notused', 'detail']);
expect(row.detail).to.be.a('string');
}
expect(plan.some(row => /entries/.test(row.detail))).to.be.true;
});
});