Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/sqlite3.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export class Statement extends events.EventEmitter {
each<T>(callback?: (err: Error | null, row: T) => void, complete?: (err: Error | null, count: number) => void): this;
each<T>(params: any, callback?: (this: RunResult, err: Error | null, row: T) => void, complete?: (err: Error | null, count: number) => void): this;
each(...params: any[]): this;

readonly expandedSql: string | undefined;
}

export class Database extends events.EventEmitter {
Expand Down
31 changes: 30 additions & 1 deletion src/statement.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Napi::Object Statement::Init(Napi::Env env, Napi::Object exports) {
InstanceMethod("each", &Statement::Each, napi_default_method),
InstanceMethod("reset", &Statement::Reset, napi_default_method),
InstanceMethod("finalize", &Statement::Finalize_, napi_default_method),
InstanceAccessor("expandedSql", &Statement::ExpandedSQLGetter, nullptr, napi_default_method),
});

exports.Set("Statement", t);
Expand Down Expand Up @@ -159,6 +160,7 @@ void Statement::Work_AfterPrepare(napi_env e, napi_status status, void* data) {
auto env = stmt->Env();
Napi::HandleScope scope(env);


if (stmt->status != SQLITE_OK) {
Error(baton.get());
stmt->Finalize_();
Expand Down Expand Up @@ -364,6 +366,7 @@ void Statement::Work_AfterBind(napi_env e, napi_status status, void* data) {
auto env = stmt->Env();
Napi::HandleScope scope(env);


if (stmt->status != SQLITE_OK) {
Error(baton.get());
}
Expand Down Expand Up @@ -431,6 +434,7 @@ void Statement::Work_AfterGet(napi_env e, napi_status status, void* data) {
auto env = stmt->Env();
Napi::HandleScope scope(env);


if (stmt->status != SQLITE_ROW && stmt->status != SQLITE_DONE) {
Error(baton.get());
}
Expand Down Expand Up @@ -505,6 +509,7 @@ void Statement::Work_AfterRun(napi_env e, napi_status status, void* data) {
auto env = stmt->Env();
Napi::HandleScope scope(env);


if (stmt->status != SQLITE_ROW && stmt->status != SQLITE_DONE) {
Error(baton.get());
}
Expand Down Expand Up @@ -575,6 +580,7 @@ void Statement::Work_AfterAll(napi_env e, napi_status status, void* data) {
auto env = stmt->Env();
Napi::HandleScope scope(env);


if (stmt->status != SQLITE_DONE) {
Error(baton.get());
}
Expand Down Expand Up @@ -740,10 +746,10 @@ void Statement::Work_AfterEach(napi_env e, napi_status status, void* data) {
auto env = stmt->Env();
Napi::HandleScope scope(env);


if (stmt->status != SQLITE_DONE) {
Error(baton.get());
}

STATEMENT_END();
}

Expand Down Expand Up @@ -777,6 +783,7 @@ void Statement::Work_AfterReset(napi_env e, napi_status status, void* data) {
auto env = stmt->Env();
Napi::HandleScope scope(env);


// Fire callbacks.
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
Expand Down Expand Up @@ -937,3 +944,25 @@ void Statement::CleanQueue() {
delete call->baton;
}
}

Napi::Value Statement::ExpandedSQLGetter(const Napi::CallbackInfo& info) {
auto env = info.Env();

if (!_handle) {
return env.Undefined();
}

// call SQLite's sqlite3_expanded_sql() to get the SQL with bound parameters
char* expanded = sqlite3_expanded_sql(_handle);

// handle memory allocation failure
if (!expanded) {
return env.Undefined();
}

// create JS string and free the C string
Napi::String result = Napi::String::New(env, expanded);
sqlite3_free(expanded);

return result;
}
1 change: 1 addition & 0 deletions src/statement.h
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ class Statement : public Napi::ObjectWrap<Statement> {
WORK_DEFINITION(Reset)

Napi::Value Finalize_(const Napi::CallbackInfo& info);
Napi::Value ExpandedSQLGetter(const Napi::CallbackInfo& info);

protected:
static void Work_BeginPrepare(Database::Baton* baton);
Expand Down
244 changes: 244 additions & 0 deletions test/expandedSql.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
var sqlite3 = require('..');
var assert = require('assert');

describe('expandedSql', function() {
var db;
before(function(done) {
db = new sqlite3.Database(':memory:');
db.serialize(function() {
db.run("CREATE TABLE foo (id INT, txt TEXT)");
db.run("CREATE TABLE complex (a INT, b INT, c INT, d INT, e INT)", done);
});
});
after(function(done) {
db.wait(function() {
db.close(done);
});
});

it('should be accessible as a property on Statement', function(done) {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)", function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(NULL,NULL)");
stmt.finalize(done);
});
});

it('should show bound parameters in callbacks', function(done) {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)");
stmt.run(1, "test", function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(1,'test')");
stmt.finalize(done);
});
});

it('should work with get callback', function(done) {
var stmt = db.prepare("SELECT * FROM foo WHERE id = ?");
stmt.get(1, function(err, row) {
if (err) throw err;
assert.equal(stmt.expandedSql, "SELECT * FROM foo WHERE id = 1");
assert.equal(row.id, 1);
stmt.finalize(done);
});
});

it('should work with all callback', function(done) {
var stmt = db.prepare("SELECT * FROM foo WHERE id > ?");
stmt.all(0, function(err, rows) {
if (err) throw err;
assert.equal(stmt.expandedSql, "SELECT * FROM foo WHERE id > 0");
assert.ok(rows.length > 0);
stmt.finalize(done);
});
});

it('should work with each callback', function(done) {
var stmt = db.prepare("SELECT * FROM foo WHERE id < ?");
var count = 0;
stmt.each(5, function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "SELECT * FROM foo WHERE id < 5");
count++;
}, function(err) {
if (err) throw err;
assert.ok(count > 0);
stmt.finalize(done);
});
});

it('should work with bind callback', function(done) {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)");
stmt.bind(10, "hello", function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(10,'hello')");
stmt.run(function(err) {
if (err) throw err;
stmt.finalize(done);
});
});
});

it('should handle string escaping correctly', function(done) {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)");
stmt.run(20, "test'quote", function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(20,'test''quote')");
stmt.finalize(done);
});
});

it('should handle incomplete bindings', function(done) {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)");
stmt.run(20, function(err) {
if (err) throw err;
// sqlite fills unbound parameters with NULL, not ?
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(20,NULL)");
stmt.finalize(done);
});
});

it('should work with named parameters', function(done) {
var stmt = db.prepare("SELECT * FROM foo WHERE id = $id");
stmt.get({ $id: 1 }, function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "SELECT * FROM foo WHERE id = 1");
stmt.finalize(done);
});
});

it('should handle NULL values', function(done) {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)");
stmt.run(200, null, function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(200,NULL)");
stmt.finalize(done);
});
});

it('should handle empty strings', function(done) {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)");
stmt.run(1, "", function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(1,'')");
stmt.finalize(done);
});
});

it('should handle large integers', function(done) {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)");
var maxSafeInt = Number.MAX_SAFE_INTEGER;
stmt.run(maxSafeInt, "large", function(err) {
if (err) throw err;
// sqlite formats large numbers - accept various scientific notations
var expanded = stmt.expandedSql;
assert.ok(
expanded.match(/INSERT INTO foo VALUES\(9\.00719.*[eE]\+1[45],'large'\)/),
'Expected large number in scientific notation, got: ' + expanded
);
stmt.finalize(done);
});
});

it('should return undefined before statement is prepared', function(done) {
var stmt = db.prepare("SELECT 1");
assert.equal(stmt.expandedSql, undefined);
stmt.finalize(done);
});

it('should return undefined after finalize', function(done) {
var stmt = db.prepare("SELECT 1");
stmt.finalize(function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, undefined);
done();
});
});

it('should update after reset and rebind', function(done) {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)");

stmt.bind(5, "first", function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(5,'first')");

stmt.run(function(err) {
if (err) throw err;

stmt.reset(function(err) {
if (err) throw err;

stmt.bind(6, "second", function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(6,'second')");
stmt.finalize(done);
});
});
});
});
});

it('should work with batch inserts', function(done) {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)");
var inserted = 0;
var batchSize = 5;

for (var i = 300; i < 300 + batchSize; i++) {
(function(id) {
stmt.run(id, 'batch' + id, function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(" + id + ",'batch" + id + "')");
inserted++;
if (inserted === batchSize) {
stmt.finalize(done);
}
});
})(i);
}
});

it('should work with complex queries with many parameters', function(done) {
var stmt = db.prepare("INSERT INTO complex VALUES(?,?,?,?,?)");
stmt.run(1, 2, 3, 4, 5, function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO complex VALUES(1,2,3,4,5)");
stmt.finalize(done);
});
});

it('should work within serialized transactions', function(done) {
db.serialize(function() {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)");
stmt.run(500, "transaction", function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(500,'transaction')");
stmt.finalize(done);
});
});
});

it('should be accessible outside callbacks after operations complete', function(done) {
var stmt = db.prepare("INSERT INTO foo VALUES(?,?)");

stmt.run(600, "outside", function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "INSERT INTO foo VALUES(600,'outside')");
stmt.finalize(done);
});
});

it('demonstrates the async queue behavior', function(done) {
var stmt = db.prepare("SELECT * FROM foo WHERE id = ?");

stmt.bind(1);

// worker thread still hasn't completed, so it isn't available
assert.equal(stmt.expandedSql, undefined);

stmt.bind(1, function(err) {
if (err) throw err;
assert.equal(stmt.expandedSql, "SELECT * FROM foo WHERE id = 1");
stmt.finalize(done);
});
});
});