WebAssembly SQLite with experimental support for browser storage extensions

Overview

wa-sqlite

This is a WebAssembly build of SQLite with experimental support for writing SQLite virtual filesystems and virtual table modules completely in Javascript. This allows alternative browser storage options such as IndexedDB.

An IndexedDB virtual filesystem and a virtual table module that accesses Javascript arrays are provided as proof of concept.

Try the demo with a modern desktop web browser.

Build

The primary motivation for this project is to enable additions to SQLite with only Javascript. Most developers should be able to use the "buildless" branch with pre-built artifacts checked in. This is also recommended when including the project as a dependency (e.g. yarn add "wa-sqlite@rhashimoto/wa-sqlite#buildless").

If you do want to build - e.g. you want to change build flags or use a specific EMSDK version - here are the prerequisites:

  • Building on Linux is known to work, compatibility with other platforms is unknown.
  • yarn - If you use a different package manager (e.g. npm) then file paths in the demo will need adjustment.
  • Emscripten SDK - Note that there is a regression in EMSDK 2.0.14 that prevents building.
  • curl, make, openssl, sed, unzip

Here are the build steps:

  • Make sure emcc works.
  • git clone ...
  • cd wa-sqlite
  • yarn install
  • make (or yarn prepack)

The default build produces ES6 modules + WASM, synchronous and asynchronous (using Asyncify) in dist/.

API

Javascript wrappers for core SQLITE C API functions (and some others) are provided. Some convenience functions are also provided to reduce boilerplate. Here's sample code to load the library and call the API:

  import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite.mjs';
  import * as SQLite from 'wa-sqlite';

  async function hello() {
    const module = await SQLiteESMFactory();
    const sqlite3 = SQLite.Factory(module);
    const db = await sqlite3.open_v2('myDB');
    await sqlite3.exec(db, `SELECT 'Hello, world!'`, (row, columns) => {
      console.log(row);
    });
    await sqlite3.close(db);
  }

  hello();

The implementation of sqlite3.exec may be of interest to anyone wanting more fine-grained use of SQLite statement objects (e.g. for binding parameters, explicit column datatypes, etc.).

API reference

Demo

To serve the demo directly from the source tree:

The demo page provides access to databases on multiple VFS implementations, including IndexedDB (which is the only one persistent across page loads and multiple tabs). In addition, in each database there is a SQLite module named "array" that provides some historical stock data from a common Javascript array - use it for virtual tables in SQL like this:

CREATE VIRTUAL TABLE IF NOT EXISTS goog USING array;

-- Use it directly out of the Javascript array:
SELECT * FROM goog LIMIT 5;

-- Copy into a native table (on the current VFS):
CREATE TABLE IF NOT EXISTS tbl AS SELECT * FROM goog;

For convenience, if any text region is selected in the editor, only that region will be executed. In addition, the editor contents are restored across page reloads using browser localStorage.

Comments
  • column_blob returns incorrect values

    column_blob returns incorrect values

    Any time a blob column is returned, the wrong values are returned for that column.

    The pointer to the sqlite3_colum_blob is the same for every row in the result set even when the blobs are different.

    I've created a small repository that reproduces the exact issue here:

    https://github.com/tantaman/sandbox/tree/main


    The interesting bits are in src/main.ts:

    await sql`DROP TABLE IF EXISTS foo`;
    await sql`CREATE TABLE IF NOT EXISTS foo (a)`
    
    console.log('INSERTING 010203');
    await sql`INSERT INTO foo VALUES (X'010203')`
    
    console.log('SELECTING');
    let result = await sql`SELECT * FROM foo`;
    printBytesResult(result);
    
    console.log('INSERTING 01020304');
    await sql`INSERT INTO foo VALUES (X'01020304')`
    
    console.log('SELECTING');
    result = await sql`SELECT * FROM foo`;
    printBytesResult(result);
    
    console.log('INSERTING 01020304');
    await sql`INSERT INTO foo VALUES (X'01020304')`
    
    console.log('SELECTING');
    result = await sql`SELECT * FROM foo`;
    printBytesResult(result);
    
    cosonle.log('QUOTE SELECTING');
    result = await sql`SELECT quote(a) FROM foo`;
    printHexResult(result);
    

    Instead of getting a byte array of values 010203 or 01020304 we get:

    INSERTING 010203
    main.ts:73 SELECTING
    main.ts:97 raw bytes Int8Array(3) [-16, -81, 81, buffer: ArrayBuffer(16777216), byteLength: 3, byteOffset: 5353200, length: 3, Symbol(Symbol.toStringTag): 'Int8Array']
    main.ts:98 hex f0af51
    main.ts:77 INSERTING 01020304
    main.ts:80 SELECTING
    main.ts:97 raw bytes Int8Array(3) [-16, -81, 81, buffer: ArrayBuffer(16777216), byteLength: 3, byteOffset: 5350640, length: 3, Symbol(Symbol.toStringTag): 'Int8Array']
    main.ts:98 hex f0af51
    main.ts:97 raw bytes Int8Array(4) [-16, -81, 81, 0, buffer: ArrayBuffer(16777216), byteLength: 4, byteOffset: 5350640, length: 4, Symbol(Symbol.toStringTag): 'Int8Array']
    main.ts:98 hex f0af5100
    main.ts:84 INSERTING 01020304
    main.ts:87 SELECTING
    main.ts:97 raw bytes Int8Array(3) [-16, -81, 81, buffer: ArrayBuffer(16777216), byteLength: 3, byteOffset: 5352176, length: 3, Symbol(Symbol.toStringTag): 'Int8Array']
    main.ts:98 hex f0af51
    main.ts:97 raw bytes Int8Array(4) [-16, -81, 81, 0, buffer: ArrayBuffer(16777216), byteLength: 4, byteOffset: 5352176, length: 4, Symbol(Symbol.toStringTag): 'Int8Array']
    main.ts:98 hex f0af5100
    main.ts:97 raw bytes Int8Array(4) [-16, -81, 81, 0, buffer: ArrayBuffer(16777216), byteLength: 4, byteOffset: 5352176, length: 4, Symbol(Symbol.toStringTag): 'Int8Array']
    main.ts:98 hex f0af5100
    main.ts:91 QUOTE SELECTING
    main.ts:104 raw bytes (3) [1, 2, 3]
    main.ts:105 hex X'010203'
    main.ts:104 raw bytes (4) [1, 2, 3, 4]
    main.ts:105 hex X'01020304'
    main.ts:104 raw bytes (4) [1, 2, 3, 4]
    main.ts:105 hex X'01020304'
    

    Notice the byteOffest parameter for each row

    E.g., byteOffset: 5352176

    Each blob has the same byte offset which is certainly incorrect.

    Doing a sqlSELECT quote(a) FROM foo; then casting a from hex to bytes returns the correct result (see console.log('QUOTE SELECTING') section). So for some strange reason sqlite3_column_text is not impacted by this bug.

    bug 
    opened by tantaman 8
  • Add WebTorrent VFS

    Add WebTorrent VFS

    Hi @rhashimoto and thanks for your awesome work, I have implemented a WebTorrent based read only VFS. Do you want to add it to the examples?

    It will probably never be really fast but being able to "serverless" host a database is very interesting. In combination with a webseed this is also very similar to sql.js-httpvfs (Thanks @phiresky by the way for introducing me into this topic).

    To make sure the demo always works it also uses a webseed (the GOOG.db file). To truly test the torrenting you have to open a new tab at https://instant.io/ and add the GOOG.db, which then acts as another peer. Also the adding of the webseed url has to be removed in demo/index.js:L64. When using a magnetUri you may have to increase the torrent ready timeout option of WebTorrentVFS, because loading the meta information from peers is rather slow.

    opened by ydylla 7
  • BigInt support

    BigInt support

    Passing BigInt to the bind APIs currently converts them to null as their type is not handled in the switch statement.

    https://github.com/rhashimoto/wa-sqlite/blob/4c5fd0e4afd9eb1dadbd621a41db4916fe47ed67/src/sqlite-api.js#L71-L90

    Looking at the official WASM build -- looks like it is possible to just expose bind_int64 from the C api and pass a BigInt to it.

    enhancement 
    opened by tantaman 5
  • How do I use wa-sqlite in Node.js?

    How do I use wa-sqlite in Node.js?

    wa-sqlite is built as a set of ES6 modules. As ES6 modules generated by Emscripten do not support Node.js very well, we have to employ a couple of workarounds for the SQLiteESMFactory as documented in emscripten-core/emscripten#11792:

    • Set globals for __dirname and require()
    • Provide a locateFile() in the config object to the factory

    The code snippet below illustrates how this is done:

      var SQLite = await import('wa-sqlite')
      var { default: SQLiteESMFactory } = await import('wa-sqlite/dist/wa-sqlite-async.mjs')
    
      // Remember the original globals
      const currentDirName = globalThis.__dirname
      const originalRequire = globalThis.require
    
      // Replace this with actual path to wa-sqlite/dist
      const sqliteDistPath = 'node_modules/wa-sqlite/dist' 
    
      // If we are using node, replace the globals below
      // with values that our module factory will use
      if (typeof process === 'object') {
        const { createRequire } = await import('module')
        globalThis.__dirname = sqliteDistPath
        globalThis.require = createRequire(__dirname + sqliteDistPath + '/wa-sqlite-async.mjs')
      }
    
      // Invoke the ES6 module factory (with a custom `locateFile` method for node)
      // to create the SQLite Emscripten module. This will fetch and compile the 
      // .wasm file.
      const options = typeof process === 'object'
        ? { locateFile: path => sqliteDistPath + '/' + path }
        : {}
      const module = await SQLiteESMFactory(options)
    
      // Reinstate the globals after creating the module
      globalThis.require = originalRequire
      globalThis.__dirname = currentDirName
    
      // Use the module to build the API instance.
      const sqlite3 = SQLite.Factory(module)
    
    faq 
    opened by LoneRifle 5
  • WebLocks doesn't support multiple connections to the same database in the same context

    WebLocks doesn't support multiple connections to the same database in the same context

    WebLocks (and WebLocksShared) associate lock state with the filename. That means it can't distinguish multiple connections to the same database using the same WebLocks instance. The connections would overwrite each other's state.

    An easy fix would be to give every open file its own instance of WebLocks, but...

    Concurrent access to the same database in the same SQLite context where one access is a writer doesn't work anyway because Asyncify-ed WebAssembly is not re-entrant during an asynchronous callback. It isn't possible for one database call to block and then be able to make another database call, so fixing the bug won't enable any new usages.

    Note that you can access the same database in the same SQLite context as long each call into SQLite completes before the next call is made.

    bug 
    opened by rhashimoto 5
  • What about replacing Asyncify with Atomics and SharedArrayBuffer?

    What about replacing Asyncify with Atomics and SharedArrayBuffer?

    There are significant time and space penalties for using WebAssembly SQLite built with Asyncify, which allows SQLite to callback to asynchronous Javascript functions. This is why wa-sqlite provides both synchronous (without Asyncify) and asynchronous builds, so developers can avoid those penalties if they don't need to use asynchronous callbacks.

    Asynchronous callbacks are especially useful for calling asynchronous APIs like IndexedDB or fetch. But @phiresky (sql.js-httpvfs) and @jlongster (absurd-sql) independently discovered a clever way to make synchronous calls to asynchronous APIs using Atomics and SharedArrayBuffer. It's pretty cool!

    The main catch with these APIs (and why it took so long to get support in Safari again) is they are potentially exploitable by Spectre malware attacks, so they are only enabled on pages served with special COOP/COEP HTTP headers which also heavily restrict including content from a different origin. This may prevent integrating certain third-party features like federated sign-in (e.g. OAuth2) and ads right now.

    Implementing a SQLite VFS or module using Atomics and SharedArrayBuffer based on the described methods should be straightforward with the wa-sqlite synchronous build and I would be excited to see it happen. I don't plan to do that myself, however - i.e. I don't plan to add a synchronous IndexedDB (or File System Access) VFS to this repo - at least until it can be made to work with federated sign-in.

    The size of the WebAssembly for wa-sqlite is about 507 KB for the synchronous build and 1,032 KB for the Asyncify build, so roughly double the size. The speed penalty is harder to characterize because although the cost of unwinding and rewinding the stack for asynchronous calls is quite high, it may still be relatively small compared to the asynchronous function itself. For example, if you're making a 1500 ms asynchronous call to WebTorrent then you may not care if Asyncify adds another 5 ms (I made these numbers up just for illustration). In general, the slower your wa-sqlite storage medium, the less you should care about Asyncify performance...though you still might want to tune your page size to reduce both Asyncify and non-Asyncify overhead.

    Those interested in synchronous WebAssembly filesystems should also track Emscripten's WasmFS which is in development.

    faq 
    opened by rhashimoto 5
  • How can I use an ES6 module in a worker?

    How can I use an ES6 module in a worker?

    The wa-sqlite build produces ES6 modules, which is nice for development. But most applications should put time-consuming operations in a Worker, and support for ES6 modules in a Worker isn't universal yet. Worker code generally needs to be bundled.

    Emscripten modules need to load their associated .wasm file, and the generated code for ES6 uses import.meta.url as its default location. import.meta.url is not valid outside an ES6 module, however, so the bundler has to translate this to something else. When the target context is a Worker, that something else needs to be valid in a Worker, so something like document.currentScript.src, which is the substitution my bundler chooses, causes a runtime exception.

    The fix I use is to configure a bundler string replacement plug-in (@rollup/plugin-replace in my case) to replace import.meta.url with an empty string. There may be better approaches out there but this is sufficient for me so far.

    faq 
    opened by rhashimoto 5
  • mjs and masm files ( /dist) are missing

    mjs and masm files ( /dist) are missing

    Hi @rhashimoto 😊 Thank you for your work 🙏🏻 I am hoping that I will be able to get the demo running. However, seems like a couple of files missing . Specifically these 2 files are missing:

    ~/dist/wa-sqlite-async.mjs
    ~/dist/wa-sqlite.mjs
    ~/dist/wa-sqlite-async.wasm
    ~/dist/wa-sqlite-async.wasm
    

    From where can I get those files ? In the meantime, I just copied them from : https://rhashimoto.github.io/wa-sqlite/dist/

    Also it seems to need CSP settings. wasm-unsafe-eval

    opened by jalsco 4
  • blobs should be uint8array not int8array?

    blobs should be uint8array not int8array?

    I notice that blobs, when read out of wa-sqlite, are being returned as int8array rather than uint8array.

    Also when inserting blobs -- you expect int8array.

    I believe these should be Uint8Arrays instead.

    wontfix 
    opened by tantaman 3
  • Typescript typings are incomplete / not a module

    Typescript typings are incomplete / not a module

    You can't import the main api via

    import * as SQLite from "wa-sqlite";

    because it throws this error:

    File '.../node_modules/wa-sqlite/src/types/sqlite-api.d.ts' is not a module.

    The dist file etc are also not typed, import SQLiteAsyncESMFactory from "wa-sqlite/dist/wa-sqlite-async.mjs"; throws a type error, as does import * as VFS from "wa-sqlite/src/VFS";

    bug 
    opened by phiresky 3
  • How to bundle / specify wasm URL?

    How to bundle / specify wasm URL?

    Using webpack, it tries to load the WASM from a file:/// url and the wasm file is not bundled as an asset. Is it possible to specify the wasm URL or make it work with webpack in some other way?

    opened by phiresky 3
  • OriginPrivateFileSystemVFS SQL_IOERR with journal_mode TRUNCATE or PERSIST

    OriginPrivateFileSystemVFS SQL_IOERR with journal_mode TRUNCATE or PERSIST

    If multiple connections are using the OriginPrivateFileSystemVFS with PRAGMA journal_mode TRUNCATE or PERSIST, an SQL_IOERR is likely to result. The problem is that in these modes multiple connections can open the journal file and OPFS access handles are exclusive. A workaround is not to use these modes with multiple connections.

    This is not a high priority to fix right now. IDBBatchAtomicVFS is better in pretty much every way that matters to application writers - ease of integration, performance, maturity - so the OPFS VFS is mostly a proof of concept.

    bug 
    opened by rhashimoto 0
Owner
Roy Hashimoto
Roy Hashimoto
A WebAssembly build of the Tesseract OCR engine for use in the browser and Node

tesseract-wasm A WebAssembly build of the Tesseract OCR engine for use in the browser and Node. tesseract-wasm can detect and recognize text in docume

Robert Knight 80 Dec 28, 2022
ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, SAP Hana, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms.

TypeORM is an ORM that can run in NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo, and Electron platforms and can be used

null 30.1k Jan 3, 2023
Explore, create and deploy your SQLite databases right from your browser. Quick and easy, no installation required.

SQLighter (under development, alpha code) SQLighter is a database explorer born for SQLite that helps you design and deploy your application database

sqlighter 11 Sep 20, 2022
Run official FLAC tools `flac` and `metaflac` as WebAssembly, on browsers or Deno.

flac.wasm Run official FLAC tools flac and metaflac as WebAssembly, on browsers or Deno. Currently we have no plans on supporting Node.js. Try it onli

Pig Fang 15 Aug 21, 2022
A template of Rust + WebAssembly with TypeScript (🦀 + 🕸️ = 💖)

rust-wasm-ts-template This repository is a template of Rust + WebAssembly with TypeScript ( ?? + ??️ = ?? ). Requirements The Rust Toolchain wasm-pack

Chris Ohk 20 Aug 26, 2022
Simple key-value storage with support for multiple backends

Simple key-value storage with support for multiple backends Keyv provides a consistent interface for key-value storage across multiple backends via st

Luke Childs 2k Jan 7, 2023
A javascript library to run SQLite on the web.

SQLite compiled to JavaScript sql.js is a javascript SQL database. It allows you to create a relational database and query it entirely in the browser.

SQL.JS 11k Jan 7, 2023
Realm is a mobile database: an alternative to SQLite & key-value stores

Realm is a mobile database that runs directly inside phones, tablets or wearables. This project hosts the JavaScript versions of Realm. Currently we s

Realm 5.1k Jan 3, 2023
TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL and SQLite databases.

TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL and SQLite datab

MikroORM 5.4k Dec 31, 2022
Next-generation ORM for Node.js & TypeScript | PostgreSQL, MySQL, MariaDB, SQL Server & SQLite

Prisma Quickstart • Website • Docs • Examples • Blog • Slack • Twitter • Prisma 1 What is Prisma? Prisma is a next-generation ORM that consists of the

Prisma 28k Jan 2, 2023
Database manager for MySQL, PostgreSQL, SQL Server, MongoDB, SQLite and others. Runs under Windows, Linux, Mac or as web application

Database manager for MySQL, PostgreSQL, SQL Server, MongoDB, SQLite and others. Runs under Windows, Linux, Mac or as web application

DbGate 2k Dec 30, 2022
Ecommerce-backend-nestjs - Ecommerce app with Nestjs + Prisma ORM + GraphQL + SQLite

ECOMMERCE BACKEND NESTJS APP Nestjs + Prisma ORM + GraphQL + SQLite USER Create Account Login Product Create Product Get Products Get Product Search P

Rui Paulo Calei 5 Apr 6, 2022
Same as sqlite-tag but without the native sqlite3 module dependency

sqlite-tag-spawned Social Media Photo by Tomas Kirvėla on Unsplash The same sqlite-tag ease but without the native sqlite3 dependency, aiming to repla

Andrea Giammarchi 17 Nov 20, 2022
A Node.js ORM for MySQL, SQLite, PostgreSQL, MongoDB, GitHub and serverless service like Deta, InspireCloud, CloudBase, LeanCloud.

Dittorm A Node.js ORM for MySQL, SQLite, PostgreSQL, MongoDB, GitHub and serverless service like Deta, InspireCloud, CloudBase, LeanCloud. Installatio

Waline 21 Dec 25, 2022
A remote nodejs Cached sqlite Database Server, for you to have your perfect MAP Cache Saved and useable remotely.

A remote nodejs Cached sqlite Database Server, for you to have your perfect MAP Cache Saved and useable remotely. Easy Server and Client Creations, fast, stores the Cache before stopping and restores it again! it uses ENMAP

Tomato6966 6 Dec 18, 2022
A simple url shorter API built with nodejs running on Kubernetes in Google Cloud, using PostgreSQL for storage and cloud sql proxy.

Simple URL Shorter - Google Cloud - Kubernetes A simple url shorter API built with nodejs running on Kubernetes in Google Cloud, using PostgreSQL for

null 3 Nov 25, 2021
An adapter-based ORM for Node.js with support for mysql, mongo, postgres, mssql (SQL Server), and more

Waterline is a next-generation storage and retrieval engine, and the default ORM used in the Sails framework. It provides a uniform API for accessing

Balderdash 5.4k Jan 4, 2023
A MongoDB-like database built on top of Hyperbee with support for indexing

hyperbeedeebee A MongoDB-like database built on top of Hyperbee with support for indexing WIP: There may be breaking changes in the indexing before th

null 35 Dec 12, 2022