@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.
- package/LICENSE +373 -373
- package/README.md +553 -99
- package/index.ts +1128 -858
- package/package.json +60 -54
- package/query-builder/base.ts +187 -221
- package/query-builder/delete.ts +447 -352
- package/query-builder/index.ts +410 -431
- package/query-builder/insert.ts +286 -249
- package/query-builder/select.ts +335 -358
- package/query-builder/update.ts +314 -278
- package/query-builder/where.ts +272 -307
- package/types.ts +608 -623
- package/utils/index.ts +46 -0
- package/utils/logger.ts +216 -0
- package/utils/sql.ts +241 -0
- package/utils/transformer.ts +259 -0
package/query-builder/insert.ts
CHANGED
|
@@ -1,249 +1,286 @@
|
|
|
1
|
-
import type { SQLQueryBindings } from "bun:sqlite"
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
}
|