Skip to content

Commit 0bf1052

Browse files
committed
sqlite: dd readNullAsUndefined to prepare options
Add support for the readNullAsUndefined option. Added to database.prepare and DatabseSync. Option can be set at db level. Or overridden at statement level. Refs: #61472 (comment) Refs: #61311
1 parent 20c3777 commit 0bf1052

File tree

4 files changed

+201
-1
lines changed

4 files changed

+201
-1
lines changed

doc/api/sqlite.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ changes:
154154
character (e.g., `foo` instead of `:foo`). **Default:** `true`.
155155
* `allowUnknownNamedParameters` {boolean} If `true`, unknown named parameters are ignored when binding.
156156
If `false`, an exception is thrown for unknown named parameters. **Default:** `false`.
157+
* `readNullAsUndefined` {boolean} If `true`, SQL `NULL` values are returned as `undefined` instead
158+
of `null`. **Default:** `false`.
157159
* `defensive` {boolean} If `true`, enables the defensive flag. When the defensive flag is enabled,
158160
language features that allow ordinary SQL to deliberately corrupt the database file are disabled.
159161
The defensive flag can also be set using `enableDefensive()`.
@@ -473,6 +475,8 @@ added: v22.5.0
473475
database options or `true`.
474476
* `allowUnknownNamedParameters` {boolean} If `true`, unknown named parameters
475477
are ignored. **Default:** inherited from database options or `false`.
478+
* `readNullAsUndefined` {boolean} If `true`, SQL `NULL` values are returned
479+
as `undefined` instead of `null`. **Default:** `false`.
476480
* Returns: {StatementSync} The prepared statement.
477481

478482
Compiles a SQL statement into a [prepared statement][]. This method is a wrapper

src/node_sqlite.cc

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,23 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
10801080
}
10811081
}
10821082

1083+
Local<Value> read_null_as_undefined_v;
1084+
if (options->Get(env->context(),
1085+
FIXED_ONE_BYTE_STRING(env->isolate(),
1086+
"readNullAsUndefined"))
1087+
.ToLocal(&read_null_as_undefined_v)) {
1088+
if (!read_null_as_undefined_v->IsUndefined()) {
1089+
if (!read_null_as_undefined_v->IsBoolean()) {
1090+
THROW_ERR_INVALID_ARG_TYPE(
1091+
env->isolate(),
1092+
R"(The "options.readNullAsUndefined" argument must be a boolean.)");
1093+
return;
1094+
}
1095+
open_config.set_read_null_as_undefined(
1096+
read_null_as_undefined_v.As<Boolean>()->Value());
1097+
}
1098+
}
1099+
10831100
Local<Value> defensive_v;
10841101
if (!options->Get(env->context(), env->defensive_string())
10851102
.ToLocal(&defensive_v)) {
@@ -1157,6 +1174,7 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
11571174
std::optional<bool> use_big_ints;
11581175
std::optional<bool> allow_bare_named_params;
11591176
std::optional<bool> allow_unknown_named_params;
1177+
std::optional<bool> read_null_as_undefined;
11601178

11611179
if (args.Length() > 1 && !args[1]->IsUndefined()) {
11621180
if (!args[1]->IsObject()) {
@@ -1237,6 +1255,23 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
12371255
}
12381256
allow_unknown_named_params = allow_unknown_named_params_v->IsTrue();
12391257
}
1258+
1259+
Local<Value> read_null_as_undefined_v;
1260+
if (!options
1261+
->Get(env->context(),
1262+
FIXED_ONE_BYTE_STRING(env->isolate(), "readNullAsUndefined"))
1263+
.ToLocal(&read_null_as_undefined_v)) {
1264+
return;
1265+
}
1266+
if (!read_null_as_undefined_v->IsUndefined()) {
1267+
if (!read_null_as_undefined_v->IsBoolean()) {
1268+
THROW_ERR_INVALID_ARG_TYPE(
1269+
env->isolate(),
1270+
"The \"options.readNullAsUndefined\" argument must be a boolean.");
1271+
return;
1272+
}
1273+
read_null_as_undefined = read_null_as_undefined_v->IsTrue();
1274+
}
12401275
}
12411276

12421277
Utf8Value sql(env->isolate(), args[0].As<String>());
@@ -1260,7 +1295,9 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
12601295
if (allow_unknown_named_params.has_value()) {
12611296
stmt->allow_unknown_named_params_ = allow_unknown_named_params.value();
12621297
}
1263-
1298+
if (read_null_as_undefined.has_value()) {
1299+
stmt->read_null_as_undefined_ = read_null_as_undefined.value();
1300+
}
12641301
args.GetReturnValue().Set(stmt->object());
12651302
}
12661303

@@ -2136,6 +2173,7 @@ StatementSync::StatementSync(Environment* env,
21362173
return_arrays_ = db_->return_arrays();
21372174
allow_bare_named_params_ = db_->allow_bare_named_params();
21382175
allow_unknown_named_params_ = db_->allow_unknown_named_params();
2176+
read_null_as_undefined_ = db_->read_null_as_undefined();
21392177

21402178
bare_named_params_ = std::nullopt;
21412179
}

test/parallel/test-sqlite-database-sync.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,29 @@ suite('DatabaseSync() constructor', () => {
282282
);
283283
});
284284

285+
test('throws if options.readNullAsUndefined is provided but is not a boolean', (t) => {
286+
t.assert.throws(() => {
287+
new DatabaseSync('foo', { readNullAsUndefined: 42 });
288+
}, {
289+
code: 'ERR_INVALID_ARG_TYPE',
290+
message: 'The "options.readNullAsUndefined" argument must be a boolean.',
291+
});
292+
});
293+
294+
test('allows reading NULL as undefined', (t) => {
295+
const dbPath = nextDb();
296+
const db = new DatabaseSync(dbPath, { readNullAsUndefined: true });
297+
t.after(() => { db.close(); });
298+
const setup = db.exec(`
299+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
300+
INSERT INTO data (key, val) VALUES (1, NULL);
301+
`);
302+
t.assert.strictEqual(setup, undefined);
303+
304+
const query = db.prepare('SELECT val FROM data WHERE key = 1');
305+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: undefined });
306+
});
307+
285308
test('has sqlite-type symbol property', (t) => {
286309
const dbPath = nextDb();
287310
const db = new DatabaseSync(dbPath);

test/parallel/test-sqlite-statement-sync.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,3 +959,138 @@ suite('StatementSync.prototype.setReadNullAsUndefined()', () => {
959959
});
960960
});
961961

962+
suite('options.readNullAsUndefined', () => {
963+
test('undefined is returned when input is true', (t) => {
964+
const db = new DatabaseSync(nextDb());
965+
t.after(() => { db.close(); });
966+
const setup = db.exec(`
967+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
968+
INSERT INTO data (key, val) VALUES (1, NULL);
969+
`);
970+
t.assert.strictEqual(setup, undefined);
971+
972+
const query = db.prepare(
973+
'SELECT val FROM data WHERE key = 1',
974+
{ readNullAsUndefined: true }
975+
);
976+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: undefined });
977+
});
978+
979+
test('null is returned when input is false', (t) => {
980+
const db = new DatabaseSync(nextDb());
981+
t.after(() => { db.close(); });
982+
const setup = db.exec(`
983+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
984+
INSERT INTO data (key, val) VALUES (1, NULL);
985+
`);
986+
t.assert.strictEqual(setup, undefined);
987+
988+
const query = db.prepare(
989+
'SELECT val FROM data WHERE key = 1',
990+
{ readNullAsUndefined: false }
991+
);
992+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: null });
993+
});
994+
995+
test('null is returned by default', (t) => {
996+
const db = new DatabaseSync(nextDb());
997+
t.after(() => { db.close(); });
998+
const setup = db.exec(`
999+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
1000+
INSERT INTO data (key, val) VALUES (1, NULL);
1001+
`);
1002+
t.assert.strictEqual(setup, undefined);
1003+
1004+
const query = db.prepare('SELECT val FROM data WHERE key = 1');
1005+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: null });
1006+
});
1007+
1008+
test('throws when input is not a boolean', (t) => {
1009+
const db = new DatabaseSync(nextDb());
1010+
t.after(() => { db.close(); });
1011+
const setup = db.exec(
1012+
'CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;'
1013+
);
1014+
t.assert.strictEqual(setup, undefined);
1015+
t.assert.throws(() => {
1016+
db.prepare('SELECT val FROM data', { readNullAsUndefined: 'true' });
1017+
}, {
1018+
code: 'ERR_INVALID_ARG_TYPE',
1019+
message: /The "options\.readNullAsUndefined" argument must be a boolean/,
1020+
});
1021+
});
1022+
1023+
test('setReadNullAsUndefined can override prepare option', (t) => {
1024+
const db = new DatabaseSync(nextDb());
1025+
t.after(() => { db.close(); });
1026+
const setup = db.exec(`
1027+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
1028+
INSERT INTO data (key, val) VALUES (1, NULL);
1029+
`);
1030+
t.assert.strictEqual(setup, undefined);
1031+
1032+
const query = db.prepare(
1033+
'SELECT val FROM data WHERE key = 1',
1034+
{ readNullAsUndefined: true }
1035+
);
1036+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: undefined });
1037+
t.assert.strictEqual(query.setReadNullAsUndefined(false), undefined);
1038+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: null });
1039+
});
1040+
1041+
test('all() returns undefined when input is true', (t) => {
1042+
const db = new DatabaseSync(nextDb());
1043+
t.after(() => { db.close(); });
1044+
const setup = db.exec(`
1045+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
1046+
INSERT INTO data (key, val) VALUES (1, NULL);
1047+
INSERT INTO data (key, val) VALUES (2, 'two');
1048+
`);
1049+
t.assert.strictEqual(setup, undefined);
1050+
1051+
const query = db.prepare(
1052+
'SELECT key, val FROM data ORDER BY key',
1053+
{ readNullAsUndefined: true }
1054+
);
1055+
t.assert.deepStrictEqual(query.all(), [
1056+
{ __proto__: null, key: 1, val: undefined },
1057+
{ __proto__: null, key: 2, val: 'two' },
1058+
]);
1059+
});
1060+
1061+
test('iterate() returns undefined when input is true', (t) => {
1062+
const db = new DatabaseSync(nextDb());
1063+
t.after(() => { db.close(); });
1064+
const setup = db.exec(`
1065+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
1066+
INSERT INTO data (key, val) VALUES (1, NULL);
1067+
INSERT INTO data (key, val) VALUES (2, NULL);
1068+
`);
1069+
t.assert.strictEqual(setup, undefined);
1070+
1071+
const query = db.prepare(
1072+
'SELECT key, val FROM data ORDER BY key',
1073+
{ readNullAsUndefined: true }
1074+
);
1075+
t.assert.deepStrictEqual(query.iterate().toArray(), [
1076+
{ __proto__: null, key: 1, val: undefined },
1077+
{ __proto__: null, key: 2, val: undefined },
1078+
]);
1079+
});
1080+
1081+
test('works with returnArrays option', (t) => {
1082+
const db = new DatabaseSync(nextDb());
1083+
t.after(() => { db.close(); });
1084+
const setup = db.exec(`
1085+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
1086+
INSERT INTO data (key, val) VALUES (1, NULL);
1087+
`);
1088+
t.assert.strictEqual(setup, undefined);
1089+
1090+
const query = db.prepare(
1091+
'SELECT key, val FROM data WHERE key = 1',
1092+
{ readNullAsUndefined: true, returnArrays: true }
1093+
);
1094+
t.assert.deepStrictEqual(query.get(), [1, undefined]);
1095+
});
1096+
});

0 commit comments

Comments
 (0)