diff --git a/src/statement.cc b/src/statement.cc index 6f3a8da19..62ecfbf96 100644 --- a/src/statement.cc +++ b/src/statement.cc @@ -23,6 +23,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("readOnly", &Statement::ReadOnlyGetter, nullptr), }); exports.Set("Statement", t); @@ -151,6 +152,9 @@ void Statement::Work_Prepare(napi_env e, void* data) { stmt->message = std::string(sqlite3_errmsg(baton->db->_handle)); stmt->_handle = NULL; } + // sqlite3_stmt_readonly is an instant operation (just reading out + // something already computed). And it can handle null. + stmt->readOnly = bool(sqlite3_stmt_readonly(stmt->_handle)); sqlite3_mutex_leave(mtx); } @@ -1026,6 +1030,11 @@ void Statement::Finalize_() { db->Unref(); } +Napi::Value Statement::ReadOnlyGetter(const Napi::CallbackInfo& info) { + Statement *statement = this; + return Napi::Boolean::New(this->Env(), statement->readOnly); +} + void Statement::CleanQueue() { Napi::Env env = this->Env(); Napi::HandleScope scope(env); diff --git a/src/statement.h b/src/statement.h index e9335e1d6..a30d0ffaf 100644 --- a/src/statement.h +++ b/src/statement.h @@ -216,6 +216,7 @@ class Statement : public Napi::ObjectWrap { WORK_DEFINITION(AllMarshal); WORK_DEFINITION(Each); WORK_DEFINITION(Reset); + Napi::Value ReadOnlyGetter(const Napi::CallbackInfo& info); Napi::Value Finalize_(const Napi::CallbackInfo& info); @@ -251,6 +252,7 @@ class Statement : public Napi::ObjectWrap { bool prepared; bool locked; bool finalized; + bool readOnly; std::queue queue; }; diff --git a/test/readOnly.test.js b/test/readOnly.test.js new file mode 100644 index 000000000..09300e520 --- /dev/null +++ b/test/readOnly.test.js @@ -0,0 +1,50 @@ +const sqlite3 = require('..'); +const assert = require('assert'); + +const testCases = [ + { sql: 'DELETE FROM foo', readOnly: false }, + { sql: 'SELECT * FROM foo', readOnly: true }, + { sql: 'UPDATE foo SET x = 10', readOnly: false }, + { sql: 'PRAGMA application_id', readOnly: true }, + { sql: 'PRAGMA application_id = 1', readOnly: false }, + { sql: 'CREATE TABLE bar(x, y)', readOnly: false }, + { sql: 'CREATE TABLE IF NOT EXISTS foo(x, y)', readOnly: false }, + + // Prepare only uses the first statement if there are multiple + // (the rest are returned in a tail string by the underlying + // sqlite3 function). The readOnly flag is for that first + // statement. Anything after the first statement could be + // readOnly or not readOnly, it just won't be running. + { sql: 'SELECT * FROM foo; DELETE FROM foo', readOnly: true }, + + // Statements that only affect the connection don't count. + // This is too bad, wish there were a way to detect such things. + { sql: 'PRAGMA query_only = false', readOnly: true }, + { sql: 'ATTACH DATABASE \'test\' AS test', readOnly: true }, +]; + +describe('readOnly', function() { + let db; + + before(function(done) { + db = new sqlite3.Database(':memory:', function(err) { + if (err) throw err; + db.run('CREATE TABLE foo(x, y)', function(err) { + if (err) throw err; + done(); + }); + }); + }); + + for (let i = 0; i < testCases.length; i++) { + const testCase = testCases[i]; + it("reports " + testCase.sql + " as " + + (testCase.readOnly ? "not " : "") + "readOnly", function(done) { + const stmt = db.prepare(testCase.sql, function(err) { + if (err) throw err; + assert.equal(stmt.readOnly, testCase.readOnly); + stmt.finalize(done); + }); + }); + } +});