@livestore/sqlite-wasm 0.4.0-dev.9 → 0.5.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/FacadeVFS.d.ts +0 -4
- package/dist/FacadeVFS.d.ts.map +1 -1
- package/dist/FacadeVFS.js +13 -17
- package/dist/FacadeVFS.js.map +1 -1
- package/dist/browser/mod.d.ts +13 -6
- package/dist/browser/mod.d.ts.map +1 -1
- package/dist/browser/mod.js +48 -42
- package/dist/browser/mod.js.map +1 -1
- package/dist/browser/opfs/AccessHandlePoolVFS.d.ts +24 -18
- package/dist/browser/opfs/AccessHandlePoolVFS.d.ts.map +1 -1
- package/dist/browser/opfs/AccessHandlePoolVFS.js +178 -166
- package/dist/browser/opfs/AccessHandlePoolVFS.js.map +1 -1
- package/dist/browser/opfs/index.d.ts +3 -2
- package/dist/browser/opfs/index.d.ts.map +1 -1
- package/dist/browser/opfs/index.js +3 -3
- package/dist/browser/opfs/index.js.map +1 -1
- package/dist/browser/opfs/opfs-sah-pool.d.ts +1 -1
- package/dist/browser/opfs/opfs-sah-pool.d.ts.map +1 -1
- package/dist/browser/opfs/opfs-sah-pool.js +4 -4
- package/dist/browser/opfs/opfs-sah-pool.js.map +1 -1
- package/dist/cf/CloudflareDurableObjectVFS.d.ts +104 -0
- package/dist/cf/CloudflareDurableObjectVFS.d.ts.map +1 -0
- package/dist/cf/CloudflareDurableObjectVFS.js +283 -0
- package/dist/cf/CloudflareDurableObjectVFS.js.map +1 -0
- package/dist/cf/CloudflareWorkerVFS.d.ts.map +1 -1
- package/dist/cf/CloudflareWorkerVFS.js +29 -28
- package/dist/cf/CloudflareWorkerVFS.js.map +1 -1
- package/dist/cf/mod.d.ts +3 -4
- package/dist/cf/mod.d.ts.map +1 -1
- package/dist/cf/mod.js +6 -14
- package/dist/cf/mod.js.map +1 -1
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js +6 -5
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js.map +1 -1
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js +4 -4
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js.map +1 -1
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js +4 -4
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js.map +1 -1
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js +4 -4
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js.map +1 -1
- package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js +195 -180
- package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js.map +1 -1
- package/dist/in-memory-vfs.d.ts.map +1 -1
- package/dist/in-memory-vfs.js +0 -1
- package/dist/in-memory-vfs.js.map +1 -1
- package/dist/load-wasm/mod.node.js +1 -1
- package/dist/load-wasm/mod.node.js.map +1 -1
- package/dist/load-wasm/mod.workerd.d.ts.map +1 -1
- package/dist/load-wasm/mod.workerd.js +2 -0
- package/dist/load-wasm/mod.workerd.js.map +1 -1
- package/dist/make-sqlite-db.d.ts.map +1 -1
- package/dist/make-sqlite-db.js +18 -9
- package/dist/make-sqlite-db.js.map +1 -1
- package/dist/node/NodeFS.d.ts.map +1 -1
- package/dist/node/NodeFS.js +14 -14
- package/dist/node/NodeFS.js.map +1 -1
- package/dist/node/mod.d.ts.map +1 -1
- package/dist/node/mod.js +3 -3
- package/dist/node/mod.js.map +1 -1
- package/package.json +48 -18
- package/src/FacadeVFS.ts +13 -17
- package/src/browser/mod.ts +38 -13
- package/src/browser/opfs/AccessHandlePoolVFS.ts +374 -268
- package/src/browser/opfs/index.ts +5 -4
- package/src/browser/opfs/opfs-sah-pool.ts +4 -4
- package/src/cf/CloudflareDurableObjectVFS.ts +324 -0
- package/src/cf/CloudflareWorkerVFS.ts +41 -39
- package/src/cf/README.md +3 -3
- package/src/cf/mod.ts +12 -17
- package/src/cf/test/README.md +63 -37
- package/src/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.ts +8 -5
- package/src/cf/test/async-storage/cloudflare-worker-vfs-core.test.ts +6 -4
- package/src/cf/test/async-storage/cloudflare-worker-vfs-integration.test.ts +6 -4
- package/src/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.ts +6 -4
- package/src/cf/test/sql/cloudflare-sql-vfs-core.test.ts +232 -198
- package/src/in-memory-vfs.ts +0 -1
- package/src/load-wasm/mod.node.ts +1 -1
- package/src/load-wasm/mod.workerd.ts +2 -1
- package/src/make-sqlite-db.ts +21 -9
- package/src/node/NodeFS.ts +25 -24
- package/src/node/mod.ts +3 -3
- package/dist/cf/BlockManager.d.ts +0 -61
- package/dist/cf/BlockManager.d.ts.map +0 -1
- package/dist/cf/BlockManager.js +0 -157
- package/dist/cf/BlockManager.js.map +0 -1
- package/dist/cf/CloudflareSqlVFS.d.ts +0 -51
- package/dist/cf/CloudflareSqlVFS.d.ts.map +0 -1
- package/dist/cf/CloudflareSqlVFS.js +0 -351
- package/dist/cf/CloudflareSqlVFS.js.map +0 -1
- package/src/ambient.d.ts +0 -18
- package/src/cf/BlockManager.ts +0 -225
- package/src/cf/CloudflareSqlVFS.ts +0 -450
|
@@ -1,20 +1,45 @@
|
|
|
1
1
|
/// <reference types="vitest/globals" />
|
|
2
2
|
|
|
3
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
3
5
|
import type { CfTypes } from '@livestore/common-cf'
|
|
4
6
|
import * as VFS from '@livestore/wa-sqlite/src/VFS.js'
|
|
5
|
-
import { beforeEach, describe, expect, it } from 'vitest'
|
|
6
|
-
import { CloudflareSqlVFS } from '../../mod.ts'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { CF_SQL_VFS_PAGE_SIZE, CloudflareDurableObjectVFS } from '../../mod.ts'
|
|
9
|
+
|
|
10
|
+
const makePage = (fillByte: number, text?: string): Uint8Array => {
|
|
11
|
+
const page = new Uint8Array(CF_SQL_VFS_PAGE_SIZE)
|
|
12
|
+
page.fill(fillByte)
|
|
13
|
+
|
|
14
|
+
if (text !== undefined) {
|
|
15
|
+
page.set(new TextEncoder().encode(text))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return page
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('CloudflareDurableObjectVFS - Core Functionality', () => {
|
|
22
|
+
let vfs: CloudflareDurableObjectVFS
|
|
10
23
|
let mockSql: CfTypes.SqlStorage
|
|
11
|
-
|
|
24
|
+
/** In-memory page store keyed by file_path and page_no */
|
|
25
|
+
let mockPages: Map<string, Map<number, Uint8Array>>
|
|
12
26
|
let queryLog: string[]
|
|
13
27
|
|
|
14
28
|
beforeEach(async () => {
|
|
15
|
-
|
|
29
|
+
mockPages = new Map()
|
|
16
30
|
queryLog = []
|
|
17
31
|
|
|
32
|
+
const getOrCreateFilePages = (filePath: string) => {
|
|
33
|
+
let pages = mockPages.get(filePath)
|
|
34
|
+
|
|
35
|
+
if (pages === undefined) {
|
|
36
|
+
pages = new Map()
|
|
37
|
+
mockPages.set(filePath, pages)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return pages
|
|
41
|
+
}
|
|
42
|
+
|
|
18
43
|
// Mock SQL storage that mimics the Cloudflare DurableObject SQL API
|
|
19
44
|
mockSql = {
|
|
20
45
|
exec: <T extends Record<string, CfTypes.SqlStorageValue>>(
|
|
@@ -23,118 +48,116 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
|
|
|
23
48
|
): CfTypes.SqlStorageCursor<T> => {
|
|
24
49
|
queryLog.push(`${query} [${bindings.join(', ')}]`)
|
|
25
50
|
|
|
26
|
-
|
|
27
|
-
const normalizedQuery = query.trim().toUpperCase()
|
|
51
|
+
const normalizedQuery = query.trim().replace(/\s+/g, ' ').toUpperCase()
|
|
28
52
|
|
|
29
|
-
if (
|
|
30
|
-
normalizedQuery.includes('CREATE TABLE') ||
|
|
31
|
-
normalizedQuery.includes('CREATE INDEX') ||
|
|
32
|
-
normalizedQuery.includes('CREATE TRIGGER')
|
|
33
|
-
) {
|
|
34
|
-
// Handle schema creation
|
|
53
|
+
if (normalizedQuery.includes('CREATE TABLE') === true) {
|
|
35
54
|
return createMockCursor([] as any)
|
|
36
55
|
}
|
|
37
56
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const existingIndex = blocks.findIndex((b: any) => b.block_id === blockId)
|
|
46
|
-
const blockEntry = { file_path: filePath, block_id: blockId, block_data: blockData }
|
|
47
|
-
|
|
48
|
-
if (existingIndex >= 0) {
|
|
49
|
-
blocks[existingIndex] = blockEntry
|
|
50
|
-
} else {
|
|
51
|
-
blocks.push(blockEntry)
|
|
52
|
-
}
|
|
57
|
+
// INSERT OR REPLACE INTO vfs_pages (file_path, page_no, page_data) VALUES (?, ?, ?)
|
|
58
|
+
if (normalizedQuery.startsWith('INSERT OR REPLACE INTO VFS_PAGES') === true) {
|
|
59
|
+
const [filePath, pageNo, pageData] = bindings
|
|
60
|
+
getOrCreateFilePages(filePath as string).set(
|
|
61
|
+
pageNo as number,
|
|
62
|
+
pageData instanceof Uint8Array ? pageData : new Uint8Array(pageData),
|
|
63
|
+
)
|
|
53
64
|
return createMockCursor([] as any)
|
|
54
65
|
}
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
modified_at: modifiedAt as number,
|
|
64
|
-
} as any)
|
|
65
|
-
return createMockCursor([] as any)
|
|
67
|
+
// SELECT page_data FROM vfs_pages WHERE file_path = ? AND page_no = ?
|
|
68
|
+
if (
|
|
69
|
+
normalizedQuery.startsWith('SELECT PAGE_DATA FROM VFS_PAGES WHERE FILE_PATH = ? AND PAGE_NO = ?') === true
|
|
70
|
+
) {
|
|
71
|
+
const [filePath, pageNo] = bindings
|
|
72
|
+
const data = mockPages.get(filePath as string)?.get(pageNo as number)
|
|
73
|
+
return createMockCursor(data !== undefined ? ([{ page_data: data }] as any) : ([] as any))
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return createMockCursor(fileData ? [fileData] : ([] as any))
|
|
76
|
+
// SELECT 1 AS x FROM vfs_pages LIMIT 1
|
|
77
|
+
if (normalizedQuery.includes('SELECT 1 AS X FROM VFS_PAGES') === true) {
|
|
78
|
+
if (mockPages.size > 0) {
|
|
79
|
+
return createMockCursor([{ x: 1 }] as any)
|
|
73
80
|
}
|
|
74
81
|
return createMockCursor([] as any)
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (normalizedQuery.includes('AND BLOCK_ID IN')) {
|
|
83
|
-
const requestedBlockIds = bindings.slice(1)
|
|
84
|
-
const matchingBlocks = blocks.filter((b: any) => requestedBlockIds.includes(b.block_id))
|
|
85
|
-
return createMockCursor(matchingBlocks as any)
|
|
86
|
-
}
|
|
84
|
+
// SELECT MAX(page_no) AS max_page FROM vfs_pages WHERE file_path = ?
|
|
85
|
+
if (normalizedQuery.includes('SELECT MAX(PAGE_NO) AS MAX_PAGE FROM VFS_PAGES WHERE FILE_PATH = ?') === true) {
|
|
86
|
+
const [filePath] = bindings
|
|
87
|
+
const pages = mockPages.get(filePath as string)
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
if (pages === undefined || pages.size === 0) {
|
|
90
|
+
return createMockCursor([{ max_page: null }] as any)
|
|
89
91
|
}
|
|
90
|
-
|
|
92
|
+
|
|
93
|
+
const maxPage = Math.max(...pages.keys())
|
|
94
|
+
return createMockCursor([{ max_page: maxPage }] as any)
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
// SELECT COUNT(*) AS total_pages, COALESCE(...) FROM vfs_pages
|
|
98
|
+
if (normalizedQuery.includes('FROM VFS_PAGES') === true && normalizedQuery.includes('COUNT(*)') === true) {
|
|
99
|
+
let totalBytes = 0
|
|
100
|
+
let totalPages = 0
|
|
101
|
+
|
|
102
|
+
for (const pages of mockPages.values()) {
|
|
103
|
+
totalPages += pages.size
|
|
104
|
+
|
|
105
|
+
for (const data of pages.values()) {
|
|
106
|
+
totalBytes += data.length
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
|
-
|
|
109
|
+
|
|
110
|
+
return createMockCursor([{ total_pages: totalPages, total_bytes: totalBytes }] as any)
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
// DELETE FROM vfs_pages WHERE file_path = ? AND page_no >= ?
|
|
114
|
+
if (normalizedQuery.startsWith('DELETE FROM VFS_PAGES WHERE FILE_PATH = ? AND PAGE_NO >= ?') === true) {
|
|
115
|
+
const [filePath, minPageNo] = bindings
|
|
116
|
+
const pages = mockPages.get(filePath as string)
|
|
117
|
+
|
|
118
|
+
if (pages !== undefined) {
|
|
119
|
+
for (const pageNo of pages.keys()) {
|
|
120
|
+
if (pageNo >= (minPageNo as number)) {
|
|
121
|
+
pages.delete(pageNo)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (pages.size === 0) {
|
|
126
|
+
mockPages.delete(filePath as string)
|
|
127
|
+
}
|
|
111
128
|
}
|
|
129
|
+
|
|
112
130
|
return createMockCursor([] as any)
|
|
113
131
|
}
|
|
114
132
|
|
|
115
|
-
|
|
133
|
+
// DELETE FROM vfs_pages WHERE file_path = ?
|
|
134
|
+
if (normalizedQuery === 'DELETE FROM VFS_PAGES WHERE FILE_PATH = ?') {
|
|
116
135
|
const [filePath] = bindings
|
|
117
|
-
|
|
118
|
-
|
|
136
|
+
mockPages.delete(filePath as string)
|
|
137
|
+
return createMockCursor([] as any)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// DELETE FROM vfs_pages (no WHERE - full wipe)
|
|
141
|
+
if (normalizedQuery === 'DELETE FROM VFS_PAGES') {
|
|
142
|
+
mockPages.clear()
|
|
119
143
|
return createMockCursor([] as any)
|
|
120
144
|
}
|
|
121
145
|
|
|
122
|
-
// Default empty result for unhandled queries
|
|
123
146
|
console.warn('Unhandled query:', query, bindings)
|
|
124
147
|
return createMockCursor([] as any)
|
|
125
148
|
},
|
|
126
149
|
|
|
127
150
|
get databaseSize(): number {
|
|
128
|
-
return 1024 * 1024
|
|
151
|
+
return 1024 * 1024
|
|
129
152
|
},
|
|
130
153
|
|
|
131
154
|
Cursor: {} as any,
|
|
132
155
|
Statement: {} as any,
|
|
133
156
|
} as CfTypes.SqlStorage
|
|
134
157
|
|
|
135
|
-
|
|
158
|
+
const createMockCursor = <T extends Record<string, CfTypes.SqlStorageValue>>(
|
|
136
159
|
data: T[],
|
|
137
|
-
): CfTypes.SqlStorageCursor<T> {
|
|
160
|
+
): CfTypes.SqlStorageCursor<T> => {
|
|
138
161
|
let index = 0
|
|
139
162
|
|
|
140
163
|
return {
|
|
@@ -153,7 +176,7 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
|
|
|
153
176
|
},
|
|
154
177
|
raw: function* () {
|
|
155
178
|
for (const item of data) {
|
|
156
|
-
yield Object.values(item)
|
|
179
|
+
yield Object.values(item)
|
|
157
180
|
}
|
|
158
181
|
},
|
|
159
182
|
columnNames: Object.keys(data[0] || {}),
|
|
@@ -171,12 +194,11 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
|
|
|
171
194
|
} as CfTypes.SqlStorageCursor<T>
|
|
172
195
|
}
|
|
173
196
|
|
|
174
|
-
vfs = new
|
|
175
|
-
await vfs.isReady()
|
|
197
|
+
vfs = new CloudflareDurableObjectVFS('test-sql-vfs', mockSql, {})
|
|
176
198
|
})
|
|
177
199
|
|
|
178
200
|
describe('Basic File Operations', () => {
|
|
179
|
-
it('should create and open files',
|
|
201
|
+
it('should create and open files', () => {
|
|
180
202
|
const path = '/test/basic.db'
|
|
181
203
|
const fileId = 1
|
|
182
204
|
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
@@ -185,34 +207,33 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
|
|
|
185
207
|
const result = vfs.jOpen(path, fileId, flags, pOutFlags)
|
|
186
208
|
expect(result).toBe(VFS.SQLITE_OK)
|
|
187
209
|
expect(pOutFlags.getUint32(0, true)).toBe(flags)
|
|
188
|
-
|
|
189
|
-
expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
|
|
190
|
-
|
|
191
|
-
// Verify file was created in mock database
|
|
192
|
-
expect(mockDatabase.has(`file:${path}`)).toBe(true)
|
|
193
210
|
})
|
|
194
211
|
|
|
195
|
-
it('should
|
|
196
|
-
const path = '/test/access.db'
|
|
197
|
-
const fileId = 1
|
|
198
|
-
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
212
|
+
it('should delete only the opened file on close when SQLITE_OPEN_DELETEONCLOSE is set', () => {
|
|
199
213
|
const pOutFlags = new DataView(new ArrayBuffer(4))
|
|
200
214
|
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
expect(
|
|
215
|
+
const keepPath = '/test/keep-on-close.db'
|
|
216
|
+
const keepFileId = 1
|
|
217
|
+
const keepFlags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
218
|
+
vfs.jOpen(keepPath, keepFileId, keepFlags, pOutFlags)
|
|
219
|
+
const keepData = makePage(0x11, 'keep-me')
|
|
220
|
+
expect(vfs.jWrite(keepFileId, keepData, 0)).toBe(VFS.SQLITE_OK)
|
|
221
|
+
|
|
222
|
+
const deletePath = '/test/delete-on-close.db'
|
|
223
|
+
const deleteFileId = 2
|
|
224
|
+
const deleteFlags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE | VFS.SQLITE_OPEN_DELETEONCLOSE
|
|
225
|
+
vfs.jOpen(deletePath, deleteFileId, deleteFlags, pOutFlags)
|
|
226
|
+
expect(vfs.jWrite(deleteFileId, makePage(0x22, 'delete-me'), 0)).toBe(VFS.SQLITE_OK)
|
|
227
|
+
|
|
228
|
+
expect(vfs.jClose(deleteFileId)).toBe(VFS.SQLITE_OK)
|
|
229
|
+
|
|
230
|
+
const readBuffer = new Uint8Array(keepData.length)
|
|
231
|
+
expect(vfs.jRead(keepFileId, readBuffer, 0)).toBe(VFS.SQLITE_OK)
|
|
232
|
+
expect(readBuffer).toEqual(keepData)
|
|
233
|
+
expect(mockPages.has(deletePath)).toBe(false)
|
|
213
234
|
})
|
|
214
235
|
|
|
215
|
-
it('should handle basic read/write operations',
|
|
236
|
+
it('should handle basic read/write operations', () => {
|
|
216
237
|
const path = '/test/readwrite.db'
|
|
217
238
|
const fileId = 1
|
|
218
239
|
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
@@ -221,18 +242,36 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
|
|
|
221
242
|
vfs.jOpen(path, fileId, flags, pOutFlags)
|
|
222
243
|
|
|
223
244
|
// Write data
|
|
224
|
-
const testData =
|
|
245
|
+
const testData = makePage(0x33, 'Hello, SQL VFS!')
|
|
225
246
|
expect(vfs.jWrite(fileId, testData, 0)).toBe(VFS.SQLITE_OK)
|
|
226
247
|
|
|
227
248
|
// Read data back
|
|
228
249
|
const readBuffer = new Uint8Array(testData.length)
|
|
229
250
|
expect(vfs.jRead(fileId, readBuffer, 0)).toBe(VFS.SQLITE_OK)
|
|
230
251
|
expect(readBuffer).toEqual(testData)
|
|
252
|
+
})
|
|
231
253
|
|
|
232
|
-
|
|
254
|
+
it('should isolate pages by file path', () => {
|
|
255
|
+
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
256
|
+
const pOutFlags = new DataView(new ArrayBuffer(4))
|
|
257
|
+
|
|
258
|
+
const oldPath = '/test/state-old.db'
|
|
259
|
+
const oldFileId = 1
|
|
260
|
+
vfs.jOpen(oldPath, oldFileId, flags, pOutFlags)
|
|
261
|
+
|
|
262
|
+
const oldData = makePage(0x44, 'stale-state')
|
|
263
|
+
expect(vfs.jWrite(oldFileId, oldData, 0)).toBe(VFS.SQLITE_OK)
|
|
264
|
+
|
|
265
|
+
const newPath = '/test/state-new.db'
|
|
266
|
+
const newFileId = 2
|
|
267
|
+
vfs.jOpen(newPath, newFileId, flags, pOutFlags)
|
|
268
|
+
|
|
269
|
+
const readBuffer = new Uint8Array(oldData.length)
|
|
270
|
+
expect(vfs.jRead(newFileId, readBuffer, 0)).toBe(VFS.SQLITE_OK)
|
|
271
|
+
expect(readBuffer).toEqual(new Uint8Array(oldData.length))
|
|
233
272
|
})
|
|
234
273
|
|
|
235
|
-
it('should handle file size operations',
|
|
274
|
+
it('should handle file size operations', () => {
|
|
236
275
|
const path = '/test/size.db'
|
|
237
276
|
const fileId = 1
|
|
238
277
|
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
@@ -245,18 +284,17 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
|
|
|
245
284
|
expect(vfs.jFileSize(fileId, pSize64)).toBe(VFS.SQLITE_OK)
|
|
246
285
|
expect(pSize64.getBigInt64(0, true)).toBe(0n)
|
|
247
286
|
|
|
248
|
-
// Write
|
|
249
|
-
const
|
|
287
|
+
// Write one full page (8 KiB) and check size
|
|
288
|
+
const pageSize = CF_SQL_VFS_PAGE_SIZE
|
|
289
|
+
const testData = new Uint8Array(pageSize)
|
|
250
290
|
testData.fill(0xaa)
|
|
251
291
|
vfs.jWrite(fileId, testData, 0)
|
|
252
292
|
|
|
253
293
|
expect(vfs.jFileSize(fileId, pSize64)).toBe(VFS.SQLITE_OK)
|
|
254
|
-
expect(pSize64.getBigInt64(0, true)).toBe(
|
|
255
|
-
|
|
256
|
-
vfs.jClose(fileId)
|
|
294
|
+
expect(pSize64.getBigInt64(0, true)).toBe(BigInt(pageSize))
|
|
257
295
|
})
|
|
258
296
|
|
|
259
|
-
it('should handle file truncation',
|
|
297
|
+
it('should handle file truncation', () => {
|
|
260
298
|
const path = '/test/truncate.db'
|
|
261
299
|
const fileId = 1
|
|
262
300
|
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
@@ -264,42 +302,21 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
|
|
|
264
302
|
|
|
265
303
|
vfs.jOpen(path, fileId, flags, pOutFlags)
|
|
266
304
|
|
|
267
|
-
// Write
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
vfs.jWrite(fileId,
|
|
305
|
+
// Write two full pages
|
|
306
|
+
const pageSize = CF_SQL_VFS_PAGE_SIZE
|
|
307
|
+
vfs.jWrite(fileId, makePage(0xbb), 0)
|
|
308
|
+
vfs.jWrite(fileId, makePage(0xcc), pageSize)
|
|
271
309
|
|
|
272
|
-
// Truncate to
|
|
273
|
-
expect(vfs.jTruncate(fileId,
|
|
310
|
+
// Truncate to one page
|
|
311
|
+
expect(vfs.jTruncate(fileId, pageSize)).toBe(VFS.SQLITE_OK)
|
|
274
312
|
|
|
275
313
|
// Verify size
|
|
276
314
|
const pSize64 = new DataView(new ArrayBuffer(8))
|
|
277
315
|
expect(vfs.jFileSize(fileId, pSize64)).toBe(VFS.SQLITE_OK)
|
|
278
|
-
expect(pSize64.getBigInt64(0, true)).toBe(
|
|
279
|
-
|
|
280
|
-
vfs.jClose(fileId)
|
|
316
|
+
expect(pSize64.getBigInt64(0, true)).toBe(BigInt(pageSize))
|
|
281
317
|
})
|
|
282
318
|
|
|
283
|
-
it('should handle
|
|
284
|
-
const path = '/test/sync.db'
|
|
285
|
-
const fileId = 1
|
|
286
|
-
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
287
|
-
const pOutFlags = new DataView(new ArrayBuffer(4))
|
|
288
|
-
|
|
289
|
-
vfs.jOpen(path, fileId, flags, pOutFlags)
|
|
290
|
-
|
|
291
|
-
const testData = new TextEncoder().encode('Sync test data')
|
|
292
|
-
vfs.jWrite(fileId, testData, 0)
|
|
293
|
-
|
|
294
|
-
// Test different sync modes - should all be no-ops for SQL VFS
|
|
295
|
-
expect(vfs.jSync(fileId, VFS.SQLITE_SYNC_NORMAL)).toBe(VFS.SQLITE_OK)
|
|
296
|
-
expect(vfs.jSync(fileId, VFS.SQLITE_SYNC_FULL)).toBe(VFS.SQLITE_OK)
|
|
297
|
-
expect(vfs.jSync(fileId, VFS.SQLITE_SYNC_DATAONLY)).toBe(VFS.SQLITE_OK)
|
|
298
|
-
|
|
299
|
-
vfs.jClose(fileId)
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
it('should handle file deletion', async () => {
|
|
319
|
+
it('should handle file deletion', () => {
|
|
303
320
|
const path = '/test/delete.db'
|
|
304
321
|
const fileId = 1
|
|
305
322
|
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
@@ -307,83 +324,80 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
|
|
|
307
324
|
|
|
308
325
|
// Create file
|
|
309
326
|
vfs.jOpen(path, fileId, flags, pOutFlags)
|
|
310
|
-
const testData =
|
|
327
|
+
const testData = makePage(0x55, 'Delete test')
|
|
311
328
|
vfs.jWrite(fileId, testData, 0)
|
|
312
|
-
vfs.jClose(fileId)
|
|
313
329
|
|
|
314
330
|
// Delete file
|
|
315
331
|
expect(vfs.jDelete(path, 0)).toBe(VFS.SQLITE_OK)
|
|
316
332
|
|
|
317
|
-
// Verify
|
|
318
|
-
expect(
|
|
319
|
-
expect(mockDatabase.has(`blocks:${path}`)).toBe(false)
|
|
333
|
+
// Verify pages are gone
|
|
334
|
+
expect(mockPages.size).toBe(0)
|
|
320
335
|
})
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
describe('VFS Management', () => {
|
|
324
|
-
it('should provide correct VFS characteristics', () => {
|
|
325
|
-
expect(vfs.jSectorSize(1)).toBe(4096)
|
|
326
|
-
expect(vfs.jDeviceCharacteristics(1)).toBe(VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN)
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
it('should handle multiple files', async () => {
|
|
330
|
-
const files = [
|
|
331
|
-
{ path: '/test/file1.db', id: 1 },
|
|
332
|
-
{ path: '/test/file2.db', id: 2 },
|
|
333
|
-
{ path: '/test/file3.db', id: 3 },
|
|
334
|
-
]
|
|
335
336
|
|
|
337
|
+
it('should delete only the requested file path', () => {
|
|
336
338
|
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
337
339
|
const pOutFlags = new DataView(new ArrayBuffer(4))
|
|
338
340
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
341
|
+
const keepPath = '/test/keep.db'
|
|
342
|
+
const keepFileId = 1
|
|
343
|
+
vfs.jOpen(keepPath, keepFileId, flags, pOutFlags)
|
|
344
|
+
const keepData = makePage(0x66, 'keep-me')
|
|
345
|
+
expect(vfs.jWrite(keepFileId, keepData, 0)).toBe(VFS.SQLITE_OK)
|
|
343
346
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
347
|
+
const deletePath = '/test/delete-only.db'
|
|
348
|
+
const deleteFileId = 2
|
|
349
|
+
vfs.jOpen(deletePath, deleteFileId, flags, pOutFlags)
|
|
350
|
+
expect(vfs.jWrite(deleteFileId, makePage(0x77, 'delete-me'), 0)).toBe(VFS.SQLITE_OK)
|
|
349
351
|
|
|
350
|
-
|
|
351
|
-
for (let i = 0; i < files.length; i++) {
|
|
352
|
-
const expected = new TextEncoder().encode(`File ${i + 1} data`)
|
|
353
|
-
const actual = new Uint8Array(expected.length)
|
|
354
|
-
expect(vfs.jRead(files[i]?.id ?? 0, actual, 0)).toBe(VFS.SQLITE_OK)
|
|
355
|
-
expect(actual).toEqual(expected)
|
|
356
|
-
}
|
|
352
|
+
expect(vfs.jDelete(deletePath, 0)).toBe(VFS.SQLITE_OK)
|
|
357
353
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
354
|
+
const readBuffer = new Uint8Array(keepData.length)
|
|
355
|
+
expect(vfs.jRead(keepFileId, readBuffer, 0)).toBe(VFS.SQLITE_OK)
|
|
356
|
+
expect(readBuffer).toEqual(keepData)
|
|
357
|
+
expect(mockPages.has(deletePath)).toBe(false)
|
|
362
358
|
})
|
|
359
|
+
})
|
|
363
360
|
|
|
361
|
+
describe('VFS Management', () => {
|
|
364
362
|
it('should provide VFS statistics', () => {
|
|
365
363
|
const stats = vfs.getStats()
|
|
366
|
-
expect(stats).toHaveProperty('
|
|
367
|
-
expect(stats).toHaveProperty('
|
|
368
|
-
expect(stats).toHaveProperty('maxFiles')
|
|
369
|
-
expect(stats).toHaveProperty('blockSize')
|
|
364
|
+
expect(stats).toHaveProperty('pageSize')
|
|
365
|
+
expect(stats).toHaveProperty('totalPages')
|
|
370
366
|
expect(stats).toHaveProperty('totalStoredBytes')
|
|
371
|
-
expect(stats.
|
|
367
|
+
expect(stats.pageSize).toBe(CF_SQL_VFS_PAGE_SIZE)
|
|
372
368
|
})
|
|
373
369
|
})
|
|
374
370
|
|
|
375
371
|
describe('Error Handling', () => {
|
|
376
|
-
it('should handle
|
|
372
|
+
it('should return SQLITE_IOERR for handle-based operations on unknown file IDs', () => {
|
|
377
373
|
const invalidFileId = 999
|
|
378
|
-
const buffer = new Uint8Array(
|
|
374
|
+
const buffer = new Uint8Array(CF_SQL_VFS_PAGE_SIZE)
|
|
375
|
+
const pSize64 = new DataView(new ArrayBuffer(8))
|
|
379
376
|
|
|
380
377
|
expect(vfs.jRead(invalidFileId, buffer, 0)).toBe(VFS.SQLITE_IOERR)
|
|
381
378
|
expect(vfs.jWrite(invalidFileId, buffer, 0)).toBe(VFS.SQLITE_IOERR)
|
|
382
|
-
expect(vfs.jTruncate(invalidFileId,
|
|
383
|
-
expect(vfs.
|
|
379
|
+
expect(vfs.jTruncate(invalidFileId, 0)).toBe(VFS.SQLITE_IOERR)
|
|
380
|
+
expect(vfs.jFileSize(invalidFileId, pSize64)).toBe(VFS.SQLITE_IOERR)
|
|
384
381
|
expect(vfs.jClose(invalidFileId)).toBe(VFS.SQLITE_OK)
|
|
385
382
|
})
|
|
386
383
|
|
|
384
|
+
it('should return SQLITE_IOERR for handle-based operations after close', () => {
|
|
385
|
+
const path = '/test/closed.db'
|
|
386
|
+
const fileId = 1
|
|
387
|
+
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
388
|
+
const pOutFlags = new DataView(new ArrayBuffer(4))
|
|
389
|
+
const buffer = new Uint8Array(CF_SQL_VFS_PAGE_SIZE)
|
|
390
|
+
const pSize64 = new DataView(new ArrayBuffer(8))
|
|
391
|
+
|
|
392
|
+
expect(vfs.jOpen(path, fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
|
|
393
|
+
expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
|
|
394
|
+
|
|
395
|
+
expect(vfs.jRead(fileId, buffer, 0)).toBe(VFS.SQLITE_IOERR)
|
|
396
|
+
expect(vfs.jWrite(fileId, buffer, 0)).toBe(VFS.SQLITE_IOERR)
|
|
397
|
+
expect(vfs.jTruncate(fileId, 0)).toBe(VFS.SQLITE_IOERR)
|
|
398
|
+
expect(vfs.jFileSize(fileId, pSize64)).toBe(VFS.SQLITE_IOERR)
|
|
399
|
+
})
|
|
400
|
+
|
|
387
401
|
it('should handle invalid paths', () => {
|
|
388
402
|
const invalidPath = ''
|
|
389
403
|
const fileId = 1
|
|
@@ -393,20 +407,40 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
|
|
|
393
407
|
expect(vfs.jOpen(invalidPath, fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
|
|
394
408
|
})
|
|
395
409
|
|
|
396
|
-
it('should
|
|
397
|
-
const path = '/test/
|
|
410
|
+
it('should reject writes that do not match the configured page size', () => {
|
|
411
|
+
const path = '/test/page-size-mismatch.db'
|
|
398
412
|
const fileId = 1
|
|
399
413
|
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
400
414
|
const pOutFlags = new DataView(new ArrayBuffer(4))
|
|
401
415
|
|
|
402
|
-
|
|
403
|
-
vfs.
|
|
404
|
-
|
|
416
|
+
expect(vfs.jOpen(path, fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
|
|
417
|
+
expect(vfs.jWrite(fileId, new Uint8Array(CF_SQL_VFS_PAGE_SIZE / 2), 0)).toBe(VFS.SQLITE_IOERR)
|
|
418
|
+
expect(mockPages.size).toBe(0)
|
|
419
|
+
})
|
|
405
420
|
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
421
|
+
it('should reject writes at non-page-aligned offsets', () => {
|
|
422
|
+
const path = '/test/page-offset-mismatch.db'
|
|
423
|
+
const fileId = 1
|
|
424
|
+
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
425
|
+
const pOutFlags = new DataView(new ArrayBuffer(4))
|
|
426
|
+
|
|
427
|
+
expect(vfs.jOpen(path, fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
|
|
428
|
+
expect(vfs.jWrite(fileId, makePage(0x88), CF_SQL_VFS_PAGE_SIZE / 2)).toBe(VFS.SQLITE_IOERR)
|
|
429
|
+
expect(mockPages.size).toBe(0)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('should always report not found from jAccess', () => {
|
|
433
|
+
const path = '/test/access.db'
|
|
434
|
+
const fileId = 1
|
|
435
|
+
const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
|
|
436
|
+
const pOutFlags = new DataView(new ArrayBuffer(4))
|
|
437
|
+
const pResOut = new DataView(new ArrayBuffer(4))
|
|
438
|
+
|
|
439
|
+
expect(vfs.jOpen(path, fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
|
|
440
|
+
expect(vfs.jWrite(fileId, makePage(0x99, 'access-test'), 0)).toBe(VFS.SQLITE_OK)
|
|
441
|
+
|
|
442
|
+
expect(vfs.jAccess(path, VFS.SQLITE_ACCESS_EXISTS, pResOut)).toBe(VFS.SQLITE_OK)
|
|
443
|
+
expect(pResOut.getUint32(0, true)).toBe(0)
|
|
410
444
|
})
|
|
411
445
|
})
|
|
412
446
|
|
package/src/in-memory-vfs.ts
CHANGED
|
@@ -5,7 +5,6 @@ let cachedMemoryVfs: MemoryVFS | undefined
|
|
|
5
5
|
|
|
6
6
|
export const makeInMemoryDb = (sqlite3: WaSqlite.SQLiteAPI) => {
|
|
7
7
|
if (sqlite3.vfs_registered.has('memory-vfs') === false) {
|
|
8
|
-
// @ts-expect-error TODO fix types
|
|
9
8
|
const vfs = new MemoryVFS('memory-vfs', (sqlite3 as any).module)
|
|
10
9
|
|
|
11
10
|
// @ts-expect-error TODO fix types
|
|
@@ -6,7 +6,7 @@ export const loadSqlite3Wasm = async () => {
|
|
|
6
6
|
// https://github.com/rhashimoto/wa-sqlite/issues/143#issuecomment-1899060056
|
|
7
7
|
// module._free(module._malloc(10_000 * 4096 + 65_536))
|
|
8
8
|
const sqlite3 = WaSqlite.Factory(module)
|
|
9
|
-
// @ts-expect-error
|
|
9
|
+
// @ts-expect-error Adding module property for internal use (not in wa-sqlite types)
|
|
10
10
|
sqlite3.module = module
|
|
11
11
|
return sqlite3
|
|
12
12
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as WaSqlite from '@livestore/wa-sqlite'
|
|
2
2
|
import WaSqliteFactory from '@livestore/wa-sqlite/dist/wa-sqlite.mjs'
|
|
3
|
-
|
|
4
3
|
// @ts-expect-error TODO fix types in wa-sqlite
|
|
5
4
|
import wasm from '@livestore/wa-sqlite/dist/wa-sqlite.wasm'
|
|
6
5
|
|
|
6
|
+
// Note that it's important that the `workerd` entry is the first one in the `exports` map in the `package.json` file.
|
|
7
7
|
export const loadSqlite3Wasm = async () => {
|
|
8
8
|
// It seems we need to pass in the wasm binary directly for workerd to work
|
|
9
|
+
// This is due to workerd not supporting the same patterns as Vite for automatically loading WASM.
|
|
9
10
|
const module = await WaSqliteFactory({
|
|
10
11
|
instantiateWasm: (info: any, receiveInstance: any) => {
|
|
11
12
|
try {
|