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,185 @@
1
+ export interface SqlFragment {
2
+ sql: string;
3
+ args: (string | number | boolean | null)[];
4
+ }
5
+
6
+ import { smartRegexToLike } from './smart-regex';
7
+
8
+ export function translateEq(field: string, value: unknown): SqlFragment {
9
+ if (value === null) {
10
+ return { sql: `${field} IS NULL`, args: [] };
11
+ }
12
+ return { sql: `${field} = ?`, args: [value as string | number | boolean] };
13
+ }
14
+
15
+ export function translateNe(field: string, value: unknown): SqlFragment {
16
+ if (value === null) {
17
+ return { sql: `${field} IS NOT NULL`, args: [] };
18
+ }
19
+ return { sql: `${field} <> ?`, args: [value as string | number | boolean] };
20
+ }
21
+
22
+ export function translateGt(field: string, value: unknown): SqlFragment {
23
+ return { sql: `${field} > ?`, args: [value as string | number] };
24
+ }
25
+
26
+ export function translateGte(field: string, value: unknown): SqlFragment {
27
+ return { sql: `${field} >= ?`, args: [value as string | number] };
28
+ }
29
+
30
+ export function translateLt(field: string, value: unknown): SqlFragment {
31
+ return { sql: `${field} < ?`, args: [value as string | number] };
32
+ }
33
+
34
+ export function translateLte(field: string, value: unknown): SqlFragment {
35
+ return { sql: `${field} <= ?`, args: [value as string | number] };
36
+ }
37
+
38
+ export function translateIn(field: string, values: unknown[]): SqlFragment {
39
+ if (!Array.isArray(values) || values.length === 0) {
40
+ return { sql: '1=0', args: [] };
41
+ }
42
+
43
+ const hasNull = values.includes(null);
44
+ const nonNullValues = values.filter(v => v !== null) as (string | number | boolean)[];
45
+
46
+ if (nonNullValues.length === 0) {
47
+ return { sql: `${field} IS NULL`, args: [] };
48
+ }
49
+
50
+ const placeholders = nonNullValues.map(() => '?').join(', ');
51
+ const inClause = `${field} IN (${placeholders})`;
52
+
53
+ if (hasNull) {
54
+ return {
55
+ sql: `(${inClause} OR ${field} IS NULL)`,
56
+ args: nonNullValues
57
+ };
58
+ }
59
+
60
+ return { sql: inClause, args: nonNullValues };
61
+ }
62
+
63
+ export function translateNin(field: string, values: unknown[]): SqlFragment {
64
+ if (!Array.isArray(values) || values.length === 0) {
65
+ return { sql: '1=1', args: [] };
66
+ }
67
+
68
+ const hasNull = values.includes(null);
69
+ const nonNullValues = values.filter(v => v !== null) as (string | number | boolean)[];
70
+
71
+ if (nonNullValues.length === 0) {
72
+ return { sql: `${field} IS NOT NULL`, args: [] };
73
+ }
74
+
75
+ const placeholders = nonNullValues.map(() => '?').join(', ');
76
+ const ninClause = `${field} NOT IN (${placeholders})`;
77
+
78
+ if (hasNull) {
79
+ return {
80
+ sql: `(${ninClause} AND ${field} IS NOT NULL)`,
81
+ args: nonNullValues
82
+ };
83
+ }
84
+
85
+ return { sql: ninClause, args: nonNullValues };
86
+ }
87
+
88
+ export function translateExists(field: string, exists: boolean): SqlFragment {
89
+ return {
90
+ sql: exists ? `${field} IS NOT NULL` : `${field} IS NULL`,
91
+ args: []
92
+ };
93
+ }
94
+
95
+ export function translateRegex(field: string, pattern: string, options?: string): SqlFragment | null {
96
+ const smartResult = smartRegexToLike(field, pattern, options);
97
+ if (smartResult) return smartResult;
98
+
99
+ return null;
100
+ }
101
+
102
+ export function translateElemMatch(field: string, criteria: any): SqlFragment | null {
103
+ return null;
104
+ }
105
+
106
+ export function translateNot(field: string, criteria: any): SqlFragment {
107
+ const inner = processOperatorValue(field, criteria);
108
+ return {
109
+ sql: `NOT(${inner.sql})`,
110
+ args: inner.args
111
+ };
112
+ }
113
+
114
+ export function translateNor(conditions: any[]): SqlFragment {
115
+ if (conditions.length === 0) {
116
+ return { sql: '1=1', args: [] };
117
+ }
118
+
119
+ const fragments = conditions.map(condition => {
120
+ const [[field, value]] = Object.entries(condition);
121
+ return processOperatorValue(field, value);
122
+ });
123
+
124
+ const sql = fragments.map(f => `(${f.sql})`).join(' OR ');
125
+ const args = fragments.flatMap(f => f.args);
126
+
127
+ return {
128
+ sql: `NOT(${sql})`,
129
+ args
130
+ };
131
+ }
132
+
133
+ function processOperatorValue(field: string, value: any): SqlFragment {
134
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
135
+ const [[op, opValue]] = Object.entries(value);
136
+
137
+ switch (op) {
138
+ case '$eq': return translateEq(field, opValue);
139
+ case '$ne': return translateNe(field, opValue);
140
+ case '$gt': return translateGt(field, opValue);
141
+ case '$gte': return translateGte(field, opValue);
142
+ case '$lt': return translateLt(field, opValue);
143
+ case '$lte': return translateLte(field, opValue);
144
+ case '$in': return translateIn(field, opValue as unknown[]);
145
+ case '$nin': return translateNin(field, opValue as unknown[]);
146
+ default: return translateEq(field, opValue);
147
+ }
148
+ }
149
+
150
+ return translateEq(field, value);
151
+ }
152
+
153
+ export function translateType(field: string, type: string): SqlFragment | null {
154
+ switch (type) {
155
+ case 'number':
156
+ return {
157
+ sql: `(typeof(${field}) = 'integer' OR typeof(${field}) = 'real')`,
158
+ args: []
159
+ };
160
+ case 'string':
161
+ return { sql: `typeof(${field}) = 'text'`, args: [] };
162
+ case 'null':
163
+ return { sql: `typeof(${field}) = 'null'`, args: [] };
164
+ case 'boolean':
165
+ case 'array':
166
+ case 'object':
167
+ case 'date':
168
+ default:
169
+ return null;
170
+ }
171
+ }
172
+
173
+ export function translateSize(field: string, size: number): SqlFragment {
174
+ return {
175
+ sql: `json_array_length(${field}) = ?`,
176
+ args: [size]
177
+ };
178
+ }
179
+
180
+ export function translateMod(field: string, [divisor, remainder]: [number, number]): SqlFragment {
181
+ return {
182
+ sql: `${field} % ? = ?`,
183
+ args: [divisor, remainder]
184
+ };
185
+ }
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { buildWhereClause } from './builder';
3
+ import type { RxJsonSchema } from 'rxdb';
4
+
5
+ const testSchema: RxJsonSchema<any> = {
6
+ version: 0,
7
+ primaryKey: 'id',
8
+ type: 'object',
9
+ properties: {
10
+ id: { type: 'string' },
11
+ age: { type: 'number' },
12
+ status: { type: 'string' },
13
+ deleted: { type: 'boolean' }
14
+ }
15
+ };
16
+
17
+ describe('$or operator', () => {
18
+ it('generates simple OR clause', () => {
19
+ const result = buildWhereClause({
20
+ $or: [
21
+ { age: 25 },
22
+ { age: 30 }
23
+ ]
24
+ }, testSchema);
25
+
26
+ expect(result.sql).toBe('json_extract(data, \'$.age\') = ? OR json_extract(data, \'$.age\') = ?');
27
+ expect(result.args).toEqual([25, 30]);
28
+ });
29
+
30
+ it('handles OR with nested AND', () => {
31
+ const result = buildWhereClause({
32
+ $or: [
33
+ { age: { $gt: 50 } },
34
+ { $and: [{ age: { $eq: 50 } }, { status: 'active' }] }
35
+ ]
36
+ }, testSchema);
37
+
38
+ expect(result.sql).toContain('json_extract(data, \'$.age\') > ?');
39
+ expect(result.sql).toContain('OR');
40
+ expect(result.sql).toContain('(json_extract(data, \'$.age\') = ? AND json_extract(data, \'$.status\') = ?)');
41
+ expect(result.args).toEqual([50, 50, 'active']);
42
+ });
43
+
44
+ it('handles complex nested OR with parentheses', () => {
45
+ const result = buildWhereClause({
46
+ $or: [
47
+ { age: { $gt: 50 } },
48
+ { $and: [{ age: { $eq: 50 } }, { status: 'active' }] },
49
+ {
50
+ $and: [
51
+ { deleted: false },
52
+ {
53
+ $or: [
54
+ { age: { $lte: 30 } },
55
+ { age: 30 },
56
+ { age: 35 }
57
+ ]
58
+ }
59
+ ]
60
+ }
61
+ ]
62
+ }, testSchema);
63
+
64
+ expect(result.sql).toContain('OR');
65
+ expect(result.sql).toContain('(');
66
+ expect(result.sql).toContain(')');
67
+ });
68
+ });
@@ -0,0 +1,43 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { Database } from "bun:sqlite";
3
+ import { translateRegex } from "./operators";
4
+
5
+ describe("Regex operator - % and _ escaping regression test", () => {
6
+ test("case-insensitive exact match with % character should escape properly", () => {
7
+ const db = new Database(":memory:");
8
+ db.run(`CREATE TABLE test (id TEXT PRIMARY KEY, data TEXT)`);
9
+
10
+ db.run(`INSERT INTO test (id, data) VALUES ('1', '{"name": "100%"}')`);
11
+ db.run(`INSERT INTO test (id, data) VALUES ('2', '{"name": "100x"}')`);
12
+ db.run(`INSERT INTO test (id, data) VALUES ('3', '{"name": "50%"}')`);
13
+
14
+ const result = translateRegex("json_extract(data, '$.name')", '^100%$', 'i');
15
+
16
+ expect(result).not.toBeNull();
17
+ const rows = db.query(`SELECT * FROM test WHERE ${result!.sql}`).all(...result!.args);
18
+
19
+ expect(rows.length).toBe(1);
20
+ expect(JSON.parse((rows[0] as any).data).name).toBe("100%");
21
+
22
+ db.close();
23
+ });
24
+
25
+ test("case-insensitive exact match with _ character should escape properly", () => {
26
+ const db = new Database(":memory:");
27
+ db.run(`CREATE TABLE test (id TEXT PRIMARY KEY, data TEXT)`);
28
+
29
+ db.run(`INSERT INTO test (id, data) VALUES ('1', '{"name": "test_name"}')`);
30
+ db.run(`INSERT INTO test (id, data) VALUES ('2', '{"name": "testxname"}')`);
31
+ db.run(`INSERT INTO test (id, data) VALUES ('3', '{"name": "test-name"}')`);
32
+
33
+ const result = translateRegex("json_extract(data, '$.name')", '^test_name$', 'i');
34
+
35
+ expect(result).not.toBeNull();
36
+ const rows = db.query(`SELECT * FROM test WHERE ${result!.sql}`).all(...result!.args);
37
+
38
+ expect(rows.length).toBe(1);
39
+ expect(JSON.parse((rows[0] as any).data).name).toBe("test_name");
40
+
41
+ db.close();
42
+ });
43
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { translateRegex } from './operators';
3
+
4
+ describe('$regex Operator', () => {
5
+ it('translates simple pattern to LIKE', () => {
6
+ const result = translateRegex('name', '^John');
7
+ expect(result?.sql).toBe("name LIKE ? ESCAPE '\\'");
8
+ expect(result?.args).toEqual(['John%']);
9
+ });
10
+
11
+ it('translates end anchor to LIKE', () => {
12
+ const result = translateRegex('email', '@gmail\\.com$');
13
+ expect(result?.sql).toBe("email LIKE ? ESCAPE '\\'");
14
+ expect(result?.args).toEqual(['%@gmail.com']);
15
+ });
16
+
17
+ it('translates contains pattern to LIKE', () => {
18
+ const result = translateRegex('description', 'urgent');
19
+ expect(result?.sql).toBe("description LIKE ? ESCAPE '\\'");
20
+ expect(result?.args).toEqual(['%urgent%']);
21
+ });
22
+
23
+ it('escapes LIKE special chars', () => {
24
+ const result = translateRegex('username', 'user_name');
25
+ expect(result?.sql).toBe("username LIKE ? ESCAPE '\\'");
26
+ expect(result?.args).toEqual(['%user\\_name%']);
27
+ });
28
+
29
+ it('handles case-insensitive with COLLATE NOCASE', () => {
30
+ const result = translateRegex('name', 'john', 'i');
31
+ expect(result?.sql).toBe("name LIKE ? COLLATE NOCASE ESCAPE '\\'");
32
+ expect(result?.args).toEqual(['%john%']);
33
+ });
34
+
35
+ it('returns null for complex regex patterns', () => {
36
+ const result = translateRegex('phone', '\\d{3}-\\d{4}');
37
+ expect(result).toBeNull();
38
+ });
39
+
40
+ it('returns null for character classes', () => {
41
+ const result = translateRegex('code', '[A-Z]{3}');
42
+ expect(result).toBeNull();
43
+ });
44
+ });
@@ -0,0 +1,27 @@
1
+ import type { RxJsonSchema, RxDocumentData } from 'rxdb';
2
+
3
+ export interface ColumnInfo {
4
+ column?: string;
5
+ jsonPath?: string;
6
+ type: 'string' | 'number' | 'boolean' | 'unknown';
7
+ }
8
+
9
+ export function getColumnInfo<RxDocType>(path: string, schema: RxJsonSchema<RxDocumentData<RxDocType>>): ColumnInfo {
10
+ if (path === '_deleted') {
11
+ return { column: 'deleted', type: 'boolean' };
12
+ }
13
+
14
+ if (path === '_meta.lwt') {
15
+ return { column: 'mtime_ms', type: 'number' };
16
+ }
17
+
18
+ if (path === '_rev') {
19
+ return { column: 'rev', type: 'string' };
20
+ }
21
+
22
+ if (path === schema.primaryKey) {
23
+ return { column: 'id', type: 'string' };
24
+ }
25
+
26
+ return { jsonPath: `$.${path}`, type: 'unknown' };
27
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { translateSize } from './operators';
3
+
4
+ describe('$size Operator', () => {
5
+ it('translates array size check to json_array_length', () => {
6
+ const result = translateSize('tags', 3);
7
+ expect(result?.sql).toBe('json_array_length(tags) = ?');
8
+ expect(result?.args).toEqual([3]);
9
+ });
10
+
11
+ it('handles size 0', () => {
12
+ const result = translateSize('items', 0);
13
+ expect(result?.sql).toBe('json_array_length(items) = ?');
14
+ expect(result?.args).toEqual([0]);
15
+ });
16
+
17
+ it('handles large array sizes', () => {
18
+ const result = translateSize('data', 100);
19
+ expect(result?.sql).toBe('json_array_length(data) = ?');
20
+ expect(result?.args).toEqual([100]);
21
+ });
22
+ });
@@ -0,0 +1,52 @@
1
+ export interface SqlFragment {
2
+ sql: string;
3
+ args: (string | number | boolean)[];
4
+ }
5
+
6
+ export function smartRegexToLike(field: string, pattern: string, options?: string): SqlFragment | null {
7
+ const caseInsensitive = options?.includes('i');
8
+
9
+ const startsWithAnchor = pattern.startsWith('^');
10
+ const endsWithAnchor = pattern.endsWith('$');
11
+
12
+ let cleanPattern = pattern.replace(/^\^/, '').replace(/\$$/, '');
13
+
14
+ if (startsWithAnchor && endsWithAnchor && !/[*+?()[\]{}|]/.test(cleanPattern)) {
15
+ const exact = cleanPattern.replace(/\\\./g, '.');
16
+ if (caseInsensitive) {
17
+ const escaped = exact.replace(/%/g, '\\%').replace(/_/g, '\\_');
18
+ return { sql: `${field} LIKE ? COLLATE NOCASE ESCAPE '\\'`, args: [escaped] };
19
+ }
20
+ return { sql: `${field} = ?`, args: [exact] };
21
+ }
22
+
23
+ if (startsWithAnchor) {
24
+ const prefix = cleanPattern.replace(/\\\./g, '.');
25
+ if (!/[*+?()[\]{}|]/.test(prefix)) {
26
+ const escaped = prefix.replace(/%/g, '\\%').replace(/_/g, '\\_');
27
+ return caseInsensitive
28
+ ? { sql: `${field} LIKE ? COLLATE NOCASE ESCAPE '\\'`, args: [escaped + '%'] }
29
+ : { sql: `${field} LIKE ? ESCAPE '\\'`, args: [escaped + '%'] };
30
+ }
31
+ }
32
+
33
+ if (endsWithAnchor) {
34
+ const suffix = cleanPattern.replace(/\\\./g, '.');
35
+ if (!/[*+?()[\]{}|]/.test(suffix)) {
36
+ const escaped = suffix.replace(/%/g, '\\%').replace(/_/g, '\\_');
37
+ return caseInsensitive
38
+ ? { sql: `${field} LIKE ? COLLATE NOCASE ESCAPE '\\'`, args: ['%' + escaped] }
39
+ : { sql: `${field} LIKE ? ESCAPE '\\'`, args: ['%' + escaped] };
40
+ }
41
+ }
42
+
43
+ cleanPattern = cleanPattern.replace(/\\\./g, '.');
44
+ if (!/[*+?()[\]{}|^$]/.test(cleanPattern)) {
45
+ const escaped = cleanPattern.replace(/%/g, '\\%').replace(/_/g, '\\_');
46
+ return caseInsensitive
47
+ ? { sql: `${field} LIKE ? COLLATE NOCASE ESCAPE '\\'`, args: ['%' + escaped + '%'] }
48
+ : { sql: `${field} LIKE ? ESCAPE '\\'`, args: ['%' + escaped + '%'] };
49
+ }
50
+
51
+ return null;
52
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import { translateType } from './operators';
3
+
4
+ describe('$type Operator', () => {
5
+ it('translates number type to typeof check', () => {
6
+ const result = translateType('age', 'number');
7
+ expect(result?.sql).toBe("(typeof(age) = 'integer' OR typeof(age) = 'real')");
8
+ expect(result?.args).toEqual([]);
9
+ });
10
+
11
+ it('translates string type to typeof check', () => {
12
+ const result = translateType('name', 'string');
13
+ expect(result?.sql).toBe("typeof(name) = 'text'");
14
+ expect(result?.args).toEqual([]);
15
+ });
16
+
17
+ it('translates null type to typeof check', () => {
18
+ const result = translateType('deleted', 'null');
19
+ expect(result?.sql).toBe("typeof(deleted) = 'null'");
20
+ expect(result?.args).toEqual([]);
21
+ });
22
+
23
+ it('returns null for boolean type (Mingo fallback)', () => {
24
+ const result = translateType('active', 'boolean');
25
+ expect(result).toBeNull();
26
+ });
27
+
28
+ it('returns null for array type (Mingo fallback)', () => {
29
+ const result = translateType('tags', 'array');
30
+ expect(result).toBeNull();
31
+ });
32
+
33
+ it('returns null for object type (Mingo fallback)', () => {
34
+ const result = translateType('metadata', 'object');
35
+ expect(result).toBeNull();
36
+ });
37
+ });