mirror of
https://github.com/nodejs/node.git
synced 2024-11-24 03:07:54 +01:00
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:
parent
1a1639de3e
commit
b4e8f1b6bb
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
328
doc/api/sqlite.md
Normal 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
|
@ -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.
|
||||
.
|
||||
|
@ -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().
|
||||
{
|
||||
|
@ -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
5
lib/sqlite.js
Normal file
@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
const { emitExperimentalWarning } = require('internal/util');
|
||||
|
||||
emitExperimentalWarning('SQLite');
|
||||
module.exports = internalBinding('sqlite');
|
2
node.gyp
2
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',
|
||||
|
@ -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) \
|
||||
|
@ -67,6 +67,7 @@
|
||||
V(serdes) \
|
||||
V(signal_wrap) \
|
||||
V(spawn_sync) \
|
||||
V(sqlite) \
|
||||
V(stream_pipe) \
|
||||
V(stream_wrap) \
|
||||
V(string_decoder) \
|
||||
|
@ -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",
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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
667
src/node_sqlite.cc
Normal 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
81
src/node_sqlite.h
Normal 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_
|
740
test/parallel/test-sqlite.js
Normal file
740
test/parallel/test-sqlite.js
Normal 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' },
|
||||
);
|
||||
});
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user