Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
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
  • Loading branch information
mattskel committed Feb 21, 2026
commit 0bf1052db74dbaa1968d764de7e17d033db1e2ac
4 changes: 4 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ changes:
character (e.g., `foo` instead of `:foo`). **Default:** `true`.
* `allowUnknownNamedParameters` {boolean} If `true`, unknown named parameters are ignored when binding.
If `false`, an exception is thrown for unknown named parameters. **Default:** `false`.
* `readNullAsUndefined` {boolean} If `true`, SQL `NULL` values are returned as `undefined` instead
of `null`. **Default:** `false`.
* `defensive` {boolean} If `true`, enables the defensive flag. When the defensive flag is enabled,
language features that allow ordinary SQL to deliberately corrupt the database file are disabled.
The defensive flag can also be set using `enableDefensive()`.
Expand Down Expand Up @@ -473,6 +475,8 @@ added: v22.5.0
database options or `true`.
* `allowUnknownNamedParameters` {boolean} If `true`, unknown named parameters
are ignored. **Default:** inherited from database options or `false`.
* `readNullAsUndefined` {boolean} If `true`, SQL `NULL` values are returned
as `undefined` instead of `null`. **Default:** `false`.
* Returns: {StatementSync} The prepared statement.

Compiles a SQL statement into a [prepared statement][]. This method is a wrapper
Expand Down
40 changes: 39 additions & 1 deletion src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,23 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
}
}

Local<Value> read_null_as_undefined_v;
if (options->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(),
"readNullAsUndefined"))
.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_read_null_as_undefined(
read_null_as_undefined_v.As<Boolean>()->Value());
}
}

Local<Value> defensive_v;
if (!options->Get(env->context(), env->defensive_string())
.ToLocal(&defensive_v)) {
Expand Down Expand Up @@ -1157,6 +1174,7 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
std::optional<bool> use_big_ints;
std::optional<bool> allow_bare_named_params;
std::optional<bool> allow_unknown_named_params;
std::optional<bool> read_null_as_undefined;

if (args.Length() > 1 && !args[1]->IsUndefined()) {
if (!args[1]->IsObject()) {
Expand Down Expand Up @@ -1237,6 +1255,23 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
}
allow_unknown_named_params = allow_unknown_named_params_v->IsTrue();
}

Local<Value> read_null_as_undefined_v;
if (!options
->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "readNullAsUndefined"))
.ToLocal(&read_null_as_undefined_v)) {
return;
}
if (!read_null_as_undefined_v->IsUndefined()) {
if (!read_null_as_undefined_v->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.readNullAsUndefined\" argument must be a boolean.");
return;
}
read_null_as_undefined = read_null_as_undefined_v->IsTrue();
}
}

Utf8Value sql(env->isolate(), args[0].As<String>());
Expand All @@ -1260,7 +1295,9 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
if (allow_unknown_named_params.has_value()) {
stmt->allow_unknown_named_params_ = allow_unknown_named_params.value();
}

if (read_null_as_undefined.has_value()) {
stmt->read_null_as_undefined_ = read_null_as_undefined.value();
}
args.GetReturnValue().Set(stmt->object());
}

Expand Down Expand Up @@ -2136,6 +2173,7 @@ StatementSync::StatementSync(Environment* env,
return_arrays_ = db_->return_arrays();
allow_bare_named_params_ = db_->allow_bare_named_params();
allow_unknown_named_params_ = db_->allow_unknown_named_params();
read_null_as_undefined_ = db_->read_null_as_undefined();

bare_named_params_ = std::nullopt;
}
Expand Down
23 changes: 23 additions & 0 deletions test/parallel/test-sqlite-database-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,29 @@ 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('allows reading NULL as undefined', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath, { readNullAsUndefined: true });
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 val FROM data WHERE key = 1');
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: undefined });
});

test('has sqlite-type symbol property', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath);
Expand Down
135 changes: 135 additions & 0 deletions test/parallel/test-sqlite-statement-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -959,3 +959,138 @@ suite('StatementSync.prototype.setReadNullAsUndefined()', () => {
});
});

suite('options.readNullAsUndefined', () => {
test('undefined is returned when input is true', (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 val FROM data WHERE key = 1',
{ readNullAsUndefined: true }
);
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: undefined });
});

test('null is returned when input is false', (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 val FROM data WHERE key = 1',
{ readNullAsUndefined: false }
);
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: null });
});

test('null is returned by default', (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 val FROM data WHERE key = 1');
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 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(key INTEGER PRIMARY KEY, val TEXT) STRICT;'
);
t.assert.strictEqual(setup, undefined);
t.assert.throws(() => {
db.prepare('SELECT val FROM data', { readNullAsUndefined: 'true' });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.readNullAsUndefined" argument must be a boolean/,
});
});

test('setReadNullAsUndefined can override prepare option', (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 val FROM data WHERE key = 1',
{ readNullAsUndefined: true }
);
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: undefined });
t.assert.strictEqual(query.setReadNullAsUndefined(false), undefined);
t.assert.deepStrictEqual(query.get(), { __proto__: null, val: null });
});

test('all() returns undefined when input is true', (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);
INSERT INTO data (key, val) VALUES (2, 'two');
`);
t.assert.strictEqual(setup, undefined);

const query = db.prepare(
'SELECT key, val FROM data ORDER BY key',
{ readNullAsUndefined: true }
);
t.assert.deepStrictEqual(query.all(), [
{ __proto__: null, key: 1, val: undefined },
{ __proto__: null, key: 2, val: 'two' },
]);
});

test('iterate() returns undefined when input is true', (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);
INSERT INTO data (key, val) VALUES (2, NULL);
`);
t.assert.strictEqual(setup, undefined);

const query = db.prepare(
'SELECT key, val FROM data ORDER BY key',
{ readNullAsUndefined: true }
);
t.assert.deepStrictEqual(query.iterate().toArray(), [
{ __proto__: null, key: 1, val: undefined },
{ __proto__: null, key: 2, val: undefined },
]);
});

test('works with returnArrays option', (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',
{ readNullAsUndefined: true, returnArrays: true }
);
t.assert.deepStrictEqual(query.get(), [1, undefined]);
});
});