@dockstat/sqlite-wrapper 1.2.7 → 1.2.8
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 +99 -99
- package/index.ts +858 -840
- package/package.json +54 -54
- package/query-builder/base.ts +221 -221
- package/query-builder/delete.ts +352 -352
- package/query-builder/index.ts +431 -431
- package/query-builder/insert.ts +249 -249
- package/query-builder/select.ts +358 -358
- package/query-builder/update.ts +278 -278
- package/query-builder/where.ts +307 -307
- package/types.ts +623 -623
package/query-builder/delete.ts
CHANGED
|
@@ -1,352 +1,352 @@
|
|
|
1
|
-
import type { SQLQueryBindings } from "bun:sqlite";
|
|
2
|
-
import type { DeleteResult } from "../types";
|
|
3
|
-
import { SelectQueryBuilder } from "./select";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Mixin class that adds DELETE functionality to the QueryBuilder.
|
|
7
|
-
* Handles safe delete operations with mandatory WHERE conditions.
|
|
8
|
-
*/
|
|
9
|
-
export class DeleteQueryBuilder<
|
|
10
|
-
T extends Record<string, unknown>,
|
|
11
|
-
> extends SelectQueryBuilder<T> {
|
|
12
|
-
/**
|
|
13
|
-
* Delete rows matching the WHERE conditions.
|
|
14
|
-
* Requires at least one WHERE condition to prevent accidental full table deletion.
|
|
15
|
-
*
|
|
16
|
-
* @returns Delete result with changes count
|
|
17
|
-
*/
|
|
18
|
-
delete(): DeleteResult {
|
|
19
|
-
this.requireWhereClause("DELETE");
|
|
20
|
-
|
|
21
|
-
// Handle regex conditions by first fetching matching rows
|
|
22
|
-
if (this.hasRegexConditions()) {
|
|
23
|
-
const result = this.deleteWithRegexConditions();
|
|
24
|
-
this.reset();
|
|
25
|
-
return result;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Build DELETE statement
|
|
29
|
-
const [whereClause, whereParams] = this.buildWhereClause();
|
|
30
|
-
const query = `DELETE FROM ${this.quoteIdentifier(this.getTableName())}${whereClause}`;
|
|
31
|
-
|
|
32
|
-
const result = this.getDb()
|
|
33
|
-
.prepare(query)
|
|
34
|
-
.run(...whereParams);
|
|
35
|
-
|
|
36
|
-
this.reset();
|
|
37
|
-
return {
|
|
38
|
-
changes: result.changes,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Handle DELETE operations when regex conditions are present.
|
|
44
|
-
* This requires client-side filtering and individual row deletion.
|
|
45
|
-
*/
|
|
46
|
-
private deleteWithRegexConditions(): DeleteResult {
|
|
47
|
-
this.reset();
|
|
48
|
-
// First, get all rows matching SQL conditions (without regex)
|
|
49
|
-
const [selectQuery, selectParams] = this.buildWhereClause();
|
|
50
|
-
const candidateRows = this.getDb()
|
|
51
|
-
.prepare(
|
|
52
|
-
`SELECT rowid, * FROM ${this.quoteIdentifier(this.getTableName())}${selectQuery}`,
|
|
53
|
-
)
|
|
54
|
-
.all(...selectParams) as (T & { rowid: number })[];
|
|
55
|
-
|
|
56
|
-
// Apply regex filtering
|
|
57
|
-
const matchingRows = this.applyRegexFiltering(candidateRows);
|
|
58
|
-
|
|
59
|
-
if (matchingRows.length === 0) {
|
|
60
|
-
return { changes: 0 };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Delete each matching row by rowid
|
|
64
|
-
const deleteQuery = `DELETE FROM ${this.quoteIdentifier(this.getTableName())} WHERE rowid = ?`;
|
|
65
|
-
const stmt = this.getDb().prepare(deleteQuery);
|
|
66
|
-
|
|
67
|
-
let totalChanges = 0;
|
|
68
|
-
for (const row of matchingRows) {
|
|
69
|
-
const result = stmt.run(row.rowid as SQLQueryBindings);
|
|
70
|
-
totalChanges += result.changes;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return { changes: totalChanges };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Delete and get the deleted rows back.
|
|
78
|
-
* This is useful when you want to see what was deleted for logging or audit purposes.
|
|
79
|
-
*
|
|
80
|
-
* @returns Array of deleted rows
|
|
81
|
-
*/
|
|
82
|
-
deleteAndGet(): T[] {
|
|
83
|
-
// First, get the rows that will be deleted
|
|
84
|
-
const rowsToDelete = this.all();
|
|
85
|
-
|
|
86
|
-
// Perform the delete
|
|
87
|
-
const deleteResult = this.delete();
|
|
88
|
-
|
|
89
|
-
if (deleteResult.changes === 0) {
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Return the rows that were deleted
|
|
94
|
-
const out = rowsToDelete;
|
|
95
|
-
this.reset();
|
|
96
|
-
return out;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Soft delete - mark rows as deleted instead of physically removing them.
|
|
101
|
-
* This requires a 'deleted_at' or similar column in your table schema.
|
|
102
|
-
*
|
|
103
|
-
* @param deletedColumn - Column name to mark as deleted (defaults to 'deleted_at')
|
|
104
|
-
* @param deletedValue - Value to set for the deleted marker (defaults to current timestamp)
|
|
105
|
-
* @returns Update result with changes count
|
|
106
|
-
*/
|
|
107
|
-
softDelete(
|
|
108
|
-
deletedColumn: keyof T = "deleted_at" as keyof T,
|
|
109
|
-
deletedValue: SQLQueryBindings = Math.floor(Date.now() / 1000),
|
|
110
|
-
): DeleteResult {
|
|
111
|
-
this.requireWhereClause("SOFT DELETE");
|
|
112
|
-
|
|
113
|
-
// Handle regex conditions
|
|
114
|
-
if (this.hasRegexConditions()) {
|
|
115
|
-
const result = this.softDeleteWithRegexConditions(deletedColumn, deletedValue);
|
|
116
|
-
this.reset();
|
|
117
|
-
return result;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Build UPDATE statement to mark as deleted
|
|
121
|
-
const [whereClause, whereParams] = this.buildWhereClause();
|
|
122
|
-
const query = `UPDATE ${this.quoteIdentifier(this.getTableName())} SET ${this.quoteIdentifier(String(deletedColumn))} = ?${whereClause}`;
|
|
123
|
-
|
|
124
|
-
const result = this.getDb()
|
|
125
|
-
.prepare(query)
|
|
126
|
-
.run(deletedValue, ...whereParams);
|
|
127
|
-
|
|
128
|
-
this.reset();
|
|
129
|
-
return {
|
|
130
|
-
changes: result.changes,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Handle soft delete operations when regex conditions are present.
|
|
136
|
-
*/
|
|
137
|
-
private softDeleteWithRegexConditions(
|
|
138
|
-
deletedColumn: keyof T,
|
|
139
|
-
deletedValue: SQLQueryBindings,
|
|
140
|
-
): DeleteResult {
|
|
141
|
-
// First, get all rows matching SQL conditions (without regex)
|
|
142
|
-
const [selectQuery, selectParams] = this.buildWhereClause();
|
|
143
|
-
const candidateRows = this.getDb()
|
|
144
|
-
.prepare(
|
|
145
|
-
`SELECT rowid, * FROM ${this.quoteIdentifier(this.getTableName())}${selectQuery}`,
|
|
146
|
-
)
|
|
147
|
-
.all(...selectParams) as (T & { rowid: number })[];
|
|
148
|
-
|
|
149
|
-
// Apply regex filtering
|
|
150
|
-
const matchingRows = this.applyRegexFiltering(candidateRows);
|
|
151
|
-
|
|
152
|
-
if (matchingRows.length === 0) {
|
|
153
|
-
return { changes: 0 };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Soft delete each matching row by rowid
|
|
157
|
-
const updateQuery = `UPDATE ${this.quoteIdentifier(this.getTableName())} SET ${this.quoteIdentifier(String(deletedColumn))} = ? WHERE rowid = ?`;
|
|
158
|
-
const stmt = this.getDb().prepare(updateQuery);
|
|
159
|
-
|
|
160
|
-
let totalChanges = 0;
|
|
161
|
-
for (const row of matchingRows) {
|
|
162
|
-
const result = stmt.run(deletedValue, row.rowid as SQLQueryBindings);
|
|
163
|
-
totalChanges += result.changes;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return { changes: totalChanges };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Restore soft deleted rows by clearing the deleted marker.
|
|
171
|
-
*
|
|
172
|
-
* @param deletedColumn - Column name that marks rows as deleted
|
|
173
|
-
* @returns Update result with changes count
|
|
174
|
-
*/
|
|
175
|
-
restore(deletedColumn: keyof T = "deleted_at" as keyof T): DeleteResult {
|
|
176
|
-
this.requireWhereClause("RESTORE");
|
|
177
|
-
|
|
178
|
-
// Handle regex conditions
|
|
179
|
-
if (this.hasRegexConditions()) {
|
|
180
|
-
const result = this.restoreWithRegexConditions(deletedColumn);
|
|
181
|
-
this.reset();
|
|
182
|
-
return result;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Build UPDATE statement to clear the deleted marker
|
|
186
|
-
const [whereClause, whereParams] = this.buildWhereClause();
|
|
187
|
-
const query = `UPDATE ${this.quoteIdentifier(this.getTableName())} SET ${this.quoteIdentifier(String(deletedColumn))} = NULL${whereClause}`;
|
|
188
|
-
|
|
189
|
-
const result = this.getDb()
|
|
190
|
-
.prepare(query)
|
|
191
|
-
.run(...whereParams);
|
|
192
|
-
|
|
193
|
-
this.reset();
|
|
194
|
-
return {
|
|
195
|
-
changes: result.changes,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Handle restore operations when regex conditions are present.
|
|
201
|
-
*/
|
|
202
|
-
private restoreWithRegexConditions(deletedColumn: keyof T): DeleteResult {
|
|
203
|
-
// First, get all rows matching SQL conditions (without regex)
|
|
204
|
-
const [selectQuery, selectParams] = this.buildWhereClause();
|
|
205
|
-
const candidateRows = this.getDb()
|
|
206
|
-
.prepare(
|
|
207
|
-
`SELECT rowid, * FROM ${this.quoteIdentifier(this.getTableName())}${selectQuery}`,
|
|
208
|
-
)
|
|
209
|
-
.all(...selectParams) as (T & { rowid: number })[];
|
|
210
|
-
|
|
211
|
-
// Apply regex filtering
|
|
212
|
-
const matchingRows = this.applyRegexFiltering(candidateRows);
|
|
213
|
-
|
|
214
|
-
if (matchingRows.length === 0) {
|
|
215
|
-
return { changes: 0 };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Restore each matching row by rowid
|
|
219
|
-
const updateQuery = `UPDATE ${this.quoteIdentifier(this.getTableName())} SET ${this.quoteIdentifier(String(deletedColumn))} = NULL WHERE rowid = ?`;
|
|
220
|
-
const stmt = this.getDb().prepare(updateQuery);
|
|
221
|
-
|
|
222
|
-
let totalChanges = 0;
|
|
223
|
-
for (const row of matchingRows) {
|
|
224
|
-
const result = stmt.run(row.rowid as SQLQueryBindings);
|
|
225
|
-
totalChanges += result.changes;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return { changes: totalChanges };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Batch delete multiple sets of rows based on different conditions.
|
|
233
|
-
* This is more efficient than individual deletes when removing many different row sets.
|
|
234
|
-
*
|
|
235
|
-
* @param conditions - Array of WHERE condition objects
|
|
236
|
-
* @returns Delete result with total changes count
|
|
237
|
-
*/
|
|
238
|
-
deleteBatch(conditions: Array<Partial<T>>): DeleteResult {
|
|
239
|
-
if (!Array.isArray(conditions) || conditions.length === 0) {
|
|
240
|
-
throw new Error("deleteBatch: conditions must be a non-empty array");
|
|
241
|
-
}
|
|
242
|
-
|
|
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
|
-
}
|
|
263
|
-
|
|
264
|
-
if (whereConditions.length === 0) {
|
|
265
|
-
throw new Error(
|
|
266
|
-
"deleteBatch: each delete must have WHERE conditions",
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Build DELETE statement
|
|
271
|
-
const whereClause = ` WHERE ${whereConditions.join(" AND ")}`;
|
|
272
|
-
const query = `DELETE FROM ${this.quoteIdentifier(this.getTableName())}${whereClause}`;
|
|
273
|
-
|
|
274
|
-
const result = db.prepare(query).run(...whereParams);
|
|
275
|
-
totalChanges += result.changes;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return { changes: totalChanges };
|
|
279
|
-
},
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
const result = transaction(conditions);
|
|
283
|
-
this.reset();
|
|
284
|
-
return result;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Truncate the entire table (delete all rows).
|
|
289
|
-
* This bypasses the WHERE condition requirement since it's explicitly destructive.
|
|
290
|
-
* USE WITH EXTREME CAUTION!
|
|
291
|
-
*
|
|
292
|
-
* @returns Delete result with changes count
|
|
293
|
-
*/
|
|
294
|
-
truncate(): DeleteResult {
|
|
295
|
-
const query = `DELETE FROM ${this.quoteIdentifier(this.getTableName())}`;
|
|
296
|
-
const result = this.getDb().prepare(query).run();
|
|
297
|
-
|
|
298
|
-
this.reset();
|
|
299
|
-
return {
|
|
300
|
-
changes: result.changes,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Delete rows older than a specified timestamp.
|
|
306
|
-
* Convenience method for common cleanup operations.
|
|
307
|
-
*
|
|
308
|
-
* @param timestampColumn - Column name containing the timestamp
|
|
309
|
-
* @param olderThan - Timestamp threshold (rows older than this will be deleted)
|
|
310
|
-
* @returns Delete result with changes count
|
|
311
|
-
*/
|
|
312
|
-
deleteOlderThan(timestampColumn: keyof T, olderThan: number): DeleteResult {
|
|
313
|
-
const changes = this.whereOp(timestampColumn, "<", olderThan).delete();
|
|
314
|
-
this.reset();
|
|
315
|
-
return changes;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Delete duplicate rows based on specified columns, keeping only the first occurrence.
|
|
320
|
-
* This is useful for data cleanup operations.
|
|
321
|
-
*
|
|
322
|
-
* @param columns - Columns to check for duplicates
|
|
323
|
-
* @returns Delete result with changes count
|
|
324
|
-
*/
|
|
325
|
-
deleteDuplicates(columns: Array<keyof T>): DeleteResult {
|
|
326
|
-
if (!Array.isArray(columns) || columns.length === 0) {
|
|
327
|
-
throw new Error("deleteDuplicates: columns must be a non-empty array");
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const columnNames = columns.map((col) => String(col));
|
|
331
|
-
const quotedColumns = columnNames
|
|
332
|
-
.map((col) => this.quoteIdentifier(col))
|
|
333
|
-
.join(", ");
|
|
334
|
-
|
|
335
|
-
// Find duplicate rows, keeping the one with the minimum rowid
|
|
336
|
-
const query = `
|
|
337
|
-
DELETE FROM ${this.quoteIdentifier(this.getTableName())}
|
|
338
|
-
WHERE rowid NOT IN (
|
|
339
|
-
SELECT MIN(rowid)
|
|
340
|
-
FROM ${this.quoteIdentifier(this.getTableName())}
|
|
341
|
-
GROUP BY ${quotedColumns}
|
|
342
|
-
)
|
|
343
|
-
`;
|
|
344
|
-
|
|
345
|
-
const result = this.getDb().prepare(query).run();
|
|
346
|
-
|
|
347
|
-
this.reset();
|
|
348
|
-
return {
|
|
349
|
-
changes: result.changes,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
}
|
|
1
|
+
import type { SQLQueryBindings } from "bun:sqlite";
|
|
2
|
+
import type { DeleteResult } from "../types";
|
|
3
|
+
import { SelectQueryBuilder } from "./select";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mixin class that adds DELETE functionality to the QueryBuilder.
|
|
7
|
+
* Handles safe delete operations with mandatory WHERE conditions.
|
|
8
|
+
*/
|
|
9
|
+
export class DeleteQueryBuilder<
|
|
10
|
+
T extends Record<string, unknown>,
|
|
11
|
+
> extends SelectQueryBuilder<T> {
|
|
12
|
+
/**
|
|
13
|
+
* Delete rows matching the WHERE conditions.
|
|
14
|
+
* Requires at least one WHERE condition to prevent accidental full table deletion.
|
|
15
|
+
*
|
|
16
|
+
* @returns Delete result with changes count
|
|
17
|
+
*/
|
|
18
|
+
delete(): DeleteResult {
|
|
19
|
+
this.requireWhereClause("DELETE");
|
|
20
|
+
|
|
21
|
+
// Handle regex conditions by first fetching matching rows
|
|
22
|
+
if (this.hasRegexConditions()) {
|
|
23
|
+
const result = this.deleteWithRegexConditions();
|
|
24
|
+
this.reset();
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Build DELETE statement
|
|
29
|
+
const [whereClause, whereParams] = this.buildWhereClause();
|
|
30
|
+
const query = `DELETE FROM ${this.quoteIdentifier(this.getTableName())}${whereClause}`;
|
|
31
|
+
|
|
32
|
+
const result = this.getDb()
|
|
33
|
+
.prepare(query)
|
|
34
|
+
.run(...whereParams);
|
|
35
|
+
|
|
36
|
+
this.reset();
|
|
37
|
+
return {
|
|
38
|
+
changes: result.changes,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Handle DELETE operations when regex conditions are present.
|
|
44
|
+
* This requires client-side filtering and individual row deletion.
|
|
45
|
+
*/
|
|
46
|
+
private deleteWithRegexConditions(): DeleteResult {
|
|
47
|
+
this.reset();
|
|
48
|
+
// First, get all rows matching SQL conditions (without regex)
|
|
49
|
+
const [selectQuery, selectParams] = this.buildWhereClause();
|
|
50
|
+
const candidateRows = this.getDb()
|
|
51
|
+
.prepare(
|
|
52
|
+
`SELECT rowid, * FROM ${this.quoteIdentifier(this.getTableName())}${selectQuery}`,
|
|
53
|
+
)
|
|
54
|
+
.all(...selectParams) as (T & { rowid: number })[];
|
|
55
|
+
|
|
56
|
+
// Apply regex filtering
|
|
57
|
+
const matchingRows = this.applyRegexFiltering(candidateRows);
|
|
58
|
+
|
|
59
|
+
if (matchingRows.length === 0) {
|
|
60
|
+
return { changes: 0 };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Delete each matching row by rowid
|
|
64
|
+
const deleteQuery = `DELETE FROM ${this.quoteIdentifier(this.getTableName())} WHERE rowid = ?`;
|
|
65
|
+
const stmt = this.getDb().prepare(deleteQuery);
|
|
66
|
+
|
|
67
|
+
let totalChanges = 0;
|
|
68
|
+
for (const row of matchingRows) {
|
|
69
|
+
const result = stmt.run(row.rowid as SQLQueryBindings);
|
|
70
|
+
totalChanges += result.changes;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { changes: totalChanges };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Delete and get the deleted rows back.
|
|
78
|
+
* This is useful when you want to see what was deleted for logging or audit purposes.
|
|
79
|
+
*
|
|
80
|
+
* @returns Array of deleted rows
|
|
81
|
+
*/
|
|
82
|
+
deleteAndGet(): T[] {
|
|
83
|
+
// First, get the rows that will be deleted
|
|
84
|
+
const rowsToDelete = this.all();
|
|
85
|
+
|
|
86
|
+
// Perform the delete
|
|
87
|
+
const deleteResult = this.delete();
|
|
88
|
+
|
|
89
|
+
if (deleteResult.changes === 0) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Return the rows that were deleted
|
|
94
|
+
const out = rowsToDelete;
|
|
95
|
+
this.reset();
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Soft delete - mark rows as deleted instead of physically removing them.
|
|
101
|
+
* This requires a 'deleted_at' or similar column in your table schema.
|
|
102
|
+
*
|
|
103
|
+
* @param deletedColumn - Column name to mark as deleted (defaults to 'deleted_at')
|
|
104
|
+
* @param deletedValue - Value to set for the deleted marker (defaults to current timestamp)
|
|
105
|
+
* @returns Update result with changes count
|
|
106
|
+
*/
|
|
107
|
+
softDelete(
|
|
108
|
+
deletedColumn: keyof T = "deleted_at" as keyof T,
|
|
109
|
+
deletedValue: SQLQueryBindings = Math.floor(Date.now() / 1000),
|
|
110
|
+
): DeleteResult {
|
|
111
|
+
this.requireWhereClause("SOFT DELETE");
|
|
112
|
+
|
|
113
|
+
// Handle regex conditions
|
|
114
|
+
if (this.hasRegexConditions()) {
|
|
115
|
+
const result = this.softDeleteWithRegexConditions(deletedColumn, deletedValue);
|
|
116
|
+
this.reset();
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Build UPDATE statement to mark as deleted
|
|
121
|
+
const [whereClause, whereParams] = this.buildWhereClause();
|
|
122
|
+
const query = `UPDATE ${this.quoteIdentifier(this.getTableName())} SET ${this.quoteIdentifier(String(deletedColumn))} = ?${whereClause}`;
|
|
123
|
+
|
|
124
|
+
const result = this.getDb()
|
|
125
|
+
.prepare(query)
|
|
126
|
+
.run(deletedValue, ...whereParams);
|
|
127
|
+
|
|
128
|
+
this.reset();
|
|
129
|
+
return {
|
|
130
|
+
changes: result.changes,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Handle soft delete operations when regex conditions are present.
|
|
136
|
+
*/
|
|
137
|
+
private softDeleteWithRegexConditions(
|
|
138
|
+
deletedColumn: keyof T,
|
|
139
|
+
deletedValue: SQLQueryBindings,
|
|
140
|
+
): DeleteResult {
|
|
141
|
+
// First, get all rows matching SQL conditions (without regex)
|
|
142
|
+
const [selectQuery, selectParams] = this.buildWhereClause();
|
|
143
|
+
const candidateRows = this.getDb()
|
|
144
|
+
.prepare(
|
|
145
|
+
`SELECT rowid, * FROM ${this.quoteIdentifier(this.getTableName())}${selectQuery}`,
|
|
146
|
+
)
|
|
147
|
+
.all(...selectParams) as (T & { rowid: number })[];
|
|
148
|
+
|
|
149
|
+
// Apply regex filtering
|
|
150
|
+
const matchingRows = this.applyRegexFiltering(candidateRows);
|
|
151
|
+
|
|
152
|
+
if (matchingRows.length === 0) {
|
|
153
|
+
return { changes: 0 };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Soft delete each matching row by rowid
|
|
157
|
+
const updateQuery = `UPDATE ${this.quoteIdentifier(this.getTableName())} SET ${this.quoteIdentifier(String(deletedColumn))} = ? WHERE rowid = ?`;
|
|
158
|
+
const stmt = this.getDb().prepare(updateQuery);
|
|
159
|
+
|
|
160
|
+
let totalChanges = 0;
|
|
161
|
+
for (const row of matchingRows) {
|
|
162
|
+
const result = stmt.run(deletedValue, row.rowid as SQLQueryBindings);
|
|
163
|
+
totalChanges += result.changes;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { changes: totalChanges };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Restore soft deleted rows by clearing the deleted marker.
|
|
171
|
+
*
|
|
172
|
+
* @param deletedColumn - Column name that marks rows as deleted
|
|
173
|
+
* @returns Update result with changes count
|
|
174
|
+
*/
|
|
175
|
+
restore(deletedColumn: keyof T = "deleted_at" as keyof T): DeleteResult {
|
|
176
|
+
this.requireWhereClause("RESTORE");
|
|
177
|
+
|
|
178
|
+
// Handle regex conditions
|
|
179
|
+
if (this.hasRegexConditions()) {
|
|
180
|
+
const result = this.restoreWithRegexConditions(deletedColumn);
|
|
181
|
+
this.reset();
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Build UPDATE statement to clear the deleted marker
|
|
186
|
+
const [whereClause, whereParams] = this.buildWhereClause();
|
|
187
|
+
const query = `UPDATE ${this.quoteIdentifier(this.getTableName())} SET ${this.quoteIdentifier(String(deletedColumn))} = NULL${whereClause}`;
|
|
188
|
+
|
|
189
|
+
const result = this.getDb()
|
|
190
|
+
.prepare(query)
|
|
191
|
+
.run(...whereParams);
|
|
192
|
+
|
|
193
|
+
this.reset();
|
|
194
|
+
return {
|
|
195
|
+
changes: result.changes,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Handle restore operations when regex conditions are present.
|
|
201
|
+
*/
|
|
202
|
+
private restoreWithRegexConditions(deletedColumn: keyof T): DeleteResult {
|
|
203
|
+
// First, get all rows matching SQL conditions (without regex)
|
|
204
|
+
const [selectQuery, selectParams] = this.buildWhereClause();
|
|
205
|
+
const candidateRows = this.getDb()
|
|
206
|
+
.prepare(
|
|
207
|
+
`SELECT rowid, * FROM ${this.quoteIdentifier(this.getTableName())}${selectQuery}`,
|
|
208
|
+
)
|
|
209
|
+
.all(...selectParams) as (T & { rowid: number })[];
|
|
210
|
+
|
|
211
|
+
// Apply regex filtering
|
|
212
|
+
const matchingRows = this.applyRegexFiltering(candidateRows);
|
|
213
|
+
|
|
214
|
+
if (matchingRows.length === 0) {
|
|
215
|
+
return { changes: 0 };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Restore each matching row by rowid
|
|
219
|
+
const updateQuery = `UPDATE ${this.quoteIdentifier(this.getTableName())} SET ${this.quoteIdentifier(String(deletedColumn))} = NULL WHERE rowid = ?`;
|
|
220
|
+
const stmt = this.getDb().prepare(updateQuery);
|
|
221
|
+
|
|
222
|
+
let totalChanges = 0;
|
|
223
|
+
for (const row of matchingRows) {
|
|
224
|
+
const result = stmt.run(row.rowid as SQLQueryBindings);
|
|
225
|
+
totalChanges += result.changes;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { changes: totalChanges };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Batch delete multiple sets of rows based on different conditions.
|
|
233
|
+
* This is more efficient than individual deletes when removing many different row sets.
|
|
234
|
+
*
|
|
235
|
+
* @param conditions - Array of WHERE condition objects
|
|
236
|
+
* @returns Delete result with total changes count
|
|
237
|
+
*/
|
|
238
|
+
deleteBatch(conditions: Array<Partial<T>>): DeleteResult {
|
|
239
|
+
if (!Array.isArray(conditions) || conditions.length === 0) {
|
|
240
|
+
throw new Error("deleteBatch: conditions must be a non-empty array");
|
|
241
|
+
}
|
|
242
|
+
|
|
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
|
+
}
|
|
263
|
+
|
|
264
|
+
if (whereConditions.length === 0) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"deleteBatch: each delete must have WHERE conditions",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Build DELETE statement
|
|
271
|
+
const whereClause = ` WHERE ${whereConditions.join(" AND ")}`;
|
|
272
|
+
const query = `DELETE FROM ${this.quoteIdentifier(this.getTableName())}${whereClause}`;
|
|
273
|
+
|
|
274
|
+
const result = db.prepare(query).run(...whereParams);
|
|
275
|
+
totalChanges += result.changes;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { changes: totalChanges };
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const result = transaction(conditions);
|
|
283
|
+
this.reset();
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Truncate the entire table (delete all rows).
|
|
289
|
+
* This bypasses the WHERE condition requirement since it's explicitly destructive.
|
|
290
|
+
* USE WITH EXTREME CAUTION!
|
|
291
|
+
*
|
|
292
|
+
* @returns Delete result with changes count
|
|
293
|
+
*/
|
|
294
|
+
truncate(): DeleteResult {
|
|
295
|
+
const query = `DELETE FROM ${this.quoteIdentifier(this.getTableName())}`;
|
|
296
|
+
const result = this.getDb().prepare(query).run();
|
|
297
|
+
|
|
298
|
+
this.reset();
|
|
299
|
+
return {
|
|
300
|
+
changes: result.changes,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Delete rows older than a specified timestamp.
|
|
306
|
+
* Convenience method for common cleanup operations.
|
|
307
|
+
*
|
|
308
|
+
* @param timestampColumn - Column name containing the timestamp
|
|
309
|
+
* @param olderThan - Timestamp threshold (rows older than this will be deleted)
|
|
310
|
+
* @returns Delete result with changes count
|
|
311
|
+
*/
|
|
312
|
+
deleteOlderThan(timestampColumn: keyof T, olderThan: number): DeleteResult {
|
|
313
|
+
const changes = this.whereOp(timestampColumn, "<", olderThan).delete();
|
|
314
|
+
this.reset();
|
|
315
|
+
return changes;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Delete duplicate rows based on specified columns, keeping only the first occurrence.
|
|
320
|
+
* This is useful for data cleanup operations.
|
|
321
|
+
*
|
|
322
|
+
* @param columns - Columns to check for duplicates
|
|
323
|
+
* @returns Delete result with changes count
|
|
324
|
+
*/
|
|
325
|
+
deleteDuplicates(columns: Array<keyof T>): DeleteResult {
|
|
326
|
+
if (!Array.isArray(columns) || columns.length === 0) {
|
|
327
|
+
throw new Error("deleteDuplicates: columns must be a non-empty array");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const columnNames = columns.map((col) => String(col));
|
|
331
|
+
const quotedColumns = columnNames
|
|
332
|
+
.map((col) => this.quoteIdentifier(col))
|
|
333
|
+
.join(", ");
|
|
334
|
+
|
|
335
|
+
// Find duplicate rows, keeping the one with the minimum rowid
|
|
336
|
+
const query = `
|
|
337
|
+
DELETE FROM ${this.quoteIdentifier(this.getTableName())}
|
|
338
|
+
WHERE rowid NOT IN (
|
|
339
|
+
SELECT MIN(rowid)
|
|
340
|
+
FROM ${this.quoteIdentifier(this.getTableName())}
|
|
341
|
+
GROUP BY ${quotedColumns}
|
|
342
|
+
)
|
|
343
|
+
`;
|
|
344
|
+
|
|
345
|
+
const result = this.getDb().prepare(query).run();
|
|
346
|
+
|
|
347
|
+
this.reset();
|
|
348
|
+
return {
|
|
349
|
+
changes: result.changes,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|