@dockstat/sqlite-wrapper 1.2.8 → 1.3.1

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