diff --git a/src/env_properties.h b/src/env_properties.h index edc013aec2d07f..cd4de6628dbced 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -338,6 +338,7 @@ V(read_host_object_string, "_readHostObject") \ V(readable_string, "readable") \ V(read_bigints_string, "readBigInts") \ + V(read_null_as_undefined_string, "readNullAsUndefined") \ V(reason_string, "reason") \ V(refresh_string, "refresh") \ V(regexp_string, "regexp") \ diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 3a7c39e72cf421..d27932ef6eb4c1 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -41,6 +41,7 @@ using v8::MaybeLocal; using v8::Name; using v8::NewStringType; using v8::Null; +using v8::Undefined; using v8::Number; using v8::Object; using v8::Promise; @@ -67,7 +68,10 @@ using v8::Value; } \ } while (0) -#define SQLITE_VALUE_TO_JS(from, isolate, use_big_int_args, result, ...) \ +#define SQLITE_VALUE_TO_JS(from, isolate, \ + use_big_int_args, \ + use_null_as_undefined_, \ + result, ...) \ do { \ switch (sqlite3_##from##_type(__VA_ARGS__)) { \ case SQLITE_INTEGER: { \ @@ -96,7 +100,11 @@ using v8::Value; break; \ } \ case SQLITE_NULL: { \ - (result) = Null((isolate)); \ + if ((use_null_as_undefined_)) { \ + (result) = Undefined((isolate)); \ + } else { \ + (result) = Null((isolate)); \ + } \ break; \ } \ case SQLITE_BLOB: { \ @@ -254,6 +262,7 @@ class CustomAggregate { explicit CustomAggregate(Environment* env, DatabaseSync* db, bool use_bigint_args, + bool use_null_as_undefined_args, Local start, Local step_fn, Local inverse_fn, @@ -261,6 +270,7 @@ class CustomAggregate { : env_(env), db_(db), use_bigint_args_(use_bigint_args), + use_null_as_undefined_args_(use_null_as_undefined_args), start_(env->isolate(), start), step_fn_(env->isolate(), step_fn), inverse_fn_(env->isolate(), inverse_fn), @@ -310,7 +320,12 @@ class CustomAggregate { for (int i = 0; i < argc; ++i) { sqlite3_value* value = argv[i]; MaybeLocal js_val; - SQLITE_VALUE_TO_JS(value, isolate, self->use_bigint_args_, js_val, value); + SQLITE_VALUE_TO_JS(value, + isolate, + self->use_bigint_args_, + self->use_null_as_undefined_args_, + js_val, + value); if (js_val.IsEmpty()) { // Ignore the SQLite error because a JavaScript exception is pending. self->db_->SetIgnoreNextSQLiteError(true); @@ -418,6 +433,7 @@ class CustomAggregate { Environment* env_; DatabaseSync* db_; bool use_bigint_args_; + bool use_null_as_undefined_args_; Global start_; Global step_fn_; Global inverse_fn_; @@ -591,11 +607,14 @@ class BackupJob : public ThreadPoolWork { UserDefinedFunction::UserDefinedFunction(Environment* env, Local fn, DatabaseSync* db, - bool use_bigint_args) + bool use_bigint_args, + bool use_null_as_undefined_args + ) : env_(env), fn_(env->isolate(), fn), db_(db), - use_bigint_args_(use_bigint_args) {} + use_bigint_args_(use_bigint_args), + use_null_as_undefined_args_(use_null_as_undefined_args) {} UserDefinedFunction::~UserDefinedFunction() {} @@ -613,7 +632,12 @@ void UserDefinedFunction::xFunc(sqlite3_context* ctx, for (int i = 0; i < argc; ++i) { sqlite3_value* value = argv[i]; MaybeLocal js_val = MaybeLocal(); - SQLITE_VALUE_TO_JS(value, isolate, self->use_bigint_args_, js_val, value); + SQLITE_VALUE_TO_JS(value, + isolate, + self->use_bigint_args_, + self->use_null_as_undefined_args_, + js_val, + value); if (js_val.IsEmpty()) { // Ignore the SQLite error because a JavaScript exception is pending. self->db_->SetIgnoreNextSQLiteError(true); @@ -981,6 +1005,21 @@ void DatabaseSync::New(const FunctionCallbackInfo& args) { } } + Local read_null_as_undefined_v; + if (options->Get(env->context(), env->read_null_as_undefined_string()) + .ToLocal(&read_null_as_undefined_v)) { + if (!read_null_as_undefined_v->IsUndefined()) { + if (!read_null_as_undefined_v->IsBoolean()) { + THROW_ERR_INVALID_ARG_TYPE( + env->isolate(), + R"(The "options.readNullAsUndefined" argument must be a boolean.)"); + return; + } + open_config.set_use_null_as_undefined(read_null_as_undefined_v + .As()->Value()); + } + } + Local return_arrays_v; if (options->Get(env->context(), env->return_arrays_string()) .ToLocal(&return_arrays_v)) { @@ -1126,6 +1165,7 @@ void DatabaseSync::CustomFunction(const FunctionCallbackInfo& args) { int fn_index = args.Length() < 3 ? 1 : 2; bool use_bigint_args = false; + bool use_null_as_undefined_args = false; bool varargs = false; bool deterministic = false; bool direct_only = false; @@ -1234,7 +1274,11 @@ void DatabaseSync::CustomFunction(const FunctionCallbackInfo& args) { } UserDefinedFunction* user_data = - new UserDefinedFunction(env, fn, db, use_bigint_args); + new UserDefinedFunction(env, + fn, + db, + use_bigint_args, + use_null_as_undefined_args); int text_rep = SQLITE_UTF8; if (deterministic) { @@ -1323,9 +1367,11 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo& args) { } bool use_bigint_args = false; + bool use_null_as_undefined_args = false; bool varargs = false; bool direct_only = false; Local use_bigint_args_v; + Local use_null_as_undefined_args_v; Local inverseFunc = Local(); if (!options ->Get(env->context(), @@ -1344,6 +1390,23 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo& args) { use_bigint_args = use_bigint_args_v.As()->Value(); } + if (!options + ->Get(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "useNullAsUndefined")) + .ToLocal(&use_null_as_undefined_args_v)) { + return; + } + + if (!use_null_as_undefined_args_v->IsUndefined()) { + if (!use_null_as_undefined_args_v->IsBoolean()) { + THROW_ERR_INVALID_ARG_TYPE( + env->isolate(), + "The \"options.useNullAsUndefined\" argument must be a boolean."); + return; + } + use_null_as_undefined_args = use_bigint_args_v.As()->Value(); + } + Local varargs_v; if (!options ->Get(env->context(), @@ -1429,13 +1492,15 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo& args) { *name, argc, text_rep, - new CustomAggregate(env, - db, - use_bigint_args, - start_v, - stepFunction, - inverseFunc, - resultFunction), + new CustomAggregate( + env, + db, + use_bigint_args, + use_null_as_undefined_args, + start_v, + stepFunction, + inverseFunc, + resultFunction), CustomAggregate::xStep, CustomAggregate::xFinal, xValue, @@ -1999,8 +2064,13 @@ bool StatementSync::BindValue(const Local& value, const int index) { MaybeLocal StatementSync::ColumnToValue(const int column) { Isolate* isolate = env()->isolate(); MaybeLocal js_val = MaybeLocal(); - SQLITE_VALUE_TO_JS( - column, isolate, use_big_ints_, js_val, statement_, column); + SQLITE_VALUE_TO_JS(column, + isolate, + use_big_ints_, + use_null_as_undefined_, + js_val, + statement_, + column); return js_val; } @@ -2378,6 +2448,24 @@ void StatementSync::SetReadBigInts(const FunctionCallbackInfo& args) { stmt->use_big_ints_ = args[0]->IsTrue(); } +void StatementSync::SetReadNullAsUndefined( + 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()) { + THROW_ERR_INVALID_ARG_TYPE( + env->isolate(), + "The \"readNullAsUndefined\" argument must be a boolean."); + return; + } + + stmt->use_null_as_undefined_ = args[0]->IsTrue(); +} + void StatementSync::SetReturnArrays(const FunctionCallbackInfo& args) { StatementSync* stmt; ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); @@ -2447,6 +2535,9 @@ Local StatementSync::GetConstructorTemplate( tmpl, "setAllowUnknownNamedParameters", StatementSync::SetAllowUnknownNamedParameters); + SetProtoMethod( + isolate, tmpl, "setReadNullAsUndefined", + StatementSync::SetReadNullAsUndefined); SetProtoMethod( isolate, tmpl, "setReadBigInts", StatementSync::SetReadBigInts); SetProtoMethod( diff --git a/src/node_sqlite.h b/src/node_sqlite.h index 983dde6d851efb..4fa62de301fadc 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -43,6 +43,14 @@ class DatabaseOpenConfiguration { inline bool get_use_big_ints() const { return use_big_ints_; } + inline void set_use_null_as_undefined(bool flag) { + use_null_as_undefined_ = flag; + } + + inline bool get_use_null_as_undefined() const { + return use_null_as_undefined_; + } + inline void set_return_arrays(bool flag) { return_arrays_ = flag; } inline bool get_return_arrays() const { return return_arrays_; } @@ -70,6 +78,7 @@ class DatabaseOpenConfiguration { bool enable_dqs_ = false; int timeout_ = 0; bool use_big_ints_ = false; + bool use_null_as_undefined_ = false; bool return_arrays_ = false; bool allow_bare_named_params_ = true; bool allow_unknown_named_params_ = false; @@ -110,7 +119,12 @@ class DatabaseSync : public BaseObject { void FinalizeBackups(); void UntrackStatement(StatementSync* statement); bool IsOpen(); - bool use_big_ints() const { return open_config_.get_use_big_ints(); } + bool use_big_ints() const { + return open_config_.get_use_big_ints(); + } + bool use_null_as_undefined() const { + return open_config_.get_use_null_as_undefined(); + } bool return_arrays() const { return open_config_.get_return_arrays(); } bool allow_bare_named_params() const { return open_config_.get_allow_bare_named_params(); @@ -173,6 +187,8 @@ class StatementSync : public BaseObject { static void SetAllowUnknownNamedParameters( const v8::FunctionCallbackInfo& args); static void SetReadBigInts(const v8::FunctionCallbackInfo& args); + static void SetReadNullAsUndefined( + const v8::FunctionCallbackInfo& args); static void SetReturnArrays(const v8::FunctionCallbackInfo& args); void Finalize(); bool IsFinalized(); @@ -186,6 +202,7 @@ class StatementSync : public BaseObject { sqlite3_stmt* statement_; bool return_arrays_ = false; bool use_big_ints_; + bool use_null_as_undefined_ = false; bool allow_bare_named_params_; bool allow_unknown_named_params_; std::optional> bare_named_params_; @@ -253,7 +270,9 @@ class UserDefinedFunction { UserDefinedFunction(Environment* env, v8::Local fn, DatabaseSync* db, - bool use_bigint_args); + bool use_bigint_args, + bool use_null_as_undefined_args + ); ~UserDefinedFunction(); static void xFunc(sqlite3_context* ctx, int argc, sqlite3_value** argv); static void xDestroy(void* self); @@ -263,6 +282,7 @@ class UserDefinedFunction { v8::Global fn_; DatabaseSync* db_; bool use_bigint_args_; + bool use_null_as_undefined_args_; }; } // namespace sqlite diff --git a/test/parallel/test-sqlite-aggregate-function.mjs b/test/parallel/test-sqlite-aggregate-function.mjs index 050705c771e6af..8356d37fc56a43 100644 --- a/test/parallel/test-sqlite-aggregate-function.mjs +++ b/test/parallel/test-sqlite-aggregate-function.mjs @@ -43,6 +43,15 @@ describe('DatabaseSync.prototype.aggregate()', () => { }); }); + test('throws if options.readNullAsUndefined is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new DatabaseSync('foo', { readNullAsUndefined: 42 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.readNullAsUndefined" argument must be a boolean.', + }); + }); + test('throws if options.varargs is not a boolean', (t) => { t.assert.throws(() => { db.aggregate('sum', { diff --git a/test/parallel/test-sqlite-database-sync.js b/test/parallel/test-sqlite-database-sync.js index 5b34dec4cc220f..a5ae03c184b911 100644 --- a/test/parallel/test-sqlite-database-sync.js +++ b/test/parallel/test-sqlite-database-sync.js @@ -205,6 +205,32 @@ suite('DatabaseSync() constructor', () => { ); }); + test('throws if options.readNullAsUndefined is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new DatabaseSync('foo', { readNullAsUndefined: 42 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.readNullAsUndefined" argument must be a boolean.', + }); + }); + + test('SQL null can be read as undefined', (t) => { + const db = new DatabaseSync(nextDb(), { readNullAsUndefined: true }); + t.after(() => { db.close(); }); + const setup = db.exec(` + CREATE TABLE data(is_null TEXT DEFAULT NULL) STRICT; + INSERT INTO data(is_null) VALUES(NULL); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT is_null FROM DATA'); + t.assert.deepStrictEqual(query.get(), { __proto__: null, is_null: null }); + t.assert.strictEqual(query.setReadNullAsUndefined(true), undefined); + t.assert.deepStrictEqual(query.get(), { __proto__: null, is_null: undefined }); + t.assert.strictEqual(query.setReadNullAsUndefined(false), undefined); + t.assert.deepStrictEqual(query.get(), { __proto__: null, is_null: null }); + }); + test('throws if options.returnArrays is provided but is not a boolean', (t) => { t.assert.throws(() => { new DatabaseSync('foo', { returnArrays: 42 }); @@ -229,6 +255,26 @@ suite('DatabaseSync() constructor', () => { t.assert.deepStrictEqual(query.get(), [1, 'one']); }); + test('null to undefined in array rows with setReadNullAsUndefined()', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec(` + CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; + INSERT INTO data (key, val) VALUES (1, NULL); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT key, val FROM data WHERE key = 1'); + t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: null }); + + query.setReturnArrays(true); + query.setReadNullAsUndefined(true); + t.assert.deepStrictEqual(query.get(), [1, undefined]); + + query.setReturnArrays(false); + t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: undefined }); + }); + test('throws if options.allowBareNamedParameters is provided but is not a boolean', (t) => { t.assert.throws(() => { new DatabaseSync('foo', { allowBareNamedParameters: 42 }); diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index 858a1486601763..d60b6aa6bb22c4 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -275,6 +275,62 @@ suite('StatementSync.prototype.expandedSQL', () => { }); }); +suite('StatementSync.prototype.setReadNullAsUndefined', () => { + test('Null can be read as undefined', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec(` + CREATE TABLE data(is_null TEXT DEFAULT NULL) STRICT; + INSERT INTO data(is_null) VALUES(NULL); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT is_null FROM DATA'); + t.assert.deepStrictEqual(query.get(), { __proto__: null, is_null: null }); + t.assert.strictEqual(Object.hasOwn(query.get(), 'is_null'), true); + t.assert.strictEqual('is_null' in query.get(), true); + + query.setReadNullAsUndefined(true); + t.assert.deepStrictEqual(query.get(), { __proto__: null, is_null: undefined }); + t.assert.strictEqual(Object.hasOwn(query.get(), 'is_null'), true); + t.assert.strictEqual('is_null' in query.get(), true); + }); + + test('does not affect non-null values', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec(` + CREATE TABLE data(is_not_null TEXT) STRICT; + INSERT INTO data(is_not_null) VALUES('This is not null'); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT is_not_null FROM DATA'); + t.assert.deepStrictEqual(query.get(), { __proto__: null, is_not_null: 'This is not null' }); + t.assert.strictEqual( + Object.hasOwn(query.get(), 'is_not_null'), true); + t.assert.strictEqual(query.setReadNullAsUndefined(true), undefined); + t.assert.deepStrictEqual(query.get(), { __proto__: null, is_not_null: 'This is not null' }); + }); + 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(is_not_null TEXT) STRICT; + INSERT INTO data(is_not_null) VALUES('This is not null'); + `); + t.assert.strictEqual(setup, undefined); + + const stmt = db.prepare('SELECT is_not_null FROM data'); + t.assert.throws(() => { + stmt.setReadNullAsUndefined(); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "readNullAsUndefined" argument must be a boolean/, + }); + }); +}); + suite('StatementSync.prototype.setReadBigInts()', () => { test('BigInts support can be toggled', (t) => { const db = new DatabaseSync(nextDb()); @@ -327,8 +383,7 @@ suite('StatementSync.prototype.setReadBigInts()', () => { 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.after(() => { db.close(); }); const bad = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`); t.assert.throws(() => { bad.get(); }, { @@ -382,6 +437,26 @@ suite('StatementSync.prototype.get() with array output', () => { t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' }); }); + test('null to undefined in array rows with setReadNullAsUndefined()', (t) => { + const db = new DatabaseSync(nextDb()); + t.after(() => { db.close(); }); + const setup = db.exec(` + CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT; + INSERT INTO data (key, val) VALUES (1, NULL); + `); + t.assert.strictEqual(setup, undefined); + + const query = db.prepare('SELECT key, val FROM data WHERE key = 1'); + t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: null }); + + query.setReturnArrays(true); + query.setReadNullAsUndefined(true); + t.assert.deepStrictEqual(query.get(), [1, undefined]); + + query.setReturnArrays(false); + t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: undefined }); + }); + test('returns array rows with BigInts when both flags are set', (t) => { const expected = [1n, 9007199254740992n]; const db = new DatabaseSync(nextDb());