Skip to content

Commit 2fa8189

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

File tree

3 files changed

+135
-0
lines changed

3 files changed

+135
-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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
const explainSql = sql.trim().toUpperCase().startsWith('EXPLAIN')
7+
? sql
8+
: `EXPLAIN ${sql}`;
9+
10+
const stmt = this.prepare(explainSql);
11+
12+
try {
13+
return stmt.all();
14+
} catch (e) {
15+
if (e.message && (e.message.includes('Too few parameter') || e.message.includes('Missing named parameter'))) {
16+
const namedParams = sql.match(/:(\w+)|@(\w+)|\$(\w+)/g);
17+
18+
if (namedParams && namedParams.length > 0) {
19+
const params = {};
20+
for (const param of namedParams) {
21+
const name = param.substring(1);
22+
params[name] = null;
23+
}
24+
return stmt.all(params);
25+
}
26+
let low = 1;
27+
let high = 100;
28+
29+
while (low <= high) {
30+
const mid = Math.floor((low + high) / 2);
31+
try {
32+
return stmt.all(Array(mid).fill(null));
33+
} catch (err) {
34+
if (err.message && err.message.includes('Too few')) {
35+
low = mid + 1;
36+
} else if (err.message && err.message.includes('Too many')) {
37+
high = mid - 1;
38+
} else {
39+
throw err;
40+
}
41+
}
42+
}
43+
}
44+
throw e;
45+
}
46+
};
47+

test/15.database.explain.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
expect(plan[0]).to.have.property('opcode');
28+
});
29+
30+
it('should work with EXPLAIN already in the SQL', function () {
31+
const plan1 = this.db.explain('SELECT * FROM entries');
32+
const plan2 = this.db.explain('EXPLAIN SELECT * FROM entries');
33+
expect(plan1).to.deep.equal(plan2);
34+
});
35+
36+
it('should work with EXPLAIN QUERY PLAN', function () {
37+
const plan = this.db.explain("EXPLAIN QUERY PLAN SELECT * FROM entries WHERE a = 'foo'");
38+
expect(plan).to.be.an('array');
39+
expect(plan.length).to.be.greaterThan(0);
40+
expect(plan[0]).to.be.an('object');
41+
});
42+
43+
it('should handle queries with parameters without throwing errors', function () {
44+
const plan1 = this.db.explain('SELECT * FROM entries WHERE a = ?');
45+
expect(plan1).to.be.an('array');
46+
expect(plan1.length).to.be.greaterThan(0);
47+
48+
const plan2 = this.db.explain('SELECT * FROM entries WHERE a = :name AND b = :value');
49+
expect(plan2).to.be.an('array');
50+
expect(plan2.length).to.be.greaterThan(0);
51+
});
52+
53+
it('should handle complex queries with multiple parameters', function () {
54+
const plan = this.db.explain('SELECT * FROM entries WHERE a = ? AND b > ? AND c < ?');
55+
expect(plan).to.be.an('array');
56+
expect(plan.length).to.be.greaterThan(0);
57+
});
58+
59+
it('should work with JOIN queries', function () {
60+
this.db.exec('CREATE TABLE users (id INTEGER, name TEXT)');
61+
const plan = this.db.explain('SELECT * FROM entries JOIN users ON entries.b = users.id WHERE users.name = ?');
62+
expect(plan).to.be.an('array');
63+
expect(plan.length).to.be.greaterThan(0);
64+
});
65+
66+
it('should throw an exception for invalid SQL', function () {
67+
expect(() => this.db.explain('INVALID SQL')).to.throw(Database.SqliteError);
68+
expect(() => this.db.explain('SELECT * FROM nonexistent')).to.throw(Database.SqliteError);
69+
});
70+
71+
it('should work with case insensitive EXPLAIN', function () {
72+
const plan1 = this.db.explain('explain SELECT * FROM entries');
73+
const plan2 = this.db.explain('ExPlAiN SELECT * FROM entries');
74+
const plan3 = this.db.explain('EXPLAIN SELECT * FROM entries');
75+
expect(plan1.length).to.equal(plan2.length);
76+
expect(plan2.length).to.equal(plan3.length);
77+
});
78+
79+
it('should respect readonly connections', function () {
80+
this.db.close();
81+
this.db = new Database(util.current(), { readonly: true, fileMustExist: true });
82+
const plan = this.db.explain('SELECT * FROM entries WHERE a = ?');
83+
expect(plan).to.be.an('array');
84+
expect(plan.length).to.be.greaterThan(0);
85+
});
86+
});
87+

0 commit comments

Comments
 (0)