Skip to content

Commit 47bbca8

Browse files
sqlite: bind undefined to NULL
Omitting a named parameter binds NULL, but passing `undefined` for that same parameter threw ERR_INVALID_ARG_TYPE. Bind `undefined` to NULL so the two forms agree. This matches the conversion already applied to a user-defined function's `undefined` return value, as well as SQLite's own WASM oo1 API. Fixes: #61824 Refs: #61472 Refs: #62008 Co-authored-by: mike-git374 <217764531+mike-git374@users.noreply.github.com> Assisted-by: claude:opus-5 Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
1 parent f9ab994 commit 47bbca8

3 files changed

Lines changed: 117 additions & 12 deletions

File tree

doc/api/sqlite.md

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ exception.
101101

102102
| Storage class | JavaScript to SQLite | SQLite to JavaScript |
103103
| ------------- | --------------------------------------------------------------- | ------------------------------------- |
104-
| `NULL` | {null} | {null} |
104+
| `NULL` | {null} or {undefined} | {null} |
105105
| `INTEGER` | {number}, {bigint}, or {boolean} | {number} or {bigint} _(configurable)_ |
106106
| `REAL` | {number} | {number} |
107107
| `TEXT` | {string} | {string} |
@@ -113,6 +113,10 @@ values (`1n` and `0n`) when reading BigInts is enabled. Writing a {bigint} that
113113
does not fit in a signed 64-bit integer throws an `ERR_INVALID_ARG_VALUE`
114114
error.
115115

116+
`undefined` is written as `NULL`, so passing it explicitly is equivalent to
117+
omitting a named parameter. `NULL` always reads back as {null}, never
118+
`undefined`.
119+
116120
APIs that read values from SQLite have a configuration option that determines
117121
whether `INTEGER` values are converted to `number` or `bigint` in JavaScript,
118122
such as the `readBigInts` option for statements and the `useBigIntArguments`
@@ -1089,6 +1093,11 @@ Binding a key that does not name a parameter of the statement throws an
10891093
`ERR_INVALID_STATE` error unless unknown named parameters are ignored. See
10901094
[`statement.setAllowUnknownNamedParameters()`][].
10911095

1096+
Parameters that are never bound are `NULL`, and binding `undefined` has the same
1097+
effect, so `{ $a: undefined }` and `{}` are equivalent. Because `undefined` is
1098+
not an object, passing it in place of `namedParameters` binds it as an anonymous
1099+
parameter instead.
1100+
10921101
See [Type conversion between JavaScript and SQLite][] for the values that can be
10931102
bound. Binding any other value throws an `ERR_INVALID_ARG_TYPE` error.
10941103

@@ -1097,6 +1106,9 @@ bound. Binding any other value throws an `ERR_INVALID_ARG_TYPE` error.
10971106
<!-- YAML
10981107
added: v22.5.0
10991108
changes:
1109+
- version: REPLACEME
1110+
pr-url: https://github.com/nodejs/node/pull/65709
1111+
description: Bind `undefined` to `NULL`.
11001112
- version: v26.8.0
11011113
pr-url: https://github.com/nodejs/node/pull/62001
11021114
description: Add support for boolean values in bound parameters.
@@ -1112,7 +1124,8 @@ changes:
11121124

11131125
* `namedParameters` {Object} An optional object used to bind named parameters.
11141126
The keys of this object are used to configure the mapping.
1115-
* `...anonymousParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
1127+
* `...anonymousParameters`
1128+
{undefined|null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
11161129
Zero or more values to bind to anonymous parameters.
11171130
* Returns: {Array} An array of objects. Each object corresponds to a row
11181131
returned by executing the prepared statement. The keys and values of each
@@ -1185,6 +1198,9 @@ execution of this prepared statement. This property is a wrapper around
11851198
<!-- YAML
11861199
added: v22.5.0
11871200
changes:
1201+
- version: REPLACEME
1202+
pr-url: https://github.com/nodejs/node/pull/65709
1203+
description: Bind `undefined` to `NULL`.
11881204
- version: v26.8.0
11891205
pr-url: https://github.com/nodejs/node/pull/62001
11901206
description: Add support for boolean values in bound parameters.
@@ -1200,7 +1216,8 @@ changes:
12001216

12011217
* `namedParameters` {Object} An optional object used to bind named parameters.
12021218
The keys of this object are used to configure the mapping.
1203-
* `...anonymousParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
1219+
* `...anonymousParameters`
1220+
{undefined|null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
12041221
Zero or more values to bind to anonymous parameters.
12051222
* Returns: {Object|undefined} An object corresponding to the first row returned
12061223
by executing the prepared statement. The keys and values of the object
@@ -1220,6 +1237,9 @@ added:
12201237
- v23.4.0
12211238
- v22.13.0
12221239
changes:
1240+
- version: REPLACEME
1241+
pr-url: https://github.com/nodejs/node/pull/65709
1242+
description: Bind `undefined` to `NULL`.
12231243
- version: v26.8.0
12241244
pr-url: https://github.com/nodejs/node/pull/62001
12251245
description: Add support for boolean values in bound parameters.
@@ -1235,7 +1255,8 @@ changes:
12351255

12361256
* `namedParameters` {Object} An optional object used to bind named parameters.
12371257
The keys of this object are used to configure the mapping.
1238-
* `...anonymousParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
1258+
* `...anonymousParameters`
1259+
{undefined|null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
12391260
Zero or more values to bind to anonymous parameters.
12401261
* Returns: {Iterator} An iterable iterator of objects. Each object corresponds to a row
12411262
returned by executing the prepared statement. The keys and values of each
@@ -1264,6 +1285,9 @@ executions of the same prepared statement.
12641285
<!-- YAML
12651286
added: v22.5.0
12661287
changes:
1288+
- version: REPLACEME
1289+
pr-url: https://github.com/nodejs/node/pull/65709
1290+
description: Bind `undefined` to `NULL`.
12671291
- version: v26.8.0
12681292
pr-url: https://github.com/nodejs/node/pull/62001
12691293
description: Add support for boolean values in bound parameters.
@@ -1279,7 +1303,8 @@ changes:
12791303

12801304
* `namedParameters` {Object} An optional object used to bind named parameters.
12811305
The keys of this object are used to configure the mapping.
1282-
* `...anonymousParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
1306+
* `...anonymousParameters`
1307+
{undefined|null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
12831308
Zero or more values to bind to anonymous parameters.
12841309
* Returns: {Object}
12851310
* `changes` {number|bigint} The number of rows modified, inserted, or deleted
@@ -1448,6 +1473,9 @@ class execute synchronously.
14481473
<!-- YAML
14491474
added: v24.9.0
14501475
changes:
1476+
- version: REPLACEME
1477+
pr-url: https://github.com/nodejs/node/pull/65709
1478+
description: Bind `undefined` to `NULL`.
14511479
- version: v26.8.0
14521480
pr-url: https://github.com/nodejs/node/pull/62001
14531481
description: Add support for boolean values in bound parameters.
@@ -1458,7 +1486,8 @@ changes:
14581486

14591487
* `stringElements` {string\[]} Template literal elements containing the SQL
14601488
query.
1461-
* `...boundParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
1489+
* `...boundParameters`
1490+
{undefined|null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
14621491
Parameter values to be bound to placeholders in the template string.
14631492
* Returns: {Array} An array of objects representing the rows returned by the query.
14641493

@@ -1473,6 +1502,9 @@ called directly.
14731502
<!-- YAML
14741503
added: v24.9.0
14751504
changes:
1505+
- version: REPLACEME
1506+
pr-url: https://github.com/nodejs/node/pull/65709
1507+
description: Bind `undefined` to `NULL`.
14761508
- version: v26.8.0
14771509
pr-url: https://github.com/nodejs/node/pull/62001
14781510
description: Add support for boolean values in bound parameters.
@@ -1483,7 +1515,8 @@ changes:
14831515

14841516
* `stringElements` {string\[]} Template literal elements containing the SQL
14851517
query.
1486-
* `...boundParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
1518+
* `...boundParameters`
1519+
{undefined|null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
14871520
Parameter values to be bound to placeholders in the template string.
14881521
* Returns: {Object | undefined} An object representing the first row returned by
14891522
the query, or `undefined` if no rows are returned.
@@ -1498,6 +1531,9 @@ called directly.
14981531
<!-- YAML
14991532
added: v24.9.0
15001533
changes:
1534+
- version: REPLACEME
1535+
pr-url: https://github.com/nodejs/node/pull/65709
1536+
description: Bind `undefined` to `NULL`.
15011537
- version: v26.8.0
15021538
pr-url: https://github.com/nodejs/node/pull/62001
15031539
description: Add support for boolean values in bound parameters.
@@ -1508,7 +1544,8 @@ changes:
15081544

15091545
* `stringElements` {string\[]} Template literal elements containing the SQL
15101546
query.
1511-
* `...boundParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
1547+
* `...boundParameters`
1548+
{undefined|null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
15121549
Parameter values to be bound to placeholders in the template string.
15131550
* Returns: {Iterator} An iterator that yields objects representing the rows returned by the query.
15141551

@@ -1522,6 +1559,9 @@ called directly.
15221559
<!-- YAML
15231560
added: v24.9.0
15241561
changes:
1562+
- version: REPLACEME
1563+
pr-url: https://github.com/nodejs/node/pull/65709
1564+
description: Bind `undefined` to `NULL`.
15251565
- version: v26.8.0
15261566
pr-url: https://github.com/nodejs/node/pull/62001
15271567
description: Add support for boolean values in bound parameters.
@@ -1532,7 +1572,8 @@ changes:
15321572

15331573
* `stringElements` {string\[]} Template literal elements containing the SQL
15341574
query.
1535-
* `...boundParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
1575+
* `...boundParameters`
1576+
{undefined|null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer}
15361577
Parameter values to be bound to placeholders in the template string.
15371578
* Returns: {Object} An object containing information about the execution, including `changes` and `lastInsertRowid`.
15381579

src/node_sqlite.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3101,7 +3101,8 @@ bool StatementSync::BindValue(const Local<Value>& value, const int index) {
31013101
// Dates could be supported by converting them to numbers. However, there
31023102
// would not be a good way to read the values back from SQLite with the
31033103
// original type. JS Boolean binds to 1 and 0 because SQLite maps true and
3104-
// false keywords to 1 and 0.
3104+
// false keywords to 1 and 0. JS undefined binds to NULL so that passing it
3105+
// explicitly matches omitting the parameter altogether.
31053106
Isolate* isolate = env()->isolate();
31063107
int r;
31073108
if (value->IsNumber()) {
@@ -3125,7 +3126,7 @@ bool StatementSync::BindValue(const Local<Value>& value, const int index) {
31253126
SQLITE_TRANSIENT,
31263127
SQLITE_UTF8);
31273128
}
3128-
} else if (value->IsNull()) {
3129+
} else if (value->IsNullOrUndefined()) {
31293130
r = sqlite3_bind_null(statement_.get(), index);
31303131
} else if (value->IsArrayBufferView() || value->IsArrayBuffer() ||
31313132
value->IsSharedArrayBuffer()) {

test/parallel/test-sqlite-data-types.js

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,70 @@ suite('data binding and mapping', () => {
8080
query.get(5),
8181
{ __proto__: null, key: 5, int: 1, double: 0, text: '1', buf: null }
8282
);
83+
84+
t.assert.deepStrictEqual(
85+
stmt.run(6, undefined, undefined, undefined, undefined),
86+
{ changes: 1, lastInsertRowid: 6 }
87+
);
88+
t.assert.deepStrictEqual(
89+
query.get(6),
90+
{ __proto__: null, key: 6, int: null, double: null, text: null, buf: null }
91+
);
92+
});
93+
94+
test('undefined is bound as NULL', (t) => {
95+
const db = new DatabaseSync(':memory:');
96+
t.after(() => { db.close(); });
97+
const setup = db.exec(
98+
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
99+
);
100+
t.assert.strictEqual(setup, undefined);
101+
const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)');
102+
const query = db.prepare('SELECT * FROM types WHERE key = ?');
103+
104+
// An explicit `undefined` and an omitted named parameter are equivalent.
105+
t.assert.deepStrictEqual(
106+
stmt.run({ k: 1, v: undefined }),
107+
{ changes: 1, lastInsertRowid: 1 }
108+
);
109+
t.assert.deepStrictEqual(
110+
stmt.run({ k: 2 }),
111+
{ changes: 1, lastInsertRowid: 2 }
112+
);
113+
t.assert.deepStrictEqual(
114+
query.get(1),
115+
{ __proto__: null, key: 1, val: null }
116+
);
117+
t.assert.deepStrictEqual(
118+
query.get(2),
119+
{ __proto__: null, key: 2, val: null }
120+
);
121+
122+
t.assert.deepStrictEqual(
123+
db.prepare('SELECT ? AS a').get(undefined),
124+
{ __proto__: null, a: null }
125+
);
126+
});
127+
128+
test('undefined is not treated as the named parameters argument', (t) => {
129+
const db = new DatabaseSync(':memory:');
130+
t.after(() => { db.close(); });
131+
132+
// `undefined` is not an object, so it is bound as an anonymous parameter
133+
// rather than being treated as the named parameters argument.
134+
t.assert.throws(() => {
135+
db.prepare('SELECT $k AS a').get(undefined);
136+
}, {
137+
code: 'ERR_SQLITE_ERROR',
138+
message: /column index out of range/,
139+
});
140+
141+
t.assert.throws(() => {
142+
db.prepare('SELECT ? AS a').get(1, undefined);
143+
}, {
144+
code: 'ERR_SQLITE_ERROR',
145+
message: /column index out of range/,
146+
});
83147
});
84148

85149
test('large strings are bound correctly', (t) => {
@@ -126,7 +190,6 @@ suite('data binding and mapping', () => {
126190
t.assert.strictEqual(setup, undefined);
127191

128192
[
129-
undefined,
130193
() => {},
131194
Symbol(),
132195
/foo/,

0 commit comments

Comments
 (0)