Skip to content

Commit 66ce7e4

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 6cb2df1 commit 66ce7e4

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
@@ -997,6 +997,21 @@ be used to read `INTEGER` data using JavaScript `BigInt`s. This method has no
997997
impact on database write operations where numbers and `BigInt`s are both
998998
supported at all times.
999999

1000+
### `statement.setReadNullAsUndefined(enabled)`
1001+
1002+
<!-- YAML
1003+
added:
1004+
-->
1005+
1006+
* `enabled` {boolean} Enables or disables returning SQL `NULL` values as
1007+
JavaScript `undefined` when reading from the database.
1008+
1009+
When reading from the database, SQLite `NULL` values are mapped to JavaScript
1010+
`null` by default. This method can be used to instead return `undefined` for
1011+
`NULL` values when materialising result rows. This setting only affects how
1012+
result rows are returned and does not impact values passed to user-defined
1013+
functions or aggregate functions.
1014+
10001015
### `statement.sourceSQL`
10011016

10021017
<!-- 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);
@@ -2366,7 +2319,7 @@ MaybeLocal<Value> StatementExecutionHelper::ColumnToValue(Environment* env,
23662319
bool read_null_as_undefined) {
23672320
Isolate* isolate = env->isolate();
23682321
MaybeLocal<Value> js_val = MaybeLocal<Value>();
2369-
SQLITE_VALUE_TO_JS_READ(
2322+
SQLITE_VALUE_TO_JS(
23702323
column, isolate, use_big_ints, read_null_as_undefined, js_val, stmt, column);
23712324
return js_val;
23722325
}

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,3 +854,108 @@ suite('options.allowBareNamedParameters', () => {
854854
);
855855
});
856856
});
857+
858+
suite('StatementSync.prototype.setReadNullAsUndefined()', () => {
859+
test('NULL conversion can be toggled', (t) => {
860+
const db = new DatabaseSync(nextDb());
861+
t.after(() => { db.close(); });
862+
863+
db.exec(`
864+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
865+
INSERT INTO data (key, val) VALUES (1, NULL);
866+
`);
867+
868+
const query = db.prepare('SELECT val FROM data WHERE key = 1');
869+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: null });
870+
871+
t.assert.strictEqual(query.setReadNullAsUndefined(true), undefined);
872+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: undefined });
873+
874+
t.assert.strictEqual(query.setReadNullAsUndefined(false), undefined);
875+
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: null });
876+
});
877+
878+
test('throws when input is not a boolean', (t) => {
879+
const db = new DatabaseSync(nextDb());
880+
t.after(() => { db.close(); });
881+
882+
db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
883+
884+
const stmt = db.prepare('SELECT val FROM data');
885+
t.assert.throws(() => {
886+
stmt.setReadNullAsUndefined();
887+
}, {
888+
code: 'ERR_INVALID_ARG_TYPE',
889+
message: /The "readNullAsUndefined" argument must be a boolean/,
890+
});
891+
});
892+
893+
test('returns array rows with undefined when both flags are set', (t) => {
894+
const db = new DatabaseSync(nextDb());
895+
t.after(() => { db.close(); });
896+
897+
db.exec(`
898+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
899+
INSERT INTO data (key, val) VALUES (1, NULL);
900+
`);
901+
902+
const query = db.prepare('SELECT key, val FROM data WHERE key = 1');
903+
query.setReturnArrays(true);
904+
query.setReadNullAsUndefined(true);
905+
906+
t.assert.deepStrictEqual(query.get(), [1, undefined]);
907+
});
908+
909+
test('applies to all()', (t) => {
910+
const db = new DatabaseSync(nextDb());
911+
t.after(() => { db.close(); });
912+
913+
db.exec(`
914+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
915+
INSERT INTO data (key, val) VALUES (1, NULL), (2, 'two');
916+
`);
917+
918+
const query = db.prepare('SELECT key, val FROM data ORDER BY key');
919+
query.setReadNullAsUndefined(true);
920+
921+
t.assert.deepStrictEqual(query.all(), [
922+
{ __proto__: null, key: 1, val: undefined },
923+
{ __proto__: null, key: 2, val: 'two' },
924+
]);
925+
});
926+
927+
test('applies to iterate()', (t) => {
928+
const db = new DatabaseSync(nextDb());
929+
t.after(() => { db.close(); });
930+
931+
db.exec(`
932+
CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
933+
INSERT INTO data (key, val) VALUES (1, NULL), (2, NULL);
934+
`);
935+
936+
const query = db.prepare('SELECT key, val FROM data ORDER BY key');
937+
query.setReadNullAsUndefined(true);
938+
939+
const iter = query.iterate();
940+
t.assert.deepStrictEqual(iter.next().value, { __proto__: null, key: 1, val: undefined });
941+
t.assert.deepStrictEqual(iter.next().value, { __proto__: null, key: 2, val: undefined });
942+
t.assert.strictEqual(iter.next().done, true);
943+
});
944+
945+
test('does not change NULL passed to user-defined functions', (t) => {
946+
const db = new DatabaseSync(nextDb());
947+
t.after(() => { db.close(); });
948+
949+
db.exec('CREATE TABLE data(val TEXT) STRICT; INSERT INTO data VALUES (NULL);');
950+
951+
let seen;
952+
db.function('echo', (x) => { seen = x; return x; });
953+
954+
const query = db.prepare('SELECT echo(val) AS out FROM data');
955+
query.setReadNullAsUndefined(true);
956+
957+
t.assert.deepStrictEqual(query.get(), { __proto__: null, out: undefined });
958+
t.assert.strictEqual(seen, null);
959+
});
960+
});
961+

0 commit comments

Comments
 (0)