From 5ea55795201d1a732208f1b99da2baa041e4e3e9 Mon Sep 17 00:00:00 2001 From: cjihrig Date: Mon, 5 Aug 2024 23:37:13 -0400 Subject: [PATCH 1/3] sqlite: ensure statement finalization on db close This commit adds statement tracking to the DatabaseSync class. When a database is closed manually or via garbage collection, it will force all associated prepared statements to be finalized. This should mitigate "zombie" connections which can introduce test flakiness in the CI on Windows. --- src/node_sqlite.cc | 102 +++++++++++++++++++++++++++++++++++---------- src/node_sqlite.h | 16 +++++-- 2 files changed, 92 insertions(+), 26 deletions(-) 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_; From 7bf62d008ca203bb22fac96d5759ec002fb870ff Mon Sep 17 00:00:00 2001 From: cjihrig Date: Wed, 24 Jul 2024 00:35:00 -0400 Subject: [PATCH 2/3] sqlite: split up large test file The original test/parallel/test-sqlite.js test appears to time out in the CI occasionally. This commit splits the test into several smaller test files. Fixes: https://github.com/nodejs/node/issues/54006 --- test/parallel/test-sqlite-data-types.js | 157 +++++ test/parallel/test-sqlite-database-sync.js | 185 +++++ test/parallel/test-sqlite-named-parameters.js | 80 +++ test/parallel/test-sqlite-statement-sync.js | 273 +++++++ test/parallel/test-sqlite-transactions.js | 67 ++ test/parallel/test-sqlite.js | 665 +----------------- 6 files changed, 765 insertions(+), 662 deletions(-) create mode 100644 test/parallel/test-sqlite-data-types.js create mode 100644 test/parallel/test-sqlite-database-sync.js create mode 100644 test/parallel/test-sqlite-named-parameters.js create mode 100644 test/parallel/test-sqlite-statement-sync.js create mode 100644 test/parallel/test-sqlite-transactions.js 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' }, From ee5b9c180ee04d105646ad8f3894c6495ecc415c Mon Sep 17 00:00:00 2001 From: cjihrig Date: Tue, 6 Aug 2024 18:51:02 -0400 Subject: [PATCH 3/3] test: unmark test-sqlite as flaky --- test/parallel/parallel.status | 3 --- 1 file changed, 3 deletions(-) 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