@dockstat/sqlite-wrapper 1.2.8 → 1.3.0
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/LICENSE +373 -373
- package/README.md +553 -99
- package/index.ts +1120 -858
- package/package.json +60 -54
- package/query-builder/base.ts +183 -221
- package/query-builder/delete.ts +441 -352
- package/query-builder/index.ts +409 -431
- package/query-builder/insert.ts +280 -249
- package/query-builder/select.ts +333 -358
- package/query-builder/update.ts +308 -278
- package/query-builder/where.ts +272 -307
- package/types.ts +608 -623
- package/utils/index.ts +44 -0
- package/utils/logger.ts +184 -0
- package/utils/sql.ts +241 -0
- package/utils/transformer.ts +256 -0
package/query-builder/delete.ts
CHANGED
|
@@ -1,352 +1,441 @@
|
|
|
1
|
-
import type { SQLQueryBindings } from "bun:sqlite"
|
|
2
|
-
import type { DeleteResult } from "../types"
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* Handles
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
this.
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
*
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
FROM ${
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
1
|
+
import type { SQLQueryBindings } from "bun:sqlite"
|
|
2
|
+
import type { DeleteResult } from "../types"
|
|
3
|
+
import { createLogger, quoteIdentifier } from "../utils"
|
|
4
|
+
import { SelectQueryBuilder } from "./select"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DeleteQueryBuilder - Handles DELETE operations with safety checks
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Safe deletes (WHERE required to prevent accidental full-table deletes)
|
|
11
|
+
* - Soft delete (mark as deleted instead of removing)
|
|
12
|
+
* - Restore soft deleted rows
|
|
13
|
+
* - Delete and get (returns deleted rows)
|
|
14
|
+
* - Batch deletes with transaction support
|
|
15
|
+
* - Delete older than timestamp
|
|
16
|
+
* - Delete duplicates
|
|
17
|
+
* - Truncate (explicit full-table delete)
|
|
18
|
+
*/
|
|
19
|
+
export class DeleteQueryBuilder<T extends Record<string, unknown>> extends SelectQueryBuilder<T> {
|
|
20
|
+
private deleteLog = createLogger("delete")
|
|
21
|
+
|
|
22
|
+
// ===== Public Delete Methods =====
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Delete rows matching the WHERE conditions
|
|
26
|
+
*
|
|
27
|
+
* Requires at least one WHERE condition to prevent accidental full-table deletes.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* table.where({ id: 1 }).delete()
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* table.where({ active: false, deleted_at: null }).delete()
|
|
34
|
+
*/
|
|
35
|
+
delete(): DeleteResult {
|
|
36
|
+
this.requireWhereClause("DELETE")
|
|
37
|
+
|
|
38
|
+
// Handle regex conditions by fetching matching rows first
|
|
39
|
+
if (this.hasRegexConditions()) {
|
|
40
|
+
return this.deleteWithRegexConditions()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Build DELETE statement
|
|
44
|
+
const [whereClause, whereParams] = this.buildWhereClause()
|
|
45
|
+
const query = `DELETE FROM ${quoteIdentifier(this.getTableName())}${whereClause}`
|
|
46
|
+
|
|
47
|
+
this.deleteLog.query("DELETE", query, whereParams)
|
|
48
|
+
|
|
49
|
+
const result = this.getDb()
|
|
50
|
+
.prepare(query)
|
|
51
|
+
.run(...whereParams)
|
|
52
|
+
|
|
53
|
+
this.deleteLog.result("DELETE", result.changes)
|
|
54
|
+
this.reset()
|
|
55
|
+
|
|
56
|
+
return { changes: result.changes }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Handle DELETE when regex conditions are present
|
|
61
|
+
*
|
|
62
|
+
* Since regex conditions are applied client-side, we need to:
|
|
63
|
+
* 1. Fetch all rows matching SQL conditions
|
|
64
|
+
* 2. Filter with regex client-side
|
|
65
|
+
* 3. Delete each matching row by rowid
|
|
66
|
+
*/
|
|
67
|
+
private deleteWithRegexConditions(): DeleteResult {
|
|
68
|
+
// Get rows matching SQL conditions (without regex)
|
|
69
|
+
// Use alias for rowid to avoid collision with INTEGER PRIMARY KEY columns
|
|
70
|
+
const [whereClause, whereParams] = this.buildWhereClause()
|
|
71
|
+
const selectQuery = `SELECT rowid as _rowid_, * FROM ${quoteIdentifier(this.getTableName())}${whereClause}`
|
|
72
|
+
|
|
73
|
+
const candidateRows = this.getDb()
|
|
74
|
+
.prepare(selectQuery)
|
|
75
|
+
.all(...whereParams) as (T & { _rowid_: number })[]
|
|
76
|
+
|
|
77
|
+
// Apply regex filtering
|
|
78
|
+
const matchingRows = this.applyRegexFiltering(candidateRows)
|
|
79
|
+
|
|
80
|
+
if (matchingRows.length === 0) {
|
|
81
|
+
this.reset()
|
|
82
|
+
return { changes: 0 }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Delete each matching row by rowid
|
|
86
|
+
const deleteQuery = `DELETE FROM ${quoteIdentifier(this.getTableName())} WHERE rowid = ?`
|
|
87
|
+
const stmt = this.getDb().prepare(deleteQuery)
|
|
88
|
+
|
|
89
|
+
this.deleteLog.query("DELETE (regex)", deleteQuery)
|
|
90
|
+
|
|
91
|
+
let totalChanges = 0
|
|
92
|
+
|
|
93
|
+
for (const row of matchingRows) {
|
|
94
|
+
const result = stmt.run(row._rowid_ as SQLQueryBindings)
|
|
95
|
+
totalChanges += result.changes
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.deleteLog.result("DELETE (regex)", totalChanges)
|
|
99
|
+
this.reset()
|
|
100
|
+
|
|
101
|
+
return { changes: totalChanges }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Delete rows and return the deleted rows
|
|
106
|
+
*
|
|
107
|
+
* Useful for logging or audit purposes.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* const deletedUsers = table.where({ active: false }).deleteAndGet()
|
|
111
|
+
* console.log(`Deleted ${deletedUsers.length} inactive users`)
|
|
112
|
+
*/
|
|
113
|
+
deleteAndGet(): T[] {
|
|
114
|
+
// Preserve WHERE state before all() resets it
|
|
115
|
+
const savedWhereConditions = [...this.state.whereConditions]
|
|
116
|
+
const savedWhereParams = [...this.state.whereParams]
|
|
117
|
+
const savedRegexConditions = [...this.state.regexConditions]
|
|
118
|
+
|
|
119
|
+
// Get rows before deletion
|
|
120
|
+
const rowsToDelete = this.all()
|
|
121
|
+
|
|
122
|
+
// Restore WHERE state for the delete
|
|
123
|
+
this.state.whereConditions = savedWhereConditions
|
|
124
|
+
this.state.whereParams = savedWhereParams
|
|
125
|
+
this.state.regexConditions = savedRegexConditions
|
|
126
|
+
|
|
127
|
+
// Perform the delete
|
|
128
|
+
const deleteResult = this.delete()
|
|
129
|
+
|
|
130
|
+
if (deleteResult.changes === 0) {
|
|
131
|
+
return []
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return rowsToDelete
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Soft delete - mark rows as deleted instead of removing them
|
|
139
|
+
*
|
|
140
|
+
* Sets a timestamp column to mark rows as deleted.
|
|
141
|
+
* Useful for data recovery and audit trails.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* // Using default column name 'deleted_at'
|
|
145
|
+
* table.where({ id: 1 }).softDelete()
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* // Using custom column name and value
|
|
149
|
+
* table.where({ id: 1 }).softDelete("removed_at", Date.now())
|
|
150
|
+
*/
|
|
151
|
+
softDelete(
|
|
152
|
+
deletedColumn: keyof T = "deleted_at" as keyof T,
|
|
153
|
+
deletedValue: SQLQueryBindings = Math.floor(Date.now() / 1000)
|
|
154
|
+
): DeleteResult {
|
|
155
|
+
this.requireWhereClause("SOFT DELETE")
|
|
156
|
+
|
|
157
|
+
// Handle regex conditions
|
|
158
|
+
if (this.hasRegexConditions()) {
|
|
159
|
+
return this.softDeleteWithRegexConditions(deletedColumn, deletedValue)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Build UPDATE statement to mark as deleted
|
|
163
|
+
const [whereClause, whereParams] = this.buildWhereClause()
|
|
164
|
+
const query = `UPDATE ${quoteIdentifier(this.getTableName())} SET ${quoteIdentifier(String(deletedColumn))} = ?${whereClause}`
|
|
165
|
+
const params = [deletedValue, ...whereParams] as SQLQueryBindings[]
|
|
166
|
+
|
|
167
|
+
this.deleteLog.query("SOFT DELETE", query, params)
|
|
168
|
+
|
|
169
|
+
const result = this.getDb()
|
|
170
|
+
.prepare(query)
|
|
171
|
+
.run(...params)
|
|
172
|
+
|
|
173
|
+
this.deleteLog.result("SOFT DELETE", result.changes)
|
|
174
|
+
this.reset()
|
|
175
|
+
|
|
176
|
+
return { changes: result.changes }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Handle soft delete when regex conditions are present
|
|
181
|
+
*/
|
|
182
|
+
private softDeleteWithRegexConditions(
|
|
183
|
+
deletedColumn: keyof T,
|
|
184
|
+
deletedValue: SQLQueryBindings
|
|
185
|
+
): DeleteResult {
|
|
186
|
+
// Get rows matching SQL conditions (without regex)
|
|
187
|
+
// Use alias for rowid to avoid collision with INTEGER PRIMARY KEY columns
|
|
188
|
+
const [whereClause, whereParams] = this.buildWhereClause()
|
|
189
|
+
const selectQuery = `SELECT rowid as _rowid_, * FROM ${quoteIdentifier(this.getTableName())}${whereClause}`
|
|
190
|
+
|
|
191
|
+
const candidateRows = this.getDb()
|
|
192
|
+
.prepare(selectQuery)
|
|
193
|
+
.all(...whereParams) as (T & { _rowid_: number })[]
|
|
194
|
+
|
|
195
|
+
// Apply regex filtering
|
|
196
|
+
const matchingRows = this.applyRegexFiltering(candidateRows)
|
|
197
|
+
|
|
198
|
+
if (matchingRows.length === 0) {
|
|
199
|
+
this.reset()
|
|
200
|
+
return { changes: 0 }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Soft delete each matching row by rowid
|
|
204
|
+
const updateQuery = `UPDATE ${quoteIdentifier(this.getTableName())} SET ${quoteIdentifier(String(deletedColumn))} = ? WHERE rowid = ?`
|
|
205
|
+
const stmt = this.getDb().prepare(updateQuery)
|
|
206
|
+
|
|
207
|
+
this.deleteLog.query("SOFT DELETE (regex)", updateQuery)
|
|
208
|
+
|
|
209
|
+
let totalChanges = 0
|
|
210
|
+
|
|
211
|
+
for (const row of matchingRows) {
|
|
212
|
+
const result = stmt.run(deletedValue, row._rowid_ as SQLQueryBindings)
|
|
213
|
+
totalChanges += result.changes
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.deleteLog.result("SOFT DELETE (regex)", totalChanges)
|
|
217
|
+
this.reset()
|
|
218
|
+
|
|
219
|
+
return { changes: totalChanges }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Restore soft deleted rows by clearing the deleted marker
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* // Restore using default column name 'deleted_at'
|
|
227
|
+
* table.where({ id: 1 }).restore()
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* // Restore using custom column name
|
|
231
|
+
* table.where({ email: "user@example.com" }).restore("removed_at")
|
|
232
|
+
*/
|
|
233
|
+
restore(deletedColumn: keyof T = "deleted_at" as keyof T): DeleteResult {
|
|
234
|
+
this.requireWhereClause("RESTORE")
|
|
235
|
+
|
|
236
|
+
// Handle regex conditions
|
|
237
|
+
if (this.hasRegexConditions()) {
|
|
238
|
+
return this.restoreWithRegexConditions(deletedColumn)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Build UPDATE statement to clear the deleted marker
|
|
242
|
+
const [whereClause, whereParams] = this.buildWhereClause()
|
|
243
|
+
const query = `UPDATE ${quoteIdentifier(this.getTableName())} SET ${quoteIdentifier(String(deletedColumn))} = NULL${whereClause}`
|
|
244
|
+
|
|
245
|
+
this.deleteLog.query("RESTORE", query, whereParams)
|
|
246
|
+
|
|
247
|
+
const result = this.getDb()
|
|
248
|
+
.prepare(query)
|
|
249
|
+
.run(...whereParams)
|
|
250
|
+
|
|
251
|
+
this.deleteLog.result("RESTORE", result.changes)
|
|
252
|
+
this.reset()
|
|
253
|
+
|
|
254
|
+
return { changes: result.changes }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Handle restore when regex conditions are present
|
|
259
|
+
*/
|
|
260
|
+
private restoreWithRegexConditions(deletedColumn: keyof T): DeleteResult {
|
|
261
|
+
// Get rows matching SQL conditions (without regex)
|
|
262
|
+
// Use alias for rowid to avoid collision with INTEGER PRIMARY KEY columns
|
|
263
|
+
const [whereClause, whereParams] = this.buildWhereClause()
|
|
264
|
+
const selectQuery = `SELECT rowid as _rowid_, * FROM ${quoteIdentifier(this.getTableName())}${whereClause}`
|
|
265
|
+
|
|
266
|
+
const candidateRows = this.getDb()
|
|
267
|
+
.prepare(selectQuery)
|
|
268
|
+
.all(...whereParams) as (T & { _rowid_: number })[]
|
|
269
|
+
|
|
270
|
+
// Apply regex filtering
|
|
271
|
+
const matchingRows = this.applyRegexFiltering(candidateRows)
|
|
272
|
+
|
|
273
|
+
if (matchingRows.length === 0) {
|
|
274
|
+
this.reset()
|
|
275
|
+
return { changes: 0 }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Restore each matching row by rowid
|
|
279
|
+
const updateQuery = `UPDATE ${quoteIdentifier(this.getTableName())} SET ${quoteIdentifier(String(deletedColumn))} = NULL WHERE rowid = ?`
|
|
280
|
+
const stmt = this.getDb().prepare(updateQuery)
|
|
281
|
+
|
|
282
|
+
this.deleteLog.query("RESTORE (regex)", updateQuery)
|
|
283
|
+
|
|
284
|
+
let totalChanges = 0
|
|
285
|
+
|
|
286
|
+
for (const row of matchingRows) {
|
|
287
|
+
const result = stmt.run(row._rowid_ as SQLQueryBindings)
|
|
288
|
+
totalChanges += result.changes
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this.deleteLog.result("RESTORE (regex)", totalChanges)
|
|
292
|
+
this.reset()
|
|
293
|
+
|
|
294
|
+
return { changes: totalChanges }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Batch delete multiple sets of rows based on different conditions
|
|
299
|
+
*
|
|
300
|
+
* Each condition set deletes rows matching those conditions.
|
|
301
|
+
* All deletes are wrapped in a transaction for atomicity.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* table.deleteBatch([
|
|
305
|
+
* { id: 1 },
|
|
306
|
+
* { id: 2 },
|
|
307
|
+
* { email: "deleted@example.com" },
|
|
308
|
+
* ])
|
|
309
|
+
*/
|
|
310
|
+
deleteBatch(conditions: Array<Partial<T>>): DeleteResult {
|
|
311
|
+
if (!Array.isArray(conditions) || conditions.length === 0) {
|
|
312
|
+
throw new Error("deleteBatch: conditions must be a non-empty array")
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const db = this.getDb()
|
|
316
|
+
|
|
317
|
+
const transaction = db.transaction((conditionsToProcess: Array<Partial<T>>) => {
|
|
318
|
+
let totalChanges = 0
|
|
319
|
+
|
|
320
|
+
for (const whereData of conditionsToProcess) {
|
|
321
|
+
// Build WHERE conditions for this delete
|
|
322
|
+
const whereConditions: string[] = []
|
|
323
|
+
const whereParams: SQLQueryBindings[] = []
|
|
324
|
+
|
|
325
|
+
for (const [column, value] of Object.entries(whereData)) {
|
|
326
|
+
if (value === null || value === undefined) {
|
|
327
|
+
whereConditions.push(`${quoteIdentifier(column)} IS NULL`)
|
|
328
|
+
} else {
|
|
329
|
+
whereConditions.push(`${quoteIdentifier(column)} = ?`)
|
|
330
|
+
whereParams.push(value as SQLQueryBindings)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (whereConditions.length === 0) {
|
|
335
|
+
throw new Error("deleteBatch: each delete must have WHERE conditions")
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Build DELETE statement
|
|
339
|
+
const whereClause = ` WHERE ${whereConditions.join(" AND ")}`
|
|
340
|
+
const query = `DELETE FROM ${quoteIdentifier(this.getTableName())}${whereClause}`
|
|
341
|
+
|
|
342
|
+
const result = db.prepare(query).run(...whereParams)
|
|
343
|
+
totalChanges += result.changes
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { changes: totalChanges }
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
this.deleteLog.query("DELETE BATCH", `${conditions.length} deletes`)
|
|
350
|
+
|
|
351
|
+
const result = transaction(conditions)
|
|
352
|
+
|
|
353
|
+
this.deleteLog.result("DELETE BATCH", result.changes)
|
|
354
|
+
this.reset()
|
|
355
|
+
|
|
356
|
+
return result
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Truncate the entire table (delete all rows)
|
|
361
|
+
*
|
|
362
|
+
* This bypasses the WHERE condition requirement since it's explicitly destructive.
|
|
363
|
+
* USE WITH EXTREME CAUTION!
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* // Delete all rows from the table
|
|
367
|
+
* table.truncate()
|
|
368
|
+
*/
|
|
369
|
+
truncate(): DeleteResult {
|
|
370
|
+
const query = `DELETE FROM ${quoteIdentifier(this.getTableName())}`
|
|
371
|
+
|
|
372
|
+
this.deleteLog.query("TRUNCATE", query)
|
|
373
|
+
|
|
374
|
+
const result = this.getDb().prepare(query).run()
|
|
375
|
+
|
|
376
|
+
this.deleteLog.result("TRUNCATE", result.changes)
|
|
377
|
+
this.reset()
|
|
378
|
+
|
|
379
|
+
return { changes: result.changes }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Delete rows older than a specified timestamp
|
|
384
|
+
*
|
|
385
|
+
* Convenience method for common cleanup operations.
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* // Delete rows older than 24 hours
|
|
389
|
+
* table.deleteOlderThan("created_at", Date.now() - 86400000)
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* // Delete rows older than 30 days (Unix timestamp)
|
|
393
|
+
* const thirtyDaysAgo = Math.floor(Date.now() / 1000) - (30 * 24 * 60 * 60)
|
|
394
|
+
* table.deleteOlderThan("timestamp", thirtyDaysAgo)
|
|
395
|
+
*/
|
|
396
|
+
deleteOlderThan(timestampColumn: keyof T, olderThan: number): DeleteResult {
|
|
397
|
+
return this.whereOp(timestampColumn, "<", olderThan).delete()
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Delete duplicate rows based on specified columns
|
|
402
|
+
*
|
|
403
|
+
* Keeps the row with the minimum rowid for each unique combination.
|
|
404
|
+
* Useful for data cleanup operations.
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* // Delete duplicate emails, keeping the first occurrence
|
|
408
|
+
* table.deleteDuplicates(["email"])
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* // Delete rows with duplicate name+email combinations
|
|
412
|
+
* table.deleteDuplicates(["name", "email"])
|
|
413
|
+
*/
|
|
414
|
+
deleteDuplicates(columns: Array<keyof T>): DeleteResult {
|
|
415
|
+
if (!Array.isArray(columns) || columns.length === 0) {
|
|
416
|
+
throw new Error("deleteDuplicates: columns must be a non-empty array")
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const quotedColumns = columns.map((col) => quoteIdentifier(String(col))).join(", ")
|
|
420
|
+
const tableName = quoteIdentifier(this.getTableName())
|
|
421
|
+
|
|
422
|
+
// Delete all rows except the one with minimum rowid for each unique combination
|
|
423
|
+
const query = `
|
|
424
|
+
DELETE FROM ${tableName}
|
|
425
|
+
WHERE rowid NOT IN (
|
|
426
|
+
SELECT MIN(rowid)
|
|
427
|
+
FROM ${tableName}
|
|
428
|
+
GROUP BY ${quotedColumns}
|
|
429
|
+
)
|
|
430
|
+
`.trim()
|
|
431
|
+
|
|
432
|
+
this.deleteLog.query("DELETE DUPLICATES", query)
|
|
433
|
+
|
|
434
|
+
const result = this.getDb().prepare(query).run()
|
|
435
|
+
|
|
436
|
+
this.deleteLog.result("DELETE DUPLICATES", result.changes)
|
|
437
|
+
this.reset()
|
|
438
|
+
|
|
439
|
+
return { changes: result.changes }
|
|
440
|
+
}
|
|
441
|
+
}
|