@dockstat/sqlite-wrapper 1.2.8 → 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/LICENSE +373 -373
- package/README.md +553 -99
- package/index.ts +1120 -858
- package/package.json +60 -54
- package/query-builder/base.ts +183 -221
- package/query-builder/delete.ts +441 -352
- package/query-builder/index.ts +409 -431
- package/query-builder/insert.ts +280 -249
- package/query-builder/select.ts +333 -358
- package/query-builder/update.ts +308 -278
- package/query-builder/where.ts +272 -307
- package/types.ts +608 -623
- 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/update.ts
CHANGED
|
@@ -1,278 +1,308 @@
|
|
|
1
|
-
import type { SQLQueryBindings } from "bun:sqlite"
|
|
2
|
-
import type { UpdateResult } from "../types"
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* Handles
|
|
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
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
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
|
-
this.
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
1
|
+
import type { SQLQueryBindings } from "bun:sqlite"
|
|
2
|
+
import type { UpdateResult } from "../types"
|
|
3
|
+
import { buildSetClause, createLogger, quoteIdentifier, type RowData } from "../utils"
|
|
4
|
+
import { SelectQueryBuilder } from "./select"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* UpdateQueryBuilder - Handles UPDATE operations with safety checks
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Safe updates (WHERE required to prevent accidental full-table updates)
|
|
11
|
+
* - Upsert (INSERT OR REPLACE)
|
|
12
|
+
* - Increment/decrement numeric columns
|
|
13
|
+
* - Update and get (returns updated rows)
|
|
14
|
+
* - Batch updates with transaction support
|
|
15
|
+
* - Automatic JSON serialization
|
|
16
|
+
*/
|
|
17
|
+
export class UpdateQueryBuilder<T extends Record<string, unknown>> extends SelectQueryBuilder<T> {
|
|
18
|
+
private updateLog = createLogger("update")
|
|
19
|
+
|
|
20
|
+
// ===== Public Update Methods =====
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Update rows matching the WHERE conditions
|
|
24
|
+
*
|
|
25
|
+
* Requires at least one WHERE condition to prevent accidental full-table updates.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* table.where({ id: 1 }).update({ name: "Updated Name" })
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* table.where({ active: false }).update({ deleted_at: Date.now() })
|
|
32
|
+
*/
|
|
33
|
+
update(data: Partial<T>): UpdateResult {
|
|
34
|
+
this.requireWhereClause("UPDATE")
|
|
35
|
+
|
|
36
|
+
// Transform data (serialize JSON, etc.)
|
|
37
|
+
const transformedData = this.transformRowToDb(data)
|
|
38
|
+
const columns = Object.keys(transformedData)
|
|
39
|
+
|
|
40
|
+
if (columns.length === 0) {
|
|
41
|
+
this.reset()
|
|
42
|
+
throw new Error("update: no columns to update")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Handle regex conditions by fetching matching rows first
|
|
46
|
+
if (this.hasRegexConditions()) {
|
|
47
|
+
return this.updateWithRegexConditions(transformedData)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build UPDATE statement
|
|
51
|
+
const setClause = buildSetClause(columns)
|
|
52
|
+
const [whereClause, whereParams] = this.buildWhereClause()
|
|
53
|
+
|
|
54
|
+
const query = `UPDATE ${quoteIdentifier(this.getTableName())} SET ${setClause}${whereClause}`
|
|
55
|
+
const updateValues = columns.map((col) => transformedData[col])
|
|
56
|
+
const allParams = [...updateValues, ...whereParams] as SQLQueryBindings[]
|
|
57
|
+
|
|
58
|
+
this.updateLog.query("UPDATE", query, allParams)
|
|
59
|
+
|
|
60
|
+
const result = this.getDb()
|
|
61
|
+
.prepare(query)
|
|
62
|
+
.run(...allParams)
|
|
63
|
+
|
|
64
|
+
this.updateLog.result("UPDATE", result.changes)
|
|
65
|
+
this.reset()
|
|
66
|
+
|
|
67
|
+
return { changes: result.changes }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Handle UPDATE when regex conditions are present
|
|
72
|
+
*
|
|
73
|
+
* Since regex conditions are applied client-side, we need to:
|
|
74
|
+
* 1. Fetch all rows matching SQL conditions
|
|
75
|
+
* 2. Filter with regex client-side
|
|
76
|
+
* 3. Update each matching row by rowid
|
|
77
|
+
*/
|
|
78
|
+
private updateWithRegexConditions(transformedData: RowData): UpdateResult {
|
|
79
|
+
const columns = Object.keys(transformedData)
|
|
80
|
+
|
|
81
|
+
// Get rows matching SQL conditions (without regex)
|
|
82
|
+
// Use alias for rowid to avoid collision with INTEGER PRIMARY KEY columns
|
|
83
|
+
const [whereClause, whereParams] = this.buildWhereClause()
|
|
84
|
+
const selectQuery = `SELECT rowid as _rowid_, * FROM ${quoteIdentifier(this.getTableName())}${whereClause}`
|
|
85
|
+
|
|
86
|
+
const candidateRows = this.getDb()
|
|
87
|
+
.prepare(selectQuery)
|
|
88
|
+
.all(...whereParams) as (T & { _rowid_: number })[]
|
|
89
|
+
|
|
90
|
+
// Apply regex filtering
|
|
91
|
+
const matchingRows = this.applyRegexFiltering(candidateRows)
|
|
92
|
+
|
|
93
|
+
if (matchingRows.length === 0) {
|
|
94
|
+
this.reset()
|
|
95
|
+
return { changes: 0 }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Update each matching row by rowid
|
|
99
|
+
const setClause = buildSetClause(columns)
|
|
100
|
+
const updateQuery = `UPDATE ${quoteIdentifier(this.getTableName())} SET ${setClause} WHERE rowid = ?`
|
|
101
|
+
const stmt = this.getDb().prepare(updateQuery)
|
|
102
|
+
|
|
103
|
+
this.updateLog.query("UPDATE (regex)", updateQuery)
|
|
104
|
+
|
|
105
|
+
let totalChanges = 0
|
|
106
|
+
const updateValues = columns.map((col) => transformedData[col])
|
|
107
|
+
|
|
108
|
+
for (const row of matchingRows) {
|
|
109
|
+
const result = stmt.run(...updateValues, row._rowid_ as SQLQueryBindings)
|
|
110
|
+
totalChanges += result.changes
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.updateLog.result("UPDATE (regex)", totalChanges)
|
|
114
|
+
this.reset()
|
|
115
|
+
|
|
116
|
+
return { changes: totalChanges }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Upsert (INSERT OR REPLACE) a row
|
|
121
|
+
*
|
|
122
|
+
* If a row with the same unique key exists, it will be replaced.
|
|
123
|
+
* Otherwise, a new row will be inserted.
|
|
124
|
+
*
|
|
125
|
+
* Requires a unique constraint or primary key on the table.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* table.upsert({ email: "alice@example.com", name: "Alice Updated" })
|
|
129
|
+
*/
|
|
130
|
+
upsert(data: Partial<T>): UpdateResult {
|
|
131
|
+
const transformedData = this.transformRowToDb(data)
|
|
132
|
+
const columns = Object.keys(transformedData)
|
|
133
|
+
|
|
134
|
+
if (columns.length === 0) {
|
|
135
|
+
this.reset()
|
|
136
|
+
throw new Error("upsert: no columns to upsert")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const columnList = columns.map((col) => quoteIdentifier(col)).join(", ")
|
|
140
|
+
const placeholders = columns.map(() => "?").join(", ")
|
|
141
|
+
const values = columns.map((col) => transformedData[col] ?? null)
|
|
142
|
+
|
|
143
|
+
const query = `INSERT OR REPLACE INTO ${quoteIdentifier(this.getTableName())} (${columnList}) VALUES (${placeholders})`
|
|
144
|
+
|
|
145
|
+
this.updateLog.query("UPSERT", query, values)
|
|
146
|
+
|
|
147
|
+
const result = this.getDb()
|
|
148
|
+
.prepare(query)
|
|
149
|
+
.run(...values)
|
|
150
|
+
|
|
151
|
+
this.updateLog.result("UPSERT", result.changes)
|
|
152
|
+
this.reset()
|
|
153
|
+
|
|
154
|
+
return { changes: result.changes }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Increment a numeric column by a specified amount
|
|
159
|
+
*
|
|
160
|
+
* More efficient than fetching, calculating, and updating.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* table.where({ id: 1 }).increment("login_count")
|
|
164
|
+
* table.where({ id: 1 }).increment("points", 10)
|
|
165
|
+
*/
|
|
166
|
+
increment(column: keyof T, amount = 1): UpdateResult {
|
|
167
|
+
this.requireWhereClause("INCREMENT")
|
|
168
|
+
|
|
169
|
+
const [whereClause, whereParams] = this.buildWhereClause()
|
|
170
|
+
const quotedColumn = quoteIdentifier(String(column))
|
|
171
|
+
|
|
172
|
+
const query = `UPDATE ${quoteIdentifier(this.getTableName())} SET ${quotedColumn} = ${quotedColumn} + ?${whereClause}`
|
|
173
|
+
const params = [amount, ...whereParams] as SQLQueryBindings[]
|
|
174
|
+
|
|
175
|
+
this.updateLog.query("INCREMENT", query, params)
|
|
176
|
+
|
|
177
|
+
const result = this.getDb()
|
|
178
|
+
.prepare(query)
|
|
179
|
+
.run(...params)
|
|
180
|
+
|
|
181
|
+
this.updateLog.result("INCREMENT", result.changes)
|
|
182
|
+
this.reset()
|
|
183
|
+
|
|
184
|
+
return { changes: result.changes }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Decrement a numeric column by a specified amount
|
|
189
|
+
*
|
|
190
|
+
* Equivalent to increment with a negative amount.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* table.where({ id: 1 }).decrement("credits")
|
|
194
|
+
* table.where({ id: 1 }).decrement("stock", 5)
|
|
195
|
+
*/
|
|
196
|
+
decrement(column: keyof T, amount = 1): UpdateResult {
|
|
197
|
+
return this.increment(column, -amount)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Update rows and return the updated rows
|
|
202
|
+
*
|
|
203
|
+
* Note: Returns the rows as they were BEFORE the update.
|
|
204
|
+
* For post-update values, query the rows again after updating.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* const updatedRows = table.where({ active: false }).updateAndGet({ deleted: true })
|
|
208
|
+
*/
|
|
209
|
+
updateAndGet(data: Partial<T>): T[] {
|
|
210
|
+
// Preserve WHERE state before all() resets it
|
|
211
|
+
const savedWhereConditions = [...this.state.whereConditions]
|
|
212
|
+
const savedWhereParams = [...this.state.whereParams]
|
|
213
|
+
const savedRegexConditions = [...this.state.regexConditions]
|
|
214
|
+
|
|
215
|
+
// Get rows before update
|
|
216
|
+
const rowsToUpdate = this.all()
|
|
217
|
+
|
|
218
|
+
// Restore WHERE state for the update
|
|
219
|
+
this.state.whereConditions = savedWhereConditions
|
|
220
|
+
this.state.whereParams = savedWhereParams
|
|
221
|
+
this.state.regexConditions = savedRegexConditions
|
|
222
|
+
|
|
223
|
+
// Perform the update
|
|
224
|
+
const updateResult = this.update(data)
|
|
225
|
+
|
|
226
|
+
if (updateResult.changes === 0) {
|
|
227
|
+
return []
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return rowsToUpdate
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Batch update multiple rows with different values
|
|
235
|
+
*
|
|
236
|
+
* Each update item specifies its own WHERE conditions and data.
|
|
237
|
+
* All updates are wrapped in a transaction for atomicity.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* table.updateBatch([
|
|
241
|
+
* { where: { id: 1 }, data: { name: "Alice" } },
|
|
242
|
+
* { where: { id: 2 }, data: { name: "Bob" } },
|
|
243
|
+
* ])
|
|
244
|
+
*/
|
|
245
|
+
updateBatch(updates: Array<{ where: Partial<T>; data: Partial<T> }>): UpdateResult {
|
|
246
|
+
if (!Array.isArray(updates) || updates.length === 0) {
|
|
247
|
+
this.reset()
|
|
248
|
+
throw new Error("updateBatch: updates must be a non-empty array")
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const db = this.getDb()
|
|
252
|
+
|
|
253
|
+
const transaction = db.transaction(
|
|
254
|
+
(updatesToProcess: Array<{ where: Partial<T>; data: Partial<T> }>) => {
|
|
255
|
+
let totalChanges = 0
|
|
256
|
+
|
|
257
|
+
for (const { where: whereData, data } of updatesToProcess) {
|
|
258
|
+
// Transform data
|
|
259
|
+
const transformedData = this.transformRowToDb(data)
|
|
260
|
+
const updateColumns = Object.keys(transformedData)
|
|
261
|
+
|
|
262
|
+
if (updateColumns.length === 0) {
|
|
263
|
+
continue // Skip empty updates
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Build WHERE conditions
|
|
267
|
+
const whereConditions: string[] = []
|
|
268
|
+
const whereParams: SQLQueryBindings[] = []
|
|
269
|
+
|
|
270
|
+
for (const [column, value] of Object.entries(whereData)) {
|
|
271
|
+
if (value === null || value === undefined) {
|
|
272
|
+
whereConditions.push(`${quoteIdentifier(column)} IS NULL`)
|
|
273
|
+
} else {
|
|
274
|
+
whereConditions.push(`${quoteIdentifier(column)} = ?`)
|
|
275
|
+
whereParams.push(value as SQLQueryBindings)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (whereConditions.length === 0) {
|
|
280
|
+
throw new Error("updateBatch: each update must have WHERE conditions")
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Build UPDATE statement
|
|
284
|
+
const setClause = buildSetClause(updateColumns)
|
|
285
|
+
const whereClause = ` WHERE ${whereConditions.join(" AND ")}`
|
|
286
|
+
const query = `UPDATE ${quoteIdentifier(this.getTableName())} SET ${setClause}${whereClause}`
|
|
287
|
+
|
|
288
|
+
const updateValues = updateColumns.map((col) => transformedData[col] ?? null)
|
|
289
|
+
const allParams = [...updateValues, ...whereParams] as SQLQueryBindings[]
|
|
290
|
+
|
|
291
|
+
const result = db.prepare(query).run(...allParams)
|
|
292
|
+
totalChanges += result.changes
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { changes: totalChanges }
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
this.updateLog.query("UPDATE BATCH", `${updates.length} updates`)
|
|
300
|
+
|
|
301
|
+
const result = transaction(updates)
|
|
302
|
+
|
|
303
|
+
this.updateLog.result("UPDATE BATCH", result.changes)
|
|
304
|
+
this.reset()
|
|
305
|
+
|
|
306
|
+
return result
|
|
307
|
+
}
|
|
308
|
+
}
|