diff --git a/lib/sqlite3.d.ts b/lib/sqlite3.d.ts index b27b0cf51..4797c57b8 100644 --- a/lib/sqlite3.d.ts +++ b/lib/sqlite3.d.ts @@ -76,7 +76,7 @@ export class Statement extends events.EventEmitter { finalize(callback?: (err: Error) => void): Database; - run(callback?: (err: Error | null) => void): this; + run(callback?: (this: RunResult, err: Error | null) => void): this; run(params: any, callback?: (this: RunResult, err: Error | null) => void): this; run(...params: any[]): this; @@ -91,6 +91,8 @@ export class Statement extends events.EventEmitter { each(callback?: (err: Error | null, row: any) => void, complete?: (err: Error | null, count: number) => void): this; each(params: any, callback?: (this: RunResult, err: Error | null, row: any) => void, complete?: (err: Error | null, count: number) => void): this; each(...params: any[]): this; + + readonly expandedSql: string; } export class Database extends events.EventEmitter { diff --git a/src/macros.h b/src/macros.h index 344642d9d..55af6ca8b 100644 --- a/src/macros.h +++ b/src/macros.h @@ -165,6 +165,13 @@ inline bool OtherIsInt(Napi::Number source) { } \ sqlite3_mutex* name = sqlite3_db_mutex(stmt->db->_handle); +#define STATEMENT_EXPAND_SQL(stmt) \ + if (stmt->_handle != NULL) { \ + Napi::Env env = stmt->Env(); \ + Napi::Value expanded_sql = Napi::String::New(env, sqlite3_expanded_sql(stmt->_handle)); \ + (stmt->Value()).Set(Napi::String::New(env, "expandedSql"), expanded_sql); \ + } + #define STATEMENT_END() \ assert(stmt->locked); \ assert(stmt->db->pending); \ @@ -210,6 +217,6 @@ inline bool OtherIsInt(Napi::Number source) { case SQLITE_BLOB: delete (Values::Blob*)(field); break; \ case SQLITE_NULL: delete (Values::Null*)(field); break; \ } \ - } + } #endif diff --git a/src/statement.cc b/src/statement.cc index f1b835ba2..0f3a3bcaa 100644 --- a/src/statement.cc +++ b/src/statement.cc @@ -161,6 +161,8 @@ void Statement::Work_AfterPrepare(napi_env e, napi_status status, void* data) { Napi::Env env = stmt->Env(); Napi::HandleScope scope(env); + STATEMENT_EXPAND_SQL(stmt); + if (stmt->status != SQLITE_OK) { Error(baton.get()); stmt->Finalize_(); @@ -170,6 +172,7 @@ void Statement::Work_AfterPrepare(napi_env e, napi_status status, void* data) { if (!baton->callback.IsEmpty() && baton->callback.Value().IsFunction()) { Napi::Function cb = baton->callback.Value(); Napi::Value argv[] = { env.Null() }; + TRY_CATCH_CALL(stmt->Value(), cb, 1, argv); } } @@ -359,6 +362,7 @@ void Statement::Work_Bind(napi_env e, void* data) { STATEMENT_MUTEX(mtx); sqlite3_mutex_enter(mtx); stmt->Bind(baton->parameters); + sqlite3_mutex_leave(mtx); } @@ -369,6 +373,8 @@ void Statement::Work_AfterBind(napi_env e, napi_status status, void* data) { Napi::Env env = stmt->Env(); Napi::HandleScope scope(env); + STATEMENT_EXPAND_SQL(stmt); + if (stmt->status != SQLITE_OK) { Error(baton.get()); } @@ -436,6 +442,8 @@ void Statement::Work_AfterGet(napi_env e, napi_status status, void* data) { Napi::Env env = stmt->Env(); Napi::HandleScope scope(env); + STATEMENT_EXPAND_SQL(stmt); + if (stmt->status != SQLITE_ROW && stmt->status != SQLITE_DONE) { Error(baton.get()); } @@ -510,6 +518,8 @@ void Statement::Work_AfterRun(napi_env e, napi_status status, void* data) { Napi::Env env = stmt->Env(); Napi::HandleScope scope(env); + STATEMENT_EXPAND_SQL(stmt); + if (stmt->status != SQLITE_ROW && stmt->status != SQLITE_DONE) { Error(baton.get()); } @@ -518,8 +528,7 @@ void Statement::Work_AfterRun(napi_env e, napi_status status, void* data) { Napi::Function cb = baton->callback.Value(); if (IS_FUNCTION(cb)) { (stmt->Value()).Set(Napi::String::New(env, "lastID"), Napi::Number::New(env, baton->inserted_id)); - (stmt->Value()).Set( Napi::String::New(env, "changes"), Napi::Number::New(env, baton->changes)); - + (stmt->Value()).Set(Napi::String::New(env, "changes"), Napi::Number::New(env, baton->changes)); Napi::Value argv[] = { env.Null() }; TRY_CATCH_CALL(stmt->Value(), cb, 1, argv); } @@ -580,6 +589,8 @@ void Statement::Work_AfterAll(napi_env e, napi_status status, void* data) { Napi::Env env = stmt->Env(); Napi::HandleScope scope(env); + STATEMENT_EXPAND_SQL(stmt); + if (stmt->status != SQLITE_DONE) { Error(baton.get()); } @@ -704,6 +715,8 @@ void Statement::AsyncEach(uv_async_t* handle) { Napi::Env env = async->stmt->Env(); Napi::HandleScope scope(env); + STATEMENT_EXPAND_SQL(async->stmt); + while (true) { // Get the contents out of the data cache for us to process in the JS callback. Rows rows; @@ -752,10 +765,11 @@ void Statement::Work_AfterEach(napi_env e, napi_status status, void* data) { Napi::Env env = stmt->Env(); Napi::HandleScope scope(env); + STATEMENT_EXPAND_SQL(stmt); + if (stmt->status != SQLITE_DONE) { Error(baton.get()); } - STATEMENT_END(); } diff --git a/src/statement.h b/src/statement.h index dec0015d1..2b8ae27e2 100644 --- a/src/statement.h +++ b/src/statement.h @@ -208,7 +208,7 @@ class Statement : public Napi::ObjectWrap { WORK_DEFINITION(Reset); Napi::Value Finalize_(const Napi::CallbackInfo& info); - + protected: static void Work_BeginPrepare(Database::Baton* baton); static void Work_Prepare(napi_env env, void* data); @@ -223,7 +223,7 @@ class Statement : public Napi::ObjectWrap { template inline Values::Field* BindParameter(const Napi::Value source, T pos); template T* Bind(const Napi::CallbackInfo& info, int start = 0, int end = -1); bool Bind(const Parameters ¶meters); - + static void GetRow(Row* row, sqlite3_stmt* stmt); static Napi::Value RowToJS(Napi::Env env, Row* row); void Schedule(Work_Callback callback, Baton* baton); diff --git a/test/expandedSql.test.js b/test/expandedSql.test.js new file mode 100644 index 000000000..46ac4adf1 --- /dev/null +++ b/test/expandedSql.test.js @@ -0,0 +1,142 @@ +var sqlite3 = require('../lib/sqlite3'); +var assert = require('assert'); + +describe('expandedSql', function() { + var db; + before(function(done) { + db = new sqlite3.Database(':memory:'); + db.run("CREATE TABLE foo (id INT PRIMARY KEY, count INT)", done); + }); + after(function(done) { + db.wait(done); + }); + + it('should expand the sql with bindings within callbacks for Statement', function() { + db.serialize(() => { + /** test Statement methods */ + let stmt = db.prepare("INSERT INTO foo VALUES(?,?)", function() { + assert.equal(this.expandedSql, "INSERT INTO foo VALUES(NULL,NULL)"); + return this; + }); + /** bind */ + stmt.bind(3,4, function() { + assert.equal(this.expandedSql, "INSERT INTO foo VALUES(3,4)"); + }); + stmt.bind(1,2, function() { + assert.equal(this.expandedSql, "INSERT INTO foo VALUES(1,2)"); + }); + /** reset */ + stmt.reset(function() { + // currently it is being retained + assert.equal(this.expandedSql, "INSERT INTO foo VALUES(1,2)"); + }); + + // only a utility for callbacks + assert.equal(stmt.expandedSql, undefined); + + stmt = db.prepare("INSERT INTO foo VALUES(?,?)", function() { + assert.equal(this.expandedSql, "INSERT INTO foo VALUES(NULL,NULL)"); + return this; + }); + /** run */ + stmt.run([2,2], function() { + assert.equal(this.expandedSql, "INSERT INTO foo VALUES(2,2)"); + }); + stmt.run(4,5, function() { + assert.equal(this.expandedSql, "INSERT INTO foo VALUES(4,5)"); + }); + stmt.finalize(); + + /** get */ + stmt = db.prepare("select * from foo where id = $id;"); + stmt.get(function() { + assert.equal(this.expandedSql, "select * from foo where id = NULL;"); + }); + stmt.get({ $id: 1 }, function() { + assert.equal(this.expandedSql, "select * from foo where id = 1;"); + }); + stmt.get([2], function() { + assert.equal(this.expandedSql, "select * from foo where id = 2;"); + }); + + /** all */ + stmt.all(1, function() { + assert.equal(this.expandedSql, "select * from foo where id = 1;"); + }); + stmt.all({ $id: 2 }, function() { + assert.equal(this.expandedSql, "select * from foo where id = 2;"); + }); + stmt.all([3], function() { + assert.equal(this.expandedSql, "select * from foo where id = 3;"); + }); + + /** each - testing within each callback */ + stmt.each(1, function() { + assert.equal(this.expandedSql, "select * from foo where id = 1;"); + }, function() { + assert.equal(this.expandedSql, "select * from foo where id = 1;"); + }); + stmt.each({ $id: 2 }, function() { + assert.equal(this.expandedSql, "select * from foo where id = 2;"); + }, function() { + assert.equal(this.expandedSql, "select * from foo where id = 2;"); + }); + stmt.each([3], function() { + assert.equal(this.expandedSql, "select * from foo where id = 3;"); + }, function() { + assert.equal(this.expandedSql, "select * from foo where id = 3;"); + }); + + stmt.finalize(); + }); + }); + + it('should expand the sql with bindings within callbacks for Database', function() { + db.serialize(() => { + db.prepare("INSERT INTO foo VALUES(?,?)", function() { + assert.equal(this.expandedSql, "INSERT INTO foo VALUES(NULL,NULL)"); + }); + + /** run */ + db.run("INSERT INTO foo VALUES(?,?)", 10, 11, function () { + assert.equal(this.expandedSql, "INSERT INTO foo VALUES(10,11)"); + }); + + /** get */ + db.get("select * from foo;", function() { + assert.equal(this.expandedSql, "select * from foo;"); + }); + db.get("select * from bar;", function() { + // since this is a sqlite error there is nothing to expand afterwards + assert.equal(this.expandedSql, undefined); + }); + + /** all */ + db.all("SELECT * FROM foo;", function(err, rows) { + assert.equal(this.expandedSql, "SELECT * FROM foo;"); + assert.deepStrictEqual(rows.sort((a,b) => a.id - b.id), [{ id: 2, count: 2 }, { id: 4, count: 5}, { id: 10, count: 11 }]); + }); + db.all("SELECT id, count FROM foo WHERE id = ?", 1, function(err, rows) { + assert.equal(this.expandedSql, "SELECT id, count FROM foo WHERE id = 1"); + assert.deepStrictEqual(rows, []); + }); + + /** each */ + db.each("select * from foo;", function() { + assert.equal(this.expandedSql, "select * from foo;"); + }, function() { + assert.equal(this.expandedSql, "select * from foo;"); + }); + db.each("select * from bar;", function() { + // since this is a sqlite error there is nothing to expand afterwards + assert.equal(this.expandedSql, undefined); + }); + + /** exec */ + db.exec("select * from foo where id = ?", function() { + // not applicable here so it is never set + assert.equal(this.expandedSql, undefined); + }); + }); + }); +});