|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ |
|
|
|
|
|
delete sqlite3.capi.sqlite3_kvvfs_methods; |
|
|
delete sqlite3.capi.KVVfsFile; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ |
|
|
'use strict'; |
|
|
const capi = sqlite3.capi, |
|
|
sqlite3_kvvfs_methods = capi.sqlite3_kvvfs_methods, |
|
|
KVVfsFile = capi.KVVfsFile, |
|
|
pKvvfs = sqlite3.capi.sqlite3_vfs_find("kvvfs") |
|
|
|
|
|
|
|
|
delete capi.sqlite3_kvvfs_methods; |
|
|
delete capi.KVVfsFile; |
|
|
|
|
|
if( !pKvvfs ) return ; |
|
|
if( 0 ){ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
capi.sqlite3_vfs_register(pKvvfs, 1); |
|
|
} |
|
|
|
|
|
const util = sqlite3.util, |
|
|
wasm = sqlite3.wasm, |
|
|
toss3 = util.toss3, |
|
|
hop = (o,k)=>Object.prototype.hasOwnProperty.call(o,k); |
|
|
|
|
|
const kvvfsMethods = new sqlite3_kvvfs_methods( |
|
|
|
|
|
wasm.exports.sqlite3__wasm_kvvfs_methods() |
|
|
); |
|
|
util.assert( 32<=kvvfsMethods.$nKeySize, "unexpected kvvfsMethods.$nKeySize: "+kvvfsMethods.$nKeySize); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const cache = Object.assign(Object.create(null),{ |
|
|
|
|
|
rxJournalSuffix: /-journal$/, |
|
|
|
|
|
zKeyJrnl: wasm.allocCString("jrnl"), |
|
|
|
|
|
zKeySz: wasm.allocCString("sz"), |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
keySize: kvvfsMethods.$nKeySize, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
buffer: Object.assign(Object.create(null),{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
n: kvvfsMethods.$nBufferSize, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pool: Object.create(null) |
|
|
}) |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cache.memBuffer = (id=0)=>cache.buffer.pool[id] ??= wasm.alloc(cache.buffer.n); |
|
|
|
|
|
|
|
|
cache.memBufferFree = (id)=>{ |
|
|
const b = cache.buffer.pool[id]; |
|
|
if( b ){ |
|
|
wasm.dealloc(b); |
|
|
delete cache.buffer.pool[id]; |
|
|
} |
|
|
}; |
|
|
|
|
|
const noop = ()=>{}; |
|
|
const debug = sqlite3.__isUnderTest |
|
|
? (...args)=>sqlite3.config.debug("kvvfs:", ...args) |
|
|
: noop; |
|
|
const warn = (...args)=>sqlite3.config.warn("kvvfs:", ...args); |
|
|
const error = (...args)=>sqlite3.config.error("kvvfs:", ...args); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KVVfsStorage { |
|
|
#map; |
|
|
#keys; |
|
|
#getKeys(){return this.#keys ??= Object.keys(this.#map);} |
|
|
|
|
|
constructor(){ |
|
|
this.clear(); |
|
|
} |
|
|
|
|
|
key(n){ |
|
|
const k = this.#getKeys(); |
|
|
return n<k.length ? k[n] : null; |
|
|
} |
|
|
|
|
|
getItem(k){ |
|
|
return this.#map[k] ?? null; |
|
|
} |
|
|
|
|
|
setItem(k,v){ |
|
|
if( !hop(this.#map, k) ){ |
|
|
this.#keys = null; |
|
|
} |
|
|
this.#map[k] = ''+v; |
|
|
} |
|
|
|
|
|
removeItem(k){ |
|
|
if( delete this.#map[k] ){ |
|
|
this.#keys = null; |
|
|
} |
|
|
} |
|
|
|
|
|
clear(){ |
|
|
this.#map = Object.create(null); |
|
|
this.#keys = null; |
|
|
} |
|
|
|
|
|
get length() { |
|
|
return this.#getKeys().length; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const kvvfsIsPersistentName = (v)=>'local'===v || 'session'===v; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const kvvfsKeyPrefix = (v)=>kvvfsIsPersistentName(v) ? 'kvvfs-'+v+'-' : ''; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const validateStorageName = function(n,mayBeJournal=false){ |
|
|
if( kvvfsIsPersistentName(n) ) return; |
|
|
const len = (new Blob([n])).size; |
|
|
if( !len ) toss3(capi.SQLITE_MISUSE, "Empty name is not permitted."); |
|
|
let maxLen = cache.keySize - 1; |
|
|
if( cache.rxJournalSuffix.test(n) ){ |
|
|
if( !mayBeJournal ){ |
|
|
toss3(capi.SQLITE_MISUSE, |
|
|
"Storage names may not have a '-journal' suffix."); |
|
|
} |
|
|
}else if( ['-wal','-shm'].filter(v=>n.endsWith(v)).length ){ |
|
|
toss3(capi.SQLITE_MISUSE, |
|
|
"Storage names may not have a -wal or -shm suffix."); |
|
|
}else{ |
|
|
maxLen -= 8 ; |
|
|
} |
|
|
if( len > maxLen ){ |
|
|
toss3(capi.SQLITE_RANGE, "Storage name is too long. Limit =", maxLen); |
|
|
} |
|
|
let i; |
|
|
for( i = 0; i < len; ++i ){ |
|
|
const ch = n.codePointAt(i); |
|
|
if( ch<32 ){ |
|
|
toss3(capi.SQLITE_RANGE, |
|
|
"Illegal character ("+ch+"d) in storage name:",n); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const newStorageObj = (name,storage=undefined)=>Object.assign(Object.create(null),{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
jzClass: name, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
refc: 1, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deleteAtRefc0: false, |
|
|
|
|
|
|
|
|
|
|
|
storage: storage || new KVVfsStorage, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
keyPrefix: kvvfsKeyPrefix(name), |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
files: [], |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
listeners: undefined |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const kvvfs = sqlite3.kvvfs = Object.create(null); |
|
|
if( sqlite3.__isUnderTest ){ |
|
|
|
|
|
kvvfs.log = Object.assign(Object.create(null),{ |
|
|
xOpen: false, |
|
|
xClose: false, |
|
|
xWrite: false, |
|
|
xRead: false, |
|
|
xSync: false, |
|
|
xAccess: false, |
|
|
xFileControl: false, |
|
|
xRcrdRead: false, |
|
|
xRcrdWrite: false, |
|
|
xRcrdDelete: false, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const deleteStorage = function(store){ |
|
|
const other = cache.rxJournalSuffix.test(store.jzClass) |
|
|
? store.jzClass.replace(cache.rxJournalSuffix,'') |
|
|
: store.jzClass+'-journal'; |
|
|
kvvfs?.log?.xClose |
|
|
&& debug("cleaning up storage handles [", store.jzClass, other,"]",store); |
|
|
delete cache.storagePool[store.jzClass]; |
|
|
delete cache.storagePool[other]; |
|
|
if( !sqlite3.__isUnderTest ){ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
delete store.storage; |
|
|
delete store.refc; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const installStorageAndJournal = (store)=> |
|
|
cache.storagePool[store.jzClass] = |
|
|
cache.storagePool[store.jzClass+'-journal'] = store; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const nameOfThisThreadStorage = '.'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cache.storagePool = Object.assign(Object.create(null),{ |
|
|
|
|
|
[nameOfThisThreadStorage]: newStorageObj(nameOfThisThreadStorage) |
|
|
}); |
|
|
|
|
|
if( globalThis.Storage ){ |
|
|
|
|
|
if( globalThis.localStorage instanceof globalThis.Storage ){ |
|
|
cache.storagePool.local = newStorageObj('local', globalThis.localStorage); |
|
|
} |
|
|
if( globalThis.sessionStorage instanceof globalThis.Storage ){ |
|
|
cache.storagePool.session = newStorageObj('session', globalThis.sessionStorage); |
|
|
} |
|
|
} |
|
|
|
|
|
cache.builtinStorageNames = Object.keys(cache.storagePool); |
|
|
|
|
|
const isBuiltinName = (n)=>cache.builtinStorageNames.indexOf(n)>-1; |
|
|
|
|
|
|
|
|
for(const k of Object.keys(cache.storagePool)){ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const orig = cache.storagePool[k]; |
|
|
cache.storagePool[k+'-journal'] = orig; |
|
|
} |
|
|
|
|
|
cache.setError = (e=undefined, dfltErrCode=capi.SQLITE_ERROR)=>{ |
|
|
if( e ){ |
|
|
cache.lastError = e; |
|
|
return (e.resultCode | 0) || dfltErrCode; |
|
|
} |
|
|
delete cache.lastError; |
|
|
return 0; |
|
|
}; |
|
|
|
|
|
cache.popError = ()=>{ |
|
|
const e = cache.lastError; |
|
|
delete cache.lastError; |
|
|
return e; |
|
|
}; |
|
|
|
|
|
|
|
|
const catchForNotify = (e)=>{ |
|
|
warn("kvvfs.listener handler threw:",e); |
|
|
}; |
|
|
|
|
|
const kvvfsDecode = wasm.exports.sqlite3__wasm_kvvfs_decode; |
|
|
const kvvfsEncode = wasm.exports.sqlite3__wasm_kvvfs_encode; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const notifyListeners = async function(eventName,store,...args){ |
|
|
try{ |
|
|
|
|
|
if( store.keyPrefix && args[0] ){ |
|
|
args[0] = args[0].replace(store.keyPrefix,''); |
|
|
} |
|
|
let u8enc, z0, z1, wcache; |
|
|
for(const ear of store.listeners){ |
|
|
const ev = Object.create(null); |
|
|
ev.storageName = store.jzClass; |
|
|
ev.type = eventName; |
|
|
const decodePages = ear.decodePages; |
|
|
const f = ear.events[eventName]; |
|
|
if( f ){ |
|
|
if( !ear.includeJournal && args[0]==='jrnl' ){ |
|
|
continue; |
|
|
} |
|
|
if( 'write'===eventName && ear.decodePages && +args[0]>0 ){ |
|
|
|
|
|
|
|
|
ev.data = [args[0]]; |
|
|
if( wcache?.[args[0]] ){ |
|
|
ev.data[1] = wcache[args[0]]; |
|
|
continue; |
|
|
} |
|
|
u8enc ??= new TextEncoder('utf-8'); |
|
|
z0 ??= cache.memBuffer(10); |
|
|
z1 ??= cache.memBuffer(11); |
|
|
const u = u8enc.encode(args[1]); |
|
|
const heap = wasm.heap8u(); |
|
|
heap.set(u, Number(z0)); |
|
|
heap[wasm.ptr.addn(z0, u.length)] = 0; |
|
|
const rc = kvvfsDecode(z0, z1, cache.buffer.n); |
|
|
if( rc>0 ){ |
|
|
wcache ??= Object.create(null); |
|
|
wcache[args[0]] |
|
|
= ev.data[1] |
|
|
= heap.slice(Number(z1), wasm.ptr.addn(z1,rc)); |
|
|
}else{ |
|
|
continue; |
|
|
} |
|
|
}else{ |
|
|
ev.data = args.length |
|
|
? ((args.length===1) ? args[0] : args) |
|
|
: undefined; |
|
|
} |
|
|
try{f(ev)?.catch?.(catchForNotify)} |
|
|
catch(e){ |
|
|
warn("notifyListeners [",store.jzClass,"]",eventName,e); |
|
|
} |
|
|
} |
|
|
} |
|
|
}catch(e){ |
|
|
catchForNotify(e); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const storageForZClass = (zClass)=> |
|
|
'string'===typeof zClass |
|
|
? cache.storagePool[zClass] |
|
|
: cache.storagePool[wasm.cstrToJs(zClass)]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fileForDb = function(pDb){ |
|
|
const stack = wasm.pstack.pointer; |
|
|
try{ |
|
|
const pOut = wasm.pstack.allocPtr(); |
|
|
return wasm.exports.sqlite3_file_control( |
|
|
pDb, wasm.ptr.null, capi.SQLITE_FCNTL_FILE_POINTER, pOut |
|
|
) |
|
|
? null |
|
|
: new KVVfsFile(wasm.peekPtr(pOut)); |
|
|
}finally{ |
|
|
wasm.pstack.restore(stack); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const alertFilesToReload = (store)=>{ |
|
|
try{ |
|
|
for( const f of store.files ){ |
|
|
|
|
|
|
|
|
f.$szPage = -1; |
|
|
f.$szDb = -1n |
|
|
} |
|
|
}catch(e){ |
|
|
error("alertFilesToReload()",store,e); |
|
|
throw e; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const kvvfsMakeKey = wasm.exports.sqlite3__wasm_kvvfsMakeKey; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const zKeyForStorage = (store, zClass, zKey)=>{ |
|
|
|
|
|
return (zClass && store.keyPrefix) ? kvvfsMakeKey(zClass, zKey) : zKey; |
|
|
}; |
|
|
|
|
|
const jsKeyForStorage = (store,zClass,zKey)=> |
|
|
wasm.cstrToJs(zKeyForStorage(store, zClass, zKey)); |
|
|
|
|
|
const storageGetDbSize = (store)=>+store.storage.getItem(store.keyPrefix + "sz"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const pFileHandles = new Map(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const originalMethods = { |
|
|
vfs: Object.create(null), |
|
|
ioDb: Object.create(null), |
|
|
ioJrnl: Object.create(null) |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const originalIoMethods = (kvvfsFile)=> |
|
|
originalMethods[kvvfsFile.$isJournal ? 'ioJrnl' : 'ioDb']; |
|
|
|
|
|
const pVfs = new capi.sqlite3_vfs(kvvfsMethods.$pVfs); |
|
|
const pIoDb = new capi.sqlite3_io_methods(kvvfsMethods.$pIoDb); |
|
|
const pIoJrnl = new capi.sqlite3_io_methods(kvvfsMethods.$pIoJrnl); |
|
|
const recordHandler = |
|
|
Object.create(null) |
|
|
; |
|
|
const kvvfsInternal = Object.assign(Object.create(null),{ |
|
|
pFileHandles, |
|
|
cache, |
|
|
storageForZClass, |
|
|
KVVfsStorage, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
disablePageSizeChange: true |
|
|
}); |
|
|
if( kvvfs.log ){ |
|
|
|
|
|
kvvfs.internal = kvvfsInternal; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const methodOverrides = { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
recordHandler: { |
|
|
xRcrdRead: (zClass, zKey, zBuf, nBuf)=>{ |
|
|
try{ |
|
|
const jzClass = wasm.cstrToJs(zClass); |
|
|
const store = storageForZClass(jzClass); |
|
|
if( !store ) return -1; |
|
|
const jXKey = jsKeyForStorage(store, zClass, zKey); |
|
|
kvvfs?.log?.xRcrdRead && warn("xRcrdRead", jzClass, jXKey, nBuf, store ); |
|
|
const jV = store.storage.getItem(jXKey); |
|
|
if(null===jV) return -1; |
|
|
const nV = jV.length |
|
|
|
|
|
; |
|
|
if( 0 ){ |
|
|
debug("xRcrdRead", jXKey, store, jV); |
|
|
} |
|
|
if(nBuf<=0) return nV; |
|
|
else if(1===nBuf){ |
|
|
wasm.poke(zBuf, 0); |
|
|
return nV; |
|
|
} |
|
|
if( nBuf+1<nV ){ |
|
|
toss3(capi.SQLITE_RANGE, |
|
|
"xRcrdRead()",jzClass,jXKey, |
|
|
"input buffer is too small: need", |
|
|
nV,"but have",nBuf); |
|
|
} |
|
|
if( 0 ){ |
|
|
debug("xRcrdRead", nBuf, zClass, wasm.cstrToJs(zClass), |
|
|
wasm.cstrToJs(zKey), nV, jV, store); |
|
|
} |
|
|
const zV = cache.memBuffer(0); |
|
|
|
|
|
const heap = wasm.heap8(); |
|
|
let i; |
|
|
for(i = 0; i < nV; ++i){ |
|
|
heap[wasm.ptr.add(zV,i)] = jV.codePointAt(i) & 0xFF; |
|
|
} |
|
|
heap.copyWithin( |
|
|
Number(zBuf), Number(zV), wasm.ptr.addn(zV, i) |
|
|
); |
|
|
heap[wasm.ptr.add(zBuf, nV)] = 0; |
|
|
return nBuf; |
|
|
}catch(e){ |
|
|
error("kvrecordRead()",e); |
|
|
cache.setError(e); |
|
|
return -2; |
|
|
} |
|
|
}, |
|
|
|
|
|
xRcrdWrite: (zClass, zKey, zData)=>{ |
|
|
try { |
|
|
const store = storageForZClass(zClass); |
|
|
const jxKey = jsKeyForStorage(store, zClass, zKey); |
|
|
const jData = wasm.cstrToJs(zData); |
|
|
kvvfs?.log?.xRcrdWrite && warn("xRcrdWrite",jxKey, store); |
|
|
store.storage.setItem(jxKey, jData); |
|
|
store.listeners && notifyListeners('write', store, jxKey, jData); |
|
|
return 0; |
|
|
}catch(e){ |
|
|
error("kvrecordWrite()",e); |
|
|
return cache.setError(e, capi.SQLITE_IOERR); |
|
|
} |
|
|
}, |
|
|
|
|
|
xRcrdDelete: (zClass, zKey)=>{ |
|
|
try { |
|
|
const store = storageForZClass(zClass); |
|
|
const jxKey = jsKeyForStorage(store, zClass, zKey); |
|
|
kvvfs?.log?.xRcrdDelete && warn("xRcrdDelete",jxKey, store); |
|
|
store.storage.removeItem(jxKey); |
|
|
store.listeners && notifyListeners('delete', store, jxKey); |
|
|
return 0; |
|
|
}catch(e){ |
|
|
error("kvrecordDelete()",e); |
|
|
return cache.setError(e, capi.SQLITE_IOERR); |
|
|
} |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vfs:{ |
|
|
|
|
|
xOpen: function(pProtoVfs,zName,pProtoFile,flags,pOutFlags){ |
|
|
cache.popError(); |
|
|
let zToFree ; |
|
|
if( 0 ){ |
|
|
|
|
|
flags |= capi.SQLITE_OPEN_CREATE; |
|
|
} |
|
|
try{ |
|
|
if( !zName ){ |
|
|
zToFree = wasm.allocCString(""+pProtoFile+"." |
|
|
+(Math.random() * 100000 | 0)); |
|
|
zName = zToFree; |
|
|
} |
|
|
const jzClass = wasm.cstrToJs(zName); |
|
|
kvvfs?.log?.xOpen && debug("xOpen",jzClass,"flags =",flags); |
|
|
validateStorageName(jzClass, true); |
|
|
if( (flags & (capi.SQLITE_OPEN_MAIN_DB |
|
|
| capi.SQLITE_OPEN_TEMP_DB |
|
|
| capi.SQLITE_OPEN_TRANSIENT_DB)) |
|
|
&& cache.rxJournalSuffix.test(jzClass) ){ |
|
|
toss3(capi.SQLITE_ERROR, |
|
|
"DB files may not have a '-journal' suffix."); |
|
|
} |
|
|
let s = storageForZClass(jzClass); |
|
|
if( !s && !(flags & capi.SQLITE_OPEN_CREATE) ){ |
|
|
toss3(capi.SQLITE_ERROR, "Storage not found:", jzClass); |
|
|
} |
|
|
const rc = originalMethods.vfs.xOpen(pProtoVfs, zName, pProtoFile, |
|
|
flags, pOutFlags); |
|
|
if( rc ) return rc; |
|
|
let deleteAt0 = !!(capi.SQLITE_OPEN_DELETEONCLOSE & flags); |
|
|
if(wasm.isPtr(arguments[1])){ |
|
|
if(capi.sqlite3_uri_boolean(zName, "delete-on-close", 0)){ |
|
|
deleteAt0 = true; |
|
|
} |
|
|
} |
|
|
const f = new KVVfsFile(pProtoFile); |
|
|
util.assert(f.$zClass, "Missing f.$zClass"); |
|
|
f.addOnDispose(zToFree); |
|
|
zToFree = undefined; |
|
|
|
|
|
if( s ){ |
|
|
++s.refc; |
|
|
|
|
|
s.files.push(f); |
|
|
wasm.poke32(pOutFlags, flags); |
|
|
}else{ |
|
|
wasm.poke32(pOutFlags, flags | capi.SQLITE_OPEN_CREATE); |
|
|
util.assert( !f.$isJournal, "Opening a journal before its db? "+jzClass ); |
|
|
|
|
|
const nm = jzClass.replace(cache.rxJournalSuffix,''); |
|
|
s = newStorageObj(nm); |
|
|
installStorageAndJournal(s); |
|
|
s.files.push(f); |
|
|
s.deleteAtRefc0 = deleteAt0; |
|
|
kvvfs?.log?.xOpen |
|
|
&& debug("xOpen installed storage handle [",nm, nm+"-journal","]", s); |
|
|
} |
|
|
pFileHandles.set(pProtoFile, {store: s, file: f, jzClass}); |
|
|
s.listeners && notifyListeners('open', s, s.files.length); |
|
|
return 0; |
|
|
}catch(e){ |
|
|
warn("xOpen:",e); |
|
|
return cache.setError(e); |
|
|
}finally{ |
|
|
zToFree && wasm.dealloc(zToFree); |
|
|
} |
|
|
}, |
|
|
|
|
|
xDelete: function(pVfs, zName, iSyncFlag){ |
|
|
cache.popError(); |
|
|
try{ |
|
|
const jzName = wasm.cstrToJs(zName); |
|
|
if( cache.rxJournalSuffix.test(jzName) ){ |
|
|
recordHandler.xRcrdDelete(zName, cache.zKeyJrnl); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return 0; |
|
|
}catch(e){ |
|
|
warn("xDelete",e); |
|
|
return cache.setError(e); |
|
|
} |
|
|
}, |
|
|
|
|
|
xAccess: function(pProtoVfs, zPath, flags, pResOut){ |
|
|
cache.popError(); |
|
|
try{ |
|
|
const s = storageForZClass(zPath); |
|
|
const jzPath = s?.jzClass || wasm.cstrToJs(zPath); |
|
|
if( kvvfs?.log?.xAccess ){ |
|
|
debug("xAccess",jzPath,"flags =", |
|
|
flags,"*pResOut =",wasm.peek32(pResOut), |
|
|
"store =",s); |
|
|
} |
|
|
if( !s ){ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try{validateStorageName(jzPath)} |
|
|
catch(e){ |
|
|
|
|
|
wasm.poke32(pResOut, 0); |
|
|
return 0; |
|
|
} |
|
|
} |
|
|
if( s ){ |
|
|
const key = s.keyPrefix+ |
|
|
(cache.rxJournalSuffix.test(jzPath) ? "jrnl" : "1"); |
|
|
const res = s.storage.getItem(key) ? 0 : 1; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
wasm.poke32(pResOut, res); |
|
|
}else{ |
|
|
wasm.poke32(pResOut, 0); |
|
|
} |
|
|
return 0; |
|
|
}catch(e){ |
|
|
error('xAccess',e); |
|
|
return cache.setError(e); |
|
|
} |
|
|
}, |
|
|
|
|
|
xRandomness: function(pVfs, nOut, pOut){ |
|
|
const heap = wasm.heap8u(); |
|
|
let i = 0; |
|
|
const npOut = Number(pOut); |
|
|
for(; i < nOut; ++i) heap[npOut + i] = (Math.random()*255000) & 0xFF; |
|
|
return nOut; |
|
|
}, |
|
|
|
|
|
xGetLastError: function(pVfs,nOut,pOut){ |
|
|
const e = cache.popError(); |
|
|
debug('xGetLastError',e); |
|
|
if(e){ |
|
|
const scope = wasm.scopedAllocPush(); |
|
|
try{ |
|
|
const [cMsg, n] = wasm.scopedAllocCString(e.message, true); |
|
|
wasm.cstrncpy(pOut, cMsg, nOut); |
|
|
if(n > nOut) wasm.poke8(wasm.ptr.add(pOut,nOut,-1), 0); |
|
|
debug("set xGetLastError",e.message); |
|
|
return (e.resultCode | 0) || capi.SQLITE_IOERR; |
|
|
}catch(e){ |
|
|
return capi.SQLITE_NOMEM; |
|
|
}finally{ |
|
|
wasm.scopedAllocPop(scope); |
|
|
} |
|
|
} |
|
|
return 0; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
xCurrentTime: function(pVfs,pOut){ |
|
|
wasm.poke64f(pOut, 2440587.5 + (Date.now()/86400000)); |
|
|
return 0; |
|
|
}, |
|
|
|
|
|
xCurrentTimeInt64: function(pVfs,pOut){ |
|
|
wasm.poke64(pOut, (2440587.5 * 86400000) + Date.now()); |
|
|
return 0; |
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ioDb:{ |
|
|
|
|
|
xClose: function(pFile){ |
|
|
cache.popError(); |
|
|
try{ |
|
|
const h = pFileHandles.get(pFile); |
|
|
kvvfs?.log?.xClose && debug("xClose", pFile, h); |
|
|
if( h ){ |
|
|
pFileHandles.delete(pFile); |
|
|
const s = h.store; |
|
|
s.files = s.files.filter((v)=>v!==h.file); |
|
|
if( --s.refc<=0 && s.deleteAtRefc0 ){ |
|
|
deleteStorage(s); |
|
|
} |
|
|
originalMethods.ioDb.xClose(pFile); |
|
|
h.file.dispose(); |
|
|
s.listeners && notifyListeners('close', s, s.files.length); |
|
|
}else{ |
|
|
|
|
|
} |
|
|
return 0; |
|
|
}catch(e){ |
|
|
error("xClose",e); |
|
|
return cache.setError(e); |
|
|
} |
|
|
}, |
|
|
|
|
|
xFileControl: function(pFile, opId, pArg){ |
|
|
cache.popError(); |
|
|
try{ |
|
|
const h = pFileHandles.get(pFile); |
|
|
util.assert(h, "Missing KVVfsFile handle"); |
|
|
kvvfs?.log?.xFileControl && debug("xFileControl",h,'op =',opId); |
|
|
if( opId===capi.SQLITE_FCNTL_PRAGMA |
|
|
&& kvvfs.internal.disablePageSizeChange ){ |
|
|
|
|
|
|
|
|
const zName = wasm.peekPtr(wasm.ptr.add(pArg, wasm.ptr.size)); |
|
|
if( "page_size"===wasm.cstrToJs(zName) ){ |
|
|
kvvfs?.log?.xFileControl |
|
|
&& debug("xFileControl pragma",wasm.cstrToJs(zName)); |
|
|
const zVal = wasm.peekPtr(wasm.ptr.add(pArg, 2*wasm.ptr.size)); |
|
|
if( zVal ){ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
kvvfs?.log?.xFileControl |
|
|
&& warn("xFileControl pragma", h, |
|
|
"NOT setting page size to", wasm.cstrToJs(zVal)); |
|
|
h.file.$szPage = -1; |
|
|
return 0; |
|
|
}else if( h.file.$szPage>0 ){ |
|
|
kvvfs?.log?.xFileControl && |
|
|
warn("xFileControl", h, "getting page size",h.file.$szPage); |
|
|
wasm.pokePtr(pArg, wasm.allocCString(""+h.file.$szPage) |
|
|
); |
|
|
return 0; |
|
|
} |
|
|
} |
|
|
} |
|
|
const rc = originalMethods.ioDb.xFileControl(pFile, opId, pArg); |
|
|
if( 0==rc && capi.SQLITE_FCNTL_SYNC===opId ){ |
|
|
h.store.listeners && notifyListeners('sync', h.store, false); |
|
|
} |
|
|
return rc; |
|
|
}catch(e){ |
|
|
error("xFileControl",e); |
|
|
return cache.setError(e); |
|
|
} |
|
|
}, |
|
|
|
|
|
xSync: function(pFile,flags){ |
|
|
cache.popError(); |
|
|
try{ |
|
|
const h = pFileHandles.get(pFile); |
|
|
kvvfs?.log?.xSync && debug("xSync", h); |
|
|
util.assert(h, "Missing KVVfsFile handle"); |
|
|
const rc = originalMethods.ioDb.xSync(pFile, flags); |
|
|
if( 0==rc && h.store.listeners ) notifyListeners('sync', h.store, true); |
|
|
return rc; |
|
|
}catch(e){ |
|
|
error("xSync",e); |
|
|
return cache.setError(e); |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
xRead: function(pFile,pTgt,n,iOff64){ |
|
|
cache.popError(); |
|
|
try{ |
|
|
if( kvvfs?.log?.xRead ){ |
|
|
const h = pFileHandles.get(pFile); |
|
|
util.assert(h, "Missing KVVfsFile handle"); |
|
|
debug("xRead", n, iOff64, h); |
|
|
} |
|
|
return originalMethods.ioDb.xRead(pFile, pTgt, n, iOff64); |
|
|
}catch(e){ |
|
|
error("xRead",e); |
|
|
return cache.setError(e); |
|
|
} |
|
|
}, |
|
|
xWrite: function(pFile,pSrc,n,iOff64){ |
|
|
cache.popError(); |
|
|
try{ |
|
|
if( kvvfs?.log?.xWrite ){ |
|
|
const h = pFileHandles.get(pFile); |
|
|
util.assert(h, "Missing KVVfsFile handle"); |
|
|
debug("xWrite", n, iOff64, h); |
|
|
} |
|
|
return originalMethods.ioDb.xWrite(pFile, pSrc, n, iOff64); |
|
|
}catch(e){ |
|
|
error("xWrite",e); |
|
|
return cache.setError(e); |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
xTruncate: function(pFile,i64){}, |
|
|
xFileSize: function(pFile,pi64Out){}, |
|
|
xLock: function(pFile,iLock){}, |
|
|
xUnlock: function(pFile,iLock){}, |
|
|
xCheckReservedLock: function(pFile,piOut){}, |
|
|
xSectorSize: function(pFile){}, |
|
|
xDeviceCharacteristics: function(pFile){} |
|
|
|
|
|
}, |
|
|
|
|
|
ioJrnl:{ |
|
|
|
|
|
|
|
|
|
|
|
xClose: true, |
|
|
|
|
|
xRead: function(pFile,pTgt,n,iOff64){}, |
|
|
xWrite: function(pFile,pSrc,n,iOff64){}, |
|
|
xTruncate: function(pFile,i64){}, |
|
|
xSync: function(pFile,flags){}, |
|
|
xFileControl: function(pFile, opId, pArg){}, |
|
|
xFileSize: function(pFile,pi64Out){}, |
|
|
xLock: true, |
|
|
xUnlock: true, |
|
|
xCheckReservedLock: true, |
|
|
xSectorSize: true, |
|
|
xDeviceCharacteristics: true |
|
|
|
|
|
} |
|
|
}; |
|
|
|
|
|
debug("pVfs and friends", pVfs, pIoDb, pIoJrnl, |
|
|
kvvfsMethods, capi.sqlite3_file.structInfo, |
|
|
KVVfsFile.structInfo); |
|
|
try { |
|
|
util.assert( cache.buffer.n>1024*129, "Heap buffer is not large enough" |
|
|
); |
|
|
for(const e of Object.entries(methodOverrides.recordHandler)){ |
|
|
|
|
|
const k = e[0], f = e[1]; |
|
|
recordHandler[k] = f; |
|
|
if( 0 ){ |
|
|
|
|
|
kvvfsMethods.installMethod(k, f); |
|
|
}else{ |
|
|
kvvfsMethods[kvvfsMethods.memberKey(k)] = |
|
|
wasm.installFunction(kvvfsMethods.memberSignature(k), f); |
|
|
} |
|
|
} |
|
|
for(const e of Object.entries(methodOverrides.vfs)){ |
|
|
|
|
|
const k = e[0], f = e[1], km = pVfs.memberKey(k), |
|
|
member = pVfs.structInfo.members[k] |
|
|
|| util.toss("Missing pVfs.structInfo[",k,"]"); |
|
|
originalMethods.vfs[k] = wasm.functionEntry(pVfs[km]); |
|
|
pVfs[km] = wasm.installFunction(member.signature, f); |
|
|
} |
|
|
for(const e of Object.entries(methodOverrides.ioDb)){ |
|
|
|
|
|
const k = e[0], f = e[1], km = pIoDb.memberKey(k); |
|
|
originalMethods.ioDb[k] = wasm.functionEntry(pIoDb[km]) |
|
|
|| util.toss("Missing native pIoDb[",km,"]"); |
|
|
pIoDb[km] = wasm.installFunction(pIoDb.memberSignature(k), f); |
|
|
} |
|
|
for(const e of Object.entries(methodOverrides.ioJrnl)){ |
|
|
|
|
|
const k = e[0], f = e[1], km = pIoJrnl.memberKey(k); |
|
|
originalMethods.ioJrnl[k] = wasm.functionEntry(pIoJrnl[km]) |
|
|
|| util.toss("Missing native pIoJrnl[",km,"]"); |
|
|
if( true===f ){ |
|
|
|
|
|
pIoJrnl[km] = pIoDb[km] || util.toss("Missing copied pIoDb[",km,"]"); |
|
|
}else{ |
|
|
pIoJrnl[km] = wasm.installFunction(pIoJrnl.memberSignature(k), f); |
|
|
} |
|
|
} |
|
|
}finally{ |
|
|
kvvfsMethods.dispose(); |
|
|
pVfs.dispose(); |
|
|
pIoDb.dispose(); |
|
|
pIoJrnl.dispose(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sqlite3_js_kvvfs_clear = function callee(which){ |
|
|
if( ''===which ){ |
|
|
return callee('local') + callee('session'); |
|
|
} |
|
|
const store = storageForZClass(which); |
|
|
if( !store ) return 0; |
|
|
if( store.files.length ){ |
|
|
if( globalThis.localStorage===store.storage |
|
|
|| globalThis.sessionStorage===store.storage ){ |
|
|
|
|
|
|
|
|
}else{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
toss3(capi.SQLITE_ACCESS, |
|
|
"Cannot clear in-use database storage."); |
|
|
} |
|
|
} |
|
|
const s = store.storage; |
|
|
const toRm = [] ; |
|
|
let i, n = s.length; |
|
|
|
|
|
for( i = 0; i < n; ++i ){ |
|
|
const k = s.key(i); |
|
|
|
|
|
if(!store.keyPrefix || k.startsWith(store.keyPrefix)) toRm.push(k); |
|
|
} |
|
|
toRm.forEach((kk)=>s.removeItem(kk)); |
|
|
|
|
|
return toRm.length; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sqlite3_js_kvvfs_size = function callee(which){ |
|
|
if( ''===which ){ |
|
|
return callee('local') + callee('session'); |
|
|
} |
|
|
const store = storageForZClass(which); |
|
|
if( !store ) return 0; |
|
|
const s = store.storage; |
|
|
let i, sz = 0; |
|
|
for(i = 0; i < s.length; ++i){ |
|
|
const k = s.key(i); |
|
|
if(!store.keyPrefix || k.startsWith(store.keyPrefix)){ |
|
|
sz += k.length; |
|
|
sz += s.getItem(k).length; |
|
|
} |
|
|
} |
|
|
return sz * 2 ; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sqlite3_js_kvvfs_export = function callee(...args){ |
|
|
let opt; |
|
|
if( 1===args.length && 'object'===typeof args[0] ){ |
|
|
opt = args[0]; |
|
|
}else if(args.length){ |
|
|
opt = Object.assign(Object.create(null),{ |
|
|
name: args[0], |
|
|
|
|
|
}); |
|
|
} |
|
|
const store = opt ? storageForZClass(opt.name) : null; |
|
|
if( !store ){ |
|
|
toss3(capi.SQLITE_NOTFOUND, |
|
|
"There is no kvvfs storage named",opt?.name); |
|
|
} |
|
|
|
|
|
const s = store.storage; |
|
|
const rc = Object.assign(Object.create(null),{ |
|
|
name: store.jzClass, |
|
|
timestamp: Date.now(), |
|
|
pages: [] |
|
|
}); |
|
|
const pages = Object.create(null); |
|
|
let xpages; |
|
|
const keyPrefix = store.keyPrefix; |
|
|
const rxTail = keyPrefix |
|
|
? /^kvvfs-[^-]+-(\w+)/ |
|
|
: undefined; |
|
|
let i = 0, n = s.length; |
|
|
for( ; i < n; ++i ){ |
|
|
const k = s.key(i); |
|
|
if( !keyPrefix || k.startsWith(keyPrefix) ){ |
|
|
let kk = (keyPrefix ? rxTail.exec(k) : undefined)?.[1] ?? k; |
|
|
switch( kk ){ |
|
|
case 'jrnl': |
|
|
if( opt.includeJournal ) rc.journal = s.getItem(k); |
|
|
break; |
|
|
case 'sz': |
|
|
rc.size = +s.getItem(k); |
|
|
break; |
|
|
default: |
|
|
kk = +kk ; |
|
|
if( !util.isInt32(kk) || kk<=0 ){ |
|
|
toss3(capi.SQLITE_RANGE, "Malformed kvvfs key: "+k); |
|
|
} |
|
|
if( opt.decodePages ){ |
|
|
const spg = s.getItem(k), |
|
|
n = spg.length, |
|
|
z = cache.memBuffer(0), |
|
|
zDec = cache.memBuffer(1), |
|
|
heap = wasm.heap8u(); |
|
|
let i = 0; |
|
|
for( ; i < n; ++i ){ |
|
|
heap[wasm.ptr.add(z, i)] = spg.codePointAt(i) & 0xff; |
|
|
} |
|
|
heap[wasm.ptr.add(z, i)] = 0; |
|
|
|
|
|
const nDec = kvvfsDecode( |
|
|
z, zDec, cache.buffer.n |
|
|
); |
|
|
|
|
|
pages[kk] = heap.slice(Number(zDec), wasm.ptr.addn(zDec, nDec)); |
|
|
}else{ |
|
|
pages[kk] = s.getItem(k); |
|
|
} |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
if( opt.decodePages ) cache.memBufferFree(1); |
|
|
|
|
|
|
|
|
|
|
|
Object.keys(pages).map((v)=>+v).sort().forEach( |
|
|
(v)=>rc.pages.push(pages[v]) |
|
|
); |
|
|
return rc; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sqlite3_js_kvvfs_import = function(exp, overwrite=false){ |
|
|
if( !exp?.timestamp |
|
|
|| !exp.name |
|
|
|| undefined===exp.size |
|
|
|| !Array.isArray(exp.pages) ){ |
|
|
toss3(capi.SQLITE_MISUSE, "Malformed export object."); |
|
|
}else if( !exp.size |
|
|
|| (exp.size !== (exp.size | 0)) |
|
|
|
|
|
|| exp.size>=0x7fffffff ){ |
|
|
toss3(capi.SQLITE_RANGE, "Invalid db size: "+exp.size); |
|
|
} |
|
|
|
|
|
validateStorageName(exp.name); |
|
|
let store = storageForZClass(exp.name); |
|
|
const isNew = !store; |
|
|
if( store ){ |
|
|
if( !overwrite ){ |
|
|
|
|
|
toss3(capi.SQLITE_ACCESS, |
|
|
"Storage '"+exp.name+"' already exists and", |
|
|
"overwrite was not specified."); |
|
|
}else if( !store.files || !store.jzClass ){ |
|
|
toss3(capi.SQLITE_ERROR, |
|
|
"Internal storage object", exp.name,"seems to be malformed."); |
|
|
}else if( store.files.length ){ |
|
|
toss3(capi.SQLITE_IOERR_ACCESS, |
|
|
"Cannot import db storage while it is in use."); |
|
|
} |
|
|
sqlite3_js_kvvfs_clear(exp.name); |
|
|
}else{ |
|
|
store = newStorageObj(exp.name); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const keyPrefix = kvvfsKeyPrefix(exp.name); |
|
|
let zEnc; |
|
|
try{ |
|
|
|
|
|
; |
|
|
const s = store.storage; |
|
|
s.setItem(keyPrefix+'sz', exp.size); |
|
|
if( exp.journal ) s.setItem(keyPrefix+'jrnl', exp.journal); |
|
|
if( exp.pages[0] instanceof Uint8Array ){ |
|
|
|
|
|
|
|
|
exp.pages.forEach((u,ndx)=>{ |
|
|
const n = u.length; |
|
|
if( 0 && cache.fixedPageSize !== n ){ |
|
|
util.toss3(capi.SQLITE_RANGE,"Unexpected page size:", n); |
|
|
} |
|
|
zEnc ??= cache.memBuffer(1); |
|
|
const zBin = cache.memBuffer(0), |
|
|
heap = wasm.heap8u(); |
|
|
|
|
|
|
|
|
|
|
|
heap.set(u, Number(zBin)); |
|
|
heap[wasm.ptr.addn(zBin,n)] = 0; |
|
|
const rc = kvvfsEncode(zBin, n, zEnc); |
|
|
util.assert( rc < cache.buffer.n, |
|
|
"Impossibly long output - possibly smashed the heap" ); |
|
|
util.assert( 0===wasm.peek8(wasm.ptr.add(zEnc,rc)), |
|
|
"Expecting NUL-terminated encoded output" ); |
|
|
const jenc = wasm.cstrToJs(zEnc); |
|
|
|
|
|
s.setItem(keyPrefix+(ndx+1), jenc); |
|
|
}); |
|
|
}else if( exp.pages[0] ){ |
|
|
|
|
|
exp.pages.forEach((v,ndx)=>s.setItem(keyPrefix+(ndx+1), v)); |
|
|
} |
|
|
if( isNew ) installStorageAndJournal(store); |
|
|
}catch{ |
|
|
if( !isNew ){ |
|
|
try{sqlite3_js_kvvfs_clear(exp.name);}catch(ee){} |
|
|
} |
|
|
}finally{ |
|
|
if( zEnc ) cache.memBufferFree(1); |
|
|
} |
|
|
return this; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sqlite3_js_kvvfs_reserve = function(name){ |
|
|
let store = storageForZClass(name); |
|
|
if( store ){ |
|
|
++store.refc; |
|
|
return; |
|
|
} |
|
|
validateStorageName(name); |
|
|
installStorageAndJournal(newStorageObj(name)); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sqlite3_js_kvvfs_unlink = function(name){ |
|
|
const store = storageForZClass(name); |
|
|
if( !store |
|
|
|| kvvfsIsPersistentName(store.jzClass) |
|
|
|| isBuiltinName(store.jzClass) |
|
|
|| cache.rxJournalSuffix.test(name) ) return false; |
|
|
if( store.refc > store.files.length || 0===store.files.length ){ |
|
|
if( --store.refc<=0 ){ |
|
|
|
|
|
deleteStorage(store); |
|
|
} |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sqlite3_js_kvvfs_listen = function(opt){ |
|
|
if( !opt || 'object'!==typeof opt ){ |
|
|
toss3(capi.SQLITE_MISUSE, "Expecting a listener object."); |
|
|
} |
|
|
let store = storageForZClass(opt.storage); |
|
|
if( !store ){ |
|
|
if( opt.storage && opt.reserve ){ |
|
|
sqlite3_js_kvvfs_reserve(opt.storage); |
|
|
store = storageForZClass(opt.storage); |
|
|
util.assert(store, |
|
|
"Unexpectedly cannot fetch reserved storage " |
|
|
+opt.storage); |
|
|
}else{ |
|
|
toss3(capi.SQLITE_NOTFOUND,"No such storage:",opt.storage); |
|
|
} |
|
|
} |
|
|
if( opt.events ){ |
|
|
(store.listeners ??= []).push(opt); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sqlite3_js_kvvfs_unlisten = function(opt){ |
|
|
const store = storageForZClass(opt?.storage); |
|
|
if( store?.listeners && opt.events ){ |
|
|
const n = store.listeners.length; |
|
|
store.listeners = store.listeners.filter((v)=>v!==opt); |
|
|
const rc = n>store.listeners.length; |
|
|
if( !store.listeners.length ){ |
|
|
|
|
|
store.listeners = undefined; |
|
|
} |
|
|
return rc; |
|
|
} |
|
|
return false; |
|
|
}; |
|
|
|
|
|
sqlite3.kvvfs.reserve = sqlite3_js_kvvfs_reserve; |
|
|
sqlite3.kvvfs.import = sqlite3_js_kvvfs_import; |
|
|
sqlite3.kvvfs.export = sqlite3_js_kvvfs_export; |
|
|
sqlite3.kvvfs.unlink = sqlite3_js_kvvfs_unlink; |
|
|
sqlite3.kvvfs.listen = sqlite3_js_kvvfs_listen; |
|
|
sqlite3.kvvfs.unlisten = sqlite3_js_kvvfs_unlisten; |
|
|
sqlite3.kvvfs.exists = (name)=>!!storageForZClass(name); |
|
|
sqlite3.kvvfs.estimateSize = sqlite3_js_kvvfs_size; |
|
|
sqlite3.kvvfs.clear = sqlite3_js_kvvfs_clear; |
|
|
|
|
|
|
|
|
if( globalThis.Storage ){ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
capi.sqlite3_js_kvvfs_size = (which="")=>sqlite3_js_kvvfs_size(which); |
|
|
capi.sqlite3_js_kvvfs_clear = (which="")=>sqlite3_js_kvvfs_clear(which); |
|
|
} |
|
|
|
|
|
|
|
|
if(sqlite3.oo1?.DB){ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const DB = sqlite3.oo1.DB; |
|
|
sqlite3.oo1.JsStorageDb = function( |
|
|
storageName = sqlite3.oo1.JsStorageDb.defaultStorageName |
|
|
){ |
|
|
const opt = DB.dbCtorHelper.normalizeArgs(...arguments); |
|
|
opt.vfs = 'kvvfs'; |
|
|
if( 0 ){ |
|
|
|
|
|
if( opt.flags ) opt.flags = 'cw'+opt.flags; |
|
|
else opt.flags = 'cw'; |
|
|
} |
|
|
switch( opt.filename ){ |
|
|
|
|
|
|
|
|
|
|
|
case ":sessionStorage:": opt.filename = 'session'; break; |
|
|
case ":localStorage:": opt.filename = 'local'; break; |
|
|
} |
|
|
const m = /(file:(\/\/)?)([^?]+)/.exec(opt.filename); |
|
|
validateStorageName( m ? m[3] : opt.filename); |
|
|
DB.dbCtorHelper.call(this, opt); |
|
|
}; |
|
|
sqlite3.oo1.JsStorageDb.defaultStorageName |
|
|
= cache.storagePool.session ? 'session' : nameOfThisThreadStorage; |
|
|
const jdb = sqlite3.oo1.JsStorageDb; |
|
|
jdb.prototype = Object.create(DB.prototype); |
|
|
jdb.clearStorage = sqlite3_js_kvvfs_clear; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
jdb.prototype.clearStorage = function(){ |
|
|
return jdb.clearStorage(this.affirmOpen().dbFilename(), true); |
|
|
}; |
|
|
|
|
|
jdb.storageSize = sqlite3_js_kvvfs_size; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
jdb.prototype.storageSize = function(){ |
|
|
return jdb.storageSize(this.affirmOpen().dbFilename(), true); |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
if( sqlite3.__isUnderTest && sqlite3.vtab ){ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const cols = Object.assign(Object.create(null),{ |
|
|
rowid: {type: 'INTEGER'}, |
|
|
name: {type: 'TEXT'}, |
|
|
nRef: {type: 'INTEGER'}, |
|
|
nOpen: {type: 'INTEGER'}, |
|
|
isTransient: {type: 'INTEGER'}, |
|
|
dbSize: {type: 'INTEGER'} |
|
|
}); |
|
|
Object.keys(cols).forEach((v,i)=>cols[v].colId = i); |
|
|
|
|
|
const VT = sqlite3.vtab; |
|
|
const ProtoCursor = Object.assign(Object.create(null),{ |
|
|
row: function(){ |
|
|
return cache.storagePool[this.names[this.rowid]]; |
|
|
} |
|
|
}); |
|
|
Object.assign(Object.create(ProtoCursor),{ |
|
|
rowid: 0, |
|
|
names: Object.keys(cache.storagePool) |
|
|
.filter(v=>!cache.rxJournalSuffix.test(v)) |
|
|
}); |
|
|
const cursorState = function(cursor, reset){ |
|
|
const o = (cursor instanceof capi.sqlite3_vtab_cursor) |
|
|
? cursor |
|
|
: VT.xCursor.get(cursor); |
|
|
if( reset || !o.vTabState ){ |
|
|
o.vTabState = Object.assign(Object.create(ProtoCursor),{ |
|
|
rowid: 0, |
|
|
names: Object.keys(cache.storagePool) |
|
|
.filter(v=>!cache.rxJournalSuffix.test(v)) |
|
|
}); |
|
|
} |
|
|
return o.vTabState; |
|
|
}; |
|
|
|
|
|
const dbg = 1 ? ()=>{} : (...args)=>debug("vtab",...args); |
|
|
|
|
|
const theModule = function f(){ |
|
|
return f.mod ??= new sqlite3.capi.sqlite3_module().setupModule({ |
|
|
catchExceptions: true, |
|
|
methods: { |
|
|
xConnect: function(pDb, pAux, argc, argv, ppVtab, pzErr){ |
|
|
dbg("xConnect"); |
|
|
try{ |
|
|
const xcol = []; |
|
|
Object.keys(cols).forEach((k)=>{ |
|
|
xcol.push(k+" "+cols[k].type); |
|
|
}); |
|
|
const rc = capi.sqlite3_declare_vtab( |
|
|
pDb, "CREATE TABLE ignored("+xcol.join(',')+")" |
|
|
); |
|
|
if(0===rc){ |
|
|
const t = VT.xVtab.create(ppVtab); |
|
|
util.assert( |
|
|
(t === VT.xVtab.get(wasm.peekPtr(ppVtab))), |
|
|
"output pointer check failed" |
|
|
); |
|
|
} |
|
|
return rc; |
|
|
}catch(e){ |
|
|
return VT.xErrror('xConnect', e, capi.SQLITE_ERROR); |
|
|
} |
|
|
}, |
|
|
xCreate: wasm.ptr.null, |
|
|
|
|
|
xDisconnect: function(pVtab){ |
|
|
dbg("xDisconnect",...arguments); |
|
|
VT.xVtab.dispose(pVtab); |
|
|
return 0; |
|
|
}, |
|
|
xOpen: function(pVtab, ppCursor){ |
|
|
dbg("xOpen",...arguments); |
|
|
VT.xCursor.create(ppCursor); |
|
|
return 0; |
|
|
}, |
|
|
xClose: function(pCursor){ |
|
|
dbg("xClose",...arguments); |
|
|
const c = VT.xCursor.unget(pCursor); |
|
|
delete c.vTabState; |
|
|
c.dispose(); |
|
|
return 0; |
|
|
}, |
|
|
xNext: function(pCursor){ |
|
|
dbg("xNext",...arguments); |
|
|
const c = VT.xCursor.get(pCursor); |
|
|
++cursorState(c).rowid; |
|
|
return 0; |
|
|
}, |
|
|
xColumn: function(pCursor, pCtx, iCol){ |
|
|
dbg("xColumn",...arguments); |
|
|
|
|
|
const st = cursorState(pCursor); |
|
|
const store = st.row(); |
|
|
util.assert(store, "Unexpected xColumn call"); |
|
|
switch(iCol){ |
|
|
case cols.rowid.colId: |
|
|
capi.sqlite3_result_int(pCtx, st.rowid); |
|
|
break; |
|
|
case cols.name.colId: |
|
|
capi.sqlite3_result_text(pCtx, store.jzClass, -1, capi.SQLITE_TRANSIENT); |
|
|
break; |
|
|
case cols.nRef.colId: |
|
|
capi.sqlite3_result_int(pCtx, store.refc); |
|
|
break; |
|
|
case cols.nOpen.colId: |
|
|
capi.sqlite3_result_int(pCtx, store.files.length); |
|
|
break; |
|
|
case cols.isTransient.colId: |
|
|
capi.sqlite3_result_int(pCtx, !!store.deleteAtRefc0); |
|
|
break; |
|
|
case cols.dbSize.colId: |
|
|
capi.sqlite3_result_int(pCtx, storageGetDbSize(store)); |
|
|
break; |
|
|
default: |
|
|
capi.sqlite3_result_error(pCtx, "Invalid column id: "+iCol); |
|
|
return capi.SQLITE_RANGE; |
|
|
} |
|
|
return 0; |
|
|
}, |
|
|
xRowid: function(pCursor, ppRowid64){ |
|
|
dbg("xRowid",...arguments); |
|
|
const st = cursorState(pCursor); |
|
|
VT.xRowid(ppRowid64, st.rowid); |
|
|
return 0; |
|
|
}, |
|
|
xEof: function(pCursor){ |
|
|
const st = cursorState(pCursor); |
|
|
dbg("xEof?="+(!st.row()),...arguments); |
|
|
return !st.row(); |
|
|
}, |
|
|
xFilter: function(pCursor, idxNum, idxCStr, |
|
|
argc, argv){ |
|
|
dbg("xFilter",...arguments); |
|
|
const st = cursorState(pCursor, true); |
|
|
return 0; |
|
|
}, |
|
|
xBestIndex: function(pVtab, pIdxInfo){ |
|
|
dbg("xBestIndex",...arguments); |
|
|
|
|
|
const pii = new capi.sqlite3_index_info(pIdxInfo); |
|
|
pii.$estimatedRows = cache.storagePool.size; |
|
|
pii.$estimatedCost = 1.0; |
|
|
pii.dispose(); |
|
|
return 0; |
|
|
} |
|
|
} |
|
|
}); |
|
|
}; |
|
|
|
|
|
sqlite3.kvvfs.create_module = function(pDb, name="sqlite_kvvfs"){ |
|
|
return capi.sqlite3_create_module(pDb, name, theModule(), |
|
|
wasm.ptr.null); |
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
kvvfs.Listener = class KvvfsListener { |
|
|
#store; |
|
|
#listener; |
|
|
|
|
|
constructor(opt){ |
|
|
this.#listenTo(opt); |
|
|
} |
|
|
|
|
|
#event(ev){ |
|
|
switch(ev.type){ |
|
|
case 'open': this.onOpen(ev.data); break; |
|
|
case 'close': this.onClose(ev.data); break; |
|
|
case 'sync': this.onSync(ev.data); break; |
|
|
case 'delete': |
|
|
switch(ev.data){ |
|
|
case 'jrnl': break; |
|
|
default:{ |
|
|
const n = +ev.data; |
|
|
util.assert( n>0, "Expecting positive db page number" ); |
|
|
this.onPageChange(n, null); |
|
|
break; |
|
|
} |
|
|
} |
|
|
break; |
|
|
case 'write':{ |
|
|
const key = ev.data[0], val = ev.data[1]; |
|
|
switch( key ){ |
|
|
case 'jrnl': break; |
|
|
case 'sz':{ |
|
|
const sz = +val; |
|
|
util.assert( sz>0, "Expecting a db page number" ); |
|
|
this.onSizeChange(sz); |
|
|
break; |
|
|
} |
|
|
default: |
|
|
T.assert( +key>0, "Expecting a positive db page number" ); |
|
|
this.onPageChange(+key, val); |
|
|
break; |
|
|
} |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
#listenTo(opt){ |
|
|
if(this.#listener){ |
|
|
sqlite3_js_kvvfs_unlisten(this.#listener); |
|
|
this.#listener = undefined; |
|
|
} |
|
|
const eventHandler = async function(ev){this.event(ev)}.bind(this); |
|
|
const li = Object.assign( |
|
|
{ |
|
|
reserve: false, |
|
|
includeJournal: false, |
|
|
decodePages: false, |
|
|
storage: null |
|
|
}, |
|
|
(opt||{}), |
|
|
{ |
|
|
events: Object.assign(Object.create(null),{ |
|
|
'open': eventHandler, |
|
|
'close': eventHandler, |
|
|
'write': eventHandler, |
|
|
'delete': eventHandler, |
|
|
'sync': eventHandler |
|
|
}) |
|
|
} |
|
|
); |
|
|
sqlite3_js_kvvfs_listen(li); |
|
|
this.#listener = li; |
|
|
} |
|
|
|
|
|
async onSizeChange(sz){} |
|
|
async onPageChange(pgNo,content){} |
|
|
async onSync(mode){} |
|
|
async onOpen(count){} |
|
|
async onClose(count){} |
|
|
}; |
|
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|