1
0

Forked SQLite deno WASM lib

This commit is contained in:
Zef Hemel 2022-10-19 09:51:25 +02:00
parent e5a192b358
commit e225c926f5
45 changed files with 260064 additions and 0 deletions

View File

@ -0,0 +1 @@
build/lib/** linguist-vendored

23
plugos/forked/deno-sqlite/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# System files
.DS_Store
# Debug builds
debug.js
# Misc
hack.*
*.br
*.db
*.db-journal
*.db-shm
*.sqlite
*.sqlite-journal
*.sqlite-shm
*.gz
build/sqlite-src
build/wasi-sdk
deno
# NPM garbage
node_modules
package-lock.json

View File

@ -0,0 +1,88 @@
# Contribute to SQLite for Deno
> Note: this is a draft
Thank you for considering to contribute to the SQLite for Deno module! Below are
a few guidelines on how to contribute.
## Prerequisites
To work on the JavaScript/ TypeScript wrapper module, all you need is a
[deno](https://deno.land) runtime.
To change the compiled SQLite WASM binary, you will require to download the
[WASI SDK][wasi-sdk]. This process should function fully automatically for most
users.
**To install build dependencies** go to the `build` folder (`cd build`), then
run `make setup`.
**To compile the binary** run `make release` (or `make debug` for a debug
build). If you changed any build flags of SQLite, also run `make amalgamation`,
before building.
If you are interested in more details regarding the compilation setup, also see
[this blog post][compile-wasm-blog].
## Code Style, Review, and Dependencies
This project uses the `deno fmt` code style.
This project uses no external dependencies (with the exception of a copy of the
SQLite C library).
For testing purposes, Deno standard library modules may be used.
## Documentation
Any user-facing interfaces should be documented. To document such interfaces,
include a **JSDoc comment**, which should be formatted as follows:
```javascript
/**
* A short but complete description, formatted
* as markdown.
*/
functionName(arg1, arg2) {
// ...
}
```
Comments with this format will be automatically parsed by `deno doc`.
These comments should not include examples unless they are essential to
illustrating an important point.
## Tests and Benchmarks
Any important functionality should be tested. Tests are in the `test.ts` file.
Changes will not be merged unless all tests pass.
Benchmarks are in the `bench.ts` file.
## Technical Direction
The goal of this module is to provide a **simple and predictable** interface to
SQLite. The interface should feel like a JavaScript library, but also
immediately make sense to someone who knows the SQLite C/C++ interface. Features
and interfaces should generally be orthogonal.
This is a low-level library, which provides access to running SQL queries and
retrieving the results of these queries. This library will only wrap SQLite C
API functions, but never try to provide a higher level interface to the database
than plain SQL. It is meant to serve as a building block for constructing higher
level interfaces, or for people who need an easy way to execute SQL queries on
their SQLite database.
The library should be easy to use and behave as any regular JavaScript library
would in Deno. This means, it should only need the required permissions (e.g. if
only in-memory databases are used, no permissions should be necessary. If a
database is opened in read-only mode, `--allow-read` should be sufficient).
## License
By making contributions, you agree that anything you submit will be distributed
under the projects license (see `LICENSE`).
[wasi-sdk]: https://github.com/CraneStation/wasi-sdk/releases
[compile-wasm-blog]: https://tilman.xyz/blog/2019/12/building-webassembly-for-deno/

View File

@ -0,0 +1,19 @@
Copyright (c) 2019 - 2022 Tilman Roeder and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,107 @@
# Deno SQLite Module
[![test status](https://github.com/dyedgreen/deno-sqlite/workflows/tests/badge.svg?branch=master)](https://github.com/dyedgreen/deno-sqlite/actions)
[![deno doc](https://doc.deno.land/badge.svg)](https://deno.land/x/sqlite/mod.ts)
This is an SQLite module for JavaScript and TypeScript. The wrapper is targeted
at [Deno](https://deno.land) and uses a version of SQLite3 compiled to
WebAssembly (WASM). This module focuses on correctness, ease of use and
performance.
This module guarantees API compatibility according to
[semantic versioning](https://semver.org). Please report any issues you
encounter. Note that the `master` branch might contain new or breaking features.
The versioning guarantee applies only to
[tagged releases](https://github.com/dyedgreen/deno-sqlite/releases).
## Documentation
Documentation is available [Deno Docs](https://deno.land/x/sqlite). There is
also a list of examples in the [`examples`](./examples) folder.
## Example
```javascript
import { DB } from "https://deno.land/x/sqlite/mod.ts";
// Open a database
const db = new DB("test.db");
db.execute(`
CREATE TABLE IF NOT EXISTS people (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT
)
`);
const names = ["Peter Parker", "Clark Kent", "Bruce Wayne"];
// Run a simple query
for (const name of names) {
db.query("INSERT INTO people (name) VALUES (?)", [name]);
}
// Print out data in table
for (const [name] of db.query("SELECT name FROM people")) {
console.log(name);
}
// Close connection
db.close();
```
## Comparison to Plugin based Modules
### TL;DR
If you want something that just works (and is fast), use this library.
Depending on your specific needs, there is also
[sqlite3](https://github.com/denodrivers/sqlite3), however using this module
requires the `--allow-ffi` and `--unstable` flags, which means the database
connection may bypass e.g. file access permissions.
### Advantages
- Security: benefit from Denos security settings, without the need to trust a
third party
- Portability: runs everywhere Deno runs and can even run in the browser
- Ease of Use: takes full advantage of Denos module cache and does not require
any network access after initial download
- Speed: thanks to WASM, the database performance is comparable to native
bindings in most situations and the API is carefully designed to provide
optimal performance
### Disadvantages
- Weaker Persistence Guarantees: due to limitations in Denos file system APIs,
SQLite can't acquire file locks or memory map files (e.g. this module does not
support WAL mode)
## Browser Version (Experimental)
There is **experimental** support for using `deno-sqlite` in the browser. You
can generate a browser compatible module by running:
```bash
deno bundle --import-map browser/import_map.json browser/mod.ts [output_bundle_path]
```
The modules documentation can be seen by running
```bash
deno doc browser/mod.ts
```
Databases created in the browser are persisted using
[indexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API).
## Users
- [cotton](https://github.com/rahmanfadhil/cotton)
- [deno-nessie](https://github.com/halvardssm/deno-nessie)
- [denodb](https://github.com/eveningkid/denodb)
- [denolib/typeorm](https://github.com/denolib/typeorm)
- [small-orm-sqlite](https://github.com/enimatek-nl/small-orm-sqlite)
_(listed in alphabetical order, please submit a PR if you are using this library
and are not included)_

View File

@ -0,0 +1,88 @@
import {
bench,
runBenchmarks,
} from "https://deno.land/std@0.135.0/testing/bench.ts";
import { DB } from "./mod.ts";
if (Deno.args[0]) {
try {
await Deno.remove(Deno.args[0]);
} catch (_) {
// ignore
}
}
const dbFile = Deno.args[0] || ":memory:";
const db = new DB(dbFile);
db.query(
"CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, balance INTEGER)",
);
/** Performance of insert statements (1 insert). */
const names = "Deno Land Peter Parker Clark Kent Robert Parr".split(" ");
bench({
name: "insert 10 000 (named)",
runs: 100,
func: (b): void => {
b.start();
const query = db.prepareQuery(
"INSERT INTO users (name, balance) VALUES (:name, :balance)",
);
db.query("begin");
for (let i = 0; i < 10_000; i++) {
query.execute({ name: names[i % names.length], balance: i });
}
db.query("commit");
b.stop();
},
});
bench({
name: "insert 10 000 (positional)",
runs: 100,
func: (b): void => {
b.start();
const query = db.prepareQuery(
"INSERT INTO users (name, balance) VALUES (?, ?)",
);
db.query("begin");
for (let i = 0; i < 10_000; i++) {
query.execute([names[i % names.length], i]);
}
db.query("commit");
b.stop();
},
});
/** Performance of select statements (select all; 10_000). */
bench({
name: "select 10 000 (select all)",
runs: 100,
func: (b): void => {
b.start();
db.query(
"SELECT name, balance FROM users LIMIT 10000",
);
b.stop();
},
});
/** Performance of select statements (select individually; 10_000). */
bench({
name: "select 10 000 (select first)",
runs: 100,
func: (b): void => {
b.start();
const query = db.prepareQuery(
"SELECT name, balance FROM users WHERE id = ?",
);
for (let id = 1; id <= 10_000; id++) {
query.first([id]);
}
b.stop();
},
});
runBenchmarks();

View File

@ -0,0 +1,5 @@
{
"imports": {
"../build/vfs.js": "../browser/vfs.js"
}
}

View File

@ -0,0 +1,38 @@
import { DB } from "../src/db.ts";
import { loadFile, writeFile } from "./vfs.js";
import { compile, instantiateBrowser } from "../build/sqlite.js";
export { SqliteError } from "../src/error.ts";
export { Status } from "../src/constants.ts";
const hasCompiled = compile();
/**
* Opens a database with the given name. If `file` is
* not provided or `:memory:`, an in-memory database
* is returned which will not persist after the database
* is closed.
*/
export async function open(file?: string): Promise<DB> {
if (file != null && file !== ":memory:") await loadFile(file);
await hasCompiled;
await instantiateBrowser();
return new DB(file);
}
/**
* Overwrite a given file with arbitrary data. This can be used
* to import a database which can later be opened.
*/
export async function write(file: string, data: Uint8Array): Promise<void> {
await writeFile(file, data);
}
/**
* Read the data currently stored for a given file. This can be used
* to export a database which has been created or modified.
*/
export async function read(file: string): Promise<Uint8Array | null> {
const buffer = await loadFile(file);
return buffer?.toUint8Array()?.slice();
}

View File

@ -0,0 +1,208 @@
import { getStr } from "../src/wasm.ts";
const DB_NAME = "sqlitevfs";
const LOADED_FILES = new Map();
const OPEN_FILES = new Map();
function nextRid() {
const rid = (nextRid?.LAST_RID ?? 0) + 1;
nextRid.LAST_RID = rid;
return rid;
}
function getOpenFile(rid) {
if (!OPEN_FILES.has(rid)) {
throw new Error(`Resource ID ${rid} does not exist.`);
}
return OPEN_FILES.get(rid);
}
const MIN_GROW_BYTES = 2048;
const MAX_GROW_BYTES = 65536;
class Buffer {
constructor(data) {
this._data = data ?? new Uint8Array();
this._size = this._data.length;
}
get size() {
return this._size;
}
read(offset, buffer) {
if (offset >= this._size) return 0;
const toCopy = this._data.subarray(
offset,
Math.min(this._size, offset + buffer.length),
);
buffer.set(toCopy);
return toCopy.length;
}
reserve(capacity) {
if (this._data.length >= capacity) return;
const neededBytes = capacity - this._data.length;
const growBy = Math.min(
MAX_GROW_BYTES,
Math.max(MIN_GROW_BYTES, this._data.length),
);
const newArray = new Uint8Array(
this._data.length + Math.max(growBy, neededBytes),
);
newArray.set(this._data);
this._data = newArray;
}
write(offset, buffer) {
this.reserve(offset + buffer.length);
this._data.set(buffer, offset);
this._size = Math.max(this._size, offset + buffer.length);
return buffer.length;
}
truncate(size) {
this._size = size;
}
toUint8Array() {
return this._data.subarray(0, this._size);
}
}
const indexedDB = window.indexedDB || window.mozIndexedDB ||
window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB;
// Web browser indexedDB database
const database = new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onupgradeneeded = () =>
request.result.createObjectStore("files", { keyPath: "name" });
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
export async function loadFile(fileName) {
const db = await database;
const file = await new Promise((resolve, reject) => {
const store = db.transaction("files", "readonly").objectStore("files");
const request = store.get(fileName);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (file != null && !LOADED_FILES.has(fileName)) {
const buffer = new Buffer(file.data);
LOADED_FILES.set(fileName, buffer);
return buffer;
} else if (LOADED_FILES.has(fileName)) {
return LOADED_FILES.get(fileName);
} else {
return null;
}
}
async function syncFile(fileName, data) {
const db = await database;
await new Promise((resolve, reject) => {
const store = db.transaction("files", "readwrite").objectStore("files");
const request = store.put({ name: fileName, data });
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async function deleteFile(fileName) {
const db = await database;
await new Promise((resolve, reject) => {
const store = db.transaction("files", "readwrite").objectStore("files");
const request = store.delete(fileName);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
export async function writeFile(fileName, data) {
await syncFile(fileName, data);
if (LOADED_FILES.has(fileName)) {
const buffer = LOADED_FILES.get(fileName);
buffer.truncate(0);
buffer.write(0, data);
}
}
// Closure to return an environment that links
// the current wasm context. This is a modified
// version, suitable for use within browsers.
export default function env(inst) {
const env = {
js_print: (str_ptr) => {
const text = getStr(inst.exports, str_ptr);
console.log(text[text.length - 1] === "\n" ? text.slice(0, -1) : text);
},
js_open: (path_ptr, mode, _flags) => {
if (mode === 1 /* temp file */) {
const rid = nextRid();
OPEN_FILES.set(rid, { path: null, buffer: new Buffer() });
return rid;
} else if (mode === 0 /* regular file */) {
const path = getStr(inst.exports, path_ptr);
const buffer = LOADED_FILES.get(path) ?? new Buffer();
if (!LOADED_FILES.has(path)) LOADED_FILES.set(path, buffer);
const rid = nextRid();
OPEN_FILES.set(rid, { path, buffer });
return rid;
}
},
js_close: (rid) => {
OPEN_FILES.delete(rid);
},
js_delete: (path_ptr) => {
const path = getStr(inst.exports, path_ptr);
LOADED_FILES.delete(path);
deleteFile(path);
},
js_read: (rid, buffer_ptr, offset, amount) => {
const buffer = new Uint8Array(
inst.exports.memory.buffer,
buffer_ptr,
amount,
);
const file = getOpenFile(rid);
return file.buffer.read(offset, buffer);
},
js_write: (rid, buffer_ptr, offset, amount) => {
const buffer = new Uint8Array(
inst.exports.memory.buffer,
buffer_ptr,
amount,
);
const file = getOpenFile(rid);
return file.buffer.write(offset, buffer);
},
js_truncate: (rid, size) => {
getOpenFile(rid).buffer.truncate(size);
},
js_sync: (rid) => {
const file = getOpenFile(rid);
if (file.path != null) syncFile(file.path, file.buffer.toUint8Array());
},
js_size: (rid) => {
return getOpenFile(rid).buffer.size;
},
js_lock: (_rid, _exclusive) => {},
js_unlock: (_rid) => {},
js_time: () => {
return Date.now();
},
js_timezone: () => {
return (new Date()).getTimezoneOffset();
},
js_exists: (path_ptr) => {
const path = getStr(inst.exports, path_ptr);
return LOADED_FILES.has(path) ? 1 : 0;
},
js_access: (_path_ptr) => 1,
};
return { env };
}

View File

@ -0,0 +1,3 @@
b9365f4aa1d3047a8d80d6bfe90e705c80e158c3 sqlite_dl.zip
022ae45d50b124b9df68be065d65b9a607923362 wasi_dl_linux.tar.gz
bc76d264214c21a603fc38adb405622a2fcda8b3 wasi_dl_darwin.tar.gz

View File

@ -0,0 +1,131 @@
DENO ?= deno
WASI ?= ./wasi-sdk
CC = $(WASI)/bin/clang
OUT_WA = "sqlite.wasm"
OUT_BN = "sqlite.js"
OUT_TY = "sqlite.d.ts"
SQLITE_DLD = "https://sqlite.org/2022/sqlite-src-3390200.zip"
SQLITE_DIR = "sqlite-src-3390200"
WASI_DLD = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-16/wasi-sdk-16.0-linux.tar.gz"
WASI_TAR = wasi_dl_linux.tar.gz
ifeq ($(shell uname), Darwin)
WASI_DLD = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-16/wasi-sdk-16.0-macos.tar.gz"
WASI_TAR = wasi_dl_darwin.tar.gz
endif
CSRC = ""
CSRC += $(shell find ./src -name "*.c")
CSRC += $(shell find ./lib -name "*.c")
CSRC += $(shell find ./hask -name "*.c")
FLGS = -Wall
RFLG = -Os
DFLG = -DDEBUG_BUILD
WAFLG = --target=wasm32-unknown-wasi -Wl,--no-entry -nostartfiles --sysroot $(WASI)/share/wasi-sysroot\
-DWASI_BUILD -Wl,--export,malloc -Wl,--export,free -Wl,--allow-undefined-file=vfs.syms
INCS = -Ilib
# Location of wrapper library which contains all c-land export
CWRP = "./src/wrapper.c"
# Configure sqlite for out use-case
SQLFLG = -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=0 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS\
-DSQLITE_DEFAULT_FOREIGN_KEYS=1 -DSQLITE_TEMP_STORE=2\
-DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_UTF16 -DSQLITE_OMIT_SHARED_CACHE\
-DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_TRACE\
-DSQLITE_OS_OTHER=1 -DSQLITE_OMIT_COMPLETE -DSQLITE_OMIT_WAL\
-DNDEBUG=1 -DSQLITE_ENABLE_COLUMN_METADATA -DHAVE_LOCALTIME_R\
-DSQLITE_OMIT_DESERIALIZE -DSQLITE_ENABLE_FTS5
# Rational:
# SQLITE_DQS -> we do not need to have backwards comp
# SQLITE_THREADSAFE -> we run single-threaded
# SQLITE_LIKE_DOESNT_MATCH_BLOBS -> faster (is recommended if no backwards comp)
# SQLITE_DEFAULT_FOREIGN_KEYS -> this should be the default
# SQLITE_TEMP_STORE -> in memory is faster, so it's the better default
# SQLITE_OMIT_DEPRECATED -> we do not need to have backwards comp
# SQLITE_OMIT_UTF16 -> we only support utf-8 encoded strings
# SQLITE_OMIT_SHARED_CACHE -> we only ever open one connection
# SQLITE_OMIT_LOAD_EXTENSION -> we don't use it
# SQLITE_OMIT_PROGRESS_CALLBACK -> we don't use it
# SQLITE_OMIT_TRACE -> we make no use of these
# SQLITE_OS_OTHER -> we provide our own vfs
# SQLITE_OMIT_COMPLETE -> we don't need these
# SQLITE_OMIT_WAL -> this is doggy, as we can not memory map files
# DNDEBUG -> "use for maximum speed"
# SQLITE_ENABLE_COLUMN_METADATA -> we depend on column metadata interfaces (`sqlite3_column_table_name` and `sqlite3_column_origin_name`)
# SQLITE_OMIT_DESERIALIZE -> we don't use these interfaces
all: release
build:
$(DENO) run --allow-read --allow-write hack/gen_syms.js vfs.syms
$(CC) $(WAFLG) $(FLGS) $(INCS) $(CSRC) $(SQLFLG) -o $(OUT_WA)
bundle:
$(DENO) run --allow-read --allow-write hack/bundle.js $(OUT_WA) $(OUT_BN)
$(DENO) fmt $(OUT_BN)
types:
$(DENO) run --allow-read --allow-write hack/gen_types.ts $(CWRP) $(OUT_TY)
$(DENO) fmt $(OUT_TY)
size.txt: sqlite.js sqlite.wasm
rm -f size.txt *.gz *.br
gzip --best < sqlite.js > sqlite.js.gz
gzip --best < sqlite.wasm > sqlite.wasm.gz
brotli --best -o sqlite.js.br < sqlite.js && \
brotli --best -o sqlite.wasm.br < sqlite.wasm || \
echo "WARN: brotli size comparison unavailable"
ls -l sqlite.* | awk '{printf "%-15s➜%7s bytes\n",$$9,$$5}' | tee size.txt
debug: FLGS += $(DFLG)
debug: build
debug: bundle
debug: types
release: FLGS += $(RFLG)
release: build
release: bundle
release: types
release: size.txt
amalgamation:
make -C sqlite-src clean
make -C sqlite-src sqlite3.c SQLFLG="$(SQLFLG)"
mv sqlite-src/sqlite3.c lib/sqlite3.c
mv sqlite-src/sqlite3.h lib/sqlite3.h
dlsqlite:
curl "$(SQLITE_DLD)" -o "sqlite_dl.zip"
sed -n '/sqlite_dl.zip/p' ".checksums" | shasum -c -
rm -rf "sqlite-src"
unzip "sqlite_dl.zip"
mv $(SQLITE_DIR) "sqlite-src"
cp "Makefile.sqlite" "sqlite-src/Makefile"
rm "sqlite_dl.zip"
dlwasi:
curl -L "$(WASI_DLD)" -o "$(WASI_TAR)"
sed -n '/${WASI_TAR}/p' ".checksums" | shasum -c -
rm -rf "wasi-sdk"
tar -xzvf "$(WASI_TAR)"
mv "wasi-sdk-16.0" "wasi-sdk"
rm "${WASI_TAR}"
testdb:
gcc -Ilib lib/sqlite3.c hack/gen_test_db.c -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -o gen_test_db
rm -f 2GB_test.db
./gen_test_db
rm gen_test_db
setup: dlsqlite
setup: dlwasi
clean:
rm -rf sqlite-src
rm -rf wasi-sdk
rm -f 2GB_test.db
.PHONY: build amalgamation dlsqlite dlwasi setup clean

View File

@ -0,0 +1,62 @@
const [src, dest] = Deno.args;
const wasm = await Deno.readFile(src);
function encode(bytes) {
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\n/g, "");
}
await Deno.writeFile(
dest,
new TextEncoder().encode(
`/// <reference types="./sqlite.d.ts" />
/* This file is automatically generated. Do not edit directly. */
import env from "./vfs.js";
const wasm =
"${encode(wasm)}";
function decode(base64) {
const bytesStr = atob(base64);
const bytes = new Uint8Array(bytesStr.length);
for (let i = 0, c = bytesStr.length; i < c; i++) {
bytes[i] = bytesStr.charCodeAt(i);
}
return bytes;
}
const moduleOrInstance = {
module: null,
instances: [],
};
export async function compile() {
moduleOrInstance.module = await WebAssembly.compile(decode(wasm));
}
export async function instantiateBrowser() {
const placeholder = { exports: null };
const instance = await WebAssembly.instantiate(moduleOrInstance.module, env(placeholder));
placeholder.exports = instance.exports;
instance.exports.seed_rng(Date.now());
moduleOrInstance.instances.push(instance);
}
export function instantiate() {
if (moduleOrInstance.instances.length) {
return moduleOrInstance.instances.pop();
} else {
const placeholder = { exports: null };
const instance = new WebAssembly.Instance(moduleOrInstance.module, env(placeholder));
placeholder.exports = instance.exports;
instance.exports.seed_rng(Date.now());
return instance;
}
}`,
),
);

View File

@ -0,0 +1,10 @@
// Generate available import symbols
// from vfs.js file
import env from "../vfs.js";
let symbols = "";
for (const symbol of Object.keys(env().env)) {
symbols += `${symbol}\n`;
}
await Deno.writeFile(Deno.args[0], new TextEncoder().encode(symbols));

View File

@ -0,0 +1,98 @@
#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>
#define TRUE 1
#define FALSE 0
#define TEST_DB_FILE "2GB_test.db"
#define SQL_CREATE_TBL "CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"
#define SQL_INSERT_VAL "INSERT INTO test (value) VALUES (?)"
#define VAL_LEN 65536
#define VAL_NUM 45000
void rand_str(char *dest, size_t length) {
char charset[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
while (length --> 0) {
size_t index = (double) rand() / RAND_MAX * (sizeof charset - 1);
*dest++ = charset[index];
}
*dest = '\0';
}
int execute_query(sqlite3* db, char* query) {
sqlite3_stmt* stmt_create;
if (sqlite3_prepare_v2(db, query, -1, &stmt_create, NULL) != SQLITE_OK) {
printf("Failed to prepare query statement: %s\n", sqlite3_errmsg(db));
return FALSE;
}
if (sqlite3_step(stmt_create) != SQLITE_DONE) {
printf("Failed to run query statement: %s\n", sqlite3_errmsg(db));
return FALSE;
}
sqlite3_finalize(stmt_create);
return TRUE;
}
int main(int argc, char* argv[]) {
sqlite3* db;
if (sqlite3_open(TEST_DB_FILE, &db) != SQLITE_OK) {
printf("Failed to open database: %s\n", sqlite3_errmsg(db));
return 1;
}
// create database table
sqlite3_stmt* stmt_create;
if (sqlite3_prepare_v2(db, SQL_CREATE_TBL, -1, &stmt_create, NULL) != SQLITE_OK) {
printf("Failed to prepare create table statement: %s\n", sqlite3_errmsg(db));
return 1;
}
if (sqlite3_step(stmt_create) != SQLITE_DONE) {
printf("Failed to run create table statement: %s\n", sqlite3_errmsg(db));
return 1;
}
sqlite3_finalize(stmt_create);
// insert values to reach 2GB
sqlite3_stmt* stmt_insert;
if (sqlite3_prepare_v2(db, SQL_INSERT_VAL, -1, &stmt_insert, NULL) != SQLITE_OK) {
printf("Failed to prepare insert table statement: %s\n", sqlite3_errmsg(db));
return 1;
}
// begin transaction
if (!execute_query(db, "begin")) {
return 1;
}
char* buffer = malloc(VAL_LEN + 1);
for (int64_t i = 0; i < VAL_NUM; i++) {
rand_str(buffer, VAL_LEN);
if (sqlite3_bind_text(stmt_insert, 1, buffer, VAL_LEN, NULL) != SQLITE_OK) {
printf("Failed to bind value `%s`: %s\n", buffer, sqlite3_errmsg(db));
return 1;
}
if (sqlite3_step(stmt_insert) != SQLITE_DONE) {
printf("Failed to run insert statement: %s\n", sqlite3_errmsg(db));
return 1;
}
if (sqlite3_reset(stmt_insert) != SQLITE_OK) {
printf("Failed to reset statement: %s\n", sqlite3_errmsg(db));
return 1;
}
}
// end transaction
if (!execute_query(db, "commit")) {
return 1;
}
sqlite3_finalize(stmt_insert);
sqlite3_close(db);
printf("Database created successfully.\n");
return 0;
}

View File

@ -0,0 +1,156 @@
interface Item {
name: string;
arguments: Argument[];
returnType: Type;
}
interface Argument {
name: string;
type: Type;
}
enum Type {
Void,
VoidPtr,
StringPtr,
StatementPtr,
Double,
Int,
}
const items = [
// exported manually in compiler invocation
{
name: "malloc",
arguments: [{ name: "size", type: Type.Int }],
returnType: Type.VoidPtr,
},
{
name: "free",
arguments: [{ name: "ptr", type: Type.VoidPtr }],
returnType: Type.Void,
},
];
const [src, dest] = Deno.args;
const wrapperSrc = await Deno.readTextFile(src);
// int EXPORT(bind_int) (sqlite3_stmt* stmt, int idx, double value)
const typeRegexp =
`(const +)?(sqlite3_stmt\\*|char\\*|void\\*|int|uint32_t|double|void)`;
const argRegexp = `${typeRegexp} +[a-z_]+`;
const exportSignature = new RegExp(
`${typeRegexp} +EXPORT\\([a-z_]+\\) +\\(((${argRegexp}( *, *${argRegexp})*)|)\\)`,
);
function nullThrows<T>(value: T | null | undefined): T {
if (value == null) {
throw new Error("Got a null value");
}
return value as T;
}
function typeFromCType(cType: string): Type {
cType = cType.replace("const", "").replace(/ /g, "");
switch (cType) {
case "void":
return Type.Void;
case "void*":
return Type.VoidPtr;
case "char*":
return Type.StringPtr;
case "sqlite3_stmt*":
return Type.StatementPtr;
case "double":
return Type.Double;
case "int":
case "uint32_t":
return Type.Int;
default:
throw new Error("Unknown type");
}
}
function getReturnType(line: string): Type {
const regexp = new RegExp(typeRegexp);
const [, _const, cType] = nullThrows(regexp.exec(line));
return typeFromCType(cType);
}
function getName(line: string): string {
const [, name] = nullThrows(/EXPORT\(([a-z_]+)\)/.exec(line));
return name;
}
function getArguments(line: string): Argument[] {
const [, argList] = nullThrows(/EXPORT\([a-z_]+\) *\(([^)]*)\)/.exec(line));
if (argList.length === 0) {
return [];
} else {
return argList.split(",").map((arg) => {
const regexp = new RegExp(`${typeRegexp} +([a-z_]+)`);
const [, _const, cType, name] = nullThrows(regexp.exec(arg));
return {
name,
type: typeFromCType(cType),
};
});
}
}
function generateType(tp: Type): string {
switch (tp) {
case Type.Void:
return "void";
case Type.VoidPtr:
return "VoidPtr";
case Type.StringPtr:
return "StringPtr";
case Type.StatementPtr:
return "StatementPtr";
case Type.Int:
case Type.Double:
return "number";
default:
throw new Error("Unknown type");
}
}
function generateDecl(item: Item): string {
const args = item.arguments.map((arg) =>
`${arg.name}: ${generateType(arg.type)}`
).join(", ");
return `${item.name}: (${args}) => ${generateType(item.returnType)}`;
}
const exportLines = wrapperSrc.split("\n").filter((line) =>
exportSignature.test(line)
);
for (const line of exportLines) {
const name = getName(line);
const returnType = getReturnType(line);
const args = getArguments(line);
items.push({ name, returnType, arguments: args });
}
const typeDeclaration =
`/* This file is automatically generated. Do not edit directly. */
export type VoidPtr = number;
export type StringPtr = number;
export type StatementPtr = number;
export interface Wasm {
memory: WebAssembly.Memory;
${items.map(generateDecl).join(";\n ")};
}
export function compile(): Promise<void>;
export function instantiateBrowser(): Promise<void>;
export function instantiate(): { exports: Wasm };
`;
await Deno.writeTextFile(dest, typeDeclaration);

View File

@ -0,0 +1,34 @@
#include "pcg.h"
// Random number generator.
// Based on:
// *Really* minimal PCG32 code / (c) 2014 M.E. O'Neill / pcg-random.org
// Licensed under Apache License 2.0 (NO WARRANTY, etc. see website)
uint64_t state = 0x853c49e6748fea9bULL;
uint64_t inc = 0xda3e39cb94b95bdbULL;
// Update seed of generator.
void pcg_seed(uint64_t seed) {
state = seed;
}
// Generate random integer.
uint32_t pcg_rand() {
uint64_t oldstate = state;
// Advance internal state
state = oldstate * 6364136223846793005ULL + (inc|1);
// Calculate output function (XSH RR), uses old state for max ILP
uint32_t xorshifted = ((oldstate >> 18u) ^ oldstate) >> 27u;
uint32_t rot = oldstate >> 59u;
return (xorshifted >> rot) | (xorshifted << ((-rot) & 31));
}
// Fill out buffer with size random bytes.
void pcg_bytes(char* out, int size) {
// TODO: We can be more efficient by using all 4
// pieces of the random number returned.
for (int i = 0; i < size; i ++) {
out[i] = (char)pcg_rand();
}
}

View File

@ -0,0 +1,13 @@
#ifndef PCG_H
#define PCG_H
#include <stdint.h>
// Seed the random number generator
void pcg_seed(uint64_t seed);
// Get random numbers and random bits
uint32_t pcg_rand();
void pcg_bytes(char* out, int size);
#endif // PCG_H

View File

@ -0,0 +1,914 @@
///////////////////////////////////////////////////////////////////////////////
// \author (c) Marco Paland (info@paland.com)
// 2014-2019, PALANDesign Hannover, Germany
//
// \license The MIT License (MIT)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// \brief Tiny printf, sprintf and (v)snprintf implementation, optimized for speed on
// embedded systems with a very limited resources. These routines are thread
// safe and reentrant!
// Use this instead of the bloated standard/newlib printf cause these use
// malloc for printf (and may not be thread safe).
//
///////////////////////////////////////////////////////////////////////////////
#include <stdbool.h>
#include <stdint.h>
#include "printf.h"
// define this globally (e.g. gcc -DPRINTF_INCLUDE_CONFIG_H ...) to include the
// printf_config.h header file
// default: undefined
#ifdef PRINTF_INCLUDE_CONFIG_H
#include "printf_config.h"
#endif
// 'ntoa' conversion buffer size, this must be big enough to hold one converted
// numeric number including padded zeros (dynamically created on stack)
// default: 32 byte
#ifndef PRINTF_NTOA_BUFFER_SIZE
#define PRINTF_NTOA_BUFFER_SIZE 32U
#endif
// 'ftoa' conversion buffer size, this must be big enough to hold one converted
// float number including padded zeros (dynamically created on stack)
// default: 32 byte
#ifndef PRINTF_FTOA_BUFFER_SIZE
#define PRINTF_FTOA_BUFFER_SIZE 32U
#endif
// support for the floating point type (%f)
// default: activated
#ifndef PRINTF_DISABLE_SUPPORT_FLOAT
#define PRINTF_SUPPORT_FLOAT
#endif
// support for exponential floating point notation (%e/%g)
// default: activated
#ifndef PRINTF_DISABLE_SUPPORT_EXPONENTIAL
#define PRINTF_SUPPORT_EXPONENTIAL
#endif
// define the default floating point precision
// default: 6 digits
#ifndef PRINTF_DEFAULT_FLOAT_PRECISION
#define PRINTF_DEFAULT_FLOAT_PRECISION 6U
#endif
// define the largest float suitable to print with %f
// default: 1e9
#ifndef PRINTF_MAX_FLOAT
#define PRINTF_MAX_FLOAT 1e9
#endif
// support for the long long types (%llu or %p)
// default: activated
#ifndef PRINTF_DISABLE_SUPPORT_LONG_LONG
#define PRINTF_SUPPORT_LONG_LONG
#endif
// support for the ptrdiff_t type (%t)
// ptrdiff_t is normally defined in <stddef.h> as long or long long type
// default: activated
#ifndef PRINTF_DISABLE_SUPPORT_PTRDIFF_T
#define PRINTF_SUPPORT_PTRDIFF_T
#endif
///////////////////////////////////////////////////////////////////////////////
// internal flag definitions
#define FLAGS_ZEROPAD (1U << 0U)
#define FLAGS_LEFT (1U << 1U)
#define FLAGS_PLUS (1U << 2U)
#define FLAGS_SPACE (1U << 3U)
#define FLAGS_HASH (1U << 4U)
#define FLAGS_UPPERCASE (1U << 5U)
#define FLAGS_CHAR (1U << 6U)
#define FLAGS_SHORT (1U << 7U)
#define FLAGS_LONG (1U << 8U)
#define FLAGS_LONG_LONG (1U << 9U)
#define FLAGS_PRECISION (1U << 10U)
#define FLAGS_ADAPT_EXP (1U << 11U)
// import float.h for DBL_MAX
#if defined(PRINTF_SUPPORT_FLOAT)
#include <float.h>
#endif
// output function type
typedef void (*out_fct_type)(char character, void* buffer, size_t idx, size_t maxlen);
// wrapper (used as buffer) for output function type
typedef struct {
void (*fct)(char character, void* arg);
void* arg;
} out_fct_wrap_type;
// internal buffer output
static inline void _out_buffer(char character, void* buffer, size_t idx, size_t maxlen)
{
if (idx < maxlen) {
((char*)buffer)[idx] = character;
}
}
// internal null output
static inline void _out_null(char character, void* buffer, size_t idx, size_t maxlen)
{
(void)character; (void)buffer; (void)idx; (void)maxlen;
}
// internal _putchar wrapper
static inline void _out_char(char character, void* buffer, size_t idx, size_t maxlen)
{
(void)buffer; (void)idx; (void)maxlen;
if (character) {
_putchar(character);
}
}
// internal output function wrapper
static inline void _out_fct(char character, void* buffer, size_t idx, size_t maxlen)
{
(void)idx; (void)maxlen;
if (character) {
// buffer is the output fct pointer
((out_fct_wrap_type*)buffer)->fct(character, ((out_fct_wrap_type*)buffer)->arg);
}
}
// internal secure strlen
// \return The length of the string (excluding the terminating 0) limited by 'maxsize'
static inline unsigned int _strnlen_s(const char* str, size_t maxsize)
{
const char* s;
for (s = str; *s && maxsize--; ++s);
return (unsigned int)(s - str);
}
// internal test if char is a digit (0-9)
// \return true if char is a digit
static inline bool _is_digit(char ch)
{
return (ch >= '0') && (ch <= '9');
}
// internal ASCII string to unsigned int conversion
static unsigned int _atoi(const char** str)
{
unsigned int i = 0U;
while (_is_digit(**str)) {
i = i * 10U + (unsigned int)(*((*str)++) - '0');
}
return i;
}
// output the specified string in reverse, taking care of any zero-padding
static size_t _out_rev(out_fct_type out, char* buffer, size_t idx, size_t maxlen, const char* buf, size_t len, unsigned int width, unsigned int flags)
{
const size_t start_idx = idx;
// pad spaces up to given width
if (!(flags & FLAGS_LEFT) && !(flags & FLAGS_ZEROPAD)) {
for (size_t i = len; i < width; i++) {
out(' ', buffer, idx++, maxlen);
}
}
// reverse string
while (len) {
out(buf[--len], buffer, idx++, maxlen);
}
// append pad spaces up to given width
if (flags & FLAGS_LEFT) {
while (idx - start_idx < width) {
out(' ', buffer, idx++, maxlen);
}
}
return idx;
}
// internal itoa format
static size_t _ntoa_format(out_fct_type out, char* buffer, size_t idx, size_t maxlen, char* buf, size_t len, bool negative, unsigned int base, unsigned int prec, unsigned int width, unsigned int flags)
{
// pad leading zeros
if (!(flags & FLAGS_LEFT)) {
if (width && (flags & FLAGS_ZEROPAD) && (negative || (flags & (FLAGS_PLUS | FLAGS_SPACE)))) {
width--;
}
while ((len < prec) && (len < PRINTF_NTOA_BUFFER_SIZE)) {
buf[len++] = '0';
}
while ((flags & FLAGS_ZEROPAD) && (len < width) && (len < PRINTF_NTOA_BUFFER_SIZE)) {
buf[len++] = '0';
}
}
// handle hash
if (flags & FLAGS_HASH) {
if (!(flags & FLAGS_PRECISION) && len && ((len == prec) || (len == width))) {
len--;
if (len && (base == 16U)) {
len--;
}
}
if ((base == 16U) && !(flags & FLAGS_UPPERCASE) && (len < PRINTF_NTOA_BUFFER_SIZE)) {
buf[len++] = 'x';
}
else if ((base == 16U) && (flags & FLAGS_UPPERCASE) && (len < PRINTF_NTOA_BUFFER_SIZE)) {
buf[len++] = 'X';
}
else if ((base == 2U) && (len < PRINTF_NTOA_BUFFER_SIZE)) {
buf[len++] = 'b';
}
if (len < PRINTF_NTOA_BUFFER_SIZE) {
buf[len++] = '0';
}
}
if (len < PRINTF_NTOA_BUFFER_SIZE) {
if (negative) {
buf[len++] = '-';
}
else if (flags & FLAGS_PLUS) {
buf[len++] = '+'; // ignore the space if the '+' exists
}
else if (flags & FLAGS_SPACE) {
buf[len++] = ' ';
}
}
return _out_rev(out, buffer, idx, maxlen, buf, len, width, flags);
}
// internal itoa for 'long' type
static size_t _ntoa_long(out_fct_type out, char* buffer, size_t idx, size_t maxlen, unsigned long value, bool negative, unsigned long base, unsigned int prec, unsigned int width, unsigned int flags)
{
char buf[PRINTF_NTOA_BUFFER_SIZE];
size_t len = 0U;
// no hash for 0 values
if (!value) {
flags &= ~FLAGS_HASH;
}
// write if precision != 0 and value is != 0
if (!(flags & FLAGS_PRECISION) || value) {
do {
const char digit = (char)(value % base);
buf[len++] = digit < 10 ? '0' + digit : (flags & FLAGS_UPPERCASE ? 'A' : 'a') + digit - 10;
value /= base;
} while (value && (len < PRINTF_NTOA_BUFFER_SIZE));
}
return _ntoa_format(out, buffer, idx, maxlen, buf, len, negative, (unsigned int)base, prec, width, flags);
}
// internal itoa for 'long long' type
#if defined(PRINTF_SUPPORT_LONG_LONG)
static size_t _ntoa_long_long(out_fct_type out, char* buffer, size_t idx, size_t maxlen, unsigned long long value, bool negative, unsigned long long base, unsigned int prec, unsigned int width, unsigned int flags)
{
char buf[PRINTF_NTOA_BUFFER_SIZE];
size_t len = 0U;
// no hash for 0 values
if (!value) {
flags &= ~FLAGS_HASH;
}
// write if precision != 0 and value is != 0
if (!(flags & FLAGS_PRECISION) || value) {
do {
const char digit = (char)(value % base);
buf[len++] = digit < 10 ? '0' + digit : (flags & FLAGS_UPPERCASE ? 'A' : 'a') + digit - 10;
value /= base;
} while (value && (len < PRINTF_NTOA_BUFFER_SIZE));
}
return _ntoa_format(out, buffer, idx, maxlen, buf, len, negative, (unsigned int)base, prec, width, flags);
}
#endif // PRINTF_SUPPORT_LONG_LONG
#if defined(PRINTF_SUPPORT_FLOAT)
#if defined(PRINTF_SUPPORT_EXPONENTIAL)
// forward declaration so that _ftoa can switch to exp notation for values > PRINTF_MAX_FLOAT
static size_t _etoa(out_fct_type out, char* buffer, size_t idx, size_t maxlen, double value, unsigned int prec, unsigned int width, unsigned int flags);
#endif
// internal ftoa for fixed decimal floating point
static size_t _ftoa(out_fct_type out, char* buffer, size_t idx, size_t maxlen, double value, unsigned int prec, unsigned int width, unsigned int flags)
{
char buf[PRINTF_FTOA_BUFFER_SIZE];
size_t len = 0U;
double diff = 0.0;
// powers of 10
static const double pow10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 };
// test for special values
if (value != value)
return _out_rev(out, buffer, idx, maxlen, "nan", 3, width, flags);
if (value < -DBL_MAX)
return _out_rev(out, buffer, idx, maxlen, "fni-", 4, width, flags);
if (value > DBL_MAX)
return _out_rev(out, buffer, idx, maxlen, (flags & FLAGS_PLUS) ? "fni+" : "fni", (flags & FLAGS_PLUS) ? 4U : 3U, width, flags);
// test for very large values
// standard printf behavior is to print EVERY whole number digit -- which could be 100s of characters overflowing your buffers == bad
if ((value > PRINTF_MAX_FLOAT) || (value < -PRINTF_MAX_FLOAT)) {
#if defined(PRINTF_SUPPORT_EXPONENTIAL)
return _etoa(out, buffer, idx, maxlen, value, prec, width, flags);
#else
return 0U;
#endif
}
// test for negative
bool negative = false;
if (value < 0) {
negative = true;
value = 0 - value;
}
// set default precision, if not set explicitly
if (!(flags & FLAGS_PRECISION)) {
prec = PRINTF_DEFAULT_FLOAT_PRECISION;
}
// limit precision to 9, cause a prec >= 10 can lead to overflow errors
while ((len < PRINTF_FTOA_BUFFER_SIZE) && (prec > 9U)) {
buf[len++] = '0';
prec--;
}
int whole = (int)value;
double tmp = (value - whole) * pow10[prec];
unsigned long frac = (unsigned long)tmp;
diff = tmp - frac;
if (diff > 0.5) {
++frac;
// handle rollover, e.g. case 0.99 with prec 1 is 1.0
if (frac >= pow10[prec]) {
frac = 0;
++whole;
}
}
else if (diff < 0.5) {
}
else if ((frac == 0U) || (frac & 1U)) {
// if halfway, round up if odd OR if last digit is 0
++frac;
}
if (prec == 0U) {
diff = value - (double)whole;
if ((!(diff < 0.5) || (diff > 0.5)) && (whole & 1)) {
// exactly 0.5 and ODD, then round up
// 1.5 -> 2, but 2.5 -> 2
++whole;
}
}
else {
unsigned int count = prec;
// now do fractional part, as an unsigned number
while (len < PRINTF_FTOA_BUFFER_SIZE) {
--count;
buf[len++] = (char)(48U + (frac % 10U));
if (!(frac /= 10U)) {
break;
}
}
// add extra 0s
while ((len < PRINTF_FTOA_BUFFER_SIZE) && (count-- > 0U)) {
buf[len++] = '0';
}
if (len < PRINTF_FTOA_BUFFER_SIZE) {
// add decimal
buf[len++] = '.';
}
}
// do whole part, number is reversed
while (len < PRINTF_FTOA_BUFFER_SIZE) {
buf[len++] = (char)(48 + (whole % 10));
if (!(whole /= 10)) {
break;
}
}
// pad leading zeros
if (!(flags & FLAGS_LEFT) && (flags & FLAGS_ZEROPAD)) {
if (width && (negative || (flags & (FLAGS_PLUS | FLAGS_SPACE)))) {
width--;
}
while ((len < width) && (len < PRINTF_FTOA_BUFFER_SIZE)) {
buf[len++] = '0';
}
}
if (len < PRINTF_FTOA_BUFFER_SIZE) {
if (negative) {
buf[len++] = '-';
}
else if (flags & FLAGS_PLUS) {
buf[len++] = '+'; // ignore the space if the '+' exists
}
else if (flags & FLAGS_SPACE) {
buf[len++] = ' ';
}
}
return _out_rev(out, buffer, idx, maxlen, buf, len, width, flags);
}
#if defined(PRINTF_SUPPORT_EXPONENTIAL)
// internal ftoa variant for exponential floating-point type, contributed by Martijn Jasperse <m.jasperse@gmail.com>
static size_t _etoa(out_fct_type out, char* buffer, size_t idx, size_t maxlen, double value, unsigned int prec, unsigned int width, unsigned int flags)
{
// check for NaN and special values
if ((value != value) || (value > DBL_MAX) || (value < -DBL_MAX)) {
return _ftoa(out, buffer, idx, maxlen, value, prec, width, flags);
}
// determine the sign
const bool negative = value < 0;
if (negative) {
value = -value;
}
// default precision
if (!(flags & FLAGS_PRECISION)) {
prec = PRINTF_DEFAULT_FLOAT_PRECISION;
}
// determine the decimal exponent
// based on the algorithm by David Gay (https://www.ampl.com/netlib/fp/dtoa.c)
union {
uint64_t U;
double F;
} conv;
conv.F = value;
int exp2 = (int)((conv.U >> 52U) & 0x07FFU) - 1023; // effectively log2
conv.U = (conv.U & ((1ULL << 52U) - 1U)) | (1023ULL << 52U); // drop the exponent so conv.F is now in [1,2)
// now approximate log10 from the log2 integer part and an expansion of ln around 1.5
int expval = (int)(0.1760912590558 + exp2 * 0.301029995663981 + (conv.F - 1.5) * 0.289529654602168);
// now we want to compute 10^expval but we want to be sure it won't overflow
exp2 = (int)(expval * 3.321928094887362 + 0.5);
const double z = expval * 2.302585092994046 - exp2 * 0.6931471805599453;
const double z2 = z * z;
conv.U = (uint64_t)(exp2 + 1023) << 52U;
// compute exp(z) using continued fractions, see https://en.wikipedia.org/wiki/Exponential_function#Continued_fractions_for_ex
conv.F *= 1 + 2 * z / (2 - z + (z2 / (6 + (z2 / (10 + z2 / 14)))));
// correct for rounding errors
if (value < conv.F) {
expval--;
conv.F /= 10;
}
// the exponent format is "%+03d" and largest value is "307", so set aside 4-5 characters
unsigned int minwidth = ((expval < 100) && (expval > -100)) ? 4U : 5U;
// in "%g" mode, "prec" is the number of *significant figures* not decimals
if (flags & FLAGS_ADAPT_EXP) {
// do we want to fall-back to "%f" mode?
if ((value >= 1e-4) && (value < 1e6)) {
if ((int)prec > expval) {
prec = (unsigned)((int)prec - expval - 1);
}
else {
prec = 0;
}
flags |= FLAGS_PRECISION; // make sure _ftoa respects precision
// no characters in exponent
minwidth = 0U;
expval = 0;
}
else {
// we use one sigfig for the whole part
if ((prec > 0) && (flags & FLAGS_PRECISION)) {
--prec;
}
}
}
// will everything fit?
unsigned int fwidth = width;
if (width > minwidth) {
// we didn't fall-back so subtract the characters required for the exponent
fwidth -= minwidth;
} else {
// not enough characters, so go back to default sizing
fwidth = 0U;
}
if ((flags & FLAGS_LEFT) && minwidth) {
// if we're padding on the right, DON'T pad the floating part
fwidth = 0U;
}
// rescale the float value
if (expval) {
value /= conv.F;
}
// output the floating part
const size_t start_idx = idx;
idx = _ftoa(out, buffer, idx, maxlen, negative ? -value : value, prec, fwidth, flags & ~FLAGS_ADAPT_EXP);
// output the exponent part
if (minwidth) {
// output the exponential symbol
out((flags & FLAGS_UPPERCASE) ? 'E' : 'e', buffer, idx++, maxlen);
// output the exponent value
idx = _ntoa_long(out, buffer, idx, maxlen, (expval < 0) ? -expval : expval, expval < 0, 10, 0, minwidth-1, FLAGS_ZEROPAD | FLAGS_PLUS);
// might need to right-pad spaces
if (flags & FLAGS_LEFT) {
while (idx - start_idx < width) out(' ', buffer, idx++, maxlen);
}
}
return idx;
}
#endif // PRINTF_SUPPORT_EXPONENTIAL
#endif // PRINTF_SUPPORT_FLOAT
// internal vsnprintf
static int _vsnprintf(out_fct_type out, char* buffer, const size_t maxlen, const char* format, va_list va)
{
unsigned int flags, width, precision, n;
size_t idx = 0U;
if (!buffer) {
// use null output function
out = _out_null;
}
while (*format)
{
// format specifier? %[flags][width][.precision][length]
if (*format != '%') {
// no
out(*format, buffer, idx++, maxlen);
format++;
continue;
}
else {
// yes, evaluate it
format++;
}
// evaluate flags
flags = 0U;
do {
switch (*format) {
case '0': flags |= FLAGS_ZEROPAD; format++; n = 1U; break;
case '-': flags |= FLAGS_LEFT; format++; n = 1U; break;
case '+': flags |= FLAGS_PLUS; format++; n = 1U; break;
case ' ': flags |= FLAGS_SPACE; format++; n = 1U; break;
case '#': flags |= FLAGS_HASH; format++; n = 1U; break;
default : n = 0U; break;
}
} while (n);
// evaluate width field
width = 0U;
if (_is_digit(*format)) {
width = _atoi(&format);
}
else if (*format == '*') {
const int w = va_arg(va, int);
if (w < 0) {
flags |= FLAGS_LEFT; // reverse padding
width = (unsigned int)-w;
}
else {
width = (unsigned int)w;
}
format++;
}
// evaluate precision field
precision = 0U;
if (*format == '.') {
flags |= FLAGS_PRECISION;
format++;
if (_is_digit(*format)) {
precision = _atoi(&format);
}
else if (*format == '*') {
const int prec = (int)va_arg(va, int);
precision = prec > 0 ? (unsigned int)prec : 0U;
format++;
}
}
// evaluate length field
switch (*format) {
case 'l' :
flags |= FLAGS_LONG;
format++;
if (*format == 'l') {
flags |= FLAGS_LONG_LONG;
format++;
}
break;
case 'h' :
flags |= FLAGS_SHORT;
format++;
if (*format == 'h') {
flags |= FLAGS_CHAR;
format++;
}
break;
#if defined(PRINTF_SUPPORT_PTRDIFF_T)
case 't' :
flags |= (sizeof(ptrdiff_t) == sizeof(long) ? FLAGS_LONG : FLAGS_LONG_LONG);
format++;
break;
#endif
case 'j' :
flags |= (sizeof(intmax_t) == sizeof(long) ? FLAGS_LONG : FLAGS_LONG_LONG);
format++;
break;
case 'z' :
flags |= (sizeof(size_t) == sizeof(long) ? FLAGS_LONG : FLAGS_LONG_LONG);
format++;
break;
default :
break;
}
// evaluate specifier
switch (*format) {
case 'd' :
case 'i' :
case 'u' :
case 'x' :
case 'X' :
case 'o' :
case 'b' : {
// set the base
unsigned int base;
if (*format == 'x' || *format == 'X') {
base = 16U;
}
else if (*format == 'o') {
base = 8U;
}
else if (*format == 'b') {
base = 2U;
}
else {
base = 10U;
flags &= ~FLAGS_HASH; // no hash for dec format
}
// uppercase
if (*format == 'X') {
flags |= FLAGS_UPPERCASE;
}
// no plus or space flag for u, x, X, o, b
if ((*format != 'i') && (*format != 'd')) {
flags &= ~(FLAGS_PLUS | FLAGS_SPACE);
}
// ignore '0' flag when precision is given
if (flags & FLAGS_PRECISION) {
flags &= ~FLAGS_ZEROPAD;
}
// convert the integer
if ((*format == 'i') || (*format == 'd')) {
// signed
if (flags & FLAGS_LONG_LONG) {
#if defined(PRINTF_SUPPORT_LONG_LONG)
const long long value = va_arg(va, long long);
idx = _ntoa_long_long(out, buffer, idx, maxlen, (unsigned long long)(value > 0 ? value : 0 - value), value < 0, base, precision, width, flags);
#endif
}
else if (flags & FLAGS_LONG) {
const long value = va_arg(va, long);
idx = _ntoa_long(out, buffer, idx, maxlen, (unsigned long)(value > 0 ? value : 0 - value), value < 0, base, precision, width, flags);
}
else {
const int value = (flags & FLAGS_CHAR) ? (char)va_arg(va, int) : (flags & FLAGS_SHORT) ? (short int)va_arg(va, int) : va_arg(va, int);
idx = _ntoa_long(out, buffer, idx, maxlen, (unsigned int)(value > 0 ? value : 0 - value), value < 0, base, precision, width, flags);
}
}
else {
// unsigned
if (flags & FLAGS_LONG_LONG) {
#if defined(PRINTF_SUPPORT_LONG_LONG)
idx = _ntoa_long_long(out, buffer, idx, maxlen, va_arg(va, unsigned long long), false, base, precision, width, flags);
#endif
}
else if (flags & FLAGS_LONG) {
idx = _ntoa_long(out, buffer, idx, maxlen, va_arg(va, unsigned long), false, base, precision, width, flags);
}
else {
const unsigned int value = (flags & FLAGS_CHAR) ? (unsigned char)va_arg(va, unsigned int) : (flags & FLAGS_SHORT) ? (unsigned short int)va_arg(va, unsigned int) : va_arg(va, unsigned int);
idx = _ntoa_long(out, buffer, idx, maxlen, value, false, base, precision, width, flags);
}
}
format++;
break;
}
#if defined(PRINTF_SUPPORT_FLOAT)
case 'f' :
case 'F' :
if (*format == 'F') flags |= FLAGS_UPPERCASE;
idx = _ftoa(out, buffer, idx, maxlen, va_arg(va, double), precision, width, flags);
format++;
break;
#if defined(PRINTF_SUPPORT_EXPONENTIAL)
case 'e':
case 'E':
case 'g':
case 'G':
if ((*format == 'g')||(*format == 'G')) flags |= FLAGS_ADAPT_EXP;
if ((*format == 'E')||(*format == 'G')) flags |= FLAGS_UPPERCASE;
idx = _etoa(out, buffer, idx, maxlen, va_arg(va, double), precision, width, flags);
format++;
break;
#endif // PRINTF_SUPPORT_EXPONENTIAL
#endif // PRINTF_SUPPORT_FLOAT
case 'c' : {
unsigned int l = 1U;
// pre padding
if (!(flags & FLAGS_LEFT)) {
while (l++ < width) {
out(' ', buffer, idx++, maxlen);
}
}
// char output
out((char)va_arg(va, int), buffer, idx++, maxlen);
// post padding
if (flags & FLAGS_LEFT) {
while (l++ < width) {
out(' ', buffer, idx++, maxlen);
}
}
format++;
break;
}
case 's' : {
const char* p = va_arg(va, char*);
unsigned int l = _strnlen_s(p, precision ? precision : (size_t)-1);
// pre padding
if (flags & FLAGS_PRECISION) {
l = (l < precision ? l : precision);
}
if (!(flags & FLAGS_LEFT)) {
while (l++ < width) {
out(' ', buffer, idx++, maxlen);
}
}
// string output
while ((*p != 0) && (!(flags & FLAGS_PRECISION) || precision--)) {
out(*(p++), buffer, idx++, maxlen);
}
// post padding
if (flags & FLAGS_LEFT) {
while (l++ < width) {
out(' ', buffer, idx++, maxlen);
}
}
format++;
break;
}
case 'p' : {
width = sizeof(void*) * 2U;
flags |= FLAGS_ZEROPAD | FLAGS_UPPERCASE;
#if defined(PRINTF_SUPPORT_LONG_LONG)
const bool is_ll = sizeof(uintptr_t) == sizeof(long long);
if (is_ll) {
idx = _ntoa_long_long(out, buffer, idx, maxlen, (uintptr_t)va_arg(va, void*), false, 16U, precision, width, flags);
}
else {
#endif
idx = _ntoa_long(out, buffer, idx, maxlen, (unsigned long)((uintptr_t)va_arg(va, void*)), false, 16U, precision, width, flags);
#if defined(PRINTF_SUPPORT_LONG_LONG)
}
#endif
format++;
break;
}
case '%' :
out('%', buffer, idx++, maxlen);
format++;
break;
default :
out(*format, buffer, idx++, maxlen);
format++;
break;
}
}
// termination
out((char)0, buffer, idx < maxlen ? idx : maxlen - 1U, maxlen);
// return written chars without terminating \0
return (int)idx;
}
///////////////////////////////////////////////////////////////////////////////
int printf_(const char* format, ...)
{
va_list va;
va_start(va, format);
char buffer[1];
const int ret = _vsnprintf(_out_char, buffer, (size_t)-1, format, va);
va_end(va);
return ret;
}
int sprintf_(char* buffer, const char* format, ...)
{
va_list va;
va_start(va, format);
const int ret = _vsnprintf(_out_buffer, buffer, (size_t)-1, format, va);
va_end(va);
return ret;
}
int snprintf_(char* buffer, size_t count, const char* format, ...)
{
va_list va;
va_start(va, format);
const int ret = _vsnprintf(_out_buffer, buffer, count, format, va);
va_end(va);
return ret;
}
int vprintf_(const char* format, va_list va)
{
char buffer[1];
return _vsnprintf(_out_char, buffer, (size_t)-1, format, va);
}
int vsnprintf_(char* buffer, size_t count, const char* format, va_list va)
{
return _vsnprintf(_out_buffer, buffer, count, format, va);
}
int fctprintf(void (*out)(char character, void* arg), void* arg, const char* format, ...)
{
va_list va;
va_start(va, format);
const out_fct_wrap_type out_fct_wrap = { out, arg };
const int ret = _vsnprintf(_out_fct, (char*)(uintptr_t)&out_fct_wrap, (size_t)-1, format, va);
va_end(va);
return ret;
}

View File

@ -0,0 +1,117 @@
///////////////////////////////////////////////////////////////////////////////
// \author (c) Marco Paland (info@paland.com)
// 2014-2019, PALANDesign Hannover, Germany
//
// \license The MIT License (MIT)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// \brief Tiny printf, sprintf and snprintf implementation, optimized for speed on
// embedded systems with a very limited resources.
// Use this instead of bloated standard/newlib printf.
// These routines are thread safe and reentrant.
//
///////////////////////////////////////////////////////////////////////////////
#ifndef _PRINTF_H_
#define _PRINTF_H_
#include <stdarg.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Output a character to a custom device like UART, used by the printf() function
* This function is declared here only. You have to write your custom implementation somewhere
* \param character Character to output
*/
void _putchar(char character);
/**
* Tiny printf implementation
* You have to implement _putchar if you use printf()
* To avoid conflicts with the regular printf() API it is overridden by macro defines
* and internal underscore-appended functions like printf_() are used
* \param format A string that specifies the format of the output
* \return The number of characters that are written into the array, not counting the terminating null character
*/
#define printf printf_
int printf_(const char* format, ...);
/**
* Tiny sprintf implementation
* Due to security reasons (buffer overflow) YOU SHOULD CONSIDER USING (V)SNPRINTF INSTEAD!
* \param buffer A pointer to the buffer where to store the formatted string. MUST be big enough to store the output!
* \param format A string that specifies the format of the output
* \return The number of characters that are WRITTEN into the buffer, not counting the terminating null character
*/
#define sprintf sprintf_
int sprintf_(char* buffer, const char* format, ...);
/**
* Tiny snprintf/vsnprintf implementation
* \param buffer A pointer to the buffer where to store the formatted string
* \param count The maximum number of characters to store in the buffer, including a terminating null character
* \param format A string that specifies the format of the output
* \param va A value identifying a variable arguments list
* \return The number of characters that COULD have been written into the buffer, not counting the terminating
* null character. A value equal or larger than count indicates truncation. Only when the returned value
* is non-negative and less than count, the string has been completely written.
*/
#define snprintf snprintf_
#define vsnprintf vsnprintf_
int snprintf_(char* buffer, size_t count, const char* format, ...);
int vsnprintf_(char* buffer, size_t count, const char* format, va_list va);
/**
* Tiny vprintf implementation
* \param format A string that specifies the format of the output
* \param va A value identifying a variable arguments list
* \return The number of characters that are WRITTEN into the buffer, not counting the terminating null character
*/
#define vprintf vprintf_
int vprintf_(const char* format, va_list va);
/**
* printf with output function
* You may use this as dynamic alternative to printf() with its fixed _putchar() output
* \param out An output function which takes one character and an argument pointer
* \param arg An argument pointer for user data passed to output function
* \param format A string that specifies the format of the output
* \return The number of characters that are sent to the output function, not counting the terminating null character
*/
int fctprintf(void (*out)(char character, void* arg), void* arg, const char* format, ...);
#ifdef __cplusplus
}
#endif
#endif // _PRINTF_H_

241619
plugos/forked/deno-sqlite/build/lib/sqlite3.c vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
sqlite.d.ts ➜ 2213 bytes
sqlite.js ➜1116045 bytes
sqlite.js.br ➜ 374501 bytes
sqlite.js.gz ➜ 457972 bytes
sqlite.wasm ➜ 836080 bytes
sqlite.wasm.br ➜ 299791 bytes
sqlite.wasm.gz ➜ 352828 bytes

View File

@ -0,0 +1,59 @@
/* This file is automatically generated. Do not edit directly. */
export type VoidPtr = number;
export type StringPtr = number;
export type StatementPtr = number;
export interface Wasm {
memory: WebAssembly.Memory;
malloc: (size: number) => VoidPtr;
free: (ptr: VoidPtr) => void;
str_len: (str: StringPtr) => number;
seed_rng: (seed: number) => void;
get_status: () => number;
open: (filename: StringPtr, flags: number) => number;
close: () => number;
get_sqlite_error_str: () => StringPtr;
prepare: (sql: StringPtr) => StatementPtr;
finalize: (stmt: StatementPtr) => number;
reset: (stmt: StatementPtr) => number;
clear_bindings: (stmt: StatementPtr) => number;
exec: (sql: StringPtr) => number;
bind_int: (stmt: StatementPtr, idx: number, value: number) => number;
bind_double: (stmt: StatementPtr, idx: number, value: number) => number;
bind_text: (stmt: StatementPtr, idx: number, value: StringPtr) => number;
bind_blob: (
stmt: StatementPtr,
idx: number,
value: VoidPtr,
size: number,
) => number;
bind_big_int: (
stmt: StatementPtr,
idx: number,
sign: number,
high: number,
low: number,
) => number;
bind_null: (stmt: StatementPtr, idx: number) => number;
bind_parameter_index: (stmt: StatementPtr, name: StringPtr) => number;
step: (stmt: StatementPtr) => number;
column_count: (stmt: StatementPtr) => number;
column_type: (stmt: StatementPtr, col: number) => number;
column_int: (stmt: StatementPtr, col: number) => number;
column_double: (stmt: StatementPtr, col: number) => number;
column_text: (stmt: StatementPtr, col: number) => StringPtr;
column_blob: (stmt: StatementPtr, col: number) => VoidPtr;
column_bytes: (stmt: StatementPtr, col: number) => number;
column_name: (stmt: StatementPtr, col: number) => StringPtr;
column_origin_name: (stmt: StatementPtr, col: number) => StringPtr;
column_table_name: (stmt: StatementPtr, col: number) => StringPtr;
last_insert_rowid: () => number;
changes: () => number;
total_changes: () => number;
}
export function compile(): Promise<void>;
export function instantiateBrowser(): Promise<void>;
export function instantiate(): { exports: Wasm };

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,27 @@
#ifndef DEBUG_H
#define DEBUG_H
#ifdef DEBUG_BUILD
#include <stdlib.h>
#include <printf.h>
#include "imports.h"
// Print debug messages
#define debug_printf(...) { \
char* __debug_msg = malloc(2048); \
if (!__debug_msg) { \
js_print("ERROR: No memory for debug message.\n"); \
} else { \
size_t __used = snprintf(__debug_msg, 2048, "DEBUG: %s:%d:%s(): ", __FILE__, __LINE__, __func__); \
snprintf(&__debug_msg[__used], 2048 - __used, __VA_ARGS__); \
js_print(__debug_msg); \
free(__debug_msg); \
} \
}
#else // DEBUG_BUILD
#define debug_printf(...)
#endif // DEBUG_BUILD
#endif // DEBUG_H

View File

@ -0,0 +1,22 @@
#ifndef IMPORTS_H
#define IMPORTS_H
// WASM imports specified in vfs.syms
extern void js_print(const char*);
extern int js_open(const char*, int, int);
extern void js_close(int);
extern void js_delete(const char*);
extern int js_read(int, const char*, double, int);
extern int js_write(int, const char*, double, int);
extern void js_truncate(int, double);
extern void js_sync(int);
extern double js_size(int);
extern void js_lock(int, int);
extern void js_unlock(int);
extern double js_time();
extern int js_timezone();
extern int js_exists(const char*);
extern int js_access(const char*);
#endif // DEBUG_H

View File

@ -0,0 +1,302 @@
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <sqlite3.h>
#include <pcg.h>
#include "debug.h"
#include "imports.h"
// SQLite VFS component.
// Based on demoVFS from SQLlite.
// https://www.sqlite.org/src/doc/trunk/src/test_demovfs.c
#define MAXPATHNAME 1024
#define JS_MAX_SAFE_INTEGER 9007199254740991
// When using this VFS, the sqlite3_file* handles that SQLite uses are
// actually pointers to instances of type DenoFile.
typedef struct DenoFile DenoFile;
struct DenoFile {
sqlite3_file base;
// Deno file resource id
int rid;
};
static int denoClose(sqlite3_file *pFile) {
DenoFile* p = (DenoFile*)pFile;
js_close(p->rid);
debug_printf("closing file (rid %i)\n", p->rid);
return SQLITE_OK;
}
// Read data from a file.
static int denoRead(sqlite3_file *pFile, void *zBuf, int iAmt, sqlite_int64 iOfst) {
DenoFile *p = (DenoFile*)pFile;
int read_bytes = 0;
if (iOfst <= JS_MAX_SAFE_INTEGER) {
// Read bytes from buffer
read_bytes = js_read(p->rid, (char*)zBuf, (double)iOfst, iAmt);
debug_printf("attempt to read from file (rid %i, amount %i, offset %lli, read %i)\n",
p->rid, iAmt, iOfst, read_bytes);
} else {
debug_printf("read offset %lli overflows JS_MAX_SAFE_INTEGER\n", iOfst);
}
// Zero memory if read was short
if (read_bytes < iAmt)
memset(&((char*)zBuf)[read_bytes], 0, iAmt-read_bytes);
return read_bytes < iAmt ? SQLITE_IOERR_SHORT_READ : SQLITE_OK;
}
// Write data to a file.
static int denoWrite(sqlite3_file *pFile, const void *zBuf, int iAmt, sqlite_int64 iOfst) {
DenoFile *p = (DenoFile*)pFile;
int write_bytes = 0;
if (iOfst <= JS_MAX_SAFE_INTEGER) {
// Write bytes to buffer
write_bytes = js_write(p->rid, (char*)zBuf, (double)iOfst, iAmt);
debug_printf("attempt to write to file (rid %i, amount %i, offset %lli, written %i)\n",
p->rid, iAmt, iOfst, write_bytes);
} else {
debug_printf("write offset %lli overflows JS_MAX_SAFE_INTEGER\n", iOfst);
}
return write_bytes == iAmt ? SQLITE_OK : SQLITE_IOERR_WRITE;
}
// Truncate file.
static int denoTruncate(sqlite3_file *pFile, sqlite_int64 size) {
DenoFile *p = (DenoFile*)pFile;
if (size <= JS_MAX_SAFE_INTEGER) {
js_truncate(p->rid, (double)size);
debug_printf("truncating file (rid %i, size: %lli)\n", p->rid, size);
return SQLITE_OK;
} else {
debug_printf("truncate length %lli overflows JS_MAX_SAFE_INTEGER\n", size);
return SQLITE_IOERR;
}
}
// Deno provides no explicit sync for us, so we
// just have a no-op here.
// TODO(dyedgreen): Investigate if there is a better way
static int denoSync(sqlite3_file *pFile, int flags) {
DenoFile *p = (DenoFile*)pFile;
js_sync(p->rid);
debug_printf("syncing file (rid %i)\n", p->rid);
return SQLITE_OK;
}
// Write the size of the file in bytes to *pSize.
static int denoFileSize(sqlite3_file *pFile, sqlite_int64 *pSize) {
DenoFile *p = (DenoFile*)pFile;
*pSize = (sqlite_int64)js_size(p->rid);
debug_printf("read file size: %lli (rid %i)\n", *pSize, p->rid);
return SQLITE_OK;
}
// File locking
static int denoLock(sqlite3_file *pFile, int eLock) {
DenoFile *p = (DenoFile*)pFile;
switch (eLock) {
case SQLITE_LOCK_NONE:
// no op
break;
case SQLITE_LOCK_SHARED:
case SQLITE_LOCK_RESERVED: // one WASM process <-> one open database
js_lock(p->rid, 0);
break;
case SQLITE_LOCK_PENDING:
case SQLITE_LOCK_EXCLUSIVE:
js_lock(p->rid, 1);
break;
}
return SQLITE_OK;
}
static int denoUnlock(sqlite3_file *pFile, int eLock) {
DenoFile *p = (DenoFile*)pFile;
switch (eLock) {
case SQLITE_LOCK_NONE:
// no op
break;
case SQLITE_LOCK_SHARED:
case SQLITE_LOCK_RESERVED:
case SQLITE_LOCK_PENDING:
case SQLITE_LOCK_EXCLUSIVE:
js_unlock(p->rid);
break;
}
return SQLITE_OK;
}
static int denoCheckReservedLock(sqlite3_file *pFile, int *pResOut) {
*pResOut = 0;
return SQLITE_OK;
}
// No xFileControl() verbs are implemented by this VFS.
static int denoFileControl(sqlite3_file *pFile, int op, void *pArg) {
return SQLITE_NOTFOUND;
}
// TODO(dyedgreen): Should we try to get these?
static int denoSectorSize(sqlite3_file *pFile) {
return 0;
}
static int denoDeviceCharacteristics(sqlite3_file *pFile) {
return 0;
}
// Open a file handle.
static int denoOpen(
sqlite3_vfs *pVfs, /* VFS */
const char *zName, /* File to open, or 0 for a temp file */
sqlite3_file *pFile, /* Pointer to DenoFile struct to populate */
int flags, /* Input SQLITE_OPEN_XXX flags */
int *pOutFlags /* Output SQLITE_OPEN_XXX flags (or NULL) */
) {
static const sqlite3_io_methods denoio = {
1, /* iVersion */
denoClose, /* xClose */
denoRead, /* xRead */
denoWrite, /* xWrite */
denoTruncate, /* xTruncate */
denoSync, /* xSync */
denoFileSize, /* xFileSize */
denoLock, /* xLock */
denoUnlock, /* xUnlock */
denoCheckReservedLock, /* xCheckReservedLock */
denoFileControl, /* xFileControl */
denoSectorSize, /* xSectorSize */
denoDeviceCharacteristics /* xDeviceCharacteristics */
};
DenoFile *p = (DenoFile*)pFile;
p->base.pMethods = &denoio;
// TODO(dyedgreen): The current approach is to raise
// the permission error on the vfs.js side of things,
// should the error be propagates through the wrapper
// and be raised on the wrapper side of things?
p->rid = js_open(zName, zName ? 0 : 1, flags);
if (pOutFlags) {
*pOutFlags = flags;
}
debug_printf("opened file (rid %i)\n", p->rid);
debug_printf("file path name: '%s'\n", zName);
return SQLITE_OK;
}
// Delete the file at the path.
static int denoDelete(sqlite3_vfs *pVfs, const char *zPath, int dirSync) {
js_delete(zPath);
return SQLITE_OK;
}
// All valid id files are accessible.
static int denoAccess(sqlite3_vfs *pVfs, const char *zPath, int flags, int *pResOut) {
switch (flags) {
case SQLITE_ACCESS_EXISTS:
*pResOut = js_exists(zPath);
break;
default:
*pResOut = js_access(zPath);
break;
}
debug_printf("determining file access (path %s, access %i)\n", zPath, *pResOut);
return SQLITE_OK;
}
// TODO(dyedgreen): Actually resolve the full path name
static int denoFullPathname(sqlite3_vfs *pVfs, const char *zPath, int nPathOut, char *zPathOut) {
sqlite3_snprintf(nPathOut, zPathOut, "%s", zPath);
debug_printf("requesting full path name for path: %s\n", zPath);
return SQLITE_OK;
}
// We don't support shared objects
static void *denoDlOpen(sqlite3_vfs *pVfs, const char *zPath) {
return 0;
}
static void denoDlError(sqlite3_vfs *pVfs, int nByte, char *zErrMsg) {
sqlite3_snprintf(nByte, zErrMsg, "Loadable extensions are not supported");
zErrMsg[nByte-1] = '\0';
}
static void (*denoDlSym(sqlite3_vfs *pVfs, void *pH, const char *z))(void) {
return 0;
}
static void denoDlClose(sqlite3_vfs *pVfs, void *pHandle) {
return;
}
// Generate pseudo-random data
static int denoRandomness(sqlite3_vfs *pVfs, int nByte, char *zByte) {
pcg_bytes(zByte, nByte);
return SQLITE_OK;
}
// TODO(dyedgreen): Can anything be done here?
static int denoSleep(sqlite3_vfs *pVfs, int nMicro) {
return 0;
}
// Retrieve the current time
static int denoCurrentTime(sqlite3_vfs *pVfs, double *pTime) {
*pTime = js_time() / 1000 / 86400.0 + 2440587.5;
return SQLITE_OK;
}
// Implement localtime_r
struct tm* localtime_r(const time_t *time, struct tm *result) {
debug_printf("running localtime_r");
time_t shifted = *time - 60 * js_timezone();
return gmtime_r(&shifted, result);
}
// This function returns a pointer to the VFS implemented in this file.
sqlite3_vfs *sqlite3_denovfs(void) {
static sqlite3_vfs denovfs = {
3, /* iVersion */
sizeof(DenoFile), /* szOsFile */
MAXPATHNAME, /* mxPathname */
0, /* pNext */
"deno", /* zName */
0, /* pAppData */
denoOpen, /* xOpen */
denoDelete, /* xDelete */
denoAccess, /* xAccess */
denoFullPathname, /* xFullPathname */
denoDlOpen, /* xDlOpen */
denoDlError, /* xDlError */
denoDlSym, /* xDlSym */
denoDlClose, /* xDlClose */
denoRandomness, /* xRandomness */
denoSleep, /* xSleep */
denoCurrentTime, /* xCurrentTime */
0, /* xGetLastError */
0, /* xCurrentTimeInt64 */
0, /* xSetSystemCall */
0, /* xGetSystemCall */
0, /* xNextSystemCall */
};
return &denovfs;
}
int sqlite3_os_init(void) {
debug_printf("running sqlite3_os_init\n");
// Register VFS
return sqlite3_vfs_register(sqlite3_denovfs(), 1);
}
int sqlite3_os_end(void) {
return SQLITE_OK;
}

View File

@ -0,0 +1,247 @@
#include <stdlib.h>
#include <sqlite3.h>
#include <pcg.h>
#include "debug.h"
#define EXPORT(name) __attribute__((used)) __attribute__((export_name (#name))) name
#define ERROR_VAL -1
#define BIG_INT_TYPE 6
#define JS_MAX_SAFE_INTEGER 9007199254740991
#define JS_MIN_SAFE_INTEGER (-JS_MAX_SAFE_INTEGER)
// Status returned by last instruction
int last_status = SQLITE_OK;
// Database handle for this instance
sqlite3* database = NULL;
// Return length of string pointed to by str.
int EXPORT(str_len) (const char* str) {
int len;
for (len = 0; str[len] != '\0'; len ++);
return len;
}
// Seed the random number generator. We pass a double, to
// get as many bytes from the JS number as possible.
void EXPORT(seed_rng) (double seed) {
pcg_seed((uint64_t)seed);
}
// Return last status encountered.
int EXPORT(get_status) () {
return last_status;
}
// Initialize the database and return the status.
int EXPORT(open) (const char* filename, int flags) {
// Return error is database is already open
if (database) {
last_status = SQLITE_MISUSE;
return last_status;
}
// Open SQLite db connection
last_status = sqlite3_open_v2(filename, &database, flags, NULL);
if (last_status != SQLITE_OK) {
debug_printf("failed to open database with status %i\n", last_status);
return last_status;
}
debug_printf("opened database at path '%s'\n", filename);
return last_status;
}
// Attempt to close the database connection.
int EXPORT(close) () {
last_status = sqlite3_close(database);
if (last_status == SQLITE_OK) {
database = NULL;
debug_printf("closed database");
} else {
debug_printf("failed to close database with status %i\n", last_status);
}
return last_status;
}
// Return most recent SQLite error as a string
const char* EXPORT(get_sqlite_error_str) () {
if (!database)
return "No open database.";
return sqlite3_errmsg(database);
}
// Wraps sqlite3_prepare. Returns statement id.
sqlite3_stmt* EXPORT(prepare) (const char* sql) {
// Prepare sqlite statement
sqlite3_stmt* stmt;
last_status = sqlite3_prepare_v2(database, sql, -1, &stmt, NULL);
debug_printf("prepared sql statement (status %i)\n", last_status);
if (last_status != SQLITE_OK)
return NULL;
return stmt;
}
// Destruct the given statement/ transaction. This will destruct the SQLite
// statement and free up it's transaction slot. Regardless of returned
// status, the statement id will be freed up.
int EXPORT(finalize) (sqlite3_stmt* stmt) {
last_status = sqlite3_finalize(stmt);
debug_printf("finalized statement (status %i)\n", last_status);
return last_status;
}
// Reset a given statement so it can be re-used.
int EXPORT(reset) (sqlite3_stmt* stmt) {
last_status = sqlite3_reset(stmt);
debug_printf("reset statement (status %i)\n", last_status);
return last_status;
}
// Resets all bound parameter values for this statement.
int EXPORT(clear_bindings) (sqlite3_stmt* stmt) {
last_status = sqlite3_clear_bindings(stmt);
debug_printf("clear bindings (status %i)\n", last_status);
return last_status;
}
// Execute multiple statements from a single string. This ignores any result
// rows.
int EXPORT(exec) (const char* sql) {
last_status = sqlite3_exec(database, sql, NULL, NULL, NULL);
debug_printf("ran exec (status %i)\n", last_status);
return last_status;
}
// Wrappers for bind statements, these return the status directly
int EXPORT(bind_int) (sqlite3_stmt* stmt, int idx, double value) {
// we use double to pass in the value, as JS does not support 64 bit integers,
// but handles floats and we can contain a 32 bit in in a 64 bit float, so there
// should be no loss.
last_status = sqlite3_bind_int64(stmt, idx, (sqlite3_int64)value);
debug_printf("binding int %lli (status %i)\n", (sqlite3_int64)value, last_status);
return last_status;
}
int EXPORT(bind_double) (sqlite3_stmt* stmt, int idx, double value) {
last_status = sqlite3_bind_double(stmt, idx, value);
debug_printf("binding double %f (status %i)\n", value, last_status);
return last_status;
}
int EXPORT(bind_text) (sqlite3_stmt* stmt, int idx, const char* value) {
// SQLite retrains the string until we execute the statement, but any strings
// passed in from JS are freed when the function returns. Thus we need to mark
// is as transient.
last_status = sqlite3_bind_text(stmt, idx, value, -1, SQLITE_TRANSIENT);
debug_printf("binding text '%s' (status %i)\n", value, last_status);
return last_status;
}
int EXPORT(bind_blob) (sqlite3_stmt* stmt, int idx, void* value, int size) {
// SQLite retrains the pointer until we execute the statement, but any pointers
// passed in from JS are freed when the function returns. Thus we need to mark
// is as transient.
last_status = sqlite3_bind_blob(stmt, idx, value, size, SQLITE_TRANSIENT);
debug_printf("binding blob '%s' (status %i)\n", value, last_status);
return last_status;
}
int EXPORT(bind_big_int) (sqlite3_stmt* stmt, int idx, int sign, uint32_t high, uint32_t low) {
// Bind a big integer within the 64 bit integer range by passing it as two 32
// bit integers. The integers are assumed to be positive, and a sign is passed
// separately.
sqlite3_int64 int_val = ((sqlite3_int64)low + ((sqlite3_int64)high << 32)) * (sqlite3_int64)sign;
debug_printf("binding big_int %lld", int_val);
last_status = sqlite3_bind_int64(stmt, idx, int_val);
return last_status;
}
int EXPORT(bind_null) (sqlite3_stmt* stmt, int idx) {
last_status = sqlite3_bind_null(stmt, idx);
debug_printf("binding null (status %i)\n", last_status);
return last_status;
}
// Determine parameter index for named parameters
int EXPORT(bind_parameter_index) (sqlite3_stmt* stmt, const char* name) {
int index = sqlite3_bind_parameter_index(stmt, name);
if (index == 0) {
debug_printf("parameter '%s' does not exist", name);
// Normalize SQLite returning 0 for not found to ERROR_VAL
return ERROR_VAL;
}
debug_printf("obtained parameter index (param '%s', index %i)\n", name, index);
return index;
}
// Wraps running statements, this returns the status directly
int EXPORT(step) (sqlite3_stmt* stmt) {
last_status = sqlite3_step(stmt);
debug_printf("stepping statement (status %i)\n", last_status);
return last_status;
}
// Count columns returned by statement.
int EXPORT(column_count) (sqlite3_stmt* stmt) {
return sqlite3_column_count(stmt);
}
// Determine type of column. Returns SQLITE column types.
int EXPORT(column_type) (sqlite3_stmt* stmt, int col) {
int type = sqlite3_column_type(stmt, col);
if (type == SQLITE_INTEGER) {
// handle integers that exceed JS_MAX_SAFE_INTEGER
sqlite3_int64 col_val = sqlite3_column_int64(stmt, col);
if (col_val > JS_MAX_SAFE_INTEGER || col_val < JS_MIN_SAFE_INTEGER) {
debug_printf("detected big integer: %lld\n", col_val);
return BIG_INT_TYPE;
}
}
return type;
}
// Wrap result returning functions.
double EXPORT(column_int) (sqlite3_stmt* stmt, int col) {
return (double)sqlite3_column_int64(stmt, col);
}
double EXPORT(column_double) (sqlite3_stmt* stmt, int col) {
return sqlite3_column_double(stmt, col);
}
const char* EXPORT(column_text) (sqlite3_stmt* stmt, int col) {
return (const char*)sqlite3_column_text(stmt, col);
}
const void* EXPORT(column_blob) (sqlite3_stmt* stmt, int col) {
return sqlite3_column_blob(stmt, col);
}
int EXPORT(column_bytes) (sqlite3_stmt* stmt, int col) {
return sqlite3_column_bytes(stmt, col);
}
const char* EXPORT(column_name) (sqlite3_stmt* stmt, int col) {
return sqlite3_column_name(stmt, col);
}
const char* EXPORT(column_origin_name) (sqlite3_stmt* stmt, int col) {
return sqlite3_column_origin_name(stmt, col);
}
const char* EXPORT(column_table_name) (sqlite3_stmt* stmt, int col) {
return sqlite3_column_table_name(stmt, col);
}
double EXPORT(last_insert_rowid) () {
return (double)sqlite3_last_insert_rowid(database);
}
double EXPORT(changes) () {
return (double)sqlite3_changes(database);
}
double EXPORT(total_changes) () {
return (double)sqlite3_total_changes(database);
}

View File

@ -0,0 +1,120 @@
import { getStr } from "../src/wasm.ts";
const isWindows = Deno.build.os === "windows";
// Closure to return an environment that links
// the current wasm context
export default function env(inst) {
// Exported environment
const env = {
// Print a string pointer to console
js_print: (str_ptr) => {
const text = getStr(inst.exports, str_ptr);
console.log(text[text.length - 1] === "\n" ? text.slice(0, -1) : text);
},
// Open the file at path, mode = 0 is open RW, mode = 1 is open TEMP
js_open: (path_ptr, mode, flags) => {
let path;
switch (mode) {
case 0:
path = getStr(inst.exports, path_ptr);
break;
case 1:
path = Deno.makeTempFileSync({ prefix: "deno_sqlite" });
break;
}
const write = !!(flags & 0x00000002);
const create = !!(flags & 0x00000004);
const rid = Deno.openSync(path, { read: true, write, create }).rid;
return rid;
},
// Close a file
js_close: (rid) => {
Deno.close(rid);
},
// Delete file at path
js_delete: (path_ptr) => {
const path = getStr(inst.exports, path_ptr);
Deno.removeSync(path);
},
// Read from a file to a buffer in the module
js_read: (rid, buffer_ptr, offset, amount) => {
const buffer = new Uint8Array(
inst.exports.memory.buffer,
buffer_ptr,
amount,
);
Deno.seekSync(rid, offset, Deno.SeekMode.Start);
return Deno.readSync(rid, buffer);
},
// Write to a file from a buffer in the module
js_write: (rid, buffer_ptr, offset, amount) => {
const buffer = new Uint8Array(
inst.exports.memory.buffer,
buffer_ptr,
amount,
);
Deno.seekSync(rid, offset, Deno.SeekMode.Start);
return Deno.writeSync(rid, buffer);
},
// Truncate the given file
js_truncate: (rid, size) => {
Deno.ftruncateSync(rid, size);
},
// Sync file data to disk
js_sync: (rid) => {
Deno.fdatasyncSync(rid);
},
// Retrieve the size of the given file
js_size: (rid) => {
return Deno.fstatSync(rid).size;
},
// Acquire a SHARED or EXCLUSIVE file lock
js_lock: (rid, exclusive) => {
// this is unstable and has issues on Windows ...
if (Deno.flockSync && !isWindows) Deno.flockSync(rid, exclusive !== 0);
},
// Release a file lock
js_unlock: (rid) => {
// this is unstable and has issues on Windows ...
if (Deno.funlockSync && !isWindows) Deno.funlockSync(rid);
},
// Return current time in ms since UNIX epoch
js_time: () => {
return Date.now();
},
// Return the timezone offset in minutes for
// the current locale.
js_timezone: () => {
return (new Date()).getTimezoneOffset();
},
// Determine if a path exists
js_exists: (path_ptr) => {
const path = getStr(inst.exports, path_ptr);
try {
Deno.statSync(path);
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return 0;
}
}
return 1;
},
// Determine if a path is accessible i.e. if it has read/write permissions
// TODO(dyedgreen): Properly determine if there are read permissions
js_access: (path_ptr) => {
const path = getStr(inst.exports, path_ptr);
try {
Deno.statSync(path);
} catch (e) {
if (e instanceof Deno.errors.PermissionDenied) {
return 0;
}
}
return 1;
},
};
return { env };
}

View File

@ -0,0 +1,15 @@
js_print
js_open
js_close
js_delete
js_read
js_write
js_truncate
js_sync
js_size
js_lock
js_unlock
js_time
js_timezone
js_exists
js_access

View File

@ -0,0 +1,79 @@
/**
* cli.ts
*
* A simple clone of the sqlite3 command line
* interface, build using deno-sqlite.
*
* This is an example, meant to illustrate using
* the API provided by deno-sqlite.
*/
import { readLines, writeAll } from "https://deno.land/std@0.134.0/io/mod.ts";
import AsciiTable from "https://deno.land/x/ascii_table@v0.1.0/mod.ts";
import { DB } from "../mod.ts";
const db = new DB(Deno.args[0] ?? undefined);
async function print(str: string) {
const enc = new TextEncoder();
await writeAll(Deno.stdout, enc.encode(str));
}
async function prompt() {
await print("sqlite> ");
}
const tablesQuery = db.prepareQuery<[string]>(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
);
const commands: Record<string, () => Promise<void>> = {
"tables": async () => {
for (const [name] of tablesQuery.iter()) {
await print(`${name}\n`);
}
},
"quit": async () => {
await print("\n");
Deno.exit(0);
},
"help": async () => {
await print(
"Type an SQL query or run a command.\nThe following commands are available:\n",
);
for (const key in commands) {
await print(`.${key}\n`);
}
},
};
await prompt();
for await (const cmd of readLines(Deno.stdin)) {
if (cmd[0] === ".") {
const action = commands[cmd.slice(1)] ??
(() => print("Unrecognized command, try .help\n"));
await action();
} else {
try {
const query = db.prepareQuery(cmd);
const rows = query.all();
const cols = query.columns();
query.finalize();
if (cols.length) {
const table = new AsciiTable();
table.setHeading("#", ...cols.map(({ name }) => name));
for (const [idx, row] of rows.entries()) {
table.addRow(idx + 1, ...row);
}
print(table.toString());
print("\n");
} else {
print(`Executed query: ${db.changes} changes\n`);
}
} catch (err) {
console.error(err);
}
}
await prompt();
}

View File

@ -0,0 +1,57 @@
/**
* notes.ts
*
* A command line tool to manage a set
* of simple notes.
*
* This is an example, meant to illustrate using
* the API provided by deno-sqlite.
*/
import { DB } from "../mod.ts";
const commands: Record<string, (...args: string[]) => Promise<void> | void> = {
"create": (file: string) => {
const db = new DB(file, { mode: "create" });
db.query(`
CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note TEXT NOT NULL,
created_at TEXT NOT NULL
)
`);
db.close();
console.log("Database created!");
},
"record": (file: string, note: string) => {
const db = new DB(file, { mode: "write" });
db.query("INSERT INTO notes (note, created_at) VALUES (?, ?)", [
note,
new Date(),
]);
db.close();
console.log("Note recorded!");
},
"delete": (file: string, noteId: string) => {
const db = new DB(file, { mode: "write" });
db.query("DELETE FROM notes WHERE id = ?", [noteId]);
db.close();
console.log("Note deleted!");
},
"list": (file: string) => {
const db = new DB(file, { mode: "read" });
const query = db.prepareQuery<[number, string, string]>(
"SELECT id, note, created_at FROM notes ORDER BY created_at DESC",
);
for (const [id, note, createdAt] of query.iter()) {
const date = new Date(createdAt);
console.log(`Note #${id} (recorded ${date.toLocaleString()})\n${note}\n`);
}
query.finalize();
db.close();
},
};
const command = commands[Deno.args[0]] ??
(() => console.error(`Unknown command '${Deno.args[0]}'.`));
await command(...Deno.args.slice(1));

View File

@ -0,0 +1,42 @@
/**
* server.ts
*
* A server which returns the number
* of hits to any given path since
* the server started running.
*
* This is an example, meant to illustrate using
* the API provided by deno-sqlite.
*/
import { serve } from "https://deno.land/std@0.134.0/http/mod.ts";
import { DB } from "../mod.ts";
const db = new DB();
db.query(`
CREATE TABLE visits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
visited_at TEXT NOT NULL
)
`);
const addVisitQuery = db.prepareQuery(
"INSERT INTO visits (url, visited_at) VALUES (:url, :time)",
);
const countVisitsQuery = db.prepareQuery<[number]>(
"SELECT COUNT(*) FROM visits WHERE url = :url",
);
console.log("Running server on localhost:8080");
await serve((req) => {
addVisitQuery.execute({
url: req.url,
time: new Date(),
});
const [count] = countVisitsQuery.one({ url: req.url });
return new Response(`This page was visited ${count} times!`);
}, { port: 8080 });

View File

@ -0,0 +1,16 @@
export { DB } from "./src/db.ts";
export { SqliteError } from "./src/error.ts";
export { Status } from "./src/constants.ts";
export type { SqliteOptions } from "./src/db.ts";
export type {
ColumnName,
PreparedQuery,
QueryParameter,
QueryParameterSet,
Row,
RowObject,
} from "./src/query.ts";
import { compile } from "./build/sqlite.js";
await compile();

View File

@ -0,0 +1,63 @@
/**
* Status codes which can be returned
* by SQLite.
*
* Also see https://www.sqlite.org/rescode.html.
*/
export enum Status {
Unknown = -1, // Unknown status
SqliteOk = 0, // Successful result
SqliteError = 1, // Generic error
SqliteInternal = 2, // Internal logic error in SQLite
SqlitePerm = 3, // Access permission denied
SqliteAbort = 4, // Callback routine requested an abort
SqliteBusy = 5, // The database file is locked
SqliteLocked = 6, // A table in the database is locked
SqliteNoMem = 7, // A malloc() failed
SqliteReadOnly = 8, // Attempt to write a readonly database
SqliteInterrupt = 9, // Operation terminated by sqlite3_interrupt()
SqliteIOErr = 10, // Some kind of disk I/O error occurred
SqliteCorrupt = 11, // The database disk image is malformed
SqliteNotFound = 12, // Unknown opcode in sqlite3_file_control()
SqliteFull = 13, // Insertion failed because database is full
SqliteCantOpen = 14, // Unable to open the database file
SqliteProtocol = 15, // Database lock protocol error
SqliteEmpty = 16, // Internal use only
SqliteSchema = 17, // The database schema changed
SqliteTooBig = 18, // String or BLOB exceeds size limit
SqliteConstraint = 19, // Abort due to constraint violation
SqliteMismatch = 20, // Data type mismatch
SqliteMisuse = 21, // Library used incorrectly
SqliteNoLFS = 22, // Uses OS features not supported on host
SqliteAuth = 23, // Authorization denied
SqliteFormat = 24, // Not used
SqliteRange = 25, // 2nd parameter to sqlite3_bind out of range
SqliteNotADB = 26, // File opened that is not a database file
SqliteNotice = 27, // Notifications from sqlite3_log()
SqliteWarning = 28, // Warnings from sqlite3_log()
SqliteRow = 100, // sqlite3_step() has another row ready
SqliteDone = 101, // sqlite3_step() has finished executing
}
export enum OpenFlags {
ReadOnly = 0x00000001,
ReadWrite = 0x00000002,
Create = 0x00000004,
Uri = 0x00000040,
Memory = 0x00000080,
}
export enum Types {
Integer = 1,
Float = 2,
Text = 3,
Blob = 4,
Null = 5,
BigInteger = 6,
}
export enum Values {
Error = -1,
Null = 0,
}

View File

@ -0,0 +1,462 @@
import {
assertAlmostEquals,
assertEquals,
assertThrows,
} from "https://deno.land/std@0.154.0/testing/asserts.ts";
import { DB } from "../mod.ts";
const TEST_DB = "test.db";
const LARGE_TEST_DB = "build/2GB_test.db";
async function dbPermissions(path: string): Promise<boolean> {
const query = async (name: "read" | "write") =>
(await Deno.permissions.query({ name, path })).state ===
"granted";
return await query("read") && await query("write");
}
const TEST_DB_PERMISSIONS = await dbPermissions(TEST_DB);
const LARGE_TEST_DB_PERMISSIONS = await dbPermissions(LARGE_TEST_DB);
async function deleteDatabase(file: string) {
try {
await Deno.remove(file);
} catch { /* no op */ }
try {
await Deno.remove(`${file}-journal`);
} catch { /* no op */ }
}
Deno.test("execute multiple statements", function () {
const db = new DB();
db.execute(`
CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT);
INSERT INTO test (id) VALUES (1);
INSERT INTO test (id) VALUES (2);
INSERT INTO test (id) VALUES (3);
`);
assertEquals(db.query("SELECT id FROM test"), [[1], [2], [3]]);
// table `test` already exists ...
assertThrows(function () {
db.execute(`
CREATE TABLE test2 (id INTEGER);
CREATE TABLE test (id INTEGER);
`);
});
// ... but table `test2` was created before the error
assertEquals(db.query("SELECT id FROM test2"), []);
// syntax error after first valid statement
assertThrows(() => db.execute("SELECT id FROM test; NOT SQL ANYMORE"));
});
Deno.test("foreign key constraints enabled", function () {
const db = new DB();
db.execute(`
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT);
CREATE TABLE orders (id INTEGER PRIMARY KEY AUTOINCREMENT, user INTEGER, FOREIGN KEY(user) REFERENCES users(id));
`);
db.query("INSERT INTO users (id) VALUES (1)");
const [{ id }] = db.queryEntries<{ id: number }>("SELECT id FROM users");
// user must exist
assertThrows(() =>
db.query("INSERT INTO orders (user) VALUES (?)", [id + 1])
);
db.query("INSERT INTO orders (user) VALUES (?)", [id]);
// can't delete if that violates the constraint ...
assertThrows(() => {
db.query("DELETE FROM users WHERE id = ?", [id]);
});
// ... after deleting the order, deleting is OK
db.query("DELETE FROM orders WHERE user = ?", [id]);
db.query("DELETE FROM users WHERE id = ?", [id]);
});
Deno.test("json functions exist", function () {
const db = new DB();
// The JSON1 functions should exist and we should be able to call them without unexpected errors
db.query(`SELECT json('{"this is": ["json"]}')`);
// We should expect an error if we pass invalid JSON where valid JSON is expected
assertThrows(() => {
db.query(`SELECT json('this is not json')`);
});
// We should be able to use bound values as arguments to the JSON1 functions,
// and they should produce the expected results for these simple expressions.
const [[objectType]] = db.query(`SELECT json_type('{}')`);
assertEquals(objectType, "object");
const [[integerType]] = db.query(`SELECT json_type(?)`, ["2"]);
assertEquals(integerType, "integer");
const [[realType]] = db.query(`SELECT json_type(?)`, ["2.5"]);
assertEquals(realType, "real");
const [[stringType]] = db.query(`SELECT json_type(?)`, [`"hello"`]);
assertEquals(stringType, "text");
const [[integerTypeAtPath]] = db.query(
`SELECT json_type(?, ?)`,
[`["hello", 2, {"world": 4}]`, `$[2].world`],
);
assertEquals(integerTypeAtPath, "integer");
});
Deno.test("date time is correct", function () {
const db = new DB();
// the date/ time is passed from JS and should be current (note that it is GMT)
const [[now]] = [...db.query("SELECT STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')")];
const jsTime = new Date().getTime();
const dbTime = new Date(`${now}Z`).getTime();
// to account for runtime latency, a small difference is ok
const tolerance = 10;
assertAlmostEquals(jsTime, dbTime, tolerance);
db.close();
});
Deno.test("SQL localtime reflects system locale", function () {
const db = new DB();
const [[timeDb]] = db.query("SELECT datetime('now', 'localtime')");
const now = new Date();
const jsMonth = `${now.getMonth() + 1}`.padStart(2, "0");
const jsDate = `${now.getDate()}`.padStart(2, "0");
const jsHour = `${now.getHours()}`.padStart(2, "0");
const jsMinute = `${now.getMinutes()}`.padStart(2, "0");
const jsSecond = `${now.getSeconds()}`.padStart(2, "0");
const timeJs =
`${now.getFullYear()}-${jsMonth}-${jsDate} ${jsHour}:${jsMinute}:${jsSecond}`;
assertEquals(timeDb, timeJs);
});
Deno.test("database has correct changes and totalChanges", function () {
const db = new DB();
db.execute(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
);
for (const name of ["a", "b", "c"]) {
db.query("INSERT INTO test (name) VALUES (?)", [name]);
assertEquals(1, db.changes);
}
assertEquals(3, db.totalChanges);
db.query("UPDATE test SET name = ?", ["new name"]);
assertEquals(3, db.changes);
assertEquals(6, db.totalChanges);
});
Deno.test("last inserted id", function () {
const db = new DB();
// By default, lastInsertRowId must be 0
assertEquals(db.lastInsertRowId, 0);
// Create table and insert value
db.query("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
const insertRowIds = [];
// Insert data to table and collect their ids
for (let i = 0; i < 10; i++) {
db.query("INSERT INTO users (name) VALUES ('John Doe')");
insertRowIds.push(db.lastInsertRowId);
}
// Now, the last inserted row id must be 10
assertEquals(db.lastInsertRowId, 10);
// All collected row ids must be the same as in the database
assertEquals(
insertRowIds,
[...db.query("SELECT id FROM users")].map(([i]) => i),
);
db.close();
// When the database is closed, the value
// will be reset to 0 again
assertEquals(db.lastInsertRowId, 0);
});
Deno.test("close database", function () {
const db = new DB();
db.close();
assertThrows(() => db.query("CREATE TABLE test (name TEXT PRIMARY KEY)"));
db.close(); // check close is idempotent and won't throw
});
Deno.test("open queries block close", function () {
const db = new DB();
db.query("CREATE TABLE test (name TEXT PRIMARY KEY)");
const query = db.prepareQuery("SELECT name FROM test");
assertThrows(() => db.close());
query.finalize();
db.close();
});
Deno.test("open queries cleaned up by forced close", function () {
const db = new DB();
db.query("CREATE TABLE test (name TEXT PRIMARY KEY)");
db.query("INSERT INTO test (name) VALUES (?)", ["Deno"]);
db.prepareQuery("SELECT name FROM test WHERE name like '%test%'");
assertThrows(() => db.close());
db.close(true);
});
Deno.test("invalid bind does not leak statements", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER)");
for (let n = 0; n < 100; n++) {
assertThrows(() => {
// deno-lint-ignore no-explicit-any
const badBinding: any = [{}];
db.query("INSERT INTO test (id) VALUES (?)", badBinding);
});
assertThrows(() => {
const badBinding = { missingKey: null };
db.query("INSERT INTO test (id) VALUES (?)", badBinding);
});
}
db.query("INSERT INTO test (id) VALUES (1)");
db.close();
});
Deno.test("transactions can be nested", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY)");
db.transaction(() => {
db.query("INSERT INTO test (id) VALUES (1)");
try {
db.transaction(() => {
db.query("INSERT INTO test (id) VALUES (2)");
throw new Error("boom!");
});
} catch (_) { /* ignore */ }
});
assertEquals([{ id: 1 }], db.queryEntries("SELECT * FROM test"));
});
Deno.test("transactions commit when closure exists", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY)");
db.transaction(() => {
db.query("INSERT INTO test (id) VALUES (1)");
});
assertThrows(() => db.query("ROLLBACK"));
assertEquals([{ id: 1 }], db.queryEntries("SELECT * FROM test"));
});
Deno.test("transaction rolls back on throw", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY)");
assertThrows(() => {
db.transaction(() => {
db.query("INSERT INTO test (id) VALUES (1)");
throw new Error("boom!");
});
});
assertEquals([], db.query("SELECT * FROM test"));
});
Deno.test(
"persist database to file",
{
ignore: !TEST_DB_PERMISSIONS,
permissions: { read: true, write: true },
sanitizeResources: true,
},
async function () {
const data = [
"Hello World!",
"Hello Deno!",
"JavaScript <3",
"This costs 0€ / $0 / £0",
"Wéll, hällö thėrè¿",
];
// ensure the test database file does not exist
await deleteDatabase(TEST_DB);
const db = new DB(TEST_DB);
db.execute(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, val TEXT)",
);
for (const val of data) {
db.query("INSERT INTO test (val) VALUES (?)", [val]);
}
// open the same database with a separate connection
const readOnlyDb = await new DB(TEST_DB, { mode: "read" });
for (
const [id, val] of readOnlyDb.query<[number, string]>(
"SELECT * FROM test",
)
) {
assertEquals(data[id - 1], val);
}
await Deno.remove(TEST_DB);
db.close();
readOnlyDb.close();
},
);
Deno.test(
"temporary file database read / write",
{
ignore: !TEST_DB_PERMISSIONS,
permissions: { read: true, write: true },
sanitizeResources: true,
},
function () {
const data = [
"Hello World!",
"Hello Deno!",
"JavaScript <3",
"This costs 0€ / $0 / £0",
"Wéll, hällö thėrè¿",
];
const tempDb = new DB("");
tempDb.execute(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, val TEXT)",
);
for (const val of data) {
tempDb.query("INSERT INTO test (val) VALUES (?)", [val]);
}
for (
const [id, val] of tempDb.query<[number, string]>("SELECT * FROM test")
) {
assertEquals(data[id - 1], val);
}
tempDb.close();
},
);
Deno.test(
"database open options",
{
ignore: !TEST_DB_PERMISSIONS,
permissions: { read: true, write: true },
sanitizeResources: true,
},
async function () {
await deleteDatabase(TEST_DB);
// when no file exists, these should error
assertThrows(() => new DB(TEST_DB, { mode: "write" }));
assertThrows(() => new DB(TEST_DB, { mode: "read" }));
// create the database
const dbCreate = new DB(TEST_DB, { mode: "create" });
dbCreate.execute(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)",
);
dbCreate.close();
// the default mode is create
await deleteDatabase(TEST_DB);
const dbCreateDefault = new DB(TEST_DB, { mode: "create" });
dbCreateDefault.execute(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)",
);
dbCreateDefault.close();
// in write mode, we can run INSERT queries ...
const dbWrite = new DB(TEST_DB, { mode: "write" });
dbWrite.query("INSERT INTO test (name) VALUES (?)", ["open-options-test"]);
dbWrite.close();
// ... which we can read in read-only mode ...
const dbRead = new DB(TEST_DB, { mode: "read" });
const rows = [...dbRead.query("SELECT id, name FROM test")];
assertEquals(rows, [[1, "open-options-test"]]);
// ... but we can't write with a read-only connection
assertThrows(() =>
dbRead.query("INTERT INTO test (name) VALUES (?)", ["this-fails"])
);
dbRead.close();
},
);
Deno.test(
"create / write mode require write permissions",
{
ignore: !TEST_DB_PERMISSIONS,
permissions: { read: true, write: false },
sanitizeResources: true,
},
function () {
// opening with these modes requires write permissions ...
assertThrows(() => new DB(TEST_DB, { mode: "create" }));
assertThrows(() => new DB(TEST_DB, { mode: "write" }));
// ... and the default mode is create
assertThrows(() => new DB(TEST_DB));
// however, opening in read-only mode should work (the file was created
// in the previous test)
(new DB(TEST_DB, { mode: "read" })).close();
// with memory flag set, the database will be in memory and
// not require any permissions
(new DB(TEST_DB, { mode: "create", memory: true })).close();
// the mode can also be specified via a URI flag
(new DB(`file:${TEST_DB}?mode=memory`, { uri: true })).close();
},
);
Deno.test(
"database larger than 2GB read / write",
{
ignore: !LARGE_TEST_DB_PERMISSIONS,
permissions: { read: true, write: true },
sanitizeResources: true,
},
function () {
// generated with `cd build && make testdb`
const db = new DB(LARGE_TEST_DB, { mode: "write" });
db.query("INSERT INTO test (value) VALUES (?)", ["This is a test..."]);
const rows = [
...db.query("SELECT value FROM test ORDER BY id DESC LIMIT 10"),
];
assertEquals(rows.length, 10);
assertEquals(rows[0][0], "This is a test...");
db.close();
},
);

View File

@ -0,0 +1,393 @@
import { instantiate, StatementPtr, Wasm } from "../build/sqlite.js";
import { setStr } from "./wasm.ts";
import { OpenFlags, Status, Values } from "./constants.ts";
import { SqliteError } from "./error.ts";
import { PreparedQuery, QueryParameterSet, Row, RowObject } from "./query.ts";
/**
* Options for opening a database.
*/
export interface SqliteOptions {
/**
* Mode in which to open the database.
*
* - `read`: read-only, throws an error if
* the database file does not exists
* - `write`: read-write, throws an error
* if the database file does not exists
* - `create`: read-write, create the database
* if the file does not exist
*
* `create` is the default if no mode is
* specified.
*/
mode?: "read" | "write" | "create";
/**
* Force the database to be in-memory. When
* this option is set, the database is opened
* in memory, regardless of the specified
* filename.
*/
memory?: boolean;
/**
* Interpret the file name as a URI.
* See https://sqlite.org/uri.html
* for more information.
*/
uri?: boolean;
}
/**
* A database handle that can be used to run
* queries.
*/
export class DB {
private _wasm: Wasm;
private _open: boolean;
private _statements: Set<StatementPtr>;
private _transactionDepth: number;
/**
* Create a new database. The file at the
* given path will be opened with the
* mode specified in options. The default
* mode is `create`.
*
* If no path is given, or if the `memory`
* option is set, the database is opened in
* memory.
*
* # Examples
*
* Create an in-memory database.
* ```typescript
* const db = new DB();
* ```
*
* Open a database backed by a file on disk.
* ```typescript
* const db = new DB("path/to/database.sqlite");
* ```
*
* Pass options to open a read-only database.
* ```typescript
* const db = new DB("path/to/database.sqlite", { mode: "read" });
* ```
*/
constructor(path: string = ":memory:", options: SqliteOptions = {}) {
this._wasm = instantiate().exports;
this._open = false;
this._statements = new Set();
this._transactionDepth = 0;
// Configure flags
let flags = 0;
switch (options.mode) {
case "read":
flags = OpenFlags.ReadOnly;
break;
case "write":
flags = OpenFlags.ReadWrite;
break;
case "create": // fall through
default:
flags = OpenFlags.ReadWrite | OpenFlags.Create;
break;
}
if (options.memory === true) {
flags |= OpenFlags.Memory;
}
if (options.uri === true) {
flags |= OpenFlags.Uri;
}
// Try to open the database
const status = setStr(
this._wasm,
path,
(ptr) => this._wasm.open(ptr, flags),
);
if (status !== Status.SqliteOk) {
throw new SqliteError(this._wasm, status);
}
this._open = true;
}
/**
* Query the database and return all matching
* rows.
*
* This is equivalent to calling `all` on
* a prepared query which is then immediately
* finalized.
*
* The type parameter `R` may be supplied by
* the user to indicated the type for the rows returned
* by the query. Notice that the user is responsible
* for ensuring the correctness of the supplied type.
*
* To avoid SQL injection, user-provided values
* should always be passed to the database through
* a query parameter.
*
* See `QueryParameterSet` for documentation on
* how values can be bound to SQL statements.
*
* See `QueryParameter` for documentation on how
* values are returned from the database.
*
* # Examples
*
* ```typescript
* const rows = db.query<[string, number]>("SELECT name, age FROM people WHERE city = ?", [city]);
* // rows = [["Peter Parker", 21], ...]
* ```
*
* ```typescript
* const rows = db.query<[string, number]>(
* "SELECT name, age FROM people WHERE city = :city",
* { city },
* );
* // rows = [["Peter Parker", 21], ...]
* ```
*/
query<R extends Row = Row>(
sql: string,
params?: QueryParameterSet,
): Array<R> {
const query = this.prepareQuery<R>(sql);
try {
const rows = query.all(params);
query.finalize();
return rows;
} catch (err) {
query.finalize();
throw err;
}
}
/**
* Like `query` except each row is returned
* as an object containing key-value pairs.
*
* # Examples
*
* ```typescript
* const rows = db.queryEntries<{ name: string, age: number }>("SELECT name, age FROM people");
* // rows = [{ name: "Peter Parker", age: 21 }, ...]
* ```
*
* ```typescript
* const rows = db.queryEntries<{ name: string, age: number }>(
* "SELECT name, age FROM people WHERE age >= :minAge",
* { minAge },
* );
* // rows = [{ name: "Peter Parker", age: 21 }, ...]
* ```
*/
queryEntries<O extends RowObject = RowObject>(
sql: string,
params?: QueryParameterSet,
): Array<O> {
const query = this.prepareQuery<Row, O>(sql);
try {
const rows = query.allEntries(params);
query.finalize();
return rows;
} catch (err) {
query.finalize();
throw err;
}
}
/**
* Prepares the given SQL query, so that it
* can be run multiple times and potentially
* with different parameters.
*
* If a query will be issued a lot, this is more
* efficient than using `query`. A prepared
* query also provides more control over how
* the query is run, as well as access to meta-data
* about the issued query.
*
* The returned `PreparedQuery` object must be
* finalized by calling its `finalize` method
* once it is no longer needed.
*
* # Typing Queries
*
* Prepared query objects accept three type parameters
* to specify precise types for returned data and
* query parameters.
*
* + The first type parameter `R` indicates the tuple type
* for rows returned by the query.
*
* + The second type parameter `O` indicates the record type
* for rows returned as entries (mappings from column names
* to values).
*
* + The third type parameter `P` indicates the type this query
* accepts as parameters.
*
* Note, that the correctness of those types must
* be guaranteed by the caller of this function.
*
* # Examples
*
* ```typescript
* const query = db.prepareQuery<
* [string, number],
* { name: string, age: number },
* { city: string },
* >("SELECT name, age FROM people WHERE city = :city");
*
* // use query ...
*
* query.finalize();
* ```
*/
prepareQuery<
R extends Row = Row,
O extends RowObject = RowObject,
P extends QueryParameterSet = QueryParameterSet,
>(
sql: string,
): PreparedQuery<R, O, P> {
if (!this._open) {
throw new SqliteError("Database was closed.");
}
const stmt = setStr(
this._wasm,
sql,
(ptr) => this._wasm.prepare(ptr),
);
if (stmt === Values.Null) {
throw new SqliteError(this._wasm);
}
this._statements.add(stmt);
return new PreparedQuery<R, O, P>(this._wasm, stmt, this._statements);
}
/**
* Run multiple semicolon-separated statements from a single
* string.
*
* This method cannot bind any query parameters, and any
* result rows are discarded. It is only for running a chunk
* of raw SQL; for example, to initialize a database.
*
* # Examples
*
* ```typescript
* db.execute(`
* CREATE TABLE people (
* id INTEGER PRIMARY KEY AUTOINCREMENT,
* name TEXT,
* age REAL,
* city TEXT
* );
* INSERT INTO people (name, age, city) VALUES ("Peter Parker", 21, "nyc");
* `);
* ```
*/
execute(sql: string) {
const status = setStr(
this._wasm,
sql,
(ptr) => this._wasm.exec(ptr),
);
if (status !== Status.SqliteOk) {
throw new SqliteError(this._wasm, status);
}
}
/**
* Run a function within the context of a database
* transaction. If the function throws an error,
* the transaction is rolled back. Otherwise, the
* transaction is committed when the function returns.
*
* Calls to `transaction` may be nested. Nested transactions
* behave like SQLite save points.
*/
transaction<V>(closure: () => V): V {
this._transactionDepth += 1;
this.query(`SAVEPOINT _deno_sqlite_sp_${this._transactionDepth}`);
let value;
try {
value = closure();
} catch (err) {
this.query(`ROLLBACK TO _deno_sqlite_sp_${this._transactionDepth}`);
this._transactionDepth -= 1;
throw err;
}
this.query(`RELEASE _deno_sqlite_sp_${this._transactionDepth}`);
this._transactionDepth -= 1;
return value;
}
/**
* Close the database. This must be called if
* the database is no longer used to avoid leaking
* open file descriptors.
*
* If `force = true` is passed, any non-finalized
* `PreparedQuery` objects will be finalized. Otherwise,
* this throws if there are active queries.
*
* `close` may safely be called multiple
* times.
*/
close(force = false) {
if (!this._open) {
return;
}
if (force) {
for (const stmt of this._statements) {
if (this._wasm.finalize(stmt) !== Status.SqliteOk) {
throw new SqliteError(this._wasm);
}
}
}
if (this._wasm.close() !== Status.SqliteOk) {
throw new SqliteError(this._wasm);
}
this._open = false;
}
/**
* Get last inserted row id. This corresponds to
* the SQLite function `sqlite3_last_insert_rowid`.
*
* Before a row is inserted for the first time (since
* the database was opened), this returns `0`.
*/
get lastInsertRowId(): number {
return this._wasm.last_insert_rowid();
}
/**
* Return the number of rows modified, inserted or
* deleted by the most recently completed query.
* This corresponds to the SQLite function
* `sqlite3_changes`.
*/
get changes(): number {
return this._wasm.changes();
}
/**
* Return the number of rows modified, inserted or
* deleted since the database was opened.
* This corresponds to the SQLite function
* `sqlite3_total_changes`.
*/
get totalChanges(): number {
return this._wasm.total_changes();
}
}

View File

@ -0,0 +1,55 @@
import {
assertEquals,
assertInstanceOf,
assertThrows,
} from "https://deno.land/std@0.154.0/testing/asserts.ts";
import { DB, SqliteError, Status } from "../mod.ts";
Deno.test("invalid SQL", function () {
const db = new DB();
const queries = [
"INSERT INTO does_not_exist (balance) VALUES (5)",
"this is not sql",
";;;",
];
for (const query of queries) assertThrows(() => db.query(query));
db.close();
});
Deno.test("constraint error code is correct", function () {
const db = new DB();
db.query("CREATE TABLE test (name TEXT PRIMARY KEY)");
db.query("INSERT INTO test (name) VALUES (?)", ["A"]);
assertThrows(
() => db.query("INSERT INTO test (name) VALUES (?)", ["A"]),
(e: Error) => {
assertInstanceOf(e, SqliteError);
assertEquals(e.code, Status.SqliteConstraint, "Got wrong error code");
assertEquals(
Status[e.codeName],
Status.SqliteConstraint,
"Got wrong error code name",
);
},
);
});
Deno.test("syntax error code is correct", function () {
const db = new DB();
assertThrows(
() => db.query("CREATE TABLEX test (name TEXT PRIMARY KEY)"),
(e: Error) => {
assertInstanceOf(e, SqliteError);
assertEquals(e.code, Status.SqliteError, "Got wrong error code");
assertEquals(
Status[e.codeName],
Status.SqliteError,
"Got wrong error code name",
);
},
);
});

View File

@ -0,0 +1,57 @@
import { Wasm } from "../build/sqlite.js";
import { getStr } from "./wasm.ts";
import { Status } from "./constants.ts";
/**
* Errors which can occur while interacting with
* a database.
*/
export class SqliteError extends Error {
/**
* Extension over the standard JS Error object
* to also contain class members for error code
* and error code name.
*
* Instances of this class should not be constructed
* directly and should only be obtained
* from exceptions raised in this module.
*/
constructor(context: Wasm | string, code?: Status) {
let message;
let status;
if (typeof context === "string") {
message = context;
status = Status.Unknown;
} else {
message = getStr(context, context.get_sqlite_error_str());
status = context.get_status();
}
super(message);
this.code = code ?? status;
this.name = "SqliteError";
}
/**
* The SQLite status code which caused this error.
*
* Errors that originate in the JavaScript part of
* the library will not have an associated status
* code. For these errors, the code will be
* `Status.Unknown`.
*
* These codes are accessible via
* the exported `Status` object.
*/
code: Status;
/**
* Key of code in exported `status`
* object.
*
* E.g. if `code` is `19`,
* `codeName` would be `SqliteConstraint`.
*/
get codeName(): keyof typeof Status {
return Status[this.code] as keyof typeof Status;
}
}

View File

@ -0,0 +1,521 @@
import {
assertEquals,
assertThrows,
} from "https://deno.land/std@0.154.0/testing/asserts.ts";
import { DB, QueryParameter } from "../mod.ts";
function roundTripValues<T extends QueryParameter>(values: T[]): unknown[] {
const db = new DB();
db.execute(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, datum ANY)",
);
for (const value of values) {
db.query("INSERT INTO test (datum) VALUES (?)", [value]);
}
return db
.queryEntries<{ datum: unknown }>("SELECT datum FROM test")
.map(({ datum }) => datum);
}
Deno.test("bind string values", function () {
const values = ["Hello World!", "I love Deno.", "Täst strüng..."];
assertEquals(values, roundTripValues(values));
});
Deno.test("bind integer values", function () {
const values = [42, 1, 2, 3, 4, 3453246, 4536787093, 45536787093];
assertEquals(values, roundTripValues(values));
});
Deno.test("bind float values", function () {
const values = [42.1, 1.235, 2.999, 1 / 3, 4.2345, 345.3246, 4536787.953e-8];
assertEquals(values, roundTripValues(values));
});
Deno.test("bind boolean values", function () {
assertEquals([1, 0], roundTripValues([true, false]));
});
Deno.test("bind date values", function () {
const values = [new Date(), new Date("2018-11-20"), new Date(123456789)];
assertEquals(
values.map((date) => date.toISOString()),
roundTripValues(values),
);
});
Deno.test("bind blob values", function () {
const values = [
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
new Uint8Array([3, 57, 45]),
];
assertEquals(values, roundTripValues(values));
});
Deno.test("blobs are copies", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, val BLOB)",
);
const data = new Uint8Array([1, 2, 3, 4, 5]);
db.query("INSERT INTO test (val) VALUES (?)", [data]);
const [[a]] = db.query<[Uint8Array]>("SELECT val FROM test");
const [[b]] = db.query<[Uint8Array]>("SELECT val FROM test");
assertEquals(data, a);
assertEquals(data, b);
assertEquals(a, b);
a[0] = 100;
assertEquals(a[0], 100);
assertEquals(b[0], 1);
assertEquals(data[0], 1);
data[0] = 5;
const [[c]] = db.query<[Uint8Array]>("SELECT val FROM test");
assertEquals(c[0], 1);
});
Deno.test("bind bigint values", function () {
assertEquals(
[9007199254741991n, 100],
roundTripValues([9007199254741991n, 100n]),
);
});
Deno.test("bind null / undefined", function () {
assertEquals([null, null], roundTripValues([null, undefined]));
});
Deno.test("bind mixed values", function () {
const values = [42, "Hello World!", 0.33333, null];
assertEquals(values, roundTripValues(values));
});
Deno.test("omitting a value binds NULL", function () {
const db = new DB();
db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, datum ANY)");
const insert = db.prepareQuery(
"INSERT INTO test (datum) VALUES (?) RETURNING datum",
);
assertEquals([null], insert.first());
assertEquals([null], insert.first([]));
assertEquals([null], insert.first({}));
// previously bound values are cleared
insert.execute(["this is not null"]);
assertEquals([null], insert.first());
});
Deno.test("prepared query clears bindings before reused", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)");
const query = db.prepareQuery("INSERT INTO test (value) VALUES (?)");
query.execute([1]);
query.execute();
assertEquals([[1], [null]], db.query("SELECT value FROM test"));
query.finalize();
db.close();
});
Deno.test("bind very large floating point numbers", function () {
const db = new DB();
db.query("CREATE TABLE numbers (id INTEGER PRIMARY KEY, number REAL)");
db.query("INSERT INTO numbers (number) VALUES (?)", [+Infinity]);
db.query("INSERT INTO numbers (number) VALUES (?)", [-Infinity]);
db.query("INSERT INTO numbers (number) VALUES (?)", [+20e20]);
db.query("INSERT INTO numbers (number) VALUES (?)", [-20e20]);
const [
[positiveInfinity],
[negativeInfinity],
[positiveTwentyTwenty],
[negativeTwentyTwenty],
] = db.query("SELECT number FROM numbers");
assertEquals(negativeInfinity, -Infinity);
assertEquals(positiveInfinity, +Infinity);
assertEquals(positiveTwentyTwenty, +20e20);
assertEquals(negativeTwentyTwenty, -20e20);
});
Deno.test("big very large integers", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, val INTEGER)",
);
const goodValues = [
0n,
42n,
-42n,
9223372036854775807n,
-9223372036854775808n,
];
const overflowValues = [
9223372036854775807n + 1n,
-9223372036854775808n - 1n,
2352359223372036854775807n,
-32453249223372036854775807n,
];
const query = db.prepareQuery("INSERT INTO test (val) VALUES (?)");
for (const val of goodValues) {
query.execute([val]);
}
const dbValues = db.query<[number | bigint]>(
"SELECT val FROM test ORDER BY id",
).map((
[id],
) => BigInt(id));
assertEquals(goodValues, dbValues);
for (const bigVal of overflowValues) {
assertThrows(() => {
query.execute([bigVal]);
});
}
query.finalize();
db.close();
});
Deno.test("bind named parameters", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, val TEXT)",
);
// :name
db.query("INSERT INTO test (val) VALUES (:val)", { val: "value" });
db.query(
"INSERT INTO test (val) VALUES (:otherVal)",
{ otherVal: "value other" },
);
db.query(
"INSERT INTO test (val) VALUES (:explicitColon)",
{ ":explicitColon": "value explicit" },
);
// @name
db.query(
"INSERT INTO test (val) VALUES (@someName)",
{ "@someName": "@value" },
);
// $name
db.query(
"INSERT INTO test (val) VALUES ($var::Name)",
{ "$var::Name": "$value" },
);
// explicit positional syntax
db.query("INSERT INTO test (id, val) VALUES (?2, ?1)", ["this-is-it", 1000]);
// names must exist
assertThrows(() => {
db.query(
"INSERT INTO test (val) VALUES (:val)",
{ Val: "miss-spelled name" },
);
});
// make sure the data came through correctly
const vals = [...db.query("SELECT val FROM test ORDER BY id ASC")]
.map(([datum]) => datum);
assertEquals(
vals,
[
"value",
"value other",
"value explicit",
"@value",
"$value",
"this-is-it",
],
);
});
Deno.test("iterate from prepared query", function () {
const db = new DB();
db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
db.execute("INSERT INTO test (id) VALUES (1), (2), (3)");
const res = [];
const query = db.prepareQuery<[number]>("SELECT id FROM test");
for (const [id] of query.iter()) {
res.push(id);
}
assertEquals(res, [1, 2, 3]);
query.finalize();
db.close();
});
Deno.test("query all from prepared query", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
const query = db.prepareQuery("SELECT id FROM test");
assertEquals(query.all(), []);
db.query("INSERT INTO test (id) VALUES (1), (2), (3)");
assertEquals(query.all(), [[1], [2], [3]]);
query.finalize();
db.close();
});
Deno.test("query first from prepared query", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
db.query("INSERT INTO test (id) VALUES (1), (2), (3)");
const querySingle = db.prepareQuery("SELECT id FROM test WHERE id = ?");
assertEquals(querySingle.first([42]), undefined);
assertEquals(querySingle.first([2]), [2]);
const queryAll = db.prepareQuery("SELECT id FROM test ORDER BY id ASC");
assertEquals(queryAll.first(), [1]);
querySingle.finalize();
queryAll.finalize();
db.close();
});
Deno.test("query one from prepared query", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
db.query("INSERT INTO test (id) VALUES (1), (2), (3)");
const queryOne = db.prepareQuery<[number]>(
"SELECT id FROM test WHERE id = ?",
);
assertThrows(() => queryOne.one([42]));
assertEquals(queryOne.one([2]), [2]);
const queryAll = db.prepareQuery("SELECT id FROM test");
assertThrows(() => queryAll.one());
queryOne.finalize();
queryAll.finalize();
db.close();
});
Deno.test("execute from prepared query", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
const insert = db.prepareQuery("INSERT INTO test (id) VALUES (:id)");
for (const id of [1, 2, 3]) {
insert.execute({ id });
}
insert.finalize();
assertEquals(db.query("SELECT id FROM test"), [[1], [2], [3]]);
db.close();
});
Deno.test("empty query returns empty array", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY)");
assertEquals([], db.query("SELECT * FROM test"));
db.close();
});
Deno.test("query entries returns correct object shapes", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, height REAL)",
);
const rowsOrig = [
{ id: 1, name: "Peter Parker", height: 1.5 },
{ id: 2, name: "Clark Kent", height: 1.9 },
{ id: 3, name: "Robert Paar", height: 2.1 },
];
const insertQuery = db.prepareQuery(
"INSERT INTO test (id, name, height) VALUES (:id, :name, :height)",
);
for (const row of rowsOrig) {
insertQuery.execute(row);
}
insertQuery.finalize();
const query = db.prepareQuery("SELECT * FROM test");
assertEquals(rowsOrig, [...query.iterEntries()]);
assertEquals(rowsOrig, query.allEntries());
assertEquals(rowsOrig[0], query.firstEntry());
assertEquals(rowsOrig, db.queryEntries("SELECT * FROM test"));
query.finalize();
db.close();
});
Deno.test("prepared query can be reused", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY)");
const query = db.prepareQuery("INSERT INTO test (id) VALUES (?)");
query.execute([1]);
query.execute([2]);
query.execute([3]);
assertEquals([[1], [2], [3]], db.query("SELECT id FROM test"));
query.finalize();
db.close();
});
Deno.test("get columns from select query", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
);
const query = db.prepareQuery("SELECT id, name from test");
assertEquals(query.columns(), [
{ name: "id", originName: "id", tableName: "test" },
{ name: "name", originName: "name", tableName: "test" },
]);
});
Deno.test("get columns from returning query", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
);
const query = db.prepareQuery(
"INSERT INTO test (name) VALUES (?) RETURNING *",
);
assertEquals(query.columns(), [
{ name: "id", originName: "id", tableName: "test" },
{ name: "name", originName: "name", tableName: "test" },
]);
assertEquals(query.all(["name"]), [[1, "name"]]);
});
Deno.test("get columns with renamed column", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)",
);
db.query("INSERT INTO test (name) VALUES (?)", ["name"]);
const query = db.prepareQuery(
"SELECT id AS test_id, name AS test_name from test",
);
const columns = query.columns();
assertEquals(columns, [
{ name: "test_id", originName: "id", tableName: "test" },
{ name: "test_name", originName: "name", tableName: "test" },
]);
});
Deno.test("columns can be obtained from empty prepared query", function () {
const db = new DB();
db.query(
"CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEST, age INTEGER)",
);
db.query("INSERT INTO test (name, age) VALUES (?, ?)", ["Peter Parker", 21]);
const query = db.prepareQuery("SELECT * FROM test");
const columnsFromPreparedQuery = query.columns();
query.finalize();
const queryEmpty = db.prepareQuery("SELECT * FROM test WHERE 1 = 0");
const columnsFromPreparedQueryWithEmptyQuery = queryEmpty.columns();
assertEquals(queryEmpty.all(), []);
query.finalize();
assertEquals(
[{ name: "id", originName: "id", tableName: "test" }, {
name: "name",
originName: "name",
tableName: "test",
}, { name: "age", originName: "age", tableName: "test" }],
columnsFromPreparedQuery,
);
assertEquals(
columnsFromPreparedQueryWithEmptyQuery,
columnsFromPreparedQuery,
);
});
Deno.test("invalid number of bound parameters throws", function () {
const db = new DB();
db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)");
// too many
assertThrows(() => {
db.query("SELECT * FROM test", [null]);
});
assertThrows(() => {
db.query("SELECT * FROM test LIMIT ?", [5, "extra"]);
});
// too few
assertThrows(() => db.query("SELECT * FROM test LIMIT ?", []));
assertThrows(() => {
db.query(
"SELECT * FROM test WHERE id >= ? AND id <= ? LIMIT ?",
[42],
);
});
});
Deno.test("using finalized prepared query throws", function () {
const db = new DB();
db.query("CREATE TABLE test (name TEXT)");
const query = db.prepareQuery("INSERT INTO test (name) VALUES (?)");
query.finalize();
assertThrows(() => query.execute(["test"]));
db.close();
});
Deno.test("invalid binding throws", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER)");
assertThrows(() => {
// deno-lint-ignore no-explicit-any
const badBinding: any = [{}];
db.query("SELECT * FORM test WHERE id = ?", badBinding);
});
db.close();
});
Deno.test("get columns from finalized query throws", function () {
const db = new DB();
db.query("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)");
const query = db.prepareQuery("SELECT id from test");
query.finalize();
// after iteration is done
assertThrows(() => {
query.columns();
});
});

View File

@ -0,0 +1,692 @@
import { StatementPtr, Wasm } from "../build/sqlite.js";
import { getStr, setArr, setStr } from "./wasm.ts";
import { Status, Types, Values } from "./constants.ts";
import { SqliteError } from "./error.ts";
/**
* The default type for returned rows.
*/
export type Row = Array<unknown>;
/**
* The default type for row returned
* as objects.
*/
export type RowObject = Record<string, unknown>;
/**
* Possible parameter values to be bound to a query.
*
* When values are bound to a query, they are
* converted between JavaScript and SQLite types
* in the following way:
*
* | JS type in | SQL type | JS type out |
* |------------|-----------------|------------------|
* | number | INTEGER or REAL | number or bigint |
* | bigint | INTEGER | number or bigint |
* | boolean | INTEGER | number |
* | string | TEXT | string |
* | Date | TEXT | string |
* | Uint8Array | BLOB | Uint8Array |
* | null | NULL | null |
* | undefined | NULL | null |
*
* If no value is provided for a given parameter,
* SQLite will default to NULL.
*
* If a `bigint` is bound, it is converted to a
* signed 64 bit integer, which may overflow.
*
* If an integer value is read from the database, which
* is too big to safely be contained in a `number`, it
* is automatically returned as a `bigint`.
*
* If a `Date` is bound, it will be converted to
* an ISO 8601 string: `YYYY-MM-DDTHH:MM:SS.SSSZ`.
* This format is understood by built-in SQLite
* date-time functions. Also see https://sqlite.org/lang_datefunc.html.
*/
export type QueryParameter =
| boolean
| number
| bigint
| string
| null
| undefined
| Date
| Uint8Array;
/**
* A set of query parameters.
*
* When a query is constructed, it can contain
* either positional or named parameters. For
* more information see https://www.sqlite.org/lang_expr.html#parameters.
*
* A set of parameters can be passed to
* a query method either as an array of
* parameters (in positional order), or
* as an object which maps parameter names
* to their values:
*
* | SQL Parameter | QueryParameterSet |
* |---------------|-------------------------|
* | `?NNN` or `?` | NNN-th value in array |
* | `:AAAA` | value `AAAA` or `:AAAA` |
* | `@AAAA` | value `@AAAA` |
* | `$AAAA` | value `$AAAA` |
*
* See `QueryParameter` for documentation on
* how values are converted between SQL
* and JavaScript types.
*/
export type QueryParameterSet =
| Record<string, QueryParameter>
| Array<QueryParameter>;
/**
* Name of a column in a database query.
*/
export interface ColumnName {
/**
* Name of the returned column.
*/
name: string;
/**
* Name of the database column that stores
* the data returned from this query.
*
* This might be different from `name` if a
* columns was renamed using e.g. as in
* `SELECT foo AS bar FROM table`.
*/
originName: string;
/**
* Name of the table that stores the data
* returned from this query.
*/
tableName: string;
}
interface RowsIterator<R> {
next: () => IteratorResult<R>;
[Symbol.iterator]: () => RowsIterator<R>;
}
/**
* A prepared query which can be executed many
* times.
*/
export class PreparedQuery<
R extends Row = Row,
O extends RowObject = RowObject,
P extends QueryParameterSet = QueryParameterSet,
> {
private _wasm: Wasm;
private _stmt: StatementPtr;
private _openStatements: Set<StatementPtr>;
private _status: number;
private _iterKv: boolean;
private _rowKeys?: Array<string>;
private _finalized: boolean;
/**
* This constructor should never be used directly.
* Instead a prepared query can be obtained by
* calling `DB.prepareQuery`.
*/
constructor(
wasm: Wasm,
stmt: StatementPtr,
openStatements: Set<StatementPtr>,
) {
this._wasm = wasm;
this._stmt = stmt;
this._openStatements = openStatements;
this._status = Status.Unknown;
this._iterKv = false;
this._finalized = false;
}
private startQuery(params?: P) {
if (this._finalized) {
throw new SqliteError("Query is finalized.");
}
// Reset query
this._wasm.reset(this._stmt);
this._wasm.clear_bindings(this._stmt);
// Prepare parameter array
let parameters = [];
if (Array.isArray(params)) {
parameters = params;
} else if (typeof params === "object") {
// Resolve parameter index for named parameter
for (const key of Object.keys(params)) {
let name = key;
// blank names default to ':'
if (name[0] !== ":" && name[0] !== "@" && name[0] !== "$") {
name = `:${name}`;
}
const idx = setStr(
this._wasm,
name,
(ptr) => this._wasm.bind_parameter_index(this._stmt, ptr),
);
if (idx === Values.Error) {
throw new SqliteError(`No parameter named '${name}'.`);
}
parameters[idx - 1] = params[key];
}
}
// Bind parameters
for (let i = 0; i < parameters.length; i++) {
let value = parameters[i];
let status;
switch (typeof value) {
case "boolean":
value = value ? 1 : 0;
// fall through
case "number":
if (Number.isSafeInteger(value)) {
status = this._wasm.bind_int(this._stmt, i + 1, value);
} else {
status = this._wasm.bind_double(this._stmt, i + 1, value);
}
break;
case "bigint":
// bigint is bound as two 32bit integers and reassembled on the C side
if (value > 9223372036854775807n || value < -9223372036854775808n) {
throw new SqliteError(
`BigInt value ${value} overflows 64 bit integer.`,
);
} else {
const posVal = value >= 0n ? value : -value;
const sign = value >= 0n ? 1 : -1;
const upper = Number(BigInt.asUintN(32, posVal >> 32n));
const lower = Number(BigInt.asUintN(32, posVal));
status = this._wasm.bind_big_int(
this._stmt,
i + 1,
sign,
upper,
lower,
);
}
break;
case "string":
status = setStr(
this._wasm,
value,
(ptr) => this._wasm.bind_text(this._stmt, i + 1, ptr),
);
break;
default:
if (value instanceof Date) {
// Dates are allowed and bound to TEXT, formatted `YYYY-MM-DDTHH:MM:SS.SSSZ`
status = setStr(
this._wasm,
value.toISOString(),
(ptr) => this._wasm.bind_text(this._stmt, i + 1, ptr),
);
} else if (value instanceof Uint8Array) {
// Uint8Arrays are allowed and bound to BLOB
const size = value.length;
status = setArr(
this._wasm,
value,
(ptr) => this._wasm.bind_blob(this._stmt, i + 1, ptr, size),
);
} else if (value === null || value === undefined) {
// Both null and undefined result in a NULL entry
status = this._wasm.bind_null(this._stmt, i + 1);
} else {
throw new SqliteError(`Can not bind ${typeof value}.`);
}
break;
}
if (status !== Status.SqliteOk) {
throw new SqliteError(this._wasm, status);
}
}
}
private getQueryRow(): R {
if (this._finalized) {
throw new SqliteError("Query is finalized.");
}
const columnCount = this._wasm.column_count(this._stmt);
const row: Row = [];
for (let i = 0; i < columnCount; i++) {
switch (this._wasm.column_type(this._stmt, i)) {
case Types.Integer:
row.push(this._wasm.column_int(this._stmt, i));
break;
case Types.Float:
row.push(this._wasm.column_double(this._stmt, i));
break;
case Types.Text:
row.push(
getStr(
this._wasm,
this._wasm.column_text(this._stmt, i),
),
);
break;
case Types.Blob: {
const ptr = this._wasm.column_blob(this._stmt, i);
if (ptr === 0) {
// Zero pointer results in null
row.push(null);
} else {
const length = this._wasm.column_bytes(this._stmt, i);
// Slice should copy the bytes, as it makes a shallow copy
row.push(
new Uint8Array(this._wasm.memory.buffer, ptr, length).slice(),
);
}
break;
}
case Types.BigInteger: {
const ptr = this._wasm.column_text(this._stmt, i);
row.push(BigInt(getStr(this._wasm, ptr)));
break;
}
default:
// TODO(dyedgreen): Differentiate between NULL and not-recognized?
row.push(null);
break;
}
}
return row as R;
}
private makeRowObject(row: Row): O {
if (this._rowKeys == null) {
const rowCount = this._wasm.column_count(this._stmt);
this._rowKeys = [];
for (let i = 0; i < rowCount; i++) {
this._rowKeys.push(
getStr(this._wasm, this._wasm.column_name(this._stmt, i)),
);
}
}
const obj = row.reduce<RowObject>((obj, val, idx) => {
obj[this._rowKeys![idx]] = val;
return obj;
}, {});
return obj as O;
}
/**
* Binds the given parameters to the query
* and returns an iterator over rows.
*
* Using an iterator avoids loading all returned
* rows into memory and hence allows to process a large
* number of rows.
*
* Calling `iter`, `all`, or `first` invalidates any iterators
* previously returned from this prepared query.
*
* # Examples
*
* ```typescript
* const query = db.prepareQuery<[number, string]>("SELECT id, name FROM people");
* for (const [id, name] of query.iter()) {
* // ...
* }
* ```
*
* To avoid SQL injection, user-provided values
* should always be passed to the database through
* a query parameter.
*
* ```typescript
* const query = db.prepareQuery("SELECT id FROM people WHERE name = ?");
* preparedQuery.iter([name]);
* ```
*
* ```typescript
* const query = db.prepareQuery("SELECT id FROM people WHERE name = :name");
* preparedQuery.iter({ name });
* ```
*
* See `QueryParameterSet` for documentation on
* how values can be bound to SQL statements.
*
* See `QueryParameter` for documentation on how
* values are returned from the database.
*/
iter(params?: P): RowsIterator<R> {
this.startQuery(params);
this._status = this._wasm.step(this._stmt);
if (
this._status !== Status.SqliteRow && this._status !== Status.SqliteDone
) {
throw new SqliteError(this._wasm, this._status);
}
this._iterKv = false;
return this as RowsIterator<R>;
}
/**
* Like `iter` except each row is returned
* as an object containing key-value pairs.
*
* # Examples
*
* ```typescript
* const query = db.prepareQuery<_, { id: number, name: string }>("SELECT id, name FROM people");
* for (const { id, name } of query.iter()) {
* // ...
* }
* ```
*/
iterEntries(params?: P): RowsIterator<O> {
this.iter(params);
this._iterKv = true;
return this as RowsIterator<O>;
}
/**
* @ignore
*
* Implements the iterable protocol. It is
* a bug to call this method directly.
*/
[Symbol.iterator](): RowsIterator<R | O> {
return this;
}
/**
* @ignore
*
* Implements the iterator protocol. It is
* a bug to call this method directly.
*/
next(): IteratorResult<R | O> {
if (this._status === Status.SqliteRow) {
const value = this.getQueryRow();
this._status = this._wasm.step(this._stmt);
if (this._iterKv) {
return { value: this.makeRowObject(value), done: false };
} else {
return { value, done: false };
}
} else if (this._status === Status.SqliteDone) {
return { value: null, done: true };
} else {
throw new SqliteError(this._wasm, this._status);
}
}
/**
* Binds the given parameters to the query
* and returns an array containing all resulting
* rows.
*
* # Examples
*
* ```typescript
* const query = db.prepareQuery<[number, string]>("SELECT id, name FROM people");
* const rows = query.all();
* // [[1, "Peter"], ...]
* ```
*
* To avoid SQL injection, user-provided values
* should always be passed to the database through
* a query parameter.
*
* ```typescript
* const query = db.prepareQuery("SELECT id FROM people WHERE name = ?");
* preparedQuery.all([name]);
* ```
*
* ```typescript
* const query = db.prepareQuery("SELECT id FROM people WHERE name = :name");
* preparedQuery.all({ name });
* ```
*
* See `QueryParameterSet` for documentation on
* how values can be bound to SQL statements.
*
* See `QueryParameter` for documentation on how
* values are returned from the database.
*/
all(params?: P): Array<R> {
this.startQuery(params);
const rows: Array<R> = [];
this._status = this._wasm.step(this._stmt);
while (this._status === Status.SqliteRow) {
rows.push(this.getQueryRow());
this._status = this._wasm.step(this._stmt);
}
if (this._status !== Status.SqliteDone) {
throw new SqliteError(this._wasm, this._status);
}
return rows;
}
/**
* Like `all` except each row is returned
* as an object containing key-value pairs.
*
* # Examples
*
* ```typescript
* const query = db.prepareQuery<_, { id: number, name: string }>("SELECT id, name FROM people");
* const rows = query.all();
* // [{ id: 1, name: "Peter" }, ...]
* ```
*/
allEntries(params?: P): Array<O> {
return this.all(params).map((row) => this.makeRowObject(row));
}
/**
* Binds the given parameters to the query
* and returns the first resulting row or
* `undefined` when there are no rows returned
* by the query.
*
* # Examples
*
* ```typescript
* const query = db.prepareQuery<[number, string]>("SELECT id, name FROM people");
* const person = query.first();
* // [1, "Peter"]
* ```
*
* ```typescript
* const query = db.prepareQuery("SELECT id, name FROM people WHERE name = ?");
* const person = query.first(["not a name"]);
* // undefined
* ```
*
* To avoid SQL injection, user-provided values
* should always be passed to the database through
* a query parameter.
*
* ```typescript
* const query = db.prepareQuery("SELECT id FROM people WHERE name = ?");
* preparedQuery.first([name]);
* ```
*
* ```typescript
* const query = db.prepareQuery("SELECT id FROM people WHERE name = :name");
* preparedQuery.first({ name });
* ```
*
* See `QueryParameterSet` for documentation on
* how values can be bound to SQL statements.
*
* See `QueryParameter` for documentation on how
* values are returned from the database.
*/
first(params?: P): R | undefined {
this.startQuery(params);
this._status = this._wasm.step(this._stmt);
let row = undefined;
if (this._status === Status.SqliteRow) {
row = this.getQueryRow();
}
while (this._status === Status.SqliteRow) {
this._status = this._wasm.step(this._stmt);
}
if (this._status !== Status.SqliteDone) {
throw new SqliteError(this._wasm, this._status);
}
return row;
}
/**
* Like `first` except the row is returned
* as an object containing key-value pairs.
*
* # Examples
*
* ```typescript
* const query = db.prepareQuery<_, { id: number, name: string }>("SELECT id, name FROM people");
* const person = query.first();
* // { id: 1, name: "Peter" }
* ```
*/
firstEntry(params?: P): O | undefined {
const row = this.first(params);
return row === undefined ? undefined : this.makeRowObject(row);
}
/**
* **Deprecated:** prefer `first`.
*/
one(params?: P): R {
const rows = this.all(params);
if (rows.length === 0) {
throw new SqliteError("The query did not return any rows.");
} else if (rows.length > 1) {
throw new SqliteError("The query returned more than one row.");
} else {
return rows[0];
}
}
/**
* **Deprecated:** prefer `firstEntry`.
*/
oneEntry(params?: P): O {
return this.makeRowObject(this.one(params));
}
/**
* Binds the given parameters to the query and
* executes the query, ignoring any rows which
* might be returned.
*
* Using this method is more efficient when the
* rows returned by a query are not needed or
* the query does not return any rows.
*
* # Examples
*
* ```typescript
* const query = db.prepareQuery<_, _, [string]>("INSERT INTO people (name) VALUES (?)");
* query.execute(["Peter"]);
* ```
*
* ```typescript
* const query = db.prepareQuery<_, _, { name: string }>("INSERT INTO people (name) VALUES (:name)");
* query.execute({ name: "Peter" });
* ```
*
* See `QueryParameterSet` for documentation on
* how values can be bound to SQL statements.
*
* See `QueryParameter` for documentation on how
* values are returned from the database.
*/
execute(params?: P) {
this.startQuery(params);
this._status = this._wasm.step(this._stmt);
while (this._status === Status.SqliteRow) {
this._status = this._wasm.step(this._stmt);
}
if (this._status !== Status.SqliteDone) {
throw new SqliteError(this._wasm, this._status);
}
}
/**
* Closes the prepared query. This must be
* called once the query is no longer needed
* to avoid leaking resources.
*
* After a prepared query has been finalized,
* calls to `iter`, `all`, `first`, `execute`,
* or `columns` will fail.
*
* Using iterators which were previously returned
* from the finalized query will fail.
*
* `finalize` may safely be called multiple
* times.
*/
finalize() {
if (!this._finalized) {
this._wasm.finalize(this._stmt);
this._openStatements.delete(this._stmt);
this._finalized = true;
}
}
/**
* Returns the column names for the query
* results.
*
* This method returns an array of objects,
* where each object has the following properties:
*
* | Property | Value |
* |--------------|--------------------------------------------|
* | `name` | the result of `sqlite3_column_name` |
* | `originName` | the result of `sqlite3_column_origin_name` |
* | `tableName` | the result of `sqlite3_column_table_name` |
*/
columns(): Array<ColumnName> {
if (this._finalized) {
throw new SqliteError(
"Unable to retrieve column names from finalized transaction.",
);
}
const columnCount = this._wasm.column_count(this._stmt);
const columns: Array<ColumnName> = [];
for (let i = 0; i < columnCount; i++) {
const name = getStr(
this._wasm,
this._wasm.column_name(this._stmt, i),
);
const originName = getStr(
this._wasm,
this._wasm.column_origin_name(this._stmt, i),
);
const tableName = getStr(
this._wasm,
this._wasm.column_table_name(this._stmt, i),
);
columns.push({ name, originName, tableName });
}
return columns;
}
}

View File

@ -0,0 +1,80 @@
import {
assertEquals,
assertMatch,
} from "https://deno.land/std@0.154.0/testing/asserts.ts";
import { DB } from "../mod.ts";
Deno.test("README example", function () {
const db = new DB(/* in memory */);
db.execute(`
CREATE TABLE IF NOT EXISTS people (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT
)
`);
const name =
["Peter Parker", "Clark Kent", "Bruce Wane"][Math.floor(Math.random() * 3)];
// Run a simple query
db.query("INSERT INTO people (name) VALUES (?)", [name]);
// Print out data in table
for (const [_name] of db.query("SELECT name FROM people")) continue; // no console.log ;)
db.close();
});
Deno.test("old README example", function () {
const db = new DB();
const first = ["Bruce", "Clark", "Peter"];
const last = ["Wane", "Kent", "Parker"];
db.query(
"CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT, subscribed INTEGER)",
);
for (let i = 0; i < 100; i++) {
const name = `${first[Math.floor(Math.random() * first.length)]} ${
last[
Math.floor(
Math.random() * last.length,
)
]
}`;
const email = `${name.replace(" ", "-")}@deno.land`;
const subscribed = Math.random() > 0.5 ? true : false;
db.query("INSERT INTO users (name, email, subscribed) VALUES (?, ?, ?)", [
name,
email,
subscribed,
]);
}
for (
const [
name,
email,
] of db.query<[string, string]>(
"SELECT name, email FROM users WHERE subscribed = ? LIMIT 100",
[true],
)
) {
assertMatch(name, /(Bruce|Clark|Peter) (Wane|Kent|Parker)/);
assertEquals(email, `${name.replace(" ", "-")}@deno.land`);
}
const res = db.query("SELECT email FROM users WHERE name LIKE ?", [
"Robert Parr",
]);
assertEquals(res, []);
const subscribers = db.query(
"SELECT name, email FROM users WHERE subscribed = ?",
[true],
);
for (const [_name, _email] of subscribers) {
if (Math.random() > 0.5) continue;
break;
}
});

View File

@ -0,0 +1,40 @@
import {
assertEquals,
assertThrows,
} from "https://deno.land/std@0.154.0/testing/asserts.ts";
import { Wasm } from "../build/sqlite.js";
import * as wasm from "./wasm.ts";
function mock(
malloc: () => number = () => 1,
free: (pts: number) => void = () => {},
): Wasm {
const memory = new Uint8Array(2048);
return {
malloc,
free,
str_len: (ptr: number) => {
let len = 0;
for (let idx = ptr; memory.at(idx) != 0; idx++) len++;
return len;
},
memory,
} as unknown as Wasm;
}
Deno.test("round trip string", function () {
const mockWasm = mock();
const testCases = ["Hello world!", "Söme, fünky lëttêrß", "你好👋"];
for (const input of testCases) {
const output = wasm.setStr(mockWasm, input, (ptr) => {
return wasm.getStr(mockWasm, ptr);
});
assertEquals(input, output);
}
});
Deno.test("throws on allocation error", function () {
const mockWasm = mock(() => 0);
assertThrows(() => wasm.setStr(mockWasm, "Hello world!", (_) => null));
});

View File

@ -0,0 +1,87 @@
import { Wasm } from "../build/sqlite.js";
import { SqliteError } from "./error.ts";
// Move string to C
export function setStr<T>(
wasm: Wasm,
str: string,
closure: (ptr: number) => T,
): T {
const bytes = new TextEncoder().encode(str);
const ptr = wasm.malloc(bytes.length + 1);
if (ptr === 0) {
throw new SqliteError("Out of memory.");
}
const mem = new Uint8Array(wasm.memory.buffer, ptr, bytes.length + 1);
mem.set(bytes);
mem[bytes.length] = 0; // \0 terminator
try {
const result = closure(ptr);
wasm.free(ptr);
return result;
} catch (error) {
wasm.free(ptr);
throw error;
}
}
// Move Uint8Array to C
export function setArr<T>(
wasm: Wasm,
arr: Uint8Array,
closure: (ptr: number) => T,
): T {
const ptr = wasm.malloc(arr.length);
if (ptr === 0) {
throw new SqliteError("Out of memory.");
}
const mem = new Uint8Array(wasm.memory.buffer, ptr, arr.length);
mem.set(arr);
try {
const result = closure(ptr);
wasm.free(ptr);
return result;
} catch (error) {
wasm.free(ptr);
throw error;
}
}
// Read string from C
export function getStr(wasm: Wasm, ptr: number): string {
const len = wasm.str_len(ptr);
const bytes = new Uint8Array(wasm.memory.buffer, ptr, len);
if (len > 16) {
return new TextDecoder().decode(bytes);
} else {
// This optimization is lifted from EMSCRIPTEN's glue code
let str = "";
let idx = 0;
while (idx < len) {
let u0 = bytes[idx++];
if (!(u0 & 0x80)) {
str += String.fromCharCode(u0);
continue;
}
const u1 = bytes[idx++] & 63;
if ((u0 & 0xE0) == 0xC0) {
str += String.fromCharCode(((u0 & 31) << 6) | u1);
continue;
}
const u2 = bytes[idx++] & 63;
if ((u0 & 0xF0) == 0xE0) {
u0 = ((u0 & 15) << 12) | (u1 << 6) | u2;
} else {
// cut warning
u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (bytes[idx++] & 63);
}
if (u0 < 0x10000) {
str += String.fromCharCode(u0);
} else {
const ch = u0 - 0x10000;
str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF));
}
}
return str;
}
}