Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Fedor Indutnyy 2022-04-07 16:46:05 -07:00
commit 3c4a7eebba
54 changed files with 3507 additions and 941 deletions

4
.gitattributes vendored
View File

@ -1,2 +1,6 @@
*.lzz linguist-language=C++
*.cpp -diff
*.hpp -diff
*.c -diff
*.h -diff
deps/sqlcipher.tar.gz filter=lfs diff=lfs merge=lfs -text

2
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,2 @@
* @JoshuaWise
/.github/workflows/build.yml @JoshuaWise @mceachen

View File

@ -10,6 +10,7 @@ on:
release:
types:
- released
workflow_dispatch: {}
jobs:
@ -17,13 +18,14 @@ jobs:
strategy:
matrix:
os:
- ubuntu-latest
- ubuntu-18.04
- macos-latest
- windows-latest
node:
- 10
- 12
- 14
- 16
name: Testing Node ${{ matrix.node }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
steps:
@ -38,13 +40,13 @@ jobs:
publish:
if: ${{ github.event_name == 'release' }}
name: Publishing to NPM
runs-on: ubuntu-latest
runs-on: ubuntu-18.04
needs: test
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
registry-url: https://registry.npmjs.org
- run: npm publish
env:
@ -54,7 +56,7 @@ jobs:
strategy:
matrix:
os:
- ubuntu-latest
- ubuntu-18.04
- macos-latest
- windows-latest
name: Prebuild on ${{ matrix.os }}
@ -64,7 +66,33 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
- run: npm install --ignore-scripts
- run: npx --no-install prebuild -r node -t 10.20.0 -t 12.0.0 -t 14.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
- run: npx --no-install prebuild -r electron -t 10.0.0 -t 11.0.0 -t 12.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
- run: npx --no-install prebuild -r node -t 10.20.0 -t 12.0.0 -t 14.0.0 -t 16.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
- run: npx --no-install prebuild -r electron -t 10.0.0 -t 11.0.0 -t 12.0.0 -t 13.0.0 -t 14.0.0 -t 15.0.0 -t 16.0.0 -t 17.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
- if: matrix.os == 'windows-latest'
run: npx --no-install prebuild -r electron -t 10.0.0 -t 11.0.0 -t 12.0.0 -t 13.0.0 -t 14.0.0 -t 15.0.0 -t 16.0.0 -t 17.0.0 --include-regex 'better_sqlite3.node$' --arch ia32 -u ${{ secrets.GITHUB_TOKEN }}
prebuild-alpine:
name: Prebuild on alpine
runs-on: ubuntu-latest
container: node:16-alpine
needs: publish
steps:
- uses: actions/checkout@v2
- run: apk add build-base git python3 --update-cache
- run: npm install --ignore-scripts
- run: npx --no-install prebuild -r node -t 10.20.0 -t 12.0.0 -t 14.0.0 -t 16.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}
prebuild-arm64-alpine:
name: Prebuild on arm64 alpine
runs-on: ubuntu-latest
needs: publish
steps:
- uses: docker/setup-qemu-action@v1
- run: |
docker run --rm --entrypoint /bin/sh --platform linux/arm64 node:16-alpine -c "apk add build-base git python3 --update-cache && \
git clone ${{ github.event.repository.clone_url }} && \
cd ${{ github.event.repository.name }} && \
npm install --ignore-scripts && \
npx --no-install prebuild -r node -t 10.20.0 -t 12.0.0 -t 14.0.0 -t 16.0.0 --include-regex 'better_sqlite3.node$' -u ${{ secrets.GITHUB_TOKEN }}"

View File

@ -5,7 +5,7 @@ The fastest and simplest library for SQLite3 in Node.js.
- Full transaction support
- High performance, efficiency, and safety
- Easy-to-use synchronous API *(better concurrency than an asynchronous API... yes, you read that correctly)*
- Support for user-defined functions, aggregates, and extensions
- Support for user-defined functions, aggregates, virtual tables, and extensions
- 64-bit integers *(invisible until you need them)*
- Worker thread support *(for large/slow queries)*
@ -32,9 +32,7 @@ The fastest and simplest library for SQLite3 in Node.js.
npm install better-sqlite3
```
> You must be using Node.js v10.20.1 or above. Prebuilt binaries are available for [LTS versions](https://nodejs.org/en/about/releases/).
> If you have trouble installing, check the [troubleshooting guide](./docs/troubleshooting.md).
> You must be using Node.js v10.20.1 or above. Prebuilt binaries are available for [LTS versions](https://nodejs.org/en/about/releases/). If you have trouble installing, check the [troubleshooting guide](./docs/troubleshooting.md).
## Usage
@ -45,6 +43,13 @@ const row = db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
console.log(row.firstName, row.lastName, row.email);
```
##### In ES6 module notation:
```js
import Database from 'better-sqlite3';
const db = new Database('foobar.db', options);
```
## Why should I use this instead of [node-sqlite3](https://github.com/mapbox/node-sqlite3)?
- `node-sqlite3` uses asynchronous APIs for tasks that are either CPU-bound or serialized. That's not only bad design, but it wastes tons of resources. It also causes [mutex thrashing](https://en.wikipedia.org/wiki/Resource_contention) which has devastating effects on performance.

10
deps/defines.gypi vendored
View File

@ -15,6 +15,8 @@
'SQLITE_DEFAULT_CACHE_SIZE=-16000',
'SQLITE_DEFAULT_FOREIGN_KEYS=1',
'SQLITE_DEFAULT_WAL_SYNCHRONOUS=1',
'SQLITE_ENABLE_MATH_FUNCTIONS',
'SQLITE_ENABLE_DESERIALIZE',
'SQLITE_ENABLE_COLUMN_METADATA',
'SQLITE_ENABLE_UPDATE_DELETE_LIMIT',
'SQLITE_ENABLE_STAT4',
@ -23,6 +25,14 @@
'SQLITE_ENABLE_RTREE',
'SQLITE_INTROSPECTION_PRAGMAS',
'HAVE_STDINT_H=1',
'HAVE_INT8_T=1',
'HAVE_INT16_T=1',
'HAVE_INT32_T=1',
'HAVE_UINT8_T=1',
'HAVE_UINT16_T=1',
'HAVE_UINT32_T=1',
# SQLCipher-specific options
'SQLITE_HAS_CODEC',
'SQLITE_TEMP_STORE=2',

30
deps/download.sh vendored
View File

@ -7,19 +7,19 @@
# 1. populate the shell environment with the defined compile-time options.
# 2. download and extract the SQLite3 source code into a temporary directory.
# 3. run "sh configure" and "make sqlite3.c" within the source directory.
# 4. bundle the generated amalgamation into a tar.gz file (sqlite3.tar.gz).
# 5. export the defined compile-time options to a gyp file (defines.gypi).
# 4. copy the generated amalgamation into the output directory (./sqlite3).
# 5. export the defined compile-time options to a gyp file (./defines.gypi).
# 6. update the docs (../docs/compilation.md) with details of this distribution.
#
# When a user builds better-sqlite3, the following steps are taken:
# 1. node-gyp loads the previously exported compile-time options (defines.gypi).
# 2. the extract.js script unpacks the bundled amalgamation (sqlite3.tar.gz).
# 3. node-gyp compiles the extracted sqlite3.c along with better_sqlite3.cpp.
# 3. node-gyp links the two resulting binaries to generate better_sqlite3.node.
# 2. the symlink.js script creates symlinks to the bundled amalgamation.
# 3. node-gyp compiles the symlinked sqlite3.c along with better_sqlite3.cpp.
# 4. node-gyp links the two resulting binaries to generate better_sqlite3.node.
# ===
YEAR="2021"
VERSION="3350200"
YEAR="2022"
VERSION="3370200"
DEFINES="
SQLITE_DQS=0
@ -36,6 +36,8 @@ SQLITE_TRACE_SIZE_LIMIT=32
SQLITE_DEFAULT_CACHE_SIZE=-16000
SQLITE_DEFAULT_FOREIGN_KEYS=1
SQLITE_DEFAULT_WAL_SYNCHRONOUS=1
SQLITE_ENABLE_MATH_FUNCTIONS
SQLITE_ENABLE_DESERIALIZE
SQLITE_ENABLE_COLUMN_METADATA
SQLITE_ENABLE_UPDATE_DELETE_LIMIT
SQLITE_ENABLE_STAT4
@ -48,6 +50,13 @@ SQLITE_ENABLE_RTREE
SQLITE_ENABLE_GEOPOLY
SQLITE_INTROSPECTION_PRAGMAS
SQLITE_SOUNDEX
HAVE_STDINT_H=1
HAVE_INT8_T=1
HAVE_INT16_T=1
HAVE_INT32_T=1
HAVE_UINT8_T=1
HAVE_UINT16_T=1
HAVE_UINT32_T=1
"
# ========== START SCRIPT ========== #
@ -55,8 +64,11 @@ SQLITE_SOUNDEX
echo "setting up environment..."
DEPS="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
TEMP="$DEPS/temp"
OUTPUT="$DEPS/sqlite3"
rm -rf "$TEMP"
rm -rf "$OUTPUT"
mkdir -p "$TEMP"
mkdir -p "$OUTPUT"
export CFLAGS=`echo $(echo "$DEFINES" | sed -e "/^\s*$/d" -e "s/^/-D/")`
echo "downloading source..."
@ -72,8 +84,8 @@ sh configure > /dev/null || exit 1
echo "building amalgamation..."
make sqlite3.c > /dev/null || exit 1
echo "generating tarball..."
tar czf "$DEPS/sqlite3.tar.gz" sqlite3.c sqlite3.h sqlite3ext.h || exit 1
echo "copying amalgamation..."
cp sqlite3.c sqlite3.h sqlite3ext.h "$OUTPUT/" || exit 1
echo "updating gyp configs..."
GYP="$DEPS/defines.gypi"

BIN
deps/sqlite3/sqlite3.c vendored Normal file

Binary file not shown.

BIN
deps/sqlite3/sqlite3.h vendored Normal file

Binary file not shown.

BIN
deps/sqlite3/sqlite3ext.h vendored Normal file

Binary file not shown.

11
deps/symlink.js vendored
View File

@ -3,14 +3,17 @@ const path = require('path');
const fs = require('fs');
const dest = process.argv[2];
const source = path.resolve(path.sep, process.argv[3]);
const source = path.resolve(path.sep, process.argv[3] || path.join(__dirname, 'sqlite3'));
const filenames = process.argv.slice(4).map(str => path.basename(str));
/*
This creates symlinks inside the <$2> directory, linking to the SQLite3
amalgamation files inside the directory specified by the absolute path <$3>.
This creates symlinks inside the <$2> directory, linking to files inside the
directory specified by the absolute path <$3>. If no path <$3> is provided,
the default path of "./deps/sqlite3" is used. The basenames of the files to
link are specified by <$4...>.
*/
for (const filename of ['sqlite3.c', 'sqlite3.h']) {
for (const filename of filenames) {
fs.accessSync(path.join(source, filename));
fs.symlinkSync(path.join(source, filename), path.join(dest, filename), 'file');
}

View File

@ -2,6 +2,8 @@
- [class `Database`](#class-database)
- [class `Statement`](#class-statement)
- [class `SqliteError`](#class-sqliteerror)
- [Binding Parameters](#binding-parameters)
# class *Database*
@ -10,8 +12,10 @@
- [Database#transaction()](#transactionfunction---function)
- [Database#pragma()](#pragmastring-options---results)
- [Database#backup()](#backupdestination-options---promise)
- [Database#serialize()](#serializeoptions---buffer)
- [Database#function()](#functionname-options-function---this)
- [Database#aggregate()](#aggregatename-options---this)
- [Database#table()](#tablename-definition---this)
- [Database#loadExtension()](#loadextensionpath-entrypoint---this)
- [Database#exec()](#execstring---this)
- [Database#close()](#close---this)
@ -19,18 +23,22 @@
### new Database(*path*, [*options*])
Creates a new database connection. If the database file does not exist, it is created. This happens synchronously, which means you can start executing queries right away. You can create an [in-memory database](https://www.sqlite.org/inmemorydb.html) by passing `":memory:"` as the first argument.
Creates a new database connection. If the database file does not exist, it is created. This happens synchronously, which means you can start executing queries right away. You can create an [in-memory database](https://www.sqlite.org/inmemorydb.html) by passing `":memory:"` as the first argument. You can create a temporary database by passing an empty string (or by omitting all arguments).
> In-memory databases can also be created by passing a buffer returned by [`.serialize()`](#serializeoptions---buffer), instead of passing a string as the first argument.
Various options are accepted:
- `options.readonly`: open the database connection in readonly mode (default: `false`).
- `options.fileMustExist`: if the database does not exist, an `Error` will be thrown instead of creating a new file. This option does not affect in-memory or readonly database connections (default: `false`).
- `options.fileMustExist`: if the database does not exist, an `Error` will be thrown instead of creating a new file. This option is ignored for in-memory, temporary, or readonly database connections (default: `false`).
- `options.timeout`: the number of milliseconds to wait when executing queries on a locked database, before throwing a `SQLITE_BUSY` error (default: `5000`).
- `options.verbose`: provide a function that gets called with every SQL string executed by the database connection (default: `null`).
- `options.nativeBinding`: if you're using a complicated build system that moves, transforms, or concatenates your JS files, `better-sqlite3` might have trouble locating its native C++ addon (`better_sqlite3.node`). If you get an error that looks like [this](https://github.com/JoshuaWise/better-sqlite3/issues/534#issuecomment-757907190), you can solve it by using this option to provide the file path of `better_sqlite3.node` (relative to the current working directory).
```js
const Database = require('better-sqlite3');
const db = new Database('foobar.db', { verbose: console.log });
@ -90,7 +98,7 @@ If you'd like to manage transactions manually, you're free to do so with regular
Transaction functions do not work with async functions. Technically speaking, async functions always return after the first `await`, which means the transaction will already be committed before any async code executes. Also, because SQLite3 serializes all transactions, it's generally a very bad idea to keep a transaction open across event loop ticks anyways.
It's important to know that SQLite3 may sometimes rollback a transaction without us asking it to. This can happen either because of an [`ON CONFLICT`](https://sqlite.org/lang_conflict.html) clause, the [`RAISE()`](https://www.sqlite.org/lang_createtrigger.html) trigger function, or certain errors such as `SQLITE_FULL` or `SQLITE_BUSY`. In other words, if you catch an SQLite3 error *within* a transaction, you must be aware that any futher SQL that you execute might not be within the same transaction. Usually, the best course of action for such cases is to simply re-throw the error, exiting the transaction function.
It's important to know that SQLite3 may sometimes rollback a transaction without us asking it to. This can happen either because of an [`ON CONFLICT`](https://sqlite.org/lang_conflict.html) clause, the [`RAISE()`](https://www.sqlite.org/lang_createtrigger.html) trigger function, or certain errors such as `SQLITE_FULL` or `SQLITE_BUSY`. In other words, if you catch an SQLite3 error *within* a transaction, you must be aware that any further SQL that you execute might not be within the same transaction. Usually, the best course of action for such cases is to simply re-throw the error, exiting the transaction function.
```js
try {
@ -118,7 +126,7 @@ It's better to use this method instead of normal [prepared statements](#prepares
### .backup(*destination*, [*options*]) -> *promise*
Initiates a [backup](https://www.sqlite.org/backup.html) of the database, returning a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) for when the backup is complete. If the backup fails, the promise will be rejected with an `Error`. You can optionally backup an attached database by setting the `attached` option to the name of the desired attached database.
Initiates a [backup](https://www.sqlite.org/backup.html) of the database, returning a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) for when the backup is complete. If the backup fails, the promise will be rejected with an `Error`. You can optionally backup an attached database instead by setting the `attached` option to the name of the desired attached database. A backup file is just a regular SQLite3 database file. It can be opened by [`new Database()`](#new-databasepath-options) just like any SQLite3 database.
```js
db.backup(`backup-${Date.now()}.db`)
@ -151,6 +159,18 @@ db.backup(`backup-${Date.now()}.db`, {
});
```
### .serialize([*options*]) -> *Buffer*
Returns a [buffer](https://nodejs.org/api/buffer.html#buffer_class_buffer) containing the serialized contents of the database. You can optionally serialize an attached database instead by setting the `attached` option to the name of the desired attached database.
The returned buffer can be written to disk to create a regular SQLite3 database file, or it can be opened directly as an in-memory database by passing it to [`new Database()`](#new-databasepath-options).
```js
const buffer = db.serialize();
db.close();
db = new Database(buffer);
```
### .function(*name*, [*options*], *function*) -> *this*
Registers a user-defined `function` so that it can be used by SQL statements.
@ -167,6 +187,8 @@ By default, user-defined functions have a strict number of arguments (determined
If `options.varargs` is `true`, the registered function can accept any number of arguments.
If `options.directOnly` is `true`, the registered function can only be invoked from top-level SQL, and cannot be used in [VIEWs](https://sqlite.org/lang_createview.html), [TRIGGERs](https://sqlite.org/lang_createtrigger.html), or schema structures such as [CHECK constraints](https://www.sqlite.org/lang_createtable.html#ckconst), [DEFAULT clauses](https://www.sqlite.org/lang_createtable.html#dfltval), etc.
If your function is [deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm), you can set `options.deterministic` to `true`, which may improve performance under some circumstances.
```js
@ -209,7 +231,7 @@ db.prepare('SELECT getAverage(dollars) FROM expenses').pluck().get(); // => 20.2
As shown above, you can use arbitrary JavaScript objects as your aggregation context, as long as a valid SQLite3 value is returned by `result()` in the end. If `step()` doesn't return anything (`undefined`), the aggregate value will not be replaced (be careful of this when using functions that return `undefined` when `null` is desired).
Just like regular [user-defined functions](#functionname-options-function---this), user-defined aggregates can accept multiple arguments. Furthermore, `options.varargs` and `options.deterministic` [are also](#functionname-options-function---this) accepted.
Just like regular [user-defined functions](#functionname-options-function---this), user-defined aggregates can accept multiple arguments. Furthermore, `options.varargs`, `options.directOnly`, and `options.deterministic` [are also](#functionname-options-function---this) accepted.
If you provide an `inverse()` function, the aggregate can be used as a [window function](https://www.sqlite.org/windowfunctions.html). Where `step()` is used to add a row to the current window, `inverse()` is used to remove a row from the current window. When using window functions, `result()` may be invoked multiple times.
@ -229,6 +251,117 @@ db.prepare(`
`).all();
```
### .table(*name*, *definition*) -> *this*
Registers a [virtual table](https://www.sqlite.org/vtab.html). Virtual tables can be queried just like real tables, except their results do not exist in the database file; instead, they are calculated on-the-fly by a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) in JavaScript.
```js
const fs = require('fs');
db.table('filesystem_directory', {
columns: ['filename', 'data'],
rows: function* () {
for (const filename of fs.readdirSync(process.cwd())) {
const data = fs.readFileSync(filename);
yield { filename, data };
}
},
});
const files = db.prepare('SELECT * FROM filesystem_directory').all();
// => [{ filename, data }, { filename, data }]
```
To generate a row in a virtual table, you can either yield an object whose keys correspond to column names, or yield an array whose elements represent columns in the order that they were declared. Every virtual table **must** declare its columns via the `columns` option.
Virtual tables can be used like [table-valued functions](https://www.sqlite.org/vtab.html#tabfunc2); you can pass parameters to them, unlike regular tables.
```js
db.table('regex_matches', {
columns: ['match', 'capture'],
rows: function* (pattern, text) {
const regex = new RegExp(pattern, 'g');
let match;
while (match = regex.exec(text)) {
yield [match[0], match[1]];
}
},
});
const stmt = db.prepare("SELECT * FROM regex('\\$(\\d+)', ?)");
stmt.all('Desks cost $500 and chairs cost $27');
// => [{ match: '$500', capture: '500' }, { match: '$27', capture: '27' }]
```
By default, the number of parameters accepted by a virtual table is inferred by `function.length`, and the parameters are automatically named `$1`, `$2`, etc. However, you can optionally provide an explicit list of parameters via the `parameters` option.
```js
db.table('regex_matches', {
columns: ['match', 'capture'],
parameters: ['pattern', 'text'],
rows: function* (pattern, text) {
...
},
});
```
> In virtual tables, parameters are actually [*hidden columns*](https://www.sqlite.org/vtab.html#hidden_columns_in_virtual_tables), and they can be selected in the result set of a query, just like any other column. That's why it may sometimes be desirable to give them explicit names.
When querying a virtual table, any omitted parameters will be `undefined`. You can use this behavior to implement required parameters and default parameter values.
```js
db.table('sequence', {
columns: ['value'],
parameters: ['length', 'start'],
rows: function* (length, start = 0) {
if (length === undefined) {
throw new TypeError('missing required parameter "length"');
}
const end = start + length;
for (let n = start; n < end; ++n) {
yield { value: n };
}
},
});
db.prepare('SELECT * FROM sequence(10)').pluck().all();
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```
> Note that when using syntax like `start = 0` for default parameter values (shown above), the function's `.length` property does not include the optional parameter, so you need to explicitly declare `parameters` in this case.
Normally, when you register a virtual table, the virtual table *automatically exists* without needing to run a `CREATE VIRTUAL TABLE` statement. However, if you provide a factory function as the second argument (a function that *returns* virtual table definitions), then no virtual table will be created automatically. Instead, you can create multiple similar virtual tables by running [`CREATE VIRTUAL TABLE`](https://sqlite.org/lang_createvtab.html) statements, each with their own module arguments. Think of it like defining a virtual table "class" that can be instantiated by running `CREATE VIRTUAL TABLE` statements.
```js
const fs = require('fs');
db.table('csv', (filename) => {
const firstLine = getFirstLineOfFile(filename);
return {
columns: firstLine.split(','),
rows: function* () {
// This is just an example. Real CSV files are more complicated to parse.
const contents = fs.readFileSync(filename, 'utf8');
for (const line of contents.split('\n')) {
yield line.split(',');
}
},
};
});
db.exec('CREATE VIRTUAL TABLE my_data USING csv(my_data.csv)');
const allData = db.prepare('SELECT * FROM my_data').all();
```
The factory function will be invoked each time a corresponding `CREATE VIRTUAL TABLE` statement runs. The arguments to the factory function correspond to the module arguments passed in the `CREATE VIRTUAL TABLE` statement; always a list of arbitrary strings separated by commas. It's your responsibility to parse and interpret those module arguments. Note that SQLite3 does not allow [bound parameters](#binding-parameters) inside module arguments.
Just like [user-defined functions](#functionname-options-function---this) and [user-defined aggregates](#aggregatename-options---this), virtual tables support `options.directOnly`, which prevents the table from being used inside [VIEWs](https://sqlite.org/lang_createview.html), [TRIGGERs](https://sqlite.org/lang_createtrigger.html), or schema structures such as [CHECK constraints](https://www.sqlite.org/lang_createtable.html#ckconst), [DEFAULT clauses](https://www.sqlite.org/lang_createtable.html#dfltval), etc.
> Some [extensions](#loadextensionpath-entrypoint---this) can provide virtual tables that have write capabilities, but `db.table()` is only capable of creating read-only virtual tables, primarily for the purpose of supporting table-valued functions.
### .loadExtension(*path*, [*entryPoint*]) -> *this*
Loads a compiled [SQLite3 extension](https://sqlite.org/loadext.html) and applies it to the current database connection.
@ -267,7 +400,7 @@ process.on('SIGTERM', () => process.exit(128 + 15));
**.name -> _string_** - The string that was used to open the database connection.
**.memory -> _boolean_** - Whether the database is an in-memory database.
**.memory -> _boolean_** - Whether the database is an in-memory or temporary database.
**.readonly -> _boolean_** - Whether the database connection was created in readonly mode.
@ -288,8 +421,6 @@ An object representing a single SQL statement.
### .run([*...bindParameters*]) -> *object*
**(only on statements that do not return data)*
Executes the prepared statement. When execution completes it returns an `info` object describing any changes made. The `info` object has two properties:
- `info.changes`: the total number of rows that were inserted, updated, or deleted by this operation. Changes made by [foreign key actions](https://www.sqlite.org/foreignkeys.html#fk_actions) or [trigger programs](https://www.sqlite.org/lang_createtrigger.html) do not count.
@ -467,6 +598,18 @@ console.log(cat.name); // => "Joey"
**.reader -> _boolean_** - Whether the prepared statement returns data.
**.readonly -> _boolean_** - Whether the prepared statement is readonly, meaning it does not mutate the database (note that [SQL functions might still change the database indirectly](https://www.sqlite.org/c3ref/stmt_readonly.html) as a side effect, even if the `.readonly` property is `true`).
**.busy -> _boolean_** - Whether the prepared statement is busy executing a query via the [`.iterate()`](#iteratebindparameters---iterator) method.
# class *SqliteError*
Whenever an error occurs within SQLite3, a `SqliteError` object will be thrown. `SqliteError` is a subclass of `Error`. Every `SqliteError` object has a `code` property, which is a string matching one of error codes defined [here](https://sqlite.org/rescode.html) (for example, `"SQLITE_CONSTRAINT"`).
If you receive a `SqliteError`, it probably means you're using SQLite3 incorrectly. The error didn't originate in `better-sqlite3`, so it's probably not an issue with `better-sqlite3`. It's recommended that you learn about the meaning of the error [here](https://sqlite.org/rescode.html), and perhaps learn more about how to use SQLite3 by reading [their docs](https://sqlite.org/docs.html).
> In the unlikely scenario that SQLite3 throws an error that is not recognized by `better-sqlite3` (this would be considered a bug in `better-sqlite3`), the `code` property will be `"UNKNOWN_SQLITE_ERROR_NNNN"`, where `NNNN` is the numeric error code. If this happens to you, please report it as an [issue](https://github.com/JoshuaWise/better-sqlite3/issues).
# Binding Parameters
This section refers to anywhere in the documentation that specifies the optional argument [*`...bindParameters`*].
@ -504,3 +647,13 @@ Below is an example of mixing anonymous parameters with named parameters.
const stmt = db.prepare('INSERT INTO people VALUES (@name, @name, ?)');
stmt.run(45, { name: 'Henry' });
```
Here is how `better-sqlite3` converts values between SQLite3 and JavaScript:
|SQLite3|JavaScript|
|---|---|
|`NULL`|`null`|
|`REAL`|`number`|
|`INTEGER`|`number` [or `BigInt`](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/integer.md#the-bigint-primitive-type)|
|`TEXT`|`string`|
|`BLOB`|[`Buffer`](https://nodejs.org/api/buffer.html#buffer_class_buffer)|

View File

@ -16,7 +16,15 @@ However, if you simply run `npm install` while `better-sqlite3` is listed as a d
}
```
Your amalgamation directory must contain `sqlite3.c` and `sqlite3.h`. Any desired [compile time options](https://www.sqlite.org/compile.html) must be defined directly within `sqlite3.c`.
Your amalgamation directory must contain `sqlite3.c` and `sqlite3.h`. Any desired [compile time options](https://www.sqlite.org/compile.html) must be defined directly within `sqlite3.c`, as shown below.
```c
// These go at the top of the file
#define SQLITE_ENABLE_FTS5 1
#define SQLITE_DEFAULT_CACHE_SIZE 16000
// ... the original content of the file remains below
```
### Step by step example
@ -34,7 +42,7 @@ If you're using a SQLite3 encryption extension that is a drop-in replacement for
# Bundled configuration
By default, this distribution currently uses SQLite3 **version 3.35.2** with the following [compilation options](https://www.sqlite.org/compile.html):
By default, this distribution currently uses SQLite3 **version 3.37.2** with the following [compilation options](https://www.sqlite.org/compile.html):
```
SQLITE_DQS=0
@ -51,6 +59,8 @@ SQLITE_TRACE_SIZE_LIMIT=32
SQLITE_DEFAULT_CACHE_SIZE=-16000
SQLITE_DEFAULT_FOREIGN_KEYS=1
SQLITE_DEFAULT_WAL_SYNCHRONOUS=1
SQLITE_ENABLE_MATH_FUNCTIONS
SQLITE_ENABLE_DESERIALIZE
SQLITE_ENABLE_COLUMN_METADATA
SQLITE_ENABLE_UPDATE_DELETE_LIMIT
SQLITE_ENABLE_STAT4
@ -63,4 +73,11 @@ SQLITE_ENABLE_RTREE
SQLITE_ENABLE_GEOPOLY
SQLITE_INTROSPECTION_PRAGMAS
SQLITE_SOUNDEX
HAVE_STDINT_H=1
HAVE_INT8_T=1
HAVE_INT16_T=1
HAVE_INT32_T=1
HAVE_UINT8_T=1
HAVE_UINT16_T=1
HAVE_UINT32_T=1
```

View File

@ -52,4 +52,28 @@ db.prepare('SELECT isInt(?)').pluck().get(10); // => "false"
db.prepare('SELECT isInt(?)').pluck().get(10n); // => "true"
```
Likewise, [user-defined aggregates](./api.md#aggregatename-options---this) and [virtual tables](./api.md#tablename-definition---this) can also receive `BigInts` as arguments:
```js
db.aggregate('addInts', {
safeIntegers: true,
start: 0n,
step: (total, nextValue) => total + nextValue,
});
```
```js
db.table('sequence', {
safeIntegers: true,
columns: ['value'],
parameters: ['length', 'start'],
rows: function* (length, start = 0n) {
const end = start + length;
for (let n = start; n < end; ++n) {
yield { value: n };
}
},
});
```
It's worth noting that REAL (FLOAT) values returned from the database will always be represented as normal numbers.

View File

@ -4,7 +4,7 @@ If you have trouble installing `better-sqlite3`, follow this checklist:
1. Make sure you're using nodejs v10.20.1 or later
2. Make sure you have [`node-gyp`](https://github.com/nodejs/node-gyp#installation) globally installed, including all of [its dependencies](https://github.com/nodejs/node-gyp#on-unix). On Windows you may need to [configure some things manually](https://github.com/nodejs/node-gyp#on-windows).
2. Make sure you have [`node-gyp`](https://github.com/nodejs/node-gyp#installation) globally installed, including all of [its dependencies](https://github.com/nodejs/node-gyp#on-unix). On Windows you may need to [configure some things manually](https://github.com/nodejs/node-gyp#on-windows). Use `npm ls node-gyp` to make sure none of your local packages installed an outdated version of `node-gyp` that is used over the global one.
3. If you're using [Electron](https://github.com/electron/electron), try running [`electron-rebuild`](https://www.npmjs.com/package/electron-rebuild)

View File

@ -2,18 +2,21 @@
const fs = require('fs');
const path = require('path');
const util = require('./util');
const SqliteError = require('./sqlite-error');
const {
Database: CPPDatabase,
setErrorConstructor,
} = require('bindings')('better_sqlite3.node');
let DEFAULT_ADDON;
function Database(filenameGiven, options) {
if (new.target !== Database) {
if (new.target == null) {
return new Database(filenameGiven, options);
}
// Apply defaults
let buffer;
if (Buffer.isBuffer(filenameGiven)) {
buffer = filenameGiven;
filenameGiven = ':memory:';
}
if (filenameGiven == null) filenameGiven = '';
if (options == null) options = {};
@ -30,12 +33,26 @@ function Database(filenameGiven, options) {
const fileMustExist = util.getBooleanOption(options, 'fileMustExist');
const timeout = 'timeout' in options ? options.timeout : 5000;
const verbose = 'verbose' in options ? options.verbose : null;
const nativeBindingPath = 'nativeBinding' in options ? options.nativeBinding : null;
// Validate interpreted options
if (readonly && anonymous) throw new TypeError('In-memory/temporary databases cannot be readonly');
if (readonly && anonymous && !buffer) throw new TypeError('In-memory/temporary databases cannot be readonly');
if (!Number.isInteger(timeout) || timeout < 0) throw new TypeError('Expected the "timeout" option to be a positive integer');
if (timeout > 0x7fffffff) throw new RangeError('Option "timeout" cannot be greater than 2147483647');
if (verbose != null && typeof verbose !== 'function') throw new TypeError('Expected the "verbose" option to be a function');
if (nativeBindingPath != null && typeof nativeBindingPath !== 'string') throw new TypeError('Expected the "nativeBinding" option to be a string');
// Load the native addon
let addon;
if (nativeBindingPath == null) {
addon = DEFAULT_ADDON || (DEFAULT_ADDON = require('bindings')('better_sqlite3.node'));
} else {
addon = require(path.resolve(nativeBindingPath).replace(/(\.node)?$/, '.node'));
}
if (!addon.isInitialized) {
addon.setErrorConstructor(SqliteError);
addon.isInitialized = true;
}
// Make sure the specified directory exists
if (!anonymous && !fs.existsSync(path.dirname(filename))) {
@ -43,7 +60,7 @@ function Database(filenameGiven, options) {
}
Object.defineProperties(this, {
[util.cppdb]: { value: new CPPDatabase(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null) },
[util.cppdb]: { value: new addon.Database(filename, filenameGiven, anonymous, readonly, fileMustExist, timeout, verbose || null, buffer || null) },
...wrappers.getters,
});
}
@ -53,8 +70,10 @@ Database.prototype.prepare = wrappers.prepare;
Database.prototype.transaction = require('./methods/transaction');
Database.prototype.pragma = require('./methods/pragma');
Database.prototype.backup = require('./methods/backup');
Database.prototype.serialize = require('./methods/serialize');
Database.prototype.function = require('./methods/function');
Database.prototype.aggregate = require('./methods/aggregate');
Database.prototype.table = require('./methods/table');
Database.prototype.loadExtension = wrappers.loadExtension;
Database.prototype.exec = wrappers.exec;
Database.prototype.close = wrappers.close;
@ -63,4 +82,3 @@ Database.prototype.unsafeMode = wrappers.unsafeMode;
Database.prototype[util.inspect] = require('./methods/inspect');
module.exports = Database;
setErrorConstructor(require('./sqlite-error'));

View File

@ -14,6 +14,7 @@ module.exports = function defineAggregate(name, options) {
const result = getFunctionOption(options, 'result', false);
const safeIntegers = 'safeIntegers' in options ? +getBooleanOption(options, 'safeIntegers') : 2;
const deterministic = getBooleanOption(options, 'deterministic');
const directOnly = getBooleanOption(options, 'directOnly');
const varargs = getBooleanOption(options, 'varargs');
let argCount = -1;
@ -24,7 +25,7 @@ module.exports = function defineAggregate(name, options) {
if (argCount > 100) throw new RangeError('User-defined functions cannot have more than 100 arguments');
}
this[cppdb].aggregate(start, step, inverse, result, name, argCount, safeIntegers, deterministic);
this[cppdb].aggregate(start, step, inverse, result, name, argCount, safeIntegers, deterministic, directOnly);
return this;
};

View File

@ -15,6 +15,7 @@ module.exports = function defineFunction(name, options, fn) {
// Interpret options
const safeIntegers = 'safeIntegers' in options ? +getBooleanOption(options, 'safeIntegers') : 2;
const deterministic = getBooleanOption(options, 'deterministic');
const directOnly = getBooleanOption(options, 'directOnly');
const varargs = getBooleanOption(options, 'varargs');
let argCount = -1;
@ -25,6 +26,6 @@ module.exports = function defineFunction(name, options, fn) {
if (argCount > 100) throw new RangeError('User-defined functions cannot have more than 100 arguments');
}
this[cppdb].function(fn, name, argCount, safeIntegers, deterministic);
this[cppdb].function(fn, name, argCount, safeIntegers, deterministic, directOnly);
return this;
};

16
lib/methods/serialize.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
const { cppdb } = require('../util');
module.exports = function serialize(options) {
if (options == null) options = {};
// Validate arguments
if (typeof options !== 'object') throw new TypeError('Expected first argument to be an options object');
// Interpret and validate options
const attachedName = 'attached' in options ? options.attached : 'main';
if (typeof attachedName !== 'string') throw new TypeError('Expected the "attached" option to be a string');
if (!attachedName) throw new TypeError('The "attached" option cannot be an empty string');
return this[cppdb].serialize(attachedName);
};

189
lib/methods/table.js Normal file
View File

@ -0,0 +1,189 @@
'use strict';
const { cppdb } = require('../util');
module.exports = function defineTable(name, factory) {
// Validate arguments
if (typeof name !== 'string') throw new TypeError('Expected first argument to be a string');
if (!name) throw new TypeError('Virtual table module name cannot be an empty string');
// Determine whether the module is eponymous-only or not
let eponymous = false;
if (typeof factory === 'object' && factory !== null) {
eponymous = true;
factory = defer(parseTableDefinition(factory, 'used', name));
} else {
if (typeof factory !== 'function') throw new TypeError('Expected second argument to be a function or a table definition object');
factory = wrapFactory(factory);
}
this[cppdb].table(factory, name, eponymous);
return this;
};
function wrapFactory(factory) {
return function virtualTableFactory(moduleName, databaseName, tableName, ...args) {
const thisObject = {
module: moduleName,
database: databaseName,
table: tableName,
};
// Generate a new table definition by invoking the factory
const def = apply.call(factory, thisObject, args);
if (typeof def !== 'object' || def === null) {
throw new TypeError(`Virtual table module "${moduleName}" did not return a table definition object`);
}
return parseTableDefinition(def, 'returned', moduleName);
};
}
function parseTableDefinition(def, verb, moduleName) {
// Validate required properties
if (!hasOwnProperty.call(def, 'rows')) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition without a "rows" property`);
}
if (!hasOwnProperty.call(def, 'columns')) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition without a "columns" property`);
}
// Validate "rows" property
const rows = def.rows;
if (typeof rows !== 'function' || Object.getPrototypeOf(rows) !== GeneratorFunctionPrototype) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "rows" property (should be a generator function)`);
}
// Validate "columns" property
let columns = def.columns;
if (!Array.isArray(columns) || !(columns = [...columns]).every(x => typeof x === 'string')) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "columns" property (should be an array of strings)`);
}
if (columns.length !== new Set(columns).size) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with duplicate column names`);
}
if (!columns.length) {
throw new RangeError(`Virtual table module "${moduleName}" ${verb} a table definition with zero columns`);
}
// Validate "parameters" property
let parameters;
if (hasOwnProperty.call(def, 'parameters')) {
parameters = def.parameters;
if (!Array.isArray(parameters) || !(parameters = [...parameters]).every(x => typeof x === 'string')) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "parameters" property (should be an array of strings)`);
}
} else {
parameters = inferParameters(rows);
}
if (parameters.length !== new Set(parameters).size) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with duplicate parameter names`);
}
if (parameters.length > 32) {
throw new RangeError(`Virtual table module "${moduleName}" ${verb} a table definition with more than the maximum number of 32 parameters`);
}
for (const parameter of parameters) {
if (columns.includes(parameter)) {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with column "${parameter}" which was ambiguously defined as both a column and parameter`);
}
}
// Validate "safeIntegers" option
let safeIntegers = 2;
if (hasOwnProperty.call(def, 'safeIntegers')) {
const bool = def.safeIntegers;
if (typeof bool !== 'boolean') {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "safeIntegers" property (should be a boolean)`);
}
safeIntegers = +bool;
}
// Validate "directOnly" option
let directOnly = false;
if (hasOwnProperty.call(def, 'directOnly')) {
directOnly = def.directOnly;
if (typeof directOnly !== 'boolean') {
throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "directOnly" property (should be a boolean)`);
}
}
// Generate SQL for the virtual table definition
const columnDefinitions = [
...parameters.map(identifier).map(str => `${str} HIDDEN`),
...columns.map(identifier),
];
return [
`CREATE TABLE x(${columnDefinitions.join(', ')});`,
wrapGenerator(rows, new Map(columns.map((x, i) => [x, parameters.length + i])), moduleName),
parameters,
safeIntegers,
directOnly,
];
}
function wrapGenerator(generator, columnMap, moduleName) {
return function* virtualTable(...args) {
/*
We must defensively clone any buffers in the arguments, because
otherwise the generator could mutate one of them, which would cause
us to return incorrect values for hidden columns, potentially
corrupting the database.
*/
const output = args.map(x => Buffer.isBuffer(x) ? Buffer.from(x) : x);
for (let i = 0; i < columnMap.size; ++i) {
output.push(null); // Fill with nulls to prevent gaps in array (v8 optimization)
}
for (const row of generator(...args)) {
if (Array.isArray(row)) {
extractRowArray(row, output, columnMap.size, moduleName);
yield output;
} else if (typeof row === 'object' && row !== null) {
extractRowObject(row, output, columnMap, moduleName);
yield output;
} else {
throw new TypeError(`Virtual table module "${moduleName}" yielded something that isn't a valid row object`);
}
}
};
}
function extractRowArray(row, output, columnCount, moduleName) {
if (row.length !== columnCount) {
throw new TypeError(`Virtual table module "${moduleName}" yielded a row with an incorrect number of columns`);
}
const offset = output.length - columnCount;
for (let i = 0; i < columnCount; ++i) {
output[i + offset] = row[i];
}
}
function extractRowObject(row, output, columnMap, moduleName) {
let count = 0;
for (const key of Object.keys(row)) {
const index = columnMap.get(key);
if (index === undefined) {
throw new TypeError(`Virtual table module "${moduleName}" yielded a row with an undeclared column "${key}"`);
}
output[index] = row[key];
count += 1;
}
if (count !== columnMap.size) {
throw new TypeError(`Virtual table module "${moduleName}" yielded a row with missing columns`);
}
}
function inferParameters({ length }) {
if (!Number.isInteger(length) || length < 0) {
throw new TypeError('Expected function.length to be a positive integer');
}
const params = [];
for (let i = 0; i < length; ++i) {
params.push(`$${i + 1}`);
}
return params;
}
const { hasOwnProperty } = Object.prototype;
const { apply } = Function.prototype;
const GeneratorFunctionPrototype = Object.getPrototypeOf(function*(){});
const identifier = str => `"${str.replace(/"/g, '""')}"`;
const defer = x => () => x;

View File

@ -12,8 +12,7 @@ function SqliteError(message, code) {
descriptor.value = '' + message;
Object.defineProperty(this, 'message', descriptor);
Error.captureStackTrace(this, SqliteError);
descriptor.value = code;
Object.defineProperty(this, 'code', descriptor);
this.code = code;
}
Object.setPrototypeOf(SqliteError, Error);
Object.setPrototypeOf(SqliteError.prototype, Error.prototype);

View File

@ -1,14 +1,20 @@
{
"name": "better-sqlite3",
"version": "7.1.4",
"version": "7.5.0",
"description": "The fastest and simplest library for SQLite3 in Node.js.",
"homepage": "http://github.com/JoshuaWise/better-sqlite3",
"author": "Joshua Wise <joshuathomaswise@gmail.com>",
"main": "lib/index.js",
"repository": {
"type": "git",
"url": "git://github.com/JoshuaWise/better-sqlite3.git"
},
"main": "lib/index.js",
"files": [
"binding.gyp",
"src/*.[ch]pp",
"lib/**",
"deps/**"
],
"dependencies": {
"bindings": "^1.5.0",
"tar": "^6.1.0"
@ -16,10 +22,10 @@
"devDependencies": {
"chai": "^4.3.4",
"cli-color": "^2.0.0",
"fs-extra": "^9.1.0",
"fs-extra": "^10.0.0",
"mocha": "^8.3.2",
"nodemark": "^0.3.0",
"sqlite": "^4.0.19",
"sqlite": "^4.0.23",
"sqlite3": "^5.0.2"
},
"scripts": {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@
#include <vector>
#include <set>
#include <unordered_map>
#include <algorithm>
#include <sqlite3.h>
#include <node.h>
#include <node_object_wrap.h>
@ -23,23 +24,14 @@ class Backup;
#insert "objects/statement.lzz"
#insert "objects/statement-iterator.lzz"
#insert "objects/backup.lzz"
#insert "util/data-converter.lzz"
#insert "util/custom-function.lzz"
#insert "util/custom-aggregate.lzz"
#insert "util/custom-table.lzz"
#insert "util/data.lzz"
#insert "util/binder.lzz"
struct Addon {
Addon(v8::Isolate* isolate) : privileged_info(NULL), next_id(0), cs(isolate) {}
CopyablePersistent<v8::Function> Statement;
CopyablePersistent<v8::Function> StatementIterator;
CopyablePersistent<v8::Function> Backup;
CopyablePersistent<v8::Function> SqliteError;
NODE_ARGUMENTS_POINTER privileged_info;
sqlite3_uint64 next_id;
CS cs;
std::set<Database*, Database::CompareDatabase> dbs;
NODE_METHOD(JS_setErrorConstructor) {
REQUIRE_ARGUMENT_FUNCTION(first, v8::Local<v8::Function> SqliteError);
OnlyAddon->SqliteError.Reset(OnlyIsolate, SqliteError);
@ -52,9 +44,23 @@ struct Addon {
delete addon;
}
explicit Addon(v8::Isolate* isolate) :
privileged_info(NULL),
next_id(0),
cs(isolate) {}
inline sqlite3_uint64 NextId() {
return next_id++;
}
CopyablePersistent<v8::Function> Statement;
CopyablePersistent<v8::Function> StatementIterator;
CopyablePersistent<v8::Function> Backup;
CopyablePersistent<v8::Function> SqliteError;
NODE_ARGUMENTS_POINTER privileged_info;
sqlite3_uint64 next_id;
CS cs;
std::set<Database*, Database::CompareDatabase> dbs;
};
#src
@ -75,8 +81,8 @@ NODE_MODULE_INIT(/* exports, context */) {
exports->Set(context, InternalizedFromLatin1(isolate, "setErrorConstructor"), v8::FunctionTemplate::New(isolate, Addon::JS_setErrorConstructor, data)->GetFunction(context).ToLocalChecked()).FromJust();
// Store addon instance data.
addon->Statement.Reset(isolate, v8::Local<v8::Function>::Cast(exports->Get(context, InternalizedFromLatin1(isolate, "Statement")).ToLocalChecked()));
addon->StatementIterator.Reset(isolate, v8::Local<v8::Function>::Cast(exports->Get(context, InternalizedFromLatin1(isolate, "StatementIterator")).ToLocalChecked()));
addon->Backup.Reset(isolate, v8::Local<v8::Function>::Cast(exports->Get(context, InternalizedFromLatin1(isolate, "Backup")).ToLocalChecked()));
addon->Statement.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "Statement")).ToLocalChecked().As<v8::Function>());
addon->StatementIterator.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "StatementIterator")).ToLocalChecked().As<v8::Function>());
addon->Backup.Reset(isolate, exports->Get(context, InternalizedFromLatin1(isolate, "Backup")).ToLocalChecked().As<v8::Function>());
}
#end

View File

@ -25,7 +25,6 @@ public:
}
}
~Backup() {
if (alive) db->RemoveBackup(this);
CloseHandles();
@ -33,13 +32,20 @@ public:
private:
explicit Backup(Database* _db, sqlite3* _dest_handle, sqlite3_backup* _backup_handle, sqlite3_uint64 _id, bool _unlink) : node::ObjectWrap(),
db(_db),
dest_handle(_dest_handle),
backup_handle(_backup_handle),
id(_id),
explicit Backup(
Database* db,
sqlite3* dest_handle,
sqlite3_backup* backup_handle,
sqlite3_uint64 id,
bool unlink
) :
node::ObjectWrap(),
db(db),
dest_handle(dest_handle),
backup_handle(backup_handle),
id(id),
alive(true),
unlink(_unlink) {
unlink(unlink) {
assert(db != NULL);
assert(dest_handle != NULL);
assert(backup_handle != NULL);
@ -54,10 +60,10 @@ private:
REQUIRE_DATABASE_OPEN(db->GetState());
REQUIRE_DATABASE_NOT_BUSY(db->GetState());
v8::Local<v8::Object> database = v8::Local<v8::Object>::Cast((*addon->privileged_info)[0]);
v8::Local<v8::String> attachedName = v8::Local<v8::String>::Cast((*addon->privileged_info)[1]);
v8::Local<v8::String> destFile = v8::Local<v8::String>::Cast((*addon->privileged_info)[2]);
bool unlink = v8::Local<v8::Boolean>::Cast((*addon->privileged_info)[3])->Value();
v8::Local<v8::Object> database = (*addon->privileged_info)[0].As<v8::Object>();
v8::Local<v8::String> attachedName = (*addon->privileged_info)[1].As<v8::String>();
v8::Local<v8::String> destFile = (*addon->privileged_info)[2].As<v8::String>();
bool unlink = (*addon->privileged_info)[3].As<v8::Boolean>()->Value();
UseIsolate;
sqlite3* dest_handle;
@ -106,8 +112,8 @@ private:
UseIsolate;
UseContext;
v8::Local<v8::Object> result = v8::Object::New(isolate);
result->Set(ctx, CS::Get(isolate, addon->cs.totalPages), v8::Int32::New(isolate, total_pages)).FromJust();
result->Set(ctx, CS::Get(isolate, addon->cs.remainingPages), v8::Int32::New(isolate, remaining_pages)).FromJust();
result->Set(ctx, addon->cs.totalPages.Get(isolate), v8::Int32::New(isolate, total_pages)).FromJust();
result->Set(ctx, addon->cs.remainingPages.Get(isolate), v8::Int32::New(isolate, remaining_pages)).FromJust();
info.GetReturnValue().Set(result);
if (status == SQLITE_DONE) backup->unlink = false;
} else {

View File

@ -6,8 +6,10 @@ public:
SetPrototypeMethod(isolate, data, t, "prepare", JS_prepare);
SetPrototypeMethod(isolate, data, t, "exec", JS_exec);
SetPrototypeMethod(isolate, data, t, "backup", JS_backup);
SetPrototypeMethod(isolate, data, t, "serialize", JS_serialize);
SetPrototypeMethod(isolate, data, t, "function", JS_function);
SetPrototypeMethod(isolate, data, t, "aggregate", JS_aggregate);
SetPrototypeMethod(isolate, data, t, "table", JS_table);
SetPrototypeMethod(isolate, data, t, "loadExtension", JS_loadExtension);
SetPrototypeMethod(isolate, data, t, "close", JS_close);
SetPrototypeMethod(isolate, data, t, "defaultSafeIntegers", JS_defaultSafeIntegers);
@ -53,7 +55,7 @@ public:
StringFromUtf8(isolate, message, -1),
addon->cs.Code(isolate, code)
};
isolate->ThrowException(v8::Local<v8::Function>::New(isolate, addon->SqliteError)
isolate->ThrowException(addon->SqliteError.Get(isolate)
->NewInstance(OnlyContext, 2, args)
.ToLocalChecked());
}
@ -64,7 +66,7 @@ public:
if (!has_logger) return false;
char* expanded = sqlite3_expanded_sql(handle);
v8::Local<v8::Value> arg = StringFromUtf8(isolate, expanded ? expanded : sqlite3_sql(handle), -1);
was_js_error = v8::Local<v8::Function>::Cast(v8::Local<v8::Value>::New(isolate, logger))
was_js_error = logger.Get(isolate).As<v8::Function>()
->Call(OnlyContext, v8::Undefined(isolate), 1, &arg)
.IsEmpty();
if (expanded) sqlite3_free(expanded);
@ -121,20 +123,26 @@ public:
private:
explicit Database(sqlite3* _db_handle, v8::Isolate* isolate, Addon* _addon, v8::Local<v8::Value> _logger) : node::ObjectWrap(),
db_handle(_db_handle),
explicit Database(
v8::Isolate* isolate,
Addon* addon,
sqlite3* db_handle,
v8::Local<v8::Value> logger
) :
node::ObjectWrap(),
db_handle(db_handle),
open(true),
busy(false),
safe_ints(false),
unsafe_mode(false),
was_js_error(false),
has_logger(_logger->IsFunction()),
has_logger(logger->IsFunction()),
iterators(0),
addon(_addon),
logger(isolate, _logger),
addon(addon),
logger(isolate, logger),
stmts(),
backups() {
assert(_db_handle != NULL);
assert(db_handle != NULL);
addon->dbs.insert(this);
}
@ -147,6 +155,7 @@ private:
REQUIRE_ARGUMENT_BOOLEAN(fifth, bool must_exist);
REQUIRE_ARGUMENT_INT32(sixth, int timeout);
REQUIRE_ARGUMENT_ANY(seventh, v8::Local<v8::Value> logger);
REQUIRE_ARGUMENT_ANY(eighth, v8::Local<v8::Value> buffer);
UseAddon;
UseIsolate;
@ -173,8 +182,14 @@ private:
status = sqlite3_db_config(db_handle, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL);
assert(status == SQLITE_OK);
if (node::Buffer::HasInstance(buffer) && !Deserialize(buffer.As<v8::Object>(), addon, db_handle, readonly)) {
int status = sqlite3_close(db_handle);
assert(status == SQLITE_OK); ((void)status);
return;
}
UseContext;
Database* db = new Database(db_handle, isolate, addon, logger);
Database* db = new Database(isolate, addon, db_handle, logger);
db->Wrap(info.This());
SetFrozen(isolate, ctx, info.This(), addon->cs.memory, v8::Boolean::New(isolate, in_memory));
SetFrozen(isolate, ctx, info.This(), addon->cs.readonly, v8::Boolean::New(isolate, readonly));
@ -192,11 +207,11 @@ private:
(void)pragmaMode;
UseAddon;
UseIsolate;
v8::Local<v8::Function> c = v8::Local<v8::Function>::New(isolate, addon->Statement);
v8::Local<v8::Function> c = addon->Statement.Get(isolate);
addon->privileged_info = &info;
v8::MaybeLocal<v8::Object> maybe_statement = c->NewInstance(OnlyContext, 0, NULL);
v8::MaybeLocal<v8::Object> maybeStatement = c->NewInstance(OnlyContext, 0, NULL);
addon->privileged_info = NULL;
if (!maybe_statement.IsEmpty()) info.GetReturnValue().Set(maybe_statement.ToLocalChecked());
if (!maybeStatement.IsEmpty()) info.GetReturnValue().Set(maybeStatement.ToLocalChecked());
}
NODE_METHOD(JS_exec) {
@ -250,11 +265,33 @@ private:
(void)unlink;
UseAddon;
UseIsolate;
v8::Local<v8::Function> c = v8::Local<v8::Function>::New(isolate, addon->Backup);
v8::Local<v8::Function> c = addon->Backup.Get(isolate);
addon->privileged_info = &info;
v8::MaybeLocal<v8::Object> maybe_backup = c->NewInstance(OnlyContext, 0, NULL);
v8::MaybeLocal<v8::Object> maybeBackup = c->NewInstance(OnlyContext, 0, NULL);
addon->privileged_info = NULL;
if (!maybe_backup.IsEmpty()) info.GetReturnValue().Set(maybe_backup.ToLocalChecked());
if (!maybeBackup.IsEmpty()) info.GetReturnValue().Set(maybeBackup.ToLocalChecked());
}
NODE_METHOD(JS_serialize) {
Database* db = Unwrap<Database>(info.This());
REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> attachedName);
REQUIRE_DATABASE_OPEN(db);
REQUIRE_DATABASE_NOT_BUSY(db);
REQUIRE_DATABASE_NO_ITERATORS(db);
UseIsolate;
v8::String::Utf8Value attached_name(isolate, attachedName);
sqlite3_int64 length = -1;
unsigned char* data = sqlite3_serialize(db->db_handle, *attached_name, &length, 0);
if (!data && length) {
ThrowError("Out of memory");
return;
}
info.GetReturnValue().Set(
node::Buffer::New(isolate, reinterpret_cast<char*>(data), length, FreeSerialization, NULL).ToLocalChecked()
);
}
NODE_METHOD(JS_function) {
@ -264,16 +301,19 @@ private:
REQUIRE_ARGUMENT_INT32(third, int argc);
REQUIRE_ARGUMENT_INT32(fourth, int safe_ints);
REQUIRE_ARGUMENT_BOOLEAN(fifth, bool deterministic);
REQUIRE_ARGUMENT_BOOLEAN(sixth, bool direct_only);
REQUIRE_DATABASE_OPEN(db);
REQUIRE_DATABASE_NOT_BUSY(db);
REQUIRE_DATABASE_NO_ITERATORS(db);
UseIsolate;
v8::String::Utf8Value name(isolate, nameString);
int mask = deterministic ? SQLITE_UTF8 | SQLITE_DETERMINISTIC : SQLITE_UTF8;
int mask = SQLITE_UTF8;
if (deterministic) mask |= SQLITE_DETERMINISTIC;
if (direct_only) mask |= SQLITE_DIRECTONLY;
safe_ints = safe_ints < 2 ? safe_ints : static_cast<int>(db->safe_ints);
if (sqlite3_create_function_v2(db->db_handle, *name, argc, mask, new CustomFunction(isolate, db, fn, *name, safe_ints), CustomFunction::xFunc, NULL, NULL, CustomFunction::xDestroy) != SQLITE_OK) {
if (sqlite3_create_function_v2(db->db_handle, *name, argc, mask, new CustomFunction(isolate, db, *name, fn, safe_ints), CustomFunction::xFunc, NULL, NULL, CustomFunction::xDestroy) != SQLITE_OK) {
db->ThrowDatabaseError();
}
}
@ -288,6 +328,7 @@ private:
REQUIRE_ARGUMENT_INT32(sixth, int argc);
REQUIRE_ARGUMENT_INT32(seventh, int safe_ints);
REQUIRE_ARGUMENT_BOOLEAN(eighth, bool deterministic);
REQUIRE_ARGUMENT_BOOLEAN(ninth, bool direct_only);
REQUIRE_DATABASE_OPEN(db);
REQUIRE_DATABASE_NOT_BUSY(db);
REQUIRE_DATABASE_NO_ITERATORS(db);
@ -296,14 +337,36 @@ private:
v8::String::Utf8Value name(isolate, nameString);
auto xInverse = inverse->IsFunction() ? CustomAggregate::xInverse : NULL;
auto xValue = xInverse ? CustomAggregate::xValue : NULL;
int mask = deterministic ? SQLITE_UTF8 | SQLITE_DETERMINISTIC : SQLITE_UTF8;
int mask = SQLITE_UTF8;
if (deterministic) mask |= SQLITE_DETERMINISTIC;
if (direct_only) mask |= SQLITE_DIRECTONLY;
safe_ints = safe_ints < 2 ? safe_ints : static_cast<int>(db->safe_ints);
if (sqlite3_create_window_function(db->db_handle, *name, argc, mask, new CustomAggregate(isolate, db, start, step, inverse, result, *name, safe_ints), CustomAggregate::xStep, CustomAggregate::xFinal, xValue, xInverse, CustomAggregate::xDestroy) != SQLITE_OK) {
if (sqlite3_create_window_function(db->db_handle, *name, argc, mask, new CustomAggregate(isolate, db, *name, start, step, inverse, result, safe_ints), CustomAggregate::xStep, CustomAggregate::xFinal, xValue, xInverse, CustomAggregate::xDestroy) != SQLITE_OK) {
db->ThrowDatabaseError();
}
}
NODE_METHOD(JS_table) {
Database* db = Unwrap<Database>(info.This());
REQUIRE_ARGUMENT_FUNCTION(first, v8::Local<v8::Function> factory);
REQUIRE_ARGUMENT_STRING(second, v8::Local<v8::String> nameString);
REQUIRE_ARGUMENT_BOOLEAN(third, bool eponymous);
REQUIRE_DATABASE_OPEN(db);
REQUIRE_DATABASE_NOT_BUSY(db);
REQUIRE_DATABASE_NO_ITERATORS(db);
UseIsolate;
v8::String::Utf8Value name(isolate, nameString);
sqlite3_module* module = eponymous ? &CustomTable::EPONYMOUS_MODULE : &CustomTable::MODULE;
db->busy = true;
if (sqlite3_create_module_v2(db->db_handle, *name, module, new CustomTable(isolate, db, *name, factory), CustomTable::Destructor) != SQLITE_OK) {
db->ThrowDatabaseError();
}
db->busy = false;
}
NODE_METHOD(JS_loadExtension) {
Database* db = Unwrap<Database>(info.This());
v8::Local<v8::String> entryPoint;
@ -358,6 +421,35 @@ private:
info.GetReturnValue().Set(db->open && !static_cast<bool>(sqlite3_get_autocommit(db->db_handle)));
}
static bool Deserialize(v8::Local<v8::Object> buffer, Addon* addon, sqlite3* db_handle, bool readonly) {
size_t length = node::Buffer::Length(buffer);
unsigned char* data = (unsigned char*)sqlite3_malloc64(length);
unsigned int flags = SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE;
if (readonly) {
flags |= SQLITE_DESERIALIZE_READONLY;
}
if (length) {
if (!data) {
ThrowError("Out of memory");
return false;
}
memcpy(data, node::Buffer::Data(buffer), length);
}
int status = sqlite3_deserialize(db_handle, "main", data, length, length, flags);
if (status != SQLITE_OK) {
ThrowSqliteError(addon, status == SQLITE_ERROR ? "unable to deserialize database" : sqlite3_errstr(status), status);
return false;
}
return true;
}
static void FreeSerialization(char* data, void* _) {
sqlite3_free(data);
}
static const int MAX_BUFFER_SIZE = node::Buffer::kMaxLength > INT_MAX ? INT_MAX : static_cast<int>(node::Buffer::kMaxLength);
static const int MAX_STRING_SIZE = v8::String::kMaxLength > INT_MAX ? INT_MAX : static_cast<int>(v8::String::kMaxLength);

View File

@ -10,19 +10,19 @@ public:
}
// The ~Statement destructor currently covers any state this object creates.
// Additionally, we actually DON'T want to set stmt->locked or db_state
// Additionally, we actually DON'T want to revert stmt->locked or db_state
// ->iterators in this destructor, to ensure deterministic database access.
~StatementIterator() {}
private:
explicit StatementIterator(Statement* _stmt, bool _bound) : node::ObjectWrap(),
stmt(_stmt),
handle(_stmt->handle),
db_state(_stmt->db->GetState()),
bound(_bound),
safe_ints(_stmt->safe_ints),
mode(_stmt->mode),
explicit StatementIterator(Statement* stmt, bool bound) : node::ObjectWrap(),
stmt(stmt),
handle(stmt->handle),
db_state(stmt->db->GetState()),
bound(bound),
safe_ints(stmt->safe_ints),
mode(stmt->mode),
alive(true),
logged(!db_state->has_logger) {
assert(stmt != NULL);
@ -118,8 +118,8 @@ private:
static inline v8::Local<v8::Object> NewRecord(v8::Isolate* isolate, v8::Local<v8::Context> ctx, v8::Local<v8::Value> value, Addon* addon, bool done) {
v8::Local<v8::Object> record = v8::Object::New(isolate);
record->Set(ctx, CS::Get(isolate, addon->cs.value), value).FromJust();
record->Set(ctx, CS::Get(isolate, addon->cs.done), v8::Boolean::New(isolate, done)).FromJust();
record->Set(ctx, addon->cs.value.Get(isolate), value).FromJust();
record->Set(ctx, addon->cs.done.Get(isolate), v8::Boolean::New(isolate, done)).FromJust();
return record;
}

View File

@ -1,5 +1,4 @@
class Statement : public node::ObjectWrap {
friend class StatementIterator;
class Statement : public node::ObjectWrap { friend class StatementIterator;
public:
INIT(Init) {
@ -14,6 +13,7 @@ public:
SetPrototypeMethod(isolate, data, t, "raw", JS_raw);
SetPrototypeMethod(isolate, data, t, "safeIntegers", JS_safeIntegers);
SetPrototypeMethod(isolate, data, t, "columns", JS_columns);
SetPrototypeGetter(isolate, data, t, "busy", JS_busy);
return t->GetFunction(OnlyContext).ToLocalChecked();
}
@ -27,7 +27,7 @@ public:
if (has_bind_map) return &extras->bind_map;
BindMap* bind_map = &extras->bind_map;
int param_count = sqlite3_bind_parameter_count(handle);
for (int i=1; i<=param_count; ++i) {
for (int i = 1; i <= param_count; ++i) {
const char* name = sqlite3_bind_parameter_name(handle, i);
if (name != NULL) bind_map->Add(isolate, name + 1, i);
}
@ -53,22 +53,28 @@ private:
// A class for holding values that are less often used.
class Extras { friend class Statement;
explicit Extras(sqlite3_uint64 _id) : bind_map(0), id(_id) {}
explicit Extras(sqlite3_uint64 id) : bind_map(0), id(id) {}
BindMap bind_map;
const sqlite3_uint64 id;
};
explicit Statement(Database* _db, sqlite3_stmt* _handle, sqlite3_uint64 _id, bool _returns_data) : node::ObjectWrap(),
db(_db),
handle(_handle),
extras(new Extras(_id)),
explicit Statement(
Database* db,
sqlite3_stmt* handle,
sqlite3_uint64 id,
bool returns_data
) :
node::ObjectWrap(),
db(db),
handle(handle),
extras(new Extras(id)),
alive(true),
locked(false),
bound(false),
has_bind_map(false),
safe_ints(_db->GetState()->safe_ints),
safe_ints(db->GetState()->safe_ints),
mode(Data::FLAT),
returns_data(_returns_data) {
returns_data(returns_data) {
assert(db != NULL);
assert(handle != NULL);
assert(db->GetState()->open);
@ -86,9 +92,9 @@ private:
REQUIRE_DATABASE_OPEN(db->GetState());
REQUIRE_DATABASE_NOT_BUSY(db->GetState());
v8::Local<v8::String> source = v8::Local<v8::String>::Cast((*addon->privileged_info)[0]);
v8::Local<v8::Object> database = v8::Local<v8::Object>::Cast((*addon->privileged_info)[1]);
bool pragmaMode = v8::Local<v8::Boolean>::Cast((*addon->privileged_info)[2])->Value();
v8::Local<v8::String> source = (*addon->privileged_info)[0].As<v8::String>();
v8::Local<v8::Object> database = (*addon->privileged_info)[1].As<v8::Object>();
bool pragmaMode = (*addon->privileged_info)[2].As<v8::Boolean>()->Value();
int flags = SQLITE_PREPARE_PERSISTENT;
if (pragmaMode) {
@ -129,10 +135,11 @@ private:
}
UseContext;
bool returns_data = (sqlite3_stmt_readonly(handle) && sqlite3_column_count(handle) >= 1) || pragmaMode;
bool returns_data = sqlite3_column_count(handle) >= 1 || pragmaMode;
Statement* stmt = new Statement(db, handle, addon->NextId(), returns_data);
stmt->Wrap(info.This());
SetFrozen(isolate, ctx, info.This(), addon->cs.reader, v8::Boolean::New(isolate, returns_data));
SetFrozen(isolate, ctx, info.This(), addon->cs.readonly, v8::Boolean::New(isolate, sqlite3_stmt_readonly(handle) != 0));
SetFrozen(isolate, ctx, info.This(), addon->cs.source, source);
SetFrozen(isolate, ctx, info.This(), addon->cs.database, database);
@ -140,7 +147,7 @@ private:
}
NODE_METHOD(JS_run) {
STATEMENT_START(REQUIRE_STATEMENT_DOESNT_RETURN_DATA, DOES_MUTATE);
STATEMENT_START(ALLOW_ANY_STATEMENT, DOES_MUTATE);
sqlite3* db_handle = db->GetHandle();
int total_changes_before = sqlite3_total_changes(db_handle);
@ -151,11 +158,11 @@ private:
Addon* addon = db->GetAddon();
UseContext;
v8::Local<v8::Object> result = v8::Object::New(isolate);
result->Set(ctx, CS::Get(isolate, addon->cs.changes), v8::Int32::New(isolate, changes)).FromJust();
result->Set(ctx, CS::Get(isolate, addon->cs.lastInsertRowid),
result->Set(ctx, addon->cs.changes.Get(isolate), v8::Int32::New(isolate, changes)).FromJust();
result->Set(ctx, addon->cs.lastInsertRowid.Get(isolate),
stmt->safe_ints
? v8::Local<v8::Value>::Cast(v8::BigInt::New(isolate, id))
: v8::Local<v8::Value>::Cast(v8::Number::New(isolate, (double)id))
? v8::BigInt::New(isolate, id).As<v8::Value>()
: v8::Number::New(isolate, (double)id).As<v8::Value>()
).FromJust();
STATEMENT_RETURN(result);
}
@ -201,11 +208,11 @@ private:
NODE_METHOD(JS_iterate) {
UseAddon;
UseIsolate;
v8::Local<v8::Function> c = v8::Local<v8::Function>::New(isolate, addon->StatementIterator);
v8::Local<v8::Function> c = addon->StatementIterator.Get(isolate);
addon->privileged_info = &info;
v8::MaybeLocal<v8::Object> maybe_iter = c->NewInstance(OnlyContext, 0, NULL);
v8::MaybeLocal<v8::Object> maybeIterator = c->NewInstance(OnlyContext, 0, NULL);
addon->privileged_info = NULL;
if (!maybe_iter.IsEmpty()) info.GetReturnValue().Set(maybe_iter.ToLocalChecked());
if (!maybeIterator.IsEmpty()) info.GetReturnValue().Set(maybeIterator.ToLocalChecked());
}
NODE_METHOD(JS_bind) {
@ -273,13 +280,13 @@ private:
int column_count = sqlite3_column_count(stmt->handle);
v8::Local<v8::Array> columns = v8::Array::New(isolate);
v8::Local<v8::String> name = CS::Get(isolate, addon->cs.name);
v8::Local<v8::String> columnName = CS::Get(isolate, addon->cs.column);
v8::Local<v8::String> tableName = CS::Get(isolate, addon->cs.table);
v8::Local<v8::String> databaseName = CS::Get(isolate, addon->cs.database);
v8::Local<v8::String> typeName = CS::Get(isolate, addon->cs.type);
v8::Local<v8::String> name = addon->cs.name.Get(isolate);
v8::Local<v8::String> columnName = addon->cs.column.Get(isolate);
v8::Local<v8::String> tableName = addon->cs.table.Get(isolate);
v8::Local<v8::String> databaseName = addon->cs.database.Get(isolate);
v8::Local<v8::String> typeName = addon->cs.type.Get(isolate);
for (int i=0; i<column_count; ++i) {
for (int i = 0; i < column_count; ++i) {
v8::Local<v8::Object> column = v8::Object::New(isolate);
column->Set(ctx, name,
@ -304,6 +311,11 @@ private:
info.GetReturnValue().Set(columns);
}
NODE_GETTER(JS_busy) {
Statement* stmt = Unwrap<Statement>(info.This());
info.GetReturnValue().Set(stmt->alive && stmt->locked);
}
Database* const db;
sqlite3_stmt* const handle;
Extras* const extras;

View File

@ -1,10 +1,9 @@
class BindMap {
public:
// This class represents a mapping between a parameter name and its
// associated parameter index in a prepared statement.
class Pair {
friend class BindMap;
// This nested class represents a single mapping between a parameter name
// and its associated parameter index in a prepared statement.
class Pair { friend class BindMap;
public:
inline int GetIndex() {
@ -12,12 +11,13 @@ public:
}
inline v8::Local<v8::String> GetName(v8::Isolate* isolate) {
return v8::Local<v8::String>::New(isolate, name);
return name.Get(isolate);
}
private:
explicit Pair(v8::Isolate* isolate, const char* _name, int _index)
: name(isolate, InternalizedFromUtf8(isolate, _name, -1)), index(_index) {}
explicit Pair(v8::Isolate* isolate, const char* name, int index)
: name(isolate, InternalizedFromUtf8(isolate, name, -1)), index(index) {}
explicit Pair(v8::Isolate* isolate, Pair* pair)
: name(isolate, pair->name), index(pair->index) {}
@ -38,8 +38,13 @@ public:
FREE_ARRAY<Pair>(pairs);
}
inline Pair* GetPairs() { return pairs; }
inline int GetSize() { return length; }
inline Pair* GetPairs() {
return pairs;
}
inline int GetSize() {
return length;
}
// Adds a pair to the bind map, expanding the capacity if necessary.
void Add(v8::Isolate* isolate, const char* name, int index) {
@ -49,11 +54,12 @@ public:
}
private:
void Grow(v8::Isolate* isolate) {
assert(capacity == length);
capacity = (capacity << 1) | 2;
Pair* new_pairs = ALLOC_ARRAY<Pair>(capacity);
for (int i=0; i<length; ++i) {
for (int i = 0; i < length; ++i) {
new (new_pairs + i) Pair(isolate, pairs + i);
pairs[i].~Pair();
}

View File

@ -26,6 +26,7 @@ public:
}
private:
struct Result {
int count;
bool bound_object;
@ -83,7 +84,7 @@ private:
return 0;
}
int len = static_cast<int>(length);
for (int i=0; i<len; ++i) {
for (int i = 0; i < len; ++i) {
v8::MaybeLocal<v8::Value> maybeValue = arr->Get(ctx, i);
if (maybeValue.IsEmpty()) {
Fail(NULL, NULL);
@ -107,7 +108,7 @@ private:
BindMap::Pair* pairs = bind_map->GetPairs();
int len = bind_map->GetSize();
for (int i=0; i<len; ++i) {
for (int i = 0; i < len; ++i) {
v8::Local<v8::String> key = pairs[i].GetName(isolate);
// Check if the named parameter was provided.
@ -118,7 +119,7 @@ private:
}
if (!has_property.FromJust()) {
v8::String::Utf8Value param_name(isolate, key);
Fail(ThrowRangeError, CONCAT("Missing named parameter \"", *param_name, "\"").c_str());
Fail(ThrowRangeError, (std::string("Missing named parameter \"") + *param_name + "\"").c_str());
return i;
}
@ -150,17 +151,17 @@ private:
int count = 0;
bool bound_object = false;
for (int i=0; i<argc; ++i) {
for (int i = 0; i < argc; ++i) {
v8::Local<v8::Value> arg = info[i];
if (arg->IsArray()) {
count += BindArray(isolate, v8::Local<v8::Array>::Cast(arg));
count += BindArray(isolate, arg.As<v8::Array>());
if (!success) break;
continue;
}
if (arg->IsObject() && !node::Buffer::HasInstance(arg)) {
v8::Local<v8::Object> obj = v8::Local<v8::Object>::Cast(arg);
v8::Local<v8::Object> obj = arg.As<v8::Object>();
if (IsPlainObject(isolate, obj)) {
if (bound_object) {
Fail(ThrowTypeError, "You cannot specify named parameters in two different objects");
@ -171,6 +172,9 @@ private:
count += BindObject(isolate, obj, stmt);
if (!success) break;
continue;
} else if (stmt->GetBindMap(isolate)->GetSize()) {
Fail(ThrowTypeError, "Named parameters can only be passed within plain objects");
break;
}
}

View File

@ -1,13 +1,10 @@
class CS {
public:
static inline v8::Local<v8::String> Get(v8::Isolate* isolate, CopyablePersistent<v8::String>& constant) {
return v8::Local<v8::String>::New(isolate, constant);
}
v8::Local<v8::String> Code(v8::Isolate* isolate, int code) {
auto element = codes.find(code);
if (element != codes.end()) return v8::Local<v8::String>::New(isolate, element->second);
return StringFromUtf8(isolate, CONCAT("UNKNOWN_SQLITE_ERROR_", std::to_string(code).c_str(), "").c_str(), -1);
if (element != codes.end()) return element->second.Get(isolate);
return StringFromUtf8(isolate, (std::string("UNKNOWN_SQLITE_ERROR_") + std::to_string(code)).c_str(), -1);
}
explicit CS(v8::Isolate* isolate) {
@ -139,11 +136,15 @@ public:
CopyablePersistent<v8::String> remainingPages;
private:
static void SetString(v8::Isolate* isolate, CopyablePersistent<v8::String>& constant, const char* str) {
constant.Reset(isolate, InternalizedFromLatin1(isolate, str));
}
void SetCode(v8::Isolate* isolate, int code, const char* str) {
codes.emplace(std::piecewise_construct, std::forward_as_tuple(code), std::forward_as_tuple(isolate, InternalizedFromLatin1(isolate, str)));
codes.emplace(std::piecewise_construct,
std::forward_as_tuple(code),
std::forward_as_tuple(isolate, InternalizedFromLatin1(isolate, str)));
}
std::unordered_map<int, CopyablePersistent<v8::String> > codes;

View File

@ -1,8 +1,22 @@
class CustomAggregate : public CustomFunction {
public:
explicit CustomAggregate(v8::Isolate* _isolate, Database* _db, v8::Local<v8::Value> _start, v8::Local<v8::Function> _step, v8::Local<v8::Value> _inverse, v8::Local<v8::Value> _result, const char* _name, bool _safe_ints)
: CustomFunction(_isolate, _db, _step, _name, _safe_ints), invoke_result(_result->IsFunction()), invoke_start(_start->IsFunction()), inverse(_isolate, _inverse->IsFunction() ? v8::Local<v8::Function>::Cast(_inverse) : v8::Local<v8::Function>()), result(_isolate, _result->IsFunction() ? v8::Local<v8::Function>::Cast(_result) : v8::Local<v8::Function>()), start(_isolate, _start) {}
explicit CustomAggregate(
v8::Isolate* isolate,
Database* db,
const char* name,
v8::Local<v8::Value> start,
v8::Local<v8::Function> step,
v8::Local<v8::Value> inverse,
v8::Local<v8::Value> result,
bool safe_ints
) :
CustomFunction(isolate, db, name, step, safe_ints),
invoke_result(result->IsFunction()),
invoke_start(start->IsFunction()),
inverse(isolate, inverse->IsFunction() ? inverse.As<v8::Function>() : v8::Local<v8::Function>()),
result(isolate, result->IsFunction() ? result.As<v8::Function>() : v8::Local<v8::Function>()),
start(isolate, start) {}
static void xStep(sqlite3_context* invocation, int argc, sqlite3_value** argv) {
xStepBase(invocation, argc, argv, &CustomAggregate::fn);
@ -21,22 +35,23 @@ public:
}
private:
static inline void xStepBase(sqlite3_context* invocation, int argc, sqlite3_value** argv, const CopyablePersistent<v8::Function> CustomAggregate::*ptrtm) {
AGGREGATE_START();
v8::Local<v8::Value> args_fast[5];
v8::Local<v8::Value>* args = argc <= 4 ? args_fast : ALLOC_ARRAY<v8::Local<v8::Value>>(argc + 1);
args[0] = v8::Local<v8::Value>::New(isolate, acc->value);
args[0] = acc->value.Get(isolate);
if (argc != 0) Data::GetArgumentsJS(isolate, args + 1, argv, argc, self->safe_ints);
v8::MaybeLocal<v8::Value> maybe_return_value = v8::Local<v8::Function>::New(isolate, self->*ptrtm)->Call(OnlyContext, v8::Undefined(isolate), argc + 1, args);
v8::MaybeLocal<v8::Value> maybeReturnValue = (self->*ptrtm).Get(isolate)->Call(OnlyContext, v8::Undefined(isolate), argc + 1, args);
if (args != args_fast) delete[] args;
if (maybe_return_value.IsEmpty()) {
if (maybeReturnValue.IsEmpty()) {
self->PropagateJSError(invocation);
} else {
v8::Local<v8::Value> return_value = maybe_return_value.ToLocalChecked();
if (!return_value->IsUndefined()) acc->value.Reset(isolate, return_value);
v8::Local<v8::Value> returnValue = maybeReturnValue.ToLocalChecked();
if (!returnValue->IsUndefined()) acc->value.Reset(isolate, returnValue);
}
}
@ -50,14 +65,14 @@ private:
return;
}
v8::Local<v8::Value> result = v8::Local<v8::Value>::New(isolate, acc->value);
v8::Local<v8::Value> result = acc->value.Get(isolate);
if (self->invoke_result) {
v8::MaybeLocal<v8::Value> maybe_result = v8::Local<v8::Function>::New(isolate, self->result)->Call(OnlyContext, v8::Undefined(isolate), 1, &result);
if (maybe_result.IsEmpty()) {
v8::MaybeLocal<v8::Value> maybeResult = self->result.Get(isolate)->Call(OnlyContext, v8::Undefined(isolate), 1, &result);
if (maybeResult.IsEmpty()) {
self->PropagateJSError(invocation);
return;
}
result = maybe_result.ToLocalChecked();
result = maybeResult.ToLocalChecked();
}
Data::ResultValueFromJS(isolate, invocation, result, self);
@ -76,9 +91,9 @@ private:
assert(acc->value.IsEmpty());
acc->initialized = true;
if (invoke_start) {
v8::MaybeLocal<v8::Value> maybe_seed = v8::Local<v8::Function>::Cast(v8::Local<v8::Value>::New(isolate, start))->Call(OnlyContext, v8::Undefined(isolate), 0, NULL);
if (maybe_seed.IsEmpty()) PropagateJSError(invocation);
else acc->value.Reset(isolate, maybe_seed.ToLocalChecked());
v8::MaybeLocal<v8::Value> maybeSeed = start.Get(isolate).As<v8::Function>()->Call(OnlyContext, v8::Undefined(isolate), 0, NULL);
if (maybeSeed.IsEmpty()) PropagateJSError(invocation);
else acc->value.Reset(isolate, maybeSeed.ToLocalChecked());
} else {
assert(!start.IsEmpty());
acc->value.Reset(isolate, start);

View File

@ -1,9 +1,20 @@
class CustomFunction {
class CustomFunction : protected DataConverter {
public:
explicit CustomFunction(v8::Isolate* _isolate, Database* _db, v8::Local<v8::Function> _fn, const char* _name, bool _safe_ints)
: name(COPY(_name)), db(_db), isolate(_isolate), fn(_isolate, _fn), safe_ints(_safe_ints) {}
virtual ~CustomFunction() { delete[] name; }
explicit CustomFunction(
v8::Isolate* isolate,
Database* db,
const char* name,
v8::Local<v8::Function> fn,
bool safe_ints
) :
name(name),
db(db),
isolate(isolate),
fn(isolate, fn),
safe_ints(safe_ints) {}
virtual ~CustomFunction() {}
static void xDestroy(void* self) {
delete static_cast<CustomFunction*>(self);
@ -19,31 +30,27 @@ public:
Data::GetArgumentsJS(isolate, args, argv, argc, self->safe_ints);
}
v8::MaybeLocal<v8::Value> maybe_return_value = v8::Local<v8::Function>::New(isolate, self->fn)->Call(OnlyContext, v8::Undefined(isolate), argc, args);
v8::MaybeLocal<v8::Value> maybeReturnValue = self->fn.Get(isolate)->Call(OnlyContext, v8::Undefined(isolate), argc, args);
if (args != args_fast) delete[] args;
if (maybe_return_value.IsEmpty()) self->PropagateJSError(invocation);
else Data::ResultValueFromJS(isolate, invocation, maybe_return_value.ToLocalChecked(), self);
}
void ThrowResultValueError(sqlite3_context* invocation, bool isBigInt) {
if (isBigInt) {
ThrowRangeError(CONCAT("User-defined function ", name, "() returned a bigint that was too big").c_str());
} else {
ThrowTypeError(CONCAT("User-defined function ", name, "() returned an invalid value").c_str());
}
PropagateJSError(invocation);
if (maybeReturnValue.IsEmpty()) self->PropagateJSError(invocation);
else Data::ResultValueFromJS(isolate, invocation, maybeReturnValue.ToLocalChecked(), self);
}
protected:
virtual void PropagateJSError(sqlite3_context* invocation) {
void PropagateJSError(sqlite3_context* invocation) {
assert(db->GetState()->was_js_error == false);
db->GetState()->was_js_error = true;
sqlite3_result_error(invocation, "", 0);
}
std::string GetDataErrorPrefix() {
return std::string("User-defined function ") + name + "() returned";
}
private:
const char* const name;
const std::string name;
Database* const db;
protected:
v8::Isolate* const isolate;

397
src/util/custom-table.lzz Normal file
View File

@ -0,0 +1,397 @@
class CustomTable {
public:
explicit CustomTable(
v8::Isolate* isolate,
Database* db,
const char* name,
v8::Local<v8::Function> factory
) :
addon(db->GetAddon()),
isolate(isolate),
db(db),
name(name),
factory(isolate, factory) {}
static void Destructor(void* self) {
delete static_cast<CustomTable*>(self);
}
static sqlite3_module MODULE = {
0, /* iVersion */
xCreate, /* xCreate */
xConnect, /* xConnect */
xBestIndex, /* xBestIndex */
xDisconnect, /* xDisconnect */
xDisconnect, /* xDestroy */
xOpen, /* xOpen */
xClose, /* xClose */
xFilter, /* xFilter */
xNext, /* xNext */
xEof, /* xEof */
xColumn, /* xColumn */
xRowid, /* xRowid */
NULL, /* xUpdate */
NULL, /* xBegin */
NULL, /* xSync */
NULL, /* xCommit */
NULL, /* xRollback */
NULL, /* xFindMethod */
NULL, /* xRename */
NULL, /* xSavepoint */
NULL, /* xRelease */
NULL, /* xRollbackTo */
NULL /* xShadowName */
};
static sqlite3_module EPONYMOUS_MODULE = {
0, /* iVersion */
NULL, /* xCreate */
xConnect, /* xConnect */
xBestIndex, /* xBestIndex */
xDisconnect, /* xDisconnect */
xDisconnect, /* xDestroy */
xOpen, /* xOpen */
xClose, /* xClose */
xFilter, /* xFilter */
xNext, /* xNext */
xEof, /* xEof */
xColumn, /* xColumn */
xRowid, /* xRowid */
NULL, /* xUpdate */
NULL, /* xBegin */
NULL, /* xSync */
NULL, /* xCommit */
NULL, /* xRollback */
NULL, /* xFindMethod */
NULL, /* xRename */
NULL, /* xSavepoint */
NULL, /* xRelease */
NULL, /* xRollbackTo */
NULL /* xShadowName */
};
private:
// This nested class is instantiated on each CREATE VIRTUAL TABLE statement.
class VTab { friend class CustomTable;
explicit VTab(
CustomTable* parent,
v8::Local<v8::Function> generator,
std::vector<std::string> parameter_names,
bool safe_ints
) :
parent(parent),
parameter_count(parameter_names.size()),
safe_ints(safe_ints),
generator(parent->isolate, generator),
parameter_names(parameter_names) {
((void)base);
}
static inline CustomTable::VTab* Upcast(sqlite3_vtab* vtab) {
return reinterpret_cast<VTab*>(vtab);
}
inline sqlite3_vtab* Downcast() {
return reinterpret_cast<sqlite3_vtab*>(this);
}
sqlite3_vtab base;
CustomTable * const parent;
const int parameter_count;
const bool safe_ints;
const CopyablePersistent<v8::Function> generator;
const std::vector<std::string> parameter_names;
};
// This nested class is instantiated each time a virtual table is scanned.
class Cursor { friend class CustomTable;
static inline CustomTable::Cursor* Upcast(sqlite3_vtab_cursor* cursor) {
return reinterpret_cast<Cursor*>(cursor);
}
inline sqlite3_vtab_cursor* Downcast() {
return reinterpret_cast<sqlite3_vtab_cursor*>(this);
}
inline CustomTable::VTab* GetVTab() {
return VTab::Upcast(base.pVtab);
}
sqlite3_vtab_cursor base;
CopyablePersistent<v8::Object> iterator;
CopyablePersistent<v8::Function> next;
CopyablePersistent<v8::Array> row;
bool done;
sqlite_int64 rowid;
};
// This nested class is used by Data::ResultValueFromJS to report errors.
class TempDataConverter : DataConverter { friend class CustomTable;
explicit TempDataConverter(CustomTable* parent) :
parent(parent),
status(SQLITE_OK) {}
void PropagateJSError(sqlite3_context* invocation) {
status = SQLITE_ERROR;
parent->PropagateJSError();
}
std::string GetDataErrorPrefix() {
return std::string("Virtual table module \"") + parent->name + "\" yielded";
}
CustomTable * const parent;
int status;
};
// Although this function does nothing, we cannot use xConnect directly,
// because that would cause SQLite to register an eponymous virtual table.
static int xCreate(sqlite3* db_handle, void* _self, int argc, const char* const * argv, sqlite3_vtab** output, char** errOutput) {
return xConnect(db_handle, _self, argc, argv, output, errOutput);
}
// This method uses the factory function to instantiate a new virtual table.
static int xConnect(sqlite3* db_handle, void* _self, int argc, const char* const * argv, sqlite3_vtab** output, char** errOutput) {
CustomTable* self = static_cast<CustomTable*>(_self);
v8::Isolate* isolate = self->isolate;
v8::HandleScope scope(isolate);
UseContext;
v8::Local<v8::Value>* args = ALLOC_ARRAY<v8::Local<v8::Value>>(argc);
for (int i = 0; i < argc; ++i) {
args[i] = StringFromUtf8(isolate, argv[i], -1);
}
// Run the factory function to receive a new virtual table definition.
v8::MaybeLocal<v8::Value> maybeReturnValue = self->factory.Get(isolate)->Call(ctx, v8::Undefined(isolate), argc, args);
delete[] args;
if (maybeReturnValue.IsEmpty()) {
self->PropagateJSError();
return SQLITE_ERROR;
}
// Extract each part of the virtual table definition.
v8::Local<v8::Array> returnValue = maybeReturnValue.ToLocalChecked().As<v8::Array>();
v8::Local<v8::String> sqlString = returnValue->Get(ctx, 0).ToLocalChecked().As<v8::String>();
v8::Local<v8::Function> generator = returnValue->Get(ctx, 1).ToLocalChecked().As<v8::Function>();
v8::Local<v8::Array> parameterNames = returnValue->Get(ctx, 2).ToLocalChecked().As<v8::Array>();
int safe_ints = returnValue->Get(ctx, 3).ToLocalChecked().As<v8::Int32>()->Value();
bool direct_only = returnValue->Get(ctx, 4).ToLocalChecked().As<v8::Boolean>()->Value();
v8::String::Utf8Value sql(isolate, sqlString);
safe_ints = safe_ints < 2 ? safe_ints : static_cast<int>(self->db->GetState()->safe_ints);
// Copy the parameter names into a std::vector.
std::vector<std::string> parameter_names;
for (int i = 0, len = parameterNames->Length(); i < len; ++i) {
v8::Local<v8::String> parameterName = parameterNames->Get(ctx, i).ToLocalChecked().As<v8::String>();
v8::String::Utf8Value parameter_name(isolate, parameterName);
parameter_names.emplace_back(*parameter_name);
}
// Pass our SQL table definition to SQLite (this should never fail).
if (sqlite3_declare_vtab(db_handle, *sql) != SQLITE_OK) {
*errOutput = sqlite3_mprintf("failed to declare virtual table \"%s\"", argv[2]);
return SQLITE_ERROR;
}
if (direct_only && sqlite3_vtab_config(db_handle, SQLITE_VTAB_DIRECTONLY) != SQLITE_OK) {
*errOutput = sqlite3_mprintf("failed to configure virtual table \"%s\"", argv[2]);
return SQLITE_ERROR;
}
// Return the successfully created virtual table.
*output = (new VTab(self, generator, parameter_names, safe_ints))->Downcast();
return SQLITE_OK;
}
static int xDisconnect(sqlite3_vtab* vtab) {
delete VTab::Upcast(vtab);
return SQLITE_OK;
}
static int xOpen(sqlite3_vtab* vtab, sqlite3_vtab_cursor** output) {
*output = (new Cursor())->Downcast();
return SQLITE_OK;
}
static int xClose(sqlite3_vtab_cursor* cursor) {
delete Cursor::Upcast(cursor);
return SQLITE_OK;
}
// This method uses a fresh cursor to start a new scan of a virtual table.
// The args and idxNum are provided by xBestIndex (idxStr is unused).
// idxNum is a bitmap that provides the proper indices of the received args.
static int xFilter(sqlite3_vtab_cursor* _cursor, int idxNum, const char* idxStr, int argc, sqlite3_value** argv) {
Cursor* cursor = Cursor::Upcast(_cursor);
VTab* vtab = cursor->GetVTab();
CustomTable* self = vtab->parent;
Addon* addon = self->addon;
v8::Isolate* isolate = self->isolate;
v8::HandleScope scope(isolate);
UseContext;
// Convert the SQLite arguments into JavaScript arguments. Note that
// the values in argv may be in the wrong order, so we fix that here.
v8::Local<v8::Value> args_fast[4];
v8::Local<v8::Value>* args = NULL;
int parameter_count = vtab->parameter_count;
if (parameter_count != 0) {
args = parameter_count <= 4 ? args_fast : ALLOC_ARRAY<v8::Local<v8::Value>>(parameter_count);
int argn = 0;
bool safe_ints = vtab->safe_ints;
for (int i = 0; i < parameter_count; ++i) {
if (idxNum & 1 << i) {
args[i] = Data::GetValueJS(isolate, argv[argn++], safe_ints);
// If any arguments are NULL, the result set is necessarily
// empty, so don't bother to run the generator function.
if (args[i]->IsNull()) {
if (args != args_fast) delete[] args;
cursor->done = true;
return SQLITE_OK;
}
} else {
args[i] = v8::Undefined(isolate);
}
}
}
// Invoke the generator function to create a new iterator.
v8::MaybeLocal<v8::Value> maybeIterator = vtab->generator.Get(isolate)->Call(ctx, v8::Undefined(isolate), parameter_count, args);
if (args != args_fast) delete[] args;
if (maybeIterator.IsEmpty()) {
self->PropagateJSError();
return SQLITE_ERROR;
}
// Store the iterator and its next() method; we'll be using it a lot.
v8::Local<v8::Object> iterator = maybeIterator.ToLocalChecked().As<v8::Object>();
v8::Local<v8::Function> next = iterator->Get(ctx, addon->cs.next.Get(isolate)).ToLocalChecked().As<v8::Function>();
cursor->iterator.Reset(isolate, iterator);
cursor->next.Reset(isolate, next);
cursor->rowid = 0;
// Advance the iterator/cursor to the first row.
return xNext(cursor->Downcast());
}
// This method advances a virtual table's cursor to the next row.
// SQLite will call this method repeatedly, driving the generator function.
static int xNext(sqlite3_vtab_cursor* _cursor) {
Cursor* cursor = Cursor::Upcast(_cursor);
CustomTable* self = cursor->GetVTab()->parent;
Addon* addon = self->addon;
v8::Isolate* isolate = self->isolate;
v8::HandleScope scope(isolate);
UseContext;
v8::Local<v8::Object> iterator = cursor->iterator.Get(isolate);
v8::Local<v8::Function> next = cursor->next.Get(isolate);
v8::MaybeLocal<v8::Value> maybeRecord = next->Call(ctx, iterator, 0, NULL);
if (maybeRecord.IsEmpty()) {
self->PropagateJSError();
return SQLITE_ERROR;
}
v8::Local<v8::Object> record = maybeRecord.ToLocalChecked().As<v8::Object>();
bool done = record->Get(ctx, addon->cs.done.Get(isolate)).ToLocalChecked().As<v8::Boolean>()->Value();
if (!done) {
cursor->row.Reset(isolate, record->Get(ctx, addon->cs.value.Get(isolate)).ToLocalChecked().As<v8::Array>());
}
cursor->done = done;
cursor->rowid += 1;
return SQLITE_OK;
}
// If this method returns 1, SQLite will stop scanning the virtual table.
static int xEof(sqlite3_vtab_cursor* cursor) {
return Cursor::Upcast(cursor)->done;
}
// This method extracts some column from the cursor's current row.
static int xColumn(sqlite3_vtab_cursor* _cursor, sqlite3_context* invocation, int column) {
Cursor* cursor = Cursor::Upcast(_cursor);
CustomTable* self = cursor->GetVTab()->parent;
TempDataConverter temp_data_converter(self);
v8::Isolate* isolate = self->isolate;
v8::HandleScope scope(isolate);
v8::Local<v8::Array> row = cursor->row.Get(isolate);
v8::MaybeLocal<v8::Value> maybeColumnValue = row->Get(OnlyContext, column);
if (maybeColumnValue.IsEmpty()) {
temp_data_converter.PropagateJSError(NULL);
} else {
Data::ResultValueFromJS(isolate, invocation, maybeColumnValue.ToLocalChecked(), &temp_data_converter);
}
return temp_data_converter.status;
}
// This method outputs the rowid of the cursor's current row.
static int xRowid(sqlite3_vtab_cursor* cursor, sqlite_int64* output) {
*output = Cursor::Upcast(cursor)->rowid;
return SQLITE_OK;
}
// This method tells SQLite how to *plan* queries on our virtual table.
// It gets invoked (typically multiple times) during db.prepare().
static int xBestIndex(sqlite3_vtab* vtab, sqlite3_index_info* output) {
int parameter_count = VTab::Upcast(vtab)->parameter_count;
int argument_count = 0;
std::vector<std::pair<int, int>> forwarded;
for (int i = 0, len = output->nConstraint; i < len; ++i) {
auto item = output->aConstraint[i];
// We only care about constraints on parameters, not regular columns.
if (item.iColumn >= 0 && item.iColumn < parameter_count) {
if (item.op != SQLITE_INDEX_CONSTRAINT_EQ) {
sqlite3_free(vtab->zErrMsg);
vtab->zErrMsg = sqlite3_mprintf(
"virtual table parameter \"%s\" can only be constrained by the '=' operator",
VTab::Upcast(vtab)->parameter_names.at(item.iColumn).c_str());
return SQLITE_ERROR;
}
if (!item.usable) {
// Don't allow SQLite to make plans that ignore arguments.
// Otherwise, a user could pass arguments, but then they
// could appear undefined in the generator function.
return SQLITE_CONSTRAINT;
}
forwarded.emplace_back(item.iColumn, i);
}
}
// Tell SQLite to forward arguments to xFilter.
std::sort(forwarded.begin(), forwarded.end());
for (std::pair<int, int> pair : forwarded) {
int bit = 1 << pair.first;
if (!(output->idxNum & bit)) {
output->idxNum |= bit;
output->aConstraintUsage[pair.second].argvIndex = ++argument_count;
output->aConstraintUsage[pair.second].omit = 1;
}
}
// Use a very high estimated cost so SQLite is not tempted to invoke the
// generator function within a loop, if it can be avoided.
output->estimatedCost = output->estimatedRows = 1000000000 / (argument_count + 1);
return SQLITE_OK;
}
void PropagateJSError() {
assert(db->GetState()->was_js_error == false);
db->GetState()->was_js_error = true;
}
Addon* const addon;
v8::Isolate* const isolate;
Database* const db;
const std::string name;
const CopyablePersistent<v8::Function> factory;
};

View File

@ -0,0 +1,17 @@
class DataConverter {
public:
void ThrowDataConversionError(sqlite3_context* invocation, bool isBigInt) {
if (isBigInt) {
ThrowRangeError((GetDataErrorPrefix() + " a bigint that was too big").c_str());
} else {
ThrowTypeError((GetDataErrorPrefix() + " an invalid value").c_str());
}
PropagateJSError(invocation);
}
protected:
virtual void PropagateJSError(sqlite3_context* invocation) = 0;
virtual std::string GetDataErrorPrefix() = 0;
};

View File

@ -2,19 +2,16 @@
if (value->IsNumber()) { \
return sqlite3_##to##_double( \
__VA_ARGS__, \
v8::Local<v8::Number>::Cast(value)->Value() \
value.As<v8::Number>()->Value() \
); \
} else if (value->IsBigInt()) { \
bool lossless; \
int64_t v = v8::Local<v8::BigInt>::Cast(value)->Int64Value(&lossless); \
int64_t v = value.As<v8::BigInt>()->Int64Value(&lossless); \
if (lossless) { \
return sqlite3_##to##_int64(__VA_ARGS__, v); \
} \
} else if (value->IsString()) { \
v8::String::Utf8Value utf8( \
isolate, \
v8::Local<v8::String>::Cast(value) \
); \
v8::String::Utf8Value utf8(isolate, value.As<v8::String>()); \
return sqlite3_##to##_text( \
__VA_ARGS__, \
*utf8, \
@ -22,9 +19,10 @@
SQLITE_TRANSIENT \
); \
} else if (node::Buffer::HasInstance(value)) { \
const char* data = node::Buffer::Data(value); \
return sqlite3_##to##_blob( \
__VA_ARGS__, \
node::Buffer::Data(value), \
data ? data : "", \
node::Buffer::Length(value), \
SQLITE_TRANSIENT \
); \
@ -82,7 +80,7 @@ namespace Data {
v8::Local<v8::Value> GetFlatRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, sqlite3_stmt* handle, bool safe_ints) {
v8::Local<v8::Object> row = v8::Object::New(isolate);
int column_count = sqlite3_column_count(handle);
for (int i=0; i<column_count; ++i) {
for (int i = 0; i < column_count; ++i) {
row->Set(ctx,
InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1),
Data::GetValueJS(isolate, handle, i, safe_ints)).FromJust();
@ -93,13 +91,13 @@ namespace Data {
v8::Local<v8::Value> GetExpandedRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, sqlite3_stmt* handle, bool safe_ints) {
v8::Local<v8::Object> row = v8::Object::New(isolate);
int column_count = sqlite3_column_count(handle);
for (int i=0; i<column_count; ++i) {
for (int i = 0; i < column_count; ++i) {
const char* table_raw = sqlite3_column_table_name(handle, i);
v8::Local<v8::String> table = InternalizedFromUtf8(isolate, table_raw == NULL ? "$" : table_raw, -1);
v8::Local<v8::String> column = InternalizedFromUtf8(isolate, sqlite3_column_name(handle, i), -1);
v8::Local<v8::Value> value = Data::GetValueJS(isolate, handle, i, safe_ints);
if (row->HasOwnProperty(ctx, table).FromJust()) {
v8::Local<v8::Object>::Cast(row->Get(ctx, table).ToLocalChecked())->Set(ctx, column, value).FromJust();
row->Get(ctx, table).ToLocalChecked().As<v8::Object>()->Set(ctx, column, value).FromJust();
} else {
v8::Local<v8::Object> nested = v8::Object::New(isolate);
row->Set(ctx, table, nested).FromJust();
@ -112,7 +110,7 @@ namespace Data {
v8::Local<v8::Value> GetRawRowJS(v8::Isolate* isolate, v8::Local<v8::Context> ctx, sqlite3_stmt* handle, bool safe_ints) {
v8::Local<v8::Array> row = v8::Array::New(isolate);
int column_count = sqlite3_column_count(handle);
for (int i=0; i<column_count; ++i) {
for (int i = 0; i < column_count; ++i) {
row->Set(ctx, i, Data::GetValueJS(isolate, handle, i, safe_ints)).FromJust();
}
return row;
@ -129,7 +127,7 @@ namespace Data {
void GetArgumentsJS(v8::Isolate* isolate, v8::Local<v8::Value>* out, sqlite3_value** values, int argument_count, bool safe_ints) {
assert(argument_count > 0);
for (int i=0; i<argument_count; ++i) {
for (int i = 0; i < argument_count; ++i) {
out[i] = Data::GetValueJS(isolate, values[i], safe_ints);
}
}
@ -139,9 +137,9 @@ namespace Data {
return value->IsBigInt() ? SQLITE_TOOBIG : -1;
}
void ResultValueFromJS(v8::Isolate* isolate, sqlite3_context* invocation, v8::Local<v8::Value> value, CustomFunction* function) {
void ResultValueFromJS(v8::Isolate* isolate, sqlite3_context* invocation, v8::Local<v8::Value> value, DataConverter* converter) {
JS_VALUE_TO_SQLITE(result, value, isolate, invocation);
function->ThrowResultValueError(invocation, value->IsBigInt());
converter->ThrowDataConversionError(invocation, value->IsBigInt());
}
}

View File

@ -7,7 +7,7 @@
#define EasyIsolate v8::Isolate* isolate = v8::Isolate::GetCurrent()
#define OnlyIsolate info.GetIsolate()
#define OnlyContext isolate->GetCurrentContext()
#define OnlyAddon static_cast<Addon*>(v8::Local<v8::External>::Cast(info.Data())->Value())
#define OnlyAddon static_cast<Addon*>(info.Data().As<v8::External>()->Value())
#define UseIsolate v8::Isolate* isolate = OnlyIsolate
#define UseContext v8::Local<v8::Context> ctx = OnlyContext
#define UseAddon Addon* addon = OnlyAddon
@ -31,7 +31,7 @@ inline v8::Local<v8::String> InternalizedFromLatin1(v8::Isolate* isolate, const
template <class T> using CopyablePersistent = v8::Persistent<T, v8::CopyablePersistentTraits<T>>;
#end
inline void SetFrozen(v8::Isolate* isolate, v8::Local<v8::Context> ctx, v8::Local<v8::Object> obj, CopyablePersistent<v8::String>& key, v8::Local<v8::Value> value) {
obj->DefineOwnProperty(ctx, CS::Get(isolate, key), value, static_cast<v8::PropertyAttribute>(v8::DontDelete | v8::ReadOnly)).FromJust();
obj->DefineOwnProperty(ctx, key.Get(isolate), value, static_cast<v8::PropertyAttribute>(v8::DontDelete | v8::ReadOnly)).FromJust();
}
void ThrowError(const char* message) { EasyIsolate; isolate->ThrowException(v8::Exception::Error(StringFromUtf8(isolate, message, -1))); }
@ -46,7 +46,7 @@ void ThrowRangeError(const char* message) { EasyIsolate; isolate->ThrowException
#define _REQUIRE_ARGUMENT(at, var, Type, message, ...) \
if (info.Length() <= (at()) || !info[at()]->Is##Type()) \
return ThrowTypeError("Expected "#at" argument to be "#message); \
var = v8::Local<v8::Type>::Cast(info[at()])__VA_ARGS__
var = (info[at()].As<v8::Type>())__VA_ARGS__
#define REQUIRE_ARGUMENT_INT32(at, var) \
_REQUIRE_ARGUMENT(at, var, Int32, a 32-bit signed integer, ->Value())
@ -87,22 +87,6 @@ void ThrowRangeError(const char* message) { EasyIsolate; isolate->ThrowException
#define ninth() 8
#define tenth() 9
// Returns a std:string of the concatenation of 3 well-formed C-strings.
std::string CONCAT(const char* a, const char* b, const char* c) {
std::string result(a);
result += b;
result += c;
return result;
}
// Returns a copy of a well-formed C-string.
const char* COPY(const char* source) {
size_t bytes = strlen(source) + 1;
char* dest = new char[bytes];
memcpy(dest, source, bytes);
return dest;
}
// Determines whether to skip the given character at the start of an SQL string.
inline bool IS_SKIPPED(char c) {
return c == ' ' || c == ';' || (c >= '\t' && c <= '\r');

View File

@ -44,7 +44,7 @@
#define DOES_NOT_MUTATE() REQUIRE_STATEMENT_NOT_LOCKED(stmt)
#define DOES_MUTATE() \
assert(!stmt->locked); \
REQUIRE_STATEMENT_NOT_LOCKED(stmt); \
REQUIRE_DATABASE_NO_ITERATORS_UNLESS_UNSAFE(db->GetState())
#define DOES_ADD_ITERATOR() \
DOES_NOT_MUTATE(); \
@ -53,9 +53,8 @@
#define REQUIRE_STATEMENT_RETURNS_DATA() \
if (!stmt->returns_data) \
return ThrowTypeError("This statement does not return data. Use run() instead")
#define REQUIRE_STATEMENT_DOESNT_RETURN_DATA() \
if (stmt->returns_data) \
return ThrowTypeError("This statement returns data. Use get(), all(), or iterate() instead")
#define ALLOW_ANY_STATEMENT() \
((void)0)
#define _FUNCTION_START(type) \

View File

@ -1,5 +1,6 @@
'use strict';
const { existsSync } = require('fs');
const fs = require('fs');
const path = require('path');
const Database = require('../.');
describe('new Database()', function () {
@ -31,10 +32,10 @@ describe('new Database()', function () {
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync('')).to.be.false;
expect(existsSync('null')).to.be.false;
expect(existsSync('undefined')).to.be.false;
expect(existsSync('[object Object]')).to.be.false;
expect(fs.existsSync('')).to.be.false;
expect(fs.existsSync('null')).to.be.false;
expect(fs.existsSync('undefined')).to.be.false;
expect(fs.existsSync('[object Object]')).to.be.false;
db.close();
}
});
@ -45,49 +46,49 @@ describe('new Database()', function () {
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(':memory:')).to.be.false;
expect(fs.existsSync(':memory:')).to.be.false;
});
it('should allow disk-bound databases to be created', function () {
expect(existsSync(util.next())).to.be.false;
const db = this.db = Database(util.current());
expect(fs.existsSync(util.next())).to.be.false;
const db = this.db = new Database(util.current());
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
});
it('should allow readonly database connections to be created', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
expect(() => (this.db = new Database(util.current(), { readonly: true }))).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
(new Database(util.current())).close();
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
const db = this.db = new Database(util.current(), { readonly: true });
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.true;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
});
it('should not allow the "readonly" option for in-memory databases', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
expect(() => (this.db = new Database(':memory:', { readonly: true }))).to.throw(TypeError);
expect(() => (this.db = new Database('', { readonly: true }))).to.throw(TypeError);
expect(existsSync(util.current())).to.be.false;
expect(fs.existsSync(util.current())).to.be.false;
});
it('should accept the "fileMustExist" option', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
expect(() => (this.db = new Database(util.current(), { fileMustExist: true }))).to.throw(Database.SqliteError).with.property('code', 'SQLITE_CANTOPEN');
(new Database(util.current())).close();
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
const db = this.db = new Database(util.current(), { fileMustExist: true });
expect(db.name).to.equal(util.current());
expect(db.memory).to.be.false;
expect(db.readonly).to.be.false;
expect(db.open).to.be.true;
expect(db.inTransaction).to.be.false;
expect(existsSync(util.current())).to.be.true;
expect(fs.existsSync(util.current())).to.be.true;
});
util.itUnix('should accept the "timeout" option', function () {
this.slow(4000);
@ -115,12 +116,38 @@ describe('new Database()', function () {
expect(() => (this.db = new Database(util.current(), { timeout: 75.01 }))).to.throw(TypeError);
expect(() => (this.db = new Database(util.current(), { timeout: 0x80000000 }))).to.throw(RangeError);
});
it('should accept the "nativeBinding" option', function () {
this.slow(500);
const oldBinding = require('bindings')({ bindings: 'better_sqlite3.node', path: true });
const newBinding = path.join(path.dirname(oldBinding), 'test.node');
expect(oldBinding).to.be.a('string');
fs.copyFileSync(oldBinding, newBinding);
const getBinding = db => db[Object.getOwnPropertySymbols(db)[0]].constructor;
let db1;
let db2;
let db3;
try {
db1 = new Database('');
db2 = new Database('', { nativeBinding: oldBinding });
db3 = new Database('', { nativeBinding: newBinding });
expect(db1.open).to.be.true;
expect(db2.open).to.be.true;
expect(db3.open).to.be.true;
expect(getBinding(db1)).to.equal(getBinding(db2));
expect(getBinding(db1)).to.not.equal(getBinding(db3));
expect(getBinding(db2)).to.not.equal(getBinding(db3));
} finally {
if (db1) db1.close();
if (db2) db2.close();
if (db3) db3.close();
}
});
it('should throw an Error if the directory does not exist', function () {
expect(existsSync(util.next())).to.be.false;
expect(fs.existsSync(util.next())).to.be.false;
const filepath = `temp/nonexistent/abcfoobar123/${util.current()}`;
expect(() => (this.db = new Database(filepath))).to.throw(TypeError);
expect(existsSync(filepath)).to.be.false;
expect(existsSync(util.current())).to.be.false;
expect(fs.existsSync(filepath)).to.be.false;
expect(fs.existsSync(util.current())).to.be.false;
});
it('should have a proper prototype chain', function () {
const db = this.db = new Database(util.next());
@ -131,4 +158,29 @@ describe('new Database()', function () {
expect(Database.prototype.close).to.equal(db.close);
expect(Database.prototype).to.equal(Object.getPrototypeOf(db));
});
it('should work properly when called as a function', function () {
const db = this.db = Database(util.next());
expect(db).to.be.an.instanceof(Database);
expect(db.constructor).to.equal(Database);
expect(Database.prototype.close).to.equal(db.close);
expect(Database.prototype).to.equal(Object.getPrototypeOf(db));
});
it('should work properly when subclassed', function () {
class MyDatabase extends Database {
foo() {
return 999;
}
}
const db = this.db = new MyDatabase(util.next());
expect(db).to.be.an.instanceof(Database);
expect(db).to.be.an.instanceof(MyDatabase);
expect(db.constructor).to.equal(MyDatabase);
expect(Database.prototype.close).to.equal(db.close);
expect(MyDatabase.prototype.close).to.equal(db.close);
expect(Database.prototype.foo).to.be.undefined;
expect(MyDatabase.prototype.foo).to.equal(db.foo);
expect(Database.prototype).to.equal(Object.getPrototypeOf(MyDatabase.prototype));
expect(MyDatabase.prototype).to.equal(Object.getPrototypeOf(db));
expect(db.foo()).to.equal(999);
});
});

View File

@ -30,6 +30,7 @@ describe('Database#close()', function () {
expect(() => this.db.pragma('cache_size')).to.throw(TypeError);
expect(() => this.db.function('foo', () => {})).to.throw(TypeError);
expect(() => this.db.aggregate('foo', { step: () => {} })).to.throw(TypeError);
expect(() => this.db.table('foo', () => {})).to.throw(TypeError);
});
it('should prevent any existing statements from running', function () {
this.db.prepare('CREATE TABLE people (name TEXT)').run();

View File

@ -9,11 +9,12 @@ describe('Database#prepare()', function () {
this.db.close();
});
function assertStmt(stmt, source, db, reader) {
function assertStmt(stmt, source, db, reader, readonly) {
expect(stmt.source).to.equal(source);
expect(stmt.constructor.name).to.equal('Statement');
expect(stmt.database).to.equal(db);
expect(stmt.reader).to.equal(reader);
expect(stmt.readonly).to.equal(readonly);
expect(() => new stmt.constructor(source)).to.throw(TypeError);
}
@ -40,13 +41,20 @@ describe('Database#prepare()', function () {
it('should create a prepared Statement object', function () {
const stmt1 = this.db.prepare('CREATE TABLE people (name TEXT) ');
const stmt2 = this.db.prepare('CREATE TABLE people (name TEXT); ');
assertStmt(stmt1, 'CREATE TABLE people (name TEXT) ', this.db, false);
assertStmt(stmt2, 'CREATE TABLE people (name TEXT); ', this.db, false);
assertStmt(stmt1, 'CREATE TABLE people (name TEXT) ', this.db, false, false);
assertStmt(stmt2, 'CREATE TABLE people (name TEXT); ', this.db, false, false);
expect(stmt1).to.not.equal(stmt2);
expect(stmt1).to.not.equal(this.db.prepare('CREATE TABLE people (name TEXT) '));
});
it('should create a prepared Statement object with just an expression', function () {
const stmt = this.db.prepare('SELECT 555');
assertStmt(stmt, 'SELECT 555', this.db, true);
assertStmt(stmt, 'SELECT 555', this.db, true, true);
});
it('should set the correct values for "reader" and "readonly"', function () {
this.db.exec('CREATE TABLE data (value)');
assertStmt(this.db.prepare('SELECT 555'), 'SELECT 555', this.db, true, true);
assertStmt(this.db.prepare('BEGIN'), 'BEGIN', this.db, false, true);
assertStmt(this.db.prepare('BEGIN EXCLUSIVE'), 'BEGIN EXCLUSIVE', this.db, false, false);
assertStmt(this.db.prepare('DELETE FROM data RETURNING *'), 'DELETE FROM data RETURNING *', this.db, true, false);
});
});

View File

@ -19,10 +19,6 @@ describe('Statement#run()', function () {
this.db.close();
});
it('should throw an exception when used on a statement that returns data', function () {
const stmt = this.db.prepare('SELECT 555');
expect(() => stmt.run()).to.throw(TypeError);
});
it('should work with CREATE TABLE', function () {
const { info } = this.db.init();
expect(info.changes).to.equal(0);
@ -34,6 +30,12 @@ describe('Statement#run()', function () {
expect(info.changes).to.equal(0);
expect(info.lastInsertRowid).to.equal(0);
});
it('should work with SELECT', function () {
const stmt = this.db.prepare('SELECT 555');
const info = stmt.run();
expect(info.changes).to.equal(0);
expect(info.lastInsertRowid).to.equal(0);
});
it('should work with INSERT INTO', function () {
let stmt = this.db.init().prepare("INSERT INTO entries VALUES ('foo', 25, 3.14, x'1133ddff')");
let info = stmt.run();

View File

@ -32,6 +32,14 @@ describe('Statement#get()', function () {
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 5 ORDER BY rowid");
expect(stmt.get()).to.deep.equal({ a: 'foo', b: 6, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null });
});
it('should work with RETURNING clause', function () {
let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *");
expect(stmt.reader).to.be.true;
expect(stmt.get()).to.deep.equal({ a: 'bar', b: 888, c: null, d: null, e: null });
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 900 ORDER BY rowid");
expect(stmt.get()).to.deep.equal({ a: 'baz', b: 999, c: null, d: null, e: null });
});
it('should obey the current pluck and expand settings', function () {
const stmt = this.db.prepare("SELECT *, 2 + 3.5 AS c FROM entries ORDER BY rowid");
const expanded = { entries: { a: 'foo', b: 1, c: 3.14, d: Buffer.alloc(4).fill(0xdd), e: null }, $: { c: 5.5 } };

View File

@ -43,6 +43,20 @@ describe('Statement#all()', function () {
expect(index).to.equal(rows.length);
}
});
it('should work with RETURNING clause', function () {
let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *");
expect(stmt.reader).to.be.true;
expect(stmt.all()).to.deep.equal([
{ a: 'bar', b: 888, c: null, d: null, e: null },
{ a: 'baz', b: 999, c: null, d: null, e: null },
]);
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 800 ORDER BY rowid");
expect(stmt.all()).to.deep.equal([
{ a: 'bar', b: 888, c: null, d: null, e: null },
{ a: 'baz', b: 999, c: null, d: null, e: null },
]);
});
it('should obey the current pluck and expand settings', function () {
const stmt = this.db.prepare("SELECT *, 2 + 3.5 AS c FROM entries ORDER BY rowid");
const expanded = new Array(10).fill().map((_, i) => ({

View File

@ -32,6 +32,7 @@ describe('Statement#iterate()', function () {
let count = 0;
let stmt = this.db.prepare("SELECT * FROM entries ORDER BY rowid");
expect(stmt.reader).to.be.true;
expect(stmt.busy).to.be.false;
const iterator = stmt.iterate();
expect(iterator).to.not.be.null;
@ -41,22 +42,43 @@ describe('Statement#iterate()', function () {
expect(iterator.throw).to.not.be.a('function');
expect(iterator[Symbol.iterator]).to.be.a('function');
expect(iterator[Symbol.iterator]()).to.equal(iterator);
expect(stmt.busy).to.be.true;
for (const data of iterator) {
row.b = ++count;
expect(data).to.deep.equal(row);
expect(stmt.busy).to.be.true;
}
expect(count).to.equal(10);
expect(stmt.busy).to.be.false;
count = 0;
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 5 ORDER BY rowid");
expect(stmt.busy).to.be.false;
const iterator2 = stmt.iterate();
expect(iterator).to.not.equal(iterator2);
expect(stmt.busy).to.be.true;
for (const data of iterator2) {
row.b = ++count + 5;
expect(data).to.deep.equal(row);
expect(stmt.busy).to.be.true;
}
expect(count).to.equal(5);
expect(stmt.busy).to.be.false;
});
it('should work with RETURNING clause', function () {
let stmt = this.db.prepare("INSERT INTO entries (a, b) VALUES ('bar', 888), ('baz', 999) RETURNING *");
expect(stmt.reader).to.be.true;
expect([...stmt.iterate()]).to.deep.equal([
{ a: 'bar', b: 888, c: null, d: null, e: null },
{ a: 'baz', b: 999, c: null, d: null, e: null },
]);
stmt = this.db.prepare("SELECT * FROM entries WHERE b > 800 ORDER BY rowid");
expect([...stmt.iterate()]).to.deep.equal([
{ a: 'bar', b: 888, c: null, d: null, e: null },
{ a: 'baz', b: 999, c: null, d: null, e: null },
]);
});
it('should obey the current pluck and expand settings', function () {
const shouldHave = (desiredData) => {

View File

@ -92,4 +92,16 @@ describe('Statement#bind()', function () {
expect(() => stmt1.bind(arr)).to.throw(err);
expect(() => stmt2.bind(obj)).to.throw(err);
});
it('should properly bind empty buffers', function () {
this.db.prepare('INSERT INTO entries (c) VALUES (?)').bind(Buffer.alloc(0)).run();
const result = this.db.prepare('SELECT c FROM entries').pluck().get();
expect(result).to.be.an.instanceof(Buffer);
expect(result.length).to.equal(0);
});
it('should properly bind empty strings', function () {
this.db.prepare('INSERT INTO entries (a) VALUES (?)').bind('').run();
const result = this.db.prepare('SELECT a FROM entries').pluck().get();
expect(result).to.be.a('string');
expect(result.length).to.equal(0);
});
});

View File

@ -27,6 +27,7 @@ describe('Database#function()', function () {
it('should throw an exception if boolean options are provided as non-booleans', function () {
expect(() => this.db.function('a', { varargs: undefined }, () => {})).to.throw(TypeError);
expect(() => this.db.function('b', { deterministic: undefined }, () => {})).to.throw(TypeError);
expect(() => this.db.function('b', { directOnly: undefined }, () => {})).to.throw(TypeError);
expect(() => this.db.function('c', { safeIntegers: undefined }, () => {})).to.throw(TypeError);
});
it('should throw an exception if the provided name is empty', function () {

View File

@ -40,6 +40,7 @@ describe('Database#aggregate()', function () {
it('should throw an exception if boolean options are provided as non-booleans', function () {
expect(() => this.db.aggregate('a', { step: () => {}, varargs: undefined })).to.throw(TypeError);
expect(() => this.db.aggregate('b', { step: () => {}, deterministic: undefined })).to.throw(TypeError);
expect(() => this.db.aggregate('b', { step: () => {}, directOnly: undefined })).to.throw(TypeError);
expect(() => this.db.aggregate('c', { step: () => {}, safeIntegers: undefined })).to.throw(TypeError);
});
it('should throw an exception if function options are provided as non-fns', function () {

671
test/34.database.table.js Normal file
View File

@ -0,0 +1,671 @@
'use strict';
const Database = require('../.');
describe('Database#table()', function () {
beforeEach(function () {
this.db = new Database(util.next());
});
afterEach(function () {
this.db.close();
});
it('should throw an exception if the correct arguments are not provided', function () {
expect(() => this.db.table()).to.throw(TypeError);
expect(() => this.db.table(null)).to.throw(TypeError);
expect(() => this.db.table('a')).to.throw(TypeError);
expect(() => this.db.table({})).to.throw(TypeError);
expect(() => this.db.table({ rows: function*(){}, columns: ['x'] })).to.throw(TypeError);
expect(() => this.db.table({ name: 'b', rows: function*(){}, columns: ['x'] })).to.throw(TypeError);
expect(() => this.db.table(() => {})).to.throw(TypeError);
expect(() => this.db.table(function* c() {})).to.throw(TypeError);
expect(() => this.db.table({}, function d() {})).to.throw(TypeError);
expect(() => this.db.table({ name: 'e', rows: function* e() {}, columns: ['x'] }, function e() {})).to.throw(TypeError);
expect(() => this.db.table('f')).to.throw(TypeError);
expect(() => this.db.table('g', null)).to.throw(TypeError);
expect(() => this.db.table('h', {})).to.throw(TypeError);
expect(() => this.db.table('i', Object.create(Function.prototype))).to.throw(TypeError);
expect(() => this.db.table('j', { columns: ['x'] }, function j() {})).to.throw(TypeError);
expect(() => this.db.table('k', { name: 'k', columns: ['x'] }, function* k() {})).to.throw(TypeError);
expect(() => this.db.table('l', { name: 'l', rows: function* l() {} })).to.throw(TypeError);
expect(() => this.db.table(new String('m'), { columns: ['x'], rows: function* m() {} })).to.throw(TypeError);
expect(() => this.db.table(new String('n'), () => {})).to.throw(TypeError);
});
it('should throw an exception if boolean options are provided as non-booleans', function () {
expect(() => this.db.table('a', { columns: ['x'], rows: function*(){}, directOnly: undefined })).to.throw(TypeError);
expect(() => this.db.table('b', { columns: ['x'], rows: function*(){}, safeIntegers: undefined })).to.throw(TypeError);
});
it('should throw an exception if the "columns" option is invalid', function () {
expect(() => this.db.table('a', { rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('b', { columns: undefined, rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('c', { columns: 'x', rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('d', { columns: { length: 1, 0: 'x', [Symbol.iterator]: () => ['x'].values() }, rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('e', { columns: ['x',, 'y'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('f', { columns: ['x', new String('y')], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('g', { columns: ['x', 'x'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('h', { columns: [], rows: function*(){} })).to.throw(RangeError);
});
it('should throw an exception if the "parameters" option is invalid', function () {
expect(() => this.db.table('a', { parameters: undefined, columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('b', { parameters: 'x', columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('c', { parameters: { length: 1, 0: 'x', [Symbol.iterator]: () => ['x'].values() }, columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('d', { parameters: ['x',, 'y'], columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('e', { parameters: ['x', new String('y')], columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('f', { parameters: ['x', 'x'], columns: ['foo'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('g', { parameters: ['x'], columns: ['x'], rows: function*(){} })).to.throw(TypeError);
expect(() => this.db.table('h', { parameters: [...Array(33)].map((_, i) => `p${i}`), columns: ['foo'], rows: function*(){} })).to.throw(RangeError);
});
it('should throw an exception if the "rows" option is invalid', function () {
expect(() => this.db.table('a', { columns: ['x'] })).to.throw(TypeError);
expect(() => this.db.table('b', { columns: ['x'], rows: undefined })).to.throw(TypeError);
expect(() => this.db.table('c', { columns: ['x'], rows: {} })).to.throw(TypeError);
expect(() => this.db.table('d', { columns: ['x'], rows: () => {} })).to.throw(TypeError);
expect(() => this.db.table('e', { columns: ['x'], rows: function () {} })).to.throw(TypeError);
expect(() => this.db.table('f', { columns: ['x'], rows: Object.create(Function.prototype) })).to.throw(TypeError);
expect(() => this.db.table('g', { columns: ['x'], rows: Object.create(Object.getPrototypeOf(function*(){})) })).to.throw(TypeError);
expect(() => this.db.table('h', { columns: ['x'], rows: Object.setPrototypeOf(() => {}, Object.create(Object.getPrototypeOf(function*(){}))) })).to.throw(TypeError);
});
it('should throw an exception if the provided name is empty', function () {
expect(() => this.db.table('', { columns: ['x'], rows: function* () {} })).to.throw(TypeError);
expect(() => this.db.table('', { name: 'a', columns: ['x'], rows: function* () {} })).to.throw(TypeError);
expect(() => this.db.table('', { name: 'b', columns: ['x'], rows: function* b() {} })).to.throw(TypeError);
expect(() => this.db.table('', function c() {})).to.throw(TypeError);
});
it('should throw an exception if generator.length is invalid', function () {
const length = x => Object.defineProperty(function*(){}, 'length', { value: x });
expect(() => this.db.table('a', { columns: ['x'], rows: length(undefined) })).to.throw(TypeError);
expect(() => this.db.table('b', { columns: ['x'], rows: length(null) })).to.throw(TypeError);
expect(() => this.db.table('c', { columns: ['x'], rows: length('1') })).to.throw(TypeError);
expect(() => this.db.table('d', { columns: ['x'], rows: length(NaN) })).to.throw(TypeError);
expect(() => this.db.table('e', { columns: ['x'], rows: length(Infinity) })).to.throw(TypeError);
expect(() => this.db.table('f', { columns: ['x'], rows: length(1.000000001) })).to.throw(TypeError);
expect(() => this.db.table('g', { columns: ['x'], rows: length(-0.000000001) })).to.throw(TypeError);
expect(() => this.db.table('h', { columns: ['x'], rows: length(-1) })).to.throw(TypeError);
expect(() => this.db.table('i', { columns: ['x'], rows: length(32.000000001) })).to.throw(TypeError);
expect(() => this.db.table('j', { columns: ['x'], rows: length(33) })).to.throw(RangeError);
});
it('should register a virtual table and return the database object', function () {
const length = x => Object.defineProperty(function*(){}, 'length', { value: x });
expect(this.db.table('a', { columns: ['x'], rows: function* () {} })).to.equal(this.db);
expect(this.db.table('b', { columns: ['x'], rows: length(1) })).to.equal(this.db);
expect(this.db.table('c', { columns: ['x'], rows: length(32) })).to.equal(this.db);
});
it('should enable the registered virtual table to be queried from SQL', function () {
const rows = [
{ a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') },
{ a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') },
];
this.db.table('vtab', {
columns: ['a', 'b', 'c', 'd', 'e'],
*rows() {
for (const obj of rows) {
yield Object.values(obj);
}
},
});
expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows);
expect(this.db.prepare('SELECT * FROM vtab WHERE b < 500').all()).to.deep.equal(rows.slice(0, 1));
expect(this.db.prepare('SELECT * FROM vtab ORDER BY d DESC').all()).to.deep.equal(rows.slice().reverse());
});
it('should infer parameters for the virtual table', function () {
this.db.table('vtab', {
columns: ['a', 'b'],
*rows(x, y) {
yield [x, y];
yield [x * 2, y * 3];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?, ?)').all(2, 3))
.to.deep.equal([{ a: 2, b: 3 }, { a: 4, b: 9 }]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ?').all(2, 3))
.to.deep.equal([{ a: 2, b: 3 }, { a: 4, b: 9 }]);
expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?)'))
.to.throw(Database.SqliteError);
expect(() => this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ? AND "$3" = ?'))
.to.throw(Database.SqliteError);
});
it('should accept explicit parameters for the virtual table', function () {
this.db.table('vtab', {
columns: ['a', 'b'],
parameters: ['x', 'y', 'z'],
*rows(p1, p2, p3, p4) {
yield [arguments[0], arguments[1] + arguments[2]];
yield [arguments[0] * 2, (arguments[1] + arguments[2]) * 3];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').all(2, 3, 4))
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').all(2, 3, 4))
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?)'))
.to.throw(Database.SqliteError);
expect(() => this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ? AND "$3" = ?'))
.to.throw(Database.SqliteError);
});
it('should accept a large number of parameters for the virtual table', function () {
const args = ['foo', 'bar', 1, -2, Buffer.from('hello'), 5, -10, 'baz', 99.9, -0.5];
this.db.table('vtab', {
columns: ['x'],
*rows(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) {
yield [p10];
yield [p9];
yield [p8];
yield [p7];
yield [p6];
yield [p5];
yield [p4];
yield [p3];
yield [p2];
yield [p1];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').pluck().all(args))
.to.deep.equal(args.slice().reverse());
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?)').pluck().all(args.slice(0, -1)))
.to.deep.equal([null].concat(args.slice(0, -1).reverse()));
expect(() => this.db.prepare('SELECT * FROM vtab(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'))
.to.throw(Database.SqliteError);
});
it('should correctly handle arguments even when used out of order', function () {
const calls = [];
this.db.table('vtab', {
columns: ['x', 'y'],
*rows(x, y) {
calls.push([...arguments]);
yield { x, y };
},
});
expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = ?').get(10, 5))
.to.deep.equal({ x: 10, y: 5 });
expect(calls.splice(0)).to.deep.equal([[10, 5]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$1" = ?').get(5, 10))
.to.deep.equal({ x: 10, y: 5 });
expect(calls.splice(0)).to.deep.equal([[10, 5]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(5, 5, 10))
.to.deep.equal({ x: 10, y: 5 });
expect(calls.splice(0)).to.deep.equal([[10, 5]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(5, 9, 10))
.to.be.undefined;
expect(calls.splice(0)).to.deep.equal([]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = ? AND "$1" = ?').get(9, 5, 10))
.to.be.undefined;
expect(calls.splice(0)).to.deep.equal([]);
});
it('should correctly handle arguments that are constrained to other arguments', function () {
const calls = [];
this.db.table('vtab', {
columns: ['x', 'y'],
*rows(x, y) {
calls.push([...arguments]);
yield { x, y };
},
});
expect(this.db.prepare('SELECT * FROM vtab WHERE "$1" = ? AND "$2" = "$1"').get(10))
.to.deep.equal({ x: 10, y: 10 });
expect(calls.splice(0)).to.deep.equal([[10, 10]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = "$1" AND "$1" = ?').get(10))
.to.deep.equal({ x: 10, y: 10 });
expect(calls.splice(0)).to.deep.equal([[10, 10]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = "$1" AND "$1" = ?').get(10, 10))
.to.deep.equal({ x: 10, y: 10 });
expect(calls.splice(0)).to.deep.equal([[10, 10]]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = ? AND "$2" = "$1" AND "$1" = ?').get(5, 10))
.to.be.undefined;
expect(calls.splice(0)).to.deep.equal([]);
expect(this.db.prepare('SELECT * FROM vtab WHERE "$2" = "$1" AND "$2" = ? AND "$1" = ?').get(5, 10))
.to.be.undefined;
expect(calls.splice(0)).to.deep.equal([]);
});
it('should throw an exception if the database is busy', function () {
let ranOnce = false;
for (const x of this.db.prepare('SELECT 2').pluck().iterate()) {
expect(x).to.equal(2);
ranOnce = true;
expect(() => this.db.table('a', { columns: ['x'], rows: function* () {} })).to.throw(TypeError);
}
expect(ranOnce).to.be.true;
this.db.table('b', { columns: ['x'], rows: function* () {} });
});
it('should cause the database to become busy when querying the virtual table', function () {
let checkCount = 0;
const expectBusy = function* () {
for (let i = 0; i < 3; ++i) {
expect(() => this.db.exec('SELECT * FROM a')).to.throw(TypeError);
expect(() => this.db.prepare('SELECT 555')).to.throw(TypeError);
expect(() => this.db.pragma('cache_size')).to.throw(TypeError);
expect(() => this.db.function('x', () => {})).to.throw(TypeError);
expect(() => this.db.table('y', { columns: ['x'], rows: function* () {} })).to.throw(TypeError);
checkCount += 1;
yield [i];
}
};
this.db.table('a', { columns: ['x'], rows: function* () {} });
this.db.table('b', { columns: ['x'], rows: expectBusy });
expect(this.db.prepare('SELECT * FROM b').pluck().all()).to.deep.equal([0, 1, 2]);
expect(checkCount).to.equal(3);
this.db.exec('SELECT * FROM a');
this.db.prepare('SELECT 555');
this.db.pragma('cache_size');
this.db.function('xx', () => {});
this.db.table('yy', { columns: ['x'], rows: function* () {} })
});
it('should cause the virtual table to throw when yielding an invalid value', function () {
this.db.table('a', {
columns: ['x'],
*rows() { yield [42]; }
});
this.db.table('b', {
columns: ['x'],
*rows() { yield 42; }
});
this.db.table('c', {
columns: ['x'],
*rows() { yield; }
});
this.db.table('d', {
columns: ['x'],
*rows() { yield null; }
});
expect(this.db.prepare('SELECT * FROM a').get()).to.deep.equal({ x: 42 });
expect(() => this.db.prepare('SELECT * FROM b').get()).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM c').get()).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM d').get()).to.throw(TypeError);
});
it('should allow arrays to be yielded as rows', function () {
const rows = [
{ a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') },
{ a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') },
];
this.db.table('vtab', {
columns: ['a', 'b', 'c', 'd', 'e'],
*rows() {
for (const obj of rows) {
yield Object.values(obj);
}
},
});
expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows);
});
it('should allow objects to be yielded as rows', function () {
const rows = [
{ a: null, b: 123, c: 456.789, d: 'foo', e: Buffer.from('bar') },
{ a: null, b: 987, c: 654.321, d: 'oof', e: Buffer.from('rab') },
{ e: Buffer.from('hello'), d: 'world', c: 0.1, b: 10, a: null },
{ d: 'old friend', c: -0.1, e: Buffer.from('goodbye'), a: null, b: -10 },
];
this.db.table('vtab', {
columns: ['a', 'b', 'c', 'd', 'e'],
*rows() {
for (const obj of rows) {
yield obj;
}
},
});
expect(this.db.prepare('SELECT * FROM vtab').all()).to.deep.equal(rows);
});
it('should throw an exception if an invalid array is yielded', function () {
const tests = [
[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5, 6],
[1, 2, 3, 4],
[],
[1, 2, 3, 4, new Number(5)],
[1, 2, 3, 4, [5]],
[1, 2, 3, 4, new Date()],
];
this.db.table('vtab', {
columns: ['a', 'b', 'c', 'd', 'e'],
*rows(n) {
yield tests[n];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?)').raw().all(0)).to.deep.equal([tests[0]]);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(1)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(2)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(3)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(4)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(5)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(6)).to.throw(TypeError);
});
it('should throw an exception if an invalid object is yielded', function () {
const tests = [
{ a: 1, b: 2, c: 3, d: 4, e: 5 },
{ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 },
{ a: 1, b: 2, c: 3, d: 4 },
{},
{ a: 1, b: 2, c: 3, d: 4, e: new Number(5) },
{ a: 1, b: 2, c: 3, d: 4, e: [5] },
{ a: 1, b: 2, c: 3, d: 4, e: new Date() },
{ a: 1, b: 2, c: 3, d: 4, f: 5 },
];
this.db.table('vtab', {
columns: ['a', 'b', 'c', 'd', 'e'],
*rows(n) {
yield tests[n];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?)').all(0)).to.deep.equal([tests[0]]);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(1)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(2)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(3)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(4)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(5)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(6)).to.throw(TypeError);
expect(() => this.db.prepare('SELECT * FROM vtab(?)').all(7)).to.throw(TypeError);
});
it('should automatically assign rowids without affecting yielded objects', function () {
let rows = [{ x: 5 }, { x: 10 }];
this.db.table('a', {
columns: ['x'],
*rows() { yield* rows; },
});
expect(this.db.prepare('SELECT rowid, * FROM a').all())
.to.deep.equal([{ rowid: 1, x: 5 }, { rowid: 2, x: 10 }]);
expect(rows).to.deep.equal([{ x: 5 }, { x: 10 }]);
rows = [{ rowid: 5 }, { rowid: 10 }];
this.db.table('b', {
columns: ['rowid'],
*rows() { yield* rows; },
});
expect(this.db.prepare('SELECT oid AS oid, * FROM b').all())
.to.deep.equal([{ oid: 1, rowid: 5 }, { oid: 2, rowid: 10 }]);
expect(rows).to.deep.equal([{ rowid: 5 }, { rowid: 10 }]);
});
it('should be driven by stmt.iterate() one row at a time', function () {
let state = 0;
this.db.table('vtab', {
columns: ['x'],
*rows() {
state += 1;
yield ['foo'];
state += 1;
yield ['bar'];
state += 1;
yield ['baz'];
state += 1;
yield ['qux'];
state += 1;
},
});
const values = [];
for (const value of this.db.prepare('SELECT * FROM vtab').pluck().iterate()) {
values.push(value);
if (value === 'baz') break;
}
expect(values).to.deep.equal(['foo', 'bar', 'baz']);
expect(state).to.equal(3);
});
it('should throw an exception if preparing a statement that uses an unsupported operator on a parameter', function () {
this.db.table('vtab', {
columns: ['a', 'b'],
parameters: ['x', 'y', 'z'],
*rows(x, y, z) {
yield [x, y + z];
yield [x * 2, (y + z) * 3];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').all(2, 3, 4))
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').all(2, 3, 4))
.to.deep.equal([{ a: 2, b: 7 }, { a: 4, b: 21 }]);
expect(() => this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z > ?'))
.to.throw(Database.SqliteError);
expect(() => this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y < ? AND z = ?'))
.to.throw(Database.SqliteError);
expect(() => this.db.prepare('SELECT * FROM vtab WHERE x IS ? AND y = ? AND z = ?'))
.to.throw(Database.SqliteError);
});
it('should properly escape column and parameter names', function () {
this.db.table('vtab', {
columns: ['foo);'],
parameters: ['x"); SELECT "y', 'y'],
*rows(x, y) {
yield [x];
yield [y];
yield [x + y];
},
});
expect(this.db.prepare('SELECT "foo);" FROM vtab WHERE "x""); SELECT ""y" = ? AND y = ?').all(5, 10))
.to.deep.equal([{ 'foo);': 5 }, { 'foo);': 10 }, { 'foo);': 15 }]);
});
it('should not allow CREATE VIRTUAL TABLE statements by default', function () {
this.db.table('mod', {
columns: ['x'],
*rows() {},
});
expect(() => this.db.exec('CREATE VIRTUAL TABLE a USING mod')).to.throw(Database.SqliteError);
expect(() => this.db.exec('CREATE VIRTUAL TABLE b USING mod()')).to.throw(Database.SqliteError);
expect(() => this.db.exec('CREATE VIRTUAL TABLE c USING mod(foo)')).to.throw(Database.SqliteError);
});
it('should support CREATE VIRTUAL TABLE statements by accepting a factory function', function () {
let table = '';
this.db.table('mod', function (...args) {
expect(this).to.deep.equal({ module: 'mod', database: 'main', table });
return {
columns: ['x'],
*rows() { yield* args.map(x => [x]); },
};
});
expect(() => this.db.prepare('SELECT * FROM mod')).to.throw(Database.SqliteError);
table = 'foo';
this.db.exec(`CREATE VIRTUAL TABLE ${table} USING mod(hello world, how are you?)`);
table = 'bar';
this.db.exec(`CREATE VIRTUAL TABLE ${table} USING mod(1, 2, 3)`);
expect(this.db.prepare('SELECT x FROM foo').pluck().all()).to.deep.equal(['hello world', 'how are you?']);
expect(this.db.prepare('SELECT x FROM bar').pluck().all()).to.deep.equal(['1', '2', '3']);
expect(() => this.db.prepare('SELECT * FROM mod')).to.throw(Database.SqliteError);
});
it('should correctly handle omitted arguments in any order', function () {
this.db.table('vtab', {
columns: ['value'],
parameters: ['x', 'y', 'z'],
*rows(x = 100, y = 10, z = 1) {
expect(arguments.length).to.equal(3);
yield [x + y + z];
},
});
expect(this.db.prepare('SELECT * FROM vtab(?, ?, ?)').pluck().get(2.2, 3.3, 4.4)).to.equal(9.9);
expect(this.db.prepare('SELECT * FROM vtab(?, ?)').pluck().get(2.2, 3.3)).to.equal(6.5);
expect(this.db.prepare('SELECT * FROM vtab(?)').pluck().get(2.2)).to.equal(13.2);
expect(this.db.prepare('SELECT * FROM vtab').pluck().get()).to.equal(111);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ? AND z = ?').pluck().get(2.2, 3.3, 4.4)).to.equal(9.9);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND y = ?').pluck().get(2.2, 3.3)).to.equal(6.5);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ? AND z = ?').pluck().get(2.2, 3.3)).to.equal(15.5);
expect(this.db.prepare('SELECT * FROM vtab WHERE y = ? AND z = ?').pluck().get(2.2, 3.3)).to.equal(105.5);
expect(this.db.prepare('SELECT * FROM vtab WHERE x = ?').pluck().get(2.2)).to.equal(13.2);
expect(this.db.prepare('SELECT * FROM vtab WHERE y = ?').pluck().get(2.2)).to.equal(103.2);
expect(this.db.prepare('SELECT * FROM vtab WHERE z = ?').pluck().get(2.2)).to.equal(112.2);
});
it('should not call the generator function if any arguments are NULL', function () {
let calls = 0;
this.db.table('vtab', {
columns: ['val'],
parameters: ['x', 'y', 'z'],
*rows(x = 0, y = 0, z = 0) {
calls += 1;
yield [x + y + z];
},
});
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, 10, 100)).to.deep.equal([111]);
expect(this.db.prepare('SELECT val FROM vtab(?, ?)').pluck().all(1, 10)).to.deep.equal([11]);
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, 10, null)).to.deep.equal([]);
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(1, null, 100)).to.deep.equal([]);
expect(this.db.prepare('SELECT val FROM vtab(?, ?, ?)').pluck().all(null, 10, 100)).to.deep.equal([]);
expect(this.db.prepare('SELECT val FROM vtab(?, ?)').pluck().all(1, null)).to.deep.equal([]);
expect(calls).to.equal(2);
});
it('should close a statement iterator that caused a virtual table to throw', function () {
this.db.prepare('CREATE TABLE iterable (x INTEGER)').run();
this.db.prepare('INSERT INTO iterable WITH RECURSIVE temp(x) AS (SELECT 1 UNION ALL SELECT x * 2 FROM temp LIMIT 10) SELECT * FROM temp').run();
let i = 0;
const err = new Error('foo');
this.db.table('vtab', {
columns: ['value'],
parameters: ['x'],
*rows(x) {
if (++i >= 5) throw err;
yield [x];
},
});
const iterator = this.db.prepare('SELECT value FROM vtab JOIN iterable USING (x)').pluck().iterate();
let total = 0;
expect(() => {
for (const value of iterator) {
total += value;
expect(() => this.db.exec('SELECT value FROM vtab JOIN iterable USING (x) LIMIT 4')).to.throw(TypeError);
}
}).to.throw(err);
expect(total).to.equal(1 + 2 + 4 + 8);
expect(iterator.next()).to.deep.equal({ value: undefined, done: true });
expect(total).to.equal(1 + 2 + 4 + 8);
i = 0;
this.db.exec('SELECT value FROM vtab JOIN iterable USING (x) LIMIT 4');
expect(i).to.equal(4);
});
it('should not be able to affect bound buffers mid-query', function () {
const input = Buffer.alloc(1024 * 8).fill(0xbb);
let called = false;
this.db.table('vtab', {
columns: ['x'],
*rows(arg) {
called = true;
input[0] = 2;
arg[0] = 2;
yield [123];
},
});
const [output, arg, num] = this.db.prepare('SELECT :input, "$1", x FROM vtab(:input)').raw().get({ input });
expect(called).to.be.true;
expect(output.equals(Buffer.alloc(1024 * 8).fill(0xbb))).to.be.true;
expect(arg.equals(Buffer.alloc(1024 * 8).fill(0xbb))).to.be.true;
expect(num).to.equal(123);
});
describe('should propagate exceptions', function () {
const exceptions = [new TypeError('foobar'), new Error('baz'), { yup: 'ok' }, 'foobarbazqux', '', null, 123.4];
const expectError = (exception, fn) => {
try { fn(); } catch (ex) {
expect(ex).to.equal(exception);
return;
}
throw new TypeError('Expected table to throw an exception');
};
specify('thrown in the factory function', function () {
exceptions.forEach((exception, index) => {
const calls = [];
this.db.table(`mod${index}`, () => {
calls.push('a');
throw exception;
calls.push('b');
return {
columns: ['x'],
*rows() {
calls.push('c');
yield [42];
calls.push('d');
},
};
});
expect(calls.splice(0)).to.deep.equal([]);
expectError(exception, () => this.db.exec(`CREATE VIRTUAL TABLE vtab${index} USING mod${index}()`));
expect(calls.splice(0)).to.deep.equal(['a']);
expect(() => this.db.prepare(`SELECT * FROM vtab${index}`)).to.throw(Database.SqliteError);
expect(calls.splice(0)).to.deep.equal([]);
});
});
specify('thrown in the rows() function', function () {
exceptions.forEach((exception, index) => {
const calls = [];
this.db.table(`mod${index}`, () => {
calls.push('a');
return {
columns: ['x'],
*rows() {
calls.push('b');
yield [42];
calls.push('c');
throw exception;
calls.push('d');
},
};
});
expect(calls.splice(0)).to.deep.equal([]);
this.db.exec(`CREATE VIRTUAL TABLE vtab${index} USING mod${index}()`);
expect(calls.splice(0)).to.deep.equal(['a']);
expect(this.db.prepare(`SELECT * FROM vtab${index}`).pluck().get()).to.equal(42);
expect(calls.splice(0)).to.deep.equal(['b']);
expectError(exception, () => this.db.prepare(`SELECT * FROM vtab${index}`).pluck().all());
expect(calls.splice(0)).to.deep.equal(['b', 'c']);
});
});
specify('thrown due to yielding an invalid value', function () {
const calls = [];
this.db.table('mod', () => {
calls.push('a');
return {
columns: ['x'],
*rows() {
calls.push('b');
yield [42];
calls.push('c');
yield [new Number(42)];
calls.push('d');
},
};
});
expect(calls.splice(0)).to.deep.equal([]);
this.db.exec('CREATE VIRTUAL TABLE vtab USING mod()');
expect(calls.splice(0)).to.deep.equal(['a']);
expect(this.db.prepare('SELECT * FROM vtab').pluck().get()).to.equal(42);
expect(calls.splice(0)).to.deep.equal(['b']);
expect(() => this.db.prepare('SELECT * FROM vtab').pluck().all()).to.throw(TypeError);
expect(calls.splice(0)).to.deep.equal(['b', 'c']);
});
});
describe('should not affect external environment', function () {
specify('busy state', function () {
this.db.table('vtab', {
columns: ['x'],
*rows(arg) {
expect(() => this.db.exec('SELECT 555')).to.throw(TypeError);
yield [arg * 2];
},
});
let ranOnce = false;
for (const x of this.db.prepare('SELECT * FROM vtab(555)').pluck().iterate()) {
ranOnce = true;
expect(x).to.equal(1110);
expect(() => this.db.exec('SELECT 555')).to.throw(TypeError);
}
expect(ranOnce).to.be.true;
this.db.exec('SELECT 555');
});
specify('was_js_error state', function () {
this.db.prepare('CREATE TABLE data (value INTEGER)').run();
const stmt = this.db.prepare('SELECT value FROM data');
this.db.prepare('DROP TABLE data').run();
const err = new Error('foo');
this.db.table('vtab', {
columns: ['x'],
*rows() { throw err; },
});
expect(() => this.db.prepare('SELECT * FROM vtab').get()).to.throw(err);
try { stmt.get(); } catch (ex) {
expect(ex).to.be.an.instanceof(Error);
expect(ex).to.not.equal(err);
expect(ex.message).to.not.equal(err.message);
expect(ex).to.be.an.instanceof(Database.SqliteError);
return;
}
throw new TypeError('Expected the statement to throw an exception');
});
});
});

View File

@ -0,0 +1,81 @@
'use strict';
const Database = require('../.');
describe('Database#serialize()', function () {
beforeEach(function () {
this.db = new Database(util.next());
this.db.prepare("CREATE TABLE entries (a TEXT, b INTEGER, c REAL, d BLOB, e TEXT)").run();
this.seed = () => {
this.db.prepare("INSERT INTO entries WITH RECURSIVE temp(a, b, c, d, e) AS (SELECT 'foo', 1, 3.14, x'dddddddd', NULL UNION ALL SELECT a, b + 1, c, d, e FROM temp LIMIT 1000) SELECT * FROM temp").run();
};
});
afterEach(function () {
this.db.close();
});
it('should serialize the database and return a buffer', async function () {
let buffer = this.db.serialize();
expect(buffer).to.be.an.instanceof(Buffer);
expect(buffer.length).to.be.above(1000);
const lengthBefore = buffer.length;
this.seed();
buffer = this.db.serialize();
expect(buffer).to.be.an.instanceof(Buffer);
expect(buffer.length).to.be.above(lengthBefore);
});
it('should return a buffer that can be used by the Database constructor', async function () {
this.seed();
const buffer = this.db.serialize();
expect(buffer).to.be.an.instanceof(Buffer);
expect(buffer.length).to.be.above(1000);
this.db.prepare('delete from entries').run();
this.db.close();
this.db = new Database(buffer);
const bufferCopy = this.db.serialize();
expect(buffer.length).to.equal(bufferCopy.length);
expect(buffer).to.deep.equal(bufferCopy);
this.db.prepare('insert into entries (rowid, a, b) values (?, ?, ?)').run(0, 'bar', -999);
expect(this.db.prepare('select a, b from entries order by rowid limit 2').all())
.to.deep.equal([{ a: 'bar', b: -999 }, { a: 'foo', b: 1 }]);
});
it('should accept the "attached" option', async function () {
const smallBuffer = this.db.serialize();
this.seed();
const bigBuffer = this.db.serialize();
this.db.close();
this.db = new Database();
this.db.prepare('attach ? as other').run(util.current());
const smallBuffer2 = this.db.serialize();
const bigBuffer2 = this.db.serialize({ attached: 'other' });
expect(bigBuffer.length === bigBuffer2.length);
expect(bigBuffer).to.deep.equal(bigBuffer2);
expect(smallBuffer.length < bigBuffer.length);
expect(smallBuffer2.length < bigBuffer.length);
expect(smallBuffer).to.not.deep.equal(smallBuffer2);
});
it('should return a buffer that can be opened with the "readonly" option', async function () {
this.seed();
const buffer = this.db.serialize();
expect(buffer).to.be.an.instanceof(Buffer);
expect(buffer.length).to.be.above(1000);
this.db.close();
this.db = new Database(buffer, { readonly: true });
expect(() => this.db.prepare('insert into entries (rowid, a, b) values (?, ?, ?)').run(0, 'bar', -999))
.to.throw(Database.SqliteError);
expect(this.db.prepare('select a, b from entries order by rowid limit 2').all())
.to.deep.equal([{ a: 'foo', b: 1 }, { a: 'foo', b: 2 }]);
const bufferCopy = this.db.serialize();
expect(buffer.length).to.equal(bufferCopy.length);
expect(buffer).to.deep.equal(bufferCopy);
});
it('should work with an empty database', async function () {
this.db.close();
this.db = new Database();
const buffer = this.db.serialize();
expect(buffer).to.be.an.instanceof(Buffer);
expect(buffer.length).to.equal(0);
this.db.close();
this.db = new Database(buffer);
expect(this.db.serialize().length).to.equal(0);
});
});

View File

@ -62,6 +62,16 @@ describe('BigInts', function () {
expect(this.db.prepare('SELECT customfunc(?)').pluck().get(2)).to.equal('number2');
expect(this.db.prepare('SELECT customfunc(?)').pluck().get(BigInt(2))).to.equal('bigint2');
});
it('should get passed to aggregates defined with the "safeIntegers" option', function () {
this.db.aggregate('customagg', { safeIntegers: true, step: (_, a) => { return (typeof a) + a; } });
expect(this.db.prepare('SELECT customagg(?)').pluck().get(2)).to.equal('number2');
expect(this.db.prepare('SELECT customagg(?)').pluck().get(BigInt(2))).to.equal('bigint2');
});
it('should get passed to virtual tables defined with the "safeIntegers" option', function () {
this.db.table('customvtab', { safeIntegers: true, columns: ['x'], *rows(a) { yield [(typeof a) + a]; } });
expect(this.db.prepare('SELECT * FROM customvtab(?)').pluck().get(2)).to.equal('number2');
expect(this.db.prepare('SELECT * FROM customvtab(?)').pluck().get(BigInt(2))).to.equal('bigint2');
});
it('should respect the default setting on the database', function () {
let arg;
const int = BigInt('1006028374637854687');
@ -70,6 +80,16 @@ describe('BigInts', function () {
this.db.prepare(`SELECT ${name}(?)`).get(int);
return arg;
};
const customAggregateArg = (name, options, dontDefine) => {
dontDefine || this.db.aggregate(name, { ...options, step: (_, a) => { arg = a; } });
this.db.prepare(`SELECT ${name}(?)`).get(int);
return arg;
};
const customTableArg = (name, options, dontDefine) => {
dontDefine || this.db.table(name, { ...options, columns: ['x'], *rows(a) { arg = a; } });
this.db.prepare(`SELECT * FROM ${name}(?)`).get(int);
return arg;
};
this.db.prepare('INSERT INTO entries VALUES (?, ?, ?)').run(int, int, int);
this.db.defaultSafeIntegers(true);
@ -78,6 +98,10 @@ describe('BigInts', function () {
expect(stmt.safeIntegers(false).get()).to.equal(1006028374637854700);
expect(customFunctionArg('a1')).to.deep.equal(int);
expect(customFunctionArg('a2', { safeIntegers: false })).to.equal(1006028374637854700);
expect(customAggregateArg('a1')).to.deep.equal(int);
expect(customAggregateArg('a2', { safeIntegers: false })).to.equal(1006028374637854700);
expect(customTableArg('a1')).to.deep.equal(int);
expect(customTableArg('a2', { safeIntegers: false })).to.equal(1006028374637854700);
this.db.defaultSafeIntegers(false);
@ -86,6 +110,10 @@ describe('BigInts', function () {
expect(stmt2.safeIntegers().get()).to.deep.equal(int);
expect(customFunctionArg('a3')).to.equal(1006028374637854700);
expect(customFunctionArg('a4', { safeIntegers: true })).to.deep.equal(int);
expect(customAggregateArg('a3')).to.equal(1006028374637854700);
expect(customAggregateArg('a4', { safeIntegers: true })).to.deep.equal(int);
expect(customTableArg('a3')).to.equal(1006028374637854700);
expect(customTableArg('a4', { safeIntegers: true })).to.deep.equal(int);
this.db.defaultSafeIntegers();
@ -95,11 +123,23 @@ describe('BigInts', function () {
expect(customFunctionArg('a2', {}, true)).to.equal(1006028374637854700);
expect(customFunctionArg('a3', {}, true)).to.equal(1006028374637854700);
expect(customFunctionArg('a4', {}, true)).to.deep.equal(int);
expect(customAggregateArg('a1', {}, true)).to.deep.equal(int);
expect(customAggregateArg('a2', {}, true)).to.equal(1006028374637854700);
expect(customAggregateArg('a3', {}, true)).to.equal(1006028374637854700);
expect(customAggregateArg('a4', {}, true)).to.deep.equal(int);
expect(customTableArg('a1', {}, true)).to.deep.equal(int);
expect(customTableArg('a2', {}, true)).to.equal(1006028374637854700);
expect(customTableArg('a3', {}, true)).to.equal(1006028374637854700);
expect(customTableArg('a4', {}, true)).to.deep.equal(int);
const stmt3 = this.db.prepare('SELECT a FROM entries').pluck();
expect(stmt3.get()).to.deep.equal(int);
expect(stmt3.safeIntegers(false).get()).to.equal(1006028374637854700);
expect(customFunctionArg('a5')).to.deep.equal(int);
expect(customFunctionArg('a6', { safeIntegers: false })).to.equal(1006028374637854700);
expect(customAggregateArg('a5')).to.deep.equal(int);
expect(customAggregateArg('a6', { safeIntegers: false })).to.equal(1006028374637854700);
expect(customTableArg('a5')).to.deep.equal(int);
expect(customTableArg('a6', { safeIntegers: false })).to.equal(1006028374637854700);
});
});

View File

@ -167,6 +167,26 @@ describe('integrity checks', function () {
});
});
describe('Database#table()', function () {
specify('while iterating (blocked)', function () {
let i = 0;
whileIterating(this, blocked(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
expect(i).to.equal(5);
normally(allowed(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
});
specify('while busy (blocked)', function () {
let i = 0;
whileBusy(this, blocked(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
expect(i).to.equal(5);
normally(allowed(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
});
specify('while closed (blocked)', function () {
let i = 0;
whileClosed(this, blocked(() => this.db.table(`tbl_${++i}`, { columns: ['x'], *rows() {} })));
expect(i).to.equal(1);
});
});
describe('Database#loadExtension()', function () {
let filepath;
before(function () {