bun-sqlite-for-rxdb 1.0.1
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/.serena/project.yml +84 -0
- package/CHANGELOG.md +300 -0
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/ROADMAP.md +532 -0
- package/benchmarks/benchmark.ts +145 -0
- package/benchmarks/case-insensitive-10runs.ts +156 -0
- package/benchmarks/fts5-1m-scale.ts +126 -0
- package/benchmarks/fts5-before-after.ts +104 -0
- package/benchmarks/indexed-benchmark.ts +141 -0
- package/benchmarks/new-operators-benchmark.ts +140 -0
- package/benchmarks/query-builder-benchmark.ts +88 -0
- package/benchmarks/query-builder-consistency.ts +109 -0
- package/benchmarks/raw-better-sqlite3-10m.ts +85 -0
- package/benchmarks/raw-better-sqlite3.ts +86 -0
- package/benchmarks/raw-bun-sqlite-10m.ts +85 -0
- package/benchmarks/raw-bun-sqlite.ts +86 -0
- package/benchmarks/regex-10runs-all.ts +216 -0
- package/benchmarks/regex-comparison-benchmark.ts +161 -0
- package/benchmarks/regex-real-comparison.ts +213 -0
- package/benchmarks/run-10x.sh +19 -0
- package/benchmarks/smart-regex-benchmark.ts +148 -0
- package/benchmarks/sql-vs-mingo-benchmark.ts +210 -0
- package/benchmarks/sql-vs-mingo-comparison.ts +175 -0
- package/benchmarks/text-vs-jsonb.ts +167 -0
- package/benchmarks/wal-benchmark.ts +112 -0
- package/docs/architectural-patterns.md +1336 -0
- package/docs/id1-testsuite-journey.md +839 -0
- package/docs/official-test-suite-setup.md +393 -0
- package/nul +0 -0
- package/package.json +44 -0
- package/src/changestream.test.ts +182 -0
- package/src/cleanup.test.ts +110 -0
- package/src/collection-isolation.test.ts +74 -0
- package/src/connection-pool.test.ts +102 -0
- package/src/connection-pool.ts +38 -0
- package/src/findDocumentsById.test.ts +122 -0
- package/src/index.ts +2 -0
- package/src/instance.ts +382 -0
- package/src/multi-instance-events.test.ts +204 -0
- package/src/query/and-operator.test.ts +39 -0
- package/src/query/builder.test.ts +96 -0
- package/src/query/builder.ts +154 -0
- package/src/query/elemMatch-operator.test.ts +24 -0
- package/src/query/exists-operator.test.ts +28 -0
- package/src/query/in-operators.test.ts +54 -0
- package/src/query/mod-operator.test.ts +22 -0
- package/src/query/nested-query.test.ts +198 -0
- package/src/query/not-operators.test.ts +49 -0
- package/src/query/operators.test.ts +70 -0
- package/src/query/operators.ts +185 -0
- package/src/query/or-operator.test.ts +68 -0
- package/src/query/regex-escaping-regression.test.ts +43 -0
- package/src/query/regex-operator.test.ts +44 -0
- package/src/query/schema-mapper.ts +27 -0
- package/src/query/size-operator.test.ts +22 -0
- package/src/query/smart-regex.ts +52 -0
- package/src/query/type-operator.test.ts +37 -0
- package/src/query-cache.test.ts +286 -0
- package/src/rxdb-helpers.test.ts +348 -0
- package/src/rxdb-helpers.ts +262 -0
- package/src/schema-version-isolation.test.ts +126 -0
- package/src/statement-manager.ts +69 -0
- package/src/storage.test.ts +589 -0
- package/src/storage.ts +21 -0
- package/src/types.ts +14 -0
- package/test/rxdb-test-suite.ts +27 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { getRxStorageBunSQLite } from './storage';
|
|
3
|
+
import type { RxDocumentData, RxStorage, RxStorageInstance, PreparedQuery, EventBulk, RxStorageChangeEvent } from 'rxdb';
|
|
4
|
+
import type { BunSQLiteStorageSettings, BunSQLiteInternals } from './types';
|
|
5
|
+
|
|
6
|
+
interface TestDocType {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
age: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('BunSQLiteStorage', () => {
|
|
13
|
+
it('creates storage instance', async () => {
|
|
14
|
+
const storage = getRxStorageBunSQLite();
|
|
15
|
+
|
|
16
|
+
expect(storage.name).toBe('bun-sqlite');
|
|
17
|
+
expect(storage.rxdbVersion).toBe('16.21.1');
|
|
18
|
+
|
|
19
|
+
const instance = await storage.createStorageInstance<TestDocType>({
|
|
20
|
+
databaseInstanceToken: 'test-token',
|
|
21
|
+
databaseName: 'testdb',
|
|
22
|
+
collectionName: 'users',
|
|
23
|
+
schema: {
|
|
24
|
+
version: 0,
|
|
25
|
+
primaryKey: 'id',
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
id: { type: 'string', maxLength: 100 },
|
|
29
|
+
name: { type: 'string' },
|
|
30
|
+
age: { type: 'number' },
|
|
31
|
+
_deleted: { type: 'boolean' },
|
|
32
|
+
_attachments: { type: 'object' },
|
|
33
|
+
_rev: { type: 'string' },
|
|
34
|
+
_meta: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
lwt: { type: 'number' }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
required: ['id', '_deleted', '_attachments', '_rev', '_meta']
|
|
42
|
+
},
|
|
43
|
+
options: {},
|
|
44
|
+
multiInstance: false,
|
|
45
|
+
devMode: false
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(instance.databaseName).toBe('testdb');
|
|
49
|
+
expect(instance.collectionName).toBe('users');
|
|
50
|
+
|
|
51
|
+
await instance.close();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('BunSQLiteStorageInstance', () => {
|
|
56
|
+
let storage: RxStorage<BunSQLiteInternals, BunSQLiteStorageSettings>;
|
|
57
|
+
let instance: RxStorageInstance<TestDocType, BunSQLiteInternals, BunSQLiteStorageSettings>;
|
|
58
|
+
|
|
59
|
+
beforeEach(async () => {
|
|
60
|
+
storage = getRxStorageBunSQLite();
|
|
61
|
+
instance = await storage.createStorageInstance<TestDocType>({
|
|
62
|
+
databaseInstanceToken: 'test-token',
|
|
63
|
+
databaseName: 'testdb',
|
|
64
|
+
collectionName: 'users',
|
|
65
|
+
schema: {
|
|
66
|
+
version: 0,
|
|
67
|
+
primaryKey: 'id',
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
id: { type: 'string', maxLength: 100 },
|
|
71
|
+
name: { type: 'string' },
|
|
72
|
+
age: { type: 'number' },
|
|
73
|
+
_deleted: { type: 'boolean' },
|
|
74
|
+
_attachments: { type: 'object' },
|
|
75
|
+
_rev: { type: 'string' },
|
|
76
|
+
_meta: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
lwt: { type: 'number' }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
required: ['id', '_deleted', '_attachments', '_rev', '_meta']
|
|
84
|
+
},
|
|
85
|
+
options: {},
|
|
86
|
+
multiInstance: false,
|
|
87
|
+
devMode: false
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('bulkWrite inserts documents', async () => {
|
|
92
|
+
const doc: RxDocumentData<TestDocType> = {
|
|
93
|
+
id: 'user1',
|
|
94
|
+
name: 'Alice',
|
|
95
|
+
age: 30,
|
|
96
|
+
_deleted: false,
|
|
97
|
+
_attachments: {},
|
|
98
|
+
_rev: '1-abc',
|
|
99
|
+
_meta: { lwt: Date.now() }
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = await instance.bulkWrite([{ document: doc }], 'test-context');
|
|
103
|
+
|
|
104
|
+
expect(result.error).toHaveLength(0);
|
|
105
|
+
await instance.remove();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('findDocumentsById retrieves documents', async () => {
|
|
109
|
+
const doc: RxDocumentData<TestDocType> = {
|
|
110
|
+
id: 'user1',
|
|
111
|
+
name: 'Alice',
|
|
112
|
+
age: 30,
|
|
113
|
+
_deleted: false,
|
|
114
|
+
_attachments: {},
|
|
115
|
+
_rev: '1-abc',
|
|
116
|
+
_meta: { lwt: Date.now() }
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
await instance.bulkWrite([{ document: doc }], 'test-context');
|
|
120
|
+
|
|
121
|
+
const found = await instance.findDocumentsById(['user1'], false);
|
|
122
|
+
|
|
123
|
+
expect(found).toHaveLength(1);
|
|
124
|
+
expect(found[0].id).toBe('user1');
|
|
125
|
+
expect(found[0].name).toBe('Alice');
|
|
126
|
+
await instance.remove();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('query returns all documents', async () => {
|
|
130
|
+
const docs: RxDocumentData<TestDocType>[] = [
|
|
131
|
+
{ id: 'user1', name: 'Alice', age: 30, _deleted: false, _attachments: {}, _rev: '1-a', _meta: { lwt: Date.now() } },
|
|
132
|
+
{ id: 'user2', name: 'Bob', age: 25, _deleted: false, _attachments: {}, _rev: '1-b', _meta: { lwt: Date.now() } }
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
await instance.bulkWrite(docs.map(doc => ({ document: doc })), 'test-context');
|
|
136
|
+
|
|
137
|
+
const result = await instance.query({
|
|
138
|
+
query: { selector: {}, sort: [], skip: 0 },
|
|
139
|
+
queryPlan: { index: [], startKeys: [], endKeys: [], inclusiveStart: true, inclusiveEnd: true, sortSatisfiedByIndex: false, selectorSatisfiedByIndex: false }
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.documents).toHaveLength(2);
|
|
143
|
+
await instance.remove();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('query filters by selector', async () => {
|
|
147
|
+
const docs: RxDocumentData<TestDocType>[] = [
|
|
148
|
+
{ id: 'user1', name: 'Alice', age: 30, _deleted: false, _attachments: {}, _rev: '1-a', _meta: { lwt: Date.now() } },
|
|
149
|
+
{ id: 'user2', name: 'Bob', age: 25, _deleted: false, _attachments: {}, _rev: '1-b', _meta: { lwt: Date.now() } }
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
await instance.bulkWrite(docs.map(doc => ({ document: doc })), 'test-context');
|
|
153
|
+
|
|
154
|
+
const result = await instance.query({
|
|
155
|
+
query: { selector: { age: { $gt: 26 } }, sort: [], skip: 0 },
|
|
156
|
+
queryPlan: { index: [], startKeys: [], endKeys: [], inclusiveStart: true, inclusiveEnd: true, sortSatisfiedByIndex: false, selectorSatisfiedByIndex: false }
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result.documents).toHaveLength(1);
|
|
160
|
+
expect(result.documents[0].name).toBe('Alice');
|
|
161
|
+
await instance.remove();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('count returns document count', async () => {
|
|
165
|
+
const docs: RxDocumentData<TestDocType>[] = [
|
|
166
|
+
{ id: 'user1', name: 'Alice', age: 30, _deleted: false, _attachments: {}, _rev: '1-a', _meta: { lwt: Date.now() } },
|
|
167
|
+
{ id: 'user2', name: 'Bob', age: 25, _deleted: false, _attachments: {}, _rev: '1-b', _meta: { lwt: Date.now() } }
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
await instance.bulkWrite(docs.map(doc => ({ document: doc })), 'test-context');
|
|
171
|
+
|
|
172
|
+
const result = await instance.count({
|
|
173
|
+
query: { selector: {}, sort: [], skip: 0 },
|
|
174
|
+
queryPlan: { index: [], startKeys: [], endKeys: [], inclusiveStart: true, inclusiveEnd: true, sortSatisfiedByIndex: false, selectorSatisfiedByIndex: false }
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(result.count).toBe(2);
|
|
178
|
+
expect(result.mode).toBe('fast');
|
|
179
|
+
await instance.remove();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('cleanup removes old deleted documents', async () => {
|
|
183
|
+
const doc: RxDocumentData<TestDocType> = {
|
|
184
|
+
id: 'user1',
|
|
185
|
+
name: 'Alice',
|
|
186
|
+
age: 30,
|
|
187
|
+
_deleted: true,
|
|
188
|
+
_attachments: {},
|
|
189
|
+
_rev: '2-deleted',
|
|
190
|
+
_meta: { lwt: Date.now() - 10000 }
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
await instance.bulkWrite([{ document: doc }], 'test-context');
|
|
194
|
+
|
|
195
|
+
const cleaned = await instance.cleanup(Date.now() - 5000);
|
|
196
|
+
|
|
197
|
+
expect(cleaned).toBe(false);
|
|
198
|
+
|
|
199
|
+
const found = await instance.findDocumentsById(['user1'], true);
|
|
200
|
+
expect(found).toHaveLength(0);
|
|
201
|
+
await instance.remove();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('changeStream emits events', async () => {
|
|
205
|
+
const events: EventBulk<RxStorageChangeEvent<TestDocType>, unknown>[] = [];
|
|
206
|
+
const subscription = instance.changeStream().subscribe((event) => {
|
|
207
|
+
events.push(event);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const doc: RxDocumentData<TestDocType> = {
|
|
211
|
+
id: 'user1',
|
|
212
|
+
name: 'Alice',
|
|
213
|
+
age: 30,
|
|
214
|
+
_deleted: false,
|
|
215
|
+
_attachments: {},
|
|
216
|
+
_rev: '1-abc',
|
|
217
|
+
_meta: { lwt: Date.now() }
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
await instance.bulkWrite([{ document: doc }], 'test-context');
|
|
221
|
+
|
|
222
|
+
expect(events).toHaveLength(1);
|
|
223
|
+
expect(events[0].context).toBe('test-context');
|
|
224
|
+
|
|
225
|
+
subscription.unsubscribe();
|
|
226
|
+
await instance.remove();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('enables WAL mode for performance', async () => {
|
|
230
|
+
const storage = getRxStorageBunSQLite({ filename: './test-wal.db' });
|
|
231
|
+
const instance = await storage.createStorageInstance<TestDocType>({
|
|
232
|
+
databaseInstanceToken: 'test-token',
|
|
233
|
+
databaseName: 'testdb-wal',
|
|
234
|
+
collectionName: 'wal_test',
|
|
235
|
+
schema: {
|
|
236
|
+
version: 0,
|
|
237
|
+
primaryKey: 'id',
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
id: { type: 'string', maxLength: 100 },
|
|
241
|
+
name: { type: 'string' },
|
|
242
|
+
age: { type: 'number' },
|
|
243
|
+
_deleted: { type: 'boolean' },
|
|
244
|
+
_attachments: { type: 'object' },
|
|
245
|
+
_rev: { type: 'string' },
|
|
246
|
+
_meta: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {
|
|
249
|
+
lwt: { type: 'number' }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
required: ['id', '_deleted', '_attachments', '_rev', '_meta']
|
|
254
|
+
},
|
|
255
|
+
options: {},
|
|
256
|
+
multiInstance: false,
|
|
257
|
+
devMode: false
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const db = instance.internals.db;
|
|
261
|
+
const result = db.prepare('PRAGMA journal_mode').get() as { journal_mode: string };
|
|
262
|
+
|
|
263
|
+
expect(result.journal_mode).toBe('wal');
|
|
264
|
+
|
|
265
|
+
await instance.remove();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('detects conflicts and returns 409 error', async () => {
|
|
269
|
+
const storage = getRxStorageBunSQLite();
|
|
270
|
+
const instance = await storage.createStorageInstance<TestDocType>({
|
|
271
|
+
databaseInstanceToken: 'test-token',
|
|
272
|
+
databaseName: 'testdb',
|
|
273
|
+
collectionName: 'conflict_test',
|
|
274
|
+
schema: {
|
|
275
|
+
version: 0,
|
|
276
|
+
primaryKey: 'id',
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: {
|
|
279
|
+
id: { type: 'string', maxLength: 100 },
|
|
280
|
+
name: { type: 'string' },
|
|
281
|
+
age: { type: 'number' },
|
|
282
|
+
_deleted: { type: 'boolean' },
|
|
283
|
+
_attachments: { type: 'object' },
|
|
284
|
+
_rev: { type: 'string' },
|
|
285
|
+
_meta: {
|
|
286
|
+
type: 'object',
|
|
287
|
+
properties: {
|
|
288
|
+
lwt: { type: 'number' }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
required: ['id', '_deleted', '_attachments', '_rev', '_meta']
|
|
293
|
+
},
|
|
294
|
+
options: {},
|
|
295
|
+
multiInstance: false,
|
|
296
|
+
devMode: false
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const doc: RxDocumentData<TestDocType> = {
|
|
300
|
+
id: 'conflict-1',
|
|
301
|
+
name: 'Original',
|
|
302
|
+
age: 30,
|
|
303
|
+
_deleted: false,
|
|
304
|
+
_attachments: {},
|
|
305
|
+
_rev: '1-abc',
|
|
306
|
+
_meta: { lwt: Date.now() }
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
await instance.bulkWrite([{ document: doc }], 'test');
|
|
310
|
+
|
|
311
|
+
const conflictDoc: RxDocumentData<TestDocType> = {
|
|
312
|
+
...doc,
|
|
313
|
+
name: 'Conflict',
|
|
314
|
+
_rev: '2-def'
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const result = await instance.bulkWrite([{ document: conflictDoc }], 'test');
|
|
318
|
+
|
|
319
|
+
expect(result.error.length).toBe(1);
|
|
320
|
+
expect(result.error[0].status).toBe(409);
|
|
321
|
+
expect(result.error[0].documentId).toBe('conflict-1');
|
|
322
|
+
if ('documentInDb' in result.error[0]) {
|
|
323
|
+
expect(result.error[0].documentInDb?.name).toBe('Original');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await instance.remove();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('emits checkpoint with correct structure', async () => {
|
|
330
|
+
const storage = getRxStorageBunSQLite();
|
|
331
|
+
const instance = await storage.createStorageInstance<TestDocType>({
|
|
332
|
+
databaseInstanceToken: 'test-token',
|
|
333
|
+
databaseName: 'testdb',
|
|
334
|
+
collectionName: 'checkpoint_test',
|
|
335
|
+
schema: {
|
|
336
|
+
version: 0,
|
|
337
|
+
primaryKey: 'id',
|
|
338
|
+
type: 'object',
|
|
339
|
+
properties: {
|
|
340
|
+
id: { type: 'string', maxLength: 100 },
|
|
341
|
+
name: { type: 'string' },
|
|
342
|
+
age: { type: 'number' },
|
|
343
|
+
_deleted: { type: 'boolean' },
|
|
344
|
+
_attachments: { type: 'object' },
|
|
345
|
+
_rev: { type: 'string' },
|
|
346
|
+
_meta: {
|
|
347
|
+
type: 'object',
|
|
348
|
+
properties: {
|
|
349
|
+
lwt: { type: 'number' }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
required: ['id', '_deleted', '_attachments', '_rev', '_meta']
|
|
354
|
+
},
|
|
355
|
+
options: {},
|
|
356
|
+
multiInstance: false,
|
|
357
|
+
devMode: false
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const events: any[] = [];
|
|
361
|
+
const subscription = instance.changeStream().subscribe(event => {
|
|
362
|
+
events.push(event);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const lwt = Date.now();
|
|
366
|
+
const doc: RxDocumentData<TestDocType> = {
|
|
367
|
+
id: 'checkpoint-1',
|
|
368
|
+
name: 'Test',
|
|
369
|
+
age: 25,
|
|
370
|
+
_deleted: false,
|
|
371
|
+
_attachments: {},
|
|
372
|
+
_rev: '1-abc',
|
|
373
|
+
_meta: { lwt }
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
await instance.bulkWrite([{ document: doc }], 'test');
|
|
377
|
+
|
|
378
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
379
|
+
|
|
380
|
+
expect(events.length).toBe(1);
|
|
381
|
+
expect(events[0].checkpoint).toEqual({ id: 'checkpoint-1', lwt });
|
|
382
|
+
|
|
383
|
+
subscription.unsubscribe();
|
|
384
|
+
await instance.remove();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('uses MessagePack for efficient storage', async () => {
|
|
388
|
+
const storage = getRxStorageBunSQLite();
|
|
389
|
+
const instance = await storage.createStorageInstance<TestDocType>({
|
|
390
|
+
databaseInstanceToken: 'test-token',
|
|
391
|
+
databaseName: 'testdb',
|
|
392
|
+
collectionName: 'msgpack_test',
|
|
393
|
+
schema: {
|
|
394
|
+
version: 0,
|
|
395
|
+
primaryKey: 'id',
|
|
396
|
+
type: 'object',
|
|
397
|
+
properties: {
|
|
398
|
+
id: { type: 'string', maxLength: 100 },
|
|
399
|
+
name: { type: 'string' },
|
|
400
|
+
age: { type: 'number' },
|
|
401
|
+
_deleted: { type: 'boolean' },
|
|
402
|
+
_attachments: { type: 'object' },
|
|
403
|
+
_rev: { type: 'string' },
|
|
404
|
+
_meta: {
|
|
405
|
+
type: 'object',
|
|
406
|
+
properties: {
|
|
407
|
+
lwt: { type: 'number' }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
required: ['id', '_deleted', '_attachments', '_rev', '_meta']
|
|
412
|
+
},
|
|
413
|
+
options: {},
|
|
414
|
+
multiInstance: false,
|
|
415
|
+
devMode: false
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const doc: RxDocumentData<TestDocType> = {
|
|
419
|
+
id: 'msgpack-1',
|
|
420
|
+
name: 'Test MessagePack',
|
|
421
|
+
age: 42,
|
|
422
|
+
_deleted: false,
|
|
423
|
+
_attachments: {},
|
|
424
|
+
_rev: '1-abc',
|
|
425
|
+
_meta: { lwt: Date.now() }
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
await instance.bulkWrite([{ document: doc }], 'test');
|
|
429
|
+
|
|
430
|
+
const result = await instance.findDocumentsById(['msgpack-1'], false);
|
|
431
|
+
|
|
432
|
+
expect(result.length).toBe(1);
|
|
433
|
+
expect(result[0].id).toBe('msgpack-1');
|
|
434
|
+
expect(result[0].name).toBe('Test MessagePack');
|
|
435
|
+
expect(result[0].age).toBe(42);
|
|
436
|
+
|
|
437
|
+
await instance.remove();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('query and count include deleted documents (storage layer returns ALL)', async () => {
|
|
441
|
+
const docs: RxDocumentData<TestDocType>[] = [
|
|
442
|
+
{ id: 'user1', name: 'Alice', age: 30, _deleted: false, _attachments: {}, _rev: '1-a', _meta: { lwt: 1000 } },
|
|
443
|
+
{ id: 'user2', name: 'Bob', age: 25, _deleted: true, _attachments: {}, _rev: '2-b', _meta: { lwt: 2000 } },
|
|
444
|
+
{ id: 'user3', name: 'Charlie', age: 35, _deleted: false, _attachments: {}, _rev: '1-c', _meta: { lwt: 3000 } }
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
await instance.bulkWrite(docs.map(doc => ({ document: doc })), 'test');
|
|
448
|
+
|
|
449
|
+
const queryResult = await instance.query({
|
|
450
|
+
query: { selector: {}, sort: [], skip: 0 },
|
|
451
|
+
queryPlan: { index: [], startKeys: [], endKeys: [], inclusiveStart: true, inclusiveEnd: true, sortSatisfiedByIndex: false, selectorSatisfiedByIndex: false }
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Storage layer returns ALL documents including deleted
|
|
455
|
+
expect(queryResult.documents).toHaveLength(3);
|
|
456
|
+
expect(queryResult.documents.find(d => d.id === 'user2')).toBeDefined();
|
|
457
|
+
|
|
458
|
+
const countResult = await instance.count({
|
|
459
|
+
query: { selector: {}, sort: [], skip: 0 },
|
|
460
|
+
queryPlan: { index: [], startKeys: [], endKeys: [], inclusiveStart: true, inclusiveEnd: true, sortSatisfiedByIndex: false, selectorSatisfiedByIndex: false }
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Storage layer counts ALL documents including deleted
|
|
464
|
+
expect(countResult.count).toBe(3);
|
|
465
|
+
await instance.remove();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('getChangedDocumentsSince returns documents after checkpoint', async () => {
|
|
469
|
+
const docs: RxDocumentData<TestDocType>[] = [
|
|
470
|
+
{ id: 'user1', name: 'Alice', age: 30, _deleted: false, _attachments: {}, _rev: '1-a', _meta: { lwt: 1000 } },
|
|
471
|
+
{ id: 'user2', name: 'Bob', age: 25, _deleted: false, _attachments: {}, _rev: '1-b', _meta: { lwt: 2000 } },
|
|
472
|
+
{ id: 'user3', name: 'Charlie', age: 35, _deleted: false, _attachments: {}, _rev: '1-c', _meta: { lwt: 3000 } }
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
await instance.bulkWrite(docs.map(doc => ({ document: doc })), 'test');
|
|
476
|
+
|
|
477
|
+
const result = await instance.getChangedDocumentsSince!(10, { id: '', lwt: 1500 });
|
|
478
|
+
|
|
479
|
+
expect(result.documents).toHaveLength(2);
|
|
480
|
+
expect(result.documents[0].id).toBe('user2');
|
|
481
|
+
expect(result.documents[1].id).toBe('user3');
|
|
482
|
+
expect(result.checkpoint).toEqual({ id: 'user3', lwt: 3000 });
|
|
483
|
+
await instance.remove();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('changeStream only emits events for successful operations', async () => {
|
|
487
|
+
const events: any[] = [];
|
|
488
|
+
const subscription = instance.changeStream().subscribe(event => {
|
|
489
|
+
events.push(event);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const doc1: RxDocumentData<TestDocType> = {
|
|
493
|
+
id: 'user1',
|
|
494
|
+
name: 'Alice',
|
|
495
|
+
age: 30,
|
|
496
|
+
_deleted: false,
|
|
497
|
+
_attachments: {},
|
|
498
|
+
_rev: '1-a',
|
|
499
|
+
_meta: { lwt: 1000 }
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
await instance.bulkWrite([{ document: doc1 }], 'test');
|
|
503
|
+
|
|
504
|
+
const conflictDoc: RxDocumentData<TestDocType> = {
|
|
505
|
+
...doc1,
|
|
506
|
+
name: 'Bob',
|
|
507
|
+
_rev: '2-b',
|
|
508
|
+
_meta: { lwt: 2000 }
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const result = await instance.bulkWrite([{ document: conflictDoc }], 'test');
|
|
512
|
+
|
|
513
|
+
expect(result.error.length).toBe(1);
|
|
514
|
+
expect(result.error[0].status).toBe(409);
|
|
515
|
+
|
|
516
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
517
|
+
|
|
518
|
+
expect(events.length).toBe(1);
|
|
519
|
+
expect(events[0].events.length).toBe(1);
|
|
520
|
+
expect(events[0].events[0].documentId).toBe('user1');
|
|
521
|
+
expect(events[0].events[0].operation).toBe('INSERT');
|
|
522
|
+
|
|
523
|
+
subscription.unsubscribe();
|
|
524
|
+
await instance.remove();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('getAttachmentData returns base64 string', async () => {
|
|
528
|
+
// Manually insert attachment data to test OUR retrieval code
|
|
529
|
+
instance.internals.db.run(
|
|
530
|
+
`INSERT INTO "${instance.collectionName}_v0_attachments" (id, data, digest) VALUES (?, ?, ?)`,
|
|
531
|
+
['user1||file.txt', 'aGVsbG8=', 'md5-abc123']
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const data = await instance.getAttachmentData('user1', 'file.txt', 'md5-abc123');
|
|
535
|
+
expect(data).toBe('aGVsbG8=');
|
|
536
|
+
await instance.remove();
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('bulkWrite preserves _attachments metadata in document', async () => {
|
|
540
|
+
const doc: RxDocumentData<TestDocType> = {
|
|
541
|
+
id: 'user2',
|
|
542
|
+
name: 'Bob',
|
|
543
|
+
age: 25,
|
|
544
|
+
_deleted: false,
|
|
545
|
+
_attachments: {
|
|
546
|
+
'photo.jpg': {
|
|
547
|
+
length: 1024,
|
|
548
|
+
type: 'image/jpeg',
|
|
549
|
+
digest: 'md5-xyz789'
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
_rev: '1-b',
|
|
553
|
+
_meta: { lwt: Date.now() }
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
await instance.bulkWrite([{ document: doc }], 'test');
|
|
557
|
+
|
|
558
|
+
const found = await instance.findDocumentsById(['user2'], false);
|
|
559
|
+
expect(found.length).toBe(1);
|
|
560
|
+
expect(found[0]._attachments).toEqual({
|
|
561
|
+
'photo.jpg': {
|
|
562
|
+
length: 1024,
|
|
563
|
+
type: 'image/jpeg',
|
|
564
|
+
digest: 'md5-xyz789'
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
await instance.remove();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('getAttachmentData throws on missing attachment', async () => {
|
|
571
|
+
await expect(
|
|
572
|
+
instance.getAttachmentData('nonexistent', 'file.txt', 'digest')
|
|
573
|
+
).rejects.toThrow('attachment does not exist');
|
|
574
|
+
await instance.remove();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('getAttachmentData throws on digest mismatch', async () => {
|
|
578
|
+
// Manually insert with correct digest
|
|
579
|
+
instance.internals.db.run(
|
|
580
|
+
`INSERT INTO "${instance.collectionName}_v0_attachments" (id, data, digest) VALUES (?, ?, ?)`,
|
|
581
|
+
['user3||file.txt', 'ZGF0YQ==', 'md5-correct']
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
await expect(
|
|
585
|
+
instance.getAttachmentData('user3', 'file.txt', 'md5-wrong')
|
|
586
|
+
).rejects.toThrow('attachment does not exist');
|
|
587
|
+
await instance.remove();
|
|
588
|
+
});
|
|
589
|
+
});
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RxStorage, RxStorageInstanceCreationParams } from 'rxdb';
|
|
2
|
+
import { addRxStorageMultiInstanceSupport } from 'rxdb';
|
|
3
|
+
import type { BunSQLiteStorageSettings, BunSQLiteInternals } from './types';
|
|
4
|
+
import { BunSQLiteStorageInstance } from './instance';
|
|
5
|
+
|
|
6
|
+
export function getRxStorageBunSQLite(
|
|
7
|
+
settings: BunSQLiteStorageSettings = {}
|
|
8
|
+
): RxStorage<BunSQLiteInternals, BunSQLiteStorageSettings> {
|
|
9
|
+
return {
|
|
10
|
+
name: 'bun-sqlite',
|
|
11
|
+
rxdbVersion: '16.21.1',
|
|
12
|
+
|
|
13
|
+
async createStorageInstance<RxDocType>(
|
|
14
|
+
params: RxStorageInstanceCreationParams<RxDocType, BunSQLiteStorageSettings>
|
|
15
|
+
) {
|
|
16
|
+
const instance = new BunSQLiteStorageInstance(params, settings);
|
|
17
|
+
addRxStorageMultiInstanceSupport('bun-sqlite', params, instance);
|
|
18
|
+
return instance;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export interface BunSQLiteStorageSettings {
|
|
4
|
+
/**
|
|
5
|
+
* Database file path. Use ':memory:' for in-memory database.
|
|
6
|
+
* @default ':memory:'
|
|
7
|
+
*/
|
|
8
|
+
filename?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BunSQLiteInternals {
|
|
12
|
+
db: Database;
|
|
13
|
+
primaryPath: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RxTestStorage } from 'rxdb';
|
|
2
|
+
import { getRxStorageBunSQLite } from '../src/storage';
|
|
3
|
+
import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv';
|
|
4
|
+
|
|
5
|
+
export const BUN_SQLITE_TEST_STORAGE: RxTestStorage = {
|
|
6
|
+
name: 'bun-sqlite',
|
|
7
|
+
|
|
8
|
+
async init() {},
|
|
9
|
+
|
|
10
|
+
getStorage() {
|
|
11
|
+
return wrappedValidateAjvStorage({
|
|
12
|
+
storage: getRxStorageBunSQLite({})
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
getPerformanceStorage() {
|
|
17
|
+
return {
|
|
18
|
+
description: 'bun-sqlite-native-jsonb',
|
|
19
|
+
storage: getRxStorageBunSQLite({})
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
hasPersistence: true,
|
|
24
|
+
hasMultiInstance: true,
|
|
25
|
+
hasAttachments: false,
|
|
26
|
+
hasReplication: true
|
|
27
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ESNext",
|
|
7
|
+
"ES2021",
|
|
8
|
+
"ES2021.WeakRef"
|
|
9
|
+
],
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"types": [
|
|
12
|
+
"bun-types"
|
|
13
|
+
],
|
|
14
|
+
"strict": true,
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
"declaration": true,
|
|
19
|
+
"declarationMap": true,
|
|
20
|
+
"outDir": "./dist",
|
|
21
|
+
"rootDir": "./src"
|
|
22
|
+
},
|
|
23
|
+
"include": [
|
|
24
|
+
"src/**/*"
|
|
25
|
+
],
|
|
26
|
+
"exclude": [
|
|
27
|
+
"node_modules",
|
|
28
|
+
"dist",
|
|
29
|
+
".ignoreFolder"
|
|
30
|
+
]
|
|
31
|
+
}
|