@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/insert.ts
CHANGED
|
@@ -1,249 +1,280 @@
|
|
|
1
|
-
import type { SQLQueryBindings } from "bun:sqlite"
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
options
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
52
|
+
return Array.from(columnSet)
|
|
53
|
+
}
|
|
31
54
|
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
52
|
-
|
|
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
|
-
|
|
60
|
-
.map((col) => this.quoteIdentifier(col))
|
|
61
|
-
.join(", ");
|
|
62
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
123
|
+
this.insertLog.query("INSERT", query)
|
|
63
124
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* @
|
|
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
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
182
|
-
*
|
|
183
|
-
*
|
|
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
|
-
|
|
195
|
-
|
|
240
|
+
// Transform all rows
|
|
241
|
+
const transformedRows = rowsToInsert.map((row) => this.transformRowToDb(row))
|
|
196
242
|
|
|
197
|
-
//
|
|
198
|
-
const
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
224
|
-
.map((col) => this.quoteIdentifier(col))
|
|
225
|
-
.join(", ");
|
|
226
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
254
|
+
this.insertLog.query("INSERT BATCH", query)
|
|
227
255
|
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
246
|
-
this.reset();
|
|
247
|
-
return result;
|
|
278
|
+
return result
|
|
248
279
|
}
|
|
249
280
|
}
|