muya 2.5.4 → 2.5.6
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/{src/__tests__ → __tests__}/bench.test.tsx +4 -4
- package/{src/__tests__ → __tests__}/compare.test.tsx +8 -6
- package/{src/__tests__ → __tests__}/create.test.tsx +2 -2
- package/{src/utils/__tests__ → __tests__}/is.test.ts +3 -3
- package/{src/__tests__ → __tests__}/scheduler.test.tsx +1 -1
- package/{src/__tests__ → __tests__}/select.test.tsx +5 -5
- package/{src/utils/__tests__ → __tests__}/shallow.test.ts +1 -1
- package/{src/__tests__ → __tests__}/use-value-loadable.test.tsx +3 -3
- package/{src/__tests__ → __tests__}/use-value.test.tsx +8 -8
- package/build.ts +67 -0
- package/dist/cjs/index.js +14 -0
- package/dist/esm/create.js +1 -0
- package/dist/esm/debug/development-tools.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/scheduler.js +1 -0
- package/dist/esm/select.js +1 -0
- package/{types → dist/types}/create-state.d.ts +1 -0
- package/dist/types/create-state.d.ts.map +1 -0
- package/{types → dist/types}/create.d.ts +2 -4
- package/dist/types/create.d.ts.map +1 -0
- package/dist/types/debug/development-tools.d.ts +13 -0
- package/dist/types/debug/development-tools.d.ts.map +1 -0
- package/{types → dist/types}/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/scheduler.d.ts +25 -0
- package/dist/types/scheduler.d.ts.map +1 -0
- package/{types → dist/types}/select.d.ts +1 -0
- package/dist/types/select.d.ts.map +1 -0
- package/{types → dist/types}/types.d.ts +1 -0
- package/dist/types/types.d.ts.map +1 -0
- package/{types → dist/types}/use-value-loadable.d.ts +1 -0
- package/dist/types/use-value-loadable.d.ts.map +1 -0
- package/{types → dist/types}/use-value.d.ts +2 -1
- package/dist/types/use-value.d.ts.map +1 -0
- package/{types → dist/types}/utils/common.d.ts +1 -0
- package/dist/types/utils/common.d.ts.map +1 -0
- package/{types → dist/types}/utils/create-emitter.d.ts +1 -0
- package/dist/types/utils/create-emitter.d.ts.map +1 -0
- package/{types → dist/types}/utils/id.d.ts +1 -0
- package/dist/types/utils/id.d.ts.map +1 -0
- package/{types → dist/types}/utils/is.d.ts +1 -0
- package/dist/types/utils/is.d.ts.map +1 -0
- package/{types → dist/types}/utils/shallow.d.ts +1 -0
- package/dist/types/utils/shallow.d.ts.map +1 -0
- package/package.json +31 -8
- package/src/create.ts +7 -2
- package/src/debug/development-tools.ts +5 -40
- package/src/index.ts +2 -1
- package/src/scheduler.ts +77 -71
- package/src/select.ts +7 -2
- package/src/use-value.ts +1 -1
- package/tsconfig.build.json +12 -0
- package/cjs/index.js +0 -1
- package/esm/__tests__/test-utils.js +0 -1
- package/esm/create.js +0 -1
- package/esm/debug/development-tools.js +0 -1
- package/esm/index.js +0 -1
- package/esm/scheduler.js +0 -1
- package/esm/select.js +0 -1
- package/esm/sqlite/__tests__/create-sqlite.test.js +0 -1
- package/esm/sqlite/__tests__/map-deque.test.js +0 -1
- package/esm/sqlite/__tests__/table.test.js +0 -1
- package/esm/sqlite/__tests__/tokenizer.test.js +0 -1
- package/esm/sqlite/__tests__/where.test.js +0 -1
- package/esm/sqlite/create-sqlite.js +0 -1
- package/esm/sqlite/index.js +0 -1
- package/esm/sqlite/table/backend.js +0 -1
- package/esm/sqlite/table/bun-backend.js +0 -1
- package/esm/sqlite/table/index.js +0 -1
- package/esm/sqlite/table/map-deque.js +0 -1
- package/esm/sqlite/table/table.js +0 -43
- package/esm/sqlite/table/table.types.js +0 -0
- package/esm/sqlite/table/tokenizer.js +0 -1
- package/esm/sqlite/table/where.js +0 -1
- package/esm/sqlite/use-sqlite-count.js +0 -1
- package/esm/sqlite/use-sqlite.js +0 -1
- package/esm/utils/__tests__/is.test.js +0 -1
- package/esm/utils/__tests__/shallow.test.js +0 -1
- package/src/sqlite/__tests__/create-sqlite.test.ts +0 -264
- package/src/sqlite/__tests__/map-deque.test.ts +0 -61
- package/src/sqlite/__tests__/table.test.ts +0 -351
- package/src/sqlite/__tests__/tokenizer.test.ts +0 -43
- package/src/sqlite/__tests__/use-slite-count.test.tsx +0 -96
- package/src/sqlite/__tests__/use-sqlite.more.test.tsx +0 -637
- package/src/sqlite/__tests__/use-sqlite.test.tsx +0 -1008
- package/src/sqlite/__tests__/where.test.ts +0 -234
- package/src/sqlite/create-sqlite.ts +0 -164
- package/src/sqlite/index.ts +0 -4
- package/src/sqlite/table/backend.ts +0 -21
- package/src/sqlite/table/bun-backend.ts +0 -47
- package/src/sqlite/table/index.ts +0 -6
- package/src/sqlite/table/map-deque.ts +0 -29
- package/src/sqlite/table/table.ts +0 -353
- package/src/sqlite/table/table.types.ts +0 -129
- package/src/sqlite/table/tokenizer.ts +0 -35
- package/src/sqlite/table/where.ts +0 -207
- package/src/sqlite/use-sqlite-count.ts +0 -69
- package/src/sqlite/use-sqlite.ts +0 -250
- package/types/__tests__/test-utils.d.ts +0 -25
- package/types/debug/development-tools.d.ts +0 -8
- package/types/scheduler.d.ts +0 -20
- package/types/sqlite/create-sqlite.d.ts +0 -31
- package/types/sqlite/index.d.ts +0 -4
- package/types/sqlite/table/backend.d.ts +0 -20
- package/types/sqlite/table/bun-backend.d.ts +0 -6
- package/types/sqlite/table/index.d.ts +0 -6
- package/types/sqlite/table/map-deque.d.ts +0 -5
- package/types/sqlite/table/table.d.ts +0 -21
- package/types/sqlite/table/table.types.d.ts +0 -91
- package/types/sqlite/table/tokenizer.d.ts +0 -11
- package/types/sqlite/table/where.d.ts +0 -37
- package/types/sqlite/use-sqlite-count.d.ts +0 -17
- package/types/sqlite/use-sqlite.d.ts +0 -39
- /package/{src/__tests__ → __tests__}/test-utils.ts +0 -0
- /package/{esm → dist/esm}/create-state.js +0 -0
- /package/{esm → dist/esm}/types.js +0 -0
- /package/{esm → dist/esm}/use-value-loadable.js +0 -0
- /package/{esm → dist/esm}/use-value.js +0 -0
- /package/{esm → dist/esm}/utils/common.js +0 -0
- /package/{esm → dist/esm}/utils/create-emitter.js +0 -0
- /package/{esm → dist/esm}/utils/id.js +0 -0
- /package/{esm → dist/esm}/utils/is.js +0 -0
- /package/{esm → dist/esm}/utils/shallow.js +0 -0
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
/* eslint-disable prefer-destructuring */
|
|
2
|
-
/* eslint-disable sonarjs/different-types-comparison */
|
|
3
|
-
/* eslint-disable sonarjs/cognitive-complexity */
|
|
4
|
-
/* eslint-disable @typescript-eslint/no-shadow */
|
|
5
|
-
/* eslint-disable no-shadow */
|
|
6
|
-
import type { Backend } from './backend'
|
|
7
|
-
import type {
|
|
8
|
-
Table,
|
|
9
|
-
DbOptions,
|
|
10
|
-
DocType,
|
|
11
|
-
Key,
|
|
12
|
-
SearchOptions,
|
|
13
|
-
MutationResult,
|
|
14
|
-
GroupByResult,
|
|
15
|
-
GroupByOptions,
|
|
16
|
-
DotPath,
|
|
17
|
-
GetFieldType,
|
|
18
|
-
} from './table.types'
|
|
19
|
-
import { unicodeTokenizer, type FtsTokenizerOptions } from './tokenizer'
|
|
20
|
-
import type { Where } from './where'
|
|
21
|
-
import { getWhereQuery } from './where'
|
|
22
|
-
|
|
23
|
-
const DELETE_IN_CHUNK = 500
|
|
24
|
-
export const DEFAULT_PAGE_SIZE = 100
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Convert a dot-separated path to a JSON path
|
|
28
|
-
* @param dot The dot-separated path string
|
|
29
|
-
* @returns The JSON path string
|
|
30
|
-
*/
|
|
31
|
-
export function toJsonPath(dot: string) {
|
|
32
|
-
return '$.' + dot
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Get a nested value from an object using a dot-separated path
|
|
37
|
-
* @param object The object to retrieve the value from
|
|
38
|
-
* @param path The dot-separated path string
|
|
39
|
-
* @returns The value at the specified path, or undefined if not found
|
|
40
|
-
*/
|
|
41
|
-
export function getByPath<T extends object>(object: T, path: string): unknown {
|
|
42
|
-
if (!object || !path) return undefined
|
|
43
|
-
// eslint-disable-next-line unicorn/no-array-reduce
|
|
44
|
-
return path.split('.').reduce<unknown>((accumulator, key) => {
|
|
45
|
-
if (typeof accumulator === 'object' && accumulator !== null && key in (accumulator as Record<string, unknown>)) {
|
|
46
|
-
return (accumulator as Record<string, unknown>)[key]
|
|
47
|
-
}
|
|
48
|
-
return
|
|
49
|
-
}, object)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Create and initialize a table in the database with the specified options
|
|
54
|
-
* @param options The options for creating the table, including table name, indexes, backend, and key
|
|
55
|
-
* @returns A promise that resolves to the created Table instance
|
|
56
|
-
*/
|
|
57
|
-
export async function createTable<Document extends DocType>(options: DbOptions<Document>): Promise<Table<Document>> {
|
|
58
|
-
const { backend, tableName, indexes, key, disablePragmaOptimization } = options
|
|
59
|
-
const hasUserKey = key !== undefined
|
|
60
|
-
|
|
61
|
-
if (!disablePragmaOptimization) {
|
|
62
|
-
await backend.execute(`PRAGMA journal_mode=WAL;`)
|
|
63
|
-
await backend.execute(`PRAGMA synchronous=NORMAL;`)
|
|
64
|
-
await backend.execute(`PRAGMA temp_store=MEMORY;`)
|
|
65
|
-
await backend.execute(`PRAGMA cache_size=-20000;`)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Base JSON table
|
|
69
|
-
if (hasUserKey) {
|
|
70
|
-
await backend.execute(`
|
|
71
|
-
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
72
|
-
key TEXT PRIMARY KEY,
|
|
73
|
-
data TEXT NOT NULL
|
|
74
|
-
);
|
|
75
|
-
`)
|
|
76
|
-
} else {
|
|
77
|
-
await backend.execute(`
|
|
78
|
-
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
79
|
-
data TEXT NOT NULL
|
|
80
|
-
);
|
|
81
|
-
`)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Track FTS fields and map dot paths to valid SQLite column names
|
|
85
|
-
let ftsTokenizer: string | undefined | FtsTokenizerOptions
|
|
86
|
-
const ftsFields: string[] = []
|
|
87
|
-
const ftsFieldMap: Record<string, string> = {} // dot path -> column name
|
|
88
|
-
|
|
89
|
-
for (const index of indexes ?? []) {
|
|
90
|
-
if (typeof index === 'string' && index.startsWith('fts:')) {
|
|
91
|
-
const path = index.slice(4)
|
|
92
|
-
const col = path.replaceAll('.', '_')
|
|
93
|
-
ftsFields.push(path)
|
|
94
|
-
ftsFieldMap[path] = col
|
|
95
|
-
} else if (typeof index === 'object' && index.type === 'fts') {
|
|
96
|
-
const path = index.path
|
|
97
|
-
const col = path.replaceAll('.', '_')
|
|
98
|
-
ftsFields.push(path)
|
|
99
|
-
ftsFieldMap[path] = col
|
|
100
|
-
if (index.tokenizer) {
|
|
101
|
-
if (!ftsTokenizer) {
|
|
102
|
-
ftsTokenizer = index.tokenizer
|
|
103
|
-
} else if (ftsTokenizer !== index.tokenizer) {
|
|
104
|
-
throw new Error(`Conflicting FTS tokenizers: already using "${ftsTokenizer}", got "${index.tokenizer}"`)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
const idx = String(index)
|
|
109
|
-
await backend.execute(
|
|
110
|
-
`CREATE INDEX IF NOT EXISTS idx_${tableName}_${idx.replaceAll(/\W/g, '_')}
|
|
111
|
-
ON ${tableName} (json_extract(data, '${toJsonPath(idx)}'));`,
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Create FTS table + triggers
|
|
117
|
-
if (ftsFields.length > 0) {
|
|
118
|
-
let tokenizerSpec: string
|
|
119
|
-
if (typeof ftsTokenizer === 'object') {
|
|
120
|
-
tokenizerSpec = unicodeTokenizer(ftsTokenizer)
|
|
121
|
-
} else if (ftsTokenizer === undefined) {
|
|
122
|
-
tokenizerSpec = '"unicode61", "remove_diacritics=1"'
|
|
123
|
-
} else {
|
|
124
|
-
tokenizerSpec = ftsTokenizer
|
|
125
|
-
}
|
|
126
|
-
// Use mapped column names for FTS columns
|
|
127
|
-
const ftsColumns = ftsFields.map((f) => ftsFieldMap[f]).join(', ')
|
|
128
|
-
const query = `
|
|
129
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS ${tableName}_fts
|
|
130
|
-
USING fts5(${ftsColumns}, tokenize=${tokenizerSpec});
|
|
131
|
-
`
|
|
132
|
-
|
|
133
|
-
await backend.execute(query)
|
|
134
|
-
|
|
135
|
-
// Insert trigger
|
|
136
|
-
await backend.execute(`
|
|
137
|
-
CREATE TRIGGER IF NOT EXISTS ${tableName}_ai
|
|
138
|
-
AFTER INSERT ON ${tableName}
|
|
139
|
-
BEGIN
|
|
140
|
-
INSERT INTO ${tableName}_fts(rowid, ${ftsColumns})
|
|
141
|
-
VALUES (
|
|
142
|
-
new.rowid,
|
|
143
|
-
${ftsFields.map((f) => `json_extract(new.data, '${toJsonPath(f)}')`).join(', ')}
|
|
144
|
-
);
|
|
145
|
-
END;
|
|
146
|
-
`)
|
|
147
|
-
|
|
148
|
-
// Delete trigger
|
|
149
|
-
await backend.execute(`
|
|
150
|
-
CREATE TRIGGER IF NOT EXISTS ${tableName}_ad
|
|
151
|
-
AFTER DELETE ON ${tableName}
|
|
152
|
-
BEGIN
|
|
153
|
-
DELETE FROM ${tableName}_fts WHERE rowid = old.rowid;
|
|
154
|
-
END;
|
|
155
|
-
`)
|
|
156
|
-
|
|
157
|
-
// Update trigger
|
|
158
|
-
await backend.execute(`
|
|
159
|
-
CREATE TRIGGER IF NOT EXISTS ${tableName}_au
|
|
160
|
-
AFTER UPDATE ON ${tableName}
|
|
161
|
-
BEGIN
|
|
162
|
-
UPDATE ${tableName}_fts
|
|
163
|
-
SET ${ftsFields.map((f) => `${ftsFieldMap[f]}=json_extract(new.data, '${toJsonPath(f)}')`).join(', ')}
|
|
164
|
-
WHERE rowid = old.rowid;
|
|
165
|
-
END;
|
|
166
|
-
`)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Get the value of the configured key from a document
|
|
171
|
-
* @param document The document to extract the key from
|
|
172
|
-
* @returns The value of the key, or undefined if not found or no key is configured
|
|
173
|
-
*/
|
|
174
|
-
function getKeyFromDocument(document: Document): Key | undefined {
|
|
175
|
-
if (!hasUserKey) return undefined
|
|
176
|
-
return getByPath(document, String(key)) as Key | undefined
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const table: Table<Document> = {
|
|
180
|
-
backend,
|
|
181
|
-
|
|
182
|
-
async set(document, backendOverride) {
|
|
183
|
-
const db = backendOverride ?? backend
|
|
184
|
-
const json = JSON.stringify(document)
|
|
185
|
-
|
|
186
|
-
if (hasUserKey) {
|
|
187
|
-
const id = getKeyFromDocument(document)
|
|
188
|
-
if (id === undefined || id === null) {
|
|
189
|
-
throw new Error(`Document is missing the configured key "${String(key)}".`)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const existing = await db.select<Array<{ key: string }>>(`SELECT key FROM ${tableName} WHERE key = ?`, [id])
|
|
193
|
-
|
|
194
|
-
if (existing.length > 0) {
|
|
195
|
-
await db.execute(`UPDATE ${tableName} SET data = ? WHERE key = ?`, [json, id])
|
|
196
|
-
return { key: id, op: 'update', document }
|
|
197
|
-
} else {
|
|
198
|
-
await db.execute(`INSERT INTO ${tableName} (key, data) VALUES (?, ?)`, [id, json])
|
|
199
|
-
return { key: id, op: 'insert', document }
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
await db.execute(`INSERT INTO ${tableName} (data) VALUES (?)`, [json])
|
|
204
|
-
const rows = await db.select<Array<{ id: number }>>(`SELECT last_insert_rowid() AS id`)
|
|
205
|
-
const rowid = rows[0]?.id
|
|
206
|
-
if (typeof rowid !== 'number') throw new Error('Failed to retrieve last_insert_rowid()')
|
|
207
|
-
return { key: rowid, op: 'insert', document }
|
|
208
|
-
},
|
|
209
|
-
|
|
210
|
-
async get<Selected = Document>(
|
|
211
|
-
keyValue: Key,
|
|
212
|
-
selector: (document: Document, meta: { rowId: number; key: Key }) => Selected = (d) => d as unknown as Selected,
|
|
213
|
-
) {
|
|
214
|
-
const whereKey = hasUserKey ? `key = ?` : `rowid = ?`
|
|
215
|
-
const result = await backend.select<Array<{ data: string; rowid: number }>>(
|
|
216
|
-
`SELECT rowid, data FROM ${tableName} WHERE ${whereKey}`,
|
|
217
|
-
[keyValue],
|
|
218
|
-
)
|
|
219
|
-
if (result.length === 0) return
|
|
220
|
-
const { data, rowid } = result[0]
|
|
221
|
-
const document = JSON.parse(data) as Document
|
|
222
|
-
const logicalKey = hasUserKey ? (getKeyFromDocument(document) ?? rowid) : rowid
|
|
223
|
-
return selector(document, { rowId: rowid, key: logicalKey }) as Selected
|
|
224
|
-
},
|
|
225
|
-
|
|
226
|
-
async delete(keyValue: Key, backendOverride?: Backend) {
|
|
227
|
-
const db = backendOverride ?? backend
|
|
228
|
-
const whereKey = hasUserKey ? `key = ?` : `rowid = ?`
|
|
229
|
-
await db.execute(`DELETE FROM ${tableName} WHERE ${whereKey}`, [keyValue])
|
|
230
|
-
const changed = await backend.select<Array<{ c: number }>>(`SELECT changes() AS c`)
|
|
231
|
-
if ((changed[0]?.c ?? 0) > 0) return { key: keyValue, op: 'delete' }
|
|
232
|
-
return
|
|
233
|
-
},
|
|
234
|
-
|
|
235
|
-
async *search<Selected = Document>(options: SearchOptions<Document, Selected> = {}): AsyncIterableIterator<Selected> {
|
|
236
|
-
const {
|
|
237
|
-
sortBy,
|
|
238
|
-
order = 'asc',
|
|
239
|
-
limit,
|
|
240
|
-
offset = 0,
|
|
241
|
-
where,
|
|
242
|
-
select = (document) => document as unknown as Selected,
|
|
243
|
-
pageSize = DEFAULT_PAGE_SIZE,
|
|
244
|
-
} = options
|
|
245
|
-
|
|
246
|
-
const whereSql = getWhereQuery<Document>(where, tableName)
|
|
247
|
-
const baseQuery = `SELECT rowid, data FROM ${tableName} ${whereSql}`
|
|
248
|
-
|
|
249
|
-
let yielded = 0
|
|
250
|
-
let currentOffset = offset
|
|
251
|
-
while (true) {
|
|
252
|
-
let query = baseQuery
|
|
253
|
-
|
|
254
|
-
if (sortBy) {
|
|
255
|
-
query += ` ORDER BY json_extract(data, '${toJsonPath(String(sortBy))}') COLLATE NOCASE ${order.toUpperCase()}`
|
|
256
|
-
} else {
|
|
257
|
-
query += hasUserKey ? ` ORDER BY key COLLATE NOCASE ${order.toUpperCase()}` : ` ORDER BY rowid ${order.toUpperCase()}`
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const batchLimit = limit ? Math.min(pageSize, limit - yielded) : pageSize
|
|
261
|
-
query += ` LIMIT ${batchLimit} OFFSET ${currentOffset}`
|
|
262
|
-
|
|
263
|
-
const results = await backend.select<Array<{ rowid: number; data: string }>>(query)
|
|
264
|
-
if (results.length === 0) break
|
|
265
|
-
|
|
266
|
-
for (const { rowid, data } of results) {
|
|
267
|
-
if (limit && yielded >= limit) return
|
|
268
|
-
const document = JSON.parse(data) as Document
|
|
269
|
-
const logicalKey = hasUserKey ? (getKeyFromDocument(document) ?? rowid) : rowid
|
|
270
|
-
// Pass both rowId and logicalKey
|
|
271
|
-
yield select(document, { rowId: rowid, key: logicalKey }) as Selected
|
|
272
|
-
yielded++
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (results.length < batchLimit || (limit && yielded >= limit)) break
|
|
276
|
-
currentOffset += results.length
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
|
-
|
|
280
|
-
async count(options: { where?: Where<Document> } = {}) {
|
|
281
|
-
const whereSql = getWhereQuery<Document>(options.where, tableName)
|
|
282
|
-
const query = `SELECT COUNT(*) as count FROM ${tableName} ${whereSql}`
|
|
283
|
-
const result = await backend.select<Array<{ count: number }>>(query)
|
|
284
|
-
return result[0]?.count ?? 0
|
|
285
|
-
},
|
|
286
|
-
|
|
287
|
-
async deleteBy(where: Where<Document>) {
|
|
288
|
-
const whereSql = getWhereQuery<Document>(where, tableName)
|
|
289
|
-
const keyCol = hasUserKey ? 'key' : 'rowid'
|
|
290
|
-
const results: MutationResult<Document>[] = []
|
|
291
|
-
|
|
292
|
-
await backend.transaction(async (tx) => {
|
|
293
|
-
const rows = await tx.select<Array<{ k: Key }>>(`SELECT ${keyCol} AS k FROM ${tableName} ${whereSql}`)
|
|
294
|
-
if (rows.length === 0) return
|
|
295
|
-
|
|
296
|
-
const allKeys = rows.map((r) => r.k)
|
|
297
|
-
for (let index = 0; index < allKeys.length; index += DELETE_IN_CHUNK) {
|
|
298
|
-
const chunk = allKeys.slice(index, index + DELETE_IN_CHUNK)
|
|
299
|
-
const placeholders = chunk.map(() => '?').join(',')
|
|
300
|
-
await tx.execute(`DELETE FROM ${tableName} WHERE ${keyCol} IN (${placeholders})`, chunk as unknown[])
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
for (const k of allKeys) results.push({ key: k, op: 'delete', document: undefined })
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
return results
|
|
307
|
-
},
|
|
308
|
-
|
|
309
|
-
async clear() {
|
|
310
|
-
await backend.execute(`DELETE FROM ${tableName}`)
|
|
311
|
-
},
|
|
312
|
-
|
|
313
|
-
async groupBy<Field extends DotPath<Document>>(
|
|
314
|
-
field: Field,
|
|
315
|
-
options: GroupByOptions<Document> = {},
|
|
316
|
-
): Promise<Array<GroupByResult<GetFieldType<Document, Field>>>> {
|
|
317
|
-
const whereSql = getWhereQuery<Document>(options.where, tableName)
|
|
318
|
-
const jsonPath = toJsonPath(String(field))
|
|
319
|
-
const query = `
|
|
320
|
-
SELECT json_extract(data, '${jsonPath}') AS groupKey, COUNT(*) AS count
|
|
321
|
-
FROM ${tableName}
|
|
322
|
-
${whereSql}
|
|
323
|
-
GROUP BY groupKey
|
|
324
|
-
`
|
|
325
|
-
type FieldType = GetFieldType<Document, Field>
|
|
326
|
-
const results = await backend.select<Array<{ groupKey: FieldType; count: number }>>(query)
|
|
327
|
-
return results.map((row) => ({ key: row.groupKey, count: row.count }))
|
|
328
|
-
},
|
|
329
|
-
|
|
330
|
-
async batchSet(documents: Document[]) {
|
|
331
|
-
const mutations: MutationResult<Document>[] = []
|
|
332
|
-
await backend.transaction(async (tx) => {
|
|
333
|
-
for (const document of documents) {
|
|
334
|
-
const m = await table.set(document, tx)
|
|
335
|
-
mutations.push(m)
|
|
336
|
-
}
|
|
337
|
-
})
|
|
338
|
-
return mutations
|
|
339
|
-
},
|
|
340
|
-
async batchDelete(keys: Key[]) {
|
|
341
|
-
const mutations: MutationResult<Document>[] = []
|
|
342
|
-
await backend.transaction(async (tx) => {
|
|
343
|
-
for (const key of keys) {
|
|
344
|
-
const m = await table.delete(key, tx)
|
|
345
|
-
if (m) mutations.push(m)
|
|
346
|
-
}
|
|
347
|
-
})
|
|
348
|
-
return mutations
|
|
349
|
-
},
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return table
|
|
353
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import type { Backend } from './backend'
|
|
2
|
-
import type { FtsTokenizerOptions } from './tokenizer'
|
|
3
|
-
import type { Where } from './where'
|
|
4
|
-
|
|
5
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
-
export type DocType = { [key: string]: any }
|
|
7
|
-
export type KeyTypeAvailable = 'string' | 'number'
|
|
8
|
-
|
|
9
|
-
export interface SqlSeachOptions<Document extends DocType> {
|
|
10
|
-
readonly sortBy?: DotPath<Document>
|
|
11
|
-
readonly order?: 'asc' | 'desc'
|
|
12
|
-
readonly limit?: number
|
|
13
|
-
readonly offset?: number
|
|
14
|
-
readonly where?: Where<Document>
|
|
15
|
-
readonly pageSize?: number
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Expand all nested keys into dot-paths
|
|
19
|
-
export type DotPrefix<T extends string> = T extends '' ? '' : `.${T}`
|
|
20
|
-
|
|
21
|
-
type Previous = [never, 0, 1, 2, 3, 4, 5]
|
|
22
|
-
|
|
23
|
-
type DotPathRaw<T, D extends number = 5> = [D] extends [never]
|
|
24
|
-
? never
|
|
25
|
-
: T extends object
|
|
26
|
-
? {
|
|
27
|
-
[K in Extract<keyof T, string>]: T[K] extends object ? K | `${K}.${DotPathRaw<T[K], Previous[D]>}` : K
|
|
28
|
-
}[Extract<keyof T, string>]
|
|
29
|
-
: never
|
|
30
|
-
|
|
31
|
-
export type DotPath<T> = DotPathRaw<MakeAllFieldAsRequired<T>>
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Extract the value type at a given dot path
|
|
35
|
-
* e.g., GetFieldType<{ user: { name: string } }, 'user.name'> = string
|
|
36
|
-
*/
|
|
37
|
-
export type GetFieldType<T, Path extends string> = Path extends `${infer First}.${infer Rest}`
|
|
38
|
-
? First extends keyof T
|
|
39
|
-
? GetFieldType<T[First], Rest>
|
|
40
|
-
: never
|
|
41
|
-
: Path extends keyof T
|
|
42
|
-
? T[Path]
|
|
43
|
-
: never
|
|
44
|
-
|
|
45
|
-
// Built-in FTS5 tokenizers
|
|
46
|
-
export type FtsTokenizer =
|
|
47
|
-
| 'porter' // English stemming
|
|
48
|
-
| 'simple' // basic ASCII tokenizer
|
|
49
|
-
| 'icu' // requires SQLite compiled with ICU extension
|
|
50
|
-
| 'unicode61' // Unicode-aware tokenizer with diacritic removal and custom token chars
|
|
51
|
-
| FtsTokenizerOptions
|
|
52
|
-
|
|
53
|
-
export interface FtsType<Document extends DocType> {
|
|
54
|
-
readonly type: 'fts'
|
|
55
|
-
readonly path: DotPath<Document>
|
|
56
|
-
readonly tokenizer?: FtsTokenizer
|
|
57
|
-
}
|
|
58
|
-
export type IndexDeclaration<Document extends DocType> =
|
|
59
|
-
| DotPath<Document> // normal JSON path index
|
|
60
|
-
| `fts:${DotPath<Document>}` // full-text index
|
|
61
|
-
| FtsType<Document> // full-text index with options
|
|
62
|
-
|
|
63
|
-
export interface DbOptions<Document extends DocType> {
|
|
64
|
-
readonly tableName: string
|
|
65
|
-
readonly indexes?: Array<IndexDeclaration<Document>>
|
|
66
|
-
readonly backend: Backend
|
|
67
|
-
readonly key?: DotPath<Document>
|
|
68
|
-
readonly disablePragmaOptimization?: boolean
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface SearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
72
|
-
readonly select?: (document: Document, meta: { rowId: number; key: Key }) => Selected
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
interface DbNotGeneric {
|
|
76
|
-
readonly backend: Backend
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export type Key = string | number
|
|
80
|
-
|
|
81
|
-
export type MutationOp = 'insert' | 'update' | 'delete'
|
|
82
|
-
|
|
83
|
-
interface MutationResultBase<T> {
|
|
84
|
-
key: Key
|
|
85
|
-
op: MutationOp
|
|
86
|
-
document?: T
|
|
87
|
-
}
|
|
88
|
-
interface MutationResultDelete<T> extends MutationResultBase<T> {
|
|
89
|
-
key: Key
|
|
90
|
-
op: 'delete'
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface MutationResultUpdateInsert<T> extends MutationResultBase<T> {
|
|
94
|
-
key: Key
|
|
95
|
-
op: 'update' | 'insert'
|
|
96
|
-
document: T
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export type MutationResult<T> = MutationResultDelete<T> | MutationResultUpdateInsert<T>
|
|
100
|
-
|
|
101
|
-
export interface GroupByResult<K> {
|
|
102
|
-
readonly key: K
|
|
103
|
-
readonly count: number
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface GroupByOptions<Document extends DocType> {
|
|
107
|
-
readonly where?: Where<Document>
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface Table<Document extends DocType> extends DbNotGeneric {
|
|
111
|
-
readonly set: (document: Document, backendOverride?: Backend) => Promise<MutationResult<Document>>
|
|
112
|
-
readonly batchSet: (documents: Document[]) => Promise<MutationResult<Document>[]>
|
|
113
|
-
readonly batchDelete: (keys: Key[]) => Promise<MutationResult<Document>[]>
|
|
114
|
-
readonly get: <Selected = Document>(key: Key, selector?: (document: Document) => Selected) => Promise<Selected | undefined>
|
|
115
|
-
|
|
116
|
-
readonly delete: (key: Key, backendOverride?: Backend) => Promise<MutationResult<Document> | undefined>
|
|
117
|
-
readonly search: <Selected = Document>(options?: SearchOptions<Document, Selected>) => AsyncIterableIterator<Selected>
|
|
118
|
-
readonly count: (options?: { where?: Where<Document> }) => Promise<number>
|
|
119
|
-
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult<Document>[]>
|
|
120
|
-
readonly clear: () => Promise<void>
|
|
121
|
-
readonly groupBy: <Field extends DotPath<Document>>(
|
|
122
|
-
field: Field,
|
|
123
|
-
options?: GroupByOptions<Document>,
|
|
124
|
-
) => Promise<Array<GroupByResult<GetFieldType<Document, Field>>>>
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export type MakeAllFieldAsRequired<T> = {
|
|
128
|
-
[K in keyof T]-?: T[K] extends object ? MakeAllFieldAsRequired<T[K]> : T[K]
|
|
129
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export interface FtsTokenizerOptions {
|
|
2
|
-
readonly removeDiacritics?: 0 | 1 | 2
|
|
3
|
-
readonly tokenChars?: string
|
|
4
|
-
readonly separators?: string
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Create a custom FTS5 tokenizer string based on the provided options
|
|
9
|
-
* @param options Options to customize the tokenizer
|
|
10
|
-
* @returns A string representing the FTS5 tokenizer configuration
|
|
11
|
-
*/
|
|
12
|
-
export function unicodeTokenizer(options: FtsTokenizerOptions = {}): string {
|
|
13
|
-
const { removeDiacritics, tokenChars, separators } = options
|
|
14
|
-
const parts: string[] = []
|
|
15
|
-
|
|
16
|
-
if (removeDiacritics !== undefined) {
|
|
17
|
-
parts.push(`"remove_diacritics=${removeDiacritics}"`)
|
|
18
|
-
}
|
|
19
|
-
if (tokenChars && tokenChars.length > 0) {
|
|
20
|
-
parts.push(`"tokenchars='${tokenChars.replaceAll("'", "''")}'"`)
|
|
21
|
-
}
|
|
22
|
-
if (separators && separators.length > 0) {
|
|
23
|
-
parts.push(`"separators='${separators.replaceAll("'", "''")}'"`)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 👉 return correct SQLite syntax
|
|
27
|
-
if (parts.length === 0) {
|
|
28
|
-
return '"unicode61"'
|
|
29
|
-
}
|
|
30
|
-
const hasParts = parts.length > 0
|
|
31
|
-
if (!hasParts) {
|
|
32
|
-
return '"unicode61"'
|
|
33
|
-
}
|
|
34
|
-
return `"unicode61", ${parts.join(', ')}`
|
|
35
|
-
}
|