Skip to content

Commit de7bf9f

Browse files
committed
sqlite: refactor existing macro.
Refactor SQLITE_NULL case in SQLITE_VALUE_TO_JS. Remove SQLITE_VALUE_TO_JS_READ macro. Update sqlite docs. Add testing for SetReadNullAsUndefined implementation.
1 parent 883c1be commit de7bf9f

3 files changed

Lines changed: 128 additions & 55 deletions

File tree

doc/api/sqlite.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,21 @@ be used to read `INTEGER` data using JavaScript `BigInt`s. This method has no
984984
impact on database write operations where numbers and `BigInt`s are both
985985
supported at all times.
986986

987+
### `statement.setReadNullAsUndefined(enabled)`
988+
989+
<!-- YAML
990+
added:
991+
-->
992+
993+
* `enabled` {boolean} Enables or disables returning SQL `NULL` values as
994+
JavaScript `undefined` when reading from the database.
995+
996+
When reading from the database, SQLite `NULL` values are mapped to JavaScript
997+
`null` by default. This method can be used to instead return `undefined` for
998+
`NULL` values when materialising result rows. This setting only affects how
999+
result rows are returned and does not impact values passed to user-defined
1000+
functions or aggregate functions.
1001+
9871002
### `statement.sourceSQL`
9881003

9891004
<!-- YAML

src/node_sqlite.cc

Lines changed: 8 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ using v8::Value;
7272
} \
7373
} while (0)
7474

75-
#define SQLITE_VALUE_TO_JS(from, isolate, use_big_int_args, result, ...) \
75+
#define SQLITE_VALUE_TO_JS(from, isolate, use_big_int_args, \
76+
read_null_as_undef, result, ...) \
7677
do { \
7778
switch (sqlite3_##from##_type(__VA_ARGS__)) { \
7879
case SQLITE_INTEGER: { \
@@ -101,57 +102,9 @@ using v8::Value;
101102
break; \
102103
} \
103104
case SQLITE_NULL: { \
104-
(result) = Null((isolate)); \
105-
break; \
106-
} \
107-
case SQLITE_BLOB: { \
108-
size_t size = \
109-
static_cast<size_t>(sqlite3_##from##_bytes(__VA_ARGS__)); \
110-
auto data = reinterpret_cast<const uint8_t*>( \
111-
sqlite3_##from##_blob(__VA_ARGS__)); \
112-
auto store = ArrayBuffer::NewBackingStore( \
113-
(isolate), size, BackingStoreInitializationMode::kUninitialized); \
114-
memcpy(store->Data(), data, size); \
115-
auto ab = ArrayBuffer::New((isolate), std::move(store)); \
116-
(result) = Uint8Array::New(ab, 0, size); \
117-
break; \
118-
} \
119-
default: \
120-
UNREACHABLE("Bad SQLite value"); \
121-
} \
122-
} while (0)
123-
124-
#define SQLITE_VALUE_TO_JS_READ(from, isolate, use_big_int_args, \
125-
read_null_as_undef, result, ...) \
126-
do { \
127-
switch (sqlite3_##from##_type(__VA_ARGS__)) { \
128-
case SQLITE_INTEGER: { \
129-
sqlite3_int64 val = sqlite3_##from##_int64(__VA_ARGS__); \
130-
if ((use_big_int_args)) { \
131-
(result) = BigInt::New((isolate), val); \
132-
} else if (std::abs(val) <= kMaxSafeJsInteger) { \
133-
(result) = Number::New((isolate), val); \
134-
} else { \
135-
THROW_ERR_OUT_OF_RANGE((isolate), \
136-
"Value is too large to be represented as a " \
137-
"JavaScript number: %" PRId64, \
138-
val); \
139-
} \
140-
break; \
141-
} \
142-
case SQLITE_FLOAT: { \
143-
(result) = \
144-
Number::New((isolate), sqlite3_##from##_double(__VA_ARGS__)); \
145-
break; \
146-
} \
147-
case SQLITE_TEXT: { \
148-
const char* v = \
149-
reinterpret_cast<const char*>(sqlite3_##from##_text(__VA_ARGS__)); \
150-
(result) = String::NewFromUtf8((isolate), v).As<Value>(); \
151-
break; \
152-
} \
153-
case SQLITE_NULL: { \
154-
(result) = (read_null_as_undef) ? Undefined((isolate)) : Null((isolate)); \
105+
(result) = (read_null_as_undef) \
106+
? Undefined((isolate)) \
107+
: Null((isolate)); \
155108
break; \
156109
} \
157110
case SQLITE_BLOB: { \
@@ -377,7 +330,7 @@ class CustomAggregate {
377330
for (int i = 0; i < argc; ++i) {
378331
sqlite3_value* value = argv[i];
379332
MaybeLocal<Value> js_val;
380-
SQLITE_VALUE_TO_JS(value, isolate, self->use_bigint_args_, js_val, value);
333+
SQLITE_VALUE_TO_JS(value, isolate, self->use_bigint_args_, false, js_val, value);
381334
if (js_val.IsEmpty()) {
382335
// Ignore the SQLite error because a JavaScript exception is pending.
383336
self->db_->SetIgnoreNextSQLiteError(true);
@@ -679,7 +632,7 @@ void UserDefinedFunction::xFunc(sqlite3_context* ctx,
679632
for (int i = 0; i < argc; ++i) {
680633
sqlite3_value* value = argv[i];
681634
MaybeLocal<Value> js_val = MaybeLocal<Value>();
682-
SQLITE_VALUE_TO_JS(value, isolate, self->use_bigint_args_, js_val, value);
635+
SQLITE_VALUE_TO_JS(value, isolate, self->use_bigint_args_, false, js_val, value);
683636
if (js_val.IsEmpty()) {
684637
// Ignore the SQLite error because a JavaScript exception is pending.
685638
self->db_->SetIgnoreNextSQLiteError(true);
@@ -2266,7 +2219,7 @@ MaybeLocal<Value> StatementExecutionHelper::ColumnToValue(Environment* env,
22662219
bool read_null_as_undefined) {
22672220
Isolate* isolate = env->isolate();
22682221
MaybeLocal<Value> js_val = MaybeLocal<Value>();
2269-
SQLITE_VALUE_TO_JS_READ(
2222+
SQLITE_VALUE_TO_JS(
22702223
column, isolate, use_big_ints, read_null_as_undefined, js_val, stmt, column);
22712224
return js_val;
22722225
}

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,3 +609,108 @@ suite('StatementSync.prototype.setAllowBareNamedParameters()', () => {
609609
});
610610
});
611611
});
612+
613+
suite('StatementSync.prototype.setReadNullAsUndefined()', () => {
614+
test('NULL conversion can be toggled', (t) => {
615+
const db = new DatabaseSync(nextDb());
616+
t.after(() => { db.close(); });
617+
618+
db.exec(`
619+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
620+
INSERT INTO data (key, val) VALUES (1, NULL);
621+
`);
622+
623+
const query = db.prepare('SELECT val FROM data WHERE key = 1');
624+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: null });
625+
626+
t.assert.strictEqual(query.setReadNullAsUndefined(true), undefined);
627+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: undefined });
628+
629+
t.assert.strictEqual(query.setReadNullAsUndefined(false), undefined);
630+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: null });
631+
});
632+
633+
test('throws when input is not a boolean', (t) => {
634+
const db = new DatabaseSync(nextDb());
635+
t.after(() => { db.close(); });
636+
637+
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
638+
639+
const stmt = db.prepare('SELECT val FROM data');
640+
t.assert.throws(() => {
641+
stmt.setReadNullAsUndefined();
642+
}, {
643+
code: 'ERR_INVALID_ARG_TYPE',
644+
message: /The "readNullAsUndefined" argument must be a boolean/,
645+
});
646+
});
647+
648+
test('returns array rows with undefined when both flags are set', (t) => {
649+
const db = new DatabaseSync(nextDb());
650+
t.after(() => { db.close(); });
651+
652+
db.exec(`
653+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
654+
INSERT INTO data (key, val) VALUES (1, NULL);
655+
`);
656+
657+
const query = db.prepare('SELECT key, val FROM data WHERE key = 1');
658+
query.setReturnArrays(true);
659+
query.setReadNullAsUndefined(true);
660+
661+
t.assert.deepStrictEqual(query.get(), [1, undefined]);
662+
});
663+
664+
test('applies to all()', (t) => {
665+
const db = new DatabaseSync(nextDb());
666+
t.after(() => { db.close(); });
667+
668+
db.exec(`
669+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
670+
INSERT INTO data (key, val) VALUES (1, NULL), (2, 'two');
671+
`);
672+
673+
const query = db.prepare('SELECT key, val FROM data ORDER BY key');
674+
query.setReadNullAsUndefined(true);
675+
676+
t.assert.deepStrictEqual(query.all(), [
677+
{ __proto__: null, key: 1, val: undefined },
678+
{ __proto__: null, key: 2, val: 'two' },
679+
]);
680+
});
681+
682+
test('applies to iterate()', (t) => {
683+
const db = new DatabaseSync(nextDb());
684+
t.after(() => { db.close(); });
685+
686+
db.exec(`
687+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
688+
INSERT INTO data (key, val) VALUES (1, NULL), (2, NULL);
689+
`);
690+
691+
const query = db.prepare('SELECT key, val FROM data ORDER BY key');
692+
query.setReadNullAsUndefined(true);
693+
694+
const iter = query.iterate();
695+
t.assert.deepStrictEqual(iter.next().value, { __proto__: null, key: 1, val: undefined });
696+
t.assert.deepStrictEqual(iter.next().value, { __proto__: null, key: 2, val: undefined });
697+
t.assert.strictEqual(iter.next().done, true);
698+
});
699+
700+
test('does not change NULL passed to user-defined functions', (t) => {
701+
const db = new DatabaseSync(nextDb());
702+
t.after(() => { db.close(); });
703+
704+
db.exec('CREATE TABLE data(val TEXT) STRICT; INSERT INTO data VALUES (NULL);');
705+
706+
let seen;
707+
db.function('echo', (x) => { seen = x; return x; });
708+
709+
const query = db.prepare('SELECT echo(val) AS out FROM data');
710+
query.setReadNullAsUndefined(true);
711+
712+
t.assert.deepStrictEqual(query.get(), { __proto__: null, out: undefined });
713+
t.assert.strictEqual(seen, null);
714+
});
715+
});
716+

0 commit comments

Comments
 (0)