diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 1d94363bf08907..4821db5303501a 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -91,8 +91,11 @@ DatabaseSync::DatabaseSync(Environment* env, } DatabaseSync::~DatabaseSync() { - sqlite3_close_v2(connection_); - connection_ = nullptr; + if (IsOpen()) { + FinalizeStatements(); + sqlite3_close_v2(connection_); + connection_ = nullptr; + } } void DatabaseSync::MemoryInfo(MemoryTracker* tracker) const { @@ -100,7 +103,7 @@ void DatabaseSync::MemoryInfo(MemoryTracker* tracker) const { } bool DatabaseSync::Open() { - if (connection_ != nullptr) { + if (IsOpen()) { node::THROW_ERR_INVALID_STATE(env(), "database is already open"); return false; } @@ -112,6 +115,29 @@ bool DatabaseSync::Open() { return true; } +void DatabaseSync::FinalizeStatements() { + for (auto stmt : statements_) { + stmt->Finalize(); + } + + statements_.clear(); +} + +void DatabaseSync::UntrackStatement(StatementSync* statement) { + auto it = statements_.find(statement); + if (it != statements_.end()) { + statements_.erase(it); + } +} + +inline bool DatabaseSync::IsOpen() { + return connection_ != nullptr; +} + +inline sqlite3* DatabaseSync::Connection() { + return connection_; +} + void DatabaseSync::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -164,8 +190,8 @@ void DatabaseSync::Close(const FunctionCallbackInfo& args) { DatabaseSync* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); - THROW_AND_RETURN_ON_BAD_STATE( - env, db->connection_ == nullptr, "database is not open"); + THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); + db->FinalizeStatements(); int r = sqlite3_close_v2(db->connection_); CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void()); db->connection_ = nullptr; @@ -175,8 +201,7 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo& args) { DatabaseSync* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); - THROW_AND_RETURN_ON_BAD_STATE( - env, db->connection_ == nullptr, "database is not open"); + THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); if (!args[0]->IsString()) { node::THROW_ERR_INVALID_ARG_TYPE(env->isolate(), @@ -188,8 +213,8 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo& args) { sqlite3_stmt* s = nullptr; int r = sqlite3_prepare_v2(db->connection_, *sql, -1, &s, 0); CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void()); - BaseObjectPtr stmt = - StatementSync::Create(env, db->connection_, s); + BaseObjectPtr stmt = StatementSync::Create(env, db, s); + db->statements_.insert(stmt.get()); args.GetReturnValue().Set(stmt->object()); } @@ -197,8 +222,7 @@ void DatabaseSync::Exec(const FunctionCallbackInfo& args) { DatabaseSync* db; ASSIGN_OR_RETURN_UNWRAP(&db, args.This()); Environment* env = Environment::GetCurrent(args); - THROW_AND_RETURN_ON_BAD_STATE( - env, db->connection_ == nullptr, "database is not open"); + THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open"); if (!args[0]->IsString()) { node::THROW_ERR_INVALID_ARG_TYPE(env->isolate(), @@ -213,7 +237,7 @@ void DatabaseSync::Exec(const FunctionCallbackInfo& args) { StatementSync::StatementSync(Environment* env, Local object, - sqlite3* db, + DatabaseSync* db, sqlite3_stmt* stmt) : BaseObject(env, object) { MakeWeak(); @@ -227,13 +251,25 @@ StatementSync::StatementSync(Environment* env, } StatementSync::~StatementSync() { + if (!IsFinalized()) { + db_->UntrackStatement(this); + Finalize(); + } +} + +void StatementSync::Finalize() { sqlite3_finalize(statement_); statement_ = nullptr; } +inline bool StatementSync::IsFinalized() { + return statement_ == nullptr; +} + bool StatementSync::BindParams(const FunctionCallbackInfo& args) { int r = sqlite3_clear_bindings(statement_); - CHECK_ERROR_OR_THROW(env()->isolate(), db_, r, SQLITE_OK, false); + CHECK_ERROR_OR_THROW( + env()->isolate(), db_->Connection(), r, SQLITE_OK, false); int anon_idx = 1; int anon_start = 0; @@ -364,7 +400,8 @@ bool StatementSync::BindValue(const Local& value, const int index) { return false; } - CHECK_ERROR_OR_THROW(env()->isolate(), db_, r, SQLITE_OK, false); + CHECK_ERROR_OR_THROW( + env()->isolate(), db_->Connection(), r, SQLITE_OK, false); return true; } @@ -435,8 +472,11 @@ void StatementSync::All(const FunctionCallbackInfo& args) { StatementSync* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsFinalized(), "statement has been finalized"); int r = sqlite3_reset(stmt->statement_); - CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_, r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW( + env->isolate(), stmt->db_->Connection(), r, SQLITE_OK, void()); if (!stmt->BindParams(args)) { return; @@ -462,7 +502,8 @@ void StatementSync::All(const FunctionCallbackInfo& args) { rows.emplace_back(row); } - CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_, r, SQLITE_DONE, void()); + CHECK_ERROR_OR_THROW( + env->isolate(), stmt->db_->Connection(), r, SQLITE_DONE, void()); args.GetReturnValue().Set( Array::New(env->isolate(), rows.data(), rows.size())); } @@ -471,8 +512,11 @@ void StatementSync::Get(const FunctionCallbackInfo& args) { StatementSync* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsFinalized(), "statement has been finalized"); int r = sqlite3_reset(stmt->statement_); - CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_, r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW( + env->isolate(), stmt->db_->Connection(), r, SQLITE_OK, void()); if (!stmt->BindParams(args)) { return; @@ -482,7 +526,7 @@ void StatementSync::Get(const FunctionCallbackInfo& args) { r = sqlite3_step(stmt->statement_); if (r == SQLITE_DONE) return; if (r != SQLITE_ROW) { - THROW_ERR_SQLITE_ERROR(env->isolate(), stmt->db_); + THROW_ERR_SQLITE_ERROR(env->isolate(), stmt->db_->Connection()); return; } @@ -511,8 +555,11 @@ void StatementSync::Run(const FunctionCallbackInfo& args) { StatementSync* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsFinalized(), "statement has been finalized"); int r = sqlite3_reset(stmt->statement_); - CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_, r, SQLITE_OK, void()); + CHECK_ERROR_OR_THROW( + env->isolate(), stmt->db_->Connection(), r, SQLITE_OK, void()); if (!stmt->BindParams(args)) { return; @@ -521,7 +568,7 @@ void StatementSync::Run(const FunctionCallbackInfo& args) { auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); r = sqlite3_step(stmt->statement_); if (r != SQLITE_ROW && r != SQLITE_DONE) { - THROW_ERR_SQLITE_ERROR(env->isolate(), stmt->db_); + THROW_ERR_SQLITE_ERROR(env->isolate(), stmt->db_->Connection()); return; } @@ -530,8 +577,9 @@ void StatementSync::Run(const FunctionCallbackInfo& args) { FIXED_ONE_BYTE_STRING(env->isolate(), "lastInsertRowid"); Local changes_string = FIXED_ONE_BYTE_STRING(env->isolate(), "changes"); - sqlite3_int64 last_insert_rowid = sqlite3_last_insert_rowid(stmt->db_); - sqlite3_int64 changes = sqlite3_changes64(stmt->db_); + sqlite3_int64 last_insert_rowid = + sqlite3_last_insert_rowid(stmt->db_->Connection()); + sqlite3_int64 changes = sqlite3_changes64(stmt->db_->Connection()); Local last_insert_rowid_val; Local changes_val; @@ -557,6 +605,8 @@ void StatementSync::SourceSQL(const FunctionCallbackInfo& args) { StatementSync* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsFinalized(), "statement has been finalized"); Local sql; if (!String::NewFromUtf8(env->isolate(), sqlite3_sql(stmt->statement_)) .ToLocal(&sql)) { @@ -569,6 +619,8 @@ void StatementSync::ExpandedSQL(const FunctionCallbackInfo& args) { StatementSync* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsFinalized(), "statement has been finalized"); char* expanded = sqlite3_expanded_sql(stmt->statement_); auto maybe_expanded = String::NewFromUtf8(env->isolate(), expanded); sqlite3_free(expanded); @@ -584,6 +636,8 @@ void StatementSync::SetAllowBareNamedParameters( StatementSync* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsFinalized(), "statement has been finalized"); if (!args[0]->IsBoolean()) { node::THROW_ERR_INVALID_ARG_TYPE( @@ -599,6 +653,8 @@ void StatementSync::SetReadBigInts(const FunctionCallbackInfo& args) { StatementSync* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); Environment* env = Environment::GetCurrent(args); + THROW_AND_RETURN_ON_BAD_STATE( + env, stmt->IsFinalized(), "statement has been finalized"); if (!args[0]->IsBoolean()) { node::THROW_ERR_INVALID_ARG_TYPE( @@ -640,7 +696,7 @@ Local StatementSync::GetConstructorTemplate( } BaseObjectPtr StatementSync::Create(Environment* env, - sqlite3* db, + DatabaseSync* db, sqlite3_stmt* stmt) { Local obj; if (!GetConstructorTemplate(env) diff --git a/src/node_sqlite.h b/src/node_sqlite.h index 56b937a719679b..ca6e8c7f23cf40 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -9,10 +9,13 @@ #include "util.h" #include +#include namespace node { namespace sqlite { +class StatementSync; + class DatabaseSync : public BaseObject { public: DatabaseSync(Environment* env, @@ -25,6 +28,10 @@ class DatabaseSync : public BaseObject { static void Close(const v8::FunctionCallbackInfo& args); static void Prepare(const v8::FunctionCallbackInfo& args); static void Exec(const v8::FunctionCallbackInfo& args); + void FinalizeStatements(); + void UntrackStatement(StatementSync* statement); + bool IsOpen(); + sqlite3* Connection(); SET_MEMORY_INFO_NAME(DatabaseSync) SET_SELF_SIZE(DatabaseSync) @@ -35,19 +42,20 @@ class DatabaseSync : public BaseObject { ~DatabaseSync() override; std::string location_; sqlite3* connection_; + std::unordered_set statements_; }; class StatementSync : public BaseObject { public: StatementSync(Environment* env, v8::Local object, - sqlite3* db, + DatabaseSync* db, sqlite3_stmt* stmt); void MemoryInfo(MemoryTracker* tracker) const override; static v8::Local GetConstructorTemplate( Environment* env); static BaseObjectPtr Create(Environment* env, - sqlite3* db, + DatabaseSync* db, sqlite3_stmt* stmt); static void All(const v8::FunctionCallbackInfo& args); static void Get(const v8::FunctionCallbackInfo& args); @@ -57,13 +65,15 @@ class StatementSync : public BaseObject { static void SetAllowBareNamedParameters( const v8::FunctionCallbackInfo& args); static void SetReadBigInts(const v8::FunctionCallbackInfo& args); + void Finalize(); + bool IsFinalized(); SET_MEMORY_INFO_NAME(StatementSync) SET_SELF_SIZE(StatementSync) private: ~StatementSync() override; - sqlite3* db_; + DatabaseSync* db_; sqlite3_stmt* statement_; bool use_big_ints_; bool allow_bare_named_params_; diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status index d18bfb72914a2d..50ba57dcbdf3af 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status @@ -19,9 +19,6 @@ test-fs-read-stream-concurrent-reads: PASS, FLAKY # https://github.com/nodejs/node/issues/52630 test-error-serdes: PASS, FLAKY -# https://github.com/nodejs/node/issues/54006 -test-sqlite: PASS, FLAKY - [$system==win32] # Windows on ARM diff --git a/test/parallel/test-sqlite-data-types.js b/test/parallel/test-sqlite-data-types.js new file mode 100644 index 00000000000000..582d5bd611edf4 --- /dev/null +++ b/test/parallel/test-sqlite-data-types.js @@ -0,0 +1,157 @@ +// Flags: --experimental-sqlite +'use strict'; +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { join } = require('node:path'); +const { DatabaseSync } = require('node:sqlite'); +const { suite, test } = require('node:test'); +let cnt = 0; + +tmpdir.refresh(); + +function nextDb() { + return join(tmpdir.path, `database-${cnt++}.db`); +} + +suite('data binding and mapping', () => { + test('supported data types', (t) => { + const u8a = new TextEncoder().encode('a☃b☃c'); + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec(` + CREATE TABLE types( + key INTEGER PRIMARY KEY, + int INTEGER, + double REAL, + text TEXT, + buf BLOB + ) STRICT; + `); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO types (key, int, double, text, buf) ' + + 'VALUES (?, ?, ?, ?, ?)'); + t.assert.deepStrictEqual( + stmt.run(1, 42, 3.14159, 'foo', u8a), + { changes: 1, lastInsertRowid: 1 }, + ); + t.assert.deepStrictEqual( + stmt.run(2, null, null, null, null), + { changes: 1, lastInsertRowid: 2 } + ); + t.assert.deepStrictEqual( + stmt.run(3, Number(8), Number(2.718), String('bar'), Buffer.from('x☃y☃')), + { changes: 1, lastInsertRowid: 3 }, + ); + t.assert.deepStrictEqual( + stmt.run(4, 99n, 0xf, '', new Uint8Array()), + { changes: 1, lastInsertRowid: 4 }, + ); + + const query = db.prepare('SELECT * FROM types WHERE key = ?'); + t.assert.deepStrictEqual(query.get(1), { + key: 1, + int: 42, + double: 3.14159, + text: 'foo', + buf: u8a, + }); + t.assert.deepStrictEqual(query.get(2), { + key: 2, + int: null, + double: null, + text: null, + buf: null, + }); + t.assert.deepStrictEqual(query.get(3), { + key: 3, + int: 8, + double: 2.718, + text: 'bar', + buf: new TextEncoder().encode('x☃y☃'), + }); + t.assert.deepStrictEqual(query.get(4), { + key: 4, + int: 99, + double: 0xf, + text: '', + buf: new Uint8Array(), + }); + }); + + test('unsupported data types', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + + [ + undefined, + () => {}, + Symbol(), + /foo/, + Promise.resolve(), + new Map(), + new Set(), + ].forEach((val) => { + t.assert.throws(() => { + db.prepare('INSERT INTO types (key, val) VALUES (?, ?)').run(1, val); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /Provided value cannot be bound to SQLite parameter 2/, + }); + }); + + t.assert.throws(() => { + const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)'); + stmt.run({ $k: 1, $v: () => {} }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /Provided value cannot be bound to SQLite parameter 2/, + }); + }); + + test('throws when binding a BigInt that is too large', (t) => { + const max = 9223372036854775807n; // Largest 64-bit signed integer value. + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO types (key, val) VALUES (?, ?)'); + t.assert.deepStrictEqual( + stmt.run(1, max), + { changes: 1, lastInsertRowid: 1 }, + ); + t.assert.throws(() => { + stmt.run(1, max + 1n); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /BigInt value is too large to bind/, + }); + }); + + test('statements are unbound on each call', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)'); + t.assert.deepStrictEqual( + stmt.run(1, 5), + { changes: 1, lastInsertRowid: 1 }, + ); + t.assert.deepStrictEqual( + stmt.run(), + { changes: 1, lastInsertRowid: 2 }, + ); + t.assert.deepStrictEqual( + db.prepare('SELECT * FROM data ORDER BY key').all(), + [{ key: 1, val: 5 }, { key: 2, val: null }], + ); + }); +}); diff --git a/test/parallel/test-sqlite-database-sync.js b/test/parallel/test-sqlite-database-sync.js new file mode 100644 index 00000000000000..1bc409926ae446 --- /dev/null +++ b/test/parallel/test-sqlite-database-sync.js @@ -0,0 +1,185 @@ +// Flags: --experimental-sqlite +'use strict'; +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { existsSync } = require('node:fs'); +const { join } = require('node:path'); +const { DatabaseSync, StatementSync } = require('node:sqlite'); +const { suite, test } = require('node:test'); +let cnt = 0; + +tmpdir.refresh(); + +function nextDb() { + return join(tmpdir.path, `database-${cnt++}.db`); +} + +suite('DatabaseSync() constructor', () => { + test('throws if called without new', (t) => { + t.assert.throws(() => { + DatabaseSync(); + }, { + code: 'ERR_CONSTRUCT_CALL_REQUIRED', + message: /Cannot call constructor without `new`/, + }); + }); + + test('throws if database path is not a string', (t) => { + t.assert.throws(() => { + new DatabaseSync(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "path" argument must be a string/, + }); + }); + + test('throws if options is provided but is not an object', (t) => { + t.assert.throws(() => { + new DatabaseSync('foo', null); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options" argument must be an object/, + }); + }); + + test('throws if options.open is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new DatabaseSync('foo', { open: 5 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.open" argument must be a boolean/, + }); + }); +}); + +suite('DatabaseSync.prototype.open()', () => { + test('opens a database connection', (t) => { + const dbPath = nextDb(); + const db = new DatabaseSync(dbPath, { open: false }); + t.after(() => { db.close(); }); + + t.assert.strictEqual(existsSync(dbPath), false); + t.assert.strictEqual(db.open(), undefined); + t.assert.strictEqual(existsSync(dbPath), true); + }); + + test('throws if database is already open', (t) => { + const db = new DatabaseSync(nextDb(), { open: false }); + t.after(() => { db.close(); }); + + db.open(); + t.assert.throws(() => { + db.open(); + }, { + code: 'ERR_INVALID_STATE', + message: /database is already open/, + }); + }); +}); + +suite('DatabaseSync.prototype.close()', () => { + test('closes an open database connection', (t) => { + const db = new DatabaseSync(nextDb()); + + t.assert.strictEqual(db.close(), undefined); + }); + + test('throws if database is not open', (t) => { + const db = new DatabaseSync(nextDb(), { open: false }); + + t.assert.throws(() => { + db.close(); + }, { + code: 'ERR_INVALID_STATE', + message: /database is not open/, + }); + }); +}); + +suite('DatabaseSync.prototype.prepare()', () => { + test('returns a prepared statement', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const stmt = db.prepare('CREATE TABLE webstorage(key TEXT)'); + t.assert.ok(stmt instanceof StatementSync); + }); + + test('throws if database is not open', (t) => { + const db = new DatabaseSync(nextDb(), { open: false }); + + t.assert.throws(() => { + db.prepare(); + }, { + code: 'ERR_INVALID_STATE', + message: /database is not open/, + }); + }); + + test('throws if sql is not a string', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + + t.assert.throws(() => { + db.prepare(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "sql" argument must be a string/, + }); + }); +}); + +suite('DatabaseSync.prototype.exec()', () => { + test('executes SQL', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const result = db.exec(` + CREATE TABLE data( + key INTEGER PRIMARY KEY, + val INTEGER + ) STRICT; + INSERT INTO data (key, val) VALUES (1, 2); + INSERT INTO data (key, val) VALUES (8, 9); + `); + t.assert.strictEqual(result, undefined); + const stmt = db.prepare('SELECT * FROM data ORDER BY key'); + t.assert.deepStrictEqual(stmt.all(), [ + { key: 1, val: 2 }, + { key: 8, val: 9 }, + ]); + }); + + test('reports errors from SQLite', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + + t.assert.throws(() => { + db.exec('CREATE TABLEEEE'); + }, { + code: 'ERR_SQLITE_ERROR', + message: /syntax error/, + }); + }); + + test('throws if database is not open', (t) => { + const db = new DatabaseSync(nextDb(), { open: false }); + + t.assert.throws(() => { + db.exec(); + }, { + code: 'ERR_INVALID_STATE', + message: /database is not open/, + }); + }); + + test('throws if sql is not a string', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + + t.assert.throws(() => { + db.exec(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "sql" argument must be a string/, + }); + }); +}); diff --git a/test/parallel/test-sqlite-named-parameters.js b/test/parallel/test-sqlite-named-parameters.js new file mode 100644 index 00000000000000..27857111953d27 --- /dev/null +++ b/test/parallel/test-sqlite-named-parameters.js @@ -0,0 +1,80 @@ +// Flags: --experimental-sqlite +'use strict'; +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { join } = require('node:path'); +const { DatabaseSync } = require('node:sqlite'); +const { suite, test } = require('node:test'); +let cnt = 0; + +tmpdir.refresh(); + +function nextDb() { + return join(tmpdir.path, `database-${cnt++}.db`); +} + +suite('named parameters', () => { + test('throws on unknown named parameters', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + + t.assert.throws(() => { + const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)'); + stmt.run({ $k: 1, $unknown: 1 }); + }, { + code: 'ERR_INVALID_STATE', + message: /Unknown named parameter '\$unknown'/, + }); + }); + + test('bare named parameters are supported', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)'); + stmt.run({ k: 1, v: 9 }); + t.assert.deepStrictEqual( + db.prepare('SELECT * FROM data').get(), + { key: 1, val: 9 }, + ); + }); + + test('duplicate bare named parameters are supported', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $k)'); + stmt.run({ k: 1 }); + t.assert.deepStrictEqual( + db.prepare('SELECT * FROM data').get(), + { key: 1, val: 1 }, + ); + }); + + test('bare named parameters throw on ambiguous names', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, @k)'); + t.assert.throws(() => { + stmt.run({ k: 1 }); + }, { + code: 'ERR_INVALID_STATE', + message: 'Cannot create bare named parameter \'k\' because of ' + + 'conflicting names \'$k\' and \'@k\'.', + }); + }); +}); diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js new file mode 100644 index 00000000000000..7a4069678af966 --- /dev/null +++ b/test/parallel/test-sqlite-statement-sync.js @@ -0,0 +1,273 @@ +// Flags: --experimental-sqlite +'use strict'; +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { join } = require('node:path'); +const { DatabaseSync, StatementSync } = require('node:sqlite'); +const { suite, test } = require('node:test'); +let cnt = 0; + +tmpdir.refresh(); + +function nextDb() { + return join(tmpdir.path, `database-${cnt++}.db`); +} + +suite('StatementSync() constructor', () => { + test('StatementSync cannot be constructed directly', (t) => { + t.assert.throws(() => { + new StatementSync(); + }, { + code: 'ERR_ILLEGAL_CONSTRUCTOR', + message: /Illegal constructor/, + }); + }); +}); + +suite('StatementSync.prototype.get()', () => { + test('executes a query and returns undefined on no results', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + t.assert.strictEqual(stmt.get(), undefined); + stmt = db.prepare('SELECT * FROM storage'); + t.assert.strictEqual(stmt.get(), undefined); + }); + + test('executes a query and returns the first result', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + t.assert.strictEqual(stmt.get(), undefined); + stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)'); + t.assert.strictEqual(stmt.get('key1', 'val1'), undefined); + t.assert.strictEqual(stmt.get('key2', 'val2'), undefined); + stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + t.assert.deepStrictEqual(stmt.get(), { key: 'key1', val: 'val1' }); + }); +}); + +suite('StatementSync.prototype.all()', () => { + test('executes a query and returns an empty array on no results', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + t.assert.deepStrictEqual(stmt.all(), []); + }); + + test('executes a query and returns all results', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); + t.assert.deepStrictEqual(stmt.run(), { changes: 0, lastInsertRowid: 0 }); + stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)'); + t.assert.deepStrictEqual( + stmt.run('key1', 'val1'), + { changes: 1, lastInsertRowid: 1 }, + ); + t.assert.deepStrictEqual( + stmt.run('key2', 'val2'), + { changes: 1, lastInsertRowid: 2 }, + ); + stmt = db.prepare('SELECT * FROM storage ORDER BY key'); + t.assert.deepStrictEqual(stmt.all(), [ + { key: 'key1', val: 'val1' }, + { key: 'key2', val: 'val2' }, + ]); + }); +}); + +suite('StatementSync.prototype.run()', () => { + test('executes a query and returns change metadata', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec(` + CREATE TABLE storage(key TEXT, val TEXT); + INSERT INTO storage (key, val) VALUES ('foo', 'bar'); + `); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('SELECT * FROM storage'); + t.assert.deepStrictEqual(stmt.run(), { changes: 1, lastInsertRowid: 1 }); + }); + + test('SQLite throws when trying to bind too many parameters', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)'); + t.assert.throws(() => { + stmt.run(1, 2, 3); + }, { + code: 'ERR_SQLITE_ERROR', + message: 'column index out of range', + errcode: 25, + errstr: 'column index out of range', + }); + }); + + test('SQLite defaults to NULL for unbound parameters', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)'); + t.assert.throws(() => { + stmt.run(1); + }, { + code: 'ERR_SQLITE_ERROR', + message: 'NOT NULL constraint failed: data.val', + errcode: 1299, + errstr: 'constraint failed', + }); + }); +}); + +suite('StatementSync.prototype.sourceSQL()', () => { + test('returns input SQL', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const sql = 'INSERT INTO types (key, val) VALUES ($k, $v)'; + const stmt = db.prepare(sql); + t.assert.strictEqual(stmt.sourceSQL(), sql); + }); +}); + +suite('StatementSync.prototype.expandedSQL()', () => { + test('returns expanded SQL', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const sql = 'INSERT INTO types (key, val) VALUES ($k, ?)'; + const expanded = 'INSERT INTO types (key, val) VALUES (\'33\', \'42\')'; + const stmt = db.prepare(sql); + t.assert.deepStrictEqual( + stmt.run({ $k: '33' }, '42'), + { changes: 1, lastInsertRowid: 33 }, + ); + t.assert.strictEqual(stmt.expandedSQL(), expanded); + }); +}); + +suite('StatementSync.prototype.setReadBigInts()', () => { + test('BigInts support can be toggled', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec(` + CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT; + INSERT INTO data (key, val) VALUES (1, 42); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT val FROM data'); + t.assert.deepStrictEqual(query.get(), { val: 42 }); + t.assert.strictEqual(query.setReadBigInts(true), undefined); + t.assert.deepStrictEqual(query.get(), { val: 42n }); + t.assert.strictEqual(query.setReadBigInts(false), undefined); + t.assert.deepStrictEqual(query.get(), { val: 42 }); + + const insert = db.prepare('INSERT INTO data (key) VALUES (?)'); + t.assert.deepStrictEqual( + insert.run(10), + { changes: 1, lastInsertRowid: 10 }, + ); + t.assert.strictEqual(insert.setReadBigInts(true), undefined); + t.assert.deepStrictEqual( + insert.run(20), + { changes: 1n, lastInsertRowid: 20n }, + ); + t.assert.strictEqual(insert.setReadBigInts(false), undefined); + t.assert.deepStrictEqual( + insert.run(30), + { changes: 1, lastInsertRowid: 30 }, + ); + }); + + test('throws when input is not a boolean', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)'); + t.assert.throws(() => { + stmt.setReadBigInts(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "readBigInts" argument must be a boolean/, + }); + }); + + test('BigInt is required for reading large integers', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const bad = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`); + t.assert.throws(() => { + bad.get(); + }, { + code: 'ERR_OUT_OF_RANGE', + message: /^The value of column 0 is too large.*: 9007199254740992$/, + }); + const good = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`); + good.setReadBigInts(true); + t.assert.deepStrictEqual(good.get(), { + [`${Number.MAX_SAFE_INTEGER} + 1`]: 2n ** 53n, + }); + }); +}); + +suite('StatementSync.prototype.setAllowBareNamedParameters()', () => { + test('bare named parameter support can be toggled', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)'); + t.assert.deepStrictEqual( + stmt.run({ k: 1, v: 2 }), + { changes: 1, lastInsertRowid: 1 }, + ); + t.assert.strictEqual(stmt.setAllowBareNamedParameters(false), undefined); + t.assert.throws(() => { + stmt.run({ k: 2, v: 4 }); + }, { + code: 'ERR_INVALID_STATE', + message: /Unknown named parameter 'k'/, + }); + t.assert.strictEqual(stmt.setAllowBareNamedParameters(true), undefined); + t.assert.deepStrictEqual( + stmt.run({ k: 3, v: 6 }), + { changes: 1, lastInsertRowid: 3 }, + ); + }); + + test('throws when input is not a boolean', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec( + 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' + ); + t.assert.strictEqual(setup, undefined); + const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)'); + t.assert.throws(() => { + stmt.setAllowBareNamedParameters(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "allowBareNamedParameters" argument must be a boolean/, + }); + }); +}); diff --git a/test/parallel/test-sqlite-transactions.js b/test/parallel/test-sqlite-transactions.js new file mode 100644 index 00000000000000..a37e635541bbf2 --- /dev/null +++ b/test/parallel/test-sqlite-transactions.js @@ -0,0 +1,67 @@ +// Flags: --experimental-sqlite +'use strict'; +require('../common'); +const tmpdir = require('../common/tmpdir'); +const { join } = require('node:path'); +const { DatabaseSync } = require('node:sqlite'); +const { suite, test } = require('node:test'); +let cnt = 0; + +tmpdir.refresh(); + +function nextDb() { + return join(tmpdir.path, `database-${cnt++}.db`); +} + +suite('manual transactions', () => { + test('a transaction is committed', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec(` + CREATE TABLE data( + key INTEGER PRIMARY KEY + ) STRICT; + `); + t.assert.strictEqual(setup, undefined); + t.assert.deepStrictEqual( + db.prepare('BEGIN').run(), + { changes: 0, lastInsertRowid: 0 }, + ); + t.assert.deepStrictEqual( + db.prepare('INSERT INTO data (key) VALUES (100)').run(), + { changes: 1, lastInsertRowid: 100 }, + ); + t.assert.deepStrictEqual( + db.prepare('COMMIT').run(), + { changes: 1, lastInsertRowid: 100 }, + ); + t.assert.deepStrictEqual( + db.prepare('SELECT * FROM data').all(), + [{ key: 100 }], + ); + }); + + test('a transaction is rolled back', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec(` + CREATE TABLE data( + key INTEGER PRIMARY KEY + ) STRICT; + `); + t.assert.strictEqual(setup, undefined); + t.assert.deepStrictEqual( + db.prepare('BEGIN').run(), + { changes: 0, lastInsertRowid: 0 }, + ); + t.assert.deepStrictEqual( + db.prepare('INSERT INTO data (key) VALUES (100)').run(), + { changes: 1, lastInsertRowid: 100 }, + ); + t.assert.deepStrictEqual( + db.prepare('ROLLBACK').run(), + { changes: 1, lastInsertRowid: 100 }, + ); + t.assert.deepStrictEqual(db.prepare('SELECT * FROM data').all(), []); + }); +}); diff --git a/test/parallel/test-sqlite.js b/test/parallel/test-sqlite.js index 3d899063f9c967..8acabb96fceab4 100644 --- a/test/parallel/test-sqlite.js +++ b/test/parallel/test-sqlite.js @@ -2,9 +2,8 @@ 'use strict'; const { spawnPromisified } = require('../common'); const tmpdir = require('../common/tmpdir'); -const { existsSync } = require('node:fs'); const { join } = require('node:path'); -const { DatabaseSync, StatementSync } = require('node:sqlite'); +const { DatabaseSync } = require('node:sqlite'); const { suite, test } = require('node:test'); let cnt = 0; @@ -42,668 +41,9 @@ suite('accessing the node:sqlite module', () => { }); }); -suite('DatabaseSync() constructor', () => { - test('throws if called without new', (t) => { - t.assert.throws(() => { - DatabaseSync(); - }, { - code: 'ERR_CONSTRUCT_CALL_REQUIRED', - message: /Cannot call constructor without `new`/, - }); - }); - - test('throws if database path is not a string', (t) => { - t.assert.throws(() => { - new DatabaseSync(); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /The "path" argument must be a string/, - }); - }); - - test('throws if options is provided but is not an object', (t) => { - t.assert.throws(() => { - new DatabaseSync('foo', null); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /The "options" argument must be an object/, - }); - }); - - test('throws if options.open is provided but is not a boolean', (t) => { - t.assert.throws(() => { - new DatabaseSync('foo', { open: 5 }); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /The "options\.open" argument must be a boolean/, - }); - }); -}); - -suite('DatabaseSync.prototype.open()', () => { - test('opens a database connection', (t) => { - const dbPath = nextDb(); - const db = new DatabaseSync(dbPath, { open: false }); - - t.assert.strictEqual(existsSync(dbPath), false); - t.assert.strictEqual(db.open(), undefined); - t.assert.strictEqual(existsSync(dbPath), true); - }); - - test('throws if database is already open', (t) => { - const db = new DatabaseSync(nextDb(), { open: false }); - - db.open(); - t.assert.throws(() => { - db.open(); - }, { - code: 'ERR_INVALID_STATE', - message: /database is already open/, - }); - }); -}); - -suite('DatabaseSync.prototype.close()', () => { - test('closes an open database connection', (t) => { - const db = new DatabaseSync(nextDb()); - - t.assert.strictEqual(db.close(), undefined); - }); - - test('throws if database is not open', (t) => { - const db = new DatabaseSync(nextDb(), { open: false }); - - t.assert.throws(() => { - db.close(); - }, { - code: 'ERR_INVALID_STATE', - message: /database is not open/, - }); - }); -}); - -suite('DatabaseSync.prototype.prepare()', () => { - test('returns a prepared statement', (t) => { - const db = new DatabaseSync(nextDb()); - const stmt = db.prepare('CREATE TABLE webstorage(key TEXT)'); - t.assert.ok(stmt instanceof StatementSync); - }); - - test('throws if database is not open', (t) => { - const db = new DatabaseSync(nextDb(), { open: false }); - - t.assert.throws(() => { - db.prepare(); - }, { - code: 'ERR_INVALID_STATE', - message: /database is not open/, - }); - }); - - test('throws if sql is not a string', (t) => { - const db = new DatabaseSync(nextDb()); - - t.assert.throws(() => { - db.prepare(); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /The "sql" argument must be a string/, - }); - }); -}); - -suite('DatabaseSync.prototype.exec()', () => { - test('executes SQL', (t) => { - const db = new DatabaseSync(nextDb()); - const result = db.exec(` - CREATE TABLE data( - key INTEGER PRIMARY KEY, - val INTEGER - ) STRICT; - INSERT INTO data (key, val) VALUES (1, 2); - INSERT INTO data (key, val) VALUES (8, 9); - `); - t.assert.strictEqual(result, undefined); - const stmt = db.prepare('SELECT * FROM data ORDER BY key'); - t.assert.deepStrictEqual(stmt.all(), [ - { key: 1, val: 2 }, - { key: 8, val: 9 }, - ]); - }); - - test('reports errors from SQLite', (t) => { - const db = new DatabaseSync(nextDb()); - - t.assert.throws(() => { - db.exec('CREATE TABLEEEE'); - }, { - code: 'ERR_SQLITE_ERROR', - message: /syntax error/, - }); - }); - - test('throws if database is not open', (t) => { - const db = new DatabaseSync(nextDb(), { open: false }); - - t.assert.throws(() => { - db.exec(); - }, { - code: 'ERR_INVALID_STATE', - message: /database is not open/, - }); - }); - - test('throws if sql is not a string', (t) => { - const db = new DatabaseSync(nextDb()); - - t.assert.throws(() => { - db.exec(); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /The "sql" argument must be a string/, - }); - }); -}); - -suite('StatementSync() constructor', () => { - test('StatementSync cannot be constructed directly', (t) => { - t.assert.throws(() => { - new StatementSync(); - }, { - code: 'ERR_ILLEGAL_CONSTRUCTOR', - message: /Illegal constructor/, - }); - }); -}); - -suite('StatementSync.prototype.get()', () => { - test('executes a query and returns undefined on no results', (t) => { - const db = new DatabaseSync(nextDb()); - let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); - t.assert.strictEqual(stmt.get(), undefined); - stmt = db.prepare('SELECT * FROM storage'); - t.assert.strictEqual(stmt.get(), undefined); - }); - - test('executes a query and returns the first result', (t) => { - const db = new DatabaseSync(nextDb()); - let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); - t.assert.strictEqual(stmt.get(), undefined); - stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)'); - t.assert.strictEqual(stmt.get('key1', 'val1'), undefined); - t.assert.strictEqual(stmt.get('key2', 'val2'), undefined); - stmt = db.prepare('SELECT * FROM storage ORDER BY key'); - t.assert.deepStrictEqual(stmt.get(), { key: 'key1', val: 'val1' }); - }); -}); - -suite('StatementSync.prototype.all()', () => { - test('executes a query and returns an empty array on no results', (t) => { - const db = new DatabaseSync(nextDb()); - const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); - t.assert.deepStrictEqual(stmt.all(), []); - }); - - test('executes a query and returns all results', (t) => { - const db = new DatabaseSync(nextDb()); - let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); - t.assert.deepStrictEqual(stmt.run(), { changes: 0, lastInsertRowid: 0 }); - stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)'); - t.assert.deepStrictEqual( - stmt.run('key1', 'val1'), - { changes: 1, lastInsertRowid: 1 }, - ); - t.assert.deepStrictEqual( - stmt.run('key2', 'val2'), - { changes: 1, lastInsertRowid: 2 }, - ); - stmt = db.prepare('SELECT * FROM storage ORDER BY key'); - t.assert.deepStrictEqual(stmt.all(), [ - { key: 'key1', val: 'val1' }, - { key: 'key2', val: 'val2' }, - ]); - }); -}); - -suite('StatementSync.prototype.run()', () => { - test('executes a query and returns change metadata', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec(` - CREATE TABLE storage(key TEXT, val TEXT); - INSERT INTO storage (key, val) VALUES ('foo', 'bar'); - `); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('SELECT * FROM storage'); - t.assert.deepStrictEqual(stmt.run(), { changes: 1, lastInsertRowid: 1 }); - }); - - test('SQLite throws when trying to bind too many parameters', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)'); - t.assert.throws(() => { - stmt.run(1, 2, 3); - }, { - code: 'ERR_SQLITE_ERROR', - message: 'column index out of range', - errcode: 25, - errstr: 'column index out of range', - }); - }); - - test('SQLite defaults to NULL for unbound parameters', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)'); - t.assert.throws(() => { - stmt.run(1); - }, { - code: 'ERR_SQLITE_ERROR', - message: 'NOT NULL constraint failed: data.val', - errcode: 1299, - errstr: 'constraint failed', - }); - }); -}); - -suite('StatementSync.prototype.sourceSQL()', () => { - test('returns input SQL', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const sql = 'INSERT INTO types (key, val) VALUES ($k, $v)'; - const stmt = db.prepare(sql); - t.assert.strictEqual(stmt.sourceSQL(), sql); - }); -}); - -suite('StatementSync.prototype.expandedSQL()', () => { - test('returns expanded SQL', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const sql = 'INSERT INTO types (key, val) VALUES ($k, ?)'; - const expanded = 'INSERT INTO types (key, val) VALUES (\'33\', \'42\')'; - const stmt = db.prepare(sql); - t.assert.deepStrictEqual( - stmt.run({ $k: '33' }, '42'), - { changes: 1, lastInsertRowid: 33 }, - ); - t.assert.strictEqual(stmt.expandedSQL(), expanded); - }); -}); - -suite('StatementSync.prototype.setReadBigInts()', () => { - test('BigInts support can be toggled', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec(` - CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT; - INSERT INTO data (key, val) VALUES (1, 42); - `); - t.assert.strictEqual(setup, undefined); - - const query = db.prepare('SELECT val FROM data'); - t.assert.deepStrictEqual(query.get(), { val: 42 }); - t.assert.strictEqual(query.setReadBigInts(true), undefined); - t.assert.deepStrictEqual(query.get(), { val: 42n }); - t.assert.strictEqual(query.setReadBigInts(false), undefined); - t.assert.deepStrictEqual(query.get(), { val: 42 }); - - const insert = db.prepare('INSERT INTO data (key) VALUES (?)'); - t.assert.deepStrictEqual( - insert.run(10), - { changes: 1, lastInsertRowid: 10 }, - ); - t.assert.strictEqual(insert.setReadBigInts(true), undefined); - t.assert.deepStrictEqual( - insert.run(20), - { changes: 1n, lastInsertRowid: 20n }, - ); - t.assert.strictEqual(insert.setReadBigInts(false), undefined); - t.assert.deepStrictEqual( - insert.run(30), - { changes: 1, lastInsertRowid: 30 }, - ); - }); - - test('throws when input is not a boolean', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)'); - t.assert.throws(() => { - stmt.setReadBigInts(); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /The "readBigInts" argument must be a boolean/, - }); - }); - - test('BigInt is required for reading large integers', (t) => { - const db = new DatabaseSync(nextDb()); - const bad = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`); - t.assert.throws(() => { - bad.get(); - }, { - code: 'ERR_OUT_OF_RANGE', - message: /^The value of column 0 is too large.*: 9007199254740992$/, - }); - const good = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`); - good.setReadBigInts(true); - t.assert.deepStrictEqual(good.get(), { - [`${Number.MAX_SAFE_INTEGER} + 1`]: 2n ** 53n, - }); - }); -}); - -suite('StatementSync.prototype.setAllowBareNamedParameters()', () => { - test('bare named parameter support can be toggled', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)'); - t.assert.deepStrictEqual( - stmt.run({ k: 1, v: 2 }), - { changes: 1, lastInsertRowid: 1 }, - ); - t.assert.strictEqual(stmt.setAllowBareNamedParameters(false), undefined); - t.assert.throws(() => { - stmt.run({ k: 2, v: 4 }); - }, { - code: 'ERR_INVALID_STATE', - message: /Unknown named parameter 'k'/, - }); - t.assert.strictEqual(stmt.setAllowBareNamedParameters(true), undefined); - t.assert.deepStrictEqual( - stmt.run({ k: 3, v: 6 }), - { changes: 1, lastInsertRowid: 3 }, - ); - }); - - test('throws when input is not a boolean', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)'); - t.assert.throws(() => { - stmt.setAllowBareNamedParameters(); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /The "allowBareNamedParameters" argument must be a boolean/, - }); - }); -}); - -suite('data binding and mapping', () => { - test('supported data types', (t) => { - const u8a = new TextEncoder().encode('a☃b☃c'); - const db = new DatabaseSync(nextDb()); - const setup = db.exec(` - CREATE TABLE types( - key INTEGER PRIMARY KEY, - int INTEGER, - double REAL, - text TEXT, - buf BLOB - ) STRICT; - `); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('INSERT INTO types (key, int, double, text, buf) ' + - 'VALUES (?, ?, ?, ?, ?)'); - t.assert.deepStrictEqual( - stmt.run(1, 42, 3.14159, 'foo', u8a), - { changes: 1, lastInsertRowid: 1 }, - ); - t.assert.deepStrictEqual( - stmt.run(2, null, null, null, null), - { changes: 1, lastInsertRowid: 2 } - ); - t.assert.deepStrictEqual( - stmt.run(3, Number(8), Number(2.718), String('bar'), Buffer.from('x☃y☃')), - { changes: 1, lastInsertRowid: 3 }, - ); - t.assert.deepStrictEqual( - stmt.run(4, 99n, 0xf, '', new Uint8Array()), - { changes: 1, lastInsertRowid: 4 }, - ); - - const query = db.prepare('SELECT * FROM types WHERE key = ?'); - t.assert.deepStrictEqual(query.get(1), { - key: 1, - int: 42, - double: 3.14159, - text: 'foo', - buf: u8a, - }); - t.assert.deepStrictEqual(query.get(2), { - key: 2, - int: null, - double: null, - text: null, - buf: null, - }); - t.assert.deepStrictEqual(query.get(3), { - key: 3, - int: 8, - double: 2.718, - text: 'bar', - buf: new TextEncoder().encode('x☃y☃'), - }); - t.assert.deepStrictEqual(query.get(4), { - key: 4, - int: 99, - double: 0xf, - text: '', - buf: new Uint8Array(), - }); - }); - - test('unsupported data types', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - - [ - undefined, - () => {}, - Symbol(), - /foo/, - Promise.resolve(), - new Map(), - new Set(), - ].forEach((val) => { - t.assert.throws(() => { - db.prepare('INSERT INTO types (key, val) VALUES (?, ?)').run(1, val); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /Provided value cannot be bound to SQLite parameter 2/, - }); - }); - - t.assert.throws(() => { - const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)'); - stmt.run({ $k: 1, $v: () => {} }); - }, { - code: 'ERR_INVALID_ARG_TYPE', - message: /Provided value cannot be bound to SQLite parameter 2/, - }); - }); - - test('throws when binding a BigInt that is too large', (t) => { - const max = 9223372036854775807n; // Largest 64-bit signed integer value. - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('INSERT INTO types (key, val) VALUES (?, ?)'); - t.assert.deepStrictEqual( - stmt.run(1, max), - { changes: 1, lastInsertRowid: 1 }, - ); - t.assert.throws(() => { - stmt.run(1, max + 1n); - }, { - code: 'ERR_INVALID_ARG_VALUE', - message: /BigInt value is too large to bind/, - }); - }); - - test('statements are unbound on each call', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)'); - t.assert.deepStrictEqual( - stmt.run(1, 5), - { changes: 1, lastInsertRowid: 1 }, - ); - t.assert.deepStrictEqual( - stmt.run(), - { changes: 1, lastInsertRowid: 2 }, - ); - t.assert.deepStrictEqual( - db.prepare('SELECT * FROM data ORDER BY key').all(), - [{ key: 1, val: 5 }, { key: 2, val: null }], - ); - }); -}); - -suite('manual transactions', () => { - test('a transaction is committed', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec(` - CREATE TABLE data( - key INTEGER PRIMARY KEY - ) STRICT; - `); - t.assert.strictEqual(setup, undefined); - t.assert.deepStrictEqual( - db.prepare('BEGIN').run(), - { changes: 0, lastInsertRowid: 0 }, - ); - t.assert.deepStrictEqual( - db.prepare('INSERT INTO data (key) VALUES (100)').run(), - { changes: 1, lastInsertRowid: 100 }, - ); - t.assert.deepStrictEqual( - db.prepare('COMMIT').run(), - { changes: 1, lastInsertRowid: 100 }, - ); - t.assert.deepStrictEqual( - db.prepare('SELECT * FROM data').all(), - [{ key: 100 }], - ); - }); - - test('a transaction is rolled back', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec(` - CREATE TABLE data( - key INTEGER PRIMARY KEY - ) STRICT; - `); - t.assert.strictEqual(setup, undefined); - t.assert.deepStrictEqual( - db.prepare('BEGIN').run(), - { changes: 0, lastInsertRowid: 0 }, - ); - t.assert.deepStrictEqual( - db.prepare('INSERT INTO data (key) VALUES (100)').run(), - { changes: 1, lastInsertRowid: 100 }, - ); - t.assert.deepStrictEqual( - db.prepare('ROLLBACK').run(), - { changes: 1, lastInsertRowid: 100 }, - ); - t.assert.deepStrictEqual(db.prepare('SELECT * FROM data').all(), []); - }); -}); - -suite('named parameters', () => { - test('throws on unknown named parameters', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - - t.assert.throws(() => { - const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)'); - stmt.run({ $k: 1, $unknown: 1 }); - }, { - code: 'ERR_INVALID_STATE', - message: /Unknown named parameter '\$unknown'/, - }); - }); - - test('bare named parameters are supported', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)'); - stmt.run({ k: 1, v: 9 }); - t.assert.deepStrictEqual( - db.prepare('SELECT * FROM data').get(), - { key: 1, val: 9 }, - ); - }); - - test('duplicate bare named parameters are supported', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $k)'); - stmt.run({ k: 1 }); - t.assert.deepStrictEqual( - db.prepare('SELECT * FROM data').get(), - { key: 1, val: 1 }, - ); - }); - - test('bare named parameters throw on ambiguous names', (t) => { - const db = new DatabaseSync(nextDb()); - const setup = db.exec( - 'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;' - ); - t.assert.strictEqual(setup, undefined); - const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, @k)'); - t.assert.throws(() => { - stmt.run({ k: 1 }); - }, { - code: 'ERR_INVALID_STATE', - message: 'Cannot create bare named parameter \'k\' because of ' + - 'conflicting names \'$k\' and \'@k\'.', - }); - }); -}); - test('ERR_SQLITE_ERROR is thrown for errors originating from SQLite', (t) => { const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); const setup = db.exec(` CREATE TABLE test( key INTEGER PRIMARY KEY @@ -747,6 +87,7 @@ test('in-memory databases are supported', (t) => { test('PRAGMAs are supported', (t) => { const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); t.assert.deepStrictEqual( db.prepare('PRAGMA journal_mode = WAL').get(), { journal_mode: 'wal' },