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