Skip to content

Commit 2ff31b2

Browse files
committed
Add Database#explain() method to support EXPLAIN with parameters
Fixes #1243
1 parent ea0d8c7 commit 2ff31b2

File tree

3 files changed

+146
-0
lines changed

3 files changed

+146
-0
lines changed

lib/database.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const wrappers = require('./methods/wrappers');
7575
Database.prototype.prepare = wrappers.prepare;
7676
Database.prototype.transaction = require('./methods/transaction');
7777
Database.prototype.pragma = require('./methods/pragma');
78+
Database.prototype.explain = require('./methods/explain');
7879
Database.prototype.backup = require('./methods/backup');
7980
Database.prototype.serialize = require('./methods/serialize');
8081
Database.prototype.function = require('./methods/function');

lib/methods/explain.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
module.exports = function explain(sql) {
4+
if (typeof sql !== 'string') throw new TypeError('Expected first argument to be a string');
5+
6+
// Prepend EXPLAIN if not already present
7+
const explainSql = sql.trim().toUpperCase().startsWith('EXPLAIN')
8+
? sql
9+
: `EXPLAIN ${sql}`;
10+
11+
// Prepare the statement normally
12+
const stmt = this.prepare(explainSql);
13+
14+
// For EXPLAIN statements, we don't need actual parameter values
15+
// We'll try to execute without parameters first
16+
try {
17+
return stmt.all();
18+
} catch (e) {
19+
// Handle parameter errors by binding nulls
20+
if (e.message && (e.message.includes('Too few parameter') || e.message.includes('Missing named parameter'))) {
21+
// Extract named parameters from the SQL
22+
const namedParams = sql.match(/:(\w+)|@(\w+)|\$(\w+)/g);
23+
24+
if (namedParams && namedParams.length > 0) {
25+
// Build an object with null values for all named parameters
26+
const params = {};
27+
for (const param of namedParams) {
28+
const name = param.substring(1); // Remove the :, @, or $ prefix
29+
params[name] = null;
30+
}
31+
return stmt.all(params);
32+
}
33+
34+
// For positional parameters, use binary search
35+
let low = 1;
36+
let high = 100; // Reasonable maximum
37+
38+
while (low <= high) {
39+
const mid = Math.floor((low + high) / 2);
40+
try {
41+
return stmt.all(Array(mid).fill(null));
42+
} catch (err) {
43+
if (err.message && err.message.includes('Too few')) {
44+
low = mid + 1;
45+
} else if (err.message && err.message.includes('Too many')) {
46+
high = mid - 1;
47+
} else {
48+
throw err;
49+
}
50+
}
51+
}
52+
}
53+
throw e;
54+
}
55+
};
56+

test/15.database.explain.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
'use strict';
2+
const Database = require('../lib');
3+
4+
describe('Database#explain()', function () {
5+
beforeEach(function () {
6+
this.db = new Database(util.next());
7+
this.db.exec('CREATE TABLE entries (a TEXT, b INTEGER, c REAL)');
8+
this.db.exec("INSERT INTO entries VALUES ('foo', 1, 3.14), ('bar', 2, 2.71)");
9+
});
10+
afterEach(function () {
11+
this.db.close();
12+
});
13+
14+
it('should throw an exception if a string is not provided', function () {
15+
expect(() => this.db.explain(123)).to.throw(TypeError);
16+
expect(() => this.db.explain(0)).to.throw(TypeError);
17+
expect(() => this.db.explain(null)).to.throw(TypeError);
18+
expect(() => this.db.explain()).to.throw(TypeError);
19+
expect(() => this.db.explain(new String('SELECT * FROM entries'))).to.throw(TypeError);
20+
});
21+
22+
it('should execute a simple EXPLAIN query without parameters', function () {
23+
const plan = this.db.explain('SELECT * FROM entries');
24+
expect(plan).to.be.an('array');
25+
expect(plan.length).to.be.greaterThan(0);
26+
expect(plan[0]).to.be.an('object');
27+
// EXPLAIN output should have columns like 'addr', 'opcode', 'p1', 'p2', 'p3', 'p4', 'p5', 'comment'
28+
expect(plan[0]).to.have.property('opcode');
29+
});
30+
31+
it('should work with EXPLAIN already in the SQL', function () {
32+
const plan1 = this.db.explain('SELECT * FROM entries');
33+
const plan2 = this.db.explain('EXPLAIN SELECT * FROM entries');
34+
expect(plan1).to.deep.equal(plan2);
35+
});
36+
37+
it('should work with EXPLAIN QUERY PLAN', function () {
38+
const plan = this.db.explain("EXPLAIN QUERY PLAN SELECT * FROM entries WHERE a = 'foo'");
39+
expect(plan).to.be.an('array');
40+
expect(plan.length).to.be.greaterThan(0);
41+
expect(plan[0]).to.be.an('object');
42+
});
43+
44+
it('should handle queries with parameters without throwing errors', function () {
45+
// This is the key fix - EXPLAIN with parameters should work
46+
const plan1 = this.db.explain('SELECT * FROM entries WHERE a = ?');
47+
expect(plan1).to.be.an('array');
48+
expect(plan1.length).to.be.greaterThan(0);
49+
50+
const plan2 = this.db.explain('SELECT * FROM entries WHERE a = :name AND b = :value');
51+
expect(plan2).to.be.an('array');
52+
expect(plan2.length).to.be.greaterThan(0);
53+
});
54+
55+
it('should handle complex queries with multiple parameters', function () {
56+
const plan = this.db.explain('SELECT * FROM entries WHERE a = ? AND b > ? AND c < ?');
57+
expect(plan).to.be.an('array');
58+
expect(plan.length).to.be.greaterThan(0);
59+
});
60+
61+
it('should work with JOIN queries', function () {
62+
this.db.exec('CREATE TABLE users (id INTEGER, name TEXT)');
63+
const plan = this.db.explain('SELECT * FROM entries JOIN users ON entries.b = users.id WHERE users.name = ?');
64+
expect(plan).to.be.an('array');
65+
expect(plan.length).to.be.greaterThan(0);
66+
});
67+
68+
it('should throw an exception for invalid SQL', function () {
69+
expect(() => this.db.explain('INVALID SQL')).to.throw(Database.SqliteError);
70+
expect(() => this.db.explain('SELECT * FROM nonexistent')).to.throw(Database.SqliteError);
71+
});
72+
73+
it('should work with case insensitive EXPLAIN', function () {
74+
const plan1 = this.db.explain('explain SELECT * FROM entries');
75+
const plan2 = this.db.explain('ExPlAiN SELECT * FROM entries');
76+
const plan3 = this.db.explain('EXPLAIN SELECT * FROM entries');
77+
expect(plan1.length).to.equal(plan2.length);
78+
expect(plan2.length).to.equal(plan3.length);
79+
});
80+
81+
it('should respect readonly connections', function () {
82+
this.db.close();
83+
this.db = new Database(util.current(), { readonly: true, fileMustExist: true });
84+
const plan = this.db.explain('SELECT * FROM entries WHERE a = ?');
85+
expect(plan).to.be.an('array');
86+
expect(plan.length).to.be.greaterThan(0);
87+
});
88+
});
89+

0 commit comments

Comments
 (0)