@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,249 +1,280 @@
1
- import type { SQLQueryBindings } from "bun:sqlite";
2
- import type { InsertResult, InsertOptions } from "../types";
3
- import { WhereQueryBuilder } from "./where";
1
+ import type { SQLQueryBindings } from "bun:sqlite"
2
+ import type { InsertOptions, InsertResult } from "../types"
3
+ import {
4
+ buildPlaceholders,
5
+ createLogger,
6
+ quoteIdentifier,
7
+ quoteIdentifiers,
8
+ type RowData,
9
+ } from "../utils"
10
+ import { WhereQueryBuilder } from "./where"
4
11
 
5
12
  /**
6
- * Mixin class that adds INSERT functionality to the QueryBuilder.
7
- * Handles single and bulk insert operations with conflict resolution.
13
+ * InsertQueryBuilder - Handles INSERT operations with conflict resolution
14
+ *
15
+ * Features:
16
+ * - Single and bulk inserts
17
+ * - Conflict resolution (OR IGNORE, OR REPLACE, etc.)
18
+ * - Insert and get (returns inserted row)
19
+ * - Batch inserts with transaction support
20
+ * - Automatic JSON/Boolean serialization
8
21
  */
9
- export class InsertQueryBuilder<
10
- T extends Record<string, unknown>,
11
- > extends WhereQueryBuilder<T> {
22
+ export class InsertQueryBuilder<T extends Record<string, unknown>> extends WhereQueryBuilder<T> {
23
+ private insertLog = createLogger("insert")
24
+
25
+ // ===== Private Helpers =====
12
26
 
13
27
  /**
14
- * Insert a single row or multiple rows into the table.
15
- *
16
- * @param data - Single object or array of objects to insert
17
- * @param options - Insert options (OR IGNORE, OR REPLACE, etc.)
18
- * @returns Insert result with insertId and changes count
28
+ * Get the conflict resolution clause for INSERT statements
19
29
  */
20
- insert(
21
- data: Partial<T> | Partial<T>[],
22
- options?: InsertOptions,
23
- ): InsertResult {
24
- this.getLogger().debug(`Building Data Array: ${data}`)
25
- const rows = Array.isArray(data) ? data : [data];
30
+ private getConflictClause(options?: InsertOptions): string {
31
+ if (!options) return "INSERT"
32
+ if (options.orIgnore) return "INSERT OR IGNORE"
33
+ if (options.orReplace) return "INSERT OR REPLACE"
34
+ if (options.orAbort) return "INSERT OR ABORT"
35
+ if (options.orFail) return "INSERT OR FAIL"
36
+ if (options.orRollback) return "INSERT OR ROLLBACK"
37
+ return "INSERT"
38
+ }
26
39
 
40
+ /**
41
+ * Extract unique columns from a set of rows
42
+ */
43
+ private extractColumns(rows: RowData[]): string[] {
44
+ const columnSet = new Set<string>()
27
45
 
46
+ for (const row of rows) {
47
+ for (const col of Object.keys(row)) {
48
+ columnSet.add(col)
49
+ }
50
+ }
28
51
 
29
- // Transform rows to handle JSON serialization
30
- const transformedRows = rows.map((row) => this.transformRowToDb(row));
52
+ return Array.from(columnSet)
53
+ }
31
54
 
32
- this.getLogger().debug(`Transformed row: ${JSON.stringify(transformedRows)}`)
55
+ /**
56
+ * Build an INSERT query
57
+ */
58
+ private buildInsertQuery(columns: string[], options?: InsertOptions): string {
59
+ const conflictClause = this.getConflictClause(options)
60
+ const tableName = quoteIdentifier(this.getTableName())
61
+ const columnList = quoteIdentifiers(columns)
62
+ const placeholders = buildPlaceholders(columns)
33
63
 
34
- if (transformedRows.length === 0) {
35
- throw new Error("insert: data cannot be empty");
64
+ return `${conflictClause} INTO ${tableName} (${columnList}) VALUES (${placeholders})`
65
+ }
66
+
67
+ /**
68
+ * Execute insert for a single row
69
+ */
70
+ private executeInsert(
71
+ query: string,
72
+ row: RowData,
73
+ columns: string[]
74
+ ): { insertId: number; changes: number } {
75
+ const values = columns.map((col) => row[col] ?? null) as SQLQueryBindings[]
76
+
77
+ const result = this.getDb()
78
+ .prepare(query)
79
+ .run(...values)
80
+
81
+ return {
82
+ insertId: result.lastInsertRowid ? Number(result.lastInsertRowid) : 0,
83
+ changes: result.changes,
36
84
  }
85
+ }
37
86
 
38
- // Get all unique columns from all rows
39
- const allColumns = new Set<string>();
40
- for (const row of transformedRows) {
41
- for (const col of Object.keys(row)) {
42
- allColumns.add(col);
43
- }
87
+ // ===== Public Insert Methods =====
88
+
89
+ /**
90
+ * Insert a single row or multiple rows into the table
91
+ *
92
+ * @example
93
+ * // Single insert
94
+ * table.insert({ name: "Alice", email: "alice@example.com" })
95
+ *
96
+ * @example
97
+ * // Multiple inserts
98
+ * table.insert([
99
+ * { name: "Alice", email: "alice@example.com" },
100
+ * { name: "Bob", email: "bob@example.com" }
101
+ * ])
102
+ */
103
+ insert(data: Partial<T> | Partial<T>[], options?: InsertOptions): InsertResult {
104
+ const rows = Array.isArray(data) ? data : [data]
105
+
106
+ if (rows.length === 0) {
107
+ throw new Error("insert: data cannot be empty")
44
108
  }
45
109
 
46
- const columns = Array.from(allColumns);
110
+ // Transform rows (serialize JSON, etc.)
111
+ const transformedRows = rows.map((row) => this.transformRowToDb(row))
112
+
113
+ // Extract columns from all rows
114
+ const columns = this.extractColumns(transformedRows)
115
+
47
116
  if (columns.length === 0) {
48
- throw new Error("insert: no columns to insert");
117
+ throw new Error("insert: no columns to insert")
49
118
  }
50
119
 
51
- // Build INSERT statement with conflict resolution
52
- let insertType = "INSERT";
53
- if (options?.orIgnore) insertType = "INSERT OR IGNORE";
54
- else if (options?.orReplace) insertType = "INSERT OR REPLACE";
55
- else if (options?.orAbort) insertType = "INSERT OR ABORT";
56
- else if (options?.orFail) insertType = "INSERT OR FAIL";
57
- else if (options?.orRollback) insertType = "INSERT OR ROLLBACK";
120
+ // Build and execute query
121
+ const query = this.buildInsertQuery(columns, options)
58
122
 
59
- const quotedColumns = columns
60
- .map((col) => this.quoteIdentifier(col))
61
- .join(", ");
62
- const placeholders = columns.map(() => "?").join(", ");
123
+ this.insertLog.query("INSERT", query)
63
124
 
64
- const query = `${insertType} INTO ${this.quoteIdentifier(this.getTableName())} (${quotedColumns}) VALUES (${placeholders})`;
65
- const stmt = this.getDb().prepare(query);
125
+ let totalChanges = 0
126
+ let lastInsertId = 0
66
127
 
67
- let totalChanges = 0;
68
- let lastInsertId = 0;
69
-
70
- // Execute for each row
71
128
  for (const row of transformedRows) {
72
- const values = columns.map(
73
- (col) => row[col as keyof typeof row] ?? null,
74
- ) as SQLQueryBindings[];
75
- const result = stmt.run(...values);
76
- totalChanges += result.changes;
77
- if (result.lastInsertRowid) {
78
- lastInsertId = Number(result.lastInsertRowid);
129
+ const result = this.executeInsert(query, row, columns)
130
+ totalChanges += result.changes
131
+ if (result.insertId > 0) {
132
+ lastInsertId = result.insertId
79
133
  }
80
134
  }
81
135
 
82
- const result = {
136
+ this.insertLog.result("INSERT", totalChanges)
137
+ this.reset()
138
+
139
+ return {
83
140
  insertId: lastInsertId,
84
141
  changes: totalChanges,
85
- };
86
- this.reset();
87
- return result;
142
+ }
88
143
  }
89
144
 
90
145
  /**
91
- * Insert with OR IGNORE conflict resolution.
92
- * Convenience method equivalent to insert(data, { orIgnore: true })
146
+ * Insert with OR IGNORE conflict resolution
93
147
  *
94
- * @param data - Single object or array of objects to insert
95
- * @returns Insert result with insertId and changes count
148
+ * Ignores the insert if it would violate a constraint
96
149
  */
97
150
  insertOrIgnore(data: Partial<T> | Partial<T>[]): InsertResult {
98
- return this.insert(data, { orIgnore: true });
151
+ return this.insert(data, { orIgnore: true })
99
152
  }
100
153
 
101
154
  /**
102
- * Insert with OR REPLACE conflict resolution.
103
- * Convenience method equivalent to insert(data, { orReplace: true })
155
+ * Insert with OR REPLACE conflict resolution
104
156
  *
105
- * @param data - Single object or array of objects to insert
106
- * @returns Insert result with insertId and changes count
157
+ * Replaces the existing row if a constraint is violated
107
158
  */
108
159
  insertOrReplace(data: Partial<T> | Partial<T>[]): InsertResult {
109
- return this.insert(data, { orReplace: true });
160
+ return this.insert(data, { orReplace: true })
110
161
  }
111
162
 
112
163
  /**
113
- * Insert with OR ABORT conflict resolution.
114
- * This is the default behavior but provided for explicit usage.
164
+ * Insert with OR ABORT conflict resolution
115
165
  *
116
- * @param data - Single object or array of objects to insert
117
- * @returns Insert result with insertId and changes count
166
+ * Aborts the current SQL statement on constraint violation (default behavior)
118
167
  */
119
168
  insertOrAbort(data: Partial<T> | Partial<T>[]): InsertResult {
120
- return this.insert(data, { orAbort: true });
169
+ return this.insert(data, { orAbort: true })
121
170
  }
122
171
 
123
172
  /**
124
- * Insert with OR FAIL conflict resolution.
173
+ * Insert with OR FAIL conflict resolution
125
174
  *
126
- * @param data - Single object or array of objects to insert
127
- * @returns Insert result with insertId and changes count
175
+ * Fails the current SQL statement on constraint violation
128
176
  */
129
177
  insertOrFail(data: Partial<T> | Partial<T>[]): InsertResult {
130
- return this.insert(data, { orFail: true });
178
+ return this.insert(data, { orFail: true })
131
179
  }
132
180
 
133
181
  /**
134
- * Insert with OR ROLLBACK conflict resolution.
182
+ * Insert with OR ROLLBACK conflict resolution
135
183
  *
136
- * @param data - Single object or array of objects to insert
137
- * @returns Insert result with insertId and changes count
184
+ * Rolls back the entire transaction on constraint violation
138
185
  */
139
186
  insertOrRollback(data: Partial<T> | Partial<T>[]): InsertResult {
140
- return this.insert(data, { orRollback: true });
187
+ return this.insert(data, { orRollback: true })
141
188
  }
142
189
 
143
190
  /**
144
- * Insert and get the inserted row back.
145
- * This is useful when you want to see the row with auto-generated fields.
191
+ * Insert a row and return the inserted row with all fields
146
192
  *
147
- * @param data - Single object to insert (bulk not supported for this method)
148
- * @param options - Insert options
149
- * @returns The inserted row with all fields, or null if insertion failed
193
+ * Useful when you want to see auto-generated values (ID, timestamps, etc.)
194
+ *
195
+ * @example
196
+ * const user = table.insertAndGet({ name: "Alice", email: "alice@example.com" })
197
+ * console.log(user.id) // Auto-generated ID
150
198
  */
151
199
  insertAndGet(data: Partial<T>, options?: InsertOptions): T | null {
152
- const result = this.insert(data, options);
200
+ const result = this.insert(data, options)
153
201
 
154
- if (result.changes === 0) {
155
- return null;
202
+ if (result.changes === 0 || result.insertId <= 0) {
203
+ return null
156
204
  }
157
205
 
158
- // If we have an insertId, try to fetch the inserted row
159
- if (result.insertId > 0) {
160
- try {
161
- const row = this.getDb()
162
- .prepare(
163
- `SELECT * FROM ${this.quoteIdentifier(this.getTableName())} WHERE rowid = ?`,
164
- )
165
- .get(result.insertId) as T | null;
166
- return row ? this.transformRowFromDb(row) : null;
167
- } catch {
168
- // If fetching by rowid fails, return null
169
- return null;
170
- }
171
- }
206
+ // Fetch the inserted row by rowid
207
+ try {
208
+ const query = `SELECT * FROM ${quoteIdentifier(this.getTableName())} WHERE rowid = ?`
209
+ const row = this.getDb().prepare(query).get(result.insertId) as T | null
172
210
 
173
- return null;
211
+ return row ? this.transformRowFromDb(row) : null
212
+ } catch {
213
+ // If fetching by rowid fails (e.g., WITHOUT ROWID table), return null
214
+ return null
215
+ }
174
216
  }
175
217
 
176
218
  /**
177
- * Batch insert with transaction support.
178
- * This method wraps multiple inserts in a transaction for better performance
219
+ * Batch insert with transaction support
220
+ *
221
+ * Wraps multiple inserts in a transaction for better performance
179
222
  * and atomicity when inserting large amounts of data.
180
223
  *
181
- * @param rows - Array of objects to insert
182
- * @param options - Insert options
183
- * @returns Insert result with total changes
224
+ * @example
225
+ * table.insertBatch([
226
+ * { name: "User 1", email: "user1@example.com" },
227
+ * { name: "User 2", email: "user2@example.com" },
228
+ * { name: "User 3", email: "user3@example.com" },
229
+ * ])
184
230
  */
185
231
  insertBatch(rows: Partial<T>[], options?: InsertOptions): InsertResult {
186
232
  if (!Array.isArray(rows) || rows.length === 0) {
187
- throw new Error("insertBatch: rows must be a non-empty array");
233
+ throw new Error("insertBatch: rows must be a non-empty array")
188
234
  }
189
235
 
190
- const db = this.getDb();
236
+ const db = this.getDb()
191
237
 
192
238
  // Use a transaction for batch operations
193
239
  const transaction = db.transaction((rowsToInsert: Partial<T>[]) => {
194
- let totalChanges = 0;
195
- let lastInsertId = 0;
240
+ // Transform all rows
241
+ const transformedRows = rowsToInsert.map((row) => this.transformRowToDb(row))
196
242
 
197
- // Transform rows to handle JSON serialization
198
- const transformedRows = rowsToInsert.map((row) =>
199
- this.transformRowToDb(row),
200
- );
201
-
202
- // Get all unique columns from all rows
203
- const allColumns = new Set<string>();
204
- for (const row of transformedRows) {
205
- for (const col of Object.keys(row)) {
206
- allColumns.add(col);
207
- }
208
- }
243
+ // Extract columns from all rows
244
+ const columns = this.extractColumns(transformedRows)
209
245
 
210
- const columns = Array.from(allColumns);
211
246
  if (columns.length === 0) {
212
- throw new Error("insertBatch: no columns to insert");
247
+ throw new Error("insertBatch: no columns to insert")
213
248
  }
214
249
 
215
- // Build INSERT statement with conflict resolution
216
- let insertType = "INSERT";
217
- if (options?.orIgnore) insertType = "INSERT OR IGNORE";
218
- else if (options?.orReplace) insertType = "INSERT OR REPLACE";
219
- else if (options?.orAbort) insertType = "INSERT OR ABORT";
220
- else if (options?.orFail) insertType = "INSERT OR FAIL";
221
- else if (options?.orRollback) insertType = "INSERT OR ROLLBACK";
250
+ // Build query and prepare statement
251
+ const query = this.buildInsertQuery(columns, options)
252
+ const stmt = db.prepare(query)
222
253
 
223
- const quotedColumns = columns
224
- .map((col) => this.quoteIdentifier(col))
225
- .join(", ");
226
- const placeholders = columns.map(() => "?").join(", ");
254
+ this.insertLog.query("INSERT BATCH", query)
227
255
 
228
- const query = `${insertType} INTO ${this.quoteIdentifier(this.getTableName())} (${quotedColumns}) VALUES (${placeholders})`;
229
- const stmt = db.prepare(query);
256
+ let totalChanges = 0
257
+ let lastInsertId = 0
230
258
 
259
+ // Execute for each row
231
260
  for (const row of transformedRows) {
232
- const values = columns.map(
233
- (col) => row[col as keyof typeof row] ?? null,
234
- ) as SQLQueryBindings[];
235
- const result = stmt.run(...values);
236
- totalChanges += result.changes;
261
+ const values = columns.map((col) => row[col] ?? null) as SQLQueryBindings[]
262
+ const result = stmt.run(...values)
263
+
264
+ totalChanges += result.changes
237
265
  if (result.lastInsertRowid) {
238
- lastInsertId = Number(result.lastInsertRowid);
266
+ lastInsertId = Number(result.lastInsertRowid)
239
267
  }
240
268
  }
241
269
 
242
- return { insertId: lastInsertId, changes: totalChanges };
243
- });
270
+ return { insertId: lastInsertId, changes: totalChanges }
271
+ })
272
+
273
+ const result = transaction(rows)
274
+
275
+ this.insertLog.result("INSERT BATCH", result.changes)
276
+ this.reset()
244
277
 
245
- const result = transaction(rows);
246
- this.reset();
247
- return result;
278
+ return result
248
279
  }
249
280
  }