@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.
@@ -1,352 +1,441 @@
1
- import type { SQLQueryBindings } from "bun:sqlite";
2
- import type { DeleteResult } from "../types";
3
- import { SelectQueryBuilder } from "./select";
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
- * Mixin class that adds DELETE functionality to the QueryBuilder.
7
- * Handles safe delete operations with mandatory WHERE conditions.
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
- T extends Record<string, unknown>,
11
- > extends SelectQueryBuilder<T> {
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
- * Requires at least one WHERE condition to prevent accidental full table deletion.
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
- * @returns Delete result with changes count
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 first fetching matching rows
38
+ // Handle regex conditions by fetching matching rows first
22
39
  if (this.hasRegexConditions()) {
23
- const result = this.deleteWithRegexConditions();
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 ${this.quoteIdentifier(this.getTableName())}${whereClause}`;
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
- this.reset();
37
- return {
38
- changes: result.changes,
39
- };
56
+ return { changes: result.changes }
40
57
  }
41
58
 
42
59
  /**
43
- * Handle DELETE operations when regex conditions are present.
44
- * This requires client-side filtering and individual row deletion.
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
- this.reset();
48
- // First, get all rows matching SQL conditions (without regex)
49
- const [selectQuery, selectParams] = this.buildWhereClause();
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
- `SELECT rowid, * FROM ${this.quoteIdentifier(this.getTableName())}${selectQuery}`,
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
- return { changes: 0 };
81
+ this.reset()
82
+ return { changes: 0 }
61
83
  }
62
84
 
63
85
  // Delete each matching row by rowid
64
- const deleteQuery = `DELETE FROM ${this.quoteIdentifier(this.getTableName())} WHERE rowid = ?`;
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.rowid as SQLQueryBindings);
70
- totalChanges += result.changes;
94
+ const result = stmt.run(row._rowid_ as SQLQueryBindings)
95
+ totalChanges += result.changes
71
96
  }
72
97
 
73
- return { changes: totalChanges };
98
+ this.deleteLog.result("DELETE (regex)", totalChanges)
99
+ this.reset()
100
+
101
+ return { changes: totalChanges }
74
102
  }
75
103
 
76
104
  /**
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.
105
+ * Delete rows and return the deleted rows
106
+ *
107
+ * Useful for logging or audit purposes.
79
108
  *
80
- * @returns Array of deleted rows
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
- // First, get the rows that will be deleted
84
- const rowsToDelete = this.all();
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
- // Return the rows that were deleted
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 physically removing them.
101
- * This requires a 'deleted_at' or similar column in your table schema.
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
- * @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
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
- const result = this.softDeleteWithRegexConditions(deletedColumn, deletedValue);
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 ${this.quoteIdentifier(this.getTableName())} SET ${this.quoteIdentifier(String(deletedColumn))} = ?${whereClause}`;
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(deletedValue, ...whereParams);
171
+ .run(...params)
127
172
 
128
- this.reset();
129
- return {
130
- changes: result.changes,
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 operations when regex conditions are present.
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
- // First, get all rows matching SQL conditions (without regex)
142
- const [selectQuery, selectParams] = this.buildWhereClause();
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
- `SELECT rowid, * FROM ${this.quoteIdentifier(this.getTableName())}${selectQuery}`,
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
- return { changes: 0 };
199
+ this.reset()
200
+ return { changes: 0 }
154
201
  }
155
202
 
156
203
  // 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);
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.rowid as SQLQueryBindings);
163
- totalChanges += result.changes;
212
+ const result = stmt.run(deletedValue, row._rowid_ as SQLQueryBindings)
213
+ totalChanges += result.changes
164
214
  }
165
215
 
166
- return { changes: totalChanges };
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
- * @param deletedColumn - Column name that marks rows as deleted
173
- * @returns Update result with changes count
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
- const result = this.restoreWithRegexConditions(deletedColumn);
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 ${this.quoteIdentifier(this.getTableName())} SET ${this.quoteIdentifier(String(deletedColumn))} = NULL${whereClause}`;
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
- this.reset();
194
- return {
195
- changes: result.changes,
196
- };
254
+ return { changes: result.changes }
197
255
  }
198
256
 
199
257
  /**
200
- * Handle restore operations when regex conditions are present.
258
+ * Handle restore when regex conditions are present
201
259
  */
202
260
  private restoreWithRegexConditions(deletedColumn: keyof T): DeleteResult {
203
- // First, get all rows matching SQL conditions (without regex)
204
- const [selectQuery, selectParams] = this.buildWhereClause();
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
- `SELECT rowid, * FROM ${this.quoteIdentifier(this.getTableName())}${selectQuery}`,
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
- return { changes: 0 };
274
+ this.reset()
275
+ return { changes: 0 }
216
276
  }
217
277
 
218
278
  // 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);
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.rowid as SQLQueryBindings);
225
- totalChanges += result.changes;
287
+ const result = stmt.run(row._rowid_ as SQLQueryBindings)
288
+ totalChanges += result.changes
226
289
  }
227
290
 
228
- return { changes: totalChanges };
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
- * This is more efficient than individual deletes when removing many different row sets.
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
- * @param conditions - Array of WHERE condition objects
236
- * @returns Delete result with total changes count
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
- if (whereConditions.length === 0) {
265
- throw new Error(
266
- "deleteBatch: each delete must have WHERE conditions",
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
- // Build DELETE statement
271
- const whereClause = ` WHERE ${whereConditions.join(" AND ")}`;
272
- const query = `DELETE FROM ${this.quoteIdentifier(this.getTableName())}${whereClause}`;
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
- const result = db.prepare(query).run(...whereParams);
275
- totalChanges += result.changes;
334
+ if (whereConditions.length === 0) {
335
+ throw new Error("deleteBatch: each delete must have WHERE conditions")
276
336
  }
277
337
 
278
- return { changes: totalChanges };
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
- const result = transaction(conditions);
283
- this.reset();
284
- return result;
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
- * @returns Delete result with changes count
365
+ * @example
366
+ * // Delete all rows from the table
367
+ * table.truncate()
293
368
  */
294
369
  truncate(): DeleteResult {
295
- const query = `DELETE FROM ${this.quoteIdentifier(this.getTableName())}`;
296
- const result = this.getDb().prepare(query).run();
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
- this.reset();
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
- * @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
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
- const changes = this.whereOp(timestampColumn, "<", olderThan).delete();
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, keeping only the first occurrence.
320
- * This is useful for data cleanup operations.
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
- * @param columns - Columns to check for duplicates
323
- * @returns Delete result with changes count
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 columnNames = columns.map((col) => String(col));
331
- const quotedColumns = columnNames
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
- // Find duplicate rows, keeping the one with the minimum rowid
422
+ // Delete all rows except the one with minimum rowid for each unique combination
336
423
  const query = `
337
- DELETE FROM ${this.quoteIdentifier(this.getTableName())}
424
+ DELETE FROM ${tableName}
338
425
  WHERE rowid NOT IN (
339
426
  SELECT MIN(rowid)
340
- FROM ${this.quoteIdentifier(this.getTableName())}
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
- const result = this.getDb().prepare(query).run();
436
+ this.deleteLog.result("DELETE DUPLICATES", result.changes)
437
+ this.reset()
346
438
 
347
- this.reset();
348
- return {
349
- changes: result.changes,
350
- };
439
+ return { changes: result.changes }
351
440
  }
352
441
  }