diff --git a/.github/ISSUE_TEMPLATE/-do-not-post-anything-other-than-a-bug-report.md b/.github/ISSUE_TEMPLATE/-do-not-post-anything-other-than-a-bug-report.md new file mode 100644 index 00000000..a4590df9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/-do-not-post-anything-other-than-a-bug-report.md @@ -0,0 +1,10 @@ +--- +name: " Do not post anything other than a bug report" +about: Issues are only for possible bugs in project code. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/Makefile b/Makefile index 152987b3..7a44a162 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # dependencies -SQLITE_VERSION = version-3.47.0 -SQLITE_TARBALL_URL = https://www.sqlite.org/src/tarball/sqlite.tar.gz?r=${SQLITE_VERSION} +SQLITE_VERSION = version-3.50.1 +SQLITE_TARBALL_URL = https://www.sqlite.org/src/tarball/$(SQLITE_VERSION)/sqlite.tar.gz EXTENSION_FUNCTIONS = extension-functions.c EXTENSION_FUNCTIONS_URL = https://www.sqlite.org/contrib/download/extension-functions.c?get=25 diff --git a/demo/hello/hello.js b/demo/hello/hello.js index 86784022..56c9c554 100644 --- a/demo/hello/hello.js +++ b/demo/hello/hello.js @@ -4,9 +4,9 @@ // to use. Note that an asynchronous VFS requires an asynchronous build // (Asyncify or JSPI). As of 2024-05-26, JSPI is only available behind // a flag on Chromium browsers. -// import SQLiteESMFactory from '../dist/wa-sqlite.mjs'; +// import SQLiteESMFactory from '../../dist/wa-sqlite.mjs'; import SQLiteESMFactory from '../../dist/wa-sqlite-async.mjs'; -// import SQLiteESMFactory from '../dist/wa-sqlite-jspi.mjs'; +// import SQLiteESMFactory from '../../dist/wa-sqlite-jspi.mjs'; // Uncomment one of the following imports to choose a VFS. Note that an // asynchronous VFS requires an asynchronous build, and an VFS using @@ -19,11 +19,11 @@ import SQLiteESMFactory from '../../dist/wa-sqlite-async.mjs'; // clear the appropriate storage for things to work. import { IDBBatchAtomicVFS as MyVFS } from '../../src/examples/IDBBatchAtomicVFS.js'; // import { IDBMirrorVFS as MyVFS } from '../../src/examples/IDBMirrorVFS.js'; -// import { AccessHandlePoolVFS as MyVFS } from '../src/examples/AccessHandlePoolVFS.js'; -// import { OPFSAdaptiveVFS as MyVFS } from '../src/examples/OPFSAdaptiveVFS.js'; +// import { AccessHandlePoolVFS as MyVFS } from '../../src/examples/AccessHandlePoolVFS.js'; +// import { OPFSAdaptiveVFS as MyVFS } from '../../src/examples/OPFSAdaptiveVFS.js'; // import { OPFSAnyContextVFS as MyVFS } from '../../src/examples/OPFSAnyContextVFS.js'; -// import { OPFSCoopSyncVFS as MyVFS } from '../src/examples/OPFSCoopSyncVFS.js'; -// import { OPFSPermutedVFS as MyVFS } from '../src/examples/OPFSPermutedVFS.js'; +// import { OPFSCoopSyncVFS as MyVFS } from '../../src/examples/OPFSCoopSyncVFS.js'; +// import { OPFSPermutedVFS as MyVFS } from '../../src/examples/OPFSPermutedVFS.js'; import * as SQLite from '../../src/sqlite-api.js'; diff --git a/dist/wa-sqlite-async.wasm b/dist/wa-sqlite-async.wasm index 140752fb..9efd1425 100755 Binary files a/dist/wa-sqlite-async.wasm and b/dist/wa-sqlite-async.wasm differ diff --git a/dist/wa-sqlite-jspi.wasm b/dist/wa-sqlite-jspi.wasm index 8972d554..e938319a 100755 Binary files a/dist/wa-sqlite-jspi.wasm and b/dist/wa-sqlite-jspi.wasm differ diff --git a/dist/wa-sqlite.wasm b/dist/wa-sqlite.wasm index c22ad5a3..5bb05df8 100755 Binary files a/dist/wa-sqlite.wasm and b/dist/wa-sqlite.wasm differ diff --git a/package.json b/package.json index 040629ac..690d10ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wa-sqlite", - "version": "1.0.6", + "version": "1.0.9", "type": "module", "main": "src/sqlite-api.js", "types": "src/types/index.d.ts", diff --git a/src/FacadeVFS.js b/src/FacadeVFS.js index c975fdc9..dbc4947a 100644 --- a/src/FacadeVFS.js +++ b/src/FacadeVFS.js @@ -405,64 +405,30 @@ export class FacadeVFS extends VFS.Base { /** * Wrapped DataView for pointer arguments. - * Pointers to a single value are passed using DataView. A Proxy - * wrapper prevents use of incorrect type or endianness. + * Pointers to a single value are passed using a DataView-like class. + * This wrapper class prevents use of incorrect type or endianness, and + * reacquires the underlying buffer when the WebAssembly memory is resized. * @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; - } - }); + // @ts-ignore + return new DataViewProxy(this._module, byteOffset, type); } /** + * Wrapped Uint8Array for buffer arguments. + * Memory blocks are passed as a Uint8Array-like class. This wrapper + * class reacquires the underlying buffer when the WebAssembly memory + * is resized. * @param {number} byteOffset * @param {number} byteLength + * @returns {Uint8Array} */ #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; - } - }); + // @ts-ignore + return new Uint8ArrayProxy(this._module, byteOffset, byteLength); } #decodeFilename(zName, flags) { @@ -506,3 +472,210 @@ export class FacadeVFS extends VFS.Base { function delegalize(lo32, hi32) { return (hi32 * 0x100000000) + lo32 + (lo32 < 0 ? 2**32 : 0); } + +// This class provides a Uint8Array-like interface for a WebAssembly memory +// buffer. It is used to access memory blocks passed as arguments to +// xRead, xWrite, etc. The class reacquires the underlying buffer when the +// WebAssembly memory is resized, which can happen when the memory is +// detached and resized by the WebAssembly module. +// +// Note that although this class implements the same methods as Uint8Array, +// it is not a real Uint8Array and passing it to functions that expect +// a Uint8Array may not work. Use subarray() to get a real Uint8Array +// if needed. +class Uint8ArrayProxy { + #module; + + #_array = new Uint8Array() + get #array() { + if (this.#_array.buffer.byteLength === 0) { + // WebAssembly memory resize detached the buffer so re-create the + // array with the new buffer. + this.#_array = this.#module.HEAPU8.subarray( + this.byteOffset, + this.byteOffset + this.byteLength); + } + return this.#_array; + } + + /** + * @param {*} module + * @param {number} byteOffset + * @param {number} byteLength + */ + constructor(module, byteOffset, byteLength) { + this.#module = module; + this.byteOffset = byteOffset; + this.length = this.byteLength = byteLength; + } + + get buffer() { + return this.#array.buffer; + } + + at(index) { + return this.#array.at(index); + } + copyWithin(target, start, end) { + this.#array.copyWithin(target, start, end); + } + entries() { + return this.#array.entries(); + } + every(predicate) { + return this.#array.every(predicate); + } + fill(value, start, end) { + this.#array.fill(value, start, end); + } + filter(predicate) { + return this.#array.filter(predicate); + } + find(predicate) { + return this.#array.find(predicate); + } + findIndex(predicate) { + return this.#array.findIndex(predicate); + } + findLast(predicate) { + return this.#array.findLast(predicate); + } + findLastIndex(predicate) { + return this.#array.findLastIndex(predicate); + } + forEach(callback) { + this.#array.forEach(callback); + } + includes(value, start) { + return this.#array.includes(value, start); + } + indexOf(value, start) { + return this.#array.indexOf(value, start); + } + join(separator) { + return this.#array.join(separator); + } + keys() { + return this.#array.keys(); + } + lastIndexOf(value, start) { + return this.#array.lastIndexOf(value, start); + } + map(callback) { + return this.#array.map(callback); + } + reduce(callback, initialValue) { + return this.#array.reduce(callback, initialValue); + } + reduceRight(callback, initialValue) { + return this.#array.reduceRight(callback, initialValue); + } + reverse() { + this.#array.reverse(); + } + set(array, offset) { + this.#array.set(array, offset); + } + slice(start, end) { + return this.#array.slice(start, end); + } + some(predicate) { + return this.#array.some(predicate); + } + sort(compareFn) { + this.#array.sort(compareFn); + } + subarray(begin, end) { + return this.#array.subarray(begin, end); + } + toLocaleString(locales, options) { + // @ts-ignore + return this.#array.toLocaleString(locales, options); + } + toReversed() { + return this.#array.toReversed(); + } + toSorted(compareFn) { + return this.#array.toSorted(compareFn); + } + toString() { + return this.#array.toString(); + } + values() { + return this.#array.values(); + } + with(index, value) { + return this.#array.with(index, value); + } + [Symbol.iterator]() { + return this.#array[Symbol.iterator](); + } +} + +// This class provides a DataView-like interface for a WebAssembly memory +// buffer, restricted to either Int32 or BigInt64 types. It also reacquires +// the underlying buffer when the WebAssembly memory is resized, which can +// happen when the memory is detached and resized by the WebAssembly module. +class DataViewProxy { + #module; + #type; + + #_view = new DataView(new ArrayBuffer(0)); + get #view() { + if (this.#_view.buffer.byteLength === 0) { + // WebAssembly memory resize detached the buffer so re-create the + // view with the new buffer. + this.#_view = new DataView( + this.#module.HEAPU8.buffer, + this.#module.HEAPU8.byteOffset + this.byteOffset); + } + return this.#_view; + } + + /** + * @param {*} module + * @param {number} byteOffset + * @param {'Int32'|'BigInt64'} type + */ + constructor(module, byteOffset, type) { + this.#module = module; + this.byteOffset = byteOffset; + this.#type = type; + } + + get buffer() { + return this.#view.buffer; + } + get byteLength() { + return this.#type === 'Int32' ? 4 : 8; + } + + getInt32(byteOffset, littleEndian) { + if (this.#type !== 'Int32') { + throw new Error('invalid type'); + } + if (!littleEndian) throw new Error('must be little endian'); + return this.#view.getInt32(byteOffset, littleEndian); + } + setInt32(byteOffset, value, littleEndian) { + if (this.#type !== 'Int32') { + throw new Error('invalid type'); + } + if (!littleEndian) throw new Error('must be little endian'); + this.#view.setInt32(byteOffset, value, littleEndian); + } + getBigInt64(byteOffset, littleEndian) { + if (this.#type !== 'BigInt64') { + throw new Error('invalid type'); + } + if (!littleEndian) throw new Error('must be little endian'); + return this.#view.getBigInt64(byteOffset, littleEndian); + } + setBigInt64(byteOffset, value, littleEndian) { + if (this.#type !== 'BigInt64') { + throw new Error('invalid type'); + } + if (!littleEndian) throw new Error('must be little endian'); + this.#view.setBigInt64(byteOffset, value, littleEndian); + } +} \ No newline at end of file diff --git a/src/WebLocksMixin.js b/src/WebLocksMixin.js index 9b1e97b6..e5fd48f9 100644 --- a/src/WebLocksMixin.js +++ b/src/WebLocksMixin.js @@ -48,18 +48,7 @@ export const WebLocksMixin = superclass => class extends superclass { */ async jLock(fileId, lockType) { try { - // Create state on first lock. - if (!this.#mapIdToState.has(fileId)) { - const name = this.getFilename(fileId); - const state = { - baseName: name, - type: VFS.SQLITE_LOCK_NONE, - writeHint: false - }; - this.#mapIdToState.set(fileId, state); - } - - const lockState = this.#mapIdToState.get(fileId); + const lockState = this.#getLockState(fileId); if (lockType <= lockState.type) return VFS.SQLITE_OK; switch (this.#options.lockPolicy) { @@ -82,10 +71,8 @@ export const WebLocksMixin = superclass => class extends superclass { */ async jUnlock(fileId, lockType) { try { - // SQLite can call xUnlock() without ever calling xLock() so - // the state may not exist. - const lockState = this.#mapIdToState.get(fileId); - if (!(lockType < lockState?.type)) return VFS.SQLITE_OK; + const lockState = this.#getLockState(fileId); + if (!(lockType < lockState.type)) return VFS.SQLITE_OK; switch (this.#options.lockPolicy) { case 'exclusive': @@ -107,7 +94,7 @@ export const WebLocksMixin = superclass => class extends superclass { */ async jCheckReservedLock(fileId, pResOut) { try { - const lockState = this.#mapIdToState.get(fileId); + const lockState = this.#getLockState(fileId); switch (this.#options.lockPolicy) { case 'exclusive': return this.#checkReservedExclusive(lockState, pResOut); @@ -130,19 +117,29 @@ export const WebLocksMixin = superclass => class extends superclass { * @returns {number|Promise} */ jFileControl(fileId, op, pArg) { - const lockState = this.#mapIdToState.get(fileId) ?? - (() => { - // Call jLock() to create the lock state. - this.jLock(fileId, VFS.SQLITE_LOCK_NONE); - return this.#mapIdToState.get(fileId); - })(); if (op === WebLocksMixin.WRITE_HINT_OP_CODE && this.#options.lockPolicy === 'shared+hint'){ + const lockState = this.#getLockState(fileId); lockState.writeHint = true; } return VFS.SQLITE_NOTFOUND; } + #getLockState(fileId) { + let lockState = this.#mapIdToState.get(fileId); + if (!lockState) { + // The state doesn't exist yet so create it. + const name = this.getFilename(fileId); + lockState = { + baseName: name, + type: VFS.SQLITE_LOCK_NONE, + writeHint: false + }; + this.#mapIdToState.set(fileId, lockState); + } + return lockState + } + /** * @param {LockState} lockState * @param {number} lockType diff --git a/src/examples/MemoryVFS.js b/src/examples/MemoryVFS.js index 9da7b71f..fd1b72c6 100644 --- a/src/examples/MemoryVFS.js +++ b/src/examples/MemoryVFS.js @@ -116,7 +116,7 @@ export class MemoryVFS extends FacadeVFS { } // Copy data. - new Uint8Array(file.data, iOffset, pData.byteLength).set(pData); + new Uint8Array(file.data, iOffset, pData.byteLength).set(pData.subarray()); file.size = Math.max(file.size, iOffset + pData.byteLength); return VFS.SQLITE_OK; } diff --git a/src/examples/OPFSCoopSyncVFS.js b/src/examples/OPFSCoopSyncVFS.js index f8b8ef2a..90630e6f 100644 --- a/src/examples/OPFSCoopSyncVFS.js +++ b/src/examples/OPFSCoopSyncVFS.js @@ -437,7 +437,7 @@ export class OPFSCoopSyncVFS extends FacadeVFS { if (file.persistentFile.isHandleRequested) { // Another connection wants the access handle. this.#releaseAccessHandle(file); - this.isHandleRequested = false; + file.persistentFile.isHandleRequested = false; } file.persistentFile.isFileLocked = false; } diff --git a/src/extra_exported_runtime_methods.json b/src/extra_exported_runtime_methods.json index d098d2bc..f7086b11 100644 --- a/src/extra_exported_runtime_methods.json +++ b/src/extra_exported_runtime_methods.json @@ -10,6 +10,8 @@ "stringToUTF16", "stringToUTF32", "AsciiToString", + "HEAP32", + "HEAPU8", "UTF8ToString", "UTF16ToString", "UTF32ToString", diff --git a/src/sqlite-api.js b/src/sqlite-api.js index 448dbbcc..2b3c04f7 100644 --- a/src/sqlite-api.js +++ b/src/sqlite-api.js @@ -34,11 +34,12 @@ export function Factory(Module) { const tmp = Module._malloc(8); const tmpPtr = [tmp, tmp + 4]; + const textEncoder = new TextEncoder(); // Convert a JS string to a C string. sqlite3_malloc is used to allocate // memory (use sqlite3_free to deallocate). function createUTF8(s) { if (typeof s !== 'string') return 0; - const utf8 = new TextEncoder().encode(s); + const utf8 = textEncoder.encode(s); const zts = Module._sqlite3_malloc(utf8.byteLength + 1); Module.HEAPU8.set(utf8, zts); Module.HEAPU8[zts + utf8.byteLength] = 0; @@ -118,6 +119,8 @@ export function Factory(Module) { } case 'string': return sqlite3.bind_text(stmt, i, value); + case "boolean": + return sqlite3.bind_int(stmt, i, value ? 1 : 0); default: if (value instanceof Uint8Array || Array.isArray(value)) { return sqlite3.bind_blob(stmt, i, value); @@ -659,7 +662,7 @@ export function Factory(Module) { const onFinally = []; try { // Encode SQL string to UTF-8. - const utf8 = new TextEncoder().encode(sql); + const utf8 = textEncoder.encode(sql); // Copy encoded string to WebAssembly memory. The SQLite docs say // zero-termination is a minor optimization so add room for that. diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index 7c4507ce..e1d2262f 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -11,7 +11,7 @@ declare function setValue(ptr: number, value: number, type: string): number; declare function mergeInto(library: object, methods: object): void; declare var HEAPU8: Uint8Array; -declare var HEAPU32: Uint32Array; +declare var HEAP32: Int32Array; declare var LibraryManager; declare var Module; declare var _vfsAccess; diff --git a/test/api_statements.js b/test/api_statements.js index d3f76d9c..830ae1ac 100644 --- a/test/api_statements.js +++ b/test/api_statements.js @@ -238,6 +238,27 @@ export function api_statements(context) { } }); + it('should bind boolean', async function() { + let rc; + const sql = 'SELECT ?'; + const storeValue = true; + const expectedRetrievedValue = 1; + + for await (const stmt of i(sqlite3.statements(db, sql))) { + // Comlink intercepts the 'bind' property so use an alias. + rc = await sqlite3.bind$(stmt, 1, storeValue); + expect(rc).toEqual(SQLite.SQLITE_OK); + + while ((rc = await sqlite3.step(stmt)) !== SQLite.SQLITE_DONE) { + expect(rc).toEqual(SQLite.SQLITE_ROW); + + expect(await sqlite3.column_count(stmt)).toEqual(1); + expect(await sqlite3.column_type(stmt, 0)).toEqual(SQLite.SQLITE_INTEGER); + expect(await sqlite3.column_int(stmt, 0)).toEqual(expectedRetrievedValue); + } + } + }); + it('should bind collection array', async function() { let rc; const sql = 'VALUES (?, ?, ?, ?, ?)'; diff --git a/yarn.lock b/yarn.lock index 8b1480ea..e40b41d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3983,8 +3983,8 @@ __metadata: linkType: hard "tar-fs@npm:^3.0.6, tar-fs@npm:^3.0.8": - version: 3.0.8 - resolution: "tar-fs@npm:3.0.8" + version: 3.0.9 + resolution: "tar-fs@npm:3.0.9" dependencies: bare-fs: "npm:^4.0.1" bare-path: "npm:^3.0.0" @@ -3995,7 +3995,7 @@ __metadata: optional: true bare-path: optional: true - checksum: fdcd1c66dc5e2cad5544ffe7eab9a470b419290b22300c344688df51bf06127963da07a1e3ae23cae80851cd9f60149e80b38e56485dd7a14aea701241ac2f81 + checksum: 00e194ef36ced339000099c3cb5205fcd9636a531158d73e0fc1ca056fbcf8dcf39a398cbc71f030bbf938d4f477f47e2913c6e37e9c007bad31e0768a120590 languageName: node linkType: hard