diff --git a/doc/api/cli.md b/doc/api/cli.md
index adc7e951f61..e9357fb1bee 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -1049,6 +1049,14 @@ added:
Use this flag to enable [ShadowRealm][] support.
+### `--experimental-sqlite`
+
+
+
+Enable the experimental [`node:sqlite`][] module.
+
### `--experimental-test-coverage`
+
+An error was returned from [SQLite][].
+
### `ERR_SRI_PARSE`
@@ -4023,6 +4033,7 @@ An error occurred trying to allocate memory. This should never happen.
[Node.js error codes]: #nodejs-error-codes
[Permission Model]: permissions.md#permission-model
[RFC 7230 Section 3]: https://tools.ietf.org/html/rfc7230#section-3
+[SQLite]: sqlite.md
[Subresource Integrity specification]: https://www.w3.org/TR/SRI/#the-integrity-attribute
[V8's stack trace API]: https://v8.dev/docs/stack-trace-api
[WHATWG Supported Encodings]: util.md#whatwg-supported-encodings
diff --git a/doc/api/index.md b/doc/api/index.md
index 81ef77491b1..51915a78d90 100644
--- a/doc/api/index.md
+++ b/doc/api/index.md
@@ -53,6 +53,7 @@
* [REPL](repl.md)
* [Report](report.md)
* [Single executable applications](single-executable-applications.md)
+* [SQLite](sqlite.md)
* [Stream](stream.md)
* [String decoder](string_decoder.md)
* [Test runner](test.md)
diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md
new file mode 100644
index 00000000000..313e793a5c5
--- /dev/null
+++ b/doc/api/sqlite.md
@@ -0,0 +1,328 @@
+# SQLite
+
+
+
+
+
+> Stability: 1.1 - Active development
+
+
+
+The `node:sqlite` module facilitates working with SQLite databases.
+To access it:
+
+```mjs
+import sqlite from 'node:sqlite';
+```
+
+```cjs
+const sqlite = require('node:sqlite');
+```
+
+This module is only available under the `node:` scheme. The following will not
+work:
+
+```mjs
+import sqlite from 'sqlite';
+```
+
+```cjs
+const sqlite = require('sqlite');
+```
+
+The following example shows the basic usage of the `node:sqlite` module to open
+an in-memory database, write data to the database, and then read the data back.
+
+```mjs
+import { DatabaseSync } from 'node:sqlite';
+const database = new DatabaseSync(':memory:');
+
+// Execute SQL statements from strings.
+database.exec(`
+ CREATE TABLE data(
+ key INTEGER PRIMARY KEY,
+ value TEXT
+ ) STRICT
+`);
+// Create a prepared statement to insert data into the database.
+const insert = database.prepare('INSERT INTO data (key, value) VALUES (?, ?)');
+// Execute the prepared statement with bound values.
+insert.run(1, 'hello');
+insert.run(2, 'world');
+// Create a prepared statement to read data from the database.
+const query = database.prepare('SELECT * FROM data ORDER BY key');
+// Execute the prepared statement and log the result set.
+console.log(query.all());
+// Prints: [ { key: 1, value: 'hello' }, { key: 2, value: 'world' } ]
+```
+
+```cjs
+'use strict';
+const { DatabaseSync } = require('node:sqlite');
+const database = new DatabaseSync(':memory:');
+
+// Execute SQL statements from strings.
+database.exec(`
+ CREATE TABLE data(
+ key INTEGER PRIMARY KEY,
+ value TEXT
+ ) STRICT
+`);
+// Create a prepared statement to insert data into the database.
+const insert = database.prepare('INSERT INTO data (key, value) VALUES (?, ?)');
+// Execute the prepared statement with bound values.
+insert.run(1, 'hello');
+insert.run(2, 'world');
+// Create a prepared statement to read data from the database.
+const query = database.prepare('SELECT * FROM data ORDER BY key');
+// Execute the prepared statement and log the result set.
+console.log(query.all());
+// Prints: [ { key: 1, value: 'hello' }, { key: 2, value: 'world' } ]
+```
+
+## Class: `DatabaseSync`
+
+
+
+This class represents a single [connection][] to a SQLite database. All APIs
+exposed by this class execute synchronously.
+
+### `new DatabaseSync(location[, options])`
+
+
+
+* `location` {string} The location of the database. A SQLite database can be
+ stored in a file or completely [in memory][]. To use a file-backed database,
+ the location should be a file path. To use an in-memory database, the location
+ should be the special name `':memory:'`.
+* `options` {Object} Configuration options for the database connection. The
+ following options are supported:
+ * `open` {boolean} If `true`, the database is opened by the constructor. When
+ this value is `false`, the database must be opened via the `open()` method.
+ **Default:** `true`.
+
+Constructs a new `DatabaseSync` instance.
+
+### `database.close()`
+
+
+
+Closes the database connection. An exception is thrown if the database is not
+open. This method is a wrapper around [`sqlite3_close_v2()`][].
+
+### `database.exec(sql)`
+
+
+
+* `sql` {string} A SQL string to execute.
+
+This method allows one or more SQL statements to be executed without returning
+any results. This method is useful when executing SQL statements read from a
+file. This method is a wrapper around [`sqlite3_exec()`][].
+
+### `database.open()`
+
+
+
+Opens the database specified in the `location` argument of the `DatabaseSync`
+constructor. This method should only be used when the database is not opened via
+the constructor. An exception is thrown if the database is already open.
+
+### `database.prepare(sql)`
+
+
+
+* `sql` {string} A SQL string to compile to a prepared statement.
+* Returns: {StatementSync} The prepared statement.
+
+Compiles a SQL statement into a [prepared statement][]. This method is a wrapper
+around [`sqlite3_prepare_v2()`][].
+
+## Class: `StatementSync`
+
+
+
+This class represents a single [prepared statement][]. This class cannot be
+instantiated via its constructor. Instead, instances are created via the
+`database.prepare()` method. All APIs exposed by this class execute
+synchronously.
+
+A prepared statement is an efficient binary representation of the SQL used to
+create it. Prepared statements are parameterizable, and can be invoked multiple
+times with different bound values. Parameters also offer protection against
+[SQL injection][] attacks. For these reasons, prepared statements are preferred
+over hand-crafted SQL strings when handling user input.
+
+### `statement.all([namedParameters][, ...anonymousParameters])`
+
+
+
+* `namedParameters` {Object} An optional object used to bind named parameters.
+ The keys of this object are used to configure the mapping.
+* `...anonymousParameters` {null|number|bigint|string|Buffer|Uint8Array} Zero or
+ more values to bind to anonymous parameters.
+* Returns: {Array} An array of objects. Each object corresponds to a row
+ returned by executing the prepared statement. The keys and values of each
+ object correspond to the column names and values of the row.
+
+This method executes a prepared statement and returns all results as an array of
+objects. If the prepared statement does not return any results, this method
+returns an empty array. The prepared statement [parameters are bound][] using
+the values in `namedParameters` and `anonymousParameters`.
+
+### `statement.expandedSQL()`
+
+
+
+* Returns: {string} The source SQL expanded to include parameter values.
+
+This method returns the source SQL of the prepared statement with parameter
+placeholders replaced by values. This method is a wrapper around
+[`sqlite3_expanded_sql()`][].
+
+### `statement.get([namedParameters][, ...anonymousParameters])`
+
+
+
+* `namedParameters` {Object} An optional object used to bind named parameters.
+ The keys of this object are used to configure the mapping.
+* `...anonymousParameters` {null|number|bigint|string|Buffer|Uint8Array} Zero or
+ more values to bind to anonymous parameters.
+* Returns: {Object|undefined} An object corresponding to the first row returned
+ by executing the prepared statement. The keys and values of the object
+ correspond to the column names and values of the row. If no rows were returned
+ from the database then this method returns `undefined`.
+
+This method executes a prepared statement and returns the first result as an
+object. If the prepared statement does not return any results, this method
+returns `undefined`. The prepared statement [parameters are bound][] using the
+values in `namedParameters` and `anonymousParameters`.
+
+### `statement.run([namedParameters][, ...anonymousParameters])`
+
+
+
+* `namedParameters` {Object} An optional object used to bind named parameters.
+ The keys of this object are used to configure the mapping.
+* `...anonymousParameters` {null|number|bigint|string|Buffer|Uint8Array} Zero or
+ more values to bind to anonymous parameters.
+* Returns: {Object}
+ * `changes`: {number|bigint} The number of rows modified, inserted, or deleted
+ by the most recently completed `INSERT`, `UPDATE`, or `DELETE` statement.
+ This field is either a number or a `BigInt` depending on the prepared
+ statement's configuration. This property is the result of
+ [`sqlite3_changes64()`][].
+ * `lastInsertRowid`: {number|bigint} The most recently inserted rowid. This
+ field is either a number or a `BigInt` depending on the prepared statement's
+ configuration. This property is the result of
+ [`sqlite3_last_insert_rowid()`][].
+
+This method executes a prepared statement and returns an object summarizing the
+resulting changes. The prepared statement [parameters are bound][] using the
+values in `namedParameters` and `anonymousParameters`.
+
+### `statement.setAllowBareNamedParameters(enabled)`
+
+
+
+* `enabled` {boolean} Enables or disables support for binding named parameters
+ without the prefix character.
+
+The names of SQLite parameters begin with a prefix character. By default,
+`node:sqlite` requires that this prefix character is present when binding
+parameters. However, with the exception of dollar sign character, these
+prefix characters also require extra quoting when used in object keys.
+
+To improve ergonomics, this method can be used to also allow bare named
+parameters, which do not require the prefix character in JavaScript code. There
+are several caveats to be aware of when enabling bare named parameters:
+
+* The prefix character is still required in SQL.
+* The prefix character is still allowed in JavaScript. In fact, prefixed names
+ will have slightly better binding performance.
+* Using ambiguous named parameters, such as `$k` and `@k`, in the same prepared
+ statement will result in an exception as it cannot be determined how to bind
+ a bare name.
+
+### `statement.setReadBigInts(enabled)`
+
+
+
+* `enabled` {boolean} Enables or disables the use of `BigInt`s when reading
+ `INTEGER` fields from the database.
+
+When reading from the database, SQLite `INTEGER`s are mapped to JavaScript
+numbers by default. However, SQLite `INTEGER`s can store values larger than
+JavaScript numbers are capable of representing. In such cases, this method can
+be used to read `INTEGER` data using JavaScript `BigInt`s. This method has no
+impact on database write operations where numbers and `BigInt`s are both
+supported at all times.
+
+### `statement.sourceSQL()`
+
+
+
+* Returns: {string} The source SQL used to create this prepared statement.
+
+This method returns the source SQL of the prepared statement. This method is a
+wrapper around [`sqlite3_sql()`][].
+
+### Type conversion between JavaScript and SQLite
+
+When Node.js writes to or reads from SQLite it is necessary to convert between
+JavaScript data types and SQLite's [data types][]. Because JavaScript supports
+more data types than SQLite, only a subset of JavaScript types are supported.
+Attempting to write an unsupported data type to SQLite will result in an
+exception.
+
+| SQLite | JavaScript |
+| --------- | -------------------- |
+| `NULL` | `null` |
+| `INTEGER` | `number` or `BigInt` |
+| `REAL` | `number` |
+| `TEXT` | `string` |
+| `BLOB` | `Uint8Array` |
+
+[SQL injection]: https://en.wikipedia.org/wiki/SQL_injection
+[`sqlite3_changes64()`]: https://www.sqlite.org/c3ref/changes.html
+[`sqlite3_close_v2()`]: https://www.sqlite.org/c3ref/close.html
+[`sqlite3_exec()`]: https://www.sqlite.org/c3ref/exec.html
+[`sqlite3_expanded_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
+[`sqlite3_last_insert_rowid()`]: https://www.sqlite.org/c3ref/last_insert_rowid.html
+[`sqlite3_prepare_v2()`]: https://www.sqlite.org/c3ref/prepare.html
+[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
+[connection]: https://www.sqlite.org/c3ref/sqlite3.html
+[data types]: https://www.sqlite.org/datatype3.html
+[in memory]: https://www.sqlite.org/inmemorydb.html
+[parameters are bound]: https://www.sqlite.org/c3ref/bind_blob.html
+[prepared statement]: https://www.sqlite.org/c3ref/stmt.html
diff --git a/doc/node.1 b/doc/node.1
index 01436ff9b71..dabf354689a 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -182,6 +182,9 @@ Enable the experimental permission model.
.It Fl -experimental-shadow-realm
Use this flag to enable ShadowRealm support.
.
+.It Fl -experimental-sqlite
+Enable the experimental node:sqlite module.
+.
.It Fl -experimental-test-coverage
Enable code coverage in the test runner.
.
diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js
index 667f1e0909d..c11f70dd6bf 100644
--- a/lib/internal/bootstrap/realm.js
+++ b/lib/internal/bootstrap/realm.js
@@ -129,11 +129,12 @@ const legacyWrapperList = new SafeSet([
// Modules that can only be imported via the node: scheme.
const schemelessBlockList = new SafeSet([
'sea',
+ 'sqlite',
'test',
'test/reporters',
]);
// Modules that will only be enabled at run time.
-const experimentalModuleList = new SafeSet();
+const experimentalModuleList = new SafeSet(['sqlite']);
// Set up process.binding() and process._linkedBinding().
{
diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js
index 27df0a9440a..2c8d77f39e1 100644
--- a/lib/internal/process/pre_execution.js
+++ b/lib/internal/process/pre_execution.js
@@ -100,6 +100,7 @@ function prepareExecution(options) {
setupInspectorHooks();
setupNavigator();
setupWarningHandler();
+ setupSQLite();
setupWebStorage();
setupWebsocket();
setupEventsource();
@@ -329,6 +330,15 @@ function setupNavigator() {
defineReplaceableLazyAttribute(globalThis, 'internal/navigator', ['navigator'], false);
}
+function setupSQLite() {
+ if (!getOptionValue('--experimental-sqlite')) {
+ return;
+ }
+
+ const { BuiltinModule } = require('internal/bootstrap/realm');
+ BuiltinModule.allowRequireByUsers('sqlite');
+}
+
function setupWebStorage() {
if (getEmbedderOptions().noBrowserGlobals ||
!getOptionValue('--experimental-webstorage')) {
diff --git a/lib/sqlite.js b/lib/sqlite.js
new file mode 100644
index 00000000000..ca3d9544f15
--- /dev/null
+++ b/lib/sqlite.js
@@ -0,0 +1,5 @@
+'use strict';
+const { emitExperimentalWarning } = require('internal/util');
+
+emitExperimentalWarning('SQLite');
+module.exports = internalBinding('sqlite');
diff --git a/node.gyp b/node.gyp
index bb3de4497f0..9617596760a 100644
--- a/node.gyp
+++ b/node.gyp
@@ -135,6 +135,7 @@
'src/node_shadow_realm.cc',
'src/node_snapshotable.cc',
'src/node_sockaddr.cc',
+ 'src/node_sqlite.cc',
'src/node_stat_watcher.cc',
'src/node_symbols.cc',
'src/node_task_queue.cc',
@@ -264,6 +265,7 @@
'src/node_snapshot_builder.h',
'src/node_sockaddr.h',
'src/node_sockaddr-inl.h',
+ 'src/node_sqlite.h',
'src/node_stat_watcher.h',
'src/node_union_bytes.h',
'src/node_url.h',
diff --git a/src/env_properties.h b/src/env_properties.h
index eb5c51b6e2b..5b322503259 100644
--- a/src/env_properties.h
+++ b/src/env_properties.h
@@ -396,6 +396,7 @@
V(secure_context_constructor_template, v8::FunctionTemplate) \
V(shutdown_wrap_template, v8::ObjectTemplate) \
V(socketaddress_constructor_template, v8::FunctionTemplate) \
+ V(sqlite_statement_sync_constructor_template, v8::FunctionTemplate) \
V(streambaseentry_ctor_template, v8::FunctionTemplate) \
V(streambaseoutputstream_constructor_template, v8::ObjectTemplate) \
V(streamentry_ctor_template, v8::FunctionTemplate) \
diff --git a/src/node_binding.cc b/src/node_binding.cc
index 79c54fcbc7b..9b75a9eeda9 100644
--- a/src/node_binding.cc
+++ b/src/node_binding.cc
@@ -67,6 +67,7 @@
V(serdes) \
V(signal_wrap) \
V(spawn_sync) \
+ V(sqlite) \
V(stream_pipe) \
V(stream_wrap) \
V(string_decoder) \
diff --git a/src/node_builtins.cc b/src/node_builtins.cc
index 1ffe2e13aae..1702ac0ac53 100644
--- a/src/node_builtins.cc
+++ b/src/node_builtins.cc
@@ -125,8 +125,9 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const {
"internal/http2/core", "internal/http2/compat",
"internal/streams/lazy_transform",
#endif // !HAVE_OPENSSL
- "sys", // Deprecated.
- "wasi", // Experimental.
+ "sqlite", // Experimental.
+ "sys", // Deprecated.
+ "wasi", // Experimental.
"internal/test/binding", "internal/v8_prof_polyfill",
"internal/v8_prof_processor",
};
diff --git a/src/node_options.cc b/src/node_options.cc
index 2e43bf8242c..1f4e402964c 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -410,6 +410,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
kAllowedInEnvvar,
true);
AddOption("--experimental-global-customevent", "", NoOp{}, kAllowedInEnvvar);
+ AddOption("--experimental-sqlite",
+ "experimental node:sqlite module",
+ &EnvironmentOptions::experimental_sqlite,
+ kAllowedInEnvvar);
AddOption("--experimental-webstorage",
"experimental Web Storage API",
&EnvironmentOptions::experimental_webstorage,
diff --git a/src/node_options.h b/src/node_options.h
index 489638739c3..7ffc7448cab 100644
--- a/src/node_options.h
+++ b/src/node_options.h
@@ -118,6 +118,7 @@ class EnvironmentOptions : public Options {
bool experimental_eventsource = false;
bool experimental_fetch = true;
bool experimental_websocket = true;
+ bool experimental_sqlite = false;
bool experimental_webstorage = false;
std::string localstorage_file;
bool experimental_global_navigator = true;
diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc
new file mode 100644
index 00000000000..cb7855a2ad1
--- /dev/null
+++ b/src/node_sqlite.cc
@@ -0,0 +1,667 @@
+#include "node_sqlite.h"
+#include "base_object-inl.h"
+#include "debug_utils-inl.h"
+#include "env-inl.h"
+#include "memory_tracker-inl.h"
+#include "node.h"
+#include "node_errors.h"
+#include "node_mem-inl.h"
+#include "sqlite3.h"
+#include "util-inl.h"
+
+#include
+
+namespace node {
+namespace sqlite {
+
+using v8::Array;
+using v8::ArrayBuffer;
+using v8::BigInt;
+using v8::Boolean;
+using v8::Context;
+using v8::Exception;
+using v8::FunctionCallbackInfo;
+using v8::FunctionTemplate;
+using v8::Integer;
+using v8::Isolate;
+using v8::Local;
+using v8::Number;
+using v8::Object;
+using v8::String;
+using v8::Uint8Array;
+using v8::Value;
+
+#define CHECK_ERROR_OR_THROW(isolate, db, expr, expected, ret) \
+ do { \
+ int r_ = (expr); \
+ if (r_ != (expected)) { \
+ THROW_ERR_SQLITE_ERROR((isolate), (db)); \
+ return (ret); \
+ } \
+ } while (0)
+
+#define THROW_AND_RETURN_ON_BAD_STATE(env, condition, msg) \
+ do { \
+ if ((condition)) { \
+ node::THROW_ERR_INVALID_STATE((env), (msg)); \
+ return; \
+ } \
+ } while (0)
+
+inline Local CreateSQLiteError(Isolate* isolate, sqlite3* db) {
+ int errcode = sqlite3_extended_errcode(db);
+ const char* errstr = sqlite3_errstr(errcode);
+ const char* errmsg = sqlite3_errmsg(db);
+ Local js_msg = String::NewFromUtf8(isolate, errmsg).ToLocalChecked();
+ Local