lib,src,test,doc: add node:sqlite module

PR-URL: https://github.com/nodejs/node/pull/53752
Fixes: https://github.com/nodejs/node/issues/53264
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Vinícius Lourenço Claro Cardoso <contact@viniciusl.com.br>
Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
This commit is contained in:
Colin Ihrig 2024-07-09 16:33:38 -04:00 committed by GitHub
parent 1a1639de3e
commit b4e8f1b6bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1872 additions and 3 deletions

View File

@ -1049,6 +1049,14 @@ added:
Use this flag to enable [ShadowRealm][] support.
### `--experimental-sqlite`
<!-- YAML
added: REPLACEME
-->
Enable the experimental [`node:sqlite`][] module.
### `--experimental-test-coverage`
<!-- YAML
@ -2852,6 +2860,7 @@ one is included in the list below.
* `--experimental-require-module`
* `--experimental-shadow-realm`
* `--experimental-specifier-resolution`
* `--experimental-sqlite`
* `--experimental-top-level-await`
* `--experimental-vm-modules`
* `--experimental-wasi-unstable-preview1`
@ -3409,6 +3418,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`dnsPromises.lookup()`]: dns.md#dnspromiseslookuphostname-options
[`import` specifier]: esm.md#import-specifiers
[`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: net.md#netgetdefaultautoselectfamilyattempttimeout
[`node:sqlite`]: sqlite.md
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
[`process.setuid()`]: process.md#processsetuidid
[`setuid(2)`]: https://man7.org/linux/man-pages/man2/setuid.2.html

View File

@ -2573,6 +2573,16 @@ disconnected socket.
A call was made and the UDP subsystem was not running.
<a id="ERR_SQLITE_ERROR"></a>
### `ERR_SQLITE_ERROR`
<!-- YAML
added: REPLACEME
-->
An error was returned from [SQLite][].
<a id="ERR_SRI_PARSE"></a>
### `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

View File

@ -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)

328
doc/api/sqlite.md Normal file
View File

@ -0,0 +1,328 @@
# SQLite
<!--introduced_in=REPLACEME-->
<!-- YAML
added: REPLACEME
-->
> Stability: 1.1 - Active development
<!-- source_link=lib/sqlite.js -->
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`
<!-- YAML
added: REPLACEME
-->
This class represents a single [connection][] to a SQLite database. All APIs
exposed by this class execute synchronously.
### `new DatabaseSync(location[, options])`
<!-- YAML
added: REPLACEME
-->
* `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()`
<!-- YAML
added: REPLACEME
-->
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)`
<!-- YAML
added: REPLACEME
-->
* `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()`
<!-- YAML
added: REPLACEME
-->
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)`
<!-- YAML
added: REPLACEME
-->
* `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`
<!-- YAML
added: REPLACEME
-->
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])`
<!-- YAML
added: REPLACEME
-->
* `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()`
<!-- YAML
added: REPLACEME
-->
* 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])`
<!-- YAML
added: REPLACEME
-->
* `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])`
<!-- YAML
added: REPLACEME
-->
* `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)`
<!-- YAML
added: REPLACEME
-->
* `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)`
<!-- YAML
added: REPLACEME
-->
* `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()`
<!-- YAML
added: REPLACEME
-->
* 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

View File

@ -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.
.

View File

@ -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().
{

View File

@ -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')) {

5
lib/sqlite.js Normal file
View File

@ -0,0 +1,5 @@
'use strict';
const { emitExperimentalWarning } = require('internal/util');
emitExperimentalWarning('SQLite');
module.exports = internalBinding('sqlite');

View File

@ -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',

View File

@ -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) \

View File

@ -67,6 +67,7 @@
V(serdes) \
V(signal_wrap) \
V(spawn_sync) \
V(sqlite) \
V(stream_pipe) \
V(stream_wrap) \
V(string_decoder) \

View File

@ -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",
};

View File

@ -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,

View File

@ -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;

667
src/node_sqlite.cc Normal file
View File

@ -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 <cinttypes>
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<Value> CreateSQLiteError(Isolate* isolate, sqlite3* db) {
int errcode = sqlite3_extended_errcode(db);
const char* errstr = sqlite3_errstr(errcode);
const char* errmsg = sqlite3_errmsg(db);
Local<String> js_msg = String::NewFromUtf8(isolate, errmsg).ToLocalChecked();
Local<Object> e = Exception::Error(js_msg)
->ToObject(isolate->GetCurrentContext())
.ToLocalChecked();
e->Set(isolate->GetCurrentContext(),
OneByteString(isolate, "code"),
OneByteString(isolate, "ERR_SQLITE_ERROR"))
.Check();
e->Set(isolate->GetCurrentContext(),
OneByteString(isolate, "errcode"),
Integer::New(isolate, errcode))
.Check();
e->Set(isolate->GetCurrentContext(),
OneByteString(isolate, "errstr"),
String::NewFromUtf8(isolate, errstr).ToLocalChecked())
.Check();
return e;
}
inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, sqlite3* db) {
isolate->ThrowException(CreateSQLiteError(isolate, db));
}
DatabaseSync::DatabaseSync(Environment* env,
Local<Object> object,
Local<String> location,
bool open)
: BaseObject(env, object) {
MakeWeak();
node::Utf8Value utf8_location(env->isolate(), location);
location_ = utf8_location.ToString();
connection_ = nullptr;
if (open) {
Open();
}
}
DatabaseSync::~DatabaseSync() {
sqlite3_close_v2(connection_);
connection_ = nullptr;
}
void DatabaseSync::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("location", location_);
}
bool DatabaseSync::Open() {
if (connection_ != nullptr) {
node::THROW_ERR_INVALID_STATE(env(), "database is already open");
return false;
}
// TODO(cjihrig): Support additional flags.
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
int r = sqlite3_open_v2(location_.c_str(), &connection_, flags, nullptr);
CHECK_ERROR_OR_THROW(env()->isolate(), connection_, r, SQLITE_OK, false);
return true;
}
void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (!args.IsConstructCall()) {
THROW_ERR_CONSTRUCT_CALL_REQUIRED(env);
return;
}
if (!args[0]->IsString()) {
node::THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"path\" argument must be a string.");
return;
}
bool open = true;
if (args.Length() > 1) {
if (!args[1]->IsObject()) {
node::THROW_ERR_INVALID_ARG_TYPE(
env->isolate(), "The \"options\" argument must be an object.");
return;
}
Local<Object> options = args[1].As<Object>();
Local<String> open_string = FIXED_ONE_BYTE_STRING(env->isolate(), "open");
Local<Value> open_v;
if (!options->Get(env->context(), open_string).ToLocal(&open_v)) {
return;
}
if (!open_v->IsUndefined()) {
if (!open_v->IsBoolean()) {
node::THROW_ERR_INVALID_ARG_TYPE(
env->isolate(), "The \"options.open\" argument must be a boolean.");
return;
}
open = open_v.As<Boolean>()->Value();
}
}
new DatabaseSync(env, args.This(), args[0].As<String>(), open);
}
void DatabaseSync::Open(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
db->Open();
}
void DatabaseSync::Close(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(
env, db->connection_ == nullptr, "database is not open");
int r = sqlite3_close_v2(db->connection_);
CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void());
db->connection_ = nullptr;
}
void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(
env, db->connection_ == nullptr, "database is not open");
if (!args[0]->IsString()) {
node::THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"sql\" argument must be a string.");
return;
}
auto sql = node::Utf8Value(env->isolate(), args[0].As<String>());
sqlite3_stmt* s = nullptr;
int r = sqlite3_prepare_v2(db->connection_, *sql, -1, &s, 0);
CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void());
BaseObjectPtr<StatementSync> stmt =
StatementSync::Create(env, db->connection_, s);
args.GetReturnValue().Set(stmt->object());
}
void DatabaseSync::Exec(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(
env, db->connection_ == nullptr, "database is not open");
if (!args[0]->IsString()) {
node::THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"sql\" argument must be a string.");
return;
}
auto sql = node::Utf8Value(env->isolate(), args[0].As<String>());
int r = sqlite3_exec(db->connection_, *sql, nullptr, nullptr, nullptr);
CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void());
}
StatementSync::StatementSync(Environment* env,
Local<Object> object,
sqlite3* db,
sqlite3_stmt* stmt)
: BaseObject(env, object) {
MakeWeak();
db_ = db;
statement_ = stmt;
// In the future, some of these options could be set at the database
// connection level and inherited by statements to reduce boilerplate.
use_big_ints_ = false;
allow_bare_named_params_ = true;
bare_named_params_ = std::nullopt;
}
StatementSync::~StatementSync() {
sqlite3_finalize(statement_);
statement_ = nullptr;
}
bool StatementSync::BindParams(const FunctionCallbackInfo<Value>& args) {
int r = sqlite3_clear_bindings(statement_);
CHECK_ERROR_OR_THROW(env()->isolate(), db_, r, SQLITE_OK, false);
int anon_idx = 1;
int anon_start = 0;
if (args[0]->IsObject() && !args[0]->IsUint8Array()) {
Local<Object> obj = args[0].As<Object>();
Local<Context> context = obj->GetIsolate()->GetCurrentContext();
Local<Array> keys;
if (!obj->GetOwnPropertyNames(context).ToLocal(&keys)) {
return false;
}
if (allow_bare_named_params_ && !bare_named_params_.has_value()) {
bare_named_params_.emplace();
int param_count = sqlite3_bind_parameter_count(statement_);
// Parameter indexing starts at one.
for (int i = 1; i <= param_count; ++i) {
const char* name = sqlite3_bind_parameter_name(statement_, i);
if (name == nullptr) {
continue;
}
auto bare_name = std::string(name + 1);
auto full_name = std::string(name);
auto insertion = bare_named_params_->insert({bare_name, full_name});
if (insertion.second == false) {
auto existing_full_name = (*insertion.first).second;
if (full_name != existing_full_name) {
node::THROW_ERR_INVALID_STATE(
env(),
"Cannot create bare named parameter '%s' because of "
"conflicting names '%s' and '%s'.",
bare_name,
existing_full_name,
full_name);
return false;
}
}
}
}
uint32_t len = keys->Length();
for (uint32_t j = 0; j < len; j++) {
Local<Value> key;
if (!keys->Get(context, j).ToLocal(&key)) {
return false;
}
auto utf8_key = node::Utf8Value(env()->isolate(), key);
int r = sqlite3_bind_parameter_index(statement_, *utf8_key);
if (r == 0) {
if (allow_bare_named_params_) {
auto lookup = bare_named_params_->find(std::string(*utf8_key));
if (lookup != bare_named_params_->end()) {
r = sqlite3_bind_parameter_index(statement_,
lookup->second.c_str());
}
}
if (r == 0) {
node::THROW_ERR_INVALID_STATE(
env(), "Unknown named parameter '%s'", *utf8_key);
return false;
}
}
Local<Value> value;
if (!obj->Get(context, key).ToLocal(&value)) {
return false;
}
if (!BindValue(value, r)) {
return false;
}
}
anon_start++;
}
for (int i = anon_start; i < args.Length(); ++i) {
while (sqlite3_bind_parameter_name(statement_, anon_idx) != nullptr) {
anon_idx++;
}
if (!BindValue(args[i], anon_idx)) {
return false;
}
anon_idx++;
}
return true;
}
bool StatementSync::BindValue(const Local<Value>& value, const int index) {
// SQLite only supports a subset of JavaScript types. Some JS types such as
// functions don't make sense to support. Other JS types such as booleans and
// Dates could be supported by converting them to numbers. However, there
// would not be a good way to read the values back from SQLite with the
// original type.
int r;
if (value->IsNumber()) {
double val = value.As<Number>()->Value();
r = sqlite3_bind_double(statement_, index, val);
} else if (value->IsString()) {
auto val = node::Utf8Value(env()->isolate(), value.As<String>());
r = sqlite3_bind_text(
statement_, index, *val, val.length(), SQLITE_TRANSIENT);
} else if (value->IsNull()) {
r = sqlite3_bind_null(statement_, index);
} else if (value->IsUint8Array()) {
ArrayBufferViewContents<uint8_t> buf(value);
r = sqlite3_bind_blob(
statement_, index, buf.data(), buf.length(), SQLITE_TRANSIENT);
} else if (value->IsBigInt()) {
bool lossless;
int64_t as_int = value.As<BigInt>()->Int64Value(&lossless);
if (!lossless) {
node::THROW_ERR_INVALID_ARG_VALUE(env(),
"BigInt value is too large to bind.");
return false;
}
r = sqlite3_bind_int64(statement_, index, as_int);
} else {
node::THROW_ERR_INVALID_ARG_TYPE(
env()->isolate(),
"Provided value cannot be bound to SQLite parameter %d.",
index);
return false;
}
CHECK_ERROR_OR_THROW(env()->isolate(), db_, r, SQLITE_OK, false);
return true;
}
Local<Value> StatementSync::ColumnToValue(const int column) {
switch (sqlite3_column_type(statement_, column)) {
case SQLITE_INTEGER:
if (use_big_ints_) {
return BigInt::New(env()->isolate(),
sqlite3_column_int64(statement_, column));
}
// Fall through.
case SQLITE_FLOAT:
return Number::New(env()->isolate(),
sqlite3_column_double(statement_, column));
case SQLITE_TEXT: {
const char* value = reinterpret_cast<const char*>(
sqlite3_column_text(statement_, column));
Local<Value> val;
if (!String::NewFromUtf8(env()->isolate(), value).ToLocal(&val)) {
return Local<Value>();
}
return val;
}
case SQLITE_NULL:
return v8::Null(env()->isolate());
case SQLITE_BLOB: {
size_t size =
static_cast<size_t>(sqlite3_column_bytes(statement_, column));
auto data = reinterpret_cast<const uint8_t*>(
sqlite3_column_blob(statement_, column));
auto store = ArrayBuffer::NewBackingStore(env()->isolate(), size);
memcpy(store->Data(), data, size);
auto ab = ArrayBuffer::New(env()->isolate(), std::move(store));
return Uint8Array::New(ab, 0, size);
}
default:
UNREACHABLE("Bad SQLite column type");
}
}
Local<Value> StatementSync::ColumnNameToValue(const int column) {
const char* col_name = sqlite3_column_name(statement_, column);
if (col_name == nullptr) {
node::THROW_ERR_INVALID_STATE(
env(), "Cannot get name of column %d", column);
return Local<Value>();
}
Local<String> key;
if (!String::NewFromUtf8(env()->isolate(), col_name).ToLocal(&key)) {
return Local<Value>();
}
return key;
}
void StatementSync::MemoryInfo(MemoryTracker* tracker) const {}
void StatementSync::All(const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
Environment* env = Environment::GetCurrent(args);
int r = sqlite3_reset(stmt->statement_);
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_, r, SQLITE_OK, void());
if (!stmt->BindParams(args)) {
return;
}
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); });
int num_cols = sqlite3_column_count(stmt->statement_);
std::vector<Local<Value>> rows;
while ((r = sqlite3_step(stmt->statement_)) == SQLITE_ROW) {
Local<Object> row = Object::New(env->isolate());
for (int i = 0; i < num_cols; ++i) {
Local<Value> key = stmt->ColumnNameToValue(i);
Local<Value> val = stmt->ColumnToValue(i);
if (row->Set(env->context(), key, val).IsNothing()) {
return;
}
}
rows.emplace_back(row);
}
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_, r, SQLITE_DONE, void());
args.GetReturnValue().Set(
Array::New(env->isolate(), rows.data(), rows.size()));
}
void StatementSync::Get(const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
Environment* env = Environment::GetCurrent(args);
int r = sqlite3_reset(stmt->statement_);
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_, r, SQLITE_OK, void());
if (!stmt->BindParams(args)) {
return;
}
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); });
r = sqlite3_step(stmt->statement_);
if (r != SQLITE_ROW && r != SQLITE_DONE) {
THROW_ERR_SQLITE_ERROR(env->isolate(), stmt->db_);
return;
}
int num_cols = sqlite3_column_count(stmt->statement_);
if (num_cols == 0) {
return;
}
Local<Object> result = Object::New(env->isolate());
for (int i = 0; i < num_cols; ++i) {
Local<Value> key = stmt->ColumnNameToValue(i);
Local<Value> val = stmt->ColumnToValue(i);
if (result->Set(env->context(), key, val).IsNothing()) {
return;
}
}
args.GetReturnValue().Set(result);
}
void StatementSync::Run(const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
Environment* env = Environment::GetCurrent(args);
int r = sqlite3_reset(stmt->statement_);
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_, r, SQLITE_OK, void());
if (!stmt->BindParams(args)) {
return;
}
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); });
r = sqlite3_step(stmt->statement_);
if (r != SQLITE_ROW && r != SQLITE_DONE) {
THROW_ERR_SQLITE_ERROR(env->isolate(), stmt->db_);
return;
}
Local<Object> result = Object::New(env->isolate());
Local<String> last_insert_rowid_string =
FIXED_ONE_BYTE_STRING(env->isolate(), "lastInsertRowid");
Local<String> changes_string =
FIXED_ONE_BYTE_STRING(env->isolate(), "changes");
sqlite3_int64 last_insert_rowid = sqlite3_last_insert_rowid(stmt->db_);
sqlite3_int64 changes = sqlite3_changes64(stmt->db_);
Local<Value> last_insert_rowid_val;
Local<Value> changes_val;
if (stmt->use_big_ints_) {
last_insert_rowid_val = BigInt::New(env->isolate(), last_insert_rowid);
changes_val = BigInt::New(env->isolate(), changes);
} else {
last_insert_rowid_val = Number::New(env->isolate(), last_insert_rowid);
changes_val = Number::New(env->isolate(), changes);
}
if (result
->Set(env->context(), last_insert_rowid_string, last_insert_rowid_val)
.IsNothing() ||
result->Set(env->context(), changes_string, changes_val).IsNothing()) {
return;
}
args.GetReturnValue().Set(result);
}
void StatementSync::SourceSQL(const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
Environment* env = Environment::GetCurrent(args);
Local<String> sql;
if (!String::NewFromUtf8(env->isolate(), sqlite3_sql(stmt->statement_))
.ToLocal(&sql)) {
return;
}
args.GetReturnValue().Set(sql);
}
void StatementSync::ExpandedSQL(const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
Environment* env = Environment::GetCurrent(args);
char* expanded = sqlite3_expanded_sql(stmt->statement_);
auto maybe_expanded = String::NewFromUtf8(env->isolate(), expanded);
sqlite3_free(expanded);
Local<String> result;
if (!maybe_expanded.ToLocal(&result)) {
return;
}
args.GetReturnValue().Set(result);
}
void StatementSync::SetAllowBareNamedParameters(
const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
Environment* env = Environment::GetCurrent(args);
if (!args[0]->IsBoolean()) {
node::THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"allowBareNamedParameters\" argument must be a boolean.");
return;
}
stmt->allow_bare_named_params_ = args[0]->IsTrue();
}
void StatementSync::SetReadBigInts(const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
Environment* env = Environment::GetCurrent(args);
if (!args[0]->IsBoolean()) {
node::THROW_ERR_INVALID_ARG_TYPE(
env->isolate(), "The \"readBigInts\" argument must be a boolean.");
return;
}
stmt->use_big_ints_ = args[0]->IsTrue();
}
void IllegalConstructor(const FunctionCallbackInfo<Value>& args) {
node::THROW_ERR_ILLEGAL_CONSTRUCTOR(Environment::GetCurrent(args));
}
Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
Environment* env) {
Local<FunctionTemplate> tmpl =
env->sqlite_statement_sync_constructor_template();
if (tmpl.IsEmpty()) {
Isolate* isolate = env->isolate();
tmpl = NewFunctionTemplate(isolate, IllegalConstructor);
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "StatementSync"));
tmpl->InstanceTemplate()->SetInternalFieldCount(
StatementSync::kInternalFieldCount);
SetProtoMethod(isolate, tmpl, "all", StatementSync::All);
SetProtoMethod(isolate, tmpl, "get", StatementSync::Get);
SetProtoMethod(isolate, tmpl, "run", StatementSync::Run);
SetProtoMethod(isolate, tmpl, "sourceSQL", StatementSync::SourceSQL);
SetProtoMethod(isolate, tmpl, "expandedSQL", StatementSync::ExpandedSQL);
SetProtoMethod(isolate,
tmpl,
"setAllowBareNamedParameters",
StatementSync::SetAllowBareNamedParameters);
SetProtoMethod(
isolate, tmpl, "setReadBigInts", StatementSync::SetReadBigInts);
env->set_sqlite_statement_sync_constructor_template(tmpl);
}
return tmpl;
}
BaseObjectPtr<StatementSync> StatementSync::Create(Environment* env,
sqlite3* db,
sqlite3_stmt* stmt) {
Local<Object> obj;
if (!GetConstructorTemplate(env)
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return BaseObjectPtr<StatementSync>();
}
return MakeBaseObject<StatementSync>(env, obj, db, stmt);
}
static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate();
Local<FunctionTemplate> db_tmpl =
NewFunctionTemplate(isolate, DatabaseSync::New);
db_tmpl->InstanceTemplate()->SetInternalFieldCount(
DatabaseSync::kInternalFieldCount);
SetProtoMethod(isolate, db_tmpl, "open", DatabaseSync::Open);
SetProtoMethod(isolate, db_tmpl, "close", DatabaseSync::Close);
SetProtoMethod(isolate, db_tmpl, "prepare", DatabaseSync::Prepare);
SetProtoMethod(isolate, db_tmpl, "exec", DatabaseSync::Exec);
SetConstructorFunction(context, target, "DatabaseSync", db_tmpl);
SetConstructorFunction(context,
target,
"StatementSync",
StatementSync::GetConstructorTemplate(env));
}
} // namespace sqlite
} // namespace node
NODE_BINDING_CONTEXT_AWARE_INTERNAL(sqlite, node::sqlite::Initialize)

81
src/node_sqlite.h Normal file
View File

@ -0,0 +1,81 @@
#ifndef SRC_NODE_SQLITE_H_
#define SRC_NODE_SQLITE_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "base_object.h"
#include "node_mem.h"
#include "sqlite3.h"
#include "util.h"
#include <map>
namespace node {
namespace sqlite {
class DatabaseSync : public BaseObject {
public:
DatabaseSync(Environment* env,
v8::Local<v8::Object> object,
v8::Local<v8::String> location,
bool open);
void MemoryInfo(MemoryTracker* tracker) const override;
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Open(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Close(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Prepare(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Exec(const v8::FunctionCallbackInfo<v8::Value>& args);
SET_MEMORY_INFO_NAME(DatabaseSync)
SET_SELF_SIZE(DatabaseSync)
private:
bool Open();
~DatabaseSync() override;
std::string location_;
sqlite3* connection_;
};
class StatementSync : public BaseObject {
public:
StatementSync(Environment* env,
v8::Local<v8::Object> object,
sqlite3* db,
sqlite3_stmt* stmt);
void MemoryInfo(MemoryTracker* tracker) const override;
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
Environment* env);
static BaseObjectPtr<StatementSync> Create(Environment* env,
sqlite3* db,
sqlite3_stmt* stmt);
static void All(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Get(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Run(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SourceSQL(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ExpandedSQL(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetAllowBareNamedParameters(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetReadBigInts(const v8::FunctionCallbackInfo<v8::Value>& args);
SET_MEMORY_INFO_NAME(StatementSync)
SET_SELF_SIZE(StatementSync)
private:
~StatementSync() override;
sqlite3* db_;
sqlite3_stmt* statement_;
bool use_big_ints_;
bool allow_bare_named_params_;
std::optional<std::map<std::string, std::string>> bare_named_params_;
bool BindParams(const v8::FunctionCallbackInfo<v8::Value>& args);
bool BindValue(const v8::Local<v8::Value>& value, const int index);
v8::Local<v8::Value> ColumnToValue(const int column);
v8::Local<v8::Value> ColumnNameToValue(const int column);
};
} // namespace sqlite
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_NODE_SQLITE_H_

View File

@ -0,0 +1,740 @@
// Flags: --experimental-sqlite
'use strict';
const { spawnPromisified } = require('../common');
const tmpdir = require('../common/tmpdir');
const { existsSync } = require('node:fs');
const { join } = require('node:path');
const { DatabaseSync, StatementSync } = require('node:sqlite');
const { suite, test } = require('node:test');
let cnt = 0;
tmpdir.refresh();
function nextDb() {
return join(tmpdir.path, `database-${cnt++}.db`);
}
suite('accessing the node:sqlite module', () => {
test('cannot be accessed without the node: scheme', (t) => {
t.assert.throws(() => {
require('sqlite');
}, {
code: 'MODULE_NOT_FOUND',
message: /Cannot find module 'sqlite'/,
});
});
test('cannot be accessed without --experimental-sqlite flag', async (t) => {
const {
stdout,
stderr,
code,
signal,
} = await spawnPromisified(process.execPath, [
'-e',
'require("node:sqlite")',
]);
t.assert.strictEqual(stdout, '');
t.assert.match(stderr, /No such built-in module: node:sqlite/);
t.assert.notStrictEqual(code, 0);
t.assert.strictEqual(signal, null);
});
});
suite('DatabaseSync() constructor', () => {
test('throws if called without new', (t) => {
t.assert.throws(() => {
DatabaseSync();
}, {
code: 'ERR_CONSTRUCT_CALL_REQUIRED',
message: /Cannot call constructor without `new`/,
});
});
test('throws if database path is not a string', (t) => {
t.assert.throws(() => {
new DatabaseSync();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "path" argument must be a string/,
});
});
test('throws if options is provided but is not an object', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', null);
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options" argument must be an object/,
});
});
test('throws if options.open is provided but is not a boolean', (t) => {
t.assert.throws(() => {
new DatabaseSync('foo', { open: 5 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.open" argument must be a boolean/,
});
});
});
suite('DatabaseSync.prototype.open()', () => {
test('opens a database connection', (t) => {
const dbPath = nextDb();
const db = new DatabaseSync(dbPath, { open: false });
t.assert.strictEqual(existsSync(dbPath), false);
t.assert.strictEqual(db.open(), undefined);
t.assert.strictEqual(existsSync(dbPath), true);
});
test('throws if database is already open', (t) => {
const db = new DatabaseSync(nextDb(), { open: false });
db.open();
t.assert.throws(() => {
db.open();
}, {
code: 'ERR_INVALID_STATE',
message: /database is already open/,
});
});
});
suite('DatabaseSync.prototype.close()', () => {
test('closes an open database connection', (t) => {
const db = new DatabaseSync(nextDb());
t.assert.strictEqual(db.close(), undefined);
});
test('throws if database is not open', (t) => {
const db = new DatabaseSync(nextDb(), { open: false });
t.assert.throws(() => {
db.close();
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
});
});
suite('DatabaseSync.prototype.prepare()', () => {
test('returns a prepared statement', (t) => {
const db = new DatabaseSync(nextDb());
const stmt = db.prepare('CREATE TABLE webstorage(key TEXT)');
t.assert.ok(stmt instanceof StatementSync);
});
test('throws if database is not open', (t) => {
const db = new DatabaseSync(nextDb(), { open: false });
t.assert.throws(() => {
db.prepare();
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
});
test('throws if sql is not a string', (t) => {
const db = new DatabaseSync(nextDb());
t.assert.throws(() => {
db.prepare();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "sql" argument must be a string/,
});
});
});
suite('DatabaseSync.prototype.exec()', () => {
test('executes SQL', (t) => {
const db = new DatabaseSync(nextDb());
const result = db.exec(`
CREATE TABLE data(
key INTEGER PRIMARY KEY,
val INTEGER
) STRICT;
INSERT INTO data (key, val) VALUES (1, 2);
INSERT INTO data (key, val) VALUES (8, 9);
`);
t.assert.strictEqual(result, undefined);
const stmt = db.prepare('SELECT * FROM data ORDER BY key');
t.assert.deepStrictEqual(stmt.all(), [
{ key: 1, val: 2 },
{ key: 8, val: 9 },
]);
});
test('reports errors from SQLite', (t) => {
const db = new DatabaseSync(nextDb());
t.assert.throws(() => {
db.exec('CREATE TABLEEEE');
}, {
code: 'ERR_SQLITE_ERROR',
message: /syntax error/,
});
});
test('throws if database is not open', (t) => {
const db = new DatabaseSync(nextDb(), { open: false });
t.assert.throws(() => {
db.exec();
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
});
test('throws if sql is not a string', (t) => {
const db = new DatabaseSync(nextDb());
t.assert.throws(() => {
db.exec();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "sql" argument must be a string/,
});
});
});
suite('StatementSync() constructor', () => {
test('StatementSync cannot be constructed directly', (t) => {
t.assert.throws(() => {
new StatementSync();
}, {
code: 'ERR_ILLEGAL_CONSTRUCTOR',
message: /Illegal constructor/,
});
});
});
suite('StatementSync.prototype.get()', () => {
test('executes a query and returns undefined on no results', (t) => {
const db = new DatabaseSync(nextDb());
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
t.assert.strictEqual(stmt.get(), undefined);
});
test('executes a query and returns the first result', (t) => {
const db = new DatabaseSync(nextDb());
let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
t.assert.strictEqual(stmt.get(), undefined);
stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
t.assert.strictEqual(stmt.get('key1', 'val1'), undefined);
t.assert.strictEqual(stmt.get('key2', 'val2'), undefined);
stmt = db.prepare('SELECT * FROM storage ORDER BY key');
t.assert.deepStrictEqual(stmt.get(), { key: 'key1', val: 'val1' });
});
});
suite('StatementSync.prototype.all()', () => {
test('executes a query and returns an empty array on no results', (t) => {
const db = new DatabaseSync(nextDb());
const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
t.assert.deepStrictEqual(stmt.all(), []);
});
test('executes a query and returns all results', (t) => {
const db = new DatabaseSync(nextDb());
let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
t.assert.deepStrictEqual(stmt.run(), { changes: 0, lastInsertRowid: 0 });
stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
t.assert.deepStrictEqual(
stmt.run('key1', 'val1'),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.deepStrictEqual(
stmt.run('key2', 'val2'),
{ changes: 1, lastInsertRowid: 2 },
);
stmt = db.prepare('SELECT * FROM storage ORDER BY key');
t.assert.deepStrictEqual(stmt.all(), [
{ key: 'key1', val: 'val1' },
{ key: 'key2', val: 'val2' },
]);
});
});
suite('StatementSync.prototype.run()', () => {
test('executes a query and returns change metadata', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(`
CREATE TABLE storage(key TEXT, val TEXT);
INSERT INTO storage (key, val) VALUES ('foo', 'bar');
`);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('SELECT * FROM storage');
t.assert.deepStrictEqual(stmt.run(), { changes: 1, lastInsertRowid: 1 });
});
test('SQLite throws when trying to bind too many parameters', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
t.assert.throws(() => {
stmt.run(1, 2, 3);
}, {
code: 'ERR_SQLITE_ERROR',
message: 'column index out of range',
errcode: 25,
errstr: 'column index out of range',
});
});
test('SQLite defaults to NULL for unbound parameters', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
t.assert.throws(() => {
stmt.run(1);
}, {
code: 'ERR_SQLITE_ERROR',
message: 'NOT NULL constraint failed: data.val',
errcode: 1299,
errstr: 'constraint failed',
});
});
});
suite('StatementSync.prototype.sourceSQL()', () => {
test('returns input SQL', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const sql = 'INSERT INTO types (key, val) VALUES ($k, $v)';
const stmt = db.prepare(sql);
t.assert.strictEqual(stmt.sourceSQL(), sql);
});
});
suite('StatementSync.prototype.expandedSQL()', () => {
test('returns expanded SQL', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const sql = 'INSERT INTO types (key, val) VALUES ($k, ?)';
const expanded = 'INSERT INTO types (key, val) VALUES (\'33\', \'42\')';
const stmt = db.prepare(sql);
t.assert.deepStrictEqual(
stmt.run({ $k: '33' }, '42'),
{ changes: 1, lastInsertRowid: 33 },
);
t.assert.strictEqual(stmt.expandedSQL(), expanded);
});
});
suite('StatementSync.prototype.setReadBigInts()', () => {
test('BigInts support can be toggled', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
INSERT INTO data (key, val) VALUES (1, 42);
`);
t.assert.strictEqual(setup, undefined);
const query = db.prepare('SELECT val FROM data');
t.assert.deepStrictEqual(query.get(), { val: 42 });
t.assert.strictEqual(query.setReadBigInts(true), undefined);
t.assert.deepStrictEqual(query.get(), { val: 42n });
t.assert.strictEqual(query.setReadBigInts(false), undefined);
t.assert.deepStrictEqual(query.get(), { val: 42 });
const insert = db.prepare('INSERT INTO data (key) VALUES (?)');
t.assert.deepStrictEqual(
insert.run(10),
{ changes: 1, lastInsertRowid: 10 },
);
t.assert.strictEqual(insert.setReadBigInts(true), undefined);
t.assert.deepStrictEqual(
insert.run(20),
{ changes: 1n, lastInsertRowid: 20n },
);
t.assert.strictEqual(insert.setReadBigInts(false), undefined);
t.assert.deepStrictEqual(
insert.run(30),
{ changes: 1, lastInsertRowid: 30 },
);
});
test('throws when input is not a boolean', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)');
t.assert.throws(() => {
stmt.setReadBigInts();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "readBigInts" argument must be a boolean/,
});
});
});
suite('StatementSync.prototype.setAllowBareNamedParameters()', () => {
test('bare named parameter support can be toggled', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
t.assert.deepStrictEqual(
stmt.run({ k: 1, v: 2 }),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.strictEqual(stmt.setAllowBareNamedParameters(false), undefined);
t.assert.throws(() => {
stmt.run({ k: 2, v: 4 });
}, {
code: 'ERR_INVALID_STATE',
message: /Unknown named parameter 'k'/,
});
t.assert.strictEqual(stmt.setAllowBareNamedParameters(true), undefined);
t.assert.deepStrictEqual(
stmt.run({ k: 3, v: 6 }),
{ changes: 1, lastInsertRowid: 3 },
);
});
test('throws when input is not a boolean', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
t.assert.throws(() => {
stmt.setAllowBareNamedParameters();
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "allowBareNamedParameters" argument must be a boolean/,
});
});
});
suite('data binding and mapping', () => {
test('supported data types', (t) => {
const u8a = new TextEncoder().encode('a☃b☃c');
const db = new DatabaseSync(nextDb());
const setup = db.exec(`
CREATE TABLE types(
key INTEGER PRIMARY KEY,
int INTEGER,
double REAL,
text TEXT,
buf BLOB
) STRICT;
`);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO types (key, int, double, text, buf) ' +
'VALUES (?, ?, ?, ?, ?)');
t.assert.deepStrictEqual(
stmt.run(1, 42, 3.14159, 'foo', u8a),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.deepStrictEqual(
stmt.run(2, null, null, null, null),
{ changes: 1, lastInsertRowid: 2 }
);
t.assert.deepStrictEqual(
stmt.run(3, Number(8), Number(2.718), String('bar'), Buffer.from('x☃y☃')),
{ changes: 1, lastInsertRowid: 3 },
);
t.assert.deepStrictEqual(
stmt.run(4, 99n, 0xf, '', new Uint8Array()),
{ changes: 1, lastInsertRowid: 4 },
);
const query = db.prepare('SELECT * FROM types WHERE key = ?');
t.assert.deepStrictEqual(query.get(1), {
key: 1,
int: 42,
double: 3.14159,
text: 'foo',
buf: u8a,
});
t.assert.deepStrictEqual(query.get(2), {
key: 2,
int: null,
double: null,
text: null,
buf: null,
});
t.assert.deepStrictEqual(query.get(3), {
key: 3,
int: 8,
double: 2.718,
text: 'bar',
buf: new TextEncoder().encode('x☃y☃'),
});
t.assert.deepStrictEqual(query.get(4), {
key: 4,
int: 99,
double: 0xf,
text: '',
buf: new Uint8Array(),
});
});
test('unsupported data types', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
[
undefined,
() => {},
Symbol(),
/foo/,
Promise.resolve(),
new Map(),
new Set(),
].forEach((val) => {
t.assert.throws(() => {
db.prepare('INSERT INTO types (key, val) VALUES (?, ?)').run(1, val);
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /Provided value cannot be bound to SQLite parameter 2/,
});
});
t.assert.throws(() => {
const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)');
stmt.run({ $k: 1, $v: () => {} });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /Provided value cannot be bound to SQLite parameter 2/,
});
});
test('throws when binding a BigInt that is too large', (t) => {
const max = 9223372036854775807n; // Largest 64-bit signed integer value.
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO types (key, val) VALUES (?, ?)');
t.assert.deepStrictEqual(
stmt.run(1, max),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.throws(() => {
stmt.run(1, max + 1n);
}, {
code: 'ERR_INVALID_ARG_VALUE',
message: /BigInt value is too large to bind/,
});
});
test('statements are unbound on each call', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
t.assert.deepStrictEqual(
stmt.run(1, 5),
{ changes: 1, lastInsertRowid: 1 },
);
t.assert.deepStrictEqual(
stmt.run(),
{ changes: 1, lastInsertRowid: 2 },
);
t.assert.deepStrictEqual(
db.prepare('SELECT * FROM data ORDER BY key').all(),
[{ key: 1, val: 5 }, { key: 2, val: null }],
);
});
});
suite('manual transactions', () => {
test('a transaction is committed', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(`
CREATE TABLE data(
key INTEGER PRIMARY KEY
) STRICT;
`);
t.assert.strictEqual(setup, undefined);
t.assert.deepStrictEqual(
db.prepare('BEGIN').run(),
{ changes: 0, lastInsertRowid: 0 },
);
t.assert.deepStrictEqual(
db.prepare('INSERT INTO data (key) VALUES (100)').run(),
{ changes: 1, lastInsertRowid: 100 },
);
t.assert.deepStrictEqual(
db.prepare('COMMIT').run(),
{ changes: 1, lastInsertRowid: 100 },
);
t.assert.deepStrictEqual(
db.prepare('SELECT * FROM data').all(),
[{ key: 100 }],
);
});
test('a transaction is rolled back', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(`
CREATE TABLE data(
key INTEGER PRIMARY KEY
) STRICT;
`);
t.assert.strictEqual(setup, undefined);
t.assert.deepStrictEqual(
db.prepare('BEGIN').run(),
{ changes: 0, lastInsertRowid: 0 },
);
t.assert.deepStrictEqual(
db.prepare('INSERT INTO data (key) VALUES (100)').run(),
{ changes: 1, lastInsertRowid: 100 },
);
t.assert.deepStrictEqual(
db.prepare('ROLLBACK').run(),
{ changes: 1, lastInsertRowid: 100 },
);
t.assert.deepStrictEqual(db.prepare('SELECT * FROM data').all(), []);
});
});
suite('named parameters', () => {
test('throws on unknown named parameters', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
t.assert.throws(() => {
const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)');
stmt.run({ $k: 1, $unknown: 1 });
}, {
code: 'ERR_INVALID_STATE',
message: /Unknown named parameter '\$unknown'/,
});
});
test('bare named parameters are supported', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
stmt.run({ k: 1, v: 9 });
t.assert.deepStrictEqual(
db.prepare('SELECT * FROM data').get(),
{ key: 1, val: 9 },
);
});
test('duplicate bare named parameters are supported', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $k)');
stmt.run({ k: 1 });
t.assert.deepStrictEqual(
db.prepare('SELECT * FROM data').get(),
{ key: 1, val: 1 },
);
});
test('bare named parameters throw on ambiguous names', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(
'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, @k)');
t.assert.throws(() => {
stmt.run({ k: 1 });
}, {
code: 'ERR_INVALID_STATE',
message: 'Cannot create bare named parameter \'k\' because of ' +
'conflicting names \'$k\' and \'@k\'.',
});
});
});
test('ERR_SQLITE_ERROR is thrown for errors originating from SQLite', (t) => {
const db = new DatabaseSync(nextDb());
const setup = db.exec(`
CREATE TABLE test(
key INTEGER PRIMARY KEY
) STRICT;
`);
t.assert.strictEqual(setup, undefined);
const stmt = db.prepare('INSERT INTO test (key) VALUES (?)');
t.assert.deepStrictEqual(stmt.run(1), { changes: 1, lastInsertRowid: 1 });
t.assert.throws(() => {
stmt.run(1);
}, {
code: 'ERR_SQLITE_ERROR',
message: 'UNIQUE constraint failed: test.key',
errcode: 1555,
errstr: 'constraint failed',
});
});
test('in-memory databases are supported', (t) => {
const db1 = new DatabaseSync(':memory:');
const db2 = new DatabaseSync(':memory:');
const setup1 = db1.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY);
INSERT INTO data (key) VALUES (1);
`);
const setup2 = db2.exec(`
CREATE TABLE data(key INTEGER PRIMARY KEY);
INSERT INTO data (key) VALUES (1);
`);
t.assert.strictEqual(setup1, undefined);
t.assert.strictEqual(setup2, undefined);
t.assert.deepStrictEqual(
db1.prepare('SELECT * FROM data').all(),
[{ key: 1 }]
);
t.assert.deepStrictEqual(
db2.prepare('SELECT * FROM data').all(),
[{ key: 1 }]
);
});
test('PRAGMAs are supported', (t) => {
const db = new DatabaseSync(nextDb());
t.assert.deepStrictEqual(
db.prepare('PRAGMA journal_mode = WAL').get(),
{ journal_mode: 'wal' },
);
t.assert.deepStrictEqual(
db.prepare('PRAGMA journal_mode').get(),
{ journal_mode: 'wal' },
);
});

View File

@ -199,6 +199,8 @@ const customTypesMap = {
'repl.REPLServer': 'repl.html#class-replserver',
'StatementSync': 'sqlite.html#class-statementsync',
'Stream': 'stream.html#stream',
'stream.Duplex': 'stream.html#class-streamduplex',
'Duplex': 'stream.html#class-streamduplex',