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.
Files changed (68) hide show
  1. package/.serena/project.yml +84 -0
  2. package/CHANGELOG.md +300 -0
  3. package/LICENSE +21 -0
  4. package/README.md +87 -0
  5. package/ROADMAP.md +532 -0
  6. package/benchmarks/benchmark.ts +145 -0
  7. package/benchmarks/case-insensitive-10runs.ts +156 -0
  8. package/benchmarks/fts5-1m-scale.ts +126 -0
  9. package/benchmarks/fts5-before-after.ts +104 -0
  10. package/benchmarks/indexed-benchmark.ts +141 -0
  11. package/benchmarks/new-operators-benchmark.ts +140 -0
  12. package/benchmarks/query-builder-benchmark.ts +88 -0
  13. package/benchmarks/query-builder-consistency.ts +109 -0
  14. package/benchmarks/raw-better-sqlite3-10m.ts +85 -0
  15. package/benchmarks/raw-better-sqlite3.ts +86 -0
  16. package/benchmarks/raw-bun-sqlite-10m.ts +85 -0
  17. package/benchmarks/raw-bun-sqlite.ts +86 -0
  18. package/benchmarks/regex-10runs-all.ts +216 -0
  19. package/benchmarks/regex-comparison-benchmark.ts +161 -0
  20. package/benchmarks/regex-real-comparison.ts +213 -0
  21. package/benchmarks/run-10x.sh +19 -0
  22. package/benchmarks/smart-regex-benchmark.ts +148 -0
  23. package/benchmarks/sql-vs-mingo-benchmark.ts +210 -0
  24. package/benchmarks/sql-vs-mingo-comparison.ts +175 -0
  25. package/benchmarks/text-vs-jsonb.ts +167 -0
  26. package/benchmarks/wal-benchmark.ts +112 -0
  27. package/docs/architectural-patterns.md +1336 -0
  28. package/docs/id1-testsuite-journey.md +839 -0
  29. package/docs/official-test-suite-setup.md +393 -0
  30. package/nul +0 -0
  31. package/package.json +44 -0
  32. package/src/changestream.test.ts +182 -0
  33. package/src/cleanup.test.ts +110 -0
  34. package/src/collection-isolation.test.ts +74 -0
  35. package/src/connection-pool.test.ts +102 -0
  36. package/src/connection-pool.ts +38 -0
  37. package/src/findDocumentsById.test.ts +122 -0
  38. package/src/index.ts +2 -0
  39. package/src/instance.ts +382 -0
  40. package/src/multi-instance-events.test.ts +204 -0
  41. package/src/query/and-operator.test.ts +39 -0
  42. package/src/query/builder.test.ts +96 -0
  43. package/src/query/builder.ts +154 -0
  44. package/src/query/elemMatch-operator.test.ts +24 -0
  45. package/src/query/exists-operator.test.ts +28 -0
  46. package/src/query/in-operators.test.ts +54 -0
  47. package/src/query/mod-operator.test.ts +22 -0
  48. package/src/query/nested-query.test.ts +198 -0
  49. package/src/query/not-operators.test.ts +49 -0
  50. package/src/query/operators.test.ts +70 -0
  51. package/src/query/operators.ts +185 -0
  52. package/src/query/or-operator.test.ts +68 -0
  53. package/src/query/regex-escaping-regression.test.ts +43 -0
  54. package/src/query/regex-operator.test.ts +44 -0
  55. package/src/query/schema-mapper.ts +27 -0
  56. package/src/query/size-operator.test.ts +22 -0
  57. package/src/query/smart-regex.ts +52 -0
  58. package/src/query/type-operator.test.ts +37 -0
  59. package/src/query-cache.test.ts +286 -0
  60. package/src/rxdb-helpers.test.ts +348 -0
  61. package/src/rxdb-helpers.ts +262 -0
  62. package/src/schema-version-isolation.test.ts +126 -0
  63. package/src/statement-manager.ts +69 -0
  64. package/src/storage.test.ts +589 -0
  65. package/src/storage.ts +21 -0
  66. package/src/types.ts +14 -0
  67. package/test/rxdb-test-suite.ts +27 -0
  68. package/tsconfig.json +31 -0
@@ -0,0 +1,110 @@
1
+ import { describe, test, expect, afterEach } from 'bun:test';
2
+ import { BunSQLiteStorageInstance } from './instance';
3
+ import type { RxStorageInstanceCreationParams } from 'rxdb';
4
+
5
+ describe('cleanup() - TDD Red Phase', () => {
6
+ const instances: BunSQLiteStorageInstance<any>[] = [];
7
+
8
+ afterEach(async () => {
9
+ for (const instance of instances) {
10
+ await instance.close();
11
+ }
12
+ instances.length = 0;
13
+ });
14
+
15
+ test('cleanup() should return false when documents are deleted', async () => {
16
+ const instance = new BunSQLiteStorageInstance({
17
+ databaseName: 'testdb',
18
+ collectionName: 'users',
19
+ databaseInstanceToken: 'test',
20
+ schema: {
21
+ version: 0,
22
+ primaryKey: 'id',
23
+ type: 'object',
24
+ properties: { id: { type: 'string' } }
25
+ },
26
+ options: {},
27
+ devMode: false,
28
+ multiInstance: false,
29
+ internals: {}
30
+ } as RxStorageInstanceCreationParams<any, any>);
31
+ instances.push(instance);
32
+
33
+ const doc = {
34
+ id: 'doc1',
35
+ _deleted: false,
36
+ _attachments: {},
37
+ _meta: { lwt: Date.now() },
38
+ _rev: '1-abc'
39
+ };
40
+
41
+ await instance.bulkWrite([{ document: doc }], 'test');
42
+
43
+ const deletedDoc = { ...doc, _deleted: true, _rev: '2-def', _meta: { lwt: Date.now() } };
44
+ await instance.bulkWrite([{ previous: doc, document: deletedDoc }], 'test');
45
+
46
+ const result = await instance.cleanup(Date.now() + 1000);
47
+
48
+ expect(result).toBe(false);
49
+ });
50
+
51
+ test('cleanup() should return true when no documents to clean', async () => {
52
+ const instance = new BunSQLiteStorageInstance({
53
+ databaseName: 'testdb',
54
+ collectionName: 'users',
55
+ databaseInstanceToken: 'test',
56
+ schema: {
57
+ version: 0,
58
+ primaryKey: 'id',
59
+ type: 'object',
60
+ properties: { id: { type: 'string' } }
61
+ },
62
+ options: {},
63
+ devMode: false,
64
+ multiInstance: false,
65
+ internals: {}
66
+ } as RxStorageInstanceCreationParams<any, any>);
67
+ instances.push(instance);
68
+
69
+ const result = await instance.cleanup(0);
70
+
71
+ expect(result).toBe(true);
72
+ });
73
+
74
+ test('cleanup() should actually remove deleted documents', async () => {
75
+ const instance = new BunSQLiteStorageInstance({
76
+ databaseName: 'testdb',
77
+ collectionName: 'users',
78
+ databaseInstanceToken: 'test',
79
+ schema: {
80
+ version: 0,
81
+ primaryKey: 'id',
82
+ type: 'object',
83
+ properties: { id: { type: 'string' } }
84
+ },
85
+ options: {},
86
+ devMode: false,
87
+ multiInstance: false,
88
+ internals: {}
89
+ } as RxStorageInstanceCreationParams<any, any>);
90
+ instances.push(instance);
91
+
92
+ const doc = {
93
+ id: 'doc1',
94
+ _deleted: false,
95
+ _attachments: {},
96
+ _meta: { lwt: Date.now() },
97
+ _rev: '1-abc'
98
+ };
99
+
100
+ await instance.bulkWrite([{ document: doc }], 'test');
101
+
102
+ const deletedDoc = { ...doc, _deleted: true, _rev: '2-def', _meta: { lwt: Date.now() } };
103
+ await instance.bulkWrite([{ previous: doc, document: deletedDoc }], 'test');
104
+
105
+ await instance.cleanup(Date.now() + 1000);
106
+
107
+ const found = await instance.findDocumentsById(['doc1'], true);
108
+ expect(found.length).toBe(0);
109
+ });
110
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { createRxDatabase, addRxPlugin } from 'rxdb';
3
+ import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode';
4
+ import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv';
5
+ import { getRxStorageBunSQLite } from './index';
6
+
7
+ addRxPlugin(RxDBDevModePlugin);
8
+
9
+ describe('Collection Isolation', () => {
10
+ it('should NOT leak events across different collections in same database', async () => {
11
+ const dbName = 'test-collection-isolation-' + Date.now();
12
+
13
+ const db = await createRxDatabase({
14
+ name: dbName,
15
+ storage: wrappedValidateAjvStorage({ storage: getRxStorageBunSQLite() }),
16
+ multiInstance: true,
17
+ ignoreDuplicate: true
18
+ });
19
+
20
+ await db.addCollections({
21
+ users: {
22
+ schema: {
23
+ version: 0,
24
+ primaryKey: 'id',
25
+ type: 'object',
26
+ properties: {
27
+ id: { type: 'string', maxLength: 100 },
28
+ name: { type: 'string' }
29
+ },
30
+ required: ['id', 'name']
31
+ }
32
+ },
33
+ posts: {
34
+ schema: {
35
+ version: 0,
36
+ primaryKey: 'id',
37
+ type: 'object',
38
+ properties: {
39
+ id: { type: 'string', maxLength: 100 },
40
+ title: { type: 'string' }
41
+ },
42
+ required: ['id', 'title']
43
+ }
44
+ }
45
+ });
46
+
47
+ let usersChangeCount = 0;
48
+ let postsChangeCount = 0;
49
+
50
+ const usersSub = db.users.find().$.subscribe(() => {
51
+ usersChangeCount++;
52
+ });
53
+
54
+ const postsSub = db.posts.find().$.subscribe(() => {
55
+ postsChangeCount++;
56
+ });
57
+
58
+ await new Promise(resolve => setTimeout(resolve, 100));
59
+
60
+ const initialUsersCount = usersChangeCount;
61
+ const initialPostsCount = postsChangeCount;
62
+
63
+ await db.posts.insert({ id: 'post1', title: 'Hello World' });
64
+
65
+ await new Promise(resolve => setTimeout(resolve, 100));
66
+
67
+ expect(postsChangeCount).toBeGreaterThan(initialPostsCount);
68
+ expect(usersChangeCount).toBe(initialUsersCount);
69
+
70
+ usersSub.unsubscribe();
71
+ postsSub.unsubscribe();
72
+ await db.remove();
73
+ });
74
+ });
@@ -0,0 +1,102 @@
1
+ import { describe, test, expect, afterEach } from 'bun:test';
2
+ import { getDatabase, releaseDatabase } from './connection-pool';
3
+
4
+ describe('Connection Pool', () => {
5
+ afterEach(() => {
6
+ // Clean up any leaked connections
7
+ const leaked = ['testdb1', 'testdb2', 'testdb3'];
8
+ leaked.forEach(name => {
9
+ try { releaseDatabase(name); } catch {}
10
+ });
11
+ });
12
+
13
+ test('should share Database object for same databaseName', () => {
14
+ const db1 = getDatabase('testdb1', ':memory:');
15
+ const db2 = getDatabase('testdb1', ':memory:');
16
+
17
+ expect(db1).toBe(db2); // Same object reference
18
+
19
+ releaseDatabase('testdb1');
20
+ releaseDatabase('testdb1');
21
+ });
22
+
23
+ test('should create separate Database objects for different databaseNames', () => {
24
+ const db1 = getDatabase('testdb1', ':memory:');
25
+ const db2 = getDatabase('testdb2', ':memory:');
26
+
27
+ expect(db1).not.toBe(db2); // Different objects
28
+
29
+ releaseDatabase('testdb1');
30
+ releaseDatabase('testdb2');
31
+ });
32
+
33
+ test('should throw error when same databaseName used with different filenames', () => {
34
+ getDatabase('testdb1', ':memory:');
35
+
36
+ expect(() => {
37
+ getDatabase('testdb1', './different.db');
38
+ }).toThrow("Database 'testdb1' already opened with different filename");
39
+
40
+ releaseDatabase('testdb1');
41
+ });
42
+
43
+ test('should increment reference count on multiple getDatabase calls', () => {
44
+ const db1 = getDatabase('testdb1', ':memory:');
45
+ const db2 = getDatabase('testdb1', ':memory:');
46
+ const db3 = getDatabase('testdb1', ':memory:');
47
+
48
+ expect(db1).toBe(db2);
49
+ expect(db2).toBe(db3);
50
+
51
+ // Create a table to verify database stays open
52
+ db1.run('CREATE TABLE test (id INTEGER)');
53
+
54
+ // Release twice - should still be open
55
+ releaseDatabase('testdb1');
56
+ releaseDatabase('testdb1');
57
+
58
+ // Database should still work
59
+ expect(() => db1.run('INSERT INTO test VALUES (1)')).not.toThrow();
60
+
61
+ // Final release - now it closes
62
+ releaseDatabase('testdb1');
63
+ });
64
+
65
+ test('should close database when reference count reaches zero', () => {
66
+ const db = getDatabase('testdb1', ':memory:');
67
+ db.run('CREATE TABLE test (id INTEGER)');
68
+
69
+ releaseDatabase('testdb1');
70
+
71
+ // Database should be closed now
72
+ expect(() => db.run('INSERT INTO test VALUES (1)')).toThrow();
73
+ });
74
+
75
+ test('should handle multiple databases independently', () => {
76
+ const db1 = getDatabase('testdb1', ':memory:');
77
+ const db2 = getDatabase('testdb2', ':memory:');
78
+
79
+ db1.run('CREATE TABLE test1 (id INTEGER)');
80
+ db2.run('CREATE TABLE test2 (id INTEGER)');
81
+
82
+ // Close db1
83
+ releaseDatabase('testdb1');
84
+ expect(() => db1.run('INSERT INTO test1 VALUES (1)')).toThrow();
85
+
86
+ // db2 should still work
87
+ expect(() => db2.run('INSERT INTO test2 VALUES (1)')).not.toThrow();
88
+
89
+ releaseDatabase('testdb2');
90
+ });
91
+
92
+ test('should allow reusing databaseName after full release', () => {
93
+ const db1 = getDatabase('testdb1', ':memory:');
94
+ releaseDatabase('testdb1');
95
+
96
+ // Should be able to get a new database with same name
97
+ const db2 = getDatabase('testdb1', ':memory:');
98
+ expect(db2).not.toBe(db1); // Different instance (old one was closed)
99
+
100
+ releaseDatabase('testdb1');
101
+ });
102
+ });
@@ -0,0 +1,38 @@
1
+ import { Database } from 'bun:sqlite';
2
+
3
+ type DatabaseState = {
4
+ db: Database;
5
+ filename: string;
6
+ openConnections: number;
7
+ };
8
+
9
+ const DATABASE_POOL = new Map<string, DatabaseState>();
10
+
11
+ export function getDatabase(databaseName: string, filename: string): Database {
12
+ let state = DATABASE_POOL.get(databaseName);
13
+ if (!state) {
14
+ state = {
15
+ db: new Database(filename),
16
+ filename,
17
+ openConnections: 1
18
+ };
19
+ DATABASE_POOL.set(databaseName, state);
20
+ } else {
21
+ if (state.filename !== filename) {
22
+ throw new Error(`Database '${databaseName}' already opened with different filename: '${state.filename}' vs '${filename}'`);
23
+ }
24
+ state.openConnections++;
25
+ }
26
+ return state.db;
27
+ }
28
+
29
+ export function releaseDatabase(databaseName: string): void {
30
+ const state = DATABASE_POOL.get(databaseName);
31
+ if (state) {
32
+ state.openConnections--;
33
+ if (state.openConnections === 0) {
34
+ state.db.close();
35
+ DATABASE_POOL.delete(databaseName);
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,122 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { getRxStorageBunSQLite } from './storage';
3
+ import type { RxDocumentData, RxJsonSchema } from 'rxdb';
4
+
5
+ interface TestDoc {
6
+ id: string;
7
+ name: string;
8
+ age: number;
9
+ _deleted: boolean;
10
+ _attachments: {};
11
+ _rev: string;
12
+ _meta: { lwt: number };
13
+ }
14
+
15
+ const testSchema: RxJsonSchema<RxDocumentData<TestDoc>> = {
16
+ version: 0,
17
+ primaryKey: 'id',
18
+ type: 'object',
19
+ properties: {
20
+ id: { type: 'string', maxLength: 100 },
21
+ name: { type: 'string' },
22
+ age: { type: 'number' },
23
+ _deleted: { type: 'boolean' },
24
+ _attachments: { type: 'object' },
25
+ _rev: { type: 'string' },
26
+ _meta: {
27
+ type: 'object',
28
+ properties: {
29
+ lwt: { type: 'number' }
30
+ },
31
+ required: ['lwt']
32
+ }
33
+ },
34
+ required: ['id', '_deleted', '_rev', '_meta']
35
+ };
36
+
37
+ describe('findDocumentsById - withDeleted semantics', () => {
38
+ test('withDeleted=false returns ONLY non-deleted docs', async () => {
39
+ const storage = getRxStorageBunSQLite();
40
+ const instance = await storage.createStorageInstance({
41
+ databaseName: 'test-db',
42
+ collectionName: 'test-collection',
43
+ schema: testSchema,
44
+ options: {},
45
+ multiInstance: false,
46
+ devMode: false,
47
+ databaseInstanceToken: 'test-token'
48
+ });
49
+
50
+ const docs: RxDocumentData<TestDoc>[] = [
51
+ { id: 'doc1', name: 'Active', age: 25, _deleted: false, _attachments: {}, _rev: '1-a', _meta: { lwt: Date.now() } },
52
+ { id: 'doc2', name: 'Deleted', age: 30, _deleted: true, _attachments: {}, _rev: '1-b', _meta: { lwt: Date.now() } },
53
+ { id: 'doc3', name: 'Active2', age: 35, _deleted: false, _attachments: {}, _rev: '1-c', _meta: { lwt: Date.now() } }
54
+ ];
55
+
56
+ await instance.bulkWrite(docs.map(doc => ({ document: doc })), 'test');
57
+
58
+ const result = await instance.findDocumentsById(['doc1', 'doc2', 'doc3'], false);
59
+
60
+ expect(result.length).toBe(2);
61
+ expect(result.map(d => d.id).sort()).toEqual(['doc1', 'doc3']);
62
+ expect(result.every(d => d._deleted === false)).toBe(true);
63
+
64
+ await instance.remove();
65
+ });
66
+
67
+ test('withDeleted=true returns ALL docs (deleted + non-deleted)', async () => {
68
+ const storage = getRxStorageBunSQLite();
69
+ const instance = await storage.createStorageInstance({
70
+ databaseName: 'test-db',
71
+ collectionName: 'test-collection',
72
+ schema: testSchema,
73
+ options: {},
74
+ multiInstance: false,
75
+ devMode: false,
76
+ databaseInstanceToken: 'test-token'
77
+ });
78
+
79
+ const docs: RxDocumentData<TestDoc>[] = [
80
+ { id: 'doc1', name: 'Active', age: 25, _deleted: false, _attachments: {}, _rev: '1-a', _meta: { lwt: Date.now() } },
81
+ { id: 'doc2', name: 'Deleted', age: 30, _deleted: true, _attachments: {}, _rev: '1-b', _meta: { lwt: Date.now() } },
82
+ { id: 'doc3', name: 'Active2', age: 35, _deleted: false, _attachments: {}, _rev: '1-c', _meta: { lwt: Date.now() } }
83
+ ];
84
+
85
+ await instance.bulkWrite(docs.map(doc => ({ document: doc })), 'test');
86
+
87
+ const result = await instance.findDocumentsById(['doc1', 'doc2', 'doc3'], true);
88
+
89
+ expect(result.length).toBe(3);
90
+ expect(result.map(d => d.id).sort()).toEqual(['doc1', 'doc2', 'doc3']);
91
+ expect(result.filter(d => d._deleted).length).toBe(1);
92
+ expect(result.filter(d => !d._deleted).length).toBe(2);
93
+
94
+ await instance.remove();
95
+ });
96
+
97
+ test('withDeleted=false with only deleted docs returns empty array', async () => {
98
+ const storage = getRxStorageBunSQLite();
99
+ const instance = await storage.createStorageInstance({
100
+ databaseName: 'test-db',
101
+ collectionName: 'test-collection',
102
+ schema: testSchema,
103
+ options: {},
104
+ multiInstance: false,
105
+ devMode: false,
106
+ databaseInstanceToken: 'test-token'
107
+ });
108
+
109
+ const docs: RxDocumentData<TestDoc>[] = [
110
+ { id: 'doc1', name: 'Deleted1', age: 25, _deleted: true, _attachments: {}, _rev: '1-a', _meta: { lwt: Date.now() } },
111
+ { id: 'doc2', name: 'Deleted2', age: 30, _deleted: true, _attachments: {}, _rev: '1-b', _meta: { lwt: Date.now() } }
112
+ ];
113
+
114
+ await instance.bulkWrite(docs.map(doc => ({ document: doc })), 'test');
115
+
116
+ const result = await instance.findDocumentsById(['doc1', 'doc2'], false);
117
+
118
+ expect(result.length).toBe(0);
119
+
120
+ await instance.remove();
121
+ });
122
+ });
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { getRxStorageBunSQLite } from './storage';
2
+ export type { BunSQLiteStorageSettings } from './types';