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