@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/update.ts
CHANGED
|
@@ -1,278 +1,308 @@
|
|
|
1
|
-
import type { SQLQueryBindings } from "bun:sqlite"
|
|
2
|
-
import type { UpdateResult } from "../types"
|
|
3
|
-
import {
|
|
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"
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
8
16
|
*/
|
|
9
|
-
export class UpdateQueryBuilder<
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
export class UpdateQueryBuilder<T extends Record<string, unknown>> extends SelectQueryBuilder<T> {
|
|
18
|
+
private updateLog = createLogger("update")
|
|
19
|
+
|
|
20
|
+
// ===== Public Update Methods =====
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
|
-
* Update rows matching the WHERE conditions
|
|
14
|
-
*
|
|
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" })
|
|
15
29
|
*
|
|
16
|
-
* @
|
|
17
|
-
*
|
|
30
|
+
* @example
|
|
31
|
+
* table.where({ active: false }).update({ deleted_at: Date.now() })
|
|
18
32
|
*/
|
|
19
33
|
update(data: Partial<T>): UpdateResult {
|
|
20
|
-
this.requireWhereClause("UPDATE")
|
|
21
|
-
|
|
22
|
-
// Transform data
|
|
23
|
-
const transformedData = this.transformRowToDb(data)
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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")
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
// Handle regex conditions by
|
|
45
|
+
// Handle regex conditions by fetching matching rows first
|
|
31
46
|
if (this.hasRegexConditions()) {
|
|
32
|
-
|
|
33
|
-
return this.updateWithRegexConditions(transformedData);
|
|
47
|
+
return this.updateWithRegexConditions(transformedData)
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
// Build UPDATE statement
|
|
37
|
-
const setClause =
|
|
38
|
-
|
|
39
|
-
.join(", ");
|
|
51
|
+
const setClause = buildSetClause(columns)
|
|
52
|
+
const [whereClause, whereParams] = this.buildWhereClause()
|
|
40
53
|
|
|
41
|
-
const
|
|
42
|
-
const
|
|
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[]
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
const allParams = [...updateValues, ...whereParams];
|
|
58
|
+
this.updateLog.query("UPDATE", query, allParams)
|
|
46
59
|
|
|
47
60
|
const result = this.getDb()
|
|
48
61
|
.prepare(query)
|
|
49
|
-
.run(...allParams)
|
|
62
|
+
.run(...allParams)
|
|
63
|
+
|
|
64
|
+
this.updateLog.result("UPDATE", result.changes)
|
|
65
|
+
this.reset()
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
changes: result.changes,
|
|
53
|
-
};
|
|
54
|
-
this.reset();
|
|
55
|
-
return out;
|
|
67
|
+
return { changes: result.changes }
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
/**
|
|
59
|
-
* Handle UPDATE
|
|
60
|
-
*
|
|
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
|
|
61
77
|
*/
|
|
62
|
-
private updateWithRegexConditions(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
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
|
+
|
|
67
86
|
const candidateRows = this.getDb()
|
|
68
|
-
.prepare(
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
.all(...selectParams) as (T & { rowid: number })[];
|
|
87
|
+
.prepare(selectQuery)
|
|
88
|
+
.all(...whereParams) as (T & { _rowid_: number })[]
|
|
72
89
|
|
|
73
90
|
// Apply regex filtering
|
|
74
|
-
const matchingRows = this.applyRegexFiltering(candidateRows)
|
|
91
|
+
const matchingRows = this.applyRegexFiltering(candidateRows)
|
|
75
92
|
|
|
76
93
|
if (matchingRows.length === 0) {
|
|
77
|
-
|
|
78
|
-
return { changes: 0 }
|
|
94
|
+
this.reset()
|
|
95
|
+
return { changes: 0 }
|
|
79
96
|
}
|
|
80
97
|
|
|
81
98
|
// Update each matching row by rowid
|
|
82
|
-
const
|
|
83
|
-
const setClause =
|
|
84
|
-
|
|
85
|
-
.join(", ");
|
|
99
|
+
const setClause = buildSetClause(columns)
|
|
100
|
+
const updateQuery = `UPDATE ${quoteIdentifier(this.getTableName())} SET ${setClause} WHERE rowid = ?`
|
|
101
|
+
const stmt = this.getDb().prepare(updateQuery)
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
const stmt = this.getDb().prepare(updateQuery);
|
|
103
|
+
this.updateLog.query("UPDATE (regex)", updateQuery)
|
|
89
104
|
|
|
90
|
-
let totalChanges = 0
|
|
91
|
-
const updateValues =
|
|
105
|
+
let totalChanges = 0
|
|
106
|
+
const updateValues = columns.map((col) => transformedData[col])
|
|
92
107
|
|
|
93
108
|
for (const row of matchingRows) {
|
|
94
|
-
const result = stmt.run(...updateValues, row.
|
|
95
|
-
totalChanges += result.changes
|
|
109
|
+
const result = stmt.run(...updateValues, row._rowid_ as SQLQueryBindings)
|
|
110
|
+
totalChanges += result.changes
|
|
96
111
|
}
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
|
|
113
|
+
this.updateLog.result("UPDATE (regex)", totalChanges)
|
|
114
|
+
this.reset()
|
|
115
|
+
|
|
116
|
+
return { changes: totalChanges }
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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.
|
|
105
124
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
125
|
+
* Requires a unique constraint or primary key on the table.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* table.upsert({ email: "alice@example.com", name: "Alice Updated" })
|
|
108
129
|
*/
|
|
109
130
|
upsert(data: Partial<T>): UpdateResult {
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
131
|
+
const transformedData = this.transformRowToDb(data)
|
|
132
|
+
const columns = Object.keys(transformedData)
|
|
133
|
+
|
|
113
134
|
if (columns.length === 0) {
|
|
114
|
-
|
|
115
|
-
throw new Error("upsert: no columns to upsert")
|
|
135
|
+
this.reset()
|
|
136
|
+
throw new Error("upsert: no columns to upsert")
|
|
116
137
|
}
|
|
117
138
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
139
|
+
const columnList = columns.map((col) => quoteIdentifier(col)).join(", ")
|
|
140
|
+
const placeholders = columns.map(() => "?").join(", ")
|
|
141
|
+
const values = columns.map((col) => transformedData[col] ?? null)
|
|
122
142
|
|
|
123
|
-
const query = `INSERT OR REPLACE INTO ${
|
|
143
|
+
const query = `INSERT OR REPLACE INTO ${quoteIdentifier(this.getTableName())} (${columnList}) VALUES (${placeholders})`
|
|
144
|
+
|
|
145
|
+
this.updateLog.query("UPSERT", query, values)
|
|
124
146
|
|
|
125
|
-
const values = columns.map((col) => transformedData[col] ?? null);
|
|
126
147
|
const result = this.getDb()
|
|
127
148
|
.prepare(query)
|
|
128
|
-
.run(...values)
|
|
149
|
+
.run(...values)
|
|
150
|
+
|
|
151
|
+
this.updateLog.result("UPSERT", result.changes)
|
|
152
|
+
this.reset()
|
|
129
153
|
|
|
130
|
-
|
|
131
|
-
changes: result.changes,
|
|
132
|
-
};
|
|
133
|
-
this.reset();
|
|
134
|
-
return out;
|
|
154
|
+
return { changes: result.changes }
|
|
135
155
|
}
|
|
136
156
|
|
|
137
157
|
/**
|
|
138
|
-
* Increment a numeric column by a specified amount
|
|
139
|
-
* This is more efficient than fetching, calculating, and updating.
|
|
158
|
+
* Increment a numeric column by a specified amount
|
|
140
159
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* @
|
|
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)
|
|
144
165
|
*/
|
|
145
166
|
increment(column: keyof T, amount = 1): UpdateResult {
|
|
146
|
-
this.requireWhereClause("INCREMENT")
|
|
167
|
+
this.requireWhereClause("INCREMENT")
|
|
168
|
+
|
|
169
|
+
const [whereClause, whereParams] = this.buildWhereClause()
|
|
170
|
+
const quotedColumn = quoteIdentifier(String(column))
|
|
147
171
|
|
|
148
|
-
const
|
|
149
|
-
const
|
|
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)
|
|
150
176
|
|
|
151
177
|
const result = this.getDb()
|
|
152
178
|
.prepare(query)
|
|
153
|
-
.run(
|
|
179
|
+
.run(...params)
|
|
180
|
+
|
|
181
|
+
this.updateLog.result("INCREMENT", result.changes)
|
|
182
|
+
this.reset()
|
|
154
183
|
|
|
155
|
-
|
|
156
|
-
changes: result.changes,
|
|
157
|
-
};
|
|
158
|
-
this.reset();
|
|
159
|
-
return out;
|
|
184
|
+
return { changes: result.changes }
|
|
160
185
|
}
|
|
161
186
|
|
|
162
187
|
/**
|
|
163
|
-
* Decrement a numeric column by a specified amount
|
|
164
|
-
* This is more efficient than fetching, calculating, and updating.
|
|
188
|
+
* Decrement a numeric column by a specified amount
|
|
165
189
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* @
|
|
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)
|
|
169
195
|
*/
|
|
170
196
|
decrement(column: keyof T, amount = 1): UpdateResult {
|
|
171
|
-
|
|
172
|
-
this.reset();
|
|
173
|
-
return out;
|
|
197
|
+
return this.increment(column, -amount)
|
|
174
198
|
}
|
|
175
199
|
|
|
176
200
|
/**
|
|
177
|
-
* Update and
|
|
178
|
-
*
|
|
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.
|
|
179
205
|
*
|
|
180
|
-
* @
|
|
181
|
-
*
|
|
206
|
+
* @example
|
|
207
|
+
* const updatedRows = table.where({ active: false }).updateAndGet({ deleted: true })
|
|
182
208
|
*/
|
|
183
209
|
updateAndGet(data: Partial<T>): T[] {
|
|
184
|
-
//
|
|
185
|
-
const
|
|
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
|
|
186
222
|
|
|
187
223
|
// Perform the update
|
|
188
|
-
const updateResult = this.update(data)
|
|
224
|
+
const updateResult = this.update(data)
|
|
189
225
|
|
|
190
226
|
if (updateResult.changes === 0) {
|
|
191
|
-
|
|
192
|
-
return [];
|
|
227
|
+
return []
|
|
193
228
|
}
|
|
194
229
|
|
|
195
|
-
|
|
196
|
-
// In a real implementation, you might want to re-fetch the updated rows
|
|
197
|
-
const out = rowsToUpdate;
|
|
198
|
-
this.reset();
|
|
199
|
-
return out;
|
|
230
|
+
return rowsToUpdate
|
|
200
231
|
}
|
|
201
232
|
|
|
202
233
|
/**
|
|
203
|
-
* Batch update multiple rows with different values
|
|
204
|
-
* This is more efficient than individual updates when updating many rows.
|
|
234
|
+
* Batch update multiple rows with different values
|
|
205
235
|
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
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
|
+
* ])
|
|
208
244
|
*/
|
|
209
|
-
updateBatch(
|
|
210
|
-
updates: Array<{ where: Partial<T>; data: Partial<T> }>,
|
|
211
|
-
): UpdateResult {
|
|
245
|
+
updateBatch(updates: Array<{ where: Partial<T>; data: Partial<T> }>): UpdateResult {
|
|
212
246
|
if (!Array.isArray(updates) || updates.length === 0) {
|
|
213
|
-
|
|
214
|
-
throw new Error("updateBatch: updates must be a non-empty array")
|
|
247
|
+
this.reset()
|
|
248
|
+
throw new Error("updateBatch: updates must be a non-empty array")
|
|
215
249
|
}
|
|
216
250
|
|
|
217
|
-
const db = this.getDb()
|
|
251
|
+
const db = this.getDb()
|
|
218
252
|
|
|
219
|
-
// Use a transaction for batch operations
|
|
220
253
|
const transaction = db.transaction(
|
|
221
254
|
(updatesToProcess: Array<{ where: Partial<T>; data: Partial<T> }>) => {
|
|
222
|
-
let totalChanges = 0
|
|
255
|
+
let totalChanges = 0
|
|
223
256
|
|
|
224
257
|
for (const { where: whereData, data } of updatesToProcess) {
|
|
225
|
-
// Transform data
|
|
226
|
-
const
|
|
227
|
-
const updateColumns = Object.keys(
|
|
258
|
+
// Transform data
|
|
259
|
+
const transformedData = this.transformRowToDb(data)
|
|
260
|
+
const updateColumns = Object.keys(transformedData)
|
|
261
|
+
|
|
228
262
|
if (updateColumns.length === 0) {
|
|
229
|
-
continue
|
|
263
|
+
continue // Skip empty updates
|
|
230
264
|
}
|
|
231
265
|
|
|
232
|
-
// Build WHERE conditions
|
|
233
|
-
const whereConditions: string[] = []
|
|
234
|
-
const whereParams: SQLQueryBindings[] = []
|
|
266
|
+
// Build WHERE conditions
|
|
267
|
+
const whereConditions: string[] = []
|
|
268
|
+
const whereParams: SQLQueryBindings[] = []
|
|
235
269
|
|
|
236
270
|
for (const [column, value] of Object.entries(whereData)) {
|
|
237
271
|
if (value === null || value === undefined) {
|
|
238
|
-
whereConditions.push(`${
|
|
272
|
+
whereConditions.push(`${quoteIdentifier(column)} IS NULL`)
|
|
239
273
|
} else {
|
|
240
|
-
whereConditions.push(`${
|
|
241
|
-
whereParams.push(value)
|
|
274
|
+
whereConditions.push(`${quoteIdentifier(column)} = ?`)
|
|
275
|
+
whereParams.push(value as SQLQueryBindings)
|
|
242
276
|
}
|
|
243
277
|
}
|
|
244
278
|
|
|
245
279
|
if (whereConditions.length === 0) {
|
|
246
|
-
|
|
247
|
-
throw new Error(
|
|
248
|
-
"updateBatch: each update must have WHERE conditions",
|
|
249
|
-
);
|
|
280
|
+
throw new Error("updateBatch: each update must have WHERE conditions")
|
|
250
281
|
}
|
|
251
282
|
|
|
252
283
|
// Build UPDATE statement
|
|
253
|
-
const setClause = updateColumns
|
|
254
|
-
|
|
255
|
-
|
|
284
|
+
const setClause = buildSetClause(updateColumns)
|
|
285
|
+
const whereClause = ` WHERE ${whereConditions.join(" AND ")}`
|
|
286
|
+
const query = `UPDATE ${quoteIdentifier(this.getTableName())} SET ${setClause}${whereClause}`
|
|
256
287
|
|
|
257
|
-
const
|
|
258
|
-
const
|
|
288
|
+
const updateValues = updateColumns.map((col) => transformedData[col] ?? null)
|
|
289
|
+
const allParams = [...updateValues, ...whereParams] as SQLQueryBindings[]
|
|
259
290
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
);
|
|
263
|
-
const allParams = [...updateValues, ...whereParams];
|
|
264
|
-
|
|
265
|
-
const result = db.prepare(query).run(...allParams);
|
|
266
|
-
totalChanges += result.changes;
|
|
291
|
+
const result = db.prepare(query).run(...allParams)
|
|
292
|
+
totalChanges += result.changes
|
|
267
293
|
}
|
|
268
294
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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()
|
|
273
305
|
|
|
274
|
-
|
|
275
|
-
this.reset();
|
|
276
|
-
return out;
|
|
306
|
+
return result
|
|
277
307
|
}
|
|
278
308
|
}
|