expo-sqlite
Version:
Provides access to a database using SQLite (https://www.sqlite.org/). The database is persisted across restarts of your app.
509 lines (466 loc) • 13.6 kB
JavaScript
// Copyright 2024 Roy T. Hashimoto. All Rights Reserved.
import * as VFS from './VFS.js';
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
// Convenience base class for a JavaScript VFS.
// The raw xOpen, xRead, etc. function signatures receive only C primitives
// which aren't easy to work with. This class provides corresponding calls
// like jOpen, jRead, etc., which receive JavaScript-friendlier arguments
// such as string, Uint8Array, and DataView.
export class FacadeVFS extends VFS.Base {
/**
* @param {string} name
* @param {object} module
*/
constructor(name, module) {
super(name, module);
}
/**
* Override to indicate which methods are asynchronous.
* @param {string} methodName
* @returns {boolean}
*/
hasAsyncMethod(methodName) {
// The input argument is a string like "xOpen", so convert to "jOpen".
// Then check if the method exists and is async.
const jMethodName = `j${methodName.slice(1)}`;
return this[jMethodName] instanceof AsyncFunction;
}
/**
* Return the filename for a file id for use by mixins.
* @param {number} pFile
* @returns {string}
*/
getFilename(pFile) {
throw new Error('unimplemented');
}
/**
* @param {string?} filename
* @param {number} pFile
* @param {number} flags
* @param {DataView} pOutFlags
* @returns {number|Promise<number>}
*/
jOpen(filename, pFile, flags, pOutFlags) {
return VFS.SQLITE_CANTOPEN;
}
/**
* @param {string} filename
* @param {number} syncDir
* @returns {number|Promise<number>}
*/
jDelete(filename, syncDir) {
return VFS.SQLITE_OK;
}
/**
* @param {string} filename
* @param {number} flags
* @param {DataView} pResOut
* @returns {number|Promise<number>}
*/
jAccess(filename, flags, pResOut) {
return VFS.SQLITE_OK;
}
/**
* @param {string} filename
* @param {Uint8Array} zOut
* @returns {number|Promise<number>}
*/
jFullPathname(filename, zOut) {
// Copy the filename to the output buffer.
const { read, written } = new TextEncoder().encodeInto(filename, zOut);
if (read < filename.length) return VFS.SQLITE_IOERR;
if (written >= zOut.length) return VFS.SQLITE_IOERR;
zOut[written] = 0;
return VFS.SQLITE_OK;
}
/**
* @param {Uint8Array} zBuf
* @returns {number|Promise<number>}
*/
jGetLastError(zBuf) {
return VFS.SQLITE_OK;
}
/**
* @param {number} pFile
* @returns {number|Promise<number>}
*/
jClose(pFile) {
return VFS.SQLITE_OK;
}
/**
* @param {number} pFile
* @param {Uint8Array} pData
* @param {number} iOffset
* @returns {number|Promise<number>}
*/
jRead(pFile, pData, iOffset) {
pData.fill(0);
return VFS.SQLITE_IOERR_SHORT_READ;
}
/**
* @param {number} pFile
* @param {Uint8Array} pData
* @param {number} iOffset
* @returns {number|Promise<number>}
*/
jWrite(pFile, pData, iOffset) {
return VFS.SQLITE_IOERR_WRITE;
}
/**
* @param {number} pFile
* @param {number} size
* @returns {number|Promise<number>}
*/
jTruncate(pFile, size) {
return VFS.SQLITE_OK;
}
/**
* @param {number} pFile
* @param {number} flags
* @returns {number|Promise<number>}
*/
jSync(pFile, flags) {
return VFS.SQLITE_OK;
}
/**
* @param {number} pFile
* @param {DataView} pSize
* @returns {number|Promise<number>}
*/
jFileSize(pFile, pSize) {
return VFS.SQLITE_OK;
}
/**
* @param {number} pFile
* @param {number} lockType
* @returns {number|Promise<number>}
*/
jLock(pFile, lockType) {
return VFS.SQLITE_OK;
}
/**
* @param {number} pFile
* @param {number} lockType
* @returns {number|Promise<number>}
*/
jUnlock(pFile, lockType) {
return VFS.SQLITE_OK;
}
/**
* @param {number} pFile
* @param {DataView} pResOut
* @returns {number|Promise<number>}
*/
jCheckReservedLock(pFile, pResOut) {
pResOut.setInt32(0, 0, true);
return VFS.SQLITE_OK;
}
/**
* @param {number} pFile
* @param {number} op
* @param {DataView} pArg
* @returns {number|Promise<number>}
*/
jFileControl(pFile, op, pArg) {
return VFS.SQLITE_NOTFOUND;
}
/**
* @param {number} pFile
* @returns {number|Promise<number>}
*/
jSectorSize(pFile) {
return super.xSectorSize(pFile);
}
/**
* @param {number} pFile
* @returns {number|Promise<number>}
*/
jDeviceCharacteristics(pFile) {
return 0;
}
/**
* @param {number} pVfs
* @param {number} zName
* @param {number} pFile
* @param {number} flags
* @param {number} pOutFlags
* @returns {number|Promise<number>}
*/
xOpen(pVfs, zName, pFile, flags, pOutFlags) {
const filename = this.#decodeFilename(zName, flags);
const pOutFlagsView = this.#makeTypedDataView('Int32', pOutFlags);
this['log']?.('jOpen', filename, pFile, '0x' + flags.toString(16));
return this.jOpen(filename, pFile, flags, pOutFlagsView);
}
/**
* @param {number} pVfs
* @param {number} zName
* @param {number} syncDir
* @returns {number|Promise<number>}
*/
xDelete(pVfs, zName, syncDir) {
const filename = this._module.UTF8ToString(zName);
this['log']?.('jDelete', filename, syncDir);
return this.jDelete(filename, syncDir);
}
/**
* @param {number} pVfs
* @param {number} zName
* @param {number} flags
* @param {number} pResOut
* @returns {number|Promise<number>}
*/
xAccess(pVfs, zName, flags, pResOut) {
const filename = this._module.UTF8ToString(zName);
const pResOutView = this.#makeTypedDataView('Int32', pResOut);
this['log']?.('jAccess', filename, flags);
return this.jAccess(filename, flags, pResOutView);
}
/**
* @param {number} pVfs
* @param {number} zName
* @param {number} nOut
* @param {number} zOut
* @returns {number|Promise<number>}
*/
xFullPathname(pVfs, zName, nOut, zOut) {
const filename = this._module.UTF8ToString(zName);
const zOutArray = this._module.HEAPU8.subarray(zOut, zOut + nOut);
this['log']?.('jFullPathname', filename, nOut);
return this.jFullPathname(filename, zOutArray);
}
/**
* @param {number} pVfs
* @param {number} nBuf
* @param {number} zBuf
* @returns {number|Promise<number>}
*/
xGetLastError(pVfs, nBuf, zBuf) {
const zBufArray = this._module.HEAPU8.subarray(zBuf, zBuf + nBuf);
this['log']?.('jGetLastError', nBuf);
return this.jGetLastError(zBufArray);
}
/**
* @param {number} pFile
* @returns {number|Promise<number>}
*/
xClose(pFile) {
this['log']?.('jClose', pFile);
return this.jClose(pFile);
}
/**
* @param {number} pFile
* @param {number} pData
* @param {number} iAmt
* @param {number} iOffsetLo
* @param {number} iOffsetHi
* @returns {number|Promise<number>}
*/
xRead(pFile, pData, iAmt, iOffsetLo, iOffsetHi) {
const pDataArray = this.#makeDataArray(pData, iAmt);
const iOffset = delegalize(iOffsetLo, iOffsetHi);
this['log']?.('jRead', pFile, iAmt, iOffset);
return this.jRead(pFile, pDataArray, iOffset);
}
/**
* @param {number} pFile
* @param {number} pData
* @param {number} iAmt
* @param {number} iOffsetLo
* @param {number} iOffsetHi
* @returns {number|Promise<number>}
*/
xWrite(pFile, pData, iAmt, iOffsetLo, iOffsetHi) {
const pDataArray = this.#makeDataArray(pData, iAmt);
const iOffset = delegalize(iOffsetLo, iOffsetHi);
this['log']?.('jWrite', pFile, pDataArray, iOffset);
return this.jWrite(pFile, pDataArray, iOffset);
}
/**
* @param {number} pFile
* @param {number} sizeLo
* @param {number} sizeHi
* @returns {number|Promise<number>}
*/
xTruncate(pFile, sizeLo, sizeHi) {
const size = delegalize(sizeLo, sizeHi);
this['log']?.('jTruncate', pFile, size);
return this.jTruncate(pFile, size);
}
/**
* @param {number} pFile
* @param {number} flags
* @returns {number|Promise<number>}
*/
xSync(pFile, flags) {
this['log']?.('jSync', pFile, flags);
return this.jSync(pFile, flags);
}
/**
*
* @param {number} pFile
* @param {number} pSize
* @returns {number|Promise<number>}
*/
xFileSize(pFile, pSize) {
const pSizeView = this.#makeTypedDataView('BigInt64', pSize);
this['log']?.('jFileSize', pFile);
return this.jFileSize(pFile, pSizeView);
}
/**
* @param {number} pFile
* @param {number} lockType
* @returns {number|Promise<number>}
*/
xLock(pFile, lockType) {
this['log']?.('jLock', pFile, lockType);
return this.jLock(pFile, lockType);
}
/**
* @param {number} pFile
* @param {number} lockType
* @returns {number|Promise<number>}
*/
xUnlock(pFile, lockType) {
this['log']?.('jUnlock', pFile, lockType);
return this.jUnlock(pFile, lockType);
}
/**
* @param {number} pFile
* @param {number} pResOut
* @returns {number|Promise<number>}
*/
xCheckReservedLock(pFile, pResOut) {
const pResOutView = this.#makeTypedDataView('Int32', pResOut);
this['log']?.('jCheckReservedLock', pFile);
return this.jCheckReservedLock(pFile, pResOutView);
}
/**
* @param {number} pFile
* @param {number} op
* @param {number} pArg
* @returns {number|Promise<number>}
*/
xFileControl(pFile, op, pArg) {
const pArgView = new DataView(
this._module.HEAPU8.buffer,
this._module.HEAPU8.byteOffset + pArg);
this['log']?.('jFileControl', pFile, op, pArgView);
return this.jFileControl(pFile, op, pArgView);
}
/**
* @param {number} pFile
* @returns {number|Promise<number>}
*/
xSectorSize(pFile) {
this['log']?.('jSectorSize', pFile);
return this.jSectorSize(pFile);
}
/**
* @param {number} pFile
* @returns {number|Promise<number>}
*/
xDeviceCharacteristics(pFile) {
this['log']?.('jDeviceCharacteristics', pFile);
return this.jDeviceCharacteristics(pFile);
}
/**
* Wrapped DataView for pointer arguments.
* Pointers to a single value are passed using DataView. A Proxy
* wrapper prevents use of incorrect type or endianness.
* @param {'Int32'|'BigInt64'} type
* @param {number} byteOffset
* @returns {DataView}
*/
#makeTypedDataView(type, byteOffset) {
const byteLength = type === 'Int32' ? 4 : 8;
const getter = `get${type}`;
const setter = `set${type}`;
const makeDataView = () => new DataView(
this._module.HEAPU8.buffer,
this._module.HEAPU8.byteOffset + byteOffset,
byteLength);
let dataView = makeDataView();
return new Proxy(dataView, {
get(_, prop) {
if (dataView.buffer.byteLength === 0) {
// WebAssembly memory resize detached the buffer.
dataView = makeDataView();
}
if (prop === getter) {
return function(byteOffset, littleEndian) {
if (!littleEndian) throw new Error('must be little endian');
return dataView[prop](byteOffset, littleEndian);
}
}
if (prop === setter) {
return function(byteOffset, value, littleEndian) {
if (!littleEndian) throw new Error('must be little endian');
return dataView[prop](byteOffset, value, littleEndian);
}
}
if (typeof prop === 'string' && (prop.match(/^(get)|(set)/))) {
throw new Error('invalid type');
}
const result = dataView[prop];
return typeof result === 'function' ? result.bind(dataView) : result;
}
});
}
/**
* @param {number} byteOffset
* @param {number} byteLength
*/
#makeDataArray(byteOffset, byteLength) {
let target = this._module.HEAPU8.subarray(byteOffset, byteOffset + byteLength);
return new Proxy(target, {
get: (_, prop, receiver) => {
if (target.buffer.byteLength === 0) {
// WebAssembly memory resize detached the buffer.
target = this._module.HEAPU8.subarray(byteOffset, byteOffset + byteLength);
}
const result = target[prop];
return typeof result === 'function' ? result.bind(target) : result;
}
});
}
#decodeFilename(zName, flags) {
if (flags & VFS.SQLITE_OPEN_URI) {
// The first null-terminated string is the URI path. Subsequent
// strings are query parameter keys and values.
// https://www.sqlite.org/c3ref/open.html#urifilenamesinsqlite3open
let pName = zName;
let state = 1;
const charCodes = [];
while (state) {
const charCode = this._module.HEAPU8[pName++];
if (charCode) {
charCodes.push(charCode);
} else {
if (!this._module.HEAPU8[pName]) state = null;
switch (state) {
case 1: // path
charCodes.push('?'.charCodeAt(0));
state = 2;
break;
case 2: // key
charCodes.push('='.charCodeAt(0));
state = 3;
break;
case 3: // value
charCodes.push('&'.charCodeAt(0));
state = 2;
break;
}
}
}
return new TextDecoder().decode(new Uint8Array(charCodes));
}
return zName ? this._module.UTF8ToString(zName) : null;
}
}
// Emscripten "legalizes" 64-bit integer arguments by passing them as
// two 32-bit signed integers.
function delegalize(lo32, hi32) {
return (hi32 * 0x100000000) + lo32 + (lo32 < 0 ? 2**32 : 0);
}