Skip to content

Commit 86b933b

Browse files
committed
feat: importDatabaseToOpfs, importDatabaseToIdb
1 parent 01a5599 commit 86b933b

File tree

4 files changed

+160
-121
lines changed

4 files changed

+160
-121
lines changed

playground/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
<button class="main">main</button>
1111
<button class="worker">worker</button>
1212
<button class="import">import</button>
13+
<button class="importW">import worker</button>
1314
<button class="download">download</button>
15+
<button class="downloadW">download worker</button>
1416
<button class="clear">clear</button>
1517
</body>
1618
<script type="module" src="/src/index.ts"></script>

playground/src/index.ts

Lines changed: 73 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import {
1111
useMemoryStorage,
1212
withExistDB,
1313
} from '../../src/index'
14-
// import { useIdbMemoryStorage } from '../../src/vfs/idb-memory'
15-
import { useIdbStorage } from '../../src/vfs/idb'
14+
import { useIdbMemoryStorage } from '../../src/vfs/idb-memory'
15+
// import { useIdbStorage } from '../../src/vfs/idb'
1616
import { runSQL } from './runSQL'
1717
import OpfsWorker from './worker?worker'
1818

19-
let db: SQLiteDB
19+
let db: SQLiteDB | undefined
2020

2121
const supportModuleWorker = isModuleWorkerSupport()
2222
const supportIDB = isIdbSupported()
@@ -26,8 +26,8 @@ console.log('support IDBBatchAtomicVFS:', supportIDB)
2626
console.log('support OPFSCoopSyncVFS:', supportOPFS)
2727
document.querySelector('.main')?.addEventListener('click', async () => {
2828
if (!db) {
29-
// db = await initSQLite(useIdbMemoryStorage('test.db', { url }))
30-
db = await initSQLite(useIdbStorage('test.db', { url }))
29+
db = await initSQLite(useIdbMemoryStorage('test.db', { url }))
30+
// db = await initSQLite(useIdbStorage('test.db', { url }))
3131
}
3232
await runSQL(db.run)
3333
await runSQL((await initSQLite(useMemoryStorage({ url: syncUrl }))).run)
@@ -43,80 +43,105 @@ document.querySelector('.import')?.addEventListener('click', async () => {
4343
return
4444
}
4545
db = await initSQLite(
46-
// useIdbMemoryStorage('test.db', withExistDB(file, { url })),
47-
useIdbStorage('test.db', { url }),
46+
useIdbMemoryStorage('test.db', withExistDB(file, { url })),
47+
// useIdbMemoryStorage('test.db', { url }),
48+
// useIdbStorage('test.db', { url }),
4849
)
49-
await importDatabase(db.vfs, db.path, file)
50+
// await importDatabase(db.vfs, db.path, file)
5051
console.log(
5152
await db.run(`SELECT "type", "tbl_name" AS "table", CASE WHEN "sql" LIKE '%PRIMARY KEY AUTOINCREMENT%' THEN 1 ELSE "name" END AS "name" FROM "sqlite_master"`),
5253
)
5354
})
5455

5556
document.querySelector('.download')?.addEventListener('click', async () => {
56-
download(await db.dump())
57+
if (db) {
58+
download(await db.dump())
59+
}
5760
})
5861

62+
const ee = mitt<{
63+
data: [any]
64+
done: []
65+
}>()
66+
67+
function test(): AsyncIterableIterator<any> {
68+
let resolver: ((value: IteratorResult<any>) => void) | null = null
69+
70+
ee.on('data', (...data) => {
71+
if (resolver) {
72+
resolver({ value: data[0] })
73+
resolver = null
74+
}
75+
})
76+
77+
ee.on('done', () => {
78+
if (resolver) {
79+
resolver({ value: undefined, done: true })
80+
}
81+
})
82+
83+
return {
84+
[Symbol.asyncIterator]() {
85+
return this
86+
},
87+
async next() {
88+
return new Promise<IteratorResult<any>>((resolve) => {
89+
resolver = resolve
90+
})
91+
},
92+
async return() {
93+
return { value: undefined, done: true }
94+
},
95+
} satisfies AsyncIterableIterator<any>
96+
}
97+
5998
document.querySelector('.worker')?.addEventListener('click', async () => {
6099
const worker = new OpfsWorker()
61-
const ee = mitt<{
62-
data: [any]
63-
done: []
64-
}>()
65100
worker.onmessage = ({ data }) => {
66101
if (data === 'done') {
67102
ee.emit('done')
68103
} else {
69104
ee.emit('data', data)
70105
}
71106
}
72-
function test(): AsyncIterableIterator<any> {
73-
let resolver: ((value: IteratorResult<any>) => void) | null = null
74-
75-
ee.on('data', (...data) => {
76-
if (resolver) {
77-
console.log('data')
78-
resolver({ value: data[0] })
79-
resolver = null
80-
}
81-
})
82-
83-
ee.on('done', () => {
84-
if (resolver) {
85-
resolver({ value: undefined, done: true })
86-
}
87-
})
88-
89-
return {
90-
[Symbol.asyncIterator]() {
91-
return this
92-
},
93-
async next() {
94-
return new Promise<IteratorResult<any>>((resolve) => {
95-
resolver = resolve
96-
})
97-
},
98-
async return() {
99-
return { value: undefined, done: true }
100-
},
101-
} satisfies AsyncIterableIterator<any>
102-
}
103107
worker.postMessage('')
104108
for await (const data of test()) {
105109
console.log('iterator', data)
106110
}
107111
})
112+
document.querySelector('.importW')?.addEventListener('click', async () => {
113+
const worker = new OpfsWorker()
114+
worker.onmessage = ({ data }) => {
115+
if (data === 'done') {
116+
ee.emit('done')
117+
} else {
118+
ee.emit('data', data)
119+
}
120+
}
121+
let file
122+
try {
123+
file = await selectFile('.db,.sqlite,.sqlite3')
124+
} catch (error) {
125+
// eslint-disable-next-line no-alert
126+
alert(`${error}`)
127+
return
128+
}
129+
worker.postMessage(file)
130+
for await (const data of test()) {
131+
console.log('[import] iterator', data)
132+
}
133+
})
108134
document.querySelector('.clear')?.addEventListener('click', async () => {
109-
await db.close()
135+
await db?.close()
110136
const root = await navigator.storage.getDirectory()
111137
for await (const [name] of root.entries()) {
112-
console.log('clear', name)
113138
await root.removeEntry(name, { recursive: true })
114-
console.log('clear success', name)
139+
console.log('delete entry:', name)
115140
}
116141
console.log('clear all OPFS')
117142
const dbs = await window.indexedDB.databases()
118143
dbs.forEach((db) => {
119-
console.log('clear', db.name)
144+
console.log('delete idb:', db.name)
120145
window.indexedDB.deleteDatabase(db.name!)
121146
})
122147
console.log('clear all IndexedDB')

playground/src/worker.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
import { uuidv7 } from 'uuidv7'
22
import url from '../../dist/wa-sqlite.wasm?url'
3-
import { customFunction, initSQLite, isOpfsSupported } from '../../src'
3+
import { customFunction, initSQLite, isOpfsSupported, withExistDB } from '../../src'
44
import { useOpfsStorage } from '../../src/vfs/opfs'
55
import { runSQLStream } from './runSQL'
66

7-
onmessage = async () => {
7+
onmessage = async ({ data }) => {
88
if (!await isOpfsSupported()) {
99
return
1010
}
11-
const { run, stream, lastInsertRowId, changes, sqlite, db } = await initSQLite(useOpfsStorage(
12-
'test',
13-
{ url },
14-
// 'https://cdn.jsdelivr.net/gh/rhashimoto/wa-sqlite@v0.9.9/dist/wa-sqlite.wasm',
11+
const db = await initSQLite(useOpfsStorage(
12+
'test.db',
13+
data ? withExistDB(data, { url }) : { url },
14+
// { url },
1515
))
16-
await runSQLStream(run, stream, data => postMessage(data))
17-
console.log(lastInsertRowId(), changes())
18-
customFunction(sqlite, db, 'uuidv7', () => uuidv7())
16+
// if (data) {
17+
// await db.sync(data)
18+
// }
19+
await runSQLStream(db.run, db.stream, data => postMessage(data))
20+
console.log(db.lastInsertRowId(), db.changes())
21+
customFunction(db.sqlite, db.pointer, 'uuidv7', () => uuidv7())
1922
console.log(
20-
await run('select uuidv7() as a'),
23+
'uuidv7():',
24+
(await db.run('select uuidv7() as a'))[0].a,
2125
)
2226
postMessage('done')
2327
}

src/io/import.ts

Lines changed: 71 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable antfu/consistent-list-newline */
12
// reference from https://github.com/rhashimoto/wa-sqlite/blob/master/demo/file/index.js
23
import type { FacadeVFS, Promisable } from '../types'
34
import {
@@ -15,79 +16,69 @@ import {
1516
} from '../constant'
1617
import { check, ignoredDataView } from './common'
1718

18-
async function* pagify(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array> {
19-
const chunks: Uint8Array[] = []
20-
const reader = stream.getReader()
19+
const SQLITE_BINARY_HEADER = new Uint8Array([
20+
0x53, 0x51, 0x4C, 0x69, 0x74, 0x65, 0x20, 0x66, // SQLite f
21+
0x6F, 0x72, 0x6D, 0x61, 0x74, 0x20, 0x33, 0x00, // ormat 3\0
22+
])
23+
24+
async function parseHeaderAndVerify(
25+
reader: ReadableStreamDefaultReader<Uint8Array<ArrayBufferLike>>,
26+
): Promise<Uint8Array<ArrayBufferLike>> {
27+
const headerData = await readExactBytes(reader, 32)
28+
for (let i = 0; i < SQLITE_BINARY_HEADER.length; i++) {
29+
if (headerData[i] !== SQLITE_BINARY_HEADER[i]) {
30+
throw new Error('Not a SQLite database file')
31+
}
32+
}
33+
return headerData
34+
}
35+
36+
async function readExactBytes(
37+
reader: ReadableStreamDefaultReader<Uint8Array>,
38+
size: number,
39+
): Promise<Uint8Array> {
40+
const result = new Uint8Array(size)
41+
let offset = 0
2142

22-
// read file header
23-
while (chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0) < 32) {
43+
while (offset < size) {
2444
const { done, value } = await reader.read()
2545
if (done) {
2646
throw new Error('Unexpected EOF')
2747
}
28-
chunks.push(value!)
29-
}
3048

31-
let copyOffset = 0
32-
const header = new DataView(new ArrayBuffer(32))
33-
for (const chunk of chunks) {
34-
const src = chunk.subarray(0, header.byteLength - copyOffset)
35-
const dst = new Uint8Array(header.buffer, copyOffset)
36-
dst.set(src)
37-
copyOffset += src.byteLength
49+
const bytesToCopy = Math.min(size - offset, value!.length)
50+
result.set(value!.subarray(0, bytesToCopy), offset)
51+
offset += bytesToCopy
3852
}
3953

40-
if (new TextDecoder().decode(header.buffer.slice(0, 16)) !== 'SQLite format 3\x00') {
41-
throw new Error('Not a SQLite database file')
42-
}
54+
return result
55+
}
4356

44-
const pageSize = (field => field === 1 ? 65536 : field)(header.getUint16(16))
45-
const pageCount = header.getUint32(28)
46-
// console.log(`${pageCount} pages, ${pageSize} bytes each, ${pageCount * pageSize} bytes total`)
47-
48-
// copy pages
49-
for (let i = 0; i < pageCount; ++i) {
50-
while (chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0) < pageSize) {
51-
const { done, value } = await reader.read()
52-
if (done) {
53-
throw new Error('Unexpected EOF')
54-
}
55-
chunks.push(value!)
56-
}
57+
async function* pagify(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array> {
58+
const reader = stream.getReader()
5759

58-
let page: Uint8Array
59-
if (chunks[0]?.byteLength >= pageSize) {
60-
page = chunks[0].subarray(0, pageSize)
61-
chunks[0] = chunks[0].subarray(pageSize)
62-
if (!chunks[0].byteLength) {
63-
chunks.shift()
64-
}
65-
} else {
66-
let copyOffset = 0
67-
page = new Uint8Array(pageSize)
68-
while (copyOffset < pageSize) {
69-
const src = chunks[0].subarray(0, pageSize - copyOffset)
70-
const dst = new Uint8Array(page.buffer, copyOffset)
71-
dst.set(src)
72-
copyOffset += src.byteLength
73-
74-
chunks[0] = chunks[0].subarray(src.byteLength)
75-
if (!chunks[0].byteLength) {
76-
chunks.shift()
77-
}
78-
}
79-
}
60+
try {
61+
const headerData = await parseHeaderAndVerify(reader)
8062

81-
yield page
82-
}
63+
const view = new DataView(headerData.buffer)
64+
const rawPageSize = view.getUint16(16)
65+
const pageSize = rawPageSize === 1 ? 65536 : rawPageSize
66+
const pageCount = view.getUint32(28)
67+
68+
for (let i = 0; i < pageCount; i++) {
69+
yield await readExactBytes(reader, pageSize)
70+
}
8371

84-
const { done } = await reader.read()
85-
if (!done) {
86-
throw new Error('Unexpected data after last page')
72+
const { done } = await reader.read()
73+
if (!done) {
74+
throw new Error('Unexpected data after last page')
75+
}
76+
} finally {
77+
reader.releaseLock()
8778
}
8879
}
8980

90-
export async function importDatabaseStream(
81+
export async function importDatabaseToIdb(
9182
vfs: FacadeVFS,
9283
path: string,
9384
stream: ReadableStream<Uint8Array>,
@@ -129,6 +120,21 @@ export async function importDatabaseStream(
129120
}
130121
}
131122

123+
async function importDatabaseToOpfs(
124+
path: string,
125+
source: ReadableStream<Uint8Array>,
126+
): Promise<void> {
127+
const root = await navigator.storage.getDirectory()
128+
const handle = await root.getFileHandle(path, { create: true })
129+
const [streamForVerify, streamData] = source.tee()
130+
131+
await parseHeaderAndVerify(streamForVerify.getReader())
132+
133+
const writable = await handle.createWritable()
134+
// `pipeTo()` will auto close `writable`
135+
await streamData.pipeTo(writable)
136+
}
137+
132138
/**
133139
* Import database from `File` or `ReadableStream`
134140
* @param vfs SQLite VFS
@@ -140,9 +146,11 @@ export async function importDatabase(
140146
path: string,
141147
data: File | ReadableStream<Uint8Array>,
142148
): Promise<void> {
143-
return await importDatabaseStream(
144-
vfs,
145-
path,
146-
data instanceof File ? data.stream() : data,
147-
)
149+
const stream = data instanceof globalThis.File ? data.stream() : data
150+
// is `OPFSCoopSyncVFS`
151+
if ('releaser' in vfs) {
152+
await importDatabaseToOpfs(path, stream)
153+
} else {
154+
await importDatabaseToIdb(vfs, path, stream)
155+
}
148156
}

0 commit comments

Comments
 (0)